@invergent/agent-chat-react 1.7.0 → 1.7.2

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.cjs CHANGED
@@ -4450,11 +4450,72 @@ var ChatMessage = react.memo(function ChatMessage2({
4450
4450
  className: "max-w-64 max-h-48 rounded-lg border border-border object-contain"
4451
4451
  },
4452
4452
  i
4453
+ )) }),
4454
+ message.attachments && message.attachments.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex flex-wrap gap-2 mt-2", children: message.attachments.map((a, i) => /* @__PURE__ */ jsxRuntime.jsx(
4455
+ AttachmentChip,
4456
+ {
4457
+ attachment: a,
4458
+ onSelect: onFileSelect
4459
+ },
4460
+ a.path ?? `pending-${i}`
4453
4461
  )) })
4454
4462
  ] }) });
4455
4463
  }
4456
4464
  return /* @__PURE__ */ jsxRuntime.jsx(AssistantMessage, { message, isLast, onFileSelect });
4457
4465
  });
4466
+ function iconForMime(mime) {
4467
+ if (!mime) return lucideReact.File;
4468
+ if (mime.startsWith("image/")) return lucideReact.FileImage;
4469
+ if (mime.startsWith("audio/")) return lucideReact.FileAudio;
4470
+ if (mime.startsWith("video/")) return lucideReact.FileVideo;
4471
+ if (mime === "application/pdf") return lucideReact.FileText;
4472
+ if (mime.startsWith("text/")) return lucideReact.FileText;
4473
+ if (mime === "application/json" || mime === "application/xml" || mime.endsWith("+xml") || mime.endsWith("+json") || mime.includes("javascript") || mime.includes("typescript")) {
4474
+ return lucideReact.FileCode;
4475
+ }
4476
+ if (mime === "application/zip" || mime === "application/x-7z-compressed" || mime === "application/x-tar" || mime === "application/gzip" || mime === "application/x-rar-compressed") {
4477
+ return lucideReact.FileArchive;
4478
+ }
4479
+ if (mime === "text/csv" || mime === "application/vnd.ms-excel" || mime.includes("spreadsheet")) {
4480
+ return lucideReact.FileSpreadsheet;
4481
+ }
4482
+ return lucideReact.File;
4483
+ }
4484
+ function formatBytes(n) {
4485
+ if (n == null || !Number.isFinite(n) || n < 0) return "";
4486
+ for (const [unit, divisor] of [
4487
+ ["GB", 1e9],
4488
+ ["MB", 1e6],
4489
+ ["KB", 1e3]
4490
+ ]) {
4491
+ if (n >= divisor) return `${(n / divisor).toFixed(1)} ${unit}`;
4492
+ }
4493
+ return `${n} B`;
4494
+ }
4495
+ function AttachmentChip({
4496
+ attachment,
4497
+ onSelect
4498
+ }) {
4499
+ const Icon = iconForMime(attachment.mimeType);
4500
+ const clickable = !!attachment.path && !!onSelect;
4501
+ const sizeLabel = formatBytes(attachment.size);
4502
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4503
+ "button",
4504
+ {
4505
+ type: "button",
4506
+ onClick: clickable ? () => onSelect(attachment.path) : void 0,
4507
+ disabled: !clickable,
4508
+ title: attachment.filename,
4509
+ "aria-label": clickable ? `Open ${attachment.filename}` : `${attachment.filename} (uploading)`,
4510
+ 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"),
4511
+ children: [
4512
+ /* @__PURE__ */ jsxRuntime.jsx(Icon, { className: "size-3.5 shrink-0" }),
4513
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: attachment.filename }),
4514
+ sizeLabel && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground shrink-0", children: sizeLabel })
4515
+ ]
4516
+ }
4517
+ );
4518
+ }
4458
4519
  function AssistantMessage({
4459
4520
  message,
4460
4521
  isLast,
@@ -4601,6 +4662,25 @@ function Command({
4601
4662
  }
4602
4663
  );
4603
4664
  }
4665
+ function CommandInput({
4666
+ className,
4667
+ ...props
4668
+ }) {
4669
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { "data-slot": "command-input-wrapper", className: "p-1", children: /* @__PURE__ */ jsxRuntime.jsxs(InputGroup, { className: "border-transparent border-b-input bg-transparent px-3", children: [
4670
+ /* @__PURE__ */ jsxRuntime.jsx(
4671
+ cmdk.Command.Input,
4672
+ {
4673
+ "data-slot": "command-input",
4674
+ className: cn(
4675
+ "w-full px-2 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
4676
+ className
4677
+ ),
4678
+ ...props
4679
+ }
4680
+ ),
4681
+ /* @__PURE__ */ jsxRuntime.jsx(InputGroupAddon, { children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.SearchIcon, { className: "size-3.5 shrink-0 opacity-50" }) })
4682
+ ] }) });
4683
+ }
4604
4684
  function CommandList({
4605
4685
  className,
4606
4686
  ...props
@@ -4646,6 +4726,19 @@ function CommandGroup({
4646
4726
  }
4647
4727
  );
4648
4728
  }
4729
+ function CommandSeparator({
4730
+ className,
4731
+ ...props
4732
+ }) {
4733
+ return /* @__PURE__ */ jsxRuntime.jsx(
4734
+ cmdk.Command.Separator,
4735
+ {
4736
+ "data-slot": "command-separator",
4737
+ className: cn("-mx-1.5 my-1.5 h-px bg-border/50", className),
4738
+ ...props
4739
+ }
4740
+ );
4741
+ }
4649
4742
  function CommandItem({
4650
4743
  className,
4651
4744
  children,
@@ -4827,7 +4920,8 @@ var PromptInputProvider = ({
4827
4920
  id: nanoid.nanoid(),
4828
4921
  mediaType: file.type,
4829
4922
  type: "file",
4830
- url: URL.createObjectURL(file)
4923
+ url: URL.createObjectURL(file),
4924
+ file
4831
4925
  }))
4832
4926
  ]);
4833
4927
  }, []);
@@ -5009,7 +5103,8 @@ var PromptInput = ({
5009
5103
  id: nanoid.nanoid(),
5010
5104
  mediaType: file.type,
5011
5105
  type: "file",
5012
- url: URL.createObjectURL(file)
5106
+ url: URL.createObjectURL(file),
5107
+ file
5013
5108
  });
5014
5109
  }
5015
5110
  return [...prev, ...next];
