Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8139a27
Prototype a copy editor in Storybook
nedtwigg Oct 1, 2026
6e66dd1
Screencast Storybook and strip dormouse.yml comments
nedtwigg Oct 1, 2026
890fdc3
Label the copy editor prototype's scope keys expand/shrink and show f…
nedtwigg Oct 1, 2026
c27d450
Add the copy editor's text model
nedtwigg Oct 1, 2026
d87c8e4
Replace the Copy Raw / Rewrapped popup with the copy editor
nedtwigg Oct 1, 2026
b7885c5
Simplify the copy editor
nedtwigg Oct 1, 2026
689413f
Shadow drags a mouse-reporting program owns, so Cmd+C opens the copy …
nedtwigg Oct 1, 2026
813185c
Offer a program's OSC 52 copy in the copy editor
nedtwigg Oct 1, 2026
a30481a
Simplify shadowed drags and OSC 52 offers; fix narrow wraps
nedtwigg Oct 1, 2026
e71694e
Describe each dev Tool's reload behavior in dormouse.yml
nedtwigg Oct 1, 2026
28b9f2b
Expect Storybook's Tool to render as an agent-browser screencast
nedtwigg Oct 1, 2026
23e6ca6
Delete the copy editor prototype stories
nedtwigg Oct 1, 2026
8b50100
Simplify the copy editor across the branch
nedtwigg Oct 1, 2026
877eb5e
Put selection edges at the cell boundary nearest the pointer
nedtwigg Oct 1, 2026
74e088c
Carry a selection through a terminal resize's reflow
nedtwigg Oct 1, 2026
f01de8d
Keep per-break edits through a resize that keeps the width
nedtwigg Oct 1, 2026
5ade883
Keep the space at a soft wrap in the copy editor
nedtwigg Oct 1, 2026
58308d6
Simplify the reflow-following selection
nedtwigg Oct 1, 2026
52941de
Add the copy editor's window-wide placement and eased motion
nedtwigg Oct 1, 2026
762a1a0
Count a portaled copy editor as part of its pane
nedtwigg Oct 1, 2026
7ef7bff
Place the copy editor anywhere in the window, easing every move
nedtwigg Oct 1, 2026
84a2dac
Spec the copy editor's window-wide placement and motion
nedtwigg Oct 1, 2026
efa71b3
Treat soft-wrapped rows as one line in copy scopes and Auto
nedtwigg Oct 1, 2026
c8401e8
Keep keyboard focus in the copy editor's pane
nedtwigg Oct 1, 2026
f3f1177
Share pane tracking between the copy editor and the focus ring
nedtwigg Oct 1, 2026
54a72c8
Trim the copy editor's measuring, placement, and motion
nedtwigg Oct 1, 2026
4d42e92
Read soft-wrapped lines through one walker
nedtwigg Oct 1, 2026
cc46048
Make the mid-drag token hint cheap
nedtwigg Oct 1, 2026
49458fc
Widen the copy editor to show its header and footer whole
nedtwigg Oct 1, 2026
3f22d60
Squish the copy editor beside its pane, and fit a side to its controls
nedtwigg Oct 1, 2026
9a63eeb
Make the copy editor's Copy a touch target, and its outcome visible
nedtwigg Oct 1, 2026
f365105
Make a copy one outcome, and copy without moving focus
nedtwigg Oct 1, 2026
ad4c029
Give the touch copy editor a hit margin, and probe its chrome once
nedtwigg Oct 1, 2026
3bd17c5
Preserve extended selections and re-read tokens; fix whitespace nudges
nedtwigg Oct 1, 2026
fb6cc3c
Derive a drag's shape in one place
nedtwigg Oct 1, 2026
da05451
Merge remote-tracking branch 'origin/main' into copy-paste-2
nedtwigg Oct 1, 2026
76924a5
Keep copy focus and preserve paragraph boundaries and relative indent
nedtwigg Oct 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ A spec is the accurate reference for the current code: it states the invariants
- **`docs/specs/terminal-escapes.md`** — Registry of every escape sequence parsed, answered, or ignored, each row pointing at its owning spec. Read before touching OSC/CSI parsing.
- **`docs/compatible-agents.md`** — Public agent guide plus the hidden recovery contract: shutdown capture, detection, single-use records, and cold-restore execution.
- **`docs/specs/transport.md`** — Adapter-agnostic webview ↔ host protocol: PTY lifecycle and buffering, reconnection, message contracts, persisted-session types, the invariants every adapter honors.
- **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned selection, copy (Raw / Rewrapped), paste tiers, smart URL/path extension, the mouse-ownership state matrix.
- **`docs/specs/mouse-and-clipboard.md`** — Terminal-owned selection, the copy editor (Auto / Exact / Spaces / No breaks, expand, per-break marks), paste tiers, smart URL/path extension, the mouse-ownership state matrix.
- **`docs/specs/theme.md`** — The two-layer CSS variable strategy, consumed-token resolver, terminal color contract, theme debugger.
- **`docs/specs/dor-cli.md`** — The `dor` CLI on every Dormouse terminal's `PATH`: bundling and env contract, `spawnAndCapture` rules, control-socket plumbing, the Surface handle model, the command set.
- **`docs/specs/dor-browser.md`** — The browser surface: `BrowserPanel` with swappable `renderMode`, browser chrome, the agent-browser stack, the iframe proxy and CSP boundaries.
Expand Down
9 changes: 6 additions & 3 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ Flat by default. Pane headers, doors, the baseboard, and terminal panes carry ze
Shadows appear only on **raised surfaces that float above content**: popovers, tooltips, dialogs. They are ambient, not structural; they say "I am temporary and on top," not "I have weight."

