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.
- package/README.md +93 -9
- package/dist/react/ApprovalButtons.d.ts +5 -0
- package/dist/react/ApprovalButtons.js +30 -0
- package/dist/react/ChatInput.js +21 -2
- package/dist/react/CollapsibleCard.js +1 -1
- package/dist/react/MessageList.js +7 -4
- package/dist/react/PendingPermissions.d.ts +3 -0
- package/dist/react/PendingPermissions.js +49 -0
- package/dist/react/ThinkingBlock.d.ts +5 -0
- package/dist/react/ThinkingBlock.js +41 -0
- package/dist/react/ToolApprovalCard.d.ts +7 -0
- package/dist/react/ToolApprovalCard.js +10 -0
- package/dist/react/ToolCallCard.js +26 -2
- package/dist/react/UserQuestionCard.d.ts +6 -0
- package/dist/react/UserQuestionCard.js +86 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +7 -0
- package/dist/react/store.d.ts +7 -1
- package/dist/react/store.js +33 -1
- package/dist/react/use-agent.d.ts +2 -1
- package/dist/react/use-agent.js +24 -1
- package/dist/react/widgets/AskUserQuestionWidget.d.ts +1 -0
- package/dist/react/widgets/AskUserQuestionWidget.js +43 -0
- package/dist/react/widgets/WebSearchWidget.d.ts +1 -0
- package/dist/react/widgets/WebSearchWidget.js +85 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/permission-gate.d.ts +12 -0
- package/dist/server/permission-gate.js +41 -0
- package/dist/server/router.js +15 -0
- package/dist/server/session.d.ts +9 -1
- package/dist/server/session.js +45 -2
- package/dist/server/translator.d.ts +1 -0
- package/dist/server/translator.js +108 -23
- package/dist/types.d.ts +40 -1
- 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
|
-
|
|
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
|
|
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
|
|
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,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
|
+
}
|
package/dist/react/ChatInput.js
CHANGED
|
@@ -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", {
|
|
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,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,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 =
|
|
24
|
+
parsed = toolCall.result;
|
|
17
25
|
}
|
|
18
|
-
const displayLabel =
|
|
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
|
+
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
|
|
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";
|