diff --git a/AGENTS.md b/AGENTS.md index 89ba575bd..e33dbe1d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,7 @@ One implementation map per spec: an exhaustive `Files` / `Code Map` section or s - **`docs/specs/tiling-engine.md`** — **Lath**, the in-house headless tiling engine: pure split-tree core, never-re-parent LathHost adapter, wall store + engine, Lath-only persistence. - **`docs/specs/alert.md`** — The Activity layer: alert tracks, attention model, TODO lifecycle, notification protocols with their sanitization rules, the Workspace union projection. - **`docs/specs/terminal-state.md`** — Per-Session semantic state: CWD, prompt/command lifecycle, title candidates and header derivation, grouping keys. +- **`docs/specs/terminal-context.md`** — Unified terminal context and helper terminal: lifecycle, promotion, source closure, and global autorun settings. - **`docs/specs/terminal-escapes.md`** — Registry of every escape sequence parsed, answered, or ignored, each row pointing at its owning spec. Read before touching OSC/CSI parsing. - **`docs/specs/transport.md`** — Adapter-agnostic webview ↔ host protocol: PTY lifecycle and buffering, reconnection, message contracts, persisted-session types, the invariants every adapter honors. - **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned selection, copy (Raw / Rewrapped), paste tiers, smart URL/path extension, the mouse-ownership state matrix. @@ -124,3 +125,25 @@ in [`lib/src/theme-colors.css`](lib/src/theme-colors.css), which a host can import on its own, and the type scale, fonts, and animation tokens in [`lib/src/theme.css`](lib/src/theme.css). When adding or removing a color token, update `theme-colors.css` and `design.tsx` together. + + +## Running inside Dormouse + +If the `DORMOUSE_SURFACE_ID` environment variable is set, this terminal is +hosted by [Dormouse](https://dormouse.sh) and the `dor` CLI is on your `PATH`. +**Run `dor skill` first and do what it says** — it teaches you to use the +terminal fully (visible panes, browser surfaces, sub-agents). Two rules are +mandatory whether or not you have read it: + +- **NEVER start a long-running process — a dev server, a `--watch`, any daemon + — as a background subprocess.** It would be invisible to the user and die with + your shell. ALWAYS run it with `dor ensure -- ` (e.g. + `dor ensure -- npm run dev`), which puts it in a visible pane that outlives + you and is reused instead of duplicated on re-runs. +- **NEVER use a built-in, native, or bundled browser tool to open, view, or + drive a web page.** ALWAYS use `dor ab` (agent-browser) — `dor ab open + `, `dor ab click @e3`, and so on — so the page renders in a Dormouse + pane the user can watch. + +If `DORMOUSE_SURFACE_ID` is not set, ignore this section — `dor` is not here. + diff --git a/DESIGN.md b/DESIGN.md index a2b02c143..6a7ec2351 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -225,7 +225,7 @@ The Windows/Linux native-style window control row in the standalone app bar. The system uses **raised surfaces**, not "cards." There are no nested cards. There is no resting card grid. - **Raised surface** (`PopupButtonRow`, tooltips, popups): `bg-surface-raised`, `border border-border`, `rounded` (4px), `shadow-md`, `font-mono text-sm`. -- **Dialog** (`KillConfirm`, `TodoAlertDialog`): `bg-surface-raised`, `border border-border`, `rounded-lg` (8px), `shadow-lg`, generous padding (`px-6 py-4` for kill-confirm). +- **Dialog** (`KillConfirm`, the terminal context's detail dialogs): `bg-surface-raised`, `border border-border`, `rounded-lg` (8px), `shadow-lg`, generous padding (`px-6 py-4` for kill-confirm). - **Modal** (`ThemePicker` dropdown, `ThemeDebugger`, `ThemeStoreDialog`): `bg-surface-raised`, `border`, `rounded`, `shadow-2xl`, fixed-position with viewport-clamped sizing. **The Viewport-Bound Rule.** Anything floating over the viewport takes its height cap from `OVERLAY_MAX_HEIGHT` in `design.tsx` — `.modal` for a `ModalFrame` surface (the viewport minus `MODAL_OVERLAY_INSET` doubled), `.popover` for an anchored overlay (matching `clampOverlayPosition`'s margin). Don't hand-write a `vh`/`dvh` literal at the call site: the six that predated this token had drifted to five different budgets, and one silently shadowed its own overlay's padding. Each entry reads its own custom property first (`--overlay-max-h-modal` / `--overlay-max-h-popover`), so a story — or a host with less room than the window — can narrow one bound without touching the component. They are deliberately separate: a popover inside a modal is a DOM descendant of it, and custom properties inherit, so one shared knob would cap the dialog too. A deliberately *smaller* budget than the viewport (a context menu at `max-h-[70vh]`) is a different decision and stays at the call site. @@ -234,6 +234,7 @@ The system uses **raised surfaces**, not "cards." There are no nested cards. The - Used by `ThemePicker`. Style: `bg-input-bg`, `border border-input-border`, `rounded`, `font-mono`, `text-sm`. - **Focus:** native browser focus outline; this is acceptable because the entire input lives inside a raised surface that already has `shadow-2xl` and a border. - **Form fields inside a dialog** use the underlined pair in `design.tsx` instead, so a form mixing them reads as one: `NumericInput` for a number (filtered at the keystroke, sized in `ch`) and `TextInput` for a string (full width, `type` passed through — `type="password"` for a credential). The app has no checkbox anywhere: a boolean is an `OnOffSwitch`. +- **On/off switch:** a compact track with the thumb left when off and right when on, followed by only the current `On` / `Off` label. Off is neutral; on uses the host link accent. Its 60×24px button uses the subdued action tint and hover from `design.tsx`, with native keyboard and disabled-fieldset behavior. Nested settings text aligns through `UNDER_SWITCH_INDENT`. ### Navigation diff --git a/docs/specs/alert.md b/docs/specs/alert.md index b30ee56c1..812d005ff 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -347,21 +347,23 @@ Where it surfaces is host-specific: ### Pane Header -The header shows an alert bell, a fixed-text `TODO` pill when `todo === true`, a hover/focus notification preview when TODO has `notification`, and a dialog opened by right-click or by some left-click actions. Placement, sizing, and width tiers belong to `docs/specs/layout.md`. +The header shows an alert bell, a fixed-text `TODO` pill when `todo === true`, a hover/focus notification preview when TODO has `notification`, and the terminal context opened by right-click or by some left-click actions. Placement, sizing, and width tiers belong to `docs/specs/layout.md`. Bell rotation follows public status; motion follows latch edges. **When a track latches, ring each mounted bell for four 800ms cycles, then hold 45° until the ring clears** (test: `runs a finite ringing burst and then holds the bell at 45 degrees` in `lib/src/components/bell-icon-class.test.ts`; rationale). **A newly mounted ringing bell may replay once without advancing `ringSeq`** (test: `replays the finite burst when a ringing presentation remounts` in `lib/src/components/AlertBell.test.tsx`; rationale). **A newly latched track replays the burst; further reports on that track only enrich its summons.** `AlertState.ringSeq` counts per-Session latches and is compared by `alertStatesEqual` (tests: `counts a second track ringing behind an already-latched one` and `does not count a track that is already ringing` in `lib/src/lib/alert-manager.test.ts`, `replaces the icon when the ring counter advances` in `lib/src/components/AlertBell.test.tsx`; rationale). **Remote Clients have no counter:** `DirectoryEntry.ringing` is an edgeless boolean, so Pocket rings on mount and holds. **The bell names the command it would act on** ("Alert on all `claude`"), not an abstract toggle — that is the scope of what a click changes. Bell interactions — one transition table, in `dismissOrToggleAlert`: -- Left-click `ALERT_RINGING`: dismiss, create TODO if needed, open dialog. -- Left-click after `attentionDismissedRing`: consume the flag and open dialog. +- Left-click `ALERT_RINGING`: dismiss, create TODO if needed, open context. +- Left-click after `attentionDismissedRing`: consume the flag and open context. - Otherwise, with a command running: toggle that command's WATCHING rule on or off. Turning it off drops the rule for every Session running it. -- Exception: from `OSC_NOTIF_BUSY` or `COMMAND_EXIT_ARMED` with no rule set, open the dialog instead. Those alarms need no rule, so a click must not create one by surprise, and must not clear the progress or the arm. -- With no command running: change nothing and open the dialog, which explains that alerts are per command. -- Pressing `a` on the selected Pane in command mode uses the same action. Right-click always opens the dialog. +- Exception: from `OSC_NOTIF_BUSY` or `COMMAND_EXIT_ARMED` with no rule set, open the context instead. Those alarms need no rule, so a click must not create one by surprise, and must not clear the progress or the arm. +- With no command running: change nothing and open the context, which explains that alerts are per command. +- Pressing `a` on the selected Pane in command mode uses the same action. Right-click always opens the context. - Pressing `t` toggles TODO. -The dialog carries the TODO switch, the WATCHING rule switch for the running command, notification detail, and the same `WatchedCommandList` the Settings dialog renders — load-bearing, not decoration, for the reason under Settings dialog. +**Must keep context alert controls scoped to the source**, with TODO, running-command WATCHING, and notification detail. Settings owns the global watched-command list. **Must suppress helper alerting until promotion, including after exit**, covering bell/notification protocols, watched commands, TODO, speech, push, and attention projections; semantic command/readiness state remains active. Promotion starts ordinary alert behavior without replaying suppressed events. + +Source of truth: `TerminalContext` in `lib/src/components/wall/TerminalContext.tsx`; `setHelper` in `lib/src/lib/alert-manager.ts`, which every host calls at helper spawn, listing, and promotion. The TODO pill always displays `TODO`; remote notification text belongs in preview/detail surfaces, not inside the pill. Clicking the pill clears TODO, and on clear the pill briefly shows the success flourish before unmounting. diff --git a/docs/specs/dor-browser.md b/docs/specs/dor-browser.md index 568046f33..c6de99cfe 100644 --- a/docs/specs/dor-browser.md +++ b/docs/specs/dor-browser.md @@ -144,46 +144,15 @@ Source of truth: `lib/src/components/wall/use-dev-server-ports.ts`, ## Pane Context Menu Connect -The terminal pane header's context menu (`docs/specs/layout.md` → Header context -menu) lists the ports a pane's process tree binds, using the **same** per-port -URL selection as `surface.resolveOpen` (`docs/specs/dor-cli.md` → Browser Open -Target Resolution). Activating a row — click, its `1`–`9` digit accelerator, or -`Enter` — reproduces `dor ab open ` against the **default** key/session, -reusing or creating that session's browser surface: the wall-side mirror of the -CLI flow, not the control plane. Host-gated on `agentBrowserCommand`; without it -the rows are inert labels. - -**Activation reveals its surface.** Unlike focus-neutral `dor ab`, a menu row is -the human asking to see and control that browser, so every arm of the lookup -below **must end by selecting the surface in passthrough mode**, reattaching it -first when minimized, exactly as clicking its Door chip does — including from -command mode with `>` (rationale). - -**Instant create.** The click is fire-and-forget: the menu closes at once and the -pane appears **before** `agent-browser open` runs (rationale). - -- The eager surface is placed synchronously and **must carry no `session`** — a - session-less `ab-screencast` pane is inert, so it cannot race the daemon boot - (rationale). It carries `key: 'default'` and the target `url`, and shows a - `Connecting to browser session…` placeholder rather than the idle - `run dor ab open ` line (rationale). -- `agent-browser open ` runs, then a best-effort `stream status`. -- **Must hand over `{session, wsPort, binaryPath}` in one params refresh** - (rationale). Failed or rejected `open` still hands over session and binary; - a rejected stream-status lookup omits only the port. Failures log into the - console after the menu closes. Pinned by `connect-port.test.ts`. - -The lookup reuses before it creates: (a) a surface bound to the default session, -else (b) a still-booting session-less `key: 'default'` pane, 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 (arm (b) reuses it). - -Source of truth: `lib/src/components/wall/connect-port.ts` -(`connectPortToDefaultBrowser`, `ensureEagerSurface`), `lib/src/components/wall/use-dor-control.ts` -(`useDorControl`'s `connectPort` and `updateSurfaceParams`, shared with -`ensureAgentBrowserSurface`), `lib/src/components/Wall.tsx` (`revealSurface`), `lib/src/components/wall/port-url.ts` -(`listenerUrlsByPort`), `lib/src/components/wall/PaneHeaderContextMenu.tsx`. +**Must scan once per context opening**, using the shared per-port URL selection in `docs/specs/dor-cli.md` → Browser Open Target Resolution. Zero/one port uses an inline row; multiple ports use a selector. Failed scans are distinct from no listeners. + +**Must offer System browser, Iframe, Agent browser, and Popout for the selected port**, disabling unavailable host capabilities with a reason. Opening a browser from context always preserves the source terminal, including an untouched one. + +**Must reuse targets per source and port**: iframe has a separate Surface; agent screencast and popout share a Session and switch display modes. Reattach minimized targets and recreate closed ones. System browser follows the OS opener's behavior. + +**Must create agent-browser Surfaces eagerly without a session**, binding the returned session only after the host launch succeeds; failures are reported in context. A launch completing after its eager Surface has closed releases its browser session. Concurrent requests for the same target are serialized. + +Source of truth: `openContextPort` in `lib/src/components/Wall.tsx`; `listenerUrlsByPort` in `lib/src/components/wall/port-url.ts`; `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`. ## Display Modal And Render Swaps diff --git a/docs/specs/dor-cli.md b/docs/specs/dor-cli.md index e693fcaf7..42b2cd8e9 100644 --- a/docs/specs/dor-cli.md +++ b/docs/specs/dor-cli.md @@ -534,6 +534,12 @@ Source of truth: `dor/src/commands/skill.ts`, `scripts/generate-dor-skill.mjs`, `dor/skill.md`, whose byte-identity with `dor skill` output is pinned by `dor/test/cli-output.test.mjs`. +## Helper exclusion + +**Must exclude unpromoted helpers from discovery and control**, including direct internal-id targets and helper-origin requests. Promotion assigns the ordinary public Surface ref without changing Session identity; subsequent CLI operations use ordinary Surface semantics. + +Source of truth: `buildDorSurfacesInternal` in `lib/src/components/Wall.tsx`; `dispatchDorControlRequest` in `lib/src/lib/platform/dor-control-dispatch.ts`. + ## Future - **Surface a dead control channel in the UI.** A lost bind leaves one diff --git a/docs/specs/glossary.md b/docs/specs/glossary.md index f28e0cc74..9f62aa7ad 100644 --- a/docs/specs/glossary.md +++ b/docs/specs/glossary.md @@ -54,10 +54,12 @@ The containment hierarchy `dor` handles commit to (`docs/specs/dor-cli.md`): Window ⊃ Workspace ⊃ Pane ⊃ Surface (terminal = Session | browser) ``` -**Surface identity:** a Surface's id is its Lath leaf id. A terminal Surface's *is* its `SessionId`, stable (I1); browser replacement and relaunch have different identity effects (I10). +**Surface identity:** a primary Surface's id is its Lath leaf id; a helper receives its Lath leaf only on promotion. A terminal Surface's *is* its `SessionId`, stable (I1); browser replacement and relaunch have different identity effects (I10). ## Containers +**Must keep a helper as an auxiliary terminal Surface in its source's Pane**, with a stable Session id and explicit parent association. It has no independent Lath leaf, public ref, or alerting until promotion; `docs/specs/terminal-context.md` owns its lifetime. A shown helper is `Paned` within the source body; a closed context leaves it `Hidden` and DOM-parked (`Mounted`). + Workspace and Window are containers, not Session layers — they group Surfaces rather than describing one Surface's state (containment is I7). | Container | Holds | Owner | @@ -148,7 +150,7 @@ A **Session** is the tuple of its `SessionId` plus one state per layer (I1). | State | Meaning | |---|---| -| `Paned` | Rendered as a pane in the content area (a Lath leaf) | +| `Paned` | Rendered in the content area: a primary Lath leaf or its shown auxiliary helper | | `Zoomed` | Subset of `Paned` — the passthrough-focused pane is maximized; acquiring zoom gives focus, losing focus returns it to `Paned` | | `Doored` | Rendered as a door on the baseboard. DOM survival is a rendering decision, not part of this state: browser DOM retention follows **parking** and eviction (`docs/specs/tiling-engine.md` → "Parked leaves"); a terminal Surface unmounts its element (Registry: `Orphaned`) and remounts the same xterm on reattach — nothing replays | | `Hidden` | In neither pane nor door — webview closed or mid-transition; inactive-Workspace presentation is staged (`docs/specs/layout.md` → Future). Process and Activity unaffected. | diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 7cbe90ad1..7f26e63c3 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -54,17 +54,33 @@ The label is the `DerivedHeader` from `deriveHeader(...)`; `docs/specs/terminal- #### Header context menu -**One menu per terminal pane, opened by right-click anywhere on the header or by `>` in command mode** — at the pointer, or under the header's left edge (`data-pane-header-for` plus a synthetic `contextmenu`, so both paths share one code path). Browser headers and Doors have no such menu, so `>` no-ops there. Only the alert bell owns its own right-click (`stopPropagation`, opening the alert dialog); every other region, the title span included, bubbles here. It is portaled to `document.body`, viewport-clamped, and dismissed by outside `pointerdown`, `Escape`, `resize`, or capture-phase `scroll` — **never by a scroll originating inside the menu**, since arrow-key focus moves auto-scroll the overflowing list. +**Must open the terminal context from terminal header, alert, body, and command-mode `>` entry points.** Browser-only Surfaces and Doors have no context. Application mouse ownership follows `docs/specs/mouse-and-clipboard.md` → Terminal context input. -Content, top to bottom: +**Must float the context inside its source Pane with a one-rem inset on every side**, overlapping the header, with a theme-derived edge and raised shadow. Render it in the Lath leaf's overlay slot, outside the body's clipping box, so it follows the leaf's layout without remounting the helper. Keep one context per Wall. Outside pointer press and explicit close dismiss it. No separate context heading or clipboard toolbar is shown. -- **Header row** — display title, the pane's `surface:N` handle (`resolveSurfaceRef`, muted), close button. -- **Title-candidates table** — latest entry per `titleCandidates` channel (`docs/specs/terminal-state.md`): channel, text, timestamp; else a muted `No title candidates`. **Diagnostic only** — it changes no title priority rule. -- **Port rows** — the TCP ports the pane's process tree binds, scanned by `getOpenPorts` **once per open** (reopen to rescan): a spinner, then one `host:port` row per distinct port (digit chip first, process name muted beside it), else a muted `no listening ports` / `port scan failed`. +**Must reveal the context from the opening pointer position, clamped to its bounds, over 320ms.** Alert activation uses the alert button center; command-mode `>` uses the header's bottom-left; openings without a position use the context's top-left. Keep final layout dimensions throughout the reveal. Start helper creation, settings reads, and port scanning immediately on mount; fade mounted content, including detail dialogs, in over 140ms after 160ms. Reduced motion or disabled layout animation skips both animations and the delay. -**The menu owns the keyboard while open**: DOM focus on mount, the previously focused element restored when a dismissal leaves input ownership unchanged, and registration as dialog-keyboard-active so command-mode keys don't fire underneath. `1`–`9` activate the matching port row and **presses during the scan are dropped, never buffered**; `↑`/`↓` rove the rows (wrapping), `Enter`/`Space` activate the focused row, `Tab`/`Shift+Tab` cycle every focusable element, `Escape` closes. +**Must contract dismissals toward the opening origin over 180ms, fading content over 100ms**, starting from the current reveal when interrupted. Make the closing context inert and pause helper polling immediately; release focus without waiting for removal. Reopening cancels pending removal. Reduced motion dismisses immediately; promotion, source removal, and replacement by another context retain their immediate lifecycle transitions. -Activating a port row reproduces `dor ab open ` for that port and closes the menu at once (`docs/specs/dor-browser.md` → Pane Context Menu Connect): the browser surface becomes the selection in passthrough, reattaching first if minimized — **the one command-mode gesture that moves selection off the pane it targeted and exits command mode** — with loading/errors surfacing in the pane, not the menu. With no `agentBrowserCommand` the rows are inert labels with no digit chips. Source of truth: `lib/src/components/wall/PaneHeaderContextMenu.tsx`, `lib/src/components/wall/TerminalPaneHeader.tsx`, `lib/src/components/wall/keyboard/handle-pane-shortcuts.ts`. +| Row | Content | +|---|---| +| Title | Derived display title, labeled Explain action, copyable source Surface ref and close at right | +| Dir | Home-abbreviated directory, native explorer action, absolute-path copy | +| Ports | One scan per opening; scanning/empty/failure states; one port inline, multiple ports in a dropdown with count beside it; four labeled actions | +| Alerts | Source Watch and TODO controls; notification details directly below | +| Helper | Remaining space; one-line status, Modify/Reset and Promote; hide its name below 48rem container width | + +**Must focus context controls on opening.** Explicit entry into helper xterm gives it terminal keys; Escape there belongs to its program. Escape from controls closes the innermost disclosure, then context. Terminal clipboard routing uses the focused helper rather than the selected source. Actions use subdued link color and shared compact `OnOffSwitch` controls. + +**Must tint the copyable Surface ref as an action and confirm each successful context copy in its button** with a checkmark and “Copied” for 1.4 seconds, preserving button width and keeping the context open. Failed copies show the action error without success feedback. + +**Must suppress context action hover and focus highlights while the window is unfocused**, including after opening a native explorer or system browser. + +**Must show “Opening…” with a spinner in the directory explorer button during launch and for at least 0.75 seconds**, preserving width and keyboard focus while blocking repeat clicks. Stop immediately on failure and show the action error. Respect reduced motion by keeping the spinner static. + +**Must promote by adopting the helper Session into a new split beside the source**, preserving identity and focusing it. Helper lifetime and source closure are owned by `docs/specs/terminal-context.md`. + +Source of truth: `TerminalContext` in `lib/src/components/wall/TerminalContext.tsx`; `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`; `TerminalLeafOverlay` in `lib/src/components/wall/LathHost.tsx`; `TerminalPanel` in `lib/src/components/wall/TerminalPanel.tsx`; `TerminalPaneHeader` in `lib/src/components/wall/TerminalPaneHeader.tsx`; `useWallKeyboard` in `lib/src/components/wall/use-wall-keyboard.ts`; `.terminal-context-enter` / `.terminal-context-content` in `lib/src/theme.css`. Tests: `lib/src/components/wall/TerminalContext.test.tsx`, `lib/src/components/Wall.test.tsx`. ### Pane body diff --git a/docs/specs/mouse-and-clipboard.md b/docs/specs/mouse-and-clipboard.md index 198a83582..3d8e91277 100644 --- a/docs/specs/mouse-and-clipboard.md +++ b/docs/specs/mouse-and-clipboard.md @@ -287,6 +287,15 @@ Dormouse's own ``s — pane rename, the browser URL editor, dialog fields --- +## Terminal context input + +**Must give application-captured right-click to the terminal program**, retaining header right-click as the context entry point. Do not add a Shift-right-click override gesture. A helper never opens a recursive context. + +**Must route clipboard chords and selection operations to the focused helper**, while leaving its Escape, Tab, arrows, and digits with xterm. Copying and selection do not disarm autorun; terminal input, paste, drops, and application mouse reports do. + +Source of truth: `TerminalPanel` in `lib/src/components/wall/TerminalPanel.tsx`; `useWallKeyboard` in `lib/src/components/wall/use-wall-keyboard.ts`; `markSessionTouched` in `lib/src/lib/terminal-lifecycle.ts`. + + ## 9. Future Not implemented today; they may be added in response to user feedback. diff --git a/docs/specs/security-local.md b/docs/specs/security-local.md index 4ef2ddd69..4b5690a05 100644 --- a/docs/specs/security-local.md +++ b/docs/specs/security-local.md @@ -171,3 +171,9 @@ does. A gap, not an accepted risk. Source of truth: `SESSION_STATE_KEY` in `vscode-ext/src/session-state.ts`, `ensureToken` in `vscode-ext/src/peer-link.ts`, `default_log_path` in `standalone/src-tauri/src/lib.rs`. + +## Terminal context directory actions + +**Must validate context directory arguments as existing absolute directories and pass the canonical path as one process argument without shell interpretation.** Keep this capability separate from the external-URL allowlist. VS Code per-terminal context requests and helper ownership updates remain scoped to the owning router. + +Source of truth: `context` in `standalone/sidecar/pty-core.js`; `attachRouter` in `vscode-ext/src/message-router.ts`. Test: `standalone/sidecar/helper-terminal.test.js`. diff --git a/docs/specs/security-remote.md b/docs/specs/security-remote.md index d45555bae..a471af08a 100644 --- a/docs/specs/security-remote.md +++ b/docs/specs/security-remote.md @@ -220,6 +220,12 @@ and nothing records connects, attaches, denials, or writes. A self-hoster cannot "did anyone connect to my laptop last night", which also means an ACL entry added by any of the paths above would be invisible after the fact. +## Auxiliary helpers + +**Must exclude unpromoted helpers from both remote directory discovery and direct attachment/resize resolution.** Promotion enables ordinary terminal access; hidden helper output and input are unavailable before that ownership change. + +Source of truth: `collectDirectorySnapshot` in `lib/src/remote/burrow/directory-collect.ts`; `driveOwnSurface` in `lib/src/remote/burrow/peer-surfaces.ts`. + ## Future ### Cloud-hosted mode diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index efa0eb1f6..9285ebc43 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -26,7 +26,7 @@ A focused cross-origin iframe surface swallows the gesture; the proxy shim detec | `,` | Rename | Inline rename of the selected terminal pane's title; consumed no-op on browser surfaces and doors. | | `a` | Toggle alert | Dismiss or toggle the bell alert. Terminal Surfaces only; doors excluded. | | `t` | Toggle todo | Toggle the TODO marker on the selected Surface, terminal or browser; doors excluded. | -| `>` | Header context menu | Terminal panes only; consumed no-op on browser panes, inert on doors. | +| `>` | Terminal context | Terminal panes only; consumed no-op on browser panes, inert on doors. | ## Navigation (command mode) @@ -67,11 +67,8 @@ Every key not claimed above forwards to the embedded page while a screencast pan |-----|--------|-------------| | `Esc` | Close / cancel | Dismiss a dialog or popover; cancel a rename or kill confirmation; abort an in-progress sash or pane drag. | | `Enter` | Confirm rename | Save the new name while renaming a pane; blur commits too. | -| `Tab` / `Shift+Tab` | Focus cycle | Cycle focus through an open popover or dialog (trapped, wrapping). | +| `Tab` / `Shift+Tab` | Focus cycle | Cycle popover/dialog controls. In terminal context, navigate controls into the helper; once helper xterm has focus, Tab belongs to its program. | | Prompted letter | Confirm kill | Type the letter shown to confirm; other keys reaching the prompt cancel (see layout's dispatch order). | -| `a` / `t` (alert dialog open) | Toggle alert / todo | Same as command-mode `a` / `t`, for the dialog's Session. | -| `1`–`9` (header context menu open) | Connect port | Open the nth port row in a browser surface, select it, enter passthrough. Dropped, never buffered, unless the scan loaded a row for that digit and the host can open one. | -| `↑` / `↓` (header context menu open) | Move row focus | Rove focus across port rows, wrapping; `Enter`/`Space` activates the focused row. | ## VS Code host @@ -91,7 +88,7 @@ The standalone host contributes no chords; `docs/specs/standalone.md` owns its n - `lib/src/components/wall/keyboard/` — one module per dispatch branch: `handle-dual-tap.ts`, `handle-editable-clipboard.ts`, `handle-mouse-selection-keys.ts`, `handle-kill-confirm.ts`, `handle-pane-shortcuts.ts`, `handle-pane-navigation.ts`; platform modifiers in `chords.ts` - `lib/src/lib/vscode-keybindings.ts` — the workbench mirror allowlist - `lib/src/lib/terminal-mouse-router.ts` — live Alt tracking during a drag -- `lib/src/components/SelectionPopup.tsx`, `lib/src/components/wall/PaneHeaderContextMenu.tsx`, `lib/src/components/TodoAlertDialog.tsx`, `lib/src/components/wall/InlineEditInput.tsx`, `lib/src/components/use-popover-focus-trap.ts` — the popover/dialog handlers +- `lib/src/components/SelectionPopup.tsx`, `lib/src/components/wall/TerminalContextView.tsx`, `lib/src/components/wall/InlineEditInput.tsx` — the popover/dialog handlers - `lib/src/components/wall/agent-browser-surface-controller.ts` — browser key forwarding and the edit-chord bridge ## Future diff --git a/docs/specs/standalone.md b/docs/specs/standalone.md index 291785646..d6bb7b1e0 100644 --- a/docs/specs/standalone.md +++ b/docs/specs/standalone.md @@ -513,3 +513,9 @@ Source of truth: `standalone/package.json` (package scripts), - `pnpm dev:standalone:ab` runs the sidecar + webview in a normal browser via the browser-dev harness instead of the Tauri WebView (`docs/specs/transport.md`, Standalone browser-dev harness). + +## Terminal context host operations + +The adapter forwards every `TerminalContextRequest` to the PTY host as a correlated request (`docs/specs/transport.md` → Auxiliary helper metadata); directory opening follows `docs/specs/security-local.md` → Terminal context directory actions, and inspection failure follows `docs/specs/terminal-context.md` → Helper lifecycle. + +Source of truth: `terminalContext` in `standalone/src/tauri-adapter.ts`; `pty_context` in `standalone/src-tauri/src/lib.rs`; `context` in `standalone/sidecar/pty-core.js`. diff --git a/docs/specs/terminal-context.md b/docs/specs/terminal-context.md new file mode 100644 index 000000000..d0bee8120 --- /dev/null +++ b/docs/specs/terminal-context.md @@ -0,0 +1,66 @@ +# Terminal context + +> See `docs/specs/glossary.md` for Surface / Session / Pane vocabulary. +> This spec owns the helper terminal lifecycle and global autorun preference. +> Layout owns context composition and input focus; terminal-state owns shell +> semantics; alert owns suppression; transport owns live recovery. + +## Helper lifecycle + +- **Must create at most one helper per source, lazily on first context opening.** Concurrent openings share the same pending creation. Closing the source during startup cancels creation. Helpers cannot have helpers. +- **Must start with the configured shell in the source's local directory**, using the ordinary split fallback when unavailable. Shell exports and virtual environments are not inherited. SSH integration is outside this feature. +- **Must inject autorun only after integrated shell readiness**, accepting prompt-start and prompt-end/editing states with no current command. User input before injection cancels it. After eight seconds without readiness, show an unsupported state and never write a timeout fallback. +- **Must treat typing, paste, accepted drops, and application mouse input as user work**, disarming automatic refresh until Reset or Promote. Selection, copying, resize, and terminal protocol replies do not count. Returning to idle does not rearm autorun. +- **Must refresh an untouched, completed or autorun-disabled helper on reopening only after a host idle check.** Recheck ownership and the touched flag after that asynchronous check; foreground commands, background descendants, and failed/unknown inspection preserve the helper. +- **Must hide a retained helper without terminating its PTY**, parking its xterm element in the document. Revealing or promoting reuses the same element; cleanup from an older mount cannot detach a newer mount. +- **Must keep a preserved helper's directory independent of its source**, showing both locations prominently when they differ. Unknown directory state is not evidence of a match. +- **Must retain exited output**, offer Reset, and avoid automatic restart loops. +- **Must pause status and process-inspection polling while the context is hidden**, invalidating cached idle results. Reopening publishes current terminal status; source closure inspects work on demand. + +| State | Status and action | +|---|---| +| Starting | Waiting for shell…; Modify | +| Autorun executing | Running the captured command; Modify | +| Untouched completion | Captured command autoran; Modify | +| User input | Skipping autorun to preserve user keystrokes; Reset | +| Empty default | Autorun off; Modify | +| No readiness | Autorun skipped: shell readiness unavailable; Modify | +| Exited | Helper exited; Reset | + +**Must make Reset an explicit discard**, confirming loss of scrollback, unfinished input, running programs, and unsaved edits. Cancellation changes nothing; confirmation disposes the old helper and launches a fresh one using the source's current directory and current global setting. Stale timers cannot write to the replacement. + +Source of truth: `openHelper` / `helperHasWork` / `disposeHelper` in `lib/src/lib/helper-terminal.ts`; `markSessionTouched` / `unmountElement` in `lib/src/lib/terminal-lifecycle.ts`; `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`. Tests: `lib/src/lib/helper-terminal.test.ts`. + +## Promotion and source closure + +**Must promote the actual Session into a regular split beside its source**, preserving the PTY, xterm, scrollback, directory, partial input, and identity. Cancel pending autorun, close context, assign the public Surface ref, and focus the promoted terminal. Failed placement restores auxiliary host ownership. The source's next opening creates a new helper. + +**Must reject Reset and duplicate Promote during ownership transfer**, including from a reopened context. A failed transfer or rollback reports its error and resumes inspection so promotion can be retried. + +**Must close an idle helper with its source**, even when it has user input or scrollback. The idle shell itself is not running work. Existing source-work confirmation remains applicable. + +**Must block source closure while its helper has running work**, warn, and reveal the helper. The user stops the work there and retries closure; no force-close-both or automatic promotion is offered. Failed process inspection keeps both terminals and reports the error. CLI attempts to close such a source return failure. + +**Must include hidden helper work in shutdown checks.** The helper's host inspection also detects background descendants; unresolved inspection, including a process table missing the live PTY process, counts conservatively as work. Minimizing the source hides its context and retains the helper. + +Source of truth: `beginPromotion` / `cancelPromotion` / `finishPromotion` / `helperHasWork` in `lib/src/lib/helper-terminal.ts`; `helperBlocksClose` / `contextActions` in `lib/src/components/Wall.tsx`; `countRunningSessions` in `lib/src/lib/terminal-state-store.ts`. + +## Global autorun setting + +**Must default to `git status`; an empty command disables autorun.** An explicit Modify edit applies to new and reset helpers, never a retained one. Its status describes the command captured at creation, even when the global default changes. + +**Must accept only a single command line of at most 4096 characters**, excluding CR, LF, and NUL. The host persists only this preference in `~/.dormouse/helper-terminal.json`, using atomic replacement with private file permissions. All desktop renderers read that shared preference through the host; the fake adapter keeps a deterministic in-memory setting. + +**Must enforce command validity at the host.** A rejected edit keeps the prior preference and displays the error in Modify. + +Source of truth: `context` in `standalone/sidecar/pty-core.js`; `terminalContext` in `lib/src/lib/platform/fake-adapter.ts`; `TerminalContextRequest` in `lib/src/lib/terminal-context-types.ts`. + +## Presentation coverage + +**Must share the context presentation between the live menu and its state gallery.** + +Source of truth: `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`; `lib/src/stories/TerminalContext.stories.tsx` supplies sample output; `lib/src/stories/Wall.stories.tsx` exercises the live helper with the fake shell. + +## Future + +Pocket context composition, remote helper creation, and SSH integration are unbuilt. diff --git a/docs/specs/terminal-state.md b/docs/specs/terminal-state.md index 4b4e7ab52..32a10fe70 100644 --- a/docs/specs/terminal-state.md +++ b/docs/specs/terminal-state.md @@ -150,3 +150,11 @@ Source of truth: `deriveHeader` / `deriveSurfaceLabel` / `resolveDisplayPrimary` - **`prompt` and `editing` collapse into one `idle` bucket**; **`finished` stays distinct** so a recently-completed pane can be filtered separately though its header label carries the same `` prefix. `statusBucket` projects the 5 `ShellActivity.kind` values onto 4. Source of truth: `groupTerminalPanes` / `TerminalGroupingMode` / `cwdIdentity` / `statusBucket` in `lib/src/lib/terminal-state.ts`. + +## Terminal context diagnostics + +**Must derive title explanation from the header's winning-title functions**, including user override, eligible OSC candidate, notification title, and command fallback. Retain the last command's captured title when later shell OSCs replace live candidates; the diagnostic table is not an OSC history. + +**Must abbreviate home only at a complete path boundary**, retaining the absolute path for copying and native directory operations. Compare helper and source host identity as well as directory paths. + +Source of truth: `explainTerminalTitle` / `cwdDisplay` in `lib/src/lib/terminal-state.ts`; `TerminalContext` in `lib/src/components/wall/TerminalContext.tsx`. diff --git a/docs/specs/tiling-engine.md b/docs/specs/tiling-engine.md index 66c5c9e2a..a59f9a04d 100644 --- a/docs/specs/tiling-engine.md +++ b/docs/specs/tiling-engine.md @@ -152,7 +152,7 @@ Source of truth: `lib/src/components/wall/lath-wall-store.ts`; `lib/src/componen **An adapter owns exactly three things**: mapping input into Wall coordinates, applying animator frames to its scene each tick, and hosting pane content. Layout, ops, sash geometry, and animation timelines are core and shared; LathHost is the engine's only non-headless part. - One flat container; one stable `position: absolute` div per leaf, keyed by id, carrying `data-lath-leaf`, moved and resized by inline styles, hosting pane content as ordinary React children. The div is **never re-parented, never reordered, and never unmounted** except on a remove commit or a park eviction, and **leaf divs render in sorted-by-id DOM order, not tree order** — reordering keyed siblings moves DOM nodes, blurring the xterm inside one and reloading a moved `