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
36 changes: 18 additions & 18 deletions docs/specs/mouse-and-clipboard.md

Large diffs are not rendered by default.

38 changes: 19 additions & 19 deletions docs/specs/notepad.md

Large diffs are not rendered by default.

23 changes: 13 additions & 10 deletions docs/specs/notepad.rationale.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ Remembering the id and deleting it in the very mutation that re-appends it repla
the batch wholesale instead, so edits, additions, and the user's own deletions all
survive. That is why deletes apply before appends, and why the note-id dedupe is
computed after them — otherwise the batch being replaced would still count as
storing its own notes. The dedupe stays because the VS Code mirror path mints a
fresh id per teardown and has nothing to address an earlier write by; a note id is
a UUID, so "already stored" is an exact test there rather than a heuristic.
storing its own notes. The dedupe stays for the VS Code mirror path: a mirrored
Surface carrying no pending batch id still mints a fresh one at teardown, leaving
that batch nothing to address an earlier write by; a note id is a UUID, so
"already stored" is an exact test there rather than a heuristic.

`MAX_SAVE_ATTEMPTS` is 5. An unbounded retry against an archive somebody else is
rewriting in a loop would spin instead of telling the user, and the closure paths
Expand Down Expand Up @@ -258,13 +259,15 @@ meant the new view showed a batch the user had already deleted, still offering
Undo, which `deactivate()` would then delete for real hours later. Committing at
the disposal makes the promise true and leaves nothing pending to hand a resume.

The mirror carries a PTY id purely so a teardown can ask where each process is: the
extension host knows nothing of Surface ids, and `ptyManager.getCwd` answers for the
Session id the webview spawned under. On an editor-panel disposal the kills moved
*after* the archive write for that one reason — the loop used to run first, so by
the time the refresh asked, the PTY it was asking about was already dead. Every
other bookkeeping step in that disposal stayed synchronous and where it was; only
the kills wait, and they run in a `finally`, so a failed write still kills them.
A teardown asks `ptyManager.getCwd` for each mirrored terminal Surface's id, which
is the Session id the webview spawned the PTY under. The mirror carried that id a
second time as a `terminalId` field until it became clear the two are always equal;
the kind is what decides whether there is anything to ask. On an editor-panel
disposal the kills moved *after* the archive write so there would be a PTY left to
ask — the loop used to run first, so by the time the refresh asked, the process was
already dead. Every other bookkeeping step in that disposal stayed synchronous and
where it was; only the kills wait, and they run in a `finally`, so a failed write
still kills them.

The original `globalState` store was workspace-independent but cached separately
in every extension host. A process-local queue and revision counter therefore
Expand Down
5 changes: 2 additions & 3 deletions docs/specs/standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -1189,9 +1189,8 @@ restart-intent tests in `standalone/src-tauri/src/quit_state.rs`,
The `WindowEvent::DragDrop` handler in `lib.rs` emits the dropped paths as
`dormouse://files-dropped`; `TauriAdapter` fans that out to `onFilesDropped` for
the Wall. The whole path is **inert today**: `tauri.conf.json` sets
`dragDropEnabled: false` to keep in-webview HTML5 drag-and-drop working, so the
native handler never fires. Behavior and status:
`docs/specs/mouse-and-clipboard.md` (§8.7 Drag-to-Paste).
`dragDropEnabled: false`, so the native handler never fires. Behavior and
status: `docs/specs/mouse-and-clipboard.md` (§8.7 Drag-to-Paste).

## Logging

Expand Down
12 changes: 2 additions & 10 deletions docs/specs/terminal-context.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,11 @@
- **Must retain exited output**, offer Reset, and avoid automatic restart loops.
- **Must pause status and process-inspection polling while the context is hidden**, invalidating cached idle results. Reopening publishes current terminal status; source closure inspects work on demand.

