fireworks-ai 0.2.1 → 0.4.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.

Potentially problematic release.


This version of fireworks-ai might be problematic. Click here for more details.

Files changed (36) hide show
  1. package/README.md +93 -9
  2. package/dist/react/ApprovalButtons.d.ts +5 -0
  3. package/dist/react/ApprovalButtons.js +30 -0
  4. package/dist/react/ChatInput.js +21 -2
  5. package/dist/react/CollapsibleCard.js +1 -1
  6. package/dist/react/MessageList.js +7 -4
  7. package/dist/react/PendingPermissions.d.ts +3 -0
  8. package/dist/react/PendingPermissions.js +49 -0
  9. package/dist/react/ThinkingBlock.d.ts +5 -0
  10. package/dist/react/ThinkingBlock.js +41 -0
  11. package/dist/react/ToolApprovalCard.d.ts +7 -0
  12. package/dist/react/ToolApprovalCard.js +10 -0
  13. package/dist/react/ToolCallCard.js +26 -2
  14. package/dist/react/UserQuestionCard.d.ts +6 -0
  15. package/dist/react/UserQuestionCard.js +86 -0
  16. package/dist/react/index.d.ts +7 -1
  17. package/dist/react/index.js +7 -0
  18. package/dist/react/store.d.ts +7 -1
  19. package/dist/react/store.js +33 -1
  20. package/dist/react/use-agent.d.ts +2 -1
  21. package/dist/react/use-agent.js +24 -1
  22. package/dist/react/widgets/AskUserQuestionWidget.d.ts +1 -0
  23. package/dist/react/widgets/AskUserQuestionWidget.js +43 -0
  24. package/dist/react/widgets/WebSearchWidget.d.ts +1 -0
  25. package/dist/react/widgets/WebSearchWidget.js +85 -0
  26. package/dist/server/index.d.ts +1 -0
  27. package/dist/server/index.js +1 -0
  28. package/dist/server/permission-gate.d.ts +12 -0
  29. package/dist/server/permission-gate.js +41 -0
  30. package/dist/server/router.js +15 -0
  31. package/dist/server/session.d.ts +9 -1
  32. package/dist/server/session.js +45 -2
  33. package/dist/server/translator.d.ts +1 -0
  34. package/dist/server/translator.js +108 -23
  35. package/dist/types.d.ts +40 -1
  36. package/package.json +1 -1
package/README.md CHANGED
@@ -14,6 +14,7 @@ The Claude Agent SDK gives you a powerful agentic loop — but it's a server-sid
14
14
  - **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`.
15
15
  - **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.
16
16
  - **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.
17
+ - **Browser-side tool approval** — The SDK's `canUseTool` callback fires on the server, but your users are in the browser. `PermissionGate` bridges the gap with deferred promises, SSE events, and an HTTP POST endpoint — the agent blocks until the user clicks Allow/Deny or answers a clarifying question.
17
18
  - **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.
18
19
 
19
20
  ## Install
@@ -70,13 +71,29 @@ app.route("/", createAgentRouter({ sessions, translator }));
70
71
  serve({ fetch: app.fetch, port: 3000 });
71
72
  ```
72
73
 
