Sign In

@neeter/react

Package Overview
Dependencies
Maintainers
1
Versions
34
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@neeter/react - npm Package Compare versions

Comparing version
0.17.4
to
0.18.0
+1
-1
dist/AgentProvider.d.ts

@@ -0,4 +1,4 @@

import { type ChatStore, type ChatStoreShape } from "@neeter/core";
import type { CustomEvent } from "@neeter/types";
import { type ReactNode } from "react";
import { type ChatStore, type ChatStoreShape } from "./store.js";
import { type UseAgentReturn } from "./use-agent.js";

@@ -5,0 +5,0 @@ interface AgentContextValue extends UseAgentReturn {

import { jsx as _jsx } from "react/jsx-runtime";
import { createChatStore } from "@neeter/core";
import { createContext, useContext, useMemo, useRef } from "react";
import { useStore } from "zustand";
import { createChatStore } from "./store.js";
import { useAgent } from "./use-agent.js";

@@ -6,0 +6,0 @@ const AgentContext = createContext(null);

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useEffect, useRef } from "react";
import { cn } from "./cn.js";
export function ApprovalButtons({ onApprove, onDeny, className, }) {

@@ -5,0 +5,0 @@ const allowRef = useRef(null);

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useCallback, useRef, useState } from "react";
import { cn } from "./cn.js";
import { SendIcon, StopIcon } from "./icons.js";

@@ -5,0 +5,0 @@ export function ChatInput({ onSend, onStop, isStreaming, placeholder = "Type a message...", disabled, className, }) {

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useState } from "react";
import { cn } from "./cn.js";
import { ChevronIcon } from "./icons.js";

@@ -5,0 +5,0 @@ import { StatusDot } from "./StatusDot.js";

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "./cn.js";
import { cn } from "@neeter/core";
export function ChevronIcon({ open, className }) {

@@ -4,0 +4,0 @@ return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round", className: cn("h-3 w-3 transition-transform", open && "rotate-90", className), "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }));

@@ -12,2 +12,3 @@ import "./widgets/AskUserQuestionWidget.js";

import "./widgets/WriteWidget.js";
export { AgentClient, type AgentClientConfig, type ChatStore, type ChatStoreShape, cn, createChatStore, findMatchingApproval, getWidget, isApprovalClaimedByToolCall, type ResumeOptions, type RewindOptions, registerWidget, replayEvents, stripMcpPrefix, } from "@neeter/core";
export type { ChatMessage, CustomEvent, ModelUsage, PermissionRequest, PermissionResponse, SessionHistoryEntry, SessionInitEvent, SSEEvent, StopReason, TokenUsage, ToolApprovalRequest, ToolApprovalResponse, ToolCallInfo, ToolCallPhase, TurnCompleteData, UserQuestion, UserQuestionOption, UserQuestionRequest, UserQuestionResponse, } from "@neeter/types";

@@ -17,9 +18,6 @@ export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";

export { CollapsibleCard } from "./CollapsibleCard.js";
export { cn } from "./cn.js";
export { MessageList } from "./MessageList.js";
export { PendingPermissions } from "./PendingPermissions.js";
export { RollbackButton } from "./RollbackButton.js";
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
export { StatusDot } from "./StatusDot.js";
export { type ChatStore, type ChatStoreShape, createChatStore, replayEvents } from "./store.js";
export { TextMessage } from "./TextMessage.js";

@@ -26,0 +24,0 @@ export { ThinkingBlock } from "./ThinkingBlock.js";

@@ -13,12 +13,11 @@ // Built-in widgets (side-effect: auto-registers)

import "./widgets/WriteWidget.js";
// Re-export everything from @neeter/core for backward compatibility
export { AgentClient, cn, createChatStore, findMatchingApproval, getWidget, isApprovalClaimedByToolCall, registerWidget, replayEvents, stripMcpPrefix, } from "@neeter/core";
export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
export { ChatInput } from "./ChatInput.js";
export { CollapsibleCard } from "./CollapsibleCard.js";
export { cn } from "./cn.js";
export { MessageList } from "./MessageList.js";
export { PendingPermissions } from "./PendingPermissions.js";
export { RollbackButton } from "./RollbackButton.js";
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
export { StatusDot } from "./StatusDot.js";
export { createChatStore, replayEvents } from "./store.js";
export { TextMessage } from "./TextMessage.js";

@@ -25,0 +24,0 @@ export { ThinkingBlock } from "./ThinkingBlock.js";

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useEffect, useRef } from "react";
import { useChatStore } from "./AgentProvider.js";
import { cn } from "./cn.js";
import { PendingPermissions } from "./PendingPermissions.js";

