noxid-cli 0.2.0

The Noxid compiler command line: check, build, test, adapt, and the agent surface
import { configureDevtools, devtoolsSnapshot, devtoolsSetState, devtoolsReplayTransaction, devtoolsInspectDom } from "./noxid-runtime.js";
import { createDevtoolsRecorder } from "./noxid-devtools.js";
import { metadata } from "./noxid-devtools-metadata.js";
import { matchRoute, parseRouteQuery } from "./noxid-router.js";

const recorder = createDevtoolsRecorder({ capacity: 1000 });
const symbols = new Map(metadata.symbols.map((symbol) => [symbol.semanticId, symbol]));
const domScopes = new Map(metadata.symbols.filter((symbol) => symbol.domScope).map((symbol) => [symbol.domScope, symbol]));
const project = metadata.project ?? { title: "Noxid application", basePath: "/", routes: [] };
const tabs = ["overview", "matcher", "graph", "owners", "reactivity", "dom", "timeline", "diagnostics", "routes", "open-graph", "accessibility", "design", "state"];
let activeTab = "overview";
let selectedId = metadata.symbols.find((symbol) => symbol.kind === "component")?.semanticId ?? null;
let renderQueued = false;
let highlighted = [];
let detachedWindow = null;
let graphKind = "all";
let matcherValue = `${location.pathname}${location.search}`;
let matcherResult = null;
let pickedDom = null;
let adjacency = null;

