@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/embed.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { render } from "preact";
3
3
 
4
4
  // src/messenger.jsx
5
- import { useEffect as useEffect4, useRef as useRef3, useState as useState4 } from "preact/hooks";
5
+ import { useEffect as useEffect5, useMemo as useMemo2, useRef as useRef4, useState as useState4 } from "preact/hooks";
6
6
 
7
7
  // core.mjs
8
8
  var HARNESS_NAMES = Object.freeze({
@@ -593,10 +593,11 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
593
593
  /* @__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 })
594
594
  ] });
595
595
  }
596
- function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, onPending }) {
596
+ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
597
597
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, queue: [] };
598
598
  const [draft, setDraft] = useState(remembered.draft);
599
599
  const [queue, setQueue] = useState(remembered.queue);
600
+ const [dispatching, setDispatching] = useState(false);
600
601
  const textarea = useRef(null);
601
602
  const remember = (nextDraft, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, queue: nextQueue });
602
603
  const updateQueue = (update) => setQueue((items) => {
@@ -604,15 +605,31 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
604
605
  remember(draft, next);
605
606
  return next;
606
607
  });
608
+ const queueBlocked = state.busy || pendingStatus !== null || dispatching;
609
+ const queuesNewMessage = state.busy || pendingStatus === "sending" || pendingStatus === "failed" || dispatching;
607
610
  useEffect(() => {
608
- if (!state.busy && state.canSend && queue.length) {
611
+ if (!queueBlocked && state.canSend && queue.length) {
609
612
  const [next, ...rest] = queue;
613
+ setDispatching(true);
610
614
  setQueue(rest);
611
615
  remember(draft, rest);
612
616
  onPending?.(next);
613
617
  adapter.onIntent({ action: "send", text: next });
614
618
  }
615
- }, [adapter, draft, memoryKey, onPending, queue, state.busy, state.canSend]);
619
+ }, [adapter, draft, memoryKey, onPending, queue, queueBlocked, state.canSend]);
620
+ useEffect(() => {
621
+ if (pendingStatus !== null || state.busy) setDispatching(false);
622
+ }, [pendingStatus, state.busy]);
623
+ useEffect(() => {
624
+ textarea.current?.focus({ preventScroll: true });
625
+ }, [memoryKey]);
626
+ useEffect(() => {
627
+ if (!restoreDraft) return;
628
+ setDraft(restoreDraft.text);
629
+ remember(restoreDraft.text, queue);
630
+ textarea.current?.focus({ preventScroll: true });
631
+ onDraftRestored?.(restoreDraft.id);
632
+ }, [onDraftRestored, restoreDraft?.id]);
616
633
  useEffect(() => {
617
634
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
618
635
  return () => clearTimeout(timer);
@@ -620,13 +637,14 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
620
637
  const send = () => {
621
638
  const text = draft.trim();
622
639
  if (!text) return;
623
- if (state.busy) updateQueue((items) => [...items, text]);
640
+ if (queuesNewMessage) updateQueue((items) => [...items, text]);
624
641
  else if (state.canSend) {
642
+ if (onPending) setDispatching(true);
625
643
  onPending?.(text);
626
644
  adapter.onIntent({ action: "send", text });
627
645
  } else return;
628
646
  setDraft("");
629
- remember("", state.busy ? [...queue, text] : queue);
647
+ remember("", queuesNewMessage ? [...queue, text] : queue);
630
648
  };
631
649
  return /* @__PURE__ */ jsxs("div", { class: "scui-compose", children: [
632
650
  queue.length ? /* @__PURE__ */ jsxs("div", { class: "scui-queue", children: [
@@ -640,7 +658,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
640
658
  ] }, `${index}:${item}`))
641
659
  ] }) : null,
642
660
  /* @__PURE__ */ jsxs("div", { class: "scui-envelope", children: [
643
- /* @__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) => {
661
+ /* @__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) => {
644
662
  const value = event.currentTarget.value;
645
663
  setDraft(value);
646
664
  remember(value, queue);
@@ -652,7 +670,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
652
670
  } }),
653
671
  /* @__PURE__ */ jsxs("span", { children: [
654
672
  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,
655
- /* @__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" })
673
+ /* @__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" })
656
674
  ] })
657
675
  ] })
658
676
  ] });
@@ -1074,6 +1092,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1074
1092
  const After = slots.afterConversation;
1075
1093
  const Empty = slots.emptyConversation;
1076
1094
  const remember = (value) => boundedSet(conversationMemory, memoryKey, value);
1095
+ const pendingMessage = typeof pending === "string" ? { text: pending, status: "sending" } : pending;
1077
1096
  const pin = () => {
1078
1097
  if (!scroller.current) return;
1079
1098
  scroller.current.scrollTop = scroller.current.scrollHeight;
@@ -1095,7 +1114,7 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1095
1114
  if (remembered.top !== null && !remembered.atBottom) element.scrollTop = remembered.top;
1096
1115
  else pin();
1097
1116
  } else if (atBottom) pin();
1098
- }, [memoryKey, state.transcript.length, state.busy]);
1117
+ }, [memoryKey, state.transcript, state.busy, state.operation, pendingMessage?.text, pendingMessage?.status]);
1099
1118
  return /* @__PURE__ */ jsxs2("div", { class: "scui-conversation-wrap", children: [
1100
1119
  /* @__PURE__ */ jsx3("div", { class: "scui-conversation", ref: scroller, tabIndex: 0, "aria-label": "Conversation", onScroll: (event) => {
1101
1120
  const element = event.currentTarget;
@@ -1115,9 +1134,15 @@ function Conversation({ state, adapter, components = {}, slots = {}, memoryKey =
1115
1134
  !blocks.length && state.startup !== "ready" ? /* @__PURE__ */ jsx3(LoadingStatus, { state }) : null,
1116
1135
  !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,
1117
1136
  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)),
1118
- pending ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", children: [
1119
- /* @__PURE__ */ jsx3(Markdown, { value: pending }),
1120
- /* @__PURE__ */ jsx3("small", { children: state.error && !state.busy ? "Not sent" : "Sending\u2026" })
1137
+ pendingMessage ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", "data-status": pendingMessage.status, children: [
1138
+ /* @__PURE__ */ jsx3(Markdown, { value: pendingMessage.text }),
1139
+ /* @__PURE__ */ jsxs2("footer", { children: [
1140
+ /* @__PURE__ */ jsx3("small", { children: pendingMessage.status === "failed" ? "Not sent" : "Sending\u2026" }),
1141
+ pendingMessage.status === "failed" ? /* @__PURE__ */ jsxs2("span", { children: [
1142
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onRetry, children: "Retry" }),
1143
+ /* @__PURE__ */ jsx3("button", { type: "button", onClick: pendingMessage.onEdit, children: "Edit" })
1144
+ ] }) : null
1145
+ ] })
1121
1146
  ] }) : null,
1122
1147
  state.busy ? /* @__PURE__ */ jsxs2("div", { class: "scui-working", role: "status", children: [
1123
1148
  /* @__PURE__ */ jsx3("span", { "aria-hidden": "true", children: "\u2726" }),
@@ -1195,12 +1220,12 @@ function HarnessLogo({ id, activity, size = 28, onMissingLogo }) {
1195
1220
  }
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();
@@ -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
- 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/index.d.ts CHANGED
@@ -237,6 +237,13 @@ export interface MessengerLabels {
237
237
  forkHere: string;
238
238
  }
239
239
 
240
+ export interface PendingMessageModel {
241
+ text: string;
242
+ status: 'sending' | 'failed';
243
+ onRetry?(): void;
244
+ onEdit?(): void;
245
+ }
246
+
240
247
  export interface ComponentOverrideProps<T = unknown> {
241
248
  state: SupercodeUiState;
242
249
  adapter: UiAdapter;
@@ -336,11 +343,11 @@ export function ActivityGroup(props: { entries: TranscriptEntryModel[]; state: S
336
343
  export function ToolRow(props: ToolRowProps): VNode;
337
344
  export function TaskPlan(props: { plan: TaskPlanModel }): VNode | null;
338
345
  export function SessionDetails(props: { semantics: SessionSemanticsModel }): VNode | null;
339
- export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | null }): VNode;
346
+ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | PendingMessageModel | null }): VNode;
340
347
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void }): VNode;
341
- export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels }): VNode;
348
+ export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels; focusKey?: string | null }): VNode;
342
349
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
343
- export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; onPending?(text: string): void }): VNode;
350
+ export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string } | null; onPending?(text: string): void; onDraftRestored?(id: string | number): void }): VNode;
344
351
  export function SupercodeMessenger(props: MessengerProps): VNode;
345
352
 
346
353
  export interface MountedMessenger {