@invergent/agent-chat-react 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cn } from './chunk-QSC4UIVT.js';
2
2
  import { createContext, memo, useRef, useEffect, useCallback, useMemo, useState, lazy, useContext, Suspense, Children } from 'react';
3
- import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, EyeIcon, GlobeIcon, CheckIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, InboxIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
3
+ import { ChevronDownIcon, AlertTriangle, ChevronDown, ChevronRight, RefreshCw, File as File$1, FileImage, FileAudio, FileVideo, FileText, FileCode, FileArchive, FileSpreadsheet, ChevronRightIcon, GitBranchIcon, XIcon, ThumbsUpIcon, ThumbsDownIcon, ActivityIcon, Loader2Icon, EyeIcon, GlobeIcon, CheckIcon, SearchIcon, ListTodoIcon, XCircleIcon, CheckCircle2Icon, Code, CopyIcon, TerminalIcon, Trash2Icon, CircleDotIcon, CircleCheckIcon, CircleXIcon, SkullIcon, ClockIcon, CheckCircleIcon, CircleIcon, InboxIcon, CalendarClockIcon, UsersIcon, MessageSquareIcon, ZapIcon, Maximize2Icon, FolderOpenIcon, UploadIcon, RefreshCwIcon, AlertCircleIcon, ExternalLinkIcon, ClipboardCheckIcon, ShieldAlertIcon, TimerIcon, SquareIcon, ArrowDownIcon, RotateCcwIcon, MousePointer2Icon, DownloadIcon, TrashIcon, ImageIcon, FileTextIcon, FolderIcon, FileIcon, ChevronLeftIcon, MinusIcon, PlusIcon, CornerDownLeftIcon } from 'lucide-react';
4
4
  import { cva } from 'class-variance-authority';
5
5
  import { Collapsible as Collapsible$1, Slot, Dialog as Dialog$1, ScrollArea as ScrollArea$1, Tooltip as Tooltip$1, Tabs as Tabs$1, Popover as Popover$1, DropdownMenu as DropdownMenu$1, HoverCard as HoverCard$1, Progress as Progress$1 } from 'radix-ui';
6
6
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
@@ -4193,11 +4193,72 @@ var ChatMessage = memo(function ChatMessage2({
4193
4193
  className: "max-w-64 max-h-48 rounded-lg border border-border object-contain"
4194
4194
  },
4195
4195
  i
4196
+ )) }),
4197
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2 mt-2", children: message.attachments.map((a, i) => /* @__PURE__ */ jsx(
4198
+ AttachmentChip,
4199
+ {
4200
+ attachment: a,
4201
+ onSelect: onFileSelect
4202
+ },
4203
+ a.path ?? `pending-${i}`
4196
4204
  )) })
4197
4205
  ] }) });
4198
4206
  }
4199
4207
  return /* @__PURE__ */ jsx(AssistantMessage, { message, isLast, onFileSelect });
4200
4208
  });
4209
+ function iconForMime(mime) {
4210
+ if (!mime) return File$1;
4211
+ if (mime.startsWith("image/")) return FileImage;
4212
+ if (mime.startsWith("audio/")) return FileAudio;
4213
+ if (mime.startsWith("video/")) return FileVideo;
4214
+ if (mime === "application/pdf") return FileText;
4215
+ if (mime.startsWith("text/")) return FileText;
4216
+ if (mime === "application/json" || mime === "application/xml" || mime.endsWith("+xml") || mime.endsWith("+json") || mime.includes("javascript") || mime.includes("typescript")) {
4217
+ return FileCode;
4218
+ }
4219
+ if (mime === "application/zip" || mime === "application/x-7z-compressed" || mime === "application/x-tar" || mime === "application/gzip" || mime === "application/x-rar-compressed") {
4220
+ return FileArchive;
4221
+ }
4222
+ if (mime === "text/csv" || mime === "application/vnd.ms-excel" || mime.includes("spreadsheet")) {
4223
+ return FileSpreadsheet;
4224
+ }
4225
+ return File$1;
4226
+ }
4227
+ function formatBytes(n) {
4228
+ if (n == null || !Number.isFinite(n) || n < 0) return "";
4229
+ for (const [unit, divisor] of [
4230
+ ["GB", 1e9],
4231
+ ["MB", 1e6],
4232
+ ["KB", 1e3]
4233
+ ]) {
4234
+ if (n >= divisor) return `${(n / divisor).toFixed(1)} ${unit}`;
4235
+ }
4236
+ return `${n} B`;
4237
+ }
4238
+ function AttachmentChip({
4239
+ attachment,
4240
+ onSelect
4241
+ }) {
4242
+ const Icon = iconForMime(attachment.mimeType);
4243
+ const clickable = !!attachment.path && !!onSelect;
4244
+ const sizeLabel = formatBytes(attachment.size);
4245
+ return /* @__PURE__ */ jsxs(
4246
+ "button",
4247
+ {
4248
+ type: "button",
4249
+ onClick: clickable ? () => onSelect(attachment.path) : void 0,
4250
+ disabled: !clickable,
4251
+ title: attachment.filename,
4252
+ "aria-label": clickable ? `Open ${attachment.filename}` : `${attachment.filename} (uploading)`,
4253
+ className: "inline-flex items-center gap-2 rounded-md border border-border bg-muted/40 px-2 py-1 text-xs max-w-[16rem] " + (clickable ? "hover:bg-muted cursor-pointer" : "opacity-60 cursor-default"),
4254
+ children: [
4255
+ /* @__PURE__ */ jsx(Icon, { className: "size-3.5 shrink-0" }),
4256
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: attachment.filename }),
4257
+ sizeLabel && /* @__PURE__ */ jsx("span", { className: "text-muted-foreground shrink-0", children: sizeLabel })
4258
+ ]
4259
+ }
4260
+ );
4261
+ }
4201
4262
  function AssistantMessage({
4202
4263
  message,
4203
4264
  isLast,
@@ -4338,6 +4399,25 @@ function Command({
4338
4399
  }
4339
4400
  );
4340
4401
  }
4402
+ function CommandInput({
4403
+ className,
4404
+ ...props
4405
+ }) {
4406
+ return /* @__PURE__ */ jsx("div", { "data-slot": "command-input-wrapper", className: "p-1", children: /* @__PURE__ */ jsxs(InputGroup, { className: "border-transparent border-b-input bg-transparent px-3", children: [
4407
+ /* @__PURE__ */ jsx(
4408
+ Command$1.Input,
4409
+ {
4410
+ "data-slot": "command-input",
4411
+ className: cn(
4412
+ "w-full px-2 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
4413
+ className
4414
+ ),
4415
+ ...props
4416
+ }
4417
+ ),
4418
+ /* @__PURE__ */ jsx(InputGroupAddon, { children: /* @__PURE__ */ jsx(SearchIcon, { className: "size-3.5 shrink-0 opacity-50" }) })
4419
+ ] }) });
4420
+ }
4341
4421
  function CommandList({
4342
4422
  className,
4343
4423
  ...props
@@ -4383,6 +4463,19 @@ function CommandGroup({
4383
4463
  }
4384
4464
  );
4385
4465
  }
4466
+ function CommandSeparator({
4467
+ className,
4468
+ ...props
4469
+ }) {
4470
+ return /* @__PURE__ */ jsx(
4471
+ Command$1.Separator,
4472
+ {
4473
+ "data-slot": "command-separator",
4474
+ className: cn("-mx-1.5 my-1.5 h-px bg-border/50", className),
4475
+ ...props
4476
+ }
4477
+ );
4478
+ }
4386
4479
  function CommandItem({
4387
4480
  className,
4388
4481
  children,
@@ -4552,7 +4645,8 @@ var PromptInputProvider = ({
4552
4645
  id: nanoid(),
4553
4646
  mediaType: file.type,
4554
4647
  type: "file",
4555
- url: URL.createObjectURL(file)
4648
+ url: URL.createObjectURL(file),
4649
+ file
4556
4650
  }))
4557
4651
  ]);
4558
4652
  }, []);
