diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd46c9c5..938b2fc1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa ## [Unreleased] ### Added +- Enter twice sends a message now while the agent works: the first Enter queues it as before, and + a second within a second (on the empty box, or while the first is still on its way) sends it at + once, as its Send now does — steering the running turn instead of waiting for it to end. It + works the same with automatic sending off. **Settings → Chat → Enter twice sends now** turns + it off. - **Settings → Phone & devices → Portal** gives this PC a public HTTPS address through [Portal](https://github.com/gosuda/portal-tunnel), with no account and no domain of your own. With Portal v2.6.1 or later installed, **Start** runs it on a relay you enter, dropping any diff --git a/scripts/ci-browser.sh b/scripts/ci-browser.sh index d35614132..66345e887 100644 --- a/scripts/ci-browser.sh +++ b/scripts/ci-browser.sh @@ -78,6 +78,7 @@ run_script scripts/chat-greeting-demo-regression.ts run_script scripts/restored-draft-demo-regression.ts run_script scripts/composer-fit-demo-regression.ts run_script scripts/held-rows-demo-regression.ts +run_script scripts/enter-twice-demo-regression.ts run_script scripts/sidebar-activity-demo-regression.ts run_script scripts/sidebar-spacing-demo-regression.ts run_script scripts/workspace-touch-reorder-demo-regression.ts diff --git a/scripts/ci-browser.test.ts b/scripts/ci-browser.test.ts index aaf927289..6929eb72f 100644 --- a/scripts/ci-browser.test.ts +++ b/scripts/ci-browser.test.ts @@ -69,7 +69,8 @@ describe("browser lane", () => { it("summarizes every script and exits zero on success without enabling local evidence", async () => { const { text, code, evidence } = await lane(); expect(code).toBe(0); - expect(rows(text)).toHaveLength(45); + expect(rows(text)).toHaveLength(46); + expect(rows(text).map((row) => row.script)).toContain("scripts/enter-twice-demo-regression.ts"); expect(rows(text).map((row) => row.script)).toContain("scripts/portal-regression.ts"); expect(rows(text).map((row) => row.script)).toContain("scripts/agy-chat-browser-qa.ts"); expect(rows(text).filter((row) => row.script === "scripts/ui-contexts-regression.ts")).toHaveLength(1); diff --git a/scripts/enter-twice-demo-regression.ts b/scripts/enter-twice-demo-regression.ts new file mode 100644 index 000000000..0cfe0c649 --- /dev/null +++ b/scripts/enter-twice-demo-regression.ts @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { Page } from "playwright-core"; +import panes from "../site/demo/fixtures/panes.json"; +import { buildDemoApp } from "./demo-build.ts"; + +// Enter twice while the agent works (Settings → Enter twice sends now), on the unmodified app over +// the demo's fixture transport: the first Enter queues the message, a second within a second sends +// it now, as its Send now does — also when it comes before the bridge's receipt for the first. A +// single Enter, a second one too late, or the setting off leaves it queued. The demo's mock turn +// takes 2.4 s and sends what is still queued when it ends, so a message sent now is one that joins +// the chat while the turn still works. All files and HTTP traffic stay in this disposable, +// loopback-only app; no herdr session is opened. +const app = mkdtempSync(join(tmpdir(), "herdr-enter-twice-demo-")); + +// playwright-core's bundle lazily requires packages the lockfile does not carry (chromium-bidi): +// fine at run time, unresolvable to `bun build` — so it is imported past the bundler's analysis +const playwrightCore = "playwright-core"; +const { chromium } = (await import(playwrightCore)) as typeof import("playwright-core"); + +/** The user turns the chat shows, in order. */ +const userTurns = (page: Page): Promise => page.locator(".chat-turn-user .chat-bubble").allTextContents(); +/** The demo turn's answers so far: a message sent now joins the chat before the running turn's answer. */ +const answers = (page: Page): Promise => page.locator(".chat-turn-agent", { hasText: "This is the demo, so nothing ran" }).count(); +const pendingRows = (page: Page): Promise => page.locator(".pending-message-bubble").allTextContents(); +/** The agent works: the empty box's control is Stop. */ +const working = (page: Page): Promise => page.getByRole("button", { name: "Stop agent", exact: true }).waitFor({ timeout: 10_000 }); + +try { + await buildDemoApp(app); + const index = join(app, "index.html"); + const html = readFileSync(index, "utf8"); + assert.match(html, /`; + writeFileSync(index, html.replace(/\n ${delay}\n