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

@econ-v1/chat-ui

v0.2.2

Published

Framework-free chat UI for Node client stages and shell surfaces: streamed replies, tool rows, approval cards and browser-tool calls over one injected transport.

Readme

@econ-v1/chat-ui

Framework-free chat UI for Node client stages and shell surfaces (Node Assistant sub-project B). It renders an agent conversation — streamed replies, tool rows, approval cards, browser-tool calls, report previews — inside the caller's shadow root, with no timers, no I/O of its own, and no shared state: everything goes through one injected ChatTransport, and every mount owns all of its state (no module-level singletons, caches or counters; two chats on one page share nothing). It only reads standard platform APIs (document, ShadowRoot, CSSStyleSheet, matchMedia, getComputedStyle, and Date.now as the controller's default clock) and never writes a global, document.head, or storage.

  • Checked JavaScript (// @ts-check + JSDoc), ESM, .d.ts shipped. No runtime dependencies.
  • Consumers bundle it with Vite into their stage or surface dist; the consuming app's build stamps integrity. It is not an installable app.
  • Runs unchanged under the PWA CSP (script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline').

Install

npm install @econ-v1/chat-ui

Use it in a stage or surface

import { mountChat, createStageTransport } from "@econ-v1/chat-ui";

const tabId = crypto.randomUUID(); // once per mount (C §5.3); pass the same value to both
const transport = createStageTransport(ctx, { agent: "node-assistant", tabId });
const chat = mountChat(ctx.host.root, {
  transport,
  sessionId,               // create sessions up front; omitted = the first prompt mints one
  mode: "normal",          // or "advanced"
  tabId,
  context: () => latestShellContext,              // sent as page_context on every prompt
  onEvent: (e) => {
    if (e.type === "client-call") runProbe(e.call);          // D executes browser tools, then:
    if (e.type === "client-call.takeover") claim(e.callId);  // D calls agent.tool_claim itself
    if (e.type === "navigate") ctx.host.navigate(e.path);
  },
});
// after a probe: await chat.completeClientCall(callId, { result }) or ({ error: { code, message } })
// on panel visibility: chat.reconcile()
// on unmount: chat.dispose()

Manifest requires for createStageTransport

{
  "capabilities": ["agent.prompt", "agent.abort", "agent.session.get", "agent.session.turn_state", "agent.card.resolve", "agent.tool_result"],
  "streams": ["app.agent_session", "transport.state-changed"]
}

agent.tool_claim is not needed by the library. A consumer that offers "Run here" calls it directly and adds it to its own requires.

API

mountChat(root, options) → ChatHandle

| Option | Type | | |---|---|---| | transport | ChatTransport | required; seven members | | mode | "normal" \| "advanced" | required | | sessionId | string | optional | | tabId | string | compared with each client call's ownerTab | | scheduler | { after(ms, fn): cancel } | optional quiet-stream check; surfaces must not pass one | | onEvent | (ChatEvent) => void | exceptions are swallowed | | labels | Partial<ChatLabels> | every user-visible string; defaults are English | | context | () => JsonObject \| undefined | read on every send |

The handle exposes dispose(), send(text), focus(), stop(), reconcile(), openSession(id | null), setMode(mode), completeClientCall(callId, outcome) and snapshot ({ sessionId, phase, gap, restored, pendingCards, pendingClientCalls }).

ChatEvent is one of: state, session, client-call, client-call.takeover, card (an unknown card kind), navigate, envelope (a stream kind the library does not render, e.g. turn.started), or error. Ignore types you do not know; new types are a minor release.

ChatTransport

The transport has exactly seven members: prompt, abort (never rejects), subscribe, history, turnState (never rejects), resolveCard and toolResult. The other four members reject with ChatTransportError { code }, where code ∈ responder_mismatch | not_found | not_pending | forbidden | unavailable | invalid | aborted | unknown.

Components and helpers

The view components return a handle { el, update(next), dispose() } and never append themselves; the caller places el. dispose() removes listeners and el.

| Export | Returns | |---|---| | createToolRow(row, { labels? }) | { el, update(row), dispose() } | | renderQuestionCard(card, { answered, onAnswer(choice) → boolean, idPrefix? }) | { el, update({ answered }), dispose() } | | renderApprovalCard(card, { local, onDecision(d) → boolean \| void, labels?, onNavigate?, idPrefix? }) | { el, update({ card, local }), dispose() }; onDecision returning false (refused) re-enables the buttons | | renderClientCallRow(model, { onTakeover(callId), tabId?, labels? }) | { el, update(model), dispose() } | | renderReportPreview(args, resolved, { labels?, onNavigate? }) | { el, update({ args, resolved }), dispose() } | | renderSessionList(rows, { onSelect(id), onNew(), activeId?, labels? }) | { el, update({ rows, activeId? }), dispose() } | | createComposer({ onSend(text) → boolean, onStop(), labels? }) | { el, update(composerView), focus(), dispose() } | | renderMarkdown(text, { onNavigate? }) | a DocumentFragment (no handle) | | setMarkdown(target, text, { onNavigate? }) | void; replaces target's children | | createScrollStick(logEl) | { noteScroll(), stick(), toBottom(), jumpState(busy) → "hidden" \| "arrow" \| "busy", pinned } (no element) | | createChatController(options) | a ChatController: { state, subscribe(listener) → unsubscribe, start(), send(text) → { accepted, settled }, stop(), reconcile(), openSession(id), setMode(mode), resolveCard(cardId, decision), completeClientCall(callId, outcome), answerQuestion(key, reply) → boolean, snapshot(), dispose() } | | adoptStyles(root) | a () => void that removes exactly what it added | | questionReply(question, choice) / questionDismiss(question) | the reply string to send |

idPrefix makes a card's element ids unique; mountChat passes a per-mount prefix plus the item key.

@econ-v1/chat-ui/wire

The DOM-free entry: the decoders (unwrapEnvelopeFrame, decodeHistoryPage, decodeTurnState, decodeSessionList, decodeApprovalCard, decodeCardResolved, extractQuestionCard, readToolEvent, toolDisplay, SayMessageExtractor, …), the verdicts, and the pure turn machine (reduce, initialState).

Styling

There is one stylesheet, wrapped in @layer chat-ui, adopted into the host shadow root and never document.head. Restyle through the --ncu-* variables, which map onto the skin's --ds-* tokens. CSS class names are not part of the API.

Without a skin, the fallback accent is #B8423E: Send (#FFFFFF on it) and Approve (it on the #FFFFFF card) are 5.40:1, WCAG AA. A skin that sets --ds-accent owns that contrast; set --ds-on-accent if white text does not suit it.

Size budgets

npm run size builds each entry with Vite (esbuild minify) and fails when the gzipped bundle is over budget:

| Entry | Budget (gzip) | |---|---| | mountChat (everything it pulls in) | 32 KiB | | @econ-v1/chat-ui/wire | 12 KiB |

Raising a budget is a reviewed change (scripts/size-report.mjs).

Provenance

Copied files carry a provenance header. Sources are pinned to econ-v1/node-app-stage-agent-builder@ae86a1d and econ-v1/node-app-agent@33cb98a.

| Library file | Source | Adaptation | |---|---|---| | src/view/markdown.js | Builder ui/src/lib/markdown.js | anchors only for http(s)://; /path → navigate button; //host is text; table alignment by class; iterative, linear-time inline scan; redaction chips | | src/wire/envelope.js | Builder builder-client.js:753-818 | none | | src/wire/transcript.js | Builder builder-client.js:1318-1446 | message_id kept; decodeHistoryPage + next_cursor | | src/wire/stream-text.js | Builder chat-real.js:575-689 | "assistant" wording | | src/model/verdicts.js | Builder chat-real.js:213-247, 394-531, 691-745 | client_tool, card, card_resolved roles; no ack-grace clock | | src/wire/tool-event.js | Builder turn-trace.js:91-152, 248-424 | assistant tool labels; client/claim/error fields | | src/view/tool-row.js | Builder turn-trace.js:470-529, 826-857 | rewritten row; no hide timer; no knowledge diff view | | src/view/scroll-stick.js | Builder chat.js:65-168 | per instance; no settle clock | | src/wire/cards.js | Builder canvas-intents.js:278-307 | approval + resolved decoders are new | | src/view/question-reply.js | Builder question-reply.js:1-36 | agentPickReply not taken | | src/view/question-card.js | Builder popups.js:715-746 | own listeners; submits via the controller | | src/wire/say-extractor.js | agent ui/src/lib/say-message-extractor.ts | TS → checked JS | | src/wire/json.js, src/transport/errors.js, src/transport/stage-transport.js (helpers) | Builder builder-client.js | see headers |

Run npm run drift (needs gh) to list watched upstream files that changed on main since the pins.

Releasing

Release is strictly tag-driven: the version is the vX.Y.Z tag and nothing else.

  1. npm run check && npm run lint && npm run guard && npm test && npm run check:consumer && npm run pack:dry
  2. npm run drift, and record any upstream changes to copied files in the release notes.
  3. One-time setup (mirrors the node repo's publish-sdks.yml — no GitHub Environment involved):
    • Configure the npm Trusted Publisher on npmjs.com: package @econ-v1/chat-ui → Settings → Trusted publishing → Add trusted publisher → GitHub Actions, with organization econ-v1, repository node-app-lib-chat-ui, workflow filename publish.yml, and environment name left blank. Equivalent CLI: npm trust github @econ-v1/chat-ui --file publish.yml --repo econ-v1/node-app-lib-chat-ui --allow-publish.
    • Trusted publishing only works on a package that already exists on npm, so the very first version is published once by a maintainer, not by the workflow: from a clean checkout of the v0.1.0 tag, run npm ci && npm run build && npm publish --access public. Every later release goes through publish.yml.
  4. Publish a GitHub release vX.Y.Z. publish.yml verifies the tag (the whole gate, including check:consumer), then stamps the tag's version into package.json and publishes with OIDC from a GitHub-hosted runner. A manual run must be started on the tag (Run workflow → Use workflow from → Tags → vX.Y.Z); on any other ref it fails. Runs for one ref never overlap, and a version already on npm is skipped.

The first release is 0.1.0; 1.0.0 follows once the Assistant ships on node main. Before 1.0, a breaking change bumps the minor version.

Adoption notes for Agent Builder

Owner: huynamboz. Nothing here is required of the Builder.

  • Direct swaps:
    • lib/markdown.js → renderMarkdown/setMarkdown.
    • The frame, envelope and transcript decoders → @econ-v1/chat-ui/wire.
    • The pure verdicts in chat-real.js → the wire verdicts.
    • question-reply.js → the same names.
  • Transport: a ChatTransport over builder-client.js is a thin object (prompt :2279, abortTurn :2340, turnState :2364, sessionMessages :1457, subscribeSession :3136).
  • What stays in the Builder: the BUILD/RUN split, crew-voice routing, wave frames, canvas ceremony, the studio binding and the thinking-word animation.
  • Upstream reports from this work: the protocol-relative link in SAFE_HREF, the shared scroll-pin module state, and the quadratic inline-mark scan.

License

MIT OR Apache-2.0.

Workspace view state and evidence

Architecture: ADR-001. handle.viewState captures { draft, scroll: { key, offset, scrollTop, pinned } }. Pass it to mountChat(..., { viewState }) when moving a conversation to another host, or call handle.restoreViewState(view) to restore the current session without new subscriptions. Hosts must keep this state associated with its originating session.

citation(id) returns consumer DOM for an opaque evidence token; the library adds data-ncu-citation. Consumers authorize and load evidence themselves. Uncited completed answers display a neutral verification footer when a citation factory is installed. Citation presence never establishes the truth of the assistant's inference.