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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/composer.mjs CHANGED
@@ -82,6 +82,7 @@ function boundedSet(map, key, value) {
82
82
  // src/icon.jsx
83
83
  import { Fragment, jsx, jsxs } from "preact/jsx-runtime";
84
84
  var ICONS = {
85
+ attach: () => /* @__PURE__ */ jsx("path", { d: "M6.25 9.75 10.6 5.4a2.1 2.1 0 0 1 2.97 2.97l-5.4 5.4a3.3 3.3 0 0 1-4.67-4.66l5.52-5.52" }),
85
86
  back: () => /* @__PURE__ */ jsx("path", { d: "m11.5 4.5-4.5 4.5 4.5 4.5" }),
86
87
  check: () => /* @__PURE__ */ jsx("path", { d: "m4 9 3.25 3.25L14 5.5" }),
87
88
  chevron: () => /* @__PURE__ */ jsx("path", { d: "m7 4.5 4.5 4.5L7 13.5" }),
@@ -97,6 +98,11 @@ var ICONS = {
97
98
  /* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
98
99
  /* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
99
100
  ] }),
101
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
102
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
103
+ /* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
104
+ /* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
105
+ ] }),
100
106
  menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
101
107
  /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
102
108
  /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
@@ -122,6 +128,107 @@ function UiIcon({ name, size = 16, class: className = "" }) {
122
128
  return /* @__PURE__ */ jsx("svg", { class: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Glyph, {}) });
123
129
  }
124
130
 
131
+ // src/context.jsx
132
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
133
+ var MAX_CONTEXT_ITEMS = 32;
134
+ var MAX_IMAGE_ITEMS = 4;
135
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
136
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
137
+ function normalizeContext(value) {
138
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
139
+ if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
140
+ const label = item.label.trim().slice(0, 200);
141
+ const detail = item.detail.slice(0, 2e4);
142
+ if (!label || !detail) return [];
143
+ return [{
144
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
145
+ ...typeof item.kind === "string" && item.kind ? { kind: item.kind.slice(0, 100) } : {},
146
+ label,
147
+ detail
148
+ }];
149
+ }).slice(0, MAX_CONTEXT_ITEMS);
150
+ }
151
+ function mergeContext(current, picked) {
152
+ const next = [...current];
153
+ const seen = new Set(current.map((item) => item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`));
154
+ for (const item of normalizeContext(picked)) {
155
+ const key = item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`;
156
+ if (seen.has(key)) continue;
157
+ seen.add(key);
158
+ next.push(item);
159
+ if (next.length === MAX_CONTEXT_ITEMS) break;
160
+ }
161
+ return next;
162
+ }
163
+ function normalizeImages(value) {
164
+ const seen = /* @__PURE__ */ new Set();
165
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
166
+ if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
167
+ const label = item.label.trim().slice(0, 200);
168
+ const url = item.url;
169
+ if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
170
+ seen.add(url);
171
+ return [{
172
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
173
+ label,
174
+ url
175
+ }];
176
+ }).slice(0, MAX_IMAGE_ITEMS);
177
+ }
178
+ function mergeImages(current, picked) {
179
+ const next = [...current];
180
+ const seen = new Set(current.map((item) => item.url));
181
+ for (const item of normalizeImages(picked)) {
182
+ if (seen.has(item.url)) continue;
183
+ seen.add(item.url);
184
+ next.push(item);
185
+ if (next.length === MAX_IMAGE_ITEMS) break;
186
+ }
187
+ return next;
188
+ }
189
+ function partitionAttachments(value) {
190
+ const values = Array.isArray(value) ? value : value ? [value] : [];
191
+ const context = [];
192
+ const images = [];
193
+ for (const item of values) {
194
+ if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
195
+ else if (item && typeof item.label === "string" && item.label.trim() && typeof item.url === "string" && (item.url.startsWith("data:image/") || item.url.startsWith("https://") || item.url.startsWith("http://"))) images.push(item);
196
+ else throw new Error("The attachment picker returned an invalid item.");
197
+ }
198
+ return { context, images };
199
+ }
200
+ async function imageAttachmentsFromFiles(value) {
201
+ const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
202
+ if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
203
+ return Promise.all(files.map(async (file) => {
204
+ if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
205
+ if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
206
+ const url = await new Promise((resolve, reject) => {
207
+ const reader = new FileReader();
208
+ reader.onload = () => resolve(reader.result);
209
+ reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
210
+ reader.readAsDataURL(file);
211
+ });
212
+ return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
213
+ }));
214
+ }
215
+ function ContextTray({ items, onRemove }) {
216
+ if (!items.length) return null;
217
+ return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
218
+ /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 12 }),
219
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
220
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
221
+ ] }, item.id ?? `${item.label}:${index}`)) });
222
+ }
223
+ function ImageTray({ items, onRemove }) {
224
+ if (!items.length) return null;
225
+ return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
226
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
227
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
228
+ onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
229
+ ] }, item.id ?? `${item.label}:${index}`)) });
230
+ }
231
+
125
232
  // src/textarea.js
