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

@adambossy/agent-ui

v0.6.1

Published

React components and a live-component runtime for building agent chat UIs.

Readme

@adambossy/agent-ui

React components and a live-component runtime for building agent chat UIs: streaming messages, reasoning, tool-call rendering, subagent expansion, and a side-panel "live document" system driven by server-streamed ops.

Backend-agnostic. The library renders UI and manages client state; you supply the transport (e.g. the Vercel AI SDK's useChat) and wire its data parts to the stores. The playground is a complete reference integration.

Install

The package is currently private. Within this monorepo it resolves through the workspace; external consumers can use a git dependency or npm pack.

import {
  Message,
  Composer,
  LiveDocsPanel,
  useLiveDocStore,
  useLiveUIStore,
  useSubagentStore,
  registerLiveComponent,
  registerToolRenderer,
} from "@adambossy/agent-ui";
import type { UIMessage, LiveOpEvent } from "@adambossy/agent-ui";

Styles (Tailwind v4)

The library is styled with Tailwind v4 utilities plus a small set of design tokens and custom classes. Consumers process the shipped stylesheet through their own Tailwind build:

@import "tailwindcss";
@import "@adambossy/agent-ui/styles.css";

/* Let Tailwind generate the utility classes the components use: */
@source "../node_modules/@adambossy/agent-ui/dist";

Public API

  • Components — Message, Composer, Markdown, Reasoning
  • Tool renderers — registerToolRenderer, resolveToolRenderer, markAsSubagent, isSubagentTool, isNonUITool, DefaultTool
  • Run status — registerToolStatusText, statusTextFor, humanizeToolName, configureRunStatus, getRunStatusConfig, deriveRunStatus
  • Tool display modes — ToolDisplayProvider, useToolDisplay, ToolDisplayMode, registerToolSummaryText, summaryTextFor, ToolSummaryTextFn, deriveTurnSummary, TurnSummaryEntry
  • Subagent store — useSubagentStore, useSubagent
  • Live-component runtime — registerLiveComponent, LiveComponentHost, LiveDocsPanel, useLiveDocStore, useLiveUIStore, LiveOpClient, zodCodec, and the live-op protocol types
  • Types — UIMessage, UIMessagePart*, LiveOpEvent, LiveComponentManifest, LiveRendererProps, SubagentEvent, …

Anything not re-exported from the package root is internal and may change without notice.

Run status

Tools without a registered renderer are ephemeral by default: instead of a persistent JSON card, Message shows a shimmering in-run status line ("Running webSearch…") that disappears once the run moves past that tool call. The status line also covers approval-requested ("Waiting for approval: webSearch…"), so a tool call stalled on human approval doesn't vanish from the run. It does not cover output-error or output-denied — a failed or denied ephemeral tool call renders nothing at all once it leaves the active states above. If a tool's failures need to stay visible to the user, register DefaultTool for it (see below) instead of leaving it ephemeral.

The line is pinned to the top of the turn — it renders as the first child of the assistant message, above any widgets, streamed text, and the summary-mode receipt, in both display modes. That keeps current activity in a fixed spot instead of trailing behind whatever has already rendered, and lets it reappear mid-turn (e.g. the model starts reasoning again after already producing some text) without disturbing anything above it.

Reasoning parts are not rendered inline. While a reasoning part streams, the pinned line reads "Thinking…" — the same placeholder shown before any content exists — instead of an inline <Reasoning> block. In "summary" display mode, finished reasoning is folded into the TurnSummary receipt, which shares the pinned top slot with the status line (see below); in "ephemeral" mode it leaves zero residue once it completes, matching how ephemeral tool calls behave. The Reasoning component itself still ships and is exported for consumers who want to compose it manually.

Migrating from ≤0.2: previously every unregistered tool rendered a collapsible JSON card. As of the ephemeral-by-default change, an unregistered tool renders only a transient shimmer line and leaves no card once it completes. To restore the old per-tool behavior — including visibility into output-error/output-denied — call registerToolRenderer(name, DefaultTool) for that tool name.

Register DefaultTool for a tool name to opt back into the old collapsible JSON card:

import { registerToolRenderer, DefaultTool } from "@adambossy/agent-ui";

registerToolRenderer("debugTool", DefaultTool); // opt back into the JSON card

Customize the shimmer text with registerToolStatusText, which is called with the live tool part so it can refine its wording as streamed input fields arrive (part.input is typed unknown, so narrow or cast it):

import { registerToolStatusText, configureRunStatus } from "@adambossy/agent-ui";

registerToolStatusText("webSearch", (part) => {
  const query = (part.input as { query?: string } | undefined)?.query;
  return query ? `Searching for ${query}` : "Searching the web";
});

configureRunStatus({ expandableParallel: true }); // stack parallel tool calls into separate lines

configureRunStatus is a setup-time global, not a per-render prop: call it once during app initialization (alongside your registerToolRenderer / registerToolStatusText calls). It is not reactive: a mid-session call takes effect only at each status line's next re-render (frequent while streaming, but not forced by the call itself).

Composer takes the same status ("ready" | "submitted" | "streaming" | "error") your transport already tracks (e.g. the AI SDK's useChat), plus onStop: while status is "submitted" or "streaming" the send button swaps to a stop button that calls onStop.

<Composer status={status} onStop={stop} onSend={sendMessage} />

Display modes

The run-status line above is one of two per-conversation display modes for non-UI tool activity (unregistered tools, and subagent-marked tools — see isNonUITool below), selected by mounting a ToolDisplayProvider around a conversation's transcript:

  • "ephemeral" (default) — the behavior described above: a shimmering status line while a non-UI tool runs, and nothing once it settles. This is the behavior you get with no provider mounted at all.
  • "summary" — finished non-UI tool calls, plus finished reasoning, additionally collect into a past-tense TurnSummary receipt — a single collapsed line ("Thought, Searched the web, Read your calendar") that expands into per-entry detail: tool entries via the same DefaultTool / SubagentTool cards, so output-error and output-denied — invisible in ephemeral mode — are visible here; reasoning entries via a compact "Thought" block showing the reasoning text. The receipt shares the pinned top slot with the run-status line rather than trailing the turn: the line wins while there's active work, and the receipt takes the slot back once it yields (prose streaming, or the turn has ended).

deriveTurnSummary(message, isNonUI) returns TurnSummaryEntry[], a discriminated union of { kind: "tool"; part } and { kind: "reasoning"; part } entries in message order — a reasoning entry appears once its part is finished (state !== "streaming") and non-empty; an actively streaming reasoning part stays covered by the pinned run-status line instead (see above), so a part is never represented in both places.

Message reads the mode via useToolDisplay() internally, so nothing outside the provider is affected — mount one per conversation (or per tab) to let each run its own mode independently:

import { useState } from "react";
import {
  ToolDisplayProvider,
  Message,
  type ToolDisplayMode,
  type UIMessage,
} from "@adambossy/agent-ui";

function Conversation({ messages }: { messages: UIMessage[] }) {
  const [mode, setMode] = useState<ToolDisplayMode>("ephemeral");
  return (
    <ToolDisplayProvider mode={mode}>
      {messages.map((m) => (
        <Message key={m.id} message={m} />
      ))}
    </ToolDisplayProvider>
  );
}

Give a summarized tool call its own past-tense wording with registerToolSummaryText — the past-tense counterpart to registerToolStatusText above. It only runs once a call has reached a terminal state, so part.input/part.output are fully settled (still typed unknown, so narrow or cast it):

import { registerToolSummaryText } from "@adambossy/agent-ui";

registerToolSummaryText("webSearch", (part) => {
  const query = (part.input as { query?: string } | undefined)?.query;
  return query ? `Searched for ${query}` : "Searched the web";
});

Tools with no registered summary text fall back to `Ran ${humanizeToolName(toolName)}`.

Subagents: a subagent call (markAsSubagent) now counts as a non-UI tool (see isNonUITool below), which changes how it renders. Previously a subagent always drew a persistent top-level SubagentTool card. Now, like any other non-UI tool, it appears only in the run-status line while running; once it finishes, in "ephemeral" mode it leaves no residue, and in "summary" mode it becomes an entry in the TurnSummary receipt instead of a standing top-level card. Expanding that entry renders the same SubagentTool component — the nested transcript inside it (the subagent's own reasoning, text, and tool parts) is unaffected by any of this.

isNonUITool: the predicate that decides which tools skip inline rendering covers unregistered tools and subagent-marked tools (an earlier revision called this isEphemeralTool; that name never shipped in a release).

Build

npm run build      # tsup -> dist/{index.js, index.d.ts, styles.css}
npm run typecheck