Devtools
Inspect live islands from tests, a panel, or a coding agent with ilha/debug and @ilha/devtools.
ilha/debug returns a JSON tree of every live island: component names, atom values, and child components. @ilha/devtools puts that tree in a panel and exposes it to browser agents.
Both are for development. Your production bundle never includes them unless you import them.
Inspect islands in code
Call inspect() to read the live roots. It works in tests, in the browser console, and in scripts an agent runs.
import { atom, mount } from "ilha";
import { inspect } from "ilha/debug";
const Counter = () => {
const count = atom(1);
const label = atom("hello");
return (
<button type="button" onclick={() => count.update((n: number) => n + 1)}>
{label()} {count()}
</button>
);
};
const host = document.querySelector("#app")!;
mount(host, Counter);
console.log(JSON.stringify(inspect(host), null, 2));
[
{
"id": "i1",
"component": "Counter",
"host": "div#app",
"atoms": [
{ "index": 0, "value": 1 },
{ "index": 1, "value": "hello" }
],
"children": []
}
]
Pass a host element to inspect one root or one child component. Omit it to get every live root.
| Field | Meaning |
|---|---|
id |
Stable while the component lives. Use it with setAtom() and element(). |
component |
The function name. Roots without a name show Root, others Anonymous. |
host |
A selector for the element the component paints into. Absent for SSR roots. |
atoms |
The component’s atom() slots by declaration order. |
children |
Child components, not the elements between them. |
Atom values are always JSON
inspect() never throws on a value it cannot serialize. It names the value instead.
| Value | Shown as |
|---|---|
| Function | "<function name>" |
| Effect, Stream, Atom | "<Effect>", "<Stream>", "<Atom>" |
AsyncResult success |
The unwrapped value |
AsyncResult failure |
"<AsyncResult failure: reason>" |
AsyncResult pending |
"<AsyncResult initial>" or "<AsyncResult waiting>" |
undefined, bigint |
"<undefined>", "<bigint 3>" |
| Date | An ISO string |
| Map, Set, class instance | "<Map(2)>", "<ClassName>" |
| Circular or deeper than 6 | "<circular>", "<Object>" |
| Atom nothing has read yet | "<unread>" |
inspect() reads atoms without starting work, so an Effect atom stays idle until a component reads it.
Change atoms and watch for updates
import { element, onChange, roots, setAtom } from "ilha/debug";
const stop = onChange(() => {
console.log("an island settled, mounted, or unmounted");
});
const [tree] = inspect(host);
setAtom(tree.id, 1, "changed"); // { ok: true }
element(tree.id); // the element to highlight or measure
roots(); // host elements of the live DOM roots
stop();
setAtom() accepts a JSON value. It returns { ok: false, reason } for an unknown id, a missing index, or a read-only atom, so an agent can correct the call.
onChange() runs synchronously after a paint, after a root settles, and when a root unmounts. Coalesce inside the listener if you re-read the whole tree. Apps that never import ilha/debug pay one set insert per root and nothing else.
Open the panel
Install @ilha/devtools.
npm install @ilha/devtoolspnpm add @ilha/devtoolsyarn add @ilha/devtoolsbun add @ilha/devtoolsnub add @ilha/devtoolsaube add @ilha/devtoolsStart the page script in your client entry. It answers the panel and agents for this page.
// entry.client.ts
import { installIlhaDevtools } from "@ilha/devtools/page";
if (import.meta.env.DEV) {
const devtools = installIlhaDevtools();
// devtools.openPanel();
}
Serve the panel with the Devframe Vite plugin.
// vite.config.ts
import { devframeVitePlugin } from "@devframes/vite/single";
import { ilhaDevtools } from "@ilha/devtools";
import { defineConfig } from "vite";
export default defineConfig({
plugins: [devframeVitePlugin(ilhaDevtools)],
});
Call devtools.openPanel() from the console or a button. The panel opens at /__ilha-devtools/ in a popup, which lets it find the page that opened it.
The panel shows the component tree. Hover a component to outline it on the page, select one to see its atoms, and edit a JSON value to write it back.
ilhaDevtools is a plain Devframe definition, so any Devframe adapter can mount it.
Let an agent drive the page
installIlhaDevtools() registers three tools on the browser’s WebMCP model context when the browser provides one.
| Tool | Use it to |
|---|---|
ilha_inspect |
Read every live island. Call it first to learn the ids. |
ilha_highlight |
Outline a component for the user. Pass null to clear it. |
ilha_set_atom |
Write a JSON value to one atom and let the island rerender. |
The same three functions carry Devframe agent metadata on the in-page channel, so a Devframe MCP bridge can list them. Pass { webmcp: false } to skip the WebMCP registration.
Without a browser agent, call the functions through a panel endpoint. The channel is devframes:plugin:ilha-devtools, and @ilha/devtools/protocol exports its types.
import { connectPanelChannel } from "devframe/in-page-channel";
import { ILHA_DEVTOOLS_CHANNEL } from "@ilha/devtools/protocol";
import type { IlhaDevtoolsProtocol } from "@ilha/devtools/protocol";
const panel = connectPanelChannel<IlhaDevtoolsProtocol>({
functions: {},
name: ILHA_DEVTOOLS_CHANNEL,
});
const islands = await panel.call("inspect");
await panel.call("setAtom", islands[0].id, 0, 2);
The tree shared state mirrors inspect() as islands change. Read it with parseTree(state.value()).
Test with the same tree
inspect() runs under happy-dom, so a test can assert on state and not only on markup.
import { expect, test } from "bun:test";
import { mount } from "ilha";
import { inspect } from "ilha/debug";
test("starts at one", () => {
const host = document.createElement("div");
mount(host, Counter);
expect(inspect(host)[0]?.atoms[0]?.value).toBe(1);
});