Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 that browser, so every arm of the eager lookup below
ends by selecting the surface — reattaching it first when it is minimized, on the
same terms as clicking its Door chip. Selection only: passthrough stays on the
terminal the menu was opened from, so connecting a port never redirects the
keyboard. 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
selection move 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
3 changes: 2 additions & 1 deletion docs/specs/dor-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,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 on close (so a passthrough terminal gets its keys back), and it registers as dialog-keyboard-active so command-mode keys don't fire underneath (`use-wall-keyboard.ts`). `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 (reattaching first if it was minimized) — the one command-mode gesture whose side effect moves selection off the pane it targeted — 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 a browser surface on the nth port row. 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
5 changes: 5 additions & 0 deletions lib/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const config: StorybookConfig = {
// at a `dist` the Storybook/Chromatic job never builds, so alias the bare
// specifier to source too.
'server-lib-common': path.resolve(here, '..', '..', 'server-lib-common', 'src'),
// And `Wall` → `useDorControl` → `connect-port` imports
// `dor-lib-common/agent-browser`, whose `exports` point at the same kind of
// unbuilt `dist`. The directory alias covers the subpath and the bare
// specifier both.
'dor-lib-common': path.resolve(here, '..', '..', 'dor-lib-common', 'src'),
};
return config;
},
Expand Down
Loading
Loading