@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.
- package/README.md +84 -2
- package/activity.d.ts +2 -0
- package/activity.mjs +941 -0
- package/components.d.ts +4 -0
- package/components.mjs +737 -485
- package/composer.mjs +82 -20
- package/controller.d.ts +43 -0
- package/controller.mjs +127 -2
- package/conversation.mjs +75 -64
- package/core.d.ts +3 -0
- package/core.mjs +222 -2
- package/embed.mjs +338 -149
- package/icon.mjs +1 -1
- package/index.d.ts +68 -2
- package/logo.mjs +11 -6
- package/messenger.mjs +338 -149
- package/package.json +54 -3
- package/react/activity.d.ts +4 -0
- package/react/activity.mjs +941 -0
- package/react/components.mjs +3081 -0
- package/react/composer.d.ts +2 -0
- package/react/composer.mjs +518 -0
- package/react/conversation.d.ts +2 -0
- package/react/conversation.mjs +1330 -0
- package/react/icon.d.ts +2 -0
- package/react/icon.mjs +51 -0
- package/react/index.d.ts +122 -0
- package/react/logo.d.ts +2 -0
- package/react/logo.mjs +92 -0
- package/react/messenger.d.ts +2 -0
- package/react/messenger.mjs +2974 -0
- package/react/sessions.d.ts +2 -0
- package/react/sessions.mjs +429 -0
- package/react/settings.d.ts +2 -0
- package/react/settings.mjs +319 -0
- package/sessions.mjs +48 -28
- package/settings.mjs +170 -69
- package/styles.css +9 -0
package/composer.mjs
CHANGED
|
@@ -32,6 +32,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
32
|
mode: "none",
|
|
33
33
|
strategy: null,
|
|
34
34
|
canSend: false,
|
|
35
|
+
canSteer: false,
|
|
35
36
|
canResume: false,
|
|
36
37
|
continuationModes: Object.freeze([]),
|
|
37
38
|
canBranch: false,
|
|
@@ -67,6 +68,16 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
67
68
|
function harnessDisplayName(id) {
|
|
68
69
|
return HARNESS_NAMES[id] ?? id;
|
|
69
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
|
+
});
|
|
70
81
|
function canContinueHere(state) {
|
|
71
82
|
if (state.mode !== "mirror" || state.canSend) return false;
|
|
72
83
|
const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
|
|
@@ -130,7 +141,7 @@ var ICONS = {
|
|
|
130
141
|
function UiIcon({ name, size = 16, class: className = "" }) {
|
|
131
142
|
const Glyph = ICONS[name];
|
|
132
143
|
if (!Glyph) return null;
|
|
133
|
-
return /* @__PURE__ */ jsx("svg", {
|
|
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, {}) });
|
|
134
145
|
}
|
|
135
146
|
|
|
136
147
|
// src/context.jsx
|
|
@@ -204,6 +215,26 @@ function partitionAttachments(value) {
|
|
|
204
215
|
}
|
|
205
216
|
return { context, images };
|
|
206
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
|
+
}
|
|
207
238
|
async function imageAttachmentsFromFiles(value) {
|
|
208
239
|
const files = Array.from(value ?? []).filter((file) => file?.type?.startsWith("image/"));
|
|
209
240
|
if (files.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images at a time.`);
|
|
@@ -221,7 +252,7 @@ async function imageAttachmentsFromFiles(value) {
|
|
|
221
252
|
}
|
|
222
253
|
function ContextTray({ items, onRemove }) {
|
|
223
254
|
if (!items.length) return null;
|
|
224
|
-
return /* @__PURE__ */ jsx2("div", {
|
|
255
|
+
return /* @__PURE__ */ jsx2("div", { className: "scui-compose-context", "aria-label": "Attached context", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
225
256
|
/* @__PURE__ */ jsx2(UiIcon, { name: "attach", size: 12 }),
|
|
226
257
|
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
227
258
|
/* @__PURE__ */ jsx2("button", { type: "button", "aria-label": `Remove context ${item.label}`, onClick: () => onRemove(index), children: /* @__PURE__ */ jsx2(UiIcon, { name: "close", size: 12 }) })
|
|
@@ -229,13 +260,23 @@ function ContextTray({ items, onRemove }) {
|
|
|
229
260
|
}
|
|
230
261
|
function ImageTray({ items, onRemove }) {
|
|
231
262
|
if (!items.length) return null;
|
|
232
|
-
return /* @__PURE__ */ jsx2("div", {
|
|
263
|
+
return /* @__PURE__ */ jsx2("div", { className: "scui-compose-images", "aria-label": "Attached images", children: items.map((item, index) => /* @__PURE__ */ jsxs2("span", { children: [
|
|
233
264
|
/* @__PURE__ */ jsx2("img", { src: item.url, alt: "" }),
|
|
234
265
|
/* @__PURE__ */ jsx2("strong", { children: item.label }),
|
|
235
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
|
|
236
267
|
] }, item.id ?? `${item.label}:${index}`)) });
|
|
237
268
|
}
|
|
238
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
|
+
|
|
239
280
|
// src/textarea.js
|
|
240
281
|
import { useLayoutEffect } from "preact/hooks";
|
|
241
282
|
function useAutosizeTextarea(ref, value) {
|
|
@@ -262,28 +303,29 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
262
303
|
const terminal = resume && state.continuationModes?.includes("terminal");
|
|
263
304
|
const join = state.canAttach;
|
|
264
305
|
const branch = state.canBranch;
|
|
265
|
-
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", {
|
|
306
|
+
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
266
307
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
267
308
|
/* @__PURE__ */ jsx3("small", { children: "This session cannot be continued by an available harness." })
|
|
268
309
|
] }) });
|
|
269
|
-
return /* @__PURE__ */ jsxs3("div", {
|
|
310
|
+
return /* @__PURE__ */ jsxs3("div", { className: "scui-continuation", children: [
|
|
270
311
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
271
312
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
272
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." })
|
|
273
314
|
] }),
|
|
274
|
-
/* @__PURE__ */ jsxs3("span", {
|
|
275
|
-
/* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter
|
|
276
|
-
terminal ? /* @__PURE__ */ jsx3("button", { type: "button",
|
|
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
|
|
277
318
|
] })
|
|
278
319
|
] });
|
|
279
320
|
}
|
|
280
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
321
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
|
|
281
322
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
|
|
282
323
|
const [draft, setDraft] = useState2(remembered.draft);
|
|
283
324
|
const [context, setContext] = useState2(remembered.context ?? []);
|
|
284
325
|
const [images, setImages] = useState2(remembered.images ?? []);
|
|
285
326
|
const [queue, setQueue] = useState2((remembered.queue ?? []).map((item) => ({ ...item, context: item.context ?? [], images: item.images ?? [] })));
|
|
286
327
|
const [dispatching, setDispatching] = useState2(false);
|
|
328
|
+
const [steering, setSteering] = useState2(false);
|
|
287
329
|
const [picking, setPicking] = useState2(false);
|
|
288
330
|
const [dragging, setDragging] = useState2(false);
|
|
289
331
|
const [pickerError, setPickerError] = useState2(null);
|
|
@@ -298,8 +340,10 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
298
340
|
remember(draft, context, images, next);
|
|
299
341
|
return next;
|
|
300
342
|
});
|
|
301
|
-
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
302
|
-
const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
|
|
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;
|
|
303
347
|
useEffect2(() => {
|
|
304
348
|
if (!queueBlocked && state.canSend && queue.length) {
|
|
305
349
|
const [next, ...rest] = queue;
|
|
@@ -351,6 +395,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
351
395
|
});
|
|
352
396
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
353
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
|
+
};
|
|
354
406
|
const addImageFiles = (value, source) => {
|
|
355
407
|
const allFiles = Array.from(value ?? []);
|
|
356
408
|
if (!allFiles.length) return false;
|
|
@@ -379,9 +431,17 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
379
431
|
setDragging(false);
|
|
380
432
|
if (addImageFiles(event.dataTransfer?.files, "drop")) event.preventDefault();
|
|
381
433
|
};
|
|
382
|
-
const send = () => {
|
|
434
|
+
const send = (forceQueue = false) => {
|
|
383
435
|
const text = draft.trim();
|
|
384
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
|
+
}
|
|
385
445
|
const message = { text, context, images };
|
|
386
446
|
if (queuesNewMessage) updateQueue((items) => [...items, message]);
|
|
387
447
|
else if (state.canSend) {
|
|
@@ -394,8 +454,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
394
454
|
setImages([]);
|
|
395
455
|
remember("", [], [], queuesNewMessage ? [...queue, message] : queue);
|
|
396
456
|
};
|
|
397
|
-
return /* @__PURE__ */ jsxs3("div", {
|
|
398
|
-
queue.length ? /* @__PURE__ */ jsxs3("div", {
|
|
457
|
+
return /* @__PURE__ */ jsxs3("div", { className: "scui-compose", children: [
|
|
458
|
+
queue.length ? /* @__PURE__ */ jsxs3("div", { className: "scui-queue", children: [
|
|
399
459
|
/* @__PURE__ */ jsxs3("strong", { children: [
|
|
400
460
|
queue.length,
|
|
401
461
|
" queued"
|
|
@@ -411,6 +471,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
411
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 }) })
|
|
412
472
|
] }, `${index}:${item.text}`))
|
|
413
473
|
] }) : null,
|
|
474
|
+
/* @__PURE__ */ jsx3(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }),
|
|
414
475
|
/* @__PURE__ */ jsx3(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
415
476
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
416
477
|
remember(draft, context, next, queue);
|
|
@@ -421,8 +482,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
421
482
|
remember(draft, next, images, queue);
|
|
422
483
|
return next;
|
|
423
484
|
}) }),
|
|
424
|
-
pickerError ? /* @__PURE__ */ jsx3("small", {
|
|
425
|
-
/* @__PURE__ */ jsxs3("div", {
|
|
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) => {
|
|
426
487
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
427
488
|
event.preventDefault();
|
|
428
489
|
setDragging(true);
|
|
@@ -432,8 +493,8 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
432
493
|
}, onDragLeave: (event) => {
|
|
433
494
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
434
495
|
}, onDrop: dropImages, children: [
|
|
435
|
-
adapter.pickContext && state.mode === "control" ? /* @__PURE__ */ jsx3("button", {
|
|
436
|
-
/* @__PURE__ */ jsx3("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onPaste: pasteImages, onInput: (event) => {
|
|
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) => {
|
|
437
498
|
const value = event.currentTarget.value;
|
|
438
499
|
setDraft(value);
|
|
439
500
|
remember(value, context, images, queue);
|
|
@@ -444,8 +505,9 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
444
505
|
}
|
|
445
506
|
} }),
|
|
446
507
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
447
|
-
state.busy ? /* @__PURE__ */ jsx3("button", {
|
|
448
|
-
/* @__PURE__ */ jsx3("button", {
|
|
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 }) })
|
|
449
511
|
] })
|
|
450
512
|
] })
|
|
451
513
|
] });
|
package/controller.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
SessionArtifact,
|
|
3
|
+
SessionDescriptor,
|
|
3
4
|
SessionFormat,
|
|
4
5
|
} from '@volter-ai-dev/supercode-harness-sdk';
|
|
5
6
|
import type {
|
|
@@ -55,6 +56,30 @@ export function projectClientSnapshot(
|
|
|
55
56
|
options?: ClientProjectionOptions,
|
|
56
57
|
): SupercodeUiState;
|
|
57
58
|
|
|
59
|
+
export interface SessionInventoryProjectionOptions {
|
|
60
|
+
/** Mint a transport-safe key while retaining the reversible locator only in the trusted host. */
|
|
61
|
+
keyFor(descriptor: SessionDescriptor): string;
|
|
62
|
+
now?: number;
|
|
63
|
+
home?: string;
|
|
64
|
+
active?: { harness: string; sessionId: string | null } | null;
|
|
65
|
+
maxSessions?: number;
|
|
66
|
+
liveWindowMs?: number;
|
|
67
|
+
preserveOrder?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function sessionConversationUpdatedAt(descriptor: SessionDescriptor): number | null;
|
|
71
|
+
export function sessionDescriptorRuntimeStatus(
|
|
72
|
+
descriptor: SessionDescriptor,
|
|
73
|
+
): 'running' | 'busy' | 'idle' | null;
|
|
74
|
+
export interface ProjectedSessionRowModel extends SessionRowModel {
|
|
75
|
+
preview: string;
|
|
76
|
+
previewUpdatedAt: number | null;
|
|
77
|
+
}
|
|
78
|
+
export function projectSessionInventory(
|
|
79
|
+
descriptors: readonly SessionDescriptor[],
|
|
80
|
+
options: SessionInventoryProjectionOptions,
|
|
81
|
+
): ProjectedSessionRowModel[];
|
|
82
|
+
|
|
58
83
|
export interface ResolvableTranscriptImage {
|
|
59
84
|
id?: string;
|
|
60
85
|
label: string;
|
|
@@ -79,6 +104,8 @@ export interface ControllerBindingOptions {
|
|
|
79
104
|
onIntent?: (intent: SupercodeUiIntent) => void | Promise<void>;
|
|
80
105
|
onUnsupported?: (intent: SupercodeUiIntent) => void | Promise<void>;
|
|
81
106
|
onError?: (error: unknown, intent: SupercodeUiIntent) => void | Promise<void>;
|
|
107
|
+
/** Return true after handling an intent to replace the standard controller mapping. */
|
|
108
|
+
handleIntent?: (intent: SupercodeUiIntent) => boolean | Promise<boolean>;
|
|
82
109
|
onArtifact?: (artifact: SessionArtifact, intent: Extract<SupercodeUiIntent, { action: 'export' }>) => void | Promise<void>;
|
|
83
110
|
onDraft?: (text: string) => void | Promise<void>;
|
|
84
111
|
onAcknowledge?: (key: string) => void | Promise<void>;
|
|
@@ -88,8 +115,24 @@ export interface ControllerBindingOptions {
|
|
|
88
115
|
onResumeTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'resume' }>) => void | Promise<void>;
|
|
89
116
|
copyText?: UiAdapter['copyText'];
|
|
90
117
|
resolveImage?: UiAdapter['resolveImage'];
|
|
118
|
+
confirmIntent?: UiAdapter['confirmIntent'];
|
|
119
|
+
pickContext?: UiAdapter['pickContext'];
|
|
120
|
+
onClose?: UiAdapter['onClose'];
|
|
121
|
+
onOpen?: UiAdapter['onOpen'];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface ControllerIntentTarget {
|
|
125
|
+
getSnapshot(): SupercodeClientSnapshot;
|
|
126
|
+
dispatch(action: Parameters<SupercodeController['dispatch']>[0]): Promise<SupercodeClientSnapshot>;
|
|
127
|
+
exportSession?: SupercodeController['exportSession'];
|
|
91
128
|
}
|
|
92
129
|
|
|
130
|
+
export function dispatchControllerIntent(
|
|
131
|
+
controller: ControllerIntentTarget,
|
|
132
|
+
intent: SupercodeUiIntent,
|
|
133
|
+
options?: ControllerBindingOptions,
|
|
134
|
+
): Promise<void>;
|
|
135
|
+
|
|
93
136
|
export interface SupercodeUiBinding {
|
|
94
137
|
adapter: UiAdapter;
|
|
95
138
|
getState(): SupercodeUiState;
|
package/controller.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { conversationPreviewText } from '@volter-ai-dev/supercode-client';
|
|
1
2
|
import { createToolPresentation, normalizeUiState, relativeAge } from './core.mjs';
|
|
2
3
|
|
|
3
4
|
const HARNESS_LABELS = {
|
|
@@ -28,6 +29,110 @@ function workspaceName(cwd) {
|
|
|
28
29
|
return cwd.replaceAll('\\', '/').split('/').filter(Boolean).at(-1) ?? cwd;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function shortWorkspacePath(cwd, home) {
|
|
33
|
+
if (typeof cwd !== 'string' || !cwd) return '';
|
|
34
|
+
const root = typeof home === 'string' && home.endsWith('/') ? home.slice(0, -1) : home;
|
|
35
|
+
return root && (cwd === root || cwd.startsWith(`${root}/`)) ? `~${cwd.slice(root.length)}` : cwd;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function compactTopic(value) {
|
|
39
|
+
const text = typeof value === 'string' ? value.replace(/\s+/g, ' ').trim() : '';
|
|
40
|
+
if (text.length <= 72) return text;
|
|
41
|
+
const prefix = text.slice(0, 71);
|
|
42
|
+
const boundary = prefix.lastIndexOf(' ');
|
|
43
|
+
return `${prefix.slice(0, boundary >= 40 ? boundary : 71).trimEnd()}…`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function sessionDescriptorRuntimeStatus(descriptor) {
|
|
47
|
+
const activity = descriptor?.activity;
|
|
48
|
+
if (activity && typeof activity === 'object') {
|
|
49
|
+
if (activity.presence === 'persisted') return null;
|
|
50
|
+
if (activity.turn === 'working') return 'busy';
|
|
51
|
+
if (activity.turn === 'idle' || activity.turn === 'needs_input') return 'idle';
|
|
52
|
+
if (activity.presence === 'running' || activity.presence === 'shutting_down') return 'running';
|
|
53
|
+
}
|
|
54
|
+
return ['running', 'busy', 'idle'].includes(descriptor?.live_status)
|
|
55
|
+
? descriptor.live_status
|
|
56
|
+
: null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function descriptorPreview(descriptor) {
|
|
60
|
+
for (const candidate of descriptor?.latest_message_candidates ?? []) {
|
|
61
|
+
const text = conversationPreviewText([candidate]);
|
|
62
|
+
if (!text) continue;
|
|
63
|
+
return { text, updatedAt: timestampFromMetadata(candidate.metadata) };
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function descriptorTitle(descriptor) {
|
|
69
|
+
const nativeTitle = typeof descriptor?.title === 'string' ? descriptor.title.trim() : '';
|
|
70
|
+
const workspace = workspaceName(descriptor?.cwd);
|
|
71
|
+
if (nativeTitle && nativeTitle !== workspace && nativeTitle !== descriptor?.cwd) {
|
|
72
|
+
return compactTopic(nativeTitle);
|
|
73
|
+
}
|
|
74
|
+
if (descriptor?.locator?.harness !== 'claude-code' && descriptor?.locator?.harness !== 'codex') {
|
|
75
|
+
return compactTopic(nativeTitle || workspace || descriptor?.locator?.session_id?.slice(0, 8)) || 'Untitled chat';
|
|
76
|
+
}
|
|
77
|
+
const openingUserMessages = (descriptor?.preview_candidates ?? []).filter(
|
|
78
|
+
(candidate) => candidate?.role === undefined || candidate.role === 'user',
|
|
79
|
+
);
|
|
80
|
+
return compactTopic(conversationPreviewText(openingUserMessages)) || 'Untitled chat';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Timestamp represented by a messenger row, independent of native store heartbeat writes. */
|
|
84
|
+
export function sessionConversationUpdatedAt(descriptor) {
|
|
85
|
+
return descriptorPreview(descriptor)?.updatedAt ?? descriptor?.updated_at_ms ?? null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Project trusted machine-wide descriptors into the same bounded rows as controller snapshots.
|
|
90
|
+
* The host supplies opaque keys because only it may retain the reversible locator mapping.
|
|
91
|
+
*/
|
|
92
|
+
export function projectSessionInventory(descriptors, options) {
|
|
93
|
+
if (typeof options?.keyFor !== 'function') {
|
|
94
|
+
throw new TypeError('projectSessionInventory requires an opaque keyFor callback');
|
|
95
|
+
}
|
|
96
|
+
const now = Number.isFinite(options.now) ? options.now : Date.now();
|
|
97
|
+
const maxSessions = positiveInteger(options.maxSessions, DEFAULT_MAX_SESSIONS);
|
|
98
|
+
const liveWindowMs = positiveInteger(options.liveWindowMs, DEFAULT_LIVE_WINDOW_MS);
|
|
99
|
+
const active = options.active;
|
|
100
|
+
const seen = new Set();
|
|
101
|
+
const rows = [];
|
|
102
|
+
const ordered = options.preserveOrder
|
|
103
|
+
? [...(descriptors ?? [])]
|
|
104
|
+
: [...(descriptors ?? [])].sort(
|
|
105
|
+
(left, right) => (sessionConversationUpdatedAt(right) ?? 0) - (sessionConversationUpdatedAt(left) ?? 0),
|
|
106
|
+
);
|
|
107
|
+
for (const descriptor of ordered) {
|
|
108
|
+
if (!descriptor?.locator) continue;
|
|
109
|
+
const key = options.keyFor(descriptor);
|
|
110
|
+
if (typeof key !== 'string' || !key || seen.has(key)) continue;
|
|
111
|
+
seen.add(key);
|
|
112
|
+
const preview = descriptorPreview(descriptor);
|
|
113
|
+
const updatedAt = typeof descriptor.updated_at_ms === 'number' ? descriptor.updated_at_ms : null;
|
|
114
|
+
rows.push({
|
|
115
|
+
key,
|
|
116
|
+
harness: descriptor.locator.harness,
|
|
117
|
+
name: workspaceName(descriptor.cwd) || 'no workspace',
|
|
118
|
+
cwd: shortWorkspacePath(descriptor.cwd, options.home),
|
|
119
|
+
title: descriptorTitle(descriptor),
|
|
120
|
+
preview: preview?.text ?? '',
|
|
121
|
+
age: relativeAge(preview?.updatedAt ?? updatedAt, now),
|
|
122
|
+
previewUpdatedAt: preview?.updatedAt ?? null,
|
|
123
|
+
updatedAt,
|
|
124
|
+
messages: typeof descriptor.message_count === 'number' ? descriptor.message_count : null,
|
|
125
|
+
active: active?.sessionId != null
|
|
126
|
+
&& descriptor.locator.harness === active.harness
|
|
127
|
+
&& descriptor.locator.session_id === active.sessionId,
|
|
128
|
+
live: updatedAt !== null && now - updatedAt <= liveWindowMs,
|
|
129
|
+
runtimeStatus: sessionDescriptorRuntimeStatus(descriptor),
|
|
130
|
+
});
|
|
131
|
+
if (rows.length >= maxSessions) break;
|
|
132
|
+
}
|
|
133
|
+
return rows;
|
|
134
|
+
}
|
|
135
|
+
|
|
31
136
|
function payloadText(payload) {
|
|
32
137
|
if (typeof payload === 'string') return payload;
|
|
33
138
|
try {
|
|
@@ -365,6 +470,7 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
|
|
|
365
470
|
mode: snapshot.connection?.mode ?? 'none',
|
|
366
471
|
strategy: snapshot.connection?.strategy ?? null,
|
|
367
472
|
canSend: actions.send === true,
|
|
473
|
+
canSteer: actions.steer === true,
|
|
368
474
|
canResume: actions.resume === true,
|
|
369
475
|
continuationModes: options.continuationModes ?? (actions.resume === true ? ['headless'] : []),
|
|
370
476
|
canBranch: actions.branch === true,
|
|
@@ -403,6 +509,19 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
|
|
|
403
509
|
installed: harness.installed === true,
|
|
404
510
|
startable: harness.availableActions?.start === true,
|
|
405
511
|
reason: harness.reason ?? null,
|
|
512
|
+
auth: harness.auth,
|
|
513
|
+
runtime: harness.runtime,
|
|
514
|
+
protocol: harness.protocol,
|
|
515
|
+
repair: harness.repair ?? null,
|
|
516
|
+
capabilities: {
|
|
517
|
+
start: harness.availableActions?.start === true,
|
|
518
|
+
resume: harness.availableActions?.resume === true,
|
|
519
|
+
attach: harness.availableActions?.attach === true,
|
|
520
|
+
send: harness.availableActions?.send === true,
|
|
521
|
+
interrupt: harness.availableActions?.interrupt === true,
|
|
522
|
+
steer: harness.availableActions?.steer === true,
|
|
523
|
+
respond: harness.availableActions?.respond === true,
|
|
524
|
+
},
|
|
406
525
|
})),
|
|
407
526
|
history: {
|
|
408
527
|
sessionLimit: options.history?.sessionLimit ?? sessions.length,
|
|
@@ -440,12 +559,14 @@ export function createClientProjection(snapshot, options = {}) {
|
|
|
440
559
|
};
|
|
441
560
|
}
|
|
442
561
|
|
|
443
|
-
async function
|
|
562
|
+
export async function dispatchControllerIntent(controller, intent, options = {}) {
|
|
563
|
+
if (await options.handleIntent?.(intent)) return;
|
|
444
564
|
const snapshot = controller.getSnapshot();
|
|
445
565
|
const active = snapshot.activeSessionKey;
|
|
446
566
|
if (intent.action === 'mounted') return;
|
|
447
567
|
if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
|
|
448
568
|
if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
|
|
569
|
+
if (intent.action === 'steer') return controller.dispatch({ type: 'steer', text: intent.text });
|
|
449
570
|
if (intent.action === 'new') {
|
|
450
571
|
if (intent.mode === 'terminal') {
|
|
451
572
|
return options.onStartTerminal
|
|
@@ -496,12 +617,16 @@ export function createControllerBinding(controller, options = {}) {
|
|
|
496
617
|
const adapter = {
|
|
497
618
|
onIntent(intent) {
|
|
498
619
|
return Promise.resolve(options.onIntent?.(intent))
|
|
499
|
-
.then(() =>
|
|
620
|
+
.then(() => dispatchControllerIntent(controller, intent, options))
|
|
500
621
|
.catch((error) => options.onError?.(error, intent))
|
|
501
622
|
.then(() => undefined);
|
|
502
623
|
},
|
|
503
624
|
...(options.copyText ? { copyText: options.copyText } : {}),
|
|
504
625
|
...(options.resolveImage ? { resolveImage: options.resolveImage } : {}),
|
|
626
|
+
...(options.confirmIntent ? { confirmIntent: options.confirmIntent } : {}),
|
|
627
|
+
...(options.pickContext ? { pickContext: options.pickContext } : {}),
|
|
628
|
+
...(options.onClose ? { onClose: options.onClose } : {}),
|
|
629
|
+
...(options.onOpen ? { onOpen: options.onOpen } : {}),
|
|
505
630
|
};
|
|
506
631
|
return {
|
|
507
632
|
adapter,
|