| 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,7 +64,7 @@ import { |
| 64 | type ThreadFeedActivity, | 64 | type ThreadFeedActivity, |
| 65 | workEntryRowLabel, | 65 | workEntryRowLabel, |
| 66 | } from "../../lib/threadActivity"; | 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 | import { useTurnItemDetail } from "../../state/queries"; | 68 | import { useTurnItemDetail } from "../../state/queries"; |
| 69 | import { | 69 | import { |
| 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 && !reasoning | 949 | expanded && !reasoning && !call |
| 934 | ? fetchedItem && !isRead | 950 | ? 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 = !expanded | 954 | const fetchedOutput = !expanded |
| 939 | ? null | 955 | ? null |
| 940 | : fetchedItem | 956 | : shownItem.type === "file_search" || shownItem.type === "web_search" |
| 941 | ? (turnItemOutputText(fetchedItem) ?? "No output.") | 957 | ? turnItemOutputText(shownItem) |
| 942 | : fetchedDetail.error | 958 | : fetchedItem |
| 943 | ? `Couldn't load output: ${fetchedDetail.error}` | 959 | ? (turnItemOutputText(fetchedItem) ?? "No output.") |
| 944 | : row.fetchesDetail | 960 | : fetchedDetail.error |
| 945 | ? fetchedDetail.data | 961 | ? `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 | <Text | 1164 | <Text |
| 1130 | selectable | 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 | {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"; |
| 7 | import { | 7 | import { |
| 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"; |
| 13 | import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react"; | 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 | import { useTheme } from "../../hooks/useTheme"; | 16 | import { useTheme } from "../../hooks/useTheme"; |
| 17 | import { cn } from "../../lib/utils"; | ||
| 17 | import { resolveDiffThemeName } from "../../lib/diffRendering"; | 18 | import { resolveDiffThemeName } from "../../lib/diffRendering"; |
| 18 | import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting"; | 19 | import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting"; |
| 19 | import { useTurnItemDetail } from "../../state/queries"; | 20 | import { 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 | } |
| 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 | function StructuredValue({ | 62 | function 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 | } |
| 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 | * The item behind a projected row, with the output the timeline withheld | 96 | * 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 !== wireItem | 123 | item !== wireItem |
| ... | @@ -134,29 +130,23 @@ function useFetchedTurnItem( | ... | @@ -134,29 +130,23 @@ function useFetchedTurnItem( |
| 134 | }; | 130 | }; |
| 135 | } | 131 | } |
| 136 | 132 | ||
| 137 | /** Output the timeline withheld, fetched while the row is open. */ | 133 | interface ToolOutputState { |
| 138 | function 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} /> | 140 | function 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 | } |
| 161 | 151 | ||
| 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 | } |
| 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 | export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspectorProps) { | 196 | export 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} |
| 199 | 220 | ||
| 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} |
| 211 | 224 | ||
| 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} |
| 252 | 265 | ||
| 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} |
| 256 | 269 | ||
| 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} |
| 260 | 273 | ||
| 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} |
| 277 | 290 | ||
| 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} |
| 305 | 318 | ||
| 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} | ||
| 317 | 320 | ||
| 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} |
| 369 | 372 | ||
| 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 | } |
| 62 | 62 | ||
| 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. */ |
| 64 | export function formatToolValue(value: unknown): string | null { | 64 | function 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 | } |
| 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 | const LIVE_TURN_ITEM_STATUSES: ReadonlySet<OrchestrationV2TurnItem["status"]> = new Set([ | 120 | const 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 !== undefined | 216 | (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": |