@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/messenger.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/messenger.jsx
2
- import { useEffect as useEffect6, useId as useId2, useMemo as useMemo2, useRef as useRef5, useState as useState4 } from "preact/hooks";
2
+ import { useEffect as useEffect7, useId as useId2, useMemo as useMemo2, useRef as useRef6, useState as useState5 } from "preact/hooks";
3
3
 
4
4
  // core.mjs
5
5
  var HARNESS_NAMES = Object.freeze({
@@ -758,7 +758,7 @@ function isSendKey(event) {
758
758
  }
759
759
 
760
760
  // src/composer.jsx
761
- import { useEffect, useRef, useState } from "preact/hooks";
761
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "preact/hooks";
762
762
 
763
763
  // src/memory.js
764
764
  var MEMORY_LIMIT = 100;
@@ -818,7 +818,8 @@ function UiIcon({ name, size = 16, class: className = "" }) {
818
818
  }
819
819
 
820
820
  // src/context.jsx
821
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
821
+ import { useEffect, useRef, useState } from "preact/hooks";
822
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
822
823
  var MAX_CONTEXT_ITEMS = 32;
823
824
  var MAX_IMAGE_ITEMS = 4;
824
825
  var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
@@ -917,12 +918,101 @@ function ImageTray({ items, onRemove }) {
917
918
  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
919
  ] }, item.id ?? `${item.label}:${index}`)) });
919
920
  }
920
- function MessageImages({ items }) {
921
+ function imageFilename(label) {
922
+ const value = label.trim().replace(/[\\/:*?"<>|]+/g, "-");
923
+ return value || "image";
924
+ }
925
+ function ImageViewer({ items, index, adapter, onChange, onClose }) {
926
+ const dialog = useRef(null);
927
+ const reset = useRef(null);
928
+ const [copyState, setCopyState2] = useState("idle");
929
+ const item = items[index];
930
+ const remote = item?.url?.startsWith("http://") || item?.url?.startsWith("https://");
931
+ useEffect(() => {
932
+ if (!dialog.current?.open) dialog.current?.showModal();
933
+ return () => clearTimeout(reset.current);
934
+ }, []);
935
+ useEffect(() => {
936
+ clearTimeout(reset.current);
937
+ setCopyState2("idle");
938
+ }, [index]);
939
+ if (!item?.url) return null;
940
+ const move = (amount) => onChange((index + amount + items.length) % items.length);
941
+ const copy = async () => {
942
+ try {
943
+ await adapter.copyText(item.url);
944
+ setCopyState2("copied");
945
+ } catch {
946
+ setCopyState2("failed");
947
+ }
948
+ clearTimeout(reset.current);
949
+ reset.current = setTimeout(() => setCopyState2("idle"), 1500);
950
+ };
951
+ const close = () => dialog.current?.close();
952
+ return /* @__PURE__ */ jsxs2(
953
+ "dialog",
954
+ {
955
+ ref: dialog,
956
+ class: "scui-image-viewer",
957
+ "aria-label": `Image preview: ${item.label}`,
958
+ onClose,
959
+ onClick: (event) => {
960
+ if (event.target === event.currentTarget) close();
961
+ },
962
+ onKeyDown: (event) => {
963
+ if (items.length < 2 || !["ArrowLeft", "ArrowRight"].includes(event.key)) return;
964
+ event.preventDefault();
965
+ move(event.key === "ArrowLeft" ? -1 : 1);
966
+ },
967
+ children: [
968
+ /* @__PURE__ */ jsxs2("header", { children: [
969
+ /* @__PURE__ */ jsxs2("span", { children: [
970
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
971
+ items.length > 1 ? /* @__PURE__ */ jsxs2("small", { children: [
972
+ index + 1,
973
+ " of ",
974
+ items.length
975
+ ] }) : null
976
+ ] }),
977
+ /* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
978
+ 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,
979
+ 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 }) }),
980
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 17 }) })
981
+ ] })
982
+ ] }),
983
+ /* @__PURE__ */ jsxs2("figure", { children: [
984
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: item.label }),
985
+ items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
986
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) }),
987
+ /* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) })
988
+ ] }) : null
989
+ ] })
990
+ ]
991
+ }
992
+ );
993
+ }
994
+ function MessageImages({ items, adapter }) {
995
+ const [active, setActive] = useState(null);
996
+ const opener = useRef(null);
921
997
  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}`)) });
998
+ const viewable = items.filter((item) => item.url);
999
+ const close = () => {
1000
+ setActive(null);
1001
+ requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
1002
+ };
1003
+ return /* @__PURE__ */ jsxs2(Fragment2, { children: [
1004
+ /* @__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) => {
1005
+ opener.current = event.currentTarget;
1006
+ setActive(viewable.indexOf(item));
1007
+ }, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
1008
+ /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
1009
+ /* @__PURE__ */ jsxs2("span", { children: [
1010
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
1011
+ /* @__PURE__ */ jsx2("small", { children: "Preview unavailable" })
1012
+ ] })
1013
+ ] }, item.id ?? `${item.label}:${index}`)) }),
1014
+ active !== null && viewable[active] ? /* @__PURE__ */ jsx2(ImageViewer, { items: viewable, index: active, adapter, onChange: setActive, onClose: close }) : null
1015
+ ] });
926
1016
  }
927
1017
 
928
1018
  // src/textarea.js
@@ -964,17 +1054,18 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
964
1054
  }
