@volter-ai-dev/supercode-ui 0.1.41 → 0.1.43

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.
@@ -1280,7 +1280,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1280
1280
  ] })
1281
1281
  ] });
1282
1282
  }
1283
- function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
1283
+ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, command = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
1284
1284
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
1285
1285
  const [draft, setDraft] = useState2(remembered.draft);
1286
1286
  const [context, setContext] = useState2(remembered.context ?? []);
@@ -1292,6 +1292,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1292
1292
  const [dragging, setDragging] = useState2(false);
1293
1293
  const [pickerError, setPickerError] = useState2(null);
1294
1294
  const textarea = useRef2(null);
1295
+ const lastCommand = useRef2(null);
1295
1296
  useAutosizeTextarea(textarea, draft);
1296
1297
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
1297
1298
  useEffect2(() => {
@@ -1333,6 +1334,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1333
1334
  textarea.current?.focus({ preventScroll: true });
1334
1335
  onDraftRestored?.(restoreDraft.id);
1335
1336
  }, [onDraftRestored, restoreDraft?.id]);
1337
+ useEffect2(() => {
1338
+ if (!command || command.id === lastCommand.current) return;
1339
+ lastCommand.current = command.id;
1340
+ if (command.action === "attach") {
1341
+ try {
1342
+ const attachments = partitionAttachments(command.attachments);
1343
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1344
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1345
+ const nextContext = mergeContext(context, attachments.context);
1346
+ const nextImages = mergeImages(images, attachments.images);
1347
+ setContext(nextContext);
1348
+ setImages(nextImages);
1349
+ setPickerError(null);
1350
+ remember(draft, nextContext, nextImages, queue);
1351
+ } catch (error) {
1352
+ setPickerError(error instanceof Error ? error.message : "Could not attach context.");
1353
+ }
1354
+ }
1355
+ textarea.current?.focus({ preventScroll: true });
1356
+ }, [command?.id]);
1336
1357
  useEffect2(() => {
1337
1358
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
1338
1359
  return () => clearTimeout(timer);
@@ -2394,6 +2415,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2394
2415
  ] }, item.id)) })
2395
2416
  ] });
2396
2417
  }
2418
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2419
+ const [open, setOpen] = useState5(false);
2420
+ const menuId = useId2();
2421
+ const root = useRef6(null);
2422
+ const trigger = useRef6(null);
2423
+ const panel = useRef6(null);
2424
+ const available = harnesses.filter((item) => item.startable);
2425
+ const unavailable = harnesses.filter((item) => !item.startable);
2426
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2427
+ const close = () => {
2428
+ setOpen(false);
2429
+ trigger.current?.focus({ preventScroll: true });
2430
+ };
2431
+ useEffect7(() => {
2432
+ if (!open) return;
2433
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2434
+ const dismiss = (event) => {
2435
+ if (event.type === "keydown" && event.key === "Escape") {
2436
+ event.preventDefault();
2437
+ close();
2438
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2439
+ setOpen(false);
2440
+ }
2441
+ };
2442
+ document.addEventListener("keydown", dismiss);
2443
+ document.addEventListener("pointerdown", dismiss, true);
2444
+ return () => {
2445
+ document.removeEventListener("keydown", dismiss);
2446
+ document.removeEventListener("pointerdown", dismiss, true);
2447
+ };
2448
+ }, [open]);
2449
+ const navigate = (event) => {
2450
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2451
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2452
+ if (!items.length) return;
2453
+ event.preventDefault();
2454
+ const current = items.indexOf(document.activeElement);
2455
+ const index = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowDown" ? (current + 1) % items.length : (current <= 0 ? items.length : current) - 1;
2456
+ items[index].focus({ preventScroll: true });
2457
+ };
2458
+ const select = (id) => {
2459
+ onChange(id);
2460
+ close();
2461
+ };
2462
+ return /* @__PURE__ */ jsxs7("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2463
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2464
+ }, children: [
2465
+ /* @__PURE__ */ jsxs7("button", { ref: trigger, className: "scui-harness-trigger", type: "button", disabled: disabled || !selected, "aria-label": selected ? `Coding harness: ${selected.label}` : "No coding harness available", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? menuId : void 0, onClick: () => setOpen((current) => !current), children: [
2466
+ selected ? /* @__PURE__ */ jsx8(Logo, { id: selected.id, size: 20 }) : null,
2467
+ /* @__PURE__ */ jsx8("strong", { children: selected?.label ?? "No harness available" }),
2468
+ /* @__PURE__ */ jsx8(UiIcon, { name: "chevron", size: 13 })
2469
+ ] }),
2470
+ open ? /* @__PURE__ */ jsxs7("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2471
+ /* @__PURE__ */ jsx8("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2472
+ /* @__PURE__ */ jsx8("div", { className: "scui-harness-options", role: "listbox", "aria-label": "Available coding harnesses", children: available.map((item) => /* @__PURE__ */ jsxs7("button", { type: "button", role: "option", "aria-selected": item.id === selected?.id, onClick: () => select(item.id), children: [
2473
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2474
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2475
+ /* @__PURE__ */ jsx8("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx8(UiIcon, { name: "check", size: 15 }) : null })
2476
+ ] }, item.id)) }),
2477
+ unavailable.length ? /* @__PURE__ */ jsxs7("details", { className: "scui-harness-manage", children: [
2478
+ /* @__PURE__ */ jsx8("summary", { children: "Manage additional harnesses" }),
2479
+ /* @__PURE__ */ jsx8("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs7("section", { children: [
2480
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2481
+ /* @__PURE__ */ jsxs7("span", { children: [
2482
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2483
+ /* @__PURE__ */ jsx8("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2484
+ ] }),
2485
+ item.repair ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2486
+ ] }, item.id)) })
2487
+ ] }) : null
2488
+ ] }) : null
2489
+ ] });
2490
+ }
2397
2491
 
