@volter-ai-dev/supercode-ui 0.1.23 → 0.1.25
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 +9 -3
- package/components.mjs +91 -10
- package/composer.mjs +1 -0
- package/controller.d.ts +20 -0
- package/controller.mjs +69 -12
- package/conversation.mjs +65 -9
- package/core.mjs +3 -0
- package/embed.mjs +69 -10
- package/index.d.ts +9 -2
- package/logo.d.ts +1 -1
- package/logo.mjs +22 -0
- package/messenger.mjs +69 -10
- package/package.json +1 -1
- package/sessions.mjs +1 -0
- package/styles.css +2 -2
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
Conversation,
|
|
60
60
|
SessionList,
|
|
61
61
|
} from '@volter-ai-dev/supercode-ui/preact';
|
|
62
|
-
import { HarnessLogo } from '@volter-ai-dev/supercode-ui/preact/logo';
|
|
62
|
+
import { HarnessLogo, harnessLogoDataUrl } from '@volter-ai-dev/supercode-ui/preact/logo';
|
|
63
63
|
import { groupConversation } from '@volter-ai-dev/supercode-ui/core';
|
|
64
64
|
```
|
|
65
65
|
|
|
@@ -70,6 +70,8 @@ formatters, and intent constructors live at `supercode-ui/core` and have no DOM
|
|
|
70
70
|
For genuine partial delivery, the `preact/logo`, `preact/icon`, `preact/conversation`, `preact/sessions`,
|
|
71
71
|
`preact/composer`, and `preact/messenger` subpaths are independently tree-shaken artifacts; taking
|
|
72
72
|
the logo does not pull in Markdown, the messenger, or session-list code.
|
|
73
|
+
`harnessLogoDataUrl(id)` gives non-Preact launchers the same self-contained canonical SVG; it
|
|
74
|
+
returns `null` for an unknown harness rather than inventing a fallback identity.
|
|
73
75
|
|
|
74
76
|
## Bind directly to the headless controller
|
|
75
77
|
|
|
@@ -134,8 +136,12 @@ is exposed and must revalidate every returned item.
|
|
|
134
136
|
|
|
135
137
|
Transcript images with a browser-safe URL open in a keyboard-accessible, viewport-bounded viewer;
|
|
136
138
|
remote images expose copy-link and open-original actions, while local data images can be downloaded.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
+
`createClientProjection` keeps large historical data URLs in a projection-scoped host registry and
|
|
140
|
+
puts only stable metadata plus an opaque `reference` in `SupercodeUiState`. An embedding host may
|
|
141
|
+
implement `adapter.resolveImage` to fetch that reference only after a click and return a bounded
|
|
142
|
+
`Blob`; the viewer owns and revokes the resulting object URL and presents loading, failure, and
|
|
143
|
+
retry states. Without that adapter—or when native data is incomplete—the attachment remains an
|
|
144
|
+
honest unavailable-preview state rather than a broken thumbnail or deceptive action.
|
|
139
145
|
|
|
140
146
|
An embedding product such as Vibewaiting should therefore be small: Lucarne owns its iframe and
|
|
141
147
|
launcher lifecycle, Supercode owns this UI and the controller semantics, and Vibewaiting only
|
package/components.mjs
CHANGED
|
@@ -507,7 +507,10 @@ function readTranscript(value) {
|
|
|
507
507
|
return image && typeof image.label === "string" ? [{
|
|
508
508
|
...typeof image.id === "string" ? { id: image.id } : {},
|
|
509
509
|
label: image.label,
|
|
510
|
-
...typeof image.url === "string" ? { url: image.url } : {}
|
|
510
|
+
...typeof image.url === "string" ? { url: image.url } : {},
|
|
511
|
+
...typeof image.reference === "string" && image.reference.length <= 4e3 ? { reference: image.reference } : {},
|
|
512
|
+
...typeof image.mediaType === "string" && image.mediaType.startsWith("image/") && image.mediaType.length <= 100 ? { mediaType: image.mediaType } : {},
|
|
513
|
+
...Number.isSafeInteger(image.byteSize) && image.byteSize >= 0 ? { byteSize: image.byteSize } : {}
|
|
511
514
|
}] : [];
|
|
512
515
|
}).slice(0, 4);
|
|
513
516
|
}
|
|
@@ -820,6 +823,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-ru
|
|
|
820
823
|
var MAX_CONTEXT_ITEMS = 32;
|
|
821
824
|
var MAX_IMAGE_ITEMS = 4;
|
|
822
825
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
826
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
823
827
|
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
824
828
|
function normalizeContext(value) {
|
|
825
829
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
@@ -922,22 +926,59 @@ function imageFilename(label) {
|
|
|
922
926
|
function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
923
927
|
const dialog = useRef(null);
|
|
924
928
|
const reset = useRef(null);
|
|
929
|
+
const resolutions = useRef(/* @__PURE__ */ new Map());
|
|
930
|
+
const alive = useRef(true);
|
|
931
|
+
const [, redraw] = useState(0);
|
|
925
932
|
const [copyState, setCopyState2] = useState("idle");
|
|
926
933
|
const item = items[index];
|
|
927
|
-
const
|
|
934
|
+
const key = item?.reference ?? item?.url ?? `${item?.id ?? ""}:${index}`;
|
|
935
|
+
const resolution = item?.url ? null : resolutions.current.get(key);
|
|
936
|
+
const imageUrl = item?.url ?? (resolution?.status === "ready" ? resolution.url : null);
|
|
937
|
+
const remote = imageUrl?.startsWith("http://") || imageUrl?.startsWith("https://");
|
|
938
|
+
const resolve = (candidate, force = false) => {
|
|
939
|
+
if (candidate?.url || !candidate?.reference || !adapter?.resolveImage) return;
|
|
940
|
+
const candidateKey = candidate.reference;
|
|
941
|
+
const current = resolutions.current.get(candidateKey);
|
|
942
|
+
if (!force && (current?.status === "loading" || current?.status === "ready")) return;
|
|
943
|
+
if (current?.url) URL.revokeObjectURL(current.url);
|
|
944
|
+
resolutions.current.set(candidateKey, { status: "loading" });
|
|
945
|
+
redraw((value) => value + 1);
|
|
946
|
+
Promise.resolve().then(() => adapter.resolveImage(candidate)).then((blob) => {
|
|
947
|
+
if (!(blob instanceof Blob) || !blob.type.startsWith("image/")) throw new Error("The host returned an invalid image.");
|
|
948
|
+
if (blob.size > MAX_RESOLVED_IMAGE_BYTES) throw new Error("This image is too large to preview safely.");
|
|
949
|
+
if (!alive.current) return;
|
|
950
|
+
const url = URL.createObjectURL(blob);
|
|
951
|
+
resolutions.current.set(candidateKey, { status: "ready", url });
|
|
952
|
+
redraw((value) => value + 1);
|
|
953
|
+
}).catch((error) => {
|
|
954
|
+
if (!alive.current) return;
|
|
955
|
+
resolutions.current.set(candidateKey, {
|
|
956
|
+
status: "error",
|
|
957
|
+
message: error instanceof Error && error.message ? error.message : "Could not load this image."
|
|
958
|
+
});
|
|
959
|
+
redraw((value) => value + 1);
|
|
960
|
+
});
|
|
961
|
+
};
|
|
928
962
|
useEffect(() => {
|
|
963
|
+
alive.current = true;
|
|
929
964
|
if (!dialog.current?.open) dialog.current?.showModal();
|
|
930
|
-
return () =>
|
|
965
|
+
return () => {
|
|
966
|
+
alive.current = false;
|
|
967
|
+
clearTimeout(reset.current);
|
|
968
|
+
for (const value of resolutions.current.values()) if (value.url) URL.revokeObjectURL(value.url);
|
|
969
|
+
resolutions.current.clear();
|
|
970
|
+
};
|
|
931
971
|
}, []);
|
|
932
972
|
useEffect(() => {
|
|
933
973
|
clearTimeout(reset.current);
|
|
934
974
|
setCopyState2("idle");
|
|
935
|
-
|
|
936
|
-
|
|
975
|
+
resolve(item);
|
|
976
|
+
}, [index, item?.reference]);
|
|
977
|
+
if (!item || !item.url && (!item.reference || !adapter?.resolveImage)) return null;
|
|
937
978
|
const move = (amount) => onChange((index + amount + items.length) % items.length);
|
|
938
979
|
const copy = async () => {
|
|
939
980
|
try {
|
|
940
|
-
await adapter.copyText(
|
|
981
|
+
await adapter.copyText(imageUrl);
|
|
941
982
|
setCopyState2("copied");
|
|
942
983
|
} catch {
|
|
943
984
|
setCopyState2("failed");
|
|
@@ -973,12 +1014,21 @@ function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
|
973
1014
|
] }),
|
|
974
1015
|
/* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
|
|
975
1016
|
remote && adapter?.copyText ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": copyState === "copied" ? "Image link copied" : copyState === "failed" ? "Could not copy image link" : "Copy image link", title: "Copy image link", "data-status": copyState, onClick: copy, children: /* @__PURE__ */ jsx2(UiIcon, { name: copyState === "copied" ? "check" : "copy", size: 16 }) }) : null,
|
|
976
|
-
remote ? /* @__PURE__ */ jsx2("a", { href:
|
|
1017
|
+
imageUrl ? remote ? /* @__PURE__ */ jsx2("a", { href: imageUrl, target: "_blank", rel: "noreferrer", "aria-label": "Open original image", title: "Open original image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }) }) : /* @__PURE__ */ jsx2("a", { href: imageUrl, download: imageFilename(item.label), "aria-label": "Download image", title: "Download image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "down", size: 16 }) }) : null,
|
|
977
1018
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 17 }) })
|
|
978
1019
|
] })
|
|
979
1020
|
] }),
|
|
980
1021
|
/* @__PURE__ */ jsxs2("figure", { children: [
|
|
981
|
-
/* @__PURE__ */ jsx2("img", { src:
|
|
1022
|
+
imageUrl ? /* @__PURE__ */ jsx2("img", { src: imageUrl, alt: item.label }) : resolution?.status === "error" ? /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "alert", children: [
|
|
1023
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 28 }),
|
|
1024
|
+
/* @__PURE__ */ jsx2("strong", { children: "Could not load image" }),
|
|
1025
|
+
/* @__PURE__ */ jsx2("small", { children: resolution.message }),
|
|
1026
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => resolve(item, true), children: "Retry" })
|
|
1027
|
+
] }) : /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "status", children: [
|
|
1028
|
+
/* @__PURE__ */ jsx2("i", { class: "scui-control-spinner" }),
|
|
1029
|
+
/* @__PURE__ */ jsx2("strong", { children: "Loading image\u2026" }),
|
|
1030
|
+
/* @__PURE__ */ jsx2("small", { children: "The original stays out of the transcript payload." })
|
|
1031
|
+
] }),
|
|
982
1032
|
items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
983
1033
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) }),
|
|
984
1034
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) })
|
|
@@ -992,7 +1042,7 @@ function MessageImages({ items, adapter }) {
|
|
|
992
1042
|
const [active, setActive] = useState(null);
|
|
993
1043
|
const opener = useRef(null);
|
|
994
1044
|
if (!items?.length) return null;
|
|
995
|
-
const viewable = items.filter((item) => item.url);
|
|
1045
|
+
const viewable = items.filter((item) => item.url || item.reference && adapter?.resolveImage);
|
|
996
1046
|
const close = () => {
|
|
997
1047
|
setActive(null);
|
|
998
1048
|
requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
|
|
@@ -1001,7 +1051,16 @@ function MessageImages({ items, adapter }) {
|
|
|
1001
1051
|
/* @__PURE__ */ jsx2("div", { class: "scui-message-images", "aria-label": "Message images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `View image ${item.label}`, onClick: (event) => {
|
|
1002
1052
|
opener.current = event.currentTarget;
|
|
1003
1053
|
setActive(viewable.indexOf(item));
|
|
1004
|
-
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("
|
|
1054
|
+
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : item.reference && adapter?.resolveImage ? /* @__PURE__ */ jsxs2("button", { type: "button", "data-lazy": "true", "aria-label": `Load image ${item.label}`, onClick: (event) => {
|
|
1055
|
+
opener.current = event.currentTarget;
|
|
1056
|
+
setActive(viewable.indexOf(item));
|
|
1057
|
+
}, children: [
|
|
1058
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }),
|
|
1059
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1060
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
1061
|
+
/* @__PURE__ */ jsx2("small", { children: "Load preview" })
|
|
1062
|
+
] })
|
|
1063
|
+
] }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
|
|
1005
1064
|
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
1006
1065
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1007
1066
|
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
@@ -1893,6 +1952,27 @@ var LOGOS = {
|
|
|
1893
1952
|
function hasHarnessLogo(id) {
|
|
1894
1953
|
return Object.hasOwn(LOGOS, id);
|
|
1895
1954
|
}
|
|
1955
|
+
var LOGO_CHROME = {
|
|
1956
|
+
codex: { background: "#f7f7f5", color: "#111" },
|
|
1957
|
+
gemini: { background: "#f7f7f5", color: "#6e79dc" },
|
|
1958
|
+
goose: { background: "#f7f7f5", color: "#101010" },
|
|
1959
|
+
pi: { background: "#efefeb", color: "#111" },
|
|
1960
|
+
supercode: { background: "#111923", color: "#f7f7f5" }
|
|
1961
|
+
};
|
|
1962
|
+
function escapeSvgAttribute(value) {
|
|
1963
|
+
return String(value).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
1964
|
+
}
|
|
1965
|
+
function harnessLogoDataUrl(id) {
|
|
1966
|
+
const logo = LOGOS[id];
|
|
1967
|
+
if (!logo) return null;
|
|
1968
|
+
const chrome = LOGO_CHROME[id] ?? { background: "#111", color: id === "claude-code" ? "#d97757" : "#f7f7f5" };
|
|
1969
|
+
const glyph = logo.paths.map(([tag, attributes]) => {
|
|
1970
|
+
const serialized = Object.entries(attributes).map(([name, value]) => `${name}="${escapeSvgAttribute(value)}"`).join(" ");
|
|
1971
|
+
return `<${tag} ${serialized}/>`;
|
|
1972
|
+
}).join("");
|
|
1973
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="20" cy="20" r="19" fill="${chrome.background}" stroke="#383a42"/><svg x="7" y="7" width="26" height="26" viewBox="${logo.viewBox}" color="${chrome.color}" fill="currentColor" preserveAspectRatio="xMidYMid meet">${glyph}</svg></svg>`;
|
|
1974
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
1975
|
+
}
|
|
1896
1976
|
function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
1897
1977
|
const logo = LOGOS[id];
|
|
1898
1978
|
useEffect5(() => {
|
|
@@ -2476,5 +2556,6 @@ export {
|
|
|
2476
2556
|
ToolRow,
|
|
2477
2557
|
TranscriptEntry,
|
|
2478
2558
|
UiIcon,
|
|
2559
|
+
harnessLogoDataUrl,
|
|
2479
2560
|
hasHarnessLogo
|
|
2480
2561
|
};
|
package/composer.mjs
CHANGED
|
@@ -134,6 +134,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-ru
|
|
|
134
134
|
var MAX_CONTEXT_ITEMS = 32;
|
|
135
135
|
var MAX_IMAGE_ITEMS = 4;
|
|
136
136
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
137
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
137
138
|
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
138
139
|
function normalizeContext(value) {
|
|
139
140
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
package/controller.d.ts
CHANGED
|
@@ -52,6 +52,25 @@ export function projectClientSnapshot(
|
|
|
52
52
|
options?: ClientProjectionOptions,
|
|
53
53
|
): SupercodeUiState;
|
|
54
54
|
|
|
55
|
+
export interface ResolvableTranscriptImage {
|
|
56
|
+
id?: string;
|
|
57
|
+
label: string;
|
|
58
|
+
url: string;
|
|
59
|
+
mediaType?: string;
|
|
60
|
+
byteSize?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ClientProjection {
|
|
64
|
+
state: SupercodeUiState;
|
|
65
|
+
/** Returns only an image admitted to this projection's bounded transcript window. */
|
|
66
|
+
resolveImage(reference: string): ResolvableTranscriptImage | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createClientProjection(
|
|
70
|
+
snapshot: SupercodeClientSnapshot,
|
|
71
|
+
options?: ClientProjectionOptions,
|
|
72
|
+
): ClientProjection;
|
|
73
|
+
|
|
55
74
|
export interface ControllerBindingOptions {
|
|
56
75
|
projection?: () => ClientProjectionOptions;
|
|
57
76
|
onIntent?: (intent: SupercodeUiIntent) => void | Promise<void>;
|
|
@@ -63,6 +82,7 @@ export interface ControllerBindingOptions {
|
|
|
63
82
|
onLoadSessions?: () => void | Promise<void>;
|
|
64
83
|
onLoadEarlier?: () => void | Promise<void>;
|
|
65
84
|
copyText?: UiAdapter['copyText'];
|
|
85
|
+
resolveImage?: UiAdapter['resolveImage'];
|
|
66
86
|
}
|
|
67
87
|
|
|
68
88
|
export interface SupercodeUiBinding {
|
package/controller.mjs
CHANGED
|
@@ -99,18 +99,53 @@ function projectContext(context) {
|
|
|
99
99
|
return projected.length ? projected : undefined;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function
|
|
102
|
+
function imageMetadata(url) {
|
|
103
|
+
if (!url.startsWith('data:image/')) return {};
|
|
104
|
+
const header = url.slice(5, 256);
|
|
105
|
+
const comma = header.indexOf(',');
|
|
106
|
+
if (comma < 0) return {};
|
|
107
|
+
const declaration = header.slice(0, comma);
|
|
108
|
+
const mediaType = declaration.split(';', 1)[0].toLowerCase();
|
|
109
|
+
if (!mediaType.startsWith('image/')) return {};
|
|
110
|
+
if (!declaration.toLowerCase().endsWith(';base64')) return { mediaType };
|
|
111
|
+
const payloadLength = url.length - 5 - comma - 1;
|
|
112
|
+
const padding = url.endsWith('==') ? 2 : url.endsWith('=') ? 1 : 0;
|
|
113
|
+
return { mediaType, byteSize: Math.max(0, Math.floor(payloadLength * 3 / 4) - padding) };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function imageReference(entryId, item, index) {
|
|
117
|
+
// The host treats this only as a key into the projection-scoped registry below. Keeping the key
|
|
118
|
+
// deterministic prevents a streamed transcript refresh from restarting an image already open in
|
|
119
|
+
// the viewer, while the registry still rejects references outside the admitted render window.
|
|
120
|
+
return JSON.stringify([entryId, typeof item.id === 'string' ? item.id : null, index]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function projectImages(images, entryId, imageRegistry) {
|
|
103
124
|
if (!Array.isArray(images)) return undefined;
|
|
104
|
-
const projected = images.flatMap((item) => {
|
|
125
|
+
const projected = images.flatMap((item, index) => {
|
|
105
126
|
if (!item || typeof item !== 'object' || typeof item.label !== 'string') return [];
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
127
|
+
const sourceUrl = typeof item.url === 'string' ? item.url : null;
|
|
128
|
+
const complete = sourceUrl !== null && !sourceUrl.endsWith('\n…');
|
|
129
|
+
const metadata = complete ? imageMetadata(sourceUrl) : {};
|
|
130
|
+
const url = complete && (!sourceUrl.startsWith('data:image/') || sourceUrl.length <= 256_000)
|
|
131
|
+
? sourceUrl
|
|
109
132
|
: null;
|
|
133
|
+
let reference = null;
|
|
134
|
+
if (complete && sourceUrl.startsWith('data:image/') && !url && imageRegistry && Number.isSafeInteger(metadata.byteSize)) {
|
|
135
|
+
reference = imageReference(entryId, item, index);
|
|
136
|
+
imageRegistry.set(reference, {
|
|
137
|
+
...(typeof item.id === 'string' ? { id: item.id } : {}),
|
|
138
|
+
label: item.label,
|
|
139
|
+
url: sourceUrl,
|
|
140
|
+
...metadata,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
110
143
|
return [{
|
|
111
144
|
...(typeof item.id === 'string' ? { id: item.id } : {}),
|
|
112
145
|
label: item.label,
|
|
146
|
+
...metadata,
|
|
113
147
|
...(url ? { url } : {}),
|
|
148
|
+
...(reference ? { reference } : {}),
|
|
114
149
|
}];
|
|
115
150
|
}).slice(0, 4);
|
|
116
151
|
return projected.length ? projected : undefined;
|
|
@@ -124,13 +159,13 @@ function requestSummary(entry) {
|
|
|
124
159
|
return options ? `${entry.requestKind}: ${options}${resolution}` : `${entry.requestKind ?? 'request'}${resolution}`;
|
|
125
160
|
}
|
|
126
161
|
|
|
127
|
-
function projectConversationEntry(entry, maxEntryChars) {
|
|
162
|
+
function projectConversationEntry(entry, maxEntryChars, imageRegistry) {
|
|
128
163
|
if (!entry || typeof entry !== 'object' || typeof entry.id !== 'string') return null;
|
|
129
164
|
if (entry.kind === 'message') {
|
|
130
165
|
if (entry.visibility === 'context') return null;
|
|
131
166
|
const body = truncate(entry.text, maxEntryChars);
|
|
132
167
|
const context = projectContext(entry.context);
|
|
133
|
-
const images = projectImages(entry.images);
|
|
168
|
+
const images = projectImages(entry.images, entry.id, imageRegistry);
|
|
134
169
|
return {
|
|
135
170
|
id: entry.id,
|
|
136
171
|
role: entry.role,
|
|
@@ -201,7 +236,7 @@ function projectConversationEntry(entry, maxEntryChars) {
|
|
|
201
236
|
return null;
|
|
202
237
|
}
|
|
203
238
|
|
|
204
|
-
function projectConversation(conversation, options) {
|
|
239
|
+
function projectConversation(conversation, options, imageRegistry) {
|
|
205
240
|
if (!Array.isArray(conversation)) return [];
|
|
206
241
|
const maxEntries = positiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
207
242
|
const maxEntryChars = positiveInteger(options.maxEntryChars, DEFAULT_MAX_ENTRY_CHARS);
|
|
@@ -215,7 +250,7 @@ function projectConversation(conversation, options) {
|
|
|
215
250
|
for (;
|
|
216
251
|
index >= 0 && rows.length < maxEntries && scanned < maxScanEntries;
|
|
217
252
|
index -= 1, scanned += 1) {
|
|
218
|
-
const row = projectConversationEntry(conversation[index], maxEntryChars);
|
|
253
|
+
const row = projectConversationEntry(conversation[index], maxEntryChars, imageRegistry);
|
|
219
254
|
if (row) rows.push(row);
|
|
220
255
|
}
|
|
221
256
|
|
|
@@ -229,7 +264,7 @@ function projectConversation(conversation, options) {
|
|
|
229
264
|
rows.length = oldestVisibleUser + 1;
|
|
230
265
|
} else {
|
|
231
266
|
for (; index >= 0 && scanned < maxScanEntries; index -= 1, scanned += 1) {
|
|
232
|
-
const row = projectConversationEntry(conversation[index], maxEntryChars);
|
|
267
|
+
const row = projectConversationEntry(conversation[index], maxEntryChars, imageRegistry);
|
|
233
268
|
if (row?.role !== 'user') continue;
|
|
234
269
|
if (rows.length === maxEntries) rows[rows.length - 1] = row;
|
|
235
270
|
else rows.push(row);
|
|
@@ -300,7 +335,7 @@ function projectPill(snapshot) {
|
|
|
300
335
|
return { tone: 'live', label: text(`${label(harness)} ready`) };
|
|
301
336
|
}
|
|
302
337
|
|
|
303
|
-
|
|
338
|
+
function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
|
|
304
339
|
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
305
340
|
const busy = ['running', 'interrupting', 'reconciling'].includes(snapshot.turn?.state);
|
|
306
341
|
const actions = snapshot.availableActions ?? {};
|
|
@@ -325,7 +360,7 @@ export function projectClientSnapshot(snapshot, options = {}) {
|
|
|
325
360
|
? snapshot.connection?.ownsRuntime ? active : null
|
|
326
361
|
: options.owned;
|
|
327
362
|
const maxEntries = positiveInteger(options.maxEntries, DEFAULT_MAX_ENTRIES);
|
|
328
|
-
const transcript = projectConversation(snapshot.conversation ?? [], options);
|
|
363
|
+
const transcript = projectConversation(snapshot.conversation ?? [], options, imageRegistry);
|
|
329
364
|
const startup = snapshot.availability === 'loading'
|
|
330
365
|
? snapshot.operation === 'start' ? 'starting' : snapshot.operation === 'refresh' ? 'discovering' : 'connecting'
|
|
331
366
|
: 'ready';
|
|
@@ -390,6 +425,27 @@ export function projectClientSnapshot(snapshot, options = {}) {
|
|
|
390
425
|
});
|
|
391
426
|
}
|
|
392
427
|
|
|
428
|
+
export function projectClientSnapshot(snapshot, options = {}) {
|
|
429
|
+
return projectClientSnapshotInternal(snapshot, options, null);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Project a bounded UI state together with a host-only resolver for data images omitted from that
|
|
434
|
+
* state. The resolver knows only the images admitted by this exact projection, so a browser can
|
|
435
|
+
* neither force a whole-history scan nor use a guessed reference to read an unrelated session.
|
|
436
|
+
*/
|
|
437
|
+
export function createClientProjection(snapshot, options = {}) {
|
|
438
|
+
const images = new Map();
|
|
439
|
+
const state = projectClientSnapshotInternal(snapshot, options, images);
|
|
440
|
+
return {
|
|
441
|
+
state,
|
|
442
|
+
resolveImage(reference) {
|
|
443
|
+
const image = typeof reference === 'string' ? images.get(reference) : null;
|
|
444
|
+
return image ? { ...image } : null;
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
393
449
|
async function dispatchStandard(controller, intent, options) {
|
|
394
450
|
const snapshot = controller.getSnapshot();
|
|
395
451
|
const active = snapshot.activeSessionKey;
|
|
@@ -433,6 +489,7 @@ export function createControllerBinding(controller, options = {}) {
|
|
|
433
489
|
.then(() => undefined);
|
|
434
490
|
},
|
|
435
491
|
...(options.copyText ? { copyText: options.copyText } : {}),
|
|
492
|
+
...(options.resolveImage ? { resolveImage: options.resolveImage } : {}),
|
|
436
493
|
};
|
|
437
494
|
return {
|
|
438
495
|
adapter,
|
package/conversation.mjs
CHANGED
|
@@ -625,6 +625,7 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
625
625
|
import { useEffect as useEffect2, useRef as useRef2, useState } from "preact/hooks";
|
|
626
626
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
627
627
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
628
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
628
629
|
function imageFilename(label) {
|
|
629
630
|
const value = label.trim().replace(/[\\/:*?"<>|]+/g, "-");
|
|
630
631
|
return value || "image";
|
|
@@ -632,22 +633,59 @@ function imageFilename(label) {
|
|
|
632
633
|
function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
633
634
|
const dialog = useRef2(null);
|
|
634
635
|
const reset = useRef2(null);
|
|
636
|
+
const resolutions = useRef2(/* @__PURE__ */ new Map());
|
|
637
|
+
const alive = useRef2(true);
|
|
638
|
+
const [, redraw] = useState(0);
|
|
635
639
|
const [copyState, setCopyState2] = useState("idle");
|
|
636
640
|
const item = items[index];
|
|
637
|
-
const
|
|
641
|
+
const key = item?.reference ?? item?.url ?? `${item?.id ?? ""}:${index}`;
|
|
642
|
+
const resolution = item?.url ? null : resolutions.current.get(key);
|
|
643
|
+
const imageUrl = item?.url ?? (resolution?.status === "ready" ? resolution.url : null);
|
|
644
|
+
const remote = imageUrl?.startsWith("http://") || imageUrl?.startsWith("https://");
|
|
645
|
+
const resolve = (candidate, force = false) => {
|
|
646
|
+
if (candidate?.url || !candidate?.reference || !adapter?.resolveImage) return;
|
|
647
|
+
const candidateKey = candidate.reference;
|
|
648
|
+
const current = resolutions.current.get(candidateKey);
|
|
649
|
+
if (!force && (current?.status === "loading" || current?.status === "ready")) return;
|
|
650
|
+
if (current?.url) URL.revokeObjectURL(current.url);
|
|
651
|
+
resolutions.current.set(candidateKey, { status: "loading" });
|
|
652
|
+
redraw((value) => value + 1);
|
|
653
|
+
Promise.resolve().then(() => adapter.resolveImage(candidate)).then((blob) => {
|
|
654
|
+
if (!(blob instanceof Blob) || !blob.type.startsWith("image/")) throw new Error("The host returned an invalid image.");
|
|
655
|
+
if (blob.size > MAX_RESOLVED_IMAGE_BYTES) throw new Error("This image is too large to preview safely.");
|
|
656
|
+
if (!alive.current) return;
|
|
657
|
+
const url = URL.createObjectURL(blob);
|
|
658
|
+
resolutions.current.set(candidateKey, { status: "ready", url });
|
|
659
|
+
redraw((value) => value + 1);
|
|
660
|
+
}).catch((error) => {
|
|
661
|
+
if (!alive.current) return;
|
|
662
|
+
resolutions.current.set(candidateKey, {
|
|
663
|
+
status: "error",
|
|
664
|
+
message: error instanceof Error && error.message ? error.message : "Could not load this image."
|
|
665
|
+
});
|
|
666
|
+
redraw((value) => value + 1);
|
|
667
|
+
});
|
|
668
|
+
};
|
|
638
669
|
useEffect2(() => {
|
|
670
|
+
alive.current = true;
|
|
639
671
|
if (!dialog.current?.open) dialog.current?.showModal();
|
|
640
|
-
return () =>
|
|
672
|
+
return () => {
|
|
673
|
+
alive.current = false;
|
|
674
|
+
clearTimeout(reset.current);
|
|
675
|
+
for (const value of resolutions.current.values()) if (value.url) URL.revokeObjectURL(value.url);
|
|
676
|
+
resolutions.current.clear();
|
|
677
|
+
};
|
|
641
678
|
}, []);
|
|
642
679
|
useEffect2(() => {
|
|
643
680
|
clearTimeout(reset.current);
|
|
644
681
|
setCopyState2("idle");
|
|
645
|
-
|
|
646
|
-
|
|
682
|
+
resolve(item);
|
|
683
|
+
}, [index, item?.reference]);
|
|
684
|
+
if (!item || !item.url && (!item.reference || !adapter?.resolveImage)) return null;
|
|
647
685
|
const move = (amount) => onChange((index + amount + items.length) % items.length);
|
|
648
686
|
const copy = async () => {
|
|
649
687
|
try {
|
|
650
|
-
await adapter.copyText(
|
|
688
|
+
await adapter.copyText(imageUrl);
|
|
651
689
|
setCopyState2("copied");
|
|
652
690
|
} catch {
|
|
653
691
|
setCopyState2("failed");
|
|
@@ -683,12 +721,21 @@ function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
|
683
721
|
] }),
|
|
684
722
|
/* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
|
|
685
723
|
remote && adapter?.copyText ? /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": copyState === "copied" ? "Image link copied" : copyState === "failed" ? "Could not copy image link" : "Copy image link", title: "Copy image link", "data-status": copyState, onClick: copy, children: /* @__PURE__ */ jsx3(UiIcon, { name: copyState === "copied" ? "check" : "copy", size: 16 }) }) : null,
|
|
686
|
-
remote ? /* @__PURE__ */ jsx3("a", { href:
|
|
724
|
+
imageUrl ? remote ? /* @__PURE__ */ jsx3("a", { href: imageUrl, target: "_blank", rel: "noreferrer", "aria-label": "Open original image", title: "Open original image", children: /* @__PURE__ */ jsx3(UiIcon, { name: "image", size: 16 }) }) : /* @__PURE__ */ jsx3("a", { href: imageUrl, download: imageFilename(item.label), "aria-label": "Download image", title: "Download image", children: /* @__PURE__ */ jsx3(UiIcon, { name: "down", size: 16 }) }) : null,
|
|
687
725
|
/* @__PURE__ */ jsx3("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx3(UiIcon, { name: "close", size: 17 }) })
|
|
688
726
|
] })
|
|
689
727
|
] }),
|
|
690
728
|
/* @__PURE__ */ jsxs2("figure", { children: [
|
|
691
|
-
/* @__PURE__ */ jsx3("img", { src:
|
|
729
|
+
imageUrl ? /* @__PURE__ */ jsx3("img", { src: imageUrl, alt: item.label }) : resolution?.status === "error" ? /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "alert", children: [
|
|
730
|
+
/* @__PURE__ */ jsx3(UiIcon, { name: "image", size: 28 }),
|
|
731
|
+
/* @__PURE__ */ jsx3("strong", { children: "Could not load image" }),
|
|
732
|
+
/* @__PURE__ */ jsx3("small", { children: resolution.message }),
|
|
733
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: () => resolve(item, true), children: "Retry" })
|
|
734
|
+
] }) : /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "status", children: [
|
|
735
|
+
/* @__PURE__ */ jsx3("i", { class: "scui-control-spinner" }),
|
|
736
|
+
/* @__PURE__ */ jsx3("strong", { children: "Loading image\u2026" }),
|
|
737
|
+
/* @__PURE__ */ jsx3("small", { children: "The original stays out of the transcript payload." })
|
|
738
|
+
] }),
|
|
692
739
|
items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
693
740
|
/* @__PURE__ */ jsx3("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx3(UiIcon, { name: "chevron", size: 19 }) }),
|
|
694
741
|
/* @__PURE__ */ jsx3("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx3(UiIcon, { name: "chevron", size: 19 }) })
|
|
@@ -702,7 +749,7 @@ function MessageImages({ items, adapter }) {
|
|
|
702
749
|
const [active, setActive] = useState(null);
|
|
703
750
|
const opener = useRef2(null);
|
|
704
751
|
if (!items?.length) return null;
|
|
705
|
-
const viewable = items.filter((item) => item.url);
|
|
752
|
+
const viewable = items.filter((item) => item.url || item.reference && adapter?.resolveImage);
|
|
706
753
|
const close = () => {
|
|
707
754
|
setActive(null);
|
|
708
755
|
requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
|
|
@@ -711,7 +758,16 @@ function MessageImages({ items, adapter }) {
|
|
|
711
758
|
/* @__PURE__ */ jsx3("div", { class: "scui-message-images", "aria-label": "Message images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx3("button", { type: "button", "aria-label": `View image ${item.label}`, onClick: (event) => {
|
|
712
759
|
opener.current = event.currentTarget;
|
|
713
760
|
setActive(viewable.indexOf(item));
|
|
714
|
-
}, children: /* @__PURE__ */ jsx3("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("
|
|
761
|
+
}, children: /* @__PURE__ */ jsx3("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : item.reference && adapter?.resolveImage ? /* @__PURE__ */ jsxs2("button", { type: "button", "data-lazy": "true", "aria-label": `Load image ${item.label}`, onClick: (event) => {
|
|
762
|
+
opener.current = event.currentTarget;
|
|
763
|
+
setActive(viewable.indexOf(item));
|
|
764
|
+
}, children: [
|
|
765
|
+
/* @__PURE__ */ jsx3(UiIcon, { name: "image", size: 16 }),
|
|
766
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
767
|
+
/* @__PURE__ */ jsx3("strong", { children: item.label }),
|
|
768
|
+
/* @__PURE__ */ jsx3("small", { children: "Load preview" })
|
|
769
|
+
] })
|
|
770
|
+
] }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
|
|
715
771
|
/* @__PURE__ */ jsx3(UiIcon, { name: "image", size: 14 }),
|
|
716
772
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
717
773
|
/* @__PURE__ */ jsx3("strong", { children: item.label }),
|
package/core.mjs
CHANGED
|
@@ -499,6 +499,9 @@ function readTranscript(value) {
|
|
|
499
499
|
...(typeof image.id === 'string' ? { id: image.id } : {}),
|
|
500
500
|
label: image.label,
|
|
501
501
|
...(typeof image.url === 'string' ? { url: image.url } : {}),
|
|
502
|
+
...(typeof image.reference === 'string' && image.reference.length <= 4_000 ? { reference: image.reference } : {}),
|
|
503
|
+
...(typeof image.mediaType === 'string' && image.mediaType.startsWith('image/') && image.mediaType.length <= 100 ? { mediaType: image.mediaType } : {}),
|
|
504
|
+
...(Number.isSafeInteger(image.byteSize) && image.byteSize >= 0 ? { byteSize: image.byteSize } : {}),
|
|
502
505
|
}]
|
|
503
506
|
: [];
|
|
504
507
|
}).slice(0, 4);
|
package/embed.mjs
CHANGED
|
@@ -510,7 +510,10 @@ function readTranscript(value) {
|
|
|
510
510
|
return image && typeof image.label === "string" ? [{
|
|
511
511
|
...typeof image.id === "string" ? { id: image.id } : {},
|
|
512
512
|
label: image.label,
|
|
513
|
-
...typeof image.url === "string" ? { url: image.url } : {}
|
|
513
|
+
...typeof image.url === "string" ? { url: image.url } : {},
|
|
514
|
+
...typeof image.reference === "string" && image.reference.length <= 4e3 ? { reference: image.reference } : {},
|
|
515
|
+
...typeof image.mediaType === "string" && image.mediaType.startsWith("image/") && image.mediaType.length <= 100 ? { mediaType: image.mediaType } : {},
|
|
516
|
+
...Number.isSafeInteger(image.byteSize) && image.byteSize >= 0 ? { byteSize: image.byteSize } : {}
|
|
514
517
|
}] : [];
|
|
515
518
|
}).slice(0, 4);
|
|
516
519
|
}
|
|
@@ -826,6 +829,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-ru
|
|
|
826
829
|
var MAX_CONTEXT_ITEMS = 32;
|
|
827
830
|
var MAX_IMAGE_ITEMS = 4;
|
|
828
831
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
832
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
829
833
|
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
830
834
|
function normalizeContext(value) {
|
|
831
835
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
@@ -928,22 +932,59 @@ function imageFilename(label) {
|
|
|
928
932
|
function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
929
933
|
const dialog = useRef(null);
|
|
930
934
|
const reset = useRef(null);
|
|
935
|
+
const resolutions = useRef(/* @__PURE__ */ new Map());
|
|
936
|
+
const alive = useRef(true);
|
|
937
|
+
const [, redraw] = useState(0);
|
|
931
938
|
const [copyState, setCopyState2] = useState("idle");
|
|
932
939
|
const item = items[index];
|
|
933
|
-
const
|
|
940
|
+
const key = item?.reference ?? item?.url ?? `${item?.id ?? ""}:${index}`;
|
|
941
|
+
const resolution = item?.url ? null : resolutions.current.get(key);
|
|
942
|
+
const imageUrl = item?.url ?? (resolution?.status === "ready" ? resolution.url : null);
|
|
943
|
+
const remote = imageUrl?.startsWith("http://") || imageUrl?.startsWith("https://");
|
|
944
|
+
const resolve = (candidate, force = false) => {
|
|
945
|
+
if (candidate?.url || !candidate?.reference || !adapter?.resolveImage) return;
|
|
946
|
+
const candidateKey = candidate.reference;
|
|
947
|
+
const current = resolutions.current.get(candidateKey);
|
|
948
|
+
if (!force && (current?.status === "loading" || current?.status === "ready")) return;
|
|
949
|
+
if (current?.url) URL.revokeObjectURL(current.url);
|
|
950
|
+
resolutions.current.set(candidateKey, { status: "loading" });
|
|
951
|
+
redraw((value) => value + 1);
|
|
952
|
+
Promise.resolve().then(() => adapter.resolveImage(candidate)).then((blob) => {
|
|
953
|
+
if (!(blob instanceof Blob) || !blob.type.startsWith("image/")) throw new Error("The host returned an invalid image.");
|
|
954
|
+
if (blob.size > MAX_RESOLVED_IMAGE_BYTES) throw new Error("This image is too large to preview safely.");
|
|
955
|
+
if (!alive.current) return;
|
|
956
|
+
const url = URL.createObjectURL(blob);
|
|
957
|
+
resolutions.current.set(candidateKey, { status: "ready", url });
|
|
958
|
+
redraw((value) => value + 1);
|
|
959
|
+
}).catch((error) => {
|
|
960
|
+
if (!alive.current) return;
|
|
961
|
+
resolutions.current.set(candidateKey, {
|
|
962
|
+
status: "error",
|
|
963
|
+
message: error instanceof Error && error.message ? error.message : "Could not load this image."
|
|
964
|
+
});
|
|
965
|
+
redraw((value) => value + 1);
|
|
966
|
+
});
|
|
967
|
+
};
|
|
934
968
|
useEffect(() => {
|
|
969
|
+
alive.current = true;
|
|
935
970
|
if (!dialog.current?.open) dialog.current?.showModal();
|
|
936
|
-
return () =>
|
|
971
|
+
return () => {
|
|
972
|
+
alive.current = false;
|
|
973
|
+
clearTimeout(reset.current);
|
|
974
|
+
for (const value of resolutions.current.values()) if (value.url) URL.revokeObjectURL(value.url);
|
|
975
|
+
resolutions.current.clear();
|
|
976
|
+
};
|
|
937
977
|
}, []);
|
|
938
978
|
useEffect(() => {
|
|
939
979
|
clearTimeout(reset.current);
|
|
940
980
|
setCopyState2("idle");
|
|
941
|
-
|
|
942
|
-
|
|
981
|
+
resolve(item);
|
|
982
|
+
}, [index, item?.reference]);
|
|
983
|
+
if (!item || !item.url && (!item.reference || !adapter?.resolveImage)) return null;
|
|
943
984
|
const move = (amount) => onChange((index + amount + items.length) % items.length);
|
|
944
985
|
const copy = async () => {
|
|
945
986
|
try {
|
|
946
|
-
await adapter.copyText(
|
|
987
|
+
await adapter.copyText(imageUrl);
|
|
947
988
|
setCopyState2("copied");
|
|
948
989
|
} catch {
|
|
949
990
|
setCopyState2("failed");
|
|
@@ -979,12 +1020,21 @@ function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
|
979
1020
|
] }),
|
|
980
1021
|
/* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
|
|
981
1022
|
remote && adapter?.copyText ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": copyState === "copied" ? "Image link copied" : copyState === "failed" ? "Could not copy image link" : "Copy image link", title: "Copy image link", "data-status": copyState, onClick: copy, children: /* @__PURE__ */ jsx2(UiIcon, { name: copyState === "copied" ? "check" : "copy", size: 16 }) }) : null,
|
|
982
|
-
remote ? /* @__PURE__ */ jsx2("a", { href:
|
|
1023
|
+
imageUrl ? remote ? /* @__PURE__ */ jsx2("a", { href: imageUrl, target: "_blank", rel: "noreferrer", "aria-label": "Open original image", title: "Open original image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }) }) : /* @__PURE__ */ jsx2("a", { href: imageUrl, download: imageFilename(item.label), "aria-label": "Download image", title: "Download image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "down", size: 16 }) }) : null,
|
|
983
1024
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 17 }) })
|
|
984
1025
|
] })
|
|
985
1026
|
] }),
|
|
986
1027
|
/* @__PURE__ */ jsxs2("figure", { children: [
|
|
987
|
-
/* @__PURE__ */ jsx2("img", { src:
|
|
1028
|
+
imageUrl ? /* @__PURE__ */ jsx2("img", { src: imageUrl, alt: item.label }) : resolution?.status === "error" ? /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "alert", children: [
|
|
1029
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 28 }),
|
|
1030
|
+
/* @__PURE__ */ jsx2("strong", { children: "Could not load image" }),
|
|
1031
|
+
/* @__PURE__ */ jsx2("small", { children: resolution.message }),
|
|
1032
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => resolve(item, true), children: "Retry" })
|
|
1033
|
+
] }) : /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "status", children: [
|
|
1034
|
+
/* @__PURE__ */ jsx2("i", { class: "scui-control-spinner" }),
|
|
1035
|
+
/* @__PURE__ */ jsx2("strong", { children: "Loading image\u2026" }),
|
|
1036
|
+
/* @__PURE__ */ jsx2("small", { children: "The original stays out of the transcript payload." })
|
|
1037
|
+
] }),
|
|
988
1038
|
items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
989
1039
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) }),
|
|
990
1040
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) })
|
|
@@ -998,7 +1048,7 @@ function MessageImages({ items, adapter }) {
|
|
|
998
1048
|
const [active, setActive] = useState(null);
|
|
999
1049
|
const opener = useRef(null);
|
|
1000
1050
|
if (!items?.length) return null;
|
|
1001
|
-
const viewable = items.filter((item) => item.url);
|
|
1051
|
+
const viewable = items.filter((item) => item.url || item.reference && adapter?.resolveImage);
|
|
1002
1052
|
const close = () => {
|
|
1003
1053
|
setActive(null);
|
|
1004
1054
|
requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
|
|
@@ -1007,7 +1057,16 @@ function MessageImages({ items, adapter }) {
|
|
|
1007
1057
|
/* @__PURE__ */ jsx2("div", { class: "scui-message-images", "aria-label": "Message images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `View image ${item.label}`, onClick: (event) => {
|
|
1008
1058
|
opener.current = event.currentTarget;
|
|
1009
1059
|
setActive(viewable.indexOf(item));
|
|
1010
|
-
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("
|
|
1060
|
+
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : item.reference && adapter?.resolveImage ? /* @__PURE__ */ jsxs2("button", { type: "button", "data-lazy": "true", "aria-label": `Load image ${item.label}`, onClick: (event) => {
|
|
1061
|
+
opener.current = event.currentTarget;
|
|
1062
|
+
setActive(viewable.indexOf(item));
|
|
1063
|
+
}, children: [
|
|
1064
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }),
|
|
1065
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1066
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
1067
|
+
/* @__PURE__ */ jsx2("small", { children: "Load preview" })
|
|
1068
|
+
] })
|
|
1069
|
+
] }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
|
|
1011
1070
|
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
1012
1071
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1013
1072
|
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
package/index.d.ts
CHANGED
|
@@ -50,6 +50,10 @@ export interface TranscriptImage {
|
|
|
50
50
|
id?: string;
|
|
51
51
|
label: string;
|
|
52
52
|
url?: string;
|
|
53
|
+
/** Opaque host-owned handle for an image deliberately omitted from the initial UI payload. */
|
|
54
|
+
reference?: string;
|
|
55
|
+
mediaType?: string;
|
|
56
|
+
byteSize?: number;
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
export type TranscriptAttachment = TranscriptContext | (TranscriptImage & { url: string });
|
|
@@ -238,6 +242,8 @@ export interface UiAdapter {
|
|
|
238
242
|
onIntent(intent: SupercodeUiIntent): void | Promise<void>;
|
|
239
243
|
/** Ask the embedding host for explicit user-selected text or image context. */
|
|
240
244
|
pickContext?(): TranscriptAttachment | TranscriptAttachment[] | null | Promise<TranscriptAttachment | TranscriptAttachment[] | null>;
|
|
245
|
+
/** Resolve one projected historical image on demand. The UI owns and revokes its object URL. */
|
|
246
|
+
resolveImage?(image: TranscriptImage): Blob | Promise<Blob>;
|
|
241
247
|
onClose?(): void;
|
|
242
248
|
onOpen?(): void;
|
|
243
249
|
copyText?(value: string): void | Promise<void>;
|
|
@@ -357,8 +363,9 @@ export function HarnessLogo(props: { id: HarnessId; activity?: SessionActivity;
|
|
|
357
363
|
export type UiIconName = 'attach' | 'back' | 'check' | 'chevron' | 'close' | 'copy' | 'down' | 'menu' | 'plus' | 'search' | 'send' | 'stop';
|
|
358
364
|
export function UiIcon(props: { name: UiIconName; size?: number; class?: string }): VNode | null;
|
|
359
365
|
export function hasHarnessLogo(id: string): boolean;
|
|
360
|
-
export function
|
|
361
|
-
export function
|
|
366
|
+
export function harnessLogoDataUrl(id: string): string | null;
|
|
367
|
+
export function ImageViewer(props: { items: TranscriptImage[]; index: number; adapter?: Pick<UiAdapter, 'copyText' | 'resolveImage'>; onChange(index: number): void; onClose(): void }): VNode | null;
|
|
368
|
+
export function MessageImages(props: { items?: TranscriptImage[]; adapter?: Pick<UiAdapter, 'copyText' | 'resolveImage'> }): VNode | null;
|
|
362
369
|
export function LoadingStatus(props: { state: SupercodeUiState; compact?: boolean }): VNode;
|
|
363
370
|
export function RequestCard(props: { entry: TranscriptEntryModel; adapter: UiAdapter; canRespond: boolean }): VNode | null;
|
|
364
371
|
export function TranscriptEntry(props: { entry: TranscriptEntryModel; state: SupercodeUiState; adapter: UiAdapter }): VNode;
|
package/logo.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export type { HarnessId, SessionActivity } from './index.js';
|
|
2
|
-
export { HarnessLogo, hasHarnessLogo } from './index.js';
|
|
2
|
+
export { HarnessLogo, harnessLogoDataUrl, hasHarnessLogo } from './index.js';
|
package/logo.mjs
CHANGED
|
@@ -48,6 +48,27 @@ var LOGOS = {
|
|
|
48
48
|
function hasHarnessLogo(id) {
|
|
49
49
|
return Object.hasOwn(LOGOS, id);
|
|
50
50
|
}
|
|
51
|
+
var LOGO_CHROME = {
|
|
52
|
+
codex: { background: "#f7f7f5", color: "#111" },
|
|
53
|
+
gemini: { background: "#f7f7f5", color: "#6e79dc" },
|
|
54
|
+
goose: { background: "#f7f7f5", color: "#101010" },
|
|
55
|
+
pi: { background: "#efefeb", color: "#111" },
|
|
56
|
+
supercode: { background: "#111923", color: "#f7f7f5" }
|
|
57
|
+
};
|
|
58
|
+
function escapeSvgAttribute(value) {
|
|
59
|
+
return String(value).replaceAll("&", "&").replaceAll('"', """).replaceAll("<", "<").replaceAll(">", ">");
|
|
60
|
+
}
|
|
61
|
+
function harnessLogoDataUrl(id) {
|
|
62
|
+
const logo = LOGOS[id];
|
|
63
|
+
if (!logo) return null;
|
|
64
|
+
const chrome = LOGO_CHROME[id] ?? { background: "#111", color: id === "claude-code" ? "#d97757" : "#f7f7f5" };
|
|
65
|
+
const glyph = logo.paths.map(([tag, attributes]) => {
|
|
66
|
+
const serialized = Object.entries(attributes).map(([name, value]) => `${name}="${escapeSvgAttribute(value)}"`).join(" ");
|
|
67
|
+
return `<${tag} ${serialized}/>`;
|
|
68
|
+
}).join("");
|
|
69
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><circle cx="20" cy="20" r="19" fill="${chrome.background}" stroke="#383a42"/><svg x="7" y="7" width="26" height="26" viewBox="${logo.viewBox}" color="${chrome.color}" fill="currentColor" preserveAspectRatio="xMidYMid meet">${glyph}</svg></svg>`;
|
|
70
|
+
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
71
|
+
}
|
|
51
72
|
function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
52
73
|
const logo = LOGOS[id];
|
|
53
74
|
useEffect(() => {
|
|
@@ -61,5 +82,6 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
|
61
82
|
}
|
|
62
83
|
export {
|
|
63
84
|
HarnessLogo,
|
|
85
|
+
harnessLogoDataUrl,
|
|
64
86
|
hasHarnessLogo
|
|
65
87
|
};
|
package/messenger.mjs
CHANGED
|
@@ -507,7 +507,10 @@ function readTranscript(value) {
|
|
|
507
507
|
return image && typeof image.label === "string" ? [{
|
|
508
508
|
...typeof image.id === "string" ? { id: image.id } : {},
|
|
509
509
|
label: image.label,
|
|
510
|
-
...typeof image.url === "string" ? { url: image.url } : {}
|
|
510
|
+
...typeof image.url === "string" ? { url: image.url } : {},
|
|
511
|
+
...typeof image.reference === "string" && image.reference.length <= 4e3 ? { reference: image.reference } : {},
|
|
512
|
+
...typeof image.mediaType === "string" && image.mediaType.startsWith("image/") && image.mediaType.length <= 100 ? { mediaType: image.mediaType } : {},
|
|
513
|
+
...Number.isSafeInteger(image.byteSize) && image.byteSize >= 0 ? { byteSize: image.byteSize } : {}
|
|
511
514
|
}] : [];
|
|
512
515
|
}).slice(0, 4);
|
|
513
516
|
}
|
|
@@ -823,6 +826,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-ru
|
|
|
823
826
|
var MAX_CONTEXT_ITEMS = 32;
|
|
824
827
|
var MAX_IMAGE_ITEMS = 4;
|
|
825
828
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
829
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
826
830
|
var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
|
827
831
|
function normalizeContext(value) {
|
|
828
832
|
return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
|
|
@@ -925,22 +929,59 @@ function imageFilename(label) {
|
|
|
925
929
|
function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
926
930
|
const dialog = useRef(null);
|
|
927
931
|
const reset = useRef(null);
|
|
932
|
+
const resolutions = useRef(/* @__PURE__ */ new Map());
|
|
933
|
+
const alive = useRef(true);
|
|
934
|
+
const [, redraw] = useState(0);
|
|
928
935
|
const [copyState, setCopyState2] = useState("idle");
|
|
929
936
|
const item = items[index];
|
|
930
|
-
const
|
|
937
|
+
const key = item?.reference ?? item?.url ?? `${item?.id ?? ""}:${index}`;
|
|
938
|
+
const resolution = item?.url ? null : resolutions.current.get(key);
|
|
939
|
+
const imageUrl = item?.url ?? (resolution?.status === "ready" ? resolution.url : null);
|
|
940
|
+
const remote = imageUrl?.startsWith("http://") || imageUrl?.startsWith("https://");
|
|
941
|
+
const resolve = (candidate, force = false) => {
|
|
942
|
+
if (candidate?.url || !candidate?.reference || !adapter?.resolveImage) return;
|
|
943
|
+
const candidateKey = candidate.reference;
|
|
944
|
+
const current = resolutions.current.get(candidateKey);
|
|
945
|
+
if (!force && (current?.status === "loading" || current?.status === "ready")) return;
|
|
946
|
+
if (current?.url) URL.revokeObjectURL(current.url);
|
|
947
|
+
resolutions.current.set(candidateKey, { status: "loading" });
|
|
948
|
+
redraw((value) => value + 1);
|
|
949
|
+
Promise.resolve().then(() => adapter.resolveImage(candidate)).then((blob) => {
|
|
950
|
+
if (!(blob instanceof Blob) || !blob.type.startsWith("image/")) throw new Error("The host returned an invalid image.");
|
|
951
|
+
if (blob.size > MAX_RESOLVED_IMAGE_BYTES) throw new Error("This image is too large to preview safely.");
|
|
952
|
+
if (!alive.current) return;
|
|
953
|
+
const url = URL.createObjectURL(blob);
|
|
954
|
+
resolutions.current.set(candidateKey, { status: "ready", url });
|
|
955
|
+
redraw((value) => value + 1);
|
|
956
|
+
}).catch((error) => {
|
|
957
|
+
if (!alive.current) return;
|
|
958
|
+
resolutions.current.set(candidateKey, {
|
|
959
|
+
status: "error",
|
|
960
|
+
message: error instanceof Error && error.message ? error.message : "Could not load this image."
|
|
961
|
+
});
|
|
962
|
+
redraw((value) => value + 1);
|
|
963
|
+
});
|
|
964
|
+
};
|
|
931
965
|
useEffect(() => {
|
|
966
|
+
alive.current = true;
|
|
932
967
|
if (!dialog.current?.open) dialog.current?.showModal();
|
|
933
|
-
return () =>
|
|
968
|
+
return () => {
|
|
969
|
+
alive.current = false;
|
|
970
|
+
clearTimeout(reset.current);
|
|
971
|
+
for (const value of resolutions.current.values()) if (value.url) URL.revokeObjectURL(value.url);
|
|
972
|
+
resolutions.current.clear();
|
|
973
|
+
};
|
|
934
974
|
}, []);
|
|
935
975
|
useEffect(() => {
|
|
936
976
|
clearTimeout(reset.current);
|
|
937
977
|
setCopyState2("idle");
|
|
938
|
-
|
|
939
|
-
|
|
978
|
+
resolve(item);
|
|
979
|
+
}, [index, item?.reference]);
|
|
980
|
+
if (!item || !item.url && (!item.reference || !adapter?.resolveImage)) return null;
|
|
940
981
|
const move = (amount) => onChange((index + amount + items.length) % items.length);
|
|
941
982
|
const copy = async () => {
|
|
942
983
|
try {
|
|
943
|
-
await adapter.copyText(
|
|
984
|
+
await adapter.copyText(imageUrl);
|
|
944
985
|
setCopyState2("copied");
|
|
945
986
|
} catch {
|
|
946
987
|
setCopyState2("failed");
|
|
@@ -976,12 +1017,21 @@ function ImageViewer({ items, index, adapter, onChange, onClose }) {
|
|
|
976
1017
|
] }),
|
|
977
1018
|
/* @__PURE__ */ jsxs2("nav", { "aria-label": "Image actions", children: [
|
|
978
1019
|
remote && adapter?.copyText ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": copyState === "copied" ? "Image link copied" : copyState === "failed" ? "Could not copy image link" : "Copy image link", title: "Copy image link", "data-status": copyState, onClick: copy, children: /* @__PURE__ */ jsx2(UiIcon, { name: copyState === "copied" ? "check" : "copy", size: 16 }) }) : null,
|
|
979
|
-
remote ? /* @__PURE__ */ jsx2("a", { href:
|
|
1020
|
+
imageUrl ? remote ? /* @__PURE__ */ jsx2("a", { href: imageUrl, target: "_blank", rel: "noreferrer", "aria-label": "Open original image", title: "Open original image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }) }) : /* @__PURE__ */ jsx2("a", { href: imageUrl, download: imageFilename(item.label), "aria-label": "Download image", title: "Download image", children: /* @__PURE__ */ jsx2(UiIcon, { name: "down", size: 16 }) }) : null,
|
|
980
1021
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": "Close image preview", title: "Close", onClick: close, children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 17 }) })
|
|
981
1022
|
] })
|
|
982
1023
|
] }),
|
|
983
1024
|
/* @__PURE__ */ jsxs2("figure", { children: [
|
|
984
|
-
/* @__PURE__ */ jsx2("img", { src:
|
|
1025
|
+
imageUrl ? /* @__PURE__ */ jsx2("img", { src: imageUrl, alt: item.label }) : resolution?.status === "error" ? /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "alert", children: [
|
|
1026
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 28 }),
|
|
1027
|
+
/* @__PURE__ */ jsx2("strong", { children: "Could not load image" }),
|
|
1028
|
+
/* @__PURE__ */ jsx2("small", { children: resolution.message }),
|
|
1029
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: () => resolve(item, true), children: "Retry" })
|
|
1030
|
+
] }) : /* @__PURE__ */ jsxs2("div", { class: "scui-image-resolution", role: "status", children: [
|
|
1031
|
+
/* @__PURE__ */ jsx2("i", { class: "scui-control-spinner" }),
|
|
1032
|
+
/* @__PURE__ */ jsx2("strong", { children: "Loading image\u2026" }),
|
|
1033
|
+
/* @__PURE__ */ jsx2("small", { children: "The original stays out of the transcript payload." })
|
|
1034
|
+
] }),
|
|
985
1035
|
items.length > 1 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
986
1036
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-previous", "aria-label": "Previous image", onClick: () => move(-1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) }),
|
|
987
1037
|
/* @__PURE__ */ jsx2("button", { type: "button", class: "scui-image-next", "aria-label": "Next image", onClick: () => move(1), children: /* @__PURE__ */ jsx2(UiIcon, { name: "chevron", size: 19 }) })
|
|
@@ -995,7 +1045,7 @@ function MessageImages({ items, adapter }) {
|
|
|
995
1045
|
const [active, setActive] = useState(null);
|
|
996
1046
|
const opener = useRef(null);
|
|
997
1047
|
if (!items?.length) return null;
|
|
998
|
-
const viewable = items.filter((item) => item.url);
|
|
1048
|
+
const viewable = items.filter((item) => item.url || item.reference && adapter?.resolveImage);
|
|
999
1049
|
const close = () => {
|
|
1000
1050
|
setActive(null);
|
|
1001
1051
|
requestAnimationFrame(() => opener.current?.focus({ preventScroll: true }));
|
|
@@ -1004,7 +1054,16 @@ function MessageImages({ items, adapter }) {
|
|
|
1004
1054
|
/* @__PURE__ */ jsx2("div", { class: "scui-message-images", "aria-label": "Message images", children: items.map((item, index) => item.url ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `View image ${item.label}`, onClick: (event) => {
|
|
1005
1055
|
opener.current = event.currentTarget;
|
|
1006
1056
|
setActive(viewable.indexOf(item));
|
|
1007
|
-
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("
|
|
1057
|
+
}, children: /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }) }, item.id ?? `${item.label}:${index}`) : item.reference && adapter?.resolveImage ? /* @__PURE__ */ jsxs2("button", { type: "button", "data-lazy": "true", "aria-label": `Load image ${item.label}`, onClick: (event) => {
|
|
1058
|
+
opener.current = event.currentTarget;
|
|
1059
|
+
setActive(viewable.indexOf(item));
|
|
1060
|
+
}, children: [
|
|
1061
|
+
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 16 }),
|
|
1062
|
+
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1063
|
+
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
1064
|
+
/* @__PURE__ */ jsx2("small", { children: "Load preview" })
|
|
1065
|
+
] })
|
|
1066
|
+
] }, item.id ?? `${item.label}:${index}`) : /* @__PURE__ */ jsxs2("span", { "data-unavailable": "true", children: [
|
|
1008
1067
|
/* @__PURE__ */ jsx2(UiIcon, { name: "image", size: 14 }),
|
|
1009
1068
|
/* @__PURE__ */ jsxs2("span", { children: [
|
|
1010
1069
|
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
package/package.json
CHANGED
package/sessions.mjs
CHANGED
|
@@ -196,6 +196,7 @@ function UiIcon({ name, size = 16, class: className = "" }) {
|
|
|
196
196
|
import { useEffect as useEffect2, useRef as useRef2, useState } from "preact/hooks";
|
|
197
197
|
import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs2 } from "preact/jsx-runtime";
|
|
198
198
|
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
199
|
+
var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
|
|
199
200
|
|
|
200
201
|
// src/conversation.jsx
|
|
201
202
|
import { Fragment as Fragment4, jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
package/styles.css
CHANGED
|
@@ -175,8 +175,8 @@
|
|
|
175
175
|
.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) }
|
|
176
176
|
.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) }
|
|
177
177
|
.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 }
|
|
178
|
-
.scui-message-images { display:flex; max-width:280px; gap:5px; margin-bottom:6px; flex-wrap:wrap }.scui-message-images > button { display:block; max-width:100%; padding:0; overflow:hidden; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-fill); cursor:zoom-in }.scui-message-images > button:hover,.scui-message-images > button:focus-visible { border-color:var(--scui-border-strong); box-shadow:0 0 0 2px color-mix(in srgb,var(--scui-accent) 16%,transparent) }.scui-message-images img { display:block; width:auto; min-width:72px; max-width:100%; height:auto; max-height:210px; object-fit:contain }.scui-message-images > span { display:flex; align-items:center; gap:6px; padding:5px 7px; border:1px solid var(--scui-border); border-radius:7px; color:var(--scui-muted); font-size:10px }.scui-message-images > span > span { display:grid; min-width:0 }.scui-message-images > span strong { max-width:180px; overflow:hidden; color:var(--scui-fg); font-size:10px; font-weight:550; text-overflow:ellipsis; white-space:nowrap }.scui-message-images > span small { color:var(--scui-muted); font-size:8.5px }
|
|
179
|
-
.scui-image-viewer { box-sizing:border-box; width:min(620px,calc(100vw - 16px)); max-width:none; height:min(620px,calc(100dvh - 16px)); max-height:none; margin:auto; padding:0; overflow:hidden; border:1px solid var(--scui-border-strong); border-radius:12px; background:var(--scui-bg); color:var(--scui-fg); box-shadow:0 18px 50px rgba(0,0,0,.28) }.scui-image-viewer::backdrop { background:rgba(10,12,15,.68); backdrop-filter:blur(2px) }.scui-image-viewer > header { display:flex; min-height:43px; align-items:center; gap:8px; padding:0 6px 0 11px; border-bottom:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-image-viewer > header > span { display:grid; min-width:0; flex:1 }.scui-image-viewer > header strong { overflow:hidden; font-size:11px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-image-viewer > header small { color:var(--scui-muted); font-size:9px }.scui-image-viewer nav { display:flex; flex:none; gap:2px }.scui-image-viewer nav button,.scui-image-viewer nav a { display:grid; box-sizing:border-box; width:31px; height:31px; padding:0; place-items:center; border:0; border-radius:7px; background:transparent; color:var(--scui-muted); cursor:pointer }.scui-image-viewer nav button:hover,.scui-image-viewer nav button:focus-visible,.scui-image-viewer nav a:hover,.scui-image-viewer nav a:focus-visible { background:var(--scui-fill); color:var(--scui-fg) }.scui-image-viewer nav button[data-status="copied"] { color:var(--scui-positive) }.scui-image-viewer nav button[data-status="failed"] { color:var(--scui-danger) }.scui-image-viewer figure { position:relative; display:grid; box-sizing:border-box; height:calc(100% - 43px); margin:0; padding:12px; overflow:hidden; place-items:center; background:color-mix(in srgb,var(--scui-fg) 4%,var(--scui-bg)) }.scui-image-viewer figure > img { display:block; max-width:100%; max-height:100%; object-fit:contain }.scui-image-viewer figure > button { position:absolute; top:50%; display:grid; width:34px; height:34px; padding:0; place-items:center; border:1px solid color-mix(in srgb,var(--scui-fg) 18%,transparent); border-radius:50%; background:color-mix(in srgb,var(--scui-bg) 82%,transparent); color:var(--scui-fg); box-shadow:0 2px 8px rgba(0,0,0,.16); cursor:pointer; transform:translateY(-50%); backdrop-filter:blur(5px) }.scui-image-viewer figure > button:hover,.scui-image-viewer figure > button:focus-visible { background:var(--scui-bg) }.scui-image-previous { left:10px }.scui-image-previous svg { transform:rotate(180deg) }.scui-image-next { right:10px }
|
|
178
|
+
.scui-message-images { display:flex; max-width:280px; gap:5px; margin-bottom:6px; flex-wrap:wrap }.scui-message-images > button { display:block; max-width:100%; padding:0; overflow:hidden; border:1px solid var(--scui-border); border-radius:8px; background:var(--scui-fill); cursor:zoom-in }.scui-message-images > button:hover,.scui-message-images > button:focus-visible { border-color:var(--scui-border-strong); box-shadow:0 0 0 2px color-mix(in srgb,var(--scui-accent) 16%,transparent) }.scui-message-images img { display:block; width:auto; min-width:72px; max-width:100%; height:auto; max-height:210px; object-fit:contain }.scui-message-images > span,.scui-message-images > button[data-lazy="true"] { display:flex; align-items:center; gap:6px; padding:5px 7px; border:1px solid var(--scui-border); border-radius:7px; color:var(--scui-muted); font-size:10px }.scui-message-images > button[data-lazy="true"] { text-align:left; cursor:pointer }.scui-message-images > span > span,.scui-message-images > button[data-lazy="true"] > span { display:grid; min-width:0 }.scui-message-images > span strong,.scui-message-images > button[data-lazy="true"] strong { max-width:180px; overflow:hidden; color:var(--scui-fg); font-size:10px; font-weight:550; text-overflow:ellipsis; white-space:nowrap }.scui-message-images > span small,.scui-message-images > button[data-lazy="true"] small { color:var(--scui-muted); font-size:8.5px }
|
|
179
|
+
.scui-image-viewer { box-sizing:border-box; width:min(620px,calc(100vw - 16px)); max-width:none; height:min(620px,calc(100dvh - 16px)); max-height:none; margin:auto; padding:0; overflow:hidden; border:1px solid var(--scui-border-strong); border-radius:12px; background:var(--scui-bg); color:var(--scui-fg); box-shadow:0 18px 50px rgba(0,0,0,.28) }.scui-image-viewer::backdrop { background:rgba(10,12,15,.68); backdrop-filter:blur(2px) }.scui-image-viewer > header { display:flex; min-height:43px; align-items:center; gap:8px; padding:0 6px 0 11px; border-bottom:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-image-viewer > header > span { display:grid; min-width:0; flex:1 }.scui-image-viewer > header strong { overflow:hidden; font-size:11px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-image-viewer > header small { color:var(--scui-muted); font-size:9px }.scui-image-viewer nav { display:flex; flex:none; gap:2px }.scui-image-viewer nav button,.scui-image-viewer nav a { display:grid; box-sizing:border-box; width:31px; height:31px; padding:0; place-items:center; border:0; border-radius:7px; background:transparent; color:var(--scui-muted); cursor:pointer }.scui-image-viewer nav button:hover,.scui-image-viewer nav button:focus-visible,.scui-image-viewer nav a:hover,.scui-image-viewer nav a:focus-visible { background:var(--scui-fill); color:var(--scui-fg) }.scui-image-viewer nav button[data-status="copied"] { color:var(--scui-positive) }.scui-image-viewer nav button[data-status="failed"] { color:var(--scui-danger) }.scui-image-viewer figure { position:relative; display:grid; box-sizing:border-box; height:calc(100% - 43px); margin:0; padding:12px; overflow:hidden; place-items:center; background:color-mix(in srgb,var(--scui-fg) 4%,var(--scui-bg)) }.scui-image-viewer figure > img { display:block; max-width:100%; max-height:100%; object-fit:contain }.scui-image-viewer figure > button { position:absolute; top:50%; display:grid; width:34px; height:34px; padding:0; place-items:center; border:1px solid color-mix(in srgb,var(--scui-fg) 18%,transparent); border-radius:50%; background:color-mix(in srgb,var(--scui-bg) 82%,transparent); color:var(--scui-fg); box-shadow:0 2px 8px rgba(0,0,0,.16); cursor:pointer; transform:translateY(-50%); backdrop-filter:blur(5px) }.scui-image-viewer figure > button:hover,.scui-image-viewer figure > button:focus-visible { background:var(--scui-bg) }.scui-image-previous { left:10px }.scui-image-previous svg { transform:rotate(180deg) }.scui-image-next { right:10px }.scui-image-resolution { display:grid; max-width:280px; justify-items:center; gap:7px; color:var(--scui-muted); text-align:center }.scui-image-resolution strong { color:var(--scui-fg); font-size:12px }.scui-image-resolution small { font-size:10px }.scui-image-resolution > button { position:static; width:auto; height:auto; padding:6px 10px; border:1px solid var(--scui-border-strong); border-radius:7px; background:var(--scui-bg); color:var(--scui-fg); transform:none; backdrop-filter:none }
|
|
180
180
|
.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 }
|
|
181
181
|
.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 }
|
|
182
182
|
.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) }
|