From 509e89d5c4549d9a4c317d6a2b0790f3499db940 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Mon, 30 Mar 2026 14:51:14 +0200 Subject: [PATCH] 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 " 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 --- .../AgentsPage/AgentChatPage.stories.tsx | 6 +-- .../tools/InlineDesktopPreview.tsx | 26 +++++++----- .../ChatElements/tools/SubagentTool.tsx | 41 +++++++++++++------ .../ChatElements/tools/Tool.stories.tsx | 6 +-- .../components/ChatElements/tools/Tool.tsx | 12 ++++++ 5 files changed, 63 insertions(+), 28 deletions(-) diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx index d541491887..619a796822 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx @@ -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(); }); }, }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx index 9f0191be3d..775e13dc84 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx @@ -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) => ( -
+
{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 && ( )}
@@ -97,7 +103,7 @@ export const InlineDesktopPreview: React.FC<{ return wrapWithOverlay(
, @@ -108,7 +114,7 @@ export const InlineDesktopPreview: React.FC<{ return wrapWithOverlay(
Desktop disconnected. Reconnecting…
, @@ -119,7 +125,7 @@ export const InlineDesktopPreview: React.FC<{ return wrapWithOverlay(
Could not connect to desktop.
, @@ -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 }} />, ); }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx index 15ca0343a7..2c82085b69 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx @@ -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 ; } if (toolStatus === "running") { + if (showDesktopPreview) { + return ( + + ); + } return ( ); @@ -155,18 +163,27 @@ export const SubagentTool: React.FC<{ isError={isError} isTimeout={isTimeout} variant={variant} - /> + showDesktopPreview={showDesktopPreview} + />{" "} - {SUBAGENT_VERBS[toolName]?.[ - isTimeout - ? "timeout" - : toolStatus === "completed" - ? "completed" - : toolStatus === "error" - ? "error" - : "running" - ] ?? ""} - {title} + {showDesktopPreview && toolStatus === "running" ? ( + + Using the computer... + + ) : ( + <> + {SUBAGENT_VERBS[toolName]?.[ + isTimeout + ? "timeout" + : toolStatus === "completed" + ? "completed" + : toolStatus === "error" + ? "error" + : "running" + ] ?? ""} + {title} + + )} {chatId && ( {showDesktopPreview && desktopChatId && ( -
+
{ 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). diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx index 846eaf14bf..b97060341b 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx @@ -333,6 +333,18 @@ const SubagentRenderer: FC = ({ (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"