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

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
 
@@ -204,7 +205,9 @@ and can overlay a machine-wide session inventory, pagination state, attention, d
204
205
  errors, and owned/attached identities without reimplementing transcript or capability semantics.
205
206
  `projectSessionInventory` performs the corresponding title, latest-preview, activity, path, age,
206
207
  sorting, and row projection for raw machine-wide descriptors; the trusted host supplies only its
207
- opaque-key callback and retains the reversible locator map.
208
+ opaque-key callback and retains the reversible locator map. Its `isWritable` capability callback
209
+ is fail-closed: observed sessions remain neutral read-only channels until the host proves a real
210
+ send or terminal-control path.
208
211
 
209
212
  ## Modularity contract
210
213
 
package/activity.mjs CHANGED
@@ -554,6 +554,7 @@ function readSessions(value) {
554
554
  updatedAt: nullableNumber(row.updatedAt),
555
555
  messages: nullableNumber(row.messages),
556
556
  active: row.active === true,
557
+ writable: row.writable === true,
557
558
  live: row.live === true,
558
559
  runtimeStatus: row.runtimeStatus === "running" || row.runtimeStatus === "busy" || row.runtimeStatus === "idle" ? row.runtimeStatus : null
559
560
  }];
@@ -738,7 +739,7 @@ function normalizeUiState(value) {
738
739
  if (!item || typeof item.id !== "string") return [];
739
740
  const startable = item.startable === true;
740
741
  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;
742
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
742
743
  const capabilities = record(item.capabilities);
743
744
  return [{
744
745
  id: item.id,
@@ -783,6 +784,7 @@ function sessionDisplayName(session) {
783
784
  return title && title !== session.name ? title : session.name || "Untitled chat";
784
785
  }
785
786
  function sessionActivity(state, row) {
787
+ if (!row.writable) return "observed";
786
788
  if (state.needsInput && row.active) return "needs-input";
787
789
  if (state.busy && row.active) return "working";
788
790
  if (row.runtimeStatus === "busy") return "working";
@@ -800,6 +802,7 @@ var ACTIVITY_PRIORITY = Object.freeze({
800
802
  finished: 30,
801
803
  running: 20,
802
804
  recent: 10,
805
+ observed: 0,
803
806
  idle: 0
804
807
  });
805
808
  function activityDetail(activity, preview, pill) {
@@ -810,6 +813,7 @@ function activityDetail(activity, preview, pill) {
810
813
  if (activity === "finished") return "Finished";
811
814
  if (activity === "unseen") return "New reply";
812
815
  if (activity === "running") return "Session is running";
816
+ if (activity === "observed") return "Read-only channel";
813
817
  return pill || "Agent chats";
814
818
  }
815
819
  function projectAgentActivity(value) {
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
@@ -566,6 +566,7 @@ function readSessions(value) {
566
566
  updatedAt: nullableNumber(row.updatedAt),
567
567
  messages: nullableNumber(row.messages),
568
568
  active: row.active === true,
569
+ writable: row.writable === true,
569
570
  live: row.live === true,
570
571
  runtimeStatus: row.runtimeStatus === "running" || row.runtimeStatus === "busy" || row.runtimeStatus === "idle" ? row.runtimeStatus : null
571
572
  }];
@@ -750,7 +751,7 @@ function normalizeUiState(value) {
750
751
  if (!item || typeof item.id !== "string") return [];
751
752
  const startable = item.startable === true;
752
753
  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
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
754
755
  const capabilities = record(item.capabilities);
755
756
  return [{
756
757
  id: item.id,
@@ -795,6 +796,7 @@ function sessionDisplayName(session) {
795
796
  return title && title !== session.name ? title : session.name || "Untitled chat";
796
797
  }
797
798
  function sessionActivity(state, row) {
799
+ if (!row.writable) return "observed";
798
800
  if (state.needsInput && row.active) return "needs-input";
799
801
  if (state.busy && row.active) return "working";
800
802
  if (row.runtimeStatus === "busy") return "working";
@@ -812,6 +814,7 @@ var ACTIVITY_PRIORITY = Object.freeze({
812
814
  finished: 30,
813
815
  running: 20,
814
816
  recent: 10,
817
+ observed: 0,
815
818
  idle: 0
816
819
  });
817
820
  function activityDetail(activity, preview, pill) {
@@ -822,6 +825,7 @@ function activityDetail(activity, preview, pill) {
822
825
  if (activity === "finished") return "Finished";
823
826
  if (activity === "unseen") return "New reply";
824
827
  if (activity === "running") return "Session is running";
828
+ if (activity === "observed") return "Read-only channel";
825
829
  return pill || "Agent chats";
826
830
  }
827
831
  function projectAgentActivity(value) {
@@ -1294,13 +1298,23 @@ function useAutosizeTextarea(ref, value) {
1294
1298
  // src/composer.jsx
1295
1299
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1296
1300
  var composerMemory = /* @__PURE__ */ new Map();
1301
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1302
+ if (modes.length < 2) return null;
1303
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1304
+ /* @__PURE__ */ jsx3("span", { children: label }),
1305
+ /* @__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)) })
1306
+ ] });
1307
+ }
1297
1308
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1309
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1310
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1311
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1312
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1298
1313
  if (state.mode !== "mirror" || state.canSend) return null;
1299
1314
  const attached = state.attached;
1300
1315
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1301
1316
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1302
1317
  const resume = canContinueHere(state);
1303
- const terminal = resume && state.continuationModes?.includes("terminal");
1304
1318
  const join = state.canAttach;
1305
1319
  const branch = state.canBranch;
1306
1320
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1313,8 +1327,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1313
1327
  /* @__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
1328
  ] }),
1315
1329
  /* @__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
1330
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1331
+ /* @__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
1332
  ] })
1319
1333
  ] });
1320
1334
  }
@@ -2289,7 +2303,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2289
2303
  const unreadCount = attention?.unreadCount ?? 0;
2290
2304
  const unreadLabel = unreadCount > 99 ? "99+" : String(unreadCount);
2291
2305
  const age = relativeAge(row.previewUpdatedAt ?? row.updatedAt, now) || row.age;
2292
- return /* @__PURE__ */ jsxs7("button", { className: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2306
+ return /* @__PURE__ */ jsxs7("button", { className: "scui-session", "data-active": row.active, "data-activity": activity, "data-writable": row.writable, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2293
2307
  /* @__PURE__ */ jsx8(HarnessLogo, { id: row.harness, activity, size: 34 }),
2294
2308
  /* @__PURE__ */ jsxs7("span", { className: "scui-session-copy", children: [
2295
2309
  /* @__PURE__ */ jsxs7("span", { className: "scui-session-title", children: [
@@ -2870,7 +2884,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2870
2884
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2871
2885
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2872
2886
  const rememberedMode = modes[harness];
2873
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2887
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2888
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2874
2889
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2875
2890
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2876
2891
  useAutosizeTextarea(textarea, draft);
@@ -2912,7 +2927,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2912
2927
  lastComposerCommand.current = composerCommand.id;
2913
2928
  if (composerCommand.action === "attach") {
2914
2929
  if (mode === "terminal" && !launchModes.includes("headless")) {
2915
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2930
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2916
2931
  } else {
2917
2932
  try {
2918
2933
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2920,12 +2935,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2920
2935
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2921
2936
  const nextContext = mergeContext(context, attachments.context);
2922
2937
  const nextImages = mergeImages(images, attachments.images);
2923
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2924
- if (nextModes !== modes) setModes(nextModes);
2925
2938
  setContext(nextContext);
2926
2939
  setImages(nextImages);
2927
2940
  setPickerError(null);
2928
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2941
+ remember({ context: nextContext, images: nextImages });
2929
2942
  } catch (error) {
2930
2943
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2931
2944
  }
@@ -2966,7 +2979,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2966
2979
  const allFiles = Array.from(value ?? []);
2967
2980
  if (!allFiles.length) return false;
2968
2981
  if (mode === "terminal") {
2969
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2982
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2970
2983
  return true;
2971
2984
  }
2972
2985
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -3031,21 +3044,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3031
3044
  ] }),
3032
3045
  /* @__PURE__ */ jsxs9("div", { className: "scui-compose", children: [
3033
3046
  /* @__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
3047
  mode !== "terminal" ? /* @__PURE__ */ jsx10(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
3050
3048
  /* @__PURE__ */ jsx10(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
3051
3049
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -3057,7 +3055,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3057
3055
  remember({ context: next });
3058
3056
  return next;
3059
3057
  }) }),
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,
3058
+ 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
3059
  /* @__PURE__ */ jsxs9("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
3062
3060
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
3063
3061
  event.preventDefault();
@@ -3084,7 +3082,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3084
3082
  setHarness(value);
3085
3083
  remember({ harness: value });
3086
3084
  } }),
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 }) }) })
3085
+ /* @__PURE__ */ jsx10(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
3086
+ const next = { ...modes, [harness]: item };
3087
+ setModes(next);
3088
+ setPickerError(null);
3089
+ remember({ modes: next });
3090
+ } }),
3091
+ /* @__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
3092
  ] })
3089
3093
  ] })
3090
3094
  ] })
@@ -3193,6 +3197,7 @@ export {
3193
3197
  ContextCandidates,
3194
3198
  ContinuationBar,
3195
3199
  Conversation,
3200
+ ExecutionModeSelect,
3196
3201
  HarnessAdvisory,
3197
3202
  HarnessLogo,
3198
3203
  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
@@ -77,6 +77,7 @@ var ACTIVITY_PRIORITY = Object.freeze({
77
77
  finished: 30,
78
78
  running: 20,
79
79
  recent: 10,
80
+ observed: 0,
80
81
  idle: 0
81
82
  });
82
83
  function canContinueHere(state) {
@@ -295,13 +296,23 @@ function useAutosizeTextarea(ref, value) {
295
296
  // src/composer.jsx
296
297
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
297
298
  var composerMemory = /* @__PURE__ */ new Map();
299
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
300
+ if (modes.length < 2) return null;
301
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
302
+ /* @__PURE__ */ jsx3("span", { children: label }),
303
+ /* @__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)) })
304
+ ] });
305
+ }
298
306
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
307
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
308
+ const [selection, setSelection] = useState2({ key: null, mode: null });
309
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
310
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
299
311
  if (state.mode !== "mirror" || state.canSend) return null;
300
312
  const attached = state.attached;
301
313
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
302
314
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
303
315
  const resume = canContinueHere(state);
304
- const terminal = resume && state.continuationModes?.includes("terminal");
305
316
  const join = state.canAttach;
306
317
  const branch = state.canBranch;
307
318
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -314,8 +325,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
314
325
  /* @__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
326
  ] }),
316
327
  /* @__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
328
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
329
+ /* @__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
330
  ] })
320
331
  ] });
321
332
  }
@@ -536,5 +547,6 @@ function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.a
536
547
  }
537
548
  export {
538
549
  Composer,
539
- ContinuationBar
550
+ ContinuationBar,
551
+ ExecutionModeSelect
540
552
  };
package/controller.d.ts CHANGED
@@ -65,6 +65,8 @@ export interface SessionInventoryProjectionOptions {
65
65
  maxSessions?: number;
66
66
  liveWindowMs?: number;
67
67
  preserveOrder?: boolean;
68
+ /** Capability check performed by the trusted host; omitted rows are read-only observations. */
69
+ isWritable?(descriptor: SessionDescriptor): boolean;
68
70
  }
69
71
 
70
72
  export function sessionConversationUpdatedAt(descriptor: SessionDescriptor): number | null;
package/controller.mjs CHANGED
@@ -72,7 +72,7 @@ function descriptorTitle(descriptor) {
72
72
  return compactTopic(nativeTitle);
73
73
  }
74
74
  if (descriptor?.locator?.harness !== 'claude-code' && descriptor?.locator?.harness !== 'codex') {
75
- return compactTopic(nativeTitle || workspace || descriptor?.locator?.session_id?.slice(0, 8)) || 'Untitled chat';
75
+ return compactTopic(nativeTitle || workspace) || 'Untitled chat';
76
76
  }
77
77
  const openingUserMessages = (descriptor?.preview_candidates ?? []).filter(
78
78
  (candidate) => candidate?.role === undefined || candidate.role === 'user',
@@ -125,6 +125,7 @@ export function projectSessionInventory(descriptors, options) {
125
125
  active: active?.sessionId != null
126
126
  && descriptor.locator.harness === active.harness
127
127
  && descriptor.locator.session_id === active.sessionId,
128
+ writable: options.isWritable?.(descriptor) === true,
128
129
  live: updatedAt !== null && now - updatedAt <= liveWindowMs,
129
130
  runtimeStatus: sessionDescriptorRuntimeStatus(descriptor),
130
131
  });
@@ -378,14 +379,14 @@ function attached(snapshot) {
378
379
  harness: snapshot.activeHarness,
379
380
  name: workspaceName(snapshot.workspace),
380
381
  cwd: snapshot.workspace,
381
- title: snapshot.activeSession?.model ?? snapshot.activeSessionId ?? 'Active session',
382
+ title: 'Untitled chat',
382
383
  } : null;
383
384
  return {
384
385
  key: session.key,
385
386
  harness: session.harness,
386
387
  name: workspaceName(session.cwd),
387
388
  cwd: session.cwd ?? '',
388
- title: session.title ?? session.model ?? session.sessionId,
389
+ title: session.title ?? 'Untitled chat',
389
390
  };
390
391
  }
391
392
 
@@ -442,11 +443,12 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
442
443
  harness: session.harness,
443
444
  name: workspaceName(session.cwd),
444
445
  cwd: session.cwd ?? '',
445
- title: session.title ?? session.model ?? session.sessionId,
446
+ title: session.title ?? 'Untitled chat',
446
447
  age: relativeAge(session.updatedAt, now),
447
448
  updatedAt: session.updatedAt ?? null,
448
449
  messages: session.messageCount ?? null,
449
450
  active: session.key === snapshot.activeSessionKey,
451
+ writable: session.key === snapshot.activeSessionKey && actions.send === true,
450
452
  live: typeof session.updatedAt === 'number' && now - session.updatedAt <= liveWindowMs,
451
453
  runtimeStatus: session.liveStatus ?? null,
452
454
  }));
package/conversation.mjs CHANGED
@@ -439,6 +439,7 @@ var ACTIVITY_PRIORITY = Object.freeze({
439
439
  finished: 30,
440
440
  running: 20,
441
441
  recent: 10,
442
+ observed: 0,
442
443
  idle: 0
443
444
  });
444
445
  function groupConversation(entries) {
package/core.mjs CHANGED
@@ -562,6 +562,7 @@ function readSessions(value) {
562
562
  updatedAt: nullableNumber(row.updatedAt),
563
563
  messages: nullableNumber(row.messages),
564
564
  active: row.active === true,
565
+ writable: row.writable === true,
565
566
  live: row.live === true,
566
567
  runtimeStatus: row.runtimeStatus === 'running' || row.runtimeStatus === 'busy' || row.runtimeStatus === 'idle' ? row.runtimeStatus : null,
567
568
  }];
@@ -806,7 +807,9 @@ export function normalizeUiState(value) {
806
807
  : startable ? ['headless'] : [];
807
808
  const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode)
808
809
  ? item.preferredLaunchMode
809
- : launchModes[0] ?? null;
810
+ : launchModes.includes('terminal')
811
+ ? 'terminal'
812
+ : launchModes[0] ?? null;
810
813
  const capabilities = record(item.capabilities);
811
814
  return [{
812
815
  id: item.id,
@@ -856,6 +859,9 @@ export function sessionDisplayName(session) {
856
859
  }
857
860
 
858
861
  export function sessionActivity(state, row) {
862
+ // Runtime activity and interaction authority are deliberately independent. A process may be
863
+ // busy in another terminal, but presenting it as this surface's active agent is misleading.
864
+ if (!row.writable) return 'observed';
859
865
  if (state.needsInput && row.active) return 'needs-input';
860
866
  if (state.busy && row.active) return 'working';
861
867
  // Unread/finished attention has its own badge. It must not suppress the
@@ -876,6 +882,7 @@ const ACTIVITY_PRIORITY = Object.freeze({
876
882
  finished: 30,
877
883
  running: 20,
878
884
  recent: 10,
885
+ observed: 0,
879
886
  idle: 0,
880
887
  });
881
888
 
@@ -887,6 +894,7 @@ function activityDetail(activity, preview, pill) {
887
894
  if (activity === 'finished') return 'Finished';
888
895
  if (activity === 'unseen') return 'New reply';
889
896
  if (activity === 'running') return 'Session is running';
897
+ if (activity === 'observed') return 'Read-only channel';
890
898
  return pill || 'Agent chats';
891
899
  }
892
900
 
package/embed.mjs CHANGED
@@ -569,6 +569,7 @@ function readSessions(value) {
569
569
  updatedAt: nullableNumber(row.updatedAt),
570
570
  messages: nullableNumber(row.messages),
571
571
  active: row.active === true,
572
+ writable: row.writable === true,
572
573
  live: row.live === true,
573
574
  runtimeStatus: row.runtimeStatus === "running" || row.runtimeStatus === "busy" || row.runtimeStatus === "idle" ? row.runtimeStatus : null
574
575
  }];
@@ -753,7 +754,7 @@ function normalizeUiState(value) {
753
754
  if (!item || typeof item.id !== "string") return [];
754
755
  const startable = item.startable === true;
755
756
  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;
757
+ const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes.includes("terminal") ? "terminal" : launchModes[0] ?? null;
757
758
  const capabilities = record(item.capabilities);
758
759
  return [{
759
760
  id: item.id,
@@ -798,6 +799,7 @@ function sessionDisplayName(session) {
798
799
  return title && title !== session.name ? title : session.name || "Untitled chat";
799
800
  }
800
801
  function sessionActivity(state, row) {
802
+ if (!row.writable) return "observed";
801
803
  if (state.needsInput && row.active) return "needs-input";
802
804
  if (state.busy && row.active) return "working";
803
805
  if (row.runtimeStatus === "busy") return "working";
@@ -815,6 +817,7 @@ var ACTIVITY_PRIORITY = Object.freeze({
815
817
  finished: 30,
816
818
  running: 20,
817
819
  recent: 10,
820
+ observed: 0,
818
821
  idle: 0
819
822
  });
820
823
  function filterSessions(rows, query) {
@@ -1260,13 +1263,23 @@ function useAutosizeTextarea(ref, value) {
1260
1263
  // src/composer.jsx
1261
1264
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1262
1265
  var composerMemory = /* @__PURE__ */ new Map();
1266
+ function ExecutionModeSelect({ modes = [], value, onChange, disabled = false, label = "Run in" }) {
1267
+ if (modes.length < 2) return null;
1268
+ return /* @__PURE__ */ jsxs3("label", { className: "scui-execution-mode", title: disabled ? void 0 : `${label}: ${value}`, children: [
1269
+ /* @__PURE__ */ jsx3("span", { children: label }),
1270
+ /* @__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)) })
1271
+ ] });
1272
+ }
1263
1273
  function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1274
+ const continuationModes = state.continuationModes?.length ? state.continuationModes : ["headless"];
1275
+ const [selection, setSelection] = useState2({ key: null, mode: null });
1276
+ const requestedMode = selection.key === state.attached?.key ? selection.mode : null;
1277
+ const executionMode = continuationModes.includes(requestedMode) ? requestedMode : continuationModes.includes("terminal") ? "terminal" : continuationModes[0];
1264
1278
  if (state.mode !== "mirror" || state.canSend) return null;
1265
1279
  const attached = state.attached;
1266
1280
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1267
1281
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1268
1282
  const resume = canContinueHere(state);
1269
- const terminal = resume && state.continuationModes?.includes("terminal");
1270
1283
  const join = state.canAttach;
1271
1284
  const branch = state.canBranch;
1272
1285
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1279,8 +1292,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1279
1292
  /* @__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
1293
  ] }),
1281
1294
  /* @__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
1295
+ !join && resume ? /* @__PURE__ */ jsx3(ExecutionModeSelect, { modes: continuationModes, value: executionMode, disabled: Boolean(state.operation), onChange: (mode) => setSelection({ key: state.attached?.key ?? null, mode }) }) : null,
1296
+ /* @__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
1297
  ] })
1285
1298
  ] });
1286
1299
  }
@@ -2209,7 +2222,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2209
2222
  const unreadCount = attention?.unreadCount ?? 0;
2210
2223
  const unreadLabel = unreadCount > 99 ? "99+" : String(unreadCount);
2211
2224
  const age = relativeAge(row.previewUpdatedAt ?? row.updatedAt, now) || row.age;
2212
- return /* @__PURE__ */ jsxs6("button", { className: "scui-session", "data-active": row.active, "data-activity": activity, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2225
+ return /* @__PURE__ */ jsxs6("button", { className: "scui-session", "data-active": row.active, "data-activity": activity, "data-writable": row.writable, "data-session-key": row.key, type: "button", "aria-label": `${title} \xB7 ${harnessDisplayName(row.harness)}${row.writable ? "" : " \xB7 Read-only"}${working ? " \xB7 Working" : ""}${path.complete ? ` \xB7 ${path.complete}` : ""}${preview ? ` \xB7 ${preview}` : ""}${unreadCount ? ` \xB7 ${unreadCount} unread` : ""}${age ? ` \xB7 ${age}` : ""}`, "aria-current": row.active ? "true" : void 0, onClick: () => onOpen(row), children: [
2213
2226
  /* @__PURE__ */ jsx7(HarnessLogo, { id: row.harness, activity, size: 34 }),
2214
2227
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-copy", children: [
2215
2228
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-title", children: [
@@ -2790,7 +2803,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2790
2803
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2791
2804
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2792
2805
  const rememberedMode = modes[harness];
2793
- const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2806
+ const requestedMode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
2807
+ const mode = (context.length > 0 || images.length > 0) && launchModes.includes("headless") ? "headless" : requestedMode;
2794
2808
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2795
2809
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2796
2810
  useAutosizeTextarea(textarea, draft);
@@ -2832,7 +2846,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2832
2846
  lastComposerCommand.current = composerCommand.id;
2833
2847
  if (composerCommand.action === "attach") {
2834
2848
  if (mode === "terminal" && !launchModes.includes("headless")) {
2835
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2849
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2836
2850
  } else {
2837
2851
  try {
2838
2852
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2840,12 +2854,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2840
2854
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2841
2855
  const nextContext = mergeContext(context, attachments.context);
2842
2856
  const nextImages = mergeImages(images, attachments.images);
2843
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2844
- if (nextModes !== modes) setModes(nextModes);
2845
2857
  setContext(nextContext);
2846
2858
  setImages(nextImages);
2847
2859
  setPickerError(null);
2848
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2860
+ remember({ context: nextContext, images: nextImages });
2849
2861
  } catch (error) {
2850
2862
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2851
2863
  }
@@ -2886,7 +2898,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2886
2898
  const allFiles = Array.from(value ?? []);
2887
2899
  if (!allFiles.length) return false;
2888
2900
  if (mode === "terminal") {
2889
- setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
2901
+ setPickerError("Terminal starts currently accept text only. Choose Headless to attach context or images.");
2890
2902
  return true;
2891
2903
  }
2892
2904
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -2951,21 +2963,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2951
2963
  ] }),
2952
2964
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2953
2965
  /* @__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
2966
  mode !== "terminal" ? /* @__PURE__ */ jsx9(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
2970
2967
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2971
2968
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -2977,7 +2974,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2977
2974
  remember({ context: next });
2978
2975
  return next;
2979
2976
  }) }),
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,
2977
+ 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
2978
  /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2982
2979
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2983
2980
  event.preventDefault();
@@ -3004,7 +3001,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3004
3001
  setHarness(value);
3005
3002
  remember({ harness: value });
3006
3003
  } }),
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 }) }) })
3004
+ /* @__PURE__ */ jsx9(ExecutionModeSelect, { modes: launchModes, value: mode, disabled: Boolean(starting) || mode !== requestedMode, onChange: (item) => {
3005
+ const next = { ...modes, [harness]: item };
3006
+ setModes(next);
3007
+ setPickerError(null);
3008
+ remember({ modes: next });
3009
+ } }),
3010
+ /* @__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
3011
  ] })
3009
3012
  ] })
3010
3013
  ] })