### Shadow Vocabulary
- **Popover** (`box-shadow: var(--tw-shadow-md)`): tooltips (`PopupButtonRow`), selection popup, illegal-rename warning, terminal-pane header tooltips.
- **Popover** (`box-shadow: var(--tw-shadow-md)`): tooltips (`PopupButtonRow`), illegal-rename warning, terminal-pane header tooltips.
- **Dialog** (`box-shadow: var(--tw-shadow-lg)`): kill-confirm sheet, TODO alert dialog.
- **Modal** (`box-shadow: var(--tw-shadow-2xl)`): theme picker dropdown (when expanded), theme debugger, theme store dialog.
- **Modal** (`box-shadow: var(--tw-shadow-2xl)`): theme picker dropdown (when expanded), theme debugger, theme store dialog, copy editor.
- **Inset hairline** (`box-shadow: inset 0 0 0 1px var(--color-focus-ring)` / `var(--color-border)`): mobile UI segmented controls. Used instead of `border` when the surface needs a 1px stroke that does not shift layout on state change.

### Named Rules
Expand Down Expand Up @@ -267,6 +267,9 @@ The selection ring around the focused pane in command mode is an SVG with `strok
#### Focus Ring Travel & Header Crossfade
When selection moves between panes/doors, the focus ring **glides** to the new target over 220ms (`FOCUS_MOTION_MS`, half the pane-motion duration) on the house curve `cubic-bezier(0.22, 1, 0.36, 1)`, and the source/destination pane headers crossfade their active/inactive palette over the same 220ms (`HEADER_PALETTE_TRANSITION_CLASS` in `design.tsx`), so the two read as one gesture. The ring's rect is a per-frame JS tween (`rect-tween.ts`), not a CSS transition; same-identity re-measures (sash drag, window resize, animator frames) snap 1:1, and a pane↔door move lerps the corner radii so the shape never pops. Reduced motion nulls both: the ring snaps and the header palette swaps instantly.

#### Copy Editor Travel
The copy editor's moves and resizes ease on the focus ring's duration and curve (`FOCUS_MOTION_MS`, `rect-tween.ts` driven by `rect-motion.ts`); `docs/specs/mouse-and-clipboard.md` §4.5 owns when.

## 6. Do's and Don'ts

### Do:
Expand All @@ -287,7 +290,7 @@ When selection moves between panes/doors, the focus ring **glides** to the new t
- **Don't** introduce a `text-muted` color inside an active or inactive pane header. Header-internal text inherits the header foreground; muting inside it breaks the focus signal.
- **Don't** use rounded SaaS cards, gradient accents, gradient text, or glassmorphism. PRODUCT.md names these directly: "Generic SaaS (rounded cards, gradients, startup illustrations)," "Electron bloat (Slack — heavy, slow-feeling, too much chrome)."
- **Don't** use hacker-aesthetic green-on-black, terminal-cliché Matrix tints, or any color that signals "this is a programmer tool." The user's theme decides what color this tool is.
- **Don't** animate layout properties (`width`, `height`, `top`, `left`, `padding`) **with a CSS transition**. Pane transitions use `clip-path` and `opacity` deliberately so layout measurements stay valid mid-animation. The one carve-out is a JS tween that writes true per-frame values on a `pointer-events: none` overlay (the Lath animator; the focus ring's `rect-tween`): it moves through real intermediate geometry every frame rather than letting the browser interpolate an opaque box, so measurements stay valid — a CSS `transition: top/left/width/height` does not qualify and stays banned.
- **Don't** animate layout properties (`width`, `height`, `top`, `left`, `padding`) **with a CSS transition**. Pane transitions use `clip-path` and `opacity` deliberately so layout measurements stay valid mid-animation. The one carve-out is a JS tween that writes true intermediate geometry every frame (the Lath animator on the leaves; the `rect-tween` that carries the focus ring and the copy editor) rather than letting the browser interpolate an opaque box, so measurements stay valid mid-flight — a CSS `transition: top/left/width/height` does not qualify and stays banned.
- **Don't** add an emoji, mascot, or illustration to chrome. PRODUCT.md is explicit: "Overly playful (too many animations, emojis, mascots)."
- **Don't** gate app chrome on `window.alert` / `confirm` / `prompt`. Native dialogs are not dependable in the desktop webview: the theme uninstall was gated on `confirm` and silently did nothing there, because the call returned without ever showing a dialog. Whether a given webview suppresses the panel or never implements it, a control gated on one cannot be trusted to run. Use `ModalFrame`, or make the action a single click when it is cheap and reversible. The marketing website is exempt — it only ever runs in a real browser, where `ShareUrlButton`'s `prompt` is a reasonable last-resort clipboard fallback.
- **Don't** wrap things in containers. Most surfaces don't need one; the host's sidebar already is the container.
Expand Down
10 changes: 5 additions & 5 deletions docs/specs/layout.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ Source of truth: `useHeldWhile` in `lib/src/components/wall/preview-transition.t

