Skip to content

Commit 4223352

Browse files
authored
Add untouched shell state to skip kill confirmation (#61)
2 parents 286545d + 13a921b commit 4223352

28 files changed

Lines changed: 704 additions & 58 deletions

docs/specs/layout.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,8 @@ All handled in a single capture-phase `keydown` listener on `window`. Every hand
183183

184184
Pressing `x` (or clicking the kill button) enters command mode and 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). Cancel with Escape key, clicking the `[ESC] to cancel` button, or clicking another panel. Any other key triggers a shake animation (400ms `shake-x` keyframe) then auto-dismisses the confirmation.
185185

186+
Untouched sessions skip this confirmation. A newly spawned shell starts `untouched: true`; the first user-originated PTY input flips it to false. Inputs that count include printable keys, Enter, control keys, keyboard CSI such as arrows/history, paste, and file-drop path insertion. Replay-time terminal reports, synthetic terminal reports, and stripped mouse-report-only input do not count. Killing an untouched pane runs the normal kill animation/dispose path immediately. Killing an untouched door first reattaches it only far enough to reuse the same pane removal path, then kills it without showing the confirmation overlay.
187+
186188
## Selection overlay
187189

188190
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.
@@ -270,14 +272,16 @@ Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on Reac
270272
- **Create**: `getOrCreateTerminal` spawns xterm.js + FitAddon + PTY, returns existing if already created
271273
- **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).
272274
- **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).
275+
- **Untouched**: new `getOrCreateTerminal` sessions start untouched. `isUntouched(id)` exposes the flag, and user-originated PTY input clears it via the registry input paths. Resume/restore seed the persisted flag; missing legacy snapshot data defaults to touched (`false`) so close confirmation remains conservative.
276+
- **Shell selection replacement**: the standalone shell dropdown and VS Code shell picker send `mouseterm:new-terminal` with `replaceUntouched` when the selected shell type changes. `Wall` always creates a new session id for that request. If the currently selected pane or door is untouched, the new terminal is inserted in the same dockview position (`direction: 'within'`; doors first reattach through the normal restore path), the old untouched session is disposed, and the old panel is removed without kill confirmation. If the selected terminal is touched or no terminal is selected, the request spawns a new pane near the active panel. Announced shell-selection spawns show a transient pane-anchored notice such as `Switched to zsh` or `Opened bash`.
273277
- During resume/restore replay, xterm.js may emit terminal-generated replies for OSC/CSI/DCS queries that were embedded in saved output. The registry drops those replay-time replies before they reach the new shell. This filter is limited to query/focus reports, and must not swallow user keyboard escape sequences such as arrows, function keys, or bracketed paste.
274278
- **mount / unmount (DOM)**: `mountElement` reparents the persistent DOM element into a container; `unmountElement` removes it. The Registry entry survives.
275279
- **Dispose**: `disposeSession` kills the PTY, disposes xterm, removes the registry entry. Only called on explicit kill (`x`).
276280
- **Swap**: `swapTerminals` swaps two registry entries and reattaches DOM elements to each other's containers.
277281

278282
### Session persistence
279283

280-
Layout, scrollback, cwd, minimized items, user-pinned titles, and alert state are saved to persistent storage via a debounced save (500ms). Derived command/app labels shown on minimized doors are display-only and are not persisted as user-pinned titles. 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.
284+
Layout, scrollback, cwd, minimized items, user-pinned titles, untouched state, and alert state are saved to persistent storage via a debounced save (500ms). Derived command/app labels shown on minimized doors are display-only and are not persisted as user-pinned titles. 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.
281285

282286
Saved snapshots are read through `readPersistedSession()`, which accepts the canonical object shape and defensively parses a JSON-stringified blob before validation and migration. This keeps malformed storage inert while covering hosts that hand back serialized JSON instead of the parsed object.
283287

