@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/core.mjs CHANGED
@@ -491,6 +491,18 @@ function readTranscript(value) {
491
491
  : [];
492
492
  });
493
493
  }
494
+ if (Array.isArray(item.images)) {
495
+ entry.images = item.images.flatMap((raw) => {
496
+ const image = record(raw);
497
+ return image && typeof image.label === 'string'
498
+ ? [{
499
+ ...(typeof image.id === 'string' ? { id: image.id } : {}),
500
+ label: image.label,
501
+ ...(typeof image.url === 'string' ? { url: image.url } : {}),
502
+ }]
503
+ : [];
504
+ }).slice(0, 4);
505
+ }
494
506
  const request = record(item.request);
495
507
  if (request && typeof request.requestKind === 'string' && typeof request.payloadText === 'string') {
496
508
  entry.request = {
package/embed.mjs CHANGED
@@ -504,6 +504,16 @@ function readTranscript(value) {
504
504
  }] : [];
505
505
  });
506
506
  }
507
+ if (Array.isArray(item.images)) {
508
+ entry.images = item.images.flatMap((raw) => {
509
+ const image = record(raw);
510
+ return image && typeof image.label === "string" ? [{
511
+ ...typeof image.id === "string" ? { id: image.id } : {},
512
+ label: image.label,
513
+ ...typeof image.url === "string" ? { url: image.url } : {}
514
+ }] : [];
515
+ }).slice(0, 4);
516
+ }
507
517
  const request = record(item.request);
508
518
  if (request && typeof request.requestKind === "string" && typeof request.payloadText === "string") {
509
519
  entry.request = {
@@ -780,6 +790,11 @@ var ICONS = {
780
790
  /* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
781
791
  /* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
782
792
  ] }),
793
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
794
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
795
+ /* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
796
+ /* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
797
+ ] }),
783
798
  menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
784
799
  /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
785
800
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
@@ -808,6 +823,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
808
823
  // src/context.jsx
809
824
  import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
810
825
  var MAX_CONTEXT_ITEMS = 32;
826
+ var MAX_IMAGE_ITEMS = 4;
827
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
828
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
811
829
  function normalizeContext(value) {
812
830
  return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
813
831
  if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
@@ -834,6 +852,58 @@ function mergeContext(current, picked) {
834
852
  }
835
853
  return next;
836
854
  }
855
+ function normalizeImages(value) {
856
+ const seen = /* @__PURE__ */ new Set();
857
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
858
+ if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
859
+ const label = item.label.trim().slice(0, 200);
860
+ const url = item.url;
861
+ if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
862
+ seen.add(url);
863
+ return [{
864
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
865
+ label,
866
+ url
867
+ }];
868
+ }).slice(0, MAX_IMAGE_ITEMS);
869
+ }
870
+ function mergeImages(current, picked) {
871
+ const next = [...current];
872
+ const seen = new Set(current.map((item) => item.url));
873
+ for (const item of normalizeImages(picked)) {
874
+ if (seen.has(item.url)) continue;
875
+ seen.add(item.url);
876
+ next.push(item);
877
+ if (next.length === MAX_IMAGE_ITEMS) break;
878
+ }
879
+ return next;
880
+ }
881
+ function partitionAttachments(value) {
882
+ const values = Array.isArray(value) ? value : value ? [value] : [];
883
+ const context = [];
884
+ const images = [];
885
+ for (const item of values) {
886
+ if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
887
+ 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);
888
+ else throw new Error("The attachment picker returned an invalid item.");
889
+ }
890
+ return { context, images };
891
+ }
892
+ async function imageAttachmentsFromFiles(value) {
893
+ const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
894
+ if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
895
+ return Promise.all(files.map(async (file) => {
896
+ if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
897
+ if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
898
+ const url = await new Promise((resolve, reject) => {
899
+ const reader = new FileReader();
900
+ reader.onload = () => resolve(reader.result);
901
+ reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
902
+ reader.readAsDataURL(file);
903
+ });
904
+ return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
905
+ }));
906
+ }
837
907
  function ContextTray({ items, onRemove }) {
838
908
  if (!items.length) return null;
839
909
  return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
@@ -842,6 +912,21 @@ function ContextTray({ items, onRemove }) {
842
912
  /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
843
913
  ] }, item.id ?? `${item.label}:${index}`)) });
844
914
  }
915
+ function ImageTray({ items, onRemove }) {
916
+ if (!items.length) return null;
917
+ return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
918
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
919
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
920
+ onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
921
+ ] }, item.id ?? `${item.label}:${index}`)) });
922
+ }
923
+ function MessageImages({ items }) {
924
+ if (!items?.length) return null;
925
+ 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: [
926
+ /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
927
+ item.label
928
+ ] }, item.id ?? `${item.label}:${index}`)) });
929
+ }
845
930
 
846
931
  // src/textarea.js
847
932
  import { useLayoutEffect } from "preact/hooks";
@@ -881,19 +966,24 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
881
966
  ] });
882
967
  }
883
968
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
884
- const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
969
+ const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
885
970
  const [draft, setDraft] = useState(remembered.draft);
886
- const [context, setContext] = useState(remembered.context);
887
- const [queue, setQueue] = useState(remembered.queue);
971
+ const [context, setContext] = useState(remembered.context ?? []);
972
+ const [images, setImages] = useState(remembered.images ?? []);
973
+ const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
888
974
  const [dispatching, setDispatching] = useState(false);
889
975
  const [picking, setPicking] = useState(false);
976
+ const [dragging, setDragging] = useState(false);
890
977
  const [pickerError, setPickerError] = useState(null);
891
978
  const textarea = useRef(null);
892
979
  useAutosizeTextarea(textarea, draft);
893
- const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
980
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
981
+ useEffect(() => {
982
+ remember(draft, context, images, queue);
983
+ }, [draft, context, images, memoryKey, queue]);
894
984
  const updateQueue = (update) => setQueue((items) => {
895
985
  const next = update(items);
896
- remember(draft, context, next);
986
+ remember(draft, context, images, next);
897
987
  return next;
898
988
  });
899
989
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -903,9 +993,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
903
993
  const [next, ...rest] = queue;
904
994
  setDispatching(true);
905
995
  setQueue(rest);
906
- remember(draft, context, rest);
907
- onPending?.(next.text, next.context);
908
- adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
996
+ remember(draft, context, images, rest);
997
+ onPending?.(next.text, next.context, next.images);
998
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
909
999
  }
910
1000
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
911
1001
  useEffect(() => {
@@ -918,8 +1008,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
918
1008
  if (!restoreDraft) return;
919
1009
  setDraft(restoreDraft.text);
920
1010
  const restoredContext = normalizeContext(restoreDraft.context);
1011
+ const restoredImages = normalizeImages(restoreDraft.images);
921
1012
  setContext(restoredContext);
922
- remember(restoreDraft.text, restoredContext, queue);
1013
+ setImages(restoredImages);
1014
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
923
1015
  textarea.current?.focus({ preventScroll: true });
924
1016
  onDraftRestored?.(restoreDraft.id);
925
1017
  }, [onDraftRestored, restoreDraft?.id]);
@@ -928,30 +1020,67 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
928
1020
  return () => clearTimeout(timer);
929
1021
  }, [adapter, draft]);
930
1022
  const pickContext = () => {
931
- if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
1023
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
932
1024
  setPicking(true);
933
1025
  setPickerError(null);
934
1026
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
1027
+ const attachments = partitionAttachments(picked);
1028
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1029
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
935
1030
  setContext((current) => {
936
- const next = mergeContext(current, picked);
937
- remember(draft, next, queue);
1031
+ const next = mergeContext(current, attachments.context);
1032
+ remember(draft, next, images, queue);
938
1033
  return next;
939
1034
  });
940
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1035
+ setImages((current) => {
1036
+ const next = mergeImages(current, attachments.images);
1037
+ remember(draft, context, next, queue);
1038
+ return next;
1039
+ });
1040
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1041
+ };
1042
+ const addImageFiles = (value, source) => {
1043
+ const allFiles = Array.from(value ?? []);
1044
+ if (!allFiles.length) return false;
1045
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
1046
+ if (files.length !== allFiles.length) {
1047
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
1048
+ return true;
1049
+ }
1050
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
1051
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1052
+ return true;
1053
+ }
1054
+ setPicking(true);
1055
+ setPickerError(null);
1056
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
1057
+ const next = mergeImages(current, picked);
1058
+ remember(draft, context, next, queue);
1059
+ return next;
1060
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
1061
+ return true;
1062
+ };
1063
+ const pasteImages = (event) => {
1064
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
1065
+ };
1066
+ const dropImages = (event) => {
1067
+ setDragging(false);
1068
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
941
1069
  };
942
1070
  const send = () => {
943
1071
  const text = draft.trim();
944
- if (!text) return;
945
- const message = { text, context };
1072
+ if (!text && !images.length) return;
1073
+ const message = { text, context, images };
946
1074
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
947
1075
  else if (state.canSend) {
948
1076
  if (onPending) setDispatching(true);
949
- onPending?.(text, context);
950
- adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
1077
+ onPending?.(text, context, images);
1078
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
951
1079
  } else return;
952
1080
  setDraft("");
953
1081
  setContext([]);
954
- remember("", [], queuesNewMessage ? [...queue, message] : queue);
1082
+ setImages([]);
1083
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
955
1084
  };
956
1085
  return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
957
1086
  queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
@@ -961,27 +1090,41 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
961
1090
  ] }),
962
1091
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
963
1092
  /* @__PURE__ */ jsxs3("span", { children: [
964
- item.text,
965
- item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
966
- item.context.length,
1093
+ item.text || "Image attachment",
1094
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1095
+ item.context.length + item.images.length,
967
1096
  " attached"
968
1097
  ] }) : null
969
1098
  ] }),
970
1099
  /* @__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 }) })
971
1100
  ] }, `${index}:${item.text}`))
972
1101
  ] }) : null,
1102
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
1103
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
1104
+ remember(draft, context, next, queue);
1105
+ return next;
1106
+ }) }),
973
1107
  /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
974
1108
  const next = items.filter((_, itemIndex) => itemIndex !== index);
975
- remember(draft, next, queue);
1109
+ remember(draft, next, images, queue);
976
1110
  return next;
977
1111
  }) }),
978
1112
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
979
- /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
980
- 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,
981
- /* @__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) => {
1113
+ /* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
1114
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
1115
+ event.preventDefault();
1116
+ setDragging(true);
1117
+ }
1118
+ }, onDragOver: (event) => {
1119
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
1120
+ }, onDragLeave: (event) => {
1121
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1122
+ }, onDrop: dropImages, children: [
1123
+ 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,
1124
+ /* @__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) => {
982
1125
  const value = event.currentTarget.value;
983
1126
  setDraft(value);
984
- remember(value, context, queue);
1127
+ remember(value, context, images, queue);
985
1128
  }, onKeyDown: (event) => {
986
1129
  if (isSendKey(event)) {
987
1130
  event.preventDefault();
@@ -990,7 +1133,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
990
1133
  } }),
991
1134
  /* @__PURE__ */ jsxs3("span", { children: [
992
1135
  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,
993
- /* @__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 }) })
1136
+ /* @__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 }) })
994
1137
  ] })
995
1138
  ] })
996
1139
  ] });
@@ -1383,6 +1526,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1383
1526
  }
1384
1527
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1385
1528
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1529
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1386
1530
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1387
1531
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1388
1532
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1585,6 +1729,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1585
1729
  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 })
1586
1730
  ] }, block.id)),
1587
1731
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1732
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1588
1733
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1589
1734
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1590
1735
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1923,9 +2068,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1923
2068
  if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
1924
2069
  else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
1925
2070
  }, [pending, state.busy, state.error, state.operation, state.transcript]);
1926
- const beginPending = (text, context = []) => setPending({
2071
+ const beginPending = (text, context = [], images = []) => setPending({
1927
2072
  text,
1928
2073
  context,
2074
+ images,
1929
2075
  status: "sending",
1930
2076
  initialError: state.error,
1931
2077
  seenBusy: state.busy,
@@ -1933,13 +2079,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1933
2079
  });
1934
2080
  const retryPending = () => {
1935
2081
  if (!pending) return;
1936
- adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
2082
+ adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
1937
2083
  setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
1938
2084
  };
1939
2085
  const editPending = () => {
1940
2086
  if (!pending) return;
1941
2087
  restoreSequence.current += 1;
1942
- setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
2088
+ setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
1943
2089
  setPending({ ...pending, status: "editing" });
1944
2090
  };
1945
2091
  useEffect6(() => {
@@ -1985,7 +2131,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1985
2131
  setPendingAction(null);
1986
2132
  }
1987
2133
  }, [pendingAction, state.error, state.operation]);
1988
- const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
2134
+ const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
1989
2135
  const action = state.operation || pendingAction?.action || null;
1990
2136
  const actionLabel = operationLabel(action);
1991
2137
  const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
@@ -2009,12 +2155,14 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2009
2155
  function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2010
2156
  const startable = state.harnesses.filter((item) => item.startable);
2011
2157
  const startableKey = startable.map((item) => item.id).join("\0");
2012
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
2158
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2013
2159
  const [harness, setHarness] = useState4(remembered.harness);
2014
2160
  const [draft, setDraft] = useState4(remembered.draft);
2015
2161
  const [context, setContext] = useState4(remembered.context);
2162
+ const [images, setImages] = useState4(remembered.images ?? []);
2016
2163
  const [starting, setStarting] = useState4(null);
2017
2164
  const [picking, setPicking] = useState4(false);
2165
+ const [dragging, setDragging] = useState4(false);
2018
2166
  const [pickerError, setPickerError] = useState4(null);
2019
2167
  const startSequence = useRef5(0);
2020
2168
  const textarea = useRef5(null);
@@ -2023,7 +2171,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2023
2171
  if (startable.some((item) => item.id === harness)) return;
2024
2172
  const next = startable[0]?.id ?? "";
2025
2173
  setHarness(next);
2026
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
2174
+ boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2027
2175
  }, [harness, startableKey]);
2028
2176
  useEffect6(() => {
2029
2177
  if (!starting) return;
@@ -2040,24 +2188,60 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2040
2188
  textarea.current?.focus({ preventScroll: true });
2041
2189
  }, []);
2042
2190
  const pickContext = () => {
2043
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
2191
+ if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2044
2192
  setPicking(true);
2045
2193
  setPickerError(null);
2046
2194
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
2195
+ const attachments = partitionAttachments(picked);
2196
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2197
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2047
2198
  setContext((current) => {
2048
- const next = mergeContext(current, picked);
2049
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2199
+ const next = mergeContext(current, attachments.context);
2200
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2201
+ return next;
2202
+ });
2203
+ setImages((current) => {
2204
+ const next = mergeImages(current, attachments.images);
2205
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2050
2206
  return next;
2051
2207
  });
2052
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2208
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2209
+ };
2210
+ const addImageFiles = (value, source) => {
2211
+ const allFiles = Array.from(value ?? []);
2212
+ if (!allFiles.length) return false;
2213
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
2214
+ if (files.length !== allFiles.length) {
2215
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
2216
+ return true;
2217
+ }
2218
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
2219
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2220
+ return true;
2221
+ }
2222
+ setPicking(true);
2223
+ setPickerError(null);
2224
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2225
+ const next = mergeImages(current, picked);
2226
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2227
+ return next;
2228
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2229
+ return true;
2230
+ };
2231
+ const pasteImages = (event) => {
2232
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
2233
+ };
2234
+ const dropImages = (event) => {
2235
+ setDragging(false);
2236
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
2053
2237
  };
2054
2238
  const send = () => {
2055
2239
  const text = draft.trim();
2056
- if (!text || !harness || starting) return;
2240
+ if (!text && !images.length || !harness || starting) return;
2057
2241
  const id = startSequence.current + 1;
2058
2242
  startSequence.current = id;
2059
2243
  setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2060
- const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {} });
2244
+ const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2061
2245
  if (result && typeof result.then === "function") {
2062
2246
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2063
2247
  }
@@ -2087,31 +2271,45 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2087
2271
  /* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2088
2272
  const value = event.currentTarget.value;
2089
2273
  setHarness(value);
2090
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
2274
+ boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2091
2275
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
2092
2276
  item.label,
2093
2277
  item.startable ? "" : " \xB7 unavailable"
2094
2278
  ] }, item.id)) })
2095
2279
  ] }),
2280
+ /* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2281
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
2282
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2283
+ return next;
2284
+ }) }),
2096
2285
  /* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2097
2286
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2098
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2287
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2099
2288
  return next;
2100
2289
  }) }),
2101
2290
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2102
- /* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
2103
- 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,
2104
- /* @__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) => {
2291
+ /* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2292
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2293
+ event.preventDefault();
2294
+ setDragging(true);
2295
+ }
2296
+ }, onDragOver: (event) => {
2297
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
2298
+ }, onDragLeave: (event) => {
2299
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2300
+ }, onDrop: dropImages, children: [
2301
+ 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,
2302
+ /* @__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) => {
2105
2303
  const value = event.currentTarget.value;
2106
2304
  setDraft(value);
2107
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
2305
+ boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2108
2306
  }, onKeyDown: (event) => {
2109
2307
  if (isSendKey(event)) {
2110
2308
  event.preventDefault();
2111
2309
  send();
2112
2310
  }
2113
2311
  } }),
2114
- /* @__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 }) }) })
2312
+ /* @__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 }) }) })
2115
2313
  ] })
2116
2314
  ] })
2117
2315
  ] });
package/icon.mjs CHANGED
@@ -17,6 +17,11 @@ var ICONS = {
17
17
  /* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
18
18
  /* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
19
19
  ] }),
20
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
21
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
22
+ /* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
23
+ /* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
24
+ ] }),
20
25
  menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
21
26
  /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
22
27
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
package/index.d.ts CHANGED
@@ -46,6 +46,14 @@ export interface TranscriptContext {
46
46
  detail: string;
47
47
  }
48
48
 
49
+ export interface TranscriptImage {
50
+ id?: string;
51
+ label: string;
52
+ url?: string;
53
+ }
54
+
55
+ export type TranscriptAttachment = TranscriptContext | (TranscriptImage & { url: string });
56
+
49
57
  export interface RequestOption {
50
58
  optionId: string;
51
59
  name: string;
@@ -78,6 +86,7 @@ export interface TranscriptEntryModel {
78
86
  request?: TranscriptRequest;
79
87
  code?: string;
80
88
  context?: TranscriptContext[];
89
+ images?: TranscriptImage[];
81
90
  presentation?: ToolPresentationModel;
82
91
  }
83
92
 
@@ -211,8 +220,8 @@ export type SupercodeUiIntent =
211
220
  | { action: 'loadSessions' }
212
221
  | { action: 'loadEarlier' }
213
222
  | { action: 'draft'; text: string }
214
- | { action: 'send'; text: string; context?: TranscriptContext[] }
215
- | { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[] }
223
+ | { action: 'send'; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
224
+ | { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
216
225
  | { action: 'resume' }
217
226
  | { action: 'join' }
218
227
  | { action: 'detach' }
@@ -227,8 +236,8 @@ export type SupercodeUiIntent =
227
236
 
228
237
  export interface UiAdapter {
229
238
  onIntent(intent: SupercodeUiIntent): void | Promise<void>;
230
- /** Ask the embedding host for explicit user-selected text context. */
231
- pickContext?(): TranscriptContext | TranscriptContext[] | null | Promise<TranscriptContext | TranscriptContext[] | null>;
239
+ /** Ask the embedding host for explicit user-selected text or image context. */
240
+ pickContext?(): TranscriptAttachment | TranscriptAttachment[] | null | Promise<TranscriptAttachment | TranscriptAttachment[] | null>;
232
241
  onClose?(): void;
233
242
  onOpen?(): void;
234
243
  copyText?(value: string): void | Promise<void>;
@@ -248,6 +257,7 @@ export interface MessengerLabels {
248
257
  export interface PendingMessageModel {
249
258
  text: string;
250
259
  context?: TranscriptContext[];
260
+ images?: TranscriptImage[];
251
261
  status: 'sending' | 'failed';
252
262
  onRetry?(): void;
253
263
  onEdit?(): void;
@@ -358,7 +368,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
358
368
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void }): VNode;
359
369
  export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string }): VNode;
360
370
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
361
- export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[] } | null; onPending?(text: string, context?: TranscriptContext[]): void; onDraftRestored?(id: string | number): void }): VNode;
371
+ export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): VNode;
362
372
  export function SupercodeMessenger(props: MessengerProps): VNode;
363
373
 
364
374
  export interface MountedMessenger {