**Must open the terminal context from terminal header, body, and command-mode `a` and `>` entry points.** Browser-only Surfaces and Doors have no context. Tool context displays its primary terminal; `docs/specs/terminal-context.md` → Tool context owns that composition. Application mouse ownership follows `docs/specs/mouse-and-clipboard.md` → Terminal context input.

**Must render one context per Wall in a stable Wall-level overlay**, with a theme-derived edge and raised shadow. Anchor it to the invoking source and follow its painted bounds without resizing panes or remounting the helper. Outside pointer press and explicit close dismiss it.
**Must render one context per Wall in a stable Wall-level overlay**, with a theme-derived edge and raised shadow. Anchor it to the invoking source and follow its painted bounds without resizing panes or remounting the helper. Outside pointer press and explicit close dismiss it; its copy editors count as inside (`anchoredTarget` in `lib/src/lib/dom.ts`).

**Must choose placement on opening and retain its side while usable.** Never reposition in response to terminal output. Minimized panes do not count; zoom uses single-pane placement.

Expand Down Expand Up @@ -255,7 +255,7 @@ Each Wall renders one Workspace's Content and Baseboard (doors). Standalone moun
- **Must mount every Workspace's Wall in one grid cell**, inactive Walls `inert`, then `visibility:hidden` after their fade and never `display:none` (rationale).
- **Must preserve mounted leaves across switches**: no re-seed, no re-parent, no leaf unmount, and no `resumeTerminal` / `restoreTerminal`; the only mount work is the terminal reattach below, which replays nothing, so I8 holds by construction (`lib/src/components/WorkspaceWindow.test.tsx`).
- **A hidden Wall's terminals hold no element and no GL context**: completion of the outgoing fade runs `unmountElement` on every terminal pane, exactly as minimize does ([Renderer](#renderer)); activation runs `mountElement` and fits through the [Animations](#animations) gate, so an unchanged grid sends no PTY resize (`lib/src/components/TerminalPane.test.tsx`). Browser Surfaces keep their live documents (rationale).
- **A hidden Wall consumes no window input**: every listener that dispatches, forwards, or `preventDefault`s window input is gated on `active`, keyboard and custom events alike, so a hidden Wall neither mounts chrome nor refits a detached element in answer to one. **Only the active Wall renders the modal hosts and the overlays that trap keys** — the kill confirmation and a terminal's selection popup (rationale): a staged prompt survives the switch and is answered only where the user can see it.
- **A hidden Wall consumes no window input**: every listener that dispatches, forwards, or `preventDefault`s window input is gated on `active`, keyboard and custom events alike, so a hidden Wall neither mounts chrome nor refits a detached element in answer to one. **Only the active Wall renders the modal hosts and the overlays that trap keys** — the kill confirmation and a terminal's copy editor (rationale): a staged prompt survives the switch and is answered only where the user can see it.
- **Exactly one Wall answers a `dor` request**, chosen by `docs/specs/dor-cli.md` → "Handle Model". Every Wall registers a handle, a bare one under `DEFAULT_WORKSPACE_ID`, so the router always finds one.
- **Never unmount a Wall before its Surfaces are disposed** — `closeAll` waits for the kill fade to commit, bounded by the engine's exit duration, since unmounting mid-fade would leave `Orphaned` Registry entries (`docs/specs/glossary.md` → "Invariants" I4). **The deadline refuses rather than reporting clean**, and the walk re-reads membership until nothing is left, so a Surface born behind it is closed too.
- **A closing Workspace takes no new Surfaces**: while `closeAll` walks, this Wall answers every Surface-creating `dor` verb with an error (`docs/specs/dor-cli.md` → "Handle Model"), **rechecked after any host round trip the verb makes before creating** (`CREATING_CONTROL_METHODS` in `lib/src/components/wall/use-dor-control.ts`; `lib/src/components/WorkspaceWindow.test.tsx`).
Expand Down Expand Up @@ -420,11 +420,11 @@ Source of truth: `lib/src/lib/ring-geometry.ts`.

