Skip to content
Merged
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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pnpm build # build lib, vscode extension, and website

The primary job of a spec is to be an accurate reference for the current state of the code. Read the relevant spec before modifying a feature it covers — the spec describes invariants, edge cases, and design decisions that are not obvious from the code alone.

- **`docs/specs/ontology.md`** — Canonical vocabulary for Session states, layers (Process / Registry / View / Link / Activity / Snapshot), transition verbs, and the Liskov contract on Registry APIs. Read this first. Other specs defer to it when naming a state or a verb.
- **`docs/specs/layout.md`** — Tiling layout, pane/door containers, dockview configuration, modes (passthrough/command), keyboard shortcuts, selection overlay, spatial navigation, minimize/reattach, inline rename, session lifecycle, session persistence, and theming. Read this when touching: `Pond.tsx`, `Baseboard.tsx`, `Door.tsx`, `TerminalPane.tsx`, `spatial-nav.ts`, `layout-snapshot.ts`, `terminal-registry.ts`, `session-save.ts`, `session-restore.ts`, `reconnect.ts`, `index.css`, `theme.css`, or any keyboard/navigation/mode behavior.
- **`docs/specs/alert.md`** — Activity monitoring state machine, alert trigger/clearing rules, attention model, TODO lifecycle (soft/hard), bell button visual states and interaction, door alert indicators, and hardening (a11y, motion, i18n, overflow). Read this when touching: `activity-monitor.ts`, `alert-manager.ts`, the alert bell or TODO pill in `Pond.tsx` (TerminalPaneHeader), alert indicators in `Door.tsx`, or the `a`/`t` keyboard shortcuts. Layout.md defers to this spec for all alert/TODO behavior.
- **`docs/specs/vscode.md`** — VS Code extension architecture: hosting modes (WebviewView + WebviewPanel), PTY lifecycle and buffering, message protocol between webview and extension host, session persistence flow, reconnection protocol, theme integration, CSP, build pipeline, and invariants (save-before-kill ordering, PTY ownership, alert state merging). Read this when touching: `extension.ts`, `webview-view-provider.ts`, `message-router.ts`, `message-types.ts`, `pty-manager.ts`, `pty-host.js`, `session-state.ts`, `webview-html.ts`, `vscode-adapter.ts`, or `pty-core.js`.
Expand Down
52 changes: 27 additions & 25 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Layout Spec

> See `docs/specs/ontology.md` for canonical state names, layer definitions, and transition verbs. This spec uses the ontology's vocabulary throughout.

## Conceptual model

A **Session** is a single PTY instance — a running shell process with its scrollback, environment, and working directory. Sessions are managed by the terminal registry and persist independently of how they are displayed. Each session also carries UI state: an alert status (from the activity monitor) and an optional TODO flag.
A **Session** is a single PTY instance — a running shell process with its scrollback, environment, and working directory. Sessions are managed by the terminal registry and persist independently of how they are displayed. Each session also carries Activity state (alert status from the activity monitor, optional TODO flag).

A Session can be in one of two containers:
A Session's **View** state places it in one of two containers:

- **Pane** — a visible container in the content area. The session's terminal output is rendered via xterm.js. The pane has a header with controls and acts as the drag handle for layout rearrangement.
- **Door** — a minimized container in the baseboard. The session is still alive (PTY running, output buffered) but not visible. The door shows the session's title plus alert and TODO indicators, and looks like a mouse hole cut into the baseboard.
Expand Down Expand Up @@ -211,24 +213,24 @@ Swaps session **content** between two panes — the layout shape is unchanged. U
## Minimize and reattach

### Minimize (`m` key or minimize header button)
1. Capture restore context before removing:
1. Capture reattach context before removing:
- `neighborId` and `direction`: spatial position relative to nearest neighbor
- `remainingPanelIds`: sorted IDs of panes that stay
- `restoreLayout`: full layout snapshot
- `detachedLayoutSignature`: structural fingerprint (ignores sizes)
- `remainingPaneIds`: sorted IDs of panes that stay
- `layoutAtMinimize`: full layout snapshot
- `layoutAtMinimizeSignature`: structural fingerprint (ignores sizes)
2. Remove pane from dockview (`api.removePanel`)
3. Add to `detached` state → door appears in baseboard
4. Session stays alive in registry (not destroyed)
3. Add to `doors` state → door appears in baseboard
4. Session stays in registry (not disposed)
5. Selection moves to the new door (stays in command mode)

