From 9548f32be3df504506e4575918d27d4ee8f07385 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 5 May 2026 23:55:08 -0700 Subject: [PATCH 01/11] Swap playground demo pane for an auto-running ascii-splash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The right column now hosts `tut-boxed` (titled "changelog", still the SCENARIO_BOXED_PARAGRAPH target for Copy Rewrapped) on top, and a new `tut-splash` pane on the bottom that auto-launches AsciiSplashRunner on mount — so the cursor-override demo's prerequisite is already running when the user reaches it. Also titles the left pane "tutorial" instead of Wall's `` default. The dropped `tut-target` shell pane is no longer needed; kb-arrows / kb-move demos navigate among the remaining three panes. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/specs/tutorial.md | 8 +++--- website/src/pages/Playground.tsx | 44 ++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/docs/specs/tutorial.md b/docs/specs/tutorial.md index c71a542..8b32a45 100644 --- a/docs/specs/tutorial.md +++ b/docs/specs/tutorial.md @@ -17,8 +17,8 @@ Three browser-side pieces in `website/src/lib/`, mirroring the pattern in `websi - `
` is a flex container so Wall's `flex-1 min-h-0` root gets a real height. - `Wall` runs `FakePtyAdapter` with `initialMode="passthrough"` and three initial panes: - **`tut-main`** (left, ~50%) — auto-launches `TutRunner` on mount via `mainShell.runCommand("tut")`. - - **`tut-target`** (right-top, ~25%) — `SCENARIO_SHELL_PROMPT`. Used as the demo pane for keyboard-nav and alert sections. - - **`tut-boxed`** (right-bottom, ~25%) — `SCENARIO_BOXED_PARAGRAPH`. The boxed paragraph for Copy Rewrapped vs Copy Raw. + - **`tut-boxed`** (right-top, ~25%) — titled "changelog". `SCENARIO_BOXED_PARAGRAPH`. The boxed paragraph for Copy Rewrapped vs Copy Raw. + - **`tut-splash`** (right-bottom, ~25%) — titled "ascii-splash". Auto-launches `AsciiSplashRunner` on mount via `splashShell.runCommand("ascii-splash")`. - The two right-side panes are added in `onApiReady` with `position: { referencePanel, direction }` after Wall creates the initial main pane. Every playground pane gets a `TutorialShell` input handler through `PlaygroundShellRegistry`. Newly split or spawned fake terminals use `SCENARIO_SHELL_PROMPT` by default. The shell dispatches by command name to a `startProgram` factory provided by the page; the factory wires `tut` → `TutRunner` and `ascii-splash` / `splash` → `AsciiSplashRunner`. @@ -64,7 +64,7 @@ The detector subscribes to `subscribeToActivity()` and tracks per-id `(status, t The detector remembers the most recent pane whose alert was enabled. 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: -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 alert-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 alert-enabled pane is known, the runner falls back to `PANE_TARGET`. `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. +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 alert-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 alert-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. ### Section 3 — Copy paste (4 items) @@ -115,7 +115,7 @@ The picker restores the persisted active theme on mount. The playground header i ## Mouse and Clipboard Feature Coverage -The Playground is the primary dogfood surface for the features in `docs/specs/mouse-and-clipboard.md`. The new tutorial layout (`tut-main` running the runner, `tut-target` shell, `tut-boxed` boxed paragraph) plus the user-launched `ascii-splash` pane covers most of the spec; one notable gap remains. +The Playground is the primary dogfood surface for the features in `docs/specs/mouse-and-clipboard.md`. The tutorial layout (`tut-main` running the runner, `tut-boxed` changelog/boxed-paragraph pane, `tut-splash` auto-running `ascii-splash`) covers most of the spec; one notable gap remains. Legend: ✅ exercisable today, ⚠️ partial, ❌ not exercisable. diff --git a/website/src/pages/Playground.tsx b/website/src/pages/Playground.tsx index e7a52cc..2e35a99 100644 --- a/website/src/pages/Playground.tsx +++ b/website/src/pages/Playground.tsx @@ -9,8 +9,8 @@ import { BUSY_DEMO_DURATION_MS, BUSY_DEMO_INTERVAL_MS, TutRunner } from "../lib/ export { Playground as Component }; const PANE_MAIN = "tut-main"; -const PANE_TARGET = "tut-target"; 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; @@ -26,6 +26,7 @@ function Playground() { const stateRef = useRef(null); const dockviewDisposablesRef = useRef([]); const tutorialAutoStartedRef = useRef(false); + const splashAutoStartedRef = useRef(false); const spawnUnsubRef = useRef<(() => void) | null>(null); const busyDemoDisposeRef = useRef<(() => void) | null>(null); const alertDemoPaneIdRef = useRef(null); @@ -38,6 +39,14 @@ function Playground() { shellRegistry.ensureShell(PANE_MAIN).runCommand("tut"); }, []); + const tryAutoStartSplash = useCallback(() => { + if (splashAutoStartedRef.current) return; + const shellRegistry = shellRegistryRef.current; + if (!shellRegistry) return; + splashAutoStartedRef.current = true; + shellRegistry.ensureShell(PANE_SPLASH).runCommand("ascii-splash"); + }, []); + useEffect(() => { let cancelled = false; async function loadWall() { @@ -55,13 +64,14 @@ function Playground() { adapterRef.current = adapter; adapter.setDefaultScenario(scenarios.SCENARIO_SHELL_PROMPT); - adapter.setScenario(PANE_TARGET, scenarios.SCENARIO_SHELL_PROMPT); adapter.setScenario(PANE_BOXED, scenarios.SCENARIO_BOXED_PARAGRAPH); - // tut-main is owned by the TutRunner — explicitly suppress the - // default shell-prompt scenario, otherwise spawnPty queues a - // delayed `user@mouseterm:~$` write that lands underneath the - // menu and stays there until the next runner re-render clears it. + // tut-main is owned by the TutRunner, tut-splash by AsciiSplashRunner — + // explicitly suppress the default shell-prompt scenario, otherwise + // spawnPty queues a delayed `user@mouseterm:~$` write that lands + // underneath the runner output and stays there until the next + // re-render clears it. adapter.setScenario(PANE_MAIN, { name: "none", chunks: [] }); + adapter.setScenario(PANE_SPLASH, { name: "none", chunks: [] }); const tutorialState = new TutorialState(); stateRef.current = tutorialState; @@ -82,7 +92,7 @@ function Playground() { state: tutorialState, onExit, onTriggerBusyDemo: () => { - const paneId = alertDemoPaneIdRef.current ?? PANE_TARGET; + const paneId = alertDemoPaneIdRef.current ?? PANE_BOXED; const sessionId = registry.resolveTerminalSessionId(paneId); busyDemoDisposeRef.current?.(); busyDemoDisposeRef.current = adapter.pumpActivity( @@ -107,16 +117,18 @@ function Playground() { shellRegistryRef.current = shellRegistry; shellRegistry.ensureShell(PANE_MAIN); - shellRegistry.ensureShell(PANE_TARGET); shellRegistry.ensureShell(PANE_BOXED); + shellRegistry.ensureShell(PANE_SPLASH); // Subscribe before Wall mounts so the spawn fired by TerminalPane's // mount effect doesn't race past us. If the pty already exists by // the time we get here, fire immediately. spawnUnsubRef.current = adapter.onPtySpawn(({ id }) => { if (id === PANE_MAIN) tryAutoStartTutorial(); + if (id === PANE_SPLASH) tryAutoStartSplash(); }); if (adapter.hasPty(PANE_MAIN)) tryAutoStartTutorial(); + if (adapter.hasPty(PANE_SPLASH)) tryAutoStartSplash(); setWallModule({ Wall: wall.Wall }); } @@ -134,6 +146,7 @@ function Playground() { shellRegistryRef.current = null; stateRef.current = null; tutorialAutoStartedRef.current = false; + splashAutoStartedRef.current = false; alertDemoPaneIdRef.current = null; spawnUnsubRef.current?.(); spawnUnsubRef.current = null; @@ -152,22 +165,25 @@ function Playground() { dockviewDisposablesRef.current.push(addDisposable); api.addPanel({ - id: PANE_TARGET, + id: PANE_BOXED, component: "terminal", tabComponent: "terminal", - title: "demo", + title: "changelog", position: { referencePanel: PANE_MAIN, direction: "right" }, }); api.addPanel({ - id: PANE_BOXED, + id: PANE_SPLASH, component: "terminal", tabComponent: "terminal", - title: "release notes", - position: { referencePanel: PANE_TARGET, direction: "below" }, + title: "ascii-splash", + position: { referencePanel: PANE_BOXED, direction: "below" }, }); const mainPanel = api.getPanel(PANE_MAIN); - if (mainPanel) mainPanel.api.setActive(); + if (mainPanel) { + mainPanel.api.setTitle("tutorial"); + mainPanel.api.setActive(); + } detectorRef.current?.attach(api); }, []); From 61b1b0973cc3e2b2af2cc02931ec11e419cbe4da Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 5 May 2026 23:59:04 -0700 Subject: [PATCH 02/11] Tighter spacing on the homepage. --- website/src/pages/Home.tsx | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/website/src/pages/Home.tsx b/website/src/pages/Home.tsx index 4870980..1d79c50 100644 --- a/website/src/pages/Home.tsx +++ b/website/src/pages/Home.tsx @@ -251,7 +251,6 @@ function Home() { const word0Ref = useRef(null); const word1Ref = useRef(null); const word2Ref = useRef(null); - const asteriskRef = useRef(null); const footnoteRef = useRef(null); const headerRef = useRef(null); const headerBrandRef = useRef(null); @@ -476,12 +475,11 @@ function Home() { el.style.transform = `translateY(${(1 - progress) * 12}px)`; } - // Asterisk + footnote - const astProgress = clamp01( + // Footnote + const footnoteProgress = clamp01( (fraction - ASTERISK_THRESHOLD) / 0.08 ); - if (asteriskRef.current) asteriskRef.current.style.opacity = String(astProgress); - if (footnoteRef.current) footnoteRef.current.style.opacity = String(astProgress * 0.7); + if (footnoteRef.current) footnoteRef.current.style.opacity = String(footnoteProgress * 0.7); // Header: reveal brand + background just before the tmux-shortcuts // footnote appears, so it reads as dark once the line is visible. @@ -659,16 +657,14 @@ function Home() { Terminal - - for Mice* - + for Mice

- *supports (and teaches) tmux shortcuts + (and hotkey wizards too)

From 642fe8988c92a475d3c55ee1d5c4e09ccc09709b Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 6 May 2026 00:38:28 -0700 Subject: [PATCH 03/11] Add Place To Paste modal toggled by `p` in the Copy paste section The cp-raw hint now points users at a draggable, CSS-resizable scratch modal where they can paste copied text and watch it reflow (or not) as they drag the corner. TutRunner intercepts `p`/`P` only while the copy section is open, mirroring the existing `s` busy-demo intercept in the alert section, and Playground flips a state flag that mounts the modal overlay above the wall. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/specs/tutorial.md | 2 + website/src/components/PlaceToPaste.tsx | 71 +++++++++++++++++++++++++ website/src/lib/tut-items.ts | 3 +- website/src/lib/tut-runner.ts | 13 +++++ website/src/pages/Playground.tsx | 6 +++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 website/src/components/PlaceToPaste.tsx diff --git a/docs/specs/tutorial.md b/docs/specs/tutorial.md index 8b32a45..aac0a4f 100644 --- a/docs/specs/tutorial.md +++ b/docs/specs/tutorial.md @@ -84,6 +84,8 @@ Prose: The Copy Rewrapped step uses `SCENARIO_BOXED_PARAGRAPH` (in `lib/src/lib/platform/fake-scenarios.ts`). Frame-only and frame-flanking box-drawing runs are stripped by `lib/src/lib/rewrap.ts` so Rewrapped joins the wrapped paragraph; clipboard contents visibly differ from Raw. +While the Copy paste section is open, pressing `p` toggles the **Place To Paste** modal — a draggable, CSS-`resize:both` scratch box rendered by `website/src/components/PlaceToPaste.tsx` and mounted at the page level. `TutRunner` intercepts `p`/`P` (mirroring the Alert section's `s` busy-demo intercept) and calls `onTogglePlaceToPaste`; `Playground` flips a `placeToPasteOpen` flag so the modal is portal-free and overlays the wall. Users paste copied text into its single textarea and resize the modal to see whether the text reflows (Rewrapped) or stays line-broken (Raw). + ## Lib changes added for this tutorial - **`WallEvent.kill`** and **`WallEvent.move`** — new discriminants on the `WallEvent` union (`lib/src/components/wall/wall-types.ts`). `kill` fires from `acceptKill` in `Wall.tsx`. `move` fires from `handle-pane-shortcuts.ts` after the Cmd/Ctrl-Arrow swap, via a new `fireEvent` callback added to `WallKeyboardCtx`. diff --git a/website/src/components/PlaceToPaste.tsx b/website/src/components/PlaceToPaste.tsx new file mode 100644 index 0000000..8e820e2 --- /dev/null +++ b/website/src/components/PlaceToPaste.tsx @@ -0,0 +1,71 @@ +import { useRef, useState } from "react"; + +interface PlaceToPasteProps { + onClose: () => void; +} + +const INITIAL = { x: 360, y: 140, w: 420, h: 280 }; + +export function PlaceToPaste({ onClose }: PlaceToPasteProps) { + const [pos, setPos] = useState({ x: INITIAL.x, y: INITIAL.y }); + const dragRef = useRef<{ mx: number; my: number; px: number; py: number } | null>(null); + + const onHeaderPointerDown = (e: React.PointerEvent) => { + if (e.button !== 0) return; + if (e.target !== e.currentTarget) return; + e.currentTarget.setPointerCapture(e.pointerId); + dragRef.current = { mx: e.clientX, my: e.clientY, px: pos.x, py: pos.y }; + }; + + const onHeaderPointerMove = (e: React.PointerEvent) => { + const d = dragRef.current; + if (!d) return; + setPos({ x: d.px + e.clientX - d.mx, y: d.py + e.clientY - d.my }); + }; + + const onHeaderPointerUp = (e: React.PointerEvent) => { + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + dragRef.current = null; + }; + + return ( +
+
+ Place To Paste + +
+