authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-10-04 09:06:24-03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-04 12:06:24+00:00
logdf4ae529eacfff8db38f2f4b9b767d3717446c8a
treebde280da1966d73d87694779367dca7639f13155
parent1d013c7ca1edc843bd3b5e23fd6573aeeaf9b11d
signature Signed by PGP key B5690EEEBB952194

feat(clients): tool calls show the call above a muted result, without cards (#15506)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

4 files changed, 191 insertions(+), 76 deletions(-)

apps/mobile/src/features/threads/thread-work-log.tsx+57-14
...@@ -64,7 +64,7 @@ import {...@@ -64,7 +64,7 @@ import {
64 type ThreadFeedActivity,64 type ThreadFeedActivity,
65 workEntryRowLabel,65 workEntryRowLabel,
66} from "../../lib/threadActivity";66} from "../../lib/threadActivity";
67import { turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail";67import { toolCallLines, turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail";
68import { useTurnItemDetail } from "../../state/queries";68import { useTurnItemDetail } from "../../state/queries";
69import {69import {
70 resolveThreadWorkGroupInitialScroll,70 resolveThreadWorkGroupInitialScroll,
...@@ -929,23 +929,41 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -929,23 +929,41 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
929 const fetchedItem = fetchedDetail.data?.item ?? null;929 const fetchedItem = fetchedDetail.data?.item ?? null;
930 // Reads keep their path list; the fetched file contents show as output.930 // Reads keep their path list; the fetched file contents show as output.
931 const isRead = toolGroupAction(row.workEntry) === "read";931 const isRead = toolGroupAction(row.workEntry) === "read";
932 // Tool calls show the call in the foreground and the result muted below it.
933 const shownItem = fetchedItem ?? row.projectedItem.item;
934 const call =
935 expanded && !isRead && shownItem.type === "command_execution"
936 ? toolCallLines({ command: shownItem.input })
937 : expanded && !isRead && shownItem.type === "dynamic_tool"
938 ? toolCallLines({ args: shownItem.input })
939 : expanded && shownItem.type === "file_search"
940 ? toolCallLines({ args: { pattern: shownItem.pattern } })
941 : expanded && shownItem.type === "web_search"
942 ? toolCallLines({ args: { query: shownItem.patterns?.join(", ") } })
943 : null;
944 const failedExitCode =
945 call && shownItem.type === "command_execution" && shownItem.exitCode
946 ? shownItem.exitCode
947 : null;
932 const fullDetail =948 const fullDetail =
933 expanded && !reasoning949 expanded && !reasoning && !call
934 ? fetchedItem && !isRead950 ? fetchedItem && !isRead
935 ? formatItemFullDetail(row.projectedItem, fetchedItem)951 ? formatItemFullDetail(row.projectedItem, fetchedItem)
936 : row.getFullDetail()952 : row.getFullDetail()
937 : null;953 : null;
938 const fetchedOutput = !expanded954 const fetchedOutput = !expanded
939 ? null955 ? null
940 : fetchedItem956 : shownItem.type === "file_search" || shownItem.type === "web_search"
941 ? (turnItemOutputText(fetchedItem) ?? "No output.")957 ? turnItemOutputText(shownItem)
942 : fetchedDetail.error958 : fetchedItem
943 ? `Couldn't load output: ${fetchedDetail.error}`959 ? (turnItemOutputText(fetchedItem) ?? "No output.")
944 : row.fetchesDetail960 : fetchedDetail.error
945 ? fetchedDetail.data961 ? `Couldn't load output: ${fetchedDetail.error}`
946 ? "Output is no longer available."962 : row.fetchesDetail
947 : "Loading output…"963 ? fetchedDetail.data
948 : null;964 ? "Output is no longer available."
965 : "Loading output…"
966 : null;
949 const viewedImagePath = workEntryViewedImagePath(row.workEntry);967 const viewedImagePath = workEntryViewedImagePath(row.workEntry);
950 const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);968 const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);
951 const previewText = workEntryRowLabel(row.workEntry);969 const previewText = workEntryRowLabel(row.workEntry);
...@@ -1091,6 +1109,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -1091,6 +1109,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
1091 {expanded &&1109 {expanded &&
1092 (reasoning ||1110 (reasoning ||
1093 fullDetail ||1111 fullDetail ||
1112 call ||
1094 fetchedOutput ||1113 fetchedOutput ||
1095 viewedImagePath ||1114 viewedImagePath ||
1096 row.workEntry.questionAnswer) ? (1115 row.workEntry.questionAnswer) ? (
...@@ -1120,19 +1139,43 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(...@@ -1120,19 +1139,43 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
1120 >1139 >
1121 {reasoning ? (1140 {reasoning ? (
1122 props.renderReasoning(reasoning.text)1141 props.renderReasoning(reasoning.text)
1123 ) : (1142 ) : call ? (
1143 [
1144 call.command,
1145 ...(call.args ?? []).map(([key, value]) => `${key} ${value}`),
1146 call.argsText,
1147 ]
1148 .filter((line): line is string => Boolean(line))
1149 .map((line, index) => (
1150 <Text
1151 key={`${index}:${line}`}
1152 selectable
1153 className="font-mono text-2xs leading-normal text-foreground"
1154 >
1155 {line}
1156 </Text>
1157 ))
1158 ) : fullDetail ? (
1124 <Text selectable className="font-mono text-2xs leading-normal text-foreground-muted">1159 <Text selectable className="font-mono text-2xs leading-normal text-foreground-muted">
1125 {fullDetail}1160 {fullDetail}
1126 </Text>1161 </Text>
1127 )}1162 ) : null}
1128 {fetchedOutput ? (1163 {fetchedOutput ? (
1129 <Text1164 <Text
1130 selectable1165 selectable
1131 className="mt-1.5 font-mono text-2xs leading-normal text-foreground-muted"1166 className={cn(
1167 "font-mono text-2xs leading-normal text-foreground-muted",
1168 (!call || call.command || call.args || call.argsText) && "mt-1.5",
1169 )}
1132 >1170 >
1133 {fetchedOutput}1171 {fetchedOutput}
1134 </Text>1172 </Text>
1135 ) : null}1173 ) : null}
1174 {failedExitCode !== null ? (
1175 <Text className="mt-1.5 font-mono text-2xs leading-normal text-danger-foreground">
1176 exit {failedExitCode}
1177 </Text>
1178 ) : null}
1136 </ScrollView>1179 </ScrollView>
1137 </Animated.View>1180 </Animated.View>
1138 ) : null}1181 ) : null}
apps/web/src/components/chat/V2ItemInspector.tsx+60-57
...@@ -5,15 +5,16 @@ import type {...@@ -5,15 +5,16 @@ import type {
5 ThreadId,5 ThreadId,
6} from "@t3tools/contracts";6} from "@t3tools/contracts";
7import {7import {
8 formatToolValue,8 toolCallLines,
9 turnItemDetailRevision,9 turnItemDetailRevision,
10 turnItemNeedsDetailFetch,10 turnItemNeedsDetailFetch,
11 turnItemOutputText,11 turnItemOutputText,
12} from "@t3tools/client-runtime/work-log/item-detail";12} from "@t3tools/client-runtime/work-log/item-detail";
13import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react";13import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react";
14import { memo, type ReactNode, Suspense, use, useMemo } from "react";14import { memo, Suspense, use, useMemo } from "react";
1515
16import { useTheme } from "../../hooks/useTheme";16import { useTheme } from "../../hooks/useTheme";
17import { cn } from "../../lib/utils";
17import { resolveDiffThemeName } from "../../lib/diffRendering";18import { resolveDiffThemeName } from "../../lib/diffRendering";
18import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting";19import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting";
19import { useTurnItemDetail } from "../../state/queries";20import { useTurnItemDetail } from "../../state/queries";
...@@ -55,6 +56,9 @@ function JsonTokens({ text }: { readonly text: string }) {...@@ -55,6 +56,9 @@ function JsonTokens({ text }: { readonly text: string }) {
55 ]);56 ]);
56}57}
5758
59const monoClassName =
60 "font-mono text-(length:--font-size-code,var(--text-2xs)) leading-relaxed whitespace-pre-wrap break-words select-text";
61
58function StructuredValue({62function StructuredValue({
59 value,63 value,
60 highlightJson = false,64 highlightJson = false,
...@@ -74,7 +78,7 @@ function StructuredValue({...@@ -74,7 +78,7 @@ function StructuredValue({
74 }, [highlightJson, text]);78 }, [highlightJson, text]);
75 if (!text) return null;79 if (!text) return null;
76 return (80 return (
77 <pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words rounded-md border border-border/50 bg-background/60 p-2 font-mono text-2xs leading-relaxed text-muted-foreground select-text">81 <pre className={cn("max-h-80 overflow-auto text-muted-foreground", monoClassName)}>
78 {isJson ? (82 {isJson ? (
79 <RenderErrorBoundary fallback={text}>83 <RenderErrorBoundary fallback={text}>
80 <Suspense fallback={text}>84 <Suspense fallback={text}>
...@@ -88,14 +92,6 @@ function StructuredValue({...@@ -88,14 +92,6 @@ function StructuredValue({
88 );92 );
89}93}
9094
91function SectionLabel({ children }: { readonly children: ReactNode }) {
92 return (
93 <p className="mb-1 text-3xs font-medium tracking-wide uppercase text-muted-foreground">
94 {children}
95 </p>
96 );
97}
98
99/**95/**
100 * The item behind a projected row, with the output the timeline withheld96 * The item behind a projected row, with the output the timeline withheld
101 * fetched while the row is open.97 * fetched while the row is open.
...@@ -121,7 +117,7 @@ function useFetchedTurnItem(...@@ -121,7 +117,7 @@ function useFetchedTurnItem(
121 return {117 return {
122 item,118 item,
123 output: {119 output: {
124 text: turnItemOutputText(item),120 output: turnItemOutputText(item),
125 pending: item === wireItem && detail.isPending,121 pending: item === wireItem && detail.isPending,
126 error:122 error:
127 item !== wireItem123 item !== wireItem
...@@ -134,29 +130,23 @@ function useFetchedTurnItem(...@@ -134,29 +130,23 @@ function useFetchedTurnItem(
134 };130 };
135}131}
136132
137/** Output the timeline withheld, fetched while the row is open. */133interface ToolOutputState {
138function ToolOutput(props: {134 readonly output: string | null;
139 readonly text: string | null;
140 readonly pending: boolean;135 readonly pending: boolean;
141 readonly error: string | null;136 readonly error: string | null;
142 readonly empty: boolean;137 readonly empty: boolean;
143}) {138}
144 const body = props.text ? (139
145 <StructuredValue value={props.text} />140function ToolOutput(props: ToolOutputState) {
141 return props.output ? (
142 <div className="max-h-80 overflow-auto text-muted-foreground">{props.output}</div>
146 ) : props.pending ? (143 ) : props.pending ? (
147 <p className="text-muted-foreground">Loading output…</p>144 <div className="text-muted-foreground italic">Loading output…</div>
148 ) : props.error ? (145 ) : props.error ? (
149 <p className="text-destructive">Couldn&apos;t load output: {props.error}</p>146 <div className="text-destructive">Couldn&apos;t load output: {props.error}</div>
150 ) : props.empty ? (147 ) : props.empty ? (
151 <p className="text-muted-foreground">No output.</p>148 <div className="text-muted-foreground italic">No output.</div>
152 ) : null;149 ) : null;
153 if (body === null) return null;
154 return (
155 <div>
156 <SectionLabel>Output</SectionLabel>
157 {body}
158 </div>
159 );
160}150}
161151
162/** Fetched output for rows that show their own plain text instead of the inspector. */152/** Fetched output for rows that show their own plain text instead of the inspector. */
...@@ -166,16 +156,47 @@ export function FetchedToolOutput(props: {...@@ -166,16 +156,47 @@ export function FetchedToolOutput(props: {
166}) {156}) {
167 const { output } = useFetchedTurnItem(props.projectedItem, props.environmentId);157 const { output } = useFetchedTurnItem(props.projectedItem, props.environmentId);
168 return (158 return (
169 <div className="mt-2 text-xs">159 <div className={cn("mt-1.5", monoClassName)}>
170 <ToolOutput {...output} />160 <ToolOutput {...output} />
171 </div>161 </div>
172 );162 );
173}163}
174164
165/** A tool call's body: the call itself in the foreground, its result muted below. */
166function ToolCallBody(
167 props: ToolOutputState & {
168 readonly command?: string;
169 readonly args?: unknown;
170 readonly exitCode?: number | undefined;
171 },
172) {
173 const call = toolCallLines({ command: props.command, args: props.args });
174 return (
175 <div className={cn("space-y-1.5", monoClassName)}>
176 {call.command ? <div className="text-foreground/85">{call.command}</div> : null}
177 {call.args ? (
178 <div className="text-foreground/85">
179 {call.args.map(([key, value]) => (
180 <div key={key}>
181 <span className="text-muted-foreground">{key} </span>
182 {value}
183 </div>
184 ))}
185 </div>
186 ) : null}
187 {call.argsText ? <StructuredValue value={call.argsText} highlightJson /> : null}
188 <ToolOutput {...props} />
189 {props.exitCode !== undefined && props.exitCode !== 0 ? (
190 <div className="text-destructive">exit {props.exitCode}</div>
191 ) : null}
192 </div>
193 );
194}
195
175export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspectorProps) {196export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspectorProps) {
176 const fetched = useFetchedTurnItem(props.projectedItem, props.environmentId);197 const fetched = useFetchedTurnItem(props.projectedItem, props.environmentId);
177 const item = fetched.item;198 const item = fetched.item;
178 const output = <ToolOutput {...fetched.output} />;199 const outputState = fetched.output;
179 const support = useV2ItemSupport({200 const support = useV2ItemSupport({
180 environmentId: props.environmentId,201 environmentId: props.environmentId,
181 sourceThreadId: props.projectedItem.sourceThreadId,202 sourceThreadId: props.projectedItem.sourceThreadId,
...@@ -198,15 +219,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec...@@ -198,15 +219,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
198 ) : null}219 ) : null}
199220
200 {item.type === "command_execution" ? (221 {item.type === "command_execution" ? (
201 <div className="space-y-2">222 <ToolCallBody command={item.input} exitCode={item.exitCode} {...outputState} />
202 <StructuredValue value={item.input} highlightJson />
203 {output}
204 {item.exitCode !== undefined ? (
205 <p className={item.exitCode === 0 ? "text-success" : "text-destructive"}>
206 Process exited with code {item.exitCode}
207 </p>
208 ) : null}
209 </div>
210 ) : null}223 ) : null}
211224
212 {item.type === "file_change" ? (225 {item.type === "file_change" ? (
...@@ -250,16 +263,16 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec...@@ -250,16 +263,16 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
250 </div>263 </div>
251 ) : null}264 ) : null}
252265
253 {item.type === "file_search" && item.pattern?.trim() && !item.results?.length ? (266 {item.type === "file_search" && item.pattern?.trim() ? (
254 <StructuredValue value={item.pattern} />267 <div className={cn("text-foreground/85", monoClassName)}>{item.pattern}</div>
255 ) : null}268 ) : null}
256269
257 {item.type === "web_search" && item.patterns?.length && !item.results?.length ? (270 {item.type === "web_search" && item.patterns?.length ? (
258 <StructuredValue value={item.patterns.join("\n")} />271 <div className={cn("text-foreground/85", monoClassName)}>{item.patterns.join("\n")}</div>
259 ) : null}272 ) : null}
260273
261 {item.type === "file_search" && item.results?.length ? (274 {item.type === "file_search" && item.results?.length ? (
262 <ul className="space-y-1 rounded-md border border-border/45 p-2">275 <ul className="space-y-1">
263 {item.results.map((result) => (276 {item.results.map((result) => (
264 <li key={JSON.stringify(result)}>277 <li key={JSON.stringify(result)}>
265 <span className="font-mono text-foreground/80">278 <span className="font-mono text-foreground/80">
...@@ -276,7 +289,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec...@@ -276,7 +289,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
276 ) : null}289 ) : null}
277290
278 {item.type === "web_search" && item.results?.length ? (291 {item.type === "web_search" && item.results?.length ? (
279 <ul className="space-y-1.5 rounded-md border border-border/45 p-2">292 <ul className="space-y-1.5">
280 {item.results.map((result) => {293 {item.results.map((result) => {
281 const safeHref = resolveExternalWebLinkHref(result.url);294 const safeHref = resolveExternalWebLinkHref(result.url);
282 return (295 return (
...@@ -303,17 +316,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec...@@ -303,17 +316,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
303 </ul>316 </ul>
304 ) : null}317 ) : null}
305318
306 {item.type === "dynamic_tool" ? (319 {item.type === "dynamic_tool" ? <ToolCallBody args={item.input} {...outputState} /> : null}
307 <div className="space-y-2">
308 {formatToolValue(item.input) ? (
309 <div>
310 <SectionLabel>Input</SectionLabel>
311 <StructuredValue value={formatToolValue(item.input)} highlightJson />
312 </div>
313 ) : null}
314 {output}
315 </div>
316 ) : null}
317320
318 {item.type === "approval_request" ? <StructuredValue value={item.prompt} /> : null}321 {item.type === "approval_request" ? <StructuredValue value={item.prompt} /> : null}
319 {item.type === "user_input_request" ? (322 {item.type === "user_input_request" ? (
...@@ -368,7 +371,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec...@@ -368,7 +371,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
368 ) : null}371 ) : null}
369372
370 {item.type === "handoff" ? (373 {item.type === "handoff" ? (
371 <div className="space-y-1 rounded-md border border-border/45 p-2 text-muted-foreground">374 <div className="space-y-1 text-muted-foreground">
372 <p>375 <p>
373 {item.fromProviderInstanceIds.join(", ")} → {item.toProviderInstanceId}376 {item.fromProviderInstanceIds.join(", ")} → {item.toProviderInstanceId}
374 </p>377 </p>
apps/web/src/components/chat/WorkLog.tsx+1-1
...@@ -123,7 +123,7 @@ export function WorkLogDetails({...@@ -123,7 +123,7 @@ export function WorkLogDetails({
123 kind === "text"123 kind === "text"
124 ? "ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text"124 ? "ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text"
125 : kind === "panel"125 : kind === "panel"
126 ? "mt-1 rounded-md bg-muted/40 px-3 py-2"126 ? "mt-0.5 mb-1.5"
127 : "mt-1",127 : "mt-1",
128 )}128 )}
129 onClick={(event) => event.stopPropagation()}129 onClick={(event) => event.stopPropagation()}
packages/client-runtime/src/work-log/itemDetail.ts+73-4
...@@ -61,7 +61,7 @@ function prettyJsonText(text: string): string {...@@ -61,7 +61,7 @@ function prettyJsonText(text: string): string {
61}61}
6262
63/** Formats a tool input or output for display: text blocks as text, the rest as JSON. */63/** Formats a tool input or output for display: text blocks as text, the rest as JSON. */
64export function formatToolValue(value: unknown): string | null {64function formatToolValue(value: unknown): string | null {
65 if (value === undefined || value === null) return null;65 if (value === undefined || value === null) return null;
66 const text = textFromBlocks(value, 0);66 const text = textFromBlocks(value, 0);
67 if (text !== null) return text.trim() ? prettyJsonText(text) : null;67 if (text !== null) return text.trim() ? prettyJsonText(text) : null;
...@@ -75,6 +75,48 @@ export function formatToolValue(value: unknown): string | null {...@@ -75,6 +75,48 @@ export function formatToolValue(value: unknown): string | null {
75 return json;75 return json;
76}76}
7777
78/**
79 * The call a tool row's body shows above its result: the full command, the
80 * arguments as `key value` pairs, or formatted text when they are not flat.
81 */
82export function toolCallLines(input: {
83 readonly command?: string | undefined;
84 readonly args?: unknown;
85}): {
86 readonly command: string | null;
87 readonly args: ReadonlyArray<readonly [string, string]> | null;
88 readonly argsText: string | null;
89} {
90 if (input.command !== undefined) {
91 // The row title truncates to its width, so the body always has the full command.
92 const command = input.command.trim();
93 return { command: command || null, args: null, argsText: null };
94 }
95 const args = input.args;
96 if (isRecord(args) && !isSummarizedValue(args)) {
97 const entries = Object.entries(args).flatMap(
98 ([key, value]): Array<readonly [string, string]> => {
99 if (value === undefined) return [];
100 // An empty string or null can be the point of a call (a clear or reset), so show it.
101 return [
102 [
103 key,
104 typeof value === "string" && value !== ""
105 ? value
106 : (JSON.stringify(value) ?? String(value)),
107 ],
108 ];
109 },
110 );
111 return { command: null, args: entries.length > 0 ? entries : null, argsText: null };
112 }
113 return { command: null, args: null, argsText: formatToolValue(args) };
114}
115
116function toolCallHasLines(lines: ReturnType<typeof toolCallLines>): boolean {
117 return lines.command !== null || lines.args !== null || lines.argsText !== null;
118}
119
78const LIVE_TURN_ITEM_STATUSES: ReadonlySet<OrchestrationV2TurnItem["status"]> = new Set([120const LIVE_TURN_ITEM_STATUSES: ReadonlySet<OrchestrationV2TurnItem["status"]> = new Set([
79 "idle",121 "idle",
80 "pending",122 "pending",
...@@ -126,6 +168,33 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null...@@ -126,6 +168,33 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null
126 return item.output?.trim() ? commandOutputText(item.output) || null : null;168 return item.output?.trim() ? commandOutputText(item.output) || null : null;
127 case "dynamic_tool":169 case "dynamic_tool":
128 return item.outputOmitted === true ? null : formatToolValue(item.output);170 return item.outputOmitted === true ? null : formatToolValue(item.output);
171 case "file_search":
172 return item.results?.length
173 ? item.results
174 .map((result) =>
175 [
176 `${result.fileName}${result.line === undefined ? "" : `:${result.line}`}`,
177 result.preview?.trim(),
178 ]
179 .filter(Boolean)
180 .join("\n"),
181 )
182 .join("\n")
183 : null;
184 case "web_search":
185 return item.results?.length
186 ? item.results
187 .map((result) =>
188 [
189 result.title?.trim() || result.url,
190 result.title ? result.url : undefined,
191 result.snippet?.trim(),
192 ]
193 .filter(Boolean)
194 .join("\n"),
195 )
196 .join("\n\n")
197 : null;
129 default:198 default:
130 return null;199 return null;
131 }200 }
...@@ -141,10 +210,10 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean {...@@ -141,10 +210,10 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean {
141 return item.text.trim().length > 0;210 return item.text.trim().length > 0;
142 case "command_execution":211 case "command_execution":
143 return (212 return (
144 item.input.trim().length > 0 ||213 toolCallLines({ command: item.input }).command !== null ||
145 item.outputOmitted === true ||214 item.outputOmitted === true ||
146 Boolean(item.output?.trim()) ||215 Boolean(item.output?.trim()) ||
147 item.exitCode !== undefined216 (item.exitCode !== undefined && item.exitCode !== 0)
148 );217 );
149 case "file_change":218 case "file_change":
150 case "checkpoint":219 case "checkpoint":
...@@ -156,7 +225,7 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean {...@@ -156,7 +225,7 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean {
156 case "web_search":225 case "web_search":
157 return (item.results?.length ?? 0) > 0 || (item.patterns?.length ?? 0) > 0;226 return (item.results?.length ?? 0) > 0 || (item.patterns?.length ?? 0) > 0;
158 case "dynamic_tool":227 case "dynamic_tool":
159 return item.outputOmitted === true || formatToolValue(item.input) !== null;228 return item.outputOmitted === true || toolCallHasLines(toolCallLines({ args: item.input }));
160 case "approval_request":229 case "approval_request":
161 return Boolean(item.prompt?.trim());230 return Boolean(item.prompt?.trim());
162 case "user_input_request":231 case "user_input_request":