@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/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,23 @@ 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);
887
973
  const [pickerError, setPickerError] = useState(null);
888
974
  const textarea = useRef(null);
889
975
  useAutosizeTextarea(textarea, draft);
890
- const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
976
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
977
+ useEffect(() => {
978
+ remember(draft, context, images, queue);
979
+ }, [draft, context, images, memoryKey, queue]);
891
980
  const updateQueue = (update) => setQueue((items) => {
892
981
  const next = update(items);
893
- remember(draft, context, next);
982
+ remember(draft, context, images, next);
894
983
  return next;
895
984
  });
896
985
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -900,9 +989,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
900
989
  const [next, ...rest] = queue;
901
990
  setDispatching(true);
902
991
  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 } : {} });
992
+ remember(draft, context, images, rest);
993
+ onPending?.(next.text, next.context, next.images);
994
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
906
995
  }
907
996
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
908
997
  useEffect(() => {
@@ -915,8 +1004,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
915
1004
  if (!restoreDraft) return;
916
1005
  setDraft(restoreDraft.text);
917
1006
  const restoredContext = normalizeContext(restoreDraft.context);
1007
+ const restoredImages = normalizeImages(restoreDraft.images);
918
1008
  setContext(restoredContext);
919
- remember(restoreDraft.text, restoredContext, queue);
1009
+ setImages(restoredImages);
1010
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
920
1011
  textarea.current?.focus({ preventScroll: true });
921
1012
  onDraftRestored?.(restoreDraft.id);
922
1013
  }, [onDraftRestored, restoreDraft?.id]);
@@ -925,30 +1016,55 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
925
1016
  return () => clearTimeout(timer);
926
1017
  }, [adapter, draft]);
927
1018
  const pickContext = () => {
928
- if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
1019
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
929
1020
  setPicking(true);
930
1021
  setPickerError(null);
931
1022
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
1023
+ const attachments = partitionAttachments(picked);
1024
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1025
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
932
1026
  setContext((current) => {
933
- const next = mergeContext(current, picked);
934
- remember(draft, next, queue);
1027
+ const next = mergeContext(current, attachments.context);
1028
+ remember(draft, next, images, queue);
935
1029
  return next;
936
1030
  });
937
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1031
+ setImages((current) => {
1032
+ const next = mergeImages(current, attachments.images);
1033
+ remember(draft, context, next, queue);
1034
+ return next;
1035
+ });
1036
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1037
+ };
1038
+ const pasteImages = (event) => {
1039
+ const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
1040
+ if (!files.length) return;
1041
+ event.preventDefault();
1042
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
1043
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1044
+ return;
1045
+ }
1046
+ setPicking(true);
1047
+ setPickerError(null);
1048
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
1049
+ const next = mergeImages(current, picked);
1050
+ remember(draft, context, next, queue);
1051
+ return next;
1052
+ }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
938
1053
  };
939
1054
  const send = () => {
940
1055
  const text = draft.trim();
941
1056
  if (!text) return;
942
- const message = { text, context };
1057
+ const message = { text, context, images };
943
1058
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
944
1059
  else if (state.canSend) {
945
1060
  if (onPending) setDispatching(true);
946
- onPending?.(text, context);
947
- adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
1061
+ onPending?.(text, context, images);
1062
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
948
1063
  } else return;
949
1064
  setDraft("");
950
1065
  setContext([]);
951
- remember("", [], queuesNewMessage ? [...queue, message] : queue);
1066
+ setImages([]);
1067
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
952
1068
  };
953
1069
  return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
954
1070
  queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
@@ -959,26 +1075,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
959
1075
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
960
1076
  /* @__PURE__ */ jsxs3("span", { children: [
961
1077
  item.text,
962
- item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
963
- item.context.length,
1078
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1079
+ item.context.length + item.images.length,
964
1080
  " attached"
965
1081
  ] }) : null
966
1082
  ] }),
