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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +320 -0
  3. package/dist/react/AgentProvider.d.ts +15 -0
  4. package/dist/react/AgentProvider.js +30 -0
  5. package/dist/react/ChatInput.d.ts +6 -0
  6. package/dist/react/ChatInput.js +21 -0
  7. package/dist/react/CollapsibleCard.d.ts +9 -0
  8. package/dist/react/CollapsibleCard.js +8 -0
  9. package/dist/react/MessageList.d.ts +3 -0
  10. package/dist/react/MessageList.js +29 -0
  11. package/dist/react/StatusDot.d.ts +5 -0
  12. package/dist/react/StatusDot.js +12 -0
  13. package/dist/react/TextMessage.d.ts +5 -0
  14. package/dist/react/TextMessage.js +13 -0
  15. package/dist/react/ThinkingIndicator.d.ts +3 -0
  16. package/dist/react/ThinkingIndicator.js +5 -0
  17. package/dist/react/ToolCallCard.d.ts +5 -0
  18. package/dist/react/ToolCallCard.js +33 -0
  19. package/dist/react/cn.d.ts +2 -0
  20. package/dist/react/cn.js +5 -0
  21. package/dist/react/index.d.ts +13 -0
  22. package/dist/react/index.js +12 -0
  23. package/dist/react/registry.d.ts +4 -0
  24. package/dist/react/registry.js +10 -0
  25. package/dist/react/registry.test.d.ts +1 -0
  26. package/dist/react/registry.test.js +26 -0
  27. package/dist/react/store.d.ts +28 -0
  28. package/dist/react/store.js +109 -0
  29. package/dist/react/store.test.d.ts +1 -0
  30. package/dist/react/store.test.js +113 -0
  31. package/dist/react/use-agent.d.ts +11 -0
  32. package/dist/react/use-agent.js +96 -0
  33. package/dist/server/index.d.ts +5 -0
  34. package/dist/server/index.js +4 -0
  35. package/dist/server/push-channel.d.ts +8 -0
  36. package/dist/server/push-channel.js +40 -0
  37. package/dist/server/push-channel.test.d.ts +1 -0
  38. package/dist/server/push-channel.test.js +57 -0
  39. package/dist/server/router.d.ts +8 -0
  40. package/dist/server/router.js +52 -0
  41. package/dist/server/session.d.ts +32 -0
  42. package/dist/server/session.js +73 -0
  43. package/dist/server/translator.d.ts +14 -0
  44. package/dist/server/translator.js +151 -0
  45. package/dist/server/translator.test.d.ts +1 -0
  46. package/dist/server/translator.test.js +156 -0
  47. package/dist/types.d.ts +39 -0
  48. package/dist/types.js +1 -0
  49. package/package.json +69 -0
  50. 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,6 @@
1
+ export declare function ChatInput({ onSend, placeholder, disabled, className, }: {
2
+ onSend: (text: string) => void;
3
+ placeholder?: string;
4
+ disabled?: boolean;
5
+ className?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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,3 @@
1
+ export declare function MessageList({ className }: {
2
+ className?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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,5 @@
1
+ import type { ToolCallPhase } from "../types.js";
2
+ export declare function StatusDot({ status, className }: {
3
+ status: ToolCallPhase;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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,5 @@
1
+ export declare function TextMessage({ role, content, className, }: {
2
+ role: "user" | "assistant";
3
+ content: string;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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,3 @@
1
+ export declare function ThinkingIndicator({ className }: {
2
+ className?: string;
3
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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,5 @@
1
+ import type { ToolCallInfo } from "../types.js";
2
+ export declare function ToolCallCard({ toolCall, className, }: {
3
+ toolCall: ToolCallInfo;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ import { type ClassValue } from "clsx";
2
+ export declare function cn(...inputs: ClassValue[]): string;
@@ -0,0 +1,5 @@
1
+ import { clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+ export function cn(...inputs) {
4
+ return twMerge(clsx(inputs));
5
+ }
@@ -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 {};