@@ -4734,7 +4828,8 @@ var PromptInput = ({
4734
4828
  id: nanoid(),
4735
4829
  mediaType: file.type,
4736
4830
  type: "file",
4737
- url: URL.createObjectURL(file)
4831
+ url: URL.createObjectURL(file),
4832
+ file
4738
4833
  });
4739
4834
  }
4740
4835
  return [...prev, ...next];
@@ -4934,15 +5029,17 @@ var PromptInput = ({
4934
5029
  }
4935
5030
  try {
4936
5031
  const convertedFiles = await Promise.all(
4937
- files.map(async ({ id: _id, ...item }) => {
4938
- if (item.url?.startsWith("blob:")) {
4939
- const dataUrl = await convertBlobUrlToDataUrl(item.url);
5032
+ files.map(async (entry) => {
5033
+ const isBlob = entry.url?.startsWith("blob:");
5034
+ const isImage = entry.mediaType?.startsWith("image/");
5035
+ if (isBlob && isImage && entry.url) {
5036
+ const dataUrl = await convertBlobUrlToDataUrl(entry.url);
4940
5037
  return {
4941
- ...item,
4942
- url: dataUrl ?? item.url
5038
+ ...entry,
5039
+ url: dataUrl ?? entry.url
4943
5040
  };
4944
5041
  }
4945
- return item;
5042
+ return entry;
4946
5043
  })
4947
5044
  );
4948
5045
  const result = onSubmit({ files: convertedFiles, text }, event);
@@ -5245,7 +5342,7 @@ var Context = ({
5245
5342
  () => ({ maxTokens, modelId, usage, usedTokens }),
5246
5343
  [maxTokens, modelId, usage, usedTokens]
5247
5344
  );
5248
- return /* @__PURE__ */ jsx(ContextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HoverCard, { closeDelay: 0, openDelay: 0, ...props }) });
5345
+ return /* @__PURE__ */ jsx(ContextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(HoverCard, { closeDelay: 100, openDelay: 0, ...props }) });
5249
5346
  };
5250
5347
  var ContextIcon = () => {
5251
5348
  const { usedTokens, maxTokens } = useContextValue();
@@ -5485,6 +5582,125 @@ var ContextCacheUsage = ({
5485
5582
  }
5486
5583
  );
5487
5584
  };
5585
+ var itemVariants = cva(
5586
+ "group/item flex w-full flex-wrap items-center rounded-none border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted",
5587
+ {
5588
+ variants: {
5589
+ variant: {
5590
+ default: "border-transparent",
5591
+ outline: "border-border",
5592
+ muted: "border-transparent bg-muted/50"
5593
+ },
5594
+ size: {
5595
+ default: "gap-3.5 px-4 py-3.5",
5596
+ sm: "gap-3.5 px-3.5 py-3",
5597
+ xs: "gap-2.5 px-3 py-2.5 in-data-[slot=dropdown-menu-content]:p-0"
5598
+ }
5599
+ },
5600
+ defaultVariants: {
5601
+ variant: "default",
5602
+ size: "default"
5603
+ }
5604
+ }
5605
+ );
5606
+ function Item({
5607
+ className,
5608
+ variant = "default",
5609
+ size = "default",
5610
+ asChild = false,
5611
+ ...props
5612
+ }) {
5613
+ const Comp = asChild ? Slot.Root : "div";
5614
+ return /* @__PURE__ */ jsx(
5615
+ Comp,
5616
+ {
5617
+ "data-slot": "item",
5618
+ "data-variant": variant,
5619
+ "data-size": size,
5620
+ className: cn(itemVariants({ variant, size, className })),
5621
+ ...props
5622
+ }
5623
+ );
5624
+ }
5625
+ var itemMediaVariants = cva(
5626
+ "flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none",
5627
+ {
5628
+ variants: {
5629
+ variant: {
5630
+ default: "bg-transparent",
5631
+ icon: "[&_svg:not([class*='size-'])]:size-4",
5632
+ image: "size-10 overflow-hidden rounded-none group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 group-data-[size=xs]/item:rounded-none [&_img]:size-full [&_img]:object-cover"
5633
+ }
5634
+ },
5635
+ defaultVariants: {
5636
+ variant: "default"
5637
+ }
5638
+ }
5639
+ );
5640
+ function ItemMedia({
5641
+ className,
5642
+ variant = "default",
5643
+ ...props
5644
+ }) {
5645
+ return /* @__PURE__ */ jsx(
5646
+ "div",
5647
+ {
5648
+ "data-slot": "item-media",
5649
+ "data-variant": variant,
5650
+ className: cn(itemMediaVariants({ variant, className })),
5651
+ ...props
5652
+ }
5653
+ );
5654
+ }
5655
+ function ItemContent({ className, ...props }) {
5656
+ return /* @__PURE__ */ jsx(
5657
+ "div",
5658
+ {
5659
+ "data-slot": "item-content",
5660
+ className: cn(
5661
+ "flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0.5 [&+[data-slot=item-content]]:flex-none",
5662
+ className
5663
+ ),
5664
+ ...props
5665
+ }
5666
+ );
5667
+ }
5668
+ function ItemTitle({ className, ...props }) {
5669
+ return /* @__PURE__ */ jsx(
5670
+ "div",
5671
+ {
5672
+ "data-slot": "item-title",
5673
+ className: cn(
5674
+ "line-clamp-1 flex w-fit items-center gap-2 text-xs leading-snug font-semibold uppercase underline-offset-4",
5675
+ className
5676
+ ),
5677
+ ...props
5678
+ }
5679
+ );
5680
+ }
5681
+ function ItemDescription({ className, ...props }) {
5682
+ return /* @__PURE__ */ jsx(
5683
+ "p",
5684
+ {
5685
+ "data-slot": "item-description",
5686
+ className: cn(
5687
+ "line-clamp-2 text-left text-sm leading-relaxed font-normal text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
5688
+ className
5689
+ ),
5690
+ ...props
5691
+ }
5692
+ );
5693
+ }
5694
+ function ItemActions({ className, ...props }) {
5695
+ return /* @__PURE__ */ jsx(
5696
+ "div",
5697
+ {
5698
+ "data-slot": "item-actions",
5699
+ className: cn("flex items-center gap-2", className),
5700
+ ...props
5701
+ }
5702
+ );
5703
+ }
5488
5704
  function Popover({
5489
5705
  ...props
5490
5706
  }) {
@@ -5518,36 +5734,82 @@ function PopoverAnchor({
5518
5734
  function ChatComposer(props) {
5519
5735
  return /* @__PURE__ */ jsx(PromptInputProvider, { children: /* @__PURE__ */ jsx(ChatComposerInner, { ...props }) });
5520
5736
  }
5737
+ function iconForMime2(mime) {
5738
+ if (!mime) return File$1;
5739
+ if (mime.startsWith("audio/")) return FileAudio;
5740
+ if (mime.startsWith("video/")) return FileVideo;
5741
+ if (mime === "application/pdf" || mime.startsWith("text/")) return FileText;
5742
+ if (mime === "application/json" || mime === "application/xml" || mime.endsWith("+xml") || mime.endsWith("+json") || mime.includes("javascript") || mime.includes("typescript")) {
5743
+ return FileCode;
5744
+ }
5745
+ if (mime === "application/zip" || mime === "application/x-7z-compressed" || mime === "application/x-tar" || mime === "application/gzip" || mime === "application/x-rar-compressed") {
5746
+ return FileArchive;
5747
+ }
5748
+ if (mime === "text/csv" || mime === "application/vnd.ms-excel" || mime.includes("spreadsheet")) {
5749
+ return FileSpreadsheet;
5750
+ }
5751
+ return File$1;
5752
+ }
5753
+ function formatBytes2(n) {
5754
+ if (n == null || !Number.isFinite(n) || n < 0) return "";
5755
+ for (const [unit, divisor] of [
5756
+ ["GB", 1e9],
5757
+ ["MB", 1e6],
5758
+ ["KB", 1e3]
5759
+ ]) {
5760
+ if (n >= divisor) return `${(n / divisor).toFixed(1)} ${unit}`;
5761
+ }
5762
+ return `${n} B`;
5763
+ }
5521
5764
  function AttachmentPreviewStrip() {
5522
5765
  const attachments = useProviderAttachments();
5523
5766
  if (attachments.files.length === 0) return null;
5524
- return /* @__PURE__ */ jsx("div", { className: "flex gap-2 px-3 pt-2 pb-1 flex-wrap", children: attachments.files.map((file) => /* @__PURE__ */ jsxs("div", { className: "relative group", children: [
5525
- file.mediaType?.startsWith("image/") && file.url ? /* @__PURE__ */ jsx(
5526
- "img",
5527
- {
5528
- src: file.url,
5529
- alt: file.filename,
5530
- className: "h-16 w-16 rounded-lg border border-border object-cover"
5531
- }
5532
- ) : /* @__PURE__ */ jsx("div", { className: "h-16 w-16 rounded-lg border border-border bg-muted flex items-center justify-center text-[10px] text-muted-foreground truncate px-1", children: file.filename }),
5533
- /* @__PURE__ */ jsx(
5534
- "button",
5767
+ return /* @__PURE__ */ jsx("div", { className: "flex gap-2 px-3 pt-2 pb-1 flex-wrap", children: attachments.files.map((file) => {
5768
+ const isImage = file.mediaType?.startsWith("image/") && !!file.url;
5769
+ const sizeLabel = formatBytes2(file.file?.size);
5770
+ const Icon = iconForMime2(file.mediaType);
5771
+ return /* @__PURE__ */ jsxs(
5772
+ Item,
5535
5773
  {
5536
- type: "button",
5537
- onClick: () => attachments.remove(file.id),
5538
- className: "absolute -top-1.5 -right-1.5 hidden group-hover:flex items-center justify-center w-4 h-4 rounded-full bg-destructive text-destructive-foreground text-[10px]",
5539
- children: "\xD7"
5540
- }
5541
- )
5542
- ] }, file.id)) });
5774
+ variant: "outline",
5775
+ size: "xs",
5776
+ className: "group w-auto max-w-[18rem] rounded-md",
5777
+ title: file.filename,
5778
+ children: [
5779
+ /* @__PURE__ */ jsx(ItemMedia, { variant: isImage ? "image" : "icon", children: isImage ? /* @__PURE__ */ jsx("img", { src: file.url, alt: file.filename }) : /* @__PURE__ */ jsx(Icon, {}) }),
5780
+ /* @__PURE__ */ jsxs(ItemContent, { className: "min-w-0", children: [
5781
+ /* @__PURE__ */ jsx(ItemTitle, { className: "block w-full truncate normal-case font-medium text-foreground", children: file.filename }),
5782
+ sizeLabel && /* @__PURE__ */ jsx(ItemDescription, { className: "text-xs", children: sizeLabel })
5783
+ ] }),
5784
+ /* @__PURE__ */ jsx(ItemActions, { children: /* @__PURE__ */ jsx(
5785
+ "button",
5786
+ {
5787
+ type: "button",
5788
+ onClick: () => attachments.remove(file.id),
5789
+ "aria-label": `Remove ${file.filename}`,
5790
+ className: "hidden group-hover:flex items-center justify-center w-4 h-4 rounded-full bg-destructive text-destructive-foreground text-[10px]",
5791
+ children: "\xD7"
5792
+ }
5793
+ ) })
5794
+ ]
5795
+ },
5796
+ file.id
5797
+ );
5798
+ }) });
5543
5799
  }
5800
+ var MAX_IMAGES_PER_MESSAGE = 5;
5801
+ var MAX_IMAGE_BYTES = 2e7;
5802
+ var MAX_ATTACHMENTS_PER_MESSAGE = 10;
5803
+ var MAX_ATTACHMENT_BYTES = 5e7;
5804
+ var MAX_ATTACHMENTS_TOTAL_BYTES = 2e8;
5544
5805
  function ChatComposerInner({
5545
5806
  onSend,
5546
5807
  onStop,
5547
5808
  isRunning,
5548
5809
  disabled = false,
5549
5810
  disabledReason,
5550
- tokenUsage
5811
+ tokenUsage,
5812
+ onComposerError
5551
5813
  }) {
5552
5814
  const { adapter } = useAgentChatAdapterContext();
5553
5815
  const { textInput } = usePromptInputController();
@@ -5572,8 +5834,8 @@ function ChatComposerInner({
5572
5834
  setWasClosed(true);
5573
5835
  }
5574
5836
  }, [adapter, showSlashMenu, wasClosed]);
5575
- const slashCommands = useMemo(() => {
5576
- const builtin = [
5837
+ const builtinCommands = useMemo(
5838
+ () => [
5577
5839
  { value: "/clear", label: "/clear", description: "Clear conversation" },
5578
5840
  { value: "/compress", label: "/compress", description: "Compress context" },
5579
5841
  { value: "/goal", label: "/goal", description: "Define an outcome goal" },
@@ -5584,195 +5846,307 @@ function ChatComposerInner({
5584
5846
  { value: "/loop", label: "/loop", description: "Schedule recurring prompt" },
5585
5847
  { value: "/loop list", label: "/loop list", description: "List active loops" },
5586
5848
  { value: "/loop cancel", label: "/loop cancel", description: "Cancel a loop by ID" }
5587
- ];
5588
- return [...adapterCommands, ...builtin];
5589
- }, [adapterCommands]);
5590
- const searchQuery = showSlashMenu ? textInput.value.slice(1).toLowerCase() : "";
5591
- const filteredCommands = useMemo(
5592
- () => slashCommands.filter(
5593
- (cmd) => cmd.value.slice(1).toLowerCase().includes(searchQuery) || cmd.description.toLowerCase().includes(searchQuery)
5594
- ),
5595
- [slashCommands, searchQuery]
5849
+ ],
5850
+ []
5596
5851
  );
5597
- const [selectedIndex, setSelectedIndex] = useState(0);
5598
- useEffect(() => {
5599
- setSelectedIndex(0);
5600
- }, [searchQuery]);
5601
- useEffect(() => {
5602
- setSelectedIndex(
5603
- (index) => Math.min(index, Math.max(filteredCommands.length - 1, 0))
5604
- );
5605
- }, [filteredCommands.length]);
5606
- const menuOpen = showSlashMenu && filteredCommands.length > 0;
5852
+ const searchQuery = showSlashMenu ? textInput.value.slice(1) : "";
5853
+ const menuOpen = showSlashMenu;
5854
+ const textareaRef = useRef(null);
5855
+ const focusTextareaAtEnd = useCallback(() => {
5856
+ requestAnimationFrame(() => {
5857
+ const textarea = textareaRef.current;
5858
+ if (!textarea) return;
5859
+ textarea.focus();
5860
+ const end = textarea.value.length;
5861
+ textarea.setSelectionRange(end, end);
5862
+ });
5863
+ }, []);
5607
5864
  const handleCommandSelect = useCallback(
5608
5865
  (commandValue) => {
5609
5866
  textInput.setInput(commandValue + " ");
5610
5867
  setButtonMenuOpen(false);
5611
5868
  setMenuDismissed(true);
5869
+ focusTextareaAtEnd();
5612
5870
  },
5613
- [textInput]
5871
+ [textInput, focusTextareaAtEnd]
5614
5872
  );
5615
- const handleKeyDown = useCallback(
5616
- (e) => {
5617
- if (!menuOpen) return;
5618
- if (e.key === "ArrowDown") {
5619
- e.preventDefault();
5620
- setSelectedIndex((i) => Math.min(i + 1, filteredCommands.length - 1));
5621
- } else if (e.key === "ArrowUp") {
5622
- e.preventDefault();
5623
- setSelectedIndex((i) => Math.max(i - 1, 0));
5624
- } else if (e.key === "Enter") {
5625
- e.preventDefault();
5626
- const command = filteredCommands[selectedIndex];
5627
- if (command) handleCommandSelect(command.value);
5628
- } else if (e.key === "Escape") {
5629
- e.preventDefault();
5630
- textInput.setInput("");
5631
- setButtonMenuOpen(false);
5632
- setMenuDismissed(true);
5633
- } else if (e.key === "Tab") {
5634
- e.preventDefault();
5635
- const command = filteredCommands[selectedIndex];
5636
- if (command) handleCommandSelect(command.value);
5637
- }
5873
+ const handleSearchChange = useCallback(
5874
+ (next) => {
5875
+ textInput.setInput("/" + next);
5638
5876
  },
5639
- [menuOpen, filteredCommands, selectedIndex, handleCommandSelect, textInput]
5877
+ [textInput]
5640
5878
  );
5641
5879
  const handleSubmit = useCallback(
5642
5880
  async (message) => {
5643
5881
  const text = message.text.trim();
5644
5882
  if (!text || disabled) return;
5645
5883
  const images = [];
5884
+ const pending = [];
5646
5885
  for (const file of message.files) {
5647
5886
  if (file.mediaType?.startsWith("image/") && file.url) {
5648
5887
  images.push({ data: file.url, mimeType: file.mediaType });
5888
+ continue;
5649
5889
  }
5890
+ if (!file.file) {
5891
+ continue;
5892
+ }
5893
+ pending.push({
5894
+ file: file.file,
5895
+ filename: file.filename ?? file.file.name,
5896
+ mimeType: file.mediaType ?? file.file.type ?? void 0,
5897
+ size: file.file.size
5898
+ });
5899
+ }
5900
+ if (images.length > MAX_IMAGES_PER_MESSAGE) {
5901
+ onComposerError?.({
5902
+ code: "max_images",
5903
+ message: `Maximum ${MAX_IMAGES_PER_MESSAGE} images per message.`
5904
+ });
5905
+ return;
5906
+ }
5907
+ if (pending.length > MAX_ATTACHMENTS_PER_MESSAGE) {
5908
+ onComposerError?.({
5909
+ code: "max_attachments",
5910
+ message: `Maximum ${MAX_ATTACHMENTS_PER_MESSAGE} attachments per message.`
5911
+ });
5912
+ return;
5913
+ }
5914
+ for (const img of images) {
5915
+ if (img.data.length > Math.ceil(MAX_IMAGE_BYTES * 4 / 3)) {
5916
+ onComposerError?.({
5917
+ code: "max_image_size",
5918
+ message: `Image exceeds ${MAX_IMAGE_BYTES / 1e6} MB.`
5919
+ });
5920
+ return;
5921
+ }
5922
+ }
5923
+ let totalAttachmentBytes = 0;
5924
+ for (const a of pending) {
5925
+ if (a.file.size > MAX_ATTACHMENT_BYTES) {
5926
+ onComposerError?.({
5927
+ code: "max_attachment_size",
5928
+ message: `"${a.filename}" exceeds ${MAX_ATTACHMENT_BYTES / 1e6} MB.`
5929
+ });
5930
+ return;
5931
+ }
5932
+ totalAttachmentBytes += a.file.size;
5933
+ }
5934
+ if (totalAttachmentBytes > MAX_ATTACHMENTS_TOTAL_BYTES) {
5935
+ onComposerError?.({
5936
+ code: "max_attachments_total",
5937
+ message: `Attachments exceed ${MAX_ATTACHMENTS_TOTAL_BYTES / 1e6} MB total for this message.`
5938
+ });
5939
+ return;
5650
5940
  }
5651
5941
  if (isRunning) {
5652
5942
  await onStop();
5653
5943
  }
5654
- await onSend(text, images.length > 0 ? images : void 0);
5944
+ await onSend(
5945
+ text,
5946
+ images.length > 0 ? images : void 0,
5947
+ pending.length > 0 ? pending : void 0
5948
+ );
5655
5949
  },
5656
- [onSend, onStop, isRunning, disabled]
5950
+ [onSend, onStop, onComposerError, isRunning, disabled]
5657
5951
  );
5658
- return /* @__PURE__ */ jsxs(Popover, { open: menuOpen, onOpenChange: (open) => {
5659
- if (!open) {
5660
- setButtonMenuOpen(false);
5661
- }
5662
- }, children: [
5663
- /* @__PURE__ */ jsx(AttachmentPreviewStrip, {}),
5664
- /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs(PromptInput, { onSubmit: handleSubmit, accept: "image/*", multiple: true, children: [
5665
- /* @__PURE__ */ jsx(PromptInputBody, { children: /* @__PURE__ */ jsx(
5666
- PromptInputTextarea,
5667
- {
5668
- placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5669
- disabled,
5670
- onKeyDown: handleKeyDown
5952
+ const handlePromptInputError = useCallback(
5953
+ (err) => {
5954
+ onComposerError?.({ code: err.code, message: err.message });
5955
+ },
5956
+ [onComposerError]
5957
+ );
5958
+ return /* @__PURE__ */ jsxs(
5959
+ Popover,
5960
+ {
5961
+ open: menuOpen,
5962
+ onOpenChange: (open) => {
5963
+ if (!open) {
5964
+ setButtonMenuOpen(false);
5965
+ setMenuDismissed(true);
5671
5966
  }
5672
- ) }),
5673
- /* @__PURE__ */ jsxs(PromptInputFooter, { children: [
5674
- /* @__PURE__ */ jsxs(PromptInputTools, { children: [
5675
- /* @__PURE__ */ jsxs(PromptInputActionMenu, { children: [
5676
- /* @__PURE__ */ jsx(PromptInputActionMenuTrigger, {}),
5677
- /* @__PURE__ */ jsx(PromptInputActionMenuContent, { children: /* @__PURE__ */ jsx(PromptInputActionAddAttachments, {}) })
5678
- ] }),
5679
- /* @__PURE__ */ jsx(
5680
- Button,
5681
- {
5682
- type: "button",
5683
- variant: "ghost",
5684
- size: "icon-xs",
5685
- className: "text-muted-foreground",
5686
- onClick: (e) => {
5687
- e.preventDefault();
5688
- setButtonMenuOpen((v) => !v);
5689
- },
5690
- "aria-label": "Slash commands",
5691
- children: /* @__PURE__ */ jsx("span", { className: "text-xs font-bold", children: "/" })
5692
- }
5693
- ),
5694
- tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsxs(
5695
- Context,
5696
- {
5697
- usedTokens: tokenUsage.totalTokens,
5698
- maxTokens: tokenUsage.contextWindow,
5699
- modelId: tokenUsage.model,
5700
- usage: {
5701
- inputTokens: tokenUsage.inputTokens,
5702
- outputTokens: tokenUsage.outputTokens,
5703
- reasoningTokens: tokenUsage.reasoningTokens,
5704
- cachedInputTokens: tokenUsage.cachedInputTokens,
5705
- totalTokens: tokenUsage.totalTokens,
5706
- inputTokenDetails: void 0,
5707
- outputTokenDetails: void 0
5708
- },
5709
- children: [
5710
- /* @__PURE__ */ jsx(ContextTrigger, {}),
5711
- /* @__PURE__ */ jsxs(ContextContent, { children: [
5712
- /* @__PURE__ */ jsx(ContextContentHeader, {}),
5713
- /* @__PURE__ */ jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
5714
- /* @__PURE__ */ jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
5715
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Input" }),
5716
- /* @__PURE__ */ jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
5717
- ] }) }),
5718
- /* @__PURE__ */ jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
5719
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Output" }),
5720
- /* @__PURE__ */ jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
5721
- ] }) }),
5722
- tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
5723
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
5724
- /* @__PURE__ */ jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
5725
- ] }) }),
5726
- tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
5727
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Cache" }),
5728
- /* @__PURE__ */ jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
5729
- ] }) })
5730
- ] }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
5731
- tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-3", children: /* @__PURE__ */ jsx(
5967
+ },
5968
+ children: [
5969
+ /* @__PURE__ */ jsx(AttachmentPreviewStrip, {}),
5970
+ /* @__PURE__ */ jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxs(
5971
+ PromptInput,
5972
+ {
5973
+ onSubmit: handleSubmit,
5974
+ multiple: true,
5975
+ maxFiles: MAX_IMAGES_PER_MESSAGE + MAX_ATTACHMENTS_PER_MESSAGE,
5976
+ maxFileSize: MAX_ATTACHMENT_BYTES,
5977
+ onError: handlePromptInputError,
5978
+ children: [
5979
+ /* @__PURE__ */ jsx(PromptInputBody, { children: /* @__PURE__ */ jsx(
5980
+ PromptInputTextarea,
5981
+ {
5982
+ ref: textareaRef,
5983
+ placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5984
+ disabled
5985
+ }
5986
+ ) }),
5987
+ /* @__PURE__ */ jsxs(PromptInputFooter, { children: [
5988
+ /* @__PURE__ */ jsxs(PromptInputTools, { children: [
5989
+ /* @__PURE__ */ jsxs(PromptInputActionMenu, { children: [
5990
+ /* @__PURE__ */ jsx(PromptInputActionMenuTrigger, {}),
5991
+ /* @__PURE__ */ jsx(PromptInputActionMenuContent, { children: /* @__PURE__ */ jsx(PromptInputActionAddAttachments, {}) })
5992
+ ] }),
5993
+ /* @__PURE__ */ jsx(
5732
5994
  Button,
5733
5995
  {
5734
5996
  type: "button",
5735
- size: "xs",
5736
- onClick: () => onSend("/compress"),
5737
- children: "Compress"
5997
+ variant: "ghost",
5998
+ size: "icon-xs",
5999
+ className: "text-muted-foreground",
6000
+ onClick: (e) => {
6001
+ e.preventDefault();
6002
+ setButtonMenuOpen((v) => !v);
6003
+ },
6004
+ "aria-label": "Slash commands",
6005
+ children: /* @__PURE__ */ jsx("span", { className: "text-xs font-bold", children: "/" })
5738
6006
  }
5739
- ) })
5740
- ] })
5741
- ]
5742
- }
5743
- )
5744
- ] }),
5745
- /* @__PURE__ */ jsx(PromptInputSubmit, { status, onStop })
5746
- ] })
5747
- ] }) }),
5748
- /* @__PURE__ */ jsx(
5749
- PopoverContent,
5750
- {
5751
- side: "top",
5752
- align: "start",
5753
- className: "p-0",
5754
- style: { width: "var(--radix-popover-trigger-width)" },
5755
- onOpenAutoFocus: (e) => e.preventDefault(),
5756
- onCloseAutoFocus: (e) => e.preventDefault(),
5757
- children: /* @__PURE__ */ jsx(Command, { value: filteredCommands[selectedIndex]?.value, filter: () => 1, children: /* @__PURE__ */ jsxs(CommandList, { children: [
5758
- /* @__PURE__ */ jsx(CommandGroup, { children: filteredCommands.map((cmd) => /* @__PURE__ */ jsxs(
5759
- CommandItem,
5760
- {
5761
- value: cmd.value,
5762
- onSelect: () => handleCommandSelect(cmd.value),
5763
- className: "grid grid-cols-[6rem_1fr] gap-2 [&_svg]:hidden",
5764
- children: [
5765
- /* @__PURE__ */ jsx("span", { className: "", children: cmd.label }),
5766
- /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: cmd.description })
5767
- ]
6007
+ ),
6008
+ tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsxs(
6009
+ Context,
6010
+ {
6011
+ usedTokens: tokenUsage.totalTokens,
6012
+ maxTokens: tokenUsage.contextWindow,
6013
+ modelId: tokenUsage.model,
6014
+ usage: {
6015
+ inputTokens: tokenUsage.inputTokens,
6016
+ outputTokens: tokenUsage.outputTokens,
6017
+ reasoningTokens: tokenUsage.reasoningTokens,
6018
+ cachedInputTokens: tokenUsage.cachedInputTokens,
6019
+ totalTokens: tokenUsage.totalTokens,
6020
+ inputTokenDetails: void 0,
6021
+ outputTokenDetails: void 0
6022
+ },
6023
+ children: [
6024
+ /* @__PURE__ */ jsx(ContextTrigger, {}),
6025
+ /* @__PURE__ */ jsxs(ContextContent, { children: [
6026
+ /* @__PURE__ */ jsx(ContextContentHeader, {}),
6027
+ /* @__PURE__ */ jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [
6028
+ /* @__PURE__ */ jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6029
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Input" }),
6030
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
6031
+ ] }) }),
6032
+ /* @__PURE__ */ jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6033
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Output" }),
6034
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
6035
+ ] }) }),
6036
+ tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6037
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
6038
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
6039
+ ] }) }),
6040
+ tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6041
+ /* @__PURE__ */ jsx("span", { className: "text-muted-foreground", children: "Cache" }),
6042
+ /* @__PURE__ */ jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
6043
+ ] }) })
6044
+ ] }) : /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
6045
+ tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-2", children: /* @__PURE__ */ jsx(
6046
+ Button,
6047
+ {
6048
+ type: "button",
6049
+ size: "xs",
6050
+ onClick: () => onSend("/compress"),
6051
+ children: "Compress"
6052
+ }
6053
+ ) })
6054
+ ] })
6055
+ ]
6056
+ }
6057
+ )
6058
+ ] }),
6059
+ /* @__PURE__ */ jsx(PromptInputSubmit, { status, onStop })
6060
+ ] })
6061
+ ]
6062
+ }
6063
+ ) }),
6064
+ /* @__PURE__ */ jsx(
6065
+ PopoverContent,
6066
+ {
6067
+ side: "top",
6068
+ align: "start",
6069
+ className: "overflow-hidden p-0",
6070
+ style: { width: "var(--radix-popover-trigger-width)" },
6071
+ onCloseAutoFocus: (e) => e.preventDefault(),
6072
+ onEscapeKeyDown: () => {
6073
+ focusTextareaAtEnd();
5768
6074
  },
5769
- cmd.value
5770
- )) }),
5771
- /* @__PURE__ */ jsx(CommandEmpty, { children: "No commands found" })
5772
- ] }) })
5773
- }
5774
- )
5775
- ] });
6075
+ children: /* @__PURE__ */ jsxs(Command, { children: [
6076
+ /* @__PURE__ */ jsx(
6077
+ CommandInput,
6078
+ {
6079
+ placeholder: "Type a command or search...",
6080
+ value: searchQuery,
6081
+ onValueChange: handleSearchChange
6082
+ }
6083
+ ),
6084
+ /* @__PURE__ */ jsxs(CommandList, { children: [
6085
+ /* @__PURE__ */ jsx(CommandEmpty, { children: "No commands found." }),
6086
+ builtinCommands.length > 0 && /* @__PURE__ */ jsx(CommandGroup, { heading: "Built-in commands", children: builtinCommands.map((cmd) => /* @__PURE__ */ jsxs(
6087
+ CommandItem,
6088
+ {
6089
+ value: cmd.value,
6090
+ keywords: [cmd.description],
6091
+ onSelect: () => handleCommandSelect(cmd.value),
6092
+ className: "grid grid-cols-[12rem_1fr] items-baseline gap-3 [&_svg]:hidden",
6093
+ children: [
6094
+ /* @__PURE__ */ jsx(
6095
+ "span",
6096
+ {
6097
+ className: "font-mono text-foreground truncate",
6098
+ title: cmd.label,
6099
+ children: cmd.label
6100
+ }
6101
+ ),
6102
+ /* @__PURE__ */ jsx(
6103
+ "span",
6104
+ {
6105
+ className: "min-w-0 truncate text-xs text-muted-foreground",
6106
+ title: cmd.description,
6107
+ children: cmd.description
6108
+ }
6109
+ )
6110
+ ]
6111
+ },
6112
+ cmd.value
6113
+ )) }),
6114
+ builtinCommands.length > 0 && adapterCommands.length > 0 && /* @__PURE__ */ jsx(CommandSeparator, {}),
6115
+ adapterCommands.length > 0 && /* @__PURE__ */ jsx(CommandGroup, { heading: "Skills", children: adapterCommands.map((cmd) => /* @__PURE__ */ jsxs(
6116
+ CommandItem,
6117
+ {
6118
+ value: cmd.value,
6119
+ keywords: [cmd.description],
6120
+ onSelect: () => handleCommandSelect(cmd.value),
6121
+ className: "grid grid-cols-[12rem_1fr] items-baseline gap-3 [&_svg]:hidden",
6122
+ children: [
6123
+ /* @__PURE__ */ jsx(
6124
+ "span",
6125
+ {
6126
+ className: "font-mono text-foreground truncate",
6127
+ title: cmd.label,
6128
+ children: cmd.label
6129
+ }
6130
+ ),
6131
+ /* @__PURE__ */ jsx(
6132
+ "span",
6133
+ {
6134
+ className: "min-w-0 truncate text-xs text-muted-foreground",
6135
+ title: cmd.description,
6136
+ children: cmd.description
6137
+ }
6138
+ )
6139
+ ]
6140
+ },
6141
+ cmd.value
6142
+ )) })
6143
+ ] })
6144
+ ] })
6145
+ }
6146
+ )
6147
+ ]
6148
+ }
6149
+ );
5776
6150
  }
