| author | |
| committer | |
| log | df4ae529eacfff8db38f2f4b9b767d3717446c8a |
| tree | bde280da1966d73d87694779367dca7639f13155 |
| parent | 1d013c7ca1edc843bd3b5e23fd6573aeeaf9b11d |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | 64 | type ThreadFeedActivity, |
| 65 | 65 | workEntryRowLabel, |
| 66 | 66 | } from "../../lib/threadActivity"; |
| 67 | import { turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail"; | |
| 67 | import { toolCallLines, turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail"; | |
| 68 | 68 | import { useTurnItemDetail } from "../../state/queries"; |
| 69 | 69 | import { |
| 70 | 70 | resolveThreadWorkGroupInitialScroll, |
| ... | ... | @@ -929,23 +929,41 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 929 | 929 | const fetchedItem = fetchedDetail.data?.item ?? null; |
| 930 | 930 | // Reads keep their path list; the fetched file contents show as output. |
| 931 | 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 | 948 | const fullDetail = |
| 933 | expanded && !reasoning | |
| 949 | expanded && !reasoning && !call | |
| 934 | 950 | ? fetchedItem && !isRead |
| 935 | 951 | ? formatItemFullDetail(row.projectedItem, fetchedItem) |
| 936 | 952 | : row.getFullDetail() |
| 937 | 953 | : null; |
| 938 | 954 | const fetchedOutput = !expanded |
| 939 | 955 | ? null |
| 940 | : fetchedItem | |
| 941 | ? (turnItemOutputText(fetchedItem) ?? "No output.") | |
| 942 | : fetchedDetail.error | |
| 943 | ? `Couldn't load output: ${fetchedDetail.error}` | |
| 944 | : row.fetchesDetail | |
| 945 | ? fetchedDetail.data | |
| 946 | ? "Output is no longer available." | |
| 947 | : "Loading output…" | |
| 948 | : null; | |
| 956 | : shownItem.type === "file_search" || shownItem.type === "web_search" | |
| 957 | ? turnItemOutputText(shownItem) | |
| 958 | : fetchedItem | |
| 959 | ? (turnItemOutputText(fetchedItem) ?? "No output.") | |
| 960 | : fetchedDetail.error | |
| 961 | ? `Couldn't load output: ${fetchedDetail.error}` | |
| 962 | : row.fetchesDetail | |
| 963 | ? fetchedDetail.data | |
| 964 | ? "Output is no longer available." | |
| 965 | : "Loading output…" | |
| 966 | : null; | |
| 949 | 967 | const viewedImagePath = workEntryViewedImagePath(row.workEntry); |
| 950 | 968 | const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry); |
| 951 | 969 | const previewText = workEntryRowLabel(row.workEntry); |
| ... | ... | @@ -1091,6 +1109,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 1091 | 1109 | {expanded && |
| 1092 | 1110 | (reasoning || |
| 1093 | 1111 | fullDetail || |
| 1112 | call || | |
| 1094 | 1113 | fetchedOutput || |
| 1095 | 1114 | viewedImagePath || |
| 1096 | 1115 | row.workEntry.questionAnswer) ? ( |
| ... | ... | @@ -1120,19 +1139,43 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow( |
| 1120 | 1139 | > |
| 1121 | 1140 | {reasoning ? ( |
| 1122 | 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 | 1159 | <Text selectable className="font-mono text-2xs leading-normal text-foreground-muted"> |
| 1125 | 1160 | {fullDetail} |
| 1126 | 1161 | </Text> |
| 1127 | )} | |
| 1162 | ) : null} | |
| 1128 | 1163 | {fetchedOutput ? ( |
| 1129 | 1164 | <Text |
| 1130 | 1165 | 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 | 1171 | {fetchedOutput} |
| 1134 | 1172 | </Text> |
| 1135 | 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 | 1179 | </ScrollView> |
| 1137 | 1180 | </Animated.View> |
| 1138 | 1181 | ) : null} |
apps/web/src/components/chat/V2ItemInspector.tsx+60-57| ... | ... | @@ -5,15 +5,16 @@ import type { |
| 5 | 5 | ThreadId, |
| 6 | 6 | } from "@t3tools/contracts"; |
| 7 | 7 | import { |
| 8 | formatToolValue, | |
| 8 | toolCallLines, | |
| 9 | 9 | turnItemDetailRevision, |
| 10 | 10 | turnItemNeedsDetailFetch, |
| 11 | 11 | turnItemOutputText, |
| 12 | 12 | } from "@t3tools/client-runtime/work-log/item-detail"; |
| 13 | 13 | import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react"; |
| 14 | import { memo, type ReactNode, Suspense, use, useMemo } from "react"; | |
| 14 | import { memo, Suspense, use, useMemo } from "react"; | |
| 15 | 15 | |
| 16 | 16 | import { useTheme } from "../../hooks/useTheme"; |
| 17 | import { cn } from "../../lib/utils"; | |
| 17 | 18 | import { resolveDiffThemeName } from "../../lib/diffRendering"; |
| 18 | 19 | import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting"; |
| 19 | 20 | import { useTurnItemDetail } from "../../state/queries"; |
| ... | ... | @@ -55,6 +56,9 @@ function JsonTokens({ text }: { readonly text: string }) { |
| 55 | 56 | ]); |
| 56 | 57 | } |
| 57 | 58 | |
| 59 | const monoClassName = | |
| 60 | "font-mono text-(length:--font-size-code,var(--text-2xs)) leading-relaxed whitespace-pre-wrap break-words select-text"; | |
| 61 | ||
| 58 | 62 | function StructuredValue({ |
| 59 | 63 | value, |
| 60 | 64 | highlightJson = false, |
| ... | ... | @@ -74,7 +78,7 @@ function StructuredValue({ |
| 74 | 78 | }, [highlightJson, text]); |
| 75 | 79 | if (!text) return null; |
| 76 | 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 | 82 | {isJson ? ( |
| 79 | 83 | <RenderErrorBoundary fallback={text}> |
| 80 | 84 | <Suspense fallback={text}> |
| ... | ... | @@ -88,14 +92,6 @@ function StructuredValue({ |
| 88 | 92 | ); |
| 89 | 93 | } |
| 90 | 94 | |
| 91 | function 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 | 96 | * The item behind a projected row, with the output the timeline withheld |
| 101 | 97 | * fetched while the row is open. |
| ... | ... | @@ -121,7 +117,7 @@ function useFetchedTurnItem( |
| 121 | 117 | return { |
| 122 | 118 | item, |
| 123 | 119 | output: { |
| 124 | text: turnItemOutputText(item), | |
| 120 | output: turnItemOutputText(item), | |
| 125 | 121 | pending: item === wireItem && detail.isPending, |
| 126 | 122 | error: |
| 127 | 123 | item !== wireItem |
| ... | ... | @@ -134,29 +130,23 @@ function useFetchedTurnItem( |
| 134 | 130 | }; |
| 135 | 131 | } |
| 136 | 132 | |
| 137 | /** Output the timeline withheld, fetched while the row is open. */ | |
| 138 | function ToolOutput(props: { | |
| 139 | readonly text: string | null; | |
| 133 | interface ToolOutputState { | |
| 134 | readonly output: string | null; | |
| 140 | 135 | readonly pending: boolean; |
| 141 | 136 | readonly error: string | null; |
| 142 | 137 | readonly empty: boolean; |
| 143 | }) { | |
| 144 | const body = props.text ? ( | |
| 145 | <StructuredValue value={props.text} /> | |
| 138 | } | |
| 139 | ||
| 140 | function ToolOutput(props: ToolOutputState) { | |
| 141 | return props.output ? ( | |
| 142 | <div className="max-h-80 overflow-auto text-muted-foreground">{props.output}</div> | |
| 146 | 143 | ) : props.pending ? ( |
| 147 | <p className="text-muted-foreground">Loading output…</p> | |
| 144 | <div className="text-muted-foreground italic">Loading output…</div> | |
| 148 | 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 | 147 | ) : props.empty ? ( |
| 151 | <p className="text-muted-foreground">No output.</p> | |
| 148 | <div className="text-muted-foreground italic">No output.</div> | |
| 152 | 149 | ) : null; |
| 153 | if (body === null) return null; | |
| 154 | return ( | |
| 155 | <div> | |
| 156 | <SectionLabel>Output</SectionLabel> | |
| 157 | {body} | |
| 158 | </div> | |
| 159 | ); | |
| 160 | 150 | } |
| 161 | 151 | |
| 162 | 152 | /** Fetched output for rows that show their own plain text instead of the inspector. */ |
| ... | ... | @@ -166,16 +156,47 @@ export function FetchedToolOutput(props: { |
| 166 | 156 | }) { |
| 167 | 157 | const { output } = useFetchedTurnItem(props.projectedItem, props.environmentId); |
| 168 | 158 | return ( |
| 169 | <div className="mt-2 text-xs"> | |
| 159 | <div className={cn("mt-1.5", monoClassName)}> | |
| 170 | 160 | <ToolOutput {...output} /> |
| 171 | 161 | </div> |
| 172 | 162 | ); |
| 173 | 163 | } |
| 174 | 164 | |
| 165 | /** A tool call's body: the call itself in the foreground, its result muted below. */ | |
| 166 | function 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 | ||
| 175 | 196 | export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspectorProps) { |
| 176 | 197 | const fetched = useFetchedTurnItem(props.projectedItem, props.environmentId); |
| 177 | 198 | const item = fetched.item; |
| 178 | const output = <ToolOutput {...fetched.output} />; | |
| 199 | const outputState = fetched.output; | |
| 179 | 200 | const support = useV2ItemSupport({ |
| 180 | 201 | environmentId: props.environmentId, |
| 181 | 202 | sourceThreadId: props.projectedItem.sourceThreadId, |
| ... | ... | @@ -198,15 +219,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 198 | 219 | ) : null} |
| 199 | 220 | |
| 200 | 221 | {item.type === "command_execution" ? ( |
| 201 | <div className="space-y-2"> | |
| 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> | |
| 222 | <ToolCallBody command={item.input} exitCode={item.exitCode} {...outputState} /> | |
| 210 | 223 | ) : null} |
| 211 | 224 | |
| 212 | 225 | {item.type === "file_change" ? ( |
| ... | ... | @@ -250,16 +263,16 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 250 | 263 | </div> |
| 251 | 264 | ) : null} |
| 252 | 265 | |
| 253 | {item.type === "file_search" && item.pattern?.trim() && !item.results?.length ? ( | |
| 254 | <StructuredValue value={item.pattern} /> | |
| 266 | {item.type === "file_search" && item.pattern?.trim() ? ( | |
| 267 | <div className={cn("text-foreground/85", monoClassName)}>{item.pattern}</div> | |
| 255 | 268 | ) : null} |
| 256 | 269 | |
| 257 | {item.type === "web_search" && item.patterns?.length && !item.results?.length ? ( | |
| 258 | <StructuredValue value={item.patterns.join("\n")} /> | |
| 270 | {item.type === "web_search" && item.patterns?.length ? ( | |
| 271 | <div className={cn("text-foreground/85", monoClassName)}>{item.patterns.join("\n")}</div> | |
| 259 | 272 | ) : null} |
| 260 | 273 | |
| 261 | 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 | 276 | {item.results.map((result) => ( |
| 264 | 277 | <li key={JSON.stringify(result)}> |
| 265 | 278 | <span className="font-mono text-foreground/80"> |
| ... | ... | @@ -276,7 +289,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 276 | 289 | ) : null} |
| 277 | 290 | |
| 278 | 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 | 293 | {item.results.map((result) => { |
| 281 | 294 | const safeHref = resolveExternalWebLinkHref(result.url); |
| 282 | 295 | return ( |
| ... | ... | @@ -303,17 +316,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 303 | 316 | </ul> |
| 304 | 317 | ) : null} |
| 305 | 318 | |
| 306 | {item.type === "dynamic_tool" ? ( | |
| 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} | |
| 319 | {item.type === "dynamic_tool" ? <ToolCallBody args={item.input} {...outputState} /> : null} | |
| 317 | 320 | |
| 318 | 321 | {item.type === "approval_request" ? <StructuredValue value={item.prompt} /> : null} |
| 319 | 322 | {item.type === "user_input_request" ? ( |
| ... | ... | @@ -368,7 +371,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec |
| 368 | 371 | ) : null} |
| 369 | 372 | |
| 370 | 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 | 375 | <p> |
| 373 | 376 | {item.fromProviderInstanceIds.join(", ")} → {item.toProviderInstanceId} |
| 374 | 377 | </p> |
apps/web/src/components/chat/WorkLog.tsx+1-1| ... | ... | @@ -123,7 +123,7 @@ export function WorkLogDetails({ |
| 123 | 123 | kind === "text" |
| 124 | 124 | ? "ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text" |
| 125 | 125 | : kind === "panel" |
| 126 | ? "mt-1 rounded-md bg-muted/40 px-3 py-2" | |
| 126 | ? "mt-0.5 mb-1.5" | |
| 127 | 127 | : "mt-1", |
| 128 | 128 | )} |
| 129 | 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 | 61 | } |
| 62 | 62 | |
| 63 | 63 | /** Formats a tool input or output for display: text blocks as text, the rest as JSON. */ |
| 64 | export function formatToolValue(value: unknown): string | null { | |
| 64 | function formatToolValue(value: unknown): string | null { | |
| 65 | 65 | if (value === undefined || value === null) return null; |
| 66 | 66 | const text = textFromBlocks(value, 0); |
| 67 | 67 | if (text !== null) return text.trim() ? prettyJsonText(text) : null; |
| ... | ... | @@ -75,6 +75,48 @@ export function formatToolValue(value: unknown): string | null { |
| 75 | 75 | return json; |
| 76 | 76 | } |
| 77 | 77 | |
| 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 | */ | |
| 82 | export 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 | ||
| 116 | function toolCallHasLines(lines: ReturnType<typeof toolCallLines>): boolean { | |
| 117 | return lines.command !== null || lines.args !== null || lines.argsText !== null; | |
| 118 | } | |
| 119 | ||
| 78 | 120 | const LIVE_TURN_ITEM_STATUSES: ReadonlySet<OrchestrationV2TurnItem["status"]> = new Set([ |
| 79 | 121 | "idle", |
| 80 | 122 | "pending", |
| ... | ... | @@ -126,6 +168,33 @@ export function turnItemOutputText(item: OrchestrationV2TurnItem): string | null |
| 126 | 168 | return item.output?.trim() ? commandOutputText(item.output) || null : null; |
| 127 | 169 | case "dynamic_tool": |
| 128 | 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 | 198 | default: |
| 130 | 199 | return null; |
| 131 | 200 | } |
| ... | ... | @@ -141,10 +210,10 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean { |
| 141 | 210 | return item.text.trim().length > 0; |
| 142 | 211 | case "command_execution": |
| 143 | 212 | return ( |
| 144 | item.input.trim().length > 0 || | |
| 213 | toolCallLines({ command: item.input }).command !== null || | |
| 145 | 214 | item.outputOmitted === true || |
| 146 | 215 | Boolean(item.output?.trim()) || |
| 147 | item.exitCode !== undefined | |
| 216 | (item.exitCode !== undefined && item.exitCode !== 0) | |
| 148 | 217 | ); |
| 149 | 218 | case "file_change": |
| 150 | 219 | case "checkpoint": |
| ... | ... | @@ -156,7 +225,7 @@ export function turnItemHasDetail(item: OrchestrationV2TurnItem): boolean { |
| 156 | 225 | case "web_search": |
| 157 | 226 | return (item.results?.length ?? 0) > 0 || (item.patterns?.length ?? 0) > 0; |
| 158 | 227 | case "dynamic_tool": |
| 159 | return item.outputOmitted === true || formatToolValue(item.input) !== null; | |
| 228 | return item.outputOmitted === true || toolCallHasLines(toolCallLines({ args: item.input })); | |
| 160 | 229 | case "approval_request": |
| 161 | 230 | return Boolean(item.prompt?.trim()); |
| 162 | 231 | case "user_input_request": |