| ... | @@ -512,6 +512,37 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null { | ... | @@ -512,6 +512,37 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null { |
| 512 | return entry.kind === "work" ? (entry.entry.turnId ?? null) : null; | 512 | return entry.kind === "work" ? (entry.entry.turnId ?? null) : null; |
| 513 | } | 513 | } |
| 514 | | 514 | |
| | 515 | /** |
| | 516 | * A promptless provider restart replaces the native turn without adding a |
| | 517 | * user message. Keep every provider turn since the latest user message in one |
| | 518 | * visual response until the replacement turn settles. A steer has its own |
| | 519 | * user message, so it naturally starts a new visual response. |
| | 520 | */ |
| | 521 | function deriveActiveVisualResponseTurnIds(input: { |
| | 522 | timelineEntries: ReadonlyArray<TimelineEntry>; |
| | 523 | unsettledTurnId: TurnId | null; |
| | 524 | isWorking: boolean; |
| | 525 | }): ReadonlySet<TurnId> { |
| | 526 | const turnIds = new Set<TurnId>(); |
| | 527 | if (input.unsettledTurnId === null) { |
| | 528 | return turnIds; |
| | 529 | } |
| | 530 | |
| | 531 | turnIds.add(input.unsettledTurnId); |
| | 532 | if (!input.isWorking) { |
| | 533 | return turnIds; |
| | 534 | } |
| | 535 | |
| | 536 | const latestUserMessageIndex = lastUserMessageIndex(input.timelineEntries); |
| | 537 | for (let index = latestUserMessageIndex + 1; index < input.timelineEntries.length; index += 1) { |
| | 538 | const turnId = timelineEntryTurnId(input.timelineEntries[index]!); |
| | 539 | if (turnId !== null) { |
| | 540 | turnIds.add(turnId); |
| | 541 | } |
| | 542 | } |
| | 543 | return turnIds; |
| | 544 | } |
| | 545 | |
| 515 | function workEntryIsActiveTurnActivity(entry: WorkLogEntry): boolean { | 546 | function workEntryIsActiveTurnActivity(entry: WorkLogEntry): boolean { |
| 516 | return ( | 547 | return ( |
| 517 | entry.toolLifecycleStatus === "inProgress" || | 548 | entry.toolLifecycleStatus === "inProgress" || |
| ... | @@ -529,7 +560,7 @@ function deriveTurnFolds(input: { | ... | @@ -529,7 +560,7 @@ function deriveTurnFolds(input: { |
| 529 | timelineEntries: ReadonlyArray<TimelineEntry>; | 560 | timelineEntries: ReadonlyArray<TimelineEntry>; |
| 530 | terminalAssistantMessageIds: ReadonlySet<string>; | 561 | terminalAssistantMessageIds: ReadonlySet<string>; |
| 531 | latestTurn: TimelineLatestTurn | null; | 562 | latestTurn: TimelineLatestTurn | null; |
| 532 | unsettledTurnId: TurnId | null; | 563 | unfoldedTurnIds: ReadonlySet<TurnId>; |
| 533 | }): ReadonlyMap<string, TurnFold> { | 564 | }): ReadonlyMap<string, TurnFold> { |
| 534 | interface TurnGroup { | 565 | interface TurnGroup { |
| 535 | entries: Array<TimelineEntry>; | 566 | entries: Array<TimelineEntry>; |
| ... | @@ -587,7 +618,7 @@ function deriveTurnFolds(input: { | ... | @@ -587,7 +618,7 @@ function deriveTurnFolds(input: { |
| 587 | | 618 | |
| 588 | const foldsByAnchorEntryId = new Map<string, TurnFold>(); | 619 | const foldsByAnchorEntryId = new Map<string, TurnFold>(); |
| 589 | for (const [turnId, group] of groupsByTurnId) { | 620 | for (const [turnId, group] of groupsByTurnId) { |
| 590 | if (turnId === input.unsettledTurnId) { | 621 | if (input.unfoldedTurnIds.has(turnId)) { |
| 591 | continue; | 622 | continue; |
| 592 | } | 623 | } |
| 593 | if (group.hasStreamingMessage) { | 624 | if (group.hasStreamingMessage) { |
| ... | @@ -769,11 +800,16 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -769,11 +800,16 @@ export function deriveMessagesTimelineRows(input: { |
| 769 | input.latestTurn ?? null, | 800 | input.latestTurn ?? null, |
| 770 | input.runningTurnId ?? null, | 801 | input.runningTurnId ?? null, |
| 771 | ); | 802 | ); |
| | 803 | const activeVisualResponseTurnIds = deriveActiveVisualResponseTurnIds({ |
| | 804 | timelineEntries: input.timelineEntries, |
| | 805 | unsettledTurnId, |
| | 806 | isWorking: input.isWorking, |
| | 807 | }); |
| 772 | const foldsByAnchorEntryId = deriveTurnFolds({ | 808 | const foldsByAnchorEntryId = deriveTurnFolds({ |
| 773 | timelineEntries: input.timelineEntries, | 809 | timelineEntries: input.timelineEntries, |
| 774 | terminalAssistantMessageIds, | 810 | terminalAssistantMessageIds, |
| 775 | latestTurn: input.latestTurn ?? null, | 811 | latestTurn: input.latestTurn ?? null, |
| 776 | unsettledTurnId, | 812 | unfoldedTurnIds: activeVisualResponseTurnIds, |
| 777 | }); | 813 | }); |
| 778 | const collapsedEntryIds = new Set<string>(); | 814 | const collapsedEntryIds = new Set<string>(); |
| 779 | for (const fold of foldsByAnchorEntryId.values()) { | 815 | for (const fold of foldsByAnchorEntryId.values()) { |
| ... | @@ -787,15 +823,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -787,15 +823,7 @@ export function deriveMessagesTimelineRows(input: { |
| 787 | let activeTurnHeaderIndex = input.timelineEntries.length; | 823 | let activeTurnHeaderIndex = input.timelineEntries.length; |
| 788 | if (input.isWorking) { | 824 | if (input.isWorking) { |
| 789 | const latestUserMessageIndex = lastUserMessageIndex(input.timelineEntries); | 825 | const latestUserMessageIndex = lastUserMessageIndex(input.timelineEntries); |
| 790 | const firstOwnedAfterUser = | 826 | activeTurnHeaderIndex = latestUserMessageIndex + 1; |
| 791 | unsettledTurnId === null | | |
| 792 | ? -1 | | |
| 793 | : input.timelineEntries.findIndex( | | |
| 794 | (entry, index) => | | |
| 795 | index > latestUserMessageIndex && timelineEntryTurnId(entry) === unsettledTurnId, | | |
| 796 | ); | | |
| 797 | activeTurnHeaderIndex = | | |
| 798 | firstOwnedAfterUser >= 0 ? firstOwnedAfterUser : latestUserMessageIndex + 1; | | |
| 799 | } | 827 | } |
| 800 | const entryBelongsToActiveTurn = (entry: TimelineEntry, index: number) => | 828 | const entryBelongsToActiveTurn = (entry: TimelineEntry, index: number) => |
| 801 | input.isWorking && | 829 | input.isWorking && |
| ... | @@ -862,10 +890,17 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -862,10 +890,17 @@ export function deriveMessagesTimelineRows(input: { |
| 862 | activeWorkRow !== null || latestToolFailed ? activeToolEntries.map((entry) => entry.id) : [], | 890 | activeWorkRow !== null || latestToolFailed ? activeToolEntries.map((entry) => entry.id) : [], |
| 863 | ); | 891 | ); |
| 864 | const appendWorkingRow = () => { | 892 | const appendWorkingRow = () => { |
| | 893 | const latestUserMessage = input.timelineEntries[lastUserMessageIndex(input.timelineEntries)]; |
| | 894 | const visualResponseStartedAt = |
| | 895 | activeVisualResponseTurnIds.size > 1 && |
| | 896 | latestUserMessage?.kind === "message" && |
| | 897 | latestUserMessage.message.role === "user" |
| | 898 | ? latestUserMessage.message.createdAt |
| | 899 | : input.activeTurnStartedAt; |
| 865 | nextRows.push({ | 900 | nextRows.push({ |
| 866 | kind: "working", | 901 | kind: "working", |
| 867 | id: "working-indicator-row", | 902 | id: "working-indicator-row", |
| 868 | createdAt: input.activeTurnStartedAt, | 903 | createdAt: visualResponseStartedAt, |
| 869 | }); | 904 | }); |
| 870 | }; | 905 | }; |
| 871 | let hasActivityRow = false; | 906 | let hasActivityRow = false; |
| ... | @@ -1081,10 +1116,11 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -1081,10 +1116,11 @@ export function deriveMessagesTimelineRows(input: { |
| 1081 | continue; | 1116 | continue; |
| 1082 | } | 1117 | } |
| 1083 | | 1118 | |
| 1084 | const assistantTurnStillInProgress = | 1119 | const assistantResponseStillInProgress = |
| 1085 | timelineEntry.message.role === "assistant" && | 1120 | timelineEntry.message.role === "assistant" && |
| 1086 | unsettledTurnId !== null && | 1121 | timelineEntry.message.turnId !== null && |
| 1087 | timelineEntry.message.turnId === unsettledTurnId; | 1122 | timelineEntry.message.turnId !== undefined && |
| | 1123 | activeVisualResponseTurnIds.has(timelineEntry.message.turnId); |
| 1088 | | 1124 | |
| 1089 | const durationStart = | 1125 | const durationStart = |
| 1090 | durationStartByMessageId.get(timelineEntry.message.id) ?? timelineEntry.message.createdAt; | 1126 | durationStartByMessageId.get(timelineEntry.message.id) ?? timelineEntry.message.createdAt; |
| ... | @@ -1095,7 +1131,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -1095,7 +1131,7 @@ export function deriveMessagesTimelineRows(input: { |
| 1095 | const showAssistantMeta = | 1131 | const showAssistantMeta = |
| 1096 | timelineEntry.message.role === "assistant" && | 1132 | timelineEntry.message.role === "assistant" && |
| 1097 | terminalAssistantMessageIds.has(timelineEntry.message.id) && | 1133 | terminalAssistantMessageIds.has(timelineEntry.message.id) && |
| 1098 | !assistantTurnStillInProgress; | 1134 | !assistantResponseStillInProgress; |
| 1099 | | 1135 | |
| 1100 | nextRows.push({ | 1136 | nextRows.push({ |
| 1101 | kind: "message", | 1137 | kind: "message", |
| ... | @@ -1105,7 +1141,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -1105,7 +1141,7 @@ export function deriveMessagesTimelineRows(input: { |
| 1105 | durationStart, | 1141 | durationStart, |
| 1106 | showAssistantMeta, | 1142 | showAssistantMeta, |
| 1107 | showAssistantCopyButton: showAssistantMeta, | 1143 | showAssistantCopyButton: showAssistantMeta, |
| 1108 | assistantCopyStreaming: timelineEntry.message.streaming || assistantTurnStillInProgress, | 1144 | assistantCopyStreaming: timelineEntry.message.streaming || assistantResponseStillInProgress, |
| 1109 | assistantTurnDiffSummary: | 1145 | assistantTurnDiffSummary: |
| 1110 | timelineEntry.message.role === "assistant" | 1146 | timelineEntry.message.role === "assistant" |
| 1111 | ? input.turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id) | 1147 | ? input.turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id) |