| State | Status and action |
|---|---|
| Starting | Waiting for shell…; Modify |
| Autorun executing | Running the captured command; Modify |
| Untouched completion | Captured command autoran; Modify |
| User input | Skipping autorun to preserve user keystrokes; Reset |
| Empty default | Autorun off; Modify |
| No readiness | Autorun skipped: shell readiness unavailable; Modify |
| Exited | Helper exited; Reset |
**Must carry one status line per helper state**, offering Reset in place of Modify only after user input and after exit.

**Must make Reset an explicit discard**, confirming loss of scrollback, unfinished input, running programs, and unsaved edits. Cancellation changes nothing; confirmation disposes the old helper and launches a fresh one using the source's current directory and current global setting. Stale timers cannot write to the replacement.

Source of truth: `openHelper` / `helperHasWork` / `disposeHelper` / `closeHelperParent` in `lib/src/lib/helper-terminal.ts`; `markSessionTouched` / `parkElement` in `lib/src/lib/terminal-lifecycle.ts`; `TerminalContextView` in `lib/src/components/wall/TerminalContextView.tsx`. Tests: `lib/src/lib/helper-terminal.test.ts`.
Source of truth: `openHelper` / `helperHasWork` / `disposeHelper` / `closeHelperParent` in `lib/src/lib/helper-terminal.ts`; `markSessionTouched` / `parkElement` in `lib/src/lib/terminal-lifecycle.ts`; `HELPER_STATUS` — the state, its status line, and whether it offers Reset — in `lib/src/components/wall/TerminalContextView.tsx`. Tests: `lib/src/lib/helper-terminal.test.ts`.

Notepad sharing and pin restrictions follow `docs/specs/notepad.md` → "Helper terminals".

Expand Down
14 changes: 7 additions & 7 deletions docs/specs/terminal-escapes.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Neither `params` nor the URI is parsed at the PTY boundary.

**Cancel/close is the safe default; long targets must wrap and scroll without truncation.** **The confirmation host must reject deceptive verdicts even if its callback runs.** **Every adapter must revalidate through `normalizeExternalUri` before opening** (VS Code before `vscode.env.openExternal`) — consent does not replace validation.

Source of truth: `lib/src/lib/external-links.ts`, `lib/src/lib/external-link-confirmation.ts`, `lib/src/components/ExternalLinkModal.tsx`.
Source of truth: `normalizeExternalUri` in `lib/src/lib/external-links.ts` (pinned by `lib/src/lib/external-links.test.ts`), `lib/src/lib/external-link-confirmation.ts`, `lib/src/components/ExternalLinkModal.tsx`, and the host's own verdict re-check in `lib/src/components/ExternalLinkModalHost.tsx`.

## Supported CSI

