From e797b0c82ce15647de9c0f72295a776666721c37 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Mon, 2 Nov 2026 22:21:27 -0800 Subject: [PATCH 1/3] Remove the Terminal Context autorun Ned's call: typing `git status` is no burden, and the setting confused more than it helped. A helper now starts its shell and runs nothing, which also retires the one accepted risk it carried: a saved command running without asking in the source's directory. Gone: the Modify control and its dialog, the command on HelperIdentity (spawn, list, restore), the sidecar's preference file and validation, the shell readiness wait and injection, the helper status machine, and the refresh that replaced an untouched helper on reopening. The Reset slot is now always there, since Modify shared it, and the status row speaks only for an exit. What a helper does now: reopening keeps it as you left it, untouched or not, with the mismatch warning if the source has moved on; only Reset replaces it. The refresh's only other job was following the source, and without a command to re-run it would kill and respawn a shell the user may be looking at. Stored values: nothing reads ~/.dormouse/helper-terminal.json any more, so an installed setting is inert and the file stays where it is rather than keep a migration alive. Live helper metadata is the only other place the command lived; a sidecar keeps only parentId from whatever it is sent. Webview and host ship together, so no build pairs a new webview with an old sidecar. `settings` becomes `home` on the context wire, the only thing it still answers. A helper's start directory keeps its rule, since a terminal's output should not choose where a shell opens. S167 keeps its git and launch-write duty and now says helper-terminal.ts writes nothing; S132 loses the watchHelper write. Co-Authored-By: Claude Opus 5.5 --- .github/audit/local.md | 2 +- AGENTS.md | 2 +- TESTING_AND_MODIFICATION_GUIDE.md | 2 +- docs/specs/dor-tool.rationale.md | 2 +- docs/specs/mouse-and-clipboard.md | 2 +- docs/specs/security-local.md | 6 +- docs/specs/security-local.rationale.md | 2 +- docs/specs/security.md | 1 - docs/specs/terminal-context.md | 29 +-- docs/specs/transport.md | 2 +- lib/src/components/Wall.test.tsx | 16 +- lib/src/components/WorkspaceWindow.test.tsx | 2 +- lib/src/components/WorkspaceWindow.tsx | 2 +- .../components/wall/TerminalContext.test.tsx | 58 +++--- lib/src/components/wall/TerminalContext.tsx | 9 +- .../components/wall/TerminalContextView.tsx | 39 ++-- lib/src/components/wall/close-kind.test.ts | 2 +- .../wall/dor-control-router.test.ts | 2 +- lib/src/components/wall/preview-slot.test.tsx | 2 +- lib/src/components/wall/window-reopen.test.ts | 2 +- lib/src/host/remote/sidecar-entry.test.ts | 2 +- lib/src/lib/helper-terminal.test.ts | 173 +++++++----------- lib/src/lib/helper-terminal.ts | 95 +++------- lib/src/lib/mirrored-constants.test.ts | 27 --- .../lib/platform/fake-adapter-helper.test.ts | 10 +- lib/src/lib/platform/fake-adapter.ts | 11 +- lib/src/lib/reconnect.test.ts | 10 +- lib/src/lib/terminal-context-types.ts | 12 +- lib/src/stories/HelperPlacement.stories.tsx | 21 +-- lib/src/stories/TerminalContext.stories.tsx | 42 ++--- lib/src/stories/settle-terminals.ts | 14 +- scripts/spec-word-budgets.json | 6 +- standalone/sidecar/helper-terminal.test.js | 56 ++++-- standalone/sidecar/pty-core.js | 33 +--- standalone/src/window-restore.test.ts | 8 +- vscode-ext/src/message-router.ts | 2 +- vscode-ext/test/message-router.test.ts | 2 +- website/src/lib/playground-shells.test.ts | 2 +- 38 files changed, 266 insertions(+), 444 deletions(-) diff --git a/.github/audit/local.md b/.github/audit/local.md index c37238604..85cc60365 100644 --- a/.github/audit/local.md +++ b/.github/audit/local.md @@ -12,7 +12,7 @@ For `## Loopback Listeners`, read `lib/src/host/loopback-guard.ts` first — it For `## Network policy`, read `docs/specs/remote-network.md` -> "Policy", then `lib/src/remote/network-policy.ts`, the policy half of `lib/src/host/remote/service.ts` (the transport guard, and each level's hold on the Burrow), and its two other choke points, `lib/src/host/managed-voice-host.ts` and `standalone/src/updater.ts`. Then run `node scripts/outbound-lint.mjs` and read its allowlist, `OUTBOUND_SITES` and `REMOTE_LITERALS`: trace each `guarded:` file to its choke point and each `user:` file's call to the trigger its class names, and check that each `loopback`, `local-ipc`, `phone`, and `dev-only` file is what its reason says. Its `FORMS` is textual, so also search the shipped directories it scans for a network reach it cannot spell — a primitive through a variable, a re-export, or a new dependency. Read `lib/src/host/remote/outbound.test.ts` and `vscode-ext/test/outbound.test.ts` for what they boot and send, and judge each level's sentence and `connectionsFor` row in `lib/src/components/NetworkSettings.tsx` against what the code opens, the STUN and phone rows included, which those suites do not drive. -For `## Terminal context directory actions`, read `docs/specs/terminal-context.md` and `docs/specs/layout.md` -> "Workspace names"; for `## Spawned programs`, `docs/specs/terminal-context.md` -> "Helper lifecycle" and "Global autorun setting", and `docs/specs/dor-browser.md` -> "Pane Context Menu Connect" and "Browser Host"; for `## Dor Tool configuration`, `docs/specs/dor-tool.md` -> "Declaring tools", "Trust", and "Serving"; then the code each section's `Source of truth` names. +For `## Terminal context directory actions`, read `docs/specs/terminal-context.md` and `docs/specs/layout.md` -> "Workspace names"; for `## Spawned programs`, `docs/specs/terminal-context.md` -> "Helper lifecycle", and `docs/specs/dor-browser.md` -> "Pane Context Menu Connect" and "Browser Host"; for `## Dor Tool configuration`, `docs/specs/dor-tool.md` -> "Declaring tools", "Trust", and "Serving"; then the code each section's `Source of truth` names. For the rest of `docs/specs/security-local.md`, read each section's owner first — `docs/specs/terminal-escapes.md` and the owners its rows name, `docs/specs/dor-tool.md` -> "Reaping", "Terminal links", and "Opening local files", `docs/specs/dor-browser.md` -> "Automated Browser", "Iframe Renderer", and "Iframe Host Capability And CSP", `docs/specs/dor-cli.md` -> "Spawning External Binaries" and "Host Plumbing", `docs/specs/vscode.md` -> "Webview message authentication" and "Burrow: a service in the extension host", `docs/specs/standalone.md` -> "Persistence", `docs/compatible-agents.md` -> "Recovery contract (maintainers)" — then the parser, the iframe shim, the control-socket code, the persistence paths they point at, and the files the checklist below names. diff --git a/AGENTS.md b/AGENTS.md index 4832f8df9..9a3b12e4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,7 +54,7 @@ A spec is the accurate reference for the current code: it states the invariants - **`docs/specs/tiling-engine.md`** — **Lath**, the in-house headless tiling engine: pure split-tree core, never-re-parent LathHost adapter, wall store + engine, Lath-only persistence. - **`docs/specs/alert.md`** — The Activity layer: alert tracks, attention model, TODO lifecycle, notification protocols with their sanitization rules, the Workspace union projection. - **`docs/specs/terminal-state.md`** — Per-Session semantic state: CWD, prompt/command lifecycle, title candidates and header derivation, grouping keys. -- **`docs/specs/terminal-context.md`** — Unified terminal context and helper terminal: lifecycle, promotion, source closure, and global autorun settings. +- **`docs/specs/terminal-context.md`** — Unified terminal context and helper terminal: lifecycle, promotion, and source closure. - **`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. A rule both hosts implement identically lives here, and each host spec points at it. diff --git a/TESTING_AND_MODIFICATION_GUIDE.md b/TESTING_AND_MODIFICATION_GUIDE.md index f7a95658a..8b2fcfb5e 100644 --- a/TESTING_AND_MODIFICATION_GUIDE.md +++ b/TESTING_AND_MODIFICATION_GUIDE.md @@ -94,7 +94,7 @@ The Hosted walkthroughs in `docs/stories/` join app and account stories. Hosted Snapshot clock: `lib/.storybook/snapshot-clock.ts` freezes `Date.now` and `new Date()` at 2026-01-15 12:00 UTC for an Argos run, before any story module loads, so a story reads `Date.now()` anywhere, a `parameters` literal included, and never pins its own clock. -Helper placement snapshots: defer the fake helper's first prompt until its terminal is mounted and fitted. Drain queued xterm writes before capture and assert one autorun echo, both complete prompts, and a viewport at the bottom. Output emitted at the initial wide grid can lose the current prompt line when xterm shrinks; waiting afterward cannot repair it. `lib/src/stories/HelperPlacement.stories.tsx` owns the gate and `lib/src/lib/platform/fake-adapter-helper.test.ts` covers deferred startup and narrow wrapping. +Helper placement snapshots: defer the fake helper's first prompt until its terminal is mounted and fitted. Drain queued xterm writes before capture and assert the one complete prompt and a viewport at the bottom. Output emitted at the initial wide grid can lose the current prompt line when xterm shrinks; waiting afterward cannot repair it. `lib/src/stories/HelperPlacement.stories.tsx` owns the gate and `lib/src/lib/platform/fake-adapter-helper.test.ts` covers deferred startup and narrow wrapping. Help snapshots: after changing any `dor` help text, rebuild and refresh with `cd dor && pnpm build && UPDATE_SNAPSHOTS=1 node --test`, then run `node --test` again and commit `dor/test/snapshots/`. diff --git a/docs/specs/dor-tool.rationale.md b/docs/specs/dor-tool.rationale.md index b7cb2f37f..5ffd2d812 100644 --- a/docs/specs/dor-tool.rationale.md +++ b/docs/specs/dor-tool.rationale.md @@ -46,7 +46,7 @@ Every preview-slot retarget of a serving Tool (the built-in file viewer, `builti ## Lifecycle -The September 2026 integration reuses Terminal Context for the Tool's primary terminal. The auxiliary helper's automatic refresh, Reset, and Promote semantics do not describe a serving command, whose Session also owns the browser and remote terminal identity. Sharing the presentation avoids introducing a second navigation mechanism or a second shell. +The September 2026 integration reuses Terminal Context for the Tool's primary terminal. The auxiliary helper's Reset and Promote semantics do not describe a serving command, whose Session also owns the browser and remote terminal identity. Sharing the presentation avoids introducing a second navigation mechanism or a second shell. ## Reaping diff --git a/docs/specs/mouse-and-clipboard.md b/docs/specs/mouse-and-clipboard.md index 9f13c764d..6dfeb2a1a 100644 --- a/docs/specs/mouse-and-clipboard.md +++ b/docs/specs/mouse-and-clipboard.md @@ -326,7 +326,7 @@ Source of truth: `handleEditableClipboard` in `lib/src/components/wall/keyboard/ **Must give application-captured right-click to the terminal program.** Do not add a Shift-right-click override gesture. -**Must route clipboard chords and selection operations to the focused helper**, while leaving its Escape, Tab, arrows, and digits with xterm. Which of those disarm autorun follows `docs/specs/terminal-context.md` → "Helper lifecycle". +**Must route clipboard chords and selection operations to the focused helper**, while leaving its Escape, Tab, arrows, and digits with xterm. **Must copy selected context diagnostic text with Cmd+C on macOS and Ctrl+C elsewhere.** Copy only a selection contained in the focused diagnostic, retaining it on clipboard failure; helper and editable-field chords keep their own routing. diff --git a/docs/specs/security-local.md b/docs/specs/security-local.md index 947740e3c..de133e12d 100644 --- a/docs/specs/security-local.md +++ b/docs/specs/security-local.md @@ -36,7 +36,7 @@ Notification text: `docs/specs/alert.md` -> "Text And Security". - **FAIL IF** `S129` an `OSC 8` activation reaches an adapter's `openExternal` without the confirmation dialog, the dialog renders an open action for a **deceptive** verdict or a blocked target, `classifyDisplayMatch` reads a respelling of another host as anything but **deceptive** (`docs/specs/mouse-and-clipboard.md` -> "OSC 8 hyperlinks"), or a scheme outside the `STANDARD_SCHEMES` allowlist opens from a dialog not naming it as another app (rationale): `linkHandler` in `lib/src/lib/terminal-lifecycle.ts`, `inspectExternalUri` and `classifyDisplayMatch` in `lib/src/lib/external-links.ts`, the render branches in `lib/src/components/ExternalLinkModal.tsx`. Pinned by `lib/src/lib/external-links.test.ts` and `lib/src/components/ExternalLinkModalHost.test.tsx`; the host also rejects a deceptive confirmation (rationale). - **FAIL IF** `S130` a host launches an external URL without revalidating it through `normalizeExternalUri` in `lib/src/lib/external-links.ts`, consent notwithstanding — VS Code's in the extension host (`vscode-ext/src/message-router.ts`), standalone's in `standalone/src/tauri-adapter.ts` and `openUrl` in `standalone/src/updater.ts` — or that function accepts a scheme in `PSEUDO_SCHEMES` or `REFUSED_APP_SCHEMES`, or the latter drops `ms-msdt`, `search-ms`, `ms-officecmd`, or `ms-appinstaller` (rationale). Pinned by `vscode-ext/test/message-router.test.ts` and `standalone/src/tauri-adapter.test.ts`. - **FAIL IF** `S131` an unconfirmed `OSC 8` activation reaches the preview path when its display text is not a whole-component suffix of its decoded target, or the host opens a `file:` URL whose host is not empty, `localhost`, or this machine: `localFileLinkPreviewPath` in `lib/src/lib/external-links.ts`, `activateTerminalLink` in `lib/src/lib/terminal-link-activation.ts`, `resolveLocalToolTarget` in `lib/src/host/tool-input.ts`. Pinned by `lib/src/lib/external-links.test.ts`, `lib/src/lib/terminal-link-activation.test.ts`, and `lib/src/host/tool-input.test.ts`. -- **FAIL IF** `S132` an OSC 367 `open` reaches `surface.tool` from replay, from a Session that is not a Tool, from output outside the run the host launched in that PTY (`docs/specs/dor-tool.md` → OSC 367), or naming anything but an absolute path free of controls; if anything output reports can begin a run but at the first command start after the host's arm, a reported command line only narrowing which start that is (output inside that window is `docs/specs/security.md` → "What is not defended"); if a write but the host's own command launch can carry `launch`; or if a control-socket request can set its `oscOpen` flag, or its failure reaches the error viewer's argv with a control character. Read `parseToolOpen` in `dor-tools-lib/src/osc.ts`; `ToolLaunchLatch` in `lib/src/lib/tool-launch-latch.ts` and its one user, `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`; the `launch` writes in `typeToolCommand` in `lib/src/components/wall/use-dor-control.ts`, `typeCommandWhenPromptReady` in `lib/src/lib/terminal-lifecycle.ts`, and `watchHelper` in `lib/src/lib/helper-terminal.ts` and each host's arm (`onPtyLaunch` in `lib/src/host/remote/sidecar-entry.ts`, `vscode-ext/src/message-router.ts`); `dispatchToolOpens` in `lib/src/lib/tool-open-requests.ts` (reached from `applyLiveToolEvents`, never from `parseReplay` in `lib/src/lib/platform/replay-parse.ts`); the `oscOpen` argument of `dispatchDorControlRequest` in `lib/src/lib/platform/dor-control-dispatch.ts`; and the `oscOpen` gate in `lib/src/components/wall/use-dor-control.ts`. Pinned by `lib/src/lib/tool-launch-latch.test.ts` and `lib/src/components/wall/preview-slot.test.tsx`. +- **FAIL IF** `S132` an OSC 367 `open` reaches `surface.tool` from replay, from a Session that is not a Tool, from output outside the run the host launched in that PTY (`docs/specs/dor-tool.md` → OSC 367), or naming anything but an absolute path free of controls; if anything output reports can begin a run but at the first command start after the host's arm, a reported command line only narrowing which start that is (output inside that window is `docs/specs/security.md` → "What is not defended"); if a write but the host's own command launch can carry `launch`; or if a control-socket request can set its `oscOpen` flag, or its failure reaches the error viewer's argv with a control character. Read `parseToolOpen` in `dor-tools-lib/src/osc.ts`; `ToolLaunchLatch` in `lib/src/lib/tool-launch-latch.ts` and its one user, `createOwnerPtyStream` in `lib/src/host/owner-pty.ts`; the `launch` writes in `typeToolCommand` in `lib/src/components/wall/use-dor-control.ts` and `typeCommandWhenPromptReady` in `lib/src/lib/terminal-lifecycle.ts`, and each host's arm (`onPtyLaunch` in `lib/src/host/remote/sidecar-entry.ts`, `vscode-ext/src/message-router.ts`); `dispatchToolOpens` in `lib/src/lib/tool-open-requests.ts` (reached from `applyLiveToolEvents`, never from `parseReplay` in `lib/src/lib/platform/replay-parse.ts`); the `oscOpen` argument of `dispatchDorControlRequest` in `lib/src/lib/platform/dor-control-dispatch.ts`; and the `oscOpen` gate in `lib/src/components/wall/use-dor-control.ts`. Pinned by `lib/src/lib/tool-launch-latch.test.ts` and `lib/src/components/wall/preview-slot.test.tsx`. - **FAIL IF** `S269` a document-link `open` — one carrying a disposition, Back/Forward through a Surface's history included — or a terminal link's click runs or focuses a target `builtin:file` would serve as a page running that file's own script, under any handler but `builtin:code` or `builtin:folder`, before the user confirms its full resolved path in host UI the Tool's page cannot reach; or a cancelled, replaced, or aborted confirmation lets it proceed; or `runsOwnScript` misses a format the viewer CSP lets run script. Read `linkOpenNeedsConfirmation` in `lib/src/components/wall/tool-navigation.ts`, `runsOwnScript` in `dor-tools-builtin/src/file-viewer-format.ts`, its call in the `surface.tool` handler of `lib/src/components/wall/use-dor-control.ts`, `activateTerminalLink` in `lib/src/lib/terminal-link-activation.ts`, and `confirmLinkOpen` in `lib/src/lib/external-link-confirmation.ts`. Pinned by `lib/src/components/wall/preview-slot.test.tsx`, `lib/src/components/wall/tool-navigation.test.ts`, and `lib/src/components/ExternalLinkModalHost.test.tsx`. - **FAIL IF** `S133` cold restore types a command `normalizeResumeCommand` in `lib/src/lib/resume-patterns.ts` did not rebuild at the typing boundary (`restoreTerminal` in `lib/src/lib/terminal-lifecycle.ts`), its grammar admits more than the invocations `docs/compatible-agents.md` -> "Detection" names — another argument, whitespace, or a shell metacharacter — or capture scans output from before its first interrupt (`captureAgentRecovery` in `lib/src/host/recovery-capture.ts`). Pinned by `lib/src/lib/coding-agents.test.ts`, `lib/src/lib/terminal-registry.alert.test.ts`, and `lib/src/host/recovery-capture.test.ts`. @@ -202,14 +202,14 @@ Source of truth: `context` in `standalone/sidecar/pty-core.js`; `attachRouter` i | Guarantee | Feature | Pinned by | | --- | --- | --- | -| **A folder you open cannot get Dormouse to run a program it planted.** On Windows no program is spawned by bare name, and git in a repository you have not trusted runs no program its config names. Nor does Dormouse run git in a repository that names a network share. Terminal Context types no command you did not set, and never into a directory only a terminal's output named. | | `standalone/sidecar/pty-core.test.js`, `lib/src/host/git-cli.test.ts`, `vscode-ext/test/pty-manager.test.ts`, `dor-lib-common/test/git.test.mjs`, `standalone/sidecar/helper-terminal.test.js`, `lib/src/lib/helper-terminal.test.ts` | +| **A folder you open cannot get Dormouse to run a program it planted.** On Windows no program is spawned by bare name, and git in a repository you have not trusted runs no program its config names. Nor does Dormouse run git in a repository that names a network share. Terminal Context types nothing into its helper. | | `standalone/sidecar/pty-core.test.js`, `lib/src/host/git-cli.test.ts`, `vscode-ext/test/pty-manager.test.ts`, `dor-lib-common/test/git.test.mjs`, `standalone/sidecar/helper-terminal.test.js`, `lib/src/lib/helper-terminal.test.ts` | | **A browser you open from a terminal's port, or a Tool's Break, reads no project's config.** It runs from a directory Dormouse owns; only your own `dor` command binds a browser to a project. | | `lib/src/components/Wall.test.tsx`, `lib/src/components/wall/tool-run-end.test.tsx`, `lib/src/host/browser-host.test.ts`, `lib/src/host/agent-browser-host.test.ts`, `lib/src/host/playwright-host.lifecycle.test.ts` | The attacker is a file planted in a folder the user opens: Windows looks a bare program name up in the working directory before `PATH` (rationale). For a browser's `binaryPath` it is also the webview realm, which writes a pane's saved params (rationale). For the Playwright installation it is also another local account, which may create folders at a Windows drive root (rationale). - **FAIL IF** `S165` a host, `dor`, or a built-in Tool spawns a program by bare name on Windows: a system program, including the `cmd.exe` running a `.cmd` shim, goes by its path under `%SystemRoot%` (or an absolute `ComSpec`), a user-installed one such as git or a requested shell by the file `docs/specs/dor-cli.md` -> "Spawning External Binaries" resolves on `PATH`, or not at all. Search `standalone/sidecar/`, `lib/src/host/`, `vscode-ext/src/`, `dor/src/`, `dor-lib-common/src/`, and `dor-tools-builtin/src/`. Pinned by `standalone/sidecar/pty-core.test.js`, `standalone/sidecar/clipboard-ops.test.js`, `dor-lib-common/test/spawn.test.mjs`, and `lib/src/host/git-upstream.test.ts`. - **FAIL IF** `S166` the VS Code pty host forks in any cwd but the extension's own directory. Pinned by `vscode-ext/test/pty-manager.test.ts`. -- **FAIL IF** `S167` git run in a repository the user has not trusted can run a program that repository's config names: every git spawn in the directories above must lead with `UNTRUSTED_REPO_GIT_ARGS` and run only a subcommand its comment says it covers, and a command Dormouse types into a shell counts: read every `launch` write `S132` lists. `watchHelper` in `lib/src/lib/helper-terminal.ts` types only the saved autorun; its factory value is empty in `savedCommand` in `standalone/sidecar/pty-core.js` and `DEFAULT_HELPER_COMMAND` in `lib/src/lib/terminal-context-types.ts`, and nothing but an explicit edit writes the file; `helperDirectory` there must never mark a directory confirmed that only the source's reported state named; the rest type only what a user, an approved Tool, or `docs/compatible-agents.md` -> "Recovery contract (maintainers)" supplies (rationale). Pinned by `lib/src/host/git-cli.test.ts`, `dor/test/open-picker.test.mjs`, and `dor-tools-builtin/test/folder-viewer.test.mjs`, each planting a `core.fsmonitor`, and for the helper by `standalone/sidecar/helper-terminal.test.js` and `lib/src/lib/helper-terminal.test.ts`. +- **FAIL IF** `S167` git run in a repository the user has not trusted can run a program that repository's config names: every git spawn in the directories above must lead with `UNTRUSTED_REPO_GIT_ARGS` and run only a subcommand its comment says it covers, and a command Dormouse types into a shell counts: read every `launch` write `S132` lists, which type only what a user, an approved Tool, or `docs/compatible-agents.md` -> "Recovery contract (maintainers)" supplies, and `lib/src/lib/helper-terminal.ts`, which writes nothing to a helper's PTY (rationale). Pinned by `lib/src/host/git-cli.test.ts`, `dor/test/open-picker.test.mjs`, and `dor-tools-builtin/test/folder-viewer.test.mjs`, each planting a `core.fsmonitor`, and for the helper by `standalone/sidecar/helper-terminal.test.js` and `lib/src/lib/helper-terminal.test.ts`. - **FAIL IF** `S277` such a spawn, or a read of the repository it queries, precedes `localGitRepository` in `dor-lib-common/src/git.ts` accepting the directory, or the spawn runs without `untrustedGitEnv`, which stops git's search at the directory checked, refusing a parent named with the path-list delimiter, which no ceiling can name; or that check accepts a repository whose `.git` entry, `gitdir:`, `commondir`, entries git opens, files those entries name (alternate object directories, the refs `HEAD` resolves through, and for a query that opens the index, the shared index a split index names), legacy remote files, reftable tables, or config-named paths include a UNC or device path or a link to one, whose config has an `include`, or, for such a query, whose index is sparse; or a query opens a ref beyond those and `packed-refs`, which the check does not walk (rationale). Pinned by `dor-lib-common/test/git.test.mjs` and `lib/src/host/git-info.test.ts`. - **FAIL IF** `S168` a host spawns a browser `binaryPath` the gate did not accept, or for Playwright loads `playwright-core` beside one, or `dor` runs such a pinned executable; the gate accepts, beyond the host's own override value by exact match, a UNC or device path, a relative path but the bare executable name, a `..` segment, a C0 or C1 control character or DEL, or a file name other than the provider's executable; or the webview sends or stores one it refuses. The path crosses from the webview realm, so a same-user writer is no reason to rate a break below WARNING. Read `isAllowedBrowserBinary` in `dor-lib-common/src/browser-providers.ts`, `runWithBinaryFallback` in `lib/src/host/agent-browser-host.ts`, `findPlaywrightInstall` in `lib/src/host/playwright-install.ts`, the pin in `dor/src/commands/browser-cli.ts`, and the callers of `isAllowedBinaryFor` in `lib/src/lib/agent-browser-binary.ts`. Pinned by `lib/src/lib/agent-browser-binary.test.ts`, `lib/src/components/Wall.test.tsx`, `lib/src/components/wall/tool-navigation.test.ts`, and `dor/test/playwright.test.mjs`. - **FAIL IF** `S169` the Playwright host loads `@playwright/cli` or `playwright-core` from outside the launcher's own installation: a directory above it, a shim script path that climbs out of the shim's directory, or a `playwright-core` neither nested in the CLI package nor beside it in its `node_modules`, though Node's lookup reaches further. Read `findPlaywrightInstall` in `lib/src/host/playwright-install.ts`. Pinned by `lib/src/host/playwright-install.test.ts`. diff --git a/docs/specs/security-local.rationale.md b/docs/specs/security-local.rationale.md index ae5240d9d..12bdeeff0 100644 --- a/docs/specs/security-local.rationale.md +++ b/docs/specs/security-local.rationale.md @@ -106,7 +106,7 @@ Why git's repository is checked before git runs (`S277`). git follows each path Why the Playwright installation is never searched upward. The host `require`s `playwright-core` into the extension host or the Tauri sidecar, so whichever copy it finds runs as the user. Windows lets any authenticated account create a folder at a drive root by default, so `C:\node_modules\@playwright\cli` and `C:\node_modules\playwright-core` can belong to another local account. The search used to climb up to six ancestors of the launcher, and a launcher whose layout matched no probe, such as a `pnpm add -g` shim in `%LOCALAPPDATA%\pnpm`, climbed to the drive root. Node's own module lookup has the same reach: from a CLI package missing its `playwright-core` it continues into every ancestor's `node_modules`, the root's included. The layouts the host accepts were checked against npm 11, pnpm 10.30, `cmd-shim` 7, and `@zkochan/cmd-shim` 7 installing `@playwright/cli` 0.1.19, whose bin is `playwright-cli.js` at the package root (2026-10). -Why a helper's autorun is empty, and only ever typed in a directory the host read (`S167`). `git status` opens the index, so a repository's config decides what it runs: `core.fsmonitor`, a hook, a pager, a clean filter. Flags can switch off the first three, but no `-c` set can name a filter, whose name that config chooses (git 2.56, 2026-11), and `UNTRUSTED_REPO_GIT_ARGS` excludes such subcommands for this reason. Reported state is whatever a terminal last printed, so it cannot stand for where the source's shell is; a process cwd can. Windows offers no such read, and an unread directory opens as a split there would, with nothing typed. +Why Dormouse never runs `git status` for you (`S167`). It opens the index, so a repository's config decides what it runs: `core.fsmonitor`, a hook, a pager, a clean filter. Flags can switch off the first three, but no `-c` set can name a filter, whose name that config chooses (git 2.56, 2026-11), and `UNTRUSTED_REPO_GIT_ARGS` excludes such subcommands for this reason. Why a browser opened from Terminal Context or Break runs in a directory of its own (`S302`). `agent-browser.json` and `.playwright/cli.config.json` can name the browser executable and plugins, and each CLI reads them from its working directory by design, so choosing that directory chooses what runs. A `dor agent-browser` keeps its project's config because its process was already there; a GUI action only knew what the terminal reported. An omitted cwd would inherit the host's, which in VS Code can be a workspace folder. The neutral directory is stable rather than per-process since a Playwright binding pins it across restarts. A UNC path is refused where the request is parsed because each later step stats it first, and on Windows that stat contacts the share. The cost: those launches ignore a project's provider config, its startup environment's directory-specific parts, and its `dormouse.yml` viewports. diff --git a/docs/specs/security.md b/docs/specs/security.md index 4662351a6..0b85f422b 100644 --- a/docs/specs/security.md +++ b/docs/specs/security.md @@ -53,7 +53,6 @@ Source of truth: root scripts in `package.json`; native jobs in `.github/workflo - **A mermaid sanitizer bypass in a Markdown document you open.** Its diagrams reach the editor page through mermaid's strict sanitizer, not the editor's allowlist, so a bypass would run script holding the editor's save, image, and document-open routes; an open of a page that runs its own scripts still waits on Dormouse's own dialog ([Local-file viewer](./security-local.md#local-file-viewer), [Terminal output](./security-local.md#terminal-output)). - **A Tool trust grant on an upstream URL trusts the URL a checkout claims.** Any directory whose own `.git/config` claims an already-granted upstream shares that grant, and so does any `dormouse.yml` nested in such a checkout, one in `node_modules` included, because a Tool resolves from the nearest file; a folder-only grant is not shared ([Dor Tool configuration](./security-local.md#dor-tool-configuration)). - **Output already flowing into a Tool's terminal at launch** can, between Dormouse typing the command and the shell reporting it, make that run's OSC 367 `open` requests ([Terminal output](./security-local.md#terminal-output)). -- **A Terminal Context autorun you set.** It runs without asking in the source terminal's own directory, so `git status`, the one Modify offers, runs what that repository's config names ([Spawned programs](./security-local.md#spawned-programs)). - **A terminal can choose which of your own agent sessions resumes.** Output a pane prints as Dormouse quits can name any session of a supported agent, which the next restore resumes in that pane's directory without asking; it can type nothing else ([Terminal output](./security-local.md#terminal-output)). - **What VS Code does with the pane state it stores.** Structure persists in VS Code's own storage under its modes, never a transcript ([Persisted state](./security-local.md#persisted-state)). - *One-time link, Hosted phone, Self-hosted Relay.* **A compromised browser or operating system, on either end.** Active XSS in the Pocket origin can use the phone's key and, with encrypted fallback storage, extract its private bytes ([Client statics](./remote-security-model.md#client-statics)). Exactly two endpoints are trusted: the distributed Burrow binaries and the exact Pocket artifact the origin serves ([Trust Model](./remote-security-model.md#trust-model)); a one-time session also trusts the page Hosted serves ([One-time connection](./remote-security-model.md#one-time-connection)). diff --git a/docs/specs/terminal-context.md b/docs/specs/terminal-context.md index 6726609e0..8089d5402 100644 --- a/docs/specs/terminal-context.md +++ b/docs/specs/terminal-context.md @@ -1,30 +1,29 @@ # Terminal context > - See `docs/specs/glossary.md` for Surface / Session / Pane vocabulary. -> - Owns helper lifecycle and global autorun. +> - Owns helper lifecycle. > - Layout owns context composition and input focus; terminal-state owns shell semantics; alert owns suppression; transport owns live recovery; dor-cli owns helper-origin `dor` commands. ## Helper lifecycle - **Must create at most one helper per source, lazily on first context opening.** Closing the source during startup cancels creation. Helpers cannot have helpers. -- **Must start with the configured shell in the source PTY's process directory, as the host reads it (`getCwd`), never the directory the source reports**, using the ordinary split fallback when unavailable. **Where the host cannot read it** (Windows, which has no such read, or a failed read), **must start in the local directory the source reports and never type autorun there**, showing that autorun was skipped; a source reporting none starts in the host default, where autorun runs. **Must preserve a directory the helper reports before autorun.** Shell exports and virtual environments are not inherited. SSH integration is outside this feature. -- **Must inject autorun only after integrated shell readiness**, accepting prompt-start and prompt-end/editing states with no current command. User input before injection cancels it. Without readiness before a timeout, show an unsupported state and never write a timeout fallback. -- **Must treat typing, paste, accepted drops, and application mouse input as user work**, disarming automatic refresh until Reset or Promote. Selection, copying, resize, and terminal protocol replies do not count. Returning to idle does not rearm autorun. -- **Must refresh an untouched helper whose autorun completed, is off, or was skipped for its directory on reopening only after a host idle check.** Foreground commands, background descendants, and failed/unknown inspection preserve the helper. +- **Must start with the configured shell in the source PTY's process directory, as the host reads it (`getCwd`), never the directory the source reports**, using the ordinary split fallback when unavailable. **Where the host cannot read it** (Windows, which has no such read, or a failed read), **must start in the local directory the source reports**; a source reporting none starts in the host default. Shell exports and virtual environments are not inherited. SSH integration is outside this feature. +- **Never write to a helper's PTY on its own**: a helper starts its shell and runs nothing. +- **Must keep a helper as the user left it across reopening**, its shell, scrollback, and directory, whether or not it has been used; only Reset replaces it. - **Must hide a retained helper without terminating its PTY**; revealing or promoting reuses the same xterm element. Renderer resource lifetime follows `docs/specs/layout.md` → "Renderer". -- **Must keep a preserved helper's directory independent of its source**, showing both locations prominently when they differ. Unknown directory state is not evidence of a match. +- **Must keep a helper's directory independent of its source**, showing both locations prominently when they differ. Unknown directory state is not evidence of a match. - **Must retain exited output**, offer Reset, and avoid automatic restart loops. - **Must pause status and process-inspection polling while the context is hidden**, its Workspace out of view included. Reopening publishes current terminal status; source closure inspects work on demand. -**Must make Reset an explicit discard**, confirmed first; under Labs it asks nothing and the old helper becomes a pending kill (`docs/specs/reopen.md` → "Labs: No-confirm delayed kill"), which the host stops counting as the source's one helper until a restore re-owns it. Cancellation changes nothing; confirmation disposes the old helper and launches a fresh one using the source's current directory and current global setting. +**Must make Reset an explicit discard**, confirmed first; under Labs it asks nothing and the old helper becomes a pending kill (`docs/specs/reopen.md` → "Labs: No-confirm delayed kill"), which the host stops counting as the source's one helper until a restore re-owns it. Cancellation changes nothing; confirmation disposes the old helper and launches a fresh one in the source's current directory. -Source of truth: `openHelper` / `closeHelperParent` in `lib/src/lib/helper-terminal.ts`; `markSessionTouched` in `lib/src/lib/terminal-lifecycle.ts`. Tests: `lib/src/lib/helper-terminal.test.ts`. +Source of truth: `openHelper` / `closeHelperParent` in `lib/src/lib/helper-terminal.ts`. Tests: `lib/src/lib/helper-terminal.test.ts`. ## Promotion and source closure Workspace movement: `docs/specs/standalone.md` → Transfer. -**Must promote the actual Session into a regular split beside its source**, preserving the PTY, xterm, scrollback, directory, partial input, and identity. Cancel pending autorun, close context, make its Surface id targetable, and focus the promoted terminal. Failed placement restores auxiliary host ownership. The source's next opening creates a new helper. +**Must promote the actual Session into a regular split beside its source**, preserving the PTY, xterm, scrollback, directory, partial input, and identity. Close context, make its Surface id targetable, and focus the promoted terminal. Failed placement restores auxiliary host ownership. The source's next opening creates a new helper. **Must reject Reset, source retirement, and duplicate Promote during ownership transfer**, including from a reopened context. A failed transfer or rollback reports its error and resumes inspection so promotion can be retried. @@ -36,16 +35,6 @@ Workspace movement: `docs/specs/standalone.md` → Transfer. Source of truth: `beginPromotion` / `cancelPromotion` / `finishPromotion` / `helperHasWork` in `lib/src/lib/helper-terminal.ts`; `closeSurface` / `requestKill` / `contextActions` in `lib/src/components/Wall.tsx`; `countRunningSessions` in `lib/src/lib/terminal-state-store.ts`. -## Global autorun setting - -**Must default to an empty command, which disables autorun**; `git status` is an opt-in Modify offers, never a default (`docs/specs/security-local.md` -> "Spawned programs"). **Never write the preference but on an explicit edit**, so an installation that never saved one follows the factory default. An explicit Modify edit applies to new and reset helpers, never a retained one. Its status describes the command captured at creation, even when the global default changes. - -**Must accept only a single command line of at most 4096 characters**, excluding CR, LF, and NUL. The host persists only this preference in `~/.dormouse/helper-terminal.json`, using atomic replacement with private file permissions. All desktop renderers read that shared preference through the host; the fake adapter keeps a deterministic in-memory setting. - -**Must enforce command validity at the host.** A rejected edit keeps the prior preference. - -Source of truth: `context` in `standalone/sidecar/pty-core.js`; `terminalContext` in `lib/src/lib/platform/fake-adapter.ts`; `TerminalContextRequest` in `lib/src/lib/terminal-context-types.ts`. - ## Presentation coverage **Must make diagnostic text drag-selectable**, including detail-dialog errors, without focusing the helper. Copy routing follows `docs/specs/mouse-and-clipboard.md` → "Terminal context input". @@ -58,7 +47,7 @@ Source of truth: `TerminalContextView` in `lib/src/components/wall/TerminalConte ## Tool context -**Must show a Tool's primary Session in Terminal Context instead of creating an auxiliary helper**; one whose run ended is a plain terminal with a helper (`docs/specs/dor-tool.md` → Run end). Reuse the title explanation, directory, port, and alert presentation, showing Tool command status without helper Modify, Reset, or Promote controls; a preview slot's adds Keep open (`docs/specs/layout.md` → Pane header). Pending approval cannot open context. +**Must show a Tool's primary Session in Terminal Context instead of creating an auxiliary helper**; one whose run ended is a plain terminal with a helper (`docs/specs/dor-tool.md` → Run end). Reuse the title explanation, directory, port, and alert presentation, showing Tool command status without helper Reset or Promote controls; a preview slot's adds Keep open (`docs/specs/layout.md` → Pane header). Pending approval cannot open context. **Must mount only one terminal view for the Tool at a time**, moving its retained xterm between the full pane and context without disposing the Session. Context keystrokes, including terminal clipboard chords, belong to that terminal and never reach the browser underneath. Closing context preserves both processes and browser state. diff --git a/docs/specs/transport.md b/docs/specs/transport.md index 10fa4ff94..c29ce808b 100644 --- a/docs/specs/transport.md +++ b/docs/specs/transport.md @@ -273,7 +273,7 @@ Source of truth: `openPortRequestTimeoutMs` in `lib/src/lib/platform/types.ts`; ## Auxiliary helper metadata -**Must carry helper parent identity and captured autorun command in live PTY metadata**, validating that the parent is owned and is not itself a helper. Promotion clears that association without restarting the PTY. Reconnect restores helper entries as helpers, outside the primary layout and its orphan-pane recovery; a missing parent recovers its helper as an ordinary Pane, and recovered helpers disable automatic refresh. Helper scrollback and editor buffers are never written to a Session snapshot. +**Must carry helper parent identity in live PTY metadata**, validating that the parent is owned and is not itself a helper. Promotion clears that association without restarting the PTY. Reconnect restores helper entries as helpers, outside the primary layout and its orphan-pane recovery; a missing parent recovers its helper as an ordinary Pane. Helper scrollback and editor buffers are never written to a Session snapshot. **Must expose terminal context operations through correlated host requests**, reporting errors and timeouts: each adapter forwards every `TerminalContextRequest` to its PTY host and answers on the same correlation. The VS Code router checks Workspace ownership for per-terminal operations and helper parent metadata. Directory opening follows `docs/specs/security-local.md` → Terminal context directory actions, and inspection failure `docs/specs/terminal-context.md` → Helper lifecycle. diff --git a/lib/src/components/Wall.test.tsx b/lib/src/components/Wall.test.tsx index 3b3680922..420310486 100644 --- a/lib/src/components/Wall.test.tsx +++ b/lib/src/components/Wall.test.tsx @@ -157,7 +157,7 @@ describe('Wall on the Lath engine', () => { params: { surfaceType: 'tool', command: 'pnpm storybook', cwd: '/repo', toolName: 'storybook', toolRender: 'iframe', toolPort: 'announce' }, }]} />)); await flush(); - const helper: helpers.HelperTerminal = { id: 'surface:helper', parentId: TOOL_ID, command: '', status: 'off' }; + const helper: helpers.HelperTerminal = { id: 'surface:helper', parentId: TOOL_ID }; vi.spyOn(helpers, 'getHelper').mockImplementation((id) => (id === TOOL_ID ? helper : undefined)); let inspected!: (busy: boolean) => void; const inspection = vi.spyOn(helpers, 'helperHasWork').mockResolvedValue(false); @@ -479,7 +479,7 @@ describe('Wall on the Lath engine', () => { it('refuses a close whose helper inspection finishes after a transfer starts', async () => { await act(async () => root.render()); await flush(); - const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a', command: '', status: 'off' }; + const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a' }; vi.spyOn(helpers, 'getHelper').mockReturnValue(helper); let inspected!: (busy: boolean) => void; vi.spyOn(helpers, 'helperHasWork').mockImplementationOnce(() => new Promise(resolve => { inspected = resolve; })); @@ -974,7 +974,7 @@ describe('Wall on the Lath engine', () => { async function openPortContext(provider = 'agent-browser') { // The mocked TerminalPane registers no terminal, so a real helper would // report its parent closed — an alert the context shows ahead of a port error. - vi.spyOn(helpers, 'openHelper').mockResolvedValue({ id: 'surface:context-helper', parentId: 'surface:a', command: '', status: 'preserved' }); + vi.spyOn(helpers, 'openHelper').mockResolvedValue({ id: 'surface:context-helper', parentId: 'surface:a' }); if (!fake.hasPty('surface:a')) fake.spawnPty('surface:a'); fake.setOpenPorts('surface:a', [{ protocol: 'tcp', family: 'IPv4', address: '127.0.0.1', port: 5173, pid: 100, processName: 'vite' }]); await act(async () => { @@ -3204,7 +3204,7 @@ describe('Wall on the Lath engine', () => { await waitUntil(() => respond.mock.calls.length > 0); expect(respond).toHaveBeenCalledWith(expect.objectContaining({ result: expect.objectContaining({ status: 'takeover' }) })); if (change === 'cancelled') controller.abort(); - if (change === 'helper opened') vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? { id: 'surface:helper-a', parentId: 'surface:a', command: '', status: 'off' } : undefined); + if (change === 'helper opened') vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? { id: 'surface:helper-a', parentId: 'surface:a' } : undefined); if (change === 'cwd changed') terminalRegistry.applyTerminalSemanticEvents('surface:a', [{ type: 'cwd', cwd: terminalRegistry.cwdFromOsc633('/elsewhere')! }]); act(() => promptBack('surface:a')); await act(async () => { await new Promise(resolve => setTimeout(resolve, 150)); }); @@ -3421,7 +3421,7 @@ describe('Wall on the Lath engine', () => { }); it.each(['agent', 'helper'] as const)('splits instead of taking over a caller with an existing %s', async (reason) => { - if (reason === 'helper') vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? { id: 'surface:helper-a', parentId: 'surface:a', command: '', status: 'off' } : undefined); + if (reason === 'helper') vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? { id: 'surface:helper-a', parentId: 'surface:a' } : undefined); const typed: string[] = []; const controller = new AbortController(); (fake as FakePtyAdapter & Pick).toolControl = vi.fn(async () => okToolLookup(null)); @@ -3948,7 +3948,7 @@ describe('Wall on the Lath engine', () => { function untouchedShellWithHelper(helperEntry: Partial): () => void { registry.set('surface:a', { untouched: true, terminal: { focus() {}, blur() {} } } as unknown as TerminalEntry); registry.set('surface:helper-a', helperEntry as TerminalEntry); - const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a', command: '', status: 'off' }; + const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a' }; vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? helper : undefined); vi.spyOn(helpers, 'helperHasWork').mockResolvedValue(false); vi.spyOn(helpers, 'closeHelperParent').mockImplementation(() => {}); @@ -4046,7 +4046,7 @@ describe('Wall on the Lath engine', () => { it('drops a kill gesture whose Helper inspection outlives the pane', async () => { vi.spyOn(terminalRegistry, 'isUntouched').mockReturnValue(false); - const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a', command: '', status: 'off' }; + const helper: helpers.HelperTerminal = { id: 'surface:helper-a', parentId: 'surface:a' }; vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:a' ? helper : undefined); vi.spyOn(helpers, 'closeHelperParent').mockImplementation(() => {}); const inspections: Array<(busy: boolean) => void> = []; @@ -4813,7 +4813,7 @@ it('recedes every other pane while a context is open and restores them as it exi }); it('moves a retained helper without resizing or replacing its source, and remembers the manual side', async () => { - const retained: helpers.HelperTerminal = { id: 'surface:placement-helper', parentId: 'surface:placement-source', command: '', status: 'preserved' }; + const retained: helpers.HelperTerminal = { id: 'surface:placement-helper', parentId: 'surface:placement-source' }; vi.spyOn(helpers, 'getHelper').mockImplementation(id => id === 'surface:placement-source' ? retained : undefined); const openHelper = vi.spyOn(helpers, 'openHelper').mockResolvedValue(retained); await act(async () => root.render()); diff --git a/lib/src/components/WorkspaceWindow.test.tsx b/lib/src/components/WorkspaceWindow.test.tsx index da4caf0f6..0f35246bc 100644 --- a/lib/src/components/WorkspaceWindow.test.tsx +++ b/lib/src/components/WorkspaceWindow.test.tsx @@ -1502,7 +1502,7 @@ describe('Labs: a Workspace close that would ask, kept pending', () => { }); it('asks as before, pending nothing, when a member\'s helper runs a command', async () => { - vi.spyOn(helperTerminal, 'getHelper').mockImplementation(id => /^surface:/.test(id) ? { id: 'surface:helper', parentId: id, command: '', status: 'running' } : undefined); + vi.spyOn(helperTerminal, 'getHelper').mockImplementation(id => /^surface:/.test(id) ? { id: 'surface:helper', parentId: id } : undefined); vi.spyOn(helperTerminal, 'helperHasWork').mockResolvedValue(true); createWorkspace({ id: 'workspace:2', name: 'build', activate: false }); await render(<>); diff --git a/lib/src/components/WorkspaceWindow.tsx b/lib/src/components/WorkspaceWindow.tsx index fac733286..84427c2a1 100644 --- a/lib/src/components/WorkspaceWindow.tsx +++ b/lib/src/components/WorkspaceWindow.tsx @@ -87,7 +87,7 @@ export function WorkspaceWindow({ useEffect(() => { const platform = getPlatform(); - const home = platform.terminalContext?.({ op: 'settings' }).then((settings) => settings.home); + const home = platform.terminalContext?.({ op: 'home' }).then((info) => info.home); return installWorkspaceAutoNaming(platform.gitInfo?.bind(platform), home); }, []); diff --git a/lib/src/components/wall/TerminalContext.test.tsx b/lib/src/components/wall/TerminalContext.test.tsx index 42fb4b8c8..66b23e680 100644 --- a/lib/src/components/wall/TerminalContext.test.tsx +++ b/lib/src/components/wall/TerminalContext.test.tsx @@ -31,8 +31,8 @@ beforeEach(() => { setPlatform(new FakePtyAdapter()); ensureResizeObserver(); container = document.createElement('div'); document.body.appendChild(container); root = createRoot(container); props = { title: 'pnpm dev', surfaceId: 'surface:3', cwd: '~/repo', titleSources: [{ source: 'OSC 2', value: 'pnpm dev', note: 'Used' }], scan: { status: 'loaded', entries: [port(5173)] }, - watchRule: 'pnpm', watching: false, todo: false, status: 'completed', command: 'git status', explorerLabel: 'Reveal in Finder', canExplore: true, browserProviders: ['agent-browser', 'playwright'], canIframe: true, - onClose: vi.fn(), onCopyId: vi.fn(), onCopyPath: vi.fn(), onExplore: vi.fn(), onWatch: vi.fn(), onTodo: vi.fn(), onPort: vi.fn(), onModify: vi.fn(async () => {}), onReset: vi.fn(async () => {}), onPromote: vi.fn(async () => {}), + watchRule: 'pnpm', watching: false, todo: false, status: 'idle', explorerLabel: 'Reveal in Finder', canExplore: true, browserProviders: ['agent-browser', 'playwright'], canIframe: true, + onClose: vi.fn(), onCopyId: vi.fn(), onCopyPath: vi.fn(), onExplore: vi.fn(), onWatch: vi.fn(), onTodo: vi.fn(), onPort: vi.fn(), onReset: vi.fn(async () => {}), onPromote: vi.fn(async () => {}), children: