@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.
@@ -1317,7 +1317,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1317
1317
  ] })
1318
1318
  ] });
1319
1319
  }
1320
- function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
1320
+ 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 }) {
1321
1321
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
1322
1322
  const [draft, setDraft] = useState2(remembered.draft);
1323
1323
  const [context, setContext] = useState2(remembered.context ?? []);
@@ -1329,6 +1329,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1329
1329
  const [dragging, setDragging] = useState2(false);
1330
1330
  const [pickerError, setPickerError] = useState2(null);
1331
1331
  const textarea = useRef2(null);
1332
+ const lastCommand = useRef2(null);
1332
1333
  useAutosizeTextarea(textarea, draft);
1333
1334
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
1334
1335
  useEffect2(() => {
@@ -1370,6 +1371,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
1370
1371
  textarea.current?.focus({ preventScroll: true });
1371
1372
  onDraftRestored?.(restoreDraft.id);
1372
1373
  }, [onDraftRestored, restoreDraft?.id]);
1374
+ useEffect2(() => {
1375
+ if (!command || command.id === lastCommand.current) return;
1376
+ lastCommand.current = command.id;
1377
+ if (command.action === "attach") {
1378
+ try {
1379
+ const attachments = partitionAttachments(command.attachments);
1380
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
1381
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
1382
+ const nextContext = mergeContext(context, attachments.context);
1383
+ const nextImages = mergeImages(images, attachments.images);
1384
+ setContext(nextContext);
1385
+ setImages(nextImages);
1386
+ setPickerError(null);
1387
+ remember(draft, nextContext, nextImages, queue);
1388
+ } catch (error) {
1389
+ setPickerError(error instanceof Error ? error.message : "Could not attach context.");
1390
+ }
1391
+ }
1392
+ textarea.current?.focus({ preventScroll: true });
1393
+ }, [command?.id]);
1373
1394
  useEffect2(() => {
1374
1395
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
1375
1396
  return () => clearTimeout(timer);
@@ -2477,6 +2498,79 @@ function HarnessReadiness({ harnesses, adapter }) {
2477
2498
  ] }, item.id)) })
2478
2499
  ] });
2479
2500
  }
2501
+ function HarnessPicker({ harnesses, value, disabled = false, adapter, onChange, logo: Logo = HarnessLogo }) {
2502
+ const [open, setOpen] = useState5(false);
2503
+ const menuId = useId2();
2504
+ const root = useRef6(null);
2505
+ const trigger = useRef6(null);
2506
+ const panel = useRef6(null);
2507
+ const available = harnesses.filter((item) => item.startable);
2508
+ const unavailable = harnesses.filter((item) => !item.startable);
2509
+ const selected = available.find((item) => item.id === value) ?? available[0] ?? null;
2510
+ const close = () => {
2511
+ setOpen(false);
2512
+ trigger.current?.focus({ preventScroll: true });
2513
+ };
2514
+ useEffect7(() => {
2515
+ if (!open) return;
2516
+ panel.current?.querySelector('[aria-selected="true"], [role="option"]')?.focus({ preventScroll: true });
2517
+ const dismiss = (event) => {
2518
+ if (event.type === "keydown" && event.key === "Escape") {
2519
+ event.preventDefault();
2520
+ close();
2521
+ } else if (event.type === "pointerdown" && !root.current?.contains(event.target)) {
2522
+ setOpen(false);
2523
+ }
2524
+ };
2525
+ document.addEventListener("keydown", dismiss);
2526
+ document.addEventListener("pointerdown", dismiss, true);
2527
+ return () => {
2528
+ document.removeEventListener("keydown", dismiss);
2529
+ document.removeEventListener("pointerdown", dismiss, true);
2530
+ };
2531
+ }, [open]);
2532
+ const navigate = (event) => {
2533
+ if (!["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) return;
2534
+ const items = [...panel.current.querySelectorAll('[role="option"]')];
2535
+ if (!items.length) return;
2536
+ event.preventDefault();
2537
+ const current = items.indexOf(document.activeElement);
2538
+ 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;
2539
+ items[index].focus({ preventScroll: true });
2540
+ };
2541
+ const select = (id) => {
2542
+ onChange(id);
2543
+ close();
2544
+ };
2545
+ return /* @__PURE__ */ jsxs8("div", { className: "scui-harness-menu", ref: root, onBlur: (event) => {
2546
+ if (event.relatedTarget && !event.currentTarget.contains(event.relatedTarget)) setOpen(false);
2547
+ }, children: [
2548
+ /* @__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: [
2549
+ selected ? /* @__PURE__ */ jsx9(Logo, { id: selected.id, size: 20 }) : null,
2550
+ /* @__PURE__ */ jsx9("strong", { children: selected?.label ?? "No harness available" }),
2551
+ /* @__PURE__ */ jsx9(UiIcon, { name: "chevron", size: 13 })
2552
+ ] }),
2553
+ open ? /* @__PURE__ */ jsxs8("div", { ref: panel, id: menuId, className: "scui-harness-popover", role: "dialog", "aria-label": "Choose coding harness", onKeyDown: navigate, children: [
2554
+ /* @__PURE__ */ jsx9("strong", { className: "scui-harness-popover-title", children: "Choose coding harness" }),
2555
+ /* @__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: [
2556
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2557
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2558
+ /* @__PURE__ */ jsx9("span", { children: item.id === selected?.id ? /* @__PURE__ */ jsx9(UiIcon, { name: "check", size: 15 }) : null })
2559
+ ] }, item.id)) }),
2560
+ unavailable.length ? /* @__PURE__ */ jsxs8("details", { className: "scui-harness-manage", children: [
2561
+ /* @__PURE__ */ jsx9("summary", { children: "Manage additional harnesses" }),
2562
+ /* @__PURE__ */ jsx9("div", { children: unavailable.map((item) => /* @__PURE__ */ jsxs8("section", { children: [
2563
+ /* @__PURE__ */ jsx9(Logo, { id: item.id, size: 24 }),
2564
+ /* @__PURE__ */ jsxs8("span", { children: [
2565
+ /* @__PURE__ */ jsx9("strong", { children: item.label }),
2566
+ /* @__PURE__ */ jsx9("small", { children: item.reason || (item.auth === "required" ? "Authentication required" : "Unavailable") })
2567
+ ] }),
2568
+ item.repair ? /* @__PURE__ */ jsx9("button", { type: "button", onClick: () => adapter?.copyText?.(item.repair), children: "Copy fix" }) : null
2569
+ ] }, item.id)) })
2570
+ ] }) : null
2571
+ ] }) : null
2572
+ ] });
2573
+ }
2480
2574
 
2481
2575
  // src/messenger.jsx
2482
2576
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
@@ -2623,7 +2717,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2623
2717
  onClose ? /* @__PURE__ */ jsx10("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx10(UiIcon, { name: "close", size: 18 }) }) : null
2624
2718
  ] });
2625
2719
  }
2626
- function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation }) {
2720
+ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels, contextCandidates, navigation, composerCommand }) {
2627
2721
  const Header = slots.header;
2628
2722
  const HeaderActions = slots.headerActions;
2629
2723
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
@@ -2750,11 +2844,11 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2750
2844
  /* @__PURE__ */ jsx10(Conversation, { state: actionState, adapter: trackedAdapter, components, slots, memoryKey, pending: pendingMessage, unreadAfterMessages }, memoryKey),
2751
2845
  /* @__PURE__ */ jsx10(ContinuationBar, { state: actionState, adapter: trackedAdapter, labels }),
2752
2846
  /* @__PURE__ */ jsx10(Advisory, { state: actionState, adapter: trackedAdapter, value: state.interopSettings?.advisories[0] ?? null, onReview: (recommendedChange) => setSettings({ recommendedChange }) }),
2753
- state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(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,
2847
+ state.mode !== "mirror" || state.canSend ? /* @__PURE__ */ jsx10(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,
2754
2848
  settings ? /* @__PURE__ */ jsx10(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
2755
2849
  ] });
2756
2850
  }
