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

agentle-ui

v0.6.0

Published

A gentle UI for chaotic AI streams — headless hooks for AI-native presentation

Readme

Install

npm i agentle-ui
npx agentle-ui init
npx agentle-ui add markdown-stabilizer

Quick start (styled template)

import { MarkdownStabilizer } from "@/components/agentle/markdown-stabilizer";

export function Answer({ content }: { content: string }) {
  return <MarkdownStabilizer content={content} />;
}

The CLI copies MarkdownStabilizer plus agentle.css. Import the CSS once in your app entry or layout.

Three input shapes

| Concern | Component / hook | What you pass | |--------|------------------|---------------| | Answer markdown | MarkdownStabilizer / useStabilizedMarkdown | Growing string or StreamSource of text | | Thinking | ThoughtVisualizer / useThoughtStream | NDJSON thought lines or ThoughtStep[] | | Tools | ActionCard / useActionState | AgentAction[] (app state, not a stream) |

Raw delta.reasoning tokens are not thought steps. SSE frames are not markdown. Map your backend to strings or NDJSON, or use the optional adapters below.

Streaming (any backend)

Accumulate tokens into a growing string in your transport layer. Pass isComplete when your backend signals done — this is the production path.

import { useState, useEffect } from "react";
import { MarkdownStabilizer } from "@/components/agentle/markdown-stabilizer";

export function StreamedAnswer({ url }: { url: string }) {
  const [content, setContent] = useState("");
  const [done, setDone] = useState(false);

  useEffect(() => {
    const controller = new AbortController();
    setContent("");
    setDone(false);

    void (async () => {
      const response = await fetch(url, { signal: controller.signal });
      const reader = response.body?.getReader();
      if (!reader) return;

      const decoder = new TextDecoder();
      while (true) {
        const { value, done: eof } = await reader.read();
        if (eof) break;
        setContent((current) => current + decoder.decode(value, { stream: true }));
      }
      setDone(true);
    })();

    return () => controller.abort();
  }, [url]);

  return <MarkdownStabilizer content={content} isComplete={done} />;
}

Production guidance

  • Prefer isComplete when your transport has a done signal (SSE end, fetch complete, SDK done event).
  • Use settleMs auto-settle only for firehose text with no explicit end event (demos, replays).
  • Pass a StreamSource factory (() => stream) for live async iterables — creates a fresh stream per subscription (StrictMode-safe). Use createStreamSource() when storing a factory in React state (see Pitfalls).
  • Pass onError on stream hooks to surface transport failures; already-rendered blocks/steps are kept.

Completion model

| Input | Completion signal | isComplete / settleMs | |-------|-------------------|---------------------------| | Growing string | You control | isComplete prop (preferred) or settleMs auto-settle | | Stream / async iterable | EOF or error | Not applicable — completes when the stream ends | | ThoughtStep[] | Always complete | Not applicable |

Headless control:

import { useStabilizedMarkdown } from "agentle-ui";

const { renderedBlocks, pendingBlocks, isStreaming } = useStabilizedMarkdown(content, {
  isComplete: done,
});

OpenAI-compatible SSE (optional adapters)

Most real backends stream SSE JSON, not plain text. Adapters parse transport framing — they are not turnkey orchestration. You still own fetch, dual-channel routing, and state. Use parseSSE / openAIStreamToText to extract text tokens first:

import { openAIStreamToText } from "agentle-ui";
import { MarkdownStabilizer } from "@/components/agentle/markdown-stabilizer";

export function AnswerFromSSE({ url }: { url: string }) {
  return (
    <MarkdownStabilizer
      content={() =>
        (async function* () {
          const res = await fetch(url);
          if (!res.body) return;
          yield* openAIStreamToText(res.body);
        })()
      }
    />
  );
}

For free-text delta.reasoning, use textToThoughtStep (generic bridge) or openAIReasoningToThoughts (OpenAI SSE sugar) with ThoughtVisualizer.

Dual-channel bodies (reasoning + content in one response): use splitReadableStream(body, 2) in a single owner effect, or route both deltas into two state strings in one fetch loop. Tee branches are single-consumption — do not attach two remounting hooks to split branches under StrictMode.

Pitfalls

Never store a bare factory in useState

StreamFactory is a function. React treats useState(fn) as a lazy initializer and setState(fn) as an updater — not as storing the factory. You get one-shot consumption and confusing partial output.

// Wrong — React invokes the factory as an updater
const [source, setSource] = useState(() => myStreamFactory);
setSource(() => myStreamFactory);

// Right — branded object, safe in state/refs
import { createStreamSource } from "agentle-ui";
const [source, setSource] = useState(createStreamSource(myStreamFactory));

// Right — functional setState wraps the factory value
setSource(() => () => freshStream());

// Right — keep factory in a ref, pass inline to the component prop
const factoryRef = useRef(myStreamFactory);
<MarkdownStabilizer content={factoryRef.current} />

StrictMode and live HTTP

A factory is StrictMode-safe only when each call creates a new fetch/stream. () => fetch(url).body aborts on unsubscribe; a shared tee() branch stalls if two hooks consume it. For dual-channel UI under StrictMode: one owner effect → two growing strings → two components.

Prefer strings for production

For live HTTP, accumulate tokens into state strings + isComplete. Reserve factories for demos, replays, or SDK async iterables where you control fresh source creation.

Headless hooks

| Hook | Returns | Purpose | |------|---------|---------| | useStabilizedMarkdown | renderedBlocks, pendingBlocks, isStreaming, isComplete, error | Buffer incomplete markdown blocks to prevent layout shift | | useThoughtStream | steps, activeStep, isComplete, summary, reducedMotion, error | Show agent thinking steps instead of a spinner | | useActionState | actions, runningCount, toggleExpanded, isExpanded, formatDuration | Track tool-call expansion and live duration | | usePromptSurface | text, setText, attachments, filteredCommands, handleKeyDown, submit, … | Multi-line input with attachments and slash commands |

CLI

npx agentle-ui init                        # Create agentle-ui.json + components/agentle/
npx agentle-ui add markdown-stabilizer     # Copy template files
npx agentle-ui add <component> --overwrite # Replace existing copied files
npx agentle-ui list                        # List available components

Available components: markdown-stabilizer, thought-visualizer, action-card, prompt-surface.

Utilities

Also exported for custom integrations:

  • createStreamSource — wrap a stream factory in a branded object safe for useState / refs
  • parseSSE, openAIStreamToText — parse OpenAI-compatible SSE and extract delta.content or delta.reasoning
  • textToThoughtStep, openAIReasoningToThoughts — map free-text reasoning into a single collapsible ThoughtStep (NDJSON)
  • splitReadableStream — vendor-neutral ReadableStream fan-out via native tee()
  • collectStreamInput, collectStreamSource, getStreamInputKey, getStreamSourceKey
  • parseThoughtJsonLine, mergeThoughtSteps, buildThoughtSummary, getActiveThoughtStep, isThoughtStreamComplete

Stream hooks accept StreamSource: a string, stream, async iterable, factory () => stream, or createStreamSource(factory). Pass onError to handle transport failures.

Documentation

Full docs, live demos, and integration recipes: edvardhov.github.io/agentle-ui

Package on npm: npmjs.com/package/agentle-ui

License

MIT