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

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/README.md CHANGED
@@ -60,14 +60,14 @@ import {
60
60
  SessionList,
61
61
  } from '@volter-ai-dev/supercode-ui/preact';
62
62
  import { HarnessLogo, harnessLogoDataUrl } from '@volter-ai-dev/supercode-ui/preact/logo';
63
- import { HarnessAdvisory, HarnessSettingsPanel } from '@volter-ai-dev/supercode-ui/preact/settings';
63
+ import { HarnessAdvisory, HarnessPicker, HarnessSettingsPanel } from '@volter-ai-dev/supercode-ui/preact/settings';
64
64
  import { groupConversation } from '@volter-ai-dev/supercode-ui/core';
65
65
  ```
66
66
 
67
67
  The public components are `SupercodeMessenger`, `AgentActivityLauncher`, `Conversation`, `TranscriptEntry`,
68
68
  `ActivityGroup`, `RequestCard`, `SessionList`, `SessionRow`, `Composer`, `ContinuationBar`,
69
- `ContextCandidate`, `ContextCandidates`, `LoadingStatus`, `TaskPlan`, `SessionDetails`, `HarnessLogo`, `HarnessAdvisory`, and
70
- `HarnessSettingsPanel`. Pure state readers, selectors,
69
+ `ContextCandidate`, `ContextCandidates`, `LoadingStatus`, `TaskPlan`, `SessionDetails`, `HarnessLogo`, `HarnessAdvisory`,
70
+ `HarnessPicker`, and `HarnessSettingsPanel`. Pure state readers, selectors,
71
71
  formatters, and intent constructors live at `supercode-ui/core` and have no DOM or Preact imports.
72
72
  For genuine partial delivery, the `preact/logo`, `preact/icon`, `preact/conversation`, `preact/sessions`,
73
73
  `preact/composer`, `preact/settings`, and `preact/messenger` subpaths are independently tree-shaken artifacts; taking
package/components.d.ts CHANGED
@@ -8,6 +8,7 @@ export {
8
8
  ContinuationBar,
9
9
  Conversation,
10
10
  HarnessLogo,
11
+ HarnessPicker,
11
12
  HarnessReadiness,
12
13
  ImageViewer,
13
14
  LoadingStatus,
package/components.mjs CHANGED
@@ -2477,6 +2477,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2477
2477
  ] }, item.id)) })
2478
2478
  ] });
2479
2479
  }
2480
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2481
+ const [open, setOpen] = useState5(false);
2482
+ const menuId = useId2();
2483
+ const root = useRef6(null);
2484
+ const trigger = useRef6(null);
2485
+ const panel = useRef6(null);
2486
+ const available = harnesses.filter((item) => item.startable);
2487
+ const unavailable = harnesses.filter((item) => !item.startable);
2488
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2489
+ const close = () => {
2490
+ setOpen(false);
2491
+ trigger.current?.focus({ preventScroll: true });
2492
+ };
2493
+ useEffect7(() => {
2494
+ if (!open) return;
2495
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2496
+ const dismiss = (event) => {
2497
+ if (event.type === "keydown" && event.key === "Escape") {
2498
+ event.preventDefault();
2499
+ close();
2500
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2501
+ setOpen(false);
2502
+ }
2503
+ };
2504
+ document.addEventListener("keydown", dismiss);
2505
+ document.addEventListener("pointerdown", dismiss, true);
2506
+ return () => {
2507
+ document.removeEventListener("keydown", dismiss);
2508
+ document.removeEventListener("pointerdown", dismiss, true);
2509
+ };
2510
+ }, [open]);
2511
+ const navigate = (event) => {
2512
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2513
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2514
+ if (!items.length) return;
2515
+ event.preventDefault();
2516
+ const current = items.indexOf(document.activeElement);
2517
+ 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;
2518
+ items[index].focus({ preventScroll: true });
2519
+ };
2520
+ const select = (id) => {
2521
+ onChange(id);
2522
+ close();
2523
+ };
2524
+ return /* @__PURE__ */ jsxs8("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2525
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2526
+ }, children: [
2527
+ /* @__PURE__ */ jsxs8("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: [
2528
+ selected ? /* @__PURE__ */ jsx9(Logo, { id: selected.id, size: 20 }) : null,
2529
+ /* @__PURE__ */ jsx9("strong", { children: selected?.label ?? "No harness available" }),
2530
+ /* @__PURE__ */ jsx9(UiIcon, { name: "chevron", size: 13 })
2531
+ ] }),
2532
+ open ? /* @__PURE__ */ jsxs8("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2533
+ /* @__PURE__ */ jsx9("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2534
+ /* @__PURE__ */ jsx9("div", { className: "scui-harness-options", role: "listbox", "aria-label": "Available coding harnesses", children: available.map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", role: "option", "aria-selected": item.id === selected?.id, onClick: () => select(item.id), children: [
2535
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2536
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2537
+ /* @__PURE__ */ jsx9("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx9(UiIcon, { name: "check", size: 15 }) : null })
2538
+ ] }, item.id)) }),
2539
+ unavailable.length ? /* @__PURE__ */ jsxs8("details", { className: "scui-harness-manage", children: [
2540
+ /* @__PURE__ */ jsx9("summary", { children: "Manage additional harnesses" }),
2541
+ /* @__PURE__ */ jsx9("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs8("section", { children: [
2542
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2543
+ /* @__PURE__ */ jsxs8("span", { children: [
2544
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2545
+ /* @__PURE__ */ jsx9("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2546
+ ] }),
2547
+ item.repair ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2548
+ ] }, item.id)) })
2549
+ ] }) : null
2550
+ ] }) : null
2551
+ ] });
2552
+ }
2480
2553
 
2481
2554
  // src/messenger.jsx
2482
2555
  import { jsx as jsx10, jsxs as jsxs9 } from "preact/jsx-runtime";
@@ -2770,6 +2843,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2770
2843
  const startSequence = useRef7(0);
2771
2844
  const textarea = useRef7(null);
2772
2845
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2846
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2773
2847
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2774
2848
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2775
2849
  const rememberedMode = modes[harness];
@@ -2907,18 +2981,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2907
2981
  /* @__PURE__ */ jsx10("small", { children: "Choose a coding harness and send the first message." })
2908
2982
  ] }),
2909
2983
  /* @__PURE__ */ jsxs9("div", { className: "scui-compose", children: [
2910
- /* @__PURE__ */ jsxs9("label", { className: "scui-harness-picker", children: [
2911
- /* @__PURE__ */ jsx10(HarnessLogo, { id: harness, size: 24 }),
2912
- /* @__PURE__ */ jsx10("span", { children: "Coding harness" }),
2913
- /* @__PURE__ */ jsx10("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2914
- const value = event.currentTarget.value;
2915
- setHarness(value);
2916
- remember({ harness: value });
2917
- }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs9("option", { value: item.id, disabled: !item.startable, children: [
2918
- item.label,
2919
- item.startable ? "" : " \xB7 unavailable"
2920
- ] }, item.id)) })
2921
- ] }),
2922
2984
  /* @__PURE__ */ jsx10(Readiness, { harnesses: state.harnesses, state, adapter }),
2923
2985
  launchModes.length > 1 ? /* @__PURE__ */ jsxs9("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2924
2986
  /* @__PURE__ */ jsx10("legend", { children: "Run as" }),
@@ -2947,7 +3009,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2947
3009
  return next;
2948
3010
  }) }),
2949
3011
  terminalAttachments ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2950
- /* @__PURE__ */ jsxs9("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
3012
+ /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2951
3013
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2952
3014
  event.preventDefault();
2953
3015
  setDragging(true);
@@ -2957,7 +3019,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2957
3019
  }, onDragLeave: (event) => {
2958
3020
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2959
3021
  }, onDrop: dropImages, children: [
2960
- adapter.pickContext ? /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
2961
3022
  /* @__PURE__ */ jsx10("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) => {
2962
3023
  const value = event.currentTarget.value;
2963
3024
  setDraft(value);
@@ -2968,7 +3029,14 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2968
3029
  send();
2969
3030
  }
2970
3031
  } }),
2971
- /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "send", size: 17 }) }) })
3032
+ /* @__PURE__ */ jsxs9("footer", { className: "scui-new-envelope-controls", children: [
3033
+ adapter.pickContext ? /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
3034
+ /* @__PURE__ */ jsx10(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
3035
+ setHarness(value);
3036
+ remember({ harness: value });
3037
+ } }),
3038
+ /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "send", size: 17 }) }) })
3039
+ ] })
2972
3040
  ] })
2973
3041
  ] })
2974
3042
  ] });
@@ -3078,6 +3146,7 @@ export {
3078
3146
  Conversation,
3079
3147
  HarnessAdvisory,
3080
3148
  HarnessLogo,
3149
+ HarnessPicker,
3081
3150
  HarnessReadiness,
3082
3151
  HarnessSettingsPanel,
3083
3152
  ImageViewer,
package/embed.mjs CHANGED
@@ -2397,6 +2397,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2397
2397
  ] }, item.id)) })
2398
2398
  ] });
2399
2399
  }
2400
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2401
+ const [open, setOpen] = useState5(false);
2402
+ const menuId = useId2();
2403
+ const root = useRef6(null);
2404
+ const trigger = useRef6(null);
2405
+ const panel = useRef6(null);
2406
+ const available = harnesses.filter((item) => item.startable);
2407
+ const unavailable = harnesses.filter((item) => !item.startable);
2408
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2409
+ const close = () => {
2410
+ setOpen(false);
2411
+ trigger.current?.focus({ preventScroll: true });
2412
+ };
2413
+ useEffect7(() => {
2414
+ if (!open) return;
2415
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2416
+ const dismiss = (event) => {
2417
+ if (event.type === "keydown" && event.key === "Escape") {
2418
+ event.preventDefault();
2419
+ close();
2420
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2421
+ setOpen(false);
2422
+ }
2423
+ };
2424
+ document.addEventListener("keydown", dismiss);
2425
+ document.addEventListener("pointerdown", dismiss, true);
2426
+ return () => {
2427
+ document.removeEventListener("keydown", dismiss);
2428
+ document.removeEventListener("pointerdown", dismiss, true);
2429
+ };
2430
+ }, [open]);
2431
+ const navigate = (event) => {
2432
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2433
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2434
+ if (!items.length) return;
2435
+ event.preventDefault();
2436
+ const current = items.indexOf(document.activeElement);
2437
+ 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;
2438
+ items[index].focus({ preventScroll: true });
2439
+ };
2440
+ const select = (id) => {
2441
+ onChange(id);
2442
+ close();
2443
+ };
2444
+ return /* @__PURE__ */ jsxs7("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2445
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2446
+ }, children: [
2447
+ /* @__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: [
2448
+ selected ? /* @__PURE__ */ jsx8(Logo, { id: selected.id, size: 20 }) : null,
2449
+ /* @__PURE__ */ jsx8("strong", { children: selected?.label ?? "No harness available" }),
2450
+ /* @__PURE__ */ jsx8(UiIcon, { name: "chevron", size: 13 })
2451
+ ] }),
2452
+ open ? /* @__PURE__ */ jsxs7("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2453
+ /* @__PURE__ */ jsx8("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2454
+ /* @__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: [
2455
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2456
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2457
+ /* @__PURE__ */ jsx8("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx8(UiIcon, { name: "check", size: 15 }) : null })
2458
+ ] }, item.id)) }),
2459
+ unavailable.length ? /* @__PURE__ */ jsxs7("details", { className: "scui-harness-manage", children: [
2460
+ /* @__PURE__ */ jsx8("summary", { children: "Manage additional harnesses" }),
2461
+ /* @__PURE__ */ jsx8("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs7("section", { children: [
2462
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2463
+ /* @__PURE__ */ jsxs7("span", { children: [
2464
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2465
+ /* @__PURE__ */ jsx8("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2466
+ ] }),
2467
+ item.repair ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2468
+ ] }, item.id)) })
2469
+ ] }) : null
2470
+ ] }) : null
2471
+ ] });
2472
+ }
2400
2473
 
2401
2474
  // src/messenger.jsx
2402
2475
  import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
@@ -2690,6 +2763,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2690
2763
  const startSequence = useRef7(0);
2691
2764
  const textarea = useRef7(null);
2692
2765
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2766
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2693
2767
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2694
2768
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2695
2769
  const rememberedMode = modes[harness];
@@ -2827,18 +2901,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2827
2901
  /* @__PURE__ */ jsx9("small", { children: "Choose a coding harness and send the first message." })
2828
2902
  ] }),
2829
2903
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2830
- /* @__PURE__ */ jsxs8("label", { className: "scui-harness-picker", children: [
2831
- /* @__PURE__ */ jsx9(HarnessLogo, { id: harness, size: 24 }),
2832
- /* @__PURE__ */ jsx9("span", { children: "Coding harness" }),
2833
- /* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2834
- const value = event.currentTarget.value;
2835
- setHarness(value);
2836
- remember({ harness: value });
2837
- }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
2838
- item.label,
2839
- item.startable ? "" : " \xB7 unavailable"
2840
- ] }, item.id)) })
2841
- ] }),
2842
2904
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2843
2905
  launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2844
2906
  /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
@@ -2867,7 +2929,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2867
2929
  return next;
2868
2930
  }) }),
2869
2931
  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,
2870
- /* @__PURE__ */ jsxs8("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2932
+ /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2871
2933
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2872
2934
  event.preventDefault();
2873
2935
  setDragging(true);
@@ -2877,7 +2939,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2877
2939
  }, onDragLeave: (event) => {
2878
2940
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2879
2941
  }, onDrop: dropImages, children: [
2880
- 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,
2881
2942
  /* @__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) => {
2882
2943
  const value = event.currentTarget.value;
2883
2944
  setDraft(value);
@@ -2888,7 +2949,14 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2888
2949
  send();
2889
2950
  }
2890
2951
  } }),
2891
- /* @__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 }) }) })
2952
+ /* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
2953
+ 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,
2954
+ /* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
2955
+ setHarness(value);
2956
+ remember({ harness: value });
2957
+ } }),
2958
+ /* @__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 }) }) })
2959
+ ] })
2892
2960
  ] })
2893
2961
  ] })
2894
2962
  ] });
package/index.d.ts CHANGED
@@ -433,6 +433,15 @@ export interface HarnessSettingsPanelProps {
433
433
  onClose(): void;
434
434
  }
435
435
 
436
+ export interface HarnessPickerProps {
437
+ harnesses: HarnessOption[];
438
+ value: HarnessId;
439
+ disabled?: boolean;
440
+ adapter?: Pick<UiAdapter, 'copyText'>;
441
+ onChange(harness: HarnessId): void;
442
+ logo?: ComponentType<{ id: HarnessId; activity?: SessionActivity; size?: number }>;
443
+ }
444
+
436
445
  export interface MessengerComponents {
437
446
  SessionRow?: ComponentType<SessionRowProps>;
438
447
  TranscriptEntry?: ComponentType<TranscriptEntryProps>;
@@ -441,6 +450,7 @@ export interface MessengerComponents {
441
450
  HarnessLogo?: ComponentType<{ id: HarnessId; activity?: SessionActivity; size?: number }>;
442
451
  HarnessAdvisory?: ComponentType<HarnessAdvisoryProps>;
443
452
  HarnessSettingsPanel?: ComponentType<HarnessSettingsPanelProps>;
453
+ HarnessPicker?: ComponentType<HarnessPickerProps>;
444
454
  ContextCandidate?: ComponentType<ContextCandidateProps>;
445
455
  HarnessReadiness?: ComponentType<{ harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }>;
446
456
  }
@@ -532,6 +542,7 @@ export function ToolRow(props: ToolRowProps): VNode;
532
542
  export function TaskPlan(props: { plan: TaskPlanModel }): VNode | null;
533
543
  export function HarnessAdvisory(props: HarnessAdvisoryProps): VNode | null;
534
544
  export function HarnessSettingsPanel(props: HarnessSettingsPanelProps): VNode;
545
+ export function HarnessPicker(props: HarnessPickerProps): VNode;
535
546
  export function HarnessReadiness(props: { harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }): VNode | null;
536
547
  export function SessionDetails(props: { semantics: SessionSemanticsModel }): VNode | null;
537
548
  export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | PendingMessageModel | null; unreadAfterMessages?: number | null }): VNode;
package/messenger.mjs CHANGED
@@ -2394,6 +2394,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2394
2394
  ] }, item.id)) })
2395
2395
  ] });
2396
2396
  }
2397
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2398
+ const [open, setOpen] = useState5(false);
2399
+ const menuId = useId2();
2400
+ const root = useRef6(null);
2401
+ const trigger = useRef6(null);
2402
+ const panel = useRef6(null);
2403
+ const available = harnesses.filter((item) => item.startable);
2404
+ const unavailable = harnesses.filter((item) => !item.startable);
2405
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2406
+ const close = () => {
2407
+ setOpen(false);
2408
+ trigger.current?.focus({ preventScroll: true });
2409
+ };
2410
+ useEffect7(() => {
2411
+ if (!open) return;
2412
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2413
+ const dismiss = (event) => {
2414
+ if (event.type === "keydown" && event.key === "Escape") {
2415
+ event.preventDefault();
2416
+ close();
2417
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2418
+ setOpen(false);
2419
+ }
2420
+ };
2421
+ document.addEventListener("keydown", dismiss);
2422
+ document.addEventListener("pointerdown", dismiss, true);
2423
+ return () => {
2424
+ document.removeEventListener("keydown", dismiss);
2425
+ document.removeEventListener("pointerdown", dismiss, true);
2426
+ };
2427
+ }, [open]);
2428
+ const navigate = (event) => {
2429
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2430
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2431
+ if (!items.length) return;
2432
+ event.preventDefault();
2433
+ const current = items.indexOf(document.activeElement);
2434
+ 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;
2435
+ items[index].focus({ preventScroll: true });
2436
+ };
2437
+ const select = (id) => {
2438
+ onChange(id);
2439
+ close();
2440
+ };
2441
+ return /* @__PURE__ */ jsxs7("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2442
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2443
+ }, children: [
2444
+ /* @__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: [
2445
+ selected ? /* @__PURE__ */ jsx8(Logo, { id: selected.id, size: 20 }) : null,
2446
+ /* @__PURE__ */ jsx8("strong", { children: selected?.label ?? "No harness available" }),
2447
+ /* @__PURE__ */ jsx8(UiIcon, { name: "chevron", size: 13 })
2448
+ ] }),
2449
+ open ? /* @__PURE__ */ jsxs7("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2450
+ /* @__PURE__ */ jsx8("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2451
+ /* @__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: [
2452
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2453
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2454
+ /* @__PURE__ */ jsx8("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx8(UiIcon, { name: "check", size: 15 }) : null })
2455
+ ] }, item.id)) }),
2456
+ unavailable.length ? /* @__PURE__ */ jsxs7("details", { className: "scui-harness-manage", children: [
2457
+ /* @__PURE__ */ jsx8("summary", { children: "Manage additional harnesses" }),
2458
+ /* @__PURE__ */ jsx8("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs7("section", { children: [
2459
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2460
+ /* @__PURE__ */ jsxs7("span", { children: [
2461
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2462
+ /* @__PURE__ */ jsx8("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2463
+ ] }),
2464
+ item.repair ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2465
+ ] }, item.id)) })
2466
+ ] }) : null
2467
+ ] }) : null
2468
+ ] });
2469
+ }
2397
2470
 
2398
2471
  // src/messenger.jsx
2399
2472
  import { jsx as jsx9, jsxs as jsxs8 } from "preact/jsx-runtime";
@@ -2687,6 +2760,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2687
2760
  const startSequence = useRef7(0);
2688
2761
  const textarea = useRef7(null);
2689
2762
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2763
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2690
2764
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2691
2765
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2692
2766
  const rememberedMode = modes[harness];
@@ -2824,18 +2898,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2824
2898
  /* @__PURE__ */ jsx9("small", { children: "Choose a coding harness and send the first message." })
2825
2899
  ] }),
2826
2900
  /* @__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
2901
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2840
2902
  launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2841
2903
  /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
@@ -2864,7 +2926,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2864
2926
  return next;
2865
2927
  }) }),
2866
2928
  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) => {
2929
+ /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2868
2930
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2869
2931
  event.preventDefault();
2870
2932
  setDragging(true);
@@ -2874,7 +2936,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2874
2936
  }, onDragLeave: (event) => {
2875
2937
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2876
2938
  }, 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
2939
  /* @__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
2940
  const value = event.currentTarget.value;
2880
2941
  setDraft(value);
@@ -2885,7 +2946,14 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2885
2946
  send();
2886
2947
  }
2887
2948
  } }),
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 }) }) })
2949
+ /* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
2950
+ 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,
2951
+ /* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
2952
+ setHarness(value);
2953
+ remember({ harness: value });
2954
+ } }),
2955
+ /* @__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 }) }) })
2956
+ ] })
2889
2957
  ] })
2890
2958
  ] })
2891
2959
  ] });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.41",
3
+ "version": "0.1.42",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
@@ -2477,6 +2477,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2477
2477
  ] }, item.id)) })
2478
2478
  ] });
2479
2479
  }
2480
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2481
+ const [open, setOpen] = useState5(false);
2482
+ const menuId = useId2();
2483
+ const root = useRef6(null);
2484
+ const trigger = useRef6(null);
2485
+ const panel = useRef6(null);
2486
+ const available = harnesses.filter((item) => item.startable);
2487
+ const unavailable = harnesses.filter((item) => !item.startable);
2488
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2489
+ const close = () => {
2490
+ setOpen(false);
2491
+ trigger.current?.focus({ preventScroll: true });
2492
+ };
2493
+ useEffect7(() => {
2494
+ if (!open) return;
2495
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2496
+ const dismiss = (event) => {
2497
+ if (event.type === "keydown" && event.key === "Escape") {
2498
+ event.preventDefault();
2499
+ close();
2500
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2501
+ setOpen(false);
2502
+ }
2503
+ };
2504
+ document.addEventListener("keydown", dismiss);
2505
+ document.addEventListener("pointerdown", dismiss, true);
2506
+ return () => {
2507
+ document.removeEventListener("keydown", dismiss);
2508
+ document.removeEventListener("pointerdown", dismiss, true);
2509
+ };
2510
+ }, [open]);
2511
+ const navigate = (event) => {
2512
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2513
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2514
+ if (!items.length) return;
2515
+ event.preventDefault();
2516
+ const current = items.indexOf(document.activeElement);
2517
+ 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;
2518
+ items[index].focus({ preventScroll: true });
2519
+ };
2520
+ const select = (id) => {
2521
+ onChange(id);
2522
+ close();
2523
+ };
2524
+ return /* @__PURE__ */ jsxs8("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2525
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2526
+ }, children: [
2527
+ /* @__PURE__ */ jsxs8("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: [
2528
+ selected ? /* @__PURE__ */ jsx9(Logo, { id: selected.id, size: 20 }) : null,
2529
+ /* @__PURE__ */ jsx9("strong", { children: selected?.label ?? "No harness available" }),
2530
+ /* @__PURE__ */ jsx9(UiIcon, { name: "chevron", size: 13 })
2531
+ ] }),
2532
+ open ? /* @__PURE__ */ jsxs8("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2533
+ /* @__PURE__ */ jsx9("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2534
+ /* @__PURE__ */ jsx9("div", { className: "scui-harness-options", role: "listbox", "aria-label": "Available coding harnesses", children: available.map((item) => /* @__PURE__ */ jsxs8("button", { type: "button", role: "option", "aria-selected": item.id === selected?.id, onClick: () => select(item.id), children: [
2535
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2536
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2537
+ /* @__PURE__ */ jsx9("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx9(UiIcon, { name: "check", size: 15 }) : null })
2538
+ ] }, item.id)) }),
2539
+ unavailable.length ? /* @__PURE__ */ jsxs8("details", { className: "scui-harness-manage", children: [
2540
+ /* @__PURE__ */ jsx9("summary", { children: "Manage additional harnesses" }),
2541
+ /* @__PURE__ */ jsx9("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs8("section", { children: [
2542
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2543
+ /* @__PURE__ */ jsxs8("span", { children: [
2544
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2545
+ /* @__PURE__ */ jsx9("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2546
+ ] }),
2547
+ item.repair ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2548
+ ] }, item.id)) })
2549
+ ] }) : null
2550
+ ] }) : null
2551
+ ] });
2552
+ }
2480
2553
 
2481
2554
  // src/messenger.jsx
2482
2555
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -2770,6 +2843,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2770
2843
  const startSequence = useRef7(0);
2771
2844
  const textarea = useRef7(null);
2772
2845
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2846
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2773
2847
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2774
2848
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2775
2849
  const rememberedMode = modes[harness];
@@ -2907,18 +2981,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2907
2981
  /* @__PURE__ */ jsx10("small", { children: "Choose a coding harness and send the first message." })
2908
2982
  ] }),
2909
2983
  /* @__PURE__ */ jsxs9("div", { className: "scui-compose", children: [
2910
- /* @__PURE__ */ jsxs9("label", { className: "scui-harness-picker", children: [
2911
- /* @__PURE__ */ jsx10(HarnessLogo, { id: harness, size: 24 }),
2912
- /* @__PURE__ */ jsx10("span", { children: "Coding harness" }),
2913
- /* @__PURE__ */ jsx10("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2914
- const value = event.currentTarget.value;
2915
- setHarness(value);
2916
- remember({ harness: value });
2917
- }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs9("option", { value: item.id, disabled: !item.startable, children: [
2918
- item.label,
2919
- item.startable ? "" : " \xB7 unavailable"
2920
- ] }, item.id)) })
2921
- ] }),
2922
2984
  /* @__PURE__ */ jsx10(Readiness, { harnesses: state.harnesses, state, adapter }),
2923
2985
  launchModes.length > 1 ? /* @__PURE__ */ jsxs9("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2924
2986
  /* @__PURE__ */ jsx10("legend", { children: "Run as" }),
@@ -2947,7 +3009,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2947
3009
  return next;
2948
3010
  }) }),
2949
3011
  terminalAttachments ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2950
- /* @__PURE__ */ jsxs9("div", { className: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
3012
+ /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2951
3013
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2952
3014
  event.preventDefault();
2953
3015
  setDragging(true);
@@ -2957,7 +3019,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2957
3019
  }, onDragLeave: (event) => {
2958
3020
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2959
3021
  }, onDrop: dropImages, children: [
2960
- adapter.pickContext ? /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
2961
3022
  /* @__PURE__ */ jsx10("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) => {
2962
3023
  const value = event.currentTarget.value;
2963
3024
  setDraft(value);
@@ -2968,7 +3029,14 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2968
3029
  send();
2969
3030
  }
2970
3031
  } }),
2971
- /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "send", size: 17 }) }) })
3032
+ /* @__PURE__ */ jsxs9("footer", { className: "scui-new-envelope-controls", children: [
3033
+ adapter.pickContext ? /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "attach", size: 17 }) }) : null,
3034
+ /* @__PURE__ */ jsx10(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
3035
+ setHarness(value);
3036
+ remember({ harness: value });
3037
+ } }),
3038
+ /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("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__ */ jsx10("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx10(UiIcon, { name: "send", size: 17 }) }) })
3039
+ ] })
2972
3040
  ] })
2973
3041
  ] })
2974
3042
  ] });
@@ -3078,6 +3146,7 @@ export {
3078
3146
  Conversation,
3079
3147
  HarnessAdvisory,
3080
3148
  HarnessLogo,
3149
+ HarnessPicker,
3081
3150
  HarnessReadiness,
3082
3151
  HarnessSettingsPanel,
3083
3152
  ImageViewer,
package/react/index.d.ts CHANGED
@@ -7,6 +7,7 @@ import type {
7
7
  ContextCandidateProps,
8
8
  HarnessAdvisoryProps,
9
9
  HarnessId,
10
+ HarnessPickerProps,
10
11
  HarnessSettingsPanelProps,
11
12
  HarnessOption,
12
13
  MessengerLabels,
@@ -36,6 +37,7 @@ export type {
36
37
  ContextCandidateProps,
37
38
  HarnessAdvisoryProps,
38
39
  HarnessId,
40
+ HarnessPickerProps,
39
41
  HarnessSettingsPanelProps,
40
42
  HarnessOption,
41
43
  MessengerLabels,
@@ -66,6 +68,7 @@ export interface MessengerComponents {
66
68
  HarnessLogo?: ComponentType<{ id: HarnessId; activity?: SessionActivity; size?: number }>;
67
69
  HarnessAdvisory?: ComponentType<HarnessAdvisoryProps>;
68
70
  HarnessSettingsPanel?: ComponentType<HarnessSettingsPanelProps>;
71
+ HarnessPicker?: ComponentType<HarnessPickerProps>;
69
72
  ContextCandidate?: ComponentType<ContextCandidateProps>;
70
73
  HarnessReadiness?: ComponentType<{ harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }>;
71
74
  }
@@ -112,6 +115,7 @@ export function ToolRow(props: ToolRowProps): ReactElement;
112
115
  export function TaskPlan(props: { plan: TaskPlanModel }): ReactElement | null;
113
116
  export function HarnessAdvisory(props: HarnessAdvisoryProps): ReactElement | null;
114
117
  export function HarnessSettingsPanel(props: HarnessSettingsPanelProps): ReactElement;
118
+ export function HarnessPicker(props: HarnessPickerProps): ReactElement;
115
119
  export function HarnessReadiness(props: { harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }): ReactElement | null;
116
120
  export function SessionDetails(props: { semantics: SupercodeUiState['semantics'] }): ReactElement | null;
117
121
  export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | PendingMessageModel | null; unreadAfterMessages?: number | null }): ReactElement;
@@ -2394,6 +2394,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2394
2394
  ] }, item.id)) })
2395
2395
  ] });
2396
2396
  }
2397
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2398
+ const [open, setOpen] = useState5(false);
2399
+ const menuId = useId2();
2400
+ const root = useRef6(null);
2401
+ const trigger = useRef6(null);
2402
+ const panel = useRef6(null);
2403
+ const available = harnesses.filter((item) => item.startable);
2404
+ const unavailable = harnesses.filter((item) => !item.startable);
2405
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2406
+ const close = () => {
2407
+ setOpen(false);
2408
+ trigger.current?.focus({ preventScroll: true });
2409
+ };
2410
+ useEffect7(() => {
2411
+ if (!open) return;
2412
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2413
+ const dismiss = (event) => {
2414
+ if (event.type === "keydown" && event.key === "Escape") {
2415
+ event.preventDefault();
2416
+ close();
2417
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2418
+ setOpen(false);
2419
+ }
2420
+ };
2421
+ document.addEventListener("keydown", dismiss);
2422
+ document.addEventListener("pointerdown", dismiss, true);
2423
+ return () => {
2424
+ document.removeEventListener("keydown", dismiss);
2425
+ document.removeEventListener("pointerdown", dismiss, true);
2426
+ };
2427
+ }, [open]);
2428
+ const navigate = (event) => {
2429
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2430
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2431
+ if (!items.length) return;
2432
+ event.preventDefault();
2433
+ const current = items.indexOf(document.activeElement);
2434
+ 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;
2435
+ items[index].focus({ preventScroll: true });
2436
+ };
2437
+ const select = (id) => {
2438
+ onChange(id);
2439
+ close();
2440
+ };
2441
+ return /* @__PURE__ */ jsxs7("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2442
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2443
+ }, children: [
2444
+ /* @__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: [
2445
+ selected ? /* @__PURE__ */ jsx8(Logo, { id: selected.id, size: 20 }) : null,
2446
+ /* @__PURE__ */ jsx8("strong", { children: selected?.label ?? "No harness available" }),
2447
+ /* @__PURE__ */ jsx8(UiIcon, { name: "chevron", size: 13 })
2448
+ ] }),
2449
+ open ? /* @__PURE__ */ jsxs7("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2450
+ /* @__PURE__ */ jsx8("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2451
+ /* @__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: [
2452
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2453
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2454
+ /* @__PURE__ */ jsx8("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx8(UiIcon, { name: "check", size: 15 }) : null })
2455
+ ] }, item.id)) }),
2456
+ unavailable.length ? /* @__PURE__ */ jsxs7("details", { className: "scui-harness-manage", children: [
2457
+ /* @__PURE__ */ jsx8("summary", { children: "Manage additional harnesses" }),
2458
+ /* @__PURE__ */ jsx8("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs7("section", { children: [
2459
+ /* @__PURE__ */ jsx8(Logo, { id: item.id, size: 24 }),
2460
+ /* @__PURE__ */ jsxs7("span", { children: [
2461
+ /* @__PURE__ */ jsx8("strong", { children: item.label }),
2462
+ /* @__PURE__ */ jsx8("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2463
+ ] }),
2464
+ item.repair ? /* @__PURE__ */ jsx8("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2465
+ ] }, item.id)) })
2466
+ ] }) : null
2467
+ ] }) : null
2468
+ ] });
2469
+ }
2397
2470
 
2398
2471
  // src/messenger.jsx
2399
2472
  import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
@@ -2687,6 +2760,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2687
2760
  const startSequence = useRef7(0);
2688
2761
  const textarea = useRef7(null);
2689
2762
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2763
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2690
2764
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2691
2765
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2692
2766
  const rememberedMode = modes[harness];
@@ -2824,18 +2898,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2824
2898
  /* @__PURE__ */ jsx9("small", { children: "Choose a coding harness and send the first message." })
2825
2899
  ] }),
2826
2900
  /* @__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
2901
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2840
2902
  launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2841
2903
  /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
@@ -2864,7 +2926,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2864
2926
  return next;
2865
2927
  }) }),
2866
2928
  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) => {
2929
+ /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2868
2930
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2869
2931
  event.preventDefault();
2870
2932
  setDragging(true);
@@ -2874,7 +2936,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2874
2936
  }, onDragLeave: (event) => {
2875
2937
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2876
2938
  }, 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
2939
  /* @__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
2940
  const value = event.currentTarget.value;
2880
2941
  setDraft(value);
@@ -2885,7 +2946,14 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2885
2946
  send();
2886
2947
  }
2887
2948
  } }),
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 }) }) })
2949
+ /* @__PURE__ */ jsxs8("footer", { className: "scui-new-envelope-controls", children: [
2950
+ 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,
2951
+ /* @__PURE__ */ jsx9(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
2952
+ setHarness(value);
2953
+ remember({ harness: value });
2954
+ } }),
2955
+ /* @__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 }) }) })
2956
+ ] })
2889
2957
  ] })
2890
2958
  ] })
2891
2959
  ] });
@@ -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
  }