2757
- function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation }) {
2851
+ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions, contextCandidates, components, navigation, composerCommand }) {
2758
2852
  const startable = state.harnesses.filter((item) => item.startable);
2759
2853
  const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
2760
2854
  const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
@@ -2768,8 +2862,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2768
2862
  const [dragging, setDragging] = useState6(false);
2769
2863
  const [pickerError, setPickerError] = useState6(null);
2770
2864
  const startSequence = useRef7(0);
2865
+ const lastComposerCommand = useRef7(null);
2771
2866
  const textarea = useRef7(null);
2772
2867
  const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2868
+ const Picker = components.HarnessPicker ?? HarnessPicker;
2773
2869
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2774
2870
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2775
2871
  const rememberedMode = modes[harness];
@@ -2810,6 +2906,32 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2810
2906
  remember({ harness: nextHarness, draft: nextDraft, context: nextContext, images: nextImages });
2811
2907
  textarea.current?.focus({ preventScroll: true });
2812
2908
  }, [navigation?.id]);
2909
+ useEffect8(() => {
2910
+ if (!composerCommand || composerCommand.id === lastComposerCommand.current) return;
2911
+ lastComposerCommand.current = composerCommand.id;
2912
+ if (composerCommand.action === "attach") {
2913
+ if (mode === "terminal" && !launchModes.includes("headless")) {
2914
+ setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2915
+ } else {
2916
+ try {
2917
+ const attachments = partitionAttachments(composerCommand.attachments);
2918
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
2919
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2920
+ const nextContext = mergeContext(context, attachments.context);
2921
+ const nextImages = mergeImages(images, attachments.images);
2922
+ const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2923
+ if (nextModes !== modes) setModes(nextModes);
2924
+ setContext(nextContext);
2925
+ setImages(nextImages);
2926
+ setPickerError(null);
2927
+ remember({ context: nextContext, images: nextImages, modes: nextModes });
2928
+ } catch (error) {
2929
+ setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2930
+ }
2931
+ }
2932
+ }
2933
+ textarea.current?.focus({ preventScroll: true });
2934
+ }, [composerCommand?.id]);
2813
2935
  const pickContext = () => {
2814
2936
  if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2815
2937
  setPicking(true);
@@ -2907,18 +3029,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2907
3029
  /* @__PURE__ */ jsx10("small", { children: "Choose a coding harness and send the first message." })
2908
3030
  ] }),
2909
3031
  /* @__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
3032
  /* @__PURE__ */ jsx10(Readiness, { harnesses: state.harnesses, state, adapter }),
2923
3033
  launchModes.length > 1 ? /* @__PURE__ */ jsxs9("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2924
3034
  /* @__PURE__ */ jsx10("legend", { children: "Run as" }),
@@ -2947,7 +3057,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2947
3057
  return next;
2948
3058
  }) }),
2949
3059
  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) => {
3060
+ /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2951
3061
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2952
3062
  event.preventDefault();
2953
3063
  setDragging(true);
@@ -2957,7 +3067,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2957
3067
  }, onDragLeave: (event) => {
2958
3068
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2959
3069
  }, 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