5777
6151
  var Artifact = ({ className, ...props }) => /* @__PURE__ */ jsx(
5778
6152
  "div",
@@ -6748,7 +7122,8 @@ function ChatThread({
6748
7122
  disabledReason,
6749
7123
  tokenUsage,
6750
7124
  retryIndicator,
6751
- onRetry
7125
+ onRetry,
7126
+ onComposerError
6752
7127
  }) {
6753
7128
  const groups = useMemo(() => groupMessages(messages), [messages]);
6754
7129
  const artifactFallbacks = useMemo(() => {
@@ -6865,7 +7240,8 @@ function ChatThread({
6865
7240
  isRunning,
6866
7241
  disabled,
6867
7242
  disabledReason,
6868
- tokenUsage
7243
+ tokenUsage,
7244
+ onComposerError
6869
7245
  }
6870
7246
  )
6871
7247
  ] })
@@ -8174,12 +8550,24 @@ function applyRetryIndicator(state, event) {
8174
8550
  }
8175
8551
  function applyUserMessage(messages, eventId, data) {
8176
8552
  const content = stringValue2(data.content);
8553
+ const images = parseUserMessageImages(data.images);
8554
+ const attachments = parseUserMessageAttachments(data.attachments);
8177
8555
  const next = [...messages];
8178
8556
  const localIdx = next.findIndex(
8179
8557
  (m) => m.role === "user" && m.id.startsWith("local-") && m.content === content
8180
8558
  );
8181
8559
  if (localIdx >= 0) {
8182
- next[localIdx] = { ...next[localIdx], id: `evt-${eventId}` };
8560
+ const local = next[localIdx];
8561
+ next[localIdx] = {
8562
+ ...local,
8563
+ id: `evt-${eventId}`,
8564
+ // Server-confirmed lists replace any optimistic display-only
8565
+ // entries: chips that were rendered while the upload was in
8566
+ // flight become clickable because the persisted refs include a
8567
+ // workspace path.
8568
+ images: images ?? local.images,
8569
+ attachments: attachments ?? local.attachments
8570
+ };
8183
8571
  return next;
8184
8572
  }
8185
8573
  next.push({
@@ -8187,10 +8575,41 @@ function applyUserMessage(messages, eventId, data) {
8187
8575
  role: "user",
8188
8576
  content,
8189
8577
  createdAt: /* @__PURE__ */ new Date(),
8190
- status: "complete"
8578
+ status: "complete",
8579
+ images,
8580
+ attachments
8191
8581
  });
8192
8582
  return next;
8193
8583
  }
8584
+ function parseUserMessageImages(raw) {
8585
+ if (!Array.isArray(raw)) return void 0;
8586
+ const out = [];
8587
+ for (const item of raw) {
8588
+ if (!item || typeof item !== "object") continue;
8589
+ const r = item;
8590
+ const data = typeof r.data === "string" ? r.data : void 0;
8591
+ if (!data) continue;
8592
+ const mimeType = typeof r.mime_type === "string" ? r.mime_type : typeof r.mimeType === "string" ? r.mimeType : void 0;
8593
+ out.push({ data, mimeType });
8594
+ }
8595
+ return out.length > 0 ? out : void 0;
8596
+ }
8597
+ function parseUserMessageAttachments(raw) {
8598
+ if (!Array.isArray(raw)) return void 0;
8599
+ const out = [];
8600
+ for (const item of raw) {
8601
+ if (!item || typeof item !== "object") continue;
8602
+ const r = item;
8603
+ const filename = typeof r.filename === "string" ? r.filename : void 0;
8604
+ if (!filename) continue;
8605
+ const path = typeof r.path === "string" ? r.path : void 0;
8606
+ if (!path) continue;
8607
+ const mimeType = typeof r.mime_type === "string" ? r.mime_type : typeof r.mimeType === "string" ? r.mimeType : void 0;
8608
+ const size = typeof r.size === "number" ? r.size : void 0;
8609
+ out.push({ path, filename, mimeType, size });
8610
+ }
8611
+ return out.length > 0 ? out : void 0;
8612
+ }
8194
8613
  function applyBrowserEvent(state, event, browser) {
8195
8614
  return withMessages(
8196
8615
  { ...state, browser },
@@ -8762,7 +9181,7 @@ function useAgentChatRuntime({
8762
9181
  };
8763
9182
  }, [adapter, clearReconnectTimer, closeStream, openStream, sessionId]);
8764
9183
  const markSending = useCallback(
8765
- (content, images) => {
9184
+ (content, images, attachments) => {
8766
9185
  setState((prev) => ({
8767
9186
  ...prev,
8768
9187
  terminal: false,
@@ -8776,7 +9195,8 @@ function useAgentChatRuntime({
8776
9195
  content,
8777
9196
  createdAt: /* @__PURE__ */ new Date(),
8778
9197
  status: "complete",
8779
- images: images?.length ? images : void 0
9198
+ images: images?.length ? images : void 0,
9199
+ attachments: attachments?.length ? attachments : void 0
8780
9200
  }
8781
9201
  ]
8782
9202
  }));
@@ -8833,14 +9253,48 @@ function useAgentChatRuntime({
8833
9253
  });
8834
9254
  }, []);
8835
9255
  const send = useCallback(
8836
- async (content, images) => {
8837
- markSending(content, images);
8838
- const goal = images?.length ? null : parseGoalDefinition(content);
9256
+ async (content, images, pendingAttachments) => {
9257
+ const displayAttachments = pendingAttachments?.length ? pendingAttachments.map((a) => ({
9258
+ filename: a.filename,
9259
+ mimeType: a.mimeType,
9260
+ size: a.size ?? a.file.size
9261
+ // path absent until upload completes.
9262
+ })) : void 0;
9263
+ markSending(content, images, displayAttachments);
9264
+ const goal = images?.length || pendingAttachments?.length ? null : parseGoalDefinition(content);
9265
+ const runUploadsAndSend = async (sid) => {
9266
+ let refs;
9267
+ if (pendingAttachments?.length) {
9268
+ const stamp = Date.now();
9269
+ refs = await Promise.all(
9270
+ pendingAttachments.map(async (a, index) => {
9271
+ const safeName = sanitizeUploadFilename(a.file.name);
9272
+ const renamed = new File(
9273
+ [a.file],
9274
+ `${stamp}-${index}-${safeName}`,
9275
+ { type: a.file.type }
9276
+ );
9277
+ const up = await adapter.uploadWorkspaceFile({
9278
+ sessionId: sid,
9279
+ file: renamed,
9280
+ directory: "uploads"
9281
+ });
9282
+ return {
9283
+ path: up.path,
9284
+ filename: a.filename,
9285
+ mimeType: a.mimeType ?? a.file.type ?? void 0,
9286
+ size: up.size
9287
+ };
9288
+ })
9289
+ );
9290
+ }
9291
+ await sendTurn(adapter, sid, content, images, refs, goal);
9292
+ };
8839
9293
  if (!sessionId) {
8840
9294
  try {
8841
9295
  const session2 = await adapter.createSession({ agentId });
8842
9296
  onSessionChange?.(session2.id);
8843
- await sendTurn(adapter, session2.id, content, images, goal);
9297
+ await runUploadsAndSend(session2.id);
8844
9298
  } catch (error) {
8845
9299
  markSendError(error instanceof Error ? error.message : "send failed");
8846
9300
  throw error;
@@ -8849,7 +9303,7 @@ function useAgentChatRuntime({
8849
9303
  }
8850
9304
  try {
8851
9305
  ensureStreamForNewTurn(sessionId);
8852
- await sendTurn(adapter, sessionId, content, images, goal);
9306
+ await runUploadsAndSend(sessionId);
8853
9307
  } catch (error) {
8854
9308
  markSendError(error instanceof Error ? error.message : "send failed");
8855
9309
  throw error;
@@ -8924,7 +9378,7 @@ function findLastAssistantIndex2(messages) {
8924
9378
  }
8925
9379
  var GOAL_RUBRIC_RE = /\n\s*(?:rubric|criteria)\s*:\s*\n/i;
8926
9380
  var GOAL_CONTROL_COMMANDS = /* @__PURE__ */ new Set(["", "status", "pause", "resume", "clear"]);
8927
- async function sendTurn(adapter, sessionId, content, images, goal) {
9381
+ async function sendTurn(adapter, sessionId, content, images, attachments, goal) {
8928
9382
  if (goal && adapter.defineOutcome) {
8929
9383
  await adapter.defineOutcome({
8930
9384
  sessionId,
@@ -8933,7 +9387,11 @@ async function sendTurn(adapter, sessionId, content, images, goal) {
8933
9387
  });
8934
9388
  return;
8935
9389
  }
8936
- await adapter.sendMessage({ sessionId, content, images });
9390
+ await adapter.sendMessage({ sessionId, content, images, attachments });
9391
+ }
9392
+ function sanitizeUploadFilename(name) {
9393
+ const stripped = name.replace(/[\/\\\0]/g, "").trim();
9394
+ return stripped.length > 0 ? stripped : "attachment";
8937
9395
  }
8938
9396
  function parseGoalDefinition(content) {
8939
9397
  const text = content.trim();
@@ -8953,7 +9411,8 @@ function AgentChat({
8953
9411
  onSessionChange,
8954
9412
  onFileSelect,
8955
9413
  onMessagesChange,
8956
- disabled
9414
+ disabled,
9415
+ onComposerError
8957
9416
  }) {
8958
9417
  const [workspacePath, setWorkspacePath] = useState(null);
8959
9418
  const runtime = useAgentChatRuntime({
@@ -9005,14 +9464,15 @@ function AgentChat({
9005
9464
  messages: runtime.messages,
9006
9465
  isRunning: runtime.isRunning,
9007
9466
  isLoadingHistory: runtime.isLoadingHistory,
9008
- onSend: (content, images) => runtime.send(content, images),
9467
+ onSend: (content, images, attachments) => runtime.send(content, images, attachments),
9009
9468
  onStop: () => runtime.stop(),
9010
9469
  onRetry: runtime.retry,
9011
9470
  onFileSelect: handleFileSelect,
9012
9471
  disabled: effectiveDisabled,
9013
9472
  disabledReason,
9014
9473
  tokenUsage: runtime.tokenUsage,
9015
- retryIndicator: runtime.retryIndicator
9474
+ retryIndicator: runtime.retryIndicator,
9475
+ onComposerError
9016
9476
  }
9017
9477
  )
9018
9478
  }