@volter-ai-dev/supercode-ui 0.1.20 → 0.1.22

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/messenger.mjs CHANGED
@@ -501,6 +501,16 @@ function readTranscript(value) {
501
501
  }] : [];
502
502
  });
503
503
  }
504
+ if (Array.isArray(item.images)) {
505
+ entry.images = item.images.flatMap((raw) => {
506
+ const image = record(raw);
507
+ return image && typeof image.label === "string" ? [{
508
+ ...typeof image.id === "string" ? { id: image.id } : {},
509
+ label: image.label,
510
+ ...typeof image.url === "string" ? { url: image.url } : {}
511
+ }] : [];
512
+ }).slice(0, 4);
513
+ }
504
514
  const request = record(item.request);
505
515
  if (request && typeof request.requestKind === "string" && typeof request.payloadText === "string") {
506
516
  entry.request = {
@@ -777,6 +787,11 @@ var ICONS = {
777
787
  /* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
778
788
  /* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
779
789
  ] }),
790
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
791
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
792
+ /* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
793
+ /* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
794
+ ] }),
780
795
  menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
781
796
  /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
782
797
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
@@ -805,6 +820,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
805
820
  // src/context.jsx
806
821
  import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
807
822
  var MAX_CONTEXT_ITEMS = 32;
823
+ var MAX_IMAGE_ITEMS = 4;
824
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
825
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
808
826
  function normalizeContext(value) {
809
827
  return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
810
828
  if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
@@ -831,6 +849,58 @@ function mergeContext(current, picked) {
831
849
  }
832
850
  return next;
833
851
  }
852
+ function normalizeImages(value) {
853
+ const seen = /* @__PURE__ */ new Set();
854
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
855
+ if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
856
+ const label = item.label.trim().slice(0, 200);
857
+ const url = item.url;
858
+ if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
859
+ seen.add(url);
860
+ return [{
861
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
862
+ label,
863
+ url
864
+ }];
865
+ }).slice(0, MAX_IMAGE_ITEMS);
866
+ }
867
+ function mergeImages(current, picked) {
868
+ const next = [...current];
869
+ const seen = new Set(current.map((item) => item.url));
870
+ for (const item of normalizeImages(picked)) {
871
+ if (seen.has(item.url)) continue;
872
+ seen.add(item.url);
873
+ next.push(item);
874
+ if (next.length === MAX_IMAGE_ITEMS) break;
875
+ }
876
+ return next;
877
+ }
878
+ function partitionAttachments(value) {
879
+ const values = Array.isArray(value) ? value : value ? [value] : [];
880
+ const context = [];
881
+ const images = [];
882
+ for (const item of values) {
883
+ if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
884
+ else if (item && typeof item.label === "string" && item.label.trim() && typeof item.url === "string" && (item.url.startsWith("data:image/") || item.url.startsWith("https://") || item.url.startsWith("http://"))) images.push(item);
885
+ else throw new Error("The attachment picker returned an invalid item.");
886
+ }
887
+ return { context, images };
888
+ }
889
+ async function imageAttachmentsFromFiles(value) {
890
+ const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
891
+ if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
892
+ return Promise.all(files.map(async (file) => {
893
+ if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
894
+ if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
895
+ const url = await new Promise((resolve, reject) => {
896
+ const reader = new FileReader();
897
+ reader.onload = () => resolve(reader.result);
898
+ reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
899
+ reader.readAsDataURL(file);
900
+ });
901
+ return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
902
+ }));
903
+ }
834
904
  function ContextTray({ items, onRemove }) {
835
905
  if (!items.length) return null;
836
906
  return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
@@ -839,6 +909,21 @@ function ContextTray({ items, onRemove }) {
839
909
  /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
840
910
  ] }, item.id ?? `${item.label}:${index}`)) });
841
911
  }
912
+ function ImageTray({ items, onRemove }) {
913
+ if (!items.length) return null;
914
+ return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
915
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
916
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
917
+ onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
918
+ ] }, item.id ?? `${item.label}:${index}`)) });
919
+ }
920
+ function MessageImages({ items }) {
921
+ if (!items?.length) return null;
922
+ return /* @__PURE__ */ jsx2("div", { class: "scui-message-images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx2("img", { src: item.url, alt: item.label }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { children: [
923
+ /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
924
+ item.label
925
+ ] }, item.id ?? `${item.label}:${index}`)) });
926
+ }
842
927
 
843
928
  // src/textarea.js
844
929
  import { useLayoutEffect } from "preact/hooks";
