@remcp/runtime 0.2.161 → 0.2.163
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/extended/browser-readiness.mjs +68 -0
- package/src/extended/browser-workflow.mjs +32 -7
- package/src/extended/catalog-schemas.mjs +1 -0
- package/src/extended/catalog.mjs +1 -1
- package/src/extended/desktop-computer-actions.mjs +28 -4
- package/src/extended/desktop-computer-snapshot.mjs +7 -2
package/package.json
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Optional bounded local CDP readiness probe. This reports availability only;
|
|
2
|
+
// it never exposes tab titles/URLs, starts Chrome, changes CDP flags or grants
|
|
3
|
+
// permission to access targets. Actual actions always re-check browser policy.
|
|
4
|
+
import { browserRemoteEnabled, endpointUrl, requestJson, validatedTargetWebSocketUrl } from './browser-endpoint.mjs';
|
|
5
|
+
|
|
6
|
+
const records = new Map();
|
|
7
|
+
const CACHE_MS = 3000;
|
|
8
|
+
const MAX_ENTRIES = 8;
|
|
9
|
+
const transportError = message =>
|
|
10
|
+
/fetch failed|ECONNREFUSED|ECONNRESET|ETIMEDOUT|abort|timed out|network error/i.test(message);
|
|
11
|
+
|
|
12
|
+
export function classifyBrowserReadiness(version, targets, endpoint) {
|
|
13
|
+
if (!version || typeof version !== 'object' || !(version.Browser || version.webSocketDebuggerUrl)
|
|
14
|
+
|| !Array.isArray(targets)) return { status:'invalid_response', ready:false, candidate_pages:0 };
|
|
15
|
+
let pages = 0;
|
|
16
|
+
for (const target of targets.slice(0, 1000)) {
|
|
17
|
+
if (target?.type !== 'page' || !target?.id || !target?.webSocketDebuggerUrl) continue;
|
|
18
|
+
try {
|
|
19
|
+
validatedTargetWebSocketUrl(target, endpointUrl(endpoint));
|
|
20
|
+
pages += 1;
|
|
21
|
+
} catch {
|
|
22
|
+
// The CDP server's untrusted page/socket metadata is not permission.
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return { status:pages ? 'ready' : 'no_tabs', ready:pages > 0, candidate_pages:pages };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function probeBrowserReadiness({
|
|
29
|
+
endpoint,
|
|
30
|
+
timeoutMs = 350,
|
|
31
|
+
request = requestJson,
|
|
32
|
+
enabled = browserRemoteEnabled(),
|
|
33
|
+
now = Date.now(),
|
|
34
|
+
cache = request === requestJson,
|
|
35
|
+
} = {}) {
|
|
36
|
+
if (!enabled) return {
|
|
37
|
+
status:'disabled', ready:false, candidate_pages:0, next_step:'use_native_accessibility',
|
|
38
|
+
};
|
|
39
|
+
let key;
|
|
40
|
+
try { key = endpointUrl(endpoint).origin; }
|
|
41
|
+
catch { return { status:'blocked_endpoint', ready:false, candidate_pages:0, next_step:'check_local_cdp_config' }; }
|
|
42
|
+
if (cache) {
|
|
43
|
+
const previous = records.get(key);
|
|
44
|
+
if (previous && previous.expires > now) return { ...previous.value, cache_hit:true };
|
|
45
|
+
}
|
|
46
|
+
const timeout = Math.max(100, Math.min(800, Number(timeoutMs) || 350));
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
const version = await request('/json/version', endpoint, { timeout_ms:timeout });
|
|
50
|
+
// CDP version alone is not evidence a useful debuggable tab exists.
|
|
51
|
+
if (!version || typeof version !== 'object' || !(version.Browser || version.webSocketDebuggerUrl)) {
|
|
52
|
+
result = { status:'invalid_response', ready:false, candidate_pages:0 };
|
|
53
|
+
} else {
|
|
54
|
+
const targets = await request('/json/list', endpoint, { timeout_ms:timeout });
|
|
55
|
+
result = classifyBrowserReadiness(version, targets, endpoint);
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
result = { status:transportError(String(error?.message || '')) ? 'unavailable' : 'invalid_response',
|
|
59
|
+
ready:false, candidate_pages:0 };
|
|
60
|
+
}
|
|
61
|
+
const next_step = result.ready ? 'use_browser_tabs' : 'use_native_accessibility';
|
|
62
|
+
const output = { ...result, next_step };
|
|
63
|
+
if (cache) {
|
|
64
|
+
records.set(key, { expires:now + CACHE_MS, value:output });
|
|
65
|
+
while (records.size > MAX_ENTRIES) records.delete(records.keys().next().value);
|
|
66
|
+
}
|
|
67
|
+
return output;
|
|
68
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// A workflow is explicitly requested by the caller after inspecting the page.
|
|
3
3
|
// No arbitrary JavaScript, cross-target access, file uploads or implicit submits.
|
|
4
4
|
import { assertResolvedPageUrl } from './browser-endpoint.mjs';
|
|
5
|
-
import {
|
|
5
|
+
import { evaluate } from './browser-session.mjs';
|
|
6
6
|
|
|
7
7
|
const ACTIONS = new Set(['fill_form','set_value','select','focus','click','wait_for']);
|
|
8
8
|
const CONDITIONS = new Set(['selector','text','url_contains','load']);
|
|
@@ -45,7 +45,28 @@ export function validateBrowserWorkflow(steps, { validateFormFields }) {
|
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// A workflow is a single-page plan. Redirects across origins require a new
|
|
49
|
+
// snapshot and a separate deliberate action, even if both URLs are public.
|
|
50
|
+
export function assertWorkflowOrigin(expectedOrigin, currentUrl) {
|
|
51
|
+
let currentOrigin;
|
|
52
|
+
try { currentOrigin = new URL(currentUrl).origin; }
|
|
53
|
+
catch { throw new Error('Browser workflow page origin could not be verified'); }
|
|
54
|
+
if (expectedOrigin !== null && currentOrigin !== expectedOrigin) {
|
|
55
|
+
throw new Error('Browser workflow origin changed');
|
|
56
|
+
}
|
|
57
|
+
return currentOrigin;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Enforce the origin in the exact same page JS evaluation that writes a field.
|
|
61
|
+
// Caller-supplied selectors/values stay JSON data, not arbitrary JavaScript.
|
|
62
|
+
async function guardedPageFunction(session, trustedFunction, args, origin) {
|
|
63
|
+
const expression = '(() => { if (location.origin !== ' + JSON.stringify(origin) +
|
|
64
|
+
') throw new Error("workflow_origin_changed"); return (' + trustedFunction +
|
|
65
|
+
')(...' + JSON.stringify(args) + '); })()';
|
|
66
|
+
return evaluate(session, expression);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function waitForStep(session, step, started, expectedOrigin) {
|
|
49
70
|
const deadline = Math.min(started + 12000, Date.now() + step.timeout_ms);
|
|
50
71
|
const expression = step.condition === 'selector' ?
|
|
51
72
|
'Boolean(document.querySelector('+JSON.stringify(step.criterion)+'))'
|
|
@@ -67,7 +88,8 @@ async function waitForStep(session, step, started) {
|
|
|
67
88
|
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
68
89
|
continue;
|
|
69
90
|
}
|
|
70
|
-
// Never accept a match on
|
|
91
|
+
// Never accept a match on an unsafe page or an unexpected origin.
|
|
92
|
+
assertWorkflowOrigin(expectedOrigin, state?.url);
|
|
71
93
|
await assertResolvedPageUrl(state?.url);
|
|
72
94
|
if (state?.matched) return;
|
|
73
95
|
await new Promise(resolve=>setTimeout(resolve, 75));
|
|
@@ -80,25 +102,28 @@ export async function executeBrowserWorkflow(session, steps, {
|
|
|
80
102
|
}) {
|
|
81
103
|
const started = Date.now();
|
|
82
104
|
const completed = [];
|
|
105
|
+
let boundOrigin = null;
|
|
83
106
|
for (let index = 0; index < steps.length; index++) {
|
|
84
107
|
const step = steps[index];
|
|
85
108
|
if (Date.now() - started > 12000) throw new Error('Workflow exceeded 12 second budget after '+completed.length+' steps');
|
|
86
109
|
try {
|
|
87
110
|
const currentUrl = await evaluate(session, 'location.href');
|
|
111
|
+
const currentOrigin = assertWorkflowOrigin(boundOrigin, currentUrl);
|
|
88
112
|
await assertResolvedPageUrl(currentUrl);
|
|
113
|
+
if (boundOrigin === null) boundOrigin = currentOrigin;
|
|
89
114
|
if (step.action === 'fill_form') {
|
|
90
|
-
await
|
|
115
|
+
await guardedPageFunction(session,pageFillForm,[step.fields],boundOrigin);
|
|
91
116
|
} else if (step.action === 'wait_for') {
|
|
92
|
-
await waitForStep(session,step,started);
|
|
117
|
+
await waitForStep(session,step,started,boundOrigin);
|
|
93
118
|
} else if (step.action === 'click') {
|
|
94
|
-
const bounds=await
|
|
119
|
+
const bounds=await guardedPageFunction(session,pageElementAction,[step.selector,null,'click',''],boundOrigin);
|
|
95
120
|
const x=Number(bounds.x)+Number(bounds.width)/2;
|
|
96
121
|
const y=Number(bounds.y)+Number(bounds.height)/2;
|
|
97
122
|
await session.send('Input.dispatchMouseEvent',{type:'mouseMoved',x,y,button:'none'});
|
|
98
123
|
await session.send('Input.dispatchMouseEvent',{type:'mousePressed',x,y,button:'left',clickCount:1});
|
|
99
124
|
await session.send('Input.dispatchMouseEvent',{type:'mouseReleased',x,y,button:'left',clickCount:1});
|
|
100
125
|
} else {
|
|
101
|
-
await
|
|
126
|
+
await guardedPageFunction(session,pageElementAction,[step.selector,null,step.action,step.value??''],boundOrigin);
|
|
102
127
|
}
|
|
103
128
|
completed.push({index:index+1,action:step.action,ok:true});
|
|
104
129
|
} catch (error) {
|
|
@@ -164,6 +164,7 @@ export const OUTPUT_FIELDS = Object.freeze({
|
|
|
164
164
|
cursor:outputObject('Pointer position when available.'),
|
|
165
165
|
clipboard:outputObject('Clipboard metadata only unless clipboard content was explicitly requested.'),
|
|
166
166
|
browser:outputObject('Optional browser DOM/CDP snapshot.'),
|
|
167
|
+
browser_readiness:outputObject('Optional bounded local CDP readiness status and fallback hint; no tab titles or URLs.'),
|
|
167
168
|
ocr:outputObject('Optional OCR words/metadata.'),
|
|
168
169
|
fallback_chain:outputObject('Which semantic/DOM/OCR/coordinate layers were requested and available.'),
|
|
169
170
|
errors:outputArray(outputObject(), 'Non-fatal sub-capture errors.'),
|
package/src/extended/catalog.mjs
CHANGED
|
@@ -63,7 +63,7 @@ const browserTarget = {
|
|
|
63
63
|
};
|
|
64
64
|
|
|
65
65
|
export const extendedToolDefinitions = [
|
|
66
|
-
define('computer_snapshot', 'Computer snapshot', 'Capture one compact computer-use state bundle: displays, visible windows, labeled semantic accessibility state, clipboard metadata, optional native browser accessibility/CDP/OCR, and real screenshot pixels. Use include_ui=false for a fast screenshot-first state; screenshot_window_id/monitor/region scope pixels without adding another tool.', o({ max_ui_nodes:n('Maximum native accessibility nodes; default 800.'), max_ui_depth:n('Maximum native accessibility depth; default 12.'), include_ui:b('Include native accessibility tree; default true. False is the fast screenshot-only path.'), ui_scope:e(['active','desktop'],'Accessibility scope for computer_snapshot. Default active keeps the compact snapshot fast; desktop requests all accessible applications/windows.'), semantic_tree:b('Include compact semantic hierarchy text alongside nodes; default true.'), ui_browser_dom:b('Prefer the native browser accessibility document subtree when the OS exposes one; on Windows this also enables the Firefox MSAA/IAccessible fallback.'), include_screenshot:b('Include real rendered desktop pixels; default true.'), screenshot_region:{type:'array',minItems:4,maxItems:4,items:{type:'number'},description:'Virtual-desktop [left,top,right,bottom] region; takes precedence over full desktop.'}, screenshot_window_id:s('Capture the window returned by list_windows instead of the whole desktop.'), screenshot_monitor:s('Monitor name substring or "primary".'), screenshot_monitor_index:n('Zero-based monitor index.'), screenshot_padding:n('Pixels of context around a targeted screenshot; 0..200.'), grid_columns:n('Optional reference-grid column count returned as visual metadata; 0..20.'), grid_rows:n('Optional reference-grid row count returned as visual metadata; 0..20.'), include_browser:b('Include CDP browser accessibility when available. Default auto: only when the active app looks Chromium-based.'), browser_max_nodes:n('Maximum browser accessibility nodes; default 500, maximum 2000.'), include_ocr:b('Run optional local OCR. Default auto: only when native accessibility and browser DOM are unavailable. Requires tesseract on PATH.'), ocr_language:s('Tesseract language code, default eng; multiple languages may use eng+deu.'), ocr_max_words:n('Maximum OCR word boxes returned; default 2000, maximum 10000.'), ocr_psm:n('Tesseract page segmentation mode, 3..13; default 11.'), ocr_timeout_ms:n('OCR timeout, default 30000 ms, maximum 120000 ms.') }), readOnlyLive, desktopHandlers.computer_snapshot),
|
|
66
|
+
define('computer_snapshot', 'Computer snapshot', 'Capture one compact computer-use state bundle: displays, visible windows, labeled semantic accessibility state, clipboard metadata, optional native browser accessibility/CDP/OCR, and real screenshot pixels. For local browser availability without tab content or launching Chrome, request browser_readiness=true; otherwise no extra CDP probe is made. Use include_ui=false for a fast screenshot-first state; screenshot_window_id/monitor/region scope pixels without adding another tool.', o({ max_ui_nodes:n('Maximum native accessibility nodes; default 800.'), max_ui_depth:n('Maximum native accessibility depth; default 12.'), include_ui:b('Include native accessibility tree; default true. False is the fast screenshot-only path.'), ui_scope:e(['active','desktop'],'Accessibility scope for computer_snapshot. Default active keeps the compact snapshot fast; desktop requests all accessible applications/windows.'), semantic_tree:b('Include compact semantic hierarchy text alongside nodes; default true.'), ui_browser_dom:b('Prefer the native browser accessibility document subtree when the OS exposes one; on Windows this also enables the Firefox MSAA/IAccessible fallback.'), include_screenshot:b('Include real rendered desktop pixels; default true.'), screenshot_region:{type:'array',minItems:4,maxItems:4,items:{type:'number'},description:'Virtual-desktop [left,top,right,bottom] region; takes precedence over full desktop.'}, screenshot_window_id:s('Capture the window returned by list_windows instead of the whole desktop.'), screenshot_monitor:s('Monitor name substring or "primary".'), screenshot_monitor_index:n('Zero-based monitor index.'), screenshot_padding:n('Pixels of context around a targeted screenshot; 0..200.'), grid_columns:n('Optional reference-grid column count returned as visual metadata; 0..20.'), grid_rows:n('Optional reference-grid row count returned as visual metadata; 0..20.'), include_browser:b('Include CDP browser accessibility when available. Default auto: only when the active app looks Chromium-based.'), browser_readiness:b('Opt-in bounded CDP readiness status: disabled, blocked endpoint, unavailable, invalid response, no tabs, or ready. No tab URLs or titles; never launches Chrome. Default false.'), browser_max_nodes:n('Maximum browser accessibility nodes; default 500, maximum 2000.'), include_ocr:b('Run optional local OCR. Default auto: only when native accessibility and browser DOM are unavailable. Requires tesseract on PATH.'), ocr_language:s('Tesseract language code, default eng; multiple languages may use eng+deu.'), ocr_max_words:n('Maximum OCR word boxes returned; default 2000, maximum 10000.'), ocr_psm:n('Tesseract page segmentation mode, 3..13; default 11.'), ocr_timeout_ms:n('OCR timeout, default 30000 ms, maximum 120000 ms.') }), readOnlyLive, desktopHandlers.computer_snapshot),
|
|
67
67
|
define(
|
|
68
68
|
'computer_action',
|
|
69
69
|
'Computer action',
|
|
@@ -72,6 +72,17 @@ export function computerActionWindowTextArgs(args = {}) {
|
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
// A UI element ID is scoped to the native accessibility tree. If it becomes
|
|
76
|
+
// stale, never reinterpret a remaining name, selector, OCR hint or x/y as a
|
|
77
|
+
// different target: refind that element in the same window first.
|
|
78
|
+
export function hasPinnedUiElement(args = {}) {
|
|
79
|
+
return Boolean(args.id || args.automation_id || args.automationId);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stalePinnedUiElement() {
|
|
83
|
+
return new Error('Pinned UI element could not be resolved; use ui_snapshot and ui_find to refresh its ID. No browser, OCR, window or coordinate fallback was attempted.');
|
|
84
|
+
}
|
|
85
|
+
|
|
75
86
|
export async function computerAction(args = {}) {
|
|
76
87
|
const legacy = ['ui','pointer','window','type','keyboard','scroll','drag','clipboard','launch_app','open_path','reveal_path','notification'];
|
|
77
88
|
const universal = [
|
|
@@ -89,6 +100,7 @@ export async function computerAction(args = {}) {
|
|
|
89
100
|
const hasOcr = Boolean(ocrNeedle);
|
|
90
101
|
const hasCoordinates = Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y));
|
|
91
102
|
const automatic = target === 'auto';
|
|
103
|
+
const pinnedUi = automatic && hasPinnedUiElement(args);
|
|
92
104
|
|
|
93
105
|
if (action === 'batch') {
|
|
94
106
|
const steps = Array.isArray(args.steps) ? args.steps : [];
|
|
@@ -170,7 +182,10 @@ export async function computerAction(args = {}) {
|
|
|
170
182
|
const failures = [];
|
|
171
183
|
if (hasUi) {
|
|
172
184
|
try { return await typeText({ ...args, method:'accessibility' }); }
|
|
173
|
-
catch (error) {
|
|
185
|
+
catch (error) {
|
|
186
|
+
if (pinnedUi) throw stalePinnedUiElement();
|
|
187
|
+
failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`);
|
|
188
|
+
}
|
|
174
189
|
}
|
|
175
190
|
if (hasBrowser) {
|
|
176
191
|
if (await browserCapabilityAvailable(args.endpoint, 300)) {
|
|
@@ -203,7 +218,10 @@ export async function computerAction(args = {}) {
|
|
|
203
218
|
let uiError = null;
|
|
204
219
|
if (hasUi) {
|
|
205
220
|
try { return await scroll(args); }
|
|
206
|
-
catch (error) {
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (pinnedUi) throw stalePinnedUiElement();
|
|
223
|
+
uiError = error;
|
|
224
|
+
}
|
|
207
225
|
}
|
|
208
226
|
if (hasBrowser) {
|
|
209
227
|
if (await browserCapabilityAvailable(args.endpoint, 300)) return browserComputerAction(args, 'scroll');
|
|
@@ -244,7 +262,10 @@ export async function computerAction(args = {}) {
|
|
|
244
262
|
let uiError = null;
|
|
245
263
|
if (hasUi) {
|
|
246
264
|
try { return await uiAction({ ...args, action:uiActionName }); }
|
|
247
|
-
catch (error) {
|
|
265
|
+
catch (error) {
|
|
266
|
+
if (pinnedUi) throw stalePinnedUiElement();
|
|
267
|
+
uiError = error;
|
|
268
|
+
}
|
|
248
269
|
}
|
|
249
270
|
if (hasBrowser) return browserComputerAction(args, uiActionName);
|
|
250
271
|
if (uiError) throw uiError;
|
|
@@ -288,7 +309,10 @@ export async function computerAction(args = {}) {
|
|
|
288
309
|
const failures = [];
|
|
289
310
|
if (hasUi) {
|
|
290
311
|
try { return await uiAction({ ...args, action:'click' }); }
|
|
291
|
-
catch (error) {
|
|
312
|
+
catch (error) {
|
|
313
|
+
if (pinnedUi) throw stalePinnedUiElement();
|
|
314
|
+
failures.push(`accessibility: ${String(error?.message || error).slice(0, 300)}`);
|
|
315
|
+
}
|
|
292
316
|
}
|
|
293
317
|
if (hasBrowser) {
|
|
294
318
|
if (await browserCapabilityAvailable(args.endpoint, 300)) {
|
|
@@ -9,6 +9,7 @@ import * as linux from './desktop-linux.mjs';
|
|
|
9
9
|
import * as macos from './desktop-macos.mjs';
|
|
10
10
|
import * as windows from './desktop-windows.mjs';
|
|
11
11
|
import { browserAction, browserCapabilityAvailable, browserSnapshot } from './browser.mjs';
|
|
12
|
+
import { probeBrowserReadiness } from './browser-readiness.mjs';
|
|
12
13
|
import { findOcrText, ocrImage } from './ocr.mjs';
|
|
13
14
|
|
|
14
15
|
|
|
@@ -20,7 +21,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
20
21
|
const screenshotRequested = args.include_screenshot !== false || args.include_ocr === true;
|
|
21
22
|
const captureScreenshot = desktopAvailable && screenshotRequested;
|
|
22
23
|
const includeUi = args.include_ui !== false;
|
|
23
|
-
const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult] = await Promise.allSettled([
|
|
24
|
+
const [windowsResult, displaysResult, uiResult, clipboardResult, cursorResult, shotResult, readinessResult] = await Promise.allSettled([
|
|
24
25
|
desktopAvailable ? listWindows() : Promise.resolve(null),
|
|
25
26
|
desktopAvailable ? displayInventory() : Promise.resolve(null),
|
|
26
27
|
includeUi && desktopAvailable
|
|
@@ -35,6 +36,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
35
36
|
desktopAvailable ? clipboard({ action:'read' }) : Promise.resolve(null),
|
|
36
37
|
desktopAvailable ? cursorPosition() : Promise.resolve(null),
|
|
37
38
|
captureScreenshot ? computerScreenshotTask(args) : Promise.resolve(null),
|
|
39
|
+
args.browser_readiness === true ? probeBrowserReadiness() : Promise.resolve(null),
|
|
38
40
|
]);
|
|
39
41
|
const parse = (result, fallback = null) => {
|
|
40
42
|
if (result.status !== 'fulfilled' || !result.value || result.value.isError === true) return fallback;
|
|
@@ -112,9 +114,11 @@ export async function computerSnapshot(args = {}) {
|
|
|
112
114
|
const activeLabel = [activeApp?.app, activeApp?.window, activeWindow?.app, activeWindow?.title].filter(Boolean).join(' ');
|
|
113
115
|
const browserRequested = args.include_browser === true
|
|
114
116
|
|| (args.include_browser !== false && /chrome|chromium|edge|brave/i.test(activeLabel));
|
|
117
|
+
const readiness = args.browser_readiness === true && readinessResult.status === 'fulfilled'
|
|
118
|
+
? objectRecord(readinessResult.value) : null;
|
|
115
119
|
let browser = null;
|
|
116
120
|
let browserError = null;
|
|
117
|
-
if (browserRequested && await browserCapabilityAvailable(undefined, 400)) {
|
|
121
|
+
if (browserRequested && (readiness ? readiness.ready : await browserCapabilityAvailable(undefined, 400))) {
|
|
118
122
|
const hint = browserTitleHint(activeWindow);
|
|
119
123
|
try {
|
|
120
124
|
const browserResult = await browserSnapshot({
|
|
@@ -184,6 +188,7 @@ export async function computerSnapshot(args = {}) {
|
|
|
184
188
|
cursor,
|
|
185
189
|
ui,
|
|
186
190
|
...(browser ? { browser } : {}),
|
|
191
|
+
...(args.browser_readiness === true ? { browser_readiness:readiness || { status:'invalid_response', ready:false, candidate_pages:0, next_step:'use_native_accessibility' } } : {}),
|
|
187
192
|
...(ocr ? { ocr } : {}),
|
|
188
193
|
fallback_chain: {
|
|
189
194
|
accessibility: { requested:includeUi, available:includeUi && desktopAvailable && uiResult.status === 'fulfilled' && Boolean(uiResult.value), nodes:uiNodes.length },
|