2398
2492
  // src/messenger.jsx
2399
2493
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2540,7 +2634,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2540
2634
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
2541
2635
  ] });
2542
2636
  }
2543
- function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
2637
+ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
2544
2638
  const Header = slots.header;
2545
2639
  const HeaderActions = slots.headerActions;
2546
2640
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
@@ -2667,11 +2761,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2667
2761
  /* @__PURE__ */ jsx9(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
2668
2762
  /* @__PURE__ */ jsx9(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
2669
2763
  /* @__PURE__ */ jsx9(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
2670
- state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
2764
+ state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx9(Composer, { state: actionState, adapter: trackedAdapter, labels, memoryKey, pendingStatus: pending?.status ?? null, restoreDraft, command: composerCommand, contextCandidates, components, onDraftRestored: (id) => setRestoreDraft((value) => value?.id === id ? null : value), onPending: beginPending }, memoryKey) : null,
2671
2765
  settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
2672
2766
  ] });
2673
2767
  }
2674
- function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
2768
+ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
2675
2769
  const startable = state.harnesses.filter((item) => item.startable);
2676
2770
  const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
2677
2771
  const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
@@ -2685,8 +2779,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2685
2779
  const [dragging, setDragging] = useState6(false);
2686
2780
  const [pickerError, setPickerError] = useState6(null);
2687
2781
  const startSequence = useRef7(0);
2782
+ const lastComposerCommand = useRef7(null);
2688
2783
  const textarea = useRef7(null);
2689
2784
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2785
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2690
2786
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2691
2787
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2692
2788
  const rememberedMode = modes[harness];
@@ -2727,6 +2823,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2727
2823
  remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
2728
2824
  textarea.current?.focus({ preventScroll: true });
2729
2825
  }, [navigation?.id]);
2826
+ useEffect8(() => {
2827
+ if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
2828
+ lastComposerCommand.current = composerCommand.id;
2829
+ if (composerCommand.action === "attach") {
2830
+ if (mode === "terminal" && !launchModes.includes("headless")) {
2831
+ setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2832
+ } else {
2833
+ try {
2834
+ const attachments = partitionAttachments(composerCommand.attachments);
2835
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2836
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2837
+ const nextContext = mergeContext(context, attachments.context);
2838
+ const nextImages = mergeImages(images, attachments.images);
2839
+ const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2840
+ if (nextModes !== modes) setModes(nextModes);
2841
+ setContext(nextContext);
2842
+ setImages(nextImages);
2843
+ setPickerError(null);
2844
+ remember({ context: nextContext, images: nextImages, modes: nextModes });
2845
+ } catch (error) {
2846
+ setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2847
+ }
2848
+ }
2849
+ }
2850
+ textarea.current?.focus({ preventScroll: true });
2851
+ }, [composerCommand?.id]);
2730
2852
  const pickContext = () => {
2731
2853
  if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2732
2854
  setPicking(true);
@@ -2824,18 +2946,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2824
2946
  /* @__PURE__ */ jsx9("small", { children: "Choose a coding harness and send the first message." })
2825
2947
  ] }),
2826
2948
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2827
- /* @__PURE__ */ jsxs8("label", { className: "scui-harness-picker", children: [
2828
- /* @__PURE__ */ jsx9(HarnessLogo, { id: harness, size: 24 }),
2829
- /* @__PURE__ */ jsx9("span", { children: "Coding harness" }),
2830
- /* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2831
- const value = event.currentTarget.value;
2832
- setHarness(value);
2833
- remember({ harness: value });
2834
- }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
2835
- item.label,
2836
- item.startable ? "" : " \xB7 unavailable"
2837
- ] }, item.id)) })
2838
- ] }),
2839
2949
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2840
2950
  launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2841
2951
  /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
@@ -2864,7 +2974,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2864
2974
  return next;
2865
2975
  }) }),
2866
2976
  terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2867
- /* @__PURE__ */ jsxs8("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2977
+ /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2868
2978
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2869
2979
  event.preventDefault();
2870
2980
  setDragging(true);
@@ -2874,7 +2984,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2874
2984
  }, onDragLeave: (event) => {
2875
2985
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2876
2986
  }, onDrop: dropImages, children: [
2877
- adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2878
2987
  /* @__PURE__ */ jsx9("textarea", { ref: 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 || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
2879
2988
  const value = event.currentTarget.value;
2880
2989
  setDraft(value);
@@ -2885,12 +2994,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2885
2994
  send();
2886
2995
  }
2887
2996
  } }),
2888
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2997
+ /* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
2998
+ adapter.pickContext ? /* @__PURE__ */ jsx9("button", { className: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2999
+ /* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
3000
+ setHarness(value);
3001
+ remember({ harness: value });
3002
+ } }),
3003
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
3004
+ ] })
2889
3005
  ] })
2890
3006
  ] })
2891
3007
  ] });
2892
3008
  }
2893
- function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
3009
+ function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
2894
3010
  const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
2895
3011
  const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
2896
3012
  const copy = { ...DEFAULT_LABELS, ...labels };
@@ -2961,7 +3077,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2961
3077
  setNewNavigation(null);
2962
3078
  setView("new");
2963
3079
  }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
2964
- view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation }) : null,
3080
+ view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation: newNavigation, composerCommand }) : null,
2965
3081
  view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
2966
3082
  setListFocus(state.attached?.key ?? listFocus);
2967
3083
  setView("list");
@@ -2970,7 +3086,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2970
3086
  setChatNavigation(null);
2971
3087
  setNewNavigation(null);
2972
3088
  setView("new");
2973
- }, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
3089
+ }, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
2974
3090
  opening ? /* @__PURE__ */ jsxs8("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
2975
3091
  /* @__PURE__ */ jsx9(HarnessLogo, { id: opening.harness, size: 34 }),
2976
3092
  /* @__PURE__ */ jsxs8("span", { children: [
@@ -1,2 +1,2 @@
1
- export type { HarnessAdvisoryProps, HarnessSettingsPanelProps } from './index.js';
2
- export { HarnessAdvisory, HarnessSettingsPanel } from './index.js';
1
+ export type { HarnessAdvisoryProps, HarnessPickerProps, HarnessSettingsPanelProps } from './index.js';
2
+ export { HarnessAdvisory, HarnessPicker, HarnessSettingsPanel } from './index.js';
@@ -307,8 +307,82 @@ function HarnessReadiness({ harnesses, adapter }) {
307
307
  ] }, item.id)) })
308
308
  ] });
309
309
  }
