From a708e9d86940c8622b88bb55327eb3329af3a675 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Sat, 28 Mar 2026 11:16:11 -0400 Subject: [PATCH] feat: add tool rendering for read_skill, read_skill_file, start_workspace (#23744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These tools previously fell through to the `GenericToolRenderer` which showed a wrench icon and the raw tool name. Now they get dedicated icons and contextual labels. ## Changes **ToolIcon.tsx** — new icon mappings: - `read_skill` / `read_skill_file` → `BookOpenIcon` - `start_workspace` → `PlayIcon` - `web_search` → `SearchIcon` **ToolLabel.tsx** — contextual labels: - `read_skill`: "Reading skill {name}…" → "Read skill {name}" - `read_skill_file`: "Reading {skill}/{path}…" → "Read {skill}/{path}" - `start_workspace`: "Starting workspace…" → "Started {name}" - `web_search`: "Searching \"{query}\"…" → "Searched \"{query}\"" **tool.stories.tsx** — 12 new stories covering running, completed, and error states for all four tools.
Visually verified in Storybook All 10 story variants verified: - ReadSkillRunning / ReadSkillCompleted / ReadSkillError - ReadSkillFileRunning / ReadSkillFileCompleted / ReadSkillFileError - StartWorkspaceRunning / StartWorkspaceCompleted / StartWorkspaceError - WebSearchRunning / WebSearchCompleted / WebSearchNoQuery
--- .../components/ai-elements/tool.stories.tsx | 136 ++++++++++++++++++ .../components/ai-elements/tool/ToolIcon.tsx | 8 ++ .../components/ai-elements/tool/ToolLabel.tsx | 34 +++++ 3 files changed, 178 insertions(+) diff --git a/site/src/components/ai-elements/tool.stories.tsx b/site/src/components/ai-elements/tool.stories.tsx index 4f7aead699..43c66be6d8 100644 --- a/site/src/components/ai-elements/tool.stories.tsx +++ b/site/src/components/ai-elements/tool.stories.tsx @@ -1257,3 +1257,139 @@ export const WaitAgentComputerUseRunning: Story = { }); }, }; + +// --------------------------------------------------------------------------- +// read_skill stories +// --------------------------------------------------------------------------- + +export const ReadSkillRunning: Story = { + args: { + name: "read_skill", + status: "running", + args: { name: "deep-review" }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Reading skill deep-review/)).toBeInTheDocument(); + }, +}; + +export const ReadSkillCompleted: Story = { + args: { + name: "read_skill", + status: "completed", + args: { name: "deep-review" }, + result: { + name: "deep-review", + body: "## Deep Review Skill\n\nReview the code changes thoroughly.\n\n1. Check for correctness\n2. Verify tests\n3. Ensure style consistency", + files: ["roles/security-reviewer.md", "templates/review-checklist.md"], + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText(/Read skill deep-review/)).toBeInTheDocument(); + }, +}; + +export const ReadSkillError: Story = { + args: { + name: "read_skill", + status: "error", + isError: true, + args: { name: "nonexistent-skill" }, + result: { error: 'skill "nonexistent-skill" not found' }, + }, + play: async ({ canvasElement }) => { + expect( + canvasElement.querySelector(".lucide-triangle-alert"), + ).not.toBeNull(); + }, +}; + +// --------------------------------------------------------------------------- +// read_skill_file stories +// --------------------------------------------------------------------------- + +export const ReadSkillFileRunning: Story = { + args: { + name: "read_skill_file", + status: "running", + args: { name: "deep-review", path: "roles/security-reviewer.md" }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + canvas.getByText(/Reading deep-review\/roles\/security-reviewer\.md/), + ).toBeInTheDocument(); + }, +}; + +export const ReadSkillFileCompleted: Story = { + args: { + name: "read_skill_file", + status: "completed", + args: { name: "deep-review", path: "roles/security-reviewer.md" }, + result: { + content: + "# Security Reviewer Role\n\nFocus on authentication, authorization, and input validation.\n\n## Checklist\n- [ ] Verify auth middleware\n- [ ] Check for SQL injection\n- [ ] Validate user inputs", + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect( + canvas.getByText(/Read deep-review\/roles\/security-reviewer\.md/), + ).toBeInTheDocument(); + }, +}; + +export const ReadSkillFileError: Story = { + args: { + name: "read_skill_file", + status: "error", + isError: true, + args: { name: "deep-review", path: "missing-file.md" }, + result: { error: "file not found" }, + }, +}; + +// --------------------------------------------------------------------------- +// start_workspace stories +// --------------------------------------------------------------------------- + +export const StartWorkspaceRunning: Story = { + args: { + name: "start_workspace", + status: "running", + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText("Starting workspace…")).toBeInTheDocument(); + }, +}; + +export const StartWorkspaceCompleted: Story = { + args: { + name: "start_workspace", + status: "completed", + result: { + started: true, + workspace_name: "my-project", + agent_status: "ready", + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.getByText("Started my-project")).toBeInTheDocument(); + }, +}; + +export const StartWorkspaceError: Story = { + args: { + name: "start_workspace", + status: "error", + isError: true, + result: { + error: "workspace was deleted; use create_workspace to make a new one", + }, + }, +}; diff --git a/site/src/components/ai-elements/tool/ToolIcon.tsx b/site/src/components/ai-elements/tool/ToolIcon.tsx index 5af37b8e97..61402984e0 100644 --- a/site/src/components/ai-elements/tool/ToolIcon.tsx +++ b/site/src/components/ai-elements/tool/ToolIcon.tsx @@ -1,9 +1,11 @@ import { + BookOpenIcon, BotIcon, ClipboardListIcon, FileIcon, FilePenIcon, MonitorIcon, + PlayIcon, PlusCircleIcon, TerminalIcon, WrenchIcon, @@ -85,6 +87,12 @@ export const ToolIcon: React.FC<{ case "computer": case "spawn_computer_use_agent": return ; + case "read_skill": + case "read_skill_file": + return ; + case "start_workspace": + return ; + default: return ; } diff --git a/site/src/components/ai-elements/tool/ToolLabel.tsx b/site/src/components/ai-elements/tool/ToolLabel.tsx index a6ee73908a..4f9d4396d9 100644 --- a/site/src/components/ai-elements/tool/ToolLabel.tsx +++ b/site/src/components/ai-elements/tool/ToolLabel.tsx @@ -170,6 +170,40 @@ export const ToolLabel: React.FC<{ ); } + case "read_skill": { + const skillName = parsed ? asString(parsed.name) : ""; + return ( + + {skillName + ? parsedResult + ? `Read skill ${skillName}` + : `Reading skill ${skillName}…` + : "Reading skill…"} + + ); + } + case "read_skill_file": { + const skillName = parsed ? asString(parsed.name) : ""; + const filePath = parsed ? asString(parsed.path) : ""; + const label = + skillName && filePath + ? `${skillName}/${filePath}` + : skillName || filePath || "skill file"; + return ( + + {parsedResult ? `Read ${label}` : `Reading ${label}…`} + + ); + } + case "start_workspace": { + const wsName = parsedResult ? asString(parsedResult.workspace_name) : ""; + return ( + + {wsName ? `Started ${wsName}` : "Starting workspace…"} + + ); + } + default: { const displayName = mcpSlug ? humanizeMCPToolName(mcpSlug, name) : name; return (