Skip to content

Commit 6b522cb

Browse files
yyq1025claude
andcommitted
tool rows: past-tense verbs replace name chips; sheet bodies for all tools
Row format is now `<verb> <summary>` everywhere — muted past-tense verb, darker object, red verb on failure — matching claude.ai/code's vocabulary (live-probed 2026-06-12): Ran / Read / Edited / Created / Searched / Searched web / Fetched / Ran agent / Used <name>. The uppercase ToolChip is gone from both the transcript row and the sheet header; Bash's old "Ran <description>" pattern is simply generalized. running rows render past tense too (no -ing flicker mid-turn). The tool-call sheet's default-null branch is gone with it — every detail variant now has a body: web_search results as native text, web_fetch and agent render their markdown output through ChunkedMarkdown (plus URL/ prompt sections), monitor reuses the Bash command+output view, ask_user shows questions with answers, tasks/schedule_wakeup show labeled fields. Agent rows surface the per-spawn model override when present (new optional protocol field — additive, no version bump; the common inherit-from-parent case has no model in input and is not reconstructible). subagent_type follows the SDK in being optional, and task_stop's summary drops its trailing "stopped" so the verb doesn't read twice. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7c05350 commit 6b522cb

8 files changed

Lines changed: 345 additions & 109 deletions

File tree

packages/app/src/components/transcript/chat-panel.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ import {
4747

4848
// ToolBlock row height, derived from its styles (keep in sync with
4949
// tool-block.tsx): Pressable py-1.5 (12) + one text-base line (lineHeight
50-
// 24, the tallest child — ToolChip is ~18). Both Bash and chip branches are
51-
// single-line by construction (numberOfLines={1}). The text line scales
52-
// with Dynamic Type; the vertical padding doesn't.
50+
// 24). The verb+summary row is single-line by construction
51+
// (numberOfLines={1}). The text line scales with Dynamic Type; the
52+
// vertical padding doesn't.
5353
const TOOL_ROW_PADDING_V = 12;
5454
const TOOL_ROW_LINE_HEIGHT = 24;
5555

Lines changed: 21 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Pressable, Text, View } from "react-native";
1+
import { Pressable, Text } from "react-native";
2+
import { toolVerb } from "@/lib/tool-verbs";
23
import type { ToolRenderBlock } from "@/lib/transcript-blocks";
3-
import { ToolChip, useToolCallSheet } from "./tool-call-sheet";
4+
import { useToolCallSheet } from "./tool-call-sheet";
45

56
/**
67
* Trigger row for a paired tool_use+tool_result. Tap → opens the shared
@@ -19,50 +20,33 @@ import { ToolChip, useToolCallSheet } from "./tool-call-sheet";
1920
* renderer (the Pierre webview) opened on demand — no per-row mounting,
2021
* no virtualization pressure, no jitter.
2122
*
22-
* Bash gets a chip-less "Ran <description>" verb-led header to match
23-
* Claude Desktop. Other tools render `<chip> <summary>`.
23+
* Row format = `<verb> <summary>` (claude.ai/code vocabulary, see
24+
* tool-verbs.ts): muted past-tense verb + darker object, red verb on
25+
* failure. Replaced the former uppercase tool-name chip (ToolChip) —
26+
* Bash rows already rendered this way ("Ran <description>"); this is
27+
* that pattern generalized to every tool.
2428
*/
2529
export function ToolBlock({ block }: { block: ToolRenderBlock }) {
2630
const { openToolCall } = useToolCallSheet();
2731
const isError = block.status === "failed";
28-
const isBash = block.name === "Bash";
2932

3033
return (
31-
<Pressable
32-
onPress={() => openToolCall(block)}
33-
className="flex-row items-center gap-2 px-4 py-1.5"
34-
>
35-
{isBash ? (
34+
<Pressable onPress={() => openToolCall(block)} className="px-4 py-1.5">
35+
<Text
36+
numberOfLines={1}
37+
className="text-base text-gray-700 dark:text-gray-300"
38+
>
3639
<Text
37-
numberOfLines={1}
38-
className="flex-1 text-base text-gray-700 dark:text-gray-300"
40+
className={
41+
isError
42+
? "text-red-600 dark:text-red-400"
43+
: "text-gray-500 dark:text-gray-400"
44+
}
3945
>
40-
<Text
41-
className={
42-
isError
43-
? "text-red-600 dark:text-red-400"
44-
: "text-gray-500 dark:text-gray-400"
45-
}
46-
>
47-
Ran{" "}
48-
</Text>
49-
{block.summary}
46+
{toolVerb(block.detail)}
5047
</Text>
51-
) : (
52-
<>
53-
<ToolChip name={block.name} isError={isError} />
54-
{block.summary ? (
55-
<Text
56-
numberOfLines={1}
57-
className="flex-1 text-base text-gray-700 dark:text-gray-300"
58-
>
59-
{block.summary}
60-
</Text>
61-
) : (
62-
<View className="flex-1" />
63-
)}
64-
</>
65-
)}
48+
{block.summary ? ` ${block.summary}` : ""}
49+
</Text>
6650
</Pressable>
6751
);
6852
}

