@weavetab/mcp 2.5.0-beta.2 → 2.5.0-beta.5

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 (127) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +6 -6
  3. package/LICENSE +216 -631
  4. package/README.md +153 -1074
  5. package/SECURITY.md +33 -22
  6. package/TROUBLESHOOTING.md +2 -2
  7. package/dist/cdp/bridge.js +2 -1
  8. package/dist/cdp/connector.d.ts +2 -0
  9. package/dist/cdp/connector.js +10 -6
  10. package/dist/cdp/helpers.d.ts +4 -0
  11. package/dist/cdp/helpers.js +181 -0
  12. package/dist/cdp/omni_broker.d.ts +1 -1
  13. package/dist/cdp/omni_broker.js +1 -1
  14. package/dist/cdp/walker.d.ts +25 -1
  15. package/dist/cdp/walker.js +304 -7
  16. package/dist/cli/plugin.d.ts +1 -0
  17. package/dist/cli/plugin.js +81 -18
  18. package/dist/cli/updater.d.ts +1 -1
  19. package/dist/cli/updater.js +27 -7
  20. package/dist/config/loader.d.ts +9 -1
  21. package/dist/config/loader.js +42 -3
  22. package/dist/desktop/discovery.d.ts +44 -0
  23. package/dist/desktop/discovery.js +364 -0
  24. package/dist/index.js +137 -21
  25. package/dist/intelligence/marks.d.ts +36 -0
  26. package/dist/intelligence/marks.js +30 -0
  27. package/dist/intelligence/patternCompiler.d.ts +118 -0
  28. package/dist/intelligence/patternCompiler.js +259 -0
  29. package/dist/intelligence/sessionMemory.d.ts +2 -0
  30. package/dist/intelligence/sessionMemory.js +18 -8
  31. package/dist/overlay/injector.js +2 -2
  32. package/dist/plugin/interface.d.ts +88 -5
  33. package/dist/plugin/loader.d.ts +12 -1
  34. package/dist/plugin/loader.js +280 -70
  35. package/dist/plugin/pipeline.d.ts +75 -0
  36. package/dist/plugin/pipeline.js +183 -0
  37. package/dist/plugin/registry.d.ts +2 -1
  38. package/dist/plugin/registry.js +24 -14
  39. package/dist/security/secrets.d.ts +7 -0
  40. package/dist/security/secrets.js +37 -0
  41. package/dist/sensors/actionability.js +1 -2
  42. package/dist/sensors/guard.js +4 -10
  43. package/dist/sensors/mutations.js +12 -0
  44. package/dist/sensors/operation_monitor.js +1 -0
  45. package/dist/server.d.ts +1 -1
  46. package/dist/server.js +453 -178
  47. package/dist/state/agent.d.ts +1 -0
  48. package/dist/state/agent.js +3 -0
  49. package/dist/state/delta.d.ts +12 -4
  50. package/dist/state/delta.js +67 -14
  51. package/dist/state/session.d.ts +6 -0
  52. package/dist/state/session.js +27 -0
  53. package/dist/tools/browser_detect.d.ts +16 -3
  54. package/dist/tools/browser_detect.js +52 -7
  55. package/dist/tools/burst.d.ts +17 -8
  56. package/dist/tools/burst.js +236 -77
  57. package/dist/tools/captcha.js +1 -1
  58. package/dist/tools/checkpoint.d.ts +38 -0
  59. package/dist/tools/checkpoint.js +271 -0
  60. package/dist/tools/click.d.ts +1 -0
  61. package/dist/tools/click.js +74 -70
  62. package/dist/tools/design_extract.js +1 -1
  63. package/dist/tools/eval.js +1 -1
  64. package/dist/tools/fill.d.ts +20 -1
  65. package/dist/tools/fill.js +199 -40
  66. package/dist/tools/find.d.ts +16 -0
  67. package/dist/tools/find.js +67 -44
  68. package/dist/tools/github/index.d.ts +19 -4
  69. package/dist/tools/github/index.js +40 -4
  70. package/dist/tools/highlight.d.ts +6 -2
  71. package/dist/tools/highlight.js +158 -72
  72. package/dist/tools/inspect.js +1 -1
  73. package/dist/tools/key.js +65 -56
  74. package/dist/tools/navigate.d.ts +1 -1
  75. package/dist/tools/navigate.js +34 -39
  76. package/dist/tools/pdf.js +21 -13
  77. package/dist/tools/pointer.d.ts +18 -3
  78. package/dist/tools/pointer.js +89 -1
  79. package/dist/tools/read.d.ts +28 -8
  80. package/dist/tools/read.js +230 -56
  81. package/dist/tools/scrape.js +67 -13
  82. package/dist/tools/screenshot.js +21 -16
  83. package/dist/tools/scroll.js +6 -11
  84. package/dist/tools/select.js +8 -2
  85. package/dist/tools/snapshot.js +3 -34
  86. package/dist/tools/tabs.js +15 -7
  87. package/dist/tools/thought.d.ts +2 -0
  88. package/dist/tools/thought.js +13 -14
  89. package/dist/tools/type.d.ts +2 -0
  90. package/dist/tools/type.js +107 -58
  91. package/dist/tools/type_secret.js +42 -76
  92. package/dist/tools/upload.js +1 -1
  93. package/dist/tools/video_compiler.d.ts +1 -1
  94. package/dist/tools/video_compiler.js +11 -12
  95. package/dist/version.d.ts +5 -0
  96. package/dist/version.js +5 -0
  97. package/docs/DOCUMENTATION.md +37 -41
  98. package/docs/PLUGINS.md +121 -0
  99. package/docs/TOOLS.md +54 -0
  100. package/docs/architecture/blind-injection.md +83 -20
  101. package/docs/architecture/overview.md +49 -49
  102. package/docs/architecture/security.md +8 -7
  103. package/docs/architecture/transport.md +2 -2
  104. package/docs/cli/cli-commands.md +7 -1
  105. package/docs/cli/cli-flags.md +15 -7
  106. package/docs/cli/cli-scripting.md +6 -3
  107. package/docs/configuration/config-locations.md +3 -4
  108. package/docs/configuration/env-variables.md +4 -4
  109. package/docs/configuration/folders.md +55 -54
  110. package/docs/desktop-automation/electron-apps.md +214 -162
  111. package/docs/get-started/introduction.md +60 -60
  112. package/docs/get-started/setup-wizard.md +7 -6
  113. package/docs/intelligence/site-profiles.md +151 -0
  114. package/docs/tools/tools.md +631 -293
  115. package/docs/troubleshooting/trouble-browser.md +1 -1
  116. package/docs/troubleshooting/trouble-loop.md +31 -31
  117. package/ext-dist/background.js +10 -62
  118. package/ext-dist/content.js +333 -920
  119. package/ext-dist/hud/hud.js +9 -9
  120. package/ext-dist/manifest.json +2 -6
  121. package/ext-dist/styles.css +6 -1
  122. package/icon.png +0 -0
  123. package/mcp.json +87 -71
  124. package/package.json +118 -119
  125. package/server.json +5 -5
  126. package/ext-dist/sidepanel/sidepanel.js +0 -25
  127. package/ext-dist/sidepanel.html +0 -547
package/dist/tools/pdf.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { getOutputDir } from "../config/loader.js";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
- import { hideWeaveTabOverlay, restoreWeaveTabOverlay } from "../cdp/helpers.js";
4
+ import { hideWeaveTabOverlay, restoreWeaveTabOverlay, maskSecretFieldsForCapture, restoreSecretFieldsAfterCapture } from "../cdp/helpers.js";
5
5
  export async function browserPdf(session, options, config) {
6
6
  // Always hide the overlay before generating PDF (agent must never see it).
7
7
  // Set hideOverlay: false explicitly to keep overlays visible.
@@ -9,18 +9,26 @@ export async function browserPdf(session, options, config) {
9
9
  if (shouldHide) {
10
10
  await hideWeaveTabOverlay(session);
11
11
  }
12
- const { data } = await session.Page.printToPDF({
13
- landscape: options.landscape ?? false,
14
- scale: options.scale ?? 1.0,
15
- pageRanges: options.pageRanges ?? "",
16
- marginTop: 0.4, // 1cm ish
17
- marginBottom: 0.4,
18
- marginLeft: 0.4,
19
- marginRight: 0.4,
20
- printBackground: true
21
- });
22
- if (shouldHide) {
23
- await restoreWeaveTabOverlay(session);
12
+ await maskSecretFieldsForCapture(session);
13
+ let data;
14
+ try {
15
+ const pdfRes = await session.Page.printToPDF({
16
+ landscape: options.landscape ?? false,
17
+ scale: options.scale ?? 1.0,
18
+ pageRanges: options.pageRanges ?? "",
19
+ marginTop: 0.4, // 1cm ish
20
+ marginBottom: 0.4,
21
+ marginLeft: 0.4,
22
+ marginRight: 0.4,
23
+ printBackground: true
24
+ });
25
+ data = pdfRes.data;
26
+ }
27
+ finally {
28
+ await restoreSecretFieldsAfterCapture(session);
29
+ if (shouldHide) {
30
+ await restoreWeaveTabOverlay(session);
31
+ }
24
32
  }
25
33
  // Use configured PDF directory or default
26
34
  const pdfsDir = getOutputDir("pdfs", config);
@@ -19,8 +19,18 @@ export interface PointerResult {
19
19
  url: string;
20
20
  dom_changed?: boolean;
21
21
  }
22
- export declare function browserPointer(session: CDP.Client, args: {
23
- action: "click" | "double_click" | "hover" | "drag_and_drop";
22
+ export interface PointerStep {
23
+ action?: "hover" | "click" | "double_click";
24
+ ref?: string;
25
+ selector?: string;
26
+ target?: string;
27
+ label?: string;
28
+ x?: number;
29
+ y?: number;
30
+ holdMs?: number;
31
+ }
32
+ export interface PointerArgs {
33
+ action?: "click" | "double_click" | "hover" | "drag_and_drop" | "hover_chain";
24
34
  ref?: string;
25
35
  selector?: string;
26
36
  target?: string;
@@ -32,4 +42,9 @@ export declare function browserPointer(session: CDP.Client, args: {
32
42
  end_y?: number;
33
43
  waitForChild?: string;
34
44
  holdMs?: number;
35
- }, config: Config): Promise<PointerResult>;
45
+ hover_chain?: Array<string | PointerStep>;
46
+ chain?: Array<string | PointerStep>;
47
+ clickFinal?: boolean;
48
+ [key: string]: unknown;
49
+ }
50
+ export declare function browserPointer(session: CDP.Client, args: PointerArgs, config: Config): Promise<PointerResult>;
@@ -117,7 +117,95 @@ async function resolveRefToCoords(session, targetId, ref, currentUrl) {
117
117
  return null;
118
118
  }
119
119
  export async function browserPointer(session, args, config) {
120
- let { action, ref, selector, label, x, y, end_ref, end_x, end_y, waitForChild, holdMs = 300 } = args;
120
+ let { action = "click", ref, selector, label, x, y, end_ref, end_x, end_y, waitForChild, holdMs = 200 } = args;
121
+ // ── HOVER CHAIN / SUBMENU NAVIGATION (Photopea, Figma, Photoshop, Flyout Menus) ──
122
+ const rawChain = args.hover_chain || args.chain;
123
+ if (action === "hover_chain" || (rawChain && rawChain.length > 0)) {
124
+ const chainItems = (rawChain || []).map((step, idx, arr) => {
125
+ if (typeof step === "string") {
126
+ const isLast = idx === arr.length - 1;
127
+ return {
128
+ action: isLast && args.clickFinal !== false ? "click" : "hover",
129
+ ref: step,
130
+ label: step,
131
+ target: step
132
+ };
133
+ }
134
+ return step;
135
+ });
136
+ const startMs = Date.now();
137
+ const { Page, Input } = session;
138
+ let targetId = "default";
139
+ const active = sessionRegistry.getActive(getConfigAgentId(config));
140
+ if (active)
141
+ targetId = active.targetId;
142
+ let lastX = 0, lastY = 0;
143
+ for (let i = 0; i < chainItems.length; i++) {
144
+ const step = chainItems[i];
145
+ const stepTarget = step.ref || step.selector || step.target || step.label;
146
+ let stepCoords = null;
147
+ if (step.x !== undefined && step.y !== undefined) {
148
+ stepCoords = { x: step.x, y: step.y, selector: step.selector || '' };
149
+ }
150
+ else if (stepTarget) {
151
+ stepCoords = await resolveRefToCoords(session, targetId, stepTarget).catch(() => null);
152
+ if (!stepCoords) {
153
+ // Submenu might have rendered after previous hover; quick retry with fresh map
154
+ await new Promise(r => setTimeout(r, 80));
155
+ await buildActionMap(session, { lite: true }, targetId).then(m => cacheActionMap(targetId, m)).catch(() => { });
156
+ stepCoords = await resolveRefToCoords(session, targetId, stepTarget).catch(() => null);
157
+ }
158
+ }
159
+ if (!stepCoords) {
160
+ throw new Error(`[browser_pointer hover_chain] Could not resolve coordinates for chain step [${i}]: '${stepTarget}'`);
161
+ }
162
+ lastX = stepCoords.x;
163
+ lastY = stepCoords.y;
164
+ // Move pointer smoothly to element
165
+ await ghostMove(session, stepCoords.x, stepCoords.y, config, step.label, "fast");
166
+ await Input.dispatchMouseEvent({ type: "mouseMoved", x: stepCoords.x, y: stepCoords.y, pointerType: "mouse" });
167
+ // Dispatch mouseenter/mouseover events for dynamic flyout/hover menus
168
+ if (stepCoords.selector) {
169
+ await session.Runtime.evaluate({
170
+ expression: `(function(sel, cx, cy) {
171
+ const el = document.querySelector(sel) || (window.__Weavetab_shadowQuery ? window.__Weavetab_shadowQuery(sel) : null);
172
+ if (!el) return;
173
+ el.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false, cancelable: true, clientX: cx, clientY: cy }));
174
+ el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, clientX: cx, clientY: cy }));
175
+ el.dispatchEvent(new MouseEvent('mousemove', { bubbles: true, cancelable: true, clientX: cx, clientY: cy }));
176
+ })(${JSON.stringify(stepCoords.selector)}, ${stepCoords.x}, ${stepCoords.y})`,
177
+ returnByValue: false
178
+ }).catch(() => { });
179
+ }
180
+ const stepHold = step.holdMs ?? (i === chainItems.length - 1 ? 40 : 100);
181
+ await new Promise(r => setTimeout(r, stepHold));
182
+ // If action is click (e.g. final step in chain)
183
+ if (step.action === "click") {
184
+ await Input.dispatchMouseEvent({ type: "mousePressed", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 1, pointerType: "mouse" });
185
+ await new Promise(r => setTimeout(r, 40));
186
+ await Input.dispatchMouseEvent({ type: "mouseReleased", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 1, pointerType: "mouse" });
187
+ }
188
+ else if (step.action === "double_click") {
189
+ await Input.dispatchMouseEvent({ type: "mousePressed", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 1, pointerType: "mouse" });
190
+ await Input.dispatchMouseEvent({ type: "mouseReleased", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 1, pointerType: "mouse" });
191
+ await new Promise(r => setTimeout(r, 60));
192
+ await Input.dispatchMouseEvent({ type: "mousePressed", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 2, pointerType: "mouse" });
193
+ await Input.dispatchMouseEvent({ type: "mouseReleased", x: stepCoords.x, y: stepCoords.y, button: "left", clickCount: 2, pointerType: "mouse" });
194
+ }
195
+ }
196
+ const execMs = Date.now() - startMs;
197
+ const { frameTree: postFrameTree } = await Page.getFrameTree().catch(() => ({ frameTree: { frame: { url: '' } } }));
198
+ return {
199
+ success: true,
200
+ action: "hover_chain",
201
+ x: lastX,
202
+ y: lastY,
203
+ execution_ms: execMs,
204
+ tab_id: targetId,
205
+ url: postFrameTree.frame.url,
206
+ dom_changed: true,
207
+ };
208
+ }
121
209
  if (selector && !ref) {
122
210
  ref = selector;
123
211
  }
@@ -4,7 +4,25 @@
4
4
  * Weavetab by fy2ne
5
5
  */
6
6
  import CDP from "chrome-remote-interface";
7
+ import { type MarkEntry } from "../intelligence/marks.js";
7
8
  import type { Config } from "../config/loader.js";
9
+ export interface VisualMark {
10
+ mark: number;
11
+ x: number;
12
+ y: number;
13
+ width: number;
14
+ height: number;
15
+ ref?: string;
16
+ label?: string;
17
+ type?: MarkEntry["type"];
18
+ }
19
+ export interface BrowserMapResult {
20
+ text: string;
21
+ /** Base64 JPEG of the page annotated with numbered SoM badges. Only present in visual mode. */
22
+ imageBase64?: string;
23
+ /** Structured mark list — numbers match badges visible on the screenshot. */
24
+ marks?: VisualMark[];
25
+ }
8
26
  export interface ReadOptions {
9
27
  lite?: boolean;
10
28
  full?: boolean;
@@ -14,17 +32,19 @@ export interface ReadOptions {
14
32
  delta?: boolean;
15
33
  intent?: string;
16
34
  ax_tree?: boolean;
17
- /** Debug: draws colored element borders via extension. The screen glow (blue edge fade) is the standard visual indicator, shown automatically during reading states. */
35
+ /** Debug: draws colored element borders via extension. */
18
36
  show_overlays?: boolean;
19
- /** Force deep shadow DOM traversal (crosses all shadow boundaries to find elements). Auto-enabled when flat query returns sparse results. */
37
+ /** Force deep shadow DOM traversal. */
20
38
  deep?: boolean;
39
+ /** Semantic DOM pruning. Defaults to true to drop non-interactive noise and SVGs. */
40
+ prune?: boolean;
21
41
  /**
22
- * Run one-shot semantic DOM pruning before returning elements.
23
- * Drops hidden elements with no interactive descendants and no known toggle-trigger.
24
- * Replaces SVG icons with [SVG Icon: label] tokens.
25
- * NOTE: distinct from delta — delta diffs across time, prune compresses the current snapshot once.
42
+ * Visual grounding mode (Set-of-Marks).
43
+ * - true → always inject numbered badges + screenshot.
44
+ * - "auto" → activate only when DOM interactive count < 5 (canvas/WebGL pages).
45
+ * Use browser_click({ mark: N }) to interact with numbered marks.
26
46
  */
27
- prune?: boolean;
47
+ visual?: boolean | "auto";
28
48
  }
29
- export declare function browserMap(session: CDP.Client, options: ReadOptions | undefined, targetId: string | undefined, config: Config): Promise<string>;
49
+ export declare function browserMap(session: CDP.Client, options: ReadOptions | undefined, targetId: string | undefined, config: Config): Promise<string | BrowserMapResult>;
30
50
  export { browserMap as weaveRead };
@@ -9,10 +9,13 @@ import { cacheActionMap, getCachedActionMap, getRawCachedActionMap } from "../st
9
9
  import { startReadingSimulation } from "../overlay/ghost.js";
10
10
  import { suppressOverlays, restoreOverlays } from "../cdp/extension.js";
11
11
  import { sendToExtension } from "../cdp/helpers.js";
12
+ import { hideWeaveTabOverlay, restoreWeaveTabOverlay } from "../cdp/helpers.js";
12
13
  import { analyzeVisualFallback } from "../intelligence/vision.js";
14
+ import { marksSet } from "../intelligence/marks.js";
13
15
  import { getConfigAgentId } from "../config/loader.js";
14
- // Cap at 80 elements; form/modal groups are always kept, utility trimmed last.
15
- const MAX_ELEMENTS = 80;
16
+ // Cap at 35 elements by default to keep prompt tokens lean.
17
+ // Form, modal, and primary interactive elements are prioritized first.
18
+ const MAX_ELEMENTS = 35;
16
19
  function withTimeout(p, ms) {
17
20
  return Promise.race([
18
21
  p,
@@ -26,33 +29,121 @@ function formatLite(el, activeElementId, changedIds) {
26
29
  parts.push("disabled");
27
30
  if (activeElementId && el.id === activeElementId)
28
31
  parts.push("FOCUSED");
29
- if (el.viewport && el.viewport !== "visible")
30
- parts.push(`offscreen:${el.viewport}`);
32
+ if (el.viewport && el.viewport !== "visible" && el.viewport !== "in-viewport") {
33
+ parts.push(el.viewport);
34
+ }
31
35
  // ── Value / checked / placeholder ──────────────────────────────────────────
32
36
  const role = (el.role || '').toLowerCase();
33
37
  const type = (el.type || '').toLowerCase();
34
- // Radio / checkbox checked state — critical for quiz forms
38
+ // Radio / checkbox checked state
35
39
  if ((role === 'radio' || role === 'checkbox' || type === 'radio' || type === 'checkbox') && el.value !== undefined) {
36
40
  parts.push(el.value === 'true' || el.value === 'checked' ? '✓ checked' : '○ unchecked');
37
41
  }
38
42
  else if (el.value) {
39
- // Truncate long values to avoid token waste
40
- const v = el.value.length > 60 ? el.value.slice(0, 57) + '…' : el.value;
41
- parts.push(`"${v}"`);
43
+ const v = el.value.length > 45 ? el.value.slice(0, 42) + '…' : el.value;
44
+ parts.push(`val:"${v}"`);
42
45
  }
43
46
  else if (el.placeholder) {
44
- parts.push(`placeholder: "${el.placeholder}"`);
47
+ const p = el.placeholder.length > 35 ? el.placeholder.slice(0, 32) + '…' : el.placeholder;
48
+ parts.push(`ph:"${p}"`);
45
49
  }
46
- // ── Type suffix for inputs (e.g. input[email], input[date]) ────────────────
47
- const typeLabel = (role === 'textbox' || role === 'searchbox' || role === 'combobox') && type
50
+ // ── Type suffix for inputs ──────────────────────────────────────────────────
51
+ const typeLabel = (role === 'textbox' || role === 'searchbox' || role === 'combobox') && type && type !== 'text'
48
52
  ? `/${type}` : '';
49
- // ── Link hint ───────────────────────────────────────────────────────────────
50
- if (role === 'link' && el.hrefType && el.hrefType !== 'same-origin') {
51
- parts.push(el.hrefType);
53
+ // ── Link hint (only if external) ───────────────────────────────────────────
54
+ if (role === 'link' && el.hrefType === 'external') {
55
+ parts.push('ext');
52
56
  }
53
57
  const state = parts.length > 0 ? ` (${parts.join(', ')})` : '';
54
58
  const changed = changedIds?.has(el.id) ? ' ★' : '';
55
- return `[${el.id}] ${el.role}${typeLabel} "${el.label}"${state}${changed}`;
59
+ const label = el.label.length > 50 ? el.label.slice(0, 47) + '…' : el.label;
60
+ return `[${el.id}] ${el.role}${typeLabel} "${label}"${state}${changed}`;
61
+ }
62
+ // ─── 3.0 High-density formatter helpers ──────────────────────────────────────
63
+ /** Collapse a cluster of nav/footer links into a compact summary token. */
64
+ function collapseNavCluster(cluster) {
65
+ if (cluster.length === 0)
66
+ return '';
67
+ if (cluster.length === 1)
68
+ return formatLite(cluster[0]);
69
+ const first = cluster[0].label.slice(0, 20);
70
+ const last = cluster[cluster.length - 1].label.slice(0, 20);
71
+ const ids = `${cluster[0].id}…${cluster[cluster.length - 1].id}`;
72
+ return `[${ids}] ${cluster.length} links: "${first}" … "${last}"`;
73
+ }
74
+ /**
75
+ * Format the main element list using 3.0 semantics:
76
+ * - Active modals hoisted to the top
77
+ * - Items under shared container headings grouped
78
+ * - Nav link clusters (4+ consecutive) collapsed
79
+ */
80
+ function formatMap3(elements, activeElementId, changedIds) {
81
+ const lines = [];
82
+ // ── 1. Hoist active modal elements ───────────────────────────────────────
83
+ const modalEls = elements.filter(e => e.isModal);
84
+ const mainEls = elements.filter(e => !e.isModal);
85
+ if (modalEls.length > 0) {
86
+ const modalTitle = modalEls[0].modalTitle || 'Dialog';
87
+ lines.push(`╔═ ACTIVE DIALOG: "${modalTitle}" ${'═'.repeat(Math.max(0, 30 - modalTitle.length))}╗`);
88
+ for (const el of modalEls) {
89
+ lines.push(' ' + formatLite(el, activeElementId, changedIds));
90
+ }
91
+ lines.push(`╚${'═'.repeat(42)}╝`);
92
+ if (mainEls.length > 0)
93
+ lines.push('');
94
+ }
95
+ // ── 2. Main elements with entity grouping + nav cluster collapsing ────────
96
+ const SECTION_LABEL = {
97
+ navigation: "── NAV ──────────────────",
98
+ main_content: "── MAIN ─────────────────",
99
+ form: "── FORM ─────────────────",
100
+ sidebar: "── SIDEBAR ──────────────",
101
+ modal: "── MODAL ────────────────",
102
+ utility: "── OTHER ────────────────",
103
+ };
104
+ let currentSection = null;
105
+ let currentContainer = null;
106
+ let navCluster = [];
107
+ const flushNavCluster = () => {
108
+ if (navCluster.length >= 4) {
109
+ lines.push(collapseNavCluster(navCluster));
110
+ }
111
+ else {
112
+ for (const ne of navCluster) {
113
+ lines.push(formatLite(ne, activeElementId, changedIds));
114
+ }
115
+ }
116
+ navCluster = [];
117
+ };
118
+ for (const el of mainEls) {
119
+ // Section header
120
+ if (el.group !== currentSection) {
121
+ flushNavCluster();
122
+ currentSection = el.group;
123
+ currentContainer = null;
124
+ lines.push(SECTION_LABEL[currentSection]);
125
+ }
126
+ // Accumulate nav/utility link cluster for potential collapsing
127
+ const isNavLink = (el.group === 'navigation' || el.group === 'utility') && el.role === 'link';
128
+ if (isNavLink) {
129
+ navCluster.push(el);
130
+ continue;
131
+ }
132
+ else {
133
+ flushNavCluster();
134
+ }
135
+ // Container/entity heading
136
+ const ctx = el.containerContext || null;
137
+ if (ctx && ctx !== currentContainer) {
138
+ currentContainer = ctx;
139
+ const header = ctx.length > 55 ? ctx.slice(0, 52) + '…' : ctx;
140
+ lines.push(` ┌ ${header}`);
141
+ }
142
+ const indent = ctx ? ' │ ' : '';
143
+ lines.push(indent + formatLite(el, activeElementId, changedIds));
144
+ }
145
+ flushNavCluster();
146
+ return lines;
56
147
  }
57
148
  /** Full markdown table mode — only used when full:true is explicitly passed */
58
149
  function formatFull(elements, url, title, hint) {
@@ -83,6 +174,99 @@ function formatFull(elements, url, title, hint) {
83
174
  }
84
175
  return lines.join('\n');
85
176
  }
177
+ /**
178
+ * Inject numbered Set-of-Marks badges over all interactive elements,
179
+ * take a screenshot, then strip the badges. Returns structured mark data
180
+ * and the annotated image. Zero page pollution after completion.
181
+ */
182
+ async function buildVisualGrounding(session, targetId, elements) {
183
+ try {
184
+ // Build mark data from DOM elements that have coordinates
185
+ const domMarks = [];
186
+ for (const el of elements) {
187
+ if (el.x === undefined || el.y === undefined)
188
+ continue;
189
+ const w = el.width ?? 80;
190
+ const h = el.height ?? 24;
191
+ domMarks.push({
192
+ mark: domMarks.length + 1,
193
+ x: el.x,
194
+ y: el.y,
195
+ width: w,
196
+ height: h,
197
+ ref: el.id,
198
+ label: el.label || undefined,
199
+ type: el.role === "link" ? "link"
200
+ : el.role === "button" ? "button"
201
+ : (el.role === "textbox" || el.role === "searchbox" || el.role === "combobox") ? "input"
202
+ : el.role === "canvas" ? "canvas_region"
203
+ : "unknown",
204
+ });
205
+ }
206
+ // Also discover canvas elements that may not be in the DOM map
207
+ const { result: canvasRes } = await session.Runtime.evaluate({
208
+ expression: `(function(){
209
+ var canvases = [...document.querySelectorAll('canvas')];
210
+ return JSON.stringify(canvases.map(function(c){
211
+ var r = c.getBoundingClientRect();
212
+ return r.width > 0 && r.height > 0 ? {
213
+ x: Math.round(r.left + r.width/2), y: Math.round(r.top + r.height/2),
214
+ w: Math.round(r.width), h: Math.round(r.height)
215
+ } : null;
216
+ }).filter(Boolean));
217
+ })()`,
218
+ returnByValue: true,
219
+ }).catch(() => ({ result: { value: '[]' } }));
220
+ const extraCanvas = JSON.parse(canvasRes.value ?? '[]');
221
+ // Add canvas regions not already covered by a nearby DOM mark
222
+ for (const c of extraCanvas) {
223
+ const alreadyCovered = domMarks.some(m => Math.abs(m.x - c.x) < 40 && Math.abs(m.y - c.y) < 40);
224
+ if (!alreadyCovered) {
225
+ domMarks.push({ mark: domMarks.length + 1, x: c.x, y: c.y, width: c.w, height: c.h, type: "canvas_region" });
226
+ }
227
+ }
228
+ if (domMarks.length === 0)
229
+ return null;
230
+ // Inject badges — fixed-position, shadow-safe, above everything
231
+ const injectExpr = `(function(marks){
232
+ var BADGE_KEY = '__wt_som_badges__';
233
+ if (window[BADGE_KEY]) window[BADGE_KEY].forEach(function(d){ d.parentNode && d.parentNode.removeChild(d); });
234
+ window[BADGE_KEY] = [];
235
+ marks.forEach(function(m){
236
+ var d = document.createElement('div');
237
+ d.setAttribute('data-wt-som', String(m.mark));
238
+ d.style.cssText = 'position:fixed;left:'+(m.x - 10)+'px;top:'+(m.y - 10)+'px;width:20px;height:20px;z-index:2147483647;pointer-events:none;background:#7c3aed;color:#fff;font:bold 11px/20px monospace;text-align:center;border-radius:50%;box-shadow:0 1px 4px rgba(0,0,0,.5);';
239
+ d.textContent = String(m.mark);
240
+ document.documentElement.appendChild(d);
241
+ window[BADGE_KEY].push(d);
242
+ });
243
+ return true;
244
+ })(${JSON.stringify(domMarks)})`;
245
+ await session.Runtime.evaluate({ expression: injectExpr, returnByValue: true }).catch(() => { });
246
+ // Hide Weavetab overlay so badges are the only overlay in the shot
247
+ await hideWeaveTabOverlay(session).catch(() => { });
248
+ // Capture screenshot
249
+ const { data } = await session.Page.captureScreenshot({ format: 'jpeg', quality: 75 });
250
+ // Restore overlay
251
+ await restoreWeaveTabOverlay(session).catch(() => { });
252
+ // Strip badges immediately
253
+ await session.Runtime.evaluate({
254
+ expression: `(function(){ var b=window['__wt_som_badges__']; if(b) b.forEach(function(d){ d.parentNode&&d.parentNode.removeChild(d); }); window['__wt_som_badges__']=[]; })()`,
255
+ returnByValue: true,
256
+ }).catch(() => { });
257
+ // Persist in mark cache for browser_click({ mark: N }) resolution
258
+ const markMap = new Map();
259
+ for (const m of domMarks) {
260
+ markMap.set(m.mark, { x: m.x, y: m.y, width: m.width, height: m.height, ref: m.ref, label: m.label, type: m.type });
261
+ }
262
+ marksSet(targetId, markMap);
263
+ return { marks: domMarks, imageBase64: data };
264
+ }
265
+ catch (err) {
266
+ logDebug(`[buildVisualGrounding] failed: ${err}`);
267
+ return null;
268
+ }
269
+ }
86
270
  export async function browserMap(session, options = {}, targetId = "default", config) {
87
271
  logDebug(`[browserMap] Initiating read for targetId: ${targetId}, options: ${JSON.stringify(options)}`);
88
272
  // ── Wait for DOM and Visual Stability ──────────────────────────────────────
@@ -174,7 +358,7 @@ export async function browserMap(session, options = {}, targetId = "default", co
174
358
  query: options.query,
175
359
  limit: options.limit,
176
360
  deep: options.deep,
177
- prune: options.prune,
361
+ prune: options.prune !== false,
178
362
  }, targetId), 8000).catch(() => _emptyMap);
179
363
  logDebug(`[browserMap] buildActionMap completed. Elements found: ${map.elements.length}`);
180
364
  // Auto-fallback to AX tree when DOM walker returns sparse or unlabeled-heavy results
@@ -335,19 +519,24 @@ export async function browserMap(session, options = {}, targetId = "default", co
335
519
  if (map.elements.length > 0 && newRefs.length === 0) {
336
520
  newRefs.push(...toRefEntries(map.elements));
337
521
  }
338
- // ── Delta mode ─────────────────────────────────────────────────────────────
339
- if (options.delta) {
522
+ // ── Delta / Auto-Delta mode ────────────────────────────────────────────────
523
+ // If delta is explicit OR if the user is re-reading the same page without full/force options,
524
+ // auto-delta returns only what changed (saving up to 95% prompt tokens).
525
+ const autoDeltaEligible = options.delta === true || (options.delta !== false &&
526
+ !options.full &&
527
+ !options.query &&
528
+ !options.scope);
529
+ if (autoDeltaEligible) {
340
530
  const richDiff = await computeRichDelta(targetId, map.url, map.elements);
341
- if (richDiff) {
342
- if ("delta_timeout" in richDiff) {
343
- // Delta mode timed out — fall through to full read
344
- }
345
- else {
531
+ if (richDiff && !("delta_timeout" in richDiff)) {
532
+ const totalChanges = richDiff.added.length + richDiff.changed.length + richDiff.removed.length;
533
+ // Auto-delta: if 0 changes or <= 15 changes, return the diff directly
534
+ if (options.delta === true || totalChanges <= 15) {
346
535
  const lines = [];
347
536
  const header = `Page: ${map.title || map.url} | ${map.elements.length} elements`;
348
537
  lines.push(header);
349
- if (richDiff.added.length === 0 && richDiff.changed.length === 0 && richDiff.removed.length === 0 && (!richDiff.iframes || richDiff.iframes.length === 0)) {
350
- lines.push("stable — no changes detected");
538
+ if (totalChanges === 0 && (!richDiff.iframes || richDiff.iframes.length === 0)) {
539
+ lines.push("stable — no DOM changes since last map (use existing refs)");
351
540
  }
352
541
  else {
353
542
  for (const r of richDiff.added)
@@ -372,7 +561,6 @@ export async function browserMap(session, options = {}, targetId = "default", co
372
561
  return lines.join("\n");
373
562
  }
374
563
  }
375
- // No prior snapshot → fall through to full read
376
564
  }
377
565
  storeSnapshot(targetId, map.url, newRefs);
378
566
  seedRichSnapshot(targetId, map.url, map.elements);
@@ -428,15 +616,7 @@ export async function browserMap(session, options = {}, targetId = "default", co
428
616
  ? ` (pruned ${beforePruneCount - map.elements.length})`
429
617
  : ""));
430
618
  lines.push("");
431
- // ── Semantic section grouping ────────────────────────────────────────────────
432
- const SECTION_LABEL = {
433
- navigation: "── NAV ──────────────────",
434
- main_content: "── MAIN ─────────────────",
435
- form: "── FORM ─────────────────",
436
- sidebar: "── SIDEBAR ──────────────",
437
- modal: "── MODAL ────────────────",
438
- utility: "── OTHER ────────────────",
439
- };
619
+ // ── Semantic section grouping (3.0) ────────────────────────────────────────
440
620
  let activeElementId;
441
621
  try {
442
622
  const { result } = await session.Runtime.evaluate({
@@ -452,36 +632,30 @@ export async function browserMap(session, options = {}, targetId = "default", co
452
632
  }
453
633
  }
454
634
  catch { }
455
- let currentSection = null;
456
- for (const el of capped) {
457
- if (el.group !== currentSection) {
458
- currentSection = el.group;
459
- lines.push(SECTION_LABEL[currentSection]);
460
- }
461
- lines.push(formatLite(el, activeElementId, changedIds));
462
- }
635
+ lines.push(...formatMap3(capped, activeElementId, changedIds));
463
636
  if (overflowCount > 0) {
464
637
  lines.push(`(+${overflowCount} more below fold — call browser_scroll down to reveal)`);
465
638
  }
466
639
  if (visualFallback)
467
640
  lines.push(visualFallback);
468
- // Context-scoped tool hint — collapse 34-tool surface to ~12 relevant tools.
469
- // Detected from map elements already computed above — zero extra latency.
470
- const hasForm = map.elements.some(e => e.group === "form" || e.role === "input" || e.role === "combobox" || e.role === "checkbox" || e.role === "radio");
471
- const hasSelect = map.elements.some(e => e.role === "combobox" || e.role === "listbox");
472
- const hasCanvas = map.url?.includes("canvas") || false;
473
- const isGithub = map.url?.includes("github.com") ?? false;
474
- const baseTools = ["browser_click", "browser_type", "browser_key", "browser_scroll", "browser_wait", "browser_navigate", "browser_tabs"];
475
- const formTools = hasForm ? ["browser_select", "browser_type_secret", "browser_find"] : [];
476
- const githubTools = isGithub ? ["github_analyze", "github_read", "github_issues", "github_get_pr"] : [];
477
- const canvasTools = hasCanvas ? ["browser_canvas", "browser_screenshot"] : ["browser_screenshot"];
478
- const diagTools = ["browser_inspect", "browser_highlight", "browser_scrape", "browser_burst"];
479
- const contextTools = [...new Set([...baseTools, ...formTools, ...githubTools, ...canvasTools, ...diagTools])];
480
- lines.push(`\n_tools [${contextTools.join(", ")}]`);
481
641
  const output = lines.join("\n");
482
642
  const wordCount = output.split(/\s+/).length;
483
643
  startReadingSimulation(session, wordCount, config);
484
644
  sendToExtension(session, { type: 'Weavetab_STATE', hud: { action: '' } }).catch(() => { });
645
+ // ── Visual Grounding (Set-of-Marks) ────────────────────────────────────────
646
+ // Trigger when: visual:true (always) OR visual:"auto" (DOM element count < 5).
647
+ const shouldVisual = options.visual === true ||
648
+ (options.visual === "auto" && capped.length < 5);
649
+ if (shouldVisual) {
650
+ const vg = await buildVisualGrounding(session, targetId, map.elements);
651
+ if (vg) {
652
+ const markSummary = vg.marks
653
+ .map(m => ` [${m.mark}] ${m.type ?? "unknown"}${m.ref ? ` ${m.ref}` : ""} "${m.label ?? ""}" @(${m.x},${m.y})`)
654
+ .join("\n");
655
+ const annotatedText = output + `\n\n── Visual Marks (use browser_click({ mark: N })) ──\n${markSummary}`;
656
+ return { text: annotatedText, imageBase64: vg.imageBase64, marks: vg.marks };
657
+ }
658
+ }
485
659
  return output;
486
660
  }
487
661
  // Backward-compat alias