const host = document.createElement("noxid-devtools");
host.setAttribute("aria-label", "Noxid DevTools");
const root = host.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host{color-scheme:dark;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;--bg:#0b1011;--panel:#111819;--line:#263235;--muted:#718487;--text:#d9e4e5;--accent:#65dce7}*{box-sizing:border-box}button,input,textarea,select{font:inherit}.trigger{position:fixed;right:18px;bottom:18px;z-index:2147483646;height:36px;padding:0 13px;border:1px solid #31545a;border-radius:7px;color:#c8fbff;background:#10191b;box-shadow:0 10px 28px #0008;cursor:pointer}.dot{display:inline-block;width:6px;height:6px;margin-right:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 10px var(--accent)}.panel{position:fixed;inset:10px;z-index:2147483647;display:none;grid-template-rows:auto auto minmax(0,1fr);border:1px solid var(--line);border-radius:10px;overflow:hidden;background:var(--bg);color:var(--text);box-shadow:0 24px 90px #000d}.panel[data-open]{display:grid}header{display:flex;align-items:center;justify-content:space-between;padding:13px 16px;border-bottom:1px solid var(--line);background:var(--panel)}h1{margin:0;font:650 13px/1.2 system-ui}h2{font:650 12px/1.2 system-ui}.subtitle{margin-top:3px;color:var(--muted);font-size:9px}.close{border:0;color:#91a2a4;background:transparent;font-size:20px;cursor:pointer}.tabs{display:flex;gap:1px;overflow:auto;border-bottom:1px solid var(--line);background:#0d1314}.tab{padding:9px 12px;border:0;border-bottom:2px solid transparent;color:#7f9295;background:transparent;cursor:pointer;font-size:9px;text-transform:uppercase;letter-spacing:.07em;white-space:nowrap}.tab[aria-selected=true]{border-color:var(--accent);color:#dfffff;background:#132022}.workspace{min-height:0;display:grid;grid-template-columns:280px minmax(0,1fr)}.rail{min-height:0;overflow:auto;border-right:1px solid var(--line)}.search-wrap{position:sticky;top:0;padding:9px;background:var(--bg);border-bottom:1px solid var(--line);z-index:1}.search,.field,.select{width:100%;height:32px;padding:0 9px;border:1px solid #2b393b;border-radius:5px;outline:0;color:var(--text);background:#12191a}.symbol{display:block;width:100%;padding:8px 11px;border:0;border-bottom:1px solid #182123;text-align:left;color:#aebdbe;background:transparent;cursor:pointer}.symbol:hover,.symbol[aria-current=true]{background:#142225;color:#e4feff}.symbol small{display:block;margin-top:3px;color:#607579}.content{min-width:0;overflow:auto;padding:15px}.cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:10px}.card,.row,.og-card{border:1px solid var(--line);border-radius:7px;background:var(--panel)}.card{padding:13px}.card strong{display:block;color:#f1f7f7;font-size:19px}.card span{color:var(--muted);font-size:9px;text-transform:uppercase}.row{margin-bottom:8px;padding:10px 12px}.row h3{margin:0 0 5px;color:#dce8e9;font-size:11px}.meta{color:#6f8588;font-size:9px;overflow-wrap:anywhere}.badge{display:inline-block;margin:2px 5px 2px 0;padding:2px 5px;border:1px solid #2c4144;border-radius:99px;color:#92cbd0;font-size:8px}.actions{display:flex;gap:7px;margin-top:9px;flex-wrap:wrap}.action{padding:5px 8px;border:1px solid #345055;border-radius:5px;color:#bde9ed;background:#122124;cursor:pointer;font-size:9px}.empty{padding:24px;color:var(--muted);font-size:10px;line-height:1.55}.tree{padding-left:14px;border-left:1px solid #273638}.payload{margin-top:7px;padding:8px;max-height:170px;overflow:auto;border-radius:5px;color:#91a3a5;background:#090d0e;font-size:8px;white-space:pre-wrap}.state-editor{width:100%;min-height:55px;margin-top:8px;padding:7px;border:1px solid #2b393b;border-radius:5px;color:var(--text);background:#090d0e}.toolbar{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:8px;margin-bottom:10px}.graph-canvas{display:block;width:100%;height:430px;border:1px solid var(--line);border-radius:7px;background:#090d0e;cursor:crosshair}.og-card{max-width:620px;padding:24px;background:linear-gradient(135deg,#132022,#0d1415)}.og-card h2{margin:0 0 10px;color:#e6feff;font-size:22px}.og-card p{margin:0;color:#8ea1a3;font:13px/1.5 system-ui}.og-card .meta{margin-top:24px}.error{color:#ff9c9c}.ok{color:#7ee3b3}@media(max-width:720px){.workspace{grid-template-columns:130px minmax(0,1fr)}.tab{padding-inline:8px}.toolbar{grid-template-columns:1fr}}
</style>
<button class="trigger" type="button" aria-expanded="false"><span class="dot"></span>NOXID · DEV</button>
<aside class="panel" aria-hidden="true"><header><div><h1>Noxid DevTools</h1><div class="subtitle">Compiler graph + live owned runtime</div></div><div class="actions"><button class="action detach" type="button">Detach</button><button class="close" type="button" aria-label="Close DevTools">×</button></div></header><nav class="tabs" role="tablist"></nav><div class="workspace"><aside class="rail"><div class="search-wrap"><input class="search" type="search" aria-label="Search semantic symbols" placeholder="Semantic ID…"></div><div data-symbols></div></aside><main class="content" data-content></main></div></aside>`;

const panel = root.querySelector(".panel");
const trigger = root.querySelector(".trigger");
const search = root.querySelector(".search");
for (const tab of tabs) { const button=document.createElement("button");button.className="tab";button.type="button";button.role="tab";button.textContent=tab;button.dataset.tab=tab;button.addEventListener("click",()=>{activeTab=tab;render()});root.querySelector(".tabs").append(button); }

function setOpen(open){panel.toggleAttribute("data-open",open);panel.setAttribute("aria-hidden",String(!open));trigger.setAttribute("aria-expanded",String(open));if(open)render()}
function detachedDocument(){return detachedWindow&&!detachedWindow.closed?detachedWindow.document:null}
function renderDetached(){const doc=detachedDocument();if(!doc)return;doc.querySelector("pre").textContent=JSON.stringify({metadata,live:model(),events:events().slice(-1000)},null,2)}
function detach(){detachedWindow=window.open("","noxid-devtools-standalone","popup,width=1180,height=800");if(!detachedWindow)return;detachedWindow.document.documentElement.innerHTML=`<head><title>Noxid DevTools</title><style>html{color-scheme:dark;background:#0b1011;color:#d9e4e5;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace}body{margin:0}header{position:sticky;top:0;padding:12px 16px;border-bottom:1px solid #263235;background:#111819}h1{margin:0;font-size:14px}pre{margin:0;padding:16px;white-space:pre-wrap;overflow-wrap:anywhere}</style></head><body><header><h1>Noxid DevTools · standalone live snapshot</h1></header><pre></pre></body>`;renderDetached()}
function esc(value){const node=document.createElement("span");node.textContent=String(value);return node.innerHTML}
function attr(value){return esc(value).replaceAll('"',"&quot;").replaceAll("'","&#39;")}
function model(){return devtoolsSnapshot()}
function events(){return recorder.snapshot()}
function filteredSymbols(){const query=search.value.trim().toLowerCase();return metadata.symbols.filter((symbol)=>!query||symbol.semanticId.toLowerCase().includes(query)||symbol.label.toLowerCase().includes(query)||symbol.kind.toLowerCase().includes(query))}
function renderRail(){const container=root.querySelector("[data-symbols]");container.replaceChildren();for(const symbol of filteredSymbols().slice(0,400)){const button=document.createElement("button");button.className="symbol";button.type="button";button.setAttribute("aria-current",String(symbol.semanticId===selectedId));button.innerHTML=`${esc(symbol.label)}<small>${esc(symbol.kind)}</small>`;button.addEventListener("click",()=>{selectedId=symbol.semanticId;render()});container.append(button)}}
function related(id){return metadata.relationships.filter((edge)=>edge.from===id||edge.to===id)}
function relationshipIndex(){if(adjacency)return adjacency;adjacency=new Map();for(const edge of metadata.relationships){if(!adjacency.has(edge.from))adjacency.set(edge.from,[]);if(!adjacency.has(edge.to))adjacency.set(edge.to,[]);adjacency.get(edge.from).push({id:edge.to,edge,direction:"out"});adjacency.get(edge.to).push({id:edge.from,edge,direction:"in"})}return adjacency}
function symbolRows(predicate){return metadata.symbols.filter(predicate).map((symbol)=>`<article class="row"><h3>${esc(symbol.label)}</h3><div class="meta">${esc(symbol.semanticId)} · ${esc(symbol.kind)}</div>${related(symbol.semanticId).slice(0,12).map((edge)=>`<span class="badge">${esc(edge.kind)} ${esc(edge.from===symbol.semanticId?"→ "+edge.to:"← "+edge.from)}</span>`).join("")}</article>`).join("")||'<div class="empty">No matching compiler semantics.</div>'}
function countKind(...kinds){return metadata.symbols.filter((symbol)=>kinds.includes(symbol.kind)).length}
function navigationDuration(){const entry=performance.getEntriesByType?.("navigation")?.[0];return Number.isFinite(entry?.duration)?`${Math.round(entry.duration)} ms`:"—"}
function overview(){const symbol=symbols.get(selectedId);const modes=Object.groupBy?Object.groupBy(project.routes,(route)=>route.render.mode):project.routes.reduce((groups,route)=>{(groups[route.render.mode]??=[]).push(route);return groups},{});const selected=symbol?`<article class="row"><h3>${esc(symbol.label)}</h3><div class="meta">${esc(symbol.semanticId)} · ${esc(symbol.kind)}</div><div class="actions"><button class="action" data-open-source>Open source · ${symbol.span.start}–${symbol.span.end}</button></div></article>`:'<div class="empty">Select a semantic symbol for source and relationship details.</div>';return `<h2>${esc(project.title)}</h2><div class="meta">${esc(project.basePath)} · Noxid ${esc(metadata.compilerVersion)} · metadata schema ${metadata.schemaVersion}</div><div class="cards" style="margin-top:12px"><div class="card"><strong>${project.routes.length}</strong><span>routes · ${Object.entries(modes).map(([mode,routes])=>`${routes.length} ${mode}`).join(" · ")||"none"}</span></div><div class="card"><strong>${countKind("endpoint","stream-endpoint")}</strong><span>endpoints</span></div><div class="card"><strong>${countKind("task")}</strong><span>tasks</span></div><div class="card"><strong>${countKind("queue")}</strong><span>queues</span></div><div class="card"><strong>${countKind("middleware")}</strong><span>middleware</span></div><div class="card"><strong>${navigationDuration()}</strong><span>page load</span></div><div class="card"><strong>${metadata.symbols.length}</strong><span>symbols</span></div><div class="card"><strong>${metadata.relationships.length}</strong><span>relations</span></div></div><section style="margin-top:12px">${selected}${symbol?related(selectedId).slice(0,40).map((edge)=>`<div class="row"><div class="meta">${esc(edge.from)} <b>${esc(edge.kind)}</b> ${esc(edge.to)}</div></div>`).join(""):""}</section>`}
function applicationPath(pathname){const base=project.basePath||"/";if(base==="/")return pathname;if(pathname===base)return "/";return pathname.startsWith(`${base}/`)?pathname.slice(base.length):null}
function runMatcher(value){try{const url=new URL(value||"/",location.origin);const pathname=applicationPath(url.pathname);if(pathname===null)return {error:`URL is outside base path ${project.basePath}.`};for(const route of project.routes){const params=matchRoute(route,pathname);if(params===null)continue;try{return {route,params,query:parseRouteQuery(route,url)}}catch(error){return {route,params,error:`${error.code??"ROUTE_QUERY_INVALID"}: ${error.message}`}}}return {error:"No route matched."}}catch(error){return {error:`Invalid URL: ${error.message}`}}}
function matcher(){if(matcherResult===null)matcherResult=runMatcher(matcherValue);const result=matcherResult;const detail=result.route?`<article class="row"><h3>${esc(result.route.pattern)}</h3><div class="meta">${esc(result.route.id)} · page ${esc(result.route.page)}</div><div><span class="badge">render ${esc(result.route.render.mode)}</span><span class="badge">cache ${esc(result.route.cache?.mode??"none")}</span></div><h3 style="margin-top:10px">Params</h3><pre class="payload">${esc(JSON.stringify(result.params,null,2))}</pre><h3 style="margin-top:10px">Query</h3><pre class="payload">${esc(JSON.stringify(result.query??{},null,2))}</pre><h3 style="margin-top:10px">Middleware chain</h3>${result.route.middleware.map((id)=>`<span class="badge">${esc(id)}</span>`).join("")||'<div class="meta">none</div>'}${result.error?`<div class="error" style="margin-top:10px">${esc(result.error)}</div>`:""}</article>`:`<div class="empty ${result.error?"error":""}">${esc(result.error??"Enter a URL.")}</div>`;return `<div class="toolbar"><input class="field" data-route-url aria-label="URL to match" value="${attr(matcherValue)}"><button class="action" data-match-route type="button">Match URL</button></div>${detail}`}
function impactFrom(id,limit=180,maxDepth=5){if(!id)return {items:[],truncated:false};const index=relationshipIndex();const seen=new Map([[id,{id,distance:0,via:null}]]);const queue=[id];while(queue.length&&seen.size<limit){const current=queue.shift();const distance=seen.get(current).distance;if(distance>=maxDepth)continue;for(const link of index.get(current)??[]){if(seen.has(link.id))continue;seen.set(link.id,{id:link.id,distance:distance+1,via:link.edge});queue.push(link.id);if(seen.size>=limit)break}}return {items:[...seen.values()],truncated:queue.length>0}}
function graph(){const impact=impactFrom(selectedId);const kinds=[...new Set(metadata.symbols.map((symbol)=>symbol.kind))].sort();const visible=impact.items.filter((item)=>graphKind==="all"||symbols.get(item.id)?.kind===graphKind);const boundaryKinds=new Set(["route","component","endpoint","stream-endpoint","task","queue","scenario"]);const affected=impact.items.filter((item)=>item.id!==selectedId&&boundaryKinds.has(symbols.get(item.id)?.kind));return `<div class="toolbar"><select class="select" data-graph-kind aria-label="Filter graph by symbol kind"><option value="all">All kinds</option>${kinds.map((kind)=>`<option value="${attr(kind)}" ${kind===graphKind?"selected":""}>${esc(kind)}</option>`).join("")}</select><span class="badge">${visible.length}/${impact.items.length} visible · max 180</span></div><canvas class="graph-canvas" data-graph aria-label="Relationship graph for ${attr(selectedId??"no selection")}"></canvas><div class="meta" style="margin:8px 0">Bidirectional traversal uses emitted app.devtools.json edges only; it never infers missing cross-wire relationships.${impact.truncated?" Result truncated at the bounded panel limit.":""}</div><h2>Impact boundary</h2>${affected.map((item)=>{const symbol=symbols.get(item.id);return `<article class="row"><h3>${esc(symbol?.label??item.id)}</h3><div class="meta">${esc(item.id)} · ${esc(symbol?.kind??"unknown")} · distance ${item.distance}</div></article>`}).join("")||'<div class="empty">No connected route, component, endpoint, task, queue, or scenario beyond the selection.</div>'}`}
function graphNodes(){const impact=impactFrom(selectedId);return impact.items.filter((item)=>graphKind==="all"||item.id===selectedId||symbols.get(item.id)?.kind===graphKind)}
function drawGraph(canvas){if(!canvas)return;const items=graphNodes();const ids=new Set(items.map((item)=>item.id));const width=Math.max(520,canvas.clientWidth||520);const height=430;const scale=window.devicePixelRatio||1;canvas.width=width*scale;canvas.height=height*scale;const context=canvas.getContext("2d");context.scale(scale,scale);context.clearRect(0,0,width,height);const groups=new Map();for(const item of items){if(!groups.has(item.distance))groups.set(item.distance,[]);groups.get(item.distance).push(item)}const positions=new Map();for(const [distance,group] of groups){if(distance===0){positions.set(group[0].id,{x:width/2,y:height/2});continue}for(let index=0;index<group.length;index+=1){const ring=distance+Math.floor(index/28)*.55;const slot=index%28;const count=Math.min(28,group.length);const angle=(slot/Math.max(1,count))*Math.PI*2-distance*.37;const radius=Math.min(width,height)*.085*ring;positions.set(group[index].id,{x:width/2+Math.cos(angle)*radius,y:height/2+Math.sin(angle)*radius})}}context.lineWidth=1;context.strokeStyle="#25383b";for(const edge of metadata.relationships){if(!ids.has(edge.from)||!ids.has(edge.to))continue;const from=positions.get(edge.from);const to=positions.get(edge.to);context.beginPath();context.moveTo(from.x,from.y);context.lineTo(to.x,to.y);context.stroke()}for(const item of items){const point=positions.get(item.id);const symbol=symbols.get(item.id);context.beginPath();context.arc(point.x,point.y,item.id===selectedId?7:4,0,Math.PI*2);context.fillStyle=item.id===selectedId?"#65dce7":symbol?.kind==="route"?"#d9a86c":symbol?.kind==="endpoint"?"#a987e8":"#668a8e";context.fill();if(item.id===selectedId||items.length<35){context.fillStyle="#b9c8ca";context.font="9px ui-monospace";context.fillText((symbol?.label??item.id).slice(0,28),point.x+8,point.y+3)}}canvas.__noxidNodes=[...positions].map(([id,point])=>({id,...point}))}
function owningComponent(id){const index=relationshipIndex();const seen=new Set([id]);const queue=[id];const ownership=new Set(["owns","contains","mounts","renders","binds","handles"]);while(queue.length&&seen.size<200){const current=queue.shift();const symbol=symbols.get(current);if(symbol?.kind==="component")return symbol;for(const link of index.get(current)??[]){if(link.direction!=="in"||!ownership.has(link.edge.kind)||seen.has(link.id))continue;seen.add(link.id);queue.push(link.id)}}return null}
function domIdsForSelection(){const symbol=symbols.get(selectedId);if(["dom-binding","dom-region"].includes(symbol?.runtimeKind))return [selectedId];return impactFrom(selectedId,200,4).items.map((item)=>symbols.get(item.id)).filter((item)=>["dom-binding","dom-region"].includes(item?.runtimeKind)).map((item)=>item.semanticId)}
function owners(){const current=model();const children=new Map();for(const owner of current.owners){const key=owner.parentOwnerId??"root";if(!children.has(key))children.set(key,[]);children.get(key).push(owner)}const draw=(parent)=>(children.get(parent)||[]).map((owner)=>`<div class="row"><h3>${esc(owner.label??owner.semanticId??owner.runtimeId)}</h3><div class="meta">${esc(owner.runtimeId)} · parent ${esc(owner.parentOwnerId??"root")}</div><div class="tree">${draw(owner.runtimeId)}</div></div>`).join("");return draw("root")||'<div class="empty">No live owners. Mount a component and interact with the page.</div>'}
function reactivity(){return symbolRows((symbol)=>["signal","computed","effect","watch"].includes(symbol.runtimeKind))}
function dom(){const live=model().dom.map((entry)=>{const owner=owningComponent(entry.semanticId);return `<article class="row"><h3>${esc(symbols.get(entry.semanticId)?.label??entry.semanticId)}</h3><div class="meta">${esc(entry.semanticId)} · ${entry.count} live node(s) · owner ${esc(owner?.semanticId??"unresolved")}</div></article>`}).join("");const picked=pickedDom?`<article class="row"><h3>Picked ${esc(pickedDom.semanticId??"unmapped element")}</h3><div class="meta">Owner ${esc(pickedDom.owner?.semanticId??"not represented by emitted edges")}</div></article>`:"";return `<div class="actions"><button class="action" data-pick-dom type="button">Pick element on page</button><button class="action" data-highlight type="button">Highlight selected ownership region</button></div><div style="margin-top:12px">${picked}${live||'<div class="empty">No live compiler-owned DOM bindings.</div>'}</div>`}
function timeline(){return events().slice(-300).reverse().map((event)=>`<article class="row"><h3>#${event.sequence} · ${esc(event.kind)}</h3><div class="meta">${esc(event.semanticId??event.runtimeId??"runtime")} · ${esc(event.transactionId??"")}</div><pre class="payload">${esc(JSON.stringify(event.payload,null,2))}</pre></article>`).join("")||'<div class="empty">Interact with the application to populate the timeline.</div>'}
function diagnostics(){const failures=events().filter((event)=>/failed|denied|loop\.detected|validation\.failed|focus-scope\.empty/.test(event.kind));return failures.map((event)=>`<article class="row"><h3 class="error">${esc(event.kind)}</h3><div class="meta">${esc(event.semanticId??event.runtimeId??"runtime")}</div><pre class="payload">${esc(JSON.stringify(event.payload,null,2))}</pre></article>`).join("")||'<div class="empty ok">No runtime diagnostics in the bounded timeline. Compiler diagnostics remain available through metadata and MCP.</div>'}
function routes(){return symbolRows((symbol)=>symbol.kind.startsWith("route")||symbol.kind==="layout"||symbol.kind.startsWith("middleware")||symbol.kind.includes("render-policy")||symbol.kind.includes("cache-policy"))}
function selectedRoute(){return project.routes.find((route)=>route.id===selectedId||`component:${route.page}`===selectedId||route.layouts.some((layout)=>`component:${layout}`===selectedId))??project.routes.find((route)=>route.metadata)??project.routes[0]??null}
function openGraph(){const route=selectedRoute();if(!route)return '<div class="empty">No routes are available.</div>';const options=project.routes.map((candidate)=>`<option value="${attr(candidate.id)}" ${candidate.id===route.id?"selected":""}>${esc(candidate.pattern)}</option>`).join("");const description=route.metadata?.description==null?"":`<p>${esc(route.metadata.description)}</p>`;const card=route.metadata?`<article class="og-card"><h2>${esc(route.metadata.title)}</h2>${description}<div class="meta">${esc(route.metadata.id)}</div></article>`:'<div class="empty">This route declares no route metadata.</div>';return `<div class="toolbar"><select class="select" data-og-route aria-label="Route for Open Graph preview">${options}</select><span class="badge">reduced metadata card</span></div>${card}<div class="meta" style="margin-top:10px">Phase 1 renders only compiler-known {id, title, description?}. Image, canonical URL, type, and social-card fields are not inferred.</div>`}
function accessibility(){return symbolRows((symbol)=>symbol.runtimeKind==="accessibility")}
function design(){return symbolRows((symbol)=>symbol.runtimeKind==="design-system")}
function state(){const current=model();const signalRows=current.signals.map((signal)=>`<article class="row"><h3>${esc(signal.semanticId??signal.runtimeId)}</h3><div class="meta">${esc(signal.type)} · ${esc(signal.ownerId??"unowned")}</div><textarea class="state-editor" data-state-id="${esc(signal.semanticId??"")}">${esc(JSON.stringify(signal.value))}</textarea><div class="actions"><button class="action" data-apply-state="${esc(signal.semanticId??"")}">Validate + apply</button></div></article>`).join("");const transactions=current.transactions.slice().reverse().map((tx)=>`<article class="row"><h3>${esc(tx.action??tx.id)}</h3><div class="meta">${esc(tx.id)} · ${tx.changes.length} signal(s) · no action side effects</div><div class="actions"><button class="action" data-replay="${esc(tx.id)}" data-direction="backward">Undo state</button><button class="action" data-replay="${esc(tx.id)}" data-direction="forward">Redo state</button></div></article>`).join("");return `<h2>Live signals</h2>${signalRows||'<div class="empty">No live signals.</div>'}<h2>Transactions</h2>${transactions||'<div class="empty">No recorded action transactions.</div>'}`}
function beginDomPicker(){setOpen(false);let candidate=null;let previousOutline="";const restore=()=>{if(candidate)candidate.style.outline=previousOutline};const inspectTarget=(target)=>{const element=target?.nodeType===1?target:target?.parentElement;const semantic=element?.closest?.("[data-noxid-semantic]")??null;if(semantic){const semanticId=semantic.getAttribute("data-noxid-semantic");return {element:semantic,semanticId,owner:owningComponent(semanticId)}}const scoped=element?.closest?.("[data-noxid-scope]")??null;const owner=scoped?domScopes.get(scoped.getAttribute("data-noxid-scope"))??null:null;return {element:scoped,semanticId:owner?.semanticId??null,owner}};const move=(event)=>{const next=inspectTarget(event.target).element;if(next===candidate)return;restore();candidate=next;if(candidate){previousOutline=candidate.style.outline;candidate.style.setProperty("outline","2px solid #65dce7","important")}};const cleanup=()=>{restore();document.removeEventListener("pointermove",move,true);document.removeEventListener("click",pick,true);window.removeEventListener("keydown",cancel,true)};const pick=(event)=>{event.preventDefault();event.stopPropagation();const inspected=inspectTarget(event.target);pickedDom={semanticId:inspected.semanticId,owner:inspected.owner};if(inspected.owner)selectedId=inspected.owner.semanticId;else if(inspected.semanticId)selectedId=inspected.semanticId;activeTab=inspected.owner?"graph":"dom";cleanup();setOpen(true)};const cancel=(event)=>{if(event.key!=="Escape")return;cleanup();setOpen(true)};document.addEventListener("pointermove",move,true);document.addEventListener("click",pick,true);window.addEventListener("keydown",cancel,true)}
function bindActions(container){container.querySelector("[data-open-source]")?.addEventListener("click",async(event)=>{const symbol=symbols.get(selectedId);window.dispatchEvent(new CustomEvent("noxid:open-source",{detail:symbol}));navigator.clipboard?.writeText(`${symbol.source}:${symbol.span.start}`);const moduleBase=import.meta.url.slice(0,import.meta.url.lastIndexOf("/")+1);const endpoint=new URL("../_noxid/devtools/open-source",moduleBase);endpoint.searchParams.set("source",symbol.source);endpoint.searchParams.set("offset",String(symbol.span.start));try{const response=await fetch(endpoint,{method:"POST"});event.currentTarget.textContent=response.status===204?"Location copied":"Opened in editor"}catch{event.currentTarget.textContent="Location copied"}});container.querySelector("[data-pick-dom]")?.addEventListener("click",beginDomPicker);container.querySelector("[data-highlight]")?.addEventListener("click",()=>{for(const node of highlighted)node.style?.removeProperty("outline");highlighted=[...new Set(domIdsForSelection().flatMap((id)=>devtoolsInspectDom(id)))];for(const node of highlighted)node.style?.setProperty("outline","2px solid #65dce7","important")});const routeInput=container.querySelector("[data-route-url]");const match=()=>{matcherValue=routeInput.value;matcherResult=runMatcher(matcherValue);render()};container.querySelector("[data-match-route]")?.addEventListener("click",match);routeInput?.addEventListener("keydown",(event)=>{if(event.key==="Enter")match()});container.querySelector("[data-graph-kind]")?.addEventListener("change",(event)=>{graphKind=event.currentTarget.value;render()});const canvas=container.querySelector("[data-graph]");canvas?.addEventListener("click",(event)=>{const box=canvas.getBoundingClientRect();const x=event.clientX-box.left;const y=event.clientY-box.top;const nearest=(canvas.__noxidNodes??[]).map((node)=>({...node,distance:Math.hypot(node.x-x,node.y-y)})).sort((left,right)=>left.distance-right.distance)[0];if(nearest&&nearest.distance<18){selectedId=nearest.id;render()}});container.querySelector("[data-og-route]")?.addEventListener("change",(event)=>{selectedId=event.currentTarget.value;render()});for(const button of container.querySelectorAll("[data-apply-state]")){button.addEventListener("click",()=>{const area=container.querySelector(`[data-state-id="${CSS.escape(button.dataset.applyState)}"]`);try{devtoolsSetState(button.dataset.applyState,JSON.parse(area.value));button.textContent="Applied"}catch(error){button.textContent=error.message;button.classList.add("error")}})}for(const button of container.querySelectorAll("[data-replay]")){button.addEventListener("click",()=>{try{devtoolsReplayTransaction(button.dataset.replay,button.dataset.direction);render()}catch(error){button.textContent=error.message}})}}
function render(){for(const button of root.querySelectorAll(".tab"))button.setAttribute("aria-selected",String(button.dataset.tab===activeTab));renderRail();const content=root.querySelector("[data-content]");const views={overview,matcher,graph,owners,reactivity,dom,timeline,diagnostics,routes,"open-graph":openGraph,accessibility,design,state};content.innerHTML=views[activeTab]();bindActions(content);if(activeTab==="graph")drawGraph(content.querySelector("[data-graph]"))}
function scheduleRender(){if(renderQueued)return;renderQueued=true;requestAnimationFrame(()=>{renderQueued=false;if(panel.hasAttribute("data-open"))render();renderDetached()})}
configureDevtools((event)=>{recorder.emit(event);scheduleRender()},{sessionId:`noxid-dev:${location.pathname}`,maxDepth:6,maxItems:100});
trigger.addEventListener("click",()=>setOpen(!panel.hasAttribute("data-open")));root.querySelector(".close").addEventListener("click",()=>setOpen(false));root.querySelector(".detach").addEventListener("click",detach);search.addEventListener("input",renderRail);window.addEventListener("keydown",(event)=>{if(event.ctrlKey&&event.shiftKey&&event.key.toLowerCase()==="d"){event.preventDefault();setOpen(!panel.hasAttribute("data-open"))}});document.documentElement.append(host);trigger.title="Noxid DevTools (Ctrl + Shift + D)";render();
globalThis.__NOXID_DEVTOOLS__=Object.freeze({metadata,recorder,snapshot:devtoolsSnapshot,setState:devtoolsSetState,replay:devtoolsReplayTransaction,inspectDom:devtoolsInspectDom,matchUrl:runMatcher,impact:impactFrom,pickDom:beginDomPicker,open:()=>setOpen(true),close:()=>setOpen(false),detach});