@volter-ai-dev/supercode-ui 0.1.22 → 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/README.md CHANGED
@@ -132,6 +132,11 @@ inventing fallback prompt text. The composer bounds images to four supported bro
132
132
  messages, retry, edit, and new-chat recovery. The host still chooses how explicit file selection
133
133
  is exposed and must revalidate every returned item.
134
134
 
135
+ Transcript images with a browser-safe URL open in a keyboard-accessible, viewport-bounded viewer;
136
+ remote images expose copy-link and open-original actions, while local data images can be downloaded.
137
+ If a host deliberately omits a large historical data URL from its bounded projection, the UI shows
138
+ an honest unavailable-preview attachment instead of a broken thumbnail or a deceptive action.
139
+
135
140
  An embedding product such as Vibewaiting should therefore be small: Lucarne owns its iframe and
136
141
  launcher lifecycle, Supercode owns this UI and the controller semantics, and Vibewaiting only
137
142
  bridges state/intents plus host-specific theme and persistence policy.
package/components.d.ts CHANGED
@@ -5,7 +5,9 @@ export {
5
5
  ContinuationBar,
6
6
  Conversation,
7
7
  HarnessLogo,
8
+ ImageViewer,
8
9
  LoadingStatus,
10
+ MessageImages,
9
11
  RequestCard,
10
12
  SessionDetails,
11
13
  SessionList,
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,18 +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 [dragging, setDragging] = useState(false);
971
- const [pickerError, setPickerError] = useState(null);
972
- 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);
973
1063
  useAutosizeTextarea(textarea, draft);