Expand Down Expand Up @@ -126,7 +126,7 @@ Source of truth: `getWebviewHtml` in `vscode-ext/src/webview-html.ts`, `app.secu

`onData` includes xterm.js *replies*. **Both classifiers require every chunk token to match**, so a report glued onto real keystrokes is never mistaken for one.

- **`inputIsReplayTerminalReport`** — dropped outright while `isReplaying` (rationale). Shapes: cursor-position / device-status (`CSI [?]<params> R` / `n`), device attributes (`CSI [?>=]<params> c`), window-manipulation reports (`CSI <params> t` / `x`), DECRQSS and XTSMGRAPHICS reports (`CSI [?]<params> $y` / `S`), focus in/out (`CSI I` / `CSI O`), kitty keyboard-query replies (`CSI ? <flags> u`), and OSC, DCS, or APC replies of any shape. Also gates recording, attention ([alert.md](alert.md)), and untouched state ([layout.md](layout.md)).
- **`inputIsReplayTerminalReport`** — dropped outright while `isReplaying` (rationale). Shapes: cursor-position / device-status (`CSI [?]<params> R` / `n`), device attributes (`CSI [?>=]<params> c`), window-manipulation reports (`CSI <params> t` / `x`), DECRQSS and XTSMGRAPHICS reports (`CSI [?]<params> $y` and `CSI ? <params> S`), focus in/out (`CSI I` / `CSI O`), kitty keyboard-query replies (`CSI ? <flags> u`), and OSC, DCS, or APC replies of any shape. Also gates recording, attention ([alert.md](alert.md)), and untouched state ([layout.md](layout.md)).
- **`inputIsSyntheticTerminalReport`** — the broader prompt-recording guard (any chunk built only of CSI, SS3 `ESC O <final>`, OSC, or APC tokens). **Never dropped, and must suppress input recording alone** — these sequences can encode real keys.
- **`stripMouseReportsFromInput`** — removes X10 (`CSI M <3 bytes>`), SGR (`CSI < b;x;y M/m`) and urxvt (`CSI b;x;y M`) mouse reports during mouse-mode override, so reports bypassing DOM interception never reach the PTY ([mouse-and-clipboard.md](mouse-and-clipboard.md)). Keyboard-attention gating: `docs/specs/alert.md` → Attention.

Expand Down Expand Up @@ -170,10 +170,10 @@ The identity provokes more iTerm2 escape codes than Dormouse implements, so **un

| Shell | Mechanism | Channel | Notes |
|---|---|---|---|
| zsh | `ZDOTDIR` → our dotfiles chain to the user's, then install `precmd`/`preexec` hooks | env (as reliable as the `PATH` prepend) | **Nothing may be written into our directory when shipped** — signed macOS app bundle (rationale). The user's real `ZDOTDIR` rides in `USER_ZDOTDIR`; our `.zshrc` hands `ZDOTDIR` back so `.zlogin` and child shells are unaffected, and `.zshenv`/`.zprofile` re-pin `ZDOTDIR` to ours after sourcing the user's. **A `HISTFILE` set inside our directory is redirected to `USER_ZDOTDIR`** after sourcing the user's rc; a user-set one is never touched. |
| bash | `--init-file` → our script installs a `DEBUG`-trap / `PROMPT_COMMAND` hook | shellArgs | Dormouse drops `-l` (mutually exclusive with `--init-file`); the script sources `/etc/profile` + the user's profile itself. **Injected only when the launch args are *purely* interactive/login flags** (`-i`/`-l`/`--login`), so Git Bash's `--login -i` is covered and a specific `-c <cmd>` is not (rationale). **Written for bash 3.2**: no `PS0`, no array `PROMPT_COMMAND`. **`E` is a pipeline's first simple command**; boundaries and exit codes stay exact. |
| PowerShell | dot-source a script that wraps the user's `prompt` and PSReadLine's `PSConsoleHostReadLine`; covers `pwsh` and `powershell.exe` | shellArgs | **`-NoProfile` is never passed**, so the user's profile defines their prompt before we wrap it. Injected for any **interactive** launch — a bare REPL gets `-NoExit -Command ". '<script>'"`, one already carrying a startup command gets our dot-source *appended* (rationale); non-interactive one-offs (`-Command`/`-File`/`-EncodedCommand` without `-NoExit`) are left untouched. `E`/`C` come from that wrapper, `D` (`$?`/`$LASTEXITCODE`) from the next `prompt`. **Without PSReadLine the whole triple falls back to the next prompt**, boundaries and exit codes still exact (rationale). |
| WSL | `wsl.exe -d <distro> -- sh -c <detector>` → the detector execs the distro's bash with our `--init-file`, referenced via its `/mnt/...` path | shellArgs (Windows-side injection cannot reach inside the distro) | The detector reads `/etc/passwd`: it steps aside for an explicit zsh/fish login shell, execs bash+integration whenever bash exists (also the empty-detection default), and falls back to the login shell only when bash is absent (rationale). **bash is the only integrated WSL shell**; assumes the default `/mnt` automount root. |
| zsh | `ZDOTDIR` → our dotfiles chain to the user's, then install `precmd`/`preexec` hooks | env (as reliable as the `PATH` prepend) | **Nothing may be written into our directory when shipped** — signed macOS app bundle (rationale). **A `HISTFILE` set inside it is redirected to `USER_ZDOTDIR`**; a user-set one is never touched. |
| bash | `--init-file` → our script installs a `DEBUG`-trap / `PROMPT_COMMAND` hook | shellArgs | **Injected only when the launch args are *purely* interactive/login flags** (`-i`/`-l`/`--login`), so Git Bash's `--login -i` is covered and a specific `-c <cmd>` is not (rationale). **The whole argv is replaced with `--init-file <script>`** — `-l` and `-i` go with it, and the script replicates login startup itself. **`E` is a pipeline's first simple command**; boundaries and exit codes stay exact. |
| PowerShell | dot-source a script that wraps the user's `prompt` and PSReadLine's `PSConsoleHostReadLine`; covers `pwsh` and `powershell.exe` | shellArgs | Injected for any **interactive** launch — a bare REPL gets `-NoExit -Command ". '<script>'"`, one already carrying a startup command gets our dot-source *appended* (rationale); non-interactive one-offs (`-Command`/`-File`/`-EncodedCommand` without `-NoExit`) are left untouched. **`-NoProfile` is never passed.** **Without PSReadLine the whole triple falls back to the next prompt**, boundaries and exit codes still exact (rationale). |
| WSL | `wsl.exe -d <distro> -- sh -c <detector>` → the detector execs the distro's bash with our `--init-file`, referenced via its `/mnt/...` path | shellArgs (Windows-side injection cannot reach inside the distro) | **Injected only for the exact two-argument `-d <distro>` launch** the shell picker emits. The detector reads `/etc/passwd` and prefers bash, stepping aside only for an explicitly configured zsh or fish login shell (rationale). **bash is the only integrated WSL shell**; assumes the default `/mnt` automount root. |
| cmd.exe | no per-command hook exists | — | Never gets real OSC 633; always uses the keystroke fallback below. |

Wired in `applyShellIntegration`, called from `resolveSpawnConfig` (`standalone/sidecar/pty-core.js`), so both distributions spawn through it. The scripts are static files under `standalone/sidecar/shell-integration/`, located via `DORMOUSE_SHELL_INTEGRATION_DIR` (set by the host, mirroring `DORMOUSE_CLI_BIN`) and falling back to the sidecar's own directory; standalone ships them through the Tauri `../sidecar/**/*` glob, the VS Code build into `dist/shell-integration`. **Injection is fail-safe** — missing scripts mean it is skipped and the shell spawns as before.
Expand All @@ -189,7 +189,7 @@ Source of truth: `__dormouse_633_escape` and `__dormouse_633_safe_cwd` in each o

When injection isn't possible (cmd.exe, an unknown shell, missing scripts) or simply doesn't take, Dormouse falls back to its keystroke heuristic: the submitted command read off the rendered prompt line and synthesized as `commandStart{source:'user_input'}`, with no real exit codes. Its rules and the per-pane promotion that retires it on the first authentic OSC boundary belong to [terminal-state.md](terminal-state.md#keystroke-fallback).

> **The VS Code `.vsix` must include zsh's dotfiles** (`dist/shell-integration/.z*`), or zsh silently degrades to the keystroke fallback.
zsh's dotfiles reach the VS Code `.vsix` as `dist/shell-integration/.z*`, through `vscode-ext/.vscodeignore`'s `!dist/**`. Nothing asserts they are packaged; dropped, zsh silently degrades to the keystroke fallback.

Two escape-aware consumers are **not** parse sites: `lib/src/lib/terminal-controls.ts` strips presentation controls ([transport.md](transport.md)) and `lib/src/lib/terminal-state-store.ts` elides alternate-screen spans ([terminal-state.md](terminal-state.md)). Both read already-stripped output; neither changes what reaches xterm.js.

Expand Down
Loading
Loading