From 4034d59f8c175533357ce7a4afbc18e67e275ed2 Mon Sep 17 00:00:00 2001 From: maria Date: Tue, 1 Sep 2026 16:54:03 -0400 Subject: [PATCH] Revert "fix(chat): reuse one row for live activity (#9062)" This reverts commit a924fbe08e681ef56d8cfb8b99e1e803d52aa463. --- .../chat/MessagesTimeline.logic.test.ts | 91 ++++++++----------- .../components/chat/MessagesTimeline.logic.ts | 19 ++-- .../components/chat/MessagesTimeline.test.tsx | 42 ++------- .../src/components/chat/MessagesTimeline.tsx | 13 +-- 4 files changed, 59 insertions(+), 106 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index d729ffa829f9..d9bfdae04d49 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -826,7 +826,7 @@ describe("deriveMessagesTimelineRows", () => { "assistant-final-entry", "user-followup-entry", "working-indicator-row", - "live-activity-row", + "thinking-indicator-row", ]); const finalRow = rows.find((row) => row.id === "assistant-final-entry"); expect(finalRow?.kind === "message" && finalRow.showAssistantMeta).toBe(true); @@ -879,11 +879,11 @@ describe("deriveMessagesTimelineRows", () => { expect(rows.map((row) => row.id)).toEqual([ "working-indicator-row", "assistant-thought-entry", - "live-activity-row", + "work-live:work-entry-1", ]); }); - it("keeps an actually running tool in the shared activity row", () => { + it("keeps adjacent active tool calls in one replacing row", () => { const rows = deriveMessagesTimelineRows({ timelineEntries: [ { @@ -948,7 +948,6 @@ describe("deriveMessagesTimelineRows", () => { expect(rows.some((row) => row.kind === "thinking")).toBe(false); expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ entry: { id: "running-command" }, - active: true, groupedEntries: [ { id: "running-command" }, { id: "completed-edit" }, @@ -1197,54 +1196,40 @@ describe("deriveMessagesTimelineRows", () => { ]); }); - it("reuses one activity row for initial thinking and the latest tool", () => { - const deriveRows = (toolLifecycleStatus: "inProgress" | "completed" | null) => - deriveMessagesTimelineRows({ - timelineEntries: - toolLifecycleStatus === null - ? [] - : [ - { - 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: toolLifecycleStatus === "inProgress" ? "Running rg" : "Ran rg", - command: "rg toolCall", - requestKind: "command", - tone: "tool" as const, - toolLifecycleStatus, - }, - }, - ], - latestTurn: { - turnId: "turn-1" as never, - state: "running", - startedAt: "2026-01-01T00:00:00Z", - completedAt: null, + it("shows thinking after the latest tool call completes 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, + }, }, - isWorking: true, - activeTurnStartedAt: "2026-01-01T00:00:00Z", - turnDiffSummaryByAssistantMessageId: new Map(), - revertTurnCountByUserMessageId: new Map(), - }); + ], + 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 initialRows = deriveRows(null); - const runningRows = deriveRows("inProgress"); - const completedRows = deriveRows("completed"); - const initialActivityRow = initialRows.find((row) => row.id === "live-activity-row"); - const runningActivityRow = runningRows.find((row) => row.id === "live-activity-row"); - const completedActivityRow = completedRows.find((row) => row.id === "live-activity-row"); - - expect(initialActivityRow).toMatchObject({ kind: "thinking" }); - expect(runningActivityRow).toMatchObject({ kind: "work-live", active: true }); - expect(completedActivityRow).toMatchObject({ kind: "work-live", active: false }); - expect(initialRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); - expect(runningRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); - expect(completedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); + expect(rows.map((row) => row.kind)).toEqual(["working", "work-live", "thinking"]); + expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ active: false }); + expect(rows.at(-1)).toMatchObject({ kind: "thinking" }); }); it("does not fold the session's running turn when latestTurn regresses", () => { @@ -1305,7 +1290,7 @@ describe("deriveMessagesTimelineRows", () => { expect(rows.filter((row) => row.kind === "turn-fold").map((row) => row.turnId)).toEqual([ "turn-1", ]); - expect(rows.map((row) => row.id)).toContain("live-activity-row"); + expect(rows.map((row) => row.id)).toContain("work-live:running-work-entry"); }); it("only shows assistant metadata on the terminal assistant message", () => { @@ -1602,8 +1587,8 @@ describe("computeStableMessagesTimelineRows", () => { initial, ); - const initialThinking = initial.byId.get("live-activity-row"); - const updatedThinking = updated.byId.get("live-activity-row"); + const initialThinking = initial.byId.get("thinking-indicator-row"); + const updatedThinking = updated.byId.get("thinking-indicator-row"); expect(initialThinking).toMatchObject({ kind: "thinking" }); expect(updatedThinking).toBe(initialThinking); expect(updated.result.at(-1)).toBe(updatedThinking); diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index ea1e2c7562be..c787446f738b 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -188,8 +188,6 @@ export type TimelineLatestTurn = Pick< "turnId" | "state" | "startedAt" | "completedAt" >; -const LIVE_ACTIVITY_ROW_ID = "live-activity-row"; - export type MessagesTimelineRow = | { kind: "work"; @@ -600,16 +598,17 @@ export function deriveMessagesTimelineRows(input: { const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) => workEntryIsActiveTurnActivity(entry.entry), ); + const displayedToolEntry = latestRunningToolEntry ?? latestVisibleToolEntry; const activeWorkPlacementEntryId = latestVisibleToolEntry?.id; const activeWorkRow = - activeWorkAnchor && latestVisibleToolEntry + activeWorkAnchor && displayedToolEntry ? (() => { const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry); return { kind: "work-live" as const, - id: LIVE_ACTIVITY_ROW_ID, + id: `work-live:${workGroupIdentity(activeWorkAnchor.id, activeWorkAnchor.entry)}`, createdAt: activeWorkAnchor.createdAt, - entry: (latestRunningToolEntry ?? latestVisibleToolEntry).entry, + entry: displayedToolEntry.entry, groupedEntries: visibleActiveToolEntries.map((entry) => entry.entry), groupId, expanded: input.expandedWorkGroupIds?.has(groupId) ?? false, @@ -627,11 +626,11 @@ export function deriveMessagesTimelineRows(input: { createdAt: input.activeTurnStartedAt, }); }; - let hasActivityRow = false; + let hasLiveWorkRow = false; const appendActiveWorkRows = () => { if (activeWorkRow === null) return; nextRows.push(activeWorkRow); - hasActivityRow = true; + hasLiveWorkRow ||= activeWorkRow.active; if (!activeWorkRow.expanded) return; for (const [entryIndex, workEntry] of activeWorkRow.groupedEntries.entries()) { nextRows.push({ @@ -731,7 +730,7 @@ export function deriveMessagesTimelineRows(input: { expanded, active: true, }); - hasActivityRow = true; + hasLiveWorkRow = true; if (expanded) { for (const [entryIndex, workEntry] of visibleGroupedEntries.entries()) { nextRows.push({ @@ -833,10 +832,10 @@ export function deriveMessagesTimelineRows(input: { if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) { appendWorkingRow(); } - if (input.isWorking && !hasActivityRow) { + if (input.isWorking && !hasLiveWorkRow) { nextRows.push({ kind: "thinking", - id: LIVE_ACTIVITY_ROW_ID, + id: "thinking-indicator-row", createdAt: input.activeTurnStartedAt, }); } diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 5835cc9fed75..024dfe69d278 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -1308,30 +1308,7 @@ describe("MessagesTimeline", () => { expect(markup).not.toContain("tool call failed"); }); - it("renders initial thinking as the shared live activity row", () => { - const turnId = TurnId.make("turn-live"); - const markup = renderToStaticMarkup( - , - ); - - expect(markup).toContain("Thinking"); - expect(markup).toContain("lucide-brain"); - expect(markup).toContain('data-timeline-row-id="live-activity-row"'); - }); - - it("keeps the completed command in the shared activity row", () => { + it("keeps declined command copy visible while thinking continues", () => { const turnId = TurnId.make("turn-live"); const markup = renderToStaticMarkup( { runningTurnId={turnId} timelineEntries={[ { - id: "entry-completed", + id: "entry-declined", kind: "work", createdAt: MESSAGE_CREATED_AT, entry: { - id: "work-completed", + id: "work-declined", createdAt: MESSAGE_CREATED_AT, turnId, - toolCallId: "call-completed", + toolCallId: "call-declined", label: "Run lint", tone: "tool", itemType: "command_execution", command: "pnpm lint", - toolLifecycleStatus: "completed", + toolLifecycleStatus: "declined", }, }, ]} />, ); - expect(markup).toContain("Ran pnpm"); - expect(markup).toContain("lucide-terminal"); - expect(markup).not.toContain("live-activity-focus"); - expect(markup).not.toContain("Running pnpm"); - expect(markup).not.toContain("Thinking"); - expect(markup).not.toContain('data-timeline-row-kind="thinking"'); + expect(markup).toContain("Declined pnpm"); + expect(markup).toContain("Thinking"); + expect(markup).toContain("tool call failed"); }); it("renders review comment contexts as structured cards instead of raw tags", () => { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index df944a98485e..dbe8507aef80 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -62,7 +62,6 @@ import { import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown"; import { BotIcon, - BrainIcon, CheckIcon, ChevronDownIcon, ChevronRightIcon, @@ -986,15 +985,14 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time : "pb-0" : isExpandedToolGroupHeader ? "pb-0" - : row.kind === "turn-fold" || row.kind === "working" + : row.kind === "turn-fold" || row.kind === "working" || row.kind === "thinking" ? "pb-1.5" : (row.kind === "message" && row.message.role === "assistant" && !row.showAssistantMeta) || row.kind === "work" || row.kind === "work-live" || - row.kind === "work-toggle" || - row.kind === "thinking" + row.kind === "work-toggle" ? "pb-2" : "pb-4", row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null, @@ -1366,7 +1364,7 @@ function ThinkingTimelineRow() { // Reserve the activity row during setup so the handoff keeps the same height. return (
- {isPreparingWorktree ? null : } + {isPreparingWorktree ? null : }
); } @@ -2125,7 +2123,6 @@ function formatWorkingTimerNow(startIso: string): string { type WorkEntryIconName = | "bot" - | "brain" | "check" | "circle-alert" | "eye" @@ -2143,8 +2140,6 @@ function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; classN switch (name) { case "bot": return ; - case "brain": - return ; case "check": return ; case "circle-alert": @@ -2184,7 +2179,7 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): { } if (tone === "thinking") { return { - iconName: "brain", + iconName: "bot", className: "text-foreground", }; }