feat(site): refactor the wait for computer use subagent card (#23780)

Right now, when an agent is waiting for the computer use subagent, it
shows a VNC preview of the desktop that spans the full width of the
chat. It also displays a standard "waiting for <subagent name>" header
above it. See https://github.com/coder/coder/pull/23684 for a recording.

This PR refactors that preview to be smaller and changes the header to a
shimmering "Using the computer" label.


https://github.com/user-attachments/assets/0db5b4dc-6899-419b-bf7f-eb0de05722f1
This commit is contained in:
Hugo Dutka
2026-03-30 14:51:14 +02:00
committed by GitHub
parent 378f11d6dc
commit 509e89d5c4
5 changed files with 63 additions and 28 deletions
@@ -1242,10 +1242,10 @@ export const WithWaitAgentComputerUseVNC: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// The wait_agent card should show "Waiting for" (running state)
// rendered via SubagentTool with VNC preview.
// The wait_agent card should show "Using the computer..." (running
// state) rendered via SubagentTool with VNC preview.
await waitFor(() => {
expect(canvas.getByText(/Waiting for/)).toBeInTheDocument();
expect(canvas.getByText(/Using the computer/)).toBeInTheDocument();
});
},
};
@@ -1,4 +1,4 @@
import { ExternalLinkIcon } from "lucide-react";
import { PanelRightOpenIcon } from "lucide-react";
import type React from "react";
import { useEffect, useRef, useState } from "react";
import { Spinner } from "#/components/Spinner/Spinner";
@@ -10,6 +10,9 @@ import {
/** Default aspect ratio used before the remote framebuffer size is known. */
const DEFAULT_ASPECT = "16 / 9";
/** Fixed pixel height for the compact preview thumbnail. */
const PREVIEW_HEIGHT = 128;
/**
* Non-interactive inline VNC desktop preview. The noVNC canvas is
* blocked from receiving pointer/keyboard events so it acts as a
@@ -75,19 +78,22 @@ export const InlineDesktopPreview: React.FC<{
}, [status]);
const wrapWithOverlay = (children: React.ReactNode) => (
<div className="group relative">
<div className="group/preview relative">
{children}
{/* Transparent overlay — dims the preview on hover and shows
an external-link icon so it's clear clicking opens the
a "View desktop" label so it's clear clicking opens the
sidebar desktop tab. */}
{onClick && (
<button
type="button"
onClick={onClick}
aria-label="Open desktop tab"
className="absolute inset-0 z-10 flex cursor-pointer items-center justify-center border-0 bg-black/0 p-0 transition-colors group-hover:bg-black/50"
className="absolute inset-0 z-10 flex cursor-pointer items-center justify-center gap-1.5 border-0 bg-black/0 p-0 transition-colors group-hover/preview:bg-black/50"
>
<ExternalLinkIcon className="h-6 w-6 text-white opacity-0 drop-shadow-md transition-opacity group-hover:opacity-100" />
<span className="text-sm font-medium text-white opacity-0 drop-shadow-md transition-opacity group-hover/preview:opacity-100">
View desktop
</span>
<PanelRightOpenIcon className="h-4 w-4 text-white opacity-0 drop-shadow-md transition-opacity group-hover/preview:opacity-100" />
</button>
)}
</div>
@@ -97,7 +103,7 @@ export const InlineDesktopPreview: React.FC<{
return wrapWithOverlay(
<div
className="flex items-center justify-center text-content-secondary"
style={{ aspectRatio: DEFAULT_ASPECT }}
style={{ aspectRatio: DEFAULT_ASPECT, height: PREVIEW_HEIGHT }}
>
<Spinner loading className="h-5 w-5" />
</div>,
@@ -108,7 +114,7 @@ export const InlineDesktopPreview: React.FC<{
return wrapWithOverlay(
<div
className="flex items-center justify-center text-xs text-content-secondary"
style={{ aspectRatio }}
style={{ aspectRatio, height: PREVIEW_HEIGHT }}
>
Desktop disconnected. Reconnecting…
</div>,
@@ -119,7 +125,7 @@ export const InlineDesktopPreview: React.FC<{
return wrapWithOverlay(
<div
className="flex items-center justify-center text-xs text-content-secondary"
style={{ aspectRatio: DEFAULT_ASPECT }}
style={{ aspectRatio: DEFAULT_ASPECT, height: PREVIEW_HEIGHT }}
>
Could not connect to desktop.
</div>,
@@ -134,8 +140,8 @@ export const InlineDesktopPreview: React.FC<{
containerRef.current = el;
if (el) attach(el);
}}
className="pointer-events-none w-full"
style={{ aspectRatio }}
className="pointer-events-none"
style={{ aspectRatio, height: PREVIEW_HEIGHT }}
/>,
);
};
@@ -13,6 +13,7 @@ import { Link } from "react-router";
import { ScrollArea } from "#/components/ScrollArea/ScrollArea";
import { cn } from "#/utils/cn";
import { Response } from "../Response";
import { Shimmer } from "../Shimmer";
import { useDesktopPanel } from "./DesktopPanelContext";
import { InlineDesktopPreview } from "./InlineDesktopPreview";
import {
@@ -70,12 +71,14 @@ const SubagentStatusIcon: React.FC<{
isError: boolean;
isTimeout: boolean;
variant?: "default" | "computer-use";
showDesktopPreview?: boolean;
}> = ({
subagentStatus,
toolStatus,
isError,
isTimeout,
variant = "default",
showDesktopPreview = false,
}) => {
const subagentCompleted = isSubagentSuccessStatus(subagentStatus);
const DefaultIcon = variant === "computer-use" ? MonitorIcon : BotIcon;
@@ -86,6 +89,11 @@ const SubagentStatusIcon: React.FC<{
return <CircleXIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
}
if (toolStatus === "running") {
if (showDesktopPreview) {
return (
<MonitorIcon className="h-4 w-4 shrink-0 text-content-secondary" />
);
}
return (
<LoaderIcon className="h-4 w-4 shrink-0 animate-spin motion-reduce:animate-none text-content-link" />
);
@@ -155,18 +163,27 @@ export const SubagentTool: React.FC<{
isError={isError}
isTimeout={isTimeout}
variant={variant}
/>
showDesktopPreview={showDesktopPreview}
/>{" "}
<span className="min-w-0 flex-1 truncate text-sm text-content-secondary">
{SUBAGENT_VERBS[toolName]?.[
isTimeout
? "timeout"
: toolStatus === "completed"
? "completed"
: toolStatus === "error"
? "error"
: "running"
] ?? ""}
<span className="text-content-secondary opacity-60">{title}</span>
{showDesktopPreview && toolStatus === "running" ? (
<Shimmer as="span" className="text-sm">
Using the computer...
</Shimmer>
) : (
<>
{SUBAGENT_VERBS[toolName]?.[
isTimeout
? "timeout"
: toolStatus === "completed"
? "completed"
: toolStatus === "error"
? "error"
: "running"
] ?? ""}
<span className="text-content-secondary opacity-60">{title}</span>
</>
)}
{chatId && (
<Link
to={`/agents/${chatId}`}
@@ -194,7 +211,7 @@ export const SubagentTool: React.FC<{
</button>
{showDesktopPreview && desktopChatId && (
<div className="mt-1.5 overflow-hidden rounded-lg border border-solid border-border-default">
<div className="mt-1.5 w-fit overflow-hidden rounded-lg border border-solid border-border-default">
<InlineDesktopPreview
chatId={desktopChatId}
onClick={onOpenDesktop}
@@ -1244,9 +1244,9 @@ export const WaitAgentComputerUseRunning: Story = {
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText(/Waiting for/)).toBeInTheDocument();
// Running state shows the spinner icon.
expect(canvasElement.querySelector(".lucide-loader")).not.toBeNull();
expect(canvas.getByText(/Using the computer/)).toBeInTheDocument();
// Running state shows the monitor icon instead of a spinner.
expect(canvasElement.querySelector(".lucide-monitor")).not.toBeNull();
// The VNC preview container should mount (the connection will
// stay in "connecting" state without a real WebSocket, which
// is expected — we only verify the container renders).
@@ -333,6 +333,18 @@ const SubagentRenderer: FC<ToolRendererProps> = ({
(resultStr.toLowerCase().includes("timed out") ||
errorStr.toLowerCase().includes("timed out"));
// Postpone rendering wait_agent / message_agent until the
// chat_id has been parsed from the streaming args. Without it
// we can't determine variant or title, which causes a brief
// flash of the generic "Waiting for Sub-agent" text.
if (
!chatId &&
status === "running" &&
(name === "wait_agent" || name === "message_agent")
) {
return null;
}
const variant =
name === "spawn_computer_use_agent" || computerUseSubagentIds?.has(chatId)
? "computer-use"