Skip to content

Reopen closed Surfaces, Workspaces, and windows (⌘⇧T / u / dor reopen) - #967

Merged
nedtwigg merged 13 commits into
mainfrom
reopen-build
Oct 3, 2026
Merged

nedtwigg merged 13 commits into
mainfrom
reopen-build

Conversation

@nedtwigg

@nedtwigg nedtwigg commented Oct 3, 2026

Copy link
Copy Markdown
Member

Builds Scope: reopen from docs/specs/reopen.md (#963). A close now confirms only when Reopen can't restore it. Everything that closes without asking goes onto a per-Window reopen stack: in memory only, newest first, capped at 20.

What changed for the user

  • Close without asking: clean builtin:file and builtin:folder viewers, iframe browsers, and Workspaces or windows that hold only those plus untouched shells. This fixes the "confirm to close a clean dor open file.txt" annoyance. Before this, every browser close confirmed.
  • Reopen brings back the newest entry:
    • macOS: File → Reopen Closed (⌘⇧T), a native menu item, so it fires in passthrough.
    • Every platform: command-mode u, and dor reopen (window.reopen). u is removed from the kill-confirm letters.
    • Never Ctrl+Shift+T on Windows or Linux, where programs receive it as Ctrl+T.
    • A reopened Surface gets a new surface:N.
    • A Door closed without asking reopens as a Door.
    • An empty stack answers "Nothing to reopen".
  • Workspace close records the whole Workspace; reopen makes a new Workspace at the same tab slot.
  • Closing one window of several hands Rust a snapshot (Rust keeps 20 in memory); reopen opens a new window from it. If opening fails, the record is put back.
  • Still asks: a touched or running shell, any other Tool, agent-browser and Playwright, and a Workspace or window containing one of those.
  • A trivial close leaves no record (an untouched blank shell), so reopen reaches past it.

Prerequisite fixes (shells that could close without asking)

  • dor send and paired-phone (Client) input now mark a shell touched. Client input reaches the webview as terminal:clientInput from the sidecar, Rust and VS Code.
  • An untouched shell that is running confirms.
  • An untouched shell whose helper holds input, or hasn't been checked yet, confirms.
  • Workspace close confirms when any member would, replacing "any Tool counts as touched".
  • A window close confirms when a Workspace's Wall hasn't mounted.
  • A Tool persists only a user rename as its title. A reopened or cold-restored viewer had been titled with its viewer URL.

Specs

Scope: reopen is promoted above the fold in reopen.md, with a new reopen.rationale.md. The promotion checklist edits are in layout (Kill confirmation, Workspace lifecycle), shortcuts, the glossary verb table, dor-cli (dor reopen), standalone (Per-window close), transport, dor-tool and dor-tools-builtin. Scope: delayed-kill stays under Future, and its PR follows this one.

Test plan

  • lib vitest (5540), standalone vitest (277), cargo (120), vscode-ext (180), dor (246), dor-tools-builtin (46), standalone scripts (30); tsc in lib, standalone and vscode-ext; spec-lint
  • Key new tests checked by mutation: each goes red with its fix removed, including the code-review fixes
  • Sidecar node tests: 196 pass. 5 fail locally, looking environmental (they flag staged dor-cli/dist build output and the macOS system bash), and are not confirmed on main.
  • Live in a Dormouse Dev bundle:
    • ⌘⇧T fires while a terminal has focus in passthrough.
    • iframe, clean viewer, Workspace and torn-out window each close without asking and reopen. A reopened iframe gets a fresh proxy lease.
    • A touched shell confirms on pane close and on window close.
    • dor reopen on an empty stack answers "Nothing to reopen".
    • A reopened builtin:file keeps its name.

🤖 Generated with Claude Code

nedtwigg and others added 12 commits October 3, 2026 04:01
dor send and a remote Client's input now clear `untouched` (the sidecar and
VS Code host tell the webview with terminal:clientInput). An untouched shell
that is running, or whose helper holds user input or has not been answered
for, confirms. The Workspace close asks whenever any member's own close would
(closeKind), replacing hasTouchedSurfaces || runningCount.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A reopenable close pushes a record (persistable leaf meta, the PersistedPane
projection, the Pane's restore token or the Door's slot, the Workspace id)
onto a per-Window stack in memory, newest first, capped at 20. Reopen pops
it and rebuilds the Surface through the cold-restore per-pane path, now
extracted as restorePane, with a new surface:N ref; a Surface whose
Workspace closed lands in the active one. Command-mode u, the macOS File
menu's Reopen Closed (Cmd+Shift+T, macOS only), and dor reopen
(window.reopen) take it; an empty stack shows a brief notice or refuses.
No kind is reopenable yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…asked

An iframe browser, a running builtin:folder, and a running builtin:file that
has reported clean now close without the letter prompt and push a reopen
record; every other Tool, agent-browser and playwright browser, and any shell
with work still confirms. A read-only builtin:file view reports clean as it
starts, since it never can change; an editor reports once it loads. A
non-confirming kill on a Door closes it as a Door, handing selection to its
next neighbor, so it reopens on the Baseboard.