@@ -5209,15 +5304,17 @@ var PromptInput = ({
5209
5304
  }
5210
5305
  try {
5211
5306
  const convertedFiles = await Promise.all(
5212
- files.map(async ({ id: _id, ...item }) => {
5213
- if (item.url?.startsWith("blob:")) {
5214
- const dataUrl = await convertBlobUrlToDataUrl(item.url);
5307
+ files.map(async (entry) => {
5308
+ const isBlob = entry.url?.startsWith("blob:");
5309
+ const isImage = entry.mediaType?.startsWith("image/");
5310
+ if (isBlob && isImage && entry.url) {
5311
+ const dataUrl = await convertBlobUrlToDataUrl(entry.url);
5215
5312
  return {
5216
- ...item,
5217
- url: dataUrl ?? item.url
5313
+ ...entry,
5314
+ url: dataUrl ?? entry.url
5218
5315
  };
5219
5316
  }
5220
- return item;
5317
+ return entry;
5221
5318
  })
5222
5319
  );
5223
5320
  const result = onSubmit({ files: convertedFiles, text }, event);
@@ -5526,7 +5623,7 @@ var Context = ({
5526
5623
  () => ({ maxTokens, modelId, usage, usedTokens }),
5527
5624
  [maxTokens, modelId, usage, usedTokens]
5528
5625
  );
5529
- return /* @__PURE__ */ jsxRuntime.jsx(ContextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(HoverCard, { closeDelay: 0, openDelay: 0, ...props }) });
5626
+ return /* @__PURE__ */ jsxRuntime.jsx(ContextContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxRuntime.jsx(HoverCard, { closeDelay: 100, openDelay: 0, ...props }) });
5530
5627
  };
5531
5628
  var ContextIcon = () => {
5532
5629
  const { usedTokens, maxTokens } = useContextValue();
@@ -5767,6 +5864,128 @@ var ContextCacheUsage = ({
5767
5864
  );
5768
5865
  };
5769
5866
 
5867
+ // src/components/ui/item.tsx
5868
+ init_utils();
5869
+ var itemVariants = classVarianceAuthority.cva(
5870
+ "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",
5871
+ {
5872
+ variants: {
5873
+ variant: {
5874
+ default: "border-transparent",
5875
+ outline: "border-border",
5876
+ muted: "border-transparent bg-muted/50"
5877
+ },
5878
+ size: {
5879
+ default: "gap-3.5 px-4 py-3.5",
5880
+ sm: "gap-3.5 px-3.5 py-3",
5881
+ xs: "gap-2.5 px-3 py-2.5 in-data-[slot=dropdown-menu-content]:p-0"
5882
+ }
5883
+ },
5884
+ defaultVariants: {
5885
+ variant: "default",
5886
+ size: "default"
5887
+ }
5888
+ }
5889
+ );
5890
+ function Item({
5891
+ className,
5892
+ variant = "default",
5893
+ size = "default",
5894
+ asChild = false,
5895
+ ...props
5896
+ }) {
5897
+ const Comp = asChild ? radixUi.Slot.Root : "div";
5898
+ return /* @__PURE__ */ jsxRuntime.jsx(
5899
+ Comp,
5900
+ {
5901
+ "data-slot": "item",
5902
+ "data-variant": variant,
5903
+ "data-size": size,
5904
+ className: cn(itemVariants({ variant, size, className })),
5905
+ ...props
5906
+ }
5907
+ );
5908
+ }
5909
+ var itemMediaVariants = classVarianceAuthority.cva(
5910
+ "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",
5911
+ {
5912
+ variants: {
5913
+ variant: {
5914
+ default: "bg-transparent",
5915
+ icon: "[&_svg:not([class*='size-'])]:size-4",
5916
+ 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"
5917
+ }
5918
+ },
5919
+ defaultVariants: {
5920
+ variant: "default"
5921
+ }
5922
+ }
5923
+ );
5924
+ function ItemMedia({
5925
+ className,
5926
+ variant = "default",
5927
+ ...props
5928
+ }) {
5929
+ return /* @__PURE__ */ jsxRuntime.jsx(
5930
+ "div",
5931
+ {
5932
+ "data-slot": "item-media",
5933
+ "data-variant": variant,
5934
+ className: cn(itemMediaVariants({ variant, className })),
5935
+ ...props
5936
+ }
5937
+ );
5938
+ }
5939
+ function ItemContent({ className, ...props }) {
5940
+ return /* @__PURE__ */ jsxRuntime.jsx(
5941
+ "div",
5942
+ {
5943
+ "data-slot": "item-content",
5944
+ className: cn(
5945
+ "flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0.5 [&+[data-slot=item-content]]:flex-none",
5946
+ className
5947
+ ),
5948
+ ...props
5949
+ }
5950
+ );
5951
+ }
5952
+ function ItemTitle({ className, ...props }) {
5953
+ return /* @__PURE__ */ jsxRuntime.jsx(
5954
+ "div",
5955
+ {
5956
+ "data-slot": "item-title",
5957
+ className: cn(
5958
+ "line-clamp-1 flex w-fit items-center gap-2 text-xs leading-snug font-semibold uppercase underline-offset-4",
5959
+ className
5960
+ ),
5961
+ ...props
5962
+ }
5963
+ );
5964
+ }
5965
+ function ItemDescription({ className, ...props }) {
5966
+ return /* @__PURE__ */ jsxRuntime.jsx(
5967
+ "p",
5968
+ {
5969
+ "data-slot": "item-description",
5970
+ className: cn(
5971
+ "line-clamp-2 text-left text-sm leading-relaxed font-normal text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
5972
+ className
5973
+ ),
5974
+ ...props
5975
+ }
5976
+ );
5977
+ }
5978
+ function ItemActions({ className, ...props }) {
5979
+ return /* @__PURE__ */ jsxRuntime.jsx(
5980
+ "div",
5981
+ {
5982
+ "data-slot": "item-actions",
5983
+ className: cn("flex items-center gap-2", className),
5984
+ ...props
5985
+ }
5986
+ );
5987
+ }
5988
+
5770
5989
  // src/components/ui/popover.tsx
5771
5990
  init_utils();
5772
5991
  function Popover({
@@ -5802,36 +6021,82 @@ function PopoverAnchor({
5802
6021
  function ChatComposer(props) {
5803
6022
  return /* @__PURE__ */ jsxRuntime.jsx(PromptInputProvider, { children: /* @__PURE__ */ jsxRuntime.jsx(ChatComposerInner, { ...props }) });
5804
6023
  }
6024
+ function iconForMime2(mime) {
6025
+ if (!mime) return lucideReact.File;
6026
+ if (mime.startsWith("audio/")) return lucideReact.FileAudio;
6027
+ if (mime.startsWith("video/")) return lucideReact.FileVideo;
6028
+ if (mime === "application/pdf" || mime.startsWith("text/")) return lucideReact.FileText;
6029
+ if (mime === "application/json" || mime === "application/xml" || mime.endsWith("+xml") || mime.endsWith("+json") || mime.includes("javascript") || mime.includes("typescript")) {
6030
+ return lucideReact.FileCode;
6031
+ }
6032
+ if (mime === "application/zip" || mime === "application/x-7z-compressed" || mime === "application/x-tar" || mime === "application/gzip" || mime === "application/x-rar-compressed") {
6033
+ return lucideReact.FileArchive;
6034
+ }
6035
+ if (mime === "text/csv" || mime === "application/vnd.ms-excel" || mime.includes("spreadsheet")) {
6036
+ return lucideReact.FileSpreadsheet;
6037
+ }
6038
+ return lucideReact.File;
6039
+ }
6040
+ function formatBytes2(n) {
6041
+ if (n == null || !Number.isFinite(n) || n < 0) return "";
6042
+ for (const [unit, divisor] of [
6043
+ ["GB", 1e9],
6044
+ ["MB", 1e6],
6045
+ ["KB", 1e3]
6046
+ ]) {
6047
+ if (n >= divisor) return `${(n / divisor).toFixed(1)} ${unit}`;
6048
+ }
6049
+ return `${n} B`;
6050
+ }
5805
6051
  function AttachmentPreviewStrip() {
5806
6052
  const attachments = useProviderAttachments();
5807
6053
  if (attachments.files.length === 0) return null;
5808
- return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 px-3 pt-2 pb-1 flex-wrap", children: attachments.files.map((file) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "relative group", children: [
5809
- file.mediaType?.startsWith("image/") && file.url ? /* @__PURE__ */ jsxRuntime.jsx(
5810
- "img",
5811
- {
5812
- src: file.url,
5813
- alt: file.filename,
5814
- className: "h-16 w-16 rounded-lg border border-border object-cover"
5815
- }
5816
- ) : /* @__PURE__ */ jsxRuntime.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 }),
5817
- /* @__PURE__ */ jsxRuntime.jsx(
5818
- "button",
6054
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex gap-2 px-3 pt-2 pb-1 flex-wrap", children: attachments.files.map((file) => {
6055
+ const isImage = file.mediaType?.startsWith("image/") && !!file.url;
6056
+ const sizeLabel = formatBytes2(file.file?.size);
6057
+ const Icon = iconForMime2(file.mediaType);
6058
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6059
+ Item,
5819
6060
  {
5820
- type: "button",
5821
- onClick: () => attachments.remove(file.id),
5822
- 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]",
5823
- children: "\xD7"
5824
- }
5825
- )
5826
- ] }, file.id)) });
6061
+ variant: "outline",
6062
+ size: "xs",
6063
+ className: "group w-auto max-w-[18rem] rounded-md",
6064
+ title: file.filename,
6065
+ children: [
6066
+ /* @__PURE__ */ jsxRuntime.jsx(ItemMedia, { variant: isImage ? "image" : "icon", children: isImage ? /* @__PURE__ */ jsxRuntime.jsx("img", { src: file.url, alt: file.filename }) : /* @__PURE__ */ jsxRuntime.jsx(Icon, {}) }),
6067
+ /* @__PURE__ */ jsxRuntime.jsxs(ItemContent, { className: "min-w-0", children: [
6068
+ /* @__PURE__ */ jsxRuntime.jsx(ItemTitle, { className: "block w-full truncate normal-case font-medium text-foreground", children: file.filename }),
6069
+ sizeLabel && /* @__PURE__ */ jsxRuntime.jsx(ItemDescription, { className: "text-xs", children: sizeLabel })
6070
+ ] }),
6071
+ /* @__PURE__ */ jsxRuntime.jsx(ItemActions, { children: /* @__PURE__ */ jsxRuntime.jsx(
6072
+ "button",
6073
+ {
6074
+ type: "button",
6075
+ onClick: () => attachments.remove(file.id),
6076
+ "aria-label": `Remove ${file.filename}`,
6077
+ className: "hidden group-hover:flex items-center justify-center w-4 h-4 rounded-full bg-destructive text-destructive-foreground text-[10px]",
6078
+ children: "\xD7"
6079
+ }
6080
+ ) })
6081
+ ]
6082
+ },
6083
+ file.id
6084
+ );
6085
+ }) });
5827
6086
  }
