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.
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-tools.d.ts","sourceRoot":"","sources":["../../../src/mcp-tools/base/artifact-tools.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAU7C,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAuD7D"}
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: readFileSync(path).toString("base64"),
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":"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"}
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) - no browser context needed
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":"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"}
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;AA8B3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAU7C;;;;;;;;;;;;;;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,GAAG,IAAI,CA64BlE"}
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
- const browser = new CBrowser({
475
- headless: true,
476
- // The CONFIGURED viewport, not a hardcoded 1920x1080.
477
- //
478
- // Every other tool renders at the configured default (1280x800) and
479
- // this one alone rendered at 1920x1080, so an attention run and a
480
- // cognitive_effort run were measuring DIFFERENT rendered pages while
481
- // reporting coordinates as if they shared a space. The tell was
482
- // attention regions coming back at x=1312 against a config that says
483
- // the viewport is 1280 wide -- impossible unless the page was wider.
484
- // (2026-08-02)
485
- ...(device ? { device: device.toLowerCase() } : {
486
- viewportWidth: getDefaultConfig().viewportWidth,
487
- viewportHeight: getDefaultConfig().viewportHeight,
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
- await browser.launch();
495
- await browser.navigate(url);
496
- await new Promise(r => setTimeout(r, 2000));
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: device
631
- ? `device: ${device.toLowerCase()}`
632
- : `${getDefaultConfig().viewportWidth}x${getDefaultConfig().viewportHeight}`,
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
- ...(relevanceSource ? { relevanceMethod: relevanceSource } : {}),
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: url,
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
- await browser.close();
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
- const browser = new CBrowser({
813
- headless: true,
814
- viewportWidth: getDefaultConfig().viewportWidth,
815
- viewportHeight: getDefaultConfig().viewportHeight,
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
- await browser.launch();
822
- await browser.navigate(url);
823
- await new Promise(r => setTimeout(r, 2000));
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
- await browser.close();
1034
+ if (ownsBrowser)
1035
+ await browser.close();
1036
+ else
1037
+ await restoreSession?.();
905
1038
  }
906
1039
  });
907
1040
  }