### Position tracking

Each pane body registers its DOM element in a `paneElements` Map on mount and removes it on unmount (`usePaneChrome`); the overlay resolves the enclosing Lath leaf (`[data-lath-leaf]`) via `resolvePaneElement`, so the ring covers header + body. Doors are registered by the `Baseboard` through `DoorElementsContext` (`[data-door-id]`), **only the *visible* subset** — an overflowed door has no element to measure.
Each pane body registers its DOM element in a `paneElements` Map while mounted (`usePaneChrome`); the overlay resolves the enclosing Lath leaf (`[data-lath-leaf]`) via `resolvePaneElement`, so the ring covers header + body. Doors are registered by the `Baseboard` through `DoorElementsContext` (`[data-door-id]`), **only the *visible* subset** — an overflowed door has no element to measure.

Re-measures on: selection change, target resize, scroll, window resize, Workspace changes, every Lath store commit, and each Lath animation frame. **Must hold the last painted frame when the target is missing, detached, or zero-sized**, including stale Door observer notifications during restore. Pinned by `restores from the last painted Door through a %s target` in `lib/src/components/wall/WorkspaceSelectionOverlay.test.tsx`.
Re-measures on: selection change, target resize, an ancestor's scroll, window resize, Workspace changes, every Lath store commit, and each Lath animation frame. **Must hold the last painted frame when the target is missing, detached, or zero-sized**, including stale Door observer notifications during restore. Pinned by `restores from the last painted Door through a %s target` in `lib/src/components/wall/WorkspaceSelectionOverlay.test.tsx`.

Source of truth: `lib/src/components/wall/WorkspaceSelectionOverlay.tsx`; `resolvePaneElement` in `lib/src/components/wall/resolve-pane-element.ts`; `WindowFocusedContext` in `lib/src/components/wall/wall-context.tsx`, which the overlay reads and the Wall fills from `useWindowFocused` in `lib/src/components/wall/use-window-focused.ts`.
Source of truth: `lib/src/components/wall/WorkspaceSelectionOverlay.tsx`; `subscribePaneMotion` in `lib/src/components/wall/pane-motion.ts`; `resolvePaneElement` in `lib/src/components/wall/resolve-pane-element.ts`; `WindowFocusedContext` in `lib/src/components/wall/wall-context.tsx`, which the overlay reads and the Wall fills from `useWindowFocused` in `lib/src/components/wall/use-window-focused.ts`.

## Spatial navigation

Expand Down
5 changes: 4 additions & 1 deletion docs/specs/mobile-terminal-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ Event routing, by mode:
alone so it still reaches the terminal, and real mouse pointers fall through
untouched. (rationale)

**Never treat a portaled descendant as pane content**: a press outside the
host's DOM starts no touch mode, tap, or keyboard dismissal.

**Must release a tracked Mouse-mode press on pointerup or cancel even after
leaving Mouse mode.** Cancellation releases on the last target.

Expand All @@ -110,7 +113,7 @@ the pointer, and **must never reach xterm or the pane** for focus, selection,
or pane interaction. **Non-primary mouse buttons
are ignored**, so their browser or host behavior continues.

Source of truth: `TOUCH_MODES` and `paneMouseOverride` in
Source of truth: `TOUCH_MODES`, `paneMouseOverride`, and `isPortaledTarget` in
`lib/src/components/MobileTerminalUi.tsx`; per-pane wiring in
`lib/src/remote/pocket-app/PocketWall.tsx` and
`website/src/components/PocketTerminalExperience.tsx`.
Expand Down
Loading
Loading