### Reattach (click door, Enter/d on door)
Three strategies based on layout state:

**Exact restore** (layout structure signature matches AND same panes exist):
**Exact reattach** (layout structure signature matches AND same panes exist):
- Deserialize the saved layout snapshot with `reuseExistingPanels: true`
- Preserves exact split ratios from before minimize

**Neighbor restore** (neighbor still exists AND pane set matches `remainingPanelIds`):
**Neighbor reattach** (neighbor still exists AND pane set matches `remainingPaneIds`):
- `addPanel` with `position: { referencePanel: neighborId, direction }`
- Restores original position relative to neighbor

Expand All @@ -249,28 +251,28 @@ The name `<span>` is replaced by an `<input>` with:

## Session lifecycle and terminal registry

Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on mount and `detachTerminal(id)` on unmount. The session (xterm.js instance, PTY, DOM element) persists in the registry across mount/unmount cycles.
Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on React mount and `unmountElement(id)` on React unmount. The session (xterm.js instance, PTY, DOM element) persists in the registry across mount/unmount cycles — the DOM element is detached from its container but the Registry entry stays `Mounted`.

- **Create**: `getOrCreateTerminal` spawns xterm.js + FitAddon + PTY, returns existing if already created
- **Reconnect**: `reconnectTerminal` creates xterm entry and writes replay data without spawning a new PTY (used after webview recreation when platform preserves live PTYs)
- **Restore**: `restoreTerminal` creates xterm entry and spawns new PTY with saved cwd and scrollback (used on app restart from saved session)
- **Attach/detach**: moves the persistent DOM element in/out of a container — no session state loss
- **Destroy**: `destroyTerminal` kills PTY, disposes xterm, removes from registry. Only called on explicit kill (`x`).
- **Swap**: `swapTerminals` swaps two registry entries and reattaches DOM elements to each other's containers
- **Resume**: `resumeTerminal` creates xterm entry and writes replay data without spawning a new PTY. Used when the webview is recreated while the host retains Live PTYs (Link: Severed → Resuming → Live).
- **Restore**: `restoreTerminal` creates xterm entry and spawns a new PTY with saved cwd and scrollback. Used on cold start from a saved Snapshot (Link: Cold → Live).
- **mount / unmount (DOM)**: `mountElement` reparents the persistent DOM element into a container; `unmountElement` removes it. The Registry entry survives.
- **Dispose**: `disposeSession` kills the PTY, disposes xterm, removes the registry entry. Only called on explicit kill (`x`).
- **Swap**: `swapTerminals` swaps two registry entries and reattaches DOM elements to each other's containers.

### Session persistence

Layout, scrollback, cwd, minimized items, and alert state are saved to persistent storage via a debounced save (500ms). Saves are triggered by layout changes, panel add/remove, and a 30s periodic interval. Saves are flushed immediately on PTY exit, `pagehide`, and extension shutdown requests.

On startup, recovery is priority-based:
1. **Live PTYs** (webview hidden/shown): request PTY list + replay data from platform, `reconnectTerminal()` for each (500ms timeout). If the saved session covers every live PTY, restore the saved dockview layout when its visible panel set matches and restore saved minimized items as doors.
2. **Saved session** (app restart): restore layout from serialized dockview state, `restoreTerminal()` for each pane with saved cwd + scrollback, and spawn each PTY with the current default shell selection
1. **Resume** (webview hidden/shown, live PTYs): request PTY list + replay data from platform, `resumeTerminal()` for each (500ms timeout). If the saved session covers every live PTY, restore the saved dockview layout when its visible panel set matches and reattach saved minimized items as doors. This still counts as a live resume when every live session is minimized, so recovery must not fall through to cold restore just because the visible `paneIds` list is empty.
2. **Restore** (app restart, cold start): restore layout from serialized dockview state, `restoreTerminal()` for each pane with saved cwd + scrollback, and spawn each PTY with the current default shell selection
3. **Fallback/manual pane creation**: when no saved layout can be safely applied, add multiple panes as splits from the previous pane rather than tabs
4. **Empty state**: create a single new pane

