From 93bb18e9310ca1b75fa74b0c55bac5d807782a4f Mon Sep 17 00:00:00 2001 From: Robert Nisipeanu Date: Sun, 20 Sep 2026 20:07:02 +0300 Subject: [PATCH 1/3] perf(mobile): reuse unchanged tool rows during chat sync --- apps/mobile/src/lib/threadActivity.test.ts | 94 ++++++++++++++++++++++ apps/mobile/src/lib/threadActivity.ts | 31 ++++++- 2 files changed, 123 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/lib/threadActivity.test.ts b/apps/mobile/src/lib/threadActivity.test.ts index 5d256f7c6206..de32f83d1cf6 100644 --- a/apps/mobile/src/lib/threadActivity.test.ts +++ b/apps/mobile/src/lib/threadActivity.test.ts @@ -291,6 +291,100 @@ function makeThread( } describe("buildThreadFeed", () => { + it("keeps completed tool rows stable when another activity arrives", () => { + const turnId = TurnId.make("tool-history"); + const started = makeActivity({ + id: EventId.make("tool-start"), + kind: "tool.updated", + summary: "Run checks", + createdAt: "2026-04-01T00:00:01.000Z", + turnId, + payload: { + toolCallId: "checks", + itemType: "command_execution", + data: { command: "vp test" }, + status: "inProgress", + }, + }); + const completed = makeActivity({ + ...started, + id: EventId.make("tool-complete"), + kind: "tool.completed", + createdAt: "2026-04-01T00:00:02.000Z", + payload: { toolCallId: "checks", status: "completed", detail: "All checks passed" }, + }); + const thread = { messages: [], activities: [started, completed] }; + const rows = (activities: ReadonlyArray) => + buildThreadFeed({ ...thread, activities }).flatMap((entry) => + entry.type === "activity-group" ? entry.activities : [], + ); + const previous = rows(thread.activities)[0]!; + const appended = makeActivity({ + id: EventId.make("next-tool"), + kind: "tool.completed", + summary: "Read file", + createdAt: "2026-04-01T00:00:03.000Z", + turnId, + payload: { toolCallId: "read", itemType: "file_read", status: "completed" }, + }); + const next = rows([...thread.activities, appended]); + expect(next).toHaveLength(2); + expect(next[0]).toBe(previous); + expect(next[0]?.getCopyText()).toContain("All checks passed"); + expect(next[0]?.status).toBe("success"); + + const failed = { + ...completed, + payload: { toolCallId: "checks", status: "failed", detail: "Checks failed" }, + }; + const updated = rows([started, failed, appended]); + expect(updated[0]).not.toBe(previous); + expect(updated[0]?.status).toBe("failure"); + expect(updated[0]?.getCopyText()).toContain("Checks failed"); + expect(updated[1]).toBe(next[1]); + }); + + it("recomputes a tool merge when pagination changes its earlier activity", () => { + const completed = makeActivity({ + id: EventId.make("completion"), + kind: "tool.completed", + summary: "Command completed", + createdAt: "2026-04-01T00:00:02.000Z", + payload: { toolCallId: "call", status: "completed", detail: "Done" }, + }); + const started = makeActivity({ + ...completed, + id: EventId.make("start"), + kind: "tool.updated", + createdAt: "2026-04-01T00:00:01.000Z", + payload: { + toolCallId: "call", + data: { command: "original command" }, + status: "inProgress", + }, + }); + const row = (activities: ReadonlyArray) => + buildThreadFeed({ messages: [], activities }).flatMap((entry) => + entry.type === "activity-group" ? entry.activities : [], + )[0]!; + const original = row([started, completed]); + expect(original.workEntry.command).toBe("original command"); + const replaced = row([ + { + ...started, + payload: { + toolCallId: "call", + data: { command: "replacement command" }, + status: "inProgress", + }, + }, + completed, + ]); + expect(replaced.workEntry.command).toBe("replacement command"); + expect(row([completed]).workEntry.command).toBeUndefined(); + expect(row([started, completed]).workEntry.command).toBe("original command"); + }); + it("reuses unchanged feed and presentation rows during an assistant text update", () => { const completedTurnId = TurnId.make("completed-turn"); const activeTurnId = TurnId.make("active-turn"); diff --git a/apps/mobile/src/lib/threadActivity.ts b/apps/mobile/src/lib/threadActivity.ts index 8f000b1b55fc..5972ce954829 100644 --- a/apps/mobile/src/lib/threadActivity.ts +++ b/apps/mobile/src/lib/threadActivity.ts @@ -238,6 +238,17 @@ const activityEntriesCache = new WeakMap< ReadonlyArray, ReadonlyArray> >(); +// Activity arrays change on every tool update; their unchanged rows must not +// repeatedly parse old output or invalidate the rendered work log. +const derivedActivityCache = new WeakMap(); +const mergedWorkEntriesCache = new WeakMap< + DerivedWorkLogEntry, + WeakMap +>(); +const activityEntryCache = new WeakMap< + DerivedWorkLogEntry, + Extract +>(); const messageEntriesCache = new WeakMap< OrchestrationThread["messages"][number], Extract @@ -476,6 +487,8 @@ function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): bool const decodeQuestionAttachmentAnswer = Schema.decodeUnknownOption(UserInputAttachmentAnswerPayload); function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWorkLogEntry { + const cached = derivedActivityCache.get(activity); + if (cached) return cached; const payload = activity.payload && typeof activity.payload === "object" ? (activity.payload as Record) @@ -630,6 +643,7 @@ function toDerivedWorkLogEntry(activity: OrchestrationThreadActivity): DerivedWo if (collapseKey) { entry.collapseKey = collapseKey; } + derivedActivityCache.set(activity, entry); return entry; } @@ -867,6 +881,9 @@ function mergeDerivedWorkLogEntries( previous: DerivedWorkLogEntry, next: DerivedWorkLogEntry, ): DerivedWorkLogEntry { + let byPrevious = mergedWorkEntriesCache.get(next); + const cached = byPrevious?.get(previous); + if (cached) return cached; const changedFiles = mergeChangedFiles(previous.changedFiles, next.changedFiles); const detail = next.detail ?? previous.detail; const viewedImagePath = next.viewedImagePath ?? previous.viewedImagePath; @@ -882,7 +899,7 @@ function mergeDerivedWorkLogEntries( const toolLifecycleStatus = next.toolLifecycleStatus ?? previous.toolLifecycleStatus; const toolCallId = next.toolCallId ?? previous.toolCallId; const toolData = next.toolData ?? previous.toolData; - return { + const merged = { ...previous, ...next, id: previous.id, @@ -903,6 +920,12 @@ function mergeDerivedWorkLogEntries( ...(toolCallId ? { toolCallId } : {}), ...(toolData !== undefined ? { toolData } : {}), }; + if (byPrevious === undefined) { + byPrevious = new WeakMap(); + mergedWorkEntriesCache.set(next, byPrevious); + } + byPrevious.set(previous, merged); + return merged; } function mergeChangedFiles( @@ -2459,6 +2482,8 @@ function getThreadFeedActivityEntries(activities: ReadonlyArray { + const cached = activityEntryCache.get(entry); + if (cached) return cached; const summary = workEntryHeading(entry); const detail = workEntryPreview(entry); const getFullDetail = memoizeValue(() => buildWorkEntryExpandedBody(entry)); @@ -2478,7 +2503,7 @@ function toThreadFeedActivityEntry( }) .join("\n"); }); - return { + const result: Extract = { type: "activity", id: entry.id, createdAt: entry.createdAt, @@ -2499,4 +2524,6 @@ function toThreadFeedActivityEntry( workEntry: entry, }, }; + activityEntryCache.set(entry, result); + return result; } From a51e5e8d57a231d6a9a11cdfc84eaaf856c74c8f Mon Sep 17 00:00:00 2001 From: Robert Nisipeanu Date: Sun, 20 Sep 2026 22:58:29 +0300 Subject: [PATCH 2/3] perf(client): skip irrelevant exit-code scans in tool output --- .../src/work-log/presentation.test.ts | 52 +++++++++++++++++++ .../src/work-log/presentation.ts | 7 +-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/packages/client-runtime/src/work-log/presentation.test.ts b/packages/client-runtime/src/work-log/presentation.test.ts index a0909117ad39..1ae5a9379cf6 100644 --- a/packages/client-runtime/src/work-log/presentation.test.ts +++ b/packages/client-runtime/src/work-log/presentation.test.ts @@ -86,6 +86,58 @@ describe("workEntryIndicatesToolFailure", () => { ).toBe(false); }); + it.each([ + "FILE NOT FOUND", + "No files found", + "ENOENT", + "No such file or directory", + "No such file", + "CommandNotFoundException", + "Command not found", + "Cannot find path 'example'\nbecause it does not exist", + "The term 'example'\nis not recognized", + "Is not recognized as the name of a cmdlet", + "A parameter cannot be found that matches parameter name", + "", + "Exited with exit code\t2", + "Exit with exit code 1", + "Exit code: 9", + "Exit code\n3", + ])("detects an error after a long successful output: %s", (error) => { + const entry = { + ...base, + tone: "tool" as const, + toolLifecycleStatus: "completed", + detail: `${"Synthetic successful output.\n".repeat(512)}${error}`, + }; + expect(workEntryIndicatesToolFailure(entry)).toBe(true); + expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(true); + }); + + it.each([ + "Cannot find path", + "Because it does not exist", + "Is not recognized", + "The term 'example'", + "", + "Exit code: 0", + "Exit code: -1", + "Exit code: 12things", + ])("keeps incomplete or non-failure markers successful: %s", (detail) => { + expect(workEntryIndicatesToolFailure({ ...base, tone: "tool", detail })).toBe(false); + }); + + it("matches paired failure phrases across legacy detail and command fields", () => { + const entry = { + ...base, + tone: "tool" as const, + detail: "Cannot find path 'example'", + command: "Because it does not exist", + }; + expect(workEntryIndicatesToolFailure(entry)).toBe(true); + expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(false); + }); + it("does not treat error text in a command as rendered failure", () => { const entry = { label: "Ran command", diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts index 9f9f049d29fe..6580bd44135c 100644 --- a/packages/client-runtime/src/work-log/presentation.ts +++ b/packages/client-runtime/src/work-log/presentation.ts @@ -409,9 +409,10 @@ function toolDetailTextLooksLikeFailure(text: string): boolean { (normalized.includes("is not recognized") && normalized.includes("the term '")) || normalized.includes("is not recognized as the name of a cmdlet") || normalized.includes("a parameter cannot be found that matches parameter name") || - //i.test(text) || - /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || - /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text) + (normalized.includes("exit code") && + (//i.test(text) || + /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || + /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text))) ); } From c80e30712fb2d2df8fe52017104ce9ff095ab0d8 Mon Sep 17 00:00:00 2001 From: Robert Nisipeanu Date: Sun, 20 Sep 2026 23:02:29 +0300 Subject: [PATCH 3/3] Revert "perf(client): skip irrelevant exit-code scans in tool output" This reverts commit c5e083fa1ae093a19bfb1711b9deb923fe3fe50a. --- .../src/work-log/presentation.test.ts | 52 ------------------- .../src/work-log/presentation.ts | 7 ++- 2 files changed, 3 insertions(+), 56 deletions(-) diff --git a/packages/client-runtime/src/work-log/presentation.test.ts b/packages/client-runtime/src/work-log/presentation.test.ts index 1ae5a9379cf6..a0909117ad39 100644 --- a/packages/client-runtime/src/work-log/presentation.test.ts +++ b/packages/client-runtime/src/work-log/presentation.test.ts @@ -86,58 +86,6 @@ describe("workEntryIndicatesToolFailure", () => { ).toBe(false); }); - it.each([ - "FILE NOT FOUND", - "No files found", - "ENOENT", - "No such file or directory", - "No such file", - "CommandNotFoundException", - "Command not found", - "Cannot find path 'example'\nbecause it does not exist", - "The term 'example'\nis not recognized", - "Is not recognized as the name of a cmdlet", - "A parameter cannot be found that matches parameter name", - "", - "Exited with exit code\t2", - "Exit with exit code 1", - "Exit code: 9", - "Exit code\n3", - ])("detects an error after a long successful output: %s", (error) => { - const entry = { - ...base, - tone: "tool" as const, - toolLifecycleStatus: "completed", - detail: `${"Synthetic successful output.\n".repeat(512)}${error}`, - }; - expect(workEntryIndicatesToolFailure(entry)).toBe(true); - expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(true); - }); - - it.each([ - "Cannot find path", - "Because it does not exist", - "Is not recognized", - "The term 'example'", - "", - "Exit code: 0", - "Exit code: -1", - "Exit code: 12things", - ])("keeps incomplete or non-failure markers successful: %s", (detail) => { - expect(workEntryIndicatesToolFailure({ ...base, tone: "tool", detail })).toBe(false); - }); - - it("matches paired failure phrases across legacy detail and command fields", () => { - const entry = { - ...base, - tone: "tool" as const, - detail: "Cannot find path 'example'", - command: "Because it does not exist", - }; - expect(workEntryIndicatesToolFailure(entry)).toBe(true); - expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(false); - }); - it("does not treat error text in a command as rendered failure", () => { const entry = { label: "Ran command", diff --git a/packages/client-runtime/src/work-log/presentation.ts b/packages/client-runtime/src/work-log/presentation.ts index 6580bd44135c..9f9f049d29fe 100644 --- a/packages/client-runtime/src/work-log/presentation.ts +++ b/packages/client-runtime/src/work-log/presentation.ts @@ -409,10 +409,9 @@ function toolDetailTextLooksLikeFailure(text: string): boolean { (normalized.includes("is not recognized") && normalized.includes("the term '")) || normalized.includes("is not recognized as the name of a cmdlet") || normalized.includes("a parameter cannot be found that matches parameter name") || - (normalized.includes("exit code") && - (//i.test(text) || - /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || - /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text))) + //i.test(text) || + /exit(?:ed)? with exit code\s+[1-9]\d*/i.test(text) || + /exit code\s*[:\s]\s*[1-9]\d*\b/i.test(text) ); }