From 330ad23d150e625e2f36da34ef1c42da5cee1a05 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 1 Oct 2026 14:29:53 -0700 Subject: [PATCH 1/6] Show title explanation as an icon beside the title --- docs/specs/layout.md | 4 ++-- lib/src/components/wall/TerminalContext.test.tsx | 8 ++++---- lib/src/components/wall/TerminalContextView.tsx | 13 ++++++------- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 921da3f04..4c82e93e5 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -88,7 +88,7 @@ Popups share the zoomed pane’s app-background halo. | Row | Content | |---|---| -| Title | Derived display title, labeled Explain action, copyable source Surface ref and close at right | +| Title | Derived display title, icon-only Explain bug immediately beside the title, copyable source Surface ref and close at right | | Dir | Home-abbreviated directory, its unlabeled absolute-path copy, native explorer action | | Ports | One scan per opening; scanning/empty/failure states; one port inline, multiple ports in a dropdown with count beside it; labeled launch actions | | Alerts | Source Watch and TODO controls; notification details directly below | @@ -98,7 +98,7 @@ Popups share the zoomed pane’s app-background halo. **Must write visible action text in the Title, Dir, and Ports rows in lowercase**, since `iframe` and `agent-browser` cannot be capitalized; proper nouns such as Finder, tooltips, and accessible names keep their case. -**Must keep the Title and Dir rows on one line.** Title: explain drops its label, the title truncates to 8 characters, the Surface ref drops to its copy icon (the tooltip keeps it), then the title truncates further. Dir: the explorer action drops its label before the directory truncates from its start, keeping its end. +**Must keep the Title and Dir rows on one line.** Title: the title truncates to 8 characters, the Surface ref drops to its copy icon (the tooltip keeps it), then the title truncates further. Dir: the explorer action drops its label before the directory truncates from its start, keeping its end. **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. diff --git a/lib/src/components/wall/TerminalContext.test.tsx b/lib/src/components/wall/TerminalContext.test.tsx index f4a1e1f8c..1bfe5d20b 100644 --- a/lib/src/components/wall/TerminalContext.test.tsx +++ b/lib/src/components/wall/TerminalContext.test.tsx @@ -136,7 +136,7 @@ it('moves trailing actions into the dropdown on resize and dispatches hidden act } finally { client.mockRestore(); offset.mockRestore(); vi.unstubAllGlobals(); } }); -it('drops the explain label, then the Surface ref text, as the title row narrows', () => { +it('keeps Explain icon-only and compacts the Surface ref as the title row narrows', () => { let width = 320; const observers = new Set<() => void>(); vi.stubGlobal('ResizeObserver', class { @@ -144,14 +144,14 @@ it('drops the explain label, then the Surface ref text, as the title row narrows observe() {} disconnect() {} unobserve() {} }); const client = vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function(this: HTMLElement) { return this.hasAttribute('data-context-title') ? width : 0; }); - // Ten pixels a character, twenty an icon, 8ch as eighty: the title is 80, explain 90 or 20, the ref 110, close 20. + // Ten pixels a character, twenty an icon, 8ch as eighty: title 80, Explain 20, ref 110, close 20. const offset = vi.spyOn(HTMLElement.prototype, 'offsetWidth', 'get').mockImplementation(function(this: HTMLElement) { return (this.textContent?.length ?? 0) * 10 + this.querySelectorAll('svg').length * 20 + (this.classList.contains('w-[8ch]') ? 80 : 0); }); const resize = (next: number) => act(() => { width = next; observers.forEach(notify => notify()); }); try { render(); - expect(button('Explain this title').textContent).toBe('explain'); + expect(button('Explain this title').textContent).toBe(''); expect(button('Copy surface:3').textContent).toBe('surface:3'); resize(260); expect(button('Explain this title').textContent).toBe(''); @@ -159,7 +159,7 @@ it('drops the explain label, then the Surface ref text, as the title row narrows resize(200); expect(button('Copy surface:3').textContent).toBe(''); resize(320); - expect(button('Explain this title').textContent).toBe('explain'); + expect(button('Explain this title').textContent).toBe(''); expect(button('Copy surface:3').textContent).toBe('surface:3'); } finally { client.mockRestore(); offset.mockRestore(); vi.unstubAllGlobals(); } }); diff --git a/lib/src/components/wall/TerminalContextView.tsx b/lib/src/components/wall/TerminalContextView.tsx index 9cf117cf8..0e3c9a478 100644 --- a/lib/src/components/wall/TerminalContextView.tsx +++ b/lib/src/components/wall/TerminalContextView.tsx @@ -189,24 +189,23 @@ function TitleRow({ title, surfaceRef, onExplain, onCopyRef, actions }: { const row = useRef(null); const measures = useRef(null); const actionsRef = useRef(null); - const [explainCompact, setExplainCompact] = useState(false); const [refCompact, setRefCompact] = useState(false); - // Explain drops its label, the title truncates to 8ch, the ref drops to its icon, then the title truncates on. - useRowFit(row, measures, (width, gap, [text, least, explainFull, explainIcon, refFull]) => { + // The title truncates to 8ch, the ref drops to its icon, then the title truncates on. + useRowFit(row, measures, (width, gap, [text, least, explainIcon, refFull]) => { const rest = (actionsRef.current?.offsetWidth ?? 0) + 3 * gap; - setExplainCompact(text + explainFull + refFull + rest > width); setRefCompact(Math.min(text, least) + explainIcon + refFull + rest > width); }, [title, surfaceRef], [actionsRef]); return
- {title} - {EXPLAIN_ICON}{!explainCompact && 'explain'} + + {title} + {EXPLAIN_ICON} + {!refCompact && {surfaceRef}}{COPY_ICON}
{actions}
; From d6e20cbfa017d56e682979d0a5d699e36800eec3 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Thu, 1 Oct 2026 14:50:35 -0700 Subject: [PATCH 2/6] Move Surfaces between Workspaces through drag, context and dor --- docs/specs/alert.md | 2 + docs/specs/dor-browser.md | 2 + docs/specs/dor-cli.md | 16 +- docs/specs/dor-tool.md | 2 + docs/specs/layout.md | 15 +- docs/specs/shortcuts.md | 2 + docs/specs/terminal-context.md | 2 + docs/specs/tiling-engine.md | 6 +- docs/specs/transport.md | 2 + dor/skill.md | 6 +- dor/src/cli.ts | 5 + dor/src/commands/move.ts | 54 +++++++ dor/src/commands/types.ts | 17 ++ dor/src/commands/workspace.ts | 4 +- dor/src/control-client.ts | 6 + dor/src/protocol.ts | 3 +- dor/test/move.test.mjs | 28 ++++ dor/test/snapshots/help/dor.md | 2 + dor/test/snapshots/help/move.md | 32 ++++ dor/test/snapshots/help/workspace.md | 2 + lib/src/components/KillConfirm.tsx | 4 +- lib/src/components/Wall.tsx | 92 ++++++++++- lib/src/components/WorkspaceKillConfirm.tsx | 1 + lib/src/components/WorkspaceStrip.tsx | 22 ++- lib/src/components/WorkspaceWindow.test.tsx | 144 ++++++++++++++++- lib/src/components/wall/LathHost.test.tsx | 23 ++- lib/src/components/wall/LathHost.tsx | 7 +- .../components/wall/MoveWorkspaceAction.tsx | 34 ++++ lib/src/components/wall/TerminalContext.tsx | 4 +- .../components/wall/TerminalContextView.tsx | 15 +- .../components/wall/lath-drag-controller.ts | 20 ++- lib/src/components/wall/lath-wall-store.ts | 5 + lib/src/components/wall/surface-move.ts | 151 ++++++++++++++++++ .../components/wall/surface-workspace-drag.ts | 49 ++++++ lib/src/components/wall/use-dor-control.ts | 22 +++ .../wall/use-session-persistence.ts | 10 +- lib/src/components/wall/wall-handles.ts | 23 +++ lib/src/components/wall/wall-types.ts | 2 + lib/src/index.css | 6 + lib/src/lib/window-session-aggregator.test.ts | 24 +++ lib/src/lib/window-session-aggregator.ts | 28 +++- lib/src/lib/workspace-ui-store.ts | 14 +- scripts/spec-word-budgets.json | 10 +- standalone/src/quit-confirm-store.test.ts | 2 +- 44 files changed, 870 insertions(+), 50 deletions(-) create mode 100644 dor/src/commands/move.ts create mode 100644 dor/test/move.test.mjs create mode 100644 dor/test/snapshots/help/move.md create mode 100644 lib/src/components/wall/MoveWorkspaceAction.tsx create mode 100644 lib/src/components/wall/surface-move.ts create mode 100644 lib/src/components/wall/surface-workspace-drag.ts diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 5d4586f4d..726107c2c 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -418,6 +418,8 @@ Source of truth: `SettingsDialog` and `TOPICS` in `lib/src/components/SettingsDi **Must project every Workspace, active or not.** The Activity store spans the whole Window, so what scopes it to one Workspace is the membership each mounted Wall publishes — panes ∪ doors, on every layout commit. +**Must retain TODO and attention by stable Surface ID across a Workspace move and republish membership on both sides**, without replaying notifications on remount. GUI focus acknowledges without input; CLI movement alone does not acknowledge. Pinned by `preserves Session identity, TODO and retained cwd, retires the source ref, and recomputes both unions` in `lib/src/components/WorkspaceWindow.test.tsx`. + Source of truth: `computeWorkspaceUnion` in `lib/src/lib/workspace-union.ts`; `setWorkspaceSurfaces` in `lib/src/lib/workspace-surfaces.ts`; `lib/src/lib/workspace-union.test.ts`. Where it surfaces is host-specific: diff --git a/docs/specs/dor-browser.md b/docs/specs/dor-browser.md index 76f40f10b..8f7ecc30d 100644 --- a/docs/specs/dor-browser.md +++ b/docs/specs/dor-browser.md @@ -971,6 +971,8 @@ Source of truth: `lib/src/lib/platform/iframe-proxy-types.ts`, `vscode-ext/src/message-router.ts`, `vscode-ext/src/webview-html.ts`, `standalone/src/tauri-adapter.ts`. +**Must remount moved iframe Surfaces at their saved URL after consent**; cross-origin navigation may not be visible to the host. Workspace move interaction follows `docs/specs/layout.md` → Moving Surfaces between Workspaces. + ## Future - Stable agent-browser profile/state persistence so pop-out preserves logins, diff --git a/docs/specs/dor-cli.md b/docs/specs/dor-cli.md index 118832f67..bb0038930 100644 --- a/docs/specs/dor-cli.md +++ b/docs/specs/dor-cli.md @@ -267,9 +267,7 @@ and each host's hop in `standalone/src/tauri-adapter.ts`, ## Handle Model -`Window ⊃ Workspace ⊃ Pane ⊃ Surface` (`docs/specs/glossary.md`). **A command's -positional target is always a Surface; a container is named by `--workspace -`** ([dor workspace](#dor-workspace)). `Reserved:` no command targets +`Window ⊃ Workspace ⊃ Pane ⊃ Surface` (`docs/specs/glossary.md`). **Must treat a command's primary target as a Surface; `dor move` also takes a destination Workspace, while `--workspace ` scopes the source** ([dor workspace](#dor-workspace)). `Reserved:` no command targets another Window; a request reaches the window that owns its Surface instead (§Standalone), and the ref grammar for one is [Future](#future). @@ -283,12 +281,12 @@ Invariants: layout/list positions: each Workspace starts at `surface:1` and assigns the next number when a Surface is created/restored. The map and its counter persist in the session snapshot, which is what keeps a retired number from - being reused (`docs/specs/transport.md` → "Persisted session types"). **Only - creation assigns a ref** — layout churn (reorder, + being reused (`docs/specs/transport.md` → "Persisted session types"). **Must assign a ref on creation or adoption into another Workspace** — layout churn (reorder, minimize/reattach, zoom, focus), replacing an untouched terminal with a browser Surface, and browser render-mode swaps all leave it unchanged. **Killing a Surface retires its ref; a later target that names it must fail rather than silently retarget.** +- **Must retire a moved Surface's source ref and allocate the destination's next unused ref**, retaining its stable ID. **Must route a moved caller by that stable ID**: its cached `surface:N` refs now resolve in the destination and may name strangers; unscoped `ensure` searches there and may duplicate work left behind. Agents must use stable IDs across moves or explicitly scope the original Workspace. Print a renderer-local notice in the moved terminal with old/new handles and these scope changes, once after success, without PTY input or Activity changes. - Surface targets also accept `title:`, for human recovery; a title can drift, so automation should prefer refs from command responses or `dor list`. Action commands (`read`, `send`, `await`, `kill`, `dor agent-browser @@ -457,6 +455,14 @@ Source of truth: `dor/src/commands/`, `HELP_PATTERN_TOKENS` and pre-parsing in `lib/src/components/wall/use-dor-control.ts`; help snapshots in `dor/test/snapshots/help/`, pinned exhaustive by `dor/test/cli-help.test.mjs`. +## dor move + +**Must move one Surface within this Window through the shared move coordinator.** Syntax and response fields are owned by `dor move --help` and `MoveSurfaceRequest` / `MoveSurfaceResponse` in `dor/src/commands/types.ts`; interaction and refusal rules follow `docs/specs/layout.md` → Moving Surfaces between Workspaces. + +**Must require exactly one destination: a Workspace argument or `--new`.** Never reserve the bare name `new`; `--workspace` scopes the source. CLI moves preserve focus unless `--focus` follows the pane; if the active source disappears, activate the destination in command mode. **Must refuse iframe moves unless `--dangerously-destroy-iframe-page-state` is explicit**, without a GUI prompt; that flag cannot bypass dirty/pending Tool refusals. Single-Workspace hosts refuse `surface.move` through `spansWorkspaces`. + +Source of truth: `moveCommand` in `dor/src/commands/move.ts`; `moveSurface` in `lib/src/components/wall/surface-move.ts`. Tests: `dor/test/move.test.mjs`, `lib/src/components/WorkspaceWindow.test.tsx`. + ## dor workspace **`dor workspace` mutates and `dor list` enumerates**, so the overview has one diff --git a/docs/specs/dor-tool.md b/docs/specs/dor-tool.md index 47f81491c..5ee399535 100644 --- a/docs/specs/dor-tool.md +++ b/docs/specs/dor-tool.md @@ -333,6 +333,8 @@ The Tool-specific local boundaries are `docs/specs/security-local.md` → Dor To **Must cold-restore an approved Tool by starting its saved command through integration-gated shell readiness**, then rediscover its port. Agent-resume commands do not override the saved Tool command. Pending approvals restore as ordinary terminals and execute nothing. **Must rebuild visible Tool metadata from its pane row when layout geometry is unusable**, rather than starting the command in a plain terminal with no serving behavior. +**Must refuse same-Window Surface moves while dirty or pending**, even with iframe-state consent; move interaction follows `docs/specs/layout.md` → Moving Surfaces between Workspaces. + **Must retain live Tool browser params and OSC announcements in volatile Workspace-transfer content**, applying them to the destination plan without mutating the durable record. A serving iframe Tool participates in the ordinary iframe move confirmation. **Must refuse transfer while a Tool awaits approval or its browser startup has no session binding.** **Must pause serving updates during Workspace closure or transfer**, and recheck that a Workspace remains available after asynchronous launch lookup. Approval completion must not launch into a closing or transferring Workspace. diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 4c82e93e5..11c4d9ac5 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -98,7 +98,7 @@ Popups share the zoomed pane’s app-background halo. **Must write visible action text in the Title, Dir, and Ports rows in lowercase**, since `iframe` and `agent-browser` cannot be capitalized; proper nouns such as Finder, tooltips, and accessible names keep their case. -**Must keep the Title and Dir rows on one line.** Title: the title truncates to 8 characters, the Surface ref drops to its copy icon (the tooltip keeps it), then the title truncates further. Dir: the explorer action drops its label before the directory truncates from its start, keeping its end. +**Must keep the title and its icon-only Explain bug on one line**, with the bug immediately beside the title. In Window hosts, wrap the copyable ref and Move to workspace picker together below the title; keep Dir on one line. Title: the title truncates to 8 characters, the Surface ref drops to its copy icon (the tooltip keeps it), then the title truncates further. Dir: the explorer action drops its label before the directory truncates from its start, keeping its end. **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. @@ -248,6 +248,19 @@ Source of truth: `deriveWorkspaceAutoName` in `lib/src/lib/workspace-autoname.ts Source of truth: `createWorkspaceMotion` in `lib/src/components/workspace-motion.ts`; `WorkspaceMotion` in `lib/src/components/WorkspaceMotion.tsx`; `closeAll` in `lib/src/components/Wall.tsx`. +### Moving Surfaces between Workspaces + +- **Must move a Pane or Door on release over another Workspace tab**, inserting beside the destination's last selected live pane. Never activate on hover; highlight valid targets. A header/ Door press owns a pane drag; a tab press owns reorder and cross-Window tear-out. Strip gaps, the source tab, and disabled targets consume the drop without a layout move or tear-out. Escape and pointer cancellation change nothing. +- **Must offer `+` and New workspace only when the source has more than one Surface**, counting Panes and Doors; create a receiving Wall with only the moved Surface. Disable the picker item and `+` drop otherwise, and refuse CLI `--new`. +- **Must offer Move to workspace beside the copyable Surface ref in terminal and Tool context**, using the same coordinator as dragging. **Never add a browser context menu. Never add a command-mode move binding.** Browser Surfaces move by dragging or CLI. +- **Must retain stable Surface identity and Session state while remounting in the destination Wall**: terminals keep their registry instance, browser automation reconnects, and a retained helper follows its source. Never close a departing Session. Pin a moved preview slot by removing its preview mark. +- **Must confirm plain iframe and serving iframe Tool moves before creating a destination or changing membership**, with a stable random character over the Window content area. Doors remain minimized while waiting. Show “moving this iframe will trigger a refresh and reopen at its saved URL, possibly losing page state or returning to an earlier page”; the prompted character confirms, anything else cancels. Saved URLs are last-known URLs, not necessarily the page's current location. CLI consent follows `docs/specs/dor-cli.md` → dor move. +- **Must refuse dirty Tools, pending Tool approval, browser startup, closing Surfaces/Workspaces and helper promotion**, rechecking after consent and asynchronous preparation. Dirty/pending refusals cannot be bypassed by iframe consent. +- **Must follow a GUI move into destination passthrough**, acknowledging without input; CLI focus policy follows `docs/specs/dor-cli.md` → dor move. Remove a source with no Panes or Doors; if Doors remain but no pane does, refill normally. +- **Must prepare before departure and roll back failed adoption**, restoring layout, Doors, parked state, selection, zoom, metadata and refs. Ref allocation belongs to `docs/specs/dor-cli.md` → Handle Model; coordinated durable publication belongs to `docs/specs/transport.md` → Persisted session types; Activity follows `docs/specs/alert.md` → Workspace union. + +Source of truth: `moveSurface` in `lib/src/components/wall/surface-move.ts`; `surfaceWorkspaceDrag` in `lib/src/components/wall/surface-workspace-drag.ts`; `MoveWorkspaceAction` in `lib/src/components/wall/MoveWorkspaceAction.tsx`; `prepareSurfaceMove` / `adoptSurfaceMove` in `lib/src/components/Wall.tsx`. Tests: `lib/src/components/WorkspaceWindow.test.tsx`, `lib/src/components/wall/LathHost.test.tsx`. + ### Workspace lifecycle Each Wall renders one Workspace's Content and Baseboard (doors). Standalone mounts one Wall **per Workspace**; VS Code and the website playground mount a bare Wall with no Workspace id, which behaves exactly as a single-Workspace Window (VS Code's per-webview mapping is `docs/specs/vscode.md`). diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index 67ac11085..6c7ac93cd 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -94,6 +94,8 @@ Mirrored workbench chords — the terminal still receives the key too; [vscode.m | `⌘⇧P` / `Ctrl+Shift+P`, or `F1` (unmodified) | `workbench.action.showCommands` | | `⌘B` / `Ctrl+B` | `workbench.action.toggleSidebarVisibility` | +Surface moves have no command-mode binding; see `docs/specs/layout.md` → Moving Surfaces between Workspaces. + The standalone host contributes no chords; `docs/specs/standalone.md` owns its native-menu contract. ## Implementation references diff --git a/docs/specs/terminal-context.md b/docs/specs/terminal-context.md index 8ac446629..0f8b7ced0 100644 --- a/docs/specs/terminal-context.md +++ b/docs/specs/terminal-context.md @@ -59,6 +59,8 @@ Source of truth: `context` in `standalone/sidecar/pty-core.js`; `terminalContext 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. `lib/src/stories/HelperPlacement.stories.tsx` checks rendered placement and real xterm input/focus retention; the gallery checks narrow controls and always-visible details. `visualSnapshot` in `lib/.storybook/preview.ts` suppresses scrollbar paint. +**Must expose the shared workspace picker only in Window hosts**, alongside the source ref; placement and behavior follow `docs/specs/layout.md` → Moving Surfaces between Workspaces. + ## Tool context **Must show a Tool's primary Session in Terminal Context instead of creating an auxiliary helper.** Reuse the title, directory, port, and alert presentation, showing Tool command status without helper Modify, Reset, or Promote controls; a preview slot's adds Keep open (`docs/specs/layout.md` → Pane header). Pending approval cannot open context. diff --git a/docs/specs/tiling-engine.md b/docs/specs/tiling-engine.md index 53481b247..8e2b8ac20 100644 --- a/docs/specs/tiling-engine.md +++ b/docs/specs/tiling-engine.md @@ -102,6 +102,8 @@ Adapter gesture (LathHost): Source of truth: `hitTest` in `lib/src/lib/lath/hit-test.ts`; `createDragController` in `lib/src/components/wall/lath-drag-controller.ts`; the drag callbacks in `lib/src/components/Wall.tsx`; presses in `lib/src/components/Door.tsx` / `lib/src/components/Baseboard.tsx`. +**Must delegate Workspace-strip pane drops to `docs/specs/layout.md` → Moving Surfaces between Workspaces** before local hit testing; cross-Wall adoption remounts the leaf. Source of truth: `createDragController` in `lib/src/components/wall/lath-drag-controller.ts`. + ## Restore tokens (Doors) `remove` returns a JSON-serializable `RestoreToken` of the leaf's ancestry: its own `leafId`; `siblingId` (the nearest same-parent sibling leaf it sat beside); `siblingLeafIds` / `siblingFingerprint` (that sibling *node's* leaf set and structure fingerprint, **written on every non-root removal and consulted only when the set holds more than one leaf**, i.e. when the sibling was itself a split subtree); `edge` (such that neighbor-tier restore is `split(siblingId, edge, leafId)`); its normalized `weight` and child `index`; and a structure-only `fingerprint` — kinds, dirs, leaf ids, no weights — of the parent split *post-removal*. **A root-leaf removal writes neither sibling field**, with `siblingId` and `fingerprint` null, so it can only reach the fallback tier. `restore` applies three tiers, driven from the Wall's `handleReattach`: @@ -154,7 +156,7 @@ The **store** is the state machine + geometry + enter hints, reached directly as **`lath-wall-engine.ts` is the Wall-facing handle**, holding only what the store does not — the animator and its `exitMs` / `markDying` / `isDying` / frame + wake signals, the vocabulary maps (Edge ↔ dor direction, arrow → direction), the meta builders (`terminalLeafMeta` / `browserLeafMeta` / `leafMetaFromPersistedDoor`), `shouldParkOnMinimize`, and the persistence conveniences `serializeLayout` / `seed` — and **no selection/focus/mode/activation state**. Two projection rules bind its readers: `listPanes()` is tree pre-order + meta, so **parked leaves are not visible and are not listed**, while `getMeta(id)` *does* resolve Doored leaves. -**All selection/focus/mode policy stays at the Wall** ([layout.md](layout.md)); three consequences bind editors here. Because nothing re-parents and nothing activates, a focus-neutral add reduces to a selection decision (`settleAddSelection`). The Cmd-Arrow swap is one `store.swapLeaves` call with **no** companion title swap — meta and registry entries follow ids. And **embed self-focus adoption rides `focusin`** (acceptance row 8), which LathHost surfaces as `onLeafFocused(id)` for the Wall to adopt like a click, there being no activation event to piggyback on. Spatial nav reaches `store.neighborOf` through the `WallNav` seam in `lib/src/components/wall/keyboard/types.ts`. +**All selection/focus/mode policy stays at the Wall** ([layout.md](layout.md)); three consequences bind editors here. Within one Wall, nothing re-parents or activates, so a focus-neutral add reduces to a selection decision (`settleAddSelection`). The Cmd-Arrow swap is one `store.swapLeaves` call with **no** companion title swap — meta and registry entries follow ids. And **embed self-focus adoption rides `focusin`** (acceptance row 8), which LathHost surfaces as `onLeafFocused(id)` for the Wall to adopt like a click, there being no activation event to piggyback on. Spatial nav reaches `store.neighborOf` through the `WallNav` seam in `lib/src/components/wall/keyboard/types.ts`. Source of truth: `lib/src/components/wall/lath-wall-store.ts`; `lib/src/components/wall/lath-wall-engine.ts`. @@ -162,7 +164,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, 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 `