diff --git a/.claude/commands/release-notes.md b/.claude/commands/release-notes.md index 7f2a22d9..d6977763 100644 --- a/.claude/commands/release-notes.md +++ b/.claude/commands/release-notes.md @@ -1,10 +1,10 @@ --- name: release-notes -description: Draft release notes, recommend and apply a version bump, and update CHANGELOG.md for the next mouseterm release by analyzing all merge commits and squash-merged PRs since the last release tag. Used as step 2 of the release checklist in docs/specs/deploy.md. +description: Draft release notes, recommend and apply a version bump, and update CHANGELOG.md for the next dormouse release by analyzing all merge commits and squash-merged PRs since the last release tag. Used as step 2 of the release checklist in docs/specs/deploy.md. user-invocable: true --- -You are drafting release notes, recommending and applying a version bump, and updating CHANGELOG.md for the next mouseterm release. +You are drafting release notes, recommending and applying a version bump, and updating CHANGELOG.md for the next dormouse release. ## 1. Gather context @@ -26,7 +26,7 @@ Also read the current version from `standalone/src-tauri/tauri.conf.json` so you ## 2. Decide the version bump -mouseterm uses **breaking.added.bugfix** semantics (semver-shaped, but named for what each segment means here): +dormouse uses **breaking.added.bugfix** semantics (semver-shaped, but named for what each segment means here): - **breaking** (major) โ€” bump if any change breaks behavior users rely on, removes a feature, or changes a VSCode extension contribution point in an incompatible way - **added** (minor) โ€” bump if any change adds a new user-facing feature, with no breaking changes @@ -50,7 +50,7 @@ _Recommended bump: **** โ€” `. For direct-push commits with no PR, link the commit instead: `https://github.com/diffplug/mouseterm/commit/` +- Link the PR using `https://github.com/diffplug/dormouse/pull/`. For direct-push commits with no PR, link the commit instead: `https://github.com/diffplug/dormouse/commit/` - Omit any of Added / Changed / Fixed if it would be empty - Use today's date (`YYYY-MM-DD`) and the recommended `X.Y.Z` diff --git a/.claude/settings.json b/.claude/settings.json index dc70c727..8aeb01f5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,8 +5,8 @@ "Bash(npx tsc:*)", "Bash(magick identify:*)", "Bash(pnpm test:*)", - "Bash(pnpm --filter mouseterm-lib test:*)", - "Bash(pnpm --filter mouseterm-standalone test:*)", + "Bash(pnpm --filter dormouse-lib test:*)", + "Bash(pnpm --filter dormouse-standalone test:*)", "mcp__Claude_Preview__preview_screenshot" ] } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ee9d3a61..9e4197f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,7 +80,7 @@ jobs: with: name: ${{ matrix.artifact-name }} path: | - standalone/src-tauri/target/${{ matrix.target }}/release/mouseterm.exe + standalone/src-tauri/target/${{ matrix.target }}/release/dormouse.exe standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.exe standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.msi standalone/src-tauri/target/${{ matrix.target }}/release/bundle/**/*.dmg @@ -109,13 +109,13 @@ jobs: run: pnpm install --frozen-lockfile - name: Test lib - run: pnpm --filter mouseterm-lib test + run: pnpm --filter dormouse-lib test - name: Build frontend for VSCode - run: pnpm --filter mouseterm build:frontend + run: pnpm --filter dormouse build:frontend - name: Build extension - run: pnpm --filter mouseterm build + run: pnpm --filter dormouse build - name: Package extension run: cd vscode-ext && npx vsce package --no-dependencies diff --git a/.impeccable/design.json b/.impeccable/design.json index 57df9e30..e1e2c7a4 100644 --- a/.impeccable/design.json +++ b/.impeccable/design.json @@ -1,7 +1,7 @@ { "schemaVersion": 2, "generatedAt": "2026-05-11T00:00:00Z", - "title": "Design System: MouseTerm", + "title": "Design System: Dormouse", "extensions": { "colorMeta": { "app-bg": { @@ -236,7 +236,7 @@ ], "narrative": { "northStar": "The Native Tenant", - "overview": "MouseTerm is a tenant in someone else's house. The house is VSCode. The user picked the furniture (their theme), the lighting (their mode), the typography (their editor font). MouseTerm moves in, multiplies what the user can do with their terminals, and leaves the decor alone. The system is intentionally minimal and bg-only. Chrome recedes; terminals are the content. Hierarchy is conveyed through background shifts between header-active-bg and header-inactive-bg, not through borders, shadows, or accent stripes. Status is conveyed through shape and position and through the active terminal palette's own ANSI red/green/yellow, not through a separate design-system palette.", + "overview": "Dormouse is a tenant in someone else's house. The house is VSCode. The user picked the furniture (their theme), the lighting (their mode), the typography (their editor font). Dormouse moves in, multiplies what the user can do with their terminals, and leaves the decor alone. The system is intentionally minimal and bg-only. Chrome recedes; terminals are the content. Hierarchy is conveyed through background shifts between header-active-bg and header-inactive-bg, not through borders, shadows, or accent stripes. Status is conveyed through shape and position and through the active terminal palette's own ANSI red/green/yellow, not through a separate design-system palette.", "keyCharacteristics": [ "Host-theme-driven palette: every color is a var(--vscode-*) passthrough.", "Bg-only chrome: no decorative borders, no resting shadows, no accent stripes.", diff --git a/.vscode/launch.json b/.vscode/launch.json index be3e16f1..819afd94 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,12 +2,12 @@ "version": "0.2.0", "configurations": [ { - "name": "MouseTerm Extension", + "name": "Dormouse Extension", "type": "extensionHost", "request": "launch", "args": ["--extensionDevelopmentPath=${workspaceFolder}/vscode-ext"], "outFiles": ["${workspaceFolder}/vscode-ext/dist/**/*.js"], - "preLaunchTask": "build-mouseterm-vscode" + "preLaunchTask": "build-dormouse-vscode" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index f4441bb6..10e60b44 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,7 +2,7 @@ "version": "2.0.0", "tasks": [ { - "label": "build-mouseterm-vscode", + "label": "build-dormouse-vscode", "type": "shell", "command": "pnpm build:vscode", "group": "build", diff --git a/AGENTS.md b/AGENTS.md index 1962f1a1..f7128ad0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,4 +1,4 @@ -# MouseTerm +# Dormouse A mouse-friendly multitasking terminal built with pnpm, react, typescript, vite, tailwind, storybook, and xterm.js. @@ -35,9 +35,9 @@ The primary job of a spec is to be an accurate reference for the current state o - **`docs/specs/alert.md`** โ€” Activity monitoring state machine, alert trigger/clearing rules, attention model, TODO lifecycle, bell button visual states and interaction, door alert indicators, hardening (a11y, motion, i18n, overflow), notification protocols (`OSC 9` / `OSC 9;4` / `OSC 99` / `OSC 777` / `BEL`), the `ActivityNotification` model, notification text handling and security, and the notification preview/detail UI. Read this when touching: `activity-monitor.ts`, `alert-manager.ts`, `AlertManager` notification/progress paths, the alert bell or TODO pill in `Wall.tsx` (TerminalPaneHeader), alert indicators in `Door.tsx`, the `a`/`t` keyboard shortcuts, or TODO notification preview UI. Layout.md defers to this spec for all alert/TODO behavior. - **`docs/specs/terminal-state.md`** โ€” Terminal semantic state for CWD, shell prompt/editing/running/finished lifecycle, command runs, terminal title fallback, normalized semantic OSC events (`OSC 7`, `OSC 9;9`, `OSC 133`, `OSC 633`, `OSC 1337`, `OSC 0/2`), title-candidate diagnostics, header derivation, and grouping keys. Read this when touching `terminal-state.ts`, `terminal-state-store.ts`, semantic event parsing in `terminal-protocol.ts`, adapter semantic event forwarding, or derived pane/door labels. - **`docs/specs/OSC.md`** โ€” Registry of every supported OSC sequence with pointers to the spec defining its behavior (alert.md or terminal-state.md), the canonical parsing-location and `pty:data` strip semantics, iTerm2 self-identification (env vars, `CSI > q` response, fail-inertly rule), and known-unimplemented iTerm2 and clipboard-capable sequences. Read this when touching: OSC parsing at the PTY data boundary, the iTerm2 identity env vars (`TERM_PROGRAM`, `LC_TERMINAL`), or adding support for a new OSC sequence. -- **`docs/specs/transport.md`** โ€” Adapter-agnostic protocol shared across VS Code, standalone, and fake adapters: PTY lifecycle (decoupled from webview), `replayChunks`/`scrollbackChunks` buffering, reconnection sequence (`mouseterm:init` โ†’ `pty:list` + `pty:replay`), the full webview โ†” host message protocol, persisted-session types, and universal invariants (shell login args, scrollback trailing newline, replay drop-replies-only). Read this when touching: `pty-manager.ts`, `pty-host.js`, `pty-core.js`, `message-router.ts`, `message-types.ts`, `vscode-adapter.ts`, `fake-adapter.ts`, `reconnect.ts`, `session-save.ts`, `session-restore.ts`, `session-types.ts`, or any code crossing the webview/host boundary. +- **`docs/specs/transport.md`** โ€” Adapter-agnostic protocol shared across VS Code, standalone, and fake adapters: PTY lifecycle (decoupled from webview), `replayChunks`/`scrollbackChunks` buffering, reconnection sequence (`dormouse:init` โ†’ `pty:list` + `pty:replay`), the full webview โ†” host message protocol, persisted-session types, and universal invariants (shell login args, scrollback trailing newline, replay drop-replies-only). Read this when touching: `pty-manager.ts`, `pty-host.js`, `pty-core.js`, `message-router.ts`, `message-types.ts`, `vscode-adapter.ts`, `fake-adapter.ts`, `reconnect.ts`, `session-save.ts`, `session-restore.ts`, `session-types.ts`, or any code crossing the webview/host boundary. - **`docs/specs/vscode.md`** โ€” VS Code-specific layer: hosting modes (WebviewView + WebviewPanel), extension manifest, VS Code persistence flow (`workspaceState`, `vscode.setState`, `WebviewPanelSerializer`, deactivate ordering, `mergeAlertStates` rule, `retainContextWhenHidden`), theme integration (`--vscode-*` โ†’ `--color-*` with the runtime resolver), CSP, build pipeline, and dream-architecture commands. The transport protocol it speaks (PTY lifecycle, message protocol, persisted-session types) lives in `transport.md`. Read this when touching: `extension.ts`, `webview-view-provider.ts`, `session-state.ts`, `webview-html.ts`, the theme resolver/observer in `terminal-theme.ts`, or VS Code commands and context keys. -- **`docs/specs/tutorial.md`** โ€” Playground tutorial on the website: 3-pane layout, interactive `tut` TUI runner with three sections (keyboard navigation, alerts/TODOs, copy/paste), per-item detection wired to `WallEvent` / activity store / mouse-selection store, single-key `mouseterm-tut-v3` localStorage scheme, theme picker, and FakePtyAdapter extensions (`sendOutput`, `pumpActivity`, `setInputHandler`). Read this when touching: `website/src/pages/Playground.tsx`, `website/src/lib/tut-runner.ts`, `website/src/lib/tut-detector.ts`, `website/src/lib/tutorial-state.ts`, `website/src/lib/tut-items.ts`, `website/src/lib/tutorial-shell.ts`, `lib/src/components/ThemePicker.tsx`, `lib/src/lib/themes/`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), the `WallEvent` union, or the `onApiReady`/`onEvent`/`initialPaneIds` props on Wall. +- **`docs/specs/tutorial.md`** โ€” Playground tutorial on the website: 3-pane layout, interactive `tut` TUI runner with three sections (keyboard navigation, alerts/TODOs, copy/paste), per-item detection wired to `WallEvent` / activity store / mouse-selection store, single-key `dormouse-tut-v3` localStorage scheme, theme picker, and FakePtyAdapter extensions (`sendOutput`, `pumpActivity`, `setInputHandler`). Read this when touching: `website/src/pages/Playground.tsx`, `website/src/lib/tut-runner.ts`, `website/src/lib/tut-detector.ts`, `website/src/lib/tutorial-state.ts`, `website/src/lib/tut-items.ts`, `website/src/lib/tutorial-shell.ts`, `lib/src/components/ThemePicker.tsx`, `lib/src/lib/themes/`, `lib/src/lib/platform/fake-scenarios.ts` (tutorial scenarios), the `WallEvent` union, or the `onApiReady`/`onEvent`/`initialPaneIds` props on Wall. - **`docs/specs/theme.md`** โ€” Theme system: two-layer CSS variable strategy, theme data model, conversion pipeline, bundled themes, localStorage store, shared ThemePicker component, standalone AppBar picker, runtime OpenVSX installer. Read this when touching: `lib/src/lib/themes/`, `lib/src/components/ThemePicker.tsx`, `lib/src/theme.css`, `lib/scripts/bundle-themes.mjs`, `standalone/src/AppBar.tsx` (theme picker), `standalone/src/main.tsx` (theme restore), or `website/src/components/SiteHeader.tsx` (themeAware mode). - **`docs/specs/mouse-and-clipboard.md`** โ€” Terminal-owned text selection, copy (Raw / Rewrapped), bracketed paste, smart URL/path extension, mouse-reporting override UI (icon + banner), and the state matrix for which layer owns mouse events. Read this when touching: `lib/src/lib/mouse-selection.ts`, `lib/src/lib/mouse-mode-observer.ts`, `lib/src/lib/clipboard.ts`, `lib/src/lib/rewrap.ts`, `lib/src/lib/selection-text.ts`, `lib/src/lib/smart-token.ts`, `lib/src/components/SelectionOverlay.tsx`, `lib/src/components/SelectionPopup.tsx`, the mouse icon / override banner / Cmd+C-V handling in `lib/src/components/Wall.tsx`, or the parser hooks + mouse listeners in `lib/src/lib/terminal-registry.ts`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 940f14a1..c748a14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,24 +10,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/). Release ## [0.9.1] - 2026-05-01 ### Changed -- ๐Ÿ–ฅ๏ธ Drop-to-paste from the OS file explorer is temporarily inert on standalone while we wait on upstream Tauri ([tauri#14373](https://github.com/tauri-apps/tauri/issues/14373)) to allow native drag-drop without blocking HTML5 drag events ([#39](https://github.com/diffplug/mouseterm/pull/39)). +- ๐Ÿ–ฅ๏ธ Drop-to-paste from the OS file explorer is temporarily inert on standalone while we wait on upstream Tauri ([tauri#14373](https://github.com/tauri-apps/tauri/issues/14373)) to allow native drag-drop without blocking HTML5 drag events ([#39](https://github.com/diffplug/dormouse/pull/39)). ### Fixed -- The mouse-override banner now renders inline in the terminal pane body and no longer stacks with the action-button tooltip ([#43](https://github.com/diffplug/mouseterm/pull/43)). -- Themes with translucent selection backgrounds (e.g. Selenized Dark) no longer bleed through MouseTerm's solid AppBar and tab fills ([#37](https://github.com/diffplug/mouseterm/pull/37)). -- ๐Ÿ–ฅ๏ธ Force-closing the standalone host now reliably kills the Node sidecar tree via a Windows Job Object / Unix process group, so subsequent builds no longer hit orphan `node.exe` processes locking files ([#41](https://github.com/diffplug/mouseterm/pull/41)). -- ๐Ÿ–ฅ๏ธ Standalone macOS terminals run zsh as a login shell when no args are provided, so `~/.zprofile` runs and Homebrew/asdf land on `PATH` ([#40](https://github.com/diffplug/mouseterm/pull/40)). -- ๐Ÿ–ฅ๏ธ Pane drag-and-drop reordering works again on standalone ([#39](https://github.com/diffplug/mouseterm/pull/39)). +- The mouse-override banner now renders inline in the terminal pane body and no longer stacks with the action-button tooltip ([#43](https://github.com/diffplug/dormouse/pull/43)). +- Themes with translucent selection backgrounds (e.g. Selenized Dark) no longer bleed through MouseTerm's solid AppBar and tab fills ([#37](https://github.com/diffplug/dormouse/pull/37)). +- ๐Ÿ–ฅ๏ธ Force-closing the standalone host now reliably kills the Node sidecar tree via a Windows Job Object / Unix process group, so subsequent builds no longer hit orphan `node.exe` processes locking files ([#41](https://github.com/diffplug/dormouse/pull/41)). +- ๐Ÿ–ฅ๏ธ Standalone macOS terminals run zsh as a login shell when no args are provided, so `~/.zprofile` runs and Homebrew/asdf land on `PATH` ([#40](https://github.com/diffplug/dormouse/pull/40)). +- ๐Ÿ–ฅ๏ธ Pane drag-and-drop reordering works again on standalone ([#39](https://github.com/diffplug/dormouse/pull/39)). ## [0.9.0] - 2026-04-30 ### Added -- ๐Ÿ–ฅ๏ธ Debug dialog for failed auto-updates โ€” surfaces the error and copies a pre-filled bug report (version, platform, last ~10 KB of `mouseterm.log`) ([#35](https://github.com/diffplug/mouseterm/pull/35)). +- ๐Ÿ–ฅ๏ธ Debug dialog for failed auto-updates โ€” surfaces the error and copies a pre-filled bug report (version, platform, last ~10 KB of `mouseterm.log`) ([#35](https://github.com/diffplug/dormouse/pull/35)). ### Fixed -- Terminals auto-spawned from a blank workspace now respect the selected shell ([#33](https://github.com/diffplug/mouseterm/pull/33)). -- ๐Ÿ–ฅ๏ธ Polish app bar header to align with pane chrome and shared design tokens ([#34](https://github.com/diffplug/mouseterm/pull/34)). -- ๐Ÿ–ฅ๏ธ macOS auto-update โ€” strip AppleDouble (`._*`) sidecars from the signed tarball that were breaking every v0.7.x โ†’ v0.8.0 install ([#35](https://github.com/diffplug/mouseterm/pull/35)). +- Terminals auto-spawned from a blank workspace now respect the selected shell ([#33](https://github.com/diffplug/dormouse/pull/33)). +- ๐Ÿ–ฅ๏ธ Polish app bar header to align with pane chrome and shared design tokens ([#34](https://github.com/diffplug/dormouse/pull/34)). +- ๐Ÿ–ฅ๏ธ macOS auto-update โ€” strip AppleDouble (`._*`) sidecars from the signed tarball that were breaking every v0.7.x โ†’ v0.8.0 install ([#35](https://github.com/diffplug/dormouse/pull/35)). ## [0.8.0] - 2026-04-29 - Add intuitive shortcuts alongside the tmux shortcuts. diff --git a/DESIGN.md b/DESIGN.md index b22e51f9..d75d379c 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,5 +1,5 @@ --- -name: MouseTerm +name: Dormouse description: A mouse-friendly multitasking terminal that feels native inside VSCode. colors: app-bg: "var(--vscode-sideBar-background)" @@ -85,13 +85,13 @@ components: padding: "16px 24px" --- -# Design System: MouseTerm +# Design System: Dormouse ## 1. Overview **Creative North Star: "The Native Tenant"** -MouseTerm is a tenant in someone else's house. The house is VSCode. The user picked the furniture (their theme), the lighting (their mode), the typography (their editor font). MouseTerm moves in, multiplies what the user can do with their terminals, and leaves the decor alone. The interface should be indistinguishable from a built-in panel: not because it imitates VSCode, but because it inherits from VSCode. Every color, every font, every surface is a passthrough of the host's tokens. +Dormouse is a tenant in someone else's house. The house is VSCode. The user picked the furniture (their theme), the lighting (their mode), the typography (their editor font). Dormouse moves in, multiplies what the user can do with their terminals, and leaves the decor alone. The interface should be indistinguishable from a built-in panel: not because it imitates VSCode, but because it inherits from VSCode. Every color, every font, every surface is a passthrough of the host's tokens. The system is intentionally minimal and bg-only. Chrome recedes; terminals are the content. Hierarchy is conveyed through background shifts between `header-active-bg` and `header-inactive-bg`, not through borders, shadows, or accent stripes. Status is conveyed through shape and position (a bell icon, a door's alert state) and through the active terminal palette's own ANSI red/green/yellow, not through a separate design-system palette. @@ -107,7 +107,7 @@ The system explicitly rejects: rounded SaaS cards, gradient accents, hacker-aest ## 2. Colors -The palette has no fixed values. Every semantic token resolves to a `--vscode-*` variable at runtime. Inside VSCode, those variables are injected by the host. Outside VSCode (standalone, website playground), `applyTheme()` materializes the same variable shape on `document.body` from a bundled MouseTerm theme. +The palette has no fixed values. Every semantic token resolves to a `--vscode-*` variable at runtime. Inside VSCode, those variables are injected by the host. Outside VSCode (standalone, website playground), `applyTheme()` materializes the same variable shape on `document.body` from a bundled Dormouse theme. ### Primary This system has no "primary" accent in the brand sense. The closest analogue is the **focused-header pair**: @@ -152,7 +152,7 @@ This system has no "primary" accent in the brand sense. The closest analogue is **Body Font:** `var(--vscode-editor-font-family)`. **Label/Mono Font:** same as body. Sans and mono resolve to the same VSCode editor font. -**Character:** monospace, the user's own editor face. The system has no opinion about Cascadia vs. SF Mono vs. JetBrains Mono vs. Fira Code; whatever is set in the editor is what MouseTerm uses, including ligature settings. This is the typographic equivalent of the host-theme rule. +**Character:** monospace, the user's own editor face. The system has no opinion about Cascadia vs. SF Mono vs. JetBrains Mono vs. Fira Code; whatever is set in the editor is what Dormouse uses, including ligature settings. This is the typographic equivalent of the host-theme rule. ### Hierarchy - **Body** (weight 500, `text-sm` = 0.75rem / 12px, line-height 1rem): pane headers, doors, popup contents, button labels. The single most-used step. diff --git a/PRODUCT.md b/PRODUCT.md index ad907a8c..41a03e61 100644 --- a/PRODUCT.md +++ b/PRODUCT.md @@ -10,11 +10,11 @@ Developers ranging from terminal beginners (non-developers using Claude Code for ### Brand Personality **Focused. Approachable. Capable.** -MouseTerm should feel like focused efficiency that cares about beginners and onboarding, without sacrificing anything that even the most extreme power user might want eventually. The interface should communicate: "everything is under control" โ€” no clutter, no distraction, just the information you need when you need it. +Dormouse should feel like focused efficiency that cares about beginners and onboarding, without sacrificing anything that even the most extreme power user might want eventually. The interface should communicate: "everything is under control" โ€” no clutter, no distraction, just the information you need when you need it. ### Aesthetic Direction -**Primary constraint: Feel native inside VSCode.** The current Catppuccin Mocha design is throwaway โ€” built to get things running. The first design priority is making MouseTerm feel completely native within VSCode, respecting whatever theme the user has chosen. This means: +**Primary constraint: Feel native inside VSCode.** The current Catppuccin Mocha design is throwaway โ€” built to get things running. The first design priority is making Dormouse feel completely native within VSCode, respecting whatever theme the user has chosen. This means: - Use VSCode's CSS variables and theme tokens, not hardcoded colors - Match VSCode's spacing, typography, and interaction patterns - Light mode and dark mode support from the start (inherited from user's VSCode theme) @@ -23,7 +23,7 @@ MouseTerm should feel like focused efficiency that cares about beginners and onb **After VSCode-native is achieved**, figure out the standalone terminal's visual identity separately. **References:** -- VSCode itself โ€” the gold standard for how MouseTerm should feel as an extension +- VSCode itself โ€” the gold standard for how Dormouse should feel as an extension - The tool should feel like a natural part of the editor, not a foreign embed **Anti-references:** @@ -34,7 +34,7 @@ MouseTerm should feel like focused efficiency that cares about beginners and onb ### Design Principles -1. **Native first** โ€” Inside VSCode, MouseTerm should be indistinguishable from a built-in feature. Use the host's theme tokens, spacing, and conventions. Never fight the environment. +1. **Native first** โ€” Inside VSCode, Dormouse should be indistinguishable from a built-in feature. Use the host's theme tokens, spacing, and conventions. Never fight the environment. 2. **Information density without intimidation** โ€” Power users want dense layouts with many terminals visible. Beginners need to not feel overwhelmed. Solve this with progressive disclosure: simple by default, powerful when you explore. diff --git a/README.md b/README.md index c253c524..c4f5ccc4 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,20 @@ -# MouseTerm - -**Multitasking terminal for the mouse, tmux-compatible.** - -Run multiple terminals side-by-side, click to split, drag to resize. -When a pane stops outputting for two seconds, it's marked done โ€” works -with any CLI tool, no plugins or config. - -![MouseTerm hero](website/src/assets/video-climb-blink-and-stare.webp) +![Dormouse โ€” Multitasking Terminal for Mice](website/public/og-image.jpg) ## Try it -- **[Playground](https://mouseterm.com/playground)** โ€” try in your browser, no install -- **[Demo videos and downloads](https://mouseterm.com)** โ€” Mac, Windows, Linux -- **[Marketplace](https://marketplace.visualstudio.com/items?itemName=diffplug.mouseterm)** / **[Open VSX](https://open-vsx.org/extension/diffplug/mouseterm)** โ€” VS Code extension (also works in Cursor, Windsurf, Antigravity) +- **[Playground](https://dormouse.sh/playground)** - try in your browser, no install +- **[VS Code Marketplace](https://marketplace.visualstudio.com/items?itemName=diffplug.dormouse)** / **[Open VSX](https://open-vsx.org/extension/diffplug/dormouse)** - works in VS Code and its forks +- **[Standalone app](https://dormouse.sh#downloads)** - Mac, Windows, Linux ## Features -- **Automatic completion detection.** When a pane goes quiet for two seconds, it's marked done. Works with builds, AI agents, scripts, anything. +- **Automatic completion detection.** Detect when an agent needs your attention with standard terminal .,mn,.mn.,mnWhen a pane goes quiet for two seconds, it's marked done. Works with builds, AI agents, scripts, anything. - **tmux-compatible keybindings.** Same prefix, same splits, same pane navigation. Muscle memory transfers. - **Full mouse support.** Click to split, drag to resize, scroll to navigate. Or stay on the keyboard. - **Copy-paste that works.** Click and drag selects text the way you'd expect, even in mouse-aware TUIs that normally swallow it as escape codes. Ctrl+C copies; killing the program is a separate gesture. - **Sleep/wake panes.** Minimize a terminal to a compact status indicator. It keeps running and you can still see whether its task finished. - **Dual distribution.** Standalone desktop app (Mac/Windows/Linux) or VS Code extension. +- **Pocket (coming soon).** Tether your sessions to your phone over WebRTC โ€” walk away, keep working. ## Development @@ -49,7 +42,7 @@ pnpm test # runs all tests | Path | Description | |------|-------------| | `lib/` | Shared terminal library | -| `website/` | mouseterm.com (including playground) | +| `website/` | dormouse.sh (including playground) | | `standalone/` | Tauri desktop app | | `vscode-ext/` | VSCode extension | @@ -61,4 +54,4 @@ This project was built with a combination of Claude, Codex, and Devin. Recommend [FSL-1.1-MIT](LICENSE) โ€” Copyright 2026 DiffPlug LLC -[Production dependencies](https://mouseterm.com/dependencies) +[Production dependencies](https://dormouse.sh/dependencies) diff --git a/docs/specs/OSC.md b/docs/specs/OSC.md index e30281a7..c85fa51b 100644 --- a/docs/specs/OSC.md +++ b/docs/specs/OSC.md @@ -1,10 +1,10 @@ # OSC Sequence Registry -> Single registry of OSC sequences MouseTerm parses. Behavioral details live in `docs/specs/alert.md` (notifications) and `docs/specs/terminal-state.md` (CWD, prompt/command, title fallback). This file also documents iTerm2 self-identification because the same identity is what causes most of these sequences to be emitted at us. +> Single registry of OSC sequences Dormouse parses. Behavioral details live in `docs/specs/alert.md` (notifications) and `docs/specs/terminal-state.md` (CWD, prompt/command, title fallback). This file also documents iTerm2 self-identification because the same identity is what causes most of these sequences to be emitted at us. ## Goal -MouseTerm parses a small set of OSC (Operating System Command) escape sequences from PTY output to drive alerts, terminal state, and titles. This document is the index โ€” every supported OSC has one row in the table below pointing to the spec that defines its full behavior. +Dormouse parses a small set of OSC (Operating System Command) escape sequences from PTY output to drive alerts, terminal state, and titles. This document is the index โ€” every supported OSC has one row in the table below pointing to the spec that defines its full behavior. ## Parsing location @@ -28,7 +28,7 @@ The parser also classifies each PTY data chunk for activity-monitor purposes: - A chunk that contains only notification/progress OSCs after parsing must not be fed to the activity monitor's `onData()` as generic meaningful output. - A chunk that contains visible output plus notification/progress OSCs still counts visible output as activity. -Unknown non-iTerm2 OSC families pass through to xterm.js unchanged so xterm.js can handle standard terminal behavior MouseTerm does not model. Security-sensitive or iTerm2-identity-triggered OSCs must not rely on xterm.js defaults: if they are not in [Supported OSCs](#supported-oscs), MouseTerm consumes and ignores them without visible terminal garbage, clipboard access, file access, focus changes, or other side effects. +Unknown non-iTerm2 OSC families pass through to xterm.js unchanged so xterm.js can handle standard terminal behavior Dormouse does not model. Security-sensitive or iTerm2-identity-triggered OSCs must not rely on xterm.js defaults: if they are not in [Supported OSCs](#supported-oscs), Dormouse consumes and ignores them without visible terminal garbage, clipboard access, file access, focus changes, or other side effects. ## Supported OSCs @@ -53,14 +53,14 @@ Some sequences are dual-purpose. The notification rows for `OSC 9 ; ST ## iTerm2 identity -MouseTerm reports an iTerm2-compatible identity so that tools (shells, build systems, agent clients) emit the iTerm2-style escape codes that this spec set supports. +Dormouse reports an iTerm2-compatible identity so that tools (shells, build systems, agent clients) emit the iTerm2-style escape codes that this spec set supports. Environment for spawned PTYs: | Variable | Value | |---|---| | `TERM_PROGRAM` | `iTerm.app` | -| `TERM_PROGRAM_VERSION` | MouseTerm's chosen iTerm2 compatibility version, not the package version | +| `TERM_PROGRAM_VERSION` | Dormouse's chosen iTerm2 compatibility version, not the package version | | `LC_TERMINAL` | `iTerm2` only if needed by real-world shell integrations | | `LC_TERMINAL_VERSION` | same compatibility version as `TERM_PROGRAM_VERSION` | @@ -70,16 +70,16 @@ Device/version query: - Use a single compatibility version across env and device responses. - Do not advertise feature-specific support until the relevant behavior exists. -Because this identity can cause tools to emit more iTerm2 escape codes than MouseTerm implements, **unsupported escape codes must fail inertly**: consume or ignore them without visible terminal garbage, privilege escalation, clipboard access, file access, or focus stealing. +Because this identity can cause tools to emit more iTerm2 escape codes than Dormouse implements, **unsupported escape codes must fail inertly**: consume or ignore them without visible terminal garbage, privilege escalation, clipboard access, file access, or focus stealing. ## Known-unimplemented iTerm2 and clipboard-capable sequences -MouseTerm intentionally does not implement the following sequences. They are mostly iTerm2-proprietary; `OSC 50` (font) and `OSC 52` (clipboard) are standard xterm extensions included here because the iTerm2 identity prompts tools to emit them and they have security implications. All of them must fail inertly per the rule above, which means they are consumed/ignored rather than forwarded to xterm.js. +Dormouse intentionally does not implement the following sequences. They are mostly iTerm2-proprietary; `OSC 50` (font) and `OSC 52` (clipboard) are standard xterm extensions included here because the iTerm2 identity prompts tools to emit them and they have security implications. All of them must fail inertly per the rule above, which means they are consumed/ignored rather than forwarded to xterm.js. | Sequence | Purpose | Reason for non-support | |---|---|---| -| `OSC 1337 ; SetMark` | Pin a navigable scrollback mark | No mark UI in MouseTerm. | -| `OSC 1337 ; CursorShape=...` | Cursor shape override | Cursor shape comes from MouseTerm settings, not the PTY. | +| `OSC 1337 ; SetMark` | Pin a navigable scrollback mark | No mark UI in Dormouse. | +| `OSC 1337 ; CursorShape=...` | Cursor shape override | Cursor shape comes from Dormouse settings, not the PTY. | | `OSC 1337 ; SetBadgeFormat=...` | Display a badge string in the terminal | No badge UI. | | `OSC 1337 ; ClearScrollback` | Clear scrollback buffer | xterm.js handles native clear-screen sequences. | | `OSC 1337 ; CopyToClipboard=...` / `EndCopy` | Programmatic clipboard write | Security: untrusted PTY output cannot write the user's clipboard. See `docs/specs/mouse-and-clipboard.md`. | @@ -89,7 +89,7 @@ MouseTerm intentionally does not implement the following sequences. They are mos | `OSC 50 ; ST` | Set font dynamically | Font is host-controlled. | | `OSC 52 ; ; ST` | Programmatic clipboard write | Security: same rationale as `CopyToClipboard`. | -This list is non-exhaustive. Any iTerm2-compatibility OSC family that MouseTerm can identify and that is not in the [Supported OSCs](#supported-oscs) table is ignored. +This list is non-exhaustive. Any iTerm2-compatibility OSC family that Dormouse can identify and that is not in the [Supported OSCs](#supported-oscs) table is ignored. ## References diff --git a/docs/specs/alert.md b/docs/specs/alert.md index 0c23b3d5..e7016243 100644 --- a/docs/specs/alert.md +++ b/docs/specs/alert.md @@ -1,900 +1,334 @@ # Alert Spec -## Goal +Alert state belongs to the **Session** Activity layer. It survives Pane <-> Door movement and is destroyed with the Session. -The alert system is a reminder for a **Session** that may finish work while the user is looking elsewhere. Alert state lives on the Session itself, not on the Pane or Door that currently displays it. +Dormouse can owe the user attention in three ways: -There are three independent ways a Session can become alert-worthy: +- **WATCHING**: the user enabled the timer-based output monitor, output became busy, then went quiet while the user was not attending the Session. +- **Terminal report**: the PTY emitted a supported notification or progress protocol (`BEL`, `OSC 9`, `OSC 9;4`, `OSC 99`, or `OSC 777`). +- **Command exit**: Dormouse saw a foreground command running while the user attended the Session, attention was lost while that same command was still running, and the command exited after at least `T_USER_ATTENTION`. -- **WATCHING**: the user explicitly enables MouseTerm's timer-based watcher, output motion becomes busy, then motion stops while the Session lacks attention. -- **Explicit protocol notification**: the PTY emits a supported terminal notification/progress report (`OSC 9`, `OSC 9;4`, `OSC 99`, `OSC 777`, or standalone `BEL`) while the Session lacks attention. -- **Command exit after attention loss**: MouseTerm observes a foreground command running while the Session has attention, attention later expires or is explicitly lost, and that same command exits after running for at least `T_USER_ATTENTION`. +Terminal-report and command-exit alerts do not require WATCHING to be enabled. All three paths share the same attention suppression rule: do not ring if the user is actively attending that Session at the completion moment. -Protocol notification/progress reports and command-exit alerts are not controlled by the WATCHING toggle. The OSC sequence registry and parsing-location rules live in `docs/specs/OSC.md`; command lifecycle state comes from `docs/specs/terminal-state.md`. +## Non-goals -This spec uses semantic state names that describe what the Session currently owes the user: +- No command/process heuristics. Dormouse does not guess that `vim`, `npm dev`, agents, or test runners deserve special alert behavior. +- No sound, native OS notifications, browser notifications, or separate progress-bar widget. +- No process-tree introspection for command-exit alerts; normalized terminal semantic events are the reliable input. +- No HTML, Markdown, ANSI styling, clickable actions, custom icons, or remote-controlled buttons in notification previews. +- No Door-specific alert menu that changes the Door actions defined in `docs/specs/layout.md`. -- `NOTHING_TO_SHOW` -- `MIGHT_BE_BUSY` -- `BUSY` -- `OSC_NOTIF_BUSY` -- `COMMAND_EXIT_ARMED` -- `MIGHT_NEED_ATTENTION` -- `ALERT_RINGING` +## Public State -This document is the source of truth for the naming and behavior of this state machine. +The public Activity state is: -## Non-goals +```ts +type WatchingSessionStatus = + | 'WATCHING_DISABLED' + | 'NOTHING_TO_SHOW' + | 'MIGHT_BE_BUSY' + | 'BUSY' + | 'MIGHT_NEED_ATTENTION' + | 'ALERT_RINGING'; -- No per-tool allow/deny heuristics. We do not try to guess whether `vim`, `npm dev`, `claude`, or any other command is "appropriate" for alerts. -- No sound, native OS notifications, or browser notifications in v1. "Alarm" means MouseTerm's existing `ALERT_RINGING` visual state. -- No standalone progress bar widget. `OSC 9;4` progress updates `protocolStatus` while active; completion/error creates TODO detail. It does not add a separate progress widget to the Pane header. -- No process-tree introspection for command-exit alerts in v1. Shell integration (`OSC 133` / `OSC 633`) is the reliable path. Heuristic user-input/prompt fallback may be used as a best-effort source, but deeper shell integration remains an open TODO. -- No full iTerm2/kitty/rxvt/WezTerm feature parity. Unsupported sequences are ignored unless another spec claims them. -- No HTML, Markdown, ANSI styling, shell command parsing, or clickable action buttons inside TODO notification previews. -- No Door-specific alert menu that overrides the existing click-to-reattach behavior from `docs/specs/layout.md`. - -## When alerts are useful - -Alerts are most useful for sessions such as: - -- long-running jobs that eventually finish, such as signing, notarization, deploys, or test runs -- slow human-in-the-loop sessions, such as AI chats where the user may switch to other work - -Alerts are usually not useful for sessions such as: - -- continuous background output, such as `npm dev` -- fast local interactive tools where the user is already present -- read-only streams that the user expects to keep changing forever - -This is guidance only. The system does not auto-enable or auto-disable alerts based on process name, shell command, exit code, or output patterns. - -## Data model - -Each Session owns: - -- `status: 'WATCHING_DISABLED' | 'NOTHING_TO_SHOW' | 'MIGHT_BE_BUSY' | 'BUSY' | 'OSC_NOTIF_BUSY' | 'COMMAND_EXIT_ARMED' | 'MIGHT_NEED_ATTENTION' | 'ALERT_RINGING'` - - This is the public projected alert and activity state for the Session. - - `WATCHING_DISABLED`: WATCHING is off and no stronger protocol or command-exit state is active. Default state. - - Stable states: `WATCHING_DISABLED`, `NOTHING_TO_SHOW`, `BUSY`, `OSC_NOTIF_BUSY`, `COMMAND_EXIT_ARMED`, `ALERT_RINGING`. - - Transitional states: `MIGHT_BE_BUSY`, `MIGHT_NEED_ATTENTION`. - - When the user enables WATCHING, `watchingStatus` transitions from `WATCHING_DISABLED` to `NOTHING_TO_SHOW` and timer-based activity tracking begins fresh from that moment. - - When the user disables WATCHING, timer-based activity tracking stops and `watchingStatus` returns to `WATCHING_DISABLED`. Public `status` may still be `OSC_NOTIF_BUSY`, `COMMAND_EXIT_ARMED`, or `ALERT_RINGING` if another track is active. -- `watchingStatus: 'WATCHING_DISABLED' | 'NOTHING_TO_SHOW' | 'MIGHT_BE_BUSY' | 'BUSY' | 'MIGHT_NEED_ATTENTION' | 'ALERT_RINGING'` - - Internal timer-based status owned by the existing activity monitor. - - It is driven only by meaningful output, silence timers, and attention. - - Spec prose should use WATCHING terminology for this track. -- `watchingEnabled: boolean` - - Public boolean exposed to UI and persistence so the WATCHING toggle remains accurate while `status` is projected to `OSC_NOTIF_BUSY`, `COMMAND_EXIT_ARMED`, or `ALERT_RINGING`. - - This is `true` exactly when the Session owns an active WATCHING monitor. -- `protocolStatus: 'IDLE' | 'OSC_NOTIF_BUSY' | 'ALERT_RINGING'` - - Internal terminal-report status owned by parsed terminal reports (see [Notification protocols](#notification-protocols)). - - It is driven only by terminal reports such as `OSC 9`, `OSC 9;4`, `OSC 99`, `OSC 777`, and standalone `BEL`. - - It does not use output/silence timers from the WATCHING activity monitor. - - It does use the shared attention model. A protocol completion/notification received while the user is actively attending that Session must not ring. - - `OSC_NOTIF_BUSY` means a terminal report says work is in progress, but there is not yet a notification owed to the user. - - `ALERT_RINGING` means a terminal report explicitly created a notification or completed/errored a reported progress cycle. -- `commandExitStatus: 'IDLE' | 'COMMAND_EXIT_ARMED' | 'ALERT_RINGING'` - - Internal command-exit status owned by terminal semantic command lifecycle events. - - It is driven by `commandStart` / `commandFinish` events from `OSC 133`, `OSC 633`, or equivalent semantic sources. - - `COMMAND_EXIT_ARMED` means MouseTerm saw a foreground command while the Session had attention, then the Session lost attention while that same command was still running. - - `ALERT_RINGING` means that same command exited after running for at least `T_USER_ATTENTION` and the Session still lacked attention. -- `commandExitWatch: CommandExitWatch | null` - - Latest foreground command eligible for command-exit alerting. - - Cleared when the command finishes, another command starts, the user returns before finish, or the Session is destroyed. -- `todo: boolean` - - Reminder state for the Session. Default `false`. - - `false`: no TODO. - - `true`: TODO is shown. It may be set explicitly by the user, or auto-created when a ringing alert is dismissed by attention or by the bell. - - Dismissing a ringing alert when `todo` is already `true` leaves it `true`. - - Legacy persisted TODO encodings migrate into this boolean shape: `-1` / `false` / unknown values become `false`; numeric soft buckets, `2`, `'soft'`, and `'hard'` become `true`. - -Each Session also owns: - -- `attentionDismissedRing: boolean` - - True when the user attended to a ringing Session (clicked into the Pane, typed in passthrough, etc.). Cleared when the bell is next clicked or the alert is toggled/disabled. Used by the bell button to show the context menu on the next click instead of immediately disabling. -- `notification: ActivityNotification | null` - - Latest explicit protocol notification detail, when a Session received a supported terminal notification sequence. - - This metadata is attached to TODO/alert state; it does not replace the boolean `todo` model or the visible TODO pill text. - - `OSC 9;4` progress is tracked through `protocolStatus` while active; completion/error promotes it into this notification field. - -`ActivityNotification` shape (intentionally small โ€” these are the only fields rendered): +type SessionStatus = + | WatchingSessionStatus + | 'OSC_NOTIF_BUSY' + | 'COMMAND_EXIT_ARMED'; -```ts -type ActivityNotificationSource = - | 'OSC 9' - | 'OSC 9;4' - | 'OSC 99' - | 'OSC 777' - | 'BEL' - | 'COMMAND_EXIT'; +type TodoState = boolean; interface ActivityNotification { - source: ActivityNotificationSource; + source: 'OSC 9' | 'OSC 9;4' | 'OSC 99' | 'OSC 777' | 'BEL' | 'COMMAND_EXIT'; title: string | null; body: string | null; } -``` - -Per-source mapping rules (full protocol semantics in [Notification protocols](#notification-protocols)): - -- `OSC 9` stores `{ source: 'OSC 9', title: null, body: message }`. -- `OSC 777` stores `{ source: 'OSC 777', title, body }`. -- `OSC 99` stores `{ source: 'OSC 99', title, body }` after chunk assembly and sanitization. -- `OSC 9;4` stores nothing while progress is active. On completion/error it generates `{ source: 'OSC 9;4', title, body }`, where `title` is a short summary such as `Progress complete`, `Progress error`, or `Progress warning`, and `body` contains the percent when available. -- Standalone `BEL` stores `{ source: 'BEL', title: 'Terminal bell', body: null }`. -- Command exit stores `{ source: 'COMMAND_EXIT', title: 'Command finished', body }`, where `body` contains the display command and exit status when available. -Persistence rules: +interface AlertState { + status: SessionStatus; + watchingEnabled: boolean; + todo: TodoState; + notification: ActivityNotification | null; + attentionDismissedRing: boolean; +} +``` -- Persist the latest `ActivityNotification` with the Session's alert state. -- Persist only sanitized text and metadata, not raw escape sequences. -- On restore, persisted notification detail should restore TODO detail, but must not create a fresh ring or re-cock the bell by itself. +Internal state is deliberately split into independent tracks: -The workspace owns: +- `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. -- `attentionSessionId: string | null` - - Which Session currently has the user's attention. -- `attentionTimer: timeout handle | null` - - Auto-clears `attentionSessionId` after `T_USER_ATTENTION`. Reset on each new attention event. +Public `status` is a projection: -Important invariants: +1. `ALERT_RINGING` if `protocolStatus`, `commandExitStatus`, or `watchingStatus` is ringing, in that order. +2. `OSC_NOTIF_BUSY` if protocol progress is active. +3. `COMMAND_EXIT_ARMED` if command-exit alerting is armed. +4. Otherwise `watchingStatus`. -- Alert state is session-scoped and survives Pane <-> Door transitions. -- `watchingStatus` describes what the timer-based WATCHING track owes the user since the last explicit attention boundary. -- `protocolStatus` describes what terminal reports say independently of the WATCHING track. -- `commandExitStatus` describes whether a known foreground command has been armed for exit-based alerting. -- Public `status` is a projection of those tracks for existing UI. -- Destroying a Session clears `todo`, `notification`, `protocolStatus`, and `commandExitStatus` with it; the activity monitor is disposed. -- Re-rendering, theme changes, resize reflow, or remounting a Pane must not create a new alert by themselves. +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/OSC.md` prevents old terminal output from firing notification side effects again. -## Attention model +Legacy TODO values migrate to boolean: `2`, numeric soft buckets `[0, 1]`, `'soft'`, and `'hard'` become `true`; `false`, `-1`, and unknown values become `false`. -We only ring when a Session produces a completion signal and the user is not actively attending to that Session. +## Attention -`attentionSessionId` is set only by explicit user actions that plausibly mean "I am looking at this Session now": +`attentionSessionId` is set only by explicit user actions that plausibly mean "I am looking at this Session": - clicking a Pane body or Pane header - entering passthrough on a Pane - typing into a Session in passthrough - clicking a Door or pressing `Enter` on a Door, because both reattach into passthrough -These do **not** count as attention: - -- a Session merely being visible -- a Session merely being selected in command mode -- hovering -- a Door existing in the baseboard -- reattaching a Door with `d`, because that restores the Pane but stays in command mode - -Attention is lost when: - -- the user has not explicitly interacted with that Session for `T_USER_ATTENTION` -- the app loses focus -- the Session is minimized into a Door while it had attention -- the Session is destroyed - -`T_USER_ATTENTION` is intentionally finite so a user can run a slow command, walk away, and still get an alert later even if that Pane remained selected. It also acts as the minimum command runtime for command-exit alerts. Start with 15s and tune with real usage. +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. -Doors never directly hold attention. A Door can only regain attention by being restored into a Pane through an action that enters passthrough. +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. -## State model +## WATCHING Track -There are three independent state models: +WATCHING is the user-controlled output/silence monitor. It starts fresh when enabled and is disposed when disabled. Meaningful output excludes resize redraw noise during `T_RESIZE_DEBOUNCE`; theme changes, remounts, DOM reparenting, selection, and focus changes are not output. -- **WATCHING track**: the existing timer-based activity monitor. It watches meaningful output, silence, and user attention only after the user has enabled WATCHING. Its internal state is `watchingStatus`. -- **Terminal-report track**: parsed terminal notification/progress reports from the PTY. It relies entirely on terminal reports and never uses the output/silence timers. Its internal state is `protocolStatus`. -- **Command-exit track**: parsed terminal semantic command lifecycle events. It arms only after the user has seen a foreground command running and later loses attention before that same command exits. Its internal state is `commandExitStatus`. +| State | Meaning | +|---|---| +| `WATCHING_DISABLED` | No monitor exists. | +| `NOTHING_TO_SHOW` | Monitor is active, but no reminder is owed. | +| `MIGHT_BE_BUSY` | Output may be turning into ongoing work. Debounce state. | +| `BUSY` | Enough output has arrived to treat the Session as doing work. | +| `MIGHT_NEED_ATTENTION` | A busy Session went quiet. Debounce state. | +| `ALERT_RINGING` | WATCHING observed likely completion while the Session lacked attention. | -The public `status` is a projection used by existing UI: +Timers live in `cfg.alert`: -1. If `protocolStatus === 'ALERT_RINGING'`, public `status = ALERT_RINGING`. -2. Else if `commandExitStatus === 'ALERT_RINGING'`, public `status = ALERT_RINGING`. -3. Else if `watchingStatus === 'ALERT_RINGING'`, public `status = ALERT_RINGING`. -4. Else if `protocolStatus === 'OSC_NOTIF_BUSY'`, public `status = OSC_NOTIF_BUSY`. -5. Else if `commandExitStatus === 'COMMAND_EXIT_ARMED'`, public `status = COMMAND_EXIT_ARMED`. -6. Else public `status = watchingStatus`. +| 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 | -This projection is deliberate. No single combined enum should attempt to encode every combination of WATCHING/protocol/command-exit state. The terminal-report and command-exit paths must be able to ring without enabling WATCHING. All three tracks rely on the shared user-attention model. +WATCHING transitions: -### WATCHING track +- 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`. -The point of the state machine is not to model every output blip. It is to answer a narrow question: +Rings must be caused by a fresh transition into `ALERT_RINGING`, never by rerender, theme change, remount, minimize, or reattach. -- Does this Session currently have nothing worth surfacing? -- Does it appear to be busy with ongoing work? -- Has it likely finished and now needs attention? +## Protocol Track -The `MIGHT_*` states exist only to absorb uncertainty. They are debounce states, not user-facing end states. +Terminal notifications are explicit requests for attention and bypass the WATCHING toggle. Direct notifications ring immediately only when the Session lacks attention; if the user has attention, that notification is suppressed and unrelated protocol progress is left alone. -### Timing reference +`OSC 9;4` active progress sets public `status = OSC_NOTIF_BUSY`. It never rings because of silence. It rings only when a completion, clear, or error report arrives while the Session lacks attention. Completion/error while attended clears the protocol progress without TODO or ring. -| Timer | Value | Purpose | -|---|---|---| -| `T_BUSY_CANDIDATE_GAP` | 1.5 s | enough elapsed time to treat ongoing output as a possible busy transition | -| `T_BUSY_CONFIRM_GAP` | 500 ms | window in `MIGHT_BE_BUSY` before reverting to `NOTHING_TO_SHOW` if no further output | -| `T_MIGHT_NEED_ATTENTION` | 2 s | silence after `BUSY` before suspecting completion | -| `T_ALERT_RINGING_CONFIRM` | 3 s | additional silence before confirming `ALERT_RINGING` | -| `T_RESIZE_DEBOUNCE` | 500 ms | ignore resize redraw noise | -| `T_USER_ATTENTION` | 15 s | attention idle expiry | - -All values are configurable via `cfg.alert`. Total silence from last meaningful output to `ALERT_RINGING`: 5 s (`T_MIGHT_NEED_ATTENTION` + `T_ALERT_RINGING_CONFIRM`). - -### State semantics - -- `NOTHING_TO_SHOW` - - Default state. - - The Session does not currently owe the user a reminder. - - Immediate command echo or a single quick response is not enough to leave this state. - -- `MIGHT_BE_BUSY` - - Transitional state entered when output suggests the Session may be moving from a quick response into ongoing work. - - If that suspicion is not confirmed quickly, fall back to `NOTHING_TO_SHOW`. - -- `BUSY` - - Stable state. - - There is enough evidence that the Session is doing ongoing work and may later produce something worth surfacing. - -- `OSC_NOTIF_BUSY` - - Stable projected state from the terminal-report track. - - The terminal explicitly reported ongoing progress or a similar protocol-backed busy condition. - - It looks the same as `BUSY` in the Pane header and Door, but it does not participate in WATCHING timers. - - WATCHING silence does not move it to `MIGHT_NEED_ATTENTION`; only a terminal report can clear it or promote it to `ALERT_RINGING`. - -- `COMMAND_EXIT_ARMED` - - Stable projected state from the command-exit track. - - MouseTerm saw a foreground command running while the Session had attention, and attention later expired or was explicitly lost while that command was still running. - - It looks the same as `BUSY` in the Pane header and Door, but it does not participate in WATCHING timers. - - Only the same command finishing, the user returning, another command starting, or Session teardown can clear or promote it. - -- `MIGHT_NEED_ATTENTION` - - Transitional state entered when a `BUSY` Session goes quiet. - - This may be true completion, or only a pause in output. - -- `ALERT_RINGING` - - Stable state. - - The Session likely completed a meaningful unit of work and the alert is actively ringing. - -### Transition rules - -| Current | Event | Next | Notes | -|---|---|---|---| -| any | explicit attention boundary | `NOTHING_TO_SHOW` | Clicking into the Pane, typing in passthrough, or restoring a Door via click/`Enter` starts a new cycle. | -| `NOTHING_TO_SHOW` | first meaningful output after an attention boundary | `NOTHING_TO_SHOW` | A single output burst may be only immediate feedback. | -| `NOTHING_TO_SHOW` | another meaningful output arrives after `T_BUSY_CANDIDATE_GAP`, or multiple rapid outputs continue through that gap | `MIGHT_BE_BUSY` | The Session may be entering a longer-running phase. | -| `MIGHT_BE_BUSY` | further output confirms ongoing work within `T_BUSY_CONFIRM_GAP` | `BUSY` | Enough evidence to treat the Session as busy. | -| `MIGHT_BE_BUSY` | output stops before confirmation | `NOTHING_TO_SHOW` | False positive; it was just a quick response. | -| `BUSY` | more meaningful output | `BUSY` | Stay busy. | -| `BUSY` | no meaningful output for `T_MIGHT_NEED_ATTENTION` | `MIGHT_NEED_ATTENTION` | The Session may have finished, or may only be pausing. | -| `MIGHT_NEED_ATTENTION` | output resumes | `BUSY` | It was only a pause. | -| `MIGHT_NEED_ATTENTION` | silence continues for `T_ALERT_RINGING_CONFIRM` and the Session lacks attention | `ALERT_RINGING` | This is the alert-eligible completion transition. | -| `MIGHT_NEED_ATTENTION` | silence continues for `T_ALERT_RINGING_CONFIRM` but the Session has attention | `NOTHING_TO_SHOW` | The user already sees it; no reminder is owed. | -| `ALERT_RINGING` | explicit attention boundary | `NOTHING_TO_SHOW` | The user attended to the result. | -| `ALERT_RINGING` | new meaningful output and the Session has attention | `MIGHT_BE_BUSY` | A new work cycle may be starting. | -| `ALERT_RINGING` | new meaningful output but the Session lacks attention | `ALERT_RINGING` | Latch: new output does not silently clear the alert without user awareness. | - -These transition rules apply to the WATCHING track only. `OSC_NOTIF_BUSY` and `COMMAND_EXIT_ARMED` are not entered, exited, or promoted by these timers. - -### Terminal-report track - -| Current | Event | Next | Notes | -|---|---|---|---| -| `IDLE` | terminal report starts progress (`OSC 9;4` active state) | `OSC_NOTIF_BUSY` | Cock the bell without enabling WATCHING. | -| `OSC_NOTIF_BUSY` | terminal report updates progress | `OSC_NOTIF_BUSY` | Refresh internal progress state. Public UI remains visually identical to `BUSY`. | -| `OSC_NOTIF_BUSY` | terminal report completes progress and Session lacks attention | `ALERT_RINGING` | Create `notification`, set `todo = true`, and ring. | -| `OSC_NOTIF_BUSY` | terminal report completes progress and Session has attention | `IDLE` | User already sees it; do not ring or create TODO. | -| `OSC_NOTIF_BUSY` | terminal report errors progress and Session lacks attention | `ALERT_RINGING` | Create error `notification`, set `todo = true`, and ring. | -| `OSC_NOTIF_BUSY` | terminal report errors progress and Session has attention | `IDLE` | User already sees it; do not ring or create TODO. | -| `OSC_NOTIF_BUSY` | Session destroyed | `IDLE` | Session teardown clears protocol state. | -| `IDLE` | explicit progress completion report (`OSC 9;4;1;100`) and Session lacks attention | `ALERT_RINGING` | Create generated completion `notification`, set `todo = true`, and ring. | -| `IDLE` | explicit progress completion report (`OSC 9;4;1;100`) and Session has attention | `IDLE` | User already sees it; do not ring or create TODO. | -| `IDLE` | explicit progress error report (`OSC 9;4;2`) and Session lacks attention | `ALERT_RINGING` | Create generated error `notification`, set `todo = true`, and ring. | -| `IDLE` | explicit progress error report (`OSC 9;4;2`) and Session has attention | `IDLE` | User already sees it; do not ring or create TODO. | -| `ALERT_RINGING` | explicit attention boundary / dismiss / TODO clear | `IDLE` | Public status falls back to the command-exit/WATCHING projection after protocol ring clears. | -| any | direct notification (`OSC 9`, completed `OSC 99`, `OSC 777`, standalone `BEL`) and Session lacks attention | `ALERT_RINGING` | Create `notification`, set `todo = true`, and ring immediately. | -| any | direct notification (`OSC 9`, completed `OSC 99`, `OSC 777`, standalone `BEL`) and Session has attention | unchanged | User already sees it; suppress that notification only. Do not create TODO, and do not clear unrelated active progress. | - -`OSC_NOTIF_BUSY` never auto-rings because of silence. If a program starts progress and never sends completion/error, MouseTerm remains cocked until another terminal report completes/errors the progress cycle or the Session is destroyed. - -### Command-exit track - -The command-exit track is intentionally stricter than WATCHING. It exists for this case: "I was watching a command run, then I stopped paying attention, then that command exited." - -| Current | Event | Next | Notes | -|---|---|---|---| -| `IDLE` | command starts while Session has attention | `IDLE` | Store `commandExitWatch` for that command. Do not arm until attention is lost. | -| `IDLE` | attention becomes active while a command is already running | `IDLE` | Store or update `commandExitWatch.seenWithAttentionAt`. | -| `IDLE` | watched command is still running and attention expires or is explicitly lost | `COMMAND_EXIT_ARMED` | Store `attentionLostAt`. | -| `COMMAND_EXIT_ARMED` | same command finishes, runtime is at least `T_USER_ATTENTION`, and Session lacks attention | `ALERT_RINGING` | Create generated command-exit notification, set `todo = true`, and ring. | -| `COMMAND_EXIT_ARMED` | same command finishes too quickly | `IDLE` | Clear without ringing. | -| `COMMAND_EXIT_ARMED` | PTY exits before a command-finish semantic event, runtime is at least `T_USER_ATTENTION`, and Session lacks attention | `ALERT_RINGING` | Treat process exit as the fallback finish event for commands such as `exec ` or shells that exit before emitting a finish marker. | -| `IDLE` | PTY exits before a command-finish semantic event | `IDLE` | Clear any stored `commandExitWatch`; a dead process must not become armed later. | -| `COMMAND_EXIT_ARMED` | Session regains attention before finish | `IDLE` | Clear the arm; the user is watching again. | -| any | a different command starts | `IDLE` | Replace the watch with the new command if it is eligible. | -| `ALERT_RINGING` | explicit attention boundary / dismiss / TODO clear | `IDLE` | Public status falls back to the other tracks. | -| any | Session destroyed | `IDLE` | Session teardown clears command-exit state. | - -Race rule: command-exit alerting is eligible only if attention was lost before the `commandFinish` event for the same command. If command finish and attention loss are observed in the opposite order, do not ring. - -Precedence rule: if a direct protocol notification/progress completion and command finish happen in the same parse batch, protocol detail wins. The command-exit track should not overwrite a richer protocol-generated `ActivityNotification`. - -### Meaningful output - -`Meaningful output` means terminal output that is not suppressed as incidental UI churn. In particular: - -- output during `T_RESIZE_DEBOUNCE` does not count -- theme changes, remounts, or DOM reparenting do not count -- pure selection or focus changes do not count - -The implementation may later learn additional suppressions, but this spec only requires resize churn suppression today. - -## Notification protocols - -Protocol notifications and standalone terminal bells are explicit application requests for attention. They bypass the WATCHING toggle: a Session may ring even when WATCHING is disabled. They must not ring while the user is actively attending that Session. - -Active/in-progress progress sequences do not ring immediately. They "cock" the alarm bell โ€” MouseTerm treats active progress as an explicit finite-work cycle and exposes `OSC_NOTIF_BUSY`. Explicit completion/error progress reports may ring immediately when the Session lacks attention. - -The OSC sequence registry, parser placement, and stripping behavior live in `docs/specs/OSC.md`. This section defines per-protocol semantics for the five supported notification sources. - -| Protocol | Shape | Fields | Notes | -|---|---|---|---| -| `BEL` | `BEL` outside an OSC sequence | none | Generic terminal-bell notification. | -| `OSC 9` | `OSC 9 ; [message] ST` | `message` | iTerm2's legacy notification form. No title/body split. | -| `OSC 9;4` | `OSC 9 ; 4 ; [state] ; [progress] ST` or `OSC 9 ; 4 ST` | progress state/progress | Progress only. Cocks the bell and may later ring on completion/error. | -| `OSC 99` | `OSC 99 ; [metadata] ; [payload] ST` | metadata keys plus payload | kitty's rich notification protocol. Chunked and extensible. | -| `OSC 777` | `OSC 777 ; notify ; [title] ; [body] ST` | `title`, `body` | rxvt/WezTerm notification form. Only `notify` is supported. | +Protocol rings set `todo = true`, store the latest sanitized `ActivityNotification`, and set `protocolStatus = ALERT_RINGING`. Clearing the protocol ring returns `protocolStatus` to `IDLE` and public status falls back to command-exit or WATCHING state. ### Standalone BEL -A `BEL` byte outside an OSC sequence creates one generated notification: - -- `source: 'BEL'` -- `title: 'Terminal bell'` -- `body: null` +A `BEL` byte outside an OSC sequence is stripped from visible output and creates: -Standalone `BEL` is for compatibility with tools that choose a plain terminal-bell notification channel. It strips the bell byte from visible terminal output and rings through the same protocol path as OSC notifications, subject to the shared user-attention check. +```ts +{ source: 'BEL', title: 'Terminal bell', body: null } +``` -If a parse batch contains both standalone `BEL` and a richer OSC notification/progress event, MouseTerm keeps the richer OSC event and drops the generic `BEL` notification detail so `iterm2_with_bell`-style tools cannot overwrite useful TODO preview text. +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 ; [message] ST` creates one notification: - -- `source: 'OSC 9'` -- `title: null` -- `body: [message]` +`OSC 9 ; ST` creates: -The message is plain text. There is no formal title, subtitle, urgency, app id, or notification id. - -OSC 9 also feeds the title-candidate channel for header/door label derivation; that side effect is specified in `docs/specs/terminal-state.md` and does not affect alert behavior. - -### OSC 9;4 progress - -If the first OSC 9 parameter is `4`, the sequence belongs to the progress protocol: +```ts +{ source: 'OSC 9', title: null, body: message } +``` -- `OSC 9 ; 4 ST` clears progress -- `OSC 9 ; 4 ; 0 ST` clears progress -- `OSC 9 ; 4 ; 1 ; [0-100] ST` sets normal progress -- `OSC 9 ; 4 ; 2 ; [0-100?] ST` sets error progress -- `OSC 9 ; 4 ; 3 ST` sets indeterminate progress -- `OSC 9 ; 4 ; 4 ; [0-100] ST` sets warning progress +Empty sanitized messages are ignored. OSC 9 also feeds title-candidate derivation in `docs/specs/terminal-state.md`; that does not change alert behavior. -The official fields are only: +### OSC 9;4 Progress -- `state` -- optional `progress` percent +`OSC 9;4` is progress only. It has no title, body, urgency, id, app name, or action fields. -There is no title, body, subtitle, notification id, application name, urgency, or message text in `OSC 9;4`. +| Sequence | Meaning | +|---|---| +| `OSC 9 ; 4 ST` or `OSC 9 ; 4 ; 0 ST` | clear progress | +| `OSC 9 ; 4 ; 1 ; <0-100> ST` | normal progress; `100` is completion | +| `OSC 9 ; 4 ; 2 ; <0-100?> ST` | error progress; percent optional | +| `OSC 9 ; 4 ; 3 ST` | indeterminate active progress | +| `OSC 9 ; 4 ; 4 ; <0-100> ST` | warning active progress | -MouseTerm behavior: +Rules: -- Non-clear states create or update an internal protocol progress cycle. -- Active progress cocks the bell by setting `protocolStatus = OSC_NOTIF_BUSY`. Public `status` projects this as `OSC_NOTIF_BUSY`, which looks the same as `BUSY` but is independent of the timer-based activity monitor. -- `state = 1` with `progress < 100` is normal active progress. Do not ring. -- `state = 1` with `progress = 100` is a completion report. Ring immediately as a completed progress cycle only if the Session lacks attention. -- `state = 2` is an error signal. Ring immediately and attach a generated progress notification to the TODO only if the Session lacks attention. -- `state = 3` is indeterminate active progress. Do not ring until cleared or replaced by an error/completion signal. -- `state = 4` is warning active progress. Do not ring immediately; remember the warning internally, and if the cycle later rings MouseTerm preserves that warning in the generated progress notification. -- `state = 0` or abbreviated `OSC 9 ; 4 ST` clears progress. If it clears an active protocol progress cycle, ring as completion. If there was no active protocol progress cycle, ignore it. -- Invalid states, missing required progress values for states `1` and `4`, and out-of-range progress values are ignored. Clamp only for display if an implementation has already accepted the sequence. +- Active normal, warning, or indeterminate progress sets `protocolStatus = OSC_NOTIF_BUSY` and does not create TODO. +- `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`. +- Invalid states, missing required percents for states `1` and `4`, and out-of-range percents are ignored. -Progress completion creates a generated notification, but does not invent copy beyond the normalized progress summary. The TODO preview should say things like `Progress complete`, `Progress error`, `Progress warning`, or `Progress 75%` rather than replacing the TODO pill text. +Generated notifications use source `OSC 9;4`, title `Progress complete`, `Progress warning`, or `Progress error`, and body `Progress %` when a percent is known. ### OSC 777 -`OSC 777 ; notify ; [title] ; [body] ST` creates one notification: +`OSC 777 ; notify ; ; <body> ST` creates: -- `source: 'OSC 777'` -- `title: [title]` -- `body: [body]` +```ts +{ source: 'OSC 777', title, body } +``` -Only the `notify` subcommand is supported. The format has no escaping for semicolons. For compatibility, parse the title as the field after `notify` and treat the rest of the sequence after the next semicolon as the body, preserving additional semicolons in the body. A title containing a semicolon cannot be represented portably. +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. ### OSC 99 -`OSC 99 ; [metadata] ; [payload] ST` uses colon-delimited metadata where each key is a single ASCII letter. Unknown keys are ignored. Unknown payload types are ignored unless this spec adds them later. +`OSC 99 ; <metadata> ; <payload> ST` is kitty's notification protocol. Metadata keys are single ASCII letters separated by `:`; unknown keys are ignored. -Initial supported metadata keys: +Supported keys: -| Key | Meaning | Initial MouseTerm behavior | +| Key | Meaning | Dormouse behavior | |---|---|---| -| `i` | notification identifier | Used to assemble chunks and coalesce updates for the same notification. | -| `d` | done flag, `0` or `1`, default `1` | `d=0` stores a partial notification without ringing. `d=1` completes and rings. | -| `e` | payload encoding, `0` plain or `1` base64 | Decode RFC 4648 base64 when `e=1`; reject invalid base64. | -| `p` | payload type, default `title` | Support `title` and `body`; handle management/query payloads separately. | -| `f` | base64 application name | Decode only if needed for protocol validity; do not store or render in this phase. | -| `t` | base64 notification type | Ignore in this phase. | -| `u` | urgency, `0`, `1`, or `2` | Ignore in this phase; urgency does not change alert mechanics. | -| `o` | occasion, `always`, `unfocused`, `invisible` | Parse but ignore for MouseTerm ringing; explicit OSC notifications always ring. | -| `w` | auto-close milliseconds | Parse but ignore for TODO lifetime. TODO clears only by MouseTerm's normal TODO clearing rules. | - -Payload types: - -| `p` value | Behavior | -|---|---| -| `title` | Append payload to the pending notification title. | -| `body` | Append payload to the pending notification body. | -| `?` | Support query. Does not ring. | -| `close` | Close/update management. Does not ring. | -| `alive` | Liveness query. Does not ring. | -| `icon` | Ignore payload content in this phase. Does not ring by itself. | -| `buttons` | Ignore payload content in this phase. Does not ring by itself. | - -Official kitty OSC 99 does not define a `subtitle` payload. If real-world agent tools emit `p=subtitle`, ignore it unless a later spec chooses to render a third user-facing text field. - -For a completed OSC 99 notification: - -- If title and body are both empty after sanitization, ignore it. -- If there is a body but no title, the body is the primary preview line. -- If there is a title but no body, render title only. -- If the same `i` arrives again after completion, treat it as an update to the same notification detail and ring again. -- If `i` is omitted, each completed notification is unique. - -Support query: - -- `OSC 99 ; i=[id] : p=? ; ST` must be answered with MouseTerm's actual support. -- Initial minimal response advertises only `title` and `body`, for example: `OSC 99 ; i=[id] : p=? ; o=always:p=title,body ST`. -- Preserve a valid query id in the response metadata. If the id is missing or cannot be safely echoed in OSC 99 metadata, omit `i=[id]` and respond with `OSC 99 ; p=? ; o=always:p=title,body ST`. -- Do not advertise click reports, close reports, urgency, sounds, icons, buttons, or auto-expiry unless implemented end-to-end. - -## Notification text handling - -Terminal notifications are untrusted terminal output. Treat all text as plain text. - -Input normalization: - -- Decode UTF-8 strictly enough to avoid replacement-character floods. -- Strip C0/C1 control characters after protocol parsing. -- Collapse CR/LF/TAB and other controls to spaces. -- Trim leading/trailing whitespace. -- Do not interpret ANSI, OSC, HTML, Markdown, URLs, shell paths, or emoji shortcodes as markup. - -Protocol-defined limits: - -- OSC 9;4 progress carries only a numeric state and optional numeric percent. There is no user-facing text payload. -- OSC 99 defines a payload chunk limit of 2048 bytes before base64 or 4096 bytes after base64. It permits chunking title/body multiple times, while allowing terminals to impose sensible denial-of-service limits. -- OSC 9 and OSC 777 do not define formal text length limits in the referenced terminal docs. - -MouseTerm-imposed limits: - -- Store at most 256 Unicode grapheme clusters for `title`. -- Store at most 4096 grapheme clusters for `body`. -- Parser memory for incomplete OSC 99 chunks is capped per Session. Drop the oldest incomplete chunks when the cap is exceeded. -- Expire incomplete OSC 99 chunks after 60 seconds if no `d=1` completion arrives. - -Expected UI copy length: - -- Titles are expected to be one short line, usually under 80 characters. -- Bodies are expected to be a few short lines at most. In MouseTerm chrome, show a compact preview and make the full stored body available in a popover/dialog. - -## Notification security - -Any remote process can emit these sequences over SSH. The feature is useful because it works over SSH, but the UI must be robust against hostile text. - -Requirements: - -- Sanitize all text before storing or rendering. -- Cap stored text and incomplete parser state. -- Never execute commands, open URLs, copy to clipboard, read files, or focus outside MouseTerm from these sequences. -- Do not render custom icons or buttons in this phase. -- Do not let notification text alter accessible labels beyond plain-text names. -- Do not allow repeated notifications to allocate unbounded history. Store only the latest detail, not an infinite list. - -## Alert trigger - -WATCHING alert logic is driven by transitions in `watchingStatus`. Protocol alert logic is driven by transitions in `protocolStatus`. Command-exit alert logic is driven by transitions in `commandExitStatus`. The public `status` projection reflects whichever track currently has the strongest user-facing claim. - -### WATCHING ring starts when all of these are true +| `i` | notification id | assemble chunks for the same pending notification | +| `d` | done flag, default `1` | `d=0` stores partial data; `d=1` completes and may ring | +| `e` | encoding, `0` plain or `1` base64 | decode base64 or reject invalid payload | +| `p` | payload type, default `title` | support `title`, `body`, `?`, `close`, `alive`, `icon`, `buttons` | -- the Session has WATCHING enabled (i.e. `watchingStatus !== 'WATCHING_DISABLED'`) -- the Session's `watchingStatus` transitions from `MIGHT_NEED_ATTENTION` into `ALERT_RINGING` -- the Session does not currently have attention +`title` and `body` chunks append to the pending notification. Completion rings once if the sanitized title or body is nonempty. If `i` is omitted, only a complete single-sequence notification is meaningful. -### Protocol override +Management payloads do not ring: -Supported terminal notification reports (see [Notification protocols](#notification-protocols)) may create a protocol ring. Supported `OSC 9;4` progress sequences set `protocolStatus = OSC_NOTIF_BUSY` and may later promote to `protocolStatus = ALERT_RINGING`. Protocol rings: +- `p=?` sends a support response advertising `o=always:p=title,body`. +- `p=close`, `p=alive`, `p=icon`, and `p=buttons` are consumed or ignored without creating notification UI. -- force public `status = ALERT_RINGING` even when the Session's activity monitor is disabled -- obey attention suppression because the user may already be typing into or reading that Session -- set `todo = true` and attach sanitized notification detail -- do not enable or disable the activity monitor -- return to `WATCHING_DISABLED` after dismissal if no activity monitor was enabled before the protocol ring +Pending OSC 99 chunks expire after 60 seconds, and at most 64 pending ids are retained per parser. -Implementation surface inside `AlertManager`: +## Command-exit Track -- A protocol-ring flag or source field independent of `ActivityMonitor`. -- `OSC 9;4` progress is tracked internally in `AlertManager`, not in public `ActivityState`. -- `getState(id).status` returns `ALERT_RINGING` while the protocol ring is active. -- `getState(id).status` returns `OSC_NOTIF_BUSY` while internal protocol progress is active and no stronger state is present. -- Dismiss/attend clears the protocol ring; status falls back to the command-exit/WATCHING projection or `WATCHING_DISABLED` if no stronger state exists. -- Completing or erroring a protocol progress cycle creates an `ActivityNotification` and promotes it into a protocol ring only if the Session lacks attention. -- Methods such as `notifyFromProtocol(id, notification)` and `updateProtocolProgress(id, state, percent)` are exposed through `PlatformAdapter` / VS Code messages. +The command-exit track consumes normalized semantic command events from `docs/specs/terminal-state.md` (`OSC 133`, `OSC 633`, or equivalent). It must not parse raw OSC itself. -### Command-exit override +Rules: -Terminal semantic command lifecycle events may create a command-exit ring. Command-exit rings: +- A command start creates `commandExitWatch` for the current foreground command. If the Session has attention, mark the command as seen. +- 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. +- 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. -- force public `status = ALERT_RINGING` even when WATCHING is disabled -- obey attention suppression because the user may already have returned to that Session -- set `todo = true` and attach generated command-exit detail unless a richer protocol notification is already ringing -- do not enable or disable WATCHING -- return to `WATCHING_DISABLED` after dismissal if no WATCHING monitor was enabled and no protocol progress is active +## Clearing And TODO -Implementation surface inside `AlertManager`: +`todo` is a boolean reminder. Protocol and command-exit rings create it immediately. WATCHING rings create it when the user attends, dismisses, or marks TODO, so a dismissed ring does not disappear without a trace. -- A `commandExitStatus` field independent of `ActivityMonitor` and `protocolStatus`. -- A `commandExitWatch` record for the current foreground command, storing command id, display command, source, `startedAt`, `seenWithAttentionAt`, and `attentionLostAt`. -- `getState(id).status` returns `COMMAND_EXIT_ARMED` while command-exit alerting is armed and no stronger state is present. -- `applyTerminalSemanticEvents(id, events)` consumes normalized command lifecycle events; it must not parse raw OSC directly. -- Dismiss/attend/TODO-clear clears the command-exit ring; status falls back to protocol or WATCHING projection. -- Command-exit rings require command runtime `>= T_USER_ATTENTION`. +Clearing behavior: -### Ringing does not start when any of these are true +- Attending a ringing Session clears active protocol/command rings, resets a WATCHING ring, sets `todo = true`, and sets `attentionDismissedRing = true`. +- Clicking the ringing bell or pressing `a` dismisses the ring, sets `todo = true`, and opens the alert/TODO dialog. +- Marking TODO clears any active ring and leaves WATCHING enabled for future cycles. +- Clearing TODO sets `todo = false`, clears `notification`, and clears active protocol/command rings. +- Typing passthrough `Enter` into the Session clears TODO. Command-mode `Enter` that only enters passthrough does not. +- Disabling WATCHING disposes only the activity monitor. It does not clear protocol progress, command-exit arms, TODO, or notification detail. +- Destroying the Session clears all alert, TODO, notification, attention, protocol, and command-exit state. -- the Session already has attention at the moment it would otherwise enter `ALERT_RINGING` -- the Session is merely re-rendered or reattached while already `ALERT_RINGING` -- the only recent output was resize noise already ignored by the completion detector -- for WATCHING rings only: WATCHING is disabled (`watchingStatus === 'WATCHING_DISABLED'`) +`attentionDismissedRing` exists so the next bell click after an attention-based dismissal opens the dialog instead of silently disabling WATCHING. -This "fresh transition into `ALERT_RINGING` only" rule is critical. It prevents duplicate alerts on remount, theme change, or Pane <-> Door movement. +## UI Contract -Resize/activity-monitor suppression rules apply only to WATCHING rings. Attention suppression applies to WATCHING, protocol, and command-exit rings. +### Pane Header -## Alert clearing rules +The header shows: -For WATCHING rings, the Session leaves `ALERT_RINGING` and returns to `NOTHING_TO_SHOW` when any of these happen: +- an alert bell in every width tier +- a fixed-text `TODO` pill when `todo === true`, except in the minimal tier +- 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 -- the user attends to the Session (clicking into the Pane, typing in passthrough, restoring a Door via click/`Enter`) -- the user dismisses the alert (clicking the ringing bell, pressing `a`) -- the user marks the Session as TODO (`t` key or context menu) -- new output arrives while the Session has attention (starts a new `MIGHT_BE_BUSY` cycle; without attention the alert stays ringing โ€” see latch in transition rules) +Bell visual state is a pure function of public `status`: -All attention-based dismissals (the first three above) set `todo = true` if it is not already set. This prevents phantom dismissals where the alert vanishes without a trace. Once the TODO is visible, the user can clear it explicitly from the pill/dialog or by typing `Enter` as passthrough input into that Session's shell (i.e., the keystroke is forwarded to the PTY). The command-mode `Enter` that *switches into* passthrough does not clear the TODO. Synthetic terminal reports (focus events, cursor-position responses) also do not count as user input for clearing. - -For protocol rings (see [Notification protocols](#notification-protocols)), clearing the protocol ring sets `protocolStatus = IDLE` and returns public `status` to the projected command-exit/WATCHING state. If no WATCHING monitor was enabled before the protocol ring and no command-exit state is active, the Session returns to `WATCHING_DISABLED`. - -For command-exit rings, clearing the command-exit ring sets `commandExitStatus = IDLE` and returns public `status` to the projected protocol/WATCHING state. If no WATCHING monitor was enabled and no protocol state is active, the Session returns to `WATCHING_DISABLED`. - -The WATCHING track leaves `ALERT_RINGING` and returns to `WATCHING_DISABLED` when: - -- the user disables WATCHING on that Session (disposes the activity monitor) - -Disabling WATCHING does not clear `protocolStatus` or `commandExitStatus`. If either stronger track is active, public `status` remains driven by that track. - -The Session's alert state is cleared entirely when: - -- the Session is destroyed - -If more output arrives later and the Session makes a fresh transition back into `ALERT_RINGING`, the alert rings again. - -Marking a Session as TODO resets a WATCHING alert to `NOTHING_TO_SHOW` and sets `todo = true`, but it does **not** disable future WATCHING. `todo` and the WATCHING toggle are separate concerns. Protocol and command-exit rings preserve the same TODO behavior; clearing TODO clears `notification` unless the user explicitly chooses a future "keep details" action. - -Disabling WATCHING disposes the activity monitor and returns `watchingStatus` to `WATCHING_DISABLED`. Public `status` returns to `WATCHING_DISABLED` only when `protocolStatus === 'IDLE'` and `commandExitStatus === 'IDLE'`. - -## UI - -### Pane header - -The Pane header exposes two independent concepts: - -- TODO pill -- alert button - -TODO pill: - -- toggled in command mode with `t` (`false` -> `true` -> `false`) -- shown when `todo === true` -- auto-created on alert dismiss or attention-based alert clearing -- typing `Enter` as passthrough input (forwarded to the Session's shell) clears the TODO; the command-mode `Enter` that switches *into* passthrough does not -- clicking the TODO pill clears it -- when TODO clears, the pill briefly morphs to a `โœ“` glyph in the success color (~500 ms) before unmounting โ€” this marks the moment of completion so the pill never vanishes silently -- no empty placeholder when off -- the visible pill remains `TODO`. It does not resize to arbitrary notification text, and does not adopt protocol-supplied title/body strings. It may show a small dot treatment when notification detail is present, as long as the pill remains fixed-width enough for narrow headers. - -Alert button: - -- shown in all header tiers, including compact and minimal -- icon-only control with tooltip and accessible label -- visual states (pure function of `status`): - - `WATCHING_DISABLED`: `BellIcon` unfilled, muted - - `NOTHING_TO_SHOW`: `BellIcon` filled, muted, upright - - `MIGHT_BE_BUSY`: `BellIcon` filled, muted, tilted slightly (-22.5ยฐ) - - `BUSY`: `BellIcon` filled, muted, tilted 45ยฐ - - `OSC_NOTIF_BUSY`: same visual treatment as `BUSY` - - `COMMAND_EXIT_ARMED`: same visual treatment as `BUSY` - - `MIGHT_NEED_ATTENTION`: `BellIcon` filled, muted, tilted 60ยฐ - - `ALERT_RINGING`: `BellIcon` filled, warning color, rocking animation (ยฑ45ยฐ bell-ring keyframe); reduced-motion: static 45ยฐ tilt -- escalation is conveyed by increasing tilt angle, not by a separate badge element -- the tilt/animation must not change the button's layout size - -Interaction (`dismissOrToggleAlert` state machine): - -- left-click the bell while `WATCHING_DISABLED`: enables WATCHING (creates activity monitor) -- left-click the bell while `ALERT_RINGING`: dismisses the alert, creates a TODO if none exists, then opens the context menu anchored below the button -- left-click the bell after an attention-based dismissal (`attentionDismissedRing` is set): clears the flag and opens the context menu. This lets the user access TODO/disable options after attending to a ringing Session without requiring a right-click. -- left-click the bell while `OSC_NOTIF_BUSY`: does not clear protocol progress. If WATCHING is enabled, disables only WATCHING; if WATCHING is disabled, opens the context menu. -- left-click the bell while `COMMAND_EXIT_ARMED`: does not clear the command-exit arm. If WATCHING is enabled, disables only WATCHING; if WATCHING is disabled, opens the context menu. -- left-click the bell in any other WATCHING-enabled state: disables WATCHING (destroys activity monitor) -- pressing `a` on a selected Pane in command mode: same as left-click -- right-click the bell (any state): opens a context menu with: - - a TODO on/off switch with `[t]` shortcut hint - - a WATCHING on/off switch with `[a]` shortcut hint - - brief description of TODO clearing behavior -- tooltip includes "Right-click for options" hint - -The alert control has higher layout priority than split or zoom controls. Long titles must truncate before the bell disappears. - -### Notification preview and detail - -Protocol notification detail appears in a preview surface anchored below the TODO pill or alert bell: - -- Shown on TODO hover/focus. -- Shown when the selected Pane has a TODO with notification detail and there is enough space. -- Shown above a Door on hover/focus without changing Door click behavior. -- Click/`Enter` on a Door remains reattach-and-attend; no Door-only menus. - -Preview content: - -- Primary line: `title` if present, otherwise the first body excerpt. -- Body: clamp to 3 lines in the hover preview. -- For generated `OSC 9;4` notifications, title/body already contain the progress summary; no separate progress widget is rendered. -- Footer metadata: protocol source (`OSC 9`, `OSC 9;4`, `OSC 99`, `OSC 777`, `BEL`). - -A full detail dialog/popover may be opened from the preview or the existing alert context menu: - -- Text wraps and can scroll. -- No raw escape sequence is shown by default. -- Focus traps and `Escape` behavior follow [Accessibility and motion](#accessibility-and-motion). - -Recommended decision: do not replace TODO text with notification text. The header and Door need fixed, scannable indicators across many Sessions. Replacing `TODO` with unbounded remote-controlled text creates overflow, localization, spoofing, and attention-noise problems. A hover/selected expansion gives the notification context without destabilizing the layout. +| 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 | + +Tilt and animation must not change layout size. Long titles truncate before alert/TODO controls disappear. + +Bell interactions: + +- Left-click `WATCHING_DISABLED`: enable WATCHING. +- Left-click `ALERT_RINGING`: dismiss, create TODO if needed, open dialog. +- Left-click after `attentionDismissedRing`: clear the flag and open dialog. +- Left-click `OSC_NOTIF_BUSY` or `COMMAND_EXIT_ARMED`: if WATCHING is enabled, disable only WATCHING; otherwise open dialog. Do not clear protocol progress or command-exit arm. +- Left-click any other WATCHING-enabled state: disable WATCHING. +- Pressing `a` on the selected Pane in command mode uses the same action. +- Right-click always opens the dialog. +- Pressing `t` toggles TODO. + +The TODO pill always displays `TODO`; remote notification text belongs in preview/detail surfaces, not inside the pill. Clicking the pill clears TODO. On clear, the pill briefly shows the success flourish before unmounting. ### Door -A Door is display-only for alert state in v1. It must not replace the existing Door primary actions defined in `docs/specs/layout.md`. - -Door indicators: - -- show bell indicator only when `status !== 'WATCHING_DISABLED'` -- show TODO pill when `todo === true` -- if `status === 'ALERT_RINGING'`, the Door bell icon uses warning color and the same rocking animation as the Pane header -- the Door bell icon shows the same tilt angles as the Pane header for escalation states -- `OSC_NOTIF_BUSY` uses the same Door bell treatment as `BUSY` -- `COMMAND_EXIT_ARMED` uses the same Door bell treatment as `BUSY` - -Door interaction: - -- click or `Enter` keeps its existing meaning: reattach and enter passthrough -- `d` keeps its existing meaning: reattach and stay in command mode -- alert-specific actions are manipulated after restore, from the Pane header UI - -Consequences: - -- clicking or `Enter` on a ringing Door counts as attention and clears the ring -- `d` on a ringing Door does not count as attention, so the ring remains until the user explicitly attends, dismisses, or disables - -## Hardening requirements - -### Text overflow and narrow layouts - -- Session titles may contain long text, emoji, CJK, RTL text, combining marks, and shell prompts with paths. -- Pane titles and Door titles must use `min-width: 0` plus truncation so indicators do not overflow their containers. -- Bell and TODO indicators must be fixed-width, non-shrinking affordances. -- The ringing treatment must not change layout size. No border-width jumps, no icon-size jumps. -- If header space becomes extremely tight, the TODO pill may collapse before the alert control does. - -### Accessibility and motion - -- Ringing must not rely on color alone. Use icon state plus outline, fill, or pulse. -- Respect `prefers-reduced-motion`. In reduced-motion mode, replace the rocking animation with a steady 45ยฐ tilt. All tilt states are static transforms and work unchanged regardless of motion preference. -- Bell button must expose accurate `aria-label` text: - - "Enable alert" - - "Disable alert" - - "Alert ringing" -- TODO pill and bell actions must remain keyboard reachable. -- Any ringing modal or popover must trap focus, support `Escape`, and restore focus to the bell button when closed. - -### Session and lifecycle edge cases - -- Multiple Sessions may ring at once. Alert state is independent per Session. -- Minimizing or reattaching a ringing Session preserves the ring because the ring belongs to the Session. -- A Session that exits while ringing continues to ring until attended, dismissed, disabled, or destroyed by the user. -- Killing the Session clears all alert and TODO state because the Session no longer exists. -- If output resumes while a Session is ringing and the Session has attention, the ring clears and the Session returns to the normal state-machine flow. If the Session lacks attention, the ring persists (latch behavior prevents silent dismissal). -- App blur clears attention but does not dismiss existing rings. - -### Internationalization - -- Icon-only header controls avoid fixed-width translated labels. -- Tooltips, menus, and modal actions must wrap cleanly for longer translations. -- Use logical CSS properties where layout direction matters so RTL remains correct. -- The literal TODO pill may remain `TODO` in v1, but the layout must tolerate a longer localized label later. - -## Scenarios - -### Slow response, same pane, user walks away - -- User enables alert on a Pane. -- User runs a slow command. -- The Session progresses through `MIGHT_BE_BUSY` and `BUSY`. -- The Session later goes quiet, then transitions through `MIGHT_NEED_ATTENTION` into `ALERT_RINGING`. -- If `T_USER_ATTENTION` has expired, the Pane rings even if it remained selected. - -### Slow response, user switched elsewhere - -- User enables alert on Session A. -- Session A becomes `MIGHT_BE_BUSY`, then `BUSY`. -- User works in Session B or another app. -- Session A later goes quiet long enough to transition into `ALERT_RINGING`. -- Session A rings because it does not have attention. +A Door is display-only for alert state: -### Door rings, user wants to inspect immediately +- show the bell only when `status !== 'WATCHING_DISABLED'` +- show the TODO pill when `todo === true` +- use the same bell tilt/animation mapping as the Pane header +- do not expose a Door-specific alert menu -- User minimizes a WATCHING-enabled Session into a Door. -- The Session later transitions into `ALERT_RINGING`. -- The Door rings. -- User clicks the Door. -- The Session reattaches into passthrough and the ring clears. +Click or `Enter` on a Door reattaches into passthrough, counts as attention, and clears a ring. `d` reattaches in command mode, does not count as attention, and leaves the ring intact. -### Door rings, user wants to keep command-mode control +## Text And Security -- User minimizes a WATCHING-enabled Session into a Door. -- The Door starts ringing. -- User presses `d` on the Door in command mode. -- The Pane is restored, but the ring remains because the user has not yet explicitly attended to the Session. +Notification text is untrusted terminal output. -### User dismisses, then new output arrives +Sanitization and limits: -- A Session rings. -- User clicks into the pane to read the output. -- The alert clears, and a TODO appears. -- User presses `Enter` into the Session โ†’ the `TODO` pill morphs to a `โœ“` and clears (they engaged). -- The Session later emits new output, progresses through `BUSY`, and eventually reaches `ALERT_RINGING` again. +- 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 only the latest `ActivityNotification`, not unbounded history. +- Cap and expire incomplete OSC 99 parser state as described above. -### User dismisses but doesn't engage +Security requirements: -- A Session rings. -- User clicks into the pane briefly, then switches to another session. -- The alert clears, and a TODO appears. -- User never presses `Enter` into the terminal โ†’ TODO persists. -- User later notices the TODO pill and clicks it to clear it. +- Never execute commands, open URLs, copy to clipboard, read files, focus outside Dormouse, or render protocol-supplied icons/buttons/actions. +- Notification text may appear only as plain text in visible UI and accessible labels. +- Layout must tolerate long text, CJK, RTL, combining marks, and emoji without pushing fixed controls out of bounds. -### OSC 9 rings with WATCHING disabled +## Hardening -- Session starts with `status = WATCHING_DISABLED`, `todo = false`. -- PTY emits `OSC 9 ; Build finished ST`. -- MouseTerm stores body `Build finished`, sets `todo = true`, and reports `ALERT_RINGING`. -- User clicks into the Pane. -- Ring clears. Because WATCHING was disabled, status returns to `WATCHING_DISABLED`; TODO remains until explicitly cleared or passthrough `Enter` is sent. +- Multiple Sessions can ring independently. +- Minimize, reattach, rerender, resize, and theme changes must preserve existing alert state without creating new rings. +- An exited Session may keep ringing until attended, dismissed, disabled, or destroyed. +- Ringing must not rely on color alone, and `prefers-reduced-motion` must be respected. +- 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. -### OSC 777 preserves title and body +## Verification Checklist -- PTY emits `OSC 777 ; notify ; Tests ; 341 passed ST`. -- Preview primary line is `Tests`. -- Preview body is `341 passed`. -- The TODO pill remains `TODO`. - -### OSC 99 chunked title/body - -- PTY emits `OSC 99 ; i=build-1:d=0 ; Build complete ST`. -- No ring yet. -- PTY emits `OSC 99 ; i=build-1:p=body:d=1 ; All tests passed ST`. -- MouseTerm combines title and body, then rings once. - -### OSC 9 progress cocks the bell - -- PTY emits `OSC 9 ; 4 ; 1 ; 50 ST`. -- MouseTerm stores progress `normal, 50%`. -- Public `status` becomes `OSC_NOTIF_BUSY`; the bell looks like `BUSY` without creating a TODO. -- PTY emits `OSC 9 ; 4 ; 0 ST` while the Session lacks attention. -- MouseTerm rings, sets `todo = true`, and the TODO preview says progress completed. - -### OSC 9 progress error rings immediately - -- PTY emits `OSC 9 ; 4 ; 2 ; 75 ST` while the Session lacks attention. -- MouseTerm stores progress `error, 75%`. -- MouseTerm rings immediately and attaches error progress detail to the TODO. - -### OSC notification while typing does not ring - -- User is typing into a Session in passthrough mode, so the Session has attention. -- PTY emits `OSC 9 ; Build finished ST`. -- MouseTerm does not ring and does not create a TODO because the user is already attending that Session. - -### Command exits after attention expires - -- User is typing into a Session in passthrough mode, so the Session has attention. -- PTY emits `OSC 633 ; E ; pnpm\x20build ST` and `OSC 633 ; C ST`; MouseTerm stores the foreground command as seen with attention. -- User stops interacting with that Session for at least `T_USER_ATTENTION`; MouseTerm clears attention and sets public `status = COMMAND_EXIT_ARMED`. -- The same command later emits `OSC 633 ; D ; 0 ST`. -- MouseTerm rings, sets `todo = true`, and stores a generated `COMMAND_EXIT` notification. - -### Quick command exit does not ring - -- User starts a command with attention and then immediately switches away. -- The command finishes before `T_USER_ATTENTION` elapsed since command start. -- MouseTerm clears the command-exit watch without ringing. - -### Returning before command exit disarms - -- User starts a command with attention, then attention expires and public `status = COMMAND_EXIT_ARMED`. -- User clicks back into the Session before the command finishes. -- MouseTerm clears the command-exit arm. If the command later finishes while the Session still has attention, it does not ring. - -### Restore does not replay old notifications - -- A Session receives an OSC notification and saves state with TODO detail. -- The app reloads and replays buffered output containing the original OSC. -- The TODO detail is restored from persisted state, but no fresh ring is emitted from replay. - -## Verification checklist - -WATCHING track: - -- Alert only rings on a fresh transition into `ALERT_RINGING` -- Single quick responses stay in `NOTHING_TO_SHOW` -- short pauses in a `BUSY` session only reach `MIGHT_NEED_ATTENTION`, not `ALERT_RINGING` -- Resize noise cannot cause a ring -- Minimize/reattach preserves alert state (`status` and `todo`) -- `d` restore from a Door does not silently clear a ring -- click/`Enter` restore from a Door does clear a ring -- very long titles do not push bell or TODO indicators out of bounds -- ringing is still understandable with reduced motion enabled -- multiple simultaneous ringing Sessions remain independently dismissible - -Notification protocols: - -- `OSC 9;message` rings and stores `message`. -- `OSC 9;4;1;50` sets `OSC_NOTIF_BUSY` and stores `normal, 50%` internally. -- `OSC 9;4;3` sets `OSC_NOTIF_BUSY` and stores indeterminate progress internally. -- `OSC 9;4;4;25` sets `OSC_NOTIF_BUSY` and stores warning progress internally. -- `OSC 9;4;2` rings immediately with indeterminate error detail. -- `OSC 9;4;0` rings as completion only if there was an active progress cycle. -- `OSC 9;4;1;100` rings immediately as an explicit completion report. -- Standalone `BEL` rings and stores generated terminal-bell detail. -- `OSC 777;notify;title;body` rings and stores title/body. -- Unsupported `OSC 777` subcommands are ignored. -- OSC 99 `d=0` chunks do not ring before completion. -- OSC 99 `d=1` completion rings once with combined title/body. -- OSC 99 `p=?` is answered and does not ring; `p=close`, `p=alive`, `p=icon`, and `p=buttons` do not ring by themselves. -- Extra standalone `BEL` in the same parse batch as a richer OSC event does not replace the richer notification detail. -- Protocol notifications ring with WATCHING disabled. -- Protocol notifications do not ring when the Session has attention. -- Dismissal returns a WATCHING-disabled Session to `WATCHING_DISABLED`. -- Dismissal returns a WATCHING-enabled Session to its monitor-backed state. -- TODO pill text remains stable under very long notification text. -- Hover/focus preview wraps long text and does not overflow narrow headers or Doors. -- Replay/restore does not re-fire notification side effects. - -Command-exit track: - -- Command start while attended stores a command-exit watch without ringing. -- Attention expiry while the same command is running sets `COMMAND_EXIT_ARMED`. -- Explicit attention loss while the same command is running sets `COMMAND_EXIT_ARMED`. -- Returning to the Session before finish clears `COMMAND_EXIT_ARMED`. -- The same command finishing after runtime `>= T_USER_ATTENTION` rings only if the Session lacks attention. -- The same command finishing before runtime `T_USER_ATTENTION` does not ring. -- A different command start replaces the prior watch. -- A protocol notification in the same parse batch as command finish wins over generated command-exit detail. +- 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 diff --git a/docs/specs/auto-update.md b/docs/specs/auto-update.md index bb347a58..ce095fa1 100644 --- a/docs/specs/auto-update.md +++ b/docs/specs/auto-update.md @@ -48,7 +48,7 @@ Update status appears as a text notice on the right side of the Baseboard (the a | `post-update-success` | "Updated to v0.5.0 โ€” from v0.4.0" | "Changelog" | 10 seconds | | `post-update-failure` | "Update failed" | "Click here to debug" | No | -The "Install when I quit" action is the user's approval to download the update now and install it when they quit. The inline "Changelog" action calls Tauri's `getVersion()` and opens `https://mouseterm.com/changelog/after/<current-version>`. +The "Install when I quit" action is the user's approval to download the update now and install it when they quit. The inline "Changelog" action calls Tauri's `getVersion()` and opens `https://dormouse.sh/changelog/after/<current-version>`. When a notice has follow-up actions, it uses ` ยท ` as the separator between the message and action labels. All states are dismissible via [ร—]. Dismissing an unapproved `available` notice means no update is downloaded or installed in that session. Dismissing a `downloading` or `downloaded` notice hides it for the session only โ€” it does not cancel an already-approved download/install. @@ -72,7 +72,7 @@ Windows uses `"installMode": "passive"` (configured in `tauri.conf.json` under ` ## localStorage -Single key: `mouseterm:update-result` +Single key: `dormouse:update-result` | Scenario | Value written | When cleared | |----------|--------------|--------------| @@ -99,7 +99,7 @@ In `standalone/src-tauri/tauri.conf.json`: "plugins": { "updater": { "pubkey": "<ed25519 public key>", - "endpoints": ["https://mouseterm.com/standalone-latest.json"], + "endpoints": ["https://dormouse.sh/standalone-latest.json"], "windows": { "installMode": "passive" } } } @@ -117,7 +117,7 @@ The Rust side registers the plugin with `tauri_plugin_updater::Builder::new().bu ## Design decisions -**Why install on quit after approval, not immediately?** MouseTerm is a terminal app with running processes. A mid-session relaunch would kill all sessions. By installing at quit time, the user has already decided to close their terminals. +**Why install on quit after approval, not immediately?** Dormouse is a terminal app with running processes. A mid-session relaunch would kill all sessions. By installing at quit time, the user has already decided to close their terminals. **Why no silent download?** Update bundles can be large, can fail for environment-specific reasons, and may surprise users who did not opt into changing the app. The launch probe is silent, but download/install only begins after explicit approval. diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md index cce12a95..8e00036a 100644 --- a/docs/specs/deploy.md +++ b/docs/specs/deploy.md @@ -21,7 +21,7 @@ Human-driven steps, in order: 4. **Push** โ€” `git push && git push origin vX.Y.Z`. This triggers CI (Stage 1). 5. **Set environment variables** โ€” copy the relevant secrets into the terminal from your password manager (see [Environment / secrets](#environment--secrets) for the list). 6. **Run local signing** โ€” plug in the PIV USB key, then `./scripts/sign-and-deploy.sh all X.Y.Z`. The script waits for CI, downloads unsigned artifacts, signs macOS + Windows, generates the Tauri update manifest into `website/public/standalone-latest.json`, and creates the GitHub Release. Run `./scripts/sign-and-deploy.sh --help` for resume-after-failure subcommands. -7. **Deploy website** โ€” commit the updated `website/public/standalone-latest.json` and deploy mouseterm.com so the updater endpoint is live. +7. **Deploy website** โ€” commit the updated `website/public/standalone-latest.json` and deploy dormouse.sh so the updater endpoint is live. 8. **Verify the release** - Check GitHub Release assets are correct - On a Mac: extract the `.tar.gz`, open the `.app`, confirm no Gatekeeper warnings @@ -87,8 +87,8 @@ Each matrix leg: Runs on `ubuntu-latest`: 1. Checkout, setup Node 22, pnpm 10 2. `pnpm install --frozen-lockfile` at the repo root -3. `pnpm --filter mouseterm-lib test` -4. `pnpm --filter mouseterm build:frontend && pnpm --filter mouseterm build` +3. `pnpm --filter dormouse-lib test` +4. `pnpm --filter dormouse build:frontend && pnpm --filter dormouse build` 5. `npx vsce package --no-dependencies` 6. Upload `.vsix` as artifact @@ -136,29 +136,29 @@ codesign/jsign the executable ### Packaged app logging -Windows release builds use the GUI subsystem, so launching `mouseterm.exe` from a terminal returns immediately and does not stream stdout/stderr. The Tauri backend writes sidecar diagnostics to `%LOCALAPPDATA%\MouseTerm\mouseterm.log` on Windows, or to `$TMPDIR/mouseterm.log` on other platforms. Set `MOUSETERM_LOG_FILE` to override the path. +Windows release builds use the GUI subsystem, so launching `dormouse.exe` from a terminal returns immediately and does not stream stdout/stderr. The Tauri backend writes sidecar diagnostics to `%LOCALAPPDATA%\Dormouse\dormouse.log` on Windows, or to `$TMPDIR/dormouse.log` on other platforms. Set `DORMOUSE_LOG_FILE` to override the path. ## Artifact filenames -All release assets use **stable filenames** (no version in the name). This allows hotlinking directly from mouseterm.com via GitHub's `/latest/download/` redirect, which always resolves to the most recent release. +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 | `MouseTerm-windows-x64-setup.exe` | Download + Tauri updater | -| macOS | `MouseTerm-macos-aarch64.tar.gz` | Download + Tauri updater | -| Linux | `MouseTerm-linux-x86_64.AppImage` | Download + Tauri updater | +| 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 | ### Download hotlinks -The mouseterm.com 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: ``` -https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-windows-x64-setup.exe -https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-macos-aarch64.tar.gz -https://github.com/diffplug/mouseterm/releases/latest/download/MouseTerm-linux-x86_64.AppImage +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 `mouseterm.com/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. +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. ## Tauri auto-updater @@ -172,7 +172,7 @@ Design notes that aren't obvious from the files: ### Update manifest (`standalone-latest.json`) -Generated by the local script after signing. The script writes it to `website/public/standalone-latest.json` so it's served from `mouseterm.com/standalone-latest.json` via Cloudflare Pages. This gives us request analytics on update checks. +Generated by the local script after signing. The script writes it to `website/public/standalone-latest.json` so it's served from `dormouse.sh/standalone-latest.json` via Cloudflare Pages. This gives us request analytics on update checks. ```json { @@ -181,30 +181,30 @@ Generated by the local script after signing. The script writes it to `website/pu "pub_date": "2026-03-25T12:00:00Z", "platforms": { "windows-x86_64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-windows-x64-setup.exe", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.1.0/Dormouse-windows-x64-setup.exe", "signature": "<contents of .sig file>" }, "darwin-aarch64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-macos-aarch64.tar.gz", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.1.0/Dormouse-macos-aarch64.tar.gz", "signature": "<contents of .sig file>" }, "linux-x86_64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-linux-x86_64.AppImage", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.1.0/Dormouse-linux-x86_64.AppImage", "signature": "<contents of .sig file>" } } } ``` -Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) but the *filenames* are stable. The manifest itself is served from `mouseterm.com/standalone-latest.json` โ€” Cloudflare Pages analytics tracks every update check. +Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) but the *filenames* are stable. The manifest itself is served from `dormouse.sh/standalone-latest.json` โ€” Cloudflare Pages analytics tracks every update check. ## Changelog A single `CHANGELOG.md` at the repo root, following [Keep a Changelog](https://keepachangelog.com/) format. The `[Unreleased]` section is promoted to `[X.Y.Z]` at release time. The release notes include both standalone and VSCode changes in one entry. The website changelog page imports generated data from `website/src/data/changelog.json`, but `CHANGELOG.md` is the source of truth and the JSON is gitignored. You do not normally run `website/scripts/generate-changelog.js` by hand: -- `pnpm --filter mouseterm-website build` runs it through the website `prebuild` script before Vite bundles the static site. -- `pnpm --filter mouseterm-website dev` and `pnpm --filter mouseterm-website test` also regenerate it through lifecycle scripts so clean checkouts work locally. +- `pnpm --filter dormouse-website build` runs it through the website `prebuild` script before Vite bundles the static site. +- `pnpm --filter dormouse-website dev` and `pnpm --filter dormouse-website test` also regenerate it through lifecycle scripts so clean checkouts work locally. If you edit `CHANGELOG.md` manually outside `/release-notes` and want to preview the generated data immediately, run `node website/scripts/generate-changelog.js`. Do not commit `website/src/data/changelog.json`. diff --git a/docs/specs/glossary.md b/docs/specs/glossary.md index fac12ba3..e72cda88 100644 --- a/docs/specs/glossary.md +++ b/docs/specs/glossary.md @@ -1,6 +1,6 @@ # Glossary -This glossary is the canonical vocabulary for states, entities, and transitions in mouseterm. Every other spec defers to this one when naming a state or a verb. When writing code or prose, pick names from here first. +This glossary is the canonical vocabulary for states, entities, and transitions in Dormouse. Every other spec defers to this one when naming a state or a verb. When writing code or prose, pick names from here first. ## The core idea diff --git a/docs/specs/layout.md b/docs/specs/layout.md index c02f3f45..4c279c8a 100644 --- a/docs/specs/layout.md +++ b/docs/specs/layout.md @@ -277,7 +277,7 @@ Pane IDs are session IDs. `TerminalPane` calls `getOrCreateTerminal(id)` on Reac - **Resume**: `resumeTerminal` creates xterm entry and writes replay data without spawning a new PTY. Used when the webview is recreated while the host retains Live PTYs (Link: Severed โ†’ Resuming โ†’ Live). - **Restore**: `restoreTerminal` creates xterm entry and spawns a new PTY with saved cwd and scrollback. Used on cold start from a saved Snapshot (Link: Cold โ†’ Live). - **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. -- **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`. +- **Shell selection replacement**: the standalone shell dropdown and VS Code shell picker send `dormouse: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`. - 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. - **mount / unmount (DOM)**: `mountElement` reparents the persistent DOM element into a container; `unmountElement` removes it. The Registry entry survives. - **Dispose**: `disposeSession` kills the PTY, disposes xterm, removes the registry entry. Only called on explicit kill (`x`). @@ -303,7 +303,7 @@ Each session also carries `TerminalPaneState` from `docs/specs/terminal-state.md ## Theme -Custom `mousetermTheme` extends dockview's `themeAbyss`: +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` diff --git a/docs/specs/mobile-ui.md b/docs/specs/mobile-ui.md index 8504e5c7..0c5a7156 100644 --- a/docs/specs/mobile-ui.md +++ b/docs/specs/mobile-ui.md @@ -14,7 +14,7 @@ The app should feel like a lightweight mobile terminal playground. It does not need remote sessions, SSH, user accounts, or production infrastructure. The website `/tether` prototype exposes a small floating theme switcher above -the terminal. It uses the shared MouseTerm `ThemePicker`. +the terminal. It uses the shared Dormouse `ThemePicker`. `/tether` uses the same fake playground terminal stack as `/playground`: `PlaygroundShellRegistry` attaches a `TutorialShell` to every spawned pane, the @@ -102,7 +102,7 @@ Touch modes: | 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 | Touches are reserved for terminal text selection and copy/paste. If the TUI is capturing mouse events, MouseTerm activates mouse override for the active pane. | +| Text selection | `Select` | `CursorTextIcon` | Always available | Touches are reserved for terminal text selection and copy/paste. If the TUI is capturing mouse events, Dormouse activates mouse override for the active pane. | | Mouse | `Mouse` | `CursorClickIcon` | Only when the active TUI is capturing mouse events | Touches are passed through as terminal mouse input. | Default touch mode is **Gestures**. @@ -285,7 +285,7 @@ Gesture action mappings: | PgUp | `\x1B[5~` | | k | `k` | | Backspace | `\x7F` | -| Paste | Existing MouseTerm paste flow for the active pane | +| Paste | Existing Dormouse paste flow for the active pane | | n | `n` | | โ—€ | `\x1B[D` | | Home | `\x1B[H` | @@ -450,7 +450,7 @@ Prototype behavior: Build exactly this: * One terminal playground screen. -* Floating theme switcher using the shared MouseTerm theme picker. +* Floating theme switcher using the shared Dormouse theme picker. * Touch mode selector: ```text diff --git a/docs/specs/shortcuts.md b/docs/specs/shortcuts.md index bdde8f17..90d48f96 100644 --- a/docs/specs/shortcuts.md +++ b/docs/specs/shortcuts.md @@ -1,8 +1,8 @@ # Keyboard Shortcuts -Complete reference for mouseterm's keyboard shortcuts. Shortcuts are grouped by the mode/context in which they apply. +Complete reference for Dormouse's keyboard shortcuts. Shortcuts are grouped by the mode/context in which they apply. -mouseterm has two modes: +Dormouse has two modes: - **Workspace mode** (a.k.a. "command" mode internally) โ€” keys drive pane layout. - **Terminal mode** (a.k.a. "passthrough" mode) โ€” keys go to the running program, except copy/paste and the mode-switch gesture. diff --git a/docs/specs/terminal-state.md b/docs/specs/terminal-state.md index 60ab1616..c4c946e9 100644 --- a/docs/specs/terminal-state.md +++ b/docs/specs/terminal-state.md @@ -4,7 +4,7 @@ ## Goal -MouseTerm models terminal panes by: +Dormouse models terminal panes by: - latest reported working directory - current command line diff --git a/docs/specs/theme.md b/docs/specs/theme.md index b8dccca6..2e01c91d 100644 --- a/docs/specs/theme.md +++ b/docs/specs/theme.md @@ -1,15 +1,15 @@ # Theme Spec -MouseTerm's theme contract is intentionally small: render the terminal chrome +Dormouse's theme contract is intentionally small: render the terminal chrome with VSCode-appropriate surfaces, and render terminal content with theme-appropriate xterm.js colors. VSCode extension mode gets `--vscode-*` variables from VSCode. Standalone and website mode apply the same shape of variables to `document.body` with -`applyTheme()` from a bundled or installed MouseTerm theme. Both paths run the +`applyTheme()` from a bundled or installed Dormouse theme. Both paths run the same consumed-token resolver from `lib/src/lib/themes/vscode-color-resolver.ts` so omitted theme JSON keys behave like VSCode registry defaults before -MouseTerm renders. +Dormouse renders. ## Surface hierarchy @@ -48,7 +48,7 @@ That is accepted; terminal content still uses the theme's terminal palette. ## Runtime model -MouseTerm has two theme layers: +Dormouse has two theme layers: 1. `--vscode-*` variables hold imported or host-provided VSCode color data. 2. `--color-*` variables in `lib/src/theme.css` provide semantic Tailwind @@ -58,7 +58,7 @@ MouseTerm has two theme layers: missing consumed variables through the VSCode resolver, and adds either `vscode-light` or `vscode-dark` for consumers that need the theme type. In real VSCode webviews, `installVscodeThemeVarResolver()` runs before React renders; -it reads host-provided variables, materializes only missing MouseTerm-consumed +it reads host-provided variables, materializes only missing Dormouse-consumed variables on `body.style`, and removes stale materialized variables when the host starts providing a real value. @@ -69,10 +69,10 @@ declarations as the runtime source of truth. `theme.css` must not contain hardcoded color defaults or `var(..., fallback)` chains. Runtime hosts plus the shared resolver are responsible for providing -the consumed `--vscode-*` variables before MouseTerm renders. +the consumed `--vscode-*` variables before Dormouse renders. VSCode color IDs with `null` registry defaults need component-equivalent -materialization because MouseTerm consumes them through direct CSS variables. +materialization because Dormouse consumes them through direct CSS variables. Important cases: - `list.inactiveSelectionForeground` resolves to normal foreground @@ -81,7 +81,7 @@ Important cases: where an inactive selected row does not force active-selection white text. - Null foregrounds inherit the nearest normal foreground. - Null backgrounds inherit the relevant surface. -- Null border colors materialize as `transparent` so MouseTerm's existing +- Null border colors materialize as `transparent` so Dormouse's existing border geometry does not accidentally draw in `currentColor`. ## Terminal color contract @@ -106,7 +106,7 @@ terminals. `terminal-registry.ts` remains the public facade for callers. ## Theme data -Bundled and installed themes are represented by `MouseTermTheme` objects in +Bundled and installed themes are represented by `DormouseTheme` objects in `lib/src/lib/themes/`. A theme's `vars` map contains only consumed `--vscode-*` variables plus resolver dependencies. `convertVscodeThemeColors()` filters imported VSCode theme JSON to `CONSUMED_VSCODE_KEYS`; themes used @@ -134,15 +134,15 @@ rings outside a full Wall instance. ## Theme debugger -MouseTerm includes a diagnostic-only Theme Debugger shared by VSCode, +Dormouse includes a diagnostic-only Theme Debugger shared by VSCode, standalone, and the website playground. It never mutates theme storage or terminal colors. It captures the current DOM-visible theme state and shows: -- active MouseTerm theme metadata when `applyTheme()` is the source +- active Dormouse theme metadata when `applyTheme()` is the source (standalone/playground); real VSCode webviews show only the inferred VSCode theme kind because VSCode exposes CSS variables, not raw built-in theme JSON. - visible `--vscode-*` variables, marked as host/theme-provided or - MouseTerm-materialized. + Dormouse-materialized. - resolver traces for every resolvable consumed variable: provided value, registry default for the current kind, null-default fallback path, final resolved value, and origin. @@ -156,8 +156,8 @@ Standalone, playground, and the website `/tether` prototype expose the debugger as `Debug current theme` in the `ThemePicker` menu. `/tether` uses the same picker in the desktop page header and as a floating control above the mobile terminal prototype, both with the Kimbie Dark default theme fallback. VSCode -opens it through the `mouseterm.debugTheme` command and the -`mouseterm:openThemeDebugger` extension-to-webview message. The debugger's +opens it through the `dormouse.debugTheme` command and the +`dormouse:openThemeDebugger` extension-to-webview message. The debugger's copied report is a shareable text dump of the same snapshot. ## Maintainer checklist @@ -171,7 +171,7 @@ When changing theme behavior: dependency used by chrome, terminal rendering, selection UI, theme-picker inline styles, or resolver fallback paths. - Keep xterm.js terminal colors sourced from `--vscode-terminal-*` variables, - not from MouseTerm chrome tokens. + not from Dormouse chrome tokens. - Keep debugger dynamic-pick reporting and runtime dynamic-palette picks sharing `pickDoorPair()` and `pickFocusRing()`; do not fork those rules in UI code. - Do not add hardcoded color defaults or CSS variable fallback chains to diff --git a/docs/specs/transport.md b/docs/specs/transport.md index dfe2879c..26a99b9b 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -52,7 +52,7 @@ Both are capped at 1M chars per PTY. When the cap is reached, oldest chunks are ``` 1. Webview becomes visible (or panel deserializes). -2. Webview sends: { type: 'mouseterm:init' }. +2. Webview sends: { type: 'dormouse:init' }. 3. Host responds with: - { type: 'pty:list', ptys: [{ id, alive, exitCode }] } // all owned PTYs - { type: 'pty:replay', id, data } // buffered output per PTY @@ -60,7 +60,7 @@ Both are capped at 1M chars per PTY. When the cap is reached, oldest chunks are 5. If the saved session covers those live PTYs, the frontend uses the saved dockview layout when its visible panels match and reattaches saved minimized doors; minimized PTYs are registered but remain doors instead of visible panes. ``` -For cold restore (no live PTYs), the webview falls back to saved session state: spawns new PTYs in saved CWDs using the currently selected MouseTerm shell, injects saved scrollback (with trailing newline to avoid the zsh `%` artifact), and restores dockview layout. The entry module (`reconnect.ts`) uses a 500ms timeout when waiting for the PTY list. +For cold restore (no live PTYs), the webview falls back to saved session state: spawns new PTYs in saved CWDs using the currently selected Dormouse shell, injects saved scrollback (with trailing newline to avoid the zsh `%` artifact), and restores dockview layout. The entry module (`reconnect.ts`) uses a 500ms timeout when waiting for the PTY list. ## Message protocol @@ -77,9 +77,9 @@ Message types live in `vscode-ext/src/message-types.ts` (the canonical schema; o | `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) | -| `mouseterm:init` | Trigger resume: get PTY list + replay data | -| `mouseterm:saveState` | Frontend persisting session state | -| `mouseterm:flushSessionSaveDone` | Ack for host-triggered flush (matched by requestId) | +| `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 | @@ -99,15 +99,15 @@ Message types live in `vscode-ext/src/message-types.ts` (the canonical schema; o | `pty:data` | PTY output after supported OSC sequences have been parsed/stripped (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 `mouseterm:init`) | -| `pty:replay` | Buffered raw output since spawn (response to `mouseterm:init`); the webview parses semantic OSCs during replay reconstruction without triggering alerts | +| `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) | -| `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. | -| `mouseterm:selectedShell` | Update the webview's default shell options for later split/spawn/restore paths. | -| `mouseterm:flushSessionSave` | Request webview to save state now (host shutdown trigger, matched by requestId) | -| `mouseterm:openThemeDebugger` | Command-triggered request to open the shared theme debugger dialog | +| `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) | The OSC parsing/stripping rules that produce `pty:data` and `terminal:semanticEvents` are specified in `docs/specs/OSC.md`. diff --git a/docs/specs/tutorial.md b/docs/specs/tutorial.md index fe49e6d6..fa1e88ea 100644 --- a/docs/specs/tutorial.md +++ b/docs/specs/tutorial.md @@ -1,14 +1,14 @@ # Playground Tutorial -At the `/playground` route on the website. Interactive TUI: each item starts pending, the first incomplete item is marked as active, and completed items become green checks when MouseTerm detects the corresponding action. +At the `/playground` route on the website. Interactive TUI: each item starts pending, the first incomplete item is marked as active, and completed items become green checks when Dormouse detects the corresponding action. ## Architecture Three browser-side pieces in `website/src/lib/`, mirroring the pattern in `website/src/lib/ascii-splash-runner.ts` (xterm alt-screen + `FakePtyAdapter` boundary, no Node `terminal-kit` package): - **`tut-runner.ts`** (`TutRunner`) โ€” alt-screen TUI. Subscribes to `TutorialState` and re-renders whenever progress changes. Routes input bytes via `FakePtyAdapter.writePty(id, โ€ฆ)`. -- **`tut-detector.ts`** (`TutDetector`) โ€” wires app events to `TutorialState.markComplete(id)`. Subscribes to `DockviewApi.onDidActivePanelChange`, the `WallEvent` stream, the `subscribeToActivity` store from `mouseterm-lib/lib/terminal-registry`, and the `subscribeToMouseSelection` store from `mouseterm-lib/lib/mouse-selection`. -- **`tutorial-state.ts`** (`TutorialState`) โ€” single in-memory progress store, persisted as a JSON array of completed item ids under the `mouseterm-tut-v3` localStorage key. +- **`tut-detector.ts`** (`TutDetector`) โ€” wires app events to `TutorialState.markComplete(id)`. Subscribes to `DockviewApi.onDidActivePanelChange`, the `WallEvent` stream, the `subscribeToActivity` store from `dormouse-lib/lib/terminal-registry`, and the `subscribeToMouseSelection` store from `dormouse-lib/lib/mouse-selection`. +- **`tutorial-state.ts`** (`TutorialState`) โ€” single in-memory progress store, persisted as a JSON array of completed item ids under the `dormouse-tut-v3` localStorage key. - **`tut-items.ts`** โ€” section + item definitions (titles, hints) shared by runner and detector. Item ids are stable; they are the localStorage key suffixes. ## Layout @@ -66,7 +66,7 @@ The detector subscribes to `subscribeToActivity()` and tracks per-id `(status, w | `al-todo-clear` | Press passthrough Enter to clear the TODO | `todo` transitions `true โ†’ false` | | `al-todo-manual` | Manually add a TODO (`t` or right-click) | `todo` transitions `false โ†’ true` while previous status was NOT `ALERT_RINGING` | -The detector remembers the most recent pane whose `watchingEnabled` flag is true, even when projected `status` is currently owned by protocol or command-exit alert tracks. The Alert section view shows a runner-local instruction: "Press `s` here to start a fake busy task." `s` is **not** a real MouseTerm shortcut; it is intercepted by `TutRunner` only while the Alert section is open. When pressed, the runner does two things: +The detector remembers the most recent pane whose `watchingEnabled` flag is true, even when projected `status` is currently owned by protocol or command-exit alert tracks. The Alert section view shows a runner-local instruction: "Press `s` here to start a fake busy task." `s` is **not** a real Dormouse shortcut; it is intercepted by `TutRunner` only while the Alert section is open. When pressed, the runner does two things: 1. Resolves that pane to its current PTY session id, then calls `adapter.pumpActivity(sessionId, BUSY_DEMO_DURATION_MS, 800)` โ€” drives the alert-manager's activity monitor on the same WATCHING-enabled session with **no text output**, so the bell tilts to BUSY without scrolling any scenario text. The session id is resolved at trigger time so `Cmd/Ctrl+Arrow` swaps do not leave the tutorial pumping an old pane id. If no WATCHING-enabled pane is known, the runner falls back to `PANE_BOXED` (the changelog pane). `BUSY_DEMO_DURATION_MS` is `cfg.alert.userAttention + 250` so silence begins after the attention idle window has expired, with a small scheduler-jitter guard; otherwise the "user is looking at this pane" check inside `ActivityMonitor.startNeedsAttentionConfirmTimer` would suppress the ring rather than let it fire. 2. Animates a countdown in-place where the "Press sโ€ฆ" hint was: `โ ‹ Fake task will finish in N seconds.` ticking down to 1, then a static `โœ“ Fake task finished. Press s to start another one.` once the activity stops. Detection is purely timing-based via the existing `ActivityMonitor`, so no shell integration is required. @@ -100,14 +100,14 @@ While the Copy paste section is open, pressing `p` toggles the **Place To Paste* ## Storage -- Completion: `localStorage["mouseterm-tut-v3"] = JSON.stringify([...completedItemIds])`. Removed on `TutorialState.reset()`. Unknown ids in a stored payload are filtered out on load, so renaming an id is a one-way reset for that item. -- Legacy keys `mouseterm-tutorial-step-N` and `mouseterm-tut-v2-*` from previous designs are not read; new playground sessions get a fresh start. +- Completion: `localStorage["dormouse-tut-v3"] = JSON.stringify([...completedItemIds])`. Removed on `TutorialState.reset()`. Unknown ids in a stored payload are filtered out on load, so renaming an id is a one-way reset for that item. +- Legacy keys `dormouse-tutorial-step-N` and `dormouse-tut-v2-*` from previous designs are not read; new playground sessions get a fresh start. ## Theme Picker -Implemented in `mouseterm-lib/lib/themes` and `mouseterm-lib/components/ThemePicker`. +Implemented in `dormouse-lib/lib/themes` and `dormouse-lib/components/ThemePicker`. -Bundled themes are provided by `mouseterm-lib/lib/themes` and include only GitHub variants. Users can install additional themes from OpenVSX through the dropdown footer action. +Bundled themes are provided by `dormouse-lib/lib/themes` and include only GitHub variants. Users can install additional themes from OpenVSX through the dropdown footer action. The picker appears only on `/playground`, inside `SiteHeader`, labeled `Theme:`. The trigger opens a dropdown of bundled and installed themes. The dropdown footer is always `Install theme from OpenVSX`, which opens the theme store dialog. Installed theme rows include an `X` delete control; deletion requires browser confirmation before removing the theme from localStorage. If the active installed theme is deleted, the picker falls back to the first bundled theme and applies it immediately. diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md index f0b9a067..845b46c2 100644 --- a/docs/specs/vscode.md +++ b/docs/specs/vscode.md @@ -1,10 +1,10 @@ -# MouseTerm VS Code Integration Spec +# 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. ## What's built -MouseTerm has two hosting modes: a `WebviewView` in the bottom panel (alongside Terminal, Problems, Output) and `WebviewPanel` editor tabs (via `mouseterm.open`, supports multiple instances). Both restore across "Developer: Reload Window". PTY lifecycle is fully decoupled from the webview โ€” PTYs live in the extension host via `pty-manager.ts`, survive panel visibility toggling, and replay buffered output on **resume**. Session persistence works across cold **restore**: pane layout, CWD, scrollback, alert state (enabled/disabled + todo), and resume commands are saved and restored on cold start. The view uses `workspaceState` for persistence; editor panels use VS Code's per-panel `vscode.setState()` so multiple panels don't clobber each other. Alert state is merged into every periodic save (not just deactivate) so it survives even if VS Code kills the extension host before deactivate completes. A `WebviewPanelSerializer` handles editor tab restoration; `onWebviewPanel:mouseterm` activation event ensures the extension activates early enough. Theme integration uses VSCode `--vscode-*` tokens plus MouseTerm semantic `--color-*` tokens, with a small resolver that materializes missing consumed VSCode colors from registry defaults. CSP is strict with nonce-gated scripts. +Dormouse has two hosting modes: a `WebviewView` in the bottom panel (alongside Terminal, Problems, Output) and `WebviewPanel` editor tabs (via `dormouse.open`, supports multiple instances). Both restore across "Developer: Reload Window". PTY lifecycle is fully decoupled from the webview โ€” PTYs live in the extension host via `pty-manager.ts`, survive panel visibility toggling, and replay buffered output on **resume**. Session persistence works across cold **restore**: pane layout, CWD, scrollback, alert state (enabled/disabled + todo), and resume commands are saved and restored on cold start. The view uses `workspaceState` for persistence; editor panels use VS Code's per-panel `vscode.setState()` so multiple panels don't clobber each other. Alert state is merged into every periodic save (not just deactivate) so it survives even if VS Code kills the extension host before deactivate completes. A `WebviewPanelSerializer` handles editor tab restoration; `onWebviewPanel:dormouse` activation event ensures the extension activates early enough. Theme integration uses VSCode `--vscode-*` tokens plus Dormouse semantic `--color-*` tokens, with a small resolver that materializes missing consumed VSCode colors from registry defaults. CSP is strict with nonce-gated scripts. **Architecture:** @@ -70,43 +70,43 @@ Universal PTY/transport invariants live in `docs/specs/transport.md`. The rules - **PTY ownership tracking.** Each router tracks its PTYs in `ownedPtyIds`. A module-level `globalOwnedPtyIds` set prevents a resuming router from stealing PTYs owned by another webview. - **mergeAlertStates on every save path.** Both the frontend periodic save (`onSaveState` callback) and the backend deactivate refresh (`refreshSavedSessionStateFromPtys`) must merge current alert states. Missing this causes alert state to revert on restore. - **retainContextWhenHidden.** Set on both `WebviewPanel` (editor tabs) and `WebviewView` (bottom panel) so that xterm.js DOM, scrollback, and PTY subscriptions survive panel hide/show without going through a resume. -- **Two save sources.** Session state is saved from two places: the frontend (debounced 500ms + 30s interval via `mouseterm:saveState`) and the backend (deactivate flushes webviews then refreshes from live PTYs). Both paths must produce consistent state. +- **Two save sources.** Session state is saved from two places: the frontend (debounced 500ms + 30s interval via `dormouse:saveState`) and the backend (deactivate flushes webviews then refreshes from live PTYs). Both paths must produce consistent state. ### Extension manifest (current) ```jsonc { "activationEvents": [ - "onView:mouseterm.view", - "onWebviewPanel:mouseterm" + "onView:dormouse.view", + "onWebviewPanel:dormouse" ], "contributes": { "commands": [ - { "command": "mouseterm.focus", "title": "MouseTerm: Focus", + { "command": "dormouse.focus", "title": "Dormouse: Focus", "icon": { "light": "icon-tiny-light.png", "dark": "icon-tiny-dark.png" } }, - { "command": "mouseterm.open", "title": "MouseTerm: Open in Editor" }, - { "command": "mouseterm.debugTheme", "title": "MouseTerm: Debug Theme" }, - { "command": "mouseterm.newTerminal", "title": "MouseTerm: New Terminal", + { "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": "mouseterm.selectShell", "title": "MouseTerm: Select Shell", + { "command": "dormouse.selectShell", "title": "Dormouse: Select Shell", "icon": "$(gear)" } ], "menus": { "view/title": [ - { "command": "mouseterm.selectShell", "group": "navigation@1", - "when": "view == mouseterm.view" }, - { "command": "mouseterm.newTerminal", "group": "navigation@2", - "when": "view == mouseterm.view" } + { "command": "dormouse.selectShell", "group": "navigation@1", + "when": "view == dormouse.view" }, + { "command": "dormouse.newTerminal", "group": "navigation@2", + "when": "view == dormouse.view" } ] }, "viewsContainers": { "panel": [ - { "id": "mouseterm-panel", "title": "MouseTerm", "icon": "$(terminal)" } + { "id": "dormouse-panel", "title": "Dormouse", "icon": "$(terminal)" } ] }, "views": { - "mouseterm-panel": [ - { "id": "mouseterm.view", "name": "MouseTerm", "type": "webview" } + "dormouse-panel": [ + { "id": "dormouse.view", "name": "Dormouse", "type": "webview" } ] } } @@ -124,16 +124,16 @@ Extension Host (always running while extension is active) โ”‚ โ”œโ”€โ”€ pty-2 (Process: Live) โ”‚ โ””โ”€โ”€ pty-3 (Process: Exited) โ”‚ -โ”œโ”€โ”€ WebviewView "MouseTerm" (bottom panel) +โ”œโ”€โ”€ WebviewView "Dormouse" (bottom panel) โ”‚ โ””โ”€โ”€ message-router: owns pty-1, pty-2 โ”‚ -โ””โ”€โ”€ WebviewPanel "MouseTerm" (editor tab, optional) +โ””โ”€โ”€ WebviewPanel "Dormouse" (editor tab, optional) โ””โ”€โ”€ message-router: owns pty-3 ``` VS Code-specific consequences: -- Hiding the MouseTerm panel doesn't kill its PTYs. +- Hiding the Dormouse panel doesn't kill its PTYs. - VS Code toggling the panel visibility doesn't destroy sessions. - Multiple VS Code windows each get their own extension host process, and therefore their own pty-host child process. @@ -141,23 +141,23 @@ PTY lifecycle, buffering, the reconnection sequence, and the full message protoc ### Shell selection -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. +The VS Code view title contributes `Dormouse: Select Shell` and `Dormouse: New Terminal`. The selected shell name is mirrored into the `WebviewView.description`, and `dormouse:selectedShell` keeps the webview's default-shell slot current for split/spawn/restore paths. -`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. +`dormouse.newTerminal` focuses the Dormouse view and posts `dormouse:newTerminal` with the currently selected shell. `dormouse.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 `dormouse: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. ### Serialization and restore `WebviewPanelSerializer` is registered so VS Code can restore editor panels after restart: ``` -activationEvents: ["onWebviewPanel:mouseterm"] +activationEvents: ["onWebviewPanel:dormouse"] ``` The persisted-session shape (`PersistedSession` / `PersistedPane` / `PersistedAlertState` / `PersistedDoor`) lives in `docs/specs/transport.md`; it is shared with the standalone and fake adapters. **VS Code persistence flow:** -1. Frontend saves state periodically (debounced 500ms + 30s interval) via `mouseterm:saveState` message. +1. Frontend saves state periodically (debounced 500ms + 30s interval) via `dormouse:saveState` message. 2. Router's `onSaveState` callback merges in current alert states via `mergeAlertStates()`. 3. WebviewView writes to `workspaceState`; WebviewPanels persist via `vscode.setState()` (per-panel, no clobbering). 4. On deactivate: flush all sessions from webviews (1s timeout), then refresh from live PTYs (queries CWD + scrollback while processes are still alive). @@ -166,7 +166,7 @@ The persisted-session shape (`PersistedSession` / `PersistedPane` / `PersistedAl ### Theme integration -Two-layer CSS variable system: VS Code injects `--vscode-*` tokens; `lib/src/theme.css` maps them directly to semantic `--color-*` tokens for use in Tailwind utility classes. The webview entry point installs `installVscodeThemeVarResolver()` before React renders. That resolver reads VSCode-provided variables, materializes only missing MouseTerm-consumed variables on `body.style`, and watches `body`/`html` class and style mutations so theme changes recompute those materialized values. +Two-layer CSS variable system: VS Code injects `--vscode-*` tokens; `lib/src/theme.css` maps them directly to semantic `--color-*` tokens for use in Tailwind utility classes. The webview entry point installs `installVscodeThemeVarResolver()` before React renders. That resolver reads VSCode-provided variables, materializes only missing Dormouse-consumed variables on `body.style`, and watches `body`/`html` class and style mutations so theme changes recompute those materialized values. Example of the pattern: ```css @@ -176,14 +176,14 @@ Example of the pattern: --color-header-inactive-fg: var(--vscode-list-inactiveSelectionForeground); ``` -`theme.css` intentionally has no hardcoded color defaults or CSS variable fallback chains. The resolver duplicates VSCode registry defaults for the MouseTerm-consumed color IDs, including `null` default behavior where MouseTerm needs a concrete CSS variable. In particular, `list.inactiveSelectionForeground` resolves to normal foreground inheritance, not `list.activeSelectionForeground`; this matches VSCode's list/tree selected-row behavior for built-in Light. +`theme.css` intentionally has no hardcoded color defaults or CSS variable fallback chains. The resolver duplicates VSCode registry defaults for the Dormouse-consumed color IDs, including `null` default behavior where Dormouse needs a concrete CSS variable. In particular, `list.inactiveSelectionForeground` resolves to normal foreground inheritance, not `list.activeSelectionForeground`; this matches VSCode's list/tree selected-row behavior for built-in Light. A `MutationObserver` in `lib/src/lib/terminal-theme.ts` watches for VS Code theme changes on `body`/`html` (class and style attribute mutations) and live-updates all xterm.js instances. The `terminal-registry.ts` facade still exposes the public lifecycle APIs. The theme resolver has its own observer on the same attributes so derived `--vscode-*` variables stay in sync before xterm rereads the terminal palette. -`mouseterm.debugTheme` focuses the MouseTerm WebviewView and posts -`mouseterm:openThemeDebugger` to the webview. `VSCodeAdapter` converts that +`dormouse.debugTheme` focuses the Dormouse WebviewView and posts +`dormouse:openThemeDebugger` to the webview. `VSCodeAdapter` converts that message into the browser event consumed by the shared Theme Debugger. The -debugger traces VSCode-exposed `--vscode-*` variables and MouseTerm +debugger traces VSCode-exposed `--vscode-*` variables and Dormouse materialized fallbacks, but it does not attempt to read raw built-in VSCode theme files. @@ -204,16 +204,16 @@ connect-src ${webview.cspSource}; ``` pnpm build:vscode = - 1. pnpm --filter mouseterm-lib build (TypeScript compile) - 2. pnpm --filter mouseterm build:frontend (Vite: lib -> vscode-ext/media/) - 3. pnpm --filter mouseterm build (esbuild: extension.ts + pty-host.js -> dist/, + 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-mouseterm-vscode" from .vscode/tasks.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) ``` @@ -226,37 +226,37 @@ The Vite config for the extension (`vscode-ext/vite.config.ts`) sets `root: ../l ### Context keys -Set context keys so menus and extensions can target MouseTerm state: +Set context keys so menus and extensions can target Dormouse state: ```typescript -// Set when any MouseTerm webview has focus -vscode.commands.executeCommand('setContext', 'mouseterm.active', true); +// Set when any Dormouse webview has focus +vscode.commands.executeCommand('setContext', 'dormouse.active', true); -// Set when MouseTerm is in passthrough/terminal mode (keys go to PTY) -vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'terminal'); +// Set when Dormouse is in passthrough/terminal mode (keys go to PTY) +vscode.commands.executeCommand('setContext', 'dormouse.mode', 'terminal'); -// Set when MouseTerm is in normal/navigation mode (keys go to MouseTerm UI) -vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'normal'); +// Set when Dormouse is in normal/navigation mode (keys go to Dormouse UI) +vscode.commands.executeCommand('setContext', 'dormouse.mode', 'normal'); ``` ### Commands | Command | Description | |---------|-------------| -| `mouseterm.focus` | Focus the MouseTerm panel view | -| `mouseterm.newPane` | Split a new pane in MouseTerm | -| `mouseterm.closePane` | Close the focused pane | -| `mouseterm.nextPane` | Focus next pane | -| `mouseterm.prevPane` | Focus previous pane | -| `mouseterm.enterTerminalMode` | Switch to passthrough mode | -| `mouseterm.enterNormalMode` | Switch to navigation mode | -| `mouseterm.listSessions` | Show QuickPick of all live PTY sessions | -| `mouseterm.reattach` | Reattach a minimized PTY to a pane | +| `dormouse.focus` | Focus the Dormouse panel view | +| `dormouse.newPane` | Split a new pane in Dormouse | +| `dormouse.closePane` | Close the focused pane | +| `dormouse.nextPane` | Focus next pane | +| `dormouse.prevPane` | Focus previous pane | +| `dormouse.enterTerminalMode` | Switch to passthrough mode | +| `dormouse.enterNormalMode` | Switch to navigation mode | +| `dormouse.listSessions` | Show QuickPick of all live PTY sessions | +| `dormouse.reattach` | Reattach a minimized PTY to a pane | ### Not yet implemented -- `TerminalProfileProvider` not registered โ€” MouseTerm doesn't appear in the terminal `+` dropdown -- Context keys not set (`mouseterm.active`, `mouseterm.mode`) โ€” needed for conditional keybindings -- Commands not registered: `mouseterm.newPane`, `closePane`, `nextPane`, `prevPane`, `enterTerminalMode`, `enterNormalMode`, `listSessions`, `reattach` +- `TerminalProfileProvider` not registered โ€” Dormouse doesn't appear in the terminal `+` dropdown +- Context keys not set (`dormouse.active`, `dormouse.mode`) โ€” needed for conditional keybindings +- Commands not registered: `dormouse.newPane`, `closePane`, `nextPane`, `prevPane`, `enterTerminalMode`, `enterNormalMode`, `listSessions`, `reattach` - No status bar item showing active session count - No QuickPick for listing/reattaching PTY sessions diff --git a/lib/.storybook/main.ts b/lib/.storybook/main.ts index fc6b3a3b..1776a5af 100644 --- a/lib/.storybook/main.ts +++ b/lib/.storybook/main.ts @@ -18,7 +18,7 @@ const config: StorybookConfig = { '@tauri-apps/api/core': stub, '@tauri-apps/plugin-shell': stub, '@tauri-apps/plugin-updater': stub, - 'mouseterm-lib': path.resolve(here, '..', 'src'), + 'dormouse-lib': path.resolve(here, '..', 'src'), }; return config; }, diff --git a/lib/.storybook/themes.ts b/lib/.storybook/themes.ts index a26fa129..8f159661 100644 --- a/lib/.storybook/themes.ts +++ b/lib/.storybook/themes.ts @@ -3,10 +3,10 @@ * applyTheme(), so isolated stories receive VSCode registry defaults too. */ import _bundled from '../src/lib/themes/bundled.json'; -import type { MouseTermTheme } from '../src/lib/themes/types'; +import type { DormouseTheme } from '../src/lib/themes/types'; import { completeThemeVars } from '../src/lib/themes/vscode-color-resolver'; -const bundled = _bundled as unknown as MouseTermTheme[]; +const bundled = _bundled as unknown as DormouseTheme[]; const STORYBOOK_HOST_TYPOGRAPHY_VARS: Record<string, string> = { '--vscode-font-size': '13px', @@ -17,7 +17,7 @@ const STORYBOOK_HOST_TYPOGRAPHY_VARS: Record<string, string> = { }; export const VSCODE_THEMES: Record<string, Record<string, string>> = {}; -export const VSCODE_THEME_TYPES: Record<string, MouseTermTheme['type']> = {}; +export const VSCODE_THEME_TYPES: Record<string, DormouseTheme['type']> = {}; for (const theme of bundled) { VSCODE_THEME_TYPES[theme.label] = theme.type; VSCODE_THEMES[theme.label] = completeThemeVars( diff --git a/lib/index.html b/lib/index.html index 42d08170..bb127d78 100644 --- a/lib/index.html +++ b/lib/index.html @@ -3,7 +3,7 @@ <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> - <title>MouseTerm + Dormouse
diff --git a/lib/package.json b/lib/package.json index 83886588..4d071773 100644 --- a/lib/package.json +++ b/lib/package.json @@ -1,5 +1,5 @@ { - "name": "mouseterm-lib", + "name": "dormouse-lib", "version": "0.9.1", "license": "FSL-1.1-MIT", "private": true, @@ -15,8 +15,8 @@ }, "dependencies": { "@phosphor-icons/react": "^2.1.10", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", + "@xterm/addon-fit": "0.12.0-beta.216", + "@xterm/xterm": "6.1.0-beta.216", "clsx": "^2.1.1", "dockview-react": "^5.1.0", "react": "^19.2.0", diff --git a/lib/scripts/bundle-themes.mjs b/lib/scripts/bundle-themes.mjs index 0e3f177a..e5827e4e 100644 --- a/lib/scripts/bundle-themes.mjs +++ b/lib/scripts/bundle-themes.mjs @@ -45,7 +45,7 @@ const EXCLUDED_THEMES = new Set([ ]); /** - * VSCode theme color keys consumed by MouseTerm. + * VSCode theme color keys consumed by Dormouse. * Keep in sync with lib/src/lib/themes/convert.ts. */ const CONSUMED_KEYS = new Set([ diff --git a/lib/src/components/ThemeDebugger.tsx b/lib/src/components/ThemeDebugger.tsx index 9e9f5352..ed07d6dd 100644 --- a/lib/src/components/ThemeDebugger.tsx +++ b/lib/src/components/ThemeDebugger.tsx @@ -8,7 +8,7 @@ import { type VscodeThemeVarTraceOrigin, } from '../lib/themes'; -export const OPEN_THEME_DEBUGGER_EVENT = 'mouseterm:openThemeDebugger'; +export const OPEN_THEME_DEBUGGER_EVENT = 'dormouse:openThemeDebugger'; export function openThemeDebugger(): void { window.dispatchEvent(new CustomEvent(OPEN_THEME_DEBUGGER_EVENT)); @@ -43,7 +43,7 @@ function originClass(origin: VscodeThemeVarTraceOrigin | VisibleVarOrigin): stri case 'host-provided': return 'text-success'; case 'registry-default': - case 'mouseterm-materialized': + case 'dormouse-materialized': return '[color:var(--vscode-terminal-ansiYellow)]'; case 'fallback': return 'text-muted'; diff --git a/lib/src/components/ThemePicker.tsx b/lib/src/components/ThemePicker.tsx index cc114640..b87c4cc6 100644 --- a/lib/src/components/ThemePicker.tsx +++ b/lib/src/components/ThemePicker.tsx @@ -1,6 +1,6 @@ import { useCallback, useId, useRef, useState } from 'react'; import { CaretDownIcon } from '@phosphor-icons/react'; -import type { MouseTermTheme } from '../lib/themes'; +import type { DormouseTheme } from '../lib/themes'; import { applyTheme, getAllThemes, @@ -32,7 +32,7 @@ export function ThemePicker({ variant, className = '', defaultThemeId }: ThemePi // the first paint already has --vscode-* on body โ€” eliminates the flash of // unstyled chrome on the website playground where ThemePicker mounts before // any other entry point has a chance to apply a theme. - const initialState = useRef<{ themes: MouseTermTheme[]; activeId: string }>(null); + const initialState = useRef<{ themes: DormouseTheme[]; activeId: string }>(null); if (initialState.current === null) { const restored = restoreActiveTheme(defaultThemeId); const themes = getAllThemes(); @@ -66,7 +66,7 @@ export function ThemePicker({ variant, className = '', defaultThemeId }: ThemePi setOpen(false); }; - const deleteTheme = (theme: MouseTermTheme) => { + const deleteTheme = (theme: DormouseTheme) => { if (theme.origin.kind !== 'installed') return; const confirmed = window.confirm(`Delete "${theme.label}"?`); if (!confirmed) return; diff --git a/lib/src/components/Wall.tsx b/lib/src/components/Wall.tsx index 584dc849..f96a9a8e 100644 --- a/lib/src/components/Wall.tsx +++ b/lib/src/components/Wall.tsx @@ -84,9 +84,9 @@ export { TerminalPaneHeader } from './wall/TerminalPaneHeader'; // --- Theme --- -const mousetermTheme: DockviewTheme = { +const dormouseTheme: DockviewTheme = { ...themeAbyss, - name: 'mouseterm', + name: 'dormouse', gap: 6, dndOverlayMounting: 'absolute', dndPanelOverlay: 'group', @@ -595,8 +595,8 @@ export function Wall({ showShellSpawnNotice(newId, `Opened ${shellName}`); } }; - window.addEventListener('mouseterm:new-terminal', handler); - return () => window.removeEventListener('mouseterm:new-terminal', handler); + window.addEventListener('dormouse:new-terminal', handler); + return () => window.removeEventListener('dormouse:new-terminal', handler); }, [generatePaneId, selectPane, showShellSpawnNotice]); const addSplitPanel = useCallback(( @@ -745,7 +745,7 @@ export function Wall({ components={components} tabComponents={tabComponents} onReady={handleReady} - theme={mousetermTheme} + theme={dormouseTheme} singleTabMode="fullwidth" /> diff --git a/lib/src/components/theme-picker/ThemeSwatch.tsx b/lib/src/components/theme-picker/ThemeSwatch.tsx index 375ddaca..951b1861 100644 --- a/lib/src/components/theme-picker/ThemeSwatch.tsx +++ b/lib/src/components/theme-picker/ThemeSwatch.tsx @@ -1,7 +1,7 @@ -import type { MouseTermTheme } from '../../lib/themes'; +import type { DormouseTheme } from '../../lib/themes'; import { themePickerStyles as styles } from './styles'; -export function ThemeSwatch({ theme, size }: { theme: MouseTermTheme; size: 'sm' | 'md' }) { +export function ThemeSwatch({ theme, size }: { theme: DormouseTheme; size: 'sm' | 'md' }) { const swatchClass = size === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'; return ( diff --git a/lib/src/components/wall/use-session-persistence.ts b/lib/src/components/wall/use-session-persistence.ts index 05a36870..0c814ae7 100644 --- a/lib/src/components/wall/use-session-persistence.ts +++ b/lib/src/components/wall/use-session-persistence.ts @@ -98,7 +98,7 @@ export function useSessionPersistence({ platform.onRequestSessionFlush(handleSessionFlushRequest); window.addEventListener('pagehide', handlePageHide); - // Inert in Tauri standalone today; see diffplug/mouseterm#38 and tauri-apps/tauri#14373. + // Inert in Tauri standalone today; see diffplug/dormouse#38 and tauri-apps/tauri#14373. const unsubFilesDropped = platform.onFilesDropped?.((paths) => { if (paths.length === 0) return; const sid = selectedTypeRef.current === 'pane' ? selectedIdRef.current : null; diff --git a/lib/src/lib/ansi.ts b/lib/src/lib/ansi.ts index a34d0abd..1d3ecbe0 100644 --- a/lib/src/lib/ansi.ts +++ b/lib/src/lib/ansi.ts @@ -23,10 +23,10 @@ export const LEAVE_ALT_SCREEN = `${CLEAR_SCREEN}${CURSOR_HOME}${ESC}?25h${ESC}?1 // SGR mouse-reporting toggles. xterm parses these and the wall's // mouse-mode-observer flips the cursor-icon override on/off so the user -// knows MouseTerm is "trapping the mouse" while the program runs. +// knows Dormouse is "trapping the mouse" while the program runs. export const MOUSE_ENABLE = `${ESC}?1000h${ESC}?1002h${ESC}?1003h${ESC}?1006h`; export const MOUSE_DISABLE = `${ESC}?1003l${ESC}?1002l${ESC}?1000l${ESC}?1006l`; -// Stylized `user@mouseterm:~$ ` prompt used by the playground shell and +// Stylized `user@dormouse:~$ ` prompt used by the playground shell and // by canned scenarios so they look the same. -export const PROMPT = `${fg(32)}user${RESET}@${fg(36)}mouseterm${RESET}:${BOLD}${fg(34)}~${RESET}$ `; +export const PROMPT = `${fg(32)}user${RESET}@${fg(36)}dormouse${RESET}:${BOLD}${fg(34)}~${RESET}$ `; diff --git a/lib/src/lib/platform/types.ts b/lib/src/lib/platform/types.ts index 0568846e..7b41c58b 100644 --- a/lib/src/lib/platform/types.ts +++ b/lib/src/lib/platform/types.ts @@ -33,7 +33,7 @@ export interface PlatformAdapter { // instead of navigator.clipboard.readText() so adapters whose webview pops // a "Paste from " confirmation (notably Tauri's WKWebView) can bypass it. readClipboardText?(): Promise; - // Only present on adapters with a native (non-DOM) drag-drop source. Currently inert in Tauri; see diffplug/mouseterm#38 and tauri-apps/tauri#14373. + // Only present on adapters with a native (non-DOM) drag-drop source. Currently inert in Tauri; see diffplug/dormouse#38 and tauri-apps/tauri#14373. onFilesDropped?(handler: (paths: string[]) => void): () => void; // PTY event listeners diff --git a/lib/src/lib/platform/vscode-adapter.test.ts b/lib/src/lib/platform/vscode-adapter.test.ts index c6271f35..2800c721 100644 --- a/lib/src/lib/platform/vscode-adapter.test.ts +++ b/lib/src/lib/platform/vscode-adapter.test.ts @@ -142,14 +142,14 @@ describe('VSCodeAdapter PTY exit handling', () => { it('forwards shell replacement requests from the extension host', () => { const requests: unknown[] = []; - windowTarget.addEventListener('mouseterm:new-terminal', (event) => { + windowTarget.addEventListener('dormouse:new-terminal', (event) => { requests.push((event as CustomEvent).detail); }); new VSCodeAdapter(); windowTarget.dispatchEvent(new MessageEvent('message', { data: { - type: 'mouseterm:newTerminal', + type: 'dormouse:newTerminal', shell: '/bin/zsh', args: ['-l'], name: 'zsh', diff --git a/lib/src/lib/platform/vscode-adapter.ts b/lib/src/lib/platform/vscode-adapter.ts index 0f271875..d22ec84e 100644 --- a/lib/src/lib/platform/vscode-adapter.ts +++ b/lib/src/lib/platform/vscode-adapter.ts @@ -60,7 +60,7 @@ export class VSCodeAdapter implements PlatformAdapter { } } else if (msg.type === 'terminal:semanticEvents') { applyTerminalSemanticEventsByPtyId(msg.id, msg.events ?? []); - } else if (msg.type === 'mouseterm:flushSessionSave') { + } else if (msg.type === 'dormouse:flushSessionSave') { for (const handler of this.flushRequestHandlers) { handler({ requestId: msg.requestId }); } @@ -75,8 +75,8 @@ export class VSCodeAdapter implements PlatformAdapter { attentionDismissedRing: msg.attentionDismissedRing, }); } - } else if (msg.type === 'mouseterm:newTerminal') { - window.dispatchEvent(new CustomEvent('mouseterm:new-terminal', { + } else if (msg.type === 'dormouse:newTerminal') { + window.dispatchEvent(new CustomEvent('dormouse:new-terminal', { detail: { shell: msg.shell, args: msg.args, @@ -85,10 +85,10 @@ export class VSCodeAdapter implements PlatformAdapter { announce: msg.announce, }, })); - } else if (msg.type === 'mouseterm:selectedShell') { + } else if (msg.type === 'dormouse:selectedShell') { setDefaultShellOpts(msg.shell ? { shell: msg.shell, args: msg.args } : null); - } else if (msg.type === 'mouseterm:openThemeDebugger') { - window.dispatchEvent(new CustomEvent('mouseterm:openThemeDebugger')); + } else if (msg.type === 'dormouse:openThemeDebugger') { + window.dispatchEvent(new CustomEvent('dormouse:openThemeDebugger')); } }); } @@ -194,7 +194,7 @@ export class VSCodeAdapter implements PlatformAdapter { } requestInit(): void { - this.vscode.postMessage({ type: 'mouseterm:init' }); + this.vscode.postMessage({ type: 'dormouse:init' }); } onPtyList(handler: (detail: { ptys: PtyInfo[] }) => void): void { @@ -222,7 +222,7 @@ export class VSCodeAdapter implements PlatformAdapter { } notifySessionFlushComplete(requestId: string): void { - this.vscode.postMessage({ type: 'mouseterm:flushSessionSaveDone', requestId }); + this.vscode.postMessage({ type: 'dormouse:flushSessionSaveDone', requestId }); } // --- Alert management (proxied to extension host) --- @@ -284,7 +284,7 @@ export class VSCodeAdapter implements PlatformAdapter { saveState(state: unknown): void { this.hostState = state; this.vscode.setState(state); - this.vscode.postMessage({ type: 'mouseterm:saveState', state }); + this.vscode.postMessage({ type: 'dormouse:saveState', state }); } getState(): unknown { diff --git a/lib/src/lib/shell-defaults.ts b/lib/src/lib/shell-defaults.ts index b8cabab6..7a3f57f3 100644 --- a/lib/src/lib/shell-defaults.ts +++ b/lib/src/lib/shell-defaults.ts @@ -1,7 +1,7 @@ // Shared "currently selected" shell, used when spawning without an explicit // choice (e.g. a keyboard-driven split). Seeded before standalone Wall mount, // updated by AppBar's ShellDropdown, and updated by the VSCode extension -// pushing mouseterm:selectedShell. +// pushing dormouse:selectedShell. // // Extracted into its own module to avoid circular dependencies between // terminal-registry and platform/vscode-adapter. diff --git a/lib/src/lib/terminal-lifecycle.ts b/lib/src/lib/terminal-lifecycle.ts index 909c8bb9..6db3c174 100644 --- a/lib/src/lib/terminal-lifecycle.ts +++ b/lib/src/lib/terminal-lifecycle.ts @@ -54,6 +54,7 @@ function createXtermHost(): { terminal: Terminal; fit: FitAddon; element: HTMLDi fontFamily: editorFontFamily, cursorBlink: true, theme, + vtExtensions: { kittyKeyboard: true }, }); const fit = new FitAddon(); diff --git a/lib/src/lib/terminal-state.test.ts b/lib/src/lib/terminal-state.test.ts index 6dd6027f..b2f773c0 100644 --- a/lib/src/lib/terminal-state.test.ts +++ b/lib/src/lib/terminal-state.test.ts @@ -142,13 +142,13 @@ describe('terminal command state reducer', () => { let state = createTerminalPaneState(); state = reduceTerminalState(state, { type: 'title', title: { title: 'zsh', source: 'osc0', updatedAt: 1 } }); state = reduceTerminalState(state, { type: 'title', title: { title: 'vim', source: 'osc2', updatedAt: 2 } }); - state = reduceTerminalState(state, { type: 'title', title: { title: 'mouseterm', source: 'osc0', updatedAt: 3 } }); + state = reduceTerminalState(state, { type: 'title', title: { title: 'dormouse', source: 'osc0', updatedAt: 3 } }); - expect(state.title).toEqual({ title: 'mouseterm', source: 'osc0', updatedAt: 3 }); - expect(state.titleCandidates.osc0).toEqual({ title: 'mouseterm', source: 'osc0', updatedAt: 3 }); + expect(state.title).toEqual({ title: 'dormouse', source: 'osc0', updatedAt: 3 }); + expect(state.titleCandidates.osc0).toEqual({ title: 'dormouse', source: 'osc0', updatedAt: 3 }); expect(state.titleCandidates.osc2).toEqual({ title: 'vim', source: 'osc2', updatedAt: 2 }); expect(titleCandidatesForDisplay(state).map((candidate) => [candidate.source, candidate.title])).toEqual([ - ['osc0', 'mouseterm'], + ['osc0', 'dormouse'], ['osc2', 'vim'], ]); }); @@ -256,11 +256,11 @@ describe('header and grouping derivation', () => { it('lets fresh app-sent terminal titles override running command labels', () => { const pane = reduceTerminalState( runningPane('/repo/app', 'lazygit'), - { type: 'title', title: { title: 'lazygit: mouseterm', source: 'osc0', updatedAt: 2 } }, + { type: 'title', title: { title: 'lazygit: dormouse', source: 'osc0', updatedAt: 2 } }, ); expect(deriveHeader(pane, [pane])).toEqual({ - primary: 'lazygit: mouseterm', + primary: 'lazygit: dormouse', }); }); @@ -358,23 +358,23 @@ describe('header and grouping derivation', () => { it('uses the in-run app-sent title as ` ${LAST_TITLE}`', () => { let pane = runningPane('/repo/app', 'lazygit'); - pane = reduceTerminalState(pane, { type: 'title', title: { title: 'lazygit: mouseterm', source: 'osc0', updatedAt: 2 } }); + pane = reduceTerminalState(pane, { type: 'title', title: { title: 'lazygit: dormouse', source: 'osc0', updatedAt: 2 } }); pane = reduceTerminalState(pane, { type: 'commandFinish', exitCode: 0 }, { now: () => 3 }); expect(deriveHeader(pane, [pane])).toEqual({ - primary: `${DEFAULT_IDLE_TITLE} lazygit: mouseterm`, + primary: `${DEFAULT_IDLE_TITLE} lazygit: dormouse`, }); }); it('ignores titles emitted after the last command finished when deriving LAST_TITLE', () => { let pane = runningPane('/repo/app', 'lazygit'); - pane = reduceTerminalState(pane, { type: 'title', title: { title: 'lazygit: mouseterm', source: 'osc0', updatedAt: 2 } }); + pane = reduceTerminalState(pane, { type: 'title', title: { title: 'lazygit: dormouse', source: 'osc0', updatedAt: 2 } }); pane = reduceTerminalState(pane, { type: 'commandFinish', exitCode: 0 }, { now: () => 3 }); // Shell sets the title back to its default after the command exits. pane = reduceTerminalState(pane, { type: 'title', title: { title: 'zsh', source: 'osc0', updatedAt: 4 } }); expect(deriveHeader(pane, [pane])).toEqual({ - primary: `${DEFAULT_IDLE_TITLE} lazygit: mouseterm`, + primary: `${DEFAULT_IDLE_TITLE} lazygit: dormouse`, }); }); diff --git a/lib/src/lib/terminal-theme.ts b/lib/src/lib/terminal-theme.ts index e5b704f7..8e1b2681 100644 --- a/lib/src/lib/terminal-theme.ts +++ b/lib/src/lib/terminal-theme.ts @@ -45,7 +45,7 @@ export function paintTerminalHost(element: HTMLDivElement, terminal: Terminal, b const hosts = element.querySelectorAll(XTERM_HOST_SELECTOR); if (hosts.length === 0 && xtermElement && !xtermSelectorWarned) { xtermSelectorWarned = true; - console.warn(`[mouseterm] paintTerminalHost: no elements matched ${XTERM_HOST_SELECTOR} - xterm DOM may have changed.`); + console.warn(`[dormouse] paintTerminalHost: no elements matched ${XTERM_HOST_SELECTOR} - xterm DOM may have changed.`); return; } hosts.forEach((el) => { diff --git a/lib/src/lib/themes/apply.ts b/lib/src/lib/themes/apply.ts index 62fa0a44..14867f12 100644 --- a/lib/src/lib/themes/apply.ts +++ b/lib/src/lib/themes/apply.ts @@ -1,4 +1,4 @@ -import type { MouseTermTheme } from './types'; +import type { DormouseTheme } from './types'; import { getAllThemes, getStoredActiveThemeId, setActiveThemeId } from './store'; import { completeThemeVars } from './vscode-color-resolver'; import { flattenSelectionAlpha } from './flatten-alpha'; @@ -6,7 +6,7 @@ import { flattenSelectionAlpha } from './flatten-alpha'; let appliedThemeSnapshot: AppliedThemeSnapshot | null = null; export interface AppliedThemeSnapshot { - theme: MouseTermTheme; + theme: DormouseTheme; providedVars: Record; resolvedVars: Record; } @@ -19,7 +19,7 @@ const HOST_TYPOGRAPHY_VARS: Record = { "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace", }; -export function applyTheme(theme: MouseTermTheme): void { +export function applyTheme(theme: DormouseTheme): void { if (typeof document === 'undefined') return; if (theme === appliedThemeSnapshot?.theme) return; @@ -34,7 +34,7 @@ export function applyTheme(theme: MouseTermTheme): void { const providedVars = { ...HOST_TYPOGRAPHY_VARS, ...theme.vars }; const vars = completeThemeVars(providedVars, theme.type); // Theme authors give list.*SelectionBackground alpha because VSCode renders - // it as an overlay on the sidebar. MouseTerm uses it as a solid AppBar / + // it as an overlay on the sidebar. Dormouse uses it as a solid AppBar / // tab fill, so flatten the alpha over sideBar.background here โ€” otherwise // whatever sits behind the surface bleeds through (Selenized Dark's bright // cyan AppBar, for instance). @@ -58,7 +58,7 @@ export function applyTheme(theme: MouseTermTheme): void { * first bundled theme. Idempotent and safe to call before render so the * first paint already has --vscode-* set on body. Returns the theme that was * applied, or null when no themes are available (e.g. SSR). */ -export function restoreActiveTheme(defaultThemeId?: string): MouseTermTheme | null { +export function restoreActiveTheme(defaultThemeId?: string): DormouseTheme | null { const all = getAllThemes(); const find = (id: string | null | undefined) => (id ? all.find((t) => t.id === id) : undefined); const theme = find(getStoredActiveThemeId()) ?? find(defaultThemeId) ?? all[0]; diff --git a/lib/src/lib/themes/convert.ts b/lib/src/lib/themes/convert.ts index 0d45d71e..44545ed8 100644 --- a/lib/src/lib/themes/convert.ts +++ b/lib/src/lib/themes/convert.ts @@ -6,7 +6,7 @@ * 2. getTerminalTheme() in terminal-theme.ts โ€” ANSI, cursor, selection */ -/** VSCode theme color keys consumed by MouseTerm. Derived from theme.css, +/** VSCode theme color keys consumed by Dormouse. Derived from theme.css, * useDynamicPalette, ThemePicker inline styles, SelectionOverlay, * terminal-theme, and the VSCode fallback resolver. */ export const CONSUMED_VSCODE_KEYS: readonly string[] = [ diff --git a/lib/src/lib/themes/diagnostics.ts b/lib/src/lib/themes/diagnostics.ts index 281b3715..582fab18 100644 --- a/lib/src/lib/themes/diagnostics.ts +++ b/lib/src/lib/themes/diagnostics.ts @@ -11,7 +11,7 @@ import { type VscodeThemeVarTrace, } from './vscode-color-resolver'; -export type VisibleVarOrigin = 'host-provided' | 'mouseterm-materialized' | 'missing'; +export type VisibleVarOrigin = 'host-provided' | 'dormouse-materialized' | 'missing'; export interface ThemeMetadataSnapshot { id: string; @@ -152,9 +152,9 @@ function captureVisibleVars( let origin: VisibleVarOrigin = value ? 'host-provided' : 'missing'; if (value && materialized.get(name) === value) { - origin = 'mouseterm-materialized'; + origin = 'dormouse-materialized'; } else if (value && applied && !applied.theme.vars[name] && applied.resolvedVars[name] === value) { - origin = 'mouseterm-materialized'; + origin = 'dormouse-materialized'; } return { @@ -247,7 +247,7 @@ function formatTrace(trace: VscodeThemeVarTrace): string { function buildReport(snapshot: Omit): string { const lines: string[] = []; - lines.push('MouseTerm theme diagnostic'); + lines.push('Dormouse theme diagnostic'); lines.push(`capturedAt: ${snapshot.capturedAt}`); lines.push(`themeKind: ${snapshot.themeKind}`); lines.push(`activeTheme: ${snapshot.activeTheme ? `${snapshot.activeTheme.label} (${snapshot.activeTheme.origin})` : 'VSCode host theme'}`); diff --git a/lib/src/lib/themes/flatten-alpha.ts b/lib/src/lib/themes/flatten-alpha.ts index c1a5cfd6..1d9d4779 100644 --- a/lib/src/lib/themes/flatten-alpha.ts +++ b/lib/src/lib/themes/flatten-alpha.ts @@ -1,6 +1,6 @@ /* Composite a translucent CSS color over an opaque base, returning an opaque * hex result. Used to flatten VSCode tokens that carry alpha (e.g. Selenized - * Dark's list.activeSelectionBackground = #0096f588) when MouseTerm applies + * Dark's list.activeSelectionBackground = #0096f588) when Dormouse applies * them as solid surface fills โ€” the AppBar, dockview tabs, etc. all expect a * fully opaque color, but VSCode authors selection tints with alpha because * VSCode itself renders them as overlays on the sidebar background. */ @@ -59,7 +59,7 @@ export function flattenAlpha(value: string, base: string): string { }); } -/* VSCode tokens that MouseTerm uses as solid surface fills but whose theme +/* VSCode tokens that Dormouse uses as solid surface fills but whose theme * authors commonly carry alpha. Flattened against sideBar.background โ€” the * surface VSCode itself composites the file-tree selection over. */ const FLATTEN_OVER_SIDEBAR: readonly string[] = [ diff --git a/lib/src/lib/themes/index.ts b/lib/src/lib/themes/index.ts index 59ee1543..5be40e8a 100644 --- a/lib/src/lib/themes/index.ts +++ b/lib/src/lib/themes/index.ts @@ -1,4 +1,4 @@ -export type { MouseTermTheme, BundledOrigin, InstalledOrigin } from './types'; +export type { DormouseTheme, BundledOrigin, InstalledOrigin } from './types'; export { CONSUMED_VSCODE_KEYS, convertVscodeThemeColors, uiThemeToType } from './convert'; export { applyTheme, getAppliedThemeSnapshot, restoreActiveTheme } from './apply'; export type { AppliedThemeSnapshot } from './apply'; diff --git a/lib/src/lib/themes/openvsx.ts b/lib/src/lib/themes/openvsx.ts index 10fffb3d..43a091b1 100644 --- a/lib/src/lib/themes/openvsx.ts +++ b/lib/src/lib/themes/openvsx.ts @@ -2,12 +2,12 @@ * Runtime OpenVSX theme installer. * * Searches for theme extensions, downloads VSIX files, extracts theme - * JSONs in the browser, and converts them to MouseTermTheme objects. + * JSONs in the browser, and converts them to DormouseTheme objects. * * fflate is dynamically imported so it doesn't affect initial bundle size. */ -import type { MouseTermTheme } from './types'; +import type { DormouseTheme } from './types'; import { convertVscodeThemeColors, uiThemeToType } from './convert'; const OPENVSX_API = 'https://open-vsx.org/api'; @@ -56,12 +56,12 @@ function slugify(label: string): string { /** * Download a theme extension from OpenVSX and return all theme variants - * as MouseTermTheme objects ready for installation. + * as DormouseTheme objects ready for installation. */ export async function fetchExtensionThemes( namespace: string, name: string, -): Promise { +): Promise { // 1. Get latest version metadata const metaRes = await fetch(`${OPENVSX_API}/${namespace}/${name}/latest`); if (!metaRes.ok) throw new Error(`OpenVSX metadata failed: ${metaRes.status}`); @@ -108,7 +108,7 @@ export async function fetchExtensionThemes( const { parse: parseJsonc } = await import('jsonc-parser'); // 6. Convert each theme variant - const themes: MouseTermTheme[] = []; + const themes: DormouseTheme[] = []; for (const contrib of themeContribs) { const themePath = `extension/${contrib.path.replace(/^\.\//, '')}`; const themeData = entries[themePath]; diff --git a/lib/src/lib/themes/store.ts b/lib/src/lib/themes/store.ts index a45732d7..b627d958 100644 --- a/lib/src/lib/themes/store.ts +++ b/lib/src/lib/themes/store.ts @@ -1,10 +1,10 @@ -import type { MouseTermTheme } from './types'; +import type { DormouseTheme } from './types'; // JSON import types are inferred too narrowly โ€” cast at the boundary. import _bundledThemes from './bundled.json'; -const bundledThemes = _bundledThemes as unknown as MouseTermTheme[]; +const bundledThemes = _bundledThemes as unknown as DormouseTheme[]; -const INSTALLED_KEY = 'mouseterm:installed-themes'; -const ACTIVE_KEY = 'mouseterm:active-theme'; +const INSTALLED_KEY = 'dormouse:installed-themes'; +const ACTIVE_KEY = 'dormouse:active-theme'; function getStorage(): Storage | null { const storage = globalThis.localStorage; @@ -18,30 +18,30 @@ function getStorage(): Storage | null { return storage; } -export function getBundledThemes(): MouseTermTheme[] { +export function getBundledThemes(): DormouseTheme[] { return bundledThemes; } -export function getInstalledThemes(): MouseTermTheme[] { +export function getInstalledThemes(): DormouseTheme[] { const storage = getStorage(); if (!storage) return []; try { const raw = storage.getItem(INSTALLED_KEY); - return raw ? (JSON.parse(raw) as MouseTermTheme[]) : []; + return raw ? (JSON.parse(raw) as DormouseTheme[]) : []; } catch { return []; } } -export function getAllThemes(): MouseTermTheme[] { +export function getAllThemes(): DormouseTheme[] { return [...getBundledThemes(), ...getInstalledThemes()]; } -export function getTheme(id: string): MouseTermTheme | undefined { +export function getTheme(id: string): DormouseTheme | undefined { return getAllThemes().find((t) => t.id === id); } -export function addInstalledTheme(theme: MouseTermTheme): void { +export function addInstalledTheme(theme: DormouseTheme): void { const storage = getStorage(); if (!storage) return; const installed = getInstalledThemes().filter((t) => t.id !== theme.id); diff --git a/lib/src/lib/themes/types.ts b/lib/src/lib/themes/types.ts index 7b4511ee..60a819b1 100644 --- a/lib/src/lib/themes/types.ts +++ b/lib/src/lib/themes/types.ts @@ -1,4 +1,4 @@ -export interface MouseTermTheme { +export interface DormouseTheme { /** Stable unique ID, e.g. "GitHub.github-vscode-theme.github-dark-default" */ id: string; /** Human-readable label from the VSCode theme */ diff --git a/lib/src/stories/KillModal.stories.tsx b/lib/src/stories/KillModal.stories.tsx index 2d53763d..40d7881f 100644 --- a/lib/src/stories/KillModal.stories.tsx +++ b/lib/src/stories/KillModal.stories.tsx @@ -6,7 +6,7 @@ function KillModal({ char = 'G', onCancel, exit }: { char?: string; onCancel?: (
{/* Simulated terminal content behind the overlay */}
-
user@mouseterm:~$ npm run build
+
user@dormouse:~$ npm run build
Building project...
{/* Kill confirmation overlay โ€” positioned over the pane */} diff --git a/lib/src/stories/MobileTerminalUi.stories.tsx b/lib/src/stories/MobileTerminalUi.stories.tsx index 63a75c98..6466fe7a 100644 --- a/lib/src/stories/MobileTerminalUi.stories.tsx +++ b/lib/src/stories/MobileTerminalUi.stories.tsx @@ -44,11 +44,11 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const TETHER_WALL_PANE = 'storybook-tether-wall'; -const TETHER_WALL_SESSIONS: MobileWallSession[] = [{ id: TETHER_WALL_PANE, title: 'ascii-splash' }]; +const POCKET_WALL_PANE = 'storybook-pocket-wall'; +const POCKET_WALL_SESSIONS: MobileWallSession[] = [{ id: POCKET_WALL_PANE, title: 'ascii-splash' }]; -const TETHER_WALL_SCENARIO: FakeScenario = { - name: 'tether-wall-ascii-splash', +const POCKET_WALL_SCENARIO: FakeScenario = { + name: 'pocket-wall-ascii-splash', chunks: [{ delay: 0, data: [ @@ -137,14 +137,14 @@ function StoryFrame(args: MobileTerminalUiProps) { ); } -function TetherWallFrame(args: MobileTerminalUiProps) { +function PocketWallFrame(args: MobileTerminalUiProps) { const adapterRef = useRef(null); if (!adapterRef.current) adapterRef.current = initPlatform('fake'); - const [activePaneId, setActivePaneId] = useState(TETHER_WALL_PANE); + const [activePaneId, setActivePaneId] = useState(POCKET_WALL_PANE); const [keyboardMode, setKeyboardMode] = useState( args.activeKeyboardMode ?? args.activeSection ?? args.defaultKeyboardMode ?? args.defaultSection ?? 'type', ); - const sessionItems = useMobileWallSessionItems(TETHER_WALL_SESSIONS, activePaneId); + const sessionItems = useMobileWallSessionItems(POCKET_WALL_SESSIONS, activePaneId); return (
@@ -153,7 +153,7 @@ function TetherWallFrame(args: MobileTerminalUiProps) { fillViewport terminal={( setKeyboardMode('sessions')} @@ -315,15 +315,15 @@ export const CursorTouchAvailable: Story = { render: (args) => , }; -export const TetherWall: Story = { +export const PocketWall: Story = { args: { defaultSection: 'type', }, parameters: { layout: 'fullscreen', - fakePty: { scenario: flattenScenario(TETHER_WALL_SCENARIO) }, + fakePty: { scenario: flattenScenario(POCKET_WALL_SCENARIO) }, }, - render: (args) => , + render: (args) => , }; export const GestureMenuOpened: Story = { diff --git a/lib/src/stories/ShellCwd.stories.tsx b/lib/src/stories/ShellCwd.stories.tsx index 1201fde5..8a017aef 100644 --- a/lib/src/stories/ShellCwd.stories.tsx +++ b/lib/src/stories/ShellCwd.stories.tsx @@ -74,7 +74,7 @@ export const CwdSourcesAndPathKinds: Story = storyFor([ caseState('cwd-osc99-drive', 'OSC 9;9 drive', idle({ cwd: osc99('C:\\Users\\me\\repo') }), 'Windows drive-letter path'), caseState('cwd-osc99-unc', 'OSC 9;9 UNC', idle({ cwd: osc99('\\\\server\\share\\repo') }), 'Windows UNC path'), caseState('cwd-osc99-wsl', 'OSC 9;9 WSL-like', idle({ cwd: osc99('/mnt/c/Users/me/repo') }), 'Unknown path kind by design'), - caseState('cwd-osc633', 'OSC 633 Cwd', idle({ cwd: osc633('/workspaces/mouseterm') }), 'VS Code shell integration CWD'), + caseState('cwd-osc633', 'OSC 633 Cwd', idle({ cwd: osc633('/workspaces/dormouse') }), 'VS Code shell integration CWD'), caseState('cwd-osc1337', 'OSC 1337 CurrentDir', idle({ cwd: osc1337('/Users/me/iterm-app') }), 'iTerm2 CurrentDir compatibility'), ]); @@ -369,7 +369,7 @@ function titleCandidateState(): TerminalPaneState { const pane = running('/repo/app', 'npm run dev'); const candidates = { user: terminalTitleAt('Pinned production API', 'user', BASE_TIME + 6_000), - osc0: terminalTitleAt('mouseterm', 'osc0', BASE_TIME + 1_000), + osc0: terminalTitleAt('dormouse', 'osc0', BASE_TIME + 1_000), osc2: terminalTitleAt('zsh', 'osc2', BASE_TIME + 2_000), osc9: terminalTitleAt('Build finished', 'osc9', BASE_TIME + 5_000), osc99: terminalTitleAt('Codex waiting', 'osc99', BASE_TIME + 4_000), diff --git a/lib/src/stories/Smoke.stories.tsx b/lib/src/stories/Smoke.stories.tsx index e5fbf27a..f8493b0e 100644 --- a/lib/src/stories/Smoke.stories.tsx +++ b/lib/src/stories/Smoke.stories.tsx @@ -130,7 +130,7 @@ function ThemeCheck() {

Storybook Theme Smoke Test

- Verifies the resolved VSCode host variables, MouseTerm semantic tokens, and dynamic + Verifies the resolved VSCode host variables, Dormouse semantic tokens, and dynamic palette picks that Storybook injects for isolated stories.

@@ -180,7 +180,7 @@ function ThemeCheck() {
-

MouseTerm Tokens

+

Dormouse Tokens

diff --git a/lib/src/stories/UpdateDebugDialog.stories.tsx b/lib/src/stories/UpdateDebugDialog.stories.tsx index 27039e02..77da4cc3 100644 --- a/lib/src/stories/UpdateDebugDialog.stories.tsx +++ b/lib/src/stories/UpdateDebugDialog.stories.tsx @@ -24,7 +24,7 @@ function UpdateDebugDialogStory({ failure, body }: StoryArgs) { ); } -const ERROR = 'EACCES: permission denied at /Applications/MouseTerm.app'; +const ERROR = 'EACCES: permission denied at /Applications/Dormouse.app'; const BODY = [ '**App version**: 0.7.0 โ†’ 0.8.0', diff --git a/lib/src/theme.css b/lib/src/theme.css index 5b2160c4..d7c3b937 100644 --- a/lib/src/theme.css +++ b/lib/src/theme.css @@ -1,4 +1,4 @@ -/* MouseTerm Theme System +/* Dormouse Theme System * * Two-layer CSS variable strategy: * @theme --color-* tokens -> var(--vscode-*) @@ -8,7 +8,7 @@ * * This file intentionally does not provide hardcoded color defaults. Runtime * hosts must provide real VSCode variables, either from VSCode itself or from - * a bundled/installed MouseTerm theme. + * a bundled/installed Dormouse theme. * * Two consumers read --vscode-* variables: * 1. @theme/body bindings below โ€” for UI colors (surfaces, tabs, etc.) diff --git a/lib/tsconfig.app.json b/lib/tsconfig.app.json index 0ff1f497..fcf334ed 100644 --- a/lib/tsconfig.app.json +++ b/lib/tsconfig.app.json @@ -16,7 +16,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "paths": { - "mouseterm-lib/*": ["./src/*"] + "dormouse-lib/*": ["./src/*"] } }, "include": ["src"], diff --git a/package.json b/package.json index d7d2e01a..3d9f193e 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "mouseterm-root", + "name": "dormouse-root", "private": true, "license": "FSL-1.1-MIT", "scripts": { - "build": "pnpm run build:vscode && pnpm --filter mouseterm-website build", + "build": "pnpm run build:vscode && pnpm --filter dormouse-website build", "test": "pnpm -r run test", - "dev:lib": "pnpm --filter mouseterm-lib dev", - "dev:standalone": "pnpm --filter mouseterm-standalone tauri dev", - "dev:website": "pnpm --filter mouseterm-website dev", - "build:vscode": "pnpm --filter mouseterm-lib build && pnpm --filter mouseterm build:frontend && pnpm --filter mouseterm build", - "build:standalone": "pnpm --filter mouseterm-standalone tauri build", - "build:website": "pnpm --filter mouseterm-website build", - "dogfood:vscode": "pnpm run build:vscode && pnpm --filter mouseterm dogfood", + "dev:lib": "pnpm --filter dormouse-lib dev", + "dev:standalone": "pnpm --filter dormouse-standalone tauri dev", + "dev:website": "pnpm --filter dormouse-website dev", + "build:vscode": "pnpm --filter dormouse-lib build && pnpm --filter dormouse build:frontend && pnpm --filter dormouse build", + "build:standalone": "pnpm --filter dormouse-standalone tauri build", + "build:website": "pnpm --filter dormouse-website build", + "dogfood:vscode": "pnpm run build:vscode && pnpm --filter dormouse dogfood", "dogfood:standalone": "bash standalone/scripts/dogfood.sh", - "storybook": "pnpm --filter mouseterm-lib storybook", + "storybook": "pnpm --filter dormouse-lib storybook", "bundle-themes": "node lib/scripts/bundle-themes.mjs" }, "pnpm": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3ede9f5..f46f02fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,11 +14,11 @@ importers: specifier: ^2.1.10 version: 2.1.10(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@xterm/addon-fit': - specifier: ^0.11.0 - version: 0.11.0 + specifier: 0.12.0-beta.216 + version: 0.12.0-beta.216(@xterm/xterm@6.1.0-beta.216) '@xterm/xterm': - specifier: ^6.0.0 - version: 6.0.0 + specifier: 6.1.0-beta.216 + version: 6.1.0-beta.216 clsx: specifier: ^2.1.1 version: 2.1.1 @@ -96,15 +96,15 @@ importers: specifier: ^2.10.1 version: 2.10.1 '@xterm/addon-fit': - specifier: ^0.11.0 - version: 0.11.0 + specifier: 0.12.0-beta.216 + version: 0.12.0-beta.216(@xterm/xterm@6.1.0-beta.216) '@xterm/xterm': - specifier: ^6.0.0 - version: 6.0.0 + specifier: 6.1.0-beta.216 + version: 6.1.0-beta.216 dockview-react: specifier: ^5.1.0 version: 5.1.0(react@19.2.4) - mouseterm-lib: + dormouse-lib: specifier: workspace:* version: link:../lib react: @@ -196,7 +196,7 @@ importers: ascii-splash: specifier: 0.3.0 version: 0.3.0 - mouseterm-lib: + dormouse-lib: specifier: workspace:* version: link:../lib react: @@ -1640,11 +1640,13 @@ packages: engines: {node: '>= 20'} hasBin: true - '@xterm/addon-fit@0.11.0': - resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + '@xterm/addon-fit@0.12.0-beta.216': + resolution: {integrity: sha512-IgKE3ngNodSnmj1O+EEYpKQZkSbAUbghPlCWd8G32RL0piIMqb3FX3BuYLnWZeLNoD9iMtublLMG1T9XjGeVvA==} + peerDependencies: + '@xterm/xterm': ^6.1.0-beta.216 - '@xterm/xterm@6.0.0': - resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + '@xterm/xterm@6.1.0-beta.216': + resolution: {integrity: sha512-87rfymzVje5eYUlGG94hz1WkOYvFRcFDGdiOAbg4d8xt8OGSGR2nMNU4I1n5MDE1RBPBqRd+WVJ5w7q3pwMoZA==} acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} @@ -4845,9 +4847,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@xterm/addon-fit@0.11.0': {} + '@xterm/addon-fit@0.12.0-beta.216(@xterm/xterm@6.1.0-beta.216)': + dependencies: + '@xterm/xterm': 6.1.0-beta.216 - '@xterm/xterm@6.0.0': {} + '@xterm/xterm@6.1.0-beta.216': {} acorn@8.16.0: {} diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index 572400ba..92715e1a 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -5,7 +5,7 @@ set -euo pipefail # Bump version across all release artifacts and sync Cargo.lock. # ============================================================================= # Edits the four version files in lockstep, then runs cargo so Cargo.lock's -# `mouseterm` entry follows along. Print a diff stat for review. +# `dormouse` entry follows along. Print a diff stat for review. # # Usage: ./scripts/bump-version.sh # Example: ./scripts/bump-version.sh 0.9.0 @@ -53,7 +53,7 @@ for f in "$TAURI_CONF" "$VSCODE_PKG" "$LIB_PKG"; do done # Sync Cargo.lock by running cargo. `cargo check` is idempotent and updates -# the lockfile's mouseterm entry to match the bumped Cargo.toml. Without this, +# the lockfile's dormouse entry to match the bumped Cargo.toml. Without this, # Cargo.lock keeps the old version and ships out of sync with the binary. echo "Syncing Cargo.lock (cargo check)โ€ฆ" ( cd standalone/src-tauri && cargo check --offline >/dev/null ) diff --git a/scripts/patch-nsis-paths.pl b/scripts/patch-nsis-paths.pl index 6aecb7e8..e5a7a84a 100644 --- a/scripts/patch-nsis-paths.pl +++ b/scripts/patch-nsis-paths.pl @@ -19,7 +19,7 @@ close $fh; # Extract CI checkout root from MAINBINARYSRCPATH. -# The path looks like: "D:\a\mouseterm\mouseterm\standalone\src-tauri\target\...\mouseterm.exe" +# The path looks like: "D:\a\dormouse\dormouse\standalone\src-tauri\target\...\dormouse.exe" # We want everything before \src-tauri\ (including \standalone) because # actions/upload-artifact strips the common ancestor (standalone/) from paths. my $ci_root; diff --git a/scripts/sign-and-deploy.sh b/scripts/sign-and-deploy.sh index df5daa61..f85a9674 100755 --- a/scripts/sign-and-deploy.sh +++ b/scripts/sign-and-deploy.sh @@ -44,12 +44,12 @@ JSIGN_ALIAS="AUTHENTICATION" TSA_URL="http://ts.ssl.com" # GitHub repo -GITHUB_REPO="diffplug/mouseterm" +GITHUB_REPO="diffplug/dormouse" # Stable filenames for release assets (update bundles only) -FNAME_WIN="MouseTerm-windows-x64-setup.exe" -FNAME_MAC="MouseTerm-macos-aarch64.tar.gz" -FNAME_LINUX="MouseTerm-linux-x86_64.AppImage" +FNAME_WIN="Dormouse-windows-x64-setup.exe" +FNAME_MAC="Dormouse-macos-aarch64.tar.gz" +FNAME_LINUX="Dormouse-linux-x86_64.AppImage" # ============================================================================= # Helper Functions @@ -496,7 +496,7 @@ notarize_macos() { # COPYFILE_DISABLE=1 stops macOS's tar from writing ._* AppleDouble # sidecar files (resource-fork metadata) into the archive. Without # this the Tauri updater's extraction fails with - # `failed to unpack ._MouseTerm.app`. + # `failed to unpack ._Dormouse.app`. COPYFILE_DISABLE=1 tar -czf "$SIGN_DIR/$FNAME_MAC" -C "$(dirname "$app")" "$app_name" # Defense in depth: if any ._* slipped in anyway, fail loudly here @@ -521,7 +521,7 @@ sign_windows() { # Find the inner exe local exe_path - exe_path=$(find "$SIGN_DIR/standalone-win-x64" \( -name "MouseTerm.exe" -o -name "mouseterm.exe" \) -not -name "*setup*" -not -name "*install*" | head -1) + exe_path=$(find "$SIGN_DIR/standalone-win-x64" \( -name "Dormouse.exe" -o -name "dormouse.exe" \) -not -name "*setup*" -not -name "*install*" | head -1) [[ -n "$exe_path" ]] || error "Windows executable not found" log "Signing inner executable: $exe_path" diff --git a/standalone/index.html b/standalone/index.html index 42d08170..bb127d78 100644 --- a/standalone/index.html +++ b/standalone/index.html @@ -3,7 +3,7 @@ - MouseTerm + Dormouse
diff --git a/standalone/package.json b/standalone/package.json index 3c0b8a56..13875d95 100644 --- a/standalone/package.json +++ b/standalone/package.json @@ -1,5 +1,5 @@ { - "name": "mouseterm-standalone", + "name": "dormouse-standalone", "private": true, "version": "0.1.0", "license": "FSL-1.1-MIT", @@ -15,10 +15,10 @@ "@tauri-apps/api": "^2.0.0", "@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-updater": "^2.10.1", - "@xterm/addon-fit": "^0.11.0", - "@xterm/xterm": "^6.0.0", + "@xterm/addon-fit": "0.12.0-beta.216", + "@xterm/xterm": "6.1.0-beta.216", "dockview-react": "^5.1.0", - "mouseterm-lib": "workspace:*", + "dormouse-lib": "workspace:*", "react": "^19.0.0", "react-dom": "^19.0.0", "tailwind-variants": "^3.2.2" diff --git a/standalone/scripts/dogfood.sh b/standalone/scripts/dogfood.sh index 49361a97..edf4a324 100755 --- a/standalone/scripts/dogfood.sh +++ b/standalone/scripts/dogfood.sh @@ -29,11 +29,11 @@ if [[ "${1:-}" == "--install" ]]; then case "$(uname -s)" in Darwin) BUNDLE_ARGS=(--bundles app) ;; esac - pnpm --filter mouseterm-standalone tauri build \ + pnpm --filter dormouse-standalone tauri build \ -c '{"bundle":{"createUpdaterArtifacts":false}}' "${BUNDLE_ARGS[@]}" else # Fast build: skip bundling entirely since we just need the exe - pnpm --filter mouseterm-standalone tauri build --no-bundle + pnpm --filter dormouse-standalone tauri build --no-bundle fi if [[ "${1:-}" == "--install" ]]; then @@ -41,23 +41,23 @@ if [[ "${1:-}" == "--install" ]]; then # Platform-specific: copy built files to system install location case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*|Windows_NT) - INSTALL_DIR="$LOCALAPPDATA/MouseTerm" + INSTALL_DIR="$LOCALAPPDATA/Dormouse" if [[ ! -f "$INSTALL_DIR/uninstall.exe" ]]; then - echo "MouseTerm is not installed yet." + echo "Dormouse is not installed yet." echo "Run the installer once first:" - echo " $RELEASE_DIR/bundle/nsis/MouseTerm_*-setup.exe" + echo " $RELEASE_DIR/bundle/nsis/Dormouse_*-setup.exe" echo "" echo "After that, 'dogfood:standalone --install' will work from then on." exit 1 fi - # Kill any running MouseTerm processes (the app + its sidecar node.exe, + # Kill any running Dormouse processes (the app + its sidecar node.exe, # plus orphan sidecars from a prior run) before we overwrite their files. # We can't use `taskkill //IM node.exe` here: that matches every node.exe # on the system, including the pnpm process that invoked this script, # and `//T` would then cascade and kill us. Filter by image path so we # only target processes loaded from the install dir. powershell.exe -NoProfile -Command \ - "Get-Process -Name mouseterm,node -EA SilentlyContinue | Where-Object Path -Like '$LOCALAPPDATA\\MouseTerm\\*' | Stop-Process -Force -EA SilentlyContinue" \ + "Get-Process -Name dormouse,node -EA SilentlyContinue | Where-Object Path -Like '$LOCALAPPDATA\\Dormouse\\*' | Stop-Process -Force -EA SilentlyContinue" \ >/dev/null 2>&1 || true # Wipe install-dir contents except uninstall.exe (managed by NSIS). # We delete *contents* rather than the directory itself so we don't trip @@ -65,23 +65,23 @@ if [[ "${1:-}" == "--install" ]]; then # an exe image from it. find "$INSTALL_DIR" -mindepth 1 -maxdepth 1 -not -name 'uninstall.exe' \ -exec rm -rf {} + - cp "$RELEASE_DIR/mouseterm.exe" "$INSTALL_DIR/" + cp "$RELEASE_DIR/dormouse.exe" "$INSTALL_DIR/" cp "$RELEASE_DIR/node.exe" "$INSTALL_DIR/" cp -r "$RELEASE_DIR/_up_/" "$INSTALL_DIR/_up_/" echo "โœฆ Installed to $INSTALL_DIR" ;; Darwin) - INSTALL_DIR="/Applications/MouseTerm.app" + INSTALL_DIR="/Applications/Dormouse.app" if [[ ! -d "$INSTALL_DIR" ]]; then - echo "MouseTerm is not installed yet." + echo "Dormouse is not installed yet." echo "Install via the DMG first:" - echo " open $RELEASE_DIR/bundle/dmg/MouseTerm_*.dmg" + echo " open $RELEASE_DIR/bundle/dmg/Dormouse_*.dmg" echo "" echo "After that, 'dogfood:standalone --install' will work from then on." exit 1 fi rm -rf "$INSTALL_DIR" - cp -r "$RELEASE_DIR/bundle/macos/MouseTerm.app" "$INSTALL_DIR" + cp -r "$RELEASE_DIR/bundle/macos/Dormouse.app" "$INSTALL_DIR" echo "โœฆ Installed to $INSTALL_DIR" ;; *) @@ -93,11 +93,11 @@ else # --- Launch mode (default) --- case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*|Windows_NT) - "$RELEASE_DIR/mouseterm.exe" ;; + "$RELEASE_DIR/dormouse.exe" ;; Darwin) - "$RELEASE_DIR/mouseterm" ;; + "$RELEASE_DIR/dormouse" ;; Linux) - "$RELEASE_DIR/mouseterm" ;; + "$RELEASE_DIR/dormouse" ;; *) echo "Unsupported platform: $(uname -s)" exit 1 ;; diff --git a/standalone/sidecar/clipboard-ops.js b/standalone/sidecar/clipboard-ops.js index 5a0b8676..c534af2d 100644 --- a/standalone/sidecar/clipboard-ops.js +++ b/standalone/sidecar/clipboard-ops.js @@ -239,7 +239,7 @@ async function readClipboardImageAsFilePath(runtime = {}) { let dir = null; let out = null; try { - dir = await fsp.mkdtemp(path.join(osModule.tmpdir(), 'mouseterm-drops-')); + dir = await fsp.mkdtemp(path.join(osModule.tmpdir(), 'dormouse-drops-')); await fsp.chmod?.(dir, 0o700); out = path.join(dir, `${cryptoModule.randomUUID()}-clipboard.png`); const ok = platform === 'darwin' ? await readImageMac(out, runtime) diff --git a/standalone/sidecar/clipboard-ops.test.js b/standalone/sidecar/clipboard-ops.test.js index f35d44d5..4402a8cc 100644 --- a/standalone/sidecar/clipboard-ops.test.js +++ b/standalone/sidecar/clipboard-ops.test.js @@ -159,10 +159,10 @@ test('readClipboardImageAsFilePath on mac returns temp path on success', async ( return { stdout: 'ok\n' }; }, }); - const expected = path.join('/t', 'mouseterm-drops-dir-0', 'uuid-I-clipboard.png'); + const expected = path.join('/t', 'dormouse-drops-dir-0', 'uuid-I-clipboard.png'); assert.equal(result, expected); assert.deepEqual(fs.chmods, [ - [path.join('/t', 'mouseterm-drops-dir-0'), 0o700], + [path.join('/t', 'dormouse-drops-dir-0'), 0o700], [expected, 0o600], ]); // Cleanup was scheduled, but not yet run: the temp file still exists. @@ -175,7 +175,7 @@ test('readClipboardImageAsFilePath on mac returns temp path on success', async ( await new Promise((r) => setImmediate(r)); await new Promise((r) => setImmediate(r)); assert.deepEqual(fs.unlinks, [expected]); - assert.deepEqual(fs.rmdirs, [path.join('/t', 'mouseterm-drops-dir-0')]); + assert.deepEqual(fs.rmdirs, [path.join('/t', 'dormouse-drops-dir-0')]); }); test('readClipboardImageAsFilePath returns null when osascript returns empty', async () => { @@ -188,7 +188,7 @@ test('readClipboardImageAsFilePath returns null when osascript returns empty', a exec: async () => ({ stdout: '' }), }); assert.equal(result, null); - assert.deepEqual(fs.rmdirs, [path.join('/t', 'mouseterm-drops-dir-0')]); + assert.deepEqual(fs.rmdirs, [path.join('/t', 'dormouse-drops-dir-0')]); }); test('readClipboardText on mac shells out to pbpaste', async () => { @@ -279,7 +279,7 @@ test('readClipboardImageAsFilePath on linux writes buffer from exec stdout', asy throw new Error('no tool'); }, }); - assert.equal(result, path.join('/t', 'mouseterm-drops-dir-0', 'uuid-L-clipboard.png')); + assert.equal(result, path.join('/t', 'dormouse-drops-dir-0', 'uuid-L-clipboard.png')); assert.equal(fs.writes.length, 1); assert.deepEqual(fs.writes[0][2], { mode: 0o600 }); }); diff --git a/standalone/sidecar/package.json b/standalone/sidecar/package.json index 90e9b1c3..08c81826 100644 --- a/standalone/sidecar/package.json +++ b/standalone/sidecar/package.json @@ -1,5 +1,5 @@ { - "name": "mouseterm-sidecar", + "name": "dormouse-sidecar", "private": true, "version": "0.1.0", "main": "main.js", diff --git a/standalone/src-tauri/Cargo.lock b/standalone/src-tauri/Cargo.lock index 4ce2a9ed..c7247f68 100644 --- a/standalone/src-tauri/Cargo.lock +++ b/standalone/src-tauri/Cargo.lock @@ -667,6 +667,20 @@ dependencies = [ "tendril 0.5.0", ] +[[package]] +name = "dormouse" +version = "0.9.1" +dependencies = [ + "process-wrap", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-shell", + "tauri-plugin-updater", + "windows 0.62.2", +] + [[package]] name = "dpi" version = "0.1.2" @@ -1942,20 +1956,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "mouseterm" -version = "0.9.1" -dependencies = [ - "process-wrap", - "serde", - "serde_json", - "tauri", - "tauri-build", - "tauri-plugin-shell", - "tauri-plugin-updater", - "windows 0.62.2", -] - [[package]] name = "muda" version = "0.17.1" diff --git a/standalone/src-tauri/Cargo.toml b/standalone/src-tauri/Cargo.toml index 50de2082..48769ed1 100644 --- a/standalone/src-tauri/Cargo.toml +++ b/standalone/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] -name = "mouseterm" +name = "dormouse" version = "0.9.1" -description = "Mouse-friendly multitasking terminal" +description = "Persistent multitasking terminal for mice (and hotkey wizards too)" authors = ["DiffPlug"] license = "FSL-1.1-MIT" edition = "2021" @@ -10,7 +10,7 @@ edition = "2021" global-min-publish-age = "14 days" [lib] -name = "mouseterm_lib" +name = "dormouse_lib" crate-type = ["lib", "cdylib", "staticlib"] [build-dependencies] diff --git a/standalone/src-tauri/capabilities/default.json b/standalone/src-tauri/capabilities/default.json index 4d2ae78d..92d765a4 100644 --- a/standalone/src-tauri/capabilities/default.json +++ b/standalone/src-tauri/capabilities/default.json @@ -1,6 +1,6 @@ { "identifier": "default", - "description": "Default capability set for MouseTerm", + "description": "Default capability set for Dormouse", "windows": ["main"], "permissions": [ "core:app:allow-version", diff --git a/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs index b3a2384c..582fc5b1 100644 --- a/standalone/src-tauri/src/lib.rs +++ b/standalone/src-tauri/src/lib.rs @@ -37,7 +37,7 @@ struct SidecarState { child: SharedChild, } -const LOG_FILE_ENV: &str = "MOUSETERM_LOG_FILE"; +const LOG_FILE_ENV: &str = "DORMOUSE_LOG_FILE"; fn log_timestamp() -> u64 { SystemTime::now() @@ -54,11 +54,11 @@ fn default_log_path() -> PathBuf { #[cfg(target_os = "windows")] if let Some(local_app_data) = env::var_os("LOCALAPPDATA") { return PathBuf::from(local_app_data) - .join("MouseTerm") - .join("mouseterm.log"); + .join("Dormouse") + .join("dormouse.log"); } - env::temp_dir().join("mouseterm.log") + env::temp_dir().join("dormouse.log") } fn log_path() -> &'static Path { @@ -100,7 +100,7 @@ fn init_log() { { let _ = writeln!( file, - "[{}] MouseTerm log started at {}", + "[{}] Dormouse log started at {}", log_timestamp(), path.display() ); @@ -599,14 +599,14 @@ pub fn run() { let refs: Vec<&dyn tauri::menu::IsMenuItem<_>> = items.iter().map(|b| b.as_ref()).collect(); Menu::with_items(handle, &refs) }) - // Inert while tauri.conf.json sets dragDropEnabled=false (needed for HTML5 pane drag). See diffplug/mouseterm#38 and tauri-apps/tauri#14373. + // Inert while tauri.conf.json sets dragDropEnabled=false (needed for HTML5 pane drag). See diffplug/dormouse#38 and tauri-apps/tauri#14373. .on_window_event(|window, event| { if let WindowEvent::DragDrop(DragDropEvent::Drop { paths, .. }) = event { let payload: Vec = paths .iter() .map(|p| p.to_string_lossy().into_owned()) .collect(); - let _ = window.emit("mouseterm://files-dropped", serde_json::json!({ "paths": payload })); + let _ = window.emit("dormouse://files-dropped", serde_json::json!({ "paths": payload })); } }) .setup(|app| { @@ -648,7 +648,7 @@ pub fn run() { read_update_log, ]) .build(tauri::generate_context!()) - .expect("error while building MouseTerm") + .expect("error while building Dormouse") .run(|app, event| { if let RunEvent::Exit = event { if let Some(state) = app.try_state::() { @@ -674,7 +674,7 @@ mod tests { .duration_since(UNIX_EPOCH) .expect("system time before unix epoch") .as_nanos(); - let path = std::env::temp_dir().join(format!("mouseterm-{name}-{suffix}")); + let path = std::env::temp_dir().join(format!("dormouse-{name}-{suffix}")); fs::create_dir_all(&path).expect("failed to create temp dir"); TempDir(path) } @@ -737,24 +737,24 @@ mod tests { #[test] fn strips_windows_verbatim_prefix_for_node_main_script() { let path = strip_windows_verbatim_prefix( - r"\\?\C:\Users\EdgarTwigg\AppData\Local\MouseTerm\_up_\sidecar\main.js", + r"\\?\C:\Users\EdgarTwigg\AppData\Local\Dormouse\_up_\sidecar\main.js", ) .expect("expected verbatim path to be stripped"); assert_eq!( path, - PathBuf::from(r"C:\Users\EdgarTwigg\AppData\Local\MouseTerm\_up_\sidecar\main.js") + PathBuf::from(r"C:\Users\EdgarTwigg\AppData\Local\Dormouse\_up_\sidecar\main.js") ); } #[test] fn strips_windows_verbatim_unc_prefix_for_node_main_script() { - let path = strip_windows_verbatim_prefix(r"\\?\UNC\server\share\MouseTerm\sidecar\main.js") + let path = strip_windows_verbatim_prefix(r"\\?\UNC\server\share\Dormouse\sidecar\main.js") .expect("expected verbatim UNC path to be stripped"); assert_eq!( path, - PathBuf::from(r"\\server\share\MouseTerm\sidecar\main.js") + PathBuf::from(r"\\server\share\Dormouse\sidecar\main.js") ); } diff --git a/standalone/src-tauri/src/main.rs b/standalone/src-tauri/src/main.rs index d3149f99..8c7cbec4 100644 --- a/standalone/src-tauri/src/main.rs +++ b/standalone/src-tauri/src/main.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - mouseterm_lib::run(); + dormouse_lib::run(); } diff --git a/standalone/src-tauri/tauri.conf.json b/standalone/src-tauri/tauri.conf.json index 32cd7672..abce3b8c 100644 --- a/standalone/src-tauri/tauri.conf.json +++ b/standalone/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "MouseTerm", + "productName": "Dormouse", "version": "0.9.1", - "identifier": "com.mouseterm.standalone", + "identifier": "sh.dormouse.standalone", "build": { "beforeDevCommand": "pnpm dev", "devUrl": "http://localhost:1420", @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "MouseTerm", + "title": "Dormouse", "titleBarStyle": "Overlay", "hiddenTitle": true, "width": 1200, @@ -47,7 +47,7 @@ "updater": { "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEFDNUE3RThENTQxQTY0REIKUldUYlpCcFVqWDVhckxRQjBFbGw4anhJMUZ5L2VEU0pGNTluS1hPR0F1OGc1T3BUYTVjbHd0WG0K", "endpoints": [ - "https://mouseterm.com/standalone-latest.json" + "https://dormouse.sh/standalone-latest.json" ], "windows": { "installMode": "passive" diff --git a/standalone/src/AppBar.tsx b/standalone/src/AppBar.tsx index 0a38240d..ac89a035 100644 --- a/standalone/src/AppBar.tsx +++ b/standalone/src/AppBar.tsx @@ -109,7 +109,7 @@ function ShellDropdown({ shells }: { shells: ShellEntry[] }) { shell: ShellEntry, options: { replaceUntouched?: boolean; announce?: boolean } = {}, ) => { - window.dispatchEvent(new CustomEvent('mouseterm:new-terminal', { + window.dispatchEvent(new CustomEvent('dormouse:new-terminal', { detail: { shell: shell.path, args: shell.args, diff --git a/standalone/src/main.tsx b/standalone/src/main.tsx index d39b8b78..29b423b1 100644 --- a/standalone/src/main.tsx +++ b/standalone/src/main.tsx @@ -1,12 +1,12 @@ import { StrictMode, useEffect, useState } from "react"; import { createRoot } from "react-dom/client"; import { invoke } from "@tauri-apps/api/core"; -import { setPlatform } from "mouseterm-lib/lib/platform"; -import { resumeOrRestore } from "mouseterm-lib/lib/reconnect"; -import { setDefaultShellOpts } from "mouseterm-lib/lib/shell-defaults"; -import { restoreActiveTheme } from "mouseterm-lib/lib/themes"; -import App from "mouseterm-lib/App"; -import "mouseterm-lib/index.css"; +import { setPlatform } from "dormouse-lib/lib/platform"; +import { resumeOrRestore } from "dormouse-lib/lib/reconnect"; +import { setDefaultShellOpts } from "dormouse-lib/lib/shell-defaults"; +import { restoreActiveTheme } from "dormouse-lib/lib/themes"; +import App from "dormouse-lib/App"; +import "dormouse-lib/index.css"; import { TauriAdapter } from "./tauri-adapter"; import { UpdateBanner } from "./UpdateBanner"; import { UpdateDebugDialog } from "./UpdateDebugDialog"; @@ -73,7 +73,7 @@ function ConnectedUpdateBanner() { // Await init() first to register event listeners before reconnecting async function bootstrap() { await platform.init(); - const { initAlertStateReceiver } = await import("mouseterm-lib/lib/terminal-registry"); + const { initAlertStateReceiver } = await import("dormouse-lib/lib/terminal-registry"); initAlertStateReceiver(); restoreActiveTheme(); diff --git a/standalone/src/tauri-adapter.ts b/standalone/src/tauri-adapter.ts index 34c0215f..b5f89160 100644 --- a/standalone/src/tauri-adapter.ts +++ b/standalone/src/tauri-adapter.ts @@ -1,16 +1,16 @@ import { invoke as rawInvoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import type { AlertStateDetail, PlatformAdapter, PtyInfo } from "mouseterm-lib/lib/platform/types"; -import { AlertManager, type SessionStatus } from "mouseterm-lib/lib/alert-manager"; +import type { AlertStateDetail, PlatformAdapter, PtyInfo } from "dormouse-lib/lib/platform/types"; +import { AlertManager, type SessionStatus } from "dormouse-lib/lib/alert-manager"; import { applyTerminalProtocolEvents, collectTerminalSemanticEvents, collectTerminalProtocolResponses, TerminalProtocolParser, -} from "mouseterm-lib/lib/terminal-protocol"; +} from "dormouse-lib/lib/terminal-protocol"; import { applyTerminalSemanticEventsByPtyId, -} from "mouseterm-lib/lib/terminal-state-store"; +} from "dormouse-lib/lib/terminal-state-store"; function invoke(cmd: string, args?: Record): void { rawInvoke(cmd, args).catch((err) => @@ -105,9 +105,9 @@ export class TauriAdapter implements PlatformAdapter { }), ); - // Inert while dragDropEnabled=false in tauri.conf.json. See diffplug/mouseterm#38 and tauri-apps/tauri#14373. + // Inert while dragDropEnabled=false in tauri.conf.json. See diffplug/dormouse#38 and tauri-apps/tauri#14373. this.unlistenFns.push( - await listen<{ paths: string[] }>("mouseterm://files-dropped", (event) => { + await listen<{ paths: string[] }>("dormouse://files-dropped", (event) => { const paths = event.payload.paths ?? []; if (paths.length === 0) return; for (const handler of this.filesDroppedHandlers) handler(paths); @@ -282,7 +282,7 @@ export class TauriAdapter implements PlatformAdapter { // --- State persistence --- - private static STATE_KEY = 'mouseterm.session'; + private static STATE_KEY = 'dormouse.session'; saveState(state: unknown): void { try { diff --git a/standalone/src/updater.test.ts b/standalone/src/updater.test.ts index 3576720e..1d3a81bb 100644 --- a/standalone/src/updater.test.ts +++ b/standalone/src/updater.test.ts @@ -36,7 +36,7 @@ vi.mock('@tauri-apps/api/core', () => ({ // --- Helpers --- -const STORAGE_KEY = 'mouseterm:update-result'; +const STORAGE_KEY = 'dormouse:update-result'; function makeUpdate(version = '0.5.0') { return { @@ -269,7 +269,7 @@ describe('updater', () => { openChangelog(); await vi.advanceTimersByTimeAsync(0); - expect(mocks.shellOpen).toHaveBeenCalledWith('https://mouseterm.com/changelog/after/0.4.0'); + expect(mocks.shellOpen).toHaveBeenCalledWith('https://dormouse.sh/changelog/after/0.4.0'); }); }); diff --git a/standalone/src/updater.ts b/standalone/src/updater.ts index 469c0615..aaef0a15 100644 --- a/standalone/src/updater.ts +++ b/standalone/src/updater.ts @@ -4,10 +4,10 @@ import { getCurrentWindow } from '@tauri-apps/api/window'; import { getVersion } from '@tauri-apps/api/app'; import { open } from '@tauri-apps/plugin-shell'; import { invoke } from '@tauri-apps/api/core'; -import { PLATFORM_STRING } from 'mouseterm-lib/lib/platform'; +import { PLATFORM_STRING } from 'dormouse-lib/lib/platform'; import type { UpdateBannerState } from './UpdateBanner'; -const GITHUB_REPO_URL = 'https://github.com/diffplug/mouseterm'; +const GITHUB_REPO_URL = 'https://github.com/diffplug/dormouse'; function openUrl(url: string, context: string): void { open(url).catch((e) => console.error(`[updater] Failed to open ${context}:`, e)); @@ -15,7 +15,7 @@ function openUrl(url: string, context: string): void { // --- State --- -const STORAGE_KEY = 'mouseterm:update-result'; +const STORAGE_KEY = 'dormouse:update-result'; let state: UpdateBannerState = { status: 'idle' }; let availableUpdate: Update | null = null; @@ -65,7 +65,7 @@ export function openChangelog(): void { async function openCurrentVersionChangelog(): Promise { const version = (await getVersion()).trim(); - openUrl(`https://mouseterm.com/changelog/after/${encodeURIComponent(version)}`, 'changelog'); + openUrl(`https://dormouse.sh/changelog/after/${encodeURIComponent(version)}`, 'changelog'); } export async function buildDebugReport(error: string, toVersion: string): Promise { diff --git a/standalone/tsconfig.json b/standalone/tsconfig.json index 58028696..e1f66d18 100644 --- a/standalone/tsconfig.json +++ b/standalone/tsconfig.json @@ -16,7 +16,7 @@ "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, "paths": { - "mouseterm-lib/*": ["../lib/src/*"] + "dormouse-lib/*": ["../lib/src/*"] } }, "include": ["src"] diff --git a/standalone/vite.config.ts b/standalone/vite.config.ts index 5acbf70a..ab69ce86 100644 --- a/standalone/vite.config.ts +++ b/standalone/vite.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ resolve: { dedupe: ["react", "react-dom"], alias: { - "mouseterm-lib": path.resolve(libDir, "src"), + "dormouse-lib": path.resolve(libDir, "src"), }, }, // Tauri expects a fixed port; fail if that port is not available diff --git a/standalone/vitest.config.ts b/standalone/vitest.config.ts index d91a0fe2..99621273 100644 --- a/standalone/vitest.config.ts +++ b/standalone/vitest.config.ts @@ -4,7 +4,7 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ resolve: { alias: { - 'mouseterm-lib': path.resolve(__dirname, '../lib/src'), + 'dormouse-lib': path.resolve(__dirname, '../lib/src'), }, }, test: { diff --git a/vscode-ext/README.md b/vscode-ext/README.md index c5217ca5..d0547bca 100644 --- a/vscode-ext/README.md +++ b/vscode-ext/README.md @@ -1,17 +1,17 @@ -# MouseTerm +# Dormouse -Terminal multiplexer for VS Code (or [standalone app](https://mouseterm.com/#download)) - tmux keybindings, mouse support, human-friendly copy-paste, and alerts for completed tasks. +Terminal multiplexer for VS Code (or [standalone app](https://dormouse.sh/#download)) โ€” tmux keybindings, mouse support, human-friendly copy-paste, and alerts for completed tasks. -[mouseterm.com/playground](https://mouseterm.com/playground) - try before you install +[dormouse.sh/playground](https://dormouse.sh/playground) โ€” try before you install TODO: Hero GIF. ## Alert System -MouseTerm can WATCH a pane the same way you do โ€” visual motion. When a watched pane stops changing for two seconds, it marks the task complete and alerts you. Apps can also ask for attention with terminal notification protocols, and shell-integrated commands can alert when they finish after you stop paying attention. +Dormouse tracks activity the same way you do โ€” visual motion. When a pane stops changing for two seconds, it marks the task complete and alerts you. Works with any CLI tool that prints to a terminal, no plugins or configuration. -- todo-disabled WATCHING disabled -- todo-enabled WATCHING enabled +- todo-disabled alerts disabled +- todo-enabled alerts enabled - todo-armed task is running, will send an alert when task completes - todo-ringing task is finished and needs your attention @@ -21,11 +21,11 @@ This lightweight TODO system remembers which tasks need follow-up so you don't h ## Mouse-Friendly Copy and Paste -When you copy-paste from a terminal, you are usually stuck with a bunch of newlines that you wouldn't get if you were copying from any other kind of program. MouseTerm can optionally remove these with `Copy Rewrapped`. +When you copy-paste from a terminal, you are usually stuck with a bunch of newlines that you wouldn't get if you were copying from any other kind of program. Dormouse can optionally remove these with `Copy Rewrapped`. copy-paste -For TUIs which register for xterm mouse interception (such as `htop` and `neovim`), most terminals make it impossible for you to copy using the mouse. MouseTerm makes it easy to temporarily override the mouse interception. +For TUIs which register for xterm mouse interception (such as `htop` and `neovim`), most terminals make it impossible for you to copy using the mouse. Dormouse makes it easy to temporarily override the mouse interception. TODO: GIF showing htop and the override mechanism @@ -43,7 +43,7 @@ TODO: layout GIF ## Keyboard Shortcuts -If you use the mouse, then MouseTerm is always in **passthrough** mode, where all keypresses passthrough to the selected terminal. If you press `LShift` followed by `RShift` in quick succession (or `LCmd โ†’ RCmd`, or `LCtrl โ†’ RCtrl`), then you will enter **command** mode where keypresses can spawn terminals, navigate panes, and rearrange the layout. +If you use the mouse, then Dormouse is always in **passthrough** mode, where all keypresses passthrough to the selected terminal. If you press `LShift` followed by `RShift` in quick succession (or `LCmd โ†’ RCmd`, or `LCtrl โ†’ RCtrl`), then you will enter **command** mode where keypresses can spawn terminals, navigate panes, and rearrange the layout. ### Command Mode Shortcuts @@ -62,24 +62,24 @@ If you use the mouse, then MouseTerm is always in **passthrough** mode, where al ## Any Theme, Anywhere -MouseTerm uses your VSCode theme โ€” colors, styling, everything. Switch themes and MouseTerm switches with you. No separate configuration, no mismatched colors. +Dormouse uses your VSCode theme โ€” colors, styling, everything. Switch themes and Dormouse switches with you. No separate configuration, no mismatched colors. -TODO: GIF showing theme switching โ€” user changes VSCode theme and MouseTerm updates instantly to match +TODO: GIF showing theme switching โ€” user changes VSCode theme and Dormouse updates instantly to match -You can also use MouseTerm in the Panel area (bottom, next to the built-in terminal), in the Editor area (center region where you edit files), or both. +You can also use Dormouse in the Panel area (bottom, next to the built-in terminal), in the Editor area (center region where you edit files), or both. -TODO: GIF showing MouseTerm in various areas +TODO: GIF showing Dormouse in various areas ## Getting Started 1. Install the extension 2. Open the command palette (`Cmd+Shift+P` / `Ctrl+Shift+P`) - - **MouseTerm: Focus** to open the "Panel" version of MouseTerm (next to the terminal) - - **MouseTerm: Open in Editor** to open a MouseTerm tab in the content area (you can open multiple) + - **Dormouse: Focus** to open the "Panel" version of Dormouse (next to the terminal) + - **Dormouse: Open in Editor** to open a Dormouse tab in the content area (you can open multiple) ## Links -- Prefer a standalone terminal app? Self-updating installers available for Win, Mac and Linux at [mouseterm.com](https://mouseterm.com/#download) -- You can try it in a [browser playground](https://mouseterm.com/playground) -- [GitHub](https://github.com/diffplug/mouseterm) +- Prefer a standalone terminal app? Self-updating installers available for Win, Mac and Linux at [dormouse.sh](https://dormouse.sh/#download) +- You can try it in a [browser playground](https://dormouse.sh/playground) +- [GitHub](https://github.com/diffplug/dormouse) - Brought to you by [DiffPlug](https://www.diffplug.com/) diff --git a/vscode-ext/package.json b/vscode-ext/package.json index 9d0aabd1..8f452555 100644 --- a/vscode-ext/package.json +++ b/vscode-ext/package.json @@ -1,15 +1,15 @@ { - "name": "mouseterm", - "displayName": "MouseTerm", - "description": "Multitasking terminal with tmux keybindings, mouse support, and a built-in alert system for completed tasks and prompts.", + "name": "dormouse", + "displayName": "Dormouse โ€” Terminal Multiplexer", + "description": "A persistent multitasking terminal โ€” tmux keybindings, mouse support, and a built-in alert system that buzzes you when builds, agents, or scripts finish.", "version": "0.9.1", "publisher": "diffplug", "license": "FSL-1.1-MIT", "icon": "icon.png", - "homepage": "https://mouseterm.com/", + "homepage": "https://dormouse.sh/", "repository": { "type": "git", - "url": "https://github.com/diffplug/mouseterm" + "url": "https://github.com/diffplug/dormouse" }, "engines": { "vscode": "^1.85.0" @@ -22,75 +22,77 @@ "terminal", "multiplexer", "mouse", + "dormouse", "ai", "agent", "alert", + "persistent", "split", "panes", "completion" ], "activationEvents": [ - "onView:mouseterm.view", - "onWebviewPanel:mouseterm" + "onView:dormouse.view", + "onWebviewPanel:dormouse" ], "main": "./dist/extension.js", "contributes": { "commands": [ { - "command": "mouseterm.focus", - "title": "MouseTerm: Focus", + "command": "dormouse.focus", + "title": "Dormouse: Focus", "icon": { "light": "icon-tiny-light.png", "dark": "icon-tiny-dark.png" } }, { - "command": "mouseterm.open", - "title": "MouseTerm: Open in Editor" + "command": "dormouse.open", + "title": "Dormouse: Open in Editor" }, { - "command": "mouseterm.debugTheme", - "title": "MouseTerm: Debug Theme" + "command": "dormouse.debugTheme", + "title": "Dormouse: Debug Theme" }, { - "command": "mouseterm.newTerminal", - "title": "MouseTerm: New Terminal", + "command": "dormouse.newTerminal", + "title": "Dormouse: New Terminal", "icon": "$(add)" }, { - "command": "mouseterm.selectShell", - "title": "MouseTerm: Select Shell", + "command": "dormouse.selectShell", + "title": "Dormouse: Select Shell", "icon": "$(gear)" } ], "menus": { "view/title": [ { - "command": "mouseterm.selectShell", + "command": "dormouse.selectShell", "group": "navigation@1", - "when": "view == mouseterm.view" + "when": "view == dormouse.view" }, { - "command": "mouseterm.newTerminal", + "command": "dormouse.newTerminal", "group": "navigation@2", - "when": "view == mouseterm.view" + "when": "view == dormouse.view" } ] }, "viewsContainers": { "panel": [ { - "id": "mouseterm-panel", - "title": "MouseTerm", + "id": "dormouse-panel", + "title": "Dormouse", "icon": "$(terminal)" } ] }, "views": { - "mouseterm-panel": [ + "dormouse-panel": [ { - "id": "mouseterm.view", - "name": "MouseTerm", + "id": "dormouse.view", + "name": "Dormouse", "type": "webview" } ] @@ -101,8 +103,8 @@ "build:frontend": "vite build --config vite.config.ts", "build": "esbuild src/extension.ts --bundle --outdir=dist --external:vscode --external:node-pty --format=cjs --platform=node && esbuild src/pty-host.js --bundle --outfile=dist/pty-host.js --external:node-pty --format=cjs --platform=node && cp -RL node_modules/node-pty dist/node-pty", "watch": "pnpm build --watch", - "package": "vsce package --no-dependencies --out mouseterm.vsix", - "dogfood": "pnpm package && code --install-extension mouseterm.vsix --force && rm -f mouseterm.vsix && echo '\\nโœฆ Reload VSCode window (Cmd+Shift+P โ†’ Reload Window) to pick up the new extension.'", + "package": "vsce package --no-dependencies --out dormouse.vsix", + "dogfood": "pnpm package && code --install-extension dormouse.vsix --force && rm -f dormouse.vsix && echo '\\nโœฆ Reload VSCode window (Cmd+Shift+P โ†’ Reload Window) to pick up the new extension.'", "publish:marketplace": "vsce publish --no-dependencies", "publish:openvsx": "ovsx publish --no-dependencies" }, diff --git a/vscode-ext/src/extension.ts b/vscode-ext/src/extension.ts index 912f0f5f..8c25cc0f 100644 --- a/vscode-ext/src/extension.ts +++ b/vscode-ext/src/extension.ts @@ -1,7 +1,7 @@ import * as vscode from 'vscode'; import * as path from 'path'; import * as ptyManager from './pty-manager'; -import { MouseTermViewProvider } from './webview-view-provider'; +import { DormouseViewProvider } from './webview-view-provider'; import { attachRouter, flushAllSessions, getAlertStates } from './message-router'; import { getWebviewHtml } from './webview-html'; import { log } from './log'; @@ -10,7 +10,7 @@ import { readPersistedSession } from '../../lib/src/lib/session-types'; import { resolveSelectedShell, setSelectedShellPath, getSelectedShellPath } from './shell-selection'; import type { ExtensionMessage } from './message-types'; -type NewTerminalMessage = Extract; +type NewTerminalMessage = Extract; let extensionContext: vscode.ExtensionContext | null = null; @@ -19,7 +19,7 @@ let extensionContext: vscode.ExtensionContext | null = null; * * @param savedState Per-panel state. For `deserializeWebviewPanel` this is the * state VS Code preserved from the panel's `vscode.setState()`; for a fresh - * panel opened via `mouseterm.open` this is `undefined`. + * panel opened via `dormouse.open` this is `undefined`. */ function setupPanel( context: vscode.ExtensionContext, @@ -63,7 +63,7 @@ export function activate(context: vscode.ExtensionContext) { extensionContext = context; ptyManager.setExtensionPath(context.extensionPath); - const provider = new MouseTermViewProvider(context); + const provider = new DormouseViewProvider(context); // Updates the shell-derived state in one place: the view header (shell // name appears next to the title via description) and the webview's @@ -74,13 +74,13 @@ export function activate(context: vscode.ExtensionContext) { }; const postNewTerminal = async (message: Omit) => { - await vscode.commands.executeCommand('mouseterm.view.focus'); + await vscode.commands.executeCommand('dormouse.view.focus'); for (const delay of [0, 50, 200]) { if (delay > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); } const posted = await provider.postMessage({ - type: 'mouseterm:newTerminal', + type: 'dormouse:newTerminal', ...message, }); if (posted) return true; @@ -97,25 +97,25 @@ export function activate(context: vscode.ExtensionContext) { }); context.subscriptions.push( - vscode.window.registerWebviewViewProvider('mouseterm.view', provider, { + vscode.window.registerWebviewViewProvider('dormouse.view', provider, { // Keep the webview script + xterm DOM alive when the Panel is hidden // (close/toggle), so PTYs and scrollback are preserved across re-show // without going through the reconnect dance. webviewOptions: { retainContextWhenHidden: true }, }), - vscode.window.registerWebviewPanelSerializer('mouseterm', { + vscode.window.registerWebviewPanelSerializer('dormouse', { async deserializeWebviewPanel(panel: vscode.WebviewPanel, state: unknown) { setupPanel(context, panel, state, () => provider.getSelectedShell()); }, }), - vscode.commands.registerCommand('mouseterm.focus', () => { - vscode.commands.executeCommand('mouseterm.view.focus'); + vscode.commands.registerCommand('dormouse.focus', () => { + vscode.commands.executeCommand('dormouse.view.focus'); }), - vscode.commands.registerCommand('mouseterm.open', () => { + vscode.commands.registerCommand('dormouse.open', () => { const mediaPath = path.join(context.extensionPath, 'media'); const panel = vscode.window.createWebviewPanel( - 'mouseterm', - 'MouseTerm', + 'dormouse', + 'Dormouse', vscode.ViewColumn.Active, { enableScripts: true, @@ -125,18 +125,18 @@ export function activate(context: vscode.ExtensionContext) { ); setupPanel(context, panel, undefined, () => provider.getSelectedShell()); }), - vscode.commands.registerCommand('mouseterm.debugTheme', async () => { - await vscode.commands.executeCommand('mouseterm.view.focus'); + vscode.commands.registerCommand('dormouse.debugTheme', async () => { + await vscode.commands.executeCommand('dormouse.view.focus'); for (const delay of [0, 50, 200]) { if (delay > 0) { await new Promise((resolve) => setTimeout(resolve, delay)); } - const posted = await provider.postMessage({ type: 'mouseterm:openThemeDebugger' }); + const posted = await provider.postMessage({ type: 'dormouse:openThemeDebugger' }); if (posted) return; } - void vscode.window.showWarningMessage('MouseTerm: open the MouseTerm view before debugging the theme.'); + void vscode.window.showWarningMessage('Dormouse: open the Dormouse view before debugging the theme.'); }), - vscode.commands.registerCommand('mouseterm.newTerminal', async () => { + vscode.commands.registerCommand('dormouse.newTerminal', async () => { const shells = await ptyManager.getAvailableShells(); const shell = resolveSelectedShell(context, shells); const posted = await postNewTerminal({ @@ -145,13 +145,13 @@ export function activate(context: vscode.ExtensionContext) { name: shell?.name, }); if (!posted) { - void vscode.window.showWarningMessage('MouseTerm: open the MouseTerm view before creating a terminal.'); + void vscode.window.showWarningMessage('Dormouse: open the Dormouse view before creating a terminal.'); } }), - vscode.commands.registerCommand('mouseterm.selectShell', async () => { + vscode.commands.registerCommand('dormouse.selectShell', async () => { const shells = await ptyManager.getAvailableShells(); if (shells.length === 0) { - void vscode.window.showWarningMessage('MouseTerm: no shells detected.'); + void vscode.window.showWarningMessage('Dormouse: no shells detected.'); return; } const currentPath = getSelectedShellPath(context) ?? shells[0].path; @@ -163,7 +163,7 @@ export function activate(context: vscode.ExtensionContext) { args: s.args, })); const picked = await vscode.window.showQuickPick(items, { - title: 'Select default shell for MouseTerm', + title: 'Select default shell for Dormouse', placeHolder: 'Changing this opens a matching terminal; new panes reuse it.', }); if (!picked) return; @@ -193,7 +193,7 @@ export function activate(context: vscode.ExtensionContext) { announce: true, }); if (!posted) { - void vscode.window.showWarningMessage('MouseTerm: open the MouseTerm view before changing the active terminal type.'); + void vscode.window.showWarningMessage('Dormouse: open the Dormouse view before changing the active terminal type.'); } } }), diff --git a/vscode-ext/src/log.ts b/vscode-ext/src/log.ts index b9880db4..a10620b4 100644 --- a/vscode-ext/src/log.ts +++ b/vscode-ext/src/log.ts @@ -5,7 +5,7 @@ let channel: vscode.OutputChannel | null = null; export const log = { init() { if (!channel) { - channel = vscode.window.createOutputChannel('MouseTerm'); + channel = vscode.window.createOutputChannel('Dormouse'); } }, info(...args: unknown[]) { diff --git a/vscode-ext/src/message-router.ts b/vscode-ext/src/message-router.ts index a61c01ff..2fad7967 100644 --- a/vscode-ext/src/message-router.ts +++ b/vscode-ext/src/message-router.ts @@ -118,7 +118,7 @@ export function attachRouter( let disposed = false; // Webview-facing subscriptions โ€” only active when the webview has live content. - // Subscribed on mouseterm:init, unsubscribed when webview content is gone. + // Subscribed on dormouse:init, unsubscribed when webview content is gone. let disconnectWebview: (() => void) | null = null; function claim(id: string): void { @@ -160,13 +160,13 @@ export function attachRouter( timeout, }); - void webview.postMessage({ type: 'mouseterm:flushSessionSave', requestId } satisfies ExtensionMessage); + void webview.postMessage({ type: 'dormouse:flushSessionSave', requestId } satisfies ExtensionMessage); }); } /** * Subscribe PTY data and alert state forwarding to the webview. - * Called when the webview sends mouseterm:init (proving it has live content). + * Called when the webview sends dormouse:init (proving it has live content). * Returns a cleanup function that unsubscribes everything. */ function connectWebview(): () => void { @@ -270,7 +270,7 @@ export function attachRouter( webview.postMessage({ type: 'clipboard:image', path: null, requestId: msg.requestId } satisfies ExtensionMessage); }); break; - case 'mouseterm:init': { + case 'dormouse:init': { // Webview has (re-)initialized โ€” subscribe to live events. // Tear down previous subscriptions first (webview was destroyed and recreated). disconnectWebview?.(); @@ -281,7 +281,7 @@ export function attachRouter( const selected = options?.getSelectedShell?.(); if (selected) { webview.postMessage({ - type: 'mouseterm:selectedShell', + type: 'dormouse:selectedShell', shell: selected.shell, args: selected.args, } satisfies ExtensionMessage); @@ -368,10 +368,10 @@ export function attachRouter( } break; } - case 'mouseterm:flushSessionSaveDone': + case 'dormouse:flushSessionSaveDone': resolveFlushRequest(msg.requestId); break; - case 'mouseterm:saveState': + case 'dormouse:saveState': options?.onSaveState?.(msg.state); break; diff --git a/vscode-ext/src/message-types.ts b/vscode-ext/src/message-types.ts index 6d0b22c7..b49e8b1c 100644 --- a/vscode-ext/src/message-types.ts +++ b/vscode-ext/src/message-types.ts @@ -12,9 +12,9 @@ export type WebviewMessage = | { type: 'pty:getShells'; requestId?: string } | { type: 'clipboard:readFiles'; requestId: string } | { type: 'clipboard:readImage'; requestId: string } - | { type: 'mouseterm:init' } - | { type: 'mouseterm:saveState'; state: unknown } - | { type: 'mouseterm:flushSessionSaveDone'; requestId: string } + | { type: 'dormouse:init' } + | { type: 'dormouse:saveState'; state: unknown } + | { type: 'dormouse:flushSessionSaveDone'; requestId: string } // Alert actions | { type: 'alert:remove'; id: string } | { type: 'alert:toggle'; id: string } @@ -47,16 +47,16 @@ export type ExtensionMessage = | { type: 'clipboard:files'; paths: string[] | null; requestId: string } | { type: 'clipboard:image'; path: string | null; requestId: string } | { - type: 'mouseterm:newTerminal'; + type: 'dormouse:newTerminal'; shell?: string; args?: string[]; name?: string; replaceUntouched?: boolean; announce?: boolean; } - | { type: 'mouseterm:selectedShell'; shell?: string; args?: string[] } - | { type: 'mouseterm:openThemeDebugger' } - | { type: 'mouseterm:flushSessionSave'; requestId: string } + | { type: 'dormouse:selectedShell'; shell?: string; args?: string[] } + | { type: 'dormouse:openThemeDebugger' } + | { type: 'dormouse:flushSessionSave'; requestId: string } // Alert state updates | { type: 'alert:state'; diff --git a/vscode-ext/src/session-state.ts b/vscode-ext/src/session-state.ts index 22b77e70..6fa612a4 100644 --- a/vscode-ext/src/session-state.ts +++ b/vscode-ext/src/session-state.ts @@ -4,7 +4,7 @@ import type { AlertState } from '../../lib/src/lib/alert-manager'; import { readPersistedSession, type PersistedAlertState, type PersistedPane, type PersistedSession } from '../../lib/src/lib/session-types'; import { log } from './log'; -const SESSION_STATE_KEY = 'mouseterm.session'; +const SESSION_STATE_KEY = 'dormouse.session'; export function getSavedSessionState(context: vscode.ExtensionContext): PersistedSession | null { const saved = readPersistedSession(context.workspaceState.get(SESSION_STATE_KEY)); diff --git a/vscode-ext/src/shell-selection.ts b/vscode-ext/src/shell-selection.ts index 7fe861c9..f765ee62 100644 --- a/vscode-ext/src/shell-selection.ts +++ b/vscode-ext/src/shell-selection.ts @@ -6,7 +6,7 @@ export interface ShellEntry { args: string[]; } -const KEY = 'mouseterm.selectedShellPath'; +const KEY = 'dormouse.selectedShellPath'; export function getSelectedShellPath(context: vscode.ExtensionContext): string | undefined { return context.workspaceState.get(KEY) ?? context.globalState.get(KEY); diff --git a/vscode-ext/src/webview-view-provider.ts b/vscode-ext/src/webview-view-provider.ts index a05d95a6..6418ec4f 100644 --- a/vscode-ext/src/webview-view-provider.ts +++ b/vscode-ext/src/webview-view-provider.ts @@ -8,7 +8,7 @@ import * as ptyManager from './pty-manager'; import { resolveSelectedShell } from './shell-selection'; import { log } from './log'; -export class MouseTermViewProvider implements vscode.WebviewViewProvider { +export class DormouseViewProvider implements vscode.WebviewViewProvider { private view: vscode.WebviewView | undefined; private routerDisposable: vscode.Disposable | undefined; private description: string | undefined; @@ -28,7 +28,7 @@ export class MouseTermViewProvider implements vscode.WebviewViewProvider { setSelectedShell(opts: { shell?: string; args?: string[] } | null): void { this.selectedShell = opts; void this.postMessage({ - type: 'mouseterm:selectedShell', + type: 'dormouse:selectedShell', shell: opts?.shell, args: opts?.args, }); diff --git a/website/index.html b/website/index.html index fca90f9b..3cf42a6b 100644 --- a/website/index.html +++ b/website/index.html @@ -4,30 +4,30 @@ - MouseTerm โ€” The multitasking terminal for mice - + Dormouse โ€” A dormouse knows when to wake up + - + - - - - - + + + + + - + - - - - + + + + diff --git a/website/package.json b/website/package.json index d8704acc..f3081aef 100644 --- a/website/package.json +++ b/website/package.json @@ -1,5 +1,5 @@ { - "name": "mouseterm-website", + "name": "dormouse-website", "private": true, "version": "0.1.0", "license": "FSL-1.1-MIT", @@ -16,7 +16,7 @@ "dependencies": { "@phosphor-icons/react": "^2.1.10", "ascii-splash": "0.3.0", - "mouseterm-lib": "workspace:*", + "dormouse-lib": "workspace:*", "react": "^19.2.0", "react-dom": "^19.2.0", "react-router-dom": "^6.14.1" diff --git a/website/public/og-image.jpg b/website/public/og-image.jpg index af098643..edbc04cc 100644 Binary files a/website/public/og-image.jpg and b/website/public/og-image.jpg differ diff --git a/website/public/standalone-latest.json b/website/public/standalone-latest.json index 95c3863b..56da6f0e 100644 --- a/website/public/standalone-latest.json +++ b/website/public/standalone-latest.json @@ -1,18 +1,18 @@ { "version": "0.9.1", - "notes": "See https://github.com/diffplug/mouseterm/releases/tag/v0.9.1", + "notes": "See https://github.com/diffplug/dormouse/releases/tag/v0.9.1", "pub_date": "2026-05-01T22:02:25Z", "platforms": { "darwin-aarch64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.9.1/MouseTerm-macos-aarch64.tar.gz", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.9.1/Dormouse-macos-aarch64.tar.gz", "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUYlpCcFVqWDVhckMvSXc5aXd0TXNFdEUwcmVxOUpvZG53SmdZQ0ZhYlhDcUQ2WU1LWlF1eGx2bG15NHJEa3phK1ZVcjFIaWVGNGUwS3BqKzFXMmNPUWlmem1wMUJBSGdNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc3NjcyOTQ0CWZpbGU6TW91c2VUZXJtLW1hY29zLWFhcmNoNjQudGFyLmd6ClE3OWlndURTdkI5Mm9FeXM0U1Q4RGxYbFFtU2xIN1RsN095VmhlZTJqQ215U01yRFphNE5jYnNvcTZjUGI5Q0dObHgyMTJieE1pK2ZwdXJFTFZMb0JBPT0K" }, "windows-x86_64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.9.1/MouseTerm-windows-x64-setup.exe", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.9.1/Dormouse-windows-x64-setup.exe", "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUYlpCcFVqWDVhckdQRmYzS1dnRnYzUzBiUUMzanNINFpzLzZTemtwNTM4L3NmOUN0SGxVcEZFcG5yOGxOS1JGRzY1Skw0RGRXenlQMHlmNlB6ZkRLYTNLeE9hWVB0U3dZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc3NjcyOTQ0CWZpbGU6TW91c2VUZXJtLXdpbmRvd3MteDY0LXNldHVwLmV4ZQp5ZU9XbGlSNFFNTVd0NFdZWi9YQW9KU1pxZWtDS3gyYWY4OUJ4eHZ4YkN0UkNicVZ5OXBFakdWWktOcmd6R29LZHRMeHlReCswa3d5VTgvbTNEK0xBQT09Cg==" }, "linux-x86_64": { - "url": "https://github.com/diffplug/mouseterm/releases/download/v0.9.1/MouseTerm-linux-x86_64.AppImage", + "url": "https://github.com/diffplug/dormouse/releases/download/v0.9.1/Dormouse-linux-x86_64.AppImage", "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVUYlpCcFVqWDVhckdvR3lCT3ZHQW51b1gxUWVVbGNNVlFMK0ZDNmdKdE5zYURXaHliaDFodHdoOFJabHB4UjVsalB1QWpTQ2NCNmlqOER5bGRpdXlCY0p2VHRCZEVWK3dBPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzc3NjcyOTQ1CWZpbGU6TW91c2VUZXJtLWxpbnV4LXg4Nl82NC5BcHBJbWFnZQoza240b1AyVWw2T3VKUmhFUXdRanRlNVVaVGRvUUUxL1VqZ3FCTmJrQmpzNmxPVnRpd3BxRzJwN3NyRnB1YmZoUUIxK2E3WG9ZbmlJUnFYK0J6Uk1EZz09Cg==" } } diff --git a/website/scripts/generate-deps.js b/website/scripts/generate-deps.js index 11a1c5a8..b7f40533 100644 --- a/website/scripts/generate-deps.js +++ b/website/scripts/generate-deps.js @@ -8,9 +8,9 @@ const repoRoot = resolve(__dirname, "../.."); const outPath = resolve(__dirname, "../src/data/dependencies.json"); const themeExtensionsPath = resolve(repoRoot, "lib/src/lib/themes/bundled-extensions.json"); const productDependencyFilters = [ - "mouseterm", - "mouseterm-standalone", - "mouseterm-lib", + "dormouse", + "dormouse-standalone", + "dormouse-lib", ]; function getInstalledStoreDir() { diff --git a/website/src/App.tsx b/website/src/App.tsx index 97eaa9b8..94fba271 100644 --- a/website/src/App.tsx +++ b/website/src/App.tsx @@ -10,8 +10,8 @@ export const routes: RouteRecord[] = [ lazy: () => import("./pages/Playground"), }, { - path: "/tether", - lazy: () => import("./pages/Tether"), + path: "/pocket", + lazy: () => import("./pages/Pocket"), }, { path: "/changelog", diff --git a/website/src/components/NotifySignupForm.tsx b/website/src/components/NotifySignupForm.tsx index bb49d4d6..3916f56a 100644 --- a/website/src/components/NotifySignupForm.tsx +++ b/website/src/components/NotifySignupForm.tsx @@ -63,7 +63,7 @@ export function NotifySignupForm() { type="submit" className="min-h-12 inline-flex items-center justify-center rounded-md border border-[var(--color-caramel)] bg-[var(--color-caramel)]/15 px-6 py-3 text-base font-display text-[var(--color-caramel)] transition hover:bg-[var(--color-caramel)]/25 sm:w-auto" > - Notify me when Tether ships + Notify me when Pocket ships {message && ( diff --git a/website/src/components/SiteHeader.tsx b/website/src/components/SiteHeader.tsx index 9cd28fae..80945a73 100644 --- a/website/src/components/SiteHeader.tsx +++ b/website/src/components/SiteHeader.tsx @@ -8,7 +8,7 @@ export const STATIC_PAGE_HEADER_STYLE: React.CSSProperties = { const NAV_LINKS: readonly { href: string; label: string; external?: boolean; hideOnMobile?: boolean }[] = [ { href: "/playground", label: "Playground", hideOnMobile: true }, { href: "/#download", label: "Download", hideOnMobile: true }, - { href: "https://github.com/diffplug/mouseterm", label: "GitHub", external: true }, + { href: "https://github.com/diffplug/dormouse", label: "GitHub", external: true }, ]; const CHROME_INACTIVE_BG = "var(--color-header-inactive-bg)"; @@ -90,7 +90,7 @@ const SiteHeader = forwardRef( } } > - MouseTerm + Dormouse
{controls ?
{controls}
: null} diff --git a/website/src/index.css b/website/src/index.css index 16134f97..baa4414c 100644 --- a/website/src/index.css +++ b/website/src/index.css @@ -36,19 +36,19 @@ html body { height: auto; } -html body.tether-marketing-body { +html body.pocket-marketing-body { overflow: auto; } -body.tether-marketing-body #root { +body.pocket-marketing-body #root { height: auto; } -html body.tether-terminal-body { +html body.pocket-terminal-body { overflow: hidden; } -body.tether-terminal-body #root { +body.pocket-terminal-body #root { height: 100vh; } diff --git a/website/src/lib/__snapshots__/tut-runner.test.ts.snap b/website/src/lib/__snapshots__/tut-runner.test.ts.snap index de1bd95c..12530a17 100644 --- a/website/src/lib/__snapshots__/tut-runner.test.ts.snap +++ b/website/src/lib/__snapshots__/tut-runner.test.ts.snap @@ -32,7 +32,7 @@ exports[`TutRunner snapshots > renders Copy paste with all items incomplete 1`] Press p to toggle the Place To Paste. Some terminal programs trap the cursor, and some do not. This tutorial pane - does not trap the cursor, so MouseTerm does not show a cursor icon. The + does not trap the cursor, so Dormouse does not show a cursor icon. The ascii-splash and changelog programs trap the cursor โ€” that is how they are able to respond to mouse movement. lazygit is an excellent and popular program which traps the cursor. @@ -78,7 +78,7 @@ exports[`TutRunner snapshots > renders Keyboard navigation with all items incomp exports[`TutRunner snapshots > renders the top-level menu 1`] = ` " - MouseTerm Playground Tutorial + Dormouse Playground Tutorial 0/17 complete ยท Esc/q to exit ยท Enter to open ยท โ†‘โ†“ to navigate โฏ Keyboard navigation [0/7 complete] diff --git a/website/src/lib/ascii-splash-runner.test.ts b/website/src/lib/ascii-splash-runner.test.ts index 439d9e4a..7ae63756 100644 --- a/website/src/lib/ascii-splash-runner.test.ts +++ b/website/src/lib/ascii-splash-runner.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +import { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import { AsciiSplashRunner } from "./ascii-splash-runner"; function createHarness(args: string[] = []) { diff --git a/website/src/lib/ascii-splash-runner.ts b/website/src/lib/ascii-splash-runner.ts index 9ce7f6b3..55dbe580 100644 --- a/website/src/lib/ascii-splash-runner.ts +++ b/website/src/lib/ascii-splash-runner.ts @@ -1,5 +1,5 @@ /* - * Browser adapter for ascii-splash@0.3.0 in the MouseTerm website playground. + * Browser adapter for ascii-splash@0.3.0 in the Dormouse website playground. * * This file is not the upstream CLI entrypoint. It imports upstream internals * from ascii-splash/dist through the website's `ascii-splash-internal` Vite @@ -14,7 +14,7 @@ * - BrowserTerminalRenderer writes ANSI output through FakePtyAdapter.sendOutput. * - Keyboard bytes and SGR mouse sequences are decoded from writePty input. * - Alt-screen, cursor, mouse-reporting, resize, start, and cleanup lifecycle - * are handled for xterm.js inside MouseTerm. + * are handled for xterm.js inside Dormouse. * - UI overlays/transitions are instantiated per runner instead of using * upstream singleton getters so multiple panes can run independently. * - Config persistence is intentionally omitted; upstream commands that need a @@ -60,8 +60,8 @@ import { LEAVE_ALT_SCREEN, MOUSE_DISABLE, MOUSE_ENABLE, -} from "mouseterm-lib/lib/ansi"; -import type { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +} from "dormouse-lib/lib/ansi"; +import type { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import type { InteractiveProgram } from "./tutorial-shell"; type QualityPreset = "low" | "medium" | "high"; diff --git a/website/src/lib/changelog-runner.ts b/website/src/lib/changelog-runner.ts index daa15720..e598cba3 100644 --- a/website/src/lib/changelog-runner.ts +++ b/website/src/lib/changelog-runner.ts @@ -10,8 +10,8 @@ import { MOUSE_ENABLE, RESET, fg, -} from "mouseterm-lib/lib/ansi"; -import type { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +} from "dormouse-lib/lib/ansi"; +import type { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import type { InteractiveProgram } from "./tutorial-shell"; import changelogData from "../data/changelog.json"; @@ -307,7 +307,7 @@ export class ChangelogRunner implements InteractiveProgram { const detailLines = this.getDetailLines(); let frame = `${CURSOR_HOME}${CLEAR_SCREEN}`; - frame += `${BOLD}MouseTerm changelog${RESET} ${DIM}${RELEASES.length} releases ยท \`q\` to quit ยท โ†‘โ†“ select ยท wheel scrolls${RESET}\r\n`; + frame += `${BOLD}Dormouse changelog${RESET} ${DIM}${RELEASES.length} releases ยท \`q\` to quit ยท โ†‘โ†“ select ยท wheel scrolls${RESET}\r\n`; frame += "\r\n"; for (let r = 0; r < bodyH; r++) { diff --git a/website/src/lib/playground-shells.test.ts b/website/src/lib/playground-shells.test.ts index 61e691da..5a99aaf9 100644 --- a/website/src/lib/playground-shells.test.ts +++ b/website/src/lib/playground-shells.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +import { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import { PlaygroundShellRegistry } from "./playground-shells"; import type { InteractiveProgram } from "./tutorial-shell"; diff --git a/website/src/lib/playground-shells.ts b/website/src/lib/playground-shells.ts index 6708b767..c39d5217 100644 --- a/website/src/lib/playground-shells.ts +++ b/website/src/lib/playground-shells.ts @@ -1,4 +1,4 @@ -import type { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +import type { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import { TutorialShell, type InteractiveProgram } from "./tutorial-shell"; export type StartPlaygroundProgram = ( diff --git a/website/src/lib/tut-detector.test.ts b/website/src/lib/tut-detector.test.ts index 87be501b..9cdcc5a8 100644 --- a/website/src/lib/tut-detector.test.ts +++ b/website/src/lib/tut-detector.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import { DEFAULT_MOUSE_SELECTION_STATE, type MouseSelectionState } from "mouseterm-lib/lib/mouse-selection"; -import type { ActivityState } from "mouseterm-lib/lib/terminal-registry"; +import { DEFAULT_MOUSE_SELECTION_STATE, type MouseSelectionState } from "dormouse-lib/lib/mouse-selection"; +import type { ActivityState } from "dormouse-lib/lib/terminal-registry"; import { TutDetector } from "./tut-detector"; import { TutorialState } from "./tutorial-state"; diff --git a/website/src/lib/tut-detector.ts b/website/src/lib/tut-detector.ts index fbbc54e7..2ab87d93 100644 --- a/website/src/lib/tut-detector.ts +++ b/website/src/lib/tut-detector.ts @@ -1,4 +1,4 @@ -import { DEFAULT_MOUSE_SELECTION_STATE } from "mouseterm-lib/lib/mouse-selection"; +import { DEFAULT_MOUSE_SELECTION_STATE } from "dormouse-lib/lib/mouse-selection"; import type { TutorialState } from "./tutorial-state"; interface DockviewApi { @@ -7,10 +7,10 @@ interface DockviewApi { listener: (panel: { id?: string } | undefined) => void, ) => { dispose: () => void }; } -type WallEvent = import("mouseterm-lib/components/Wall").WallEvent; -type WallMode = import("mouseterm-lib/components/Wall").WallMode; -type ActivityState = import("mouseterm-lib/lib/terminal-registry").ActivityState; -type MouseSelectionState = import("mouseterm-lib/lib/mouse-selection").MouseSelectionState; +type WallEvent = import("dormouse-lib/components/Wall").WallEvent; +type WallMode = import("dormouse-lib/components/Wall").WallMode; +type ActivityState = import("dormouse-lib/lib/terminal-registry").ActivityState; +type MouseSelectionState = import("dormouse-lib/lib/mouse-selection").MouseSelectionState; interface ActivityStoreModule { subscribeToActivity: (listener: () => void) => () => void; diff --git a/website/src/lib/tut-items.ts b/website/src/lib/tut-items.ts index 8931f75f..a1bc36fb 100644 --- a/website/src/lib/tut-items.ts +++ b/website/src/lib/tut-items.ts @@ -1,4 +1,4 @@ -import { cfg } from "mouseterm-lib/cfg"; +import { cfg } from "dormouse-lib/cfg"; const USER_ATTENTION_SECS = Math.round(cfg.alert.userAttention / 1000); @@ -99,7 +99,7 @@ export const SECTIONS: readonly Section[] = [ id: 'al-ring', title: 'Bell rings when the task completes', hint: - `Don't type! If you type, MouseTerm will think you are paying attention to this task and the bell will not ring. The bell only rings if (a) the pane is not selected or (b) you have not interacted with the pane for the past ${USER_ATTENTION_SECS} seconds.`, + `Don't type! If you type, Dormouse will think you are paying attention to this task and the bell will not ring. The bell only rings if (a) the pane is not selected or (b) you have not interacted with the pane for the past ${USER_ATTENTION_SECS} seconds.`, }, { id: 'al-todo-auto', @@ -145,7 +145,7 @@ export const SECTIONS: readonly Section[] = [ }, ], prose: [ - 'Some terminal programs trap the cursor, and some do not. This tutorial pane does not trap the cursor, so MouseTerm does not show a cursor icon. The `ascii-splash` and `changelog` programs trap the cursor โ€” that is how they are able to respond to mouse movement. `lazygit` is an excellent and popular program which traps the cursor.', + 'Some terminal programs trap the cursor, and some do not. This tutorial pane does not trap the cursor, so Dormouse does not show a cursor icon. The `ascii-splash` and `changelog` programs trap the cursor โ€” that is how they are able to respond to mouse movement. `lazygit` is an excellent and popular program which traps the cursor.', ], }, ]; diff --git a/website/src/lib/tut-runner.test.ts b/website/src/lib/tut-runner.test.ts index 60041939..7f583530 100644 --- a/website/src/lib/tut-runner.test.ts +++ b/website/src/lib/tut-runner.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +import { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import { SECTIONS, type ItemId } from "./tut-items"; import { TutRunner } from "./tut-runner"; import { TutorialState } from "./tutorial-state"; @@ -83,7 +83,7 @@ describe("TutRunner snapshots", () => { sendKeys("\r"); sendKeys("q"); - expect(lastFrame()).toContain("MouseTerm Playground Tutorial"); + expect(lastFrame()).toContain("Dormouse Playground Tutorial"); expect(exitCount()).toBe(0); sendKeys("q"); diff --git a/website/src/lib/tut-runner.ts b/website/src/lib/tut-runner.ts index 4fa081df..aef9e240 100644 --- a/website/src/lib/tut-runner.ts +++ b/website/src/lib/tut-runner.ts @@ -9,9 +9,9 @@ import { LEAVE_ALT_SCREEN, RESET, fg, -} from "mouseterm-lib/lib/ansi"; -import { cfg } from "mouseterm-lib/cfg"; -import type { FakePtyAdapter } from "mouseterm-lib/lib/platform/fake-adapter"; +} from "dormouse-lib/lib/ansi"; +import { cfg } from "dormouse-lib/cfg"; +import type { FakePtyAdapter } from "dormouse-lib/lib/platform/fake-adapter"; import type { InteractiveProgram } from "./tutorial-shell"; import { SECTIONS, type Item } from "./tut-items"; import type { TutorialState } from "./tutorial-state"; @@ -319,7 +319,7 @@ export class TutRunner implements InteractiveProgram { const total = this.state.totalProgress(); const lines: string[] = []; lines.push(""); - lines.push(` ${BOLD}MouseTerm Playground Tutorial${RESET}`); + lines.push(` ${BOLD}Dormouse Playground Tutorial${RESET}`); lines.push( ` ${DIM}${total.done}/${total.total} complete ยท \`Esc\`/\`q\` to exit ยท \`Enter\` to open ยท \`โ†‘โ†“\` to navigate${RESET}`, ); diff --git a/website/src/lib/tutorial-shell.ts b/website/src/lib/tutorial-shell.ts index c596385b..8caa1fec 100644 --- a/website/src/lib/tutorial-shell.ts +++ b/website/src/lib/tutorial-shell.ts @@ -1,4 +1,4 @@ -import { CLEAR_LINE, PROMPT, RESET, fg } from 'mouseterm-lib/lib/ansi'; +import { CLEAR_LINE, PROMPT, RESET, fg } from 'dormouse-lib/lib/ansi'; export type SendOutput = (data: string) => void; diff --git a/website/src/lib/tutorial-state.ts b/website/src/lib/tutorial-state.ts index a52ee542..210f4557 100644 --- a/website/src/lib/tutorial-state.ts +++ b/website/src/lib/tutorial-state.ts @@ -1,6 +1,6 @@ import { ALL_ITEM_IDS, ITEM_IDS, SECTIONS, type ItemId } from "./tut-items"; -const STORAGE_KEY = "mouseterm-tut-v3"; +const STORAGE_KEY = "dormouse-tut-v3"; const KNOWN_IDS: ReadonlySet = new Set(ITEM_IDS); export class TutorialState { diff --git a/website/src/pages/Changelog.tsx b/website/src/pages/Changelog.tsx index a56a93c2..1e5fb39d 100644 --- a/website/src/pages/Changelog.tsx +++ b/website/src/pages/Changelog.tsx @@ -131,7 +131,7 @@ function ReleaseArticle({ release }: { release: ChangelogRelease }) { ) : null}

- Release notes for MouseTerm. + Release notes for Dormouse.

diff --git a/website/src/pages/Dependencies.tsx b/website/src/pages/Dependencies.tsx index 3814cbf7..33751501 100644 --- a/website/src/pages/Dependencies.tsx +++ b/website/src/pages/Dependencies.tsx @@ -12,7 +12,7 @@ export function Component() { Dependencies

- MouseTerm (standalone app and VS Code plugin) has {deps.length} transitive dependencies. Thank you to every author and contributor. + Dormouse (standalone app and VS Code plugin) has {deps.length} transitive dependencies. Thank you to every author and contributor.

Thanks also to ascii-splash and react-router and their transitive dependencies, which we use for this marketing page but are not part of the end-user application. diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 92bafdca..acd9d99e 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -20,7 +20,7 @@ import visualStudioIconUrl from "../assets/visual-studio-icon.svg"; import tinyIconUrl from "../assets/icon-tiny-dark.png"; import phoneMockupUrl from "../assets/phone-mockup.webp"; import standaloneLatest from "@standalone-latest"; -import { prefersReducedMotion } from "mouseterm-lib/lib/ui-geometry"; +import { prefersReducedMotion } from "dormouse-lib/lib/ui-geometry"; import { NotifySignupForm } from "../components/NotifySignupForm"; export { Home as Component }; @@ -34,10 +34,17 @@ const RUNWAY_VH = 300 * HERO_SLOMO_FACTOR; /** Scroll thresholds within the pinned runway (0โ€“1) */ const ICON_INITIAL_HIDE_FRAC = 0.67; // Fraction of icon's rendered height hidden at load โ€” leaves top third visible -const HOOK_FADE_REMAINING = 0.10; // Hook begins fading when bottom 10% of icon enters viewport +const HOOK_CROSSFADE_START = 0.05; +const HOOK_CROSSFADE_DURATION = 0.08; const WORD_THRESHOLDS = [0.25, 0.40, 0.55] as const; const FOOTNOTE_THRESHOLD = 0.65; const HEADER_REVEAL_LEAD = 0.04; +/** Runway fractions over which the dormouse line fades out. The dormouse + * line fades IN crossfaded with lines 1+2 (shared hookFadeProgress), + * then keeps carrying the brand alone until "Multitasking" pops in at + * WORD_THRESHOLDS[0] โ€” this range governs that final exit. */ +const DORMOUSE_LINE_FADE_OUT_START = 0.17; +const DORMOUSE_LINE_FADE_OUT_END = WORD_THRESHOLDS[0]; /** Fraction of runway where the hero text unpins and scrolls away (0โ€“1). * The video keeps scrubbing underneath. */ @@ -56,6 +63,12 @@ const SECTION_PY = "py-8"; /** Clamp a value to 0โ€“1. */ const clamp01 = (v: number) => Math.min(1, Math.max(0, v)); +/** Snap a 0..1 progress to crisp 0/1 endpoints when within `eps`. The scroll + * smoother settles asymptotically, so opacity values can land at e.g. 0.004 + * near a threshold โ€” visually a ghost, and worse, it disables the browser's + * `opacity: 0` compositor fast-path. Snapping makes the endpoints exact. */ +const snapProgress = (p: number, eps = 0.005): number => + p < eps ? 0 : p > 1 - eps ? 1 : p; const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; const downloadAccentStyle = { @@ -129,15 +142,15 @@ const INSTALL_STEPS: Record(null); const headerBrandRef = useRef(null); const hookRef = useRef(null); + const dormouseLineRef = useRef(null); const contentRef = useRef(null); const [installGuide, setInstallGuide] = useState(null); const [heroVideoSrc, setHeroVideoSrc] = useState(); @@ -497,21 +511,21 @@ function Home() { const progress = clamp01( (fraction - WORD_THRESHOLDS[i]) / 0.08 ); - el.style.opacity = String(progress); + el.style.opacity = String(snapProgress(progress)); el.style.transform = `translateY(${(1 - progress) * 12}px)`; } // Footnote - const footnoteProgress = clamp01( + const footnoteProgress = snapProgress(clamp01( (fraction - FOOTNOTE_THRESHOLD) / 0.08 - ); + )); if (footnoteRef.current) footnoteRef.current.style.opacity = String(footnoteProgress * 0.7); // Header: reveal brand + background just before the tmux-shortcuts // footnote appears, so it reads as dark once the line is visible. - const headerProgress = clamp01( + const headerProgress = snapProgress(clamp01( (fraction - (FOOTNOTE_THRESHOLD - HEADER_REVEAL_LEAD)) / HEADER_REVEAL_LEAD - ); + )); if (headerBrandRef.current) { headerBrandRef.current.style.opacity = String(headerProgress); } @@ -536,14 +550,31 @@ function Home() { video.style.transform = mediaTransform; poster.style.transform = mediaTransform; - // Hook text: visible until the icon nearly finishes rising, then fades out. + // Hook text: visible on load, then fades out early in the runway. + // hookFadeProgress: 0 = fully visible, 1 = fully gone. Shared with the + // dormouse line so its fade-in crossfades exactly with this fade-out. + const hookFadeProgress = clamp01( + (fraction - HOOK_CROSSFADE_START) / HOOK_CROSSFADE_DURATION + ); if (hookRef.current) { - const remainingHidden = iconHeight > 0 ? iconCurrentOffset / iconHeight : 0; - const fadeProgress = iconCurrentOffset === 0 - ? 1 - : clamp01(1 - remainingHidden / HOOK_FADE_REMAINING); - hookRef.current.style.opacity = String(1 - fadeProgress); - hookRef.current.style.transform = `translateY(${-fadeProgress * 24}px)`; + hookRef.current.style.opacity = String(snapProgress(1 - hookFadeProgress)); + hookRef.current.style.transform = `translateY(${-hookFadeProgress * 24}px)`; + } + + // Dormouse line ("A dormouse knows when to wake."): fades in exactly as + // lines 1+2 are leaving (shared hookFadeProgress = a true crossfade), + // holds while the icon settles, then fades out before "Multitasking" pops in. + if (dormouseLineRef.current) { + const fadeIn = hookFadeProgress; + const fadeOut = clamp01( + (fraction - DORMOUSE_LINE_FADE_OUT_START) + / (DORMOUSE_LINE_FADE_OUT_END - DORMOUSE_LINE_FADE_OUT_START) + ); + dormouseLineRef.current.style.opacity = String(snapProgress(fadeIn * (1 - fadeOut))); + // (1-fadeIn) lifts it into place from below as it appears; fadeOut + // lifts it further out as it leaves, matching the hook's exit motion. + const translateY = (1 - fadeIn) * 8 + fadeOut * -20; + dormouseLineRef.current.style.transform = `translateY(${translateY.toFixed(2)}px)`; } // Hero: cap so it stops at unstick (fraction = 1); natural scroll takes over. @@ -704,13 +735,22 @@ function Home() { {/* โ”€โ”€ Pinned scroll runway: hero text overlay โ”€โ”€ */}

- {/* Hook copy โ€” visible on load, fades out on first scroll */} -
- Too many terminals. - Not enough focus. + {/* Hook copy โ€” lines 1+2 visible on load and fade out as the icon + nears the top. The dormouse line is a sibling, not a child of + hookRef, so its opacity is driven independently and it can + linger after lines 1+2 are gone. */} +
+
+ So many terminals. + Which ones need attention? +
+ + A dormouse knows when to wake. +
{/* Hero words โ€” crossfade in place with the hook, just below the header */}
@@ -718,10 +758,10 @@ function Home() { Multitasking - Terminal + terminal - for Mice + for mice

-

-

Stop watching terminals spin

-

- MouseTerm tracks activity the same way you do โ€” visual motion. When a - pane stops changing for two seconds, it marks the task complete and - alerts you. -

-

- Works with any CLI tool that prints to a terminal โ€” no plugins, no - configuration. -

- + {/* Section 1: narrow text over a full-width video โ€” lead with the tmux story */} +
+
+

Soft as a mouse, sharp as a tmux

+

+ Upgrade your VS Code or native terminal with a flexible multipane + layout. Sleep the tasks you're not watching down to a compact + status indicator. +

+

+ Do it all with the mouse, or keep your hands on the keyboard with + tmux keybinds. +

+
+
- {/* Section 2: text left, image right */} -
-
-

Copy paste like you meant

+ {/* Section 2: image left, text right */} +
+ +
+

Stop watching terminals spin

- Click and drag in a "mouse conformant" terminal doesn't select text; - it sends escape code{" "} - {"\\e[<0;x;yM"}. - And Ctrl+C{" "} - doesn't copy; it asks your program to kill itself. + Dormouse tracks activity the same way you do โ€” visual motion. When a + pane stops changing for two seconds, it marks the task complete and + alerts you.

- MouseTerm lets you copy paste like a human, not a terminal. + Works with any CLI tool that prints to a terminal โ€” no plugins, no + configuration. Also supports{" "} + BEL{" "} + and{" "} + OSC 9/99/777{" "} + for native TUI integration.

-
- {/* Section 3: image left, text right */} -
- -
-

Soft as a mouse, sharp as tmux

+ {/* Section 3: text left, image right */} +
+
+

Newlines and copy paste like you meant

- Run builds, agents, servers, and scripts side by side. Minimize the - ones you're not watching to a compact status indicator. Every pane - keeps running and every alert still fires whether you can see it or - not. + You're used to{" "} + Shift+Enter{" "} + for a newline in the browser โ€” but it's broken in your terminal? + Not anymore. Dormouse works the way you'd expect, no arcane + terminal knowledge required.

- Do it all with the mouse, or keep your hands on the keyboard with - tmux keybinds. + Click and drag in a "mouse conformant" terminal doesn't select text; + it sends escape code{" "} + {"\\e[<0;x;yM"}. + Dormouse lets you copy-paste like a human, not a terminal.

+
-

Get MouseTerm

-

The multitasking terminal for mice.

+

Get Dormouse

+

A dormouse knows when to wake up. Multitasking terminal for mice.

} @@ -803,7 +852,7 @@ function Home() {

Also works in Cursor, Windsurf, Antigravity, or any other VS Code fork.

} peek="marketplace" variant="wide" @@ -811,7 +860,7 @@ function Home() { Visual Studio Marketplace } peek="openVsx" variant="wide" @@ -822,7 +871,7 @@ function Home() {
}>Standalone App -

Don't settle for your operating system's built-in terminal, get a nice one!

+

Don't settle for your operating system's built-in terminal. Get a nice one.

} peek="other" variant="compact" @@ -884,7 +933,7 @@ function Home() {
MouseTerm Tether running on a phone
@@ -892,13 +941,13 @@ function Home() { Walk away. Keep going.

- Coming next: Tether. Pair a - terminal session to your phone over WebRTC and take a stroll, the MouseTerm alert - system will buzz you if there's anything to do. A hosted auto-pairing service comes - later โ€” just leave and keep working, no "I'm walking away" dance. + Coming next: Dormouse Pocket. + Tether a terminal session to your phone over WebRTC and take a stroll โ€” Dormouse + buzzes your phone when something needs attention. A hosted auto-pairing service comes + later, so you can close the laptop and walk away, no setup dance.

- Open source and free to self-host, or pay us a little bit and you can use ours. We'll discount for early adopters, so don't miss out! + Open source and free to self-host, or pay a small monthly fee for our hosted version. Early adopters get a launch discount.

@@ -907,7 +956,7 @@ function Home() {