974
1064
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
975
- useEffect(() => {
1065
+ useEffect2(() => {
976
1066
  remember(draft, context, images, queue);
977
1067
  }, [draft, context, images, memoryKey, queue]);
978
1068
  const updateQueue = (update) => setQueue((items) => {
@@ -982,7 +1072,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
982
1072
  });
983
1073
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
984
1074
  const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
985
- useEffect(() => {
1075
+ useEffect2(() => {
986
1076
  if (!queueBlocked && state.canSend && queue.length) {
987
1077
  const [next, ...rest] = queue;
988
1078
  setDispatching(true);
@@ -992,13 +1082,13 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
992
1082
  adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
993
1083
  }
994
1084
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
995
- useEffect(() => {
1085
+ useEffect2(() => {
996
1086
  if (pendingStatus !== null || state.busy) setDispatching(false);
997
1087
  }, [pendingStatus, state.busy]);
998
- useEffect(() => {
1088
+ useEffect2(() => {
999
1089
  textarea.current?.focus({ preventScroll: true });
1000
1090
  }, [memoryKey]);
1001
- useEffect(() => {
1091
+ useEffect2(() => {
1002
1092
  if (!restoreDraft) return;
1003
1093
  setDraft(restoreDraft.text);
1004
1094
  const restoredContext = normalizeContext(restoreDraft.context);
@@ -1009,7 +1099,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1009
1099
  textarea.current?.focus({ preventScroll: true });
1010
1100
  onDraftRestored?.(restoreDraft.id);
1011
1101
  }, [onDraftRestored, restoreDraft?.id]);
1012
- useEffect(() => {
1102
+ useEffect2(() => {
1013
1103
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
1014
1104
  return () => clearTimeout(timer);
1015
1105
  }, [adapter, draft]);
@@ -1134,12 +1224,12 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1134
1224
  }
1135
1225
 
1136
1226
  // src/conversation.jsx
1137
- import { Fragment as Fragment2 } from "preact";
1138
- 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";
1139
1229
 
1140
1230
  // src/markdown.jsx
1141
1231
  import MarkdownIt from "markdown-it";
1142
- import { useEffect as useEffect2, useMemo, useRef as useRef2 } from "preact/hooks";
1232
+ import { useEffect as useEffect3, useMemo, useRef as useRef3 } from "preact/hooks";
1143
1233
  import { jsx as jsx4 } from "preact/jsx-runtime";
1144
1234
  var markdown = new MarkdownIt({ html: false, linkify: true, breaks: false });
1145
1235
  var LANGUAGE_LABELS = {
@@ -1201,8 +1291,8 @@ function setCopyState(button, status) {
1201
1291
  }
1202
1292
  function Markdown({ value, copyText }) {
1203
1293
  const html = useMemo(() => markdown.render(value), [value]);
1204
- const resets = useRef2(/* @__PURE__ */ new Map());
1205
- useEffect2(() => () => {
1294
+ const resets = useRef3(/* @__PURE__ */ new Map());
1295
+ useEffect3(() => () => {
1206
1296
  for (const timer of resets.current.values()) clearTimeout(timer);
1207
1297
  resets.current.clear();
1208
1298
  }, []);
@@ -1231,7 +1321,7 @@ function Markdown({ value, copyText }) {
1231
1321
  }
1232
1322
 
1233
1323
  // src/conversation.jsx
1234
- 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";
1235
1325
  function LoadingStatus({ state, compact = false }) {
1236
1326
  const copy = {
1237
1327
  connecting: ["Connecting to coding agents", "Checking installed harnesses and capabilities.", 0],
@@ -1280,9 +1370,9 @@ function ContextDisclosure({ context }) {
1280
1370
  ] });
1281
1371
  }
1282
1372
  function MessageMeta({ entry, adapter }) {
1283
- const [copyState, setCopyState2] = useState2("idle");
1284
- const reset = useRef3(null);
1285
- useEffect3(() => () => clearTimeout(reset.current), []);
1373
+ const [copyState, setCopyState2] = useState3("idle");
1374
+ const reset = useRef4(null);
1375
+ useEffect4(() => () => clearTimeout(reset.current), []);
1286
1376
  const date = entry.ts === null ? null : new Date(entry.ts);
1287
1377
  const validDate = date && Number.isFinite(date.valueOf()) ? date : null;
1288
1378
  if (!validDate && (!adapter?.copyText || !entry.text)) return null;
@@ -1303,33 +1393,33 @@ function MessageMeta({ entry, adapter }) {
1303
1393
  ] });
1304
1394
  }
1305
1395
  var TOOL_ICONS = {
1306
- read: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1396
+ read: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1307
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" }),
1308
1398
  /* @__PURE__ */ jsx5("path", { d: "M12 2.9v3h2.85M6 9h6M6 12h4" })
1309
1399
  ] }),
1310
- search: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1400
+ search: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1311
1401
  /* @__PURE__ */ jsx5("circle", { cx: "8", cy: "8", r: "4.5" }),
1312
1402
  /* @__PURE__ */ jsx5("path", { d: "m11.5 11.5 3 3" })
1313
1403
  ] }),
1314
- edit: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1404
+ edit: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1315
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" }),
1316
1406
  /* @__PURE__ */ jsx5("path", { d: "m10 5 3 3" })
1317
1407
  ] }),
1318
- command: () => /* @__PURE__ */ jsx5(Fragment3, { children: /* @__PURE__ */ jsx5("path", { d: "m3 5 3 3-3 3M8 12h6" }) }),
1319
- 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: [
1320
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" }),
1321
1411
  /* @__PURE__ */ jsx5("path", { d: "M5 2.5h8" })
1322
1412
  ] }),
1323
- web: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1413
+ web: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1324
1414
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "6.5" }),
1325
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" })
1326
1416
  ] }),
1327
- agent: () => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1417
+ agent: () => /* @__PURE__ */ jsxs4(Fragment4, { children: [
1328
1418
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "6", r: "2.5" }),
1329
1419
  /* @__PURE__ */ jsx5("path", { d: "M4 15c.4-3 2-4.5 5-4.5s4.6 1.5 5 4.5" })
1330
1420
  ] }),
1331
- 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" }) }),
1332
- 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: [
1333
1423
  /* @__PURE__ */ jsx5("path", { d: "M9 2.5v3M9 12.5v3M2.5 9h3M12.5 9h3" }),
1334
1424
  /* @__PURE__ */ jsx5("circle", { cx: "9", cy: "9", r: "3.5" })
1335
1425
  ] })
@@ -1395,9 +1485,9 @@ function ToolMetrics({ presentation }) {
1395
1485
  }
