@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/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,12 @@ 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 directly. The composer bounds images to four supported browser
|
|
130
|
+
formats at 5 MB each, previews them locally, and keeps both attachment kinds associated with queued
|
|
131
|
+
messages, retry, edit, and new-chat recovery. The host still chooses how explicit file selection
|
|
132
|
+
is exposed and must revalidate every returned item.
|
|
130
133
|
|
|
131
134
|
An embedding product such as Vibewaiting should therefore be small: Lucarne owns its iframe and
|
|
132
135
|
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,23 @@ 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);
|
|
884
970
|
const [pickerError, setPickerError] = useState(null);
|
|
885
971
|
const textarea = useRef(null);
|
|
886
972
|
useAutosizeTextarea(textarea, draft);
|
|
887
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
973
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
974
|
+
useEffect(() => {
|
|
975
|
+
remember(draft, context, images, queue);
|
|
976
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
888
977
|
const updateQueue = (update) => setQueue((items) => {
|
|
889
978
|
const next = update(items);
|
|
890
|
-
remember(draft, context, next);
|
|
979
|
+
remember(draft, context, images, next);
|
|
891
980
|
return next;
|
|
892
981
|
});
|
|
893
982
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -897,9 +986,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
897
986
|
const [next, ...rest] = queue;
|
|
898
987
|
setDispatching(true);
|
|
899
988
|
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 } : {} });
|
|
989
|
+
remember(draft, context, images, rest);
|
|
990
|
+
onPending?.(next.text, next.context, next.images);
|
|
991
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
903
992
|
}
|
|
904
993
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
905
994
|
useEffect(() => {
|
|
@@ -912,8 +1001,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
912
1001
|
if (!restoreDraft) return;
|
|
913
1002
|
setDraft(restoreDraft.text);
|
|
914
1003
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
1004
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
915
1005
|
setContext(restoredContext);
|
|
916
|
-
|
|
1006
|
+
setImages(restoredImages);
|
|
1007
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
917
1008
|
textarea.current?.focus({ preventScroll: true });
|
|
918
1009
|
onDraftRestored?.(restoreDraft.id);
|
|
919
1010
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -922,30 +1013,55 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
922
1013
|
return () => clearTimeout(timer);
|
|
923
1014
|
}, [adapter, draft]);
|
|
924
1015
|
const pickContext = () => {
|
|
925
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
1016
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
926
1017
|
setPicking(true);
|
|
927
1018
|
setPickerError(null);
|
|
928
1019
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
1020
|
+
const attachments = partitionAttachments(picked);
|
|
1021
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1022
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
929
1023
|
setContext((current) => {
|
|
930
|
-
const next = mergeContext(current,
|
|
931
|
-
remember(draft, next, queue);
|
|
1024
|
+
const next = mergeContext(current, attachments.context);
|
|
1025
|
+
remember(draft, next, images, queue);
|
|
932
1026
|
return next;
|
|
933
1027
|
});
|
|
934
|
-
|
|
1028
|
+
setImages((current) => {
|
|
1029
|
+
const next = mergeImages(current, attachments.images);
|
|
1030
|
+
remember(draft, context, next, queue);
|
|
1031
|
+
return next;
|
|
1032
|
+
});
|
|
1033
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1034
|
+
};
|
|
1035
|
+
const pasteImages = (event) => {
|
|
1036
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
1037
|
+
if (!files.length) return;
|
|
1038
|
+
event.preventDefault();
|
|
1039
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1040
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
setPicking(true);
|
|
1044
|
+
setPickerError(null);
|
|
1045
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
1046
|
+
const next = mergeImages(current, picked);
|
|
1047
|
+
remember(draft, context, next, queue);
|
|
1048
|
+
return next;
|
|
1049
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
|
|
935
1050
|
};
|
|
936
1051
|
const send = () => {
|
|
937
1052
|
const text = draft.trim();
|
|
938
1053
|
if (!text) return;
|
|
939
|
-
const message = { text, context };
|
|
1054
|
+
const message = { text, context, images };
|
|
940
1055
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
941
1056
|
else if (state.canSend) {
|
|
942
1057
|
if (onPending) setDispatching(true);
|
|
943
|
-
onPending?.(text, context);
|
|
944
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
1058
|
+
onPending?.(text, context, images);
|
|
1059
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
945
1060
|
} else return;
|
|
946
1061
|
setDraft("");
|
|
947
1062
|
setContext([]);
|
|
948
|
-
|
|
1063
|
+
setImages([]);
|
|
1064
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
949
1065
|
};
|
|
950
1066
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
951
1067
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -956,26 +1072,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
956
1072
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
957
1073
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
958
1074
|
item.text,
|
|
959
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
960
|
-
item.context.length,
|
|
1075
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1076
|
+
item.context.length + item.images.length,
|
|
961
1077
|
" attached"
|
|
962
1078
|
] }) : null
|
|
963
1079
|
] }),
|
|
964
1080
|
/* @__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
1081
|
] }, `${index}:${item.text}`))
|
|
966
1082
|
] }) : null,
|
|
1083
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
1084
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
1085
|
+
remember(draft, context, next, queue);
|
|
1086
|
+
return next;
|
|
1087
|
+
}) }),
|
|
967
1088
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
968
1089
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
969
|
-
remember(draft, next, queue);
|
|
1090
|
+
remember(draft, next, images, queue);
|
|
970
1091
|
return next;
|
|
971
1092
|
}) }),
|
|
972
1093
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
973
1094
|
/* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
|
|
974
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach
|
|
975
|
-
/* @__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) => {
|
|
1095
|
+
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,
|
|
1096
|
+
/* @__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
1097
|
const value = event.currentTarget.value;
|
|
977
1098
|
setDraft(value);
|
|
978
|
-
remember(value, context, queue);
|
|
1099
|
+
remember(value, context, images, queue);
|
|
979
1100
|
}, onKeyDown: (event) => {
|
|
980
1101
|
if (isSendKey(event)) {
|
|
981
1102
|
event.preventDefault();
|
|
@@ -1377,6 +1498,7 @@ function TranscriptEntry({ entry, state, adapter }) {
|
|
|
1377
1498
|
}
|
|
1378
1499
|
if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
|
|
1379
1500
|
return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
|
|
1501
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
|
|
1380
1502
|
/* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
|
|
1381
1503
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
|
|
1382
1504
|
entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
|
|
@@ -1579,6 +1701,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1579
1701
|
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
1702
|
] }, block.id)),
|
|
1581
1703
|
pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
|
|
1704
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
|
|
1582
1705
|
/* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
|
|
1583
1706
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
|
|
1584
1707
|
/* @__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();
|