@volter-ai-dev/supercode-ui 0.1.44 → 0.1.45

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
@@ -183,15 +183,16 @@ drafts, and artifact materialization remain explicit callbacks. A
183
183
  browser should receive projected state from a trusted host rather than instantiate a local
184
184
  controller or gain filesystem authority.
185
185
 
186
- Native continuation is headless by default. A host with a real terminal provider can add
187
- `terminal` to `continuationModes` and handle `onResumeTerminal`; the continuation bar then exposes
188
- that strategy beside “Continue here.” The UI never infers terminal support from a generic runtime
189
- handoff, and never presents a terminal strategy when the host cannot create one.
186
+ Native continuation exposes one action and a quiet execution-transport selector. A host with a real
187
+ terminal provider can add `terminal` to `continuationModes` and handle `onResumeTerminal`; Terminal
188
+ is then the initial choice and Headless remains available from the selector. The UI never infers
189
+ terminal support from a generic runtime handoff, and never presents a terminal transport when the
190
+ host cannot create one.
190
191
 
191
192
  New-session execution is advertised per harness. Set `launchModes: ['headless', 'terminal']` and
192
- `preferredLaunchMode` on a `HarnessOption`; the complete messenger renders a compact “Chat” versus
193
- “Terminal” choice and emits that `mode` on the `new` intent. Direct controller bindings keep Chat as
194
- the standard path and delegate Terminal only through `onStartTerminal`, so a terminal selection can
193
+ `preferredLaunchMode` on a `HarnessOption`; the complete messenger renders the same compact Terminal
194
+ versus Headless selector in the composer footer and emits that `mode` on the `new` intent. Direct
195
+ controller bindings delegate Terminal only through `onStartTerminal`, so a terminal selection can
195
196
  never accidentally create a second headless runtime. Hosts may remember the preference per harness;
196
197
  the messenger also retains the current browser draft choice while its New Chat view is open.
197
198
 
package/activity.mjs CHANGED
@@ -738,7 +738,7 @@ function normalizeUiState(value) {
738
738
  if (!item || typeof item.id !== "string") return [];
739
739
  const startable = item.startable === true;
740
740
  const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
741
- const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
741
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
742
742
  const capabilities = record(item.capabilities);
743
743
  return [{
744
744
  id: item.id,
package/components.d.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  ContextCandidate,
7
7
  ContextCandidates,
8
8
  ContinuationBar,
9
+ ExecutionModeSelect,
9
10
  Conversation,
10
11
  HarnessLogo,
11
12
  HarnessPicker,
package/components.mjs CHANGED
@@ -750,7 +750,7 @@ function normalizeUiState(value) {
750
750
  if (!item || typeof item.id !== "string") return [];
751
751
  const startable = item.startable === true;
752
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;
753
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
754
754
  const capabilities = record(item.capabilities);
755
755
  return [{
756
756
  id: item.id,
@@ -1294,13 +1294,23 @@ function useAutosizeTextarea(ref, value) {
1294
1294
  // src/composer.jsx
1295
1295
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1296
1296
  var composerMemory = /* @__PURE__ */ new Map();
1297
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1298
+ if (modes.length < 2) return null;
1299
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1300
+ /* @__PURE__ */ jsx3("span", { children: label }),
1301
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
1302
+ ] });
1303
+ }
1297
1304
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1305
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1306
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1307
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1308
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1298
1309
  if (state.mode !== "mirror" || state.canSend) return null;
1299
1310
  const attached = state.attached;
1300
1311
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1301
1312
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1302
1313
  const resume = canContinueHere(state);
1303
- const terminal = resume && state.continuationModes?.includes("terminal");
1304
1314
  const join = state.canAttach;
1305
1315
  const branch = state.canBranch;
1306
1316
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1313,8 +1323,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1313
1323
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
1314
1324
  ] }),
1315
1325
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
1316
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
1317
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
1326
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1327
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
1318
1328
  ] })
1319
1329
  ] });
1320
1330
  }
@@ -2870,7 +2880,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2870
2880
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2871
2881
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2872
2882
  const rememberedMode = modes[harness];
2873
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2883
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2884
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2874
2885
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2875
2886
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2876
2887
  useAutosizeTextarea(textarea, draft);
@@ -2912,7 +2923,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2912
2923
  lastComposerCommand.current = composerCommand.id;
2913
2924
  if (composerCommand.action === "attach") {
2914
2925
  if (mode === "terminal" && !launchModes.includes("headless")) {
2915
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2926
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2916
2927
  } else {
2917
2928
  try {
2918
2929
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2920,12 +2931,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2920
2931
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2921
2932
  const nextContext = mergeContext(context, attachments.context);
2922
2933
  const nextImages = mergeImages(images, attachments.images);
2923
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2924
- if (nextModes !== modes) setModes(nextModes);
2925
2934
  setContext(nextContext);
2926
2935
  setImages(nextImages);
2927
2936
  setPickerError(null);
2928
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2937
+ remember({ context: nextContext, images: nextImages });
2929
2938
  } catch (error) {
2930
2939
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2931
2940
  }
@@ -2966,7 +2975,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2966
2975
  const allFiles = Array.from(value ?? []);
2967
2976
  if (!allFiles.length) return false;
2968
2977
  if (mode === "terminal") {
2969
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2978
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2970
2979
  return true;
2971
2980
  }
2972
2981
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -3031,21 +3040,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3031
3040
  ] }),