310
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
311
+ const [open, setOpen] = useState(false);
312
+ const menuId = useId();
313
+ const root = useRef(null);
314
+ const trigger = useRef(null);
315
+ const panel = useRef(null);
316
+ const available = harnesses.filter((item) => item.startable);
317
+ const unavailable = harnesses.filter((item) => !item.startable);
318
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
319
+ const close = () => {
320
+ setOpen(false);
321
+ trigger.current?.focus({ preventScroll: true });
322
+ };
323
+ useEffect2(() => {
324
+ if (!open) return;
325
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
326
+ const dismiss = (event) => {
327
+ if (event.type === "keydown" && event.key === "Escape") {
328
+ event.preventDefault();
329
+ close();
330
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
331
+ setOpen(false);
332
+ }
333
+ };
334
+ document.addEventListener("keydown", dismiss);
335
+ document.addEventListener("pointerdown", dismiss, true);
336
+ return () => {
337
+ document.removeEventListener("keydown", dismiss);
338
+ document.removeEventListener("pointerdown", dismiss, true);
339
+ };
340
+ }, [open]);
341
+ const navigate = (event) => {
342
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
343
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
344
+ if (!items.length) return;
345
+ event.preventDefault();
346
+ const current = items.indexOf(document.activeElement);
347
+ const index = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowDown" ? (current + 1) % items.length : (current <= 0 ? items.length : current) - 1;
348
+ items[index].focus({ preventScroll: true });
349
+ };
350
+ const select = (id) => {
351
+ onChange(id);
352
+ close();
353
+ };
354
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
355
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
356
+ }, children: [
357
+ /* @__PURE__ */ jsxs3("button", { ref: trigger, className: "scui-harness-trigger", type: "button", disabled: disabled || !selected, "aria-label": selected ? `Coding harness: ${selected.label}` : "No coding harness available", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? menuId : void 0, onClick: () => setOpen((current) => !current), children: [
358
+ selected ? /* @__PURE__ */ jsx3(Logo, { id: selected.id, size: 20 }) : null,
359
+ /* @__PURE__ */ jsx3("strong", { children: selected?.label ?? "No harness available" }),
360
+ /* @__PURE__ */ jsx3(UiIcon, { name: "chevron", size: 13 })
361
+ ] }),
362
+ open ? /* @__PURE__ */ jsxs3("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
363
+ /* @__PURE__ */ jsx3("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
364
+ /* @__PURE__ */ jsx3("div", { className: "scui-harness-options", role: "listbox", "aria-label": "Available coding harnesses", children: available.map((item) => /* @__PURE__ */ jsxs3("button", { type: "button", role: "option", "aria-selected": item.id === selected?.id, onClick: () => select(item.id), children: [
365
+ /* @__PURE__ */ jsx3(Logo, { id: item.id, size: 24 }),
366
+ /* @__PURE__ */ jsx3("strong", { children: item.label }),
367
+ /* @__PURE__ */ jsx3("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx3(UiIcon, { name: "check", size: 15 }) : null })
368
+ ] }, item.id)) }),
369
+ unavailable.length ? /* @__PURE__ */ jsxs3("details", { className: "scui-harness-manage", children: [
370
+ /* @__PURE__ */ jsx3("summary", { children: "Manage additional harnesses" }),
371
+ /* @__PURE__ */ jsx3("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs3("section", { children: [
372
+ /* @__PURE__ */ jsx3(Logo, { id: item.id, size: 24 }),
373
+ /* @__PURE__ */ jsxs3("span", { children: [
374
+ /* @__PURE__ */ jsx3("strong", { children: item.label }),
375
+ /* @__PURE__ */ jsx3("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
376
+ ] }),
377
+ item.repair ? /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
378
+ ] }, item.id)) })
379
+ ] }) : null
380
+ ] }) : null
381
+ ] });
382
+ }
310
383
  export {
311
384
  HarnessAdvisory,
385
+ HarnessPicker,
312
386
  HarnessReadiness,
313
387
  HarnessSettingsPanel
314
388
  };
package/settings.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { HarnessAdvisory, HarnessSettingsPanel } from './index.js';
2
- export type { HarnessAdvisoryProps, HarnessSettingsPanelProps } from './index.js';
1
+ export { HarnessAdvisory, HarnessPicker, HarnessSettingsPanel } from './index.js';
2
+ export type { HarnessAdvisoryProps, HarnessPickerProps, HarnessSettingsPanelProps } from './index.js';
package/settings.mjs CHANGED
@@ -307,8 +307,82 @@ function HarnessReadiness({ harnesses, adapter }) {
307
307
  ] }, item.id)) })