@@ -878,19 +963,24 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
878
963
  ] });
879
964
  }
880
965
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
881
- const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
966
+ const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
882
967
  const [draft, setDraft] = useState(remembered.draft);
883
- const [context, setContext] = useState(remembered.context);
884
- const [queue, setQueue] = useState(remembered.queue);
968
+ const [context, setContext] = useState(remembered.context ?? []);
969
+ const [images, setImages] = useState(remembered.images ?? []);
970
+ const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
885
971
  const [dispatching, setDispatching] = useState(false);
886
972
  const [picking, setPicking] = useState(false);
973
+ const [dragging, setDragging] = useState(false);
887
974
  const [pickerError, setPickerError] = useState(null);
888
975
  const textarea = useRef(null);
889
976
  useAutosizeTextarea(textarea, draft);
890
- const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
977
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
978
+ useEffect(() => {
979
+ remember(draft, context, images, queue);
980
+ }, [draft, context, images, memoryKey, queue]);
891
981
  const updateQueue = (update) => setQueue((items) => {
892
982
  const next = update(items);
893
- remember(draft, context, next);
983
+ remember(draft, context, images, next);
894
984
  return next;
895
985
  });
896
986
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -900,9 +990,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
900
990
  const [next, ...rest] = queue;
901
991
  setDispatching(true);
902
992
  setQueue(rest);
903
- remember(draft, context, rest);
904
- onPending?.(next.text, next.context);
905
- adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
993
+ remember(draft, context, images, rest);
994
+ onPending?.(next.text, next.context, next.images);
995
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
906
996
  }
907
997
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
908
998
  useEffect(() => {
@@ -915,8 +1005,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
915
1005
  if (!restoreDraft) return;
916
1006
  setDraft(restoreDraft.text);
917
1007
  const restoredContext = normalizeContext(restoreDraft.context);
1008
+ const restoredImages = normalizeImages(restoreDraft.images);
918
1009
  setContext(restoredContext);
919
- remember(restoreDraft.text, restoredContext, queue);
1010
+ setImages(restoredImages);
1011
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
920
1012
  textarea.current?.focus({ preventScroll: true });
921
1013
  onDraftRestored?.(restoreDraft.id);
922
1014
  }, [onDraftRestored, restoreDraft?.id]);
@@ -925,30 +1017,67 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
925
1017
  return () => clearTimeout(timer);
926
1018
  }, [adapter, draft]);
927
1019
  const pickContext = () => {
928
- if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
1020
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
929
1021
  setPicking(true);
930
1022
  setPickerError(null);
931
1023
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
1024
+ const attachments = partitionAttachments(picked);
1025
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1026
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
932
1027
  setContext((current) => {
933
- const next = mergeContext(current, picked);
934
- remember(draft, next, queue);
1028
+ const next = mergeContext(current, attachments.context);
1029
+ remember(draft, next, images, queue);
935
1030
  return next;
936
1031
  });
937
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1032
+ setImages((current) => {
1033
+ const next = mergeImages(current, attachments.images);
1034
+ remember(draft, context, next, queue);
1035
+ return next;
1036
+ });
1037
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1038
+ };
1039
+ const addImageFiles = (value, source) => {
1040
+ const allFiles = Array.from(value ?? []);
1041
+ if (!allFiles.length) return false;
1042
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
1043
+ if (files.length !== allFiles.length) {
1044
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
1045
+ return true;
1046
+ }
1047
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
1048
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1049
+ return true;
1050
+ }
1051
+ setPicking(true);
1052
+ setPickerError(null);
1053
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
1054
+ const next = mergeImages(current, picked);
1055
+ remember(draft, context, next, queue);
1056
+ return next;
1057
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
1058
+ return true;
1059
+ };
1060
+ const pasteImages = (event) => {
1061
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
1062
+ };
1063
+ const dropImages = (event) => {
1064
+ setDragging(false);
1065
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
938
1066
  };
939
1067
  const send = () => {
940
1068
  const text = draft.trim();
941
- if (!text) return;
942
- const message = { text, context };
1069
+ if (!text && !images.length) return;
1070
+ const message = { text, context, images };
943
1071
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
944
1072
  else if (state.canSend) {
945
1073
  if (onPending) setDispatching(true);
946
- onPending?.(text, context);
947
- adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
1074
+ onPending?.(text, context, images);
1075
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
948
1076
  } else return;
949
1077
  setDraft("");
950
1078
  setContext([]);
951
- remember("", [], queuesNewMessage ? [...queue, message] : queue);
1079
+ setImages([]);
1080
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
952
1081
  };
