@volter-ai-dev/supercode-ui 0.1.11 → 0.1.13
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/components.mjs +116 -39
- package/composer.d.ts +1 -1
- package/composer.mjs +25 -7
- package/conversation.d.ts +1 -0
- package/conversation.mjs +11 -4
- package/embed.mjs +116 -39
- package/index.d.ts +10 -3
- package/messenger.mjs +116 -39
- package/package.json +1 -1
- package/sessions.mjs +11 -5
- package/styles.css +3 -0
package/components.mjs
CHANGED
|
@@ -587,10 +587,11 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
587
587
|
/* @__PURE__ */ jsx("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
|
|
588
588
|
] });
|
|
589
589
|
}
|
|
590
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, onPending }) {
|
|
590
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
591
591
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
|
|
592
592
|
const [draft, setDraft] = useState(remembered.draft);
|
|
593
593
|
const [queue, setQueue] = useState(remembered.queue);
|
|
594
|
+
const [dispatching, setDispatching] = useState(false);
|
|
594
595
|
const textarea = useRef(null);
|
|
595
596
|
const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
|
|
596
597
|
const updateQueue = (update) => setQueue((items) => {
|
|
@@ -598,15 +599,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
598
599
|
remember(draft, next);
|
|
599
600
|
return next;
|
|
600
601
|
});
|
|
602
|
+
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
603
|
+
const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
|
|
601
604
|
useEffect(() => {
|
|
602
|
-
if (!
|
|
605
|
+
if (!queueBlocked && state.canSend && queue.length) {
|
|
603
606
|
const [next, ...rest] = queue;
|
|
607
|
+
setDispatching(true);
|
|
604
608
|
setQueue(rest);
|
|
605
609
|
remember(draft, rest);
|
|
606
610
|
onPending?.(next);
|
|
607
611
|
adapter.onIntent({ action: "send", text: next });
|
|
608
612
|
}
|
|
609
|
-
}, [adapter, draft, memoryKey, onPending, queue,
|
|
613
|
+
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
614
|
+
useEffect(() => {
|
|
615
|
+
if (pendingStatus !== null || state.busy) setDispatching(false);
|
|
616
|
+
}, [pendingStatus, state.busy]);
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
textarea.current?.focus({ preventScroll: true });
|
|
619
|
+
}, [memoryKey]);
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
if (!restoreDraft) return;
|
|
622
|
+
setDraft(restoreDraft.text);
|
|
623
|
+
remember(restoreDraft.text, queue);
|
|
624
|
+
textarea.current?.focus({ preventScroll: true });
|
|
625
|
+
onDraftRestored?.(restoreDraft.id);
|
|
626
|
+
}, [onDraftRestored, restoreDraft?.id]);
|
|
610
627
|
useEffect(() => {
|
|
611
628
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
612
629
|
return () => clearTimeout(timer);
|
|
@@ -614,13 +631,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
614
631
|
const send = () => {
|
|
615
632
|
const text = draft.trim();
|
|
616
633
|
if (!text) return;
|
|
617
|
-
if (
|
|
634
|
+
if (queuesNewMessage) updateQueue((items) => [...items, text]);
|
|
618
635
|
else if (state.canSend) {
|
|
636
|
+
if (onPending) setDispatching(true);
|
|
619
637
|
onPending?.(text);
|
|
620
638
|
adapter.onIntent({ action: "send", text });
|
|
621
639
|
} else return;
|
|
622
640
|
setDraft("");
|
|
623
|
-
remember("",
|
|
641
|
+
remember("", queuesNewMessage ? [...queue, text] : queue);
|
|
624
642
|
};
|
|
625
643
|
return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
|
|
626
644
|
queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
|
|
@@ -634,7 +652,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
634
652
|
] }, `${index}:${item}`))
|
|
635
653
|
] }) : null,
|
|
636
654
|
/* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
|
|
637
|
-
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" :
|
|
655
|
+
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
|
|
638
656
|
const value = event.currentTarget.value;
|
|
639
657
|
setDraft(value);
|
|
640
658
|
remember(value, queue);
|
|
@@ -646,7 +664,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
646
664
|
} }),
|
|
647
665
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
648
666
|
state.busy ? /* @__PURE__ */ jsx("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: "\u25A0" }) : null,
|
|
649
|
-
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label":
|
|
667
|
+
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: queuesNewMessage ? "+" : "\u2191" })
|
|
650
668
|
] })
|
|
651
669
|
] })
|
|
652
670
|
] });
|
|
@@ -1068,6 +1086,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1068
1086
|
const After = slots.afterConversation;
|
|
1069
1087
|
const Empty = slots.emptyConversation;
|
|
1070
1088
|
const remember = (value) => boundedSet(conversationMemory, memoryKey, value);
|
|
1089
|
+
const pendingMessage = typeof pending === "string" ? { text: pending, status: "sending" } : pending;
|
|
1071
1090
|
const pin = () => {
|
|
1072
1091
|
if (!scroller.current) return;
|
|
1073
1092
|
scroller.current.scrollTop = scroller.current.scrollHeight;
|
|
@@ -1089,7 +1108,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1089
1108
|
if (remembered.top !== null && !remembered.atBottom) element.scrollTop = remembered.top;
|
|
1090
1109
|
else pin();
|
|
1091
1110
|
} else if (atBottom) pin();
|
|
1092
|
-
}, [memoryKey, state.transcript.
|
|
1111
|
+
}, [memoryKey, state.transcript, state.busy, state.operation, pendingMessage?.text, pendingMessage?.status]);
|
|
1093
1112
|
return /* @__PURE__ */ jsxs2("div", { class: "scui-conversation-wrap", children: [
|
|
1094
1113
|
/* @__PURE__ */ jsx3("div", { class: "scui-conversation", ref: scroller, tabIndex: 0, "aria-label": "Conversation", onScroll: (event) => {
|
|
1095
1114
|
const element = event.currentTarget;
|
|
@@ -1109,9 +1128,15 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1109
1128
|
!blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx3(LoadingStatus, { state }) : null,
|
|
1110
1129
|
!blocks.length && state.startup === "ready" ? Empty ? /* @__PURE__ */ jsx3(Empty, { state, adapter, value: null }) : /* @__PURE__ */ jsx3("div", { class: "scui-empty", children: state.error ?? (state.harness ? `${harnessDisplayName(state.harness)} is listening. Say something.` : "No transcript yet.") }) : null,
|
|
1111
1130
|
blocks.map((block) => block.kind === "activity" ? /* @__PURE__ */ jsx3(Group, { value: block.entries, entries: block.entries, state, adapter }, block.id) : /* @__PURE__ */ jsx3(Entry, { value: block.entry, entry: block.entry, state, adapter }, block.id)),
|
|
1112
|
-
|
|
1113
|
-
/* @__PURE__ */ jsx3(Markdown, { value:
|
|
1114
|
-
/* @__PURE__ */
|
|
1131
|
+
pendingMessage ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, children: [
|
|
1132
|
+
/* @__PURE__ */ jsx3(Markdown, { value: pendingMessage.text }),
|
|
1133
|
+
/* @__PURE__ */ jsxs2("footer", { children: [
|
|
1134
|
+
/* @__PURE__ */ jsx3("small", { children: pendingMessage.status === "failed" ? "Not sent" : "Sending\u2026" }),
|
|
1135
|
+
pendingMessage.status === "failed" ? /* @__PURE__ */ jsxs2("span", { children: [
|
|
1136
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onRetry, children: "Retry" }),
|
|
1137
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onEdit, children: "Edit" })
|
|
1138
|
+
] }) : null
|
|
1139
|
+
] })
|
|
1115
1140
|
] }) : null,
|
|
1116
1141
|
state.busy ? /* @__PURE__ */ jsxs2("div", { class: "scui-working", role: "status", children: [
|
|
1117
1142
|
/* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -1192,15 +1217,15 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
|
1192
1217
|
}
|
|
1193
1218
|
|
|
1194
1219
|
// src/messenger.jsx
|
|
1195
|
-
import { useEffect as
|
|
1220
|
+
import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState4 } from "preact/hooks";
|
|
1196
1221
|
|
|
1197
1222
|
// src/sessions.jsx
|
|
1198
|
-
import { useState as useState3 } from "preact/hooks";
|
|
1223
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "preact/hooks";
|
|
1199
1224
|
import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
1200
1225
|
function SessionRow({ row, state, onOpen }) {
|
|
1201
1226
|
const activity = sessionActivity(state, row);
|
|
1202
1227
|
const preview = state.attention.find((item) => item.key === row.key)?.preview;
|
|
1203
|
-
return /* @__PURE__ */ jsxs4("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, type: "button", onClick: () => onOpen(row), children: [
|
|
1228
|
+
return /* @__PURE__ */ jsxs4("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", onClick: () => onOpen(row), children: [
|
|
1204
1229
|
/* @__PURE__ */ jsx5(HarnessLogo, { id: row.harness, activity, size: 34 }),
|
|
1205
1230
|
/* @__PURE__ */ jsxs4("span", { class: "scui-session-copy", children: [
|
|
1206
1231
|
/* @__PURE__ */ jsxs4("span", { children: [
|
|
@@ -1215,11 +1240,17 @@ function SessionRow({ row, state, onOpen }) {
|
|
|
1215
1240
|
] })
|
|
1216
1241
|
] });
|
|
1217
1242
|
}
|
|
1218
|
-
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS }) {
|
|
1243
|
+
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null }) {
|
|
1219
1244
|
const [query, setQuery] = useState3("");
|
|
1245
|
+
const root = useRef3(null);
|
|
1220
1246
|
const rows = filterSessions(state.sessions, query);
|
|
1221
1247
|
const Row = components.SessionRow ?? SessionRow;
|
|
1222
|
-
|
|
1248
|
+
useEffect4(() => {
|
|
1249
|
+
if (!focusKey || !root.current) return;
|
|
1250
|
+
const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
|
|
1251
|
+
(target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
|
|
1252
|
+
}, [focusKey, rows.length]);
|
|
1253
|
+
return /* @__PURE__ */ jsxs4("section", { class: "scui-list", ref: root, children: [
|
|
1223
1254
|
/* @__PURE__ */ jsxs4("header", { class: "scui-head", children: [
|
|
1224
1255
|
/* @__PURE__ */ jsxs4("span", { class: "scui-head-copy", children: [
|
|
1225
1256
|
/* @__PURE__ */ jsx5("strong", { children: labels.chats }),
|
|
@@ -1228,7 +1259,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
1228
1259
|
" recent conversations"
|
|
1229
1260
|
] })
|
|
1230
1261
|
] }),
|
|
1231
|
-
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
1262
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
1232
1263
|
onClose ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Close", onClick: onClose, children: "\xD7" }) : null
|
|
1233
1264
|
] }),
|
|
1234
1265
|
state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state, compact: rows.length > 0 }) : null,
|
|
@@ -1247,6 +1278,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
1247
1278
|
|
|
1248
1279
|
// src/messenger.jsx
|
|
1249
1280
|
import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
|
|
1281
|
+
var pendingMessageMemory = /* @__PURE__ */ new Map();
|
|
1250
1282
|
function Receipt({ state, adapter }) {
|
|
1251
1283
|
const receipt = state.reductionReceipt;
|
|
1252
1284
|
if (receipt) return /* @__PURE__ */ jsx6("div", { class: "scui-receipt", children: /* @__PURE__ */ jsxs5("span", { children: [
|
|
@@ -1284,14 +1316,14 @@ function Receipt({ state, adapter }) {
|
|
|
1284
1316
|
] });
|
|
1285
1317
|
return null;
|
|
1286
1318
|
}
|
|
1287
|
-
function ChatHeader({ state, adapter, onBack, onNew, onClose }) {
|
|
1319
|
+
function ChatHeader({ state, adapter, pendingStatus, onBack, onNew, onClose }) {
|
|
1288
1320
|
const harness = state.attached?.harness ?? state.harness;
|
|
1289
1321
|
const targets = state.harnesses.filter((item) => item.startable);
|
|
1290
1322
|
const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
|
|
1291
|
-
const status = state.needsInput ? "Needs input" : state.busy ? "Working" : state.mode === "mirror" ? "Read-only" : "Ready";
|
|
1323
|
+
const status = state.needsInput ? "Needs input" : pendingStatus === "failed" ? "Send failed" : state.busy ? "Working" : pendingStatus === "sending" ? "Sending" : pendingStatus === "editing" ? "Editing message" : state.mode === "mirror" ? "Read-only" : "Ready";
|
|
1292
1324
|
const menu = state.canDetach || state.canOpenTerminal || state.canBranch || state.canAttach || state.canExport || state.canReduce;
|
|
1293
1325
|
return /* @__PURE__ */ jsxs5("header", { class: "scui-head scui-chat-head", children: [
|
|
1294
|
-
/* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Back to chats", onClick: onBack, children: "\u2039" }),
|
|
1326
|
+
/* @__PURE__ */ jsx6("button", { type: "button", autofocus: state.mode === "mirror" && !state.canSend, "aria-label": "Back to chats", onClick: onBack, children: "\u2039" }),
|
|
1295
1327
|
/* @__PURE__ */ jsx6(HarnessLogo, { id: harness, size: 28 }),
|
|
1296
1328
|
/* @__PURE__ */ jsxs5("span", { class: "scui-head-copy", children: [
|
|
1297
1329
|
/* @__PURE__ */ jsx6("strong", { children: title }),
|
|
@@ -1321,15 +1353,48 @@ function ChatHeader({ state, adapter, onBack, onNew, onClose }) {
|
|
|
1321
1353
|
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
|
|
1322
1354
|
const Header = slots.header;
|
|
1323
1355
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
1324
|
-
const [pending,
|
|
1325
|
-
const
|
|
1326
|
-
|
|
1327
|
-
|
|
1356
|
+
const [pending, setPendingState] = useState4(() => pendingMessageMemory.get(memoryKey) ?? null);
|
|
1357
|
+
const [restoreDraft, setRestoreDraft] = useState4(null);
|
|
1358
|
+
const restoreSequence = useRef4(0);
|
|
1359
|
+
const acknowledged = useRef4(/* @__PURE__ */ new Set());
|
|
1360
|
+
const setPending = (update) => setPendingState((current) => {
|
|
1361
|
+
const next = typeof update === "function" ? update(current) : update;
|
|
1362
|
+
if (next) boundedSet(pendingMessageMemory, memoryKey, next);
|
|
1363
|
+
else pendingMessageMemory.delete(memoryKey);
|
|
1364
|
+
return next;
|
|
1365
|
+
});
|
|
1366
|
+
useEffect5(() => {
|
|
1367
|
+
setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
|
|
1368
|
+
setRestoreDraft(null);
|
|
1328
1369
|
}, [memoryKey]);
|
|
1329
|
-
|
|
1330
|
-
if (pending
|
|
1331
|
-
|
|
1332
|
-
|
|
1370
|
+
useEffect5(() => {
|
|
1371
|
+
if (!pending) return;
|
|
1372
|
+
if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
|
|
1373
|
+
setPending(null);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1377
|
+
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1378
|
+
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1379
|
+
const beginPending = (text) => setPending({
|
|
1380
|
+
text,
|
|
1381
|
+
status: "sending",
|
|
1382
|
+
initialError: state.error,
|
|
1383
|
+
seenBusy: state.busy,
|
|
1384
|
+
baselineIds: state.transcript.filter((entry) => entry.role === "user" && entry.text.trim() === text.trim()).map((entry) => entry.id)
|
|
1385
|
+
});
|
|
1386
|
+
const retryPending = () => {
|
|
1387
|
+
if (!pending) return;
|
|
1388
|
+
adapter.onIntent({ action: "send", text: pending.text });
|
|
1389
|
+
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1390
|
+
};
|
|
1391
|
+
const editPending = () => {
|
|
1392
|
+
if (!pending) return;
|
|
1393
|
+
restoreSequence.current += 1;
|
|
1394
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text });
|
|
1395
|
+
setPending({ ...pending, status: "editing" });
|
|
1396
|
+
};
|
|
1397
|
+
useEffect5(() => {
|
|
1333
1398
|
const key = state.attached?.key;
|
|
1334
1399
|
if (!key || !state.attention.some((item) => item.key === key)) {
|
|
1335
1400
|
if (key) acknowledged.current.delete(key);
|
|
@@ -1340,8 +1405,9 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1340
1405
|
adapter.onIntent({ action: "ack", key });
|
|
1341
1406
|
}
|
|
1342
1407
|
}, [adapter, state.attached?.key, state.attention]);
|
|
1408
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1343
1409
|
return /* @__PURE__ */ jsxs5("section", { class: "scui-chat", children: [
|
|
1344
|
-
Header ? /* @__PURE__ */ jsx6(Header, { state, adapter, value: null }) : /* @__PURE__ */ jsx6(ChatHeader, { state, adapter, onBack, onNew, onClose }),
|
|
1410
|
+
Header ? /* @__PURE__ */ jsx6(Header, { state, adapter, value: null }) : /* @__PURE__ */ jsx6(ChatHeader, { state, adapter, pendingStatus: pending?.status ?? null, onBack, onNew, onClose }),
|
|
1345
1411
|
operationLabel(state.operation) ? /* @__PURE__ */ jsxs5("div", { class: "scui-operation", role: "status", children: [
|
|
1346
1412
|
/* @__PURE__ */ jsx6("i", {}),
|
|
1347
1413
|
operationLabel(state.operation)
|
|
@@ -1351,9 +1417,9 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1351
1417
|
state.recoverable ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => adapter.onIntent({ action: "refresh" }), children: "Retry" }) : null
|
|
1352
1418
|
] }) : null,
|
|
1353
1419
|
/* @__PURE__ */ jsx6(Receipt, { state, adapter }),
|
|
1354
|
-
/* @__PURE__ */ jsx6(Conversation, { state, adapter, components, slots, memoryKey, pending }, memoryKey),
|
|
1420
|
+
/* @__PURE__ */ jsx6(Conversation, { state, adapter, components, slots, memoryKey, pending: pendingMessage }, memoryKey),
|
|
1355
1421
|
/* @__PURE__ */ jsx6(ContinuationBar, { state, adapter, labels }),
|
|
1356
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx6(Composer, { state, adapter, labels, memoryKey, onPending:
|
|
1422
|
+
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx6(Composer, { state, adapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null
|
|
1357
1423
|
] });
|
|
1358
1424
|
}
|
|
1359
1425
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
@@ -1362,13 +1428,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
|
1362
1428
|
const [harness, setHarness] = useState4(startable[0]?.id ?? "");
|
|
1363
1429
|
const [draft, setDraft] = useState4("");
|
|
1364
1430
|
const [starting, setStarting] = useState4(false);
|
|
1365
|
-
|
|
1431
|
+
useEffect5(() => {
|
|
1366
1432
|
if (!startable.some((item) => item.id === harness)) setHarness(startable[0]?.id ?? "");
|
|
1367
1433
|
}, [harness, startableKey]);
|
|
1368
|
-
|
|
1434
|
+
useEffect5(() => {
|
|
1369
1435
|
if (starting && (state.busy || state.transcript.length)) onStarted();
|
|
1370
1436
|
}, [onStarted, starting, state.busy, state.transcript.length]);
|
|
1371
|
-
|
|
1437
|
+
useEffect5(() => {
|
|
1372
1438
|
if (starting && state.error && !state.busy && !state.operation) setStarting(false);
|
|
1373
1439
|
}, [starting, state.busy, state.error, state.operation]);
|
|
1374
1440
|
const send = () => {
|
|
@@ -1401,7 +1467,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
|
1401
1467
|
] }, item.id)) })
|
|
1402
1468
|
] }),
|
|
1403
1469
|
/* @__PURE__ */ jsxs5("div", { class: "scui-envelope", children: [
|
|
1404
|
-
/* @__PURE__ */ jsx6("textarea", { rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || starting, onInput: (event) => setDraft(event.currentTarget.value), onKeyDown: (event) => {
|
|
1470
|
+
/* @__PURE__ */ jsx6("textarea", { autofocus: true, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || starting, onInput: (event) => setDraft(event.currentTarget.value), onKeyDown: (event) => {
|
|
1405
1471
|
if (isSendKey(event)) {
|
|
1406
1472
|
event.preventDefault();
|
|
1407
1473
|
send();
|
|
@@ -1413,11 +1479,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
|
1413
1479
|
] });
|
|
1414
1480
|
}
|
|
1415
1481
|
function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, labels, components = {}, slots = {} }) {
|
|
1416
|
-
const state = normalizeUiState(stateInput);
|
|
1482
|
+
const state = useMemo2(() => normalizeUiState(stateInput), [stateInput]);
|
|
1417
1483
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
1418
1484
|
const [view, setView] = useState4(initialView ?? (state.attention.length ? "list" : state.attached || state.transcript.length ? "chat" : "list"));
|
|
1419
1485
|
const [opening, setOpening] = useState4(null);
|
|
1420
|
-
|
|
1486
|
+
const [listFocus, setListFocus] = useState4(null);
|
|
1487
|
+
useEffect5(() => {
|
|
1421
1488
|
if (!opening) return;
|
|
1422
1489
|
if (state.attached?.key === opening.key) {
|
|
1423
1490
|
setOpening(null);
|
|
@@ -1427,6 +1494,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
1427
1494
|
}
|
|
1428
1495
|
}, [opening, state.attachError?.key, state.attached?.key, state.error, state.operation]);
|
|
1429
1496
|
const open = (row) => {
|
|
1497
|
+
setListFocus(row.key);
|
|
1430
1498
|
setOpening(row);
|
|
1431
1499
|
adapter.onIntent({ action: "ack", key: row.key });
|
|
1432
1500
|
adapter.onIntent({ action: "attach", key: row.key });
|
|
@@ -1434,9 +1502,18 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
1434
1502
|
const close = () => adapter.onClose?.();
|
|
1435
1503
|
const Footer = slots.footer;
|
|
1436
1504
|
return /* @__PURE__ */ jsxs5("main", { class: `scui-root ${className}`, "data-view": view, "data-mode": state.mode, "aria-label": "Supercode messenger", children: [
|
|
1437
|
-
view === "list" ? /* @__PURE__ */ jsx6(SessionList, { state, adapter, onOpen: open, onNew: () =>
|
|
1505
|
+
view === "list" ? /* @__PURE__ */ jsx6(SessionList, { state, adapter, focusKey: listFocus, onOpen: open, onNew: () => {
|
|
1506
|
+
setListFocus("@new");
|
|
1507
|
+
setView("new");
|
|
1508
|
+
}, onClose: adapter.onClose ? close : void 0, components, labels: copy }) : null,
|
|
1438
1509
|
view === "new" ? /* @__PURE__ */ jsx6(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy }) : null,
|
|
1439
|
-
view === "chat" ? /* @__PURE__ */ jsx6(Chat, { state, adapter, onBack: () =>
|
|
1510
|
+
view === "chat" ? /* @__PURE__ */ jsx6(Chat, { state, adapter, onBack: () => {
|
|
1511
|
+
setListFocus(state.attached?.key ?? listFocus);
|
|
1512
|
+
setView("list");
|
|
1513
|
+
}, onNew: () => {
|
|
1514
|
+
setListFocus("@new");
|
|
1515
|
+
setView("new");
|
|
1516
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy }) : null,
|
|
1440
1517
|
opening ? /* @__PURE__ */ jsxs5("div", { class: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
1441
1518
|
/* @__PURE__ */ jsx6(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
1442
1519
|
/* @__PURE__ */ jsxs5("span", { children: [
|
package/composer.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { MessengerLabels, SupercodeUiState, UiAdapter } from './index.js';
|
|
1
|
+
export type { MessengerLabels, PendingMessageModel, SupercodeUiState, UiAdapter } from './index.js';
|
|
2
2
|
export { Composer, ContinuationBar } from './index.js';
|
package/composer.mjs
CHANGED
|
@@ -102,10 +102,11 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
102
102
|
/* @__PURE__ */ jsx("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
|
|
103
103
|
] });
|
|
104
104
|
}
|
|
105
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, onPending }) {
|
|
105
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
106
106
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
|
|
107
107
|
const [draft, setDraft] = useState(remembered.draft);
|
|
108
108
|
const [queue, setQueue] = useState(remembered.queue);
|
|
109
|
+
const [dispatching, setDispatching] = useState(false);
|
|
109
110
|
const textarea = useRef(null);
|
|
110
111
|
const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
|
|
111
112
|
const updateQueue = (update) => setQueue((items) => {
|
|
@@ -113,15 +114,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
113
114
|
remember(draft, next);
|
|
114
115
|
return next;
|
|
115
116
|
});
|
|
117
|
+
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
118
|
+
const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
|
|
116
119
|
useEffect(() => {
|
|
117
|
-
if (!
|
|
120
|
+
if (!queueBlocked && state.canSend && queue.length) {
|
|
118
121
|
const [next, ...rest] = queue;
|
|
122
|
+
setDispatching(true);
|
|
119
123
|
setQueue(rest);
|
|
120
124
|
remember(draft, rest);
|
|
121
125
|
onPending?.(next);
|
|
122
126
|
adapter.onIntent({ action: "send", text: next });
|
|
123
127
|
}
|
|
124
|
-
}, [adapter, draft, memoryKey, onPending, queue,
|
|
128
|
+
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (pendingStatus !== null || state.busy) setDispatching(false);
|
|
131
|
+
}, [pendingStatus, state.busy]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
textarea.current?.focus({ preventScroll: true });
|
|
134
|
+
}, [memoryKey]);
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!restoreDraft) return;
|
|
137
|
+
setDraft(restoreDraft.text);
|
|
138
|
+
remember(restoreDraft.text, queue);
|
|
139
|
+
textarea.current?.focus({ preventScroll: true });
|
|
140
|
+
onDraftRestored?.(restoreDraft.id);
|
|
141
|
+
}, [onDraftRestored, restoreDraft?.id]);
|
|
125
142
|
useEffect(() => {
|
|
126
143
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
127
144
|
return () => clearTimeout(timer);
|
|
@@ -129,13 +146,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
129
146
|
const send = () => {
|
|
130
147
|
const text = draft.trim();
|
|
131
148
|
if (!text) return;
|
|
132
|
-
if (
|
|
149
|
+
if (queuesNewMessage) updateQueue((items) => [...items, text]);
|
|
133
150
|
else if (state.canSend) {
|
|
151
|
+
if (onPending) setDispatching(true);
|
|
134
152
|
onPending?.(text);
|
|
135
153
|
adapter.onIntent({ action: "send", text });
|
|
136
154
|
} else return;
|
|
137
155
|
setDraft("");
|
|
138
|
-
remember("",
|
|
156
|
+
remember("", queuesNewMessage ? [...queue, text] : queue);
|
|
139
157
|
};
|
|
140
158
|
return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
|
|
141
159
|
queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
|
|
@@ -149,7 +167,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
149
167
|
] }, `${index}:${item}`))
|
|
150
168
|
] }) : null,
|
|
151
169
|
/* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
|
|
152
|
-
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" :
|
|
170
|
+
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" : pendingStatus === "failed" ? "Retry or edit the unsent message\u2026" : pendingStatus === "editing" ? "Edit and resend\u2026" : queueBlocked ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
|
|
153
171
|
const value = event.currentTarget.value;
|
|
154
172
|
setDraft(value);
|
|
155
173
|
remember(value, queue);
|
|
@@ -161,7 +179,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
161
179
|
} }),
|
|
162
180
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
163
181
|
state.busy ? /* @__PURE__ */ jsx("button", { class: "scui-stop", type: "button", "aria-label": "Stop agent", disabled: !state.canInterrupt, onClick: () => adapter.onIntent({ action: "interrupt" }), children: "\u25A0" }) : null,
|
|
164
|
-
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label":
|
|
182
|
+
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label": queuesNewMessage ? "Queue message" : "Send message", disabled: !draft.trim() || !queuesNewMessage && !state.canSend, onClick: send, children: queuesNewMessage ? "+" : "\u2191" })
|
|
165
183
|
] })
|
|
166
184
|
] })
|
|
167
185
|
] });
|
package/conversation.d.ts
CHANGED
package/conversation.mjs
CHANGED
|
@@ -702,6 +702,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
702
702
|
const After = slots.afterConversation;
|
|
703
703
|
const Empty = slots.emptyConversation;
|
|
704
704
|
const remember = (value) => boundedSet(conversationMemory, memoryKey, value);
|
|
705
|
+
const pendingMessage = typeof pending === "string" ? { text: pending, status: "sending" } : pending;
|
|
705
706
|
const pin = () => {
|
|
706
707
|
if (!scroller.current) return;
|
|
707
708
|
scroller.current.scrollTop = scroller.current.scrollHeight;
|
|
@@ -723,7 +724,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
723
724
|
if (remembered.top !== null && !remembered.atBottom) element.scrollTop = remembered.top;
|
|
724
725
|
else pin();
|
|
725
726
|
} else if (atBottom) pin();
|
|
726
|
-
}, [memoryKey, state.transcript.
|
|
727
|
+
}, [memoryKey, state.transcript, state.busy, state.operation, pendingMessage?.text, pendingMessage?.status]);
|
|
727
728
|
return /* @__PURE__ */ jsxs("div", { class: "scui-conversation-wrap", children: [
|
|
728
729
|
/* @__PURE__ */ jsx2("div", { class: "scui-conversation", ref: scroller, tabIndex: 0, "aria-label": "Conversation", onScroll: (event) => {
|
|
729
730
|
const element = event.currentTarget;
|
|
@@ -743,9 +744,15 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
743
744
|
!blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx2(LoadingStatus, { state }) : null,
|
|
744
745
|
!blocks.length && state.startup === "ready" ? Empty ? /* @__PURE__ */ jsx2(Empty, { state, adapter, value: null }) : /* @__PURE__ */ jsx2("div", { class: "scui-empty", children: state.error ?? (state.harness ? `${harnessDisplayName(state.harness)} is listening. Say something.` : "No transcript yet.") }) : null,
|
|
745
746
|
blocks.map((block) => block.kind === "activity" ? /* @__PURE__ */ jsx2(Group, { value: block.entries, entries: block.entries, state, adapter }, block.id) : /* @__PURE__ */ jsx2(Entry, { value: block.entry, entry: block.entry, state, adapter }, block.id)),
|
|
746
|
-
|
|
747
|
-
/* @__PURE__ */ jsx2(Markdown, { value:
|
|
748
|
-
/* @__PURE__ */
|
|
747
|
+
pendingMessage ? /* @__PURE__ */ jsxs("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, children: [
|
|
748
|
+
/* @__PURE__ */ jsx2(Markdown, { value: pendingMessage.text }),
|
|
749
|
+
/* @__PURE__ */ jsxs("footer", { children: [
|
|
750
|
+
/* @__PURE__ */ jsx2("small", { children: pendingMessage.status === "failed" ? "Not sent" : "Sending\u2026" }),
|
|
751
|
+
pendingMessage.status === "failed" ? /* @__PURE__ */ jsxs("span", { children: [
|
|
752
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: pendingMessage.onRetry, children: "Retry" }),
|
|
753
|
+
/* @__PURE__ */ jsx2("button", { type: "button", onClick: pendingMessage.onEdit, children: "Edit" })
|
|
754
|
+
] }) : null
|
|
755
|
+
] })
|
|
749
756
|
] }) : null,
|
|
750
757
|
state.busy ? /* @__PURE__ */ jsxs("div", { class: "scui-working", role: "status", children: [
|
|
751
758
|
/* @__PURE__ */ jsx2("span", { "aria-hidden": "true", children: "\u2726" }),
|