diff --git a/AGENTS.md b/AGENTS.md index 2532460f..4f90d6c2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,6 +43,10 @@ The primary job of a spec is to be an accurate reference for the current state o When updating code covered by a spec, update the spec to match. When the two specs overlap (e.g. pane header elements appear in both), layout.md documents placement and sizing while alert.md documents behavior and visual states. +When editing specs, keep them concise but do not replace invariants or edge cases with only a code pointer. Use `Source of truth:` for implementation references, and include direction/scope for protocols, command orchestration, and cross-package boundaries. For docs-only compression, spot-check referenced symbols, message directions, and root-vs-package script ownership against code before committing. + +Every spec that uses Session / Pane / Door / baseboard / passthrough vocabulary leads with a `> See \`docs/specs/glossary.md\` for ...` blockquote (see `layout.md`, `alert.md`, `terminal-state.md`). When introducing glossary vocabulary into a spec that lacks the callout, add it in the same edit. + ## Design See [DESIGN.md](DESIGN.md) for full design context. Key principles: diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 5189086e..a81f4dee 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -1,5 +1,7 @@ # Alert Spec +> See `docs/specs/glossary.md` for Session / Pane / Door vocabulary. This spec uses it throughout. + Alert state belongs to the **Session** Activity layer. It survives Pane <-> Door movement and is destroyed with the Session. Dormouse can owe the user attention in three ways: @@ -20,48 +22,9 @@ Terminal-report and command-exit alerts do not require WATCHING to be enabled. A ## Public State -The public Activity state is: - -```ts -type WatchingSessionStatus = - | 'WATCHING_DISABLED' - | 'NOTHING_TO_SHOW' - | 'MIGHT_BE_BUSY' - | 'BUSY' - | 'MIGHT_NEED_ATTENTION' - | 'ALERT_RINGING'; - -type SessionStatus = - | WatchingSessionStatus - | 'OSC_NOTIF_BUSY' - | 'COMMAND_EXIT_ARMED'; - -type TodoState = boolean; - -interface ActivityNotification { - source: 'OSC 9' | 'OSC 9;4' | 'OSC 99' | 'OSC 777' | 'BEL' | 'COMMAND_EXIT'; - title: string | null; - body: string | null; -} - -interface AlertState { - status: SessionStatus; - watchingEnabled: boolean; - todo: TodoState; - notification: ActivityNotification | null; - attentionDismissedRing: boolean; -} -``` - -Internal state is deliberately split into independent tracks: - -- `watchingStatus`: `WatchingSessionStatus`, or `WATCHING_DISABLED` when no `ActivityMonitor` exists. -- `protocolStatus`: `IDLE | OSC_NOTIF_BUSY | ALERT_RINGING`. -- `commandExitStatus`: `IDLE | COMMAND_EXIT_ARMED | ALERT_RINGING`. -- `progress`: active `OSC 9;4` progress, if any. -- `commandExitWatch`: the current foreground command eligible for command-exit alerting, if any. - -Public `status` is a projection: +Source of truth: `AlertState` / `ActivityNotification` in `lib/src/lib/alert-manager.ts` and `SessionStatus` in `lib/src/lib/activity-monitor.ts` define the public Activity state. Internal state is deliberately split into independent tracks (`watchingStatus`, `protocolStatus`, `commandExitStatus`, plus `progress` and `commandExitWatch` companion state) so each axis evolves without entangling the others. + +Public `status` is a projection — first match wins: 1. `ALERT_RINGING` if `protocolStatus`, `commandExitStatus`, or `watchingStatus` is ringing, in that order. 2. `OSC_NOTIF_BUSY` if protocol progress is active. @@ -70,7 +33,7 @@ Public `status` is a projection: Persist `status`, `watchingEnabled`, `todo`, and sanitized `notification`. Restore `todo` and `notification`, then restart WATCHING only if `watchingEnabled` is true. Restore must not recreate protocol progress, command-exit arms, or a fresh ring; replay filtering in `docs/specs/terminal-escapes.md` prevents old terminal output from firing notification side effects again. -Legacy TODO values migrate to boolean: `2`, numeric soft buckets `[0, 1]`, `'soft'`, and `'hard'` become `true`; `false`, `-1`, and unknown values become `false`. +Source of truth: `migrateTodoState` in `lib/src/lib/alert-manager.ts` defines the legacy-TODO-value migration to boolean. ## Attention @@ -83,7 +46,7 @@ Legacy TODO values migrate to boolean: `2`, numeric soft buckets `[0, 1]`, `'sof These do not count as attention: mere visibility, command-mode selection, hover, a Door existing in the baseboard, or reattaching a Door with `d` into command mode. -Attention is lost when the attention timer expires, the app loses focus, the attended Session is minimized or destroyed, or another Session becomes attended. `T_USER_ATTENTION` is 15 seconds by default and also acts as the minimum runtime for command-exit alerts. +Attention is lost when the attention timer expires, the app loses focus, the attended Session is minimized or destroyed, or another Session becomes attended. `T_USER_ATTENTION` also acts as the minimum runtime for command-exit alerts. ## WATCHING Track @@ -98,28 +61,15 @@ WATCHING is the user-controlled output/silence monitor. It starts fresh when ena | `MIGHT_NEED_ATTENTION` | A busy Session went quiet. Debounce state. | | `ALERT_RINGING` | WATCHING observed likely completion while the Session lacked attention. | -Timers live in `cfg.alert`: - -| Timer | Default | Purpose | -|---|---:|---| -| `busyCandidateGap` | 1500 ms | elapsed output window before busy is plausible | -| `busyConfirmGap` | 500 ms | confirmation window for `MIGHT_BE_BUSY` | -| `mightNeedAttention` | 2000 ms | silence after `BUSY` before possible completion | -| `needsAttentionConfirm` | 3000 ms | additional silence before ringing | -| `resizeDebounce` | 500 ms | ignore resize redraw output | -| `userAttention` | 15000 ms | attention idle expiry and command-exit minimum runtime | - -WATCHING transitions: - -- First output in `NOTHING_TO_SHOW` starts candidate tracking but stays `NOTHING_TO_SHOW`. -- Continued output across `busyCandidateGap` enters `MIGHT_BE_BUSY`; more output confirms `BUSY`, while no confirmation returns to `NOTHING_TO_SHOW`. -- Output in `BUSY` restarts the silence timer. -- Silence moves `BUSY -> MIGHT_NEED_ATTENTION -> ALERT_RINGING`, unless the Session has attention at confirmation time; if attended, reset to `NOTHING_TO_SHOW`. -- Output in `MIGHT_NEED_ATTENTION` returns to `BUSY`. -- `ALERT_RINGING` latches. New output without attention does not clear it; new output with attention starts a new `MIGHT_BE_BUSY` cycle. -- Attending or dismissing a WATCHING ring resets the monitor to `NOTHING_TO_SHOW`. +Source of truth: `cfg.alert` in `lib/src/cfg.ts` defines timer defaults and their purpose. -Rings must be caused by a fresh transition into `ALERT_RINGING`, never by rerender, theme change, remount, minimize, or reattach. +Source of truth: `ActivityMonitor` in `lib/src/lib/activity-monitor.ts` implements the transitions. The invariants the implementation must honor: + +- Output drives the monitor up the chain `NOTHING_TO_SHOW` -> `MIGHT_BE_BUSY` -> `BUSY`; silence drives it down `BUSY` -> `MIGHT_NEED_ATTENTION` -> `ALERT_RINGING`. The `MIGHT_*` states are debounce windows in both directions. +- First output starts candidate tracking without changing status; unconfirmed `MIGHT_BE_BUSY` returns to `NOTHING_TO_SHOW`; `ALERT_RINGING` ignores new output until the Session has attention. +- Attention at confirmation time suppresses the ring and resets to `NOTHING_TO_SHOW`. `ALERT_RINGING` otherwise latches; new output with attention starts a fresh `MIGHT_BE_BUSY` cycle. +- Attending or dismissing a WATCHING ring resets the monitor to `NOTHING_TO_SHOW`. +- Rings must be caused by a fresh transition into `ALERT_RINGING`, never by rerender, theme change, remount, minimize, or reattach. ## Protocol Track @@ -131,21 +81,13 @@ Protocol rings set `todo = true`, store the latest sanitized `ActivityNotificati ### Standalone BEL -A `BEL` byte outside an OSC sequence is stripped from visible output and creates: - -```ts -{ source: 'BEL', title: 'Terminal bell', body: null } -``` +A `BEL` byte outside an OSC sequence is stripped from visible output and creates the BEL notification (`TERMINAL_BELL_NOTIFICATION` in `lib/src/lib/terminal-protocol.ts`). If a parse batch also contains a richer OSC notification/progress event, drop the generic `BEL` detail so it cannot overwrite useful preview text. Multiple standalone bells in one batch collapse to one notification. ### OSC 9 -`OSC 9 ; ST` creates: - -```ts -{ source: 'OSC 9', title: null, body: message } -``` +`OSC 9 ; ST` creates an OSC 9 notification with the message as body (title null); see `parseOsc9` in `lib/src/lib/terminal-protocol.ts`. Empty sanitized messages are ignored. OSC 9 also feeds title-candidate derivation in `docs/specs/terminal-state.md`; that does not change alert behavior. @@ -167,18 +109,14 @@ Rules: - `state=1, progress=100` rings as completion if unattended. - `state=2` rings as error if unattended. - Clear rings as completion only if there was an active progress cycle; otherwise ignore it. -- Warning progress does not ring by itself, but completion of a warning cycle uses the generated title `Progress warning`. +- Warning progress does not ring by itself, but completion of a warning cycle rings with a generated warning title. - Invalid states, missing required percents for states `1` and `4`, and out-of-range percents are ignored. -Generated notifications use source `OSC 9;4`, title `Progress complete`, `Progress warning`, or `Progress error`, and body `Progress %` when a percent is known. +Source of truth: `ringOrSuppressProtocolProgress` / `completeProtocolProgress` in `lib/src/lib/alert-manager.ts` produce generated titles/bodies for these rings. ### OSC 777 -`OSC 777 ; notify ; ; <body> ST` creates: - -```ts -{ source: 'OSC 777', title, body } -``` +`OSC 777 ; notify ; <title> ; <body> ST` creates an OSC 777 notification; see `parseOsc777` in `lib/src/lib/terminal-protocol.ts`. Only `notify` is supported. The first field after `notify` is the title; everything after the next semicolon is body, preserving additional semicolons there. Unsupported subcommands and empty sanitized notifications are ignored. @@ -199,10 +137,10 @@ Supported keys: Management payloads do not ring: -- `p=?` sends a support response advertising `o=always:p=title,body`. +- `p=?` sends a support response advertising the support payload defined in `lib/src/lib/terminal-protocol.ts` (`OSC99_SUPPORT_PAYLOAD`). - `p=close`, `p=alive`, `p=icon`, and `p=buttons` are consumed or ignored without creating notification UI. -Pending OSC 99 chunks expire after 60 seconds, and at most 64 pending ids are retained per parser. +Source of truth: `lib/src/lib/terminal-protocol.ts` defines the pending OSC 99 chunk TTL and max-pending-id cap. ## Command-exit Track @@ -214,7 +152,7 @@ Rules: - If the user attends while a command is already running, mark that command as seen. - If attention is later lost while that same seen command is still running, set `commandExitStatus = COMMAND_EXIT_ARMED`. - If the same command finishes, or the PTY exits before a finish event, ring only when all are true: it was armed, the Session still lacks attention, and runtime is at least `T_USER_ATTENTION`. -- A command-exit ring sets `todo = true` and stores `{ source: 'COMMAND_EXIT', title: 'Command finished', body }`, where body is the summarized command plus exit code when known. +- A command-exit ring sets `todo = true` and stores the COMMAND_EXIT notification built by `setCommandExitRinging` / `formatCommandExitBody` in `lib/src/lib/alert-manager.ts` (title "Command finished", body = summarized command + exit code). - Returning to the Session before finish disarms the watch. A quick finish, a different command start, or Session destruction clears it without ringing. - Race rule: attention must be lost before the finish event is observed. - Precedence rule: a protocol ring must keep its richer `ActivityNotification`; command-exit must not overwrite it. @@ -246,18 +184,7 @@ The header shows: - a hover/focus notification preview when TODO has `notification` - a dialog from right-click or some left-click actions, containing TODO and WATCHING switches plus notification detail -Bell visual state is a pure function of public `status`: - -| Status | Visual | -|---|---| -| `WATCHING_DISABLED` | outline bell, muted | -| `NOTHING_TO_SHOW` | filled bell, muted, upright | -| `MIGHT_BE_BUSY` | filled bell, muted, -22.5 degree tilt | -| `BUSY` | filled bell, muted, 45 degree tilt | -| `OSC_NOTIF_BUSY` | same as `BUSY` | -| `COMMAND_EXIT_ARMED` | same as `BUSY` | -| `MIGHT_NEED_ATTENTION` | filled bell, muted, 60 degree tilt | -| `ALERT_RINGING` | filled bell, warning color, rocking animation; reduced motion uses static 45 degree tilt | +Bell visual state is a pure function of public status. Source of truth: `bellIconClass` in `lib/src/components/bell-icon-class.ts` defines the tilt/animation mapping. Tilt and animation must not change layout size. Long titles truncate before alert/TODO controls disappear. @@ -294,7 +221,7 @@ Sanitization and limits: - Treat all text as plain text. - Strip C0/C1 controls after protocol parsing, collapse whitespace controls to spaces, and trim. - Do not interpret ANSI, OSC, HTML, Markdown, URLs, paths, or emoji shortcodes as markup. -- Store at most 256 Unicode code points for title and 4096 for body. +- Store at most the `TITLE_LIMIT` / `BODY_LIMIT` Unicode code points defined in `lib/src/lib/terminal-protocol.ts`. - Store only the latest `ActivityNotification`, not unbounded history. - Cap and expire incomplete OSC 99 parser state as described above. @@ -313,23 +240,6 @@ Security requirements: - Bell, TODO, preview, and dialog controls must remain keyboard reachable; dialogs trap focus and support `Escape`. - Tooltips, dialog copy, and future localized TODO labels must wrap in narrow layouts. -## Verification Checklist - -- WATCHING rings only on a fresh unattended transition into `ALERT_RINGING`. -- Quick output stays in `NOTHING_TO_SHOW`; pauses in busy output debounce through `MIGHT_NEED_ATTENTION`. -- Resize noise cannot cause a WATCHING ring. -- Alert/TODO state survives Pane <-> Door transitions. -- Door click/`Enter` clears a ring; Door `d` does not. -- Protocol notifications ring with WATCHING disabled, but not while the Session has attention. -- `OSC 9;4` active progress shows `OSC_NOTIF_BUSY`; completion, error, and active-progress clear ring only when unattended. -- Standalone `BEL` does not replace richer OSC detail in the same parse batch. -- OSC 99 chunking, base64, support query, and management payloads behave as specified. -- Command-exit arms only after a seen command loses attention and rings only on the same command after the minimum runtime. -- Protocol detail wins over generated command-exit detail. -- Dismiss/attend creates TODO; passthrough `Enter` clears TODO. -- Restore/replay does not refire old notification side effects. -- Long titles and notification text do not overflow fixed header or Door controls. - ## References - iTerm2 proprietary escape codes (OSC 9, OSC 9;4): https://iterm2.com/documentation-escape-codes.html diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index 462a3f74..0368e18a 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -58,80 +58,22 @@ Stage 2: Local (sign-and-deploy.sh) ## Stage 1: CI workflow -Triggered by tag push `v*`. Three jobs run in parallel — `build-standalone`, `build-vscode`, and `security-audit` — and `publish-vscode` runs after all three succeed: +Triggered by tag push `v*`. Three jobs run in parallel — `build-standalone`, `build-vscode`, and `security-audit` — and `publish-vscode` runs after all three succeed. -The workflow defaults `GITHUB_TOKEN` to read-only repository access with: +Jobs, matrix targets, pnpm/Node versions, and step ordering are defined in [.github/workflows/release.yml](../../.github/workflows/release.yml). -```yaml -permissions: - contents: read -``` - -Only the build jobs request additional permissions, and only for provenance: - -```yaml -permissions: - contents: read - id-token: write - attestations: write -``` - -The publish job stays on the workflow read-only default and is separately gated by the `vscode-extension-publish` environment. - -### Job: `build-standalone` (matrix) - -Runs on `ubuntu-22.04` (linux), `macos-latest` (mac), and `windows-latest` (win). Uses `tauri-apps/tauri-action@v0`. - -```yaml -strategy: - matrix: - include: - - platform: ubuntu-22.04 - target: x86_64-unknown-linux-gnu - - platform: macos-latest - target: aarch64-apple-darwin - - platform: windows-latest - target: x86_64-pc-windows-msvc -``` - -Each matrix leg: -1. Checkout, setup Node 22, pnpm 11.0.6, Rust stable -2. Install workspace dependencies once from the repo root with `pnpm install --frozen-lockfile` -3. Install system deps (Linux: libgtk, libwebkit, etc.) -4. Generate an ephemeral, per-job Tauri updater key with `pnpm --dir standalone exec tauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --force` -5. Build via `tauri-action` with `TAURI_SIGNING_PRIVATE_KEY` pointing at that ephemeral key, but **no real updater signing secret** and no `APPLE_SIGNING_IDENTITY` -6. Generate `artifact-manifest.sha256` with SHA-256 hashes for the files that will be uploaded -7. Publish a GitHub artifact attestation for the manifest -8. Upload the manifest plus artifacts (installers + bundles) via `actions/upload-artifact` +The workflow defaults `GITHUB_TOKEN` to read-only repository access (`contents: read`). Only the build jobs request additional permissions, and only for provenance (`id-token: write` + `attestations: write`). The publish job stays on the workflow read-only default and is separately gated by the `vscode-extension-publish` environment. **Note:** We do NOT use `tauri-action`'s built-in GitHub Release creation. We create the release locally after signing. The CI updater key exists only so Tauri emits updater-shaped artifacts during unsigned builds. It is generated inside the runner, is not stored in source control or GitHub Secrets, and its public key is not the public key trusted by shipped apps. The final release bundles are re-signed locally by `scripts/sign-and-deploy.sh` with the production Tauri updater key before upload. -### Job: `build-vscode` - -Runs on `ubuntu-latest`: -1. Checkout, setup Node 22, pnpm 11.0.6 -2. `pnpm install --frozen-lockfile` at the repo root -3. `pnpm --filter dormouse-lib test` -4. `pnpm --filter dormouse build:frontend && pnpm --filter dormouse build` -5. `pnpm --dir vscode-ext exec vsce package --no-dependencies` -6. Generate `artifact-manifest.sha256` for the `.vsix` -7. Publish a GitHub artifact attestation for the manifest -8. Upload the manifest plus `.vsix` as artifact - ### Job: `security-audit` Calls the reusable `security-audit.yaml` workflow, which audits the repo against `SECURITY.md` (the same audit that runs nightly via schedule). `publish-vscode` is gated on it, so a failing security audit blocks the VS Code Marketplace publish. ### Job: `publish-vscode` -Runs after `build-standalone`, `build-vscode`, and `security-audit` succeed: -1. Enter the `vscode-extension-publish` GitHub environment -2. Download `.vsix` artifact -3. `pnpm exec vsce publish --packagePath *.vsix --no-dependencies` -4. `pnpm exec ovsx publish --packagePath *.vsix --no-dependencies` - This runs in CI because VSCode Marketplace publishing uses PAT tokens (no hardware key needed). The `vscode-extension-publish` environment must require reviewer approval and allow deployments only from `v*` tags. Store `VSCE_PAT` and `OVSX_PAT` as environment secrets there, not broad repository secrets. ## Stage 2: Local script @@ -145,18 +87,9 @@ Before any local signing step runs, downloaded CI artifacts must pass two checks The manifest itself is the attested subject, not the final signed app. This closes the gap between CI artifact production and the local machine that holds signing credentials: stale cached artifacts, wrong-tag artifacts, and tampered downloads are rejected before codesign, jsign, notarization, Tauri signing, or release upload can run. -The local script must also select release artifacts by strict expected paths instead of broad `find | head` matches. Release signing fails closed unless the expected files exist at the expected locations: - -| Artifact | Expected local path under `release-signed/work` | -|----------|-------------------------------------------------| -| macOS app bundle | `standalone-mac-aarch64/src-tauri/target/aarch64-apple-darwin/release/bundle/macos/Dormouse.app` | -| Windows app executable | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/dormouse.exe` | -| Windows installer | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/Dormouse_X.Y.Z_x64-setup.exe` | -| NSIS script | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/nsis/x64/installer.nsi` | -| NSIS plugin | `standalone-win-x64/src-tauri/target/x86_64-pc-windows-msvc/release/nsis/x64/plugins/nsis_tauri_utils.dll` | -| Linux AppImage | `standalone-linux-x64/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/Dormouse_X.Y.Z_amd64.AppImage` | +The local script must also select release artifacts by strict expected paths instead of broad `find | head` matches. Release signing fails closed unless the expected files exist at the expected locations. The exact expected paths are enforced in `scripts/sign-and-deploy.sh`. -Release upload likewise uses only the three stable output filenames (`Dormouse-macos-aarch64.tar.gz`, `Dormouse-windows-x64-setup.exe`, `Dormouse-linux-x86_64.AppImage`) and fails if `release-signed/release-assets` contains any other files. +Release upload likewise uses only the three stable output filenames (the `FNAME_*` constants in `scripts/sign-and-deploy.sh`) and fails if `release-signed/release-assets` contains any other files. When rebuilding the Windows installer locally, the script rewrites the absolute CI-runner paths baked into the Tauri-generated NSIS `.nsi` script (via `scripts/patch-nsis-paths.pl`) and patches the `ADDITIONALPLUGINSPATH` and `OUTFILE` defines to the expected local plugin directory and installer path before running `makensis`. @@ -194,22 +127,14 @@ Windows release builds use the GUI subsystem, so launching `dormouse.exe` from a ## Artifact filenames -All release assets use **stable filenames** (no version in the name). This allows hotlinking directly from dormouse.sh via GitHub's `/latest/download/` redirect, which always resolves to the most recent release. - -| Asset | Filename | Purpose | -|-------|----------|---------| -| Windows | `Dormouse-windows-x64-setup.exe` | Download + Tauri updater | -| macOS | `Dormouse-macos-aarch64.tar.gz` | Download + Tauri updater | -| Linux | `Dormouse-linux-x86_64.AppImage` | Download + Tauri updater | +All release assets use **stable filenames** (no version in the name). This allows hotlinking directly from dormouse.sh via GitHub's `/latest/download/` redirect, which always resolves to the most recent release. Stable output filenames are the `FNAME_*` constants in `scripts/sign-and-deploy.sh`. ### Download hotlinks -The dormouse.sh download page can link directly to the latest release with no server-side logic: +The dormouse.sh download page can link directly to the latest release with no server-side logic, e.g.: ``` -https://github.com/diffplug/dormouse/releases/latest/download/Dormouse-windows-x64-setup.exe https://github.com/diffplug/dormouse/releases/latest/download/Dormouse-macos-aarch64.tar.gz -https://github.com/diffplug/dormouse/releases/latest/download/Dormouse-linux-x86_64.AppImage ``` These can later be migrated to `dormouse.sh/download/...` URLs backed by Cloudflare R2 (for analytics) without changing anything in the app — only the website links and the updater endpoint URL in `tauri.conf.json` would change. diff --git a/docs/specs/layout.md b/docs/specs/layout.md index 10d88bb2..7e3e7f3a 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -303,11 +303,12 @@ Each session also carries `TerminalPaneState` from `docs/specs/terminal-state.md ## Theme -Custom `dormouseTheme` extends dockview's `themeAbyss`: -- `gap: 6` — 6px between groups in both directions -- `dndOverlayMounting: 'absolute'`, `dndPanelOverlay: 'group'` -- Pane header height: `--dv-tabs-and-actions-container-height: 30px` -- 6px top/sides inset and 2px bottom inset around the dockview area (`px-1.5 pt-1.5 pb-0.5` on wrapper, `inset-x-1.5 top-1.5 bottom-0.5` on container) +Custom `dormouseTheme` extends dockview's `themeAbyss`. Source of truth: +`dormouseTheme` in `lib/src/components/Wall.tsx` defines gap and dnd overlay +settings; `lib/src/index.css` defines dockview CSS-var overrides such as pane +header height. The dockview area uses a 6px top/sides inset and 2px bottom +inset (`px-1.5 pt-1.5 pb-0.5` on wrapper, `inset-x-1.5 top-1.5 bottom-0.5` on +container). Colors use a two-layer CSS variable strategy: `@theme --color-*` tokens → `var(--vscode-*)`. VSCode provides host theme variables in extension mode; standalone and website mode apply bundled or installed theme variables before rendering. Tailwind v4 `@theme` block registers `--color-*` tokens as Tailwind colors (e.g., `bg-app-bg`, `text-app-fg`, `border-border`). See `theme.css` for the full token map. diff --git a/docs/specs/mobile-ui.md b/docs/specs/mobile-ui.md index fb9555e3..2e14dda5 100644 --- a/docs/specs/mobile-ui.md +++ b/docs/specs/mobile-ui.md @@ -1,5 +1,7 @@ # Mobile Terminal Website Prototype Spec +> See `docs/specs/glossary.md` for Session / Pane / Door vocabulary. This spec uses it throughout. + ## 1. Overview This document specifies the `/playground/pocket` mobile terminal prototype. @@ -103,13 +105,14 @@ The selector must be self-labeling through segmented buttons that include both an icon and a short mode label. Icon-only touch controls are too hard to discover in this prototype. -Touch modes: +Source of truth: `TOUCH_MODES` in `lib/src/components/MobileTerminalUi.tsx` +defines touch-mode button labels and icons. -| Mode | Button label | Icon | Availability | Behavior | -| --- | --- | --- | --- | --- | -| Gestures | `Gestures` | `HandPointingIcon` | Always available | Pane-content touches, pen presses, and primary mouse/trackpad clicks open the Gesture mode radial menu. | -| Text selection | `Select` | `CursorTextIcon` | Always available | Pane-content touch, pen, and primary mouse/trackpad drags use the same terminal text selection and copy/paste behavior as desktop. If a mounted pane's TUI is capturing mouse events, Dormouse activates mouse override for that pane. | -| Mouse | `Mouse` | `CursorClickIcon` | Only when the active TUI is capturing mouse events | Touches are passed through as terminal mouse input. | +| Mode | Availability | Behavior | +| --- | --- | --- | +| Gestures | Always available | Pane-content touches, pen presses, and primary mouse/trackpad clicks open the Gesture mode radial menu. | +| Text selection | Always available | Pane-content touch, pen, and primary mouse/trackpad drags use the same terminal text selection and copy/paste behavior as desktop. If a mounted pane's TUI is capturing mouse events, Dormouse activates mouse override for that pane. | +| Mouse | Only when the active TUI is capturing mouse events | Touches are passed through as terminal mouse input. | Default touch mode is **Gestures**. @@ -161,14 +164,16 @@ compass rose. It must not draw a line directly under the user's thumb. The guide line is solid and fully opaque, and the offset rose center renders a small fully opaque circle. -Gesture mode uses these radii: +Source of truth: `RADIUS_LAYOUT`, `RADIUS_SELECT`, `RADIUS_FADE_START`, and +`RADIUS_HIGHLIGHT` in `lib/src/lib/mobile-gesture-menu.ts` define the radii +used below. -| Variable | Value | Behavior | -| --- | --- | --- | -| `RADIUS_LAYOUT` | `92px` | Base circular radius for exploded option anchors around the offset compass rose origin. Diagonal exploded labels use normalized compass vectors, so their x/y offsets are `RADIUS_LAYOUT * Math.SQRT1_2`. Root labels use separate packed square-keypad geometry so long labels do not overlap. | -| `RADIUS_SELECT` | `RADIUS_LAYOUT * 0.75` | Visible circle drawn around the offset compass rose origin. When the mirrored drag reaches this distance, the closest compass direction is selected. | -| `RADIUS_FADE_START` | `RADIUS_SELECT * 0.25` | No directional root-group fading happens before this drag distance. | -| `RADIUS_HIGHLIGHT` | `RADIUS_SELECT * 0.5` | No circle is drawn. When the drag reaches this distance, the closest compass direction is highlighted, but not selected. | +| Variable | Behavior | +| --- | --- | +| `RADIUS_LAYOUT` | Base circular radius for exploded option anchors around the offset compass rose origin. Diagonal exploded labels use normalized compass vectors, so their x/y offsets are `RADIUS_LAYOUT * Math.SQRT1_2`. Root labels use separate packed square-keypad geometry so long labels do not overlap. | +| `RADIUS_SELECT` | Visible circle drawn around the offset compass rose origin. When the mirrored drag reaches this distance, the closest compass direction is selected. | +| `RADIUS_FADE_START` | No directional root-group fading happens before this drag distance. | +| `RADIUS_HIGHLIGHT` | No circle is drawn. When the drag reaches this distance, the closest compass direction is highlighted, but not selected. | Gesture menu item state uses the same palette as pane headers. Idle groups and options use inactive header background/foreground. Highlighted or selected @@ -190,17 +195,12 @@ and the select circle grows from zero radius to `RADIUS_SELECT`. This is a short state-reveal motion, not an ongoing decoration; reduced-motion users get the final state immediately. -While the user is still choosing a root group, the root groups fade according to -the current drag vector only after the drag exceeds `RADIUS_FADE_START`. Before -that threshold, all root groups render at full opacity. After the threshold, -define `dragHat = (currentPoint - origin) / RADIUS_SELECT` and `unitToGroup` as -the unit vector from the origin to the group's compass direction. The root group -target opacity is `clamp(0.75 + dragHat dot unitToGroup, 0, 1)`. The rendered -opacity blends smoothly from `1` at `RADIUS_FADE_START` to that target at -`RADIUS_SELECT` using -`fadeProgress = clamp((dragDistance - RADIUS_FADE_START) / (RADIUS_SELECT - -RADIUS_FADE_START), 0, 1)` and -`opacity = 1 + (targetOpacity - 1) * fadeProgress`. +While the user is still choosing a root group, all root groups stay fully +opaque until the drag exceeds `RADIUS_FADE_START`; past that threshold each +group fades by its alignment with the drag vector (the group the user is +moving toward stays brightest, the rest dim toward a floor), reaching the +full per-direction opacity at `RADIUS_SELECT`. Source of truth: +`rootGroupOpacity()` in `lib/src/components/MobileGestureRadialMenu.tsx`. N, S, E, and W root labels render as single arrow chips. Dragging to `RADIUS_SELECT` in one of those four cardinal directions immediately sends the @@ -214,6 +214,8 @@ newly spawned option labels. Root labels are laid out as a square keypad, not on a circle. The four cardinal arrow chips use one shared `GAP_CARDINAL_RING` from the select circle edge. +Source of truth: `GAP_CARDINAL_RING` and `GAP_CLUSTER` in +`lib/src/components/MobileGestureRadialMenu.tsx`. Diagonal groups use an EW-dominant corner-and-stack layout: the center option's inward corner is aligned with the diagonal tick mark at the same ring gap used by the cardinal arrow chips, measured on screen as the same horizontal/vertical @@ -227,9 +229,9 @@ labels use circular direction anchors at `RADIUS_LAYOUT` from the reset center. The root label pack stays close to the select circle, while preserving enough room for long labels like Backspace. -Each diagonal root cluster uses `GAP_CLUSTER = 2px`. The first option in each -diagonal group is the cluster center. Secondary options use the same edge-and-gap -rule above or below the center chip. +Each diagonal root cluster uses the shared `GAP_CLUSTER` spacing. The first +option in each diagonal group is the cluster center. Secondary options use the +same edge-and-gap rule above or below the center chip. For cardinal directions, the radial menu is a one-stage gesture: @@ -261,14 +263,9 @@ For diagonal directions, the radial menu is a two-stage gesture: If the user releases after the first group selection but before choosing one of the exploded options, the gesture is cancelled. -Exploded option directions for diagonal groups: - -| Selected group | Option directions | -| --- | --- | -| NE | SW, W, S | -| SE | NW, N, W | -| SW | NE, N, E | -| NW | SE, E, S | +Source of truth: `MOBILE_GESTURE_OPTION_DIRECTIONS` in +`lib/src/lib/mobile-gesture-menu.ts` defines exploded-option directions per +diagonal group. Examples: @@ -306,28 +303,10 @@ The quit submenu uses the same reset-center, highlight-radius, and select-radius rules as the main option selection. Its final selected item uses the same expand-and-fade completion feedback as the root menu options. -Gesture action mappings: - -| Action | Sequence | -| --- | --- | -| Esc | `\x1B` | -| ⌃C | `\x03` | -| q | `q` | -| ⌃X | `\x18` | -| `:q↵` | `:q\r` | -| ▲ | `\x1B[A` | -| Backspace | `\x7F` | -| Paste | Existing Dormouse paste flow for the active pane | -| n | `n` | -| ◀ | `\x1B[D` | -| ▶ | `\x1B[C` | -| Tab | `\x09` | -| ⬆︎Tab | `\x1B[Z` | -| Space | ` ` | -| ▼ | `\x1B[B` | -| Enter | `\r` | -| ⬆︎Enter | `\x1B[13;2u` | -| y | `y` | +Source of truth: `MOBILE_TERMINAL_KEY_SEQUENCES` in +`lib/src/components/MobileTerminalUi.tsx` defines action-to-byte-sequence +mapping; `MOBILE_GESTURE_GROUPS` and `MOBILE_GESTURE_QUIT_GROUP` in +`lib/src/lib/mobile-gesture-menu.ts` define root and quit submenu actions. ## 6. Input Mode Selector @@ -341,14 +320,16 @@ Sessions | Recent | Type | Draft The selector must be self-labeling through segmented buttons that include both an icon and a short mode label. -Input modes: +Source of truth: `KEYBOARD_MODES` and `RESERVE_PLACEHOLDER_COPY` in +`lib/src/components/MobileTerminalUi.tsx` define input-mode button labels, +icons, and placeholder copy. -| Mode | Button label | Icon | Reserve area content | -| --- | --- | --- | --- | -| Sessions | `Sessions` | `TerminalWindowIcon` | The reserve area displays mobile session rows with active, alert, and TODO state. Selecting a session makes it the single visible terminal. | -| Recent | `Recent` | `ClockCounterClockwiseIcon` | The entire reserve area displays `WIP - commands you have recently executed will be available here`. | -| Type | `Type` | `TextTIcon` | The reserve area displays `Onscreen keyboard goes here` and focuses the hidden terminal input. Every typed key is echoed into the terminal as it happens. | -| Draft | `Draft` | `ArticleNyTimesIcon` | The entire reserve area displays `WIP - this will be a place to draft prompts before pasting into the terminal`. | +| Mode | Reserve area content | +| --- | --- | +| Sessions | The reserve area displays mobile session rows with active, alert, and TODO state. Selecting a session makes it the single visible terminal. | +| Recent | The entire reserve area displays the Recent reserve placeholder copy. | +| Type | The reserve area displays the Type reserve placeholder copy and focuses the hidden terminal input. Every typed key is echoed into the terminal as it happens. | +| Draft | The entire reserve area displays the Draft reserve placeholder copy. | Default input mode is **Type**. @@ -423,8 +404,7 @@ The keyboard reserve area has a stable height. It should not be recomputed from `visualViewport` while the native keyboard animates. When the OS keyboard is hidden, the reserve area shows the selected app keyboard -UI: session list, `WIP - commands you have recently executed will be available here`, -`Onscreen keyboard goes here`, or `WIP - this will be a place to draft prompts before pasting into the terminal`. +UI: session list, or the Recent/Type/Draft reserve placeholder copy. When the OS keyboard is visible, the OS keyboard may cover or occupy that same physical area. This is preferred over resizing the whole app around the keyboard. @@ -491,8 +471,8 @@ Sessions | Recent | Type | Draft * Stable keyboard reserve area. * Sessions reserve content: active session rows with alert and TODO state. -* Recent reserve content: `WIP - commands you have recently executed will be available here`. -* Draft reserve content: `WIP - this will be a place to draft prompts before pasting into the terminal`. +* Recent reserve content: the Recent reserve placeholder copy. +* Draft reserve content: the Draft reserve placeholder copy. * Type mode native mobile keyboard input. * Gesture mode radial menu for arrows, navigation keys, Esc, Tab, Enter, simple vim-like keys, confirmed Ctrl+C, confirmed Paste, and Quit breakout. * Pocket `tut` starts directly in the Gesture navigation section, uses the title `Dormouse Pocket Tutorial`, and credits gesture items from radial-menu input callbacks rather than from native keyboard input. diff --git a/docs/specs/mouse-and-clipboard.md b/docs/specs/mouse-and-clipboard.md index fa35b16e..b461f8a8 100644 --- a/docs/specs/mouse-and-clipboard.md +++ b/docs/specs/mouse-and-clipboard.md @@ -1,5 +1,7 @@ # Terminal Mouse and Clipboard Behavior Specification +> See `docs/specs/glossary.md` for Session / Pane vocabulary. This spec uses it for the pane-level scoping of mouse regime, override state, and selection. + ## Overview Mouse handling and clipboard (copy and paste) behavior for the terminal across macOS, Linux, and Windows. The core design goal is to make text selection, copying, pasting, and mouse-driven interaction with TUI programs coexist cleanly, with visible state and predictable transitions between modes. @@ -25,63 +27,32 @@ The terminal makes the current regime visible in the pane header, provides a way ## 1. The Mouse Icon (Header Indicator) -### 1.1 Visibility - -- When the inside program has **not** requested mouse reporting: no icon is shown. -- When the inside program **has** requested mouse reporting: a **Mouse icon** (Phosphor `CursorClickIcon`) is shown in the terminal header. -- When the user has activated an override: the Mouse icon is replaced by a **No-Mouse icon** (Phosphor `SelectionSlashIcon`) in the same header location. - -### 1.2 Hover Text - -- Mouse icon hover text: `TUI is intercepting mouse commands. Click to override.` -- No-Mouse icon hover text: `You're overriding the TUI's mouse capture. Click to restore.` +**Visibility.** No icon when the inside program has not requested mouse reporting; a **Mouse icon** (Phosphor `CursorClickIcon`) when it has; replaced by a **No-Mouse icon** (Phosphor `SelectionSlashIcon`) in the same header location while the user has activated an override. -### 1.3 Click Behavior +**Click.** Clicking the Mouse icon activates a **temporary override** (see §2). Clicking the No-Mouse icon ends the override immediately and restores mouse reporting to the inside program. -- Clicking the **Mouse icon** activates a **temporary override** (see §2). -- Clicking the **No-Mouse icon** ends the override immediately and restores mouse reporting to the inside program. +Source of truth: `lib/src/components/wall/TerminalPaneHeader.tsx` defines hover text for both icons. --- ## 2. Override State -### 2.1 Temporary Override +There are two override modes: temporary (the default) and sticky. -Activated by clicking the Mouse icon. While the temporary override is active: +**Temporary override.** Activated by clicking the Mouse icon. While active: - Mouse events are handled by the terminal, not forwarded to the inside program. -- Wheel events are also suppressed so xterm cannot translate scroll input into - mouse reports or alternate-screen arrow-key input for the inside program. +- Wheel events are also suppressed so xterm cannot translate scroll input into mouse reports or alternate-screen arrow-key input for the inside program. - The Mouse icon is replaced with the No-Mouse icon. - A banner appears below the No-Mouse icon reading `Temporary mouse override until mouse-up.` followed by two buttons: **Make sticky** and **Cancel**. -- The override persists until the **next mouse-up event inside the terminal content area** (live region or scrollback) that is paired with a prior mouse-down in the same area. This includes plain clicks (a mouse-down/up pair that never crossed the drag threshold) as well as completed drags. The click on the No-Mouse icon itself, the banner's buttons, and any "orphan" mouse-up from a drag that started outside the terminal do **not** count as that mouse-up. -- After that mouse-up, the override automatically ends: mouse reporting is restored to the inside program, the banner is dismissed, and the icon reverts to the Mouse icon. - -### 2.2 Making the Override Sticky - -- Clicking **Make sticky** in the banner converts the temporary override into a sticky one. -- The banner is dismissed. -- The No-Mouse icon remains visible with its "click to restore" hover text. -- Mouse and wheel events continue to be handled by the terminal rather than the - inside program. -- The override persists until the user clicks the No-Mouse icon, or until the inside program stops requesting mouse reporting. - -### 2.3 Canceling the Temporary Override - -- Clicking **Cancel** in the banner ends the override immediately. -- The banner is dismissed, mouse reporting is restored, and the icon reverts to the Mouse icon. -### 2.4 No Keyboard Activation +The temporary override ends on the **next mouse-up event inside the terminal content area** (live region or scrollback) that is paired with a prior mouse-down in the same area. This includes plain clicks (a mouse-down/up pair that never crossed the drag threshold) and completed drags; it excludes clicks on the No-Mouse icon, the banner buttons, and any "orphan" mouse-up from a drag that started outside the terminal. After that mouse-up, mouse reporting is restored, the banner is dismissed, and the icon reverts to the Mouse icon. Clicking **Cancel** in the banner ends the override immediately with the same outcome. If the user activates an override and never performs a mouse action, it remains in place indefinitely; there is no timeout. -The mouse icon, No-Mouse icon, and banner buttons are mouse-only. They are not keyboard-activatable. +**Sticky override.** Clicking **Make sticky** in the banner converts the temporary override into a sticky one. The banner is dismissed; the No-Mouse icon remains visible with its "click to restore" hover text; mouse and wheel events continue to be handled by the terminal rather than the inside program. The sticky override persists until the user clicks the No-Mouse icon, or until the inside program stops requesting mouse reporting. -### 2.5 Edge Case: No Drag After Override +**Auto-clear on reporting off.** If the inside program stops requesting mouse reporting (e.g. exits or sends DECRST `?1000l`/`?1002l`/`?1003l`) while either override is active, the override is cleared. The icon and banner are removed because there is no longer anything to override. -If the user activates an override and then never performs a mouse action, the override remains in place indefinitely. There is no timeout. - -### 2.6 Auto-Cleared on Reporting Off - -If the inside program stops requesting mouse reporting (e.g. exits or explicitly sends DECRST `?1000l`/`?1002l`/`?1003l`) while an override is active, the override is cleared. The icon and banner are removed because there is no longer anything to override. +**No keyboard activation.** The Mouse icon, No-Mouse icon, and banner buttons are mouse-only. They are not keyboard-activatable. --- @@ -105,14 +76,11 @@ Selection is available whenever the terminal is handling mouse events — that i ### 3.3 Selection Hint Text -While a drag is in progress, a small hint is displayed adjacent to the selection (below when dragging downward, above when dragging upward): - -- `Hold Alt for block selection` on Windows and Linux. -- `Hold Opt for block selection` on macOS. +While a drag is in progress, a small hint is displayed adjacent to the selection (below when dragging downward, above when dragging upward). The exact hint strings (mouse vs. touch, block-selection, and the URL/path extension hint) live in `lib/src/components/SelectionOverlay.tsx`. The hint is always shown during an active drag. It does not fade with use. -When a URL or path token is detected near the current drag position, an additional extension hint (`Press e to select the full URL` / `Press e to select the full path`) is shown alongside it. See §5 for full details. +When a URL or path token is detected near the current drag position, an additional extension hint is shown alongside it. See §5 for full details. ### 3.4 Selection Follows Content @@ -148,12 +116,7 @@ When a selection is finalized, a popup appears adjacent to the selection (on the ### 4.1 Copy Buttons -The popup shows two copy buttons: - -- `[Cmd+C] Copy Raw` -- `[Cmd+Shift+C] Copy Rewrapped` - -On non-macOS platforms, the labels show `Ctrl` and `Ctrl+Shift` respectively. +Source of truth: `lib/src/components/SelectionPopup.tsx` defines the Copy Raw and Copy Rewrapped buttons and their platform-dependent shortcut labels. #### 4.1.1 Copy Raw @@ -195,21 +158,13 @@ Smart extension is offered **mid-drag**, in parallel with the Alt block-selectio ### 5.1 Detection -A token is whitespace-delimited and matches one of (in priority order, see `lib/src/lib/smart-token.ts`): - -- A URL: `https?://...`, `file://...`. -- An error location: `<path>:line` or `<path>:line:col`. (Matched first so it beats the generic path patterns; trailing `:line` digits are preserved.) -- An absolute path beginning with `~/`, `/`, `./`, or `../`. -- A Windows-style path (`C:\...`). +A token is whitespace-delimited. Source of truth: `PATTERNS` in `lib/src/lib/smart-token.ts` defines the detected shapes in priority order. Error locations (`<path>:line` or `<path>:line:col`) are matched before the generic path patterns, and their trailing `:line` digits are preserved. For all kinds **except** error locations, trailing characters that are unlikely to be part of the token — `.`, `,`, `;`, `:`, `!`, `?`, single quotes, double quotes — are stripped from the detected token's end. Unmatched closing brackets (`)`, `]`, `}`, `>`) are also stripped, but matched pairs are preserved (e.g. `https://en.wikipedia.org/wiki/Foo_(bar)` keeps its trailing `)`). ### 5.2 Mid-Drag Hint -When a qualifying token is detected during a drag, a hint is shown alongside the existing `Hold Alt for block selection` hint: - -- `Press e to select the full URL` (for URLs) -- `Press e to select the full path` (for paths and error locations) +When a qualifying token is detected during a drag, a hint is shown alongside the existing block-selection hint (the exact strings live in `lib/src/components/SelectionOverlay.tsx`). The hint appears and disappears live as the drag moves into and out of qualifying tokens. If no qualifying token is present at the current drag position, no extension hint is shown. diff --git a/docs/specs/terminal-state.md b/docs/specs/terminal-state.md index 81dc1bb0..7386b81a 100644 --- a/docs/specs/terminal-state.md +++ b/docs/specs/terminal-state.md @@ -231,29 +231,9 @@ Disambiguation: ## Grouping -Supported grouping modes are `none`, `directory`, `command`, and `status`. +Source of truth: `groupTerminalPanes()` in `lib/src/lib/terminal-state.ts` defines grouping modes (`TerminalGroupingMode`) and per-mode key derivation (directory uses `cwdAtStart ?? cwd`; command uses the running command's `displayCommand`, else the idle label). -Directory grouping uses: - -```ts -pane.currentCommand?.cwdAtStart ?? pane.cwd -``` - -Command grouping uses: - -```ts -pane.currentCommand?.displayCommand ?? "<idle>" -``` - -Status grouping projects `ShellActivity.kind` (5 values) onto 4 buckets: - -| `pane.activity.kind` | Status bucket | -|---|---| -| `unknown` | `unknown` | -| `prompt` | `idle` | -| `editing` | `idle` | -| `running` | `running` | -| `finished` | `finished` | +Source of truth: `statusBucket()` in `lib/src/lib/terminal-state.ts` projects the 5 `ShellActivity.kind` values onto 4 buckets (prompt+editing collapse to `idle`). `prompt` and `editing` collapse into a single `idle` bucket because the user-visible distinction between "at the prompt" and "typing a command" is not load-bearing for grouping. `finished` stays distinct so a recently-completed pane can be filtered separately even though its header label has the same `<idle>` prefix as plain idle panes. diff --git a/docs/specs/theme.md b/docs/specs/theme.md index 5b339d5d..d598c55a 100644 --- a/docs/specs/theme.md +++ b/docs/specs/theme.md @@ -17,21 +17,13 @@ The chrome is anchored on VSCode's file-tree styling because those colors are designed to read clearly inside the sidebar host area. Use bg-only chrome for panes and doors; do not add borders to make the hierarchy work. -| Token | VSCode key | Where used | -| --- | --- | --- | -| `--color-terminal-bg` / `-fg` | `terminal.background` / `terminal.foreground` | terminal container and xterm defaults | -| `--color-app-bg` / `--color-app-fg` | `sideBar.background` / `sideBar.foreground` | baseboard, dockview gutters, gaps around panes | -| `--color-header-inactive-bg` / `-fg` | `list.inactiveSelectionBackground` / `list.inactiveSelectionForeground` | unfocused pane headers | -| `--color-header-active-bg` / `-fg` | `list.activeSelectionBackground` / `list.activeSelectionForeground` | focused pane header | -| `--color-door-bg` / `-fg` | runtime pick from inactive header vs terminal bg/fg | baseboard doors | -| `--color-focus-ring` | runtime pick from `focusBorder` and active header background | marching-ants ring and terminal text-selection border | - -Door colors and the focus ring are chosen at runtime by -`computeDynamicPalette()` in `lib/src/lib/themes/dynamic-palette.ts`, using -OKLab distance/chroma helpers from `lib/src/lib/color-contrast.ts`. +Source of truth: `lib/src/theme.css` defines token→VSCode-key bindings. The +runtime-picked `--color-door-bg` and `--color-focus-ring` are computed by +`computeDynamicPalette()` in `lib/src/lib/themes/dynamic-palette.ts` using OKLab +distance/chroma helpers from `lib/src/lib/color-contrast.ts`; `useDynamicPalette()` in `lib/src/lib/themes/use-dynamic-palette.ts` publishes -the chosen variables on `document.body`. Public theme helpers are exported -from `lib/src/lib/themes/index.ts`. +the chosen variables on `document.body`. Public theme helpers are exported from +`lib/src/lib/themes/index.ts`. The pick rules: - Door bg/fg chooses whichever pair, inactive-header or terminal bg/fg, has stronger perceptual separation from @@ -135,9 +127,10 @@ Storybook simulates VSCode themes through `lib/.storybook/themes.ts`. It must also run bundled theme vars through `completeThemeVars()` (with the same host typography defaults as `applyTheme()`) before injecting them, so isolated component stories see the same materialized `--vscode-*` token set as the app. -Storybook's default simulated host theme is `Light (Visual Studio)`, with a -first-bundled-theme fallback so a renamed or removed bundle cannot leave stories -without theme vars. +Source of truth: `PREFERRED_STORYBOOK_THEME` in `lib/.storybook/preview.ts` +defines Storybook's default simulated host theme, with fallback to the first +bundled theme so a renamed or removed bundle cannot leave stories without theme +vars. The Storybook preview decorator also computes and publishes the dynamic palette vars (`--color-door-bg`, `--color-door-fg`, `--color-focus-ring`) through the shared `computeDynamicPalette()` helper, matching the runtime diff --git a/docs/specs/transport.md b/docs/specs/transport.md index 980ca208..1f74af28 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -64,92 +64,29 @@ For cold restore (no live PTYs), the webview falls back to saved session state: ## Message protocol -Message types live in `vscode-ext/src/message-types.ts` (the canonical schema; other adapters import or mirror it). The persisted-session types in the next section live in `lib/src/lib/session-types.ts` because they cross the webview/host boundary and are also consumed by frontend persistence helpers. Webview-side handling lives in adapter modules (e.g., `vscode-adapter.ts`, `fake-adapter.ts`); host-side handling lives in the per-adapter message router. - -**Webview → host:** - -| Message | Purpose | -|---------|---------| -| `pty:spawn` | Create new PTY (id, optional cols/rows/cwd/shell/args) | -| `pty:input` | Write data to PTY | -| `pty:resize` | Resize PTY dimensions | -| `pty:kill` | Kill PTY and release ownership | -| `pty:getCwd` | Query PTY working directory (request-response via requestId) | -| `pty:getScrollback` | Query PTY scrollback buffer (request-response via requestId) | -| `pty:getShells` | Query available shells (request-response via requestId) | -| `dormouse:openExternal` | Request the host to open a user-confirmed external URI from an OSC 8 hyperlink. Hosts must revalidate and reject malformed, control-character-bearing, or blocked pseudo-scheme targets (`javascript:`, `data:`, `blob:`, `about:`). | -| `dormouse:init` | Trigger resume: get PTY list + replay data | -| `dormouse:saveState` | Frontend persisting session state | -| `dormouse:flushSessionSaveDone` | Ack for host-triggered flush (matched by requestId) | -| `alert:toggle` | Toggle alert enabled/disabled for a PTY | -| `alert:disable` | Disable alert for a PTY | -| `alert:dismiss` | Dismiss ringing alert | -| `alert:dismissOrToggle` | Context-dependent: dismiss if ringing, else toggle | -| `alert:attend` | Mark user as attending to a PTY | -| `alert:remove` | Remove alert state entirely | -| `alert:resize` | Notify alert of terminal resize (debounce noise) | -| `alert:clearAttention` | Clear attention timer | -| `alert:toggleTodo` | Toggle TODO (`false` ↔ `true`) | -| `alert:markTodo` | Set TODO to `true` | -| `alert:clearTodo` | Remove TODO | - -**Host → webview:** - -| Message | Purpose | -|---------|---------| -| `pty:data` | PTY output after state-driving supported OSC sequences have been parsed/stripped; `OSC 8` hyperlinks are preserved for xterm.js (routed only to owning router) | -| `pty:exit` | PTY process exited (with exitCode) | -| `terminal:semanticEvents` | Normalized CWD/title/prompt/command events parsed in the host from live PTY data | -| `pty:list` | List of all resumable PTYs (response to `dormouse:init`) | -| `pty:replay` | Buffered raw output since spawn (response to `dormouse:init`); the webview parses semantic OSCs during replay reconstruction without triggering alerts | -| `pty:cwd` | CWD query response (matched by requestId) | -| `pty:scrollback` | Scrollback query response (matched by requestId) | -| `pty:shells` | Available shells list response (matched by requestId) | -| `dormouse: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. | -| `dormouse:selectedShell` | Update the webview's default shell options for later split/spawn/restore paths. | -| `dormouse:flushSessionSave` | Request webview to save state now (host shutdown trigger, matched by requestId) | -| `dormouse:openThemeDebugger` | Command-triggered request to open the shared theme debugger dialog | -| `alert:state` | Alert state change (projected status, watchingEnabled, todo, notification, attentionDismissedRing) | +Source of truth: + +| Scope | Source | +| --- | --- | +| Message schema | `vscode-ext/src/message-types.ts` (`WebviewMessage`, `ExtensionMessage`; other adapters import or mirror it) | +| Persisted-session types | `lib/src/lib/session-types.ts` (shared webview/host boundary types) | +| Webview handlers | Adapter modules such as `vscode-adapter.ts` and `fake-adapter.ts` | +| Host handlers | The per-adapter message router | + +Non-obvious message contracts: + +| Direction | Message | Source type | Contract | +| --- | --- | --- | --- | +| Webview → host | `dormouse:openExternal` | `WebviewMessage` | Request the host to open a user-confirmed external URI from an OSC 8 hyperlink. Hosts must revalidate and reject malformed, control-character-bearing, or blocked pseudo-scheme targets (`javascript:`, `data:`, `blob:`, `about:`). | +| Host → webview | `pty:data` | `ExtensionMessage` | PTY output after state-driving supported OSC sequences have been parsed/stripped; `OSC 8` hyperlinks are preserved for xterm.js and routed only to the owning router. | +| Host → webview | `pty:replay` | `ExtensionMessage` | Buffered raw output since spawn; the webview parses semantic OSCs during replay reconstruction without triggering alerts. | +| Host → webview | `dormouse:newTerminal` | `ExtensionMessage` | 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. | The OSC parsing/stripping rules that produce `pty:data` and `terminal:semanticEvents` are specified in `docs/specs/terminal-escapes.md`. ## Persisted session types -```typescript -interface PersistedSession { - version: 3; - panes: PersistedPane[]; - doors?: PersistedDoor[]; - layout: unknown; // SerializedDockview -} - -interface PersistedPane { - id: string; - cwd: string | null; - title: string; - scrollback: string | null; - resumeCommand: string | null; - untouched: boolean; - alert?: PersistedAlertState | null; -} - -interface PersistedAlertState { - status: SessionStatus; - watchingEnabled?: boolean; - todo: boolean; - notification?: ActivityNotification | null; -} - -interface PersistedDoor { - id: string; - title: string; - neighborId: string | null; - direction: DoorDirection; - remainingPaneIds: string[]; - layoutAtMinimize: unknown; - layoutAtMinimizeSignature: string; -} -``` +Source of truth: `lib/src/lib/session-types.ts` defines the persisted-session interfaces (`PersistedSession` v3, `PersistedPane`, `PersistedAlertState`, `PersistedDoor`) and their v1→v2→v3 migrations. Every saved-session entry point must pass through `readPersistedSession()`. That reader accepts both the canonical parsed object and a JSON-stringified session blob before validating/migrating; this covers host state APIs that may hand back the inner serialized JSON string. diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index 845b46c2..b4683d72 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -1,6 +1,6 @@ # Dormouse VS Code Integration Spec -> See `docs/specs/transport.md` for the PTY lifecycle, message protocol, persisted-session types, and adapter-agnostic invariants that VS Code shares with the standalone and fake adapters. This spec covers the VS Code-specific layer: panel/view registration, persistence APIs, theme integration, CSP, build, and dream-architecture commands. +> See `docs/specs/glossary.md` for Session / Pane / Door vocabulary. See `docs/specs/transport.md` for the PTY lifecycle, message protocol, persisted-session types, and adapter-agnostic invariants that VS Code shares with the standalone and fake adapters. This spec covers the VS Code-specific layer: panel/view registration, persistence APIs, theme integration, CSP, build, and dream-architecture commands. ## What's built @@ -74,44 +74,7 @@ Universal PTY/transport invariants live in `docs/specs/transport.md`. The rules ### Extension manifest (current) -```jsonc -{ - "activationEvents": [ - "onView:dormouse.view", - "onWebviewPanel:dormouse" - ], - "contributes": { - "commands": [ - { "command": "dormouse.focus", "title": "Dormouse: Focus", - "icon": { "light": "icon-tiny-light.png", "dark": "icon-tiny-dark.png" } }, - { "command": "dormouse.open", "title": "Dormouse: Open in Editor" }, - { "command": "dormouse.debugTheme", "title": "Dormouse: Debug Theme" }, - { "command": "dormouse.newTerminal", "title": "Dormouse: New Terminal", - "icon": "$(add)" }, - { "command": "dormouse.selectShell", "title": "Dormouse: Select Shell", - "icon": "$(gear)" } - ], - "menus": { - "view/title": [ - { "command": "dormouse.selectShell", "group": "navigation@1", - "when": "view == dormouse.view" }, - { "command": "dormouse.newTerminal", "group": "navigation@2", - "when": "view == dormouse.view" } - ] - }, - "viewsContainers": { - "panel": [ - { "id": "dormouse-panel", "title": "Dormouse", "icon": "$(terminal)" } - ] - }, - "views": { - "dormouse-panel": [ - { "id": "dormouse.view", "name": "Dormouse", "type": "webview" } - ] - } - } -} -``` +Source of truth: `vscode-ext/package.json` defines the activation events and `contributes` block (commands with titles/icons, menus, view container, webview view). ### Webview hosting @@ -189,34 +152,19 @@ theme files. ### CSP policy -``` -default-src 'none'; -style-src ${webview.cspSource} 'unsafe-inline'; -script-src 'nonce-${nonce}'; -font-src ${webview.cspSource}; -img-src ${webview.cspSource} data: blob:; -connect-src ${webview.cspSource}; -``` +Source of truth: `vscode-ext/src/webview-html.ts` assembles the CSP directives (`getNonce()` + the directive list). `unsafe-inline` for styles is needed because VS Code injects theme CSS variables via inline styles on the body element. Scripts remain nonce-gated (32-char random alphanumeric nonce). The webview HTML is built by Vite from the `lib` package, then at runtime `webview-html.ts` rewrites asset URLs to webview URIs, injects the CSP meta tag, applies nonces to all script tags, and injects initial state via a nonce-gated inline script. ### Build and development -``` -pnpm build:vscode = - 1. pnpm --filter dormouse-lib build (TypeScript compile) - 2. pnpm --filter dormouse build:frontend (Vite: lib -> vscode-ext/media/) - 3. pnpm --filter dormouse build (esbuild: extension.ts + pty-host.js -> dist/, - copy node-pty prebuilds -> dist/node-pty) - -pnpm dogfood:vscode = build + package VSIX + install locally - (then: Cmd+Shift+P -> "Developer: Reload Window" to pick up changes) - -F5 in VS Code = launch Extension Development Host (see .vscode/launch.json) - (runs preLaunchTask "build-dormouse-vscode" from .vscode/tasks.json, - which just calls `pnpm build:vscode`, then opens a new VS Code window - with the extension loaded) -``` +Source of truth: + +| Scope | Source | Covers | +| --- | --- | --- | +| Root commands | `package.json` | `pnpm build:vscode`, `pnpm dogfood:vscode` orchestration | +| Extension scripts | `vscode-ext/package.json` | `build:frontend`, `build`, `dogfood` package-local steps | +| F5 launch | `.vscode/launch.json` + `.vscode/tasks.json` | Extension Development Host debugging chain | **Dogfooding vs Extension Development Host:** Day-to-day development uses `pnpm dogfood:vscode` to install the extension into your real VS Code instance. This catches real-world issues since you're running with your actual settings, extensions, and workspaces. The F5 Extension Development Host workflow exists for when you need **breakpoint debugging** of extension host code (`extension.ts`, `message-router.ts`, `pty-manager.ts`, etc.) — it launches a separate VS Code window where the debugger can attach to the extension host process.