@@ -892,10 +892,10 @@ 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 — the Dormouse
+ alert system buzzes you if there's anything to do. A hosted auto-pairing service comes
+ later, so you can just leave and keep working, no "I'm walking away" 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!
From 5b299e249f227c38e15842251b3f6459af99c039 Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 16:50:10 -0700
Subject: [PATCH 02/24] =?UTF-8?q?Rename=20Tether=20page/route=20=E2=86=92?=
=?UTF-8?q?=20Pocket?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- pages/Tether.tsx → pages/Pocket.tsx
- Route /tether → /pocket
- All internal symbols (Tether* → Pocket*, TETHER_* → POCKET_*) and
CSS body classes (tether-marketing-body → pocket-marketing-body,
tether-terminal-body → pocket-terminal-body)
- Share title, aria-label, and page body updated to "Dormouse Pocket"
- "Tether" kept as the verb in body copy
("Tether a terminal session to your phone…")
Co-Authored-By: Claude Opus 4.7 (1M context)
---
website/src/App.tsx | 4 +-
website/src/index.css | 8 +--
website/src/pages/{Tether.tsx => Pocket.tsx} | 60 ++++++++++----------
3 files changed, 36 insertions(+), 36 deletions(-)
rename website/src/pages/{Tether.tsx => Pocket.tsx} (86%)
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/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/pages/Tether.tsx b/website/src/pages/Pocket.tsx
similarity index 86%
rename from website/src/pages/Tether.tsx
rename to website/src/pages/Pocket.tsx
index d3c58b8e..80e58229 100644
--- a/website/src/pages/Tether.tsx
+++ b/website/src/pages/Pocket.tsx
@@ -16,13 +16,13 @@ import { TutorialState } from "../lib/tutorial-state";
import { BUSY_DEMO_DURATION_MS, BUSY_DEMO_INTERVAL_MS, TutRunner } from "../lib/tut-runner";
import { ChangelogRunner } from "../lib/changelog-runner";
-export { Tether as Component };
+export { Pocket as Component };
type FakePtyAdapter = import("mouseterm-lib/lib/platform/fake-adapter").FakePtyAdapter;
-const TETHER_PANE = "tether-ascii-splash";
-const TETHER_THEME_ID = "vscode.theme-kimbie-dark.kimbie-dark";
-const TETHER_SESSIONS: MobileWallSession[] = [{ id: TETHER_PANE, title: "ascii-splash" }];
+const POCKET_PANE = "pocket-ascii-splash";
+const POCKET_THEME_ID = "vscode.theme-kimbie-dark.kimbie-dark";
+const POCKET_SESSIONS: MobileWallSession[] = [{ id: POCKET_PANE, title: "ascii-splash" }];
function useIsMobileViewport() {
const [isMobile, setIsMobile] = useState(false);
@@ -38,32 +38,32 @@ function useIsMobileViewport() {
return isMobile;
}
-function useTetherTheme() {
+function usePocketTheme() {
const restoredRef = useRef(false);
if (!restoredRef.current) {
- restoreActiveTheme(TETHER_THEME_ID);
+ restoreActiveTheme(POCKET_THEME_ID);
restoredRef.current = true;
}
}
-function TetherTerminalExperience({
+function PocketTerminalExperience({
interactive,
fillViewport = false,
}: {
interactive: boolean;
fillViewport?: boolean;
}) {
- useTetherTheme();
+ usePocketTheme();
const [terminalReady, setTerminalReady] = useState(false);
const adapterRef = useRef(null);
const shellRegistryRef = useRef(null);
const autoStartedRef = useRef>(new Set());
const spawnUnsubRef = useRef<(() => void) | null>(null);
const busyDemoDisposeRef = useRef<(() => void) | null>(null);
- const [activePaneId, setActivePaneId] = useState(TETHER_PANE);
+ const [activePaneId, setActivePaneId] = useState(POCKET_PANE);
const [touchMode, setTouchMode] = useState("gestures");
const [keyboardMode, setKeyboardMode] = useState("type");
- const sessionItems = useMobileWallSessionItems(TETHER_SESSIONS, activePaneId);
+ const sessionItems = useMobileWallSessionItems(POCKET_SESSIONS, activePaneId);
const mouseStates = useSyncExternalStore(
subscribeToMouseSelection,
getMouseSelectionSnapshot,
@@ -74,7 +74,7 @@ function TetherTerminalExperience({
&& activeMouseState.mouseReporting !== "none";
const tryAutoStart = useCallback((id: string) => {
- if (id !== TETHER_PANE) return;
+ if (id !== POCKET_PANE) return;
if (autoStartedRef.current.has(id)) return;
const shellRegistry = shellRegistryRef.current;
if (!shellRegistry) return;
@@ -99,7 +99,7 @@ function TetherTerminalExperience({
registry.initAlertStateReceiver();
adapterRef.current = adapter;
adapter.setDefaultScenario(scenarios.SCENARIO_SHELL_PROMPT);
- adapter.setScenario(TETHER_PANE, { name: "none", chunks: [] });
+ adapter.setScenario(POCKET_PANE, { name: "none", chunks: [] });
const tutorialState = new TutorialState();
const shellRegistry = new PlaygroundShellRegistry(
@@ -137,13 +137,13 @@ function TetherTerminalExperience({
},
);
shellRegistryRef.current = shellRegistry;
- shellRegistry.ensureShell(TETHER_PANE);
+ shellRegistry.ensureShell(POCKET_PANE);
spawnUnsubRef.current = adapter.onPtySpawn(({ id }) => {
shellRegistry.ensureShell(id);
tryAutoStart(id);
});
- if (adapter.hasPty(TETHER_PANE)) tryAutoStart(TETHER_PANE);
+ if (adapter.hasPty(POCKET_PANE)) tryAutoStart(POCKET_PANE);
setTerminalReady(true);
}
@@ -177,7 +177,7 @@ function TetherTerminalExperience({
terminal={
terminalReady ? (
setKeyboardMode("sessions")}
@@ -202,12 +202,12 @@ function TetherTerminalExperience({
);
}
-function MobileTetherPage() {
+function MobilePocketPage() {
return (
-
+
-
+
);
@@ -220,7 +220,7 @@ function ShareUrlButton() {
const url = window.location.href;
if (navigator.share) {
try {
- await navigator.share({ url, title: "MouseTerm Tether" });
+ await navigator.share({ url, title: "Dormouse Pocket" });
return;
} catch (err) {
if ((err as DOMException)?.name === "AbortError") return;
@@ -248,13 +248,13 @@ function ShareUrlButton() {
);
}
-function DesktopTetherPage() {
+function DesktopPocketPage() {
return (
}
+ controls={
}
/>
@@ -267,9 +267,9 @@ function DesktopTetherPage() {
to try it out! (WIP)
- 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.
+ Tether a terminal session to your phone over WebRTC and take a stroll — the Dormouse
+ alert system buzzes you if there's anything to do. A hosted auto-pairing service comes
+ later, so you can just leave and keep working, no "I'm walking away" 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!
@@ -277,12 +277,12 @@ function DesktopTetherPage() {
-
+
@@ -292,14 +292,14 @@ function DesktopTetherPage() {
);
}
-function Tether() {
+function Pocket() {
const isMobile = useIsMobileViewport();
useEffect(() => {
- const className = isMobile ? "tether-terminal-body" : "tether-marketing-body";
+ const className = isMobile ? "pocket-terminal-body" : "pocket-marketing-body";
document.body.classList.add(className);
return () => document.body.classList.remove(className);
}, [isMobile]);
- return isMobile ? : ;
+ return isMobile ? : ;
}
From ecd59a1c548dc7b7f37477db3980ec9426ec3d8b Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 16:56:36 -0700
Subject: [PATCH 03/24] Rebrand VS Code extension to Dormouse
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
vscode-ext/package.json:
- name: mouseterm → dormouse
- displayName: "Dormouse — Terminal Multiplexer"
(descriptive suffix for marketplace SEO; brand is "Dormouse" elsewhere)
- description rewritten with persistence + alert framing
- homepage → dormouse.sh
- All command IDs mouseterm.* → dormouse.*
- Command titles "MouseTerm: …" → "Dormouse: …"
- View container + view IDs (mouseterm-panel/view → dormouse-panel/view)
- activationEvents updated
- Keywords: added "dormouse" and "persistent"
- vsix output filename + dogfood script names
vscode-ext/src/*.ts:
- MouseTermViewProvider → DormouseViewProvider
- All command registrations and the executeCommand("…view.focus")
call updated to dormouse.* IDs
- Webview panel serializer + createWebviewPanel viewType: mouseterm
→ dormouse
- Warning messages "MouseTerm:" → "Dormouse:"
- Output channel name → "Dormouse"
- Storage keys: mouseterm.session, mouseterm.selectedShellPath →
dormouse.session, dormouse.selectedShellPath (userbase=0, no migration)
Cross-cutting message-type rename (extension ↔ webview wire protocol):
- All `mouseterm:*` ExtensionMessage / WebviewMessage discriminators
→ `dormouse:*` (init, saveState, flushSessionSave[Done], newTerminal,
selectedShell, openThemeDebugger)
- Lib side: vscode-adapter.ts (+ test), Wall.tsx window event
("mouseterm:new-terminal" → "dormouse:new-terminal"), ThemeDebugger
OPEN_THEME_DEBUGGER_EVENT constant
Lib-side miscellany:
- themes/store.ts localStorage keys
("mouseterm:installed-themes/active-theme" → "dormouse:…")
- ansi.ts playground PROMPT visual: user@mouseterm → user@dormouse
- KillModal story content + Playground.tsx comment matched
- shell-defaults.ts comment
Internal TS type `MouseTermTheme` left as-is — purely internal, 27-site
cascade, no user-facing impact. Separate refactor.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
lib/src/components/ThemeDebugger.tsx | 2 +-
lib/src/components/Wall.tsx | 4 +-
lib/src/lib/ansi.ts | 6 +--
lib/src/lib/platform/vscode-adapter.test.ts | 4 +-
lib/src/lib/platform/vscode-adapter.ts | 18 +++----
lib/src/lib/shell-defaults.ts | 2 +-
lib/src/lib/themes/store.ts | 4 +-
lib/src/stories/KillModal.stories.tsx | 2 +-
vscode-ext/package.json | 56 +++++++++++----------
vscode-ext/src/extension.ts | 46 ++++++++---------
vscode-ext/src/log.ts | 2 +-
vscode-ext/src/message-router.ts | 14 +++---
vscode-ext/src/message-types.ts | 14 +++---
vscode-ext/src/session-state.ts | 2 +-
vscode-ext/src/shell-selection.ts | 2 +-
vscode-ext/src/webview-view-provider.ts | 4 +-
website/src/pages/Playground.tsx | 2 +-
17 files changed, 93 insertions(+), 91 deletions(-)
diff --git a/lib/src/components/ThemeDebugger.tsx b/lib/src/components/ThemeDebugger.tsx
index 9e9f5352..c939c893 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));
diff --git a/lib/src/components/Wall.tsx b/lib/src/components/Wall.tsx
index 584dc849..edd07c1d 100644
--- a/lib/src/components/Wall.tsx
+++ b/lib/src/components/Wall.tsx
@@ -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((
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/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/themes/store.ts b/lib/src/lib/themes/store.ts
index a45732d7..f6cf95e3 100644
--- a/lib/src/lib/themes/store.ts
+++ b/lib/src/lib/themes/store.ts
@@ -3,8 +3,8 @@ import type { MouseTermTheme } from './types';
import _bundledThemes from './bundled.json';
const bundledThemes = _bundledThemes as unknown as MouseTermTheme[];
-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;
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/vscode-ext/package.json b/vscode-ext/package.json
index 9d0aabd1..b313f6a6 100644
--- a/vscode-ext/package.json
+++ b/vscode-ext/package.json
@@ -1,12 +1,12 @@
{
- "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"
@@ -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/src/pages/Playground.tsx b/website/src/pages/Playground.tsx
index 8ea587aa..9c83dfdb 100644
--- a/website/src/pages/Playground.tsx
+++ b/website/src/pages/Playground.tsx
@@ -84,7 +84,7 @@ function Playground() {
adapter.setDefaultScenario(scenarios.SCENARIO_SHELL_PROMPT);
// Each runner-owned pane suppresses the default shell-prompt scenario,
- // otherwise spawnPty queues a delayed `user@mouseterm:~$` write that
+ // otherwise spawnPty queues a delayed `user@dormouse:~$` write that
// would land in the runner's alt-screen and corrupt its output.
for (const pane of panes) {
adapter.setScenario(pane.id, { name: "none", chunks: [] });
From 7bed9eee858433e248e797913802a6766bf4cf8e Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:01:24 -0700
Subject: [PATCH 04/24] Rebrand standalone (Tauri) app to Dormouse
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
tauri.conf.json:
- productName: MouseTerm → Dormouse
(this is what produces Dormouse.app / Dormouse-windows-x64-setup.exe /
Dormouse-linux-x86_64.AppImage on next bundle build)
- identifier: com.mouseterm.standalone → sh.dormouse.standalone
(userbase=0, no migration concern)
- Window title: Dormouse
- Updater endpoint URL → dormouse.sh
Rust crate rename (Cargo.toml):
- [package] name: mouseterm → dormouse
- [package] description rewritten
- [lib] name: mouseterm_lib → dormouse_lib
- main.rs entry call: dormouse_lib::run()
- lib.rs: log dir / log file names (~/Library/Logs/Dormouse/dormouse.log,
%LOCALAPPDATA%/Dormouse/dormouse.log, /tmp/dormouse.log fallback)
- lib.rs: window event "mouseterm://files-dropped" →
"dormouse://files-dropped"
- lib.rs: panic message + temp-dir test prefix
- Test fixture paths (\\Users\\…\\Local\\Dormouse\\…)
Frontend / Tauri JS bridge:
- standalone/index.html
- AppBar.tsx: window event "mouseterm:new-terminal" →
"dormouse:new-terminal"
- tauri-adapter.ts: matching files-dropped listener, STATE_KEY
("mouseterm.session" → "dormouse.session")
- updater.ts: STORAGE_KEY ("dormouse:update-result"), changelog URL
(mouseterm.com → dormouse.sh); GitHub repo URL left as-is (repo not
renamed in this pass)
- updater.test.ts: matching STORAGE_KEY + changelog URL expectations
Capability description:
- standalone/src-tauri/capabilities/default.json
scripts/dogfood.sh:
- Install dirs ($LOCALAPPDATA/Dormouse, /Applications/Dormouse.app)
- Bundle filename patterns (Dormouse_*-setup.exe, Dormouse_*.dmg)
- Binary names ($RELEASE_DIR/dormouse, dormouse.exe)
- PowerShell process filter -Name dormouse,node
- The `pnpm --filter mouseterm-standalone` workspace filter stays —
npm package names are internal infrastructure, not renamed in this
pass
Internal npm/pnpm workspace package names left as-is (mouseterm-lib,
mouseterm-standalone, mouseterm-sidecar) — purely internal, no
user-facing impact; separate refactor.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
standalone/index.html | 2 +-
standalone/scripts/dogfood.sh | 26 +++++++++----------
standalone/src-tauri/Cargo.toml | 6 ++---
.../src-tauri/capabilities/default.json | 2 +-
standalone/src-tauri/src/lib.rs | 22 ++++++++--------
standalone/src-tauri/src/main.rs | 2 +-
standalone/src-tauri/tauri.conf.json | 8 +++---
standalone/src/AppBar.tsx | 2 +-
standalone/src/tauri-adapter.ts | 4 +--
standalone/src/updater.test.ts | 4 +--
standalone/src/updater.ts | 4 +--
11 files changed, 41 insertions(+), 41 deletions(-)
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/scripts/dogfood.sh b/standalone/scripts/dogfood.sh
index 49361a97..bcff5a66 100755
--- a/standalone/scripts/dogfood.sh
+++ b/standalone/scripts/dogfood.sh
@@ -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/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..62e24fa1 100644
--- a/standalone/src-tauri/src/lib.rs
+++ b/standalone/src-tauri/src/lib.rs
@@ -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()
);
@@ -606,7 +606,7 @@ pub fn run() {
.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/tauri-adapter.ts b/standalone/src/tauri-adapter.ts
index 34c0215f..5705620e 100644
--- a/standalone/src/tauri-adapter.ts
+++ b/standalone/src/tauri-adapter.ts
@@ -107,7 +107,7 @@ export class TauriAdapter implements PlatformAdapter {
// Inert while dragDropEnabled=false in tauri.conf.json. See diffplug/mouseterm#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..cb54144f 100644
--- a/standalone/src/updater.ts
+++ b/standalone/src/updater.ts
@@ -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 {
From 335165f67d030c74d000113c0cbff546ee04c814 Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:03:12 -0700
Subject: [PATCH 05/24] Update release pipeline for Dormouse artifact filenames
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
.github/workflows/release.yml:
- Upload path for inner binary: mouseterm.exe → dormouse.exe
- VS Code build pnpm filter: --filter mouseterm → --filter dormouse
(matches the new vscode-ext/package.json name)
- `--filter mouseterm-lib` workspace filter unchanged (internal pkg name)
scripts/sign-and-deploy.sh:
- FNAME_WIN/MAC/LINUX renamed: MouseTerm-* → Dormouse-*
- Windows inner-exe lookup accepts both Dormouse.exe / dormouse.exe
- Comment about AppleDouble extraction updated
scripts/bump-version.sh:
- Comment refs to the Cargo.lock `mouseterm` entry → `dormouse`
website/public/standalone-latest.json left unchanged: it points to the
already-published v0.9.1 release whose artifacts were uploaded as
MouseTerm-*. sign-and-deploy.sh regenerates this file on the next
release; the new entries will use Dormouse-*.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.github/workflows/release.yml | 6 +++---
scripts/bump-version.sh | 4 ++--
scripts/sign-and-deploy.sh | 10 +++++-----
3 files changed, 10 insertions(+), 10 deletions(-)
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ee9d3a61..52e4a133 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
@@ -112,10 +112,10 @@ jobs:
run: pnpm --filter mouseterm-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/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/sign-and-deploy.sh b/scripts/sign-and-deploy.sh
index df5daa61..5d2fb194 100755
--- a/scripts/sign-and-deploy.sh
+++ b/scripts/sign-and-deploy.sh
@@ -47,9 +47,9 @@ TSA_URL="http://ts.ssl.com"
GITHUB_REPO="diffplug/mouseterm"
# 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"
From 66d97407a7dcd93f2d519da30a7a94619e4fda8a Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:04:56 -0700
Subject: [PATCH 06/24] Rewrite README.md and vscode-ext/README.md for Dormouse
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Root README:
- Title and tagline reframed: "A dormouse knows when to wake up.
Multitasking terminal for mice (and hotkey wizards too)."
- Opening paragraph re-anchors with the homepage hook ("So many
terminals — which one needs attention?")
- Try-it links updated to dormouse.sh and the new marketplace IDs
- Added a "Pocket (coming soon)" bullet to the features list
- Folder structure description: website builds dormouse.sh
vscode-ext/README.md:
- Title and all body copy MouseTerm → Dormouse
- All URLs mouseterm.com → dormouse.sh
- Command palette entries: "Dormouse: Focus" / "Dormouse: Open in Editor"
GitHub repo URL still points at diffplug/mouseterm (not renamed in this
pass; GitHub redirects renamed repos so this is forward-compatible).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
README.md | 24 +++++++++++++-----------
vscode-ext/README.md | 34 +++++++++++++++++-----------------
2 files changed, 30 insertions(+), 28 deletions(-)
diff --git a/README.md b/README.md
index c253c524..9704cd4d 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,19 @@
-# MouseTerm
+# Dormouse
-**Multitasking terminal for the mouse, tmux-compatible.**
+**A dormouse knows when to wake up. Multitasking terminal for mice (and hotkey wizards too).**
-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.
+So many terminals — which one needs attention? Dormouse tracks activity the
+way you do: visual motion. When a pane stops changing for two seconds,
+it's marked done. Works with any CLI tool that prints to a terminal —
+no plugins, no configuration.
-
+
## 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
+- **[Demo videos and downloads](https://dormouse.sh)** — Mac, Windows, Linux
+- **[Marketplace](https://marketplace.visualstudio.com/items?itemName=diffplug.dormouse)** / **[Open VSX](https://open-vsx.org/extension/diffplug/dormouse)** — VS Code extension (also works in Cursor, Windsurf, Antigravity)
## Features
@@ -22,6 +23,7 @@ with any CLI tool, no plugins or config.
- **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 +51,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 +63,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/vscode-ext/README.md b/vscode-ext/README.md
index c5217ca5..86b8ccc2 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.
--
WATCHING disabled
--
WATCHING enabled
+-
alerts disabled
+-
alerts enabled
-
task is running, will send an alert when task completes
-
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`.
-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)
+- 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/mouseterm)
- Brought to you by [DiffPlug](https://www.diffplug.com/)
From b67707a3f87602d03958ef86e32589be375e99ee Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:10:49 -0700
Subject: [PATCH 07/24] Rename brand in internal docs, storybook, and remaining
identifiers
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Docs:
- AGENTS.md, DESIGN.md, PRODUCT.md: MouseTerm → Dormouse in prose
- docs/specs/*.md: prose + URLs (mouseterm.com → dormouse.sh) +
command IDs (mouseterm.* → dormouse.*) + message types
(mouseterm:* → dormouse:*) + view container IDs to match code
- docs/specs/deploy.md: example artifact filenames, log paths, env vars
- docs/specs/glossary.md, shortcuts.md: brand prose
- docs/specs/layout.md: `mousetermTheme` symbol → `dormouseTheme`
- pnpm filter `--filter mouseterm` (vscode-ext build) updated to
`--filter dormouse`. The `--filter mouseterm-lib` and
`--filter mouseterm-standalone` filters stay (internal pkg names).
Storybook fixtures:
- UpdateDebugDialog.stories.tsx: example error path /Applications/Dormouse.app
- Smoke.stories.tsx: "Dormouse Tokens" panel header / semantic-token caption
- MobileTerminalUi.stories.tsx: TETHER_WALL_* → POCKET_WALL_*,
story export name TetherWall → PocketWall, scenario id
Code-side identifier renames discovered during doc sweep:
- lib/src/components/Wall.tsx: `mousetermTheme` → `dormouseTheme`
- standalone/src-tauri/src/lib.rs: `MOUSETERM_LOG_FILE` env var
→ `DORMOUSE_LOG_FILE`
- website/src/lib/tutorial-state.ts: localStorage key
"mouseterm-tut-v3" → "dormouse-tut-v3" (userbase=0, no migration)
Left as-is:
- CHANGELOG.md historical entries (per "leave historical changelog" guidance)
- Internal npm workspace package names (mouseterm-lib, mouseterm-standalone,
mouseterm-sidecar, mouseterm-website) — purely build-tool plumbing
- github.com/diffplug/mouseterm URLs — repo not renamed in this pass
- .claude/commands/release-notes.md — agent self-modification guard
prevented edit; needs a manual MouseTerm → Dormouse pass
Co-Authored-By: Claude Opus 4.7 (1M context)
---
AGENTS.md | 6 +-
DESIGN.md | 10 +-
PRODUCT.md | 8 +-
docs/specs/OSC.md | 20 +-
docs/specs/alert.md | 1016 ++++-------------
docs/specs/auto-update.md | 8 +-
docs/specs/deploy.md | 34 +-
docs/specs/glossary.md | 2 +-
docs/specs/layout.md | 4 +-
docs/specs/mobile-ui.md | 8 +-
docs/specs/shortcuts.md | 4 +-
docs/specs/terminal-state.md | 2 +-
docs/specs/theme.md | 30 +-
docs/specs/transport.md | 22 +-
docs/specs/tutorial.md | 4 +-
docs/specs/vscode.md | 76 +-
lib/src/components/Wall.tsx | 4 +-
lib/src/stories/MobileTerminalUi.stories.tsx | 22 +-
lib/src/stories/Smoke.stories.tsx | 4 +-
lib/src/stories/UpdateDebugDialog.stories.tsx | 2 +-
standalone/src-tauri/src/lib.rs | 2 +-
website/src/lib/tutorial-state.ts | 2 +-
22 files changed, 362 insertions(+), 928 deletions(-)
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/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/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 ; ; 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 ; ; 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/`.
+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/`.
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": "",
- "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..73bed060 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
@@ -88,7 +88,7 @@ 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`
+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/mouseterm/releases/latest/download/Dormouse-windows-x64-setup.exe
+https://github.com/diffplug/mouseterm/releases/latest/download/Dormouse-macos-aarch64.tar.gz
+https://github.com/diffplug/mouseterm/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,22 +181,22 @@ 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/mouseterm/releases/download/v0.1.0/Dormouse-windows-x64-setup.exe",
"signature": ""
},
"darwin-aarch64": {
- "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-macos-aarch64.tar.gz",
+ "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/Dormouse-macos-aarch64.tar.gz",
"signature": ""
},
"linux-x86_64": {
- "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/MouseTerm-linux-x86_64.AppImage",
+ "url": "https://github.com/diffplug/mouseterm/releases/download/v0.1.0/Dormouse-linux-x86_64.AppImage",
"signature": ""
}
}
}
```
-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
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..3bdc97f1 100644
--- a/docs/specs/tutorial.md
+++ b/docs/specs/tutorial.md
@@ -1,6 +1,6 @@
# 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
@@ -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.
diff --git a/docs/specs/vscode.md b/docs/specs/vscode.md
index f0b9a067..1ce26b8d 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.
@@ -205,8 +205,8 @@ 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/,
+ 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
@@ -226,16 +226,16 @@ 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
+// Set when any Dormouse webview has focus
vscode.commands.executeCommand('setContext', 'mouseterm.active', true);
-// Set when MouseTerm is in passthrough/terminal mode (keys go to PTY)
+// Set when Dormouse is in passthrough/terminal mode (keys go to PTY)
vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'terminal');
-// Set when MouseTerm is in normal/navigation mode (keys go to MouseTerm UI)
+// Set when Dormouse is in normal/navigation mode (keys go to Dormouse UI)
vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'normal');
```
@@ -243,8 +243,8 @@ vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'normal');
| Command | Description |
|---------|-------------|
-| `mouseterm.focus` | Focus the MouseTerm panel view |
-| `mouseterm.newPane` | Split a new pane in MouseTerm |
+| `dormouse.focus` | Focus the Dormouse panel view |
+| `mouseterm.newPane` | Split a new pane in Dormouse |
| `mouseterm.closePane` | Close the focused pane |
| `mouseterm.nextPane` | Focus next pane |
| `mouseterm.prevPane` | Focus previous pane |
@@ -255,7 +255,7 @@ vscode.commands.executeCommand('setContext', 'mouseterm.mode', 'normal');
### Not yet implemented
-- `TerminalProfileProvider` not registered — MouseTerm doesn't appear in the terminal `+` dropdown
+- `TerminalProfileProvider` not registered — Dormouse 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`
- No status bar item showing active session count
diff --git a/lib/src/components/Wall.tsx b/lib/src/components/Wall.tsx
index edd07c1d..e94e6b60 100644
--- a/lib/src/components/Wall.tsx
+++ b/lib/src/components/Wall.tsx
@@ -84,7 +84,7 @@ export { TerminalPaneHeader } from './wall/TerminalPaneHeader';
// --- Theme ---
-const mousetermTheme: DockviewTheme = {
+const dormouseTheme: DockviewTheme = {
...themeAbyss,
name: 'mouseterm',
gap: 6,
@@ -745,7 +745,7 @@ export function Wall({
components={components}
tabComponents={tabComponents}
onReady={handleReady}
- theme={mousetermTheme}
+ theme={dormouseTheme}
singleTabMode="fullwidth"
/>
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/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/standalone/src-tauri/src/lib.rs b/standalone/src-tauri/src/lib.rs
index 62e24fa1..721b020c 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()
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 {
From a2865e1c6036ff0aadeb72cbf4c5c99aa0cff86c Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:13:04 -0700
Subject: [PATCH 08/24] Update root package.json pnpm filters for renamed
vscode-ext
build:vscode and dogfood:vscode used --filter mouseterm to match the
vscode-ext package name. That was renamed to "dormouse" in
vscode-ext/package.json; update the filters to match. Workspace
filters for mouseterm-lib, mouseterm-standalone, mouseterm-website,
mouseterm-root remain unchanged (internal package names).
Co-Authored-By: Claude Opus 4.7 (1M context)
---
package.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/package.json b/package.json
index d7d2e01a..af3e019d 100644
--- a/package.json
+++ b/package.json
@@ -8,10 +8,10 @@
"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:vscode": "pnpm --filter mouseterm-lib build && pnpm --filter dormouse build:frontend && pnpm --filter dormouse 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",
+ "dogfood:vscode": "pnpm run build:vscode && pnpm --filter dormouse dogfood",
"dogfood:standalone": "bash standalone/scripts/dogfood.sh",
"storybook": "pnpm --filter mouseterm-lib storybook",
"bundle-themes": "node lib/scripts/bundle-themes.mjs"
From 585f75f5d2b66aa91d80cb8dbab76d8a1eef07db Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Fri, 15 May 2026 17:32:09 -0700
Subject: [PATCH 09/24] =?UTF-8?q?Rename=20pnpm=20workspace=20packages:=20m?=
=?UTF-8?q?ouseterm-*=20=E2=86=92=20dormouse-*?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Workspace package names renamed (private packages, internal only —
no published npm-registry impact):
- mouseterm-root → dormouse-root (package.json)
- mouseterm-lib → dormouse-lib (lib/package.json)
- mouseterm-standalone → dormouse-standalone (standalone/package.json)
- mouseterm-website → dormouse-website (website/package.json)
- mouseterm-sidecar → dormouse-sidecar (standalone/sidecar/package.json)
Cascading updates:
Imports (TS/TSX): every `from "mouseterm-lib/…"` and dynamic
`import("mouseterm-lib/…")` → `from "dormouse-lib/…"` across lib,
standalone, and website source/test files.
Path / alias mappings:
- standalone/tsconfig.json, website/tsconfig.json, lib/tsconfig.app.json:
path mappings keyed on "mouseterm-lib/*" → "dormouse-lib/*"
- standalone/vite.config.ts, standalone/vitest.config.ts,
website/vite.config.ts: alias keys
- lib/.storybook/main.ts: any moduleNameMapper / alias
`workspace:*` deps:
- standalone/package.json + website/package.json: dependency entry
"mouseterm-lib" → "dormouse-lib"
pnpm `--filter` references:
- root package.json scripts (build/build:vscode/dev:lib/dev:website/
dev:standalone/build:standalone/build:website/storybook)
- .github/workflows/release.yml (--filter dormouse-lib test)
- standalone/scripts/dogfood.sh
- docs/specs/deploy.md, docs/specs/vscode.md, docs/specs/tutorial.md
- .claude/settings.json permission allowlist patterns
pnpm-lock.yaml regenerated via `pnpm install`.
Verified: all 514 tests pass (lib 460, website 38, standalone 16);
dev server serves /, /pocket, /playground with 200 OK and the build
output reports `dormouse-root@ dev:website` / `dormouse-website@0.1.0
predev`.
Co-Authored-By: Claude Opus 4.7 (1M context)
---
.claude/settings.json | 4 ++--
.github/workflows/release.yml | 2 +-
docs/specs/deploy.md | 6 +++---
docs/specs/tutorial.md | 6 +++---
docs/specs/vscode.md | 2 +-
lib/.storybook/main.ts | 2 +-
lib/package.json | 2 +-
lib/tsconfig.app.json | 2 +-
package.json | 18 ++++++++---------
pnpm-lock.yaml | 4 ++--
standalone/package.json | 4 ++--
standalone/scripts/dogfood.sh | 4 ++--
standalone/sidecar/package.json | 2 +-
standalone/src/main.tsx | 14 ++++++-------
standalone/src/tauri-adapter.ts | 8 ++++----
standalone/src/updater.ts | 2 +-
standalone/tsconfig.json | 2 +-
standalone/vite.config.ts | 2 +-
standalone/vitest.config.ts | 2 +-
website/package.json | 4 ++--
website/src/lib/ascii-splash-runner.test.ts | 2 +-
website/src/lib/ascii-splash-runner.ts | 4 ++--
website/src/lib/changelog-runner.ts | 4 ++--
website/src/lib/playground-shells.test.ts | 2 +-
website/src/lib/playground-shells.ts | 2 +-
website/src/lib/tut-detector.test.ts | 4 ++--
website/src/lib/tut-detector.ts | 10 +++++-----
website/src/lib/tut-items.ts | 2 +-
website/src/lib/tut-runner.test.ts | 2 +-
website/src/lib/tut-runner.ts | 6 +++---
website/src/lib/tutorial-shell.ts | 2 +-
website/src/pages/Home.tsx | 2 +-
website/src/pages/Playground.tsx | 18 ++++++++---------
website/src/pages/Pocket.tsx | 22 ++++++++++-----------
website/tsconfig.json | 2 +-
website/vite.config.ts | 2 +-
36 files changed, 89 insertions(+), 89 deletions(-)
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 52e4a133..9e4197f9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -109,7 +109,7 @@ 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 dormouse build:frontend
diff --git a/docs/specs/deploy.md b/docs/specs/deploy.md
index 73bed060..52c11953 100644
--- a/docs/specs/deploy.md
+++ b/docs/specs/deploy.md
@@ -87,7 +87,7 @@ 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`
+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
@@ -203,8 +203,8 @@ Note: the update manifest URLs include the version in the *path* (`/v0.1.0/`) bu
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/tutorial.md b/docs/specs/tutorial.md
index 3bdc97f1..133c6c54 100644
--- a/docs/specs/tutorial.md
+++ b/docs/specs/tutorial.md
@@ -7,7 +7,7 @@ At the `/playground` route on the website. Interactive TUI: each item starts pen
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`.
+- **`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 `mouseterm-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.
@@ -105,9 +105,9 @@ While the Copy paste section is open, pressing `p` toggles the **Place To Paste*
## 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 1ce26b8d..ff626839 100644
--- a/docs/specs/vscode.md
+++ b/docs/specs/vscode.md
@@ -204,7 +204,7 @@ connect-src ${webview.cspSource};
```
pnpm build:vscode =
- 1. pnpm --filter mouseterm-lib build (TypeScript compile)
+ 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)
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/package.json b/lib/package.json
index 83886588..c0b94091 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,
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 af3e019d..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 dormouse build:frontend && pnpm --filter dormouse build",
- "build:standalone": "pnpm --filter mouseterm-standalone tauri build",
- "build:website": "pnpm --filter mouseterm-website build",
+ "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..f5acbc51 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -104,7 +104,7 @@ importers:
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:
diff --git a/standalone/package.json b/standalone/package.json
index 3c0b8a56..5f1731e1 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",
@@ -18,7 +18,7 @@
"@xterm/addon-fit": "^0.11.0",
"@xterm/xterm": "^6.0.0",
"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 bcff5a66..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
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/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 5705620e..9c81583a 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) =>
diff --git a/standalone/src/updater.ts b/standalone/src/updater.ts
index cb54144f..fc455038 100644
--- a/standalone/src/updater.ts
+++ b/standalone/src/updater.ts
@@ -4,7 +4,7 @@ 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';
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/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/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..2e0fde56 100644
--- a/website/src/lib/ascii-splash-runner.ts
+++ b/website/src/lib/ascii-splash-runner.ts
@@ -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..622b0488 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";
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..4d060433 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);
diff --git a/website/src/lib/tut-runner.test.ts b/website/src/lib/tut-runner.test.ts
index 60041939..2a4178ca 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";
diff --git a/website/src/lib/tut-runner.ts b/website/src/lib/tut-runner.ts
index 4fa081df..e228fd1b 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";
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/pages/Home.tsx b/website/src/pages/Home.tsx
index f48e527f..f76cb056 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 };
diff --git a/website/src/pages/Playground.tsx b/website/src/pages/Playground.tsx
index 9c83dfdb..a00d4dfb 100644
--- a/website/src/pages/Playground.tsx
+++ b/website/src/pages/Playground.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback, useRef } from "react";
import SiteHeader from "../components/SiteHeader";
import { PlaceToPaste } from "../components/PlaceToPaste";
-import { ThemePicker } from "mouseterm-lib/components/ThemePicker";
+import { ThemePicker } from "dormouse-lib/components/ThemePicker";
import { PlaygroundShellRegistry } from "../lib/playground-shells";
import { TutorialState } from "../lib/tutorial-state";
import { TutDetector } from "../lib/tut-detector";
@@ -14,8 +14,8 @@ const PANE_MAIN = "tut-main";
const PANE_BOXED = "tut-boxed";
const PANE_SPLASH = "tut-splash";
-type FakePtyAdapter = import("mouseterm-lib/lib/platform/fake-adapter").FakePtyAdapter;
-type WallEvent = import("mouseterm-lib/components/Wall").WallEvent;
+type FakePtyAdapter = import("dormouse-lib/lib/platform/fake-adapter").FakePtyAdapter;
+type WallEvent = import("dormouse-lib/components/Wall").WallEvent;
type DockviewDisposable = { dispose: () => void };
// Tailwind's md breakpoint — matches the header's `md:top-20` so the pane
@@ -69,13 +69,13 @@ function Playground() {
{ id: PANE_SPLASH, command: "ascii-splash" },
];
async function loadWall() {
- const platform = await import("mouseterm-lib/lib/platform");
- const registry = await import("mouseterm-lib/lib/terminal-registry");
- const mouseSelection = await import("mouseterm-lib/lib/mouse-selection");
- const wall = await import("mouseterm-lib/components/Wall");
- const scenarios = await import("mouseterm-lib/lib/platform/fake-scenarios");
+ const platform = await import("dormouse-lib/lib/platform");
+ const registry = await import("dormouse-lib/lib/terminal-registry");
+ const mouseSelection = await import("dormouse-lib/lib/mouse-selection");
+ const wall = await import("dormouse-lib/components/Wall");
+ const scenarios = await import("dormouse-lib/lib/platform/fake-scenarios");
const asciiSplash = await import("../lib/ascii-splash-runner");
- await import("mouseterm-lib/index.css");
+ await import("dormouse-lib/index.css");
if (cancelled) return;
const adapter = platform.initPlatform("fake");
diff --git a/website/src/pages/Pocket.tsx b/website/src/pages/Pocket.tsx
index 80e58229..edfb3178 100644
--- a/website/src/pages/Pocket.tsx
+++ b/website/src/pages/Pocket.tsx
@@ -2,15 +2,15 @@ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "
import { ShareIcon } from "@phosphor-icons/react";
import SiteHeader, { STATIC_PAGE_HEADER_STYLE } from "../components/SiteHeader";
import { NotifySignupForm } from "../components/NotifySignupForm";
-import { MobileTerminalUi, type MobileTerminalKeyboardMode, type MobileTerminalTouchMode } from "mouseterm-lib/components/MobileTerminalUi";
-import { MobileWall, useMobileWallSessionItems, type MobileWallSession } from "mouseterm-lib/components/MobileWall";
-import { ThemePicker } from "mouseterm-lib/components/ThemePicker";
-import { restoreActiveTheme } from "mouseterm-lib/lib/themes";
+import { MobileTerminalUi, type MobileTerminalKeyboardMode, type MobileTerminalTouchMode } from "dormouse-lib/components/MobileTerminalUi";
+import { MobileWall, useMobileWallSessionItems, type MobileWallSession } from "dormouse-lib/components/MobileWall";
+import { ThemePicker } from "dormouse-lib/components/ThemePicker";
+import { restoreActiveTheme } from "dormouse-lib/lib/themes";
import {
getMouseSelectionSnapshot,
setOverride as setMouseOverride,
subscribeToMouseSelection,
-} from "mouseterm-lib/lib/mouse-selection";
+} from "dormouse-lib/lib/mouse-selection";
import { PlaygroundShellRegistry } from "../lib/playground-shells";
import { TutorialState } from "../lib/tutorial-state";
import { BUSY_DEMO_DURATION_MS, BUSY_DEMO_INTERVAL_MS, TutRunner } from "../lib/tut-runner";
@@ -18,7 +18,7 @@ import { ChangelogRunner } from "../lib/changelog-runner";
export { Pocket as Component };
-type FakePtyAdapter = import("mouseterm-lib/lib/platform/fake-adapter").FakePtyAdapter;
+type FakePtyAdapter = import("dormouse-lib/lib/platform/fake-adapter").FakePtyAdapter;
const POCKET_PANE = "pocket-ascii-splash";
const POCKET_THEME_ID = "vscode.theme-kimbie-dark.kimbie-dark";
@@ -86,11 +86,11 @@ function PocketTerminalExperience({
let cancelled = false;
async function loadWall() {
- const platform = await import("mouseterm-lib/lib/platform");
- const registry = await import("mouseterm-lib/lib/terminal-registry");
- const scenarios = await import("mouseterm-lib/lib/platform/fake-scenarios");
+ const platform = await import("dormouse-lib/lib/platform");
+ const registry = await import("dormouse-lib/lib/terminal-registry");
+ const scenarios = await import("dormouse-lib/lib/platform/fake-scenarios");
const asciiSplash = await import("../lib/ascii-splash-runner");
- await import("mouseterm-lib/index.css");
+ await import("dormouse-lib/index.css");
if (cancelled) return;
const adapter = platform.initPlatform("fake");
@@ -195,7 +195,7 @@ function PocketTerminalExperience({
onSessionSelect={setActivePaneId}
onSendInput={(data) => adapterRef.current?.writePty(activePaneId, data)}
onPaste={async () => {
- const { doPaste } = await import("mouseterm-lib/lib/clipboard");
+ const { doPaste } = await import("dormouse-lib/lib/clipboard");
await doPaste(activePaneId);
}}
/>
diff --git a/website/tsconfig.json b/website/tsconfig.json
index 0d3e696c..42a9f690 100644
--- a/website/tsconfig.json
+++ b/website/tsconfig.json
@@ -14,7 +14,7 @@
"baseUrl": ".",
"paths": {
"ascii-splash-internal/*": ["node_modules/ascii-splash/dist/*"],
- "mouseterm-lib/*": ["../lib/src/*"]
+ "dormouse-lib/*": ["../lib/src/*"]
},
"strict": true,
"noUnusedLocals": true,
diff --git a/website/vite.config.ts b/website/vite.config.ts
index ea31e169..a815f43b 100644
--- a/website/vite.config.ts
+++ b/website/vite.config.ts
@@ -7,7 +7,7 @@ export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
alias: {
- "mouseterm-lib": path.resolve(__dirname, "../lib/src"),
+ "dormouse-lib": path.resolve(__dirname, "../lib/src"),
"ascii-splash-internal": path.resolve(
__dirname,
"node_modules/ascii-splash/dist",
From 35b57d7ec6b4ea26abf617e0d6835037485d733b Mon Sep 17 00:00:00 2001
From: Ned Twigg
Date: Sun, 17 May 2026 21:50:10 -0700
Subject: [PATCH 10/24] Change the share image.
---
website/public/og-image.jpg | Bin 76478 -> 83787 bytes
1 file changed, 0 insertions(+), 0 deletions(-)
diff --git a/website/public/og-image.jpg b/website/public/og-image.jpg
index af0986434a688ec12152bb38ca024ed668d2dc99..edbc04cceb2739b28be7768f5f1b1fab06df660e 100644
GIT binary patch
literal 83787
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
-xE%_}-d
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