Skip to content

Commit fb2d11f

Browse files
committed
fix(web): bound preview mutations and viewport waits
- reject expired open mutations before starting side effects - clamp viewport polling and revalidate runtime identity - hide inactive capture guests from host assistive technology - document focused tests and integrated verification limits 🤖 Co-authored by GPT-5 in Codex via T3 Code
1 parent c649f2d commit fb2d11f

7 files changed

Lines changed: 137 additions & 31 deletions

BRANCH_DETAILS.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,10 @@ Expected behavior:
2727
- A background snapshot that times out before desktop capture begins releases its presentation lease even when Chromium has paused compositor-frame callbacks. Once desktop capture starts, a timed-out snapshot retains its presentation lease until that capture settles, so response timeouts cannot tear down compositor staging beneath an in-flight capture. The desktop snapshot receives the operation's remaining timeout and bounds its control session accordingly.
2828
- The shared preview contract treats snapshot screenshots as nullable. MCP snapshot responses omit image content when capture is unavailable while preserving structured semantic content and explicitly reporting `screenshot: null`; tool descriptions promise a PNG only when capture is available. In `packages/contracts/src/ipc.ts`, the branch's desktop snapshot schema and upstream's create-tab defaults coexist: snapshot calls default an omitted `background` flag to `false`, while tab creation carries viewport, zoom, and color-scheme defaults through the preload and desktop manager.
2929
- The renderer automation consumer reserves response grace before the broker deadline and converts a stalled host operation into a typed `PreviewAutomationTimeoutError` instead of leaving the broker to surface a generic execution failure. Short caller-supplied timeouts retain their full execution budget, and the transition into grace reservation remains monotonic as requested timeouts increase. Requests that ask to open the inline preview use the request's remaining bounded visibility budget rather than a fixed two-second ceiling. Best-effort presentation settling uses a non-throwing remaining-budget read and clamps its 500-millisecond ceiling to that budget. Overlay status calls are themselves bounded by the remaining deadline and revalidate runtime guest identity after awaiting the desktop bridge, while overlay, navigation, and visibility polling clamp each sleep to the remaining deadline. Stable-presentation dwell also contracts to fit short deadlines instead of requiring an impossible fixed 100 milliseconds. Reused empty or failed tabs acknowledge without waiting for a browser surface those states intentionally hide. Visibility timeouts separately report the inline preview's selected tab, the right panel's active surface and open state, the active presentation kind, whether the requested browser surface was registered, and whether it had a presentation rectangle.
30-
- `PreviewAutomationHosts.tsx` resolves `browserDefaults.ts` once at the start of each automation-open request, before taking the session snapshot that pins a reused runtime guest. That single settings snapshot supplies the new-tab viewport and the automatic floating-preview preference, so creation and presentation cannot observe different settings during one request, while cold settings hydration cannot mix a pre-await session snapshot with a post-await server epoch. Explicit `open` or its deprecated `show` alias remains authoritative; when both are omitted, `autoShowFloatingPreview` decides presentation. The resulting `shouldPresentPreview` value is passed unchanged to `previewAutomationOpenReadiness.ts`, so a reused rendered tab left in the background does not wait for visibility while an explicitly shown tab does. A newly created tab applies its server snapshot and assigned tab id, uses the configured viewport or the branch's deterministic 1280×800 fallback when the snapshot remains `fill`, initiates any requested selection, and acknowledges server-side creation without depending on cold React panel rendering, Electron overlay registration, or page readiness. Its initial URL continues loading exactly once in that same tab; later wait, snapshot, or interaction operations own attachment and page readiness. Reopening an existing shown tab selects both the preview-state tab and its matching inline mini-player surface, then waits for stable presentation and reasserts that selection across same-server route hydration or session reconciliation. A server-epoch change aborts the pending open with `PreviewAutomationTargetUnavailableError`; the old request never adopts the replacement runtime guest.
30+
- `PreviewAutomationHosts.tsx` resolves `browserDefaults.ts` once at the start of each automation-open request, before taking the session snapshot that pins a reused runtime guest. That single settings snapshot supplies the new-tab viewport and the automatic floating-preview preference, so creation and presentation cannot observe different settings during one request, while cold settings hydration cannot mix a pre-await session snapshot with a post-await server epoch. Explicit `open` or its deprecated `show` alias remains authoritative; when both are omitted, `autoShowFloatingPreview` decides presentation. The resulting `shouldPresentPreview` value is passed unchanged to `previewAutomationOpenReadiness.ts`, so a reused rendered tab left in the background does not wait for visibility while an explicitly shown tab does. After settings and session synchronization, the host rechecks its remaining deadline immediately before tab creation and every later irreversible open-side mutation, so an already expired request cannot create, resize, reveal, or navigate a preview. A newly created tab applies its server snapshot and assigned tab id, uses the configured viewport or the branch's deterministic 1280×800 fallback when the snapshot remains `fill`, initiates any requested selection, and acknowledges server-side creation without depending on cold React panel rendering, Electron overlay registration, or page readiness. Its initial URL continues loading exactly once in that same tab; later wait, snapshot, or interaction operations own attachment and page readiness. Reopening an existing shown tab selects both the preview-state tab and its matching inline mini-player surface, then waits for stable presentation and reasserts that selection across same-server route hydration or session reconciliation. A server-epoch change aborts the pending open with `PreviewAutomationTargetUnavailableError`; the old request never adopts the replacement runtime guest.
3131
- The standard dev runner keeps local navigation and direct backend URLs on `127.0.0.1`. Browser modes use a single origin: they leave client HTTP/WebSocket URLs and generic `HOST` unset so remote sharing and origin-derived HMR keep working, while Vite's default listener and its default backend proxy use explicit IPv4 loopback. Explicit IPv6 backend binds proxy through IPv6 loopback. Desktop mode pins `HOST` and its renderer/backend URLs to `127.0.0.1`; server-only mode keeps direct HTTP/WebSocket URLs on the same IPv4 loopback.
32-
- Retained browser guests subscribe only to render state for their own epoch-scoped runtime tab, plus their thread's active panel and mini-player selection. Presentation or background-capture updates for another tab do not rerender every mounted `HostedBrowserWebview`; selection changes override a stale surface-visible flag so background staging remains nearly transparent and exposes its readiness marker, and staging derives its viewport-fitted rectangle from the target tab's own stable rectangle.
32+
- Retained browser guests subscribe only to render state for their own epoch-scoped runtime tab, plus their thread's active panel and mini-player selection. Presentation or background-capture updates for another tab do not rerender every mounted `HostedBrowserWebview`; selection changes override a stale surface-visible flag so background staging remains nearly transparent and exposes its readiness marker, and staging derives its viewport-fitted rectangle from the target tab's own stable rectangle. Only the active guest is exposed through the host accessibility tree; a nearly transparent background-capture guest stays `aria-hidden` without affecting CDP accessibility-tree collection against the guest target.
33+
- Rendered viewport readiness revalidates the epoch-scoped runtime guest after each awaited guest measurement and clamps every polling sleep to the remaining resize deadline, preserving typed target-replacement and viewport-timeout failures for short budgets.
3334
- The primary pairing route watches for later URL-fragment changes while it remains mounted. Navigating an already-loaded `/pair` document to `/pair#token=...` claims each new token once, removes the secret fragment, and runs the normal pairing exchange without requiring a reload or a second desktop window. Multiple tokens received while an exchange is pending are serialized, and the submitting state remains active until every queued exchange settles.
3435
- `dev:desktop` derives `T3CODE_DESKTOP_USER_DATA_DIR=<resolved base dir>/userdata/electron` whenever the runner has an explicit base directory. Desktop configuration resolves that override to an absolute path, and app identity uses it before legacy migration or the normal Electron user-data default. This keeps an isolated worktree dev desktop from reusing an installed or earlier development profile whose incompatible IndexedDB schema can prevent the renderer from starting. Packaged/default startup remains unchanged when no override is supplied.
3536