3032
3041
  /* @__PURE__ */ jsxs9("div", { className: "scui-compose", children: [
3033
3042
  /* @__PURE__ */ jsx10(Readiness, { harnesses: state.harnesses, state, adapter }),
3034
- launchModes.length > 1 ? /* @__PURE__ */ jsxs9("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
3035
- /* @__PURE__ */ jsx10("legend", { children: "Run as" }),
3036
- launchModes.map((item) => /* @__PURE__ */ jsxs9("label", { children: [
3037
- /* @__PURE__ */ jsx10("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
3038
- const next = { ...modes, [harness]: item };
3039
- setModes(next);
3040
- setPickerError(null);
3041
- remember({ modes: next });
3042
- } }),
3043
- /* @__PURE__ */ jsxs9("span", { children: [
3044
- /* @__PURE__ */ jsx10("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
3045
- /* @__PURE__ */ jsx10("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
3046
- ] })
3047
- ] }, item))
3048
- ] }) : null,
3049
3043
  mode !== "terminal" ? /* @__PURE__ */ jsx10(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
3050
3044
  /* @__PURE__ */ jsx10(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
3051
3045
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -3057,7 +3051,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3057
3051
  remember({ context: next });
3058
3052
  return next;
3059
3053
  }) }),
3060
- 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,
3054
+ terminalAttachments ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
3061
3055
  /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
3062
3056
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
3063
3057
  event.preventDefault();
@@ -3084,7 +3078,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3084
3078
  setHarness(value);
3085
3079
  remember({ harness: value });
3086
3080
  } }),
3087
- /* @__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 }) }) })
3081
+ /* @__PURE__ */ jsx10(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
3082
+ const next = { ...modes, [harness]: item };
3083
+ setModes(next);
3084
+ setPickerError(null);
3085
+ remember({ modes: next });
3086
+ } }),
3087
+ /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", 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 }) }) })
3088
3088
  ] })
3089
3089
  ] })
3090
3090
  ] })
@@ -3193,6 +3193,7 @@ export {
3193
3193
  ContextCandidates,
3194
3194
  ContinuationBar,
3195
3195
  Conversation,
3196
+ ExecutionModeSelect,
3196
3197
  HarnessAdvisory,
3197
3198
  HarnessLogo,
3198
3199
  HarnessPicker,
package/composer.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export type { MessengerLabels, PendingMessageModel, SupercodeUiState, UiAdapter } from './index.js';
2
- export { Composer, ContinuationBar } from './index.js';
2
+ export { Composer, ContinuationBar, ExecutionModeSelect } from './index.js';
package/composer.mjs CHANGED
@@ -295,13 +295,23 @@ function useAutosizeTextarea(ref, value) {
295
295
  // src/composer.jsx
296
296
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
297
297
  var composerMemory = /* @__PURE__ */ new Map();
298
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
299
+ if (modes.length < 2) return null;
300
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
301
+ /* @__PURE__ */ jsx3("span", { children: label }),
302
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
303
+ ] });
304
+ }
298
305
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
306
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
307
+ const [selection, setSelection] = useState2({ key: null, mode: null });
308
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
309
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
299
310
  if (state.mode !== "mirror" || state.canSend) return null;
300
311
  const attached = state.attached;
301
312
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
302
313
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
303
314
  const resume = canContinueHere(state);
304
- const terminal = resume && state.continuationModes?.includes("terminal");
305
315
  const join = state.canAttach;
306
316
  const branch = state.canBranch;
307
317
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -314,8 +324,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
314
324
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
315
325
  ] }),
316
326
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
317
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
318
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
327
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
328
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
319
329
  ] })
320
330
  ] });
321
331
  }
@@ -536,5 +546,6 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
536
546
  }
537
547
  export {
538
548
  Composer,
539
- ContinuationBar
549
+ ContinuationBar,
550
+ ExecutionModeSelect
540
551
  };
package/core.mjs CHANGED
@@ -806,7 +806,9 @@ export function normalizeUiState(value) {
806
806
  : startable ? ['headless'] : [];
807
807
  const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode)
808
808
  ? item.preferredLaunchMode
809
- : launchModes[0] ?? null;
809
+ : launchModes.includes('terminal')
810
+ ? 'terminal'
811
+ : launchModes[0] ?? null;
810
812
  const capabilities = record(item.capabilities);
811
813
  return [{
812
814
  id: item.id,
package/embed.mjs CHANGED
@@ -753,7 +753,7 @@ function normalizeUiState(value) {
753
753
  if (!item || typeof item.id !== "string") return [];
754
754
  const startable = item.startable === true;
755
755
  const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
756
- const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
756
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
757
757
  const capabilities = record(item.capabilities);
758
758
  return [{
759
759
  id: item.id,
@@ -1260,13 +1260,23 @@ function useAutosizeTextarea(ref, value) {
1260
1260
  // src/composer.jsx
1261
1261
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1262
1262
  var composerMemory = /* @__PURE__ */ new Map();
1263
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1264
+ if (modes.length < 2) return null;
1265
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1266
+ /* @__PURE__ */ jsx3("span", { children: label }),
1267
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
1268
+ ] });
1269
+ }
1263
1270
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1271
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1272
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1273
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1274
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1264
1275
  if (state.mode !== "mirror" || state.canSend) return null;
1265
1276
  const attached = state.attached;
1266
1277
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1267
1278
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1268
1279
  const resume = canContinueHere(state);
1269
- const terminal = resume && state.continuationModes?.includes("terminal");
1270
1280
  const join = state.canAttach;
1271
1281
  const branch = state.canBranch;
1272
1282
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1279,8 +1289,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1279
1289
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
1280
1290
  ] }),
