@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/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,23 @@ 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);
|
|
887
973
|
const [pickerError, setPickerError] = useState(null);
|
|
888
974
|
const textarea = useRef(null);
|
|
889
975
|
useAutosizeTextarea(textarea, draft);
|
|
890
|
-
const remember = (nextDraft, nextContext, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, queue: nextQueue });
|
|
976
|
+
const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
|
|
977
|
+
useEffect(() => {
|
|
978
|
+
remember(draft, context, images, queue);
|
|
979
|
+
}, [draft, context, images, memoryKey, queue]);
|
|
891
980
|
const updateQueue = (update) => setQueue((items) => {
|
|
892
981
|
const next = update(items);
|
|
893
|
-
remember(draft, context, next);
|
|
982
|
+
remember(draft, context, images, next);
|
|
894
983
|
return next;
|
|
895
984
|
});
|
|
896
985
|
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
@@ -900,9 +989,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
900
989
|
const [next, ...rest] = queue;
|
|
901
990
|
setDispatching(true);
|
|
902
991
|
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 } : {} });
|
|
992
|
+
remember(draft, context, images, rest);
|
|
993
|
+
onPending?.(next.text, next.context, next.images);
|
|
994
|
+
adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
|
|
906
995
|
}
|
|
907
996
|
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
908
997
|
useEffect(() => {
|
|
@@ -915,8 +1004,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
915
1004
|
if (!restoreDraft) return;
|
|
916
1005
|
setDraft(restoreDraft.text);
|
|
917
1006
|
const restoredContext = normalizeContext(restoreDraft.context);
|
|
1007
|
+
const restoredImages = normalizeImages(restoreDraft.images);
|
|
918
1008
|
setContext(restoredContext);
|
|
919
|
-
|
|
1009
|
+
setImages(restoredImages);
|
|
1010
|
+
remember(restoreDraft.text, restoredContext, restoredImages, queue);
|
|
920
1011
|
textarea.current?.focus({ preventScroll: true });
|
|
921
1012
|
onDraftRestored?.(restoreDraft.id);
|
|
922
1013
|
}, [onDraftRestored, restoreDraft?.id]);
|
|
@@ -925,30 +1016,55 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
925
1016
|
return () => clearTimeout(timer);
|
|
926
1017
|
}, [adapter, draft]);
|
|
927
1018
|
const pickContext = () => {
|
|
928
|
-
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
1019
|
+
if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
929
1020
|
setPicking(true);
|
|
930
1021
|
setPickerError(null);
|
|
931
1022
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
1023
|
+
const attachments = partitionAttachments(picked);
|
|
1024
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
1025
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
932
1026
|
setContext((current) => {
|
|
933
|
-
const next = mergeContext(current,
|
|
934
|
-
remember(draft, next, queue);
|
|
1027
|
+
const next = mergeContext(current, attachments.context);
|
|
1028
|
+
remember(draft, next, images, queue);
|
|
935
1029
|
return next;
|
|
936
1030
|
});
|
|
937
|
-
|
|
1031
|
+
setImages((current) => {
|
|
1032
|
+
const next = mergeImages(current, attachments.images);
|
|
1033
|
+
remember(draft, context, next, queue);
|
|
1034
|
+
return next;
|
|
1035
|
+
});
|
|
1036
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
1037
|
+
};
|
|
1038
|
+
const pasteImages = (event) => {
|
|
1039
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
1040
|
+
if (!files.length) return;
|
|
1041
|
+
event.preventDefault();
|
|
1042
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
1043
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
setPicking(true);
|
|
1047
|
+
setPickerError(null);
|
|
1048
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
1049
|
+
const next = mergeImages(current, picked);
|
|
1050
|
+
remember(draft, context, next, queue);
|
|
1051
|
+
return next;
|
|
1052
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
|
|
938
1053
|
};
|
|
939
1054
|
const send = () => {
|
|
940
1055
|
const text = draft.trim();
|
|
941
1056
|
if (!text) return;
|
|
942
|
-
const message = { text, context };
|
|
1057
|
+
const message = { text, context, images };
|
|
943
1058
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
944
1059
|
else if (state.canSend) {
|
|
945
1060
|
if (onPending) setDispatching(true);
|
|
946
|
-
onPending?.(text, context);
|
|
947
|
-
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {} });
|
|
1061
|
+
onPending?.(text, context, images);
|
|
1062
|
+
adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
948
1063
|
} else return;
|
|
949
1064
|
setDraft("");
|
|
950
1065
|
setContext([]);
|
|
951
|
-
|
|
1066
|
+
setImages([]);
|
|
1067
|
+
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
952
1068
|
};
|
|
953
1069
|
return /* @__PURE__ */ jsxs3("div", { class: "scui-compose", children: [
|
|
954
1070
|
queue.length ? /* @__PURE__ */ jsxs3("div", { class: "scui-queue", children: [
|
|
@@ -959,26 +1075,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
959
1075
|
queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
|
|
960
1076
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
961
1077
|
item.text,
|
|
962
|
-
item.context.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
963
|
-
item.context.length,
|
|
1078
|
+
item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
|
|
1079
|
+
item.context.length + item.images.length,
|
|
964
1080
|
" attached"
|
|
965
1081
|
] }) : null
|
|
966
1082
|
] }),
|
|
967
1083
|
/* @__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
1084
|
] }, `${index}:${item.text}`))
|
|
969
1085
|
] }) : null,
|
|
1086
|
+
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
1087
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
1088
|
+
remember(draft, context, next, queue);
|
|
1089
|
+
return next;
|
|
1090
|
+
}) }),
|
|
970
1091
|
/* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
971
1092
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
972
|
-
remember(draft, next, queue);
|
|
1093
|
+
remember(draft, next, images, queue);
|
|
973
1094
|
return next;
|
|
974
1095
|
}) }),
|
|
975
1096
|
pickerError ? /* @__PURE__ */ jsx3("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
976
1097
|
/* @__PURE__ */ jsxs3("div", { class: "scui-envelope", children: [
|
|
977
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { class: "scui-attach", type: "button", "aria-label": "Attach
|
|
978
|
-
/* @__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) => {
|
|
1098
|
+
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,
|
|
1099
|
+
/* @__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
1100
|
const value = event.currentTarget.value;
|
|
980
1101
|
setDraft(value);
|
|
981
|
-
remember(value, context, queue);
|
|
1102
|
+
remember(value, context, images, queue);
|
|
982
1103
|
}, onKeyDown: (event) => {
|
|
983
1104
|
if (isSendKey(event)) {
|
|
984
1105
|
event.preventDefault();
|
|
@@ -1380,6 +1501,7 @@ function TranscriptEntry({ entry, state, adapter }) {
|
|
|
1380
1501
|
}
|
|
1381
1502
|
if (entry.role === "notice" || entry.role === "system") return /* @__PURE__ */ jsx5("div", { class: "scui-notice", "data-code": entry.code, children: entry.text });
|
|
1382
1503
|
return /* @__PURE__ */ jsxs4("article", { class: "scui-message", "data-role": entry.role, "aria-label": `${entry.role === "user" ? "Your" : "Assistant"} message`, children: [
|
|
1504
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: entry.images }),
|
|
1383
1505
|
/* @__PURE__ */ jsx5(Markdown, { value: entry.text, copyText: adapter?.copyText }),
|
|
1384
1506
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: entry.context }),
|
|
1385
1507
|
entry.truncated ? /* @__PURE__ */ jsx5("small", { class: "scui-truncated", children: "Entry truncated by host \xB7 load native session for the complete value" }) : null,
|
|
@@ -1582,6 +1704,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1582
1704
|
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
1705
|
] }, block.id)),
|
|
1584
1706
|
pendingMessage ? /* @__PURE__ */ jsxs4("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, "aria-label": "Your pending message", children: [
|
|
1707
|
+
/* @__PURE__ */ jsx5(MessageImages, { items: pendingMessage.images }),
|
|
1585
1708
|
/* @__PURE__ */ jsx5(Markdown, { value: pendingMessage.text, copyText: adapter?.copyText }),
|
|
1586
1709
|
/* @__PURE__ */ jsx5(ContextDisclosure, { context: pendingMessage.context }),
|
|
1587
1710
|
/* @__PURE__ */ jsxs4("footer", { children: [
|
|
@@ -1920,9 +2043,10 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1920
2043
|
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1921
2044
|
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1922
2045
|
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1923
|
-
const beginPending = (text, context = []) => setPending({
|
|
2046
|
+
const beginPending = (text, context = [], images = []) => setPending({
|
|
1924
2047
|
text,
|
|
1925
2048
|
context,
|
|
2049
|
+
images,
|
|
1926
2050
|
status: "sending",
|
|
1927
2051
|
initialError: state.error,
|
|
1928
2052
|
seenBusy: state.busy,
|
|
@@ -1930,13 +2054,13 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1930
2054
|
});
|
|
1931
2055
|
const retryPending = () => {
|
|
1932
2056
|
if (!pending) return;
|
|
1933
|
-
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {} });
|
|
2057
|
+
adapter.onIntent({ action: "send", text: pending.text, ...pending.context?.length ? { context: pending.context } : {}, ...pending.images?.length ? { images: pending.images } : {} });
|
|
1934
2058
|
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1935
2059
|
};
|
|
1936
2060
|
const editPending = () => {
|
|
1937
2061
|
if (!pending) return;
|
|
1938
2062
|
restoreSequence.current += 1;
|
|
1939
|
-
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context });
|
|
2063
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text, context: pending.context, images: pending.images });
|
|
1940
2064
|
setPending({ ...pending, status: "editing" });
|
|
1941
2065
|
};
|
|
1942
2066
|
useEffect6(() => {
|
|
@@ -1982,7 +2106,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1982
2106
|
setPendingAction(null);
|
|
1983
2107
|
}
|
|
1984
2108
|
}, [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;
|
|
2109
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, context: pending.context, images: pending.images, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1986
2110
|
const action = state.operation || pendingAction?.action || null;
|
|
1987
2111
|
const actionLabel = operationLabel(action);
|
|
1988
2112
|
const actionState = action ? { ...state, operation: action, canInterrupt: false, canRespond: false } : state;
|
|
@@ -2006,10 +2130,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2006
2130
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2007
2131
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2008
2132
|
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2009
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [] };
|
|
2133
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2010
2134
|
const [harness, setHarness] = useState4(remembered.harness);
|
|
2011
2135
|
const [draft, setDraft] = useState4(remembered.draft);
|
|
2012
2136
|
const [context, setContext] = useState4(remembered.context);
|
|
2137
|
+
const [images, setImages] = useState4(remembered.images ?? []);
|
|
2013
2138
|
const [starting, setStarting] = useState4(null);
|
|
2014
2139
|
const [picking, setPicking] = useState4(false);
|
|
2015
2140
|
const [pickerError, setPickerError] = useState4(null);
|
|
@@ -2020,7 +2145,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2020
2145
|
if (startable.some((item) => item.id === harness)) return;
|
|
2021
2146
|
const next = startable[0]?.id ?? "";
|
|
2022
2147
|
setHarness(next);
|
|
2023
|
-
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context });
|
|
2148
|
+
boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
|
|
2024
2149
|
}, [harness, startableKey]);
|
|
2025
2150
|
useEffect6(() => {
|
|
2026
2151
|
if (!starting) return;
|
|
@@ -2037,16 +2162,40 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2037
2162
|
textarea.current?.focus({ preventScroll: true });
|
|
2038
2163
|
}, []);
|
|
2039
2164
|
const pickContext = () => {
|
|
2040
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS) return;
|
|
2165
|
+
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2041
2166
|
setPicking(true);
|
|
2042
2167
|
setPickerError(null);
|
|
2043
2168
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
2169
|
+
const attachments = partitionAttachments(picked);
|
|
2170
|
+
if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
|
|
2171
|
+
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2044
2172
|
setContext((current) => {
|
|
2045
|
-
const next = mergeContext(current,
|
|
2046
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2173
|
+
const next = mergeContext(current, attachments.context);
|
|
2174
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2047
2175
|
return next;
|
|
2048
2176
|
});
|
|
2049
|
-
|
|
2177
|
+
setImages((current) => {
|
|
2178
|
+
const next = mergeImages(current, attachments.images);
|
|
2179
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2180
|
+
return next;
|
|
2181
|
+
});
|
|
2182
|
+
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
2183
|
+
};
|
|
2184
|
+
const pasteImages = (event) => {
|
|
2185
|
+
const files = Array.from(event.clipboardData?.files ?? []).filter((file) => file.type.startsWith("image/"));
|
|
2186
|
+
if (!files.length) return;
|
|
2187
|
+
event.preventDefault();
|
|
2188
|
+
if (images.length + files.length > MAX_IMAGE_ITEMS) {
|
|
2189
|
+
setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
2192
|
+
setPicking(true);
|
|
2193
|
+
setPickerError(null);
|
|
2194
|
+
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2195
|
+
const next = mergeImages(current, picked);
|
|
2196
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2197
|
+
return next;
|
|
2198
|
+
}), (error) => setPickerError(error instanceof Error ? error.message : "Could not paste image.")).finally(() => setPicking(false));
|
|
2050
2199
|
};
|
|
2051
2200
|
const send = () => {
|
|
2052
2201
|
const text = draft.trim();
|
|
@@ -2054,7 +2203,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2054
2203
|
const id = startSequence.current + 1;
|
|
2055
2204
|
startSequence.current = id;
|
|
2056
2205
|
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 } : {} });
|
|
2206
|
+
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2058
2207
|
if (result && typeof result.then === "function") {
|
|
2059
2208
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2060
2209
|
}
|
|
@@ -2084,24 +2233,29 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2084
2233
|
/* @__PURE__ */ jsx8("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2085
2234
|
const value = event.currentTarget.value;
|
|
2086
2235
|
setHarness(value);
|
|
2087
|
-
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context });
|
|
2236
|
+
boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
|
|
2088
2237
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs7("option", { value: item.id, disabled: !item.startable, children: [
|
|
2089
2238
|
item.label,
|
|
2090
2239
|
item.startable ? "" : " \xB7 unavailable"
|
|
2091
2240
|
] }, item.id)) })
|
|
2092
2241
|
] }),
|
|
2242
|
+
/* @__PURE__ */ jsx8(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2243
|
+
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2244
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
|
|
2245
|
+
return next;
|
|
2246
|
+
}) }),
|
|
2093
2247
|
/* @__PURE__ */ jsx8(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2094
2248
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2095
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next });
|
|
2249
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
|
|
2096
2250
|
return next;
|
|
2097
2251
|
}) }),
|
|
2098
2252
|
pickerError ? /* @__PURE__ */ jsx8("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2099
2253
|
/* @__PURE__ */ jsxs7("div", { class: "scui-envelope", children: [
|
|
2100
|
-
adapter.pickContext ? /* @__PURE__ */ jsx8("button", { class: "scui-attach", type: "button", "aria-label": "Attach
|
|
2101
|
-
/* @__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) => {
|
|
2254
|
+
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,
|
|
2255
|
+
/* @__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
2256
|
const value = event.currentTarget.value;
|
|
2103
2257
|
setDraft(value);
|
|
2104
|
-
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context });
|
|
2258
|
+
boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
|
|
2105
2259
|
}, onKeyDown: (event) => {
|
|
2106
2260
|
if (isSendKey(event)) {
|
|
2107
2261
|
event.preventDefault();
|
package/package.json
CHANGED
package/sessions.mjs
CHANGED
|
@@ -162,6 +162,11 @@ var ICONS = {
|
|
|
162
162
|
/* @__PURE__ */ jsx2("path", { d: "M9 3.5v10" }),
|
|
163
163
|
/* @__PURE__ */ jsx2("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
|
|
164
164
|
] }),
|
|
165
|
+
image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
|
+
/* @__PURE__ */ jsx2("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
|
|
167
|
+
/* @__PURE__ */ jsx2("circle", { cx: "6.5", cy: "7", r: "1.25" }),
|
|
168
|
+
/* @__PURE__ */ jsx2("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
|
|
169
|
+
] }),
|
|
165
170
|
menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
166
171
|
/* @__PURE__ */ jsx2("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
167
172
|
/* @__PURE__ */ jsx2("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
|
|
@@ -187,8 +192,12 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
187
192
|
return /* @__PURE__ */ jsx2("svg", { class: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", "stroke-width": "1.5", "stroke-linecap": "round", "stroke-linejoin": "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx2(Glyph, {}) });
|
|
188
193
|
}
|
|
189
194
|
|
|
195
|
+
// src/context.jsx
|
|
196
|
+
import { jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
197
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
198
|
+
|
|
190
199
|
// src/conversation.jsx
|
|
191
|
-
import { Fragment as Fragment3, jsx as
|
|
200
|
+
import { Fragment as Fragment3, jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
192
201
|
function LoadingStatus({ state, compact = false }) {
|
|
193
202
|
const copy = {
|
|
194
203
|
connecting: ["Connecting to coding agents", "Checking installed harnesses and capabilities.", 0],
|
|
@@ -196,19 +205,19 @@ function LoadingStatus({ state, compact = false }) {
|
|
|
196
205
|
discovering: ["Loading recent sessions", "Scanning native session stores without loading full transcripts.", 2],
|
|
197
206
|
ready: ["Ready", "Coding sessions are up to date.", 3]
|
|
198
207
|
}[state.startup];
|
|
199
|
-
return /* @__PURE__ */
|
|
200
|
-
/* @__PURE__ */
|
|
201
|
-
/* @__PURE__ */
|
|
202
|
-
/* @__PURE__ */
|
|
203
|
-
/* @__PURE__ */
|
|
208
|
+
return /* @__PURE__ */ jsxs3("div", { class: `scui-loading${compact ? " scui-loading-compact" : ""}`, role: "status", "aria-busy": state.startup !== "ready", children: [
|
|
209
|
+
/* @__PURE__ */ jsx4("span", { class: "scui-orbit", "aria-hidden": "true", children: /* @__PURE__ */ jsx4("i", {}) }),
|
|
210
|
+
/* @__PURE__ */ jsxs3("span", { class: "scui-loading-copy", children: [
|
|
211
|
+
/* @__PURE__ */ jsx4("strong", { children: copy[0] }),
|
|
212
|
+
/* @__PURE__ */ jsx4("small", { children: copy[1] })
|
|
204
213
|
] }),
|
|
205
|
-
/* @__PURE__ */
|
|
214
|
+
/* @__PURE__ */ jsx4("span", { class: "scui-progress", "aria-hidden": "true", children: [1, 2, 3].map((step) => /* @__PURE__ */ jsx4("i", { "data-progress": step <= copy[2] ? "done" : step === copy[2] + 1 ? "current" : "waiting" }, step)) })
|
|
206
215
|
] });
|
|
207
216
|
}
|
|
208
217
|
|
|
209
218
|
// src/logo.jsx
|
|
210
219
|
import { useEffect as useEffect3 } from "preact/hooks";
|
|
211
|
-
import { jsx as
|
|
220
|
+
import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
212
221
|
var LOGOS = {
|
|
213
222
|
"claude-code": {
|
|
214
223
|
viewBox: "-1 3.5 26 18",
|
|
@@ -259,29 +268,29 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
|
259
268
|
if (!logo) onMissingLogo?.(id);
|
|
260
269
|
}, [id, logo, onMissingLogo]);
|
|
261
270
|
if (!logo) return null;
|
|
262
|
-
return /* @__PURE__ */
|
|
263
|
-
/* @__PURE__ */
|
|
264
|
-
activity && activity !== "idle" ? /* @__PURE__ */
|
|
271
|
+
return /* @__PURE__ */ jsxs4("span", { class: "scui-logo", "data-harness": id, "data-activity": activity, style: `--scui-logo-size:${size}px`, "aria-hidden": "true", children: [
|
|
272
|
+
/* @__PURE__ */ jsx5("svg", { viewBox: logo.viewBox, preserveAspectRatio: "xMidYMid meet", focusable: "false", children: logo.paths.map(([Tag, props], index) => /* @__PURE__ */ jsx5(Tag, { ...props }, index)) }),
|
|
273
|
+
activity && activity !== "idle" ? /* @__PURE__ */ jsx5("i", {}) : null
|
|
265
274
|
] });
|
|
266
275
|
}
|
|
267
276
|
|
|
268
277
|
// src/sessions.jsx
|
|
269
|
-
import { jsx as
|
|
278
|
+
import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
|
|
270
279
|
var sessionListMemory = /* @__PURE__ */ new Map();
|
|
271
280
|
function SessionRow({ row, state, onOpen }) {
|
|
272
281
|
const activity = sessionActivity(state, row);
|
|
273
282
|
const attentionPreview = state.attention.find((item) => item.key === row.key)?.preview;
|
|
274
283
|
const title = sessionDisplayName(row);
|
|
275
284
|
const detail = attentionPreview || row.preview || (row.name && row.name !== title ? row.name : row.cwd);
|
|
276
|
-
return /* @__PURE__ */
|
|
277
|
-
/* @__PURE__ */
|
|
278
|
-
/* @__PURE__ */
|
|
279
|
-
/* @__PURE__ */
|
|
280
|
-
/* @__PURE__ */
|
|
281
|
-
/* @__PURE__ */
|
|
285
|
+
return /* @__PURE__ */ jsxs5("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${detail ? ` \xB7 ${detail}` : ""}${row.age ? ` \xB7 ${row.age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
|
|
286
|
+
/* @__PURE__ */ jsx6(HarnessLogo, { id: row.harness, activity, size: 34 }),
|
|
287
|
+
/* @__PURE__ */ jsxs5("span", { class: "scui-session-copy", children: [
|
|
288
|
+
/* @__PURE__ */ jsxs5("span", { children: [
|
|
289
|
+
/* @__PURE__ */ jsx6("strong", { children: title }),
|
|
290
|
+
/* @__PURE__ */ jsx6("small", { children: row.age })
|
|
282
291
|
] }),
|
|
283
|
-
detail ? /* @__PURE__ */
|
|
284
|
-
state.attachError?.key === row.key ? /* @__PURE__ */
|
|
292
|
+
detail ? /* @__PURE__ */ jsx6("span", { children: /* @__PURE__ */ jsx6("small", { title: row.cwd, children: detail }) }) : null,
|
|
293
|
+
state.attachError?.key === row.key ? /* @__PURE__ */ jsx6("em", { children: state.attachError.message }) : null
|
|
285
294
|
] })
|
|
286
295
|
] });
|
|
287
296
|
}
|
|
@@ -312,33 +321,33 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
312
321
|
Promise.resolve(result).then(() => setLoadingMore(false), () => setLoadingMore(false));
|
|
313
322
|
}
|
|
314
323
|
};
|
|
315
|
-
return /* @__PURE__ */
|
|
316
|
-
/* @__PURE__ */
|
|
317
|
-
/* @__PURE__ */
|
|
318
|
-
/* @__PURE__ */
|
|
319
|
-
/* @__PURE__ */
|
|
324
|
+
return /* @__PURE__ */ jsxs5("section", { class: "scui-list", ref: root, children: [
|
|
325
|
+
/* @__PURE__ */ jsxs5("header", { class: "scui-head", children: [
|
|
326
|
+
/* @__PURE__ */ jsxs5("span", { class: "scui-head-copy", children: [
|
|
327
|
+
/* @__PURE__ */ jsx6("strong", { children: labels.chats }),
|
|
328
|
+
/* @__PURE__ */ jsxs5("small", { children: [
|
|
320
329
|
state.sessions.length,
|
|
321
330
|
" recent conversations"
|
|
322
331
|
] })
|
|
323
332
|
] }),
|
|
324
|
-
/* @__PURE__ */
|
|
325
|
-
onClose ? /* @__PURE__ */
|
|
333
|
+
/* @__PURE__ */ jsx6("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx6(UiIcon, { name: "plus", size: 18 }) }),
|
|
334
|
+
onClose ? /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx6(UiIcon, { name: "close", size: 18 }) }) : null
|
|
326
335
|
] }),
|
|
327
|
-
state.startup !== "ready" ? /* @__PURE__ */
|
|
328
|
-
state.sessions.length > 4 ? /* @__PURE__ */
|
|
329
|
-
/* @__PURE__ */
|
|
330
|
-
/* @__PURE__ */
|
|
336
|
+
state.startup !== "ready" ? /* @__PURE__ */ jsx6(LoadingStatus, { state, compact: rows.length > 0 }) : null,
|
|
337
|
+
state.sessions.length > 4 ? /* @__PURE__ */ jsxs5("label", { class: "scui-search", children: [
|
|
338
|
+
/* @__PURE__ */ jsx6(UiIcon, { name: "search", size: 15 }),
|
|
339
|
+
/* @__PURE__ */ jsx6("input", { type: "search", "aria-label": labels.searchChats, placeholder: labels.searchChats, value: query, onInput: (event) => {
|
|
331
340
|
const value = event.currentTarget.value;
|
|
332
341
|
setQuery(value);
|
|
333
342
|
boundedSet(sessionListMemory, memoryKey, { query: value, top: 0 });
|
|
334
343
|
if (rowScroller.current) rowScroller.current.scrollTop = 0;
|
|
335
344
|
} }),
|
|
336
|
-
/* @__PURE__ */
|
|
345
|
+
/* @__PURE__ */ jsx6("small", { children: rows.length })
|
|
337
346
|
] }) : null,
|
|
338
|
-
/* @__PURE__ */
|
|
339
|
-
!rows.length && state.startup === "ready" ? /* @__PURE__ */
|
|
340
|
-
rows.map((row) => /* @__PURE__ */
|
|
341
|
-
state.history.hasMoreSessions ? /* @__PURE__ */
|
|
347
|
+
/* @__PURE__ */ jsxs5("div", { class: "scui-session-rows", ref: rowScroller, onScroll: (event) => boundedSet(sessionListMemory, memoryKey, { query, top: event.currentTarget.scrollTop }), children: [
|
|
348
|
+
!rows.length && state.startup === "ready" ? /* @__PURE__ */ jsx6("div", { class: "scui-empty", children: query ? "No chats match your search." : state.error ?? "No coding chats found." }) : null,
|
|
349
|
+
rows.map((row) => /* @__PURE__ */ jsx6(Row, { value: row, row, state, adapter, onOpen }, row.key)),
|
|
350
|
+
state.history.hasMoreSessions ? /* @__PURE__ */ jsx6("button", { class: "scui-load", type: "button", disabled: loadingMore, onClick: loadMore, children: loadingMore ? "Loading older chats\u2026" : "Load older chats" }) : null
|
|
342
351
|
] })
|
|
343
352
|
] });
|
|
344
353
|
}
|
package/styles.css
CHANGED
|
@@ -173,6 +173,8 @@
|
|
|
173
173
|
.scui-envelope { display:flex; align-items:flex-end; gap:7px; padding:7px; border:1px solid var(--scui-border-strong); border-radius:16px; background:var(--scui-bg) }.scui-envelope textarea { flex:1; min-width:0; min-height:34px; max-height:150px; resize:none; overflow-y:hidden; border:0; outline:0; background:transparent; color:var(--scui-fg) }.scui-envelope > span { display:flex; gap:4px }
|
|
174
174
|
.scui-attach { display:grid; flex:0 0 34px; width:34px; height:34px; place-items:center; border:0; border-radius:50%; background:transparent; color:var(--scui-muted) }.scui-attach:hover:not(:disabled) { background:var(--scui-fill); color:var(--scui-fg) }
|
|
175
175
|
.scui-compose-context { display:flex; gap:5px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-context > span { display:flex; flex:0 0 auto; max-width:210px; align-items:center; gap:5px; padding:4px 5px 4px 7px; border:1px solid var(--scui-border); border-radius:999px; background:var(--scui-fill); font-size:10px }.scui-compose-context strong { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-compose-context button { display:grid; width:18px; height:18px; padding:0; place-items:center; border:0; border-radius:50%; background:transparent }.scui-compose-context button:hover { background:color-mix(in srgb,var(--scui-fg) 9%,transparent) }.scui-context-error { display:block; margin:0 2px 6px; color:var(--scui-danger) }
|
|
176
|
+
.scui-compose-images { display:flex; gap:6px; margin-bottom:6px; overflow-x:auto; scrollbar-width:thin }.scui-compose-images > span { position:relative; display:grid; flex:0 0 58px; width:58px; height:58px; overflow:hidden; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-fill); place-items:center }.scui-compose-images img { width:100%; height:100%; object-fit:cover }.scui-compose-images strong { position:absolute; right:2px; bottom:2px; left:2px; overflow:hidden; padding:2px 3px; border-radius:4px; background:rgba(0,0,0,.65); color:#fff; font-size:8px; font-weight:500; text-overflow:ellipsis; white-space:nowrap }.scui-compose-images button { position:absolute; z-index:1; top:2px; right:2px; display:grid; width:19px; height:19px; padding:0; border:1px solid rgba(255,255,255,.32); border-radius:50%; background:rgba(0,0,0,.68); color:#fff; place-items:center; cursor:pointer }
|
|
177
|
+
.scui-message-images { display:flex; max-width:280px; gap:5px; margin-bottom:6px; flex-wrap:wrap }.scui-message-images img { display:block; width:auto; min-width:72px; max-width:100%; height:auto; max-height:210px; border:1px solid var(--scui-border); border-radius:8px; object-fit:contain; background:var(--scui-fill) }.scui-message-images > span { display:flex; align-items:center; gap:5px; padding:5px 7px; border:1px solid var(--scui-border); border-radius:7px; color:var(--scui-muted); font-size:10px }
|
|
176
178
|
.scui-send,.scui-stop { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:var(--scui-accent); color:#fff; cursor:pointer }.scui-stop { background:var(--scui-danger) }.scui-send:disabled,.scui-stop:disabled { opacity:.4; cursor:default }.scui-control-spinner { box-sizing:border-box; width:13px; height:13px; border:1.5px solid currentColor; border-right-color:transparent; border-radius:50%; animation:scui-spin .75s linear infinite }
|
|
177
179
|
.scui-queue { display:grid; gap:4px; max-height:85px; margin-bottom:6px; overflow:auto; font-size:10.5px }.scui-queue > span { display:flex; gap:6px; padding:4px 6px; border-radius:5px; background:var(--scui-fill) }.scui-queue > span > span { display:grid; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-queue small { color:var(--scui-muted) }.scui-queue > span button { margin-left:auto; border:0; background:transparent }
|
|
178
180
|
.scui-harness-picker { display:flex; align-items:center; gap:7px; margin-bottom:7px }.scui-harness-picker select { margin-left:auto; max-width:55%; padding:4px; border:1px solid var(--scui-border); border-radius:6px; background:var(--scui-bg); color:var(--scui-fg) }
|