965
1055
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
966
1056
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
967
- const [draft, setDraft] = useState(remembered.draft);
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 ?? [] })));
971
- const [dispatching, setDispatching] = useState(false);
972
- const [picking, setPicking] = useState(false);
973
- const [pickerError, setPickerError] = useState(null);
974
- const textarea = useRef(null);
1057
+ const [draft, setDraft] = useState2(remembered.draft);
1058
+ const [context, setContext] = useState2(remembered.context ?? []);
1059
+ const [images, setImages] = useState2(remembered.images ?? []);
1060
+ const [queue, setQueue] = useState2((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
1061
+ const [dispatching, setDispatching] = useState2(false);
1062
+ const [picking, setPicking] = useState2(false);
1063
+ const [dragging, setDragging] = useState2(false);
1064
+ const [pickerError, setPickerError] = useState2(null);
1065
+ const textarea = useRef2(null);
975
1066
  useAutosizeTextarea(textarea, draft);
976
1067
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
977
- useEffect(() => {
1068
+ useEffect2(() => {
978
1069
  remember(draft, context, images, queue);
979
1070
  }, [draft, context, images, memoryKey, queue]);
980
1071
  const updateQueue = (update) => setQueue((items) => {
@@ -984,7 +1075,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
984
1075
  });
985
1076
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
986
1077
  const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
987
- useEffect(() => {
1078
+ useEffect2(() => {
988
1079
  if (!queueBlocked && state.canSend && queue.length) {
989
1080
  const [next, ...rest] = queue;
990
1081
  setDispatching(true);
@@ -994,13 +1085,13 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
994
1085
  adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
995
1086
  }
996
1087
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
997
- useEffect(() => {
1088
+ useEffect2(() => {
998
1089
  if (pendingStatus !== null || state.busy) setDispatching(false);
999
1090
  }, [pendingStatus, state.busy]);
1000
- useEffect(() => {
1091
+ useEffect2(() => {
1001
1092
  textarea.current?.focus({ preventScroll: true });
1002
1093
  }, [memoryKey]);
1003
- useEffect(() => {
1094
+ useEffect2(() => {
1004
1095
  if (!restoreDraft) return;
1005
1096
  setDraft(restoreDraft.text);
1006
1097
  const restoredContext = normalizeContext(restoreDraft.context);
@@ -1011,7 +1102,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1011
1102
  textarea.current?.focus({ preventScroll: true });
1012
1103
  onDraftRestored?.(restoreDraft.id);
1013
1104
  }, [onDraftRestored, restoreDraft?.id]);
1014
- useEffect(() => {
1105
+ useEffect2(() => {
1015
1106
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
1016
1107
  return () => clearTimeout(timer);
1017
1108
  }, [adapter, draft]);
@@ -1035,13 +1126,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1035
1126
  });
1036
1127
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
1037
1128
  };
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();
1129
+ const addImageFiles = (value, source) => {
1130
+ const allFiles = Array.from(value ?? []);
1131
+ if (!allFiles.length) return false;
1132
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
1133
+ if (files.length !== allFiles.length) {
1134
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
1135
+ return true;
1136
+ }
1042
1137
  if (images.length + files.length > MAX_IMAGE_ITEMS) {
1043
1138
  setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1044
- return;
1139
+ return true;
1045
1140
  }
1046
1141
  setPicking(true);
1047
1142
  setPickerError(null);
@@ -1049,11 +1144,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1049
1144
  const next = mergeImages(current, picked);
1050
1145
  remember(draft, context, next, queue);
1051
1146
  return next;
1052
- }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
1147
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
1148
+ return true;
1149
+ };
1150
+ const pasteImages = (event) => {
1151
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
1152
+ };
1153
+ const dropImages = (event) => {
1154
+ setDragging(false);
1155
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
1053
1156
  };
1054
1157
  const send = () => {
1055
1158
  const text = draft.trim();
1056
- if (!text) return;
1159
+ if (!text && !images.length) return;
1057
1160
  const message = { text, context, images };
1058
1161
  if (queuesNewMessage) updateQueue((items) => [...items, message]);
1059
1162
  else if (state.canSend) {
@@ -1074,7 +1177,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1074
1177
  ] }),
1075
1178
  queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
1076
1179
  /* @__PURE__ */ jsxs3("span", { children: [
1077
- item.text,
1180
+ item.text || "Image attachment",
1078
1181
  item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
1079
1182
  item.context.length + item.images.length,
1080
1183
  " attached"
@@ -1094,7 +1197,16 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1094
1197
  return next;
1095
1198
  }) }),
1096
1199
  pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
1097
- /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
1200
+ /* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
1201
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
1202
+ event.preventDefault();
1203
+ setDragging(true);
1204
+ }
1205
+ }, onDragOver: (event) => {
1206
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
1207
+ }, onDragLeave: (event) => {
1208
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
1209
+ }, onDrop: dropImages, children: [
1098
1210
  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
1211
  /* @__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) => {
1100
1212
  const value = event.currentTarget.value;
@@ -1108,19 +1220,19 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1108
1220
  } }),
1109
1221
  /* @__PURE__ */ jsxs3("span", { children: [
1110
1222
  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,
1111
- /* @__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 }) })
1223
+ /* @__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 }) })
1112
1224
  ] })
1113
1225
  ] })
1114
1226
  ] });
1115
1227
  }
1116
1228
 
1117
1229
  // src/conversation.jsx
1118
- import { Fragment as Fragment2 } from "preact";
1119
- import { useEffect as useEffect3, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef3, useState as useState2 } from "preact/hooks";
1230
+ import { Fragment as Fragment3 } from "preact";
1231
+ import { useEffect as useEffect4, useId, useLayoutEffect as useLayoutEffect2, useRef as useRef4, useState as useState3 } from "preact/hooks";
1120
1232
 
1121
1233
  // src/markdown.jsx
1122
1234
  import MarkdownIt from "markdown-it";
1123
- import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "preact/hooks";
1235
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "preact/hooks";
1124
1236
  import { jsx as jsx4 } from "preact/jsx-runtime";
1125
1237
  var markdown = new MarkdownIt({ html: false, linkify: true, breaks: false });
1126
1238
  var LANGUAGE_LABELS = {
@@ -1182,8 +1294,8 @@ function setCopyState(button, status) {
1182
1294
  }
1183
1295
  function Markdown({ value, copyText }) {
1184
1296
  const html = useMemo(() => markdown.render(value), [value]);
1185
- const resets = useRef2(/* @__PURE__ */ new Map());
1186
- useEffect2(() => () => {
1297
+ const resets = useRef3(/* @__PURE__ */ new Map());
1298
+ useEffect3(() => () => {
1187
1299
  for (const timer of resets.current.values()) clearTimeout(timer);
1188
1300
  resets.current.clear();
1189
1301
  }, []);
@@ -1212,7 +1324,7 @@ function Markdown({ value, copyText }) {
1212
1324
  }
1213
1325
 
1214
1326
  // src/conversation.jsx
1215
- import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1327
+ import { Fragment as Fragment4, jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
1216
1328
  function LoadingStatus({ state, compact = false }) {
1217
1329
  const copy = {
1218
1330
  connecting: ["Connecting to coding agents", "Checking installed harnesses and capabilities.", 0],
@@ -1261,9 +1373,9 @@ function ContextDisclosure({ context }) {
1261
1373
  ] });
1262
1374
  }
1263
1375
  function MessageMeta({ entry, adapter }) {
1264
- const [copyState, setCopyState2] = useState2("idle");
1265
- const reset = useRef3(null);
1266
- useEffect3(() => () => clearTimeout(reset.current), []);
1376
+ const [copyState, setCopyState2] = useState3("idle");
1377
+ const reset = useRef4(null);
1378
+ useEffect4(() => () => clearTimeout(reset.current), []);
1267
1379
  const date = entry.ts === null ? null : new Date(entry.ts);
1268
1380
  const validDate = date && Number.isFinite(date.valueOf()) ? date : null;
1269
1381
  if (!validDate && (!adapter?.copyText || !entry.text)) return null;
@@ -1284,33 +1396,33 @@ function MessageMeta({ entry, adapter }) {
1284
1396
  ] });
1285
1397
  }
1286
1398
  var TOOL_ICONS = {
1287
- read: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1399
+ read: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1288
1400
  /* @__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" }),
1289
1401
  /* @__PURE__ */ jsx5("path", { d: "M12 2.9v3h2.85M6 9h6M6 12h4" })
1290
1402
  ] }),
1291
- search: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1403
+ search: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1292
1404
  /* @__PURE__ */ jsx5("circle", { cx: "8", cy: "8", r: "4.5" }),
1293
1405
  /* @__PURE__ */ jsx5("path", { d: "m11.5 11.5 3 3" })
1294
1406
  ] }),
1295
- edit: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1407
+ edit: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1296
1408
  /* @__PURE__ */ jsx5("path", { d: "m11.75 3.25 3 3-8.5 8.5-3.75.75.75-3.75 8.5-8.5Z" }),
1297
1409
  /* @__PURE__ */ jsx5("path", { d: "m10 5 3 3" })
1298
1410
  ] }),
1299
- command: () => /* @__PURE__ */ jsx5(Fragment3, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 3 3-3 3M8 12h6" }) }),
1300
- test: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1411
+ command: () => /* @__PURE__ */ jsx5(Fragment4, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 3 3-3 3M8 12h6" }) }),
1412
+ test: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1301
1413
  /* @__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" }),
1302
1414
  /* @__PURE__ */ jsx5("path", { d: "M5 2.5h8" })
1303
1415
  ] }),
1304
- web: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1416
+ web: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1305
1417
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "6.5" }),
1306
1418
  /* @__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" })
1307
1419
  ] }),
1308
- agent: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1420
+ agent: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1309
1421
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "6", r: "2.5" }),
1310
1422
  /* @__PURE__ */ jsx5("path", { d: "M4 15c.4-3 2-4.5 5-4.5s4.6 1.5 5 4.5" })
1311
1423
  ] }),