@@ -66,18 +67,19 @@ Primary files:
6667
- `packages/contracts/src/ipc.ts`
6768
- `scripts/dev-runner.ts`
6869

69-
Focused regression coverage lives in `scripts/dev-runner.test.ts`, `apps/desktop/src/app/DesktopAppIdentity.test.ts`, `apps/desktop/src/app/DesktopEnvironment.test.ts`, `apps/desktop/src/preview/Manager.test.ts`, `apps/server/src/mcp/McpHttpServer.test.ts`, `apps/web/src/browser/browserSurfaceStore.test.ts`, `apps/web/src/browser/hostedBrowserWebviewStyle.test.ts`, `apps/web/src/browser/browserViewportActions.test.ts`, `apps/web/src/browser/browserViewportLayout.test.ts`, `apps/web/src/browser/previewRuntimeTabId.test.ts`, `apps/web/src/components/auth/PairingRouteSurface.logic.test.ts`, `apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts`, `apps/web/src/components/preview/previewAutomationOverlayReadiness.test.ts`, `apps/web/src/components/preview/previewAutomationPresentation.test.ts`, `apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts`, `apps/web/src/components/preview/previewNavigationReadiness.test.ts`, `apps/web/src/components/preview/previewViewportRollback.test.ts`, `packages/contracts/src/ipc.test.ts`, and `packages/contracts/src/preview.test.ts`.
70+
Focused regression coverage lives in `scripts/dev-runner.test.ts`, `apps/desktop/src/app/DesktopAppIdentity.test.ts`, `apps/desktop/src/app/DesktopEnvironment.test.ts`, `apps/desktop/src/preview/Manager.test.ts`, `apps/server/src/mcp/McpHttpServer.test.ts`, `apps/web/src/browser/browserSurfaceStore.test.ts`, `apps/web/src/browser/hostedBrowserWebviewStyle.test.ts`, `apps/web/src/browser/browserViewportActions.test.ts`, `apps/web/src/browser/browserViewportLayout.test.ts`, `apps/web/src/browser/previewRuntimeTabId.test.ts`, `apps/web/src/components/auth/PairingRouteSurface.logic.test.ts`, `apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts`, `apps/web/src/components/preview/previewAutomationOverlayReadiness.test.ts`, `apps/web/src/components/preview/previewAutomationPresentation.test.ts`, `apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts`, `apps/web/src/components/preview/previewNavigationReadiness.test.ts`, `apps/web/src/components/preview/previewViewportReadiness.test.ts`, `apps/web/src/components/preview/previewViewportRollback.test.ts`, `packages/contracts/src/ipc.test.ts`, and `packages/contracts/src/preview.test.ts`.
7071

