@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 +8 -7
- package/activity.mjs +1 -1
- package/components.d.ts +1 -0
- package/components.mjs +28 -27
- package/composer.d.ts +1 -1
- package/composer.mjs +15 -4
- package/core.mjs +3 -1
- package/embed.mjs +27 -27
- package/index.d.ts +3 -2
- package/messenger.mjs +27 -27
- package/package.json +1 -1
- package/react/activity.mjs +1 -1
- package/react/components.mjs +28 -27
- package/react/composer.mjs +15 -4
- package/react/messenger.mjs +27 -27
- package/styles.css +2 -2
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
|
|
187
|
-
`terminal` to `continuationModes` and handle `onResumeTerminal`;
|
|
188
|
-
|
|
189
|
-
handoff, and never presents a terminal
|
|
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
|
|
193
|
-
|
|
194
|
-
|
|
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
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(
|
|
1317
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
318
|
-
|
|
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
|
|
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(
|
|
1283
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
1280
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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
package/react/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/react/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 "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(
|
|
1317
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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/react/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 "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(
|
|
318
|
-
|
|
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/react/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 "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(
|
|
1280
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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 }
|
|
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
|
|