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

@assistant-ui/react-pi

v0.0.25

Published

Pi coding-agent runtime adapter for assistant-ui

Readme

@assistant-ui/react-pi

Pi coding-agent runtime adapter for assistant-ui.

This package lets assistant-ui render and drive Pi-backed threads: streaming assistant/reasoning output, tool calls with live streaming results, mid-run steering and follow-up, per-thread model/thinking controls, the blocking extension UI (Pi's entire human-in-the-loop/approval surface), and a multi-thread thread list.

Package boundary

The package has two entry points:

  • @assistant-ui/react-pi — browser-safe. The runtime hook, the pure event reducer, the message projection, and the HTTP PiClient. This entry never imports @earendil-works/pi-*; it speaks an RPC-isomorphic, JSON-safe contract (PiClient) over an arbitrary transport.
  • @assistant-ui/react-pi/node — node-only. createPiNodeClient, which drives Pi's AgentSession SDK in-process behind a process-singleton PiThreadSupervisor. Only this entry pulls in Pi's Node packages.

HTTP is one implementation of PiClient, not the contract — the runtime hook never bakes in a transport. You can serve the node client over HTTP/SSE (below), hand it straight to usePiRuntime in a colocated Electron/main-process setup, or write any other transport that satisfies PiClient.

Install

npm install @assistant-ui/react-pi @assistant-ui/react
# the node entry drives the Pi SDK on the server:
npm install @earendil-works/pi-coding-agent

@earendil-works/pi-coding-agent is an optional peer dependency — browser-only consumers (those importing a remote PiClient) never pull it in. The ./node entry needs it.

Quickstart (HTTP/SSE transport)

The two halves of one PiClient contract:

browser                         server (Node)
─────────                       ─────────────
usePiRuntime                    route layer (GET/POST /api/pi/**)
  └ createPiHttpClient  ──HTTP──▶ createPiNodeClient(...)
       (fetch + SSE)               └ PiThreadSupervisor → Pi SDK

1. Server: the node client

createPiNodeClient returns a PiClient. It owns a process-singleton supervisor pinned to globalThis, so it survives Next.js dev HMR — create it in a module-level server file, never per request.

// lib/pi-server.ts  (server-only — imported only from route handlers)
import { createPiNodeClient } from "@assistant-ui/react-pi/node";

export const piClient = createPiNodeClient({
  workspacePath: process.env.PI_WORKSPACE_PATH ?? process.cwd(),
  // agentDir?: override Pi's config dir (default ~/.pi/agent)
  // model?:    a resolved Pi `Model` to seed new sessions (see Model controls)
});

2. Server: the route layer

Expose the PiClient methods over HTTP, and subscribe over SSE. The wire contract createPiHttpClient expects (relative to baseUrl, default /api/pi):

GET    /threads                 → PiThreadMetadata[]
POST   /threads                 → PiThreadSnapshot      (body: create input)
GET    /threads/:id             → PiThreadSnapshot      (read-only snapshot)
PATCH  /threads/:id             → 204                   (body: { title })
POST   /threads/:id/messages    → 204                   (body: { input })
POST   /threads/:id/cancel      → 204
GET    /models                  → PiModelInfo[]
POST   /threads/:id/model       → 204                   (body: { provider, modelId })
POST   /threads/:id/thinking    → 204                   (body: { level })
POST   /threads/:id/archive     → 204
POST   /threads/:id/unarchive   → 204
DELETE /threads/:id             → 204                   (also when the thread is already gone)
POST   /threads/:id/host-ui     → 204                   (body: { response })
GET    /threads/:id/events      → SSE of PiClientEvent  (?snapshot=false skips initial snapshot)

The events route must stream piClient.subscribe(threadId, …) and unsubscribe on request abort. A browser disconnect must not abort the run — see Reconnect. See examples/with-pi/app/api/pi/** for a complete Next.js App Router implementation.

3. Browser: the runtime

"use client";
import { AssistantRuntimeProvider } from "@assistant-ui/react";
import { createPiHttpClient, usePiRuntime } from "@assistant-ui/react-pi";
import { useMemo } from "react";

export function PiRuntimeProvider({ children }: { children: React.ReactNode }) {
  const client = useMemo(() => createPiHttpClient(), []); // baseUrl defaults to /api/pi
  const runtime = usePiRuntime({ client /*, workspacePath, includeArchived */ });
  return <AssistantRuntimeProvider runtime={runtime}>{children}</AssistantRuntimeProvider>;
}

