@volter-ai-dev/supercode-ui 0.1.20 → 0.1.22
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 +8 -4
- package/components.mjs +244 -46
- package/composer.mjs +153 -27
- package/controller.mjs +21 -2
- package/conversation.mjs +213 -195
- package/core.mjs +12 -0
- package/embed.mjs +244 -46
- package/icon.mjs +5 -0
- package/index.d.ts +15 -5
- package/messenger.mjs +244 -46
- package/package.json +1 -1
- package/sessions.mjs +45 -36
- package/styles.css +3 -0
package/composer.mjs
CHANGED
|
@@ -98,6 +98,11 @@ var ICONS = {
|
|
|
98
98
|
/* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
|
|
99
99
|
/* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
100
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
|
+
] }),
|
|
101
106
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
102
107
|
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
103
108
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
@@ -126,6 +131,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
126
131
|
// src/context.jsx
|
|
127
132
|
import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
128
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"]);
|
|
129
137
|
function normalizeContext(value) {
|
|
130
138
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
131
139
|
if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
|
|
@@ -152,6 +160,58 @@ function mergeContext(current, picked) {
|
|
|
152
160
|
}
|
|
153
161
|
return next;
|
|
154
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
|
+
}
|
|
155
215
|
function ContextTray({ items, onRemove }) {
|
|
156
216
|
if (!items.length) return null;
|
|
157
217
|
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
@@ -160,6 +220,14 @@ function ContextTray({ items, onRemove }) {
|
|
|
160
220
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
|
|
161
221
|
] }, item.id ?? `${item.label}:${index}`)) });
|
|
162
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
|
+
}
|
|
163
231
|
|
|
164
232
|
// src/textarea.js
|
|
165
233
|
import { useLayoutEffect } from "preact/hooks";
|
|
@@ -199,19 +267,24 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
199
267
|
] });
|
|
200
268
|
}
|
|
201
269
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
202
|
-
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
|
|
270
|
+
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
203
271
|
const [draft, setDraft] = useState(remembered.draft);
|
|
204
|
-
const [context, setContext] = useState(remembered.context);
|
|
205
|
-
const [
|
|
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 ?? [] })));
|
|
206
275
|
const [dispatching, setDispatching] = useState(false);
|
|
207
276
|
const [picking, setPicking] = useState(false);
|
|
277
|
+
const [dragging, setDragging] = useState(false);
|
|
208
278
|
const [pickerError, setPickerError] = useState(null);
|
|
209
279
|
const textarea = useRef(null);
|
|
210
280
|
useAutosizeTextarea(textarea, draft);
|
|
211
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
281
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
remember(draft, context, images, queue);
|
|
284
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
212
285
|
const updateQueue = (update) => setQueue((items) => {
|
|
213
286
|
const next = update(items);
|
|
214
|
-
remember(draft, context, next);
|
|
287
|
+
remember(draft, context, images, next);
|
|
215
288
|
return next;
|
|
216
289
|
});
|
|
217
290
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -221,9 +294,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
221
294
|
const [next, ...rest] = queue;
|
|
222
295
|
setDispatching(true);
|
|
223
296
|
setQueue(rest);
|
|
224
|
-
remember(draft, context, rest);
|
|
225
|
-
onPending?.(next.text, next.context);
|
|
226
|
-
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
|
|
297
|
+
remember(draft, context, images, rest);
|
|
298
|
+
onPending?.(next.text, next.context, next.images);
|
|
299
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
227
300
|
}
|
|
228
301
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
229
302
|
useEffect(() => {
|
|
@@ -236,8 +309,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
236
309
|
if (!restoreDraft) return;
|
|
237
310
|
setDraft(restoreDraft.text);
|
|
238
311
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
312
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
239
313
|
setContext(restoredContext);
|
|
240
|
-
|
|
314
|
+
setImages(restoredImages);
|
|
315
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
241
316
|
textarea.current?.focus({ preventScroll: true });
|
|
242
317
|
onDraftRestored?.(restoreDraft.id);
|
|
243
318
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -246,30 +321,67 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
246
321
|
return () => clearTimeout(timer);
|
|
247
322
|
}, [adapter, draft]);
|
|
248
323
|
const pickContext = () => {
|
|
249
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
324
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
250
325
|
setPicking(true);
|
|
251
326
|
setPickerError(null);
|
|
252
327
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
328
|
+
const attachments = partitionAttachments(picked);
|
|
329
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
330
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
253
331
|
setContext((current) => {
|
|
254
|
-
const next = mergeContext(current,
|
|
255
|
-
remember(draft, next, queue);
|
|
332
|
+
const next = mergeContext(current, attachments.context);
|
|
333
|
+
remember(draft, next, images, queue);
|
|
256
334
|
return next;
|
|
257
335
|
});
|
|
258
|
-
|
|
336
|
+
setImages((current) => {
|
|
337
|
+
const next = mergeImages(current, attachments.images);
|
|
338
|
+
remember(draft, context, next, queue);
|
|
339
|
+
return next;
|
|
340
|
+
});
|
|
341
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
342
|
+
};
|
|
343
|
+
const addImageFiles = (value, source) => {
|
|
344
|
+
const allFiles = Array.from(value ?? []);
|
|
345
|
+
if (!allFiles.length) return false;
|
|
346
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
347
|
+
if (files.length !== allFiles.length) {
|
|
348
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
352
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
setPicking(true);
|
|
356
|
+
setPickerError(null);
|
|
357
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
358
|
+
const next = mergeImages(current, picked);
|
|
359
|
+
remember(draft, context, next, queue);
|
|
360
|
+
return next;
|
|
361
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
362
|
+
return true;
|
|
363
|
+
};
|
|
364
|
+
const pasteImages = (event) => {
|
|
365
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
366
|
+
};
|
|
367
|
+
const dropImages = (event) => {
|
|
368
|
+
setDragging(false);
|
|
369
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
259
370
|
};
|
|
260
371
|
const send = () => {
|
|
261
372
|
const text = draft.trim();
|
|
262
|
-
if (!text) return;
|
|
263
|
-
const message = { text, context };
|
|
373
|
+
if (!text && !images.length) return;
|
|
374
|
+
const message = { text, context, images };
|
|
264
375
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
265
376
|
else if (state.canSend) {
|
|
266
377
|
if (onPending) setDispatching(true);
|
|
267
|
-
onPending?.(text, context);
|
|
268
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
378
|
+
onPending?.(text, context, images);
|
|
379
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
269
380
|
} else return;
|
|
270
381
|
setDraft("");
|
|
271
382
|
setContext([]);
|
|
272
|
-
|
|
383
|
+
setImages([]);
|
|
384
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
273
385
|
};
|
|
274
386
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
275
387
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -279,27 +391,41 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
279
391
|
] }),
|
|
280
392
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
281
393
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
282
|
-
item.text,
|
|
283
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
284
|
-
item.context.length,
|
|
394
|
+
item.text || "Image attachment",
|
|
395
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
396
|
+
item.context.length + item.images.length,
|
|
285
397
|
" attached"
|
|
286
398
|
] }) : null
|
|
287
399
|
] }),
|
|
288
400
|
/* @__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 }) })
|
|
289
401
|
] }, `${index}:${item.text}`))
|
|
290
402
|
] }) : null,
|
|
403
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
404
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
405
|
+
remember(draft, context, next, queue);
|
|
406
|
+
return next;
|
|
407
|
+
}) }),
|
|
291
408
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
292
409
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
293
|
-
remember(draft, next, queue);
|
|
410
|
+
remember(draft, next, images, queue);
|
|
294
411
|
return next;
|
|
295
412
|
}) }),
|
|
296
413
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
297
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
298
|
-
|
|
299
|
-
|
|
414
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
415
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
416
|
+
event.preventDefault();
|
|
417
|
+
setDragging(true);
|
|
418
|
+
}
|
|
419
|
+
}, onDragOver: (event) => {
|
|
420
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
421
|
+
}, onDragLeave: (event) => {
|
|
422
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
423
|
+
}, onDrop: dropImages, children: [
|
|
424
|
+
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,
|
|
425
|
+
/* @__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) => {
|
|
300
426
|
const value = event.currentTarget.value;
|
|
301
427
|
setDraft(value);
|
|
302
|
-
remember(value, context, queue);
|
|
428
|
+
remember(value, context, images, queue);
|
|
303
429
|
}, onKeyDown: (event) => {
|
|
304
430
|
if (isSendKey(event)) {
|
|
305
431
|
event.preventDefault();
|
|
@@ -308,7 +434,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
308
434
|
} }),
|
|
309
435
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
310
436
|
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,
|
|
311
|
-
/* @__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 }) })
|
|
437
|
+
/* @__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 }) })
|
|
312
438
|
] })
|
|
313
439
|
] })
|
|
314
440
|
] });
|
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, ...(intent.context?.length ? { context: intent.context } : {}) });
|
|
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, ...(intent.context?.length ? { context: intent.context } : {}) });
|
|
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 });
|