7172
```sh
72-
vp test run scripts/dev-runner.test.ts apps/desktop/src/app/DesktopAppIdentity.test.ts apps/desktop/src/app/DesktopEnvironment.test.ts apps/desktop/src/preview/Manager.test.ts apps/server/src/mcp/McpHttpServer.test.ts apps/web/src/browser/browserSurfaceStore.test.ts apps/web/src/browser/hostedBrowserWebviewStyle.test.ts apps/web/src/browser/browserViewportActions.test.ts apps/web/src/browser/browserViewportLayout.test.ts apps/web/src/browser/previewRuntimeTabId.test.ts apps/web/src/components/auth/PairingRouteSurface.logic.test.ts apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts apps/web/src/components/preview/previewAutomationOverlayReadiness.test.ts apps/web/src/components/preview/previewAutomationPresentation.test.ts apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts apps/web/src/components/preview/previewNavigationReadiness.test.ts apps/web/src/components/preview/previewViewportRollback.test.ts packages/contracts/src/ipc.test.ts packages/contracts/src/preview.test.ts
73+
vp test run scripts/dev-runner.test.ts apps/desktop/src/app/DesktopAppIdentity.test.ts apps/desktop/src/app/DesktopEnvironment.test.ts apps/desktop/src/preview/Manager.test.ts apps/server/src/mcp/McpHttpServer.test.ts apps/web/src/browser/browserSurfaceStore.test.ts apps/web/src/browser/hostedBrowserWebviewStyle.test.ts apps/web/src/browser/browserViewportActions.test.ts apps/web/src/browser/browserViewportLayout.test.ts apps/web/src/browser/previewRuntimeTabId.test.ts apps/web/src/components/auth/PairingRouteSurface.logic.test.ts apps/web/src/components/preview/previewAutomationOpenReadiness.test.ts apps/web/src/components/preview/previewAutomationOverlayReadiness.test.ts apps/web/src/components/preview/previewAutomationPresentation.test.ts apps/web/src/components/preview/previewAutomationRequestConsumer.test.ts apps/web/src/components/preview/previewNavigationReadiness.test.ts apps/web/src/components/preview/previewViewportReadiness.test.ts apps/web/src/components/preview/previewViewportRollback.test.ts packages/contracts/src/ipc.test.ts packages/contracts/src/preview.test.ts
7374
```
7475

7576
Current verification:
7677