1281
1291
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
1282
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
1283
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
1292
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1293
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
1284
1294
  ] })
1285
1295
  ] });
1286
1296
  }
@@ -2790,7 +2800,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2790
2800
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2791
2801
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2792
2802
  const rememberedMode = modes[harness];
2793
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2803
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2804
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2794
2805
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2795
2806
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2796
2807
  useAutosizeTextarea(textarea, draft);
@@ -2832,7 +2843,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2832
2843
  lastComposerCommand.current = composerCommand.id;
2833
2844
  if (composerCommand.action === "attach") {
2834
2845
  if (mode === "terminal" && !launchModes.includes("headless")) {
2835
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2846
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2836
2847
  } else {
2837
2848
  try {
2838
2849
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2840,12 +2851,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2840
2851
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2841
2852
  const nextContext = mergeContext(context, attachments.context);
2842
2853
  const nextImages = mergeImages(images, attachments.images);
2843
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2844
- if (nextModes !== modes) setModes(nextModes);
2845
2854
  setContext(nextContext);
2846
2855
  setImages(nextImages);
2847
2856
  setPickerError(null);
2848
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2857
+ remember({ context: nextContext, images: nextImages });
2849
2858
  } catch (error) {
2850
2859
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2851
2860
  }
@@ -2886,7 +2895,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2886
2895
  const allFiles = Array.from(value ?? []);
2887
2896
  if (!allFiles.length) return false;
2888
2897
  if (mode === "terminal") {
2889
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2898
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2890
2899
  return true;
2891
2900
  }
2892
2901
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -2951,21 +2960,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2951
2960
  ] }),
2952
2961
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2953
2962
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2954
- launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2955
- /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2956
- launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2957
- /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2958
- const next = { ...modes, [harness]: item };
2959
- setModes(next);
2960
- setPickerError(null);
2961
- remember({ modes: next });
2962
- } }),
2963
- /* @__PURE__ */ jsxs8("span", { children: [
2964
- /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2965
- /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2966
- ] })
2967
- ] }, item))
2968
- ] }) : null,
2969
2963
  mode !== "terminal" ? /* @__PURE__ */ jsx9(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
2970
2964
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2971
2965
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -2977,7 +2971,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2977
2971
  remember({ context: next });
2978
2972
  return next;
2979
2973
  }) }),
2980
- terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2974
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2981
2975
  /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2982
2976
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2983
2977
  event.preventDefault();
@@ -3004,7 +2998,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3004
2998
  setHarness(value);
3005
2999
  remember({ harness: value });
3006
3000
  } }),
3007
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
3001
+ /* @__PURE__ */ jsx9(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
3002
+ const next = { ...modes, [harness]: item };
3003
+ setModes(next);
3004
+ setPickerError(null);
3005
+ remember({ modes: next });
3006
+ } }),
3007
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
3008
3008
  ] })
3009
3009
  ] })
3010
3010
  ] })
package/index.d.ts CHANGED
@@ -40,7 +40,7 @@ export interface HarnessOption {
40
40
  };
41
41
  /** Host-supported ways to start this harness. Omitted means headless when startable. */
42
42
  launchModes?: ExecutionMode[];
43
- /** Host preference, used unless this browser has a remembered choice for the harness. */
43
+ /** Host preference, used unless this browser has a remembered choice. Defaults to terminal when advertised. */
44
44
  preferredLaunchMode?: ExecutionMode | null;
45
45
  }
46
46
 
@@ -264,7 +264,7 @@ export interface SupercodeUiState {
264
264
  canSend: boolean;
265
265
  canSteer: boolean;
266
266
  canResume: boolean;
267
- /** Execution strategies the current host can actually provide. Headless is the default. */
267
+ /** Execution transports the current host can actually provide. */
268
268
  continuationModes: ContinuationMode[];
269
269
  canBranch: boolean;
270
270
  canAttach: boolean;
@@ -557,6 +557,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
557
557
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): VNode;
558
558
  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'] }): VNode;
559
559
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
560
+ export function ExecutionModeSelect(props: { modes?: ExecutionMode[]; value: ExecutionMode; onChange?(mode: ExecutionMode): void; disabled?: boolean; label?: string }): VNode | null;
560
561
  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 }): VNode;
561
562
  export function SupercodeMessenger(props: MessengerProps): VNode;
562
563
 
package/messenger.mjs CHANGED
@@ -750,7 +750,7 @@ function normalizeUiState(value) {
750
750
  if (!item || typeof item.id !== "string") return [];
751
751
  const startable = item.startable === true;
752
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;
753
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
754
754
  const capabilities = record(item.capabilities);
755
755
  return [{
756
756
  id: item.id,
@@ -1257,13 +1257,23 @@ function useAutosizeTextarea(ref, value) {
1257
1257
  // src/composer.jsx
1258
1258
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1259
1259
  var composerMemory = /* @__PURE__ */ new Map();
1260
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1261
+ if (modes.length < 2) return null;
1262
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1263
+ /* @__PURE__ */ jsx3("span", { children: label }),
1264
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
1265
+ ] });
1266
+ }
1260
1267
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1268
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1269
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1270
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1271
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1261
1272
  if (state.mode !== "mirror" || state.canSend) return null;
1262
1273
  const attached = state.attached;