6087
+ var MAX_IMAGES_PER_MESSAGE = 5;
6088
+ var MAX_IMAGE_BYTES = 2e7;
6089
+ var MAX_ATTACHMENTS_PER_MESSAGE = 10;
6090
+ var MAX_ATTACHMENT_BYTES = 5e7;
6091
+ var MAX_ATTACHMENTS_TOTAL_BYTES = 2e8;
5828
6092
  function ChatComposerInner({
5829
6093
  onSend,
5830
6094
  onStop,
5831
6095
  isRunning,
5832
6096
  disabled = false,
5833
6097
  disabledReason,
5834
- tokenUsage
6098
+ tokenUsage,
6099
+ onComposerError
5835
6100
  }) {
5836
6101
  const { adapter } = useAgentChatAdapterContext();
5837
6102
  const { textInput } = usePromptInputController();
@@ -5856,8 +6121,8 @@ function ChatComposerInner({
5856
6121
  setWasClosed(true);
5857
6122
  }
5858
6123
  }, [adapter, showSlashMenu, wasClosed]);
5859
- const slashCommands = react.useMemo(() => {
5860
- const builtin = [
6124
+ const builtinCommands = react.useMemo(
6125
+ () => [
5861
6126
  { value: "/clear", label: "/clear", description: "Clear conversation" },
5862
6127
  { value: "/compress", label: "/compress", description: "Compress context" },
5863
6128
  { value: "/goal", label: "/goal", description: "Define an outcome goal" },
@@ -5868,195 +6133,307 @@ function ChatComposerInner({
5868
6133
  { value: "/loop", label: "/loop", description: "Schedule recurring prompt" },
5869
6134
  { value: "/loop list", label: "/loop list", description: "List active loops" },
5870
6135
  { value: "/loop cancel", label: "/loop cancel", description: "Cancel a loop by ID" }
5871
- ];
5872
- return [...adapterCommands, ...builtin];
5873
- }, [adapterCommands]);
5874
- const searchQuery = showSlashMenu ? textInput.value.slice(1).toLowerCase() : "";
5875
- const filteredCommands = react.useMemo(
5876
- () => slashCommands.filter(
5877
- (cmd) => cmd.value.slice(1).toLowerCase().includes(searchQuery) || cmd.description.toLowerCase().includes(searchQuery)
5878
- ),
5879
- [slashCommands, searchQuery]
6136
+ ],
6137
+ []
5880
6138
  );
5881
- const [selectedIndex, setSelectedIndex] = react.useState(0);
5882
- react.useEffect(() => {
5883
- setSelectedIndex(0);
5884
- }, [searchQuery]);
5885
- react.useEffect(() => {
5886
- setSelectedIndex(
5887
- (index) => Math.min(index, Math.max(filteredCommands.length - 1, 0))
5888
- );
5889
- }, [filteredCommands.length]);
5890
- const menuOpen = showSlashMenu && filteredCommands.length > 0;
6139
+ const searchQuery = showSlashMenu ? textInput.value.slice(1) : "";
6140
+ const menuOpen = showSlashMenu;
6141
+ const textareaRef = react.useRef(null);
6142
+ const focusTextareaAtEnd = react.useCallback(() => {
6143
+ requestAnimationFrame(() => {
6144
+ const textarea = textareaRef.current;
6145
+ if (!textarea) return;
6146
+ textarea.focus();
6147
+ const end = textarea.value.length;
6148
+ textarea.setSelectionRange(end, end);
6149
+ });
6150
+ }, []);
5891
6151
  const handleCommandSelect = react.useCallback(
5892
6152
  (commandValue) => {
5893
6153
  textInput.setInput(commandValue + " ");
5894
6154
  setButtonMenuOpen(false);
5895
6155
  setMenuDismissed(true);
6156
+ focusTextareaAtEnd();
5896
6157
  },
5897
- [textInput]
6158
+ [textInput, focusTextareaAtEnd]
5898
6159
  );
5899
- const handleKeyDown = react.useCallback(
5900
- (e) => {
5901
- if (!menuOpen) return;
5902
- if (e.key === "ArrowDown") {
5903
- e.preventDefault();
5904
- setSelectedIndex((i) => Math.min(i + 1, filteredCommands.length - 1));
5905
- } else if (e.key === "ArrowUp") {
5906
- e.preventDefault();
5907
- setSelectedIndex((i) => Math.max(i - 1, 0));
5908
- } else if (e.key === "Enter") {
5909
- e.preventDefault();
5910
- const command = filteredCommands[selectedIndex];
5911
- if (command) handleCommandSelect(command.value);
5912
- } else if (e.key === "Escape") {
5913
- e.preventDefault();
5914
- textInput.setInput("");
5915
- setButtonMenuOpen(false);
5916
- setMenuDismissed(true);
5917
- } else if (e.key === "Tab") {
5918
- e.preventDefault();
5919
- const command = filteredCommands[selectedIndex];
5920
- if (command) handleCommandSelect(command.value);
5921
- }
6160
+ const handleSearchChange = react.useCallback(
6161
+ (next) => {
6162
+ textInput.setInput("/" + next);
5922
6163
  },
5923
- [menuOpen, filteredCommands, selectedIndex, handleCommandSelect, textInput]
6164
+ [textInput]
5924
6165
  );
5925
6166
  const handleSubmit = react.useCallback(
5926
6167
  async (message) => {
5927
6168
  const text = message.text.trim();
5928
6169
  if (!text || disabled) return;
5929
6170
  const images = [];
6171
+ const pending = [];
5930
6172
  for (const file of message.files) {
5931
6173
  if (file.mediaType?.startsWith("image/") && file.url) {
5932
6174
  images.push({ data: file.url, mimeType: file.mediaType });
6175
+ continue;
6176
+ }
6177
+ if (!file.file) {
6178
+ continue;
5933
6179
  }
6180
+ pending.push({
6181
+ file: file.file,
6182
+ filename: file.filename ?? file.file.name,
6183
+ mimeType: file.mediaType ?? file.file.type ?? void 0,
6184
+ size: file.file.size
6185
+ });
6186
+ }
6187
+ if (images.length > MAX_IMAGES_PER_MESSAGE) {
6188
+ onComposerError?.({
6189
+ code: "max_images",
6190
+ message: `Maximum ${MAX_IMAGES_PER_MESSAGE} images per message.`
6191
+ });
6192
+ return;
6193
+ }
6194
+ if (pending.length > MAX_ATTACHMENTS_PER_MESSAGE) {
6195
+ onComposerError?.({
6196
+ code: "max_attachments",
6197
+ message: `Maximum ${MAX_ATTACHMENTS_PER_MESSAGE} attachments per message.`
6198
+ });
6199
+ return;
6200
+ }
6201
+ for (const img of images) {
6202
+ if (img.data.length > Math.ceil(MAX_IMAGE_BYTES * 4 / 3)) {
6203
+ onComposerError?.({
6204
+ code: "max_image_size",
6205
+ message: `Image exceeds ${MAX_IMAGE_BYTES / 1e6} MB.`
6206
+ });
6207
+ return;
6208
+ }
6209
+ }
6210
+ let totalAttachmentBytes = 0;
6211
+ for (const a of pending) {
6212
+ if (a.file.size > MAX_ATTACHMENT_BYTES) {
6213
+ onComposerError?.({
6214
+ code: "max_attachment_size",
6215
+ message: `"${a.filename}" exceeds ${MAX_ATTACHMENT_BYTES / 1e6} MB.`
6216
+ });
6217
+ return;
6218
+ }
6219
+ totalAttachmentBytes += a.file.size;
6220
+ }
6221
+ if (totalAttachmentBytes > MAX_ATTACHMENTS_TOTAL_BYTES) {
6222
+ onComposerError?.({
6223
+ code: "max_attachments_total",
6224
+ message: `Attachments exceed ${MAX_ATTACHMENTS_TOTAL_BYTES / 1e6} MB total for this message.`
6225
+ });
6226
+ return;
5934
6227
  }
5935
6228
  if (isRunning) {
5936
6229
  await onStop();
5937
6230
  }
5938
- await onSend(text, images.length > 0 ? images : void 0);
6231
+ await onSend(
6232
+ text,
6233
+ images.length > 0 ? images : void 0,
6234
+ pending.length > 0 ? pending : void 0
6235
+ );
5939
6236
  },
5940
- [onSend, onStop, isRunning, disabled]
6237
+ [onSend, onStop, onComposerError, isRunning, disabled]
5941
6238
  );
5942
- return /* @__PURE__ */ jsxRuntime.jsxs(Popover, { open: menuOpen, onOpenChange: (open) => {
5943
- if (!open) {
5944
- setButtonMenuOpen(false);
5945
- }
5946
- }, children: [
5947
- /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreviewStrip, {}),
5948
- /* @__PURE__ */ jsxRuntime.jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(PromptInput, { onSubmit: handleSubmit, accept: "image/*", multiple: true, children: [
5949
- /* @__PURE__ */ jsxRuntime.jsx(PromptInputBody, { children: /* @__PURE__ */ jsxRuntime.jsx(
5950
- PromptInputTextarea,
5951
- {
5952
- placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
5953
- disabled,
5954
- onKeyDown: handleKeyDown
6239
+ const handlePromptInputError = react.useCallback(
6240
+ (err) => {
6241
+ onComposerError?.({ code: err.code, message: err.message });
6242
+ },
6243
+ [onComposerError]
6244
+ );
6245
+ return /* @__PURE__ */ jsxRuntime.jsxs(
6246
+ Popover,
6247
+ {
6248
+ open: menuOpen,
6249
+ onOpenChange: (open) => {
6250
+ if (!open) {
6251
+ setButtonMenuOpen(false);
6252
+ setMenuDismissed(true);
5955
6253
  }
5956
- ) }),
5957
- /* @__PURE__ */ jsxRuntime.jsxs(PromptInputFooter, { children: [
5958
- /* @__PURE__ */ jsxRuntime.jsxs(PromptInputTools, { children: [
5959
- /* @__PURE__ */ jsxRuntime.jsxs(PromptInputActionMenu, { children: [
5960
- /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionMenuTrigger, {}),
5961
- /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionMenuContent, { children: /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionAddAttachments, {}) })
5962
- ] }),
5963
- /* @__PURE__ */ jsxRuntime.jsx(
5964
- Button,
5965
- {
5966
- type: "button",
5967
- variant: "ghost",
5968
- size: "icon-xs",
5969
- className: "text-muted-foreground",
5970
- onClick: (e) => {
5971
- e.preventDefault();
5972
- setButtonMenuOpen((v) => !v);
5973
- },
5974
- "aria-label": "Slash commands",
5975
- children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-bold", children: "/" })
5976
- }
5977
- ),
5978
- tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
5979
- Context,
5980
- {
5981
- usedTokens: tokenUsage.totalTokens,
5982
- maxTokens: tokenUsage.contextWindow,
5983
- modelId: tokenUsage.model,
5984
- usage: {
5985
- inputTokens: tokenUsage.inputTokens,
5986
- outputTokens: tokenUsage.outputTokens,
5987
- reasoningTokens: tokenUsage.reasoningTokens,
5988
- cachedInputTokens: tokenUsage.cachedInputTokens,
5989
- totalTokens: tokenUsage.totalTokens,
5990
- inputTokenDetails: void 0,
5991
- outputTokenDetails: void 0
5992
- },
5993
- children: [
5994
- /* @__PURE__ */ jsxRuntime.jsx(ContextTrigger, {}),
5995
- /* @__PURE__ */ jsxRuntime.jsxs(ContextContent, { children: [
5996
- /* @__PURE__ */ jsxRuntime.jsx(ContextContentHeader, {}),
5997
- /* @__PURE__ */ jsxRuntime.jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
5998
- /* @__PURE__ */ jsxRuntime.jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
5999
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Input" }),
6000
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
6001
- ] }) }),
6002
- /* @__PURE__ */ jsxRuntime.jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6003
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Output" }),
6004
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
6005
- ] }) }),
6006
- tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6007
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
6008
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
6009
- ] }) }),
6010
- tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6011
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Cache" }),
6012
- /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
6013
- ] }) })
6014
- ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
6015
- tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-3", children: /* @__PURE__ */ jsxRuntime.jsx(
6254
+ },
6255
+ children: [
6256
+ /* @__PURE__ */ jsxRuntime.jsx(AttachmentPreviewStrip, {}),
6257
+ /* @__PURE__ */ jsxRuntime.jsx(PopoverAnchor, { asChild: true, children: /* @__PURE__ */ jsxRuntime.jsxs(
6258
+ PromptInput,
6259
+ {
6260
+ onSubmit: handleSubmit,
6261
+ multiple: true,
6262
+ maxFiles: MAX_IMAGES_PER_MESSAGE + MAX_ATTACHMENTS_PER_MESSAGE,
6263
+ maxFileSize: MAX_ATTACHMENT_BYTES,
6264
+ onError: handlePromptInputError,
6265
+ children: [
6266
+ /* @__PURE__ */ jsxRuntime.jsx(PromptInputBody, { children: /* @__PURE__ */ jsxRuntime.jsx(
6267
+ PromptInputTextarea,
6268
+ {
6269
+ ref: textareaRef,
6270
+ placeholder: disabled ? disabledReason ?? "Session disabled" : "Send a message...",
6271
+ disabled
6272
+ }
6273
+ ) }),
6274
+ /* @__PURE__ */ jsxRuntime.jsxs(PromptInputFooter, { children: [
6275
+ /* @__PURE__ */ jsxRuntime.jsxs(PromptInputTools, { children: [
6276
+ /* @__PURE__ */ jsxRuntime.jsxs(PromptInputActionMenu, { children: [
6277
+ /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionMenuTrigger, {}),
6278
+ /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionMenuContent, { children: /* @__PURE__ */ jsxRuntime.jsx(PromptInputActionAddAttachments, {}) })
6279
+ ] }),
6280
+ /* @__PURE__ */ jsxRuntime.jsx(
6016
6281
  Button,
6017
6282
  {
6018
6283
  type: "button",
6019
- size: "xs",
6020
- onClick: () => onSend("/compress"),
6021
- children: "Compress"
6284
+ variant: "ghost",
6285
+ size: "icon-xs",
6286
+ className: "text-muted-foreground",
6287
+ onClick: (e) => {
6288
+ e.preventDefault();
6289
+ setButtonMenuOpen((v) => !v);
6290
+ },
6291
+ "aria-label": "Slash commands",
6292
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs font-bold", children: "/" })
6022
6293
  }
6023
- ) })
6024
- ] })
6025
- ]
6026
- }
6027
- )
6028
- ] }),
6029
- /* @__PURE__ */ jsxRuntime.jsx(PromptInputSubmit, { status, onStop })
6030
- ] })
6031
- ] }) }),
6032
- /* @__PURE__ */ jsxRuntime.jsx(
6033
- PopoverContent,
6034
- {
6035
- side: "top",
6036
- align: "start",
6037
- className: "p-0",
6038
- style: { width: "var(--radix-popover-trigger-width)" },
6039
- onOpenAutoFocus: (e) => e.preventDefault(),
6040
- onCloseAutoFocus: (e) => e.preventDefault(),
6041
- children: /* @__PURE__ */ jsxRuntime.jsx(Command, { value: filteredCommands[selectedIndex]?.value, filter: () => 1, children: /* @__PURE__ */ jsxRuntime.jsxs(CommandList, { children: [
6042
- /* @__PURE__ */ jsxRuntime.jsx(CommandGroup, { children: filteredCommands.map((cmd) => /* @__PURE__ */ jsxRuntime.jsxs(
6043
- CommandItem,
6044
- {
6045
- value: cmd.value,
6046
- onSelect: () => handleCommandSelect(cmd.value),
6047
- className: "grid grid-cols-[6rem_1fr] gap-2 [&_svg]:hidden",
6048
- children: [
6049
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "", children: cmd.label }),
6050
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: cmd.description })
6051
- ]
6294
+ ),
6295
+ tokenUsage && tokenUsage.contextWindow > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
6296
+ Context,
6297
+ {
6298
+ usedTokens: tokenUsage.totalTokens,
6299
+ maxTokens: tokenUsage.contextWindow,
6300
+ modelId: tokenUsage.model,
6301
+ usage: {
6302
+ inputTokens: tokenUsage.inputTokens,
6303
+ outputTokens: tokenUsage.outputTokens,
6304
+ reasoningTokens: tokenUsage.reasoningTokens,
6305
+ cachedInputTokens: tokenUsage.cachedInputTokens,
6306
+ totalTokens: tokenUsage.totalTokens,
6307
+ inputTokenDetails: void 0,
6308
+ outputTokenDetails: void 0
6309
+ },
6310
+ children: [
6311
+ /* @__PURE__ */ jsxRuntime.jsx(ContextTrigger, {}),
6312
+ /* @__PURE__ */ jsxRuntime.jsxs(ContextContent, { children: [
6313
+ /* @__PURE__ */ jsxRuntime.jsx(ContextContentHeader, {}),
6314
+ /* @__PURE__ */ jsxRuntime.jsx(ContextContentBody, { children: tokenUsage.totalTokens > 0 ? /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6315
+ /* @__PURE__ */ jsxRuntime.jsx(ContextInputUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6316
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Input" }),
6317
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.inputTokens.toLocaleString() })
6318
+ ] }) }),
6319
+ /* @__PURE__ */ jsxRuntime.jsx(ContextOutputUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6320
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Output" }),
6321
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.outputTokens.toLocaleString() })
6322
+ ] }) }),
6323
+ tokenUsage.reasoningTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx(ContextReasoningUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6324
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Reasoning" }),
6325
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.reasoningTokens.toLocaleString() })
6326
+ ] }) }),
6327
+ tokenUsage.cachedInputTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx(ContextCacheUsage, { children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center justify-between text-xs", children: [
6328
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-muted-foreground", children: "Cache" }),
6329
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: tokenUsage.cachedInputTokens.toLocaleString() })
6330
+ ] }) })
6331
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("p", { className: "text-xs text-muted-foreground text-center py-1", children: "Empty" }) }),
6332
+ tokenUsage.totalTokens > 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex w-full items-center justify-end gap-3 bg-secondary p-2", children: /* @__PURE__ */ jsxRuntime.jsx(
6333
+ Button,
6334
+ {
6335
+ type: "button",
6336
+ size: "xs",
6337
+ onClick: () => onSend("/compress"),
6338
+ children: "Compress"
6339
+ }
6340
+ ) })
6341
+ ] })
6342
+ ]
6343
+ }
6344
+ )
6345
+ ] }),
6346
+ /* @__PURE__ */ jsxRuntime.jsx(PromptInputSubmit, { status, onStop })
6347
+ ] })
6348
+ ]
6349
+ }
6350
+ ) }),
6351
+ /* @__PURE__ */ jsxRuntime.jsx(
6352
+ PopoverContent,
6353
+ {
6354
+ side: "top",
6355
+ align: "start",
6356
+ className: "overflow-hidden p-0",
6357
+ style: { width: "var(--radix-popover-trigger-width)" },
6358
+ onCloseAutoFocus: (e) => e.preventDefault(),
6359
+ onEscapeKeyDown: () => {
6360
+ focusTextareaAtEnd();
6052
6361
  },
6053
- cmd.value
6054
- )) }),
6055
- /* @__PURE__ */ jsxRuntime.jsx(CommandEmpty, { children: "No commands found" })
6056
- ] }) })
6057
- }
6058
- )
6059
- ] });
6362
+ children: /* @__PURE__ */ jsxRuntime.jsxs(Command, { children: [
6363
+ /* @__PURE__ */ jsxRuntime.jsx(
6364
+ CommandInput,
6365
+ {
6366
+ placeholder: "Type a command or search...",
6367
+ value: searchQuery,
6368
+ onValueChange: handleSearchChange
6369
+ }
6370
+ ),
6371
+ /* @__PURE__ */ jsxRuntime.jsxs(CommandList, { children: [
6372
+ /* @__PURE__ */ jsxRuntime.jsx(CommandEmpty, { children: "No commands found." }),
6373
+ builtinCommands.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(CommandGroup, { heading: "Built-in commands", children: builtinCommands.map((cmd) => /* @__PURE__ */ jsxRuntime.jsxs(
6374
+ CommandItem,
6375
+ {
6376
+ value: cmd.value,
6377
+ keywords: [cmd.description],
6378
+ onSelect: () => handleCommandSelect(cmd.value),
6379
+ className: "grid grid-cols-[12rem_1fr] items-baseline gap-3 [&_svg]:hidden",
6380
+ children: [
6381
+ /* @__PURE__ */ jsxRuntime.jsx(
6382
+ "span",
6383
+ {
6384
+ className: "font-mono text-foreground truncate",
6385
+ title: cmd.label,
6386
+ children: cmd.label
6387
+ }
6388
+ ),
6389
+ /* @__PURE__ */ jsxRuntime.jsx(
6390
+ "span",
6391
+ {
6392
+ className: "min-w-0 truncate text-xs text-muted-foreground",
6393
+ title: cmd.description,
6394
+ children: cmd.description
6395
+ }
6396
+ )
6397
+ ]
6398
+ },
6399
+ cmd.value
6400
+ )) }),
6401
+ builtinCommands.length > 0 && adapterCommands.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(CommandSeparator, {}),
6402
+ adapterCommands.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(CommandGroup, { heading: "Skills", children: adapterCommands.map((cmd) => /* @__PURE__ */ jsxRuntime.jsxs(
6403
+ CommandItem,
6404
+ {
6405
+ value: cmd.value,
6406
+ keywords: [cmd.description],
6407
+ onSelect: () => handleCommandSelect(cmd.value),
6408
+ className: "grid grid-cols-[12rem_1fr] items-baseline gap-3 [&_svg]:hidden",
6409
+ children: [
6410
+ /* @__PURE__ */ jsxRuntime.jsx(
6411
+ "span",
6412
+ {
6413
+ className: "font-mono text-foreground truncate",
6414
+ title: cmd.label,
6415
+ children: cmd.label
6416
+ }
6417
+ ),
6418
+ /* @__PURE__ */ jsxRuntime.jsx(
6419
+ "span",
6420
+ {
6421
+ className: "min-w-0 truncate text-xs text-muted-foreground",
6422
+ title: cmd.description,
6423
+ children: cmd.description
6424
+ }
6425
+ )
6426
+ ]
6427
+ },
6428
+ cmd.value
6429
+ )) })
6430
+ ] })
6431
+ ] })
6432
+ }
6433
+ )
6434
+ ]
6435
+ }
6436
+ );
6060
6437
  }
