import { describe, expect, it } from "vite-plus/test"; import { MessageId, TurnId } from "@t3tools/contracts"; import { computeStableMessagesTimelineRows, computeMessageDurationStart, deriveMessagesTimelineRows, normalizeCompactToolLabel, resolveAssistantMessageCopyState, shouldPreserveAssistantLineBreaks, } from "./MessagesTimeline.logic"; describe("shouldPreserveAssistantLineBreaks", () => { it("preserves Claude insight formatting without changing regular markdown", () => { expect( shouldPreserveAssistantLineBreaks( "★ Insight ─────────────────\\nFirst observation\\nSecond observation\\n─────────────────", ), ).toBe(true); expect(shouldPreserveAssistantLineBreaks("A normal\\nmarkdown paragraph")).toBe(false); }); }); describe("computeMessageDurationStart", () => { it("returns message createdAt when there is no preceding user message", () => { const result = computeMessageDurationStart([ { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:05Z", updatedAt: "2026-01-01T00:00:10Z", streaming: false, }, ]); expect(result).toEqual(new Map([["a1", "2026-01-01T00:00:05Z"]])); }); it("uses the user message createdAt for the first assistant response", () => { const result = computeMessageDurationStart([ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, ]); expect(result).toEqual( new Map([ ["u1", "2026-01-01T00:00:00Z"], ["a1", "2026-01-01T00:00:00Z"], ]), ); }); it("uses the previous completed assistant updatedAt for subsequent assistant responses", () => { const result = computeMessageDurationStart([ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, { id: "a2", role: "assistant", createdAt: "2026-01-01T00:00:55Z", updatedAt: "2026-01-01T00:00:55Z", streaming: false, }, ]); expect(result).toEqual( new Map([ ["u1", "2026-01-01T00:00:00Z"], ["a1", "2026-01-01T00:00:00Z"], ["a2", "2026-01-01T00:00:30Z"], ]), ); }); it("does not advance the boundary for a streaming message", () => { const result = computeMessageDurationStart([ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z", updatedAt: "2026-01-01T00:00:40Z", streaming: true, }, { id: "a2", role: "assistant", createdAt: "2026-01-01T00:00:55Z", updatedAt: "2026-01-01T00:00:55Z", streaming: false, }, ]); expect(result).toEqual( new Map([ ["u1", "2026-01-01T00:00:00Z"], ["a1", "2026-01-01T00:00:00Z"], ["a2", "2026-01-01T00:00:00Z"], ]), ); }); it("resets the boundary on a new user message", () => { const result = computeMessageDurationStart([ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, { id: "u2", role: "user", createdAt: "2026-01-01T00:01:00Z", updatedAt: "2026-01-01T00:01:00Z", streaming: false, }, { id: "a2", role: "assistant", createdAt: "2026-01-01T00:01:20Z", updatedAt: "2026-01-01T00:01:20Z", streaming: false, }, ]); expect(result).toEqual( new Map([ ["u1", "2026-01-01T00:00:00Z"], ["a1", "2026-01-01T00:00:00Z"], ["u2", "2026-01-01T00:01:00Z"], ["a2", "2026-01-01T00:01:00Z"], ]), ); }); it("handles system messages without affecting the boundary", () => { const result = computeMessageDurationStart([ { id: "u1", role: "user", createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, { id: "s1", role: "system", createdAt: "2026-01-01T00:00:01Z", updatedAt: "2026-01-01T00:00:01Z", streaming: false, }, { id: "a1", role: "assistant", createdAt: "2026-01-01T00:00:30Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, ]); expect(result).toEqual( new Map([ ["u1", "2026-01-01T00:00:00Z"], ["s1", "2026-01-01T00:00:00Z"], ["a1", "2026-01-01T00:00:00Z"], ]), ); }); it("returns empty map for empty input", () => { expect(computeMessageDurationStart([])).toEqual(new Map()); }); }); describe("normalizeCompactToolLabel", () => { it("removes trailing completion wording from command labels", () => { expect(normalizeCompactToolLabel("Ran command complete")).toBe("Ran command"); }); it("removes trailing completion wording from other labels", () => { expect(normalizeCompactToolLabel("Read file completed")).toBe("Read file"); }); }); describe("resolveAssistantMessageCopyState", () => { it("returns enabled copy state for completed assistant messages", () => { expect( resolveAssistantMessageCopyState({ showCopyButton: true, text: "Ship it", streaming: false, }), ).toEqual({ text: "Ship it", visible: true, }); }); it("hides copy while an assistant message is still streaming", () => { expect( resolveAssistantMessageCopyState({ showCopyButton: true, text: "Still streaming", streaming: true, }), ).toEqual({ text: "Still streaming", visible: false, }); }); it("hides copy for empty completed assistant messages", () => { expect( resolveAssistantMessageCopyState({ showCopyButton: true, text: " ", streaming: false, }), ).toEqual({ text: null, visible: false, }); }); it("hides copy for non-terminal assistant messages", () => { expect( resolveAssistantMessageCopyState({ showCopyButton: false, text: "Interim thought", streaming: false, }), ).toEqual({ text: "Interim thought", visible: false, }); }); it("copies the rendered representation of Codex directives", () => { expect( resolveAssistantMessageCopyState({ showCopyButton: true, text: [ 'Created :codex-file-citation{path="outputs/report.xlsx" purpose="output"}.', "", '::artifact-template{skill_name="artifact-template-hello-world" skill_directory="/Users/test/.codex/skills/artifact-template-hello-world" display_name="Hello World" artifact_kind="document"}', ].join("\n"), streaming: false, }), ).toEqual({ text: "Created [report.xlsx]().\n\nHello World (Document template)", visible: true, }); }); }); describe("deriveMessagesTimelineRows", () => { it("only enables assistant copy for the terminal assistant message in a turn", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "user-1-entry", kind: "message", createdAt: "2026-01-01T00:00:00Z", message: { id: "user-1" as never, role: "user", text: "Write a poem", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, }, { id: "assistant-thought-entry", kind: "message", createdAt: "2026-01-01T00:00:10Z", message: { id: "assistant-thought" as never, role: "assistant", text: "I should ground this first.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:11Z", streaming: false, }, }, { id: "assistant-final-entry", kind: "message", createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-final" as never, role: "assistant", text: "Here is the poem.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, }, ], expandedTurnIds: new Set(["turn-1" as never]), isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const assistantRows = rows.filter( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "assistant", ); expect(assistantRows).toHaveLength(2); expect(assistantRows[0]?.showAssistantCopyButton).toBe(false); expect(assistantRows[1]?.showAssistantCopyButton).toBe(true); }); it("marks only the active assistant turn as streaming for copy controls", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "assistant-one-entry", kind: "message", createdAt: "2026-01-01T00:00:10Z", message: { id: "assistant-one" as never, role: "assistant", text: "Earlier response.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:11Z", streaming: false, }, }, { id: "assistant-two-entry", kind: "message", createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-two" as never, role: "assistant", text: "Active response.", turnId: "turn-2" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, }, ], latestTurn: { turnId: "turn-2" as never, state: "running", startedAt: "2026-01-01T00:00:19Z", completedAt: null, }, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const assistantRows = rows.filter( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "assistant", ); expect(assistantRows[0]?.assistantCopyStreaming).toBe(false); expect(assistantRows[1]?.assistantCopyStreaming).toBe(true); }); it("projects assistant diff summaries and user revert counts onto the affected rows", () => { const assistantTurnDiffSummary = { turnId: "turn-1" as never, completedAt: "2026-01-01T00:00:30Z", assistantMessageId: "assistant-1" as never, checkpointTurnCount: 2, checkpointRef: "checkpoint-1" as never, status: "ready" as const, files: [{ path: "src/index.ts", kind: "modified", additions: 3, deletions: 1 }], }; const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "user-entry", kind: "message", createdAt: "2026-01-01T00:00:00Z", message: { id: "user-1" as never, role: "user", text: "Do the thing", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, }, { id: "assistant-entry", kind: "message", createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-1" as never, role: "assistant", text: "Done", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, }, ], isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map([ ["assistant-1" as never, assistantTurnDiffSummary], ]), revertTurnCountByUserMessageId: new Map([["user-1" as never, 1]]), }); const userRow = rows.find( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "user", ); const assistantRow = rows.find( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "assistant", ); expect(userRow?.revertTurnCount).toBe(1); expect(assistantRow?.assistantTurnDiffSummary).toBe(assistantTurnDiffSummary); }); it("folds the first assistant message and settled work before the terminal response", () => { const timelineEntries = [ { id: "user-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:00Z", message: { id: "user-1" as never, role: "user" as const, text: "Build it", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, }, { id: "assistant-first-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:05Z", message: { id: "assistant-first" as never, role: "assistant" as const, text: "Synthetic deployment checklist\n1. Confirm the deployment is ready.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:05Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, { id: "work-entry-1", kind: "work" as const, createdAt: "2026-01-01T00:00:08Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:08Z", turnId: "turn-1" as never, label: "Ran command", tone: "tool" as const, }, }, { id: "assistant-final-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-final" as never, role: "assistant" as const, text: "Done", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:22Z", streaming: false, }, }, ]; const collapsedRows = deriveMessagesTimelineRows({ timelineEntries, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const foldRow = collapsedRows.find( (row): row is Extract<(typeof collapsedRows)[number], { kind: "turn-fold" }> => row.kind === "turn-fold", ); expect(foldRow?.turnId).toBe("turn-1"); expect(foldRow?.expanded).toBe(false); // User message boundary (00:00:00) → terminal message updatedAt (00:00:22). expect(foldRow?.label).toBe("Worked for 22s"); expect(collapsedRows.map((row) => row.id)).toEqual([ "user-entry", "turn-fold:turn-1", "assistant-final-entry", ]); const expandedRows = deriveMessagesTimelineRows({ timelineEntries, expandedTurnIds: new Set(["turn-1" as never]), isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(expandedRows.map((row) => row.id)).toEqual([ "user-entry", "turn-fold:turn-1", "assistant-first-entry", "work-toggle:work-entry-1", "assistant-final-entry", ]); expect( expandedRows.find((row) => row.kind === "turn-fold" && row.expanded === true), ).toBeDefined(); }); it("folds all assistant messages before the terminal message", () => { const timelineEntries = [ { id: "assistant-first-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:01Z", message: { id: "assistant-first" as never, role: "assistant" as const, text: "The main result is ready.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:01Z", updatedAt: "2026-01-01T00:00:02Z", streaming: false, }, }, { id: "assistant-middle-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:03Z", message: { id: "assistant-middle" as never, role: "assistant" as const, text: "I am checking one more detail.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:03Z", updatedAt: "2026-01-01T00:00:04Z", streaming: false, }, }, { id: "assistant-final-entry", kind: "message" as const, createdAt: "2026-01-01T00:00:05Z", message: { id: "assistant-final" as never, role: "assistant" as const, text: "Verification finished.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:05Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, ]; const rows = deriveMessagesTimelineRows({ timelineEntries, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.id)).toEqual(["turn-fold:turn-1", "assistant-final-entry"]); }); it("derives a sane duration for a steer-superseded turn with one instant commentary message", () => { // A steer ends the previous turn early: its only message completes the // instant it is created, and trailing work entries land after it. The // fold duration must span from the user message that started the turn to // the last entry, not message createdAt → message updatedAt (~0ms). const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "user-entry", kind: "message", createdAt: "2026-01-01T00:00:00Z", message: { id: "user-1" as never, role: "user" as const, text: "do it once more", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }, }, { id: "assistant-commentary-entry", kind: "message", createdAt: "2026-01-01T00:00:09Z", message: { id: "assistant-commentary" as never, role: "assistant" as const, text: "Kicking off call 1.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:09Z", updatedAt: "2026-01-01T00:00:09Z", streaming: false, }, }, { id: "work-entry-1", kind: "work", createdAt: "2026-01-01T00:00:12Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:12Z", turnId: "turn-1" as never, label: "Ran command", tone: "tool" as const, }, }, { id: "steer-user-entry", kind: "message", createdAt: "2026-01-01T00:00:14Z", message: { id: "user-2" as never, role: "user" as const, text: "actually do 15", turnId: null, createdAt: "2026-01-01T00:00:14Z", updatedAt: "2026-01-01T00:00:14Z", streaming: false, }, }, { id: "assistant-next-turn-entry", kind: "message", createdAt: "2026-01-01T00:00:17Z", message: { id: "assistant-next" as never, role: "assistant" as const, text: "One down — adjusting.", turnId: "turn-2" as never, createdAt: "2026-01-01T00:00:17Z", updatedAt: "2026-01-01T00:00:17Z", streaming: true, }, }, ], latestTurn: { turnId: "turn-2" as never, state: "running", startedAt: "2026-01-01T00:00:14Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:14Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const foldRow = rows.find( (row): row is Extract<(typeof rows)[number], { kind: "turn-fold" }> => row.kind === "turn-fold", ); // User message (00:00:00) → trailing work entry (00:00:12). expect(foldRow?.turnId).toBe("turn-1"); expect(foldRow?.label).toBe("Worked for 12s"); }); it("uses latest-turn timings and the stopped label for an interrupted latest turn", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "work-entry-1", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Ran command", tone: "tool" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "interrupted", startedAt: "2026-01-01T00:00:00Z", completedAt: "2026-01-01T00:00:47Z", }, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows).toEqual([ expect.objectContaining({ kind: "turn-fold", turnId: "turn-1", label: "You stopped after 47s", expanded: false, }), ]); }); it("keeps the previous turn folded while a newly sent message awaits its turn", () => { // Right after send, isWorking is true but latestTurn still points at the // previous, settled turn — it must stay folded through that window. const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "work-entry-1", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Ran command", tone: "tool" as const, }, }, { id: "assistant-final-entry", kind: "message", createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-final" as never, role: "assistant", text: "Done", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:22Z", streaming: false, }, }, { id: "user-followup-entry", kind: "message", createdAt: "2026-01-01T00:01:00Z", message: { id: "user-followup" as never, role: "user", text: "yooo", turnId: null, createdAt: "2026-01-01T00:01:00Z", updatedAt: "2026-01-01T00:01:00Z", streaming: false, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "completed", startedAt: "2026-01-01T00:00:00Z", completedAt: "2026-01-01T00:00:22Z", }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:01:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.id)).toEqual([ "turn-fold:turn-1", "assistant-final-entry", "user-followup-entry", "working-indicator-row", ]); const finalRow = rows.find((row) => row.id === "assistant-final-entry"); expect(finalRow?.kind === "message" && finalRow.showAssistantMeta).toBe(true); expect(rows.find((row) => row.kind === "working")).toMatchObject({ showThinking: true }); }); it("does not fold the active in-progress turn", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "assistant-thought-entry", kind: "message", createdAt: "2026-01-01T00:00:05Z", message: { id: "assistant-thought" as never, role: "assistant", text: "Working on it.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:05Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, { id: "work-entry-1", kind: "work", createdAt: "2026-01-01T00:00:08Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:08Z", turnId: "turn-1" as never, label: "Ran command", tone: "tool" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.some((row) => row.kind === "turn-fold")).toBe(false); expect(rows.map((row) => row.id)).toEqual([ "working-indicator-row", "assistant-thought-entry", "work-live:work-entry-1", ]); }); it("keeps adjacent active tool calls in one replacing row", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "completed-command-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "completed-command", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Ran rg", command: "rg toolCall", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "completed" as const, }, }, { id: "completed-edit-entry", kind: "work", createdAt: "2026-01-01T00:00:06Z", entry: { id: "completed-edit", createdAt: "2026-01-01T00:00:06Z", turnId: "turn-1" as never, label: "Edited files", requestKind: "file-change", changedFiles: ["src/one.ts", "src/two.ts"], tone: "tool" as const, toolLifecycleStatus: "completed" as const, }, }, { id: "running-command-entry", kind: "work", createdAt: "2026-01-01T00:00:07Z", entry: { id: "running-command", createdAt: "2026-01-01T00:00:07Z", turnId: "turn-1" as never, label: "Running tests", command: "vp test run", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.kind)).toEqual(["working", "work-live"]); expect(rows.find((row) => row.kind === "working")).toMatchObject({ showThinking: false }); expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ entry: { id: "running-command" }, groupedEntries: [ { id: "completed-command" }, { id: "completed-edit" }, { id: "running-command" }, ], }); }); it("summarizes a tool run after commentary starts a new run", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "completed-command-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "completed-command", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Ran rg", command: "rg toolCall", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "completed" as const, }, }, { id: "assistant-commentary-entry", kind: "message", createdAt: "2026-01-01T00:00:06Z", message: { id: "assistant-commentary" as never, role: "assistant", text: "Checking another thing.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:06Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, { id: "running-command-entry", kind: "work", createdAt: "2026-01-01T00:00:07Z", entry: { id: "running-command", createdAt: "2026-01-01T00:00:07Z", turnId: "turn-1" as never, label: "Running tests", command: "vp test run", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]); expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ hiddenCount: 1, summary: "Ran 1 command", }); }); it("keeps separated in-progress tool runs visible", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "first-running-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "first-running", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Running first command", command: "rg first", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, { id: "assistant-commentary-entry", kind: "message", createdAt: "2026-01-01T00:00:06Z", message: { id: "assistant-commentary" as never, role: "assistant", text: "Starting another command.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:06Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, { id: "second-running-entry", kind: "work", createdAt: "2026-01-01T00:00:07Z", entry: { id: "second-running", createdAt: "2026-01-01T00:00:07Z", turnId: "turn-1" as never, label: "Running second command", command: "rg second", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.kind)).toEqual(["working", "work-live", "message", "work-live"]); expect(rows.filter((row) => row.kind === "work-live").map((row) => row.entry.id)).toEqual([ "first-running", "second-running", ]); }); it("does not revive stale in-progress tools before a fresh send has a turn id", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "stale-running-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "stale-running", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Running stale command", command: "rg stale", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, { id: "user-followup-entry", kind: "message", createdAt: "2026-01-01T00:01:00Z", message: { id: "user-followup" as never, role: "user", text: "continue", turnId: null, createdAt: "2026-01-01T00:01:00Z", updatedAt: "2026-01-01T00:01:00Z", streaming: false, }, }, ], latestTurn: null, isWorking: true, activeTurnStartedAt: "2026-01-01T00:01:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.some((row) => row.kind === "work-live")).toBe(false); }); it("does not revive separated historical task progress", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "stale-progress-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "stale-progress", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Old progress", tone: "thinking" as const, sourceActivityKind: "task.progress" as const, }, }, { id: "assistant-commentary-entry", kind: "message", createdAt: "2026-01-01T00:00:06Z", message: { id: "assistant-commentary" as never, role: "assistant", text: "Starting another command.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:06Z", updatedAt: "2026-01-01T00:00:06Z", streaming: false, }, }, { id: "running-command-entry", kind: "work", createdAt: "2026-01-01T00:00:07Z", entry: { id: "running-command", createdAt: "2026-01-01T00:00:07Z", turnId: "turn-1" as never, label: "Running command", command: "rg current", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "inProgress" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.filter((row) => row.kind === "work-live").map((row) => row.entry.id)).toEqual([ "running-command", ]); }); it("keeps the latest completed tool call live while the turn is running", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "latest-command-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "latest-command", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Ran rg", command: "rg toolCall", requestKind: "command", tone: "tool" as const, toolLifecycleStatus: "completed" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.map((row) => row.kind)).toEqual(["working", "work-live"]); expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ entry: { id: "latest-command" }, groupedEntries: [{ id: "latest-command" }], }); }); it("does not fold the session's running turn when latestTurn regresses", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "previous-work-entry", kind: "work", createdAt: "2026-01-01T00:00:05Z", entry: { id: "previous-work", createdAt: "2026-01-01T00:00:05Z", turnId: "turn-1" as never, label: "Read files", tone: "tool" as const, }, }, { id: "user-followup-entry", kind: "message", createdAt: "2026-01-01T00:01:00Z", message: { id: "user-followup" as never, role: "user", text: "continue", turnId: null, createdAt: "2026-01-01T00:01:00Z", updatedAt: "2026-01-01T00:01:00Z", streaming: false, }, }, { id: "running-work-entry", kind: "work", createdAt: "2026-01-01T00:01:05Z", entry: { id: "running-work", createdAt: "2026-01-01T00:01:05Z", turnId: "turn-2" as never, label: "Searched files", tone: "tool" as const, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "completed", startedAt: "2026-01-01T00:00:00Z", completedAt: "2026-01-01T00:00:25Z", }, runningTurnId: "turn-2" as never, isWorking: true, activeTurnStartedAt: "2026-01-01T00:01:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.filter((row) => row.kind === "turn-fold").map((row) => row.turnId)).toEqual([ "turn-1", ]); expect(rows.map((row) => row.id)).toContain("work-live:running-work-entry"); }); it("only shows assistant metadata on the terminal assistant message", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "assistant-thought-entry", kind: "message", createdAt: "2026-01-01T00:00:10Z", message: { id: "assistant-thought" as never, role: "assistant", text: "Checking first.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:11Z", streaming: false, }, }, { id: "assistant-final-entry", kind: "message", createdAt: "2026-01-01T00:00:20Z", message: { id: "assistant-final" as never, role: "assistant", text: "Done.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:20Z", updatedAt: "2026-01-01T00:00:30Z", streaming: false, }, }, ], expandedTurnIds: new Set(["turn-1" as never]), isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const assistantRows = rows.filter( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "assistant", ); expect(assistantRows.map((row) => row.showAssistantMeta)).toEqual([false, true]); }); it("withholds assistant metadata while the active turn is still in progress", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "assistant-thought-entry", kind: "message", createdAt: "2026-01-01T00:00:10Z", message: { id: "assistant-thought" as never, role: "assistant", text: "Working on it.", turnId: "turn-1" as never, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:11Z", streaming: false, }, }, ], latestTurn: { turnId: "turn-1" as never, state: "running", startedAt: "2026-01-01T00:00:00Z", completedAt: null, }, isWorking: true, activeTurnStartedAt: "2026-01-01T00:00:00Z", turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const assistantRow = rows.find( (row): row is Extract<(typeof rows)[number], { kind: "message" }> => row.kind === "message" && row.message.role === "assistant", ); expect(assistantRow?.showAssistantMeta).toBe(false); expect(assistantRow?.showAssistantCopyButton).toBe(false); expect(rows.find((row) => row.kind === "working")).toMatchObject({ showThinking: false }); }); it.each([ ["tools", "tool", "Used 3 tools"], ["tools and status updates", "info", "Used 2 tools and received 1 update"], ] as const)("expands %s through the same activity group", (_, middleTone, summary) => { const timelineEntries = [ { id: "work-entry-1", kind: "work" as const, createdAt: "2026-01-01T00:00:01Z", entry: { id: "work-1", createdAt: "2026-01-01T00:00:01Z", label: "read", detail: "Reading package.json", tone: "tool" as const, }, }, { id: "work-entry-2", kind: "work" as const, createdAt: "2026-01-01T00:00:02Z", entry: { id: "work-2", createdAt: "2026-01-01T00:00:02Z", label: "Status updated", detail: "Editing MessagesTimeline.tsx", tone: middleTone, }, }, { id: "work-entry-3", kind: "work" as const, createdAt: "2026-01-01T00:00:03Z", entry: { id: "work-3", createdAt: "2026-01-01T00:00:03Z", label: "test", detail: "Running tests", tone: "tool" as const, }, }, ]; const baseInput = { timelineEntries, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }; const collapsedRows = deriveMessagesTimelineRows(baseInput); const expandedRows = deriveMessagesTimelineRows({ ...baseInput, expandedWorkGroupIds: new Set(["work-group:work-entry-1"]), }); expect(collapsedRows.map((row) => row.id)).toEqual(["work-toggle:work-entry-1"]); expect(collapsedRows.find((row) => row.kind === "work-toggle")).toMatchObject({ groupId: "work-group:work-entry-1", hiddenCount: 3, expanded: false, summary, }); expect(expandedRows.map((row) => row.id)).toEqual([ "work-toggle:work-entry-1", "work-1", "work-2", "work-3", ]); expect(expandedRows.find((row) => row.kind === "work-toggle")).toMatchObject({ expanded: true, }); }); it.each([ ["recovered", ["failed", "completed"], false], ["ending in failure", ["completed", "failed"], true], ["failed", ["failed", "failed"], true], ] as const)("uses the final call for %s tool groups", (_, statuses, hasFailure) => { const timelineEntries = statuses.map((status, index) => ({ id: `work-entry-${index}`, kind: "work" as const, createdAt: `2026-01-01T00:00:0${index}Z`, entry: { id: `work-${index}`, createdAt: `2026-01-01T00:00:0${index}Z`, label: "Ran command", tone: "tool" as const, itemType: "command_execution" as const, toolLifecycleStatus: status, }, })); const rows = deriveMessagesTimelineRows({ timelineEntries, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ hiddenCount: 2, hasFailure, }); }); it.each([ ["the later success is hidden", ["failed", "completed", "info"], false], ["the later success is visible", ["failed", "info", "completed"], false], ["an error-toned entry recovers", ["error", "info", "completed"], false], ["the final failure is hidden", ["completed", "failed", "info"], true], ["the final failure is visible", ["failed", "info", "failed"], true], ["the only failure is visible", ["completed", "info", "failed"], true], ] as const)( "uses the final tool call for mixed work groups when %s", (_, statuses, hasFailure) => { const timelineEntries = statuses.map((status, index) => { const id = `work-${index}`; const createdAt = `2026-01-01T00:00:0${index}Z`; return { id: `work-entry-${index}`, kind: "work" as const, createdAt, entry: status === "info" ? { id, createdAt, label: "Status updated", tone: "info" as const } : status === "error" ? { id, createdAt, label: "Command failed", tone: "error" as const } : { id, createdAt, label: "Ran command", tone: "tool" as const, toolLifecycleStatus: status, }, }; }); const rows = deriveMessagesTimelineRows({ timelineEntries, isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ hiddenCount: statuses.some((status) => status === "error") ? 2 : 3, summary: statuses.some((status) => status === "error") ? "Received 1 update and used 1 tool" : "Used 2 tools and received 1 update", hasFailure, }); if (statuses.some((status) => status === "error")) { expect(rows[0]).toMatchObject({ kind: "work", groupedEntries: [{ tone: "error", label: "Command failed" }], }); } }, ); }); describe("computeStableMessagesTimelineRows", () => { it.each(["", " \n"])("replaces Thinking when assistant content grows from %j", (text) => { const startedAt = "2026-01-01T00:00:00Z"; const turnId = TurnId.make("turn-1"); const input = { runningTurnId: turnId, isWorking: true, activeTurnStartedAt: startedAt, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }; const assistantEntry = { id: "assistant-entry", kind: "message" as const, createdAt: startedAt, message: { id: MessageId.make("assistant-1"), role: "assistant" as const, text, turnId, createdAt: startedAt, updatedAt: startedAt, streaming: true, }, }; const initial = computeStableMessagesTimelineRows( deriveMessagesTimelineRows({ ...input, timelineEntries: [assistantEntry] }), { byId: new Map(), result: [] }, ); const updated = computeStableMessagesTimelineRows( deriveMessagesTimelineRows({ ...input, timelineEntries: [ { ...assistantEntry, message: { ...assistantEntry.message, text: "I will inspect the repository." }, }, ], }), initial, ); const initialWorking = initial.byId.get("working-indicator-row"); const updatedWorking = updated.byId.get("working-indicator-row"); expect(initialWorking).toMatchObject({ showThinking: true }); expect(updatedWorking).toMatchObject({ showThinking: false }); expect(updatedWorking).not.toBe(initialWorking); }); it("returns the previous result when row order and content are unchanged", () => { const firstUserMessage = { id: "user-1" as never, role: "user" as const, text: "First", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }; const secondUserMessage = { id: "user-2" as never, role: "user" as const, text: "Second", turnId: null, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:10Z", streaming: false, }; const rows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "entry-user-1", kind: "message", createdAt: firstUserMessage.createdAt, message: firstUserMessage, }, { id: "entry-user-2", kind: "message", createdAt: secondUserMessage.createdAt, message: secondUserMessage, }, ], isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const initial = computeStableMessagesTimelineRows(rows, { byId: new Map(), result: [], }); const repeated = computeStableMessagesTimelineRows(rows, initial); expect(repeated).toBe(initial); expect(repeated.result).toBe(initial.result); }); it("reuses work rows when equivalent timeline derivations create new grouped arrays", () => { const firstWorkEntry = { id: "work-1", createdAt: "2026-01-01T00:00:00Z", label: "thinking", detail: "Inspecting repository state", tone: "thinking" as const, }; const secondWorkEntry = { id: "work-2", createdAt: "2026-01-01T00:00:01Z", label: "read", detail: "Reading package.json", tone: "tool" as const, }; const createRows = () => deriveMessagesTimelineRows({ timelineEntries: [ { id: "entry-work-1", kind: "work", createdAt: firstWorkEntry.createdAt, entry: firstWorkEntry, }, { id: "entry-work-2", kind: "work", createdAt: secondWorkEntry.createdAt, entry: secondWorkEntry, }, ], isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const firstRows = createRows(); const initial = computeStableMessagesTimelineRows(firstRows, { byId: new Map(), result: [], }); const secondRows = createRows(); expect(secondRows[0]).not.toBe(firstRows[0]); const repeated = computeStableMessagesTimelineRows(secondRows, initial); expect(repeated).toBe(initial); expect(repeated.result[0]).toBe(initial.result[0]); }); it("returns a new result when row order changes without content changes", () => { const firstUserMessage = { id: "user-1" as never, role: "user" as const, text: "First", turnId: null, createdAt: "2026-01-01T00:00:00Z", updatedAt: "2026-01-01T00:00:00Z", streaming: false, }; const secondUserMessage = { id: "user-2" as never, role: "user" as const, text: "Second", turnId: null, createdAt: "2026-01-01T00:00:10Z", updatedAt: "2026-01-01T00:00:10Z", streaming: false, }; const firstRows = deriveMessagesTimelineRows({ timelineEntries: [ { id: "entry-user-1", kind: "message", createdAt: firstUserMessage.createdAt, message: firstUserMessage, }, { id: "entry-user-2", kind: "message", createdAt: secondUserMessage.createdAt, message: secondUserMessage, }, ], isWorking: false, activeTurnStartedAt: null, turnDiffSummaryByAssistantMessageId: new Map(), revertTurnCountByUserMessageId: new Map(), }); const initial = computeStableMessagesTimelineRows(firstRows, { byId: new Map(), result: [], }); const reordered = computeStableMessagesTimelineRows([firstRows[1]!, firstRows[0]!], initial); expect(reordered).not.toBe(initial); expect(reordered.result).toEqual([initial.result[1], initial.result[0]]); }); });