Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
97a3ad6
feat(server): fetch a tool call's full input and output on demand
maria-rcks Oct 3, 2026
2a286d7
fix(server): providers keep tool queries and results
maria-rcks Oct 3, 2026
9a5035e
fix(clients): expanded tool calls show their output, empty ones don't…
maria-rcks Oct 3, 2026
68bc164
Merge remote-tracking branch 'origin/main' into fix/tool-call-results…
maria-rcks Oct 4, 2026
943c2d2
fix(claude): bash output shows its text instead of the raw result json
maria-rcks Oct 4, 2026
dddedf4
feat(clients): tool calls show the call above a muted result, without…
maria-rcks Oct 4, 2026
bb37f89
fix(server): read turn items through ThreadManagementService and tigh…
maria-rcks Oct 4, 2026
93a2a30
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
775682f
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
2059d48
style(server): format turn item read
maria-rcks Oct 4, 2026
d1da705
style(clients): format tool call display and keep formatToolValue int…
maria-rcks Oct 4, 2026
f6c1363
fix(clients): read and skill rows show fetched output, live rows fetc…
maria-rcks Oct 4, 2026
a43af92
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
9cb0abd
fix: hydrate migrated threads before reading a turn item, show mobile…
maria-rcks Oct 4, 2026
4165d91
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
d413c53
fix(claude): keep a background bash call's acknowledgement as its output
maria-rcks Oct 4, 2026
f06e6c4
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
7ed9ff1
fix(clients): show empty and null tool arguments
maria-rcks Oct 4, 2026
f7d7489
fix: failed file edits keep their error for the expanded row
maria-rcks Oct 4, 2026
0f3bc4f
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
633a467
fix(clients): search rows show their query above results, mobile tool…
maria-rcks Oct 4, 2026
8b40c02
fix: Pi and OpenCode failed edits keep their error, mobile shows it
maria-rcks Oct 4, 2026
bd70f43
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
49a9ca0
fix(cursor): failed edits and searches keep their error
maria-rcks Oct 4, 2026
7f7f5c5
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
2d555ed
test(cursor): expect errors from failed lint calls
maria-rcks Oct 4, 2026
9ea78d4
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
91fde5e
test(cursor): expect errors from failed lint calls
maria-rcks Oct 4, 2026
4fab711
fix(cursor): label search errors with the searched path
maria-rcks Oct 4, 2026
d2d7f2b
Merge branch 'fix/tool-call-results-on-expand' into feat/tool-call-di…
maria-rcks Oct 4, 2026
c62d16c
fix(cursor): label search errors with the searched path
maria-rcks Oct 4, 2026
a19b853
fix(clients): remove the expanded tool body inset
maria-rcks Oct 4, 2026
10af405
fix(opencode): omit empty search results
maria-rcks Oct 4, 2026
70b437d
fix(clients): include empty search result correction
maria-rcks Oct 4, 2026
33e58e5
fix(server): bound fetched command input
maria-rcks Oct 4, 2026
a598f19
fix(clients): include command detail input limit
maria-rcks Oct 4, 2026
c370e39
fix(clients): resolve tool display conflicts with main
maria-rcks Oct 4, 2026
7808fe9
fix(mobile): keep tool call line keys unique
maria-rcks Oct 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 57 additions & 14 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {
type ThreadFeedActivity,
workEntryRowLabel,
} from "../../lib/threadActivity";
import { turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail";
import { toolCallLines, turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail";
import { useTurnItemDetail } from "../../state/queries";
import {
resolveThreadWorkGroupInitialScroll,
Expand Down Expand Up @@ -929,23 +929,41 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
const fetchedItem = fetchedDetail.data?.item ?? null;
// Reads keep their path list; the fetched file contents show as output.
const isRead = toolGroupAction(row.workEntry) === "read";
// Tool calls show the call in the foreground and the result muted below it.
const shownItem = fetchedItem ?? row.projectedItem.item;
const call =
expanded && !isRead && shownItem.type === "command_execution"
? toolCallLines({ command: shownItem.input })
: expanded && !isRead && shownItem.type === "dynamic_tool"
? toolCallLines({ args: shownItem.input })
: expanded && shownItem.type === "file_search"
? toolCallLines({ args: { pattern: shownItem.pattern } })
: expanded && shownItem.type === "web_search"
? toolCallLines({ args: { query: shownItem.patterns?.join(", ") } })
: null;
const failedExitCode =
call && shownItem.type === "command_execution" && shownItem.exitCode
? shownItem.exitCode
: null;
const fullDetail =
expanded && !reasoning
expanded && !reasoning && !call
? fetchedItem && !isRead
? formatItemFullDetail(row.projectedItem, fetchedItem)
: row.getFullDetail()
: null;
const fetchedOutput = !expanded
? null
: fetchedItem
? (turnItemOutputText(fetchedItem) ?? "No output.")
: fetchedDetail.error
? `Couldn't load output: ${fetchedDetail.error}`
: row.fetchesDetail
? fetchedDetail.data
? "Output is no longer available."
: "Loading output…"
: null;
: shownItem.type === "file_search" || shownItem.type === "web_search"
? turnItemOutputText(shownItem)
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
: fetchedItem
? (turnItemOutputText(fetchedItem) ?? "No output.")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
: fetchedDetail.error
? `Couldn't load output: ${fetchedDetail.error}`
: row.fetchesDetail
? fetchedDetail.data
? "Output is no longer available."
: "Loading output…"
: null;
const viewedImagePath = workEntryViewedImagePath(row.workEntry);
const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);
const previewText = workEntryRowLabel(row.workEntry);
Expand Down Expand Up @@ -1091,6 +1109,7 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
{expanded &&
(reasoning ||
fullDetail ||
call ||
fetchedOutput ||
viewedImagePath ||
row.workEntry.questionAnswer) ? (
Expand Down Expand Up @@ -1120,19 +1139,43 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
>
{reasoning ? (
props.renderReasoning(reasoning.text)
) : (
) : call ? (
[
call.command,
...(call.args ?? []).map(([key, value]) => `${key} ${value}`),
call.argsText,
]
.filter((line): line is string => Boolean(line))
.map((line, index) => (
<Text
key={`${index}:${line}`}
selectable
className="font-mono text-2xs leading-normal text-foreground"
>
{line}
</Text>
))
) : fullDetail ? (
<Text selectable className="font-mono text-2xs leading-normal text-foreground-muted">
{fullDetail}
</Text>
)}
) : null}
{fetchedOutput ? (
<Text
selectable
className="mt-1.5 font-mono text-2xs leading-normal text-foreground-muted"
className={cn(
"font-mono text-2xs leading-normal text-foreground-muted",
(!call || call.command || call.args || call.argsText) && "mt-1.5",
)}
>
{fetchedOutput}
</Text>
) : null}
{failedExitCode !== null ? (
<Text className="mt-1.5 font-mono text-2xs leading-normal text-danger-foreground">
exit {failedExitCode}
</Text>
) : null}
</ScrollView>
</Animated.View>
) : null}
Expand Down
117 changes: 60 additions & 57 deletions apps/web/src/components/chat/V2ItemInspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import type {
ThreadId,
} from "@t3tools/contracts";
import {
formatToolValue,
toolCallLines,
turnItemDetailRevision,
turnItemNeedsDetailFetch,
turnItemOutputText,
} from "@t3tools/client-runtime/work-log/item-detail";
import { ExternalLinkIcon, GitBranchIcon, RotateCcwIcon } from "lucide-react";
import { memo, type ReactNode, Suspense, use, useMemo } from "react";
import { memo, Suspense, use, useMemo } from "react";

