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

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/components.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/composer.jsx
2
- import { useEffect, useRef, useState } from "preact/hooks";
2
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "preact/hooks";
3
3
 
4
4
  // core.mjs
5
5
  var HARNESS_NAMES = Object.freeze({
@@ -815,7 +815,8 @@ function UiIcon({ name, size = 16, class: className = "" }) {
815
815
  }
816
816
 
817
817
  // src/context.jsx
818
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
818
+ import { useEffect, useRef, useState } from "preact/hooks";
819
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
819
820
  var MAX_CONTEXT_ITEMS = 32;
820
821
  var MAX_IMAGE_ITEMS = 4;
821
822
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
@@ -914,12 +915,101 @@ function ImageTray({ items, onRemove }) {
914
915
  onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
915
916
  ] }, item.id ?? `${item.label}:${index}`)) });
916
917
  }
917
- function MessageImages({ items }) {
918
+ function imageFilename(label) {
919
+ const value = label.trim().replace(/[\\/:*?"<>|]+/g, "-");
920
+ return value || "image";
921
+ }
922
+ function ImageViewer({ items, index, adapter, onChange, onClose }) {
923
+ const dialog = useRef(null);
924
+ const reset = useRef(null);
925
+ const [copyState, setCopyState2] = useState("idle");
926
+ const item = items[index];
927
+ const remote = item?.url?.startsWith("http://") || item?.url?.startsWith("https://");
928
+ useEffect(() => {
929
+ if (!dialog.current?.open) dialog.current?.showModal();
930
+ return () => clearTimeout(reset.current);
931
+ }, []);
932
+ useEffect(() => {
933
+ clearTimeout(reset.current);
934
+ setCopyState2("idle");
935
+ }, [index]);
936
+ if (!item?.url) return null;
937
+ const move = (amount) => onChange((index + amount + items.length) % items.length);
938
+ const copy = async () => {
939
+ try {
940
+ await adapter.copyText(item.url);
941
+ setCopyState2("copied");
942
+ } catch {
943
+ setCopyState2("failed");
944
+ }
945
+ clearTimeout(reset.current);
946
+ reset.current = setTimeout(() => setCopyState2("idle"), 1500);
947
+ };
948
+ const close = () => dialog.current?.close();
949
+ return /* @__PURE__ */ jsxs2(
950
+ "dialog",
951
+ {
952
+ ref: dialog,
953
+ class: "scui-image-viewer",
954
+ "aria-label": `Image preview: ${item.label}`,
955
+ onClose,
956
+ onClick: (event) => {
957
+ if (event.target === event.currentTarget) close();
958
+ },
959
+ onKeyDown: (event) => {
960
+ if (items.length < 2 || !["ArrowLeft", "ArrowRight"].includes(event.key)) return;
961
+ event.preventDefault();
962
+ move(event.key === "ArrowLeft" ? -1 : 1);
963
+ },
964
+ children: [
965
+ /* @__PURE__ */ jsxs2("header", { children: [
966
+ /* @__PURE__ */ jsxs2("span", { children: [
967
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
968
+ items.length > 1 ? /* @__PURE__ */ jsxs2("small", { children: [
969
+ index + 1,
970
+ " of ",
971
+ items.length
972
+ ] }) : null
973
+ ] }),
974
+ /* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
975
+ remote && adapter?.copyText ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": copyState === "copied" ? "Image link copied" : copyState === "failed" ? "Could not copy image link" : "Copy image link", title: "Copy image link", "data-status": copyState, onClick: copy, children: /* @__PURE__ */ jsx2(UiIcon, { name: copyState === "copied" ? "check" : "copy", size: 16 }) }) : null,
976
+ remote ? /* @__PURE__ */ jsx2("a", { href: item.url, target: "_blank", rel: "noreferrer", "aria-label": "Open original image", title: "Open original image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }) }) : /* @__PURE__ */ jsx2("a", { href: item.url, download: imageFilename(item.label), "aria-label": "Download image", title: "Download image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "down", size: 16 }) }),
977
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 17 }) })
978
+ ] })
979
+ ] }),
980
+ /* @__PURE__ */ jsxs2("figure", { children: [
981
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: item.label }),
982
+ items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
983
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) }),
984
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) })
985
+ ] }) : null
986
+ ] })
987
+ ]
988
+ }
989
+ );
990
+ }
991
+ function MessageImages({ items, adapter }) {
992
+ const [active, setActive] = useState(null);
993
+ const opener = useRef(null);
918
994
  if (!items?.length) return null;
919
- 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: [
920
- /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
921
- item.label
922
- ] }, item.id ?? `${item.label}:${index}`)) });
995
+ const viewable = items.filter((item) => item.url);
996
+ const close = () => {
997
+ setActive(null);
998
+ requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
999
+ };
1000
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1001
+ /* @__PURE__ */ jsx2("div", { class: "scui-message-images", "aria-label": "Message images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `View image ${item.label}`, onClick: (event) => {
1002
+ opener.current = event.currentTarget;
1003
+ setActive(viewable.indexOf(item));
1004
+ }, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
1005
+ /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
1006
+ /* @__PURE__ */ jsxs2("span", { children: [
1007
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
1008
+ /* @__PURE__ */ jsx2("small", { children: "Preview unavailable" })
1009
+ ] })
1010
+ ] }, item.id ?? `${item.label}:${index}`)) }),
1011
+ active !== null && viewable[active] ? /* @__PURE__ */ jsx2(ImageViewer, { items: viewable, index: active, adapter, onChange: setActive, onClose: close }) : null
1012
+ ] });
923
1013
  }
