@weavetab/mcp 2.5.0-beta.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/AGENTS.md +48 -0
- package/ARCHITECTURE.md +590 -0
- package/LICENSE +631 -0
- package/README.md +909 -0
- package/SECURITY.md +196 -0
- package/TROUBLESHOOTING.md +365 -0
- package/dist/anticaptcha/detector.d.ts +49 -0
- package/dist/anticaptcha/detector.js +465 -0
- package/dist/anticaptcha/integration.d.ts +36 -0
- package/dist/anticaptcha/integration.js +123 -0
- package/dist/anticaptcha/solver.d.ts +27 -0
- package/dist/anticaptcha/solver.js +552 -0
- package/dist/audit/logger.d.ts +13 -0
- package/dist/audit/logger.js +177 -0
- package/dist/audit/telemetry.d.ts +36 -0
- package/dist/audit/telemetry.js +162 -0
- package/dist/cdp/bridge.d.ts +16 -0
- package/dist/cdp/bridge.js +826 -0
- package/dist/cdp/confirm.d.ts +26 -0
- package/dist/cdp/confirm.js +79 -0
- package/dist/cdp/connector.d.ts +37 -0
- package/dist/cdp/connector.js +661 -0
- package/dist/cdp/extension.d.ts +36 -0
- package/dist/cdp/extension.js +118 -0
- package/dist/cdp/finder.d.ts +1 -0
- package/dist/cdp/finder.js +2 -0
- package/dist/cdp/helpers.d.ts +11 -0
- package/dist/cdp/helpers.js +195 -0
- package/dist/cdp/label_cache.d.ts +12 -0
- package/dist/cdp/label_cache.js +51 -0
- package/dist/cdp/lock.d.ts +33 -0
- package/dist/cdp/lock.js +206 -0
- package/dist/cdp/omni_broker.d.ts +17 -0
- package/dist/cdp/omni_broker.js +119 -0
- package/dist/cdp/profiles.d.ts +1 -0
- package/dist/cdp/profiles.js +87 -0
- package/dist/cdp/pruner.d.ts +41 -0
- package/dist/cdp/pruner.js +263 -0
- package/dist/cdp/ref_cache.d.ts +101 -0
- package/dist/cdp/ref_cache.js +234 -0
- package/dist/cdp/retry.d.ts +5 -0
- package/dist/cdp/retry.js +39 -0
- package/dist/cdp/retry_enhanced.d.ts +44 -0
- package/dist/cdp/retry_enhanced.js +102 -0
- package/dist/cdp/security.d.ts +50 -0
- package/dist/cdp/security.js +237 -0
- package/dist/cdp/verify.d.ts +39 -0
- package/dist/cdp/verify.js +143 -0
- package/dist/cdp/walker.d.ts +91 -0
- package/dist/cdp/walker.js +896 -0
- package/dist/cdp/walker.test.d.ts +2 -0
- package/dist/cdp/walker.test.js +172 -0
- package/dist/cdp/websocket_transport.d.ts +15 -0
- package/dist/cdp/websocket_transport.js +118 -0
- package/dist/cli/plugin.d.ts +9 -0
- package/dist/cli/plugin.js +1134 -0
- package/dist/cli/updater.d.ts +1 -0
- package/dist/cli/updater.js +72 -0
- package/dist/config/loader.d.ts +120 -0
- package/dist/config/loader.js +704 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1248 -0
- package/dist/intelligence/hints.d.ts +11 -0
- package/dist/intelligence/hints.js +39 -0
- package/dist/intelligence/intent.d.ts +33 -0
- package/dist/intelligence/intent.js +171 -0
- package/dist/intelligence/sessionMemory.d.ts +66 -0
- package/dist/intelligence/sessionMemory.js +284 -0
- package/dist/intelligence/sessionMemory.test.d.ts +2 -0
- package/dist/intelligence/sessionMemory.test.js +62 -0
- package/dist/intelligence/trail.d.ts +64 -0
- package/dist/intelligence/trail.js +233 -0
- package/dist/intelligence/trail.test.d.ts +2 -0
- package/dist/intelligence/trail.test.js +110 -0
- package/dist/intelligence/vision.d.ts +24 -0
- package/dist/intelligence/vision.js +151 -0
- package/dist/lifecycle/mission_manager.d.ts +16 -0
- package/dist/lifecycle/mission_manager.js +44 -0
- package/dist/overlay/bubble.d.ts +16 -0
- package/dist/overlay/bubble.js +37 -0
- package/dist/overlay/ghost.d.ts +35 -0
- package/dist/overlay/ghost.js +508 -0
- package/dist/overlay/injector.d.ts +22 -0
- package/dist/overlay/injector.js +198 -0
- package/dist/plugin/interface.d.ts +134 -0
- package/dist/plugin/interface.js +1 -0
- package/dist/plugin/loader.d.ts +18 -0
- package/dist/plugin/loader.js +155 -0
- package/dist/plugin/registry.d.ts +50 -0
- package/dist/plugin/registry.js +234 -0
- package/dist/security/blacklist.d.ts +24 -0
- package/dist/security/blacklist.js +80 -0
- package/dist/security/filter.d.ts +2 -0
- package/dist/security/filter.js +20 -0
- package/dist/security/guard.d.ts +8 -0
- package/dist/security/guard.js +35 -0
- package/dist/security/ratelimit.d.ts +11 -0
- package/dist/security/ratelimit.js +57 -0
- package/dist/security/secrets.d.ts +75 -0
- package/dist/security/secrets.js +365 -0
- package/dist/sensors/actionability.d.ts +81 -0
- package/dist/sensors/actionability.js +303 -0
- package/dist/sensors/guard.d.ts +25 -0
- package/dist/sensors/guard.js +168 -0
- package/dist/sensors/mutations.d.ts +23 -0
- package/dist/sensors/mutations.js +93 -0
- package/dist/sensors/network.d.ts +67 -0
- package/dist/sensors/network.js +193 -0
- package/dist/sensors/operation_monitor.d.ts +80 -0
- package/dist/sensors/operation_monitor.js +203 -0
- package/dist/sensors/pacing.d.ts +44 -0
- package/dist/sensors/pacing.js +111 -0
- package/dist/sensors/thoughts.d.ts +2 -0
- package/dist/sensors/thoughts.js +59 -0
- package/dist/sensors/wait.d.ts +12 -0
- package/dist/sensors/wait.js +44 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.js +1687 -0
- package/dist/state/agent.d.ts +36 -0
- package/dist/state/agent.js +101 -0
- package/dist/state/cache.d.ts +16 -0
- package/dist/state/cache.js +92 -0
- package/dist/state/cache.test.d.ts +2 -0
- package/dist/state/cache.test.js +76 -0
- package/dist/state/delta.d.ts +71 -0
- package/dist/state/delta.js +234 -0
- package/dist/state/delta.test.d.ts +2 -0
- package/dist/state/delta.test.js +279 -0
- package/dist/state/groups.d.ts +15 -0
- package/dist/state/groups.js +80 -0
- package/dist/state/session.d.ts +30 -0
- package/dist/state/session.js +155 -0
- package/dist/tools/accessibility.d.ts +14 -0
- package/dist/tools/accessibility.js +32 -0
- package/dist/tools/ask.d.ts +14 -0
- package/dist/tools/ask.js +49 -0
- package/dist/tools/automation.d.ts +36 -0
- package/dist/tools/automation.js +218 -0
- package/dist/tools/browser_detect.d.ts +13 -0
- package/dist/tools/browser_detect.js +73 -0
- package/dist/tools/burst.d.ts +35 -0
- package/dist/tools/burst.js +344 -0
- package/dist/tools/canvas.d.ts +25 -0
- package/dist/tools/canvas.js +132 -0
- package/dist/tools/captcha.d.ts +38 -0
- package/dist/tools/captcha.js +209 -0
- package/dist/tools/click.d.ts +36 -0
- package/dist/tools/click.js +561 -0
- package/dist/tools/click_and_wait.d.ts +33 -0
- package/dist/tools/click_and_wait.js +149 -0
- package/dist/tools/clipboard.d.ts +29 -0
- package/dist/tools/clipboard.js +279 -0
- package/dist/tools/console.d.ts +21 -0
- package/dist/tools/console.js +107 -0
- package/dist/tools/cookies.d.ts +67 -0
- package/dist/tools/cookies.js +121 -0
- package/dist/tools/design_extract.d.ts +109 -0
- package/dist/tools/design_extract.js +641 -0
- package/dist/tools/dialog.d.ts +24 -0
- package/dist/tools/dialog.js +109 -0
- package/dist/tools/drag.d.ts +15 -0
- package/dist/tools/drag.js +29 -0
- package/dist/tools/emulation.d.ts +33 -0
- package/dist/tools/emulation.js +69 -0
- package/dist/tools/eval.d.ts +20 -0
- package/dist/tools/eval.js +92 -0
- package/dist/tools/fill.d.ts +41 -0
- package/dist/tools/fill.js +667 -0
- package/dist/tools/find.d.ts +30 -0
- package/dist/tools/find.js +60 -0
- package/dist/tools/geo.d.ts +19 -0
- package/dist/tools/geo.js +29 -0
- package/dist/tools/github/analyze.d.ts +4 -0
- package/dist/tools/github/analyze.js +113 -0
- package/dist/tools/github/cache.d.ts +21 -0
- package/dist/tools/github/cache.js +28 -0
- package/dist/tools/github/index.d.ts +4 -0
- package/dist/tools/github/index.js +4 -0
- package/dist/tools/github/issues.d.ts +9 -0
- package/dist/tools/github/issues.js +88 -0
- package/dist/tools/github/pr.d.ts +5 -0
- package/dist/tools/github/pr.js +88 -0
- package/dist/tools/github/rateLimiter.d.ts +4 -0
- package/dist/tools/github/rateLimiter.js +46 -0
- package/dist/tools/github/read.d.ts +34 -0
- package/dist/tools/github/read.js +74 -0
- package/dist/tools/github/repoContext.d.ts +20 -0
- package/dist/tools/github/repoContext.js +129 -0
- package/dist/tools/highlight.d.ts +20 -0
- package/dist/tools/highlight.js +140 -0
- package/dist/tools/hover.d.ts +19 -0
- package/dist/tools/hover.js +57 -0
- package/dist/tools/inspect.d.ts +17 -0
- package/dist/tools/inspect.js +125 -0
- package/dist/tools/integration.test.d.ts +2 -0
- package/dist/tools/integration.test.js +96 -0
- package/dist/tools/key.d.ts +25 -0
- package/dist/tools/key.js +173 -0
- package/dist/tools/macro_compiler.d.ts +7 -0
- package/dist/tools/macro_compiler.js +82 -0
- package/dist/tools/mouse.d.ts +21 -0
- package/dist/tools/mouse.js +62 -0
- package/dist/tools/navigate.d.ts +16 -0
- package/dist/tools/navigate.js +188 -0
- package/dist/tools/network_intercept.d.ts +17 -0
- package/dist/tools/network_intercept.js +151 -0
- package/dist/tools/office/index.d.ts +9 -0
- package/dist/tools/office/index.js +7 -0
- package/dist/tools/office/list.d.ts +15 -0
- package/dist/tools/office/list.js +41 -0
- package/dist/tools/office/parse.d.ts +20 -0
- package/dist/tools/office/parse.js +172 -0
- package/dist/tools/pdf.d.ts +22 -0
- package/dist/tools/pdf.js +42 -0
- package/dist/tools/performance.d.ts +12 -0
- package/dist/tools/performance.js +47 -0
- package/dist/tools/performance_test.d.ts +70 -0
- package/dist/tools/performance_test.js +128 -0
- package/dist/tools/plan.d.ts +39 -0
- package/dist/tools/plan.js +161 -0
- package/dist/tools/pointer.d.ts +35 -0
- package/dist/tools/pointer.js +522 -0
- package/dist/tools/read.d.ts +30 -0
- package/dist/tools/read.js +488 -0
- package/dist/tools/recording.d.ts +45 -0
- package/dist/tools/recording.js +124 -0
- package/dist/tools/response.d.ts +129 -0
- package/dist/tools/response.js +5 -0
- package/dist/tools/scrape.d.ts +24 -0
- package/dist/tools/scrape.js +140 -0
- package/dist/tools/screenshot.d.ts +40 -0
- package/dist/tools/screenshot.js +79 -0
- package/dist/tools/scroll.d.ts +23 -0
- package/dist/tools/scroll.js +196 -0
- package/dist/tools/select.d.ts +13 -0
- package/dist/tools/select.js +217 -0
- package/dist/tools/snapshot.d.ts +27 -0
- package/dist/tools/snapshot.js +99 -0
- package/dist/tools/storage.d.ts +18 -0
- package/dist/tools/storage.js +29 -0
- package/dist/tools/tabs.d.ts +77 -0
- package/dist/tools/tabs.js +342 -0
- package/dist/tools/thought.d.ts +16 -0
- package/dist/tools/thought.js +79 -0
- package/dist/tools/type.d.ts +65 -0
- package/dist/tools/type.js +1061 -0
- package/dist/tools/type_secret.d.ts +23 -0
- package/dist/tools/type_secret.js +229 -0
- package/dist/tools/upload.d.ts +19 -0
- package/dist/tools/upload.js +103 -0
- package/dist/tools/video_compiler.d.ts +24 -0
- package/dist/tools/video_compiler.js +199 -0
- package/dist/tools/viewport.d.ts +44 -0
- package/dist/tools/viewport.js +230 -0
- package/dist/tools/wait.d.ts +19 -0
- package/dist/tools/wait.js +202 -0
- package/dist/tools/wait_for.d.ts +23 -0
- package/dist/tools/wait_for.js +30 -0
- package/dist/tools/windows.d.ts +22 -0
- package/dist/tools/windows.js +57 -0
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +12 -0
- package/docs/DEV_MODE.md +68 -0
- package/docs/DOCUMENTATION.md +532 -0
- package/docs/PLUGINS.md +538 -0
- package/docs/TOOLS.md +104 -0
- package/docs/dev_plugin.md +310 -0
- package/ext-dist/background.js +467 -0
- package/ext-dist/content.js +3089 -0
- package/ext-dist/dashboard.html +583 -0
- package/ext-dist/hud/hud.js +73 -0
- package/ext-dist/hud/index.html +20 -0
- package/ext-dist/manifest.json +77 -0
- package/ext-dist/sidepanel/sidepanel.js +25 -0
- package/ext-dist/sidepanel.html +547 -0
- package/ext-dist/styles.css +218 -0
- package/icon.png +0 -0
- package/mcp.json +796 -0
- package/package.json +113 -0
- package/server.json +23 -0
- package/smithery.yaml +11 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { logAction, logDebug } from "../audit/logger.js";
|
|
2
|
+
import { recordAction, detectStuckLoop, detectCrossToolLoop, trackToolCall } from "../intelligence/trail.js";
|
|
3
|
+
import { confirmChange } from "../cdp/confirm.js";
|
|
4
|
+
import { verifyActionability } from "../sensors/actionability.js";
|
|
5
|
+
function toSelector(ref) {
|
|
6
|
+
const refId = ref.startsWith("w:") ? ref.split(":")[1] : null;
|
|
7
|
+
return refId ? `[data-wt-id="${refId}"]` : ref;
|
|
8
|
+
}
|
|
9
|
+
function esc(s) {
|
|
10
|
+
return JSON.stringify(s);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Detect the field type of an element WITHOUT mutating it.
|
|
14
|
+
* Runs in a single CDP call — returns { type, exists, fillable }.
|
|
15
|
+
*/
|
|
16
|
+
async function detectFieldType(session, selector) {
|
|
17
|
+
const { result } = await session.Runtime.evaluate({
|
|
18
|
+
expression: `(function() {
|
|
19
|
+
var el = document.querySelector(${esc(selector)});
|
|
20
|
+
if (!el) return JSON.stringify({ exists: false, fillable: false });
|
|
21
|
+
if (el.tagName === 'LABEL') {
|
|
22
|
+
var forId = el.getAttribute('for');
|
|
23
|
+
var target = forId ? document.getElementById(forId) : el.querySelector('input, textarea, select');
|
|
24
|
+
if (target) el = target;
|
|
25
|
+
} else if (!['input', 'select', 'textarea'].includes(el.tagName.toLowerCase()) && !el.isContentEditable) {
|
|
26
|
+
var inner = el.querySelector('input, textarea, select');
|
|
27
|
+
if (inner) el = inner;
|
|
28
|
+
}
|
|
29
|
+
var tag = el.tagName.toLowerCase();
|
|
30
|
+
var t = (el.getAttribute('type') || 'text').toLowerCase();
|
|
31
|
+
var role = (el.getAttribute('role') || '').toLowerCase();
|
|
32
|
+
var isCE = el.isContentEditable || el.getAttribute('contenteditable') === 'true' || el.getAttribute('contenteditable') === '';
|
|
33
|
+
var isCombobox = role === 'combobox' || role === 'textbox';
|
|
34
|
+
var type;
|
|
35
|
+
if (tag === 'select') type = 'select';
|
|
36
|
+
else if (tag === 'input' && t === 'checkbox') type = 'checkbox';
|
|
37
|
+
else if (tag === 'input' && t === 'radio') type = 'radio';
|
|
38
|
+
else if (role === 'checkbox') type = 'custom-checkbox';
|
|
39
|
+
else if (role === 'radio') type = 'custom-radio';
|
|
40
|
+
else if (role === 'switch') type = 'custom-checkbox';
|
|
41
|
+
else if (role === 'listbox' || role === 'option') type = 'select';
|
|
42
|
+
else if (tag === 'textarea' || (tag === 'input' && ['text','email','tel','url','password','search','number','time','date','datetime-local','month','week'].includes(t)) || isCE || isCombobox) type = 'text';
|
|
43
|
+
else type = 'unknown';
|
|
44
|
+
// Check fillability — disabled or readonly fields cannot be written to.
|
|
45
|
+
var ariaDisabled = el.getAttribute('aria-disabled');
|
|
46
|
+
var fillable = !el.disabled && !el.readOnly && ariaDisabled !== 'true' && ariaDisabled !== '';
|
|
47
|
+
// For checkboxes/radios, readOnly doesn't apply — but disabled does.
|
|
48
|
+
if (type === 'checkbox' || type === 'radio' || type === 'custom-checkbox' || type === 'custom-radio') {
|
|
49
|
+
fillable = !el.disabled && ariaDisabled !== 'true';
|
|
50
|
+
}
|
|
51
|
+
var isSecret = el.hasAttribute('data-wt-secret') || (el.tagName === 'INPUT' && el.type === 'password');
|
|
52
|
+
return JSON.stringify({ exists: true, type: type, fillable: fillable, isSecret: isSecret });
|
|
53
|
+
})()`,
|
|
54
|
+
returnByValue: true,
|
|
55
|
+
});
|
|
56
|
+
return JSON.parse(result?.value || '{"exists":false,"type":"unknown","fillable":false,"isSecret":false}');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Snapshot the current state of a field for before/after comparison.
|
|
60
|
+
* Captures whatever property matters for that field type.
|
|
61
|
+
*/
|
|
62
|
+
async function getFieldState(session, selector, type) {
|
|
63
|
+
const { result } = await session.Runtime.evaluate({
|
|
64
|
+
expression: `(function(type) {
|
|
65
|
+
var el = document.querySelector(${esc(selector)});
|
|
66
|
+
if (!el) return JSON.stringify({ exists: "false" });
|
|
67
|
+
if (el.tagName === 'LABEL') {
|
|
68
|
+
var forId = el.getAttribute('for');
|
|
69
|
+
var target = forId ? document.getElementById(forId) : el.querySelector('input, textarea, select');
|
|
70
|
+
if (target) el = target;
|
|
71
|
+
} else if (!['input', 'select', 'textarea'].includes(el.tagName.toLowerCase()) && !el.isContentEditable) {
|
|
72
|
+
var inner = el.querySelector('input, textarea, select');
|
|
73
|
+
if (inner) el = inner;
|
|
74
|
+
}
|
|
75
|
+
var s = { exists: "true" };
|
|
76
|
+
if (el.tagName === 'SELECT') {
|
|
77
|
+
var idx = el.selectedIndex;
|
|
78
|
+
s.selectedIndex = String(idx);
|
|
79
|
+
s.value = el.value;
|
|
80
|
+
s.text = idx >= 0 ? el.options[idx].text : "";
|
|
81
|
+
} else if (el.type === 'checkbox' || el.type === 'radio' || type === 'custom-checkbox' || type === 'custom-radio') {
|
|
82
|
+
// Native checkbox/radio uses .checked; ARIA elements use aria-checked
|
|
83
|
+
var ariaChecked = el.getAttribute('aria-checked');
|
|
84
|
+
var isChecked = el.checked !== undefined ? el.checked : (ariaChecked === 'true' || ariaChecked === 'mixed');
|
|
85
|
+
s.checked = isChecked ? "true" : "false";
|
|
86
|
+
if (ariaChecked !== null) s.ariaChecked = ariaChecked;
|
|
87
|
+
} else if (el.isContentEditable) {
|
|
88
|
+
s.text = el.textContent || "";
|
|
89
|
+
} else {
|
|
90
|
+
s.value = el.value;
|
|
91
|
+
}
|
|
92
|
+
return JSON.stringify(s);
|
|
93
|
+
})(${esc(type)})`,
|
|
94
|
+
returnByValue: true,
|
|
95
|
+
});
|
|
96
|
+
return JSON.parse(result?.value || '{"exists":"false"}');
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Apply a value to a field. Returns { ok, msg }. Mutates the DOM.
|
|
100
|
+
* Native prototype setters + event dispatch keep React/Vue state in sync.
|
|
101
|
+
*/
|
|
102
|
+
async function applyFieldValue(session, selector, value, type) {
|
|
103
|
+
const { result } = await session.Runtime.evaluate({
|
|
104
|
+
expression: `(function(selector, value, type) {
|
|
105
|
+
try {
|
|
106
|
+
var el = document.querySelector(selector);
|
|
107
|
+
if (!el) return JSON.stringify({ ok: false, msg: 'Element not found' });
|
|
108
|
+
if (el.tagName === 'LABEL') {
|
|
109
|
+
var forId = el.getAttribute('for');
|
|
110
|
+
var target = forId ? document.getElementById(forId) : el.querySelector('input, textarea, select');
|
|
111
|
+
if (target) el = target;
|
|
112
|
+
} else if (!['input', 'select', 'textarea'].includes(el.tagName.toLowerCase()) && !el.isContentEditable) {
|
|
113
|
+
var inner = el.querySelector('input, textarea, select');
|
|
114
|
+
if (inner) el = inner;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (type === 'text') {
|
|
118
|
+
el.focus();
|
|
119
|
+
// NOTE: el.click() intentionally removed — it generates bubbling click events
|
|
120
|
+
// that can activate extension UI elements (wt-minimize-btn etc.) when the
|
|
121
|
+
// field is adjacent to the WeaveTab panel. el.focus() is sufficient; the
|
|
122
|
+
// value is written via native setter + synthetic input/change events.
|
|
123
|
+
if (el.isContentEditable) {
|
|
124
|
+
// contenteditable: set via textContent and also try execCommand for rich editors
|
|
125
|
+
el.textContent = value;
|
|
126
|
+
try { document.execCommand('selectAll', false, null); document.execCommand('insertText', false, value); } catch(e2) {}
|
|
127
|
+
} else {
|
|
128
|
+
var tag = el.tagName.toLowerCase();
|
|
129
|
+
var proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
|
130
|
+
var ns = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
131
|
+
if (ns) ns.call(el, value); else el.value = value;
|
|
132
|
+
}
|
|
133
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: value, inputType: 'insertText' }));
|
|
134
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
135
|
+
return JSON.stringify({ ok: true, msg: 'filled' });
|
|
136
|
+
}
|
|
137
|
+
if (type === 'checkbox' || type === 'custom-checkbox') {
|
|
138
|
+
var shouldCheck = value === 'true' || value === 'yes' || value === 'checked' || value === '1' || value === 'on';
|
|
139
|
+
var isNative = el.tagName === 'INPUT' && el.type === 'checkbox';
|
|
140
|
+
if (isNative) {
|
|
141
|
+
var currentlyChecked = el.checked;
|
|
142
|
+
if (currentlyChecked !== shouldCheck) {
|
|
143
|
+
// Use native checked setter to bypass React's synthetic event system
|
|
144
|
+
var nativeSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked')?.set;
|
|
145
|
+
if (nativeSetter) nativeSetter.call(el, shouldCheck); else el.checked = shouldCheck;
|
|
146
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
147
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
148
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
// ARIA custom checkbox (role=checkbox/switch): toggle aria-checked and click
|
|
152
|
+
var ariaChecked = el.getAttribute('aria-checked');
|
|
153
|
+
var isCurrentlyChecked = ariaChecked === 'true';
|
|
154
|
+
if (isCurrentlyChecked !== shouldCheck) {
|
|
155
|
+
el.setAttribute('aria-checked', shouldCheck ? 'true' : 'false');
|
|
156
|
+
el.click();
|
|
157
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
158
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return JSON.stringify({ ok: true, msg: shouldCheck ? 'checked' : 'unchecked' });
|
|
162
|
+
}
|
|
163
|
+
if (type === 'radio' || type === 'custom-radio') {
|
|
164
|
+
var isNativeRadio = el.tagName === 'INPUT' && el.type === 'radio';
|
|
165
|
+
if (isNativeRadio) {
|
|
166
|
+
if (!el.checked) {
|
|
167
|
+
var nativeRadioSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked')?.set;
|
|
168
|
+
if (nativeRadioSetter) nativeRadioSetter.call(el, true); else el.checked = true;
|
|
169
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
170
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
171
|
+
}
|
|
172
|
+
} else {
|
|
173
|
+
if (el.getAttribute('aria-checked') !== 'true') {
|
|
174
|
+
el.setAttribute('aria-checked', 'true');
|
|
175
|
+
el.click();
|
|
176
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
177
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return JSON.stringify({ ok: true, msg: 'selected' });
|
|
181
|
+
}
|
|
182
|
+
if (type === 'select') {
|
|
183
|
+
var foundIdx = -1;
|
|
184
|
+
for (var j = 0; j < el.options.length; j++) {
|
|
185
|
+
var opt = el.options[j];
|
|
186
|
+
if (opt.value === value || opt.text === value || opt.text.trim() === value.trim()) {
|
|
187
|
+
foundIdx = j; break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (foundIdx === -1) return JSON.stringify({ ok: false, msg: 'Option not found: ' + value });
|
|
191
|
+
var nativeSelectSetter = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set;
|
|
192
|
+
if (nativeSelectSetter) nativeSelectSetter.call(el, el.options[foundIdx].value);
|
|
193
|
+
else el.selectedIndex = foundIdx;
|
|
194
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
195
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
196
|
+
el.options[foundIdx].dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
|
197
|
+
return JSON.stringify({ ok: true, msg: 'selected:' + el.options[foundIdx].text });
|
|
198
|
+
}
|
|
199
|
+
return JSON.stringify({ ok: false, msg: 'Unsupported field type: ' + type });
|
|
200
|
+
} catch(e) {
|
|
201
|
+
return JSON.stringify({ ok: false, msg: e.message || String(e) });
|
|
202
|
+
}
|
|
203
|
+
})(${esc(selector)}, ${esc(value)}, ${esc(type)})`,
|
|
204
|
+
returnByValue: true,
|
|
205
|
+
});
|
|
206
|
+
return JSON.parse(result?.value || '{"ok":false,"msg":"eval failed"}');
|
|
207
|
+
}
|
|
208
|
+
// ── Google Sheets grid fill helpers ──────────────────────────────────────
|
|
209
|
+
/** Parse A1 notation into { col: number (0-indexed), row: number (0-indexed) }. */
|
|
210
|
+
function parseA1(cell) {
|
|
211
|
+
const m = cell.trim().toUpperCase().match(/^([A-Z]+)(\d+)$/);
|
|
212
|
+
if (!m)
|
|
213
|
+
return null;
|
|
214
|
+
let col = 0;
|
|
215
|
+
for (const ch of m[1])
|
|
216
|
+
col = col * 26 + (ch.charCodeAt(0) - 64);
|
|
217
|
+
return { col: col - 1, row: parseInt(m[2], 10) - 1 };
|
|
218
|
+
}
|
|
219
|
+
/** Find a cell in the AX tree by A1 address. Returns { x, y } center coordinates. */
|
|
220
|
+
async function findSheetCellCoords(session, cellAddress) {
|
|
221
|
+
const parsed = parseA1(cellAddress);
|
|
222
|
+
if (!parsed)
|
|
223
|
+
return null;
|
|
224
|
+
// Enable accessibility domain
|
|
225
|
+
try {
|
|
226
|
+
await session.Accessibility.enable();
|
|
227
|
+
}
|
|
228
|
+
catch { /* already enabled */ }
|
|
229
|
+
let nodes = [];
|
|
230
|
+
try {
|
|
231
|
+
const result = await session.Accessibility.getFullAXTree();
|
|
232
|
+
nodes = result?.nodes ?? [];
|
|
233
|
+
}
|
|
234
|
+
catch (e) {
|
|
235
|
+
logDebug(`[GridFill] getFullAXTree failed: ${e}`);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
// Google Sheets exposes cells with role="gridcell" and name like "A1" or "Row 1, Column A"
|
|
239
|
+
// Prefer exact name match first, then fall back to positional match
|
|
240
|
+
const colLetter = String.fromCharCode(65 + parsed.col);
|
|
241
|
+
const rowNum = parsed.row + 1;
|
|
242
|
+
const exactLabel = `${colLetter}${rowNum}`;
|
|
243
|
+
const altLabel = `Row ${rowNum}, Column ${colLetter}`;
|
|
244
|
+
const cellNode = nodes.find((n) => {
|
|
245
|
+
if (n.ignored)
|
|
246
|
+
return false;
|
|
247
|
+
const role = (n.role?.value || '').toLowerCase();
|
|
248
|
+
if (role !== 'gridcell' && role !== 'cell')
|
|
249
|
+
return false;
|
|
250
|
+
const name = n.name?.value || '';
|
|
251
|
+
return name === exactLabel || name.startsWith(exactLabel + ' ') ||
|
|
252
|
+
name === altLabel || name.includes(exactLabel);
|
|
253
|
+
});
|
|
254
|
+
if (!cellNode?.backendDOMNodeId)
|
|
255
|
+
return null;
|
|
256
|
+
// Resolve to screen coordinates via DOM.getBoxModel
|
|
257
|
+
try {
|
|
258
|
+
const { nodeIds } = await session.DOM.pushNodesByBackendIdsToFrontend({
|
|
259
|
+
backendNodeIds: [cellNode.backendDOMNodeId],
|
|
260
|
+
});
|
|
261
|
+
if (!nodeIds?.[0])
|
|
262
|
+
return null;
|
|
263
|
+
const { model } = await session.DOM.getBoxModel({ nodeId: nodeIds[0] });
|
|
264
|
+
if (model?.content) {
|
|
265
|
+
const c = model.content;
|
|
266
|
+
return { x: Math.round((c[0] + c[4]) / 2), y: Math.round((c[1] + c[5]) / 2) };
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch (e) {
|
|
270
|
+
logDebug(`[GridFill] Box model resolution failed: ${e}`);
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
/** Type a string into a focused element using CDP Input.dispatchKeyEvent (bypasses DOM). */
|
|
275
|
+
async function cdpTypeString(session, text) {
|
|
276
|
+
for (const char of text) {
|
|
277
|
+
const code = char.charCodeAt(0);
|
|
278
|
+
const params = {
|
|
279
|
+
type: "keyDown",
|
|
280
|
+
key: char,
|
|
281
|
+
text: char,
|
|
282
|
+
unmodifiedText: char,
|
|
283
|
+
windowsVirtualKeyCode: code,
|
|
284
|
+
nativeVirtualKeyCode: code,
|
|
285
|
+
};
|
|
286
|
+
await session.Input.dispatchKeyEvent(params);
|
|
287
|
+
await session.Input.dispatchKeyEvent({ ...params, type: "char" });
|
|
288
|
+
await session.Input.dispatchKeyEvent({ ...params, type: "keyUp" });
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/** Confirm a cell write by pressing Tab to move to the next cell (commits value). */
|
|
292
|
+
async function cdpCommitCell(session) {
|
|
293
|
+
const tabParams = {
|
|
294
|
+
type: "keyDown",
|
|
295
|
+
key: "Tab",
|
|
296
|
+
code: "Tab",
|
|
297
|
+
windowsVirtualKeyCode: 9,
|
|
298
|
+
nativeVirtualKeyCode: 9,
|
|
299
|
+
};
|
|
300
|
+
await session.Input.dispatchKeyEvent(tabParams);
|
|
301
|
+
await session.Input.dispatchKeyEvent({ ...tabParams, type: "keyUp" });
|
|
302
|
+
}
|
|
303
|
+
/** Execute grid fill for Google Sheets or canvas-rendered spreadsheets. */
|
|
304
|
+
async function gridFill(session, cells) {
|
|
305
|
+
const startMs = Date.now();
|
|
306
|
+
const results = [];
|
|
307
|
+
for (const cell of cells) {
|
|
308
|
+
const coords = await findSheetCellCoords(session, cell.cell);
|
|
309
|
+
if (!coords) {
|
|
310
|
+
logAction("browser_fill", cell.cell, "FAIL — cell not found in AX tree");
|
|
311
|
+
results.push({
|
|
312
|
+
ref: cell.cell,
|
|
313
|
+
field_type: "gridcell",
|
|
314
|
+
success: false,
|
|
315
|
+
message: `Cell ${cell.cell} not found in accessibility tree — ensure the sheet is loaded and cell is visible`,
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
// Click the cell to focus it
|
|
321
|
+
await session.Input.dispatchMouseEvent({ type: "mousePressed", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
|
|
322
|
+
await session.Input.dispatchMouseEvent({ type: "mouseReleased", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
|
|
323
|
+
// Small pause to let the cell enter edit mode
|
|
324
|
+
await new Promise(r => setTimeout(r, 80));
|
|
325
|
+
// Type value via CDP key events
|
|
326
|
+
await cdpTypeString(session, cell.value);
|
|
327
|
+
// Commit with Tab
|
|
328
|
+
await cdpCommitCell(session);
|
|
329
|
+
logAction("browser_fill", cell.cell, `gridcell: OK — wrote "${cell.value}" at (${coords.x},${coords.y})`);
|
|
330
|
+
results.push({
|
|
331
|
+
ref: cell.cell,
|
|
332
|
+
field_type: "gridcell",
|
|
333
|
+
success: true,
|
|
334
|
+
message: `Written at (${coords.x},${coords.y})`,
|
|
335
|
+
value: cell.value,
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
logAction("browser_fill", cell.cell, `gridcell: FAIL — ${e.message}`);
|
|
340
|
+
results.push({
|
|
341
|
+
ref: cell.cell,
|
|
342
|
+
field_type: "gridcell",
|
|
343
|
+
success: false,
|
|
344
|
+
message: `CDP error: ${e.message}`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
const succeeded = results.filter(r => r.success).length;
|
|
349
|
+
const failed = results.filter(r => !r.success).length;
|
|
350
|
+
return {
|
|
351
|
+
success: failed === 0,
|
|
352
|
+
total_fields: cells.length,
|
|
353
|
+
succeeded,
|
|
354
|
+
failed,
|
|
355
|
+
submitted: false,
|
|
356
|
+
results,
|
|
357
|
+
execution_ms: Date.now() - startMs,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
export async function browserFill(session, options, config) {
|
|
361
|
+
// ── Cross-tool loop detection (catches click→fill→click→fill patterns) ──
|
|
362
|
+
const crossLoopCheck = detectCrossToolLoop();
|
|
363
|
+
if (crossLoopCheck.stuck) {
|
|
364
|
+
return {
|
|
365
|
+
success: false,
|
|
366
|
+
total_fields: 0,
|
|
367
|
+
succeeded: 0,
|
|
368
|
+
failed: 0,
|
|
369
|
+
submitted: false,
|
|
370
|
+
results: [],
|
|
371
|
+
execution_ms: 0,
|
|
372
|
+
suggestedNext: crossLoopCheck.diagnosis,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
// ── Single-tool stuck loop detection ──
|
|
376
|
+
const stuckCheck = detectStuckLoop();
|
|
377
|
+
if (stuckCheck.stuck) {
|
|
378
|
+
return {
|
|
379
|
+
success: false,
|
|
380
|
+
total_fields: 0,
|
|
381
|
+
succeeded: 0,
|
|
382
|
+
failed: 0,
|
|
383
|
+
submitted: false,
|
|
384
|
+
results: [],
|
|
385
|
+
execution_ms: 0,
|
|
386
|
+
suggestedNext: stuckCheck.diagnosis || "browser_fill is stuck in a loop. Call browser_map() to re-read the page and try a different approach.",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
// Track this call for loop detection
|
|
390
|
+
const fillTarget = options.fields?.map(f => f.ref).join(',') || 'grid';
|
|
391
|
+
trackToolCall('browser_fill', fillTarget, {});
|
|
392
|
+
// ── grid_data: Google Sheets / canvas spreadsheet filling ──
|
|
393
|
+
if (options.grid_data && options.grid_data.length > 0) {
|
|
394
|
+
return gridFill(session, options.grid_data);
|
|
395
|
+
}
|
|
396
|
+
const startMs = Date.now();
|
|
397
|
+
const results = [];
|
|
398
|
+
if (!options.fields || options.fields.length === 0) {
|
|
399
|
+
return { success: false, total_fields: 0, succeeded: 0, failed: 0, submitted: false, results: [], execution_ms: 0, suggestedNext: "No fields provided. Use browser_map() to get field ref IDs, then call browser_fill with fields:[{ref:'w:NN', value:'...'}]." };
|
|
400
|
+
}
|
|
401
|
+
// ── scan_only: return field schema without mutating DOM ──
|
|
402
|
+
if (options.scan_only) {
|
|
403
|
+
for (const field of options.fields) {
|
|
404
|
+
const selector = toSelector(field.ref);
|
|
405
|
+
const detected = await detectFieldType(session, selector);
|
|
406
|
+
// Report success=false for non-fillable fields (disabled, readonly, unknown
|
|
407
|
+
// type) so scan mode doesn't give false confidence before the actual fill.
|
|
408
|
+
const scanSuccess = detected.exists && detected.type !== "unknown" && detected.fillable !== false;
|
|
409
|
+
const scanMsg = !detected.exists
|
|
410
|
+
? "Element not found"
|
|
411
|
+
: detected.type === "unknown"
|
|
412
|
+
? "Unsupported field type — fill will fail"
|
|
413
|
+
: !detected.fillable
|
|
414
|
+
? `Detected: ${detected.type} (not fillable — disabled or readonly)`
|
|
415
|
+
: `Detected: ${detected.type}`;
|
|
416
|
+
results.push({
|
|
417
|
+
ref: field.ref,
|
|
418
|
+
field_type: detected.exists ? detected.type : "not_found",
|
|
419
|
+
success: scanSuccess,
|
|
420
|
+
message: scanMsg,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
success: true,
|
|
425
|
+
total_fields: options.fields.length,
|
|
426
|
+
succeeded: results.filter(r => r.success).length,
|
|
427
|
+
failed: results.filter(r => !r.success).length,
|
|
428
|
+
submitted: false,
|
|
429
|
+
results,
|
|
430
|
+
execution_ms: Date.now() - startMs,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
// ── blast_mode: bulk fill in single CDP roundtrip ──
|
|
434
|
+
if (options.blast_mode) {
|
|
435
|
+
// blast_mode: single CDP round-trip filling all fields at once.
|
|
436
|
+
// Handles text, checkbox, radio, select, contenteditable, and ARIA variants.
|
|
437
|
+
const blastExpression = `(function(fields) {
|
|
438
|
+
var results = [];
|
|
439
|
+
fields.forEach(function(f) {
|
|
440
|
+
var el = document.querySelector(f.selector);
|
|
441
|
+
if (!el) { results.push({ ref: f.ref, ok: false, msg: 'not found', type: 'unknown' }); return; }
|
|
442
|
+
var tag = el.tagName.toLowerCase();
|
|
443
|
+
var inputType = (el.getAttribute('type') || '').toLowerCase();
|
|
444
|
+
var role = (el.getAttribute('role') || '').toLowerCase();
|
|
445
|
+
try {
|
|
446
|
+
if (tag === 'select') {
|
|
447
|
+
// Select element
|
|
448
|
+
var foundIdx = -1;
|
|
449
|
+
for (var j = 0; j < el.options.length; j++) {
|
|
450
|
+
var opt = el.options[j];
|
|
451
|
+
if (opt.value === f.value || opt.text === f.value || opt.text.trim() === f.value.trim()) { foundIdx = j; break; }
|
|
452
|
+
}
|
|
453
|
+
if (foundIdx === -1) { results.push({ ref: f.ref, ok: false, msg: 'Option not found: ' + f.value, type: 'select' }); return; }
|
|
454
|
+
var nativeSS = Object.getOwnPropertyDescriptor(window.HTMLSelectElement.prototype, 'value')?.set;
|
|
455
|
+
if (nativeSS) nativeSS.call(el, el.options[foundIdx].value); else el.selectedIndex = foundIdx;
|
|
456
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
457
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
458
|
+
results.push({ ref: f.ref, ok: true, msg: 'selected:' + el.options[foundIdx].text, type: 'select' });
|
|
459
|
+
} else if (tag === 'input' && inputType === 'checkbox') {
|
|
460
|
+
// Native checkbox
|
|
461
|
+
var want = f.value === 'true' || f.value === 'yes' || f.value === 'checked' || f.value === '1' || f.value === 'on';
|
|
462
|
+
if (el.checked !== want) {
|
|
463
|
+
var ns = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked')?.set;
|
|
464
|
+
if (ns) ns.call(el, want); else el.checked = want;
|
|
465
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
466
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
467
|
+
}
|
|
468
|
+
results.push({ ref: f.ref, ok: true, msg: want ? 'checked' : 'unchecked', type: 'checkbox' });
|
|
469
|
+
} else if (tag === 'input' && inputType === 'radio') {
|
|
470
|
+
// Native radio
|
|
471
|
+
if (!el.checked) {
|
|
472
|
+
var nsr = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'checked')?.set;
|
|
473
|
+
if (nsr) nsr.call(el, true); else el.checked = true;
|
|
474
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
475
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
476
|
+
}
|
|
477
|
+
results.push({ ref: f.ref, ok: true, msg: 'selected', type: 'radio' });
|
|
478
|
+
} else if (role === 'checkbox' || role === 'switch') {
|
|
479
|
+
// ARIA custom checkbox
|
|
480
|
+
var wantAria = f.value === 'true' || f.value === 'yes' || f.value === 'checked' || f.value === '1' || f.value === 'on';
|
|
481
|
+
var curAria = el.getAttribute('aria-checked') === 'true';
|
|
482
|
+
if (curAria !== wantAria) {
|
|
483
|
+
el.setAttribute('aria-checked', wantAria ? 'true' : 'false');
|
|
484
|
+
el.click();
|
|
485
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
486
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
487
|
+
}
|
|
488
|
+
results.push({ ref: f.ref, ok: true, msg: wantAria ? 'checked' : 'unchecked', type: 'custom-checkbox' });
|
|
489
|
+
} else if (role === 'radio') {
|
|
490
|
+
// ARIA custom radio
|
|
491
|
+
if (el.getAttribute('aria-checked') !== 'true') {
|
|
492
|
+
el.setAttribute('aria-checked', 'true');
|
|
493
|
+
el.click();
|
|
494
|
+
el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
|
|
495
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
496
|
+
}
|
|
497
|
+
results.push({ ref: f.ref, ok: true, msg: 'selected', type: 'custom-radio' });
|
|
498
|
+
} else if (el.isContentEditable || el.getAttribute('contenteditable') === 'true' || el.getAttribute('contenteditable') === '') {
|
|
499
|
+
// contenteditable
|
|
500
|
+
el.focus();
|
|
501
|
+
el.textContent = f.value;
|
|
502
|
+
try { document.execCommand('selectAll', false, null); document.execCommand('insertText', false, f.value); } catch(e2) {}
|
|
503
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: f.value, inputType: 'insertText' }));
|
|
504
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
505
|
+
results.push({ ref: f.ref, ok: true, msg: 'filled', type: 'contenteditable' });
|
|
506
|
+
} else {
|
|
507
|
+
// text / textarea / other inputs
|
|
508
|
+
el.focus();
|
|
509
|
+
var proto2 = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype;
|
|
510
|
+
var ns2 = Object.getOwnPropertyDescriptor(proto2, 'value')?.set;
|
|
511
|
+
if (ns2) ns2.call(el, f.value); else el.value = f.value;
|
|
512
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: f.value, inputType: 'insertText' }));
|
|
513
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
514
|
+
results.push({ ref: f.ref, ok: true, msg: 'filled', type: tag });
|
|
515
|
+
}
|
|
516
|
+
} catch(e) { results.push({ ref: f.ref, ok: false, msg: e.message || String(e), type: tag }); }
|
|
517
|
+
});
|
|
518
|
+
return JSON.stringify(results);
|
|
519
|
+
})(${JSON.stringify(options.fields.map(f => ({ ref: f.ref, selector: toSelector(f.ref), value: f.value })))})`;
|
|
520
|
+
const { result } = await session.Runtime.evaluate({ expression: blastExpression, returnByValue: true });
|
|
521
|
+
const blastResults = JSON.parse(result?.value || '[]');
|
|
522
|
+
for (const r of blastResults) {
|
|
523
|
+
results.push({
|
|
524
|
+
ref: r.ref,
|
|
525
|
+
field_type: r.type,
|
|
526
|
+
success: r.ok,
|
|
527
|
+
message: r.ok ? r.msg : `FAIL — ${r.msg}`,
|
|
528
|
+
value: options.fields.find(f => f.ref === r.ref)?.value,
|
|
529
|
+
});
|
|
530
|
+
if (!r.ok)
|
|
531
|
+
logAction("browser_fill", r.ref, `${r.type}: FAIL — ${r.msg}`);
|
|
532
|
+
else
|
|
533
|
+
logAction("browser_fill", r.ref, `${r.type}: OK — ${r.msg}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
for (const field of options.fields) {
|
|
538
|
+
const selector = toSelector(field.ref);
|
|
539
|
+
// ── Pre-fill actionability gate (Playwright strategy) ─────────────────────
|
|
540
|
+
// Text + select fields on complex sites can be under overlays or still animating.
|
|
541
|
+
// Verify visibility, stability, and clear hit-target before writing.
|
|
542
|
+
const actResult = await verifyActionability(session, selector, {
|
|
543
|
+
stabilityTimeoutMs: 250,
|
|
544
|
+
totalTimeoutMs: 1200,
|
|
545
|
+
});
|
|
546
|
+
if (actResult.status === 'OCCLUDED' && !actResult.occludedBy?.isWeavetabOwned) {
|
|
547
|
+
const occ = actResult.occludedBy;
|
|
548
|
+
const blockerStr = `<${occ.tag}${occ.id ? '#' + occ.id : ''}> intercepts pointer events`;
|
|
549
|
+
results.push({ ref: field.ref, field_type: "unknown", success: false, message: `Field blocked by overlay: ${blockerStr}` });
|
|
550
|
+
logAction("browser_fill", field.ref, `FAIL — occluded: ${blockerStr}`);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (actResult.status === 'INVISIBLE') {
|
|
554
|
+
results.push({ ref: field.ref, field_type: "unknown", success: false, message: "Field is not visible — scroll to reveal it before filling" });
|
|
555
|
+
logAction("browser_fill", field.ref, "FAIL — invisible");
|
|
556
|
+
continue;
|
|
557
|
+
}
|
|
558
|
+
// 1. Detect field type
|
|
559
|
+
const detected = await detectFieldType(session, selector);
|
|
560
|
+
if (!detected.exists) {
|
|
561
|
+
results.push({ ref: field.ref, field_type: "unknown", success: false, message: "Element not found" });
|
|
562
|
+
logAction("browser_fill", field.ref, "unknown: FAIL — Element not found");
|
|
563
|
+
continue;
|
|
564
|
+
}
|
|
565
|
+
const type = detected.type;
|
|
566
|
+
if (type === "unknown") {
|
|
567
|
+
results.push({ ref: field.ref, field_type: "unknown", success: false, message: "Unsupported field type" });
|
|
568
|
+
logAction("browser_fill", field.ref, "unknown: FAIL — Unsupported field type");
|
|
569
|
+
continue;
|
|
570
|
+
}
|
|
571
|
+
// 2. Snapshot before
|
|
572
|
+
const before = await getFieldState(session, selector, type);
|
|
573
|
+
let actionOk = true;
|
|
574
|
+
let actionMsg = "";
|
|
575
|
+
// Build a success predicate based on the desired target value so that:
|
|
576
|
+
// (a) already-correct fields don't falsely fail (before === after but value IS correct)
|
|
577
|
+
// (b) framework-reverted fields are still caught
|
|
578
|
+
function buildCheckSuccess(fieldType, desiredValue) {
|
|
579
|
+
if (fieldType === 'checkbox' || fieldType === 'custom-checkbox') {
|
|
580
|
+
const want = desiredValue === 'true' || desiredValue === 'yes' || desiredValue === 'checked' || desiredValue === '1' || desiredValue === 'on';
|
|
581
|
+
return (after) => (after.checked === 'true') === want;
|
|
582
|
+
}
|
|
583
|
+
if (fieldType === 'radio' || fieldType === 'custom-radio') {
|
|
584
|
+
return (after) => after.checked === 'true';
|
|
585
|
+
}
|
|
586
|
+
if (fieldType === 'select') {
|
|
587
|
+
return (after) => after.value === desiredValue || after.text === desiredValue || (after.text || '').trim() === desiredValue.trim();
|
|
588
|
+
}
|
|
589
|
+
// text / contenteditable
|
|
590
|
+
return (after) => (after.value === desiredValue || after.text === desiredValue);
|
|
591
|
+
}
|
|
592
|
+
// 4. Confirm via settle+verify — catches React/Vue handlers that revert the change.
|
|
593
|
+
// We pass the 'before' snapshot we already took, and applyFieldValue as the action.
|
|
594
|
+
// Uses a checkSuccess predicate so already-correct values are not false-negatives.
|
|
595
|
+
const confirm = await confirmChange(session, () => getFieldState(session, selector, type), async () => {
|
|
596
|
+
const applied = await applyFieldValue(session, selector, field.value, type);
|
|
597
|
+
actionOk = applied.ok;
|
|
598
|
+
actionMsg = applied.msg;
|
|
599
|
+
}, {
|
|
600
|
+
precomputedBefore: before,
|
|
601
|
+
verifyPasses: [15, 50, 150],
|
|
602
|
+
checkSuccess: buildCheckSuccess(type, field.value),
|
|
603
|
+
});
|
|
604
|
+
if (!actionOk) {
|
|
605
|
+
results.push({ ref: field.ref, field_type: type, success: false, message: actionMsg });
|
|
606
|
+
logAction("browser_fill", field.ref, `${type}: FAIL — ${actionMsg}`);
|
|
607
|
+
continue;
|
|
608
|
+
}
|
|
609
|
+
if (confirm.applied) {
|
|
610
|
+
results.push({
|
|
611
|
+
ref: field.ref,
|
|
612
|
+
field_type: type,
|
|
613
|
+
success: true,
|
|
614
|
+
message: `${actionMsg} (verified after ${confirm.settleMs}ms)`,
|
|
615
|
+
value: detected.isSecret ? "[REDACTED]" : field.value,
|
|
616
|
+
});
|
|
617
|
+
logAction("browser_fill", field.ref, `${type}: OK — ${actionMsg}`);
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
// Field reverted — framework rejected the value (e.g., controlled component)
|
|
621
|
+
results.push({
|
|
622
|
+
ref: field.ref,
|
|
623
|
+
field_type: type,
|
|
624
|
+
success: false,
|
|
625
|
+
message: `Value not retained — framework may have reverted it (before=${detected.isSecret ? '"[REDACTED]"' : JSON.stringify(before)}, after=${detected.isSecret ? '"[REDACTED]"' : JSON.stringify(confirm.after)})`,
|
|
626
|
+
});
|
|
627
|
+
logAction("browser_fill", field.ref, `${type}: FAIL — reverted by framework`);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
const succeeded = results.filter(r => r.success).length;
|
|
632
|
+
const failed = results.filter(r => !r.success).length;
|
|
633
|
+
let submitted = false;
|
|
634
|
+
if (options.submit && failed === 0) {
|
|
635
|
+
const subSelector = toSelector(options.submit);
|
|
636
|
+
const { result: subResult } = await session.Runtime.evaluate({
|
|
637
|
+
expression: `(function(sel) {
|
|
638
|
+
try {
|
|
639
|
+
var el = document.querySelector(sel);
|
|
640
|
+
if (!el) return 'not found';
|
|
641
|
+
el.focus();
|
|
642
|
+
el.click();
|
|
643
|
+
el.dispatchEvent(new Event('click', { bubbles: true }));
|
|
644
|
+
return 'clicked';
|
|
645
|
+
} catch(e) { return e.message; }
|
|
646
|
+
})(${esc(subSelector)})`,
|
|
647
|
+
returnByValue: true,
|
|
648
|
+
});
|
|
649
|
+
submitted = subResult?.value === "clicked";
|
|
650
|
+
}
|
|
651
|
+
const totalMs = Date.now() - startMs;
|
|
652
|
+
recordAction({
|
|
653
|
+
timestamp: new Date().toISOString(),
|
|
654
|
+
tool: "browser_fill",
|
|
655
|
+
input: { fieldCount: options.fields.length, submit: !!options.submit },
|
|
656
|
+
result: { success: failed === 0, total: options.fields.length, succeeded, failed },
|
|
657
|
+
});
|
|
658
|
+
return {
|
|
659
|
+
success: failed === 0,
|
|
660
|
+
total_fields: options.fields.length,
|
|
661
|
+
succeeded,
|
|
662
|
+
failed,
|
|
663
|
+
submitted,
|
|
664
|
+
results,
|
|
665
|
+
execution_ms: totalMs,
|
|
666
|
+
};
|
|
667
|
+
}
|