77-
- The branch-focused suite passed 254 tests across 17 files on Windows. The two unchanged desktop path-fixture files in the command above currently produce eight POSIX-versus-Windows path assertion failures; the affected runtime behavior passes in the remaining focused suite.
78+
- The branch-focused suite passed 261 tests across 18 files on Windows, including the new short-deadline viewport polling, post-read runtime-replacement, and background-guest accessibility regressions. The two unchanged desktop path-fixture files in the command above currently produce eight POSIX-versus-Windows path assertion failures; the affected runtime behavior passes in the remaining focused suite.
7879
- The incoming browser-default, provider-access, pull-request budget, tooltip, update-copy, and settings coverage passed 552 tests across 26 files. The focused open-policy and open-session subset passes 18 tests, including a shared explicit-over-default presentation decision and a reused rendered tab that remains in the background without a visibility wait.
7980
- Desktop, server, contracts, and mobile typechecks pass. Web typecheck currently reports 16 existing errors in `BranchToolbarBranchSelector.tsx`, `ModelPickerContent.tsx`, `PreviewAutomationHosts.tsx` at the pre-existing registry access, `FontFamilyPicker.tsx`, `use-atom-command.ts`, and `use-atom-query-runner.ts`; none are in the browser-default integration changed here.
8081
- An isolated web client on ports `5744`/`13784` paired and loaded successfully. Settings → Integrations exposed the browser defaults and agent-access setting; agent browser access could be disabled and restored. The non-Electron client correctly disabled desktop-only viewport, zoom, and appearance controls.
82+
- The review-follow-up isolated stack on ports `5744`/`13784` served the React app shell with HTTP 200. No current integrated UI result is available for the follow-up changes because the in-app browser inventory was empty, Playwright was not installed, and the Windows control pipe was unavailable; this is a verification-harness blocker rather than a product failure or pass.
8183
- Android paired with an isolated server, loaded the seeded project, created a thread, and received a response. The changed Android thread-settings header remains visually unverified because Metro first exhausted file handles and a clean restart then exposed an installed-dev-client `EventEmitter` runtime mismatch.
8284

8385
## Development Ports

apps/web/src/browser/HostedBrowserWebview.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { BrowserDeviceToolbar } from "./BrowserDeviceToolbar";
2424
import { BrowserViewportResizeHandles } from "./BrowserViewportResizeHandles";
2525
import { acquireDesktopTab, type AcquiredDesktopTab } from "./desktopTabLifetime";
2626
import {
27+
resolveHostedBrowserWebviewAriaHidden,
2728
resolveHostedBrowserWebviewPresentation,
2829
resolveHostedBrowserWebviewWrapperStyle,
2930
} from "./hostedBrowserWebviewStyle";
@@ -308,7 +309,7 @@ export function HostedBrowserWebview(props: {
308309
? Math.max(1, Math.round(layout.viewportHeight / normalizedZoomFactor))
309310
: effectiveViewport.height
310311
}
311-
aria-hidden={active || backgroundCapture ? undefined : true}
312+
aria-hidden={resolveHostedBrowserWebviewAriaHidden(active)}
312313
className={cn(
313314
"absolute flex overflow-hidden bg-background",
314315
active && !layout.fillsPanel && "ring-1 ring-border/70 shadow-sm",

apps/web/src/browser/hostedBrowserWebviewStyle.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,18 @@ import {
44
BACKGROUND_CAPTURE_BROWSER_WEBVIEW_OPACITY,
55
BACKGROUND_CAPTURE_BROWSER_WEBVIEW_Z_INDEX,
66
HIDDEN_BROWSER_WEBVIEW_OFFSET,
7+
resolveHostedBrowserWebviewAriaHidden,
78
resolveHostedBrowserWebviewPresentation,
89
resolveHostedBrowserWebviewWrapperStyle,
910
} from "./hostedBrowserWebviewStyle";
1011

12+
describe("resolveHostedBrowserWebviewAriaHidden", () => {
13+
it("exposes only the active guest to host assistive technology", () => {
14+
expect(resolveHostedBrowserWebviewAriaHidden(true)).toBeUndefined();
15+
expect(resolveHostedBrowserWebviewAriaHidden(false)).toBe(true);
16+
});
17+
});
18+
1119
describe("resolveHostedBrowserWebviewPresentation", () => {
1220
it("stages a background capture when visibility is stale after selection changes", () => {
1321
expect(

apps/web/src/browser/hostedBrowserWebviewStyle.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export const HIDDEN_BROWSER_WEBVIEW_OFFSET = -100_000;
2121
export const BACKGROUND_CAPTURE_BROWSER_WEBVIEW_Z_INDEX = 31;
2222
export const BACKGROUND_CAPTURE_BROWSER_WEBVIEW_OPACITY = 0.001;
2323

24+
export function resolveHostedBrowserWebviewAriaHidden(active: boolean): true | undefined {
25+
return active ? undefined : true;
26+
}
27+
2428
export function resolveHostedBrowserWebviewPresentation(input: {
2529
readonly backgroundCaptureRequested: boolean;
2630
readonly hasRect: boolean;

0 commit comments

Comments
 (0)