packages/app/src/components/transcript/tool-call-sheet.tsx

Lines changed: 189 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
import { KeyboardController } from "react-native-keyboard-controller";
2424
import { useSafeAreaInsets } from "react-native-safe-area-context";
2525
import { useWorkingTreeDiff } from "@/hooks/use-working-tree-diff";
26+
import { ChunkedMarkdown } from "@/lib/markdown/chunked-markdown";
27+
import { toolVerb } from "@/lib/tool-verbs";
2628
import type { ToolRenderBlock } from "@/lib/transcript-blocks";
2729
import workerPortableAsset from "../../../assets/pierre/worker-portable.pwt";
2830
import PierreView from "./pierre-view";
@@ -290,22 +292,23 @@ export function ToolCallSheetProvider({ children }: { children: ReactNode }) {
290292
</View>
291293
<View className="flex-row items-center gap-2 border-b border-gray-200 px-4 py-3 dark:border-gray-800">
292294
{showing?.kind === "tool" ? (
293-
<>
294-
<ToolChip
295-
name={showing.block.name}
296-
isError={showing.block.status === "failed"}
297-
/>
298-
{showing.block.summary ? (
299-
<Text
300-
numberOfLines={1}
301-
className="flex-1 text-base text-gray-700 dark:text-gray-300"
302-
>
303-
{showing.block.summary}
304-
</Text>
305-
) : (
306-
<View className="flex-1" />
307-
)}
308-
</>
295+
// Same `<verb> <summary>` line as the transcript row (ToolBlock),
296+
// so the sheet header reads as a continuation of the tapped row.
297+
<Text
298+
numberOfLines={1}
299+
className="flex-1 text-base text-gray-700 dark:text-gray-300"
300+
>
301+
<Text
302+
className={
303+
showing.block.status === "failed"
304+
? "text-red-600 dark:text-red-400"
305+
: "text-gray-500 dark:text-gray-400"
306+
}
307+
>
308+
{toolVerb(showing.block.detail)}
309+
</Text>
310+
{showing.block.summary ? ` ${showing.block.summary}` : ""}
311+
</Text>
309312
) : showing?.kind === "gitDiff" ? (
310313
<Text
311314
numberOfLines={1}
@@ -406,9 +409,11 @@ type Descriptor =
406409
| { mode: "native"; node: ReactNode };
407410

408411
/**
409-
* Map a tool block to how its body renders. Diff/code go to the Pierre webview;
410-
* errors, `unknown`, and empty output render natively (plain Text — no
411-
* react-native-diffs). Mirrors the old `DetailBody` dispatch.
412+
* Map a tool block to how its body renders. Diffs and code-shaped output
413+
* (bash/read/grep/glob/monitor) go to the Pierre webview; everything else —
414+
* errors, empty output, markdown-bearing tools (web_fetch/agent via
415+
* ChunkedMarkdown), field-shaped tools (tasks, schedule_wakeup, ask_user),
416+
* and `unknown` — renders natively inside the sheet's ScrollView.
412417
*/
413418
function describeDetail(block: ToolRenderBlock): Descriptor {
414419
const isError = block.status === "failed";
@@ -450,6 +455,7 @@ function describeDetail(block: ToolRenderBlock): Descriptor {
450455
}
451456
case "grep":
452457
case "glob":
458+
if (isError) return errorNative(error ?? "Tool failed.");
453459
return detail.output.length === 0
454460
? NATIVE_EMPTY
455461
: {
@@ -460,15 +466,61 @@ function describeDetail(block: ToolRenderBlock): Descriptor {
460466
noHeader: true,
461467
noLineNumbers: true,
462468
};
469+
case "web_search":
470+
// Result list (title + URL per line) — plain native text, no need
471+
// for the webview's tokenizer or horizontal code scroll.
472+
if (isError) return errorNative(error ?? "Search failed.");
473+
return detail.output.length === 0
474+
? NATIVE_EMPTY
475+
: { mode: "native", node: <FieldText>{detail.output}</FieldText> };
476+
case "monitor": {
477+
// Monitor is a watch command — same command+output body as Bash.
478+
const body = formatBashBody(detail.command, detail.output);
479+
return body.length === 0
480+
? NATIVE_EMPTY
481+
: {
482+
mode: "webview",
483+
kind: "code",
484+
content: body,
485+
name: "command.sh",
486+
noHeader: true,
487+
noLineNumbers: true,
488+
};
489+
}
490+
case "web_fetch":
491+
if (isError) return errorNative(error ?? "Fetch failed.");
492+
return { mode: "native", node: <WebFetchDetail detail={detail} /> };
493+
case "agent":
494+
if (isError) return errorNative(error ?? "Agent failed.");
495+
return { mode: "native", node: <AgentDetail detail={detail} /> };
496+
case "ask_user":
497+
return { mode: "native", node: <AskUserDetail detail={detail} /> };
498+
case "task_create":
499+
return labeledFields([
500+
["Subject", detail.subject],
501+
["Description", detail.description],
502+
["Active form", detail.activeForm],
503+
["Task ID", detail.taskId ? `#${detail.taskId}` : undefined],
504+
]);
505+
case "task_update":
506+
return labeledFields([
507+
["Task ID", `#${detail.taskId}`],
508+
["Status", detail.status],
509+
["Active form", detail.activeForm],
510+
]);
511+
case "task_stop":
512+
return labeledFields([["Task ID", `#${detail.taskId}`]]);
513+
case "schedule_wakeup":
514+
return labeledFields([
515+
["Delay", `${detail.delaySeconds}s`],
516+
["Reason", detail.reason],
517+
["Prompt", detail.prompt],
518+
]);
463519
case "unknown":
464520
return {
465521
mode: "native",
466522
node: <UnknownDetail detail={detail} isError={isError} error={error} />,
467523
};
468-
default:
469-
// Long-tail types not specially rendered in V0 (web_fetch, agent, etc.) —
470-
// render nothing, matching the old switch's implicit fall-through.
471-
return { mode: "native", node: null };
472524
}
473525
}
474526

@@ -546,6 +598,120 @@ function UnknownDetail({
546598
);
547599
}
548600

601+
/** Monospace-ish selectable text block, same chrome as UnknownDetail input. */
602+
function FieldText({ children }: { children: string }) {
603+
return (
604+
<Text
605+
selectable
606+
className="rounded bg-gray-100 p-2 text-xs text-gray-900 dark:bg-gray-900 dark:text-gray-100"
607+
>
608+
{children}
609+
</Text>
610+
);
611+
}
612+
613+
/** Labeled-fields native body; skips empty/undefined values. */
614+
function labeledFields(
615+
fields: Array<[label: string, value: string | undefined]>,
616+
): Descriptor {
617+
const visible = fields.filter(
618+
(f): f is [string, string] => f[1] !== undefined && f[1].length > 0,
619+
);
620+
if (visible.length === 0) return NATIVE_EMPTY;
621+
return {
622+
mode: "native",
623+
node: (
624+
<View>
625+
{visible.map(([label, value], i) => (
626+
<View key={label} className={i > 0 ? "mt-3" : undefined}>
627+
<SectionLabel>{label}</SectionLabel>
628+
<FieldText>{value}</FieldText>
629+
</View>
630+
))}
631+
</View>
632+
),
633+
};
634+
}
635+
636+
function WebFetchDetail({
637+
detail,
638+
}: {
639+
detail: Extract<ToolCallDetail, { type: "web_fetch" }>;
640+
}) {
641+
return (
642+
<View>
643+
<SectionLabel>URL</SectionLabel>
644+
<FieldText>{detail.url}</FieldText>
645+
<View className="mt-3">
646+
<SectionLabel>Prompt</SectionLabel>
647+
<FieldText>{detail.prompt}</FieldText>
648+
</View>
649+
<View className="mt-3">
650+
<SectionLabel>Output</SectionLabel>
651+
{/* WebFetch output is the fetch-side model's prose summary —
652+
markdown, not raw page text. Render it like chat. */}
653+
{detail.output.length > 0 ? (
654+
<ChunkedMarkdown markdown={detail.output} streamDone />
655+
) : (
656+
<EmptyOutput />
657+
)}
658+
</View>
659+
</View>
660+
);
661+
}
662+
663+
function AgentDetail({
664+
detail,
665+
}: {
666+
detail: Extract<ToolCallDetail, { type: "agent" }>;
667+
}) {
668+
return (
669+
<View>
670+
<SectionLabel>Prompt</SectionLabel>
671+
<FieldText>{detail.prompt}</FieldText>
672+
<View className="mt-3">
673+
<SectionLabel>Output</SectionLabel>
674+
{/* The subagent's final text — markdown prose, render like chat.
675+
Its intermediate tool calls live in the subagent JSONL and are
676+
not surfaced here (V0.5+ Background Tasks panel). */}
677+
{detail.output.length > 0 ? (
678+
<ChunkedMarkdown markdown={detail.output} streamDone />
679+
) : (
680+
<EmptyOutput />
681+
)}
682+
</View>
683+
</View>
684+
);
685+
}
686+
687+
function AskUserDetail({
688+
detail,
689+
}: {
690+
detail: Extract<ToolCallDetail, { type: "ask_user" }>;
691+
}) {
692+
return (
693+
<View>
694+
{detail.questions.map((q, i) => (
695+
// biome-ignore lint/suspicious/noArrayIndexKey: questions are positional and static
696+
<View key={i} className={i > 0 ? "mt-3" : undefined}>
697+
<SectionLabel>{q.header || `Question ${i + 1}`}</SectionLabel>
698+
<Text
699+
selectable
700+
className="mb-1 text-sm text-gray-900 dark:text-gray-100"
701+
>
702+
{q.question}
703+
</Text>
704+
<Text className="text-sm text-gray-500 dark:text-gray-400">
705+
{detail.answers?.[i]
706+
? `Answered: ${detail.answers[i]}`
707+
: "(unanswered)"}
708+
</Text>
709+
</View>
710+
))}
711+
</View>
712+
);
713+
}
714+
549715
function SectionLabel({
550716
children,
551717
error,
@@ -566,34 +732,6 @@ function SectionLabel({
566732
);
567733
}
568734

569-
// ─── Shared chip (used by trigger row in tool-block.tsx and sheet header) ──
570-
571-
export function ToolChip({
572-
name,
573-
isError,
574-
}: {
575-
name: string;
576-
isError: boolean;
577-
}) {
578-
return (
579-
<View
580-
className={`rounded-md px-2 py-0.5 ${
581-
isError ? "bg-red-100 dark:bg-red-900" : "bg-blue-100 dark:bg-blue-900"
582-
}`}
583-
>
584-
<Text
585-
className={`text-[11px] font-semibold uppercase tracking-wide ${
586-
isError
587-
? "text-red-700 dark:text-red-200"
588-
: "text-blue-700 dark:text-blue-200"
589-
}`}
590-
>
591-
{name}
592-
</Text>
593-
</View>
594-
);
595-
}
596-
597735
// ─── Helpers ───────────────────────────────────────────────────────────────
598736

599737
/**

0 commit comments

Comments
 (0)