@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,1061 @@
|
|
|
1
|
+
import { logDebug } from "../audit/logger.js";
|
|
2
|
+
import { buildActionMap, findBestMatch, findAllMatches, safeSelector, toMarkdownTable, resolveByAria } from "../cdp/walker.js";
|
|
3
|
+
import { checkDomain } from "../security/blacklist.js";
|
|
4
|
+
import { logAction } from "../audit/logger.js";
|
|
5
|
+
import { recordAction, getLoopDiagnostics, trackToolCall, detectStuckLoop } from "../intelligence/trail.js";
|
|
6
|
+
import { waitForDOMStable } from "../sensors/guard.js";
|
|
7
|
+
import { waitForNetworkIdle } from "../sensors/wait.js";
|
|
8
|
+
import { ghostType, ghostMove, ghostClick } from "../overlay/ghost.js";
|
|
9
|
+
import { cacheActionMap, getCachedActionMap } from "../state/cache.js";
|
|
10
|
+
import { storeSnapshot, computeRichDelta } from "../state/delta.js";
|
|
11
|
+
import { sessionRegistry } from "../state/session.js";
|
|
12
|
+
import { saveDomainProfile, getBestSelectorForIntent, logMemorySuccess, logMemoryFailure, loadDomainProfile } from "../intelligence/sessionMemory.js";
|
|
13
|
+
import { getConfigAgentId, getConfigHeadless } from "../config/loader.js";
|
|
14
|
+
import { sendToExtension } from "../cdp/helpers.js";
|
|
15
|
+
import { appendKnowledgeHint } from "../intelligence/hints.js";
|
|
16
|
+
import { verifyActionability, quickHitCheck } from "../sensors/actionability.js";
|
|
17
|
+
/**
|
|
18
|
+
* Detect the JavaScript framework used by the current page
|
|
19
|
+
* This helps determine the best input event strategy
|
|
20
|
+
*/
|
|
21
|
+
export async function detectFramework(session) {
|
|
22
|
+
try {
|
|
23
|
+
const { result } = await session.Runtime.evaluate({
|
|
24
|
+
expression: `(function() {
|
|
25
|
+
const root = document.documentElement;
|
|
26
|
+
if (root.__reactFiber || root._reactRootContainer || window.React || window.react) return 'react';
|
|
27
|
+
if (root.__vue__ || document.querySelector('[data-v-]') || window.Vue || window.vue) return 'vue';
|
|
28
|
+
if (root.ng || document.querySelector('[ng-version]') || window.angular || window.ng) return 'angular';
|
|
29
|
+
return 'vanilla';
|
|
30
|
+
})()`,
|
|
31
|
+
returnByValue: true,
|
|
32
|
+
});
|
|
33
|
+
return result?.value || 'vanilla';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return 'vanilla';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async function focusEditable(session, objectId, clearFirst, fast = false) {
|
|
40
|
+
const { Input } = session;
|
|
41
|
+
await session.Runtime.callFunctionOn({
|
|
42
|
+
functionDeclaration: `async function() {
|
|
43
|
+
if (this.scrollIntoViewIfNeeded) this.scrollIntoViewIfNeeded();
|
|
44
|
+
else {
|
|
45
|
+
this.scrollIntoView({behavior:'smooth',block:'center'});
|
|
46
|
+
await new Promise(r => setTimeout(r, ${fast ? 50 : 400}));
|
|
47
|
+
}
|
|
48
|
+
this.focus();
|
|
49
|
+
this.click();
|
|
50
|
+
}`,
|
|
51
|
+
objectId,
|
|
52
|
+
silent: true,
|
|
53
|
+
awaitPromise: true,
|
|
54
|
+
});
|
|
55
|
+
if (clearFirst) {
|
|
56
|
+
await session.Runtime.callFunctionOn({
|
|
57
|
+
functionDeclaration: `function() {
|
|
58
|
+
if (typeof this.select === 'function') this.select();
|
|
59
|
+
else if (this.isContentEditable) {
|
|
60
|
+
this.textContent = '';
|
|
61
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
62
|
+
} else {
|
|
63
|
+
this.value = '';
|
|
64
|
+
this.dispatchEvent(new Event('input', { bubbles: true }));
|
|
65
|
+
}
|
|
66
|
+
}`,
|
|
67
|
+
objectId,
|
|
68
|
+
silent: true,
|
|
69
|
+
});
|
|
70
|
+
await Input.dispatchKeyEvent({ type: "keyDown", key: "Delete", nativeVirtualKeyCode: 46, windowsVirtualKeyCode: 46 });
|
|
71
|
+
await Input.dispatchKeyEvent({ type: "keyUp", key: "Delete", nativeVirtualKeyCode: 46, windowsVirtualKeyCode: 46 });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Framework-aware text injection.
|
|
76
|
+
* React/Vue controlled inputs require the native prototype setter + InputEvent
|
|
77
|
+
* to trigger synthetic event reconciliation. Firing a plain Event('input') AFTER
|
|
78
|
+
* an insertText causes a second state update — the source of triplication.
|
|
79
|
+
*
|
|
80
|
+
* Returns true when the native-setter path was taken (caller skips redundant
|
|
81
|
+
* post-type event dispatch).
|
|
82
|
+
*/
|
|
83
|
+
export async function frameworkAwareType(session, objectId, text, framework) {
|
|
84
|
+
const { Input, Runtime } = session;
|
|
85
|
+
if (framework === 'react' || framework === 'vue') {
|
|
86
|
+
// Use native prototype setter so React/Vue synthetic reconciler sees the
|
|
87
|
+
// change as coming from the user, not an external mutation.
|
|
88
|
+
// One InputEvent with inputType:'insertText' is what React 16/17/18 listens
|
|
89
|
+
// for — the old `.simulated = true` trick only worked in React <16 and is a
|
|
90
|
+
// no-op in modern React (Event properties are frozen after construction).
|
|
91
|
+
await Runtime.callFunctionOn({
|
|
92
|
+
functionDeclaration: `function(text) {
|
|
93
|
+
const nativeInputSetter = Object.getOwnPropertyDescriptor(
|
|
94
|
+
window.HTMLInputElement.prototype, 'value'
|
|
95
|
+
)?.set;
|
|
96
|
+
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(
|
|
97
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
98
|
+
)?.set;
|
|
99
|
+
|
|
100
|
+
if (this.tagName === 'INPUT' && nativeInputSetter) {
|
|
101
|
+
nativeInputSetter.call(this, text);
|
|
102
|
+
} else if (this.tagName === 'TEXTAREA' && nativeTextAreaSetter) {
|
|
103
|
+
nativeTextAreaSetter.call(this, text);
|
|
104
|
+
} else if (this.isContentEditable) {
|
|
105
|
+
this.textContent = text;
|
|
106
|
+
} else {
|
|
107
|
+
this.value = text;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Single InputEvent with inputType:'insertText' — React 16/17/18 SyntheticEvent
|
|
111
|
+
// source. Do NOT also dispatch a plain Event('input') here; that would
|
|
112
|
+
// fire the onChange handler a second time and duplicate the value.
|
|
113
|
+
this.dispatchEvent(new InputEvent('input', {
|
|
114
|
+
bubbles: true,
|
|
115
|
+
cancelable: true,
|
|
116
|
+
inputType: 'insertText',
|
|
117
|
+
data: text,
|
|
118
|
+
}));
|
|
119
|
+
this.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
120
|
+
}`,
|
|
121
|
+
objectId,
|
|
122
|
+
arguments: [{ value: text }],
|
|
123
|
+
silent: true,
|
|
124
|
+
awaitPromise: true,
|
|
125
|
+
});
|
|
126
|
+
// Signal to caller: native-setter path taken — caller must NOT re-dispatch events
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// For vanilla JS / Angular: standard CDP engine-level text insertion
|
|
131
|
+
await Input.insertText({ text });
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function resolveTypingTarget(session, map, args, _targetId) {
|
|
136
|
+
const { id, label, intent, backendNodeId: directNodeId } = args;
|
|
137
|
+
const { DOM } = session;
|
|
138
|
+
if (directNodeId) {
|
|
139
|
+
const resolved = await DOM.resolveNode({ backendNodeId: directNodeId }).catch(() => null);
|
|
140
|
+
const objectId = resolved?.object?.objectId;
|
|
141
|
+
return {
|
|
142
|
+
backendNodeId: directNodeId,
|
|
143
|
+
objectId,
|
|
144
|
+
label: `node-${directNodeId}`,
|
|
145
|
+
ref: `node-${directNodeId}`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
let element = id ? map.elements.find((el) => el.id === id || el.id.endsWith(id)) : undefined;
|
|
149
|
+
// Fast-fail: w:NN IDs not in the current map are stale. Return immediately.
|
|
150
|
+
if (!element && id && id.startsWith("w:")) {
|
|
151
|
+
return {
|
|
152
|
+
success: false,
|
|
153
|
+
error_code: "ELEMENT_NOT_FOUND",
|
|
154
|
+
message: `[!] Ref '${id}' not found in the current page map. The page may have updated. Call browser_map() to get fresh ref IDs.`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
if (!element && id && !id.startsWith("w:")) {
|
|
158
|
+
try {
|
|
159
|
+
const checkEl = await session.Runtime.evaluate({
|
|
160
|
+
expression: `(function(){
|
|
161
|
+
const el = document.querySelector(${JSON.stringify(id)}) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(${JSON.stringify(id)}) : null);
|
|
162
|
+
if (!el) return null;
|
|
163
|
+
const r = el.getBoundingClientRect();
|
|
164
|
+
return {
|
|
165
|
+
label: el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name') || 'selector-match',
|
|
166
|
+
x: Math.round(r.left + r.width / 2),
|
|
167
|
+
y: Math.round(r.top + r.height / 2)
|
|
168
|
+
};
|
|
169
|
+
})()`,
|
|
170
|
+
returnByValue: true
|
|
171
|
+
});
|
|
172
|
+
if (checkEl?.result?.value) {
|
|
173
|
+
const val = checkEl.result.value;
|
|
174
|
+
element = {
|
|
175
|
+
id: id,
|
|
176
|
+
label: val.label,
|
|
177
|
+
x: val.x,
|
|
178
|
+
y: val.y,
|
|
179
|
+
selector: id,
|
|
180
|
+
role: "textbox",
|
|
181
|
+
group: "utility",
|
|
182
|
+
depth: 0,
|
|
183
|
+
fingerprint: "selector:" + id,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
logDebug("Failed raw selector resolve", e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (!element && label)
|
|
192
|
+
element = findBestMatch(map.elements, label) ?? undefined;
|
|
193
|
+
if (!element && intent)
|
|
194
|
+
element = findBestMatch(map.elements, intent) ?? undefined;
|
|
195
|
+
if (element) {
|
|
196
|
+
if (element.backendNodeId) {
|
|
197
|
+
const { nodeIds } = await DOM.pushNodesByBackendIdsToFrontend({ backendNodeIds: [element.backendNodeId] }).catch(() => ({ nodeIds: [] }));
|
|
198
|
+
let objectId;
|
|
199
|
+
let x = element.x;
|
|
200
|
+
let y = element.y;
|
|
201
|
+
if (nodeIds[0]) {
|
|
202
|
+
const resolved = await DOM.resolveNode({ nodeId: nodeIds[0] }).catch(() => null);
|
|
203
|
+
objectId = resolved?.object?.objectId;
|
|
204
|
+
if (x === undefined || y === undefined) {
|
|
205
|
+
const box = await DOM.getBoxModel({ nodeId: nodeIds[0] }).catch(() => null);
|
|
206
|
+
if (box?.model?.content) {
|
|
207
|
+
const q = box.model.content;
|
|
208
|
+
x = q[0] + (q[2] - q[0]) / 2;
|
|
209
|
+
y = q[1] + (q[5] - q[1]) / 2;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return { backendNodeId: element.backendNodeId, objectId, label: element.label, ref: element.id, x, y, selector: safeSelector(element), structuralIdiom: element.structuralIdiom };
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
return { label: element.label, ref: element.id, x: element.x, y: element.y, selector: safeSelector(element), structuralIdiom: element.structuralIdiom };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// ARIA fallback before DOM heuristic
|
|
220
|
+
if (!element && (label || intent)) {
|
|
221
|
+
const ariaMatch = await resolveByAria(session, label || intent || "");
|
|
222
|
+
if (ariaMatch) {
|
|
223
|
+
return {
|
|
224
|
+
backendNodeId: undefined,
|
|
225
|
+
objectId: undefined,
|
|
226
|
+
label: ariaMatch.label,
|
|
227
|
+
ref: ariaMatch.id,
|
|
228
|
+
x: ariaMatch.x,
|
|
229
|
+
y: ariaMatch.y,
|
|
230
|
+
selector: ariaMatch.selector,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// If a specific target was requested (id, label, or intent) but ALL resolution
|
|
235
|
+
// paths above failed, return null — do NOT fall through to the generic
|
|
236
|
+
// activeElement/largest-field heuristic. Falling through causes consecutive
|
|
237
|
+
// type() calls to silently write into the same focused element, producing
|
|
238
|
+
// cross-contaminated values.
|
|
239
|
+
if (id || label || intent) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
const active = await session.Runtime.evaluate({
|
|
243
|
+
expression: `(function(){
|
|
244
|
+
const pick = (el) => {
|
|
245
|
+
if (!el) return null;
|
|
246
|
+
const r = el.getBoundingClientRect();
|
|
247
|
+
if (r.width < 20 || r.height < 10) return null;
|
|
248
|
+
if (r.bottom < 0 || r.top > innerHeight) return null;
|
|
249
|
+
return {
|
|
250
|
+
label: el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name') || (el.textContent||'').trim().slice(0,40) || 'input',
|
|
251
|
+
selector: ['textarea[name="q"]','input[name="q"]','[aria-label="Search"]','[title="Search"]','input[type="search"]','textarea','input[type="text"]','input:not([type])','[role="combobox"]','[role="textbox"]','[contenteditable="true"]','[contenteditable=""]'].find(function(s){try{return el.matches(s)}catch(e){return false}}) || '',
|
|
252
|
+
x: Math.round(r.left + r.width/2),
|
|
253
|
+
y: Math.round(r.top + r.height/2)
|
|
254
|
+
};
|
|
255
|
+
};
|
|
256
|
+
var sel;
|
|
257
|
+
sel = 'textarea[name="q"], input[name="q"], [aria-label="Search"], [title="Search"]';
|
|
258
|
+
var q = document.querySelector(sel);
|
|
259
|
+
var fromQ = pick(q);
|
|
260
|
+
if (fromQ) return JSON.stringify(fromQ);
|
|
261
|
+
var el = document.activeElement;
|
|
262
|
+
var isEditable = el && (
|
|
263
|
+
el.isContentEditable ||
|
|
264
|
+
el.tagName === 'TEXTAREA' ||
|
|
265
|
+
(el.tagName === 'INPUT' && !['button','submit','checkbox','radio','hidden','file'].includes((el.type||'').toLowerCase()))
|
|
266
|
+
);
|
|
267
|
+
if (isEditable) return JSON.stringify(pick(el));
|
|
268
|
+
var sels = ['[contenteditable="true"]','[contenteditable=""]','textarea','input[type="search"]','input[type="text"]','input:not([type])','[role="textbox"]','[role="combobox"]','input[name="q"]'];
|
|
269
|
+
var best = null, bestArea = 0;
|
|
270
|
+
for (var si = 0; si < sels.length; si++) {
|
|
271
|
+
var nodes = document.querySelectorAll(sels[si]);
|
|
272
|
+
for (var ni = 0; ni < nodes.length; ni++) {
|
|
273
|
+
var r = nodes[ni].getBoundingClientRect();
|
|
274
|
+
var area = r.width * r.height;
|
|
275
|
+
if (area > bestArea && r.top >= 0 && r.top < innerHeight) { bestArea = area; best = nodes[ni]; }
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (!best) return null;
|
|
279
|
+
return JSON.stringify(pick(best));
|
|
280
|
+
})()`,
|
|
281
|
+
returnByValue: true,
|
|
282
|
+
}).catch(() => null);
|
|
283
|
+
const raw = active?.result?.value;
|
|
284
|
+
if (!raw || raw === "null")
|
|
285
|
+
return null;
|
|
286
|
+
const parsed = JSON.parse(raw);
|
|
287
|
+
if (!parsed || parsed.x === undefined || parsed.y === undefined)
|
|
288
|
+
return null;
|
|
289
|
+
// Try DOM.querySelector first (more reliable than getNodeForLocation for overlays)
|
|
290
|
+
if (parsed.selector) {
|
|
291
|
+
try {
|
|
292
|
+
const { root } = await DOM.getDocument().catch(() => ({ root: null }));
|
|
293
|
+
if (root?.nodeId) {
|
|
294
|
+
const docResult = await DOM.querySelector({ nodeId: root.nodeId, selector: parsed.selector });
|
|
295
|
+
if (docResult?.nodeId) {
|
|
296
|
+
const resolvedNode = await DOM.resolveNode({ nodeId: docResult.nodeId }).catch(() => null);
|
|
297
|
+
const described = await DOM.describeNode({ nodeId: docResult.nodeId }).catch(() => null);
|
|
298
|
+
return {
|
|
299
|
+
backendNodeId: described?.node?.backendNodeId,
|
|
300
|
+
objectId: resolvedNode?.object?.objectId,
|
|
301
|
+
label: parsed.label,
|
|
302
|
+
ref: "focused",
|
|
303
|
+
x: parsed.x,
|
|
304
|
+
y: parsed.y,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
catch { /* DOM.querySelector failed, fallback to getNodeForLocation */ }
|
|
310
|
+
}
|
|
311
|
+
const loc = await DOM.getNodeForLocation({ x: parsed.x, y: parsed.y }).catch(() => null);
|
|
312
|
+
if (!loc?.nodeId)
|
|
313
|
+
return { label: parsed.label, ref: "focused", x: parsed.x, y: parsed.y };
|
|
314
|
+
const resolvedNode = await DOM.resolveNode({ nodeId: loc.nodeId }).catch(() => null);
|
|
315
|
+
const described = await DOM.describeNode({ nodeId: loc.nodeId }).catch(() => null);
|
|
316
|
+
return {
|
|
317
|
+
backendNodeId: described?.node?.backendNodeId,
|
|
318
|
+
objectId: resolvedNode?.object?.objectId,
|
|
319
|
+
label: parsed.label,
|
|
320
|
+
ref: "focused",
|
|
321
|
+
x: parsed.x,
|
|
322
|
+
y: parsed.y,
|
|
323
|
+
selector: parsed.selector,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
export async function weaveType(session, args, config) {
|
|
327
|
+
const { id, label, intent, backendNodeId: directNodeId, text, clearFirst, fast = false } = args;
|
|
328
|
+
const { Page, DOM } = session;
|
|
329
|
+
const startMs = Date.now();
|
|
330
|
+
const { frameTree } = await Page.getFrameTree();
|
|
331
|
+
const url = frameTree.frame.url;
|
|
332
|
+
if (!id && !label && !intent && args.x === undefined && args.y === undefined) {
|
|
333
|
+
return { success: false, error_code: "NO_TARGET", message: `[!] browser_type requires id, label, or intent — no target specified.` };
|
|
334
|
+
}
|
|
335
|
+
const domainCheck = checkDomain(url, config);
|
|
336
|
+
if (!domainCheck.allowed) {
|
|
337
|
+
return { success: false, error_code: "DOMAIN_BLOCKED", message: `[!] Weave blocked: ${domainCheck.reason}` };
|
|
338
|
+
}
|
|
339
|
+
// Check for stuck loop before processing
|
|
340
|
+
const stuckCheck = detectStuckLoop();
|
|
341
|
+
if (stuckCheck.stuck) {
|
|
342
|
+
return {
|
|
343
|
+
success: false,
|
|
344
|
+
error_code: "STUCK_LOOP",
|
|
345
|
+
message: stuckCheck.diagnosis || "Agent is stuck in a loop",
|
|
346
|
+
suggestion: "Try a different approach or reset the loop counter",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
let targetId = "default";
|
|
350
|
+
const active = sessionRegistry.getActive(getConfigAgentId(config));
|
|
351
|
+
if (active)
|
|
352
|
+
targetId = active.targetId;
|
|
353
|
+
const urlObj = new URL(url);
|
|
354
|
+
const hostname = urlObj.hostname;
|
|
355
|
+
const pathname = urlObj.pathname;
|
|
356
|
+
let usedMemoryIntent = false;
|
|
357
|
+
let sniperWarning = "";
|
|
358
|
+
// FAST PATH: Memory Intent Intercept
|
|
359
|
+
// Runs when intent is provided (with or without label — intent takes priority).
|
|
360
|
+
// Soft-miss: if no high-confidence hit, fall through to normal DOM resolution.
|
|
361
|
+
if (intent && !id) {
|
|
362
|
+
const memHit = await getBestSelectorForIntent(hostname, intent, pathname);
|
|
363
|
+
if (memHit) {
|
|
364
|
+
args.id = memHit.selector;
|
|
365
|
+
usedMemoryIntent = true;
|
|
366
|
+
sniperWarning = `(Memory Intercept: Skipped DOM evaluation for intent: '${intent}')`;
|
|
367
|
+
logDebug(`[Memory] Hit! Typing into learned selector '${memHit.selector}' for intent '${intent}'`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
logDebug(`[Memory] Miss for intent '${intent}'. Falling through to DOM resolution.`);
|
|
371
|
+
// Do NOT return an error — fall through to normal resolution below
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// ── 1. Resolve element (fresh map + editable fallbacks) ───────────────────
|
|
375
|
+
// On a memory hit we already have an exact selector — skip the full DOM map build
|
|
376
|
+
// (that's the expensive part on heavy pages, 1–2s). Go straight to coord resolution.
|
|
377
|
+
let map = null;
|
|
378
|
+
let target;
|
|
379
|
+
if (usedMemoryIntent && args.id) {
|
|
380
|
+
// Memory fast-path: build a minimal synthetic target directly from the selector.
|
|
381
|
+
// No buildActionMap, no resolveTypingTarget, no snapshot delta overhead.
|
|
382
|
+
const syntheticSelector = args.id;
|
|
383
|
+
target = {
|
|
384
|
+
label: intent ?? syntheticSelector,
|
|
385
|
+
ref: "memory",
|
|
386
|
+
selector: syntheticSelector,
|
|
387
|
+
x: undefined,
|
|
388
|
+
y: undefined,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
else {
|
|
392
|
+
map = getCachedActionMap(targetId, url);
|
|
393
|
+
if (!map) {
|
|
394
|
+
map = await buildActionMap(session, { lite: true }, targetId);
|
|
395
|
+
cacheActionMap(targetId, map);
|
|
396
|
+
}
|
|
397
|
+
storeSnapshot(targetId, map.url, map.elements);
|
|
398
|
+
// When not memory-resolved: if intent was provided alongside label, label takes fallback slot.
|
|
399
|
+
const query = args.id ?? label ?? intent;
|
|
400
|
+
target = await resolveTypingTarget(session, map, args, targetId);
|
|
401
|
+
if (!target && args.x !== undefined && args.y !== undefined) {
|
|
402
|
+
target = {
|
|
403
|
+
label: `coords(${args.x},${args.y})`,
|
|
404
|
+
ref: `coords`,
|
|
405
|
+
x: args.x,
|
|
406
|
+
y: args.y,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (query && !target) {
|
|
410
|
+
const matches = findAllMatches(map.elements, query ?? "");
|
|
411
|
+
if (matches.length > 1) {
|
|
412
|
+
const skipped = matches.slice(1, 4).map(m => `[${m.id}]`).join(", ") + (matches.length > 4 ? ` (+${matches.length - 4} more)` : "");
|
|
413
|
+
return { success: false, error_code: "AMBIGUOUS_TARGET", message: `[!] Refusing to type into ambiguous target. Found ${matches.length} matches for '${query}'. Matches: ${skipped}` };
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else if (query && target) {
|
|
417
|
+
const matches = findAllMatches(map.elements, query ?? "");
|
|
418
|
+
if (matches.length > 1) {
|
|
419
|
+
const skipped = matches.slice(1, 4).map(m => `[${m.id}]`).join(", ") + (matches.length > 4 ? ` (+${matches.length - 4} more)` : "");
|
|
420
|
+
return { success: false, error_code: "AMBIGUOUS_TARGET", message: `[!] Refusing to type into ambiguous target. Found ${matches.length} matches for '${query}'. Please be more specific or use exact ID. Matches: ${skipped}` };
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (target && target.selector) {
|
|
425
|
+
const isEditableRes = await session.Runtime.evaluate({
|
|
426
|
+
expression: `(function(sel) {
|
|
427
|
+
const el = document.querySelector(sel);
|
|
428
|
+
if (!el) return true; // optimistic: let it try
|
|
429
|
+
const tag = el.tagName.toLowerCase();
|
|
430
|
+
const t = (el.getAttribute('type') || '').toLowerCase();
|
|
431
|
+
const role = (el.getAttribute('role') || '').toLowerCase();
|
|
432
|
+
// Standard editable
|
|
433
|
+
if (el.isContentEditable) return true;
|
|
434
|
+
if (tag === 'textarea') return true;
|
|
435
|
+
if (tag === 'input' && !['button','submit','checkbox','radio','hidden','file','image','reset'].includes(t)) return true;
|
|
436
|
+
// ARIA editable roles — many custom widgets use these
|
|
437
|
+
if (role === 'textbox' || role === 'combobox' || role === 'searchbox' || role === 'spinbutton') return true;
|
|
438
|
+
// Custom div/span inputs often have no role — check for contenteditable ancestors
|
|
439
|
+
if (el.closest('[contenteditable]')) return true;
|
|
440
|
+
return false;
|
|
441
|
+
})(${JSON.stringify(target.selector)})`,
|
|
442
|
+
returnByValue: true
|
|
443
|
+
}).catch(() => null);
|
|
444
|
+
// Soft fail: warn but don't hard-block. Some custom widgets look non-editable but still accept input.
|
|
445
|
+
if (isEditableRes && isEditableRes.result?.value === false) {
|
|
446
|
+
// Return a graceful error with actionable fallback instructions
|
|
447
|
+
return {
|
|
448
|
+
success: false,
|
|
449
|
+
error_code: "NOT_EDITABLE",
|
|
450
|
+
message: `[!] Element (${target.selector}) does not appear to be a standard editable field. Fallback: (1) use browser_click on the field to focus it, then (2) use browser_key to send individual characters or (3) use browser_eval to set the value directly via JS.`,
|
|
451
|
+
suggestion: `browser_click { id: "${target.ref}" } then browser_key { key: "<yourtext>" }`,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (!target) {
|
|
456
|
+
if (intent && args.id) {
|
|
457
|
+
await logMemoryFailure(hostname, intent, args.id, "DEAD_SELECTOR", pathname);
|
|
458
|
+
}
|
|
459
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Could not find input matching "${id ?? label ?? intent ?? "any field"}"` };
|
|
460
|
+
}
|
|
461
|
+
const loopDiag = getLoopDiagnostics("browser_type", target.ref);
|
|
462
|
+
if (loopDiag.severity === "critical") {
|
|
463
|
+
return { success: false, error_code: "STUCK_LOOP_WARNING", message: `[!] Loop detected: You have tried to type into [${target.ref}] ${loopDiag.strikes} times without success. Call browser_reset_loop_counter if this is a new task.` };
|
|
464
|
+
}
|
|
465
|
+
if (loopDiag.severity === "warning") {
|
|
466
|
+
logAction("browser_type", target.ref, `loop warning: ${loopDiag.strikes} consecutive strikes on [${target.ref}]`);
|
|
467
|
+
}
|
|
468
|
+
// Get stable selector for coordinate resolution
|
|
469
|
+
const selector = target.selector || (target.ref === "focused" ? "document.activeElement" : `[data-wt-id="${(target.ref ?? '').split(':')[1] ?? target.ref ?? ''}"]`);
|
|
470
|
+
// 2. Frame Awareness & Coordinate Resolution
|
|
471
|
+
let coords = null;
|
|
472
|
+
let frameUrl = null;
|
|
473
|
+
try {
|
|
474
|
+
const res = await session.Runtime.evaluate({
|
|
475
|
+
expression: `(function(sel) {
|
|
476
|
+
const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
477
|
+
if (!el) return null;
|
|
478
|
+
return {
|
|
479
|
+
coords: window.__Weavetab_getCoords(sel),
|
|
480
|
+
inIframe: el.ownerDocument.defaultView !== window,
|
|
481
|
+
frameUrl: el.ownerDocument.defaultView.location.href
|
|
482
|
+
};
|
|
483
|
+
})(${JSON.stringify(selector)})`,
|
|
484
|
+
returnByValue: true
|
|
485
|
+
});
|
|
486
|
+
coords = res.result?.value?.coords;
|
|
487
|
+
frameUrl = res.result?.value?.frameUrl;
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
logDebug(`Failed to resolve typing target ${selector}:`, e);
|
|
491
|
+
}
|
|
492
|
+
if (!coords) {
|
|
493
|
+
if (target.x !== undefined && target.y !== undefined) {
|
|
494
|
+
coords = { x: target.x, y: target.y };
|
|
495
|
+
}
|
|
496
|
+
else if (args.x !== undefined && args.y !== undefined) {
|
|
497
|
+
coords = { x: args.x, y: args.y };
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
return { success: false, error_code: "COORD_RESOLUTION_FAILED", message: `[!] Extension could not resolve coordinates for input ${selector} and no fallback coordinates available` };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
let resolvedLabel = target.label || "";
|
|
504
|
+
const resolvedRef = target.ref;
|
|
505
|
+
// ── Playwright-strategy pre-focus actionability gate ─────────────────────────
|
|
506
|
+
// Verify the target is stable, unoccluded, and in viewport before any
|
|
507
|
+
// mouse events are fired. Uses 4-way scroll + RAF stability + WT transparency.
|
|
508
|
+
if (selector && selector !== 'document.activeElement') {
|
|
509
|
+
const actCheck = await verifyActionability(session, selector, {
|
|
510
|
+
stabilityTimeoutMs: 300,
|
|
511
|
+
totalTimeoutMs: 1800,
|
|
512
|
+
});
|
|
513
|
+
if (actCheck.status === 'OCCLUDED' && !actCheck.occludedBy?.isWeavetabOwned) {
|
|
514
|
+
const occ = actCheck.occludedBy;
|
|
515
|
+
const id = occ.id ? `#${occ.id}` : '';
|
|
516
|
+
const cls = occ.className ? `.${occ.className.split(' ')[0]}` : '';
|
|
517
|
+
return {
|
|
518
|
+
success: false,
|
|
519
|
+
error_code: "ELEMENT_OCCLUDED_BY",
|
|
520
|
+
message: `[!] <${occ.tag}${id}${cls}${occ.ariaLabel ? ` "${occ.ariaLabel}"` : ''}> intercepts pointer events — cannot type into ${selector}. Handle the blocking element first.`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
else if (actCheck.status === 'INVISIBLE') {
|
|
524
|
+
return {
|
|
525
|
+
success: false,
|
|
526
|
+
error_code: "ELEMENT_INVISIBLE",
|
|
527
|
+
message: `[!] Target field ${selector} is not visible. Use browser_scroll or browser_click to reveal it first.`,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
else if (actCheck.status === 'NOT_FOUND') {
|
|
531
|
+
// Non-fatal for type — the element might be in shadow DOM; continue with coord path
|
|
532
|
+
logDebug(`[browser_type] verifyActionability: NOT_FOUND for ${selector}, proceeding with raw coords`);
|
|
533
|
+
}
|
|
534
|
+
else if (actCheck.status === 'ACTIONABLE' && actCheck.x !== undefined) {
|
|
535
|
+
// Use the verified stable coords from the actionability engine
|
|
536
|
+
coords = { x: actCheck.x, y: actCheck.y };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
const typePreview = text.slice(0, 20).replace(/'/g, "");
|
|
540
|
+
const typeStatus = ["typing", "entering", "inputting", "writing"][Math.floor(Math.random() * 4)];
|
|
541
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: `${typeStatus} ${typePreview}...` } });
|
|
542
|
+
// ── 3. Focus + Type ──────────────────────────────────────────────────────
|
|
543
|
+
if (coords.x !== undefined && coords.y !== undefined) {
|
|
544
|
+
const ghostMoveProfile = args.profile === "fast" ? "fast" : "human";
|
|
545
|
+
await ghostMove(session, coords.x, coords.y, config, resolvedLabel.slice(0, 30), ghostMoveProfile);
|
|
546
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', cursor: { x: coords.x, y: coords.y }, state: 'typing' });
|
|
547
|
+
if (!getConfigHeadless(config))
|
|
548
|
+
await new Promise(r => setTimeout(r, 100 + Math.random() * 150));
|
|
549
|
+
}
|
|
550
|
+
// Focus + optional clear via focusEditable when we have an objectId
|
|
551
|
+
// Phase 3b: clearFirst only runs when explicitly true — insertText replaces content naturally
|
|
552
|
+
if (target.objectId) {
|
|
553
|
+
await focusEditable(session, target.objectId, clearFirst === true, fast);
|
|
554
|
+
await new Promise(r => setTimeout(r, fast ? 10 : 80 + Math.random() * 80));
|
|
555
|
+
}
|
|
556
|
+
else {
|
|
557
|
+
// Fallback: click-to-focus via CDP dispatchMouseEvent (Phase 3a)
|
|
558
|
+
// ── Pre-focus hit-target check (mirrors Playwright Step 6) ────────────
|
|
559
|
+
// The misclick-onto-overlay issue happens because ghostClick fires at coords
|
|
560
|
+
// without checking what is actually on top. Run quickHitCheck here so we
|
|
561
|
+
// abort before touching the field.
|
|
562
|
+
if (coords.x !== undefined && coords.y !== undefined && selector && selector !== 'document.activeElement') {
|
|
563
|
+
const hitCheck = await quickHitCheck(session, coords.x, coords.y, selector);
|
|
564
|
+
if (!hitCheck.clear) {
|
|
565
|
+
return {
|
|
566
|
+
success: false,
|
|
567
|
+
error_code: "ELEMENT_OCCLUDED_BY",
|
|
568
|
+
message: `[!] ${hitCheck.blockerDescription ?? 'An overlay intercepts pointer events'} — cannot focus ${selector}. Dismiss the overlay first, then retry browser_type.`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (coords.x !== undefined && coords.y !== undefined) {
|
|
573
|
+
await ghostClick(session, coords.x, coords.y);
|
|
574
|
+
await session.Input.dispatchMouseEvent({ type: "mouseReleased", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
|
|
575
|
+
await new Promise(r => setTimeout(r, fast ? 10 : 100 + Math.random() * 100));
|
|
576
|
+
}
|
|
577
|
+
// Manual clear only when explicitly requested (insertText replaces naturally)
|
|
578
|
+
const shouldPaste = args.paste || text.length > 8;
|
|
579
|
+
if (clearFirst === true && !shouldPaste) {
|
|
580
|
+
await session.Runtime.evaluate({
|
|
581
|
+
expression: `(function(sel) {
|
|
582
|
+
const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
583
|
+
if (!el) return;
|
|
584
|
+
if (el.isContentEditable) {
|
|
585
|
+
el.textContent = '';
|
|
586
|
+
} else {
|
|
587
|
+
el.value = '';
|
|
588
|
+
const nativeSetter = el.tagName === 'TEXTAREA'
|
|
589
|
+
? Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set
|
|
590
|
+
: Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set;
|
|
591
|
+
if (nativeSetter) nativeSetter.call(el, '');
|
|
592
|
+
}
|
|
593
|
+
el.dispatchEvent(new Event('input',{bubbles:true}));
|
|
594
|
+
el.dispatchEvent(new Event('change',{bubbles:true}));
|
|
595
|
+
})(${JSON.stringify(selector)})`,
|
|
596
|
+
returnByValue: false,
|
|
597
|
+
}).catch(() => { });
|
|
598
|
+
await new Promise(r => setTimeout(r, 40));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Verify focus landed correctly
|
|
602
|
+
const focusCheck = await session.Runtime.evaluate({
|
|
603
|
+
expression: `(function(sel) {
|
|
604
|
+
const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
605
|
+
return document.activeElement === el || (el ? el.contains(document.activeElement) : false);
|
|
606
|
+
})(${JSON.stringify(selector)})`,
|
|
607
|
+
returnByValue: true
|
|
608
|
+
}).catch(() => null);
|
|
609
|
+
if (focusCheck?.result?.value === false) {
|
|
610
|
+
console.warn(`[Weavetab] Focus didn't land on ${selector}, re-clicking...`);
|
|
611
|
+
if (coords.x !== undefined && coords.y !== undefined) {
|
|
612
|
+
await ghostClick(session, coords.x, coords.y);
|
|
613
|
+
await session.Input.dispatchMouseEvent({ type: "mouseReleased", x: coords.x, y: coords.y, button: "left", clickCount: 1 });
|
|
614
|
+
await new Promise(r => setTimeout(r, 60));
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
// ── Phase 3b: Framework-aware typing method ───────────────────────────────
|
|
618
|
+
// Detect framework to choose optimal input strategy
|
|
619
|
+
const framework = await detectFramework(session);
|
|
620
|
+
// Tracks whether frameworkAwareType's native-setter path was used.
|
|
621
|
+
// When true the post-type event dispatch block is suppressed to prevent
|
|
622
|
+
// the double-onChange triplication bug on React/Vue controlled inputs.
|
|
623
|
+
let nativeSetterUsed = false;
|
|
624
|
+
// Detect password fields — always ghost type these regardless of profile
|
|
625
|
+
const isPassword = await session.Runtime.evaluate({
|
|
626
|
+
expression: `(function(sel) {
|
|
627
|
+
const el = document.querySelector(sel);
|
|
628
|
+
return el ? (el.getAttribute('type') || '').toLowerCase() === 'password' : false;
|
|
629
|
+
})(${JSON.stringify(selector)})`,
|
|
630
|
+
returnByValue: true
|
|
631
|
+
}).then(r => r.result?.value === true).catch(() => false);
|
|
632
|
+
// Click-to-type delay
|
|
633
|
+
await new Promise(r => setTimeout(r, 50 + Math.random() * 100));
|
|
634
|
+
// Auto-enable paste mode for long inputs to prevent character dropping
|
|
635
|
+
const shouldPasteForType = args.paste || text.length > 8;
|
|
636
|
+
if (shouldPasteForType && !isPassword) {
|
|
637
|
+
// Paste mode: Use clipboard for reliable long input handling
|
|
638
|
+
// This prevents character dropping on long inputs
|
|
639
|
+
let clipboardOk = false;
|
|
640
|
+
try {
|
|
641
|
+
const { browserClipboard } = await import("./clipboard.js");
|
|
642
|
+
// skipActivation=true: field is already focused; the (5,5) activation click
|
|
643
|
+
// in browserClipboard would blur the input before Ctrl+V.
|
|
644
|
+
await browserClipboard(session, { action: "write", text, skipActivation: true }, config);
|
|
645
|
+
clipboardOk = true;
|
|
646
|
+
}
|
|
647
|
+
catch (clipErr) {
|
|
648
|
+
logDebug(`[browser_type] Clipboard write failed (${clipErr}), falling back to insertText`);
|
|
649
|
+
}
|
|
650
|
+
if (clipboardOk) {
|
|
651
|
+
await new Promise(r => setTimeout(r, 50));
|
|
652
|
+
// Paste via Ctrl+V
|
|
653
|
+
await session.Input.dispatchKeyEvent({ type: "rawKeyDown", key: "v", windowsVirtualKeyCode: 86, nativeVirtualKeyCode: 86, modifiers: 2 });
|
|
654
|
+
await session.Input.dispatchKeyEvent({ type: "keyUp", key: "v", windowsVirtualKeyCode: 86, nativeVirtualKeyCode: 86, modifiers: 2 });
|
|
655
|
+
await new Promise(r => setTimeout(r, 120));
|
|
656
|
+
// Paste verification: check if the field actually received the text.
|
|
657
|
+
// Vue/React controlled inputs re-render the DOM node during focus, making
|
|
658
|
+
// Ctrl+V land on the old (detached) node. If el.value is still empty,
|
|
659
|
+
// fallback to Input.insertText which targets the currently-focused element.
|
|
660
|
+
if (selector && selector !== 'document.activeElement') {
|
|
661
|
+
const pasteCheck = await session.Runtime.evaluate({
|
|
662
|
+
expression: `(function(sel) {
|
|
663
|
+
var el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
664
|
+
if (!el) return null;
|
|
665
|
+
return el.value !== undefined ? el.value : (el.textContent || null);
|
|
666
|
+
})(${JSON.stringify(selector)})`,
|
|
667
|
+
returnByValue: true,
|
|
668
|
+
}).catch(() => null);
|
|
669
|
+
const pastedVal = pasteCheck?.result?.value;
|
|
670
|
+
if (!pastedVal || pastedVal.trim() === '') {
|
|
671
|
+
logDebug(`[browser_type] Paste landed empty on ${selector} — retrying with Input.insertText`);
|
|
672
|
+
await session.Input.insertText({ text });
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
// Clipboard unavailable — fall back to direct engine-level text insertion
|
|
678
|
+
await session.Input.insertText({ text });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
else if (isPassword || (args.profile === 'deliberate' && !shouldPasteForType)) {
|
|
682
|
+
// Security/anti-bot: ghostType — per-character timing with human jitter
|
|
683
|
+
await ghostType(session, text, config, selector, isPassword ? 'fast' : 'deliberate');
|
|
684
|
+
}
|
|
685
|
+
else if (framework === 'react' || framework === 'vue') {
|
|
686
|
+
// Framework-aware: native-setter path for React/Vue controlled inputs.
|
|
687
|
+
// frameworkAwareType returns true when the native-setter + InputEvent path
|
|
688
|
+
// was taken. In that case we MUST NOT fire an additional Event('input') below
|
|
689
|
+
// — doing so causes a second onChange dispatch and duplicates/triplicates text.
|
|
690
|
+
if (target.objectId) {
|
|
691
|
+
nativeSetterUsed = await frameworkAwareType(session, target.objectId, text, framework);
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
// Selector-based fallback — same native-setter logic, single InputEvent only
|
|
695
|
+
await session.Runtime.evaluate({
|
|
696
|
+
expression: `(function(sel, txt) {
|
|
697
|
+
const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
698
|
+
if (!el) return;
|
|
699
|
+
|
|
700
|
+
const nativeInputSetter = Object.getOwnPropertyDescriptor(
|
|
701
|
+
window.HTMLInputElement.prototype, 'value'
|
|
702
|
+
)?.set;
|
|
703
|
+
const nativeTextAreaSetter = Object.getOwnPropertyDescriptor(
|
|
704
|
+
window.HTMLTextAreaElement.prototype, 'value'
|
|
705
|
+
)?.set;
|
|
706
|
+
|
|
707
|
+
if (el.tagName === 'INPUT' && nativeInputSetter) {
|
|
708
|
+
nativeInputSetter.call(el, txt);
|
|
709
|
+
} else if (el.tagName === 'TEXTAREA' && nativeTextAreaSetter) {
|
|
710
|
+
nativeTextAreaSetter.call(el, txt);
|
|
711
|
+
} else if (el.isContentEditable) {
|
|
712
|
+
el.textContent = txt;
|
|
713
|
+
} else {
|
|
714
|
+
el.value = txt;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Single InputEvent — React 16/17/18 SyntheticEvent source.
|
|
718
|
+
// Do NOT also dispatch plain Event('input') — that fires onChange twice.
|
|
719
|
+
el.dispatchEvent(new InputEvent('input', {
|
|
720
|
+
bubbles: true, cancelable: true, inputType: 'insertText', data: txt
|
|
721
|
+
}));
|
|
722
|
+
el.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
|
723
|
+
})(${JSON.stringify(selector)}, ${JSON.stringify(text)})`,
|
|
724
|
+
returnByValue: false,
|
|
725
|
+
awaitPromise: true,
|
|
726
|
+
});
|
|
727
|
+
nativeSetterUsed = true;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
// PRIMARY: Input.insertText — engine-level text insertion for vanilla JS/Angular
|
|
732
|
+
await session.Input.insertText({ text });
|
|
733
|
+
}
|
|
734
|
+
await sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } });
|
|
735
|
+
// Post-type event dispatch: only needed for vanilla/Angular paths.
|
|
736
|
+
// React/Vue native-setter path already dispatched InputEvent + change above.
|
|
737
|
+
// Firing again here would trigger a second onChange and duplicate the value.
|
|
738
|
+
if (!nativeSetterUsed) {
|
|
739
|
+
await session.Runtime.evaluate({
|
|
740
|
+
expression: `(function(sel) {
|
|
741
|
+
const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
742
|
+
if (!el) return;
|
|
743
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
744
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
745
|
+
})(${JSON.stringify(selector)})`,
|
|
746
|
+
returnByValue: false,
|
|
747
|
+
}).catch(() => { });
|
|
748
|
+
}
|
|
749
|
+
const executionMs = Date.now() - startMs;
|
|
750
|
+
// ── 4. Post-Action Pipeline — settle + delta ──────────────────────────────
|
|
751
|
+
await Promise.all([
|
|
752
|
+
waitForDOMStable(session, 50, 400),
|
|
753
|
+
waitForNetworkIdle(session, 50, 300).catch(() => { }),
|
|
754
|
+
]);
|
|
755
|
+
let nextMap = getCachedActionMap(targetId, url);
|
|
756
|
+
if (!nextMap) {
|
|
757
|
+
nextMap = await buildActionMap(session, { lite: true }, targetId);
|
|
758
|
+
cacheActionMap(targetId, nextMap);
|
|
759
|
+
}
|
|
760
|
+
let postUrl = url;
|
|
761
|
+
let postTitle = "";
|
|
762
|
+
try {
|
|
763
|
+
const { frameTree: postFrame } = await Page.getFrameTree();
|
|
764
|
+
postUrl = postFrame.frame.url;
|
|
765
|
+
try {
|
|
766
|
+
const titleRes = await session.Runtime.evaluate({ expression: 'document.title', returnByValue: true });
|
|
767
|
+
postTitle = titleRes?.result?.value || postUrl;
|
|
768
|
+
}
|
|
769
|
+
catch {
|
|
770
|
+
postTitle = postUrl;
|
|
771
|
+
}
|
|
772
|
+
const activeSession = sessionRegistry.getActive(getConfigAgentId(config));
|
|
773
|
+
if (activeSession) {
|
|
774
|
+
activeSession.url = postUrl;
|
|
775
|
+
activeSession.title = postTitle;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
catch { /* ignore */ }
|
|
779
|
+
// Targeted field verification — always runs, independent of computeDelta.
|
|
780
|
+
// This catches cases where a framework (React/Vue) overrides the value after render.
|
|
781
|
+
let textApplied = false;
|
|
782
|
+
if (text) {
|
|
783
|
+
try {
|
|
784
|
+
const isActiveElementRef = selector === "document.activeElement";
|
|
785
|
+
const getFieldValue = async () => {
|
|
786
|
+
const expression = isActiveElementRef
|
|
787
|
+
? `(function() {
|
|
788
|
+
var el = document.activeElement;
|
|
789
|
+
if (!el) return null;
|
|
790
|
+
return el.value !== undefined ? el.value : (el.textContent || null);
|
|
791
|
+
})()`
|
|
792
|
+
: `(function(sel) {
|
|
793
|
+
var el = document.querySelector(sel);
|
|
794
|
+
if (!el) return null;
|
|
795
|
+
return el.value !== undefined ? el.value : (el.textContent || null);
|
|
796
|
+
})(${JSON.stringify(selector)})`;
|
|
797
|
+
const { result } = await session.Runtime.evaluate({ expression, returnByValue: true });
|
|
798
|
+
return result?.value;
|
|
799
|
+
};
|
|
800
|
+
const val = await getFieldValue();
|
|
801
|
+
textApplied = val === text || (typeof val === 'string' && val.trim() === text.trim())
|
|
802
|
+
// tel/formatted inputs may strip chars (e.g. dashes); also
|
|
803
|
+
// accept if the value simply contains the digits we typed.
|
|
804
|
+
|| (typeof val === 'string' && typeof text === 'string' &&
|
|
805
|
+
val.replace(/\D/g, '') === text.replace(/\D/g, '') && text.length > 3);
|
|
806
|
+
if (!textApplied) {
|
|
807
|
+
await new Promise(r => setTimeout(r, 200));
|
|
808
|
+
await waitForDOMStable(session, 50, 200).catch(() => { });
|
|
809
|
+
const retryVal = await getFieldValue();
|
|
810
|
+
textApplied = retryVal === text || (typeof retryVal === 'string' && retryVal.trim() === text.trim());
|
|
811
|
+
}
|
|
812
|
+
if (!textApplied) {
|
|
813
|
+
await new Promise(r => setTimeout(r, 400));
|
|
814
|
+
await waitForDOMStable(session, 50, 300).catch(() => { });
|
|
815
|
+
const finalVal = await getFieldValue();
|
|
816
|
+
textApplied = finalVal === text || (typeof finalVal === 'string' && finalVal.trim() === text.trim());
|
|
817
|
+
}
|
|
818
|
+
// ghostType may drop characters on busy Windows machines.
|
|
819
|
+
// Retry with atomic Input.insertText if ghostType failed verification.
|
|
820
|
+
if (!textApplied && !args.noRetry && (isPassword || args.profile === 'deliberate')) {
|
|
821
|
+
await session.Input.insertText({ text });
|
|
822
|
+
await new Promise(r => setTimeout(r, 300));
|
|
823
|
+
const retryVal = await getFieldValue();
|
|
824
|
+
textApplied = retryVal === text || (typeof retryVal === 'string' && retryVal.trim() === text.trim());
|
|
825
|
+
}
|
|
826
|
+
// Paste mode failed — retry with Input.insertText (already focused, so this lands correctly).
|
|
827
|
+
if (!textApplied && !args.noRetry && shouldPasteForType && !isPassword) {
|
|
828
|
+
await session.Input.insertText({ text });
|
|
829
|
+
await new Promise(r => setTimeout(r, 200));
|
|
830
|
+
const pasteRetryVal = await getFieldValue();
|
|
831
|
+
textApplied = pasteRetryVal === text || (typeof pasteRetryVal === 'string' && pasteRetryVal.trim() === text.trim());
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
catch { /* non-fatal */ }
|
|
835
|
+
}
|
|
836
|
+
// ─── SPA InputEvent retry (Q2: conditional auto, driven by sessionMemory) ────────────────
|
|
837
|
+
// If the text wasn't confirmed AND we know this is a React/Vue/Angular site
|
|
838
|
+
// (or a prior hint tells us), auto-retry with nativeInputValueSetter strategy.
|
|
839
|
+
// No extra CDP calls when textApplied is true (common case).
|
|
840
|
+
let spaRetryApplied = false;
|
|
841
|
+
let spaRetryStrategy = '';
|
|
842
|
+
if (!textApplied && !args.noRetry && selector && selector !== 'document.activeElement') {
|
|
843
|
+
// Load domain profile for framework detection (already in memory, no I/O)
|
|
844
|
+
const domProfile = await loadDomainProfile(hostname).catch(() => null);
|
|
845
|
+
const profileFramework = domProfile?.framework;
|
|
846
|
+
// Inline framework probe — single evaluate, zero extra round-trips beyond sessionMemory miss
|
|
847
|
+
let detectedFramework = profileFramework;
|
|
848
|
+
if (!detectedFramework) {
|
|
849
|
+
const fwCheck = await session.Runtime.evaluate({
|
|
850
|
+
expression: `(function(){
|
|
851
|
+
var r=document.documentElement;
|
|
852
|
+
if(r.__reactFiber||r._reactRootContainer) return 'react';
|
|
853
|
+
if(r.__vue__||document.querySelector('[data-v-]')) return 'vue';
|
|
854
|
+
if(r.ng||document.querySelector('[ng-version]')) return 'angular';
|
|
855
|
+
return '';
|
|
856
|
+
})()`,
|
|
857
|
+
returnByValue: true,
|
|
858
|
+
}).catch(() => null);
|
|
859
|
+
detectedFramework = fwCheck?.result?.value || '';
|
|
860
|
+
}
|
|
861
|
+
const hasSpaHint = domProfile?.memories?.some((m) => m.note?.includes('react_input_required') ||
|
|
862
|
+
m.hint?.includes('react_input_required')) ?? false;
|
|
863
|
+
const isSpa = ['react', 'vue', 'angular', 'next', 'nuxt', 'gatsby'].includes(detectedFramework) || hasSpaHint;
|
|
864
|
+
if (isSpa) {
|
|
865
|
+
const { omniEvaluate } = await import("../cdp/omni_broker.js");
|
|
866
|
+
// Single Runtime.evaluate — nativeInputValueSetter + InputEvent dispatch
|
|
867
|
+
const rawRes = await omniEvaluate(session, targetId, `(function(sel, value) {
|
|
868
|
+
try {
|
|
869
|
+
var el = document.querySelector(sel) ||
|
|
870
|
+
(window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
871
|
+
if (!el) return JSON.stringify({ ok: false, strategy: 'NOT_FOUND', exists: false });
|
|
872
|
+
|
|
873
|
+
// Strategy A: React's internal nativeInputValueSetter override
|
|
874
|
+
var nativeSetter =
|
|
875
|
+
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value')?.set ||
|
|
876
|
+
Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value')?.set;
|
|
877
|
+
|
|
878
|
+
if (nativeSetter) {
|
|
879
|
+
nativeSetter.call(el, value);
|
|
880
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
881
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
882
|
+
el.dispatchEvent(new InputEvent('input', {
|
|
883
|
+
bubbles: true, data: value, inputType: 'insertText'
|
|
884
|
+
}));
|
|
885
|
+
return JSON.stringify({ ok: el.value === value, strategy: 'native-setter', value: el.value, exists: true });
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// Strategy B: contenteditable with InputEvent
|
|
889
|
+
if (el.isContentEditable) {
|
|
890
|
+
el.textContent = value;
|
|
891
|
+
el.dispatchEvent(new InputEvent('input', { bubbles: true, data: value }));
|
|
892
|
+
return JSON.stringify({ ok: el.textContent === value, strategy: 'contenteditable', value: el.textContent, exists: true });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Strategy C: direct value + dispatchEvent fallback
|
|
896
|
+
el.value = value;
|
|
897
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
898
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
899
|
+
return JSON.stringify({ ok: el.value === value, strategy: 'direct-event', value: el.value, exists: true });
|
|
900
|
+
} catch(e) {
|
|
901
|
+
return JSON.stringify({ ok: false, strategy: 'ERROR', error: e.message, exists: false });
|
|
902
|
+
}
|
|
903
|
+
})(${JSON.stringify(selector)}, ${JSON.stringify(text)})`);
|
|
904
|
+
if (rawRes) {
|
|
905
|
+
try {
|
|
906
|
+
const spaRes = typeof rawRes === 'string' ? JSON.parse(rawRes) : rawRes;
|
|
907
|
+
if (spaRes.ok) {
|
|
908
|
+
textApplied = true;
|
|
909
|
+
spaRetryApplied = true;
|
|
910
|
+
spaRetryStrategy = spaRes.strategy;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
catch { /* non-fatal */ }
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
else if (!isSpa) {
|
|
917
|
+
// Framework unknown and confirmed:false — write a generic hint so next call knows
|
|
918
|
+
await appendKnowledgeHint(hostname, 'input_not_confirmed: browser_type confirmed:false on this domain — typing succeeded but value could not be verified, consider browser_fill for guaranteed fill').catch(() => { });
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
922
|
+
const diff = await computeRichDelta(targetId, postUrl, nextMap.elements);
|
|
923
|
+
const changesCount = diff && !("delta_timeout" in diff) ? diff.added.length + diff.removed.length + diff.changed.length : 0;
|
|
924
|
+
let isStuck = !textApplied && changesCount === 0 && postUrl === url;
|
|
925
|
+
// page_map is expensive on heavy pages. Only compute when explicitly requested
|
|
926
|
+
// or when the URL changed (agent needs the new page's element list).
|
|
927
|
+
const includeMap = args.include_map || postUrl !== url;
|
|
928
|
+
const pageMap = includeMap ? toMarkdownTable(nextMap.elements, postUrl, postTitle) : undefined;
|
|
929
|
+
const domChanged = !isStuck || postUrl !== url;
|
|
930
|
+
// Record typing strategy and known input in domain profile for future sessions
|
|
931
|
+
try {
|
|
932
|
+
const actionUrlObj = new URL(postUrl);
|
|
933
|
+
const actionHostname = actionUrlObj.hostname;
|
|
934
|
+
const actionPathname = actionUrlObj.pathname;
|
|
935
|
+
if (intent && (args.id || selector)) {
|
|
936
|
+
const finalSelector = args.id || selector;
|
|
937
|
+
if (isStuck) {
|
|
938
|
+
await logMemoryFailure(actionHostname, intent, finalSelector, "ACTION_FAILED", actionPathname);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
await logMemorySuccess(actionHostname, intent, finalSelector, {
|
|
942
|
+
role: 'textbox',
|
|
943
|
+
structuralIdiom: target.structuralIdiom
|
|
944
|
+
}, actionPathname);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
else {
|
|
948
|
+
const knownInputs = resolvedLabel && resolvedLabel !== "focused" && resolvedLabel !== ""
|
|
949
|
+
? [{ label: resolvedLabel, selector: selector }]
|
|
950
|
+
: undefined;
|
|
951
|
+
await saveDomainProfile(actionHostname, {
|
|
952
|
+
strategies: ["insertText"],
|
|
953
|
+
...(knownInputs ? { knownInputs } : {}),
|
|
954
|
+
}).catch(() => { });
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
catch { /* best-effort */ }
|
|
958
|
+
logAction("browser_type", resolvedRef, `[text] into ${resolvedLabel} (exec: ${executionMs}ms)`);
|
|
959
|
+
recordAction({
|
|
960
|
+
timestamp: new Date().toISOString(),
|
|
961
|
+
tool: "browser_type",
|
|
962
|
+
input: { ref: resolvedRef, label: resolvedLabel, text: "[REDACTED]" },
|
|
963
|
+
result: { success: true, stuck: isStuck },
|
|
964
|
+
});
|
|
965
|
+
// Track tool call for loop detection
|
|
966
|
+
trackToolCall("browser_type", resolvedRef, {
|
|
967
|
+
success: !isStuck,
|
|
968
|
+
domChanged: domChanged
|
|
969
|
+
});
|
|
970
|
+
// Capture element state after typing
|
|
971
|
+
let elementState;
|
|
972
|
+
if (selector) {
|
|
973
|
+
try {
|
|
974
|
+
const { result: stateResult } = await session.Runtime.evaluate({
|
|
975
|
+
expression: `(function(sel) {
|
|
976
|
+
var el = document.querySelector(sel);
|
|
977
|
+
if (!el) return null;
|
|
978
|
+
var isSecret = el.hasAttribute('data-wt-secret') || (el.tagName === 'INPUT' && el.type === 'password');
|
|
979
|
+
return {
|
|
980
|
+
tag: el.tagName.toLowerCase(),
|
|
981
|
+
visible: el.offsetParent !== null,
|
|
982
|
+
disabled: el.disabled,
|
|
983
|
+
value: isSecret ? '[REDACTED]' : el.value,
|
|
984
|
+
label: el.getAttribute('aria-label') || el.getAttribute('placeholder') || el.getAttribute('name') || (el.textContent||'').trim().slice(0,40),
|
|
985
|
+
textContent: isSecret ? '[REDACTED]' : (el.isContentEditable ? el.textContent?.slice(0, 100) : undefined)
|
|
986
|
+
};
|
|
987
|
+
})(${JSON.stringify(selector)})`,
|
|
988
|
+
returnByValue: true,
|
|
989
|
+
});
|
|
990
|
+
if (stateResult?.value) {
|
|
991
|
+
elementState = stateResult.value;
|
|
992
|
+
if (elementState && elementState.label && typeof elementState.label === 'string') {
|
|
993
|
+
resolvedLabel = elementState.label;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
catch { /* ignore */ }
|
|
998
|
+
}
|
|
999
|
+
// Suggest next action based on what happened
|
|
1000
|
+
let suggestedNext;
|
|
1001
|
+
if (isStuck) {
|
|
1002
|
+
suggestedNext = "Typing had no visible effect - try browser_fill for React/Vue controlled inputs or check validation";
|
|
1003
|
+
}
|
|
1004
|
+
else if (domChanged) {
|
|
1005
|
+
suggestedNext = "Typing successful and page responded - check for validation messages or proceed to next field";
|
|
1006
|
+
}
|
|
1007
|
+
else {
|
|
1008
|
+
suggestedNext = "Typing successful - consider clicking submit or proceeding to next field";
|
|
1009
|
+
}
|
|
1010
|
+
// Read back the verified field value to report accurate length.
|
|
1011
|
+
// Previously this was always text.length, which was misleading when paste failed silently.
|
|
1012
|
+
let verifiedLength = text.length;
|
|
1013
|
+
if (selector && selector !== 'document.activeElement') {
|
|
1014
|
+
try {
|
|
1015
|
+
const { result: vlRes } = await session.Runtime.evaluate({
|
|
1016
|
+
expression: `(function(sel) {
|
|
1017
|
+
var el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
|
|
1018
|
+
if (!el) return -1;
|
|
1019
|
+
var v = el.value !== undefined ? el.value : (el.textContent || '');
|
|
1020
|
+
return (v || '').length;
|
|
1021
|
+
})(${JSON.stringify(selector)})`,
|
|
1022
|
+
returnByValue: true,
|
|
1023
|
+
});
|
|
1024
|
+
if (typeof vlRes?.value === 'number' && vlRes.value >= 0) {
|
|
1025
|
+
verifiedLength = vlRes.value;
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
catch { /* non-fatal */ }
|
|
1029
|
+
}
|
|
1030
|
+
return {
|
|
1031
|
+
success: true,
|
|
1032
|
+
typed_into: resolvedLabel,
|
|
1033
|
+
ref: resolvedRef,
|
|
1034
|
+
length: verifiedLength,
|
|
1035
|
+
execution_ms: executionMs,
|
|
1036
|
+
tab_id: targetId,
|
|
1037
|
+
url: postUrl,
|
|
1038
|
+
title: postTitle,
|
|
1039
|
+
page_map: pageMap,
|
|
1040
|
+
dom_changed: domChanged,
|
|
1041
|
+
stuck_warning: isStuck,
|
|
1042
|
+
stuck_hint: isStuck
|
|
1043
|
+
? (verifiedLength === 0 && text.length > 0
|
|
1044
|
+
? `Field value is still empty after typing — the DOM read-back confirmed nothing was written (length: 0). ` +
|
|
1045
|
+
`This is a controlled input (React/Vue/Angular) that ignores synthetic keyboard events. ` +
|
|
1046
|
+
`Try browser_fill which uses nativeInputValueSetter to bypass framework input guards.`
|
|
1047
|
+
: `Page did not visibly respond to the typed text (DOM unchanged, URL unchanged). ` +
|
|
1048
|
+
`If the input is controlled by React/Vue, try browser_fill instead.`)
|
|
1049
|
+
: undefined,
|
|
1050
|
+
sniper_warning: sniperWarning || undefined,
|
|
1051
|
+
memory_hit: usedMemoryIntent,
|
|
1052
|
+
delta: diff ?? undefined,
|
|
1053
|
+
// SEMANTIC RESPONSE CONTRACT
|
|
1054
|
+
confirmed: !isStuck,
|
|
1055
|
+
strategy: usedMemoryIntent ? "memory_intent"
|
|
1056
|
+
: spaRetryApplied ? `spa-retry:${spaRetryStrategy}`
|
|
1057
|
+
: target.x !== undefined ? "coordinates" : "direct",
|
|
1058
|
+
elementState,
|
|
1059
|
+
suggestedNext
|
|
1060
|
+
};
|
|
1061
|
+
}
|