diff --git a/CHANGELOG.md b/CHANGELOG.md index d39c544202..22df925091 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ between releases do not reach them. Remote-PC runtime bundles are versioned sepa chat or terminal, under the header's: its actions (rename, split, zoom, close) are in the tab's ellipsis menu, between the tab's panes and the tab's own actions. A split pane on a wide touch screen keeps its own menu. +- On a phone, a long press on the chat selects text and offers copy as the phone does, instead of + opening the pane's actions menu; on the terminal it opens no menu either. A right-click, or a + menu asked for from the keyboard, still opens it. ## [0.4.7] - 2026-10-12 diff --git a/scripts/ci-browser.sh b/scripts/ci-browser.sh index 6d37dfcd4f..15a230648a 100644 --- a/scripts/ci-browser.sh +++ b/scripts/ci-browser.sh @@ -88,6 +88,7 @@ run_script scripts/prompt-dock-demo-regression.ts run_script scripts/plugin-actions-demo-regression.ts run_script scripts/palette-machines-demo-regression.ts run_script scripts/narrow-pane-demo-regression.ts +run_script scripts/touch-long-press-demo-regression.ts run_script scripts/recent-folders-regression.ts run_script scripts/machine-dialog-regression.ts run_script scripts/machine-conflict-regression.ts diff --git a/scripts/ci-browser.test.ts b/scripts/ci-browser.test.ts index ccf2f31434..41d49816dc 100644 --- a/scripts/ci-browser.test.ts +++ b/scripts/ci-browser.test.ts @@ -59,7 +59,7 @@ 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(36); + expect(rows(text)).toHaveLength(37); expect(rows(text).map((row) => row.script)).toContain("scripts/agy-chat-browser-qa.ts"); expect(rows(text).map((row) => row.script)).toContain("scripts/pending-pane-switch-regression.ts"); expect(rows(text).map((row) => row.script)).toContain("scripts/chat-fallback-browser-qa.ts"); diff --git a/scripts/touch-long-press-demo-regression.ts b/scripts/touch-long-press-demo-regression.ts new file mode 100644 index 0000000000..952c246489 --- /dev/null +++ b/scripts/touch-long-press-demo-regression.ts @@ -0,0 +1,79 @@ +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 { chromium, type Page } from "playwright-core"; +import panes from "../site/demo/fixtures/panes.json"; +import { buildDemoApp } from "./demo-build.ts"; + +// A long press on a pane on a touch screen is the phone's own (select text, copy): it does not +// open the pane menu. A right-click still does. On the unmodified app over the demo's fixture +// transport: no herdr session is opened. +const app = mkdtempSync(join(tmpdir(), "herdr-touch-long-press-demo-")); + +/** + * Fires a contextmenu on the chat as `pointerType` would, and says whether the page took it. + * "legacy-touch": a browser whose contextmenu carries no pointer type, after a touch; + * "keyboard": Shift+F10 after that touch, a contextmenu with no pointer type either. + */ +const press = (page: Page, how: "touch" | "mouse" | "legacy-touch" | "keyboard"): Promise => page.locator(".pane-frame.is-current .pane-frame-body").evaluate((body, how) => { + const target = body.querySelector(".chat-view, .terminal-stack") ?? body; + const rect = target.getBoundingClientRect(); + const type = how === "mouse" ? "mouse" : "touch"; + const at = { bubbles: true, cancelable: true, composed: true, clientX: rect.left + rect.width / 2, clientY: rect.top + rect.height / 3, pointerType: type, isPrimary: true }; + target.dispatchEvent(new PointerEvent("pointerdown", { ...at, button: type === "mouse" ? 2 : 0, buttons: type === "mouse" ? 2 : 1 })); + target.dispatchEvent(new PointerEvent("pointerup", { ...at, button: type === "mouse" ? 2 : 0, buttons: 0 })); + if (how === "keyboard") target.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, composed: true, key: "F10", shiftKey: true })); + const event = how === "touch" || how === "mouse" ? new PointerEvent("contextmenu", { ...at, button: 2 }) : new MouseEvent("contextmenu", { ...at, button: 2 }); + target.dispatchEvent(event); + return event.defaultPrevented; +}, how); + +try { + await buildDemoApp(app); + const index = join(app, "index.html"); + writeFileSync(index, readFileSync(index, "utf8").replace(/\n