@weavetab/mcp 2.5.0-beta.1 → 2.5.0-beta.4

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.
Files changed (156) hide show
  1. package/AGENTS.md +2 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +1 -1
  4. package/README.md +164 -802
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +3 -3
  7. package/dist/cdp/bridge.d.ts +10 -0
  8. package/dist/cdp/bridge.js +285 -78
  9. package/dist/cdp/connector.d.ts +7 -3
  10. package/dist/cdp/connector.js +196 -75
  11. package/dist/cdp/helpers.d.ts +4 -0
  12. package/dist/cdp/helpers.js +181 -0
  13. package/dist/cdp/lock.d.ts +9 -6
  14. package/dist/cdp/lock.js +72 -59
  15. package/dist/cdp/omni_broker.d.ts +2 -1
  16. package/dist/cdp/omni_broker.js +13 -7
  17. package/dist/cdp/profiles.d.ts +10 -0
  18. package/dist/cdp/profiles.js +65 -0
  19. package/dist/cdp/walker.d.ts +25 -1
  20. package/dist/cdp/walker.js +354 -7
  21. package/dist/cli/plugin.d.ts +0 -2
  22. package/dist/cli/plugin.js +51 -696
  23. package/dist/cli/updater.d.ts +1 -1
  24. package/dist/cli/updater.js +27 -7
  25. package/dist/config/loader.d.ts +33 -6
  26. package/dist/config/loader.js +174 -22
  27. package/dist/desktop/discovery.d.ts +44 -0
  28. package/dist/desktop/discovery.js +364 -0
  29. package/dist/index.js +140 -64
  30. package/dist/intelligence/marks.d.ts +36 -0
  31. package/dist/intelligence/marks.js +30 -0
  32. package/dist/intelligence/patternCompiler.d.ts +118 -0
  33. package/dist/intelligence/patternCompiler.js +259 -0
  34. package/dist/intelligence/sessionMemory.d.ts +2 -0
  35. package/dist/intelligence/sessionMemory.js +18 -8
  36. package/dist/overlay/ghost.js +2 -2
  37. package/dist/overlay/injector.js +3 -5
  38. package/dist/plugin/extension_bridge.d.ts +40 -0
  39. package/dist/plugin/extension_bridge.js +158 -0
  40. package/dist/plugin/interface.d.ts +151 -15
  41. package/dist/plugin/loader.d.ts +19 -1
  42. package/dist/plugin/loader.js +331 -63
  43. package/dist/plugin/pipeline.d.ts +75 -0
  44. package/dist/plugin/pipeline.js +183 -0
  45. package/dist/plugin/registry.js +12 -1
  46. package/dist/plugin/storage.d.ts +51 -0
  47. package/dist/plugin/storage.js +164 -0
  48. package/dist/security/secrets.d.ts +1 -0
  49. package/dist/security/secrets.js +13 -0
  50. package/dist/sensors/actionability.js +1 -2
  51. package/dist/sensors/guard.js +4 -10
  52. package/dist/sensors/mutations.js +12 -0
  53. package/dist/sensors/operation_monitor.js +1 -0
  54. package/dist/server.d.ts +1 -1
  55. package/dist/server.js +600 -236
  56. package/dist/state/agent.d.ts +1 -0
  57. package/dist/state/agent.js +3 -0
  58. package/dist/state/delta.d.ts +12 -4
  59. package/dist/state/delta.js +67 -14
  60. package/dist/state/session.d.ts +8 -2
  61. package/dist/state/session.js +60 -13
  62. package/dist/tools/browser_detect.d.ts +16 -2
  63. package/dist/tools/browser_detect.js +54 -6
  64. package/dist/tools/burst.d.ts +17 -7
  65. package/dist/tools/burst.js +240 -83
  66. package/dist/tools/checkpoint.d.ts +38 -0
  67. package/dist/tools/checkpoint.js +271 -0
  68. package/dist/tools/click.d.ts +1 -0
  69. package/dist/tools/click.js +97 -98
  70. package/dist/tools/clipboard.js +31 -24
  71. package/dist/tools/design_extract.js +1 -1
  72. package/dist/tools/eval.js +1 -1
  73. package/dist/tools/fill.d.ts +20 -1
  74. package/dist/tools/fill.js +199 -40
  75. package/dist/tools/find.d.ts +16 -0
  76. package/dist/tools/find.js +67 -44
  77. package/dist/tools/github/index.d.ts +19 -4
  78. package/dist/tools/github/index.js +40 -4
  79. package/dist/tools/highlight.d.ts +6 -2
  80. package/dist/tools/highlight.js +158 -72
  81. package/dist/tools/inspect.js +1 -1
  82. package/dist/tools/key.js +65 -56
  83. package/dist/tools/navigate.d.ts +1 -1
  84. package/dist/tools/navigate.js +57 -44
  85. package/dist/tools/pdf.js +21 -13
  86. package/dist/tools/pointer.d.ts +18 -3
  87. package/dist/tools/pointer.js +89 -1
  88. package/dist/tools/read.d.ts +28 -8
  89. package/dist/tools/read.js +230 -56
  90. package/dist/tools/scrape.js +67 -13
  91. package/dist/tools/screenshot.js +21 -16
  92. package/dist/tools/scroll.js +6 -11
  93. package/dist/tools/select.js +8 -2
  94. package/dist/tools/snapshot.js +3 -34
  95. package/dist/tools/tabs.d.ts +10 -10
  96. package/dist/tools/tabs.js +55 -39
  97. package/dist/tools/thought.d.ts +2 -0
  98. package/dist/tools/thought.js +13 -14
  99. package/dist/tools/type.d.ts +2 -0
  100. package/dist/tools/type.js +109 -60
  101. package/dist/tools/type_secret.js +42 -76
  102. package/dist/tools/upload.js +1 -1
  103. package/dist/tools/video_compiler.d.ts +1 -1
  104. package/dist/tools/video_compiler.js +11 -12
  105. package/dist/version.d.ts +5 -0
  106. package/dist/version.js +5 -0
  107. package/docs/DOCUMENTATION.md +82 -36
  108. package/docs/PLUGINS.md +82 -499
  109. package/docs/TOOLS.md +51 -99
  110. package/docs/architecture/blind-injection.md +83 -0
  111. package/docs/architecture/dual-injection.md +32 -0
  112. package/docs/architecture/overview.md +49 -0
  113. package/docs/architecture/security.md +35 -0
  114. package/docs/architecture/transport.md +37 -0
  115. package/docs/captcha-stealth/captcha-solver.md +75 -0
  116. package/docs/captcha-stealth/stealth-physics.md +60 -0
  117. package/docs/cli/cli-commands.md +44 -0
  118. package/docs/cli/cli-flags.md +20 -0
  119. package/docs/cli/cli-introduction.md +20 -0
  120. package/docs/cli/cli-scripting.md +32 -0
  121. package/docs/cli/g-ide/ide-antigravity.md +24 -0
  122. package/docs/cli/g-ide/ide-claude-code.md +24 -0
  123. package/docs/cli/g-ide/ide-claude-desktop.md +32 -0
  124. package/docs/cli/g-ide/ide-cursor.md +31 -0
  125. package/docs/cli/g-ide/ide-generic.md +34 -0
  126. package/docs/cli/g-ide/ide-opencode.md +24 -0
  127. package/docs/cli/g-ide/ide-vscode-copilot.md +24 -0
  128. package/docs/cli/g-ide/ide-windsurf.md +24 -0
  129. package/docs/configuration/config-locations.md +24 -0
  130. package/docs/configuration/configuration.md +73 -0
  131. package/docs/configuration/env-variables.md +19 -0
  132. package/docs/configuration/folders.md +55 -0
  133. package/docs/desktop-automation/electron-apps.md +217 -0
  134. package/docs/get-started/installation.md +47 -0
  135. package/docs/get-started/introduction.md +60 -0
  136. package/docs/get-started/quickstart.md +54 -0
  137. package/docs/get-started/setup-wizard.md +26 -0
  138. package/docs/intelligence/site-profiles.md +151 -0
  139. package/docs/skills-ecosystem/skills-overview.md +35 -0
  140. package/docs/skills-ecosystem/skills-recipes.md +36 -0
  141. package/docs/tools/tools.md +631 -0
  142. package/docs/troubleshooting/trouble-browser.md +27 -0
  143. package/docs/troubleshooting/trouble-config.md +20 -0
  144. package/docs/troubleshooting/trouble-loop.md +31 -0
  145. package/ext-dist/background.js +10 -62
  146. package/ext-dist/content.js +333 -921
  147. package/ext-dist/hud/hud.js +9 -9
  148. package/ext-dist/manifest.json +2 -6
  149. package/ext-dist/styles.css +6 -1
  150. package/mcp.json +108 -72
  151. package/package.json +14 -9
  152. package/server.json +3 -3
  153. package/docs/DEV_MODE.md +0 -68
  154. package/docs/dev_plugin.md +0 -310
  155. package/ext-dist/sidepanel/sidepanel.js +0 -25
  156. package/ext-dist/sidepanel.html +0 -547
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Weavetab — tools/checkpoint.ts
3
+ * browser_checkpoint: Session time-travel and rollback.
4
+ *
5
+ * Captures and restores named checkpoints of browser state:
6
+ * - Cookies (HttpOnly and Secure via CDP Network)
7
+ * - LocalStorage & SessionStorage
8
+ * - URL, page title, and viewport scroll position
9
+ * - Optional lightweight DOM snapshot
10
+ *
11
+ * Saves tokens and time when trial-and-error automation branches fail.
12
+ *
13
+ * Weavetab by fy2ne
14
+ */
15
+ import { logAction } from "../audit/logger.js";
16
+ import { recordAction } from "../intelligence/trail.js";
17
+ import { clearTabCache } from "../state/cache.js";
18
+ import { sessionRegistry } from "../state/session.js";
19
+ import { getConfigAgentId } from "../config/loader.js";
20
+ // In-memory checkpoint registry keyed by targetId -> name -> CheckpointData
21
+ const checkpointRegistry = new Map();
22
+ export function getCheckpointsForTarget(targetId) {
23
+ let map = checkpointRegistry.get(targetId);
24
+ if (!map) {
25
+ map = new Map();
26
+ checkpointRegistry.set(targetId, map);
27
+ }
28
+ return map;
29
+ }
30
+ export function clearAllCheckpoints() {
31
+ checkpointRegistry.clear();
32
+ }
33
+ export async function browserCheckpoint(session, args, config) {
34
+ let targetId = "default";
35
+ const active = sessionRegistry.getActive(getConfigAgentId(config));
36
+ if (active)
37
+ targetId = active.targetId;
38
+ const targetCheckpoints = getCheckpointsForTarget(targetId);
39
+ // ── ACTION: LIST ─────────────────────────────────────────────────────────────
40
+ if (args.action === "list") {
41
+ const list = Array.from(targetCheckpoints.values()).map((cp) => ({
42
+ name: cp.name,
43
+ url: cp.url,
44
+ title: cp.title,
45
+ timestamp: cp.timestamp,
46
+ cookiesCount: cp.cookies.length,
47
+ localStorageKeys: Object.keys(cp.localStorage).length,
48
+ sessionStorageKeys: Object.keys(cp.sessionStorage).length,
49
+ hasDomSnapshot: !!cp.domSnapshot,
50
+ }));
51
+ return {
52
+ success: true,
53
+ action: "list",
54
+ count: list.length,
55
+ checkpoints: list,
56
+ };
57
+ }
58
+ // ── ACTION: DELETE ───────────────────────────────────────────────────────────
59
+ if (args.action === "delete") {
60
+ if (!args.name) {
61
+ return {
62
+ success: false,
63
+ error_code: "INVALID_ARGS",
64
+ message: "Checkpoint 'name' is required for action 'delete'.",
65
+ };
66
+ }
67
+ const existed = targetCheckpoints.delete(args.name);
68
+ return {
69
+ success: existed,
70
+ action: "delete",
71
+ name: args.name,
72
+ message: existed
73
+ ? `Checkpoint '${args.name}' deleted.`
74
+ : `Checkpoint '${args.name}' not found.`,
75
+ };
76
+ }
77
+ // ── ACTION: SAVE ─────────────────────────────────────────────────────────────
78
+ if (args.action === "save") {
79
+ if (!args.name) {
80
+ return {
81
+ success: false,
82
+ error_code: "INVALID_ARGS",
83
+ message: "Checkpoint 'name' is required for action 'save'.",
84
+ };
85
+ }
86
+ const { Page, Network, Runtime } = session;
87
+ const { frameTree } = await Page.getFrameTree();
88
+ const currentUrl = frameTree.frame.url;
89
+ // 1. Snapshot cookies
90
+ let cookies = [];
91
+ try {
92
+ if (typeof Network.getCookies === "function") {
93
+ const res = await Network.getCookies({ urls: [currentUrl] });
94
+ cookies = res.cookies || [];
95
+ }
96
+ else if (typeof Network.getAllCookies === "function") {
97
+ const res = await Network.getAllCookies();
98
+ cookies = res.cookies || [];
99
+ }
100
+ }
101
+ catch {
102
+ // Cookies best-effort
103
+ }
104
+ // 2. Snapshot LocalStorage, SessionStorage, Scroll, and Title
105
+ let storageData = {
106
+ localStorage: {},
107
+ sessionStorage: {},
108
+ scroll: { x: 0, y: 0 },
109
+ title: "",
110
+ };
111
+ try {
112
+ const evalRes = await Runtime.evaluate({
113
+ expression: `(function() {
114
+ var ls = {};
115
+ try {
116
+ for (var i = 0; i < localStorage.length; i++) {
117
+ var k = localStorage.key(i);
118
+ if (k) ls[k] = localStorage.getItem(k);
119
+ }
120
+ } catch(e) {}
121
+ var ss = {};
122
+ try {
123
+ for (var j = 0; j < sessionStorage.length; j++) {
124
+ var sk = sessionStorage.key(j);
125
+ if (sk) ss[sk] = sessionStorage.getItem(sk);
126
+ }
127
+ } catch(e) {}
128
+ return {
129
+ localStorage: ls,
130
+ sessionStorage: ss,
131
+ scroll: { x: window.scrollX || 0, y: window.scrollY || 0 },
132
+ title: document.title || ""
133
+ };
134
+ })()`,
135
+ returnByValue: true,
136
+ });
137
+ if (evalRes?.result?.value) {
138
+ storageData = evalRes.result.value;
139
+ }
140
+ }
141
+ catch {
142
+ // Runtime best-effort
143
+ }
144
+ // 3. Optional lightweight DOM capture
145
+ let domSnapshot;
146
+ if (args.includeDom) {
147
+ try {
148
+ const domRes = await Runtime.evaluate({
149
+ expression: `document.documentElement.outerHTML.slice(0, 100000)`,
150
+ returnByValue: true,
151
+ });
152
+ domSnapshot = domRes?.result?.value;
153
+ }
154
+ catch {
155
+ // ignore
156
+ }
157
+ }
158
+ const checkpoint = {
159
+ name: args.name,
160
+ url: currentUrl,
161
+ title: storageData.title,
162
+ timestamp: new Date().toISOString(),
163
+ cookies,
164
+ localStorage: storageData.localStorage,
165
+ sessionStorage: storageData.sessionStorage,
166
+ scroll: storageData.scroll,
167
+ domSnapshot,
168
+ };
169
+ targetCheckpoints.set(args.name, checkpoint);
170
+ logAction("browser_checkpoint", args.name, `Saved checkpoint at ${currentUrl}`);
171
+ recordAction({
172
+ timestamp: checkpoint.timestamp,
173
+ tool: "browser_checkpoint",
174
+ input: { action: "save", name: args.name, url: currentUrl },
175
+ result: { success: true },
176
+ });
177
+ return {
178
+ success: true,
179
+ action: "save",
180
+ name: args.name,
181
+ url: currentUrl,
182
+ title: storageData.title,
183
+ timestamp: checkpoint.timestamp,
184
+ cookiesCount: cookies.length,
185
+ localStorageKeysCount: Object.keys(storageData.localStorage).length,
186
+ sessionStorageKeysCount: Object.keys(storageData.sessionStorage).length,
187
+ };
188
+ }
189
+ // ── ACTION: RESTORE ──────────────────────────────────────────────────────────
190
+ if (args.action === "restore") {
191
+ if (!args.name) {
192
+ return {
193
+ success: false,
194
+ error_code: "INVALID_ARGS",
195
+ message: "Checkpoint 'name' is required for action 'restore'.",
196
+ };
197
+ }
198
+ const checkpoint = targetCheckpoints.get(args.name);
199
+ if (!checkpoint) {
200
+ return {
201
+ success: false,
202
+ error_code: "CHECKPOINT_NOT_FOUND",
203
+ message: `No checkpoint found with name '${args.name}'. Use action: 'list' to see available checkpoints.`,
204
+ };
205
+ }
206
+ const { Page, Network, Runtime } = session;
207
+ const { frameTree } = await Page.getFrameTree();
208
+ const currentUrl = frameTree.frame.url;
209
+ // 1. Navigate to checkpoint URL if different
210
+ if (currentUrl !== checkpoint.url) {
211
+ await Page.navigate({ url: checkpoint.url });
212
+ await Page.loadEventFired().catch(() => { });
213
+ await new Promise((r) => setTimeout(r, 400));
214
+ }
215
+ // 2. Restore Cookies
216
+ if (checkpoint.cookies && checkpoint.cookies.length > 0) {
217
+ try {
218
+ if (typeof Network.setCookies === "function") {
219
+ await Network.setCookies({ cookies: checkpoint.cookies });
220
+ }
221
+ }
222
+ catch {
223
+ // Cookies best-effort
224
+ }
225
+ }
226
+ // 3. Restore LocalStorage, SessionStorage, and Scroll position
227
+ try {
228
+ await Runtime.evaluate({
229
+ expression: `(function(ls, ss, scroll) {
230
+ try {
231
+ localStorage.clear();
232
+ for (var k in ls) { localStorage.setItem(k, ls[k]); }
233
+ } catch(e) {}
234
+ try {
235
+ sessionStorage.clear();
236
+ for (var sk in ss) { sessionStorage.setItem(sk, ss[sk]); }
237
+ } catch(e) {}
238
+ if (scroll) {
239
+ window.scrollTo(scroll.x, scroll.y);
240
+ }
241
+ })(${JSON.stringify(checkpoint.localStorage)}, ${JSON.stringify(checkpoint.sessionStorage)}, ${JSON.stringify(checkpoint.scroll)})`,
242
+ returnByValue: false,
243
+ });
244
+ }
245
+ catch {
246
+ // Storage best-effort
247
+ }
248
+ // Invalidate cached action map for this target so fresh refs get generated
249
+ clearTabCache(targetId);
250
+ logAction("browser_checkpoint", args.name, `Restored checkpoint to ${checkpoint.url}`);
251
+ recordAction({
252
+ timestamp: new Date().toISOString(),
253
+ tool: "browser_checkpoint",
254
+ input: { action: "restore", name: args.name, targetUrl: checkpoint.url },
255
+ result: { success: true },
256
+ });
257
+ return {
258
+ success: true,
259
+ action: "restore",
260
+ name: args.name,
261
+ url: checkpoint.url,
262
+ title: checkpoint.title,
263
+ restoredTimestamp: checkpoint.timestamp,
264
+ };
265
+ }
266
+ return {
267
+ success: false,
268
+ error_code: "INVALID_ACTION",
269
+ message: `Unsupported action '${args.action}'. Supported actions: save, restore, list, delete.`,
270
+ };
271
+ }
@@ -33,4 +33,5 @@ export declare function weaveClick(session: CDP.Client, args: {
33
33
  x?: number;
34
34
  y?: number;
35
35
  include_map?: boolean;
36
+ mark?: number;
36
37
  }, config: Config): Promise<ClickResult | WeaveError>;
