Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
adb6ee4
Extract port→URL and browser-surface plumbing for the header connect …
nedtwigg Jul 10, 2026
e0230c2
/simplify: bind connectPort inside useDorControl, thread the lazy ref…
nedtwigg Jul 10, 2026
9489c26
Pane-header right-click menu: surface handle + bound ports, click to …
nedtwigg Jul 10, 2026
4538e4e
/simplify: shared popover dismissal + chrome, context-fed menu, test-…
nedtwigg Jul 10, 2026
4fabe94
One header menu + instant connect: session-less eager pane, backgroun…
nedtwigg Jul 11, 2026
4feaa9b
/simplify: consolidate the eager create into ensure, pin the inertnes…
nedtwigg Jul 11, 2026
0a4b348
Connect reveals its surface; name the session-less pane's state
nedtwigg Jul 22, 2026
694b376
Run blocking sidecar commands off the main thread
nedtwigg Jul 22, 2026
99652fb
Merge remote-tracking branch 'origin/main' into right-click-to-connect
nedtwigg Jul 22, 2026
0a9278e
Alias dor-lib-common to source in the bundling configs
nedtwigg Jul 22, 2026
b37f6e5
Spec the header context menu's keyboard path; fold title candidates in
nedtwigg Jul 23, 2026
a7c92b6
Fold the title-candidates table into the header context menu
nedtwigg Jul 23, 2026
3f55319
Keyboard path into the header context menu
nedtwigg Jul 23, 2026
0e750de
Simplify the menu's keyboard/dismissal plumbing
nedtwigg Jul 23, 2026
63fe827
Merge remote-tracking branch 'origin/main' into right-click-to-connect
nedtwigg Jul 27, 2026
b65810a
Fix lib build ordering and browser focus behavior
nedtwigg Jul 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/specs/dor-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,68 @@ polls only while a wanted port is still unmatched. Reload revalidates
optimistically.

Source of truth: `lib/src/components/wall/use-dev-server-ports.ts`,
`lib/src/components/wall/port-url.ts` (the `servesLoopback` predicate),
`lib/src/components/wall/agent-browser-ports.ts`,
`lib/src/components/wall/browser-url.ts`.

## Pane Context Menu Connect

The terminal pane header's context menu (`docs/specs/layout.md` → Pane
header — right-click, or `>` in command mode; that spec owns the menu's
layout and keyboard contract) lists the ports a pane's process tree binds
using the **same** per-port URL selection as `surface.resolveOpen`
(`dor ab open <surface>` / `dor iframe <surface>`): `listenerUrlsByPort` in
`port-url.ts` groups TCP listeners into one openable `http://<host>:<port>/`
per distinct port (loopback-reachable bind wins `localhost`; otherwise the
bound LAN/Tailnet address, IPv6 bracketed).

Activating a port row — click, its `1`–`9` digit accelerator, or `Enter` on
the focused row — reproduces `dor ab open <url>` against the **default**
key/session: it runs `agent-browser open <url>` on that session and
reuses-or-creates the session's browser surface — the wall-side mirror of the CLI
flow, not the control plane. It is host-gated on `agentBrowserCommand`: absent
(e.g. the web demo host), the rows render as inert labels.

**Activation reveals its surface.** Unlike `dor ab` (focus-neutral: an agent must
not steal focus from the human), activating a menu row — by mouse or keyboard —
*is* the human asking to see and control that browser, so every arm of the eager
lookup below ends by selecting the surface **in passthrough mode** — reattaching
it first when it is minimized, on the same terms as clicking its Door chip. This
applies even when the menu was opened from command mode with `>`: activation
hands keyboard input to the browser. This is why a repeat activation on an
already-connected port is visible at all — the session merely re-navigates to
the URL it is already on, so the focus transfer is the only feedback.
Source of truth: `revealSurface` in `Wall.tsx`, threaded into `useDorControl`.