@@ -6,0 +6,0 @@ import { RollbackButton } from "./RollbackButton.js";

import { jsx as _jsx } from "react/jsx-runtime";
import { cn, isApprovalClaimedByToolCall } from "@neeter/core";
import { useAgentContext, useChatStore } from "./AgentProvider.js";
import { isApprovalClaimedByToolCall } from "./approval-matching.js";
import { cn } from "./cn.js";
import { ToolApprovalCard } from "./ToolApprovalCard.js";

@@ -6,0 +5,0 @@ import { UserQuestionCard } from "./UserQuestionCard.js";

import { jsx as _jsx } from "react/jsx-runtime";
import { cn } from "./cn.js";
import { cn } from "@neeter/core";
const phaseClasses = {

@@ -4,0 +4,0 @@ pending: "bg-muted-foreground/40",

import { jsx as _jsx } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "./cn.js";
import { markdownComponents } from "./markdown-overrides.js";

@@ -6,0 +6,0 @@ export function TextMessage({ role, content, className, }) {

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useCallback, useState } from "react";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { cn } from "./cn.js";
import { ChevronIcon } from "./icons.js";

@@ -7,0 +7,0 @@ import { markdownComponents } from "./markdown-overrides.js";

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "./cn.js";
import { cn } from "@neeter/core";
export function ThinkingIndicator({ className }) {
return (_jsx("div", { className: cn("flex justify-start", className), children: _jsxs("div", { className: "flex items-center gap-1 rounded-lg bg-muted px-3 py-2", children: [_jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:0ms]" }), _jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:150ms]" }), _jsx("span", { className: "h-1.5 w-1.5 rounded-full bg-muted-foreground animate-bounce [animation-delay:300ms]" })] }) }));
}
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn, getWidget, stripMcpPrefix } from "@neeter/core";
import { ApprovalButtons } from "./ApprovalButtons.js";
import { cn } from "./cn.js";
import { getWidget, stripMcpPrefix } from "./registry.js";
import { PulsingDot } from "./StatusDot.js";

@@ -6,0 +5,0 @@ export function ToolApprovalCard({ request, onApprove, onDeny, className, }) {

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn, findMatchingApproval, getWidget, stripMcpPrefix } from "@neeter/core";
import { useAgentContext, useChatStore } from "./AgentProvider.js";
import { ApprovalButtons } from "./ApprovalButtons.js";
import { findMatchingApproval } from "./approval-matching.js";
import { CollapsibleCard } from "./CollapsibleCard.js";
import { cn } from "./cn.js";
import { getWidget, stripMcpPrefix } from "./registry.js";
import { PulsingDot, StatusDot } from "./StatusDot.js";

