@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; }

Debugging with Agents

An LLM agent can troubleshoot a Nucleus Stack app the way you would in DevTools: read the State, ask why an attribute has its value, test an expression, then edit the sheet. The Nucleus DevTools extension exposes that as tools, on development and production pages alike, with no change to the app and no runtime cost.

What the agent gets

Eleven read-only tools. Each takes plain JSON, returns plain JSON, and addresses elements by CSS selector; every element in a result carries a unique selector the agent can pass straight back.

Tool Use it when
diagnostics Anything is wrong. Failed expressions, effect errors, loop-guard trips, definition audits.
state_snapshot You need the State: an element tree with attributes, provision data and Quark $variables.
inspect_element One element: props, $variables, listeners, the rules matching it now, its recent history.
explain_attribute "Why is is-open still set?" Every recorded write (rule + expression + result, or Neutron effect), the rules that could write it.
list_sheets / matching_rules Which sheets exist, which rules apply to an element and in what order.
evaluate_expression Test a Quark expression in an element's context before editing the sheet.
trace What happened after an action: page-wide publications, filterable by element, path, or sinceSeq.
list_definitions Which Neutron elements are defined, their attributes, audit findings.
heatmap_top Which elements paint most (fan-out / cycle triage).
dump One JSON bug report of all of the above.

Tool descriptions carry the facts an agent tends to get wrong about this stack: rules never revert when they stop matching, the later matching rule wins, attributes (not classes) drive rules, rich data lives in prop("provision").

Setup

Install the extension from the Chrome Web Store in the Chrome you debug with, then point chrome-devtools-mcp at that profile with the third-party tools category enabled:

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

--autoConnect drives your real browser (Chrome 144+, one-time toggle in chrome://inspect/#remote-debugging). The agent then finds a Nucleus Stack group through list_3p_developer_tools and calls tools with execute_3p_developer_tool, alongside chrome-devtools-mcp's own navigation, DOM, console and network tools.

Any other browser automation

The same functions live on the page, so Playwright MCP, Cursor's browser, Puppeteer or a console work without flags:

__NUCLEUS_DEVTOOLS__.listTools();
__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' });

No extension available

Evaluate agent-tools.js from the extension's build output (.output/chrome-mv3/ after pnpm run build in packages/nucleus-devtools) in the page (evaluate_script, addScriptTag, or a <script> when CSP allows). It installs the same tools; history starts at injection, so boot-time records are missed. With chrome-devtools-mcp, call list_3p_developer_tools after injecting.

A workflow that works

  1. diagnostics — an error usually names the attribute and the expression.
  2. state_snapshot on the failing region — confirm what the State actually is.
  3. explain_attribute on the attribute that looks wrong — the last write stands; find the rule that should have written its inverse.
  4. matching_rules / evaluate_expression — test the fix against the live element.
  5. Edit the sheet, reload, trace({ sinceSeq }) to confirm the new run.

Add this to your AGENTS.md so the agent starts there:

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.

Sharing a bug

The extension's Element pane has a Copy for AI button. It copies dump() as 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.

Limits

  • Tools are read-only. evaluate_expression runs Quark's evaluator, which cannot assign; a @use function it calls can still side-effect.
  • Output is capped: 500 trace records, 200 diagnostics, 200 history records per element and producer, 500 snapshot nodes by default; long strings are clipped with … [+n].
  • chrome-devtools-mcp's third-party tools are experimental and need the flag above; the page-global API is the stable fallback.
  • Boot-time history needs the extension installed before the page loads (reload once after installing).

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.