@volter-ai-dev/supercode-ui 0.1.20 → 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 +7 -4
- package/components.mjs +193 -39
- package/composer.mjs +127 -23
- package/controller.mjs +21 -2
- package/conversation.mjs +213 -195
- package/core.mjs +12 -0
- package/embed.mjs +193 -39
- package/icon.mjs +5 -0
- package/index.d.ts +15 -5
- package/messenger.mjs +193 -39
- package/package.json +1 -1
- package/sessions.mjs +45 -36
- package/styles.css +2 -0
package/core.mjs
CHANGED
|
@@ -491,6 +491,18 @@ function readTranscript(value) {
|
|
|
491
491
|
: [];
|
|
492
492
|
});
|
|
493
493
|
}
|
|
494
|
+
if (Array.isArray(item.images)) {
|
|
495
|
+
entry.images = item.images.flatMap((raw) => {
|
|
496
|
+
const image = record(raw);
|
|
497
|
+
return image && typeof image.label === 'string'
|
|
498
|
+
? [{
|
|
499
|
+
...(typeof image.id === 'string' ? { id: image.id } : {}),
|
|
500
|
+
label: image.label,
|
|
501
|
+
...(typeof image.url === 'string' ? { url: image.url } : {}),
|
|
502
|
+
}]
|
|
503
|
+
: [];
|
|
504
|
+
}).slice(0, 4);
|
|
505
|
+
}
|
|
494
506
|
const request = record(item.request);
|
|
495
507
|
if (request && typeof request.requestKind === 'string' && typeof request.payloadText === 'string') {
|
|
496
508
|
entry.request = {
|
package/embed.mjs
CHANGED
|
@@ -504,6 +504,16 @@ function readTranscript(value) {
|
|
|
504
504
|
}] : [];
|
|
505
505
|
});
|
|
506
506
|
}
|
|
507
|
+
if (Array.isArray(item.images)) {
|
|
508
|
+
entry.images = item.images.flatMap((raw) => {
|
|
509
|
+
const image = record(raw);
|
|
510
|
+
return image && typeof image.label === "string" ? [{
|
|
511
|
+
...typeof image.id === "string" ? { id: image.id } : {},
|
|
512
|
+
label: image.label,
|
|
513
|
+
...typeof image.url === "string" ? { url: image.url } : {}
|
|
514
|
+
}] : [];
|
|
515
|
+
}).slice(0, 4);
|
|
516
|
+
}
|
|
507
517
|
const request = record(item.request);
|
|
508
518
|
if (request && typeof request.requestKind === "string" && typeof request.payloadText === "string") {
|
|
509
519
|
entry.request = {
|
|
@@ -780,6 +790,11 @@ var ICONS = {
|
|
|
780
790
|
/* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
|
|
781
791
|
/* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
782
792
|
] }),
|
|
793
|
+
image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
794
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
|
|
795
|
+
/* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
|
|
796
|
+
/* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
|
|
797
|
+
] }),
|
|
783
798
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
784
799
|
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
785
800
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
@@ -808,6 +823,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
808
823
|
// src/context.jsx
|
|
809
824
|
import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
810
825
|
var MAX_CONTEXT_ITEMS = 32;
|
|
826
|
+
var MAX_IMAGE_ITEMS = 4;
|
|
827
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
828
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
811
829
|
function normalizeContext(value) {
|
|
812
830
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
813
831
|
if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
|
|
@@ -834,6 +852,58 @@ function mergeContext(current, picked) {
|
|
|
834
852
|
}
|
|
835
853
|
return next;
|
|
836
854
|
}
|
|
855
|
+
function normalizeImages(value) {
|
|
856
|
+
const seen = /* @__PURE__ */ new Set();
|
|
857
|
+
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
858
|
+
if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
|
|
859
|
+
const label = item.label.trim().slice(0, 200);
|
|
860
|
+
const url = item.url;
|
|
861
|
+
if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
|
|
862
|
+
seen.add(url);
|
|
863
|
+
return [{
|
|
864
|
+
...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
|
|
865
|
+
label,
|
|
866
|
+
url
|
|
867
|
+
}];
|
|
868
|
+
}).slice(0, MAX_IMAGE_ITEMS);
|
|
869
|
+
}
|
|
870
|
+
function mergeImages(current, picked) {
|
|
871
|
+
const next = [...current];
|
|
872
|
+
const seen = new Set(current.map((item) => item.url));
|
|
873
|
+
for (const item of normalizeImages(picked)) {
|
|
874
|
+
if (seen.has(item.url)) continue;
|
|
875
|
+
seen.add(item.url);
|
|
876
|
+
next.push(item);
|
|
877
|
+
if (next.length === MAX_IMAGE_ITEMS) break;
|
|
878
|
+
}
|
|
879
|
+
return next;
|
|
880
|
+
}
|
|
881
|
+
function partitionAttachments(value) {
|
|
882
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
883
|
+
const context = [];
|
|
884
|
+
const images = [];
|
|
885
|
+
for (const item of values) {
|
|
886
|
+
if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
|
|
887
|
+
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);
|
|
888
|
+
else throw new Error("The attachment picker returned an invalid item.");
|
|
889
|
+
}
|
|
890
|
+
return { context, images };
|
|
891
|
+
}
|
|
892
|
+
async function imageAttachmentsFromFiles(value) {
|
|
893
|
+
const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
|
|
894
|
+
if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
|
|
895
|
+
return Promise.all(files.map(async (file) => {
|
|
896
|
+
if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
|
|
897
|
+
if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
|
|
898
|
+
const url = await new Promise((resolve, reject) => {
|
|
899
|
+
const reader = new FileReader();
|
|
900
|
+
reader.onload = () => resolve(reader.result);
|
|
901
|
+
reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
|
|
902
|
+
reader.readAsDataURL(file);
|
|
903
|
+
});
|
|
904
|
+
return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
|
|
905
|
+
}));
|
|
906
|
+
}
|
|
837
907
|
function ContextTray({ items, onRemove }) {
|
|
838
908
|
if (!items.length) return null;
|
|
839
909
|
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
@@ -842,6 +912,21 @@ function ContextTray({ items, onRemove }) {
|
|
|
842
912
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
|
|
843
913
|
] }, item.id ?? `${item.label}:${index}`)) });
|
|
844
914
|
}
|
|
915
|
+
function ImageTray({ items, onRemove }) {
|
|
916
|
+
if (!items.length) return null;
|
|
917
|
+
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
918
|
+
/* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
|
|
919
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
920
|
+
onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
|
|
921
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
922
|
+
}
|
|
923
|
+
function MessageImages({ items }) {
|
|
924
|
+
if (!items?.length) return null;
|
|
925
|
+
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: [
|
|
926
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
927
|
+
item.label
|
|
928
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
929
|
+
}
|
|
845
930
|
|
|
846
931
|
// src/textarea.js
|
|
847
932
|
import { useLayoutEffect } from "preact/hooks";
|
|
@@ -881,19 +966,23 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
881
966
|
] });
|
|
882
967
|
}
|
|
883
968
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
884
|
-
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
|
|
969
|
+
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
885
970
|
const [draft, setDraft] = useState(remembered.draft);
|
|
886
|
-
const [context, setContext] = useState(remembered.context);
|
|
887
|
-
const [
|
|
971
|
+
const [context, setContext] = useState(remembered.context ?? []);
|
|
972
|
+
const [images, setImages] = useState(remembered.images ?? []);
|
|
973
|
+
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
888
974
|
const [dispatching, setDispatching] = useState(false);
|
|
889
975
|
const [picking, setPicking] = useState(false);
|
|
890
976
|
const [pickerError, setPickerError] = useState(null);
|
|
891
977
|
const textarea = useRef(null);
|
|
892
978
|
useAutosizeTextarea(textarea, draft);
|
|
893
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
979
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
980
|
+
useEffect(() => {
|
|
981
|
+
remember(draft, context, images, queue);
|
|
982
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
894
983
|
const updateQueue = (update) => setQueue((items) => {
|
|
895
984
|
const next = update(items);
|
|
896
|
-
remember(draft, context, next);
|
|
985
|
+
remember(draft, context, images, next);
|
|
897
986
|
return next;
|
|
898
987
|
});
|
|
899
988
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -903,9 +992,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
903
992
|
const [next, ...rest] = queue;
|
|
904
993
|
setDispatching(true);
|
|
905
994
|
setQueue(rest);
|
|
906
|
-
remember(draft, context, rest);
|
|
907
|
-
onPending?.(next.text, next.context);
|
|
908
|
-
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
|
|
995
|
+
remember(draft, context, images, rest);
|
|
996
|
+
onPending?.(next.text, next.context, next.images);
|
|
997
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
909
998
|
}
|
|
910
999
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
911
1000
|
useEffect(() => {
|
|
@@ -918,8 +1007,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
918
1007
|
if (!restoreDraft) return;
|
|
919
1008
|
setDraft(restoreDraft.text);
|
|
920
1009
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
1010
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
921
1011
|
setContext(restoredContext);
|
|
922
|
-
|
|
1012
|
+
setImages(restoredImages);
|
|
1013
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
923
1014
|
textarea.current?.focus({ preventScroll: true });
|
|
924
1015
|
onDraftRestored?.(restoreDraft.id);
|
|
925
1016
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -928,30 +1019,55 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
928
1019
|
return () => clearTimeout(timer);
|
|
929
1020
|
}, [adapter, draft]);
|
|
930
1021
|
const pickContext = () => {
|
|
931
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
1022
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
932
1023
|
setPicking(true);
|
|
933
1024
|
setPickerError(null);
|
|
934
1025
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
1026
|
+
const attachments = partitionAttachments(picked);
|
|
1027
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1028
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
935
1029
|
setContext((current) => {
|
|
936
|
-
const next = mergeContext(current,
|
|
937
|
-
remember(draft, next, queue);
|
|
1030
|
+
const next = mergeContext(current, attachments.context);
|
|
1031
|
+
remember(draft, next, images, queue);
|
|
938
1032
|
return next;
|
|
939
1033
|
});
|
|
940
|
-
|
|
1034
|
+
setImages((current) => {
|
|
1035
|
+
const next = mergeImages(current, attachments.images);
|
|
1036
|
+
remember(draft, context, next, queue);
|
|
1037
|
+
return next;
|
|
1038
|
+
});
|
|
1039
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1040
|
+
};
|
|
1041
|
+
const pasteImages = (event) => {
|
|
1042
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
1043
|
+
if (!files.length) return;
|
|
1044
|
+
event.preventDefault();
|
|
1045
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1046
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1047
|
+
return;
|
|
1048
|
+
}
|
|
1049
|
+
setPicking(true);
|
|
1050
|
+
setPickerError(null);
|
|
1051
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
1052
|
+
const next = mergeImages(current, picked);
|
|
1053
|
+
remember(draft, context, next, queue);
|
|
1054
|
+
return next;
|
|
1055
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
|
|
941
1056
|
};
|
|
942
1057
|
const send = () => {
|
|
943
1058
|
const text = draft.trim();
|
|
944
1059
|
if (!text) return;
|
|
945
|
-
const message = { text, context };
|
|
1060
|
+
const message = { text, context, images };
|
|
946
1061
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
947
1062
|
else if (state.canSend) {
|
|
948
1063
|
if (onPending) setDispatching(true);
|
|
949
|
-
onPending?.(text, context);
|
|
950
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
1064
|
+
onPending?.(text, context, images);
|
|
1065
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
951
1066
|
} else return;
|
|
952
1067
|
setDraft("");
|
|
953
1068
|
setContext([]);
|
|
954
|
-
|
|
1069
|
+
setImages([]);
|
|
1070
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
955
1071
|
};
|
|
956
1072
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
957
1073
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -962,26 +1078,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
962
1078
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
963
1079
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
964
1080
|
item.text,
|
|
965
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
966
|
-
item.context.length,
|
|
1081
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1082
|
+
item.context.length + item.images.length,
|
|
967
1083
|
" attached"
|
|
968
1084
|
] }) : null
|
|
969
1085
|
] }),
|
|
970
1086
|
/* @__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 }) })
|
|
971
1087
|
] }, `${index}:${item.text}`))
|
|
972
1088
|
] }) : null,
|
|
1089
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
1090
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
1091
|
+
remember(draft, context, next, queue);
|
|
1092
|
+
return next;
|
|
1093
|
+
}) }),
|
|
973
1094
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
974
1095
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
975
|
-
remember(draft, next, queue);
|
|
1096
|
+
remember(draft, next, images, queue);
|
|
976
1097
|
return next;
|
|
977
1098
|
}) }),
|
|
978
1099
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
979
1100
|
/* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
|
|
980
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach
|
|
981
|
-
/* @__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, onInput: (event) => {
|
|
1101
|
+
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,
|
|
1102
|
+
/* @__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) => {
|
|
982
1103
|
const value = event.currentTarget.value;
|
|
983
1104
|
setDraft(value);
|
|
984
|
-
remember(value, context, queue);
|
|
1105
|
+
remember(value, context, images, queue);
|
|
985
1106
|
}, onKeyDown: (event) => {
|
|
986
1107
|
if (isSendKey(event)) {
|
|
987
1108
|
event.preventDefault();
|
|
@@ -1383,6 +1504,7 @@ function TranscriptEntry({ entry, state, adapter }) {
|
|
|
1383
1504
|
}
|
|
1384
1505
|
if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
|
|
1385
1506
|
return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
|
|
1507
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
|
|
1386
1508
|
/* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
|
|
1387
1509
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
|
|
1388
1510
|
entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
|
|
@@ -1585,6 +1707,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1585
1707
|
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 })
|
|
1586
1708
|
] }, block.id)),
|
|
1587
1709
|
pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
|
|
1710
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
|
|
1588
1711
|
/* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
|
|
1589
1712
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
|
|
1590
1713
|
/* @__PURE__ */ jsxs4("footer", { children: [
|
|
@@ -1923,9 +2046,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1923
2046
|
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1924
2047
|
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1925
2048
|
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1926
|
-
const beginPending = (text, context = []) => setPending({
|
|
2049
|
+
const beginPending = (text, context = [], images = []) => setPending({
|
|
1927
2050
|
text,
|
|
1928
2051
|
context,
|
|
2052
|
+
images,
|
|
1929
2053
|
status: "sending",
|
|
1930
2054
|
initialError: state.error,
|
|
1931
2055
|
seenBusy: state.busy,
|
|
@@ -1933,13 +2057,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1933
2057
|
});
|
|
1934
2058
|
const retryPending = () => {
|
|
1935
2059
|
if (!pending) return;
|
|
1936
|
-
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
|
|
2060
|
+
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
|
|
1937
2061
|
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1938
2062
|
};
|
|
1939
2063
|
const editPending = () => {
|
|
1940
2064
|
if (!pending) return;
|
|
1941
2065
|
restoreSequence.current += 1;
|
|
1942
|
-
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
|
|
2066
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
|
|
1943
2067
|
setPending({ ...pending, status: "editing" });
|
|
1944
2068
|
};
|
|
1945
2069
|
useEffect6(() => {
|
|
@@ -1985,7 +2109,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1985
2109
|
setPendingAction(null);
|
|
1986
2110
|
}
|
|
1987
2111
|
}, [pendingAction, state.error, state.operation]);
|
|
1988
|
-
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
2112
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1989
2113
|
const action = state.operation || pendingAction?.action || null;
|
|
1990
2114
|
const actionLabel = operationLabel(action);
|
|
1991
2115
|
const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
|
|
@@ -2009,10 +2133,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2009
2133
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2010
2134
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2011
2135
|
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2012
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
|
|
2136
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2013
2137
|
const [harness, setHarness] = useState4(remembered.harness);
|
|
2014
2138
|
const [draft, setDraft] = useState4(remembered.draft);
|
|
2015
2139
|
const [context, setContext] = useState4(remembered.context);
|
|
2140
|
+
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2016
2141
|
const [starting, setStarting] = useState4(null);
|
|
2017
2142
|
const [picking, setPicking] = useState4(false);
|
|
2018
2143
|
const [pickerError, setPickerError] = useState4(null);
|
|
@@ -2023,7 +2148,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2023
2148
|
if (startable.some((item) => item.id === harness)) return;
|
|
2024
2149
|
const next = startable[0]?.id ?? "";
|
|
2025
2150
|
setHarness(next);
|
|
2026
|
-
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
|
|
2151
|
+
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
|
|
2027
2152
|
}, [harness, startableKey]);
|
|
2028
2153
|
useEffect6(() => {
|
|
2029
2154
|
if (!starting) return;
|
|
@@ -2040,16 +2165,40 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2040
2165
|
textarea.current?.focus({ preventScroll: true });
|
|
2041
2166
|
}, []);
|
|
2042
2167
|
const pickContext = () => {
|
|
2043
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
2168
|
+
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2044
2169
|
setPicking(true);
|
|
2045
2170
|
setPickerError(null);
|
|
2046
2171
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
2172
|
+
const attachments = partitionAttachments(picked);
|
|
2173
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2174
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2047
2175
|
setContext((current) => {
|
|
2048
|
-
const next = mergeContext(current,
|
|
2049
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2176
|
+
const next = mergeContext(current, attachments.context);
|
|
2177
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2050
2178
|
return next;
|
|
2051
2179
|
});
|
|
2052
|
-
|
|
2180
|
+
setImages((current) => {
|
|
2181
|
+
const next = mergeImages(current, attachments.images);
|
|
2182
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2183
|
+
return next;
|
|
2184
|
+
});
|
|
2185
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2186
|
+
};
|
|
2187
|
+
const pasteImages = (event) => {
|
|
2188
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
2189
|
+
if (!files.length) return;
|
|
2190
|
+
event.preventDefault();
|
|
2191
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2192
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2193
|
+
return;
|
|
2194
|
+
}
|
|
2195
|
+
setPicking(true);
|
|
2196
|
+
setPickerError(null);
|
|
2197
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2198
|
+
const next = mergeImages(current, picked);
|
|
2199
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2200
|
+
return next;
|
|
2201
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
|
|
2053
2202
|
};
|
|
2054
2203
|
const send = () => {
|
|
2055
2204
|
const text = draft.trim();
|
|
@@ -2057,7 +2206,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2057
2206
|
const id = startSequence.current + 1;
|
|
2058
2207
|
startSequence.current = id;
|
|
2059
2208
|
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2060
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {} });
|
|
2209
|
+
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2061
2210
|
if (result && typeof result.then === "function") {
|
|
2062
2211
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2063
2212
|
}
|
|
@@ -2087,24 +2236,29 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2087
2236
|
/* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2088
2237
|
const value = event.currentTarget.value;
|
|
2089
2238
|
setHarness(value);
|
|
2090
|
-
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
|
|
2239
|
+
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
|
|
2091
2240
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
|
|
2092
2241
|
item.label,
|
|
2093
2242
|
item.startable ? "" : " \xB7 unavailable"
|
|
2094
2243
|
] }, item.id)) })
|
|
2095
2244
|
] }),
|
|
2245
|
+
/* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2246
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2247
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2248
|
+
return next;
|
|
2249
|
+
}) }),
|
|
2096
2250
|
/* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2097
2251
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2098
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2252
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2099
2253
|
return next;
|
|
2100
2254
|
}) }),
|
|
2101
2255
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2102
2256
|
/* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
|
|
2103
|
-
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach
|
|
2104
|
-
/* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onInput: (event) => {
|
|
2257
|
+
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2258
|
+
/* @__PURE__ */ jsx8("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2105
2259
|
const value = event.currentTarget.value;
|
|
2106
2260
|
setDraft(value);
|
|
2107
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
|
|
2261
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
|
|
2108
2262
|
}, onKeyDown: (event) => {
|
|
2109
2263
|
if (isSendKey(event)) {
|
|
2110
2264
|
event.preventDefault();
|
package/icon.mjs
CHANGED
|
@@ -17,6 +17,11 @@ var ICONS = {
|
|
|
17
17
|
/* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
|
|
18
18
|
/* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
19
19
|
] }),
|
|
20
|
+
image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
21
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
|
|
22
|
+
/* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
|
|
23
|
+
/* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
|
|
24
|
+
] }),
|
|
20
25
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
21
26
|
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
22
27
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
package/index.d.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface TranscriptContext {
|
|
|
46
46
|
detail: string;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
export interface TranscriptImage {
|
|
50
|
+
id?: string;
|
|
51
|
+
label: string;
|
|
52
|
+
url?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type TranscriptAttachment = TranscriptContext | (TranscriptImage & { url: string });
|
|
56
|
+
|
|
49
57
|
export interface RequestOption {
|
|
50
58
|
optionId: string;
|
|
51
59
|
name: string;
|
|
@@ -78,6 +86,7 @@ export interface TranscriptEntryModel {
|
|
|
78
86
|
request?: TranscriptRequest;
|
|
79
87
|
code?: string;
|
|
80
88
|
context?: TranscriptContext[];
|
|
89
|
+
images?: TranscriptImage[];
|
|
81
90
|
presentation?: ToolPresentationModel;
|
|
82
91
|
}
|
|
83
92
|
|
|
@@ -211,8 +220,8 @@ export type SupercodeUiIntent =
|
|
|
211
220
|
| { action: 'loadSessions' }
|
|
212
221
|
| { action: 'loadEarlier' }
|
|
213
222
|
| { action: 'draft'; text: string }
|
|
214
|
-
| { action: 'send'; text: string; context?: TranscriptContext[] }
|
|
215
|
-
| { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[] }
|
|
223
|
+
| { action: 'send'; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
224
|
+
| { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
216
225
|
| { action: 'resume' }
|
|
217
226
|
| { action: 'join' }
|
|
218
227
|
| { action: 'detach' }
|
|
@@ -227,8 +236,8 @@ export type SupercodeUiIntent =
|
|
|
227
236
|
|
|
228
237
|
export interface UiAdapter {
|
|
229
238
|
onIntent(intent: SupercodeUiIntent): void | Promise<void>;
|
|
230
|
-
/** Ask the embedding host for explicit user-selected text context. */
|
|
231
|
-
pickContext?():
|
|
239
|
+
/** Ask the embedding host for explicit user-selected text or image context. */
|
|
240
|
+
pickContext?(): TranscriptAttachment | TranscriptAttachment[] | null | Promise<TranscriptAttachment | TranscriptAttachment[] | null>;
|
|
232
241
|
onClose?(): void;
|
|
233
242
|
onOpen?(): void;
|
|
234
243
|
copyText?(value: string): void | Promise<void>;
|
|
@@ -248,6 +257,7 @@ export interface MessengerLabels {
|
|
|
248
257
|
export interface PendingMessageModel {
|
|
249
258
|
text: string;
|
|
250
259
|
context?: TranscriptContext[];
|
|
260
|
+
images?: TranscriptImage[];
|
|
251
261
|
status: 'sending' | 'failed';
|
|
252
262
|
onRetry?(): void;
|
|
253
263
|
onEdit?(): void;
|
|
@@ -358,7 +368,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
|
|
|
358
368
|
export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void }): VNode;
|
|
359
369
|
export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string }): VNode;
|
|
360
370
|
export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
|
|
361
|
-
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[] } | null; onPending?(text: string, context?: TranscriptContext[]): void; onDraftRestored?(id: string | number): void }): VNode;
|
|
371
|
+
export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): VNode;
|
|
362
372
|
export function SupercodeMessenger(props: MessengerProps): VNode;
|
|
363
373
|
|
|
364
374
|
export interface MountedMessenger {
|