| author | |
| committer | |
| log | ec4378614046ab58864ff5e640de2b46f6704505 |
| tree | 8fbb9762890764e2e05db49bcc6f41c6d5f2fa1c |
| parent | f82796a16abb707c1e2c41301ad8d6e3153668c5 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>5 files changed, 197 insertions(+), 5 deletions(-)
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+130| ... | @@ -973,6 +973,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -973,6 +973,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 973 | status: "superseded" as const, | 973 | status: "superseded" as const, |
| 974 | attemptOrdinal: 0, | 974 | attemptOrdinal: 0, |
| 975 | rootNodeId: "old-root" as never, | 975 | rootNodeId: "old-root" as never, |
| 976 | completedAt: null, | ||
| 976 | }, | 977 | }, |
| 977 | } | 978 | } |
| 978 | : {}), | 979 | : {}), |
| ... | @@ -3819,6 +3820,7 @@ describe("v2 run and attempt history", () => { | ... | @@ -3819,6 +3820,7 @@ describe("v2 run and attempt history", () => { |
| 3819 | attemptOrdinal: 1, | 3820 | attemptOrdinal: 1, |
| 3820 | rootNodeId: "node-attempt-1" as never, | 3821 | rootNodeId: "node-attempt-1" as never, |
| 3821 | status: "superseded" as const, | 3822 | status: "superseded" as const, |
| 3823 | completedAt: null, | ||
| 3822 | }; | 3824 | }; |
| 3823 | const activeAttempt = { | 3825 | const activeAttempt = { |
| 3824 | id: activeAttemptId, | 3826 | id: activeAttemptId, |
| ... | @@ -3826,6 +3828,7 @@ describe("v2 run and attempt history", () => { | ... | @@ -3826,6 +3828,7 @@ describe("v2 run and attempt history", () => { |
| 3826 | attemptOrdinal: 2, | 3828 | attemptOrdinal: 2, |
| 3827 | rootNodeId: "node-attempt-2" as never, | 3829 | rootNodeId: "node-attempt-2" as never, |
| 3828 | status: "running" as const, | 3830 | status: "running" as const, |
| 3831 | completedAt: null, | ||
| 3829 | }; | 3832 | }; |
| 3830 | const timelineEntries = [ | 3833 | const timelineEntries = [ |
| 3831 | { | 3834 | { |
| ... | @@ -3957,6 +3960,131 @@ describe("v2 run and attempt history", () => { | ... | @@ -3957,6 +3960,131 @@ describe("v2 run and attempt history", () => { |
| 3957 | "active-assistant-entry", | 3960 | "active-assistant-entry", |
| 3958 | ]); | 3961 | ]); |
| 3959 | }); | 3962 | }); |
| 3963 | |||
| 3964 | describe("a steer that supersedes an attempt", () => { | ||
| 3965 | const runId = RunId.make("run-steered"); | ||
| 3966 | const at = (second: number) => `2026-01-01T00:00:${String(second).padStart(2, "0")}Z`; | ||
| 3967 | // The steer stamps the superseded attempt at 0:10. | ||
| 3968 | const supersededAttempt = { | ||
| 3969 | id: RunAttemptId.make("attempt-1"), | ||
| 3970 | runId, | ||
| 3971 | attemptOrdinal: 1, | ||
| 3972 | rootNodeId: NodeId.make("node-attempt-1"), | ||
| 3973 | status: "superseded" as const, | ||
| 3974 | completedAt: DateTime.makeUnsafe(at(10)), | ||
| 3975 | }; | ||
| 3976 | const nextAttempt = { | ||
| 3977 | id: RunAttemptId.make("attempt-2"), | ||
| 3978 | runId, | ||
| 3979 | attemptOrdinal: 2, | ||
| 3980 | rootNodeId: NodeId.make("node-attempt-2"), | ||
| 3981 | status: "completed" as const, | ||
| 3982 | completedAt: DateTime.makeUnsafe(at(14)), | ||
| 3983 | }; | ||
| 3984 | const assistantEntry = ( | ||
| 3985 | id: string, | ||
| 3986 | attempt: typeof supersededAttempt | typeof nextAttempt, | ||
| 3987 | completedSecond: number, | ||
| 3988 | ): TimelineEntry => ({ | ||
| 3989 | id, | ||
| 3990 | kind: "message", | ||
| 3991 | createdAt: at(2), | ||
| 3992 | attempt, | ||
| 3993 | message: { | ||
| 3994 | id: MessageId.make(id), | ||
| 3995 | role: "assistant", | ||
| 3996 | text: `${id} text`, | ||
| 3997 | runId, | ||
| 3998 | createdAt: at(2), | ||
| 3999 | updatedAt: at(completedSecond), | ||
| 4000 | streaming: false, | ||
| 4001 | }, | ||
| 4002 | projectedItem: { | ||
| 4003 | item: { | ||
| 4004 | type: "assistant_message", | ||
| 4005 | status: "completed", | ||
| 4006 | completedAt: DateTime.makeUnsafe(at(completedSecond)), | ||
| 4007 | }, | ||
| 4008 | } as never, | ||
| 4009 | }); | ||
| 4010 | const rowsFor = ( | ||
| 4011 | replyCompletedSecond: number, | ||
| 4012 | runStatus: "running" | "completed" = "completed", | ||
| 4013 | ) => | ||
| 4014 | deriveMessagesTimelineRows({ | ||
| 4015 | timelineEntries: [ | ||
| 4016 | { | ||
| 4017 | id: "initial-user-entry", | ||
| 4018 | kind: "message", | ||
| 4019 | createdAt: at(0), | ||
| 4020 | attempt: supersededAttempt, | ||
| 4021 | message: { | ||
| 4022 | id: MessageId.make("initial-user"), | ||
| 4023 | role: "user", | ||
| 4024 | text: "Build it", | ||
| 4025 | runId, | ||
| 4026 | inputIntent: "turn_start", | ||
| 4027 | createdAt: at(0), | ||
| 4028 | updatedAt: at(0), | ||
| 4029 | streaming: false, | ||
| 4030 | }, | ||
| 4031 | }, | ||
| 4032 | { | ||
| 4033 | id: "old-work-entry", | ||
| 4034 | kind: "work", | ||
| 4035 | createdAt: at(1), | ||
| 4036 | attempt: supersededAttempt, | ||
| 4037 | entry: { id: "old-work", createdAt: at(1), runId, label: "Read", tone: "tool" }, | ||
| 4038 | }, | ||
| 4039 | assistantEntry("old-reply-entry", supersededAttempt, replyCompletedSecond), | ||
| 4040 | { | ||
| 4041 | id: "steer-user-entry", | ||
| 4042 | kind: "message", | ||
| 4043 | createdAt: at(10), | ||
| 4044 | attempt: nextAttempt, | ||
| 4045 | message: { | ||
| 4046 | id: MessageId.make("steer-user"), | ||
| 4047 | role: "user", | ||
| 4048 | text: "What are you working on?", | ||
| 4049 | runId, | ||
| 4050 | inputIntent: "steer", | ||
| 4051 | createdAt: at(10), | ||
| 4052 | updatedAt: at(10), | ||
| 4053 | streaming: false, | ||
| 4054 | }, | ||
| 4055 | }, | ||
| 4056 | assistantEntry("new-reply-entry", nextAttempt, 14), | ||
| 4057 | ], | ||
| 4058 | latestRun: { | ||
| 4059 | runId, | ||
| 4060 | status: runStatus, | ||
| 4061 | startedAt: at(0), | ||
| 4062 | completedAt: runStatus === "completed" ? at(14) : null, | ||
| 4063 | }, | ||
| 4064 | isWorking: false, | ||
| 4065 | turnDiffSummaries: [], | ||
| 4066 | supportsConversationRollback: false, | ||
| 4067 | }); | ||
| 4068 | |||
| 4069 | it.each(["running", "completed"] as const)( | ||
| 4070 | "keeps a reply that finished before the steer visible while the run is %s", | ||
| 4071 | (runStatus) => { | ||
| 4072 | const rows = rowsFor(4, runStatus); | ||
| 4073 | expect(rows.some((row) => row.kind === "attempt-fold")).toBe(false); | ||
| 4074 | expect(rows.map((row) => row.id)).toContain("old-reply-entry"); | ||
| 4075 | }, | ||
| 4076 | ); | ||
| 4077 | |||
| 4078 | it("folds a reply the steer cut off as partial output", () => { | ||
| 4079 | const rows = rowsFor(11, "running"); | ||
| 4080 | expect(rows.find((row) => row.kind === "attempt-fold")).toMatchObject({ | ||
| 4081 | attemptId: supersededAttempt.id, | ||
| 4082 | label: "Superseded attempt", | ||
| 4083 | }); | ||
| 4084 | expect(rows.map((row) => row.id)).not.toContain("old-reply-entry"); | ||
| 4085 | }); | ||
| 4086 | }); | ||
| 4087 | |||
| 3960 | it("hides the interruption request while keeping intervening work and the result", () => { | 4088 | it("hides the interruption request while keeping intervening work and the result", () => { |
| 3961 | const runId = "turn-1" as never; | 4089 | const runId = "turn-1" as never; |
| 3962 | const interruptEvent = (type: "run_interrupt_request" | "run_interrupt_result") => ({ | 4090 | const interruptEvent = (type: "run_interrupt_request" | "run_interrupt_result") => ({ |
| ... | @@ -4962,6 +5090,7 @@ describe("failed turn transcript", () => { | ... | @@ -4962,6 +5090,7 @@ describe("failed turn transcript", () => { |
| 4962 | attemptOrdinal: 1, | 5090 | attemptOrdinal: 1, |
| 4963 | rootNodeId: NodeId.make("superseded-root"), | 5091 | rootNodeId: NodeId.make("superseded-root"), |
| 4964 | status: "superseded" as const, | 5092 | status: "superseded" as const, |
| 5093 | completedAt: null, | ||
| 4965 | }, | 5094 | }, |
| 4966 | })), | 5095 | })), |
| 4967 | latestRun: { | 5096 | latestRun: { |
| ... | @@ -5160,6 +5289,7 @@ describe("live subagents after their parent turn settles", () => { | ... | @@ -5160,6 +5289,7 @@ describe("live subagents after their parent turn settles", () => { |
| 5160 | attemptOrdinal: 1, | 5289 | attemptOrdinal: 1, |
| 5161 | rootNodeId: NodeId.make("superseded-root"), | 5290 | rootNodeId: NodeId.make("superseded-root"), |
| 5162 | status: "superseded" as const, | 5291 | status: "superseded" as const, |
| 5292 | completedAt: null, | ||
| 5163 | }; | 5293 | }; |
| 5164 | const entries = deriveTimelineEntriesFromVisibleTurnItems({ | 5294 | const entries = deriveTimelineEntriesFromVisibleTurnItems({ |
| 5165 | visibleTurnItems: ( | 5295 | visibleTurnItems: ( |
apps/web/src/components/chat/MessagesTimeline.logic.ts+62-2| ... | @@ -1,6 +1,7 @@ | ... | @@ -1,6 +1,7 @@ |
| 1 | import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; | 1 | import { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; |
| 2 | export { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; | 2 | export { worktreeSetupAgentStarted } from "@t3tools/client-runtime/worktree-setup"; |
| 3 | import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime"; | 3 | import { isLiveSubagentTurnItem } from "@t3tools/client-runtime/state/subagentRuntime"; |
| 4 | import * as DateTime from "effect/DateTime"; | ||
| 4 | import * as Equal from "effect/Equal"; | 5 | import * as Equal from "effect/Equal"; |
| 5 | import { | 6 | import { |
| 6 | assistantCitationLabel, | 7 | assistantCitationLabel, |
| ... | @@ -752,7 +753,52 @@ interface SupersededAttemptFold { | ... | @@ -752,7 +753,52 @@ interface SupersededAttemptFold { |
| 752 | } | 753 | } |
| 753 | 754 | ||
| 754 | /** | 755 | /** |
| 755 | * Groups only provider output owned by an explicitly superseded V2 attempt. | 756 | * Final replies of superseded attempts that finished before the steer replaced |
| 757 | * them. A steer stamps the attempt's completedAt when it is dispatched, and the | ||
| 758 | * provider closes a reply it cut off only after that, so a reply whose item | ||
| 759 | * completed earlier was whole. Item status alone cannot tell: an interrupted | ||
| 760 | * stream also closes as `completed`. The reply must be the attempt's last | ||
| 761 | * output; trailing tool or reasoning work means the attempt was still going. | ||
| 762 | */ | ||
| 763 | function completeSupersededReplyEntryIds( | ||
| 764 | timelineEntries: ReadonlyArray<TimelineEntry>, | ||
| 765 | ): ReadonlySet<string> { | ||
| 766 | const lastOutputByAttemptId = new Map<RunAttemptId, TimelineEntry>(); | ||
| 767 | for (const entry of timelineEntries) { | ||
| 768 | if ( | ||
| 769 | entry.attempt?.status !== "superseded" || | ||
| 770 | (entry.kind === "message" && entry.message.role === "user") || | ||
| 771 | (entry.kind === "work" && | ||
| 772 | (entry.entry.itemType === "system_notice" || entry.entry.itemType === "notification")) || | ||
| 773 | (entry.kind !== "message" && entry.kind !== "work") | ||
| 774 | ) { | ||
| 775 | continue; | ||
| 776 | } | ||
| 777 | lastOutputByAttemptId.set(entry.attempt.id, entry); | ||
| 778 | } | ||
| 779 | const replyEntryIds = new Set<string>(); | ||
| 780 | for (const entry of lastOutputByAttemptId.values()) { | ||
| 781 | const supersededAt = entry.attempt?.completedAt ?? null; | ||
| 782 | const item = entry.kind === "message" ? entry.projectedItem?.item : undefined; | ||
| 783 | if ( | ||
| 784 | entry.kind === "message" && | ||
| 785 | !entry.message.streaming && | ||
| 786 | item?.type === "assistant_message" && | ||
| 787 | item.status === "completed" && | ||
| 788 | item.completedAt !== null && | ||
| 789 | supersededAt !== null && | ||
| 790 | DateTime.isLessThan(item.completedAt, supersededAt) | ||
| 791 | ) { | ||
| 792 | replyEntryIds.add(entry.id); | ||
| 793 | } | ||
| 794 | } | ||
| 795 | return replyEntryIds; | ||
| 796 | } | ||
| 797 | |||
| 798 | /** | ||
| 799 | * Groups only provider output owned by an explicitly superseded V2 attempt | ||
| 800 | * that the steer cut off. An attempt that had already finished its reply is | ||
| 801 | * left in the timeline like any other turn output. | ||
| 756 | * User messages remain visible because they are inputs to the logical run, | 802 | * User messages remain visible because they are inputs to the logical run, |
| 757 | * including the steer message that started the replacement attempt. | 803 | * including the steer message that started the replacement attempt. |
| 758 | */ | 804 | */ |
| ... | @@ -760,11 +806,18 @@ function deriveSupersededAttemptFolds( | ... | @@ -760,11 +806,18 @@ function deriveSupersededAttemptFolds( |
| 760 | timelineEntries: ReadonlyArray<TimelineEntry>, | 806 | timelineEntries: ReadonlyArray<TimelineEntry>, |
| 761 | unfoldedRunIds: ReadonlySet<RunId>, | 807 | unfoldedRunIds: ReadonlySet<RunId>, |
| 762 | liveSubagentEntryIds: ReadonlySet<string>, | 808 | liveSubagentEntryIds: ReadonlySet<string>, |
| 809 | completeReplyEntryIds: ReadonlySet<string>, | ||
| 763 | ): ReadonlyMap<string, SupersededAttemptFold> { | 810 | ): ReadonlyMap<string, SupersededAttemptFold> { |
| 811 | const completeAttemptIds = new Set( | ||
| 812 | timelineEntries.flatMap((entry) => | ||
| 813 | entry.attempt !== undefined && completeReplyEntryIds.has(entry.id) ? [entry.attempt.id] : [], | ||
| 814 | ), | ||
| 815 | ); | ||
| 764 | const entriesByAttemptId = new Map<RunAttemptId, TimelineEntry[]>(); | 816 | const entriesByAttemptId = new Map<RunAttemptId, TimelineEntry[]>(); |
| 765 | for (const entry of timelineEntries) { | 817 | for (const entry of timelineEntries) { |
| 766 | if ( | 818 | if ( |
| 767 | entry.attempt?.status !== "superseded" || | 819 | entry.attempt?.status !== "superseded" || |
| 820 | completeAttemptIds.has(entry.attempt.id) || | ||
| 768 | unfoldedRunIds.has(entry.attempt.runId) || | 821 | unfoldedRunIds.has(entry.attempt.runId) || |
| 769 | (entry.kind === "message" && entry.message.role === "user") || | 822 | (entry.kind === "message" && entry.message.role === "user") || |
| 770 | // A published page stays visible, as it does when its turn folds. | 823 | // A published page stays visible, as it does when its turn folds. |
| ... | @@ -966,6 +1019,8 @@ function deriveTurnFolds(input: { | ... | @@ -966,6 +1019,8 @@ function deriveTurnFolds(input: { |
| 966 | /** Keeps the latest runless response open; V2 work must not reopen imported turns. */ | 1019 | /** Keeps the latest runless response open; V2 work must not reopen imported turns. */ |
| 967 | runlessWorkActive: boolean; | 1020 | runlessWorkActive: boolean; |
| 968 | liveSubagentEntryIds: ReadonlySet<string>; | 1021 | liveSubagentEntryIds: ReadonlySet<string>; |
| 1022 | /** Finished replies a steer superseded; they answer their prompt, so they stay out. */ | ||
| 1023 | completeSupersededReplyEntryIds: ReadonlySet<string>; | ||
| 969 | }): ReadonlyMap<string, TurnFold> { | 1024 | }): ReadonlyMap<string, TurnFold> { |
| 970 | const interruptedRunIds = new Set<RunId>(); | 1025 | const interruptedRunIds = new Set<RunId>(); |
| 971 | for (const entry of input.timelineEntries) { | 1026 | for (const entry of input.timelineEntries) { |
| ... | @@ -1082,7 +1137,8 @@ function deriveTurnFolds(input: { | ... | @@ -1082,7 +1137,8 @@ function deriveTurnFolds(input: { |
| 1082 | // launching run and stay visible after the surrounding work folds. | 1137 | // launching run and stay visible after the surrounding work folds. |
| 1083 | if ( | 1138 | if ( |
| 1084 | timelineEntryIsPersistentResourceCard(entry) || | 1139 | timelineEntryIsPersistentResourceCard(entry) || |
| 1085 | input.liveSubagentEntryIds.has(entry.id) | 1140 | input.liveSubagentEntryIds.has(entry.id) || |
| 1141 | input.completeSupersededReplyEntryIds.has(entry.id) | ||
| 1086 | ) { | 1142 | ) { |
| 1087 | continue; | 1143 | continue; |
| 1088 | } | 1144 | } |
| ... | @@ -1320,6 +1376,7 @@ function deriveTimelineTurnFolds( | ... | @@ -1320,6 +1376,7 @@ function deriveTimelineTurnFolds( |
| 1320 | unfoldedRunIds: new Set([...activeVisualResponseRunIds, ...failedRunIds]), | 1376 | unfoldedRunIds: new Set([...activeVisualResponseRunIds, ...failedRunIds]), |
| 1321 | runlessWorkActive: input.isWorking && input.runlessWorkActive === true, | 1377 | runlessWorkActive: input.isWorking && input.runlessWorkActive === true, |
| 1322 | liveSubagentEntryIds: liveSubagentCardEntryIds(timelineEntries), | 1378 | liveSubagentEntryIds: liveSubagentCardEntryIds(timelineEntries), |
| 1379 | completeSupersededReplyEntryIds: completeSupersededReplyEntryIds(timelineEntries), | ||
| 1323 | }); | 1380 | }); |
| 1324 | } | 1381 | } |
| 1325 | 1382 | ||
| ... | @@ -1365,10 +1422,12 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -1365,10 +1422,12 @@ export function deriveMessagesTimelineRows(input: { |
| 1365 | const unsettledRunId = deriveUnsettledRunId(input.latestRun ?? null, input.runningRunId ?? null); | 1422 | const unsettledRunId = deriveUnsettledRunId(input.latestRun ?? null, input.runningRunId ?? null); |
| 1366 | const failedRunIds = failedTimelineRunIds(timelineEntries, input.latestRun ?? null); | 1423 | const failedRunIds = failedTimelineRunIds(timelineEntries, input.latestRun ?? null); |
| 1367 | const liveSubagentEntryIds = liveSubagentCardEntryIds(timelineEntries); | 1424 | const liveSubagentEntryIds = liveSubagentCardEntryIds(timelineEntries); |
| 1425 | const completeSupersededReplyIds = completeSupersededReplyEntryIds(timelineEntries); | ||
| 1368 | const supersededFoldsByAnchorEntryId = deriveSupersededAttemptFolds( | 1426 | const supersededFoldsByAnchorEntryId = deriveSupersededAttemptFolds( |
| 1369 | timelineEntries, | 1427 | timelineEntries, |
| 1370 | failedRunIds, | 1428 | failedRunIds, |
| 1371 | liveSubagentEntryIds, | 1429 | liveSubagentEntryIds, |
| 1430 | completeSupersededReplyIds, | ||
| 1372 | ); | 1431 | ); |
| 1373 | const activeVisualResponseRunIds = deriveActiveVisualResponseRunIds({ | 1432 | const activeVisualResponseRunIds = deriveActiveVisualResponseRunIds({ |
| 1374 | timelineEntries: timelineEntries, | 1433 | timelineEntries: timelineEntries, |
| ... | @@ -1383,6 +1442,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -1383,6 +1442,7 @@ export function deriveMessagesTimelineRows(input: { |
| 1383 | unfoldedRunIds: new Set([...activeVisualResponseRunIds, ...failedRunIds]), | 1442 | unfoldedRunIds: new Set([...activeVisualResponseRunIds, ...failedRunIds]), |
| 1384 | runlessWorkActive, | 1443 | runlessWorkActive, |
| 1385 | liveSubagentEntryIds, | 1444 | liveSubagentEntryIds, |
| 1445 | completeSupersededReplyEntryIds: completeSupersededReplyIds, | ||
| 1386 | }); | 1446 | }); |
| 1387 | const collapsedEntryIds = new Set<string>(); | 1447 | const collapsedEntryIds = new Set<string>(); |
| 1388 | for (const fold of foldsByAnchorEntryId.values()) { | 1448 | for (const fold of foldsByAnchorEntryId.values()) { |
apps/web/src/components/chat/MessagesTimeline.test.tsx+3-1| ... | @@ -1471,6 +1471,7 @@ describe("MessagesTimeline", () => { | ... | @@ -1471,6 +1471,7 @@ describe("MessagesTimeline", () => { |
| 1471 | attemptOrdinal: 1, | 1471 | attemptOrdinal: 1, |
| 1472 | rootNodeId: "node-attempt-1" as never, | 1472 | rootNodeId: "node-attempt-1" as never, |
| 1473 | status: "superseded" as const, | 1473 | status: "superseded" as const, |
| 1474 | completedAt: null, | ||
| 1474 | }; | 1475 | }; |
| 1475 | const activeAttempt = { | 1476 | const activeAttempt = { |
| 1476 | id: "attempt-2" as never, | 1477 | id: "attempt-2" as never, |
| ... | @@ -1478,6 +1479,7 @@ describe("MessagesTimeline", () => { | ... | @@ -1478,6 +1479,7 @@ describe("MessagesTimeline", () => { |
| 1478 | attemptOrdinal: 2, | 1479 | attemptOrdinal: 2, |
| 1479 | rootNodeId: "node-attempt-2" as never, | 1480 | rootNodeId: "node-attempt-2" as never, |
| 1480 | status: "running" as const, | 1481 | status: "running" as const, |
| 1482 | completedAt: null, | ||
| 1481 | }; | 1483 | }; |
| 1482 | const markup = renderToStaticMarkup( | 1484 | const markup = renderToStaticMarkup( |
| 1483 | <MessagesTimeline | 1485 | <MessagesTimeline |
| ... | @@ -1526,7 +1528,7 @@ describe("MessagesTimeline", () => { | ... | @@ -1526,7 +1528,7 @@ describe("MessagesTimeline", () => { |
| 1526 | expect(markup).toContain('data-superseded-attempt-id="attempt-1"'); | 1528 | expect(markup).toContain('data-superseded-attempt-id="attempt-1"'); |
| 1527 | expect(markup).toContain('aria-expanded="false"'); | 1529 | expect(markup).toContain('aria-expanded="false"'); |
| 1528 | expect(markup).toContain("Superseded attempt"); | 1530 | expect(markup).toContain("Superseded attempt"); |
| 1529 | expect(markup).toContain("Partial output retained"); | 1531 | expect(markup).toContain("Cut off by a steer"); |
| 1530 | expect(markup).toContain("Current response remains visible"); | 1532 | expect(markup).toContain("Current response remains visible"); |
| 1531 | expect(markup).not.toContain("Partial response from the old attempt"); | 1533 | expect(markup).not.toContain("Partial response from the old attempt"); |
| 1532 | }); | 1534 | }); |
apps/web/src/components/chat/MessagesTimeline.tsx+1-1| ... | @@ -2744,7 +2744,7 @@ function AttemptFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "at | ... | @@ -2744,7 +2744,7 @@ function AttemptFoldTimelineRow({ row }: { row: Extract<TimelineRow, { kind: "at |
| 2744 | icon={row.expanded ? ChevronDown : ChevronRight} | 2744 | icon={row.expanded ? ChevronDown : ChevronRight} |
| 2745 | /> | 2745 | /> |
| 2746 | <span className="text-xs font-medium text-foreground/80">{row.label}</span> | 2746 | <span className="text-xs font-medium text-foreground/80">{row.label}</span> |
| 2747 | <span className="text-2xs text-muted-foreground">Partial output retained</span> | 2747 | <span className="text-2xs text-muted-foreground">Cut off by a steer</span> |
| 2748 | </button> | 2748 | </button> |
| 2749 | ); | 2749 | ); |
| 2750 | } | 2750 | } |
apps/web/src/session-logic.ts+1-1| ... | @@ -119,7 +119,7 @@ export interface LatestProposedPlanState { | ... | @@ -119,7 +119,7 @@ export interface LatestProposedPlanState { |
| 119 | 119 | ||
| 120 | export type TimelineAttempt = Pick< | 120 | export type TimelineAttempt = Pick< |
| 121 | OrchestrationV2RunAttempt, | 121 | OrchestrationV2RunAttempt, |
| 122 | "id" | "runId" | "attemptOrdinal" | "rootNodeId" | "status" | 122 | "id" | "runId" | "attemptOrdinal" | "rootNodeId" | "status" | "completedAt" |
| 123 | >; | 123 | >; |
| 124 | 124 | ||
| 125 | export type TimelineEntry = ( | 125 | export type TimelineEntry = ( |