@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,561 @@
|
|
|
1
|
+
// src/tools/click.ts
|
|
2
|
+
// Priority 2: Sub-10ms Click Path & Predictive Action Queue
|
|
3
|
+
import { buildActionMap, findAllMatches, safeSelector, toMarkdownTable, resolveByAria } from "../cdp/walker.js";
|
|
4
|
+
import { logAction } from "../audit/logger.js";
|
|
5
|
+
import { recordAction, getLoopDiagnostics } from "../intelligence/trail.js";
|
|
6
|
+
import { waitForDOMStable } from "../sensors/guard.js";
|
|
7
|
+
import { waitForNetworkIdle } from "../sensors/wait.js";
|
|
8
|
+
import { ghostMove } from "../overlay/ghost.js";
|
|
9
|
+
import { getCachedActionMap, cacheActionMap } from "../state/cache.js";
|
|
10
|
+
import { storeSnapshot, computeDelta } from "../state/delta.js";
|
|
11
|
+
import { hasOpenDialog } from "./dialog.js";
|
|
12
|
+
import { sessionRegistry } from "../state/session.js";
|
|
13
|
+
import { appendKnowledgeHint } from "../intelligence/hints.js";
|
|
14
|
+
import { sendToExtension } from "../cdp/helpers.js";
|
|
15
|
+
import { labelCache } from "../cdp/label_cache.js";
|
|
16
|
+
import { getBestSelectorForIntent, logMemorySuccess, logMemoryFailure } from "../intelligence/sessionMemory.js";
|
|
17
|
+
import { verifyActionability, quickHitCheck } from "../sensors/actionability.js";
|
|
18
|
+
/** Converts SelectorFingerprint (sessionMemory) to the format verifyActionability expects. */
|
|
19
|
+
function fpToOpts(fingerprint) {
|
|
20
|
+
if (!fingerprint)
|
|
21
|
+
return undefined;
|
|
22
|
+
return { tag: fingerprint.tag, role: fingerprint.role };
|
|
23
|
+
}
|
|
24
|
+
/** Formats an OccluderInfo into a human-readable string for error messages. */
|
|
25
|
+
function describeOccluder(occ) {
|
|
26
|
+
const id = occ.id ? `#${occ.id}` : '';
|
|
27
|
+
const cls = occ.className ? `.${occ.className.split(' ')[0]}` : '';
|
|
28
|
+
const label = occ.ariaLabel ? ` "${occ.ariaLabel}"` : '';
|
|
29
|
+
return `<${occ.tag}${id}${cls}${label}> intercepts pointer events`;
|
|
30
|
+
}
|
|
31
|
+
export async function weaveClick(session, args, config) {
|
|
32
|
+
const { id, label, intent, backendNodeId: directNodeId } = args;
|
|
33
|
+
const { Page, DOM, Input } = session;
|
|
34
|
+
const startMs = Date.now();
|
|
35
|
+
const targetId = sessionRegistry.getActive(config.agentId)?.targetId ?? "default";
|
|
36
|
+
// Track URL before click
|
|
37
|
+
const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
38
|
+
// 1. Resolve Element and Selector
|
|
39
|
+
let map = getCachedActionMap(targetId, initialUrl);
|
|
40
|
+
if (!map) {
|
|
41
|
+
map = await buildActionMap(session, { lite: true }, targetId);
|
|
42
|
+
cacheActionMap(targetId, map);
|
|
43
|
+
}
|
|
44
|
+
await storeSnapshot(targetId, initialUrl, map.elements);
|
|
45
|
+
let element;
|
|
46
|
+
let sniperWarning = "";
|
|
47
|
+
let usedMemoryIntent = false;
|
|
48
|
+
let coords = null;
|
|
49
|
+
const urlObj = new URL(initialUrl);
|
|
50
|
+
const hostname = urlObj.hostname;
|
|
51
|
+
const pathname = urlObj.pathname;
|
|
52
|
+
// FAST PATH: Memory Intent Intercept
|
|
53
|
+
if (intent && !id && !label) {
|
|
54
|
+
const memHit = await getBestSelectorForIntent(hostname, intent, pathname);
|
|
55
|
+
if (memHit) {
|
|
56
|
+
const actCheck = await verifyActionability(session, memHit.selector, {
|
|
57
|
+
fingerprint: fpToOpts(memHit.fingerprint),
|
|
58
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
|
|
59
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
|
|
60
|
+
});
|
|
61
|
+
if (actCheck.status === 'ACTIONABLE') {
|
|
62
|
+
args.id = memHit.selector;
|
|
63
|
+
usedMemoryIntent = true;
|
|
64
|
+
sniperWarning = `(Memory Intercept: Skipped DOM evaluation and used learned selector for intent: '${intent}')`;
|
|
65
|
+
coords = { x: actCheck.x, y: actCheck.y, visible: true };
|
|
66
|
+
element = {
|
|
67
|
+
id: memHit.selector,
|
|
68
|
+
label: intent,
|
|
69
|
+
x: actCheck.x,
|
|
70
|
+
y: actCheck.y,
|
|
71
|
+
selector: memHit.selector,
|
|
72
|
+
role: "button",
|
|
73
|
+
group: "utility",
|
|
74
|
+
depth: 0,
|
|
75
|
+
fingerprint: "memory:" + memHit.selector,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
else if (actCheck.status === 'OCCLUDED') {
|
|
79
|
+
const occ = actCheck.occludedBy;
|
|
80
|
+
// WeaveTab-owned blockers are transparent — retry as actionable if only WT nodes blocked
|
|
81
|
+
if (!occ?.isWeavetabOwned) {
|
|
82
|
+
return {
|
|
83
|
+
success: false,
|
|
84
|
+
error_code: "ELEMENT_OCCLUDED_BY",
|
|
85
|
+
message: `[!] ${describeOccluder(occ)}. Handle the blocking element first.`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// SLOW PATH: If not using Memory Intent Fast-Path
|
|
92
|
+
if (!usedMemoryIntent) {
|
|
93
|
+
const query = label || intent;
|
|
94
|
+
if (args.id) {
|
|
95
|
+
const targetId = args.id;
|
|
96
|
+
element = map.elements.find((el) => el.id === targetId);
|
|
97
|
+
// Fast-fail for stale w:NN refs: if a weavetab ref ID is not in the current map,
|
|
98
|
+
// the DOM mutated since the last browser_map call (e.g. a radio was selected, SPA navigated).
|
|
99
|
+
// Tell the agent explicitly so it knows to refresh — don't silently fall through.
|
|
100
|
+
if (!element && targetId.startsWith("w:")) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error_code: "ELEMENT_NOT_FOUND",
|
|
104
|
+
message: `[!] Ref '${targetId}' is no longer in the current page map — the DOM was updated since browser_map was called. Call browser_map() to get fresh ref IDs.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (!element && (targetId.startsWith("#") || targetId.startsWith(".") || targetId.includes("[") || targetId.includes(">") || targetId.includes(" "))) {
|
|
108
|
+
const actCheck = await verifyActionability(session, targetId, {
|
|
109
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
|
|
110
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
|
|
111
|
+
});
|
|
112
|
+
if (actCheck.status === 'ACTIONABLE') {
|
|
113
|
+
element = {
|
|
114
|
+
id: targetId,
|
|
115
|
+
label: 'selector-match',
|
|
116
|
+
x: actCheck.x,
|
|
117
|
+
y: actCheck.y,
|
|
118
|
+
selector: targetId,
|
|
119
|
+
role: "button",
|
|
120
|
+
group: "utility",
|
|
121
|
+
depth: 0,
|
|
122
|
+
fingerprint: "selector:" + targetId,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
else if (actCheck.status === 'OCCLUDED' && !actCheck.occludedBy?.isWeavetabOwned) {
|
|
126
|
+
return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${targetId} is blocked.` };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
else if (label) {
|
|
131
|
+
const cachedRef = labelCache.get(initialUrl, label);
|
|
132
|
+
if (cachedRef) {
|
|
133
|
+
element = map.elements.find(e => e.id === cachedRef);
|
|
134
|
+
if (element) {
|
|
135
|
+
sniperWarning = `(Cache Hit: Clicked label '${label}' via LRU cache for fast resolution)`;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!element) {
|
|
139
|
+
const matches = findAllMatches(map.elements, label);
|
|
140
|
+
if (matches.length > 0) {
|
|
141
|
+
element = matches[0];
|
|
142
|
+
labelCache.set(initialUrl, label, element.id);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
// Aria fallback
|
|
146
|
+
const ariaEl = await resolveByAria(session, label);
|
|
147
|
+
if (ariaEl) {
|
|
148
|
+
element = { ...ariaEl, role: "button", group: "utility", fingerprint: "aria", depth: 0 };
|
|
149
|
+
labelCache.set(initialUrl, label, element.id);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Could not find any clickable element matching label: '${label}'` };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
else if (intent) {
|
|
158
|
+
const matches = findAllMatches(map.elements, intent);
|
|
159
|
+
if (matches.length > 0) {
|
|
160
|
+
element = matches[0];
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const ariaEl = await resolveByAria(session, intent);
|
|
164
|
+
if (ariaEl) {
|
|
165
|
+
element = { ...ariaEl, role: "button", group: "utility", fingerprint: "aria", depth: 0 };
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Could not resolve intent '${intent}' to any element. Try browser_map.` };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!element && args.x !== undefined && args.y !== undefined) {
|
|
173
|
+
element = {
|
|
174
|
+
id: "coords",
|
|
175
|
+
label: `coords(${args.x},${args.y})`,
|
|
176
|
+
x: args.x,
|
|
177
|
+
y: args.y,
|
|
178
|
+
role: "point",
|
|
179
|
+
group: "utility",
|
|
180
|
+
depth: 0,
|
|
181
|
+
fingerprint: "coords"
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
if (!element && (label || intent)) {
|
|
185
|
+
const ariaMatch = await resolveByAria(session, label || intent || "");
|
|
186
|
+
if (ariaMatch) {
|
|
187
|
+
element = {
|
|
188
|
+
id: ariaMatch.id,
|
|
189
|
+
label: ariaMatch.label,
|
|
190
|
+
x: ariaMatch.x,
|
|
191
|
+
y: ariaMatch.y,
|
|
192
|
+
selector: ariaMatch.selector,
|
|
193
|
+
role: "button",
|
|
194
|
+
group: "utility",
|
|
195
|
+
depth: 0,
|
|
196
|
+
fingerprint: "aria:" + ariaMatch.label,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (!element) {
|
|
201
|
+
if (intent && args.id) {
|
|
202
|
+
await logMemoryFailure(hostname, intent, args.id, "DEAD_SELECTOR", pathname);
|
|
203
|
+
}
|
|
204
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Could not find target` };
|
|
205
|
+
}
|
|
206
|
+
const loopDiag = getLoopDiagnostics("browser_click", element.id);
|
|
207
|
+
if (loopDiag.severity === "critical") {
|
|
208
|
+
return { success: false, error_code: "STUCK_LOOP_WARNING", message: `[!] Loop detected: You have tried to click [${element.id}] ${loopDiag.strikes} times without success. Call browser_reset_loop_counter if this is a new task.` };
|
|
209
|
+
}
|
|
210
|
+
if (loopDiag.severity === "warning") {
|
|
211
|
+
logAction("browser_click", element.id, `loop warning: ${loopDiag.strikes} consecutive strikes on [${element.id}]`);
|
|
212
|
+
}
|
|
213
|
+
// Resolve coordinates for slow-path element — uses full Playwright strategy engine
|
|
214
|
+
if (element.id !== "coords") {
|
|
215
|
+
const selector = safeSelector(element);
|
|
216
|
+
if (selector) {
|
|
217
|
+
const actCheck = await verifyActionability(session, selector, {
|
|
218
|
+
stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
|
|
219
|
+
totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 4,
|
|
220
|
+
});
|
|
221
|
+
if (actCheck.status === 'ACTIONABLE') {
|
|
222
|
+
coords = { x: actCheck.x, y: actCheck.y, visible: true };
|
|
223
|
+
}
|
|
224
|
+
else if (actCheck.status === 'OCCLUDED') {
|
|
225
|
+
if (actCheck.occludedBy?.isWeavetabOwned) {
|
|
226
|
+
// WeaveTab's own overlay — treat as clear, use computed coords
|
|
227
|
+
coords = { x: actCheck.x, y: actCheck.y, visible: true };
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — ${selector} is blocked.` };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (actCheck.status === 'NOT_FOUND') {
|
|
234
|
+
return { success: false, error_code: "ELEMENT_NOT_FOUND", message: `[!] Element ${selector} was in the cached map but no longer exists in the DOM. Run browser_map again to refresh refs.` };
|
|
235
|
+
}
|
|
236
|
+
else if (actCheck.status === 'INVISIBLE') {
|
|
237
|
+
return { success: false, error_code: "ELEMENT_INVISIBLE", message: `[!] Element ${selector} has zero dimensions (hidden or collapsed). Use browser_inspect to check display/visibility, or scroll to reveal it.` };
|
|
238
|
+
}
|
|
239
|
+
else if (actCheck.status === 'FINGERPRINT_MISMATCH') {
|
|
240
|
+
return { success: false, error_code: "FINGERPRINT_MISMATCH", message: `[!] Element ${selector} no longer matches its recorded tag/role fingerprint — the DOM was likely rebuilt. Run browser_map to get fresh refs.` };
|
|
241
|
+
}
|
|
242
|
+
else if (actCheck.status === 'UNSTABLE') {
|
|
243
|
+
// Still unstable after full backoff — use raw cached coords but warn
|
|
244
|
+
if (element.x !== undefined && element.y !== undefined) {
|
|
245
|
+
coords = { x: element.x, y: element.y, visible: true };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (!coords) {
|
|
251
|
+
if (element.x !== undefined && element.y !== undefined) {
|
|
252
|
+
coords = { x: element.x, y: element.y, visible: true };
|
|
253
|
+
}
|
|
254
|
+
else if (args.x !== undefined && args.y !== undefined) {
|
|
255
|
+
coords = { x: args.x, y: args.y, visible: true };
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
return { success: false, error_code: "COORD_RESOLUTION_FAILED", message: `[!] Could not resolve coordinates for ${element.label}` };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let neededScroll = false; // Note: scrolling is now handled primarily inside checkActionability
|
|
263
|
+
const { x, y } = coords;
|
|
264
|
+
// 3. Execute interaction with Human Simulation (Move + Hardware Click)
|
|
265
|
+
// Pre-dispatch hit check (Playwright Step 6) runs before every mouse event.
|
|
266
|
+
const element_ = element;
|
|
267
|
+
async function performClick(cx, cy) {
|
|
268
|
+
await ghostMove(session, cx, cy, config, element_.label);
|
|
269
|
+
sendToExtension(session, { type: 'Weavetab_STATE', cursor: { x: cx, y: cy }, state: 'clicking', ripple: { cx, cy } }).catch(() => { });
|
|
270
|
+
// ── Pre-dispatch hit-target check (Playwright Step 6) ──────────────────
|
|
271
|
+
// Verify the coordinate is still clear right before the event fires.
|
|
272
|
+
// This catches overlays/modals that appeared since verifyActionability ran.
|
|
273
|
+
const targetSel = safeSelector(element_) ?? "";
|
|
274
|
+
if (targetSel) {
|
|
275
|
+
const hitCheck = await quickHitCheck(session, cx, cy, targetSel);
|
|
276
|
+
if (!hitCheck.clear) {
|
|
277
|
+
// Raise to performClick caller so the retry loop can try another alignment
|
|
278
|
+
throw Object.assign(new Error(hitCheck.blockerDescription ?? "hit-target blocked"), { code: "HIT_TARGET_BLOCKED" });
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!config.headless)
|
|
282
|
+
await new Promise(r => setTimeout(r, 30 + Math.random() * 50));
|
|
283
|
+
await Input.dispatchMouseEvent({ type: 'mousePressed', x: cx, y: cy, button: 'left', clickCount: 1, pointerType: 'mouse' });
|
|
284
|
+
await new Promise(r => setTimeout(r, 30 + Math.random() * 40));
|
|
285
|
+
await Input.dispatchMouseEvent({ type: 'mouseReleased', x: cx, y: cy, button: 'left', clickCount: 1, pointerType: 'mouse' });
|
|
286
|
+
}
|
|
287
|
+
let scrollYBefore = 0;
|
|
288
|
+
try {
|
|
289
|
+
const r = await session.Runtime.evaluate({ expression: 'Math.round(window.scrollY)', returnByValue: true });
|
|
290
|
+
scrollYBefore = r?.result?.value || 0;
|
|
291
|
+
}
|
|
292
|
+
catch { /* ignore */ }
|
|
293
|
+
async function isClickStuck(currentUrl) {
|
|
294
|
+
// Check if a dialog was opened by the click immediately. If a dialog is open,
|
|
295
|
+
// the click succeeded in triggering an action. Waiting for DOM stable will
|
|
296
|
+
// just hang for 5-10s because Runtime.evaluate blocks on JS dialogs.
|
|
297
|
+
await new Promise(r => setTimeout(r, 100)); // give dialog time to trigger
|
|
298
|
+
if (hasOpenDialog(session)) {
|
|
299
|
+
return { stuck: false, url: currentUrl, newMap: null, dialog_open: true };
|
|
300
|
+
}
|
|
301
|
+
await Promise.all([
|
|
302
|
+
waitForDOMStable(session, 50, 300),
|
|
303
|
+
waitForNetworkIdle(session, 50, 200).catch(() => { })
|
|
304
|
+
]);
|
|
305
|
+
const finalUrl = (await Page.getFrameTree()).frameTree.frame.url;
|
|
306
|
+
// Fast path: URL changed → definitely not stuck, skip map rebuild
|
|
307
|
+
if (finalUrl !== currentUrl) {
|
|
308
|
+
labelCache.invalidate(currentUrl); // URL changed, invalidate old labels
|
|
309
|
+
return { stuck: false, url: finalUrl, newMap: null };
|
|
310
|
+
}
|
|
311
|
+
// Canvas fast path: if the clicked element is a <canvas>, DOM will never change
|
|
312
|
+
// because canvas is pixel-rendered. Treat as success and guide agent to browser_canvas.
|
|
313
|
+
if (element) {
|
|
314
|
+
const sel = safeSelector(element);
|
|
315
|
+
if (sel) {
|
|
316
|
+
try {
|
|
317
|
+
const { result: canvasCheck } = await session.Runtime.evaluate({
|
|
318
|
+
expression: `(function(s) {
|
|
319
|
+
const el = document.querySelector(s);
|
|
320
|
+
if (!el) return false;
|
|
321
|
+
// Check if the element itself or its closest ancestor is a canvas
|
|
322
|
+
return el.tagName === 'CANVAS' || !!el.closest('canvas');
|
|
323
|
+
})(${JSON.stringify(sel)})`,
|
|
324
|
+
returnByValue: true,
|
|
325
|
+
});
|
|
326
|
+
if (canvasCheck?.value === true) {
|
|
327
|
+
return {
|
|
328
|
+
stuck: false,
|
|
329
|
+
url: finalUrl,
|
|
330
|
+
newMap: null,
|
|
331
|
+
canvas_hint: 'Canvas element clicked. To read chart data or pixel content from this canvas, use browser_canvas with action:"get_image".'
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
catch { /* non-fatal */ }
|
|
336
|
+
}
|
|
337
|
+
// Also check by coordinates — elementFromPoint may return a canvas even if the AX ref is a wrapper
|
|
338
|
+
if (coords) {
|
|
339
|
+
try {
|
|
340
|
+
const { result: coordCanvasCheck } = await session.Runtime.evaluate({
|
|
341
|
+
expression: `(function(x, y) {
|
|
342
|
+
const el = document.elementFromPoint(x, y);
|
|
343
|
+
if (!el) return false;
|
|
344
|
+
return el.tagName === 'CANVAS' || !!el.closest('canvas');
|
|
345
|
+
})(${coords.x}, ${coords.y})`,
|
|
346
|
+
returnByValue: true,
|
|
347
|
+
});
|
|
348
|
+
if (coordCanvasCheck?.value === true) {
|
|
349
|
+
return {
|
|
350
|
+
stuck: false,
|
|
351
|
+
url: finalUrl,
|
|
352
|
+
newMap: null,
|
|
353
|
+
canvas_hint: 'Canvas element clicked via coordinates. To read chart data or pixel content from this canvas, use browser_canvas with action:"get_image".'
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
catch { /* non-fatal */ }
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
let newMap = getCachedActionMap(targetId, finalUrl);
|
|
361
|
+
if (!newMap) {
|
|
362
|
+
newMap = await buildActionMap(session, { lite: true }, targetId);
|
|
363
|
+
cacheActionMap(targetId, newMap);
|
|
364
|
+
}
|
|
365
|
+
const delta = computeDelta(targetId, initialUrl, newMap.elements);
|
|
366
|
+
const changesCount = delta ? delta.added.length + delta.removed.length + delta.changed.length : 0;
|
|
367
|
+
// Check scroll position — same-page anchors scroll without URL/DOM change
|
|
368
|
+
let scrollYAfter = 0;
|
|
369
|
+
try {
|
|
370
|
+
const r = await session.Runtime.evaluate({ expression: 'Math.round(window.scrollY)', returnByValue: true });
|
|
371
|
+
scrollYAfter = r?.result?.value || 0;
|
|
372
|
+
}
|
|
373
|
+
catch { /* ignore */ }
|
|
374
|
+
const scrolled = Math.abs(scrollYAfter - scrollYBefore) > 50;
|
|
375
|
+
// Radio/checkbox: computeDelta doesn't track `checked` attribute changes.
|
|
376
|
+
// Do a targeted check on the clicked element to see if its checked state flipped.
|
|
377
|
+
// Also handles clicking a <label for="..."> — follow the `for` attr to the real input.
|
|
378
|
+
let radioCheckboxFixed = false;
|
|
379
|
+
if (delta !== null && changesCount === 0 && !scrolled && element) {
|
|
380
|
+
const sel = safeSelector(element);
|
|
381
|
+
if (sel) {
|
|
382
|
+
const { result: ckResult } = await session.Runtime.evaluate({
|
|
383
|
+
expression: `(function(sel) {
|
|
384
|
+
var el = document.querySelector(sel);
|
|
385
|
+
if (!el) return null;
|
|
386
|
+
// Check the element itself
|
|
387
|
+
var t = (el.getAttribute('type') || '').toLowerCase();
|
|
388
|
+
if (t === 'radio' || t === 'checkbox') return el.checked;
|
|
389
|
+
// If it's a <label>, follow its 'for' attribute to the referenced input
|
|
390
|
+
if (el.tagName === 'LABEL') {
|
|
391
|
+
var forId = el.getAttribute('for');
|
|
392
|
+
var target = forId ? document.getElementById(forId) : el.querySelector('input');
|
|
393
|
+
if (target) {
|
|
394
|
+
var t2 = (target.getAttribute('type') || '').toLowerCase();
|
|
395
|
+
if (t2 === 'radio' || t2 === 'checkbox') return target.checked;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
})(${JSON.stringify(sel)})`,
|
|
400
|
+
returnByValue: true,
|
|
401
|
+
}).catch(() => ({ result: { value: null } }));
|
|
402
|
+
if (ckResult?.value !== null && ckResult?.value !== undefined) {
|
|
403
|
+
radioCheckboxFixed = true;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ARIA-state change: buttons that toggle a panel/menu/checked state change
|
|
408
|
+
// aria-expanded, aria-selected, aria-pressed, or aria-checked without DOM-tree changes.
|
|
409
|
+
// These are real successful clicks. Detect them before labelling as stuck.
|
|
410
|
+
let ariaStateChanged = false;
|
|
411
|
+
if (delta !== null && changesCount === 0 && !scrolled && !radioCheckboxFixed && element) {
|
|
412
|
+
const sel = safeSelector(element);
|
|
413
|
+
if (sel) {
|
|
414
|
+
const { result: ariaRes } = await session.Runtime.evaluate({
|
|
415
|
+
expression: `(function(sel) {
|
|
416
|
+
const el = document.querySelector(sel);
|
|
417
|
+
if (!el) return null;
|
|
418
|
+
return JSON.stringify({
|
|
419
|
+
expanded: el.getAttribute('aria-expanded'),
|
|
420
|
+
selected: el.getAttribute('aria-selected'),
|
|
421
|
+
pressed: el.getAttribute('aria-pressed'),
|
|
422
|
+
checked: el.getAttribute('aria-checked'),
|
|
423
|
+
current: el.getAttribute('aria-current')
|
|
424
|
+
});
|
|
425
|
+
})(${JSON.stringify(sel)})`,
|
|
426
|
+
returnByValue: true,
|
|
427
|
+
}).catch(() => ({ result: { value: null } }));
|
|
428
|
+
// Any non-null aria state present means element is togglable; count it as changed
|
|
429
|
+
if (ariaRes?.value) {
|
|
430
|
+
try {
|
|
431
|
+
const ariaState = JSON.parse(ariaRes.value);
|
|
432
|
+
ariaStateChanged = Object.values(ariaState).some(v => v !== null);
|
|
433
|
+
}
|
|
434
|
+
catch { /* ignore */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
// We no longer blindly consider "no DOM changes" as stuck because many static elements (tabs, menus, external links, pure JS)
|
|
439
|
+
// do not mutate the DOM predictably. Only treat it as stuck if we explicitly failed to hit the target.
|
|
440
|
+
const stuck = false;
|
|
441
|
+
return { stuck, url: finalUrl, newMap };
|
|
442
|
+
}
|
|
443
|
+
let { x: clickX, y: clickY } = coords;
|
|
444
|
+
let hitTargetBlocked = false;
|
|
445
|
+
try {
|
|
446
|
+
await performClick(clickX, clickY);
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
if (err?.code === "HIT_TARGET_BLOCKED") {
|
|
450
|
+
hitTargetBlocked = true;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
throw err;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
// ── Post-click: get page state (URL, map, DOM delta) ─────────────────────
|
|
457
|
+
// The 'stuck' flag is purely informational — it powers stuck_warning in the
|
|
458
|
+
// result. A dispatched, non-blocked click is NOT retried based on DOM delta
|
|
459
|
+
// alone, because: (a) many real interactions produce no measurable DOM delta
|
|
460
|
+
// (static buttons, CSS-only toggles, anchor scrolls); (b) retrying doubles the
|
|
461
|
+
// click on checkboxes/accordions, net-zeroing the primary action.
|
|
462
|
+
let postCheck = hitTargetBlocked
|
|
463
|
+
? { stuck: true, url: initialUrl, newMap: null }
|
|
464
|
+
: await isClickStuck(initialUrl);
|
|
465
|
+
// SPA JS-click fallback: only when the first click was genuinely hit-blocked
|
|
466
|
+
if (hitTargetBlocked && element_) {
|
|
467
|
+
const sel = safeSelector(element_);
|
|
468
|
+
if (sel) {
|
|
469
|
+
try {
|
|
470
|
+
const { result: isButton } = await session.Runtime.evaluate({
|
|
471
|
+
expression: `(function(s) {
|
|
472
|
+
var el = document.querySelector(s);
|
|
473
|
+
if (!el) return false;
|
|
474
|
+
var tag = el.tagName.toLowerCase();
|
|
475
|
+
var role = (el.getAttribute('role') || '').toLowerCase();
|
|
476
|
+
if (tag === 'button' || tag === 'a' || tag === 'input' || tag === 'label' || tag === 'select') return true;
|
|
477
|
+
if (['button','link','menuitem','menuitemcheckbox','menuitemradio',
|
|
478
|
+
'radio','checkbox','tab','option','treeitem','gridcell','switch'].includes(role)) return true;
|
|
479
|
+
if (el.onclick || el.getAttribute('onclick')) return true;
|
|
480
|
+
if (el.isContentEditable || tag === 'summary') return true;
|
|
481
|
+
return false;
|
|
482
|
+
})(${JSON.stringify(sel)})`,
|
|
483
|
+
returnByValue: true,
|
|
484
|
+
}).catch(() => ({ result: { value: false } }));
|
|
485
|
+
if (isButton?.value) {
|
|
486
|
+
await session.Runtime.evaluate({
|
|
487
|
+
expression: `(function(s) {
|
|
488
|
+
var el = document.querySelector(s);
|
|
489
|
+
if (el) { el.focus(); el.click(); }
|
|
490
|
+
})(${JSON.stringify(sel)})`,
|
|
491
|
+
returnByValue: false,
|
|
492
|
+
}).catch(() => { });
|
|
493
|
+
await new Promise(r => setTimeout(r, 300));
|
|
494
|
+
postCheck = { stuck: false, url: initialUrl, newMap: null };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
catch { /* non-fatal */ }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
const executionMs = Date.now() - startMs;
|
|
501
|
+
const { stuck: isStuck, url: finalUrl, newMap } = postCheck;
|
|
502
|
+
if (neededScroll) {
|
|
503
|
+
try {
|
|
504
|
+
const hostname = new URL(initialUrl).hostname;
|
|
505
|
+
await appendKnowledgeHint(hostname, "elements on this site often need to be scrolled into view before clicking");
|
|
506
|
+
}
|
|
507
|
+
catch { /* ignore */ }
|
|
508
|
+
}
|
|
509
|
+
const stuckHint = postCheck.dialog_open
|
|
510
|
+
? 'A dialog opened. Use browser_dialog to interact with it.'
|
|
511
|
+
: isStuck
|
|
512
|
+
? `Page did not respond to click (no visible DOM change). Try browser_pointer with raw coords (x: ${clickX}, y: ${clickY}) if the element is visible, or browser_eval to click it directly.`
|
|
513
|
+
: undefined;
|
|
514
|
+
// Logging success or action failure
|
|
515
|
+
if (intent && (args.id || element_.selector)) {
|
|
516
|
+
const finalSelector = args.id || element_.selector;
|
|
517
|
+
if (isStuck) {
|
|
518
|
+
await logMemoryFailure(hostname, intent, finalSelector, "ACTION_FAILED", pathname);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
await logMemorySuccess(hostname, intent, finalSelector, {
|
|
522
|
+
role: element_.role,
|
|
523
|
+
classes: element_.className ? element_.className.split(' ') : undefined,
|
|
524
|
+
tag: element_.type, // assuming type roughly maps to tag
|
|
525
|
+
structuralIdiom: element_.structuralIdiom
|
|
526
|
+
}, pathname);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
recordAction({
|
|
530
|
+
timestamp: new Date().toISOString(),
|
|
531
|
+
tool: "browser_click",
|
|
532
|
+
input: { ref: element_?.id ?? args.id ?? args.label, label: element_?.label },
|
|
533
|
+
result: { success: true, stuck: isStuck },
|
|
534
|
+
});
|
|
535
|
+
if (isStuck) {
|
|
536
|
+
return {
|
|
537
|
+
success: false,
|
|
538
|
+
error_code: "CLICK_NO_EFFECT",
|
|
539
|
+
message: "The click executed but resulted in no visible changes to the page. It may have been intercepted by an overlay, or the element might require a different interaction.",
|
|
540
|
+
suggestion: "Try looking for an invisible overlay blocking the click, or try using browser_eval to click it directly.",
|
|
541
|
+
tab_id: targetId,
|
|
542
|
+
url: finalUrl,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
return {
|
|
546
|
+
success: true,
|
|
547
|
+
clicked: element_.label,
|
|
548
|
+
ref: element_.id,
|
|
549
|
+
execution_ms: executionMs,
|
|
550
|
+
tab_id: targetId,
|
|
551
|
+
url: finalUrl,
|
|
552
|
+
title: newMap?.title || "",
|
|
553
|
+
url_changed: finalUrl !== initialUrl,
|
|
554
|
+
...(args.include_map ? { page_map: newMap ? toMarkdownTable(newMap.elements, finalUrl, newMap.title) : "" } : {}),
|
|
555
|
+
dom_changed: true,
|
|
556
|
+
stuck_warning: false,
|
|
557
|
+
...(stuckHint ? { stuck_hint: stuckHint } : {}),
|
|
558
|
+
...(sniperWarning ? { sniper_warning: sniperWarning } : {}),
|
|
559
|
+
...(postCheck.canvas_hint ? { canvas_hint: postCheck.canvas_hint } : {})
|
|
560
|
+
};
|
|
561
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import CDP from "chrome-remote-interface";
|
|
2
|
+
import type { Config } from "../config/loader.js";
|
|
3
|
+
import type { ClickResult, WeaveError } from "./click.js";
|
|
4
|
+
export interface ClickAndWaitOptions {
|
|
5
|
+
/** Element identifier - can be id, label, intent, or selector */
|
|
6
|
+
id?: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
intent?: string;
|
|
9
|
+
/** Direct coordinates bypass */
|
|
10
|
+
x?: number;
|
|
11
|
+
y?: number;
|
|
12
|
+
/** Time to wait after click for page response (ms) */
|
|
13
|
+
waitAfter?: number;
|
|
14
|
+
/** Whether to wait for network idle after click */
|
|
15
|
+
waitForNetwork?: boolean;
|
|
16
|
+
/** Whether to return the page map after click */
|
|
17
|
+
includeMap?: boolean;
|
|
18
|
+
/** Fast mode - skip stability checks */
|
|
19
|
+
fast?: boolean;
|
|
20
|
+
/** Execute in a specific tab instead of active */
|
|
21
|
+
tab_id?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ClickAndWaitResult extends ClickResult {
|
|
24
|
+
/** Time waited after click */
|
|
25
|
+
waited_ms: number;
|
|
26
|
+
/** Whether network was idle after wait */
|
|
27
|
+
network_idle?: boolean;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Consolidated tool that combines element resolution, clicking, and waiting.
|
|
31
|
+
* Eliminates 2-3 tool calls in the common pattern: find → click → wait
|
|
32
|
+
*/
|
|
33
|
+
export declare function weaveClickAndWait(session: CDP.Client, args: ClickAndWaitOptions, config: Config): Promise<ClickAndWaitResult | WeaveError>;
|