@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.js CHANGED
@@ -4,8 +4,21 @@ import {
4
4
  } from "../chunk-SSUO4EPD.js";
5
5
 
6
6
  // src/ui/agent-chat-composer.tsx
7
+ import * as React2 from "react";
8
+ import { ArrowUp, Plus, Square } from "lucide-react";
9
+
10
+ // src/ui/adapters.ts
7
11
  import * as React from "react";
8
- import { ArrowUp, Paperclip, Plus, Square, X } from "lucide-react";
12
+ import { FileIcon } from "@untitledui/file-icons";
13
+ import {
14
+ ImageOff,
15
+ LoaderCircle,
16
+ X
17
+ } from "lucide-react";
18
+
19
+ // src/ui/attachment.tsx
20
+ import { cva } from "class-variance-authority";
21
+ import { Slot } from "@radix-ui/react-slot";
9
22
 
10
23
  // src/ui/utils.ts
11
24
  import { clsx } from "clsx";
@@ -14,266 +27,8 @@ function cn(...values) {
14
27
  return twMerge(clsx(values));
15
28
  }
16
29
 
17
- // src/ui/agent-chat-composer.tsx
18
- import { jsx, jsxs } from "react/jsx-runtime";
19
- var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `file-${Date.now()}-${Math.random().toString(36).slice(2)}`;
20
- function createComposerFile(file) {
21
- return {
22
- id: createId(),
23
- type: "file",
24
- url: URL.createObjectURL(file),
25
- filename: file.name || "attachment",
26
- mediaType: file.type || "application/octet-stream",
27
- source: file
28
- };
29
- }
30
- function revokeComposerFiles(files) {
31
- files.forEach((file) => {
32
- try {
33
- URL.revokeObjectURL(file.url);
34
- } catch {
35
- }
36
- });
37
- }
38
- function formatFileCount(files) {
39
- if (files.length === 1) return files[0].filename;
40
- return `${files.length} files`;
41
- }
42
- 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";
43
- 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";
44
- function AgentChatComposer({
45
- accept,
46
- allowAttachments = true,
47
- attachmentLayout = "flow",
48
- className,
49
- disabled = false,
50
- forceExpanded = false,
51
- inputToolbarContent,
52
- isRunning = false,
53
- onAttachmentCountChange,
54
- onStop,
55
- onSubmit,
56
- placeholder = "Message the agent",
57
- textareaRef
58
- }) {
59
- const fileInputRef = React.useRef(null);
60
- const filesRef = React.useRef([]);
61
- const localTextareaRef = React.useRef(null);
62
- const [text, setText] = React.useState("");
63
- const [files, setFiles] = React.useState([]);
64
- const [isSubmitting, setIsSubmitting] = React.useState(false);
65
- const isExpanded = forceExpanded || text.includes("\n") || text.length > 62;
66
- const setTextareaRef = React.useCallback(
67
- (node) => {
68
- localTextareaRef.current = node;
69
- if (typeof textareaRef === "function") {
70
- textareaRef(node);
71
- } else if (textareaRef) {
72
- textareaRef.current = node;
73
- }
74
- },
75
- [textareaRef]
76
- );
77
- React.useEffect(() => {
78
- filesRef.current = files;
79
- onAttachmentCountChange?.(files.length);
80
- }, [files, onAttachmentCountChange]);
81
- React.useEffect(() => () => revokeComposerFiles(filesRef.current), []);
82
- React.useLayoutEffect(() => {
83
- const textarea = localTextareaRef.current;
84
- if (!textarea) return;
85
- if (isExpanded) {
86
- textarea.style.height = "auto";
87
- const scrollHeight = textarea.scrollHeight;
88
- textarea.style.height = `${Math.min(scrollHeight, 224)}px`;
89
- textarea.style.overflowY = scrollHeight >= 224 ? "auto" : "hidden";
90
- return;
91
- }
92
- textarea.style.height = "";
93
- textarea.style.overflowY = "hidden";
94
- }, [isExpanded, text]);
95
- const canSubmit = !disabled && !isSubmitting && !isRunning && (text.trim().length > 0 || files.length > 0);
96
- const handleFilesChange = (event) => {
97
- const selected = Array.from(event.target.files || []).map(createComposerFile);
98
- if (selected.length > 0) {
99
- setFiles((current) => [...current, ...selected]);
100
- }
101
- event.currentTarget.value = "";
102
- };
103
- const removeFile = (fileId) => {
104
- setFiles((current) => {
105
- const target = current.find((file) => file.id === fileId);
106
- if (target) revokeComposerFiles([target]);
107
- return current.filter((file) => file.id !== fileId);
108
- });
109
- };
110
- const submit = async () => {
111
- if (!canSubmit) return;
112
- const submittedText = text.trim();
113
- const submittedFiles = files;
114
- setIsSubmitting(true);
115
- try {
116
- await onSubmit({ text: submittedText, files: submittedFiles });
117
- setText("");
118
- setFiles([]);
119
- revokeComposerFiles(submittedFiles);
120
- } catch {
121
- } finally {
122
- setIsSubmitting(false);
123
- }
124
- };
125
- const handleKeyDown = (event) => {
126
- if (event.key === "Enter" && !event.shiftKey) {
127
- event.preventDefault();
128
- void submit();
129
- }
130
- };
131
- return /* @__PURE__ */ jsxs("div", { className: cn("a24-composer relative mx-auto w-full max-w-3xl overflow-visible", className), "data-agents24-chat-composer": "", children: [
132
- files.length > 0 ? /* @__PURE__ */ jsx(
133
- "div",
134
- {
135
- "data-testid": "agent-chat-composer-attachments",
136
- "data-layout": attachmentLayout,
137
- className: cn(
138
- "a24-composer__attachments flex min-w-0 gap-2 py-1",
139
- 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"
140
- ),
141
- children: files.map((file) => /* @__PURE__ */ jsxs(
142
- "div",
143
- {
144
- 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",
145
- children: [
146
- /* @__PURE__ */ jsx("span", { className: "flex size-5 shrink-0 items-center justify-center rounded bg-background text-muted-foreground", children: /* @__PURE__ */ jsx(Paperclip, { className: "size-3" }) }),
147
- /* @__PURE__ */ jsx("span", { className: "min-w-0 flex-1 truncate", children: file.filename }),
148
- /* @__PURE__ */ jsx(
149
- "button",
150
- {
151
- "aria-label": `Remove ${file.filename}`,
152
- 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",
153
- onClick: () => removeFile(file.id),
154
- type: "button",
155
- children: /* @__PURE__ */ jsx(X, { className: "size-3" })
156
- }
157
- )
158
- ]
159
- },
160
- file.id
161
- ))
162
- }
163
- ) : null,
164
- /* @__PURE__ */ jsx(
165
- "div",
166
- {
167
- "data-expanded": isExpanded ? "" : void 0,
168
- className: cn(
169
- "a24-composer__surface relative w-full border border-input bg-background shadow-sm",
170
- isExpanded ? "rounded-xl p-2 pb-1.5" : "rounded-xl px-2 py-[3px]"
171
- ),
172
- children: /* @__PURE__ */ jsxs(
173
- "div",
174
- {
175
- className: "a24-composer__grid w-full",
176
- style: {
177
- alignItems: "center",
178
- display: "grid",
179
- gridTemplateAreas: isExpanded ? `"textarea textarea textarea" "plus toolbar submit"` : `"plus textarea composer mic submit"`,
180
- gridTemplateColumns: isExpanded ? "auto 1fr auto" : "auto 1fr auto auto auto",
181
- gap: isExpanded ? "3px 10px" : "8px"
182
- },
183
- children: [
184
- /* @__PURE__ */ jsxs("div", { style: { gridArea: "plus" }, className: "a24-composer__attach-slot flex shrink-0 items-center justify-center", children: [
185
- /* @__PURE__ */ jsx(
186
- "input",
187
- {
188
- ref: fileInputRef,
189
- accept,
190
- "aria-label": "Attach files",
191
- className: "hidden",
192
- multiple: true,
193
- name: "attachments",
194
- onChange: handleFilesChange,
195
- type: "file"
196
- }
197
- ),
198
- /* @__PURE__ */ jsx(
199
- "button",
200
- {
201
- "aria-label": "Attach files",
202
- className: cn("a24-composer__attach", pillButtonClass),
203
- disabled: disabled || isSubmitting || isRunning || !allowAttachments,
204
- onClick: () => fileInputRef.current?.click(),
205
- title: "Attach files",
206
- type: "button",
207
- children: /* @__PURE__ */ jsx(Plus, { className: "size-4" })
208
- }
209
- )
210
- ] }),
211
- /* @__PURE__ */ jsx("div", { style: { gridArea: "textarea" }, className: "a24-composer__textarea-slot min-w-0 w-full", children: /* @__PURE__ */ jsx(
212
- "textarea",
213
- {
214
- ref: setTextareaRef,
215
- "aria-label": "Message",
216
- autoComplete: "off",
217
- className: cn(
218
- "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",
219
- isExpanded ? "min-h-9 max-h-[224px] py-1" : "min-h-9 h-9 max-h-9 py-2"
220
- ),
221
- disabled: disabled || isSubmitting || isRunning,
222
- name: "message",
223
- onChange: (event) => setText(event.target.value),
224
- onKeyDown: handleKeyDown,
225
- placeholder,
226
- rows: isExpanded ? 2 : 1,
227
- value: text
228
- }
229
- ) }),
230
- isExpanded ? /* @__PURE__ */ jsx("div", { style: { gridArea: "toolbar" }, className: "a24-composer__toolbar flex min-w-0 items-center gap-1.5", children: inputToolbarContent }) : null,
231
- !isExpanded ? /* @__PURE__ */ jsx("div", { style: { gridArea: "composer" }, className: "flex min-w-0 items-center gap-1.5" }) : null,
232
- !isExpanded ? /* @__PURE__ */ jsx("div", { style: { gridArea: "mic" }, className: "flex shrink-0 items-center justify-end" }) : null,
233
- /* @__PURE__ */ jsx("div", { style: { gridArea: "submit" }, className: "a24-composer__submit-slot flex shrink-0 items-center justify-end", children: isRunning ? /* @__PURE__ */ jsx(
234
- "button",
235
- {
236
- "aria-label": "Stop generating",
237
- className: cn("a24-composer__submit", submitButtonClass),
238
- disabled: disabled || !onStop,
239
- onClick: onStop,
240
- title: "Stop",
241
- type: "button",
242
- children: /* @__PURE__ */ jsx(Square, { className: "size-3.5 fill-current" })
243
- }
244
- ) : /* @__PURE__ */ jsx(
245
- "button",
246
- {
247
- "aria-label": files.length > 0 ? `Send ${formatFileCount(files)}` : "Send message",
248
- className: cn("a24-composer__submit", submitButtonClass, text.trim().length > 0 && "hover:scale-105"),
249
- disabled: !canSubmit,
250
- onClick: () => void submit(),
251
- title: "Send",
252
- type: "button",
253
- children: /* @__PURE__ */ jsx(ArrowUp, { className: "size-4", strokeWidth: 2.5 })
254
- }
255
- ) })
256
- ]
257
- }
258
- )
259
- }
260
- ),
261
- !isExpanded && inputToolbarContent ? /* @__PURE__ */ jsx("div", { className: "a24-composer__toolbar a24-composer__toolbar--external mt-2 flex items-center gap-2 px-1", children: inputToolbarContent }) : null
262
- ] });
263
- }
264
-
265
- // src/ui/agent-chat-actions.tsx
266
- import * as React3 from "react";
267
- import { Check, Copy, RefreshCcw, ThumbsDown, ThumbsUp } from "lucide-react";
268
- import { Popover } from "radix-ui";
269
-
270
- // src/ui/adapters.ts
271
- import * as React2 from "react";
272
-
273
30
  // src/ui/attachment.tsx
