mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user