@cntyclub/agent-react 0.17.0 → 0.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,11 +1,27 @@
1
- import { cn, Button, ThinkingOrb, Tooltip, TooltipTrigger, TooltipContent, ChatMessage, ChatMessageBubble, Markdown, AuroraText, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, ChatHeaderActions, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, DataTablePaged } from '@cntyclub/ui-react';
2
- import { XIcon, Trash2Icon, SquareIcon, PauseIcon, PlayIcon, CheckIcon, FileTextIcon, PaperclipIcon, MicIcon, ArrowUpIcon, CopyIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, ChevronsRightIcon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
- import * as React12 from 'react';
1
+ import { cn, Button, ThinkingOrb, Tooltip, TooltipTrigger, TooltipContent, Popover, PopoverTrigger, PopoverPopup, ChatMessage, ChatMessageBubble, Markdown, AuroraText, Spinner, Checkbox, Collapsible, CollapsibleTrigger, ChatToolChip, CollapsiblePanel, ChatToolCard, ChatToolCardHeader, ChatToolCardActions, Group, Menu, MenuTrigger, MenuPopup, MenuItem, ChatHeader, ChatHeaderAvatar, ChatHeaderTitle, AvatarGroup, ChatHeaderActions, ContextMeter, Chat, ChatEmptyState, ChatEmptyStateIcon, ChatEmptyStateTitle, ChatEmptyStateDescription, ChatSuggestions, ChatSuggestion, useMediaQuery, TooltipProvider, DataTablePaged } from '@cntyclub/ui-react';
2
+ import { XIcon, Trash2Icon, SquareIcon, PauseIcon, PlayIcon, CheckIcon, FileTextIcon, PaperclipIcon, MicIcon, ArrowUpIcon, UsersIcon, LockIcon, GlobeIcon, CopyIcon, Maximize2Icon, ListChecksIcon, ChevronDownIcon, ShieldAlertIcon, CheckCheckIcon, UploadCloudIcon, HistoryIcon, SquarePenIcon, Minimize2Icon, ChevronsRightIcon, SparklesIcon, DownloadIcon, CodeIcon } from 'lucide-react';
3
+ import * as React13 from 'react';
4
4
  import { AnimatePresence, motion } from 'motion/react';
5
5
  import { createPortal } from 'react-dom';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
7
7
 
8
8
  // src/api-client.ts
9
+ function collabBody(options) {
10
+ if (!options) return {};
11
+ const body = {};
12
+ if (options.idempotencyKey) body.idempotency_key = options.idempotencyKey;
13
+ if (options.replyToId) body.reply_to_id = options.replyToId;
14
+ if (options.threadRootId) body.thread_root_id = options.threadRootId;
15
+ if (options.mentionUserIds?.length) body.mention_user_ids = options.mentionUserIds;
16
+ return body;
17
+ }
18
+ function appendCollab(form, options) {
19
+ if (!options) return;
20
+ if (options.idempotencyKey) form.append("idempotency_key", options.idempotencyKey);
21
+ if (options.replyToId) form.append("reply_to_id", options.replyToId);
22
+ if (options.threadRootId) form.append("thread_root_id", options.threadRootId);
23
+ for (const id of options.mentionUserIds ?? []) form.append("mention_user_ids", id);
24
+ }
9
25
  var AgentApiError = class extends Error {
10
26
  status;
11
27
  constructor(message, status) {
@@ -65,20 +81,22 @@ var AgentApiClient = class {
65
81
  method: "POST"
66
82
  });
67
83
  }
68
- sendMessage(message, conversationId, files) {
84
+ sendMessage(message, conversationId, files, options) {
69
85
  if (files && files.length > 0) {
70
86
  const form = new FormData();
71
87
  form.append("client_id", this.config.clientId);
72
88
  form.append("message", message);
73
89
  if (conversationId) form.append("conversation_id", conversationId);
74
90
  for (const file of files) form.append("files", file, file.name);
91
+ appendCollab(form, options);
75
92
  return this.request("/agent-mode/chat/", { body: form, method: "POST" });
76
93
  }
77
94
  return this.request("/agent-mode/chat/", {
78
95
  body: {
79
96
  client_id: this.config.clientId,
80
97
  conversation_id: conversationId ?? null,
81
- message
98
+ message,
99
+ ...collabBody(options)
82
100
  },
83
101
  method: "POST"
84
102
  });
@@ -91,7 +109,7 @@ var AgentApiClient = class {
91
109
  }
92
110
  // ------------------------------------------------------------- streaming
93
111
  /** POST a chat turn and yield parsed SSE events as they stream in. */
94
- async streamMessage(message, conversationId, files, onEvent, signal) {
112
+ async streamMessage(message, conversationId, files, onEvent, signal, options) {
95
113
  let body;
96
114
  const headers = await this.streamHeaders();
97
115
  if (files && files.length > 0) {
@@ -100,13 +118,15 @@ var AgentApiClient = class {
100
118
  form.append("message", message);
101
119
  if (conversationId) form.append("conversation_id", conversationId);
102
120
  for (const file of files) form.append("files", file, file.name);
121
+ appendCollab(form, options);
103
122
  body = form;
104
123
  } else {
105
124
  headers["Content-Type"] = "application/json";
106
125
  body = JSON.stringify({
107
126
  client_id: this.config.clientId,
108
127
  conversation_id: conversationId ?? null,
109
- message
128
+ message,
129
+ ...collabBody(options)
110
130
  });
111
131
  }
112
132
  await this.consumeStream("/agent-mode/chat/stream/", { body, headers, signal }, onEvent);
@@ -184,14 +204,219 @@ var AgentApiClient = class {
184
204
  listConversations() {
185
205
  return this.request("/agent-mode/conversations/");
186
206
  }
187
- getConversation(conversationId) {
188
- return this.request(`/agent-mode/conversations/${conversationId}/`);
207
+ getConversation(conversationId, options) {
208
+ const qs = new URLSearchParams();
209
+ if (options?.beforeSeq != null) qs.set("before_seq", String(options.beforeSeq));
210
+ if (options?.limit != null) qs.set("limit", String(options.limit));
211
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
212
+ return this.request(`/agent-mode/conversations/${conversationId}/${suffix}`);
189
213
  }
214
+ /** Soft-delete (creator/admin) or leave (member). Never a hard delete. */
190
215
  deleteConversation(conversationId) {
191
216
  return this.request(`/agent-mode/conversations/${conversationId}/`, {
192
217
  method: "DELETE"
193
218
  });
194
219
  }
220
+ // ------------------------------------------------------------- collaboration
221
+ setVisibility(conversationId, visibility, scope) {
222
+ return this.request(`/agent-mode/conversations/${conversationId}/visibility/`, {
223
+ method: "POST",
224
+ body: {
225
+ client_id: this.config.clientId,
226
+ visibility,
227
+ scope_type: scope?.scopeType ?? "none",
228
+ scope_event_id: scope?.scopeEventId
229
+ }
230
+ });
231
+ }
232
+ listParticipants(conversationId) {
233
+ return this.request(`/agent-mode/conversations/${conversationId}/participants/`);
234
+ }
235
+ markRead(conversationId, seq) {
236
+ return this.request(`/agent-mode/conversations/${conversationId}/read/`, {
237
+ method: "POST",
238
+ body: { client_id: this.config.clientId, seq }
239
+ });
240
+ }
241
+ saveDraft(conversationId, text) {
242
+ return this.request(`/agent-mode/conversations/${conversationId}/draft/`, {
243
+ method: "POST",
244
+ body: { client_id: this.config.clientId, text }
245
+ });
246
+ }
247
+ setFlags(conversationId, flags) {
248
+ return this.request(`/agent-mode/conversations/${conversationId}/flags/`, {
249
+ method: "POST",
250
+ body: { client_id: this.config.clientId, ...flags }
251
+ });
252
+ }
253
+ listFiles(conversationId) {
254
+ return this.request(`/agent-mode/conversations/${conversationId}/files/`);
255
+ }
256
+ summarize(conversationId) {
257
+ return this.request(`/agent-mode/conversations/${conversationId}/summarize/`, {
258
+ method: "POST",
259
+ body: { client_id: this.config.clientId }
260
+ });
261
+ }
262
+ listContext(conversationId) {
263
+ return this.request(`/agent-mode/conversations/${conversationId}/context/`);
264
+ }
265
+ pinContext(conversationId, item) {
266
+ return this.request(`/agent-mode/conversations/${conversationId}/context/`, {
267
+ method: "POST",
268
+ body: {
269
+ client_id: this.config.clientId,
270
+ kind: item.kind,
271
+ label: item.label,
272
+ ref_type: item.refType,
273
+ ref_id: item.refId
274
+ }
275
+ });
276
+ }
277
+ editMessage(conversationId, messageId, message) {
278
+ return this.request(`/agent-mode/conversations/${conversationId}/messages/${messageId}/`, {
279
+ method: "PATCH",
280
+ body: { client_id: this.config.clientId, message }
281
+ });
282
+ }
283
+ deleteMessage(conversationId, messageId) {
284
+ return this.request(`/agent-mode/conversations/${conversationId}/messages/${messageId}/`, {
285
+ method: "DELETE"
286
+ });
287
+ }
288
+ search(query, limit = 20) {
289
+ const qs = new URLSearchParams({ query, limit: String(limit) });
290
+ return this.request(`/agent-mode/search/?${qs.toString()}`);
291
+ }
292
+ getToolTrust() {
293
+ return this.request("/agent-mode/tool-trust/");
294
+ }
295
+ setToolTrust(toolName, trusted) {
296
+ return this.request("/agent-mode/tool-trust/", {
297
+ method: "POST",
298
+ body: { client_id: this.config.clientId, tool_name: toolName, trusted }
299
+ });
300
+ }
301
+ };
302
+
303
+ // src/realtime-client.ts
304
+ var AgentRealtimeClient = class {
305
+ config;
306
+ conversationId;
307
+ handlers;
308
+ socket = null;
309
+ lastSeq = 0;
310
+ closed = false;
311
+ retries = 0;
312
+ reconnectTimer = null;
313
+ constructor(config, conversationId, handlers, lastSeq = 0) {
314
+ this.config = config;
315
+ this.conversationId = conversationId;
316
+ this.handlers = handlers;
317
+ this.lastSeq = lastSeq;
318
+ }
319
+ async connect() {
320
+ if (this.closed || typeof WebSocket === "undefined") return;
321
+ const url = await this.buildUrl();
322
+ if (!url) return;
323
+ this.handlers.onStatus?.("connecting");
324
+ let socket;
325
+ try {
326
+ socket = new WebSocket(url);
327
+ } catch {
328
+ this.scheduleReconnect();
329
+ return;
330
+ }
331
+ this.socket = socket;
332
+ socket.onopen = () => {
333
+ this.retries = 0;
334
+ this.handlers.onStatus?.("open");
335
+ this.send({ action: "sync", last_seq: this.lastSeq });
336
+ };
337
+ socket.onmessage = (raw) => {
338
+ let event;
339
+ try {
340
+ event = JSON.parse(raw.data);
341
+ } catch {
342
+ return;
343
+ }
344
+ this.trackSeq(event);
345
+ this.handlers.onEvent(event);
346
+ };
347
+ socket.onclose = () => {
348
+ this.handlers.onStatus?.("closed");
349
+ if (!this.closed) this.scheduleReconnect();
350
+ };
351
+ socket.onerror = () => {
352
+ try {
353
+ socket.close();
354
+ } catch {
355
+ }
356
+ };
357
+ }
358
+ /** Advance the local high-water mark so a reconnect replays only the true gap. */
359
+ trackSeq(event) {
360
+ if (event.type === "sync") {
361
+ if (typeof event.last_seq === "number") this.lastSeq = Math.max(this.lastSeq, event.last_seq);
362
+ } else if (event.type === "message" || event.type === "message_edited") {
363
+ const seq = event.message?.seq;
364
+ if (typeof seq === "number") this.lastSeq = Math.max(this.lastSeq, seq);
365
+ }
366
+ }
367
+ setLastSeq(seq) {
368
+ if (seq > this.lastSeq) this.lastSeq = seq;
369
+ }
370
+ sendTyping(isTyping) {
371
+ this.send({ action: "typing", is_typing: isTyping });
372
+ }
373
+ markRead(seq) {
374
+ this.setLastSeq(seq);
375
+ this.send({ action: "read", seq });
376
+ }
377
+ requestSync() {
378
+ this.send({ action: "sync", last_seq: this.lastSeq });
379
+ }
380
+ close() {
381
+ this.closed = true;
382
+ if (this.reconnectTimer) clearTimeout(this.reconnectTimer);
383
+ this.reconnectTimer = null;
384
+ if (this.socket) {
385
+ try {
386
+ this.socket.close();
387
+ } catch {
388
+ }
389
+ this.socket = null;
390
+ }
391
+ }
392
+ send(payload) {
393
+ if (this.socket && this.socket.readyState === WebSocket.OPEN) {
394
+ try {
395
+ this.socket.send(JSON.stringify(payload));
396
+ } catch {
397
+ }
398
+ }
399
+ }
400
+ scheduleReconnect() {
401
+ if (this.closed || this.reconnectTimer) return;
402
+ this.retries += 1;
403
+ const delay = Math.min(3e4, 1e3 * 2 ** Math.min(this.retries, 5));
404
+ this.reconnectTimer = setTimeout(() => {
405
+ this.reconnectTimer = null;
406
+ void this.connect();
407
+ }, delay);
408
+ }
409
+ async buildUrl() {
410
+ const token = await this.config.getAccessToken();
411
+ if (!token) return null;
412
+ const base = (this.config.realtime?.baseUrl ?? this.config.apiBaseUrl).replace(/\/$/, "").replace(/^http/, "ws");
413
+ const params = new URLSearchParams({
414
+ client_id: this.config.clientId,
415
+ token,
416
+ last_seq: String(this.lastSeq)
417
+ });
418
+ return `${base}/ws/agent-mode/conversations/${this.conversationId}/?${params.toString()}`;
419
+ }
195
420
  };
196
421
 
197
422
  // src/file-support.ts
@@ -259,7 +484,7 @@ function formatBytes(bytes) {
259
484
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
260
485
  }
261
486
  function AgentImageLightbox({ src, alt, onClose }) {
262
- React12.useEffect(() => {
487
+ React13.useEffect(() => {
263
488
  if (!src) return;
264
489
  const onKey = (event) => {
265
490
  if (event.key === "Escape") onClose();
@@ -329,24 +554,24 @@ function pickMimeType() {
329
554
  return "";
330
555
  }
331
556
  function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError }) {
332
- const [phase, setPhase] = React12.useState("recording");
333
- const [elapsed, setElapsed] = React12.useState(0);
334
- const [playing, setPlaying] = React12.useState(false);
335
- const recorderRef = React12.useRef(null);
336
- const streamRef = React12.useRef(null);
337
- const chunksRef = React12.useRef([]);
338
- const blobRef = React12.useRef(null);
339
- const audioUrlRef = React12.useRef(null);
340
- const audioRef = React12.useRef(null);
341
- const timerRef = React12.useRef(null);
342
- const canvasRef = React12.useRef(null);
343
- const audioContextRef = React12.useRef(null);
344
- const analyserRef = React12.useRef(null);
345
- const rafRef = React12.useRef(null);
346
- const levelsRef = React12.useRef(new Array(MAX_BARS).fill(0));
347
- const onErrorRef = React12.useRef(onError);
557
+ const [phase, setPhase] = React13.useState("recording");
558
+ const [elapsed, setElapsed] = React13.useState(0);
559
+ const [playing, setPlaying] = React13.useState(false);
560
+ const recorderRef = React13.useRef(null);
561
+ const streamRef = React13.useRef(null);
562
+ const chunksRef = React13.useRef([]);
563
+ const blobRef = React13.useRef(null);
564
+ const audioUrlRef = React13.useRef(null);
565
+ const audioRef = React13.useRef(null);
566
+ const timerRef = React13.useRef(null);
567
+ const canvasRef = React13.useRef(null);
568
+ const audioContextRef = React13.useRef(null);
569
+ const analyserRef = React13.useRef(null);
570
+ const rafRef = React13.useRef(null);
571
+ const levelsRef = React13.useRef(new Array(MAX_BARS).fill(0));
572
+ const onErrorRef = React13.useRef(onError);
348
573
  onErrorRef.current = onError;
349
- const stopMeter = React12.useCallback(() => {
574
+ const stopMeter = React13.useCallback(() => {
350
575
  if (rafRef.current != null) {
351
576
  cancelAnimationFrame(rafRef.current);
352
577
  rafRef.current = null;
@@ -357,7 +582,7 @@ function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError
357
582
  if (ctx && ctx.state !== "closed") void ctx.close().catch(() => {
358
583
  });
359
584
  }, []);
360
- const stopTracks = React12.useCallback(() => {
585
+ const stopTracks = React13.useCallback(() => {
361
586
  streamRef.current?.getTracks().forEach((track) => track.stop());
362
587
  streamRef.current = null;
363
588
  if (timerRef.current) {
@@ -366,7 +591,7 @@ function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError
366
591
  }
367
592
  stopMeter();
368
593
  }, [stopMeter]);
369
- const startMeter = React12.useCallback((stream) => {
594
+ const startMeter = React13.useCallback((stream) => {
370
595
  const Ctor = typeof window === "undefined" ? void 0 : window.AudioContext ?? window.webkitAudioContext;
371
596
  if (!Ctor) return;
372
597
  let context;
@@ -436,7 +661,7 @@ function AgentVoiceRecorder({ maxSeconds, transcribe, onDone, onCancel, onError
436
661
  };
437
662
  rafRef.current = requestAnimationFrame(draw);
438
663
  }, []);
439
- React12.useEffect(() => {
664
+ React13.useEffect(() => {
440
665
  let cancelled = false;
441
666
  void (async () => {
442
667
  if (typeof navigator === "undefined" || !navigator.mediaDevices?.getUserMedia) {
@@ -589,40 +814,40 @@ function AgentComposer({
589
814
  voiceEnabled,
590
815
  voiceMaxSeconds = 600
591
816
  }) {
592
- const textareaRef = React12.useRef(null);
593
- const inputRef = React12.useRef(null);
594
- const [recording, setRecording] = React12.useState(false);
595
- const [lightboxSrc, setLightboxSrc] = React12.useState(null);
817
+ const textareaRef = React13.useRef(null);
818
+ const inputRef = React13.useRef(null);
819
+ const [recording, setRecording] = React13.useState(false);
820
+ const [lightboxSrc, setLightboxSrc] = React13.useState(null);
596
821
  const canSubmit = !disabled && !busy && (value.trim().length > 0 || files.length > 0);
597
822
  const voiceAvailable = Boolean(voiceEnabled && onTranscribe);
598
- const submit = React12.useCallback(() => {
823
+ const submit = React13.useCallback(() => {
599
824
  if (!canSubmit) return;
600
825
  onSubmit(value.trim());
601
826
  textareaRef.current?.focus();
602
827
  }, [canSubmit, onSubmit, value]);
603
- React12.useEffect(() => {
828
+ React13.useEffect(() => {
604
829
  if (!disabled) textareaRef.current?.focus();
605
830
  }, [disabled]);
606
- React12.useEffect(() => {
831
+ React13.useEffect(() => {
607
832
  const textarea = textareaRef.current;
608
833
  if (!textarea) return;
609
834
  textarea.style.height = "auto";
610
835
  textarea.style.height = `${Math.min(textarea.scrollHeight, 144)}px`;
611
836
  }, [value]);
612
- const previews = React12.useMemo(
837
+ const previews = React13.useMemo(
613
838
  () => files.map((file, originalIndex) => {
614
839
  const image = isImageFile(file);
615
840
  return { file, image, originalIndex, url: image ? URL.createObjectURL(file) : null };
616
841
  }),
617
842
  [files]
618
843
  );
619
- React12.useEffect(
844
+ React13.useEffect(
620
845
  () => () => {
621
846
  previews.forEach((preview) => preview.url && URL.revokeObjectURL(preview.url));
622
847
  },
623
848
  [previews]
624
849
  );
625
- const ordered = React12.useMemo(
850
+ const ordered = React13.useMemo(
626
851
  () => [...previews].sort((a, b) => Number(b.image) - Number(a.image)),
627
852
  [previews]
628
853
  );
@@ -866,16 +1091,114 @@ function groupMessages(messages) {
866
1091
  flush();
867
1092
  return items;
868
1093
  }
1094
+ function audienceLabel(scope) {
1095
+ return scope === "event" ? "this event's organizers" : "your team";
1096
+ }
1097
+ function AgentVisibilityControl({ config, conversation, onSetVisibility }) {
1098
+ const [open, setOpen] = React13.useState(false);
1099
+ const collab = config.collaboration;
1100
+ const scope = collab?.scope;
1101
+ const isPublic = conversation.visibility === "public";
1102
+ if (collab?.enabled === false) return null;
1103
+ if (!conversation.can_toggle_visibility) {
1104
+ if (!isPublic) return null;
1105
+ return /* @__PURE__ */ jsxs(Tooltip, { children: [
1106
+ /* @__PURE__ */ jsx(
1107
+ TooltipTrigger,
1108
+ {
1109
+ render: /* @__PURE__ */ jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-primary/10 px-2 py-0.5 text-primary text-xs", children: [
1110
+ /* @__PURE__ */ jsx(UsersIcon, { className: "size-3" }),
1111
+ "Shared"
1112
+ ] })
1113
+ }
1114
+ ),
1115
+ /* @__PURE__ */ jsxs(TooltipContent, { children: [
1116
+ "Shared with ",
1117
+ audienceLabel(scope)
1118
+ ] })
1119
+ ] });
1120
+ }
1121
+ const eventId = scope === "event" ? collab?.getEventId?.() ?? null : null;
1122
+ const missingEvent = scope === "event" && !eventId;
1123
+ const makePublic = () => {
1124
+ onSetVisibility("public", eventId ? { scopeEventId: eventId } : void 0);
1125
+ setOpen(false);
1126
+ };
1127
+ const makePrivate = () => {
1128
+ onSetVisibility("private");
1129
+ setOpen(false);
1130
+ };
1131
+ return /* @__PURE__ */ jsxs(Popover, { onOpenChange: setOpen, open, children: [
1132
+ /* @__PURE__ */ jsxs(Tooltip, { children: [
1133
+ /* @__PURE__ */ jsx(
1134
+ TooltipTrigger,
1135
+ {
1136
+ render: /* @__PURE__ */ jsx(
1137
+ PopoverTrigger,
1138
+ {
1139
+ render: /* @__PURE__ */ jsx(
1140
+ Button,
1141
+ {
1142
+ "aria-label": isPublic ? "Shared conversation" : "Private conversation",
1143
+ size: "icon-sm",
1144
+ variant: "ghost",
1145
+ children: isPublic ? /* @__PURE__ */ jsx(UsersIcon, { className: "text-primary" }) : /* @__PURE__ */ jsx(LockIcon, {})
1146
+ }
1147
+ )
1148
+ }
1149
+ )
1150
+ }
1151
+ ),
1152
+ /* @__PURE__ */ jsx(TooltipContent, { children: isPublic ? "Shared \u2014 manage access" : "Private \u2014 share" })
1153
+ ] }),
1154
+ /* @__PURE__ */ jsx(PopoverPopup, { className: "w-72 p-3", children: isPublic ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1155
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 font-medium text-sm", children: [
1156
+ /* @__PURE__ */ jsx(GlobeIcon, { className: "size-4 text-primary" }),
1157
+ "Shared conversation"
1158
+ ] }),
1159
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
1160
+ "Everyone in ",
1161
+ audienceLabel(scope),
1162
+ " can see this conversation and everything in it."
1163
+ ] }),
1164
+ /* @__PURE__ */ jsx(
1165
+ Button,
1166
+ {
1167
+ className: "mt-1 w-full",
1168
+ disabled: !conversation.can_go_private,
1169
+ onClick: makePrivate,
1170
+ size: "sm",
1171
+ variant: "outline",
1172
+ children: "Make private"
1173
+ }
1174
+ ),
1175
+ !conversation.can_go_private ? /* @__PURE__ */ jsx("p", { className: "text-muted-foreground text-xs", children: "Others have already taken part, so this can no longer be made private." }) : null
1176
+ ] }) : /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
1177
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 font-medium text-sm", children: [
1178
+ /* @__PURE__ */ jsx(UsersIcon, { className: "size-4" }),
1179
+ "Share this conversation"
1180
+ ] }),
1181
+ /* @__PURE__ */ jsxs("p", { className: "text-muted-foreground text-xs", children: [
1182
+ audienceLabel(scope),
1183
+ " will be able to see this conversation",
1184
+ /* @__PURE__ */ jsx("strong", { children: " and its full history" }),
1185
+ ", and take part in it."
1186
+ ] }),
1187
+ missingEvent ? /* @__PURE__ */ jsx("p", { className: "text-destructive text-xs", children: "Open an event first \u2014 a shared conversation is tied to a specific event." }) : null,
1188
+ /* @__PURE__ */ jsx(Button, { className: "mt-1 w-full", disabled: missingEvent, onClick: makePublic, size: "sm", children: "Make public" })
1189
+ ] }) })
1190
+ ] });
1191
+ }
869
1192
  function ChatScroller({ children, className, ...props }) {
870
- const viewportRef = React12.useRef(null);
871
- const pinnedRef = React12.useRef(true);
872
- const handleScroll = React12.useCallback(() => {
1193
+ const viewportRef = React13.useRef(null);
1194
+ const pinnedRef = React13.useRef(true);
1195
+ const handleScroll = React13.useCallback(() => {
873
1196
  const viewport = viewportRef.current;
874
1197
  if (!viewport) return;
875
1198
  const distanceFromBottom = viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight;
876
1199
  pinnedRef.current = distanceFromBottom < 64;
877
1200
  }, []);
878
- React12.useEffect(() => {
1201
+ React13.useEffect(() => {
879
1202
  const viewport = viewportRef.current;
880
1203
  if (!viewport || !pinnedRef.current) return;
881
1204
  viewport.scrollTop = viewport.scrollHeight;
@@ -895,10 +1218,10 @@ function ChatScroller({ children, className, ...props }) {
895
1218
  var MIN_CHARS_PER_FRAME = 2;
896
1219
  var CATCH_UP = 0.22;
897
1220
  function usePrefersReducedMotion() {
898
- const [reduced, setReduced] = React12.useState(
1221
+ const [reduced, setReduced] = React13.useState(
899
1222
  () => typeof matchMedia !== "undefined" && matchMedia("(prefers-reduced-motion: reduce)").matches
900
1223
  );
901
- React12.useEffect(() => {
1224
+ React13.useEffect(() => {
902
1225
  if (typeof matchMedia === "undefined") return;
903
1226
  const mq = matchMedia("(prefers-reduced-motion: reduce)");
904
1227
  const onChange = (event) => setReduced(event.matches);
@@ -910,12 +1233,12 @@ function usePrefersReducedMotion() {
910
1233
  function useSmoothText(target, animate) {
911
1234
  const prefersReduced = usePrefersReducedMotion();
912
1235
  const enabled = animate && !prefersReduced;
913
- const [count, setCount] = React12.useState(() => enabled ? 0 : target.length);
914
- const countRef = React12.useRef(count);
1236
+ const [count, setCount] = React13.useState(() => enabled ? 0 : target.length);
1237
+ const countRef = React13.useRef(count);
915
1238
  countRef.current = count;
916
- const everEnabled = React12.useRef(enabled);
1239
+ const everEnabled = React13.useRef(enabled);
917
1240
  if (enabled) everEnabled.current = true;
918
- React12.useEffect(() => {
1241
+ React13.useEffect(() => {
919
1242
  if (!everEnabled.current) {
920
1243
  if (countRef.current !== target.length) setCount(target.length);
921
1244
  return;
@@ -944,14 +1267,14 @@ function useSmoothText(target, animate) {
944
1267
  return everEnabled.current ? target.slice(0, Math.floor(count)) : target;
945
1268
  }
946
1269
  function CopyButton({ className, text }) {
947
- const [copied, setCopied] = React12.useState(false);
948
- const timerRef = React12.useRef(null);
949
- React12.useEffect(() => {
1270
+ const [copied, setCopied] = React13.useState(false);
1271
+ const timerRef = React13.useRef(null);
1272
+ React13.useEffect(() => {
950
1273
  return () => {
951
1274
  if (timerRef.current) clearTimeout(timerRef.current);
952
1275
  };
953
1276
  }, []);
954
- const copy = React12.useCallback(async () => {
1277
+ const copy = React13.useCallback(async () => {
955
1278
  try {
956
1279
  await navigator.clipboard.writeText(text);
957
1280
  } catch {
@@ -984,7 +1307,7 @@ function CopyButton({ className, text }) {
984
1307
  }
985
1308
  var isImageAttachment = (format) => IMAGE_ACCEPT.includes(`.${format.toLowerCase()}`);
986
1309
  function MessageItem({ animate = false, message }) {
987
- const [lightboxSrc, setLightboxSrc] = React12.useState(null);
1310
+ const [lightboxSrc, setLightboxSrc] = React13.useState(null);
988
1311
  const displayed = useSmoothText(message.content ?? "", animate && message.role === "assistant");
989
1312
  if (message.role === "user") {
990
1313
  const attachments = message.attachments ?? [];
@@ -1069,8 +1392,8 @@ var DEFAULT_PHRASES = [
1069
1392
  ];
1070
1393
  function ThinkingIndicator({ className, intervalMs = 1800, phrases }) {
1071
1394
  const words = phrases && phrases.length ? phrases : DEFAULT_PHRASES;
1072
- const [index, setIndex] = React12.useState(0);
1073
- React12.useEffect(() => {
1395
+ const [index, setIndex] = React13.useState(0);
1396
+ React13.useEffect(() => {
1074
1397
  setIndex(0);
1075
1398
  const timer = setInterval(() => {
1076
1399
  setIndex((current) => (current + 1) % words.length);
@@ -1318,7 +1641,7 @@ function DownloadButton({ block, size = "sm" }) {
1318
1641
  );
1319
1642
  }
1320
1643
  function DocumentViewer({ block, onClose }) {
1321
- React12.useEffect(() => {
1644
+ React13.useEffect(() => {
1322
1645
  const onKey = (event) => event.key === "Escape" && onClose();
1323
1646
  window.addEventListener("keydown", onKey);
1324
1647
  return () => window.removeEventListener("keydown", onKey);
@@ -1343,7 +1666,7 @@ function DocumentViewer({ block, onClose }) {
1343
1666
  ] });
1344
1667
  }
1345
1668
  function DocumentCard({ block }) {
1346
- const [open, setOpen] = React12.useState(false);
1669
+ const [open, setOpen] = React13.useState(false);
1347
1670
  const previewLines = (block.preview || "").split("\n").slice(0, 6).join("\n");
1348
1671
  return /* @__PURE__ */ jsxs("div", { className: "w-full max-w-full overflow-hidden rounded-xl border border-border bg-card shadow-xs", children: [
1349
1672
  /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 border-border/72 border-b px-3 py-2", children: [
@@ -1465,7 +1788,7 @@ function StepDetail({
1465
1788
  ] });
1466
1789
  }
1467
1790
  function ToolActivityGroup({ describeToolCall, steps }) {
1468
- const [open, setOpen] = React12.useState(false);
1791
+ const [open, setOpen] = React13.useState(false);
1469
1792
  if (steps.length === 0) return null;
1470
1793
  const last = steps[steps.length - 1];
1471
1794
  const anyRunning = steps.some((step) => step.status === "running");
@@ -1558,6 +1881,12 @@ function ToolApprovalCard({
1558
1881
  ] })
1559
1882
  ] });
1560
1883
  }
1884
+ function contextMeterLabel(meter) {
1885
+ const pct = Math.round(meter.pct * 100);
1886
+ if (meter.state === "full") return `Context full (${pct}%) \u2014 start a new chat to continue`;
1887
+ if (meter.state === "summarizing") return `Context ${pct}% \u2014 summarizing older messages`;
1888
+ return `Context ${pct}% used`;
1889
+ }
1561
1890
  function AgentPanel({
1562
1891
  agentMode,
1563
1892
  canToggleAgentMode,
@@ -1570,12 +1899,12 @@ function AgentPanel({
1570
1899
  onClose,
1571
1900
  onToggleAgentMode
1572
1901
  }) {
1573
- const [draft, setDraft] = React12.useState("");
1574
- const [view, setView] = React12.useState("chat");
1575
- const [files, setFiles] = React12.useState([]);
1576
- const [uploadError, setUploadError] = React12.useState(null);
1577
- const [dragging, setDragging] = React12.useState(false);
1578
- const dragDepth = React12.useRef(0);
1902
+ const [draft, setDraft] = React13.useState("");
1903
+ const [view, setView] = React13.useState("chat");
1904
+ const [files, setFiles] = React13.useState([]);
1905
+ const [uploadError, setUploadError] = React13.useState(null);
1906
+ const [dragging, setDragging] = React13.useState(false);
1907
+ const dragDepth = React13.useRef(0);
1579
1908
  const agentName = config.agentName ?? chat.agentInfo?.name ?? "Assistant";
1580
1909
  const caps = {
1581
1910
  visionEnabled: chat.agentInfo?.vision_enabled,
@@ -1586,7 +1915,25 @@ function AgentPanel({
1586
1915
  "What can you help me with?",
1587
1916
  "Give me a quick summary of my data."
1588
1917
  ];
1589
- const addFiles = React12.useCallback(
1918
+ const typingLabel = React13.useMemo(() => {
1919
+ const names = Object.values(chat.typingUsers);
1920
+ if (!names.length) return null;
1921
+ if (names.length === 1) return `${names[0]} is typing\u2026`;
1922
+ if (names.length === 2) return `${names[0]} and ${names[1]} are typing\u2026`;
1923
+ return `${names.length} people are typing\u2026`;
1924
+ }, [chat.typingUsers]);
1925
+ const draftEpochRef = React13.useRef(-1);
1926
+ React13.useEffect(() => {
1927
+ if (draftEpochRef.current === chat.conversationEpoch) return;
1928
+ draftEpochRef.current = chat.conversationEpoch;
1929
+ setDraft(chat.myState?.draft_text ?? "");
1930
+ }, [chat.conversationEpoch, chat.myState?.draft_text]);
1931
+ React13.useEffect(() => {
1932
+ if (!chat.conversationId) return;
1933
+ const handle = setTimeout(() => chat.saveDraft(draft), 600);
1934
+ return () => clearTimeout(handle);
1935
+ }, [draft, chat.conversationId, chat.saveDraft]);
1936
+ const addFiles = React13.useCallback(
1590
1937
  (incoming) => {
1591
1938
  const accepted = [];
1592
1939
  let rejection = null;
@@ -1603,10 +1950,10 @@ function AgentPanel({
1603
1950
  },
1604
1951
  [config, caps.visionEnabled, caps.visionMaxMb]
1605
1952
  );
1606
- const removeFile = React12.useCallback((index) => {
1953
+ const removeFile = React13.useCallback((index) => {
1607
1954
  setFiles((current) => current.filter((_, i) => i !== index));
1608
1955
  }, []);
1609
- const handleSubmit = React12.useCallback(
1956
+ const handleSubmit = React13.useCallback(
1610
1957
  (text) => {
1611
1958
  const staged = files;
1612
1959
  setDraft("");
@@ -1616,7 +1963,7 @@ function AgentPanel({
1616
1963
  },
1617
1964
  [chat, files]
1618
1965
  );
1619
- const onDragEnter = React12.useCallback(
1966
+ const onDragEnter = React13.useCallback(
1620
1967
  (event) => {
1621
1968
  if (!uploadEnabled || !event.dataTransfer?.types?.includes("Files")) return;
1622
1969
  dragDepth.current += 1;
@@ -1624,12 +1971,12 @@ function AgentPanel({
1624
1971
  },
1625
1972
  [uploadEnabled]
1626
1973
  );
1627
- const onDragLeave = React12.useCallback((event) => {
1974
+ const onDragLeave = React13.useCallback((event) => {
1628
1975
  if (!event.dataTransfer?.types?.includes("Files")) return;
1629
1976
  dragDepth.current = Math.max(0, dragDepth.current - 1);
1630
1977
  if (dragDepth.current === 0) setDragging(false);
1631
1978
  }, []);
1632
- const onDrop = React12.useCallback(
1979
+ const onDrop = React13.useCallback(
1633
1980
  (event) => {
1634
1981
  if (!uploadEnabled) return;
1635
1982
  event.preventDefault();
@@ -1640,7 +1987,7 @@ function AgentPanel({
1640
1987
  },
1641
1988
  [addFiles, uploadEnabled]
1642
1989
  );
1643
- const openHistory = React12.useCallback(() => {
1990
+ const openHistory = React13.useCallback(() => {
1644
1991
  setView("history");
1645
1992
  void chat.refreshConversations();
1646
1993
  }, [chat]);
@@ -1671,10 +2018,59 @@ function AgentPanel({
1671
2018
  /* @__PURE__ */ jsx(ChatHeaderAvatar, {}),
1672
2019
  /* @__PURE__ */ jsxs(ChatHeaderTitle, { children: [
1673
2020
  /* @__PURE__ */ jsx("div", { className: "truncate font-medium text-sm", children: agentName }),
1674
- config.tagline ? /* @__PURE__ */ jsx("div", { className: "truncate text-muted-foreground text-xs", children: config.tagline }) : null
1675
- ] })
2021
+ typingLabel ? /* @__PURE__ */ jsx("div", { className: "truncate text-primary text-xs", children: typingLabel }) : config.tagline ? /* @__PURE__ */ jsx("div", { className: "truncate text-muted-foreground text-xs", children: config.tagline }) : null
2022
+ ] }),
2023
+ chat.participants.length > 1 ? /* @__PURE__ */ jsx(
2024
+ AvatarGroup,
2025
+ {
2026
+ className: "ml-0.5",
2027
+ items: chat.participants.map((participant) => ({
2028
+ alt: participant.name,
2029
+ name: participant.name,
2030
+ src: participant.avatar_url ?? void 0
2031
+ })),
2032
+ max: 4,
2033
+ showNameOnHover: true,
2034
+ size: "sm"
2035
+ }
2036
+ ) : null
1676
2037
  ] }),
1677
2038
  /* @__PURE__ */ jsxs(ChatHeaderActions, { children: [
2039
+ chat.currentConversation ? /* @__PURE__ */ jsx(
2040
+ AgentVisibilityControl,
2041
+ {
2042
+ config,
2043
+ conversation: chat.currentConversation,
2044
+ onSetVisibility: chat.setVisibility
2045
+ }
2046
+ ) : null,
2047
+ chat.contextMeter && chat.contextMeter.pct > 0 ? /* @__PURE__ */ jsxs(Tooltip, { children: [
2048
+ /* @__PURE__ */ jsx(
2049
+ TooltipTrigger,
2050
+ {
2051
+ render: /* @__PURE__ */ jsx(
2052
+ "button",
2053
+ {
2054
+ "aria-label": contextMeterLabel(chat.contextMeter),
2055
+ className: "mr-0.5 inline-flex cursor-default items-center rounded-full outline-none focus-visible:ring-2 focus-visible:ring-ring",
2056
+ type: "button",
2057
+ children: /* @__PURE__ */ jsx(
2058
+ ContextMeter,
2059
+ {
2060
+ lockAt: chat.contextMeter.lock_at,
2061
+ pct: chat.contextMeter.pct,
2062
+ size: 18,
2063
+ state: chat.contextMeter.state,
2064
+ summarizeAt: chat.contextMeter.summarize_at,
2065
+ warnAt: chat.contextMeter.warn_at
2066
+ }
2067
+ )
2068
+ }
2069
+ )
2070
+ }
2071
+ ),
2072
+ /* @__PURE__ */ jsx(TooltipContent, { children: contextMeterLabel(chat.contextMeter) })
2073
+ ] }) : null,
1678
2074
  /* @__PURE__ */ jsxs(Tooltip, { children: [
1679
2075
  /* @__PURE__ */ jsx(
1680
2076
  TooltipTrigger,
@@ -1813,7 +2209,7 @@ function AgentPanel({
1813
2209
  }
1814
2210
  }
1815
2211
  }
1816
- return grouped.map((item) => /* @__PURE__ */ jsxs(React12.Fragment, { children: [
2212
+ return grouped.map((item) => /* @__PURE__ */ jsxs(React13.Fragment, { children: [
1817
2213
  item.key === anchorKey ? /* @__PURE__ */ jsx(NewMessagesDivider, {}) : null,
1818
2214
  /* @__PURE__ */ jsx(
1819
2215
  motion.div,
@@ -1896,6 +2292,43 @@ var OPTIMISTIC_ID_PREFIX = "optimistic-";
1896
2292
  var STREAMING_ID_PREFIX = "streaming-";
1897
2293
  var optimisticCounter = 0;
1898
2294
  var streamingCounter = 0;
2295
+ function newIdempotencyKey() {
2296
+ try {
2297
+ if (typeof crypto !== "undefined" && "randomUUID" in crypto) return crypto.randomUUID();
2298
+ } catch {
2299
+ }
2300
+ return `idem-${Date.now()}-${Math.floor(Math.random() * 1e9)}`;
2301
+ }
2302
+ function mergeMessages(current, incoming) {
2303
+ if (!incoming?.length) return current;
2304
+ const settledIds = new Set(
2305
+ current.filter((m) => !m.id.startsWith(OPTIMISTIC_ID_PREFIX)).map((m) => m.id)
2306
+ );
2307
+ const additions = incoming.filter((m) => m.id && !settledIds.has(m.id));
2308
+ if (!additions.length) return current;
2309
+ let base = current;
2310
+ const optimistic = current.filter((m) => m.id.startsWith(OPTIMISTIC_ID_PREFIX));
2311
+ const placeholder = optimistic[optimistic.length - 1];
2312
+ if (placeholder) {
2313
+ const echoIndex = additions.findIndex(
2314
+ (m) => m.role === "user" && (m.content ?? "") === (placeholder.content ?? "")
2315
+ );
2316
+ if (echoIndex !== -1) {
2317
+ base = current.filter((m) => m.id !== placeholder.id);
2318
+ additions[echoIndex] = {
2319
+ ...additions[echoIndex],
2320
+ client_key: placeholder.client_key ?? placeholder.id
2321
+ };
2322
+ }
2323
+ }
2324
+ const merged = [...base, ...additions];
2325
+ merged.sort((a, b) => {
2326
+ const sa = typeof a.seq === "number" ? a.seq : Number.MAX_SAFE_INTEGER;
2327
+ const sb = typeof b.seq === "number" ? b.seq : Number.MAX_SAFE_INTEGER;
2328
+ return sa - sb;
2329
+ });
2330
+ return merged;
2331
+ }
1899
2332
  function buildOptimisticUserMessage(text, files) {
1900
2333
  optimisticCounter += 1;
1901
2334
  const attachments = (files ?? []).map((file, index) => ({
@@ -1913,17 +2346,25 @@ function buildOptimisticUserMessage(text, files) {
1913
2346
  };
1914
2347
  }
1915
2348
  function useAgentChat(config) {
1916
- const client = React12.useMemo(() => new AgentApiClient(config), [config]);
1917
- const [agentInfo, setAgentInfo] = React12.useState(null);
1918
- const [conversationId, setConversationId] = React12.useState(null);
1919
- const [conversationEpoch, setConversationEpoch] = React12.useState(0);
1920
- const [conversations, setConversations] = React12.useState([]);
1921
- const [messages, setMessages] = React12.useState([]);
1922
- const [pendingApprovals, setPendingApprovals] = React12.useState([]);
1923
- const [sending, setSending] = React12.useState(false);
1924
- const [resolvingApprovalId, setResolvingApprovalId] = React12.useState(null);
1925
- const [error, setError] = React12.useState(null);
1926
- React12.useEffect(() => {
2349
+ const client = React13.useMemo(() => new AgentApiClient(config), [config]);
2350
+ const configRef = React13.useRef(config);
2351
+ configRef.current = config;
2352
+ const [agentInfo, setAgentInfo] = React13.useState(null);
2353
+ const [conversationId, setConversationId] = React13.useState(null);
2354
+ const [conversationEpoch, setConversationEpoch] = React13.useState(0);
2355
+ const [conversations, setConversations] = React13.useState([]);
2356
+ const [messages, setMessages] = React13.useState([]);
2357
+ const [pendingApprovals, setPendingApprovals] = React13.useState([]);
2358
+ const [sending, setSending] = React13.useState(false);
2359
+ const [resolvingApprovalId, setResolvingApprovalId] = React13.useState(null);
2360
+ const [error, setError] = React13.useState(null);
2361
+ const [currentConversation, setCurrentConversation] = React13.useState(null);
2362
+ const [contextMeter, setContextMeter] = React13.useState(null);
2363
+ const [participants, setParticipants] = React13.useState([]);
2364
+ const [myState, setMyState] = React13.useState(null);
2365
+ const [typingUsers, setTypingUsers] = React13.useState({});
2366
+ const [realtimeStatus, setRealtimeStatus] = React13.useState("idle");
2367
+ React13.useEffect(() => {
1927
2368
  let cancelled = false;
1928
2369
  client.getConfig().then((info) => {
1929
2370
  if (!cancelled) setAgentInfo(info);
@@ -1933,11 +2374,11 @@ function useAgentChat(config) {
1933
2374
  cancelled = true;
1934
2375
  };
1935
2376
  }, [client]);
1936
- const handleFailure = React12.useCallback((err) => {
2377
+ const handleFailure = React13.useCallback((err) => {
1937
2378
  const message = err instanceof AgentApiError ? err.message : "Something went wrong. Please try again.";
1938
2379
  setError(message);
1939
2380
  }, []);
1940
- const applyResponse = React12.useCallback(
2381
+ const applyResponse = React13.useCallback(
1941
2382
  (response) => {
1942
2383
  if (response.status === "error" && !response.messages?.length) {
1943
2384
  setError(response.message ?? "The assistant could not answer.");
@@ -1967,7 +2408,7 @@ function useAgentChat(config) {
1967
2408
  },
1968
2409
  []
1969
2410
  );
1970
- const handleStreamEvent = React12.useCallback(
2411
+ const handleStreamEvent = React13.useCallback(
1971
2412
  (event, ctx) => {
1972
2413
  switch (event.type) {
1973
2414
  case "conversation":
@@ -2045,29 +2486,93 @@ function useAgentChat(config) {
2045
2486
  },
2046
2487
  []
2047
2488
  );
2489
+ const applyRealtime = React13.useCallback((event) => {
2490
+ switch (event.type) {
2491
+ case "sync":
2492
+ setMessages((current) => mergeMessages(current, event.messages ?? []));
2493
+ break;
2494
+ case "message":
2495
+ setMessages((current) => mergeMessages(current, [event.message]));
2496
+ break;
2497
+ case "message_edited":
2498
+ setMessages(
2499
+ (current) => current.map(
2500
+ (message) => message.id === event.message.id ? { ...event.message, client_key: message.client_key ?? message.id } : message
2501
+ )
2502
+ );
2503
+ break;
2504
+ case "message_deleted":
2505
+ setMessages(
2506
+ (current) => current.map(
2507
+ (message) => message.id === event.message_id ? { ...message, deleted: true, content: "" } : message
2508
+ )
2509
+ );
2510
+ break;
2511
+ case "typing":
2512
+ setTypingUsers((current) => {
2513
+ const next = { ...current };
2514
+ if (event.is_typing) next[event.user_id] = event.name;
2515
+ else delete next[event.user_id];
2516
+ return next;
2517
+ });
2518
+ break;
2519
+ case "visibility_changed":
2520
+ setCurrentConversation(
2521
+ (current) => current ? { ...current, visibility: event.visibility } : current
2522
+ );
2523
+ break;
2524
+ }
2525
+ }, []);
2526
+ const realtimeEnabled = config.realtime?.enabled !== false;
2527
+ const isPublic = currentConversation?.visibility === "public";
2528
+ const baselineSeq = currentConversation?.last_seq ?? 0;
2529
+ React13.useEffect(() => {
2530
+ if (!realtimeEnabled || !conversationId || !isPublic) {
2531
+ setRealtimeStatus("idle");
2532
+ return;
2533
+ }
2534
+ const rt = new AgentRealtimeClient(
2535
+ configRef.current,
2536
+ conversationId,
2537
+ { onEvent: applyRealtime, onStatus: setRealtimeStatus },
2538
+ baselineSeq
2539
+ );
2540
+ void rt.connect();
2541
+ return () => {
2542
+ rt.close();
2543
+ };
2544
+ }, [applyRealtime, baselineSeq, conversationId, isPublic, realtimeEnabled]);
2048
2545
  const streamingEnabled = config.streaming !== false && typeof ReadableStream !== "undefined";
2049
- const send = React12.useCallback(
2546
+ const send = React13.useCallback(
2050
2547
  async (text, files) => {
2051
2548
  if (sending) return;
2052
2549
  if (!text.trim() && !(files && files.length)) return;
2053
2550
  setSending(true);
2054
2551
  setError(null);
2055
2552
  setMessages((current) => [...current, buildOptimisticUserMessage(text, files)]);
2553
+ const idempotencyKey = newIdempotencyKey();
2056
2554
  try {
2057
2555
  if (streamingEnabled) {
2058
2556
  const ctx = { streamingId: null };
2059
2557
  let started = false;
2060
2558
  try {
2061
- await client.streamMessage(text, conversationId, files, (event) => {
2062
- started = true;
2063
- handleStreamEvent(event, ctx);
2064
- });
2559
+ await client.streamMessage(
2560
+ text,
2561
+ conversationId,
2562
+ files,
2563
+ (event) => {
2564
+ started = true;
2565
+ handleStreamEvent(event, ctx);
2566
+ },
2567
+ void 0,
2568
+ { idempotencyKey }
2569
+ );
2065
2570
  return;
2066
2571
  } catch (err) {
2067
2572
  if (started) throw err;
2068
2573
  }
2069
2574
  }
2070
- const response = await client.sendMessage(text, conversationId, files);
2575
+ const response = await client.sendMessage(text, conversationId, files, { idempotencyKey });
2071
2576
  applyResponse(response);
2072
2577
  } catch (err) {
2073
2578
  handleFailure(err);
@@ -2077,14 +2582,14 @@ function useAgentChat(config) {
2077
2582
  },
2078
2583
  [applyResponse, client, conversationId, handleFailure, handleStreamEvent, sending, streamingEnabled]
2079
2584
  );
2080
- const transcribe = React12.useCallback(
2585
+ const transcribe = React13.useCallback(
2081
2586
  async (audio) => {
2082
2587
  const { text } = await client.transcribe(audio);
2083
2588
  return text;
2084
2589
  },
2085
2590
  [client]
2086
2591
  );
2087
- const resolveApproval = React12.useCallback(
2592
+ const resolveApproval = React13.useCallback(
2088
2593
  async (callId, approve) => {
2089
2594
  if (resolvingApprovalId) return;
2090
2595
  setResolvingApprovalId(callId);
@@ -2113,14 +2618,19 @@ function useAgentChat(config) {
2113
2618
  },
2114
2619
  [applyResponse, client, handleFailure, handleStreamEvent, resolvingApprovalId, streamingEnabled]
2115
2620
  );
2116
- const newChat = React12.useCallback(() => {
2621
+ const newChat = React13.useCallback(() => {
2117
2622
  setConversationId(null);
2118
2623
  setMessages([]);
2119
2624
  setPendingApprovals([]);
2120
2625
  setError(null);
2626
+ setCurrentConversation(null);
2627
+ setContextMeter(null);
2628
+ setParticipants([]);
2629
+ setMyState(null);
2630
+ setTypingUsers({});
2121
2631
  setConversationEpoch((e) => e + 1);
2122
2632
  }, []);
2123
- const refreshConversations = React12.useCallback(async () => {
2633
+ const refreshConversations = React13.useCallback(async () => {
2124
2634
  try {
2125
2635
  const { conversations: list } = await client.listConversations();
2126
2636
  setConversations(list);
@@ -2130,7 +2640,7 @@ function useAgentChat(config) {
2130
2640
  return [];
2131
2641
  }
2132
2642
  }, [client, handleFailure]);
2133
- const openConversation = React12.useCallback(
2643
+ const openConversation = React13.useCallback(
2134
2644
  async (id) => {
2135
2645
  setError(null);
2136
2646
  try {
@@ -2138,6 +2648,23 @@ function useAgentChat(config) {
2138
2648
  setConversationId(id);
2139
2649
  setMessages(payload.messages ?? []);
2140
2650
  setPendingApprovals(payload.pending_approvals ?? []);
2651
+ const convo = payload.conversation;
2652
+ setCurrentConversation(
2653
+ convo ? {
2654
+ id: convo.id,
2655
+ title: convo.title,
2656
+ visibility: convo.visibility,
2657
+ scope_type: convo.scope_type,
2658
+ last_seq: convo.last_seq,
2659
+ is_creator: convo.is_creator,
2660
+ can_toggle_visibility: convo.can_toggle_visibility,
2661
+ can_go_private: convo.can_go_private
2662
+ } : null
2663
+ );
2664
+ setContextMeter(payload.context_meter ?? null);
2665
+ setParticipants(payload.participants ?? []);
2666
+ setMyState(payload.my_state ?? null);
2667
+ setTypingUsers({});
2141
2668
  setConversationEpoch((e) => e + 1);
2142
2669
  } catch (err) {
2143
2670
  handleFailure(err);
@@ -2145,7 +2672,7 @@ function useAgentChat(config) {
2145
2672
  },
2146
2673
  [client, handleFailure]
2147
2674
  );
2148
- const deleteConversation = React12.useCallback(
2675
+ const deleteConversation = React13.useCallback(
2149
2676
  async (id) => {
2150
2677
  try {
2151
2678
  await client.deleteConversation(id);
@@ -2157,25 +2684,91 @@ function useAgentChat(config) {
2157
2684
  },
2158
2685
  [client, conversationId, handleFailure, newChat]
2159
2686
  );
2160
- const clearError = React12.useCallback(() => setError(null), []);
2687
+ const setVisibility = React13.useCallback(
2688
+ async (visibility, opts) => {
2689
+ if (!conversationId) return;
2690
+ setError(null);
2691
+ try {
2692
+ const scope = configRef.current.collaboration?.scope;
2693
+ const res = await client.setVisibility(
2694
+ conversationId,
2695
+ visibility,
2696
+ visibility === "public" ? { scopeType: scope, scopeEventId: opts?.scopeEventId } : void 0
2697
+ );
2698
+ if (res.status === "error") {
2699
+ setError(res.message ?? "Could not change the conversation's visibility.");
2700
+ return;
2701
+ }
2702
+ setCurrentConversation(
2703
+ (current) => current ? { ...current, visibility, can_go_private: Boolean(res.can_go_private) } : current
2704
+ );
2705
+ void client.listParticipants(conversationId).then((p) => setParticipants(p.participants ?? [])).catch(() => {
2706
+ });
2707
+ } catch (err) {
2708
+ handleFailure(err);
2709
+ }
2710
+ },
2711
+ [client, conversationId, handleFailure]
2712
+ );
2713
+ const markRead = React13.useCallback(
2714
+ (seq) => {
2715
+ if (!conversationId || seq <= 0) return;
2716
+ setMyState(
2717
+ (current) => current && seq > current.last_read_seq ? { ...current, last_read_seq: seq } : current
2718
+ );
2719
+ void client.markRead(conversationId, seq).catch(() => {
2720
+ });
2721
+ },
2722
+ [client, conversationId]
2723
+ );
2724
+ const saveDraft = React13.useCallback(
2725
+ (text) => {
2726
+ if (!conversationId) return;
2727
+ void client.saveDraft(conversationId, text).catch(() => {
2728
+ });
2729
+ },
2730
+ [client, conversationId]
2731
+ );
2732
+ const summarize = React13.useCallback(async () => {
2733
+ if (!conversationId) return;
2734
+ setError(null);
2735
+ try {
2736
+ await client.summarize(conversationId);
2737
+ const detail = await client.getConversation(conversationId);
2738
+ setContextMeter(detail.context_meter ?? null);
2739
+ } catch (err) {
2740
+ handleFailure(err);
2741
+ }
2742
+ }, [client, conversationId, handleFailure]);
2743
+ const clearError = React13.useCallback(() => setError(null), []);
2161
2744
  return {
2162
2745
  agentInfo,
2163
2746
  clearError,
2747
+ contextMeter,
2164
2748
  conversationId,
2165
2749
  conversationEpoch,
2166
2750
  conversations,
2751
+ currentConversation,
2167
2752
  deleteConversation,
2168
2753
  error,
2754
+ markRead,
2169
2755
  messages,
2756
+ myState,
2170
2757
  newChat,
2171
2758
  openConversation,
2759
+ participants,
2172
2760
  pendingApprovals,
2761
+ realtimeStatus,
2173
2762
  refreshConversations,
2174
2763
  resolveApproval,
2175
2764
  resolvingApprovalId,
2765
+ saveDraft,
2176
2766
  send,
2177
2767
  sending,
2178
- transcribe
2768
+ setVisibility,
2769
+ summarize,
2770
+ transcribe,
2771
+ typingUsers
2179
2772
  };
2180
2773
  }
2181
2774
  var STORAGE_PREFIX = "cc-agent-always-approve:";
@@ -2194,11 +2787,11 @@ function readStored(clientId) {
2194
2787
  }
2195
2788
  }
2196
2789
  function useAlwaysApprove(clientId) {
2197
- const [approved, setApproved] = React12.useState(() => /* @__PURE__ */ new Set());
2198
- React12.useEffect(() => {
2790
+ const [approved, setApproved] = React13.useState(() => /* @__PURE__ */ new Set());
2791
+ React13.useEffect(() => {
2199
2792
  setApproved(readStored(clientId));
2200
2793
  }, [clientId]);
2201
- const persist = React12.useCallback(
2794
+ const persist = React13.useCallback(
2202
2795
  (next) => {
2203
2796
  setApproved(next);
2204
2797
  if (typeof window === "undefined") return;
@@ -2209,8 +2802,8 @@ function useAlwaysApprove(clientId) {
2209
2802
  },
2210
2803
  [clientId]
2211
2804
  );
2212
- const isAlwaysApproved = React12.useCallback((toolName) => approved.has(toolName), [approved]);
2213
- const setAlwaysApprove = React12.useCallback(
2805
+ const isAlwaysApproved = React13.useCallback((toolName) => approved.has(toolName), [approved]);
2806
+ const setAlwaysApprove = React13.useCallback(
2214
2807
  (toolName) => {
2215
2808
  if (approved.has(toolName)) return;
2216
2809
  const next = new Set(approved);
@@ -2219,7 +2812,7 @@ function useAlwaysApprove(clientId) {
2219
2812
  },
2220
2813
  [approved, persist]
2221
2814
  );
2222
- const clearAlwaysApprove = React12.useCallback(
2815
+ const clearAlwaysApprove = React13.useCallback(
2223
2816
  (toolName) => {
2224
2817
  if (!approved.has(toolName)) return;
2225
2818
  const next = new Set(approved);
@@ -2245,10 +2838,10 @@ function AgentWidget({
2245
2838
  const isControlled = controlledOpen !== void 0;
2246
2839
  const isDocked = presentation === "docked";
2247
2840
  const storageKey2 = `cc-agent-open:${config.clientId}`;
2248
- const [uncontrolledOpen, setUncontrolledOpen] = React12.useState(false);
2841
+ const [uncontrolledOpen, setUncontrolledOpen] = React13.useState(false);
2249
2842
  const open = controlledOpen ?? uncontrolledOpen;
2250
- const [bootstrapDone, setBootstrapDone] = React12.useState(isControlled);
2251
- const persist = React12.useCallback(
2843
+ const [bootstrapDone, setBootstrapDone] = React13.useState(isControlled);
2844
+ const persist = React13.useCallback(
2252
2845
  (next) => {
2253
2846
  if (isControlled || !behavior.persistOpenState) return;
2254
2847
  try {
@@ -2258,7 +2851,7 @@ function AgentWidget({
2258
2851
  },
2259
2852
  [behavior.persistOpenState, isControlled, storageKey2]
2260
2853
  );
2261
- const setOpen = React12.useCallback(
2854
+ const setOpen = React13.useCallback(
2262
2855
  (next) => {
2263
2856
  setUncontrolledOpen(next);
2264
2857
  persist(next);
@@ -2266,14 +2859,14 @@ function AgentWidget({
2266
2859
  },
2267
2860
  [onOpenChange, persist]
2268
2861
  );
2269
- const [agentMode, setAgentMode] = React12.useState(false);
2862
+ const [agentMode, setAgentMode] = React13.useState(false);
2270
2863
  const isDesktop = useMediaQuery("(min-width: 640px)", { defaultSSRValue: true, ssr: true });
2271
2864
  const fullscreen = !isDesktop || agentMode;
2272
2865
  const chat = useAgentChat(config);
2273
2866
  const alwaysApprove = useAlwaysApprove(config.clientId);
2274
2867
  const { messages, pendingApprovals, resolveApproval, resolvingApprovalId, sending } = chat;
2275
- const bootRef = React12.useRef(false);
2276
- React12.useEffect(() => {
2868
+ const bootRef = React13.useRef(false);
2869
+ React13.useEffect(() => {
2277
2870
  if (bootRef.current) return;
2278
2871
  if (isControlled && !isDocked) return;
2279
2872
  bootRef.current = true;
@@ -2305,15 +2898,15 @@ function AgentWidget({
2305
2898
  cancelled = true;
2306
2899
  };
2307
2900
  }, []);
2308
- const seenCountRef = React12.useRef(0);
2309
- const [baselined, setBaselined] = React12.useState(false);
2310
- const [anchorId, setAnchorId] = React12.useState(null);
2311
- React12.useEffect(() => {
2901
+ const seenCountRef = React13.useRef(0);
2902
+ const [baselined, setBaselined] = React13.useState(false);
2903
+ const [anchorId, setAnchorId] = React13.useState(null);
2904
+ React13.useEffect(() => {
2312
2905
  if (baselined || !bootstrapDone) return;
2313
2906
  seenCountRef.current = messages.length;
2314
2907
  setBaselined(true);
2315
2908
  }, [baselined, bootstrapDone, messages.length]);
2316
- React12.useEffect(() => {
2909
+ React13.useEffect(() => {
2317
2910
  if (open) {
2318
2911
  if (messages.length > seenCountRef.current) {
2319
2912
  const firstUnread = messages[seenCountRef.current];
@@ -2323,7 +2916,7 @@ function AgentWidget({
2323
2916
  }
2324
2917
  }, [open, messages]);
2325
2918
  const unreadCount = open || !baselined ? 0 : messages.slice(seenCountRef.current).filter(isUnreadable).length;
2326
- const workLabel = React12.useMemo(() => {
2919
+ const workLabel = React13.useMemo(() => {
2327
2920
  if (!sending) return null;
2328
2921
  const last = messages[messages.length - 1];
2329
2922
  if (last?.role === "tool") return "Calling tools\u2026";
@@ -2332,14 +2925,14 @@ function AgentWidget({
2332
2925
  }, [sending, messages]);
2333
2926
  const approvalWaiting = pendingApprovals.length > 0;
2334
2927
  const { isAlwaysApproved } = alwaysApprove;
2335
- React12.useEffect(() => {
2928
+ React13.useEffect(() => {
2336
2929
  if (resolvingApprovalId) return;
2337
2930
  const target = pendingApprovals.find((approval) => isAlwaysApproved(approval.tool_name));
2338
2931
  if (target) void resolveApproval(target.id, true);
2339
2932
  }, [pendingApprovals, resolvingApprovalId, isAlwaysApproved, resolveApproval]);
2340
- const firedWriteIds = React12.useRef(/* @__PURE__ */ new Set());
2933
+ const firedWriteIds = React13.useRef(/* @__PURE__ */ new Set());
2341
2934
  const { onWriteSuccess } = config;
2342
- React12.useEffect(() => {
2935
+ React13.useEffect(() => {
2343
2936
  if (!onWriteSuccess) return;
2344
2937
  for (const message of messages) {
2345
2938
  if (message.role !== "tool" || !message.is_write || message.tool_status !== "success") continue;
@@ -2349,9 +2942,9 @@ function AgentWidget({
2349
2942
  onWriteSuccess({ arguments: args, toolName: message.tool_name ?? "" });
2350
2943
  }
2351
2944
  }, [messages, onWriteSuccess]);
2352
- const lastNavigatedMessageId = React12.useRef(null);
2353
- const navEpochRef = React12.useRef(-1);
2354
- React12.useEffect(() => {
2945
+ const lastNavigatedMessageId = React13.useRef(null);
2946
+ const navEpochRef = React13.useRef(-1);
2947
+ React13.useEffect(() => {
2355
2948
  if (!open || fullscreen || !config.navigate || !config.pages?.length) return;
2356
2949
  const toolMessages = messages.filter((message) => message.role === "tool");
2357
2950
  const latest = toolMessages[toolMessages.length - 1];
@@ -2370,7 +2963,7 @@ function AgentWidget({
2370
2963
  const scrollTo = typeof mapping.scrollTo === "function" ? mapping.scrollTo(args) : mapping.scrollTo;
2371
2964
  config.navigate(path, { scrollTo: scrollTo ?? null });
2372
2965
  }, [chat.conversationEpoch, config, fullscreen, messages, open]);
2373
- const closePanel = React12.useCallback(() => {
2966
+ const closePanel = React13.useCallback(() => {
2374
2967
  setOpen(false);
2375
2968
  setAgentMode(false);
2376
2969
  }, [setOpen]);
@@ -2543,6 +3136,6 @@ function defineAgentConfig(config) {
2543
3136
  return config;
2544
3137
  }
2545
3138
 
2546
- export { AgentApiClient, AgentApiError, AgentComposer, AgentImageLightbox, AgentPanel, AgentVoiceRecorder, AgentWidget, ChatScroller, CopyButton, DEFAULT_ACCEPT, DocumentCard, IMAGE_ACCEPT, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, attachmentsEnabled, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, isImageFile, resolveToolPresentation, textExtensions, useAgentChat, useAlwaysApprove, useSmoothText, validateFile };
3139
+ export { AgentApiClient, AgentApiError, AgentComposer, AgentImageLightbox, AgentPanel, AgentRealtimeClient, AgentVisibilityControl, AgentVoiceRecorder, AgentWidget, ChatScroller, CopyButton, DEFAULT_ACCEPT, DocumentCard, IMAGE_ACCEPT, MessageItem, TaskChecklist, ThinkingIndicator, ToolActivityGroup, ToolApprovalCard, acceptAttribute, acceptedExtensions, attachmentsEnabled, defineAgentConfig, deriveDetails, fileUploadEnabled, formatBytes, groupMessages, humanizeToolName, isImageFile, resolveToolPresentation, textExtensions, useAgentChat, useAlwaysApprove, useSmoothText, validateFile };
2547
3140
  //# sourceMappingURL=index.js.map
2548
3141
  //# sourceMappingURL=index.js.map