@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/README.md +9 -0
- package/components.mjs +602 -348
- package/composer.mjs +209 -34
- package/controller.mjs +21 -2
- package/conversation.mjs +226 -201
- package/core.mjs +13 -1
- package/embed.mjs +604 -350
- package/icon.mjs +6 -0
- package/index.d.ts +17 -4
- package/messenger.mjs +602 -348
- package/package.json +1 -1
- package/sessions.mjs +47 -37
- package/styles.css +5 -1
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
|
|
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__ */
|
|
151
|
-
/* @__PURE__ */
|
|
152
|
-
/* @__PURE__ */
|
|
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__ */
|
|
155
|
-
/* @__PURE__ */
|
|
156
|
-
/* @__PURE__ */
|
|
157
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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 [
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
+
setContext([]);
|
|
370
|
+
setImages([]);
|
|
371
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
215
372
|
};
|
|
216
|
-
return /* @__PURE__ */
|
|
217
|
-
queue.length ? /* @__PURE__ */
|
|
218
|
-
/* @__PURE__ */
|
|
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__ */
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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__ */
|
|
228
|
-
|
|
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__ */
|
|
239
|
-
state.busy ? /* @__PURE__ */
|
|
240
|
-
/* @__PURE__ */
|
|
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 });
|