DevTools.
aktion-runtime/devtools is an in‑page debugger and QA workbench for
every <aktion-app> on the document. Mount it with one line —
there is no browser extension — and you get sixteen sections over the running
program: an element picker and component inspector you can edit live, an editable
$state tree with time travel, the query cache and browser storage, a
unified timeline, a network inspector that mocks and throttles, a console with an
expression REPL, a flame‑chart profiler with Core Web Vitals, a WCAG 2.2
accessibility audit, a security scanner, a test recorder that exports Aktion and
Playwright tests, a live source editor, and a theme token editor.
What it is
DevTools is a second, opt‑in entry point of the same package. It renders inside its own shadow root — floating, or docked to an edge with the page pushed aside — so the app it inspects cannot restyle it, and it keeps working while that app is throwing. Everything it shows is the real runtime: edits go through the genuine reactive pipeline, the profiler reads the actual memoisation decisions, and the network view taps the real HTTP layer.
The sections, in the order the sidebar shows them:
Overview Home
Health, performance at a glance, render activity, what drives re-renders, and one-click actions.
Inspect Inspect
Pick any element to find its component; edit props, hooks and internal state; box model, styles, accessibility.
State Inspect
Every atom, editable in place, with activity heat, a leaf-level diff, a change log, a reactivity graph, and time travel.
Data Inspect
The query cache (refetch, invalidate, simulate loading or failure), stores and forms, and browser storage.
Routes Inspect
The current route, every declared arm with a form for its params, a match tester, coverage, and history.
Timeline Activity
Interactions, commits, state, effects, requests, routes, events, logs, errors and long tasks on one zoomable axis.
Network Activity
Requests with a waterfall, bodies and timings; copy as cURL or fetch, export HAR; mock, delay, fail, throttle.
Console Activity
Program and runtime logs, an expression REPL with completion and history, and pinned watch expressions.
Effects Activity
Every mounted effect with its triggers and cost, a per-effect timeline, and the full lifecycle log.
Performance Performance
Commit chart, flame chart, ranked and per-component costs, why each render happened, insights, and web vitals.
Accessibility Quality
A WCAG 2.2 audit numbered on the page, the accessibility tree, a keyboard focus walk, and vision simulation.
Security Quality
What the program can reach, sanitiser escapes in the DOM, origins, secrets in storage, headers and CSP.
Testing Quality
Record and replay flows, export Aktion or Playwright tests, scenarios, coverage, a query playground, chaos, emulation.
Source App
The program with diagnostics on their lines, an outline, find, a live-validated editor, history, diff, and revert.
Theme App
Every design token, editable live, with colour pickers, contrast checks for the pairs the library paints, and export.
Settings System
Appearance, behaviour, runtime instrumentation, session export/import, shortcuts, and versions.
A section earns a badge on the sidebar only when something needs you — failing queries, unmatched navigations, serious accessibility issues, high-severity security findings, program errors, a recording in progress — so a quiet sidebar means a healthy app.
Enable it
The runtime never installs the panel. You do, from your own code, and the whole surface is one function call.
From a bundler
import "aktion-runtime"; // registers <aktion-app>
import { mountDevtools } from "aktion-runtime/devtools";
mountDevtools(); // opens immediately, attaches to every app on the page
mountDevtools() installs the global hook, appends the panel to
document.body, and adopts every <aktion-app> already in
the document — including apps that mounted before the panel existed.
From a CDN
<script type="module">
import "https://cdn.jsdelivr.net/npm/aktion-runtime/dist/aktion.js";
import { mountDevtools } from "https://cdn.jsdelivr.net/npm/aktion-runtime/dist/devtools.js";
mountDevtools();
</script>
Both bundles are self‑contained ES modules. The DevTools bundle talks to the runtime only through the global hook, which is why loading them from two different URLs still works.
Behind a flag
Because it is a separate entry, a dynamic import() keeps the panel out of
your main chunk until someone asks for it. Once mounted, the panel binds its own
page‑wide shortcut, Shift + Alt + D,
to show and hide itself.
let devtools = null;
async function openDevtools() {
if (devtools) return devtools.open();
const { mountDevtools } = await import("aktion-runtime/devtools");
devtools = mountDevtools(); // from here on, Shift+Alt+D toggles it
}
if (new URLSearchParams(location.search).has("debug")) openDevtools();
Mount once and keep the controller: every call to mountDevtools() adds
another panel element, even though they all share one hook and one event stream.
Client only
mountDevtools() needs a live document.
Under SSR, call it from a client‑only effect (useEffect,
onMounted, a <script> in the browser bundle), never at
module scope in shared code.
Try it live
The app below is an ordinary <aktion-app> rendered from the program on
the left. Press Launch DevTools to open the real panel over this page,
then use the app and watch the panel fill up. Inside it, press
Ctrl / Cmd + K for the command
palette or ? for every shortcut.
$count = 0
$seconds = 0
$user = { name: "Ada", role: "engineer", prefs: { notify: true } }
// An interval effect: one `run` per second, so the timeline fills live.
$effect(() => { $seconds = $seconds + 1 }, ["every(1000)"])
// A user component: its own instance, and its own memo record, in the profiler.
function Stat(label, value, tone) {
return Card([Column([
Text(label, { variant: "small", tone: "muted" }),
Text(value, { variant: "title" }),
Badge(tone, { tone: tone })
], { gap: "xs" })])
}
$app(Column([
CardHeader("DevTools demo", { subtitle: "Open the inspector, then interact." }),
Row([Stat("Count", `${$count}`, "info"), Stat("Uptime", `${$seconds}s`, "success")], { grow: true }),
Button("Increment", { onClick: () => $count = $count + 1, variant: "primary" }),
Text(`Signed in as ${$user.name} (${$user.role})`, { variant: "small", tone: "muted" })
], { gap: "md" }))
Four things to try, in order:
- Inspect — press the crosshair (or Shift + Alt + C) and click a card: you land on its
Statinstance with its arguments, editable. - State — expand
user→prefs, clicktrue, typefalse, press Enter. The write lands in the real store and the app re‑renders. - Performance — press Increment a few times, then open Why to see which atom each commit was for and why every component rendered or was skipped.
- Accessibility — run the audit, switch on Markers, then try Structure → Start focus walk.
The panel
The panel floats by default — drag its title bar anywhere, resize it from any edge or corner, double‑click the title bar to maximise. Drag it against a screen edge and it snaps into a dock; drag a docked panel away from its edge and it tears off into a window again. Docked right, left, or at the bottom, it pushes the page aside the way browser DevTools shrink the viewport, so the app stays fully visible (switch this off in Settings to overlay instead).
- Minimise collapses it to a small launcher pill that shows the error and warning counts; drag the pill wherever it is out of the way, right‑click it for options.
- Themes: dark, light, or follow the system; comfortable or compact density; reduced motion is respected unless you override it.
- The status bar is always live: commits and the last commit's cost, frame rate, INP, requests in flight, JS heap, errors and warnings. Each one is a shortcut to the section that explains it.
- Live / Paused in the title bar stops the panel ingesting events so you can read a moving timeline. Paused events are dropped, not queued, and the button counts them —
Paused · 34— instead of looking hung. - Several apps? The app switcher in the title bar lists every
<aktion-app>on the page, plus any session files you imported. Name apps withdata-devtools-label.
Typing is never interrupted
The panel re‑renders on runtime events, several times a second in a busy app. It is built on a keyed reconciler that updates the DOM in place, so the field you are typing in keeps its focus, caret and draft, lists keep their scroll offset, and charts keep their zoom while events stream in behind them.
The command palette, and the keyboard
Press Ctrl / Cmd + K inside the panel, or Shift + Alt + K anywhere on the page, and type. The palette fuzzy‑matches every section, every action a section offers (run accessibility audit, emulate offline, simulate deuteranopia, start recording a test), every component on screen (Inspect Counter), every atom, every declared route, and every panel command (dock to the right, export the session, copy a bug report).
| Key | Where | Does |
|---|---|---|
| Shift + Alt + D | Anywhere | Show or hide DevTools. |
| Shift + Alt + C | Anywhere | Pick an element to inspect. |
| Shift + Alt + K | Anywhere | Open the command palette. |
| Alt + 1…9 | Anywhere | Jump to a section. |
| Alt + [ / ] | Anywhere | Previous / next section. |
| Ctrl / Cmd + K | Panel | Command palette. |
| ? | Panel | Every shortcut. |
| / | Panel | Focus the current section's filter. |
| Escape | Panel | Back out of a menu, dialog, the palette, the picker, or an edit. |
The page‑wide shortcuts use Shift + Alt so they cannot collide with the app's own bindings — Ctrl/Cmd + K is how half of all apps open their search — and none of them fire while you are typing in one of the app's fields.
Watching an app while you use it
- Highlight re‑renders. Outlines every component as it renders, with a running count; renders a forced commit caused although nothing the component reads changed glow amber. It is the fastest way to find wasted work. Toggle it from Overview, Performance, the palette, or Settings.
-
Watch expressions. Type an Aktion expression in the Console
—
$todos.length,$user.role— and press the eye: it is re‑evaluated on every render and shown in the Console and on the Overview. Watches survive a reload. -
Break on change. Arm an atom in State and the browser pauses in
its debugger the moment anything writes to it, with the write on the stack (the
browser's own DevTools must be open, as for any
debuggerstatement). -
User Timing marks. Mirror every commit into
performance.measure(), so Aktion commits line up with layout and paint in the browser's Performance panel. - Web vitals, live. Frame rate, INP (with input delay, processing and presentation for every interaction), LCP, CLS, long tasks and long‑animation frames are measured while the panel is open. Clicks on the panel itself are excluded, so the numbers are the app's.
Inspect
Press the crosshair and hover the page: the box model is drawn over the element with its component, size, role and accessible name, and a contrast chip for text. Click to select. While picking, Alt + wheel or ↑/↓ walks to the parent and back, so you can reach a container that has no gap of its own to click. The picker's crosshair takes the click, so picking a Delete button does not delete anything.
- The tree is the instance tree the renderer actually built: your components are marked, library components can be hidden without flattening the hierarchy, each row shows its render count and self time, and rows flash when they render. Arrow keys navigate; filter by name.
- Props are editable in place. Editing a
$‑bound prop writes the atom; editing any other prop installs a DevTools override that lasts until you clear it, so you can tryvariant: "danger"on one live instance without touching the program. - Hooks and UI state — per‑instance
$state/$memocells, and the internal state a library component keeps (a Tabs' active pane, a DataGrid's sort) — are writable too. Remount drops an instance's memo, hooks and UI state. - DOM, Styles, Accessibility: the element's attributes, box model, grouped computed styles, the
--rui-*variables in effect, and what a screen reader would announce.
State
Every atom, as a tree you can edit in place: click a leaf, type, press Enter. The type
follows the edit (42 into a number field writes a number; into a string
field it stays the text "42"). Add keys and items, delete them, or edit a
whole value as validated JSON. Derived atoms, cached queries and runtime‑owned atoms
are labelled, and an activity bar shows how often each atom changes — sort by
Activity to find the one churning.
- Diff compares any two commits leaf by leaf, Changes is a log of every write with before and after, and Graph draws who reads what: atoms, derived atoms, components and effects.
- Time travel. Scrub the slider to see
$stateas it was at any retained commit. Preview in app hydrates the running app as you scrub; Restore into app makes an old snapshot the live state. Bookmark a state to come back to it, and export or import the whole store as JSON.
Data
- Queries: every cached
$query/Http({…})resource with its state, status and data. Refetch, invalidate (one, or every key matching a pattern), cancel — and Simulate loading or Simulate error, which install a request rule for that endpoint and refetch, so you can see the loading and error states you wrote without breaking the server. - Stores & forms: every
$storeand$formhandle, its state (editable), its methods (call them with JSON arguments), and for forms the fields, errors, touched and validity. - Storage:
localStorage,sessionStorageand script‑visible cookies, editable in place or as JSON. Values that look like credentials are flagged, and JWTs are decoded to show their subject, issuer and expiry.
Routes
The current path, the arm that matched, and its params; then every route the program
declares, found by a static walk of $router({…}) rather than from history,
so you can visit a page before you have built a link to it. Arms with
:params get a small form. Type a path to see which arm it would hit, check
the session's route coverage (which declared routes were never
visited), revisit any navigation from the history, or replay the whole journey.
Timeline
One axis for everything: interactions, commits, state changes, effect runs, requests, navigations, custom events, logs, errors and long tasks, each on its own track. Wheel to zoom, drag to pan, Shift + drag to select a range, and the list below follows. Select an event for its details and a link to the section that owns it.
Network
Every request the Aktion HTTP layer makes — $query,
$mutation, Http({…}), $http — with status,
size, duration and a waterfall. Filter by text, status class, or failures. The detail
pane shows headers, payload, the response as a tree or raw, and timing, and each
request can be copied as cURL or fetch. Export the
whole list as a HAR file.
- Rules intercept matching requests: mock (status and body), delay, fail, or offline, optionally only some of the time (a flaky 30%). Mock this on a real request pre‑fills the rule from what came back.
- Throttling presets — Fast 3G, Slow 3G, Flaky, Offline — apply after your own rules.
Console
The program's console output and the runtime's own warnings in one stream, filterable
by level and by origin, with consecutive duplicates collapsed. The REPL evaluates any
Aktion expression against the live scope — $count + 1,
$todos.filter(t => !t.done).length, even $count = 5 —
with completion for atoms (Tab) and history (↑/↓).
Effects
Every mounted effect with what it is for (on $count, every 3s, on mount), how often it ran, how long it takes, and whether it threw. Select one for its triggers, subscriptions, timers and recent lifecycle; run it on demand; switch to Timeline to see runs as spans per effect, or Log for every mount, run, cleanup and unmount.
Performance
The commit chart along the top shows every commit against a 16 ms frame budget; select one and the views below explain it.
- Flame: the commit as a flame chart. A user component's width is its body plus its children; memoised instances are drawn hatched at the width they had when they last rendered, so you can see what memoisation saved. Wheel to zoom, drag to pan, double‑click a span to focus it.
- Ranked sorts that commit's components by self time; Components aggregates across the whole session: renders, total and average time, how often each was skipped.
- Why groups a commit's renders by reason — state dependency changed, named args changed, full render, memoized — and names the atoms that triggered it.
- Insights turns the data into advice: slow components, components that never get skipped, wasted renders from forced commits, commit loops, DOM-heavy commits, a large DOM, slow interactions.
- Vitals: INP with its phases per interaction, LCP, CLS, frame rate over time, and long tasks with script attribution where the browser provides it.
Accessibility
Issues runs an audit over the rendered tree — names, labels, contrast, ARIA validity and wiring, keyboard reachability, target size, structure and media — and scores it out of 100 the way Lighthouse does: each failing rule costs by impact, so ten icon buttons missing a name cost less than ten different problems. Findings are grouped by rule, filterable by impact and category, and numbered; switch on Markers and the same numbers appear over the page. Select one for what is wrong, how to fix it in Aktion terms, the WCAG 2.2 success criteria it fails (linked to the W3C's Understanding documents), the element, and what a screen reader announces for it. Contrast failures come with the nearest passing colour in the same hue. Turn on Auto to re‑run after every commit.
- A11y tree: the page as assistive technology reads it — roles, names, states, and what is in the tab order — with unnamed controls called out.
- Structure: landmarks (outline them on the page), the heading outline with skipped levels flagged, and the keyboard tab order. Start focus walk moves real focus through the app one stop at a time and announces each stop, which is the quickest way to find a focus trap or an unreachable control. Show path on page draws the tab order as numbered arrows.
- Vision: simulate deuteranopia, protanopia, tritanopia and achromatopsia (Machado et al. colour matrices), blurred vision and low contrast on the app only; scale the theme's text up to 200% (WCAG 1.4.4); and check any colour pair in the contrast checker against AA and AAA.
Automated checks catch roughly a third of real barriers. The focus walk and the vision simulations are there for the rest.
Security
Aktion's trust model has two inputs with opposite privileges: the program text is trusted code, and everything it renders is untrusted data the library sanitises (see Security). The Security section follows that split instead of printing a generic checklist, and scores the page out of 100.
- Program: the global access policy in force, the host globals the program names, dynamic code (
eval, string timers), escape hatches (HTMLTag,Styles,Svg,Markdown), endpoints, windows it opens, events it emits, and storage it touches — each with a line you can jump to. - Rendered output: whether the sanitiser guarantees hold in the live DOM — no
javascript:,vbscript:or non-mediadata:URLs, inline handlers or stray scripts, sandboxed frames, safetarget="_blank", forms that post securely, precise password autocomplete. - Transport: every origin the app contacted, first- or third-party, whether it carried credentials; mixed content; secrets in query strings.
- Storage: tokens in
localStorage,sessionStorageand script-readable cookies, with JWTs decoded. - Headers: on request, one same‑origin
HEADfor the page, graded — CSP (against what an Aktion app actually needs), HSTS,nosniff, framing, Referrer‑Policy, Permissions‑Policy, COOP — plus the CSP violations the browser reported while the panel was open.
Every finding names its evidence and its fix, and exports as Markdown or JSON. Nothing here sends data anywhere: the header check is the only request the panel makes, and only when you press the button.
Testing
-
Record. Press Record and use the app. Clicks, typing
(coalesced into one step per field), selects, checkboxes, keys and navigation become
steps, each with the most robust query available — test id, then role and name,
then label, then text; a step that had to fall back to a CSS path is flagged as
brittle. A navigation caused by a click becomes an assertion that the click got
there, so a broken link fails the test instead of being papered over. Add assertions
by pointing at the page (is visible, has its text, holds its
value, is checked), reorder or delete steps, Replay them against
the live app step by step, and export either an
aktion-runtime/testtest (program inlined, changed state asserted) or a Playwright spec. - Scenarios save the current state, request rules and route under a name — empty cart, API down, admin on /settings — and re‑apply them in one click. They persist per app and export as JSON for teammates.
- Coverage measures the program itself — lines, functions and every branch arm — from the interpreter, lists what never ran, and downloads LCOV. (V8 coverage cannot do this: an
.aktionfile compiles to oneJSON.parse.) - Queries is a Testing Library playground: type a role and name, a label, text, a test id or a selector and see what it matches, highlighted on the page, with a warning when a
getBywould throw. Pick to suggest gives the most robust query for any element, as Aktion-test and Playwright code. - Chaos clicks random controls and fuzzes text fields with the inputs that break real apps — empty, 500 characters, emoji, RTL text, markup, injection strings — with a render between each action, and reports every error. Controls named like delete, remove or sign out are skipped, and links, popups and dialogs are neutralised so the run stays on the page. Every run is seeded: re‑run the seed, or open the run as a recording to trim it into a regression test.
- Emulate network conditions, right-to-left layout, larger text, the other theme, and badges showing every element's
data-testid.
Source
The program with syntax highlighting, diagnostics marked on their lines and explained at the end of them, find‑in‑source, and a sidebar with the outline (components, state, actions, effects, bindings), the problem list, and the program's version history. Edit opens an editor that validates the draft as you type (the runtime's own parser, so it never disagrees with the app); review the draft as a diff, then Apply (Ctrl/Cmd + S) to hot‑swap it with state preserved — with an undo. Every version the app ran is kept, with a diff against the current one and one‑click revert, so an edit that broke the app is one click from undone.
Theme
Every design token the library reads, grouped the way a designer thinks about them
(surfaces, text, brand, status, typography, spacing and shape, elevation and motion).
Click a swatch for a colour picker, or edit any value — ↑/↓
nudge numbers in their own unit, with Shift for ×10 — and every
component restyles instantly. The contrast panel checks the pairs the library actually
paints (body text, muted text, buttons, links, status text, control borders, focus ring)
and offers the nearest passing colour for any pair that fails. Copy your edits as a
$theme({…}) block, as JSON, or as CSS variables.
Settings
Appearance (theme, density, motion, position, page push, sidebar labels, the launcher), behaviour (console capture, re‑render highlighting, commit flashes, User Timing marks, automatic accessibility audits), and the runtime instrumentation the app pays for while DevTools is open:
| Switch | Records | Needed by |
|---|---|---|
| Capture props | Per-instance props and arguments in every commit | Inspect's props, Performance's reasons |
| Tag DOM nodes | data-aktion-instance on rendered elements | The picker and every highlight |
| State snapshots | A $state snapshot per commit | Time travel and diffs |
| Network events | Requests from the HTTP layer | Network, and every request rule |
| Count DOM nodes | A tree walk after each commit | DOM-size insights |
Turning one off makes the app faster, not just the panel — which is what you want when the thing you are measuring is render time itself.
Sessions and bug reports
Export the session (Settings, or the palette) writes everything the panel captured to one JSON file: the program and its versions, the state and its history, commits, effects, requests, navigations, logs, errors, and recorded test steps. Import it on another machine and it appears in the app switcher as a read‑only app: the timeline, commits, requests, logs and state history, exactly as they happened. Copy a bug report writes Markdown with the environment, steps to reproduce, errors, failed requests and vitals, ready to paste into a ticket.
API reference
Importing aktion-runtime/devtools registers the
<aktion-devtools> custom element as a side effect, so you can also drop
the tag straight into your HTML.
| Export | Purpose |
|---|---|
mountDevtools(options?) | Install the hook and mount a panel. Returns a controller. |
isDevtoolsInstalled() / isDevtoolsActive() | Whether a hook exists; whether a frontend is subscribed (the flag the runtime gates instrumentation on). |
installDevtoolsHook(version?) / getDevtoolsHook() | The hub without any UI, for an extension or a custom frontend. |
AktionDevtoolsElement, defineDevtoolsElement() | The panel element ("aktion-devtools") and its idempotent registration. |
HOOK_KEY, DEVTOOLS_PROTOCOL_VERSION, DEVTOOLS_UI_VERSION | "__AKTION_DEVTOOLS_HOOK__"; protocol 3; the panel's own version. |
exportSessionJson, importSessionJson, bugReportMarkdown | Session files and bug reports, for your own "report a problem" button. |
auditAccessibility, a11yScore, tabOrder, landmarks, headingOutline, accessibilityTree, announce | The accessibility engine, usable in your own tests. |
scanSecurity, checkHeaders, decodeJwt, classifySecret | The security scanner. |
InteractionRecorder, generateTest, generatePlaywrightTest, resolveQuery, replayStep | The recorder, both code generators, and replay. |
toCurl, toFetch, toHar | Request export. |
diffSnapshots, fuzzyScore, rankCommands | Leaf-level state diffs, and the palette's ranking. |
The event shapes are exported as types (DevtoolsEvent,
CommitRecord, ComponentRenderRecord, StateEvent,
EffectEvent, …) so a custom frontend can be fully typed. The runtime
also exports its own VERSION, which is what the panel reports.
mountDevtools(options?)
const devtools = mountDevtools({
container: document.querySelector("#debug-dock"), // default: document.body
appId: "aktion-app-2", // default: the first app that registered
tab: "network", // default: last used, else "overview"
dock: "right", // "float" | "right" | "bottom" | "left"
theme: "dark", // "system" | "dark" | "light"
open: false, // default true; false starts as the launcher
launcher: true // show the launcher pill while minimised
});
Nothing here is required. Section ids are overview, inspect,
state, data, routes, timeline,
network, console, effects, profiler,
a11y, security, test, source,
theme and settings. Floating, the panel opens at about
62% × 72% of the viewport near the bottom‑right corner and can be
resized down to 380×280. Position, size, dock, theme, density, section sizes and the
last section are remembered in localStorage.
The controller
| Member | Does |
|---|---|
element, hook | The live panel element; the shared hook. |
open(), close(), toggle() | Show, minimise to the launcher, or flip. A minimised panel stays subscribed and keeps recording. |
selectApp(id), selectTab(tab), dock(position) | Switch the inspected app, the section, or the dock position. |
importSession(json, fileName?) | Open an exported session for offline inspection; returns its id in the app switcher. |
flush() | Render synchronously — for tests that assert on the panel. |
destroy() | Remove the panel. It unsubscribes, so the runtime goes dormant again; the hook stays installed. |
close() and destroy() are not interchangeable:
close() is a UI state, destroy() turns the instrumentation off.
The hook and the app record
const hook = window.__AKTION_DEVTOOLS_HOOK__;
hook.aktion; // true — the marker that says you found the real hub
hook.protocolVersion; // 3
hook.libraryVersion; // the runtime's VERSION, e.g. "0.7.0"
hook.apps.size; // how many <aktion-app> elements registered
hook.active; // true only while a frontend is subscribed
hook.buffer.length; // recent events, capped at hook.bufferLimit (default 2000)
hook.setOptions({ captureProps: false }); // an instrumentation switch
const stop = hook.subscribe((event) => console.log(event.kind, event));
stop();
Events come in eight kinds: commit, state,
effect, network, route, emit,
log and error. Each entry in hook.apps is a
deliberately narrow handle: values are serialised before they cross, every write goes
through the same reactive pipeline an event handler uses, and there is no path to the
raw runtime internals. Beyond the seven core members every capability is optional, so a
frontend feature‑detects and degrades instead of throwing.
| Member | Gives you |
|---|---|
id, label, element | Identity: aktion-app-1; the data-devtools-label (else the element id); the host element. |
getState(), setState(path, value), getProgram(), forceRender() | The version‑1 contract, always present. |
| Program & diagnostics | |
setProgram, getSources, getDiagnostics, analyzeProgram(text?), reload | Hot swap, per-module sources, diagnostics, and a parse + validate + outline of any candidate text. |
| Inspector | |
getRenderRoot, getComponentTree, getInstance, instanceForNode, nodeForInstance | The render root, the instance tree, one instance in full, and the DOM mapping both ways. |
setInstanceHook, setInstanceUiState, setPropOverride, clearPropOverride, listPropOverrides, remountInstance | Writes into one instance. |
| State, effects, data, router, theme, network | |
getStateMeta, resetState, hydrateState, evaluateExpression | Atom metadata, reset, time travel, the REPL. |
getEffects, runEffect; getQueries, refetchQuery, cancelQuery, invalidateQueries; getStores, callStoreMethod | Effects, the query cache, stores and forms. |
getRoute, navigate; getTheme, setThemeTokens, clearThemeTokens, setThemeName; setNetworkRules, getNetworkRules; getStats | Router, theme, request rules, counters. |
| Protocol 3 | |
getReactivityGraph() | Who reads what: atoms and their derivations, component instances and their dependencies, effects and their triggers. |
getSecurityProfile() | What the program text can reach, from its AST: the access policy, host globals, dynamic code, escape hatches, endpoints, opened URLs, emitted events, storage. |
With several apps on a page, name them — otherwise the switcher reads
aktion-app-1, aktion-app-2:
<aktion-app data-devtools-label="Checkout form"></aktion-app>
<aktion-app data-devtools-label="Live order feed"></aktion-app>
How it works
- The runtime emits to a global hook at
window.__AKTION_DEVTOOLS_HOOK__, the way React emits to__REACT_DEVTOOLS_GLOBAL_HOOK__. Nothing installs it until a frontend does. - The hook buffers recent events, so a panel that opens after an app rendered can backfill; the runtime only records while something is subscribed, so work from before that is simply not there.
- The protocol is transport‑agnostic. No payload holds a DOM node, class instance or function, so the same events could cross a
postMessageboundary into an extension. - The panel is its own small UI stack: a keyed virtual DOM with widgets, virtualised lists for anything long, and canvas charts drawn at device resolution — no framework, no dependency on the app.
| Cap | Limit |
|---|---|
| Hook event buffer | 2000 events (hook.bufferLimit) |
| Commits / effect events / requests per app | 300 / 600 / 300 |
| Console lines per app | 500, consecutive duplicates collapsed |
| Navigations / emitted events / errors | 200 each |
| State snapshots for time travel | 60 commits |
| Changes remembered per atom | 60 |
| Elements examined by the accessibility audit | 4000 — it says when it stopped |
What it costs when it is not open
| State | isDevtoolsActive() | What the runtime pays |
|---|---|---|
You never import aktion-runtime/devtools | false | One property read per emit site. No records, no panel code in the bundle. |
Hook installed, nothing subscribed (including after destroy()) | false | The same single property read. |
| A panel is mounted — even minimised | true | Per-commit timing and one record per instance, a snapshot per flush, and whatever the instrumentation switches are set to. A minimised panel does not render or sample frames. |
Do not ship it enabled
Gate the mount, not just the panel. Put mountDevtools()
behind a dynamic import() that only a flag or a non‑production build
can reach. The panel can read and write any atom and swap the program, so an
always‑mounted DevTools in production is a debugging surface you did not intend to
publish — see Security.
Troubleshooting
No panel appears
- The import alone does nothing. Call
mountDevtools(), or place an<aktion-devtools>element. A bundler may also tree‑shake an import whose result you never used. - It is minimised. Look for the launcher pill (bottom right by default), press Shift + Alt + D, or call
controller.open(). If you hid the launcher, the shortcut still works. - A
containerancestor traps it. Atransform,filterorcontainon an ancestor makes it the containing block forposition: fixed. Mount intodocument.body, the default. - Settings → Reset preferences puts position, size and theme back to their defaults.
“No Aktion app on this page yet”
- The element never upgraded.
import "aktion-runtime"has to run for<aktion-app>to become a custom element. - The app is in another realm. A panel inside an iframe installs the hook on that iframe's
globalThis. Mount the panel in the app's document. - The app is inside a shadow root the panel's document query cannot reach. Call
el.connectDevtools()on it yourself.
A section is empty
- Only what happened while the panel was listening is recorded. Requests, commits and effects from before it opened were never instrumented; attaching forces one render so Performance always has a commit. For requests, Refetch cached queries in the Network empty state (or refetch from Data).
- Recording is paused — the title bar says Paused. Paused events are dropped, not queued.
- An instrumentation switch is off in Settings: no props without Capture props, no picker without Tag DOM nodes, no time travel without State snapshots, no requests or rules without Network events.
- Network only taps Aktion's HTTP layer. A bare
fetch()from host code does not appear — use the browser's own network panel for that (and the Security view flags programs that bypass$http).
An edit does not stick
- The value is derived. Derived atoms recompute from their inputs, and a query's data is replaced when it refetches. Edit the input, or mock the endpoint.
- The prop is
$‑bound. Then the atom is the source of truth and the edit writes it — other readers move too. - The program was re‑planned (
setResponse,appendChunk,reload()): prop overrides are dropped. - The theme's own
$theme({…})block re‑applies on every render, so it wins over a token edit for the tokens it sets.
Next
Performance
Turn a profiler reading into a fix — memoization, virtual lists, and workers.
Read the guide → GuideAccessibility
What the library does for you, and what the audit above cannot check.
Read the guide → GuideTesting
The aktion-runtime/test API the recorder writes tests for.