π Window Globals tab β surface configured window.* values in the panel#18
Open
jjpaulino wants to merge 6 commits into
Open
π Window Globals tab β surface configured window.* values in the panel#18jjpaulino wants to merge 6 commits into
jjpaulino wants to merge 6 commits into
Conversation
User-approved design for a new panel tab that surfaces user-configured window.* globals (e.g. window.dataLayer, window.nymGtmPage). Designed around a manual-refresh model so there is zero ambient cost on the host page. Spec covers: page-bridge architecture (content script β main world via injected <script> + postMessage with correlation IDs), top-level path syntax (strip optional `window.` prefix, validate as JS identifier), Options-page config UI, tab rendering (collapsible sections all closed by default, JsonPreview reuse for arrays/objects, muted '(not defined)' placeholder), error handling (undefined, circular refs, CSP-blocked bridges), cross-browser parity (Chromium + Firefox, no new permissions), and the test plan (~12-15 new tests, ~190 total). Explicitly out of scope for v1: dot-paths/nested access, polling/dataLayer.push wrapping, per-section truncation, last-read snapshot caching. Implementation begins on this branch after review. Co-authored-by: Cursor <cursoragent@cursor.com>
First slice of the Window Globals tab work. Pure types + parser + storage wiring only β no UI, no page-bridge yet. Lays the contract the rest of the feature builds on. - UserPreferences gains `windowGlobals: readonly string[]`, defaults to []. - `parseWindowGlobal` accepts both `nymGtmPage` and `window.nymGtmPage` shapes, strips a single leading `window.`, validates the remainder as a plain JS identifier. Returns a tagged result with one of four reason codes so the Options page can show a specific inline hint per failure mode (empty / dotted-path / bracketed / invalid-identifier). - `parseResultMessage` maps each reason to user-facing copy; returns null on success so call sites can `?? ''` instead of branching. - `normalizeWindowGlobals` is the defense-in-depth normalizer used both at save time and at read time so hand-edited storage never surfaces as ghost sections in the panel. Tests: 15 for the parser (every accept + reject path, plus the `window.window.foo` double-prefix case), 3 new round-trip tests for the storage layer (default, explicit, partial-update sibling preservation). All 22 green. Next: page-bridge for reading values out of the host page's main world, then the Options UI, then the Globals tab itself. Co-authored-by: Cursor <cursoragent@cursor.com>
Content scripts run in an isolated world β `window.nymGtmPage` from a content script returns undefined even when the page has it defined. This adds the bridge layer the new Globals tab needs. - `installPageBridge()` is idempotent: first call injects a <script> element whose textContent is an inlined IIFE that installs a single persistent listener on the page-world `window`. Subsequent calls no-op. The <script> tag self-removes after execution β the listener it set up lives on regardless, which is what we actually need. - `readGlobals(keys)` posts a `clay-slip:globals:request` with a correlation ID and resolves the Promise from the matching `clay-slip:globals:result` reply. - Per-key failure shapes are explicit, not collapsed into a single "error" code: `undefined` (key not on window), `not-serializable` (top-level function), `serialize-error` (circular structures), `access-error` (throwing getter), `bridge-unavailable` (CSP / Trusted Types blocked injection, or response timed out at 1s). - Concurrent reads are correlation-ID-routed β spamming Refresh can't cross-talk results between calls. - Forged messages get dropped via `event.source !== window` and unknown-correlation-ID checks, so a malicious iframe can't inject fake values into an in-flight read. Why inline-string + persistent listener (vs `chrome.scripting` with `world: 'MAIN'` or a separate WAR file): MAIN world needs Firefox 128+, our `strict_min_version` is 121, and a separate WAR entry would mean a Vite build entry, hashed filenames, and `chrome.runtime.getURL` resolution for ~30 lines of code. Inline- string is dramatically simpler. CSP / Trusted Types edge cases are caught and surfaced as `bridge-unavailable` rather than a silent hang. Tests (8/8 green): empty-keys fast path, single-key round-trip, each failure-reason variant propagated verbatim, concurrent reads without cross-talk, forged-correlation-ID rejection, timeout fallback to `bridge-unavailable`, cross-source message rejection, and `installPageBridge` idempotence. Next: Options UI for managing the global list, then the Globals tab. Co-authored-by: Cursor <cursoragent@cursor.com>
New section above Workflow on the Options page. Lets users add /
remove top-level window.* keys to surface in the upcoming Globals
panel tab. Mirrors the Site host mappings UX so there's nothing
new to learn.
UX details:
- Two-column row (input + remove). Simpler than the site-host
mappings 5-col layout because each entry is a single identifier.
- Placeholder shows both accepted shapes ("nymGtmPage or
window.dataLayer") so users discover the prefix-stripping
behavior without reading the help paragraph.
- Inline validation under each row, only shown after the user
has typed something β empty rows look "ready", not "broken".
- Each parse failure surfaces a reason-specific hint (dotted-
path vs bracketed vs invalid-identifier vs empty), so users
know whether they made a typo or hit a v1 limitation.
- Drafts state is kept separate from persisted state β invalid
rows hold their editor slot but contribute nothing to storage,
so fixing one row doesn't make sibling rows disappear.
- Hydration from chrome.storage is deliberately single-shot on
the first prefsLoaded transition. Re-hydrating on every prefs
change would clobber the row the user is typing (each keystroke
persists β mutates prefs β would re-fire the effect). Cross-
window sync isn't wired in this page anyway.
- The "+ Add" button appends a draft row without immediately
persisting (an empty draft contributes nothing), so spamming
Add doesn't pollute storage.
Help paragraph documents the v1 limits up front: no nested paths,
no array indices, and a heads-up that functions/symbols can't be
JSON-serialized (so they'll show as "not serializable").
CSS: new .options-globals-* classes for the 2-col layout, plus
.options-row-error for the per-row inline hint and aria-invalid
styling so screen readers + sighted users get the same signal.
Typecheck clean, lint clean. Next: panel-side Globals tab.
Co-authored-by: Cursor <cursoragent@cursor.com>
The reason the previous three commits exist. Adds the user-facing
'Globals' tab as the last entry in the panel's tab strip, rendering
the configured window.* globals as a collapsible card per key.
Tab behavior:
- Empty state when no globals are configured points the user at
the Options page (no other way to populate the tab).
- Each configured global is one <details> card, collapsed by
default. Cards reuse the .cs-jsonld-* class family so the
visual language matches the SEO tab's JSON-LD viewer.
- Card header shows `window.<key>` + a one-line shape summary
(e.g. "object Β· 14 keys", "array Β· 7 items") so users can
scan the tab without expanding everything.
- Two icon buttons per card: a per-row "Re-read" (refresh)
and a "Copy pretty-printed JSON" β the latter reparses the
bridge's compact JSON.stringify output before copying so
what lands on the clipboard is human-readable.
- A tab-level "Refresh all" button next to the section title.
Posts an info toast so the user gets feedback even if every
row was already cached.
- Body is only mounted once the user expands the row β defers
the syntax-highlight regex pass for huge payloads (some
dataLayer setups carry 10k+ entries) until requested.
Data flow (matches the spec):
- On mount, fire one readGlobals(prefs.windowGlobals); seed the
results map. Subsequent prefs changes diff the configured
list and only fetch the new keys (the user adding one global
in Options shouldn't refetch the world).
- Per-row Refresh fetches just that key.
- Tab-level "Refresh all" clears the fetched-set and re-reads
every key. No ambient polling anywhere.
- Failure shapes are surfaced explicitly per row instead of a
blanket "could not read":
undefined β "(not defined on this page)"
not-serializable β flags function/Symbol globals
serialize-error β embeds the underlying JS message
access-error β embeds the throwing-getter message
bridge-unavailable β CSP-blocked / timed-out fallback
Failure border tints follow severity β undefined/bridge-unavailable
get the neutral warn color (neither is actually a bug), the
other three get the alarming danger color.
Code hygiene:
- failureMessage + summarizeValue extracted to globals-format.ts
so the user-facing copy can be unit-tested without spinning
up React Testing Library just for one component.
- New `refresh` SVG icon added to Icon.tsx (circular-arrow glyph
legible at 14px).
- New .cs-section-header rule for "title + trailing action" rows;
reusable beyond this tab.
- New "globals" PanelTab in the store + a count badge in the
Tabs strip that mirrors the existing Notes-tab badge pattern.
Tests: 12 new for globals-format (distinct messages per failure
reason, null-vs-object disambiguation, plural correctness, primitive
fallback). All 31 Window-Globals-scope tests pass together. tsc + lint
clean.
Next: README + spec back-link, then validate + ship.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds the user-facing documentation for the feature and the lint /
prettier touch-ups from the validate pass.
README:
- New Highlights bullet between SEO and Recently viewed.
- New Usage table row ("Read a window global").
- New Configuration subsection with the input contract
("nymGtmPage" or "window.nymGtmPage", top-level only), the
refresh model (manual; no ambient polling), and every failure
message the panel can show β so users searching the README for
"(not defined on this page)" / "(could not read)" find an
explanation without digging into the code. Back-links the
spec document for the full design rationale.
PRIVACY:
- Updated date to 2026-05-19.
- Added a paragraph to "What the extension reads from the page"
spelling out the page-bridge mechanism: only the keys the user
explicitly configured, only when the Globals tab is opened or
refreshed, JSON-stringified and posted back to the panel,
nothing else.
- Added `windowGlobals` to the storage.sync description.
Lint / format housekeeping from the validate run:
- Dropped a setResults-in-effect call in GlobalsTab that was
pruning stale entries on `configured` change. The render path
already only iterates `configured`, so removed keys are simply
not displayed β the in-state pruning was tidiness, not
correctness. Net: no setState in the configured-keys effect,
which keeps the React-19 `set-state-in-effect` rule happy.
- One scoped eslint-disable on the Options hydration effect: it
IS a genuine async hydration from chrome.storage.sync, with no
derive-during-render equivalent. Comment explains why.
- Prettier sweep across the four files it flagged.
Full validate (typecheck + lint + format:check + 210 tests) passes.
release:dry:both builds and zips both Chromium and Firefox cleanly;
spot-checked that the bundled content script chunk contains the
clay-slip:globals:request bridge protocol and that the Firefox
manifest still has the gecko id + strict_min_version 121.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a new Globals panel tab that surfaces user-configured top-level
window.*values (e.g.nymGtmPage,dataLayer) as syntax-highlighted JSON. Configured per install via a new Window globals section on the Options page; both objects and arrays render identically.Reads happen on initial tab open and on explicit Refresh (per-row or tab-level). No ambient polling β zero measurable cost on the host page when the tab isn't open.
Design write-up:
docs/specs/2026-05-19-window-globals-tab.md.What's in this PR (commit-by-commit)
feat: add windowGlobals preference + parseWindowGlobal helperβ pure types/parser slice.UserPreferences.windowGlobals: readonly string[], plus aparseWindowGlobalthat accepts bothnymGtmPageandwindow.nymGtmPageshapes and returns tagged failure reasons (empty/dotted-path/bracketed/invalid-identifier) so the Options UI can show a hint per failure mode. 18 new unit tests (parser + storage round-trip).feat: add page-bridge for reading window globals from main worldβ content scripts run in an isolated world; this commit adds the bridge that injects a one-time<script>into the host page's main world. Persistent listener + correlation-ID-routed Promises so concurrent Refreshes can't cross-talk. Forged messages and bad-source iframes are dropped. Times out at 1s with a cleanbridge-unavailablefallback for strict-CSP / Trusted-Types pages instead of hanging forever. 8 new tests.feat(options): add 'Window globals' editor sectionβ add/remove rows, inline per-row validation that only shows after the user has typed something, draft state kept separate from persisted state so invalid rows don't make valid siblings disappear, single-shot hydration on the firstprefsLoadedtransition (re-hydrating on every change would clobber the row the user is typing).feat(panel): add Globals tab with refresh + collapsible cardsβ the actual tab. Each configured global is one<details>card (collapsed by default), with a one-line shape summary on the header (object Β· 14 keys,array Β· 7 items), a per-row Refresh button, a Copy-pretty-printed-JSON button, and a tab-level "Refresh all". Body is only mounted once expanded so hugedataLayerpayloads don't pay the syntax-highlight cost until requested. Reuses the.cs-jsonld-*class family so the visual language matches the SEO tab's JSON-LD viewer. 12 new format-helper tests.docs: document the Window Globals tab in README + PRIVACYβ Highlights bullet, Usage table row, full Configuration subsection (input contract, refresh model, every failure message the panel can show). PRIVACY paragraph spelling out the page-bridge mechanism (only configured keys, only on user action).Failure handling (surfaced inline in each row)
undefined(not defined on this page)not-serializable(value is not JSON-serializable β likely a function or Symbol)serialize-error(could not serialize: <underlying message>)access-error(reading the global threw: <underlying message>)bridge-unavailable(could not read from this page β strict CSP or page never responded)undefinedandbridge-unavailableget the neutral warn tint because neither is actually a bug β just a missing global or a strict-CSP page.Why inline-string bridge (vs
chrome.scriptingMAIN-world or a separate WAR file)chrome.scripting.executeScript({ world: 'MAIN' })requires Firefox 128+; ourstrict_min_versionis 121, so it would silently no-op on older Firefox.chrome.runtime.getURL, and divergent WAR semantics between Chromium and Firefox. For ~30 lines of bridge code it's the wrong trade.bridge-unavailablerather than a silent hang.Permissions
None added. The bridge runs entirely inside the existing content-script context β no
scriptingpermission, no host changes.Test plan
npm run validateβ typecheck + lint + format:check + 210 tests, all greennpm run release:dry:bothβ both Chromium and Firefox zips build cleanlyclay-slip:globals:requestbridge protocolstrict_min_version: 121.0nymGtmPagein Options, open Globals tab, expand the row, verify JSON renders, click Refresh, verify the bridge re-readsscript-src 'self', expectbridge-unavailableplaceholder per row, not a stuck spinnerScreenshots
TODO before merge:
(not defined on this page)row so reviewers see the neutral-warn tint.Made with Cursor