diff --git a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx index b1264c3e18..2037aa1903 100644 --- a/site/src/pages/AgentsPage/AgentChatPage.stories.tsx +++ b/site/src/pages/AgentsPage/AgentChatPage.stories.tsx @@ -201,6 +201,507 @@ const buildQueries = ( ]; }; +// --------------------------------------------------------------------------- +// Every-tool showcase: a single completed assistant turn that exercises +// every tool renderer registered in Tool.tsx, plus the SubagentRenderer +// variants and the generic MCP fallback. Used by WithEveryTool +// so the page demonstrates every tool card type at once. +// +// The generated `ChatToolCallPart`/`ChatToolResultPart` types declare +// `args`/`result` as `Record`, but the runtime accepts +// arbitrary JSON. We cast through `unknown` here so the showcase can +// use realistic numbers, booleans, arrays, and nested objects without +// stringifying every field. +// --------------------------------------------------------------------------- + +const EVERY_TOOL_ASSISTANT_TURN = { + id: 4, + chat_id: CHAT_ID, + created_at: "2026-02-18T00:00:04.000Z", + role: "assistant", + content: [ + { + type: "text", + text: "Here is a recap of every tool I have at my disposal, exercised against this workspace so each card type renders.", + }, + + // execute -- shell command output + { + type: "tool-call", + tool_call_id: "every-execute", + tool_name: "execute", + args: { command: "go test ./coderd/httpmw/..." }, + }, + { + type: "tool-result", + tool_call_id: "every-execute", + tool_name: "execute", + result: { + output: [ + "ok \tgithub.com/coder/coder/coderd/httpmw\t1.842s", + "ok \tgithub.com/coder/coder/coderd/httpmw/auth\t0.612s", + ].join("\n"), + exit_code: 0, + }, + }, + + // process_output -- background process output + { + type: "tool-call", + tool_call_id: "every-process-output", + tool_name: "process_output", + args: { process_id: "proc-42" }, + }, + { + type: "tool-result", + tool_call_id: "every-process-output", + tool_name: "process_output", + result: { + output: [ + "[server] listening on :3000", + "[server] handled GET / 200 in 4ms", + "[server] handled GET /api/v2/users 200 in 12ms", + ].join("\n"), + exit_code: null, + }, + }, + + // process_signal -- signal sent to a background process + { + type: "tool-call", + tool_call_id: "every-process-signal", + tool_name: "process_signal", + args: { process_id: "proc-42", signal: "terminate" }, + }, + { + type: "tool-result", + tool_call_id: "every-process-signal", + tool_name: "process_signal", + result: { success: true, signal: "terminate" }, + }, + + // read_file -- completed file read with content viewer + { + type: "tool-call", + tool_call_id: "every-read-file", + tool_name: "read_file", + args: { path: "coderd/httpmw/apikey.go" }, + }, + { + type: "tool-result", + tool_call_id: "every-read-file", + tool_name: "read_file", + result: { + content: [ + "package httpmw", + "", + "// ExtractAPIKeyMW will be split into a dedicated auth", + "// package with three layers: transport, validation, authz.", + "func ExtractAPIKeyMW(/* ... */) {}", + ].join("\n"), + }, + }, + + // write_file -- completed file write with diff viewer + { + type: "tool-call", + tool_call_id: "every-write-file", + tool_name: "write_file", + args: { + path: "coderd/httpmw/auth/transport.go", + content: [ + "package auth", + "", + "// ExtractCredentials reads credentials from the request.", + "func ExtractCredentials(r *http.Request) (Credential, error) {", + " return Credential{}, nil", + "}", + ].join("\n"), + }, + }, + { + type: "tool-result", + tool_call_id: "every-write-file", + tool_name: "write_file", + result: { content: "wrote 6 lines" }, + }, + + // edit_files -- completed multi-file edit + { + type: "tool-call", + tool_call_id: "every-edit-files", + tool_name: "edit_files", + args: { + files: [ + { + path: "coderd/coderd.go", + edits: [ + { + search: "httpmw.ExtractAPIKeyMW(opts)", + replace: "auth.Middleware(opts)", + }, + ], + }, + ], + }, + }, + { + type: "tool-result", + tool_call_id: "every-edit-files", + tool_name: "edit_files", + result: { applied: 1 }, + }, + + // list_templates -- completed template listing + { + type: "tool-call", + tool_call_id: "every-list-templates", + tool_name: "list_templates", + args: {}, + }, + { + type: "tool-result", + tool_call_id: "every-list-templates", + tool_name: "list_templates", + result: { + templates: [ + { + id: "tpl-go", + name: "go-template", + display_name: "Go Development", + description: "Workspace for Go services with VS Code.", + }, + { + id: "tpl-py", + name: "python-template", + description: "Python development environment.", + }, + ], + count: 2, + }, + }, + + // read_template -- completed single-template read + { + type: "tool-call", + tool_call_id: "every-read-template", + tool_name: "read_template", + args: { name: "go-template" }, + }, + { + type: "tool-result", + tool_call_id: "every-read-template", + tool_name: "read_template", + result: { + template: { name: "go-template", display_name: "Go Development" }, + }, + }, + + // read_skill -- completed skill load + { + type: "tool-call", + tool_call_id: "every-read-skill", + tool_name: "read_skill", + args: { name: "deep-review" }, + }, + { + type: "tool-result", + tool_call_id: "every-read-skill", + tool_name: "read_skill", + result: { + name: "deep-review", + body: [ + "## Deep Review Skill", + "", + "Review the code changes thoroughly:", + "", + "1. Check for correctness", + "2. Verify tests cover the new branches", + "3. Ensure style consistency", + ].join("\n"), + files: ["roles/security-reviewer.md"], + }, + }, + + // read_skill_file -- completed skill file fetch + { + type: "tool-call", + tool_call_id: "every-read-skill-file", + tool_name: "read_skill_file", + args: { name: "deep-review", path: "roles/security-reviewer.md" }, + }, + { + type: "tool-result", + tool_call_id: "every-read-skill-file", + tool_name: "read_skill_file", + result: { + content: [ + "# Security Reviewer Role", + "", + "Focus on authentication, authorization, and input validation.", + ].join("\n"), + }, + }, + + // chat_summarized -- compaction summary card + { + type: "tool-call", + tool_call_id: "every-summarized", + tool_name: "chat_summarized", + args: {}, + }, + { + type: "tool-result", + tool_call_id: "every-summarized", + tool_name: "chat_summarized", + result: { + summary: [ + "Summarized the previous turns to free up context: the agent", + "split the auth middleware into transport and validation layers", + "and is about to wire up the call sites.", + ].join(" "), + }, + }, + + // ask_user_question -- completed clarification + { + type: "tool-call", + tool_call_id: "every-ask-user", + tool_name: "ask_user_question", + args: { + questions: [ + { + header: "Migration approach", + question: + "How should we structure the database migration for the auth split?", + options: [ + { + label: "Single migration", + description: "One migration with all changes.", + }, + { + label: "Incremental migrations", + description: "Multiple sequential migrations.", + }, + ], + }, + ], + }, + }, + { + type: "tool-result", + tool_call_id: "every-ask-user", + tool_name: "ask_user_question", + result: { questions: [{ answer: "Incremental migrations" }] }, + }, + + // propose_plan -- proposed plan with content + { + type: "tool-call", + tool_call_id: "every-propose-plan", + tool_name: "propose_plan", + args: { path: "/home/coder/.coder/plans/AUTH_SPLIT.md" }, + }, + { + type: "tool-result", + tool_call_id: "every-propose-plan", + tool_name: "propose_plan", + result: { + file_id: "plan-file-1", + content: [ + "# Auth Split Plan", + "", + "1. Carve transport, validation, and authz packages.", + "2. Update call sites in coderd and enterprise/coderd.", + "3. Add an incremental migration for session lookups.", + "4. Refresh the changelog and run the suite.", + ].join("\n"), + }, + }, + + // computer -- screenshot tool result with text fallback + { + type: "tool-call", + tool_call_id: "every-computer", + tool_name: "computer", + args: { action: "screenshot" }, + }, + { + type: "tool-result", + tool_call_id: "every-computer", + tool_name: "computer", + result: { + data: "", + text: "Screen resolution: 1920x1080\nActive window: Terminal", + mime_type: "image/png", + }, + }, + + // attach_file -- generic renderer with explicit attach label + { + type: "tool-call", + tool_call_id: "every-attach-file", + tool_name: "attach_file", + args: { path: "docs/runbooks/auth-split.md" }, + }, + { + type: "tool-result", + tool_call_id: "every-attach-file", + tool_name: "attach_file", + result: {}, + }, + + // generic / MCP fallback -- unknown tool name with no server + { + type: "tool-call", + tool_call_id: "every-generic", + tool_name: "web_search", + args: { query: "OAuth2 token rotation strategies" }, + }, + { + type: "tool-result", + tool_call_id: "every-generic", + tool_name: "web_search", + result: { + results: [ + { + title: "OAuth 2.0 RFC 6749", + url: "https://datatracker.ietf.org/doc/html/rfc6749", + }, + ], + }, + }, + + // spawn_agent (general subagent variant) + { + type: "tool-call", + tool_call_id: "every-spawn-general", + tool_name: "spawn_agent", + args: { + type: "general", + title: "Workspace diagnostics", + prompt: "Collect logs and summarize why startup failed.", + }, + }, + { + type: "tool-result", + tool_call_id: "every-spawn-general", + tool_name: "spawn_agent", + result: { + chat_id: "every-general-child", + type: "general", + title: "Workspace diagnostics", + status: "completed", + duration_ms: 3200, + }, + }, + + // spawn_explore_agent (explore subagent variant) + { + type: "tool-call", + tool_call_id: "every-spawn-explore", + tool_name: "spawn_explore_agent", + args: { + prompt: "Read the repo and summarize the auth flow.", + }, + }, + { + type: "tool-result", + tool_call_id: "every-spawn-explore", + tool_name: "spawn_explore_agent", + result: { + chat_id: "every-explore-child", + type: "explore", + status: "completed", + duration_ms: 4100, + }, + }, + + // spawn_computer_use_agent (desktop subagent variant) + { + type: "tool-call", + tool_call_id: "every-spawn-desktop", + tool_name: "spawn_computer_use_agent", + args: { + title: "Visual regression check", + prompt: "Open the dashboard and look for visual regressions.", + }, + }, + { + type: "tool-result", + tool_call_id: "every-spawn-desktop", + tool_name: "spawn_computer_use_agent", + result: { + chat_id: "every-desktop-child", + type: "computer_use", + title: "Visual regression check", + status: "completed", + duration_ms: 12400, + }, + }, + + // message_agent -- send a follow-up to a subagent + { + type: "tool-call", + tool_call_id: "every-message-agent", + tool_name: "message_agent", + args: { chat_id: "every-general-child", message: "continue" }, + }, + { + type: "tool-result", + tool_call_id: "every-message-agent", + tool_name: "message_agent", + result: { + chat_id: "every-general-child", + type: "general", + status: "running", + }, + }, + + // wait_agent -- wait for a subagent to finish + { + type: "tool-call", + tool_call_id: "every-wait-agent", + tool_name: "wait_agent", + args: { chat_id: "every-general-child" }, + }, + { + type: "tool-result", + tool_call_id: "every-wait-agent", + tool_name: "wait_agent", + result: { + chat_id: "every-general-child", + type: "general", + status: "completed", + report: "Diagnostics finished; logs were uploaded.", + }, + }, + + // close_agent -- terminate a subagent + { + type: "tool-call", + tool_call_id: "every-close-agent", + tool_name: "close_agent", + args: { chat_id: "every-explore-child" }, + }, + { + type: "tool-result", + tool_call_id: "every-close-agent", + tool_name: "close_agent", + result: { + chat_id: "every-explore-child", + type: "explore", + status: "completed", + }, + }, + + { + type: "text", + text: "With every renderer exercised, I'll continue the auth split below. Note the streaming flurry of file tool calls in the next turn.", + }, + ], +} as unknown as TypesGen.ChatMessage; + // --------------------------------------------------------------------------- // Meta // --------------------------------------------------------------------------- @@ -1343,6 +1844,315 @@ export const StreamedReasoning: Story = { // This made the stories render empty chats and fail interaction // tests in both local and CI environments. +/** + * Live agent turn with streaming reasoning and a back-to-back flurry of + * in-progress file tool calls. The persisted history establishes context + * (an earlier completed read+write pair); the WebSocket then streams an + * assistant turn with reasoning followed by read/write/edit/read/write + * tool calls that intentionally have no matching tool-result parts, so + * each card stays in the "running" state and shows its loading spinner. + */ +export const WithEveryTool: Story = { + parameters: { + queries: buildQueries( + { + id: CHAT_ID, + ...baseChatFields, + title: "Refactoring the auth module", + status: "running", + }, + { + messages: [ + // -- Turn 1: user kicks off the task -- + { + id: 1, + chat_id: CHAT_ID, + created_at: "2026-02-18T00:00:01.000Z", + role: "user", + content: [ + { + type: "text", + text: "Refactor the auth module: split httpmw/apikey.go into a transport, validation, and authorization layer. Update the imports and add a CHANGELOG entry while you are at it.", + }, + ], + }, + // -- Turn 2: previous assistant turn (completed) -- + // Establishes that the agent already inspected and patched + // a couple of files before the streaming turn begins. + { + id: 2, + chat_id: CHAT_ID, + created_at: "2026-02-18T00:00:02.000Z", + role: "assistant", + content: [ + { + type: "reasoning", + text: "I'll start by reading the existing middleware to understand its responsibilities, then sketch out the three-layer split before touching anything else.", + }, + { + type: "text", + text: "Reading the existing middleware first so I can plan the split.", + }, + { + type: "tool-call", + tool_call_id: "call-read-apikey", + tool_name: "read_file", + args: { path: "coderd/httpmw/apikey.go" }, + }, + { + type: "tool-result", + tool_call_id: "call-read-apikey", + tool_name: "read_file", + result: { + content: [ + "package httpmw", + "", + "// ExtractAPIKeyMW does too many things: it parses", + "// the token, validates the signature, looks up the", + "// session, and authorizes the user. We will split", + "// these into transport, validation, and authz.", + "func ExtractAPIKeyMW(/* ... */) {}", + ].join("\n"), + }, + }, + { + type: "tool-call", + tool_call_id: "call-write-transport", + tool_name: "write_file", + args: { + path: "coderd/httpmw/auth/transport.go", + content: [ + "package auth", + "", + "// ExtractCredentials pulls the credential out of the", + "// HTTP request without doing any validation.", + "func ExtractCredentials(r *http.Request) (Credential, error) {", + ' if h := r.Header.Get("Authorization"); h != "" {', + " return Credential{Kind: KindBearer, Value: h}, nil", + " }", + ' if c, err := r.Cookie("coder_session_token"); err == nil {', + " return Credential{Kind: KindCookie, Value: c.Value}, nil", + " }", + " return Credential{}, ErrNoCredential", + "}", + ].join("\n"), + }, + }, + { + type: "tool-result", + tool_call_id: "call-write-transport", + tool_name: "write_file", + result: { content: "wrote 12 lines" }, + }, + { + type: "text", + text: "Transport layer is in place. Next I'll carve out validation and authorization, update the call sites, and bump the changelog.", + }, + ], + }, + // -- Turn 3: user asks for a tool-by-tool tour -- + { + id: 3, + chat_id: CHAT_ID, + created_at: "2026-02-18T00:00:03.000Z", + role: "user", + content: [ + { + type: "text", + text: "Before you keep going, can you take one quick pass and exercise every tool you have so I can confirm each one renders the way I expect?", + }, + ], + }, + // -- Turn 4: assistant runs every tool exactly once -- + EVERY_TOOL_ASSISTANT_TURN, + ], + queued_messages: [], + has_more: false, + }, + { diffUrl: undefined }, + ), + // The streaming turn arrives over the WebSocket. None of the + // tool-call parts have a matching tool-result, so each card + // renders in the "running" state with its spinner. + webSocket: { + "/chats/": [ + { + event: "message", + data: JSON.stringify([ + // Streaming reasoning, delivered in two deltas so the + // thinking block grows as the turn unfolds. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "reasoning", + text: "Now I'll pull in the validation file and start carving out the JWT and session checks. ", + }, + }, + }, + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "reasoning", + text: "After that I need to wire the new package into the existing call sites and refresh the changelog so reviewers can follow the split.", + }, + }, + }, + // Some streaming response text before the tool calls. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "text", + text: "Reading the validation helpers, then writing the new package, patching the consumers, and updating the changelog.", + }, + }, + }, + // 1) read_file - in progress. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "tool-call", + tool_call_id: "stream-read-validate", + tool_name: "read_file", + args_delta: JSON.stringify({ + path: "coderd/httpmw/validate.go", + }), + }, + }, + }, + // 2) write_file - in progress. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "tool-call", + tool_call_id: "stream-write-validation", + tool_name: "write_file", + args_delta: JSON.stringify({ + path: "coderd/httpmw/auth/validation.go", + content: [ + "package auth", + "", + "// Validate verifies the supplied credential and", + "// returns the resolved subject. It does not make", + "// authorization decisions.", + "func Validate(ctx context.Context, c Credential) (Subject, error) {", + " switch c.Kind {", + " case KindBearer:", + " return validateBearer(ctx, c.Value)", + " case KindCookie:", + " return validateSession(ctx, c.Value)", + " default:", + " return Subject{}, ErrUnsupportedCredential", + " }", + "}", + ].join("\n"), + }), + }, + }, + }, + // 3) edit_files - in progress (multi-file). + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "tool-call", + tool_call_id: "stream-edit-callsites", + tool_name: "edit_files", + args_delta: JSON.stringify({ + files: [ + { + path: "coderd/coderd.go", + edits: [ + { + search: "httpmw.ExtractAPIKeyMW(opts)", + replace: "auth.Middleware(opts)", + }, + ], + }, + { + path: "enterprise/coderd/coderd.go", + edits: [ + { + search: "httpmw.ExtractAPIKeyMW(opts)", + replace: "auth.Middleware(opts)", + }, + ], + }, + ], + }), + }, + }, + }, + // 4) read_file - in progress. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "tool-call", + tool_call_id: "stream-read-changelog", + tool_name: "read_file", + args_delta: JSON.stringify({ + path: "CHANGELOG.md", + }), + }, + }, + }, + // 5) write_file - in progress. + { + type: "message_part", + chat_id: CHAT_ID, + message_part: { + part: { + type: "tool-call", + tool_call_id: "stream-write-changelog", + tool_name: "write_file", + args_delta: JSON.stringify({ + path: "CHANGELOG.md", + content: [ + "# Changelog", + "", + "## Unreleased", + "", + "- Split the auth middleware into transport, validation,", + " and authorization layers under coderd/httpmw/auth.", + ].join("\n"), + }), + }, + }, + }, + ] satisfies TypesGen.ChatStreamEvent[]), + }, + ], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + // All five streamed tool calls should appear simultaneously. + // read_file, write_file, and edit_files all switch to a + // progressive label ("Reading" / "Writing" / "Editing") + // while running; the spinner conveys progress. + await waitFor(() => { + expect(canvas.getByText(/Reading validate\.go/)).toBeInTheDocument(); + expect(canvas.getByText(/Writing validation\.go/)).toBeInTheDocument(); + expect(canvas.getByText(/Editing 2 files/)).toBeInTheDocument(); + expect(canvas.getByText(/Reading CHANGELOG\.md/)).toBeInTheDocument(); + expect(canvas.getByText(/Writing CHANGELOG\.md/)).toBeInTheDocument(); + }); + }, +}; + /** wait_agent for a computer-use subagent renders the VNC preview card * (SubagentTool with computer-use variant) instead of the plain SubagentTool card. */ export const WithWaitAgentComputerUseVNC: Story = { diff --git a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx index 20fdf695c8..50fea4f2b9 100644 --- a/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx +++ b/site/src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.tsx @@ -147,52 +147,62 @@ const ReasoningDisclosure = memo<{ }, [displayTextLength, isPreviewConstrained]); return ( - setManualToggle(open)} - className="w-full" - > - - {isStreaming ? ( - - Thinking - - ) : ( - Thinking - )} - - - {hasText && ( - -
- - {displayText} - -
-
+
+ > + setManualToggle(open)} + className="w-full" + > + + {isStreaming ? ( + + Thinking + + ) : ( + Thinking + )} + + + {hasText && ( + +
+ + {displayText} + +
+
+ )} +
+
); }, ); diff --git a/site/src/pages/AgentsPage/components/ChatElements/Message.tsx b/site/src/pages/AgentsPage/components/ChatElements/Message.tsx index 2f56aaa975..c3d8ba35cf 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/Message.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/Message.tsx @@ -20,7 +20,7 @@ export const MessageContent = ({
= ({ id={id} value={value} /> - {label} -

+ + {label} + +

{description}

@@ -282,18 +288,20 @@ const QuestionStep: FC = ({ Question {questionIndex + 1} of {questionCount}

)} -
-

- {questionHeader} -

+
+
@@ -343,12 +351,19 @@ const AnsweredQuestionText: FC = ({ idPrefix, }) => { return ( -

- {getQuestionText(question)} -

+
+
); }; @@ -523,7 +538,7 @@ export const AskUserQuestionTool: FC = ({
= ({ aria-live="polite" className="flex items-center gap-1.5 py-0.5" > - + Asking for clarification... = ({ />
) : ( -

+

No questions available.

)} @@ -606,7 +621,7 @@ export const AskUserQuestionTool: FC = ({

Submitted answer

-

+

{displayedSubmittedResponseText || "No answer recorded."}

@@ -615,7 +630,7 @@ export const AskUserQuestionTool: FC = ({ {submitError && (
{submitError} @@ -670,7 +685,7 @@ export const AskUserQuestionTool: FC = ({ aria-live="polite" className="flex items-center gap-1.5 py-0.5" > - + Asking for clarification... - + {isRunning ? "Summarizing…" : "Summarized"} {isError && ( - + {errorMessage || "Failed to summarize conversation"} @@ -44,7 +43,7 @@ export const ChatSummarizedTool: React.FC<{ )} {isRunning && ( - + )} } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ComputerTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ComputerTool.tsx index 5d23a998e0..06761eecbf 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ComputerTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ComputerTool.tsx @@ -6,7 +6,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { ImageLightbox } from "../../ImageLightbox"; import { ToolCollapsible } from "./ToolCollapsible"; import type { ToolStatus } from "./utils"; @@ -40,13 +39,13 @@ export const ComputerTool: React.FC<{ defaultExpanded={hasImage} header={ <> - + {isRunning ? "Taking screenshot…" : "Screenshot"} {isError && ( - + {errorMessage || "Failed to take screenshot"} @@ -54,7 +53,7 @@ export const ComputerTool: React.FC<{ )} {isRunning && ( - + )} } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/CreateWorkspaceTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/CreateWorkspaceTool.tsx index 307e71360a..4b70db8853 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/CreateWorkspaceTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/CreateWorkspaceTool.tsx @@ -68,12 +68,12 @@ export const CreateWorkspaceTool: React.FC<{ const header = ( <> - - {label} + + {label} {isError && ( - + {errorMessage || "Failed to create workspace"} @@ -81,7 +81,7 @@ export const CreateWorkspaceTool: React.FC<{ )} {isRunning && ( - + )} {workspaceLink && !isRunning && ( - - {label} - + {label} {isError && ( - + {errorMessage || "Failed to edit files"} @@ -75,7 +72,7 @@ export const EditFilesTool: React.FC<{ )} {isRunning && ( - + )} } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx index 14288bd736..49bec3889e 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ExecuteTool.tsx @@ -213,7 +213,7 @@ export const ExecuteAuthRequiredTool: React.FC<{
- + Authenticate with {providerLabel} to continue this command.
@@ -302,7 +302,7 @@ export const WaitForExternalAuthTool: React.FC<{
{icon} - {label} + {label}
); diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx index 64f9b935b9..5a6de7ff03 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx @@ -86,7 +86,7 @@ export const InlineDesktopPreview: React.FC<{ aria-label="Open desktop tab" 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" > - + View desktop diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ListTemplatesTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ListTemplatesTool.tsx index c18623123a..c600feeddb 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ListTemplatesTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ListTemplatesTool.tsx @@ -6,7 +6,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { ToolCollapsible } from "./ToolCollapsible"; import { asRecord, asString, type ToolStatus } from "./utils"; @@ -37,13 +36,11 @@ export const ListTemplatesTool: React.FC<{ hasContent={hasContent} header={ <> - - {label} - + {label} {isError && ( - + {errorMessage || "Failed to list templates"} @@ -51,7 +48,7 @@ export const ListTemplatesTool: React.FC<{ )} {isRunning && ( - + )} } @@ -68,7 +65,7 @@ export const ListTemplatesTool: React.FC<{ if (!name) { return ( -
+
{templateName}
); @@ -79,7 +76,7 @@ export const ListTemplatesTool: React.FC<{ e.stopPropagation()} - className="flex items-center gap-1.5 text-sm text-content-secondary opacity-50 transition-opacity hover:opacity-100" + className="flex items-center gap-1.5 text-[13px] text-content-secondary opacity-50 transition-opacity hover:opacity-100" > {templateName} diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx index cacbb94369..523670a6a4 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ProposePlanTool.tsx @@ -9,7 +9,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { Response } from "../Response"; import type { ToolStatus } from "./utils"; @@ -73,8 +72,8 @@ export const ProposePlanTool: React.FC<{ return (
-
- +
+ {isRunning ? `Proposing ${filename}…` : `Proposed ${filename}`} {effectiveError && ( @@ -91,7 +90,7 @@ export const ProposePlanTool: React.FC<{ )} {isRunning && ( - + )}
{hasDisplayContent ? ( @@ -132,13 +131,13 @@ export const ProposePlanTool: React.FC<{ ) : ( !fetchLoading && !effectiveError && ( -

+

No plan content.

) )} {fetchLoading && ( -
+
Loading plan…
diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx index 9ef4ecdf27..6117683103 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadFileTool.tsx @@ -8,7 +8,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { ToolCollapsible } from "./ToolCollapsible"; import { DIFFS_FONT_STYLE, @@ -31,6 +30,8 @@ export const ReadFileTool: React.FC<{ const isDark = theme.palette.mode === "dark"; const hasContent = content.length > 0; const isRunning = status === "running"; + const filename = path.split("/").pop() || path; + const label = isRunning ? `Reading ${filename}…` : `Read ${filename}`; return ( - - Read {path.split("/").pop() || path} - + {label} {isError && ( - + {errorMessage || "Failed to read file"} @@ -52,7 +51,7 @@ export const ReadFileTool: React.FC<{ )} {isRunning && ( - + )} } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadSkillTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadSkillTool.tsx index 6a35d426d9..2caabb419f 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadSkillTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadSkillTool.tsx @@ -6,7 +6,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { Response } from "../Response"; import { ToolCollapsible } from "./ToolCollapsible"; import type { ToolStatus } from "./utils"; @@ -27,14 +26,14 @@ export const ReadSkillTool: React.FC<{ hasContent={hasContent} header={ <> - - + + {isRunning ? `Reading ${label}…` : `Read ${label}`} {isError && ( - + {errorMessage || "Failed to read skill"} @@ -42,7 +41,7 @@ export const ReadSkillTool: React.FC<{ )} {isRunning && ( - + )} } diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadTemplateTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadTemplateTool.tsx index abeb09b275..3a158430a4 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ReadTemplateTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ReadTemplateTool.tsx @@ -5,7 +5,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import type { ToolStatus } from "./utils"; /** @@ -27,12 +26,12 @@ export const ReadTemplateTool: React.FC<{ : "Read template"; return ( -
- {label} +
+ {label} {isError && ( - + {errorMessage || "Failed to read template"} @@ -40,7 +39,7 @@ export const ReadTemplateTool: React.FC<{ )} {isRunning && ( - + )}
); diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/StartWorkspaceTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/StartWorkspaceTool.tsx index 3195d7efb5..b2dbf1a178 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/StartWorkspaceTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/StartWorkspaceTool.tsx @@ -38,12 +38,12 @@ export const StartWorkspaceTool: FC = ({ const header = ( <> - - {label} + + {label} {isError && ( - + {errorMessage || "Failed to start workspace"} @@ -51,7 +51,7 @@ export const StartWorkspaceTool: FC = ({ )} {isRunning && ( - + )} ); diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx index c8ccee3aca..883d14dd19 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/SubagentTool.tsx @@ -67,7 +67,7 @@ function getSubagentLabel( ): React.ReactNode { if (showDesktopPreview && toolStatus === "running") { return ( - + Using the computer... ); @@ -79,8 +79,7 @@ function getSubagentLabel( ) { return ( <> - Used the computer{" "} - {title} + Used the computer {title} ); } @@ -94,7 +93,7 @@ function getSubagentLabel( return ( <> {SUBAGENT_VERBS[descriptor.action][phase]} - {title} + {title} ); } @@ -124,22 +123,20 @@ const SubagentStatusIcon: React.FC<{ const subagentCompleted = isSubagentSuccessStatus(subagentStatus); const DefaultIcon = iconKind === "monitor" ? MonitorIcon : BotIcon; if (isTimeout && !subagentCompleted) { - return ; + return ; } if ((isError && !subagentCompleted) || toolStatus === "error") { - return ; + return ; } if (toolStatus === "running") { if (showDesktopPreview) { - return ( - - ); + return ; } return ( ); } - return ; + return ; }; /** @@ -199,7 +196,8 @@ export const SubagentTool: React.FC<{ className={cn( "border-0 bg-transparent p-0 m-0 font-[inherit] text-[inherit] text-left", "flex w-full items-center gap-2", - hasExpandableContent && "cursor-pointer", + "text-content-secondary transition-colors", + hasExpandableContent && "cursor-pointer hover:text-content-primary", )} > {" "} - + {getSubagentLabel( showDesktopPreview, toolStatus, @@ -229,19 +227,19 @@ export const SubagentTool: React.FC<{ )} - {durationLabel && ( - - {`Worked for ${durationLabel}`} - - )} {hasExpandableContent && ( )} + {durationLabel && ( + + {`Worked for ${durationLabel}`} + + )} {showDesktopPreview && desktopChatId && toolStatus !== "completed" && ( diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx index fd84d2a590..33414d0136 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/Tool.tsx @@ -813,7 +813,7 @@ const GenericToolRenderer: FC = ({ serverName={mcpServer?.display_name} /> {modelIntent ? ( - + {modelIntent.charAt(0).toUpperCase() + modelIntent.slice(1)} ) : ( @@ -827,7 +827,7 @@ const GenericToolRenderer: FC = ({ {isError && ( - + {errorMessage || "Tool call failed"} @@ -835,7 +835,7 @@ const GenericToolRenderer: FC = ({ )} {isRunning && ( - + )} } @@ -988,12 +988,18 @@ export const Tool = memo( return (
= ({ {header} ) : ( -
+
{header}
)} diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ToolIcon.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ToolIcon.tsx index a1fe1bb949..70494a6f77 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ToolIcon.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ToolIcon.tsx @@ -33,7 +33,7 @@ export const ToolIcon: React.FC<{ subagentIconKind?: SubagentIconKind; }> = ({ name, iconUrl, isRunning, serverName, subagentIconKind }) => { const [imgError, setImgError] = useState(false); - const color = "text-content-secondary"; + const color = "text-current"; const base = cn("h-4 w-4 shrink-0", color, isRunning && "grayscale"); // If an MCP icon URL is provided and hasn't failed, render it. diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/ToolLabel.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/ToolLabel.tsx index cd35d94d7a..371a45dde1 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/ToolLabel.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/ToolLabel.tsx @@ -45,9 +45,7 @@ const renderSubagentLabel = ( } })(); - return ( - {text} - ); + return {text}; }; export const ToolLabel: React.FC<{ @@ -77,17 +75,11 @@ export const ToolLabel: React.FC<{ ); } - return ( - - Running command - - ); + return Running command; } case "process_output": return ( - - Reading process output - + Reading process output ); case "process_signal": { const signal = parsed ? asString(parsed.signal) : ""; @@ -98,7 +90,7 @@ export const ToolLabel: React.FC<{ if (hasResult && success) { const verb = signal === "kill" ? "Killed" : "Terminated"; return ( - + {verb} process{shortId ? ` ${shortId}` : ""} ); @@ -111,13 +103,13 @@ export const ToolLabel: React.FC<{ ? "terminate" : "signal"; return ( - + Failed to {verb} process{shortId ? ` ${shortId}` : ""} ); } return ( - + {signal === "kill" ? "Killing process…" : signal === "terminate" @@ -127,17 +119,9 @@ export const ToolLabel: React.FC<{ ); } case "process_list": - return ( - - Listing processes - - ); + return Listing processes; case "read_file": - return ( - - Reading file… - - ); + return Reading file…; case "write_file": { const path = parsed ? asString(parsed.path) : ""; if (path) { @@ -147,11 +131,7 @@ export const ToolLabel: React.FC<{ ); } - return ( - - Writing file - - ); + return Writing file; } case "edit_files": { const files = parsed?.files; @@ -165,33 +145,21 @@ export const ToolLabel: React.FC<{ ); } } - return ( - - Editing files - - ); + return Editing files; } case "create_workspace": { const wsName = parsedResult ? asString(parsedResult.workspace_name) : ""; if (wsName) { - return ( - - Created {wsName} - - ); + return Created {wsName}; } - return ( - - Creating workspace - - ); + return Creating workspace; } case "list_templates": { const count = parsedResult ? ((parsedResult.count as number | undefined) ?? 0) : 0; return ( - + {count === 0 ? "Listing templates…" : count === 1 @@ -208,17 +176,13 @@ export const ToolLabel: React.FC<{ ? asString(templateRec.display_name) || asString(templateRec.name) : ""; return ( - + {tmplName ? `Read template ${tmplName}` : "Reading template…"} ); } case "chat_summarized": - return ( - - Summarized - - ); + return Summarized; case "attach_file": { const attachedName = (parsedResult ? asString(parsedResult.name) : "") || @@ -226,30 +190,20 @@ export const ToolLabel: React.FC<{ (parsed ? asString(parsed.path).split("/").pop() : "") || "file"; return ( - - {`Attached ${attachedName}`} - + {`Attached ${attachedName}`} ); } case "computer": - return ( - - Screenshot - - ); + return Screenshot; case "propose_plan": { const path = parsed ? asString(parsed.path) || "PLAN.md" : "PLAN.md"; const filename = path.split("/").pop() || "PLAN.md"; - return ( - - {filename} - - ); + return {filename}; } case "read_skill": { const skillName = parsed ? asString(parsed.name) : ""; return ( - + {skillName ? parsedResult ? `Read skill ${skillName}` @@ -266,7 +220,7 @@ export const ToolLabel: React.FC<{ ? `${skillName}/${filePath}` : skillName || filePath || "skill file"; return ( - + {parsedResult ? `Read ${label}` : `Reading ${label}…`} ); @@ -274,7 +228,7 @@ export const ToolLabel: React.FC<{ case "start_workspace": { const wsName = parsedResult ? asString(parsedResult.workspace_name) : ""; return ( - + {wsName ? `Started ${wsName}` : "Starting workspace…"} ); @@ -282,11 +236,7 @@ export const ToolLabel: React.FC<{ default: { const displayName = mcpSlug ? humanizeMCPToolName(mcpSlug, name) : name; - return ( - - {displayName} - - ); + return {displayName}; } } }; diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/WebSearchSources.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/WebSearchSources.tsx index 815356f995..d1bc62308d 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/WebSearchSources.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/WebSearchSources.tsx @@ -36,8 +36,8 @@ const WebSearchSources: FC = ({ sources }) => { hasContent={unique.length > 0} header={ <> - - + + Searched {detail} diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/WriteFileTool.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/WriteFileTool.tsx index f1ba6cac92..fa79904c70 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/WriteFileTool.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/WriteFileTool.tsx @@ -9,7 +9,6 @@ import { TooltipContent, TooltipTrigger, } from "#/components/Tooltip/Tooltip"; -import { cn } from "#/utils/cn"; import { ToolCollapsible } from "./ToolCollapsible"; import { DIFFS_FONT_STYLE, @@ -43,13 +42,11 @@ export const WriteFileTool: React.FC<{ hasContent={hasDiff} header={ <> - - {label} - + {label} {isError && ( - + {errorMessage || "Failed to write file"} @@ -57,7 +54,7 @@ export const WriteFileTool: React.FC<{ )} {isRunning && ( - + )} }