From a5fa9637f27b27fafb8c34ac62ce943e5d6c0995 Mon Sep 17 00:00:00 2001 From: Julius Marminge <51714798+juliusmarminge@users.noreply.github.com> Date: Sat, 26 Sep 2026 00:04:01 -0700 Subject: [PATCH] fix(web): a command you stop stays in the timeline Stopping a turn mid-command marks the command interrupted, which the work log maps to "stopped". The timeline treated stopped calls as neutral (neither success nor failure) and filtered them out like empty rows, so the command vanished once the run settled. Stopped is an outcome, so keep it visible, as mobile already does. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../chat/MessagesTimeline.logic.test.ts | 41 +++++++++++++++++++ .../components/chat/MessagesTimeline.logic.ts | 2 + 2 files changed, 43 insertions(+) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index 245a745a898b..e64d9dcf6621 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -3640,6 +3640,47 @@ describe("v2 run and attempt history", () => { expect(rows.map((row) => row.id)).toEqual(["work-entry", "interrupt-result"]); expect(rows.some((row) => row.kind === "turn-fold")).toBe(false); + + // The command the interrupt cut short stays visible with its outcome. + const stoppedRows = deriveMessagesTimelineRows({ + timelineEntries: [ + { + id: "stopped-command-entry", + kind: "work", + createdAt: "2026-01-01T00:00:01Z", + entry: { + id: "stopped-command", + createdAt: "2026-01-01T00:00:01Z", + runId, + label: "Ran command", + tone: "tool", + itemType: "command_execution", + command: "/bin/bash -lc 'sleep 90 && echo slept'", + toolLifecycleStatus: "stopped", + }, + }, + { + id: "interrupt-result", + kind: "event", + createdAt: "2026-01-01T00:00:03Z", + projectedItem: interruptEvent("run_interrupt_result") as never, + }, + ], + latestRun: { + runId, + status: "interrupted", + startedAt: "2026-01-01T00:00:00Z", + completedAt: "2026-01-01T00:00:03Z", + }, + isWorking: false, + turnDiffSummaries: [], + supportsConversationRollback: false, + }); + expect(stoppedRows.map((row) => row.id)).toEqual(["stopped-command-entry", "interrupt-result"]); + expect(stoppedRows[0]).toMatchObject({ + kind: "work", + displayLabel: "sleep 90 && echo slept", + }); }); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index d079a0501d2c..b468a50b36f7 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -151,6 +151,8 @@ export function workEntryIsVisibleInGroup( (expandedToolGroupEntry && (entry.toolLifecycleStatus === "inProgress" || entry.sourceActivityKind === "task.progress")) || + // A stopped call is an outcome ("Stopped sleep"), not an empty row. + entry.toolLifecycleStatus === "stopped" || !workEntryIndicatesToolNeutralStatus(entry) ); }