Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
29 changes: 28 additions & 1 deletion apps/mobile/src/features/threads/thread-work-log.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,13 @@ const ThreadWorkLogRow = function ThreadWorkLogRow(
const fetchedItem = fetchedDetail.data?.item ?? null;
const isRead = toolGroupAction(row.workEntry) === "read";
const shownItem = fetchedItem ?? row.projectedItem.item;
const diffStats =
shownItem.type === "file_change" &&
shownItem.status === "completed" &&
shownItem.additions !== undefined &&
shownItem.deletions !== undefined
? { additions: shownItem.additions, deletions: shownItem.deletions }
: null;
const call =
expanded && !isRead && shownItem.type === "command_execution"
? toolCallLines({ command: shownItem.input })
Expand Down Expand Up @@ -1007,7 +1014,13 @@ const ThreadWorkLogRow = function ThreadWorkLogRow(
const answerPreview = row.workEntry.questionAnswer
? getQuestionAnswerPreview(row.workEntry.questionAnswer)
: null;
const accessiblePreview = [previewText, answerPreview].filter(Boolean).join(": ");
const accessiblePreview = [
previewText,
answerPreview,
diffStats ? `${diffStats.additions} additions, ${diffStats.deletions} deletions` : null,
]
.filter(Boolean)
.join(": ");
const displayText = row.groupStandIn?.label ?? workEntryRowLabel(row.workEntry, expanded);
const isSystemNotice = row.projectedItem.item.type === "system_notice";
const isUsageLimit =
Expand Down Expand Up @@ -1115,6 +1128,20 @@ const ThreadWorkLogRow = function ThreadWorkLogRow(
)}

<View className="shrink-0 flex-row items-center gap-px">
{diffStats ? (
<View
className="mr-1 flex-row gap-1"
accessible
accessibilityLabel={`${diffStats.additions} additions, ${diffStats.deletions} deletions`}
>
<Text className="font-mono text-3xs text-adaptive-emerald-600-400">
+{diffStats.additions.toLocaleString()}
</Text>
<Text className="font-mono text-3xs text-adaptive-rose-600-400">
-{diffStats.deletions.toLocaleString()}
</Text>
</View>
) : null}
{props.copied ? (
<Text className="pr-1 font-t3-medium text-3xs text-adaptive-emerald-600-400">
Copied
Expand Down
45 changes: 45 additions & 0 deletions apps/mobile/src/lib/threadActivity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,28 @@ describe("buildThreadFeed", () => {
return row.activities[0]?.groupStandIn;
};

it.each([
{ driver: "claudeAgent", label: "Check repository status" },
{ driver: "codex", label: "vp check" },
])("uses the completed $driver command label in its stand-in row", ({ driver, label }) => {
const item = {
...command(),
title: "Check repository status",
nativeItemRef: {
driver: ProviderDriverKind.make(driver),
nativeId: "bash-singleton",
strength: "strong" as const,
},
};
const feed = buildThreadFeed([projected(item, 0)]);
const rows = deriveThreadFeedPresentation(feed, null, new Set([runId]));
expect(standIn(rows)?.label).toBe(label);
const activity = rows.flatMap((row) =>
row.type === "activity-group" ? row.activities : [],
)[0];
expect(activity?.workEntry.command).toBe(item.input);
});

it("is its own row, with no group header to open first", () => {
// The second call never finished, so only one call is visible.
for (const items of [[wait()], [wait(), read(running)]]) {
Expand Down Expand Up @@ -2546,6 +2568,29 @@ it("uses a compact reasoning preview and a short expanded heading", () => {
expect(workEntryRowLabel({ ...entry, toolLifecycleStatus: "completed" }, true)).toBe("Thought");
});

it("uses native Claude command descriptions for work-log rows", () => {
const item = {
...command(),
title: "Check repository status",
nativeItemRef: {
driver: ProviderDriverKind.make("claudeAgent"),
nativeId: "bash-1",
strength: "strong" as const,
},
};
const entry = {
id: "bash-1",
createdAt: "2026-09-17T12:00:00Z",
label: "Ran command",
tone: "tool" as const,
command: item.input,
structuredPayload: item,
};
expect(workEntryRowLabel(entry)).toBe("Check repository status");
expect(workEntryRowLabel(entry, true)).toBe("Check repository status");
expect(entry.command).toBe(item.input);
});

it("caps one-line work-log labels for multi-kilobyte commands", () => {
const command = `cd /repo && python3 - <<'PY'\n${"print('x')\n".repeat(400)}PY`;
const entry = {
Expand Down
8 changes: 6 additions & 2 deletions apps/mobile/src/lib/threadActivity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from "@t3tools/client-runtime/work-log/command-label";
import {
contextCompactionLabel,
commandDescription,
liveThoughtLine,
toolItemForDisplay,
workEntryDisplayIndicatesToolFailure,
Expand Down Expand Up @@ -272,7 +273,8 @@ export function workEntryRowLabel(entry: WorkLogPresentationEntry, expanded = fa
return entry.toolLifecycleStatus === "inProgress" ? "Thinking" : "Thought";
const presentation = resolveWorkEntryToolPresentation(entry);
if (presentation) return presentation.displayName;
if (entry.command?.trim()) return compactWorkEntryText(commandDisplayText(entry.command));
if (entry.command?.trim())
return compactWorkEntryText(commandDescription(entry) ?? commandDisplayText(entry.command));
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const action = toolGroupAction(entry);
const isToolRead = action === "read" && entry.itemType === "dynamic_tool";
if (action === "code-search" || action === "search") {
Expand Down Expand Up @@ -875,7 +877,7 @@ function singleToolCallLabel(activity: ThreadFeedActivity, expanded: boolean): s
: compactWorkEntryText(activity.workEntry.detail ?? "") || "Thought";
const presentation = resolveWorkEntryToolPresentation(activity.workEntry, "completed");
if (presentation) return presentation.displayName;
const command = activity.workEntry.command?.trim();
const command = commandDescription(activity.workEntry) ?? activity.workEntry.command?.trim();
return command ? truncate(command, WORK_LOG_LABEL_MAX_LENGTH) : activity.summary;
}

Expand Down Expand Up @@ -1641,6 +1643,8 @@ function liveToolActivitySummary(activity: ThreadFeedActivity, presentTense: boo
if (presentation) return presentation.displayName;
const command = activity.workEntry.command?.trim();
if (command) {
const description = commandDescription(activity.workEntry);
if (description) return compactWorkEntryText(description);
const program = commandProgramName(command);
const verb =
status === "inProgress"
Expand Down
2 changes: 1 addition & 1 deletion apps/server/scripts/acp-mock-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1072,7 +1072,7 @@ const program = Effect.gen(function* () {
],
patch: {
format: "git_patch",
text: "diff --git a/old.ts b/new.ts\nrename from old.ts\nrename to new.ts\n",
text: "diff --git a/old.ts b/new.ts\nrename from old.ts\nrename to new.ts\n@@ -1 +1,2 @@\n-before\n+after\n+extra\n",
},
},
],
Expand Down
130 changes: 130 additions & 0 deletions apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,31 @@ describe("acpToolCallDiffPatch", () => {
]),
);
});

it("keeps every ACP v2 file patch instead of counting only the first block", () => {
const patches = ["@@ -0,0 +1 @@\n+new\n", "@@ -1 +0,0 @@\n-old\n"];
assert.equal(
acpToolCallDiffPatch(
patches.map((text, index) => ({
type: "diff",
changes: [{ operation: "modify", path: `/repo/${index}.txt` }],
patch: { format: "git_patch", text },
})),
),
patches.join("\n"),
);
});

it("does not present partial patches when one ACP file exceeds the diff budget", () => {
const text = (prefix: string) =>
Array.from({ length: 2_000 }, (_, i) => `${prefix} ${i}`).join("\n");
assert.isUndefined(
acpToolCallDiffPatch([
{ type: "diff", path: "/repo/new.txt", oldText: null, newText: "new\n" },
{ type: "diff", path: "/repo/large.txt", oldText: text("old"), newText: text("new") },
]),
);
});
});

describe("ACP continuation ownership", () => {
Expand Down Expand Up @@ -1064,6 +1089,103 @@ describe("AcpAdapterV2", () => {
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("projects Codex ACP v1 creation, edit, and removal counts", () =>
Effect.gen(function* () {
const instanceId = ProviderInstanceId.make("codex-acp-diff-counts");
const path = yield* Path.Path;
type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"];
let handler: Parameters<Runtime["handleSessionUpdate"]>[0] | undefined;
const original = Array.from({ length: 20 }, (_, i) => `original ${i}\n`).join("");
const edited = original.replaceAll("original", "edited");
const snapshots = [
{ oldText: null, newText: original },
{ oldText: original, newText: edited },
{ oldText: edited, newText: "" },
];
const adapter = yield* makeAcpAdapterV2({
instanceId,
selfInvocation: yield* resolveSelfInvocation(),
flavor: {
driver: ACP_TEST_DRIVER,
capabilities: AcpProviderCapabilitiesV2,
makeRuntime: makeMockRuntime({
childProcessSpawner: yield* ChildProcessSpawner.ChildProcessSpawner,
mockAgentPath: yield* path.fromFileUrl(
new URL("../../../scripts/acp-mock-agent.ts", import.meta.url),
),
wrapRuntime: (runtime) => ({
...runtime,
handleSessionUpdate: (next) =>
Effect.sync(() => {
handler = next;
}).pipe(Effect.andThen(runtime.handleSessionUpdate(next))),
prompt: () =>
Effect.gen(function* () {
if (handler === undefined)
return yield* Effect.die("Missing session update handler");
for (const [index, snapshot] of snapshots.entries()) {
yield* handler({
sessionId: "mock-session-1",
update: {
sessionUpdate: "tool_call",
toolCallId: `patch-${index}`,
kind: "edit",
status: "completed",
title: "Changed files",
content: [{ type: "diff", path: "/repo/demo.txt", ...snapshot }],
},
});
}
return { stopReason: "end_turn" as const };
}),
}),
}),
},
});
const threadId = ThreadId.make("codex-acp-diff-counts");
const modelSelection = { instanceId, model: "default" };
const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({
runtimeMode: "full-access",
interactionMode: "default",
cwd: process.cwd(),
});
const runtime = yield* adapter.openSession({
threadId,
providerSessionId: ProviderSessionId.make("codex-acp-diff-counts"),
modelSelection,
runtimePolicy,
});
const providerThread = yield* runtime.ensureThread({
threadId,
modelSelection,
runtimePolicy,
});
yield* runtime.startTurn(
makeTurnInput({
threadId,
providerThread,
instanceId,
runtimePolicy,
now: yield* DateTime.now,
}),
);
const events = yield* runtime.events.pipe(
Stream.takeUntil((event) => event.type === "turn.terminal"),
Stream.runCollect,
);
const counts = events.flatMap((event) =>
event.type === "turn_item.updated" && event.turnItem.type === "file_change"
? [{ additions: event.turnItem.additions, deletions: event.turnItem.deletions }]
: [],
);
assert.deepEqual(counts, [
{ additions: 20, deletions: 0 },
{ additions: 20, deletions: 20 },
{ additions: 0, deletions: 20 },
]);
}).pipe(Effect.provide(layerTest), Effect.scoped),
);

it.effect("persists a bounded subset of streamed root and child tool updates", () =>
Effect.gen(function* () {
const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner;
Expand Down Expand Up @@ -1872,6 +1994,14 @@ describe("AcpAdapterV2", () => {
item.changes[0]?.oldPath === "/workspace/old.ts",
),
);
const edited = items.findLast(
(item) => item.type === "file_change" && item.changes?.[0]?.oldPath === "/workspace/old.ts",
);
assert.equal(edited?.type, "file_change");
if (edited?.type === "file_change") {
assert.equal(edited.additions, 2);
assert.equal(edited.deletions, 1);
}
const read = items.find((item) => item.type === "dynamic_tool" && item.toolName === "Read");
assert.deepEqual(
read?.type === "dynamic_tool" ? { title: read.title, input: read.input } : null,
Expand Down
Loading
Loading