Skip to content

Pane-header right-click menu: bound ports, click to connect#289

Open
nedtwigg wants to merge 14 commits into
mainfrom
right-click-to-connect
Open

Pane-header right-click menu: bound ports, click to connect#289
nedtwigg wants to merge 14 commits into
mainfrom
right-click-to-connect

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Right-clicking a terminal pane header opens one context menu showing the pane's
stable surface:N handle and the TCP ports its process tree binds. Clicking a
port row reproduces dor ab open <url> against the default agent-browser
session — the browser pane appears in the same tick and connects in the
background.

What's here

  • One menu per header. The title span no longer owns its own right-click;
    only the alert bell does. The diagnostic title-candidates popover became a
    title candidates item at the bottom of the menu, still anchored to the title
    span.
  • Instant connect. The click is fire-and-forget: the menu closes and the pane
    is created synchronously, before agent-browser open runs (a cold daemon boot
    is 1–3s). The eager pane is created without a session on purpose — a
    session-less ab-screencast pane is inert, so the controller's stale-port
    recovery cannot race the daemon boot. Once open succeeds the pane receives
    {session, wsPort, binaryPath} as one params refresh, and setting session is
    what connects it.
  • The click reveals its surface. Unlike dor ab (focus-neutral, because an
    agent 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.
  • Port→URL selection is shared with surface.resolveOpen, so the menu and
    the CLI can't drift: one openable URL per distinct port, loopback wins
    localhost, IPv6 bracketed.
  • Rows render as inert labels on hosts that can't run agent-browser (e.g. the web
    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_command was a plain #[tauri::command], which Tauri runs
on the main thread, and its body blocks on recv_timeout until the sidecar
answers — so the webview couldn't paint for the whole daemon boot. The twelve
commands that reach the blocking sidecar helpers are now (async), which moves
the 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 that
created it. Session-less panes now show Connecting to browser session….

Testing

pnpm test green (spec-lint + all packages, 1102 lib tests), cargo check
clean. 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: revealSurface has no test — there is no hook-level test file for
use-dor-control, and connect-port.test.ts stubs ensureEagerSurface
wholesale, so the three eager arms aren't exercised. Building that harness is a
follow-up.

🤖 Generated with Claude Code

nedtwigg and others added 9 commits July 10, 2026 14:17
…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
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 22, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 0e750de
Status: ✅  Deploy successful!
Preview URL: https://11c418b3.mouseterm.pages.dev
Branch Preview URL: https://right-click-to-connect.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 (WalluseDorControlconnect-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 Smoketestconnect-port.ts(19,31): error TS2307: Cannot find module 'dor-lib-common/agent-browser'. The job runs pnpm install + tsc --noEmit with no dor-lib-common build, and there's no paths mapping for it.
  • Visual RegressionRolldown 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 alongside server-lib-common:
    "dor-lib-common": path.resolve(__dirname, "../dor-lib-common/src")
  • lib/.storybook/main.ts (viteFinal alias 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>
nedtwigg and others added 4 commits July 22, 2026 18:21
`>` 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants