mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Preserve timeline rows across delayed hydration
This commit is contained in:
@@ -660,6 +660,53 @@ describe("processTimelineResponse", () => {
|
||||
expect(getUserTexts(result.head)).toEqual(["remote live prompt"]);
|
||||
});
|
||||
|
||||
it("keeps a provider-acknowledged painted-tail prompt newer than the bootstrap page", () => {
|
||||
const submitted = makeSubmittedUserMessage("submitted before bootstrap", "client-message-1");
|
||||
const live = processAgentStreamEvent({
|
||||
...baseStreamInput,
|
||||
event: {
|
||||
type: "timeline",
|
||||
provider: "claude",
|
||||
item: {
|
||||
type: "user_message",
|
||||
text: "canonical presentation",
|
||||
clientMessageId: "client-message-1",
|
||||
messageId: "provider-message-1",
|
||||
},
|
||||
},
|
||||
seq: 51,
|
||||
epoch: "epoch-1",
|
||||
currentTail: [makeAssistantItem("painted replica"), submitted],
|
||||
currentCursor: undefined,
|
||||
hasAuthoritativeBaseline: false,
|
||||
});
|
||||
|
||||
const result = processTimelineResponse({
|
||||
...baseTimelineInput,
|
||||
currentTail: live.tail,
|
||||
currentHead: live.head,
|
||||
sendingClientMessageIds: [],
|
||||
isInitializing: true,
|
||||
hasActiveInitDeferred: true,
|
||||
hasAuthoritativeBaseline: false,
|
||||
payload: {
|
||||
...baseTimelineInput.payload,
|
||||
direction: "tail",
|
||||
startCursor: { seq: 11 },
|
||||
endCursor: { seq: 50 },
|
||||
entries: [makeTimelineEntry(50, "canonical answer")],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getUserTexts([...result.tail, ...result.head])).toEqual(["submitted before bootstrap"]);
|
||||
expect(result.head[0]).toMatchObject({
|
||||
kind: "user_message",
|
||||
clientMessageId: "client-message-1",
|
||||
messageId: "provider-message-1",
|
||||
timelineCursor: { epoch: "epoch-1", seq: 51 },
|
||||
});
|
||||
});
|
||||
|
||||
it("does not duplicate a live row already covered by the bootstrap page", () => {
|
||||
const live = processAgentStreamEvent({
|
||||
...baseStreamInput,
|
||||
@@ -2202,7 +2249,10 @@ describe("processTimelineResponse", () => {
|
||||
|
||||
expect(getAssistantTexts(result.tail)).toEqual(["older chunk newer chunk"]);
|
||||
expect(result.tail[0]).toEqual(
|
||||
expect.objectContaining({ timelineCursor: { epoch: "epoch-1", seq: 3 } }),
|
||||
expect.objectContaining({
|
||||
id: "assistant-newer",
|
||||
timelineCursor: { epoch: "epoch-1", seq: 3 },
|
||||
}),
|
||||
);
|
||||
expect(result.cursor).toEqual({
|
||||
epoch: "epoch-1",
|
||||
|
||||
@@ -410,16 +410,15 @@ function mergePrependedCanonicalTail(olderTail: StreamItem[], currentTail: Strea
|
||||
return [...olderTail, ...currentTail];
|
||||
}
|
||||
|
||||
return [
|
||||
...olderTail.slice(0, -1),
|
||||
{
|
||||
...olderLast,
|
||||
text: `${olderLast.text}${currentFirst.text}`,
|
||||
timestamp: currentFirst.timestamp,
|
||||
...(currentFirst.timelineCursor ? { timelineCursor: currentFirst.timelineCursor } : {}),
|
||||
},
|
||||
...currentTail.slice(1),
|
||||
];
|
||||
const mergedAssistant: AssistantMessageItem = {
|
||||
...currentFirst,
|
||||
text: `${olderLast.text}${currentFirst.text}`,
|
||||
};
|
||||
if (mergedAssistant.messageId === undefined && olderLast.messageId !== undefined) {
|
||||
mergedAssistant.messageId = olderLast.messageId;
|
||||
}
|
||||
|
||||
return [...olderTail.slice(0, -1), mergedAssistant, ...currentTail.slice(1)];
|
||||
}
|
||||
|
||||
function replaceLiveAssistantWithProjectedText(params: {
|
||||
|
||||
@@ -406,6 +406,16 @@ export interface CanonicalStreamReplacementResult {
|
||||
acknowledgedClientMessageIds: string[];
|
||||
}
|
||||
|
||||
function isAfterCanonicalCoverage(
|
||||
position: TimelinePosition,
|
||||
coverage: CanonicalStreamReplacementInput["canonicalCoverage"],
|
||||
): boolean {
|
||||
return (
|
||||
position.epoch === coverage.epoch &&
|
||||
(coverage.endSeq === null || position.seq > coverage.endSeq)
|
||||
);
|
||||
}
|
||||
|
||||
function removeUserMessageAt(items: UserMessageItem[], index: number): UserMessageItem[] {
|
||||
return [...items.slice(0, index), ...items.slice(index + 1)];
|
||||
}
|
||||
@@ -503,9 +513,7 @@ function preserveReplacementHead(
|
||||
? currentHead.filter(
|
||||
(item) =>
|
||||
!item.timelineCursor ||
|
||||
(item.timelineCursor.epoch === canonicalCoverage.epoch &&
|
||||
(canonicalCoverage.endSeq === null ||
|
||||
item.timelineCursor.seq > canonicalCoverage.endSeq)) ||
|
||||
isAfterCanonicalCoverage(item.timelineCursor, canonicalCoverage) ||
|
||||
(item.kind === "assistant_message" &&
|
||||
canonicalTailAssistant?.kind === "assistant_message" &&
|
||||
item.text.startsWith(canonicalTailAssistant.text)),
|
||||
@@ -631,22 +639,25 @@ export function replaceWithCanonicalStream(
|
||||
nextTail.push(item);
|
||||
}
|
||||
|
||||
const retainedTailMessages: UserMessageItem[] = [];
|
||||
for (const local of unmatchedTailMessages) {
|
||||
if (!local.clientMessageId || !sendingClientMessageIds.has(local.clientMessageId)) {
|
||||
continue;
|
||||
if (local.clientMessageId && sendingClientMessageIds.has(local.clientMessageId)) {
|
||||
nextTail.push(local);
|
||||
} else if (
|
||||
input.preserveLiveHead &&
|
||||
local.timelineCursor &&
|
||||
isAfterCanonicalCoverage(local.timelineCursor, input.canonicalCoverage)
|
||||
) {
|
||||
retainedTailMessages.push(local);
|
||||
}
|
||||
nextTail.push(local);
|
||||
}
|
||||
nextHead = [...retainedTailMessages, ...nextHead];
|
||||
|
||||
nextHead = nextHead.filter((item) => {
|
||||
if (item.kind !== "user_message" || !item.clientMessageId) return true;
|
||||
if (sendingClientMessageIds.has(item.clientMessageId)) return true;
|
||||
if (!input.preserveLiveHead || !item.timelineCursor) return false;
|
||||
return (
|
||||
item.timelineCursor.epoch === input.canonicalCoverage.epoch &&
|
||||
(input.canonicalCoverage.endSeq === null ||
|
||||
item.timelineCursor.seq > input.canonicalCoverage.endSeq)
|
||||
);
|
||||
return isAfterCanonicalCoverage(item.timelineCursor, input.canonicalCoverage);
|
||||
});
|
||||
|
||||
const replacement = preserveReplacementHead(
|
||||
|
||||
Reference in New Issue
Block a user