Skip to content
Ilha
Esc
↑↓navigate↵open⌘Jpreview
On this page

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/devtools
pnpm add @ilha/devtools
yarn add @ilha/devtools
bun add @ilha/devtools
nub add @ilha/devtools
aube add @ilha/devtools

Start 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);
});

Was this page helpful?