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 alarm status (from the activity monitor) and an optional TODO flag.
A Session can be 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 alarm and TODO indicators, and looks like a mouse hole cut into the baseboard.
Transitioning between Pane and Door does not alter the Session in any way. Detaching a pane creates a door; reattaching a door creates a pane. The terminal content, scrollback, and process state are preserved across transitions.
There are two areas:
- Content — tiling layout containing Panes, powered by dockview
- Baseboard — always-visible bottom strip containing Doors and shortcut hints
The user can navigate between all elements using the mouse, or by entering command mode and using the keyboard.
Pond
├── Context providers (Mode, SelectedId, PondActions, PanelElements, DoorElements, RenamingId, Zoomed)
│ └── div (h-screen, flex col)
│ ├── Dockview wrapper (flex-1, 6px padding around edges)
│ │ ├── DockviewReact (tiling layout engine, singleTabMode="fullwidth")
│ │ │ └── Groups (one session per group, no tab stacking)
│ │ │ ├── TerminalPanel → TerminalPane → xterm.js
│ │ │ └── TerminalPaneHeader (tab component, drag handle)
│ │ └── SelectionOverlay (fixed positioned, pointer-events: none)
│ ├── Baseboard (always-visible bottom strip, shortcut hints when empty)
│ │ └── Door components (one per detached session)
│ └── KillConfirmOverlay (conditional)
- Spatial arrangement of groups in a grid
- Resize sashes between groups
- Drag-and-drop rearrangement via pane headers
- Group sizing and positioning
- Focus and selection state (
selectedId,selectedType) - Passthrough/command mode system
- Keyboard shortcuts and selection overlay rendering
- Session lifecycle: detach (pane → door), reattach (door → pane), kill
- Terminal lifecycle (via terminal-registry)
- Activity monitoring and alarm state
- TODO state management
- Session persistence (save/restore across restarts)
The content area is a tiling layout of panes, powered by dockview. Each pane occupies its own group (no tab stacking). Panes are separated by a 6px gap. DockviewReact uses singleTabMode="fullwidth" so tabs stretch to fill the header.
One session per group. Dockview supports multiple panels per group (tabs), but we enforce one-panel-per-group to behave like a tiling window manager.
No tab stacking. Prevented via:
onWillShowOverlay:event.kind === 'tab'→ blockedgroup.model.onWillDrop:event.position === 'center'→ intercepted and converted to a swap- All other positions and kinds are allowed — these create splits
Center drop = swap. Dropping a pane onto the center of another swaps their session content (same as Cmd+Arrow). The overlay is allowed so the user sees a valid drop target, but group.model.onWillDrop intercepts it, calls swapTerminals() + swaps titles, then preventDefault() to block the merge.
Each pane has a 30px header that doubles as a drag handle. The header uses cursor-grab / active:cursor-grabbing and select-none. Background uses --color-tab-* theme tokens (adapts to VSCode host theme). Dockview's default close button and right-actions container are hidden via CSS.
Elements from left to right:
- Session name (click to rename, truncates with ellipsis)
- Alarm bell button (reflects session activity status)
- TODO pill (if todo state is set; hidden in minimal tier)
- Flexible gap
- SplitHorizontalIcon
split horizontal ["](full tier only) - SplitVerticalIcon
split vertical [%](full tier only) - ArrowsOutIcon / ArrowsInIcon
zoom / unzoom [z](full tier only) - ArrowLineDownIcon
detach [d] - XIcon
kill [x](hover turns error-red)
The alarm bell and TODO pill are defined in docs/specs/alarm.md (visual states, interaction, context menu, and hardening).
The header adapts to available width via ResizeObserver in three tiers:
- Full (>280px): all controls visible — alarm, TODO, split, zoom, detach, kill
- Compact (160–280px): SplitH/SplitV/Zoom hidden; alarm, TODO, detach, kill visible
- Minimal (<160px): SplitH/SplitV/Zoom and TODO pill hidden; alarm, detach, kill visible. Session name truncates with ellipsis as needed.
Below the content area is the baseboard (h-8, 32px). It is always visible — a thin strip when empty, showing keyboard shortcut hints when there are no doors and the container is wider than 350px (currently: LCmd → RCmd to enter command mode).
When a session is detached, it becomes a door on the baseboard. The door displays the session's title, a TODO badge (if set), and an alarm bell icon with activity dot. It uses the bottom edge of the window as its bottom border, with left, top, and right borders with rounded-t-md — resembling a mouse hole. Door dimensions: min-w-[68px] max-w-[220px] h-6.
- Clicking a door (in any mode): restores the session into the content area as a pane and enters passthrough mode. The terminal gets focus immediately.
- Enter on a door (command mode): same as clicking — restores and enters passthrough mode.
- d on a door (command mode): restores the session into a pane but stays in command mode. This is the inverse of pressing
don a pane (which detaches it), makingda toggle. - x on a door (command mode): restores the session into a pane, then immediately shows the kill confirmation.
- Arrow keys can navigate to doors from panes (see Navigation).
Doors are measured in a hidden off-screen container first:
- If they all fit, display them all. If there is remaining space, show the keyboard shortcut hint.
- If they do not all fit:
- Reserve space for a
N more →button on the right edge - Add doors until no more fit
- If scrolled, show
← N moreon the left and/orN more →on the right - Assume single-digit overflow counts
- Reserve space for a
Clicking an overflow arrow reveals one door in that direction. A longer title may push more doors off the opposite side.
Extreme case: a single door with a very long title, with more doors on both sides. Show both arrows with counts, and the single door with as much title as fits (ellipsis for the rest).
- All keyboard input routes to the active session's xterm.js instance
- Only the mode-exit gesture (LCmd → RCmd) is intercepted
- Selection overlay shows 2px solid border with glow
- Terminal has DOM focus
- Keyboard drives navigation and commands (see Shortcuts)
- Session does not receive keyboard input
- Selection overlay shows animated marching-ants SVG border (2px stroke, dashed pattern animated at 0.4s cycle)
Enter passthrough mode:
- Click any pane body or header
- Press
Enteron a selected pane in command mode - Click or press
Enteron a door (restores session first) - Focus is deferred via
requestAnimationFrameto prevent dockview from stealing it
Enter command mode:
- Left Cmd keydown, then Right Cmd keydown within 500ms
- Detected via capture-phase
keydownlistener one.key === 'Meta'ande.location(1 = left, 2 = right) - Works even when xterm has DOM focus because listener uses capture phase
All handled in a single capture-phase keydown listener on window. Every handled key calls preventDefault() + stopPropagation(). While a rename input is active, all shortcuts are bypassed.
| Key | On pane | On door |
|---|---|---|
" |
Horizontal split — new pane to the right | — |
% |
Vertical split — new pane below | — |
| Arrow keys | Spatial navigation between panes | Left/Right between doors, Up to panes |
Cmd+Arrow |
Swap session content with neighbor | — |
Enter |
Enter passthrough mode | Restore session + enter passthrough |
, |
Inline rename | — |
x |
Kill with confirmation | Restore session + kill confirmation |
d |
Detach to door | Restore session (stay in command) |
z |
Toggle maximize/restore | — |
t |
Toggle TODO flag (none/soft → hard → none) | — |
a |
Dismiss or toggle alarm | — |
Pressing x shows a pane-centered semi-transparent overlay (KillConfirmOverlay → KillConfirmCard) with a random uppercase letter (A-Z, excluding X). Typing that letter confirms the kill (destroys session, removes pane). Escape cancels.
A fixed-positioned element rendered on top of dockview. Covers the active element's area inflated by 3px (half the 6px gap) for panes, or 2px for doors.
- A pane or door can be active or inactive. Only one element is active at a time.
- Passthrough:
border: 2px solid ${color}+box-shadow: 0 0 15px color-mix(in srgb, ${color} 30%, transparent) - Command: animated SVG marching-ants border — rounded rectangle path with
stroke-dasharrayanimation (10px segment, 60% dash / 40% gap, 0.4s cycle, 2px stroke) - Border radius:
0.5remfor panes,0.375rem 0.375rem 0 0for doors - Color from CSS custom property
--mt-selection-terminal z-index: 50,pointer-events: none,transition: 150ms
- Each
TerminalPanelregisters its DOM element in apanelElementsMap on mount, removes on unmount - Door elements are registered by the
BaseboardviaDoorElementsContext(queries[data-door-id]attributes) - Updates on: selection change, resize (
ResizeObserver), layout change (api.onDidLayoutChange)
Uses DOM positions of pane elements (registered in panelElements Map). For each candidate:
- Edge-based direction check: candidate must be entirely in the correct direction on the primary axis
- Overlap requirement: candidate must overlap on the secondary axis
- Distance: edge-to-edge on the primary axis
- Fallback: if no overlapping candidate, nearest non-overlapping candidate
A breadcrumb tracks the last navigation direction and origin pane. Pressing the opposite direction returns to the origin instead of spatial lookup. This handles asymmetric layouts (tall pane left, stacked panes right).
Down from the bottom-most pane navigates to the first door in the baseboard. Up from a door navigates to the last pane. Left/Right navigates between doors.
Swaps session content between two panes — the layout shape is unchanged. Uses swapTerminals() from terminal-registry which swaps registry entries and reattaches DOM elements to each other's containers. Also swaps dockview panel titles. Selection follows the moved session. Uses the same back-navigation breadcrumb as arrow keys.
- Capture restore context before removing:
neighborIdanddirection: spatial position relative to nearest neighborremainingPanelIds: sorted IDs of panes that stayrestoreLayout: full layout snapshotdetachedLayoutSignature: structural fingerprint (ignores sizes)
- Remove pane from dockview (
api.removePanel) - Add to
detachedstate → door appears in baseboard - Session stays alive in registry (not destroyed)
- Selection moves to the new door (stays in command mode)
Three strategies based on layout state:
Exact restore (layout structure signature matches AND same panes exist):
- Deserialize the saved layout snapshot with
reuseExistingPanels: true - Preserves exact split ratios from before detach
Neighbor restore (neighbor still exists AND pane set matches remainingPanelIds):
addPanelwithposition: { referencePanel: neighborId, direction }- Restores original position relative to neighbor
Aspect-ratio split (layout changed):
- Split the currently selected pane
- Direction: wider than tall → split right, otherwise split below
Triggered by pressing , in command mode or clicking the session name in the pane header.
The name <span> is replaced by an <input> with:
- Same font (
font-mono font-medium),bg-transparent, no border - Text pre-selected on mount
Enterconfirms,Escapecancels,blurconfirmsstopPropagationonmousedown/click/keydownto prevent panel click or drag- All command-mode shortcuts are bypassed while renaming
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.
- Create:
getOrCreateTerminalspawns xterm.js + FitAddon + PTY, returns existing if already created - Reconnect:
reconnectTerminalcreates xterm entry and writes replay data without spawning a new PTY (used after webview recreation when platform preserves live PTYs) - Restore:
restoreTerminalcreates 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:
destroyTerminalkills PTY, disposes xterm, removes from registry. Only called on explicit kill (x). - Swap:
swapTerminalsswaps two registry entries and reattaches DOM elements to each other's containers
Layout, scrollback, cwd, detached items, and alarm 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:
- Live PTYs (webview hidden/shown): request PTY list + replay data from platform,
reconnectTerminal()for each (500ms timeout) - Saved session (app restart): restore layout from serialized dockview state,
restoreTerminal()for each pane with saved cwd + scrollback - Empty state: create a single new pane
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.
Custom mousetermTheme extends dockview's themeAbyss:
gap: 6— 6px between groups in both directionsdndOverlayMounting: 'absolute',dndPanelOverlay: 'group'- Pane header height:
--dv-tabs-and-actions-container-height: 30px - 6px padding around the dockview area (
p-1.5on wrapper,inset-1.5on container)
Colors use a two-layer CSS variable strategy: @theme --color-* tokens → var(--vscode-*, <fallback>). In VSCode, host theme variables take precedence. In standalone mode, fallback values apply with prefers-color-scheme: light overrides. Tailwind v4 @theme block registers --color-* tokens as Tailwind colors (e.g., bg-surface, text-foreground, border-border). See theme.css for the full token map.
Dockview's separator borders, sash handles, and groupview borders are all set to transparent/none — the 6px gap is the only visual separator between panes. All dockview container backgrounds are flattened to var(--color-surface).
- Dual React instance: dockview bundles its own React. Fixed with
resolve.dedupe: ['react', 'react-dom']in Vite config. - White screen on boot:
DockviewReactneeds pixel dimensions. Fixed with relative wrapper + absolute inner container. - Theme as prop: dockview v5 uses
theme={themeObject}prop, not a CSS class. - xterm steals Meta keys: mode-exit gesture uses
capture: trueon the window keydown listener. - Click doesn't focus terminal: focus deferred to
requestAnimationFrameto prevent dockview from stealing it. - Stale hitboxes after DnD: each
TerminalPanelregisters its own DOM element in a Map for overlay/navigation. - Asymmetric back-navigation: breadcrumb tracks last direction + origin for opposite-direction return.
- Center drop merges panels: intercepted at group-level
model.onWillDropand converted to a swap. - Group drag has null panelId: falls back to
api.getGroup(groupId).activePanel.id. - Auto-spawn on empty:
onDidRemovePanelcreates a new session when the last pane is removed and no doors exist.
| File | Role |
|---|---|
lib/src/components/Pond.tsx |
Main layout orchestrator: modes, keyboard, selection overlay, detach/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 alarm/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/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 → alarm escalation |
lib/src/lib/alarm-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-save.ts |
Serialization: collects layout, scrollback, cwd, alarm 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 |
lib/src/lib/resume-patterns.ts |
Detects resumable commands (claude --resume, etc.) in scrollback |
lib/src/index.css |
Dockview theme overrides — separator/sash/border removal, background flattening |
lib/src/theme.css |
Two-layer VSCode theme token system (@theme --color-* → --vscode-*) and Tailwind v4 @theme integration |