Conversation
Reconciles the Playwright provider with main's workspace-scoped browser identity and hardened binary resolution: - Main's which-mirroring PATH walk, spawn-by-resolved-path, and --workspace interception move into the shared dor/src/commands/browser-cli.ts; dor pw gains --workspace and now spawns the resolved path too. - Bring-to-front stays deleted, as on main. - isToolParams names its discriminant so the negative branch keeps a params record rather than narrowing to never. - The sidecar bundle keeps ws's optional accelerators external on every bundle, including burrow's native-direct list. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…owser
Applies the /simplify review of this branch (reuse, simplification,
efficiency, altitude). No intended behavior changes.
CLI and shared code:
- The which-mirroring PATH walk moves to dor-lib-common (resolve-binary.ts);
the Playwright host's install resolution uses it instead of a laxer copy,
and memoizes the result per (hint, env override, PATH).
- PLAYWRIGHT_BIN_ENV / DEFAULT_PLAYWRIGHT_BIN replace scattered literals.
- extractSessionFlags takes a session noun and aliases, replacing dor pw's
-s pre-mapping and replaceAll('agent-browser', 'Playwright').
- resolveBrowser / browserSurface are required ControlClient methods; dor pw
proposes only { cwd, binaryPath } and resolves its binding in one helper.
Hosts:
- The VS Code relay uses BrowserStreamGrants instead of its own copy.
- Edit scripts, GUI session minting and JPEG quality live in
browser-host-shared.ts; both hosts share privateCaptureDir (Playwright
now gets the 0700 chmod and skips empty clipboard writes).
- The Playwright poll skips the tab-list spawn with one page and publishes
tabs/url/status only on change (which also stops page titles being
cleared every tick); a fresh GUI session skips its redundant close.
- The sidecar loads the Playwright host on first use; Rust shares one
capture reader between both screenshot commands.
Webview:
- automationProvider returns null for non-automated modes, backed by one
mode table; browserPlatform / browserSessionKey take the provider.
- The controller caches its platform per cwd instead of rebuilding it per
frame; isAllowedBinaryFor replaces four provider ternaries.
- One surface.browser / surface.agentBrowser handler and one
requireAutomationSession gate serve both providers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`surface.resolveBrowser` hands `dor pw` the binding a pane stores, including its `binaryPath`, and `runPlaywrightCli` spawned that path as-is. The host and webview only ever spawn or store a binary that passes the filename allowlist, but the value is persisted in the session file, so a hand-edited one reached `dor`'s spawn unchecked. Move the agent-browser/Playwright allowlist predicates into dor-lib-common (they were already Node-free), keep lib's module as a re-export plus the provider switch, and have `dor pw` apply `isAllowedPlaywrightBinary` with the caller's DORMOUSE_PLAYWRIGHT_BIN as the exact-match override. A refused path falls back to the caller's own resolution, like the host's drop-not-fail rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`dor pw` binds its pane through `surface.browser`, which was missing from CREATING_CONTROL_METHODS, so it could add a Surface behind `closeAll`'s walk and ride the Wall's unmount out as an orphaned Session (glossary I4). The Playwright arm also awaits a host `streamStatus` before it creates anything, so it now rechecks the closing flag after that round trip. No other creating verb was missing: the `resolve*` verbs only answer, and the rest address an existing Surface. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The Display modal offered Playwright screencast and popout on every Surface whenever the host wired Playwright, reading the global platform instead of the Surface. A Tool renders only as `iframe` or `ab-screencast`, so picking a Playwright mode reached `onSwapRenderMode`'s Tool branch, which wrote `toolRender: 'pw-*'`, cleared the session and launched nothing: the pane sat on its placeholder, and `pw-popout` popped a Tool out. The screen controller now declares `renderModes` (replacing `canPopOut`), computed by `offeredRenderModes` from the host's per-provider capabilities and the Surface kind, and the modal offers only those plus the current mode. Both registration sites refuse any other mode in `setRenderMode`, since the in-controller popout never reaches the Wall, and the Wall's Tool branch refuses anything that is not a declarable Tool render. The Tool render set now has one webview-safe owner (`TOOL_RENDERS` / `isToolRender` in tool-types.ts), used by the registry, persistence and the new checks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A failed Playwright launch from the terminal context reported "Could not open agent browser" (or "Agent browser is unavailable"), and the render-swap and controller warnings named agent-browser on Playwright panes too. They now take the provider's label from one table, `PROVIDER_LABEL`, which the Display modal already uses; a provider-neutral message covers the one path that has no provider in hand. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The controller's paste bridge replays text as a key down and up per character, which agent-browser's stream needs (it takes only key and mouse events). The Playwright host closes a viewer socket (1008) once 256 input messages are queued, so any paste over ~128 characters arriving as one burst was truncated and dropped the pane into a reconnect. For Playwright the controller now sends `input_text` messages of at most 8192 characters (`playwrightTextInputs`: CRLF/CR become LF, no chunk ends inside a surrogate pair), and the host validates the length and maps each to CDP `Input.insertText`, so a paste costs one queued message per 8192 characters. Checked against Chromium 1243 via playwright-core: a textarea keeps the line breaks, a text input folds them to spaces without submitting its form (native paste semantics), and emoji survive. agent-browser's path is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
`dor pw open --headed` relaunches the native browser outside Dormouse; the Wall records the host-reported mode and the new viewer port in the pane's params, but `AgentBrowserPanel` never passed `renderMode` (or `cwd`) to the controller, so its Playwright branch ran only in a unit test. The pane stayed a screencast of a headed window and sync-to-pane kept issuing `set viewport` at a real OS window; a GUI-swapped pane whose host filled in `cwd` kept calling the host, and keying its closed mark, without it. The panel now feeds both. `followParamsHeadedness` applies a Playwright mode before the port change (whose sync reclaim would otherwise size the headed window), clears the old browser's status so auto-revert waits for the new stream, and ignores the mode mid-relaunch or while the controller's own mode write is still buffered while detached (the store is behind it then). agent-browser still treats renderMode as its own popOut/popIn echo. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The controller now follows the params' renderMode for Playwright, so a params value older than its own popOut/popIn write would flip the mode back: a write buffered while the pane was detached (auto-revert on a minimized pane) meets the stale store on reattach, and StrictMode's second effect pass runs before the store catches up even while attached. Track the last mode written and ignore params until they show it, instead of checking only the detached buffer. Drop the `headedConnected` reset: clearing the old status already leaves it false on every path auto-revert has not handled. Pin the provider-named warnings added with the launch-error fix: the render swap refusal for each provider, and a failed Playwright host command. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
A provider is offered only where its host can launch it (or it is the running one, which relaunches in place), its popout only where the host can also pop out, and a Tool only its declarable renders. The panel tests run on fully capable hosts, so these gates had no test of their own. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
The earlier text never ended a chunk on a high surrogate, so the chunker's surrogate-pair guard could be deleted with the test still green. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Deploying mouseterm with
|
| Latest commit: |
1e16cf1
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://047fdf4b.mouseterm.pages.dev |
| Branch Preview URL: | https://playwright-browser.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
This is feedback on a draft, not a merge verdict. Mark the PR ready when you want the full review. The inline comments cover --key binding durability, the error message when a saved binding goes stale, a persisted key that changed, and teardown in VS Code. One more issue has no single line to attach to:
A cancelled launch can leave a headed window running. launch() starts cli(b, ['open', …]) without awaiting it. When the 30 s loop gives up, or shutdown sets closed, it runs cli(b, ['close']) right away. If that close lands before the open daemon has registered the session, the close does nothing, and the open then finishes and brings up a Chromium window nothing tracks. The Tauri and VS Code transports time out at 40 s, which is less than the worst-case launch: close + 30 s of polling + an 8 s connect + close. So a slow pop-out can also finish on the host after the webview has already restored the previous renderer. Awaiting opening (bounded) before the final close would close both gaps.
A Playwright launch that gave up closed its session right away, while the `open` it had started unawaited might not have registered the session yet: the close did nothing and the `open` then brought up a window nothing tracked. Its worst case also ran past the webview's 40 s wait, so a slow pop-out could land after the webview had restored the previous renderer. A launch now has a deadline measured from the request's arrival (queueing included), connects no longer than what is left, waits up to 4 s for its `open` before closing, and closes again when a later `open` lands unless a newer launch owns the session. PLAYWRIGHT_REQUEST_TIMEOUT_MS is the one transport budget the host sizes its launch to fit. A `dor pw --key` first command that succeeded without a viewer (a non-Chromium browser) lost its session after the reservation's two minutes. `surface.browser` now confirms the reservation, since `dor pw` asks only after the native command succeeded, and a confirmed one lasts until a Surface binds. A stale binding reported "playwright-cli is not installed" on every command: a pinned executable that is gone now falls back to the caller's own with a warning (and the pane learns the new one), and a pinned directory that is gone is named instead of surfacing as the spawn's ENOENT. The context-menu port key kept agent-browser's persisted `agent` suffix, so an upgraded Wall still reveals a restored pane instead of opening a second. VS Code's deactivate joined both browser hosts' cleanup with no deadline, ahead of the notepad archive and session flush; it now shares the sidecar's 1.5 s bound. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
|
Re the cancelled-launch finding in the draft review body: fixed in 3aeacda. A GUI launch now has a deadline measured from when the request arrives, so time spent queued behind an earlier launch counts, and the loop's connects are capped at whatever time is left. When a launch gives up, it waits up to 4 s for its The budget comes from one constant,
The same bound is still missing from agent-browser's own relaunch loop. That gap predates this PR, and it has no orphan race because that loop doesn't close on give-up. It is left for the shared launch lifecycle in the stacked PRs. |
|
Thanks, 3aeacda resolves both halves. The launch now waits (bounded) for A smaller point that doesn't block anything: the deadline bounds the polling and each |
The launch deadline bounded the polling and each `chromium.connect`, but not
the CLI spawns around them (the pre-loop `close`, `list` inside `connect`,
the final `close`), and `spawnAndCapture` had no timeout, so a hanging
playwright-cli could still run a launch past the webview's 40 s wait.
`spawnAndCapture` takes an optional `timeoutMs`: past it the child is killed
and the call resolves `{ ok: false }` with `SPAWN_TIMEOUT_CODE` (ETIMEDOUT),
without waiting for the kill. POSIX SIGKILLs the child; Windows ends the whole
tree with `taskkill /T /F` by absolute path, since a `.cmd` shim's child is
`cmd.exe` and the real CLI its descendant.
The Playwright host now derives every launch bound from one request budget:
startup (queueing, the pre-close, listing, polling, connecting) ends at its
deadline, and the closing call gets what remains of the request. Every other
CLI call (tab-list, tab ops, close, the late close) is bounded to 10 s, so a
wedged CLI cannot hold a viewer refresh forever. `open` alone stays
unbounded: it lasts as long as the page load, nothing waits on it past a
launch's own bounds, and ending it could take down the browser it started.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
|
Re the unbounded CLI spawns: fixed in 28f0aa0.
The Playwright host now derives every launch bound from one request budget. Startup covers queueing, the pre-close,
|
|
Thanks, 28f0aa0 closes it. The pre-close, |
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
dormouse-bot
left a comment
There was a problem hiding this comment.
All the points from the draft review are fixed. Four new findings from this pass, inline. The first two can leave a pane with no working browser.
Playwright pop-out and pop-in failed for any page that is not http(s): the host ran the http(s) check on every launch, and the controller sends no URL for about:blank and passes file:, data: and error pages as they are, so a bare `dor pw open` pane could never pop out and closing a headed window on such a page left no browser. Only a GUI open's URL is now checked; a relaunch without an http(s) page reopens blank (`playwright-cli open` with no URL), matching agent-browser's host. A minimized pane keeps its AgentBrowserPanel mounted while the Wall restores a failed cross-provider swap in place, and the panel's controller was keyed on the id alone, so it held the controller disposed a line earlier: the relaunched session's params reached nothing. The panel now keys its controller on provider too, and the registry replaces a controller driving the other provider rather than handing it back (disposing it outside the render that asked). A same-provider disposal still leaves the controller alone, since it only precedes the pane going away. `dor pw` read an empty DORMOUSE_PLAYWRIGHT_BIN as a binary named '', where `dor ab` and both hosts treat it as unset; it now does too. A screencast whose CDP attach or `Page.startScreencast` failed (a page navigating mid-attach) left `v.page` current, so no later refresh saw a page change: the screencast was never retried, and after a failed start the set `v.cdp` also blocked the retry on socket connect. The failed start now forgets the page, so the next poll releases the attachment and retries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
dormouse-bot
left a comment
There was a problem hiding this comment.
Argos Visual Tests is red on this commit, and the cause is in this PR. The TerminalContext → Minimum Width story fails its play check in both Chromium and WebKit: expected 286.421875 to be less than or equal to 284 (run). The previous commit failed the same way, and argos.yml passes on main. I'm withdrawing the approval until it's fixed.
CI failed — TerminalContext Minimum Width story overflows (Argos Visual Tests).
At the 280 px minimum, the Playwright popout action (disabled there, as "Playwright unavailable on this host") ended ~3 px past the context panel's right edge, failing the TerminalContext gallery's Minimum Width play check in Chromium and WebKit. Its row grows with the host's browser providers, so a shortened label would break again with the next one: the port actions now shrink to their row and truncate their text, and the tooltip keeps the full label. The gallery's play check also measures each button against its own box, so a label that spills or wraps out of a fitted button fails too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe
Adds Playwright as a second browser automation provider beside agent-browser.
dor pw/dor playwrightdrives the user's installed@playwright/cli, and its Chromium sessions render in the same browser Surface as agent-browser: same chrome, Display modal, input, screenshots, parking, and pop-out/pop-in.Base of a stack; the PRs above it carry the browser-stack improvements from the deep dive.
What's here
pw-screencast/pw-popout. Host side:lib/src/host/playwright-host.tsconnects to the CLI's own browser and serves one guarded viewer stream. It accepts only fixed host operations, with no arbitrary CLI arguments, JavaScript, or CDP from the webview. The host is wired into the Tauri sidecar (loaded on first use), the VS Code extension host, and the dev harness.dor pw. Native passthrough, plus Dormouse addressing (--key/--session/-s/--surface/--workspace) andopen/gototarget resolution. It sharesdor/src/commands/browser-cli.tswithdor ab.origin/main. Includes main's workspace-scoped--keyand hardened binary resolution. Main's which-mirroring PATH walk now lives indor-lib-common/src/resolve-binary.ts, wheredor ab,dor pw, and the Playwright host all use it.dor pwspawns the resolved path, never a bare name.BrowserStreamGrants).browser-host-shared.ts,private-capture-dir.ts).automationProvider.surface.browser/surface.agentBrowserhandler.renderModes, and the Wall's Tool branch refuses anything else.surface.browseris refused while its Workspace closes.cwdnow reach the controller.dor pwchecks a host-returned executable against the allowlist before spawning it.Specs:
docs/specs/dor-browser.md→ "Playwright Renderer",docs/specs/dor-cli.md→ "Playwright Surface Addressing", plus the touched sections ofglossary.md,standalone.md,dor-tool.md, andlayout.md.Testing
cargo checkall pass.Input.insertTextpaste semantics were checked against a real Chromium via playwright-core.playwright-cliend-to-end test is opt-in viaDORMOUSE_PLAYWRIGHT_TEST_BIN; it was not run here.🤖 Generated with Claude Code
https://claude.ai/code/session_01NCV5Uq6FeZbBfzfpk3vrpe