mirror of
https://github.com/agent0ai/agent-zero.git
synced 2026-07-09 17:08:29 +00:00
Make Browser viewer CDP-live by default
Default the WebUI Browser surface back to live CDP screencast while keeping snapshot/state transport as a fallback. Carry viewer transport through the WebSocket API and Browser store, use screencast metadata to avoid extra frame dimension decode, and include frame-chain action metadata in Browser content results. Update Browser prompts, skill guidance, DOX, and focused regressions so agent actions prefer DOM/CDP refs while the visible Browser remains responsive.
This commit is contained in:
parent
b9153f718e
commit
d398656d33
7 changed files with 211 additions and 20 deletions
|
|
@ -16,12 +16,16 @@
|
|||
## Local Contracts
|
||||
|
||||
- Keep browser actions safe around external pages, credentials, and user data.
|
||||
- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility.
|
||||
- Preserve Playwright lifecycle cleanup and WebSocket viewer compatibility across regular host browsers and Electron WebContentsView embedding.
|
||||
- Keep the WebUI Browser inside its own modal/canvas affordance; do not replace it with page-level navigation.
|
||||
- Default the visible WebUI Browser to live CDP screencast for responsiveness. Keep lightweight CDP/DOM state snapshots as the fallback transport.
|
||||
- Prefer DOM/CDP browser actions with refs, selectors, frame-chain refs, and screenshots over viewport coordinate input. Coordinates remain a visual fallback.
|
||||
- Do not hardcode user-specific browser paths or secrets.
|
||||
|
||||
## Work Guidance
|
||||
|
||||
- Coordinate tool, helper, and panel changes so browser state shown in the UI matches tool behavior.
|
||||
- Do not depend on nested Electron `<webview>` support or launcher-specific preload bridges unless the launcher exposes that bridge as an explicit contract.
|
||||
- Keep `prompts/agent.system.tool.browser.md` as a compact callable contract; move detailed browser workflows into `skills/browser-automation/SKILL.md`.
|
||||
- Keep `skills/browser-automation/SKILL.md` frontmatter triggers current with rendered browsing, host-browser, screenshot, and web-interaction user phrasing so relevant-skill recall can surface the skill before the full browser workflow is needed.
|
||||
- Keep fragile form guidance progressively disclosed through `skills/browser-form-workflows/SKILL.md`, linked from the browser prompt through `browser-automation`.
|
||||
|
|
|
|||
|
|
@ -14,7 +14,11 @@ from plugins._browser.helpers.runtime import get_runtime, list_runtime_sessions
|
|||
FRAME_IDLE_POLL_SECONDS = 0.05
|
||||
FRAME_RETRY_DELAY_SECONDS = 0.5
|
||||
FRAME_STATE_REFRESH_SECONDS = 0.75
|
||||
SNAPSHOT_STATE_POLL_SECONDS = 0.75
|
||||
SCREENCAST_QUALITY = 92
|
||||
VIEWER_TRANSPORT_SCREENCAST = "screencast"
|
||||
VIEWER_TRANSPORT_SNAPSHOT = "snapshot"
|
||||
VIEWER_TRANSPORTS = {VIEWER_TRANSPORT_SCREENCAST, VIEWER_TRANSPORT_SNAPSHOT}
|
||||
|
||||
|
||||
class WsBrowser(WsHandler):
|
||||
|
|
@ -92,11 +96,14 @@ class WsBrowser(WsHandler):
|
|||
if existing:
|
||||
existing.cancel()
|
||||
viewer_id = str(data.get("viewer_id") or "")
|
||||
viewer_transport = self._viewer_transport(data)
|
||||
snapshot = None
|
||||
if runtime:
|
||||
self._streams[stream_key] = asyncio.create_task(
|
||||
self._stream_frames(sid, context_id, active_id, viewer_id)
|
||||
)
|
||||
if viewer_transport == VIEWER_TRANSPORT_SCREENCAST:
|
||||
stream_task = self._stream_frames(sid, context_id, active_id, viewer_id)
|
||||
else:
|
||||
stream_task = self._stream_state(sid, context_id, active_id, viewer_id)
|
||||
self._streams[stream_key] = asyncio.create_task(stream_task)
|
||||
snapshot = await self._snapshot_for_browser(runtime, active_id)
|
||||
|
||||
return {
|
||||
|
|
@ -107,6 +114,7 @@ class WsBrowser(WsHandler):
|
|||
"browsers": await self._all_browser_tabs(),
|
||||
"all_browsers": True,
|
||||
"viewer_id": viewer_id,
|
||||
"viewer_transport": viewer_transport,
|
||||
}
|
||||
|
||||
def _unsubscribe(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
|
||||
|
|
@ -215,6 +223,7 @@ class WsBrowser(WsHandler):
|
|||
"browsers": all_browsers,
|
||||
"all_browsers": True,
|
||||
"last_interacted_browser_id": last_interacted_browser_id,
|
||||
"viewer_transport": self._viewer_transport(data),
|
||||
},
|
||||
correlation_id=data.get("correlationId"),
|
||||
)
|
||||
|
|
@ -228,6 +237,7 @@ class WsBrowser(WsHandler):
|
|||
"command": command,
|
||||
"browser_id": browser_id,
|
||||
"viewer_id": viewer_id,
|
||||
"viewer_transport": self._viewer_transport(data),
|
||||
}
|
||||
|
||||
async def _input(self, data: dict[str, Any], sid: str) -> dict[str, Any] | WsResult:
|
||||
|
|
@ -374,7 +384,12 @@ class WsBrowser(WsHandler):
|
|||
try:
|
||||
runtime = await get_runtime(context_id, create=False)
|
||||
if not runtime:
|
||||
await self._emit_empty_frame(sid, context_id, viewer_id=viewer_id)
|
||||
await self._emit_empty_frame(
|
||||
sid,
|
||||
context_id,
|
||||
viewer_id=viewer_id,
|
||||
frame_source=VIEWER_TRANSPORT_SCREENCAST,
|
||||
)
|
||||
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
|
||||
continue
|
||||
|
||||
|
|
@ -382,7 +397,13 @@ class WsBrowser(WsHandler):
|
|||
browsers = listing.get("browsers") or []
|
||||
active_id = self._active_browser_id(listing, browser_id)
|
||||
if not active_id:
|
||||
await self._emit_empty_frame(sid, context_id, browsers=browsers, viewer_id=viewer_id)
|
||||
await self._emit_empty_frame(
|
||||
sid,
|
||||
context_id,
|
||||
browsers=browsers,
|
||||
viewer_id=viewer_id,
|
||||
frame_source=VIEWER_TRANSPORT_SCREENCAST,
|
||||
)
|
||||
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
|
||||
continue
|
||||
|
||||
|
|
@ -407,6 +428,7 @@ class WsBrowser(WsHandler):
|
|||
"mime": "",
|
||||
"state": state,
|
||||
"frame_source": "state",
|
||||
"viewer_transport": VIEWER_TRANSPORT_SCREENCAST,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -435,7 +457,8 @@ class WsBrowser(WsHandler):
|
|||
frame["browser_id"] = active_id
|
||||
frame["browsers"] = browsers
|
||||
frame["state"] = state
|
||||
frame["frame_source"] = "screencast"
|
||||
frame["frame_source"] = VIEWER_TRANSPORT_SCREENCAST
|
||||
frame["viewer_transport"] = VIEWER_TRANSPORT_SCREENCAST
|
||||
await self.emit_to(sid, "browser_viewer_frame", frame)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
|
|
@ -447,6 +470,70 @@ class WsBrowser(WsHandler):
|
|||
await runtime.call("stop_screencast", stream_id)
|
||||
stream_id = None
|
||||
|
||||
async def _stream_state(
|
||||
self,
|
||||
sid: str,
|
||||
context_id: str,
|
||||
browser_id: int | str | None,
|
||||
viewer_id: str = "",
|
||||
) -> None:
|
||||
last_signature = None
|
||||
while True:
|
||||
try:
|
||||
runtime = await get_runtime(context_id, create=False)
|
||||
if not runtime:
|
||||
signature = (None, ())
|
||||
if signature != last_signature:
|
||||
await self._emit_empty_frame(
|
||||
sid,
|
||||
context_id,
|
||||
viewer_id=viewer_id,
|
||||
frame_source=VIEWER_TRANSPORT_SNAPSHOT,
|
||||
)
|
||||
last_signature = signature
|
||||
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
|
||||
continue
|
||||
|
||||
listing = await runtime.call("list")
|
||||
browsers = listing.get("browsers") or []
|
||||
active_id = self._active_browser_id(listing, browser_id)
|
||||
state = self._state_for_browser(browsers, active_id, None) if active_id else None
|
||||
signature = (
|
||||
str(active_id or ""),
|
||||
tuple(
|
||||
(
|
||||
str(browser.get("context_id") or context_id),
|
||||
str(browser.get("id") or ""),
|
||||
str(browser.get("currentUrl") or ""),
|
||||
str(browser.get("title") or ""),
|
||||
bool(browser.get("loading")),
|
||||
)
|
||||
for browser in browsers
|
||||
),
|
||||
)
|
||||
if signature != last_signature:
|
||||
await self.emit_to(
|
||||
sid,
|
||||
"browser_viewer_frame",
|
||||
{
|
||||
"context_id": context_id,
|
||||
"viewer_id": viewer_id,
|
||||
"browser_id": active_id,
|
||||
"browsers": browsers,
|
||||
"image": "",
|
||||
"mime": "",
|
||||
"state": state,
|
||||
"frame_source": VIEWER_TRANSPORT_SNAPSHOT,
|
||||
"viewer_transport": VIEWER_TRANSPORT_SNAPSHOT,
|
||||
},
|
||||
)
|
||||
last_signature = signature
|
||||
await asyncio.sleep(SNAPSHOT_STATE_POLL_SECONDS)
|
||||
except asyncio.CancelledError:
|
||||
raise
|
||||
except Exception:
|
||||
await asyncio.sleep(FRAME_RETRY_DELAY_SECONDS)
|
||||
|
||||
@staticmethod
|
||||
def _active_browser_id(
|
||||
listing: dict[str, Any],
|
||||
|
|
@ -484,6 +571,7 @@ class WsBrowser(WsHandler):
|
|||
*,
|
||||
browsers: list[dict[str, Any]] | None = None,
|
||||
viewer_id: str = "",
|
||||
frame_source: str = "",
|
||||
) -> None:
|
||||
await self.emit_to(
|
||||
sid,
|
||||
|
|
@ -496,9 +584,28 @@ class WsBrowser(WsHandler):
|
|||
"image": "",
|
||||
"mime": "",
|
||||
"state": None,
|
||||
"frame_source": frame_source,
|
||||
"viewer_transport": frame_source or VIEWER_TRANSPORT_SNAPSHOT,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _viewer_transport(data: dict[str, Any]) -> str:
|
||||
raw = (
|
||||
data.get("viewer_transport")
|
||||
or data.get("viewerTransport")
|
||||
or data.get("surface_transport")
|
||||
or data.get("surfaceTransport")
|
||||
or data.get("transport")
|
||||
or ""
|
||||
)
|
||||
normalized = str(raw or "").strip().lower().replace("-", "_")
|
||||
if normalized == "live":
|
||||
normalized = VIEWER_TRANSPORT_SCREENCAST
|
||||
if normalized in VIEWER_TRANSPORTS:
|
||||
return normalized
|
||||
return VIEWER_TRANSPORT_SNAPSHOT
|
||||
|
||||
@staticmethod
|
||||
def _viewport_from_data(data: dict[str, Any]) -> dict[str, int] | None:
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
(() => {
|
||||
const GLOBAL_KEY = "__spaceBrowserPageContent__";
|
||||
const DOM_HELPER_KEY = "__spaceBrowserDomHelper__";
|
||||
const VERSION = "12";
|
||||
const VERSION = "13";
|
||||
const REQUIRED_API_NAMES = Object.freeze([
|
||||
"annotate",
|
||||
"boundingBoxFor",
|
||||
|
|
@ -3103,8 +3103,12 @@
|
|||
|
||||
function buildActionResult(entry, extra = {}) {
|
||||
return {
|
||||
actionStrategy: entry.helperBacked ? "frame_chain_ref" : "dom_ref",
|
||||
captureId: state.captureId,
|
||||
descriptorTags: Array.isArray(entry?.descriptorTags) ? entry.descriptorTags.slice() : [],
|
||||
frameChain: Array.isArray(entry?.frameChain) ? entry.frameChain.slice() : [],
|
||||
frameId: entry.frameId || "",
|
||||
nodeId: entry.nodeId || "",
|
||||
referenceId: entry.referenceId,
|
||||
semanticTags: Array.isArray(entry?.semanticTags) ? entry.semanticTags.slice() : [],
|
||||
state: entry.state || collectElementStateMetadata(entry.element, state.captureOptions),
|
||||
|
|
@ -3116,6 +3120,7 @@
|
|||
|
||||
function buildHelperBackedActionResult(entry, helperResult, extra = {}) {
|
||||
return {
|
||||
actionStrategy: "frame_chain_ref",
|
||||
captureId: state.captureId,
|
||||
descriptorTags: Array.isArray(helperResult?.descriptorTags) ? helperResult.descriptorTags : (entry.descriptorTags || []),
|
||||
frameChain: entry.frameChain.slice(),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ Actions: tabs `open`, `list`, `state`, `set_active`, `navigate`, `back`, `forwar
|
|||
|
||||
Rules:
|
||||
- If the user asks for an existing tab, page title, or already-open URL, call `list` first, match by `title` or `currentUrl`, then use `set_active` or `navigate` on that `browser_id`.
|
||||
- Use refs from the latest `content`; same-page controls may use `selector`.
|
||||
- Prefer DOM/CDP actions: use refs from the latest `content`, including frame-chain refs, or same-page `selector`; use coordinates only when refs/selectors are unavailable or the user explicitly asks for visual manual control.
|
||||
- Screenshots are explicit only; the browser does not automatically load screenshots. Call `vision_load` with the returned `vision_load.tool_args.paths` value before reasoning visually.
|
||||
- Keep the tab set small; close pages after extracting what you need.
|
||||
- `multi` is only a browser action: use `tool_name: "browser"` with `tool_args.action: "multi"`. Never use `tool_name: "multi"`.
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ For fragile forms, load `browser-form-workflows` with `skills_tool:load` before
|
|||
|
||||
1. `open` creates a browser tab and returns a `browser_id`.
|
||||
2. `content` returns readable markdown plus typed refs like `[link 3]`, `[button 6]`, `[input text 8]`.
|
||||
3. Interact with refs using `click`, `type`, `submit`, `scroll`, etc.
|
||||
3. Interact with refs using `click`, `type`, `submit`, `scroll`, etc.; iframe/shadow targets may return frame-chain metadata in action results.
|
||||
4. Use `navigate` on an existing `browser_id` for serial browsing.
|
||||
5. Keep only a small working tab set; close pages when finished.
|
||||
6. If the user asks for an existing tab, page title, or already-open URL, call `list` first, match by `title` or `currentUrl`, then use `set_active` or `navigate` on that `browser_id` instead of opening a new tab.
|
||||
|
|
@ -63,8 +63,9 @@ Screenshot args include `quality`, `full_page`, and optional `path`. Without `pa
|
|||
|
||||
## Pointer And Keyboard
|
||||
|
||||
- `hover`, `double_click`, `right_click`, and `drag` accept refs or viewport coordinates.
|
||||
- Coordinates are Chromium viewport CSS pixels and match screenshots.
|
||||
- Prefer refs/selectors and DOM/CDP actions over viewport coordinates.
|
||||
- `hover`, `double_click`, `right_click`, and `drag` accept refs or viewport coordinates when no reliable ref exists.
|
||||
- Coordinates are Chromium viewport CSS pixels and match screenshots; treat them as visual fallback, not the default interaction path.
|
||||
- `key_chord` presses keys in order and releases in reverse.
|
||||
- `clipboard` actions are copy, cut, or paste.
|
||||
- `set_viewport` resizes the page viewport.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ const EXTENSIONS_ROOT = "/a0/usr/_browser/extensions";
|
|||
const BROWSER_SUBSCRIBE_TIMEOUT_MS = 60000;
|
||||
const BROWSER_FIRST_INSTALL_TIMEOUT_MS = 300000;
|
||||
const BROWSER_CONFIG_REFRESH_MS = 15000;
|
||||
const BROWSER_VIEWER_TRANSPORT_SNAPSHOT = "snapshot";
|
||||
const BROWSER_VIEWER_TRANSPORT_SCREENCAST = "screencast";
|
||||
const VIEWPORT_SYNC_DEBOUNCE_MS = 220;
|
||||
const VIEWPORT_SYNC_SIZE_TOLERANCE = 4;
|
||||
const CANVAS_VIEWPORT_SETTLE_MS = 520;
|
||||
|
|
@ -123,6 +125,8 @@ const model = {
|
|||
address: "",
|
||||
frameSrc: "",
|
||||
frameState: null,
|
||||
viewerTransport: BROWSER_VIEWER_TRANSPORT_SCREENCAST,
|
||||
liveScreencastEnabled: true,
|
||||
annotating: false,
|
||||
annotationComments: [],
|
||||
annotationDraft: null,
|
||||
|
|
@ -885,12 +889,42 @@ const model = {
|
|||
if (!this.isCurrentSurfaceOpen(sequence)) {
|
||||
return;
|
||||
}
|
||||
await this.syncViewport(true, { restartStream: this._mode === "canvas" });
|
||||
await this.syncViewport(true, {
|
||||
restartStream: this._mode === "canvas" && this.usesScreencastTransport(),
|
||||
});
|
||||
if (this._mode !== "canvas") return;
|
||||
this.scheduleViewportSyncForSurface(sequence, 240);
|
||||
this.scheduleViewportSyncForSurface(sequence, 520);
|
||||
},
|
||||
|
||||
requestedViewerTransport() {
|
||||
return this.liveScreencastEnabled
|
||||
? BROWSER_VIEWER_TRANSPORT_SCREENCAST
|
||||
: BROWSER_VIEWER_TRANSPORT_SNAPSHOT;
|
||||
},
|
||||
|
||||
normalizeViewerTransport(value = "") {
|
||||
const normalized = String(value || "").trim().toLowerCase().replace("-", "_");
|
||||
if (normalized === BROWSER_VIEWER_TRANSPORT_SCREENCAST) {
|
||||
return BROWSER_VIEWER_TRANSPORT_SCREENCAST;
|
||||
}
|
||||
return BROWSER_VIEWER_TRANSPORT_SNAPSHOT;
|
||||
},
|
||||
|
||||
usesScreencastTransport() {
|
||||
return this.viewerTransport === BROWSER_VIEWER_TRANSPORT_SCREENCAST;
|
||||
},
|
||||
|
||||
frameDimensionsFromMetadata(metadata = null) {
|
||||
if (!metadata || typeof metadata !== "object") return null;
|
||||
const width = Number(metadata.expectedWidth || metadata.deviceWidth || metadata.jpegWidth || 0);
|
||||
const height = Number(metadata.expectedHeight || metadata.deviceHeight || metadata.jpegHeight || 0);
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
|
||||
return null;
|
||||
}
|
||||
return { width, height };
|
||||
},
|
||||
|
||||
scheduleViewportSyncForSurface(sequence, delayMs = 0) {
|
||||
globalThis.setTimeout?.(() => {
|
||||
if (!this.isCurrentSurfaceOpen(sequence) || this._mode !== "canvas") {
|
||||
|
|
@ -951,6 +985,7 @@ const model = {
|
|||
browser_id: requestedBrowserId,
|
||||
viewer_id: viewerToken,
|
||||
create_browser: Boolean(options.createBrowser || options.create_browser),
|
||||
viewer_transport: this.requestedViewerTransport(),
|
||||
viewport_width: initialViewport?.width,
|
||||
viewport_height: initialViewport?.height,
|
||||
},
|
||||
|
|
@ -973,6 +1008,7 @@ const model = {
|
|||
}
|
||||
const data = firstOk(response);
|
||||
this.applyBrowserListing(data.browsers || [], contextId, { replaceAll: Boolean(data.all_browsers) });
|
||||
this.viewerTransport = this.normalizeViewerTransport(data.viewer_transport);
|
||||
this.setActiveBrowserId(
|
||||
data.active_browser_id || requestedBrowserId || this.activeBrowserId || null,
|
||||
data.active_browser_context_id || contextId,
|
||||
|
|
@ -987,6 +1023,9 @@ const model = {
|
|||
const frameHandler = ({ data }) => {
|
||||
if (data?.context_id !== this.contextId) return;
|
||||
if (data?.viewer_id && data.viewer_id !== this._viewerToken) return;
|
||||
if (data?.viewer_transport) {
|
||||
this.viewerTransport = this.normalizeViewerTransport(data.viewer_transport);
|
||||
}
|
||||
const incomingContextId = this.normalizeContextId(data.context_id || this.contextId);
|
||||
const incomingBrowserId = this.normalizeBrowserId(data.browser_id || data.state?.id);
|
||||
this.applyBrowserListing(data.browsers || [], incomingContextId, { replaceContext: true });
|
||||
|
|
@ -1011,6 +1050,7 @@ const model = {
|
|||
this.queueFrameRender(`data:${data.mime || "image/jpeg"};base64,${data.image}`, {
|
||||
browserId: frameBrowserId,
|
||||
contextId: incomingContextId,
|
||||
dimensions: this.frameDimensionsFromMetadata(data.metadata),
|
||||
onAccepted: () => {
|
||||
if (
|
||||
this.sameBrowserId(this.switchingBrowserId, frameBrowserId)
|
||||
|
|
@ -1041,6 +1081,9 @@ const model = {
|
|||
const stateHandler = ({ data }) => {
|
||||
if (data?.context_id !== this.contextId) return;
|
||||
if (data?.viewer_id && data.viewer_id !== this._viewerToken) return;
|
||||
if (data?.viewer_transport) {
|
||||
this.viewerTransport = this.normalizeViewerTransport(data.viewer_transport);
|
||||
}
|
||||
const commandContextId = this.normalizeContextId(data.active_browser_context_id || data.context_id || this.contextId);
|
||||
this.applyBrowserListing(data.browsers || [], commandContextId, { replaceAll: Boolean(data.all_browsers) });
|
||||
const command = String(data.command || "").toLowerCase();
|
||||
|
|
@ -1109,7 +1152,7 @@ const model = {
|
|||
}
|
||||
return;
|
||||
}
|
||||
const dimensions = await loadFrameDimensions(frameSrc);
|
||||
const dimensions = options?.dimensions || await loadFrameDimensions(frameSrc);
|
||||
if (sequence !== this._frameRenderSequence || surfaceSequence !== this._surfaceOpenSequence) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -1254,12 +1297,14 @@ const model = {
|
|||
context_id: targetContextId,
|
||||
browser_id: targetBrowserId,
|
||||
viewer_id: this._viewerToken,
|
||||
viewer_transport: this.requestedViewerTransport(),
|
||||
command,
|
||||
},
|
||||
{ timeoutMs: 20000 },
|
||||
);
|
||||
const data = firstOk(response);
|
||||
this.applyBrowserListing(data.browsers || [], targetContextId, { replaceAll: Boolean(data.all_browsers) });
|
||||
this.viewerTransport = this.normalizeViewerTransport(data.viewer_transport);
|
||||
const result = data.result || {};
|
||||
const resultContextId = this.normalizeContextId(
|
||||
result.context_id
|
||||
|
|
@ -1317,6 +1362,9 @@ const model = {
|
|||
},
|
||||
|
||||
async restartCanvasStreamAfterPageChange() {
|
||||
if (!this.usesScreencastTransport()) {
|
||||
return;
|
||||
}
|
||||
const surfaceSequence = this._surfaceOpenSequence;
|
||||
if (this._mode !== "canvas" || !this.isCurrentSurfaceOpen(surfaceSequence) || !this.activeBrowserId) {
|
||||
return;
|
||||
|
|
@ -2340,7 +2388,7 @@ const model = {
|
|||
input_type: "viewport",
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
restart_stream: restartStream,
|
||||
restart_stream: restartStream && this.usesScreencastTransport(),
|
||||
});
|
||||
this._lastViewportKey = key;
|
||||
this._lastViewport = {
|
||||
|
|
@ -2516,6 +2564,7 @@ const model = {
|
|||
this._connectSequence += 1;
|
||||
this._viewerToken = "";
|
||||
this.switchingBrowserId = null;
|
||||
this.viewerTransport = this.requestedViewerTransport();
|
||||
this._surfaceMounted = false;
|
||||
this._surfaceSwitching = false;
|
||||
this.commandInFlight = false;
|
||||
|
|
|
|||
|
|
@ -688,6 +688,7 @@ def test_browser_canvas_restarts_stream_after_page_navigation():
|
|||
)
|
||||
|
||||
assert "async restartCanvasStreamAfterPageChange()" in js
|
||||
assert "if (!this.usesScreencastTransport())" in js
|
||||
assert '["navigate", "back", "forward", "reload"].includes(commandName)' in js
|
||||
assert "await this.restartCanvasStreamAfterPageChange();" in js
|
||||
assert "await this.waitForSurfaceViewport({ sequence: surfaceSequence });" in js
|
||||
|
|
@ -1089,7 +1090,7 @@ def test_browser_tabs_close_without_confirmation_or_busy_lock():
|
|||
assert "_commandInFlightCount" in browser_store
|
||||
|
||||
|
||||
def test_browser_viewer_uses_cdp_screencast_transport():
|
||||
def test_browser_viewer_defaults_to_live_screencast_with_snapshot_fallback():
|
||||
ws_browser = (PROJECT_ROOT / "plugins" / "_browser" / "api" / "ws_browser.py").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
|
@ -1107,12 +1108,20 @@ def test_browser_viewer_uses_cdp_screencast_transport():
|
|||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'runtime.call("screenshot"' in ws_browser
|
||||
assert 'VIEWER_TRANSPORT_SNAPSHOT = "snapshot"' in ws_browser
|
||||
assert 'VIEWER_TRANSPORT_SCREENCAST = "screencast"' in ws_browser
|
||||
assert "def _viewer_transport(data: dict[str, Any])" in ws_browser
|
||||
assert "return VIEWER_TRANSPORT_SNAPSHOT" in ws_browser
|
||||
assert "self._stream_state" in ws_browser
|
||||
assert "SCREENCAST_QUALITY = 92" in ws_browser
|
||||
assert "initial_viewport = self._viewport_from_data(data)" in ws_browser
|
||||
assert '"set_viewport"' in ws_browser
|
||||
assert "start_screencast" in ws_browser
|
||||
assert "pop_screencast_frame" in ws_browser
|
||||
assert "stop_screencast" in ws_browser
|
||||
assert "viewer_transport == VIEWER_TRANSPORT_SCREENCAST" in ws_browser
|
||||
assert '"viewer_transport": viewer_transport' in ws_browser
|
||||
assert '"viewer_transport": VIEWER_TRANSPORT_SNAPSHOT' in ws_browser
|
||||
assert '"Page.startScreencast"' in runtime
|
||||
assert '"Page.screencastFrame"' in runtime
|
||||
assert '"Page.screencastFrameAck"' in runtime
|
||||
|
|
@ -1123,9 +1132,21 @@ def test_browser_viewer_uses_cdp_screencast_transport():
|
|||
assert "await self._stop_screencasts_for_browser(resolved_id)" in runtime
|
||||
assert "queueFrameRender" in browser_store
|
||||
assert "requestAnimationFrame" in browser_store
|
||||
assert 'const BROWSER_VIEWER_TRANSPORT_SNAPSHOT = "snapshot";' in browser_store
|
||||
assert 'const BROWSER_VIEWER_TRANSPORT_SCREENCAST = "screencast";' in browser_store
|
||||
assert "viewerTransport: BROWSER_VIEWER_TRANSPORT_SCREENCAST" in browser_store
|
||||
assert "liveScreencastEnabled: true" in browser_store
|
||||
assert "requestedViewerTransport()" in browser_store
|
||||
assert "normalizeViewerTransport(value = \"\")" in browser_store
|
||||
assert "usesScreencastTransport()" in browser_store
|
||||
assert "frameDimensionsFromMetadata(metadata = null)" in browser_store
|
||||
assert "metadata.expectedWidth || metadata.deviceWidth || metadata.jpegWidth" in browser_store
|
||||
assert "dimensions: this.frameDimensionsFromMetadata(data.metadata)" in browser_store
|
||||
assert "const dimensions = options?.dimensions || await loadFrameDimensions(frameSrc)" in browser_store
|
||||
assert "viewer_transport: this.requestedViewerTransport()" in browser_store
|
||||
assert "viewport_width: initialViewport?.width" in browser_store
|
||||
assert "viewport_height: initialViewport?.height" in browser_store
|
||||
assert "restart_stream: restartStream" in browser_store
|
||||
assert "restart_stream: restartStream && this.usesScreencastTransport()" in browser_store
|
||||
assert 'restart_screencast=bool(data.get("restart_stream"))' in ws_browser
|
||||
assert "restart_screencast: bool = False" in runtime
|
||||
assert "should_remount_viewport = changed or restart_screencast" in runtime
|
||||
|
|
@ -1135,7 +1156,7 @@ def test_browser_viewer_uses_cdp_screencast_transport():
|
|||
assert "await self._remount_viewport(page, viewport)" in runtime
|
||||
assert "await asyncio.sleep(VIEWPORT_REMOUNT_PAUSE_SECONDS)" in runtime
|
||||
assert "def _nudged_viewport(viewport: dict[str, int])" in runtime
|
||||
assert 'await this.syncViewport(true, { restartStream: this._mode === "canvas" });' in browser_store
|
||||
assert 'restartStream: this._mode === "canvas" && this.usesScreencastTransport()' in browser_store
|
||||
assert "this.frameState = data.state || null" not in browser_store
|
||||
assert "function loadFrameDimensions(src)" in browser_store
|
||||
assert "frameMatchesViewport(dimensions = null, viewport = null)" in browser_store
|
||||
|
|
@ -1155,7 +1176,8 @@ def test_browser_viewer_uses_cdp_screencast_transport():
|
|||
assert "startBrowserScreenshotPreview(button, image, resolveBrowserPayload)" in browser_tool_handler
|
||||
assert "FRAME_FALLBACK_SCREENSHOT_SECONDS" not in ws_browser
|
||||
assert '"frame_source": "state"' in ws_browser
|
||||
assert '"frame_source"] = "screencast"' in ws_browser
|
||||
assert '"frame_source"] = VIEWER_TRANSPORT_SCREENCAST' in ws_browser
|
||||
assert '"viewer_transport"] = VIEWER_TRANSPORT_SCREENCAST' in ws_browser
|
||||
assert "fallback_screenshot" not in ws_browser
|
||||
assert "canvas_wheel_screenshot" not in ws_browser
|
||||
assert "surface_mode: this._mode" not in browser_store
|
||||
|
|
@ -1279,7 +1301,7 @@ def test_browser_content_helper_keeps_label_wrapped_controls_referenceable():
|
|||
PROJECT_ROOT / "plugins" / "_browser" / "assets" / "browser-page-content.js"
|
||||
).read_text(encoding="utf-8")
|
||||
|
||||
assert 'const VERSION = "12"' in helper
|
||||
assert 'const VERSION = "13"' in helper
|
||||
assert "function patchOpenShadowDom" in helper
|
||||
assert "Element.prototype.attachShadow = patched" in helper
|
||||
assert "const REQUIRED_API_NAMES = Object.freeze([" in helper
|
||||
|
|
@ -1294,6 +1316,9 @@ def test_browser_content_helper_keeps_label_wrapped_controls_referenceable():
|
|||
assert "function isGlobalOrDelegatedEventBinding" in helper
|
||||
assert 'parts.includes("window")' in helper
|
||||
assert 'parts.includes("outside")' in helper
|
||||
assert 'actionStrategy: entry.helperBacked ? "frame_chain_ref" : "dom_ref"' in helper
|
||||
assert 'actionStrategy: "frame_chain_ref"' in helper
|
||||
assert "frameChain: Array.isArray(entry?.frameChain)" in helper
|
||||
|
||||
|
||||
def test_browser_panel_exposes_agent_friendly_address_input():
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue