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

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,23 @@ 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);
890
976
  const [pickerError, setPickerError] = useState(null);
891
977
  const textarea = useRef(null);
892
978
  useAutosizeTextarea(textarea, draft);
893
- const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
979
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
980
+ useEffect(() => {
981
+ remember(draft, context, images, queue);
982
+ }, [draft, context, images, memoryKey, queue]);
894
983
  const updateQueue = (update) => setQueue((items) => {
895
984
  const next = update(items);
896
- remember(draft, context, next);
985
+ remember(draft, context, images, next);
897
986
  return next;
898
987
  });
899
988
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -903,9 +992,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
903
992
  const [next, ...rest] = queue;
904
993
  setDispatching(true);
905
994
  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 } : {} });
995
+ remember(draft, context, images, rest);
996
+ onPending?.(next.text, next.context, next.images);
997
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
909
998
  }
910
999
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
911
1000
  useEffect(() => {
@@ -918,8 +1007,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
918
1007
  if (!restoreDraft) return;
919
1008
  setDraft(restoreDraft.text);
920
1009
  const restoredContext = normalizeContext(restoreDraft.context);
1010
+ const restoredImages = normalizeImages(restoreDraft.images);
921
1011
  setContext(restoredContext);
922
- remember(restoreDraft.text, restoredContext, queue);
1012
+ setImages(restoredImages);
1013
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
923
1014
  textarea.current?.focus({ preventScroll: true });
924
1015
  onDraftRestored?.(restoreDraft.id);
925
1016
  }, [onDraftRestored, restoreDraft?.id]);
@@ -928,30 +1019,55 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
928
1019
  return () => clearTimeout(timer);
929
1020
  }, [adapter, draft]);
930
1021
  const pickContext = () => {
931
- if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
1022
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
932
1023
  setPicking(true);
933
1024
  setPickerError(null);
934
1025
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
1026
+ const attachments = partitionAttachments(picked);
1027
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1028
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
935
1029
  setContext((current) => {
936
- const next = mergeContext(current, picked);
937
- remember(draft, next, queue);
1030
+ const next = mergeContext(current, attachments.context);
1031
+ remember(draft, next, images, queue);
938
1032
  return next;
939
1033
  });
940
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1034
+ setImages((current) => {
1035
+ const next = mergeImages(current, attachments.images);
1036
+ remember(draft, context, next, queue);
1037
+ return next;
1038
+ });
1039
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1040
+ };
1041
+ const pasteImages = (event) => {
1042
+ const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
1043
+ if (!files.length) return;
1044
+ event.preventDefault();
1045
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
1046
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1047
+ return;
1048
+ }
1049
+ setPicking(true);
1050
+ setPickerError(null);
1051
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
1052
+ const next = mergeImages(current, picked);
1053
+ remember(draft, context, next, queue);
1054
+ return next;
1055
+ }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
941
1056
  };
942
1057
  const send = () => {
943
1058
  const text = draft.trim();
944
1059
  if (!text) return;
945
- const message = { text, context };
1060
+ const message = { text, context, images };
946
1061
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
947
1062
  else if (state.canSend) {
948
1063
  if (onPending) setDispatching(true);
949
- onPending?.(text, context);
950
- adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
1064
+ onPending?.(text, context, images);
1065
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
951
1066
  } else return;
952
1067
  setDraft("");
953
1068
  setContext([]);
954
- remember("", [], queuesNewMessage ? [...queue, message] : queue);
1069
+ setImages([]);
1070
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
955
1071
  };
956
1072
  return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
957
1073
  queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
@@ -962,26 +1078,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
962
1078
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
963
1079
  /* @__PURE__ */ jsxs3("span", { children: [
964
1080
  item.text,
965
- item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
966
- item.context.length,
1081
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1082
+ item.context.length + item.images.length,
967
1083
  " attached"
968
1084
  ] }) : null
969
1085
  ] }),
970
1086
  /* @__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
1087
  ] }, `${index}:${item.text}`))
972
1088
  ] }) : null,
1089
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
1090
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
1091
+ remember(draft, context, next, queue);
1092
+ return next;
1093
+ }) }),
973
1094
  /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
974
1095
  const next = items.filter((_, itemIndex) => itemIndex !== index);
975
- remember(draft, next, queue);
1096
+ remember(draft, next, images, queue);
976
1097
  return next;
977
1098
  }) }),
978
1099
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
979
1100
  /* @__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) => {
1101
+ 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,
1102
+ /* @__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
1103
  const value = event.currentTarget.value;
983
1104
  setDraft(value);
984
- remember(value, context, queue);
1105
+ remember(value, context, images, queue);
985
1106
  }, onKeyDown: (event) => {
986
1107
  if (isSendKey(event)) {
987
1108
  event.preventDefault();
@@ -1383,6 +1504,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1383
1504
  }
1384
1505
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1385
1506
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1507
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1386
1508
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1387
1509
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1388
1510
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1585,6 +1707,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1585
1707
  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
1708
  ] }, block.id)),
1587
1709
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1710
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1588
1711
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1589
1712
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1590
1713
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1923,9 +2046,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1923
2046
  if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
1924
2047
  else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
1925
2048
  }, [pending, state.busy, state.error, state.operation, state.transcript]);
1926
- const beginPending = (text, context = []) => setPending({
2049
+ const beginPending = (text, context = [], images = []) => setPending({
1927
2050
  text,
1928
2051
  context,
2052
+ images,
1929
2053
  status: "sending",
1930
2054
  initialError: state.error,
1931
2055
  seenBusy: state.busy,
@@ -1933,13 +2057,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1933
2057
  });
1934
2058
  const retryPending = () => {
1935
2059
  if (!pending) return;
1936
- adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
2060
+ adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
1937
2061
  setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
1938
2062
  };
1939
2063
  const editPending = () => {
1940
2064
  if (!pending) return;
1941
2065
  restoreSequence.current += 1;
1942
- setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
2066
+ setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
1943
2067
  setPending({ ...pending, status: "editing" });
1944
2068
  };
1945
2069
  useEffect6(() => {
@@ -1985,7 +2109,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1985
2109
  setPendingAction(null);
1986
2110
  }
1987
2111
  }, [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;
2112
+ const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
1989
2113
  const action = state.operation || pendingAction?.action || null;
1990
2114
  const actionLabel = operationLabel(action);
1991
2115
  const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
@@ -2009,10 +2133,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2009
2133
  function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2010
2134
  const startable = state.harnesses.filter((item) => item.startable);
2011
2135
  const startableKey = startable.map((item) => item.id).join("\0");
2012
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
2136
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2013
2137
  const [harness, setHarness] = useState4(remembered.harness);
2014
2138
  const [draft, setDraft] = useState4(remembered.draft);
2015
2139
  const [context, setContext] = useState4(remembered.context);
2140
+ const [images, setImages] = useState4(remembered.images ?? []);
2016
2141
  const [starting, setStarting] = useState4(null);
2017
2142
  const [picking, setPicking] = useState4(false);
2018
2143
  const [pickerError, setPickerError] = useState4(null);
@@ -2023,7 +2148,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2023
2148
  if (startable.some((item) => item.id === harness)) return;
2024
2149
  const next = startable[0]?.id ?? "";
2025
2150
  setHarness(next);
2026
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
2151
+ boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2027
2152
  }, [harness, startableKey]);
2028
2153
  useEffect6(() => {
2029
2154
  if (!starting) return;
@@ -2040,16 +2165,40 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2040
2165
  textarea.current?.focus({ preventScroll: true });
2041
2166
  }, []);
2042
2167
  const pickContext = () => {
2043
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
2168
+ if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2044
2169
  setPicking(true);
2045
2170
  setPickerError(null);
2046
2171
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
2172
+ const attachments = partitionAttachments(picked);
2173
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2174
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2047
2175
  setContext((current) => {
2048
- const next = mergeContext(current, picked);
2049
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2176
+ const next = mergeContext(current, attachments.context);
2177
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2050
2178
  return next;
2051
2179
  });
2052
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2180
+ setImages((current) => {
2181
+ const next = mergeImages(current, attachments.images);
2182
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2183
+ return next;
2184
+ });
2185
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2186
+ };
2187
+ const pasteImages = (event) => {
2188
+ const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
2189
+ if (!files.length) return;
2190
+ event.preventDefault();
2191
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
2192
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2193
+ return;
2194
+ }
2195
+ setPicking(true);
2196
+ setPickerError(null);
2197
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2198
+ const next = mergeImages(current, picked);
2199
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2200
+ return next;
2201
+ }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
2053
2202
  };
2054
2203
  const send = () => {
2055
2204
  const text = draft.trim();
@@ -2057,7 +2206,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2057
2206
  const id = startSequence.current + 1;
2058
2207
  startSequence.current = id;
2059
2208
  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 } : {} });
2209
+ const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2061
2210
  if (result && typeof result.then === "function") {
2062
2211
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2063
2212
  }
@@ -2087,24 +2236,29 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2087
2236
  /* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2088
2237
  const value = event.currentTarget.value;
2089
2238
  setHarness(value);
2090
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
2239
+ boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2091
2240
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
2092
2241
  item.label,
2093
2242
  item.startable ? "" : " \xB7 unavailable"
2094
2243
  ] }, item.id)) })
2095
2244
  ] }),
2245
+ /* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2246
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
2247
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2248
+ return next;
2249
+ }) }),
2096
2250
  /* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2097
2251
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2098
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2252
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2099
2253
  return next;
2100
2254
  }) }),
2101
2255
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2102
2256
  /* @__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) => {
2257
+ 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,
2258
+ /* @__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
2259
  const value = event.currentTarget.value;
2106
2260
  setDraft(value);
2107
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
2261
+ boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2108
2262
  }, onKeyDown: (event) => {
2109
2263
  if (isSendKey(event)) {
2110
2264
  event.preventDefault();
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 {