Fix Browser viewer input after stream restarts

Notify screencast consumers when a stream is stopped so the WebSocket frame supervisor restarts after viewport-driven screencast resets.

Keep the active canvas as the frame/input surface and avoid replacing live screencasts with click snapshots, preserving hover, click, and wheel feedback.

Tested: conda run -n a0 pytest tests/test_browser_agent_regressions.py -q
This commit is contained in:
Alessandro 2026-07-04 16:21:46 +02:00
parent 153ffcc221
commit 9bfa3b5c81
3 changed files with 60 additions and 9 deletions

View file

@ -411,8 +411,11 @@ class _BrowserScreencast:
async def stop(self) -> None:
if self._closed:
return
was_stopped = self.stopped
self._closed = True
self.stopped = True
if not was_stopped:
self._notify_stopped()
self._drop_queued_frames()
with contextlib.suppress(asyncio.QueueFull):
self.queue.put_nowait(None)

View file

@ -1412,8 +1412,15 @@ const model = {
this._frameCanvas = canvas || null;
},
currentFrameCanvas() {
const stageCanvas = this._stageElement?.querySelector?.(".browser-frame-canvas");
if (stageCanvas?.isConnected) return stageCanvas;
if (this._frameCanvas?.isConnected) return this._frameCanvas;
return null;
},
canUseCanvasFrames() {
return Boolean(BROWSER_CANVAS_FRAMES_SUPPORTED && this._frameCanvas?.getContext);
return Boolean(BROWSER_CANVAS_FRAMES_SUPPORTED && this.currentFrameCanvas()?.getContext);
},
hasFrame() {
@ -1421,7 +1428,7 @@ const model = {
},
paintFrameBitmap(bitmap) {
const canvas = this._frameCanvas;
const canvas = this.currentFrameCanvas();
if (!canvas || !bitmap?.width || !bitmap?.height) return false;
if (canvas.width !== bitmap.width) canvas.width = bitmap.width;
if (canvas.height !== bitmap.height) canvas.height = bitmap.height;
@ -1433,7 +1440,7 @@ const model = {
},
clearFrameCanvas() {
const canvas = this._frameCanvas;
const canvas = this.currentFrameCanvas();
if (canvas?.width && canvas?.height) {
canvas.getContext("2d")?.clearRect(0, 0, canvas.width, canvas.height);
}
@ -1441,9 +1448,10 @@ const model = {
},
freezeCanvasFrameToImage() {
if (!this.frameCanvasReady || !this._frameCanvas) return;
const canvas = this.currentFrameCanvas();
if (!this.frameCanvasReady || !canvas) return;
try {
this.frameSrc = this._frameCanvas.toDataURL("image/jpeg", 0.86);
this.frameSrc = canvas.toDataURL("image/jpeg", 0.86);
} catch {
this.frameSrc = "";
}
@ -1451,8 +1459,9 @@ const model = {
},
frameElement() {
if (this.frameCanvasReady && this._frameCanvas) {
return this._frameCanvas;
if (this.frameCanvasReady) {
const canvas = this.currentFrameCanvas();
if (canvas) return canvas;
}
return this._stageElement?.querySelector?.(".browser-frame-image") || null;
},
@ -2616,7 +2625,9 @@ const model = {
const response = await websocket.request("browser_viewer_input", payload, { timeoutMs: 10000 });
const data = firstOk(response);
this.applyActiveFrameState(data.state);
this.applySnapshot(data.snapshot);
if (!this.frameCanvasReady || !this.usesScreencastTransport()) {
this.applySnapshot(data.snapshot);
}
} catch (error) {
this.error = error instanceof Error ? error.message : String(error);
}