fireworks-ai 0.2.0
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/LICENSE +21 -0
- package/README.md +320 -0
- package/dist/react/AgentProvider.d.ts +15 -0
- package/dist/react/AgentProvider.js +30 -0
- package/dist/react/ChatInput.d.ts +6 -0
- package/dist/react/ChatInput.js +21 -0
- package/dist/react/CollapsibleCard.d.ts +9 -0
- package/dist/react/CollapsibleCard.js +8 -0
- package/dist/react/MessageList.d.ts +3 -0
- package/dist/react/MessageList.js +29 -0
- package/dist/react/StatusDot.d.ts +5 -0
- package/dist/react/StatusDot.js +12 -0
- package/dist/react/TextMessage.d.ts +5 -0
- package/dist/react/TextMessage.js +13 -0
- package/dist/react/ThinkingIndicator.d.ts +3 -0
- package/dist/react/ThinkingIndicator.js +5 -0
- package/dist/react/ToolCallCard.d.ts +5 -0
- package/dist/react/ToolCallCard.js +33 -0
- package/dist/react/cn.d.ts +2 -0
- package/dist/react/cn.js +5 -0
- package/dist/react/index.d.ts +13 -0
- package/dist/react/index.js +12 -0
- package/dist/react/registry.d.ts +4 -0
- package/dist/react/registry.js +10 -0
- package/dist/react/registry.test.d.ts +1 -0
- package/dist/react/registry.test.js +26 -0
- package/dist/react/store.d.ts +28 -0
- package/dist/react/store.js +109 -0
- package/dist/react/store.test.d.ts +1 -0
- package/dist/react/store.test.js +113 -0
- package/dist/react/use-agent.d.ts +11 -0
- package/dist/react/use-agent.js +96 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.js +4 -0
- package/dist/server/push-channel.d.ts +8 -0
- package/dist/server/push-channel.js +40 -0
- package/dist/server/push-channel.test.d.ts +1 -0
- package/dist/server/push-channel.test.js +57 -0
- package/dist/server/router.d.ts +8 -0
- package/dist/server/router.js +52 -0
- package/dist/server/session.d.ts +32 -0
- package/dist/server/session.js +73 -0
- package/dist/server/translator.d.ts +14 -0
- package/dist/server/translator.js +151 -0
- package/dist/server/translator.test.d.ts +1 -0
- package/dist/server/translator.test.js +156 -0
- package/dist/types.d.ts +39 -0
- package/dist/types.js +1 -0
- package/package.json +69 -0
- package/src/theme.css +133 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dan Leeper
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
# fireworks-ai
|
|
2
|
+
|
|
3
|
+
A React + Hono toolkit for building chat UIs on top of the [Claude Agent SDK](https://github.com/anthropics/claude-agent-sdk). Tool calls, text deltas, and results are individual sparks — fireworks-ai launches them into a unified, vivid display.
|
|
4
|
+
|
|
5
|
+
## Why fireworks-ai
|
|
6
|
+
|
|
7
|
+
The Claude Agent SDK gives you a powerful agentic loop — but it's a server-side `AsyncGenerator` with no opinion on how to get those events to a browser. fireworks-ai bridges that gap:
|
|
8
|
+
|
|
9
|
+
- **Multi-turn persistent sessions** — `PushChannel` + `SessionManager` let users send messages at any time. Messages queue and the SDK picks them up when ready — no "wait for the agent to finish" lockout.
|
|
10
|
+
- **Named SSE event routing** — The SDK yields a flat stream of internal message types. The `MessageTranslator` reshapes them into semantically named SSE events (`text_delta`, `tool_start`, `tool_call`, `tool_result`, ...) that the browser's `EventSource` can route with native `addEventListener`.
|
|
11
|
+
- **UI-friendly tool lifecycle** — Tool calls move through `pending` → `streaming_input` → `running` → `complete` phases with streaming JSON input, giving your UI fine-grained control over loading states and progressive rendering.
|
|
12
|
+
- **Structured custom events** — Hook into tool results with `onToolResult` and emit typed `{ name, value }` events for app-specific reactivity (e.g. "document saved", "data refreshed") without touching the core protocol.
|
|
13
|
+
- **Client-server separation** — Server handles transport (SSE encoding, session routing). Client handles state (Zustand store, React components). The translator is the clean seam between them.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pnpm add fireworks-ai
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Peer dependencies:
|
|
22
|
+
|
|
23
|
+
```json
|
|
24
|
+
{
|
|
25
|
+
"@anthropic-ai/claude-agent-sdk": ">=0.2.0",
|
|
26
|
+
"hono": ">=4.0.0",
|
|
27
|
+
"react": ">=18.0.0",
|
|
28
|
+
"react-markdown": ">=10.0.0",
|
|
29
|
+
"zustand": ">=5.0.0",
|
|
30
|
+
"immer": ">=10.0.0",
|
|
31
|
+
"tailwindcss": ">=4.0.0"
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Server
|
|
36
|
+
|
|
37
|
+
`fireworks-ai/server` gives you a Hono router that manages Agent SDK sessions and streams events to the client over SSE.
|
|
38
|
+
|
|
39
|
+
The Claude Agent SDK reads your API key from the environment automatically. Make sure it's set before starting your server:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
export ANTHROPIC_API_KEY=your-api-key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { Hono } from "hono";
|
|
47
|
+
import { serve } from "@hono/node-server";
|
|
48
|
+
import {
|
|
49
|
+
createAgentRouter,
|
|
50
|
+
SessionManager,
|
|
51
|
+
MessageTranslator,
|
|
52
|
+
} from "fireworks-ai/server";
|
|
53
|
+
|
|
54
|
+
const sessions = new SessionManager(() => ({
|
|
55
|
+
context: {},
|
|
56
|
+
model: "claude-sonnet-4-5-20250929",
|
|
57
|
+
systemPrompt: "You are a helpful assistant.",
|
|
58
|
+
maxTurns: 50,
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
const translator = new MessageTranslator();
|
|
62
|
+
|
|
63
|
+
const app = new Hono();
|
|
64
|
+
app.route("/", createAgentRouter({ sessions, translator }));
|
|
65
|
+
|
|
66
|
+
serve({ fetch: app.fetch, port: 3000 });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
This gives you three endpoints:
|
|
70
|
+
|
|
71
|
+
| Method | Path | Description |
|
|
72
|
+
|--------|------|-------------|
|
|
73
|
+
| `POST` | `/api/sessions` | Create a session, returns `{ sessionId }` |
|
|
74
|
+
| `POST` | `/api/sessions/:id/messages` | Send `{ text }` to a session |
|
|
75
|
+
| `GET` | `/api/sessions/:id/events` | SSE stream of agent events |
|
|
76
|
+
|
|
77
|
+
### Session context
|
|
78
|
+
|
|
79
|
+
`SessionManager` takes a factory function that runs once per session. The generic type parameter lets you attach per-session state:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
interface MyContext {
|
|
83
|
+
history: string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sessions = new SessionManager<MyContext>(() => ({
|
|
87
|
+
context: { history: [] },
|
|
88
|
+
model: "claude-sonnet-4-5-20250929",
|
|
89
|
+
systemPrompt: "You are a helpful assistant.",
|
|
90
|
+
mcpServers: { myServer: createMyServer() },
|
|
91
|
+
allowedTools: ["mcp__myServer__*"],
|
|
92
|
+
maxTurns: 100,
|
|
93
|
+
}));
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The context is available in translator hooks (see below).
|
|
97
|
+
|
|
98
|
+
### Reacting to tool results
|
|
99
|
+
|
|
100
|
+
Use `onToolResult` to inspect what the agent did and emit structured custom events:
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const translator = new MessageTranslator<MyContext>({
|
|
104
|
+
onToolResult: (toolName, result, session) => {
|
|
105
|
+
if (toolName === "save_note") {
|
|
106
|
+
session.context.history.push(result);
|
|
107
|
+
return [{ name: "notes_updated", value: session.context.history }];
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Each returned `{ name, value }` object is sent to the client as a `custom` SSE event.
|
|
115
|
+
|
|
116
|
+
## Client
|
|
117
|
+
|
|
118
|
+
`fireworks-ai/react` provides a drop-in chat UI that connects to your server.
|
|
119
|
+
|
|
120
|
+
```tsx
|
|
121
|
+
import { AgentProvider, MessageList, ChatInput, useAgentContext } from "fireworks-ai/react";
|
|
122
|
+
|
|
123
|
+
function App() {
|
|
124
|
+
return (
|
|
125
|
+
<AgentProvider>
|
|
126
|
+
<Chat />
|
|
127
|
+
</AgentProvider>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Chat() {
|
|
132
|
+
const { sendMessage } = useAgentContext();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className="flex h-screen flex-col">
|
|
136
|
+
<MessageList className="flex-1" />
|
|
137
|
+
<ChatInput onSend={sendMessage} />
|
|
138
|
+
</div>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Components use Tailwind utility classes and accept `className` for overrides.
|
|
144
|
+
|
|
145
|
+
### Custom events
|
|
146
|
+
|
|
147
|
+
If your server emits custom events (via `onToolResult`), handle them with `onCustomEvent`:
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<AgentProvider
|
|
151
|
+
onCustomEvent={(e) => {
|
|
152
|
+
if (e.name === "notes_updated") {
|
|
153
|
+
myStore.getState().setNotes(e.value);
|
|
154
|
+
}
|
|
155
|
+
}}
|
|
156
|
+
>
|
|
157
|
+
<Chat />
|
|
158
|
+
</AgentProvider>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Each event is a typed `CustomEvent<T>` with `name` and `value` fields.
|
|
162
|
+
|
|
163
|
+
### Widgets
|
|
164
|
+
|
|
165
|
+
Register custom components for tool results. When a tool completes, `ToolCallCard` looks up the matching widget and renders it with typed props.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { registerWidget, type WidgetProps } from "fireworks-ai/react";
|
|
169
|
+
|
|
170
|
+
interface SearchResult {
|
|
171
|
+
query: string;
|
|
172
|
+
results: { title: string; url: string }[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function SearchWidget({ result }: WidgetProps<SearchResult>) {
|
|
176
|
+
if (!result) return null;
|
|
177
|
+
return (
|
|
178
|
+
<ul>
|
|
179
|
+
{result.results.map((r) => (
|
|
180
|
+
<li key={r.url}>
|
|
181
|
+
<a href={r.url}>{r.title}</a>
|
|
182
|
+
</li>
|
|
183
|
+
))}
|
|
184
|
+
</ul>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
registerWidget({
|
|
189
|
+
toolName: "search",
|
|
190
|
+
label: "Search",
|
|
191
|
+
richLabel: (r) => `Search: ${r.query}`,
|
|
192
|
+
component: SearchWidget,
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`toolName` matches the short name — MCP prefixes (`mcp__server__`) are stripped automatically. Call `registerWidget` at module scope; barrel-import your widgets directory so registrations run before render.
|
|
197
|
+
|
|
198
|
+
Tool calls without a registered widget show a minimal status indicator.
|
|
199
|
+
|
|
200
|
+
### Tool call lifecycle
|
|
201
|
+
|
|
202
|
+
Each tool call moves through phases, reflected in `WidgetProps.phase`:
|
|
203
|
+
|
|
204
|
+
| Phase | Trigger | What's available |
|
|
205
|
+
|-------|---------|-----------------:|
|
|
206
|
+
| `pending` | `tool_start` SSE event | `input: {}` |
|
|
207
|
+
| `streaming_input` | `tool_input_delta` events | `partialInput` accumulates |
|
|
208
|
+
| `running` | `tool_call` event (input finalized) | `input` is complete |
|
|
209
|
+
| `complete` | `tool_result` event | `result` is JSON-parsed |
|
|
210
|
+
| `error` | Error during execution | `error` message |
|
|
211
|
+
|
|
212
|
+
## Styling
|
|
213
|
+
|
|
214
|
+
Fireworks components use Tailwind v4 utility classes and [shadcn/ui](https://ui.shadcn.com)-compatible CSS variable names (`bg-primary`, `text-muted-foreground`, `border-border`, etc.).
|
|
215
|
+
|
|
216
|
+
### With shadcn/ui
|
|
217
|
+
|
|
218
|
+
Your existing theme variables are already compatible. Add one line to your main CSS so Tailwind scans fireworks-ai's component source for utility classes:
|
|
219
|
+
|
|
220
|
+
```css
|
|
221
|
+
@import "tailwindcss";
|
|
222
|
+
@source "../node_modules/fireworks-ai/src";
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The `@source` path is relative to your CSS file — adjust if your stylesheet lives in a nested directory (e.g. `../../node_modules/fireworks-ai/src`).
|
|
226
|
+
|
|
227
|
+
### Without shadcn/ui
|
|
228
|
+
|
|
229
|
+
Import the bundled theme, which includes source scanning automatically:
|
|
230
|
+
|
|
231
|
+
```css
|
|
232
|
+
@import "tailwindcss";
|
|
233
|
+
@import "fireworks-ai/theme.css";
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
This provides a neutral OKLCH palette with light + dark mode support and the Tailwind v4 `@theme inline` variable bridge.
|
|
237
|
+
|
|
238
|
+
### Dark mode
|
|
239
|
+
|
|
240
|
+
Dark mode activates via:
|
|
241
|
+
- `.dark` class on `<html>` (recommended), or
|
|
242
|
+
- `prefers-color-scheme: dark` system preference (automatic)
|
|
243
|
+
|
|
244
|
+
Add `.light` to `<html>` to force light mode when using system preference detection.
|
|
245
|
+
|
|
246
|
+
### Switching to shadcn later
|
|
247
|
+
|
|
248
|
+
Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme takes over with zero migration.
|
|
249
|
+
|
|
250
|
+
## API Reference
|
|
251
|
+
|
|
252
|
+
### `fireworks-ai/server`
|
|
253
|
+
|
|
254
|
+
| Export | Description |
|
|
255
|
+
|--------|-------------|
|
|
256
|
+
| `SessionManager<TCtx>` | Manages agent sessions with per-session context |
|
|
257
|
+
| `Session<TCtx>` | A single session — `id`, `context`, `pushMessage()`, `abort()` |
|
|
258
|
+
| `SessionInit<TCtx>` | Factory return type — `model`, `systemPrompt`, `mcpServers`, etc. |
|
|
259
|
+
| `MessageTranslator<TCtx>` | Converts SDK messages to SSE events |
|
|
260
|
+
| `TranslatorConfig<TCtx>` | Translator options — `onToolResult` hook |
|
|
261
|
+
| `createAgentRouter<TCtx>(config)` | Returns a Hono app with session + SSE routes |
|
|
262
|
+
| `PushChannel<T>` | Async iterable queue for feeding messages to the SDK |
|
|
263
|
+
| `sseEncode(event)` | Formats an `SSEEvent` as an SSE string |
|
|
264
|
+
| `streamSession(session, translator)` | Async generator yielding `SSEEvent`s |
|
|
265
|
+
|
|
266
|
+
### `fireworks-ai/react`
|
|
267
|
+
|
|
268
|
+
| Export | Description |
|
|
269
|
+
|--------|-------------|
|
|
270
|
+
| `AgentProvider` | Context provider — wraps store + SSE connection |
|
|
271
|
+
| `useAgentContext()` | Returns `{ sessionId, sendMessage, store }` |
|
|
272
|
+
| `useChatStore(selector)` | Zustand selector hook into chat state |
|
|
273
|
+
| `createChatStore()` | Creates a vanilla Zustand store (for advanced use) |
|
|
274
|
+
| `useAgent(store, config?)` | SSE connection hook (used internally by `AgentProvider`) |
|
|
275
|
+
| `MessageList` | Auto-scrolling message list with thinking indicator |
|
|
276
|
+
| `TextMessage` | Markdown-rendered message bubble |
|
|
277
|
+
| `ChatInput` | Textarea + send button |
|
|
278
|
+
| `ToolCallCard` | Lifecycle-aware tool call display |
|
|
279
|
+
| `ThinkingIndicator` | Animated dots shown while agent is generating |
|
|
280
|
+
| `CollapsibleCard` | Expandable card wrapper |
|
|
281
|
+
| `StatusDot` | Phase-colored status indicator |
|
|
282
|
+
| `cn(...inputs)` | `clsx` + `tailwind-merge` utility for class merging |
|
|
283
|
+
| `registerWidget(registration)` | Register a component for a tool name |
|
|
284
|
+
| `getWidget(toolName)` | Look up a registered widget |
|
|
285
|
+
| `stripMcpPrefix(name)` | `"mcp__server__tool"` → `"tool"` |
|
|
286
|
+
|
|
287
|
+
### Types (re-exported from both entry points)
|
|
288
|
+
|
|
289
|
+
| Type | Description |
|
|
290
|
+
|------|-------------|
|
|
291
|
+
| `SSEEvent` | `{ event: string, data: string }` |
|
|
292
|
+
| `ChatMessage` | `{ id, role, content, toolCalls? }` |
|
|
293
|
+
| `ToolCallInfo` | `{ id, name, input, partialInput?, result?, error?, status }` |
|
|
294
|
+
| `ToolCallPhase` | `"pending" \| "streaming_input" \| "running" \| "complete" \| "error"` |
|
|
295
|
+
| `WidgetProps<TResult>` | Props passed to widget components |
|
|
296
|
+
| `WidgetRegistration<TResult>` | Widget registration descriptor |
|
|
297
|
+
| `ChatStore` | `StoreApi<ChatStoreShape>` — vanilla Zustand store |
|
|
298
|
+
| `ChatStoreShape` | Full state + actions interface |
|
|
299
|
+
| `CustomEvent<T>` | `{ name: string, value: T }` — structured app-level event |
|
|
300
|
+
|
|
301
|
+
## SSE Events
|
|
302
|
+
|
|
303
|
+
Events emitted by the server, handled automatically by `useAgent`:
|
|
304
|
+
|
|
305
|
+
| Event | Payload | Description |
|
|
306
|
+
|-------|---------|-------------|
|
|
307
|
+
| `message_start` | `{}` | Agent began generating a response |
|
|
308
|
+
| `text_delta` | `{ text }` | Streaming text chunk |
|
|
309
|
+
| `tool_start` | `{ id, name }` | Agent began calling a tool |
|
|
310
|
+
| `tool_input_delta` | `{ id, partialJson }` | Streaming tool input JSON |
|
|
311
|
+
| `tool_call` | `{ id, name, input }` | Tool input finalized |
|
|
312
|
+
| `tool_result` | `{ toolUseId, result }` | Tool execution result |
|
|
313
|
+
| `tool_progress` | `{ toolName, elapsed }` | Long-running tool heartbeat |
|
|
314
|
+
| `turn_complete` | `{ numTurns, cost }` | Agent turn finished |
|
|
315
|
+
| `custom` | `{ name, value }` | App-specific event from `onToolResult` |
|
|
316
|
+
| `session_error` | `{ subtype }` | Session ended with error |
|
|
317
|
+
|
|
318
|
+
## License
|
|
319
|
+
|
|
320
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { CustomEvent } from "../types.js";
|
|
3
|
+
import { type ChatStore, type ChatStoreShape } from "./store.js";
|
|
4
|
+
import { type UseAgentReturn } from "./use-agent.js";
|
|
5
|
+
interface AgentContextValue extends UseAgentReturn {
|
|
6
|
+
store: ChatStore;
|
|
7
|
+
}
|
|
8
|
+
export declare function AgentProvider(props: {
|
|
9
|
+
endpoint?: string;
|
|
10
|
+
onCustomEvent?: (event: CustomEvent) => void;
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function useAgentContext(): AgentContextValue;
|
|
14
|
+
export declare function useChatStore<T>(selector: (state: ChatStoreShape) => T): T;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useMemo, useRef } from "react";
|
|
3
|
+
import { useStore } from "zustand";
|
|
4
|
+
import { createChatStore } from "./store.js";
|
|
5
|
+
import { useAgent } from "./use-agent.js";
|
|
6
|
+
const AgentContext = createContext(null);
|
|
7
|
+
export function AgentProvider(props) {
|
|
8
|
+
const storeRef = useRef(null);
|
|
9
|
+
if (!storeRef.current) {
|
|
10
|
+
storeRef.current = createChatStore();
|
|
11
|
+
}
|
|
12
|
+
const store = storeRef.current;
|
|
13
|
+
const agentConfig = useMemo(() => ({
|
|
14
|
+
endpoint: props.endpoint,
|
|
15
|
+
onCustomEvent: props.onCustomEvent,
|
|
16
|
+
}), [props.endpoint, props.onCustomEvent]);
|
|
17
|
+
const agent = useAgent(store, agentConfig);
|
|
18
|
+
const value = useMemo(() => ({ ...agent, store }), [agent, store]);
|
|
19
|
+
return _jsx(AgentContext, { value: value, children: props.children });
|
|
20
|
+
}
|
|
21
|
+
export function useAgentContext() {
|
|
22
|
+
const ctx = useContext(AgentContext);
|
|
23
|
+
if (!ctx)
|
|
24
|
+
throw new Error("useAgentContext must be used within <AgentProvider>");
|
|
25
|
+
return ctx;
|
|
26
|
+
}
|
|
27
|
+
export function useChatStore(selector) {
|
|
28
|
+
const { store } = useAgentContext();
|
|
29
|
+
return useStore(store, selector);
|
|
30
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
export function ChatInput({ onSend, placeholder = "Type a message...", disabled, className, }) {
|
|
5
|
+
const [text, setText] = useState("");
|
|
6
|
+
const isDisabled = disabled ?? false;
|
|
7
|
+
function handleSend() {
|
|
8
|
+
const trimmed = text.trim();
|
|
9
|
+
if (!trimmed || isDisabled)
|
|
10
|
+
return;
|
|
11
|
+
onSend(trimmed);
|
|
12
|
+
setText("");
|
|
13
|
+
}
|
|
14
|
+
function handleKeyDown(e) {
|
|
15
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
16
|
+
e.preventDefault();
|
|
17
|
+
handleSend();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs("div", { className: cn("flex items-end gap-2 p-3", className), children: [_jsx("textarea", { value: text, onChange: (e) => setText(e.target.value), 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" }), _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: _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: "h-4 w-4", "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" })] }) })] }));
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ToolCallPhase } from "../types.js";
|
|
3
|
+
export declare function CollapsibleCard({ label, status, defaultOpen, children, className, }: {
|
|
4
|
+
label: string;
|
|
5
|
+
status?: ToolCallPhase;
|
|
6
|
+
defaultOpen?: boolean;
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
import { StatusDot } from "./StatusDot.js";
|
|
5
|
+
export function CollapsibleCard({ label, status, defaultOpen = true, children, className, }) {
|
|
6
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
7
|
+
return (_jsxs("div", { className: cn("rounded-md border border-border bg-accent/50 overflow-hidden", className), children: [_jsxs("button", { type: "button", onClick: () => setOpen(!open), className: "flex w-full items-center gap-2 px-2.5 py-1.5 text-xs text-muted-foreground hover:bg-accent transition-colors", children: [_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"), "aria-hidden": "true", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }), status && _jsx(StatusDot, { status: status }), _jsx("span", { children: label })] }), open && _jsx("div", { className: "px-2.5 pb-2", children: children })] }));
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef } from "react";
|
|
3
|
+
import { useChatStore } from "./AgentProvider.js";
|
|
4
|
+
import { cn } from "./cn.js";
|
|
5
|
+
import { TextMessage } from "./TextMessage.js";
|
|
6
|
+
import { ThinkingIndicator } from "./ThinkingIndicator.js";
|
|
7
|
+
import { ToolCallCard } from "./ToolCallCard.js";
|
|
8
|
+
export function MessageList({ className }) {
|
|
9
|
+
const messages = useChatStore((s) => s.messages);
|
|
10
|
+
const streamingText = useChatStore((s) => s.streamingText);
|
|
11
|
+
const isThinking = useChatStore((s) => s.isThinking);
|
|
12
|
+
const bottomRef = useRef(null);
|
|
13
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: scroll on content changes
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
bottomRef.current?.scrollIntoView({ behavior: "smooth" });
|
|
16
|
+
}, [messages, streamingText, isThinking]);
|
|
17
|
+
return (_jsx("div", { className: cn("flex-1 overflow-y-auto", className), children: _jsxs("div", { className: "flex flex-col gap-3 p-4 text-sm", children: [messages.map((msg) => {
|
|
18
|
+
if (msg.toolCalls?.length) {
|
|
19
|
+
return (_jsx("div", { className: "flex flex-col gap-1.5", children: msg.toolCalls.map((tc) => (_jsx(ToolCallCard, { toolCall: tc }, tc.id))) }, msg.id));
|
|
20
|
+
}
|
|
21
|
+
if (msg.role === "system") {
|
|
22
|
+
return (_jsx("div", { className: "text-center text-xs text-destructive", children: msg.content }, msg.id));
|
|
23
|
+
}
|
|
24
|
+
if (msg.content) {
|
|
25
|
+
return (_jsx(TextMessage, { role: msg.role, content: msg.content }, msg.id));
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
}), streamingText && _jsx(TextMessage, { role: "assistant", content: streamingText }), isThinking && _jsx(ThinkingIndicator, {}), _jsx("div", { ref: bottomRef })] }) }));
|
|
29
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
const phaseClasses = {
|
|
4
|
+
pending: "bg-muted-foreground/40",
|
|
5
|
+
streaming_input: "bg-yellow-500 animate-pulse",
|
|
6
|
+
running: "bg-yellow-500 animate-pulse",
|
|
7
|
+
complete: "bg-green-500",
|
|
8
|
+
error: "bg-red-500",
|
|
9
|
+
};
|
|
10
|
+
export function StatusDot({ status, className }) {
|
|
11
|
+
return _jsx("span", { className: cn("h-2 w-2 shrink-0 rounded-full", phaseClasses[status], className) });
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import Markdown from "react-markdown";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
export function TextMessage({ role, content, className, }) {
|
|
5
|
+
const isUser = role === "user";
|
|
6
|
+
return (_jsx("div", { className: cn("flex", isUser ? "justify-end" : "justify-start", className), children: _jsx("div", { className: cn("max-w-[85%] rounded-lg px-3 py-2 text-sm overflow-hidden break-words", isUser ? "bg-primary text-primary-foreground" : "bg-muted text-foreground"), children: isUser ? (_jsx("span", { className: "whitespace-pre-wrap", children: content })) : (_jsx("div", { className: "[&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: _jsx(Markdown, { components: {
|
|
7
|
+
p: ({ children }) => _jsx("p", { className: "mb-2 last:mb-0", children: children }),
|
|
8
|
+
ul: ({ children }) => _jsx("ul", { className: "mb-2 ml-4 list-disc last:mb-0", children: children }),
|
|
9
|
+
ol: ({ children }) => (_jsx("ol", { className: "mb-2 ml-4 list-decimal last:mb-0", children: children })),
|
|
10
|
+
li: ({ children }) => _jsx("li", { className: "mb-0.5", children: children }),
|
|
11
|
+
strong: ({ children }) => _jsx("strong", { className: "font-semibold", children: children }),
|
|
12
|
+
}, children: content }) })) }) }));
|
|
13
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { cn } from "./cn.js";
|
|
3
|
+
export function ThinkingIndicator({ className }) {
|
|
4
|
+
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]" })] }) }));
|
|
5
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { CollapsibleCard } from "./CollapsibleCard.js";
|
|
3
|
+
import { cn } from "./cn.js";
|
|
4
|
+
import { getWidget, stripMcpPrefix } from "./registry.js";
|
|
5
|
+
import { StatusDot } from "./StatusDot.js";
|
|
6
|
+
export function ToolCallCard({ toolCall, className, }) {
|
|
7
|
+
const short = stripMcpPrefix(toolCall.name);
|
|
8
|
+
const reg = getWidget(short);
|
|
9
|
+
const label = reg?.label ?? short;
|
|
10
|
+
if (toolCall.status === "complete" && toolCall.result && reg) {
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(toolCall.result);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
parsed = undefined;
|
|
17
|
+
}
|
|
18
|
+
const displayLabel = parsed && reg.richLabel ? (reg.richLabel(parsed) ?? label) : label;
|
|
19
|
+
const widgetProps = {
|
|
20
|
+
phase: toolCall.status,
|
|
21
|
+
toolUseId: toolCall.id,
|
|
22
|
+
input: toolCall.input,
|
|
23
|
+
partialInput: toolCall.partialInput,
|
|
24
|
+
result: parsed,
|
|
25
|
+
error: toolCall.error,
|
|
26
|
+
};
|
|
27
|
+
return (_jsx(CollapsibleCard, { label: displayLabel, status: toolCall.status, className: className, children: _jsx(reg.component, { ...widgetProps }) }));
|
|
28
|
+
}
|
|
29
|
+
if (toolCall.status === "error") {
|
|
30
|
+
return (_jsxs("div", { className: cn("flex items-center gap-2 rounded-md border border-destructive/50 bg-destructive/10 px-2.5 py-1.5 text-xs text-muted-foreground", className), children: [_jsx(StatusDot, { status: toolCall.status }), _jsx("span", { children: label }), toolCall.error && _jsx("span", { className: "ml-auto text-destructive", children: toolCall.error })] }));
|
|
31
|
+
}
|
|
32
|
+
return (_jsxs("div", { className: cn("flex items-center gap-2 rounded-md border border-border bg-accent/50 px-2.5 py-1.5 text-xs text-muted-foreground", className), children: [_jsx(StatusDot, { status: toolCall.status }), _jsx("span", { children: label }), toolCall.status === "streaming_input" && toolCall.partialInput && (_jsx("span", { className: "ml-auto truncate max-w-[200px] opacity-50 font-mono text-[10px]", children: toolCall.partialInput.slice(0, 80) }))] }));
|
|
33
|
+
}
|
package/dist/react/cn.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type { ChatMessage, CustomEvent, SSEEvent, ToolCallInfo, ToolCallPhase, WidgetProps, WidgetRegistration, } from "../types.js";
|
|
2
|
+
export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
|
|
3
|
+
export { ChatInput } from "./ChatInput.js";
|
|
4
|
+
export { CollapsibleCard } from "./CollapsibleCard.js";
|
|
5
|
+
export { cn } from "./cn.js";
|
|
6
|
+
export { MessageList } from "./MessageList.js";
|
|
7
|
+
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
|
|
8
|
+
export { StatusDot } from "./StatusDot.js";
|
|
9
|
+
export { type ChatStore, type ChatStoreShape, createChatStore } from "./store.js";
|
|
10
|
+
export { TextMessage } from "./TextMessage.js";
|
|
11
|
+
export { ThinkingIndicator } from "./ThinkingIndicator.js";
|
|
12
|
+
export { ToolCallCard } from "./ToolCallCard.js";
|
|
13
|
+
export { type UseAgentConfig, type UseAgentReturn, useAgent } from "./use-agent.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
|
|
2
|
+
export { ChatInput } from "./ChatInput.js";
|
|
3
|
+
export { CollapsibleCard } from "./CollapsibleCard.js";
|
|
4
|
+
export { cn } from "./cn.js";
|
|
5
|
+
export { MessageList } from "./MessageList.js";
|
|
6
|
+
export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
|
|
7
|
+
export { StatusDot } from "./StatusDot.js";
|
|
8
|
+
export { createChatStore } from "./store.js";
|
|
9
|
+
export { TextMessage } from "./TextMessage.js";
|
|
10
|
+
export { ThinkingIndicator } from "./ThinkingIndicator.js";
|
|
11
|
+
export { ToolCallCard } from "./ToolCallCard.js";
|
|
12
|
+
export { useAgent } from "./use-agent.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { WidgetRegistration } from "../types.js";
|
|
2
|
+
export declare function registerWidget<TResult>(reg: WidgetRegistration<TResult>): void;
|
|
3
|
+
export declare function getWidget(toolName: string): WidgetRegistration | undefined;
|
|
4
|
+
export declare function stripMcpPrefix(name: string): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const widgets = new Map();
|
|
2
|
+
export function registerWidget(reg) {
|
|
3
|
+
widgets.set(reg.toolName, reg);
|
|
4
|
+
}
|
|
5
|
+
export function getWidget(toolName) {
|
|
6
|
+
return widgets.get(toolName);
|
|
7
|
+
}
|
|
8
|
+
export function stripMcpPrefix(name) {
|
|
9
|
+
return name.replace(/^mcp__[^_]+__/, "");
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|