73
- This gives you three endpoints:
74
+ ### Extended thinking
75
+
76
+ Enable Claude's chain-of-thought reasoning by setting `thinking` in your session config:
77
+
78
+ ```typescript
79
+ const sessions = new SessionManager(() => ({
80
+ context: {},
81
+ model: "claude-sonnet-4-5-20250929",
82
+ systemPrompt: "You are a helpful assistant.",
83
+ thinking: { type: "enabled", budgetTokens: 10000 },
84
+ }));
85
+ ```
86
+
87
+ When enabled, thinking blocks stream to the client as `thinking_delta` SSE events and render as collapsible cards in `MessageList`. Set `{ type: "disabled" }` to explicitly turn thinking off (it's off by default).
88
+
89
+ This gives you four endpoints:
74
90
 
75
91
  | Method | Path | Description |
76
92
  |--------|------|-------------|
77
93
  | `POST` | `/api/sessions` | Create a session, returns `{ sessionId }` |
78
94
  | `POST` | `/api/sessions/:id/messages` | Send `{ text }` to a session |
79
95
  | `GET` | `/api/sessions/:id/events` | SSE stream of agent events |
96
+ | `POST` | `/api/sessions/:id/permissions` | Respond to a permission request (see [Permissions](#permissions)) |
80
97
 
81
98
  ### Session context
82
99
 
@@ -117,6 +134,35 @@ const translator = new MessageTranslator<MyContext>({
117
134
 
118
135
  Each returned `{ name, value }` object is sent to the client as a `custom` SSE event.
119
136
 
137
+ ### Permissions
138
+
139
+ By default sessions run with `permissionMode: "bypassPermissions"` — all tools execute automatically. Set `permissionMode` to `"default"` (or `"acceptEdits"`) to require browser-side approval before each tool runs:
140
+
141
+ ```typescript
142
+ const sessions = new SessionManager(() => ({
143
+ context: {},
144
+ model: "claude-sonnet-4-5-20250929",
145
+ systemPrompt: "You are a helpful assistant.",
146
+ permissionMode: "default",
147
+ }));
148
+ ```
149
+
150
+ When `permissionMode` is not `"bypassPermissions"`:
151
+
152
+ 1. Every tool call blocks the SDK until the user responds
153
+ 2. `AskUserQuestion` calls surface as structured questions with options
154
+ 3. `permission_request` SSE events fire to the client
155
+ 4. The user's response is POSTed back to `/api/sessions/:id/permissions`
156
+
157
+ The `PermissionGate` on each session manages the deferred promises internally — no additional wiring needed.
158
+
159
+ | Mode | Behavior |
160
+ |------|----------|
161
+ | `"bypassPermissions"` | All tools auto-approved (default) |
162
+ | `"default"` | Every tool call requires explicit approval |
163
+ | `"acceptEdits"` | File edits auto-approved, other tools require approval |
164
+ | `"plan"` | Planning mode — SDK-defined behavior |
165
+
120
166
  ## Client
121
167
 
122
168
  `fireworks-ai/react` provides a drop-in chat UI that connects to your server.
@@ -201,6 +247,29 @@ registerWidget({
201
247
 
202
248
  Tool calls without a registered widget show a minimal status indicator.
203
249
 
250
+ #### Built-in widgets
251
+
252
+ fireworks-ai ships two built-in widgets that auto-register on import:
253
+
254
+ - **WebSearchWidget** — renders web search results as link pills with favicons
255
+ - **AskUserQuestionWidget** — displays completed question/answer pairs from `AskUserQuestion`
256
+
257
+ These register automatically when you import from `fireworks-ai/react`.
258
+
259
+ #### Input preview
260
+
261
+ Widgets can provide an `inputRenderer` to customize how tool input is displayed in the approval card (when `permissionMode` is not `"bypassPermissions"`):
262
+
263
+ ```tsx
264
+ registerWidget({
265
+ toolName: "web_search",
266
+ label: "Web Search",
267
+ richLabel: (result, input) => `Search: ${input.query}`,
268
+ inputRenderer: ({ input }) => <span>Searching: {input.query as string}</span>,
269
+ component: WebSearchWidget,
270
+ });
271
+ ```
272
+
204
273
  ### Tool call lifecycle
205
274
 
206
275
  Each tool call moves through phases, reflected in `WidgetProps.phase`:
@@ -258,11 +327,12 @@ Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme
258
327
  | Export | Description |
259
328
  |--------|-------------|
260
329
  | `SessionManager<TCtx>` | Manages agent sessions with per-session context |
261
- | `Session<TCtx>` | A single session — `id`, `context`, `pushMessage()`, `abort()` |
262
- | `SessionInit<TCtx>` | Factory return type — `model`, `systemPrompt`, `mcpServers`, etc. |
330
+ | `Session<TCtx>` | A single session — `id`, `context`, `pushMessage()`, `permissionGate`, `abort()` |
331
+ | `SessionInit<TCtx>` | Factory return type — `model`, `systemPrompt`, `permissionMode`, `mcpServers`, etc. |
263
332
  | `MessageTranslator<TCtx>` | Converts SDK messages to SSE events |
264
333
  | `TranslatorConfig<TCtx>` | Translator options — `onToolResult` hook |
265
- | `createAgentRouter<TCtx>(config)` | Returns a Hono app with session + SSE routes |
334
+ | `createAgentRouter<TCtx>(config)` | Returns a Hono app with session, SSE, and permission routes |
335
+ | `PermissionGate` | Per-session deferred-promise map for tool approval and user questions |
266
336
  | `PushChannel<T>` | Async iterable queue for feeding messages to the SDK |
267
337
  | `sseEncode(event)` | Formats an `SSEEvent` as an SSE string |
268
338
  | `streamSession(session, translator)` | Async generator yielding `SSEEvent`s |
@@ -272,14 +342,18 @@ Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme
272
342
  | Export | Description |
273
343
  |--------|-------------|
274
344
  | `AgentProvider` | Context provider — wraps store + SSE connection |
275
- | `useAgentContext()` | Returns `{ sessionId, sendMessage, store }` |
345
+ | `useAgentContext()` | Returns `{ sessionId, sendMessage, respondToPermission, store }` |
276
346
  | `useChatStore(selector)` | Zustand selector hook into chat state |
277
347
  | `createChatStore()` | Creates a vanilla Zustand store (for advanced use) |
278
348
  | `useAgent(store, config?)` | SSE connection hook (used internally by `AgentProvider`) |
279
- | `MessageList` | Auto-scrolling message list with thinking indicator |
349
+ | `MessageList` | Auto-scrolling message list with pending permissions and thinking indicator |
280
350
  | `TextMessage` | Markdown-rendered message bubble |
281
351
  | `ChatInput` | Textarea + send button |
282
- | `ToolCallCard` | Lifecycle-aware tool call display |
352
+ | `ToolCallCard` | Lifecycle-aware tool call display with inline approval |
353
+ | `PendingPermissions` | Renders pending tool approval and user question cards |
354
+ | `ToolApprovalCard` | Tool approval card with Allow/Deny buttons |
355
+ | `UserQuestionCard` | Structured question card with option selection |
356
+ | `ThinkingBlock` | Collapsible card displaying extended thinking text |
283
357
  | `ThinkingIndicator` | Animated dots shown while agent is generating |
284
358
  | `CollapsibleCard` | Expandable card wrapper |
285
359
  | `StatusDot` | Phase-colored status indicator |
@@ -293,14 +367,21 @@ Drop the `fireworks-ai/theme.css` import and add `@source` — your shadcn theme
293
367
  | Type | Description |
294
368
  |------|-------------|
295
369
  | `SSEEvent` | `{ event: string, data: string }` |
296
- | `ChatMessage` | `{ id, role, content, toolCalls? }` |
370
+ | `ChatMessage` | `{ id, role, content, thinking?, toolCalls? }` |
297
371
  | `ToolCallInfo` | `{ id, name, input, partialInput?, result?, error?, status }` |
298
372
  | `ToolCallPhase` | `"pending" \| "streaming_input" \| "running" \| "complete" \| "error"` |
299
373
  | `WidgetProps<TResult>` | Props passed to widget components |
300
- | `WidgetRegistration<TResult>` | Widget registration descriptor |
374
+ | `WidgetRegistration<TResult>` | Widget registration `toolName`, `label`, `richLabel?`, `inputRenderer?`, `component` |
301
375
  | `ChatStore` | `StoreApi<ChatStoreShape>` — vanilla Zustand store |
302
376
  | `ChatStoreShape` | Full state + actions interface |
303
377
  | `CustomEvent<T>` | `{ name: string, value: T }` — structured app-level event |
378
+ | `PermissionRequest` | `ToolApprovalRequest \| UserQuestionRequest` — pending permission |
379
+ | `PermissionResponse` | `ToolApprovalResponse \| UserQuestionResponse` — user's answer |
380
+ | `ToolApprovalRequest` | `{ kind, requestId, toolName, input, description? }` |
381
+ | `ToolApprovalResponse` | `{ kind, requestId, behavior: "allow" \| "deny", message? }` |
382
+ | `UserQuestion` | `{ question, header?, options?, multiSelect? }` |
383
+ | `UserQuestionRequest` | `{ kind, requestId, questions: UserQuestion[] }` |
384
+ | `UserQuestionResponse` | `{ kind, requestId, answers: Record<string, string> }` |
304
385
 
305
386
  ## SSE Events
306
387
 
@@ -309,12 +390,15 @@ Events emitted by the server, handled automatically by `useAgent`:
309
390
  | Event | Payload | Description |
310
391
  |-------|---------|-------------|
311
392
  | `message_start` | `{}` | Agent began generating a response |
393
+ | `thinking_start` | `{}` | Extended thinking block began |
394
+ | `thinking_delta` | `{ text }` | Streaming thinking text chunk |
312
395
  | `text_delta` | `{ text }` | Streaming text chunk |
313
396
  | `tool_start` | `{ id, name }` | Agent began calling a tool |
314
397
  | `tool_input_delta` | `{ id, partialJson }` | Streaming tool input JSON |
315
398
  | `tool_call` | `{ id, name, input }` | Tool input finalized |
316
399
  | `tool_result` | `{ toolUseId, result }` | Tool execution result |
317
400
  | `tool_progress` | `{ toolName, elapsed }` | Long-running tool heartbeat |
401
+ | `permission_request` | `PermissionRequest` | Tool approval or user question awaiting response |
318
402
  | `turn_complete` | `{ numTurns, cost }` | Agent turn finished |
319
403
  | `custom` | `{ name, value }` | App-specific event from `onToolResult` |
320
404
  | `session_error` | `{ subtype }` | Session ended with error |
@@ -0,0 +1,5 @@
1
+ export declare function ApprovalButtons({ onApprove, onDeny, className, }: {
2
+ onApprove: () => void;
3
+ onDeny: () => void;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { cn } from "./cn.js";
4
+ export function ApprovalButtons({ onApprove, onDeny, className, }) {
5
+ const allowRef = useRef(null);
6
+ // Auto-focus the Allow button when mounted
7
+ useEffect(() => {
8
+ allowRef.current?.focus();
9
+ }, []);
10
+ // Keyboard shortcuts: 1 = Allow, 2 = Deny (skip when typing in inputs)
11
+ useEffect(() => {
12
+ function handleKeyDown(e) {
13
+ const tag = e.target?.tagName;
14
+ if (tag === "INPUT" || tag === "TEXTAREA" || e.target?.isContentEditable) {
15
+ return;
16
+ }
17
+ if (e.key === "1") {
18
+ e.preventDefault();
19
+ onApprove();
20
+ }
21
+ else if (e.key === "2") {
22
+ e.preventDefault();
23
+ onDeny();
24
+ }
25
+ }
26
+ document.addEventListener("keydown", handleKeyDown);
27
+ return () => document.removeEventListener("keydown", handleKeyDown);
28
+ }, [onApprove, onDeny]);
29
+ return (_jsxs("div", { className: cn("mt-2 flex items-center gap-2", className), children: [_jsxs("button", { ref: allowRef, type: "button", onClick: onApprove, className: "rounded bg-primary px-3 py-1 text-primary-foreground hover:bg-primary/90 transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50", children: ["Allow", _jsx("kbd", { className: "ml-1.5 text-[9px] opacity-60", children: "1" })] }), _jsxs("button", { type: "button", onClick: onDeny, className: "rounded border border-border px-3 py-1 text-muted-foreground hover:bg-accent transition-colors focus:outline-none focus:ring-2 focus:ring-border", children: ["Deny", _jsx("kbd", { className: "ml-1.5 text-[9px] opacity-60", children: "2" })] })] }));
30
+ }
@@ -1,15 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useCallback, useRef, useState } from "react";
3
3
  import { cn } from "./cn.js";
4
4
  export function ChatInput({ onSend, placeholder = "Type a message...", disabled, className, }) {
5
5
  const [text, setText] = useState("");
6
+ const textareaRef = useRef(null);
6
7
  const isDisabled = disabled ?? false;
8
+ const MAX_H = 160; // matches max-h-40 (10rem)
9
+ const autoResize = useCallback(() => {
10
+ const el = textareaRef.current;
11
+ if (!el)
12
+ return;
13
+ el.style.height = "auto";
14
+ el.style.height = `${el.scrollHeight}px`;
15
+ el.style.overflowY = el.scrollHeight > MAX_H ? "auto" : "hidden";
16
+ }, []);
7
17
  function handleSend() {
8
18
  const trimmed = text.trim();
9
19
  if (!trimmed || isDisabled)
10
20
  return;
11
21
  onSend(trimmed);
12
22
  setText("");
23
+ requestAnimationFrame(() => {
24
+ const el = textareaRef.current;
25
+ if (el) {
26
+ el.style.height = "auto";
27
+ }
28
+ });
13
29
  }
14
30
  function handleKeyDown(e) {
15
31
  if (e.key === "Enter" && !e.shiftKey) {
@@ -17,5 +33,8 @@ export function ChatInput({ onSend, placeholder = "Type a message...", disabled,
17
33
  handleSend();
18
34
  }
19
35
  }
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" })] }) })] }));
36
+ return (_jsxs("div", { className: cn("flex items-end gap-2 p-3", className), children: [_jsx("textarea", { ref: textareaRef, value: text, onChange: (e) => {
37
+ setText(e.target.value);
38
+ autoResize();
39
+ }, 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: _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
40
  }
@@ -4,5 +4,5 @@ import { cn } from "./cn.js";
4
4
  import { StatusDot } from "./StatusDot.js";
5
5
  export function CollapsibleCard({ label, status, defaultOpen = true, children, className, }) {
6
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 })] }));
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", { className: "truncate min-w-0", children: label })] }), open && _jsx("div", { className: "px-2.5 pb-2", children: children })] }));
8
8
  }
@@ -2,18 +2,21 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef } from "react";
3
3
  import { useChatStore } from "./AgentProvider.js";
4
4
  import { cn } from "./cn.js";
5
+ import { PendingPermissions } from "./PendingPermissions.js";
5
6
  import { TextMessage } from "./TextMessage.js";
7
+ import { ThinkingBlock } from "./ThinkingBlock.js";
6
8
  import { ThinkingIndicator } from "./ThinkingIndicator.js";
7
9
  import { ToolCallCard } from "./ToolCallCard.js";
8
10
  export function MessageList({ className }) {
9
11
  const messages = useChatStore((s) => s.messages);
10
12
  const streamingText = useChatStore((s) => s.streamingText);
13
+ const streamingThinking = useChatStore((s) => s.streamingThinking);
11
14
  const isThinking = useChatStore((s) => s.isThinking);
12
15
  const bottomRef = useRef(null);
13
16
  // biome-ignore lint/correctness/useExhaustiveDependencies: scroll on content changes
14
17
  useEffect(() => {
15
18
  bottomRef.current?.scrollIntoView({ behavior: "smooth" });
16
- }, [messages, streamingText, isThinking]);
19
+ }, [messages, streamingText, streamingThinking, isThinking]);
17
20
  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
21
  if (msg.toolCalls?.length) {
19
22
  return (_jsx("div", { className: "flex flex-col gap-1.5", children: msg.toolCalls.map((tc) => (_jsx(ToolCallCard, { toolCall: tc }, tc.id))) }, msg.id));
@@ -21,9 +24,9 @@ export function MessageList({ className }) {
21
24
  if (msg.role === "system") {
22
25
  return (_jsx("div", { className: "text-center text-xs text-destructive", children: msg.content }, msg.id));
23
26
  }
24
- if (msg.content) {
25
- return (_jsx(TextMessage, { role: msg.role, content: msg.content }, msg.id));
27
+ if (msg.content || msg.thinking) {
28
+ return (_jsxs("div", { className: "flex flex-col gap-1.5", children: [msg.thinking && _jsx(ThinkingBlock, { thinking: msg.thinking }), msg.content && (_jsx(TextMessage, { role: msg.role, content: msg.content }))] }, msg.id));
26
29
  }
27
30
  return null;
28
- }), streamingText && _jsx(TextMessage, { role: "assistant", content: streamingText }), isThinking && _jsx(ThinkingIndicator, {}), _jsx("div", { ref: bottomRef })] }) }));
31
+ }), streamingThinking && _jsx(ThinkingBlock, { thinking: streamingThinking, streaming: true }), streamingText && _jsx(TextMessage, { role: "assistant", content: streamingText }), _jsx(PendingPermissions, {}), isThinking && _jsx(ThinkingIndicator, {}), _jsx("div", { ref: bottomRef })] }) }));
29
32
  }
