@use "/shell" as *; @use "quark:list" as list; #release-notice { /* opens once: sheet re-runs restart a delay, so gate it on the fact it writes */ &:not([data-did-open]) { @delay 7000 { is-open: ""; data-did-open: ""; } } /* prevent clicks from bubbling to the sheet */ @on mouseup, click (stop-propagation); } provider-fetch[api-url*="package-metas/index.json"][is-success] { $package-indices: prop("provision").body; /* `excom.navGroup` moves a package out of its type's list, into that group */ $ungrouped: list.reject($package-indices.packages, "navGroup"); .package-links { [bind-elements] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "kit-element")); } [bind-element-bases] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "element-base")); } [bind-tools] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "tool")); } /* the standalone libraries: same rows, rendered flat (shell.css) */ [bind-libraries] ul:not(ul ul) { content: iterate(getPackagesByType($ungrouped, "library")); } [bind-library-group] ul:not(ul ul) { content: iterate(list.filter($package-indices.packages, "navGroup", "libraries")); } ul:not(ul ul) > li { $pkg: item.shortName; > spa-a { route-href: "/packages/#{item.shortName}"; } } /* one `content` rule per link: a second one would rewrite the first's text on every pass */ ul:not(ul ul, [bind-libraries] ul) > li > spa-a { content: item.shortName; } [bind-libraries] ul:not(ul ul) > li > spa-a { content: displayName(item.shortName); } /* packages whose docs span several pages: one collapsible group per section */ [bind-sections] { content: iterate(item.docSections); summary { content: item.title; } details > ul { content: iterate(item.docs); spa-a { route-href: "/packages/#{$pkg}/#{item.name}"; content: item.title; } } details:not(:has(spa-a[is-active])) { open: none; } } } /* page titles: the page, then the site. The docs home keeps the title in the document head; the other routes carry theirs in markup */ spa-route[is-active] { /* a site guide */ &[route-href$=":name"] { $title-guide: list.find($package-indices.docs, "name", $route.params.name); document-title: "#{$title-guide.title or $route.params.name} · Nucleus · docs"; } /* a package README */ &[route-href$=":packageName"] { document-title: "#{displayName($route.params.packageName)} · Nucleus · docs"; } /* one page of a package's docs */ &[route-href$=":docName"] { $title-package: list.find($package-indices.packages, "shortName", $route.params.packageName); document-title: "#{docTitle($title-package, $route.params.docName)} · #{displayName($route.params.packageName)} · Nucleus · docs"; } } /* the page's markdown file, for the footer link: the docs home and each guide the index lists, and a package's page whose index entry says `markdown` (build-docs-index sets it; the dev server's index has none, so no link there). siteDocHref and the path check say which URL is the page's own: the 404 and a package's doc pages have none */ > spa-manager[active-url] { $page-path: attr("active-url").split("#").at(0).split("?").at(0); $page-name: if($page-path == SITE_HOME: SITE_HOME_DOC; else: $page-path.split("/").at(-1)); $page-guide: list.find($package-indices.docs, "name", $page-name); $page-package: list.find($package-indices.packages, "shortName", $page-name); $page-markdown: if($page-guide and siteDocHref($page-guide.name) == $page-path: "/docs/#{$page-guide.name}.md"; $page-package.markdown and "#{SITE_BASE}/packages/#{$page-package.shortName}" == $page-path: "/#{$page-package.shortName}.md"); footer [bind-page-markdown] { href: $page-markdown; content: ternary($page-markdown, "Markdown version of this page"); } } } spa-route { $route: prop("provision"); /* paramless routes name their guide in markup (the docs home, `/`); /docs/:name gets it from params */ $route-doc-name: attr("data-doc-name"); } /* the desktop aside and the mobile sheet stamp the same nav template */ [data-site-nav] { details:has(spa-a[is-active]) { /* not kosher */ open: ""; } spa-a[is-active] { aria-current: ""; } spa-a:not([is-active]) { aria-current: none; } } /* mobile sheet: Escape / back gesture while open; a tapped link closes it */ #site-menu { &[is-open] dismiss-watcher { is-active: ""; } &:not([is-open]) dismiss-watcher { is-active: none; } @on click (target: "spa-a") { is-open: none; } } #site-menu-gesture { &:has(> #site-menu[is-open]) { progress-offset: 1; } &:not(:has(> #site-menu[is-open])) { progress-offset: 0; } @on gesture-handler-start { #site-menu { is-scrubbing: ""; } } @on gesture-handler-end { #site-menu { is-open: event.detail.snap == 1; is-scrubbing: none; } } } main { @on copy-source (handle: copySource); } #search-dialog[open] { > include-content { is-active: ""; /* for some reason this is necessary on first render */ @on include-content-did-render (handle: focusInput); } input[type="search"] { /* works on all subsequent opens */ autofocus: ""; } } #search-dialog:not([open]) input[type="search"] { autofocus: none; }