6061
6438
 
6062
6439
  // src/components/ai-elements/artifact.tsx
@@ -7042,7 +7419,8 @@ function ChatThread({
7042
7419
  disabledReason,
7043
7420
  tokenUsage,
7044
7421
  retryIndicator,
7045
- onRetry
7422
+ onRetry,
7423
+ onComposerError
7046
7424
  }) {
7047
7425
  const groups = react.useMemo(() => groupMessages(messages), [messages]);
7048
7426
  const artifactFallbacks = react.useMemo(() => {
@@ -7159,7 +7537,8 @@ function ChatThread({
7159
7537
  isRunning,
7160
7538
  disabled,
7161
7539
  disabledReason,
7162
- tokenUsage
7540
+ tokenUsage,
7541
+ onComposerError
7163
7542
  }
7164
7543
  )
7165
7544
  ] })
@@ -8477,12 +8856,24 @@ function applyRetryIndicator(state, event) {
8477
8856
  }
8478
8857
  function applyUserMessage(messages, eventId, data) {
8479
8858
  const content = stringValue2(data.content);
8859
+ const images = parseUserMessageImages(data.images);
8860
+ const attachments = parseUserMessageAttachments(data.attachments);
8480
8861
  const next = [...messages];
8481
8862
  const localIdx = next.findIndex(
8482
8863
  (m) => m.role === "user" && m.id.startsWith("local-") && m.content === content
8483
8864
  );
8484
8865
  if (localIdx >= 0) {
8485
- next[localIdx] = { ...next[localIdx], id: `evt-${eventId}` };
8866
+ const local = next[localIdx];
8867
+ next[localIdx] = {
8868
+ ...local,
8869
+ id: `evt-${eventId}`,
8870
+ // Server-confirmed lists replace any optimistic display-only
8871
+ // entries: chips that were rendered while the upload was in
8872
+ // flight become clickable because the persisted refs include a
8873
+ // workspace path.
8874
+ images: images ?? local.images,
8875
+ attachments: attachments ?? local.attachments
8876
+ };
8486
8877
  return next;
8487
8878
  }
8488
8879
  next.push({
@@ -8490,10 +8881,41 @@ function applyUserMessage(messages, eventId, data) {
8490
8881
  role: "user",
8491
8882
  content,
8492
8883
  createdAt: /* @__PURE__ */ new Date(),
8493
- status: "complete"
8884
+ status: "complete",
8885
+ images,
8886
+ attachments
8494
8887
  });
8495
8888
  return next;
8496
8889
  }
8890
+ function parseUserMessageImages(raw) {
8891
+ if (!Array.isArray(raw)) return void 0;
8892
+ const out = [];
8893
+ for (const item of raw) {
8894
+ if (!item || typeof item !== "object") continue;
8895
+ const r = item;
8896
+ const data = typeof r.data === "string" ? r.data : void 0;
8897
+ if (!data) continue;
8898
+ const mimeType = typeof r.mime_type === "string" ? r.mime_type : typeof r.mimeType === "string" ? r.mimeType : void 0;
8899
+ out.push({ data, mimeType });
8900
+ }
8901
+ return out.length > 0 ? out : void 0;
8902
+ }
8903
+ function parseUserMessageAttachments(raw) {
8904
+ if (!Array.isArray(raw)) return void 0;
8905
+ const out = [];
8906
+ for (const item of raw) {
8907
+ if (!item || typeof item !== "object") continue;
8908
+ const r = item;
8909
+ const filename = typeof r.filename === "string" ? r.filename : void 0;
8910
+ if (!filename) continue;
8911
+ const path = typeof r.path === "string" ? r.path : void 0;
8912
+ if (!path) continue;
8913
+ const mimeType = typeof r.mime_type === "string" ? r.mime_type : typeof r.mimeType === "string" ? r.mimeType : void 0;
8914
+ const size = typeof r.size === "number" ? r.size : void 0;
8915
+ out.push({ path, filename, mimeType, size });
8916
+ }
8917
+ return out.length > 0 ? out : void 0;
8918
+ }
8497
8919
  function applyBrowserEvent(state, event, browser) {
8498
8920
  return withMessages(
8499
8921
  { ...state, browser },
@@ -9065,7 +9487,7 @@ function useAgentChatRuntime({
9065
9487
  };
9066
9488
  }, [adapter, clearReconnectTimer, closeStream, openStream, sessionId]);
9067
9489
  const markSending = react.useCallback(
9068
- (content, images) => {
9490
+ (content, images, attachments) => {
9069
9491
  setState((prev) => ({
9070
9492
  ...prev,
9071
9493
  terminal: false,
@@ -9079,7 +9501,8 @@ function useAgentChatRuntime({
9079
9501
  content,
9080
9502
  createdAt: /* @__PURE__ */ new Date(),
9081
9503
  status: "complete",
9082
- images: images?.length ? images : void 0
9504
+ images: images?.length ? images : void 0,
9505
+ attachments: attachments?.length ? attachments : void 0
9083
9506
  }
9084
9507
  ]
9085
9508
  }));
@@ -9136,14 +9559,48 @@ function useAgentChatRuntime({
9136
9559
  });
9137
9560
  }, []);
9138
9561
  const send = react.useCallback(
9139
- async (content, images) => {
9140
- markSending(content, images);
9141
- const goal = images?.length ? null : parseGoalDefinition(content);
9562
+ async (content, images, pendingAttachments) => {
9563
+ const displayAttachments = pendingAttachments?.length ? pendingAttachments.map((a) => ({
9564
+ filename: a.filename,
9565
+ mimeType: a.mimeType,
9566
+ size: a.size ?? a.file.size
9567
+ // path absent until upload completes.
9568
+ })) : void 0;
9569
+ markSending(content, images, displayAttachments);
9570
+ const goal = images?.length || pendingAttachments?.length ? null : parseGoalDefinition(content);
9571
+ const runUploadsAndSend = async (sid) => {
9572
+ let refs;
9573
+ if (pendingAttachments?.length) {
9574
+ const stamp = Date.now();
9575
+ refs = await Promise.all(
9576
+ pendingAttachments.map(async (a, index) => {
9577
+ const safeName = sanitizeUploadFilename(a.file.name);
9578
+ const renamed = new File(
9579
+ [a.file],
9580
+ `${stamp}-${index}-${safeName}`,
9581
+ { type: a.file.type }
9582
+ );
9583
+ const up = await adapter.uploadWorkspaceFile({
9584
+ sessionId: sid,
9585
+ file: renamed,
9586
+ directory: "uploads"
9587
+ });
9588
+ return {
9589
+ path: up.path,
9590
+ filename: a.filename,
9591
+ mimeType: a.mimeType ?? a.file.type ?? void 0,
9592
+ size: up.size
9593
+ };
9594
+ })
9595
+ );
9596
+ }
9597
+ await sendTurn(adapter, sid, content, images, refs, goal);
9598
+ };
9142
9599
  if (!sessionId) {
9143
9600
  try {
9144
9601
  const session2 = await adapter.createSession({ agentId });
9145
9602
  onSessionChange?.(session2.id);
9146
- await sendTurn(adapter, session2.id, content, images, goal);
9603
+ await runUploadsAndSend(session2.id);
9147
9604
  } catch (error) {
9148
9605
  markSendError(error instanceof Error ? error.message : "send failed");
9149
9606
  throw error;
@@ -9152,7 +9609,7 @@ function useAgentChatRuntime({
9152
9609
  }
9153
9610
  try {
9154
9611
  ensureStreamForNewTurn(sessionId);
9155
- await sendTurn(adapter, sessionId, content, images, goal);
9612
+ await runUploadsAndSend(sessionId);
9156
9613
  } catch (error) {
9157
9614
  markSendError(error instanceof Error ? error.message : "send failed");
9158
9615
  throw error;
@@ -9227,7 +9684,7 @@ function findLastAssistantIndex2(messages) {
9227
9684
  }
9228
9685
  var GOAL_RUBRIC_RE = /\n\s*(?:rubric|criteria)\s*:\s*\n/i;
9229
9686
  var GOAL_CONTROL_COMMANDS = /* @__PURE__ */ new Set(["", "status", "pause", "resume", "clear"]);
9230
- async function sendTurn(adapter, sessionId, content, images, goal) {
9687
+ async function sendTurn(adapter, sessionId, content, images, attachments, goal) {
9231
9688
  if (goal && adapter.defineOutcome) {
9232
9689
  await adapter.defineOutcome({
9233
9690
  sessionId,
@@ -9236,7 +9693,11 @@ async function sendTurn(adapter, sessionId, content, images, goal) {
9236
9693
  });
9237
9694
  return;
9238
9695
  }
9239
- await adapter.sendMessage({ sessionId, content, images });
9696
+ await adapter.sendMessage({ sessionId, content, images, attachments });
9697
+ }
9698
+ function sanitizeUploadFilename(name) {
9699
+ const stripped = name.replace(/[\/\\\0]/g, "").trim();
9700
+ return stripped.length > 0 ? stripped : "attachment";
9240
9701
  }
9241
9702
  function parseGoalDefinition(content) {
9242
9703
  const text = content.trim();
@@ -9256,7 +9717,8 @@ function AgentChat({
9256
9717
  onSessionChange,
9257
9718
  onFileSelect,
9258
9719
  onMessagesChange,
9259
- disabled
9720
+ disabled,
9721
+ onComposerError
9260
9722
  }) {
9261
9723
  const [workspacePath, setWorkspacePath] = react.useState(null);
9262
9724
  const runtime = useAgentChatRuntime({
@@ -9308,14 +9770,15 @@ function AgentChat({
9308
9770
  messages: runtime.messages,
9309
9771
  isRunning: runtime.isRunning,
9310
9772
  isLoadingHistory: runtime.isLoadingHistory,
9311
- onSend: (content, images) => runtime.send(content, images),
9773
+ onSend: (content, images, attachments) => runtime.send(content, images, attachments),
9312
9774
  onStop: () => runtime.stop(),
9313
9775
  onRetry: runtime.retry,
9314
9776
  onFileSelect: handleFileSelect,
9315
9777
  disabled: effectiveDisabled,
9316
9778
  disabledReason,
9317
9779
  tokenUsage: runtime.tokenUsage,
9318
- retryIndicator: runtime.retryIndicator
9780
+ retryIndicator: runtime.retryIndicator,
9781
+ onComposerError
9319
9782
  }
9320
9783
  )
9321
9784
  }