Pane-header right-click menu: bound ports, click to connect#289
Pane-header right-click menu: bound ports, click to connect#289nedtwigg wants to merge 14 commits into
Conversation
…menu Groundwork for a pane-header right-click menu that lists a terminal's bound ports and connects to one like `dor ab open`: - port-url.ts: listenerUrlsByPort grouping shared by surface.resolveOpen and the upcoming menu (loopback wins localhost, IPv6 bracketed) - use-dor-control: reuse-or-create surface logic extracted as ensureAgentBrowserSurface; the split reference resolves lazily so a minimized caller can still refresh an existing surface - connect-port.ts: webview reproduction of `dor ab open <url>` against the workspace's default agent-browser session - WallActions: resolveSurfaceRef + onConnectPort - dor-lib-common: browser-safe ./agent-browser subpath export Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erence through - useDorControl returns a bound connectPort(id, url) instead of exposing ensureAgentBrowserSurface for Wall.tsx to reassemble; Wall delegates in one line like its sibling actions - connectPortToDefaultBrowser takes the reference as a lazy thunk, so the reuse path survives a pane vanishing between right-click and connect - wall-context reuses ConnectPortResult instead of restating it inline - hoist the duplicated refreshedParams guard; drop an eta-expansion Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…connect Right-clicking a terminal pane header's bare regions opens a context menu showing the pane's stable dor handle (surface:N) and the TCP ports its process tree binds — spinner while the scan runs, then one host:port row per distinct port (process name muted), or no-ports / scan-failed lines. Clicking a row reproduces `dor ab open <url>`: the URL opens in the default agent-browser session and its browser surface is reused or created, focus-neutrally. Rows are inert labels on hosts that cannot run agent-browser. Title-span and bell right-clicks keep their own popovers. Verified live in the standalone agent-browser harness: menu shows surface:1 + localhost:8123 (Python), click splits in the screencast surface on dormouse.1.default, second connect reuses the same pane. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…stub dedupe
- useDismissOverlay: one dismissal contract (outside pointerdown, Escape,
resize, capture scroll) replacing three verbatim copies in the context
menu, TitleCandidatesPopover (now self-owned, matching the menu's
placement), and IllegalRenameWarning (auto-dismiss timer stays local)
- POPUP_SURFACE_CLASS in design.tsx replaces four copies of the anchored
popover chrome recipe
- PaneHeaderContextMenu reads WallActions from context: props shrink to
{id, anchor, onClose}, the surfaceRef snapshot and pre-bound onConnect
disappear; drop the no-op aliveRef and a redundant scanning reset
- stubWallActions/ensureResizeObserver in wall-test-utils.ts replace four
byte-identical stubActions copies (excluded from the app tsconfig — it
imports vitest)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…d boot
Fixes two pieces of feedback on the pane-header context menu:
- One right-click menu: the title span no longer owns its own right-click
(only the alert bell keeps one); the diagnostic title-candidates popover
is now a "title candidates" item at the bottom of the context menu,
anchored to the title span.
- Instant connect: clicking a port closes the menu and creates the browser
pane in the same tick (~16ms measured, was 1-3s of daemon boot). The
eager pane is deliberately created without `session` so the controller's
stale-port recovery stays inert and cannot race the daemon boot; the
panel shows "Connecting to browser session…" while the background
`agent-browser open` + `stream status` run, then one params refresh
hands it {session, wsPort, binaryPath} and the stream connects. Rapid
re-clicks reuse the still-booting pane; failures log to the console.
Verified live in the standalone harness: click→pane 16ms, menu closed
same-tick, stream connected and navigated; title-span right-click opens
the single menu and its item opens the candidates popover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s invariant - ensureAgentBrowserSurface takes optional session/url; the connect flow's eager session-less create is now its create arm instead of a second createContentSurface literal (the two ab-screencast creators can't drift) - findSurfaceByParams generalizes the panes-then-doors scan; the session lookup and the booting-pane lookup are two calls of one function - onConnectPort is void: nothing read the promised result since the menu stopped displaying errors; five stub sites shrink to no-ops - the ensure types are module-private again (their last external importer left in the previous commit) and the title-candidates handler is inlined where TypeScript narrows contextMenu - the session-less-is-inert guarantee the instant connect hangs on is now stated at its enforcement point (maybeRecoverStalePort), pinned by a controller test (no CLI spawn, no socket until params deliver the session), and cross-referenced in the Agent-Browser Connection spec Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two pieces of feedback on the pane-header connect menu: - A port click now selects the browser surface it connected (reattaching it first when minimized, like clicking its Door chip). The create path had inherited `dor ab`'s `focusNeutral: true`, so `settleAddSelection` never selected the new pane, and the reuse arm never touched selection at all — making a repeat click on an already-connected port look like a no-op (the session just re-navigates to the URL it is already on, so the selection move is the only feedback there is). Selection only: passthrough stays on the terminal that was right-clicked. - The eager session-less pane rendered `Waiting for browser session — run dor ab open <url>`: an empty session name and instructions to redo the click that created it. It now shows `Connecting to browser session…`, which is what the spec already promised. `revealSurface` lives in Wall.tsx (the selection authority) and is threaded into useDorControl; the `dor ab` control path stays focus-neutral. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The pane-header connect created its browser pane in 1ms but nothing painted for ~2.9s, so the pane looked like it appeared only after the daemon booted. The eager create was never at fault: `agent_browser_command` was a plain `#[tauri::command]`, which Tauri runs on the main thread, and its body blocks on `recv_timeout` until the sidecar answers. The webview cannot paint during that, so every frame between the click and the daemon coming up was dropped. Declares `(async)` on the twelve commands that reach the blocking sidecar helpers, moving the same body onto a runtime worker. Fixes the connect freeze and a class of smaller ones with it — including `pty_get_open_ports`, whose 3s budget could freeze the very "scanning ports…" spinner that covers it. The three clipboard readers stay sync on purpose: their Windows branches call the Win32 clipboard rather than the sidecar, and that threading is not something this change can test. The invariant is stated at the helpers themselves, where a new command author will see it, and in the standalone spec's bridge section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts: # lib/src/components/wall/SurfacePaneHeader.test.tsx # lib/src/components/wall/TerminalPaneHeader.tsx
Deploying mouseterm with
|
| Latest commit: |
0e750de
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://11c418b3.mouseterm.pages.dev |
| Branch Preview URL: | https://right-click-to-connect.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Nice feature — the eager session-less pane + one-shot params reconcile is a clean way to hide the daemon-boot latency, and the port-URL selection reuse plus the shared useDismissOverlay / POPUP_SURFACE_CLASS extractions are good de-duplication. One blocker, all one root cause.
dor-lib-common/agent-browser isn't resolvable in the bundling jobs (3 red checks)
connect-port.ts adds import { sessionForKey } from 'dor-lib-common/agent-browser', and connect-port is now reachable from the webview Wall graph (Wall → useDorControl → connect-port). That makes dor-lib-common the first package pulled into the webview bundle/typecheck through its package exports, which resolve to a ./dist/ that these jobs never build:
- Standalone Smoketest —
connect-port.ts(19,31): error TS2307: Cannot find module 'dor-lib-common/agent-browser'. The job runspnpm install+tsc --noEmitwith nodor-lib-commonbuild, and there's nopathsmapping for it. - Visual Regression —
Rolldown failed to resolve import "dor-lib-common/agent-browser"while building Storybook. - Cloudflare Pages — same website build, same unresolved import.
Build & Test is green only because pnpm -r run test builds dor-lib-common's dist (its test script runs build first, in topo order) before the lib typechecks — the other three jobs skip that step.
This is exactly the server-lib-common situation, which is already handled by aliasing the bare specifier to source in every bundler config and mapping it in tsconfig paths (its exports also "resolve to a dist this build never compiles"). dor-lib-common just needs the same treatment. A single directory alias covers both the . and /agent-browser subpaths:
website/vite.config.ts— add alongsideserver-lib-common:
"dor-lib-common": path.resolve(__dirname, "../dor-lib-common/src")lib/.storybook/main.ts(viteFinalalias block) — add:
'dor-lib-common': path.resolve(here, '..', '..', 'dor-lib-common', 'src')standalone/tsconfig.json(paths) — add:
"dor-lib-common": ["../dor-lib-common/src/index.ts"]and
"dor-lib-common/agent-browser": ["../dor-lib-common/src/agent-browser.ts"]
For consistency (and so pnpm --filter dormouse-lib test / the Pocket bundle don't rely on a pre-built dist), the same alias is worth adding to lib/vite.pocket.config.ts and lib/vite.config.ts too — both already alias dor / server-lib-common for the same reason.
Want me to push this as a fix commit? It's mechanical and follows the existing server-lib-common alias pattern exactly.
`connect-port.ts` imports `dor-lib-common/agent-browser`, and `connect-port` is reachable from the webview `Wall` graph — making `dor-lib-common` the first package pulled into the webview bundle/typecheck through its package `exports`, which resolve to a `dist/` that three CI jobs never build: - Standalone Smoketest — `npx tsc --noEmit` with no `dor-lib-common` build and no `paths` mapping: TS2307 on the import. - Visual Regression — Storybook build can't resolve the specifier. - Cloudflare Pages — the website build, same unresolved import. Build & Test stayed green only because `pnpm -r run test` builds the `dist` in topo order before lib typechecks; the other three jobs skip that step. This is the `server-lib-common` situation exactly, so it takes the same treatment: alias the bare specifier to source in every bundler config, map it in the standalone tsconfig `paths`. One directory alias covers both the `.` and `/agent-browser` subpaths (tsconfig needs the two entries spelled out). Added to lib's own vite config and the Pocket build too, so neither depends on a pre-built `dist`. Verified with `dor-lib-common/dist` deleted — reproducing the CI condition: the standalone typecheck reproduces TS2307 without the paths entries and passes with them, and the website, Storybook, and Pocket builds all succeed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`>` in command mode opens the selected terminal pane's header context menu, which now embeds the title-candidates table (header row + candidate rows on top, port rows below) instead of offering it as a separate popover. Digit accelerators 1-9 connect ports; arrows rove, Tab cycles, Escape closes; the menu registers as dialog-keyboard-active and restores focus on close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The menu now leads with a header row (display title, surface:N, close) and the diagnostic title-candidates table, with the port rows below; the separate TitleCandidatesPopover and the menu's "title candidates" item are gone. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`>` in command mode opens the selected terminal pane's menu (synthetic contextmenu at the header's bottom-left; browser surfaces and doors no-op). While open the menu owns the keyboard: it takes DOM focus (restored on close), reports dialog-keyboard-active, roves ArrowUp/Down across port rows, connects with digit accelerators 1-9 (shown as chips, dropped while the scan runs), and traps Tab/Escape via the focus trap extracted from TodoAlertDialog. Scrolls inside the menu no longer dismiss it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-angle review cleanups: extract stepFocus so the wrap arithmetic exists once (popover trap, modal trap, menu arrows); make the menu's own keydown handler cover Tab and drop usePopoverFocusTrap there, leaving useDismissOverlay the single dismissal owner; make the overlay root a required useDismissOverlay argument so self-originated scrolls never dismiss any overlay; rename the stale data-title-candidates-for locator to data-pane-title-for. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Right-clicking a terminal pane header opens one context menu showing the pane's
stable
surface:Nhandle and the TCP ports its process tree binds. Clicking aport row reproduces
dor ab open <url>against the default agent-browsersession — the browser pane appears in the same tick and connects in the
background.
What's here
only the alert bell does. The diagnostic title-candidates popover became a
title candidatesitem at the bottom of the menu, still anchored to the titlespan.
is created synchronously, before
agent-browser openruns (a cold daemon bootis 1–3s). The eager pane is created without a
sessionon purpose — asession-less
ab-screencastpane is inert, so the controller's stale-portrecovery cannot race the daemon boot. Once
opensucceeds the pane receives{session, wsPort, binaryPath}as one params refresh, and settingsessioniswhat connects it.
dor ab(focus-neutral, because anagent must not steal focus), a menu click is a human asking to see that
browser: every eager-lookup arm ends by selecting the surface, reattaching it
first if minimized. Selection only — passthrough stays on the terminal that was
right-clicked.
surface.resolveOpen, so the menu andthe CLI can't drift: one openable URL per distinct port, loopback wins
localhost, IPv6 bracketed.demo).
Two fixes found while dogfooding
A 3s freeze on connect that wasn't in the connect path. Timing showed the
surface created at 1.0ms but nothing painting until the command settled at
2888ms.
agent_browser_commandwas a plain#[tauri::command], which Tauri runson the main thread, and its body blocks on
recv_timeoutuntil the sidecaranswers — so the webview couldn't paint for the whole daemon boot. The twelve
commands that reach the blocking sidecar helpers are now
(async), which movesthe same body onto a runtime worker. This also fixes a class of smaller freezes,
including
pty_get_open_ports, whose 3s budget could freeze the very"scanning ports…" spinner that covers it. The invariant is stated at the helpers
themselves and in the standalone spec.
The eager pane's placeholder read
Waiting for browser session — run dor ab open <url>: an empty session name plus instructions to redo the click thatcreated it. Session-less panes now show
Connecting to browser session….Testing
pnpm testgreen (spec-lint + all packages, 1102 lib tests),cargo checkclean. New unit tests cover the port→URL grouping, the connect sequencing
(including the failure arms), and the menu's states — scanning, loaded, empty,
scan-failed, inert-label, dismissal, and both title-candidates paths.
Verified live in standalone: click→pane is immediate, the app stays responsive
during the daemon boot, repeat clicks reuse and select the existing browser.
Known gap:
revealSurfacehas no test — there is no hook-level test file foruse-dor-control, andconnect-port.test.tsstubsensureEagerSurfacewholesale, so the three eager arms aren't exercised. Building that harness is a
follow-up.
🤖 Generated with Claude Code