nucleus-devtools

See exactly what every element did and why — Neutron lifecycles and Quark rule applications, live, inside Chrome DevTools.

The Nucleus DevTools extension adds an Element pane to the Elements panel. Select any node and read its history and current state from both halves of the Nucleus Stack:

  • Neutron — every lifecycle effect the element applied (onConnected(), onPropSet("label"), …) with the exact props, events and calls it produced, plus the element's current props.
  • Quark — every rule that matched the element, grouped per rule run (selector, the declarations it applied, the values they resolved to), plus the element's current $variables, Quark-written attributes and CSS custom properties, listeners and iterate() row context.

State is the DOM, so the Elements panel already shows what changed. This pane shows who changed it and how.

Features

  • Lifecycle history Effects in order, nested by re-entrancy, with the lifecycle that fired them
  • Orchestration history Matched rules per run: selector, declarations, resolved values, no-op / wipe / error status
  • Hover for source Hover any key to see its declaration; hover a listener to see the expression that attached it
  • Current state Props, $variables, attributes, custom properties, listeners, loop item / index
  • Live New publications append as they happen; picking another node re-reads its history
  • Zero setup Works on any page using @excom/neutron or @excom/quark; no app changes, no debug flags
  • Paint heatmap A toolbar toggle overlays every Quark-painted element with its paint count, blue → red = fewest → most, for the whole document
  • Definition audits Every Neutron element definition is checked for non-dashed, data- / aria- attribute names and prop names that override HTMLElement members; warnings go to the page console
  • Agent tools Eleven selector-addressed, JSON-only tools (diagnostics, state_snapshot, explain_attribute, …) for coding agents, discoverable by chrome-devtools-mcp and callable from any browser automation; a Copy for AI button in the pane produces a one-file bug report
  • Familiar Styled after the Styles tab — same type, colors and hairlines

Installation

Install from the Chrome Web Store (also loads in Edge, Brave and other Chromium browsers). It updates itself from there.

To run a local build instead, build the extension from the repository with pnpm run build in packages/nucleus-devtools, open chrome://extensions, enable Developer mode and choose Load unpacked on .output/chrome-mv3. A build installed this way does not update itself.

The extension collects no data and makes no network requests — see the privacy policy.

Usage

  1. Open DevTools on a page built with the Nucleus Stack.
  2. Select an element in the Elements panel.
  3. Open the Element pane in the sidebar (next to Styles / Computed) and switch between the Neutron and Quark tabs.

Pages opened before the extension was installed need one reload: the probe records from the first byte of the document, so nothing is missed after that.

Rows

Each row is one trigger and the changes it applied — a lifecycle handler for Neutron, a rule run for Quark. Expand a value to walk into it; hover a key to see the declaration that produced it.

▾ provider-fetch[is-success] h1#title      ← the rule that matched
    content: "Hello devtools"              ← hover: content: $todo.title
    data-x: "(no-op)"                      ← preserve left it alone

Rows are muted for no-ops / wipes and red for a failed expression (which never wipes — see Orchestrating).

Heatmap

Click the extension's toolbar icon and tick Paint heatmap (whole document). It is off by default and applies to every open tab for the rest of the browser session. While on, each element Quark has painted since the page loaded is overlaid with a translucent box and its paint count, colored from blue (fewest paints) to red (most). The overlay is drawn by the extension only: nothing is written to the page's elements, and turning the toggle off removes it.

Definition audits

When a page defines a Neutron element the extension checks its prop config and warns in the page console (prefixed [nucleus-devtools]) about custom attributes without a dash, attributes starting with data- / aria-, and prop names that already exist on HTMLElement. Nothing is logged for a clean definition.

Agent tools

The probe the extension installs on every page also exposes a tool API for coding agents, so an LLM can troubleshoot a Nucleus Stack app (development or production) without any change to the app: the runtime only carries the dormant hook it always had. Every tool takes and returns plain JSON, addresses elements by CSS selector (results include a unique selector for each element they mention, ready to pass back), caps its output, and never writes to the page.

