@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 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 (!state.busy && state.canSend && queue.length) {
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, state.busy, state.canSend]);
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 (state.busy) updateQueue((items) => [...items, text]);
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("", state.busy ? [...queue, text] : queue);
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" : state.busy ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
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": state.busy ? "Queue message" : "Send message", disabled: !draft.trim() || !state.busy && !state.canSend, onClick: send, children: state.busy ? "+" : "\u2191" })
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, state.busy, state.operation]);
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
- pending ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", children: [
1113
- /* @__PURE__ */ jsx3(Markdown, { value: pending }),
1114
- /* @__PURE__ */ jsx3("small", { children: state.error && !state.busy ? "Not sent" : "Sending\u2026" })
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 useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "preact/hooks";
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
- return /* @__PURE__ */ jsxs4("section", { class: "scui-list", children: [
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, setPending] = useState4(null);
1325
- const acknowledged = useRef3(/* @__PURE__ */ new Set());
1326
- useEffect4(() => {
1327
- setPending(null);
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
- useEffect4(() => {
1330
- if (pending && state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.trim())) setPending(null);
1331
- }, [pending, state.transcript]);
1332
- useEffect4(() => {
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: setPending }, memoryKey) : null
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
- useEffect4(() => {
1431
+ useEffect5(() => {
1366
1432
  if (!startable.some((item) => item.id === harness)) setHarness(startable[0]?.id ?? "");
1367
1433
  }, [harness, startableKey]);
1368
- useEffect4(() => {
1434
+ useEffect5(() => {
1369
1435
  if (starting && (state.busy || state.transcript.length)) onStarted();
1370
1436
  }, [onStarted, starting, state.busy, state.transcript.length]);
1371
- useEffect4(() => {
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();
@@ -1417,7 +1483,8 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
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
- useEffect4(() => {
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: () => setView("new"), onClose: adapter.onClose ? close : void 0, components, labels: copy }) : null,
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: () => setView("list"), onNew: () => setView("new"), onClose: adapter.onClose ? close : void 0, components, slots, labels: copy }) : null,
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 (!state.busy && state.canSend && queue.length) {
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, state.busy, state.canSend]);
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 (state.busy) updateQueue((items) => [...items, text]);
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("", state.busy ? [...queue, text] : queue);
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" : state.busy ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
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": state.busy ? "Queue message" : "Send message", disabled: !draft.trim() || !state.busy && !state.canSend, onClick: send, children: state.busy ? "+" : "\u2191" })
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
@@ -2,6 +2,7 @@ export type {
2
2
  ActivityGroupProps,
3
3
  MessengerComponents,
4
4
  MessengerSlots,
5
+ PendingMessageModel,
5
6
  SessionSemanticsModel,
6
7
  SupercodeUiState,
7
8
  TaskPlanProps,
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, state.busy, state.operation]);
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
- pending ? /* @__PURE__ */ jsxs("article", { class: "scui-message scui-pending", "data-role": "user", children: [
747
- /* @__PURE__ */ jsx2(Markdown, { value: pending }),
748
- /* @__PURE__ */ jsx2("small", { children: state.error && !state.busy ? "Not sent" : "Sending\u2026" })
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" }),