@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.
- package/AGENTS.md +1 -1
- package/ARCHITECTURE.md +6 -6
- package/LICENSE +216 -631
- package/README.md +153 -1074
- package/SECURITY.md +33 -22
- package/TROUBLESHOOTING.md +2 -2
- package/dist/cdp/bridge.js +2 -1
- package/dist/cdp/connector.d.ts +2 -0
- package/dist/cdp/connector.js +10 -6
- package/dist/cdp/helpers.d.ts +4 -0
- package/dist/cdp/helpers.js +181 -0
- package/dist/cdp/omni_broker.d.ts +1 -1
- package/dist/cdp/omni_broker.js +1 -1
- package/dist/cdp/walker.d.ts +25 -1
- package/dist/cdp/walker.js +304 -7
- package/dist/cli/plugin.d.ts +1 -0
- package/dist/cli/plugin.js +81 -18
- package/dist/cli/updater.d.ts +1 -1
- package/dist/cli/updater.js +27 -7
- package/dist/config/loader.d.ts +9 -1
- package/dist/config/loader.js +42 -3
- package/dist/desktop/discovery.d.ts +44 -0
- package/dist/desktop/discovery.js +364 -0
- package/dist/index.js +137 -21
- package/dist/intelligence/marks.d.ts +36 -0
- package/dist/intelligence/marks.js +30 -0
- package/dist/intelligence/patternCompiler.d.ts +118 -0
- package/dist/intelligence/patternCompiler.js +259 -0
- package/dist/intelligence/sessionMemory.d.ts +2 -0
- package/dist/intelligence/sessionMemory.js +18 -8
- package/dist/overlay/injector.js +2 -2
- package/dist/plugin/interface.d.ts +88 -5
- package/dist/plugin/loader.d.ts +12 -1
- package/dist/plugin/loader.js +280 -70
- package/dist/plugin/pipeline.d.ts +75 -0
- package/dist/plugin/pipeline.js +183 -0
- package/dist/plugin/registry.d.ts +2 -1
- package/dist/plugin/registry.js +24 -14
- package/dist/security/secrets.d.ts +7 -0
- package/dist/security/secrets.js +37 -0
- package/dist/sensors/actionability.js +1 -2
- package/dist/sensors/guard.js +4 -10
- package/dist/sensors/mutations.js +12 -0
- package/dist/sensors/operation_monitor.js +1 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +453 -178
- package/dist/state/agent.d.ts +1 -0
- package/dist/state/agent.js +3 -0
- package/dist/state/delta.d.ts +12 -4
- package/dist/state/delta.js +67 -14
- package/dist/state/session.d.ts +6 -0
- package/dist/state/session.js +27 -0
- package/dist/tools/browser_detect.d.ts +16 -3
- package/dist/tools/browser_detect.js +52 -7
- package/dist/tools/burst.d.ts +17 -8
- package/dist/tools/burst.js +236 -77
- package/dist/tools/captcha.js +1 -1
- package/dist/tools/checkpoint.d.ts +38 -0
- package/dist/tools/checkpoint.js +271 -0
- package/dist/tools/click.d.ts +1 -0
- package/dist/tools/click.js +74 -70
- package/dist/tools/design_extract.js +1 -1
- package/dist/tools/eval.js +1 -1
- package/dist/tools/fill.d.ts +20 -1
- package/dist/tools/fill.js +199 -40
- package/dist/tools/find.d.ts +16 -0
- package/dist/tools/find.js +67 -44
- package/dist/tools/github/index.d.ts +19 -4
- package/dist/tools/github/index.js +40 -4
- package/dist/tools/highlight.d.ts +6 -2
- package/dist/tools/highlight.js +158 -72
- package/dist/tools/inspect.js +1 -1
- package/dist/tools/key.js +65 -56
- package/dist/tools/navigate.d.ts +1 -1
- package/dist/tools/navigate.js +34 -39
- package/dist/tools/pdf.js +21 -13
- package/dist/tools/pointer.d.ts +18 -3
- package/dist/tools/pointer.js +89 -1
- package/dist/tools/read.d.ts +28 -8
- package/dist/tools/read.js +230 -56
- package/dist/tools/scrape.js +67 -13
- package/dist/tools/screenshot.js +21 -16
- package/dist/tools/scroll.js +6 -11
- package/dist/tools/select.js +8 -2
- package/dist/tools/snapshot.js +3 -34
- package/dist/tools/tabs.js +15 -7
- package/dist/tools/thought.d.ts +2 -0
- package/dist/tools/thought.js +13 -14
- package/dist/tools/type.d.ts +2 -0
- package/dist/tools/type.js +107 -58
- package/dist/tools/type_secret.js +42 -76
- package/dist/tools/upload.js +1 -1
- package/dist/tools/video_compiler.d.ts +1 -1
- package/dist/tools/video_compiler.js +11 -12
- package/dist/version.d.ts +5 -0
- package/dist/version.js +5 -0
- package/docs/DOCUMENTATION.md +37 -41
- package/docs/PLUGINS.md +121 -0
- package/docs/TOOLS.md +54 -0
- package/docs/architecture/blind-injection.md +83 -20
- package/docs/architecture/overview.md +49 -49
- package/docs/architecture/security.md +8 -7
- package/docs/architecture/transport.md +2 -2
- package/docs/cli/cli-commands.md +7 -1
- package/docs/cli/cli-flags.md +15 -7
- package/docs/cli/cli-scripting.md +6 -3
- package/docs/configuration/config-locations.md +3 -4
- package/docs/configuration/env-variables.md +4 -4
- package/docs/configuration/folders.md +55 -54
- package/docs/desktop-automation/electron-apps.md +214 -162
- package/docs/get-started/introduction.md +60 -60
- package/docs/get-started/setup-wizard.md +7 -6
- package/docs/intelligence/site-profiles.md +151 -0
- package/docs/tools/tools.md +631 -293
- package/docs/troubleshooting/trouble-browser.md +1 -1
- package/docs/troubleshooting/trouble-loop.md +31 -31
- package/ext-dist/background.js +10 -62
- package/ext-dist/content.js +333 -920
- package/ext-dist/hud/hud.js +9 -9
- package/ext-dist/manifest.json +2 -6
- package/ext-dist/styles.css +6 -1
- package/icon.png +0 -0
- package/mcp.json +87 -71
- package/package.json +118 -119
- package/server.json +5 -5
- package/ext-dist/sidepanel/sidepanel.js +0 -25
- 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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
package/dist/tools/pointer.d.ts
CHANGED
|
@@ -19,8 +19,18 @@ export interface PointerResult {
|
|
|
19
19
|
url: string;
|
|
20
20
|
dom_changed?: boolean;
|
|
21
21
|
}
|
|
22
|
-
export
|
|
23
|
-
action
|
|
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
|
-
|
|
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>;
|
package/dist/tools/pointer.js
CHANGED
|
@@ -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 =
|
|
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
|
}
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -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.
|
|
35
|
+
/** Debug: draws colored element borders via extension. */
|
|
18
36
|
show_overlays?: boolean;
|
|
19
|
-
/** Force deep shadow DOM traversal
|
|
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
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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 };
|
package/dist/tools/read.js
CHANGED
|
@@ -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
|
|
15
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
51
|
-
parts.push(
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
343
|
-
|
|
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 (
|
|
350
|
-
lines.push("stable — no changes
|
|
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
|
-
|
|
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
|