308
308
  ] });
309
309
  }
310
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
311
+ const [open, setOpen] = useState(false);
312
+ const menuId = useId();
313
+ const root = useRef(null);
314
+ const trigger = useRef(null);
315
+ const panel = useRef(null);
316
+ const available = harnesses.filter((item) => item.startable);
317
+ const unavailable = harnesses.filter((item) => !item.startable);
318
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
319
+ const close = () => {
320
+ setOpen(false);
321
+ trigger.current?.focus({ preventScroll: true });
322
+ };
323
+ useEffect2(() => {
324
+ if (!open) return;
325
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
326
+ const dismiss = (event) => {
327
+ if (event.type === "keydown" && event.key === "Escape") {
328
+ event.preventDefault();
329
+ close();
330
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
331
+ setOpen(false);
332
+ }
333
+ };
334
+ document.addEventListener("keydown", dismiss);
335
+ document.addEventListener("pointerdown", dismiss, true);
336
+ return () => {
337
+ document.removeEventListener("keydown", dismiss);
338
+ document.removeEventListener("pointerdown", dismiss, true);
339
+ };
340
+ }, [open]);
341
+ const navigate = (event) => {
342
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
343
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
344
+ if (!items.length) return;
345
+ event.preventDefault();
346
+ const current = items.indexOf(document.activeElement);
347
+ const index = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : event.key === "ArrowDown" ? (current + 1) % items.length : (current <= 0 ? items.length : current) - 1;
348
+ items[index].focus({ preventScroll: true });
349
+ };
350
+ const select = (id) => {
351
+ onChange(id);
352
+ close();
353
+ };
354
+ return /* @__PURE__ */ jsxs3("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
355
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
356
+ }, children: [
357
+ /* @__PURE__ */ jsxs3("button", { ref: trigger, className: "scui-harness-trigger", type: "button", disabled: disabled || !selected, "aria-label": selected ? `Coding harness: ${selected.label}` : "No coding harness available", "aria-haspopup": "dialog", "aria-expanded": open, "aria-controls": open ? menuId : void 0, onClick: () => setOpen((current) => !current), children: [
358
+ selected ? /* @__PURE__ */ jsx3(Logo, { id: selected.id, size: 20 }) : null,
359
+ /* @__PURE__ */ jsx3("strong", { children: selected?.label ?? "No harness available" }),
360
+ /* @__PURE__ */ jsx3(UiIcon, { name: "chevron", size: 13 })
361
+ ] }),
362
+ open ? /* @__PURE__ */ jsxs3("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
363
+ /* @__PURE__ */ jsx3("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
364
+ /* @__PURE__ */ jsx3("div", { className: "scui-harness-options", role: "listbox", "aria-label": "Available coding harnesses", children: available.map((item) => /* @__PURE__ */ jsxs3("button", { type: "button", role: "option", "aria-selected": item.id === selected?.id, onClick: () => select(item.id), children: [
365
+ /* @__PURE__ */ jsx3(Logo, { id: item.id, size: 24 }),
366
+ /* @__PURE__ */ jsx3("strong", { children: item.label }),
367
+ /* @__PURE__ */ jsx3("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx3(UiIcon, { name: "check", size: 15 }) : null })
368
+ ] }, item.id)) }),
369
+ unavailable.length ? /* @__PURE__ */ jsxs3("details", { className: "scui-harness-manage", children: [
370
+ /* @__PURE__ */ jsx3("summary", { children: "Manage additional harnesses" }),
371
+ /* @__PURE__ */ jsx3("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs3("section", { children: [
372
+ /* @__PURE__ */ jsx3(Logo, { id: item.id, size: 24 }),
373
+ /* @__PURE__ */ jsxs3("span", { children: [
374
+ /* @__PURE__ */ jsx3("strong", { children: item.label }),
375
+ /* @__PURE__ */ jsx3("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
376
+ ] }),
377
+ item.repair ? /* @__PURE__ */ jsx3("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
378
+ ] }, item.id)) })
379
+ ] }) : null
380
+ ] }) : null
381
+ ] });
382
+ }
310
383
  export {
311
384
  HarnessAdvisory,
385
+ HarnessPicker,
312
386
  HarnessReadiness,
313
387
  HarnessSettingsPanel
314
388
  };
