@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/messenger.mjs
CHANGED
|
@@ -501,6 +501,16 @@ function readTranscript(value) {
|
|
|
501
501
|
}] : [];
|
|
502
502
|
});
|
|
503
503
|
}
|
|
504
|
+
if (Array.isArray(item.images)) {
|
|
505
|
+
entry.images = item.images.flatMap((raw) => {
|
|
506
|
+
const image = record(raw);
|
|
507
|
+
return image && typeof image.label === "string" ? [{
|
|
508
|
+
...typeof image.id === "string" ? { id: image.id } : {},
|
|
509
|
+
label: image.label,
|
|
510
|
+
...typeof image.url === "string" ? { url: image.url } : {}
|
|
511
|
+
}] : [];
|
|
512
|
+
}).slice(0, 4);
|
|
513
|
+
}
|
|
504
514
|
const request = record(item.request);
|
|
505
515
|
if (request && typeof request.requestKind === "string" && typeof request.payloadText === "string") {
|
|
506
516
|
entry.request = {
|
|
@@ -777,6 +787,11 @@ var ICONS = {
|
|
|
777
787
|
/* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
|
|
778
788
|
/* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
779
789
|
] }),
|
|
790
|
+
image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
791
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
|
|
792
|
+
/* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
|
|
793
|
+
/* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
|
|
794
|
+
] }),
|
|
780
795
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
781
796
|
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
782
797
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
@@ -805,6 +820,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
805
820
|
// src/context.jsx
|
|
806
821
|
import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
807
822
|
var MAX_CONTEXT_ITEMS = 32;
|
|
823
|
+
var MAX_IMAGE_ITEMS = 4;
|
|
824
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
825
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
808
826
|
function normalizeContext(value) {
|
|
809
827
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
810
828
|
if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
|
|
@@ -831,6 +849,58 @@ function mergeContext(current, picked) {
|
|
|
831
849
|
}
|
|
832
850
|
return next;
|
|
833
851
|
}
|
|
852
|
+
function normalizeImages(value) {
|
|
853
|
+
const seen = /* @__PURE__ */ new Set();
|
|
854
|
+
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
855
|
+
if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
|
|
856
|
+
const label = item.label.trim().slice(0, 200);
|
|
857
|
+
const url = item.url;
|
|
858
|
+
if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
|
|
859
|
+
seen.add(url);
|
|
860
|
+
return [{
|
|
861
|
+
...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
|
|
862
|
+
label,
|
|
863
|
+
url
|
|
864
|
+
}];
|
|
865
|
+
}).slice(0, MAX_IMAGE_ITEMS);
|
|
866
|
+
}
|
|
867
|
+
function mergeImages(current, picked) {
|
|
868
|
+
const next = [...current];
|
|
869
|
+
const seen = new Set(current.map((item) => item.url));
|
|
870
|
+
for (const item of normalizeImages(picked)) {
|
|
871
|
+
if (seen.has(item.url)) continue;
|
|
872
|
+
seen.add(item.url);
|
|
873
|
+
next.push(item);
|
|
874
|
+
if (next.length === MAX_IMAGE_ITEMS) break;
|
|
875
|
+
}
|
|
876
|
+
return next;
|
|
877
|
+
}
|
|
878
|
+
function partitionAttachments(value) {
|
|
879
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
880
|
+
const context = [];
|
|
881
|
+
const images = [];
|
|
882
|
+
for (const item of values) {
|
|
883
|
+
if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
|
|
884
|
+
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);
|
|
885
|
+
else throw new Error("The attachment picker returned an invalid item.");
|
|
886
|
+
}
|
|
887
|
+
return { context, images };
|
|
888
|
+
}
|
|
889
|
+
async function imageAttachmentsFromFiles(value) {
|
|
890
|
+
const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
|
|
891
|
+
if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
|
|
892
|
+
return Promise.all(files.map(async (file) => {
|
|
893
|
+
if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
|
|
894
|
+
if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
|
|
895
|
+
const url = await new Promise((resolve, reject) => {
|
|
896
|
+
const reader = new FileReader();
|
|
897
|
+
reader.onload = () => resolve(reader.result);
|
|
898
|
+
reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
|
|
899
|
+
reader.readAsDataURL(file);
|
|
900
|
+
});
|
|
901
|
+
return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
|
|
902
|
+
}));
|
|
903
|
+
}
|
|
834
904
|
function ContextTray({ items, onRemove }) {
|
|
835
905
|
if (!items.length) return null;
|
|
836
906
|
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
@@ -839,6 +909,21 @@ function ContextTray({ items, onRemove }) {
|
|
|
839
909
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
|
|
840
910
|
] }, item.id ?? `${item.label}:${index}`)) });
|
|
841
911
|
}
|
|
912
|
+
function ImageTray({ items, onRemove }) {
|
|
913
|
+
if (!items.length) return null;
|
|
914
|
+
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
915
|
+
/* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
|
|
916
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
917
|
+
onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
|
|
918
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
919
|
+
}
|
|
920
|
+
function MessageImages({ items }) {
|
|
921
|
+
if (!items?.length) return null;
|
|
922
|
+
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: [
|
|
923
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
924
|
+
item.label
|
|
925
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
926
|
+
}
|
|
842
927
|
|
|
843
928
|
// src/textarea.js
|
|
844
929
|
import { useLayoutEffect } from "preact/hooks";
|
|
@@ -878,19 +963,24 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
878
963
|
] });
|
|
879
964
|
}
|
|
880
965
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
881
|
-
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
|
|
966
|
+
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
882
967
|
const [draft, setDraft] = useState(remembered.draft);
|
|
883
|
-
const [context, setContext] = useState(remembered.context);
|
|
884
|
-
const [
|
|
968
|
+
const [context, setContext] = useState(remembered.context ?? []);
|
|
969
|
+
const [images, setImages] = useState(remembered.images ?? []);
|
|
970
|
+
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
885
971
|
const [dispatching, setDispatching] = useState(false);
|
|
886
972
|
const [picking, setPicking] = useState(false);
|
|
973
|
+
const [dragging, setDragging] = useState(false);
|
|
887
974
|
const [pickerError, setPickerError] = useState(null);
|
|
888
975
|
const textarea = useRef(null);
|
|
889
976
|
useAutosizeTextarea(textarea, draft);
|
|
890
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
977
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
978
|
+
useEffect(() => {
|
|
979
|
+
remember(draft, context, images, queue);
|
|
980
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
891
981
|
const updateQueue = (update) => setQueue((items) => {
|
|
892
982
|
const next = update(items);
|
|
893
|
-
remember(draft, context, next);
|
|
983
|
+
remember(draft, context, images, next);
|
|
894
984
|
return next;
|
|
895
985
|
});
|
|
896
986
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -900,9 +990,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
900
990
|
const [next, ...rest] = queue;
|
|
901
991
|
setDispatching(true);
|
|
902
992
|
setQueue(rest);
|
|
903
|
-
remember(draft, context, rest);
|
|
904
|
-
onPending?.(next.text, next.context);
|
|
905
|
-
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
|
|
993
|
+
remember(draft, context, images, rest);
|
|
994
|
+
onPending?.(next.text, next.context, next.images);
|
|
995
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
906
996
|
}
|
|
907
997
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
908
998
|
useEffect(() => {
|
|
@@ -915,8 +1005,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
915
1005
|
if (!restoreDraft) return;
|
|
916
1006
|
setDraft(restoreDraft.text);
|
|
917
1007
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
1008
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
918
1009
|
setContext(restoredContext);
|
|
919
|
-
|
|
1010
|
+
setImages(restoredImages);
|
|
1011
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
920
1012
|
textarea.current?.focus({ preventScroll: true });
|
|
921
1013
|
onDraftRestored?.(restoreDraft.id);
|
|
922
1014
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -925,30 +1017,67 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
925
1017
|
return () => clearTimeout(timer);
|
|
926
1018
|
}, [adapter, draft]);
|
|
927
1019
|
const pickContext = () => {
|
|
928
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
1020
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
929
1021
|
setPicking(true);
|
|
930
1022
|
setPickerError(null);
|
|
931
1023
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
1024
|
+
const attachments = partitionAttachments(picked);
|
|
1025
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1026
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
932
1027
|
setContext((current) => {
|
|
933
|
-
const next = mergeContext(current,
|
|
934
|
-
remember(draft, next, queue);
|
|
1028
|
+
const next = mergeContext(current, attachments.context);
|
|
1029
|
+
remember(draft, next, images, queue);
|
|
935
1030
|
return next;
|
|
936
1031
|
});
|
|
937
|
-
|
|
1032
|
+
setImages((current) => {
|
|
1033
|
+
const next = mergeImages(current, attachments.images);
|
|
1034
|
+
remember(draft, context, next, queue);
|
|
1035
|
+
return next;
|
|
1036
|
+
});
|
|
1037
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1038
|
+
};
|
|
1039
|
+
const addImageFiles = (value, source) => {
|
|
1040
|
+
const allFiles = Array.from(value ?? []);
|
|
1041
|
+
if (!allFiles.length) return false;
|
|
1042
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
1043
|
+
if (files.length !== allFiles.length) {
|
|
1044
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
1045
|
+
return true;
|
|
1046
|
+
}
|
|
1047
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1048
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1049
|
+
return true;
|
|
1050
|
+
}
|
|
1051
|
+
setPicking(true);
|
|
1052
|
+
setPickerError(null);
|
|
1053
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
1054
|
+
const next = mergeImages(current, picked);
|
|
1055
|
+
remember(draft, context, next, queue);
|
|
1056
|
+
return next;
|
|
1057
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
1058
|
+
return true;
|
|
1059
|
+
};
|
|
1060
|
+
const pasteImages = (event) => {
|
|
1061
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
1062
|
+
};
|
|
1063
|
+
const dropImages = (event) => {
|
|
1064
|
+
setDragging(false);
|
|
1065
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
938
1066
|
};
|
|
939
1067
|
const send = () => {
|
|
940
1068
|
const text = draft.trim();
|
|
941
|
-
if (!text) return;
|
|
942
|
-
const message = { text, context };
|
|
1069
|
+
if (!text && !images.length) return;
|
|
1070
|
+
const message = { text, context, images };
|
|
943
1071
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
944
1072
|
else if (state.canSend) {
|
|
945
1073
|
if (onPending) setDispatching(true);
|
|
946
|
-
onPending?.(text, context);
|
|
947
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
1074
|
+
onPending?.(text, context, images);
|
|
1075
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
948
1076
|
} else return;
|
|
949
1077
|
setDraft("");
|
|
950
1078
|
setContext([]);
|
|
951
|
-
|
|
1079
|
+
setImages([]);
|
|
1080
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
952
1081
|
};
|
|
953
1082
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
954
1083
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -958,27 +1087,41 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
958
1087
|
] }),
|
|
959
1088
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
960
1089
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
961
|
-
item.text,
|
|
962
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
963
|
-
item.context.length,
|
|
1090
|
+
item.text || "Image attachment",
|
|
1091
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1092
|
+
item.context.length + item.images.length,
|
|
964
1093
|
" attached"
|
|
965
1094
|
] }) : null
|
|
966
1095
|
] }),
|
|
967
1096
|
/* @__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 }) })
|
|
968
1097
|
] }, `${index}:${item.text}`))
|
|
969
1098
|
] }) : null,
|
|
1099
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
1100
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
1101
|
+
remember(draft, context, next, queue);
|
|
1102
|
+
return next;
|
|
1103
|
+
}) }),
|
|
970
1104
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
971
1105
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
972
|
-
remember(draft, next, queue);
|
|
1106
|
+
remember(draft, next, images, queue);
|
|
973
1107
|
return next;
|
|
974
1108
|
}) }),
|
|
975
1109
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
976
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
977
|
-
|
|
978
|
-
|
|
1110
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
1111
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
1112
|
+
event.preventDefault();
|
|
1113
|
+
setDragging(true);
|
|
1114
|
+
}
|
|
1115
|
+
}, onDragOver: (event) => {
|
|
1116
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1117
|
+
}, onDragLeave: (event) => {
|
|
1118
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1119
|
+
}, onDrop: dropImages, children: [
|
|
1120
|
+
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,
|
|
1121
|
+
/* @__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) => {
|
|
979
1122
|
const value = event.currentTarget.value;
|
|
980
1123
|
setDraft(value);
|
|
981
|
-
remember(value, context, queue);
|
|
1124
|
+
remember(value, context, images, queue);
|
|
982
1125
|
}, onKeyDown: (event) => {
|
|
983
1126
|
if (isSendKey(event)) {
|
|
984
1127
|
event.preventDefault();
|
|
@@ -987,7 +1130,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
987
1130
|
} }),
|
|
988
1131
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
989
1132
|
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,
|
|
990
|
-
/* @__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 }) })
|
|
1133
|
+
/* @__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 }) })
|
|
991
1134
|
] })
|
|
992
1135
|
] })
|
|
993
1136
|
] });
|
|
@@ -1380,6 +1523,7 @@ function TranscriptEntry({ entry, state, adapter }) {
|
|
|
1380
1523
|
}
|
|
1381
1524
|
if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
|
|
1382
1525
|
return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
|
|
1526
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
|
|
1383
1527
|
/* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
|
|
1384
1528
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
|
|
1385
1529
|
entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
|
|
@@ -1582,6 +1726,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1582
1726
|
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 })
|
|
1583
1727
|
] }, block.id)),
|
|
1584
1728
|
pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
|
|
1729
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
|
|
1585
1730
|
/* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
|
|
1586
1731
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
|
|
1587
1732
|
/* @__PURE__ */ jsxs4("footer", { children: [
|
|
@@ -1920,9 +2065,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1920
2065
|
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1921
2066
|
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1922
2067
|
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1923
|
-
const beginPending = (text, context = []) => setPending({
|
|
2068
|
+
const beginPending = (text, context = [], images = []) => setPending({
|
|
1924
2069
|
text,
|
|
1925
2070
|
context,
|
|
2071
|
+
images,
|
|
1926
2072
|
status: "sending",
|
|
1927
2073
|
initialError: state.error,
|
|
1928
2074
|
seenBusy: state.busy,
|
|
@@ -1930,13 +2076,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1930
2076
|
});
|
|
1931
2077
|
const retryPending = () => {
|
|
1932
2078
|
if (!pending) return;
|
|
1933
|
-
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
|
|
2079
|
+
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
|
|
1934
2080
|
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1935
2081
|
};
|
|
1936
2082
|
const editPending = () => {
|
|
1937
2083
|
if (!pending) return;
|
|
1938
2084
|
restoreSequence.current += 1;
|
|
1939
|
-
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
|
|
2085
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
|
|
1940
2086
|
setPending({ ...pending, status: "editing" });
|
|
1941
2087
|
};
|
|
1942
2088
|
useEffect6(() => {
|
|
@@ -1982,7 +2128,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1982
2128
|
setPendingAction(null);
|
|
1983
2129
|
}
|
|
1984
2130
|
}, [pendingAction, state.error, state.operation]);
|
|
1985
|
-
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
2131
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1986
2132
|
const action = state.operation || pendingAction?.action || null;
|
|
1987
2133
|
const actionLabel = operationLabel(action);
|
|
1988
2134
|
const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
|
|
@@ -2006,12 +2152,14 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2006
2152
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2007
2153
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2008
2154
|
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2009
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
|
|
2155
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2010
2156
|
const [harness, setHarness] = useState4(remembered.harness);
|
|
2011
2157
|
const [draft, setDraft] = useState4(remembered.draft);
|
|
2012
2158
|
const [context, setContext] = useState4(remembered.context);
|
|
2159
|
+
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2013
2160
|
const [starting, setStarting] = useState4(null);
|
|
2014
2161
|
const [picking, setPicking] = useState4(false);
|
|
2162
|
+
const [dragging, setDragging] = useState4(false);
|
|
2015
2163
|
const [pickerError, setPickerError] = useState4(null);
|
|
2016
2164
|
const startSequence = useRef5(0);
|
|
2017
2165
|
const textarea = useRef5(null);
|
|
@@ -2020,7 +2168,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2020
2168
|
if (startable.some((item) => item.id === harness)) return;
|
|
2021
2169
|
const next = startable[0]?.id ?? "";
|
|
2022
2170
|
setHarness(next);
|
|
2023
|
-
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
|
|
2171
|
+
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
|
|
2024
2172
|
}, [harness, startableKey]);
|
|
2025
2173
|
useEffect6(() => {
|
|
2026
2174
|
if (!starting) return;
|
|
@@ -2037,24 +2185,60 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2037
2185
|
textarea.current?.focus({ preventScroll: true });
|
|
2038
2186
|
}, []);
|
|
2039
2187
|
const pickContext = () => {
|
|
2040
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
2188
|
+
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2041
2189
|
setPicking(true);
|
|
2042
2190
|
setPickerError(null);
|
|
2043
2191
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
2192
|
+
const attachments = partitionAttachments(picked);
|
|
2193
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2194
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2044
2195
|
setContext((current) => {
|
|
2045
|
-
const next = mergeContext(current,
|
|
2046
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2196
|
+
const next = mergeContext(current, attachments.context);
|
|
2197
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2198
|
+
return next;
|
|
2199
|
+
});
|
|
2200
|
+
setImages((current) => {
|
|
2201
|
+
const next = mergeImages(current, attachments.images);
|
|
2202
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2047
2203
|
return next;
|
|
2048
2204
|
});
|
|
2049
|
-
}
|
|
2205
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2206
|
+
};
|
|
2207
|
+
const addImageFiles = (value, source) => {
|
|
2208
|
+
const allFiles = Array.from(value ?? []);
|
|
2209
|
+
if (!allFiles.length) return false;
|
|
2210
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2211
|
+
if (files.length !== allFiles.length) {
|
|
2212
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
2213
|
+
return true;
|
|
2214
|
+
}
|
|
2215
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2216
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2217
|
+
return true;
|
|
2218
|
+
}
|
|
2219
|
+
setPicking(true);
|
|
2220
|
+
setPickerError(null);
|
|
2221
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2222
|
+
const next = mergeImages(current, picked);
|
|
2223
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2224
|
+
return next;
|
|
2225
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2226
|
+
return true;
|
|
2227
|
+
};
|
|
2228
|
+
const pasteImages = (event) => {
|
|
2229
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
2230
|
+
};
|
|
2231
|
+
const dropImages = (event) => {
|
|
2232
|
+
setDragging(false);
|
|
2233
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
2050
2234
|
};
|
|
2051
2235
|
const send = () => {
|
|
2052
2236
|
const text = draft.trim();
|
|
2053
|
-
if (!text || !harness || starting) return;
|
|
2237
|
+
if (!text && !images.length || !harness || starting) return;
|
|
2054
2238
|
const id = startSequence.current + 1;
|
|
2055
2239
|
startSequence.current = id;
|
|
2056
2240
|
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2057
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {} });
|
|
2241
|
+
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2058
2242
|
if (result && typeof result.then === "function") {
|
|
2059
2243
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2060
2244
|
}
|
|
@@ -2084,31 +2268,45 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2084
2268
|
/* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2085
2269
|
const value = event.currentTarget.value;
|
|
2086
2270
|
setHarness(value);
|
|
2087
|
-
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
|
|
2271
|
+
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
|
|
2088
2272
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
|
|
2089
2273
|
item.label,
|
|
2090
2274
|
item.startable ? "" : " \xB7 unavailable"
|
|
2091
2275
|
] }, item.id)) })
|
|
2092
2276
|
] }),
|
|
2277
|
+
/* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2278
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2279
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2280
|
+
return next;
|
|
2281
|
+
}) }),
|
|
2093
2282
|
/* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2094
2283
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2095
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2284
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2096
2285
|
return next;
|
|
2097
2286
|
}) }),
|
|
2098
2287
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2099
|
-
/* @__PURE__ */ jsxs7("div", { class:
|
|
2100
|
-
|
|
2101
|
-
|
|
2288
|
+
/* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2289
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2290
|
+
event.preventDefault();
|
|
2291
|
+
setDragging(true);
|
|
2292
|
+
}
|
|
2293
|
+
}, onDragOver: (event) => {
|
|
2294
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
2295
|
+
}, onDragLeave: (event) => {
|
|
2296
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2297
|
+
}, onDrop: dropImages, children: [
|
|
2298
|
+
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,
|
|
2299
|
+
/* @__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) => {
|
|
2102
2300
|
const value = event.currentTarget.value;
|
|
2103
2301
|
setDraft(value);
|
|
2104
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
|
|
2302
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
|
|
2105
2303
|
}, onKeyDown: (event) => {
|
|
2106
2304
|
if (isSendKey(event)) {
|
|
2107
2305
|
event.preventDefault();
|
|
2108
2306
|
send();
|
|
2109
2307
|
}
|
|
2110
2308
|
} }),
|
|
2111
|
-
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2309
|
+
/* @__PURE__ */ jsx8("span", { children: /* @__PURE__ */ jsx8("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx8("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx8(UiIcon, { name: "send", size: 17 }) }) })
|
|
2112
2310
|
] })
|
|
2113
2311
|
] })
|
|
2114
2312
|
] });
|