1396
1486
  function PendingElapsed({ now }) {
1397
1487
  const clock = now ?? Date.now;
1398
- const started = useRef3(clock());
1399
- const [elapsed, setElapsed] = useState2(0);
1400
- useEffect3(() => {
1488
+ const started = useRef4(clock());
1489
+ const [elapsed, setElapsed] = useState3(0);
1490
+ useEffect4(() => {
1401
1491
  const timer = setInterval(() => setElapsed(Math.max(0, clock() - started.current)), 1e3);
1402
1492
  return () => clearInterval(timer);
1403
1493
  }, [clock]);
@@ -1438,7 +1528,7 @@ function SearchPreview({ presentation }) {
1438
1528
  ] }) : null,
1439
1529
  lines.length ? /* @__PURE__ */ jsx5("ol", { children: lines.map((line, index) => {
1440
1530
  const match = /^(.*?):(\d+)(?::(\d+))?:(.*)$/.exec(line);
1441
- return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment3, { children: [
1531
+ return /* @__PURE__ */ jsx5("li", { children: match ? /* @__PURE__ */ jsxs4(Fragment4, { children: [
1442
1532
  /* @__PURE__ */ jsx5("code", { children: match[1] }),
1443
1533
  /* @__PURE__ */ jsxs4("small", { children: [
1444
1534
  match[2],
@@ -1471,9 +1561,9 @@ function ToolPreview({ presentation, entry }) {
1471
1561
  return presentation.preview ? /* @__PURE__ */ jsx5("pre", { class: "scui-tool-output", "data-error": failed, children: stripAnsi(presentation.preview) }) : null;
1472
1562
  }
1473
1563
  function ToolActions({ presentation, adapter }) {
1474
- const [copied, setCopied] = useState2(false);
1475
- const reset = useRef3(null);
1476
- useEffect3(() => () => clearTimeout(reset.current), []);
1564
+ const [copied, setCopied] = useState3(false);
1565
+ const reset = useRef4(null);
1566
+ useEffect4(() => () => clearTimeout(reset.current), []);
1477
1567
  if (!adapter?.copyText) return null;
1478
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;
1479
1569
  const copy = async () => {
@@ -1520,7 +1610,7 @@ function TranscriptEntry({ entry, state, adapter }) {
1520
1610
  }
1521
1611
  if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
1522
1612
  return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
1523
- /* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
1613
+ /* @__PURE__ */ jsx5(MessageImages, { items: entry.images, adapter }),
1524
1614
  /* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
1525
1615
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
1526
1616
  entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
@@ -1529,14 +1619,14 @@ function TranscriptEntry({ entry, state, adapter }) {
1529
1619
  }
1530
1620
  function ToolRow({ entry, workspace, open = false, adapter }) {
1531
1621
  const presentation = entry.presentation ?? createToolPresentation(entry);
1532
- const [expanded, setExpanded] = useState2(open || entry.status === "pending");
1533
- useEffect3(() => {
1622
+ const [expanded, setExpanded] = useState3(open || entry.status === "pending");
1623
+ useEffect4(() => {
1534
1624
  if (entry.status === "pending") setExpanded(true);
1535
1625
  }, [entry.status]);
1536
1626
  const target = compactToolTarget(presentation.target, workspace);
1537
1627
  const hasDetail = Boolean(entry.arguments || entry.resultText || presentation.preview || presentation.fields.length);
1538
1628
  const category = presentation.category ?? toolCategory(entry);
1539
- const summary = /* @__PURE__ */ jsxs4(Fragment3, { children: [
1629
+ const summary = /* @__PURE__ */ jsxs4(Fragment4, { children: [
1540
1630
  /* @__PURE__ */ jsx5(ToolIcon, { category }),
1541
1631
  /* @__PURE__ */ jsx5("strong", { children: toolAction2(entry, category, presentation) }),
1542
1632
  target ? /* @__PURE__ */ jsx5("code", { class: "scui-tool-target", title: presentation.target, children: target }) : null,
@@ -1564,9 +1654,9 @@ function ToolRow({ entry, workspace, open = false, adapter }) {
1564
1654
  function ActivityGroup({ entries, state, adapter }) {
1565
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 }) });
1566
1656
  const active = entries.some((entry) => entry.status === "pending");
1567
- const [open, setOpen] = useState2(active);
1657
+ const [open, setOpen] = useState3(active);
1568
1658
  const id = useId();
1569
- useEffect3(() => {
1659
+ useEffect4(() => {
1570
1660
  if (active) setOpen(true);
1571
1661
  }, [active]);
1572
1662
  return /* @__PURE__ */ jsxs4("section", { class: "scui-activity", children: [
@@ -1642,9 +1732,9 @@ function SessionDetails({ semantics }) {
1642
1732
  }
1643
1733
  var conversationMemory = /* @__PURE__ */ new Map();
1644
1734
  function ConversationAnnouncements({ state }) {
1645
- const previousBusy = useRef3(state.busy);
1646
- const [announcement, setAnnouncement] = useState2("");
1647
- useEffect3(() => {
1735
+ const previousBusy = useRef4(state.busy);
1736
+ const [announcement, setAnnouncement] = useState3("");
1737
+ useEffect4(() => {
1648
1738
  if (previousBusy.current && !state.busy && !state.error) {
1649
1739
  setAnnouncement(`${harnessDisplayName(state.harness) || "Coding agent"} finished working`);
1650
1740
  }
@@ -1653,13 +1743,13 @@ function ConversationAnnouncements({ state }) {
1653
1743
  return /* @__PURE__ */ jsx5("span", { class: "scui-sr-only", role: "status", "aria-live": "polite", "aria-atomic": "true", children: announcement });
1654
1744
  }
1655
1745
  function Conversation({ state, adapter, components = {}, slots = {}, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pending = null, unreadAfterMessages = null }) {
1656
- const scroller = useRef3(null);
1746
+ const scroller = useRef4(null);
1657
1747
  const remembered = conversationMemory.get(memoryKey) ?? { top: null, atBottom: true };
1658
- const [atBottom, setAtBottom] = useState2(remembered.atBottom);
1659
- const earlierAnchor = useRef3(null);
1660
- const restored = useRef3(false);
1748
+ const [atBottom, setAtBottom] = useState3(remembered.atBottom);
1749
+ const earlierAnchor = useRef4(null);
1750
+ const restored = useRef4(false);
1661
1751
  const blocks = groupConversation(state.transcript);
1662
- const unreadBoundary = useRef3(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1752
+ const unreadBoundary = useRef4(Number.isSafeInteger(unreadAfterMessages) && unreadAfterMessages >= 0 ? unreadAfterMessages : null);
1663
1753
  const unreadBlock = unreadBoundary.current === null ? -1 : blocks.findIndex((block) => {
1664
1754
  const entries = block.kind === "activity" ? block.entries : [block.entry];
1665
1755
  return entries.some((entry) => Number.isSafeInteger(entry.messageIndex) && entry.messageIndex > unreadBoundary.current);
@@ -1718,12 +1808,12 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1718
1808
  }, children: "Load earlier messages" }) : null,
1719
1809
  !blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state }) : null,
1720
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,
1721
- blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment2, { children: [
1811
+ blocks.map((block, index) => /* @__PURE__ */ jsxs4(Fragment3, { children: [
1722
1812
  index === unreadBlock ? /* @__PURE__ */ jsx5("div", { class: "scui-unread-divider", role: "separator", "aria-label": "New messages", children: /* @__PURE__ */ jsx5("span", { children: "New" }) }) : null,
1723
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 })
1724
1814
  ] }, block.id)),
1725
1815
  pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
1726
- /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
1816
+ /* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images, adapter }),
1727
1817
  /* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
1728
1818
  /* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
1729
1819
  /* @__PURE__ */ jsxs4("footer", { children: [
@@ -1754,7 +1844,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1754
1844
  }
1755
1845
 
1756
1846
  // src/logo.jsx
1757
- import { useEffect as useEffect4 } from "preact/hooks";
1847
+ import { useEffect as useEffect5 } from "preact/hooks";
1758
1848
  import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
1759
1849
  var LOGOS = {
1760
1850
  "claude-code": {
@@ -1805,7 +1895,7 @@ function hasHarnessLogo(id) {
1805
1895
  }
1806
1896
  function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1807
1897
  const logo = LOGOS[id];
1808
- useEffect4(() => {
1898
+ useEffect5(() => {
1809
1899
  if (!logo) onMissingLogo?.(id);
1810
1900
  }, [id, logo, onMissingLogo]);
1811
1901
  if (!logo) return null;
@@ -1816,10 +1906,10 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1816
1906
  }
1817
1907
 
1818
1908
  // src/messenger.jsx
1819
- 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";
1820
1910
 
1821
1911
  // src/sessions.jsx
1822
- 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";
1823
1913
  import { jsx as jsx7, jsxs as jsxs6 } from "preact/jsx-runtime";
1824
1914
  var sessionListMemory = /* @__PURE__ */ new Map();
1825
1915
  function SessionRow({ row, state, onOpen }) {
@@ -1841,21 +1931,21 @@ function SessionRow({ row, state, onOpen }) {
1841
1931
  }
1842
1932
  function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
1843
1933
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
1844
- const [query, setQuery] = useState3(remembered.query);
1845
- const [loadingMore, setLoadingMore] = useState3(false);
1846
- const root = useRef4(null);
1847
- 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);
1848
1938
  const rows = filterSessions(state.sessions, query);
1849
1939
  const Row = components.SessionRow ?? SessionRow;
1850
1940
  useLayoutEffect3(() => {
1851
1941
  if (rowScroller.current) rowScroller.current.scrollTop = remembered.top;
1852
1942
  }, [memoryKey]);
1853
- useEffect5(() => {
1943
+ useEffect6(() => {
1854
1944
  if (!focusKey || !root.current) return;
1855
1945
  const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
1856
1946
  (target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
1857
1947
  }, [focusKey, rows.length]);
1858
- useEffect5(() => {
1948
+ useEffect6(() => {
1859
1949
  if (loadingMore) setLoadingMore(false);
1860
1950
  }, [state.error, state.history.hasMoreSessions, state.sessions.length]);
1861
1951
  const loadMore = () => {
@@ -1941,10 +2031,10 @@ function Receipt({ state, adapter }) {
1941
2031
  return null;
1942
2032
  }
1943
2033
  function ConversationActions({ state, adapter, actionPending }) {
1944
- const [open, setOpen] = useState4(false);
1945
- const root = useRef5(null);
1946
- const panel = useRef5(null);
1947
- 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);
1948
2038
  const menuId = useId2();
1949
2039
  const targets = state.harnesses.filter((item) => item.startable);
1950
2040
  const groups = [
@@ -1965,7 +2055,7 @@ function ConversationActions({ state, adapter, actionPending }) {
1965
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 } })) : []
1966
2056
  }
1967
2057
  ].filter((group) => group.items.length);
1968
- useEffect6(() => {
2058
+ useEffect7(() => {
1969
2059
  if (!open) return;
1970
2060
  panel.current?.querySelector("button:not(:disabled)")?.focus({ preventScroll: true });
1971
2061
  const dismiss = (event) => {
@@ -2015,12 +2105,12 @@ function ConversationActions({ state, adapter, actionPending }) {
2015
2105
  ] });
2016
2106
  }
2017
2107
  function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose }) {
2018
- const back = useRef5(null);
2108
+ const back = useRef6(null);
2019
2109
  const harness = state.attached?.harness ?? state.harness;
2020
2110
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
2021
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";
2022
2112
  const menu = state.canDetach || state.canOpenTerminal || state.canBranch || state.canAttach || state.canExport || state.canReduce;
2023
- useEffect6(() => {
2113
+ useEffect7(() => {
2024
2114
  if (state.mode === "mirror" && !state.canSend) back.current?.focus({ preventScroll: true });
2025
2115
  }, []);
2026
2116
  return /* @__PURE__ */ jsxs7("header", { class: "scui-head scui-chat-head", children: [
@@ -2042,24 +2132,24 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2042
2132
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2043
2133
  const Header = slots.header;
2044
2134
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2045
- const [pending, setPendingState] = useState4(() => pendingMessageMemory.get(memoryKey) ?? null);
2046
- const [pendingAction, setPendingAction] = useState4(null);
2047
- const [restoreDraft, setRestoreDraft] = useState4(null);
2048
- const restoreSequence = useRef5(0);
2049
- const actionSequence = useRef5(0);
2050
- 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());
2051
2141
  const setPending = (update) => setPendingState((current) => {
2052
2142
  const next = typeof update === "function" ? update(current) : update;
2053
2143
  if (next) boundedSet(pendingMessageMemory, memoryKey, next);
2054
2144
  else pendingMessageMemory.delete(memoryKey);
2055
2145
  return next;
2056
2146
  });
2057
- useEffect6(() => {
2147
+ useEffect7(() => {
2058
2148
  setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
2059
2149
  setPendingAction(null);
2060
2150
  setRestoreDraft(null);
2061
2151
  }, [memoryKey]);
2062
- useEffect6(() => {
2152
+ useEffect7(() => {
2063
2153
  if (!pending) return;
2064
2154
  if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
2065
2155
  setPending(null);
@@ -2088,7 +2178,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2088
2178
  setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
2089
2179
  setPending({ ...pending, status: "editing" });
2090
2180
  };
2091
- useEffect6(() => {
2181
+ useEffect7(() => {
2092
2182
  const key = state.attached?.key;
2093
2183
  if (!key || !state.attention.some((item) => item.key === key)) {
2094
2184
  if (key) acknowledged.current.delete(key);
@@ -2123,7 +2213,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2123
2213
  return result;
2124
2214
  }
2125
2215
  }), [adapter, state.error]);
2126
- useEffect6(() => {
2216
+ useEffect7(() => {
2127
2217
  if (!pendingAction) return;
2128
2218
  if (state.operation && !pendingAction.seenOperation) {
2129
2219
  setPendingAction({ ...pendingAction, seenOperation: true });
@@ -2156,24 +2246,24 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2156
2246
  const startable = state.harnesses.filter((item) => item.startable);
2157
2247
  const startableKey = startable.map((item) => item.id).join("\0");
2158
2248
  const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2159
- const [harness, setHarness] = useState4(remembered.harness);
2160
- const [draft, setDraft] = useState4(remembered.draft);
2161
- const [context, setContext] = useState4(remembered.context);
2162
- const [images, setImages] = useState4(remembered.images ?? []);
2163
- const [starting, setStarting] = useState4(null);
2164
- const [picking, setPicking] = useState4(false);
2165
- const [dragging, setDragging] = useState4(false);
2166
- const [pickerError, setPickerError] = useState4(null);
2167
- const startSequence = useRef5(0);
2168
- 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);
2169
2259
  useAutosizeTextarea(textarea, draft);
2170
- useEffect6(() => {
2260
+ useEffect7(() => {
2171
2261
  if (startable.some((item) => item.id === harness)) return;
2172
2262
  const next = startable[0]?.id ?? "";
2173
2263
  setHarness(next);
2174
2264
  boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2175
2265
  }, [harness, startableKey]);
2176
- useEffect6(() => {
2266
+ useEffect7(() => {
2177
2267
  if (!starting) return;
2178
2268
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2179
2269
  const beganWorking = !starting.busy && state.busy;
@@ -2181,10 +2271,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2181
2271
  newChatMemory.delete(memoryKey);
2182
2272
  onStarted();
2183
2273
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2184
- useEffect6(() => {
2274
+ useEffect7(() => {
2185
2275
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
2186
2276
  }, [starting, state.busy, state.error, state.operation]);
2187
- useEffect6(() => {
2277
+ useEffect7(() => {
2188
2278
  textarea.current?.focus({ preventScroll: true });
2189
2279
  }, []);
2190
2280
  const pickContext = () => {
@@ -2318,14 +2408,14 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2318
2408
  const state = useMemo2(() => normalizeUiState(stateInput), [stateInput]);
2319
2409
  const copy = { ...DEFAULT_LABELS, ...labels };
2320
2410
  const memoryKey = state.workspace || "@default";
2321
- const [view, setViewState] = useState4(initialView ?? (state.attention.length ? "list" : messengerViewMemory.get(memoryKey) ?? (state.attached || state.transcript.length ? "chat" : "list")));
2322
- const [opening, setOpening] = useState4(null);
2323
- 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);
2324
2414
  const setView = (next) => {
2325
2415
  boundedSet(messengerViewMemory, memoryKey, next);
2326
2416
  setViewState(next);
2327
2417
  };
2328
- useEffect6(() => {
2418
+ useEffect7(() => {
2329
2419
  if (!opening) return;
2330
2420
  if (state.attached?.key === opening.key) {
2331
2421
  setOpening(null);
@@ -2374,7 +2464,9 @@ export {
2374
2464
  ContinuationBar,
2375
2465
  Conversation,
2376
2466
  HarnessLogo,
2467
+ ImageViewer,
2377
2468
  LoadingStatus,
2469
+ MessageImages,
2378
2470
  RequestCard,
2379
2471
  SessionDetails,
2380
2472
  SessionList,