@@ -0,0 +1,3 @@
1
+ export declare function PendingPermissions({ className }: {
2
+ className?: string;
3
+ }): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,49 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useAgentContext, useChatStore } from "./AgentProvider.js";
3
+ import { cn } from "./cn.js";
4
+ import { ToolApprovalCard } from "./ToolApprovalCard.js";
5
+ import { UserQuestionCard } from "./UserQuestionCard.js";
6
+ export function PendingPermissions({ className }) {
7
+ const pending = useChatStore((s) => s.pendingPermissions);
8
+ const messages = useChatStore((s) => s.messages);
9
+ const { respondToPermission } = useAgentContext();
10
+ // Tool approvals that match a non-terminal tool call are rendered inline
11
+ // by ToolCallCard — skip them here to avoid duplicate UI.
12
+ const unclaimed = pending.filter((request) => {
13
+ if (request.kind !== "tool_approval")
14
+ return true;
15
+ for (const msg of messages) {
16
+ if (msg.toolCalls) {
17
+ for (const tc of msg.toolCalls) {
18
+ if (tc.name === request.toolName &&
19
+ tc.status !== "complete" &&
20
+ tc.status !== "error") {
21
+ return false;
22
+ }
23
+ }
24
+ }
25
+ }
26
+ return true;
27
+ });
28
+ if (!unclaimed.length)
29
+ return null;
30
+ return (_jsx("div", { className: cn("flex flex-col gap-2", className), children: unclaimed.map((request) => {
31
+ if (request.kind === "tool_approval") {
32
+ return (_jsx(ToolApprovalCard, { request: request, onApprove: () => respondToPermission({
33
+ kind: "tool_approval",
34
+ requestId: request.requestId,
35
+ behavior: "allow",
36
+ }), onDeny: (message) => respondToPermission({
37
+ kind: "tool_approval",
38
+ requestId: request.requestId,
39
+ behavior: "deny",
40
+ message: message ?? "Denied by user",
41
+ }) }, request.requestId));
42
+ }
43
+ return (_jsx(UserQuestionCard, { request: request, onSubmit: (answers) => respondToPermission({
44
+ kind: "user_question",
45
+ requestId: request.requestId,
46
+ answers,
47
+ }) }, request.requestId));
48
+ }) }));
49
+ }
@@ -0,0 +1,5 @@
1
+ export declare function ThinkingBlock({ thinking, streaming, className, }: {
2
+ thinking: string;
3
+ streaming?: boolean;
4
+ className?: string;
5
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,41 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useState } from "react";
3
+ import Markdown from "react-markdown";
4
+ import { cn } from "./cn.js";
5
+ const STORAGE_KEY = "fireworks-thinking-open";
6
+ function readPref() {
7
+ try {
8
+ return localStorage.getItem(STORAGE_KEY) === "true";
9
+ }
10
+ catch {
11
+ return false;
12
+ }
13
+ }
14
+ function writePref(open) {
15
+ try {
16
+ localStorage.setItem(STORAGE_KEY, String(open));
17
+ }
18
+ catch {
19
+ /* SSR / restricted env */
20
+ }
21
+ }
22
+ export function ThinkingBlock({ thinking, streaming = false, className, }) {
23
+ const [open, setOpen] = useState(readPref);
24
+ const toggle = useCallback(() => {
25
+ setOpen((prev) => {
26
+ const next = !prev;
27
+ writePref(next);
28
+ return next;
29
+ });
30
+ }, []);
31
+ return (_jsx("div", { className: cn("flex justify-start", className), children: _jsxs("div", { className: "rounded-md border border-border/40 bg-accent/20 overflow-hidden", children: [_jsxs("button", { type: "button", onClick: toggle, className: "flex w-full items-center gap-2 px-2.5 py-1.5 text-xs text-muted-foreground/60 hover:bg-accent/30 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" }) }), streaming && !open && (_jsx("span", { className: "h-2 w-2 shrink-0 rounded-full bg-yellow-500 animate-pulse" })), _jsx("span", { className: "truncate min-w-0", children: "Thinking" })] }), open && (_jsx("div", { className: "px-2.5 pt-1 pb-2", children: _jsx(ThinkingContent, { children: thinking }) }))] }) }));
32
+ }
33
+ function ThinkingContent({ children }) {
34
+ return (_jsx("div", { className: "text-xs text-muted-foreground/70 italic [&>*:first-child]:mt-0 [&>*:last-child]:mb-0", children: _jsx(Markdown, { components: {
35
+ p: ({ children }) => _jsx("p", { className: "mb-2 last:mb-0", children: children }),
36
+ ul: ({ children }) => _jsx("ul", { className: "mb-2 ml-4 list-disc last:mb-0", children: children }),
37
+ ol: ({ children }) => _jsx("ol", { className: "mb-2 ml-4 list-decimal last:mb-0", children: children }),
38
+ li: ({ children }) => _jsx("li", { className: "mb-0.5", children: children }),
39
+ strong: ({ children }) => _jsx("strong", { className: "font-semibold", children: children }),
40
+ }, children: children }) }));
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { ToolApprovalRequest } from "../types.js";
2
+ export declare function ToolApprovalCard({ request, onApprove, onDeny, className, }: {
3
+ request: ToolApprovalRequest;
4
+ onApprove: () => void;
5
+ onDeny: (message?: string) => void;
6
+ className?: string;
7
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ApprovalButtons } from "./ApprovalButtons.js";
3
+ import { cn } from "./cn.js";
4
+ import { getWidget, stripMcpPrefix } from "./registry.js";
5
+ export function ToolApprovalCard({ request, onApprove, onDeny, className, }) {
6
+ const reg = getWidget(stripMcpPrefix(request.toolName));
7
+ const label = reg?.label ?? request.toolName;
8
+ const InputRenderer = reg?.inputRenderer;
9
+ return (_jsxs("div", { className: cn("rounded-md border border-yellow-500/40 bg-yellow-500/5 px-3 py-2.5 text-xs", className), children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx("span", { className: "inline-block h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), _jsx("span", { className: "font-medium", children: label }), request.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", request.description] }))] }), InputRenderer ? (_jsx(InputRenderer, { input: request.input })) : (Object.keys(request.input).length > 0 && (_jsx("pre", { className: "mt-1.5 max-h-32 overflow-auto rounded bg-background/60 px-2 py-1 font-mono text-[10px] text-muted-foreground", children: JSON.stringify(request.input, null, 2) }))), _jsx(ApprovalButtons, { onApprove: onApprove, onDeny: () => onDeny() })] }));
10
+ }
@@ -1,4 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useAgentContext, useChatStore } from "./AgentProvider.js";
3
+ import { ApprovalButtons } from "./ApprovalButtons.js";
2
4
  import { CollapsibleCard } from "./CollapsibleCard.js";
