Skip to content

dor agent-browser: browser-chrome header (nav + URL + dev-server link)#141

Merged
nedtwigg merged 14 commits into
mainfrom
browser-spike
Jun 17, 2026
Merged

dor agent-browser: browser-chrome header (nav + URL + dev-server link)#141
nedtwigg merged 14 commits into
mainfrom
browser-spike

Conversation

@nedtwigg

Copy link
Copy Markdown
Member

Reshapes the dor agent-browser surface header to read like a real browser, and adds the one thing only Dormouse can show: which terminal pane in the workspace is serving the localhost URL you're looking at. Builds on the agent-browser surface from #140.

What's new

  • Browser-chrome header layout (SurfacePaneHeader.tsx) — left→right like Chrome: SYNCED/SCALED chip · back/forward/reload · a quiet inline --key label (non-default keys only) · the URL · the dev-server link. Width-responsive; terminals/iframes untouched.
  • URL over HTML title — the active tab's URL is the primary text (HTML <title> → tooltip). When a dev-server link fronts it, the URL collapses to just the path (the domain is redundant with the chip).
  • Click-to-navigate — clicking the URL opens an inline editor (the terminal-rename pattern), pre-filled + all-selected. Enter navigates (open <url>, scheme-normalized — http:// for loopback so a bare localhost:5173 doesn't SSL-error); Escape/blur cancels, omnibox-style.
  • Back / forward / reload — native agent-browser commands, always-enabled.
  • Dev-server connection chip (the differentiated piece) — when the active tab is loopback, correlate <port> to the terminal pane serving it (via getOpenPorts, its first UI consumer) and surface pnpm dev :5173; click focuses that terminal. Resolution is strictly off the hot path: debounced + idle-scheduled (never competes with the tab's first screencast frames), scans once then settles, and re-validates only on reload.

Notable internals

  • A second chrome snapshot channel on the per-surface controller (URL/key), separate from the screen snapshot so tab updates don't churn the SYNCED/SCALED chip.
  • A Wall-side port→pane correlation store + onFocusPane action (visible pane → focus, minimized door → reattach).
  • Editable-target guard added to the panel's key-forwarder so header inputs keep their keystrokes.

Storybook

New Components/BrowserChromeHeader playground (every piece driven by controls); fixed a pre-existing dor/* resolution gap in Storybook's Vite config that broke all Wall-importing stories.

DRY / cleanup

A /simplify pass plus cross-codebase dedup: shared isEditableTarget (3 drifted copies), deriveSurfaceLabel (the deriveHeader+resolveDisplayPrimary triple across Baseboard/Wall/dev-server), and hostPathDisplay(url, includeSearch) replacing Wall's duplicate iframeTitle.

Spec & tests

docs/specs/dor-agent-browser.md updated throughout. Full suite green (592 tests), typecheck clean; verified live in dor ab against a running Storybook. Also carries the docs: split the iframe surface into its own spec commit the agent-browser spec links to.

🤖 Generated with Claude Code

nedtwigg and others added 14 commits June 16, 2026 14:12
Pull the "Iframe Surface: Limitations And Status" section out of dor-cli.md
into a dedicated docs/specs/dor-iframe.md (mirroring dor-agent-browser.md), and
append the roadmap discussed for case-3 (tools/plugins) as Future Work:

- the transparent proxy + injected keyboard shim (the substrate), framed as
  resolving limitations #1-#4 by making Dormouse the server of the bytes
- the two-axis render-backend model: (screencast | embed) x (URL | owned-backend)
- Path 1 (swappable render backend via in-place renderer replacement) and
  Path 2 (plugin system: process supervisor + the per-surface teardown hook)

dor-cli.md now points to dor-iframe.md from the command list; the
dor-agent-browser.md cross-ref is repointed. Status reframed: the iframe
surface is kept and re-backed by the proxy for local tools, not dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reshape the agent-browser surface header to read like a browser: the
active tab's URL as primary text (HTML title demoted to tooltip),
Chrome-style back/forward/reload, a badge for non-default --key, and a
dev-server connection chip that correlates a loopback URL's port to the
terminal pane serving it and focuses that pane on click.

- Extend the body↔header bridge (agent-browser-screen) with a separate
  chrome-snapshot channel (URL / key / connection) plus nav actions, so
  tab/status updates don't churn the SYNCED/SCALED screen snapshot;
  AgentBrowserPanel computes and publishes it.
- New browser-url helpers (host+path display, loopback-port parsing) and
  a port→pane correlation store (agent-browser-ports) driven Wall-side
  via use-dev-server-ports — getOpenPorts' first UI consumer — with a new
  onFocusPane WallAction to jump to the serving terminal (0.0.0.0/wildcard
  binds count as serving localhost; specific non-loopback interfaces do
  not).
- Add reload/back/forward to the agentBrowserCommand allowlist.
- Reorder the shared SurfacePaneHeader for browser surfaces (sync chip
  far-left, nav, key badge, URL, connection chip, spacer) with responsive
  collapse; terminals/iframes unchanged.
- Storybook playground (BrowserChromeHeader) + unit and render tests;
  move the feature out of the spec's Future Expansions into the built
  surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The connection-chip correlation was async but fired a full N-pane
`getOpenPorts` scan the instant a loopback URL appeared — i.e. exactly as
the tab opened — and then re-scanned every 5s. Each scan shells out
(`lsof`/PowerShell) per pane on the same host that drives the live
screencast, so the burst contended with the opening tab's first
screenshots and the steady poll kept the host busy.

Make the decoration strictly off the hot path:
- debounce interest changes (~600ms) so tab-open/navigation settles first
  and never triggers a synchronous scan;
- run the scan in requestIdleCallback time (timeout fallback for webviews
  without it), yielding to rendering + the screencast;
- pace adaptively — poll quickly (4s) only while a wanted port is still
  unmatched (a dev server may start after the tab), back off to 15s once
  matched, and stop entirely when no loopback URL is on screen.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stop the periodic re-scan. Once a loopback port is matched to a pane it's
remembered and never rescanned — the serving pane rarely moves, so the
steady-state getOpenPorts poll was wasted work. We keep retrying (slow
idle poll) only while a wanted port is still unmatched (the dev server
may start after the tab opened).

A surface reload un-settles every port and re-validates, but
optimistically: the published resolution stays in place until the rescan
overwrites it, so the chip never flickers when nothing changed. Navigating
to a different loopback port re-scans naturally (the old port leaves the
wanted set). Wire the header reload button to triggerDevServerRescan().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Importing the contexts from '../components/Wall' dragged the whole Wall
orchestrator — and its `dor/commands/shell-quote` import — into the story
graph, which Storybook's Vite can't resolve (the story crashed with
"Failed to resolve import 'dor/commands/shell-quote'"). Pull the contexts
+ WallActions type straight from wall-context instead (type-only deps, no
runtime dor import), which is also the right layering for a header story.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Storybook's Vite doesn't read tsconfig paths, so the tsconfig.app.json
mapping `dor/* → ../dor/src/*` never applied — any story importing Wall
(TerminalPaneHeader, MouseHeaderIcon, ShellCwd, …) crashed at load with
"Failed to resolve import 'dor/commands/shell-quote' from Wall.tsx". The
main app + webview builds resolve it; only Storybook was missing the
mapping. Add a `dor` alias to viteFinal mirroring the tsconfig path.

Verified against a fresh Storybook instance: all three previously-broken
stories now render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the text key badge with a single filled KeyIcon; the key name
moves to the hover tooltip (`--key <name>`). Non-default-keys-only
behavior is unchanged. Update the header test to assert the icon's
aria-label instead of the old badge text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Move the dev-server connection chip in front of the URL.
- When the chip is shown the URL drops the now-redundant domain and shows
  only the path (new pathDisplay helper); with no chip it keeps host+path.
- Drop the chip's fixed 14ch command cap so the whole command shows when
  there's room, and give the URL a much higher shrink priority so it
  truncates long before the command does.
- Remove the green status dot from the chip.
- Simplify header opacity: full-strength at rest everywhere except a
  single /70 accent on the chip's :port; /10 stays the lone hover wash.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chrome snapshot carried connected/connecting/lost, but nothing in the
header ever rendered it — the pane *body* placeholder uses the underlying
status/connectionLost directly, which is untouched. Remove the dead field,
its panel computation, the Storybook control + ConnectionLost story, and
the spec mention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the filled key icon with the key name inline, small + /70 quiet,
so it reads as a quiet label next to the URL. Hover still reveals
`--key <name>`; non-default-keys-only behavior unchanged. Drop the now
unused KeyIcon import and update the header test to assert the inline text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Clicking the header URL opens an inline editor (the terminal-rename
pattern) pre-filled with the full URL, all selected. Enter navigates,
Escape/blur cancels (browser-omnibox style).

- New chromeActions.navigate(url) issues `open <url>`; `open` added to the
  agentBrowserCommand allowlist.
- normalizeNavUrl keeps an explicit scheme, else adds http:// for loopback
  hosts (a bare localhost:5173 SSL-errors under https) and https:// for
  the rest.
- Keyboard coordination: the editor flags dialog-keyboard so the Wall's
  chord handler stands down, and the panel's key-forwarder now skips
  editable targets so a header input keeps its keystrokes (also hardens
  the screen modal's fields).
- Tests for normalizeNavUrl and the editor open/navigate/cancel flow;
  spec updated (allowlist + click-to-navigate note).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/simplify cleanups:
- normalizeNavUrl reuses isLoopbackHostname instead of inlining a second
  (already-drifting) copy of the loopback-host set.
- Drop the redundant displayUrl dep from the chrome publish effect
  (displayUrl is hostPathDisplay(url), so url already covers it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…play

Cross-codebase dedup of three patterns /simplify flagged but I'd left
inside the agent-browser diff:

- isEditableTarget(target) (new lib/dom.ts) replaces three hand-rolled
  "is this a form field" predicates that had drifted: the mouse-selection
  chord guard (keeps its xterm-helper-textarea carve-out on top), the mobile
  blur, and AgentBrowserPanel's key-forwarder (drops a speculative SELECT).
- deriveSurfaceLabel(pane, visible, appTitleForPane, fallback) (terminal-state)
  is now the one place to compose deriveHeader + resolveDisplayPrimary for a
  single pane; Baseboard.deriveDoorTitle, Wall.buildDorSurfaces, and the
  dev-server chip's labelForPane all call it.
- hostPathDisplay gains an includeSearch flag; Wall's duplicate iframeTitle
  is deleted in favor of hostPathDisplay(url, true).

Tests for the new helpers; full suite green (592).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mouseterm with  Cloudflare Pages  Cloudflare Pages

Latest commit: 3d0eb63
Status: ✅  Deploy successful!
Preview URL: https://1f1d3cca.mouseterm.pages.dev
Branch Preview URL: https://browser-spike.mouseterm.pages.dev

View logs

@nedtwigg nedtwigg merged commit 818c920 into main Jun 17, 2026
9 checks passed
@nedtwigg nedtwigg deleted the browser-spike branch June 17, 2026 04:53
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