From 8f14d0d2dbf780ac3e77e3bace616db5e54c0f6d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 17 Jun 2026 00:14:35 -0700 Subject: [PATCH 01/57] display-modal: storybook UI for render swap + pop-out Reframe the surface's far-left chip as a render-mode + sync indicator and turn its modal into the single "Display" control center for how a surface renders (docs/specs/dor-iframe.md -> Path 1; dor-agent-browser.md -> Headed Pop-Out). UI-only, driven by Storybook: the production panel does not wire the new actions yet, so the live modal is unchanged apart from the cosmetic chip glyphs. - chip: FrameCorners = embed, LockSimple = screencast synced, LockSimpleOpen = scaled (SurfacePaneHeader) - modal: new Render section (Screencast/Embed) gated on setRenderMode; viewport controls grey out in embed; Pop out button gated on canPopOut - types: optional renderMode / setRenderMode / popOut / canPopOut on the screen controller, so existing constructions stay green - stories: renderMode + canPopOut knobs; Embed / EmbedRender / NoPopOut Co-Authored-By: Claude Opus 4.8 (1M context) --- .../wall/AgentBrowserScreenModal.tsx | 130 ++++++++++++++---- lib/src/components/wall/SurfacePaneHeader.tsx | 33 +++-- .../components/wall/agent-browser-screen.ts | 23 ++++ .../AgentBrowserScreenModal.stories.tsx | 49 ++++++- .../stories/BrowserChromeHeader.stories.tsx | 18 ++- 5 files changed, 218 insertions(+), 35 deletions(-) diff --git a/lib/src/components/wall/AgentBrowserScreenModal.tsx b/lib/src/components/wall/AgentBrowserScreenModal.tsx index 816d993c..59ceff46 100644 --- a/lib/src/components/wall/AgentBrowserScreenModal.tsx +++ b/lib/src/components/wall/AgentBrowserScreenModal.tsx @@ -1,20 +1,24 @@ /** - * Screen / viewport modal for an agent-browser surface - * (docs/specs/dor-agent-browser.md → "Screen Indicator & Viewport → The - * modal"). It is purely a GUI front-end for native `agent-browser set - * viewport` / `set device`, plus the one Dormouse-side concept, *Sync to pane*. + * Display modal for a web surface (docs/specs/dor-agent-browser.md → "Screen + * Indicator & Viewport → The modal"; docs/specs/dor-iframe.md → "Path 1 — + * Swappable Render Backend"). Opened from the header's far-left chip, it is the + * single place that owns *how* a surface renders: * - * Three mutually exclusive targets: - * - Sync to pane → engageSync() (auto-issues `set viewport ` on resize) - * - Device → applyDevice() (fixed registry; bundles viewport+DPR+touch+UA) - * - Custom → applyViewport() + * - Render — swap the backend in place (Screencast ↔ Embed), preserving the + * target. Shown only when the controller wires `setRenderMode`. + * - Screen — viewport for the screencast backend; a GUI front-end for native + * `agent-browser set viewport` / `set device`, plus the Dormouse-side + * *Sync to pane*. Greyed out in embed (the iframe renders at the pane size). + * - Pop out — relaunch the browser headed as an OS window (screencast only, + * gated on `canPopOut`). * * It reads the live snapshot on open and pre-selects accordingly, reflecting * reality rather than a stored intent. */ import { useMemo, useRef, useState } from 'react'; +import { ArrowSquareOutIcon } from '@phosphor-icons/react'; import { ModalCloseButton, ModalFrame, modalActionButton } from '../design'; -import type { ScreenController, ScreenSnapshot } from './agent-browser-screen'; +import type { RenderMode, ScreenController, ScreenSnapshot } from './agent-browser-screen'; import { useAgentBrowserScreenSnapshot } from './agent-browser-screen'; // Fixed registry — the CLI's own device set. No custom descriptors; touch + @@ -65,6 +69,16 @@ export function AgentBrowserScreenModal({ const [customH, setCustomH] = useState(String(initial?.viewport.h ?? 720)); const [customDpi, setCustomDpi] = useState(String(initial?.viewport.dpr ?? 1)); + // Render backend (Path 1). The Render section only appears when the surface + // wires `setRenderMode` (the swap is wired); otherwise the modal is the plain + // screencast viewport modal it has always been. + const currentMode: RenderMode = snapshot?.renderMode ?? 'screencast'; + const canSwapRender = !!controller.actions.setRenderMode; + const [renderMode, setRenderMode] = useState(currentMode); + const isEmbed = renderMode === 'embed'; + // Pop out is a screencast-only escape hatch, gated per host/platform. + const canPopOut = (controller.canPopOut ?? false) && !!controller.actions.popOut; + const customValid = useMemo(() => { const w = Number(customW); const h = Number(customH); @@ -72,13 +86,22 @@ export function AgentBrowserScreenModal({ return Number.isInteger(w) && w > 0 && Number.isInteger(h) && h > 0 && dpi > 0 && Number.isFinite(dpi); }, [customW, customH, customDpi]); - const applyDisabled = !hostCapable || (target === 'custom' && !customValid); + // Embed has no viewport to set, so its only gate is the render swap itself. + const applyDisabled = isEmbed ? false : (!hostCapable || (target === 'custom' && !customValid)); const apply = () => { if (applyDisabled) return; - if (target === 'sync') controller.actions.engageSync(); - else if (target === 'device') controller.actions.applyDevice(device); - else controller.actions.applyViewport(Number(customW), Number(customH), Number(customDpi)); + if (renderMode !== currentMode) controller.actions.setRenderMode?.(renderMode); + if (renderMode === 'screencast') { + if (target === 'sync') controller.actions.engageSync(); + else if (target === 'device') controller.actions.applyDevice(device); + else controller.actions.applyViewport(Number(customW), Number(customH), Number(customDpi)); + } + onClose(); + }; + + const popOut = () => { + controller.actions.popOut?.(); onClose(); }; @@ -101,23 +124,70 @@ export function AgentBrowserScreenModal({ id="agent-browser-screen-modal-title" className="min-w-0 flex-1 text-sm leading-5 text-foreground" > - Screen — {label} + Display — {label} - {snapshot && vp && pane && ( + {snapshot && (
- Currently {snapshot.state} -
- browser {vp.w}×{vp.h} - {' · '} - pane {pane.w}×{pane.h} @{formatDpr(snapshot.displayDpr)} -
+ Currently{' '} + + {currentMode === 'embed' ? 'EMBED' : snapshot.state} + + {currentMode === 'embed' ? ( +
the page's own DOM, rendered at the pane size
+ ) : vp && pane ? ( +
+ browser {vp.w}×{vp.h} + {' · '} + pane {pane.w}×{pane.h} @{formatDpr(snapshot.displayDpr)} +
+ ) : null}
)} -
+ {canSwapRender && ( +
+ Render +
+ + +
+
+ )} + +
+ {canSwapRender && ( + + Screen · screencast only + + )} +
-
+
+ - {!hostCapable && ( + {!hostCapable && !isEmbed && (

This host can't drive the browser viewport; run dor ab set … from a terminal instead.

)} + {canPopOut && renderMode === 'screencast' && ( + + )} +
{/* Back / forward / refresh — native agent-browser commands; always diff --git a/lib/src/components/wall/agent-browser-screen.ts b/lib/src/components/wall/agent-browser-screen.ts index fb250424..e37b2655 100644 --- a/lib/src/components/wall/agent-browser-screen.ts +++ b/lib/src/components/wall/agent-browser-screen.ts @@ -20,8 +20,18 @@ import { useSyncExternalStore } from 'react'; export type ScreenState = 'SYNCED' | 'SCALED'; +/** How a web surface is rendered (docs/specs/dor-iframe.md → "Render Backends: + * Two Axes"). `screencast` = real Chromium drawn to a canvas (agent-drivable, + * any URL, laggy); `embed` = the page's own DOM in a proxied iframe (zero-lag, + * loopback-only). Absent ⇒ `screencast` — the only backend wired today, so a + * surface with no explicit mode reads as a screencast. */ +export type RenderMode = 'screencast' | 'embed'; + export interface ScreenSnapshot { state: ScreenState; + /** The surface's current render backend; absent ⇒ `screencast`. Drives the + * far-left chip glyph (frame-corners = embed; lock = screencast). */ + renderMode?: RenderMode; /** The browser's live CSS viewport + inferred device pixel ratio. */ viewport: { w: number; h: number; dpr: number }; /** The pane's CSS pixel size (the canvas render area). */ @@ -40,6 +50,14 @@ export interface ScreenActions { applyViewport(w: number, h: number, dpr: number): void; /** Open the screen modal for this surface. */ openModal(): void; + /** Swap this surface's render backend in place, preserving the target + * (docs/specs/dor-iframe.md → "Path 1 — Swappable Render Backend"). Absent + * until the swap is wired; the modal hides its Render section without it. */ + setRenderMode?(mode: RenderMode): void; + /** Relaunch the browser headed as an OS window + * (docs/specs/dor-agent-browser.md → "Headed Pop-Out"). Absent until wired; + * gated additionally by `ScreenController.canPopOut` per host/platform. */ + popOut?(): void; } /** What the browser-chrome header reads about the active tab @@ -82,6 +100,9 @@ export interface ScreenController { readonly chromeActions: ChromeActions; /** Whether the host can run `agentBrowserCommand` (false ⇒ resizes inert). */ readonly hostCapable: boolean; + /** Whether this host/platform can pop the surface out to a headed OS window + * (false/absent on web; gates the modal's "Pop out to window" button). */ + readonly canPopOut?: boolean; } interface ScreenEntry { @@ -122,6 +143,7 @@ export function registerAgentBrowserScreen( chrome: ChromeSnapshot; chromeActions: ChromeActions; hostCapable: boolean; + canPopOut?: boolean; }, ): ScreenRegistration { const entry: ScreenEntry = { @@ -144,6 +166,7 @@ export function registerAgentBrowserScreen( chrome: () => entry.chrome, chromeActions: init.chromeActions, hostCapable: init.hostCapable, + canPopOut: init.canPopOut, }, }; registry.set(id, entry); diff --git a/lib/src/stories/AgentBrowserScreenModal.stories.tsx b/lib/src/stories/AgentBrowserScreenModal.stories.tsx index c53b09ee..71e1c455 100644 --- a/lib/src/stories/AgentBrowserScreenModal.stories.tsx +++ b/lib/src/stories/AgentBrowserScreenModal.stories.tsx @@ -1,9 +1,13 @@ import { useMemo } from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { AgentBrowserScreenModal } from '../components/wall/AgentBrowserScreenModal'; -import type { ScreenController, ScreenSnapshot, ScreenState } from '../components/wall/agent-browser-screen'; +import type { RenderMode, ScreenController, ScreenSnapshot, ScreenState } from '../components/wall/agent-browser-screen'; interface StoryArgs { + /** Render backend — `embed` greys out the Screen (viewport) section. */ + renderMode: RenderMode; + /** Whether the host can pop out (gates the "Pop out to window" button). */ + canPopOut: boolean; state: ScreenState; /** Browser CSS viewport + inferred DPR. */ vpW: number; @@ -26,6 +30,7 @@ function useMockController(args: StoryArgs): ScreenController { return useMemo(() => { const snapshot: ScreenSnapshot = { state: args.state, + renderMode: args.renderMode, viewport: { w: args.vpW, h: args.vpH, dpr: args.vpDpr }, paneCss: { w: args.paneW, h: args.paneH }, displayDpr: args.displayDpr, @@ -49,14 +54,17 @@ function useMockController(args: StoryArgs): ScreenController { reload: () => console.log('[story] reload'), }, hostCapable: args.hostCapable, + canPopOut: args.canPopOut, actions: { engageSync: () => console.log('[story] engageSync'), applyDevice: (name) => console.log('[story] applyDevice', name), applyViewport: (w, h, dpr) => console.log('[story] applyViewport', w, h, dpr), openModal: () => {}, + setRenderMode: (mode) => console.log('[story] setRenderMode', mode), + popOut: () => console.log('[story] popOut'), }, }; - }, [args.state, args.vpW, args.vpH, args.vpDpr, args.paneW, args.paneH, args.displayDpr, args.syncEngaged, args.hostCapable]); + }, [args.state, args.renderMode, args.vpW, args.vpH, args.vpDpr, args.paneW, args.paneH, args.displayDpr, args.syncEngaged, args.hostCapable, args.canPopOut]); } function AgentBrowserScreenModalStory(args: StoryArgs) { @@ -73,6 +81,8 @@ const meta: Meta = { title: 'Modals/AgentBrowserScreenModal', component: AgentBrowserScreenModalStory, argTypes: { + renderMode: { control: 'inline-radio', options: ['screencast', 'embed'] }, + canPopOut: { control: 'boolean' }, state: { control: 'inline-radio', options: ['SYNCED', 'SCALED'] }, vpW: { control: 'number' }, vpH: { control: 'number' }, @@ -83,6 +93,12 @@ const meta: Meta = { syncEngaged: { control: 'boolean' }, hostCapable: { control: 'boolean' }, }, + // Defaults shared by every story (each story overrides the viewport knobs); + // a swap-capable, pop-out-capable surface so both new affordances show. + args: { + renderMode: 'screencast', + canPopOut: true, + }, }; export default meta; @@ -139,3 +155,32 @@ export const HostIncapable: Story = { hostCapable: false, }, }; + +// Embed (iframe) render mode: the Render section pre-selects Embed and the +// Screen (viewport) section greys out — the iframe renders at the pane size, so +// there's nothing to set. Pop-out hides (it's a screencast-only escape hatch). +export const EmbedRender: Story = { + args: { + renderMode: 'embed', + state: 'SYNCED', + vpW: 980, vpH: 560, vpDpr: 2, + paneW: 980, paneH: 560, + displayDpr: 2, + syncEngaged: true, + hostCapable: true, + }, +}; + +// Host can't pop out (e.g. the web host) ⇒ the "Pop out to window" button is +// hidden; the render swap + viewport controls remain. +export const NoPopOut: Story = { + args: { + canPopOut: false, + state: 'SYNCED', + vpW: 980, vpH: 560, vpDpr: 2, + paneW: 980, paneH: 560, + displayDpr: 2, + syncEngaged: true, + hostCapable: true, + }, +}; diff --git a/lib/src/stories/BrowserChromeHeader.stories.tsx b/lib/src/stories/BrowserChromeHeader.stories.tsx index 9f6e1835..e32704d5 100644 --- a/lib/src/stories/BrowserChromeHeader.stories.tsx +++ b/lib/src/stories/BrowserChromeHeader.stories.tsx @@ -12,6 +12,7 @@ import { SurfacePaneHeader } from '../components/wall/SurfacePaneHeader'; import { registerAgentBrowserScreen, type ChromeSnapshot, + type RenderMode, type ScreenRegistration, type ScreenSnapshot, type ScreenState, @@ -48,6 +49,9 @@ const loggingActions: WallActions = { }; interface StoryArgs { + /** Render backend — drives the far-left chip glyph: frame = embed, lock = + * screencast (closed when synced, open when scaled). */ + renderMode: RenderMode; /** Drives the SYNCED/SCALED chip + the modal it opens. */ state: ScreenState; /** Active tab URL — also the source of the host+path text and loopback port. */ @@ -74,11 +78,12 @@ function BrowserChromeStory(args: StoryArgs) { const screenSnapshot: ScreenSnapshot = useMemo(() => ({ state: args.state, + renderMode: args.renderMode, viewport: { w: 1280, h: 720, dpr: 1 }, paneCss: args.state === 'SYNCED' ? { w: 1280, h: 720 } : { w: 980, h: 560 }, displayDpr: 2, syncEngaged: args.state === 'SYNCED', - }), [args.state]); + }), [args.state, args.renderMode]); const chromeSnapshot: ChromeSnapshot = useMemo(() => ({ url: args.url, @@ -98,6 +103,8 @@ function BrowserChromeStory(args: StoryArgs) { applyDevice: (name) => console.log('[story] applyDevice', name), applyViewport: (w, h, dpr) => console.log('[story] applyViewport', w, h, dpr), openModal: () => console.log('[story] openModal'), + setRenderMode: (mode) => console.log('[story] setRenderMode', mode), + popOut: () => console.log('[story] popOut'), }, chromeActions: { navigate: (url) => console.log('[story] navigate', url), @@ -158,6 +165,7 @@ const meta: Meta = { title: 'Components/BrowserChromeHeader', component: BrowserChromeStory, argTypes: { + renderMode: { control: 'inline-radio', options: ['screencast', 'embed'] }, state: { control: 'radio', options: ['SYNCED', 'SCALED'] }, url: { control: 'text' }, htmlTitle: { control: 'text' }, @@ -168,6 +176,7 @@ const meta: Meta = { selected: { control: 'boolean' }, }, args: { + renderMode: 'screencast', state: 'SYNCED', url: 'http://localhost:5173/app', htmlTitle: 'Vite + React', @@ -185,6 +194,13 @@ type Story = StoryObj; /** Everything on at once: key badge + URL + dev-server chip + nav. */ export const Playground: Story = {}; +/** Embed (iframe) render mode — the unified chrome is identical to screencast, + * but the far-left chip becomes the frame-corners glyph. Same URL/nav/dev-server + * header; only the chip + body renderer differ. */ +export const Embed: Story = { + args: { renderMode: 'embed' }, +}; + /** Letterboxed viewport — the chip reads SCALED (click it for the modal). */ export const Scaled: Story = { args: { state: 'SCALED' }, From f9336eba58a02cca6972b1b7be608f79756fe514 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 17 Jun 2026 09:57:25 -0700 Subject: [PATCH 02/57] display-modal: pop-out as a render mode; resolution model + per-option icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds on the swappable-render storybook UI: make pop-out a third render backend and restyle the Display modal around the three modes. Still UI-only (driven by Storybook); production wiring lands later. - RenderMode is now screencast | popout | embed; drop the separate popOut() action — pop-out is just setRenderMode('popout'), gated by canPopOut (hidden on web). - Each render option uses its exact name (agent-browser screencast / agent-browser popout / iframe embed) and lists its agent/URL/feel trade-offs as green-check / red-x rows. - Screencast's resolution nests under it: Resize with pane (link) vs Fixed (lock, via Device/Custom); greys out for the other modes. Drop the now-redundant "Currently"/"RENDER" chrome. - Far-left chip icons reiterated: link / lock for screencast resize / fixed, ArrowSquareOut for popout, FrameCorners for embed. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../wall/AgentBrowserScreenModal.tsx | 315 ++++++++++-------- lib/src/components/wall/SurfacePaneHeader.tsx | 23 +- .../components/wall/agent-browser-screen.ts | 31 +- .../AgentBrowserScreenModal.stories.tsx | 24 +- .../stories/BrowserChromeHeader.stories.tsx | 10 +- 5 files changed, 228 insertions(+), 175 deletions(-) diff --git a/lib/src/components/wall/AgentBrowserScreenModal.tsx b/lib/src/components/wall/AgentBrowserScreenModal.tsx index 59ceff46..abca0786 100644 --- a/lib/src/components/wall/AgentBrowserScreenModal.tsx +++ b/lib/src/components/wall/AgentBrowserScreenModal.tsx @@ -4,19 +4,28 @@ * Swappable Render Backend"). Opened from the header's far-left chip, it is the * single place that owns *how* a surface renders: * - * - Render — swap the backend in place (Screencast ↔ Embed), preserving the - * target. Shown only when the controller wires `setRenderMode`. - * - Screen — viewport for the screencast backend; a GUI front-end for native - * `agent-browser set viewport` / `set device`, plus the Dormouse-side - * *Sync to pane*. Greyed out in embed (the iframe renders at the pane size). - * - Pop out — relaunch the browser headed as an OS window (screencast only, - * gated on `canPopOut`). + * - Render — swap the backend in place, preserving the target: + * `agent-browser screencast`, `agent-browser popout` (relaunch headed as a + * native OS window), or `iframe embed`. Each lists its agent/URL/feel + * trade-offs. Shown only when the controller wires `setRenderMode`; the + * popout option is gated on `canPopOut` (hidden on web). + * - Resolution — the screencast viewport: *Resize with pane* (linked to the + * pane) or *Fixed* (a specific resolution chosen via Device or Custom). + * Specific to screencast, so it nests under that option and greys out + * whenever a different render mode is selected. * * It reads the live snapshot on open and pre-selects accordingly, reflecting * reality rather than a stored intent. */ -import { useMemo, useRef, useState } from 'react'; -import { ArrowSquareOutIcon } from '@phosphor-icons/react'; +import { useMemo, useRef, useState, type ReactNode } from 'react'; +import { + ArrowSquareOutIcon, + CheckIcon, + FrameCornersIcon, + LinkIcon, + LockSimpleIcon, + XIcon, +} from '@phosphor-icons/react'; import { ModalCloseButton, ModalFrame, modalActionButton } from '../design'; import type { RenderMode, ScreenController, ScreenSnapshot } from './agent-browser-screen'; import { useAgentBrowserScreenSnapshot } from './agent-browser-screen'; @@ -69,15 +78,20 @@ export function AgentBrowserScreenModal({ const [customH, setCustomH] = useState(String(initial?.viewport.h ?? 720)); const [customDpi, setCustomDpi] = useState(String(initial?.viewport.dpr ?? 1)); - // Render backend (Path 1). The Render section only appears when the surface - // wires `setRenderMode` (the swap is wired); otherwise the modal is the plain - // screencast viewport modal it has always been. + // Render backend (Path 1 + Headed Pop-Out). The Render section only appears + // when the surface wires `setRenderMode` (the swap is wired); otherwise the + // modal is the plain screencast viewport modal it has always been. const currentMode: RenderMode = snapshot?.renderMode ?? 'screencast'; const canSwapRender = !!controller.actions.setRenderMode; const [renderMode, setRenderMode] = useState(currentMode); - const isEmbed = renderMode === 'embed'; - // Pop out is a screencast-only escape hatch, gated per host/platform. - const canPopOut = (controller.canPopOut ?? false) && !!controller.actions.popOut; + // Pop-out is a render mode, gated per host/platform (hidden on web). + const canPopOut = controller.canPopOut ?? false; + // Only the screencast backend has a Dormouse-settable viewport; pop-out is a + // native OS window and embed renders at the pane size, so both grey it out. + const viewportDisabled = renderMode !== 'screencast'; + // Within screencast, the resolution is either linked to the pane (resize with + // pane) or fixed — Device/Custom are the two ways to pick the fixed size. + const isFixed = target === 'device' || target === 'custom'; const customValid = useMemo(() => { const w = Number(customW); @@ -86,8 +100,8 @@ export function AgentBrowserScreenModal({ return Number.isInteger(w) && w > 0 && Number.isInteger(h) && h > 0 && dpi > 0 && Number.isFinite(dpi); }, [customW, customH, customDpi]); - // Embed has no viewport to set, so its only gate is the render swap itself. - const applyDisabled = isEmbed ? false : (!hostCapable || (target === 'custom' && !customValid)); + // A non-screencast mode has no viewport to set, so its only gate is the swap. + const applyDisabled = viewportDisabled ? false : (!hostCapable || (target === 'custom' && !customValid)); const apply = () => { if (applyDisabled) return; @@ -100,14 +114,80 @@ export function AgentBrowserScreenModal({ onClose(); }; - const popOut = () => { - controller.actions.popOut?.(); - onClose(); - }; - - const vp = snapshot?.viewport; const pane = snapshot?.paneCss; + // Screencast resolution controls: Resize with pane (viewport linked to the + // pane) vs a Fixed resolution chosen via Device or Custom. Rendered nested + // under the screencast render option (or standalone when the surface can't + // swap render mode), and greyed whenever the active mode isn't screencast. + const viewportControls = ( +
+
Resolution
+
+ + +
+ +
+
+ + Device · emulates touch + mobile UA + +
+ {DEVICES.map((name) => ( + + ))} +
+ dimensions fill in after applying +
+
+ Custom + setTarget('custom')} /> + setTarget('custom')} /> + setTarget('custom')} /> +
+
+
+
+
+ ); + return ( @@ -129,151 +209,81 @@ export function AgentBrowserScreenModal({
- {snapshot && ( -
- Currently{' '} - - {currentMode === 'embed' ? 'EMBED' : snapshot.state} - - {currentMode === 'embed' ? ( -
the page's own DOM, rendered at the pane size
- ) : vp && pane ? ( -
- browser {vp.w}×{vp.h} - {' · '} - pane {pane.w}×{pane.h} @{formatDpr(snapshot.displayDpr)} -
- ) : null} -
- )} - - {canSwapRender && ( -
- Render -
-
- )} - -
- {canSwapRender && ( - - Screen · screencast only - - )} -
- - - - -
-
+ ) : ( + // No render swap wired: the legacy plain screencast resolution modal. +
{viewportControls}
+ )} - {!hostCapable && !isEmbed && ( + {!hostCapable && !viewportDisabled && (

This host can't drive the browser viewport; run dor ab set … from a terminal instead.

)} - {canPopOut && renderMode === 'screencast' && ( - - )} -
- ))} -
- dimensions fill in after applying - -
- Custom - setTarget('custom')} /> - setTarget('custom')} /> - setTarget('custom')} /> +
+ + {/* Dimensions inline; or pick a device via Emulate below (emulating + disables the dims — they fill in from the next frames). */} +
+ setTarget('custom')} /> + setTarget('custom')} /> + setTarget('custom')} />
+
@@ -223,7 +207,6 @@ export function AgentBrowserScreenModal({ checked={renderMode === 'screencast'} onChange={() => setRenderMode('screencast')} /> - agent-browser screencast
@@ -324,22 +307,29 @@ function DimInput({ value, onChange, onFocus, + disabled, + chars = 4, }: { label: string; value: string; onChange: (next: string) => void; onFocus: () => void; + disabled?: boolean; + /** Max digits the field holds — sizes the box so W/H/DPI stay compact. */ + chars?: number; }) { return ( - + {label} onChange(e.target.value.replace(/[^0-9.]/g, ''))} - className="w-16 rounded border border-border bg-app-bg px-1.5 py-1 font-mono text-foreground outline-none focus:border-focus-ring" + style={{ width: `calc(${chars}ch + 0.5rem)` }} + className="border-0 border-b border-border bg-transparent px-0.5 py-0.5 font-mono text-foreground outline-none focus:border-focus-ring" /> ); From 38b855c5fa63c768f5065ff3c2c136e8cd12dcfd Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Wed, 17 Jun 2026 11:06:30 -0700 Subject: [PATCH 04/57] display-modal: extract RenderOption, merge chip icon+label Quality cleanup (behavior-preserving): - collapse the three near-identical render-option blocks into one RenderOption helper driven by a features array; screencast passes its nested resolution controls as children. - fuse screenChipLabel + ScreenChipIcon into one screenChip() returning { icon, label } so the glyph and its label can't drift apart, and the icon renders as a value rather than its own component fiber. - drop an empty-string className branch. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../wall/AgentBrowserScreenModal.tsx | 114 +++++++++--------- lib/src/components/wall/SurfacePaneHeader.tsx | 31 ++--- 2 files changed, 72 insertions(+), 73 deletions(-) diff --git a/lib/src/components/wall/AgentBrowserScreenModal.tsx b/lib/src/components/wall/AgentBrowserScreenModal.tsx index ad469793..8cae99a3 100644 --- a/lib/src/components/wall/AgentBrowserScreenModal.tsx +++ b/lib/src/components/wall/AgentBrowserScreenModal.tsx @@ -22,6 +22,7 @@ import { ArrowSquareOutIcon, CheckIcon, FrameCornersIcon, + type Icon, LinkIcon, LockSimpleIcon, XIcon, @@ -115,7 +116,7 @@ export function AgentBrowserScreenModal({ // under the screencast render option (or standalone when the surface can't // swap render mode), and greyed whenever the active mode isn't screencast. const viewportControls = ( -
+
Resolution