@hypothesi/tauri-mcp-server 0.12.0 → 0.13.0
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/dist/driver/app-discovery.d.ts +4 -2
- package/dist/driver/app-discovery.js +23 -3
- package/dist/driver/plugin-client.d.ts +23 -1
- package/dist/driver/plugin-client.js +127 -2
- package/dist/driver/plugin-commands.d.ts +8 -4
- package/dist/driver/plugin-commands.js +71 -16
- package/dist/driver/scripts/find-element.js +129 -16
- package/dist/driver/scripts/focus.js +31 -4
- package/dist/driver/scripts/html2canvas-loader.js +20 -2
- package/dist/driver/scripts/index.d.ts +2 -4
- package/dist/driver/scripts/index.js +40 -11
- package/dist/driver/scripts/interact.js +66 -5
- package/dist/driver/scripts/resolve-ref.js +101 -50
- package/dist/driver/session-manager.d.ts +1 -0
- package/dist/driver/session-manager.js +53 -6
- package/dist/driver/webview-executor.d.ts +20 -8
- package/dist/driver/webview-executor.js +159 -58
- package/dist/driver/webview-interactions.d.ts +56 -41
- package/dist/driver/webview-interactions.js +62 -40
- package/dist/monitor/logs.d.ts +14 -2
- package/dist/monitor/logs.js +37 -5
- package/dist/tools-registry.js +35 -33
- package/dist/version.d.ts +5 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/dist/driver/scripts/get-styles.js +0 -47
- package/dist/driver/scripts/keyboard.js +0 -76
|
@@ -28,18 +28,20 @@ export interface ExecuteInWebviewResult {
|
|
|
28
28
|
* @param script - JavaScript code to execute in the webview context
|
|
29
29
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
30
30
|
* @param appIdentifier - Optional app identifier to target specific app
|
|
31
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
31
32
|
* @returns Result of the script execution with window context
|
|
32
33
|
*/
|
|
33
|
-
export declare function executeInWebview(script: string, windowId?: string, appIdentifier?: string | number): Promise<string>;
|
|
34
|
+
export declare function executeInWebview(script: string, windowId?: string, appIdentifier?: string | number, timeoutMs?: number): Promise<string>;
|
|
34
35
|
/**
|
|
35
36
|
* Execute JavaScript in the Tauri webview and return window context.
|
|
36
37
|
*
|
|
37
38
|
* @param script - JavaScript code to execute in the webview context
|
|
38
39
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
39
40
|
* @param appIdentifier - Optional app identifier to target specific app
|
|
41
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
40
42
|
* @returns Result of the script execution with window context
|
|
41
43
|
*/
|
|
42
|
-
export declare function executeInWebviewWithContext(script: string, windowId?: string, appIdentifier?: string | number): Promise<ExecuteInWebviewResult>;
|
|
44
|
+
export declare function executeInWebviewWithContext(script: string, windowId?: string, appIdentifier?: string | number, timeoutMs?: number, isRetry?: boolean): Promise<ExecuteInWebviewResult>;
|
|
43
45
|
/**
|
|
44
46
|
* Execute async JavaScript in the webview with timeout support.
|
|
45
47
|
*
|
|
@@ -61,13 +63,19 @@ export declare function initializeConsoleCapture(): Promise<string>;
|
|
|
61
63
|
/**
|
|
62
64
|
* Retrieve captured console logs with optional filtering.
|
|
63
65
|
*
|
|
64
|
-
* @param
|
|
65
|
-
* @param since - Optional ISO timestamp to filter logs after this time
|
|
66
|
-
* @param windowId - Optional window label to target (defaults to "main")
|
|
67
|
-
* @param appIdentifier - Optional app identifier to target specific app
|
|
66
|
+
* @param options - Filtering and formatting options
|
|
68
67
|
* @returns Formatted console logs as string
|
|
69
68
|
*/
|
|
70
|
-
export declare function getConsoleLogs(
|
|
69
|
+
export declare function getConsoleLogs(options?: {
|
|
70
|
+
filter?: string;
|
|
71
|
+
since?: string;
|
|
72
|
+
lines?: number;
|
|
73
|
+
windowId?: string;
|
|
74
|
+
appIdentifier?: string | number;
|
|
75
|
+
level?: string;
|
|
76
|
+
maxChars?: number;
|
|
77
|
+
maxCharsPerEntry?: number;
|
|
78
|
+
}): Promise<string>;
|
|
71
79
|
/**
|
|
72
80
|
* Clear all captured console logs.
|
|
73
81
|
*/
|
|
@@ -85,9 +93,10 @@ export interface CaptureScreenshotOptions {
|
|
|
85
93
|
windowId?: string;
|
|
86
94
|
appIdentifier?: string | number;
|
|
87
95
|
maxWidth?: number;
|
|
96
|
+
allowScreenCapture?: boolean;
|
|
88
97
|
}
|
|
89
98
|
/**
|
|
90
|
-
* Capture a screenshot of the
|
|
99
|
+
* Capture a screenshot of the visible webview viewport.
|
|
91
100
|
*
|
|
92
101
|
* @param options - Screenshot options (format, quality, windowId, appIdentifier, etc.)
|
|
93
102
|
* @returns Screenshot result with image content
|
|
@@ -113,10 +122,13 @@ export declare const GetConsoleLogsSchema: z.ZodObject<{
|
|
|
113
122
|
export declare const CaptureScreenshotSchema: z.ZodObject<{
|
|
114
123
|
format: z.ZodDefault<z.ZodOptional<z.ZodEnum<["png", "jpeg"]>>>;
|
|
115
124
|
quality: z.ZodOptional<z.ZodNumber>;
|
|
125
|
+
allowScreenCapture: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
116
126
|
}, "strip", z.ZodTypeAny, {
|
|
117
127
|
format: "png" | "jpeg";
|
|
128
|
+
allowScreenCapture: boolean;
|
|
118
129
|
quality?: number | undefined;
|
|
119
130
|
}, {
|
|
120
131
|
format?: "png" | "jpeg" | undefined;
|
|
121
132
|
quality?: number | undefined;
|
|
133
|
+
allowScreenCapture?: boolean | undefined;
|
|
122
134
|
}>;
|
|
@@ -18,6 +18,14 @@ import { getResolveRefSource, RESOLVE_REF_SCRIPT_ID } from './scripts/index.js';
|
|
|
18
18
|
// ============================================================================
|
|
19
19
|
const initializedTargets = new Set();
|
|
20
20
|
const driverLogger = createMcpLogger('DRIVER');
|
|
21
|
+
const MCP_HELPER_MISSING = /__MCP__|resolveRef|resolveAll|countAll/;
|
|
22
|
+
function isHelperMissingError(message) {
|
|
23
|
+
return MCP_HELPER_MISSING.test(message) &&
|
|
24
|
+
/undefined is not an object|is not a function|Cannot read propert/.test(message);
|
|
25
|
+
}
|
|
26
|
+
function targetKeyFor(session, windowId) {
|
|
27
|
+
return `${session.host}:${session.port}:${windowId ?? 'main'}`;
|
|
28
|
+
}
|
|
21
29
|
/**
|
|
22
30
|
* Ensures the MCP server is fully initialized and ready to use.
|
|
23
31
|
* This is called automatically by all tool functions.
|
|
@@ -43,7 +51,7 @@ export async function ensureReady(windowId, appIdentifier) {
|
|
|
43
51
|
if (!session.client.isConnected()) {
|
|
44
52
|
await session.client.connect();
|
|
45
53
|
}
|
|
46
|
-
const targetKey =
|
|
54
|
+
const targetKey = targetKeyFor(session, windowId);
|
|
47
55
|
if (initializedTargets.has(targetKey)) {
|
|
48
56
|
return;
|
|
49
57
|
}
|
|
@@ -84,10 +92,11 @@ async function waitForResolveRefHelper(session, windowId) {
|
|
|
84
92
|
* @param script - JavaScript code to execute in the webview context
|
|
85
93
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
86
94
|
* @param appIdentifier - Optional app identifier to target specific app
|
|
95
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
87
96
|
* @returns Result of the script execution with window context
|
|
88
97
|
*/
|
|
89
|
-
export async function executeInWebview(script, windowId, appIdentifier) {
|
|
90
|
-
const { result } = await executeInWebviewWithContext(script, windowId, appIdentifier);
|
|
98
|
+
export async function executeInWebview(script, windowId, appIdentifier, timeoutMs) {
|
|
99
|
+
const { result } = await executeInWebviewWithContext(script, windowId, appIdentifier, timeoutMs);
|
|
91
100
|
return result;
|
|
92
101
|
}
|
|
93
102
|
/**
|
|
@@ -96,21 +105,38 @@ export async function executeInWebview(script, windowId, appIdentifier) {
|
|
|
96
105
|
* @param script - JavaScript code to execute in the webview context
|
|
97
106
|
* @param windowId - Optional window label to target (defaults to "main")
|
|
98
107
|
* @param appIdentifier - Optional app identifier to target specific app
|
|
108
|
+
* @param timeoutMs - Optional timeout in milliseconds
|
|
99
109
|
* @returns Result of the script execution with window context
|
|
100
110
|
*/
|
|
101
|
-
export async function executeInWebviewWithContext(script, windowId, appIdentifier) {
|
|
111
|
+
export async function executeInWebviewWithContext(script, windowId, appIdentifier, timeoutMs, isRetry = false) {
|
|
102
112
|
try {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
113
|
+
return await runInWebview(script, windowId, appIdentifier, timeoutMs);
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
117
|
+
if (isRetry || !isHelperMissingError(message)) {
|
|
118
|
+
throw new Error(`WebView execution failed: ${message}`);
|
|
119
|
+
}
|
|
120
|
+
driverLogger.warn(`__MCP__ helper missing, re-injecting and retrying once: ${message}`);
|
|
106
121
|
const session = resolveTargetApp(appIdentifier);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
122
|
+
initializedTargets.delete(targetKeyFor(session, windowId));
|
|
123
|
+
await ensureReady(windowId, appIdentifier);
|
|
124
|
+
return executeInWebviewWithContext(script, windowId, appIdentifier, timeoutMs, true);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async function runInWebview(script, windowId, appIdentifier, timeoutMs) {
|
|
128
|
+
// Ensure we're fully initialized
|
|
129
|
+
await ensureReady(windowId, appIdentifier);
|
|
130
|
+
// Resolve target session
|
|
131
|
+
const session = resolveTargetApp(appIdentifier);
|
|
132
|
+
const client = session.client;
|
|
133
|
+
// Send script directly - Rust handles wrapping and IPC callbacks.
|
|
134
|
+
// Use timeoutMs if provided, otherwise default 7s timeout (longer than Rust's 5s)
|
|
135
|
+
try {
|
|
110
136
|
const response = await client.sendCommand({
|
|
111
137
|
command: 'execute_js',
|
|
112
138
|
args: { script, windowLabel: windowId },
|
|
113
|
-
}, 7000);
|
|
139
|
+
}, timeoutMs ?? 7000);
|
|
114
140
|
if (!response.success) {
|
|
115
141
|
throw new Error(response.error || 'Unknown execution error');
|
|
116
142
|
}
|
|
@@ -136,7 +162,34 @@ export async function executeInWebviewWithContext(script, windowId, appIdentifie
|
|
|
136
162
|
}
|
|
137
163
|
catch (error) {
|
|
138
164
|
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
-
|
|
165
|
+
// Check for transport timeout
|
|
166
|
+
if (/^Request timeout after \d+ms$/.test(message)) {
|
|
167
|
+
const probeResult = await probeBridge(session, windowId);
|
|
168
|
+
if (probeResult) {
|
|
169
|
+
throw new Error(`Bridge did not respond within ${timeoutMs ?? 7000}ms. A follow-up probe succeeded, so the bridge is alive and this ` +
|
|
170
|
+
'specific script was too slow. Raise `timeout`, or split the work across calls.');
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
throw new Error(`Bridge did not respond within ${timeoutMs ?? 7000}ms and a follow-up probe also timed out. The webview is wedged. ` +
|
|
174
|
+
'Call driver_session with action "stop" then "start". A runaway loop in a previous script is the usual cause.');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
throw error;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function probeBridge(session, windowId) {
|
|
181
|
+
if (!session) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const response = await session.client.sendCommand({
|
|
186
|
+
command: 'execute_js',
|
|
187
|
+
args: { script: 'return 1', windowLabel: windowId },
|
|
188
|
+
}, 2000);
|
|
189
|
+
return response.success === true;
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
140
193
|
}
|
|
141
194
|
}
|
|
142
195
|
/**
|
|
@@ -149,6 +202,7 @@ export async function executeInWebviewWithContext(script, windowId, appIdentifie
|
|
|
149
202
|
*/
|
|
150
203
|
export async function executeAsyncInWebview(script, windowId, timeout, appIdentifier) {
|
|
151
204
|
const resolvedTimeout = timeout ?? 5000;
|
|
205
|
+
const transportTimeout = resolvedTimeout + 2000;
|
|
152
206
|
const wrappedScript = `
|
|
153
207
|
return (async () => {
|
|
154
208
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -162,7 +216,7 @@ export async function executeAsyncInWebview(script, windowId, timeout, appIdenti
|
|
|
162
216
|
return await Promise.race([scriptPromise, timeoutPromise]);
|
|
163
217
|
})();
|
|
164
218
|
`;
|
|
165
|
-
return executeInWebview(wrappedScript, windowId, appIdentifier);
|
|
219
|
+
return executeInWebview(wrappedScript, windowId, appIdentifier, transportTimeout);
|
|
166
220
|
}
|
|
167
221
|
// ============================================================================
|
|
168
222
|
// Console Log Capture System
|
|
@@ -209,14 +263,12 @@ export async function initializeConsoleCapture() {
|
|
|
209
263
|
/**
|
|
210
264
|
* Retrieve captured console logs with optional filtering.
|
|
211
265
|
*
|
|
212
|
-
* @param
|
|
213
|
-
* @param since - Optional ISO timestamp to filter logs after this time
|
|
214
|
-
* @param windowId - Optional window label to target (defaults to "main")
|
|
215
|
-
* @param appIdentifier - Optional app identifier to target specific app
|
|
266
|
+
* @param options - Filtering and formatting options
|
|
216
267
|
* @returns Formatted console logs as string
|
|
217
268
|
*/
|
|
218
|
-
export async function getConsoleLogs(
|
|
219
|
-
const
|
|
269
|
+
export async function getConsoleLogs(options = {}) {
|
|
270
|
+
const { filter, since, lines = 50, windowId, appIdentifier, level, maxChars = 20000, maxCharsPerEntry = 2000 } = options;
|
|
271
|
+
const resolvedLines = lines, resolvedMaxChars = maxChars, resolvedMaxCharsPerEntry = maxCharsPerEntry, resolvedLevel = level ?? '', filterStr = filter ? filter.replace(/'/g, '\\\'') : '', levelStr = resolvedLevel;
|
|
220
272
|
const sinceStr = since || '';
|
|
221
273
|
const script = `
|
|
222
274
|
const logs = window.__MCP_CONSOLE_LOGS__ || [];
|
|
@@ -227,6 +279,10 @@ export async function getConsoleLogs(filter, since, windowId, appIdentifier) {
|
|
|
227
279
|
filtered = filtered.filter(l => l.timestamp > sinceTime);
|
|
228
280
|
}
|
|
229
281
|
|
|
282
|
+
if ('${levelStr}') {
|
|
283
|
+
filtered = filtered.filter(l => l.level === '${levelStr}');
|
|
284
|
+
}
|
|
285
|
+
|
|
230
286
|
if ('${filterStr}') {
|
|
231
287
|
try {
|
|
232
288
|
const regex = new RegExp('${filterStr}', 'i');
|
|
@@ -236,9 +292,34 @@ export async function getConsoleLogs(filter, since, windowId, appIdentifier) {
|
|
|
236
292
|
}
|
|
237
293
|
}
|
|
238
294
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
295
|
+
filtered = filtered.slice(-${resolvedLines});
|
|
296
|
+
|
|
297
|
+
var budget = ${resolvedMaxChars};
|
|
298
|
+
var dropped = 0;
|
|
299
|
+
var out = [];
|
|
300
|
+
|
|
301
|
+
for (var i = filtered.length - 1; i >= 0; i--) {
|
|
302
|
+
var entry = filtered[i],
|
|
303
|
+
excess = entry.message.length - ${resolvedMaxCharsPerEntry},
|
|
304
|
+
message = entry.message.length > ${resolvedMaxCharsPerEntry}
|
|
305
|
+
? entry.message.slice(0, ${resolvedMaxCharsPerEntry}) + '…[' + excess + ' more chars]'
|
|
306
|
+
: entry.message,
|
|
307
|
+
prefix = '[ ' + new Date(entry.timestamp).toISOString() + ' ] [ ' + entry.level.toUpperCase() + ' ] ',
|
|
308
|
+
line = prefix + message;
|
|
309
|
+
|
|
310
|
+
if (line.length > budget) { dropped = i + 1; break; }
|
|
311
|
+
budget -= line.length + 1;
|
|
312
|
+
out.unshift(line);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (dropped > 0) {
|
|
316
|
+
out.unshift(
|
|
317
|
+
'[ ' + dropped + ' older entries dropped to fit maxChars=' + ${resolvedMaxChars} +
|
|
318
|
+
'. Narrow with level, filter, or since. ]'
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return out.join('\\n');
|
|
242
323
|
`;
|
|
243
324
|
return executeInWebview(script, windowId, appIdentifier);
|
|
244
325
|
}
|
|
@@ -265,7 +346,7 @@ function parseDataUrl(dataUrl) {
|
|
|
265
346
|
/**
|
|
266
347
|
* Build screenshot result with image content and optional text context.
|
|
267
348
|
*/
|
|
268
|
-
function buildScreenshotResult(dataUrl, method, windowContext) {
|
|
349
|
+
function buildScreenshotResult(dataUrl, method, windowContext, metadata) {
|
|
269
350
|
const parsed = parseDataUrl(dataUrl);
|
|
270
351
|
if (!parsed) {
|
|
271
352
|
throw new Error(`Invalid data URL format: ${dataUrl.substring(0, 50)}...`);
|
|
@@ -279,6 +360,16 @@ function buildScreenshotResult(dataUrl, method, windowContext) {
|
|
|
279
360
|
contextText += `\n\n⚠️ ${windowContext.warning}`;
|
|
280
361
|
}
|
|
281
362
|
}
|
|
363
|
+
// Add screenshot metadata if available
|
|
364
|
+
if (metadata) {
|
|
365
|
+
const scale = metadata.cssWidth > 0 ? metadata.imageWidth / metadata.cssWidth : 0;
|
|
366
|
+
const inverseScale = scale > 0 ? (1 / scale).toFixed(1) : '1.0';
|
|
367
|
+
contextText += `\nImage is ${metadata.imageWidth}x${metadata.imageHeight} px. ` +
|
|
368
|
+
`Webview viewport is ${Math.round(metadata.cssWidth)}x${Math.round(metadata.cssHeight)} CSS px. ` +
|
|
369
|
+
`Scale ${scale.toFixed(3)}.`;
|
|
370
|
+
contextText += '\nwebview_interact x and y are CSS pixels: ' +
|
|
371
|
+
`multiply image coordinates by ${inverseScale}.`;
|
|
372
|
+
}
|
|
282
373
|
content.push({ type: 'text', text: contextText });
|
|
283
374
|
// Add the image content
|
|
284
375
|
content.push({
|
|
@@ -309,49 +400,53 @@ async function prepareHtml2canvasScript(format, quality, windowId, appIdentifier
|
|
|
309
400
|
return buildScreenshotScript(format, quality);
|
|
310
401
|
}
|
|
311
402
|
}
|
|
403
|
+
function parseNativeScreenshotPayload(rawPayload) {
|
|
404
|
+
if (typeof rawPayload === 'string') {
|
|
405
|
+
return { dataUrl: rawPayload };
|
|
406
|
+
}
|
|
407
|
+
const payload = rawPayload, dimensions = [payload.imageWidth, payload.imageHeight, payload.cssWidth, payload.cssHeight], hasMetadata = dimensions.every((value) => { return typeof value === 'number'; });
|
|
408
|
+
if (!hasMetadata) {
|
|
409
|
+
return { dataUrl: payload.dataUrl };
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
dataUrl: payload.dataUrl,
|
|
413
|
+
metadata: {
|
|
414
|
+
imageWidth: payload.imageWidth,
|
|
415
|
+
imageHeight: payload.imageHeight,
|
|
416
|
+
cssWidth: payload.cssWidth,
|
|
417
|
+
cssHeight: payload.cssHeight,
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async function captureNativeScreenshot(options) {
|
|
422
|
+
const { format, quality, windowId, appIdentifier, maxWidth } = options;
|
|
423
|
+
await ensureReady(windowId, appIdentifier);
|
|
424
|
+
const session = resolveTargetApp(appIdentifier), args = { format, quality, windowLabel: windowId, maxWidth }, command = { command: 'capture_native_screenshot', args }, response = await session.client.sendCommand(command, 15000);
|
|
425
|
+
if (!response.success || !response.data) {
|
|
426
|
+
throw new Error(response.error || 'Native screenshot returned invalid data');
|
|
427
|
+
}
|
|
428
|
+
const { dataUrl, metadata } = parseNativeScreenshotPayload(response.data);
|
|
429
|
+
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
430
|
+
throw new Error('Native screenshot returned invalid data');
|
|
431
|
+
}
|
|
432
|
+
return buildScreenshotResult(dataUrl, 'native API', response.windowContext, metadata);
|
|
433
|
+
}
|
|
312
434
|
/**
|
|
313
|
-
* Capture a screenshot of the
|
|
435
|
+
* Capture a screenshot of the visible webview viewport.
|
|
314
436
|
*
|
|
315
437
|
* @param options - Screenshot options (format, quality, windowId, appIdentifier, etc.)
|
|
316
438
|
* @returns Screenshot result with image content
|
|
317
439
|
*/
|
|
318
440
|
export async function captureScreenshot(options = {}) {
|
|
319
|
-
const { format = 'jpeg', quality = 80, windowId, appIdentifier, maxWidth } = options;
|
|
320
|
-
|
|
321
|
-
// - macOS: WKWebView takeSnapshot
|
|
322
|
-
// - Windows: WebView2 CapturePreview
|
|
323
|
-
// - Linux: Chromium/WebKit screenshot APIs
|
|
441
|
+
const { format = 'jpeg', quality = 80, windowId, appIdentifier, maxWidth, allowScreenCapture = false, } = options;
|
|
442
|
+
let nativeErrorMsg = 'Native screenshot unavailable';
|
|
324
443
|
try {
|
|
325
|
-
|
|
326
|
-
await ensureReady(windowId, appIdentifier);
|
|
327
|
-
// Resolve target session
|
|
328
|
-
const session = resolveTargetApp(appIdentifier);
|
|
329
|
-
const client = session.client;
|
|
330
|
-
// Use longer timeout (15s) for native screenshot - the Rust code waits up to 10s
|
|
331
|
-
const response = await client.sendCommand({
|
|
332
|
-
command: 'capture_native_screenshot',
|
|
333
|
-
args: {
|
|
334
|
-
format,
|
|
335
|
-
quality,
|
|
336
|
-
windowLabel: windowId,
|
|
337
|
-
maxWidth,
|
|
338
|
-
},
|
|
339
|
-
}, 15000);
|
|
340
|
-
if (!response.success || !response.data) {
|
|
341
|
-
throw new Error(response.error || 'Native screenshot returned invalid data');
|
|
342
|
-
}
|
|
343
|
-
// The native command returns a base64 data URL
|
|
344
|
-
const dataUrl = response.data;
|
|
345
|
-
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
346
|
-
throw new Error('Native screenshot returned invalid data');
|
|
347
|
-
}
|
|
348
|
-
// Build response with window context
|
|
349
|
-
return buildScreenshotResult(dataUrl, 'native API', response.windowContext);
|
|
444
|
+
return await captureNativeScreenshot({ format, quality, windowId, appIdentifier, maxWidth });
|
|
350
445
|
}
|
|
351
446
|
catch (nativeError) {
|
|
352
447
|
// Log the native error for debugging, then fall back
|
|
353
|
-
|
|
354
|
-
driverLogger.error(`Native screenshot failed: ${
|
|
448
|
+
nativeErrorMsg = nativeError instanceof Error ? nativeError.message : String(nativeError);
|
|
449
|
+
driverLogger.error(`Native screenshot failed: ${nativeErrorMsg}, falling back to html2canvas`);
|
|
355
450
|
}
|
|
356
451
|
// Fallback 1: Use html2canvas library for high-quality DOM rendering
|
|
357
452
|
// Try to use the script manager to register html2canvas for persistence
|
|
@@ -422,6 +517,12 @@ export async function captureScreenshot(options = {}) {
|
|
|
422
517
|
throw new Error(`html2canvas returned invalid result: ${result?.substring(0, 100) || 'null'}`);
|
|
423
518
|
}
|
|
424
519
|
catch (html2canvasError) {
|
|
520
|
+
const html2canvasMsg = html2canvasError instanceof Error ? html2canvasError.message : 'html2canvas failed';
|
|
521
|
+
if (!allowScreenCapture) {
|
|
522
|
+
throw new Error(`Screenshot capture failed. Native API error: ${nativeErrorMsg}, ` +
|
|
523
|
+
`html2canvas error: ${html2canvasMsg}. ` +
|
|
524
|
+
'Screen Capture API fallback is disabled; set allowScreenCapture to true to request interactive permission.');
|
|
525
|
+
}
|
|
425
526
|
try {
|
|
426
527
|
// Fallback to Screen Capture API
|
|
427
528
|
const result = await executeAsyncInWebview(screenCaptureScript, windowId, 5000, appIdentifier);
|
|
@@ -433,9 +534,8 @@ export async function captureScreenshot(options = {}) {
|
|
|
433
534
|
}
|
|
434
535
|
catch (screenCaptureError) {
|
|
435
536
|
// All methods failed - throw a proper error
|
|
436
|
-
const html2canvasMsg = html2canvasError instanceof Error ? html2canvasError.message : 'html2canvas failed';
|
|
437
537
|
const screenCaptureMsg = screenCaptureError instanceof Error ? screenCaptureError.message : 'Screen Capture API failed';
|
|
438
|
-
throw new Error(
|
|
538
|
+
throw new Error(`Screenshot capture failed. Native API error: ${nativeErrorMsg}, ` +
|
|
439
539
|
`html2canvas error: ${html2canvasMsg}, ` +
|
|
440
540
|
`Screen Capture API error: ${screenCaptureMsg}`);
|
|
441
541
|
}
|
|
@@ -454,4 +554,5 @@ export const GetConsoleLogsSchema = z.object({
|
|
|
454
554
|
export const CaptureScreenshotSchema = z.object({
|
|
455
555
|
format: z.enum(['png', 'jpeg']).optional().default('jpeg').describe('Image format'),
|
|
456
556
|
quality: z.number().min(0).max(100).optional().describe('JPEG quality (0-100)'),
|
|
557
|
+
allowScreenCapture: z.boolean().optional().default(false).describe('Allow the Screen Capture API fallback, which opens an interactive OS permission prompt'),
|
|
457
558
|
});
|