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.9.0
to
0.10.0
+1
-0
dist/AgentProvider.d.ts

@@ -10,2 +10,3 @@ import type { CustomEvent } from "@neeter/types";

endpoint?: string;
resumeSessionId?: string;
onCustomEvent?: (event: CustomEvent) => void;

@@ -12,0 +13,0 @@ children: ReactNode;

+2
-1

@@ -15,4 +15,5 @@ import { jsx as _jsx } from "react/jsx-runtime";

endpoint: props.endpoint,
resumeSessionId: props.resumeSessionId,
onCustomEvent: props.onCustomEvent,
}), [props.endpoint, props.onCustomEvent]);
}), [props.endpoint, props.resumeSessionId, props.onCustomEvent]);
const agent = useAgent(store, agentConfig);

@@ -19,0 +20,0 @@ const value = useMemo(() => ({ ...agent, store }), [agent, store]);

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

import "./widgets/WriteWidget.js";
export type { ChatMessage, CustomEvent, PermissionRequest, PermissionResponse, SSEEvent, ToolApprovalRequest, ToolApprovalResponse, ToolCallInfo, ToolCallPhase, UserQuestion, UserQuestionOption, UserQuestionRequest, UserQuestionResponse, } from "@neeter/types";
export type { ChatMessage, CustomEvent, PermissionRequest, PermissionResponse, SessionHistoryEntry, SessionInitEvent, SSEEvent, ToolApprovalRequest, ToolApprovalResponse, ToolCallInfo, ToolCallPhase, UserQuestion, UserQuestionOption, UserQuestionRequest, UserQuestionResponse, } from "@neeter/types";
export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";

@@ -22,3 +22,3 @@ export { ChatInput } from "./ChatInput.js";

export { StatusDot } from "./StatusDot.js";
export { type ChatStore, type ChatStoreShape, createChatStore } from "./store.js";
export { type ChatStore, type ChatStoreShape, createChatStore, replayEvents } from "./store.js";
export { TextMessage } from "./TextMessage.js";

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

@@ -21,3 +21,3 @@ // Built-in widgets (side-effect: auto-registers)

export { StatusDot } from "./StatusDot.js";
export { createChatStore } from "./store.js";
export { createChatStore, replayEvents } from "./store.js";
export { TextMessage } from "./TextMessage.js";

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

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

import type { ChatMessage, PermissionRequest } from "@neeter/types";
import type { ChatMessage, PermissionRequest, SSEEvent } from "@neeter/types";
import { type StoreApi } from "zustand/vanilla";
interface ChatStoreState {
sessionId: string | null;
sdkSessionId: string | null;
messages: ChatMessage[];

@@ -16,2 +17,3 @@ isStreaming: boolean;

setSessionId: (id: string) => void;
setSdkSessionId: (id: string) => void;
addUserMessage: (text: string) => void;

@@ -39,2 +41,3 @@ appendStreamingText: (text: string) => void;

export declare function createChatStore(): ChatStore;
export declare function replayEvents(store: ChatStore, events: SSEEvent[]): void;
export {};

@@ -18,2 +18,3 @@ import { immer } from "zustand/middleware/immer";

sessionId: null,
sdkSessionId: null,
messages: [],

@@ -30,2 +31,5 @@ isStreaming: false,

}),
setSdkSessionId: (id) => set((s) => {
s.sdkSessionId = id;
}),
addUserMessage: (text) => set((s) => {

@@ -155,2 +159,3 @@ s.messages.push({

s.sessionId = null;
s.sdkSessionId = null;
s.messages = [];

@@ -167,1 +172,44 @@ s.isStreaming = false;

}
export function replayEvents(store, events) {
const s = store.getState();
for (const evt of events) {
const data = JSON.parse(evt.data);
switch (evt.event) {
case "user_message":
s.addUserMessage(data.text);
break;
case "session_init":
s.setSdkSessionId(data.sdkSessionId);
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":
s.completeToolCall(data.toolUseId, data.result);
break;
case "turn_complete":
s.flushStreamingThinking();
s.flushStreamingText();
s.addCost(data.cost ?? 0, data.numTurns ?? 0);
break;
case "session_error":
s.flushStreamingThinking();
s.flushStreamingText();
s.addSystemMessage(`Session ended: ${data.subtype}`);
break;
}
}
}

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

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

@@ -9,6 +10,14 @@ }

sessionId: string | null;
sdkSessionId: string | null;
sessionHistory: SessionHistoryEntry[];
sendMessage: (text: string) => Promise<void>;
stopSession: () => Promise<void>;
respondToPermission: (response: PermissionResponse) => Promise<void>;
resumeSession: (options?: {
fork?: boolean;
sdkSessionId?: string;
}) => Promise<void>;
newSession: () => Promise<void>;
refreshHistory: () => Promise<void>;
}
export declare function useAgent(store: ChatStore, config?: UseAgentConfig): UseAgentReturn;

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

import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
import { replayEvents } from "./store.js";
export function useAgent(store, config) {
const endpoint = config?.endpoint ?? "/api";
const resumeSessionId = config?.resumeSessionId;
const onCustomEvent = config?.onCustomEvent;

@@ -8,6 +10,28 @@ const eventSourceRef = useRef(null);

const sessionId = useSyncExternalStore(store.subscribe, () => store.getState().sessionId);
const sdkSessionId = useSyncExternalStore(store.subscribe, () => store.getState().sdkSessionId);
const [sessionHistory, setSessionHistory] = useState([]);
useEffect(() => {
let cancelled = false;
async function init() {
const res = await fetch(`${endpoint}/sessions`, { method: "POST" });
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 }),
});
}
else {
res = await fetch(`${endpoint}/sessions`, { method: "POST" });
}
const data = await res.json();