**Instant create.** The click is fire-and-forget: the menu closes at once and the
pane appears **before** `agent-browser open` runs (a cold daemon boot is 1–3s).
The eager surface is created **without a `session`** — deliberately: a
session-less `ab-screencast` pane is inert (the controller's `maybeRecoverStalePort`
returns early with no session, so it spawns no CLI and cannot race the daemon
boot), while the panel still shows `Connecting to browser session…` — the
session-less placeholder branch exists for exactly this pane, and deliberately
does *not* fall through to the idle `run dor ab open <url>` line, which would ask
the user to redo the click they just made. It carries
`key: 'default'` and the target `url` so the browser chrome shows the destination
immediately. Once `open` succeeds, a best-effort `stream status` runs, then the
pane receives `{session, wsPort, binaryPath}` as **one** params refresh — setting
`session` reconciles the controller and connects it (safe now: the daemon is up).
If `open` fails, the pane is still handed the `session` (so its placeholder names
it) and the failure is logged, not shown in the (already closed) menu.

The eager lookup reuses before it creates: (a) a surface already bound to the
default session, else (b) a still-booting session-less `key: 'default'` pane from a
rapid earlier click (so a double-click doesn't spawn two panes), else (c) a fresh
session-less pane. Accepted edge: a pane persisted mid-boot restores session-less
and stays a `Connecting…` placeholder — kill it, or connect again (the eager
lookup reuses it via arm (b)).

Source of truth: `lib/src/components/wall/connect-port.ts`
(`connectPortToDefaultBrowser`), the `connectPort` binding in
`use-dor-control.ts` (its `ensureEagerSurface` + `updateSurfaceParams` seams,
shared with `ensureAgentBrowserSurface`),
`lib/src/components/wall/PaneHeaderContextMenu.tsx`.

## Display Modal And Render Swaps

The Display modal is the sole GUI for changing render mode and screencast
Expand Down Expand Up @@ -257,6 +316,11 @@ the same zero-resource end state with less thrash. The agent-browser
daemon/session stays alive throughout and reattaches from persisted params. The
controller's client resources are released only at pane kill or a render swap
away from the renderer (`disposeAgentBrowserSurfaceController` in `Wall.tsx`).
A controller whose params carry no `session` is deliberately inert — no
connection, no `stream status` query: the instant connect flow
([Pane Context Menu Connect](#pane-context-menu-connect)) depends on that
inertness to keep the eager pane from racing the daemon boot, so the session
must never be derived from `key` here.

Hidden-but-mounted panes park too. A Lath leaf is always mounted (no active-tab
gating), so a backgrounded window would keep its ~20Hz stream plus per-pulse
Expand Down Expand Up @@ -540,6 +604,8 @@ Source of truth: `lib/src/lib/platform/types.ts`,
- Shell/render swap/lifecycle: `lib/src/components/Wall.tsx`,
`lib/src/components/wall/BrowserPanel.tsx`,
`lib/src/components/wall/browser-surface.ts`.
- Pane context-menu connect: `lib/src/components/wall/PaneHeaderContextMenu.tsx`,
`lib/src/components/wall/connect-port.ts`, `lib/src/components/wall/port-url.ts`.
- Chrome/modal: `SurfacePaneHeader.tsx`, `AgentBrowserScreenModal.tsx`,
`agent-browser-screen.ts`, `browser-url.ts`.
- Agent-browser renderer: `AgentBrowserPanel.tsx`,
Expand Down
7 changes: 5 additions & 2 deletions docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,9 @@ fds, closing the pipe — which is why none of this surfaced on macOS.)
Resolution: `dor-lib-common`'s package `exports` point at its built `dist` (clean,
Node-type-free `.d.ts` for `dor`'s `tsc`, which deliberately avoids `@types/node`);
every esbuild/Vite consumer (`dist/dor.js`, the sidecar `.cjs`, vscode-ext) inlines
it. `dor`'s `prebuild` builds `dor-lib-common` first so its `.d.ts` exists.
it. The `dor` and `dormouse-lib` prebuilds build `dor-lib-common` first so its
`.d.ts` files exist before either package typechecks imports through those
exports.

## Host Plumbing

Expand Down Expand Up @@ -433,7 +435,8 @@ Source of truth: `dor/src/commands/open-target.ts` (classification + `:port`
sugar + `surface.resolveOpen` call), `dor/src/commands/iframe.ts` /
`dor/src/commands/agent-browser.ts` (the two entry points),
`dor/src/protocol.ts` (`resolveOpen`), the `surface.resolveOpen` handler in
`lib/src/components/wall/use-dor-control.ts`.
`lib/src/components/wall/use-dor-control.ts`, and the port→URL grouping/selection
in `lib/src/components/wall/port-url.ts` (`listenerUrlsByPort`).

## Agent Workflows

Expand Down
19 changes: 15 additions & 4 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,23 @@ The content area is a tiling layout of panes rendered by Lath (`docs/specs/tilin

Each pane has a 30px header that doubles as a drag handle (a `pointerdown` on the header, past a 5px threshold, begins a Lath pane drag; below the threshold the header's own click behavior stands). The header uses `cursor-grab` / `active:cursor-grabbing`, `select-none`, and the shared terminal top radius from `lib/src/components/design.tsx`. Background and foreground use the `--color-header-active-*` / `--color-header-inactive-*` token pairs, which map to VSCode file-tree list colors.

The header label is the `DerivedHeader` returned by `deriveHeader(paneState, visiblePanes)` in `docs/specs/terminal-state.md` — that spec is the single source of truth for the priority chain (user-pinned title, app-sent overrides, current command title, `<idle> ${LAST_TITLE}` for finished panes, plain `<idle>` for fresh panes), the disambiguator rule, and which OSC sources contribute. Layout's job is to render the result: the primary label truncates with ellipsis, the secondary label (when present) is shown muted next to it, click renames/pins, right-click opens the diagnostic popup.
The header label is the `DerivedHeader` returned by `deriveHeader(paneState, visiblePanes)` in `docs/specs/terminal-state.md` — that spec is the single source of truth for the priority chain (user-pinned title, app-sent overrides, current command title, `<idle> ${LAST_TITLE}` for finished panes, plain `<idle>` for fresh panes), the disambiguator rule, and which OSC sources contribute. Layout's job is to render the result: the primary label truncates with ellipsis, the secondary label (when present) is shown muted next to it, click renames/pins, right-click — or `>` in command mode — opens the header context menu, which leads with the title-candidates table.

The diagnostic popup lists the latest entry per `titleCandidates` channel as defined in `docs/specs/terminal-state.md`. Each row shows the channel, latest candidate text, and timestamp. The popup is diagnostic only; it does not change the title priority rules.
Right-clicking anywhere on the header opens the pane's single **context menu** at the pointer; pressing `>` in command mode opens the same menu for the selected pane, anchored under the header's left edge (the handler finds the header via `data-pane-header-for` and dispatches a synthetic `contextmenu` at that corner, so both paths share one code path; browser surfaces and Doors have no such header, so `>` no-ops there). Only the alert bell owns its own right-click (`stopPropagation`, opening the alert dialog); every other region — including the title span — bubbles to this one menu. It is portaled to `document.body`, viewport-clamped, and dismissed by outside `pointerdown`, `Escape`, `resize`, or capture-phase `scroll` — except scrolls originating inside the menu itself, which must not dismiss it (arrow-key focus moves auto-scroll the overflowing list).

Content, top to bottom:

- **Header row** — the current display title, the pane's `surface:N` handle (`resolveSurfaceRef`, muted), and a close button.
- **Title-candidates table** — the latest entry per `titleCandidates` channel as defined in `docs/specs/terminal-state.md`: channel, latest candidate text, and timestamp per row, or a muted `No title candidates` line. Diagnostic only; it does not change the title priority rules.
- **Port rows** — the TCP ports the pane's process tree binds: a spinner while `getOpenPorts` runs (once per open — reopen to rescan), then one `host:port` row per distinct port (digit accelerator chip first, process name muted beside it), or a muted `no listening ports` / `port scan failed` line.

The menu owns the keyboard while open: it takes DOM focus on mount and restores the previously focused element when a dismissal leaves input ownership unchanged, and it registers as dialog-keyboard-active so command-mode keys don't fire underneath (`use-wall-keyboard.ts`). A port activation instead focuses its destination browser in passthrough. `1`–`9` activate the corresponding port row; presses while the scan is still running are dropped, not buffered — the spinner explains why nothing happened. `↑`/`↓` rove focus across the port rows (wrapping), `Enter`/`Space` activate the focused row, `Tab`/`Shift+Tab` cycle every focusable element, and `Escape` closes.

Activating a port row (click, digit, or `Enter` on the focused row) reproduces `dor ab open <url>` for that port and closes the menu at once (`docs/specs/dor-browser.md` → Pane Context Menu Connect): the browser surface appears immediately and becomes the selection in passthrough (reattaching first if it was minimized) — the one command-mode gesture whose side effect moves selection off the pane it targeted and exits command mode — and loading/errors surface in the pane, not the menu. On hosts that cannot open a browser surface the rows are inert labels with no digit chips, and digits do nothing. Only terminal panes have this menu. Source of truth: `PaneHeaderContextMenu.tsx`, `TerminalPaneHeader.tsx`, `handle-pane-shortcuts.ts`.

Elements from left to right:

- Derived session label (click to rename/pin, right-click to inspect title candidates, truncates with ellipsis)
- Derived session label (click to rename/pin, right-click opens the header context menu with the title-candidates table, truncates with ellipsis)
- Alert bell button (reflects session activity status)
- TODO pill (if todo state is set; hidden in minimal tier)
- Flexible gap
Expand Down Expand Up @@ -382,7 +392,8 @@ The refill adopts the replacement (`selectPane`) only when the current selection
| `lib/src/components/wall/wall-types.ts` / `wall-context.tsx` | Shared Wall types and React contexts used by Wall, pane headers, panels, overlays, and the baseboard |
| `lib/src/components/wall/LathHost.tsx` | The tiling engine's HTML adapter: leaf divs, sashes, the pane/door drag gesture, and imperative animator frame application. Engine internals are mapped in `docs/specs/tiling-engine.md`. |
| `lib/src/components/wall/TerminalPanel.tsx` | Pane body wrapper; registers the pane's DOM element (`usePaneChrome`) |
| `lib/src/components/wall/TerminalPaneHeader.tsx` | Pane header with rename, alert/TODO, mouse override, split/zoom/minimize/kill controls |
| `lib/src/components/wall/TerminalPaneHeader.tsx` | Pane header with rename, alert/TODO, mouse override, split/zoom/minimize/kill controls, and the right-click context menu |
| `lib/src/components/wall/PaneHeaderContextMenu.tsx` | Pane-header right-click menu: the `surface:N` handle plus the pane's bound TCP ports; a port click connects it to the default browser (`docs/specs/dor-browser.md`) |
| `lib/src/components/wall/WorkspaceSelectionOverlay.tsx` | Pane/door focus ring and marching-ants overlay; re-measures on Lath store commits + animator frames |
| `lib/src/components/wall/MarchingAntsRect.tsx` | SVG marching-ants border path and dash sizing |
| `lib/src/components/wall/MouseOverrideBanner.tsx` | Temporary mouse override banner shown from the header icon |
Expand Down
3 changes: 3 additions & 0 deletions docs/specs/shortcuts.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ In the VS Code extension host, selected workbench chords are mirrored: the termi
| `,` | Rename | Enter rename mode for the selected pane's title. |
| `a` | Toggle alert | Dismiss or toggle the bell alert for the selected pane. Meaningful only for a terminal Surface — a browser surface has no bell to ring (`docs/specs/glossary.md`). |
| `t` | Toggle todo | Toggle the TODO marker on or off for the selected pane's Surface. Works on any Surface — a terminal Session or a browser surface. |
| `>` | Header context menu | Open the selected pane's header context menu — current title + `surface:N`, title candidates, and bound ports with digit-to-connect (mirrors tmux's pane `display-menu` binding). Terminal panes only; no-op on browser surfaces and doors. |

## Navigation (command mode)

Expand Down Expand Up @@ -65,6 +66,8 @@ On macOS, `Ctrl+C` passes through to the running program (only `⌘C` copies); `
| Prompted character | Confirm kill | Type the character shown in the kill prompt to confirm termination. |
| `a` (alert dialog open) | Toggle alert | Same as command-mode `a`. |
| `t` (alert dialog open) | Toggle todo | Same as command-mode `t`. |
| `1`–`9` (header context menu open) | Connect port | Open the nth port row in a browser surface, select it, and enter passthrough. Dropped while the port scan is running; inert on hosts that can't open a browser surface. |
| `↑` / `↓` (header context menu open) | Move row focus | Rove focus across port rows, wrapping; `Enter` or `Space` activates the focused row. |

## Implementation references

Expand Down
10 changes: 10 additions & 0 deletions docs/specs/standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,16 @@ and reads the bytes in Rust so images never ride the JSON-lines pipe shared
with PTY traffic (`docs/specs/dor-browser.md`). Request/response commands block on the
sidecar's reply with a timeout; `OPEN_PORT_TIMEOUT_MS` in `lib.rs` mirrors the
constant in `lib/src/lib/platform/types.ts` and the two must stay in sync.

**Blocking commands must be `#[tauri::command(async)]`.** `request_from_sidecar`
and `request_from_sidecar_timeout` block the calling thread on a `recv_timeout`,
and Tauri runs a *plain* sync command on the main thread — where that block stops
the webview from painting for the whole round trip (up to `AGENT_BROWSER_TIMEOUT`
= 30s for a hung agent-browser; a cold `agent-browser open` froze the UI ~3s,
long enough that a pane created instantly before it looked like it never
appeared). `(async)` runs the same blocking body on a runtime worker instead. The
three clipboard readers are knowingly still sync: their Windows branches call the
Win32 clipboard directly rather than the sidecar.
`pty_graceful_kill_all` (`TauriAdapter.gracefulKillAllPtys`) SIGTERMs every live
PTY and awaits the sidecar's `gracefulKillDone` (echoing the request's
`requestId`; bounded at `timeout + 1.5s`). `gracefulKillDone` fires early once
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/terminal-escapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Unknown non-iTerm2 OSC families pass through to xterm.js unchanged so xterm.js c

(`BEL` is not itself an OSC; it has a row here because a standalone `BEL` is parsed and stripped at the same PTY data boundary as the OSCs.)

Some sequences are dual-purpose. The notification rows for `OSC 9 ; <message> ST`, `OSC 99` (`p=title`/`p=body`), and `OSC 777 ; notify` also feed the title-candidate channel in `terminal-state.md` — see its [Title candidate diagnostics](terminal-state.md#supported-osc-inputs) table. Only the OSC 9 *message* form can become a header/door label; OSC 99 and OSC 777 candidates are stored for the diagnostic popup only. The OSC 9 *progress* form (`OSC 9 ; 4`) carries no text and never contributes a title candidate.
Some sequences are dual-purpose. The notification rows for `OSC 9 ; <message> ST`, `OSC 99` (`p=title`/`p=body`), and `OSC 777 ; notify` also feed the title-candidate channel in `terminal-state.md` — see its [Title candidate diagnostics](terminal-state.md#supported-osc-inputs) table. Only the OSC 9 *message* form can become a header/door label; OSC 99 and OSC 777 candidates are stored only for the diagnostic title-candidates table in the header context menu. The OSC 9 *progress* form (`OSC 9 ; 4`) carries no text and never contributes a title candidate.

#### OSC color queries on Windows require the bundled ConPTY

Expand Down
2 changes: 1 addition & 1 deletion docs/specs/terminal-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ Header priority — first match wins:
When the finished command exited non-zero, a trailing fail glyph (`✗`) is appended — `<idle> ${LAST_TITLE} ✗` — and `lastCommandFailed` is set on the result. "Failed" requires a real non-zero `exitCode`: the keystroke fallback never records one, so it shows no glyph either way. The glyph rides in `primary` so plain-text title consumers (OS/tab titles) carry it, while the pane header reads `lastCommandFailed` to color it red without re-parsing the string.
4. Otherwise (no running command and no last command): `<idle>`.

Rich notification titles from `OSC 99` and `OSC 777` are stored in `titleCandidates` for the diagnostic popup but never become header/door labels. Older shell titles (terminal titles emitted before the current command started, or after the last command finished) remain fallback-only and do not replace `<idle>` or pollute `LAST_TITLE`.
Rich notification titles from `OSC 99` and `OSC 777` are stored in `titleCandidates` for the diagnostic title-candidates table (in the header context menu) but never become header/door labels. Older shell titles (terminal titles emitted before the current command started, or after the last command finished) remain fallback-only and do not replace `<idle>` or pollute `LAST_TITLE`.

`<idle> ${LAST_TITLE}` keeps the just-finished context visible so the user can see at a glance which program just exited. The header surfaces failure minimally — the trailing `✗` glyph for a non-zero exit, nothing more; output and TODO notification are still surfaced via the alert/TODO machinery (`docs/specs/alert.md`). `<idle> ${LAST_TITLE}` persists across subsequent prompt/editing transitions until a new `commandStart` replaces it; only a fresh pane (no `lastCommand` at all) shows plain `<idle>`.

Expand Down
4 changes: 4 additions & 0 deletions dor-lib-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./agent-browser": {
"types": "./dist/agent-browser.d.ts",
"default": "./dist/agent-browser.js"
}
},
"scripts": {
Expand Down
Loading
Loading