3
5
  import { cn } from "./cn.js";
4
6
  import { getWidget, stripMcpPrefix } from "./registry.js";
@@ -7,15 +9,21 @@ export function ToolCallCard({ toolCall, className, }) {
7
9
  const short = stripMcpPrefix(toolCall.name);
8
10
  const reg = getWidget(short);
9
11
  const label = reg?.label ?? short;
12
+ const pendingPermissions = useChatStore((s) => s.pendingPermissions);
13
+ const { respondToPermission, store } = useAgentContext();
14
+ const isNonTerminal = toolCall.status !== "complete" && toolCall.status !== "error";
15
+ const matchingApproval = isNonTerminal
16
+ ? pendingPermissions.find((p) => p.kind === "tool_approval" && p.toolName === toolCall.name)
17
+ : undefined;
10
18
  if (toolCall.status === "complete" && toolCall.result && reg) {
11
19
  let parsed;
12
20
  try {
13
21
  parsed = JSON.parse(toolCall.result);
14
22
  }
15
23
  catch {
16
- parsed = undefined;
24
+ parsed = toolCall.result;
17
25
  }
18
- const displayLabel = parsed && reg.richLabel ? (reg.richLabel(parsed) ?? label) : label;
26
+ const displayLabel = reg.richLabel ? (reg.richLabel(parsed, toolCall.input) ?? label) : label;
19
27
  const widgetProps = {
20
28
  phase: toolCall.status,
21
29
  toolUseId: toolCall.id,
@@ -29,5 +37,21 @@ export function ToolCallCard({ toolCall, className, }) {
29
37
  if (toolCall.status === "error") {
30
38
  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
39
  }
40
+ if (matchingApproval) {
41
+ const InputRenderer = reg?.inputRenderer;
42
+ return (_jsxs("div", { className: cn("rounded-md border border-yellow-500/40 bg-yellow-500/5 px-3 py-2.5 text-xs", className), children: [_jsxs("div", { className: "flex items-center gap-2 text-muted-foreground", children: [_jsx("span", { className: "inline-block h-2 w-2 rounded-full bg-yellow-500 animate-pulse" }), _jsx("span", { className: "font-medium", children: label }), matchingApproval.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", matchingApproval.description] }))] }), InputRenderer ? (_jsx(InputRenderer, { input: matchingApproval.input })) : (Object.keys(matchingApproval.input).length > 0 && (_jsx("pre", { className: "mt-1.5 max-h-32 overflow-auto rounded bg-background/60 px-2 py-1 font-mono text-[10px] text-muted-foreground", children: JSON.stringify(matchingApproval.input, null, 2) }))), _jsx(ApprovalButtons, { onApprove: () => respondToPermission({
43
+ kind: "tool_approval",
44
+ requestId: matchingApproval.requestId,
45
+ behavior: "allow",
46
+ }), onDeny: () => {
47
+ store.getState().errorToolCall(toolCall.id, "Not approved");
48
+ respondToPermission({
49
+ kind: "tool_approval",
50
+ requestId: matchingApproval.requestId,
51
+ behavior: "deny",
52
+ message: "Denied by user",
53
+ });
54
+ } })] }));
55
+ }
32
56
  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
57
  }
@@ -0,0 +1,6 @@
1
+ import type { UserQuestionRequest } from "../types.js";
2
+ export declare function UserQuestionCard({ request, onSubmit, className, }: {
3
+ request: UserQuestionRequest;
4
+ onSubmit: (answers: Record<string, string>) => void;
5
+ className?: string;
6
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from "react";
3
+ import { cn } from "./cn.js";
4
+ const OTHER_LABEL = "__other__";
5
+ export function UserQuestionCard({ request, onSubmit, className, }) {
6
+ const [answers, setAnswers] = useState({});
7
+ const [otherText, setOtherText] = useState({});
8
+ const otherInputRefs = useRef({});
9
+ const allAnswered = request.questions.every((q) => {
10
+ const val = answers[q.question];
11
+ if (!val)
12
+ return false;
13
+ if (val === OTHER_LABEL)
14
+ return !!otherText[q.question]?.trim();
15
+ return true;
16
+ });
17
+ function resolvedAnswers() {
18
+ const resolved = {};
19
+ for (const q of request.questions) {
20
+ const val = answers[q.question] ?? "";
21
+ resolved[q.question] = val === OTHER_LABEL ? (otherText[q.question] ?? "") : val;
22
+ }
23
+ return resolved;
24
+ }
25
+ function selectOption(question, label, multiSelect) {
26
+ setAnswers((prev) => {
27
+ if (!multiSelect)
28
+ return { ...prev, [question]: label };
29
+ const current = prev[question] ?? "";
30
+ const labels = current ? current.split(", ") : [];
31
+ const next = labels.includes(label)
32
+ ? labels.filter((l) => l !== label)
33
+ : [...labels, label];
34
+ return { ...prev, [question]: next.join(", ") };
35
+ });
36
+ if (label === OTHER_LABEL) {
37
+ setTimeout(() => otherInputRefs.current[question]?.focus(), 0);
38
+ }
39
+ }
40
+ // Keyboard shortcuts: number keys select options
41
+ useEffect(() => {
42
+ function handleKeyDown(e) {
43
+ const tag = e.target?.tagName;
44
+ if (tag === "INPUT" || tag === "TEXTAREA" || e.target?.isContentEditable) {
45
+ return;
46
+ }
47
+ // Only handle single-question cards for keyboard shortcuts
48
+ if (request.questions.length !== 1)
49
+ return;
50
+ const q = request.questions[0];
51
+ if (!q.options?.length)
52
+ return;
53
+ const totalOptions = q.options.length + 1; // +1 for "Other"
54
+ const num = parseInt(e.key, 10);
55
+ if (num >= 1 && num <= totalOptions) {
56
+ e.preventDefault();
57
+ if (num <= q.options.length) {
58
+ selectOption(q.question, q.options[num - 1].label, q.multiSelect);
59
+ }
60
+ else {
61
+ selectOption(q.question, OTHER_LABEL, q.multiSelect);
62
+ }
63
+ }
64
+ }
65
+ document.addEventListener("keydown", handleKeyDown);
66
+ return () => document.removeEventListener("keydown", handleKeyDown);
67
+ });
68
+ return (_jsxs("div", { className: cn("rounded-md border border-blue-500/40 bg-blue-500/5 px-3 py-2.5 text-xs", className), children: [request.questions.map((q) => {
69
+ const hasOptions = q.options && q.options.length > 0;
70
+ const isSingleQuestion = request.questions.length === 1;
71
+ return (_jsxs("div", { className: "mb-3 last:mb-0", children: [q.header && (_jsx("span", { className: "text-[10px] uppercase tracking-wider text-muted-foreground/70", children: q.header })), _jsx("p", { className: "mt-0.5 text-foreground", children: q.question }), hasOptions && (_jsxs("div", { className: "mt-1.5 flex flex-col gap-1", children: [q.options.map((opt, idx) => {
72
+ const selected = (answers[q.question] ?? "")
73
+ .split(", ")
74
+ .includes(opt.label);
75
+ return (_jsxs("button", { type: "button", onClick: () => selectOption(q.question, opt.label, q.multiSelect), className: cn("flex items-start gap-2 rounded border px-2 py-1.5 text-left transition-colors", selected
76
+ ? "border-blue-500 bg-blue-500/15 text-foreground ring-1 ring-blue-500/30"
77
+ : "border-border text-muted-foreground hover:bg-accent"), children: [_jsx("span", { className: "mt-px shrink-0", children: q.multiSelect ? (selected ? "☑" : "☐") : selected ? "●" : "○" }), _jsxs("span", { className: "flex-1", children: [_jsx("span", { className: "font-medium", children: opt.label }), opt.description && (_jsxs("span", { className: "ml-1 opacity-70", children: ["\u2014 ", opt.description] }))] }), isSingleQuestion && (_jsx("kbd", { className: "ml-auto shrink-0 text-[9px] opacity-40", children: idx + 1 }))] }, opt.label));
78
+ }), (() => {
79
+ const isOther = answers[q.question] === OTHER_LABEL;
80
+ const otherIdx = q.options.length + 1;
81
+ return (_jsxs("div", { children: [_jsxs("button", { type: "button", onClick: () => selectOption(q.question, OTHER_LABEL, q.multiSelect), className: cn("flex w-full items-start gap-2 rounded border px-2 py-1.5 text-left transition-colors", isOther
82
+ ? "border-blue-500 bg-blue-500/15 text-foreground ring-1 ring-blue-500/30"
83
+ : "border-border text-muted-foreground hover:bg-accent"), children: [_jsx("span", { className: "mt-px shrink-0", children: q.multiSelect ? (isOther ? "☑" : "☐") : isOther ? "●" : "○" }), _jsx("span", { className: "font-medium", children: "Other" }), isSingleQuestion && (_jsx("kbd", { className: "ml-auto shrink-0 text-[9px] opacity-40", children: otherIdx }))] }), isOther && (_jsx("input", { ref: (el) => { otherInputRefs.current[q.question] = el; }, type: "text", className: "mt-1 w-full rounded border border-border bg-background px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-blue-500/50", placeholder: "Type your answer\u2026", value: otherText[q.question] ?? "", onChange: (e) => setOtherText((prev) => ({ ...prev, [q.question]: e.target.value })) }))] }));
84
+ })()] })), !hasOptions && (_jsx("input", { type: "text", className: "mt-1.5 w-full rounded border border-border bg-background px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-blue-500/50", placeholder: "Type your answer\u2026", value: answers[q.question] ?? "", onChange: (e) => setAnswers((prev) => ({ ...prev, [q.question]: e.target.value })) }))] }, q.question));
85
+ }), _jsx("button", { type: "button", disabled: !allAnswered, onClick: () => onSubmit(resolvedAnswers()), className: cn("mt-2 rounded bg-primary px-3 py-1 text-primary-foreground transition-colors focus:outline-none focus:ring-2 focus:ring-primary/50", allAnswered ? "hover:bg-primary/90" : "opacity-50 cursor-not-allowed"), children: "Submit" })] }));
86
+ }
@@ -1,13 +1,19 @@
1
- export type { ChatMessage, CustomEvent, SSEEvent, ToolCallInfo, ToolCallPhase, WidgetProps, WidgetRegistration, } from "../types.js";
1
+ import "./widgets/AskUserQuestionWidget.js";
2
+ import "./widgets/WebSearchWidget.js";
3
+ export type { ChatMessage, CustomEvent, PermissionRequest, PermissionResponse, SSEEvent, ToolApprovalRequest, ToolApprovalResponse, ToolCallInfo, ToolCallPhase, UserQuestion, UserQuestionOption, UserQuestionRequest, UserQuestionResponse, WidgetProps, WidgetRegistration, } from "../types.js";
2
4
  export { AgentProvider, useAgentContext, useChatStore } from "./AgentProvider.js";
3
5
  export { ChatInput } from "./ChatInput.js";
4
6
  export { CollapsibleCard } from "./CollapsibleCard.js";
5
7
  export { cn } from "./cn.js";
6
8
  export { MessageList } from "./MessageList.js";
9
+ export { PendingPermissions } from "./PendingPermissions.js";
7
10
  export { getWidget, registerWidget, stripMcpPrefix } from "./registry.js";
8
11
  export { StatusDot } from "./StatusDot.js";
9
12
  export { type ChatStore, type ChatStoreShape, createChatStore } from "./store.js";
10
13
  export { TextMessage } from "./TextMessage.js";
14
+ export { ThinkingBlock } from "./ThinkingBlock.js";
11
15
  export { ThinkingIndicator } from "./ThinkingIndicator.js";
16
+ export { ToolApprovalCard } from "./ToolApprovalCard.js";
12
17
  export { ToolCallCard } from "./ToolCallCard.js";
18
+ export { UserQuestionCard } from "./UserQuestionCard.js";
13
19
  export { type UseAgentConfig, type UseAgentReturn, useAgent } from "./use-agent.js";