@hypothesi/tauri-mcp-server 0.7.0 → 0.8.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 +31 -24
- package/dist/driver/scripts/aria-api-loader.js +29 -0
- package/dist/driver/scripts/aria-api.bundle.js +1295 -0
- package/dist/driver/scripts/dom-snapshot.js +536 -0
- package/dist/driver/scripts/find-element.js +10 -2
- package/dist/driver/scripts/focus.js +17 -4
- package/dist/driver/scripts/get-styles.js +23 -4
- package/dist/driver/scripts/index.js +1 -0
- package/dist/driver/scripts/interact.js +20 -9
- package/dist/driver/scripts/wait-for.js +15 -2
- package/dist/driver/session-manager.js +2 -2
- package/dist/driver/webview-executor.js +5 -5
- package/dist/driver/webview-interactions.js +39 -3
- package/dist/prompts-registry.js +5 -5
- package/dist/tools-registry.js +65 -35
- package/package.json +8 -2
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DOM Snapshot - Generates structured DOM representations for AI consumption
|
|
3
|
+
*
|
|
4
|
+
* Supports two snapshot types:
|
|
5
|
+
*
|
|
6
|
+
* 'accessibility' - Uses aria-api for comprehensive, spec-compliant accessibility computation:
|
|
7
|
+
* - WAI-ARIA 1.3 role computation
|
|
8
|
+
* - HTML-AAM 1.0 implicit role mappings
|
|
9
|
+
* - Accessible Name and Description Computation 1.2
|
|
10
|
+
* - aria-owns relationship handling
|
|
11
|
+
* - Shadow DOM traversal
|
|
12
|
+
*
|
|
13
|
+
* 'structure' - DOM structure tree with:
|
|
14
|
+
* - Element tag names
|
|
15
|
+
* - Element IDs (if present)
|
|
16
|
+
* - CSS classes (if present)
|
|
17
|
+
* - data-testid attribute (if present)
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} params
|
|
20
|
+
* @param {string} params.type - Snapshot type ('accessibility' or 'structure')
|
|
21
|
+
* @param {string|null} params.selector - Optional CSS selector to scope snapshot
|
|
22
|
+
*/
|
|
23
|
+
(function(params) {
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const { type, selector } = params;
|
|
27
|
+
|
|
28
|
+
// ARIA states to include in snapshot (used by accessibility type)
|
|
29
|
+
const ARIA_STATES = [
|
|
30
|
+
'checked', 'disabled', 'expanded', 'pressed', 'selected',
|
|
31
|
+
'hidden', 'invalid', 'required', 'readonly', 'busy',
|
|
32
|
+
'current', 'grabbed', 'haspopup', 'live', 'modal',
|
|
33
|
+
'multiline', 'multiselectable', 'orientation', 'sort'
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Roles that should include value
|
|
37
|
+
const VALUE_ROLES = new Set([
|
|
38
|
+
'textbox', 'searchbox', 'spinbutton', 'slider',
|
|
39
|
+
'scrollbar', 'progressbar', 'meter', 'combobox'
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
// ========================================================================
|
|
43
|
+
// Ref ID System
|
|
44
|
+
// ========================================================================
|
|
45
|
+
|
|
46
|
+
let refCounter = 0;
|
|
47
|
+
const refMap = new Map(),
|
|
48
|
+
reverseRefMap = new Map();
|
|
49
|
+
|
|
50
|
+
function getOrCreateRef(element) {
|
|
51
|
+
if (!refMap.has(element)) {
|
|
52
|
+
const ref = 'e' + refCounter++;
|
|
53
|
+
refMap.set(element, ref);
|
|
54
|
+
reverseRefMap.set(ref, element);
|
|
55
|
+
}
|
|
56
|
+
return refMap.get(element);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
window.__MCP_ARIA_REFS__ = refMap;
|
|
60
|
+
window.__MCP_ARIA_REFS_REVERSE__ = reverseRefMap;
|
|
61
|
+
|
|
62
|
+
// ========================================================================
|
|
63
|
+
// Visibility (using aria-api for correct aria-hidden inheritance)
|
|
64
|
+
// ========================================================================
|
|
65
|
+
|
|
66
|
+
function isAccessibilityVisible(element) {
|
|
67
|
+
// Use aria-api for aria-hidden (handles inheritance)
|
|
68
|
+
try {
|
|
69
|
+
var ariaHidden = ariaApi.getAttribute(element, 'hidden');
|
|
70
|
+
if (ariaHidden === true || ariaHidden === 'true') return false;
|
|
71
|
+
} catch (e) {
|
|
72
|
+
// Ignore errors from aria-api
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (element.hidden) return false;
|
|
76
|
+
if (element.inert || element.closest('[inert]')) return false;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
const style = window.getComputedStyle(element);
|
|
80
|
+
if (style.display === 'none') return false;
|
|
81
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isPresentational(element) {
|
|
90
|
+
const role = element.getAttribute('role');
|
|
91
|
+
return role === 'presentation' || role === 'none';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ========================================================================
|
|
95
|
+
// Accessibility Properties (using aria-api)
|
|
96
|
+
// ========================================================================
|
|
97
|
+
|
|
98
|
+
function getRole(element) {
|
|
99
|
+
return ariaApi.getRole(element);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getName(element) {
|
|
103
|
+
return ariaApi.getName(element) || '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getDescription(element) {
|
|
107
|
+
return ariaApi.getDescription(element) || '';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeGetAttribute(element, attr) {
|
|
111
|
+
try {
|
|
112
|
+
return ariaApi.getAttribute(element, attr);
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function getStates(element) {
|
|
119
|
+
const states = {};
|
|
120
|
+
const role = getRole(element);
|
|
121
|
+
|
|
122
|
+
for (const attr of ARIA_STATES) {
|
|
123
|
+
var value = safeGetAttribute(element, attr);
|
|
124
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
125
|
+
if (value === true || value === 'true') {
|
|
126
|
+
states[attr] = true;
|
|
127
|
+
} else if (value === false || value === 'false') {
|
|
128
|
+
if (['expanded', 'pressed', 'checked', 'selected'].includes(attr)) {
|
|
129
|
+
states[attr] = false;
|
|
130
|
+
}
|
|
131
|
+
} else if (value === 'mixed') {
|
|
132
|
+
states[attr] = 'mixed';
|
|
133
|
+
} else {
|
|
134
|
+
states[attr] = value;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Heading level
|
|
140
|
+
if (role === 'heading') {
|
|
141
|
+
var ariaLevel = safeGetAttribute(element, 'level');
|
|
142
|
+
if (ariaLevel) {
|
|
143
|
+
states.level = parseInt(ariaLevel, 10);
|
|
144
|
+
} else if (/^H[1-6]$/i.test(element.tagName)) {
|
|
145
|
+
states.level = parseInt(element.tagName[1], 10);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Value for value-bearing roles
|
|
150
|
+
if (VALUE_ROLES.has(role)) {
|
|
151
|
+
var valueNow = safeGetAttribute(element, 'valuenow');
|
|
152
|
+
var valueText = safeGetAttribute(element, 'valuetext');
|
|
153
|
+
if (valueText) states.valuetext = valueText;
|
|
154
|
+
else if (valueNow !== null) states.valuenow = valueNow;
|
|
155
|
+
|
|
156
|
+
if ((role === 'textbox' || role === 'searchbox') && element.value) {
|
|
157
|
+
states.value = element.value;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return states;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ========================================================================
|
|
165
|
+
// Tree Building (using aria-api for aria-owns aware traversal)
|
|
166
|
+
// ========================================================================
|
|
167
|
+
|
|
168
|
+
// Roles to skip (filter out like Chrome DevTools does)
|
|
169
|
+
const SKIP_ROLES = new Set(['generic', 'none', 'presentation']);
|
|
170
|
+
|
|
171
|
+
// Roles that are structural landmarks (keep even without name)
|
|
172
|
+
const LANDMARK_ROLES = new Set([
|
|
173
|
+
'banner', 'main', 'contentinfo', 'navigation', 'complementary',
|
|
174
|
+
'region', 'search', 'form', 'application', 'document'
|
|
175
|
+
]);
|
|
176
|
+
|
|
177
|
+
function buildAriaTree(element, depth, maxDepth) {
|
|
178
|
+
depth = depth || 0;
|
|
179
|
+
maxDepth = maxDepth || 100;
|
|
180
|
+
|
|
181
|
+
if (depth > maxDepth) return { type: 'error', message: 'Max depth exceeded' };
|
|
182
|
+
if (!isAccessibilityVisible(element)) return null;
|
|
183
|
+
|
|
184
|
+
// Handle presentational elements
|
|
185
|
+
if (isPresentational(element)) {
|
|
186
|
+
const children = getAccessibleChildren(element, depth, maxDepth);
|
|
187
|
+
if (children.length === 0) return null;
|
|
188
|
+
if (children.length === 1) return children[0];
|
|
189
|
+
return { type: 'fragment', children: children };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const role = getRole(element);
|
|
193
|
+
const name = getName(element);
|
|
194
|
+
const description = getDescription(element);
|
|
195
|
+
const states = getStates(element);
|
|
196
|
+
const ref = getOrCreateRef(element);
|
|
197
|
+
const children = getAccessibleChildren(element, depth, maxDepth);
|
|
198
|
+
|
|
199
|
+
// Skip generic/none roles (like Chrome DevTools) - just return children
|
|
200
|
+
if (SKIP_ROLES.has(role) && !name) {
|
|
201
|
+
if (children.length === 0) return null;
|
|
202
|
+
if (children.length === 1) return children[0];
|
|
203
|
+
return { type: 'fragment', children: children };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (!role && !name && children.length === 0) return null;
|
|
207
|
+
|
|
208
|
+
// Get URL for links (like Playwright)
|
|
209
|
+
var url;
|
|
210
|
+
if (role === 'link' && element.href) {
|
|
211
|
+
url = element.href;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check cursor style for interactive elements (like Playwright's [cursor=pointer])
|
|
215
|
+
var cursor;
|
|
216
|
+
try {
|
|
217
|
+
var computedStyle = window.getComputedStyle(element);
|
|
218
|
+
if (computedStyle.cursor === 'pointer') {
|
|
219
|
+
cursor = 'pointer';
|
|
220
|
+
}
|
|
221
|
+
} catch (e) {
|
|
222
|
+
// Ignore style errors
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
type: 'element',
|
|
227
|
+
role: role || 'generic',
|
|
228
|
+
name: name || undefined,
|
|
229
|
+
description: description || undefined,
|
|
230
|
+
states: Object.keys(states).length > 0 ? states : undefined,
|
|
231
|
+
url: url,
|
|
232
|
+
cursor: cursor,
|
|
233
|
+
ref: ref,
|
|
234
|
+
children: children.length > 0 ? children : undefined
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function getAccessibleChildren(element, depth, maxDepth) {
|
|
239
|
+
const children = [];
|
|
240
|
+
// Use aria-api's getChildNodes for aria-owns support
|
|
241
|
+
const childNodes = ariaApi.getChildNodes(element);
|
|
242
|
+
|
|
243
|
+
for (const child of childNodes) {
|
|
244
|
+
if (child.nodeType === Node.ELEMENT_NODE) {
|
|
245
|
+
const childTree = buildAriaTree(child, depth + 1, maxDepth);
|
|
246
|
+
if (childTree) {
|
|
247
|
+
if (childTree.type === 'fragment') {
|
|
248
|
+
children.push.apply(children, childTree.children);
|
|
249
|
+
} else {
|
|
250
|
+
children.push(childTree);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
} else if (child.nodeType === Node.TEXT_NODE) {
|
|
254
|
+
const text = child.textContent ? child.textContent.trim() : '';
|
|
255
|
+
// Use text type like Playwright
|
|
256
|
+
if (text) children.push({ type: 'text', content: text });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return children;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ========================================================================
|
|
264
|
+
// Playwright-compatible YAML Rendering
|
|
265
|
+
// ========================================================================
|
|
266
|
+
|
|
267
|
+
function yamlEscape(str) {
|
|
268
|
+
if (!str) return '""';
|
|
269
|
+
var needsQuotes = /[\n\r\t:#{}\[\],&*?|<>=!%@`]/.test(str) ||
|
|
270
|
+
str.startsWith(' ') || str.endsWith(' ') ||
|
|
271
|
+
str.includes('"') || str.includes("'");
|
|
272
|
+
if (!needsQuotes && !/^[\d.+-]/.test(str)) return str;
|
|
273
|
+
return '"' + str.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
|
274
|
+
.replace(/\n/g, '\\n').replace(/\r/g, '\\r') + '"';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function renderYaml(node, indent) {
|
|
278
|
+
indent = indent || 0;
|
|
279
|
+
var prefix = ' '.repeat(indent);
|
|
280
|
+
if (!node) return '';
|
|
281
|
+
|
|
282
|
+
// Text nodes (like Playwright)
|
|
283
|
+
if (node.type === 'text') {
|
|
284
|
+
return prefix + '- text: ' + yamlEscape(node.content) + '\n';
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (node.type === 'fragment') {
|
|
288
|
+
return node.children.map(function(c) { return renderYaml(c, indent); }).join('');
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (node.type === 'error') {
|
|
292
|
+
return prefix + '# ERROR: ' + node.message + '\n';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Build line: - role "name" [attrs] [ref=X]
|
|
296
|
+
var line = prefix + '- ' + node.role;
|
|
297
|
+
if (node.name) line += ' ' + yamlEscape(node.name);
|
|
298
|
+
|
|
299
|
+
// Build attributes in brackets like Playwright
|
|
300
|
+
var attrs = [];
|
|
301
|
+
if (node.states) {
|
|
302
|
+
for (var key in node.states) {
|
|
303
|
+
if (Object.prototype.hasOwnProperty.call(node.states, key)) {
|
|
304
|
+
var value = node.states[key];
|
|
305
|
+
// Skip false values (cleaner output)
|
|
306
|
+
if (value === false) continue;
|
|
307
|
+
if (value === true) {
|
|
308
|
+
attrs.push(key);
|
|
309
|
+
} else if (typeof value === 'number') {
|
|
310
|
+
attrs.push(key + '=' + value);
|
|
311
|
+
} else {
|
|
312
|
+
attrs.push(key + '=' + yamlEscape(String(value)));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
attrs.push('ref=' + node.ref);
|
|
318
|
+
|
|
319
|
+
// Add cursor style like Playwright
|
|
320
|
+
if (node.cursor) {
|
|
321
|
+
attrs.push('cursor=' + node.cursor);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (attrs.length > 0) {
|
|
325
|
+
line += ' [' + attrs.join('] [') + ']';
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Check if we have children or URL/description to add
|
|
329
|
+
var hasChildren = node.children && node.children.length > 0;
|
|
330
|
+
var hasUrl = !!node.url;
|
|
331
|
+
var hasDescription = !!node.description;
|
|
332
|
+
|
|
333
|
+
if (hasChildren || hasUrl || hasDescription) {
|
|
334
|
+
line += ':\n';
|
|
335
|
+
// Add URL as child like Playwright
|
|
336
|
+
if (hasUrl) {
|
|
337
|
+
line += prefix + ' - /url: ' + node.url + '\n';
|
|
338
|
+
}
|
|
339
|
+
// Add description as child if present
|
|
340
|
+
if (hasDescription) {
|
|
341
|
+
line += prefix + ' - /description: ' + yamlEscape(node.description) + '\n';
|
|
342
|
+
}
|
|
343
|
+
// Render children
|
|
344
|
+
if (hasChildren) {
|
|
345
|
+
line += node.children.map(function(c) { return renderYaml(c, indent + 1); }).join('');
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
line += '\n';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return line;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ========================================================================
|
|
355
|
+
// Main Execution
|
|
356
|
+
// ========================================================================
|
|
357
|
+
|
|
358
|
+
if (type !== 'accessibility' && type !== 'structure') {
|
|
359
|
+
throw new Error('Unsupported snapshot type: "' + type + '". Supported: \'accessibility\', \'structure\'');
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ========================================================================
|
|
363
|
+
// Structure Snapshot (DOM structure tree)
|
|
364
|
+
// ========================================================================
|
|
365
|
+
|
|
366
|
+
function isStructureVisible(element) {
|
|
367
|
+
if (element.hidden) return false;
|
|
368
|
+
try {
|
|
369
|
+
var style = window.getComputedStyle(element);
|
|
370
|
+
if (style.display === 'none') return false;
|
|
371
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
372
|
+
} catch (e) {
|
|
373
|
+
return false;
|
|
374
|
+
}
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function buildStructureTree(element, depth, maxDepth) {
|
|
379
|
+
depth = depth || 0;
|
|
380
|
+
maxDepth = maxDepth || 100;
|
|
381
|
+
|
|
382
|
+
if (depth > maxDepth) return { type: 'error', message: 'Max depth exceeded' };
|
|
383
|
+
if (!isStructureVisible(element)) return null;
|
|
384
|
+
|
|
385
|
+
var tag = element.tagName.toLowerCase();
|
|
386
|
+
var id = element.id || undefined;
|
|
387
|
+
var classes = element.classList.length > 0
|
|
388
|
+
? Array.from(element.classList)
|
|
389
|
+
: undefined;
|
|
390
|
+
var testId = element.getAttribute('data-testid') || undefined;
|
|
391
|
+
var ref = getOrCreateRef(element);
|
|
392
|
+
|
|
393
|
+
var children = [];
|
|
394
|
+
for (var i = 0; i < element.children.length; i++) {
|
|
395
|
+
var childTree = buildStructureTree(element.children[i], depth + 1, maxDepth);
|
|
396
|
+
if (childTree) children.push(childTree);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
type: 'element',
|
|
401
|
+
tag: tag,
|
|
402
|
+
id: id,
|
|
403
|
+
classes: classes,
|
|
404
|
+
testId: testId,
|
|
405
|
+
ref: ref,
|
|
406
|
+
children: children.length > 0 ? children : undefined
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function renderStructureYaml(node, indent) {
|
|
411
|
+
indent = indent || 0;
|
|
412
|
+
var prefix = ' '.repeat(indent);
|
|
413
|
+
if (!node) return '';
|
|
414
|
+
|
|
415
|
+
if (node.type === 'error') {
|
|
416
|
+
return prefix + '# ERROR: ' + node.message + '\n';
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Build element descriptor: tag#id.class1.class2
|
|
420
|
+
var descriptor = node.tag;
|
|
421
|
+
if (node.id) descriptor += '#' + node.id;
|
|
422
|
+
if (node.classes) descriptor += '.' + node.classes.join('.');
|
|
423
|
+
|
|
424
|
+
var attrs = ['ref=' + node.ref];
|
|
425
|
+
if (node.testId) attrs.push('data-testid=' + yamlEscape(node.testId));
|
|
426
|
+
|
|
427
|
+
var line = prefix + '- ' + descriptor + ' [' + attrs.join(' ') + ']';
|
|
428
|
+
|
|
429
|
+
if (node.children && node.children.length > 0) {
|
|
430
|
+
line += ':\n' + node.children.map(function(c) {
|
|
431
|
+
return renderStructureYaml(c, indent + 1);
|
|
432
|
+
}).join('');
|
|
433
|
+
} else {
|
|
434
|
+
line += '\n';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return line;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// For structure type, we don't need aria-api
|
|
441
|
+
if (type === 'structure') {
|
|
442
|
+
var structureRoots = [];
|
|
443
|
+
var structureScopeInfo = '';
|
|
444
|
+
|
|
445
|
+
if (selector) {
|
|
446
|
+
try {
|
|
447
|
+
document.querySelector(selector);
|
|
448
|
+
} catch (e) {
|
|
449
|
+
return 'Error: Invalid CSS selector "' + selector + '": ' + e.message;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
var structureElements = document.querySelectorAll(selector);
|
|
453
|
+
if (structureElements.length === 0) {
|
|
454
|
+
return 'Error: No elements found matching selector "' + selector + '"';
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
structureRoots = Array.from(structureElements);
|
|
458
|
+
structureScopeInfo = '# Scoped to: ' + selector + '\n';
|
|
459
|
+
if (structureRoots.length > 1) structureScopeInfo += '# ' + structureRoots.length + ' elements matched\n';
|
|
460
|
+
} else {
|
|
461
|
+
structureRoots = [document.body];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
var structureOutput = structureScopeInfo;
|
|
465
|
+
|
|
466
|
+
structureRoots.forEach(function(root, index) {
|
|
467
|
+
if (structureRoots.length > 1) structureOutput += '\n# ─── Match ' + (index + 1) + ' of ' + structureRoots.length + ' ───\n';
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
var tree = buildStructureTree(root);
|
|
471
|
+
structureOutput += tree ? renderStructureYaml(tree) : '# (empty or hidden)\n';
|
|
472
|
+
} catch (e) {
|
|
473
|
+
structureOutput += '# ERROR: ' + e.message + '\n';
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
structureOutput += '\n# ───────────────────────────────────────\n';
|
|
478
|
+
structureOutput += '# Generated: ' + new Date().toISOString() + '\n';
|
|
479
|
+
structureOutput += '# Elements indexed: ' + refCounter + '\n';
|
|
480
|
+
structureOutput += '# Use [ref=eN] with other webview tools\n';
|
|
481
|
+
|
|
482
|
+
return structureOutput.trim();
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ========================================================================
|
|
486
|
+
// Accessibility Snapshot (requires aria-api)
|
|
487
|
+
// ========================================================================
|
|
488
|
+
|
|
489
|
+
// Validate aria-api is available
|
|
490
|
+
var ariaApi = window.ariaApi;
|
|
491
|
+
if (!ariaApi) {
|
|
492
|
+
throw new Error('aria-api library not loaded');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
var roots = [];
|
|
496
|
+
var scopeInfo = '';
|
|
497
|
+
|
|
498
|
+
if (selector) {
|
|
499
|
+
try {
|
|
500
|
+
document.querySelector(selector);
|
|
501
|
+
} catch (e) {
|
|
502
|
+
return 'Error: Invalid CSS selector "' + selector + '": ' + e.message;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
var elements = document.querySelectorAll(selector);
|
|
506
|
+
if (elements.length === 0) {
|
|
507
|
+
return 'Error: No elements found matching selector "' + selector + '"';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
roots = Array.from(elements);
|
|
511
|
+
scopeInfo = '# Scoped to: ' + selector + '\n';
|
|
512
|
+
if (roots.length > 1) scopeInfo += '# ' + roots.length + ' elements matched\n';
|
|
513
|
+
} else {
|
|
514
|
+
roots = [document.body];
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
var output = scopeInfo;
|
|
518
|
+
|
|
519
|
+
roots.forEach(function(root, index) {
|
|
520
|
+
if (roots.length > 1) output += '\n# ─── Match ' + (index + 1) + ' of ' + roots.length + ' ───\n';
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
var tree = buildAriaTree(root);
|
|
524
|
+
output += tree ? renderYaml(tree) : '# (empty or hidden)\n';
|
|
525
|
+
} catch (e) {
|
|
526
|
+
output += '# ERROR: ' + e.message + '\n';
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
output += '\n# ───────────────────────────────────────\n';
|
|
531
|
+
output += '# Generated: ' + new Date().toISOString() + '\n';
|
|
532
|
+
output += '# Elements indexed: ' + refCounter + '\n';
|
|
533
|
+
output += '# Use [ref=eN] with other webview tools\n';
|
|
534
|
+
|
|
535
|
+
return output.trim();
|
|
536
|
+
})
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
* Find an element using various selector strategies
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} params
|
|
5
|
-
* @param {string} params.selector - Element selector
|
|
5
|
+
* @param {string} params.selector - Element selector, ref ID (e.g., "ref=e3"), or text
|
|
6
6
|
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
7
7
|
*/
|
|
8
8
|
(function(params) {
|
|
9
9
|
const { selector, strategy } = params;
|
|
10
10
|
let element;
|
|
11
11
|
|
|
12
|
-
if (
|
|
12
|
+
// Check if it's a ref ID first (works with any strategy)
|
|
13
|
+
const refMatch = selector.match(/^(?:ref=)?(e\d+)$/);
|
|
14
|
+
if (refMatch) {
|
|
15
|
+
const refId = refMatch[1],
|
|
16
|
+
refMap = window.__MCP_ARIA_REFS_REVERSE__;
|
|
17
|
+
if (refMap) {
|
|
18
|
+
element = refMap.get(refId);
|
|
19
|
+
}
|
|
20
|
+
} else if (strategy === 'text') {
|
|
13
21
|
// Find element containing text
|
|
14
22
|
const xpath = "//*[contains(text(), '" + selector + "')]";
|
|
15
23
|
const result = document.evaluate(
|
|
@@ -2,16 +2,29 @@
|
|
|
2
2
|
* Focus an element
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} params
|
|
5
|
-
* @param {string} params.selector - CSS selector for element to focus
|
|
5
|
+
* @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element to focus
|
|
6
6
|
*/
|
|
7
7
|
(function(params) {
|
|
8
8
|
const { selector } = params;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// Resolve element from CSS selector or ref ID (e.g., "ref=e3" or "e3")
|
|
11
|
+
function resolveElement(selectorOrRef) {
|
|
12
|
+
if (!selectorOrRef) return null;
|
|
13
|
+
var refMatch = selectorOrRef.match(/^(?:ref=)?(e\d+)$/);
|
|
14
|
+
if (refMatch) {
|
|
15
|
+
var refId = refMatch[1],
|
|
16
|
+
refMap = window.__MCP_ARIA_REFS_REVERSE__;
|
|
17
|
+
if (!refMap) throw new Error('Ref "' + refId + '" not found. Run webview_dom_snapshot first to index elements.');
|
|
18
|
+
var el = refMap.get(refId);
|
|
19
|
+
if (!el) throw new Error('Ref "' + refId + '" not found. The DOM may have changed since the snapshot.');
|
|
20
|
+
return el;
|
|
21
|
+
}
|
|
22
|
+
var el = document.querySelector(selectorOrRef);
|
|
23
|
+
if (!el) throw new Error('Element not found: ' + selectorOrRef);
|
|
24
|
+
return el;
|
|
13
25
|
}
|
|
14
26
|
|
|
27
|
+
const element = resolveElement(selector);
|
|
15
28
|
element.focus();
|
|
16
29
|
return `Focused element: ${selector}`;
|
|
17
30
|
})
|
|
@@ -2,16 +2,35 @@
|
|
|
2
2
|
* Get computed CSS styles for elements
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} params
|
|
5
|
-
* @param {string} params.selector - CSS selector for element(s)
|
|
5
|
+
* @param {string} params.selector - CSS selector or ref ID (e.g., "ref=e3") for element(s)
|
|
6
6
|
* @param {string[]} params.properties - Specific CSS properties to retrieve
|
|
7
7
|
* @param {boolean} params.multiple - Whether to get styles for all matching elements
|
|
8
8
|
*/
|
|
9
9
|
(function(params) {
|
|
10
10
|
const { selector, properties, multiple } = params;
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// Resolve element from CSS selector or ref ID (e.g., "ref=e3" or "e3")
|
|
13
|
+
function resolveElement(selectorOrRef) {
|
|
14
|
+
if (!selectorOrRef) return null;
|
|
15
|
+
var refMatch = selectorOrRef.match(/^(?:ref=)?(e\d+)$/);
|
|
16
|
+
if (refMatch) {
|
|
17
|
+
var refId = refMatch[1],
|
|
18
|
+
refMap = window.__MCP_ARIA_REFS_REVERSE__;
|
|
19
|
+
if (!refMap) throw new Error('Ref "' + refId + '" not found. Run webview_dom_snapshot first to index elements.');
|
|
20
|
+
var el = refMap.get(refId);
|
|
21
|
+
if (!el) throw new Error('Ref "' + refId + '" not found. The DOM may have changed since the snapshot.');
|
|
22
|
+
return el;
|
|
23
|
+
}
|
|
24
|
+
var el = document.querySelector(selectorOrRef);
|
|
25
|
+
if (!el) throw new Error('Element not found: ' + selectorOrRef);
|
|
26
|
+
return el;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Check if selector is a ref ID - if so, multiple doesn't apply
|
|
30
|
+
const isRef = /^(?:ref=)?(e\d+)$/.test(selector);
|
|
31
|
+
const elements = isRef
|
|
32
|
+
? [resolveElement(selector)]
|
|
33
|
+
: (multiple ? Array.from(document.querySelectorAll(selector)) : [document.querySelector(selector)]);
|
|
15
34
|
|
|
16
35
|
if (!elements[0]) {
|
|
17
36
|
throw new Error(`Element not found: ${selector}`);
|