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:
Bruno Quaresma
2025-10-22 10:16:53 -03:00
committed by GitHub
parent 1230cacf78
commit aa689cbb39
2 changed files with 68 additions and 25 deletions
+15 -9
View File
@@ -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}
/>
);
};
+53 -16
View File
@@ -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>
);
};