924
1014
 
925
1015
  // src/textarea.js
@@ -961,17 +1051,18 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
961
1051
  }
962
1052
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
963
1053
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
964
- const [draft, setDraft] = useState(remembered.draft);
965
- const [context, setContext] = useState(remembered.context ?? []);
966
- const [images, setImages] = useState(remembered.images ?? []);
967
- const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
968
- const [dispatching, setDispatching] = useState(false);
969
- const [picking, setPicking] = useState(false);
970
- const [pickerError, setPickerError] = useState(null);
971
- const textarea = useRef(null);
1054
+ const [draft, setDraft] = useState2(remembered.draft);
1055
+ const [context, setContext] = useState2(remembered.context ?? []);
1056
+ const [images, setImages] = useState2(remembered.images ?? []);
1057
+ const [queue, setQueue] = useState2((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
1058
+ const [dispatching, setDispatching] = useState2(false);
1059
+ const [picking, setPicking] = useState2(false);
1060
+ const [dragging, setDragging] = useState2(false);
1061
+ const [pickerError, setPickerError] = useState2(null);
1062
+ const textarea = useRef2(null);
972
1063
  useAutosizeTextarea(textarea, draft);
973
1064
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
974
- useEffect(() => {
1065
+ useEffect2(() => {
975
1066
  remember(draft, context, images, queue);
976
1067
  }, [draft, context, images, memoryKey, queue]);
977
1068
  const updateQueue = (update) => setQueue((items) => {
@@ -981,7 +1072,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
981
1072
  });
982
1073
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
983
1074
  const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
984
- useEffect(() => {
1075
+ useEffect2(() => {
985
1076
  if (!queueBlocked && state.canSend && queue.length) {
986
1077
  const [next, ...rest] = queue;
987
1078
  setDispatching(true);
@@ -991,13 +1082,13 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
991
1082
  adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
992
1083
  }
993
1084
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
994
- useEffect(() => {
1085
+ useEffect2(() => {
995
1086
  if (pendingStatus !== null || state.busy) setDispatching(false);
996
1087
  }, [pendingStatus, state.busy]);
997
- useEffect(() => {
1088
+ useEffect2(() => {
998
1089
  textarea.current?.focus({ preventScroll: true });
999
1090
  }, [memoryKey]);
1000
- useEffect(() => {
1091
+ useEffect2(() => {
1001
1092
  if (!restoreDraft) return;
1002
1093
  setDraft(restoreDraft.text);
1003
1094
  const restoredContext = normalizeContext(restoreDraft.context);
@@ -1008,7 +1099,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1008
1099
  textarea.current?.focus({ preventScroll: true });
1009
1100
  onDraftRestored?.(restoreDraft.id);
1010
1101
  }, [onDraftRestored, restoreDraft?.id]);
1011
- useEffect(() => {
1102
+ useEffect2(() => {
1012
1103
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
1013
1104
  return () => clearTimeout(timer);
1014
1105
  }, [adapter, draft]);
@@ -1032,13 +1123,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1032
1123
  });
1033
1124
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1034
1125
  };
1035
- const pasteImages = (event) => {
1036
- const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
1037
- if (!files.length) return;
1038
- event.preventDefault();
1126
+ const addImageFiles = (value, source) => {
1127
+ const allFiles = Array.from(value ?? []);
1128
+ if (!allFiles.length) return false;
1129
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
1130
+ if (files.length !== allFiles.length) {
1131
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
1132
+ return true;
1133
+ }
1039
1134
  if (images.length + files.length > MAX_IMAGE_ITEMS) {
1040
1135
  setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1041
- return;
1136
+ return true;
1042
1137
  }
1043
1138
  setPicking(true);
1044
1139
  setPickerError(null);
@@ -1046,11 +1141,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1046
1141
  const next = mergeImages(current, picked);
1047
1142
  remember(draft, context, next, queue);
1048
1143
  return next;
1049
- }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
1144
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
1145
+ return true;
1146
+ };
1147
+ const pasteImages = (event) => {
1148
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
1149
+ };
1150
+ const dropImages = (event) => {
1151
+ setDragging(false);
1152
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
1050
1153
  };
1051
1154
  const send = () => {
1052
1155
  const text = draft.trim();
1053
- if (!text) return;
1156
+ if (!text && !images.length) return;
1054
1157
  const message = { text, context, images };
1055
1158
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
1056
1159
  else if (state.canSend) {
@@ -1071,7 +1174,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1071
1174
  ] }),
1072
1175
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
1073
1176
  /* @__PURE__ */ jsxs3("span", { children: [
1074
- item.text,
1177
+ item.text || "Image attachment",
1075
1178
  item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1076
1179
  item.context.length + item.images.length,
1077
1180
  " attached"
@@ -1091,7 +1194,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1091
1194
  return next;
1092
1195
  }) }),
1093
1196
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
1094
- /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
1197
+ /* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
1198
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
1199
+ event.preventDefault();
1200
+ setDragging(true);
1201
+ }
1202
+ }, onDragOver: (event) => {
1203
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
1204
+ }, onDragLeave: (event) => {
1205
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1206
+ }, onDrop: dropImages, children: [
1095
1207
  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,
1096
1208
  /* @__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) => {
1097
1209
  const value = event.currentTarget.value;
@@ -1105,19 +1217,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1105
1217
  } }),
1106
1218
  /* @__PURE__ */ jsxs3("span", { children: [
1107
1219
  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,
1108
- /* @__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 }) })
1220
+ /* @__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 }) })
1109
1221
  ] })
1110
1222
  ] })
1111
1223
  ] });
1112
1224
  }
1113
1225
 
1114
1226
  // src/conversation.jsx
1115
- import { Fragment as Fragment2 } from "preact";
1116
- import { useEffect as useEffect3, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState2 } from "preact/hooks";
1227
+ import { Fragment as Fragment3 } from "preact";
1228
+ import { useEffect as useEffect4, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState3 } from "preact/hooks";
1117
1229
 
1118
1230
  // src/markdown.jsx
1119
1231
  import MarkdownIt from "markdown-it";
1120
- import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "preact/hooks";
1232
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "preact/hooks";
1121
1233
  import { jsx as jsx4 } from "preact/jsx-runtime";
1122
1234
  var markdown = new MarkdownIt({ html: false, linkify: true, breaks: false });
1123
1235
  var LANGUAGE_LABELS = {
@@ -1179,8 +1291,8 @@ function setCopyState(button, status) {
1179
1291
  }
1180
1292
  function Markdown({ value, copyText }) {
1181
1293
  const html = useMemo(() => markdown.render(value), [value]);
1182
- const resets = useRef2(/* @__PURE__ */ new Map());
1183
- useEffect2(() => () => {
1294
+ const resets = useRef3(/* @__PURE__ */ new Map());
1295
+ useEffect3(() => () => {
1184
1296
  for (const timer of resets.current.values()) clearTimeout(timer);
1185
1297
  resets.current.clear();
1186
1298
  }, []);
@@ -1209,7 +1321,7 @@ function Markdown({ value, copyText }) {
1209
1321
  }
1210
1322
 
1211
1323
  // src/conversation.jsx
1212
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1324
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1213
1325
  function LoadingStatus({ state, compact = false }) {
1214
1326
  const copy = {
1215
1327
  connecting: ["Connecting to coding agents", "Checking installed harnesses and capabilities.", 0],
@@ -1258,9 +1370,9 @@ function ContextDisclosure({ context }) {
1258
1370
  ] });
1259
1371
  }
1260
1372
  function MessageMeta({ entry, adapter }) {
1261
- const [copyState, setCopyState2] = useState2("idle");
1262
- const reset = useRef3(null);
1263
- useEffect3(() => () => clearTimeout(reset.current), []);
1373
+ const [copyState, setCopyState2] = useState3("idle");
1374
+ const reset = useRef4(null);
1375
+ useEffect4(() => () => clearTimeout(reset.current), []);
1264
1376
  const date = entry.ts === null ? null : new Date(entry.ts);
1265
1377
  const validDate = date && Number.isFinite(date.valueOf()) ? date : null;
1266
1378
  if (!validDate && (!adapter?.copyText || !entry.text)) return null;
@@ -1281,33 +1393,33 @@ function MessageMeta({ entry, adapter }) {
1281
1393
  ] });
1282
1394
  }
1283
1395
  var TOOL_ICONS = {
1284
- read: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1396
+ read: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1285
1397
  /* @__PURE__ */ jsx5("path", { d: "M5 2.75h7.25L15 5.5v7.75A1.75 1.75 0 0 1 13.25 15h-8.5A1.75 1.75 0 0 1 3 13.25v-8.5A2 2 0 0 1 5 2.75Z" }),
1286
1398
  /* @__PURE__ */ jsx5("path", { d: "M12 2.9v3h2.85M6 9h6M6 12h4" })
1287
1399
  ] }),
1288
- search: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1400
+ search: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1289
1401
  /* @__PURE__ */ jsx5("circle", { cx: "8", cy: "8", r: "4.5" }),
1290
1402
  /* @__PURE__ */ jsx5("path", { d: "m11.5 11.5 3 3" })
1291
1403
  ] }),
1292
- edit: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1404
+ edit: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1293
1405
  /* @__PURE__ */ jsx5("path", { d: "m11.75 3.25 3 3-8.5 8.5-3.75.75.75-3.75 8.5-8.5Z" }),
1294
1406
  /* @__PURE__ */ jsx5("path", { d: "m10 5 3 3" })
1295
1407
  ] }),
1296
- command: () => /* @__PURE__ */ jsx5(Fragment3, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 3 3-3 3M8 12h6" }) }),
1297
- test: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1408
+ command: () => /* @__PURE__ */ jsx5(Fragment4, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 3 3-3 3M8 12h6" }) }),
1409
+ test: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1298
1410
  /* @__PURE__ */ jsx5("path", { d: "M6 2.5v3L3 12a2 2 0 0 0 1.8 3h8.4a2 2 0 0 0 1.8-3l-3-6.5v-3M5 9h8" }),
1299
1411
  /* @__PURE__ */ jsx5("path", { d: "M5 2.5h8" })
1300
1412
  ] }),
1301
- web: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1413
+ web: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1302
1414
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "6.5" }),
1303
1415
  /* @__PURE__ */ jsx5("path", { d: "M2.75 9h12.5M9 2.5c2 1.8 3 4 3 6.5s-1 4.7-3 6.5c-2-1.8-3-4-3-6.5s1-4.7 3-6.5Z" })
1304
1416
  ] }),
