mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
feat: add terminal in the task page (#20396)
**Demo:** <img width="1624" height="967" alt="Screenshot 2025-10-21 at 10 45 24" src="https://github.com/user-attachments/assets/b0ae724f-055a-4b13-b2a6-f11f4432de9b" /> Closes https://github.com/coder/internal/issues/1077
This commit is contained in:
@@ -10,7 +10,7 @@ import { useProxy } from "contexts/ProxyContext";
|
||||
import { EllipsisVertical, ExternalLinkIcon, HouseIcon } from "lucide-react";
|
||||
import { useAppLink } from "modules/apps/useAppLink";
|
||||
import type { Task, WorkspaceAppWithAgent } from "modules/tasks/tasks";
|
||||
import { type FC, useRef } from "react";
|
||||
import { type FC, type HTMLProps, useRef } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { cn } from "utils/cn";
|
||||
import { TaskWildcardWarning } from "./TaskWildcardWarning";
|
||||
@@ -85,14 +85,7 @@ export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
|
||||
)}
|
||||
|
||||
{app.health === "healthy" || app.health === "disabled" ? (
|
||||
<iframe
|
||||
ref={frameRef}
|
||||
src={link.href}
|
||||
title={link.label}
|
||||
loading="eager"
|
||||
className={"w-full h-full border-0"}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
/>
|
||||
<TaskIframe ref={frameRef} src={link.href} title={link.label} />
|
||||
) : app.health === "unhealthy" ? (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center p-4">
|
||||
<h3 className="m-0 font-medium text-content-primary text-base text-center">
|
||||
@@ -145,3 +138,16 @@ export const TaskAppIFrame: FC<TaskAppIFrameProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type TaskIframeProps = HTMLProps<HTMLIFrameElement>;
|
||||
|
||||
export const TaskIframe: FC<TaskIframeProps> = ({ className, ...props }) => {
|
||||
return (
|
||||
<iframe
|
||||
loading="eager"
|
||||
className={cn("w-full h-full border-0", className)}
|
||||
allow="clipboard-read; clipboard-write"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,24 +9,26 @@ import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
|
||||
import { Link } from "components/Link/Link";
|
||||
import { ScrollArea, ScrollBar } from "components/ScrollArea/ScrollArea";
|
||||
import { ChevronDownIcon, LayoutGridIcon } from "lucide-react";
|
||||
import { ChevronDownIcon, LayoutGridIcon, TerminalIcon } from "lucide-react";
|
||||
import { getTerminalHref } from "modules/apps/apps";
|
||||
import { useAppLink } from "modules/apps/useAppLink";
|
||||
import {
|
||||
getTaskApps,
|
||||
type Task,
|
||||
type WorkspaceAppWithAgent,
|
||||
} from "modules/tasks/tasks";
|
||||
import type React from "react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link as RouterLink } from "react-router";
|
||||
import { type LinkProps, Link as RouterLink } from "react-router";
|
||||
import { cn } from "utils/cn";
|
||||
import { docs } from "utils/docs";
|
||||
import { TaskAppIFrame } from "./TaskAppIframe";
|
||||
import { TaskAppIFrame, TaskIframe } from "./TaskAppIframe";
|
||||
|
||||
type TaskAppsProps = {
|
||||
task: Task;
|
||||
};
|
||||
|
||||
const TERMINAL_TAB_ID = "terminal";
|
||||
|
||||
export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
|
||||
const apps = getTaskApps(task).filter(
|
||||
// The Chat UI app will be displayed in the sidebar, so we don't want to
|
||||
@@ -39,6 +41,13 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
|
||||
const [activeAppId, setActiveAppId] = useState(embeddedApps.at(0)?.id);
|
||||
const hasAvailableAppsToDisplay =
|
||||
embeddedApps.length > 0 || externalApps.length > 0;
|
||||
const taskAgent = apps.at(0)?.agent;
|
||||
const terminalHref = getTerminalHref({
|
||||
username: task.workspace.owner_name,
|
||||
workspace: task.workspace.name,
|
||||
agent: taskAgent?.name,
|
||||
});
|
||||
const isTerminalActive = activeAppId === TERMINAL_TAB_ID;
|
||||
|
||||
return (
|
||||
<main className="flex flex-col h-full">
|
||||
@@ -58,6 +67,17 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
<TaskTab
|
||||
to={terminalHref}
|
||||
active={isTerminalActive}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setActiveAppId(TERMINAL_TAB_ID);
|
||||
}}
|
||||
>
|
||||
<TerminalIcon />
|
||||
Terminal
|
||||
</TaskTab>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" className="h-2" />
|
||||
</ScrollArea>
|
||||
@@ -78,6 +98,14 @@ export const TaskApps: FC<TaskAppsProps> = ({ task }) => {
|
||||
task={task}
|
||||
/>
|
||||
))}
|
||||
|
||||
<TaskIframe
|
||||
src={terminalHref}
|
||||
title="Terminal"
|
||||
className={cn({
|
||||
hidden: !isTerminalActive,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mx-auto my-auto flex flex-col items-center">
|
||||
@@ -161,11 +189,30 @@ const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
|
||||
workspace: task.workspace,
|
||||
});
|
||||
|
||||
return (
|
||||
<TaskTab active={active} to={link.href} onClick={onClick}>
|
||||
{app.icon ? <ExternalImage src={app.icon} /> : <LayoutGridIcon />}
|
||||
{link.label}
|
||||
{app.health === "unhealthy" && (
|
||||
<InfoTooltip
|
||||
title="This app is unhealthy."
|
||||
message="The health check failed."
|
||||
type="warning"
|
||||
/>
|
||||
)}
|
||||
</TaskTab>
|
||||
);
|
||||
};
|
||||
|
||||
type TaskTabProps = LinkProps & {
|
||||
active: boolean;
|
||||
};
|
||||
|
||||
const TaskTab: FC<TaskTabProps> = ({ active, ...routerLinkProps }) => {
|
||||
return (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
key={app.id}
|
||||
asChild
|
||||
className={cn([
|
||||
"px-3",
|
||||
@@ -176,17 +223,7 @@ const TaskAppTab: FC<TaskAppTabProps> = ({ task, app, active, onClick }) => {
|
||||
{ "opacity-75 hover:opacity-100": !active },
|
||||
])}
|
||||
>
|
||||
<RouterLink to={link.href} onClick={onClick}>
|
||||
{app.icon ? <ExternalImage src={app.icon} /> : <LayoutGridIcon />}
|
||||
{link.label}
|
||||
{app.health === "unhealthy" && (
|
||||
<InfoTooltip
|
||||
title="This app is unhealthy."
|
||||
message="The health check failed."
|
||||
type="warning"
|
||||
/>
|
||||
)}
|
||||
</RouterLink>
|
||||
<RouterLink {...routerLinkProps} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user