1263
1274
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1264
1275
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1265
1276
  const resume = canContinueHere(state);
1266
- const terminal = resume && state.continuationModes?.includes("terminal");
1267
1277
  const join = state.canAttach;
1268
1278
  const branch = state.canBranch;
1269
1279
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1276,8 +1286,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1276
1286
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
1277
1287
  ] }),
1278
1288
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
1279
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
1280
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
1289
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1290
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
1281
1291
  ] })
1282
1292
  ] });
1283
1293
  }
@@ -2787,7 +2797,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2787
2797
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2788
2798
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2789
2799
  const rememberedMode = modes[harness];
2790
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2800
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2801
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2791
2802
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2792
2803
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2793
2804
  useAutosizeTextarea(textarea, draft);
@@ -2829,7 +2840,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2829
2840
  lastComposerCommand.current = composerCommand.id;
2830
2841
  if (composerCommand.action === "attach") {
2831
2842
  if (mode === "terminal" && !launchModes.includes("headless")) {
2832
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2843
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2833
2844
  } else {
2834
2845
  try {
2835
2846
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2837,12 +2848,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2837
2848
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2838
2849
  const nextContext = mergeContext(context, attachments.context);
2839
2850
  const nextImages = mergeImages(images, attachments.images);
2840
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2841
- if (nextModes !== modes) setModes(nextModes);
2842
2851
  setContext(nextContext);
2843
2852
  setImages(nextImages);
2844
2853
  setPickerError(null);
2845
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2854
+ remember({ context: nextContext, images: nextImages });
2846
2855
  } catch (error) {
2847
2856
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2848
2857
  }
@@ -2883,7 +2892,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2883
2892
  const allFiles = Array.from(value ?? []);
2884
2893
  if (!allFiles.length) return false;
2885
2894
  if (mode === "terminal") {
2886
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2895
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2887
2896
  return true;
2888
2897
  }
2889
2898
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -2948,21 +2957,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2948
2957
  ] }),
2949
2958
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2950
2959
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2951
- launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2952
- /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2953
- launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2954
- /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2955
- const next = { ...modes, [harness]: item };
2956
- setModes(next);
2957
- setPickerError(null);
2958
- remember({ modes: next });
2959
- } }),
2960
- /* @__PURE__ */ jsxs8("span", { children: [
2961
- /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2962
- /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2963
- ] })
2964
- ] }, item))
2965
- ] }) : null,
2966
2960
  mode !== "terminal" ? /* @__PURE__ */ jsx9(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
2967
2961
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2968
2962
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -2974,7 +2968,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2974
2968
  remember({ context: next });
2975
2969
  return next;
2976
2970
  }) }),
2977
- terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2971
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2978
2972
  /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2979
2973
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2980
2974
  event.preventDefault();
@@ -3001,7 +2995,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3001
2995
  setHarness(value);
3002
2996
  remember({ harness: value });
3003
2997
  } }),
3004
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2998
+ /* @__PURE__ */ jsx9(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
2999
+ const next = { ...modes, [harness]: item };
3000
+ setModes(next);
3001
+ setPickerError(null);
3002
+ remember({ modes: next });
3003
+ } }),
3004
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
3005
3005
  ] })
3006
3006
  ] })
3007
3007
  ] })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter-ai-dev/supercode-ui",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
@@ -738,7 +738,7 @@ function normalizeUiState(value) {
738
738
  if (!item || typeof item.id !== "string") return [];
739
739
  const startable = item.startable === true;
740
740
  const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
741
- const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
741
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
742
742
  const capabilities = record(item.capabilities);
743
743
  return [{
744
744
  id: item.id,
@@ -750,7 +750,7 @@ function normalizeUiState(value) {
750
750
  if (!item || typeof item.id !== "string") return [];
751
751
  const startable = item.startable === true;
752
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;
753
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
754
754
  const capabilities = record(item.capabilities);
755
755
  return [{
756
756
  id: item.id,
@@ -1294,13 +1294,23 @@ function useAutosizeTextarea(ref, value) {
1294
1294
  // src/composer.jsx
1295
1295
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1296
1296
  var composerMemory = /* @__PURE__ */ new Map();
1297
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1298
+ if (modes.length < 2) return null;
1299
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1300
+ /* @__PURE__ */ jsx3("span", { children: label }),
1301
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
1302
+ ] });
1303
+ }
1297
1304
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1305
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1306
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1307
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1308
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1298
1309
  if (state.mode !== "mirror" || state.canSend) return null;
1299
1310
  const attached = state.attached;
1300
1311
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1301
1312
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1302
1313
  const resume = canContinueHere(state);
1303
- const terminal = resume && state.continuationModes?.includes("terminal");
1304
1314
  const join = state.canAttach;
1305
1315
  const branch = state.canBranch;
1306
1316
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1313,8 +1323,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1313
1323
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
1314
1324
  ] }),
1315
1325
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
1316
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
1317
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
1326
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1327
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
1318
1328
  ] })
1319
1329
  ] });
1320
1330
  }
@@ -2870,7 +2880,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2870
2880
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2871
2881
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2872
2882
  const rememberedMode = modes[harness];
2873
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2883
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2884
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2874
2885
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2875
2886
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2876
2887
  useAutosizeTextarea(textarea, draft);
@@ -2912,7 +2923,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2912
2923
  lastComposerCommand.current = composerCommand.id;
