Skip to content

Workspaces 4/10: dor workspace verbs - #617

Merged
nedtwigg merged 18 commits into
workspaces-multiwindowfrom
workspaces-dor
Sep 15, 2026
Merged

nedtwigg merged 18 commits into
workspaces-multiwindowfrom
workspaces-dor

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

dor workspace new|rename|close|switch, dor list --workspaces|--all, --workspace on every action command, the VS Code refusal of Workspace-spanning requests.

Based on workspaces-multiwindow.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u

nedtwigg and others added 9 commits September 10, 2026 04:24
Every fresh Window installed the lib's `DEFAULT_WORKSPACE_ID`, so a second
window opened after the first one closed wrote a blob naming a Workspace id
that is already live in another window's blob; a relaunch then met the same id
twice and the whole restore was refused. A fresh standalone Window now mints
its own id; a bare Wall — one Window's whole application — keeps the default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
Adds `WORKSPACE_CONTROL_METHODS` beside the Surface methods, the
`surface.list` scope param, and the window-level handler behind them: one row
per Workspace with its union status, background create, rename/switch/close
resolving `workspace:<n|name>` (a name only when exactly one Workspace carries
it), and a close that refuses running or touched work unless forced rather
than raising a prompt no caller can see.

Routing grows two steps: `--workspace` now resolves names, and a target named
by its stable Surface id is answered by whichever Workspace holds it, since
that handle — unlike `surface:N` — is unique across the Window.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
`dor workspace new|rename|close|switch` mutates; `dor list` keeps every read:
`--workspace <ref>` narrows to another Workspace, `--all` groups every
Workspace's Surfaces under a header, and `--workspaces` prints the overview.
Every action command gains `--workspace <ref>`, including the `dor ab`
passthrough, which intercepts it beside the identity flags.

One command with a leading action rather than a route map: the published CLI
reference renders one help page per top-level command, and a nested one would
have no page of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
Each Workspace is its own webview there, so a `--all` listing would report one
webview's Workspace as the whole Window and a container verb would move a strip
that does not exist. The extension host — the only side that knows how many
webviews it holds — answers them with what VS Code can and cannot do, and
leaves every other request untouched.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
dor-cli.md gains a `dor workspace` section and the promoted Handle Model rules:
the name handle and its ambiguity error, the routing order including the
Window's own verbs and stable-id targeting, `surface.list`'s scope, and
`--workspace` on every action command. Its two staged bullets are gone, and
cross-Window targeting — still reserved — is now the Future item the Reserved
line points at.

The workspaces-rollout scope is empty, so its ledger and every reference to it
are retired; layout.md keeps the Workspace model, and vscode.md and
standalone.md record the refusal and the minted first Workspace id.

Budgets ratcheted: dor-cli 5450, standalone 8350, vscode 7450.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
The flag names a Workspace rather than a browser, so it must reach the two
control calls and never the agent-browser binary, which knows nothing about it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
Moves each rule to one home. `dor/src/protocol.ts` gains the wire-only
predicates the whole control plane shares — `isWorkspaceControlMethod`,
`spansWorkspaces`, `parseWorkspaceRef` — so the router, the `workspace.*`
handlers, and the VS Code guard read one enumeration and one ref grammar; the
guard becomes "this host serves positional 1, nothing that spans".

Caller identity is rewritten at the seam that knows it: `dispatchDorControl`
drops a `surfaceId` the answering Wall does not hold, so every `surface:self` /
implicit-target consumer is correct by construction rather than re-deriving it.
One `classifySurfaceTarget` beside the matcher replaces the router's private
copy of the target grammar, and the Wall's `DorControlParams` sheds the
Window-level params it never read.

Elsewhere: one `WorkspaceScopedRequest` / `WorkspaceScopedFlags` behind the
per-command copies, `workspaceFlag` / `workspaceParam` at every call site, one
`ACTIONS` table for `dor workspace` in place of four parallel enumerations, one
tag trailer shared by both listings, a `switch` with an exhaustiveness check
over the container verbs, a parallel `surface.list --all` fan-out, and
`resolveWorkspaceRef` / `installWindowPersistence` answering with what their
callers were reading back out of the store.