967
1083
  /* @__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
1084
  ] }, `${index}:${item.text}`))
969
1085
  ] }) : null,
1086
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
1087
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
1088
+ remember(draft, context, next, queue);
1089
+ return next;
1090
+ }) }),
970
1091
  /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
971
1092
  const next = items.filter((_, itemIndex) => itemIndex !== index);
972
- remember(draft, next, queue);
1093
+ remember(draft, next, images, queue);
973
1094
  return next;
974
1095
  }) }),
975
1096
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
976
1097
  /* @__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) => {
1098
+ 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,
1099
+ /* @__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
1100
  const value = event.currentTarget.value;
980
1101
  setDraft(value);
981
- remember(value, context, queue);
1102
+ remember(value, context, images, queue);
982
1103
  }, onKeyDown: (event) => {
983
1104
  if (isSendKey(event)) {
984
1105
  event.preventDefault();
@@ -1380,6 +1501,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1380
1501
  }
1381
1502
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1382
1503
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1504
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1383
1505
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1384
1506
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1385
1507
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1582,6 +1704,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1582
1704
  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
1705
  ] }, block.id)),
1584
1706
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1707
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1585
1708
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1586
1709
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1587
1710
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1920,9 +2043,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1920
2043
  if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
1921
2044
  else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
1922
2045
  }, [pending, state.busy, state.error, state.operation, state.transcript]);
1923
- const beginPending = (text, context = []) => setPending({
2046
+ const beginPending = (text, context = [], images = []) => setPending({
1924
2047
  text,
1925
2048
  context,
2049
+ images,
1926
2050
  status: "sending",
1927
2051
  initialError: state.error,
1928
2052
  seenBusy: state.busy,
@@ -1930,13 +2054,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1930
2054
  });
1931
2055
  const retryPending = () => {
1932
2056
  if (!pending) return;
1933
- adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
2057
+ adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
1934
2058
  setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
1935
2059
  };
1936
2060
  const editPending = () => {
1937
2061
  if (!pending) return;
1938
2062
  restoreSequence.current += 1;
1939
- setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
2063
+ setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
1940
2064
  setPending({ ...pending, status: "editing" });
1941
2065
  };
1942
2066
  useEffect6(() => {
@@ -1982,7 +2106,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
1982
2106
  setPendingAction(null);
1983
2107
  }
1984
2108
  }, [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;
2109
+ const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
1986
2110
  const action = state.operation || pendingAction?.action || null;
1987
2111
  const actionLabel = operationLabel(action);
1988
2112
  const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
@@ -2006,10 +2130,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2006
2130
  function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2007
2131
  const startable = state.harnesses.filter((item) => item.startable);
2008
2132
  const startableKey = startable.map((item) => item.id).join("\0");
2009
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
2133
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2010
2134
  const [harness, setHarness] = useState4(remembered.harness);
2011
2135
  const [draft, setDraft] = useState4(remembered.draft);
2012
2136
  const [context, setContext] = useState4(remembered.context);
2137
+ const [images, setImages] = useState4(remembered.images ?? []);
2013
2138
  const [starting, setStarting] = useState4(null);
2014
2139
  const [picking, setPicking] = useState4(false);
2015
2140
  const [pickerError, setPickerError] = useState4(null);
@@ -2020,7 +2145,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2020
2145
  if (startable.some((item) => item.id === harness)) return;
2021
2146
  const next = startable[0]?.id ?? "";
2022
2147
  setHarness(next);
2023
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
2148
+ boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2024
2149
  }, [harness, startableKey]);
2025
2150
  useEffect6(() => {
2026
2151
  if (!starting) return;
@@ -2037,16 +2162,40 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2037
2162
  textarea.current?.focus({ preventScroll: true });
2038
2163
  }, []);
2039
2164
  const pickContext = () => {
2040
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
2165
+ if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2041
2166
  setPicking(true);
2042
2167
  setPickerError(null);
2043
2168
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
2169
+ const attachments = partitionAttachments(picked);
2170
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2171
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2044
2172
  setContext((current) => {
2045
- const next = mergeContext(current, picked);
2046
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2173
+ const next = mergeContext(current, attachments.context);
2174
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2047
2175
  return next;
2048
2176
  });
2049
- }, (error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2177
+ setImages((current) => {
2178
+ const next = mergeImages(current, attachments.images);
2179
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2180
+ return next;
2181
+ });
2182
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2183
+ };
2184
+ const pasteImages = (event) => {
2185
+ const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
2186
+ if (!files.length) return;
2187
+ event.preventDefault();
2188
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
2189
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2190
+ return;
2191
+ }
2192
+ setPicking(true);
2193
+ setPickerError(null);
2194
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2195
+ const next = mergeImages(current, picked);
2196
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2197
+ return next;
2198
+ }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
2050
2199
  };
2051
2200
  const send = () => {
2052
2201
  const text = draft.trim();
@@ -2054,7 +2203,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2054
2203
  const id = startSequence.current + 1;
2055
2204
  startSequence.current = id;
2056
2205
  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 } : {} });
2206
+ const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2058
2207
  if (result && typeof result.then === "function") {
2059
2208
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2060
2209
  }
@@ -2084,24 +2233,29 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2084
2233
  /* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2085
2234
  const value = event.currentTarget.value;
2086
2235
  setHarness(value);
2087
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
2236
+ boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2088
2237
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
2089
2238
  item.label,
2090
2239
  item.startable ? "" : " \xB7 unavailable"
2091
2240
  ] }, item.id)) })
2092
2241
  ] }),
2242
+ /* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2243
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
2244
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2245
+ return next;
2246
+ }) }),
2093
2247
  /* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2094
2248
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2095
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
2249
+ boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2096
2250
  return next;
2097
2251
  }) }),
2098
2252
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2099
2253
  /* @__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) => {
2254
+ 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,
2255
+ /* @__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
2256
  const value = event.currentTarget.value;
2103
2257
  setDraft(value);
2104
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
2258
+ boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2105
2259
  }, onKeyDown: (event) => {
2106
2260
  if (isSendKey(event)) {
2107
2261
  event.preventDefault();
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.21",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
package/sessions.mjs CHANGED
@@ -162,6 +162,11 @@ var ICONS = {
162
162
  /* @__PURE__ */ jsx2("path", { d: "M9 3.5v10" }),
