@volter-ai-dev/supercode-ui 0.1.32 → 0.1.34

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
@@ -103,6 +103,13 @@ Native continuation is headless by default. A host with a real terminal provider
103
103
  that strategy beside “Continue here.” The UI never infers terminal support from a generic runtime
104
104
  handoff, and never presents a terminal strategy when the host cannot create one.
105
105
 
106
+ New-session execution is advertised per harness. Set `launchModes: ['headless', 'terminal']` and
107
+ `preferredLaunchMode` on a `HarnessOption`; the complete messenger renders a compact “Chat” versus
108
+ “Terminal” choice and emits that `mode` on the `new` intent. Direct controller bindings keep Chat as
109
+ the standard path and delegate Terminal only through `onStartTerminal`, so a terminal selection can
110
+ never accidentally create a second headless runtime. Hosts may remember the preference per harness;
111
+ the messenger also retains the current browser draft choice while its New Chat view is open.
112
+
106
113
  The default controller projection is display-bounded: 120 visible transcript rows, at most 480
107
114
  native entries inspected to fill that tail, 16,000 characters per independent entry field, 100
108
115
  session rows, 20 fidelity-residue details, and 50 subagents. It filters harness-injected context
@@ -116,6 +123,8 @@ errors, and owned/attached identities without reimplementing transcript or capab
116
123
  - Every component accepts data and callbacks. No component reaches into a global controller.
117
124
  - The complete messenger accepts `slots` for high-level replacement and `components` for row-level
118
125
  replacement. Replacements receive the same typed, capability-filtered props as defaults.
126
+ - `slots.headerActions` adds compact product controls to the default list, chat, and new-chat
127
+ headers without replacing their navigation, status, or accessibility behavior.
119
128
  - Stable `scui-*` classes and `data-*` attributes support additive styling; CSS custom properties
120
129
  are the supported theme API.
121
130
  - `styles.css` contains both neutral tokens and default component rules. Teams may load it whole,
package/components.mjs CHANGED
@@ -744,7 +744,11 @@ function normalizeUiState(value) {
744
744
  recoverable: raw.recoverable === true,
745
745
  harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
746
746
  const item = record(candidate);
747
- return item && typeof item.id === "string" ? [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable: item.startable === true, reason: typeof item.reason === "string" ? item.reason : null }] : [];
747
+ if (!item || typeof item.id !== "string") return [];
748
+ const startable = item.startable === true;
749
+ const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
750
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
751
+ return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
748
752
  }) : [],
749
753
  history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
750
754
  savedDraft: string(raw.savedDraft),
@@ -829,7 +833,7 @@ function activitySummary(entries) {
829
833
  function canContinueHere(state) {
830
834
  if (state.mode !== "mirror" || state.canSend) return false;
831
835
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
832
- return state.canResume && row?.runtimeStatus !== "running" && row?.runtimeStatus !== "busy" && row?.runtimeStatus !== "idle";
836
+ return Boolean(row) && state.canResume && row.runtimeStatus !== "running" && row.runtimeStatus !== "busy" && row.runtimeStatus !== "idle";
833
837
  }
834
838
  function operationLabel(operation) {
835
839
  if (!operation) return "";
@@ -2126,7 +2130,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2126
2130
  ] })
2127
2131
  ] });
2128
2132
  }
2129
- function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
2133
+ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default", headerActions: HeaderActions }) {
2130
2134
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
2131
2135
  const [query, setQuery] = useState4(remembered.query);
2132
2136
  const [loadingMore, setLoadingMore] = useState4(false);
@@ -2167,6 +2171,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
2167
2171
  " recent conversations"
2168
2172
  ] })
2169
2173
  ] }),
2174
+ HeaderActions ? /* @__PURE__ */ jsx7(HeaderActions, { state, adapter, value: "list" }) : null,
2170
2175
  /* @__PURE__ */ jsx7("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx7(UiIcon, { name: "plus", size: 18 }) }),
2171
2176
  onClose ? /* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx7(UiIcon, { name: "close", size: 18 }) }) : null
2172
2177
  ] }),
@@ -2405,7 +2410,7 @@ function ConversationActions({ state, adapter, actionPending, onSettings }) {
2405
2410
  ] }, group.label)) }) : null
2406
2411
  ] });
2407
2412
  }
2408
- function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings }) {
2413
+ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings, headerActions: HeaderActions }) {
2409
2414
  const back = useRef7(null);
2410
2415
  const harness = state.attached?.harness ?? state.harness;
2411
2416
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
@@ -2425,6 +2430,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2425
2430
  status
2426
2431
  ] })
2427
2432
  ] }),
2433
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "chat" }) : null,
2428
2434
  menu ? /* @__PURE__ */ jsx9(ConversationActions, { state, adapter, actionPending, onSettings }) : null,
2429
2435
  /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "New chat", onClick: onNew, children: /* @__PURE__ */ jsx9(UiIcon, { name: "plus", size: 18 }) }),
2430
2436
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
@@ -2432,6 +2438,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2432
2438
  }
2433
2439
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2434
2440
  const Header = slots.header;
2441
+ const HeaderActions = slots.headerActions;
2435
2442
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2436
2443
  const [pending, setPendingState] = useState6(() => pendingMessageMemory.get(memoryKey) ?? null);
2437
2444
  const [pendingAction, setPendingAction] = useState6(null);
@@ -2531,7 +2538,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2531
2538
  const Advisory = components.HarnessAdvisory ?? HarnessAdvisory;
2532
2539
  const SettingsPanel = components.HarnessSettingsPanel ?? HarnessSettingsPanel;
2533
2540
  return /* @__PURE__ */ jsxs8("section", { class: "scui-chat", children: [
2534
- Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }) }),
2541
+ Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }), headerActions: HeaderActions }),
2535
2542
  actionLabel ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2536
2543
  /* @__PURE__ */ jsx9("i", {}),
2537
2544
  actionLabel
@@ -2548,34 +2555,41 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2548
2555
  settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
2549
2556
  ] });
2550
2557
  }
