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

streamkit-ui

v0.1.0

Published

Rendering and state primitives for streaming LLM UI — token streaming, incremental markdown, tool-call state machines, and multi-stream orchestration. Vendor-agnostic: works with Vercel AI SDK, OpenAI, and Anthropic streams.

Readme

streamkit-ui

Rendering and state primitives for streaming LLM UI.

npm install streamkit-ui

What this is

A React library that sits between a streaming LLM response and a production UI. It's not a chat component, a model client, or an agent framework — it's the layer that handles the rendering and state problems so your code can focus on product problems.

import { useChatStream, StreamingMarkdown, StreamingCodeBlock, StreamStatus } from "streamkit-ui";
import { fromAnthropic } from "streamkit-ui/adapters/anthropic";

function Chat() {
  const { messages, isStreaming, sendMessage, abort } = useChatStream({
    getAssistantStream: (history) => (signal) => fromAnthropic({ stream: myStream(history, signal) }),
  });

  return (
    <>
      <StreamStatus status={isStreaming ? "streaming" : "idle"} />
      {messages.map((m) =>
        m.role === "user" ? (
          <p key={m.id}>{m.text}</p>
        ) : (
          <StreamingMarkdown
            key={m.id}
            text={m.text}
            isStreaming={isStreaming && m.status === "streaming"}
            showCursor
          />
        )
      )}
      {isStreaming && <button onClick={abort}>Stop</button>}
    </>
  );
}

The problems it solves

1. Full-document re-parsing on every token

A naive streaming markdown implementation calls marked(text) and re-injects the full HTML on every chunk. Earlier paragraphs — which haven't changed — get diffed and potentially re-rendered on every token.

StreamingMarkdown re-lexes the text into block-level tokens on every update (cheap), but renders each token in its own memoized subcomponent keyed on raw content. React skips re-rendering a block whose text hasn't changed since the last update — only the currently-growing trailing block re-renders per chunk. Verified: the DOM node reference for a sealed block is the same object before and after a new block arrives.

2. XSS from unsanitized rendered markdown

LLM output that passes through tool results, retrieved documents, or prompt injection can carry <script> tags, onerror attributes, and javascript: links. Both StreamingMarkdown and StreamingCodeBlock sanitize through DOMPurify with an explicit allowlist immediately before dangerouslySetInnerHTML. Sanitizing at the rendering boundary — not at the data layer — means it can't be bypassed by a caller forgetting to do it upstream.

3. No good state model for tool calls

A model's streamed response is interleaved text and tool calls — not "text, then maybe a single tool call." useChatStream handles a full message_list reducer with interleaved text and tool-call chunks in the same stream. useToolCallState owns execution lifecycle separately from stream consumption: a tool call can outlive the stream that requested it, and needs idempotency guarantees that text deltas don't. Registering the same toolCallId twice is a safe no-op.

4. Per-stream render thrash under concurrent streams

React 18's automatic batching coalesces setState calls within the same synchronous callback — but not across separate macrotask callbacks. N independent setInterval timers firing a few milliseconds apart still cause N separate render passes. useStreamQueue runs all active streams under a shared 30fps flush tick so simultaneous updates produce one render pass regardless of how many streams are active.

5. The inline-factory footgun

The obvious way to pass a stream source to a hook is as a prop: useStream(() => adapter(response, signal)). The problem: that arrow function gets a new identity on every render, and a hook that re-runs on factory identity change triggers an infinite restart loop. useTokenStream separates the re-run trigger (streamKey) from the factory reference, so inline closures are safe and the re-run is controlled explicitly.

6. Vendor lock-in at the rendering layer

Building streaming UI directly against the Vercel AI SDK's TextStreamPart shape means touching component code when switching providers. Every hook and component in streamkit-ui only sees StreamChunk — a single type owned by this library. The three adapters do real translation work:

  • Vercel AI SDK v6: text-delta uses .text (not .textDelta — a field renamed mid-v5 that broke a lot of code). Tool calls arrive as a complete tool-call event with parsed input; the adapter emits a synthetic start+ready pair so the tool-call state machine sees a consistent sequence.
  • Anthropic: Tool arguments arrive as input_json_delta events keyed by content-block index (not by a stable id). The adapter tracks a Map<index, BlockTracker> and defers JSON.parse() until content_block_stop fires, because individual delta fragments can split a JSON token at arbitrary byte boundaries.
  • OpenAI: Same accumulate-then-parse pattern as Anthropic, via delta.tool_calls[].function.arguments fragments keyed by tool call index. Flushes all accumulated calls on finish_reason === "tool_calls".