126
233
  import { useLayoutEffect } from "preact/hooks";
127
234
  function useAutosizeTextarea(ref, value) {
@@ -137,7 +244,7 @@ function useAutosizeTextarea(ref, value) {
137
244
  }
138
245
 
139
246
  // src/composer.jsx
140
- import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
247
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
141
248
  var composerMemory = /* @__PURE__ */ new Map();
142
249
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
143
250
  if (state.mode !== "mirror" || state.canSend) return null;
@@ -147,29 +254,36 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
147
254
  const resume = canContinueHere(state);
148
255
  const join = state.canAttach;
149
256
  const branch = state.canBranch;
150
- if (!resume && !join && !branch) return /* @__PURE__ */ jsx2("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs2("span", { children: [
151
- /* @__PURE__ */ jsx2("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
152
- /* @__PURE__ */ jsx2("small", { children: "This session cannot be continued by an available harness." })
257
+ if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
258
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
259
+ /* @__PURE__ */ jsx3("small", { children: "This session cannot be continued by an available harness." })
153
260
  ] }) });
154
- return /* @__PURE__ */ jsxs2("div", { class: "scui-continuation", children: [
155
- /* @__PURE__ */ jsxs2("span", { children: [
156
- /* @__PURE__ */ jsx2("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
157
- /* @__PURE__ */ jsx2("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
261
+ return /* @__PURE__ */ jsxs3("div", { class: "scui-continuation", children: [
262
+ /* @__PURE__ */ jsxs3("span", { children: [
263
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
264
+ /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
158
265
  ] }),
159
- /* @__PURE__ */ jsx2("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
266
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
160
267
  ] });
161
268
  }
162
269
  function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
163
- const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
270
+ const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
164
271
  const [draft, setDraft] = useState(remembered.draft);
165
- const [queue, setQueue] = useState(remembered.queue);
272
+ const [context, setContext] = useState(remembered.context ?? []);
273
+ const [images, setImages] = useState(remembered.images ?? []);
274
+ const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
166
275
  const [dispatching, setDispatching] = useState(false);
276
+ const [picking, setPicking] = useState(false);
277
+ const [pickerError, setPickerError] = useState(null);
167
278
  const textarea = useRef(null);
168
279
  useAutosizeTextarea(textarea, draft);
169
- const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
280
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
281
+ useEffect(() => {
282
+ remember(draft, context, images, queue);
283
+ }, [draft, context, images, memoryKey, queue]);
170
284
  const updateQueue = (update) => setQueue((items) => {
171
285
  const next = update(items);
172
- remember(draft, next);
286
+ remember(draft, context, images, next);
173
287
  return next;
174
288
  });
175
289
  const queueBlocked = state.busy || pendingStatus !== null || dispatching;
@@ -179,9 +293,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
179
293
  const [next, ...rest] = queue;
180
294
  setDispatching(true);
181
295
  setQueue(rest);
182
- remember(draft, rest);
183
- onPending?.(next);
184
- adapter.onIntent({ action: "send", text: next });
296
+ remember(draft, context, images, rest);
297
+ onPending?.(next.text, next.context, next.images);
298
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
185
299
  }
186
300
  }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
187
301
  useEffect(() => {
@@ -193,7 +307,11 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
193
307
  useEffect(() => {
194
308
  if (!restoreDraft) return;
195
309
  setDraft(restoreDraft.text);
196
- remember(restoreDraft.text, queue);
310
+ const restoredContext = normalizeContext(restoreDraft.context);
311
+ const restoredImages = normalizeImages(restoreDraft.images);
312
+ setContext(restoredContext);
313
+ setImages(restoredImages);
314
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
197
315
  textarea.current?.focus({ preventScroll: true });
198
316
  onDraftRestored?.(restoreDraft.id);
199
317
  }, [onDraftRestored, restoreDraft?.id]);
@@ -201,43 +319,100 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
201
319
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
202
320
  return () => clearTimeout(timer);
203
321
  }, [adapter, draft]);
322
+ const pickContext = () => {
323
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
324
+ setPicking(true);
325
+ setPickerError(null);
326
+ Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
327
+ const attachments = partitionAttachments(picked);
328
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
329
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
330
+ setContext((current) => {
331
+ const next = mergeContext(current, attachments.context);
332
+ remember(draft, next, images, queue);
333
+ return next;
334
+ });
335
+ setImages((current) => {
336
+ const next = mergeImages(current, attachments.images);
337
+ remember(draft, context, next, queue);
338
+ return next;
339
+ });
340
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
341
+ };
342
+ const pasteImages = (event) => {
343
+ const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
344
+ if (!files.length) return;
345
+ event.preventDefault();
346
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
347
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
348
+ return;
349
+ }
350
+ setPicking(true);
351
+ setPickerError(null);
352
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
353
+ const next = mergeImages(current, picked);
354
+ remember(draft, context, next, queue);
355
+ return next;
356
+ }), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
357
+ };
204
358
  const send = () => {
205
359
  const text = draft.trim();
206
360
  if (!text) return;
207
- if (queuesNewMessage) updateQueue((items) => [...items, text]);
361
+ const message = { text, context, images };
362
+ if (queuesNewMessage) updateQueue((items) => [...items, message]);
208
363
  else if (state.canSend) {
209
364
  if (onPending) setDispatching(true);
210
- onPending?.(text);
211
- adapter.onIntent({ action: "send", text });
365
+ onPending?.(text, context, images);
366
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
212
367
  } else return;
213
368
  setDraft("");
214
- remember("", queuesNewMessage ? [...queue, text] : queue);
369
+ setContext([]);
370
+ setImages([]);
371
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
215
372
  };
216
- return /* @__PURE__ */ jsxs2("div", { class: "scui-compose", children: [
217
- queue.length ? /* @__PURE__ */ jsxs2("div", { class: "scui-queue", children: [
218
- /* @__PURE__ */ jsxs2("strong", { children: [
373
+ return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
374
+ queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
375
+ /* @__PURE__ */ jsxs3("strong", { children: [
219
376
  queue.length,
220
377
  " queued"
221
378
  ] }),
222
- queue.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
223
- item,
224
- /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 13 }) })
225
- ] }, `${index}:${item}`))
379
+ queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
380
+ /* @__PURE__ */ jsxs3("span", { children: [
381
+ item.text,
382
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
383
+ item.context.length + item.images.length,
384
+ " attached"
385
+ ] }) : null
386
+ ] }),
387
+ /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `Remove queued message ${index + 1}`, onClick: () => updateQueue((items) => items.filter((_, itemIndex) => itemIndex !== index)), children: /* @__PURE__ */ jsx3(UiIcon, { name: "close", size: 13 }) })
388
+ ] }, `${index}:${item.text}`))
226
389
  ] }) : null,