@@ -319,6 +323,8 @@ When a pane is added, its dockview group element gets a directional `.pane-spawn
319323

320324
The direction is carried via `FreshlySpawnedContext` — a `Map<paneId, SpawnDirection>` written by the spawn call site and consumed once by `TerminalPanel`'s `useLayoutEffect` on first mount.
321325

326+
Shell-selection replacement uses the same pane add/remove primitives but also shows a short fixed-position notice over the resulting pane. The notice fades in/out over 1500ms via `.shell-spawn-notice` and is suppressed to a static render for reduced-motion users.
327+
322328
### Kill (in-place fade + FLIP reclaim)
323329

324330
`orchestrateKill(api, killedId)` in `lib/src/lib/kill-animation.ts` runs on kill confirmation. `Wall.tsx` owns the command dispatch and calls it after the user confirms. It fades the real pane element in place (its content dissolves against the same-colored background), then removes the panel and FLIP-reveals the survivors:

docs/specs/transport.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ Message types live in `vscode-ext/src/message-types.ts` (the canonical schema; o
104104
| `pty:cwd` | CWD query response (matched by requestId) |
105105
| `pty:scrollback` | Scrollback query response (matched by requestId) |
106106
| `pty:shells` | Available shells list response (matched by requestId) |
107+
| `mouseterm:newTerminal` | Host/UI request to spawn a terminal. Payload may include `shell`, `args`, display `name`, `replaceUntouched`, and `announce`; the webview replaces the selected untouched terminal in-place only when `replaceUntouched` is true, otherwise it spawns a new pane. |
108+
| `mouseterm:selectedShell` | Update the webview's default shell options for later split/spawn/restore paths. |
107109
| `mouseterm:flushSessionSave` | Request webview to save state now (host shutdown trigger, matched by requestId) |
108110
| `mouseterm:openThemeDebugger` | Command-triggered request to open the shared theme debugger dialog |
109111
| `alert:state` | Alert state change (projected status, todo, notification, attentionDismissedRing) |
@@ -126,6 +128,7 @@ interface PersistedPane {
126128
title: string;
127129
scrollback: string | null;
128130
resumeCommand: string | null;
131+
untouched: boolean;
129132
alert?: PersistedAlertState | null;
130133
}
131134

@@ -155,5 +158,6 @@ These rules apply to every adapter. Adapter-specific layering (deactivate orderi
155158
- **Shell login args are shell-specific.** The shared `pty-core.js` launches POSIX shells with `-l` only for shells that accept it. `csh`/`tcsh` must be spawned without `-l` so users whose login shell is C-shell-derived can open a usable terminal in any adapter.
156159
- **Scrollback trailing newline.** Restored scrollback must end with `\n` to avoid zsh printing a `%` artifact at the top of the terminal.
157160
- **Replay drops terminal replies only.** While saved output is being replayed into xterm.js, terminal-generated OSC/CSI/DCS query and focus reports are dropped so they do not enter the resumed/restored shell's input buffer. The replay filter must preserve user keyboard escape sequences, including arrows, function keys, and bracketed paste.
161+
- **Untouched defaults conservatively.** New saved panes include `untouched`. Older saved panes without the field are read as `untouched: false`, so legacy sessions still require kill confirmation.
158162
- **PTY ownership.** Each message router tracks the PTY ids it owns. A PTY routed to one webview must not be stolen by another router; new routers attaching to a host must respect existing ownership.
159163
- **Replay filtering does not re-fire alerts.** `pty:replay` re-injects buffered output into xterm.js but must not re-trigger `AlertManager`, activity-monitor events, or protocol notifications.

docs/specs/vscode.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,20 @@ Universal PTY/transport invariants live in `docs/specs/transport.md`. The rules
8585
{ "command": "mouseterm.focus", "title": "MouseTerm: Focus",
8686
"icon": { "light": "icon-tiny-light.png", "dark": "icon-tiny-dark.png" } },
8787
{ "command": "mouseterm.open", "title": "MouseTerm: Open in Editor" },
88-
{ "command": "mouseterm.debugTheme", "title": "MouseTerm: Debug Theme" }
88+
{ "command": "mouseterm.debugTheme", "title": "MouseTerm: Debug Theme" },
89+
{ "command": "mouseterm.newTerminal", "title": "MouseTerm: New Terminal",
90+
"icon": "$(add)" },
91+
{ "command": "mouseterm.selectShell", "title": "MouseTerm: Select Shell",
92+
"icon": "$(gear)" }
8993
],
94+
"menus": {
95+
"view/title": [
96+
{ "command": "mouseterm.selectShell", "group": "navigation@1",
97+
"when": "view == mouseterm.view" },
98+
{ "command": "mouseterm.newTerminal", "group": "navigation@2",
99+
"when": "view == mouseterm.view" }
100+
]
101+
},
90102
"viewsContainers": {
91103
"panel": [
92104
{ "id": "mouseterm-panel", "title": "MouseTerm", "icon": "$(terminal)" }
@@ -127,6 +139,12 @@ VS Code-specific consequences:
127139

128140
PTY lifecycle, buffering, the reconnection sequence, and the full message protocol live in `docs/specs/transport.md`.
129141

142+
### Shell selection
143+
144+
The VS Code view title contributes `MouseTerm: Select Shell` and `MouseTerm: New Terminal`. The selected shell name is mirrored into the `WebviewView.description`, and `mouseterm:selectedShell` keeps the webview's default-shell slot current for split/spawn/restore paths.
145+
146+
`mouseterm.newTerminal` focuses the MouseTerm view and posts `mouseterm:newTerminal` with the currently selected shell. `mouseterm.selectShell` opens a QuickPick, saves the shell path globally or per workspace, applies the description/default-shell update, and, when the picked shell differs from the previous selection, focuses the view and posts `mouseterm:newTerminal` with `replaceUntouched: true` and `announce: true`. The shared `Wall` logic then replaces only a selected untouched terminal in-place; touched terminals cause an additional pane to be spawned instead.
147+
130148
### Serialization and restore
131149

132150
`WebviewPanelSerializer` is registered so VS Code can restore editor panels after restart:

0 commit comments

Comments
 (0)