Behavior is unchanged; the one help edit is `dor list --workspace` taking the
shared flag's wording.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
Ten confirmed findings on the `dor` Workspace CLI, each pinned by a test:

- `dor ab --key` is Workspace-scoped for real. `sessionForKey(key, workspaceId)`
  namespaces a managed key under the Workspace that holds the browser, and the
  CLI asks the host for the name (`surface.resolveAgentBrowser` with `key`)
  rather than building `dormouse.1.<key>` itself. A bare Wall — VS Code, the
  website — keeps the unscoped names, and so does a `dor ab` with no control
  endpoint at all.
- `dor list --all` marks only the active Workspace's selection focused; every
  Wall marks its own, and the Window has one focus.
- A refused Workspace close reveals the Workspace only in `prompt` mode: a
  `dor workspace close` gets the message and leaves the user where they were.
- `--all` fails the listing on a Workspace whose Wall never registers, after the
  routing retry, instead of dropping it; the text renderer keeps every
  Workspace's header so it agrees with the JSON `workspaces` array.
- `workspace.close` refuses a Workspace with no registered Wall rather than
  dropping it with its Sessions still running.
- An explicit `--workspace` whose Wall has not registered waits out the same
  retry and then answers "still mounting", not "unknown workspace target".
- `--all --ports` scans once for the whole Window: a batched `getOpenPortsMany`
  through the sidecar (one process table, one socket scan), Rust, both
  standalone adapters, the browser-dev bridge, and an optional adapter method
  with a per-id fallback.
- `dor workspace`'s CLI test drives one client across all four verbs and asserts
  the whole ordered conversation.
- The VS Code guard accepts its own Workspace by name as well as by position.
- notepad.md's Closure section carries the Workspace close path, prompt vs
  silent.