227
- /* @__PURE__ */ jsxs2("div", { class: "scui-envelope", children: [
228
- /* @__PURE__ */ jsx2("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
390
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
391
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
392
+ remember(draft, context, next, queue);
393
+ return next;
394
+ }) }),
395
+ /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
396
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
397
+ remember(draft, next, images, queue);
398
+ return next;
399
+ }) }),
400
+ pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
401
+ /* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
402
+ 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,
403
+ /* @__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) => {
229
404
  const value = event.currentTarget.value;
230
405
  setDraft(value);
231
- remember(value, queue);
406
+ remember(value, context, images, queue);
232
407
  }, onKeyDown: (event) => {
233
408
  if (isSendKey(event)) {
234
409
  event.preventDefault();
235
410
  send();
236
411
  }
237
412
  } }),
238
- /* @__PURE__ */ jsxs2("span", { children: [
239
- state.busy ? /* @__PURE__ */ jsx2("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx2(UiIcon, { name: "stop", size: 15 }) }) : null,
240
- /* @__PURE__ */ jsx2("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: /* @__PURE__ */ jsx2(UiIcon, { name: queuesNewMessage ? "plus" : "send", size: 17 }) })
413
+ /* @__PURE__ */ jsxs3("span", { children: [
414
+ 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,
415
+ /* @__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 }) })
241
416
  ] })
242
417
  ] })
243
418
  ] });
package/controller.mjs CHANGED
@@ -99,6 +99,23 @@ function projectContext(context) {
99
99
  return projected.length ? projected : undefined;
100
100
  }
101
101
 
102
+ function projectImages(images) {
103
+ if (!Array.isArray(images)) return undefined;
104
+ const projected = images.flatMap((item) => {
105
+ if (!item || typeof item !== 'object' || typeof item.label !== 'string') return [];
106
+ const url = typeof item.url === 'string' && !item.url.endsWith('\n…')
107
+ && (!item.url.startsWith('data:image/') || item.url.length <= 256_000)
108
+ ? item.url
109
+ : null;
110
+ return [{
111
+ ...(typeof item.id === 'string' ? { id: item.id } : {}),
112
+ label: item.label,
113
+ ...(url ? { url } : {}),
114
+ }];
115
+ }).slice(0, 4);
116
+ return projected.length ? projected : undefined;
117
+ }
118
+
102
119
  function requestSummary(entry) {
103
120
  const options = Array.isArray(entry.options)
104
121
  ? entry.options.map((option) => option?.name).filter(Boolean).join(' / ')
@@ -113,6 +130,7 @@ function projectConversationEntry(entry, maxEntryChars) {
113
130
  if (entry.visibility === 'context') return null;
114
131
  const body = truncate(entry.text, maxEntryChars);
115
132
  const context = projectContext(entry.context);
133
+ const images = projectImages(entry.images);
116
134
  return {
117
135
  id: entry.id,
118
136
  role: entry.role,
@@ -121,6 +139,7 @@ function projectConversationEntry(entry, maxEntryChars) {
121
139
  ts: timestampFromMetadata(entry.metadata),
122
140
  truncated: body.truncated,
123
141
  ...(context ? { context } : {}),
142
+ ...(images ? { images } : {}),
124
143
  };
125
144
  }
126
145
  if (entry.kind === 'tool') {
@@ -376,10 +395,10 @@ async function dispatchStandard(controller, intent, options) {
376
395
  const active = snapshot.activeSessionKey;
377
396
  if (intent.action === 'mounted') return;
378
397
  if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
379
- if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text });
398
+ if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
380
399
  if (intent.action === 'new') {
381
400
  await controller.dispatch({ type: 'start', harness: intent.harness });
382
- return controller.dispatch({ type: 'send', text: intent.text });
401
+ return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
383
402
  }
384
403
  if (intent.action === 'resume' && active) return controller.dispatch({ type: 'resume', sessionKey: active });
385
404
  if (intent.action === 'join' && active) return controller.dispatch({ type: 'attach', sessionKey: active });