@volter-ai-dev/supercode-ui 0.1.32 → 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 +7 -0
- package/components.mjs +58 -23
- package/controller.d.ts +1 -0
- package/controller.mjs +5 -0
- package/core.mjs +9 -1
- package/embed.mjs +58 -23
- package/index.d.ts +8 -2
- package/messenger.mjs +58 -23
- package/package.json +1 -1
- package/styles.css +1 -0
package/README.md
CHANGED
|
@@ -103,6 +103,13 @@ Native continuation is headless by default. A host with a real terminal provider
|
|
|
103
103
|
that strategy beside “Continue here.” The UI never infers terminal support from a generic runtime
|
|
104
104
|
handoff, and never presents a terminal strategy when the host cannot create one.
|
|
105
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
|
+
|
|
106
113
|
The default controller projection is display-bounded: 120 visible transcript rows, at most 480
|
|
107
114
|
native entries inspected to fill that tail, 16,000 characters per independent entry field, 100
|
|
108
115
|
session rows, 20 fidelity-residue details, and 50 subagents. It filters harness-injected context
|
package/components.mjs
CHANGED
|
@@ -744,7 +744,11 @@ function normalizeUiState(value) {
|
|
|
744
744
|
recoverable: raw.recoverable === true,
|
|
745
745
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
746
746
|
const item = record(candidate);
|
|
747
|
-
|
|
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 }];
|
|
748
752
|
}) : [],
|
|
749
753
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
750
754
|
savedDraft: string(raw.savedDraft),
|
|
@@ -2550,32 +2554,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2550
2554
|
}
|
|
2551
2555
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2552
2556
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2553
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2554
|
-
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: {} };
|
|
2555
2559
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2556
2560
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2557
2561
|
const [context, setContext] = useState6(remembered.context);
|
|
2558
2562
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2563
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2559
2564
|
const [starting, setStarting] = useState6(null);
|
|
2560
2565
|
const [picking, setPicking] = useState6(false);
|
|
2561
2566
|
const [dragging, setDragging] = useState6(false);
|
|
2562
2567
|
const [pickerError, setPickerError] = useState6(null);
|
|
2563
2568
|
const startSequence = useRef7(0);
|
|
2564
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 });
|
|
2565
2576
|
useAutosizeTextarea(textarea, draft);
|
|
2566
2577
|
useEffect8(() => {
|
|
2567
2578
|
if (startable.some((item) => item.id === harness)) return;
|
|
2568
2579
|
const next = startable[0]?.id ?? "";
|
|
2569
2580
|
setHarness(next);
|
|
2570
|
-
|
|
2581
|
+
remember({ harness: next });
|
|
2571
2582
|
}, [harness, startableKey]);
|
|
2572
2583
|
useEffect8(() => {
|
|
2573
|
-
if (!starting) return;
|
|
2584
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2574
2585
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2575
2586
|
const beganWorking = !starting.busy && state.busy;
|
|
2576
2587
|
if (!sessionChanged && !beganWorking) return;
|
|
2577
2588
|
newChatMemory.delete(memoryKey);
|
|
2578
|
-
onStarted();
|
|
2589
|
+
onStarted("headless");
|
|
2579
2590
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2580
2591
|
useEffect8(() => {
|
|
2581
2592
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2584,7 +2595,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2584
2595
|
textarea.current?.focus({ preventScroll: true });
|
|
2585
2596
|
}, []);
|
|
2586
2597
|
const pickContext = () => {
|
|
2587
|
-
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;
|
|
2588
2599
|
setPicking(true);
|
|
2589
2600
|
setPickerError(null);
|
|
2590
2601
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2593,12 +2604,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2593
2604
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2594
2605
|
setContext((current) => {
|
|
2595
2606
|
const next = mergeContext(current, attachments.context);
|
|
2596
|
-
|
|
2607
|
+
remember({ context: next });
|
|
2597
2608
|
return next;
|
|
2598
2609
|
});
|
|
2599
2610
|
setImages((current) => {
|
|
2600
2611
|
const next = mergeImages(current, attachments.images);
|
|
2601
|
-
|
|
2612
|
+
remember({ images: next });
|
|
2602
2613
|
return next;
|
|
2603
2614
|
});
|
|
2604
2615
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2606,6 +2617,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2606
2617
|
const addImageFiles = (value, source) => {
|
|
2607
2618
|
const allFiles = Array.from(value ?? []);
|
|
2608
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
|
+
}
|
|
2609
2624
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2610
2625
|
if (files.length !== allFiles.length) {
|
|
2611
2626
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2619,7 +2634,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2619
2634
|
setPickerError(null);
|
|
2620
2635
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2621
2636
|
const next = mergeImages(current, picked);
|
|
2622
|
-
|
|
2637
|
+
remember({ images: next });
|
|
2623
2638
|
return next;
|
|
2624
2639
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2625
2640
|
return true;
|
|
@@ -2633,12 +2648,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2633
2648
|
};
|
|
2634
2649
|
const send = () => {
|
|
2635
2650
|
const text = draft.trim();
|
|
2636
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2651
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2637
2652
|
const id = startSequence.current + 1;
|
|
2638
2653
|
startSequence.current = id;
|
|
2639
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2640
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2641
|
-
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") {
|
|
2642
2662
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2643
2663
|
}
|
|
2644
2664
|
};
|
|
@@ -2653,7 +2673,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2653
2673
|
] }),
|
|
2654
2674
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2655
2675
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2656
|
-
operationLabel("start")
|
|
2676
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2657
2677
|
] }) : null,
|
|
2658
2678
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2659
2679
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2667,23 +2687,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2667
2687
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2668
2688
|
const value = event.currentTarget.value;
|
|
2669
2689
|
setHarness(value);
|
|
2670
|
-
|
|
2690
|
+
remember({ harness: value });
|
|
2671
2691
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2672
2692
|
item.label,
|
|
2673
2693
|
item.startable ? "" : " \xB7 unavailable"
|
|
2674
2694
|
] }, item.id)) })
|
|
2675
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,
|
|
2676
2711
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2677
2712
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2678
|
-
|
|
2713
|
+
remember({ images: next });
|
|
2679
2714
|
return next;
|
|
2680
2715
|
}) }),
|
|
2681
2716
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2682
2717
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2683
|
-
|
|
2718
|
+
remember({ context: next });
|
|
2684
2719
|
return next;
|
|
2685
2720
|
}) }),
|
|
2686
|
-
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,
|
|
2687
2722
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2688
2723
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2689
2724
|
event.preventDefault();
|
|
@@ -2694,18 +2729,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2694
2729
|
}, onDragLeave: (event) => {
|
|
2695
2730
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2696
2731
|
}, onDrop: dropImages, children: [
|
|
2697
|
-
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,
|
|
2698
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) => {
|
|
2699
2734
|
const value = event.currentTarget.value;
|
|
2700
2735
|
setDraft(value);
|
|
2701
|
-
|
|
2736
|
+
remember({ draft: value });
|
|
2702
2737
|
}, onKeyDown: (event) => {
|
|
2703
2738
|
if (isSendKey(event)) {
|
|
2704
2739
|
event.preventDefault();
|
|
2705
2740
|
send();
|
|
2706
2741
|
}
|
|
2707
2742
|
} }),
|
|
2708
|
-
/* @__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 }) }) })
|
|
2709
2744
|
] })
|
|
2710
2745
|
] })
|
|
2711
2746
|
] });
|
|
@@ -2742,7 +2777,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2742
2777
|
setListFocus("@new");
|
|
2743
2778
|
setView("new");
|
|
2744
2779
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2745
|
-
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,
|
|
2746
2781
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2747
2782
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2748
2783
|
setView("list");
|
package/controller.d.ts
CHANGED
|
@@ -84,6 +84,7 @@ export interface ControllerBindingOptions {
|
|
|
84
84
|
onAcknowledge?: (key: string) => void | Promise<void>;
|
|
85
85
|
onLoadSessions?: () => void | Promise<void>;
|
|
86
86
|
onLoadEarlier?: () => void | Promise<void>;
|
|
87
|
+
onStartTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'new' }>) => void | Promise<void>;
|
|
87
88
|
onResumeTerminal?: (intent: Extract<SupercodeUiIntent, { action: 'resume' }>) => void | Promise<void>;
|
|
88
89
|
copyText?: UiAdapter['copyText'];
|
|
89
90
|
resolveImage?: UiAdapter['resolveImage'];
|
package/controller.mjs
CHANGED
|
@@ -447,6 +447,11 @@ async function dispatchStandard(controller, intent, options) {
|
|
|
447
447
|
if (intent.action === 'attach') return controller.dispatch({ type: 'observe', sessionKey: intent.key });
|
|
448
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 } : {}) });
|
|
449
449
|
if (intent.action === 'new') {
|
|
450
|
+
if (intent.mode === 'terminal') {
|
|
451
|
+
return options.onStartTerminal
|
|
452
|
+
? options.onStartTerminal(intent)
|
|
453
|
+
: options.onUnsupported?.(intent);
|
|
454
|
+
}
|
|
450
455
|
await controller.dispatch({ type: 'start', harness: intent.harness });
|
|
451
456
|
return controller.dispatch({ type: 'send', text: intent.text, ...(intent.context?.length ? { context: intent.context } : {}), ...(intent.images?.length ? { images: intent.images } : {}) });
|
|
452
457
|
}
|
package/core.mjs
CHANGED
|
@@ -796,7 +796,15 @@ export function normalizeUiState(value) {
|
|
|
796
796
|
recoverable: raw.recoverable === true,
|
|
797
797
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
798
798
|
const item = record(candidate);
|
|
799
|
-
|
|
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 }];
|
|
800
808
|
}) : [],
|
|
801
809
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
802
810
|
savedDraft: string(raw.savedDraft),
|
package/embed.mjs
CHANGED
|
@@ -747,7 +747,11 @@ function normalizeUiState(value) {
|
|
|
747
747
|
recoverable: raw.recoverable === true,
|
|
748
748
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
749
749
|
const item = record(candidate);
|
|
750
|
-
|
|
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 }];
|
|
751
755
|
}) : [],
|
|
752
756
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
753
757
|
savedDraft: string(raw.savedDraft),
|
|
@@ -2529,32 +2533,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2529
2533
|
}
|
|
2530
2534
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2531
2535
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2532
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2533
|
-
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: {} };
|
|
2534
2538
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2535
2539
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2536
2540
|
const [context, setContext] = useState6(remembered.context);
|
|
2537
2541
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2542
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2538
2543
|
const [starting, setStarting] = useState6(null);
|
|
2539
2544
|
const [picking, setPicking] = useState6(false);
|
|
2540
2545
|
const [dragging, setDragging] = useState6(false);
|
|
2541
2546
|
const [pickerError, setPickerError] = useState6(null);
|
|
2542
2547
|
const startSequence = useRef7(0);
|
|
2543
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 });
|
|
2544
2555
|
useAutosizeTextarea(textarea, draft);
|
|
2545
2556
|
useEffect8(() => {
|
|
2546
2557
|
if (startable.some((item) => item.id === harness)) return;
|
|
2547
2558
|
const next = startable[0]?.id ?? "";
|
|
2548
2559
|
setHarness(next);
|
|
2549
|
-
|
|
2560
|
+
remember({ harness: next });
|
|
2550
2561
|
}, [harness, startableKey]);
|
|
2551
2562
|
useEffect8(() => {
|
|
2552
|
-
if (!starting) return;
|
|
2563
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2553
2564
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2554
2565
|
const beganWorking = !starting.busy && state.busy;
|
|
2555
2566
|
if (!sessionChanged && !beganWorking) return;
|
|
2556
2567
|
newChatMemory.delete(memoryKey);
|
|
2557
|
-
onStarted();
|
|
2568
|
+
onStarted("headless");
|
|
2558
2569
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2559
2570
|
useEffect8(() => {
|
|
2560
2571
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2563,7 +2574,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2563
2574
|
textarea.current?.focus({ preventScroll: true });
|
|
2564
2575
|
}, []);
|
|
2565
2576
|
const pickContext = () => {
|
|
2566
|
-
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;
|
|
2567
2578
|
setPicking(true);
|
|
2568
2579
|
setPickerError(null);
|
|
2569
2580
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2572,12 +2583,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2572
2583
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2573
2584
|
setContext((current) => {
|
|
2574
2585
|
const next = mergeContext(current, attachments.context);
|
|
2575
|
-
|
|
2586
|
+
remember({ context: next });
|
|
2576
2587
|
return next;
|
|
2577
2588
|
});
|
|
2578
2589
|
setImages((current) => {
|
|
2579
2590
|
const next = mergeImages(current, attachments.images);
|
|
2580
|
-
|
|
2591
|
+
remember({ images: next });
|
|
2581
2592
|
return next;
|
|
2582
2593
|
});
|
|
2583
2594
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2585,6 +2596,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2585
2596
|
const addImageFiles = (value, source) => {
|
|
2586
2597
|
const allFiles = Array.from(value ?? []);
|
|
2587
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
|
+
}
|
|
2588
2603
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2589
2604
|
if (files.length !== allFiles.length) {
|
|
2590
2605
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2598,7 +2613,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2598
2613
|
setPickerError(null);
|
|
2599
2614
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2600
2615
|
const next = mergeImages(current, picked);
|
|
2601
|
-
|
|
2616
|
+
remember({ images: next });
|
|
2602
2617
|
return next;
|
|
2603
2618
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2604
2619
|
return true;
|
|
@@ -2612,12 +2627,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2612
2627
|
};
|
|
2613
2628
|
const send = () => {
|
|
2614
2629
|
const text = draft.trim();
|
|
2615
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2630
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2616
2631
|
const id = startSequence.current + 1;
|
|
2617
2632
|
startSequence.current = id;
|
|
2618
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2619
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2620
|
-
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") {
|
|
2621
2641
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2622
2642
|
}
|
|
2623
2643
|
};
|
|
@@ -2632,7 +2652,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2632
2652
|
] }),
|
|
2633
2653
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2634
2654
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2635
|
-
operationLabel("start")
|
|
2655
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2636
2656
|
] }) : null,
|
|
2637
2657
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2638
2658
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2646,23 +2666,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2646
2666
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2647
2667
|
const value = event.currentTarget.value;
|
|
2648
2668
|
setHarness(value);
|
|
2649
|
-
|
|
2669
|
+
remember({ harness: value });
|
|
2650
2670
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2651
2671
|
item.label,
|
|
2652
2672
|
item.startable ? "" : " \xB7 unavailable"
|
|
2653
2673
|
] }, item.id)) })
|
|
2654
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,
|
|
2655
2690
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2656
2691
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2657
|
-
|
|
2692
|
+
remember({ images: next });
|
|
2658
2693
|
return next;
|
|
2659
2694
|
}) }),
|
|
2660
2695
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2661
2696
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2662
|
-
|
|
2697
|
+
remember({ context: next });
|
|
2663
2698
|
return next;
|
|
2664
2699
|
}) }),
|
|
2665
|
-
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,
|
|
2666
2701
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2667
2702
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2668
2703
|
event.preventDefault();
|
|
@@ -2673,18 +2708,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2673
2708
|
}, onDragLeave: (event) => {
|
|
2674
2709
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2675
2710
|
}, onDrop: dropImages, children: [
|
|
2676
|
-
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,
|
|
2677
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) => {
|
|
2678
2713
|
const value = event.currentTarget.value;
|
|
2679
2714
|
setDraft(value);
|
|
2680
|
-
|
|
2715
|
+
remember({ draft: value });
|
|
2681
2716
|
}, onKeyDown: (event) => {
|
|
2682
2717
|
if (isSendKey(event)) {
|
|
2683
2718
|
event.preventDefault();
|
|
2684
2719
|
send();
|
|
2685
2720
|
}
|
|
2686
2721
|
} }),
|
|
2687
|
-
/* @__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 }) }) })
|
|
2688
2723
|
] })
|
|
2689
2724
|
] })
|
|
2690
2725
|
] });
|
|
@@ -2721,7 +2756,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2721
2756
|
setListFocus("@new");
|
|
2722
2757
|
setView("new");
|
|
2723
2758
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2724
|
-
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,
|
|
2725
2760
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2726
2761
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2727
2762
|
setView("list");
|
package/index.d.ts
CHANGED
|
@@ -5,7 +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
|
|
8
|
+
export type ExecutionMode = 'headless' | 'terminal';
|
|
9
|
+
/** @deprecated Use ExecutionMode. */
|
|
10
|
+
export type ContinuationMode = ExecutionMode;
|
|
9
11
|
export type SessionActivity = 'idle' | 'recent' | 'running' | 'working' | 'needs-input' | 'finished' | 'failed' | 'unseen';
|
|
10
12
|
|
|
11
13
|
export interface HarnessOption {
|
|
@@ -14,6 +16,10 @@ export interface HarnessOption {
|
|
|
14
16
|
installed: boolean;
|
|
15
17
|
startable: boolean;
|
|
16
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;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
export interface SessionRowModel {
|
|
@@ -287,7 +293,7 @@ export type SupercodeUiIntent =
|
|
|
287
293
|
| { action: 'loadEarlier' }
|
|
288
294
|
| { action: 'draft'; text: string }
|
|
289
295
|
| { action: 'send'; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
290
|
-
| { action: 'new'; harness: HarnessId; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
296
|
+
| { action: 'new'; harness: HarnessId; mode?: ExecutionMode; text: string; context?: TranscriptContext[]; images?: Array<TranscriptImage & { url: string }> }
|
|
291
297
|
| { action: 'resume'; mode?: ContinuationMode }
|
|
292
298
|
| { action: 'join' }
|
|
293
299
|
| { action: 'detach' }
|
package/messenger.mjs
CHANGED
|
@@ -744,7 +744,11 @@ function normalizeUiState(value) {
|
|
|
744
744
|
recoverable: raw.recoverable === true,
|
|
745
745
|
harnesses: Array.isArray(raw.harnesses) ? raw.harnesses.flatMap((candidate) => {
|
|
746
746
|
const item = record(candidate);
|
|
747
|
-
|
|
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 }];
|
|
748
752
|
}) : [],
|
|
749
753
|
history: { sessionLimit: number(history?.sessionLimit), hasMoreSessions: history?.hasMoreSessions === true, transcriptLimit: number(history?.transcriptLimit, 120), hasEarlier: history?.hasEarlier === true },
|
|
750
754
|
savedDraft: string(raw.savedDraft),
|
|
@@ -2526,32 +2530,39 @@ function Chat({ state, adapter, onBack, onNew, onClose, components, slots, label
|
|
|
2526
2530
|
}
|
|
2527
2531
|
function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey }) {
|
|
2528
2532
|
const startable = state.harnesses.filter((item) => item.startable);
|
|
2529
|
-
const startableKey = startable.map((item) => item.id).join("\0");
|
|
2530
|
-
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: {} };
|
|
2531
2535
|
const [harness, setHarness] = useState6(remembered.harness);
|
|
2532
2536
|
const [draft, setDraft] = useState6(remembered.draft);
|
|
2533
2537
|
const [context, setContext] = useState6(remembered.context);
|
|
2534
2538
|
const [images, setImages] = useState6(remembered.images ?? []);
|
|
2539
|
+
const [modes, setModes] = useState6(remembered.modes ?? {});
|
|
2535
2540
|
const [starting, setStarting] = useState6(null);
|
|
2536
2541
|
const [picking, setPicking] = useState6(false);
|
|
2537
2542
|
const [dragging, setDragging] = useState6(false);
|
|
2538
2543
|
const [pickerError, setPickerError] = useState6(null);
|
|
2539
2544
|
const startSequence = useRef7(0);
|
|
2540
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 });
|
|
2541
2552
|
useAutosizeTextarea(textarea, draft);
|
|
2542
2553
|
useEffect8(() => {
|
|
2543
2554
|
if (startable.some((item) => item.id === harness)) return;
|
|
2544
2555
|
const next = startable[0]?.id ?? "";
|
|
2545
2556
|
setHarness(next);
|
|
2546
|
-
|
|
2557
|
+
remember({ harness: next });
|
|
2547
2558
|
}, [harness, startableKey]);
|
|
2548
2559
|
useEffect8(() => {
|
|
2549
|
-
if (!starting) return;
|
|
2560
|
+
if (!starting || starting.mode === "terminal") return;
|
|
2550
2561
|
const sessionChanged = (state.attached?.key ?? null) !== starting.attachedKey;
|
|
2551
2562
|
const beganWorking = !starting.busy && state.busy;
|
|
2552
2563
|
if (!sessionChanged && !beganWorking) return;
|
|
2553
2564
|
newChatMemory.delete(memoryKey);
|
|
2554
|
-
onStarted();
|
|
2565
|
+
onStarted("headless");
|
|
2555
2566
|
}, [memoryKey, onStarted, starting, state.attached?.key, state.busy]);
|
|
2556
2567
|
useEffect8(() => {
|
|
2557
2568
|
if (starting && state.error !== starting.initialError && !state.busy && !state.operation) setStarting(null);
|
|
@@ -2560,7 +2571,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2560
2571
|
textarea.current?.focus({ preventScroll: true });
|
|
2561
2572
|
}, []);
|
|
2562
2573
|
const pickContext = () => {
|
|
2563
|
-
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;
|
|
2564
2575
|
setPicking(true);
|
|
2565
2576
|
setPickerError(null);
|
|
2566
2577
|
Promise.resolve().then(() => adapter.pickContext()).then((picked) => {
|
|
@@ -2569,12 +2580,12 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2569
2580
|
if (images.length + attachments.images.length > MAX_IMAGE_ITEMS) throw new Error(`Attach at most ${MAX_IMAGE_ITEMS} images.`);
|
|
2570
2581
|
setContext((current) => {
|
|
2571
2582
|
const next = mergeContext(current, attachments.context);
|
|
2572
|
-
|
|
2583
|
+
remember({ context: next });
|
|
2573
2584
|
return next;
|
|
2574
2585
|
});
|
|
2575
2586
|
setImages((current) => {
|
|
2576
2587
|
const next = mergeImages(current, attachments.images);
|
|
2577
|
-
|
|
2588
|
+
remember({ images: next });
|
|
2578
2589
|
return next;
|
|
2579
2590
|
});
|
|
2580
2591
|
}).catch((error) => setPickerError(error instanceof Error ? error.message : "Could not attach context.")).finally(() => setPicking(false));
|
|
@@ -2582,6 +2593,10 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2582
2593
|
const addImageFiles = (value, source) => {
|
|
2583
2594
|
const allFiles = Array.from(value ?? []);
|
|
2584
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
|
+
}
|
|
2585
2600
|
const files = allFiles.filter((file) => file.type.startsWith("image/"));
|
|
2586
2601
|
if (files.length !== allFiles.length) {
|
|
2587
2602
|
setPickerError("Drop or paste PNG, JPEG, GIF, or WebP images.");
|
|
@@ -2595,7 +2610,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2595
2610
|
setPickerError(null);
|
|
2596
2611
|
imageAttachmentsFromFiles(files).then((picked) => setImages((current) => {
|
|
2597
2612
|
const next = mergeImages(current, picked);
|
|
2598
|
-
|
|
2613
|
+
remember({ images: next });
|
|
2599
2614
|
return next;
|
|
2600
2615
|
}), (error) => setPickerError(error instanceof Error ? error.message : `Could not ${source} image.`)).finally(() => setPicking(false));
|
|
2601
2616
|
return true;
|
|
@@ -2609,12 +2624,17 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2609
2624
|
};
|
|
2610
2625
|
const send = () => {
|
|
2611
2626
|
const text = draft.trim();
|
|
2612
|
-
if (!text && !images.length || !harness || starting) return;
|
|
2627
|
+
if (!text && !images.length || !harness || starting || terminalAttachments || mode === "terminal" && !text) return;
|
|
2613
2628
|
const id = startSequence.current + 1;
|
|
2614
2629
|
startSequence.current = id;
|
|
2615
|
-
setStarting({ id, attachedKey: state.attached?.key ?? null, busy: state.busy, initialError: state.error });
|
|
2616
|
-
const result = adapter.onIntent({ action: "new", harness, text, ...context.length ? { context } : {}, ...images.length ? { images } : {} });
|
|
2617
|
-
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") {
|
|
2618
2638
|
Promise.resolve(result).catch(() => setStarting((current) => current?.id === id ? null : current));
|
|
2619
2639
|
}
|
|
2620
2640
|
};
|
|
@@ -2629,7 +2649,7 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2629
2649
|
] }),
|
|
2630
2650
|
starting ? /* @__PURE__ */ jsxs8("div", { class: "scui-operation", role: "status", children: [
|
|
2631
2651
|
/* @__PURE__ */ jsx9("i", {}),
|
|
2632
|
-
operationLabel("start")
|
|
2652
|
+
mode === "terminal" ? "Opening terminal\u2026" : operationLabel("start")
|
|
2633
2653
|
] }) : null,
|
|
2634
2654
|
/* @__PURE__ */ jsxs8("div", { class: "scui-new", children: [
|
|
2635
2655
|
/* @__PURE__ */ jsx9("span", { "aria-hidden": "true", children: "\u2726" }),
|
|
@@ -2643,23 +2663,38 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2643
2663
|
/* @__PURE__ */ jsx9("select", { value: harness, disabled: Boolean(starting), onChange: (event) => {
|
|
2644
2664
|
const value = event.currentTarget.value;
|
|
2645
2665
|
setHarness(value);
|
|
2646
|
-
|
|
2666
|
+
remember({ harness: value });
|
|
2647
2667
|
}, children: state.harnesses.map((item) => /* @__PURE__ */ jsxs8("option", { value: item.id, disabled: !item.startable, children: [
|
|
2648
2668
|
item.label,
|
|
2649
2669
|
item.startable ? "" : " \xB7 unavailable"
|
|
2650
2670
|
] }, item.id)) })
|
|
2651
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,
|
|
2652
2687
|
/* @__PURE__ */ jsx9(ImageTray, { items: images, onRemove: (index) => setImages((items) => {
|
|
2653
2688
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2654
|
-
|
|
2689
|
+
remember({ images: next });
|
|
2655
2690
|
return next;
|
|
2656
2691
|
}) }),
|
|
2657
2692
|
/* @__PURE__ */ jsx9(ContextTray, { items: context, onRemove: (index) => setContext((items) => {
|
|
2658
2693
|
const next = items.filter((_, itemIndex) => itemIndex !== index);
|
|
2659
|
-
|
|
2694
|
+
remember({ context: next });
|
|
2660
2695
|
return next;
|
|
2661
2696
|
}) }),
|
|
2662
|
-
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,
|
|
2663
2698
|
/* @__PURE__ */ jsxs8("div", { class: `scui-envelope${dragging ? " scui-drop-target" : ""}`, onDragEnter: (event) => {
|
|
2664
2699
|
if (Array.from(event.dataTransfer?.types ?? []).includes("Files")) {
|
|
2665
2700
|
event.preventDefault();
|
|
@@ -2670,18 +2705,18 @@ function NewChat({ state, adapter, onBack, onClose, onStarted, labels, memoryKey
|
|
|
2670
2705
|
}, onDragLeave: (event) => {
|
|
2671
2706
|
if (!event.currentTarget.contains(event.relatedTarget)) setDragging(false);
|
|
2672
2707
|
}, onDrop: dropImages, children: [
|
|
2673
|
-
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,
|
|
2674
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) => {
|
|
2675
2710
|
const value = event.currentTarget.value;
|
|
2676
2711
|
setDraft(value);
|
|
2677
|
-
|
|
2712
|
+
remember({ draft: value });
|
|
2678
2713
|
}, onKeyDown: (event) => {
|
|
2679
2714
|
if (isSendKey(event)) {
|
|
2680
2715
|
event.preventDefault();
|
|
2681
2716
|
send();
|
|
2682
2717
|
}
|
|
2683
2718
|
} }),
|
|
2684
|
-
/* @__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 }) }) })
|
|
2685
2720
|
] })
|
|
2686
2721
|
] })
|
|
2687
2722
|
] });
|
|
@@ -2718,7 +2753,7 @@ function SupercodeMessenger({ state: stateInput, adapter, class: className = "",
|
|
|
2718
2753
|
setListFocus("@new");
|
|
2719
2754
|
setView("new");
|
|
2720
2755
|
}, onClose: adapter.onClose ? close : void 0, components, labels: copy, memoryKey }) : null,
|
|
2721
|
-
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,
|
|
2722
2757
|
view === "chat" ? /* @__PURE__ */ jsx9(Chat, { state, adapter, onBack: () => {
|
|
2723
2758
|
setListFocus(state.attached?.key ?? listFocus);
|
|
2724
2759
|
setView("list");
|
package/package.json
CHANGED
package/styles.css
CHANGED
|
@@ -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
|
|