Skip to content
Draft
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
32 changes: 26 additions & 6 deletions docs/specs/dor-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,32 @@ Source of truth: `ToolPanel` in `lib/src/components/wall/ToolPanel.tsx`; `ToolPa

Source of truth: `toolCommand` in `dor/src/commands/tool.ts`; `dor/test/snapshots/help/tool.md`; `ToolSurfaceResponse` in `dor/src/commands/types.ts`.

## Take-over

**Must run a standalone `dor tool` invocation in its calling pane when every takeover condition holds.** Otherwise use the ordinary split path. Trust approval and keyed reuse take precedence. (rationale)

| Condition | Required state |
| --- | --- |
| Caller | Visible, integrated plain terminal; not closing or dying |
| Command line | OSC 633 reports `dor tool` alone; compound shell syntax rejects takeover |
| Directory | Resolved Tool CWD equals the caller's reported CWD |
| Placement | Neither `--surface` nor `--minimize` supplied |
| Helper | No existing auxiliary helper; preserve it by splitting |
| Trust | Already approved; pending approval always uses its own pane |

**Must answer `takeover` before waiting for the calling shell's prompt**, then transform and type the command. The answer promises placement, not successful command startup.

- **Must leave the caller unchanged on prompt timeout or cancellation**, and recheck visibility, closing state, CWD, kind, and helper presence after the wait. A helper opened during the handshake prevents transformation.
- **Must change components and params in one metadata commit**, retaining the Session id, Surface ref, scrollback, notes, source pins, and any user rename.
- **Must clear previous OSC 367 hints before typing the new command.**
- **Must retain the spawn lock until the typed command is observed running or a new completed run is observed**, or the wait ends. A command that starts and exits between samples releases the lock too.
- **Must rerun a keyed match in the caller through the same answer/prompt handshake**, reporting `adopted`, when its line is standalone and integrated. Never interrupt the waiting `dor` process. Placement flags do not relocate an existing match; run in its current directory.
- **Must report an error when the caller is the keyed match but its command line cannot be typed behind**, instead of reporting a misleading `existing` result.
- **May interleave user keystrokes arriving between the prompt and command injection.** Takeover does not reserve the shell input buffer.
- **Must include already-owned background listeners in the usual process-tree scan.** Under `auto`, they can become the sole candidate or cause a conflict.

Source of truth: `toolTakesOverCaller` / `toolRerunsInCaller` in `lib/src/components/wall/tool-takeover.ts`; `runToolInCallerPane` in `lib/src/components/wall/use-dor-control.ts`; `setMeta` in `lib/src/components/wall/lath-wall-store.ts`. Tests: `lib/src/components/wall/tool-takeover.test.ts`, `lib/src/components/Wall.test.tsx`.

## OSC 367

**Must consume OSC 367 at the PTY owner's parser**, including malformed and unknown verbs, and emit no reply. `serve` is the only implemented verb. The escape registry is `docs/specs/terminal-escapes.md`.
Expand Down Expand Up @@ -166,12 +192,6 @@ Source of truth: `PersistedToolMetadata` in `lib/src/lib/session-types.ts`; `sav
`DORMOUSE_DEHYDRATE`; the `dehydrate` flag is reserved in the serve payload
from the shipped `serve` payload. The Windows graceful-stop is needed here
only.
- **Pane take-over.** `dor tool` typed alone at a prompt should run in that
pane rather than splitting — typing a command at a prompt is how a terminal
works. The gate is three conditions the host can already read (sole command on
the OSC 633 line, pane at a prompt, pane not already a tool); what it needs is
the handshake, since `dor` is itself the foreground process when it answers,
so the command can only be typed once its own shell returns to a prompt.
- **The announced `name`.** Wire the reserved [OSC 367](#osc-367) `name` into
the title-candidates channel and `dor list`'s location column.
- **Later** — `prespawn_*` beyond the dedupe literal: a computed key, and
Expand Down
6 changes: 6 additions & 0 deletions docs/specs/dor-tool.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ A hardcoded Storybook port can disagree with the port it obtains under contentio

The September 2026 integration reuses Terminal Context for the Tool's primary terminal. The auxiliary helper's automatic refresh, Reset, and Promote semantics do not describe a serving command, whose Session also owns the browser and remote terminal identity. Sharing the presentation avoids introducing a second navigation mechanism or a second shell.

## Take-over

**Why the gate is conservative in the split direction.** Every condition can be read wrong in two directions, and the two costs are nowhere near equal. Declining a take-over that should have happened costs a pane the user closes — the tool still runs, in the placement `dor tool` has always used. Taking over a pane that should have split types a command into a shell that belongs to something else: an agent's session, a line with work queued behind `dor`, a directory the tool was not asked to run in. So each condition is written to fail closed, and quoting is not unpicked — a line carrying `&&` inside quotes splits rather than being parsed for whether that `&&` is real.

**Why the naked test is worth having at all, given `dor send`.** It answers "did a human ask for this *here*", not "is this trustworthy". The discrimination it actually makes is placement: an agent's `dor tool` runs under the agent's own command line, so the pane reports `claude` (or `bash script.sh`) and never matches — which is the whole point, since an agent's tool must not commandeer the pane the human is watching the agent in. Trust is a separate gate with a separate ceremony, and it is the one that carries the security weight.

## Security

Hostile text printed by the designated command can contain an announcement. The current process-tree check limits port selection to that Session's discovered listeners; browser content still executes under the existing renderer boundaries. Earlier text describing arbitrary local-port selection did not match the scan implementation.
Expand Down
2 changes: 1 addition & 1 deletion docs/specs/tiling-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ A **parked** leaf is mounted by the adapter but absent from the split tree: its
| `forgetLeaf(id)` | — | destroyed | unmounted if parked — destroys a Door |

- **Parking must be one commit** — an id absent from both the tree and `parked` for even one render unmounts the leaf and loses its DOM state. Every re-admitting op (`addLeaf`, `restoreLeaf`, `insertLeaf`, `replaceLeaf`, `seed`) unparks in that same commit through the one shared `admit` helper, which also seeds the enter hint. **`seed` admits by tree membership**, never by the metadata it is handed (rationale). Dormant while `seed` runs once at startup; live in the workspaces-rollout switch.
- **One `leafMeta` map holds every leaf the Wall owns**, laid out or Doored; `parked` is pure render state (`Map<id, Rect | null>`) naming the subset that keeps its DOM. Detachment is a fact about the *tree*, so **no Door record carries a metadata copy that can go stale** — `setTitle` / `updateParams` reach a Doored leaf by the same single path as a visible one, and every reader goes through `lath.getMeta(id)` (rationale). `serializeLayout` filters `leafMeta` to the tree's own leaves; a Door persists as its own row.
- **One `leafMeta` map holds every leaf the Wall owns**, laid out or Doored; `parked` is pure render state (`Map<id, Rect | null>`) naming the subset that keeps its DOM. Detachment is a fact about the *tree*, so **no Door record carries a metadata copy that can go stale** — `setTitle` / `updateParams` / `setMeta` reach a Doored leaf by the same single path as a visible one, and every reader goes through `lath.getMeta(id)` (rationale). `serializeLayout` filters `leafMeta` to the tree's own leaves; a Door persists as its own row.
- **The store holds a parked leaf's last rect, never the adapter** — `registerEl(null)` is a ref detach, not an unmount (rationale). `doorLeaf({ park: true })` captures the rect in the commit that removes the leaf from the tree, `admit` replays it into the animator on re-admission (Animation → Enter), and LathHost renders parked ids there behind `visibility: hidden; pointer-events: none` and `data-lath-parked`, so the guest never sees a zero-extent viewport (rationale). A leaf parked before the Wall reports geometry falls back to the whole wall rect.
- **Parked is a visibility signal, not just a layout fact** — it reaches the body as `PaneProps.parked` (Pane props contract), so a minimized `ab-screencast` stays mounted, releases viewer resources, and retains its daemon session.
- **Who parks**: `shouldParkOnMinimize` — browser Surfaces, not terminals, whose persistent xterm instance remounts without replay ([glossary.md → View](glossary.md#view)).
Expand Down
3 changes: 2 additions & 1 deletion dor/src/commands/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,14 @@ A dormouse.yml is repo-controlled and its entries execute, so it is inert until

Approving an upstream covers every worktree and clone of that repo. Approving a folder covers that checkout only, which is what you want for a branch you have not read.

Where the tool lands: it always splits without taking focus and prints the new surface's handle, whether a human typed it or a script did. Taking over the calling pane when the invocation is typed alone at a prompt is designed but not built.
Where the tool lands: typed alone at a prompt, it takes over the pane you typed it in — no split, same surface, same scrollback — and reports "takeover". Anything else splits without taking focus and prints the new surface's handle. The take-over needs an integrated shell running \`dor tool\` as the whole command line, a visible plain terminal pane without an auxiliary helper, and the tool's directory to be that pane's own, so an agent's invocation, a compound line, --minimize, --surface, and --cwd elsewhere all split instead. The handle prints before the command starts, since dor has to exit before its own shell is free to run it.

--cwd sets the working directory used to find dormouse.yml and to run the command; it defaults to the directory dor was invoked from.

Text output:
created surface:3 "pnpm storybook"
existing surface:3 "pnpm storybook"
takeover surface:1 "pnpm storybook"

JSON output:
{
Expand Down
6 changes: 5 additions & 1 deletion dor/src/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,12 @@ export interface ToolSurfaceResponse {
* `existing` is a key match on a live tool: the redundant spawn never
* started. `adopted` is a key match whose command had exited — the Surface is
* reused and the command re-run in place, keeping its position and scrollback.
* On the calling pane's own match it is answered before the re-run is typed,
* for the same reason `takeover` is. `takeover` is the calling pane itself
* becoming the tool, answered before the command is typed — `dor` has to exit
* before its own shell is free to run it.
*/
status: 'created' | 'existing' | 'adopted' | 'pending';
status: 'created' | 'existing' | 'adopted' | 'pending' | 'takeover';
surfaceId: string;
surfaceRef: string;
/** The rendered command, as typed into the shell. */
Expand Down
3 changes: 2 additions & 1 deletion dor/test/snapshots/help/tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ A dormouse.yml is repo-controlled and its entries execute, so it is inert until

Approving an upstream covers every worktree and clone of that repo. Approving a folder covers that checkout only, which is what you want for a branch you have not read.

Where the tool lands: it always splits without taking focus and prints the new surface's handle, whether a human typed it or a script did. Taking over the calling pane when the invocation is typed alone at a prompt is designed but not built.
Where the tool lands: typed alone at a prompt, it takes over the pane you typed it in — no split, same surface, same scrollback — and reports "takeover". Anything else splits without taking focus and prints the new surface's handle. The take-over needs an integrated shell running `dor tool` as the whole command line, a visible plain terminal pane without an auxiliary helper, and the tool's directory to be that pane's own, so an agent's invocation, a compound line, --minimize, --surface, and --cwd elsewhere all split instead. The handle prints before the command starts, since dor has to exit before its own shell is free to run it.

--cwd sets the working directory used to find dormouse.yml and to run the command; it defaults to the directory dor was invoked from.

Text output:
created surface:3 "pnpm storybook"
existing surface:3 "pnpm storybook"
takeover surface:1 "pnpm storybook"

JSON output:
{
Expand Down
Loading