@@ -23,6 +23,8 @@ import {
2323import { KeyboardController } from "react-native-keyboard-controller" ;
2424import { useSafeAreaInsets } from "react-native-safe-area-context" ;
2525import { useWorkingTreeDiff } from "@/hooks/use-working-tree-diff" ;
26+ import { ChunkedMarkdown } from "@/lib/markdown/chunked-markdown" ;
27+ import { toolVerb } from "@/lib/tool-verbs" ;
2628import type { ToolRenderBlock } from "@/lib/transcript-blocks" ;
2729import workerPortableAsset from "../../../assets/pierre/worker-portable.pwt" ;
2830import 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 */
413418function 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+
549715function 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