npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@wildix/wilma-web

v0.1.2

Published

The Wilma assistant as a themed React widget on @wildix/uikit: compound components, default leaves and the DOM platform.

Readme

@wildix/wilma-web

The Wilma assistant as a themed React widget: the DOM distribution package, built on @wildix/uikit and the wilma-assistant event protocol.

Three packages, and which one you want depends on how much of a UI you need:

| Package | Holds | Platform | React | |---|---|---|---| | @wildix/wilma-core | events · reducer · thread items · markdown tokenizer · platform ports · traces and the test simulator | none | no | | @wildix/wilma-headless | provider · session engine · registries · selectors · item routing | none | yes | | @wildix/wilma-web | <Wilma.*> · default leaves · <WilmaAssistant> · webPlatform | DOM | yes |

Each re-exports the ones below it, so installing this one is enough for a web app. Fold a recorded log in a Lambda and you want wilma-core alone — it has no React peer at all, which is the whole point of the split.

import {WilmaAssistant} from '@wildix/wilma-web';

<WilmaAssistant client={client} />;

One prop. The consumer constructs and injects the WilmaAssistantClient — the SDK owns no transport, no tokens and no environment configuration, because both consumer apps already have a client factory and a second one here would be a worse copy of it.

Install

pnpm add @wildix/wilma-web

@wildix/wilma-core, @wildix/wilma-headless, @wildix/uikit, @wildix/wilma-assistant-client, react and react-dom are peer dependencies. Two of those matter more than the rest: a duplicate uikit means duplicated Tailwind output and two sets of CSS-variable themes, and a duplicate wilma-headless means two WilmaContext objects and a useWilma() that throws "must be rendered inside <Wilma.Root>" from inside one.

Add the stylesheet next to uikit's, so your Tailwind build scans the SDK's classes:

@import 'tailwindcss';
@import '@wildix/uikit/styles/globals.css';
@import '@wildix/wilma-web/styles/globals.css';

Where the design comes from

The visual layer is a port of the assistant in rnc/packages/shared-baseline/src/wilma, not a new design. That UI has been through real use, and its decisions are the reason the thread reads well:

  • an execution block for what the agent did and a response block for what it said, so a long tool sequence never buries the answer. The block opens while the turn runs, collapses itself when it finishes, and arrives already collapsed — silently — when a session is loaded from history;
  • tool activity drawn as a timeline: an icon rail with a connector line, and the call's input inline as label | value chips, so a row reads as what was searched rather than "Searched contacts" with a disclosure to open. The result collapses to 128 words, one click from the full output;
  • a right-aligned user bubble with 10px 10px 2px corners, capped at min(70%, 480px);
  • a 24px-radius composer whose border picks up the focus colour, with a two-row autosizing textarea and a round 36px send button;
  • an 800px column, 16px between messages, and the 32px action row reserved on every turn so revealing it on hover never shifts the transcript.

Two things are deliberately not ported. rnc's raw --gray117__LM_gray162__DM variables become uikit's semantic classes, so the SDK themes with the design system rather than against it — the same components render x-bees, x-hoppers, Collaboration and WMS with no per-brand code. And a call the user has to answer is lifted out of the collapsible block into the response area: rnc had no gated tools, and burying an approval in a block that collapses itself would make the session unresumable.

Three layers, all public

| Import | What it gives you | | --- | --- | | @wildix/wilma-core | Raw AssistantSessionEvents, the SSE decoder, the incremental JSON parser, the pure (state, event) => state reducer and the derived visual items. No React. | | @wildix/wilma-headless | The provider, the session engine, the registries and the selectors. All behaviour, no markup. | | @wildix/wilma-web | Compound components, the default leaves and <WilmaAssistant>. |

They are layers, not tiers: the widget is written against the same public hooks you would use, and nothing in it has privileged access.

import {replaySessionEvents, buildThreadItems} from '@wildix/wilma-core';

// Fold a recorded log with no React and no transport at all.
const state = replaySessionEvents(events);
const items = buildThreadItems(state);

