| ... | ... | @@ -512,6 +512,37 @@ function timelineEntryTurnId(entry: TimelineEntry): TurnId | null { |
| 512 | 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 | 546 | function workEntryIsActiveTurnActivity(entry: WorkLogEntry): boolean { |
| 516 | 547 | return ( |
| 517 | 548 | entry.toolLifecycleStatus === "inProgress" || |
| ... | ... | @@ -529,7 +560,7 @@ function deriveTurnFolds(input: { |
| 529 | 560 | timelineEntries: ReadonlyArray<TimelineEntry>; |
| 530 | 561 | terminalAssistantMessageIds: ReadonlySet<string>; |
| 531 | 562 | latestTurn: TimelineLatestTurn | null; |
| 532 | | unsettledTurnId: TurnId | null; |
| 563 | unfoldedTurnIds: ReadonlySet<TurnId>; |
| 533 | 564 | }): ReadonlyMap<string, TurnFold> { |
| 534 | 565 | interface TurnGroup { |
| 535 | 566 | entries: Array<TimelineEntry>; |
| ... | ... | @@ -587,7 +618,7 @@ function deriveTurnFolds(input: { |
| 587 | 618 | |
| 588 | 619 | const foldsByAnchorEntryId = new Map<string, TurnFold>(); |
| 589 | 620 | for (const [turnId, group] of groupsByTurnId) { |
| 590 | | if (turnId === input.unsettledTurnId) { |
| 621 | if (input.unfoldedTurnIds.has(turnId)) { |
| 591 | 622 | continue; |
| 592 | 623 | } |
| 593 | 624 | if (group.hasStreamingMessage) { |
| ... | ... | @@ -769,11 +800,16 @@ export function deriveMessagesTimelineRows(input: { |
| 769 | 800 | input.latestTurn ?? null, |
| 770 | 801 | input.runningTurnId ?? null, |
| 771 | 802 | ); |
| 803 | const activeVisualResponseTurnIds = deriveActiveVisualResponseTurnIds({ |
| 804 | timelineEntries: input.timelineEntries, |
| 805 | unsettledTurnId, |
| 806 | isWorking: input.isWorking, |
| 807 | }); |
| 772 | 808 | const foldsByAnchorEntryId = deriveTurnFolds({ |
| 773 | 809 | timelineEntries: input.timelineEntries, |
| 774 | 810 | terminalAssistantMessageIds, |
| 775 | 811 | latestTurn: input.latestTurn ?? null, |
| 776 | | unsettledTurnId, |
| 812 | unfoldedTurnIds: activeVisualResponseTurnIds, |
| 777 | 813 | }); |
| 778 | 814 | const collapsedEntryIds = new Set<string>(); |
| 779 | 815 | for (const fold of foldsByAnchorEntryId.values()) { |
| ... | ... | @@ -787,15 +823,7 @@ export function deriveMessagesTimelineRows(input: { |
| 787 | 823 | let activeTurnHeaderIndex = input.timelineEntries.length; |
| 788 | 824 | if (input.isWorking) { |
| 789 | 825 | const latestUserMessageIndex = lastUserMessageIndex(input.timelineEntries); |
| 790 | | const firstOwnedAfterUser = |
| 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; |
| 826 | activeTurnHeaderIndex = latestUserMessageIndex + 1; |
| 799 | 827 | } |
| 800 | 828 | const entryBelongsToActiveTurn = (entry: TimelineEntry, index: number) => |
| 801 | 829 | input.isWorking && |
| ... | ... | @@ -862,10 +890,17 @@ export function deriveMessagesTimelineRows(input: { |
| 862 | 890 | activeWorkRow !== null || latestToolFailed ? activeToolEntries.map((entry) => entry.id) : [], |
| 863 | 891 | ); |
| 864 | 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 | 900 | nextRows.push({ |
| 866 | 901 | kind: "working", |
| 867 | 902 | id: "working-indicator-row", |
| 868 | | createdAt: input.activeTurnStartedAt, |
| 903 | createdAt: visualResponseStartedAt, |
| 869 | 904 | }); |
| 870 | 905 | }; |
| 871 | 906 | let hasActivityRow = false; |
| ... | ... | @@ -1081,10 +1116,11 @@ export function deriveMessagesTimelineRows(input: { |
| 1081 | 1116 | continue; |
| 1082 | 1117 | } |
| 1083 | 1118 | |
| 1084 | | const assistantTurnStillInProgress = |
| 1119 | const assistantResponseStillInProgress = |
| 1085 | 1120 | timelineEntry.message.role === "assistant" && |
| 1086 | | unsettledTurnId !== null && |
| 1087 | | timelineEntry.message.turnId === unsettledTurnId; |
| 1121 | timelineEntry.message.turnId !== null && |
| 1122 | timelineEntry.message.turnId !== undefined && |
| 1123 | activeVisualResponseTurnIds.has(timelineEntry.message.turnId); |
| 1088 | 1124 | |
| 1089 | 1125 | const durationStart = |
| 1090 | 1126 | durationStartByMessageId.get(timelineEntry.message.id) ?? timelineEntry.message.createdAt; |
| ... | ... | @@ -1095,7 +1131,7 @@ export function deriveMessagesTimelineRows(input: { |
| 1095 | 1131 | const showAssistantMeta = |
| 1096 | 1132 | timelineEntry.message.role === "assistant" && |
| 1097 | 1133 | terminalAssistantMessageIds.has(timelineEntry.message.id) && |
| 1098 | | !assistantTurnStillInProgress; |
| 1134 | !assistantResponseStillInProgress; |
| 1099 | 1135 | |
| 1100 | 1136 | nextRows.push({ |
| 1101 | 1137 | kind: "message", |
| ... | ... | @@ -1105,7 +1141,7 @@ export function deriveMessagesTimelineRows(input: { |
| 1105 | 1141 | durationStart, |
| 1106 | 1142 | showAssistantMeta, |
| 1107 | 1143 | showAssistantCopyButton: showAssistantMeta, |
| 1108 | | assistantCopyStreaming: timelineEntry.message.streaming || assistantTurnStillInProgress, |
| 1144 | assistantCopyStreaming: timelineEntry.message.streaming || assistantResponseStillInProgress, |
| 1109 | 1145 | assistantTurnDiffSummary: |
| 1110 | 1146 | timelineEntry.message.role === "assistant" |
| 1111 | 1147 | ? input.turnDiffSummaryByAssistantMessageId.get(timelineEntry.message.id) |