Primitives

Hooks

| Hook | What it does | |------|--------------| | useTokenStream | Consumes a StreamSource. Backpressure-safe (~30fps flush), abort-correct, tool-call-tracking. | | useToolCallState | Registry of tool implementations. Manages execution lifecycle, idempotency, abort-on-unmount. | | useChatStream | Multi-turn message-list reducer. Composes the above two. | | useStreamQueue | N concurrent streams under a shared flush tick, with optional concurrency cap and admission control. | | createResumableStream | Wraps a factory with retry/resume: exponential backoff, abort-awareness, textSoFar accumulation. |

Components

| Component | What it does | |-----------|--------------| | StreamingMarkdown | Incremental markdown without per-chunk full re-render. Block-sealed memoization, DOMPurify sanitization. | | StreamingCodeBlock | Debounced syntax highlighting (highlight.js). Tolerates incomplete code. Copy button. | | StreamStatus | Lifecycle indicator (idle → streaming → done/error/aborted). Headless-capable via render prop. |

Adapters (subpath imports)

import { fromVercelAISDK } from "streamkit-ui/adapters/vercel-ai-sdk";
import { fromAnthropic }    from "streamkit-ui/adapters/anthropic";
import { fromOpenAI }       from "streamkit-ui/adapters/openai";

Design decisions documented in the codebase

Every non-obvious design decision is commented at the call site with the problem it solves and the alternative it rejects. Some of the ones worth reading:

useTokenStreamstreamKey vs factory identity (src/hooks/useTokenStream.ts): why depending on factory identity for re-runs causes infinite loops, and why streamKey is the right primitive.

StreamingMarkdown — block-sealed memoization (src/components/StreamingMarkdown.tsx): why re-lexing is cheap while re-rendering is expensive, and how the memo comparator's token.raw check does the actual work.

createResumableStream — deferred error chunk emission (src/hooks/createResumableStream.ts): why yielding an error chunk before deciding whether to retry was a bug — it would flip a consuming hook to "error" status even for transient errors that are about to be successfully retried.

Anthropic adapter — JSON accumulation (src/adapters/anthropic.ts): why partial_json fragments can't be parsed individually and why deferring to content_block_stop is the only safe approach.


Test suite

94 tests across 11 files. Each test exercises a specific claim:

  • StreamingMarkdown verifies DOM node object identity for sealed blocks (React actually skips the render, not just "produces equal output")
  • useTokenStream verifies that an inline factory with a new identity every render does not cause infinite restarts
  • createResumableStream verifies that successfully-retried errors never leak an error chunk to the consumer
  • useStreamQueue verifies that a stream's factory is never called when the concurrency cap is full (actual admission control, not rendering)
  • All three adapters verify against real types from installed SDK packages, not from memory or docs
npm test           # run all 94 tests
npm test:coverage  # with v8 coverage report

Running the example app

cd examples/next-app
cp .env.local.example .env.local
# Add your ANTHROPIC_API_KEY to .env.local
npm install && npm run dev

The example app wires useChatStream to a real Anthropic streaming endpoint via an ndjson protocol (no vendor SDK on the client side — just the custom backend adapter pattern from the docs).


Running Storybook

npm run storybook

Interactive stories for StreamStatus, StreamingMarkdown, StreamingCodeBlock, useTokenStream, and useChatStream — each with live-typing simulations, controls, and documentation.


Building

npm run build   # ESM + CJS + .d.ts to dist/
npm run typecheck

Dual ESM/CJS output with proper subpath exports for the adapter packages (streamkit-ui/adapters/anthropic, etc.) and "types" condition first in all exports map entries.


License

MIT