@@ -1,6 +1,6 @@
1
1
  // src/tools/click.ts
2
2
  // Priority 2: Sub-10ms Click Path & Predictive Action Queue
3
- import { buildActionMap, findAllMatches, safeSelector, toMarkdownTable, resolveByAria } from "../cdp/walker.js";
3
+ import { buildActionMap, findAllMatches, safeSelector, toMarkdownTable, resolveByAria, recoverDetachedRef } from "../cdp/walker.js";
4
4
  import { logAction } from "../audit/logger.js";
5
5
  import { recordAction, getLoopDiagnostics } from "../intelligence/trail.js";
6
6
  import { waitForDOMStable } from "../sensors/guard.js";
@@ -15,6 +15,7 @@ import { sendToExtension } from "../cdp/helpers.js";
15
15
  import { labelCache } from "../cdp/label_cache.js";
16
16
  import { getBestSelectorForIntent, logMemorySuccess, logMemoryFailure } from "../intelligence/sessionMemory.js";
17
17
  import { verifyActionability, quickHitCheck } from "../sensors/actionability.js";
18
+ import { marksGet } from "../intelligence/marks.js";
18
19
  /** Converts SelectorFingerprint (sessionMemory) to the format verifyActionability expects. */
19
20
  function fpToOpts(fingerprint) {
20
21
  if (!fingerprint)
@@ -32,7 +33,38 @@ export async function weaveClick(session, args, config) {
32
33
  const { id, label, intent, backendNodeId: directNodeId } = args;
33
34
  const { Page, DOM, Input } = session;
34
35
  const startMs = Date.now();
35
- const targetId = sessionRegistry.getActive(config.agentId)?.targetId ?? "default";
36
+ const clientMatch = sessionRegistry.getAll().find(c => c.session === session);
37
+ const targetId = clientMatch?.targetId ?? session.targetId ?? "default";
38
+ // ── MARK FAST PATH: resolve by SoM mark number ─────────────────────────────
39
+ // Highest priority — zero DOM walk, straight coordinate dispatch.
40
+ if (args.mark !== undefined) {
41
+ const entry = marksGet(targetId, args.mark);
42
+ if (!entry) {
43
+ return {
44
+ success: false,
45
+ error_code: "MARK_NOT_FOUND",
46
+ message: `[!] Mark [${args.mark}] not found. Call browser_map({ visual: true }) or browser_map({ visual: "auto" }) first to generate marks.`,
47
+ };
48
+ }
49
+ // Ghost-move to coordinates, then dispatch hardware click
50
+ await ghostMove(session, entry.x, entry.y, config).catch(() => { });
51
+ await Input.dispatchMouseEvent({ type: "mousePressed", x: entry.x, y: entry.y, button: "left", clickCount: 1 });
52
+ await Input.dispatchMouseEvent({ type: "mouseReleased", x: entry.x, y: entry.y, button: "left", clickCount: 1 });
53
+ const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
54
+ logAction("browser_click", initialUrl, `mark:${args.mark} @(${entry.x},${entry.y})`);
55
+ return {
56
+ success: true,
57
+ clicked: entry.label ?? `mark:${args.mark}`,
58
+ ref: entry.ref ?? `mark:${args.mark}`,
59
+ execution_ms: Date.now() - startMs,
60
+ tab_id: targetId,
61
+ url: initialUrl,
62
+ title: "",
63
+ url_changed: false,
64
+ dom_changed: false,
65
+ stuck_warning: false,
66
+ };
67
+ }
36
68
  // Track URL before click
37
69
  const initialUrl = (await Page.getFrameTree()).frameTree.frame.url;
38
70
  // 1. Resolve Element and Selector
@@ -55,8 +87,8 @@ export async function weaveClick(session, args, config) {
55
87
  if (memHit) {
56
88
  const actCheck = await verifyActionability(session, memHit.selector, {
57
89
  fingerprint: fpToOpts(memHit.fingerprint),
58
- stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
59
- totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
90
+ stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
91
+ totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
60
92
  });
61
93
  if (actCheck.status === 'ACTIONABLE') {
62
94
  args.id = memHit.selector;
@@ -92,38 +124,50 @@ export async function weaveClick(session, args, config) {
92
124
  if (!usedMemoryIntent) {
93
125
  const query = label || intent;
94
126
  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:")) {
127
+ const reqId = args.id;
128
+ element = map.elements.find((el) => el.id === reqId);
129
+ // If not found in cache, rebuild map once to catch recent DOM updates or cache misses
130
+ if (!element && reqId.startsWith("w:")) {
131
+ map = await buildActionMap(session, { lite: true }, targetId);
132
+ cacheActionMap(targetId, map);
133
+ element = map.elements.find((el) => el.id === reqId);
134
+ }
135
+ // If still not found, trigger the SPA auto-recovery heuristic before hard-failing
136
+ if (!element && reqId.startsWith("w:")) {
137
+ const recovery = await recoverDetachedRef(session, targetId, reqId);
138
+ if (recovery.recovered && recovery.element) {
139
+ element = recovery.element;
140
+ sniperWarning = recovery.warning || "";
141
+ }
142
+ }
143
+ // Fast-fail if still not found
144
+ if (!element && reqId.startsWith("w:")) {
101
145
  return {
102
146
  success: false,
103
147
  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.`,
148
+ message: `[!] Ref '${reqId}' 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
149
  };
106
150
  }
107
- if (!element && (targetId.startsWith("#") || targetId.startsWith(".") || targetId.includes("[") || targetId.includes(">") || targetId.includes(" "))) {
108
- const actCheck = await verifyActionability(session, targetId, {
151
+ if (!element && (reqId.startsWith("#") || reqId.startsWith(".") || reqId.includes("[") || reqId.includes(">") || reqId.includes(" "))) {
152
+ const actCheck = await verifyActionability(session, reqId, {
109
153
  stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
110
154
  totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 3,
111
155
  });
112
156
  if (actCheck.status === 'ACTIONABLE') {
113
157
  element = {
114
- id: targetId,
158
+ id: reqId,
115
159
  label: 'selector-match',
116
160
  x: actCheck.x,
117
161
  y: actCheck.y,
118
- selector: targetId,
162
+ selector: reqId,
119
163
  role: "button",
120
164
  group: "utility",
121
165
  depth: 0,
122
- fingerprint: "selector:" + targetId,
166
+ fingerprint: "selector:" + reqId,
123
167
  };
124
168
  }
125
169
  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.` };
170
+ return { success: false, error_code: "ELEMENT_OCCLUDED_BY", message: `[!] ${describeOccluder(actCheck.occludedBy)} — element ${reqId} is blocked.` };
127
171
  }
128
172
  }
129
173
  }
@@ -205,7 +249,7 @@ export async function weaveClick(session, args, config) {
205
249
  }
206
250
  const loopDiag = getLoopDiagnostics("browser_click", element.id);
207
251
  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.` };
252
+ return { success: false, error_code: "STUCK_LOOP_WARNING", message: `[!] Loop detected: You have tried to click [${element.id}] ${loopDiag.strikes} times without success. Run browser_map({ force: true }) if this is a new task.` };
209
253
  }
210
254
  if (loopDiag.severity === "warning") {
211
255
  logAction("browser_click", element.id, `loop warning: ${loopDiag.strikes} consecutive strikes on [${element.id}]`);
@@ -215,8 +259,8 @@ export async function weaveClick(session, args, config) {
215
259
  const selector = safeSelector(element);
216
260
  if (selector) {
217
261
  const actCheck = await verifyActionability(session, selector, {
218
- stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 400,
219
- totalTimeoutMs: (config.actionabilityTimeoutMs ?? 400) * 4,
262
+ stabilityTimeoutMs: config.actionabilityTimeoutMs ?? 200,
263
+ totalTimeoutMs: (config.actionabilityTimeoutMs ?? 200) * 3,
220
264
  });
221
265
  if (actCheck.status === 'ACTIONABLE') {
222
266
  coords = { x: actCheck.x, y: actCheck.y, visible: true };
@@ -231,7 +275,20 @@ export async function weaveClick(session, args, config) {
231
275
  }
232
276
  }
233
277
  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.` };
278
+ if (element.id.startsWith("w:")) {
279
+ const recovery = await recoverDetachedRef(session, targetId, element.id, element);
280
+ if (recovery.recovered && recovery.element) {
281
+ element = recovery.element;
282
+ coords = { x: element.x, y: element.y, visible: true };
283
+ sniperWarning = recovery.warning || "";
284
+ }
285
+ else {
286
+ 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.` };
287
+ }
288
+ }
289
+ else {
290
+ 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.` };
291
+ }
235
292
  }
236
293
  else if (actCheck.status === 'INVISIBLE') {
237
294
  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.` };
@@ -267,15 +324,15 @@ export async function weaveClick(session, args, config) {
267
324
  async function performClick(cx, cy) {
268
325
  await ghostMove(session, cx, cy, config, element_.label);
269
326
  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.
327
+ // ── Pre-dispatch hit-target check (lightweight) ─────────────────────────
328
+ // verifyActionability already confirmed actionable; this is a last-chance race guard.
329
+ // We do NOT throw here — transient tooltips/overlays cause false positives.
330
+ // If genuinely blocked, the post-click stuck detection handles the fallback.
273
331
  const targetSel = safeSelector(element_) ?? "";
274
332
  if (targetSel) {
275
333
  const hitCheck = await quickHitCheck(session, cx, cy, targetSel);
276
334
  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" });
335
+ logAction("browser_click", targetSel, `[quickHitCheck] soft-blocked by ${hitCheck.blockerDescription ?? "unknown"} — proceeding anyway`);
279
336
  }
280
337
  }
281
338
  if (!config.headless)
@@ -294,13 +351,13 @@ export async function weaveClick(session, args, config) {
294
351
  // Check if a dialog was opened by the click immediately. If a dialog is open,
295
352
  // the click succeeded in triggering an action. Waiting for DOM stable will
296
353
  // 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
354
+ await new Promise(r => setTimeout(r, 30)); // give dialog time to trigger
298
355
  if (hasOpenDialog(session)) {
299
356
  return { stuck: false, url: currentUrl, newMap: null, dialog_open: true };
300
357
  }
301
358
  await Promise.all([
302
- waitForDOMStable(session, 50, 300),
303
- waitForNetworkIdle(session, 50, 200).catch(() => { })
359
+ waitForDOMStable(session, 20, 150),
360
+ waitForNetworkIdle(session, 30, 150).catch(() => { })
304
361
  ]);
305
362
  const finalUrl = (await Page.getFrameTree()).frameTree.frame.url;
306
363
  // Fast path: URL changed → definitely not stuck, skip map rebuild
@@ -440,63 +497,15 @@ export async function weaveClick(session, args, config) {
440
497
  const stuck = false;
441
498
  return { stuck, url: finalUrl, newMap };
442
499
  }
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
- }
500
+ const { x: clickX, y: clickY } = coords;
501
+ await performClick(clickX, clickY);
456
502
  // ── Post-click: get page state (URL, map, DOM delta) ─────────────────────
457
503
  // 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
- }
504
+ // result. A dispatched click is NOT retried based on DOM delta alone, because:
505
+ // (a) many real interactions produce no measurable DOM delta (static buttons,
506
+ // CSS-only toggles, anchor scrolls);
507
+ // (b) retrying doubles the click on checkboxes/accordions, net-zeroing it.
508
+ const postCheck = await isClickStuck(initialUrl);
500
509
  const executionMs = Date.now() - startMs;
501
510
  const { stuck: isStuck, url: finalUrl, newMap } = postCheck;
502
511
  if (neededScroll) {
@@ -532,28 +541,18 @@ export async function weaveClick(session, args, config) {
532
541
  input: { ref: element_?.id ?? args.id ?? args.label, label: element_?.label },
533
542
  result: { success: true, stuck: isStuck },
534
543
  });
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
544
  return {
546
545
  success: true,
547
- clicked: element_.label,
548
- ref: element_.id,
546
+ clicked: element_?.label || (coords ? `coords(${coords.x},${coords.y})` : (args.label || args.id || "")),
547
+ ref: element_?.id || (coords ? "coords" : (args.id || "unknown")),
549
548
  execution_ms: executionMs,
550
549
  tab_id: targetId,
551
550
  url: finalUrl,
552
551
  title: newMap?.title || "",
553
552
  url_changed: finalUrl !== initialUrl,
554
553
  ...(args.include_map ? { page_map: newMap ? toMarkdownTable(newMap.elements, finalUrl, newMap.title) : "" } : {}),
555
- dom_changed: true,
556
- stuck_warning: false,
554
+ dom_changed: !isStuck,
555
+ stuck_warning: isStuck,
557
556
  ...(stuckHint ? { stuck_hint: stuckHint } : {}),
558
557
  ...(sniperWarning ? { sniper_warning: sniperWarning } : {}),
559
558
  ...(postCheck.canvas_hint ? { canvas_hint: postCheck.canvas_hint } : {})
@@ -69,32 +69,30 @@ export async function browserClipboard(session, options, _config) {
69
69
  throw new Error("Failed to write image to clipboard: " + (val || "unknown"));
70
70
  }
71
71
  // Text-only write
72
- const res = await session.Runtime.evaluate({
73
- expression: `(async function() {
74
- try {
75
- ${secureCtx ? `await navigator.clipboard.writeText(${JSON.stringify(text)});` : `throw new Error('non-secure');`}
76
- return true;
77
- } catch(e) {
78
- const ta = document.createElement('textarea');
79
- ta.value = ${JSON.stringify(text)};
80
- ta.style.position = 'fixed'; ta.style.opacity = '0';
81
- document.body.appendChild(ta);
82
- ta.focus(); ta.select();
83
- const ok = document.execCommand('copy');
84
- document.body.removeChild(ta);
85
- return ok;
86
- }
87
- })()`,
88
- awaitPromise: true,
89
- returnByValue: true
90
- });
91
- if (res.result?.value === true) {
72
+ if (hasText && !hasImage) {
73
+ try {
74
+ const clipboardy = (await import("clipboardy")).default;
75
+ await clipboardy.write(text);
76
+ return { success: true, message: "Copied to clipboard" };
77
+ }
78
+ catch { /* fallback to browser API */ }
79
+ const res = await session.Runtime.evaluate({
80
+ expression: `(async function() {
81
+ try {
82
+ ${secureCtx ? `await navigator.clipboard.writeText(${JSON.stringify(text)});` : `throw new Error('non-secure');`}
83
+ return true;
84
+ } catch(e) {
85
+ return false;
86
+ }
87
+ })()`,
88
+ awaitPromise: true,
89
+ returnByValue: true
90
+ });
91
+ if (res.result?.value === true) {
92
+ return { success: true, message: "Copied to clipboard" };
93
+ }
92
94
  return { success: true, message: "Copied to clipboard" };
93
95
  }
94
- if (!secureCtx) {
95
- throw new Error("browser_clipboard: write failed on a non-secure context (file:// or http://). The Clipboard API is unavailable here; navigate to an https page and retry.");
96
- }
97
- throw new Error("Failed to write to clipboard (page may not be focused)");
98
96
  }
99
97
  // ── Read action ──────────────────────────────────────────────────
100
98
  // 4-layer clipboard read strategy:
@@ -147,6 +145,15 @@ export async function browserClipboard(session, options, _config) {
147
145
  });
148
146
  return valRes?.value || null;
149
147
  }
148
+ // Layer 0: Direct OS system clipboard read via clipboardy
149
+ try {
150
+ const clipboardy = (await import("clipboardy")).default;
151
+ const sysText = await clipboardy.read();
152
+ if (sysText && sysText.trim().length > 0) {
153
+ return { success: true, text: sysText, message: "Read from clipboard" };
154
+ }
155
+ }
156
+ catch { }
150
157
  await grantUserActivation();
151
158
  const secureCtx = await isSecureContext();
152
159
  const originRes = await session.Runtime.evaluate({ expression: "window.location.origin" }).catch(() => null);