@agents24/chat-react 0.5.5 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/index.cjs +11 -6
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +11 -6
  4. package/dist/index.js.map +1 -1
  5. package/dist/latest-thread-scroller.d.ts +2 -1
  6. package/dist/runtime/index.cjs +17 -11
  7. package/dist/runtime/index.cjs.map +1 -1
  8. package/dist/runtime/index.js +17 -11
  9. package/dist/runtime/index.js.map +1 -1
  10. package/dist/runtime/use-agent-chat-runtime.d.ts +7 -2
  11. package/dist/scaffold/components/chat/chat-composer.d.ts +2 -1
  12. package/dist/scaffold/components/chat/chat-message.d.ts +3 -1
  13. package/dist/scaffold/components/chat/turn-outline.d.ts +1 -1
  14. package/dist/scaffold/index.cjs +195 -154
  15. package/dist/scaffold/index.cjs.map +1 -1
  16. package/dist/scaffold/index.js +215 -168
  17. package/dist/scaffold/index.js.map +1 -1
  18. package/dist/styles.css +25 -6
  19. package/dist/ui/adapters.d.ts +14 -2
  20. package/dist/ui/agent-chat-composer.d.ts +2 -9
  21. package/dist/ui/agent-chat-message.d.ts +7 -1
  22. package/dist/ui/composer-attachments.d.ts +11 -0
  23. package/dist/ui/index.cjs +781 -389
  24. package/dist/ui/index.cjs.map +1 -1
  25. package/dist/ui/index.d.ts +1 -0
  26. package/dist/ui/index.js +766 -375
  27. package/dist/ui/index.js.map +1 -1
  28. package/package.json +4 -3
  29. package/scaffold/src/components/chat/chat-composer.tsx +43 -19
  30. package/scaffold/src/components/chat/chat-message.tsx +19 -10
  31. package/scaffold/src/components/chat/chat-shell.tsx +65 -34
  32. package/scaffold/src/components/chat/turn-outline.tsx +17 -59
  33. package/scaffold/src/components/ui/message-scroller.tsx +1 -1
  34. package/scaffold/src/components/ui/message.tsx +1 -1
  35. package/scaffold/src/components/ui/sidebar.tsx +1 -1
package/dist/ui/index.cjs CHANGED
@@ -44,6 +44,7 @@ __export(ui_exports, {
44
44
  AgentChatToolPart: () => AgentChatToolPart,
45
45
  AgentChatToolRow: () => AgentChatToolRow,
46
46
  AgentChatToolSession: () => AgentChatToolSession,
47
+ AgentChatUserMessageActionRow: () => AgentChatUserMessageActionRow,
47
48
  AgentChatUserMessageContent: () => AgentChatUserMessageContent,
48
49
  AgentResponseTimeline: () => AgentResponseTimeline,
49
50
  AskUserInteraction: () => AskUserInteraction,
@@ -61,6 +62,7 @@ __export(ui_exports, {
61
62
  BubbleGroup: () => BubbleGroup,
62
63
  BubbleReactions: () => BubbleReactions,
63
64
  ChatAttachment: () => ChatAttachment,
65
+ ChatAttachmentRows: () => ChatAttachmentRows,
64
66
  ChatAttachments: () => ChatAttachments,
65
67
  ChatMessage: () => ChatMessage,
66
68
  ChatMessageAction: () => ChatMessageAction,
@@ -74,6 +76,7 @@ __export(ui_exports, {
74
76
  MessageAction: () => ChatMessageAction,
75
77
  MessageActions: () => ChatMessageActions,
76
78
  MessageAttachment: () => ChatAttachment,
79
+ MessageAttachmentRows: () => ChatAttachmentRows,
77
80
  MessageAttachments: () => ChatAttachments,
78
81
  MessageAvatar: () => MessageAvatar,
79
82
  MessageContent: () => ChatMessageContent,
@@ -89,6 +92,7 @@ __export(ui_exports, {
89
92
  bubbleVariants: () => bubbleVariants,
90
93
  cn: () => cn,
91
94
  contextStatusMetrics: () => contextStatusMetrics,
95
+ createAgentChatComposerFile: () => createAgentChatComposerFile,
92
96
  hitlActionLabel: () => hitlActionLabel,
93
97
  hitlTitle: () => hitlTitle,
94
98
  isAgentChatSubagent: () => isAgentChatSubagent,
@@ -97,15 +101,25 @@ __export(ui_exports, {
97
101
  markerVariants: () => markerVariants,
98
102
  partitionAgentChatToolTimeline: () => partitionAgentChatToolTimeline,
99
103
  resolvedHitlLabel: () => resolvedHitlLabel,
104
+ revokeAgentChatComposerFiles: () => revokeAgentChatComposerFiles,
100
105
  shouldCollapseUserMessage: () => shouldCollapseUserMessage,
101
106
  useAudioRecorder: () => useAudioRecorder
102
107
  });
103
108
  module.exports = __toCommonJS(ui_exports);
104
109
 
105
110
  // src/ui/agent-chat-composer.tsx
111
+ var React2 = __toESM(require("react"), 1);
112
+ var import_lucide_react2 = require("lucide-react");
113
+
114
+ // src/ui/adapters.ts
106
115
  var React = __toESM(require("react"), 1);
116
+ var import_file_icons = require("@untitledui/file-icons");
107
117
  var import_lucide_react = require("lucide-react");
108
118
 
119
+ // src/ui/attachment.tsx
120
+ var import_class_variance_authority = require("class-variance-authority");
121
+ var import_react_slot = require("@radix-ui/react-slot");
122
+
109
123
  // src/ui/utils.ts
110
124
  var import_clsx = require("clsx");
111
125
  var import_tailwind_merge = require("tailwind-merge");
@@ -113,266 +127,8 @@ function cn(...values) {
113
127
  return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(values));
114
128
  }
115
129
 
116
- // src/ui/agent-chat-composer.tsx
117
- var import_jsx_runtime = require("react/jsx-runtime");
118
- var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `file-${Date.now()}-${Math.random().toString(36).slice(2)}`;
119
- function createComposerFile(file) {
120
- return {
121
- id: createId(),
122
- type: "file",
123
- url: URL.createObjectURL(file),
124
- filename: file.name || "attachment",
125
- mediaType: file.type || "application/octet-stream",
126
- source: file
127
- };
128
- }
129
- function revokeComposerFiles(files) {
130
- files.forEach((file) => {
131
- try {
132
- URL.revokeObjectURL(file.url);
133
- } catch {
134
- }
135
- });
136
- }
137
- function formatFileCount(files) {
138
- if (files.length === 1) return files[0].filename;
139
- return `${files.length} files`;
140
- }
141
- var pillButtonClass = "inline-flex size-8 shrink-0 items-center justify-center gap-2 rounded-lg border-0 bg-muted text-sm text-muted-foreground shadow-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50";
142
- var submitButtonClass = "inline-flex size-9 shrink-0 items-center justify-center gap-2 rounded-lg border-0 bg-primary text-sm text-primary-foreground shadow-none transition-[background-color,color,opacity,transform] duration-200 hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50";
143
- function AgentChatComposer({
144
- accept,
145
- allowAttachments = true,
146
- attachmentLayout = "flow",
147
- className,
148
- disabled = false,
149
- forceExpanded = false,
150
- inputToolbarContent,
151
- isRunning = false,
152
- onAttachmentCountChange,
153
- onStop,
154
- onSubmit,
155
- placeholder = "Message the agent",
156
- textareaRef
157
- }) {
158
- const fileInputRef = React.useRef(null);
159
- const filesRef = React.useRef([]);
160
- const localTextareaRef = React.useRef(null);
161
- const [text, setText] = React.useState("");
162
- const [files, setFiles] = React.useState([]);
163
- const [isSubmitting, setIsSubmitting] = React.useState(false);
164
- const isExpanded = forceExpanded || text.includes("\n") || text.length > 62;
165
- const setTextareaRef = React.useCallback(
166
- (node) => {
167
- localTextareaRef.current = node;
168
- if (typeof textareaRef === "function") {
169
- textareaRef(node);
170
- } else if (textareaRef) {
171
- textareaRef.current = node;
172
- }
173
- },
174
- [textareaRef]
175
- );
176
- React.useEffect(() => {
177
- filesRef.current = files;
178
- onAttachmentCountChange?.(files.length);
179
- }, [files, onAttachmentCountChange]);
180
- React.useEffect(() => () => revokeComposerFiles(filesRef.current), []);
181
- React.useLayoutEffect(() => {
182
- const textarea = localTextareaRef.current;
183
- if (!textarea) return;
184
- if (isExpanded) {
185
- textarea.style.height = "auto";
186
- const scrollHeight = textarea.scrollHeight;
187
- textarea.style.height = `${Math.min(scrollHeight, 224)}px`;
188
- textarea.style.overflowY = scrollHeight >= 224 ? "auto" : "hidden";
189
- return;
190
- }
191
- textarea.style.height = "";
192
- textarea.style.overflowY = "hidden";
193
- }, [isExpanded, text]);
194
- const canSubmit = !disabled && !isSubmitting && !isRunning && (text.trim().length > 0 || files.length > 0);
195
- const handleFilesChange = (event) => {
196
- const selected = Array.from(event.target.files || []).map(createComposerFile);
197
- if (selected.length > 0) {
198
- setFiles((current) => [...current, ...selected]);
199
- }
200
- event.currentTarget.value = "";
201
- };
202
- const removeFile = (fileId) => {
203
- setFiles((current) => {
204
- const target = current.find((file) => file.id === fileId);
205
- if (target) revokeComposerFiles([target]);
206
- return current.filter((file) => file.id !== fileId);
207
- });
208
- };
209
- const submit = async () => {
210
- if (!canSubmit) return;
211
- const submittedText = text.trim();
212
- const submittedFiles = files;
213
- setIsSubmitting(true);
214
- try {
215
- await onSubmit({ text: submittedText, files: submittedFiles });
216
- setText("");
217
- setFiles([]);
218
- revokeComposerFiles(submittedFiles);
219
- } catch {
220
- } finally {
221
- setIsSubmitting(false);
222
- }
223
- };
224
- const handleKeyDown = (event) => {
225
- if (event.key === "Enter" && !event.shiftKey) {
226
- event.preventDefault();
227
- void submit();
228
- }
229
- };
230
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("a24-composer relative mx-auto w-full max-w-3xl overflow-visible", className), "data-agents24-chat-composer": "", children: [
231
- files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
232
- "div",
233
- {
234
- "data-testid": "agent-chat-composer-attachments",
235
- "data-layout": attachmentLayout,
236
- className: cn(
237
- "a24-composer__attachments flex min-w-0 gap-2 py-1",
238
- attachmentLayout === "overlay" ? "absolute bottom-full left-0 right-0 mb-1.5 flex-nowrap overflow-x-auto overflow-y-hidden overscroll-x-contain px-2" : "mb-2 flex-wrap px-1"
239
- ),
240
- children: files.map((file) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
241
- "div",
242
- {
243
- className: "a24-composer__attachment group relative flex h-8 max-w-[min(20rem,calc(100vw-2rem))] cursor-default select-none items-center gap-1.5 rounded-md border border-border bg-card px-1.5 text-sm font-medium text-card-foreground transition-colors hover:bg-accent",
244
- children: [
245
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "flex size-5 shrink-0 items-center justify-center rounded bg-background text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Paperclip, { className: "size-3" }) }),
246
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "min-w-0 flex-1 truncate", children: file.filename }),
247
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
248
- "button",
249
- {
250
- "aria-label": `Remove ${file.filename}`,
251
- className: "a24-composer__attachment-remove inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground opacity-70 transition-colors hover:bg-muted hover:text-foreground hover:opacity-100 focus-visible:outline-none",
252
- onClick: () => removeFile(file.id),
253
- type: "button",
254
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.X, { className: "size-3" })
255
- }
256
- )
257
- ]
258
- },
259
- file.id
260
- ))
261
- }
262
- ) : null,
263
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
264
- "div",
265
- {
266
- "data-expanded": isExpanded ? "" : void 0,
267
- className: cn(
268
- "a24-composer__surface relative w-full border border-input bg-background shadow-sm",
269
- isExpanded ? "rounded-xl p-2 pb-1.5" : "rounded-xl px-2 py-[3px]"
270
- ),
271
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
272
- "div",
273
- {
274
- className: "a24-composer__grid w-full",
275
- style: {
276
- alignItems: "center",
277
- display: "grid",
278
- gridTemplateAreas: isExpanded ? `"textarea textarea textarea" "plus toolbar submit"` : `"plus textarea composer mic submit"`,
279
- gridTemplateColumns: isExpanded ? "auto 1fr auto" : "auto 1fr auto auto auto",
280
- gap: isExpanded ? "3px 10px" : "8px"
281
- },
282
- children: [
283
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { gridArea: "plus" }, className: "a24-composer__attach-slot flex shrink-0 items-center justify-center", children: [
284
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
285
- "input",
286
- {
287
- ref: fileInputRef,
288
- accept,
289
- "aria-label": "Attach files",
290
- className: "hidden",
291
- multiple: true,
292
- name: "attachments",
293
- onChange: handleFilesChange,
294
- type: "file"
295
- }
296
- ),
297
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
298
- "button",
299
- {
300
- "aria-label": "Attach files",
301
- className: cn("a24-composer__attach", pillButtonClass),
302
- disabled: disabled || isSubmitting || isRunning || !allowAttachments,
303
- onClick: () => fileInputRef.current?.click(),
304
- title: "Attach files",
305
- type: "button",
306
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Plus, { className: "size-4" })
307
- }
308
- )
309
- ] }),
310
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridArea: "textarea" }, className: "a24-composer__textarea-slot min-w-0 w-full", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
311
- "textarea",
312
- {
313
- ref: setTextareaRef,
314
- "aria-label": "Message",
315
- autoComplete: "off",
316
- className: cn(
317
- "a24-composer__textarea w-full resize-none border-0 bg-transparent px-1 text-[15px] leading-relaxed text-foreground shadow-none outline-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0",
318
- isExpanded ? "min-h-9 max-h-[224px] py-1" : "min-h-9 h-9 max-h-9 py-2"
319
- ),
320
- disabled: disabled || isSubmitting || isRunning,
321
- name: "message",
322
- onChange: (event) => setText(event.target.value),
323
- onKeyDown: handleKeyDown,
324
- placeholder,
325
- rows: isExpanded ? 2 : 1,
326
- value: text
327
- }
328
- ) }),
329
- isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridArea: "toolbar" }, className: "a24-composer__toolbar flex min-w-0 items-center gap-1.5", children: inputToolbarContent }) : null,
330
- !isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridArea: "composer" }, className: "flex min-w-0 items-center gap-1.5" }) : null,
331
- !isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridArea: "mic" }, className: "flex shrink-0 items-center justify-end" }) : null,
332
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { gridArea: "submit" }, className: "a24-composer__submit-slot flex shrink-0 items-center justify-end", children: isRunning ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
333
- "button",
334
- {
335
- "aria-label": "Stop generating",
336
- className: cn("a24-composer__submit", submitButtonClass),
337
- disabled: disabled || !onStop,
338
- onClick: onStop,
339
- title: "Stop",
340
- type: "button",
341
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.Square, { className: "size-3.5 fill-current" })
342
- }
343
- ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
344
- "button",
345
- {
346
- "aria-label": files.length > 0 ? `Send ${formatFileCount(files)}` : "Send message",
347
- className: cn("a24-composer__submit", submitButtonClass, text.trim().length > 0 && "hover:scale-105"),
348
- disabled: !canSubmit,
349
- onClick: () => void submit(),
350
- title: "Send",
351
- type: "button",
352
- children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_lucide_react.ArrowUp, { className: "size-4", strokeWidth: 2.5 })
353
- }
354
- ) })
355
- ]
356
- }
357
- )
358
- }
359
- ),
360
- !isExpanded && inputToolbarContent ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "a24-composer__toolbar a24-composer__toolbar--external mt-2 flex items-center gap-2 px-1", children: inputToolbarContent }) : null
361
- ] });
362
- }
363
-
364
- // src/ui/agent-chat-actions.tsx
365
- var React3 = __toESM(require("react"), 1);
366
- var import_lucide_react2 = require("lucide-react");
367
- var import_radix_ui = require("radix-ui");
368
-
369
- // src/ui/adapters.ts
370
- var React2 = __toESM(require("react"), 1);
371
-
372
130
  // src/ui/attachment.tsx