953
1082
  return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
954
1083
  queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
@@ -958,27 +1087,41 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
958
1087
  ] }),
959
1088
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
960
1089
  /* @__PURE__ */ jsxs3("span", { children: [
961
- item.text,
962
- item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
963
- item.context.length,
1090
+ item.text || "Image attachment",
1091
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1092
+ item.context.length + item.images.length,
964
1093
  " attached"
965
1094
  ] }) : null
966
1095
  ] }),
967
1096
  /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ jsx3(UiIcon, { name: "close", size: 13 }) })
968
1097
  ] }, `${index}:${item.text}`))
969
1098
  ] }) : null,
1099
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
1100
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
1101
+ remember(draft, context, next, queue);
1102
+ return next;
1103
+ }) }),
970
1104
  /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
971
1105
  const next = items.filter((_, itemIndex) => itemIndex !== index);
972
- remember(draft, next, queue);
1106
+ remember(draft, next, images, queue);
973
1107
  return next;
974
1108
  }) }),
975
1109
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
976
- /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
977
- adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach context", disabled: picking || context.length >= MAX_CONTEXT_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
978
- /* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
1110
+ /* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
1111
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
1112
+ event.preventDefault();
1113
+ setDragging(true);
1114
+ }
1115
+ }, onDragOver: (event) => {
1116
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
1117
+ }, onDragLeave: (event) => {
1118
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1119
+ }, onDrop: dropImages, children: [
1120
+ adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS, onClick: pickContext, children: picking ? /* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
1121
+ /* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
979
1122
  const value = event.currentTarget.value;
980
1123
  setDraft(value);
981
- remember(value, context, queue);
1124
+ remember(value, context, images, queue);
982
1125
  }, onKeyDown: (event) => {
983
1126
  if (isSendKey(event)) {
984
1127
  event.preventDefault();
@@ -987,7 +1130,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
987
1130
  } }),
988
1131
  /* @__PURE__ */ jsxs3("span", { children: [
989
1132
  state.busy ? /* @__PURE__ */ jsx3("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
990
- /* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
1133
+ /* @__PURE__ */ jsx3("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx3(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
991
1134
  ] })
992
1135
  ] })
993
1136
  ] });
@@ -1380,6 +1523,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1380
1523
  }
1381
1524
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1382
1525
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1526
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1383
1527
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1384
1528
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1385
1529
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1582,6 +1726,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1582
1726
  block.kind === "activity" ? /* @__PURE__ */ jsx5(Group, { value: block.entries, entries: block.entries, state, adapter }) : /* @__PURE__ */ jsx5(Entry, { value: block.entry, entry: block.entry, state, adapter })
1583
1727
  ] }, block.id)),
1584
1728
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1729
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1585
1730
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1586
1731
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1587
1732
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1920,9 +2065,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1920
2065
  if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
1921
2066
  else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
1922
2067
  }, [pending, state.busy, state.error, state.operation, state.transcript]);
1923
- const beginPending = (text, context = []) => setPending({
2068
+ const beginPending = (text, context = [], images = []) => setPending({
1924
2069
  text,
1925
2070
  context,
2071
+ images,
1926
2072
  status: "sending",
1927
2073
  initialError: state.error,
1928
2074
  seenBusy: state.busy,
@@ -1930,13 +2076,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1930
2076
  });
1931
2077
  const retryPending = () => {
1932
2078
  if (!pending) return;
1933
- adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
2079
+ adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
1934
2080
  setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
1935
2081
  };
1936
2082
  const editPending = () => {
1937
2083
  if (!pending) return;
1938
2084
  restoreSequence.current += 1;
1939
- setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
2085
+ setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
1940
2086
  setPending({ ...pending, status: "editing" });
1941
2087
  };
1942
2088
  useEffect6(() => {
@@ -1982,7 +2128,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1982
2128
  setPendingAction(null);
1983
2129
  }
1984
2130
  }, [pendingAction, state.error, state.operation]);
1985
- const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
2131
+ const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
1986
2132
  const action = state.operation || pendingAction?.action || null;
1987
2133
  const actionLabel = operationLabel(action);
1988
2134
  const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
@@ -2006,12 +2152,14 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2006
2152
  function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2007
2153
  const startable = state.harnesses.filter((item) => item.startable);
2008
2154
  const startableKey = startable.map((item) => item.id).join("\0");
2009
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
2155
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2010
2156
  const [harness, setHarness] = useState4(remembered.harness);
2011
2157
  const [draft, setDraft] = useState4(remembered.draft);
2012
2158
  const [context, setContext] = useState4(remembered.context);
2159
+ const [images, setImages] = useState4(remembered.images ?? []);
2013
2160
  const [starting, setStarting] = useState4(null);
2014
2161
  const [picking, setPicking] = useState4(false);
2162
+ const [dragging, setDragging] = useState4(false);
2015
2163
  const [pickerError, setPickerError] = useState4(null);
2016
2164
  const startSequence = useRef5(0);
2017
2165
  const textarea = useRef5(null);
@@ -2020,7 +2168,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2020
2168
  if (startable.some((item) => item.id === harness)) return;
2021
2169
  const next = startable[0]?.id ?? "";
2022
2170
  setHarness(next);
2023
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
2171
+ boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2024
2172
  }, [harness, startableKey]);
