@volter-ai-dev/supercode-ui 0.1.12 → 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 +115 -38
- 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 +115 -38
- package/index.d.ts +10 -3
- package/messenger.mjs +115 -38
- package/package.json +1 -1
- package/sessions.mjs +11 -5
- package/styles.css +3 -0
package/messenger.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/messenger.jsx
|
|
2
|
-
import { useEffect as
|
|
2
|
+
import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState4 } from "preact/hooks";
|
|
3
3
|
|
|
4
4
|
// core.mjs
|
|
5
5
|
var HARNESS_NAMES = Object.freeze({
|
|
@@ -590,10 +590,11 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
590
590
|
/* @__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 })
|
|
591
591
|
] });
|
|
592
592
|
}
|
|
593
|
-
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, onPending }) {
|
|
593
|
+
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
594
594
|
const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
|
|
595
595
|
const [draft, setDraft] = useState(remembered.draft);
|
|
596
596
|
const [queue, setQueue] = useState(remembered.queue);
|
|
597
|
+
const [dispatching, setDispatching] = useState(false);
|
|
597
598
|
const textarea = useRef(null);
|
|
598
599
|
const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
|
|
599
600
|
const updateQueue = (update) => setQueue((items) => {
|
|
@@ -601,15 +602,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
601
602
|
remember(draft, next);
|
|
602
603
|
return next;
|
|
603
604
|
});
|
|
605
|
+
const queueBlocked = state.busy || pendingStatus !== null || dispatching;
|
|
606
|
+
const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
|
|
604
607
|
useEffect(() => {
|
|
605
|
-
if (!
|
|
608
|
+
if (!queueBlocked && state.canSend && queue.length) {
|
|
606
609
|
const [next, ...rest] = queue;
|
|
610
|
+
setDispatching(true);
|
|
607
611
|
setQueue(rest);
|
|
608
612
|
remember(draft, rest);
|
|
609
613
|
onPending?.(next);
|
|
610
614
|
adapter.onIntent({ action: "send", text: next });
|
|
611
615
|
}
|
|
612
|
-
}, [adapter, draft, memoryKey, onPending, queue,
|
|
616
|
+
}, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
if (pendingStatus !== null || state.busy) setDispatching(false);
|
|
619
|
+
}, [pendingStatus, state.busy]);
|
|
620
|
+
useEffect(() => {
|
|
621
|
+
textarea.current?.focus({ preventScroll: true });
|
|
622
|
+
}, [memoryKey]);
|
|
623
|
+
useEffect(() => {
|
|
624
|
+
if (!restoreDraft) return;
|
|
625
|
+
setDraft(restoreDraft.text);
|
|
626
|
+
remember(restoreDraft.text, queue);
|
|
627
|
+
textarea.current?.focus({ preventScroll: true });
|
|
628
|
+
onDraftRestored?.(restoreDraft.id);
|
|
629
|
+
}, [onDraftRestored, restoreDraft?.id]);
|
|
613
630
|
useEffect(() => {
|
|
614
631
|
const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
|
|
615
632
|
return () => clearTimeout(timer);
|
|
@@ -617,13 +634,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
617
634
|
const send = () => {
|
|
618
635
|
const text = draft.trim();
|
|
619
636
|
if (!text) return;
|
|
620
|
-
if (
|
|
637
|
+
if (queuesNewMessage) updateQueue((items) => [...items, text]);
|
|
621
638
|
else if (state.canSend) {
|
|
639
|
+
if (onPending) setDispatching(true);
|
|
622
640
|
onPending?.(text);
|
|
623
641
|
adapter.onIntent({ action: "send", text });
|
|
624
642
|
} else return;
|
|
625
643
|
setDraft("");
|
|
626
|
-
remember("",
|
|
644
|
+
remember("", queuesNewMessage ? [...queue, text] : queue);
|
|
627
645
|
};
|
|
628
646
|
return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
|
|
629
647
|
queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
|
|
@@ -637,7 +655,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
637
655
|
] }, `${index}:${item}`))
|
|
638
656
|
] }) : null,
|
|
639
657
|
/* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
|
|
640
|
-
/* @__PURE__ */ jsx("textarea", { ref: textarea, rows: 1, "aria-label": `Message ${harnessDisplayName(state.harness) || "agent"}`, placeholder: state.startup !== "ready" ? "Connecting\u2026" :
|
|
658
|
+
/* @__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) => {
|
|
641
659
|
const value = event.currentTarget.value;
|
|
642
660
|
setDraft(value);
|
|
643
661
|
remember(value, queue);
|
|
@@ -649,7 +667,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
|
|
|
649
667
|
} }),
|
|
650
668
|
/* @__PURE__ */ jsxs("span", { children: [
|
|
651
669
|
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,
|
|
652
|
-
/* @__PURE__ */ jsx("button", { class: "scui-send", type: "button", "aria-label":
|
|
670
|
+
/* @__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" })
|
|
653
671
|
] })
|
|
654
672
|
] })
|
|
655
673
|
] });
|
|
@@ -1071,6 +1089,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1071
1089
|
const After = slots.afterConversation;
|
|
1072
1090
|
const Empty = slots.emptyConversation;
|
|
1073
1091
|
const remember = (value) => boundedSet(conversationMemory, memoryKey, value);
|
|
1092
|
+
const pendingMessage = typeof pending === "string" ? { text: pending, status: "sending" } : pending;
|
|
1074
1093
|
const pin = () => {
|
|
1075
1094
|
if (!scroller.current) return;
|
|
1076
1095
|
scroller.current.scrollTop = scroller.current.scrollHeight;
|
|
@@ -1092,7 +1111,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1092
1111
|
if (remembered.top !== null && !remembered.atBottom) element.scrollTop = remembered.top;
|
|
1093
1112
|
else pin();
|
|
1094
1113
|
} else if (atBottom) pin();
|
|
1095
|
-
}, [memoryKey, state.transcript, state.busy, state.operation]);
|
|
1114
|
+
}, [memoryKey, state.transcript, state.busy, state.operation, pendingMessage?.text, pendingMessage?.status]);
|
|
1096
1115
|
return /* @__PURE__ */ jsxs2("div", { class: "scui-conversation-wrap", children: [
|
|
1097
1116
|
/* @__PURE__ */ jsx3("div", { class: "scui-conversation", ref: scroller, tabIndex: 0, "aria-label": "Conversation", onScroll: (event) => {
|
|
1098
1117
|
const element = event.currentTarget;
|
|
@@ -1112,9 +1131,15 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
|
|
|
1112
1131
|
!blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx3(LoadingStatus, { state }) : null,
|
|
1113
1132
|
!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,
|
|
1114
1133
|
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)),
|
|
1115
|
-
|
|
1116
|
-
/* @__PURE__ */ jsx3(Markdown, { value:
|
|
1117
|
-
/* @__PURE__ */
|
|
1134
|
+
pendingMessage ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, children: [
|
|
1135
|
+
/* @__PURE__ */ jsx3(Markdown, { value: pendingMessage.text }),
|
|
1136
|
+
/* @__PURE__ */ jsxs2("footer", { children: [
|
|
1137
|
+
/* @__PURE__ */ jsx3("small", { children: pendingMessage.status === "failed" ? "Not sent" : "Sending\u2026" }),
|
|
1138
|
+
pendingMessage.status === "failed" ? /* @__PURE__ */ jsxs2("span", { children: [
|
|
1139
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onRetry, children: "Retry" }),
|
|
1140
|
+
/* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onEdit, children: "Edit" })
|
|
1141
|
+
] }) : null
|
|
1142
|
+
] })
|
|
1118
1143
|
] }) : null,
|
|
1119
1144
|
state.busy ? /* @__PURE__ */ jsxs2("div", { class: "scui-working", role: "status", children: [
|
|
1120
1145
|
/* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -1192,12 +1217,12 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
|
|
|
1192
1217
|
}
|
|
1193
1218
|
|
|
1194
1219
|
// src/sessions.jsx
|
|
1195
|
-
import { useState as useState3 } from "preact/hooks";
|
|
1220
|
+
import { useEffect as useEffect4, useRef as useRef3, useState as useState3 } from "preact/hooks";
|
|
1196
1221
|
import { jsx as jsx5, jsxs as jsxs4 } from "preact/jsx-runtime";
|
|
1197
1222
|
function SessionRow({ row, state, onOpen }) {
|
|
1198
1223
|
const activity = sessionActivity(state, row);
|
|
1199
1224
|
const preview = state.attention.find((item) => item.key === row.key)?.preview;
|
|
1200
|
-
return /* @__PURE__ */ jsxs4("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, type: "button", onClick: () => onOpen(row), children: [
|
|
1225
|
+
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: [
|
|
1201
1226
|
/* @__PURE__ */ jsx5(HarnessLogo, { id: row.harness, activity, size: 34 }),
|
|
1202
1227
|
/* @__PURE__ */ jsxs4("span", { class: "scui-session-copy", children: [
|
|
1203
1228
|
/* @__PURE__ */ jsxs4("span", { children: [
|
|
@@ -1212,11 +1237,17 @@ function SessionRow({ row, state, onOpen }) {
|
|
|
1212
1237
|
] })
|
|
1213
1238
|
] });
|
|
1214
1239
|
}
|
|
1215
|
-
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS }) {
|
|
1240
|
+
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null }) {
|
|
1216
1241
|
const [query, setQuery] = useState3("");
|
|
1242
|
+
const root = useRef3(null);
|
|
1217
1243
|
const rows = filterSessions(state.sessions, query);
|
|
1218
1244
|
const Row = components.SessionRow ?? SessionRow;
|
|
1219
|
-
|
|
1245
|
+
useEffect4(() => {
|
|
1246
|
+
if (!focusKey || !root.current) return;
|
|
1247
|
+
const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
|
|
1248
|
+
(target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
|
|
1249
|
+
}, [focusKey, rows.length]);
|
|
1250
|
+
return /* @__PURE__ */ jsxs4("section", { class: "scui-list", ref: root, children: [
|
|
1220
1251
|
/* @__PURE__ */ jsxs4("header", { class: "scui-head", children: [
|
|
1221
1252
|
/* @__PURE__ */ jsxs4("span", { class: "scui-head-copy", children: [
|
|
1222
1253
|
/* @__PURE__ */ jsx5("strong", { children: labels.chats }),
|
|
@@ -1225,7 +1256,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
1225
1256
|
" recent conversations"
|
|
1226
1257
|
] })
|
|
1227
1258
|
] }),
|
|
1228
|
-
/* @__PURE__ */ jsx5("button", { type: "button", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
1259
|
+
/* @__PURE__ */ jsx5("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
1229
1260
|
onClose ? /* @__PURE__ */ jsx5("button", { type: "button", "aria-label": "Close", onClick: onClose, children: "\xD7" }) : null
|
|
1230
1261
|
] }),
|
|
1231
1262
|
state.startup !== "ready" ? /* @__PURE__ */ jsx5(LoadingStatus, { state, compact: rows.length > 0 }) : null,
|
|
@@ -1244,6 +1275,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
1244
1275
|
|
|
1245
1276
|
// src/messenger.jsx
|
|
1246
1277
|
import { jsx as jsx6, jsxs as jsxs5 } from "preact/jsx-runtime";
|
|
1278
|
+
var pendingMessageMemory = /* @__PURE__ */ new Map();
|
|
1247
1279
|
function Receipt({ state, adapter }) {
|
|
1248
1280
|
const receipt = state.reductionReceipt;
|
|
1249
1281
|
if (receipt) return /* @__PURE__ */ jsx6("div", { class: "scui-receipt", children: /* @__PURE__ */ jsxs5("span", { children: [
|
|
@@ -1281,14 +1313,14 @@ function Receipt({ state, adapter }) {
|
|
|
1281
1313
|
] });
|
|
1282
1314
|
return null;
|
|
1283
1315
|
}
|
|
1284
|
-
function ChatHeader({ state, adapter, onBack, onNew, onClose }) {
|
|
1316
|
+
function ChatHeader({ state, adapter, pendingStatus, onBack, onNew, onClose }) {
|
|
1285
1317
|
const harness = state.attached?.harness ?? state.harness;
|
|
1286
1318
|
const targets = state.harnesses.filter((item) => item.startable);
|
|
1287
1319
|
const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
|
|
1288
|
-
const status = state.needsInput ? "Needs input" : state.busy ? "Working" : state.mode === "mirror" ? "Read-only" : "Ready";
|
|
1320
|
+
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";
|
|
1289
1321
|
const menu = state.canDetach || state.canOpenTerminal || state.canBranch || state.canAttach || state.canExport || state.canReduce;
|
|
1290
1322
|
return /* @__PURE__ */ jsxs5("header", { class: "scui-head scui-chat-head", children: [
|
|
1291
|
-
/* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Back to chats", onClick: onBack, children: "\u2039" }),
|
|
1323
|
+
/* @__PURE__ */ jsx6("button", { type: "button", autofocus: state.mode === "mirror" && !state.canSend, "aria-label": "Back to chats", onClick: onBack, children: "\u2039" }),
|
|
1292
1324
|
/* @__PURE__ */ jsx6(HarnessLogo, { id: harness, size: 28 }),
|
|
1293
1325
|
/* @__PURE__ */ jsxs5("span", { class: "scui-head-copy", children: [
|
|
1294
1326
|
/* @__PURE__ */ jsx6("strong", { children: title }),
|
|
@@ -1318,15 +1350,48 @@ function ChatHeader({ state, adapter, onBack, onNew, onClose }) {
|
|
|
1318
1350
|
function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
|
|
1319
1351
|
const Header = slots.header;
|
|
1320
1352
|
const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
|
|
1321
|
-
const [pending,
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1353
|
+
const [pending, setPendingState] = useState4(() => pendingMessageMemory.get(memoryKey) ?? null);
|
|
1354
|
+
const [restoreDraft, setRestoreDraft] = useState4(null);
|
|
1355
|
+
const restoreSequence = useRef4(0);
|
|
1356
|
+
const acknowledged = useRef4(/* @__PURE__ */ new Set());
|
|
1357
|
+
const setPending = (update) => setPendingState((current) => {
|
|
1358
|
+
const next = typeof update === "function" ? update(current) : update;
|
|
1359
|
+
if (next) boundedSet(pendingMessageMemory, memoryKey, next);
|
|
1360
|
+
else pendingMessageMemory.delete(memoryKey);
|
|
1361
|
+
return next;
|
|
1362
|
+
});
|
|
1363
|
+
useEffect5(() => {
|
|
1364
|
+
setPendingState(pendingMessageMemory.get(memoryKey) ?? null);
|
|
1365
|
+
setRestoreDraft(null);
|
|
1325
1366
|
}, [memoryKey]);
|
|
1326
|
-
|
|
1327
|
-
if (pending
|
|
1328
|
-
|
|
1329
|
-
|
|
1367
|
+
useEffect5(() => {
|
|
1368
|
+
if (!pending) return;
|
|
1369
|
+
if (state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.text.trim() && !pending.baselineIds.includes(entry.id))) {
|
|
1370
|
+
setPending(null);
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
if (pending.status === "sending" && state.busy && !pending.seenBusy) setPending({ ...pending, seenBusy: true });
|
|
1374
|
+
else if (pending.status === "sending" && state.error && !state.busy && !state.operation && (pending.seenBusy || state.error !== pending.initialError)) setPending({ ...pending, status: "failed" });
|
|
1375
|
+
}, [pending, state.busy, state.error, state.operation, state.transcript]);
|
|
1376
|
+
const beginPending = (text) => setPending({
|
|
1377
|
+
text,
|
|
1378
|
+
status: "sending",
|
|
1379
|
+
initialError: state.error,
|
|
1380
|
+
seenBusy: state.busy,
|
|
1381
|
+
baselineIds: state.transcript.filter((entry) => entry.role === "user" && entry.text.trim() === text.trim()).map((entry) => entry.id)
|
|
1382
|
+
});
|
|
1383
|
+
const retryPending = () => {
|
|
1384
|
+
if (!pending) return;
|
|
1385
|
+
adapter.onIntent({ action: "send", text: pending.text });
|
|
1386
|
+
setPending({ ...pending, status: "sending", initialError: state.error, seenBusy: state.busy });
|
|
1387
|
+
};
|
|
1388
|
+
const editPending = () => {
|
|
1389
|
+
if (!pending) return;
|
|
1390
|
+
restoreSequence.current += 1;
|
|
1391
|
+
setRestoreDraft({ id: restoreSequence.current, text: pending.text });
|
|
1392
|
+
setPending({ ...pending, status: "editing" });
|
|
1393
|
+
};
|
|
1394
|
+
useEffect5(() => {
|
|
1330
1395
|
const key = state.attached?.key;
|
|
1331
1396
|
if (!key || !state.attention.some((item) => item.key === key)) {
|
|
1332
1397
|
if (key) acknowledged.current.delete(key);
|
|
@@ -1337,8 +1402,9 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1337
1402
|
adapter.onIntent({ action: "ack", key });
|
|
1338
1403
|
}
|
|
1339
1404
|
}, [adapter, state.attached?.key, state.attention]);
|
|
1405
|
+
const pendingMessage = pending && pending.status !== "editing" ? { text: pending.text, status: pending.status, onRetry: retryPending, onEdit: editPending } : null;
|
|
1340
1406
|
return /* @__PURE__ */ jsxs5("section", { class: "scui-chat", children: [
|
|
1341
|
-
Header ? /* @__PURE__ */ jsx6(Header, { state, adapter, value: null }) : /* @__PURE__ */ jsx6(ChatHeader, { state, adapter, onBack, onNew, onClose }),
|
|
1407
|
+
Header ? /* @__PURE__ */ jsx6(Header, { state, adapter, value: null }) : /* @__PURE__ */ jsx6(ChatHeader, { state, adapter, pendingStatus: pending?.status ?? null, onBack, onNew, onClose }),
|
|
1342
1408
|
operationLabel(state.operation) ? /* @__PURE__ */ jsxs5("div", { class: "scui-operation", role: "status", children: [
|
|
1343
1409
|
/* @__PURE__ */ jsx6("i", {}),
|
|
1344
1410
|
operationLabel(state.operation)
|
|
@@ -1348,9 +1414,9 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
1348
1414
|
state.recoverable ? /* @__PURE__ */ jsx6("button", { type: "button", onClick: () => adapter.onIntent({ action: "refresh" }), children: "Retry" }) : null
|
|
1349
1415
|
] }) : null,
|
|
1350
1416
|
/* @__PURE__ */ jsx6(Receipt, { state, adapter }),
|
|
1351
|
-
/* @__PURE__ */ jsx6(Conversation, { state, adapter, components, slots, memoryKey, pending }, memoryKey),
|
|
1417
|
+
/* @__PURE__ */ jsx6(Conversation, { state, adapter, components, slots, memoryKey, pending: pendingMessage }, memoryKey),
|
|
1352
1418
|
/* @__PURE__ */ jsx6(ContinuationBar, { state, adapter, labels }),
|
|
1353
|
-
state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx6(Composer, { state, adapter, labels, memoryKey, onPending:
|
|
1419
|
+
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
|
|
1354
1420
|
] });
|
|
1355
1421
|
}
|
|
1356
1422
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
@@ -1359,13 +1425,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
|
1359
1425
|
const [harness, setHarness] = useState4(startable[0]?.id ?? "");
|
|
1360
1426
|
const [draft, setDraft] = useState4("");
|
|
1361
1427
|
const [starting, setStarting] = useState4(false);
|
|
1362
|
-
|
|
1428
|
+
useEffect5(() => {
|
|
1363
1429
|
if (!startable.some((item) => item.id === harness)) setHarness(startable[0]?.id ?? "");
|
|
1364
1430
|
}, [harness, startableKey]);
|
|
1365
|
-
|
|
1431
|
+
useEffect5(() => {
|
|
1366
1432
|
if (starting && (state.busy || state.transcript.length)) onStarted();
|
|
1367
1433
|
}, [onStarted, starting, state.busy, state.transcript.length]);
|
|
1368
|
-
|
|
1434
|
+
useEffect5(() => {
|
|
1369
1435
|
if (starting && state.error && !state.busy && !state.operation) setStarting(false);
|
|
1370
1436
|
}, [starting, state.busy, state.error, state.operation]);
|
|
1371
1437
|
const send = () => {
|
|
@@ -1398,7 +1464,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels }) {
|
|
|
1398
1464
|
] }, item.id)) })
|
|
1399
1465
|
] }),
|
|
1400
1466
|
/* @__PURE__ */ jsxs5("div", { class: "scui-envelope", children: [
|
|
1401
|
-
/* @__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) => {
|
|
1467
|
+
/* @__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) => {
|
|
1402
1468
|
if (isSendKey(event)) {
|
|
1403
1469
|
event.preventDefault();
|
|
1404
1470
|
send();
|
|
@@ -1414,7 +1480,8 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
1414
1480
|
const copy = { ...DEFAULT_LABELS, ...labels };
|
|
1415
1481
|
const [view, setView] = useState4(initialView ?? (state.attention.length ? "list" : state.attached || state.transcript.length ? "chat" : "list"));
|
|
1416
1482
|
const [opening, setOpening] = useState4(null);
|
|
1417
|
-
|
|
1483
|
+
const [listFocus, setListFocus] = useState4(null);
|
|
1484
|
+
useEffect5(() => {
|
|
1418
1485
|
if (!opening) return;
|
|
1419
1486
|
if (state.attached?.key === opening.key) {
|
|
1420
1487
|
setOpening(null);
|
|
@@ -1424,6 +1491,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
1424
1491
|
}
|
|
1425
1492
|
}, [opening, state.attachError?.key, state.attached?.key, state.error, state.operation]);
|
|
1426
1493
|
const open = (row) => {
|
|
1494
|
+
setListFocus(row.key);
|
|
1427
1495
|
setOpening(row);
|
|
1428
1496
|
adapter.onIntent({ action: "ack", key: row.key });
|
|
1429
1497
|
adapter.onIntent({ action: "attach", key: row.key });
|
|
@@ -1431,9 +1499,18 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
1431
1499
|
const close = () => adapter.onClose?.();
|
|
1432
1500
|
const Footer = slots.footer;
|
|
1433
1501
|
return /* @__PURE__ */ jsxs5("main", { class: `scui-root ${className}`, "data-view": view, "data-mode": state.mode, "aria-label": "Supercode messenger", children: [
|
|
1434
|
-
view === "list" ? /* @__PURE__ */ jsx6(SessionList, { state, adapter, onOpen: open, onNew: () =>
|
|
1502
|
+
view === "list" ? /* @__PURE__ */ jsx6(SessionList, { state, adapter, focusKey: listFocus, onOpen: open, onNew: () => {
|
|
1503
|
+
setListFocus("@new");
|
|
1504
|
+
setView("new");
|
|
1505
|
+
}, onClose: adapter.onClose ? close : void 0, components, labels: copy }) : null,
|
|
1435
1506
|
view === "new" ? /* @__PURE__ */ jsx6(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy }) : null,
|
|
1436
|
-
view === "chat" ? /* @__PURE__ */ jsx6(Chat, { state, adapter, onBack: () =>
|
|
1507
|
+
view === "chat" ? /* @__PURE__ */ jsx6(Chat, { state, adapter, onBack: () => {
|
|
1508
|
+
setListFocus(state.attached?.key ?? listFocus);
|
|
1509
|
+
setView("list");
|
|
1510
|
+
}, onNew: () => {
|
|
1511
|
+
setListFocus("@new");
|
|
1512
|
+
setView("new");
|
|
1513
|
+
}, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy }) : null,
|
|
1437
1514
|
opening ? /* @__PURE__ */ jsxs5("div", { class: "scui-opening", role: "status", "aria-busy": "true", children: [
|
|
1438
1515
|
/* @__PURE__ */ jsx6(HarnessLogo, { id: opening.harness, size: 34 }),
|
|
1439
1516
|
/* @__PURE__ */ jsxs5("span", { children: [
|
package/package.json
CHANGED
package/sessions.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/sessions.jsx
|
|
2
|
-
import { useState as useState2 } from "preact/hooks";
|
|
2
|
+
import { useEffect as useEffect3, useRef as useRef2, useState as useState2 } from "preact/hooks";
|
|
3
3
|
|
|
4
4
|
// core.mjs
|
|
5
5
|
var HARNESS_NAMES = Object.freeze({
|
|
@@ -179,7 +179,7 @@ import { jsx as jsx4, jsxs as jsxs3 } from "preact/jsx-runtime";
|
|
|
179
179
|
function SessionRow({ row, state, onOpen }) {
|
|
180
180
|
const activity = sessionActivity(state, row);
|
|
181
181
|
const preview = state.attention.find((item) => item.key === row.key)?.preview;
|
|
182
|
-
return /* @__PURE__ */ jsxs3("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, type: "button", onClick: () => onOpen(row), children: [
|
|
182
|
+
return /* @__PURE__ */ jsxs3("button", { class: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", onClick: () => onOpen(row), children: [
|
|
183
183
|
/* @__PURE__ */ jsx4(HarnessLogo, { id: row.harness, activity, size: 34 }),
|
|
184
184
|
/* @__PURE__ */ jsxs3("span", { class: "scui-session-copy", children: [
|
|
185
185
|
/* @__PURE__ */ jsxs3("span", { children: [
|
|
@@ -194,11 +194,17 @@ function SessionRow({ row, state, onOpen }) {
|
|
|
194
194
|
] })
|
|
195
195
|
] });
|
|
196
196
|
}
|
|
197
|
-
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS }) {
|
|
197
|
+
function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null }) {
|
|
198
198
|
const [query, setQuery] = useState2("");
|
|
199
|
+
const root = useRef2(null);
|
|
199
200
|
const rows = filterSessions(state.sessions, query);
|
|
200
201
|
const Row = components.SessionRow ?? SessionRow;
|
|
201
|
-
|
|
202
|
+
useEffect3(() => {
|
|
203
|
+
if (!focusKey || !root.current) return;
|
|
204
|
+
const target = focusKey === "@new" ? root.current.querySelector('[data-list-focus="new"]') : [...root.current.querySelectorAll("[data-session-key]")].find((element) => element.dataset.sessionKey === focusKey);
|
|
205
|
+
(target ?? root.current.querySelector("input,button"))?.focus({ preventScroll: true });
|
|
206
|
+
}, [focusKey, rows.length]);
|
|
207
|
+
return /* @__PURE__ */ jsxs3("section", { class: "scui-list", ref: root, children: [
|
|
202
208
|
/* @__PURE__ */ jsxs3("header", { class: "scui-head", children: [
|
|
203
209
|
/* @__PURE__ */ jsxs3("span", { class: "scui-head-copy", children: [
|
|
204
210
|
/* @__PURE__ */ jsx4("strong", { children: labels.chats }),
|
|
@@ -207,7 +213,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
|
|
|
207
213
|
" recent conversations"
|
|
208
214
|
] })
|
|
209
215
|
] }),
|
|
210
|
-
/* @__PURE__ */ jsx4("button", { type: "button", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
216
|
+
/* @__PURE__ */ jsx4("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: "\uFF0B" }),
|
|
211
217
|
onClose ? /* @__PURE__ */ jsx4("button", { type: "button", "aria-label": "Close", onClick: onClose, children: "\xD7" }) : null
|
|
212
218
|
] }),
|
|
213
219
|
state.startup !== "ready" ? /* @__PURE__ */ jsx4(LoadingStatus, { state, compact: rows.length > 0 }) : null,
|
package/styles.css
CHANGED
|
@@ -115,6 +115,9 @@
|
|
|
115
115
|
.scui-latest { position:absolute; z-index:5; right:12px; bottom:8px; padding:5px 9px; border:1px solid var(--scui-border-strong); border-radius:999px; background:var(--scui-bg-raised); box-shadow:0 2px 8px rgba(0,0,0,.2); cursor:pointer }
|
|
116
116
|
.scui-message { position:relative; overflow-wrap:anywhere }
|
|
117
117
|
.scui-message[data-role="user"] { align-self:flex-end; max-width:90%; padding:8px 9px; border:1px solid var(--scui-border); border-radius:10px; background:var(--scui-fill) }
|
|
118
|
+
.scui-pending[data-status="failed"] { border-color:color-mix(in srgb,var(--scui-danger) 45%,var(--scui-border)); background:color-mix(in srgb,var(--scui-danger) 7%,var(--scui-fill)); opacity:1 }
|
|
119
|
+
.scui-pending > footer { display:flex; align-items:center; gap:8px; margin-top:5px }.scui-pending > footer small { color:var(--scui-muted) }.scui-pending[data-status="failed"] > footer small { color:var(--scui-danger) }
|
|
120
|
+
.scui-pending > footer > span { display:flex; gap:4px; margin-left:auto }.scui-pending > footer button { padding:2px 6px; border:1px solid var(--scui-border-strong); border-radius:5px; background:transparent; color:var(--scui-fg); cursor:pointer; font:inherit; font-size:9.5px }
|
|
118
121
|
.scui-pending { opacity:.7 }.scui-pending > small { display:block; margin-top:3px; color:var(--scui-muted); font-size:9.5px }
|
|
119
122
|
.scui-markdown { font-size:12.5px; line-height:1.55 }.scui-markdown > :first-child { margin-top:0 }.scui-markdown > :last-child { margin-bottom:0 }
|
|
120
123
|
.scui-markdown p { margin:0 0 8px }.scui-markdown h1,.scui-markdown h2,.scui-markdown h3 { margin:12px 0 5px; line-height:1.25 }.scui-markdown h1 { font-size:17px }.scui-markdown h2 { font-size:15px }.scui-markdown h3 { font-size:13px }
|