1305
- agent: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1417
+ agent: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1306
1418
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "6", r: "2.5" }),
1307
1419
  /* @__PURE__ */ jsx5("path", { d: "M4 15c.4-3 2-4.5 5-4.5s4.6 1.5 5 4.5" })
1308
1420
  ] }),
1309
- plan: () => /* @__PURE__ */ jsx5(Fragment3, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 1 1 2-2M3 9l1 1 2-2M3 13l1 1 2-2M8 5h7M8 9h7M8 13h7" }) }),
1310
- other: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1421
+ plan: () => /* @__PURE__ */ jsx5(Fragment4, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 1 1 2-2M3 9l1 1 2-2M3 13l1 1 2-2M8 5h7M8 9h7M8 13h7" }) }),
1422
+ other: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1311
1423
  /* @__PURE__ */ jsx5("path", { d: "M9 2.5v3M9 12.5v3M2.5 9h3M12.5 9h3" }),
1312
1424
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "3.5" })
1313
1425
  ] })
@@ -1373,9 +1485,9 @@ function ToolMetrics({ presentation }) {
1373
1485
  }
1374
1486
  function PendingElapsed({ now }) {
1375
1487
  const clock = now ?? Date.now;
1376
- const started = useRef3(clock());
1377
- const [elapsed, setElapsed] = useState2(0);
1378
- useEffect3(() => {
1488
+ const started = useRef4(clock());
1489
+ const [elapsed, setElapsed] = useState3(0);
1490
+ useEffect4(() => {
1379
1491
  const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
1380
1492
  return () => clearInterval(timer);
1381
1493
  }, [clock]);
@@ -1416,7 +1528,7 @@ function SearchPreview({ presentation }) {
1416
1528
  ] }) : null,
1417
1529
  lines.length ? /* @__PURE__ */ jsx5("ol", { children: lines.map((line, index) => {
1418
1530
  const match = /^(.*?):(\d+)(?::(\d+))?:(.*)$/.exec(line);
1419
- return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1531
+ return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment4, { children: [
1420
1532
  /* @__PURE__ */ jsx5("code", { children: match[1] }),
1421
1533
  /* @__PURE__ */ jsxs4("small", { children: [
1422
1534
  match[2],
@@ -1449,9 +1561,9 @@ function ToolPreview({ presentation, entry }) {
1449
1561
  return presentation.preview ? /* @__PURE__ */ jsx5("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
1450
1562
  }
1451
1563
  function ToolActions({ presentation, adapter }) {
1452
- const [copied, setCopied] = useState2(false);
1453
- const reset = useRef3(null);
1454
- useEffect3(() => () => clearTimeout(reset.current), []);
1564
+ const [copied, setCopied] = useState3(false);
1565
+ const reset = useRef4(null);
1566
+ useEffect4(() => () => clearTimeout(reset.current), []);
1455
1567
  if (!adapter?.copyText) return null;
1456
1568
  const action = presentation.command ? ["Copy command", presentation.command] : presentation.path ? ["Copy path", presentation.path] : presentation.url ? ["Copy URL", presentation.url] : presentation.query ? ["Copy query", presentation.query] : null;
1457
1569
  const copy = async () => {
@@ -1498,7 +1610,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1498
1610
  }
1499
1611
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1500
1612
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1501
- /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1613
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images, adapter }),
1502
1614
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1503
1615
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1504
1616
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1507,14 +1619,14 @@ function TranscriptEntry({ entry, state, adapter }) {
1507
1619
  }
1508
1620
  function ToolRow({ entry, workspace, open = false, adapter }) {
1509
1621
  const presentation = entry.presentation ?? createToolPresentation(entry);
1510
- const [expanded, setExpanded] = useState2(open || entry.status === "pending");
1511
- useEffect3(() => {
1622
+ const [expanded, setExpanded] = useState3(open || entry.status === "pending");
1623
+ useEffect4(() => {
1512
1624
  if (entry.status === "pending") setExpanded(true);
1513
1625
  }, [entry.status]);
1514
1626
  const target = compactToolTarget(presentation.target, workspace);
1515
1627
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
1516
1628
  const category = presentation.category ?? toolCategory(entry);
1517
- const summary = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1629
+ const summary = /* @__PURE__ */ jsxs4(Fragment4, { children: [
1518
1630
  /* @__PURE__ */ jsx5(ToolIcon, { category }),
1519
1631
  /* @__PURE__ */ jsx5("strong", { children: toolAction2(entry, category, presentation) }),
1520
1632
  target ? /* @__PURE__ */ jsx5("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
@@ -1542,9 +1654,9 @@ function ToolRow({ entry, workspace, open = false, adapter }) {
1542
1654
  function ActivityGroup({ entries, state, adapter }) {
1543
1655
  if (entries.length === 1) return /* @__PURE__ */ jsx5("section", { class: "scui-activity", "data-single": "true", children: /* @__PURE__ */ jsx5(ToolRow, { entry: entries[0], workspace: state.workspace, adapter }) });
1544
1656
  const active = entries.some((entry) => entry.status === "pending");
1545
- const [open, setOpen] = useState2(active);
1657
+ const [open, setOpen] = useState3(active);
1546
1658
  const id = useId();
1547
- useEffect3(() => {
1659
+ useEffect4(() => {
1548
1660
  if (active) setOpen(true);
1549
1661
  }, [active]);
1550
1662
  return /* @__PURE__ */ jsxs4("section", { class: "scui-activity", children: [
@@ -1620,9 +1732,9 @@ function SessionDetails({ semantics }) {
1620
1732
  }
1621
1733
  var conversationMemory = /* @__PURE__ */ new Map();
1622
1734
  function ConversationAnnouncements({ state }) {
1623
- const previousBusy = useRef3(state.busy);
1624
- const [announcement, setAnnouncement] = useState2("");
1625
- useEffect3(() => {
1735
+ const previousBusy = useRef4(state.busy);
1736
+ const [announcement, setAnnouncement] = useState3("");
1737
+ useEffect4(() => {
1626
1738
  if (previousBusy.current && !state.busy && !state.error) {
1627
1739
  setAnnouncement(`${harnessDisplayName(state.harness) || "Coding agent"} finished working`);
1628
1740
  }
@@ -1631,13 +1743,13 @@ function ConversationAnnouncements({ state }) {
1631
1743
  return /* @__PURE__ */ jsx5("span", { class: "scui-sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement });
1632
1744
  }
1633
1745
  function Conversation({ state, adapter, components = {}, slots = {}, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pending = null, unreadAfterMessages = null }) {
1634
- const scroller = useRef3(null);
1746
+ const scroller = useRef4(null);
1635
1747
  const remembered = conversationMemory.get(memoryKey) ?? { top: null, atBottom: true };
1636
- const [atBottom, setAtBottom] = useState2(remembered.atBottom);
1637
- const earlierAnchor = useRef3(null);
1638
- const restored = useRef3(false);
1748
+ const [atBottom, setAtBottom] = useState3(remembered.atBottom);
1749
+ const earlierAnchor = useRef4(null);
1750
+ const restored = useRef4(false);
1639
1751
  const blocks = groupConversation(state.transcript);
1640
- const unreadBoundary = useRef3(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1752
+ const unreadBoundary = useRef4(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1641
1753
  const unreadBlock = unreadBoundary.current === null ? -1 : blocks.findIndex((block) => {
1642
1754
  const entries = block.kind === "activity" ? block.entries : [block.entry];
1643
1755
  return entries.some((entry) => Number.isSafeInteger(entry.messageIndex) && entry.messageIndex > unreadBoundary.current);
@@ -1696,12 +1808,12 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1696
1808
  }, children: "Load earlier messages" }) : null,
1697
1809
  !blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state }) : null,
1698
1810
  !blocks.length && state.startup === "ready" ? Empty ? /* @__PURE__ */ jsx5(Empty, { state, adapter, value: null }) : /* @__PURE__ */ jsx5("div", { class: "scui-empty", children: state.error ?? (state.harness ? `${harnessDisplayName(state.harness)} is listening. Say something.` : "No transcript yet.") }) : null,
1699
- blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
1811
+ blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1700
1812
  index === unreadBlock ? /* @__PURE__ */ jsx5("div", { class: "scui-unread-divider", role: "separator", "aria-label": "New messages", children: /* @__PURE__ */ jsx5("span", { children: "New" }) }) : null,
1701
1813
  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 })
1702
1814
  ] }, block.id)),
1703
1815
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1704
- /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1816
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images, adapter }),
1705
1817
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1706
1818
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1707
1819
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1732,7 +1844,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1732
1844
  }
1733
1845
 
1734
1846
  // src/logo.jsx
1735
- import { useEffect as useEffect4 } from "preact/hooks";
1847
+ import { useEffect as useEffect5 } from "preact/hooks";
1736
1848
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1737
1849
  var LOGOS = {
1738
1850
  "claude-code": {
@@ -1783,7 +1895,7 @@ function hasHarnessLogo(id) {
1783
1895
  }
1784
1896
  function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1785
1897
  const logo = LOGOS[id];
1786
- useEffect4(() => {
1898
+ useEffect5(() => {
1787
1899
  if (!logo) onMissingLogo?.(id);
1788
1900
  }, [id, logo, onMissingLogo]);
1789
1901
  if (!logo) return null;
@@ -1794,10 +1906,10 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1794
1906
  }
1795
1907
 
1796
1908
  // src/messenger.jsx
1797
- import { useEffect as useEffect6, useId as useId2, useMemo as useMemo2, useRef as useRef5, useState as useState4 } from "preact/hooks";
1909
+ import { useEffect as useEffect7, useId as useId2, useMemo as useMemo2, useRef as useRef6, useState as useState5 } from "preact/hooks";
1798
1910
 
1799
1911
  // src/sessions.jsx
1800
- import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useRef as useRef4, useState as useState3 } from "preact/hooks";
1912
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef5, useState as useState4 } from "preact/hooks";
1801
1913
  import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1802
1914
  var sessionListMemory = /* @__PURE__ */ new Map();
1803
1915
  function SessionRow({ row, state, onOpen }) {
@@ -1819,21 +1931,21 @@ function SessionRow({ row, state, onOpen }) {
1819
1931
  }
1820
1932
  function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
1821
1933
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
1822
- const [query, setQuery] = useState3(remembered.query);
1823
- const [loadingMore, setLoadingMore] = useState3(false);
1824
- const root = useRef4(null);
1825
- const rowScroller = useRef4(null);
1934
+ const [query, setQuery] = useState4(remembered.query);
1935
+ const [loadingMore, setLoadingMore] = useState4(false);
1936
+ const root = useRef5(null);
1937
+ const rowScroller = useRef5(null);
1826
1938
  const rows = filterSessions(state.sessions, query);
1827
1939
  const Row = components.SessionRow ?? SessionRow;
1828
1940
  useLayoutEffect3(() => {
1829
1941
  if (rowScroller.current) rowScroller.current.scrollTop = remembered.top;
1830
1942
  }, [memoryKey]);
1831
- useEffect5(() => {
1943
+ useEffect6(() => {
1832
1944
  if (!focusKey || !root.current) return;
1833
1945
  const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
1834
1946
  (target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
1835
1947
  }, [focusKey, rows.length]);
1836
- useEffect5(() => {
1948
+ useEffect6(() => {
1837
1949
  if (loadingMore) setLoadingMore(false);
1838
1950
  }, [state.error, state.history.hasMoreSessions, state.sessions.length]);
1839
1951
  const loadMore = () => {
@@ -1919,10 +2031,10 @@ function Receipt({ state, adapter }) {
1919
2031
  return null;
1920
2032
  }
1921
2033
  function ConversationActions({ state, adapter, actionPending }) {
1922
- const [open, setOpen] = useState4(false);
1923
- const root = useRef5(null);
1924
- const panel = useRef5(null);
1925
- const trigger = useRef5(null);
2034
+ const [open, setOpen] = useState5(false);
2035
+ const root = useRef6(null);
2036
+ const panel = useRef6(null);
2037
+ const trigger = useRef6(null);
1926
2038
  const menuId = useId2();
1927
2039
  const targets = state.harnesses.filter((item) => item.startable);
1928
2040
  const groups = [
@@ -1943,7 +2055,7 @@ function ConversationActions({ state, adapter, actionPending }) {
1943
2055
  items: state.canBranch ? targets.map((target) => ({ key: `branch:${target.id}`, label: target.id === state.harness ? `Fork in ${target.label}` : `Continue with ${target.label}`, intent: { action: "branch", targetHarness: target.id } })) : []
1944
2056
  }
1945
2057
  ].filter((group) => group.items.length);
1946
- useEffect6(() => {
2058
+ useEffect7(() => {
1947
2059
  if (!open) return;
1948
2060
  panel.current?.querySelector("button:not(:disabled)")?.focus({ preventScroll: true });
1949
2061
  const dismiss = (event) => {
@@ -1993,12 +2105,12 @@ function ConversationActions({ state, adapter, actionPending }) {
1993
2105
  ] });
1994
2106
  }
1995
2107
  function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose }) {
1996
- const back = useRef5(null);
2108
+ const back = useRef6(null);
1997
2109
  const harness = state.attached?.harness ?? state.harness;
1998
2110
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
1999
2111
  const status = state.needsInput ? "Needs input" : pendingStatus === "failed" ? "Send failed" : state.busy ? "Working" : pendingStatus === "sending" ? "Sending" : pendingStatus === "editing" ? "Editing message" : state.mode === "mirror" ? "Read-only" : "Ready";
2000
2112
  const menu = state.canDetach || state.canOpenTerminal || state.canBranch || state.canAttach || state.canExport || state.canReduce;
2001
- useEffect6(() => {
2113
+ useEffect7(() => {
2002
2114
  if (state.mode === "mirror" && !state.canSend) back.current?.focus({ preventScroll: true });
2003
2115
  }, []);
2004
2116
  return /* @__PURE__ */ jsxs7("header", { class: "scui-head scui-chat-head", children: [
@@ -2020,24 +2132,24 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2020
2132
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2021
2133
  const Header = slots.header;
2022
2134
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2023
- const [pending, setPendingState] = useState4(() => pendingMessageMemory.get(memoryKey) ?? null);
2024
- const [pendingAction, setPendingAction] = useState4(null);
2025
- const [restoreDraft, setRestoreDraft] = useState4(null);
2026
- const restoreSequence = useRef5(0);
2027
- const actionSequence = useRef5(0);
2028
- const acknowledged = useRef5(/* @__PURE__ */ new Set());
2135
+ const [pending, setPendingState] = useState5(() => pendingMessageMemory.get(memoryKey) ?? null);
2136
+ const [pendingAction, setPendingAction] = useState5(null);
2137
+ const [restoreDraft, setRestoreDraft] = useState5(null);
2138
+ const restoreSequence = useRef6(0);
2139
+ const actionSequence = useRef6(0);
2140
+ const acknowledged = useRef6(/* @__PURE__ */ new Set());
2029
2141
  const setPending = (update) => setPendingState((current) => {
2030
2142
  const next = typeof update === "function" ? update(current) : update;
2031
2143
  if (next) boundedSet(pendingMessageMemory, memoryKey, next);
2032
2144
  else pendingMessageMemory.delete(memoryKey);
2033
2145
  return next;
2034
2146
  });
2035
- useEffect6(() => {
2147
+ useEffect7(() => {
2036
2148
  setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
2037
2149
  setPendingAction(null);
2038
2150
  setRestoreDraft(null);
2039
2151
  }, [memoryKey]);
2040
- useEffect6(() => {
2152
+ useEffect7(() => {
2041
2153
  if (!pending) return;
2042
2154
  if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
2043
2155
  setPending(null);
@@ -2066,7 +2178,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2066
2178
  setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
2067
2179
  setPending({ ...pending, status: "editing" });
2068
2180
  };
2069
- useEffect6(() => {
2181
+ useEffect7(() => {
2070
2182
  const key = state.attached?.key;
2071
2183
  if (!key || !state.attention.some((item) => item.key === key)) {
2072
2184
  if (key) acknowledged.current.delete(key);
@@ -2101,7 +2213,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2101
2213
  return result;
2102
2214
  }
2103
2215
  }), [adapter, state.error]);
2104
- useEffect6(() => {
2216
+ useEffect7(() => {
2105
2217
  if (!pendingAction) return;
2106
2218
  if (state.operation && !pendingAction.seenOperation) {
2107
2219
  setPendingAction({ ...pendingAction, seenOperation: true });
@@ -2134,23 +2246,24 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2134
2246
  const startable = state.harnesses.filter((item) => item.startable);
2135
2247
  const startableKey = startable.map((item) => item.id).join("\0");
2136
2248
  const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2137
- const [harness, setHarness] = useState4(remembered.harness);
2138
- const [draft, setDraft] = useState4(remembered.draft);
2139
- const [context, setContext] = useState4(remembered.context);
2140
- const [images, setImages] = useState4(remembered.images ?? []);
2141
- const [starting, setStarting] = useState4(null);
2142
- const [picking, setPicking] = useState4(false);
2143
- const [pickerError, setPickerError] = useState4(null);
2144
- const startSequence = useRef5(0);
2145
- const textarea = useRef5(null);
2249
+ const [harness, setHarness] = useState5(remembered.harness);
2250
+ const [draft, setDraft] = useState5(remembered.draft);
2251
+ const [context, setContext] = useState5(remembered.context);
2252
+ const [images, setImages] = useState5(remembered.images ?? []);
2253
+ const [starting, setStarting] = useState5(null);
2254
+ const [picking, setPicking] = useState5(false);
2255
+ const [dragging, setDragging] = useState5(false);
2256
+ const [pickerError, setPickerError] = useState5(null);
2257
+ const startSequence = useRef6(0);
2258
+ const textarea = useRef6(null);
2146
2259
  useAutosizeTextarea(textarea, draft);
2147
- useEffect6(() => {
2260
+ useEffect7(() => {
2148
2261
  if (startable.some((item) => item.id === harness)) return;
2149
2262
  const next = startable[0]?.id ?? "";
2150
2263
  setHarness(next);
2151
2264
  boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2152
2265
  }, [harness, startableKey]);
2153
- useEffect6(() => {
2266
+ useEffect7(() => {
2154
2267
  if (!starting) return;
2155
2268
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2156
2269
  const beganWorking = !starting.busy && state.busy;
@@ -2158,10 +2271,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2158
2271
  newChatMemory.delete(memoryKey);
2159
2272
  onStarted();
2160
2273
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2161
- useEffect6(() => {
2274
+ useEffect7(() => {
2162
2275
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
2163
2276
  }, [starting, state.busy, state.error, state.operation]);
2164
- useEffect6(() => {
2277
+ useEffect7(() => {
2165
2278
  textarea.current?.focus({ preventScroll: true });
2166
2279
  }, []);
2167
2280
  const pickContext = () => {
@@ -2184,13 +2297,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2184
2297
  });
2185
2298
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2186
2299
  };
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();
2300
+ const addImageFiles = (value, source) => {
2301
+ const allFiles = Array.from(value ?? []);
2302
+ if (!allFiles.length) return false;
2303
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
2304
+ if (files.length !== allFiles.length) {
2305
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
2306
+ return true;
2307
+ }
2191
2308
  if (images.length + files.length > MAX_IMAGE_ITEMS) {
2192
2309
  setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2193
- return;
2310
+ return true;
2194
2311
  }
2195
2312
  setPicking(true);
2196
2313
  setPickerError(null);
@@ -2198,11 +2315,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2198
2315
  const next = mergeImages(current, picked);
2199
2316
  boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2200
2317
  return next;
2201
- }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
2318
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2319
+ return true;
2320
+ };
2321
+ const pasteImages = (event) => {
2322
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
2323
+ };
2324
+ const dropImages = (event) => {
2325
+ setDragging(false);
2326
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
2202
2327
  };
2203
2328
  const send = () => {
2204
2329
  const text = draft.trim();
2205
- if (!text || !harness || starting) return;
2330
+ if (!text && !images.length || !harness || starting) return;
2206
2331
  const id = startSequence.current + 1;
2207
2332
  startSequence.current = id;
2208
2333
  setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
@@ -2253,7 +2378,16 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2253
2378
  return next;
2254
2379
  }) }),
2255
2380
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2256
- /* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
2381
+ /* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2382
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2383
+ event.preventDefault();
2384
+ setDragging(true);
2385
+ }
2386
+ }, onDragOver: (event) => {
2387
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
2388
+ }, onDragLeave: (event) => {
2389
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2390
+ }, onDrop: dropImages, children: [
2257
2391
  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
2392
  /* @__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) => {
2259
2393
  const value = event.currentTarget.value;
@@ -2265,7 +2399,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2265
2399
  send();
2266
2400
  }
2267
2401
  } }),
2268
- /* @__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 }) }) })
2402
+ /* @__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 }) }) })
2269
2403
  ] })
2270
2404
  ] })
2271
2405
  ] });
@@ -2274,14 +2408,14 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2274
2408
  const state = useMemo2(() => normalizeUiState(stateInput), [stateInput]);
2275
2409
  const copy = { ...DEFAULT_LABELS, ...labels };
2276
2410
  const memoryKey = state.workspace || "@default";
2277
- const [view, setViewState] = useState4(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
2278
- const [opening, setOpening] = useState4(null);
2279
- const [listFocus, setListFocus] = useState4(null);
2411
+ const [view, setViewState] = useState5(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
2412
+ const [opening, setOpening] = useState5(null);
2413
+ const [listFocus, setListFocus] = useState5(null);
2280
2414
  const setView = (next) => {
2281
2415
  boundedSet(messengerViewMemory, memoryKey, next);
2282
2416
  setViewState(next);
2283
2417
  };
2284
- useEffect6(() => {
2418
+ useEffect7(() => {
2285
2419
  if (!opening) return;
2286
2420
  if (state.attached?.key === opening.key) {
2287
2421
  setOpening(null);
@@ -2330,7 +2464,9 @@ export {
2330
2464
  ContinuationBar,
2331
2465
  Conversation,
2332
2466
  HarnessLogo,
2467
+ ImageViewer,
2333
2468
  LoadingStatus,
2469
+ MessageImages,
2334
2470
  RequestCard,
2335
2471
  SessionDetails,
2336
2472
  SessionList,