2025
2173
  useEffect6(() => {
2026
2174
  if (!starting) return;
@@ -2037,24 +2185,60 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2037
2185
  textarea.current?.focus({ preventScroll: true });
2038
2186
  }, []);
2039
2187
  const pickContext = () => {
2040
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
2188
+ if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2041
2189
  setPicking(true);
2042
2190
  setPickerError(null);
2043
2191
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
2192
+ const attachments = partitionAttachments(picked);
2193
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2194
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2044
2195
  setContext((current) => {
2045
- const next = mergeContext(current, picked);
2046
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2196
+ const next = mergeContext(current, attachments.context);
2197
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2198
+ return next;
2199
+ });
2200
+ setImages((current) => {
2201
+ const next = mergeImages(current, attachments.images);
2202
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2047
2203
  return next;
2048
2204
  });
2049
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2205
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2206
+ };
2207
+ const addImageFiles = (value, source) => {
2208
+ const allFiles = Array.from(value ?? []);
2209
+ if (!allFiles.length) return false;
2210
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
2211
+ if (files.length !== allFiles.length) {
2212
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
2213
+ return true;
2214
+ }
2215
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
2216
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2217
+ return true;
2218
+ }
2219
+ setPicking(true);
2220
+ setPickerError(null);
2221
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2222
+ const next = mergeImages(current, picked);
2223
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2224
+ return next;
2225
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2226
+ return true;
2227
+ };
2228
+ const pasteImages = (event) => {
2229
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
2230
+ };
2231
+ const dropImages = (event) => {
2232
+ setDragging(false);
2233
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
2050
2234
  };
2051
2235
  const send = () => {
2052
2236
  const text = draft.trim();
2053
- if (!text || !harness || starting) return;
2237
+ if (!text && !images.length || !harness || starting) return;
2054
2238
  const id = startSequence.current + 1;
2055
2239
  startSequence.current = id;
2056
2240
  setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2057
- const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {} });
2241
+ const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2058
2242
  if (result && typeof result.then === "function") {
2059
2243
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2060
2244
  }
@@ -2084,31 +2268,45 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2084
2268
  /* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2085
2269
  const value = event.currentTarget.value;
2086
2270
  setHarness(value);
2087
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
2271
+ boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2088
2272
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
2089
2273
  item.label,
2090
2274
  item.startable ? "" : " \xB7 unavailable"
2091
2275
  ] }, item.id)) })
2092
2276
  ] }),
2277
+ /* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2278
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
2279
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2280
+ return next;
2281
+ }) }),
2093
2282
  /* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2094
2283
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2095
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2284
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2096
2285
  return next;
2097
2286
  }) }),
2098
2287
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2099
- /* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
2100
- adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach context", disabled: picking || context.length >= MAX_CONTEXT_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
2101
- /* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onInput: (event) => {
2288
+ /* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2289
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2290
+ event.preventDefault();
2291
+ setDragging(true);
2292
+ }
2293
+ }, onDragOver: (event) => {
2294
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
2295
+ }, onDragLeave: (event) => {
2296
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2297
+ }, onDrop: dropImages, children: [
2298
+ adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
2299
+ /* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
2102
2300
  const value = event.currentTarget.value;
2103
2301
  setDraft(value);
2104
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
2302
+ boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2105
2303
  }, onKeyDown: (event) => {
2106
2304
  if (isSendKey(event)) {
2107
2305
  event.preventDefault();
2108
2306
  send();
2109
2307
  }
2110
2308
  } }),
2111
- /* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
2309
+ /* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
2112
2310
  ] })
2113
2311
  ] })
2114
2312
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {