mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
* feat(chat): fork assistant turns into new drafts Builds chat-history attachments on the daemon and routes them into draft composers so users can continue an assistant turn in an existing workspace tab or New Workspace. * fix(chat): clean up fork draft handling * fix(chat): address fork draft review feedback * fix(server): build fork context from bounded timeline * fix(app): restore assistant turn footer in streams * fix(app): preserve fork draft setup * fix(app): tighten assistant fork footers * fix(app): lock active fork draft handoff * fix(app): preserve attachment-only draft retries * fix(app): unify composer attachment scopes * fix(app): align chat history attachment labels * fix(app): centralize attachment pill content * fix(app): trim fork context for workspace naming
160 lines
5.8 KiB
TypeScript
160 lines
5.8 KiB
TypeScript
import { test, expect } from "./fixtures";
|
|
import {
|
|
awaitAssistantMessage,
|
|
expectAgentIdle,
|
|
expectInlineWorkingIndicator,
|
|
expectTurnCopyButton,
|
|
expectScrollFollowsNewContent,
|
|
} from "./helpers/agent-stream";
|
|
import {
|
|
expectScrollStaysFixed,
|
|
readScrollMetrics,
|
|
scrollAgentChatToBottom,
|
|
scrollChatAwayFromBottom,
|
|
waitForScrollableChat,
|
|
} from "./helpers/agent-bottom-anchor";
|
|
import { delayCreatedAgentInitialTailResponse } from "./helpers/agent-timeline-gate";
|
|
import { selectModel } from "./helpers/app";
|
|
import { clickNewChat } from "./helpers/launcher";
|
|
import { expectComposerVisible, startRunningMockAgent } from "./helpers/composer";
|
|
import { openAgentRoute, seedMockAgentWorkspace } from "./helpers/mock-agent";
|
|
|
|
const SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE = 360;
|
|
|
|
test.describe("Agent stream UI", () => {
|
|
test("auto-scroll sticks to bottom across token bursts", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await startRunningMockAgent(page, {
|
|
prefix: "stream-scroll-",
|
|
model: "one-minute-stream",
|
|
prompt: "Stream for auto-scroll test.",
|
|
});
|
|
try {
|
|
await awaitAssistantMessage(page);
|
|
await expectScrollFollowsNewContent(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps the viewport fixed after the user scrolls away during a stream", async ({ page }) => {
|
|
test.setTimeout(120_000);
|
|
const agent = await seedMockAgentWorkspace({
|
|
repoPrefix: "stream-scroll-away-",
|
|
title: "Scroll-away anchor",
|
|
model: "five-minute-stream",
|
|
initialPrompt: "emit 120 agent stream updates for scroll-away setup.",
|
|
});
|
|
try {
|
|
await agent.client.waitForFinish(agent.agentId, 30_000);
|
|
await openAgentRoute(page, {
|
|
workspaceId: agent.workspaceId,
|
|
agentId: agent.agentId,
|
|
});
|
|
await expectComposerVisible(page);
|
|
await agent.client.sendAgentMessage(agent.agentId, "Stream for scroll-away anchor test.");
|
|
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await awaitAssistantMessage(page);
|
|
await waitForScrollableChat(page, {
|
|
minScrollableDistance: SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE,
|
|
timeout: 30_000,
|
|
});
|
|
const baseline = await scrollChatAwayFromBottom(page, {
|
|
deltaY: -900,
|
|
minDistanceFromBottom: 300,
|
|
});
|
|
await expectScrollStaysFixed(page, baseline, { durationMs: 30_000 });
|
|
|
|
const finalMetrics = await readScrollMetrics(page);
|
|
expect(finalMetrics.contentHeight).toBeGreaterThan(baseline.contentHeight);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("keeps the viewport fixed when delayed authoritative history arrives after scroll-away", async ({
|
|
page,
|
|
withWorkspace,
|
|
}) => {
|
|
test.setTimeout(180_000);
|
|
const timelineGate = await delayCreatedAgentInitialTailResponse(page);
|
|
const workspace = await withWorkspace({
|
|
prefix: "stream-scroll-away-delayed-history-",
|
|
});
|
|
await workspace.navigateTo();
|
|
await clickNewChat(page);
|
|
await page.getByText("Model defaults are still loading").waitFor({
|
|
state: "hidden",
|
|
timeout: 30_000,
|
|
});
|
|
await expectComposerVisible(page);
|
|
await selectModel(page, "Five minute stream");
|
|
|
|
const prompt = "Stream for delayed authoritative history scroll-away test.";
|
|
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
|
await composer.fill(prompt);
|
|
await page.getByRole("button", { name: "Send message" }).click();
|
|
await page.getByText(prompt, { exact: true }).first().waitFor({
|
|
state: "visible",
|
|
timeout: 30_000,
|
|
});
|
|
await timelineGate.waitForCreatedAgent();
|
|
await timelineGate.waitForDelayedResponse();
|
|
await expect(page.getByRole("button", { name: /stop|cancel/i }).first()).toBeVisible({
|
|
timeout: 30_000,
|
|
});
|
|
await awaitAssistantMessage(page);
|
|
await waitForScrollableChat(page, {
|
|
minScrollableDistance: SCROLL_AWAY_MIN_SCROLLABLE_DISTANCE,
|
|
timeout: 45_000,
|
|
});
|
|
const baseline = await scrollChatAwayFromBottom(page, {
|
|
deltaY: -900,
|
|
minDistanceFromBottom: 300,
|
|
});
|
|
|
|
timelineGate.release();
|
|
await timelineGate.waitForForwardedResponse();
|
|
await expectScrollStaysFixed(page, baseline);
|
|
});
|
|
|
|
test("working-indicator transitions to copy-button when stream ends", async ({ page }) => {
|
|
test.setTimeout(60_000);
|
|
const agent = await startRunningMockAgent(page, {
|
|
prefix: "stream-indicator-",
|
|
model: "ten-second-stream",
|
|
prompt: "Stream briefly for indicator transition test.",
|
|
});
|
|
try {
|
|
await awaitAssistantMessage(page);
|
|
await expectInlineWorkingIndicator(page);
|
|
await expectAgentIdle(page, 30_000);
|
|
await scrollAgentChatToBottom(page);
|
|
await expectTurnCopyButton(page);
|
|
} finally {
|
|
await agent.cleanup();
|
|
}
|
|
});
|
|
|
|
test("shows elapsed timer on first app-created running turn", async ({ page, withWorkspace }) => {
|
|
test.setTimeout(90_000);
|
|
const workspace = await withWorkspace({ prefix: "stream-first-app-turn-timer-" });
|
|
await workspace.navigateTo();
|
|
await clickNewChat(page);
|
|
await page.getByText("Model defaults are still loading").waitFor({
|
|
state: "hidden",
|
|
timeout: 30_000,
|
|
});
|
|
const prompt = "Stream briefly for first app-created turn timer test.";
|
|
const composer = page.getByRole("textbox", { name: "Message agent..." }).first();
|
|
await composer.fill(prompt);
|
|
await page.getByRole("button", { name: "Send message" }).click();
|
|
await page.getByText(prompt, { exact: true }).first().waitFor({ state: "visible" });
|
|
await awaitAssistantMessage(page);
|
|
await expectInlineWorkingIndicator(page);
|
|
await page.getByTestId("turn-working-elapsed").waitFor({ state: "visible", timeout: 5_000 });
|
|
});
|
|
});
|