@agents24/chat-react 0.5.4 → 0.5.6

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 (34) hide show
  1. package/README.md +8 -2
  2. package/compatibility.json +4 -4
  3. package/dist/runtime/index.cjs +127 -18
  4. package/dist/runtime/index.cjs.map +1 -1
  5. package/dist/runtime/index.js +125 -18
  6. package/dist/runtime/index.js.map +1 -1
  7. package/dist/runtime/use-agent-chat-runtime.d.ts +17 -2
  8. package/dist/scaffold/app.d.ts +1 -0
  9. package/dist/scaffold/components/chat/chat-composer.d.ts +10 -2
  10. package/dist/scaffold/components/chat/chat-message.d.ts +3 -1
  11. package/dist/scaffold/components/chat/chat-shell.d.ts +3 -1
  12. package/dist/scaffold/index.cjs +415 -253
  13. package/dist/scaffold/index.cjs.map +1 -1
  14. package/dist/scaffold/index.js +408 -240
  15. package/dist/scaffold/index.js.map +1 -1
  16. package/dist/styles.css +18 -4
  17. package/dist/ui/adapters.d.ts +14 -2
  18. package/dist/ui/agent-chat-composer.d.ts +2 -9
  19. package/dist/ui/agent-chat-message.d.ts +3 -1
  20. package/dist/ui/composer-attachments.d.ts +11 -0
  21. package/dist/ui/index.cjs +653 -343
  22. package/dist/ui/index.cjs.map +1 -1
  23. package/dist/ui/index.d.ts +1 -0
  24. package/dist/ui/index.js +638 -328
  25. package/dist/ui/index.js.map +1 -1
  26. package/package.json +4 -3
  27. package/scaffold/src/app.tsx +2 -1
  28. package/scaffold/src/chat.css +13 -1
  29. package/scaffold/src/components/chat/chat-composer.tsx +107 -27
  30. package/scaffold/src/components/chat/chat-message.tsx +8 -9
  31. package/scaffold/src/components/chat/chat-shell.tsx +24 -11
  32. package/scaffold/src/components/ui/button.tsx +1 -1
  33. package/scaffold/src/components/ui/dropdown-menu.tsx +1 -1
  34. package/scaffold/src/components/ui/sidebar.tsx +4 -4
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
302
  "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",
303
+ isUser ? "a24-chat-message--user is-user max-w-full justify-end" : "a24-chat-message--assistant is-assistant max-w-full",
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,
@@ -603,60 +357,310 @@ function ChatMessageAction({
603
357
  type,
604
358
  ...props
605
359
  },
606
- children,
607
- React2.createElement(
608
- "span",
609
- { className: "sr-only" },
610
- label || tooltipLabel
611
- )
360
+ children,
361
+ React.createElement(
362
+ "span",
363
+ { className: "sr-only" },
364
+ label || tooltipLabel
365
+ )
366
+ );
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
612
601
  );
613
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
  }
