import { describe, expect, it } from "vite-plus/test";import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads"; import { EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadActivity,} from "@t3tools/contracts"; import { buildPendingUserInputAnswers, buildThreadFeed, derivePendingApprovals, deriveThreadFeedPresentation, isPendingUserInputOptionSelected, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type ThreadFeedActivity, type ThreadFeedEntry,} from "./threadActivity"; describe("Codex feedback pseudo-messages", () => { it("keeps pending and completed feedback messages in the mobile thread body", () => { const pending = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:00.000Z", status: "uploading" as const, }; const entries = [codexFeedbackMessage(pending), codexFeedbackMessage(pending, "assistant")].map( (message) => ({ type: "message" as const, id: message.id, createdAt: message.createdAt, message, }), ); expect(deriveThreadFeedPresentation(entries, null, new Set())).toEqual(entries); expect(entries[1]?.message.text).toBe("Sending feedback to OpenAI..."); const completed = codexFeedbackMessage( { ...pending, status: "sent", feedbackId: "codex-thread-1" }, "assistant", ); expect(completed.text).toContain("codex-thread-1"); });}); const singleSelectQuestion = { id: "runtime", header: "Runtime", question: "Which runtime should be used?", options: [ { label: "Go", description: "One binary" }, { label: "Node.js", description: "Reuse TypeScript" }, ], multiSelect: false,} as const; const multiSelectQuestion = { id: "scope", header: "Scope", question: "Which data should be collected?", options: [ { label: "Orders", description: "Receipts" }, { label: "Listings", description: "Inventory" }, ], multiSelect: true,} as const; describe("pending user input answers", () => { it("replaces single-select options and toggles multi-select options", () => { expect( togglePendingUserInputOptionSelection( singleSelectQuestion, { selectedOptionLabels: ["Go"] }, "Node.js", ), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Node.js"] }); const orders = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Orders"); const ordersAndListings = togglePendingUserInputOptionSelection( multiSelectQuestion, orders, "Listings", ); expect(ordersAndListings).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders", "Listings"], }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, ordersAndListings, "Orders"), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Listings"] }); const paddedOrders = togglePendingUserInputOptionSelection( multiSelectQuestion, undefined, " Orders ", ); expect(paddedOrders).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders"] }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, paddedOrders, " Orders "), ).toEqual({ customAnswer: "" }); }); it("builds array answers for multi-select questions", () => { expect( buildPendingUserInputAnswers([singleSelectQuestion, multiSelectQuestion], { runtime: { selectedOptionLabels: ["Go"] }, scope: { selectedOptionLabels: ["Orders", "Listings"] }, }), ).toEqual({ runtime: "Go", scope: ["Orders", "Listings"], }); }); it("clears selected options while a custom answer is active", () => { expect( setPendingUserInputCustomAnswer( { selectedOptionLabels: ["Orders", "Listings"] }, "Orders first", ), ).toEqual({ customAnswer: "Orders first" }); }); it("matches selected chips against normalized option labels", () => { expect( isPendingUserInputOptionSelected({ selectedOptionLabels: ["Orders"] }, " Orders "), ).toBe(true); expect( isPendingUserInputOptionSelected( { selectedOptionLabels: ["Orders"], customAnswer: "Orders first" }, " Orders ", ), ).toBe(false); });}); describe("pending approvals", () => { it("keeps app access approvals and persistence choices from remote environments", () => { const options = [ { decision: "decline", label: "Decline" }, { decision: "acceptAlways", label: "Always allow Safari" }, { decision: "accept", label: "Approve" }, ]; const activity = makeActivity({ id: EventId.make("approval-safari"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestType: "mcp_elicitation_approval", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, }); expect(derivePendingApprovals([activity])).toEqual([ { requestId: "req-safari", requestKind: "mcp-elicitation", createdAt: "2026-08-24T00:00:00.000Z", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, ]); }); it("removes an app access approval after a remote client rejects it", () => { const requested = makeActivity({ id: EventId.make("approval-safari-open"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestKind: "mcp-elicitation" }, }); const resolved = makeActivity({ id: EventId.make("approval-safari-resolved"), kind: "approval.resolved", summary: "Approval resolved", createdAt: "2026-08-24T00:00:01.000Z", payload: { requestId: "req-safari", decision: "decline" }, }); expect(derivePendingApprovals([requested, resolved])).toEqual([]); });}); function makeActivity( input: Partial<OrchestrationThreadActivity> & Pick<OrchestrationThreadActivity, "id" | "kind" | "summary" | "createdAt">,): OrchestrationThreadActivity { return { tone: "info", payload: {}, turnId: null, ...input, };} function makeThread( input: Partial<OrchestrationThread> & Pick<OrchestrationThread, "id" | "projectId" | "title">,): OrchestrationThread { return { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, ...input, settledOverride: input.settledOverride ?? null, settledAt: input.settledAt ?? null, };} describe("buildThreadFeed", () => { it("keeps older local feedback before newer messages returned by the server", () => { const submission = { id: MessageId.make("feedback-command-ordering"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:01.000Z", status: "sent" as const, feedbackId: "codex-thread-1", }; const laterMessage = { id: MessageId.make("later-server-message"), role: "assistant" as const, text: "Newer server response", turnId: null, createdAt: "2026-08-23T00:00:02.000Z", updatedAt: "2026-08-23T00:00:02.000Z", streaming: false, }; const thread = makeThread({ id: ThreadId.make("thread-feedback-ordering"), projectId: ProjectId.make("project-1"), title: "Feedback ordering", messages: [laterMessage], }); const feed = buildThreadFeed(thread, { localMessages: [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ], }); expect(feed.map((entry) => entry.id)).toEqual([ "feedback-command-ordering", "feedback-command-ordering:feedback", "later-server-message", ]); }); it("keeps historic work entries attributed to their turns", () => { const thread = makeThread({ id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Runtime warning thread", latestTurn: { turnId: TurnId.make("turn-latest"), state: "running", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: null, assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("activity-old"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-old"), payload: { message: "Old warning", }, }), makeActivity({ id: EventId.make("activity-latest"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-latest"), payload: { message: "Latest warning", }, }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", turnId: "turn-old", activities: [{ id: "activity-old", turnId: "turn-old" }], }, { type: "activity-group", turnId: "turn-latest", activities: [{ id: "activity-latest", turnId: "turn-latest" }], }, ]); }); it("collapses matching tool lifecycle rows like desktop", () => { const thread = makeThread({ id: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "Collapsed tools", latestTurn: { turnId: TurnId.make("turn-1"), state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("tool-updated"), kind: "tool.updated", tone: "tool", summary: "Run tests", createdAt: "2026-04-01T00:00:01.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), makeActivity({ id: EventId.make("tool-completed"), kind: "tool.completed", tone: "tool", summary: "Run tests completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), ], }); const feed = buildThreadFeed(thread); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group", }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(1); expect(group.activities[0]).toMatchObject({ id: "tool-completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: "turn-1", summary: "Run tests", detail: "bun run test", canExpand: true, icon: "command", toolLike: true, status: "success", }); expect(group.activities[0]?.getFullDetail()).toBe("/bin/zsh -lc 'bun run test'"); expect(group.activities[0]?.getCopyText()).toBe( "Run tests\nbun run test\n/bin/zsh -lc 'bun run test'", ); }); it("keeps MCP inputs available to expanded mobile work rows", () => { const turnId = TurnId.make("turn-mcp"); const thread = makeThread({ id: ThreadId.make("thread-mcp"), projectId: ProjectId.make("project-1"), title: "Expandable MCP call", latestTurn: { turnId, state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("mcp-completed"), kind: "tool.completed", tone: "tool", summary: "Call repository tool", createdAt: "2026-04-01T00:00:02.000Z", turnId, payload: { title: "Call repository tool", itemType: "mcp_tool_call", detail: "repository.search", status: "completed", data: { item: { server: "repository", tool: "search", arguments: { query: "work log" }, }, }, }, }), ], }); const group = buildThreadFeed(thread)[0]; expect(group).toMatchObject({ type: "activity-group" }); if (!group || group.type !== "activity-group") { return; } expect(group.activities[0]?.icon).toBe("wrench"); expect(group.activities[0]?.getFullDetail()).toContain('"query": "work log"'); expect(group.activities[0]?.getFullDetail()).toContain("repository.search"); }); it("defers large tool output expansion until a work row is opened or copied", () => { let serializedToolOutputs = 0; const activities = Array.from({ length: 5_000 }, (_, index) => makeActivity({ id: EventId.make(`large-tool-${index}`), kind: "tool.completed", tone: "tool", summary: `Tool ${index}`, createdAt: new Date(Date.UTC(2026, 3, 1, 0, 0, index)).toISOString(), payload: { title: `Tool ${index}`, itemType: "mcp_tool_call", status: "completed", data: { item: { toJSON: () => { serializedToolOutputs += 1; return { output: "x".repeat(32_768) }; }, }, }, }, }), ); const thread = makeThread({ id: ThreadId.make("thread-large-tools"), projectId: ProjectId.make("project-1"), title: "Large tools", activities, }); const feed = buildThreadFeed(thread); expect(serializedToolOutputs).toBe(0); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group" }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(5_000); expect(group.activities[0]?.getFullDetail()).toContain('"output"'); expect(serializedToolOutputs).toBe(1); expect(group.activities[0]?.getCopyText()).toContain('"output"'); expect(serializedToolOutputs).toBe(1); }); it("keeps the first and terminal assistant messages visible around settled work", () => { const turnId = TurnId.make("turn-1"); const thread = makeThread({ id: ThreadId.make("thread-3"), projectId: ProjectId.make("project-1"), title: "Folded work",Lines 1–500 of 845. Load more
import { describe, expect, it } from "vite-plus/test"; import { codexFeedbackMessage } from "@t3tools/client-runtime/state/threads"; import { EventId, MessageId, ProjectId, ProviderInstanceId, ThreadId, TurnId, type OrchestrationThread, type OrchestrationThreadActivity, } from "@t3tools/contracts"; import { buildPendingUserInputAnswers, buildThreadFeed, derivePendingApprovals, deriveThreadFeedPresentation, isPendingUserInputOptionSelected, setPendingUserInputCustomAnswer, togglePendingUserInputOptionSelection, type ThreadFeedActivity, type ThreadFeedEntry, } from "./threadActivity"; describe("Codex feedback pseudo-messages", () => { it("keeps pending and completed feedback messages in the mobile thread body", () => { const pending = { id: MessageId.make("feedback-command"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:00.000Z", status: "uploading" as const, }; const entries = [codexFeedbackMessage(pending), codexFeedbackMessage(pending, "assistant")].map( (message) => ({ type: "message" as const, id: message.id, createdAt: message.createdAt, message, }), ); expect(deriveThreadFeedPresentation(entries, null, new Set())).toEqual(entries); expect(entries[1]?.message.text).toBe("Sending feedback to OpenAI..."); const completed = codexFeedbackMessage( { ...pending, status: "sent", feedbackId: "codex-thread-1" }, "assistant", ); expect(completed.text).toContain("codex-thread-1"); }); }); const singleSelectQuestion = { id: "runtime", header: "Runtime", question: "Which runtime should be used?", options: [ { label: "Go", description: "One binary" }, { label: "Node.js", description: "Reuse TypeScript" }, ], multiSelect: false, } as const; const multiSelectQuestion = { id: "scope", header: "Scope", question: "Which data should be collected?", options: [ { label: "Orders", description: "Receipts" }, { label: "Listings", description: "Inventory" }, ], multiSelect: true, } as const; describe("pending user input answers", () => { it("replaces single-select options and toggles multi-select options", () => { expect( togglePendingUserInputOptionSelection( singleSelectQuestion, { selectedOptionLabels: ["Go"] }, "Node.js", ), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Node.js"] }); const orders = togglePendingUserInputOptionSelection(multiSelectQuestion, undefined, "Orders"); const ordersAndListings = togglePendingUserInputOptionSelection( multiSelectQuestion, orders, "Listings", ); expect(ordersAndListings).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders", "Listings"], }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, ordersAndListings, "Orders"), ).toEqual({ customAnswer: "", selectedOptionLabels: ["Listings"] }); const paddedOrders = togglePendingUserInputOptionSelection( multiSelectQuestion, undefined, " Orders ", ); expect(paddedOrders).toEqual({ customAnswer: "", selectedOptionLabels: ["Orders"] }); expect( togglePendingUserInputOptionSelection(multiSelectQuestion, paddedOrders, " Orders "), ).toEqual({ customAnswer: "" }); }); it("builds array answers for multi-select questions", () => { expect( buildPendingUserInputAnswers([singleSelectQuestion, multiSelectQuestion], { runtime: { selectedOptionLabels: ["Go"] }, scope: { selectedOptionLabels: ["Orders", "Listings"] }, }), ).toEqual({ runtime: "Go", scope: ["Orders", "Listings"], }); }); it("clears selected options while a custom answer is active", () => { expect( setPendingUserInputCustomAnswer( { selectedOptionLabels: ["Orders", "Listings"] }, "Orders first", ), ).toEqual({ customAnswer: "Orders first" }); }); it("matches selected chips against normalized option labels", () => { expect( isPendingUserInputOptionSelected({ selectedOptionLabels: ["Orders"] }, " Orders "), ).toBe(true); expect( isPendingUserInputOptionSelected( { selectedOptionLabels: ["Orders"], customAnswer: "Orders first" }, " Orders ", ), ).toBe(false); }); }); describe("pending approvals", () => { it("keeps app access approvals and persistence choices from remote environments", () => { const options = [ { decision: "decline", label: "Decline" }, { decision: "acceptAlways", label: "Always allow Safari" }, { decision: "accept", label: "Approve" }, ]; const activity = makeActivity({ id: EventId.make("approval-safari"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestType: "mcp_elicitation_approval", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, }); expect(derivePendingApprovals([activity])).toEqual([ { requestId: "req-safari", requestKind: "mcp-elicitation", createdAt: "2026-08-24T00:00:00.000Z", detail: "Allow ChatGPT to use Safari?", appName: "Safari", options, }, ]); }); it("removes an app access approval after a remote client rejects it", () => { const requested = makeActivity({ id: EventId.make("approval-safari-open"), kind: "approval.requested", summary: "App access approval requested", createdAt: "2026-08-24T00:00:00.000Z", payload: { requestId: "req-safari", requestKind: "mcp-elicitation" }, }); const resolved = makeActivity({ id: EventId.make("approval-safari-resolved"), kind: "approval.resolved", summary: "Approval resolved", createdAt: "2026-08-24T00:00:01.000Z", payload: { requestId: "req-safari", decision: "decline" }, }); expect(derivePendingApprovals([requested, resolved])).toEqual([]); }); }); function makeActivity( input: Partial<OrchestrationThreadActivity> & Pick<OrchestrationThreadActivity, "id" | "kind" | "summary" | "createdAt">, ): OrchestrationThreadActivity { return { tone: "info", payload: {}, turnId: null, ...input, }; } function makeThread( input: Partial<OrchestrationThread> & Pick<OrchestrationThread, "id" | "projectId" | "title">, ): OrchestrationThread { return { modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, runtimeMode: "full-access", interactionMode: "default", branch: null, worktreePath: null, latestTurn: null, createdAt: "2026-04-01T00:00:00.000Z", updatedAt: "2026-04-01T00:00:00.000Z", archivedAt: null, deletedAt: null, messages: [], proposedPlans: [], activities: [], checkpoints: [], session: null, ...input, settledOverride: input.settledOverride ?? null, settledAt: input.settledAt ?? null, }; } describe("buildThreadFeed", () => { it("keeps older local feedback before newer messages returned by the server", () => { const submission = { id: MessageId.make("feedback-command-ordering"), command: "/feedback The agent stopped early.", createdAt: "2026-08-23T00:00:01.000Z", status: "sent" as const, feedbackId: "codex-thread-1", }; const laterMessage = { id: MessageId.make("later-server-message"), role: "assistant" as const, text: "Newer server response", turnId: null, createdAt: "2026-08-23T00:00:02.000Z", updatedAt: "2026-08-23T00:00:02.000Z", streaming: false, }; const thread = makeThread({ id: ThreadId.make("thread-feedback-ordering"), projectId: ProjectId.make("project-1"), title: "Feedback ordering", messages: [laterMessage], }); const feed = buildThreadFeed(thread, { localMessages: [ codexFeedbackMessage(submission), codexFeedbackMessage(submission, "assistant"), ], }); expect(feed.map((entry) => entry.id)).toEqual([ "feedback-command-ordering", "feedback-command-ordering:feedback", "later-server-message", ]); }); it("keeps historic work entries attributed to their turns", () => { const thread = makeThread({ id: ThreadId.make("thread-1"), projectId: ProjectId.make("project-1"), title: "Runtime warning thread", latestTurn: { turnId: TurnId.make("turn-latest"), state: "running", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: null, assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("activity-old"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-old"), payload: { message: "Old warning", }, }), makeActivity({ id: EventId.make("activity-latest"), kind: "runtime.warning", summary: "Runtime warning", createdAt: "2026-04-01T00:00:03.000Z", turnId: TurnId.make("turn-latest"), payload: { message: "Latest warning", }, }), ], }); const feed = buildThreadFeed(thread); expect(feed).toMatchObject([ { type: "activity-group", turnId: "turn-old", activities: [{ id: "activity-old", turnId: "turn-old" }], }, { type: "activity-group", turnId: "turn-latest", activities: [{ id: "activity-latest", turnId: "turn-latest" }], }, ]); }); it("collapses matching tool lifecycle rows like desktop", () => { const thread = makeThread({ id: ThreadId.make("thread-2"), projectId: ProjectId.make("project-1"), title: "Collapsed tools", latestTurn: { turnId: TurnId.make("turn-1"), state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("tool-updated"), kind: "tool.updated", tone: "tool", summary: "Run tests", createdAt: "2026-04-01T00:00:01.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), makeActivity({ id: EventId.make("tool-completed"), kind: "tool.completed", tone: "tool", summary: "Run tests completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: TurnId.make("turn-1"), payload: { title: "Run tests", itemType: "command_execution", detail: "/bin/zsh -lc 'bun run test'", }, }), ], }); const feed = buildThreadFeed(thread); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group", }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(1); expect(group.activities[0]).toMatchObject({ id: "tool-completed", createdAt: "2026-04-01T00:00:02.000Z", turnId: "turn-1", summary: "Run tests", detail: "bun run test", canExpand: true, icon: "command", toolLike: true, status: "success", }); expect(group.activities[0]?.getFullDetail()).toBe("/bin/zsh -lc 'bun run test'"); expect(group.activities[0]?.getCopyText()).toBe( "Run tests\nbun run test\n/bin/zsh -lc 'bun run test'", ); }); it("keeps MCP inputs available to expanded mobile work rows", () => { const turnId = TurnId.make("turn-mcp"); const thread = makeThread({ id: ThreadId.make("thread-mcp"), projectId: ProjectId.make("project-1"), title: "Expandable MCP call", latestTurn: { turnId, state: "completed", requestedAt: "2026-04-01T00:00:00.000Z", startedAt: "2026-04-01T00:00:01.000Z", completedAt: "2026-04-01T00:00:03.000Z", assistantMessageId: null, }, activities: [ makeActivity({ id: EventId.make("mcp-completed"), kind: "tool.completed", tone: "tool", summary: "Call repository tool", createdAt: "2026-04-01T00:00:02.000Z", turnId, payload: { title: "Call repository tool", itemType: "mcp_tool_call", detail: "repository.search", status: "completed", data: { item: { server: "repository", tool: "search", arguments: { query: "work log" }, }, }, }, }), ], }); const group = buildThreadFeed(thread)[0]; expect(group).toMatchObject({ type: "activity-group" }); if (!group || group.type !== "activity-group") { return; } expect(group.activities[0]?.icon).toBe("wrench"); expect(group.activities[0]?.getFullDetail()).toContain('"query": "work log"'); expect(group.activities[0]?.getFullDetail()).toContain("repository.search"); }); it("defers large tool output expansion until a work row is opened or copied", () => { let serializedToolOutputs = 0; const activities = Array.from({ length: 5_000 }, (_, index) => makeActivity({ id: EventId.make(`large-tool-${index}`), kind: "tool.completed", tone: "tool", summary: `Tool ${index}`, createdAt: new Date(Date.UTC(2026, 3, 1, 0, 0, index)).toISOString(), payload: { title: `Tool ${index}`, itemType: "mcp_tool_call", status: "completed", data: { item: { toJSON: () => { serializedToolOutputs += 1; return { output: "x".repeat(32_768) }; }, }, }, }, }), ); const thread = makeThread({ id: ThreadId.make("thread-large-tools"), projectId: ProjectId.make("project-1"), title: "Large tools", activities, }); const feed = buildThreadFeed(thread); expect(serializedToolOutputs).toBe(0); const group = feed[0]; expect(group).toMatchObject({ type: "activity-group" }); if (!group || group.type !== "activity-group") { return; } expect(group.activities).toHaveLength(5_000); expect(group.activities[0]?.getFullDetail()).toContain('"output"'); expect(serializedToolOutputs).toBe(1); expect(group.activities[0]?.getCopyText()).toContain('"output"'); expect(serializedToolOutputs).toBe(1); }); it("keeps the first and terminal assistant messages visible around settled work", () => { const turnId = TurnId.make("turn-1"); const thread = makeThread({ id: ThreadId.make("thread-3"), projectId: ProjectId.make("project-1"), title: "Folded work",