Keep older chat history and image previews stable (#2490)

* fix(app): keep timeline history and previews stable

Treat persisted timeline replicas as display-only so authoritative pagination always comes from the daemon. Require renewed user intent between older-history pages unless the viewport genuinely remains at the history edge.

Model assistant image acquisition as loading, loaded, or failed so recreating a preview URL cannot be rendered as an error.

* fix(app): keep file image previews current

* fix(app): stabilize history pagination edges

* fix(app): recover transient timeline loads

* fix(app): retain assistant image previews

* Stabilize history pagination lifecycle

* Stabilize history settlement and image previews

* Retry file images after reconnect

* Keep active previews and pagination requests alive

* Replace image hook tests with typed ports

* Integrate timeline hydration with submission authority

* Harden pre-hydration timeline reconciliation

* Make rewind E2E use real scroll intent

* Preserve live timeline state through hydration

* Keep mounted image attachments retained

* Protect preview persistence and lifecycle hydration

* Preserve idless live assistant continuations

* Preserve timeline rows across delayed hydration

* test(app): cover real assistant image files

* Preserve assistant tool ordering during hydration
This commit is contained in:
Mohamed Boudra
2026-07-28 22:29:03 +02:00
committed by GitHub
parent bbf3d0f9cc
commit 504b687f89
55 changed files with 4550 additions and 722 deletions
@@ -284,6 +284,39 @@ describe("MockLoadTestAgentClient", () => {
unsubscribe();
});
test("emits a settled assistant Markdown image path selected by prompt", async () => {
vi.useFakeTimers();
const client = new MockLoadTestAgentClient();
const session = await client.createSession({
provider: "mock",
cwd: process.cwd(),
model: "ten-second-stream",
});
const events: AgentStreamEvent[] = [];
const unsubscribe = session.subscribe((event) => events.push(event));
const markdown = "![Fixture image](screenshots/fixture.png)";
const resultPromise = session.run(`Emit settled assistant image Markdown: ${markdown}`);
await vi.advanceTimersByTimeAsync(0);
expect(
events.flatMap((event): AgentTimelineItem[] =>
event.type === "timeline" && event.item.type === "assistant_message" ? [event.item] : [],
),
).toEqual([
expect.objectContaining({
type: "assistant_message",
text: markdown,
}),
]);
await expect(resultPromise).resolves.toMatchObject({
sessionId: session.id,
finalText: markdown,
canceled: false,
});
unsubscribe();
});
test("agent manager coalesces adjacent assistant tokens into fewer messages", async () => {
vi.useFakeTimers();
const workdir = mkdtempSync(join(tmpdir(), "paseo-mock-load-test-"));
@@ -36,6 +36,8 @@ export const MOCK_LOAD_TEST_DEFAULT_MODEL_ID = "five-minute-stream";
const MOCK_LOAD_TEST_MODE_ID = "load-test";
const MOCK_LOAD_TEST_DURATION_MS = 5 * 60 * 1000;
const MOCK_LOAD_TEST_INTERVAL_MS = 40;
const ONE_PIXEL_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Wl4Kj8AAAAASUVORK5CYII=";
const CAPABILITIES: AgentCapabilityFlags = {
supportsStreaming: true,
@@ -156,6 +158,13 @@ function shouldEmitTurnFailure(prompt: AgentPromptInput): boolean {
return /emit\s+(?:a\s+)?synthetic\s+turn\s+failure/i.test(promptToText(prompt));
}
function parseSettledAssistantImageMarkdown(prompt: AgentPromptInput): string | null {
const match = /^emit settled assistant image markdown:\s*(!\[[^\]\r\n]*\]\(.+\))\s*$/i.exec(
promptToText(prompt),
);
return match?.[1] ?? null;
}
function parseMockQuestionPrompt(prompt: AgentPromptInput): MockQuestionPromptRequest | null {
const text = promptToText(prompt);
if (!/emit\s+(?:a\s+)?synthetic\s+questions?/i.test(text)) {
@@ -669,10 +678,13 @@ export class MockLoadTestAgentSession implements AgentSession {
const stress = parseAgentStreamStressPrompt(prompt);
const questionPrompt = parseMockQuestionPrompt(prompt);
const structuredBranchName = parseStructuredBranchNamePrompt(prompt);
const settledAssistantImageMarkdown = parseSettledAssistantImageMarkdown(prompt);
if (shouldEmitTurnFailure(prompt)) {
this.scheduleFailedTurn(turn);
} else if (structuredBranchName) {
this.scheduleStructuredJsonTurn(turn, structuredBranchName);
this.scheduleSettledAssistantTurn(turn, JSON.stringify(structuredBranchName));
} else if (settledAssistantImageMarkdown) {
this.scheduleSettledAssistantTurn(turn, settledAssistantImageMarkdown);
} else if (shouldEmitPlanApprovalPrompt(prompt)) {
this.schedulePlanApprovalTurn(turn);
} else if (questionPrompt) {
@@ -887,14 +899,14 @@ export class MockLoadTestAgentSession implements AgentSession {
turn.timer.unref?.();
}
private scheduleStructuredJsonTurn(turn: ActiveTurn, result: Record<string, string>): void {
private scheduleSettledAssistantTurn(turn: ActiveTurn, finalText: string): void {
turn.timer = setTimeout(() => {
this.emitStructuredJsonTurn(turn, result);
this.emitSettledAssistantTurn(turn, finalText);
}, 0);
turn.timer.unref?.();
}
private emitStructuredJsonTurn(turn: ActiveTurn, result: Record<string, string>): void {
private emitSettledAssistantTurn(turn: ActiveTurn, finalText: string): void {
if (this.activeTurn !== turn) {
return;
}
@@ -906,7 +918,6 @@ export class MockLoadTestAgentSession implements AgentSession {
turnId: turn.turnId,
});
const finalText = JSON.stringify(result);
this.emitTimeline(turn.turnId, {
type: "assistant_message",
text: finalText,
@@ -1117,9 +1128,14 @@ export class MockLoadTestAgentSession implements AgentSession {
}),
);
} else {
const imageBytes = Buffer.from(ONE_PIXEL_PNG_BASE64, "base64");
const imagePayload = Buffer.concat([
imageBytes,
Buffer.alloc(Math.max(0, largePayload.bytes - imageBytes.length)),
]).toString("base64");
this.emitTimeline(turn.turnId, {
type: "assistant_message",
text: `data:image/png;base64,${payload}`,
text: `![Synthetic image](data:image/png;base64,${imagePayload})`,
messageId: turn.assistantMessageId,
});
}