373
- var import_class_variance_authority = require("class-variance-authority");
374
- var import_react_slot = require("@radix-ui/react-slot");
375
- var import_jsx_runtime2 = require("react/jsx-runtime");
131
+ var import_jsx_runtime = require("react/jsx-runtime");
376
132
  var attachmentVariants = (0, import_class_variance_authority.cva)(
377
133
  "group/attachment relative flex w-fit max-w-full min-w-0 shrink-0 flex-wrap rounded-xl border bg-card text-card-foreground transition-colors focus-within:border-neutral-300 focus-within:ring-0 has-[>a,>button]:hover:bg-muted/50 data-[state=error]:border-destructive/30 data-[state=idle]:border-dashed",
378
134
  {
@@ -396,7 +152,7 @@ function Attachment({
396
152
  orientation = "horizontal",
397
153
  ...props
398
154
  }) {
399
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
155
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
400
156
  "div",
401
157
  {
402
158
  "data-slot": "attachment",
@@ -409,12 +165,12 @@ function Attachment({
409
165
  );
410
166
  }
411
167
  var attachmentMediaVariants = (0, import_class_variance_authority.cva)(
412
- "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:bg-destructive/10 group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
168
+ "relative flex aspect-square w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg text-foreground group-data-[orientation=vertical]/attachment:w-full group-data-[size=sm]/attachment:w-8 group-data-[size=xs]/attachment:w-7 group-data-[size=xs]/attachment:rounded-md group-data-[state=error]/attachment:text-destructive group-data-[orientation=vertical]/attachment:*:data-[slot=spinner]:size-6! [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 group-data-[orientation=vertical]/attachment:[&_svg:not([class*='size-'])]:size-6 group-data-[size=xs]/attachment:[&_svg:not([class*='size-'])]:size-3.5",
413
169
  {
414
170
  variants: {
415
171
  variant: {
416
172
  icon: "",
417
- image: "opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover"
173
+ image: "bg-muted opacity-60 group-data-[state=done]/attachment:opacity-100 group-data-[state=idle]/attachment:opacity-100 *:[img]:aspect-square *:[img]:w-full *:[img]:object-cover"
418
174
  }
419
175
  },
420
176
  defaultVariants: {
@@ -427,7 +183,7 @@ function AttachmentMedia({
427
183
  variant = "icon",
428
184
  ...props
429
185
  }) {
430
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
186
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
431
187
  "div",
432
188
  {
433
189
  "data-slot": "attachment-media",
@@ -441,7 +197,7 @@ function AttachmentContent({
441
197
  className,
442
198
  ...props
443
199
  }) {
444
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
200
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
445
201
  "div",
446
202
  {
447
203
  "data-slot": "attachment-content",
@@ -457,7 +213,7 @@ function AttachmentTitle({
457
213
  className,
458
214
  ...props
459
215
  }) {
460
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
216
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
461
217
  "span",
462
218
  {
463
219
  "data-slot": "attachment-title",
@@ -473,7 +229,7 @@ function AttachmentDescription({
473
229
  className,
474
230
  ...props
475
231
  }) {
476
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
232
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
477
233
  "span",
478
234
  {
479
235
  "data-slot": "attachment-description",
@@ -490,7 +246,7 @@ function AttachmentActions({
490
246
  className,
491
247
  ...props
492
248
  }) {
493
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
249
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
494
250
  "div",
495
251
  {
496
252
  "data-slot": "attachment-actions",
@@ -507,7 +263,7 @@ function AttachmentAction({
507
263
  type = "button",
508
264
  ...props
509
265
  }) {
510
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
266
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
511
267
  "button",
512
268
  {
513
269
  "data-slot": "attachment-action",
@@ -527,7 +283,7 @@ function AttachmentTrigger({
527
283
  ...props
528
284
  }) {
529
285
  const Comp = asChild ? import_react_slot.Slot : "button";
530
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
286
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
531
287
  Comp,
532
288
  {
533
289
  "data-slot": "attachment-trigger",
@@ -538,7 +294,7 @@ function AttachmentTrigger({
538
294
  );
539
295
  }
540
296
  function AttachmentGroup({ className, ...props }) {
541
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
297
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
542
298
  "div",
543
299
  {
544
300
  "data-slot": "attachment-group",
@@ -552,9 +308,9 @@ function AttachmentGroup({ className, ...props }) {
552
308
  }
553
309
 
554
310
  // src/ui/message.tsx
555
- var import_jsx_runtime3 = require("react/jsx-runtime");
311
+ var import_jsx_runtime2 = require("react/jsx-runtime");
556
312
  function MessageGroup({ className, ...props }) {
557
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
313
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
558
314
  "div",
559
315
  {
560
316
  "data-slot": "message-group",
@@ -568,7 +324,7 @@ function Message({
568
324
  align = "start",
569
325
  ...props
570
326
  }) {
571
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
327
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
572
328
  "div",
573
329
  {
574
330
  "data-slot": "message",
@@ -582,7 +338,7 @@ function Message({
582
338
  );
583
339
  }
584
340
  function MessageAvatar({ className, ...props }) {
585
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
341
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
586
342
  "div",
587
343
  {
588
344
  "data-slot": "message-avatar",
@@ -595,7 +351,7 @@ function MessageAvatar({ className, ...props }) {
595
351
  );
596
352
  }
597
353
  function MessageContent({ className, ...props }) {
598
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
354
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
599
355
  "div",
600
356
  {
601
357
  "data-slot": "message-content",
@@ -608,7 +364,7 @@ function MessageContent({ className, ...props }) {
608
364
  );
609
365
  }
610
366
  function MessageHeader({ className, ...props }) {
611
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
367
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
612
368
  "div",
613
369
  {
614
370
  "data-slot": "message-header",
@@ -621,7 +377,7 @@ function MessageHeader({ className, ...props }) {
621
377
  );
622
378
  }
623
379
  function MessageFooter({ className, ...props }) {
624
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
380
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
625
381
  "div",
626
382
  {
627
383
  "data-slot": "message-footer",
@@ -635,23 +391,22 @@ function MessageFooter({ className, ...props }) {
635
391
  }
636
392
 
637
393
  // src/ui/adapters.ts
638
- var ChatMessageRoleContext = React2.createContext("assistant");
394
+ var ChatMessageRoleContext = React.createContext("assistant");
639
395
  function ChatMessage(input) {
640
396
  const { className, from, align, ...props } = input;
641
397
  const isUser = from === "user";
642
- const isRtl = props.dir === "rtl";
643
398
  const resolvedAlign = align || "start";
644
399
  const messageProps = Object.assign({}, props);
645
400
  messageProps.align = resolvedAlign;
646
401
  messageProps.className = cn(
647
- "a24-chat-message group flex w-full flex-col gap-2",
648
- isUser ? cn("a24-chat-message--user is-user max-w-[80%] justify-end", isRtl ? "mr-auto" : "ml-auto") : "a24-chat-message--assistant is-assistant max-w-full",
402
+ "a24-chat-message group flex w-full flex-col",
403
+ isUser ? "a24-chat-message--user is-user max-w-full justify-end gap-0" : "a24-chat-message--assistant is-assistant max-w-full gap-2",
649
404
  className
650
405
  );
651
- return React2.createElement(
406
+ return React.createElement(
652
407
  ChatMessageRoleContext.Provider,
653
408
  { value: from },
654
- React2.createElement(Message, messageProps)
409
+ React.createElement(Message, messageProps)
655
410
  );
656
411
  }
657
412
  function ChatMessageContent({
@@ -659,9 +414,9 @@ function ChatMessageContent({
659
414
  from,
660
415
  ...props
661
416
  }) {
662
- const inheritedRole = React2.useContext(ChatMessageRoleContext);
417
+ const inheritedRole = React.useContext(ChatMessageRoleContext);
663
418
  const isUser = (from ?? inheritedRole) === "user";
664
- return React2.createElement("div", {
419
+ return React.createElement("div", {
665
420
  className: cn(
666
421
  "a24-chat-message-content flex max-w-full min-w-0 flex-col gap-2 text-sm",
667
422
  isUser ? "a24-chat-message-content--user is-user:dark w-fit overflow-visible rounded-lg bg-secondary px-4 py-3 text-foreground" : "a24-chat-message-content--assistant w-full overflow-visible text-foreground",
@@ -675,7 +430,7 @@ function ChatMessageActions({
675
430
  className,
676
431
  ...props
677
432
  }) {
678
- return React2.createElement("div", {
433
+ return React.createElement("div", {
679
434
  className: cn("flex items-center gap-1", className),
680
435
  "data-slot": "message-actions",
681
436
  ...props
@@ -690,7 +445,7 @@ function ChatMessageAction({
690
445
  ...props
691
446
  }) {
692
447
  const tooltipLabel = typeof tooltip === "string" ? tooltip : void 0;
693
- return React2.createElement(
448
+ return React.createElement(
694
449
  "button",
695
450
  {
696
451
  "aria-label": label || tooltipLabel,
@@ -703,83 +458,629 @@ function ChatMessageAction({
703
458
  ...props
704
459
  },
705
460
  children,
706
- React2.createElement(
461
+ React.createElement(
707
462
  "span",
708
463
  { className: "sr-only" },
709
464
  label || tooltipLabel
710
465
  )
711
466
  );
712
467
  }
468
+ var resolvedContentCache = /* @__PURE__ */ new WeakMap();
469
+ var CONTENT_CACHE_EXPIRY_BUFFER_MS = 15e3;
470
+ var CONTENT_CACHE_MAX_ENTRIES = 100;
471
+ function attachmentCacheKey(data) {
472
+ const id = typeof data.id === "string" ? data.id.trim() : "";
473
+ return id || null;
474
+ }
475
+ function cachedAttachmentContent(resolver, data) {
476
+ const key = attachmentCacheKey(data);
477
+ if (!key) return null;
478
+ const cached = resolvedContentCache.get(resolver)?.get(key);
479
+ if (!cached) return null;
480
+ if (!cached.validUntil) return cached;
481
+ const validUntil = Date.parse(cached.validUntil);
482
+ if (Number.isFinite(validUntil) && validUntil - CONTENT_CACHE_EXPIRY_BUFFER_MS > Date.now()) {
483
+ return cached;
484
+ }
485
+ resolvedContentCache.get(resolver)?.delete(key);
486
+ return null;
487
+ }
488
+ function cacheAttachmentContent(resolver, data, result) {
489
+ const key = attachmentCacheKey(data);
490
+ if (!key) return;
491
+ const cache = resolvedContentCache.get(resolver) || /* @__PURE__ */ new Map();
492
+ cache.delete(key);
493
+ cache.set(key, result);
494
+ while (cache.size > CONTENT_CACHE_MAX_ENTRIES) {
495
+ const oldestKey = cache.keys().next().value;
496
+ if (typeof oldestKey !== "string") break;
497
+ cache.delete(oldestKey);
498
+ }
499
+ resolvedContentCache.set(resolver, cache);
500
+ }
501
+ function invalidateAttachmentContent(resolver, data) {
502
+ const key = attachmentCacheKey(data);
503
+ if (key) resolvedContentCache.get(resolver)?.delete(key);
504
+ }
505
+ function attachmentMediaType(data) {
506
+ return data.mediaType || data.contentType || String(data.type || "application/octet-stream");
507
+ }
508
+ function attachmentFilename(data) {
509
+ return data.filename || data.name || "Attachment";
510
+ }
511
+ function attachmentExtension(filename) {
512
+ const extension = filename.split(".").pop();
513
+ return extension && extension !== filename ? extension.toLowerCase() : "";
514
+ }
515
+ function resolveAttachmentVisual(data) {
516
+ const mediaType = attachmentMediaType(data).toLowerCase();
517
+ const extension = attachmentExtension(attachmentFilename(data));
518
+ if (mediaType.startsWith("image/") || ["avif", "gif", "heic", "jpeg", "jpg", "png", "svg", "webp"].includes(extension)) {
519
+ return { fileIconType: extension || "image", kind: "image" };
520
+ }
521
+ if (mediaType === "application/pdf" || extension === "pdf") {
522
+ return { fileIconType: "pdf", kind: "pdf" };
523
+ }
524
+ if (mediaType.includes("spreadsheet") || mediaType.includes("csv") || ["csv", "numbers", "ods", "xls", "xlsm", "xlsx"].includes(extension)) {
525
+ if (["csv", "xls", "xlsx"].includes(extension)) {
526
+ return { fileIconType: extension, kind: "spreadsheet" };
527
+ }
528
+ return { fileIconType: "spreadsheets", kind: "spreadsheet" };
529
+ }
530
+ if (mediaType.includes("presentation") || ["key", "odp", "ppt", "pptx"].includes(extension)) {
531
+ return { fileIconType: extension === "ppt" ? "ppt" : "pptx", kind: "presentation" };
532
+ }
533
+ if (mediaType.includes("word") || mediaType.includes("rtf") || ["doc", "docx", "odt", "rtf"].includes(extension)) {
534
+ if (extension === "doc" || extension === "docx") {
535
+ return { fileIconType: extension, kind: "document" };
536
+ }
537
+ return { fileIconType: "document", kind: "document" };
538
+ }
539
+ if (mediaType.startsWith("audio/") || ["aac", "flac", "m4a", "mp3", "ogg", "wav"].includes(extension)) {
540
+ if (extension === "mp3" || extension === "wav") {
541
+ return { fileIconType: extension, kind: "audio" };
542
+ }
543
+ return { fileIconType: "audio", kind: "audio" };
544
+ }
545
+ if (mediaType.startsWith("video/") || ["avi", "m4v", "mkv", "mov", "mp4", "webm"].includes(extension)) {
546
+ if (["avi", "mkv", "mp4", "mpeg"].includes(extension)) {
547
+ return { fileIconType: extension, kind: "video" };
548
+ }
549
+ return { fileIconType: "video", kind: "video" };
550
+ }
551
+ if (mediaType.includes("zip") || mediaType.includes("compressed") || ["7z", "bz2", "gz", "rar", "tar", "tgz", "zip"].includes(extension)) {
552
+ return { fileIconType: extension === "rar" ? "rar" : "zip", kind: "archive" };
553
+ }
554
+ if (mediaType.includes("json") || mediaType.includes("javascript") || mediaType.includes("typescript") || mediaType.includes("xml") || ["css", "html", "js", "jsx", "json", "py", "sql", "ts", "tsx", "xml", "yaml", "yml"].includes(extension)) {
555
+ if (["css", "html", "java", "js", "json", "sql", "xml"].includes(extension)) {
556
+ return { fileIconType: extension, kind: "code" };
557
+ }
558
+ return { fileIconType: "code", kind: "code" };
559
+ }
560
+ if (mediaType.startsWith("text/") || ["md", "markdown", "txt"].includes(extension)) {
561
+ return { fileIconType: "txt", kind: "file" };
562
+ }
563
+ return { fileIconType: "empty", kind: "file" };
564
+ }
565
+ function solidFileIconBackground(fileIconType, kind) {
566
+ if (["txt", "zip", "rar"].includes(fileIconType)) return "#344054";
567
+ if (kind === "pdf") return "#D92D20";
568
+ if (kind === "spreadsheet") return "#079455";
569
+ if (kind === "document" || kind === "video") return "#155EEF";
570
+ if (kind === "presentation") return "#E62E05";
571
+ if (kind === "audio") return "#DD2590";
572
+ if (kind === "code") return "#444CE7";
573
+ return "#7F56D9";
574
+ }
575
+ function isImageAttachment(data) {
576
+ return resolveAttachmentVisual(data).kind === "image";
577
+ }
578
+ function attachmentRenderKey(data, index, rowKind) {
579
+ const id = typeof data.id === "string" ? data.id.trim() : "";
580
+ const identity = id || `${attachmentFilename(data)}:${attachmentMediaType(data)}`;
581
+ return `${rowKind}:${identity}:${index}`;
582
+ }
583
+ function ChatImageAttachment({
584
+ className,
585
+ data,
586
+ onRemove,
587
+ resolveContent,
588
+ ...props
589
+ }) {
590
+ const filename = attachmentFilename(data);
591
+ const initialUrl = typeof data.url === "string" ? data.url.trim() : "";
592
+ const contentStatus = String(data.contentStatus || data.content_status || "available");
593
+ const attachmentKey = attachmentCacheKey(data);
594
+ const dataRef = React.useRef(data);
595
+ dataRef.current = data;
596
+ const rootRef = React.useRef(null);
597
+ const retryCountRef = React.useRef(0);
598
+ const [isNearViewport, setIsNearViewport] = React.useState(Boolean(initialUrl));
599
+ const [sourceUrl, setSourceUrl] = React.useState(initialUrl);
600
+ const [status, setStatus] = React.useState(
601
+ initialUrl ? "loading" : contentStatus === "expired" || !resolveContent ? "error" : "idle"
602
+ );
603
+ const [resolutionAttempt, setResolutionAttempt] = React.useState(0);
604
+ React.useEffect(() => {
605
+ if (initialUrl && resolutionAttempt === 0 || contentStatus === "expired" || !resolveContent) return;
606
+ const node = rootRef.current;
607
+ if (!node || typeof IntersectionObserver === "undefined") {
608
+ setIsNearViewport(true);
609
+ return;
610
+ }
611
+ const observer = new IntersectionObserver(
612
+ (entries) => {
613
+ if (entries.some((entry) => entry.isIntersecting)) {
614
+ setIsNearViewport(true);
615
+ observer.disconnect();
616
+ }
617
+ },
618
+ { rootMargin: "240px" }
619
+ );
620
+ observer.observe(node);
621
+ return () => observer.disconnect();
622
+ }, [contentStatus, initialUrl, resolutionAttempt, resolveContent]);
623
+ React.useEffect(() => {
624
+ if (initialUrl && resolutionAttempt === 0) {
625
+ setSourceUrl(initialUrl);
626
+ setStatus("loading");
627
+ return;
628
+ }
629
+ if (!isNearViewport || !resolveContent || contentStatus === "expired") return;
630
+ const attachment = dataRef.current;
631
+ const cached = cachedAttachmentContent(resolveContent, attachment);
632
+ if (cached) {
633
+ setSourceUrl(cached.url);
634
+ setStatus("loading");
635
+ return;
636
+ }
637
+ const controller = new AbortController();
638
+ setStatus("resolving");
639
+ void resolveContent(attachment, controller.signal).then((result) => {
640
+ if (controller.signal.aborted) return;
641
+ if (!result.url?.trim()) throw new Error("Attachment content resolver returned an empty URL");
642
+ cacheAttachmentContent(resolveContent, attachment, result);
643
+ setSourceUrl(result.url);
644
+ setStatus("loading");
645
+ }).catch(() => {
646
+ if (!controller.signal.aborted) setStatus("error");
647
+ });
648
+ return () => controller.abort();
649
+ }, [attachmentKey, contentStatus, initialUrl, isNearViewport, resolutionAttempt, resolveContent]);
650
+ const handleImageError = () => {
651
+ if (resolveContent && retryCountRef.current === 0) {
652
+ retryCountRef.current = 1;
653
+ invalidateAttachmentContent(resolveContent, dataRef.current);
654
+ setSourceUrl("");
655
+ setStatus("resolving");
656
+ setResolutionAttempt((current) => current + 1);
657
+ return;
658
+ }
659
+ setStatus("error");
660
+ };
661
+ return React.createElement(
662
+ Attachment,
663
+ {
664
+ ...props,
665
+ className: cn("a24-chat-image-attachment", className),
666
+ orientation: "vertical",
667
+ ref: rootRef,
668
+ state: status === "error" ? "error" : status === "done" ? "done" : "processing",
669
+ title: filename
670
+ },
671
+ React.createElement(
672
+ AttachmentMedia,
673
+ { className: "a24-chat-image-media", variant: "image" },
674
+ sourceUrl ? React.createElement("img", {
675
+ alt: filename,
676
+ decoding: "async",
677
+ loading: "lazy",
678
+ onError: handleImageError,
679
+ onLoad: () => setStatus("done"),
680
+ src: sourceUrl
681
+ }) : status === "error" ? React.createElement(import_lucide_react.ImageOff, { "aria-hidden": "true", className: "size-5" }) : React.createElement(import_lucide_react.LoaderCircle, {
682
+ "aria-hidden": "true",
683
+ className: "a24-chat-image-spinner size-5 animate-spin"
684
+ }),
685
+ status !== "done" && status !== "error" ? React.createElement("span", { className: "sr-only", role: "status" }, `Loading ${filename}`) : null,
686
+ status === "error" ? React.createElement("span", { className: "sr-only" }, `${filename} preview unavailable`) : null
687
+ ),
688
+ onRemove ? React.createElement(
689
+ "button",
690
+ {
691
+ "aria-label": `Remove ${filename}`,
692
+ className: "a24-chat-image-remove",
693
+ onClick: (event) => {
694
+ event.stopPropagation();
695
+ onRemove();
696
+ },
697
+ type: "button"
698
+ },
699
+ React.createElement(import_lucide_react.X, { "aria-hidden": "true", className: "size-3.5" })
700
+ ) : null
701
+ );
702
+ }
713
703
  function ChatAttachment({
704
+ className,
714
705
  data,
715
706
  onRemove,
716
707
  orientation,
708
+ resolveContent,
717
709
  ...props
718
710
  }) {
719
- const mediaType = data.mediaType || data.contentType || String(data.type || "application/octet-stream");
720
- const filename = data.filename || data.name || "Attachment";
721
- const isImage = mediaType.startsWith("image/") && Boolean(data.url);
722
- return React2.createElement(
711
+ const filename = attachmentFilename(data);
712
+ const { fileIconType, kind } = resolveAttachmentVisual(data);
713
+ if (kind === "image") {
714
+ return React.createElement(ChatImageAttachment, {
715
+ ...props,
716
+ className,
717
+ data,
718
+ onRemove,
719
+ resolveContent
720
+ });
721
+ }
722
+ return React.createElement(
723
723
  Attachment,
724
724
  {
725
- orientation: orientation || (isImage ? "vertical" : "horizontal"),
725
+ className: cn("a24-chat-attachment", className),
726
+ orientation: orientation || "horizontal",
727
+ size: "sm",
726
728
  ...props
727
729
  },
728
- React2.createElement(
730
+ React.createElement(
729
731
  AttachmentMedia,
730
- { variant: isImage ? "image" : "icon" },
731
- isImage ? React2.createElement("img", {
732
- alt: filename,
733
- className: "size-full object-cover",
734
- src: data.url
735
- }) : React2.createElement(
736
- "span",
737
- { "aria-hidden": "true", className: "text-sm" },
738
- filename.slice(0, 1).toUpperCase()
739
- )
732
+ Object.assign(
733
+ {
734
+ className: "a24-chat-attachment-icon",
735
+ style: { backgroundColor: solidFileIconBackground(fileIconType, kind) },
736
+ variant: "icon"
737
+ },
738
+ { "data-file-kind": kind }
739
+ ),
740
+ React.createElement(import_file_icons.FileIcon, {
741
+ "aria-hidden": "true",
742
+ size: 28,
743
+ type: fileIconType,
744
+ variant: "solid"
745
+ })
740
746
  ),
741
- React2.createElement(
747
+ React.createElement(
742
748
  AttachmentContent,
743
- null,
744
- React2.createElement(AttachmentTitle, null, filename),
745
- React2.createElement(AttachmentDescription, null, mediaType)
749
+ { className: "a24-chat-attachment-content" },
750
+ React.createElement(AttachmentTitle, { title: filename }, filename)
746
751
  ),
747
- onRemove ? React2.createElement(
752
+ onRemove ? React.createElement(
748
753
  "button",
749
754
  {
750
755
  "aria-label": "Remove attachment",
751
- className: "relative z-20 mr-1 inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
756
+ className: "relative z-20 inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground hover:bg-muted hover:text-foreground",
752
757
  onClick: (event) => {
753
758
  event.stopPropagation();
754
759
  onRemove();
755
760
  },
756
761
  type: "button"
757
762
  },
758
- React2.createElement("span", { "aria-hidden": "true" }, "x")
763
+ React.createElement(import_lucide_react.X, { "aria-hidden": "true", className: "size-3.5" })
759
764
  ) : null
760
765
  );
761
766
  }
762
- function ChatAttachments({
763
- children,
767
+ function ChatAttachments({
768
+ children,
769
+ className,
770
+ ...props
771
+ }) {
772
+ if (!children) return null;
773
+ return React.createElement(
774
+ AttachmentGroup,
775
+ {
776
+ className: cn(
777
+ "a24-chat-attachments w-full flex-nowrap",
778
+ className
779
+ ),
780
+ ...props
781
+ },
782
+ children
783
+ );
784
+ }
785
+ function ChatAttachmentRows({
786
+ attachments,
787
+ className,
788
+ dir,
789
+ onRemove,
790
+ resolveContent,
791
+ ...props
792
+ }) {
793
+ if (attachments.length === 0) return null;
794
+ const imageAttachments = attachments.filter(isImageAttachment);
795
+ const fileAttachments = attachments.filter((attachment) => !isImageAttachment(attachment));
796
+ const renderRow = (items, kind) => items.length > 0 ? React.createElement(
797
+ ChatAttachments,
798
+ Object.assign({ dir, key: kind }, { "data-attachment-row": kind }),
799
+ items.map(
800
+ (attachment, index) => React.createElement(ChatAttachment, {
801
+ data: attachment,
802
+ dir: "ltr",
803
+ key: attachmentRenderKey(attachment, index, kind),
804
+ onRemove: onRemove ? () => onRemove(attachment) : void 0,
805
+ resolveContent
806
+ })
807
+ )
808
+ ) : null;
809
+ return React.createElement(
810
+ "div",
811
+ {
812
+ className: cn("a24-chat-attachment-rows", className),
813
+ dir,
814
+ ...props
815
+ },
816
+ renderRow(imageAttachments, "images"),
817
+ renderRow(fileAttachments, "files")
818
+ );
819
+ }
820
+
821
+ // src/ui/composer-attachments.ts
822
+ var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `file-${Date.now()}-${Math.random().toString(36).slice(2)}`;
823
+ function createAgentChatComposerFile(file) {
824
+ return {
825
+ id: createId(),
826
+ type: "file",
827
+ url: URL.createObjectURL(file),
828
+ filename: file.name || "attachment",
829
+ mediaType: file.type || "application/octet-stream",
830
+ source: file
831
+ };
832
+ }
833
+ function revokeAgentChatComposerFiles(files) {
834
+ files.forEach((file) => {
835
+ try {
836
+ URL.revokeObjectURL(file.url);
837
+ } catch {
838
+ }
839
+ });
840
+ }
841
+
842
+ // src/ui/agent-chat-composer.tsx
843
+ var import_jsx_runtime3 = require("react/jsx-runtime");
844
+ function formatFileCount(files) {
845
+ if (files.length === 1) return files[0].filename;
846
+ return `${files.length} files`;
847
+ }
848
+ var pillButtonClass = "inline-flex size-8 shrink-0 items-center justify-center gap-2 rounded-lg border-0 bg-muted text-sm text-muted-foreground shadow-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50";
849
+ var submitButtonClass = "inline-flex size-9 shrink-0 items-center justify-center gap-2 rounded-lg border-0 bg-primary text-sm text-primary-foreground shadow-none transition-[background-color,color,opacity,transform] duration-200 hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-0 disabled:pointer-events-none disabled:opacity-50";
850
+ function AgentChatComposer({
851
+ accept,
852
+ allowAttachments = true,
853
+ attachmentLayout = "flow",
764
854
  className,
765
- ...props
855
+ disabled = false,
856
+ forceExpanded = false,
857
+ inputToolbarContent,
858
+ isRunning = false,
859
+ onAttachmentCountChange,
860
+ onStop,
861
+ onSubmit,
862
+ placeholder = "Message the agent",
863
+ textareaRef
766
864
  }) {
767
- if (!children) return null;
768
- return React2.createElement(
769
- AttachmentGroup,
770
- {
771
- className: cn(
772
- props.dir === "rtl" ? "mr-auto" : "ml-auto",
773
- "w-fit flex-wrap overflow-visible",
774
- className
775
- ),
776
- ...props
865
+ const fileInputRef = React2.useRef(null);
866
+ const filesRef = React2.useRef([]);
867
+ const inFlightFilesRef = React2.useRef([]);
868
+ const localTextareaRef = React2.useRef(null);
869
+ const draftRevisionRef = React2.useRef(0);
870
+ const [text, setText] = React2.useState("");
871
+ const [files, setFiles] = React2.useState([]);
872
+ const [isSubmitting, setIsSubmitting] = React2.useState(false);
873
+ const isExpanded = forceExpanded || text.includes("\n") || text.length > 62;
874
+ const setTextareaRef = React2.useCallback(
875
+ (node) => {
876
+ localTextareaRef.current = node;
877
+ if (typeof textareaRef === "function") {
878
+ textareaRef(node);
879
+ } else if (textareaRef) {
880
+ textareaRef.current = node;
881
+ }
777
882
  },
778
- children
883
+ [textareaRef]
779
884
  );
885
+ React2.useEffect(() => {
886
+ filesRef.current = files;
887
+ onAttachmentCountChange?.(files.length);
888
+ }, [files, onAttachmentCountChange]);
889
+ React2.useEffect(() => () => {
890
+ revokeAgentChatComposerFiles(filesRef.current);
891
+ revokeAgentChatComposerFiles(inFlightFilesRef.current);
892
+ }, []);
893
+ React2.useLayoutEffect(() => {
894
+ const textarea = localTextareaRef.current;
895
+ if (!textarea) return;
896
+ if (isExpanded) {
897
+ textarea.style.height = "auto";
898
+ const scrollHeight = textarea.scrollHeight;
899
+ textarea.style.height = `${Math.min(scrollHeight, 224)}px`;
900
+ textarea.style.overflowY = scrollHeight >= 224 ? "auto" : "hidden";
901
+ return;
902
+ }
903
+ textarea.style.height = "";
904
+ textarea.style.overflowY = "hidden";
905
+ }, [isExpanded, text]);
906
+ const canSubmit = !disabled && !isSubmitting && !isRunning && (text.trim().length > 0 || files.length > 0);
907
+ const handleFilesChange = (event) => {
908
+ const selected = Array.from(event.target.files || []).map(createAgentChatComposerFile);
909
+ if (selected.length > 0) {
910
+ draftRevisionRef.current += 1;
911
+ setFiles((current) => [...current, ...selected]);
912
+ }
913
+ event.currentTarget.value = "";
914
+ };
915
+ const removeFile = (fileId) => {
916
+ draftRevisionRef.current += 1;
917
+ setFiles((current) => {
918
+ const target = current.find((file) => file.id === fileId);
919
+ if (target) revokeAgentChatComposerFiles([target]);
920
+ return current.filter((file) => file.id !== fileId);
921
+ });
922
+ };
923
+ const submit = async () => {
924
+ if (!canSubmit) return;
925
+ const submittedText = text.trim();
926
+ const submittedFiles = files;
927
+ const clearedRevision = draftRevisionRef.current + 1;
928
+ draftRevisionRef.current = clearedRevision;
929
+ inFlightFilesRef.current = submittedFiles;
930
+ setIsSubmitting(true);
931
+ setText("");
932
+ setFiles([]);
933
+ try {
934
+ await onSubmit({ text: submittedText, files: submittedFiles });
935
+ revokeAgentChatComposerFiles(submittedFiles);
936
+ inFlightFilesRef.current = [];
937
+ } catch {
938
+ if (draftRevisionRef.current === clearedRevision) {
939
+ draftRevisionRef.current += 1;
940
+ setText(submittedText);
941
+ setFiles(submittedFiles);
942
+ inFlightFilesRef.current = [];
943
+ } else {
944
+ revokeAgentChatComposerFiles(submittedFiles);
945
+ inFlightFilesRef.current = [];
946
+ }
947
+ } finally {
948
+ setIsSubmitting(false);
949
+ }
950
+ };
951
+ const handleKeyDown = (event) => {
952
+ if (event.key === "Enter" && !event.shiftKey) {
953
+ event.preventDefault();
954
+ void submit();
955
+ }
956
+ };
957
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("a24-composer relative mx-auto w-full max-w-3xl overflow-visible", className), "data-agents24-chat-composer": "", children: [
958
+ files.length > 0 ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
959
+ "div",
960
+ {
961
+ "data-testid": "agent-chat-composer-attachments",
962
+ "data-layout": attachmentLayout,
963
+ className: cn(
964
+ "a24-composer__attachments min-w-0 py-1",
965
+ attachmentLayout === "overlay" ? "absolute bottom-full left-0 right-0 mb-1.5 flex-nowrap overflow-x-auto overflow-y-hidden overscroll-x-contain px-2" : "mb-2 flex-wrap px-1"
966
+ ),
967
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
968
+ ChatAttachmentRows,
969
+ {
970
+ attachments: files,
971
+ onRemove: (attachment) => attachment.id && removeFile(attachment.id)
972
+ }
973
+ )
974
+ }
975
+ ) : null,
976
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
977
+ "div",
978
+ {
979
+ "data-expanded": isExpanded ? "" : void 0,
980
+ className: cn(
981
+ "a24-composer__surface relative w-full border border-input bg-background shadow-sm",
982
+ isExpanded ? "rounded-xl p-2 pb-1.5" : "rounded-xl px-2 py-[3px]"
983
+ ),
984
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
985
+ "div",
986
+ {
987
+ className: "a24-composer__grid w-full",
988
+ style: {
989
+ alignItems: "center",
990
+ display: "grid",
991
+ gridTemplateAreas: isExpanded ? `"textarea textarea textarea" "plus toolbar submit"` : `"plus textarea composer mic submit"`,
992
+ gridTemplateColumns: isExpanded ? "auto 1fr auto" : "auto 1fr auto auto auto",
993
+ gap: isExpanded ? "3px 10px" : "8px"
994
+ },
995
+ children: [
996
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { style: { gridArea: "plus" }, className: "a24-composer__attach-slot flex shrink-0 items-center justify-center", children: [
997
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
998
+ "input",
999
+ {
1000
+ ref: fileInputRef,
1001
+ accept,
1002
+ "aria-label": "Attach files",
1003
+ className: "hidden",
1004
+ multiple: true,
1005
+ name: "attachments",
1006
+ onChange: handleFilesChange,
1007
+ type: "file"
1008
+ }
1009
+ ),
1010
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1011
+ "button",
1012
+ {
1013
+ "aria-label": "Attach files",
1014
+ className: cn("a24-composer__attach", pillButtonClass),
1015
+ disabled: disabled || isSubmitting || isRunning || !allowAttachments,
1016
+ onClick: () => fileInputRef.current?.click(),
1017
+ title: "Attach files",
1018
+ type: "button",
1019
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Plus, { className: "size-4" })
1020
+ }
1021
+ )
1022
+ ] }),
1023
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { gridArea: "textarea" }, className: "a24-composer__textarea-slot min-w-0 w-full", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1024
+ "textarea",
1025
+ {
1026
+ ref: setTextareaRef,
1027
+ "aria-label": "Message",
1028
+ autoComplete: "off",
1029
+ className: cn(
1030
+ "a24-composer__textarea w-full resize-none border-0 bg-transparent px-1 text-[15px] leading-relaxed text-foreground shadow-none outline-none placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-0",
1031
+ isExpanded ? "min-h-9 max-h-[224px] py-1" : "min-h-9 h-9 max-h-9 py-2"
1032
+ ),
1033
+ disabled: disabled || isSubmitting || isRunning,
1034
+ name: "message",
1035
+ onChange: (event) => {
1036
+ draftRevisionRef.current += 1;
1037
+ setText(event.target.value);
1038
+ },
1039
+ onKeyDown: handleKeyDown,
1040
+ placeholder,
1041
+ rows: isExpanded ? 2 : 1,
1042
+ value: text
1043
+ }
1044
+ ) }),
1045
+ isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { gridArea: "toolbar" }, className: "a24-composer__toolbar flex min-w-0 items-center gap-1.5", children: inputToolbarContent }) : null,
1046
+ !isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { gridArea: "composer" }, className: "flex min-w-0 items-center gap-1.5" }) : null,
1047
+ !isExpanded ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { gridArea: "mic" }, className: "flex shrink-0 items-center justify-end" }) : null,
1048
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { gridArea: "submit" }, className: "a24-composer__submit-slot flex shrink-0 items-center justify-end", children: isRunning ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1049
+ "button",
1050
+ {
1051
+ "aria-label": "Stop generating",
1052
+ className: cn("a24-composer__submit", submitButtonClass),
1053
+ disabled: disabled || !onStop,
1054
+ onClick: onStop,
1055
+ title: "Stop",
1056
+ type: "button",
1057
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.Square, { className: "size-3.5 fill-current" })
1058
+ }
1059
+ ) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1060
+ "button",
1061
+ {
1062
+ "aria-label": files.length > 0 ? `Send ${formatFileCount(files)}` : "Send message",
1063
+ className: cn("a24-composer__submit", submitButtonClass, text.trim().length > 0 && "hover:scale-105"),
1064
+ disabled: !canSubmit,
1065
+ onClick: () => void submit(),
1066
+ title: "Send",
1067
+ type: "button",
1068
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_lucide_react2.ArrowUp, { className: "size-4", strokeWidth: 2.5 })
1069
+ }
1070
+ ) })
1071
+ ]
1072
+ }
1073
+ )
1074
+ }
1075
+ ),
1076
+ !isExpanded && inputToolbarContent ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "a24-composer__toolbar a24-composer__toolbar--external mt-2 flex items-center gap-2 px-1", children: inputToolbarContent }) : null
1077
+ ] });
780
1078
  }
781
1079
 
782
1080
  // src/ui/agent-chat-actions.tsx
1081
+ var React3 = __toESM(require("react"), 1);
1082
+ var import_lucide_react3 = require("lucide-react");
1083
+ var import_radix_ui = require("radix-ui");
783
1084
  var import_jsx_runtime4 = require("react/jsx-runtime");
784
1085
  var FEEDBACK_REASONS = [
785
1086
  ["incorrect", "Incorrect"],
@@ -789,6 +1090,15 @@ var FEEDBACK_REASONS = [
789
1090
  ["citation_issue", "Citation issue"],
790
1091
  ["other", "Other"]
791
1092
  ];
1093
+ function formatSentAt(value) {
1094
+ if (!value) return null;
1095
+ const date = value instanceof Date ? value : new Date(value);
1096
+ if (Number.isNaN(date.getTime())) return null;
1097
+ return {
1098
+ dateTime: date.toISOString(),
1099
+ label: new Intl.DateTimeFormat(void 0, { timeStyle: "short" }).format(date)
1100
+ };
1101
+ }
792
1102
  function AgentChatDefaultActions({
793
1103
  additionalActions,
794
1104
  dir,
@@ -799,11 +1109,43 @@ function AgentChatDefaultActions({
799
1109
  const [feedbackTrigger, setFeedbackTrigger] = React3.useState(null);
800
1110
  const [reason, setReason] = React3.useState(handlers?.feedback?.reason ?? null);
801
1111
  const [comment, setComment] = React3.useState(handlers?.feedback?.comment ?? "");
1112
+ const [optimisticCopied, setOptimisticCopied] = React3.useState(false);
1113
+ const copyResetTimer = React3.useRef(null);
802
1114
  React3.useEffect(() => {
803
1115
  setReason(handlers?.feedback?.reason ?? null);
804
1116
  setComment(handlers?.feedback?.comment ?? "");
805
1117
  }, [handlers?.feedback?.comment, handlers?.feedback?.reason]);
806
- if (!handlers || message.role !== "assistant" || message.isFinal === false) return null;
1118
+ React3.useEffect(() => () => {
1119
+ if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current);
1120
+ }, []);
1121
+ const copied = handlers?.copied ?? optimisticCopied;
1122
+ const copyMessage = () => {
1123
+ handlers?.onCopy?.(message.content || "", message.id);
1124
+ if (handlers?.copied !== void 0) return;
1125
+ setOptimisticCopied(true);
1126
+ if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current);
1127
+ copyResetTimer.current = window.setTimeout(() => {
1128
+ copyResetTimer.current = null;
1129
+ setOptimisticCopied(false);
1130
+ }, 1200);
1131
+ };
1132
+ if (!handlers || message.isFinal === false) return null;
1133
+ if (message.role === "user") {
1134
+ const sentAt = formatSentAt(message.createdAt);
1135
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(ChatMessageActions, { className: "a24-user-message-actions", children: [
1136
+ sentAt ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("time", { dateTime: sentAt.dateTime, children: sentAt.label }) : null,
1137
+ handlers.onCopy ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
1138
+ ChatMessageAction,
1139
+ {
1140
+ label: "Copy",
1141
+ onClick: copyMessage,
1142
+ tooltip: copied ? "Copied!" : "Copy to clipboard",
1143
+ className: "a24-user-message-copy",
1144
+ children: copied ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Check, { className: "a24-user-message-copy__check text-green-600" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Copy, { className: "a24-user-message-copy__icon" })
1145
+ }
1146
+ ) : null
1147
+ ] });
1148
+ }
807
1149
  const canWriteFeedback = Boolean(message.runId && handlers.onSetFeedback);
808
1150
  return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(ChatMessageActions, { children: [
809
1151
  handlers.onRetry ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -812,7 +1154,7 @@ function AgentChatDefaultActions({
812
1154
  label: "Retry",
813
1155
  onClick: () => void handlers.onRetry?.(message),
814
1156
  tooltip: "Regenerate response",
815
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.RefreshCcw, { className: "size-4" })
1157
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.RefreshCcw, { className: "size-4" })
816
1158
  }
817
1159
  ) : null,
818
1160
  canWriteFeedback ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -823,7 +1165,7 @@ function AgentChatDefaultActions({
823
1165
  rating: handlers.feedback?.rating === "like" ? null : "like"
824
1166
  }),
825
1167
  tooltip: "Like this response",
826
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.ThumbsUp, { className: "size-4", fill: handlers.feedback?.rating === "like" ? "currentColor" : "none" })
1168
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.ThumbsUp, { className: "size-4", fill: handlers.feedback?.rating === "like" ? "currentColor" : "none" })
827
1169
  }
828
1170
  ) : null,
829
1171
  canWriteFeedback ? /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_radix_ui.Popover.Root, { onOpenChange: setFeedbackOpen, open: feedbackOpen, children: [
@@ -839,7 +1181,7 @@ function AgentChatDefaultActions({
839
1181
  },
840
1182
  ref: setFeedbackTrigger,
841
1183
  tooltip: "Dislike this response",
842
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.ThumbsDown, { className: "size-4", fill: handlers.feedback?.rating === "dislike" ? "currentColor" : "none" })
1184
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.ThumbsDown, { className: "size-4", fill: handlers.feedback?.rating === "dislike" ? "currentColor" : "none" })
843
1185
  }
844
1186
  ) }),
845
1187
  /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
@@ -914,9 +1256,9 @@ function AgentChatDefaultActions({
914
1256
  ChatMessageAction,
915
1257
  {
916
1258
  label: "Copy",
917
- onClick: () => handlers.onCopy?.(message.content || "", message.id),
918
- tooltip: handlers.copied ? "Copied!" : "Copy to clipboard",
919
- children: handlers.copied ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.Check, { className: "size-4 text-green-600" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react2.Copy, { className: "size-4" })
1259
+ onClick: copyMessage,
1260
+ tooltip: copied ? "Copied!" : "Copy to clipboard",
1261
+ children: copied ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Check, { className: "size-4 text-green-600" }) : /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_lucide_react3.Copy, { className: "size-4" })
920
1262
  }
921
1263
  ) : null
922
1264
  ] });
@@ -924,7 +1266,7 @@ function AgentChatDefaultActions({
924
1266
 
925
1267
  // src/ui/agent-chat-message.tsx
926
1268
  var React8 = __toESM(require("react"), 1);
927
- var import_lucide_react5 = require("lucide-react");
1269
+ var import_lucide_react6 = require("lucide-react");
928
1270
 
929
1271
  // src/ui/agent-response-timeline.tsx
930
1272
  var React7 = __toESM(require("react"), 1);
@@ -1113,7 +1455,7 @@ function useStreamingText({
1113
1455
 
1114
1456
  // src/ui/ask-user-interaction.tsx
1115
1457
  var React4 = __toESM(require("react"), 1);
1116
- var import_lucide_react3 = require("lucide-react");
1458
+ var import_lucide_react4 = require("lucide-react");
1117
1459
 
1118
1460
  // src/ui/ask-user-interaction-state.ts
1119
1461
  function emptyAskUserDrafts(questions) {
@@ -1207,8 +1549,8 @@ function ResolvedQuestions({
1207
1549
  " question",
1208
1550
  count === 1 ? "" : "s"
1209
1551
  ] }),
1210
- open ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react3.ChevronDown, { "aria-hidden": "true", className: "size-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1211
- import_lucide_react3.ChevronRight,
1552
+ open ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react4.ChevronDown, { "aria-hidden": "true", className: "size-3.5" }) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1553
+ import_lucide_react4.ChevronRight,
1212
1554
  {
1213
1555
  "aria-hidden": "true",
1214
1556
  className: "size-3.5 opacity-70 rtl:rotate-180"
@@ -1323,7 +1665,7 @@ function AskUserInteraction({
1323
1665
  onClick: () => setStep((current) => Math.max(0, current - 1)),
1324
1666
  type: "button",
1325
1667
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1326
- import_lucide_react3.ChevronLeft,
1668
+ import_lucide_react4.ChevronLeft,
1327
1669
  {
1328
1670
  "aria-hidden": "true",
1329
1671
  className: "size-4 rtl:rotate-180"
@@ -1352,7 +1694,7 @@ function AskUserInteraction({
1352
1694
  onClick: () => completeStep(drafts),
1353
1695
  type: "button",
1354
1696
  children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1355
- import_lucide_react3.ChevronRight,
1697
+ import_lucide_react4.ChevronRight,
1356
1698
  {
1357
1699
  "aria-hidden": "true",
1358
1700
  className: "size-4 rtl:rotate-180"
@@ -1389,7 +1731,7 @@ function AskUserInteraction({
1389
1731
  ),
1390
1732
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "min-w-0 flex-1 font-medium text-foreground", children: option.label }),
1391
1733
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1392
- import_lucide_react3.ChevronRight,
1734
+ import_lucide_react4.ChevronRight,
1393
1735
  {
1394
1736
  "aria-hidden": "true",
1395
1737
  className: "size-4 shrink-0 text-muted-foreground opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100 rtl:rotate-180"
@@ -1401,7 +1743,7 @@ function AskUserInteraction({
1401
1743
  );
1402
1744
  }),
1403
1745
  /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-h-11 items-center gap-2 rounded-xl bg-muted px-2.5 py-1.5 focus-within:ring-1 focus-within:ring-ring", children: [
1404
- /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-full border border-border/70 bg-background/70 text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react3.Pencil, { "aria-hidden": "true", className: "size-4" }) }),
1746
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "flex size-8 shrink-0 items-center justify-center rounded-full border border-border/70 bg-background/70 text-muted-foreground", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react4.Pencil, { "aria-hidden": "true", className: "size-4" }) }),
1405
1747
  /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
1406
1748
  "input",
1407
1749
  {
@@ -1653,7 +1995,7 @@ function resolvedHitlLabel(part) {
1653
1995
 
1654
1996
  // src/ui/tool-sessions.tsx
1655
1997
  var React6 = __toESM(require("react"), 1);
1656
- var import_lucide_react4 = require("lucide-react");
1998
+ var import_lucide_react5 = require("lucide-react");
1657
1999
  var import_thinking_orbs = require("thinking-orbs");
1658
2000
  var import_jsx_runtime8 = require("react/jsx-runtime");
1659
2001
  function agentChatToolLabel(part) {
@@ -1821,7 +2163,7 @@ function AgentChatNodeActivityRow({
1821
2163
  type: "button",
1822
2164
  children: [
1823
2165
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "a24-tool-trigger__label", children: label }),
1824
- open ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.ChevronDown, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.ChevronRight, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" })
2166
+ open ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react5.ChevronDown, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react5.ChevronRight, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" })
1825
2167
  ]
1826
2168
  }
1827
2169
  ) }),
@@ -1849,10 +2191,9 @@ function ToolDisclosure({
1849
2191
  const [open, setOpen] = React6.useState(false);
1850
2192
  const contentId = React6.useId();
1851
2193
  const expandable = React6.Children.toArray(children).length > 0;
1852
- const textOnlyRoot = Boolean(root && !grouped && !showOrb);
1853
2194
  if (!expandable) {
1854
2195
  const triggerClassName = cn("a24-tool-trigger", root && "a24-tool-trigger--root", root && !showOrb && "a24-tool-trigger--without-orb", error && "a24-tool-trigger--error", loading && "a24-tool-trigger--active");
1855
- return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
2196
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "a24-tool-trigger-row", children: [
1856
2197
  onSelect && part ? /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("button", { className: triggerClassName, onClick: () => onSelect(part), type: "button", children: [
1857
2198
  showOrb ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ToolActivityOrb, { label: statusLabel }) : null,
1858
2199
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "a24-tool-trigger__label", children: label })
@@ -1864,7 +2205,7 @@ function ToolDisclosure({
1864
2205
  ] });
1865
2206
  }
1866
2207
  return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "a24-tool-disclosure", children: [
1867
- /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
2208
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)("div", { className: "a24-tool-trigger-row", children: [
1868
2209
  /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
1869
2210
  "button",
1870
2211
  {
@@ -1881,7 +2222,7 @@ function ToolDisclosure({
1881
2222
  children: [
1882
2223
  showOrb ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(ToolActivityOrb, { label: statusLabel }) : null,
1883
2224
  /* @__PURE__ */ (0, import_jsx_runtime8.jsx)("span", { className: "a24-tool-trigger__label", children: label }),
1884
- open ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.ChevronDown, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.ChevronRight, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" })
2225
+ open ? /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react5.ChevronDown, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" }) : /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react5.ChevronRight, { "aria-hidden": "true", className: "a24-tool-trigger__chevron" })
1885
2226
  ]
1886
2227
  }
1887
2228
  ),
@@ -2342,7 +2683,7 @@ function AgentChatUserMessageContent({
2342
2683
  const [isExpanded, setIsExpanded] = React8.useState(false);
2343
2684
  const contentId = React8.useId();
2344
2685
  const shouldCollapse = shouldCollapseUserMessage(content, collapseThreshold);
2345
- const Icon = isExpanded ? import_lucide_react5.ChevronUp : import_lucide_react5.ChevronDown;
2686
+ const Icon = isExpanded ? import_lucide_react6.ChevronUp : import_lucide_react6.ChevronDown;
2346
2687
  const clampedStyle = shouldCollapse && !isExpanded ? {
2347
2688
  WebkitBoxOrient: "vertical",
2348
2689
  WebkitLineClamp: Math.max(1, collapsedLines),
@@ -2370,11 +2711,47 @@ function AgentChatUserMessageContent({
2370
2711
  ) : null
2371
2712
  ] });
2372
2713
  }
2714
+ function AgentChatUserMessageActionRow({
2715
+ children,
2716
+ className
2717
+ }) {
2718
+ const rowRef = React8.useRef(null);
2719
+ const [visible, setVisible] = React8.useState(false);
2720
+ React8.useEffect(() => {
2721
+ const message = rowRef.current?.closest('[data-slot="message"]');
2722
+ if (!message) return;
2723
+ const show = () => setVisible(true);
2724
+ const hide = () => setVisible(false);
2725
+ const hideWhenFocusLeaves = (event) => {
2726
+ if (!message.contains(event.relatedTarget)) hide();
2727
+ };
2728
+ message.addEventListener("mouseenter", show);
2729
+ message.addEventListener("mouseleave", hide);
2730
+ message.addEventListener("focusin", show);
2731
+ message.addEventListener("focusout", hideWhenFocusLeaves);
2732
+ return () => {
2733
+ message.removeEventListener("mouseenter", show);
2734
+ message.removeEventListener("mouseleave", hide);
2735
+ message.removeEventListener("focusin", show);
2736
+ message.removeEventListener("focusout", hideWhenFocusLeaves);
2737
+ };
2738
+ }, []);
2739
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2740
+ "div",
2741
+ {
2742
+ ref: rowRef,
2743
+ className: cn("a24-chat-message-action-row--user", visible && "is-visible", className),
2744
+ style: { opacity: visible ? 1 : 0, pointerEvents: visible ? "auto" : "none" },
2745
+ children
2746
+ }
2747
+ );
2748
+ }
2373
2749
  function AgentChatMessage({
2374
2750
  actionHandlers,
2375
2751
  additionalActions,
2376
2752
  actions,
2377
2753
  actionsClassName,
2754
+ attachmentContentResolver,
2378
2755
  className,
2379
2756
  contentClassName,
2380
2757
  dir,
@@ -2407,48 +2784,58 @@ function AgentChatMessage({
2407
2784
  message
2408
2785
  }
2409
2786
  ) : null);
2410
- return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(ChatMessage, { className, dir, from: message.role, children: [
2411
- showAttachments ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(ChatAttachments, { className: "mb-2", dir, children: message.attachments?.map((attachment, index) => /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2412
- ChatAttachment,
2413
- {
2414
- data: attachment,
2415
- dir: "ltr"
2416
- },
2417
- String(attachment.url || attachment.filename || index)
2418
- )) }) : null,
2419
- shouldRenderContent ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2420
- ChatMessageContent,
2421
- {
2422
- className: cn(isAssistant ? "bg-transparent p-0" : void 0, contentClassName),
2423
- dir,
2424
- from: message.role,
2425
- children: [
2426
- isAssistant ? renderAssistantContent ? renderAssistantContent({ isStreaming, message }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2427
- Timeline,
2428
- {
2429
- getHitlActionState,
2430
- isLoading: isStreaming,
2431
- message,
2432
- nodeActivityMode,
2433
- onHitlAction,
2434
- parts: message.parts
2435
- }
2436
- ) : null,
2437
- !isAssistant ? renderUserContent ? renderUserContent({ message }) : resolvedUserContent ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2438
- AgentChatUserMessageContent,
2439
- {
2440
- collapsedLines: userMessageCollapsedLines,
2441
- collapseThreshold: userMessageCollapseThreshold,
2442
- content: resolvedUserContent,
2443
- showLessLabel: userMessageShowLessLabel,
2444
- showMoreLabel: userMessageShowMoreLabel
2445
- }
2446
- ) : null : null
2447
- ]
2448
- }
2449
- ) : null,
2450
- renderedActions ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn(dir === "rtl" ? "mt-1 h-9 w-full" : "mt-1 h-9 w-full -translate-x-2", actionsClassName), children: renderedActions }) : null
2451
- ] });
2787
+ const defaultUserActions = !actions && Boolean(actionHandlers) && !isAssistant;
2788
+ return /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2789
+ ChatMessage,
2790
+ {
2791
+ className,
2792
+ dir,
2793
+ from: message.role,
2794
+ children: [
2795
+ showAttachments ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2796
+ ChatAttachmentRows,
2797
+ {
2798
+ attachments: message.attachments || [],
2799
+ className: "mb-2",
2800
+ dir,
2801
+ resolveContent: attachmentContentResolver
2802
+ }
2803
+ ) : null,
2804
+ shouldRenderContent ? /* @__PURE__ */ (0, import_jsx_runtime10.jsxs)(
2805
+ ChatMessageContent,
2806
+ {
2807
+ className: cn(isAssistant ? "bg-transparent p-0" : void 0, contentClassName),
2808
+ dir,
2809
+ from: message.role,
2810
+ children: [
2811
+ isAssistant ? renderAssistantContent ? renderAssistantContent({ isStreaming, message }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2812
+ Timeline,
2813
+ {
2814
+ getHitlActionState,
2815
+ isLoading: isStreaming,
2816
+ message,
2817
+ nodeActivityMode,
2818
+ onHitlAction,
2819
+ parts: message.parts
2820
+ }
2821
+ ) : null,
2822
+ !isAssistant ? renderUserContent ? renderUserContent({ message }) : resolvedUserContent ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(
2823
+ AgentChatUserMessageContent,
2824
+ {
2825
+ collapsedLines: userMessageCollapsedLines,
2826
+ collapseThreshold: userMessageCollapseThreshold,
2827
+ content: resolvedUserContent,
2828
+ showLessLabel: userMessageShowLessLabel,
2829
+ showMoreLabel: userMessageShowMoreLabel
2830
+ }
2831
+ ) : null : null
2832
+ ]
2833
+ }
2834
+ ) : null,
2835
+ renderedActions ? defaultUserActions ? /* @__PURE__ */ (0, import_jsx_runtime10.jsx)(AgentChatUserMessageActionRow, { className: actionsClassName, children: renderedActions }) : /* @__PURE__ */ (0, import_jsx_runtime10.jsx)("div", { className: cn(dir === "rtl" ? "mt-1 h-9 w-full" : "mt-1 h-9 w-full -translate-x-2", actionsClassName), children: renderedActions }) : null
2836
+ ]
2837
+ }
2838
+ );
2452
2839
  }
2453
2840
 
2454
2841
  // src/ui/agent-chat-context-status.tsx
@@ -2854,6 +3241,7 @@ function useAudioRecorder(onRecorded, options = {}) {
2854
3241
  AgentChatToolPart,
2855
3242
  AgentChatToolRow,
2856
3243
  AgentChatToolSession,
3244
+ AgentChatUserMessageActionRow,
2857
3245
  AgentChatUserMessageContent,
2858
3246
  AgentResponseTimeline,
2859
3247
  AskUserInteraction,
@@ -2871,6 +3259,7 @@ function useAudioRecorder(onRecorded, options = {}) {
2871
3259
  BubbleGroup,
2872
3260
  BubbleReactions,
2873
3261
  ChatAttachment,
3262
+ ChatAttachmentRows,
2874
3263
  ChatAttachments,
2875
3264
  ChatMessage,
2876
3265
  ChatMessageAction,
@@ -2884,6 +3273,7 @@ function useAudioRecorder(onRecorded, options = {}) {
2884
3273
  MessageAction,
2885
3274
  MessageActions,
2886
3275
  MessageAttachment,
3276
+ MessageAttachmentRows,
2887
3277
  MessageAttachments,
2888
3278
  MessageAvatar,
2889
3279
  MessageContent,
@@ -2899,6 +3289,7 @@ function useAudioRecorder(onRecorded, options = {}) {
2899
3289
  bubbleVariants,
2900
3290
  cn,
2901
3291
  contextStatusMetrics,
3292
+ createAgentChatComposerFile,
2902
3293
  hitlActionLabel,
2903
3294
  hitlTitle,
2904
3295
  isAgentChatSubagent,
@@ -2907,6 +3298,7 @@ function useAudioRecorder(onRecorded, options = {}) {
2907
3298
  markerVariants,
2908
3299
  partitionAgentChatToolTimeline,
2909
3300
  resolvedHitlLabel,
3301
+ revokeAgentChatComposerFiles,
2910
3302
  shouldCollapseUserMessage,
2911
3303
  useAudioRecorder
2912
3304
  });