import { useTheme } from "../../hooks/useTheme";
import { cn } from "../../lib/utils";
import { resolveDiffThemeName } from "../../lib/diffRendering";
import { getSyntaxHighlighterPromise } from "../../lib/syntaxHighlighting";
import { useTurnItemDetail } from "../../state/queries";
Expand Down Expand Up @@ -55,6 +56,9 @@ function JsonTokens({ text }: { readonly text: string }) {
]);
}

const monoClassName =
"font-mono text-(length:--font-size-code,var(--text-2xs)) leading-relaxed whitespace-pre-wrap break-words select-text";

function StructuredValue({
value,
highlightJson = false,
Expand All @@ -74,7 +78,7 @@ function StructuredValue({
}, [highlightJson, text]);
if (!text) return null;
return (
<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">
<pre className={cn("max-h-80 overflow-auto text-muted-foreground", monoClassName)}>
{isJson ? (
<RenderErrorBoundary fallback={text}>
<Suspense fallback={text}>
Expand All @@ -88,14 +92,6 @@ function StructuredValue({
);
}

function SectionLabel({ children }: { readonly children: ReactNode }) {
return (
<p className="mb-1 text-3xs font-medium tracking-wide uppercase text-muted-foreground">
{children}
</p>
);
}

/**
* The item behind a projected row, with the output the timeline withheld
* fetched while the row is open.
Expand All @@ -121,7 +117,7 @@ function useFetchedTurnItem(
return {
item,
output: {
text: turnItemOutputText(item),
output: turnItemOutputText(item),
pending: item === wireItem && detail.isPending,
error:
item !== wireItem
Expand All @@ -134,29 +130,23 @@ function useFetchedTurnItem(
};
}

/** Output the timeline withheld, fetched while the row is open. */
function ToolOutput(props: {
readonly text: string | null;
interface ToolOutputState {
readonly output: string | null;
readonly pending: boolean;
readonly error: string | null;
readonly empty: boolean;
}) {
const body = props.text ? (
<StructuredValue value={props.text} />
}

function ToolOutput(props: ToolOutputState) {
return props.output ? (
<div className="max-h-80 overflow-auto text-muted-foreground">{props.output}</div>
) : props.pending ? (
<p className="text-muted-foreground">Loading output…</p>
<div className="text-muted-foreground italic">Loading output…</div>
) : props.error ? (
<p className="text-destructive">Couldn&apos;t load output: {props.error}</p>
<div className="text-destructive">Couldn&apos;t load output: {props.error}</div>
) : props.empty ? (
<p className="text-muted-foreground">No output.</p>
<div className="text-muted-foreground italic">No output.</div>
) : null;
if (body === null) return null;
return (
<div>
<SectionLabel>Output</SectionLabel>
{body}
</div>
);
}

/** Fetched output for rows that show their own plain text instead of the inspector. */
Expand All @@ -166,16 +156,47 @@ export function FetchedToolOutput(props: {
}) {
const { output } = useFetchedTurnItem(props.projectedItem, props.environmentId);
return (
<div className="mt-2 text-xs">
<div className={cn("mt-1.5", monoClassName)}>
<ToolOutput {...output} />
</div>
);
}

/** A tool call's body: the call itself in the foreground, its result muted below. */
function ToolCallBody(
props: ToolOutputState & {
readonly command?: string;
readonly args?: unknown;
readonly exitCode?: number | undefined;
},
) {
const call = toolCallLines({ command: props.command, args: props.args });
return (
<div className={cn("space-y-1.5", monoClassName)}>
{call.command ? <div className="text-foreground/85">{call.command}</div> : null}
{call.args ? (
<div className="text-foreground/85">
{call.args.map(([key, value]) => (
<div key={key}>
<span className="text-muted-foreground">{key} </span>
{value}
</div>
))}
</div>
) : null}
{call.argsText ? <StructuredValue value={call.argsText} highlightJson /> : null}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<ToolOutput {...props} />
{props.exitCode !== undefined && props.exitCode !== 0 ? (
<div className="text-destructive">exit {props.exitCode}</div>
) : null}
</div>
);
}

export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspectorProps) {
const fetched = useFetchedTurnItem(props.projectedItem, props.environmentId);
const item = fetched.item;
const output = <ToolOutput {...fetched.output} />;
const outputState = fetched.output;
const support = useV2ItemSupport({
environmentId: props.environmentId,
sourceThreadId: props.projectedItem.sourceThreadId,
Expand All @@ -198,15 +219,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
) : null}

{item.type === "command_execution" ? (
<div className="space-y-2">
<StructuredValue value={item.input} highlightJson />
{output}
{item.exitCode !== undefined ? (
<p className={item.exitCode === 0 ? "text-success" : "text-destructive"}>
Process exited with code {item.exitCode}
</p>
) : null}
</div>
<ToolCallBody command={item.input} exitCode={item.exitCode} {...outputState} />
) : null}

{item.type === "file_change" ? (
Expand Down Expand Up @@ -250,16 +263,16 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
</div>
) : null}

{item.type === "file_search" && item.pattern?.trim() && !item.results?.length ? (
<StructuredValue value={item.pattern} />
{item.type === "file_search" && item.pattern?.trim() ? (
<div className={cn("text-foreground/85", monoClassName)}>{item.pattern}</div>
) : null}

{item.type === "web_search" && item.patterns?.length && !item.results?.length ? (
<StructuredValue value={item.patterns.join("\n")} />
{item.type === "web_search" && item.patterns?.length ? (
<div className={cn("text-foreground/85", monoClassName)}>{item.patterns.join("\n")}</div>
) : null}

{item.type === "file_search" && item.results?.length ? (
<ul className="space-y-1 rounded-md border border-border/45 p-2">
<ul className="space-y-1">
{item.results.map((result) => (
<li key={JSON.stringify(result)}>
<span className="font-mono text-foreground/80">
Expand All @@ -276,7 +289,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
) : null}

{item.type === "web_search" && item.results?.length ? (
<ul className="space-y-1.5 rounded-md border border-border/45 p-2">
<ul className="space-y-1.5">
{item.results.map((result) => {
const safeHref = resolveExternalWebLinkHref(result.url);
return (
Expand All @@ -303,17 +316,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
</ul>
) : null}

{item.type === "dynamic_tool" ? (
<div className="space-y-2">
{formatToolValue(item.input) ? (
<div>
<SectionLabel>Input</SectionLabel>
<StructuredValue value={formatToolValue(item.input)} highlightJson />
</div>
) : null}
{output}
</div>
) : null}
{item.type === "dynamic_tool" ? <ToolCallBody args={item.input} {...outputState} /> : null}

{item.type === "approval_request" ? <StructuredValue value={item.prompt} /> : null}
{item.type === "user_input_request" ? (
Expand Down Expand Up @@ -368,7 +371,7 @@ export const V2ItemInspector = memo(function V2ItemInspector(props: V2ItemInspec
) : null}

{item.type === "handoff" ? (
<div className="space-y-1 rounded-md border border-border/45 p-2 text-muted-foreground">
<div className="space-y-1 text-muted-foreground">
<p>
{item.fromProviderInstanceIds.join(", ")} → {item.toProviderInstanceId}
</p>
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/components/chat/WorkLog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ export function WorkLogDetails({
kind === "text"
? "ms-7 flex max-h-96 flex-col gap-3 overflow-auto px-0.5 py-1 select-text"
: kind === "panel"
? "mt-1 rounded-md bg-muted/40 px-3 py-2"
? "mt-0.5 mb-1.5"
: "mt-1",
)}
onClick={(event) => event.stopPropagation()}
Expand Down
Loading
Loading