@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/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export type ControlStrategy = 'start' | 'resume' | 'attach' | 'branch' | 'reduce
8
8
  export type ExecutionMode = 'headless' | 'terminal';
9
9
  /** @deprecated Use ExecutionMode. */
10
10
  export type ContinuationMode = ExecutionMode;
11
- export type SessionActivity = 'idle' | 'recent' | 'running' | 'working' | 'needs-input' | 'finished' | 'failed' | 'unseen';
11
+ export type SessionActivity = 'idle' | 'observed' | 'recent' | 'running' | 'working' | 'needs-input' | 'finished' | 'failed' | 'unseen';
12
12
 
13
13
  export interface AgentActivityModel {
14
14
  harness: HarnessId | '';
@@ -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
 
@@ -58,6 +58,8 @@ export interface SessionRowModel {
58
58
  updatedAt: number | null;
59
59
  messages: number | null;
60
60
  active: boolean;
61
+ /** This surface currently has a real send or terminal-control path for the session. */
62
+ writable: boolean;
61
63
  live: boolean;
62
64
  runtimeStatus: 'running' | 'busy' | 'idle' | null;
63
65
  }
@@ -264,7 +266,7 @@ export interface SupercodeUiState {
264
266
  canSend: boolean;
265
267
  canSteer: boolean;
266
268
  canResume: boolean;
267
- /** Execution strategies the current host can actually provide. Headless is the default. */
269
+ /** Execution transports the current host can actually provide. */
268
270
  continuationModes: ContinuationMode[];
269
271
  canBranch: boolean;
270
272
  canAttach: boolean;
@@ -557,6 +559,7 @@ export function Conversation(props: { state: SupercodeUiState; adapter: UiAdapte
557
559
  export function SessionRow(props: { row: SessionRowModel; state: SupercodeUiState; adapter: UiAdapter; now?: number; onOpen(row: SessionRowModel): void }): VNode;
558
560
  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
561
  export function ContinuationBar(props: { state: SupercodeUiState; adapter: UiAdapter; labels?: MessengerLabels }): VNode | null;
562
+ export function ExecutionModeSelect(props: { modes?: ExecutionMode[]; value: ExecutionMode; onChange?(mode: ExecutionMode): void; disabled?: boolean; label?: string }): VNode | null;
560
563
  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
564
  export function SupercodeMessenger(props: MessengerProps): VNode;
562
565
 
package/messenger.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 filterSessions(rows, query) {
@@ -1257,13 +1260,23 @@ function useAutosizeTextarea(ref, value) {
1257
1260
  // src/composer.jsx
1258
1261
  import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
1259
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
+ }
1260
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];
1261
1275
  if (state.mode !== "mirror" || state.canSend) return null;
1262
1276
  const attached = state.attached;
1263
1277
  const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
1264
1278
  const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
1265
1279
  const resume = canContinueHere(state);
1266
- const terminal = resume && state.continuationModes?.includes("terminal");
1267
1280
  const join = state.canAttach;
1268
1281
  const branch = state.canBranch;
1269
1282
  if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { className: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
@@ -1276,8 +1289,8 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
1276
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." })
1277
1290
  ] }),
1278
1291
  /* @__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
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 })
1281
1294
  ] })
1282
1295
  ] });
1283
1296
  }
@@ -2206,7 +2219,7 @@ function SessionRow({ row, state, onOpen, now = Date.now() }) {
2206
2219
  const unreadCount = attention?.unreadCount ?? 0;
2207
2220
  const unreadLabel = unreadCount > 99 ? "99+" : String(unreadCount);
2208
2221
  const age = relativeAge(row.previewUpdatedAt ?? row.updatedAt, now) || row.age;
2209
- 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: [
2222
+ 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: [
2210
2223
  /* @__PURE__ */ jsx7(HarnessLogo, { id: row.harness, activity, size: 34 }),
2211
2224
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-copy", children: [
2212
2225
  /* @__PURE__ */ jsxs6("span", { className: "scui-session-title", children: [
@@ -2787,7 +2800,8 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2787
2800
  const Readiness = components.HarnessReadiness ?? HarnessReadiness;
2788
2801
  const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
2789
2802
  const rememberedMode = modes[harness];
2790
- 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;
2791
2805
  const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
2792
2806
  const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
2793
2807
  useAutosizeTextarea(textarea, draft);
@@ -2829,7 +2843,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2829
2843
  lastComposerCommand.current = composerCommand.id;
2830
2844
  if (composerCommand.action === "attach") {
2831
2845
  if (mode === "terminal" && !launchModes.includes("headless")) {
2832
- 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.");
2833
2847
  } else {
2834
2848
  try {
2835
2849
  const attachments = partitionAttachments(composerCommand.attachments);
@@ -2837,12 +2851,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2837
2851
  if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
2838
2852
  const nextContext = mergeContext(context, attachments.context);
2839
2853
  const nextImages = mergeImages(images, attachments.images);
2840
- const nextModes = mode === "terminal" ? { ...modes, [harness]: "headless" } : modes;
2841
- if (nextModes !== modes) setModes(nextModes);
2842
2854
  setContext(nextContext);
2843
2855
  setImages(nextImages);
2844
2856
  setPickerError(null);
2845
- remember({ context: nextContext, images: nextImages, modes: nextModes });
2857
+ remember({ context: nextContext, images: nextImages });
2846
2858
  } catch (error) {
2847
2859
  setPickerError(error instanceof Error ? error.message : "Could not attach context.");
2848
2860
  }
@@ -2883,7 +2895,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2883
2895
  const allFiles = Array.from(value ?? []);
2884
2896
  if (!allFiles.length) return false;
2885
2897
  if (mode === "terminal") {
2886
- 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.");
2887
2899
  return true;
2888
2900
  }
2889
2901
  const files = allFiles.filter((file) => file.type.startsWith("image/"));
@@ -2948,21 +2960,6 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2948
2960
  ] }),
2949
2961
  /* @__PURE__ */ jsxs8("div", { className: "scui-compose", children: [
2950
2962
  /* @__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
2963
  mode !== "terminal" ? /* @__PURE__ */ jsx9(ContextCandidates, { items: contextCandidates, context, images, state, adapter, onAttach: attachCandidate, component: components.ContextCandidate }) : null,
2967
2964
  /* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
2968
2965
  const next = items.filter((_, itemIndex) => itemIndex !== index);
@@ -2974,7 +2971,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
2974
2971
  remember({ context: next });
2975
2972
  return next;
2976
2973
  }) }),
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,
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,
2978
2975
  /* @__PURE__ */ jsxs8("div", { className: `scui-envelope scui-new-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
2979
2976
  if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
2980
2977
  event.preventDefault();
@@ -3001,7 +2998,13 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
3001
2998
  setHarness(value);
3002
2999
  remember({ harness: value });
3003
3000
  } }),
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 }) }) })
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 }) }) })
3005
3008
  ] })
3006
3009
  ] })
3007
3010
  ] })
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.46",
4
4
  "type": "module",
5
5
  "description": "Composable default UI kit for Supercode-powered coding-agent experiences",
6
6
  "exports": {
@@ -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) {
@@ -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 "react/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,
@@ -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 "react/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
  };
@@ -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) {