Also exported: ./settings, ./connectors (loads the optional @nangohq/frontend peer lazily, so a host that renders no connectors never pays for it) and ./testing (the protocol's worked traces plus an in-memory assistant).

Customising it

Three mechanisms with three different jobs. Picking by job is what keeps any one of them from becoming a dumping ground.

1. Compound components own structure and order.

<Wilma.Root client={client}>
  <Wilma.Thread>
    <Wilma.Viewport>
      <Wilma.Messages />
    </Wilma.Viewport>
    <Wilma.Error />
    <Wilma.Composer />
  </Wilma.Thread>
</Wilma.Root>

Every part takes base-ui's render prop — the convention uikit uses, not Radix's asChild — so any of them can be re-based onto a different element:

<Wilma.ComposerSend render={<Button size="sm">Send</Button>} />

One placement is load-bearing rather than a matter of taste: <Wilma.QuestionDock> has to be somewhere. <Wilma.Composer> renders it, and a host that replaces the composer wholesale has to render it too — the transcript deliberately leaves the live question out (see The timeline below), so without the dock a suspended session has no way to be answered.

2. The components registry owns leaf rendering. Message text, the tool row, the reasoning block, the question form, the getting-started screen. Everything in it is a pure presentational leaf with no SDK state: props in, elements out. Anything that owns state or order is a compound component instead.

<Wilma.Root client={client} components={{MessageText: MyText, Composer: MyComposer}} />

3. Type-keyed registries own domain extensiontools, artifacts, mentions.

Two leaves worth knowing about. MessageText is the smallest and most-used entry, and a replacement has exactly one obligation — honour streaming, or streamed text will look stuck. ToolRow is the timeline's whole visual grammar, so replacing it re-skins every piece of agent activity at once.

Registration is also how features switch on. A capability you did not supply produces no affordance rather than a broken one: no artifact renderer means no artifact button, not a button that opens an empty panel.

The timeline

The transcript renders the assistant's parts in the order they happened, one line each. Three decisions carry most of the difference from a plain message list, and each one is worth knowing before you restyle it:

Activity is a line, not a card. A tool call is Title · subtitle args on one 20px row, with a chevron only when there is something under it. The subtitle is the point: Read says nothing, Read · session-turn.tsx says whether the agent looked in the right place, and costs no vertical space.

Consecutive activity calls fold into one row. Four reads and two searches become Worked · Read file ×4 · Search ×2, expandable. Grouping keys off surface, never off tool names — a call you want kept out of the fold is surface: 'content'. A call that failed never folds either: activity was a claim about intent, made before anyone knew the outcome, and filing a failure under Worked would say the opposite of what happened.

Reasoning is inline and muted. 13px in the muted colour, in the flow. Thinking is worth reading and is never the answer, so it sits one step quieter than the reply rather than behind a disclosure nobody opens. showReasoning={false} drops it.

A live question is in the composer, not the transcript. The turn is stopped until it is answered, so the form has to be next to the cursor and reachable from the keyboard (/ to move, ⌘↵ to advance, Esc to dismiss). The transcript leaves a hole for it and shows the question with its answer once it resolves.

A sub-agent is a place. A delegation is the one thing in the timeline that is not a line, because it is not something this agent did — it is something a different agent did. So it draws as opencode's task-tool-card: a tinted pill with the agent, its brief and an arrow that appears on hover. Clicking it swaps the transcript for that task's own session — back control, breadcrumb, the brief as the opening user message, the sub-agent's own timeline, its answer, and a composer that reads Subagent sessions cannot be prompted. Back to main session.

It does not unfold in place: a sub-agent has its own reasoning, its own calls and its own answer, so inlining it puts a second transcript inside the first. Escape steps out one level, and a sub-agent that delegated again opens exactly the same way.

The card's icon takes --wilma-task-agent-color (opencode's --task-agent-color), with nothing invented behind it — our protocol carries no agent colour, so a host that has one sets the variable and gets per-agent tinting, and a host that does not gets the muted default rather than a palette the SDK made up.

Delegation reaches the stream twice — the delegate call, and the taskStarted for the execution that call kicked off — so buildThreadItems folds the call into the task and draws one row for the pair. mergeTaskInvocations: false keeps both, for an inspector.

Every part carries a stable data-slot, so spacing and typography can be tuned from a stylesheet without wrapping anything — which is what the Timeline → Playground story in Storybook does, and what it exports.

Writing a tool

A generative-UI tool

invocationDelta.soFar is a prefix of raw JSON, so the SDK parses it incrementally and your component renders the input as the model writes it — the difference between "the assistant froze for six seconds" and "the assistant is writing".

const showEmail = defineTool<EmailInput>({
  name: 'show_email',
  fulfilment: 'synthetic', // nothing executes; the runtime answers with `ack`
  gate: 'none', // …so no turn suspends and no round trip happens
  surface: 'content', // the call *is* user-facing output
  stream: true, // emit deltas: the input carries content
  ack: '<instructions>Draft shown with its own send control.</instructions>',
  schema: {/* JSON Schema */},
  render: ({input, isStreaming}) => <EmailCard email={input} writing={isStreaming} />,
});

<WilmaAssistant client={client} tools={[showEmail]} />;

input is the incremental parse while it streams and the parsed document afterwards — the same field either way, and always a prefix of the eventual value, never a different one.

A caller-executed tool

const getAccountSummary = defineTool({
  name: 'get_account_summary',
  fulfilment: 'client', // the caller executes it
  gate: 'result', // …so the turn suspends until it answers
  surface: 'activity',
  schema: {/* … */},
  execute: async ({accountId}) => myApp.accounts.summary(accountId),
});

The SDK suspends, runs your handler, and resumes with the result. A client tool with no registered handler is left visible and waiting rather than answered with something invented.

What the protocol does and does not support

The SDK implements the contract in wilma-services/docs/assistant/events.md, and where the protocol has no operation, the SDK has no affordance. Nothing is faked.

Supported: start / continue / steer / stop / resume · streamed text and reasoning · the full invocation lifecycle including generative UI, caller-executed tools, questions and approvals · delegated sub-agents · sandbox and compaction · artifacts (through a registry slot) · attachments · session history and rehydration · settings and connectors.

Not supported, because the assistant channel has no operation for it:

  • message feedback — there is no feedback operation, so thumbs up/down has nowhere to post; a local-only rating would be a lie told to the user. rnc's message row had both; here it carries copy only;
  • edit and regenerate — messages have no identity and there is no branching, so rnc's regenerate button has no operation behind it;
  • backend-driven non-tool UI events — there is no custom/data escape hatch, so generative UI goes through an invocation or an artifact.

capabilities.feedback and capabilities.branching are therefore hard false.

Testing

  • pnpm test — reducer and parser unit tests, including a replay-equivalence test: folding a recorded log from index 0 must produce the same state as streaming it.
  • pnpm e2e — Playwright against a Vite app with the network mocked by @msw/playwright.
  • Storybook lives in packages/uikit-storybook under wilma/*, with MSW serving a real streamed SSE body — which is where the incremental streaming states are exercised, since Playwright's routing buffers response bodies. One story per feature (Timeline → Tool rows / Reasoning / Subagents / Attachments, Composer → Questions), plus Timeline → Playground: a sidebar that assembles a turn out of real protocol events and tunes the layout live.

@wildix/wilma-core/testing ships the same fixtures, so a consumer can test its own tools against the protocol's traces rather than hand-rolling mocks that drift from it.