2551
- function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2558
+ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions }) {
2552
2559
  const startable = state.harnesses.filter((item) => item.startable);
2553
- const startableKey = startable.map((item) => item.id).join("\0");
2554
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2560
+ const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
2561
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
2555
2562
  const [harness, setHarness] = useState6(remembered.harness);
2556
2563
  const [draft, setDraft] = useState6(remembered.draft);
2557
2564
  const [context, setContext] = useState6(remembered.context);
2558
2565
  const [images, setImages] = useState6(remembered.images ?? []);
2566
+ const [modes, setModes] = useState6(remembered.modes ?? {});
2559
2567
  const [starting, setStarting] = useState6(null);
2560
2568
  const [picking, setPicking] = useState6(false);
2561
2569
  const [dragging, setDragging] = useState6(false);
2562
2570
  const [pickerError, setPickerError] = useState6(null);
2563
2571
  const startSequence = useRef7(0);
2564
2572
  const textarea = useRef7(null);
2573
+ const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2574
+ const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2575
+ const rememberedMode = modes[harness];
2576
+ const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2577
+ const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2578
+ const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2565
2579
  useAutosizeTextarea(textarea, draft);
2566
2580
  useEffect8(() => {
2567
2581
  if (startable.some((item) => item.id === harness)) return;
2568
2582
  const next = startable[0]?.id ?? "";
2569
2583
  setHarness(next);
2570
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2584
+ remember({ harness: next });
2571
2585
  }, [harness, startableKey]);
2572
2586
  useEffect8(() => {
2573
- if (!starting) return;
2587
+ if (!starting || starting.mode === "terminal") return;
2574
2588
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2575
2589
  const beganWorking = !starting.busy && state.busy;
2576
2590
  if (!sessionChanged && !beganWorking) return;
2577
2591
  newChatMemory.delete(memoryKey);
2578
- onStarted();
2592
+ onStarted("headless");
2579
2593
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2580
2594
  useEffect8(() => {
2581
2595
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
@@ -2584,7 +2598,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2584
2598
  textarea.current?.focus({ preventScroll: true });
2585
2599
  }, []);
2586
2600
  const pickContext = () => {
2587
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2601
+ if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2588
2602
  setPicking(true);
2589
2603
  setPickerError(null);
2590
2604
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
@@ -2593,12 +2607,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2593
2607
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2594
2608
  setContext((current) => {
2595
2609
  const next = mergeContext(current, attachments.context);
2596
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2610
+ remember({ context: next });
2597
2611
  return next;
2598
2612
  });
2599
2613
  setImages((current) => {
2600
2614
  const next = mergeImages(current, attachments.images);
2601
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2615
+ remember({ images: next });
2602
2616
  return next;
2603
2617
  });
2604
2618
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
@@ -2606,6 +2620,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2606
2620
  const addImageFiles = (value, source) => {
2607
2621
  const allFiles = Array.from(value ?? []);
2608
2622
  if (!allFiles.length) return false;
2623
+ if (mode === "terminal") {
2624
+ setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2625
+ return true;
2626
+ }
2609
2627
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
2610
2628
  if (files.length !== allFiles.length) {
2611
2629
  setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
@@ -2619,7 +2637,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2619
2637
  setPickerError(null);
2620
2638
  imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2621
2639
  const next = mergeImages(current, picked);
2622
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2640
+ remember({ images: next });
2623
2641
  return next;
2624
2642
  }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2625
2643
  return true;
@@ -2633,12 +2651,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2633
2651
  };
2634
2652
  const send = () => {
2635
2653
  const text = draft.trim();
2636
- if (!text && !images.length || !harness || starting) return;
2654
+ if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
2637
2655
  const id = startSequence.current + 1;
2638
2656
  startSequence.current = id;
2639
- setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2640
- const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2641
- if (result && typeof result.then === "function") {
2657
+ setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2658
+ const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2659
+ if (mode === "terminal") {
2660
+ Promise.resolve(result).then(() => {
2661
+ newChatMemory.delete(memoryKey);
2662
+ onStarted("terminal");
2663
+ }, () => setStarting((current) => current?.id === id ? null : current));
2664
+ } else if (result && typeof result.then === "function") {
2642
2665
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2643
2666
  }
2644
2667
  };
@@ -2649,11 +2672,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2649
2672
  /* @__PURE__ */ jsx9("strong", { children: labels.newChat }),
2650
2673
  /* @__PURE__ */ jsx9("small", { children: "No session is created until you send" })
2651
2674
  ] }),
2675
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "new" }) : null,
2652
2676
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
2653
2677
  ] }),
2654
2678
  starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2655
2679
  /* @__PURE__ */ jsx9("i", {}),
2656
- operationLabel("start")
2680
+ mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
2657
2681
  ] }) : null,
2658
2682
  /* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
2659
2683
  /* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
@@ -2667,23 +2691,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2667
2691
  /* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2668
2692
  const value = event.currentTarget.value;
2669
2693
  setHarness(value);
2670
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2694
+ remember({ harness: value });
2671
2695
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
2672
2696
  item.label,
2673
2697
  item.startable ? "" : " \xB7 unavailable"
2674
2698
  ] }, item.id)) })
2675
2699
  ] }),
2700
+ launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
2701
+ /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2702
+ launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2703
+ /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2704
+ const next = { ...modes, [harness]: item };
2705
+ setModes(next);
2706
+ setPickerError(null);
2707
+ remember({ modes: next });
2708
+ } }),
2709
+ /* @__PURE__ */ jsxs8("span", { children: [
2710
+ /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2711
+ /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2712
+ ] })
2713
+ ] }, item))
2714
+ ] }) : null,
2676
2715
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2677
2716
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2678
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2717
+ remember({ images: next });
2679
2718
  return next;
2680
2719
  }) }),
2681
2720
  /* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2682
2721
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2683
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2722
+ remember({ context: next });
2684
2723
  return next;
2685
2724
  }) }),
2686
- pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2725
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2687
2726
  /* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2688
2727
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2689
2728
  event.preventDefault();
@@ -2694,18 +2733,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2694
2733
  }, onDragLeave: (event) => {
2695
2734
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2696
2735
  }, onDrop: dropImages, children: [
2697
- adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2736
+ adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2698
2737
  /* @__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) => {
2699
2738
  const value = event.currentTarget.value;
2700
2739
  setDraft(value);
2701
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2740
+ remember({ draft: value });
2702
2741
  }, onKeyDown: (event) => {
2703
2742
  if (isSendKey(event)) {
2704
2743
  event.preventDefault();
2705
2744
  send();
2706
2745
  }
2707
2746
  } }),
2708
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2747
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2709
2748
  ] })
2710
2749
  ] })
2711
2750
  ] });
@@ -2737,12 +2776,13 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2737
2776
  };
2738
2777
  const close = () => adapter.onClose?.();
2739
2778
  const Footer = slots.footer;
2779
+ const HeaderActions = slots.headerActions;
2740
2780
  return /* @__PURE__ */ jsxs8("main", { class: `scui-root ${className}`, "data-view": view, "data-mode": state.mode, "aria-label": "Supercode messenger", children: [
2741
2781
  view === "list" ? /* @__PURE__ */ jsx9(SessionList, { state, adapter, focusKey: listFocus, onOpen: open, onNew: () => {
2742
2782
  setListFocus("@new");
2743
2783
  setView("new");
2744
- }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
2745
- view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
2784
+ }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2785
+ view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2746
2786
  view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
2747
2787
  setListFocus(state.attached?.key ?? listFocus);
2748
2788
  setView("list");
package/composer.mjs CHANGED
@@ -70,7 +70,7 @@ function harnessDisplayName(id) {
70
70
  function canContinueHere(state) {
71
71
  if (state.mode !== "mirror" || state.canSend) return false;
72
72
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
73
- return state.canResume && row?.runtimeStatus !== "running" && row?.runtimeStatus !== "busy" && row?.runtimeStatus !== "idle";
73
+ return Boolean(row) && state.canResume && row.runtimeStatus !== "running" && row.runtimeStatus !== "busy" && row.runtimeStatus !== "idle";
74
74
  }
75
75
  function isSendKey(event) {
76
76
  return event.key === "Enter" && !event.shiftKey && !event.isComposing;
package/controller.d.ts CHANGED
@@ -84,6 +84,7 @@ export interface ControllerBindingOptions {
84
84
  onAcknowledge?: (key: string) => void | Promise<void>;
85
85
  onLoadSessions?: () => void | Promise<void>;
86
86
  onLoadEarlier?: () => void | Promise<void>;
87
+ onStartTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'new' }>) => void | Promise<void>;
87
88
  onResumeTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'resume' }>) => void | Promise<void>;
88
89
  copyText?: UiAdapter['copyText'];
89
90
  resolveImage?: UiAdapter['resolveImage'];
package/controller.mjs CHANGED
@@ -447,6 +447,11 @@ async function dispatchStandard(controller, intent, options) {
447
447
  if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
448
448
  if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
449
449
  if (intent.action === 'new') {
450
+ if (intent.mode === 'terminal') {
451
+ return options.onStartTerminal
452
+ ? options.onStartTerminal(intent)
453
+ : options.onUnsupported?.(intent);
454
+ }
450
455
  await controller.dispatch({ type: 'start', harness: intent.harness });
451
456
  return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
452
457
  }
package/core.mjs CHANGED
@@ -796,7 +796,15 @@ export function normalizeUiState(value) {
796
796
  recoverable: raw.recoverable === true,
797
797
  harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
798
798
  const item = record(candidate);
799
- return item && typeof item.id === 'string' ? [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable: item.startable === true, reason: typeof item.reason === 'string' ? item.reason : null }] : [];
799
+ if (!item || typeof item.id !== 'string') return [];
800
+ const startable = item.startable === true;
801
+ const launchModes = Array.isArray(item.launchModes)
802
+ ? [...new Set(item.launchModes.filter((mode) => mode === 'headless' || mode === 'terminal'))]
803
+ : startable ? ['headless'] : [];
804
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode)
805
+ ? item.preferredLaunchMode
806
+ : launchModes[0] ?? null;
807
+ return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === 'string' ? item.reason : null, launchModes, preferredLaunchMode }];
800
808
  }) : [],
801
809
  history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
802
810
  savedDraft: string(raw.savedDraft),
@@ -895,7 +903,7 @@ export function activitySummary(entries) {
895
903
  export function canContinueHere(state) {
896
904
  if (state.mode !== 'mirror' || state.canSend) return false;
897
905
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
898
- return state.canResume && row?.runtimeStatus !== 'running' && row?.runtimeStatus !== 'busy' && row?.runtimeStatus !== 'idle';
906
+ return Boolean(row) && state.canResume && row.runtimeStatus !== 'running' && row.runtimeStatus !== 'busy' && row.runtimeStatus !== 'idle';
899
907
  }
900
908
 
901
909
  export function operationLabel(operation) {
package/embed.mjs CHANGED
@@ -747,7 +747,11 @@ function normalizeUiState(value) {
747
747
  recoverable: raw.recoverable === true,
748
748
  harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
749
749
  const item = record(candidate);
750
- return item && typeof item.id === "string" ? [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable: item.startable === true, reason: typeof item.reason === "string" ? item.reason : null }] : [];
750
+ if (!item || typeof item.id !== "string") return [];
751
+ const startable = item.startable === true;
752
+ const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
753
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
754
+ return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
751
755
  }) : [],
752
756
  history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
753
757
  savedDraft: string(raw.savedDraft),
@@ -832,7 +836,7 @@ function activitySummary(entries) {
832
836
  function canContinueHere(state) {
833
837
  if (state.mode !== "mirror" || state.canSend) return false;
834
838
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
835
- return state.canResume && row?.runtimeStatus !== "running" && row?.runtimeStatus !== "busy" && row?.runtimeStatus !== "idle";
839
+ return Boolean(row) && state.canResume && row.runtimeStatus !== "running" && row.runtimeStatus !== "busy" && row.runtimeStatus !== "idle";
836
840
  }
837
841
  function operationLabel(operation) {
838
842
  if (!operation) return "";
@@ -2105,7 +2109,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2105
2109
  ] })
2106
2110
  ] });
2107
2111
  }
2108
- function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
2112
+ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default", headerActions: HeaderActions }) {
2109
2113
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
2110
2114
  const [query, setQuery] = useState4(remembered.query);
2111
2115
  const [loadingMore, setLoadingMore] = useState4(false);
@@ -2146,6 +2150,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
2146
2150
  " recent conversations"
2147
2151
  ] })
2148
2152
  ] }),
2153
+ HeaderActions ? /* @__PURE__ */ jsx7(HeaderActions, { state, adapter, value: "list" }) : null,
2149
2154
  /* @__PURE__ */ jsx7("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx7(UiIcon, { name: "plus", size: 18 }) }),
2150
2155
  onClose ? /* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx7(UiIcon, { name: "close", size: 18 }) }) : null
2151
2156
  ] }),
@@ -2384,7 +2389,7 @@ function ConversationActions({ state, adapter, actionPending, onSettings }) {
2384
2389
  ] }, group.label)) }) : null
2385
2390
  ] });
2386
2391
  }
2387
- function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings }) {
2392
+ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings, headerActions: HeaderActions }) {
2388
2393
  const back = useRef7(null);
2389
2394
  const harness = state.attached?.harness ?? state.harness;
2390
2395
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
@@ -2404,6 +2409,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2404
2409
  status
2405
2410
  ] })
2406
2411
  ] }),
2412
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "chat" }) : null,
2407
2413
  menu ? /* @__PURE__ */ jsx9(ConversationActions, { state, adapter, actionPending, onSettings }) : null,
2408
2414
  /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "New chat", onClick: onNew, children: /* @__PURE__ */ jsx9(UiIcon, { name: "plus", size: 18 }) }),
2409
2415
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
@@ -2411,6 +2417,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2411
2417
  }
2412
2418
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2413
2419
  const Header = slots.header;
2420
+ const HeaderActions = slots.headerActions;
2414
2421
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2415
2422
  const [pending, setPendingState] = useState6(() => pendingMessageMemory.get(memoryKey) ?? null);
2416
2423
  const [pendingAction, setPendingAction] = useState6(null);
@@ -2510,7 +2517,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2510
2517
  const Advisory = components.HarnessAdvisory ?? HarnessAdvisory;
2511
2518
  const SettingsPanel = components.HarnessSettingsPanel ?? HarnessSettingsPanel;
2512
2519
  return /* @__PURE__ */ jsxs8("section", { class: "scui-chat", children: [
2513
- Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }) }),
2520
+ Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }), headerActions: HeaderActions }),
2514
2521
  actionLabel ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2515
2522
  /* @__PURE__ */ jsx9("i", {}),
2516
2523
  actionLabel
@@ -2527,34 +2534,41 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2527
2534
  settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
2528
2535
  ] });
2529
2536
  }
2530
- function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2537
+ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions }) {
2531
2538
  const startable = state.harnesses.filter((item) => item.startable);
2532
- const startableKey = startable.map((item) => item.id).join("\0");
2533
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2539
+ const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
2540
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
2534
2541
  const [harness, setHarness] = useState6(remembered.harness);
2535
2542
  const [draft, setDraft] = useState6(remembered.draft);
2536
2543
  const [context, setContext] = useState6(remembered.context);
2537
2544
  const [images, setImages] = useState6(remembered.images ?? []);
2545
+ const [modes, setModes] = useState6(remembered.modes ?? {});
2538
2546
  const [starting, setStarting] = useState6(null);
2539
2547
  const [picking, setPicking] = useState6(false);
2540
2548
  const [dragging, setDragging] = useState6(false);
2541
2549
  const [pickerError, setPickerError] = useState6(null);
2542
2550
  const startSequence = useRef7(0);
2543
2551
  const textarea = useRef7(null);
2552
+ const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2553
+ const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2554
+ const rememberedMode = modes[harness];
2555
+ const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2556
+ const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2557
+ const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2544
2558
  useAutosizeTextarea(textarea, draft);
2545
2559
  useEffect8(() => {
2546
2560
  if (startable.some((item) => item.id === harness)) return;
2547
2561
  const next = startable[0]?.id ?? "";
2548
2562
  setHarness(next);
2549
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2563
+ remember({ harness: next });
2550
2564
  }, [harness, startableKey]);
2551
2565
  useEffect8(() => {
2552
- if (!starting) return;
2566
+ if (!starting || starting.mode === "terminal") return;
2553
2567
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2554
2568
  const beganWorking = !starting.busy && state.busy;
2555
2569
  if (!sessionChanged && !beganWorking) return;
2556
2570
  newChatMemory.delete(memoryKey);
2557
- onStarted();
2571
+ onStarted("headless");
2558
2572
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2559
2573
  useEffect8(() => {
2560
2574
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
@@ -2563,7 +2577,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2563
2577
  textarea.current?.focus({ preventScroll: true });
2564
2578
  }, []);
2565
2579
  const pickContext = () => {
2566
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2580
+ if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2567
2581
  setPicking(true);
2568
2582
  setPickerError(null);
2569
2583
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
@@ -2572,12 +2586,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2572
2586
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2573
2587
  setContext((current) => {
2574
2588
  const next = mergeContext(current, attachments.context);
2575
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2589
+ remember({ context: next });
2576
2590
  return next;
2577
2591
  });
2578
2592
  setImages((current) => {
2579
2593
  const next = mergeImages(current, attachments.images);
2580
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2594
+ remember({ images: next });
2581
2595
  return next;
2582
2596
  });
2583
2597
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
@@ -2585,6 +2599,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2585
2599
  const addImageFiles = (value, source) => {
2586
2600
  const allFiles = Array.from(value ?? []);
2587
2601
  if (!allFiles.length) return false;
2602
+ if (mode === "terminal") {
2603
+ setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2604
+ return true;
2605
+ }
2588
2606
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
2589
2607
  if (files.length !== allFiles.length) {
2590
2608
  setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
@@ -2598,7 +2616,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2598
2616
  setPickerError(null);
2599
2617
  imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2600
2618
  const next = mergeImages(current, picked);
2601
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2619
+ remember({ images: next });
2602
2620
  return next;
2603
2621
  }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2604
2622
  return true;
@@ -2612,12 +2630,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2612
2630
  };
2613
2631
  const send = () => {
2614
2632
  const text = draft.trim();
2615
- if (!text && !images.length || !harness || starting) return;
2633
+ if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
2616
2634
  const id = startSequence.current + 1;
2617
2635
  startSequence.current = id;
2618
- setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2619
- const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2620
- if (result && typeof result.then === "function") {
2636
+ setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2637
+ const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2638
+ if (mode === "terminal") {
2639
+ Promise.resolve(result).then(() => {
2640
+ newChatMemory.delete(memoryKey);
2641
+ onStarted("terminal");
2642
+ }, () => setStarting((current) => current?.id === id ? null : current));
2643
+ } else if (result && typeof result.then === "function") {
2621
2644
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2622
2645
  }
2623
2646
  };
@@ -2628,11 +2651,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2628
2651
  /* @__PURE__ */ jsx9("strong", { children: labels.newChat }),
2629
2652
  /* @__PURE__ */ jsx9("small", { children: "No session is created until you send" })
2630
2653
  ] }),
2654
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "new" }) : null,
2631
2655
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
2632
2656
  ] }),