@@ -9,0 +7,0 @@ export function ToolCallCard({ toolCall, className, }) {

@@ -1,15 +0,5 @@

import type { ToolCallPhase } from "@neeter/types";
import type { WidgetProps as CoreWidgetProps, WidgetRegistration as CoreWidgetRegistration } from "@neeter/core";
import type { ComponentType } from "react";
export interface WidgetProps<TResult = unknown> {
phase: ToolCallPhase;
toolUseId: string;
input: Record<string, unknown>;
partialInput?: string;
result?: TResult;
error?: string;
}
export interface WidgetRegistration<TResult = unknown> {
toolName: string;
label: string;
richLabel?: (result: TResult, input: Record<string, unknown>) => string | null;
export type WidgetProps<TResult = unknown> = CoreWidgetProps<TResult>;
export interface WidgetRegistration<TResult = unknown> extends Omit<CoreWidgetRegistration<TResult>, "inputRenderer" | "component"> {
inputRenderer?: ComponentType<{

@@ -16,0 +6,0 @@ input: Record<string, unknown>;

@@ -1,7 +0,7 @@

import type { CustomEvent, PermissionResponse, RewindFilesResult, SessionHistoryEntry } from "@neeter/types";
import { type ChatStore } from "./store.js";
import { type ChatStore } from "@neeter/core";
import type { PermissionResponse, RewindFilesResult, SessionHistoryEntry } from "@neeter/types";
export interface UseAgentConfig {
endpoint?: string;
resumeSessionId?: string;
onCustomEvent?: (event: CustomEvent) => void;
onCustomEvent?: (event: import("@neeter/types").CustomEvent) => void;
}

@@ -27,3 +27,3 @@ export interface UseAgentReturn {

/**
* Low-level hook that manages the EventSource lifecycle and feeds SSE events
* Low-level hook that manages the AgentClient lifecycle and feeds SSE events
* into the Zustand store. Most apps should use `AgentProvider` instead —

@@ -30,0 +30,0 @@ * this hook is for custom provider implementations.

@@ -0,5 +1,5 @@

import { AgentClient } from "@neeter/core";
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { replayEvents } from "./store.js";
/**
* Low-level hook that manages the EventSource lifecycle and feeds SSE events
* Low-level hook that manages the AgentClient lifecycle and feeds SSE events
* into the Zustand store. Most apps should use `AgentProvider` instead —

@@ -12,260 +12,61 @@ * this hook is for custom provider implementations.

const onCustomEvent = config?.onCustomEvent;
const eventSourceRef = useRef(null);
const abortedRef = useRef(false);
const clientRef = useRef(null);
const [sessionHistory, setSessionHistory] = useState([]);
const sessionId = useSyncExternalStore(store.subscribe, () => store.getState().sessionId);
const sdkSessionId = useSyncExternalStore(store.subscribe, () => store.getState().sdkSessionId);
const [sessionHistory, setSessionHistory] = useState([]);
// Effect 1: Create client, connect session, destroy on cleanup
// onCustomEvent is NOT a dep — changing it shouldn't create a new session
useEffect(() => {
let cancelled = false;
async function init() {
let res;
if (resumeSessionId) {
try {
const eventsRes = await fetch(`${endpoint}/sessions/replay/${resumeSessionId}`);
if (eventsRes.ok && !cancelled) {
const events = await eventsRes.json();
replayEvents(store, events);
}
}
catch {
/* replay is best-effort */
}
res = await fetch(`${endpoint}/sessions/resume`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ sdkSessionId: resumeSessionId }),
});
const client = new AgentClient(store, { endpoint });
client.onHistoryChange = setSessionHistory;
clientRef.current = client;
client.connect(resumeSessionId).catch(() => {
// Error already surfaced via store.addSystemMessage in AgentClient.connect
});
return () => {
client.destroy();
if (clientRef.current === client) {
clientRef.current = null;
}
else {
res = await fetch(`${endpoint}/sessions`, { method: "POST" });
}
const data = await res.json();
if (!cancelled)
store.getState().setSessionId(data.sessionId);
}
init();
return () => {
cancelled = true;
};
}, [endpoint, resumeSessionId, store]);
// Effect 2: Attach EventSource with explicit cleanup (Strict Mode safe)
// endpoint and store are intentional deps — matches the original hook behavior
// biome-ignore lint/correctness/useExhaustiveDependencies: re-attach ES when endpoint/store change
useEffect(() => {
if (!sessionId)
return;
const es = new EventSource(`${endpoint}/sessions/${sessionId}/events`);
eventSourceRef.current = es;
abortedRef.current = false;
es.addEventListener("session_init", (e) => {
const { sdkSessionId: id, mcpServers, fileCheckpointing } = JSON.parse(e.data);
store.getState().setSdkSessionId(id);
if (mcpServers)
store.getState().setMcpServers(mcpServers);
if (fileCheckpointing)
store.getState().setFileCheckpointing(true);
});
es.addEventListener("message_start", () => {
store.getState().setThinking(true);
});
es.addEventListener("thinking_delta", (e) => {
const { text } = JSON.parse(e.data);
store.getState().appendStreamingThinking(text);
});
es.addEventListener("text_delta", (e) => {
store.getState().setThinking(false);
store.getState().flushStreamingThinking();
const { text } = JSON.parse(e.data);
store.getState().appendStreamingText(text);
});
es.addEventListener("tool_start", (e) => {
store.getState().setThinking(false);
store.getState().flushStreamingThinking();
store.getState().flushStreamingText();
const { id, name } = JSON.parse(e.data);
store.getState().startToolCall(id, name);
});
es.addEventListener("tool_input_delta", (e) => {
const { id, partialJson } = JSON.parse(e.data);
store.getState().appendToolInput(id, partialJson);
});
es.addEventListener("tool_call", (e) => {
store.getState().flushStreamingText();
const { id, name, input } = JSON.parse(e.data);
store.getState().finalizeToolCall(id, name, input);
});
es.addEventListener("tool_result", (e) => {
const { toolUseId, result, isError } = JSON.parse(e.data);
if (isError) {
store.getState().errorToolCall(toolUseId, result);
}
else {
store.getState().completeToolCall(toolUseId, result);
}
if (store.getState().isStreaming) {
store.getState().setThinking(true);
}
});
es.addEventListener("permission_request", (e) => {
const request = JSON.parse(e.data);
store.getState().addPermissionRequest(request);
});
es.addEventListener("session_error", (e) => {
const { subtype, stopReason } = JSON.parse(e.data);
store.getState().flushStreamingThinking();
store.getState().flushStreamingText();
store.getState().setThinking(false);
store.getState().setStreaming(false);
store.getState().setStopReason(stopReason ?? null);
if (abortedRef.current) {
store.getState().cancelInflightToolCalls();
store.getState().addSystemMessage("Interrupted");
abortedRef.current = false;
}
else {
store.getState().addSystemMessage(`Session ended: ${subtype}`);
}
});
es.addEventListener("turn_complete", (e) => {
const data = JSON.parse(e.data);
store.getState().flushStreamingThinking();
store.getState().flushStreamingText();
store.getState().setThinking(false);
store.getState().setStreaming(false);
store.getState().setStopReason(data.stopReason ?? null);
store.getState().addCost({
cost: data.cost ?? 0,
numTurns: data.numTurns ?? 0,
stopReason: data.stopReason ?? null,
usage: data.usage ?? null,
modelUsage: data.modelUsage ?? null,
});
if (abortedRef.current) {
store.getState().cancelInflightToolCalls();
store.getState().addSystemMessage("Interrupted");
abortedRef.current = false;
}
});
es.addEventListener("error", () => {
if (abortedRef.current) {
store.getState().flushStreamingThinking();
store.getState().flushStreamingText();
store.getState().cancelInflightToolCalls();
store.getState().setThinking(false);
store.getState().setStreaming(false);
store.getState().addSystemMessage("Interrupted");
abortedRef.current = false;
es.close();
}
else if (es.readyState === EventSource.CLOSED) {
store.getState().flushStreamingThinking();
store.getState().flushStreamingText();
store.getState().setThinking(false);
store.getState().setStreaming(false);
}
});
es.addEventListener("checkpoint", (e) => {
const { userMessageUuid } = JSON.parse(e.data);
store.getState().addCheckpoint(userMessageUuid);
});
if (onCustomEvent) {
es.addEventListener("custom", (e) => {
onCustomEvent(JSON.parse(e.data));
});
}
const client = clientRef.current;
if (!client)
return;
client.onCustomEvent = onCustomEvent;
client.attachEventSource();
return () => {
es.close();
eventSourceRef.current = null;
client.closeEventSource();
};
}, [sessionId, endpoint, store, onCustomEvent]);
const sendMessage = useCallback(async (text) => {
if (!sessionId)
return;
store.getState().addUserMessage(text);
store.getState().setStreaming(true);
store.getState().setThinking(true);
await fetch(`${endpoint}/sessions/${sessionId}/messages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
}, [sessionId, endpoint, store]);
await clientRef.current?.sendMessage(text);
}, []);
const stopSession = useCallback(async () => {
if (!sessionId)
return;
abortedRef.current = true;
store.getState().setThinking(false);
store.getState().setStreaming(false);
await fetch(`${endpoint}/sessions/${sessionId}/abort`, { method: "POST" });
}, [sessionId, endpoint, store]);
await clientRef.current?.stopSession();
}, []);
const respondToPermission = useCallback(async (response) => {
if (!sessionId)
return;
store.getState().removePermissionRequest(response.requestId);
await fetch(`${endpoint}/sessions/${sessionId}/permissions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(response),
});
}, [sessionId, endpoint, store]);
await clientRef.current?.respondToPermission(response);
}, []);
const resumeSession = useCallback(async (options) => {
const targetSdkSessionId = options?.sdkSessionId ?? store.getState().sdkSessionId;
if (!targetSdkSessionId)
return;
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
store.getState().reset();
try {
const eventsRes = await fetch(`${endpoint}/sessions/replay/${targetSdkSessionId}`);
if (eventsRes.ok) {
const events = await eventsRes.json();
replayEvents(store, events, {
stopAtCheckpoint: options?.resumeSessionAt,
});
}
}
catch {
/* replay is best-effort */
}
const res = await fetch(`${endpoint}/sessions/resume`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
sdkSessionId: targetSdkSessionId,
forkSession: options?.fork,
resumeSessionAt: options?.resumeSessionAt,
}),
});
const data = await res.json();
store.getState().setSessionId(data.sessionId);
}, [endpoint, store]);
await clientRef.current?.resumeSession(options);
}, []);
const refreshHistory = useCallback(async () => {
try {
const res = await fetch(`${endpoint}/sessions/history`);
if (res.ok)
setSessionHistory(await res.json());
}
catch {
/* ignore */
}
}, [endpoint]);
await clientRef.current?.refreshHistory();
}, []);
const rewindSession = useCallback(async (checkpointId, options) => {
if (!sessionId)
const client = clientRef.current;
if (!client)
return { canRewind: false, error: "No active session" };
const res = await fetch(`${endpoint}/sessions/${sessionId}/rewind`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ userMessageId: checkpointId, dryRun: options?.dryRun }),
});
return res.json();
}, [sessionId, endpoint]);
return client.rewindSession(checkpointId, options);
}, []);
const newSession = useCallback(async () => {
if (eventSourceRef.current) {
eventSourceRef.current.close();
eventSourceRef.current = null;
}
store.getState().reset();
const res = await fetch(`${endpoint}/sessions`, { method: "POST" });
const data = await res.json();
store.getState().setSessionId(data.sessionId);
}, [endpoint, store]);
await clientRef.current?.newSession();
}, []);
return {

@@ -272,0 +73,0 @@ sessionId,

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from "@neeter/core";
import { useEffect, useRef, useState } from "react";
import { cn } from "./cn.js";
const OTHER_LABEL = "__other__";

@@ -5,0 +5,0 @@ export function UserQuestionCard({ request, onSubmit, className, }) {

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function parseAnswers(result) {

@@ -4,0 +4,0 @@ if (typeof result !== "string")

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function BashInputRenderer({ input }) {

@@ -4,0 +4,0 @@ const command = typeof input.command === "string" ? input.command : null;

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function basename(filePath) {

@@ -4,0 +4,0 @@ return filePath.split("/").pop() ?? filePath;

import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function GlobInputRenderer({ input }) {

@@ -4,0 +4,0 @@ const pattern = typeof input.pattern === "string" ? input.pattern : null;

import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function GrepInputRenderer({ input }) {

@@ -4,0 +4,0 @@ const pattern = typeof input.pattern === "string" ? input.pattern : null;

import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function basename(filePath) {

@@ -4,0 +4,0 @@ return filePath.split("/").pop() ?? filePath;

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
/** Show path relative to likely project root, falling back to last 3 segments. */

@@ -4,0 +4,0 @@ function shortPath(filePath) {

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function parseTodos(input) {

@@ -4,0 +4,0 @@ if (!Array.isArray(input.todos))

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "@neeter/core";
import Markdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { markdownComponents } from "../markdown-overrides.js";
import { registerWidget } from "../registry.js";
function domain(url) {

@@ -7,0 +7,0 @@ try {

import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { registerWidget } from "@neeter/core";
import { useState } from "react";
import { registerWidget } from "../registry.js";
function extractJsonArray(str, start) {

@@ -5,0 +5,0 @@ if (str[start] !== "[")

import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { registerWidget } from "../registry.js";
import { registerWidget } from "@neeter/core";
function basename(filePath) {

@@ -4,0 +4,0 @@ return filePath.split("/").pop() ?? filePath;

{
"name": "@neeter/react",
"version": "0.17.4",
"version": "0.18.0",
"description": "React components and hooks for building chat UIs on top of the Claude Agent SDK",

@@ -27,5 +27,4 @@ "license": "MIT",

"dependencies": {
"clsx": "^2.1.1",
"tailwind-merge": "^3.5.0",
"@neeter/types": "0.17.4"
"@neeter/core": "0.18.0",
"@neeter/types": "0.18.0"
},

@@ -32,0 +31,0 @@ "peerDependencies": {

# @neeter/react
React components and hooks for building chat UIs on top of the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk). Connects to an `@neeter/server` backend over SSE with a Zustand store, drop-in components, and a widget system for tool call rendering.
React components and hooks for building chat UIs on top of the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk). Builds on `@neeter/core` for state management and agent lifecycle — with drop-in components and a widget system for tool call rendering.

@@ -10,3 +10,3 @@ Part of the [neeter](https://github.com/quantumleeps/neeter) toolkit.

```bash
pnpm add @neeter/react
pnpm add @neeter/core @neeter/react
```

@@ -13,0 +13,0 @@

import type { ChatMessage, PermissionRequest, ToolApprovalRequest, ToolCallInfo } from "@neeter/types";
/**
* Find the pending approval that matches a specific tool call.
* Matches by toolUseId when available, falls back to toolName.
* Only considers non-terminal tool calls (not complete/error).
*/
export declare function findMatchingApproval(pendingPermissions: PermissionRequest[], toolCall: ToolCallInfo): ToolApprovalRequest | undefined;
/**
* Check if an approval request is "claimed" by any non-terminal tool call
* in the message list. Claimed approvals are rendered inline by ToolCallCard
* and should be skipped by PendingPermissions to avoid duplicate UI.
*/
export declare function isApprovalClaimedByToolCall(request: ToolApprovalRequest, messages: ChatMessage[]): boolean;
/**
* Find the pending approval that matches a specific tool call.
* Matches by toolUseId when available, falls back to toolName.
* Only considers non-terminal tool calls (not complete/error).
*/
export function findMatchingApproval(pendingPermissions, toolCall) {
if (toolCall.status === "complete" || toolCall.status === "error")
return undefined;
return pendingPermissions.find((p) => p.kind === "tool_approval" &&
(p.toolUseId ? p.toolUseId === toolCall.id : p.toolName === toolCall.name));
}
/**
* Check if an approval request is "claimed" by any non-terminal tool call
* in the message list. Claimed approvals are rendered inline by ToolCallCard
* and should be skipped by PendingPermissions to avoid duplicate UI.
*/
export function isApprovalClaimedByToolCall(request, messages) {
for (const msg of messages) {
if (msg.toolCalls) {
for (const tc of msg.toolCalls) {
if (tc.status === "complete" || tc.status === "error")
continue;
if (request.toolUseId ? tc.id === request.toolUseId : tc.name === request.toolName) {
return true;
}
}
}
}
return false;
}
import { type ClassValue } from "clsx";
export declare function cn(...inputs: ClassValue[]): string;
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs) {
return twMerge(clsx(inputs));
}
import type { WidgetRegistration } from "./types.js";
/**
* Register a component for a tool name. Must run before React renders any
* tool calls — use a side-effect import at your app's entry point.
* Later registrations overwrite earlier ones for the same `toolName`.
*/
export declare function registerWidget<TResult>(reg: WidgetRegistration<TResult>): void;
export declare function getWidget(toolName: string): WidgetRegistration | undefined;
export declare function stripMcpPrefix(name: string): string;
const widgets = new Map();
/**
* Register a component for a tool name. Must run before React renders any
* tool calls — use a side-effect import at your app's entry point.
* Later registrations overwrite earlier ones for the same `toolName`.
*/
export function registerWidget(reg) {
widgets.set(reg.toolName, reg);
}
export function getWidget(toolName) {
return widgets.get(toolName);
}
export function stripMcpPrefix(name) {
return name.replace(/^mcp__.+?__/, "");
}
import type { ChatMessage, McpServerStatus, ModelUsage, PermissionRequest, SSEEvent, StopReason, TurnCompleteData } from "@neeter/types";
import { type StoreApi } from "zustand/vanilla";
interface ChatStoreState {
sessionId: string | null;
sdkSessionId: string | null;
messages: ChatMessage[];
isStreaming: boolean;
isThinking: boolean;
streamingText: string;
streamingThinking: string;
pendingPermissions: PermissionRequest[];
mcpServers: McpServerStatus[];
checkpoints: string[];
fileCheckpointing: boolean;
totalCost: number;
totalTurns: number;
totalInputTokens: number;
totalOutputTokens: number;
modelUsage: Record<string, ModelUsage> | null;
lastStopReason: StopReason;
}
interface ChatStoreActions {
setSessionId: (id: string) => void;
setSdkSessionId: (id: string) => void;
addUserMessage: (text: string) => void;
appendStreamingText: (text: string) => void;
appendStreamingThinking: (text: string) => void;
flushStreamingText: () => void;
flushStreamingThinking: () => void;
addSystemMessage: (text: string) => void;
startToolCall: (toolUseId: string, name: string) => void;
appendToolInput: (toolUseId: string, partialJson: string) => void;
finalizeToolCall: (toolUseId: string, name: string, input: Record<string, unknown>) => void;
completeToolCall: (toolUseId: string, result: string) => void;
errorToolCall: (toolUseId: string, error: string) => void;
setStreaming: (v: boolean) => void;
setThinking: (v: boolean) => void;
addPermissionRequest: (request: PermissionRequest) => void;
removePermissionRequest: (requestId: string) => void;
setMcpServers: (servers: McpServerStatus[]) => void;
addCheckpoint: (uuid: string) => void;
setFileCheckpointing: (v: boolean) => void;
addCost: (data: TurnCompleteData) => void;
setStopReason: (reason: StopReason) => void;
cancelInflightToolCalls: () => void;
reset: () => void;
}
export type ChatStoreShape = ChatStoreState & ChatStoreActions;
export type ChatStore = StoreApi<ChatStoreShape>;
/**
* Creates a vanilla Zustand store for chat state. `AgentProvider` creates one
* internally — use this directly only for custom provider implementations.
*/
export declare function createChatStore(): ChatStore;
/**
* Reconstructs chat store state from persisted SSE events. Uses the same
* store actions as the live SSE stream, so rendering is identical.
* Call before connecting the EventSource (e.g. during session resume).
*/
export declare function replayEvents(store: ChatStore, events: SSEEvent[], options?: {
stopAtCheckpoint?: string;
}): void;
export {};
import { immer } from "zustand/middleware/immer";
import { createStore } from "zustand/vanilla";
let nextId = 0;
function findToolCall(messages, toolUseId) {
for (let i = messages.length - 1; i >= 0; i--) {
const tc = messages[i].toolCalls;
if (tc?.length) {
const match = tc.find((t) => t.id === toolUseId);
if (match)
return match;
}
}
return undefined;
}
/**
* Creates a vanilla Zustand store for chat state. `AgentProvider` creates one
* internally — use this directly only for custom provider implementations.
*/
export function createChatStore() {
return createStore()(immer((set) => ({
sessionId: null,
sdkSessionId: null,
messages: [],
isStreaming: false,
isThinking: false,
streamingText: "",
streamingThinking: "",
pendingPermissions: [],
mcpServers: [],
checkpoints: [],
fileCheckpointing: false,
totalCost: 0,
totalTurns: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
modelUsage: null,
lastStopReason: null,
setSessionId: (id) => set((s) => {
s.sessionId = id;
}),
setSdkSessionId: (id) => set((s) => {
s.sdkSessionId = id;
}),
addUserMessage: (text) => set((s) => {
s.messages.push({
id: `msg-${++nextId}`,
role: "user",
content: text,
});
}),
addSystemMessage: (text) => set((s) => {
s.messages.push({
id: `msg-${++nextId}`,
role: "system",
content: text,
});
}),
appendStreamingText: (text) => set((s) => {
s.streamingText += text;
}),
appendStreamingThinking: (text) => set((s) => {
s.streamingThinking += text;
}),
flushStreamingThinking: () => set((s) => {
if (s.streamingThinking) {
const last = s.messages[s.messages.length - 1];
if (last?.role === "assistant" && !last.toolCalls?.length) {
last.thinking = (last.thinking ?? "") + s.streamingThinking;
}
else {
s.messages.push({
id: `msg-${++nextId}`,
role: "assistant",
content: "",
thinking: s.streamingThinking,
});
}
s.streamingThinking = "";
}
}),
flushStreamingText: () => set((s) => {
if (s.streamingText) {
const last = s.messages[s.messages.length - 1];
if (last?.role === "assistant" && !last.toolCalls?.length) {
last.content += s.streamingText;
}
else {
s.messages.push({
id: `msg-${++nextId}`,
role: "assistant",
content: s.streamingText,
});
}
s.streamingText = "";
}
}),
startToolCall: (toolUseId, name) => set((s) => {
s.messages.push({
id: `msg-${++nextId}`,
role: "assistant",
content: "",
toolCalls: [{ id: toolUseId, name, input: {}, status: "pending" }],
});
}),
appendToolInput: (toolUseId, partialJson) => set((s) => {
const tc = findToolCall(s.messages, toolUseId);
if (tc) {
tc.partialInput = (tc.partialInput ?? "") + partialJson;
tc.status = "streaming_input";
}
}),
finalizeToolCall: (toolUseId, name, input) => set((s) => {
const tc = findToolCall(s.messages, toolUseId);
if (tc) {
tc.name = name;
tc.input = input;
tc.status = "running";
}
}),
completeToolCall: (toolUseId, result) => set((s) => {
const tc = findToolCall(s.messages, toolUseId);
if (tc && tc.status !== "error") {
tc.result = result;
tc.status = "complete";
}
}),
errorToolCall: (toolUseId, error) => set((s) => {
const tc = findToolCall(s.messages, toolUseId);
if (tc) {
tc.error = error;
tc.status = "error";
}
}),
setStreaming: (v) => set((s) => {
s.isStreaming = v;
}),
setThinking: (v) => set((s) => {
s.isThinking = v;
}),
addPermissionRequest: (request) => set((s) => {
if (!s.pendingPermissions.some((p) => p.requestId === request.requestId)) {
s.pendingPermissions.push(request);
}
}),
removePermissionRequest: (requestId) => set((s) => {
s.pendingPermissions = s.pendingPermissions.filter((p) => p.requestId !== requestId);
}),
setMcpServers: (servers) => set((s) => {
s.mcpServers = servers;
}),
addCheckpoint: (uuid) => set((s) => {
s.checkpoints.push(uuid);
}),
setFileCheckpointing: (v) => set((s) => {
s.fileCheckpointing = v;
}),
addCost: (data) => set((s) => {
s.totalCost += data.cost;
s.totalTurns += data.numTurns;
if (data.usage) {
s.totalInputTokens += data.usage.inputTokens;
s.totalOutputTokens += data.usage.outputTokens;
}
if (data.modelUsage) {
if (!s.modelUsage) {
s.modelUsage = data.modelUsage;
}
else {
for (const [model, usage] of Object.entries(data.modelUsage)) {
const existing = s.modelUsage[model];
if (existing) {
existing.inputTokens += usage.inputTokens;
existing.outputTokens += usage.outputTokens;
existing.cacheCreationInputTokens += usage.cacheCreationInputTokens;
existing.cacheReadInputTokens += usage.cacheReadInputTokens;
existing.webSearchRequests += usage.webSearchRequests;
existing.costUSD += usage.costUSD;
existing.contextWindow = usage.contextWindow;
}
else {
s.modelUsage[model] = { ...usage };
}
}
}
}
}),
setStopReason: (reason) => set((s) => {
s.lastStopReason = reason;
}),
cancelInflightToolCalls: () => set((s) => {
for (const msg of s.messages) {
if (msg.toolCalls) {
for (const tc of msg.toolCalls) {
if (tc.status === "pending" ||
tc.status === "streaming_input" ||
tc.status === "running") {
tc.status = "error";
tc.error = "Interrupted";
}
}
}
}
}),
reset: () => set((s) => {
s.sessionId = null;
s.sdkSessionId = null;
s.messages = [];
s.isStreaming = false;
s.isThinking = false;
s.streamingText = "";
s.streamingThinking = "";
s.pendingPermissions = [];
s.mcpServers = [];
s.checkpoints = [];
s.fileCheckpointing = false;
s.totalCost = 0;
s.totalTurns = 0;
s.totalInputTokens = 0;
s.totalOutputTokens = 0;
s.modelUsage = null;
s.lastStopReason = null;
}),
})));
}
/**
* Reconstructs chat store state from persisted SSE events. Uses the same
* store actions as the live SSE stream, so rendering is identical.
* Call before connecting the EventSource (e.g. during session resume).
*/
export function replayEvents(store, events, options) {
const s = store.getState();
const stopAt = options?.stopAtCheckpoint;
// "Before" semantics: truncate events before the target checkpoint's
// user message. This matches rewindFiles() which restores files to
// their state before that message's turn.
let replayable = events;
if (stopAt) {
const cpIdx = events.findIndex((e) => e.event === "checkpoint" && JSON.parse(e.data).userMessageUuid === stopAt);
if (cpIdx >= 0) {
let cutIdx = cpIdx;
for (let i = cpIdx - 1; i >= 0; i--) {
if (events[i].event === "user_message") {
cutIdx = i;
break;
}
}
replayable = events.slice(0, cutIdx);
}
}
for (const evt of replayable) {
const data = JSON.parse(evt.data);
switch (evt.event) {
case "user_message":
s.addUserMessage(data.text);
break;
case "session_init":
s.setSdkSessionId(data.sdkSessionId);
if (data.mcpServers)
s.setMcpServers(data.mcpServers);
if (data.fileCheckpointing)
s.setFileCheckpointing(true);
break;
case "thinking_delta":
s.appendStreamingThinking(data.text);
break;
case "text_delta":
s.flushStreamingThinking();
s.appendStreamingText(data.text);
break;
case "tool_start":
s.flushStreamingThinking();
s.flushStreamingText();
s.startToolCall(data.id, data.name);
break;
case "tool_call":
s.flushStreamingText();
s.finalizeToolCall(data.id, data.name, data.input);
break;
case "tool_result":
if (data.isError) {
s.errorToolCall(data.toolUseId, data.result);
}
else {
s.completeToolCall(data.toolUseId, data.result);
}
break;
case "checkpoint":
s.addCheckpoint(data.userMessageUuid);
break;
case "turn_complete":
s.flushStreamingThinking();
s.flushStreamingText();
s.setStopReason(data.stopReason ?? null);
s.addCost({
cost: data.cost ?? 0,
numTurns: data.numTurns ?? 0,
stopReason: data.stopReason ?? null,
usage: data.usage ?? null,
modelUsage: data.modelUsage ?? null,
});
break;
case "session_error":
s.flushStreamingThinking();
s.flushStreamingText();
s.setStopReason(data.stopReason ?? null);
s.addSystemMessage(`Session ended: ${data.subtype}`);
break;
}
}
}