fix: stop stale chat renders after session switches
Roll back aggressive chat reuse paths that froze message parts, tool state, and completed assistant bodies when sessions were inactive or backgrounded.
This commit is contained in:
@@ -32,99 +32,6 @@ const getMessageCompletedAt = (message: ChatMessageEntry): number | undefined =>
|
||||
return typeof completed === 'number' ? completed : undefined;
|
||||
};
|
||||
|
||||
const getMessageFinish = (message: ChatMessageEntry): string | undefined => {
|
||||
const finish = (message.info as { finish?: unknown }).finish;
|
||||
return typeof finish === 'string' ? finish : undefined;
|
||||
};
|
||||
|
||||
const getMessageStatus = (message: ChatMessageEntry): string | undefined => {
|
||||
const status = (message.info as { status?: unknown }).status;
|
||||
return typeof status === 'string' ? status : undefined;
|
||||
};
|
||||
|
||||
const getPartText = (part: ChatMessageEntry['parts'][number]): string | undefined => {
|
||||
const text = (part as { text?: unknown }).text;
|
||||
if (typeof text === 'string') {
|
||||
return text;
|
||||
}
|
||||
const content = (part as { content?: unknown }).content;
|
||||
return typeof content === 'string' ? content : undefined;
|
||||
};
|
||||
|
||||
const arePartsEquivalentForReuse = (
|
||||
previousPart: ChatMessageEntry['parts'][number],
|
||||
nextPart: ChatMessageEntry['parts'][number],
|
||||
): boolean => {
|
||||
if (previousPart === nextPart) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previousPart.type !== nextPart.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousPart.id && nextPart.id && previousPart.id !== nextPart.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousPart.type === 'text' || previousPart.type === 'reasoning') {
|
||||
return getPartText(previousPart) === getPartText(nextPart);
|
||||
}
|
||||
|
||||
if (previousPart.type === 'tool') {
|
||||
const previousTool = previousPart as {
|
||||
tool?: unknown;
|
||||
callID?: unknown;
|
||||
state?: { status?: unknown };
|
||||
};
|
||||
const nextTool = nextPart as {
|
||||
tool?: unknown;
|
||||
callID?: unknown;
|
||||
state?: { status?: unknown };
|
||||
};
|
||||
|
||||
return previousTool.tool === nextTool.tool
|
||||
&& previousTool.callID === nextTool.callID
|
||||
&& previousTool.state?.status === nextTool.state?.status;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const areMessagesEquivalentForReuse = (previousMessage: ChatMessageEntry, nextMessage: ChatMessageEntry): boolean => {
|
||||
if (previousMessage === nextMessage) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (previousMessage.info.id !== nextMessage.info.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getMessageCompletedAt(previousMessage) !== getMessageCompletedAt(nextMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getMessageFinish(previousMessage) !== getMessageFinish(nextMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getMessageStatus(previousMessage) !== getMessageStatus(nextMessage)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousMessage.parts.length !== nextMessage.parts.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < previousMessage.parts.length; index += 1) {
|
||||
if (!arePartsEquivalentForReuse(previousMessage.parts[index], nextMessage.parts[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getUserSummaryBody = (message: ChatMessageEntry): string | undefined => {
|
||||
const summaryBody = (message.info as { summary?: { body?: unknown } | null | undefined })?.summary?.body;
|
||||
if (typeof summaryBody !== 'string') {
|
||||
@@ -194,7 +101,6 @@ export const projectTurnRecords = (
|
||||
|
||||
const turns: TurnRecord[] = [];
|
||||
const turnByUserId = new Map<string, TurnRecord>();
|
||||
const previousTurnsById = new Map((effectiveOptions.previousProjection?.turns ?? []).map((turn) => [turn.turnId, turn]));
|
||||
const groupedMessageIds = new Set<string>();
|
||||
let currentTurn: TurnRecord | undefined;
|
||||
|
||||
@@ -254,43 +160,6 @@ export const projectTurnRecords = (
|
||||
});
|
||||
|
||||
turns.forEach((turn) => {
|
||||
const previousTurn = previousTurnsById.get(turn.turnId);
|
||||
const canReuseComputed = (() => {
|
||||
if (!previousTurn) {
|
||||
return false;
|
||||
}
|
||||
if (previousTurn.stream.isStreaming) {
|
||||
return false;
|
||||
}
|
||||
if (!areMessagesEquivalentForReuse(previousTurn.userMessage, turn.userMessage)) {
|
||||
return false;
|
||||
}
|
||||
if (previousTurn.assistantMessages.length !== turn.assistantMessages.length) {
|
||||
return false;
|
||||
}
|
||||
for (let index = 0; index < turn.assistantMessages.length; index += 1) {
|
||||
if (!areMessagesEquivalentForReuse(previousTurn.assistantMessages[index], turn.assistantMessages[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})();
|
||||
|
||||
if (canReuseComputed && previousTurn) {
|
||||
turn.summary = previousTurn.summary;
|
||||
turn.summaryText = previousTurn.summaryText;
|
||||
turn.diffStats = previousTurn.diffStats;
|
||||
turn.activityParts = previousTurn.activityParts;
|
||||
turn.activitySegments = previousTurn.activitySegments;
|
||||
turn.hasTools = previousTurn.hasTools;
|
||||
turn.hasReasoning = previousTurn.hasReasoning;
|
||||
turn.stream = previousTurn.stream;
|
||||
turn.startedAt = previousTurn.startedAt;
|
||||
turn.completedAt = previousTurn.completedAt;
|
||||
turn.durationMs = previousTurn.durationMs;
|
||||
return;
|
||||
}
|
||||
|
||||
turn.summary = projectTurnSummary(turn.assistantMessages);
|
||||
turn.summaryText = turn.summary.text ?? getUserSummaryBody(turn.userMessage);
|
||||
turn.diffStats = projectTurnDiffStats(turn.userMessage);
|
||||
|
||||
@@ -1,6 +1,24 @@
|
||||
import { projectTurnIndexes } from './projectTurnIndexes';
|
||||
import type { TurnProjectionResult, TurnRecord } from './types';
|
||||
|
||||
const areTurnMessagesReferenceStable = (previousTurn: TurnRecord, nextTurn: TurnRecord): boolean => {
|
||||
if (previousTurn.userMessage !== nextTurn.userMessage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (previousTurn.assistantMessages.length !== nextTurn.assistantMessages.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (let index = 0; index < previousTurn.assistantMessages.length; index += 1) {
|
||||
if (previousTurn.assistantMessages[index] !== nextTurn.assistantMessages[index]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const buildTurnSignature = (turn: TurnRecord): string => {
|
||||
const assistantIds = turn.assistantMessageIds.join(',');
|
||||
return [
|
||||
@@ -40,6 +58,10 @@ export const stabilizeTurnProjection = (
|
||||
return turn;
|
||||
}
|
||||
|
||||
if (!areTurnMessagesReferenceStable(previousTurn, turn)) {
|
||||
return turn;
|
||||
}
|
||||
|
||||
reused = true;
|
||||
return previousTurn;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user