Plus the cleanups: `workspaceRows()` goes through `workspaceRefFor`, `dor list
--workspaces` refuses by allowlist so a new flag is refused by default, and
standalone.md cites `window-restore.test.ts` by its full path.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QBf4rQbwuxV6n6v7E3twYe
Carries origin/main (#611, #612), the hidden-Workspace minimize, the
repeat-quit vote fix, and the arrival deadline up the stack. Only the spec
word budgets conflicted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PkPyEFCxiPo5UFeju5Ya9u
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 10, 2026

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9e3b0ec
Status: ✅  Deploy successful!
Preview URL: https://83abb09d.mouseterm.pages.dev
Branch Preview URL: https://workspaces-dor.mouseterm.pages.dev

View logs

@nedtwigg
nedtwigg added this pull request to stack #624 September 10, 2026 21:44

@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.

Feedback on work in progress — not a merge verdict. The routing rewrite, the --all fan-out and the VS Code guard all land with tests I'd have asked for, and pnpm lint:specs / lint:public-docs pass on the head once AGENTS.md is read from the PR rather than the base-restored worktree copy. Five points are inline; two spec-drift items sit outside the diff hunks and are below.

docs/specs/layout.md corner case #8 now contradicts the code and notepad.md. It still reads "A refused close reveals its Workspace: a closeAll that returns a refusal activates that Workspace" — unconditional — while revealForPrompt in lib/src/components/wall/workspace-lifecycle.ts reveals only in prompt mode. docs/specs/notepad.md got the qualified version in this PR; layout.md did not, and the corner-case list is the one whose "numbers are stable" for cross-spec citation. The same section's close-refusal enumeration ("the last remaining Workspace cannot be closed", "One close runs at a time") also predates the new NO_WALL_REFUSAL, which the strip and the command-mode & can now hit — it is documented only in dor-cli.md's close row, i.e. in the spec that does not own the strip path.

docs/specs/dor-tool.md links a scope this PR deleted. Its "Dehydrate and rehydrate" section points at "docs/specs/layout.md → Future, workspaces-rollout", and **Scope: workspaces-rollout** is gone from layout.md's ## Future as of this PR. It is the only remaining hit in docs/, and spec-lint's scope check does not catch it: SCOPE_REF_RE matches only the bolded forms, so this prose reference reads as fine forever. docs/specs/layout.md → Workspaces is the live target.

Comment thread standalone/sidecar/pty-core.js Outdated
Comment thread dor/src/commands/agent-browser.ts
Comment thread lib/src/components/wall/workspace-lifecycle.ts Outdated
Comment thread dor/src/commands/list.ts Outdated
Comment thread dor-lib-common/src/agent-browser.ts Outdated
nedtwigg and others added 2 commits September 10, 2026 17:38
…ough the registration gap

Review findings on the workspaces-dor branch:

- macListeningPorts threw away the stdout lsof printed before a non-zero exit,
  so one descendant exiting between `ps` and `lsof` emptied a whole Window's
  `dor list --all --ports`. It now parses `err.stdout`, the shape
  getCwdsForPids already documents. The batched `pty_get_open_ports_many`
  reused the per-terminal 3 s for the whole Window while the sidecar spends it
  twice serially; it now waits both scans plus `OPEN_PORT_TIMEOUT_PER_ID_MS`
  per id, with the sidecar's socket scan capped to match. The new constant is
  mirrored TS/sidecar/Rust and pinned by mirrored-constants.test.ts.
- The router left a request with no Wall mounted to the client's 5 s deadline,
  which every managed `dor ab` now pays. After the bounded retry it answers
  the mounting refusal for the active Workspace; `dor ab` reports it and exits
  non-zero without running the binary, and there is deliberately no fallback
  to a CLI-namespaced key (it would name the wrong Workspace's browser).
- The strip's `×` / command-mode `&` swallowed the no-Wall refusal and did
  nothing. requestWorkspaceClose now waits out the registration gap the way
  `dor workspace close` does, and the refusal is one wording everywhere
  (`mountingRefusal`; NO_WALL_REFUSAL is gone).
- `dor list --all --json` adds caller_workspace_ref / focused_workspace_ref,
  since the `_surface_ref` pair names a `surface:N` every Workspace has.
- sessionForKey scrubs the key like the scope: it arrives over the control
  socket from clients other than `dor`.
- Specs: layout.md corner case #8 reveals only in `prompt` mode and the close
  refusals list the no-Wall case; dor-tool.md links the live Workspaces section
  instead of the deleted workspaces-rollout scope; dor-browser.md states that
  every managed `dor ab` depends on the host answering.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus
Carries the review fixes from the earlier stages up the stack; the two
mirrored-constants blocks that met here are kept side by side.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RChsJ5rMUMyfu22UZDfUus

@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.

Feedback on work in progress — not a merge verdict; mark the PR ready when you want the full one. Every finding from the previous round is genuinely closed: the lsof stdout salvage, the batched budget, the router answering the no-Wall case, the --all workspace pointers, and the key scrub all land with tests, and the layout.md / dor-tool.md spec drift is fixed (pnpm lint:specs and lint:public-docs pass on the head once AGENTS.md is read from the PR rather than the base-restored worktree copy). Two new points, both inline.

The first is a hard failure: the new sessionForKey scrub assertion does not match what the function returns, so pnpm -r run test (which builds dor-lib-common first) goes red. Build & Test is still pending at 70de2a3, so this hasn't surfaced yet.

Two smaller things, neither worth a change on its own:

resolveDorControlRoute's pending and none variants are now handled identically at their only consumer — dor-control-router.ts does if (route.kind === 'none' || route.kind === 'pending') and then reads route.message from either. Nothing outside the tests distinguishes them any more. Worth collapsing them, or keeping both deliberately because the provenance (a named Workspace vs the active fallback) is what a future consumer would branch on.

docs/specs/standalone.md → "Open ports" states the batch budget unconditionally — "the socket scan runs under OPEN_PORT_TIMEOUT_MS + OPEN_PORT_TIMEOUT_PER_ID_MS × ids" — but only macListeningPorts reads scanTimeoutMs. The Windows branch still passes a flat timeout: OPEN_PORT_TIMEOUT_MS to its PowerShell call and Linux reads /proc with no subprocess at all. No bug follows (the Windows scan's cost doesn't grow with the batch — Get-NetTCPConnection lists every connection regardless), but the rule as written is macOS-only, and the sidecar's own comment on OPEN_PORT_TIMEOUT_PER_ID_MS reads the same way.

Comment thread dor-lib-common/test/agent-browser.test.mjs Outdated
Comment thread standalone/src-tauri/src/lib.rs Outdated
# Conflicts:
#	docs/specs/layout.md
#	lib/src/components/wall/workspace-lifecycle.test.ts
#	lib/src/components/wall/workspace-lifecycle.ts

@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.

Feedback on work in progress — not a merge verdict; mark the PR ready when you want the full one.

One new point inline: the deadline this commit matched to both scans is matched only on the Tauri side. The VS Code host runs the same pty-corepty-host.js requires a shim re-exporting standalone/sidecar/pty-core.js — behind a flat 3 s at two boundaries, so the pane header there can still abandon a scan the host is about to answer. A smaller note on the 1000 literal is inline too.

Comment thread lib/src/lib/platform/types.ts
Comment thread standalone/src-tauri/src/lib.rs Outdated

@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.

Feedback on work in progress — not a merge verdict; mark the PR ready when you want the full one.

Both prior points are closed: pty_get_open_ports now takes open_ports_many_timeout(1), the VS Code child and webview boundaries carry the derived deadline, and the 1000 is a named constant with a mirror pin. The two new tests discriminate the change they pin — 6500 ms fails at hops = 0, 7500 ms fails at hops = 1. pnpm lint:specs and lint:public-docs pass on the head (reading AGENTS.md from the PR rather than the base-restored worktree copy).

Four points inline, the first about the rule itself rather than the arithmetic.

Comment thread docs/specs/transport.md
Comment thread docs/specs/dor-cli.md
Comment thread lib/src/lib/mirrored-constants.test.ts
Comment thread standalone/src-tauri/src/lib.rs

@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.

Feedback on work in progress — not a merge verdict; mark the PR ready when you want the full one.

The deadline gap from the last round is closed on the arithmetic: all three Windows subprocesses now draw on one openPortScanTimeoutMs(count), so the worst case is 2 × OPEN_PORT_TIMEOUT_MS + count × OPEN_PORT_TIMEOUT_PER_ID_MS on every platform, matching what docs/specs/transport.md → "Port scan deadlines" now states. Both new tests discriminate the change — the budget test pins the exact remaining-time sequence [3000, budget, budget − 500, budget − 1500], and the exhaustion test pins that netstat is never spawned. node --test standalone/sidecar/pty-core.test.js is green (101/101), and pnpm lint:specs passes on the head once AGENTS.md is read from the PR rather than the base-restored worktree copy.

One point inline is about what sharing the budget does to the fallback order; the second is a naming collision.

One smaller thing, outside the hunks: the netstat call in windowsListeningPorts still passes timeout: OPEN_PORT_TIMEOUT_MS in its options literal, which execFileSyncFn now always overwrites with remaining. It is the one place a reader would look to check the new rule, and it reads as the old flat 3 s — worth deleting so the options object doesn't contradict the comment above it.

Comment thread standalone/sidecar/pty-core.js Outdated
Comment thread standalone/sidecar/pty-core.js Outdated

@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.

Feedback on work in progress — not a merge verdict; mark the PR ready when you want the full one.

The reorder closes the ordering point from the last round, and both tests discriminate it: the budget test's [3000, budget, budget − 1000, budget − 1500] sequence only holds if Get-NetTCPConnection draws on the scan allowance first, and the new test pins that a name lookup consuming the entire remainder still returns the port that was already enumerated. node --test standalone/sidecar/pty-core.test.js is green (102/102), and pnpm lint:specs passes on the head once AGENTS.md is read from the PR rather than the base-restored worktree copy.

One point inline, about what the reorder left behind at the two parse calls.

Comment thread standalone/sidecar/pty-core.js
# Conflicts:
#	scripts/spec-word-budgets.json
@nedtwigg
nedtwigg marked this pull request as ready for review September 14, 2026 22:29

@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.

Out of draft, so here is the verdict the draft rounds deferred: comment, not
approve.
All four threads from the last three rounds are still open at
9e3b0ec, and the code they point at is unchanged — the only commit since
99161e1 is the workspaces-multiwindow merge.

Nothing new surfaced in this pass. I re-read the routing rewrite, the
workspace.* verbs, the --all fan-out, the VS Code guard and the port-scan
deadline arithmetic end to end. node --test standalone/sidecar/pty-core.test.js
is green (102/102) and lint:public-docs passes on the head; lint:specs is
clean apart from two standalone/sidecar/node_modules path checks that need a
pnpm install this checkout does not have.

What holds the approval, most consequential first:

  • docs/specs/dor-cli.md → "Port enumeration is opt-in" still caps the scan
    at OPEN_PORT_TIMEOUT_MS. The Windows reorder made that wrong on both
    scanning platforms rather than just macOS — windowsListeningPorts derives
    its deadline from runtime.scanTimeoutMs now too, so this is the one number
    in the diff a reader would take and be wrong about.
  • windowsListeningPorts threads nameByPid into both parse calls where it
    can only be empty, and the netstat call lost the comment recording why
    windowsHide is load-bearing there.
  • The budget formula's two hand-written copiesopenPortRequestTimeoutMs
    and open_ports_many_timeout — plus the 1000 restated as prose in that
    function's doc comment, are unpinned: docs/specs/transport.md names both as
    Source of truth: for one formula and nothing goes red if they diverge.

Three of the four carry a single-line suggestion on their thread.

@dormouse-bot

Copy link
Copy Markdown
Collaborator

All four findings are resolved in #630 at 3c5fc72c, verified file by file against that commit. Nothing in them holds this PR from my side.

The review I posted four minutes after your replies was composed against 9e3b0ec and read the four threads as open because the fixes live downstream in the train, not on this branch. The three blockers it lists are exactly these — that verdict is stale, disregard it.

What I checked on workspaces-transfer-fixes:

  • docs/specs/dor-cli.md — "Port enumeration is opt-in" no longer names a cap, and docs/specs/transport.md → "Port scan deadlines" carries the contract with the full formula, the per-hop counts, and lib/src/lib/mirrored-constants.test.ts named as a pin. That is the right owner for it.
  • The budget formula's two hand-written copies — the new port request deadline derivation mirrors case parses open_ports_many_timeout's actual Duration::from_millis expression and evaluates its terms instead of restating the formula, so a change to one side the other does not follow goes red. I traced it against the current Rust source rather than running it (no pnpm install in this checkout): the expression strips as u64, resolves to the three named constants, and reproduces openPortRequestTimeoutMs(count) at its default hops = 1. An unknown term throws, so the pin cannot silently weaken to a tautology — which is the failure mode a mirror test of this shape usually has.
  • standalone/src-tauri/src/lib.rs — the doc comment reads plus OPEN_PORT_ROUND_TRIP_MARGIN_MS for IPC in place of the hand-written 1 s.
  • windowsListeningPortsnameByPid is now constructed beside its population, both parse calls dropped the argument, and the windowsHide console-allocation comment is back on the netstat call.

@nedtwigg
nedtwigg merged commit 43c4a1d into main Sep 15, 2026
33 of 34 checks passed
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