See
docs/specs/glossary.mdfor Session / Pane vocabulary, used here for the playground's pane layout and detection wiring.
Device routes (website/src/routes.ts):
/playground— dispatcher: Pocket for coarse pointers or narrow viewports, Desktop otherwise, then replaces the history entry, preserving search + hash (query inwebsite/src/lib/playground-routing.ts)./playground/desktop— desktop tiling tutorial; where the dispatcher would pick Pocket, a link to/playground/pocketinstead ofWall./playground/pocket— mobile Pocket playground; on desktop, the temporary Pocket marketing/share page./pocket— temporary redirect to/playground/pocket. Keep the real tethering surface off the playground URL.
Must hydrate the desktop prerender, then reconcile browser media. Must dispatch using browser media, never the hydration fallback. Must skip desktop runtime loading when browser media selects Pocket. Pinned by website/src/pages/Playground.test.tsx.
DESKTOP_TUTORIAL_PROFILE and POCKET_TUTORIAL_PROFILE open inside their initialSectionId; desktop's is Make it yours, a single change-the-theme item (rationale). Pocket's Copy paste is desktop's minus cp-override (Pocket Copy paste specifics).
Item ids are stable — they are the localStorage payload entries (Storage).
Must run the tutorial as a browser-side xterm alt-screen program behind FakePtyAdapter, never Node terminal-kit.
Must derive progress from app events and semantic snapshots, never touch the tiling engine.
- Must credit a keyboard split before its automatic passthrough transition, and tell the user to re-enter command mode for the following navigation item (rationale).
- Must credit
kb-arrowsonly on command-mode selection of a distinct pane; an arrow or click counts, a swap's resulting focus does not. - Must credit
al-spreadsonly when newly enabled WATCHING shares a command key with another live pane. - Must credit the
dor opensection from the command lines the shells report, never from a hook in the playgrounddoror its viewers.
Source of truth: TutRunner in website/src/lib/tut-runner.ts; TutDetector in website/src/lib/tut-detector.ts; TutorialState in website/src/lib/tutorial-state.ts; profiles in website/src/lib/tut-items.ts.
- The desktop page must restore its own theme with
useRestoredTheme(WEBSITE_DEFAULT_THEME_ID)(website/src/lib/website-theme.ts), which also declares the host fallback the Settings picker re-resolves through; itsSiteHeadercarries no controls (rationale). /playground/desktoprunsWall(FakePtyAdapter,initialMode="passthrough"). Must seed its three-pane L-shape as an explicit Lath snapshot —restoredLathLayoutfromDESKTOP_PLAYGROUND_LAYOUT— never the synchronousinitialPaneIdspath (rationale);website/src/lib/playground-desktop-layout.test.tspins it.DESKTOP_PANESin the same file owns each seed's id, command, and title;tut-boxedis the Auto-copy +cp-overridetarget (rationale). Titles are seeded as pending shell opts (setPendingShellOpts(id, { title })) before the Wall mounts, and a user-pin outranks the engine fallback (docs/specs/terminal-state.md→ "Header Derivation").
Every visible pane gets a TutorialShell via PlaygroundShellRegistry. ensureShell must stay idempotent — paneAdded covers every pane that becomes visible, and FakePtyAdapter.onPtySpawn covers the seed panes again, auto-launching each seed's command exactly once (rationale). The page's startProgram knows tutorial (TutRunner), ascii-splash/splash, changelog, and dor (Playground filesystem).
/playground/pocket runs MobileWall with pocket-tut (active, TutRunner on POCKET_TUTORIAL_PROFILE) and pocket-changelog (ChangelogRunner), and starts a TutDetector over the same shared stores. Must credit Pocket gesture items only on the active tutorial Session's Gesture navigation screen, through MobileTerminalUi.onGestureScroll and onGestureInput.
Source of truth: PlaygroundDesktop in website/src/pages/PlaygroundDesktop.tsx; PocketPlayground in website/src/pages/PocketPlayground.tsx.
Must credit both edge-scroll directions (gn-scroll), then all four arrows (gn-arrows), then Enter, then Escape, each only once its predecessor is complete; an arrow sent before scrolling completes does not count. Must clear partial direction counts on tutorial reset. Keyboard input and native wheels never grant gesture credit.
Must capture the mouse only on this screen, where vertical wheels scroll its starfield, and must stop animation and release capture on leaving or disposal. Reduced motion disables idle animation, retaining scroll movement.
Source of truth: TutRunner and GestureStarfield in website/src/lib/tut-runner.ts; GESTURE_NAVIGATION_SECTION in website/src/lib/tut-items.ts.
Must consume unsupported CSI/SS3 key sequences without treating their prefix as Esc; arrows accept CSI and application-mode SS3. Pinned by website/src/lib/tut-runner.test.ts. Reset progress requires the user type reset, then clears storage (Storage) and returns to the profile's initial screen.
Extras: Starred on GitHub (persisted separately), FlappyTerm, Reset progress — none of the three ever counts toward a section's progress. FlappyTerm stays locked until every section checklist item is complete.
TutRunner intercepts four keys while a specific section is open; they are not real Dormouse shortcuts. The three alert demos report fake commands as OSC 633 ; E / C / D through FakePtyAdapter.sendOutput, which the real TerminalProtocolParser strips from visible output (rationale). Must snapshot the live inactivity timeout at demo launch; the run outlasts it and the BUSY-confirm floor. Each demo's countdown, page timer, and re-press guard run the same snapshotted duration — longer for s, whose fake command must outlive WATCHING's silence chain. Pinned by website/src/lib/tut-runner.test.ts.
s(Alerts) — reportslongtaskon both alert panes so command-keyed WATCHING demonstratesal-spreads, pumping only the quiettut-boxed(rationale). A press while that command is still running is ignored, and the page cancels any prior pump and exit timer — the runner's guard is per instance, so a re-runtutorialwould otherwise stack them.n(Alerts) — writes a rawOSC 777notification totut-boxed, exercising a terminal report, which needs no WATCHING rule.x(Alerts) — starts a fakeslowbuildontut-splashand reports its exit after the captured duration. The command name must stay unwatched, so a command exit rather than WATCHING raises the ring (rationale). The page must cancel the prior exit timer across runner instances.p(Copy paste) — toggles the Place To Paste scratch modal (website/src/components/PlaceToPaste.tsx). Desktop only.
Pocket reuses cp-select / cp-raw / cp-rewrap but drops cp-override: Select mode auto-overrides mouse capture for every Pocket session whose TUI captures the mouse (docs/specs/mobile-terminal-ui.md → "Touch mode selector"), so it never asks the user to click the cursor icon. A live prompt above the checklist reflects the touch mode, neither stored nor counted.
TutorialShell (Layout):
- Shell integration must be reported for every command it runs —
OSC 633 ; A/Baround the prompt,633 ; E+633 ; Con launch,633 ; Don exit. WATCHING is keyed on the running command's name (docs/specs/alert.md), and the OSCs also keepdocs/specs/terminal-state.md's keystroke fallback from engaging here (rationale). - While a program runs, every input byte goes to it —
\x03included, which the runners treat as quit — as do bytes left in the chunk after the Enter that launched it. On exit the terminal returns to the prompt instead of restarting the program. At the prompt,\x03abandons the line for a new prompt and runs nothing. Tab completes the last word: a command name, and on the desktopdor's verb or a snapshot path.
The only commands are the ones startProgram knows (Layout) and the desktop's cd, ls, and pwd; anything else prints an "Unknown command" line and exits 127.
The desktop shells share one read-only filesystem: the tracked files of dor-tools-lib/, inlined at build time and mounted at /home/demo/dor-tools-lib, every shell's starting directory. Beside it sits the playground's own user config at ~/.config/dormouse/dormouse.yml, inert: shown, never read for rules. website/src/lib/playground-fs/playground-fs.test.ts pins the snapshot to git ls-files dor-tools-lib and the config to a warning-free user Tool file.
- Must report the shell's directory with every prompt (
OSC 633 ; P ; Cwd=): take-over and launch matching compare it (docs/specs/dor-tool.md→ Take-over).cd,ls, andpwdare the filesystem builtins; nothing writes. - Must print each spawned non-helper terminal's first prompt from
PlaygroundShellRegistry; no scenario plays on the desktop, and a split Tool waits on that prompt's integration (rationale). - Must name
/bin/fakeas the default shell, so a Windows visitor's Tool commands quote as posix rather than being refused ascmd(rationale). - The playground
dormust run the real CLI (runCliindor/src/cli-core.ts) over a playgroundCliHostand aControlClientthat sends each request to the Wall; what needs Node must failUNSUPPORTED IN PLAYGROUND. It serves the__view-*entries itself.FakePtyAdapter.toolControlanswersopenandopen-handlersfrom the snapshot as a host with no userdormouse.ymldoes. - A
__view-*entry must report its port before announcing it, and withdraw it on exit. - Must serve the real viewer pages and CSPs on the Node viewers' routes, answered from the snapshot;
save, and a Markdown viewer'simageandrename, answer403. The editors are unchanged, so edits stay in the page. createIframeProxyUrlmust map only alocalhostURL under/playground-fs/to the page's own origin, refusing others (scheme). The viewer pages carry the shim frominstrumentHtml, so theme and the save channel connect as behind a proxy, and are same-origin with the Wall: first-party pages over a fixed snapshot.- The
/playground-fs/service worker must stay stateless, relaying each viewer request as below (rationale). It registers when the first viewer starts; without one,__view-*exits1.
sequenceDiagram
participant F as viewer iframe
participant SW as /playground-fs/ service worker
participant P as top-level playground windows
F->>SW: request /playground-fs/token/route
alt route is assets/*
SW-->>F: the static /builtin-viewer/ build
else any other route
SW->>P: playground-fs-request, to every window
P-->>SW: the window holding the token answers from the snapshot
SW-->>F: that answer, or 404 when none answers in time
end
Source of truth: installPlaygroundFs in website/src/lib/playground-fs/index.ts; TutorialShell in website/src/lib/tutorial-shell.ts; website/public/playground-fs/sw.js; website/scripts/build-builtin-viewer.js.
TutorialState persists to localStorage. Unknown ids in a stored payload are filtered on load, so renaming an id is a one-way reset. Both profiles share the completion key, so markComplete must reject an id outside the profile's own sections — a Pocket detection that names a desktop-only item would otherwise arrive pre-checked. Must share completed common items across profiles; keep absent-profile ids stored but exclude them from that profile's totals.
Must keep progress and reset working without storage, pinned by website/src/lib/tutorial-state.test.ts.
dormouse-tut-v3— JSON array of completed item ids.dormouse-tut-star-v1—"true"afterStarred on GitHub.dormouse-flappy-high-v1— high score.
Must remove all three on TutorialState.reset(), even when rejected stored values left progress empty. Legacy dormouse-tutorial-step-N / dormouse-tut-v2-* keys are never read.
Hooks in dormouse-lib / MobileTerminalUi that exist for tutorial observability:
WallEvent.kill/move/paneAdded— discriminants on theWallEventunion.killfires fromkillPaneImmediately, so every kill path creditskb-kill.movemust fire from both the Cmd/Ctrl-Arrow swap inlib/src/components/wall/keyboard/handle-pane-shortcuts.tsand the center-drop swap inWall.onProposeMove(rationale).paneAddedfires once per pane that becomes visible — seed ids, splits, dor surfaces, restores, auto-spawn — with seeds announced explicitly.FakePtyAdapter.pumpActivity— drives the alert manager for a fixed duration with no data output (thesdemo). Returns a cancel handle; stops on its own if the pty dies mid-duration.FakePtyAdapter.sendOutput— pushes data through the real protocol parser as if the PTY produced it (rationale). UnlikewritePtyit is not suppressed while a scenario is playing.FakePtyAdapter.onPtySpawn— fires synchronously insidespawnPty, before the scenario plays, so a page attaches a shell without racingTerminalPane's mount.subscribeToWatchedCommands/getWatchedCommands(lib/src/lib/watched-commands.ts, re-exported fromterminal-registry) — the WATCHING rule set; must credital-watch-cmdonly oncelongtaskis watched.MobileTerminalUi.onGestureInput— optional, reports radial-menu input only; must fire before the input is sent, so the final Escape is credited before it leaves the tutorial screen. Pinned bylib/src/components/MobileTerminalUi.test.tsx.MobileTerminalUi.onGestureScroll— optional, reports signed line counts only for edge scrolling.subscribeToActiveTheme/getActiveThemeId(lib/src/lib/themes/) — the active theme, watched to creditth-theme. Must seed the detector's previous theme atstart()and compare consecutive ids, so boot-time restore cannot grant the item and choosing the startup theme after a reset still can. Pinned bywebsite/src/lib/tut-detector.test.ts(rationale).
Source of truth: WallEvent in lib/src/components/wall/wall-types.ts, emitted from lib/src/components/Wall.tsx; FakePtyAdapter in lib/src/lib/platform/fake-adapter.ts; MobileTerminalUi in lib/src/components/MobileTerminalUi.tsx.
Scope: tutorial-coverage
Two tut-boxed scenarios close the playground's docs/specs/mouse-and-clipboard.md coverage gaps, needing no section change: