Fix browser canvas startup viewport settle

Wait for the right-canvas browser surface to finish its opening transition before using its dimensions as the Playwright viewport. Measure raw stage dimensions for stability, then apply the existing clamped viewport values so initial screencasts do not render into a stretched canvas. Add a browser regression guard for the raw viewport settle path.

Alessandro committed Apr 27, 2026 at 17:35 UTC e412f5faf71820da97d6691f7fab96cc4e08d9fc
2 files changed +41 -8
plugins/_browser/webui/browser-store.js
+28 -8
@@ -13,6 +13,8 @@ const BROWSER_FIRST_INSTALL_TIMEOUT_MS = 300000;
13 const BROWSER_CONFIG_REFRESH_MS = 15000;
14 const VIEWPORT_SYNC_DEBOUNCE_MS = 220;
15 const VIEWPORT_SYNC_SIZE_TOLERANCE = 4;
16 +const CANVAS_VIEWPORT_SETTLE_MS = 260;
17 +const SURFACE_VIEWPORT_STABLE_FRAMES = 2;
18 const ANNOTATION_DRAG_THRESHOLD = 6;
19 const ANNOTATION_MAX_COMMENTS = 24;
20 const ANNOTATION_DOM_LIMIT = 1200;
@@ -91,6 +93,7 @@ const model = {
93 _mode: "",
94 _surfaceMounted: false,
95 _surfaceSwitching: false,
96 + _surfaceOpenedAt: 0,
97 _connectSequence: 0,
98 _viewerToken: "",
99 extensionMenuOpen: false,
@@ -403,6 +406,7 @@ const model = {
406 const targetBrowserId = requestedBrowserId || this.activeBrowserId || this.firstBrowserId();
407 this._mode = nextMode;
408 this._surfaceMounted = true;
409 + this._surfaceOpenedAt = Date.now();
410 this._lastViewportKey = "";
411 if (!modeChanged && (this.frameSrc || !targetBrowserId)) return;
412
@@ -442,12 +446,17 @@ const model = {
446 let stableCount = 0;
447 for (let index = 0; index < 24; index += 1) {
448 await nextAnimationFrame();
445 - const viewport = this.currentViewportSize();
449 + const viewport = this.surfaceViewportMeasurement();
450 if (!viewport) continue;
447 - const key = `${viewport.width}x${viewport.height}`;
451 + const key = `${viewport.rawWidth}x${viewport.rawHeight}`;
452 if (key === lastKey) {
453 stableCount += 1;
450 - if (stableCount >= 2) return viewport;
454 + const canvasSettled = this._mode !== "canvas"
455 + || !this._surfaceOpenedAt
456 + || Date.now() - this._surfaceOpenedAt >= CANVAS_VIEWPORT_SETTLE_MS;
457 + if (canvasSettled && stableCount >= SURFACE_VIEWPORT_STABLE_FRAMES) {
458 + return { width: viewport.width, height: viewport.height };
459 + }
460 } else {
461 stableCount = 0;
462 lastKey = key;
@@ -1286,15 +1295,26 @@ const model = {
1295 },
1296
1297 currentViewportSize() {
1298 + const measurement = this.surfaceViewportMeasurement();
1299 + if (!measurement) return null;
1300 + return {
1301 + width: measurement.width,
1302 + height: measurement.height,
1303 + };
1304 + },
1305 +
1306 + surfaceViewportMeasurement() {
1307 const stage = this._stageElement;
1308 if (!stage) return null;
1309 const rect = stage.getBoundingClientRect?.();
1292 - const width = Math.round(rect?.width || stage.clientWidth || 0);
1293 - const height = Math.round(rect?.height || stage.clientHeight || 0);
1294 - if (width < 80 || height < 80) return null;
1310 + const rawWidth = Math.round(rect?.width || stage.clientWidth || 0);
1311 + const rawHeight = Math.round(rect?.height || stage.clientHeight || 0);
1312 + if (rawWidth < 80 || rawHeight < 80) return null;
1313 return {
1296 - width: Math.max(320, width),
1297 - height: Math.max(200, height),
1314 + rawWidth,
1315 + rawHeight,
1316 + width: Math.max(320, rawWidth),
1317 + height: Math.max(200, rawHeight),
1318 };
1319 },
1320
tests/test_browser_agent_regressions.py
+13
@@ -347,6 +347,19 @@ def test_browser_viewer_allows_slow_extension_startup():
347 assert "Installing Chromium for the first Browser run" in js
348
349
350 +def test_browser_canvas_startup_waits_for_raw_viewport_settle():
351 + js = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-store.js").read_text(
352 + encoding="utf-8"
353 + )
354 +
355 + assert "const CANVAS_VIEWPORT_SETTLE_MS = 260;" in js
356 + assert "surfaceViewportMeasurement()" in js
357 + assert "rawWidth" in js
358 + assert "rawHeight" in js
359 + assert "const key = `${viewport.rawWidth}x${viewport.rawHeight}`;" in js
360 + assert "Date.now() - this._surfaceOpenedAt >= CANVAS_VIEWPORT_SETTLE_MS" in js
361 +
362 +
363 def test_browser_ui_spinners_have_browser_local_animation():
364 main_html = (PROJECT_ROOT / "plugins" / "_browser" / "webui" / "browser-panel.html").read_text(
365 encoding="utf-8"