### Session UI state
### Activity state

Each session carries `SessionUiState` with `status: SessionStatus` and `todo: TodoState`. These are synced to React via `useSyncExternalStore`. State that arrives from the platform before a terminal entry is created (reconnect scenario) is held as "primed state" and applied when the terminal is finally created.
Each session carries `ActivityState` with `status: SessionStatus` and `todo: TodoState`. These are synced to React via `useSyncExternalStore`. State that arrives from the platform before a registry entry exists (resume scenario) is held as "primed state" and applied when the registry entry is created.

## Theme

Expand Down Expand Up @@ -304,7 +306,7 @@ The direction is carried via `FreshlySpawnedContext` — a `Map<paneId, SpawnDir

1. Add `.pane-fading-out` (or `.pane-fading-and-shrinking-to-br` for a last-pane kill) to the killed pane's group element. Block pointer events during the fade.
2. On `animationend`, snapshot `getBoundingClientRect` for every surviving panel's group element.
3. `destroyTerminal` + `api.removePanel`; dockview snaps the layout.
3. `disposeSession` + `api.removePanel`; dockview snaps the layout.
4. Measure post-rects. Any panel whose rect grew is a "grower."
5. For each grower, apply an inline `clip-path: inset(...)` with the newly-claimed territory clipped off, force a reflow, then transition to `inset(0)`. This reveals the grower into the vacated space without affecting `getBoundingClientRect`. Clears on `transitionend`.

Expand Down Expand Up @@ -337,13 +339,13 @@ The deferred spawn also only calls `selectPanel` if selection is null. The kill
| `lib/src/components/Pond.tsx` | Main layout orchestrator: modes, keyboard, selection overlay, minimize/reattach. Also defines `TerminalPanel`, `TerminalPaneHeader`, `KillConfirmOverlay` |
| `lib/src/components/Baseboard.tsx` | Always-visible bottom strip with door components, overflow arrows, and shortcut hints |
| `lib/src/components/Door.tsx` | Individual door element — mouse-hole styled button with alert/TODO indicators |
| `lib/src/components/TerminalPane.tsx` | Thin xterm.js mount point — attaches/detaches persistent session elements |
| `lib/src/lib/terminal-registry.ts` | Session lifecycle: create, reconnect, restore, attach, detach, destroy, swap, focus, refit. Session UI state store |
| `lib/src/lib/spatial-nav.ts` | Spatial navigation (`findPanelInDirection`) and restore-neighbor detection (`findRestoreNeighbor`) |
| `lib/src/components/TerminalPane.tsx` | Thin xterm.js mount point — mounts/unmounts persistent session elements |
| `lib/src/lib/terminal-registry.ts` | Session lifecycle: create, resume, restore, mount, unmount, dispose, swap, focus, refit. Activity state store |
| `lib/src/lib/spatial-nav.ts` | Spatial navigation (`findPanelInDirection`) and reattach-neighbor detection (`findReattachNeighbor`) |
| `lib/src/lib/layout-snapshot.ts` | Layout cloning (`cloneLayout`) and structural signature (`getLayoutStructureSignature`) for restore comparison |
| `lib/src/lib/activity-monitor.ts` | Per-session activity state machine: output timing → alert escalation |
| `lib/src/lib/alert-manager.ts` | Manages ActivityMonitors + attention tracking + TODO state per session |
| `lib/src/lib/session-types.ts` | Type definitions for persisted sessions (`PersistedPane`, `PersistedDetachedItem`, `PersistedSession`) |
| `lib/src/lib/session-types.ts` | Type definitions for persisted sessions (`PersistedPane`, `PersistedDoor`, `PersistedSession`) |
| `lib/src/lib/session-save.ts` | Serialization: collects layout, scrollback, cwd, alert state for persistence |
| `lib/src/lib/session-restore.ts` | Deserialization: loads saved session, calls `restoreTerminal()` for each pane |
| `lib/src/lib/reconnect.ts` | Priority-based recovery: live PTYs first, then saved session, then empty |
Expand Down
Loading
Loading