cbrowser 19.2.3 → 19.3.1
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/dist/mcp-tools/base/artifact-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/artifact-tools.js +29 -2
- package/dist/mcp-tools/base/artifact-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.js +2 -2
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.d.ts +2 -2
- package/dist/mcp-tools/base/visual-testing-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/visual-testing-tools.js +175 -42
- package/dist/mcp-tools/base/visual-testing-tools.js.map +1 -1
- package/dist/testing/nl-test-suite.d.ts +8 -0
- package/dist/testing/nl-test-suite.d.ts.map +1 -1
- package/dist/testing/nl-test-suite.js +96 -25
- package/dist/testing/nl-test-suite.js.map +1 -1
- package/dist/visual/attention-quality.d.ts +16 -2
- package/dist/visual/attention-quality.d.ts.map +1 -1
- package/dist/visual/attention-quality.js +42 -12
- package/dist/visual/attention-quality.js.map +1 -1
- package/dist/visual/llm-relevance.d.ts +30 -0
- package/dist/visual/llm-relevance.d.ts.map +1 -1
- package/dist/visual/llm-relevance.js +54 -7
- package/dist/visual/llm-relevance.js.map +1 -1
- package/dist/visual/narrative-reconcile.d.ts +18 -0
- package/dist/visual/narrative-reconcile.d.ts.map +1 -1
- package/dist/visual/narrative-reconcile.js +36 -0
- package/dist/visual/narrative-reconcile.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/artifact-tools.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"artifact-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/artifact-tools.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAuB7C,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAoE7D"}
|
|
@@ -14,8 +14,22 @@ import { z } from "zod";
|
|
|
14
14
|
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
15
15
|
import { join, basename, extname } from "node:path";
|
|
16
16
|
import { artifactDir } from "../../artifact-store.js";
|
|
17
|
+
import { fitImageForInline } from "../inline-image.js";
|
|
18
|
+
import { MAX_RESPONSE_SIZE } from "../screenshot-utils.js";
|
|
17
19
|
/** Largest artifact returned inline. Base64 adds a third on top of this. */
|
|
18
20
|
const MAX_BYTES = 3_500_000;
|
|
21
|
+
/**
|
|
22
|
+
* Base64 budget for an image this tool returns by default.
|
|
23
|
+
*
|
|
24
|
+
* The comment above says it: hosts truncate a tool result near 150k chars. That
|
|
25
|
+
* holds for a view's callServerTool too. A 1280x800 attention heatmap PNG is
|
|
26
|
+
* ~666k chars of base64, so the attention view asked for it, the host cut it,
|
|
27
|
+
* and the panel showed no image -- while the tool result it rendered from
|
|
28
|
+
* carried a perfectly good inline preview it never reads (Alexa, 2026-10-09:
|
|
29
|
+
* "the attention analysis image doesn't load inline again"). Images over this
|
|
30
|
+
* are fitted (JPEG quality ladder, then downscale) unless `full: true`.
|
|
31
|
+
*/
|
|
32
|
+
const VIEW_IMAGE_BUDGET = MAX_RESPONSE_SIZE - 10_000;
|
|
19
33
|
const MIME = {
|
|
20
34
|
".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg",
|
|
21
35
|
".gif": "image/gif", ".webp": "image/webp",
|
|
@@ -26,6 +40,7 @@ export function registerArtifactTools(server) {
|
|
|
26
40
|
description: "Fetch a generated image artifact (heatmap, diff, capture frame) as inline image data. Called by interactive views to display images the widget sandbox cannot load by URL; not usually needed directly.",
|
|
27
41
|
inputSchema: {
|
|
28
42
|
file: z.string().describe("Artifact filename as returned by the producing tool, e.g. 'heatmap-abc123.png'"),
|
|
43
|
+
full: z.boolean().optional().describe("Return the original bytes even when they exceed the host's ~150k-char result cap (a host may then truncate the result). Default false: a larger still image comes back as a JPEG fitted under the cap."),
|
|
29
44
|
},
|
|
30
45
|
annotations: {
|
|
31
46
|
title: "Fetch Artifact",
|
|
@@ -37,7 +52,7 @@ export function registerArtifactTools(server) {
|
|
|
37
52
|
// Hidden from the model's tool list: it exists for views to call, and an
|
|
38
53
|
// agent reaching for it directly would be fetching bytes it cannot use.
|
|
39
54
|
_meta: { ui: { visibility: ["app"] } },
|
|
40
|
-
}, async ({ file }) => {
|
|
55
|
+
}, async ({ file, full }) => {
|
|
41
56
|
// basename strips any directory component, so a traversal attempt
|
|
42
57
|
// resolves to a plain name inside the artifact dir rather than escaping it.
|
|
43
58
|
// The name is then re-validated, because basename alone would happily
|
|
@@ -66,10 +81,22 @@ export function registerArtifactTools(server) {
|
|
|
66
81
|
isError: true,
|
|
67
82
|
};
|
|
68
83
|
}
|
|
84
|
+
const bytes = readFileSync(path);
|
|
85
|
+
const base64 = bytes.toString("base64");
|
|
86
|
+
// Animated GIFs are never re-encoded: a JPEG would keep one frame.
|
|
87
|
+
if (full !== true && ext !== ".gif" && base64.length > VIEW_IMAGE_BUDGET) {
|
|
88
|
+
const fitted = await fitImageForInline(bytes, VIEW_IMAGE_BUDGET).catch(() => null);
|
|
89
|
+
if (fitted) {
|
|
90
|
+
return {
|
|
91
|
+
content: [{ type: "image", data: fitted.data, mimeType: fitted.mimeType }],
|
|
92
|
+
_meta: { fitted: { width: fitted.width, height: fitted.height, sourceWidth: fitted.sourceWidth, sourceHeight: fitted.sourceHeight, quality: fitted.quality, originalBase64Chars: base64.length } },
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
69
96
|
return {
|
|
70
97
|
content: [{
|
|
71
98
|
type: "image",
|
|
72
|
-
data:
|
|
99
|
+
data: base64,
|
|
73
100
|
mimeType: MIME[ext],
|
|
74
101
|
}],
|
|
75
102
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"artifact-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/artifact-tools.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC7D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"artifact-tools.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/artifact-tools.ts"],"names":[],"mappings":"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"}
|
|
@@ -78,8 +78,8 @@ export function registerBaseTools(server, context) {
|
|
|
78
78
|
registerSessionTools(server, context);
|
|
79
79
|
// Healing (1)
|
|
80
80
|
registerHealingTools(server, context);
|
|
81
|
-
// Visual Testing (6) -
|
|
82
|
-
registerVisualTestingTools(server);
|
|
81
|
+
// Visual Testing (6) - attention_analysis can analyze a live session by token
|
|
82
|
+
registerVisualTestingTools(server, context);
|
|
83
83
|
// Testing (5) - no browser context needed
|
|
84
84
|
registerTestingTools(server);
|
|
85
85
|
// Bug Analysis (2)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,mBAAmB;AACnB,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AACnE,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,+BAA+B,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,8BAA8B,EAAE,MAAM,+BAA+B,CAAC;AAC/E,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,wBAAwB,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,gCAAgC,EAAE,MAAM,iCAAiC,CAAC;AACnF,OAAO,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAE5D;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAiB,EACjB,OAAgC;IAEhC,iBAAiB;IACjB,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEzC,kBAAkB;IAClB,wBAAwB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE1C,iBAAiB;IACjB,uBAAuB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEzC,gBAAgB;IAChB,sBAAsB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAExC,eAAe;IACf,qBAAqB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEvC,cAAc;IACd,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEtC,cAAc;IACd,oBAAoB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEtC,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/mcp-tools/base/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @license MIT
|
|
6
6
|
*/
|
|
7
7
|
import type { ContentBlock } from "../screenshot-utils.js";
|
|
8
|
-
import type { McpServer } from "../types.js";
|
|
8
|
+
import type { McpServer, ToolRegistrationContext } from "../types.js";
|
|
9
9
|
/**
|
|
10
10
|
* attention_analysis's result: the JSON, then the heatmap as an image block.
|
|
11
11
|
*
|
|
@@ -25,5 +25,5 @@ export declare function assembleAttentionContent(data: Record<string, unknown>,
|
|
|
25
25
|
/**
|
|
26
26
|
* Register visual testing tools (6 tools: visual_baseline, visual_regression, cross_browser_test, cross_browser_diff, responsive_test, ab_comparison)
|
|
27
27
|
*/
|
|
28
|
-
export declare function registerVisualTestingTools(server: McpServer): void;
|
|
28
|
+
export declare function registerVisualTestingTools(server: McpServer, context?: ToolRegistrationContext): void;
|
|
29
29
|
//# sourceMappingURL=visual-testing-tools.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"visual-testing-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/visual-testing-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"visual-testing-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/visual-testing-tools.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAmE3D,OAAO,KAAK,EAAE,SAAS,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAUtE;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,YAAY,EAAE,CAAC,CAOzB;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,uBAAuB,GAAG,IAAI,CA69BrG"}
|
|
@@ -9,6 +9,39 @@ import { getDefaultConfig } from "../../config.js";
|
|
|
9
9
|
import { resolveValuesForPersona } from "../../values/persona-values.js";
|
|
10
10
|
import { writeArtifact } from "../../artifact-store.js";
|
|
11
11
|
import { buildContentWithPreviews } from "../inline-image.js";
|
|
12
|
+
/**
|
|
13
|
+
* Scroll a page for an attention measurement, and say how to put it back.
|
|
14
|
+
*
|
|
15
|
+
* Shared by attention_analysis and attention_compare so the two cannot drift:
|
|
16
|
+
* scrolling happens BEFORE the screenshot and the DOM read, both of which are
|
|
17
|
+
* taken in the viewport, so the heatmap and element coordinates describe the
|
|
18
|
+
* same band of the page. The page clamps an over-large offset; what applied is
|
|
19
|
+
* reported, not what was asked. `restore` returns a caller's session to the
|
|
20
|
+
* scroll position it had. (2026-10-09)
|
|
21
|
+
*/
|
|
22
|
+
async function scrollForAnalysis(page, scrollY) {
|
|
23
|
+
const original = await page.evaluate(() => window.scrollY).catch(() => 0);
|
|
24
|
+
let scroll;
|
|
25
|
+
if (scrollY !== undefined) {
|
|
26
|
+
// behavior "instant" overrides a page's CSS scroll-behavior: smooth.
|
|
27
|
+
// Without it the scroll ANIMATES: the position read back is where the page
|
|
28
|
+
// still was (cbrowser.ai: asked 2000, reported 0), and the screenshot can
|
|
29
|
+
// land mid-scroll. (Live check, 2026-10-09)
|
|
30
|
+
const applied = await page.evaluate((y) => {
|
|
31
|
+
window.scrollTo({ top: y, left: 0, behavior: "instant" });
|
|
32
|
+
return { applied: Math.round(window.scrollY), max: Math.max(0, Math.round(document.documentElement.scrollHeight - window.innerHeight)) };
|
|
33
|
+
}, scrollY);
|
|
34
|
+
// Lazy content and sticky headers settle on scroll.
|
|
35
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
36
|
+
scroll = { requested: scrollY, applied: applied.applied, maxScrollY: applied.max };
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
...(scroll ? { scroll } : {}),
|
|
40
|
+
restore: async () => {
|
|
41
|
+
await page.evaluate((y) => window.scrollTo({ top: y, left: 0, behavior: "instant" }), original).catch(() => { });
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
12
45
|
/**
|
|
13
46
|
* Resolve a persona before anything is measured, or return the tool's refusal.
|
|
14
47
|
*
|
|
@@ -61,7 +94,7 @@ export async function assembleAttentionContent(data, heatmapPng, fullResolution)
|
|
|
61
94
|
/**
|
|
62
95
|
* Register visual testing tools (6 tools: visual_baseline, visual_regression, cross_browser_test, cross_browser_diff, responsive_test, ab_comparison)
|
|
63
96
|
*/
|
|
64
|
-
export function registerVisualTestingTools(server) {
|
|
97
|
+
export function registerVisualTestingTools(server, context) {
|
|
65
98
|
server.registerTool("visual_baseline", {
|
|
66
99
|
title: "Capture Visual Baseline",
|
|
67
100
|
description: "Capture a visual baseline using Wasserstein barycenter. Takes multiple screenshots, rejects outliers, computes optimal consensus reference with adaptive threshold. Robust to dynamic content, animations, and timing variations.",
|
|
@@ -446,7 +479,9 @@ export function registerVisualTestingTools(server) {
|
|
|
446
479
|
title: "Attention Saliency Analysis",
|
|
447
480
|
description: "Analyze where a persona's attention goes on a page. Two-layer model: (1) visual saliency via W₂ on CIE-Lab (what POPS), (2) DOM semantic analysis (what MATTERS — CTAs, headings, forms, nav). Blended 35/65 so a gray search bar a power-user prioritizes outweighs a flashy banner they ignore. Returns attention metrics, a quality score, and the heatmap overlay: inline as a JPEG preview sized to fit the result (described in heatmapPreview), with the full-resolution PNG at heatmapUrl / artifact_fetch({ file: heatmapFile }).",
|
|
448
481
|
inputSchema: {
|
|
449
|
-
url: z.string().describe("URL to analyze"),
|
|
482
|
+
url: z.string().optional().describe("URL to analyze. Optional with _browserToken: omit it to analyze the session's current page as it stands (logged in, mid-flow); pass it to navigate that session first. Required without a token."),
|
|
483
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call (navigate, click, fill...). Analyzes that live session instead of a fresh browser, so authenticated and mid-flow states can be measured. The session keeps its viewport (device is ignored), and its scroll position and animation state are restored afterwards."),
|
|
484
|
+
scrollY: z.number().min(0).optional().describe("Vertical scroll offset in CSS pixels to analyze at. The screenshot, heatmap and DOM coordinates are all taken in the viewport at this offset. Clamped to the page's maximum scroll; the applied value is reported in `scroll`."),
|
|
450
485
|
persona: z.string().optional().default("first-timer").describe("Persona name"),
|
|
451
486
|
goal: z.string().optional().describe("Task goal — elements matching this goal get boosted attention (e.g., 'find pricing', 'sign up for an account')"),
|
|
452
487
|
cellSize: z.number().optional().default(4).describe("Saliency grid cell size in pixels (smaller = finer heatmap, default: 4)"),
|
|
@@ -465,36 +500,56 @@ export function registerVisualTestingTools(server) {
|
|
|
465
500
|
// Declares the attention view. The heatmap is the finding here, and a JSON
|
|
466
501
|
// list of scores is a lossy description of where a persona actually looks.
|
|
467
502
|
_meta: { ui: { resourceUri: "ui://cbrowser/attention" } },
|
|
468
|
-
}, async ({ url, persona: requestedPersona, goal, cellSize, heatmap, device, useValues, freezeAnimations: doFreeze }) => {
|
|
503
|
+
}, async ({ url, persona: requestedPersona, goal, cellSize, heatmap, device, useValues, freezeAnimations: doFreeze, _browserToken, scrollY }) => {
|
|
504
|
+
const refuse = (error) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error }, null, 2) }] });
|
|
505
|
+
if (!url && !_browserToken)
|
|
506
|
+
return refuse("attention_analysis needs a url, or a _browserToken to analyze that session's current page.");
|
|
507
|
+
if (_browserToken && !context?.getBrowserByToken)
|
|
508
|
+
return refuse("This server does not support browser session tokens; pass a url instead.");
|
|
469
509
|
const resolved = await resolveOrRefuse(requestedPersona);
|
|
470
510
|
if (!resolved.ok)
|
|
471
511
|
return resolved.result;
|
|
472
512
|
const persona = resolved.name;
|
|
473
513
|
const { CBrowser } = await import("../../browser.js");
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
514
|
+
// A session is the caller's: analyze it, leave it as found (scroll,
|
|
515
|
+
// animation state), never close it. A fresh browser is ours to close.
|
|
516
|
+
let sessionToken;
|
|
517
|
+
const browser = _browserToken
|
|
518
|
+
? await (async () => { const r = await context.getBrowserByToken(_browserToken); sessionToken = r.token; return r.browser; })()
|
|
519
|
+
: new CBrowser({
|
|
520
|
+
headless: true,
|
|
521
|
+
// The CONFIGURED viewport, not a hardcoded 1920x1080.
|
|
522
|
+
//
|
|
523
|
+
// Every other tool renders at the configured default (1280x800) and
|
|
524
|
+
// this one alone rendered at 1920x1080, so an attention run and a
|
|
525
|
+
// cognitive_effort run were measuring DIFFERENT rendered pages while
|
|
526
|
+
// reporting coordinates as if they shared a space. The tell was
|
|
527
|
+
// attention regions coming back at x=1312 against a config that says
|
|
528
|
+
// the viewport is 1280 wide -- impossible unless the page was wider.
|
|
529
|
+
// (2026-08-02)
|
|
530
|
+
...(device ? { device: device.toLowerCase() } : {
|
|
531
|
+
viewportWidth: getDefaultConfig().viewportWidth,
|
|
532
|
+
viewportHeight: getDefaultConfig().viewportHeight,
|
|
533
|
+
}),
|
|
534
|
+
});
|
|
490
535
|
const { join } = await import("path");
|
|
491
536
|
const { tmpdir } = await import("os");
|
|
492
537
|
const { unlinkSync } = await import("fs");
|
|
538
|
+
const ownsBrowser = !_browserToken;
|
|
539
|
+
let restoreSession;
|
|
493
540
|
try {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
541
|
+
if (ownsBrowser)
|
|
542
|
+
await browser.launch();
|
|
543
|
+
if (url) {
|
|
544
|
+
await browser.navigate(url);
|
|
545
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
546
|
+
}
|
|
497
547
|
const page = await browser.getPage();
|
|
548
|
+
const pageUrl = url ?? page.url();
|
|
549
|
+
const viewport = page.viewportSize?.() ?? null;
|
|
550
|
+
const { scroll, restore: restoreScroll } = await scrollForAnalysis(page, scrollY);
|
|
551
|
+
if (!ownsBrowser)
|
|
552
|
+
restoreSession = restoreScroll;
|
|
498
553
|
// Freeze BEFORE the screenshot, which is the only moment that matters:
|
|
499
554
|
// the saliency layer reads the image, so anything still moving when the
|
|
500
555
|
// shutter opens is in the measurement.
|
|
@@ -506,6 +561,30 @@ export function registerVisualTestingTools(server) {
|
|
|
506
561
|
animationState = await doFreezeAnimations(page);
|
|
507
562
|
}
|
|
508
563
|
catch { /* a failed freeze is reported below, never fatal */ }
|
|
564
|
+
if (!ownsBrowser) {
|
|
565
|
+
// The freeze stylesheet and reduced-motion emulation would outlive
|
|
566
|
+
// this call in the caller's session; undo both with the scroll.
|
|
567
|
+
const restoreBefore = restoreSession;
|
|
568
|
+
restoreSession = async () => {
|
|
569
|
+
await restoreBefore?.();
|
|
570
|
+
await page.evaluate(() => {
|
|
571
|
+
for (const st of Array.from(document.querySelectorAll("style"))) {
|
|
572
|
+
if ((st.textContent ?? "").includes("cbrowser-freeze-animations"))
|
|
573
|
+
st.remove();
|
|
574
|
+
}
|
|
575
|
+
try {
|
|
576
|
+
for (const a of document.getAnimations?.() ?? []) {
|
|
577
|
+
try {
|
|
578
|
+
a.play();
|
|
579
|
+
}
|
|
580
|
+
catch { /* not resumable */ }
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
catch { /* older engines */ }
|
|
584
|
+
}).catch(() => { });
|
|
585
|
+
await page.emulateMedia?.({ reducedMotion: null }).catch(() => { });
|
|
586
|
+
};
|
|
587
|
+
}
|
|
509
588
|
}
|
|
510
589
|
// Measured either way. On a live run it says how much motion the number
|
|
511
590
|
// was taken through; on a frozen run a non-zero value means the freeze
|
|
@@ -517,7 +596,7 @@ export function registerVisualTestingTools(server) {
|
|
|
517
596
|
// attention model. This was the only inline copy of the collect+DPR-scale
|
|
518
597
|
// dance; it now shares one implementation with visual_cognitive_story and
|
|
519
598
|
// journey_heatmap_gif, so the scaling cannot drift between the three.
|
|
520
|
-
const { computeAttentionQuality, extractPageElementsForAttention, collectDomAttentionElements } = await import("../../visual/attention-quality.js");
|
|
599
|
+
const { computeAttentionQuality, extractPageElementsForAttention, collectDomAttentionElements, MIN_DISTINCT_ELEMENTS } = await import("../../visual/attention-quality.js");
|
|
521
600
|
const rawElements = await extractPageElementsForAttention(page);
|
|
522
601
|
const dpr = await page.evaluate(() => window.devicePixelRatio).catch(() => 1);
|
|
523
602
|
const pageElements = rawElements.map(el => ({
|
|
@@ -538,9 +617,12 @@ export function registerVisualTestingTools(server) {
|
|
|
538
617
|
let relevanceScores;
|
|
539
618
|
let relevanceSource;
|
|
540
619
|
let relevanceReasoning;
|
|
620
|
+
// Method AND provenance: whether this call judged, or replayed a
|
|
621
|
+
// cached judgement (and how old it is). See relevanceProvenance.
|
|
622
|
+
let relevanceFields = {};
|
|
541
623
|
if (domAttentionElements.length > 0) {
|
|
542
624
|
try {
|
|
543
|
-
const { judgeRelevance } = await import("../../visual/llm-relevance.js");
|
|
625
|
+
const { judgeRelevance, relevanceProvenance } = await import("../../visual/llm-relevance.js");
|
|
544
626
|
const { getAnthropicApiKey } = await import("../../cognitive/index.js");
|
|
545
627
|
const { getActiveTier } = await import("../tier-gate.js");
|
|
546
628
|
const { tierHasAccess } = await import("../tool-categories.js");
|
|
@@ -559,6 +641,7 @@ export function registerVisualTestingTools(server) {
|
|
|
559
641
|
relevanceScores = judged.scores;
|
|
560
642
|
relevanceSource = judged.source;
|
|
561
643
|
relevanceReasoning = judged.reasoning;
|
|
644
|
+
relevanceFields = relevanceProvenance(judged);
|
|
562
645
|
}
|
|
563
646
|
catch { /* keyword path inside buildSemanticMap remains the floor */ }
|
|
564
647
|
}
|
|
@@ -616,9 +699,16 @@ export function registerVisualTestingTools(server) {
|
|
|
616
699
|
// only ever annotated, never rewritten: the narrative carries the "why"
|
|
617
700
|
// the metrics don't.
|
|
618
701
|
let narrativeReconciliation;
|
|
702
|
+
// What the narrative was computed over versus what the metrics were:
|
|
703
|
+
// N candidates judged by the LLM, M elements the hotspots sampled.
|
|
704
|
+
let reasoningScope;
|
|
619
705
|
try {
|
|
620
|
-
const { reconcileAttentionNarrative } = await import("../../visual/narrative-reconcile.js");
|
|
706
|
+
const { reconcileAttentionNarrative, describeReasoningScope } = await import("../../visual/narrative-reconcile.js");
|
|
621
707
|
narrativeReconciliation = reconcileAttentionNarrative(relevanceReasoning, attentionQuality);
|
|
708
|
+
if (relevanceReasoning) {
|
|
709
|
+
const sampled = attentionQuality?.sampledElements;
|
|
710
|
+
reasoningScope = describeReasoningScope(domAttentionElements.length, sampled, MIN_DISTINCT_ELEMENTS);
|
|
711
|
+
}
|
|
622
712
|
}
|
|
623
713
|
catch { /* annotation only; never block the result on it */ }
|
|
624
714
|
const data = {
|
|
@@ -627,9 +717,14 @@ export function registerVisualTestingTools(server) {
|
|
|
627
717
|
// Stated, because two tools silently rendering at different sizes is
|
|
628
718
|
// exactly what made these coordinates incomparable with a CTC run.
|
|
629
719
|
// A reader can now check instead of assuming they match.
|
|
630
|
-
renderedViewport:
|
|
631
|
-
?
|
|
632
|
-
:
|
|
720
|
+
renderedViewport: !ownsBrowser
|
|
721
|
+
? (viewport ? `${viewport.width}x${viewport.height} (session)` : "session viewport")
|
|
722
|
+
: device
|
|
723
|
+
? `device: ${device.toLowerCase()}`
|
|
724
|
+
: `${getDefaultConfig().viewportWidth}x${getDefaultConfig().viewportHeight}`,
|
|
725
|
+
url: pageUrl,
|
|
726
|
+
...(scroll ? { scroll } : {}),
|
|
727
|
+
...(sessionToken ? { _browserToken: sessionToken, ...(device ? { deviceIgnored: "device does not apply to an existing session; its own viewport was used" } : {}) } : {}),
|
|
633
728
|
coordinateSpace: "CSS pixels in the rendered viewport above — compare only against runs reporting the same renderedViewport",
|
|
634
729
|
// What actually ran, not what was asked for. A freeze that silently
|
|
635
730
|
// failed would produce exactly the variance it was asked to remove,
|
|
@@ -654,6 +749,9 @@ export function registerVisualTestingTools(server) {
|
|
|
654
749
|
// with no stated reason is one the caller cannot argue with, which
|
|
655
750
|
// was the point of leaving keyword matching.
|
|
656
751
|
...(relevanceReasoning ? { attentionReasoning: relevanceReasoning } : {}),
|
|
752
|
+
// Beside the narrative, so it cannot be read as describing the
|
|
753
|
+
// measured targets: it was computed over a different, larger set.
|
|
754
|
+
...(reasoningScope ? { attentionReasoningScope: reasoningScope } : {}),
|
|
657
755
|
// Only present when a checkable claim in the narrative actually
|
|
658
756
|
// contradicts a computed metric. Silent otherwise — a spurious
|
|
659
757
|
// "these disagree" on a narrative that was fine is its own defect.
|
|
@@ -664,7 +762,8 @@ export function registerVisualTestingTools(server) {
|
|
|
664
762
|
note: narrativeReconciliation.reconciliationNote,
|
|
665
763
|
},
|
|
666
764
|
} : {}),
|
|
667
|
-
|
|
765
|
+
// relevanceMethod, relevanceCached, and the cache age when replayed.
|
|
766
|
+
...(relevanceSource ? relevanceFields : {}),
|
|
668
767
|
// These two lines used to read as a contradiction — "Scattered
|
|
669
768
|
// attention (overwhelmed)" printed directly above "Attention
|
|
670
769
|
// concentrated on few areas". The numbers were never wrong; the
|
|
@@ -757,7 +856,7 @@ export function registerVisualTestingTools(server) {
|
|
|
757
856
|
apiKey: getSessionApiKey(),
|
|
758
857
|
imageUrl: publicUrl,
|
|
759
858
|
toolName: "attention_analysis",
|
|
760
|
-
targetUrl:
|
|
859
|
+
targetUrl: pageUrl,
|
|
761
860
|
persona,
|
|
762
861
|
metadata: { entropy: result.entropy, concentration: result.concentration, alignmentScore: result.alignmentScore },
|
|
763
862
|
});
|
|
@@ -775,16 +874,21 @@ export function registerVisualTestingTools(server) {
|
|
|
775
874
|
return { content: await assembleAttentionContent(data, heatmapPng, heatmapFullResolution) };
|
|
776
875
|
}
|
|
777
876
|
finally {
|
|
778
|
-
|
|
877
|
+
if (ownsBrowser)
|
|
878
|
+
await browser.close();
|
|
879
|
+
else
|
|
880
|
+
await restoreSession?.();
|
|
779
881
|
}
|
|
780
882
|
});
|
|
781
883
|
server.registerTool("attention_compare", {
|
|
782
884
|
title: "Compare Persona Attention",
|
|
783
885
|
description: "Compare attention patterns between two personas on the same page. Shows where they look differently and the Wasserstein divergence between their saliency maps. The comparison map comes back inline as a JPEG preview sized to fit the result (described in comparisonHeatmapPreview), with the full-resolution PNG at comparisonHeatmapUrl / artifact_fetch({ file: comparisonHeatmapFile }).",
|
|
784
886
|
inputSchema: {
|
|
785
|
-
url: z.string().describe("URL to analyze"),
|
|
887
|
+
url: z.string().optional().describe("URL to analyze. Optional with _browserToken: omit it to compare on the session's current page as it stands (logged in, mid-flow); pass it to navigate that session first. Required without a token."),
|
|
786
888
|
personaA: z.string().describe("First persona"),
|
|
787
889
|
personaB: z.string().describe("Second persona"),
|
|
890
|
+
_browserToken: z.string().optional().describe("Browser session token from a previous tool call. Compares on that live session instead of a fresh browser; the session keeps its viewport, is never closed, and its scroll position is restored afterwards."),
|
|
891
|
+
scrollY: z.number().min(0).optional().describe("Vertical scroll offset in CSS pixels to compare at. Both personas are measured on the same screenshot at this offset. Clamped to the page's maximum scroll; the applied value is reported in `scroll`."),
|
|
788
892
|
},
|
|
789
893
|
annotations: {
|
|
790
894
|
title: "Compare Persona Attention",
|
|
@@ -793,7 +897,12 @@ export function registerVisualTestingTools(server) {
|
|
|
793
897
|
idempotentHint: true,
|
|
794
898
|
openWorldHint: true,
|
|
795
899
|
},
|
|
796
|
-
}, async ({ url, personaA: requestedA, personaB: requestedB }) => {
|
|
900
|
+
}, async ({ url, personaA: requestedA, personaB: requestedB, _browserToken, scrollY }) => {
|
|
901
|
+
const refuse = (error) => ({ isError: true, content: [{ type: "text", text: JSON.stringify({ error }, null, 2) }] });
|
|
902
|
+
if (!url && !_browserToken)
|
|
903
|
+
return refuse("attention_compare needs a url, or a _browserToken to compare on that session's current page.");
|
|
904
|
+
if (_browserToken && !context?.getBrowserByToken)
|
|
905
|
+
return refuse("This server does not support browser session tokens; pass a url instead.");
|
|
797
906
|
// Both refused before measuring: an unknown name compared as "maximally
|
|
798
907
|
// different" from power-user, more than the extreme real pair. (2026-10-09)
|
|
799
908
|
const resolvedA = await resolveOrRefuse(requestedA);
|
|
@@ -809,19 +918,34 @@ export function registerVisualTestingTools(server) {
|
|
|
809
918
|
// attention runs, so rendering at a size no other tool uses meant its
|
|
810
919
|
// comparison was internally consistent and incomparable with everything
|
|
811
920
|
// else that reports attention coordinates. (2026-08-02)
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
921
|
+
// A session is the caller's: compare on it, leave its scroll as found,
|
|
922
|
+
// never close it. A fresh browser is ours to close. (2026-10-09)
|
|
923
|
+
let sessionToken;
|
|
924
|
+
const browser = _browserToken
|
|
925
|
+
? await (async () => { const r = await context.getBrowserByToken(_browserToken); sessionToken = r.token; return r.browser; })()
|
|
926
|
+
: new CBrowser({
|
|
927
|
+
headless: true,
|
|
928
|
+
viewportWidth: getDefaultConfig().viewportWidth,
|
|
929
|
+
viewportHeight: getDefaultConfig().viewportHeight,
|
|
930
|
+
});
|
|
931
|
+
const ownsBrowser = !_browserToken;
|
|
932
|
+
let restoreSession;
|
|
817
933
|
const { join } = await import("path");
|
|
818
934
|
const { tmpdir } = await import("os");
|
|
819
935
|
const { unlinkSync } = await import("fs");
|
|
820
936
|
try {
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
937
|
+
if (ownsBrowser)
|
|
938
|
+
await browser.launch();
|
|
939
|
+
if (url) {
|
|
940
|
+
await browser.navigate(url);
|
|
941
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
942
|
+
}
|
|
824
943
|
const page = await browser.getPage();
|
|
944
|
+
const pageUrl = url ?? page.url();
|
|
945
|
+
const viewport = page.viewportSize?.() ?? null;
|
|
946
|
+
const { scroll, restore: restoreScroll } = await scrollForAnalysis(page, scrollY);
|
|
947
|
+
if (!ownsBrowser)
|
|
948
|
+
restoreSession = restoreScroll;
|
|
825
949
|
const screenshotPath = join(tmpdir(), `attn-cmp-${Date.now()}.png`);
|
|
826
950
|
await page.screenshot({ path: screenshotPath, fullPage: false });
|
|
827
951
|
// Extract DOM elements for semantic attention layer
|
|
@@ -837,6 +961,12 @@ export function registerVisualTestingTools(server) {
|
|
|
837
961
|
const { compareAttention } = await import("../../visual/attention-transport.js");
|
|
838
962
|
const result = await compareAttention(screenshotPath, personaA, personaB, 4, domEls);
|
|
839
963
|
const responseData = {
|
|
964
|
+
url: pageUrl,
|
|
965
|
+
renderedViewport: !ownsBrowser
|
|
966
|
+
? (viewport ? `${viewport.width}x${viewport.height} (session)` : "session viewport")
|
|
967
|
+
: `${getDefaultConfig().viewportWidth}x${getDefaultConfig().viewportHeight}`,
|
|
968
|
+
...(scroll ? { scroll } : {}),
|
|
969
|
+
...(sessionToken ? { _browserToken: sessionToken } : {}),
|
|
840
970
|
personaA: {
|
|
841
971
|
name: personaA,
|
|
842
972
|
...(resolvedA.resolvedFrom ? { resolvedFrom: resolvedA.resolvedFrom } : {}),
|
|
@@ -901,7 +1031,10 @@ export function registerVisualTestingTools(server) {
|
|
|
901
1031
|
return { content };
|
|
902
1032
|
}
|
|
903
1033
|
finally {
|
|
904
|
-
|
|
1034
|
+
if (ownsBrowser)
|
|
1035
|
+
await browser.close();
|
|
1036
|
+
else
|
|
1037
|
+
await restoreSession?.();
|
|
905
1038
|
}
|
|
906
1039
|
});
|
|
907
1040
|
}
|