@neeter/react 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -0
- package/dist/ChatInput.d.ts +3 -1
- package/dist/ChatInput.js +3 -3
- package/dist/icons.d.ts +3 -0
- package/dist/icons.js +3 -0
- package/dist/store.d.ts +4 -0
- package/dist/store.js +22 -0
- package/dist/use-agent.d.ts +1 -0
- package/dist/use-agent.js +40 -5
- package/package.json +4 -3
package/README.md
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# @neeter/react
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
Part of the [neeter](https://github.com/quantumleeps/neeter) toolkit.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @neeter/react
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Peer dependencies:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"react": ">=18.0.0",
|
|
18
|
+
"react-markdown": ">=10.0.0",
|
|
19
|
+
"zustand": ">=5.0.0",
|
|
20
|
+
"immer": ">=10.0.0"
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Components use [Tailwind CSS v4](https://tailwindcss.com/) utility classes.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```tsx
|
|
29
|
+
import { AgentProvider, MessageList, ChatInput, useAgentContext } from "@neeter/react";
|
|
30
|
+
|
|
31
|
+
function App() {
|
|
32
|
+
return (
|
|
33
|
+
<AgentProvider>
|
|
34
|
+
<Chat />
|
|
35
|
+
</AgentProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Chat() {
|
|
40
|
+
const { sendMessage } = useAgentContext();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex h-screen flex-col">
|
|
44
|
+
<MessageList className="flex-1" />
|
|
45
|
+
<ChatInput onSend={sendMessage} />
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Styling
|
|
52
|
+
|
|
53
|
+
Components use [shadcn/ui](https://ui.shadcn.com)-compatible CSS variable names. If you already have a shadcn theme, add one line:
|
|
54
|
+
|
|
55
|
+
```css
|
|
56
|
+
@import "tailwindcss";
|
|
57
|
+
@source "../node_modules/@neeter/react/dist";
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Without shadcn, import the bundled theme:
|
|
61
|
+
|
|
62
|
+
```css
|
|
63
|
+
@import "tailwindcss";
|
|
64
|
+
@import "@neeter/react/theme.css";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Key features
|
|
68
|
+
|
|
69
|
+
- **11 built-in widgets** — Diff views for edits, code blocks for reads, expandable pills for web searches, and more. Auto-registered on import.
|
|
70
|
+
- **Custom widgets** — Register your own components for MCP tools or app-specific rendering with `registerWidget()`.
|
|
71
|
+
- **Tool call lifecycle** — Each tool moves through `pending` → `streaming_input` → `running` → `complete` with streaming JSON input.
|
|
72
|
+
- **Permissions UI** — `ToolApprovalCard` and `UserQuestionCard` for browser-side tool approval.
|
|
73
|
+
- **Extended thinking** — Collapsible thinking blocks with streaming text.
|
|
74
|
+
- **Custom events** — Handle app-specific events from `onToolResult` via `AgentProvider`'s `onCustomEvent` prop.
|
|
75
|
+
- **Abort** — Stop the agent mid-turn with `stopSession()` from `useAgentContext()`.
|
|
76
|
+
|
|
77
|
+
## Examples
|
|
78
|
+
|
|
79
|
+
| Example | Description |
|
|
80
|
+
|---------|-------------|
|
|
81
|
+
| [basic-chat](https://github.com/quantumleeps/neeter/tree/main/examples/basic-chat) | Minimal component setup |
|
|
82
|
+
| [live-preview](https://github.com/quantumleeps/neeter/tree/main/examples/live-preview) | Custom events, widgets, split-pane preview |
|
|
83
|
+
|
|
84
|
+
## Documentation
|
|
85
|
+
|
|
86
|
+
- [Full API reference](https://github.com/quantumleeps/neeter#readme)
|
|
87
|
+
- [Built-in widgets](https://github.com/quantumleeps/neeter/blob/main/docs/built-in-widgets.md)
|
|
88
|
+
- [Custom widgets](https://github.com/quantumleeps/neeter/blob/main/docs/custom-widgets.md)
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
package/dist/ChatInput.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
export declare function ChatInput({ onSend, placeholder, disabled, className, }: {
|
|
1
|
+
export declare function ChatInput({ onSend, onStop, isStreaming, placeholder, disabled, className, }: {
|
|
2
2
|
onSend: (text: string) => void;
|
|
3
|
+
onStop?: () => void;
|
|
4
|
+
isStreaming?: boolean;
|
|
3
5
|
placeholder?: string;
|
|
4
6
|
disabled?: boolean;
|
|
5
7
|
className?: string;
|
package/dist/ChatInput.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useRef, useState } from "react";
|
|
3
3
|
import { cn } from "./cn.js";
|
|
4
|
-
import { SendIcon } from "./icons.js";
|
|
5
|
-
export function ChatInput({ onSend, placeholder = "Type a message...", disabled, className, }) {
|
|
4
|
+
import { SendIcon, StopIcon } from "./icons.js";
|
|
5
|
+
export function ChatInput({ onSend, onStop, isStreaming, placeholder = "Type a message...", disabled, className, }) {
|
|
6
6
|
const [text, setText] = useState("");
|
|
7
7
|
const textareaRef = useRef(null);
|
|
8
8
|
const isDisabled = disabled ?? false;
|
|
@@ -37,5 +37,5 @@ export function ChatInput({ onSend, placeholder = "Type a message...", disabled,
|
|
|
37
37
|
return (_jsxs("div", { className: cn("flex items-end gap-2 p-3", className), children: [_jsx("textarea", { ref: textareaRef, value: text, onChange: (e) => {
|
|
38
38
|
setText(e.target.value);
|
|
39
39
|
autoResize();
|
|
40
|
-
}, onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring max-h-40 overflow-y-hidden" }), _jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsx(SendIcon, {}) })] }));
|
|
40
|
+
}, onKeyDown: handleKeyDown, placeholder: placeholder, rows: 1, className: "flex-1 resize-none rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring max-h-40 overflow-y-hidden" }), isStreaming && onStop ? (_jsx("button", { type: "button", onClick: onStop, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90", children: _jsx(StopIcon, {}) })) : (_jsx("button", { type: "button", onClick: handleSend, disabled: !text.trim() || isDisabled, className: "inline-flex h-9 w-9 items-center justify-center rounded-md bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-50 disabled:pointer-events-none", children: _jsx(SendIcon, {}) }))] }));
|
|
41
41
|
}
|
package/dist/icons.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ export declare function ChevronIcon({ open, className }: {
|
|
|
2
2
|
open: boolean;
|
|
3
3
|
className?: string;
|
|
4
4
|
}): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
export declare function StopIcon({ className }: {
|
|
6
|
+
className?: string;
|
|
7
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
5
8
|
export declare function SendIcon({ className }: {
|
|
6
9
|
className?: string;
|
|
7
10
|
}): import("react/jsx-runtime").JSX.Element;
|
package/dist/icons.js
CHANGED
|
@@ -3,6 +3,9 @@ import { cn } from "./cn.js";
|
|
|
3
3
|
export function ChevronIcon({ open, className }) {
|
|
4
4
|
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" }) }));
|
|
5
5
|
}
|
|
6
|
+
export function StopIcon({ className }) {
|
|
7
|
+
return (_jsx("svg", { xmlns: "http://www.w3.org/2000/svg", viewBox: "0 0 24 24", fill: "currentColor", className: cn("h-4 w-4", className), "aria-hidden": "true", children: _jsx("rect", { x: "6", y: "6", width: "12", height: "12", rx: "1" }) }));
|
|
8
|
+
}
|
|
6
9
|
export function SendIcon({ className }) {
|
|
7
10
|
return (_jsxs("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-4 w-4", className), "aria-hidden": "true", children: [_jsx("path", { d: "M14.536 21.686a.5.5 0 0 0 .937-.024l6.5-19a.496.496 0 0 0-.635-.635l-19 6.5a.5.5 0 0 0-.024.937l7.93 3.18a2 2 0 0 1 1.112 1.11z" }), _jsx("path", { d: "m21.854 2.147-10.94 10.939" })] }));
|
|
8
11
|
}
|
package/dist/store.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ interface ChatStoreState {
|
|
|
8
8
|
streamingText: string;
|
|
9
9
|
streamingThinking: string;
|
|
10
10
|
pendingPermissions: PermissionRequest[];
|
|
11
|
+
totalCost: number;
|
|
12
|
+
totalTurns: number;
|
|
11
13
|
}
|
|
12
14
|
interface ChatStoreActions {
|
|
13
15
|
setSessionId: (id: string) => void;
|
|
@@ -26,6 +28,8 @@ interface ChatStoreActions {
|
|
|
26
28
|
setThinking: (v: boolean) => void;
|
|
27
29
|
addPermissionRequest: (request: PermissionRequest) => void;
|
|
28
30
|
removePermissionRequest: (requestId: string) => void;
|
|
31
|
+
addCost: (cost: number, turns: number) => void;
|
|
32
|
+
cancelInflightToolCalls: () => void;
|
|
29
33
|
reset: () => void;
|
|
30
34
|
}
|
|
31
35
|
export type ChatStoreShape = ChatStoreState & ChatStoreActions;
|
package/dist/store.js
CHANGED
|
@@ -21,6 +21,8 @@ export function createChatStore() {
|
|
|
21
21
|
streamingText: "",
|
|
22
22
|
streamingThinking: "",
|
|
23
23
|
pendingPermissions: [],
|
|
24
|
+
totalCost: 0,
|
|
25
|
+
totalTurns: 0,
|
|
24
26
|
setSessionId: (id) => set((s) => {
|
|
25
27
|
s.sessionId = id;
|
|
26
28
|
}),
|
|
@@ -128,6 +130,24 @@ export function createChatStore() {
|
|
|
128
130
|
removePermissionRequest: (requestId) => set((s) => {
|
|
129
131
|
s.pendingPermissions = s.pendingPermissions.filter((p) => p.requestId !== requestId);
|
|
130
132
|
}),
|
|
133
|
+
addCost: (cost, turns) => set((s) => {
|
|
134
|
+
s.totalCost += cost;
|
|
135
|
+
s.totalTurns += turns;
|
|
136
|
+
}),
|
|
137
|
+
cancelInflightToolCalls: () => set((s) => {
|
|
138
|
+
for (const msg of s.messages) {
|
|
139
|
+
if (msg.toolCalls) {
|
|
140
|
+
for (const tc of msg.toolCalls) {
|
|
141
|
+
if (tc.status === "pending" ||
|
|
142
|
+
tc.status === "streaming_input" ||
|
|
143
|
+
tc.status === "running") {
|
|
144
|
+
tc.status = "error";
|
|
145
|
+
tc.error = "Interrupted";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}),
|
|
131
151
|
reset: () => set((s) => {
|
|
132
152
|
s.sessionId = null;
|
|
133
153
|
s.messages = [];
|
|
@@ -136,6 +156,8 @@ export function createChatStore() {
|
|
|
136
156
|
s.streamingText = "";
|
|
137
157
|
s.streamingThinking = "";
|
|
138
158
|
s.pendingPermissions = [];
|
|
159
|
+
s.totalCost = 0;
|
|
160
|
+
s.totalTurns = 0;
|
|
139
161
|
}),
|
|
140
162
|
})));
|
|
141
163
|
}
|
package/dist/use-agent.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface UseAgentConfig {
|
|
|
7
7
|
export interface UseAgentReturn {
|
|
8
8
|
sessionId: string | null;
|
|
9
9
|
sendMessage: (text: string) => Promise<void>;
|
|
10
|
+
stopSession: () => Promise<void>;
|
|
10
11
|
respondToPermission: (response: PermissionResponse) => Promise<void>;
|
|
11
12
|
}
|
|
12
13
|
export declare function useAgent(store: ChatStore, config?: UseAgentConfig): UseAgentReturn;
|
package/dist/use-agent.js
CHANGED
|
@@ -3,6 +3,7 @@ export function useAgent(store, config) {
|
|
|
3
3
|
const endpoint = config?.endpoint ?? "/api";
|
|
4
4
|
const onCustomEvent = config?.onCustomEvent;
|
|
5
5
|
const eventSourceRef = useRef(null);
|
|
6
|
+
const abortedRef = useRef(false);
|
|
6
7
|
const sessionId = useSyncExternalStore(store.subscribe, () => store.getState().sessionId);
|
|
7
8
|
useEffect(() => {
|
|
8
9
|
let cancelled = false;
|
|
@@ -22,6 +23,7 @@ export function useAgent(store, config) {
|
|
|
22
23
|
return;
|
|
23
24
|
const es = new EventSource(`${endpoint}/sessions/${sessionId}/events`);
|
|
24
25
|
eventSourceRef.current = es;
|
|
26
|
+
abortedRef.current = false;
|
|
25
27
|
es.addEventListener("message_start", () => {
|
|
26
28
|
store.getState().setThinking(true);
|
|
27
29
|
});
|
|
@@ -66,20 +68,45 @@ export function useAgent(store, config) {
|
|
|
66
68
|
store.getState().flushStreamingThinking();
|
|
67
69
|
store.getState().flushStreamingText();
|
|
68
70
|
store.getState().setThinking(false);
|
|
69
|
-
const { subtype } = JSON.parse(e.data);
|
|
70
|
-
store.getState().addSystemMessage(`Session ended: ${subtype}`);
|
|
71
71
|
store.getState().setStreaming(false);
|
|
72
|
+
if (abortedRef.current) {
|
|
73
|
+
store.getState().cancelInflightToolCalls();
|
|
74
|
+
store.getState().addSystemMessage("Interrupted");
|
|
75
|
+
abortedRef.current = false;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const { subtype } = JSON.parse(e.data);
|
|
79
|
+
store.getState().addSystemMessage(`Session ended: ${subtype}`);
|
|
80
|
+
}
|
|
72
81
|
});
|
|
73
|
-
es.addEventListener("turn_complete", () => {
|
|
82
|
+
es.addEventListener("turn_complete", (e) => {
|
|
83
|
+
const { cost, numTurns } = JSON.parse(e.data);
|
|
74
84
|
store.getState().flushStreamingThinking();
|
|
75
85
|
store.getState().flushStreamingText();
|
|
76
86
|
store.getState().setThinking(false);
|
|
77
87
|
store.getState().setStreaming(false);
|
|
88
|
+
store.getState().addCost(cost ?? 0, numTurns ?? 0);
|
|
89
|
+
if (abortedRef.current) {
|
|
90
|
+
store.getState().cancelInflightToolCalls();
|
|
91
|
+
store.getState().addSystemMessage("Interrupted");
|
|
92
|
+
abortedRef.current = false;
|
|
93
|
+
}
|
|
78
94
|
});
|
|
79
95
|
es.addEventListener("error", () => {
|
|
80
|
-
if (
|
|
96
|
+
if (abortedRef.current) {
|
|
97
|
+
store.getState().flushStreamingThinking();
|
|
98
|
+
store.getState().flushStreamingText();
|
|
99
|
+
store.getState().cancelInflightToolCalls();
|
|
100
|
+
store.getState().setThinking(false);
|
|
101
|
+
store.getState().setStreaming(false);
|
|
102
|
+
store.getState().addSystemMessage("Interrupted");
|
|
103
|
+
abortedRef.current = false;
|
|
104
|
+
es.close();
|
|
105
|
+
}
|
|
106
|
+
else if (es.readyState === EventSource.CLOSED) {
|
|
81
107
|
store.getState().flushStreamingThinking();
|
|
82
108
|
store.getState().flushStreamingText();
|
|
109
|
+
store.getState().setThinking(false);
|
|
83
110
|
store.getState().setStreaming(false);
|
|
84
111
|
}
|
|
85
112
|
});
|
|
@@ -105,6 +132,14 @@ export function useAgent(store, config) {
|
|
|
105
132
|
body: JSON.stringify({ text }),
|
|
106
133
|
});
|
|
107
134
|
}, [sessionId, endpoint, store]);
|
|
135
|
+
const stopSession = useCallback(async () => {
|
|
136
|
+
if (!sessionId)
|
|
137
|
+
return;
|
|
138
|
+
abortedRef.current = true;
|
|
139
|
+
store.getState().setThinking(false);
|
|
140
|
+
store.getState().setStreaming(false);
|
|
141
|
+
await fetch(`${endpoint}/sessions/${sessionId}/abort`, { method: "POST" });
|
|
142
|
+
}, [sessionId, endpoint, store]);
|
|
108
143
|
const respondToPermission = useCallback(async (response) => {
|
|
109
144
|
if (!sessionId)
|
|
110
145
|
return;
|
|
@@ -115,5 +150,5 @@ export function useAgent(store, config) {
|
|
|
115
150
|
body: JSON.stringify(response),
|
|
116
151
|
});
|
|
117
152
|
}, [sessionId, endpoint, store]);
|
|
118
|
-
return { sessionId, sendMessage, respondToPermission };
|
|
153
|
+
return { sessionId, sendMessage, stopSession, respondToPermission };
|
|
119
154
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neeter/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "React components and hooks for building chat UIs on top of the Claude Agent SDK",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Dan Leeper",
|
|
@@ -20,12 +20,13 @@
|
|
|
20
20
|
},
|
|
21
21
|
"files": [
|
|
22
22
|
"dist",
|
|
23
|
-
"src/theme.css"
|
|
23
|
+
"src/theme.css",
|
|
24
|
+
"README.md"
|
|
24
25
|
],
|
|
25
26
|
"dependencies": {
|
|
26
27
|
"clsx": "^2.1.1",
|
|
27
28
|
"tailwind-merge": "^3.4.0",
|
|
28
|
-
"@neeter/types": "0.
|
|
29
|
+
"@neeter/types": "0.8.1"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"react": ">=18.0.0",
|