Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 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
bb37f89
fix(server): read turn items through ThreadManagementService and tigh…
maria-rcks Oct 4, 2026
2059d48
style(server): format turn item read
maria-rcks Oct 4, 2026
f6c1363
fix(clients): read and skill rows show fetched output, live rows fetc…
maria-rcks Oct 4, 2026
9cb0abd
fix: hydrate migrated threads before reading a turn item, show mobile…
maria-rcks Oct 4, 2026
d413c53
fix(claude): keep a background bash call's acknowledgement as its output
maria-rcks Oct 4, 2026
f7d7489
fix: failed file edits keep their error for the expanded row
maria-rcks Oct 4, 2026
8b40c02
fix: Pi and OpenCode failed edits keep their error, mobile shows it
maria-rcks Oct 4, 2026
49a9ca0
fix(cursor): failed edits and searches keep their error
maria-rcks Oct 4, 2026
91fde5e
test(cursor): expect errors from failed lint calls
maria-rcks Oct 4, 2026
c62d16c
fix(cursor): label search errors with the searched path
maria-rcks Oct 4, 2026
10af405
fix(opencode): omit empty search results
maria-rcks Oct 4, 2026
33e58e5
fix(server): bound fetched command input
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
45 changes: 43 additions & 2 deletions apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,20 @@ import { cn } from "../../lib/cn";
import { THREAD_WORK_ROW_MIN_HEIGHT, type deriveThreadWorkLogSizing } from "../../lib/layout";
import {
type AgentSpawnSummary,
formatItemFullDetail,
type ThreadFeedActivity,
workEntryRowLabel,
} from "../../lib/threadActivity";
import { turnItemOutputText } from "@t3tools/client-runtime/work-log/item-detail";
import { useTurnItemDetail } from "../../state/queries";
import {
resolveThreadWorkGroupInitialScroll,
shouldFollowThreadWorkGroupAppend,
type ThreadWorkGroupScrollPosition,
} from "./thread-feed-live-follow";
import {
resolveWorkEntryToolPresentation,
toolGroupAction,
type ToolGroupSummaryKind,
workEntryViewedImagePath,
} from "@t3tools/client-runtime/work-log/presentation";
Expand Down Expand Up @@ -788,6 +792,11 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
) {
const { row, expanded } = props;
const navigation = useNavigation();
const fetchedDetail = useTurnItemDetail(
expanded && row.fetchesDetail
? { environmentId: props.environmentId, row: row.projectedItem }
: null,
);
const failureItem = row.projectedItem.item;
if (failureItem.type === "error" && failureItem.status === "failed") {
const warning = failureItem.failure.class === "usage_limit";
Expand Down Expand Up @@ -861,7 +870,26 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
: undefined;
const canExpand = row.canExpand && notifiedSubagentThreadId === undefined;
const reasoning = row.projectedItem.item.type === "reasoning" ? row.projectedItem.item : null;
const fullDetail = expanded && !reasoning ? row.getFullDetail() : null;
const fetchedItem = fetchedDetail.data?.item ?? null;
// Reads keep their path list; the fetched file contents show as output.
const isRead = toolGroupAction(row.workEntry) === "read";
const fullDetail =
expanded && !reasoning
? 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;
const viewedImagePath = workEntryViewedImagePath(row.workEntry);
const toolPresentation = resolveWorkEntryToolPresentation(row.workEntry);
const previewText = workEntryRowLabel(row.workEntry);
Expand Down Expand Up @@ -1004,7 +1032,12 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
</View>
</WorkLogPressable>

{expanded && (reasoning || fullDetail || viewedImagePath || row.workEntry.questionAnswer) ? (
{expanded &&
(reasoning ||
fullDetail ||
fetchedOutput ||
viewedImagePath ||
row.workEntry.questionAnswer) ? (
<Animated.View
entering={WORK_LOG_DETAIL_ENTER_TRANSITION}
exiting={WORK_LOG_DETAIL_EXIT_TRANSITION}
Expand Down Expand Up @@ -1036,6 +1069,14 @@ const ThreadWorkLogRow = memo(function ThreadWorkLogRow(
{fullDetail}
</Text>
)}
{fetchedOutput ? (
<Text
selectable
className="mt-1.5 font-mono text-2xs leading-normal text-foreground-muted"
>
{fetchedOutput}
</Text>
) : null}
</ScrollView>
</Animated.View>
) : null}
Expand Down
30 changes: 30 additions & 0 deletions apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,35 @@ describe("buildThreadFeed", () => {
expect(items[0]).toMatchObject({ output: rawOutput });
});

it("expands tool rows only when they have detail or withheld output", () => {
const items: OrchestrationV2TurnItem[] = [
{ ...command(), input: "", outputOmitted: true },
{
...base("dynamic-empty", "2026-06-20T00:00:03.000Z", 2),
type: "dynamic_tool",
toolName: "example",
input: {},
},
{
...base("read-omitted", "2026-06-20T00:00:04.000Z", 3),
type: "dynamic_tool",
toolName: "Read",
input: { path: "src/env.ts" },
outputOmitted: true,
},
];
const activities = buildThreadFeed(items.map((item, index) => projected(item, index))).flatMap(
(entry) => (entry.type === "activity-group" ? entry.activities : []),
);
expect(
activities.map(({ canExpand, fetchesDetail }) => ({ canExpand, fetchesDetail })),
).toEqual([
{ canExpand: true, fetchesDetail: true },
{ canExpand: false, fetchesDetail: false },
{ canExpand: true, fetchesDetail: true },
]);
});

it("recognizes automation attribution after projecting a user message", () => {
const feed = buildThreadFeed([
projected(
Expand Down Expand Up @@ -1236,6 +1265,7 @@ describe("buildThreadFeed", () => {
summary: `Tool ${id}`,
detail: null,
canExpand: false,
fetchesDetail: false,
getFullDetail: () => null,
getCopyText: () => id,
icon: "command",
Expand Down
49 changes: 33 additions & 16 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import type {
import { turnItemIsWorkspacePreparation } from "@t3tools/client-runtime/state/turn-item-presentation";
import { formatSubagentDisplayTitle } from "@t3tools/client-runtime/state/subagent-display";
import { extractToolActivityPresentation } from "@t3tools/client-runtime/work-log/tool-presentation";
import {
turnItemHasDetail,
turnItemNeedsDetailFetch,
} from "@t3tools/client-runtime/work-log/item-detail";
import {
commandDisplayText,
commandProgramName,
Expand Down Expand Up @@ -74,6 +78,8 @@ export interface ThreadFeedActivity {
readonly summary: string;
readonly detail: string | null;
readonly canExpand: boolean;
/** Expanding fetches the withheld input and output with getTurnItem. */
readonly fetchesDetail: boolean;
readonly getFullDetail: () => string | null;
readonly getCopyText: () => string;
readonly icon:
Expand Down Expand Up @@ -721,6 +727,23 @@ function toWorkLogEntry(
}
}

/** Expanded detail for a row, from its wire item or the full item from getTurnItem. */
export function formatItemFullDetail(
row: OrchestrationV2ProjectedTurnItem,
item: OrchestrationV2TurnItem,
): string {
return JSON.stringify(
{
visibility: row.visibility,
sourceThreadId: row.sourceThreadId,
sourceItemId: row.sourceItemId,
item: toolItemForDisplay(item),
},
null,
2,
);
}

function toFeedActivity(
row: OrchestrationV2ProjectedTurnItem,
attemptId: RunAttemptId | null,
Expand All @@ -735,21 +758,9 @@ function toFeedActivity(
item.type === "dynamic_tool" && toolGroupAction(workEntry) === "read"
? collectToolFilePaths(item)
: null;
const getFullDetail = memoizeValue(() => {
if (readPaths) {
return readPaths.join("\n") || null;
}
return JSON.stringify(
{
visibility: row.visibility,
sourceThreadId: row.sourceThreadId,
sourceItemId: row.sourceItemId,
item: toolItemForDisplay(item),
},
null,
2,
);
});
const getFullDetail = memoizeValue(() =>
readPaths ? readPaths.join("\n") || null : formatItemFullDetail(row, item),
);
const getCopyText = memoizeValue(() =>
[summary, detail, getFullDetail()]
.filter(
Expand All @@ -765,7 +776,13 @@ function toFeedActivity(
attemptId,
summary,
detail,
canExpand: !(item.type === "error" && item.status === "failed") && (readPaths?.length ?? 1) > 0,
canExpand:
!(item.type === "error" && item.status === "failed") &&
(readPaths
? readPaths.length > 0 || turnItemNeedsDetailFetch(item)
: turnItemHasDetail(item) || workEntry.questionAnswer !== undefined),
// Read rows show their paths, then the fetched file contents.
fetchesDetail: turnItemNeedsDetailFetch(item),
getFullDetail,
getCopyText,
icon: workEntry.toolSurface ?? itemIcon(item),
Expand Down
23 changes: 23 additions & 0 deletions apps/mobile/src/state/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { filterComposerPullRequestMatches } from "@t3tools/shared/composerPullRe
import type { VcsRefTarget } from "@t3tools/client-runtime/state/vcs";
import type {
EnvironmentId,
OrchestrationV2ProjectedTurnItem,
ProjectId,
ThreadId,
VcsListRefsResult,
Expand All @@ -14,6 +15,7 @@ import {
} from "@t3tools/client-runtime/state/thread-search";
import { useAtomValue } from "@effect/atom-react";
import * as Cause from "effect/Cause";
import { turnItemDetailRevision } from "@t3tools/client-runtime/work-log/item-detail";
import * as Option from "effect/Option";
import { AsyncResult, Atom } from "effect/unstable/reactivity";
import { useCallback, useEffect, useMemo, useState } from "react";
Expand Down Expand Up @@ -351,3 +353,24 @@ export function useCheckpointDiff(target: CheckpointDiffTarget) {
);
return targets.fullThread === null ? turn : fullThread;
}

/** Full input and output for one tool row; pass null to skip fetching. */
export function useTurnItemDetail(
target: {
readonly environmentId: EnvironmentId;
readonly row: OrchestrationV2ProjectedTurnItem;
} | null,
) {
return useEnvironmentQuery(
target === null
? null
: orchestrationEnvironment.turnItem({
environmentId: target.environmentId,
input: {
threadId: target.row.sourceThreadId,
itemId: target.row.sourceItemId,
revision: turnItemDetailRevision(target.row.item),
},
}),
);
}
64 changes: 64 additions & 0 deletions apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1077,6 +1077,70 @@ const program = Effect.gen(function* () {
status: "completed",
rawInput: { query: "TODO", path: "apps/web" },
},
// Grok backend searches: the query only arrives in the completed rawOutput.
{
sessionUpdate: "tool_call_update",
toolCallId: "grok-x-search",
title: "X search:",
kind: "search",
status: "in_progress",
rawInput: { variant: "XSearch", backend: true },
},
{
sessionUpdate: "tool_call_update",
toolCallId: "grok-x-search",
title: "X search:",
status: "completed",
rawOutput: {
call_id: "xs_call-1",
input: '{"query":"conversation_id:42","limit":"10","mode":"Latest"}',
name: "x_keyword_search",
id: "grok-x-search",
},
},
{
sessionUpdate: "tool_call_update",
toolCallId: "grok-web-search",
title: "Web search:",
kind: "search",
status: "completed",
rawInput: { variant: "WebSearch", backend: true },
rawOutput: {
action: {
type: "search",
query: "t3 code",
sources: [
{ type: "url", url: "https://t3.codes" },
{ type: "url", url: "https://t3.codes" },
{ type: "url", url: "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/pingdotgg/t3code" },
],
},
id: "grok-web-search",
status: "completed",
},
},
{
sessionUpdate: "tool_call_update",
toolCallId: "grok-web-fetch",
title: "Fetch: https://t3.codes",
kind: "fetch",
status: "completed",
rawInput: { variant: "WebFetch", url: "https://t3.codes" },
rawOutput: {
type: "WebFetch",
Content: { url: "https://t3.codes", content: "T3 Code page" },
},
content: [{ type: "content", content: { type: "text", text: "T3 Code page" } }],
},
{
sessionUpdate: "tool_call_update",
toolCallId: "antigravity-shell",
title: "run_command",
kind: "execute",
status: "completed",
rawInput: { command: "cat probe.txt" },
rawOutput: { commandLine: "cat probe.txt", exitCode: 0, combinedOutput: "after\n" },
},
{
sessionUpdate: "compaction_update",
compactionId: "compact-1",
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const RPC_REQUIRED_SCOPES = {
[ORCHESTRATION_V2_WS_METHODS.searchThreads]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getArchivedShellSnapshot]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getThreadProjection]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getTurnItem]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.launchThread]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.subscribeArchivedShell]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.subscribeShell]: AuthOrchestrationReadScope,
Expand Down
35 changes: 35 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1324,6 +1324,12 @@ describe("AcpAdapterV2", () => {
),
{ input: "printf proof", output: "proof" },
);
assert.deepInclude(
items.flatMap((item) =>
item.type === "command_execution" ? [{ input: item.input, output: item.output }] : [],
),
{ input: "cat probe.txt", output: "after\n" },
);
assert.isTrue(
items.some((item) => item.title === "Action required" && item.status === "waiting"),
);
Expand All @@ -1345,6 +1351,35 @@ describe("AcpAdapterV2", () => {
search?.type === "file_search" ? { title: search.title, pattern: search.pattern } : null,
{ title: "Searched TODO in web", pattern: "apps/web" },
);
const webItem = (nativeId: string, status: string) => {
const item = items.findLast(
(candidate) =>
candidate.type === "web_search" &&
candidate.status === status &&
candidate.nativeItemRef?.nativeId?.endsWith(nativeId) === true,
);
return item?.type === "web_search"
? { title: item.title, patterns: item.patterns, results: item.results }
: null;
};
assert.deepEqual(webItem("grok-x-search", "running"), {
title: "X search",
patterns: undefined,
results: undefined,
});
assert.deepEqual(webItem("grok-x-search", "completed"), {
title: "X search: conversation_id:42",
patterns: ["conversation_id:42"],
results: undefined,
});
assert.deepEqual(webItem("grok-web-search", "completed"), {
title: "Web search: t3 code",
patterns: ["t3 code"],
results: [{ url: "https://t3.codes" }, { url: "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/pingdotgg/t3code" }],
});
assert.deepEqual(webItem("grok-web-fetch", "completed")?.results, [
{ url: "https://t3.codes", snippet: "T3 Code page" },
]);
const completedCompaction = items.find(
(item) =>
item.type === "compaction" &&
Expand Down
Loading
Loading