usePiRuntime requires options.client — there is no implicit transport. Drop the provider above any assistant-ui thread UI (Thread, ThreadList, …).

Assistant Cloud

Pass cloud to back the thread list with Assistant Cloud instead of the Pi server's thread list. cloud is the only switch: unlike the runtimes whose list lives in the browser, NEXT_PUBLIC_ASSISTANT_BASE_URL alone keeps the Pi server's list, so setting it never moves an existing app's threads.

const runtime = usePiRuntime({ client, cloud });

Each cloud thread maps to a Pi thread through its external id: a new thread creates a Pi thread in workspacePath, and deleting the thread deletes its Pi thread first, where a Pi thread that is already gone does not block it. The list holds every thread of the cloud project, archived ones in their own section, so workspacePath only places new threads and includeArchived is not used. A cloud thread that no Pi runtime created has no Pi thread: it opens empty, and sending in it rejects. With cloud, threadId and initialThreadId take cloud thread ids, so a link that names a Pi thread id no longer opens it. Renaming and archiving change the cloud thread only; the Pi thread keeps its own title and archive state, which is what the list shows again if cloud is removed.

Environment / model resolution

A Pi session needs a model and credentials. Resolution mirrors Pi's own createAgentSession: an explicit PI_PROVIDER + PI_MODEL_ID wins, otherwise Pi falls back to its configured default (settings.json's defaultProvider / defaultModel). A user who is authenticated with pi and has a default model picked needs no env at all.

| Variable | Purpose | | --------------------- | -------------------------------------------------------------------------------------------- | | PI_PROVIDER | Override provider for new sessions (e.g. anthropic). | | PI_MODEL_ID | Override model id for new sessions. | | PI_WORKSPACE_PATH | Working directory the agent reads/writes/runs shell commands in. Point it at a scratch dir. | | PI_CODING_AGENT_DIR | Pi's agent config dir (~/.pi/agent by default). |

The ./node entry takes a resolved Pi Model via createPiNodeClient({ model }) to seed new sessions. Use Pi's own ModelRegistry / SettingsManager / AuthStorage on the server to resolve it. See examples/with-pi/lib/pi-server.ts.

Model & thinking controls

Per-thread, surfaced through usePiRuntimeExtras():

import { usePiRuntimeExtras } from "@assistant-ui/react-pi";

const { readiness, contextUsage, setModel, setThinkingLevel } = usePiRuntimeExtras();
// readiness: "ready" | "missing-model" | "missing-credentials" | "unavailable-model"
// await setModel({ provider, modelId });
// await setThinkingLevel("off" | "minimal" | "low" | "medium" | "high" | "xhigh");

getAvailableModels() on the client returns the catalog (auth-configured models first, falling back to the full built-in list). setThinkingLevel is clamped by Pi to the model's supported levels; the UI reflects the effective level from the next snapshot/event. contextUsage ({ tokens, contextWindow, percent }) answers "am I about to auto-compact?" and is available even for cold threads.

readiness gates sending — block the composer and show the readiness.message (a concrete next step, e.g. "Authenticate with pi, then restart the server.") when it is not "ready".

Composer run semantics

Pi's defining interaction is mid-run steering, and a plain prompt() while streaming throws. The runtime exposes Pi's native queue to assistant-ui (capabilities.queue), so the standard composer keeps accepting input during a run and derives the right behavior:

| State / action | Behavior | | ------------------------------ | ---------------------------------------------------- | | idle + submit | prompt() | | running + Enter | follow-up (streamingBehavior: "followUp") — queued | | running + Cmd/Ctrl+Shift+Enter | steer (streamingBehavior: "steer") |

To force steer from your own composer, send with steer: true or set message.runConfig.custom.streamingBehavior = "steer". While running, an omitted behavior defaults to "followUp".

Queued messages live in Pi's queue, not the transcript — Pi appends the user message only when the queue flushes. The runtime mirrors them as queue items (s.composer.queue, renderable with ComposerPrimitive.Queue) and on the thread metadata's queuedMessages. Pi supports clearing the whole queue (clearQueue() on the client / usePiRuntimeExtras().clearQueue(), which resolves with the cleared text so it can be restored into the composer), but no per-item remove/promote (see Known limitations).

Host-UI requests (the approval surface)