package/styles.css CHANGED
@@ -198,7 +198,10 @@
198
198
  .scui-send,.scui-stop { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:var(--scui-accent); color:#fff; cursor:pointer }.scui-stop { background:var(--scui-danger) }.scui-send:disabled,.scui-stop:disabled { opacity:.4; cursor:default }.scui-control-spinner { box-sizing:border-box; width:13px; height:13px; border:1.5px solid currentColor; border-right-color:transparent; border-radius:50%; animation:scui-spin .75s linear infinite }
199
199
  .scui-queue-send { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:transparent; color:var(--scui-muted); cursor:pointer }.scui-queue-send:hover { background:var(--scui-fill); color:var(--scui-fg) }
200
200
  .scui-queue { display:grid; gap:4px; max-height:85px; margin-bottom:6px; overflow:auto; font-size:10.5px }.scui-queue > span { display:flex; gap:6px; padding:4px 6px; border-radius:5px; background:var(--scui-fill) }.scui-queue > span > span { display:grid; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-queue small { color:var(--scui-muted) }.scui-queue > span button { margin-left:auto; border:0; background:transparent }
201
- .scui-harness-picker { display:flex; align-items:center; gap:7px; margin-bottom:7px }.scui-harness-picker select { margin-left:auto; max-width:55%; padding:4px; border:1px solid var(--scui-border); border-radius:6px; background:var(--scui-bg); color:var(--scui-fg) }
201
+ .scui-new-envelope { display:grid; align-items:stretch; gap:3px; padding:7px }.scui-new-envelope textarea { box-sizing:border-box; width:100%; min-height:54px; padding:2px 3px }.scui-new-envelope-controls { display:flex; min-width:0; align-items:center; gap:3px }.scui-new-envelope-controls > span { display:flex; margin-left:auto }.scui-new-envelope-controls .scui-attach { flex-basis:29px; width:29px; height:29px }
202
+ .scui-harness-menu { position:relative; min-width:0 }.scui-harness-trigger { display:flex; max-width:170px; height:29px; align-items:center; gap:6px; padding:3px 7px 3px 4px; border:0; border-radius:8px; background:transparent; color:var(--scui-fg); cursor:pointer }.scui-harness-trigger:hover:not(:disabled),.scui-harness-trigger:focus-visible,.scui-harness-trigger[aria-expanded="true"] { background:var(--scui-fill) }.scui-harness-trigger:disabled { cursor:default; opacity:.55 }.scui-harness-trigger > strong { min-width:0; overflow:hidden; font-size:10.5px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-harness-trigger > .scui-icon { flex:none; color:var(--scui-muted); transform:rotate(90deg); transition:transform 120ms }.scui-harness-trigger[aria-expanded="true"] > .scui-icon { transform:rotate(-90deg) }
203
+ .scui-harness-popover { position:absolute; z-index:25; bottom:calc(100% + 7px); left:0; display:grid; box-sizing:border-box; width:min(250px,calc(100vw - 24px)); max-height:min(330px,calc(100dvh - 90px)); overflow:auto; padding:5px; border:1px solid var(--scui-border-strong); border-radius:10px; background:var(--scui-bg-raised); box-shadow:0 12px 34px rgba(0,0,0,.2) }.scui-harness-popover-title { padding:5px 7px; color:var(--scui-muted); font-size:8.5px; font-weight:650; letter-spacing:.055em; text-transform:uppercase }.scui-harness-options { display:grid; gap:1px }.scui-harness-options > button { display:grid; grid-template-columns:auto minmax(0,1fr) 18px; width:100%; min-height:36px; align-items:center; gap:8px; padding:5px 7px; border:0; border-radius:7px; background:transparent; color:var(--scui-fg); text-align:left; cursor:pointer }.scui-harness-options > button:hover,.scui-harness-options > button:focus-visible { background:var(--scui-fill) }.scui-harness-options > button[aria-selected="true"] { background:color-mix(in srgb,var(--scui-accent) 8%,var(--scui-bg-raised)) }.scui-harness-options > button > strong { min-width:0; overflow:hidden; font-size:11px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-harness-options > button > span { display:grid; color:var(--scui-accent); place-items:center }
204
+ .scui-harness-manage { margin-top:4px; padding-top:4px; border-top:1px solid var(--scui-border) }.scui-harness-manage > summary { padding:6px 7px; color:var(--scui-muted); cursor:pointer; font-size:9.5px }.scui-harness-manage > div { display:grid; gap:1px }.scui-harness-manage section { display:grid; grid-template-columns:auto minmax(0,1fr) auto; min-height:36px; align-items:center; gap:8px; padding:5px 7px }.scui-harness-manage section > span { display:grid; min-width:0 }.scui-harness-manage section strong,.scui-harness-manage section small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-harness-manage section strong { font-size:10.5px; font-weight:600 }.scui-harness-manage section small { color:var(--scui-muted); font-size:8.5px }.scui-harness-manage section > button { padding:4px 6px; border:1px solid var(--scui-border); border-radius:6px; background:transparent; color:var(--scui-muted); cursor:pointer; font-size:8.5px }.scui-harness-manage section > button:hover { border-color:var(--scui-border-strong); color:var(--scui-fg) }
202
205
  .scui-readiness { margin:0 0 7px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg) }.scui-readiness > summary { padding:6px 8px; color:var(--scui-muted); cursor:pointer; font-size:10px }.scui-readiness > div { display:grid; border-top:1px solid var(--scui-border) }.scui-readiness section { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:7px; padding:7px 8px }.scui-readiness section + section { border-top:1px solid var(--scui-border) }.scui-readiness section > span { display:grid; min-width:0 }.scui-readiness section strong,.scui-readiness section small,.scui-readiness section em { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-readiness section small { color:var(--scui-muted); font-size:9px }.scui-readiness section em { color:var(--scui-muted); font-size:8px; font-style:normal }.scui-readiness section button { padding:4px 6px; border:1px solid var(--scui-border); border-radius:6px; background:transparent; cursor:pointer; font-size:9px }
203
206
  .scui-launch-modes { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:5px; margin:0 0 7px; padding:0; border:0 }.scui-launch-modes legend { position:absolute; overflow:hidden; width:1px; height:1px; clip:rect(0 0 0 0); white-space:nowrap }.scui-launch-modes label { position:relative; min-width:0; cursor:pointer }.scui-launch-modes input { position:absolute; opacity:0; pointer-events:none }.scui-launch-modes label > span { display:flex; min-height:42px; flex-direction:column; justify-content:center; padding:6px 9px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg); transition:border-color .14s ease,background .14s ease }.scui-launch-modes strong { font-size:11px; font-weight:650 }.scui-launch-modes small { overflow:hidden; color:var(--scui-muted); font-size:9px; text-overflow:ellipsis; white-space:nowrap }.scui-launch-modes input:checked + span { border-color:color-mix(in srgb,var(--scui-accent) 58%,var(--scui-border)); background:color-mix(in srgb,var(--scui-accent) 8%,var(--scui-bg)) }.scui-launch-modes input:focus-visible + span { outline:2px solid var(--scui-accent); outline-offset:1px }.scui-launch-modes:disabled label { cursor:default; opacity:.62 }
204
207
  .scui-new { display:grid; justify-items:center; gap:5px; margin:auto; padding:20px; color:var(--scui-muted); text-align:center }.scui-new > span { color:var(--scui-accent); font-size:28px }.scui-new strong { color:var(--scui-fg) }
@@ -215,6 +218,8 @@
215
218
  .scui-head { min-height:52px }
216
219
  .scui-menu-panel button,.scui-request-actions button,.scui-load { min-height:40px }
217
220
  .scui-send,.scui-stop { width:40px; height:40px }
221
+ .scui-new-envelope-controls .scui-attach,.scui-harness-trigger { min-height:40px }
222
+ .scui-harness-options > button,.scui-harness-manage section { min-height:44px }
218
223
  .scui-message-meta button,.scui-code-copy { min-width:36px; min-height:36px }
219
224
  .scui-latest { min-height:36px; padding-inline:12px }
220
225
  }