163
163
  /* @__PURE__ */ jsx2("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
164
164
  ] }),
165
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
166
+ /* @__PURE__ */ jsx2("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
167
+ /* @__PURE__ */ jsx2("circle", { cx: "6.5", cy: "7", r: "1.25" }),
168
+ /* @__PURE__ */ jsx2("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
169
+ ] }),
165
170
  menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
166
171
  /* @__PURE__ */ jsx2("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
167
172
  /* @__PURE__ */ jsx2("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
@@ -187,8 +192,12 @@ function UiIcon({ name, size = 16, class: className = "" }) {
187
192
  return /* @__PURE__ */ jsx2("svg", { class: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(Glyph, {}) });
188
193
  }
189
194
 
195
+ // src/context.jsx
196
+ import { jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
197
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
198
+
190
199
  // src/conversation.jsx
191
- import { Fragment as Fragment3, jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
200
+ import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
192
201
  function LoadingStatus({ state, compact = false }) {
193
202
  const copy = {
194
203
  connecting: ["Connecting to coding agents", "Checking installed harnesses and capabilities.", 0],
@@ -196,19 +205,19 @@ function LoadingStatus({ state, compact = false }) {
196
205
  discovering: ["Loading recent sessions", "Scanning native session stores without loading full transcripts.", 2],
197
206
  ready: ["Ready", "Coding sessions are up to date.", 3]
198
207
  }[state.startup];
199
- return /* @__PURE__ */ jsxs2("div", { class: `scui-loading${compact ? " scui-loading-compact" : ""}`, role: "status", "aria-busy": state.startup !== "ready", children: [
200
- /* @__PURE__ */ jsx3("span", { class: "scui-orbit", "aria-hidden": "true", children: /* @__PURE__ */ jsx3("i", {}) }),
201
- /* @__PURE__ */ jsxs2("span", { class: "scui-loading-copy", children: [
202
- /* @__PURE__ */ jsx3("strong", { children: copy[0] }),
203
- /* @__PURE__ */ jsx3("small", { children: copy[1] })
208
+ return /* @__PURE__ */ jsxs3("div", { class: `scui-loading${compact ? " scui-loading-compact" : ""}`, role: "status", "aria-busy": state.startup !== "ready", children: [
209
+ /* @__PURE__ */ jsx4("span", { class: "scui-orbit", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("i", {}) }),
210
+ /* @__PURE__ */ jsxs3("span", { class: "scui-loading-copy", children: [
211
+ /* @__PURE__ */ jsx4("strong", { children: copy[0] }),
212
+ /* @__PURE__ */ jsx4("small", { children: copy[1] })
204
213
  ] }),
205
- /* @__PURE__ */ jsx3("span", { class: "scui-progress", "aria-hidden": "true", children: [1, 2, 3].map((step) => /* @__PURE__ */ jsx3("i", { "data-progress": step <= copy[2] ? "done" : step === copy[2] + 1 ? "current" : "waiting" }, step)) })
214
+ /* @__PURE__ */ jsx4("span", { class: "scui-progress", "aria-hidden": "true", children: [1, 2, 3].map((step) => /* @__PURE__ */ jsx4("i", { "data-progress": step <= copy[2] ? "done" : step === copy[2] + 1 ? "current" : "waiting" }, step)) })
206
215
  ] });
207
216
  }
208
217
 
209
218
  // src/logo.jsx
210
219
  import { useEffect as useEffect3 } from "preact/hooks";
211
- import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
220
+ import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
212
221
  var LOGOS = {
213
222
  "claude-code": {
214
223
  viewBox: "-1 3.5 26 18",
@@ -259,29 +268,29 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
259
268
  if (!logo) onMissingLogo?.(id);
260
269
  }, [id, logo, onMissingLogo]);
261
270
  if (!logo) return null;
262
- return /* @__PURE__ */ jsxs3("span", { class: "scui-logo", "data-harness": id, "data-activity": activity, style: `--scui-logo-size:${size}px`, "aria-hidden": "true", children: [
263
- /* @__PURE__ */ jsx4("svg", { viewBox: logo.viewBox, preserveAspectRatio: "xMidYMid meet", focusable: "false", children: logo.paths.map(([Tag, props], index) => /* @__PURE__ */ jsx4(Tag, { ...props }, index)) }),
264
- activity && activity !== "idle" ? /* @__PURE__ */ jsx4("i", {}) : null
271
+ return /* @__PURE__ */ jsxs4("span", { class: "scui-logo", "data-harness": id, "data-activity": activity, style: `--scui-logo-size:${size}px`, "aria-hidden": "true", children: [
272
+ /* @__PURE__ */ jsx5("svg", { viewBox: logo.viewBox, preserveAspectRatio: "xMidYMid meet", focusable: "false", children: logo.paths.map(([Tag, props], index) => /* @__PURE__ */ jsx5(Tag, { ...props }, index)) }),
273
+ activity && activity !== "idle" ? /* @__PURE__ */ jsx5("i", {}) : null
265
274
  ] });
266
275
  }
267
276
 
268
277
  // src/sessions.jsx
269
- import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
278
+ import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
270
279
  var sessionListMemory = /* @__PURE__ */ new Map();
271
280
  function SessionRow({ row, state, onOpen }) {
272
281
  const activity = sessionActivity(state, row);
273
282
  const attentionPreview = state.attention.find((item) => item.key === row.key)?.preview;
274
283
  const title = sessionDisplayName(row);
275
284
  const detail = attentionPreview || row.preview || (row.name && row.name !== title ? row.name : row.cwd);
276
- return /* @__PURE__ */ jsxs4("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${detail ? ` \xB7 ${detail}` : ""}${row.age ? ` \xB7 ${row.age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
277
- /* @__PURE__ */ jsx5(HarnessLogo, { id: row.harness, activity, size: 34 }),
278
- /* @__PURE__ */ jsxs4("span", { class: "scui-session-copy", children: [
279
- /* @__PURE__ */ jsxs4("span", { children: [
280
- /* @__PURE__ */ jsx5("strong", { children: title }),
281
- /* @__PURE__ */ jsx5("small", { children: row.age })
285
+ return /* @__PURE__ */ jsxs5("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${detail ? ` \xB7 ${detail}` : ""}${row.age ? ` \xB7 ${row.age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
286
+ /* @__PURE__ */ jsx6(HarnessLogo, { id: row.harness, activity, size: 34 }),
287
+ /* @__PURE__ */ jsxs5("span", { class: "scui-session-copy", children: [
288
+ /* @__PURE__ */ jsxs5("span", { children: [
289
+ /* @__PURE__ */ jsx6("strong", { children: title }),
290
+ /* @__PURE__ */ jsx6("small", { children: row.age })
282
291
  ] }),
283
- detail ? /* @__PURE__ */ jsx5("span", { children: /* @__PURE__ */ jsx5("small", { title: row.cwd, children: detail }) }) : null,
284
- state.attachError?.key === row.key ? /* @__PURE__ */ jsx5("em", { children: state.attachError.message }) : null
292
+ detail ? /* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("small", { title: row.cwd, children: detail }) }) : null,
293
+ state.attachError?.key === row.key ? /* @__PURE__ */ jsx6("em", { children: state.attachError.message }) : null
285
294
  ] })
286
295
  ] });
287
296
  }
@@ -312,33 +321,33 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
312
321
  Promise.resolve(result).then(() => setLoadingMore(false), () => setLoadingMore(false));
313
322
  }
314
323
  };
315
- return /* @__PURE__ */ jsxs4("section", { class: "scui-list", ref: root, children: [
316
- /* @__PURE__ */ jsxs4("header", { class: "scui-head", children: [
317
- /* @__PURE__ */ jsxs4("span", { class: "scui-head-copy", children: [
318
- /* @__PURE__ */ jsx5("strong", { children: labels.chats }),
319
- /* @__PURE__ */ jsxs4("small", { children: [
324
+ return /* @__PURE__ */ jsxs5("section", { class: "scui-list", ref: root, children: [
325
+ /* @__PURE__ */ jsxs5("header", { class: "scui-head", children: [
326
+ /* @__PURE__ */ jsxs5("span", { class: "scui-head-copy", children: [
327
+ /* @__PURE__ */ jsx6("strong", { children: labels.chats }),
328
+ /* @__PURE__ */ jsxs5("small", { children: [
320
329
  state.sessions.length,
321
330
  " recent conversations"
322
331
  ] })
323
332
  ] }),
324
- /* @__PURE__ */ jsx5("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx5(UiIcon, { name: "plus", size: 18 }) }),
325
- onClose ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx5(UiIcon, { name: "close", size: 18 }) }) : null
333
+ /* @__PURE__ */ jsx6("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx6(UiIcon, { name: "plus", size: 18 }) }),
334
+ onClose ? /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx6(UiIcon, { name: "close", size: 18 }) }) : null
326
335
  ] }),
327
- state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state, compact: rows.length > 0 }) : null,
328
- state.sessions.length > 4 ? /* @__PURE__ */ jsxs4("label", { class: "scui-search", children: [
329
- /* @__PURE__ */ jsx5(UiIcon, { name: "search", size: 15 }),
330
- /* @__PURE__ */ jsx5("input", { type: "search", "aria-label": labels.searchChats, placeholder: labels.searchChats, value: query, onInput: (event) => {
336
+ state.startup !== "ready" ? /* @__PURE__ */ jsx6(LoadingStatus, { state, compact: rows.length > 0 }) : null,
337
+ state.sessions.length > 4 ? /* @__PURE__ */ jsxs5("label", { class: "scui-search", children: [
338
+ /* @__PURE__ */ jsx6(UiIcon, { name: "search", size: 15 }),
339
+ /* @__PURE__ */ jsx6("input", { type: "search", "aria-label": labels.searchChats, placeholder: labels.searchChats, value: query, onInput: (event) => {
331
340
  const value = event.currentTarget.value;
332
341
  setQuery(value);
333
342
  boundedSet(sessionListMemory, memoryKey, { query: value, top: 0 });
334
343
  if (rowScroller.current) rowScroller.current.scrollTop = 0;
335
344
  } }),
336
- /* @__PURE__ */ jsx5("small", { children: rows.length })
345
+ /* @__PURE__ */ jsx6("small", { children: rows.length })
337
346
  ] }) : null,
338
- /* @__PURE__ */ jsxs4("div", { class: "scui-session-rows", ref: rowScroller, onScroll: (event) => boundedSet(sessionListMemory, memoryKey, { query, top: event.currentTarget.scrollTop }), children: [
339
- !rows.length && state.startup === "ready" ? /* @__PURE__ */ jsx5("div", { class: "scui-empty", children: query ? "No chats match your search." : state.error ?? "No coding chats found." }) : null,
340
- rows.map((row) => /* @__PURE__ */ jsx5(Row, { value: row, row, state, adapter, onOpen }, row.key)),
341
- state.history.hasMoreSessions ? /* @__PURE__ */ jsx5("button", { class: "scui-load", type: "button", disabled: loadingMore, onClick: loadMore, children: loadingMore ? "Loading older chats\u2026" : "Load older chats" }) : null
347
+ /* @__PURE__ */ jsxs5("div", { class: "scui-session-rows", ref: rowScroller, onScroll: (event) => boundedSet(sessionListMemory, memoryKey, { query, top: event.currentTarget.scrollTop }), children: [
348
+ !rows.length && state.startup === "ready" ? /* @__PURE__ */ jsx6("div", { class: "scui-empty", children: query ? "No chats match your search." : state.error ?? "No coding chats found." }) : null,
349
+ rows.map((row) => /* @__PURE__ */ jsx6(Row, { value: row, row, state, adapter, onOpen }, row.key)),
350
+ state.history.hasMoreSessions ? /* @__PURE__ */ jsx6("button", { class: "scui-load", type: "button", disabled: loadingMore, onClick: loadMore, children: loadingMore ? "Loading older chats\u2026" : "Load older chats" }) : null
342
351
  ] })
343
352
  ] });
344
353
  }
package/styles.css CHANGED
@@ -173,6 +173,8 @@
173
173
  .scui-envelope { display:flex; align-items:flex-end; gap:7px; padding:7px; border:1px solid var(--scui-border-strong); border-radius:16px; background:var(--scui-bg) }.scui-envelope textarea { flex:1; min-width:0; min-height:34px; max-height:150px; resize:none; overflow-y:hidden; border:0; outline:0; background:transparent; color:var(--scui-fg) }.scui-envelope > span { display:flex; gap:4px }
174
174
  .scui-attach { display:grid; flex:0 0 34px; width:34px; height:34px; place-items:center; border:0; border-radius:50%; background:transparent; color:var(--scui-muted) }.scui-attach:hover:not(:disabled) { background:var(--scui-fill); color:var(--scui-fg) }
175
175
  .scui-compose-context { display:flex; gap:5px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-context > span { display:flex; flex:0 0 auto; max-width:210px; align-items:center; gap:5px; padding:4px 5px 4px 7px; border:1px solid var(--scui-border); border-radius:999px; background:var(--scui-fill); font-size:10px }.scui-compose-context strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-compose-context button { display:grid; width:18px; height:18px; padding:0; place-items:center; border:0; border-radius:50%; background:transparent }.scui-compose-context button:hover { background:color-mix(in srgb,var(--scui-fg) 9%,transparent) }.scui-context-error { display:block; margin:0 2px 6px; color:var(--scui-danger) }
176
+ .scui-compose-images { display:flex; gap:6px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-images > span { position:relative; display:grid; flex:0 0 58px; width:58px; height:58px; overflow:hidden; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-fill); place-items:center }.scui-compose-images img { width:100%; height:100%; object-fit:cover }.scui-compose-images strong { position:absolute; right:2px; bottom:2px; left:2px; overflow:hidden; padding:2px 3px; border-radius:4px; background:rgba(0,0,0,.65); color:#fff; font-size:8px; font-weight:500; text-overflow:ellipsis; white-space:nowrap }.scui-compose-images button { position:absolute; z-index:1; top:2px; right:2px; display:grid; width:19px; height:19px; padding:0; border:1px solid rgba(255,255,255,.32); border-radius:50%; background:rgba(0,0,0,.68); color:#fff; place-items:center; cursor:pointer }
177
+ .scui-message-images { display:flex; max-width:280px; gap:5px; margin-bottom:6px; flex-wrap:wrap }.scui-message-images img { display:block; width:auto; min-width:72px; max-width:100%; height:auto; max-height:210px; border:1px solid var(--scui-border); border-radius:8px; object-fit:contain; background:var(--scui-fill) }.scui-message-images > span { display:flex; align-items:center; gap:5px; padding:5px 7px; border:1px solid var(--scui-border); border-radius:7px; color:var(--scui-muted); font-size:10px }
176
178
  .scui-send,.scui-stop { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:var(--scui-accent); color:#fff; cursor:pointer }.scui-stop { background:var(--scui-danger) }.scui-send:disabled,.scui-stop:disabled { opacity:.4; cursor:default }.scui-control-spinner { box-sizing:border-box; width:13px; height:13px; border:1.5px solid currentColor; border-right-color:transparent; border-radius:50%; animation:scui-spin .75s linear infinite }
177
179
  .scui-queue { display:grid; gap:4px; max-height:85px; margin-bottom:6px; overflow:auto; font-size:10.5px }.scui-queue > span { display:flex; gap:6px; padding:4px 6px; border-radius:5px; background:var(--scui-fill) }.scui-queue > span > span { display:grid; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-queue small { color:var(--scui-muted) }.scui-queue > span button { margin-left:auto; border:0; background:transparent }
178
180
  .scui-harness-picker { display:flex; align-items:center; gap:7px; margin-bottom:7px }.scui-harness-picker select { margin-left:auto; max-width:55%; padding:4px; border:1px solid var(--scui-border); border-radius:6px; background:var(--scui-bg); color:var(--scui-fg) }