Pi has no built-in permission system — the only human-in-the-loop mechanism is extensions/tools calling ctx.ui.confirm / select / input / editor. This package implements and binds the ExtensionUIContext on the server and routes the four blocking dialogs to the UI, split by causality:

  • Tool-associated (a dialog raised while exactly one tool is executing) → rendered as the tool call's ToolCallMessagePart.approval and answered through the runtime's onRespondToToolApproval. confirm asks for a decision, select offers one option per choice (option ids are the choice indexes), and input / editor ask for a text answer, so the default tool fallback renders the matching controls. A custom tool UI answers with respondToApproval({ approved }), respondToApproval({ optionId, approved: true }) or respondToApproval({ text }), and approved: false dismisses a select, input or editor request; those requests are projected dismissible, so the default tool fallback offers a Dismiss control for them. The fallback does not show an input placeholder or an editor prefill; a custom tool UI reads them from usePiRuntimeExtras().allHostUiRequests.
  • Free-standing (extension commands, any request raised while multiple tools are in flight, and any request the tool call's approval cannot answer, such as a select without choices or a kind this client does not know) → a side channel:
import { usePiHostUiRequests } from "@assistant-ui/react-pi";

const { requests, respond } = usePiHostUiRequests();
// confirm:                 respond({ requestId, confirmed: boolean })
// select/input/editor:     respond({ requestId, value: string })
//                      or  respond({ requestId, dismissed: true })

Pi's UI requests carry no toolCallId, so causality is only inferred when a single tool is executing; otherwise requests stay on the side channel. Pending requests are tracked on the server record (not the SSE connection), so a reconnecting client still sees them. Unsupported ExtensionUIContext methods degrade rather than crash.

Reconnect semantics

  • The supervisor keeps the runtime alive across browser disconnects — only an explicit cancelRun or process exit stops a run. A dropped SSE never aborts.
  • HTTP reconnects receive an authoritative snapshot event, then live events. includeSnapshot: false skips only the initial snapshot. Reconnect snapshots recover missed messages and thread status without event replay.
  • Browser subscribers share the long-lived SSE connection. A subscriber joining late receives the current snapshot first; when the cached snapshot is behind, one short-lived snapshot request is shared by all subscribers waiting for it.
  • Cold/historical threads load via a read-only session-file snapshot — opening a thread to read it does not spin up a live AgentSession. A live runtime is created only when you send, cancel, change model/thinking, answer host UI, or explicitly subscribe to live events.

Selector hooks

The runtime keeps high-frequency transcript state separate from low-frequency metadata, so metadata controls don't rerender on every token:

  • usePiRuntimeExtras() — status, readiness, contextUsage, queue, compaction, retry, lastError, host-UI requests, and the cancel / refresh / clearQueue / setModel / setThinkingLevel / respondTo* / resumeToolCall actions.
  • usePiSession() — the current PiThreadMetadata (or null).
  • usePiThreadState(selector?) — the raw reducer state, optionally selected.
  • usePiHostUiRequests() — free-standing host-UI requests + a responder.

Known limitations (MVP)

  • No RPC-subprocess transport. The SDK-in-process node client assumes one long-lived Node process; it does not survive serverless/edge. The contract is RPC-isomorphic so a subprocess/remote transport can drop in later.
  • No durable event replay / backpressure / version negotiation. Recovery is snapshot-first only.
  • Idle threads are not auto-followed on view. Opening a thread is a cheap read-only snapshot; the runtime auto-subscribes to live events only when the loaded snapshot reports the thread as running (or when you send). A run that starts elsewhere after you opened an idle view shows up on the next refresh, not live. (controller.connect() exists for explicit always-live subscription.)
  • The node host's catalog is process-local and in-memory. It caches SessionManager.list() and tracks archive state for the running process; nothing is persisted across restarts (no last-selected workspace, drafts, labels, or search index). Workspace is just a workspacePath string on the client — the UI for choosing one (text field, directory picker, …) is the consuming app's concern, not this package's.
  • No fork/clone/navigate or per-item queue editing. The reducer/projection preserve the underlying data — tree linkage (parentSessionPath), queued messages (queuedMessages), and compaction state — so a consumer can build those surfaces, but the client exposes no methods to fork a session, navigate the tree, or reorder the queue. Queue items mirror Pi's server-side queue: enqueueing works (that's how mid-run follow-up/steer is sent) and clearing all works (clearQueue, mirroring Pi's only queue mutation), but per-item steer/remove affordances are no-ops — Pi has no such API.
  • Attachments are image-passthrough only. Non-image input parts aren't converted into Pi user content.
  • Model/thinking are the only runtime-config actions. There are no provider/auth/credential or default-model/settings methods on the client — manage those with Pi's own SDK on the server. Skills/extensions/slash-commands aren't surfaced.