mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-28 12:12:47 +00:00
* fix(app): smooth chat history pagination and reserve image space Select the range at the pending reading offset before a prepend mounts rows. Reuse committed geometry for scroll input so a layout correction cannot hide the user's movement. Reserve image dimensions before loading and measure transient mounts in the browser regression. * fix(app): scope cached image dimensions to their source Raw file-path aliases let one workspace supply dimensions for another. Keep file metadata tied to its daemon and workspace, and close the delayed-image test server even when setup fails. * fix(app): retain local image context in row height estimates Pass the rendering server and workspace through height estimation so scoped image metadata remains usable before an older row mounts. * refactor(app): name row height estimate inputs
252 lines
7.5 KiB
TypeScript
252 lines
7.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { DEFAULT_CONTENT_MAX_WIDTH } from "@/styles/theme";
|
|
import type { StreamItem } from "@/types/stream";
|
|
import {
|
|
clearAssistantImageMetadataCache,
|
|
setAssistantImageMetadata,
|
|
} from "@/utils/assistant-image-metadata";
|
|
import {
|
|
DEFAULT_WEB_MOUNTED_RECENT_STREAM_ITEMS,
|
|
DEFAULT_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD,
|
|
estimateStreamItemHeight,
|
|
findMountedWindowStart,
|
|
getWebMountedRecentStreamItems,
|
|
getWebPartialVirtualizationThreshold,
|
|
splitWebVirtualizedHistory,
|
|
type IndexedStreamItem,
|
|
} from "./web-virtualization";
|
|
|
|
function createTimestamp(seed: number): Date {
|
|
return new Date(`2026-01-01T00:00:${seed.toString().padStart(2, "0")}.000Z`);
|
|
}
|
|
|
|
function userMessage(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "user_message",
|
|
id,
|
|
text: id,
|
|
timestamp: createTimestamp(seed),
|
|
};
|
|
}
|
|
|
|
function assistantMessage(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "assistant_message",
|
|
id,
|
|
text: id,
|
|
timestamp: createTimestamp(seed),
|
|
};
|
|
}
|
|
|
|
function toolCall(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "tool_call",
|
|
id,
|
|
timestamp: createTimestamp(seed),
|
|
payload: {
|
|
source: "orchestrator",
|
|
data: {
|
|
toolCallId: id,
|
|
toolName: "test_tool",
|
|
arguments: {},
|
|
status: "completed",
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function thought(id: string, seed: number): StreamItem {
|
|
return {
|
|
kind: "thought",
|
|
id,
|
|
text: id,
|
|
status: "ready",
|
|
timestamp: createTimestamp(seed),
|
|
};
|
|
}
|
|
|
|
function indexEntries(items: StreamItem[]): IndexedStreamItem[] {
|
|
return items.map((item, index) => ({ item, index }));
|
|
}
|
|
|
|
describe("findMountedWindowStart", () => {
|
|
it("keeps all items mounted when the chat is below the threshold", () => {
|
|
const items = [userMessage("u1", 1), assistantMessage("a1", 2)];
|
|
|
|
expect(
|
|
findMountedWindowStart({
|
|
items,
|
|
minMountedCount: 50,
|
|
}),
|
|
).toBe(0);
|
|
});
|
|
|
|
it("rewinds to the previous user boundary when the cutoff lands inside a turn", () => {
|
|
const items: StreamItem[] = [];
|
|
for (let index = 0; index < 30; index += 1) {
|
|
const seed = index * 3;
|
|
items.push(userMessage(`u${index}`, seed + 1));
|
|
items.push(toolCall(`t${index}`, seed + 2));
|
|
items.push(assistantMessage(`a${index}`, seed + 3));
|
|
}
|
|
|
|
expect(
|
|
findMountedWindowStart({
|
|
items,
|
|
minMountedCount: 50,
|
|
}),
|
|
).toBe(39);
|
|
});
|
|
});
|
|
|
|
describe("splitWebVirtualizedHistory", () => {
|
|
it("splits older entries into the virtualized section and keeps the recent window mounted", () => {
|
|
const items: StreamItem[] = [];
|
|
for (let index = 0; index < 30; index += 1) {
|
|
const seed = index * 2;
|
|
items.push(userMessage(`u${index}`, seed + 1));
|
|
items.push(assistantMessage(`a${index}`, seed + 2));
|
|
}
|
|
|
|
const window = splitWebVirtualizedHistory({
|
|
entries: indexEntries(items),
|
|
minMountedCount: 50,
|
|
});
|
|
|
|
expect(window.virtualizedEntries).toHaveLength(10);
|
|
expect(window.virtualizedEntries[0]?.item.id).toBe("u0");
|
|
expect(window.virtualizedEntries.at(-1)?.item.id).toBe("a4");
|
|
expect(window.mountedEntries[0]?.item.id).toBe("u5");
|
|
expect(window.mountedEntries).toHaveLength(50);
|
|
});
|
|
});
|
|
|
|
describe("estimateStreamItemHeight", () => {
|
|
it("uses compact estimates for collapsed tool sequence rows", () => {
|
|
expect(
|
|
estimateStreamItemHeight({
|
|
item: toolCall("tool", 1),
|
|
contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH,
|
|
}),
|
|
).toBe(40);
|
|
expect(
|
|
estimateStreamItemHeight({
|
|
item: thought("thought", 2),
|
|
contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH,
|
|
}),
|
|
).toBe(40);
|
|
});
|
|
|
|
it("uses a larger estimate for user messages with image attachments", () => {
|
|
const item: StreamItem = {
|
|
kind: "user_message",
|
|
id: "u-image",
|
|
text: "image",
|
|
timestamp: createTimestamp(1),
|
|
images: [
|
|
{
|
|
id: "att-1",
|
|
mimeType: "image/png",
|
|
storageType: "desktop-file",
|
|
storageKey: "/tmp/screenshot.png",
|
|
fileName: "screenshot.png",
|
|
byteSize: 1024,
|
|
createdAt: Date.now(),
|
|
},
|
|
],
|
|
};
|
|
|
|
expect(estimateStreamItemHeight({ item, contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH })).toBe(
|
|
220,
|
|
);
|
|
});
|
|
|
|
it("uses local image dimensions only from the row's server and workspace", () => {
|
|
clearAssistantImageMetadataCache();
|
|
const imageContext = { serverId: "server-one", workspaceRoot: "/workspace/one" };
|
|
setAssistantImageMetadata(
|
|
{ source: "portrait.png", ...imageContext },
|
|
{ width: 800, height: 1600 },
|
|
);
|
|
const item: StreamItem = {
|
|
kind: "assistant_message",
|
|
id: "local-image",
|
|
text: "",
|
|
timestamp: createTimestamp(2),
|
|
};
|
|
|
|
expect(estimateStreamItemHeight({ item, contentMaxWidth: 608, imageContext })).toBe(1264);
|
|
expect(
|
|
estimateStreamItemHeight({
|
|
item,
|
|
contentMaxWidth: 608,
|
|
imageContext: { ...imageContext, workspaceRoot: "/workspace/two" },
|
|
}),
|
|
).toBe(464);
|
|
expect(
|
|
estimateStreamItemHeight({
|
|
item,
|
|
contentMaxWidth: 608,
|
|
imageContext: { ...imageContext, serverId: "server-two" },
|
|
}),
|
|
).toBe(464);
|
|
});
|
|
|
|
it("uses cached assistant image metadata when available", () => {
|
|
clearAssistantImageMetadataCache();
|
|
setAssistantImageMetadata(
|
|
{
|
|
source: "https://example.com/tall.png",
|
|
},
|
|
{ width: 800, height: 1600 },
|
|
);
|
|
|
|
const item: StreamItem = {
|
|
kind: "assistant_message",
|
|
id: "a-image",
|
|
text: "Look at this\n\n",
|
|
timestamp: createTimestamp(2),
|
|
};
|
|
|
|
expect(
|
|
estimateStreamItemHeight({ item, contentMaxWidth: DEFAULT_CONTENT_MAX_WIDTH }),
|
|
).toBeGreaterThan(220);
|
|
});
|
|
});
|
|
|
|
describe("web virtualization test overrides", () => {
|
|
it("uses defaults unless explicit positive integer overrides are present", () => {
|
|
const globalWithOverrides = globalThis as typeof globalThis & {
|
|
__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD?: unknown;
|
|
__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS?: unknown;
|
|
};
|
|
const previousThreshold = globalWithOverrides.__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD;
|
|
const previousMounted = globalWithOverrides.__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS;
|
|
|
|
try {
|
|
delete globalWithOverrides.__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD;
|
|
delete globalWithOverrides.__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS;
|
|
expect(getWebPartialVirtualizationThreshold()).toBe(
|
|
DEFAULT_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD,
|
|
);
|
|
expect(getWebMountedRecentStreamItems()).toBe(DEFAULT_WEB_MOUNTED_RECENT_STREAM_ITEMS);
|
|
|
|
globalWithOverrides.__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD = 6;
|
|
globalWithOverrides.__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS = 4;
|
|
expect(getWebPartialVirtualizationThreshold()).toBe(6);
|
|
expect(getWebMountedRecentStreamItems()).toBe(4);
|
|
} finally {
|
|
if (previousThreshold === undefined) {
|
|
delete globalWithOverrides.__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD;
|
|
} else {
|
|
globalWithOverrides.__PASEO_E2E_WEB_PARTIAL_VIRTUALIZATION_THRESHOLD = previousThreshold;
|
|
}
|
|
if (previousMounted === undefined) {
|
|
delete globalWithOverrides.__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS;
|
|
} else {
|
|
globalWithOverrides.__PASEO_E2E_WEB_MOUNTED_RECENT_STREAM_ITEMS = previousMounted;
|
|
}
|
|
}
|
|
});
|
|
});
|