274
- import { cva } from "class-variance-authority";
275
- import { Slot } from "@radix-ui/react-slot";
276
- import { jsx as jsx2 } from "react/jsx-runtime";
31
+ import { jsx } from "react/jsx-runtime";
277
32
  var attachmentVariants = cva(
278
33
  "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",
279
34
  {
@@ -297,7 +52,7 @@ function Attachment({
297
52
  orientation = "horizontal",
298
53
  ...props
299
54
  }) {
300
- return /* @__PURE__ */ jsx2(
55
+ return /* @__PURE__ */ jsx(
301
56
  "div",
302
57
  {
303
58
  "data-slot": "attachment",
@@ -310,12 +65,12 @@ function Attachment({
310
65
  );
311
66
  }
312
67
  var attachmentMediaVariants = cva(
313
- "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",
68
+ "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",
314
69
  {
315
70
  variants: {
316
71
  variant: {
317
72
  icon: "",
318
- 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"
73
+ 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"
319
74
  }
320
75
  },
321
76
  defaultVariants: {
@@ -328,7 +83,7 @@ function AttachmentMedia({
328
83
  variant = "icon",
329
84
  ...props
330
85
  }) {
331
- return /* @__PURE__ */ jsx2(
86
+ return /* @__PURE__ */ jsx(
332
87
  "div",
333
88
  {
334
89
  "data-slot": "attachment-media",
@@ -342,7 +97,7 @@ function AttachmentContent({
342
97
  className,
343
98
  ...props
344
99
  }) {
345
- return /* @__PURE__ */ jsx2(
100
+ return /* @__PURE__ */ jsx(
346
101
  "div",
347
102
  {
348
103
  "data-slot": "attachment-content",
@@ -358,7 +113,7 @@ function AttachmentTitle({
358
113
  className,
359
114
  ...props
360
115
  }) {
361
- return /* @__PURE__ */ jsx2(
116
+ return /* @__PURE__ */ jsx(
362
117
  "span",
363
118
  {
364
119
  "data-slot": "attachment-title",
@@ -374,7 +129,7 @@ function AttachmentDescription({
374
129
  className,
375
130
  ...props
376
131
  }) {
377
- return /* @__PURE__ */ jsx2(
132
+ return /* @__PURE__ */ jsx(
378
133
  "span",
379
134
  {
380
135
  "data-slot": "attachment-description",
@@ -391,7 +146,7 @@ function AttachmentActions({
391
146
  className,
392
147
  ...props
393
148
  }) {
394
- return /* @__PURE__ */ jsx2(
149
+ return /* @__PURE__ */ jsx(
395
150
  "div",
396
151
  {
397
152
  "data-slot": "attachment-actions",
@@ -408,7 +163,7 @@ function AttachmentAction({
408
163
  type = "button",
409
164
  ...props
410
165
  }) {
411
- return /* @__PURE__ */ jsx2(
166
+ return /* @__PURE__ */ jsx(
412
167
  "button",
413
168
  {
414
169
  "data-slot": "attachment-action",
@@ -428,7 +183,7 @@ function AttachmentTrigger({
428
183
  ...props
429
184
  }) {
430
185
  const Comp = asChild ? Slot : "button";
431
- return /* @__PURE__ */ jsx2(
186
+ return /* @__PURE__ */ jsx(
432
187
  Comp,
433
188
  {
434
189
  "data-slot": "attachment-trigger",
@@ -439,7 +194,7 @@ function AttachmentTrigger({
439
194
  );
440
195
  }
441
196
  function AttachmentGroup({ className, ...props }) {
442
- return /* @__PURE__ */ jsx2(
197
+ return /* @__PURE__ */ jsx(
443
198
  "div",
444
199
  {
445
200
  "data-slot": "attachment-group",
@@ -453,9 +208,9 @@ function AttachmentGroup({ className, ...props }) {
453
208
  }
454
209
 
455
210
  // src/ui/message.tsx
456
- import { jsx as jsx3 } from "react/jsx-runtime";
211
+ import { jsx as jsx2 } from "react/jsx-runtime";
457
212
  function MessageGroup({ className, ...props }) {
458
- return /* @__PURE__ */ jsx3(
213
+ return /* @__PURE__ */ jsx2(
459
214
  "div",
460
215
  {
461
216
  "data-slot": "message-group",
@@ -469,7 +224,7 @@ function Message({
469
224
  align = "start",
470
225
  ...props
471
226
  }) {
472
- return /* @__PURE__ */ jsx3(
227
+ return /* @__PURE__ */ jsx2(
473
228
  "div",
474
229
  {
475
230
  "data-slot": "message",
@@ -483,7 +238,7 @@ function Message({
483
238
  );
484
239
  }
485
240
  function MessageAvatar({ className, ...props }) {
486
- return /* @__PURE__ */ jsx3(
241
+ return /* @__PURE__ */ jsx2(
487
242
  "div",
488
243
  {
489
244
  "data-slot": "message-avatar",
@@ -496,7 +251,7 @@ function MessageAvatar({ className, ...props }) {
496
251
  );
497
252
  }
498
253
  function MessageContent({ className, ...props }) {
499
- return /* @__PURE__ */ jsx3(
254
+ return /* @__PURE__ */ jsx2(
500
255
  "div",
501
256
  {
502
257
  "data-slot": "message-content",
@@ -509,7 +264,7 @@ function MessageContent({ className, ...props }) {
509
264
  );
510
265
  }
511
266
  function MessageHeader({ className, ...props }) {
512
- return /* @__PURE__ */ jsx3(
267
+ return /* @__PURE__ */ jsx2(
513
268
  "div",
514
269
  {
515
270
  "data-slot": "message-header",
@@ -522,7 +277,7 @@ function MessageHeader({ className, ...props }) {
522
277
  );
523
278
  }
524
279
  function MessageFooter({ className, ...props }) {
525
- return /* @__PURE__ */ jsx3(
280
+ return /* @__PURE__ */ jsx2(
526
281
  "div",
527
282
  {
528
283
  "data-slot": "message-footer",
@@ -536,23 +291,22 @@ function MessageFooter({ className, ...props }) {
536
291
  }
537
292
 
538
293
  // src/ui/adapters.ts
539
- var ChatMessageRoleContext = React2.createContext("assistant");
294
+ var ChatMessageRoleContext = React.createContext("assistant");
540
295
  function ChatMessage(input) {
541
296
  const { className, from, align, ...props } = input;
542
297
  const isUser = from === "user";
543
- const isRtl = props.dir === "rtl";
544
298
  const resolvedAlign = align || "start";
545
299
  const messageProps = Object.assign({}, props);
546
300
  messageProps.align = resolvedAlign;
547
301
  messageProps.className = cn(
548
- "a24-chat-message group flex w-full flex-col gap-2",
549
- 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",
302
+ "a24-chat-message group flex w-full flex-col",
303
+ 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",
550
304
  className
551
305
  );
552
- return React2.createElement(
306
+ return React.createElement(
553
307
  ChatMessageRoleContext.Provider,
554
308
  { value: from },
555
- React2.createElement(Message, messageProps)
309
+ React.createElement(Message, messageProps)
556
310
  );
557
311
  }
558
312
  function ChatMessageContent({
@@ -560,9 +314,9 @@ function ChatMessageContent({
560
314
  from,
561
315
  ...props
562
316
  }) {
563
- const inheritedRole = React2.useContext(ChatMessageRoleContext);
317
+ const inheritedRole = React.useContext(ChatMessageRoleContext);
564
318
  const isUser = (from ?? inheritedRole) === "user";
565
- return React2.createElement("div", {
319
+ return React.createElement("div", {
566
320
  className: cn(
567
321
  "a24-chat-message-content flex max-w-full min-w-0 flex-col gap-2 text-sm",
568
322
  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",
@@ -576,7 +330,7 @@ function ChatMessageActions({
576
330
  className,
577
331
  ...props
578
332
  }) {
579
- return React2.createElement("div", {
333
+ return React.createElement("div", {
580
334
  className: cn("flex items-center gap-1", className),
581
335
  "data-slot": "message-actions",
582
336
  ...props
@@ -591,7 +345,7 @@ function ChatMessageAction({
591
345
  ...props
592
346
  }) {
593
347
  const tooltipLabel = typeof tooltip === "string" ? tooltip : void 0;
594
- return React2.createElement(
348
+ return React.createElement(
595
349
  "button",
596
350
  {
597
351
  "aria-label": label || tooltipLabel,
@@ -604,83 +358,629 @@ function ChatMessageAction({
604
358
  ...props
605
359
  },
606
360
  children,
607
- React2.createElement(
361
+ React.createElement(
608
362
  "span",
609
363
  { className: "sr-only" },
610
364
  label || tooltipLabel
611
365
  )
612
366
  );
613
367
  }
368
+ var resolvedContentCache = /* @__PURE__ */ new WeakMap();
369
+ var CONTENT_CACHE_EXPIRY_BUFFER_MS = 15e3;
370
+ var CONTENT_CACHE_MAX_ENTRIES = 100;
371
+ function attachmentCacheKey(data) {
372
+ const id = typeof data.id === "string" ? data.id.trim() : "";
373
+ return id || null;
374
+ }
375
+ function cachedAttachmentContent(resolver, data) {
376
+ const key = attachmentCacheKey(data);
377
+ if (!key) return null;
378
+ const cached = resolvedContentCache.get(resolver)?.get(key);
379
+ if (!cached) return null;
380
+ if (!cached.validUntil) return cached;
381
+ const validUntil = Date.parse(cached.validUntil);
382
+ if (Number.isFinite(validUntil) && validUntil - CONTENT_CACHE_EXPIRY_BUFFER_MS > Date.now()) {
383
+ return cached;
384
+ }
385
+ resolvedContentCache.get(resolver)?.delete(key);
386
+ return null;
387
+ }
388
+ function cacheAttachmentContent(resolver, data, result) {
389
+ const key = attachmentCacheKey(data);
390
+ if (!key) return;
391
+ const cache = resolvedContentCache.get(resolver) || /* @__PURE__ */ new Map();
392
+ cache.delete(key);
393
+ cache.set(key, result);
394
+ while (cache.size > CONTENT_CACHE_MAX_ENTRIES) {
395
+ const oldestKey = cache.keys().next().value;
396
+ if (typeof oldestKey !== "string") break;
397
+ cache.delete(oldestKey);
398
+ }
399
+ resolvedContentCache.set(resolver, cache);
400
+ }
401
+ function invalidateAttachmentContent(resolver, data) {
402
+ const key = attachmentCacheKey(data);
403
+ if (key) resolvedContentCache.get(resolver)?.delete(key);
404
+ }
405
+ function attachmentMediaType(data) {
406
+ return data.mediaType || data.contentType || String(data.type || "application/octet-stream");
407
+ }
408
+ function attachmentFilename(data) {
409
+ return data.filename || data.name || "Attachment";
410
+ }
411
+ function attachmentExtension(filename) {
412
+ const extension = filename.split(".").pop();
413
+ return extension && extension !== filename ? extension.toLowerCase() : "";
414
+ }
415
+ function resolveAttachmentVisual(data) {
416
+ const mediaType = attachmentMediaType(data).toLowerCase();
417
+ const extension = attachmentExtension(attachmentFilename(data));
418
+ if (mediaType.startsWith("image/") || ["avif", "gif", "heic", "jpeg", "jpg", "png", "svg", "webp"].includes(extension)) {
419
+ return { fileIconType: extension || "image", kind: "image" };
420
+ }
421
+ if (mediaType === "application/pdf" || extension === "pdf") {
422
+ return { fileIconType: "pdf", kind: "pdf" };
423
+ }
424
+ if (mediaType.includes("spreadsheet") || mediaType.includes("csv") || ["csv", "numbers", "ods", "xls", "xlsm", "xlsx"].includes(extension)) {
425
+ if (["csv", "xls", "xlsx"].includes(extension)) {
426
+ return { fileIconType: extension, kind: "spreadsheet" };
427
+ }
428
+ return { fileIconType: "spreadsheets", kind: "spreadsheet" };
429
+ }
430
+ if (mediaType.includes("presentation") || ["key", "odp", "ppt", "pptx"].includes(extension)) {
431
+ return { fileIconType: extension === "ppt" ? "ppt" : "pptx", kind: "presentation" };
432
+ }
433
+ if (mediaType.includes("word") || mediaType.includes("rtf") || ["doc", "docx", "odt", "rtf"].includes(extension)) {
434
+ if (extension === "doc" || extension === "docx") {
435
+ return { fileIconType: extension, kind: "document" };
436
+ }
437
+ return { fileIconType: "document", kind: "document" };
438
+ }
439
+ if (mediaType.startsWith("audio/") || ["aac", "flac", "m4a", "mp3", "ogg", "wav"].includes(extension)) {
440
+ if (extension === "mp3" || extension === "wav") {
441
+ return { fileIconType: extension, kind: "audio" };
442
+ }
443
+ return { fileIconType: "audio", kind: "audio" };
444
+ }
445
+ if (mediaType.startsWith("video/") || ["avi", "m4v", "mkv", "mov", "mp4", "webm"].includes(extension)) {
446
+ if (["avi", "mkv", "mp4", "mpeg"].includes(extension)) {
447
+ return { fileIconType: extension, kind: "video" };
448
+ }
449
+ return { fileIconType: "video", kind: "video" };
450
+ }
451
+ if (mediaType.includes("zip") || mediaType.includes("compressed") || ["7z", "bz2", "gz", "rar", "tar", "tgz", "zip"].includes(extension)) {
452
+ return { fileIconType: extension === "rar" ? "rar" : "zip", kind: "archive" };
453
+ }
454
+ 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)) {
455
+ if (["css", "html", "java", "js", "json", "sql", "xml"].includes(extension)) {
456
+ return { fileIconType: extension, kind: "code" };
457
+ }
458
+ return { fileIconType: "code", kind: "code" };
459
+ }
460
+ if (mediaType.startsWith("text/") || ["md", "markdown", "txt"].includes(extension)) {
461
+ return { fileIconType: "txt", kind: "file" };
462
+ }
463
+ return { fileIconType: "empty", kind: "file" };
464
+ }
465
+ function solidFileIconBackground(fileIconType, kind) {
466
+ if (["txt", "zip", "rar"].includes(fileIconType)) return "#344054";
467
+ if (kind === "pdf") return "#D92D20";
468
+ if (kind === "spreadsheet") return "#079455";
469
+ if (kind === "document" || kind === "video") return "#155EEF";
470
+ if (kind === "presentation") return "#E62E05";
471
+ if (kind === "audio") return "#DD2590";
472
+ if (kind === "code") return "#444CE7";
473
+ return "#7F56D9";
474
+ }
475
+ function isImageAttachment(data) {
476
+ return resolveAttachmentVisual(data).kind === "image";
477
+ }
478
+ function attachmentRenderKey(data, index, rowKind) {
479
+ const id = typeof data.id === "string" ? data.id.trim() : "";
480
+ const identity = id || `${attachmentFilename(data)}:${attachmentMediaType(data)}`;
481
+ return `${rowKind}:${identity}:${index}`;
482
+ }
483
+ function ChatImageAttachment({
484
+ className,
485
+ data,
486
+ onRemove,
487
+ resolveContent,
488
+ ...props
489
+ }) {
490
+ const filename = attachmentFilename(data);
491
+ const initialUrl = typeof data.url === "string" ? data.url.trim() : "";
492
+ const contentStatus = String(data.contentStatus || data.content_status || "available");
493
+ const attachmentKey = attachmentCacheKey(data);
494
+ const dataRef = React.useRef(data);
495
+ dataRef.current = data;
496
+ const rootRef = React.useRef(null);
497
+ const retryCountRef = React.useRef(0);
498
+ const [isNearViewport, setIsNearViewport] = React.useState(Boolean(initialUrl));
499
+ const [sourceUrl, setSourceUrl] = React.useState(initialUrl);
500
+ const [status, setStatus] = React.useState(
501
+ initialUrl ? "loading" : contentStatus === "expired" || !resolveContent ? "error" : "idle"
502
+ );
503
+ const [resolutionAttempt, setResolutionAttempt] = React.useState(0);
504
+ React.useEffect(() => {
505
+ if (initialUrl && resolutionAttempt === 0 || contentStatus === "expired" || !resolveContent) return;
506
+ const node = rootRef.current;
507
+ if (!node || typeof IntersectionObserver === "undefined") {
508
+ setIsNearViewport(true);
509
+ return;
510
+ }
511
+ const observer = new IntersectionObserver(
512
+ (entries) => {
513
+ if (entries.some((entry) => entry.isIntersecting)) {
514
+ setIsNearViewport(true);
515
+ observer.disconnect();
516
+ }
517
+ },
518
+ { rootMargin: "240px" }
519
+ );
520
+ observer.observe(node);
521
+ return () => observer.disconnect();
522
+ }, [contentStatus, initialUrl, resolutionAttempt, resolveContent]);
523
+ React.useEffect(() => {
524
+ if (initialUrl && resolutionAttempt === 0) {
525
+ setSourceUrl(initialUrl);
526
+ setStatus("loading");
527
+ return;
528
+ }
529
+ if (!isNearViewport || !resolveContent || contentStatus === "expired") return;
530
+ const attachment = dataRef.current;
531
+ const cached = cachedAttachmentContent(resolveContent, attachment);
532
+ if (cached) {
533
+ setSourceUrl(cached.url);
534
+ setStatus("loading");
535
+ return;
536
+ }
537
+ const controller = new AbortController();
538
+ setStatus("resolving");
539
+ void resolveContent(attachment, controller.signal).then((result) => {
540
+ if (controller.signal.aborted) return;
541
+ if (!result.url?.trim()) throw new Error("Attachment content resolver returned an empty URL");
542
+ cacheAttachmentContent(resolveContent, attachment, result);
543
+ setSourceUrl(result.url);
544
+ setStatus("loading");
545
+ }).catch(() => {
546
+ if (!controller.signal.aborted) setStatus("error");
547
+ });
548
+ return () => controller.abort();
549
+ }, [attachmentKey, contentStatus, initialUrl, isNearViewport, resolutionAttempt, resolveContent]);
550
+ const handleImageError = () => {
551
+ if (resolveContent && retryCountRef.current === 0) {
552
+ retryCountRef.current = 1;
553
+ invalidateAttachmentContent(resolveContent, dataRef.current);
554
+ setSourceUrl("");
555
+ setStatus("resolving");
556
+ setResolutionAttempt((current) => current + 1);
557
+ return;
558
+ }
559
+ setStatus("error");
560
+ };
561
+ return React.createElement(
562
+ Attachment,
563
+ {
564
+ ...props,
565
+ className: cn("a24-chat-image-attachment", className),
566
+ orientation: "vertical",
567
+ ref: rootRef,
568
+ state: status === "error" ? "error" : status === "done" ? "done" : "processing",
569
+ title: filename
570
+ },
571
+ React.createElement(
572
+ AttachmentMedia,
573
+ { className: "a24-chat-image-media", variant: "image" },
574
+ sourceUrl ? React.createElement("img", {
575
+ alt: filename,
576
+ decoding: "async",
577
+ loading: "lazy",
578
+ onError: handleImageError,
579
+ onLoad: () => setStatus("done"),
580
+ src: sourceUrl
581
+ }) : status === "error" ? React.createElement(ImageOff, { "aria-hidden": "true", className: "size-5" }) : React.createElement(LoaderCircle, {
582
+ "aria-hidden": "true",
583
+ className: "a24-chat-image-spinner size-5 animate-spin"
584
+ }),
585
+ status !== "done" && status !== "error" ? React.createElement("span", { className: "sr-only", role: "status" }, `Loading ${filename}`) : null,
586
+ status === "error" ? React.createElement("span", { className: "sr-only" }, `${filename} preview unavailable`) : null
587
+ ),
588
+ onRemove ? React.createElement(
589
+ "button",
590
+ {
591
+ "aria-label": `Remove ${filename}`,
592
+ className: "a24-chat-image-remove",
593
+ onClick: (event) => {
594
+ event.stopPropagation();
595
+ onRemove();
596
+ },
597
+ type: "button"
598
+ },
599
+ React.createElement(X, { "aria-hidden": "true", className: "size-3.5" })
600
+ ) : null
601
+ );
602
+ }
614
603
  function ChatAttachment({
604
+ className,
615
605
  data,
616
606
  onRemove,
617
607
  orientation,
608
+ resolveContent,
618
609
  ...props
619
610
  }) {
620
- const mediaType = data.mediaType || data.contentType || String(data.type || "application/octet-stream");
621
- const filename = data.filename || data.name || "Attachment";
622
- const isImage = mediaType.startsWith("image/") && Boolean(data.url);
623
- return React2.createElement(
611
+ const filename = attachmentFilename(data);
612
+ const { fileIconType, kind } = resolveAttachmentVisual(data);
613
+ if (kind === "image") {
614
+ return React.createElement(ChatImageAttachment, {
615
+ ...props,
616
+ className,
617
+ data,
618
+ onRemove,
619
+ resolveContent
620
+ });
621
+ }
622
+ return React.createElement(
624
623
  Attachment,
625
624
  {
626
- orientation: orientation || (isImage ? "vertical" : "horizontal"),
625
+ className: cn("a24-chat-attachment", className),
626
+ orientation: orientation || "horizontal",
627
+ size: "sm",
627
628
  ...props
628
629
  },
629
- React2.createElement(
630
+ React.createElement(
630
631
  AttachmentMedia,
631
- { variant: isImage ? "image" : "icon" },
632
- isImage ? React2.createElement("img", {
633
- alt: filename,
634
- className: "size-full object-cover",
635
- src: data.url
636
- }) : React2.createElement(
637
- "span",
638
- { "aria-hidden": "true", className: "text-sm" },
639
- filename.slice(0, 1).toUpperCase()
640
- )
632
+ Object.assign(
633
+ {
634
+ className: "a24-chat-attachment-icon",
635
+ style: { backgroundColor: solidFileIconBackground(fileIconType, kind) },
636
+ variant: "icon"
637
+ },
638
+ { "data-file-kind": kind }
639
+ ),
640
+ React.createElement(FileIcon, {
641
+ "aria-hidden": "true",
642
+ size: 28,
643
+ type: fileIconType,
644
+ variant: "solid"
645
+ })
641
646
  ),
642
- React2.createElement(
647
+ React.createElement(
643
648
  AttachmentContent,
644
- null,
645
- React2.createElement(AttachmentTitle, null, filename),
646
- React2.createElement(AttachmentDescription, null, mediaType)
649
+ { className: "a24-chat-attachment-content" },
650
+ React.createElement(AttachmentTitle, { title: filename }, filename)
647
651
  ),
648
- onRemove ? React2.createElement(
652
+ onRemove ? React.createElement(
649
653
  "button",
650
654
  {
651
655
  "aria-label": "Remove attachment",
652
- 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",
656
+ 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",
653
657
  onClick: (event) => {
654
658
  event.stopPropagation();
655
659
  onRemove();
656
660
  },
657
661
  type: "button"
658
662
  },
659
- React2.createElement("span", { "aria-hidden": "true" }, "x")
663
+ React.createElement(X, { "aria-hidden": "true", className: "size-3.5" })
660
664
  ) : null
661
665
  );
662
666
  }
663
- function ChatAttachments({
664
- children,
667
+ function ChatAttachments({
668
+ children,
669
+ className,
670
+ ...props
671
+ }) {
672
+ if (!children) return null;
673
+ return React.createElement(
674
+ AttachmentGroup,
675
+ {
676
+ className: cn(
677
+ "a24-chat-attachments w-full flex-nowrap",
678
+ className
679
+ ),
680
+ ...props
681
+ },
682
+ children
683
+ );
684
+ }
685
+ function ChatAttachmentRows({
686
+ attachments,
687
+ className,
688
+ dir,
689
+ onRemove,
690
+ resolveContent,
691
+ ...props
692
+ }) {
693
+ if (attachments.length === 0) return null;
694
+ const imageAttachments = attachments.filter(isImageAttachment);
695
+ const fileAttachments = attachments.filter((attachment) => !isImageAttachment(attachment));
696
+ const renderRow = (items, kind) => items.length > 0 ? React.createElement(
697
+ ChatAttachments,
698
+ Object.assign({ dir, key: kind }, { "data-attachment-row": kind }),
699
+ items.map(
700
+ (attachment, index) => React.createElement(ChatAttachment, {
701
+ data: attachment,
702
+ dir: "ltr",
703
+ key: attachmentRenderKey(attachment, index, kind),
704
+ onRemove: onRemove ? () => onRemove(attachment) : void 0,
705
+ resolveContent
706
+ })
707
+ )
708
+ ) : null;
709
+ return React.createElement(
710
+ "div",
711
+ {
712
+ className: cn("a24-chat-attachment-rows", className),
713
+ dir,
714
+ ...props
715
+ },
716
+ renderRow(imageAttachments, "images"),
717
+ renderRow(fileAttachments, "files")
718
+ );
719
+ }
720
+
721
+ // src/ui/composer-attachments.ts
722
+ var createId = () => typeof crypto !== "undefined" && "randomUUID" in crypto ? crypto.randomUUID() : `file-${Date.now()}-${Math.random().toString(36).slice(2)}`;
723
+ function createAgentChatComposerFile(file) {
724
+ return {
725
+ id: createId(),
726
+ type: "file",
727
+ url: URL.createObjectURL(file),
728
+ filename: file.name || "attachment",
729
+ mediaType: file.type || "application/octet-stream",
730
+ source: file
731
+ };
732
+ }
733
+ function revokeAgentChatComposerFiles(files) {
734
+ files.forEach((file) => {
735
+ try {
736
+ URL.revokeObjectURL(file.url);
737
+ } catch {
738
+ }
739
+ });
740
+ }
741
+
742
+ // src/ui/agent-chat-composer.tsx
743
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
744
+ function formatFileCount(files) {
745
+ if (files.length === 1) return files[0].filename;
746
+ return `${files.length} files`;
747
+ }
748
+ 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";
749
+ 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";
750
+ function AgentChatComposer({
751
+ accept,
752
+ allowAttachments = true,
753
+ attachmentLayout = "flow",
665
754
  className,
666
- ...props
755
+ disabled = false,
756
+ forceExpanded = false,
757
+ inputToolbarContent,
758
+ isRunning = false,
759
+ onAttachmentCountChange,
760
+ onStop,
761
+ onSubmit,
762
+ placeholder = "Message the agent",
763
+ textareaRef
667
764
  }) {
668
- if (!children) return null;
669
- return React2.createElement(
670
- AttachmentGroup,
671
- {
672
- className: cn(
673
- props.dir === "rtl" ? "mr-auto" : "ml-auto",
674
- "w-fit flex-wrap overflow-visible",
675
- className
676
- ),
677
- ...props
765
+ const fileInputRef = React2.useRef(null);
766
+ const filesRef = React2.useRef([]);
767
+ const inFlightFilesRef = React2.useRef([]);
768
+ const localTextareaRef = React2.useRef(null);
769
+ const draftRevisionRef = React2.useRef(0);
770
+ const [text, setText] = React2.useState("");
771
+ const [files, setFiles] = React2.useState([]);
772
+ const [isSubmitting, setIsSubmitting] = React2.useState(false);
773
+ const isExpanded = forceExpanded || text.includes("\n") || text.length > 62;
774
+ const setTextareaRef = React2.useCallback(
775
+ (node) => {
776
+ localTextareaRef.current = node;
777
+ if (typeof textareaRef === "function") {
778
+ textareaRef(node);
779
+ } else if (textareaRef) {
780
+ textareaRef.current = node;
781
+ }
678
782
  },
679
- children
783
+ [textareaRef]
680
784
  );
785
+ React2.useEffect(() => {
786
+ filesRef.current = files;
787
+ onAttachmentCountChange?.(files.length);
788
+ }, [files, onAttachmentCountChange]);
789
+ React2.useEffect(() => () => {
790
+ revokeAgentChatComposerFiles(filesRef.current);
791
+ revokeAgentChatComposerFiles(inFlightFilesRef.current);
792
+ }, []);
793
+ React2.useLayoutEffect(() => {
794
+ const textarea = localTextareaRef.current;
795
+ if (!textarea) return;
796
+ if (isExpanded) {
797
+ textarea.style.height = "auto";
798
+ const scrollHeight = textarea.scrollHeight;
799
+ textarea.style.height = `${Math.min(scrollHeight, 224)}px`;
800
+ textarea.style.overflowY = scrollHeight >= 224 ? "auto" : "hidden";
801
+ return;
802
+ }
803
+ textarea.style.height = "";
804
+ textarea.style.overflowY = "hidden";
805
+ }, [isExpanded, text]);
806
+ const canSubmit = !disabled && !isSubmitting && !isRunning && (text.trim().length > 0 || files.length > 0);
807
+ const handleFilesChange = (event) => {
808
+ const selected = Array.from(event.target.files || []).map(createAgentChatComposerFile);
809
+ if (selected.length > 0) {
810
+ draftRevisionRef.current += 1;
811
+ setFiles((current) => [...current, ...selected]);
812
+ }
813
+ event.currentTarget.value = "";
814
+ };
815
+ const removeFile = (fileId) => {
816
+ draftRevisionRef.current += 1;
817
+ setFiles((current) => {
818
+ const target = current.find((file) => file.id === fileId);
819
+ if (target) revokeAgentChatComposerFiles([target]);
820
+ return current.filter((file) => file.id !== fileId);
821
+ });
822
+ };
823
+ const submit = async () => {
824
+ if (!canSubmit) return;
825
+ const submittedText = text.trim();
826
+ const submittedFiles = files;
827
+ const clearedRevision = draftRevisionRef.current + 1;
828
+ draftRevisionRef.current = clearedRevision;
829
+ inFlightFilesRef.current = submittedFiles;
830
+ setIsSubmitting(true);
831
+ setText("");
832
+ setFiles([]);
833
+ try {
834
+ await onSubmit({ text: submittedText, files: submittedFiles });
835
+ revokeAgentChatComposerFiles(submittedFiles);
836
+ inFlightFilesRef.current = [];
837
+ } catch {
838
+ if (draftRevisionRef.current === clearedRevision) {
839
+ draftRevisionRef.current += 1;
840
+ setText(submittedText);
841
+ setFiles(submittedFiles);
842
+ inFlightFilesRef.current = [];
843
+ } else {
844
+ revokeAgentChatComposerFiles(submittedFiles);
845
+ inFlightFilesRef.current = [];
846
+ }
847
+ } finally {
848
+ setIsSubmitting(false);
849
+ }
850
+ };
851
+ const handleKeyDown = (event) => {
852
+ if (event.key === "Enter" && !event.shiftKey) {
853
+ event.preventDefault();
854
+ void submit();
855
+ }
856
+ };
857
+ return /* @__PURE__ */ jsxs("div", { className: cn("a24-composer relative mx-auto w-full max-w-3xl overflow-visible", className), "data-agents24-chat-composer": "", children: [
858
+ files.length > 0 ? /* @__PURE__ */ jsx3(
859
+ "div",
860
+ {
861
+ "data-testid": "agent-chat-composer-attachments",
862
+ "data-layout": attachmentLayout,
863
+ className: cn(
864
+ "a24-composer__attachments min-w-0 py-1",
865
+ 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"
866
+ ),
867
+ children: /* @__PURE__ */ jsx3(
868
+ ChatAttachmentRows,
869
+ {
870
+ attachments: files,
871
+ onRemove: (attachment) => attachment.id && removeFile(attachment.id)
872
+ }
873
+ )
874
+ }
875
+ ) : null,
876
+ /* @__PURE__ */ jsx3(
877
+ "div",
878
+ {
879
+ "data-expanded": isExpanded ? "" : void 0,
880
+ className: cn(
881
+ "a24-composer__surface relative w-full border border-input bg-background shadow-sm",
882
+ isExpanded ? "rounded-xl p-2 pb-1.5" : "rounded-xl px-2 py-[3px]"
883
+ ),
884
+ children: /* @__PURE__ */ jsxs(
885
+ "div",
886
+ {
887
+ className: "a24-composer__grid w-full",
888
+ style: {
889
+ alignItems: "center",
890
+ display: "grid",
891
+ gridTemplateAreas: isExpanded ? `"textarea textarea textarea" "plus toolbar submit"` : `"plus textarea composer mic submit"`,
892
+ gridTemplateColumns: isExpanded ? "auto 1fr auto" : "auto 1fr auto auto auto",
893
+ gap: isExpanded ? "3px 10px" : "8px"
894
+ },
895
+ children: [
896
+ /* @__PURE__ */ jsxs("div", { style: { gridArea: "plus" }, className: "a24-composer__attach-slot flex shrink-0 items-center justify-center", children: [
897
+ /* @__PURE__ */ jsx3(
898
+ "input",
899
+ {
900
+ ref: fileInputRef,
901
+ accept,
902
+ "aria-label": "Attach files",
903
+ className: "hidden",
904
+ multiple: true,
905
+ name: "attachments",
906
+ onChange: handleFilesChange,
907
+ type: "file"
908
+ }
909
+ ),
910
+ /* @__PURE__ */ jsx3(
911
+ "button",
912
+ {
913
+ "aria-label": "Attach files",
914
+ className: cn("a24-composer__attach", pillButtonClass),
915
+ disabled: disabled || isSubmitting || isRunning || !allowAttachments,
916
+ onClick: () => fileInputRef.current?.click(),
917
+ title: "Attach files",
918
+ type: "button",
919
+ children: /* @__PURE__ */ jsx3(Plus, { className: "size-4" })
920
+ }
921
+ )
922
+ ] }),
923
+ /* @__PURE__ */ jsx3("div", { style: { gridArea: "textarea" }, className: "a24-composer__textarea-slot min-w-0 w-full", children: /* @__PURE__ */ jsx3(
924
+ "textarea",
925
+ {
926
+ ref: setTextareaRef,
927
+ "aria-label": "Message",
928
+ autoComplete: "off",
929
+ className: cn(
930
+ "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",
931
+ isExpanded ? "min-h-9 max-h-[224px] py-1" : "min-h-9 h-9 max-h-9 py-2"
932
+ ),
933
+ disabled: disabled || isSubmitting || isRunning,
934
+ name: "message",
935
+ onChange: (event) => {
936
+ draftRevisionRef.current += 1;
937
+ setText(event.target.value);
938
+ },
939
+ onKeyDown: handleKeyDown,
940
+ placeholder,
941
+ rows: isExpanded ? 2 : 1,
942
+ value: text
943
+ }
944
+ ) }),
945
+ isExpanded ? /* @__PURE__ */ jsx3("div", { style: { gridArea: "toolbar" }, className: "a24-composer__toolbar flex min-w-0 items-center gap-1.5", children: inputToolbarContent }) : null,
946
+ !isExpanded ? /* @__PURE__ */ jsx3("div", { style: { gridArea: "composer" }, className: "flex min-w-0 items-center gap-1.5" }) : null,
947
+ !isExpanded ? /* @__PURE__ */ jsx3("div", { style: { gridArea: "mic" }, className: "flex shrink-0 items-center justify-end" }) : null,
948
+ /* @__PURE__ */ jsx3("div", { style: { gridArea: "submit" }, className: "a24-composer__submit-slot flex shrink-0 items-center justify-end", children: isRunning ? /* @__PURE__ */ jsx3(
949
+ "button",
950
+ {
951
+ "aria-label": "Stop generating",
952
+ className: cn("a24-composer__submit", submitButtonClass),
953
+ disabled: disabled || !onStop,
954
+ onClick: onStop,
955
+ title: "Stop",
956
+ type: "button",
957
+ children: /* @__PURE__ */ jsx3(Square, { className: "size-3.5 fill-current" })
958
+ }
959
+ ) : /* @__PURE__ */ jsx3(
960
+ "button",
961
+ {
962
+ "aria-label": files.length > 0 ? `Send ${formatFileCount(files)}` : "Send message",
963
+ className: cn("a24-composer__submit", submitButtonClass, text.trim().length > 0 && "hover:scale-105"),
964
+ disabled: !canSubmit,
965
+ onClick: () => void submit(),
966
+ title: "Send",
967
+ type: "button",
968
+ children: /* @__PURE__ */ jsx3(ArrowUp, { className: "size-4", strokeWidth: 2.5 })
969
+ }
970
+ ) })
971
+ ]
972
+ }
973
+ )
974
+ }
975
+ ),
976
+ !isExpanded && inputToolbarContent ? /* @__PURE__ */ jsx3("div", { className: "a24-composer__toolbar a24-composer__toolbar--external mt-2 flex items-center gap-2 px-1", children: inputToolbarContent }) : null
977
+ ] });
681
978
  }
682
979
 
683
980
  // src/ui/agent-chat-actions.tsx
981
+ import * as React3 from "react";
982
+ import { Check, Copy, RefreshCcw, ThumbsDown, ThumbsUp } from "lucide-react";
983
+ import { Popover } from "radix-ui";
684
984
  import { jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
685
985
  var FEEDBACK_REASONS = [
686
986
  ["incorrect", "Incorrect"],
@@ -690,6 +990,15 @@ var FEEDBACK_REASONS = [
690
990
  ["citation_issue", "Citation issue"],
691
991
  ["other", "Other"]
692
992
  ];
993
+ function formatSentAt(value) {
994
+ if (!value) return null;
995
+ const date = value instanceof Date ? value : new Date(value);
996
+ if (Number.isNaN(date.getTime())) return null;
997
+ return {
998
+ dateTime: date.toISOString(),
999
+ label: new Intl.DateTimeFormat(void 0, { timeStyle: "short" }).format(date)
1000
+ };
1001
+ }
693
1002
  function AgentChatDefaultActions({
694
1003
  additionalActions,
695
1004
  dir,
@@ -700,11 +1009,43 @@ function AgentChatDefaultActions({
700
1009
  const [feedbackTrigger, setFeedbackTrigger] = React3.useState(null);
701
1010
  const [reason, setReason] = React3.useState(handlers?.feedback?.reason ?? null);
702
1011
  const [comment, setComment] = React3.useState(handlers?.feedback?.comment ?? "");
1012
+ const [optimisticCopied, setOptimisticCopied] = React3.useState(false);
1013
+ const copyResetTimer = React3.useRef(null);
703
1014
  React3.useEffect(() => {
704
1015
  setReason(handlers?.feedback?.reason ?? null);
705
1016
  setComment(handlers?.feedback?.comment ?? "");
706
1017
  }, [handlers?.feedback?.comment, handlers?.feedback?.reason]);
707
- if (!handlers || message.role !== "assistant" || message.isFinal === false) return null;
1018
+ React3.useEffect(() => () => {
1019
+ if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current);
1020
+ }, []);
1021
+ const copied = handlers?.copied ?? optimisticCopied;
1022
+ const copyMessage = () => {
1023
+ handlers?.onCopy?.(message.content || "", message.id);
1024
+ if (handlers?.copied !== void 0) return;
1025
+ setOptimisticCopied(true);
1026
+ if (copyResetTimer.current !== null) window.clearTimeout(copyResetTimer.current);
1027
+ copyResetTimer.current = window.setTimeout(() => {
1028
+ copyResetTimer.current = null;
1029
+ setOptimisticCopied(false);
1030
+ }, 1200);
1031
+ };
1032
+ if (!handlers || message.isFinal === false) return null;
1033
+ if (message.role === "user") {
1034
+ const sentAt = formatSentAt(message.createdAt);
1035
+ return /* @__PURE__ */ jsxs2(ChatMessageActions, { className: "a24-user-message-actions", children: [
1036
+ sentAt ? /* @__PURE__ */ jsx4("time", { dateTime: sentAt.dateTime, children: sentAt.label }) : null,
1037
+ handlers.onCopy ? /* @__PURE__ */ jsx4(
1038
+ ChatMessageAction,
1039
+ {
1040
+ label: "Copy",
1041
+ onClick: copyMessage,
1042
+ tooltip: copied ? "Copied!" : "Copy to clipboard",
1043
+ className: "a24-user-message-copy",
1044
+ children: copied ? /* @__PURE__ */ jsx4(Check, { className: "a24-user-message-copy__check text-green-600" }) : /* @__PURE__ */ jsx4(Copy, { className: "a24-user-message-copy__icon" })
1045
+ }
1046
+ ) : null
1047
+ ] });
1048
+ }
708
1049
  const canWriteFeedback = Boolean(message.runId && handlers.onSetFeedback);
709
1050
  return /* @__PURE__ */ jsxs2(ChatMessageActions, { children: [
710
1051
  handlers.onRetry ? /* @__PURE__ */ jsx4(
@@ -815,9 +1156,9 @@ function AgentChatDefaultActions({
815
1156
  ChatMessageAction,
816
1157
  {
817
1158
  label: "Copy",
818
- onClick: () => handlers.onCopy?.(message.content || "", message.id),
819
- tooltip: handlers.copied ? "Copied!" : "Copy to clipboard",
820
- children: handlers.copied ? /* @__PURE__ */ jsx4(Check, { className: "size-4 text-green-600" }) : /* @__PURE__ */ jsx4(Copy, { className: "size-4" })
1159
+ onClick: copyMessage,
1160
+ tooltip: copied ? "Copied!" : "Copy to clipboard",
1161
+ children: copied ? /* @__PURE__ */ jsx4(Check, { className: "size-4 text-green-600" }) : /* @__PURE__ */ jsx4(Copy, { className: "size-4" })
821
1162
  }
822
1163
  ) : null
823
1164
  ] });
@@ -1573,10 +1914,9 @@ function ToolDisclosure({
1573
1914
  const [open, setOpen] = React6.useState(false);
1574
1915
  const contentId = React6.useId();
1575
1916
  const expandable = React6.Children.toArray(children).length > 0;
1576
- const textOnlyRoot = Boolean(root && !grouped && !showOrb);
1577
1917
  if (!expandable) {
1578
1918
  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");
1579
- return /* @__PURE__ */ jsxs4("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
1919
+ return /* @__PURE__ */ jsxs4("div", { className: "a24-tool-trigger-row", children: [
1580
1920
  onSelect && part ? /* @__PURE__ */ jsxs4("button", { className: triggerClassName, onClick: () => onSelect(part), type: "button", children: [
1581
1921
  showOrb ? /* @__PURE__ */ jsx8(ToolActivityOrb, { label: statusLabel }) : null,
1582
1922
  /* @__PURE__ */ jsx8("span", { className: "a24-tool-trigger__label", children: label })
@@ -1588,7 +1928,7 @@ function ToolDisclosure({
1588
1928
  ] });
1589
1929
  }
1590
1930
  return /* @__PURE__ */ jsxs4("div", { className: "a24-tool-disclosure", children: [
1591
- /* @__PURE__ */ jsxs4("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
1931
+ /* @__PURE__ */ jsxs4("div", { className: "a24-tool-trigger-row", children: [
1592
1932
  /* @__PURE__ */ jsxs4(
1593
1933
  "button",
1594
1934
  {
@@ -2094,11 +2434,47 @@ function AgentChatUserMessageContent({
2094
2434
  ) : null
2095
2435
  ] });
2096
2436
  }
2437
+ function AgentChatUserMessageActionRow({
2438
+ children,
2439
+ className
2440
+ }) {
2441
+ const rowRef = React8.useRef(null);
2442
+ const [visible, setVisible] = React8.useState(false);
2443
+ React8.useEffect(() => {
2444
+ const message = rowRef.current?.closest('[data-slot="message"]');
2445
+ if (!message) return;
2446
+ const show = () => setVisible(true);
2447
+ const hide = () => setVisible(false);
2448
+ const hideWhenFocusLeaves = (event) => {
2449
+ if (!message.contains(event.relatedTarget)) hide();
2450
+ };
2451
+ message.addEventListener("mouseenter", show);
2452
+ message.addEventListener("mouseleave", hide);
2453
+ message.addEventListener("focusin", show);
2454
+ message.addEventListener("focusout", hideWhenFocusLeaves);
2455
+ return () => {
2456
+ message.removeEventListener("mouseenter", show);
2457
+ message.removeEventListener("mouseleave", hide);
2458
+ message.removeEventListener("focusin", show);
2459
+ message.removeEventListener("focusout", hideWhenFocusLeaves);
2460
+ };
2461
+ }, []);
2462
+ return /* @__PURE__ */ jsx10(
2463
+ "div",
2464
+ {
2465
+ ref: rowRef,
2466
+ className: cn("a24-chat-message-action-row--user", visible && "is-visible", className),
2467
+ style: { opacity: visible ? 1 : 0, pointerEvents: visible ? "auto" : "none" },
2468
+ children
2469
+ }
2470
+ );
2471
+ }
2097
2472
  function AgentChatMessage({
2098
2473
  actionHandlers,
2099
2474
  additionalActions,
2100
2475
  actions,
2101
2476
  actionsClassName,
2477
+ attachmentContentResolver,
2102
2478
  className,
2103
2479
  contentClassName,
2104
2480
  dir,
@@ -2131,48 +2507,58 @@ function AgentChatMessage({
2131
2507
  message
2132
2508
  }
2133
2509
  ) : null);
2134
- return /* @__PURE__ */ jsxs6(ChatMessage, { className, dir, from: message.role, children: [
2135
- showAttachments ? /* @__PURE__ */ jsx10(ChatAttachments, { className: "mb-2", dir, children: message.attachments?.map((attachment, index) => /* @__PURE__ */ jsx10(
2136
- ChatAttachment,
2137
- {
2138
- data: attachment,
2139
- dir: "ltr"
2140
- },
2141
- String(attachment.url || attachment.filename || index)
2142
- )) }) : null,
2143
- shouldRenderContent ? /* @__PURE__ */ jsxs6(
2144
- ChatMessageContent,
2145
- {
2146
- className: cn(isAssistant ? "bg-transparent p-0" : void 0, contentClassName),
2147
- dir,
2148
- from: message.role,
2149
- children: [
2150
- isAssistant ? renderAssistantContent ? renderAssistantContent({ isStreaming, message }) : /* @__PURE__ */ jsx10(
2151
- Timeline,
2152
- {
2153
- getHitlActionState,
2154
- isLoading: isStreaming,
2155
- message,
2156
- nodeActivityMode,
2157
- onHitlAction,
2158
- parts: message.parts
2159
- }
2160
- ) : null,
2161
- !isAssistant ? renderUserContent ? renderUserContent({ message }) : resolvedUserContent ? /* @__PURE__ */ jsx10(
2162
- AgentChatUserMessageContent,
2163
- {
2164
- collapsedLines: userMessageCollapsedLines,
2165
- collapseThreshold: userMessageCollapseThreshold,
2166
- content: resolvedUserContent,
2167
- showLessLabel: userMessageShowLessLabel,
2168
- showMoreLabel: userMessageShowMoreLabel
2169
- }
2170
- ) : null : null
2171
- ]
2172
- }
2173
- ) : null,
2174
- renderedActions ? /* @__PURE__ */ jsx10("div", { className: cn(dir === "rtl" ? "mt-1 h-9 w-full" : "mt-1 h-9 w-full -translate-x-2", actionsClassName), children: renderedActions }) : null
2175
- ] });
2510
+ const defaultUserActions = !actions && Boolean(actionHandlers) && !isAssistant;
2511
+ return /* @__PURE__ */ jsxs6(
2512
+ ChatMessage,
2513
+ {
2514
+ className,
2515
+ dir,
2516
+ from: message.role,
2517
+ children: [
2518
+ showAttachments ? /* @__PURE__ */ jsx10(
2519
+ ChatAttachmentRows,
2520
+ {
2521
+ attachments: message.attachments || [],
2522
+ className: "mb-2",
2523
+ dir,
2524
+ resolveContent: attachmentContentResolver
2525
+ }
2526
+ ) : null,
2527
+ shouldRenderContent ? /* @__PURE__ */ jsxs6(
2528
+ ChatMessageContent,
2529
+ {
2530
+ className: cn(isAssistant ? "bg-transparent p-0" : void 0, contentClassName),
2531
+ dir,
2532
+ from: message.role,
2533
+ children: [
2534
+ isAssistant ? renderAssistantContent ? renderAssistantContent({ isStreaming, message }) : /* @__PURE__ */ jsx10(
2535
+ Timeline,
2536
+ {
2537
+ getHitlActionState,
2538
+ isLoading: isStreaming,
2539
+ message,
2540
+ nodeActivityMode,
2541
+ onHitlAction,
2542
+ parts: message.parts
2543
+ }
2544
+ ) : null,
2545
+ !isAssistant ? renderUserContent ? renderUserContent({ message }) : resolvedUserContent ? /* @__PURE__ */ jsx10(
2546
+ AgentChatUserMessageContent,
2547
+ {
2548
+ collapsedLines: userMessageCollapsedLines,
2549
+ collapseThreshold: userMessageCollapseThreshold,
2550
+ content: resolvedUserContent,
2551
+ showLessLabel: userMessageShowLessLabel,
2552
+ showMoreLabel: userMessageShowMoreLabel
2553
+ }
2554
+ ) : null : null
2555
+ ]
2556
+ }
2557
+ ) : null,
2558
+ renderedActions ? defaultUserActions ? /* @__PURE__ */ jsx10(AgentChatUserMessageActionRow, { className: actionsClassName, children: renderedActions }) : /* @__PURE__ */ jsx10("div", { className: cn(dir === "rtl" ? "mt-1 h-9 w-full" : "mt-1 h-9 w-full -translate-x-2", actionsClassName), children: renderedActions }) : null
2559
+ ]
2560
+ }
2561
+ );
2176
2562
  }
2177
2563
 
2178
2564
  // src/ui/agent-chat-context-status.tsx
@@ -2577,6 +2963,7 @@ export {
2577
2963
  AgentChatToolPart,
2578
2964
  AgentChatToolRow,
2579
2965
  AgentChatToolSession,
2966
+ AgentChatUserMessageActionRow,
2580
2967
  AgentChatUserMessageContent,
2581
2968
  AgentResponseTimeline,
2582
2969
  AskUserInteraction,
@@ -2594,6 +2981,7 @@ export {
2594
2981
  BubbleGroup,
2595
2982
  BubbleReactions,
2596
2983
  ChatAttachment,
2984
+ ChatAttachmentRows,
2597
2985
  ChatAttachments,
2598
2986
  ChatMessage,
2599
2987
  ChatMessageAction,
@@ -2607,6 +2995,7 @@ export {
2607
2995
  ChatMessageAction as MessageAction,
2608
2996
  ChatMessageActions as MessageActions,
2609
2997
  ChatAttachment as MessageAttachment,
2998
+ ChatAttachmentRows as MessageAttachmentRows,
2610
2999
  ChatAttachments as MessageAttachments,
2611
3000
  MessageAvatar,
2612
3001
  ChatMessageContent as MessageContent,
@@ -2622,6 +3011,7 @@ export {
2622
3011
  bubbleVariants,
2623
3012
  cn,
2624
3013
  contextStatusMetrics,
3014
+ createAgentChatComposerFile,
2625
3015
  hitlActionLabel,
2626
3016
  hitlTitle,
2627
3017
  isAgentChatSubagent,
@@ -2630,6 +3020,7 @@ export {
2630
3020
  markerVariants,
2631
3021
  partitionAgentChatToolTimeline,
2632
3022
  resolvedHitlLabel,
3023
+ revokeAgentChatComposerFiles,
2633
3024
  shouldCollapseUserMessage,
2634
3025
  useAudioRecorder
2635
3026
  };