@volter-ai-dev/supercode-ui 0.1.31 → 0.1.33
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 +12 -0
- package/components.mjs +70 -26
- package/composer.mjs +7 -1
- package/controller.d.ts +5 -0
- package/controller.mjs +14 -1
- package/conversation.mjs +2 -0
- package/core.d.ts +1 -0
- package/core.mjs +20 -3
- package/embed.mjs +70 -26
- package/index.d.ts +12 -2
- package/messenger.mjs +70 -26
- package/package.json +1 -1
- package/sessions.mjs +3 -1
- package/settings.mjs +2 -0
- package/styles.css +2 -1
package/README.md
CHANGED
|
@@ -98,6 +98,18 @@ drafts, and artifact materialization remain explicit callbacks. A
|
|
|
98
98
|
browser should receive projected state from a trusted host rather than instantiate a local
|
|
99
99
|
controller or gain filesystem authority.
|
|
100
100
|
|
|
101
|
+
Native continuation is headless by default. A host with a real terminal provider can add
|
|
102
|
+
`terminal` to `continuationModes` and handle `onResumeTerminal`; the continuation bar then exposes
|
|
103
|
+
that strategy beside “Continue here.” The UI never infers terminal support from a generic runtime
|
|
104
|
+
handoff, and never presents a terminal strategy when the host cannot create one.
|
|
105
|
+
|
|
106
|
+
New-session execution is advertised per harness. Set `launchModes: ['headless', 'terminal']` and
|
|
107
|
+
`preferredLaunchMode` on a `HarnessOption`; the complete messenger renders a compact “Chat” versus
|
|
108
|
+
“Terminal” choice and emits that `mode` on the `new` intent. Direct controller bindings keep Chat as
|
|
109
|
+
the standard path and delegate Terminal only through `onStartTerminal`, so a terminal selection can
|
|
110
|
+
never accidentally create a second headless runtime. Hosts may remember the preference per harness;
|
|
111
|
+
the messenger also retains the current browser draft choice while its New Chat view is open.
|
|
112
|
+
|
|
101
113
|
The default controller projection is display-bounded: 120 visible transcript rows, at most 480
|
|
102
114
|
native entries inspected to fill that tail, 16,000 characters per independent entry field, 100
|
|
103
115
|
session rows, 20 fidelity-residue details, and 50 subagents. It filters harness-injected context
|
package/components.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
19
|
continueHere: "Continue here",
|
|
20
|
+
continueWithTerminal: "Continue with terminal",
|
|
20
21
|
joinLive: "Join live",
|
|
21
22
|
forkHere: "Fork here"
|
|
22
23
|
});
|
|
@@ -32,6 +33,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
33
|
strategy: null,
|
|
33
34
|
canSend: false,
|
|
34
35
|
canResume: false,
|
|
36
|
+
continuationModes: Object.freeze([]),
|
|
35
37
|
canBranch: false,
|
|
36
38
|
canAttach: false,
|
|
37
39
|
canDetach: false,
|
|
@@ -704,6 +706,8 @@ function normalizeUiState(value) {
|
|
|
704
706
|
const pill = record(raw.pill);
|
|
705
707
|
const history = record(raw.history);
|
|
706
708
|
const attachError = record(raw.attachError);
|
|
709
|
+
const canResume = raw.canResume === true;
|
|
710
|
+
const continuationModes = Array.isArray(raw.continuationModes) ? [...new Set(raw.continuationModes.filter((mode) => mode === "headless" || mode === "terminal"))] : canResume ? ["headless"] : [];
|
|
707
711
|
return {
|
|
708
712
|
pill: { tone: ["live", "warn", "dead"].includes(pill?.tone) ? pill.tone : "off", label: string(pill?.label, "connecting\u2026") },
|
|
709
713
|
startup: STARTUP.has(raw.startup) ? raw.startup : "connecting",
|
|
@@ -715,7 +719,8 @@ function normalizeUiState(value) {
|
|
|
715
719
|
mode: MODES.has(raw.mode) ? raw.mode : "none",
|
|
716
720
|
strategy: STRATEGIES.has(raw.strategy) ? raw.strategy : null,
|
|
717
721
|
canSend: raw.canSend === true,
|
|
718
|
-
canResume
|
|
722
|
+
canResume,
|
|
723
|
+
continuationModes,
|
|
719
724
|
canBranch: raw.canBranch === true,
|
|
720
725
|
canAttach: raw.canAttach === true,
|
|
721
726
|
canDetach: raw.canDetach === true,
|
|
@@ -739,7 +744,11 @@ function normalizeUiState(value) {
|
|
|
739
744
|
recoverable: raw.recoverable === true,
|
|
740
745
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
741
746
|
const item = record(candidate);
|
|
742
|
-
|
|
747
|
+
if (!item || typeof item.id !== "string") return [];
|
|
748
|
+
const startable = item.startable === true;
|
|
749
|
+
const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
|
|
750
|
+
const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
|
|
751
|
+
return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
|
|
743
752
|
}) : [],
|
|
744
753
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
745
754
|
savedDraft: string(raw.savedDraft),
|
|
@@ -763,9 +772,9 @@ function sessionDisplayName(session) {
|
|
|
763
772
|
function sessionActivity(state, row) {
|
|
764
773
|
if (state.needsInput && row.active) return "needs-input";
|
|
765
774
|
if (state.busy && row.active) return "working";
|
|
775
|
+
if (row.runtimeStatus === "busy") return "working";
|
|
766
776
|
const attention = state.attention.find((item) => item.key === row.key)?.kind;
|
|
767
777
|
if (attention) return attention;
|
|
768
|
-
if (row.runtimeStatus === "busy") return "working";
|
|
769
778
|
if (row.runtimeStatus === "running") return "running";
|
|
770
779
|
if (row.live || row.runtimeStatus === "idle") return "recent";
|
|
771
780
|
return "idle";
|
|
@@ -1173,6 +1182,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1173
1182
|
const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
|
|
1174
1183
|
const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
|
|
1175
1184
|
const resume = canContinueHere(state);
|
|
1185
|
+
const terminal = resume && state.continuationModes?.includes("terminal");
|
|
1176
1186
|
const join = state.canAttach;
|
|
1177
1187
|
const branch = state.canBranch;
|
|
1178
1188
|
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
@@ -1184,7 +1194,10 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1184
1194
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
1185
1195
|
/* @__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." })
|
|
1186
1196
|
] }),
|
|
1187
|
-
/* @__PURE__ */
|
|
1197
|
+
/* @__PURE__ */ jsxs3("span", { class: "scui-continuation-actions", children: [
|
|
1198
|
+
/* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
|
|
1199
|
+
terminal ? /* @__PURE__ */ jsx3("button", { type: "button", class: "scui-secondary", disabled: Boolean(state.operation), onClick: () => adapter.onIntent({ action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
|
|
1200
|
+
] })
|
|
1188
1201
|
] });
|
|
1189
1202
|
}
|
|
1190
1203
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
@@ -2541,32 +2554,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2541
2554
|
}
|
|
2542
2555
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2543
2556
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2544
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2545
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2557
|
+
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2558
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
2546
2559
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2547
2560
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2548
2561
|
const [context, setContext] = useState6(remembered.context);
|
|
2549
2562
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2563
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2550
2564
|
const [starting, setStarting] = useState6(null);
|
|
2551
2565
|
const [picking, setPicking] = useState6(false);
|
|
2552
2566
|
const [dragging, setDragging] = useState6(false);
|
|
2553
2567
|
const [pickerError, setPickerError] = useState6(null);
|
|
2554
2568
|
const startSequence = useRef7(0);
|
|
2555
2569
|
const textarea = useRef7(null);
|
|
2570
|
+
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2571
|
+
const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
|
|
2572
|
+
const rememberedMode = modes[harness];
|
|
2573
|
+
const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
|
|
2574
|
+
const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
|
|
2575
|
+
const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
|
|
2556
2576
|
useAutosizeTextarea(textarea, draft);
|
|
2557
2577
|
useEffect8(() => {
|
|
2558
2578
|
if (startable.some((item) => item.id === harness)) return;
|
|
2559
2579
|
const next = startable[0]?.id ?? "";
|
|
2560
2580
|
setHarness(next);
|
|
2561
|
-
|
|
2581
|
+
remember({ harness: next });
|
|
2562
2582
|
}, [harness, startableKey]);
|
|
2563
2583
|
useEffect8(() => {
|
|
2564
|
-
if (!starting) return;
|
|
2584
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2565
2585
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2566
2586
|
const beganWorking = !starting.busy && state.busy;
|
|
2567
2587
|
if (!sessionChanged && !beganWorking) return;
|
|
2568
2588
|
newChatMemory.delete(memoryKey);
|
|
2569
|
-
onStarted();
|
|
2589
|
+
onStarted("headless");
|
|
2570
2590
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2571
2591
|
useEffect8(() => {
|
|
2572
2592
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2575,7 +2595,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2575
2595
|
textarea.current?.focus({ preventScroll: true });
|
|
2576
2596
|
}, []);
|
|
2577
2597
|
const pickContext = () => {
|
|
2578
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2598
|
+
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2579
2599
|
setPicking(true);
|
|
2580
2600
|
setPickerError(null);
|
|
2581
2601
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2584,12 +2604,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2584
2604
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2585
2605
|
setContext((current) => {
|
|
2586
2606
|
const next = mergeContext(current, attachments.context);
|
|
2587
|
-
|
|
2607
|
+
remember({ context: next });
|
|
2588
2608
|
return next;
|
|
2589
2609
|
});
|
|
2590
2610
|
setImages((current) => {
|
|
2591
2611
|
const next = mergeImages(current, attachments.images);
|
|
2592
|
-
|
|
2612
|
+
remember({ images: next });
|
|
2593
2613
|
return next;
|
|
2594
2614
|
});
|
|
2595
2615
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2597,6 +2617,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2597
2617
|
const addImageFiles = (value, source) => {
|
|
2598
2618
|
const allFiles = Array.from(value ?? []);
|
|
2599
2619
|
if (!allFiles.length) return false;
|
|
2620
|
+
if (mode === "terminal") {
|
|
2621
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2622
|
+
return true;
|
|
2623
|
+
}
|
|
2600
2624
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2601
2625
|
if (files.length !== allFiles.length) {
|
|
2602
2626
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2610,7 +2634,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2610
2634
|
setPickerError(null);
|
|
2611
2635
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2612
2636
|
const next = mergeImages(current, picked);
|
|
2613
|
-
|
|
2637
|
+
remember({ images: next });
|
|
2614
2638
|
return next;
|
|
2615
2639
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2616
2640
|
return true;
|
|
@@ -2624,12 +2648,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2624
2648
|
};
|
|
2625
2649
|
const send = () => {
|
|
2626
2650
|
const text = draft.trim();
|
|
2627
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2651
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2628
2652
|
const id = startSequence.current + 1;
|
|
2629
2653
|
startSequence.current = id;
|
|
2630
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2631
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2632
|
-
if (
|
|
2654
|
+
setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2655
|
+
const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2656
|
+
if (mode === "terminal") {
|
|
2657
|
+
Promise.resolve(result).then(() => {
|
|
2658
|
+
newChatMemory.delete(memoryKey);
|
|
2659
|
+
onStarted("terminal");
|
|
2660
|
+
}, () => setStarting((current) => current?.id === id ? null : current));
|
|
2661
|
+
} else if (result && typeof result.then === "function") {
|
|
2633
2662
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2634
2663
|
}
|
|
2635
2664
|
};
|
|
@@ -2644,7 +2673,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2644
2673
|
] }),
|
|
2645
2674
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2646
2675
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2647
|
-
operationLabel("start")
|
|
2676
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2648
2677
|
] }) : null,
|
|
2649
2678
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2650
2679
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2658,23 +2687,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2658
2687
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2659
2688
|
const value = event.currentTarget.value;
|
|
2660
2689
|
setHarness(value);
|
|
2661
|
-
|
|
2690
|
+
remember({ harness: value });
|
|
2662
2691
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2663
2692
|
item.label,
|
|
2664
2693
|
item.startable ? "" : " \xB7 unavailable"
|
|
2665
2694
|
] }, item.id)) })
|
|
2666
2695
|
] }),
|
|
2696
|
+
launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
|
|
2697
|
+
/* @__PURE__ */ jsx9("legend", { children: "Run as" }),
|
|
2698
|
+
launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
|
|
2699
|
+
/* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
|
|
2700
|
+
const next = { ...modes, [harness]: item };
|
|
2701
|
+
setModes(next);
|
|
2702
|
+
setPickerError(null);
|
|
2703
|
+
remember({ modes: next });
|
|
2704
|
+
} }),
|
|
2705
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
2706
|
+
/* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
|
|
2707
|
+
/* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
|
|
2708
|
+
] })
|
|
2709
|
+
] }, item))
|
|
2710
|
+
] }) : null,
|
|
2667
2711
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2668
2712
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2669
|
-
|
|
2713
|
+
remember({ images: next });
|
|
2670
2714
|
return next;
|
|
2671
2715
|
}) }),
|
|
2672
2716
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2673
2717
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2674
|
-
|
|
2718
|
+
remember({ context: next });
|
|
2675
2719
|
return next;
|
|
2676
2720
|
}) }),
|
|
2677
|
-
pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2721
|
+
terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2678
2722
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2679
2723
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2680
2724
|
event.preventDefault();
|
|
@@ -2685,18 +2729,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2685
2729
|
}, onDragLeave: (event) => {
|
|
2686
2730
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2687
2731
|
}, onDrop: dropImages, children: [
|
|
2688
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2732
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2689
2733
|
/* @__PURE__ */ jsx9("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2690
2734
|
const value = event.currentTarget.value;
|
|
2691
2735
|
setDraft(value);
|
|
2692
|
-
|
|
2736
|
+
remember({ draft: value });
|
|
2693
2737
|
}, onKeyDown: (event) => {
|
|
2694
2738
|
if (isSendKey(event)) {
|
|
2695
2739
|
event.preventDefault();
|
|
2696
2740
|
send();
|
|
2697
2741
|
}
|
|
2698
2742
|
} }),
|
|
2699
|
-
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2743
|
+
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2700
2744
|
] })
|
|
2701
2745
|
] })
|
|
2702
2746
|
] });
|
|
@@ -2733,7 +2777,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2733
2777
|
setListFocus("@new");
|
|
2734
2778
|
setView("new");
|
|
2735
2779
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2736
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
|
|
2780
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey }) : null,
|
|
2737
2781
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2738
2782
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2739
2783
|
setView("list");
|
package/composer.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
19
|
continueHere: "Continue here",
|
|
20
|
+
continueWithTerminal: "Continue with terminal",
|
|
20
21
|
joinLive: "Join live",
|
|
21
22
|
forkHere: "Fork here"
|
|
22
23
|
});
|
|
@@ -32,6 +33,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
33
|
strategy: null,
|
|
33
34
|
canSend: false,
|
|
34
35
|
canResume: false,
|
|
36
|
+
continuationModes: Object.freeze([]),
|
|
35
37
|
canBranch: false,
|
|
36
38
|
canAttach: false,
|
|
37
39
|
canDetach: false,
|
|
@@ -257,6 +259,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
257
259
|
const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
|
|
258
260
|
const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
|
|
259
261
|
const resume = canContinueHere(state);
|
|
262
|
+
const terminal = resume && state.continuationModes?.includes("terminal");
|
|
260
263
|
const join = state.canAttach;
|
|
261
264
|
const branch = state.canBranch;
|
|
262
265
|
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
@@ -268,7 +271,10 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
268
271
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
269
272
|
/* @__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." })
|
|
270
273
|
] }),
|
|
271
|
-
/* @__PURE__ */
|
|
274
|
+
/* @__PURE__ */ jsxs3("span", { class: "scui-continuation-actions", children: [
|
|
275
|
+
/* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
|
|
276
|
+
terminal ? /* @__PURE__ */ jsx3("button", { type: "button", class: "scui-secondary", disabled: Boolean(state.operation), onClick: () => adapter.onIntent({ action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
|
|
277
|
+
] })
|
|
272
278
|
] });
|
|
273
279
|
}
|
|
274
280
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
package/controller.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
} from '@volter-ai-dev/supercode-client';
|
|
9
9
|
import type {
|
|
10
10
|
AttachedSessionModel,
|
|
11
|
+
ContinuationMode,
|
|
11
12
|
SessionAttention,
|
|
12
13
|
SessionRowModel,
|
|
13
14
|
StartupPhase,
|
|
@@ -40,6 +41,8 @@ export interface ClientProjectionOptions {
|
|
|
40
41
|
exportBackTarget?: SessionFormat | null;
|
|
41
42
|
exportReceipt?: SupercodeUiState['exportReceipt'];
|
|
42
43
|
reductionReceipt?: SupercodeUiState['reductionReceipt'];
|
|
44
|
+
/** Host-provided execution strategies. A plain controller projects headless resume only. */
|
|
45
|
+
continuationModes?: ContinuationMode[];
|
|
43
46
|
/** Trusted-host inventory and lifecycle overlays (for example a machine-wide session catalog). */
|
|
44
47
|
sessions?: SessionRowModel[];
|
|
45
48
|
attached?: AttachedSessionModel | null;
|
|
@@ -81,6 +84,8 @@ export interface ControllerBindingOptions {
|
|
|
81
84
|
onAcknowledge?: (key: string) => void | Promise<void>;
|
|
82
85
|
onLoadSessions?: () => void | Promise<void>;
|
|
83
86
|
onLoadEarlier?: () => void | Promise<void>;
|
|
87
|
+
onStartTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'new' }>) => void | Promise<void>;
|
|
88
|
+
onResumeTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'resume' }>) => void | Promise<void>;
|
|
84
89
|
copyText?: UiAdapter['copyText'];
|
|
85
90
|
resolveImage?: UiAdapter['resolveImage'];
|
|
86
91
|
}
|
package/controller.mjs
CHANGED
|
@@ -366,6 +366,7 @@ function projectClientSnapshotInternal(snapshot, options, imageRegistry) {
|
|
|
366
366
|
strategy: snapshot.connection?.strategy ?? null,
|
|
367
367
|
canSend: actions.send === true,
|
|
368
368
|
canResume: actions.resume === true,
|
|
369
|
+
continuationModes: options.continuationModes ?? (actions.resume === true ? ['headless'] : []),
|
|
369
370
|
canBranch: actions.branch === true,
|
|
370
371
|
canAttach: actions.attach === true,
|
|
371
372
|
canDetach: actions.detach === true,
|
|
@@ -446,10 +447,22 @@ async function dispatchStandard(controller, intent, options) {
|
|
|
446
447
|
if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
|
|
447
448
|
if (intent.action === 'send') return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
|
|
448
449
|
if (intent.action === 'new') {
|
|
450
|
+
if (intent.mode === 'terminal') {
|
|
451
|
+
return options.onStartTerminal
|
|
452
|
+
? options.onStartTerminal(intent)
|
|
453
|
+
: options.onUnsupported?.(intent);
|
|
454
|
+
}
|
|
449
455
|
await controller.dispatch({ type: 'start', harness: intent.harness });
|
|
450
456
|
return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
|
|
451
457
|
}
|
|
452
|
-
if (intent.action === 'resume' && active)
|
|
458
|
+
if (intent.action === 'resume' && active) {
|
|
459
|
+
if (intent.mode === 'terminal') {
|
|
460
|
+
return options.onResumeTerminal
|
|
461
|
+
? options.onResumeTerminal(intent)
|
|
462
|
+
: options.onUnsupported?.(intent);
|
|
463
|
+
}
|
|
464
|
+
return controller.dispatch({ type: 'resume', sessionKey: active });
|
|
465
|
+
}
|
|
453
466
|
if (intent.action === 'join' && active) return controller.dispatch({ type: 'attach', sessionKey: active });
|
|
454
467
|
if (intent.action === 'detach') return controller.dispatch({ type: 'detach' });
|
|
455
468
|
if (intent.action === 'branch' && active) return controller.dispatch({ type: 'branch', sessionKey: active, ...(intent.targetHarness ? { targetHarness: intent.targetHarness } : {}) });
|
package/conversation.mjs
CHANGED
|
@@ -18,6 +18,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
18
18
|
searchChats: "Search chats",
|
|
19
19
|
askAgent: "Ask your agent\u2026",
|
|
20
20
|
continueHere: "Continue here",
|
|
21
|
+
continueWithTerminal: "Continue with terminal",
|
|
21
22
|
joinLive: "Join live",
|
|
22
23
|
forkHere: "Fork here"
|
|
23
24
|
});
|
|
@@ -33,6 +34,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
33
34
|
strategy: null,
|
|
34
35
|
canSend: false,
|
|
35
36
|
canResume: false,
|
|
37
|
+
continuationModes: Object.freeze([]),
|
|
36
38
|
canBranch: false,
|
|
37
39
|
canAttach: false,
|
|
38
40
|
canDetach: false,
|
package/core.d.ts
CHANGED
package/core.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export const DEFAULT_LABELS = Object.freeze({
|
|
|
14
14
|
searchChats: 'Search chats',
|
|
15
15
|
askAgent: 'Ask your agent…',
|
|
16
16
|
continueHere: 'Continue here',
|
|
17
|
+
continueWithTerminal: 'Continue with terminal',
|
|
17
18
|
joinLive: 'Join live',
|
|
18
19
|
forkHere: 'Fork here',
|
|
19
20
|
});
|
|
@@ -30,6 +31,7 @@ export const EMPTY_UI_STATE = Object.freeze({
|
|
|
30
31
|
strategy: null,
|
|
31
32
|
canSend: false,
|
|
32
33
|
canResume: false,
|
|
34
|
+
continuationModes: Object.freeze([]),
|
|
33
35
|
canBranch: false,
|
|
34
36
|
canAttach: false,
|
|
35
37
|
canDetach: false,
|
|
@@ -754,6 +756,10 @@ export function normalizeUiState(value) {
|
|
|
754
756
|
const pill = record(raw.pill);
|
|
755
757
|
const history = record(raw.history);
|
|
756
758
|
const attachError = record(raw.attachError);
|
|
759
|
+
const canResume = raw.canResume === true;
|
|
760
|
+
const continuationModes = Array.isArray(raw.continuationModes)
|
|
761
|
+
? [...new Set(raw.continuationModes.filter((mode) => mode === 'headless' || mode === 'terminal'))]
|
|
762
|
+
: canResume ? ['headless'] : [];
|
|
757
763
|
return {
|
|
758
764
|
pill: { tone: ['live', 'warn', 'dead'].includes(pill?.tone) ? pill.tone : 'off', label: string(pill?.label, 'connecting…') },
|
|
759
765
|
startup: STARTUP.has(raw.startup) ? raw.startup : 'connecting',
|
|
@@ -765,7 +771,8 @@ export function normalizeUiState(value) {
|
|
|
765
771
|
mode: MODES.has(raw.mode) ? raw.mode : 'none',
|
|
766
772
|
strategy: STRATEGIES.has(raw.strategy) ? raw.strategy : null,
|
|
767
773
|
canSend: raw.canSend === true,
|
|
768
|
-
canResume
|
|
774
|
+
canResume,
|
|
775
|
+
continuationModes,
|
|
769
776
|
canBranch: raw.canBranch === true,
|
|
770
777
|
canAttach: raw.canAttach === true,
|
|
771
778
|
canDetach: raw.canDetach === true,
|
|
@@ -789,7 +796,15 @@ export function normalizeUiState(value) {
|
|
|
789
796
|
recoverable: raw.recoverable === true,
|
|
790
797
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
791
798
|
const item = record(candidate);
|
|
792
|
-
|
|
799
|
+
if (!item || typeof item.id !== 'string') return [];
|
|
800
|
+
const startable = item.startable === true;
|
|
801
|
+
const launchModes = Array.isArray(item.launchModes)
|
|
802
|
+
? [...new Set(item.launchModes.filter((mode) => mode === 'headless' || mode === 'terminal'))]
|
|
803
|
+
: startable ? ['headless'] : [];
|
|
804
|
+
const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode)
|
|
805
|
+
? item.preferredLaunchMode
|
|
806
|
+
: launchModes[0] ?? null;
|
|
807
|
+
return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === 'string' ? item.reason : null, launchModes, preferredLaunchMode }];
|
|
793
808
|
}) : [],
|
|
794
809
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
795
810
|
savedDraft: string(raw.savedDraft),
|
|
@@ -818,9 +833,11 @@ export function sessionDisplayName(session) {
|
|
|
818
833
|
export function sessionActivity(state, row) {
|
|
819
834
|
if (state.needsInput && row.active) return 'needs-input';
|
|
820
835
|
if (state.busy && row.active) return 'working';
|
|
836
|
+
// Unread/finished attention has its own badge. It must not suppress the
|
|
837
|
+
// transient working indicator when the harness proves this row is busy.
|
|
838
|
+
if (row.runtimeStatus === 'busy') return 'working';
|
|
821
839
|
const attention = state.attention.find((item) => item.key === row.key)?.kind;
|
|
822
840
|
if (attention) return attention;
|
|
823
|
-
if (row.runtimeStatus === 'busy') return 'working';
|
|
824
841
|
if (row.runtimeStatus === 'running') return 'running';
|
|
825
842
|
if (row.live || row.runtimeStatus === 'idle') return 'recent';
|
|
826
843
|
return 'idle';
|
package/embed.mjs
CHANGED
|
@@ -20,6 +20,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
20
20
|
searchChats: "Search chats",
|
|
21
21
|
askAgent: "Ask your agent\u2026",
|
|
22
22
|
continueHere: "Continue here",
|
|
23
|
+
continueWithTerminal: "Continue with terminal",
|
|
23
24
|
joinLive: "Join live",
|
|
24
25
|
forkHere: "Fork here"
|
|
25
26
|
});
|
|
@@ -35,6 +36,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
35
36
|
strategy: null,
|
|
36
37
|
canSend: false,
|
|
37
38
|
canResume: false,
|
|
39
|
+
continuationModes: Object.freeze([]),
|
|
38
40
|
canBranch: false,
|
|
39
41
|
canAttach: false,
|
|
40
42
|
canDetach: false,
|
|
@@ -707,6 +709,8 @@ function normalizeUiState(value) {
|
|
|
707
709
|
const pill = record(raw.pill);
|
|
708
710
|
const history = record(raw.history);
|
|
709
711
|
const attachError = record(raw.attachError);
|
|
712
|
+
const canResume = raw.canResume === true;
|
|
713
|
+
const continuationModes = Array.isArray(raw.continuationModes) ? [...new Set(raw.continuationModes.filter((mode) => mode === "headless" || mode === "terminal"))] : canResume ? ["headless"] : [];
|
|
710
714
|
return {
|
|
711
715
|
pill: { tone: ["live", "warn", "dead"].includes(pill?.tone) ? pill.tone : "off", label: string(pill?.label, "connecting\u2026") },
|
|
712
716
|
startup: STARTUP.has(raw.startup) ? raw.startup : "connecting",
|
|
@@ -718,7 +722,8 @@ function normalizeUiState(value) {
|
|
|
718
722
|
mode: MODES.has(raw.mode) ? raw.mode : "none",
|
|
719
723
|
strategy: STRATEGIES.has(raw.strategy) ? raw.strategy : null,
|
|
720
724
|
canSend: raw.canSend === true,
|
|
721
|
-
canResume
|
|
725
|
+
canResume,
|
|
726
|
+
continuationModes,
|
|
722
727
|
canBranch: raw.canBranch === true,
|
|
723
728
|
canAttach: raw.canAttach === true,
|
|
724
729
|
canDetach: raw.canDetach === true,
|
|
@@ -742,7 +747,11 @@ function normalizeUiState(value) {
|
|
|
742
747
|
recoverable: raw.recoverable === true,
|
|
743
748
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
744
749
|
const item = record(candidate);
|
|
745
|
-
|
|
750
|
+
if (!item || typeof item.id !== "string") return [];
|
|
751
|
+
const startable = item.startable === true;
|
|
752
|
+
const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
|
|
753
|
+
const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
|
|
754
|
+
return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
|
|
746
755
|
}) : [],
|
|
747
756
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
748
757
|
savedDraft: string(raw.savedDraft),
|
|
@@ -766,9 +775,9 @@ function sessionDisplayName(session) {
|
|
|
766
775
|
function sessionActivity(state, row) {
|
|
767
776
|
if (state.needsInput && row.active) return "needs-input";
|
|
768
777
|
if (state.busy && row.active) return "working";
|
|
778
|
+
if (row.runtimeStatus === "busy") return "working";
|
|
769
779
|
const attention = state.attention.find((item) => item.key === row.key)?.kind;
|
|
770
780
|
if (attention) return attention;
|
|
771
|
-
if (row.runtimeStatus === "busy") return "working";
|
|
772
781
|
if (row.runtimeStatus === "running") return "running";
|
|
773
782
|
if (row.live || row.runtimeStatus === "idle") return "recent";
|
|
774
783
|
return "idle";
|
|
@@ -1179,6 +1188,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1179
1188
|
const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
|
|
1180
1189
|
const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
|
|
1181
1190
|
const resume = canContinueHere(state);
|
|
1191
|
+
const terminal = resume && state.continuationModes?.includes("terminal");
|
|
1182
1192
|
const join = state.canAttach;
|
|
1183
1193
|
const branch = state.canBranch;
|
|
1184
1194
|
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
@@ -1190,7 +1200,10 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1190
1200
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
1191
1201
|
/* @__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." })
|
|
1192
1202
|
] }),
|
|
1193
|
-
/* @__PURE__ */
|
|
1203
|
+
/* @__PURE__ */ jsxs3("span", { class: "scui-continuation-actions", children: [
|
|
1204
|
+
/* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
|
|
1205
|
+
terminal ? /* @__PURE__ */ jsx3("button", { type: "button", class: "scui-secondary", disabled: Boolean(state.operation), onClick: () => adapter.onIntent({ action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
|
|
1206
|
+
] })
|
|
1194
1207
|
] });
|
|
1195
1208
|
}
|
|
1196
1209
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
@@ -2520,32 +2533,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2520
2533
|
}
|
|
2521
2534
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2522
2535
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2523
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2524
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2536
|
+
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2537
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
2525
2538
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2526
2539
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2527
2540
|
const [context, setContext] = useState6(remembered.context);
|
|
2528
2541
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2542
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2529
2543
|
const [starting, setStarting] = useState6(null);
|
|
2530
2544
|
const [picking, setPicking] = useState6(false);
|
|
2531
2545
|
const [dragging, setDragging] = useState6(false);
|
|
2532
2546
|
const [pickerError, setPickerError] = useState6(null);
|
|
2533
2547
|
const startSequence = useRef7(0);
|
|
2534
2548
|
const textarea = useRef7(null);
|
|
2549
|
+
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2550
|
+
const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
|
|
2551
|
+
const rememberedMode = modes[harness];
|
|
2552
|
+
const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
|
|
2553
|
+
const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
|
|
2554
|
+
const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
|
|
2535
2555
|
useAutosizeTextarea(textarea, draft);
|
|
2536
2556
|
useEffect8(() => {
|
|
2537
2557
|
if (startable.some((item) => item.id === harness)) return;
|
|
2538
2558
|
const next = startable[0]?.id ?? "";
|
|
2539
2559
|
setHarness(next);
|
|
2540
|
-
|
|
2560
|
+
remember({ harness: next });
|
|
2541
2561
|
}, [harness, startableKey]);
|
|
2542
2562
|
useEffect8(() => {
|
|
2543
|
-
if (!starting) return;
|
|
2563
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2544
2564
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2545
2565
|
const beganWorking = !starting.busy && state.busy;
|
|
2546
2566
|
if (!sessionChanged && !beganWorking) return;
|
|
2547
2567
|
newChatMemory.delete(memoryKey);
|
|
2548
|
-
onStarted();
|
|
2568
|
+
onStarted("headless");
|
|
2549
2569
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2550
2570
|
useEffect8(() => {
|
|
2551
2571
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2554,7 +2574,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2554
2574
|
textarea.current?.focus({ preventScroll: true });
|
|
2555
2575
|
}, []);
|
|
2556
2576
|
const pickContext = () => {
|
|
2557
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2577
|
+
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2558
2578
|
setPicking(true);
|
|
2559
2579
|
setPickerError(null);
|
|
2560
2580
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2563,12 +2583,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2563
2583
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2564
2584
|
setContext((current) => {
|
|
2565
2585
|
const next = mergeContext(current, attachments.context);
|
|
2566
|
-
|
|
2586
|
+
remember({ context: next });
|
|
2567
2587
|
return next;
|
|
2568
2588
|
});
|
|
2569
2589
|
setImages((current) => {
|
|
2570
2590
|
const next = mergeImages(current, attachments.images);
|
|
2571
|
-
|
|
2591
|
+
remember({ images: next });
|
|
2572
2592
|
return next;
|
|
2573
2593
|
});
|
|
2574
2594
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2576,6 +2596,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2576
2596
|
const addImageFiles = (value, source) => {
|
|
2577
2597
|
const allFiles = Array.from(value ?? []);
|
|
2578
2598
|
if (!allFiles.length) return false;
|
|
2599
|
+
if (mode === "terminal") {
|
|
2600
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2601
|
+
return true;
|
|
2602
|
+
}
|
|
2579
2603
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2580
2604
|
if (files.length !== allFiles.length) {
|
|
2581
2605
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2589,7 +2613,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2589
2613
|
setPickerError(null);
|
|
2590
2614
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2591
2615
|
const next = mergeImages(current, picked);
|
|
2592
|
-
|
|
2616
|
+
remember({ images: next });
|
|
2593
2617
|
return next;
|
|
2594
2618
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2595
2619
|
return true;
|
|
@@ -2603,12 +2627,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2603
2627
|
};
|
|
2604
2628
|
const send = () => {
|
|
2605
2629
|
const text = draft.trim();
|
|
2606
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2630
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2607
2631
|
const id = startSequence.current + 1;
|
|
2608
2632
|
startSequence.current = id;
|
|
2609
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2610
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2611
|
-
if (
|
|
2633
|
+
setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2634
|
+
const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2635
|
+
if (mode === "terminal") {
|
|
2636
|
+
Promise.resolve(result).then(() => {
|
|
2637
|
+
newChatMemory.delete(memoryKey);
|
|
2638
|
+
onStarted("terminal");
|
|
2639
|
+
}, () => setStarting((current) => current?.id === id ? null : current));
|
|
2640
|
+
} else if (result && typeof result.then === "function") {
|
|
2612
2641
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2613
2642
|
}
|
|
2614
2643
|
};
|
|
@@ -2623,7 +2652,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2623
2652
|
] }),
|
|
2624
2653
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2625
2654
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2626
|
-
operationLabel("start")
|
|
2655
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2627
2656
|
] }) : null,
|
|
2628
2657
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2629
2658
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2637,23 +2666,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2637
2666
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2638
2667
|
const value = event.currentTarget.value;
|
|
2639
2668
|
setHarness(value);
|
|
2640
|
-
|
|
2669
|
+
remember({ harness: value });
|
|
2641
2670
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2642
2671
|
item.label,
|
|
2643
2672
|
item.startable ? "" : " \xB7 unavailable"
|
|
2644
2673
|
] }, item.id)) })
|
|
2645
2674
|
] }),
|
|
2675
|
+
launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
|
|
2676
|
+
/* @__PURE__ */ jsx9("legend", { children: "Run as" }),
|
|
2677
|
+
launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
|
|
2678
|
+
/* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
|
|
2679
|
+
const next = { ...modes, [harness]: item };
|
|
2680
|
+
setModes(next);
|
|
2681
|
+
setPickerError(null);
|
|
2682
|
+
remember({ modes: next });
|
|
2683
|
+
} }),
|
|
2684
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
2685
|
+
/* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
|
|
2686
|
+
/* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
|
|
2687
|
+
] })
|
|
2688
|
+
] }, item))
|
|
2689
|
+
] }) : null,
|
|
2646
2690
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2647
2691
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2648
|
-
|
|
2692
|
+
remember({ images: next });
|
|
2649
2693
|
return next;
|
|
2650
2694
|
}) }),
|
|
2651
2695
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2652
2696
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2653
|
-
|
|
2697
|
+
remember({ context: next });
|
|
2654
2698
|
return next;
|
|
2655
2699
|
}) }),
|
|
2656
|
-
pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2700
|
+
terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2657
2701
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2658
2702
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2659
2703
|
event.preventDefault();
|
|
@@ -2664,18 +2708,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2664
2708
|
}, onDragLeave: (event) => {
|
|
2665
2709
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2666
2710
|
}, onDrop: dropImages, children: [
|
|
2667
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2711
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2668
2712
|
/* @__PURE__ */ jsx9("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2669
2713
|
const value = event.currentTarget.value;
|
|
2670
2714
|
setDraft(value);
|
|
2671
|
-
|
|
2715
|
+
remember({ draft: value });
|
|
2672
2716
|
}, onKeyDown: (event) => {
|
|
2673
2717
|
if (isSendKey(event)) {
|
|
2674
2718
|
event.preventDefault();
|
|
2675
2719
|
send();
|
|
2676
2720
|
}
|
|
2677
2721
|
} }),
|
|
2678
|
-
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2722
|
+
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2679
2723
|
] })
|
|
2680
2724
|
] })
|
|
2681
2725
|
] });
|
|
@@ -2712,7 +2756,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2712
2756
|
setListFocus("@new");
|
|
2713
2757
|
setView("new");
|
|
2714
2758
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2715
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
|
|
2759
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey }) : null,
|
|
2716
2760
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2717
2761
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2718
2762
|
setView("list");
|
package/index.d.ts
CHANGED
|
@@ -5,6 +5,9 @@ export type UiTone = 'live' | 'warn' | 'dead' | 'off';
|
|
|
5
5
|
export type StartupPhase = 'connecting' | 'starting' | 'discovering' | 'ready';
|
|
6
6
|
export type SessionMode = 'none' | 'control' | 'mirror';
|
|
7
7
|
export type ControlStrategy = 'start' | 'resume' | 'attach' | 'branch' | 'reduce' | null;
|
|
8
|
+
export type ExecutionMode = 'headless' | 'terminal';
|
|
9
|
+
/** @deprecated Use ExecutionMode. */
|
|
10
|
+
export type ContinuationMode = ExecutionMode;
|
|
8
11
|
export type SessionActivity = 'idle' | 'recent' | 'running' | 'working' | 'needs-input' | 'finished' | 'failed' | 'unseen';
|
|
9
12
|
|
|
10
13
|
export interface HarnessOption {
|
|
@@ -13,6 +16,10 @@ export interface HarnessOption {
|
|
|
13
16
|
installed: boolean;
|
|
14
17
|
startable: boolean;
|
|
15
18
|
reason: string | null;
|
|
19
|
+
/** Host-supported ways to start this harness. Omitted means headless when startable. */
|
|
20
|
+
launchModes?: ExecutionMode[];
|
|
21
|
+
/** Host preference, used unless this browser has a remembered choice for the harness. */
|
|
22
|
+
preferredLaunchMode?: ExecutionMode | null;
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export interface SessionRowModel {
|
|
@@ -234,6 +241,8 @@ export interface SupercodeUiState {
|
|
|
234
241
|
strategy: ControlStrategy;
|
|
235
242
|
canSend: boolean;
|
|
236
243
|
canResume: boolean;
|
|
244
|
+
/** Execution strategies the current host can actually provide. Headless is the default. */
|
|
245
|
+
continuationModes: ContinuationMode[];
|
|
237
246
|
canBranch: boolean;
|
|
238
247
|
canAttach: boolean;
|
|
239
248
|
canDetach: boolean;
|
|
@@ -284,8 +293,8 @@ export type SupercodeUiIntent =
|
|
|
284
293
|
| { action: 'loadEarlier' }
|
|
285
294
|
| { action: 'draft'; text: string }
|
|
286
295
|
| { action: 'send'; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
287
|
-
| { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
288
|
-
| { action: 'resume' }
|
|
296
|
+
| { action: 'new'; harness: HarnessId; mode?: ExecutionMode; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
297
|
+
| { action: 'resume'; mode?: ContinuationMode }
|
|
289
298
|
| { action: 'join' }
|
|
290
299
|
| { action: 'detach' }
|
|
291
300
|
| { action: 'branch'; targetHarness?: HarnessId }
|
|
@@ -316,6 +325,7 @@ export interface MessengerLabels {
|
|
|
316
325
|
searchChats: string;
|
|
317
326
|
askAgent: string;
|
|
318
327
|
continueHere: string;
|
|
328
|
+
continueWithTerminal?: string;
|
|
319
329
|
joinLive: string;
|
|
320
330
|
forkHere: string;
|
|
321
331
|
}
|
package/messenger.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
19
|
continueHere: "Continue here",
|
|
20
|
+
continueWithTerminal: "Continue with terminal",
|
|
20
21
|
joinLive: "Join live",
|
|
21
22
|
forkHere: "Fork here"
|
|
22
23
|
});
|
|
@@ -32,6 +33,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
33
|
strategy: null,
|
|
33
34
|
canSend: false,
|
|
34
35
|
canResume: false,
|
|
36
|
+
continuationModes: Object.freeze([]),
|
|
35
37
|
canBranch: false,
|
|
36
38
|
canAttach: false,
|
|
37
39
|
canDetach: false,
|
|
@@ -704,6 +706,8 @@ function normalizeUiState(value) {
|
|
|
704
706
|
const pill = record(raw.pill);
|
|
705
707
|
const history = record(raw.history);
|
|
706
708
|
const attachError = record(raw.attachError);
|
|
709
|
+
const canResume = raw.canResume === true;
|
|
710
|
+
const continuationModes = Array.isArray(raw.continuationModes) ? [...new Set(raw.continuationModes.filter((mode) => mode === "headless" || mode === "terminal"))] : canResume ? ["headless"] : [];
|
|
707
711
|
return {
|
|
708
712
|
pill: { tone: ["live", "warn", "dead"].includes(pill?.tone) ? pill.tone : "off", label: string(pill?.label, "connecting\u2026") },
|
|
709
713
|
startup: STARTUP.has(raw.startup) ? raw.startup : "connecting",
|
|
@@ -715,7 +719,8 @@ function normalizeUiState(value) {
|
|
|
715
719
|
mode: MODES.has(raw.mode) ? raw.mode : "none",
|
|
716
720
|
strategy: STRATEGIES.has(raw.strategy) ? raw.strategy : null,
|
|
717
721
|
canSend: raw.canSend === true,
|
|
718
|
-
canResume
|
|
722
|
+
canResume,
|
|
723
|
+
continuationModes,
|
|
719
724
|
canBranch: raw.canBranch === true,
|
|
720
725
|
canAttach: raw.canAttach === true,
|
|
721
726
|
canDetach: raw.canDetach === true,
|
|
@@ -739,7 +744,11 @@ function normalizeUiState(value) {
|
|
|
739
744
|
recoverable: raw.recoverable === true,
|
|
740
745
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
741
746
|
const item = record(candidate);
|
|
742
|
-
|
|
747
|
+
if (!item || typeof item.id !== "string") return [];
|
|
748
|
+
const startable = item.startable === true;
|
|
749
|
+
const launchModes = Array.isArray(item.launchModes) ? [...new Set(item.launchModes.filter((mode) => mode === "headless" || mode === "terminal"))] : startable ? ["headless"] : [];
|
|
750
|
+
const preferredLaunchMode = launchModes.includes(item.preferredLaunchMode) ? item.preferredLaunchMode : launchModes[0] ?? null;
|
|
751
|
+
return [{ id: item.id, label: string(item.label, harnessDisplayName(item.id)), installed: item.installed === true, startable, reason: typeof item.reason === "string" ? item.reason : null, launchModes, preferredLaunchMode }];
|
|
743
752
|
}) : [],
|
|
744
753
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
745
754
|
savedDraft: string(raw.savedDraft),
|
|
@@ -763,9 +772,9 @@ function sessionDisplayName(session) {
|
|
|
763
772
|
function sessionActivity(state, row) {
|
|
764
773
|
if (state.needsInput && row.active) return "needs-input";
|
|
765
774
|
if (state.busy && row.active) return "working";
|
|
775
|
+
if (row.runtimeStatus === "busy") return "working";
|
|
766
776
|
const attention = state.attention.find((item) => item.key === row.key)?.kind;
|
|
767
777
|
if (attention) return attention;
|
|
768
|
-
if (row.runtimeStatus === "busy") return "working";
|
|
769
778
|
if (row.runtimeStatus === "running") return "running";
|
|
770
779
|
if (row.live || row.runtimeStatus === "idle") return "recent";
|
|
771
780
|
return "idle";
|
|
@@ -1176,6 +1185,7 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1176
1185
|
const row = attached?.key ? state.sessions.find((item) => item.key === attached.key) : null;
|
|
1177
1186
|
const activeElsewhere = row?.runtimeStatus === "running" || row?.runtimeStatus === "busy" || row?.runtimeStatus === "idle";
|
|
1178
1187
|
const resume = canContinueHere(state);
|
|
1188
|
+
const terminal = resume && state.continuationModes?.includes("terminal");
|
|
1179
1189
|
const join = state.canAttach;
|
|
1180
1190
|
const branch = state.canBranch;
|
|
1181
1191
|
if (!resume && !join && !branch) return /* @__PURE__ */ jsx3("div", { class: "scui-continuation", children: /* @__PURE__ */ jsxs3("span", { children: [
|
|
@@ -1187,7 +1197,10 @@ function ContinuationBar({ state, adapter, labels = DEFAULT_LABELS }) {
|
|
|
1187
1197
|
/* @__PURE__ */ jsx3("strong", { children: activeElsewhere ? "Active elsewhere" : "Read-only" }),
|
|
1188
1198
|
/* @__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." })
|
|
1189
1199
|
] }),
|
|
1190
|
-
/* @__PURE__ */
|
|
1200
|
+
/* @__PURE__ */ jsxs3("span", { class: "scui-continuation-actions", children: [
|
|
1201
|
+
/* @__PURE__ */ jsx3("button", { type: "button", disabled: Boolean(state.operation), onClick: () => adapter.onIntent(join ? { action: "join" } : resume ? { action: "resume", mode: "headless" } : { action: "branch" }), children: join ? labels.joinLive : resume ? labels.continueHere : labels.forkHere }),
|
|
1202
|
+
terminal ? /* @__PURE__ */ jsx3("button", { type: "button", class: "scui-secondary", disabled: Boolean(state.operation), onClick: () => adapter.onIntent({ action: "resume", mode: "terminal" }), children: labels.continueWithTerminal ?? DEFAULT_LABELS.continueWithTerminal }) : null
|
|
1203
|
+
] })
|
|
1191
1204
|
] });
|
|
1192
1205
|
}
|
|
1193
1206
|
function Composer({ state, adapter, labels = DEFAULT_LABELS, memoryKey = state.attached?.key || `${state.harness}:${state.workspace}`, pendingStatus = null, restoreDraft = null, onPending, onDraftRestored }) {
|
|
@@ -2517,32 +2530,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2517
2530
|
}
|
|
2518
2531
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2519
2532
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2520
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2521
|
-
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [] };
|
|
2533
|
+
const startableKey = startable.map((item) => `${item.id}:${item.launchModes?.join(",")}:${item.preferredLaunchMode ?? ""}`).join("\0");
|
|
2534
|
+
const remembered = newChatMemory.get(memoryKey) ?? { harness: startable[0]?.id ?? "", draft: "", context: [], images: [], modes: {} };
|
|
2522
2535
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2523
2536
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2524
2537
|
const [context, setContext] = useState6(remembered.context);
|
|
2525
2538
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2539
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2526
2540
|
const [starting, setStarting] = useState6(null);
|
|
2527
2541
|
const [picking, setPicking] = useState6(false);
|
|
2528
2542
|
const [dragging, setDragging] = useState6(false);
|
|
2529
2543
|
const [pickerError, setPickerError] = useState6(null);
|
|
2530
2544
|
const startSequence = useRef7(0);
|
|
2531
2545
|
const textarea = useRef7(null);
|
|
2546
|
+
const selectedHarness = startable.find((item) => item.id === harness) ?? null;
|
|
2547
|
+
const launchModes = selectedHarness?.launchModes?.length ? selectedHarness.launchModes : ["headless"];
|
|
2548
|
+
const rememberedMode = modes[harness];
|
|
2549
|
+
const mode = launchModes.includes(rememberedMode) ? rememberedMode : launchModes.includes(selectedHarness?.preferredLaunchMode) ? selectedHarness.preferredLaunchMode : launchModes[0];
|
|
2550
|
+
const terminalAttachments = mode === "terminal" && (context.length > 0 || images.length > 0);
|
|
2551
|
+
const remember = (overrides = {}) => boundedSet(newChatMemory, memoryKey, { harness, draft, context, images, modes, ...overrides });
|
|
2532
2552
|
useAutosizeTextarea(textarea, draft);
|
|
2533
2553
|
useEffect8(() => {
|
|
2534
2554
|
if (startable.some((item) => item.id === harness)) return;
|
|
2535
2555
|
const next = startable[0]?.id ?? "";
|
|
2536
2556
|
setHarness(next);
|
|
2537
|
-
|
|
2557
|
+
remember({ harness: next });
|
|
2538
2558
|
}, [harness, startableKey]);
|
|
2539
2559
|
useEffect8(() => {
|
|
2540
|
-
if (!starting) return;
|
|
2560
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2541
2561
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2542
2562
|
const beganWorking = !starting.busy && state.busy;
|
|
2543
2563
|
if (!sessionChanged && !beganWorking) return;
|
|
2544
2564
|
newChatMemory.delete(memoryKey);
|
|
2545
|
-
onStarted();
|
|
2565
|
+
onStarted("headless");
|
|
2546
2566
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2547
2567
|
useEffect8(() => {
|
|
2548
2568
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2551,7 +2571,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2551
2571
|
textarea.current?.focus({ preventScroll: true });
|
|
2552
2572
|
}, []);
|
|
2553
2573
|
const pickContext = () => {
|
|
2554
|
-
if (!adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2574
|
+
if (mode === "terminal" || !adapter.pickContext || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS) return;
|
|
2555
2575
|
setPicking(true);
|
|
2556
2576
|
setPickerError(null);
|
|
2557
2577
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2560,12 +2580,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2560
2580
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2561
2581
|
setContext((current) => {
|
|
2562
2582
|
const next = mergeContext(current, attachments.context);
|
|
2563
|
-
|
|
2583
|
+
remember({ context: next });
|
|
2564
2584
|
return next;
|
|
2565
2585
|
});
|
|
2566
2586
|
setImages((current) => {
|
|
2567
2587
|
const next = mergeImages(current, attachments.images);
|
|
2568
|
-
|
|
2588
|
+
remember({ images: next });
|
|
2569
2589
|
return next;
|
|
2570
2590
|
});
|
|
2571
2591
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2573,6 +2593,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2573
2593
|
const addImageFiles = (value, source) => {
|
|
2574
2594
|
const allFiles = Array.from(value ?? []);
|
|
2575
2595
|
if (!allFiles.length) return false;
|
|
2596
|
+
if (mode === "terminal") {
|
|
2597
|
+
setPickerError("Terminal starts currently accept text only. Choose Chat to attach context or images.");
|
|
2598
|
+
return true;
|
|
2599
|
+
}
|
|
2576
2600
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2577
2601
|
if (files.length !== allFiles.length) {
|
|
2578
2602
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2586,7 +2610,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2586
2610
|
setPickerError(null);
|
|
2587
2611
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2588
2612
|
const next = mergeImages(current, picked);
|
|
2589
|
-
|
|
2613
|
+
remember({ images: next });
|
|
2590
2614
|
return next;
|
|
2591
2615
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2592
2616
|
return true;
|
|
@@ -2600,12 +2624,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2600
2624
|
};
|
|
2601
2625
|
const send = () => {
|
|
2602
2626
|
const text = draft.trim();
|
|
2603
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2627
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2604
2628
|
const id = startSequence.current + 1;
|
|
2605
2629
|
startSequence.current = id;
|
|
2606
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2607
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2608
|
-
if (
|
|
2630
|
+
setStarting({ id, mode, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2631
|
+
const result = adapter.onIntent({ action: "new", harness, mode, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2632
|
+
if (mode === "terminal") {
|
|
2633
|
+
Promise.resolve(result).then(() => {
|
|
2634
|
+
newChatMemory.delete(memoryKey);
|
|
2635
|
+
onStarted("terminal");
|
|
2636
|
+
}, () => setStarting((current) => current?.id === id ? null : current));
|
|
2637
|
+
} else if (result && typeof result.then === "function") {
|
|
2609
2638
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2610
2639
|
}
|
|
2611
2640
|
};
|
|
@@ -2620,7 +2649,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2620
2649
|
] }),
|
|
2621
2650
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2622
2651
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2623
|
-
operationLabel("start")
|
|
2652
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2624
2653
|
] }) : null,
|
|
2625
2654
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2626
2655
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2634,23 +2663,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2634
2663
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2635
2664
|
const value = event.currentTarget.value;
|
|
2636
2665
|
setHarness(value);
|
|
2637
|
-
|
|
2666
|
+
remember({ harness: value });
|
|
2638
2667
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2639
2668
|
item.label,
|
|
2640
2669
|
item.startable ? "" : " \xB7 unavailable"
|
|
2641
2670
|
] }, item.id)) })
|
|
2642
2671
|
] }),
|
|
2672
|
+
launchModes.length > 1 ? /* @__PURE__ */ jsxs8("fieldset", { class: "scui-launch-modes", disabled: Boolean(starting), children: [
|
|
2673
|
+
/* @__PURE__ */ jsx9("legend", { children: "Run as" }),
|
|
2674
|
+
launchModes.map((item) => /* @__PURE__ */ jsxs8("label", { children: [
|
|
2675
|
+
/* @__PURE__ */ jsx9("input", { type: "radio", name: `launch-mode-${memoryKey}`, value: item, checked: mode === item, onChange: () => {
|
|
2676
|
+
const next = { ...modes, [harness]: item };
|
|
2677
|
+
setModes(next);
|
|
2678
|
+
setPickerError(null);
|
|
2679
|
+
remember({ modes: next });
|
|
2680
|
+
} }),
|
|
2681
|
+
/* @__PURE__ */ jsxs8("span", { children: [
|
|
2682
|
+
/* @__PURE__ */ jsx9("strong", { children: item === "terminal" ? "Terminal" : "Chat" }),
|
|
2683
|
+
/* @__PURE__ */ jsx9("small", { children: item === "terminal" ? "Interactive tmux session" : "Managed here" })
|
|
2684
|
+
] })
|
|
2685
|
+
] }, item))
|
|
2686
|
+
] }) : null,
|
|
2643
2687
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2644
2688
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2645
|
-
|
|
2689
|
+
remember({ images: next });
|
|
2646
2690
|
return next;
|
|
2647
2691
|
}) }),
|
|
2648
2692
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2649
2693
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2650
|
-
|
|
2694
|
+
remember({ context: next });
|
|
2651
2695
|
return next;
|
|
2652
2696
|
}) }),
|
|
2653
|
-
pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2697
|
+
terminalAttachments ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: "Terminal starts currently accept text only. Remove attachments or choose Chat." }) : pickerError ? /* @__PURE__ */ jsx9("small", { class: "scui-context-error", role: "alert", children: pickerError }) : null,
|
|
2654
2698
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2655
2699
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2656
2700
|
event.preventDefault();
|
|
@@ -2661,18 +2705,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2661
2705
|
}, onDragLeave: (event) => {
|
|
2662
2706
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2663
2707
|
}, onDrop: dropImages, children: [
|
|
2664
|
-
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2708
|
+
adapter.pickContext ? /* @__PURE__ */ jsx9("button", { class: "scui-attach", type: "button", "aria-label": "Attach files or images", disabled: mode === "terminal" || picking || context.length >= MAX_CONTEXT_ITEMS && images.length >= MAX_IMAGE_ITEMS || Boolean(starting), onClick: pickContext, children: picking ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "attach", size: 17 }) }) : null,
|
|
2665
2709
|
/* @__PURE__ */ jsx9("textarea", { ref: textarea, rows: 3, "aria-label": "Message coding agent", placeholder: startable.length ? "What should the agent do?" : "No coding harness is available", value: draft, disabled: !startable.length || Boolean(starting), onPaste: pasteImages, onInput: (event) => {
|
|
2666
2710
|
const value = event.currentTarget.value;
|
|
2667
2711
|
setDraft(value);
|
|
2668
|
-
|
|
2712
|
+
remember({ draft: value });
|
|
2669
2713
|
}, onKeyDown: (event) => {
|
|
2670
2714
|
if (isSendKey(event)) {
|
|
2671
2715
|
event.preventDefault();
|
|
2672
2716
|
send();
|
|
2673
2717
|
}
|
|
2674
2718
|
} }),
|
|
2675
|
-
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2719
|
+
/* @__PURE__ */ jsx9("span", { children: /* @__PURE__ */ jsx9("button", { type: "button", class: "scui-send", "aria-label": mode === "terminal" ? "Start terminal session" : "Start chat", disabled: !draft.trim() && !images.length || !harness || Boolean(starting) || terminalAttachments || mode === "terminal" && !draft.trim(), onClick: send, children: starting ? /* @__PURE__ */ jsx9("i", { class: "scui-control-spinner" }) : /* @__PURE__ */ jsx9(UiIcon, { name: "send", size: 17 }) }) })
|
|
2676
2720
|
] })
|
|
2677
2721
|
] })
|
|
2678
2722
|
] });
|
|
@@ -2709,7 +2753,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2709
2753
|
setListFocus("@new");
|
|
2710
2754
|
setView("new");
|
|
2711
2755
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2712
|
-
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: () => setView("chat"), labels: copy, memoryKey }) : null,
|
|
2756
|
+
view === "new" ? /* @__PURE__ */ jsx9(NewChat, { state, adapter, onBack: () => setView("list"), onClose: adapter.onClose ? close : void 0, onStarted: (mode) => setView(mode === "terminal" ? "list" : "chat"), labels: copy, memoryKey }) : null,
|
|
2713
2757
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2714
2758
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2715
2759
|
setView("list");
|
package/package.json
CHANGED
package/sessions.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
19
|
continueHere: "Continue here",
|
|
20
|
+
continueWithTerminal: "Continue with terminal",
|
|
20
21
|
joinLive: "Join live",
|
|
21
22
|
forkHere: "Fork here"
|
|
22
23
|
});
|
|
@@ -32,6 +33,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
33
|
strategy: null,
|
|
33
34
|
canSend: false,
|
|
34
35
|
canResume: false,
|
|
36
|
+
continuationModes: Object.freeze([]),
|
|
35
37
|
canBranch: false,
|
|
36
38
|
canAttach: false,
|
|
37
39
|
canDetach: false,
|
|
@@ -81,9 +83,9 @@ function sessionDisplayName(session) {
|
|
|
81
83
|
function sessionActivity(state, row) {
|
|
82
84
|
if (state.needsInput && row.active) return "needs-input";
|
|
83
85
|
if (state.busy && row.active) return "working";
|
|
86
|
+
if (row.runtimeStatus === "busy") return "working";
|
|
84
87
|
const attention = state.attention.find((item) => item.key === row.key)?.kind;
|
|
85
88
|
if (attention) return attention;
|
|
86
|
-
if (row.runtimeStatus === "busy") return "working";
|
|
87
89
|
if (row.runtimeStatus === "running") return "running";
|
|
88
90
|
if (row.live || row.runtimeStatus === "idle") return "recent";
|
|
89
91
|
return "idle";
|
package/settings.mjs
CHANGED
|
@@ -17,6 +17,7 @@ var DEFAULT_LABELS = Object.freeze({
|
|
|
17
17
|
searchChats: "Search chats",
|
|
18
18
|
askAgent: "Ask your agent\u2026",
|
|
19
19
|
continueHere: "Continue here",
|
|
20
|
+
continueWithTerminal: "Continue with terminal",
|
|
20
21
|
joinLive: "Join live",
|
|
21
22
|
forkHere: "Fork here"
|
|
22
23
|
});
|
|
@@ -32,6 +33,7 @@ var EMPTY_UI_STATE = Object.freeze({
|
|
|
32
33
|
strategy: null,
|
|
33
34
|
canSend: false,
|
|
34
35
|
canResume: false,
|
|
36
|
+
continuationModes: Object.freeze([]),
|
|
35
37
|
canBranch: false,
|
|
36
38
|
canAttach: false,
|
|
37
39
|
canDetach: false,
|
package/styles.css
CHANGED
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
.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 }
|
|
180
180
|
.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 }
|
|
181
181
|
|
|
182
|
-
.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 { display:grid; flex:1 }.scui-continuation small { color:var(--scui-muted); font-size:10px }.scui-continuation button { padding:5px 8px; border:1px solid var(--scui-accent); border-radius:7px; background:transparent; color:var(--scui-accent); cursor:pointer }
|
|
182
|
+
.scui-continuation { display:flex; align-items:center; gap:8px; padding:7px 9px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }.scui-continuation > span:not(.scui-continuation-actions) { display:grid; flex:1 }.scui-continuation small { color:var(--scui-muted); font-size:10px }.scui-continuation-actions { display:flex; align-items:center; gap:5px; flex-wrap:wrap; justify-content:flex-end }.scui-continuation button { padding:5px 8px; border:1px solid var(--scui-accent); border-radius:7px; background:transparent; color:var(--scui-accent); cursor:pointer; white-space:nowrap }.scui-continuation button.scui-secondary { border-color:var(--scui-border-strong); color:var(--scui-fg) }
|
|
183
183
|
.scui-compose { flex:none; padding:8px; border-top:1px solid var(--scui-border); background:var(--scui-bg-raised) }
|
|
184
184
|
.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 }
|
|
185
185
|
.scui-envelope.scui-drop-target { border-color:var(--scui-accent); background:color-mix(in srgb,var(--scui-accent) 7%,var(--scui-bg)); box-shadow:0 0 0 2px color-mix(in srgb,var(--scui-accent) 16%,transparent) }
|
|
@@ -191,6 +191,7 @@
|
|
|
191
191
|
.scui-send,.scui-stop { display:grid; width:29px; height:29px; padding:0; place-items:center; border:0; border-radius:8px; background:var(--scui-accent); color:#fff; cursor:pointer }.scui-stop { background:var(--scui-danger) }.scui-send:disabled,.scui-stop:disabled { opacity:.4; cursor:default }.scui-control-spinner { box-sizing:border-box; width:13px; height:13px; border:1.5px solid currentColor; border-right-color:transparent; border-radius:50%; animation:scui-spin .75s linear infinite }
|
|
192
192
|
.scui-queue { display:grid; gap:4px; max-height:85px; margin-bottom:6px; overflow:auto; font-size:10.5px }.scui-queue > span { display:flex; gap:6px; padding:4px 6px; border-radius:5px; background:var(--scui-fill) }.scui-queue > span > span { display:grid; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap }.scui-queue small { color:var(--scui-muted) }.scui-queue > span button { margin-left:auto; border:0; background:transparent }
|
|
193
193
|
.scui-harness-picker { display:flex; align-items:center; gap:7px; margin-bottom:7px }.scui-harness-picker select { margin-left:auto; max-width:55%; padding:4px; border:1px solid var(--scui-border); border-radius:6px; background:var(--scui-bg); color:var(--scui-fg) }
|
|
194
|
+
.scui-launch-modes { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:5px; margin:0 0 7px; padding:0; border:0 }.scui-launch-modes legend { position:absolute; overflow:hidden; width:1px; height:1px; clip:rect(0 0 0 0); white-space:nowrap }.scui-launch-modes label { position:relative; min-width:0; cursor:pointer }.scui-launch-modes input { position:absolute; opacity:0; pointer-events:none }.scui-launch-modes label > span { display:flex; min-height:42px; flex-direction:column; justify-content:center; padding:6px 9px; border:1px solid var(--scui-border); border-radius:7px; background:var(--scui-bg); transition:border-color .14s ease,background .14s ease }.scui-launch-modes strong { font-size:11px; font-weight:650 }.scui-launch-modes small { overflow:hidden; color:var(--scui-muted); font-size:9px; text-overflow:ellipsis; white-space:nowrap }.scui-launch-modes input:checked + span { border-color:color-mix(in srgb,var(--scui-accent) 58%,var(--scui-border)); background:color-mix(in srgb,var(--scui-accent) 8%,var(--scui-bg)) }.scui-launch-modes input:focus-visible + span { outline:2px solid var(--scui-accent); outline-offset:1px }.scui-launch-modes:disabled label { cursor:default; opacity:.62 }
|
|
194
195
|
.scui-new { display:grid; justify-items:center; gap:5px; margin:auto; padding:20px; color:var(--scui-muted); text-align:center }.scui-new > span { color:var(--scui-accent); font-size:28px }.scui-new strong { color:var(--scui-fg) }
|
|
195
196
|
.scui-opening { position:absolute; z-index:30; inset:49px 0 0; display:flex; align-items:center; justify-content:center; gap:10px; padding:20px; background:var(--scui-bg) }.scui-opening > span { display:grid }.scui-opening small { color:var(--scui-muted) }.scui-opening > i { width:16px; height:16px; border:2px solid var(--scui-border); border-top-color:var(--scui-accent); border-radius:50%; animation:scui-spin .8s linear infinite }
|
|
196
197
|
|