@@ -21,3 +45,3 @@ if (!cancelled)

};
}, [endpoint, store]);
}, [endpoint, resumeSessionId, store]);
useEffect(() => {

@@ -29,2 +53,6 @@ if (!sessionId)

abortedRef.current = false;
es.addEventListener("session_init", (e) => {
const { sdkSessionId: id } = JSON.parse(e.data);
store.getState().setSdkSessionId(id);
});
es.addEventListener("message_start", () => {

@@ -156,3 +184,63 @@ store.getState().setThinking(true);

}, [sessionId, endpoint, store]);
return { sessionId, sendMessage, stopSession, respondToPermission };
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);
}
}
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,
}),
});
const data = await res.json();
store.getState().setSessionId(data.sessionId);
}, [endpoint, store]);
const refreshHistory = useCallback(async () => {
try {
const res = await fetch(`${endpoint}/sessions/history`);
if (res.ok)
setSessionHistory(await res.json());
}
catch {
/* ignore */
}
}, [endpoint]);
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]);
return {
sessionId,
sdkSessionId,
sessionHistory,
sendMessage,
stopSession,
respondToPermission,
resumeSession,
newSession,
refreshHistory,
};
}
{
"name": "@neeter/react",
"version": "0.9.0",
"version": "0.10.0",
"description": "React components and hooks for building chat UIs on top of the Claude Agent SDK",

@@ -29,14 +29,18 @@ "license": "MIT",

"tailwind-merge": "^3.4.0",
"@neeter/types": "0.9.0"
"@neeter/types": "0.10.0"
},
"peerDependencies": {
"immer": ">=10.0.0",
"react": ">=18.0.0",
"react-markdown": ">=10.0.0",
"zustand": ">=5.0.0",
"immer": ">=10.0.0"
"zustand": ">=5.0.0"
},
"devDependencies": {
"@testing-library/react": "^16.3.2",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"immer": "^11.1.4",
"jsdom": "^28.1.0",
"react": "^19.2.0",
"react-dom": "^19.2.4",
"zustand": "^5.0.11"

@@ -43,0 +47,0 @@ },

@@ -74,2 +74,3 @@ # @neeter/react

- **Extended thinking** — Collapsible thinking blocks with streaming text.
- **Session resume** — Resume past sessions with `resumeSession()`, start fresh with `newSession()`, and browse history with `refreshHistory()`. `replayEvents` reconstructs the chat UI from persisted events.
- **Custom events** — Handle app-specific events from `onToolResult` via `AgentProvider`'s `onCustomEvent` prop.

@@ -76,0 +77,0 @@ - **Abort** — Stop the agent mid-turn with `stopSession()` from `useAgentContext()`.