feat(app): add chat outline navigation (#2792)
* feat(app): add chat outline navigation Long agent sessions need prompt navigation without coupling viewport movement to disposable server windows. Keep fetched timeline pages in client state so repeated jumps stay local, and settle prompt jumps against virtualized row measurement changes. * fix(app): preserve chat outline timeline coverage * fix(app): reconcile outline window submissions * fix(app): harden outline timeline navigation
This commit is contained in:
@@ -17,6 +17,8 @@ interface LongTimelineAgentOptions {
|
||||
}
|
||||
|
||||
export interface LongTimelineAgent extends MockAgentWorkspace {
|
||||
/** Every seeded prompt, oldest first, so a test can name the nth turn. */
|
||||
prompts: string[];
|
||||
firstOlderPagePrompt: string;
|
||||
initialTailAnchorPrompt: string;
|
||||
initialTailOldestPrompt: string;
|
||||
@@ -60,6 +62,7 @@ interface OlderHistoryPages {
|
||||
expectRepeatedEntries(): void;
|
||||
expectOwnedTextRendered(text: string): Promise<void>;
|
||||
releasePage(pageNumber: number): void;
|
||||
requestCount(): number;
|
||||
}
|
||||
|
||||
function promptForTurn(index: number): string {
|
||||
@@ -82,6 +85,7 @@ export async function seedLongMockAgentTimeline(
|
||||
|
||||
return {
|
||||
...agent,
|
||||
prompts: Array.from({ length: options.turns }, (_unused, index) => promptForTurn(index)),
|
||||
firstOlderPagePrompt: promptForTurn(Math.max(0, options.turns - 40)),
|
||||
initialTailAnchorPrompt: promptForTurn(Math.max(0, options.turns - 5)),
|
||||
initialTailOldestPrompt: promptForTurn(Math.max(0, options.turns - 20)),
|
||||
@@ -148,6 +152,25 @@ export async function rememberTimelinePromptPosition(
|
||||
return { prompt, top: box.y };
|
||||
}
|
||||
|
||||
export async function expectTimelinePromptLandedBelowTop(
|
||||
page: Page,
|
||||
prompt: string,
|
||||
): Promise<void> {
|
||||
const timeline = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||
const promptRow = timeline.locator("[data-history-row-id]").filter({ hasText: prompt });
|
||||
await expect(promptRow).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [timelineBox, promptBox] = await Promise.all([
|
||||
timeline.boundingBox(),
|
||||
promptRow.boundingBox(),
|
||||
]);
|
||||
if (!timelineBox || !promptBox) return Number.POSITIVE_INFINITY;
|
||||
return Math.abs(promptBox.y - timelineBox.y - 8);
|
||||
})
|
||||
.toBeLessThanOrEqual(2);
|
||||
}
|
||||
|
||||
export async function expectTimelinePromptPositionPreserved(
|
||||
page: Page,
|
||||
before: TimelinePromptPositionSnapshot,
|
||||
@@ -163,6 +186,39 @@ export async function expectTimelinePromptPositionPreserved(
|
||||
.toBeLessThanOrEqual(2);
|
||||
}
|
||||
|
||||
export async function expectTimelineAtMaximumScrollWithPromptVisible(
|
||||
page: Page,
|
||||
prompt: string,
|
||||
): Promise<void> {
|
||||
const timeline = page.locator('[data-testid="agent-chat-scroll"]:visible').first();
|
||||
const item = timeline.getByText(prompt, { exact: true });
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [metrics, timelineBox, promptBox] = await Promise.all([
|
||||
timeline.evaluate((element) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
throw new Error("Agent chat scroll element is not an HTMLElement");
|
||||
}
|
||||
return {
|
||||
clientHeight: element.clientHeight,
|
||||
scrollHeight: element.scrollHeight,
|
||||
scrollTop: element.scrollTop,
|
||||
};
|
||||
}),
|
||||
timeline.boundingBox(),
|
||||
item.boundingBox(),
|
||||
]);
|
||||
if (!timelineBox || !promptBox) return false;
|
||||
const isAtMaximumScroll =
|
||||
Math.abs(metrics.scrollTop + metrics.clientHeight - metrics.scrollHeight) <= 2;
|
||||
const isPromptFullyVisible =
|
||||
promptBox.y >= timelineBox.y &&
|
||||
promptBox.y + promptBox.height <= timelineBox.y + timelineBox.height;
|
||||
return isAtMaximumScroll && isPromptFullyVisible;
|
||||
})
|
||||
.toBe(true);
|
||||
}
|
||||
|
||||
export async function openAgentTimeline(
|
||||
page: Page,
|
||||
agent: MockAgentWorkspace,
|
||||
@@ -299,6 +355,9 @@ export async function holdOlderHistoryPages(
|
||||
releasePage(pageNumber) {
|
||||
gate.releasePage(pageNumber);
|
||||
},
|
||||
requestCount() {
|
||||
return gate.getRequestCount();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user