Skip to content

Commit 3f00a58

Browse files
fix(web): finished replies keep their hover timestamp while commands run
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 1280ccd commit 3f00a58

4 files changed

Lines changed: 72 additions & 3 deletions

File tree

‎apps/web/src/components/chat/MessagesTimeline.logic.test.ts‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,6 +1528,60 @@ describe("deriveMessagesTimelineRows", () => {
15281528
expect(rows.at(-1)).toMatchObject({ kind: "thinking" });
15291529
});
15301530

1531+
it("keeps the latest finished reply's timestamp while a command is still running", () => {
1532+
const reply = (streaming: boolean) => ({
1533+
id: "assistant-reply-entry",
1534+
kind: "message" as const,
1535+
createdAt: "2026-01-01T00:00:10Z",
1536+
message: {
1537+
id: "assistant-reply" as never,
1538+
role: "assistant" as const,
1539+
text: "Training finished, checking memory next.",
1540+
runId: "turn-1" as never,
1541+
createdAt: "2026-01-01T00:00:10Z",
1542+
updatedAt: "2026-01-01T00:00:12Z",
1543+
streaming,
1544+
},
1545+
});
1546+
const rowsWith = (streaming: boolean) =>
1547+
deriveMessagesTimelineRows({
1548+
timelineEntries: [
1549+
reply(streaming),
1550+
{
1551+
id: "command-entry",
1552+
kind: "work",
1553+
createdAt: "2026-01-01T00:00:15Z",
1554+
entry: {
1555+
id: "command",
1556+
createdAt: "2026-01-01T00:00:15Z",
1557+
runId: "turn-1" as never,
1558+
label: "Running command",
1559+
command: "nvidia-smi",
1560+
tone: "tool" as const,
1561+
toolLifecycleStatus: "inProgress" as const,
1562+
},
1563+
},
1564+
],
1565+
latestRun: {
1566+
runId: "turn-1" as never,
1567+
status: "running",
1568+
startedAt: "2026-01-01T00:00:00Z",
1569+
completedAt: null,
1570+
},
1571+
isWorking: true,
1572+
activeTurnStartedAt: "2026-01-01T00:00:00Z",
1573+
turnDiffSummaries: [],
1574+
supportsConversationRollback: false,
1575+
}).find((row) => row.id === "assistant-reply-entry");
1576+
1577+
expect(rowsWith(false)).toMatchObject({
1578+
showAssistantMeta: false,
1579+
showAssistantTimestamp: true,
1580+
showAssistantCopyButton: false,
1581+
});
1582+
expect(rowsWith(true)).toMatchObject({ showAssistantTimestamp: false });
1583+
});
1584+
15311585
it("does not show Thinking once the open run's provider turn has ended", () => {
15321586
const input = {
15331587
timelineEntries: [

‎apps/web/src/components/chat/MessagesTimeline.logic.ts‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,8 @@ type MessagesTimelineRowContent =
591591
message: ChatMessage;
592592
projectedItem?: OrchestrationV2ProjectedTurnItem;
593593
showAssistantMeta: boolean;
594+
/** The latest finished reply of a still-running response shows only its timestamp. */
595+
showAssistantTimestamp: boolean;
594596
showAssistantCopyButton: boolean;
595597
assistantCopyStreaming: boolean;
596598
assistantTurnDiffSummary?: TurnDiffSummary | undefined;
@@ -2027,10 +2029,10 @@ export function deriveMessagesTimelineRows(input: {
20272029
timelineEntry.message.role === "assistant" &&
20282030
runIdIsActiveResponse(timelineEntry.message.runId);
20292031

2030-
const showAssistantMeta =
2032+
const isTerminalAssistantMessage =
20312033
timelineEntry.message.role === "assistant" &&
2032-
terminalAssistantMessageIds.has(timelineEntry.message.id) &&
2033-
!assistantResponseStillInProgress;
2034+
terminalAssistantMessageIds.has(timelineEntry.message.id);
2035+
const showAssistantMeta = isTerminalAssistantMessage && !assistantResponseStillInProgress;
20342036

20352037
nextRows.push({
20362038
kind: "message",
@@ -2041,6 +2043,10 @@ export function deriveMessagesTimelineRows(input: {
20412043
? {}
20422044
: { projectedItem: timelineEntry.projectedItem }),
20432045
showAssistantMeta,
2046+
showAssistantTimestamp:
2047+
isTerminalAssistantMessage &&
2048+
assistantResponseStillInProgress &&
2049+
!timelineEntry.message.streaming,
20442050
showAssistantCopyButton: showAssistantMeta,
20452051
assistantCopyStreaming: timelineEntry.message.streaming || assistantResponseStillInProgress,
20462052
assistantTurnDiffSummary:
@@ -2504,6 +2510,7 @@ function isRowUnchanged(a: MessagesTimelineRow, b: MessagesTimelineRow): boolean
25042510
a.message === bm.message &&
25052511
a.projectedItem === bm.projectedItem &&
25062512
a.showAssistantMeta === bm.showAssistantMeta &&
2513+
a.showAssistantTimestamp === bm.showAssistantTimestamp &&
25072514
a.showAssistantCopyButton === bm.showAssistantCopyButton &&
25082515
a.assistantCopyStreaming === bm.assistantCopyStreaming &&
25092516
a.assistantTurnDiffSummary === bm.assistantTurnDiffSummary &&

‎apps/web/src/components/chat/TimelineAssistantRows.tsx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ export function AssistantTimelineRow({ row }: { row: Extract<TimelineRow, { kind
7676
showCopyButton={row.showAssistantCopyButton}
7777
copyStreaming={row.assistantCopyStreaming}
7878
/>
79+
) : row.showAssistantTimestamp ? (
80+
<AssistantMessageMeta
81+
className="mt-1.5"
82+
message={row.message}
83+
showCopyButton={false}
84+
copyStreaming={row.assistantCopyStreaming}
85+
/>
7986
) : null}
8087
</div>
8188
</>

‎apps/web/src/components/chat/timelineMinimapItems.test.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ describe("deriveTimelineMinimapItems", () => {
1515
id: "image-only",
1616
createdAt: at,
1717
showAssistantMeta: false,
18+
showAssistantTimestamp: false,
1819
showAssistantCopyButton: false,
1920
assistantCopyStreaming: false,
2021
message: {

0 commit comments

Comments
 (0)