@@ -666,12 +670,11 @@ function ChatAttachments({
666
670
  ...props
667
671
  }) {
668
672
  if (!children) return null;
669
- return React2.createElement(
673
+ return React.createElement(
670
674
  AttachmentGroup,
671
675
  {
672
676
  className: cn(
673
- props.dir === "rtl" ? "mr-auto" : "ml-auto",
674
- "w-fit flex-wrap overflow-visible",
677
+ "a24-chat-attachments w-full flex-nowrap",
675
678
  className
676
679
  ),
677
680
  ...props
@@ -679,8 +682,305 @@ function ChatAttachments({
679
682
  children
680
683
  );
681
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",
754
+ className,
755
+ disabled = false,
756
+ forceExpanded = false,
757
+ inputToolbarContent,
758
+ isRunning = false,
759
+ onAttachmentCountChange,
760
+ onStop,
761
+ onSubmit,
762
+ placeholder = "Message the agent",
763
+ textareaRef
764
+ }) {
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
+ }
782
+ },
783
+ [textareaRef]
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
+ ] });
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"],
@@ -1559,6 +1859,7 @@ function toolRowLabel(part) {
1559
1859
  function ToolDisclosure({
1560
1860
  children,
1561
1861
  error,
1862
+ grouped,
1562
1863
  label,
1563
1864
  loading,
1564
1865
  onExpandedChange,
@@ -1572,9 +1873,10 @@ function ToolDisclosure({
1572
1873
  const [open, setOpen] = React6.useState(false);
1573
1874
  const contentId = React6.useId();
1574
1875
  const expandable = React6.Children.toArray(children).length > 0;
1876
+ const textOnlyRoot = Boolean(root && !grouped && !showOrb);
1575
1877
  if (!expandable) {
1576
1878
  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");
1577
- return /* @__PURE__ */ jsxs4("div", { className: "a24-tool-trigger-row", children: [
1879
+ return /* @__PURE__ */ jsxs4("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
1578
1880
  onSelect && part ? /* @__PURE__ */ jsxs4("button", { className: triggerClassName, onClick: () => onSelect(part), type: "button", children: [
1579
1881
  showOrb ? /* @__PURE__ */ jsx8(ToolActivityOrb, { label: statusLabel }) : null,
1580
1882
  /* @__PURE__ */ jsx8("span", { className: "a24-tool-trigger__label", children: label })
@@ -1586,7 +1888,7 @@ function ToolDisclosure({
1586
1888
  ] });
1587
1889
  }
1588
1890
  return /* @__PURE__ */ jsxs4("div", { className: "a24-tool-disclosure", children: [
1589
- /* @__PURE__ */ jsxs4("div", { className: "a24-tool-trigger-row", children: [
1891
+ /* @__PURE__ */ jsxs4("div", { className: cn("a24-tool-trigger-row", textOnlyRoot && "a24-tool-trigger-row--text"), children: [
1590
1892
  /* @__PURE__ */ jsxs4(
1591
1893
  "button",
1592
1894
  {
@@ -1711,6 +2013,7 @@ function AgentChatToolSession({
1711
2013
  {
1712
2014
  accessory: slots?.renderToolAccessory?.(context),
1713
2015
  error: failed,
2016
+ grouped: true,
1714
2017
  label: labelContent,
1715
2018
  loading: presentingActivity,
1716
2019
  onExpandedChange: slots?.onExpandedChange,
@@ -1755,6 +2058,7 @@ function AgentChatSubagentGroup({ isLatestActivity = true, isLive = false, part,
1755
2058
  {
1756
2059
  accessory: slots?.renderToolAccessory?.(context),
1757
2060
  error: failed,
2061
+ grouped: true,
1758
2062
  label: labelContent,
1759
2063
  loading: presentingActivity,
1760
2064
  onExpandedChange: slots?.onExpandedChange,
@@ -2095,6 +2399,7 @@ function AgentChatMessage({
2095
2399
  additionalActions,
2096
2400
  actions,
2097
2401
  actionsClassName,
2402
+ attachmentContentResolver,
2098
2403
  className,
2099
2404
  contentClassName,
2100
2405
  dir,
@@ -2128,14 +2433,15 @@ function AgentChatMessage({
2128
2433
  }
2129
2434
  ) : null);
2130
2435
  return /* @__PURE__ */ jsxs6(ChatMessage, { className, dir, from: message.role, children: [
2131
- showAttachments ? /* @__PURE__ */ jsx10(ChatAttachments, { className: "mb-2", dir, children: message.attachments?.map((attachment, index) => /* @__PURE__ */ jsx10(
2132
- ChatAttachment,
2436
+ showAttachments ? /* @__PURE__ */ jsx10(
2437
+ ChatAttachmentRows,
2133
2438
  {
2134
- data: attachment,
2135
- dir: "ltr"
2136
- },
2137
- String(attachment.url || attachment.filename || index)
2138
- )) }) : null,
2439
+ attachments: message.attachments || [],
2440
+ className: "mb-2",
2441
+ dir,
2442
+ resolveContent: attachmentContentResolver
2443
+ }
2444
+ ) : null,
2139
2445
  shouldRenderContent ? /* @__PURE__ */ jsxs6(
2140
2446
  ChatMessageContent,
2141
2447
  {
@@ -2590,6 +2896,7 @@ export {
2590
2896
  BubbleGroup,
2591
2897
  BubbleReactions,
2592
2898
  ChatAttachment,
2899
+ ChatAttachmentRows,
2593
2900
  ChatAttachments,
2594
2901
  ChatMessage,
2595
2902
  ChatMessageAction,
@@ -2603,6 +2910,7 @@ export {
2603
2910
  ChatMessageAction as MessageAction,
2604
2911
  ChatMessageActions as MessageActions,
2605
2912
  ChatAttachment as MessageAttachment,
2913
+ ChatAttachmentRows as MessageAttachmentRows,
2606
2914
  ChatAttachments as MessageAttachments,
2607
2915
  MessageAvatar,
2608
2916
  ChatMessageContent as MessageContent,
@@ -2618,6 +2926,7 @@ export {
2618
2926
  bubbleVariants,
2619
2927
  cn,
2620
2928
  contextStatusMetrics,
2929
+ createAgentChatComposerFile,
2621
2930
  hitlActionLabel,
2622
2931
  hitlTitle,
2623
2932
  isAgentChatSubagent,
@@ -2626,6 +2935,7 @@ export {
2626
2935
  markerVariants,
2627
2936
  partitionAgentChatToolTimeline,
2628
2937
  resolvedHitlLabel,
2938
+ revokeAgentChatComposerFiles,
2629
2939
  shouldCollapseUserMessage,
2630
2940
  useAudioRecorder
2631
2941
  };