@hypothesi/tauri-mcp-server 0.1.2 ā 0.2.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/README.md +116 -0
- package/dist/driver/plugin-commands.js +29 -0
- package/dist/driver/script-manager.js +97 -0
- package/dist/driver/scripts/html2canvas-loader.js +54 -42
- package/dist/driver/webview-executor.js +84 -35
- package/dist/driver/webview-interactions.js +58 -33
- package/dist/index.js +29 -2
- package/dist/prompts-registry.js +54 -0
- package/dist/tools-registry.js +265 -39
- package/dist/types/window.js +4 -0
- package/package.json +58 -58
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @hypothesi/tauri-mcp-server
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hypothesi/tauri-mcp-server)
|
|
4
|
+
[](https://github.com/hypothesi/mcp-server-tauri/blob/main/LICENSE)
|
|
5
|
+
|
|
6
|
+
A **Model Context Protocol (MCP) server** that enables AI assistants like Claude, Cursor, and Windsurf to build, test, and debug Tauri v2 applications.
|
|
7
|
+
|
|
8
|
+
š **[Full Documentation](https://hypothesi.github.io/mcp-server-tauri)**
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
| Category | Capabilities |
|
|
13
|
+
|----------|-------------|
|
|
14
|
+
| šÆ **UI Automation** | Screenshots, clicks, typing, scrolling, element finding |
|
|
15
|
+
| š **IPC Monitoring** | Capture and inspect Tauri IPC calls in real-time |
|
|
16
|
+
| š± **Mobile Dev** | Manage Android emulators & iOS simulators |
|
|
17
|
+
| š ļø **CLI Integration** | Run any Tauri command (`init`, `dev`, `build`, etc.) |
|
|
18
|
+
| āļø **Configuration** | Read/write Tauri config files with validation |
|
|
19
|
+
| š **Logs** | Stream Android logcat, iOS device logs, system logs |
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
### 1. Add the MCP Bridge Plugin to Your Tauri App
|
|
24
|
+
|
|
25
|
+
```toml
|
|
26
|
+
# Cargo.toml
|
|
27
|
+
[dependencies]
|
|
28
|
+
tauri-plugin-mcp-bridge = "0.1"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```rust
|
|
32
|
+
// src-tauri/src/main.rs
|
|
33
|
+
fn main() {
|
|
34
|
+
tauri::Builder::default()
|
|
35
|
+
.plugin(tauri_plugin_mcp_bridge::init())
|
|
36
|
+
.run(tauri::generate_context!())
|
|
37
|
+
.expect("error while running tauri application");
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 2. Configure Your AI Assistant
|
|
42
|
+
|
|
43
|
+
**Claude Code:**
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
claude mcp add tauri npx @hypothesi/tauri-mcp-server
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Cursor / VS Code / Windsurf / Cline:**
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"mcpServers": {
|
|
54
|
+
"tauri": {
|
|
55
|
+
"command": "npx",
|
|
56
|
+
"args": ["-y", "@hypothesi/tauri-mcp-server"]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Available Tools
|
|
63
|
+
|
|
64
|
+
### UI Automation
|
|
65
|
+
|
|
66
|
+
| Tool | Description |
|
|
67
|
+
|------|-------------|
|
|
68
|
+
| `tauri_webview_screenshot` | Capture webview screenshots |
|
|
69
|
+
| `tauri_driver_session` | Start/stop automation session |
|
|
70
|
+
| `tauri_webview_find_element` | Find elements by selector |
|
|
71
|
+
| `tauri_webview_interact` | Click, scroll, swipe, long-press |
|
|
72
|
+
| `tauri_webview_keyboard` | Type text or send key events |
|
|
73
|
+
| `tauri_webview_wait_for` | Wait for elements, text, or events |
|
|
74
|
+
| `tauri_webview_get_styles` | Get computed CSS styles |
|
|
75
|
+
| `tauri_webview_execute_js` | Execute JavaScript in webview |
|
|
76
|
+
| `tauri_webview_focus_element` | Focus on elements |
|
|
77
|
+
| `tauri_driver_get_console_logs` | Get browser console logs |
|
|
78
|
+
| `tauri_read_platform_logs` | Read Android/iOS/system logs |
|
|
79
|
+
|
|
80
|
+
### IPC & Plugin
|
|
81
|
+
|
|
82
|
+
| Tool | Description |
|
|
83
|
+
|------|-------------|
|
|
84
|
+
| `tauri_plugin_execute_ipc` | Execute Tauri IPC commands |
|
|
85
|
+
| `tauri_plugin_get_window_info` | Get window information |
|
|
86
|
+
| `tauri_plugin_get_backend_state` | Get app metadata and state |
|
|
87
|
+
| `tauri_plugin_ipc_monitor` | Start/stop IPC monitoring |
|
|
88
|
+
| `tauri_plugin_ipc_get_events` | Get captured IPC events |
|
|
89
|
+
| `tauri_plugin_emit_event` | Emit custom events |
|
|
90
|
+
|
|
91
|
+
### Mobile Development
|
|
92
|
+
|
|
93
|
+
| Tool | Description |
|
|
94
|
+
|------|-------------|
|
|
95
|
+
| `tauri_list_devices` | List Android devices and iOS simulators |
|
|
96
|
+
| `tauri_launch_emulator` | Launch Android AVD or iOS Simulator |
|
|
97
|
+
|
|
98
|
+
### Project Management
|
|
99
|
+
|
|
100
|
+
| Tool | Description |
|
|
101
|
+
|------|-------------|
|
|
102
|
+
| `tauri_run_command` | Run any Tauri CLI command |
|
|
103
|
+
| `tauri_read_config` | Read Tauri config files |
|
|
104
|
+
| `tauri_write_config` | Write config files with validation |
|
|
105
|
+
| `tauri_get_docs` | Fetch Tauri documentation |
|
|
106
|
+
|
|
107
|
+
## Links
|
|
108
|
+
|
|
109
|
+
- [Documentation](https://hypothesi.github.io/mcp-server-tauri)
|
|
110
|
+
- [GitHub Repository](https://github.com/hypothesi/mcp-server-tauri)
|
|
111
|
+
- [MCP Bridge Plugin (crates.io)](https://crates.io/crates/tauri-plugin-mcp-bridge)
|
|
112
|
+
- [Changelog](https://github.com/hypothesi/mcp-server-tauri/blob/main/packages/mcp-server/CHANGELOG.md)
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT Ā© [hypothesi](https://github.com/hypothesi)
|
|
@@ -140,3 +140,32 @@ export async function getBackendState() {
|
|
|
140
140
|
throw new Error(`Failed to get backend state: ${message}`);
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// Window Management
|
|
145
|
+
// ============================================================================
|
|
146
|
+
export const ListWindowsSchema = z.object({});
|
|
147
|
+
/**
|
|
148
|
+
* Lists all open webview windows in the Tauri application.
|
|
149
|
+
*/
|
|
150
|
+
export async function listWindows() {
|
|
151
|
+
try {
|
|
152
|
+
await connectPlugin();
|
|
153
|
+
const client = getPluginClient();
|
|
154
|
+
const response = await client.sendCommand({
|
|
155
|
+
command: 'list_windows',
|
|
156
|
+
});
|
|
157
|
+
if (!response.success) {
|
|
158
|
+
throw new Error(response.error || 'Unknown error');
|
|
159
|
+
}
|
|
160
|
+
const windows = response.data;
|
|
161
|
+
return JSON.stringify({
|
|
162
|
+
windows,
|
|
163
|
+
defaultWindow: 'main',
|
|
164
|
+
totalCount: windows.length,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
169
|
+
throw new Error(`Failed to list windows: ${message}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Script Manager - Manages persistent script injection across page navigations.
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to register, remove, and manage scripts that
|
|
5
|
+
* should be automatically re-injected when pages load or navigate.
|
|
6
|
+
*
|
|
7
|
+
* @internal This module is for internal use only and is not exposed as MCP tools.
|
|
8
|
+
*/
|
|
9
|
+
import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
10
|
+
/**
|
|
11
|
+
* Registers a script to be injected into the webview.
|
|
12
|
+
*
|
|
13
|
+
* The script will be immediately injected if the page is loaded, and will be
|
|
14
|
+
* automatically re-injected on subsequent page loads/navigations.
|
|
15
|
+
*
|
|
16
|
+
* @param id - Unique identifier for the script
|
|
17
|
+
* @param type - Type of script ('inline' for code, 'url' for external script)
|
|
18
|
+
* @param content - The script content (JavaScript code) or URL
|
|
19
|
+
* @param windowLabel - Optional window label to target
|
|
20
|
+
* @returns Promise resolving to registration result
|
|
21
|
+
*/
|
|
22
|
+
export async function registerScript(id, type, content, windowLabel) {
|
|
23
|
+
await connectPlugin();
|
|
24
|
+
const client = getPluginClient();
|
|
25
|
+
const response = await client.sendCommand({
|
|
26
|
+
command: 'register_script',
|
|
27
|
+
args: { id, type, content, windowLabel },
|
|
28
|
+
});
|
|
29
|
+
if (!response.success) {
|
|
30
|
+
throw new Error(response.error || 'Failed to register script');
|
|
31
|
+
}
|
|
32
|
+
return response.data;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Removes a script from the registry and DOM.
|
|
36
|
+
*
|
|
37
|
+
* @param id - The script ID to remove
|
|
38
|
+
* @param windowLabel - Optional window label to target
|
|
39
|
+
* @returns Promise resolving to removal result
|
|
40
|
+
*/
|
|
41
|
+
export async function removeScript(id, windowLabel) {
|
|
42
|
+
await connectPlugin();
|
|
43
|
+
const client = getPluginClient();
|
|
44
|
+
const response = await client.sendCommand({
|
|
45
|
+
command: 'remove_script',
|
|
46
|
+
args: { id, windowLabel },
|
|
47
|
+
});
|
|
48
|
+
if (!response.success) {
|
|
49
|
+
throw new Error(response.error || 'Failed to remove script');
|
|
50
|
+
}
|
|
51
|
+
return response.data;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Clears all registered scripts from the registry and DOM.
|
|
55
|
+
*
|
|
56
|
+
* @param windowLabel - Optional window label to target
|
|
57
|
+
* @returns Promise resolving to the number of scripts cleared
|
|
58
|
+
*/
|
|
59
|
+
export async function clearScripts(windowLabel) {
|
|
60
|
+
await connectPlugin();
|
|
61
|
+
const client = getPluginClient();
|
|
62
|
+
const response = await client.sendCommand({
|
|
63
|
+
command: 'clear_scripts',
|
|
64
|
+
args: { windowLabel },
|
|
65
|
+
});
|
|
66
|
+
if (!response.success) {
|
|
67
|
+
throw new Error(response.error || 'Failed to clear scripts');
|
|
68
|
+
}
|
|
69
|
+
return response.data;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Gets all registered scripts.
|
|
73
|
+
*
|
|
74
|
+
* @returns Promise resolving to the list of registered scripts
|
|
75
|
+
*/
|
|
76
|
+
export async function getScripts() {
|
|
77
|
+
await connectPlugin();
|
|
78
|
+
const client = getPluginClient();
|
|
79
|
+
const response = await client.sendCommand({
|
|
80
|
+
command: 'get_scripts',
|
|
81
|
+
args: {},
|
|
82
|
+
});
|
|
83
|
+
if (!response.success) {
|
|
84
|
+
throw new Error(response.error || 'Failed to get scripts');
|
|
85
|
+
}
|
|
86
|
+
return response.data;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Checks if a script with the given ID is registered.
|
|
90
|
+
*
|
|
91
|
+
* @param id - The script ID to check
|
|
92
|
+
* @returns Promise resolving to true if the script is registered
|
|
93
|
+
*/
|
|
94
|
+
export async function isScriptRegistered(id) {
|
|
95
|
+
const { scripts } = await getScripts();
|
|
96
|
+
return scripts.some((s) => { return s.id === id; });
|
|
97
|
+
}
|
|
@@ -9,6 +9,8 @@ import { createRequire } from 'module';
|
|
|
9
9
|
// Use createRequire to resolve the path to html2canvas in node_modules
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
let html2canvasSource = null;
|
|
12
|
+
/** Script ID used for the html2canvas library in the script registry. */
|
|
13
|
+
export const HTML2CANVAS_SCRIPT_ID = '__mcp_html2canvas__';
|
|
12
14
|
/**
|
|
13
15
|
* Get the html2canvas library source code.
|
|
14
16
|
* Loaded lazily and cached.
|
|
@@ -21,13 +23,63 @@ export function getHtml2CanvasSource() {
|
|
|
21
23
|
}
|
|
22
24
|
return html2canvasSource;
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Build a script that captures a screenshot using html2canvas.
|
|
28
|
+
* Assumes html2canvas is already loaded (either via script manager or inline).
|
|
29
|
+
*/
|
|
30
|
+
export function buildScreenshotCaptureScript(format, quality) {
|
|
31
|
+
// Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
|
|
32
|
+
return `
|
|
33
|
+
// Get the html2canvas function (may be on window, self, or globalThis)
|
|
34
|
+
const html2canvasFn = typeof html2canvas !== 'undefined' ? html2canvas :
|
|
35
|
+
(typeof window !== 'undefined' && window.html2canvas) ? window.html2canvas :
|
|
36
|
+
(typeof self !== 'undefined' && self.html2canvas) ? self.html2canvas :
|
|
37
|
+
(typeof globalThis !== 'undefined' && globalThis.html2canvas) ? globalThis.html2canvas : null;
|
|
38
|
+
|
|
39
|
+
if (!html2canvasFn) {
|
|
40
|
+
throw new Error('html2canvas not loaded - function not found on any global');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Capture the entire document
|
|
44
|
+
const element = document.documentElement;
|
|
45
|
+
if (!element) {
|
|
46
|
+
throw new Error('document.documentElement is null');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Configure html2canvas options
|
|
50
|
+
const options = {
|
|
51
|
+
backgroundColor: null,
|
|
52
|
+
scale: window.devicePixelRatio || 1,
|
|
53
|
+
logging: false,
|
|
54
|
+
useCORS: true,
|
|
55
|
+
allowTaint: false,
|
|
56
|
+
imageTimeout: 5000,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Capture the webview
|
|
60
|
+
const canvas = await html2canvasFn(element, options);
|
|
61
|
+
if (!canvas) {
|
|
62
|
+
throw new Error('html2canvas returned null canvas');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Convert to data URL
|
|
66
|
+
const mimeType = '${format}' === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
67
|
+
const dataUrl = canvas.toDataURL(mimeType, ${quality / 100});
|
|
68
|
+
|
|
69
|
+
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
70
|
+
throw new Error('canvas.toDataURL returned invalid result: ' + (dataUrl ? dataUrl.substring(0, 50) : 'null'));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return dataUrl;
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
24
76
|
/**
|
|
25
77
|
* Build a script that injects html2canvas and captures a screenshot.
|
|
78
|
+
* This is the legacy function that inlines the library - kept for fallback.
|
|
26
79
|
*/
|
|
27
80
|
export function buildScreenshotScript(format, quality) {
|
|
28
81
|
const html2canvas = getHtml2CanvasSource();
|
|
29
82
|
// Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
|
|
30
|
-
// The wrapper adds: (async () => { const scriptPromise = (async () => { ...script... })(); ... })()
|
|
31
83
|
return `
|
|
32
84
|
try {
|
|
33
85
|
// Inject html2canvas if not already present
|
|
@@ -37,47 +89,7 @@ export function buildScreenshotScript(format, quality) {
|
|
|
37
89
|
// After loading, html2canvas should be on globalThis/self/window
|
|
38
90
|
}
|
|
39
91
|
|
|
40
|
-
|
|
41
|
-
const html2canvasFn = typeof html2canvas !== 'undefined' ? html2canvas :
|
|
42
|
-
(typeof window !== 'undefined' && window.html2canvas) ? window.html2canvas :
|
|
43
|
-
(typeof self !== 'undefined' && self.html2canvas) ? self.html2canvas :
|
|
44
|
-
(typeof globalThis !== 'undefined' && globalThis.html2canvas) ? globalThis.html2canvas : null;
|
|
45
|
-
|
|
46
|
-
if (!html2canvasFn) {
|
|
47
|
-
throw new Error('html2canvas failed to load - function not found on any global');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Capture the entire document
|
|
51
|
-
const element = document.documentElement;
|
|
52
|
-
if (!element) {
|
|
53
|
-
throw new Error('document.documentElement is null');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Configure html2canvas options
|
|
57
|
-
const options = {
|
|
58
|
-
backgroundColor: null,
|
|
59
|
-
scale: window.devicePixelRatio || 1,
|
|
60
|
-
logging: false,
|
|
61
|
-
useCORS: true,
|
|
62
|
-
allowTaint: false,
|
|
63
|
-
imageTimeout: 5000,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
// Capture the webview
|
|
67
|
-
const canvas = await html2canvasFn(element, options);
|
|
68
|
-
if (!canvas) {
|
|
69
|
-
throw new Error('html2canvas returned null canvas');
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Convert to data URL
|
|
73
|
-
const mimeType = '${format}' === 'jpeg' ? 'image/jpeg' : 'image/png';
|
|
74
|
-
const dataUrl = canvas.toDataURL(mimeType, ${quality / 100});
|
|
75
|
-
|
|
76
|
-
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
77
|
-
throw new Error('canvas.toDataURL returned invalid result: ' + (dataUrl ? dataUrl.substring(0, 50) : 'null'));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return dataUrl;
|
|
92
|
+
${buildScreenshotCaptureScript(format, quality)}
|
|
81
93
|
} catch (screenshotError) {
|
|
82
94
|
// Re-throw with more context
|
|
83
95
|
throw new Error('Screenshot capture failed: ' + (screenshotError.message || String(screenshotError)));
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { getPluginClient, connectPlugin } from './plugin-client.js';
|
|
3
|
-
import { buildScreenshotScript } from './scripts/html2canvas-loader.js';
|
|
3
|
+
import { buildScreenshotScript, buildScreenshotCaptureScript, getHtml2CanvasSource, HTML2CANVAS_SCRIPT_ID, } from './scripts/html2canvas-loader.js';
|
|
4
|
+
import { registerScript, isScriptRegistered } from './script-manager.js';
|
|
4
5
|
/**
|
|
5
6
|
* WebView Executor - Native IPC-based JavaScript execution
|
|
6
7
|
*
|
|
@@ -38,16 +39,25 @@ export async function ensureReady() {
|
|
|
38
39
|
export function resetInitialization() {
|
|
39
40
|
isInitialized = false;
|
|
40
41
|
}
|
|
41
|
-
// ============================================================================
|
|
42
|
-
// Core Execution Functions
|
|
43
|
-
// ============================================================================
|
|
44
42
|
/**
|
|
45
43
|
* Execute JavaScript in the Tauri webview using native IPC via WebSocket.
|
|
46
44
|
*
|
|
47
45
|
* @param script - JavaScript code to execute in the webview context
|
|
48
|
-
* @
|
|
46
|
+
* @param windowId - Optional window label to target (defaults to "main")
|
|
47
|
+
* @returns Result of the script execution with window context
|
|
48
|
+
*/
|
|
49
|
+
export async function executeInWebview(script, windowId) {
|
|
50
|
+
const { result } = await executeInWebviewWithContext(script, windowId);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Execute JavaScript in the Tauri webview and return window context.
|
|
55
|
+
*
|
|
56
|
+
* @param script - JavaScript code to execute in the webview context
|
|
57
|
+
* @param windowId - Optional window label to target (defaults to "main")
|
|
58
|
+
* @returns Result of the script execution with window context
|
|
49
59
|
*/
|
|
50
|
-
export async function
|
|
60
|
+
export async function executeInWebviewWithContext(script, windowId) {
|
|
51
61
|
try {
|
|
52
62
|
// Ensure we're fully initialized
|
|
53
63
|
await ensureReady();
|
|
@@ -56,22 +66,30 @@ export async function executeInWebview(script) {
|
|
|
56
66
|
// Use 7s timeout (longer than Rust's 5s) so errors return before Node times out.
|
|
57
67
|
const response = await client.sendCommand({
|
|
58
68
|
command: 'execute_js',
|
|
59
|
-
args: { script },
|
|
69
|
+
args: { script, windowLabel: windowId },
|
|
60
70
|
}, 7000);
|
|
61
|
-
// console.log('executeInWebview response:', JSON.stringify(response));
|
|
62
71
|
if (!response.success) {
|
|
63
72
|
throw new Error(response.error || 'Unknown execution error');
|
|
64
73
|
}
|
|
74
|
+
// Extract window context from response
|
|
75
|
+
const windowContext = response.windowContext;
|
|
65
76
|
// Parse and return the result
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
77
|
+
const data = response.data;
|
|
78
|
+
let result;
|
|
79
|
+
if (data === null || data === undefined) {
|
|
80
|
+
result = 'null';
|
|
81
|
+
}
|
|
82
|
+
else if (typeof data === 'string') {
|
|
83
|
+
result = data;
|
|
70
84
|
}
|
|
71
|
-
|
|
72
|
-
|
|
85
|
+
else {
|
|
86
|
+
result = JSON.stringify(data);
|
|
73
87
|
}
|
|
74
|
-
return
|
|
88
|
+
return {
|
|
89
|
+
result,
|
|
90
|
+
windowLabel: windowContext?.windowLabel || 'main',
|
|
91
|
+
warning: windowContext?.warning,
|
|
92
|
+
};
|
|
75
93
|
}
|
|
76
94
|
catch (error) {
|
|
77
95
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -82,10 +100,11 @@ export async function executeInWebview(script) {
|
|
|
82
100
|
* Execute async JavaScript in the webview with timeout support.
|
|
83
101
|
*
|
|
84
102
|
* @param script - JavaScript code to execute (can use await)
|
|
103
|
+
* @param windowId - Optional window label to target (defaults to "main")
|
|
85
104
|
* @param timeout - Timeout in milliseconds (default: 5000)
|
|
86
105
|
* @returns Result of the script execution
|
|
87
106
|
*/
|
|
88
|
-
export async function executeAsyncInWebview(script, timeout = 5000) {
|
|
107
|
+
export async function executeAsyncInWebview(script, windowId, timeout = 5000) {
|
|
89
108
|
const wrappedScript = `
|
|
90
109
|
return (async () => {
|
|
91
110
|
const timeoutPromise = new Promise((_, reject) => {
|
|
@@ -99,7 +118,7 @@ export async function executeAsyncInWebview(script, timeout = 5000) {
|
|
|
99
118
|
return await Promise.race([scriptPromise, timeoutPromise]);
|
|
100
119
|
})();
|
|
101
120
|
`;
|
|
102
|
-
return executeInWebview(wrappedScript);
|
|
121
|
+
return executeInWebview(wrappedScript, windowId);
|
|
103
122
|
}
|
|
104
123
|
// ============================================================================
|
|
105
124
|
// Console Log Capture System
|
|
@@ -187,17 +206,46 @@ export async function clearConsoleLogs() {
|
|
|
187
206
|
`;
|
|
188
207
|
return executeInWebview(script);
|
|
189
208
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
function formatScreenshotResponse(dataUrl, windowContext) {
|
|
210
|
+
let result = 'Webview screenshot captured (native)';
|
|
211
|
+
if (windowContext) {
|
|
212
|
+
result += ` in window "${windowContext.windowLabel}"`;
|
|
213
|
+
if (windowContext.warning) {
|
|
214
|
+
result += `\n\nā ļø ${windowContext.warning}`;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
result += `:\n\n`;
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Prepares the html2canvas script for screenshot capture.
|
|
222
|
+
* Tries to use the script manager for persistence, falls back to inline injection.
|
|
223
|
+
*/
|
|
224
|
+
async function prepareHtml2canvasScript(format, quality) {
|
|
225
|
+
try {
|
|
226
|
+
// Check if html2canvas is already registered
|
|
227
|
+
const isRegistered = await isScriptRegistered(HTML2CANVAS_SCRIPT_ID);
|
|
228
|
+
if (!isRegistered) {
|
|
229
|
+
// Register html2canvas via script manager for persistence across navigations
|
|
230
|
+
const html2canvasSource = getHtml2CanvasSource();
|
|
231
|
+
await registerScript(HTML2CANVAS_SCRIPT_ID, 'inline', html2canvasSource);
|
|
232
|
+
}
|
|
233
|
+
// Use the capture-only script since html2canvas is now registered
|
|
234
|
+
return buildScreenshotCaptureScript(format, quality);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Script manager not available, fall back to inline injection
|
|
238
|
+
return buildScreenshotScript(format, quality);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
193
241
|
/**
|
|
194
242
|
* Capture a screenshot of the entire webview.
|
|
195
243
|
*
|
|
196
|
-
* @param
|
|
197
|
-
* @param quality - JPEG quality (0-100), only used for jpeg format
|
|
244
|
+
* @param options - Screenshot options (format, quality, windowId)
|
|
198
245
|
* @returns Base64-encoded image data URL
|
|
199
246
|
*/
|
|
200
|
-
export async function captureScreenshot(
|
|
247
|
+
export async function captureScreenshot(options = {}) {
|
|
248
|
+
const { format = 'png', quality = 90, windowId } = options;
|
|
201
249
|
// Primary implementation: Use native platform-specific APIs
|
|
202
250
|
// - macOS: WKWebView takeSnapshot
|
|
203
251
|
// - Windows: WebView2 CapturePreview
|
|
@@ -212,18 +260,19 @@ export async function captureScreenshot(format = 'png', quality = 90) {
|
|
|
212
260
|
args: {
|
|
213
261
|
format,
|
|
214
262
|
quality,
|
|
263
|
+
windowLabel: windowId,
|
|
215
264
|
},
|
|
216
265
|
}, 15000);
|
|
217
|
-
if (response.success
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
266
|
+
if (!response.success || !response.data) {
|
|
267
|
+
throw new Error(response.error || 'Native screenshot returned invalid data');
|
|
268
|
+
}
|
|
269
|
+
// The native command returns a base64 data URL
|
|
270
|
+
const dataUrl = response.data;
|
|
271
|
+
if (!dataUrl || !dataUrl.startsWith('data:image/')) {
|
|
272
|
+
throw new Error('Native screenshot returned invalid data');
|
|
224
273
|
}
|
|
225
|
-
//
|
|
226
|
-
|
|
274
|
+
// Build response with window context
|
|
275
|
+
return formatScreenshotResponse(dataUrl, response.windowContext);
|
|
227
276
|
}
|
|
228
277
|
catch (nativeError) {
|
|
229
278
|
// Log the native error for debugging, then fall back
|
|
@@ -231,8 +280,8 @@ export async function captureScreenshot(format = 'png', quality = 90) {
|
|
|
231
280
|
console.error(`Native screenshot failed: ${nativeMsg}, falling back to html2canvas`);
|
|
232
281
|
}
|
|
233
282
|
// Fallback 1: Use html2canvas library for high-quality DOM rendering
|
|
234
|
-
//
|
|
235
|
-
const html2canvasScript =
|
|
283
|
+
// Try to use the script manager to register html2canvas for persistence
|
|
284
|
+
const html2canvasScript = await prepareHtml2canvasScript(format, quality);
|
|
236
285
|
// Fallback: Try Screen Capture API if available
|
|
237
286
|
// Note: This script is wrapped by executeAsyncInWebview, so we don't need an IIFE
|
|
238
287
|
const screenCaptureScript = `
|
|
@@ -291,7 +340,7 @@ export async function captureScreenshot(format = 'png', quality = 90) {
|
|
|
291
340
|
`;
|
|
292
341
|
try {
|
|
293
342
|
// Try html2canvas second (after native APIs)
|
|
294
|
-
const result = await executeAsyncInWebview(html2canvasScript, 10000); // Longer timeout for library loading
|
|
343
|
+
const result = await executeAsyncInWebview(html2canvasScript, undefined, 10000); // Longer timeout for library loading
|
|
295
344
|
// Validate that we got a real data URL, not 'null' or empty
|
|
296
345
|
if (result && result !== 'null' && result.startsWith('data:image/')) {
|
|
297
346
|
return `Webview screenshot captured:\n\n`;
|