2913
2924
  if (composerCommand.action === "attach") {
2914
2925
  if (mode === "terminal" && !launchModes.includes("headless")) {
2915
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2926
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2916
2927
  } else {
2917
2928
  try {
2918
2929
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2920,12 +2931,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2920
2931
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2921
2932
  const nextContext = mergeContext(context, attachments.context);
2922
2933
  const nextImages = mergeImages(images, attachments.images);
2923
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2924
- if (nextModes !== modes) setModes(nextModes);
2925
2934
  setContext(nextContext);
2926
2935
  setImages(nextImages);
2927
2936
  setPickerError(null);
2928
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2937
+ remember({ context: nextContext, images: nextImages });
2929
2938
  } catch (error) {
2930
2939
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2931
2940
  }
@@ -2966,7 +2975,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2966
2975
  const allFiles = Array.from(value ?? []);
2967
2976
  if (!allFiles.length) return false;
2968
2977
  if (mode === "terminal") {
2969
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2978
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2970
2979
  return true;
2971
2980
  }
2972
2981
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -3031,21 +3040,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3031
3040
  ] }),
3032
3041
  /* @__PURE__ */ jsxs9("div", { className: "scui-compose", children: [
3033
3042
  /* @__PURE__ */ jsx10(Readiness, { harnesses: state.harnesses, state, adapter }),
3034
- launchModes.length > 1 ? /* @__PURE__ */ jsxs9("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
3035
- /* @__PURE__ */ jsx10("legend", { children: "Run as" }),
3036
- launchModes.map((item) => /* @__PURE__ */ jsxs9("label", { children: [
3037
- /* @__PURE__ */ jsx10("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
3038
- const next = { ...modes, [harness]: item };
3039
- setModes(next);
3040
- setPickerError(null);
3041
- remember({ modes: next });
3042
- } }),
3043
- /* @__PURE__ */ jsxs9("span", { children: [
3044
- /* @__PURE__ */ jsx10("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
3045
- /* @__PURE__ */ jsx10("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
3046
- ] })
3047
- ] }, item))
3048
- ] }) : null,
3049
3043
  mode !== "terminal" ? /* @__PURE__ */ jsx10(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
3050
3044
  /* @__PURE__ */ jsx10(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
3051
3045
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -3057,7 +3051,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3057
3051
  remember({ context: next });
3058
3052
  return next;
3059
3053
  }) }),
3060
- 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,
3054
+ terminalAttachments ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx10("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
3061
3055
  /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
3062
3056
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
3063
3057
  event.preventDefault();
@@ -3084,7 +3078,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3084
3078
  setHarness(value);
3085
3079
  remember({ harness: value });
3086
3080
  } }),
3087
- /* @__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 }) }) })
3081
+ /* @__PURE__ */ jsx10(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
3082
+ const next = { ...modes, [harness]: item };
3083
+ setModes(next);
3084
+ setPickerError(null);
3085
+ remember({ modes: next });
3086
+ } }),
3087
+ /* @__PURE__ */ jsx10("span", { children: /* @__PURE__ */ jsx10("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", 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 }) }) })
3088
3088
  ] })
3089
3089
  ] })
3090
3090
  ] })
@@ -3193,6 +3193,7 @@ export {
3193
3193
  ContextCandidates,
3194
3194
  ContinuationBar,
3195
3195
  Conversation,
3196
+ ExecutionModeSelect,
3196
3197
  HarnessAdvisory,
3197
3198
  HarnessLogo,
3198
3199
  HarnessPicker,
@@ -295,13 +295,23 @@ function useAutosizeTextarea(ref, value) {
295
295
  // src/composer.jsx
296
296
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
297
297
  var composerMemory = /* @__PURE__ */ new Map();
298
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
299
+ if (modes.length < 2) return null;
300
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
301
+ /* @__PURE__ */ jsx3("span", { children: label }),
302
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
303
+ ] });
304
+ }
298
305
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
306
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
307
+ const [selection, setSelection] = useState2({ key: null, mode: null });
308
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
309
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
299
310
  if (state.mode !== "mirror" || state.canSend) return null;
300
311
  const attached = state.attached;
301
312
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
302
313
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
303
314
  const resume = canContinueHere(state);
304
- const terminal = resume && state.continuationModes?.includes("terminal");
305
315
  const join = state.canAttach;
306
316
  const branch = state.canBranch;
307
317
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -314,8 +324,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
314
324
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
315
325
  ] }),
316
326
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
317
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
318
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
327
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
328
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
319
329
  ] })
320
330
  ] });
321
331
  }
@@ -536,5 +546,6 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
536
546
  }
537
547
  export {
538
548
  Composer,
539
- ContinuationBar
549
+ ContinuationBar,
550
+ ExecutionModeSelect
540
551
  };
@@ -750,7 +750,7 @@ function normalizeUiState(value) {
750
750
  if (!item || typeof item.id !== "string") return [];
751
751
  const startable = item.startable === true;
752
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;
753
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
754
754
  const capabilities = record(item.capabilities);
755
755
  return [{
756
756
  id: item.id,
@@ -1257,13 +1257,23 @@ function useAutosizeTextarea(ref, value) {
1257
1257
  // src/composer.jsx
1258
1258
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1259
1259
  var composerMemory = /* @__PURE__ */ new Map();
1260
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1261
+ if (modes.length < 2) return null;
1262
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1263
+ /* @__PURE__ */ jsx3("span", { children: label }),
1264
+ /* @__PURE__ */ jsx3("select", { "aria-label": label, value, disabled, onChange: (event) => onChange?.(event.currentTarget.value), children: modes.map((mode) => /* @__PURE__ */ jsx3("option", { value: mode, children: mode === "terminal" ? "Terminal" : "Headless" }, mode)) })
1265
+ ] });
1266
+ }
1260
1267
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1268
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1269
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1270
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1271
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1261
1272
  if (state.mode !== "mirror" || state.canSend) return null;
1262
1273
  const attached = state.attached;
1263
1274
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1264
1275
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1265
1276
  const resume = canContinueHere(state);
1266
- const terminal = resume && state.continuationModes?.includes("terminal");
1267
1277
  const join = state.canAttach;
1268
1278
  const branch = state.canBranch;
1269
1279
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1276,8 +1286,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1276
1286
  /* @__PURE__ */ jsx3("small", { children: join ? "Join the proven live runtime without taking it over." : resume ? `Resume this ${harnessDisplayName(state.harness)} session here.` : "Start an independent continuation." })
1277
1287
  ] }),
1278
1288
  /* @__PURE__ */ jsxs3("span", { className: "scui-continuation-actions", children: [
1279
- /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
1280
- terminal ? /* @__PURE__ */ jsx3("button", { type: "button", className: "scui-secondary", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, { action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
1289
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1290
+ /* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => dispatchConfirmedIntent(adapter, join ? { action: "join" } : resume ? { action: "resume", mode: executionMode } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere })
1281
1291
  ] })
1282
1292
  ] });
1283
1293
  }
@@ -2787,7 +2797,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2787
2797
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2788
2798
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2789
2799
  const rememberedMode = modes[harness];
2790
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2800
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2801
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2791
2802
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2792
2803
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2793
2804
  useAutosizeTextarea(textarea, draft);
@@ -2829,7 +2840,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2829
2840
  lastComposerCommand.current = composerCommand.id;
2830
2841
  if (composerCommand.action === "attach") {
2831
2842
  if (mode === "terminal" && !launchModes.includes("headless")) {
2832
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2843
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2833
2844
  } else {
2834
2845
  try {
2835
2846
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2837,12 +2848,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2837
2848
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2838
2849
  const nextContext = mergeContext(context, attachments.context);
2839
2850
  const nextImages = mergeImages(images, attachments.images);
2840
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2841
- if (nextModes !== modes) setModes(nextModes);
2842
2851
  setContext(nextContext);
2843
2852
  setImages(nextImages);
2844
2853
  setPickerError(null);
2845
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2854
+ remember({ context: nextContext, images: nextImages });
2846
2855
  } catch (error) {
2847
2856
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2848
2857
  }
@@ -2883,7 +2892,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2883
2892
  const allFiles = Array.from(value ?? []);
2884
2893
  if (!allFiles.length) return false;
2885
2894
  if (mode === "terminal") {
2886
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2895
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2887
2896
  return true;
2888
2897
  }
2889
2898
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -2948,21 +2957,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2948
2957
  ] }),
2949
2958
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2950
2959
  /* @__PURE__ */ jsx9(Readiness, { harnesses: state.harnesses, state, adapter }),
2951
- launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { className: "scui-launch-modes", disabled: Boolean(starting), children: [
2952
- /* @__PURE__ */ jsx9("legend", { children: "Run as" }),
2953
- launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
2954
- /* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
2955
- const next = { ...modes, [harness]: item };
2956
- setModes(next);
2957
- setPickerError(null);
2958
- remember({ modes: next });
2959
- } }),
2960
- /* @__PURE__ */ jsxs8("span", { children: [
2961
- /* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
2962
- /* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
2963
- ] })
2964
- ] }, item))
2965
- ] }) : null,
2966
2960
  mode !== "terminal" ? /* @__PURE__ */ jsx9(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
2967
2961
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2968
2962
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -2974,7 +2968,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2974
2968
  remember({ context: next });
2975
2969
  return next;
2976
2970
  }) }),
2977
- terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2971
+ terminalAttachments ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Headless." }) : pickerError ? /* @__PURE__ */ jsx9("small", { className: "scui-context-error", role: "alert", children: pickerError }) : null,
2978
2972
  /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2979
2973
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2980
2974
  event.preventDefault();
@@ -3001,7 +2995,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3001
2995
  setHarness(value);
3002
2996
  remember({ harness: value });
3003
2997
  } }),
3004
- /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
2998
+ /* @__PURE__ */ jsx9(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
2999
+ const next = { ...modes, [harness]: item };
3000
+ setModes(next);
3001
+ setPickerError(null);
3002
+ remember({ modes: next });
3003
+ } }),
3004
+ /* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", className: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start headless session", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { className: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
3005
3005
  ] })
3006
3006
  ] })
3007
3007
  ] })
package/styles.css CHANGED
@@ -185,7 +185,7 @@
185
185
  .scui-plan > summary { display:flex; justify-content:space-between; padding:6px 8px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg-raised) }.scui-plan ol { display:grid; gap:4px; box-sizing:border-box; max-height:140px; margin:5px 0 0; padding:7px 8px 7px 25px; overflow:auto; border:1px solid var(--scui-border); border-radius:7px }.scui-plan li[data-status="completed"] { color:var(--scui-fg); text-decoration:line-through }
186
186
  .scui-working { display:flex; align-items:center; gap:4px; color:var(--scui-muted) }.scui-working > span { color:var(--scui-accent) }.scui-working > i { width:4px; height:4px; border-radius:50%; background:currentColor; animation:scui-dots 1.2s infinite }.scui-working > i:nth-of-type(2) { animation-delay:.15s }.scui-working > i:nth-of-type(3) { animation-delay:.3s }
187
187
 
188
- .scui-continuation { display:flex; align-items:center; gap:8px; padding:7px 9px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-continuation > span:not(.scui-continuation-actions) { display:grid; flex:1 }.scui-continuation small { color:var(--scui-muted); font-size:10px }.scui-continuation-actions { display:flex; align-items:center; gap:5px; flex-wrap:wrap; justify-content:flex-end }.scui-continuation button { padding:5px 8px; border:1px solid var(--scui-accent); border-radius:7px; background:transparent; color:var(--scui-accent); cursor:pointer; white-space:nowrap }.scui-continuation button.scui-secondary { border-color:var(--scui-border-strong); color:var(--scui-fg) }
188
+ .scui-continuation { display:flex; align-items:center; gap:8px; padding:7px 9px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-continuation > span:not(.scui-continuation-actions) { display:grid; flex:1 }.scui-continuation small { color:var(--scui-muted); font-size:10px }.scui-continuation-actions { display:flex; align-items:center; gap:5px; flex-wrap:wrap; justify-content:flex-end }.scui-continuation button { padding:5px 8px; border:1px solid var(--scui-accent); border-radius:7px; background:transparent; color:var(--scui-accent); cursor:pointer; white-space:nowrap }
189
189
  .scui-compose { flex:none; padding:8px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }
190
190
  .scui-context-candidates { display:flex; gap:5px; margin-bottom:6px; overflow-x:auto; padding:1px; scrollbar-width:thin }.scui-context-candidates > button { display:grid; grid-template-columns:auto minmax(0,1fr) auto; flex:0 0 auto; width:min(180px,55vw); min-height:34px; align-items:center; gap:6px; padding:4px 6px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg); color:var(--scui-fg); text-align:left; cursor:pointer }.scui-context-candidates > button:hover:not(:disabled) { border-color:var(--scui-border-strong); background:var(--scui-fill) }.scui-context-candidates > button:disabled { cursor:default; opacity:.58 }.scui-context-candidates img { width:24px; height:24px; border-radius:4px; object-fit:cover }.scui-context-candidates span { display:grid; min-width:0 }.scui-context-candidates strong,.scui-context-candidates small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-context-candidates strong { font-size:10px }.scui-context-candidates small { color:var(--scui-muted); font-size:8.5px }
191
191
  .scui-envelope { display:flex; align-items:flex-end; gap:7px; padding:7px; border:1px solid var(--scui-border-strong); border-radius:16px; background:var(--scui-bg) }.scui-envelope textarea { flex:1; min-width:0; min-height:34px; max-height:150px; resize:none; overflow-y:hidden; border:0; outline:0; background:transparent; color:var(--scui-fg) }.scui-envelope > span { display:flex; gap:4px }
@@ -199,11 +199,11 @@
199
199
  .scui-queue-send { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:transparent; color:var(--scui-muted); cursor:pointer }.scui-queue-send:hover { background:var(--scui-fill); color:var(--scui-fg) }
200
200
  .scui-queue { display:grid; gap:4px; max-height:85px; margin-bottom:6px; overflow:auto; font-size:10.5px }.scui-queue > span { display:flex; gap:6px; padding:4px 6px; border-radius:5px; background:var(--scui-fill) }.scui-queue > span > span { display:grid; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-queue small { color:var(--scui-muted) }.scui-queue > span button { margin-left:auto; border:0; background:transparent }
201
201
  .scui-new-envelope { display:grid; align-items:stretch; gap:3px; padding:7px }.scui-new-envelope textarea { box-sizing:border-box; width:100%; min-height:54px; padding:2px 3px }.scui-new-envelope-controls { display:flex; min-width:0; align-items:center; gap:3px }.scui-new-envelope-controls > span { display:flex; margin-left:auto }.scui-new-envelope-controls .scui-attach { flex-basis:29px; width:29px; height:29px }
202
+ .scui-execution-mode { position:relative; display:flex; flex:none; height:29px; align-items:center; border-radius:8px; color:var(--scui-muted) }.scui-execution-mode > span { position:absolute; overflow:hidden; width:1px; height:1px; clip:rect(0 0 0 0); white-space:nowrap }.scui-execution-mode select { max-width:92px; height:29px; padding:0 19px 0 7px; border:0; border-radius:8px; outline:0; background:transparent; color:var(--scui-muted); cursor:pointer; font:inherit; font-size:10px }.scui-execution-mode select:hover:not(:disabled),.scui-execution-mode select:focus-visible { background-color:var(--scui-fill); color:var(--scui-fg) }.scui-execution-mode select:focus-visible { box-shadow:0 0 0 2px color-mix(in srgb,var(--scui-accent) 35%,transparent) }.scui-execution-mode select:disabled { cursor:default; opacity:.55 }.scui-continuation .scui-execution-mode { height:27px }.scui-continuation .scui-execution-mode select { height:27px; border:1px solid var(--scui-border); background-color:var(--scui-bg) }
202
203
  .scui-harness-menu { position:relative; min-width:0 }.scui-harness-trigger { display:flex; max-width:170px; height:29px; align-items:center; gap:6px; padding:3px 7px 3px 4px; border:0; border-radius:8px; background:transparent; color:var(--scui-fg); cursor:pointer }.scui-harness-trigger:hover:not(:disabled),.scui-harness-trigger:focus-visible,.scui-harness-trigger[aria-expanded="true"] { background:var(--scui-fill) }.scui-harness-trigger:disabled { cursor:default; opacity:.55 }.scui-harness-trigger > strong { min-width:0; overflow:hidden; font-size:10.5px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-harness-trigger > .scui-icon { flex:none; color:var(--scui-muted); transform:rotate(90deg); transition:transform 120ms }.scui-harness-trigger[aria-expanded="true"] > .scui-icon { transform:rotate(-90deg) }
203
204
  .scui-harness-popover { position:absolute; z-index:25; bottom:calc(100% + 7px); left:0; display:grid; box-sizing:border-box; width:min(250px,calc(100vw - 24px)); max-height:min(330px,calc(100dvh - 90px)); overflow:auto; padding:5px; border:1px solid var(--scui-border-strong); border-radius:10px; background:var(--scui-bg-raised); box-shadow:0 12px 34px rgba(0,0,0,.2) }.scui-harness-popover-title { padding:5px 7px; color:var(--scui-muted); font-size:8.5px; font-weight:650; letter-spacing:.055em; text-transform:uppercase }.scui-harness-options { display:grid; gap:1px }.scui-harness-options > button { display:grid; grid-template-columns:auto minmax(0,1fr) 18px; width:100%; min-height:36px; align-items:center; gap:8px; padding:5px 7px; border:0; border-radius:7px; background:transparent; color:var(--scui-fg); text-align:left; cursor:pointer }.scui-harness-options > button:hover,.scui-harness-options > button:focus-visible { background:var(--scui-fill) }.scui-harness-options > button[aria-selected="true"] { background:color-mix(in srgb,var(--scui-accent) 8%,var(--scui-bg-raised)) }.scui-harness-options > button > strong { min-width:0; overflow:hidden; font-size:11px; font-weight:600; text-overflow:ellipsis; white-space:nowrap }.scui-harness-options > button > span { display:grid; color:var(--scui-accent); place-items:center }
204
205
  .scui-harness-manage { margin-top:4px; padding-top:4px; border-top:1px solid var(--scui-border) }.scui-harness-manage > summary { padding:6px 7px; color:var(--scui-muted); cursor:pointer; font-size:9.5px }.scui-harness-manage > div { display:grid; gap:1px }.scui-harness-manage section { display:grid; grid-template-columns:auto minmax(0,1fr) auto; min-height:36px; align-items:center; gap:8px; padding:5px 7px }.scui-harness-manage section > span { display:grid; min-width:0 }.scui-harness-manage section strong,.scui-harness-manage section small { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-harness-manage section strong { font-size:10.5px; font-weight:600 }.scui-harness-manage section small { color:var(--scui-muted); font-size:8.5px }.scui-harness-manage section > button { padding:4px 6px; border:1px solid var(--scui-border); border-radius:6px; background:transparent; color:var(--scui-muted); cursor:pointer; font-size:8.5px }.scui-harness-manage section > button:hover { border-color:var(--scui-border-strong); color:var(--scui-fg) }
205
206
  .scui-readiness { margin:0 0 7px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg) }.scui-readiness > summary { padding:6px 8px; color:var(--scui-muted); cursor:pointer; font-size:10px }.scui-readiness > div { display:grid; border-top:1px solid var(--scui-border) }.scui-readiness section { display:grid; grid-template-columns:auto minmax(0,1fr) auto; align-items:center; gap:7px; padding:7px 8px }.scui-readiness section + section { border-top:1px solid var(--scui-border) }.scui-readiness section > span { display:grid; min-width:0 }.scui-readiness section strong,.scui-readiness section small,.scui-readiness section em { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-readiness section small { color:var(--scui-muted); font-size:9px }.scui-readiness section em { color:var(--scui-muted); font-size:8px; font-style:normal }.scui-readiness section button { padding:4px 6px; border:1px solid var(--scui-border); border-radius:6px; background:transparent; cursor:pointer; font-size:9px }
206
- .scui-launch-modes { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:5px; margin:0 0 7px; padding:0; border:0 }.scui-launch-modes legend { position:absolute; overflow:hidden; width:1px; height:1px; clip:rect(0 0 0 0); white-space:nowrap }.scui-launch-modes label { position:relative; min-width:0; cursor:pointer }.scui-launch-modes input { position:absolute; opacity:0; pointer-events:none }.scui-launch-modes label > span { display:flex; min-height:42px; flex-direction:column; justify-content:center; padding:6px 9px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg); transition:border-color .14s ease,background .14s ease }.scui-launch-modes strong { font-size:11px; font-weight:650 }.scui-launch-modes small { overflow:hidden; color:var(--scui-muted); font-size:9px; text-overflow:ellipsis; white-space:nowrap }.scui-launch-modes input:checked + span { border-color:color-mix(in srgb,var(--scui-accent) 58%,var(--scui-border)); background:color-mix(in srgb,var(--scui-accent) 8%,var(--scui-bg)) }.scui-launch-modes input:focus-visible + span { outline:2px solid var(--scui-accent); outline-offset:1px }.scui-launch-modes:disabled label { cursor:default; opacity:.62 }
207
207
  .scui-new { display:grid; justify-items:center; gap:5px; margin:auto; padding:20px; color:var(--scui-muted); text-align:center }.scui-new > span { color:var(--scui-accent); font-size:28px }.scui-new strong { color:var(--scui-fg) }
208
208
  .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 }
209
209