refactor(site/src/pages/AgentsPage): align tool-call and message styling (#24790)

Tighten visual rhythm and typography in the agent chat page so tool
calls, reasoning, and assistant text share the same baseline.

## Highlights

- Unify font size to **13px** across user messages, assistant
`Response`, reasoning, and every tool-call label.
- Reuse the `text-content-secondary → hover:text-content-primary`
transition on tool-call rows so labels, chevrons, and lucide icons
brighten together. Icons inside hover-aware headers switch to
`text-current` so they inherit the parent transition; static icons in
non-collapsible cards (`ExecuteTool`, `ProcessOutputTool`) keep the
constant secondary color.
- Collapse padding between adjacent tool/thinking blocks via a shared
`data-tool-call` attribute and adjacent-sibling selectors
(`[&:has(+[data-tool-call])]:pb-0` + `[[data-tool-call]+&]:pt-0`).
First/last items keep their padding against text and reasoning siblings.
- `read_file` now mirrors `write_file`: `Reading <name>…` while running,
`Read <name>` once complete.
- `ask_user_question` flips the inline label from `Asking:` to `Asked:`
once answered.
- Subagent row layout: status icon + label + chevron sit together at the
start of the row, while the `Worked for <duration>` text uses `ml-auto`
to anchor the right edge.
- New Storybook story `WithEveryTool` (under `Pages / AgentsPage /
AgentChatPage`) exercises every tool renderer plus subagent variants and
the generic MCP fallback in a single completed-then-streaming turn.

---

_Authored with help from a Coder Agent._
This commit is contained in:
Kyle Carberry
2026-04-28 12:50:51 -04:00
committed by GitHub
parent 881df9a5b0
commit 4a91656fe5
23 changed files with 1008 additions and 229 deletions
@@ -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<string, string>`, 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 = {
@@ -147,52 +147,62 @@ const ReasoningDisclosure = memo<{
}, [displayTextLength, isPreviewConstrained]);
return (
<Collapsible
open={expanded}
onOpenChange={(open) => setManualToggle(open)}
className="w-full"
>
<CollapsibleTrigger
className={cn(
"border-0 bg-transparent p-0 m-0 font-[inherit] text-[inherit] text-left",
"flex w-full items-center gap-2 cursor-pointer",
"text-content-secondary transition-colors hover:text-content-primary",
)}
>
{isStreaming ? (
<Shimmer as="span" className="text-sm">
Thinking
</Shimmer>
) : (
<span className="text-sm">Thinking</span>
)}
<ChevronDownIcon
className={cn(
"ml-auto h-3 w-3 shrink-0 text-current transition-transform",
expanded ? "rotate-0" : "-rotate-90",
)}
/>
</CollapsibleTrigger>
{hasText && (
<CollapsibleContent>
<div
ref={previewScrollRef}
className={cn(
"mt-1.5",
isPreviewConstrained && "max-h-24 overflow-y-auto",
)}
>
<Response
className="text-[11px] text-content-secondary"
urlTransform={urlTransform}
streaming={isStreaming}
>
{displayText}
</Response>
</div>
</CollapsibleContent>
<div
data-tool-call=""
className={cn(
"py-0.5",
// Collapse padding between adjacent tool/thinking blocks.
"[&:has(+[data-tool-call])]:pb-0",
"[[data-tool-call]+&]:pt-0",
)}
</Collapsible>
>
<Collapsible
open={expanded}
onOpenChange={(open) => setManualToggle(open)}
className="w-full"
>
<CollapsibleTrigger
className={cn(
"border-0 bg-transparent p-0 m-0 font-[inherit] text-[inherit] text-left",
"flex w-full items-center gap-2 cursor-pointer",
"text-content-secondary transition-colors hover:text-content-primary",
)}
>
{isStreaming ? (
<Shimmer as="span" className="text-[13px]">
Thinking
</Shimmer>
) : (
<span className="text-[13px]">Thinking</span>
)}
<ChevronDownIcon
className={cn(
"h-3 w-3 shrink-0 text-current transition-transform",
expanded ? "rotate-0" : "-rotate-90",
)}
/>
</CollapsibleTrigger>
{hasText && (
<CollapsibleContent>
<div
ref={previewScrollRef}
className={cn(
"mt-1.5",
isPreviewConstrained && "max-h-24 overflow-y-auto",
)}
>
<Response
className="text-[11px] text-content-secondary"
urlTransform={urlTransform}
streaming={isStreaming}
>
{displayText}
</Response>
</div>
</CollapsibleContent>
)}
</Collapsible>
</div>
);
},
);
@@ -20,7 +20,7 @@ export const MessageContent = ({
<div
ref={ref}
className={cn(
"whitespace-pre-wrap break-words text-sm leading-relaxed text-content-primary",
"whitespace-pre-wrap break-words text-[13px] leading-relaxed text-content-primary",
className,
)}
{...props}
@@ -1,4 +1,8 @@
import { LoaderIcon, TriangleAlertIcon } from "lucide-react";
import {
LoaderIcon,
MessageCircleQuestionIcon,
TriangleAlertIcon,
} from "lucide-react";
import { type FC, type FormEvent, useId, useState } from "react";
import { useMutation } from "react-query";
import { Button } from "#/components/Button/Button";
@@ -161,8 +165,10 @@ const SelectableAnswerOption: FC<SelectableAnswerOptionProps> = ({
id={id}
value={value}
/>
<span className="text-sm font-medium text-content-primary">{label}</span>
<p className="col-start-2 m-0 whitespace-pre-wrap text-sm text-content-secondary">
<span className="text-[13px] font-medium text-content-primary">
{label}
</span>
<p className="col-start-2 m-0 whitespace-pre-wrap text-[13px] text-content-secondary">
{description}
</p>
</label>
@@ -282,18 +288,20 @@ const QuestionStep: FC<QuestionStepProps> = ({
Question {questionIndex + 1} of {questionCount}
</p>
)}
<div className="space-y-1.5">
<p
id={questionHeaderId}
className="text-xs font-medium text-content-secondary"
>
{questionHeader}
</p>
<div className="flex items-start gap-1.5 text-content-secondary">
<MessageCircleQuestionIcon
aria-hidden="true"
className="mt-0.5 h-4 w-4 shrink-0"
/>
<p
id={questionTextId}
className="whitespace-pre-wrap text-sm text-content-primary"
className="m-0 min-w-0 flex-1 whitespace-pre-wrap text-[13px]"
>
{questionText}
<span className="sr-only" id={questionHeaderId}>
{questionHeader}
</span>
<span aria-hidden="true">Asking: </span>
<span>{questionText}</span>
</p>
</div>
<div className="rounded-md border border-solid border-border-default px-3 py-1">
@@ -343,12 +351,19 @@ const AnsweredQuestionText: FC<AnsweredQuestionTextProps> = ({
idPrefix,
}) => {
return (
<p
id={`${idPrefix}-question-${questionIndex}-text`}
className="whitespace-pre-wrap text-sm text-content-primary"
>
{getQuestionText(question)}
</p>
<div className="flex items-start gap-1.5 text-content-secondary">
<MessageCircleQuestionIcon
aria-hidden="true"
className="mt-0.5 h-4 w-4 shrink-0"
/>
<p
id={`${idPrefix}-question-${questionIndex}-text`}
className="m-0 min-w-0 flex-1 whitespace-pre-wrap text-[13px]"
>
<span aria-hidden="true">Asked: </span>
<span>{getQuestionText(question)}</span>
</p>
</div>
);
};
@@ -523,7 +538,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
<div className="w-full">
<div
role="alert"
className="flex items-center gap-1.5 py-0.5 text-sm text-content-secondary"
className="flex items-center gap-1.5 py-0.5 text-[13px] text-content-secondary"
>
<TriangleAlertIcon
aria-label="Error"
@@ -544,7 +559,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
aria-live="polite"
className="flex items-center gap-1.5 py-0.5"
>
<span className="text-sm text-content-secondary">
<span className="text-[13px] text-content-secondary">
Asking for clarification...
</span>
<LoaderIcon
@@ -553,7 +568,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
/>
</div>
) : (
<p className="text-sm italic text-content-secondary">
<p className="text-[13px] italic text-content-secondary">
No questions available.
</p>
)}
@@ -606,7 +621,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
<p className="text-xs font-medium text-content-secondary">
Submitted answer
</p>
<p className="mt-1 whitespace-pre-wrap text-sm text-content-primary">
<p className="mt-1 whitespace-pre-wrap text-[13px] text-content-primary">
{displayedSubmittedResponseText || "No answer recorded."}
</p>
</div>
@@ -615,7 +630,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
{submitError && (
<div
role="alert"
className="mt-3 flex items-center gap-1.5 text-sm text-content-destructive"
className="mt-3 flex items-center gap-1.5 text-[13px] text-content-destructive"
>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0" />
<span>{submitError}</span>
@@ -670,7 +685,7 @@ export const AskUserQuestionTool: FC<AskUserQuestionToolProps> = ({
aria-live="polite"
className="flex items-center gap-1.5 py-0.5"
>
<span className="text-sm text-content-secondary">
<span className="text-[13px] text-content-secondary">
Asking for clarification...
</span>
<LoaderIcon
@@ -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";
@@ -30,13 +29,13 @@ export const ChatSummarizedTool: React.FC<{
hasContent={hasSummary}
header={
<>
<span className={cn("text-sm", "text-content-secondary")}>
<span className="text-[13px]">
{isRunning ? "Summarizing…" : "Summarized"}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to summarize conversation"}
@@ -44,7 +43,7 @@ export const ChatSummarizedTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -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={
<>
<span className={cn("text-sm", "text-content-secondary")}>
<span className="text-[13px]">
{isRunning ? "Taking screenshot…" : "Screenshot"}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to take screenshot"}
@@ -54,7 +53,7 @@ export const ComputerTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -68,12 +68,12 @@ export const CreateWorkspaceTool: React.FC<{
const header = (
<>
<MonitorIcon className="h-4 w-4 shrink-0 text-content-secondary" />
<span className="text-sm text-content-secondary">{label}</span>
<MonitorIcon className="h-4 w-4 shrink-0 text-current" />
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to create workspace"}
@@ -81,7 +81,7 @@ export const CreateWorkspaceTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
{workspaceLink && !isRunning && (
<Link
@@ -9,7 +9,6 @@ import {
TooltipContent,
TooltipTrigger,
} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
import { ToolCollapsible } from "./ToolCollapsible";
import {
DIFFS_FONT_STYLE,
@@ -61,13 +60,11 @@ export const EditFilesTool: React.FC<{
defaultExpanded
header={
<>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to edit files"}
@@ -75,7 +72,7 @@ export const EditFilesTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -213,7 +213,7 @@ export const ExecuteAuthRequiredTool: React.FC<{
<div className="w-full overflow-hidden rounded-md border border-solid border-border-default bg-surface-primary">
<div className="flex flex-wrap items-center gap-2 px-3 py-2">
<CircleAlertIcon className="h-4 w-4 shrink-0 text-content-warning" />
<span className="text-sm text-content-primary">
<span className="text-[13px] text-content-primary">
Authenticate with {providerLabel} to continue this command.
</span>
</div>
@@ -302,7 +302,7 @@ export const WaitForExternalAuthTool: React.FC<{
<div className="w-full overflow-hidden rounded-md border border-solid border-border-default bg-surface-primary px-3 py-2">
<div className="flex items-center gap-2">
{icon}
<span className="text-sm text-content-primary">{label}</span>
<span className="text-[13px] text-content-primary">{label}</span>
</div>
</div>
);
@@ -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"
>
<span className="text-sm font-medium text-white opacity-0 drop-shadow-md transition-opacity group-hover/preview:opacity-100">
<span className="text-[13px] 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" />
@@ -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={
<>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to list templates"}
@@ -51,7 +48,7 @@ export const ListTemplatesTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -68,7 +65,7 @@ export const ListTemplatesTool: React.FC<{
if (!name) {
return (
<div key={index} className="text-sm text-content-secondary">
<div key={index} className="text-[13px] text-content-secondary">
{templateName}
</div>
);
@@ -79,7 +76,7 @@ export const ListTemplatesTool: React.FC<{
<Link
to={`/templates/${name}`}
onClick={(e) => 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"
>
<span>{templateName}</span>
<ExternalLinkIcon className="h-3 w-3 shrink-0" />
@@ -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 (
<div className="w-full">
<div className="flex items-center gap-1.5 py-0.5">
<span className={cn("text-sm", "text-content-secondary")}>
<div className="flex items-center gap-1.5 py-0.5 text-content-secondary">
<span className="text-[13px]">
{isRunning ? `Proposing ${filename}…` : `Proposed ${filename}`}
</span>
{effectiveError && (
@@ -91,7 +90,7 @@ export const ProposePlanTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</div>
{hasDisplayContent ? (
@@ -132,13 +131,13 @@ export const ProposePlanTool: React.FC<{
) : (
!fetchLoading &&
!effectiveError && (
<p className="text-sm text-content-secondary italic">
<p className="text-[13px] text-content-secondary italic">
No plan content.
</p>
)
)}
{fetchLoading && (
<div className="flex items-center gap-1.5 py-2 text-sm text-content-secondary">
<div className="flex items-center gap-1.5 py-2 text-[13px] text-content-secondary">
<LoaderIcon className="h-3.5 w-3.5 animate-spin motion-reduce:animate-none" />
Loading plan…
</div>
@@ -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 (
<ToolCollapsible
@@ -38,13 +39,11 @@ export const ReadFileTool: React.FC<{
hasContent={hasContent}
header={
<>
<span className={cn("text-sm", "text-content-secondary")}>
Read {path.split("/").pop() || path}
</span>
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read file"}
@@ -52,7 +51,7 @@ export const ReadFileTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -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={
<>
<BookOpenIcon className="h-4 w-4 shrink-0 text-content-secondary" />
<span className={cn("text-sm", "text-content-secondary")}>
<BookOpenIcon className="h-4 w-4 shrink-0 text-current" />
<span className="text-[13px]">
{isRunning ? `Reading ${label}…` : `Read ${label}`}
</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read skill"}
@@ -42,7 +41,7 @@ export const ReadSkillTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -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 (
<div className="flex items-center gap-1.5">
<span className={cn("text-sm", "text-content-secondary")}>{label}</span>
<div className="flex items-center gap-1.5 text-content-secondary">
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to read template"}
@@ -40,7 +39,7 @@ export const ReadTemplateTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</div>
);
@@ -38,12 +38,12 @@ export const StartWorkspaceTool: FC<StartWorkspaceToolProps> = ({
const header = (
<>
<MonitorPlayIcon className="h-4 w-4 shrink-0 text-content-secondary" />
<span className="text-sm text-content-secondary">{label}</span>
<MonitorPlayIcon className="h-4 w-4 shrink-0 text-current" />
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to start workspace"}
@@ -51,7 +51,7 @@ export const StartWorkspaceTool: FC<StartWorkspaceToolProps> = ({
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
);
@@ -67,7 +67,7 @@ function getSubagentLabel(
): React.ReactNode {
if (showDesktopPreview && toolStatus === "running") {
return (
<Shimmer as="span" className="text-sm">
<Shimmer as="span" className="text-[13px]">
Using the computer...
</Shimmer>
);
@@ -79,8 +79,7 @@ function getSubagentLabel(
) {
return (
<>
Used the computer{" "}
<span className="text-content-secondary opacity-60">{title}</span>
Used the computer <span className="opacity-60">{title}</span>
</>
);
}
@@ -94,7 +93,7 @@ function getSubagentLabel(
return (
<>
{SUBAGENT_VERBS[descriptor.action][phase]}
<span className="text-content-secondary opacity-60">{title}</span>
<span className="opacity-60">{title}</span>
</>
);
}
@@ -124,22 +123,20 @@ const SubagentStatusIcon: React.FC<{
const subagentCompleted = isSubagentSuccessStatus(subagentStatus);
const DefaultIcon = iconKind === "monitor" ? MonitorIcon : BotIcon;
if (isTimeout && !subagentCompleted) {
return <ClockIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
return <ClockIcon className="h-4 w-4 shrink-0 text-current" />;
}
if ((isError && !subagentCompleted) || toolStatus === "error") {
return <CircleXIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
return <CircleXIcon className="h-4 w-4 shrink-0 text-current" />;
}
if (toolStatus === "running") {
if (showDesktopPreview) {
return (
<MonitorIcon className="h-4 w-4 shrink-0 text-content-secondary" />
);
return <MonitorIcon className="h-4 w-4 shrink-0 text-current" />;
}
return (
<LoaderIcon className="h-4 w-4 shrink-0 animate-spin motion-reduce:animate-none text-content-link" />
);
}
return <DefaultIcon className="h-4 w-4 shrink-0 text-content-secondary" />;
return <DefaultIcon className="h-4 w-4 shrink-0 text-current" />;
};
/**
@@ -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",
)}
>
<SubagentStatusIcon
@@ -210,7 +208,7 @@ export const SubagentTool: React.FC<{
iconKind={descriptor.iconKind}
showDesktopPreview={showDesktopPreview}
/>{" "}
<span className="min-w-0 flex-1 truncate text-sm text-content-secondary">
<span className="min-w-0 truncate text-[13px]">
{getSubagentLabel(
showDesktopPreview,
toolStatus,
@@ -229,19 +227,19 @@ export const SubagentTool: React.FC<{
</Link>
)}
</span>
{durationLabel && (
<span className="shrink-0 text-xs text-content-secondary">
{`Worked for ${durationLabel}`}
</span>
)}
{hasExpandableContent && (
<ChevronDownIcon
className={cn(
"h-3 w-3 shrink-0 text-content-secondary transition-transform",
"h-3 w-3 shrink-0 text-current transition-transform",
expanded ? "rotate-0" : "-rotate-90",
)}
/>
)}
{durationLabel && (
<span className="ml-auto shrink-0 text-xs">
{`Worked for ${durationLabel}`}
</span>
)}
</button>
{showDesktopPreview && desktopChatId && toolStatus !== "completed" && (
@@ -813,7 +813,7 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
serverName={mcpServer?.display_name}
/>
{modelIntent ? (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{modelIntent.charAt(0).toUpperCase() + modelIntent.slice(1)}
</span>
) : (
@@ -827,7 +827,7 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Tool call failed"}
@@ -835,7 +835,7 @@ const GenericToolRenderer: FC<ToolRendererProps> = ({
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}
@@ -988,12 +988,18 @@ export const Tool = memo(
return (
<div
ref={ref}
data-tool-call=""
className={cn(
name === "execute" ||
name === "process_output" ||
name === "propose_plan"
? "w-full py-0.5"
: "py-0.5",
// Collapse padding between adjacent tool calls so they hug.
// Bottom padding is removed on a tool followed by a tool, and
// top padding is removed on a tool preceded by a tool.
"[&:has(+[data-tool-call])]:pb-0",
"[[data-tool-call]+&]:pt-0",
className,
)}
{...props}
@@ -38,13 +38,18 @@ export const ToolCollapsible: FC<ToolCollapsibleProps> = ({
{header}
<ChevronDownIcon
className={cn(
"ml-auto h-3 w-3 shrink-0 text-current transition-transform",
"h-3 w-3 shrink-0 text-current transition-transform",
expanded ? "rotate-0" : "-rotate-90",
)}
/>
</button>
) : (
<div className={cn("flex items-center gap-2", headerClassName)}>
<div
className={cn(
"flex items-center gap-2 text-content-secondary",
headerClassName,
)}
>
{header}
</div>
)}
@@ -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.
@@ -45,9 +45,7 @@ const renderSubagentLabel = (
}
})();
return (
<span className="truncate text-sm text-content-secondary">{text}</span>
);
return <span className="truncate text-[13px]">{text}</span>;
};
export const ToolLabel: React.FC<{
@@ -77,17 +75,11 @@ export const ToolLabel: React.FC<{
</code>
);
}
return (
<span className="truncate text-sm text-content-secondary">
Running command
</span>
);
return <span className="truncate text-[13px]">Running command</span>;
}
case "process_output":
return (
<span className="truncate text-sm text-content-secondary">
Reading process output
</span>
<span className="truncate text-[13px]">Reading process output</span>
);
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 (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{verb} process{shortId ? ` ${shortId}` : ""}
</span>
);
@@ -111,13 +103,13 @@ export const ToolLabel: React.FC<{
? "terminate"
: "signal";
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
Failed to {verb} process{shortId ? ` ${shortId}` : ""}
</span>
);
}
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{signal === "kill"
? "Killing process…"
: signal === "terminate"
@@ -127,17 +119,9 @@ export const ToolLabel: React.FC<{
);
}
case "process_list":
return (
<span className="truncate text-sm text-content-secondary">
Listing processes
</span>
);
return <span className="truncate text-[13px]">Listing processes</span>;
case "read_file":
return (
<span className="truncate text-sm text-content-secondary">
Reading file…
</span>
);
return <span className="truncate text-[13px]">Reading file…</span>;
case "write_file": {
const path = parsed ? asString(parsed.path) : "";
if (path) {
@@ -147,11 +131,7 @@ export const ToolLabel: React.FC<{
</code>
);
}
return (
<span className="truncate text-sm text-content-secondary">
Writing file
</span>
);
return <span className="truncate text-[13px]">Writing file</span>;
}
case "edit_files": {
const files = parsed?.files;
@@ -165,33 +145,21 @@ export const ToolLabel: React.FC<{
);
}
}
return (
<span className="truncate text-sm text-content-secondary">
Editing files
</span>
);
return <span className="truncate text-[13px]">Editing files</span>;
}
case "create_workspace": {
const wsName = parsedResult ? asString(parsedResult.workspace_name) : "";
if (wsName) {
return (
<span className="truncate text-sm text-content-secondary">
Created {wsName}
</span>
);
return <span className="truncate text-[13px]">Created {wsName}</span>;
}
return (
<span className="truncate text-sm text-content-secondary">
Creating workspace
</span>
);
return <span className="truncate text-[13px]">Creating workspace</span>;
}
case "list_templates": {
const count = parsedResult
? ((parsedResult.count as number | undefined) ?? 0)
: 0;
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{count === 0
? "Listing templates…"
: count === 1
@@ -208,17 +176,13 @@ export const ToolLabel: React.FC<{
? asString(templateRec.display_name) || asString(templateRec.name)
: "";
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{tmplName ? `Read template ${tmplName}` : "Reading template…"}
</span>
);
}
case "chat_summarized":
return (
<span className="truncate text-sm text-content-secondary">
Summarized
</span>
);
return <span className="truncate text-[13px]">Summarized</span>;
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 (
<span className="truncate text-sm text-content-secondary">
{`Attached ${attachedName}`}
</span>
<span className="truncate text-[13px]">{`Attached ${attachedName}`}</span>
);
}
case "computer":
return (
<span className="truncate text-sm text-content-secondary">
Screenshot
</span>
);
return <span className="truncate text-[13px]">Screenshot</span>;
case "propose_plan": {
const path = parsed ? asString(parsed.path) || "PLAN.md" : "PLAN.md";
const filename = path.split("/").pop() || "PLAN.md";
return (
<span className="truncate text-sm text-content-secondary">
{filename}
</span>
);
return <span className="truncate text-[13px]">{filename}</span>;
}
case "read_skill": {
const skillName = parsed ? asString(parsed.name) : "";
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{skillName
? parsedResult
? `Read skill ${skillName}`
@@ -266,7 +220,7 @@ export const ToolLabel: React.FC<{
? `${skillName}/${filePath}`
: skillName || filePath || "skill file";
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{parsedResult ? `Read ${label}` : `Reading ${label}…`}
</span>
);
@@ -274,7 +228,7 @@ export const ToolLabel: React.FC<{
case "start_workspace": {
const wsName = parsedResult ? asString(parsedResult.workspace_name) : "";
return (
<span className="truncate text-sm text-content-secondary">
<span className="truncate text-[13px]">
{wsName ? `Started ${wsName}` : "Starting workspace…"}
</span>
);
@@ -282,11 +236,7 @@ export const ToolLabel: React.FC<{
default: {
const displayName = mcpSlug ? humanizeMCPToolName(mcpSlug, name) : name;
return (
<span className="truncate text-sm text-content-secondary">
{displayName}
</span>
);
return <span className="truncate text-[13px]">{displayName}</span>;
}
}
};
@@ -36,8 +36,8 @@ const WebSearchSources: FC<WebSearchSourcesProps> = ({ sources }) => {
hasContent={unique.length > 0}
header={
<>
<GlobeIcon className="h-4 w-4 shrink-0 text-content-secondary" />
<span className="text-sm text-content-secondary">
<GlobeIcon className="h-4 w-4 shrink-0 text-current" />
<span className="text-[13px]">
Searched <span className="text-content-secondary/60">{detail}</span>
</span>
</>
@@ -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={
<>
<span className={cn("text-sm", "text-content-secondary")}>
{label}
</span>
<span className="text-[13px]">{label}</span>
{isError && (
<Tooltip>
<TooltipTrigger asChild>
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-content-secondary" />
<TriangleAlertIcon className="h-3.5 w-3.5 shrink-0 text-current" />
</TooltipTrigger>
<TooltipContent>
{errorMessage || "Failed to write file"}
@@ -57,7 +54,7 @@ export const WriteFileTool: React.FC<{
</Tooltip>
)}
{isRunning && (
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-content-secondary" />
<LoaderIcon className="h-3.5 w-3.5 shrink-0 animate-spin motion-reduce:animate-none text-current" />
)}
</>
}