Before this, every browser close confirmed (no `untouched` notion).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The VS Code adapter importing terminal-lifecycle for it pulled the whole
terminal lifecycle into the platform module's graph, which broke tests that
mock the platform. It only touches a registry entry, so it lives in
terminal-store now; terminal-registry still re-exports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Workspace close that asks nothing — every member trivial or reopenable —
records the Workspace whole (its record with reported cwds, name, and strip
slot) once it is gone; its members push no records of their own. Reopen
brings it back as a new Workspace at that slot through cold restore, every
Surface with a fresh id and refs starting over. One member whose close would
ask makes the close confirm and leaves no record. dor workspace close's
refusal now says the Workspace holds Surfaces Reopen cannot restore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A window close asks when any of its Workspaces' closes would, instead of on
running commands alone. One that asks nothing hands Rust its snapshot —
every Workspace and Surface given a fresh id — before removing it from disk;
Rust keeps up to 20 in memory, newest first. Reopen asks the host first:
the newest closed window that closed after this Window's own newest record
is written under a new label and opened as a new window, restoring like any
other. Otherwise the Window's own stack answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Tool row persisted its leaf title, which the iframe layer sets to the
URL it shows. Restore and Reopen apply a persisted title as the user's, so a
reopened or cold-restored builtin:file read "localhost:51526/…/view" instead
of the file name its viewer announces. Only a user rename persists now; pane
and Door alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reopen's command-mode u must never answer a confirmation staged for a
different close.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reopen.md now states the built rule, kinds table, record, verb, and
Workspace/window behavior in the present tense with their pointers; the
remaining Future is Scope: delayed-kill. Its decisions move to a new
reopen.rationale.md. The promotion checklist edits: layout.md Kill
confirmation (which closes skip it, the counted inputs, u out of the
alphabet) and Workspace lifecycle; shortcuts.md (u, Cmd+Shift+T); the
glossary's reopen verb; dor-cli.md's dor reopen; standalone.md's Per-window
close and File menu; transport.md's terminal:clientInput; dor-tool.md's Tool
title persistence; dor-tools-builtin.md's clean report.

Budgets: reopen.md ratchets down to 1200. dor-cli (+100), dor-tool (+50),
dor-tools-builtin (+50), glossary (+50), and shortcuts (+50) grow by the new
verb, row, and rules they must now carry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- ReopenResponse is a discriminated union reopenClosed returns directly,
  replacing ReopenOutcome, its mapping, and the CLI's fallbacks.
- withFreshSurfaceIds mints its own ids; window-reopen no longer exports one.
- Wall: selectAfterKill shared by the pane and Door kill paths, insertDoorAt
  shared by split and reopen, and the record's placement built once.
- helperMayBeBusy shared by the running count and closeKind.
- serializeReported filters once and reuses cwdSurfaceIds.
- A window-close dialog says Reopen cannot bring the window back only when
  that is why it asks (unreopenable on the intent), not for a pending update.
- Drop the unused countRunningSessionsIn re-export and its stale comment;
  reopenClosedWindow no longer sits under transferWorkspace's doc comment.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
# Conflicts:
#	docs/specs/layout.md
#	docs/specs/reopen.md
- reopen_closed_window puts the record back, and removes a snapshot it wrote,
  when opening the window fails; the webview falls back to its own records
  when the host call rejects.
- A window close asks when a Workspace's Wall has not mounted to say what it
  holds, rather than closing past its running work.
- A Surface already fading out is not recorded a second time by a later kill.
- A reopened preview viewer comes back pinned, never a second preview slot.
- dor reopen answers "still mounting" when no Wall can take the record,
  keeping it, rather than "Nothing to reopen".
- A gesture close that skips the confirmation passes `ask`, so a Tool that
  turns dirty meanwhile prompts instead of refusing in silence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: c0c978c
Status: ✅  Deploy successful!
Preview URL: https://032a54b9.mouseterm.pages.dev
Branch Preview URL: https://reopen-build.mouseterm.pages.dev

View logs

@dormouse-bot dormouse-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI has not run on this head. The branch conflicts with main in scripts/spec-word-budgets.json, and the CI workflow doesn't start on a conflicting PR, so only Cloudflare Pages and this review have reported. Once the budget conflict is resolved (re-ratchet the touched specs against main's numbers), CI will run, and this review can become an approval if it passes. The code review itself found nothing to change.

# Conflicts:
#	scripts/spec-word-budgets.json
@nedtwigg
nedtwigg merged commit ac1d347 into main Oct 3, 2026
15 of 16 checks passed

This branch is waiting to be deployed

1 waiting deployment
hosted-preview — c0c978c0 Waiting Oct 3, 2026 by nedtwigg via deploy #937
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants