@cntyclub/agent-react 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatMessages, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
1
+ import { Button, cn, ChatMessage, ChatMessageBubble, Markdown, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Tooltip, TooltipTrigger, TooltipContent, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, Fab, DataTablePaged } from '@cntyclub/ui-react';
2
2
  import { FileTextIcon, XIcon, PaperclipIcon, ArrowUpIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, CheckIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, Trash2Icon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
- import * as React6 from 'react';
3
+ import * as React7 from 'react';
4
4
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
5
 
6
6
  // src/api-client.ts
@@ -75,6 +75,98 @@ var AgentApiClient = class {
75
75
  method: "POST"
76
76
  });
77
77
  }
78
+ // ------------------------------------------------------------- streaming
79
+ /** POST a chat turn and yield parsed SSE events as they stream in. */
80
+ async streamMessage(message, conversationId, files, onEvent, signal) {
81
+ let body;
82
+ const headers = await this.streamHeaders();
83
+ if (files && files.length > 0) {
84
+ const form = new FormData();
85
+ form.append("client_id", this.config.clientId);
86
+ form.append("message", message);
87
+ if (conversationId) form.append("conversation_id", conversationId);
88
+ for (const file of files) form.append("files", file, file.name);
89
+ body = form;
90
+ } else {
91
+ headers["Content-Type"] = "application/json";
92
+ body = JSON.stringify({
93
+ client_id: this.config.clientId,
94
+ conversation_id: conversationId ?? null,
95
+ message
96
+ });
97
+ }
98
+ await this.consumeStream("/agent-mode/chat/stream/", { body, headers, signal }, onEvent);
99
+ }
100
+ /** POST an approval decision and yield the continued reply as SSE events. */
101
+ async streamResolveApproval(callId, approve, onEvent, signal) {
102
+ const headers = await this.streamHeaders();
103
+ headers["Content-Type"] = "application/json";
104
+ await this.consumeStream(
105
+ `/agent-mode/approvals/${callId}/resolve/stream/`,
106
+ {
107
+ body: JSON.stringify({ approve, client_id: this.config.clientId }),
108
+ headers,
109
+ signal
110
+ },
111
+ onEvent
112
+ );
113
+ }
114
+ async streamHeaders() {
115
+ const token = await this.config.getAccessToken();
116
+ if (!token) {
117
+ throw new AgentApiError("You need to be signed in to use the assistant.", 401);
118
+ }
119
+ return {
120
+ // Include application/json so DRF content-negotiation always finds a match.
121
+ Accept: "text/event-stream, application/json",
122
+ Authorization: `Bearer ${token}`,
123
+ "X-Agent-Client-Id": this.config.clientId
124
+ };
125
+ }
126
+ async consumeStream(path, init, onEvent) {
127
+ const base = this.config.apiBaseUrl.replace(/\/$/, "");
128
+ const separator = path.includes("?") ? "&" : "?";
129
+ const url = `${base}${path}${separator}client_id=${encodeURIComponent(this.config.clientId)}`;
130
+ const response = await fetch(url, {
131
+ body: init.body,
132
+ headers: init.headers,
133
+ method: "POST",
134
+ signal: init.signal
135
+ });
136
+ if (!response.ok || !response.body) {
137
+ let detail = `Request failed (${response.status})`;
138
+ try {
139
+ const payload = await response.json();
140
+ detail = payload.detail || payload.message || detail;
141
+ } catch {
142
+ }
143
+ throw new AgentApiError(detail, response.status || 0);
144
+ }
145
+ const reader = response.body.getReader();
146
+ const decoder = new TextDecoder();
147
+ let buffer = "";
148
+ const flushFrame = (frame) => {
149
+ const dataLines = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart());
150
+ if (dataLines.length === 0) return;
151
+ const raw = dataLines.join("\n");
152
+ try {
153
+ onEvent(JSON.parse(raw));
154
+ } catch {
155
+ }
156
+ };
157
+ for (; ; ) {
158
+ const { done, value } = await reader.read();
159
+ if (done) break;
160
+ buffer += decoder.decode(value, { stream: true });
161
+ let boundary = buffer.indexOf("\n\n");
162
+ while (boundary !== -1) {
163
+ flushFrame(buffer.slice(0, boundary));
164
+ buffer = buffer.slice(boundary + 2);
165
+ boundary = buffer.indexOf("\n\n");
166
+ }
167
+ }
168
+ if (buffer.trim()) flushFrame(buffer);
169
+ }
78
170
  listConversations() {
79
171
  return this.request("/agent-mode/conversations/");
80
172
  }
@@ -127,6 +219,7 @@ function formatBytes(bytes) {
127
219
  }
128
220
  function AgentComposer({
129
221
  acceptAttribute: acceptAttribute2,
222
+ busy,
130
223
  className,
131
224
  disabled,
132
225
  files,
@@ -137,14 +230,18 @@ function AgentComposer({
137
230
  placeholder = "Ask anything\u2026",
138
231
  value
139
232
  }) {
140
- const textareaRef = React6.useRef(null);
141
- const inputRef = React6.useRef(null);
142
- const canSubmit = !disabled && (value.trim().length > 0 || files.length > 0);
143
- const submit = React6.useCallback(() => {
233
+ const textareaRef = React7.useRef(null);
234
+ const inputRef = React7.useRef(null);
235
+ const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
236
+ const submit = React7.useCallback(() => {
144
237
  if (!canSubmit) return;
145
238
  onSubmit(value.trim());
239
+ textareaRef.current?.focus();
146
240
  }, [canSubmit, onSubmit, value]);
147
- React6.useEffect(() => {
241
+ React7.useEffect(() => {
242
+ if (!disabled) textareaRef.current?.focus();
243
+ }, [disabled]);
244
+ React7.useEffect(() => {
148
245
  const textarea = textareaRef.current;
149
246
  if (!textarea) return;
150
247
  textarea.style.height = "auto";
@@ -325,6 +422,32 @@ function groupMessages(messages) {
325
422
  flush();
326
423
  return items;
327
424
  }
425
+ function ChatScroller({ children, className, ...props }) {
426
+ const viewportRef = React7.useRef(null);
427
+ const pinnedRef = React7.useRef(true);
428
+ const handleScroll = React7.useCallback(() => {
429
+ const viewport = viewportRef.current;
430
+ if (!viewport) return;
431
+ const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
432
+ pinnedRef.current = distanceFromBottom < 64;
433
+ }, []);
434
+ React7.useEffect(() => {
435
+ const viewport = viewportRef.current;
436
+ if (!viewport || !pinnedRef.current) return;
437
+ viewport.scrollTop = viewport.scrollHeight;
438
+ });
439
+ return /* @__PURE__ */ jsx(
440
+ "div",
441
+ {
442
+ className: cn("flex min-h-0 flex-1 flex-col gap-3 overflow-y-auto p-4", className),
443
+ "data-slot": "chat-scroller",
444
+ onScroll: handleScroll,
445
+ ref: viewportRef,
446
+ ...props,
447
+ children
448
+ }
449
+ );
450
+ }
328
451
  function MessageItem({ message }) {
329
452
  if (message.role === "user") {
330
453
  const attachments = message.attachments ?? [];
@@ -359,8 +482,8 @@ var DEFAULT_PHRASES = [
359
482
  ];
360
483
  function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
361
484
  const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
362
- const [index, setIndex] = React6.useState(0);
363
- React6.useEffect(() => {
485
+ const [index, setIndex] = React7.useState(0);
486
+ React7.useEffect(() => {
364
487
  setIndex(0);
365
488
  const timer = setInterval(() => {
366
489
  setIndex((current) => (current + 1) % words.length);
@@ -389,7 +512,7 @@ function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
389
512
  className: "fade-in-0 animate-in text-muted-foreground text-xs duration-300",
390
513
  children: [
391
514
  words[index],
392
- "\u2026"
515
+ " \u2026"
393
516
  ]
394
517
  },
395
518
  index
@@ -622,7 +745,7 @@ function DownloadButton({ block, size = "sm" }) {
622
745
  );
623
746
  }
624
747
  function DocumentViewer({ block, onClose }) {
625
- React6.useEffect(() => {
748
+ React7.useEffect(() => {
626
749
  const onKey = (event) => event.key === "Escape" && onClose();
627
750
  window.addEventListener("keydown", onKey);
628
751
  return () => window.removeEventListener("keydown", onKey);
@@ -647,7 +770,7 @@ function DocumentViewer({ block, onClose }) {
647
770
  ] });
648
771
  }
649
772
  function DocumentCard({ block }) {
650
- const [open, setOpen] = React6.useState(false);
773
+ const [open, setOpen] = React7.useState(false);
651
774
  const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
652
775
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
653
776
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
@@ -769,7 +892,7 @@ function StepDetail({
769
892
  ] });
770
893
  }
771
894
  function ToolActivityGroup({ describeToolCall, steps }) {
772
- const [open, setOpen] = React6.useState(false);
895
+ const [open, setOpen] = React7.useState(false);
773
896
  if (steps.length === 0) return null;
774
897
  const last = steps[steps.length - 1];
775
898
  const anyRunning = steps.some((step) => step.status === "running");
@@ -872,19 +995,19 @@ function AgentPanel({
872
995
  onClose,
873
996
  onToggleAgentMode
874
997
  }) {
875
- const [draft, setDraft] = React6.useState("");
876
- const [view, setView] = React6.useState("chat");
877
- const [files, setFiles] = React6.useState([]);
878
- const [uploadError, setUploadError] = React6.useState(null);
879
- const [dragging, setDragging] = React6.useState(false);
880
- const dragDepth = React6.useRef(0);
998
+ const [draft, setDraft] = React7.useState("");
999
+ const [view, setView] = React7.useState("chat");
1000
+ const [files, setFiles] = React7.useState([]);
1001
+ const [uploadError, setUploadError] = React7.useState(null);
1002
+ const [dragging, setDragging] = React7.useState(false);
1003
+ const dragDepth = React7.useRef(0);
881
1004
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
882
1005
  const uploadEnabled = fileUploadEnabled(config);
883
1006
  const suggestions = config.suggestions ?? [
884
1007
  "What can you help me with?",
885
1008
  "Give me a quick summary of my data."
886
1009
  ];
887
- const addFiles = React6.useCallback(
1010
+ const addFiles = React7.useCallback(
888
1011
  (incoming) => {
889
1012
  const accepted = [];
890
1013
  let rejection = null;
@@ -901,10 +1024,10 @@ function AgentPanel({
901
1024
  },
902
1025
  [config]
903
1026
  );
904
- const removeFile = React6.useCallback((index) => {
1027
+ const removeFile = React7.useCallback((index) => {
905
1028
  setFiles((current) => current.filter((_, i) => i !== index));
906
1029
  }, []);
907
- const handleSubmit = React6.useCallback(
1030
+ const handleSubmit = React7.useCallback(
908
1031
  (text) => {
909
1032
  const staged = files;
910
1033
  setDraft("");
@@ -914,7 +1037,7 @@ function AgentPanel({
914
1037
  },
915
1038
  [chat, files]
916
1039
  );
917
- const onDragEnter = React6.useCallback(
1040
+ const onDragEnter = React7.useCallback(
918
1041
  (event) => {
919
1042
  if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
920
1043
  dragDepth.current += 1;
@@ -922,12 +1045,12 @@ function AgentPanel({
922
1045
  },
923
1046
  [uploadEnabled]
924
1047
  );
925
- const onDragLeave = React6.useCallback((event) => {
1048
+ const onDragLeave = React7.useCallback((event) => {
926
1049
  if (!event.dataTransfer?.types?.includes("Files")) return;
927
1050
  dragDepth.current = Math.max(0, dragDepth.current - 1);
928
1051
  if (dragDepth.current === 0) setDragging(false);
929
1052
  }, []);
930
- const onDrop = React6.useCallback(
1053
+ const onDrop = React7.useCallback(
931
1054
  (event) => {
932
1055
  if (!uploadEnabled) return;
933
1056
  event.preventDefault();
@@ -938,11 +1061,14 @@ function AgentPanel({
938
1061
  },
939
1062
  [addFiles, uploadEnabled]
940
1063
  );
941
- const openHistory = React6.useCallback(() => {
1064
+ const openHistory = React7.useCallback(() => {
942
1065
  setView("history");
943
1066
  void chat.refreshConversations();
944
1067
  }, [chat]);
945
1068
  const busy = chat.sending || chat.resolvingApprovalId !== null;
1069
+ const lastMessage = chat.messages[chat.messages.length - 1];
1070
+ const replyStreaming = lastMessage?.role === "assistant" && Boolean(lastMessage.content);
1071
+ const showThinking = busy && !replyStreaming;
946
1072
  return /* @__PURE__ */ jsxs(
947
1073
  "div",
948
1074
  {
@@ -1065,7 +1191,7 @@ function AgentPanel({
1065
1191
  },
1066
1192
  conversation.id
1067
1193
  )) }) : /* @__PURE__ */ jsxs(Chat, { children: [
1068
- /* @__PURE__ */ jsx(ChatMessages, { className: cn(agentMode && "mx-auto w-full max-w-3xl"), children: chat.messages.length === 0 && !chat.sending ? /* @__PURE__ */ jsxs(ChatEmptyState, { children: [
1194
+ /* @__PURE__ */ jsx(ChatScroller, { className: cn(agentMode && "mx-auto w-full max-w-3xl"), children: chat.messages.length === 0 && !chat.sending ? /* @__PURE__ */ jsxs(ChatEmptyState, { children: [
1069
1195
  /* @__PURE__ */ jsx(ChatEmptyStateIcon, {}),
1070
1196
  /* @__PURE__ */ jsxs("div", { children: [
1071
1197
  /* @__PURE__ */ jsxs(ChatEmptyStateTitle, { children: [
@@ -1099,7 +1225,7 @@ function AgentPanel({
1099
1225
  },
1100
1226
  approval.id
1101
1227
  )),
1102
- busy ? /* @__PURE__ */ jsx(ThinkingIndicator, {}) : null
1228
+ showThinking ? /* @__PURE__ */ jsx(ThinkingIndicator, {}) : null
1103
1229
  ] }) }),
1104
1230
  chat.error || uploadError ? /* @__PURE__ */ jsxs("div", { className: "mx-3 mb-1 flex items-center justify-between gap-2 rounded-lg bg-destructive/10 px-3 py-2 text-destructive text-xs", children: [
1105
1231
  /* @__PURE__ */ jsx("span", { className: "min-w-0 truncate", children: chat.error ?? uploadError }),
@@ -1120,8 +1246,9 @@ function AgentPanel({
1120
1246
  AgentComposer,
1121
1247
  {
1122
1248
  acceptAttribute: acceptAttribute(config),
1249
+ busy,
1123
1250
  className: cn(agentMode && "mx-auto w-full max-w-3xl"),
1124
- disabled: busy || chat.pendingApprovals.length > 0,
1251
+ disabled: chat.pendingApprovals.length > 0,
1125
1252
  files,
1126
1253
  onPickFiles: uploadEnabled ? addFiles : void 0,
1127
1254
  onRemoveFile: removeFile,
@@ -1137,7 +1264,9 @@ function AgentPanel({
1137
1264
  );
1138
1265
  }
1139
1266
  var OPTIMISTIC_ID_PREFIX = "optimistic-";
1267
+ var STREAMING_ID_PREFIX = "streaming-";
1140
1268
  var optimisticCounter = 0;
1269
+ var streamingCounter = 0;
1141
1270
  function buildOptimisticUserMessage(text, files) {
1142
1271
  optimisticCounter += 1;
1143
1272
  const attachments = (files ?? []).map((file, index) => ({
@@ -1155,16 +1284,16 @@ function buildOptimisticUserMessage(text, files) {
1155
1284
  };
1156
1285
  }
1157
1286
  function useAgentChat(config) {
1158
- const client = React6.useMemo(() => new AgentApiClient(config), [config]);
1159
- const [agentInfo, setAgentInfo] = React6.useState(null);
1160
- const [conversationId, setConversationId] = React6.useState(null);
1161
- const [conversations, setConversations] = React6.useState([]);
1162
- const [messages, setMessages] = React6.useState([]);
1163
- const [pendingApprovals, setPendingApprovals] = React6.useState([]);
1164
- const [sending, setSending] = React6.useState(false);
1165
- const [resolvingApprovalId, setResolvingApprovalId] = React6.useState(null);
1166
- const [error, setError] = React6.useState(null);
1167
- React6.useEffect(() => {
1287
+ const client = React7.useMemo(() => new AgentApiClient(config), [config]);
1288
+ const [agentInfo, setAgentInfo] = React7.useState(null);
1289
+ const [conversationId, setConversationId] = React7.useState(null);
1290
+ const [conversations, setConversations] = React7.useState([]);
1291
+ const [messages, setMessages] = React7.useState([]);
1292
+ const [pendingApprovals, setPendingApprovals] = React7.useState([]);
1293
+ const [sending, setSending] = React7.useState(false);
1294
+ const [resolvingApprovalId, setResolvingApprovalId] = React7.useState(null);
1295
+ const [error, setError] = React7.useState(null);
1296
+ React7.useEffect(() => {
1168
1297
  let cancelled = false;
1169
1298
  client.getConfig().then((info) => {
1170
1299
  if (!cancelled) setAgentInfo(info);
@@ -1174,11 +1303,11 @@ function useAgentChat(config) {
1174
1303
  cancelled = true;
1175
1304
  };
1176
1305
  }, [client]);
1177
- const handleFailure = React6.useCallback((err) => {
1306
+ const handleFailure = React7.useCallback((err) => {
1178
1307
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
1179
1308
  setError(message);
1180
1309
  }, []);
1181
- const applyResponse = React6.useCallback(
1310
+ const applyResponse = React7.useCallback(
1182
1311
  (response) => {
1183
1312
  if (response.status === "error" && !response.messages?.length) {
1184
1313
  setError(response.message ?? "The assistant could not answer.");
@@ -1200,7 +1329,72 @@ function useAgentChat(config) {
1200
1329
  },
1201
1330
  []
1202
1331
  );
1203
- const send = React6.useCallback(
1332
+ const handleStreamEvent = React7.useCallback(
1333
+ (event, ctx) => {
1334
+ switch (event.type) {
1335
+ case "conversation":
1336
+ setConversationId(event.conversation_id);
1337
+ break;
1338
+ case "user_message":
1339
+ setMessages((current) => {
1340
+ const settled = current.filter((message) => !message.id.startsWith(OPTIMISTIC_ID_PREFIX));
1341
+ const seen = new Set(settled.map((message) => message.id));
1342
+ return seen.has(event.message.id) ? settled : [...settled, event.message];
1343
+ });
1344
+ break;
1345
+ case "text_delta": {
1346
+ if (!ctx.streamingId) {
1347
+ streamingCounter += 1;
1348
+ ctx.streamingId = `${STREAMING_ID_PREFIX}${streamingCounter}`;
1349
+ const bubble = {
1350
+ id: ctx.streamingId,
1351
+ role: "assistant",
1352
+ content: event.delta,
1353
+ created_at: (/* @__PURE__ */ new Date()).toISOString()
1354
+ };
1355
+ setMessages((current) => [...current, bubble]);
1356
+ } else {
1357
+ const id = ctx.streamingId;
1358
+ setMessages(
1359
+ (current) => current.map(
1360
+ (message) => message.id === id ? { ...message, content: (message.content ?? "") + event.delta } : message
1361
+ )
1362
+ );
1363
+ }
1364
+ break;
1365
+ }
1366
+ case "assistant_message": {
1367
+ const streamingId = ctx.streamingId;
1368
+ ctx.streamingId = null;
1369
+ setMessages((current) => {
1370
+ const withoutTransient = streamingId ? current.filter((message) => message.id !== streamingId) : current;
1371
+ const seen = new Set(withoutTransient.map((message) => message.id));
1372
+ return seen.has(event.message.id) ? withoutTransient : [...withoutTransient, event.message];
1373
+ });
1374
+ break;
1375
+ }
1376
+ case "tool_result":
1377
+ setMessages((current) => {
1378
+ const seen = new Set(current.map((message) => message.id));
1379
+ return seen.has(event.message.id) ? current : [...current, event.message];
1380
+ });
1381
+ break;
1382
+ case "pending_approvals":
1383
+ setPendingApprovals(event.approvals ?? []);
1384
+ break;
1385
+ case "error":
1386
+ setError(event.message);
1387
+ break;
1388
+ case "done":
1389
+ if (event.conversation_id) setConversationId(event.conversation_id);
1390
+ if (event.pending_approvals) setPendingApprovals(event.pending_approvals);
1391
+ break;
1392
+ }
1393
+ },
1394
+ []
1395
+ );
1396
+ const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
1397
+ const send = React7.useCallback(
1204
1398
  async (text, files) => {
1205
1399
  if (sending) return;
1206
1400
  if (!text.trim() && !(files && files.length)) return;
@@ -1208,6 +1402,19 @@ function useAgentChat(config) {
1208
1402
  setError(null);
1209
1403
  setMessages((current) => [...current, buildOptimisticUserMessage(text, files)]);
1210
1404
  try {
1405
+ if (streamingEnabled) {
1406
+ const ctx = { streamingId: null };
1407
+ let started = false;
1408
+ try {
1409
+ await client.streamMessage(text, conversationId, files, (event) => {
1410
+ started = true;
1411
+ handleStreamEvent(event, ctx);
1412
+ });
1413
+ return;
1414
+ } catch (err) {
1415
+ if (started) throw err;
1416
+ }
1417
+ }
1211
1418
  const response = await client.sendMessage(text, conversationId, files);
1212
1419
  applyResponse(response);
1213
1420
  } catch (err) {
@@ -1216,14 +1423,27 @@ function useAgentChat(config) {
1216
1423
  setSending(false);
1217
1424
  }
1218
1425
  },
1219
- [applyResponse, client, conversationId, handleFailure, sending]
1426
+ [applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
1220
1427
  );
1221
- const resolveApproval = React6.useCallback(
1428
+ const resolveApproval = React7.useCallback(
1222
1429
  async (callId, approve) => {
1223
1430
  if (resolvingApprovalId) return;
1224
1431
  setResolvingApprovalId(callId);
1225
1432
  setError(null);
1226
1433
  try {
1434
+ if (streamingEnabled) {
1435
+ const ctx = { streamingId: null };
1436
+ let started = false;
1437
+ try {
1438
+ await client.streamResolveApproval(callId, approve, (event) => {
1439
+ started = true;
1440
+ handleStreamEvent(event, ctx);
1441
+ });
1442
+ return;
1443
+ } catch (err) {
1444
+ if (started) throw err;
1445
+ }
1446
+ }
1227
1447
  const response = await client.resolveApproval(callId, approve);
1228
1448
  applyResponse(response);
1229
1449
  } catch (err) {
@@ -1232,15 +1452,15 @@ function useAgentChat(config) {
1232
1452
  setResolvingApprovalId(null);
1233
1453
  }
1234
1454
  },
1235
- [applyResponse, client, handleFailure, resolvingApprovalId]
1455
+ [applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
1236
1456
  );
1237
- const newChat = React6.useCallback(() => {
1457
+ const newChat = React7.useCallback(() => {
1238
1458
  setConversationId(null);
1239
1459
  setMessages([]);
1240
1460
  setPendingApprovals([]);
1241
1461
  setError(null);
1242
1462
  }, []);
1243
- const refreshConversations = React6.useCallback(async () => {
1463
+ const refreshConversations = React7.useCallback(async () => {
1244
1464
  try {
1245
1465
  const { conversations: list } = await client.listConversations();
1246
1466
  setConversations(list);
@@ -1248,7 +1468,7 @@ function useAgentChat(config) {
1248
1468
  handleFailure(err);
1249
1469
  }
1250
1470
  }, [client, handleFailure]);
1251
- const openConversation = React6.useCallback(
1471
+ const openConversation = React7.useCallback(
1252
1472
  async (id) => {
1253
1473
  setError(null);
1254
1474
  try {
@@ -1262,7 +1482,7 @@ function useAgentChat(config) {
1262
1482
  },
1263
1483
  [client, handleFailure]
1264
1484
  );
1265
- const deleteConversation = React6.useCallback(
1485
+ const deleteConversation = React7.useCallback(
1266
1486
  async (id) => {
1267
1487
  try {
1268
1488
  await client.deleteConversation(id);
@@ -1274,7 +1494,7 @@ function useAgentChat(config) {
1274
1494
  },
1275
1495
  [client, conversationId, handleFailure, newChat]
1276
1496
  );
1277
- const clearError = React6.useCallback(() => setError(null), []);
1497
+ const clearError = React7.useCallback(() => setError(null), []);
1278
1498
  return {
1279
1499
  agentInfo,
1280
1500
  clearError,
@@ -1309,11 +1529,11 @@ function readStored(clientId) {
1309
1529
  }
1310
1530
  }
1311
1531
  function useAlwaysApprove(clientId) {
1312
- const [approved, setApproved] = React6.useState(() => /* @__PURE__ */ new Set());
1313
- React6.useEffect(() => {
1532
+ const [approved, setApproved] = React7.useState(() => /* @__PURE__ */ new Set());
1533
+ React7.useEffect(() => {
1314
1534
  setApproved(readStored(clientId));
1315
1535
  }, [clientId]);
1316
- const persist = React6.useCallback(
1536
+ const persist = React7.useCallback(
1317
1537
  (next) => {
1318
1538
  setApproved(next);
1319
1539
  if (typeof window === "undefined") return;
@@ -1324,8 +1544,8 @@ function useAlwaysApprove(clientId) {
1324
1544
  },
1325
1545
  [clientId]
1326
1546
  );
1327
- const isAlwaysApproved = React6.useCallback((toolName) => approved.has(toolName), [approved]);
1328
- const setAlwaysApprove = React6.useCallback(
1547
+ const isAlwaysApproved = React7.useCallback((toolName) => approved.has(toolName), [approved]);
1548
+ const setAlwaysApprove = React7.useCallback(
1329
1549
  (toolName) => {
1330
1550
  if (approved.has(toolName)) return;
1331
1551
  const next = new Set(approved);
@@ -1334,7 +1554,7 @@ function useAlwaysApprove(clientId) {
1334
1554
  },
1335
1555
  [approved, persist]
1336
1556
  );
1337
- const clearAlwaysApprove = React6.useCallback(
1557
+ const clearAlwaysApprove = React7.useCallback(
1338
1558
  (toolName) => {
1339
1559
  if (!approved.has(toolName)) return;
1340
1560
  const next = new Set(approved);
@@ -1346,16 +1566,16 @@ function useAlwaysApprove(clientId) {
1346
1566
  return { clearAlwaysApprove, isAlwaysApproved, setAlwaysApprove };
1347
1567
  }
1348
1568
  function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1349
- const [uncontrolledOpen, setUncontrolledOpen] = React6.useState(false);
1569
+ const [uncontrolledOpen, setUncontrolledOpen] = React7.useState(false);
1350
1570
  const open = controlledOpen ?? uncontrolledOpen;
1351
- const setOpen = React6.useCallback(
1571
+ const setOpen = React7.useCallback(
1352
1572
  (next) => {
1353
1573
  setUncontrolledOpen(next);
1354
1574
  onOpenChange?.(next);
1355
1575
  },
1356
1576
  [onOpenChange]
1357
1577
  );
1358
- const [agentMode, setAgentMode] = React6.useState(false);
1578
+ const [agentMode, setAgentMode] = React7.useState(false);
1359
1579
  const isDesktop = useMediaQuery("(min-width: 640px)", {
1360
1580
  defaultSSRValue: true,
1361
1581
  ssr: true
@@ -1365,14 +1585,14 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1365
1585
  const alwaysApprove = useAlwaysApprove(config.clientId);
1366
1586
  const { pendingApprovals, resolveApproval, resolvingApprovalId } = chat;
1367
1587
  const { isAlwaysApproved } = alwaysApprove;
1368
- React6.useEffect(() => {
1588
+ React7.useEffect(() => {
1369
1589
  if (!open || resolvingApprovalId) return;
1370
1590
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
1371
1591
  if (target) void resolveApproval(target.id, true);
1372
1592
  }, [open, pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
1373
- const firedWriteIds = React6.useRef(/* @__PURE__ */ new Set());
1593
+ const firedWriteIds = React7.useRef(/* @__PURE__ */ new Set());
1374
1594
  const { onWriteSuccess } = config;
1375
- React6.useEffect(() => {
1595
+ React7.useEffect(() => {
1376
1596
  if (!onWriteSuccess) return;
1377
1597
  for (const message of chat.messages) {
1378
1598
  if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
@@ -1382,9 +1602,9 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1382
1602
  onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
1383
1603
  }
1384
1604
  }, [chat.messages, onWriteSuccess]);
1385
- const lastNavigatedMessageId = React6.useRef(null);
1605
+ const lastNavigatedMessageId = React7.useRef(null);
1386
1606
  const { messages } = chat;
1387
- React6.useEffect(() => {
1607
+ React7.useEffect(() => {
1388
1608
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
1389
1609
  const toolMessages = messages.filter((message) => message.role === "tool");
1390
1610
  const latest = toolMessages[toolMessages.length - 1];
@@ -1396,7 +1616,7 @@ function AgentWidget({ config, onOpenChange, open: controlledOpen }) {
1396
1616
  const path = mapping.buildRoute ? mapping.buildRoute(args) : mapping.route;
1397
1617
  if (path) config.navigate(path);
1398
1618
  }, [config, fullscreen, messages, open]);
1399
- const closePanel = React6.useCallback(() => {
1619
+ const closePanel = React7.useCallback(() => {
1400
1620
  setOpen(false);
1401
1621
  setAgentMode(false);
1402
1622
  }, [setOpen]);
@@ -1475,6 +1695,6 @@ function defineAgentConfig(config) {
1475
1695
  return config;
1476
1696
  }
1477
1697
 
1478
- export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
1698
+ export { AgentApiClient, AgentApiError, AgentComposer, AgentPanel, AgentWidget, ChatScroller, DEFAULT_ACCEPT, DocumentCard, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, resolveToolPresentation, useAgentChat, useAlwaysApprove, validateFile };
1479
1699
  //# sourceMappingURL=index.js.map
1480
1700
  //# sourceMappingURL=index.js.map