Tool Answers
diagnostics Failed Quark expressions, Neutron effect errors, loop-guard trips, definition audits. Start here.
state_snapshot The application State as a tree: attributes, each element's provision, Quark $variables, text.
inspect_element Everything about one element: props, $variables, listeners, matching rules, recent history.
explain_attribute Every recorded write to one attribute (rule + expression + result, or Neutron effect), plus the rules that could write it.
list_sheets Registered Quark sheets, optionally with source and rules.
matching_rules Rules matching an element right now, in the order they apply.
evaluate_expression A Quark expression evaluated in an element's context ($bindings, attr(), prop(), @use modules).
trace Page-wide publication log, filterable by path, element and sinceSeq for "what happened after I clicked".
list_definitions Neutron element definitions seen on the page with props and audit findings.
heatmap_top Most-painted elements since load.
dump One JSON bug report bundling all of the above (what Copy for AI copies).

Descriptions carry the ASO facts an agent needs (rules never revert, later rules win, attributes not classes drive rules, prop("provision") for rich data), so a fresh model can reason correctly from the first call.

The tools register with chrome-devtools-mcp's third-party developer tools protocol. Configure the server with --autoConnect (drives your real Chrome profile, where the extension is installed; Chrome 144+, one-time toggle in chrome://inspect/#remote-debugging) and the experimental third-party category:

{
  "mcpServers": {
    "chrome-devtools": {
      "command": "npx",
      "args": ["chrome-devtools-mcp@latest", "--autoConnect", "--categoryExperimentalThirdParty"]
    }
  }
}

The agent then sees a Nucleus Stack tool group via list_3p_developer_tools and calls each tool with execute_3p_developer_tool. Chrome for Testing / --browserUrl setups work too, as long as the extension is loaded in that profile.

With any evaluate (Playwright MCP, Cursor browser, Puppeteer, …)

The same functions live on the page global, so no protocol support is needed:

__NUCLEUS_DEVTOOLS__.listTools();                       // descriptors with JSON Schemas
__NUCLEUS_DEVTOOLS__.tools.diagnostics({});
__NUCLEUS_DEVTOOLS__.tools.explain_attribute({ selector: "#cart", name: "is-open" });
await __NUCLEUS_DEVTOOLS__.tools.evaluate_expression({ selector: "#cart", expression: "prop(\"provision\").items.length" });

Without the extension

agent-tools.js (.output/chrome-mv3/agent-tools.js after pnpm run build) installs the same probe and tools when evaluated in a page: evaluate_script with the file's contents, Playwright addScriptTag, or a <script> tag when the page's CSP allows. Evaluating it returns { installed, tools, hint }. What it cannot do is see the past: histories start at injection, neutron/defined records from boot are missed, and chrome-devtools-mcp only re-discovers tools on list_3p_developer_tools, so call that after injecting.

Copy for AI

The pane's Copy for AI button puts dump() on the clipboard as pretty JSON: the State tree, every sheet with source and rules, definitions, diagnostics, the last 200 publications and the paint heatmap. Paste it into a chat with the failing selector and the question.

Trade-offs

  • Extension required for boot-time history. Sites run without any runtime cost, but the agent's browser must have the extension (or inject agent-tools.js and accept a history that starts late).
  • Experimental protocol. chrome-devtools-mcp's third-party tools need --categoryExperimentalThirdParty and may change; the page-global API is the stable fallback.
  • Read-only by design. evaluate_expression runs Quark's evaluator, which cannot assign; a @use function it calls can still side-effect.
  • Caps. 500 trace records, 200 diagnostics, 200 history records per element and producer, 500 snapshot nodes by default; long strings are clipped with … [+n].

Snippet for AGENTS.md

When debugging a Nucleus Stack page, use the "Nucleus Stack" DevTools tools
(chrome-devtools-mcp `list_3p_developer_tools`, or
`__NUCLEUS_DEVTOOLS__.tools.*` via evaluate). Order: `diagnostics` →
`state_snapshot` → `explain_attribute` for the attribute that looks wrong →
`matching_rules` / `evaluate_expression` to test the fix before editing the
sheet. State lives in attributes and `provision`; rules never revert and the
later matching rule wins.

Programmatic hook

The extension installs the same global hook that Neutron.attachDevtools() / Quark.attachDevtools() accept, so tests and tooling can subscribe without the browser:

import { Quark } from "@excom/quark";
​
Quark.attachDevtools({
  version: 1,
  publicize: (path, meta) => console.log(path.join("/"), meta),
});

Publications are ["neutron", …] / ["quark", …] paths with JSON-safe metadata; see the neutron and quark package pages for the full list.

Beta. The Nucleus Stack is in beta for a few weeks until features are stabilized and optimized.

Thanks — we'll email you when it ships.

Something went wrong. Please try again.