@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/messenger.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/messenger.jsx
2
- import { useEffect as useEffect4, useMemo as useMemo2, useRef as useRef3, useState as useState4 } from "preact/hooks";
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 (!state.busy && state.canSend && queue.length) {
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, state.busy, state.canSend]);
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 (state.busy) updateQueue((items) => [...items, text]);
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("", state.busy ? [...queue, text] : queue);
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" : state.busy ? "Queue a follow-up\u2026" : labels.askAgent, value: draft, disabled: state.mode !== "control" && !state.canSend, onInput: (event) => {
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": state.busy ? "Queue message" : "Send message", disabled: !draft.trim() || !state.busy && !state.canSend, onClick: send, children: state.busy ? "+" : "\u2191" })
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
- pending ? /* @__PURE__ */ jsxs2("article", { class: "scui-message scui-pending", "data-role": "user", children: [
1116
- /* @__PURE__ */ jsx3(Markdown, { value: pending }),
1117
- /* @__PURE__ */ jsx3("small", { children: state.error && !state.busy ? "Not sent" : "Sending\u2026" })
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
- return /* @__PURE__ */ jsxs4("section", { class: "scui-list", children: [
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, setPending] = useState4(null);
1322
- const acknowledged = useRef3(/* @__PURE__ */ new Set());
1323
- useEffect4(() => {
1324
- setPending(null);
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
- useEffect4(() => {
1327
- if (pending && state.transcript.some((entry) => entry.role === "user" && entry.text.trim() === pending.trim())) setPending(null);
1328
- }, [pending, state.transcript]);
1329
- useEffect4(() => {
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: setPending }, memoryKey) : null
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
- useEffect4(() => {
1428
+ useEffect5(() => {
1363
1429
  if (!startable.some((item) => item.id === harness)) setHarness(startable[0]?.id ?? "");
1364
1430
  }, [harness, startableKey]);
1365
- useEffect4(() => {
1431
+ useEffect5(() => {
1366
1432
  if (starting && (state.busy || state.transcript.length)) onStarted();
1367
1433
  }, [onStarted, starting, state.busy, state.transcript.length]);
1368
- useEffect4(() => {
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
- useEffect4(() => {
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: () => setView("new"), onClose: adapter.onClose ? close : void 0, components, labels: copy }) : null,
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: () => setView("list"), onNew: () => setView("new"), onClose: adapter.onClose ? close : void 0, components, slots, labels: copy }) : null,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
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
- return /* @__PURE__ */ jsxs3("section", { class: "scui-list", children: [
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 }