2633
2657
  starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2634
2658
  /* @__PURE__ */ jsx9("i", {}),
2635
- operationLabel("start")
2659
+ mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
2636
2660
  ] }) : null,
2637
2661
  /* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
2638
2662
  /* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
@@ -2646,23 +2670,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2646
2670
  /* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2647
2671
  const value = event.currentTarget.value;
2648
2672
  setHarness(value);
2649
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2673
+ remember({ harness: value });
2650
2674
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
2651
2675
  item.label,
2652
2676
  item.startable ? "" : " \xB7 unavailable"
2653
2677
  ] }, item.id)) })
2654
2678
  ] }),
2679
+ launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
2680
+ /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2681
+ launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2682
+ /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2683
+ const next = { ...modes, [harness]: item };
2684
+ setModes(next);
2685
+ setPickerError(null);
2686
+ remember({ modes: next });
2687
+ } }),
2688
+ /* @__PURE__ */ jsxs8("span", { children: [
2689
+ /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2690
+ /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2691
+ ] })
2692
+ ] }, item))
2693
+ ] }) : null,
2655
2694
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2656
2695
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2657
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2696
+ remember({ images: next });
2658
2697
  return next;
2659
2698
  }) }),
2660
2699
  /* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2661
2700
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2662
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2701
+ remember({ context: next });
2663
2702
  return next;
2664
2703
  }) }),
2665
- pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2704
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2666
2705
  /* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2667
2706
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2668
2707
  event.preventDefault();
@@ -2673,18 +2712,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2673
2712
  }, onDragLeave: (event) => {
2674
2713
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2675
2714
  }, onDrop: dropImages, children: [
2676
- adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2715
+ adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2677
2716
  /* @__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) => {
2678
2717
  const value = event.currentTarget.value;
2679
2718
  setDraft(value);
2680
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2719
+ remember({ draft: value });
2681
2720
  }, onKeyDown: (event) => {
2682
2721
  if (isSendKey(event)) {
2683
2722
  event.preventDefault();
2684
2723
  send();
2685
2724
  }
2686
2725
  } }),
2687
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2726
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2688
2727
  ] })
2689
2728
  ] })
2690
2729
  ] });
@@ -2716,12 +2755,13 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2716
2755
  };
2717
2756
  const close = () => adapter.onClose?.();
2718
2757
  const Footer = slots.footer;
2758
+ const HeaderActions = slots.headerActions;
2719
2759
  return /* @__PURE__ */ jsxs8("main", { class: `scui-root ${className}`, "data-view": view, "data-mode": state.mode, "aria-label": "Supercode messenger", children: [
2720
2760
  view === "list" ? /* @__PURE__ */ jsx9(SessionList, { state, adapter, focusKey: listFocus, onOpen: open, onNew: () => {
2721
2761
  setListFocus("@new");
2722
2762
  setView("new");
2723
- }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
2724
- view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
2763
+ }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2764
+ view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2725
2765
  view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
2726
2766
  setListFocus(state.attached?.key ?? listFocus);
2727
2767
  setView("list");
package/index.d.ts CHANGED
@@ -5,7 +5,9 @@ export type UiTone = 'live' | 'warn' | 'dead' | 'off';
5
5
  export type StartupPhase = 'connecting' | 'starting' | 'discovering' | 'ready';
6
6
  export type SessionMode = 'none' | 'control' | 'mirror';
7
7
  export type ControlStrategy = 'start' | 'resume' | 'attach' | 'branch' | 'reduce' | null;
8
- export type ContinuationMode = 'headless' | 'terminal';
8
+ export type ExecutionMode = 'headless' | 'terminal';
9
+ /** @deprecated Use ExecutionMode. */
10
+ export type ContinuationMode = ExecutionMode;
9
11
  export type SessionActivity = 'idle' | 'recent' | 'running' | 'working' | 'needs-input' | 'finished' | 'failed' | 'unseen';
10
12
 
11
13
  export interface HarnessOption {
@@ -14,6 +16,10 @@ export interface HarnessOption {
14
16
  installed: boolean;
15
17
  startable: boolean;
16
18
  reason: string | null;
19
+ /** Host-supported ways to start this harness. Omitted means headless when startable. */
20
+ launchModes?: ExecutionMode[];
21
+ /** Host preference, used unless this browser has a remembered choice for the harness. */
22
+ preferredLaunchMode?: ExecutionMode | null;
17
23
  }
18
24
 
19
25
  export interface SessionRowModel {
@@ -287,7 +293,7 @@ export type SupercodeUiIntent =
287
293
  | { action: 'loadEarlier' }
288
294
  | { action: 'draft'; text: string }
289
295
  | { action: 'send'; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
290
- | { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
296
+ | { action: 'new'; harness: HarnessId; mode?: ExecutionMode; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
291
297
  | { action: 'resume'; mode?: ContinuationMode }
292
298
  | { action: 'join' }
293
299
  | { action: 'detach' }
@@ -403,6 +409,8 @@ export interface MessengerComponents {
403
409
 
404
410
  export interface MessengerSlots {
405
411
  header?: ComponentType<ComponentOverrideProps>;
412
+ /** Add a compact consumer control to every default messenger header. `value` is the current view. */
413
+ headerActions?: ComponentType<ComponentOverrideProps<'list' | 'chat' | 'new'>>;
406
414
  emptyConversation?: ComponentType<ComponentOverrideProps>;
407
415
  beforeConversation?: ComponentType<ComponentOverrideProps>;
408
416
  afterConversation?: ComponentType<ComponentOverrideProps>;
@@ -460,7 +468,7 @@ export function HarnessSettingsPanel(props: HarnessSettingsPanelProps): VNode;
460
468
  export function SessionDetails(props: { semantics: SessionSemanticsModel }): VNode | null;
461
469
  export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapter; components?: MessengerComponents; slots?: MessengerSlots; memoryKey?: string; pending?: string | PendingMessageModel | null; unreadAfterMessages?: number | null }): VNode;
462
470
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): VNode;
463
- export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string }): VNode;
471
+ export function SessionList(props: { state: SupercodeUiState; adapter: UiAdapter; onOpen(row: SessionRowModel): void; onNew?(): void; onClose?(): void; components?: MessengerComponents; labels?: MessengerLabels; focusKey?: string | null; memoryKey?: string; headerActions?: MessengerSlots['headerActions'] }): VNode;
464
472
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
465
473
  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; onPending?(text: string, context?: TranscriptContext[], images?: TranscriptImage[]): void; onDraftRestored?(id: string | number): void }): VNode;
466
474
  export function SupercodeMessenger(props: MessengerProps): VNode;
package/messenger.mjs CHANGED
@@ -744,7 +744,11 @@ function normalizeUiState(value) {
744
744
  recoverable: raw.recoverable === true,
745
745
  harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
746
746
  const item = record(candidate);
747
- return item && typeof item.id === "string" ? [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable: item.startable === true, reason: typeof item.reason === "string" ? item.reason : null }] : [];
747
+ if (!item || typeof item.id !== "string") return [];
748
+ const startable = item.startable === true;
749
+ const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
750
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
751
+ return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
748
752
  }) : [],
749
753
  history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
750
754
  savedDraft: string(raw.savedDraft),
@@ -829,7 +833,7 @@ function activitySummary(entries) {
829
833
  function canContinueHere(state) {
830
834
  if (state.mode !== "mirror" || state.canSend) return false;
831
835
  const row = state.attached?.key ? state.sessions.find((item) => item.key === state.attached.key) : null;
832
- return state.canResume && row?.runtimeStatus !== "running" && row?.runtimeStatus !== "busy" && row?.runtimeStatus !== "idle";
836
+ return Boolean(row) && state.canResume && row.runtimeStatus !== "running" && row.runtimeStatus !== "busy" && row.runtimeStatus !== "idle";
833
837
  }
834
838
  function operationLabel(operation) {
835
839
  if (!operation) return "";
@@ -2102,7 +2106,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2102
2106
  ] })
2103
2107
  ] });
2104
2108
  }
2105
- function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
2109
+ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default", headerActions: HeaderActions }) {
2106
2110
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
2107
2111
  const [query, setQuery] = useState4(remembered.query);
2108
2112
  const [loadingMore, setLoadingMore] = useState4(false);
@@ -2143,6 +2147,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
2143
2147
  " recent conversations"
2144
2148
  ] })
2145
2149
  ] }),
2150
+ HeaderActions ? /* @__PURE__ */ jsx7(HeaderActions, { state, adapter, value: "list" }) : null,
2146
2151
  /* @__PURE__ */ jsx7("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx7(UiIcon, { name: "plus", size: 18 }) }),
2147
2152
  onClose ? /* @__PURE__ */ jsx7("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx7(UiIcon, { name: "close", size: 18 }) }) : null
2148
2153
  ] }),
@@ -2381,7 +2386,7 @@ function ConversationActions({ state, adapter, actionPending, onSettings }) {
2381
2386
  ] }, group.label)) }) : null
2382
2387
  ] });
2383
2388
  }
2384
- function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings }) {
2389
+ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNew, onClose, onSettings, headerActions: HeaderActions }) {
2385
2390
  const back = useRef7(null);
2386
2391
  const harness = state.attached?.harness ?? state.harness;
2387
2392
  const title = state.attached ? sessionDisplayName(state.attached) : harnessDisplayName(harness) || "Agent chat";
@@ -2401,6 +2406,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2401
2406
  status
2402
2407
  ] })
2403
2408
  ] }),
2409
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "chat" }) : null,
2404
2410
  menu ? /* @__PURE__ */ jsx9(ConversationActions, { state, adapter, actionPending, onSettings }) : null,
2405
2411
  /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "New chat", onClick: onNew, children: /* @__PURE__ */ jsx9(UiIcon, { name: "plus", size: 18 }) }),
