mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: add tool rendering for read_skill, read_skill_file, start_workspace (#23744)
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.
<details>
<summary>Visually verified in Storybook</summary>
All 10 story variants verified:
- ReadSkillRunning / ReadSkillCompleted / ReadSkillError
- ReadSkillFileRunning / ReadSkillFileCompleted / ReadSkillFileError
- StartWorkspaceRunning / StartWorkspaceCompleted / StartWorkspaceError
- WebSearchRunning / WebSearchCompleted / WebSearchNoQuery
</details>
This commit is contained in:
@@ -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",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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 <MonitorIcon className={base} />;
|
||||
case "read_skill":
|
||||
case "read_skill_file":
|
||||
return <BookOpenIcon className={base} />;
|
||||
case "start_workspace":
|
||||
return <PlayIcon className={base} />;
|
||||
|
||||
default:
|
||||
return <WrenchIcon className={base} />;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,40 @@ export const ToolLabel: React.FC<{
|
||||
</span>
|
||||
);
|
||||
}
|
||||
case "read_skill": {
|
||||
const skillName = parsed ? asString(parsed.name) : "";
|
||||
return (
|
||||
<span className="truncate text-sm text-content-secondary">
|
||||
{skillName
|
||||
? parsedResult
|
||||
? `Read skill ${skillName}`
|
||||
: `Reading skill ${skillName}…`
|
||||
: "Reading skill…"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<span className="truncate text-sm text-content-secondary">
|
||||
{parsedResult ? `Read ${label}` : `Reading ${label}…`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
case "start_workspace": {
|
||||
const wsName = parsedResult ? asString(parsedResult.workspace_name) : "";
|
||||
return (
|
||||
<span className="truncate text-sm text-content-secondary">
|
||||
{wsName ? `Started ${wsName}` : "Starting workspace…"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
default: {
|
||||
const displayName = mcpSlug ? humanizeMCPToolName(mcpSlug, name) : name;
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user