1312
- 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" }) }),
1313
- other: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1424
+ 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" }) }),
1425
+ other: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1314
1426
  /* @__PURE__ */ jsx5("path", { d: "M9 2.5v3M9 12.5v3M2.5 9h3M12.5 9h3" }),
1315
1427
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "3.5" })
1316
1428
  ] })
@@ -1376,9 +1488,9 @@ function ToolMetrics({ presentation }) {
1376
1488
  }
1377
1489
  function PendingElapsed({ now }) {
1378
1490
  const clock = now ?? Date.now;
1379
- const started = useRef3(clock());
1380
- const [elapsed, setElapsed] = useState2(0);
1381
- useEffect3(() => {
1491
+ const started = useRef4(clock());
1492
+ const [elapsed, setElapsed] = useState3(0);
1493
+ useEffect4(() => {
1382
1494
  const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
1383
1495
  return () => clearInterval(timer);
1384
1496
  }, [clock]);
@@ -1419,7 +1531,7 @@ function SearchPreview({ presentation }) {
1419
1531
  ] }) : null,
1420
1532
  lines.length ? /* @__PURE__ */ jsx5("ol", { children: lines.map((line, index) => {
1421
1533
  const match = /^(.*?):(\d+)(?::(\d+))?:(.*)$/.exec(line);
1422
- return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1534
+ return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment4, { children: [
1423
1535
  /* @__PURE__ */ jsx5("code", { children: match[1] }),
1424
1536
  /* @__PURE__ */ jsxs4("small", { children: [
1425
1537
  match[2],
@@ -1452,9 +1564,9 @@ function ToolPreview({ presentation, entry }) {
1452
1564
  return presentation.preview ? /* @__PURE__ */ jsx5("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
1453
1565
  }
1454
1566
  function ToolActions({ presentation, adapter }) {
1455
- const [copied, setCopied] = useState2(false);
1456
- const reset = useRef3(null);
1457
- useEffect3(() => () => clearTimeout(reset.current), []);
1567
+ const [copied, setCopied] = useState3(false);
1568
+ const reset = useRef4(null);
1569
+ useEffect4(() => () => clearTimeout(reset.current), []);
1458
1570
  if (!adapter?.copyText) return null;
1459
1571
  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;
1460
1572
  const copy = async () => {
@@ -1501,7 +1613,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1501
1613
  }
1502
1614
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1503
1615
  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 }),
1616
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images, adapter }),
1505
1617
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1506
1618
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1507
1619
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1510,14 +1622,14 @@ function TranscriptEntry({ entry, state, adapter }) {
1510
1622
  }
1511
1623
  function ToolRow({ entry, workspace, open = false, adapter }) {
1512
1624
  const presentation = entry.presentation ?? createToolPresentation(entry);
1513
- const [expanded, setExpanded] = useState2(open || entry.status === "pending");
1514
- useEffect3(() => {
1625
+ const [expanded, setExpanded] = useState3(open || entry.status === "pending");
1626
+ useEffect4(() => {
1515
1627
  if (entry.status === "pending") setExpanded(true);
1516
1628
  }, [entry.status]);
1517
1629
  const target = compactToolTarget(presentation.target, workspace);
1518
1630
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
1519
1631
  const category = presentation.category ?? toolCategory(entry);
1520
- const summary = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1632
+ const summary = /* @__PURE__ */ jsxs4(Fragment4, { children: [
1521
1633
  /* @__PURE__ */ jsx5(ToolIcon, { category }),
1522
1634
  /* @__PURE__ */ jsx5("strong", { children: toolAction2(entry, category, presentation) }),
1523
1635
  target ? /* @__PURE__ */ jsx5("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
@@ -1545,9 +1657,9 @@ function ToolRow({ entry, workspace, open = false, adapter }) {
1545
1657
  function ActivityGroup({ entries, state, adapter }) {
1546
1658
  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 }) });
1547
1659
  const active = entries.some((entry) => entry.status === "pending");
1548
- const [open, setOpen] = useState2(active);
1660
+ const [open, setOpen] = useState3(active);
1549
1661
  const id = useId();
1550
- useEffect3(() => {
1662
+ useEffect4(() => {
1551
1663
  if (active) setOpen(true);
1552
1664
  }, [active]);
1553
1665
  return /* @__PURE__ */ jsxs4("section", { class: "scui-activity", children: [
@@ -1623,9 +1735,9 @@ function SessionDetails({ semantics }) {
1623
1735
  }
1624
1736
  var conversationMemory = /* @__PURE__ */ new Map();
1625
1737
  function ConversationAnnouncements({ state }) {
1626
- const previousBusy = useRef3(state.busy);
1627
- const [announcement, setAnnouncement] = useState2("");
1628
- useEffect3(() => {
1738
+ const previousBusy = useRef4(state.busy);
1739
+ const [announcement, setAnnouncement] = useState3("");
1740
+ useEffect4(() => {
1629
1741
  if (previousBusy.current && !state.busy && !state.error) {
1630
1742
  setAnnouncement(`${harnessDisplayName(state.harness) || "Coding agent"} finished working`);
1631
1743
  }
@@ -1634,13 +1746,13 @@ function ConversationAnnouncements({ state }) {
1634
1746
  return /* @__PURE__ */ jsx5("span", { class: "scui-sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement });
1635
1747
  }
1636
1748
  function Conversation({ state, adapter, components = {}, slots = {}, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pending = null, unreadAfterMessages = null }) {
1637
- const scroller = useRef3(null);
1749
+ const scroller = useRef4(null);
1638
1750
  const remembered = conversationMemory.get(memoryKey) ?? { top: null, atBottom: true };
1639
- const [atBottom, setAtBottom] = useState2(remembered.atBottom);
1640
- const earlierAnchor = useRef3(null);
1641
- const restored = useRef3(false);
1751
+ const [atBottom, setAtBottom] = useState3(remembered.atBottom);
1752
+ const earlierAnchor = useRef4(null);
1753
+ const restored = useRef4(false);
1642
1754
  const blocks = groupConversation(state.transcript);
1643
- const unreadBoundary = useRef3(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1755
+ const unreadBoundary = useRef4(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1644
1756
  const unreadBlock = unreadBoundary.current === null ? -1 : blocks.findIndex((block) => {
1645
1757
  const entries = block.kind === "activity" ? block.entries : [block.entry];
1646
1758
  return entries.some((entry) => Number.isSafeInteger(entry.messageIndex) && entry.messageIndex > unreadBoundary.current);
@@ -1699,12 +1811,12 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1699
1811
  }, children: "Load earlier messages" }) : null,
1700
1812
  !blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state }) : null,
1701
1813
  !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,
1702
- blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
1814
+ blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1703
1815
  index === unreadBlock ? /* @__PURE__ */ jsx5("div", { class: "scui-unread-divider", role: "separator", "aria-label": "New messages", children: /* @__PURE__ */ jsx5("span", { children: "New" }) }) : null,
1704
1816
  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 })
1705
1817
  ] }, block.id)),
1706
1818
  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 }),
1819
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images, adapter }),
1708
1820
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1709
1821
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1710
1822
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1735,7 +1847,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1735
1847
  }
1736
1848
 
1737
1849
  // src/logo.jsx
1738
- import { useEffect as useEffect4 } from "preact/hooks";
1850
+ import { useEffect as useEffect5 } from "preact/hooks";
1739
1851
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1740
1852
  var LOGOS = {
1741
1853
  "claude-code": {
@@ -1783,7 +1895,7 @@ var LOGOS = {
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,7 +1906,7 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1794
1906
  }
1795
1907
 
1796
1908
  // src/sessions.jsx
1797
- import { useEffect as useEffect5, useLayoutEffect as useLayoutEffect3, useRef as useRef4, useState as useState3 } from "preact/hooks";
1909
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef5, useState as useState4 } from "preact/hooks";
1798
1910
  import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1799
1911
  var sessionListMemory = /* @__PURE__ */ new Map();
1800
1912
  function SessionRow({ row, state, onOpen }) {
@@ -1816,21 +1928,21 @@ function SessionRow({ row, state, onOpen }) {
1816
1928
  }
1817
1929
  function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
1818
1930
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
1819
- const [query, setQuery] = useState3(remembered.query);
1820
- const [loadingMore, setLoadingMore] = useState3(false);
1821
- const root = useRef4(null);
1822
- const rowScroller = useRef4(null);
1931
+ const [query, setQuery] = useState4(remembered.query);
1932
+ const [loadingMore, setLoadingMore] = useState4(false);
1933
+ const root = useRef5(null);
1934
+ const rowScroller = useRef5(null);
1823
1935
  const rows = filterSessions(state.sessions, query);
1824
1936
  const Row = components.SessionRow ?? SessionRow;
1825
1937
  useLayoutEffect3(() => {
1826
1938
  if (rowScroller.current) rowScroller.current.scrollTop = remembered.top;
1827
1939
  }, [memoryKey]);
1828
- useEffect5(() => {
1940
+ useEffect6(() => {
1829
1941
  if (!focusKey || !root.current) return;
1830
1942
  const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
1831
1943
  (target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
1832
1944
  }, [focusKey, rows.length]);
1833
- useEffect5(() => {
1945
+ useEffect6(() => {
1834
1946
  if (loadingMore) setLoadingMore(false);
1835
1947
  }, [state.error, state.history.hasMoreSessions, state.sessions.length]);
1836
1948
  const loadMore = () => {
@@ -1916,10 +2028,10 @@ function Receipt({ state, adapter }) {
1916
2028
  return null;
1917
2029
  }
1918
2030
  function ConversationActions({ state, adapter, actionPending }) {
1919
- const [open, setOpen] = useState4(false);
1920
- const root = useRef5(null);
1921
- const panel = useRef5(null);
1922
- const trigger = useRef5(null);
2031
+ const [open, setOpen] = useState5(false);
2032
+ const root = useRef6(null);
2033
+ const panel = useRef6(null);
2034
+ const trigger = useRef6(null);
1923
2035
  const menuId = useId2();
1924
2036
  const targets = state.harnesses.filter((item) => item.startable);
1925
2037
  const groups = [
@@ -1940,7 +2052,7 @@ function ConversationActions({ state, adapter, actionPending }) {
1940
2052
  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 } })) : []
1941
2053
  }
1942
2054
  ].filter((group) => group.items.length);
1943
- useEffect6(() => {
2055
+ useEffect7(() => {
1944
2056
  if (!open) return;
1945
2057
  panel.current?.querySelector("button:not(:disabled)")?.focus({ preventScroll: true });
1946
2058
  const dismiss = (event) => {
@@ -1990,12 +2102,12 @@ function ConversationActions({ state, adapter, actionPending }) {
1990
2102
  ] });
1991
2103
  }
1992
2104
  function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose }) {
1993
- const back = useRef5(null);
2105
+ const back = useRef6(null);
1994
2106
  const harness = state.attached?.harness ?? state.harness;
1995
2107
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
1996
2108
  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";
1997
2109
  const menu = state.canDetach || state.canOpenTerminal || state.canBranch || state.canAttach || state.canExport || state.canReduce;
1998
- useEffect6(() => {
2110
+ useEffect7(() => {
1999
2111
  if (state.mode === "mirror" && !state.canSend) back.current?.focus({ preventScroll: true });
2000
2112
  }, []);
2001
2113
  return /* @__PURE__ */ jsxs7("header", { class: "scui-head scui-chat-head", children: [
@@ -2017,24 +2129,24 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2017
2129
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2018
2130
  const Header = slots.header;
2019
2131
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2020
- const [pending, setPendingState] = useState4(() => pendingMessageMemory.get(memoryKey) ?? null);
2021
- const [pendingAction, setPendingAction] = useState4(null);
2022
- const [restoreDraft, setRestoreDraft] = useState4(null);
2023
- const restoreSequence = useRef5(0);
2024
- const actionSequence = useRef5(0);
2025
- const acknowledged = useRef5(/* @__PURE__ */ new Set());
2132
+ const [pending, setPendingState] = useState5(() => pendingMessageMemory.get(memoryKey) ?? null);
2133
+ const [pendingAction, setPendingAction] = useState5(null);
2134
+ const [restoreDraft, setRestoreDraft] = useState5(null);
2135
+ const restoreSequence = useRef6(0);
2136
+ const actionSequence = useRef6(0);
2137
+ const acknowledged = useRef6(/* @__PURE__ */ new Set());
2026
2138
  const setPending = (update) => setPendingState((current) => {
2027
2139
  const next = typeof update === "function" ? update(current) : update;
2028
2140
  if (next) boundedSet(pendingMessageMemory, memoryKey, next);
2029
2141
  else pendingMessageMemory.delete(memoryKey);
2030
2142
  return next;
2031
2143
  });
2032
- useEffect6(() => {
2144
+ useEffect7(() => {
2033
2145
  setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
2034
2146
  setPendingAction(null);
2035
2147
  setRestoreDraft(null);
2036
2148
  }, [memoryKey]);
2037
- useEffect6(() => {
2149
+ useEffect7(() => {
2038
2150
  if (!pending) return;
2039
2151
  if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
2040
2152
  setPending(null);
@@ -2063,7 +2175,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2063
2175
  setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
2064
2176
  setPending({ ...pending, status: "editing" });
2065
2177
  };
2066
- useEffect6(() => {
2178
+ useEffect7(() => {
2067
2179
  const key = state.attached?.key;
2068
2180
  if (!key || !state.attention.some((item) => item.key === key)) {
2069
2181
  if (key) acknowledged.current.delete(key);
@@ -2098,7 +2210,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2098
2210
  return result;
2099
2211
  }
2100
2212
  }), [adapter, state.error]);
2101
- useEffect6(() => {
2213
+ useEffect7(() => {
2102
2214
  if (!pendingAction) return;
2103
2215
  if (state.operation && !pendingAction.seenOperation) {
2104
2216
  setPendingAction({ ...pendingAction, seenOperation: true });
@@ -2131,23 +2243,24 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2131
2243
  const startable = state.harnesses.filter((item) => item.startable);
2132
2244
  const startableKey = startable.map((item) => item.id).join("\0");
2133
2245
  const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2134
- const [harness, setHarness] = useState4(remembered.harness);
2135
- const [draft, setDraft] = useState4(remembered.draft);
2136
- const [context, setContext] = useState4(remembered.context);
2137
- const [images, setImages] = useState4(remembered.images ?? []);
2138
- const [starting, setStarting] = useState4(null);
2139
- const [picking, setPicking] = useState4(false);
2140
- const [pickerError, setPickerError] = useState4(null);
2141
- const startSequence = useRef5(0);
2142
- const textarea = useRef5(null);
2246
+ const [harness, setHarness] = useState5(remembered.harness);
2247
+ const [draft, setDraft] = useState5(remembered.draft);
2248
+ const [context, setContext] = useState5(remembered.context);
2249
+ const [images, setImages] = useState5(remembered.images ?? []);
2250
+ const [starting, setStarting] = useState5(null);
2251
+ const [picking, setPicking] = useState5(false);
2252
+ const [dragging, setDragging] = useState5(false);
2253
+ const [pickerError, setPickerError] = useState5(null);
2254
+ const startSequence = useRef6(0);
2255
+ const textarea = useRef6(null);
2143
2256
  useAutosizeTextarea(textarea, draft);
2144
- useEffect6(() => {
2257
+ useEffect7(() => {
2145
2258
  if (startable.some((item) => item.id === harness)) return;
2146
2259
  const next = startable[0]?.id ?? "";
2147
2260
  setHarness(next);
2148
2261
  boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2149
2262
  }, [harness, startableKey]);
2150
- useEffect6(() => {
2263
+ useEffect7(() => {
2151
2264
  if (!starting) return;
2152
2265
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2153
2266
  const beganWorking = !starting.busy && state.busy;
@@ -2155,10 +2268,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2155
2268
  newChatMemory.delete(memoryKey);
2156
2269
  onStarted();
2157
2270
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2158
- useEffect6(() => {
2271
+ useEffect7(() => {
2159
2272
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
2160
2273
  }, [starting, state.busy, state.error, state.operation]);
2161
- useEffect6(() => {
2274
+ useEffect7(() => {
2162
2275
  textarea.current?.focus({ preventScroll: true });
2163
2276
  }, []);
2164
2277
  const pickContext = () => {
@@ -2181,13 +2294,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2181
2294
  });
2182
2295
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
2183
2296
  };
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();
2297
+ const addImageFiles = (value, source) => {
2298
+ const allFiles = Array.from(value ?? []);
2299
+ if (!allFiles.length) return false;
2300
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
2301
+ if (files.length !== allFiles.length) {
2302
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
2303
+ return true;
2304
+ }
2188
2305
  if (images.length + files.length > MAX_IMAGE_ITEMS) {
2189
2306
  setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2190
- return;
2307
+ return true;
2191
2308
  }
2192
2309
  setPicking(true);
2193
2310
  setPickerError(null);
@@ -2195,11 +2312,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2195
2312
  const next = mergeImages(current, picked);
2196
2313
  boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2197
2314
  return next;
2198
- }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
2315
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2316
+ return true;
2317
+ };
2318
+ const pasteImages = (event) => {
2319
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
2320
+ };
2321
+ const dropImages = (event) => {
2322
+ setDragging(false);
2323
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
2199
2324
  };
2200
2325
  const send = () => {
2201
2326
  const text = draft.trim();
2202
- if (!text || !harness || starting) return;
2327
+ if (!text && !images.length || !harness || starting) return;
2203
2328
  const id = startSequence.current + 1;
2204
2329
  startSequence.current = id;
2205
2330
  setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
@@ -2250,7 +2375,16 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2250
2375
  return next;
2251
2376
  }) }),
2252
2377
  pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2253
- /* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
2378
+ /* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2379
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2380
+ event.preventDefault();
2381
+ setDragging(true);
2382
+ }
2383
+ }, onDragOver: (event) => {
2384
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
2385
+ }, onDragLeave: (event) => {
2386
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2387
+ }, onDrop: dropImages, children: [
2254
2388
  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
2389
  /* @__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) => {
2256
2390
  const value = event.currentTarget.value;
@@ -2262,7 +2396,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2262
2396
  send();
2263
2397
  }
2264
2398
  } }),
2265
- /* @__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 }) }) })
2399
+ /* @__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 }) }) })
2266
2400
  ] })
2267
2401
  ] })
2268
2402
  ] });
@@ -2271,14 +2405,14 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2271
2405
  const state = useMemo2(() => normalizeUiState(stateInput), [stateInput]);
2272
2406
  const copy = { ...DEFAULT_LABELS, ...labels };
2273
2407
  const memoryKey = state.workspace || "@default";
2274
- const [view, setViewState] = useState4(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
2275
- const [opening, setOpening] = useState4(null);
2276
- const [listFocus, setListFocus] = useState4(null);
2408
+ const [view, setViewState] = useState5(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
2409
+ const [opening, setOpening] = useState5(null);
2410
+ const [listFocus, setListFocus] = useState5(null);
2277
2411
  const setView = (next) => {
2278
2412
  boundedSet(messengerViewMemory, memoryKey, next);
2279
2413
  setViewState(next);
2280
2414
  };
2281
- useEffect6(() => {
2415
+ useEffect7(() => {
2282
2416
  if (!opening) return;
2283
2417
  if (state.attached?.key === opening.key) {
2284
2418
  setOpening(null);