2406
2412
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
@@ -2408,6 +2414,7 @@ function ChatHeader({ state, adapter, pendingStatus, actionPending, onBack, onNe
2408
2414
  }
2409
2415
  function Chat({ state, adapter, onBack, onNew, onClose, components, slots, labels }) {
2410
2416
  const Header = slots.header;
2417
+ const HeaderActions = slots.headerActions;
2411
2418
  const memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`;
2412
2419
  const [pending, setPendingState] = useState6(() => pendingMessageMemory.get(memoryKey) ?? null);
2413
2420
  const [pendingAction, setPendingAction] = useState6(null);
@@ -2507,7 +2514,7 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2507
2514
  const Advisory = components.HarnessAdvisory ?? HarnessAdvisory;
2508
2515
  const SettingsPanel = components.HarnessSettingsPanel ?? HarnessSettingsPanel;
2509
2516
  return /* @__PURE__ */ jsxs8("section", { class: "scui-chat", children: [
2510
- Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }) }),
2517
+ Header ? /* @__PURE__ */ jsx9(Header, { state: actionState, adapter: trackedAdapter, value: null }) : /* @__PURE__ */ jsx9(ChatHeader, { state: actionState, adapter: trackedAdapter, pendingStatus: pending?.status ?? null, actionPending: Boolean(action), onBack, onNew, onClose, onSettings: () => setSettings({ recommendedChange: null }), headerActions: HeaderActions }),
2511
2518
  actionLabel ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2512
2519
  /* @__PURE__ */ jsx9("i", {}),
2513
2520
  actionLabel
@@ -2524,34 +2531,41 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
2524
2531
  settings ? /* @__PURE__ */ jsx9(SettingsPanel, { state: actionState, adapter: trackedAdapter, value: state.interopSettings, recommendedChange: settings.recommendedChange, onClose: () => setSettings(null) }) : null
2525
2532
  ] });
2526
2533
  }
2527
- function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
2534
+ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey, headerActions: HeaderActions }) {
2528
2535
  const startable = state.harnesses.filter((item) => item.startable);
2529
- const startableKey = startable.map((item) => item.id).join("\0");
2530
- const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
2536
+ const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
2537
+ const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
2531
2538
  const [harness, setHarness] = useState6(remembered.harness);
2532
2539
  const [draft, setDraft] = useState6(remembered.draft);
2533
2540
  const [context, setContext] = useState6(remembered.context);
2534
2541
  const [images, setImages] = useState6(remembered.images ?? []);
2542
+ const [modes, setModes] = useState6(remembered.modes ?? {});
2535
2543
  const [starting, setStarting] = useState6(null);
2536
2544
  const [picking, setPicking] = useState6(false);
2537
2545
  const [dragging, setDragging] = useState6(false);
2538
2546
  const [pickerError, setPickerError] = useState6(null);
2539
2547
  const startSequence = useRef7(0);
2540
2548
  const textarea = useRef7(null);
2549
+ const selectedHarness = startable.find((item) => item.id === harness) ?? null;
2550
+ const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2551
+ const rememberedMode = modes[harness];
2552
+ const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2553
+ const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2554
+ const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2541
2555
  useAutosizeTextarea(textarea, draft);
2542
2556
  useEffect8(() => {
2543
2557
  if (startable.some((item) => item.id === harness)) return;
2544
2558
  const next = startable[0]?.id ?? "";
2545
2559
  setHarness(next);
2546
- boundedSet(newChatMemory, memoryKey, { harness: next, draft, context, images });
2560
+ remember({ harness: next });
2547
2561
  }, [harness, startableKey]);
2548
2562
  useEffect8(() => {
2549
- if (!starting) return;
2563
+ if (!starting || starting.mode === "terminal") return;
2550
2564
  const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
2551
2565
  const beganWorking = !starting.busy && state.busy;
2552
2566
  if (!sessionChanged && !beganWorking) return;
2553
2567
  newChatMemory.delete(memoryKey);
2554
- onStarted();
2568
+ onStarted("headless");
2555
2569
  }, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
2556
2570
  useEffect8(() => {
2557
2571
  if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
@@ -2560,7 +2574,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2560
2574
  textarea.current?.focus({ preventScroll: true });
2561
2575
  }, []);
2562
2576
  const pickContext = () => {
2563
- if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2577
+ if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
2564
2578
  setPicking(true);
2565
2579
  setPickerError(null);
2566
2580
  Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
@@ -2569,12 +2583,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2569
2583
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2570
2584
  setContext((current) => {
2571
2585
  const next = mergeContext(current, attachments.context);
2572
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2586
+ remember({ context: next });
2573
2587
  return next;
2574
2588
  });
2575
2589
  setImages((current) => {
2576
2590
  const next = mergeImages(current, attachments.images);
2577
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2591
+ remember({ images: next });
2578
2592
  return next;
2579
2593
  });
2580
2594
  }).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
@@ -2582,6 +2596,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2582
2596
  const addImageFiles = (value, source) => {
2583
2597
  const allFiles = Array.from(value ?? []);
2584
2598
  if (!allFiles.length) return false;
2599
+ if (mode === "terminal") {
2600
+ setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2601
+ return true;
2602
+ }
2585
2603
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
2586
2604
  if (files.length !== allFiles.length) {
2587
2605
  setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
@@ -2595,7 +2613,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2595
2613
  setPickerError(null);
2596
2614
  imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
2597
2615
  const next = mergeImages(current, picked);
2598
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2616
+ remember({ images: next });
2599
2617
  return next;
2600
2618
  }), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
2601
2619
  return true;
@@ -2609,12 +2627,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2609
2627
  };
2610
2628
  const send = () => {
2611
2629
  const text = draft.trim();
2612
- if (!text && !images.length || !harness || starting) return;
2630
+ if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
2613
2631
  const id = startSequence.current + 1;
2614
2632
  startSequence.current = id;
2615
- setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2616
- const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2617
- if (result && typeof result.then === "function") {
2633
+ setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
2634
+ const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
2635
+ if (mode === "terminal") {
2636
+ Promise.resolve(result).then(() => {
2637
+ newChatMemory.delete(memoryKey);
2638
+ onStarted("terminal");
2639
+ }, () => setStarting((current) => current?.id === id ? null : current));
2640
+ } else if (result && typeof result.then === "function") {
2618
2641
  Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
2619
2642
  }
2620
2643
  };
@@ -2625,11 +2648,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2625
2648
  /* @__PURE__ */ jsx9("strong", { children: labels.newChat }),
2626
2649
  /* @__PURE__ */ jsx9("small", { children: "No session is created until you send" })
2627
2650
  ] }),
2651
+ HeaderActions ? /* @__PURE__ */ jsx9(HeaderActions, { state, adapter, value: "new" }) : null,
2628
2652
  onClose ? /* @__PURE__ */ jsx9("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx9(UiIcon, { name: "close", size: 18 }) }) : null
2629
2653
  ] }),
2630
2654
  starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
2631
2655
  /* @__PURE__ */ jsx9("i", {}),
2632
- operationLabel("start")
2656
+ mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
2633
2657
  ] }) : null,
2634
2658
  /* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
2635
2659
  /* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
@@ -2643,23 +2667,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2643
2667
  /* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
2644
2668
  const value = event.currentTarget.value;
2645
2669
  setHarness(value);
2646
- boundedSet(newChatMemory, memoryKey, { harness: value, draft, context, images });
2670
+ remember({ harness: value });
2647
2671
  }, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
2648
2672
  item.label,
2649
2673
  item.startable ? "" : " \xB7 unavailable"
2650
2674
  ] }, item.id)) })
2651
2675
  ] }),
2676
+ launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
2677
+ /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2678
+ launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2679
+ /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2680
+ const next = { ...modes, [harness]: item };
2681
+ setModes(next);
2682
+ setPickerError(null);
2683
+ remember({ modes: next });
2684
+ } }),
2685
+ /* @__PURE__ */ jsxs8("span", { children: [
2686
+ /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2687
+ /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2688
+ ] })
2689
+ ] }, item))
2690
+ ] }) : null,
2652
2691
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2653
2692
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2654
- boundedSet(newChatMemory, memoryKey, { harness, draft, context, images: next });
2693
+ remember({ images: next });
2655
2694
  return next;
2656
2695
  }) }),
2657
2696
  /* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
2658
2697
  const next = items.filter((_, itemIndex) => itemIndex !== index);
2659
- boundedSet(newChatMemory, memoryKey, { harness, draft, context: next, images });
2698
+ remember({ context: next });
2660
2699
  return next;
2661
2700
  }) }),
2662
- pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2701
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
2663
2702
  /* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2664
2703
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2665
2704
  event.preventDefault();
@@ -2670,18 +2709,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2670
2709
  }, onDragLeave: (event) => {
2671
2710
  if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
2672
2711
  }, onDrop: dropImages, children: [
2673
- adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2712
+ adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
2674
2713
  /* @__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) => {
2675
2714
  const value = event.currentTarget.value;
2676
2715
  setDraft(value);
2677
- boundedSet(newChatMemory, memoryKey, { harness, draft: value, context, images });
2716
+ remember({ draft: value });
2678
2717
  }, onKeyDown: (event) => {
2679
2718
  if (isSendKey(event)) {
2680
2719
  event.preventDefault();
2681
2720
  send();
2682
2721
  }
2683
2722
  } }),
2684
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2723
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "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", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2685
2724
  ] })
2686
2725
  ] })
2687
2726
  ] });
@@ -2713,12 +2752,13 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
2713
2752
  };
2714
2753
  const close = () => adapter.onClose?.();
2715
2754
  const Footer = slots.footer;
2755
+ const HeaderActions = slots.headerActions;
2716
2756
  return /* @__PURE__ */ jsxs8("main", { class: `scui-root ${className}`, "data-view": view, "data-mode": state.mode, "aria-label": "Supercode messenger", children: [
2717
2757
  view === "list" ? /* @__PURE__ */ jsx9(SessionList, { state, adapter, focusKey: listFocus, onOpen: open, onNew: () => {
2718
2758
  setListFocus("@new");
2719
2759
  setView("new");
2720
- }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
2721
- view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
2760
+ }, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2761
+ view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey, headerActions: HeaderActions }) : null,
2722
2762
  view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
2723
2763
  setListFocus(state.attached?.key ?? listFocus);
2724
2764
  setView("list");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
package/sessions.mjs CHANGED
@@ -340,7 +340,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
340
340
  ] })
341
341
  ] });
342
342
  }
343
- function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default" }) {
343
+ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {}, labels = DEFAULT_LABELS, focusKey = null, memoryKey = state.workspace || "@default", headerActions: HeaderActions }) {
344
344
  const remembered = sessionListMemory.get(memoryKey) ?? { query: "", top: 0 };
345
345
  const [query, setQuery] = useState3(remembered.query);
346
346
  const [loadingMore, setLoadingMore] = useState3(false);
@@ -381,6 +381,7 @@ function SessionList({ state, adapter, onOpen, onNew, onClose, components = {},
381
381
  " recent conversations"
382
382
  ] })
383
383
  ] }),
384
+ HeaderActions ? /* @__PURE__ */ jsx6(HeaderActions, { state, adapter, value: "list" }) : null,
384
385
  /* @__PURE__ */ jsx6("button", { type: "button", "data-list-focus": "new", "aria-label": labels.newChat, disabled: !state.harnesses.some((item) => item.startable), onClick: onNew, children: /* @__PURE__ */ jsx6(UiIcon, { name: "plus", size: 18 }) }),
385
386
  onClose ? /* @__PURE__ */ jsx6("button", { type: "button", "aria-label": "Close", onClick: onClose, children: /* @__PURE__ */ jsx6(UiIcon, { name: "close", size: 18 }) }) : null
386
387
  ] }),
package/styles.css CHANGED
@@ -191,6 +191,7 @@
191
191
  .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 }
192
192
  .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 }
193
193
  .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) }
194
+ .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 }
194
195
  .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) }
195
196
  .scui-opening { position:absolute; z-index:30; inset:49px 0 0; display:flex; align-items:center; justify-content:center; gap:10px; padding:20px; background:var(--scui-bg) }.scui-opening > span { display:grid }.scui-opening small { color:var(--scui-muted) }.scui-opening > i { width:16px; height:16px; border:2px solid var(--scui-border); border-top-color:var(--scui-accent); border-radius:50%; animation:scui-spin .8s linear infinite }
196
197