diff --git a/packages/app/e2e/browser/agent-timeline-resume.spec.ts b/packages/app/e2e/browser/agent-timeline-resume.spec.ts index 8d164e954a..06c373bc00 100644 --- a/packages/app/e2e/browser/agent-timeline-resume.spec.ts +++ b/packages/app/e2e/browser/agent-timeline-resume.spec.ts @@ -11,6 +11,7 @@ import { seedLongMockAgentTimeline, } from "../support/helpers/timeline-pagination"; import { + answerFreshnessCheck, commitTimelineTurnsWhileDisconnected, disconnectViewedTimeline, expectResumeAnsweredWithoutFetch, @@ -21,6 +22,7 @@ import { restoreUnchangedViewedTimeline, restoreViewedTimelineHoldingCatchUp, restoreViewedTimelineWithHeldResponse, + returnWithFreshnessCheckUnanswered, waitForResumeAnswer, reopenAppWatchingForUpdatingToast, watchForUpdatingToast, @@ -101,6 +103,25 @@ test.describe("Agent timeline resume", () => { } }); + test("a check the daemon has not answered reports the update until it does", async ({ page }) => { + test.setTimeout(120_000); + const gate = await installDaemonWebSocketGate(page); + const agent = await seedLongMockAgentTimeline({ turns: 3 }); + try { + await openAgentTimeline(page, agent); + await expectTimelinePromptVisible(page, agent.newestPrompt); + const requests = rememberTimelineRequestCounts(gate); + + await returnWithFreshnessCheckUnanswered(page, gate); + await answerFreshnessCheck(page, gate); + + expectResumeAnsweredWithoutFetch(gate, requests); + } finally { + gate.restore(); + await agent.cleanup(); + } + }); + test("returning to a changed chat reports the update and shows the new messages", async ({ page, }) => { diff --git a/packages/app/e2e/browser/plugin-composer-items.spec.ts b/packages/app/e2e/browser/plugin-composer-items.spec.ts index 19218c55d4..2c669105ae 100644 --- a/packages/app/e2e/browser/plugin-composer-items.spec.ts +++ b/packages/app/e2e/browser/plugin-composer-items.spec.ts @@ -1,5 +1,11 @@ import { test } from "../support/fixtures"; -import { withComposerControlsExample } from "../support/helpers/plugin-composer-items"; +import { + expectToolbarNeverCondensedOrClipped, + recordToolbarFramesFromLoad, + withComposerControlsExample, + withPickerComposerItem, + withToggleAndWatcherComposerItems, +} from "../support/helpers/plugin-composer-items"; test("a narrow composer's overflow sheet shows a plugin item's row", async ({ page }) => { test.setTimeout(120_000); @@ -10,3 +16,66 @@ test("a narrow composer's overflow sheet shows a plugin item's row", async ({ pa await composer.expectFeatureRow("Insert checklist"); }); }); + +test("a toolbar with room keeps a plugin toggle inline at full density", async ({ page }) => { + test.setTimeout(120_000); + await withToggleAndWatcherComposerItems(page, async (composer) => { + // Wider than what renders, too narrow for two 125-wide "Plugin item" estimates. + await composer.setWindowWidth(1000); + await composer.expectToolbarWidthBetween(470, 620); + await composer.expectToggleInlineAtFullDensity(); + }); +}); + +test("a toolbar without room moves a plugin toggle to the overflow sheet and back", async ({ + page, +}) => { + test.setTimeout(120_000); + await withToggleAndWatcherComposerItems(page, async (composer) => { + await composer.setWindowWidth(1000); + await composer.expectToggleInlineAtFullDensity(); + // Too narrow for full labels, wide enough that the controls have not folded away. + await composer.setWindowWidth(880); + await composer.expectToolbarWidthBetween(300, 395); + await composer.expectToggleInOverflow(); + await composer.closeOverflowSheet(); + await composer.setWindowWidth(1000); + await composer.expectToggleInlineAtFullDensity(); + }); +}); + +test("an open plugin picker closes when its trigger moves to the overflow", async ({ page }) => { + test.setTimeout(120_000); + await withPickerComposerItem(page, async (composer) => { + await composer.setWindowWidth(1000); + await composer.openInlinePicker(); + await composer.setWindowWidth(880); + await composer.expectToolbarWidthBetween(300, 395); + await composer.expectPickerClosed(); + }); +}); + +test("the toolbar settles on one density without flipping as the window widens", async ({ + page, +}) => { + test.setTimeout(120_000); + await withToggleAndWatcherComposerItems(page, async (composer) => { + await composer.setWindowWidth(880); + await composer.expectToolbarWidthBetween(300, 395); + await composer.startRecordingDensity(); + for (let width = 880; width <= 1000; width += 8) await composer.setWindowWidth(width); + await composer.expectAtMostOneDensityChange(); + }); +}); + +test("a toolbar with room never paints condensed or clipped while the plugin loads", async ({ + page, +}) => { + test.setTimeout(120_000); + await page.setViewportSize({ width: 1000, height: 800 }); + await recordToolbarFramesFromLoad(page); + await withToggleAndWatcherComposerItems(page, async (composer) => { + await composer.expectToggleInlineAtFullDensity(); + await expectToolbarNeverCondensedOrClipped(page); + }); +}); diff --git a/packages/app/e2e/support/helpers/plugin-composer-items.ts b/packages/app/e2e/support/helpers/plugin-composer-items.ts index bac676ab38..678c9544da 100644 --- a/packages/app/e2e/support/helpers/plugin-composer-items.ts +++ b/packages/app/e2e/support/helpers/plugin-composer-items.ts @@ -1,8 +1,11 @@ +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; import { expect, test, type Page } from "@playwright/test"; import { gotoAppShell } from "./app"; import { openAgentRoute } from "./mock-agent"; import { connectNewWorkspaceDaemonClient } from "./new-workspace"; -import { copyPluginExample } from "./plugin-fixture"; +import { copyPluginExample, pluginRequirements } from "./plugin-fixture"; import { seedWorkspace } from "./seed-client"; const PLUGIN_ID = "composer-controls-example"; @@ -57,3 +60,236 @@ export async function withComposerControlsExample( await workspace.cleanup(); } } + +const ONE_TOGGLE_PLUGIN_ID = "composer-toggle-and-watcher-e2e"; +const ONE_TOGGLE_NAME = "Sandbox flag"; +const ONE_TOGGLE_PLUGIN_SOURCE = `import { useComposer } from "@getpaseo/plugin/client"; +import { Toggle } from "@getpaseo/plugin/client/ui"; +import { useState } from "react"; + +function PromptWatcher() { + useComposer(); + return null; +} + +function SandboxFlag() { + const [value, setValue] = useState(false); + return ( + setValue(next)} + /> + ); +} + +export default function contribute(client) { + client.addComposerItem({ id: "prompt-watcher", Component: PromptWatcher }); + client.addComposerItem({ id: "sandbox-flag", Component: SandboxFlag }); + return () => {}; +}`; + +const PICKER_NAME = "Review state"; +const PICKER_PLUGIN_SOURCE = `import { Select } from "@getpaseo/plugin/client/ui"; +import { useState } from "react"; + +const OPTIONS = [ + { value: "draft", label: "Draft" }, + { value: "ready", label: "Ready" }, +]; + +function ReviewState() { + const [value, setValue] = useState("draft"); + return ( +