safari-pilot 0.1.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/.claude-plugin/plugin.json +35 -0
- package/.mcp.json +11 -0
- package/LICENSE +21 -0
- package/README.md +324 -0
- package/bin/.gitkeep +0 -0
- package/bin/Safari Pilot.app/Contents/CodeResources +0 -0
- package/bin/Safari Pilot.app/Contents/Info.plist +58 -0
- package/bin/Safari Pilot.app/Contents/MacOS/Safari Pilot +0 -0
- package/bin/Safari Pilot.app/Contents/PkgInfo +1 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Info.plist +55 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/MacOS/Safari Pilot Extension +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/background.js +294 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-isolated.js +80 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/content-main.js +310 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-128.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-48.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/icons/icon-96.png +0 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/Resources/manifest.json +39 -0
- package/bin/Safari Pilot.app/Contents/PlugIns/Safari Pilot Extension.appex/Contents/_CodeSignature/CodeResources +194 -0
- package/bin/Safari Pilot.app/Contents/Resources/AppIcon.icns +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Assets.car +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.html +19 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/Info.plist +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/MainMenu.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/NSWindowController-B8D-0N-5wS.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Base.lproj/Main.storyboardc/XfG-lQ-9wD-view-m2S-Jp-Qdl.nib +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Icon.png +0 -0
- package/bin/Safari Pilot.app/Contents/Resources/Script.js +22 -0
- package/bin/Safari Pilot.app/Contents/Resources/Style.css +45 -0
- package/bin/Safari Pilot.app/Contents/_CodeSignature/CodeResources +236 -0
- package/bin/Safari Pilot.zip +0 -0
- package/bin/SafariPilotd +0 -0
- package/dist/engine-selector.d.ts +10 -0
- package/dist/engine-selector.js +55 -0
- package/dist/engine-selector.js.map +1 -0
- package/dist/engines/applescript.d.ts +53 -0
- package/dist/engines/applescript.js +290 -0
- package/dist/engines/applescript.js.map +1 -0
- package/dist/engines/daemon.d.ts +19 -0
- package/dist/engines/daemon.js +187 -0
- package/dist/engines/daemon.js.map +1 -0
- package/dist/engines/engine.d.ts +15 -0
- package/dist/engines/engine.js +42 -0
- package/dist/engines/engine.js.map +1 -0
- package/dist/engines/extension.d.ts +34 -0
- package/dist/engines/extension.js +66 -0
- package/dist/engines/extension.js.map +1 -0
- package/dist/errors.d.ts +128 -0
- package/dist/errors.js +250 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/security/audit-log.d.ts +23 -0
- package/dist/security/audit-log.js +68 -0
- package/dist/security/audit-log.js.map +1 -0
- package/dist/security/circuit-breaker.d.ts +29 -0
- package/dist/security/circuit-breaker.js +114 -0
- package/dist/security/circuit-breaker.js.map +1 -0
- package/dist/security/domain-policy.d.ts +29 -0
- package/dist/security/domain-policy.js +96 -0
- package/dist/security/domain-policy.js.map +1 -0
- package/dist/security/human-approval.d.ts +20 -0
- package/dist/security/human-approval.js +150 -0
- package/dist/security/human-approval.js.map +1 -0
- package/dist/security/idpi-scanner.d.ts +20 -0
- package/dist/security/idpi-scanner.js +102 -0
- package/dist/security/idpi-scanner.js.map +1 -0
- package/dist/security/kill-switch.d.ts +51 -0
- package/dist/security/kill-switch.js +103 -0
- package/dist/security/kill-switch.js.map +1 -0
- package/dist/security/rate-limiter.d.ts +30 -0
- package/dist/security/rate-limiter.js +70 -0
- package/dist/security/rate-limiter.js.map +1 -0
- package/dist/security/screenshot-redaction.d.ts +42 -0
- package/dist/security/screenshot-redaction.js +134 -0
- package/dist/security/screenshot-redaction.js.map +1 -0
- package/dist/security/tab-ownership.d.ts +46 -0
- package/dist/security/tab-ownership.js +85 -0
- package/dist/security/tab-ownership.js.map +1 -0
- package/dist/server.d.ts +53 -0
- package/dist/server.js +347 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/clipboard.d.ts +15 -0
- package/dist/tools/clipboard.js +128 -0
- package/dist/tools/clipboard.js.map +1 -0
- package/dist/tools/compound.d.ts +68 -0
- package/dist/tools/compound.js +491 -0
- package/dist/tools/compound.js.map +1 -0
- package/dist/tools/extraction.d.ts +26 -0
- package/dist/tools/extraction.js +414 -0
- package/dist/tools/extraction.js.map +1 -0
- package/dist/tools/frames.d.ts +22 -0
- package/dist/tools/frames.js +165 -0
- package/dist/tools/frames.js.map +1 -0
- package/dist/tools/interaction.d.ts +30 -0
- package/dist/tools/interaction.js +651 -0
- package/dist/tools/interaction.js.map +1 -0
- package/dist/tools/navigation.d.ts +41 -0
- package/dist/tools/navigation.js +316 -0
- package/dist/tools/navigation.js.map +1 -0
- package/dist/tools/network.d.ts +27 -0
- package/dist/tools/network.js +721 -0
- package/dist/tools/network.js.map +1 -0
- package/dist/tools/performance.d.ts +16 -0
- package/dist/tools/performance.js +240 -0
- package/dist/tools/performance.js.map +1 -0
- package/dist/tools/permissions.d.ts +25 -0
- package/dist/tools/permissions.js +308 -0
- package/dist/tools/permissions.js.map +1 -0
- package/dist/tools/service-workers.d.ts +15 -0
- package/dist/tools/service-workers.js +136 -0
- package/dist/tools/service-workers.js.map +1 -0
- package/dist/tools/shadow.d.ts +21 -0
- package/dist/tools/shadow.js +126 -0
- package/dist/tools/shadow.js.map +1 -0
- package/dist/tools/storage.d.ts +30 -0
- package/dist/tools/storage.js +679 -0
- package/dist/tools/storage.js.map +1 -0
- package/dist/tools/structured-extraction.d.ts +22 -0
- package/dist/tools/structured-extraction.js +433 -0
- package/dist/tools/structured-extraction.js.map +1 -0
- package/dist/tools/wait.d.ts +18 -0
- package/dist/tools/wait.js +182 -0
- package/dist/tools/wait.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/extension/background.js +294 -0
- package/extension/content-isolated.js +80 -0
- package/extension/content-main.js +310 -0
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/icon-96.png +0 -0
- package/extension/manifest.json +39 -0
- package/hooks/session-end.sh +67 -0
- package/hooks/session-start.sh +66 -0
- package/package.json +46 -0
- package/scripts/build-extension.sh +135 -0
- package/scripts/postinstall.sh +91 -0
- package/scripts/preuninstall.sh +25 -0
- package/scripts/update-daemon.sh +62 -0
- package/skills/safari-pilot/SKILL.md +157 -0
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
export class InteractionTools {
|
|
2
|
+
engine;
|
|
3
|
+
handlers = new Map();
|
|
4
|
+
constructor(engine) {
|
|
5
|
+
this.engine = engine;
|
|
6
|
+
this.registerHandlers();
|
|
7
|
+
}
|
|
8
|
+
registerHandlers() {
|
|
9
|
+
this.handlers.set('safari_click', this.handleClick.bind(this));
|
|
10
|
+
this.handlers.set('safari_double_click', this.handleDoubleClick.bind(this));
|
|
11
|
+
this.handlers.set('safari_fill', this.handleFill.bind(this));
|
|
12
|
+
this.handlers.set('safari_select_option', this.handleSelectOption.bind(this));
|
|
13
|
+
this.handlers.set('safari_check', this.handleCheck.bind(this));
|
|
14
|
+
this.handlers.set('safari_hover', this.handleHover.bind(this));
|
|
15
|
+
this.handlers.set('safari_type', this.handleType.bind(this));
|
|
16
|
+
this.handlers.set('safari_press_key', this.handlePressKey.bind(this));
|
|
17
|
+
this.handlers.set('safari_scroll', this.handleScroll.bind(this));
|
|
18
|
+
this.handlers.set('safari_drag', this.handleDrag.bind(this));
|
|
19
|
+
this.handlers.set('safari_handle_dialog', this.handleHandleDialog.bind(this));
|
|
20
|
+
}
|
|
21
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
22
|
+
getDefinitions() {
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
name: 'safari_click',
|
|
26
|
+
description: 'Click an element identified by CSS selector. Dispatches full click event sequence (mousedown, mouseup, click).',
|
|
27
|
+
inputSchema: {
|
|
28
|
+
type: 'object',
|
|
29
|
+
properties: {
|
|
30
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
31
|
+
selector: { type: 'string', description: 'CSS selector for the element to click' },
|
|
32
|
+
shadowSelector: { type: 'string', description: 'Selector within Shadow DOM (extension-only)' },
|
|
33
|
+
button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' },
|
|
34
|
+
modifiers: { type: 'array', items: { type: 'string', enum: ['ctrl', 'shift', 'alt', 'meta'] } },
|
|
35
|
+
waitForNavigation: { type: 'boolean', default: false },
|
|
36
|
+
timeout: { type: 'number', default: 5000 },
|
|
37
|
+
},
|
|
38
|
+
required: ['tabUrl', 'selector'],
|
|
39
|
+
},
|
|
40
|
+
requirements: {},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'safari_double_click',
|
|
44
|
+
description: 'Double-click an element. Often used to select text or trigger edit modes.',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
49
|
+
selector: { type: 'string', description: 'CSS selector for the element to double-click' },
|
|
50
|
+
timeout: { type: 'number', default: 5000 },
|
|
51
|
+
},
|
|
52
|
+
required: ['tabUrl', 'selector'],
|
|
53
|
+
},
|
|
54
|
+
requirements: {},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: 'safari_fill',
|
|
58
|
+
description: 'Fill a form input with text. Uses framework-aware filling for React, Vue, and Web Components. ' +
|
|
59
|
+
'Clears existing value before typing.',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
64
|
+
selector: { type: 'string', description: 'CSS selector for the input element' },
|
|
65
|
+
value: { type: 'string', description: 'Text to fill' },
|
|
66
|
+
framework: {
|
|
67
|
+
type: 'string',
|
|
68
|
+
enum: ['auto', 'react', 'vue', 'vanilla'],
|
|
69
|
+
default: 'auto',
|
|
70
|
+
description: 'Framework hint for event dispatch strategy',
|
|
71
|
+
},
|
|
72
|
+
clearFirst: { type: 'boolean', default: true },
|
|
73
|
+
pressEnterAfter: { type: 'boolean', default: false },
|
|
74
|
+
timeout: { type: 'number' },
|
|
75
|
+
},
|
|
76
|
+
required: ['tabUrl', 'selector', 'value'],
|
|
77
|
+
},
|
|
78
|
+
requirements: {},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'safari_select_option',
|
|
82
|
+
description: 'Select an option from a <select> dropdown.',
|
|
83
|
+
inputSchema: {
|
|
84
|
+
type: 'object',
|
|
85
|
+
properties: {
|
|
86
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
87
|
+
selector: { type: 'string', description: 'Selector for the <select> element' },
|
|
88
|
+
value: { type: 'string', description: 'Option value attribute' },
|
|
89
|
+
label: { type: 'string', description: 'Option visible text' },
|
|
90
|
+
index: { type: 'number', description: 'Option index' },
|
|
91
|
+
},
|
|
92
|
+
required: ['tabUrl', 'selector'],
|
|
93
|
+
},
|
|
94
|
+
requirements: {},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'safari_check',
|
|
98
|
+
description: 'Check or uncheck a checkbox or radio button.',
|
|
99
|
+
inputSchema: {
|
|
100
|
+
type: 'object',
|
|
101
|
+
properties: {
|
|
102
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
103
|
+
selector: { type: 'string', description: 'CSS selector for the checkbox/radio' },
|
|
104
|
+
checked: { type: 'boolean', description: 'true to check, false to uncheck' },
|
|
105
|
+
},
|
|
106
|
+
required: ['tabUrl', 'selector', 'checked'],
|
|
107
|
+
},
|
|
108
|
+
requirements: {},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'safari_hover',
|
|
112
|
+
description: 'Hover over an element. Triggers CSS :hover states and mouseover/mouseenter events.',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
117
|
+
selector: { type: 'string', description: 'CSS selector for the element to hover' },
|
|
118
|
+
duration: { type: 'number', description: 'How long to hover in ms', default: 0 },
|
|
119
|
+
},
|
|
120
|
+
required: ['tabUrl', 'selector'],
|
|
121
|
+
},
|
|
122
|
+
requirements: {},
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: 'safari_type',
|
|
126
|
+
description: 'Type text character by character with key events. Unlike fill, dispatches individual ' +
|
|
127
|
+
'keydown/keypress/keyup events per character.',
|
|
128
|
+
inputSchema: {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
132
|
+
selector: { type: 'string', description: 'CSS selector for the target element' },
|
|
133
|
+
text: { type: 'string', description: 'Text to type' },
|
|
134
|
+
delay: { type: 'number', description: 'Delay between keystrokes in ms', default: 50 },
|
|
135
|
+
},
|
|
136
|
+
required: ['tabUrl', 'selector', 'text'],
|
|
137
|
+
},
|
|
138
|
+
requirements: {},
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'safari_press_key',
|
|
142
|
+
description: 'Press a keyboard key or key combination. Works globally (not targeted to an element) ' +
|
|
143
|
+
'unless selector is provided.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
148
|
+
key: { type: 'string', description: 'Key name: Enter, Tab, Escape, ArrowDown, a, etc.' },
|
|
149
|
+
modifiers: {
|
|
150
|
+
type: 'array',
|
|
151
|
+
items: { type: 'string', enum: ['ctrl', 'shift', 'alt', 'meta'] },
|
|
152
|
+
},
|
|
153
|
+
selector: { type: 'string', description: 'Focus this element before pressing' },
|
|
154
|
+
},
|
|
155
|
+
required: ['tabUrl', 'key'],
|
|
156
|
+
},
|
|
157
|
+
requirements: {},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: 'safari_scroll',
|
|
161
|
+
description: 'Scroll the page or a specific element.',
|
|
162
|
+
inputSchema: {
|
|
163
|
+
type: 'object',
|
|
164
|
+
properties: {
|
|
165
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
166
|
+
selector: { type: 'string', description: 'Element to scroll. If omitted, scrolls the page.' },
|
|
167
|
+
direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] },
|
|
168
|
+
amount: { type: 'number', description: 'Pixels to scroll', default: 500 },
|
|
169
|
+
toTop: { type: 'boolean' },
|
|
170
|
+
toBottom: { type: 'boolean' },
|
|
171
|
+
toElement: { type: 'string', description: 'Scroll until this selector is visible' },
|
|
172
|
+
},
|
|
173
|
+
required: ['tabUrl'],
|
|
174
|
+
},
|
|
175
|
+
requirements: {},
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: 'safari_drag',
|
|
179
|
+
description: 'Drag an element from one position to another.',
|
|
180
|
+
inputSchema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
properties: {
|
|
183
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
184
|
+
sourceSelector: { type: 'string', description: 'CSS selector for the drag source element' },
|
|
185
|
+
targetSelector: { type: 'string', description: 'CSS selector for the drag target element' },
|
|
186
|
+
steps: { type: 'number', description: 'Number of intermediate mousemove steps', default: 10 },
|
|
187
|
+
},
|
|
188
|
+
required: ['tabUrl', 'sourceSelector', 'targetSelector'],
|
|
189
|
+
},
|
|
190
|
+
requirements: {},
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'safari_handle_dialog',
|
|
194
|
+
description: 'Install a proactive dialog interceptor that automatically handles alert, confirm, and prompt dialogs. ' +
|
|
195
|
+
'MUST be called BEFORE the action that triggers the dialog — dialogs block JavaScript execution ' +
|
|
196
|
+
'so they cannot be handled reactively. Patches window.alert, window.confirm, and window.prompt. ' +
|
|
197
|
+
'Use action: "accept" to confirm/ok dialogs, "dismiss" to cancel. ' +
|
|
198
|
+
'For prompts, provide promptText to set the return value.',
|
|
199
|
+
inputSchema: {
|
|
200
|
+
type: 'object',
|
|
201
|
+
properties: {
|
|
202
|
+
tabUrl: { type: 'string', description: 'Current URL of the tab' },
|
|
203
|
+
autoHandle: {
|
|
204
|
+
type: 'boolean',
|
|
205
|
+
description: 'Must be true to install the interceptor. Set to false to restore native dialogs.',
|
|
206
|
+
},
|
|
207
|
+
action: {
|
|
208
|
+
type: 'string',
|
|
209
|
+
enum: ['accept', 'dismiss'],
|
|
210
|
+
description: 'How to handle dialogs: accept (ok/confirm) or dismiss (cancel)',
|
|
211
|
+
},
|
|
212
|
+
promptText: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Text to return from prompt() dialogs when action is accept',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
required: ['tabUrl', 'autoHandle', 'action'],
|
|
218
|
+
},
|
|
219
|
+
requirements: { requiresDialogIntercept: true },
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
}
|
|
223
|
+
getHandler(name) {
|
|
224
|
+
return this.handlers.get(name);
|
|
225
|
+
}
|
|
226
|
+
// ── Handlers ────────────────────────────────────────────────────────────────
|
|
227
|
+
async handleClick(params) {
|
|
228
|
+
const start = Date.now();
|
|
229
|
+
const tabUrl = params['tabUrl'];
|
|
230
|
+
const selector = params['selector'];
|
|
231
|
+
const timeout = typeof params['timeout'] === 'number' ? params['timeout'] : 5000;
|
|
232
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
233
|
+
const js = `
|
|
234
|
+
var el = document.querySelector('${escapedSelector}');
|
|
235
|
+
if (!el) throw Object.assign(new Error('Element not found: ${escapedSelector}'), { name: 'ELEMENT_NOT_FOUND' });
|
|
236
|
+
if (el.offsetParent === null && getComputedStyle(el).display === 'none') throw Object.assign(new Error('Element not visible'), { name: 'ELEMENT_NOT_VISIBLE' });
|
|
237
|
+
|
|
238
|
+
var rect = el.getBoundingClientRect();
|
|
239
|
+
var opts = { bubbles: true, cancelable: true, view: window, clientX: rect.x + rect.width/2, clientY: rect.y + rect.height/2 };
|
|
240
|
+
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
241
|
+
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
242
|
+
el.dispatchEvent(new MouseEvent('click', opts));
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
clicked: true,
|
|
246
|
+
element: {
|
|
247
|
+
tagName: el.tagName,
|
|
248
|
+
id: el.id || undefined,
|
|
249
|
+
textContent: (el.textContent || '').slice(0, 100),
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
`;
|
|
253
|
+
const result = await this.engine.executeJsInTab(tabUrl, js, timeout);
|
|
254
|
+
if (!result.ok)
|
|
255
|
+
throw new Error(result.error?.message ?? 'Click failed');
|
|
256
|
+
const data = result.value ? JSON.parse(result.value) : { clicked: true };
|
|
257
|
+
return this.makeResponse(data, Date.now() - start);
|
|
258
|
+
}
|
|
259
|
+
async handleDoubleClick(params) {
|
|
260
|
+
const start = Date.now();
|
|
261
|
+
const tabUrl = params['tabUrl'];
|
|
262
|
+
const selector = params['selector'];
|
|
263
|
+
const timeout = typeof params['timeout'] === 'number' ? params['timeout'] : 5000;
|
|
264
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
265
|
+
const js = `
|
|
266
|
+
var el = document.querySelector('${escapedSelector}');
|
|
267
|
+
if (!el) throw Object.assign(new Error('Element not found: ${escapedSelector}'), { name: 'ELEMENT_NOT_FOUND' });
|
|
268
|
+
|
|
269
|
+
var rect = el.getBoundingClientRect();
|
|
270
|
+
var opts = { bubbles: true, cancelable: true, view: window, clientX: rect.x + rect.width/2, clientY: rect.y + rect.height/2 };
|
|
271
|
+
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
272
|
+
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
273
|
+
el.dispatchEvent(new MouseEvent('click', opts));
|
|
274
|
+
el.dispatchEvent(new MouseEvent('mousedown', opts));
|
|
275
|
+
el.dispatchEvent(new MouseEvent('mouseup', opts));
|
|
276
|
+
el.dispatchEvent(new MouseEvent('click', opts));
|
|
277
|
+
el.dispatchEvent(new MouseEvent('dblclick', opts));
|
|
278
|
+
|
|
279
|
+
var selection = window.getSelection();
|
|
280
|
+
return {
|
|
281
|
+
clicked: true,
|
|
282
|
+
element: { tagName: el.tagName, id: el.id || undefined, textContent: (el.textContent || '').slice(0, 100) },
|
|
283
|
+
selectedText: selection ? selection.toString() : undefined,
|
|
284
|
+
};
|
|
285
|
+
`;
|
|
286
|
+
const result = await this.engine.executeJsInTab(tabUrl, js, timeout);
|
|
287
|
+
if (!result.ok)
|
|
288
|
+
throw new Error(result.error?.message ?? 'Double click failed');
|
|
289
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { clicked: true }, Date.now() - start);
|
|
290
|
+
}
|
|
291
|
+
async handleFill(params) {
|
|
292
|
+
const start = Date.now();
|
|
293
|
+
const tabUrl = params['tabUrl'];
|
|
294
|
+
const selector = params['selector'];
|
|
295
|
+
const value = params['value'];
|
|
296
|
+
const framework = params['framework'] ?? 'auto';
|
|
297
|
+
const clearFirst = params['clearFirst'] !== false;
|
|
298
|
+
const pressEnterAfter = params['pressEnterAfter'] === true;
|
|
299
|
+
const timeout = typeof params['timeout'] === 'number' ? params['timeout'] : 10000;
|
|
300
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
301
|
+
const escapedValue = value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
302
|
+
const js = `
|
|
303
|
+
var el = document.querySelector('${escapedSelector}');
|
|
304
|
+
if (!el) throw Object.assign(new Error('Element not found: ${escapedSelector}'), { name: 'ELEMENT_NOT_FOUND' });
|
|
305
|
+
|
|
306
|
+
var detectedFramework = 'vanilla';
|
|
307
|
+
if (Object.keys(el).some(function(k) { return k.startsWith('__reactFiber$'); })) {
|
|
308
|
+
detectedFramework = 'react';
|
|
309
|
+
} else if (el.__vue__ || el.__vueParentComponent) {
|
|
310
|
+
detectedFramework = 'vue';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
var fw = '${framework}' === 'auto' ? detectedFramework : '${framework}';
|
|
314
|
+
|
|
315
|
+
if (${clearFirst}) {
|
|
316
|
+
el.focus();
|
|
317
|
+
el.value = '';
|
|
318
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (fw === 'react') {
|
|
322
|
+
var nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')
|
|
323
|
+
? Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set
|
|
324
|
+
: Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')
|
|
325
|
+
? Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value').set
|
|
326
|
+
: null;
|
|
327
|
+
if (nativeSetter) {
|
|
328
|
+
nativeSetter.call(el, '${escapedValue}');
|
|
329
|
+
} else {
|
|
330
|
+
el.value = '${escapedValue}';
|
|
331
|
+
}
|
|
332
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
333
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
334
|
+
el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
|
|
335
|
+
} else if (fw === 'vue') {
|
|
336
|
+
el.value = '${escapedValue}';
|
|
337
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
338
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
339
|
+
} else {
|
|
340
|
+
el.focus();
|
|
341
|
+
el.value = '${escapedValue}';
|
|
342
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
343
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
344
|
+
el.dispatchEvent(new FocusEvent('blur', { bubbles: true }));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
${pressEnterAfter ? 'el.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", code: "Enter", bubbles: true }));' : ''}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
filled: true,
|
|
351
|
+
element: { tagName: el.tagName, id: el.id || undefined, name: el.name || undefined, type: el.type || undefined },
|
|
352
|
+
framework: fw,
|
|
353
|
+
verifiedValue: el.value,
|
|
354
|
+
};
|
|
355
|
+
`;
|
|
356
|
+
const result = await this.engine.executeJsInTab(tabUrl, js, timeout);
|
|
357
|
+
if (!result.ok)
|
|
358
|
+
throw new Error(result.error?.message ?? 'Fill failed');
|
|
359
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { filled: true }, Date.now() - start);
|
|
360
|
+
}
|
|
361
|
+
async handleSelectOption(params) {
|
|
362
|
+
const start = Date.now();
|
|
363
|
+
const tabUrl = params['tabUrl'];
|
|
364
|
+
const selector = params['selector'];
|
|
365
|
+
const value = params['value'];
|
|
366
|
+
const label = params['label'];
|
|
367
|
+
const index = params['index'];
|
|
368
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
369
|
+
const js = `
|
|
370
|
+
var el = document.querySelector('${escapedSelector}');
|
|
371
|
+
if (!el) throw Object.assign(new Error('Element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
372
|
+
if (el.tagName !== 'SELECT') throw new Error('Element is not a <select>');
|
|
373
|
+
|
|
374
|
+
var option;
|
|
375
|
+
${value !== undefined ? `option = Array.from(el.options).find(function(o) { return o.value === '${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'; });` : ''}
|
|
376
|
+
${label !== undefined ? `if (!option) option = Array.from(el.options).find(function(o) { return o.textContent.trim() === '${label.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'; });` : ''}
|
|
377
|
+
${index !== undefined ? `if (!option) option = el.options[${index}];` : ''}
|
|
378
|
+
if (!option) throw Object.assign(new Error('Option not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
379
|
+
|
|
380
|
+
el.value = option.value;
|
|
381
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
382
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
383
|
+
|
|
384
|
+
return {
|
|
385
|
+
selected: true,
|
|
386
|
+
option: { value: option.value, label: option.textContent.trim(), index: option.index },
|
|
387
|
+
};
|
|
388
|
+
`;
|
|
389
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
390
|
+
if (!result.ok)
|
|
391
|
+
throw new Error(result.error?.message ?? 'Select failed');
|
|
392
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { selected: true }, Date.now() - start);
|
|
393
|
+
}
|
|
394
|
+
async handleCheck(params) {
|
|
395
|
+
const start = Date.now();
|
|
396
|
+
const tabUrl = params['tabUrl'];
|
|
397
|
+
const selector = params['selector'];
|
|
398
|
+
const checked = params['checked'];
|
|
399
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
400
|
+
const js = `
|
|
401
|
+
var el = document.querySelector('${escapedSelector}');
|
|
402
|
+
if (!el) throw Object.assign(new Error('Element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
403
|
+
|
|
404
|
+
if (el.checked !== ${checked}) {
|
|
405
|
+
el.click();
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
toggled: true,
|
|
410
|
+
element: { tagName: el.tagName, type: el.type, name: el.name || undefined },
|
|
411
|
+
checked: el.checked,
|
|
412
|
+
};
|
|
413
|
+
`;
|
|
414
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
415
|
+
if (!result.ok)
|
|
416
|
+
throw new Error(result.error?.message ?? 'Check failed');
|
|
417
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { toggled: true }, Date.now() - start);
|
|
418
|
+
}
|
|
419
|
+
async handleHover(params) {
|
|
420
|
+
const start = Date.now();
|
|
421
|
+
const tabUrl = params['tabUrl'];
|
|
422
|
+
const selector = params['selector'];
|
|
423
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
424
|
+
const js = `
|
|
425
|
+
var el = document.querySelector('${escapedSelector}');
|
|
426
|
+
if (!el) throw Object.assign(new Error('Element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
427
|
+
|
|
428
|
+
var rect = el.getBoundingClientRect();
|
|
429
|
+
var opts = { bubbles: true, cancelable: true, view: window, clientX: rect.x + rect.width/2, clientY: rect.y + rect.height/2 };
|
|
430
|
+
el.dispatchEvent(new MouseEvent('mouseenter', opts));
|
|
431
|
+
el.dispatchEvent(new MouseEvent('mouseover', opts));
|
|
432
|
+
el.dispatchEvent(new MouseEvent('mousemove', opts));
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
hovered: true,
|
|
436
|
+
element: { tagName: el.tagName, id: el.id || undefined, textContent: (el.textContent || '').slice(0, 100) },
|
|
437
|
+
};
|
|
438
|
+
`;
|
|
439
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
440
|
+
if (!result.ok)
|
|
441
|
+
throw new Error(result.error?.message ?? 'Hover failed');
|
|
442
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { hovered: true }, Date.now() - start);
|
|
443
|
+
}
|
|
444
|
+
async handleType(params) {
|
|
445
|
+
const start = Date.now();
|
|
446
|
+
const tabUrl = params['tabUrl'];
|
|
447
|
+
const selector = params['selector'];
|
|
448
|
+
const text = params['text'];
|
|
449
|
+
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
450
|
+
const escapedText = text.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
451
|
+
const js = `
|
|
452
|
+
var el = document.querySelector('${escapedSelector}');
|
|
453
|
+
if (!el) throw Object.assign(new Error('Element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
454
|
+
el.focus();
|
|
455
|
+
|
|
456
|
+
var text = '${escapedText}';
|
|
457
|
+
for (var i = 0; i < text.length; i++) {
|
|
458
|
+
var char = text[i];
|
|
459
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: char, code: 'Key' + char.toUpperCase(), bubbles: true }));
|
|
460
|
+
el.dispatchEvent(new KeyboardEvent('keypress', { key: char, code: 'Key' + char.toUpperCase(), bubbles: true }));
|
|
461
|
+
el.value += char;
|
|
462
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
463
|
+
el.dispatchEvent(new KeyboardEvent('keyup', { key: char, code: 'Key' + char.toUpperCase(), bubbles: true }));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { typed: true, length: text.length };
|
|
467
|
+
`;
|
|
468
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
469
|
+
if (!result.ok)
|
|
470
|
+
throw new Error(result.error?.message ?? 'Type failed');
|
|
471
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { typed: true }, Date.now() - start);
|
|
472
|
+
}
|
|
473
|
+
async handlePressKey(params) {
|
|
474
|
+
const start = Date.now();
|
|
475
|
+
const tabUrl = params['tabUrl'];
|
|
476
|
+
const key = params['key'];
|
|
477
|
+
const modifiers = params['modifiers'] ?? [];
|
|
478
|
+
const selector = params['selector'];
|
|
479
|
+
const escapedKey = key.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
480
|
+
const escapedSelector = selector ? selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'") : '';
|
|
481
|
+
const js = `
|
|
482
|
+
${selector ? `var el = document.querySelector('${escapedSelector}'); if (el) el.focus();` : ''}
|
|
483
|
+
var target = ${selector ? `(el || document.activeElement || document.body)` : 'document.activeElement || document.body'};
|
|
484
|
+
var opts = {
|
|
485
|
+
key: '${escapedKey}',
|
|
486
|
+
code: '${escapedKey}'.length === 1 ? 'Key' + '${escapedKey}'.toUpperCase() : '${escapedKey}',
|
|
487
|
+
bubbles: true,
|
|
488
|
+
cancelable: true,
|
|
489
|
+
ctrlKey: ${modifiers.includes('ctrl')},
|
|
490
|
+
shiftKey: ${modifiers.includes('shift')},
|
|
491
|
+
altKey: ${modifiers.includes('alt')},
|
|
492
|
+
metaKey: ${modifiers.includes('meta')},
|
|
493
|
+
};
|
|
494
|
+
target.dispatchEvent(new KeyboardEvent('keydown', opts));
|
|
495
|
+
target.dispatchEvent(new KeyboardEvent('keypress', opts));
|
|
496
|
+
target.dispatchEvent(new KeyboardEvent('keyup', opts));
|
|
497
|
+
|
|
498
|
+
return { pressed: true, key: '${escapedKey}', modifiers: [${modifiers.map(m => `'${m}'`).join(',')}] };
|
|
499
|
+
`;
|
|
500
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
501
|
+
if (!result.ok)
|
|
502
|
+
throw new Error(result.error?.message ?? 'Press key failed');
|
|
503
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { pressed: true }, Date.now() - start);
|
|
504
|
+
}
|
|
505
|
+
async handleScroll(params) {
|
|
506
|
+
const start = Date.now();
|
|
507
|
+
const tabUrl = params['tabUrl'];
|
|
508
|
+
const selector = params['selector'];
|
|
509
|
+
const direction = params['direction'] ?? 'down';
|
|
510
|
+
const amount = typeof params['amount'] === 'number' ? params['amount'] : 500;
|
|
511
|
+
const toTop = params['toTop'] === true;
|
|
512
|
+
const toBottom = params['toBottom'] === true;
|
|
513
|
+
const toElement = params['toElement'];
|
|
514
|
+
const escapedSelector = selector ? selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'") : '';
|
|
515
|
+
const escapedToElement = toElement ? toElement.replace(/\\/g, '\\\\').replace(/'/g, "\\'") : '';
|
|
516
|
+
const js = `
|
|
517
|
+
var target = ${selector ? `document.querySelector('${escapedSelector}')` : 'document.documentElement'};
|
|
518
|
+
if (!target) throw Object.assign(new Error('Scroll target not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
519
|
+
|
|
520
|
+
${toTop ? 'target.scrollTo({ top: 0, behavior: "smooth" });' : ''}
|
|
521
|
+
${toBottom ? 'target.scrollTo({ top: target.scrollHeight, behavior: "smooth" });' : ''}
|
|
522
|
+
${toElement ? `var scrollTarget = document.querySelector('${escapedToElement}'); if (scrollTarget) scrollTarget.scrollIntoView({ behavior: 'smooth' });` : ''}
|
|
523
|
+
${!toTop && !toBottom && !toElement ? `
|
|
524
|
+
var amt = ${amount};
|
|
525
|
+
var dir = '${direction}';
|
|
526
|
+
if (dir === 'down') target.scrollBy({ top: amt, behavior: 'smooth' });
|
|
527
|
+
else if (dir === 'up') target.scrollBy({ top: -amt, behavior: 'smooth' });
|
|
528
|
+
else if (dir === 'right') target.scrollBy({ left: amt, behavior: 'smooth' });
|
|
529
|
+
else if (dir === 'left') target.scrollBy({ left: -amt, behavior: 'smooth' });
|
|
530
|
+
` : ''}
|
|
531
|
+
|
|
532
|
+
return {
|
|
533
|
+
scrolled: true,
|
|
534
|
+
scrollPosition: { x: target.scrollLeft || window.scrollX, y: target.scrollTop || window.scrollY },
|
|
535
|
+
atTop: (target.scrollTop || window.scrollY) === 0,
|
|
536
|
+
atBottom: (target.scrollTop || window.scrollY) + (target.clientHeight || window.innerHeight) >= (target.scrollHeight - 1),
|
|
537
|
+
};
|
|
538
|
+
`;
|
|
539
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
540
|
+
if (!result.ok)
|
|
541
|
+
throw new Error(result.error?.message ?? 'Scroll failed');
|
|
542
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { scrolled: true }, Date.now() - start);
|
|
543
|
+
}
|
|
544
|
+
async handleDrag(params) {
|
|
545
|
+
const start = Date.now();
|
|
546
|
+
const tabUrl = params['tabUrl'];
|
|
547
|
+
const sourceSelector = params['sourceSelector'];
|
|
548
|
+
const targetSelector = params['targetSelector'];
|
|
549
|
+
const steps = typeof params['steps'] === 'number' ? params['steps'] : 10;
|
|
550
|
+
const escapedSource = sourceSelector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
551
|
+
const escapedTarget = targetSelector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
552
|
+
const js = `
|
|
553
|
+
var source = document.querySelector('${escapedSource}');
|
|
554
|
+
var target = document.querySelector('${escapedTarget}');
|
|
555
|
+
if (!source) throw Object.assign(new Error('Source element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
556
|
+
if (!target) throw Object.assign(new Error('Target element not found'), { name: 'ELEMENT_NOT_FOUND' });
|
|
557
|
+
|
|
558
|
+
var srcRect = source.getBoundingClientRect();
|
|
559
|
+
var tgtRect = target.getBoundingClientRect();
|
|
560
|
+
var startX = srcRect.x + srcRect.width/2, startY = srcRect.y + srcRect.height/2;
|
|
561
|
+
var endX = tgtRect.x + tgtRect.width/2, endY = tgtRect.y + tgtRect.height/2;
|
|
562
|
+
var numSteps = ${steps};
|
|
563
|
+
|
|
564
|
+
source.dispatchEvent(new MouseEvent('mousedown', { clientX: startX, clientY: startY, bubbles: true }));
|
|
565
|
+
|
|
566
|
+
for (var i = 1; i <= numSteps; i++) {
|
|
567
|
+
var x = startX + (endX - startX) * i / numSteps;
|
|
568
|
+
var y = startY + (endY - startY) * i / numSteps;
|
|
569
|
+
var moveTarget = document.elementFromPoint(x, y);
|
|
570
|
+
if (moveTarget) moveTarget.dispatchEvent(new MouseEvent('mousemove', { clientX: x, clientY: y, bubbles: true }));
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
target.dispatchEvent(new MouseEvent('mouseup', { clientX: endX, clientY: endY, bubbles: true }));
|
|
574
|
+
|
|
575
|
+
var dt = new DataTransfer();
|
|
576
|
+
source.dispatchEvent(new DragEvent('dragstart', { dataTransfer: dt, bubbles: true }));
|
|
577
|
+
target.dispatchEvent(new DragEvent('dragover', { dataTransfer: dt, bubbles: true }));
|
|
578
|
+
target.dispatchEvent(new DragEvent('drop', { dataTransfer: dt, bubbles: true }));
|
|
579
|
+
source.dispatchEvent(new DragEvent('dragend', { dataTransfer: dt, bubbles: true }));
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
dragged: true,
|
|
583
|
+
source: { tagName: source.tagName, id: source.id || undefined },
|
|
584
|
+
target: { tagName: target.tagName, id: target.id || undefined },
|
|
585
|
+
};
|
|
586
|
+
`;
|
|
587
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
588
|
+
if (!result.ok)
|
|
589
|
+
throw new Error(result.error?.message ?? 'Drag failed');
|
|
590
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { dragged: true }, Date.now() - start);
|
|
591
|
+
}
|
|
592
|
+
async handleHandleDialog(params) {
|
|
593
|
+
const start = Date.now();
|
|
594
|
+
const tabUrl = params['tabUrl'];
|
|
595
|
+
const autoHandle = params['autoHandle'] === true;
|
|
596
|
+
const action = params['action'] ?? 'accept';
|
|
597
|
+
const promptText = params['promptText'] ?? '';
|
|
598
|
+
const escapedPromptText = promptText.replace(/'/g, "\\'");
|
|
599
|
+
const js = `
|
|
600
|
+
var autoHandle = ${autoHandle};
|
|
601
|
+
var action = '${action}';
|
|
602
|
+
var promptText = '${escapedPromptText}';
|
|
603
|
+
|
|
604
|
+
if (!window.__safariPilotDialogs) {
|
|
605
|
+
window.__safariPilotDialogs = {
|
|
606
|
+
origAlert: window.alert,
|
|
607
|
+
origConfirm: window.confirm,
|
|
608
|
+
origPrompt: window.prompt,
|
|
609
|
+
intercepted: [],
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (!autoHandle) {
|
|
614
|
+
// Restore native dialogs
|
|
615
|
+
window.alert = window.__safariPilotDialogs.origAlert;
|
|
616
|
+
window.confirm = window.__safariPilotDialogs.origConfirm;
|
|
617
|
+
window.prompt = window.__safariPilotDialogs.origPrompt;
|
|
618
|
+
return { status: 'restored', intercepted: window.__safariPilotDialogs.intercepted.length };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
window.alert = function(message) {
|
|
622
|
+
window.__safariPilotDialogs.intercepted.push({ type: 'alert', message: String(message), timestamp: Date.now() });
|
|
623
|
+
// alert returns undefined — no-op
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
window.confirm = function(message) {
|
|
627
|
+
window.__safariPilotDialogs.intercepted.push({ type: 'confirm', message: String(message), action: action, timestamp: Date.now() });
|
|
628
|
+
return action === 'accept';
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
window.prompt = function(message, defaultValue) {
|
|
632
|
+
window.__safariPilotDialogs.intercepted.push({ type: 'prompt', message: String(message), action: action, returnValue: action === 'accept' ? promptText : null, timestamp: Date.now() });
|
|
633
|
+
return action === 'accept' ? promptText : null;
|
|
634
|
+
};
|
|
635
|
+
|
|
636
|
+
return { status: 'installed', action: action, promptText: promptText };
|
|
637
|
+
`;
|
|
638
|
+
const result = await this.engine.executeJsInTab(tabUrl, js);
|
|
639
|
+
if (!result.ok)
|
|
640
|
+
throw new Error(result.error?.message ?? 'Handle dialog failed');
|
|
641
|
+
return this.makeResponse(result.value ? JSON.parse(result.value) : { status: 'installed' }, Date.now() - start);
|
|
642
|
+
}
|
|
643
|
+
// ── Private helpers ─────────────────────────────────────────────────────────
|
|
644
|
+
makeResponse(data, latencyMs = 0) {
|
|
645
|
+
return {
|
|
646
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
647
|
+
metadata: { engine: 'applescript', degraded: false, latencyMs },
|
|
648
|
+
};
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
//# sourceMappingURL=interaction.js.map
|