3070
  /* @__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
3071
  const value = event.currentTarget.value;
2963
3072
  setDraft(value);
@@ -2968,12 +3077,19 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2968
3077
  send();
2969
3078
  }
2970
3079
  } }),
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 }) }) })
3080
+ /* @__PURE__ */ jsxs9("footer", { className: "scui-new-envelope-controls", children: [
3081
+ 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,
3082
+ /* @__PURE__ */ jsx10(Picker, { harnesses: state.harnesses, value: harness, disabled: Boolean(starting), adapter, logo: components.HarnessLogo, onChange: (value) => {
3083
+ setHarness(value);
3084
+ remember({ harness: value });
3085
+ } }),
3086
+ /* @__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 }) }) })
3087
+ ] })
2972
3088
  ] })
2973
3089
  ] })
2974
3090
  ] });
2975
3091
  }
2976
- function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
3092
+ function SupercodeMessenger({ state: stateInput, adapter, class: className = "", initialView, navigation = null, composerCommand = null, onViewChange, contextCandidates: candidatesInput = [], labels, components = {}, slots = {} }) {
2977
3093
  const state = useMemo3(() => normalizeUiState(stateInput), [stateInput]);
2978
3094
  const contextCandidates = useMemo3(() => normalizeAttachmentCandidates(candidatesInput), [candidatesInput]);
2979
3095
  const copy = { ...DEFAULT_LABELS, ...labels };
@@ -3044,7 +3160,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
3044
3160
  setNewNavigation(null);
3045
3161
  setView("new");
3046
3162
  }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, slots, headerActions: HeaderActions }) : null,
3047
- view === "new" ? /* @__PURE__ */ jsx10(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,
3163
+ view === "new" ? /* @__PURE__ */ jsx10(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,
3048
3164
  view === "chat" ? /* @__PURE__ */ jsx10(Chat, { state, adapter, onBack: () => {
3049
3165
  setListFocus(state.attached?.key ?? listFocus);
3050
3166
  setView("list");
@@ -3053,7 +3169,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
3053
3169
  setChatNavigation(null);
3054
3170
  setNewNavigation(null);
3055
3171
  setView("new");
3056
- }, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null }) : null,
3172
+ }, onClose: adapter.onClose ? close : void 0, components, slots, labels: copy, contextCandidates, navigation: chatNavigation?.sessionKey === state.attached?.key ? chatNavigation : null, composerCommand }) : null,
3057
3173
  opening ? /* @__PURE__ */ jsxs9("div", { className: "scui-opening", role: "status", "aria-busy": "true", children: [
3058
3174
  /* @__PURE__ */ jsx10(HarnessLogo, { id: opening.harness, size: 34 }),
3059
3175
  /* @__PURE__ */ jsxs9("span", { children: [
@@ -3078,6 +3194,7 @@ export {
3078
3194
  Conversation,
3079
3195
  HarnessAdvisory,
3080
3196
  HarnessLogo,
3197
+ HarnessPicker,
3081
3198
  HarnessReadiness,
3082
3199
  HarnessSettingsPanel,
3083
3200
  ImageViewer,
@@ -318,7 +318,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
318
318
  ] })
319
319
  ] });
320
320
  }
321
- function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, contextCandidates = [], components = {}, onPending, onDraftRestored }) {
321
+ 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 }) {
322
322
  const remembered = composerMemory.get(memoryKey) ?? { draft: state.savedDraft, context: [], images: [], queue: [] };
323
323
  const [draft, setDraft] = useState2(remembered.draft);
324
324
  const [context, setContext] = useState2(remembered.context ?? []);
@@ -330,6 +330,7 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
330
330
  const [dragging, setDragging] = useState2(false);
331
331
  const [pickerError, setPickerError] = useState2(null);
332
332
  const textarea = useRef2(null);
333
+ const lastCommand = useRef2(null);
333
334
  useAutosizeTextarea(textarea, draft);
334
335
  const remember = (nextDraft, nextContext, nextImages, nextQueue) => boundedSet(composerMemory, memoryKey, { draft: nextDraft, context: nextContext, images: nextImages, queue: nextQueue });
335
336
  useEffect2(() => {
@@ -371,6 +372,26 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
371
372
  textarea.current?.focus({ preventScroll: true });
372
373
  onDraftRestored?.(restoreDraft.id);
373
374
  }, [onDraftRestored, restoreDraft?.id]);
375
+ useEffect2(() => {
376
+ if (!command || command.id === lastCommand.current) return;
377
+ lastCommand.current = command.id;
378
+ if (command.action === "attach") {
379
+ try {
380
+ const attachments = partitionAttachments(command.attachments);
381
+ if (context.length + attachments.context.length > MAX_CONTEXT_ITEMS) throw new Error(`Attach at most ${MAX_CONTEXT_ITEMS} text items.`);
382
+ if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
383
+ const nextContext = mergeContext(context, attachments.context);
384
+ const nextImages = mergeImages(images, attachments.images);
385
+ setContext(nextContext);
386
+ setImages(nextImages);
387
+ setPickerError(null);
388
+ remember(draft, nextContext, nextImages, queue);
389
+ } catch (error) {
390
+ setPickerError(error instanceof Error ? error.message : "Could not attach context.");
391
+ }
392
+ }
393
+ textarea.current?.focus({ preventScroll: true });
394
+ }, [command?.id]);
374
395
  useEffect2(() => {
375
396
  const timer = setTimeout(() => adapter.onIntent({ action: "draft", text: draft }), 250);
376
397
  return () => clearTimeout(timer);
package/react/index.d.ts CHANGED
@@ -7,9 +7,11 @@ import type {
7
7
  ContextCandidateProps,
8
8
  HarnessAdvisoryProps,
9
9
  HarnessId,
10
+ HarnessPickerProps,
10
11
  HarnessSettingsPanelProps,
11
12
  HarnessOption,
12
13
  MessengerLabels,
14
+ MessengerComposerCommand,
13
15
  MessengerNavigation,
14
16
  PendingMessageModel,
15
17
  SessionActivity,
@@ -36,9 +38,11 @@ export type {
36
38
  ContextCandidateProps,
37
39
  HarnessAdvisoryProps,
38
40
  HarnessId,
41
+ HarnessPickerProps,
39
42
  HarnessSettingsPanelProps,
40
43
  HarnessOption,
41
44
  MessengerLabels,
45
+ MessengerComposerCommand,
42
46
  MessengerNavigation,
43
47
  PendingMessageModel,
44
48
  SessionActivity,
@@ -66,6 +70,7 @@ export interface MessengerComponents {
66
70
  HarnessLogo?: ComponentType<{ id: HarnessId; activity?: SessionActivity; size?: number }>;
67
71
  HarnessAdvisory?: ComponentType<HarnessAdvisoryProps>;
68
72
  HarnessSettingsPanel?: ComponentType<HarnessSettingsPanelProps>;
73
+ HarnessPicker?: ComponentType<HarnessPickerProps>;
69
74
  ContextCandidate?: ComponentType<ContextCandidateProps>;
70
75
  HarnessReadiness?: ComponentType<{ harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }>;
71
76
  }
@@ -87,6 +92,7 @@ export interface MessengerProps {
87
92
  class?: string;
88
93
  initialView?: 'list' | 'chat' | 'new';
89
94
  navigation?: MessengerNavigation | null;
95
+ composerCommand?: MessengerComposerCommand | null;
90
96
  onViewChange?(view: 'list' | 'chat' | 'new'): void;
91
97
  contextCandidates?: TranscriptAttachment[];
92
98
  labels?: Partial<MessengerLabels>;
@@ -112,11 +118,12 @@ export function ToolRow(props: ToolRowProps): ReactElement;
112
118
  export function TaskPlan(props: { plan: TaskPlanModel }): ReactElement | null;
113
119
  export function HarnessAdvisory(props: HarnessAdvisoryProps): ReactElement | null;
114
120
  export function HarnessSettingsPanel(props: HarnessSettingsPanelProps): ReactElement;
121
+ export function HarnessPicker(props: HarnessPickerProps): ReactElement;
115
122
  export function HarnessReadiness(props: { harnesses: HarnessOption[]; state: SupercodeUiState; adapter: UiAdapter }): ReactElement | null;
116
123
  export function SessionDetails(props: { semantics: SupercodeUiState['semantics'] }): ReactElement | null;
117
124
  export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | PendingMessageModel | null; unreadAfterMessages?: number | null }): ReactElement;
118
125
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): ReactElement;
119
126
  export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; slots?: MessengerSlots; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string; headerActions?: MessengerSlots['headerActions'] }): ReactElement;
120
127
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): ReactElement | null;
121
- export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): ReactElement;
128
+ export function Composer(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels; memoryKey?: string; pendingStatus?: PendingMessageModel['status'] | 'editing' | null; restoreDraft?: { id: string | number; text: string; context?: TranscriptContext[]; images?: TranscriptImage[] } | null; command?: MessengerComposerCommand | null; contextCandidates?: TranscriptAttachment[]; components?: Pick<MessengerComponents, 'ContextCandidate'>; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): ReactElement;
122
129
  export function SupercodeMessenger(props: MessengerProps): ReactElement;