@volter-ai-dev/supercode-ui 0.1.35 → 0.1.37

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.
@@ -0,0 +1,2 @@
1
+ export type { MessengerLabels, PendingMessageModel, SupercodeUiState, UiAdapter } from './index.js';
2
+ export { Composer, ContinuationBar } from './index.js';
@@ -0,0 +1,518 @@
1
+ // src/composer.jsx
2
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
3
+
4
+ // core.mjs
5
+ var HARNESS_NAMES = Object.freeze({
6
+ "claude-code": "Claude Code",
7
+ codex: "Codex",
8
+ gemini: "Gemini CLI",
9
+ goose: "Goose",
10
+ opencode: "OpenCode",
11
+ pi: "Pi",
12
+ grok: "Grok"
13
+ });
14
+ var DEFAULT_LABELS = Object.freeze({
15
+ chats: "Chats",
16
+ newChat: "New chat",
17
+ searchChats: "Search chats",
18
+ askAgent: "Ask your agent\u2026",
19
+ continueHere: "Continue here",
20
+ continueWithTerminal: "Continue with terminal",
21
+ joinLive: "Join live",
22
+ forkHere: "Fork here"
23
+ });
24
+ var EMPTY_UI_STATE = Object.freeze({
25
+ pill: Object.freeze({ tone: "off", label: "connecting\u2026" }),
26
+ startup: "connecting",
27
+ transcript: Object.freeze([]),
28
+ busy: false,
29
+ operation: null,
30
+ needsInput: false,
31
+ harness: "",
32
+ mode: "none",
33
+ strategy: null,
34
+ canSend: false,
35
+ canSteer: false,
36
+ canResume: false,
37
+ continuationModes: Object.freeze([]),
38
+ canBranch: false,
39
+ canAttach: false,
40
+ canDetach: false,
41
+ canOpenTerminal: false,
42
+ canExport: false,
43
+ canReduce: false,
44
+ canInterrupt: false,
45
+ canRespond: false,
46
+ canConfigureSettings: false,
47
+ messaging: null,
48
+ workspace: "",
49
+ taskPlan: Object.freeze({ source: "none", items: Object.freeze([]), residueCount: 0, observedAt: null }),
50
+ semantics: Object.freeze({ fidelity: null, residue: Object.freeze([]), residueCount: 0, parseErrors: 0, rawRecords: 0, subagents: Object.freeze([]) }),
51
+ terminalHandoff: null,
52
+ exportBackTarget: null,
53
+ exportReceipt: null,
54
+ reductionReceipt: null,
55
+ interopSettings: null,
56
+ interopSettingsError: null,
57
+ error: null,
58
+ recoverable: false,
59
+ harnesses: Object.freeze([]),
60
+ history: Object.freeze({ sessionLimit: 0, hasMoreSessions: false, transcriptLimit: 120, hasEarlier: false }),
61
+ savedDraft: "",
62
+ attention: Object.freeze([]),
63
+ sessions: Object.freeze([]),
64
+ attached: null,
65
+ owned: null,
66
+ attachError: null
67
+ });
68
+ function harnessDisplayName(id) {
69
+ return HARNESS_NAMES[id] ?? id;
70
+ }
71
+ var ACTIVITY_PRIORITY = Object.freeze({
72
+ "needs-input": 70,
73
+ failed: 60,
74
+ working: 50,
75
+ unseen: 40,
76
+ finished: 30,
77
+ running: 20,
78
+ recent: 10,
79
+ idle: 0
80
+ });
81
+ function canContinueHere(state) {
82
+ if (state.mode !== "mirror" || state.canSend) return false;
83
+ const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
84
+ return Boolean(row) && state.canResume && row.runtimeStatus !== "running" && row.runtimeStatus !== "busy" && row.runtimeStatus !== "idle";
85
+ }
86
+ function isSendKey(event) {
87
+ return event.key === "Enter" && !event.shiftKey && !event.isComposing;
88
+ }
89
+
90
+ // src/memory.js
91
+ var MEMORY_LIMIT = 100;
92
+ function boundedSet(map, key, value) {
93
+ map.delete(key);
94
+ map.set(key, value);
95
+ while (map.size > MEMORY_LIMIT) map.delete(map.keys().next().value);
96
+ }
97
+
98
+ // src/icon.jsx
99
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
100
+ var ICONS = {
101
+ attach: () => /* @__PURE__ */ jsx("path", { d: "M6.25 9.75 10.6 5.4a2.1 2.1 0 0 1 2.97 2.97l-5.4 5.4a3.3 3.3 0 0 1-4.67-4.66l5.52-5.52" }),
102
+ back: () => /* @__PURE__ */ jsx("path", { d: "m11.5 4.5-4.5 4.5 4.5 4.5" }),
103
+ check: () => /* @__PURE__ */ jsx("path", { d: "m4 9 3.25 3.25L14 5.5" }),
104
+ chevron: () => /* @__PURE__ */ jsx("path", { d: "m7 4.5 4.5 4.5L7 13.5" }),
105
+ close: () => /* @__PURE__ */ jsxs(Fragment, { children: [
106
+ /* @__PURE__ */ jsx("path", { d: "m4.75 4.75 8.5 8.5" }),
107
+ /* @__PURE__ */ jsx("path", { d: "m13.25 4.75-8.5 8.5" })
108
+ ] }),
109
+ copy: () => /* @__PURE__ */ jsxs(Fragment, { children: [
110
+ /* @__PURE__ */ jsx("rect", { x: "5", y: "5", width: "8", height: "8", rx: "1.5" }),
111
+ /* @__PURE__ */ jsx("path", { d: "M3 10.5V4.25C3 3.56 3.56 3 4.25 3h6.25" })
112
+ ] }),
113
+ down: () => /* @__PURE__ */ jsxs(Fragment, { children: [
114
+ /* @__PURE__ */ jsx("path", { d: "M9 3.5v10" }),
115
+ /* @__PURE__ */ jsx("path", { d: "m4.75 9.5 4.25 4 4.25-4" })
116
+ ] }),
117
+ image: () => /* @__PURE__ */ jsxs(Fragment, { children: [
118
+ /* @__PURE__ */ jsx("rect", { x: "3", y: "3.5", width: "12", height: "11", rx: "1.75" }),
119
+ /* @__PURE__ */ jsx("circle", { cx: "6.5", cy: "7", r: "1.25" }),
120
+ /* @__PURE__ */ jsx("path", { d: "m4.5 13 3.25-3 2.1 1.85 1.65-1.5L14 13" })
121
+ ] }),
122
+ menu: () => /* @__PURE__ */ jsxs(Fragment, { children: [
123
+ /* @__PURE__ */ jsx("circle", { cx: "4", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
124
+ /* @__PURE__ */ jsx("circle", { cx: "9", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" }),
125
+ /* @__PURE__ */ jsx("circle", { cx: "14", cy: "9", r: "1.25", fill: "currentColor", stroke: "none" })
126
+ ] }),
127
+ plus: () => /* @__PURE__ */ jsxs(Fragment, { children: [
128
+ /* @__PURE__ */ jsx("path", { d: "M9 3.5v11" }),
129
+ /* @__PURE__ */ jsx("path", { d: "M3.5 9h11" })
130
+ ] }),
131
+ search: () => /* @__PURE__ */ jsxs(Fragment, { children: [
132
+ /* @__PURE__ */ jsx("circle", { cx: "7.75", cy: "7.75", r: "4.25" }),
133
+ /* @__PURE__ */ jsx("path", { d: "m11 11 3.5 3.5" })
134
+ ] }),
135
+ send: () => /* @__PURE__ */ jsxs(Fragment, { children: [
136
+ /* @__PURE__ */ jsx("path", { d: "M9 14.5v-11" }),
137
+ /* @__PURE__ */ jsx("path", { d: "m4.75 7.75 4.25-4.25 4.25 4.25" })
138
+ ] }),
139
+ stop: () => /* @__PURE__ */ jsx("rect", { x: "4.5", y: "4.5", width: "9", height: "9", rx: "1.5", fill: "currentColor", stroke: "none" })
140
+ };
141
+ function UiIcon({ name, size = 16, class: className = "" }) {
142
+ const Glyph = ICONS[name];
143
+ if (!Glyph) return null;
144
+ return /* @__PURE__ */ jsx("svg", { className: `scui-icon ${className}`, style: { "--scui-icon-size": `${size}px` }, viewBox: "0 0 18 18", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: /* @__PURE__ */ jsx(Glyph, {}) });
145
+ }
146
+
147
+ // src/context.jsx
148
+ import { useEffect, useRef, useState } from "react";
149
+ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
150
+ var MAX_CONTEXT_ITEMS = 32;
151
+ var MAX_IMAGE_ITEMS = 4;
152
+ var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
153
+ var MAX_RESOLVED_IMAGE_BYTES = 16 * 1024 * 1024;
154
+ var IMAGE_TYPES = /* @__PURE__ */ new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
155
+ function normalizeContext(value) {
156
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
157
+ if (!item || typeof item.label !== "string" || typeof item.detail !== "string") return [];
158
+ const label = item.label.trim().slice(0, 200);
159
+ const detail = item.detail.slice(0, 2e4);
160
+ if (!label || !detail) return [];
161
+ return [{
162
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
163
+ ...typeof item.kind === "string" && item.kind ? { kind: item.kind.slice(0, 100) } : {},
164
+ label,
165
+ detail
166
+ }];
167
+ }).slice(0, MAX_CONTEXT_ITEMS);
168
+ }
169
+ function mergeContext(current, picked) {
170
+ const next = [...current];
171
+ const seen = new Set(current.map((item) => item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`));
172
+ for (const item of normalizeContext(picked)) {
173
+ const key = item.id ? `id:${item.id}` : `value:${item.kind ?? ""}\0${item.label}\0${item.detail}`;
174
+ if (seen.has(key)) continue;
175
+ seen.add(key);
176
+ next.push(item);
177
+ if (next.length === MAX_CONTEXT_ITEMS) break;
178
+ }
179
+ return next;
180
+ }
181
+ function normalizeImages(value) {
182
+ const seen = /* @__PURE__ */ new Set();
183
+ return (Array.isArray(value) ? value : value ? [value] : []).flatMap((item) => {
184
+ if (!item || typeof item.label !== "string" || typeof item.url !== "string") return [];
185
+ const label = item.label.trim().slice(0, 200);
186
+ const url = item.url;
187
+ if (!label || seen.has(url) || !(url.startsWith("data:image/") || url.startsWith("https://") || url.startsWith("http://"))) return [];
188
+ seen.add(url);
189
+ return [{
190
+ ...typeof item.id === "string" && item.id ? { id: item.id.slice(0, 2e3) } : {},
191
+ label,
192
+ url
193
+ }];
194
+ }).slice(0, MAX_IMAGE_ITEMS);
195
+ }
196
+ function mergeImages(current, picked) {
197
+ const next = [...current];
198
+ const seen = new Set(current.map((item) => item.url));
199
+ for (const item of normalizeImages(picked)) {
200
+ if (seen.has(item.url)) continue;
201
+ seen.add(item.url);
202
+ next.push(item);
203
+ if (next.length === MAX_IMAGE_ITEMS) break;
204
+ }
205
+ return next;
206
+ }
207
+ function partitionAttachments(value) {
208
+ const values = Array.isArray(value) ? value : value ? [value] : [];
209
+ const context = [];
210
+ const images = [];
211
+ for (const item of values) {
212
+ if (item && typeof item.label === "string" && item.label.trim() && typeof item.detail === "string" && item.detail) context.push(item);
213
+ 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);
214
+ else throw new Error("The attachment picker returned an invalid item.");
215
+ }
216
+ return { context, images };
217
+ }
218
+ function attachmentKey(item) {
219
+ if (item.id) return `id:${item.id}`;
220
+ return "detail" in item ? `context:${item.kind ?? ""}\0${item.label}\0${item.detail}` : `image:${item.url}`;
221
+ }
222
+ function ContextCandidate({ attachment, attached, onAttach }) {
223
+ const image = "url" in attachment;
224
+ return /* @__PURE__ */ jsxs2("button", { type: "button", disabled: attached, "aria-label": `${attached ? "Attached" : "Attach"} ${attachment.label}`, onClick: () => onAttach(attachment), children: [
225
+ image ? /* @__PURE__ */ jsx2("img", { src: attachment.url, alt: "" }) : /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 13 }),
226
+ /* @__PURE__ */ jsxs2("span", { children: [
227
+ /* @__PURE__ */ jsx2("strong", { children: attachment.label }),
228
+ /* @__PURE__ */ jsx2("small", { children: image ? "Image" : attachment.kind || "Context" })
229
+ ] }),
230
+ attached ? /* @__PURE__ */ jsx2(UiIcon, { name: "check", size: 13 }) : /* @__PURE__ */ jsx2(UiIcon, { name: "plus", size: 13 })
231
+ ] });
232
+ }
233
+ function ContextCandidates({ items, context, images, state, adapter, onAttach, component: Candidate = ContextCandidate }) {
234
+ if (!items.length) return null;
235
+ const attached = new Set([...context, ...images].map(attachmentKey));
236
+ return /* @__PURE__ */ jsx2("div", { className: "scui-context-candidates", "aria-label": "Available context", children: items.map((attachment, index) => /* @__PURE__ */ jsx2(Candidate, { value: attachment, attachment, attached: attached.has(attachmentKey(attachment)), state, adapter, onAttach, index }, attachmentKey(attachment))) });
237
+ }
238
+ async function imageAttachmentsFromFiles(value) {
239
+ const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
240
+ if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
241
+ return Promise.all(files.map(async (file) => {
242
+ if (!IMAGE_TYPES.has(file.type)) throw new Error(`${file.name || "That image"} is not PNG, JPEG, GIF, or WebP.`);
243
+ if (file.size > MAX_IMAGE_BYTES) throw new Error(`${file.name || "That image"} is larger than 5 MB.`);
244
+ const url = await new Promise((resolve, reject) => {
245
+ const reader = new FileReader();
246
+ reader.onload = () => resolve(reader.result);
247
+ reader.onerror = () => reject(reader.error ?? new Error(`Could not read ${file.name || "image"}.`));
248
+ reader.readAsDataURL(file);
249
+ });
250
+ return { id: `${file.name}:${file.size}:${file.lastModified}`, label: file.name || "Pasted image", url };
251
+ }));
252
+ }
253
+ function ContextTray({ items, onRemove }) {
254
+ if (!items.length) return null;
255
+ return /* @__PURE__ */ jsx2("div", { className: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
256
+ /* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 12 }),
257
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
258
+ /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
259
+ ] }, item.id ?? `${item.label}:${index}`)) });
260
+ }
261
+ function ImageTray({ items, onRemove }) {
262
+ if (!items.length) return null;
263
+ return /* @__PURE__ */ jsx2("div", { className: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
264
+ /* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
265
+ /* @__PURE__ */ jsx2("strong", { children: item.label }),
266
+ onRemove ? /* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove image ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) }) : null
267
+ ] }, item.id ?? `${item.label}:${index}`)) });
268
+ }
269
+
270
+ // src/intent.js
271
+ var CONFIRMABLE_ACTIONS = /* @__PURE__ */ new Set(["resume", "join", "branch", "reduce", "export"]);
272
+ async function dispatchConfirmedIntent(adapter, intent) {
273
+ if (CONFIRMABLE_ACTIONS.has(intent.action) && adapter.confirmIntent) {
274
+ const confirmed = await adapter.confirmIntent(intent);
275
+ if (!confirmed) return;
276
+ }
277
+ return adapter.onIntent(intent);
278
+ }
279
+
280
+ // src/textarea.js
281
+ import { useLayoutEffect } from "react";
282
+ function useAutosizeTextarea(ref, value) {
283
+ useLayoutEffect(() => {
284
+ const element = ref.current;
285
+ if (!element) return;
286
+ element.style.height = "auto";
287
+ const maxHeight = Number.parseFloat(getComputedStyle(element).maxHeight) || 150;
288
+ const height = Math.min(element.scrollHeight, maxHeight);
289
+ element.style.height = `${height}px`;
290
+ element.style.overflowY = element.scrollHeight > maxHeight ? "auto" : "hidden";
291
+ }, [ref, value]);
292
+ }
293
+
294
+ // src/composer.jsx
295
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
296
+ var composerMemory = /* @__PURE__ */ new Map();
297
+ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
298
+ if (state.mode !== "mirror" || state.canSend) return null;
299
+ const attached = state.attached;
300
+ const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
301
+ const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
302
+ const resume = canContinueHere(state);
303
+ const terminal = resume && state.continuationModes?.includes("terminal");
304
+ const join = state.canAttach;
305
+ const branch = state.canBranch;
306
+ if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
307
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
308
+ /* @__PURE__ */ jsx3("small", { children: "This session cannot be continued by an available harness." })
309
+ ] }) });
310
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-continuation", children: [
311
+ /* @__PURE__ */ jsxs3("span", { children: [
312
+ /* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
313
+ /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
314
+ ] }),
315
+ /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
316
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
317
+ terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
318
+ ] })
319
+ ] });
320
+ }
321
+ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
322
+ const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
323
+ const [draft, setDraft] = useState2(remembered.draft);
324
+ const [context, setContext] = useState2(remembered.context ?? []);
325
+ const [images, setImages] = useState2(remembered.images ?? []);
326
+ const [queue, setQueue] = useState2((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
327
+ const [dispatching, setDispatching] = useState2(false);
328
+ const [steering, setSteering] = useState2(false);
329
+ const [picking, setPicking] = useState2(false);
330
+ const [dragging, setDragging] = useState2(false);
331
+ const [pickerError, setPickerError] = useState2(null);
332
+ const textarea = useRef2(null);
333
+ useAutosizeTextarea(textarea, draft);
334
+ const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
335
+ useEffect2(() => {
336
+ remember(draft, context, images, queue);
337
+ }, [draft, context, images, memoryKey, queue]);
338
+ const updateQueue = (update) => setQueue((items) => {
339
+ const next = update(items);
340
+ remember(draft, context, images, next);
341
+ return next;
342
+ });
343
+ const queueBlocked = state.busy || pendingStatus !== null || dispatching || steering;
344
+ const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching || steering;
345
+ const steerAvailable = state.busy && state.canSteer && !steering && pendingStatus === null;
346
+ const canSteerDraft = steerAvailable && Boolean(draft.trim()) && context.length === 0 && images.length === 0;
347
+ useEffect2(() => {
348
+ if (!queueBlocked && state.canSend && queue.length) {
349
+ const [next, ...rest] = queue;
350
+ setDispatching(true);
351
+ setQueue(rest);
352
+ remember(draft, context, images, rest);
353
+ onPending?.(next.text, next.context, next.images);
354
+ adapter.onIntent({ action: "send", text: next.text, ...next.context.length ? { context: next.context } : {}, ...next.images.length ? { images: next.images } : {} });
355
+ }
356
+ }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
357
+ useEffect2(() => {
358
+ if (pendingStatus !== null || state.busy) setDispatching(false);
359
+ }, [pendingStatus, state.busy]);
360
+ useEffect2(() => {
361
+ textarea.current?.focus({ preventScroll: true });
362
+ }, [memoryKey]);
363
+ useEffect2(() => {
364
+ if (!restoreDraft) return;
365
+ setDraft(restoreDraft.text);
366
+ const restoredContext = normalizeContext(restoreDraft.context);
367
+ const restoredImages = normalizeImages(restoreDraft.images);
368
+ setContext(restoredContext);
369
+ setImages(restoredImages);
370
+ remember(restoreDraft.text, restoredContext, restoredImages, queue);
371
+ textarea.current?.focus({ preventScroll: true });
372
+ onDraftRestored?.(restoreDraft.id);
373
+ }, [onDraftRestored, restoreDraft?.id]);
374
+ useEffect2(() => {
375
+ const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
376
+ return () => clearTimeout(timer);
377
+ }, [adapter, draft]);
378
+ const pickContext = () => {
379
+ if (!adapter.pickContext || state.mode !== "control" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
380
+ setPicking(true);
381
+ setPickerError(null);
382
+ Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
383
+ const attachments = partitionAttachments(picked);
384
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
385
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
386
+ setContext((current) => {
387
+ const next = mergeContext(current, attachments.context);
388
+ remember(draft, next, images, queue);
389
+ return next;
390
+ });
391
+ setImages((current) => {
392
+ const next = mergeImages(current, attachments.images);
393
+ remember(draft, context, next, queue);
394
+ return next;
395
+ });
396
+ }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
397
+ };
398
+ const attachCandidate = (picked) => {
399
+ const attachments = partitionAttachments(picked);
400
+ const nextContext = mergeContext(context, attachments.context);
401
+ const nextImages = mergeImages(images, attachments.images);
402
+ setContext(nextContext);
403
+ setImages(nextImages);
404
+ remember(draft, nextContext, nextImages, queue);
405
+ };
406
+ const addImageFiles = (value, source) => {
407
+ const allFiles = Array.from(value ?? []);
408
+ if (!allFiles.length) return false;
409
+ const files = allFiles.filter((file) => file.type.startsWith("image/"));
410
+ if (files.length !== allFiles.length) {
411
+ setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
412
+ return true;
413
+ }
414
+ if (images.length + files.length > MAX_IMAGE_ITEMS) {
415
+ setPickerError(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
416
+ return true;
417
+ }
418
+ setPicking(true);
419
+ setPickerError(null);
420
+ imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
421
+ const next = mergeImages(current, picked);
422
+ remember(draft, context, next, queue);
423
+ return next;
424
+ }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
425
+ return true;
426
+ };
427
+ const pasteImages = (event) => {
428
+ if (addImageFiles(event.clipboardData?.files, "paste")) event.preventDefault();
429
+ };
430
+ const dropImages = (event) => {
431
+ setDragging(false);
432
+ if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
433
+ };
434
+ const send = (forceQueue = false) => {
435
+ const text = draft.trim();
436
+ if (!text && !images.length) return;
437
+ if (canSteerDraft && !forceQueue) {
438
+ setSteering(true);
439
+ Promise.resolve(adapter.onIntent({ action: "steer", text })).then(() => {
440
+ setDraft("");
441
+ remember("", context, images, queue);
442
+ }, (error) => setPickerError(error instanceof Error ? error.message : "Could not steer the active turn.")).finally(() => setSteering(false));
443
+ return;
444
+ }
445
+ const message = { text, context, images };
446
+ if (queuesNewMessage) updateQueue((items) => [...items, message]);
447
+ else if (state.canSend) {
448
+ if (onPending) setDispatching(true);
449
+ onPending?.(text, context, images);
450
+ adapter.onIntent({ action: "send", text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
451
+ } else return;
452
+ setDraft("");
453
+ setContext([]);
454
+ setImages([]);
455
+ remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
456
+ };
457
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-compose", children: [
458
+ queue.length ? /* @__PURE__ */ jsxs3("div", { className: "scui-queue", children: [
459
+ /* @__PURE__ */ jsxs3("strong", { children: [
460
+ queue.length,
461
+ " queued"
462
+ ] }),
463
+ queue.map((item, index) => /* @__PURE__ */ jsxs3("span", { children: [
464
+ /* @__PURE__ */ jsxs3("span", { children: [
465
+ item.text || "Image attachment",
466
+ item.context.length + item.images.length ? /* @__PURE__ */ jsxs3("small", { children: [
467
+ item.context.length + item.images.length,
468
+ " attached"
469
+ ] }) : null
470
+ ] }),
471
+ /* @__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 }) })
472
+ ] }, `${index}:${item.text}`))
473
+ ] }) : null,
474
+ /* @__PURE__ */ jsx3(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }),
475
+ /* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
476
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
477
+ remember(draft, context, next, queue);
478
+ return next;
479
+ }) }),
480
+ /* @__PURE__ */ jsx3(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
481
+ const next = items.filter((_, itemIndex) => itemIndex !== index);
482
+ remember(draft, next, images, queue);
483
+ return next;
484
+ }) }),
485
+ pickerError ? /* @__PURE__ */ jsx3("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
486
+ /* @__PURE__ */ jsxs3("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
487
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
488
+ event.preventDefault();
489
+ setDragging(true);
490
+ }
491
+ }, onDragOver: (event) => {
492
+ if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) event.preventDefault();
493
+ }, onDragLeave: (event) => {
494
+ if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
495
+ }, onDrop: dropImages, children: [
496
+ adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", { className: "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", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: "attach", size: 17 }) }) : null,
497
+ /* @__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" : steerAvailable && context.length === 0 && images.length === 0 ? "Redirect the current turn\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
498
+ const value = event.currentTarget.value;
499
+ setDraft(value);
500
+ remember(value, context, images, queue);
501
+ }, onKeyDown: (event) => {
502
+ if (isSendKey(event)) {
503
+ event.preventDefault();
504
+ send();
505
+ }
506
+ } }),
507
+ /* @__PURE__ */ jsxs3("span", { children: [
508
+ state.busy ? /* @__PURE__ */ jsx3("button", { className: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: /* @__PURE__ */ jsx3(UiIcon, { name: "stop", size: 15 }) }) : null,
509
+ canSteerDraft ? /* @__PURE__ */ jsx3("button", { className: "scui-queue-send", type: "button", "aria-label": "Queue follow-up instead", onClick: () => send(true), children: /* @__PURE__ */ jsx3(UiIcon, { name: "plus", size: 15 }) }) : null,
510
+ /* @__PURE__ */ jsx3("button", { className: "scui-send", type: "button", "aria-label": canSteerDraft ? "Steer current turn" : queuesNewMessage ? "Queue message" : "Send message", disabled: steering || !draft.trim() && !images.length || !queuesNewMessage && !state.canSend, onClick: () => send(), children: steering ? /* @__PURE__ */ jsx3("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx3(UiIcon, { name: canSteerDraft ? "send" : queuesNewMessage ? "plus" : "send", size: 17 }) })
511
+ ] })
512
+ ] })
513
+ ] });
514
+ }
515
+ export {
516
+ Composer,
517
+ ContinuationBar
518
+ };
@@ -0,0 +1,2 @@
1
+ export type { ActivityGroupProps, MessengerComponents, MessengerSlots, PendingMessageModel, SupercodeUiState, TaskPlanModel, TaskPlanProps, ToolRowProps, TranscriptEntryModel, TranscriptEntryProps, UiAdapter } from './index.js';
2
+ export { ActivityGroup, Conversation, ImageViewer, LoadingStatus, MessageImages, RequestCard, SessionDetails, TaskPlan, ToolRow, TranscriptEntry } from './index.js';