Skip to content

feat(peek): R2 — stable refs + diff-after-action + get_element_detail#104

Merged
harry-harish merged 3 commits into
mainfrom
feat/peek-r2-diff-detail
Jun 20, 2026
Merged

feat(peek): R2 — stable refs + diff-after-action + get_element_detail#104
harry-harish merged 3 commits into
mainfrom
feat/peek-r2-diff-detail

Conversation

@harry-harish

@harry-harish harry-harish commented Jun 20, 2026

Copy link
Copy Markdown
Member

Supersedes #102 (auto-closed when its stacked base branch feat/peek-live-page-view was deleted on the #101 merge). Same work, now re-stacked as a single clean R2-only commit on top of main.

What

R2 of the MCP token-optimization work, building on R1 (#101, merged). Three pieces that make the steady-state write loop cost near-zero perception tokens:

  • Stable refs — an element keeps the same ref across get_page_view snapshots, keyed by a MAIN-world WeakMap<Element,ref> + counter (JS globals, never DOM attrs → rrweb still never records them). Forward map keeps R1's __peekRefs name, so the dispatcher is untouched; SPA soft-navs keep refs alive.
  • get_element_detail(ref) (new tool) — on-demand, lossless, masked single-element drill-in (role, full name, all aria-*, state, value, href, position, nearby heading, interactive descendants with refs). Level 1+, audited, reuses level-1-read. Never returns raw outerHTML.
  • observe: true on a mutating execute_action — returns a masked details.viewDelta (added/removed/changed) in one round-trip; navigation returns a {navigated:true} "refs expired" marker.

Safety / privacy

  • Gate ordering verified: observe runs only after gate() allows AND the action succeeds — never a read-on-denial; a ref-targeted destructive action with observe still hits the confirm banner (no viewDelta on deny).
  • In-page + SW masking; .rr-mask/data-private/data-dd-privacy/data-peek-mask regions masked to ••• across names/values/text/aria/children (and context.heading). Honest free-text caveat kept. CSP-safe MAIN-world injection (no eval).
  • Independent security review: no critical/permission-bypass; the one HIGH (annotation gap) + mediums/lows fixed and re-verified.

Testing

621 extension + 303 mcp unit tests; 6/6 real-Chromium e2e (e2e/page-view.spec.ts); typecheck + biome + build clean. Changeset: minor for both packages.

Headroom lineage

Borrows CacheAligner → stable refs, CCR → get_element_detail, don't-re-send → diff-after-action — as architecture, realized locally with no model/network. Library not adopted (would break the no-network privacy claim). Spec: _context/docs/specs/2026-06-20-peek-r2-diff-stable-refs-element-detail-spec.md.

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added get_element_detail action to retrieve full masked details for specific element references
    • Added observe: true option on action execution to receive post-action changes in a single round-trip
    • Enhanced privacy masking for sensitive data across page snapshots and element details
  • Documentation

    • Updated privacy policy to document new capabilities and privacy masking behavior
  • Tests

    • Added E2E tests for page snapshots, masking, reference stability, and strict Content Security Policy scenarios

Builds on R1 (get_page_view). Three pieces that make the steady-state write
loop cost near-zero perception tokens:

- Stable refs: an element keeps the same ref across get_page_view snapshots,
  keyed by a MAIN-world WeakMap<Element,ref> + counter (JS globals, never DOM
  attrs -> rrweb still never records them). Forward map keeps R1's __peekRefs
  name, so the dispatcher is untouched; SPA soft-navs keep refs alive.
- get_element_detail(ref): on-demand, lossless, masked single-element drill-in
  (role, full name, all aria-*, state, value, href, position, nearby heading,
  interactive descendants with refs). Level 1+, audited, reuses level-1-read.
  Never returns raw outerHTML.
- observe:true on a mutating execute_action returns a masked details.viewDelta
  (added/removed/changed) in one round-trip; navigation returns a
  {navigated:true} "refs expired" marker.

Privacy: in-page + SW masking; .rr-mask/data-private/data-dd-privacy/
data-peek-mask regions masked to '•••' across names/values/text/aria/children
(and via context.heading); CSP-safe MAIN-world injection (no eval). Honest
free-text caveat kept. Independent security review: no critical/bypass; the one
HIGH (annotation gap) + mediums/lows fixed.

621 extension + 303 mcp unit tests; 6/6 real-Chromium e2e. Changeset minor for
both packages. Headroom concepts (CacheAligner/CCR/no-re-send) borrowed as
architecture; library not adopted (no-network privacy).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@harry-harish, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 26 minutes and 18 seconds. Learn how PR review limits work.

To continue reviewing without waiting, enable usage-based billing in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 750b2f05-4f54-4cce-b7bc-e5910dd5cb5c

📥 Commits

Reviewing files that changed from the base of the PR and between c7243ec and 394dbb7.

📒 Files selected for processing (4)
  • packages/peek-extension/e2e/page-view.spec.ts
  • packages/peek-extension/src/__tests__/snapshot.test.ts
  • packages/peek-extension/src/permissions/snapshot.ts
  • packages/peek-mcp/src/mcp/server.ts
📝 Walkthrough

Walkthrough

Adds identity-stable element refs using a MAIN-world WeakMap registry, a new get_element_detail(ref) read action returning masked per-element drill-in data, an observe: true mode on execute_action returning details.viewDelta post-action diffs, broadened privacy masking across get_page_view and get_element_detail, and a new SW-side mask-view module. MCP tool surface grows from 15 to 16 tools.

Changes

R2 Agent API: stable refs, element detail, observe delta, masking

Layer / File(s) Summary
Wire protocol contracts and new types
packages/peek-extension/src/permissions/action-protocol.ts, packages/peek-extension/src/relay/mask-url.ts, packages/peek-extension/src/relay/mask.ts, packages/peek-mcp/src/mcp/action-schema.ts
observe?: boolean added to all mutating action interfaces; ElementDetailAction introduced; ElementDetail/PageViewDelta/ElementDetailError result shapes added; maskUrl extracted into its own module and re-exported from mask.ts.
MAIN-world snapshot: stable refs, masking, diff, buildElementDetail
packages/peek-extension/src/permissions/snapshot.ts
buildPageView refactored to use WeakMap-backed stable refs and persist __peekLastView; MASK_SELECTOR/isPrivacyMasked added; name/value masking broadened; diffPageView, diffPageViewStandalone (CSP-safe), and buildElementDetail fully implemented.
SW-side mask-view helpers
packages/peek-extension/src/permissions/mask-view.ts
New module exporting maskPageViewNode (redacts name/value, preserves structural fields) and maskElementDetail (redacts name/value/text/aria values/child names/context.heading, masks href via maskUrl).
Permission-layer element_detail routing
packages/peek-extension/src/permissions/action-handler.ts
New element_detail branch in handleActionRequest enforces Level 1+, dispatches to MAIN world, returns allow/ok with level-1-read approver or allow/error for expired refs.
Background: element_detail dispatch, observe delta, URL masking
packages/peek-extension/entrypoints/background.ts
Adds OBSERVE_DIFFABLE_VERBS/OBSERVE_NAVIGATING_VERBS classification; new element_detail MAIN-world dispatch branch; page_view URL now masked; computeObserveDelta attaches viewDelta on action success (navigated marker or re-diff with node masking).
MCP tool surface: get_element_detail registration
packages/peek-mcp/src/mcp/server.ts, packages/peek-mcp/README.md
get_element_detail registered as Level-1 read tool dispatching element_detail action; PEEK_MCP_TOOLS updated to 16; README tool table updated.
Unit tests: snapshot, action-handler, masking
packages/peek-extension/src/__tests__/snapshot.test.ts, packages/peek-extension/src/__tests__/action-handler.test.ts, packages/peek-mcp/test/...
Stable-ref identity, diffPageView/diffPageViewStandalone CSP parity, buildElementDetail masking/error cases, element_detail permission gating, observe:true behavior, maskPageViewNode/maskElementDetail unit tests, MCP server/schema tests updated for 16 tools.
E2E tests: real-browser coverage
packages/peek-extension/e2e/fixtures/*, packages/peek-extension/e2e/page-view.spec.ts
page-view.html and strict-csp.html fixtures added; Playwright spec covers ref stability across DOM mutations, password masking, CSP-safe injection (no eval/new Function), rrweb DOM invisibility, data-private region redaction, and element_detail structured output.
Docs and privacy policy
apps/peek-docs/src/pages/privacy.astro, docs/peek/PRIVACY_POLICY.md, .changeset/peek-r2-diff-detail.md
Privacy policy updated to document get_element_detail, observe:true delta, stable refs, and masking rules; changeset entry added.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over Agent,MAIN: get_element_detail flow
  end
  participant Agent
  participant MCPServer as MCP Server
  participant BGScript as Background SW
  participant ActionHandler as action-handler
  participant MAIN as MAIN World

  Agent->>MCPServer: get_element_detail(sessionId, ref)
  MCPServer->>BGScript: execute_action {type:'element_detail', ref}
  BGScript->>ActionHandler: handleActionRequest(element_detail)
  ActionHandler-->>BGScript: verdict:allow (Level 1+, level-1-read)
  BGScript->>MAIN: buildElementDetail(ref)
  MAIN-->>BGScript: ElementDetail (masked) or ElementDetailError
  BGScript-->>MCPServer: masked detail payload
  MCPServer-->>Agent: tool result text

  rect rgba(144, 238, 144, 0.5)
    Note over Agent,MAIN: execute_action + observe:true flow
  end
  Agent->>MCPServer: execute_action(action, observe:true)
  MCPServer->>BGScript: dispatch action
  BGScript->>MAIN: run action
  MAIN-->>BGScript: action result
  alt navigating verb
    BGScript->>MAIN: maskUrl(currentUrl)
    BGScript-->>MCPServer: details.viewDelta = {navigated:true}
  else diffable verb
    BGScript->>MAIN: diffPageViewStandalone()
    MAIN-->>BGScript: PageViewDelta
    BGScript->>BGScript: maskPageViewNode(each changed node)
    BGScript-->>MCPServer: details.viewDelta = {added, removed, changed}
  end
  MCPServer-->>Agent: tool result with viewDelta
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Cubenest/rrweb-stack#37: Shares the same handleActionRequest/background.ts MAIN-world dispatch plumbing that this PR extends with element_detail and observe support.
  • Cubenest/rrweb-stack#101: Introduced the get_page_view snapshot/ref pipeline in snapshot.ts that this PR refactors for stable refs and broadened masking.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main R2 features: stable refs, diff-after-action (observe), and the new get_element_detail tool.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/peek-r2-diff-detail

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…cold-install CI)

R2's SW masking helpers (maskPageViewNode/maskElementDetail) were top-level
EXPORTED functions in the background entrypoint that use @cubenest/rrweb-core's
maskTextContent. `wxt prepare` (the extension's postinstall, run BEFORE
`pnpm build`) keeps an entrypoint's top-level exports but elides its
defineBackground callback — so that kept rrweb-core import forced Vite to
resolve rrweb-core's gitignored, unbuilt dist/ and `pnpm install --frozen-lockfile`
failed on a cold CI checkout (ci/ci-windows/e2e-peek/e2e-wdio). main passed only
because it uses maskTextContent solely inside the (elided) callback.

Fix (match main's pattern — rrweb-core reachable only through the elided callback):
- move maskPageViewNode/maskElementDetail into src/permissions/mask-view.ts
- extract the pure maskUrl into src/relay/mask-url.ts (no rrweb-core); mask.ts
  re-exports it; background imports maskUrl from there
- background's remaining maskTextContent/maskUrl uses are all inside
  defineBackground (tree-shaken by wxt prepare)

Verified with a true cold `pnpm install --frozen-lockfile` (rrweb-core dist
wiped) — now succeeds. 621 ext + 303 mcp tests, 6/6 e2e, typecheck + biome clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/peek-extension/e2e/page-view.spec.ts`:
- Around line 343-350: The find predicate on line 344 that selects pwRef using
only the condition n.value === '•••' is too generic and may match other masked
inputs in the fixture, allowing the test to pass without actually exercising the
password-specific detail path. Add an additional condition to the find predicate
that uniquely identifies the password node, such as checking a specific property
like node type, id, name attribute, or another identifying characteristic that
distinguishes the password field from other masked inputs in the fixture.

In `@packages/peek-extension/src/permissions/snapshot.ts`:
- Around line 321-333: The noise filtering check that skips unnamed non-control
elements happens after masked elements are assigned the '•••' name, which
prevents them from being filtered out since the name is no longer empty. Reorder
the logic in the masked element handling block to perform the empty-name filter
check before assigning the redaction marker: first determine the real accessible
name using accName(el), check if it should be skipped via the empty-name and
non-control condition, and only after the element passes that filter should the
name be replaced with '•••' if masked.

In `@packages/peek-mcp/src/mcp/server.ts`:
- Around line 841-847: The documentation comment for the get_element_detail
function incorrectly describes the returned data as "direct interactive
children" when the actual implementation returns interactive descendants with a
cap. Update the comment block describing get_element_detail to accurately
reflect that the function returns interactive descendants (capped) rather than
direct children, ensuring the tool contract documentation matches the actual
payload shape being returned.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cb3adca-386a-451a-9cb7-96adffaba2f7

📥 Commits

Reviewing files that changed from the base of the PR and between 2adf61f and c7243ec.

📒 Files selected for processing (21)
  • .changeset/peek-r2-diff-detail.md
  • apps/peek-docs/src/pages/privacy.astro
  • docs/peek/PRIVACY_POLICY.md
  • packages/peek-extension/e2e/fixtures/page-view.html
  • packages/peek-extension/e2e/fixtures/strict-csp.html
  • packages/peek-extension/e2e/page-view.spec.ts
  • packages/peek-extension/entrypoints/background.ts
  • packages/peek-extension/src/__tests__/action-handler.test.ts
  • packages/peek-extension/src/__tests__/snapshot.test.ts
  • packages/peek-extension/src/permissions/action-handler.ts
  • packages/peek-extension/src/permissions/action-protocol.ts
  • packages/peek-extension/src/permissions/mask-view.ts
  • packages/peek-extension/src/permissions/snapshot.ts
  • packages/peek-extension/src/relay/mask-url.ts
  • packages/peek-extension/src/relay/mask.ts
  • packages/peek-mcp/README.md
  • packages/peek-mcp/src/mcp/action-schema.ts
  • packages/peek-mcp/src/mcp/server.ts
  • packages/peek-mcp/test/action-schema.test.ts
  • packages/peek-mcp/test/server.test.ts
  • packages/peek-mcp/test/stdio-smoke.test.ts

Comment thread packages/peek-extension/e2e/page-view.spec.ts Outdated
Comment thread packages/peek-extension/src/permissions/snapshot.ts
Comment thread packages/peek-mcp/src/mcp/server.ts
- snapshot.ts (MAJOR): run the empty-name noise filter on the REAL name BEFORE
  masking, in both buildPageView and the diffPageViewStandalone walker. The HIGH
  privacy fix replaced empty names with '•••' first, so unnamed non-controls in a
  masked region stopped being filtered → snapshot/delta bloat + maxElements
  pressure. + regression test (masked empty role=group is dropped, not surfaced).
- server.ts: get_element_detail description now says "interactive descendants
  (capped)" to match the implementation (was "direct interactive children").
- e2e page-view.spec.ts: pin the password drill-in to the node named "Password"
  (other masked inputs, e.g. the data-private SSN, also carry value '•••', so
  matching on '•••' alone could drill the wrong element) + assert it's found.

622 ext + 303 mcp tests, 6/6 e2e, typecheck + biome clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: harry-harish <22562634+harry-harish@users.noreply.github.com>
@harry-harish harry-harish merged commit c2f2090 into main Jun 20, 2026
5 checks passed
@harry-harish harry-harish deleted the feat/peek-r2-diff-detail branch June 20, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant