@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/README.md
CHANGED
|
@@ -41,7 +41,7 @@ const mounted = mountSupercodeMessenger(document.querySelector('#agent'), {
|
|
|
41
41
|
host.send(intent);
|
|
42
42
|
},
|
|
43
43
|
async pickContext() {
|
|
44
|
-
return host.
|
|
44
|
+
return host.pickFilesAndImages();
|
|
45
45
|
},
|
|
46
46
|
onClose() {
|
|
47
47
|
panel.close();
|
|
@@ -124,9 +124,13 @@ errors, and owned/attached identities without reimplementing transcript or capab
|
|
|
124
124
|
`SupercodeController` snapshots and persisted inventory into it, then handles `SupercodeUiIntent`.
|
|
125
125
|
The browser cannot supply locators, credentials, policy, environment variables, or arbitrary
|
|
126
126
|
materialization paths. Session keys and target harnesses must be revalidated by the host.
|
|
127
|
-
`pickContext` is optional and host-owned: the default composer shows
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
`pickContext` is optional and host-owned: the default composer shows one attachment control when
|
|
128
|
+
this callback exists and accepts either typed text context (`detail`) or native image inputs
|
|
129
|
+
(`url`). Images can also be pasted or dropped directly, and an image-only turn is sent without
|
|
130
|
+
inventing fallback prompt text. The composer bounds images to four supported browser formats at
|
|
131
|
+
5 MB each, previews them locally, and keeps both attachment kinds associated with queued
|
|
132
|
+
messages, retry, edit, and new-chat recovery. The host still chooses how explicit file selection
|
|
133
|
+
is exposed and must revalidate every returned item.
|
|
130
134
|
|
|
131
135
|
An embedding product such as Vibewaiting should therefore be small: Lucarne owns its iframe and
|
|
132
136
|
launcher lifecycle, Supercode owns this UI and the controller semantics, and Vibewaiting only
|
package/components.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 = {
|
|
@@ -774,6 +784,11 @@ var ICONS = {
|
|
|
774
784
|
/* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
|
|
775
785
|
/* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
776
786
|
] }),
|
|
787
|
+
image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
788
|
+
/* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
|
|
789
|
+
/* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
|
|
790
|
+
/* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
|
|
791
|
+
] }),
|
|
777
792
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
778
793
|
/* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
779
794
|
/* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
@@ -802,6 +817,9 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
802
817
|
// src/context.jsx
|
|
803
818
|
import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
804
819
|
var MAX_CONTEXT_ITEMS = 32;
|
|
820
|
+
var MAX_IMAGE_ITEMS = 4;
|
|
821
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
822
|
+
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
805
823
|
function normalizeContext(value) {
|
|
806
824
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
807
825
|
if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
|
|
@@ -828,6 +846,58 @@ function mergeContext(current, picked) {
|
|
|
828
846
|
}
|
|
829
847
|
return next;
|
|
830
848
|
}
|
|
849
|
+
function normalizeImages(value) {
|
|
850
|
+
const seen = /* @__PURE__ */ new Set();
|
|
851
|
+
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
852
|
+
if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
|
|
853
|
+
const label = item.label.trim().slice(0, 200);
|
|
854
|
+
const url = item.url;
|
|
855
|
+
if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
|
|
856
|
+
seen.add(url);
|
|
857
|
+
return [{
|
|
858
|
+
...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
|
|
859
|
+
label,
|
|
860
|
+
url
|
|
861
|
+
}];
|
|
862
|
+
}).slice(0, MAX_IMAGE_ITEMS);
|
|
863
|
+
}
|
|
864
|
+
function mergeImages(current, picked) {
|
|
865
|
+
const next = [...current];
|
|
866
|
+
const seen = new Set(current.map((item) => item.url));
|
|
867
|
+
for (const item of normalizeImages(picked)) {
|
|
868
|
+
if (seen.has(item.url)) continue;
|
|
869
|
+
seen.add(item.url);
|
|
870
|
+
next.push(item);
|
|
871
|
+
if (next.length === MAX_IMAGE_ITEMS) break;
|
|
872
|
+
}
|
|
873
|
+
return next;
|
|
874
|
+
}
|
|
875
|
+
function partitionAttachments(value) {
|
|
876
|
+
const values = Array.isArray(value) ? value : value ? [value] : [];
|
|
877
|
+
const context = [];
|
|
878
|
+
const images = [];
|
|
879
|
+
for (const item of values) {
|
|
880
|
+
if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
|
|
881
|
+
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);
|
|
882
|
+
else throw new Error("The attachment picker returned an invalid item.");
|
|
883
|
+
}
|
|
884
|
+
return { context, images };
|
|
885
|
+
}
|
|
886
|
+
async function imageAttachmentsFromFiles(value) {
|
|
887
|
+
const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
|
|
888
|
+
if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
|
|
889
|
+
return Promise.all(files.map(async (file) => {
|
|
890
|
+
if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
|
|
891
|
+
if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
|
|
892
|
+
const url = await new Promise((resolve, reject) => {
|
|
893
|
+
const reader = new FileReader();
|
|
894
|
+
reader.onload = () => resolve(reader.result);
|
|
895
|
+
reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
|
|
896
|
+
reader.readAsDataURL(file);
|
|
897
|
+
});
|
|
898
|
+
return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
|
|
899
|
+
}));
|
|
900
|
+
}
|
|
831
901
|
function ContextTray({ items, onRemove }) {
|
|
832
902
|
if (!items.length) return null;
|
|
833
903
|
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
@@ -836,6 +906,21 @@ function ContextTray({ items, onRemove }) {
|
|
|
836
906
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
|
|
837
907
|
] }, item.id ?? `${item.label}:${index}`)) });
|
|
838
908
|
}
|
|
909
|
+
function ImageTray({ items, onRemove }) {
|
|
910
|
+
if (!items.length) return null;
|
|
911
|
+
return /* @__PURE__ */ jsx2("div", { class: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
912
|
+
/* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
|
|
913
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
914
|
+
onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
|
|
915
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
916
|
+
}
|
|
917
|
+
function MessageImages({ items }) {
|
|
918
|
+
if (!items?.length) return null;
|
|
919
|
+
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: [
|
|
920
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
921
|
+
item.label
|
|
922
|
+
] }, item.id ?? `${item.label}:${index}`)) });
|
|
923
|
+
}
|
|
839
924
|
|
|
840
925
|
// src/textarea.js
|
|
841
926
|
import { useLayoutEffect } from "preact/hooks";
|
|
@@ -875,19 +960,24 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
875
960
|
] });
|
|
876
961
|
}
|
|
877
962
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
878
|
-
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], queue: [] };
|
|
963
|
+
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
879
964
|
const [draft, setDraft] = useState(remembered.draft);
|
|
880
|
-
const [context, setContext] = useState(remembered.context);
|
|
881
|
-
const [
|
|
965
|
+
const [context, setContext] = useState(remembered.context ?? []);
|
|
966
|
+
const [images, setImages] = useState(remembered.images ?? []);
|
|
967
|
+
const [queue, setQueue] = useState((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
882
968
|
const [dispatching, setDispatching] = useState(false);
|
|
883
969
|
const [picking, setPicking] = useState(false);
|
|
970
|
+
const [dragging, setDragging] = useState(false);
|
|
884
971
|
const [pickerError, setPickerError] = useState(null);
|
|
885
972
|
const textarea = useRef(null);
|
|
886
973
|
useAutosizeTextarea(textarea, draft);
|
|
887
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
974
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
975
|
+
useEffect(() => {
|
|
976
|
+
remember(draft, context, images, queue);
|
|
977
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
888
978
|
const updateQueue = (update) => setQueue((items) => {
|
|
889
979
|
const next = update(items);
|
|
890
|
-
remember(draft, context, next);
|
|
980
|
+
remember(draft, context, images, next);
|
|
891
981
|
return next;
|
|
892
982
|
});
|
|
893
983
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -897,9 +987,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
897
987
|
const [next, ...rest] = queue;
|
|
898
988
|
setDispatching(true);
|
|
899
989
|
setQueue(rest);
|
|
900
|
-
remember(draft, context, rest);
|
|
901
|
-
onPending?.(next.text, next.context);
|
|
902
|
-
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {} });
|
|
990
|
+
remember(draft, context, images, rest);
|
|
991
|
+
onPending?.(next.text, next.context, next.images);
|
|
992
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
903
993
|
}
|
|
904
994
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
905
995
|
useEffect(() => {
|
|
@@ -912,8 +1002,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
912
1002
|
if (!restoreDraft) return;
|
|
913
1003
|
setDraft(restoreDraft.text);
|
|
914
1004
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
1005
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
915
1006
|
setContext(restoredContext);
|
|
916
|
-
|
|
1007
|
+
setImages(restoredImages);
|
|
1008
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
917
1009
|
textarea.current?.focus({ preventScroll: true });
|
|
918
1010
|
onDraftRestored?.(restoreDraft.id);
|
|
919
1011
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -922,30 +1014,67 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
922
1014
|
return () => clearTimeout(timer);
|
|
923
1015
|
}, [adapter, draft]);
|
|
924
1016
|
const pickContext = () => {
|
|
925
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
1017
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
926
1018
|
setPicking(true);
|
|
927
1019
|
setPickerError(null);
|
|
928
1020
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
1021
|
+
const attachments = partitionAttachments(picked);
|
|
1022
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1023
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
929
1024
|
setContext((current) => {
|
|
930
|
-
const next = mergeContext(current,
|
|
931
|
-
remember(draft, next, queue);
|
|
1025
|
+
const next = mergeContext(current, attachments.context);
|
|
1026
|
+
remember(draft, next, images, queue);
|
|
932
1027
|
return next;
|
|
933
1028
|
});
|
|
934
|
-
|
|
1029
|
+
setImages((current) => {
|
|
1030
|
+
const next = mergeImages(current, attachments.images);
|
|
1031
|
+
remember(draft, context, next, queue);
|
|
1032
|
+
return next;
|
|
1033
|
+
});
|
|
1034
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1035
|
+
};
|
|
1036
|
+
const addImageFiles = (value, source) => {
|
|
1037
|
+
const allFiles = Array.from(value ?? []);
|
|
1038
|
+
if (!allFiles.length) return false;
|
|
1039
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
1040
|
+
if (files.length !== allFiles.length) {
|
|
1041
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1045
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1046
|
+
return true;
|
|
1047
|
+
}
|
|
1048
|
+
setPicking(true);
|
|
1049
|
+
setPickerError(null);
|
|
1050
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
1051
|
+
const next = mergeImages(current, picked);
|
|
1052
|
+
remember(draft, context, next, queue);
|
|
1053
|
+
return next;
|
|
1054
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
1055
|
+
return true;
|
|
1056
|
+
};
|
|
1057
|
+
const pasteImages = (event) => {
|
|
1058
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
1059
|
+
};
|
|
1060
|
+
const dropImages = (event) => {
|
|
1061
|
+
setDragging(false);
|
|
1062
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
935
1063
|
};
|
|
936
1064
|
const send = () => {
|
|
937
1065
|
const text = draft.trim();
|
|
938
|
-
if (!text) return;
|
|
939
|
-
const message = { text, context };
|
|
1066
|
+
if (!text && !images.length) return;
|
|
1067
|
+
const message = { text, context, images };
|
|
940
1068
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
941
1069
|
else if (state.canSend) {
|
|
942
1070
|
if (onPending) setDispatching(true);
|
|
943
|
-
onPending?.(text, context);
|
|
944
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
1071
|
+
onPending?.(text, context, images);
|
|
1072
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
945
1073
|
} else return;
|
|
946
1074
|
setDraft("");
|
|
947
1075
|
setContext([]);
|
|
948
|
-
|
|
1076
|
+
setImages([]);
|
|
1077
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
949
1078
|
};
|
|
950
1079
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
951
1080
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -955,27 +1084,41 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
955
1084
|
] }),
|
|
956
1085
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
957
1086
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
958
|
-
item.text,
|
|
959
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
960
|
-
item.context.length,
|
|
1087
|
+
item.text || "Image attachment",
|
|
1088
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1089
|
+
item.context.length + item.images.length,
|
|
961
1090
|
" attached"
|
|
962
1091
|
] }) : null
|
|
963
1092
|
] }),
|
|
964
1093
|
/* @__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 }) })
|
|
965
1094
|
] }, `${index}:${item.text}`))
|
|
966
1095
|
] }) : null,
|
|
1096
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
1097
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
1098
|
+
remember(draft, context, next, queue);
|
|
1099
|
+
return next;
|
|
1100
|
+
}) }),
|
|
967
1101
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
968
1102
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
969
|
-
remember(draft, next, queue);
|
|
1103
|
+
remember(draft, next, images, queue);
|
|
970
1104
|
return next;
|
|
971
1105
|
}) }),
|
|
972
1106
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
973
|
-
/* @__PURE__ */ jsxs3("div", { class:
|
|
974
|
-
|
|
975
|
-
|
|
1107
|
+
/* @__PURE__ */ jsxs3("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
1108
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
1109
|
+
event.preventDefault();
|
|
1110
|
+
setDragging(true);
|
|
1111
|
+
}
|
|
1112
|
+
}, onDragOver: (event) => {
|
|
1113
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
1114
|
+
}, onDragLeave: (event) => {
|
|
1115
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
1116
|
+
}, onDrop: dropImages, children: [
|
|
1117
|
+
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,
|
|
1118
|
+
/* @__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) => {
|
|
976
1119
|
const value = event.currentTarget.value;
|
|
977
1120
|
setDraft(value);
|
|
978
|
-
remember(value, context, queue);
|
|
1121
|
+
remember(value, context, images, queue);
|
|
979
1122
|
}, onKeyDown: (event) => {
|
|
980
1123
|
if (isSendKey(event)) {
|
|
981
1124
|
event.preventDefault();
|
|
@@ -984,7 +1127,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
984
1127
|
} }),
|
|
985
1128
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
986
1129
|
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,
|
|
987
|
-
/* @__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 }) })
|
|
1130
|
+
/* @__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 }) })
|
|
988
1131
|
] })
|
|
989
1132
|
] })
|
|
990
1133
|
] });
|
|
@@ -1377,6 +1520,7 @@ function TranscriptEntry({ entry, state, adapter }) {
|
|
|
1377
1520
|
}
|
|
1378
1521
|
if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
|
|
1379
1522
|
return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
|
|
1523
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
|
|
1380
1524
|
/* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
|
|
1381
1525
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
|
|
1382
1526
|
entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
|
|
@@ -1579,6 +1723,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1579
1723
|
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 })
|
|
1580
1724
|
] }, block.id)),
|
|
1581
1725
|
pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
|
|
1726
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
|
|
1582
1727
|
/* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
|
|
1583
1728
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
|
|
1584
1729
|
/* @__PURE__ */ jsxs4("footer", { children: [
|
|
@@ -1923,9 +2068,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1923
2068
|
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1924
2069
|
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1925
2070
|
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1926
|
-
const beginPending = (text, context = []) => setPending({
|
|
2071
|
+
const beginPending = (text, context = [], images = []) => setPending({
|
|
1927
2072
|
text,
|
|
1928
2073
|
context,
|
|
2074
|
+
images,
|
|
1929
2075
|
status: "sending",
|
|
1930
2076
|
initialError: state.error,
|
|
1931
2077
|
seenBusy: state.busy,
|
|
@@ -1933,13 +2079,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1933
2079
|
});
|
|
1934
2080
|
const retryPending = () => {
|
|
1935
2081
|
if (!pending) return;
|
|
1936
|
-
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
|
|
2082
|
+
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
|
|
1937
2083
|
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1938
2084
|
};
|
|
1939
2085
|
const editPending = () => {
|
|
1940
2086
|
if (!pending) return;
|
|
1941
2087
|
restoreSequence.current += 1;
|
|
1942
|
-
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
|
|
2088
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
|
|
1943
2089
|
setPending({ ...pending, status: "editing" });
|
|
1944
2090
|
};
|
|
1945
2091
|
useEffect6(() => {
|
|
@@ -1985,7 +2131,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1985
2131
|
setPendingAction(null);
|
|
1986
2132
|
}
|
|
1987
2133
|
}, [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;
|
|
2134
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1989
2135
|
const action = state.operation || pendingAction?.action || null;
|
|
1990
2136
|
const actionLabel = operationLabel(action);
|
|
1991
2137
|
const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
|
|
@@ -2009,12 +2155,14 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2009
2155
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2010
2156
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2011
2157
|
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2012
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
|
|
2158
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2013
2159
|
const [harness, setHarness] = useState4(remembered.harness);
|
|
2014
2160
|
const [draft, setDraft] = useState4(remembered.draft);
|
|
2015
2161
|
const [context, setContext] = useState4(remembered.context);
|
|
2162
|
+
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2016
2163
|
const [starting, setStarting] = useState4(null);
|
|
2017
2164
|
const [picking, setPicking] = useState4(false);
|
|
2165
|
+
const [dragging, setDragging] = useState4(false);
|
|
2018
2166
|
const [pickerError, setPickerError] = useState4(null);
|
|
2019
2167
|
const startSequence = useRef5(0);
|
|
2020
2168
|
const textarea = useRef5(null);
|
|
@@ -2023,7 +2171,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2023
2171
|
if (startable.some((item) => item.id === harness)) return;
|
|
2024
2172
|
const next = startable[0]?.id ?? "";
|
|
2025
2173
|
setHarness(next);
|
|
2026
|
-
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
|
|
2174
|
+
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
|
|
2027
2175
|
}, [harness, startableKey]);
|
|
2028
2176
|
useEffect6(() => {
|
|
2029
2177
|
if (!starting) return;
|
|
@@ -2040,24 +2188,60 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2040
2188
|
textarea.current?.focus({ preventScroll: true });
|
|
2041
2189
|
}, []);
|
|
2042
2190
|
const pickContext = () => {
|
|
2043
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
2191
|
+
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2044
2192
|
setPicking(true);
|
|
2045
2193
|
setPickerError(null);
|
|
2046
2194
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
2195
|
+
const attachments = partitionAttachments(picked);
|
|
2196
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2197
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2047
2198
|
setContext((current) => {
|
|
2048
|
-
const next = mergeContext(current,
|
|
2049
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2199
|
+
const next = mergeContext(current, attachments.context);
|
|
2200
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2201
|
+
return next;
|
|
2202
|
+
});
|
|
2203
|
+
setImages((current) => {
|
|
2204
|
+
const next = mergeImages(current, attachments.images);
|
|
2205
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2050
2206
|
return next;
|
|
2051
2207
|
});
|
|
2052
|
-
}
|
|
2208
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2209
|
+
};
|
|
2210
|
+
const addImageFiles = (value, source) => {
|
|
2211
|
+
const allFiles = Array.from(value ?? []);
|
|
2212
|
+
if (!allFiles.length) return false;
|
|
2213
|
+
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2214
|
+
if (files.length !== allFiles.length) {
|
|
2215
|
+
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
2216
|
+
return true;
|
|
2217
|
+
}
|
|
2218
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2219
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2220
|
+
return true;
|
|
2221
|
+
}
|
|
2222
|
+
setPicking(true);
|
|
2223
|
+
setPickerError(null);
|
|
2224
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2225
|
+
const next = mergeImages(current, picked);
|
|
2226
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2227
|
+
return next;
|
|
2228
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2229
|
+
return true;
|
|
2230
|
+
};
|
|
2231
|
+
const pasteImages = (event) => {
|
|
2232
|
+
if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
|
|
2233
|
+
};
|
|
2234
|
+
const dropImages = (event) => {
|
|
2235
|
+
setDragging(false);
|
|
2236
|
+
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
2053
2237
|
};
|
|
2054
2238
|
const send = () => {
|
|
2055
2239
|
const text = draft.trim();
|
|
2056
|
-
if (!text || !harness || starting) return;
|
|
2240
|
+
if (!text && !images.length || !harness || starting) return;
|
|
2057
2241
|
const id = startSequence.current + 1;
|
|
2058
2242
|
startSequence.current = id;
|
|
2059
2243
|
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 } : {} });
|
|
2244
|
+
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2061
2245
|
if (result && typeof result.then === "function") {
|
|
2062
2246
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2063
2247
|
}
|
|
@@ -2087,31 +2271,45 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2087
2271
|
/* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2088
2272
|
const value = event.currentTarget.value;
|
|
2089
2273
|
setHarness(value);
|
|
2090
|
-
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
|
|
2274
|
+
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
|
|
2091
2275
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
|
|
2092
2276
|
item.label,
|
|
2093
2277
|
item.startable ? "" : " \xB7 unavailable"
|
|
2094
2278
|
] }, item.id)) })
|
|
2095
2279
|
] }),
|
|
2280
|
+
/* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2281
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2282
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2283
|
+
return next;
|
|
2284
|
+
}) }),
|
|
2096
2285
|
/* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2097
2286
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2098
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2287
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2099
2288
|
return next;
|
|
2100
2289
|
}) }),
|
|
2101
2290
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2102
|
-
/* @__PURE__ */ jsxs7("div", { class:
|
|
2103
|
-
|
|
2104
|
-
|
|
2291
|
+
/* @__PURE__ */ jsxs7("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2292
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2293
|
+
event.preventDefault();
|
|
2294
|
+
setDragging(true);
|
|
2295
|
+
}
|
|
2296
|
+
}, onDragOver: (event) => {
|
|
2297
|
+
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
|
|
2298
|
+
}, onDragLeave: (event) => {
|
|
2299
|
+
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2300
|
+
}, onDrop: dropImages, children: [
|
|
2301
|
+
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,
|
|
2302
|
+
/* @__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
2303
|
const value = event.currentTarget.value;
|
|
2106
2304
|
setDraft(value);
|
|
2107
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
|
|
2305
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
|
|
2108
2306
|
}, onKeyDown: (event) => {
|
|
2109
2307
|
if (isSendKey(event)) {
|
|
2110
2308
|
event.preventDefault();
|
|
2111
2309
|
send();
|
|
2112
2310
|
}
|
|
2113
2311
|
} }),
|
|
2114
|
-
/* @__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 }) }) })
|
|
2312
|
+
/* @__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 }) }) })
|
|
2115
2313
|
] })
|
|
2116
2314
|
] })
|
|
2117
2315
|
] });
|