muonroi-cli 1.6.5 → 1.6.6
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/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/ui/app.js +91 -24
- package/dist/src/ui/hooks/use-session-picker.d.ts +14 -0
- package/dist/src/ui/hooks/use-session-picker.js +20 -0
- package/dist/src/ui/modals/session-picker-modal.d.ts +14 -0
- package/dist/src/ui/modals/session-picker-modal.js +39 -0
- package/dist/src/ui/utils/relaunch.d.ts +41 -0
- package/dist/src/ui/utils/relaunch.js +71 -0
- package/dist/src/ui/utils/relaunch.test.d.ts +1 -0
- package/dist/src/ui/utils/relaunch.test.js +83 -0
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "1.6.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.6.6";
|
|
2
2
|
export declare const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// AUTO-GENERATED by scripts/sync-version.cjs. DO NOT EDIT BY HAND.
|
|
2
2
|
// Sourced from package.json at build time so it survives bun --compile bundling.
|
|
3
|
-
export const PACKAGE_VERSION = "1.6.
|
|
3
|
+
export const PACKAGE_VERSION = "1.6.6";
|
|
4
4
|
export const PACKAGE_DESCRIPTION = "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.";
|
|
5
5
|
//# sourceMappingURL=version.js.map
|
package/dist/src/ui/app.js
CHANGED
|
@@ -58,6 +58,7 @@ import { usePairQuoteBuffer } from "./components/use-pair-quote-buffer.js";
|
|
|
58
58
|
import { useAgentEditor } from "./hooks/use-agent-editor.js";
|
|
59
59
|
import { useMcpEditor } from "./hooks/use-mcp-editor.js";
|
|
60
60
|
import { useModelPicker } from "./hooks/use-model-picker.js";
|
|
61
|
+
import { useSessionPicker } from "./hooks/use-session-picker.js";
|
|
61
62
|
import { useTypeahead } from "./hooks/useTypeahead.js";
|
|
62
63
|
import { Markdown } from "./markdown.js";
|
|
63
64
|
import { buildMcpBrowseRows, McpBrowserModal, McpEditorModal } from "./mcp-modal.js";
|
|
@@ -66,6 +67,7 @@ import { ApiKeyModal } from "./modals/api-key-modal.js";
|
|
|
66
67
|
import { ConnectModal, TelegramPairModal, TelegramTokenModal } from "./modals/connect-modal.js";
|
|
67
68
|
import { ModelPickerModal } from "./modals/model-picker-modal.js";
|
|
68
69
|
import { SandboxPickerModal } from "./modals/sandbox-picker-modal.js";
|
|
70
|
+
import { SessionPickerModal } from "./modals/session-picker-modal.js";
|
|
69
71
|
import { UpdateModal } from "./modals/update-modal.js";
|
|
70
72
|
import { PaymentApprovalPanel, WalletPickerModal } from "./modals/wallet-picker-modal.js";
|
|
71
73
|
import { resolvePickerProviders } from "./picker-providers.js";
|
|
@@ -77,6 +79,7 @@ import { StatusBar } from "./status-bar/index.js";
|
|
|
77
79
|
import { statusBarStore, wireStatusBar } from "./status-bar/store.js";
|
|
78
80
|
import { getCompactTuiSelectionText } from "./terminal-selection-text.js";
|
|
79
81
|
import { dark } from "./theme.js";
|
|
82
|
+
import { relaunchWithSession } from "./utils/relaunch.js";
|
|
80
83
|
import "./slash/route.js";
|
|
81
84
|
import "./slash/optimize.js";
|
|
82
85
|
import "./slash/discuss.js";
|
|
@@ -565,6 +568,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
565
568
|
const dismissToast = useCallback(() => setActiveToast(null), []);
|
|
566
569
|
// ─── /Phase 21 toast subscriber ────────────────────────────────────────────
|
|
567
570
|
const { model, setModel, showModelPicker, setShowModelPicker, modelPickerIndex, setModelPickerIndex, modelSearchQuery, setModelSearchQuery, configuredProviders, setConfiguredProviders, disabledProviders, setDisabledProvidersState, defaultProvider, setDefaultProviderState, disabledModels, setDisabledModelsState, modelPickerFocus, setModelPickerFocus, providerChipIndex, setProviderChipIndex, reasoningEffortByModel, setReasoningEffortByModel, } = useModelPicker(agent.getModel());
|
|
571
|
+
const { showSessionPicker, setShowSessionPicker, sessionPickerIndex, setSessionPickerIndex, sessions: sessionPickerList, setSessions: setSessionPickerList, } = useSessionPicker();
|
|
568
572
|
const modelRef = useRef(model);
|
|
569
573
|
const [providersWithKey, setProvidersWithKey] = useState(() => new Set());
|
|
570
574
|
const refreshProvidersWithKey = useCallback(async () => {
|
|
@@ -3051,6 +3055,27 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3051
3055
|
openSandboxPicker();
|
|
3052
3056
|
return true;
|
|
3053
3057
|
}
|
|
3058
|
+
if (c === "/sessions" || c === "/session") {
|
|
3059
|
+
try {
|
|
3060
|
+
const { SessionStore } = require("../storage/sessions.js");
|
|
3061
|
+
const list = new SessionStore(agent.getCwd()).listRecentSessions(20);
|
|
3062
|
+
setSessionPickerList(list);
|
|
3063
|
+
setSessionPickerIndex(0);
|
|
3064
|
+
setShowSessionPicker(true);
|
|
3065
|
+
}
|
|
3066
|
+
catch (err) {
|
|
3067
|
+
console.error(`[session-picker] list failed: ${err?.message ?? err}`);
|
|
3068
|
+
setMessages((p) => [
|
|
3069
|
+
...p,
|
|
3070
|
+
{
|
|
3071
|
+
type: "assistant",
|
|
3072
|
+
content: `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`,
|
|
3073
|
+
timestamp: new Date(),
|
|
3074
|
+
},
|
|
3075
|
+
]);
|
|
3076
|
+
}
|
|
3077
|
+
return true;
|
|
3078
|
+
}
|
|
3054
3079
|
if (c === "/wallet") {
|
|
3055
3080
|
openWalletPicker();
|
|
3056
3081
|
return true;
|
|
@@ -3747,6 +3772,9 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3747
3772
|
model,
|
|
3748
3773
|
messages.length,
|
|
3749
3774
|
messages,
|
|
3775
|
+
setSessionPickerList,
|
|
3776
|
+
setSessionPickerIndex,
|
|
3777
|
+
setShowSessionPicker,
|
|
3750
3778
|
]);
|
|
3751
3779
|
const handleSlashMenuSelect = useCallback((item) => {
|
|
3752
3780
|
setShowSlashMenuSync(false);
|
|
@@ -3921,34 +3949,27 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3921
3949
|
]);
|
|
3922
3950
|
break;
|
|
3923
3951
|
case "sessions": {
|
|
3924
|
-
//
|
|
3925
|
-
//
|
|
3926
|
-
|
|
3952
|
+
// Open the picker (delegates to the same path as typing `/sessions`)
|
|
3953
|
+
// so the user can pick a session and resume it directly instead of
|
|
3954
|
+
// having to remember the id + relaunch by hand.
|
|
3927
3955
|
try {
|
|
3928
3956
|
const { SessionStore } = require("../storage/sessions.js");
|
|
3929
|
-
const
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
const ts = new Date(s.updatedAt).toLocaleString();
|
|
3934
|
-
const title = s.title?.trim() || "(untitled)";
|
|
3935
|
-
const truncTitle = title.length > 80 ? `${title.slice(0, 77)}...` : title;
|
|
3936
|
-
return `${String(idx + 1).padStart(2)}. [${s.id}] ${ts} ${s.model}\n ${truncTitle}`;
|
|
3937
|
-
});
|
|
3938
|
-
body = [
|
|
3939
|
-
"Recent sessions in this workspace:",
|
|
3940
|
-
"",
|
|
3941
|
-
...lines,
|
|
3942
|
-
"",
|
|
3943
|
-
"Resume on next launch: muonroi-cli --session <id>",
|
|
3944
|
-
"Or: muonroi-cli --session latest",
|
|
3945
|
-
].join("\n");
|
|
3946
|
-
}
|
|
3957
|
+
const list = new SessionStore(agent.getCwd()).listRecentSessions(20);
|
|
3958
|
+
setSessionPickerList(list);
|
|
3959
|
+
setSessionPickerIndex(0);
|
|
3960
|
+
setShowSessionPicker(true);
|
|
3947
3961
|
}
|
|
3948
3962
|
catch (err) {
|
|
3949
|
-
|
|
3963
|
+
console.error(`[session-picker] list failed: ${err?.message ?? err}`);
|
|
3964
|
+
setMessages((p) => [
|
|
3965
|
+
...p,
|
|
3966
|
+
{
|
|
3967
|
+
type: "assistant",
|
|
3968
|
+
content: `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`,
|
|
3969
|
+
timestamp: new Date(),
|
|
3970
|
+
},
|
|
3971
|
+
]);
|
|
3950
3972
|
}
|
|
3951
|
-
setMessages((p) => [...p, { type: "assistant", content: body, timestamp: new Date() }]);
|
|
3952
3973
|
break;
|
|
3953
3974
|
}
|
|
3954
3975
|
default: {
|
|
@@ -4076,12 +4097,16 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
4076
4097
|
setModelPickerIndex,
|
|
4077
4098
|
setModelSearchQuery,
|
|
4078
4099
|
setShowModelPicker,
|
|
4100
|
+
setSessionPickerList,
|
|
4101
|
+
setSessionPickerIndex,
|
|
4102
|
+
setShowSessionPicker,
|
|
4079
4103
|
]);
|
|
4080
4104
|
const blockPrompt = showConnectModal ||
|
|
4081
4105
|
showTelegramTokenModal ||
|
|
4082
4106
|
showTelegramPairModal ||
|
|
4083
4107
|
showMcpModal ||
|
|
4084
4108
|
showSandboxPicker ||
|
|
4109
|
+
showSessionPicker ||
|
|
4085
4110
|
showWalletPicker ||
|
|
4086
4111
|
!!pendingPaymentApproval ||
|
|
4087
4112
|
showScheduleModal ||
|
|
@@ -5182,6 +5207,43 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
5182
5207
|
}
|
|
5183
5208
|
return;
|
|
5184
5209
|
}
|
|
5210
|
+
if (showSessionPicker) {
|
|
5211
|
+
if (isEscapeKey(key)) {
|
|
5212
|
+
setShowSessionPicker(false);
|
|
5213
|
+
return;
|
|
5214
|
+
}
|
|
5215
|
+
if (key.name === "up") {
|
|
5216
|
+
setSessionPickerIndex((i) => Math.max(0, i - 1));
|
|
5217
|
+
return;
|
|
5218
|
+
}
|
|
5219
|
+
if (key.name === "down") {
|
|
5220
|
+
setSessionPickerIndex((i) => Math.min(Math.max(0, sessionPickerList.length - 1), i + 1));
|
|
5221
|
+
return;
|
|
5222
|
+
}
|
|
5223
|
+
if (key.name === "return") {
|
|
5224
|
+
const picked = sessionPickerList[sessionPickerIndex];
|
|
5225
|
+
if (!picked) {
|
|
5226
|
+
setShowSessionPicker(false);
|
|
5227
|
+
return;
|
|
5228
|
+
}
|
|
5229
|
+
// Close the modal first so the toast renders before the spawn.
|
|
5230
|
+
setShowSessionPicker(false);
|
|
5231
|
+
pushToast("info", `Resuming session ${picked.id.slice(-8)}… restarting CLI`);
|
|
5232
|
+
// Defer to the next tick so OpenTUI flushes the toast frame; then
|
|
5233
|
+
// spawn the child (which inherits the TTY) and exit this process.
|
|
5234
|
+
setTimeout(() => {
|
|
5235
|
+
try {
|
|
5236
|
+
relaunchWithSession(picked.id);
|
|
5237
|
+
}
|
|
5238
|
+
catch (err) {
|
|
5239
|
+
console.error(`[session-picker] relaunch failed: ${err?.message ?? err}`);
|
|
5240
|
+
pushToast("error", `Resume failed: ${err?.message ?? err}`);
|
|
5241
|
+
}
|
|
5242
|
+
}, 50);
|
|
5243
|
+
return;
|
|
5244
|
+
}
|
|
5245
|
+
return;
|
|
5246
|
+
}
|
|
5185
5247
|
if (showModelPicker) {
|
|
5186
5248
|
// Sub-modal: BW sync (password + provider picker phases).
|
|
5187
5249
|
if (bwSync) {
|
|
@@ -5806,6 +5868,11 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
5806
5868
|
setShowModelPicker,
|
|
5807
5869
|
setModelPickerIndex,
|
|
5808
5870
|
setModel,
|
|
5871
|
+
showSessionPicker,
|
|
5872
|
+
sessionPickerList,
|
|
5873
|
+
sessionPickerIndex,
|
|
5874
|
+
setShowSessionPicker,
|
|
5875
|
+
setSessionPickerIndex,
|
|
5809
5876
|
]);
|
|
5810
5877
|
useKeyboard(handleKey);
|
|
5811
5878
|
const handlePaste = useCallback((event) => {
|
|
@@ -6040,7 +6107,7 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
6040
6107
|
: `💭 Thought for ${(lastReasoningElapsedMs / 1000).toFixed(1)}s` }) })), streamContent && (_jsx("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, children: _jsx(Markdown, { content: streamContent, t: t }) })), isProcessing && !streamContent && activeToolCalls.length === 0 && (_jsx(ShimmerText, { t: t, text: "Planning next moves" })), showPlanPanel && _jsx(PlanQuestionsPanel, { t: t, questions: planQuestions, state: pqs }), pendingPaymentApproval && _jsx(PaymentApprovalPanel, { t: t, payment: pendingPaymentApproval }), activeHaltCard && (_jsx(HaltRecoveryCard, { halt: activeHaltCard, selectedIndex: haltSelectedIndex, terminalCols: width, theme: t })), initNewForm && _jsx(InitNewFormCard, { state: initNewForm, terminalCols: width, theme: t }), pointToExistingForm && (_jsx(PointToExistingFormCard, { state: pointToExistingForm, terminalCols: width, theme: t })), councilProgress && (_jsx(Semantic, { id: "continue-as-council-progress", role: "log", name: "Council brainstorm", children: _jsx("box", { flexDirection: "column", borderStyle: "single", borderColor: councilProgress.status === "error" ? t.initFormError : t.text, padding: 1, marginTop: 1, children: _jsxs("text", { fg: t.text, children: [councilProgress.status === "running" && "Council brainstorming — writing spec.md...", councilProgress.status === "done" &&
|
|
6041
6108
|
`Council brainstorm complete: ${councilProgress.specPath}${councilProgress.hasContent ? "" : " (no content — production council wiring deferred)"}`, councilProgress.status === "error" && `Council brainstorm failed: ${councilProgress.error}`] }) }) }))] }) }), btwState && _jsx(BtwOverlay, { state: btwState, theme: t }), _jsx("box", { flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, queuedCount: queuedMessages.length, queuedMessages: queuedMessages, typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) })] }), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 })] })] })) : (
|
|
6042
6109
|
/* ── Home ───────────────────────────────────────── */
|
|
6043
|
-
_jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsx("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, bwSync: bwSync })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
|
|
6110
|
+
_jsxs(_Fragment, { children: [_jsxs("box", { flexGrow: 1, alignItems: "center", paddingLeft: 2, paddingRight: 2, children: [_jsx("box", { flexGrow: 1, minHeight: 0 }), _jsx("box", { flexShrink: 0, alignItems: "center", children: _jsx(HeroLogo, { t: t }) }), _jsx("box", { height: 1, minHeight: 0, flexShrink: 1 }), _jsx("box", { width: "100%", maxWidth: 75, flexShrink: 0, children: _jsx(PromptBox, { t: t, inputRef: inputRef, isProcessing: isProcessing, showModelPicker: showModelPicker, showSandboxPicker: showSandboxPicker, showWalletPicker: showWalletPicker, showSlashMenu: showSlashMenu, showPlanQuestions: showPlanPanel, showApiKeyModal: showApiKeyModal, blockPrompt: blockPrompt, onSubmit: handleSubmit, onPaste: handlePaste, pasteBlocks: pasteBlocks, modeInfo: modeInfo, model: model, modelInfo: modelInfo, contextStats: contextStats, placeholder: "What are we building?", typeahead: typeahead, slashItems: filteredSlashItems, slashSelectedIndex: slashMenuIndex, slashInputIsMatched: slashInputIsMatched, composerValue: showSlashMenu ? `/${slashSearchQuery}` : undefined }) }), _jsx("box", { height: 2, minHeight: 0, flexShrink: 1 }), _jsx("box", { flexGrow: 1, minHeight: 0 })] }), updateInfo?.hasUpdate && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: "#f59e0b", children: ["┃ Update available: v", startupConfig.version, " → v", updateInfo.latestVersion, " — run /update to install"] }) })), isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsx("text", { fg: "#f59e0b", children: "┃ Updating..." }) })), updateOutput && !isUpdating && (_jsx("box", { paddingLeft: 2, paddingRight: 2, flexDirection: "row", flexShrink: 0, children: _jsxs("text", { fg: updateOutput.startsWith("Update complete") ? "#22c55e" : "#ef4444", children: ["┃ ", updateOutput] }) })), _jsx("box", { paddingLeft: 2, paddingRight: 2, flexShrink: 0, children: _jsx(StatusBar, {}) }), _jsxs("box", { paddingLeft: 2, paddingRight: 2, paddingBottom: 1, flexDirection: "row", flexShrink: 0, children: [_jsx("text", { fg: t.textDim, children: agent.getCwd().replace(os.homedir(), "~") }), sandboxMode === "shuru" ? _jsx("text", { fg: "#f97316", children: " · sandbox" }) : null, _jsx("box", { flexGrow: 1 }), _jsx("text", { fg: t.textDim, children: `v${startupConfig.version}` })] })] })), showApiKeyModal && (_jsx(ApiKeyModal, { t: t, width: width, height: height, inputRef: apiKeyInputRef, error: apiKeyError, onSubmit: submitApiKey })), showUpdateModal && updateInfo && (_jsx(UpdateModal, { t: t, width: width, height: height, currentVersion: startupConfig.version, latestVersion: updateInfo.latestVersion })), showMcpModal && !showMcpEditor && (_jsx(McpBrowserModal, { t: t, width: width, height: height, selectedIndex: mcpModalIndex, searchQuery: mcpSearchQuery, rows: mcpRows })), showMcpEditor && (_jsx(McpEditorModal, { t: t, width: width, height: height, draft: mcpEditorDraft, focusedField: mcpEditorField, syncKey: mcpEditorSyncKey, error: mcpEditorError, title: editingMcpId ? "Edit MCP Server" : "Add MCP Server", labelRef: mcpLabelRef, urlRef: mcpUrlRef, headersRef: mcpHeadersRef, commandRef: mcpCommandRef, argsRef: mcpArgsRef, cwdRef: mcpCwdRef, envRef: mcpEnvRef, onSubmit: submitMcpEditor })), showScheduleModal && (_jsx(ScheduleBrowserModal, { t: t, width: width, height: height, selectedIndex: scheduleModalIndex, searchQuery: scheduleSearchQuery, rows: scheduleRows })), showAgentsModal && !showAgentsEditor && (_jsx(SubagentsBrowserModal, { t: t, width: width, height: height, selectedIndex: agentsModalIndex, searchQuery: agentsSearchQuery, rows: agentRows })), showAgentsEditor && (_jsx(SubagentEditorModal, { t: t, width: width, height: height, draft: agentsEditorDraft, focusedField: agentsEditorField, modelIndex: agentsEditorModelIndex, error: agentsEditorError, title: editingSubagent ? `Edit sub-agent: ${formatSubagentName(editingSubagent.name)}` : "Add sub-agent", nameRef: subagentNameRef, instructionRef: subagentInstructionRef, onSubmit: submitSubagentEditor, showRemoveHint: !!editingSubagent }, `subagent-editor-${agentsEditorSyncKey}`)), showModelPicker && (_jsx(ModelPickerModal, { t: t, currentModel: model, selectedIndex: modelPickerIndex, width: width, height: height, searchQuery: modelSearchQuery, filteredModels: filteredModels, reasoningEffortByModel: reasoningEffortByModel, configuredProviders: configuredProviders, disabledProviders: disabledProviders, disabledModels: disabledModels, defaultProvider: defaultProvider, focus: modelPickerFocus, providerChipIndex: providerChipIndex, providersWithKey: providersWithKey, apiKeyPrompt: apiKeyPrompt, bwSync: bwSync })), showSessionPicker && (_jsx(SessionPickerModal, { t: t, sessions: sessionPickerList, focusIndex: sessionPickerIndex, width: width, height: height })), showWalletPicker && (_jsx(WalletPickerModal, { t: t, settings: walletSettings, walletInfo: walletDisplayInfo, focusIndex: walletFocusIndex, width: width, height: height })), showSandboxPicker && (_jsx(SandboxPickerModal, { t: t, currentMode: sandboxMode, settings: sandboxSettings, focusIndex: sandboxSettingsFocusIndex, editing: sandboxSettingsEditing, editBuffer: sandboxSettingsEditBuffer, width: width, height: height })), showConnectModal && (_jsx(ConnectModal, { t: t, width: width, height: height, selectedIndex: connectModalIndex, channels: CONNECT_CHANNELS })), showTelegramTokenModal && (_jsx(TelegramTokenModal, { t: t, width: width, height: height, inputRef: telegramTokenInputRef, error: telegramTokenError, onSubmit: submitTelegramToken })), showTelegramPairModal && (_jsx(TelegramPairModal, { t: t, width: width, height: height, inputRef: telegramPairInputRef, error: telegramPairError, onSubmit: () => void submitTelegramPair() }))] }) }));
|
|
6044
6111
|
}
|
|
6045
6112
|
export { computeMcpRunInfo } from "./components/message-view.js";
|
|
6046
6113
|
/* ── Slash Menu ──────────────────────────────────────────────── */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionInfo } from "../../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* State for the /sessions picker modal. Sessions are loaded lazily when the
|
|
4
|
+
* picker is opened (the SQLite query is cheap — ORDER BY updated_at LIMIT 20
|
|
5
|
+
* on an indexed column) so we do not pay for it on cold boot.
|
|
6
|
+
*/
|
|
7
|
+
export declare function useSessionPicker(): {
|
|
8
|
+
showSessionPicker: boolean;
|
|
9
|
+
setShowSessionPicker: import("react").Dispatch<import("react").SetStateAction<boolean>>;
|
|
10
|
+
sessionPickerIndex: number;
|
|
11
|
+
setSessionPickerIndex: import("react").Dispatch<import("react").SetStateAction<number>>;
|
|
12
|
+
sessions: SessionInfo[];
|
|
13
|
+
setSessions: import("react").Dispatch<import("react").SetStateAction<SessionInfo[]>>;
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* State for the /sessions picker modal. Sessions are loaded lazily when the
|
|
4
|
+
* picker is opened (the SQLite query is cheap — ORDER BY updated_at LIMIT 20
|
|
5
|
+
* on an indexed column) so we do not pay for it on cold boot.
|
|
6
|
+
*/
|
|
7
|
+
export function useSessionPicker() {
|
|
8
|
+
const [showSessionPicker, setShowSessionPicker] = useState(false);
|
|
9
|
+
const [sessionPickerIndex, setSessionPickerIndex] = useState(0);
|
|
10
|
+
const [sessions, setSessions] = useState([]);
|
|
11
|
+
return {
|
|
12
|
+
showSessionPicker,
|
|
13
|
+
setShowSessionPicker,
|
|
14
|
+
sessionPickerIndex,
|
|
15
|
+
setSessionPickerIndex,
|
|
16
|
+
sessions,
|
|
17
|
+
setSessions,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=use-session-picker.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { SessionInfo } from "../../types/index.js";
|
|
2
|
+
import type { Theme } from "../theme.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
5
|
+
* row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
|
|
6
|
+
* the user does not need to remember the id or restart by hand.
|
|
7
|
+
*/
|
|
8
|
+
export declare function SessionPickerModal({ t, sessions, focusIndex, width, height, }: {
|
|
9
|
+
t: Theme;
|
|
10
|
+
sessions: SessionInfo[];
|
|
11
|
+
focusIndex: number;
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
}): import("react").ReactNode;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
|
+
/**
|
|
4
|
+
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
5
|
+
* row relaunches the CLI with `--session <id>` (see ui/utils/relaunch.ts) so
|
|
6
|
+
* the user does not need to remember the id or restart by hand.
|
|
7
|
+
*/
|
|
8
|
+
export function SessionPickerModal({ t, sessions, focusIndex, width, height, }) {
|
|
9
|
+
const panelWidth = Math.min(80, width - 6);
|
|
10
|
+
const rowCount = Math.max(sessions.length, 1);
|
|
11
|
+
// 4 chrome lines (title row + spacer + footer + paddings) + the rows
|
|
12
|
+
const contentHeight = rowCount + 4;
|
|
13
|
+
const maxH = Math.floor(height * 0.7);
|
|
14
|
+
const panelHeight = Math.min(contentHeight, maxH);
|
|
15
|
+
const top = bottomAlignedModalTop(height, panelHeight);
|
|
16
|
+
const overlayBg = "#000000cc";
|
|
17
|
+
return (_jsx("box", { position: "absolute", left: 0, top: 0, width: width, height: height, alignItems: "center", paddingTop: top, backgroundColor: overlayBg, children: _jsxs("box", { width: panelWidth, height: panelHeight, backgroundColor: t.backgroundPanel, paddingTop: 1, paddingBottom: 1, flexDirection: "column", children: [_jsxs("box", { flexShrink: 0, flexDirection: "row", justifyContent: "space-between", paddingLeft: 2, paddingRight: 2, children: [_jsx("text", { fg: t.primary, children: _jsx("b", { children: "Resume session" }) }), _jsx("text", { fg: t.textMuted, children: "esc" })] }), _jsx("scrollbox", { flexGrow: 1, minHeight: 0, children: sessions.length === 0 ? (_jsx("box", { paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "No prior sessions in this workspace." }) })) : (sessions.map((s, idx) => {
|
|
18
|
+
const focused = idx === focusIndex;
|
|
19
|
+
const ts = formatTimestamp(s.updatedAt);
|
|
20
|
+
const titleRaw = s.title?.trim() || "(untitled)";
|
|
21
|
+
const titleMax = Math.max(8, panelWidth - 38);
|
|
22
|
+
const title = titleRaw.length > titleMax ? `${titleRaw.slice(0, titleMax - 1)}…` : titleRaw;
|
|
23
|
+
const idShort = s.id.slice(-8);
|
|
24
|
+
return (_jsxs("box", { backgroundColor: focused ? t.selectedBg : undefined, paddingLeft: 2, paddingRight: 2, width: "100%", flexDirection: "row", justifyContent: "space-between", children: [_jsx("text", { fg: focused ? t.selected : t.text, children: `${ts} ${title}` }), _jsx("text", { fg: focused ? t.primary : t.textMuted, children: `${s.model} ${idShort}` })] }, s.id));
|
|
25
|
+
})) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "↑↓ navigate · enter resume (restarts CLI) · esc cancel" }) })] }) }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Compact MM-DD HH:MM timestamp for the picker rows. Trades the year for
|
|
29
|
+
* space — the picker is workspace-scoped + lists the latest 20 sessions, so
|
|
30
|
+
* a year boundary is rare and an obvious context.
|
|
31
|
+
*/
|
|
32
|
+
function formatTimestamp(d) {
|
|
33
|
+
const mm = String(d.getMonth() + 1).padStart(2, "0");
|
|
34
|
+
const dd = String(d.getDate()).padStart(2, "0");
|
|
35
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
36
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
37
|
+
return `${mm}-${dd} ${hh}:${min}`;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=session-picker-modal.js.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/relaunch.ts
|
|
3
|
+
*
|
|
4
|
+
* Helpers for "relaunch the CLI with a different session" — used by the
|
|
5
|
+
* /sessions picker so the user does not have to remember the id + restart
|
|
6
|
+
* the binary manually (the whole motivation of the picker).
|
|
7
|
+
*
|
|
8
|
+
* The argv mangling is a PURE function so it is unit-testable in isolation
|
|
9
|
+
* from the spawn side effects. `relaunchWithSession` glues argv mangling +
|
|
10
|
+
* child_process spawn + parent exit; it returns nothing (process replaces).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
/**
|
|
14
|
+
* Strip any existing `-s <id>` / `--session <id>` / `--session=<id>` from
|
|
15
|
+
* argv (kept indices intact otherwise) and append a fresh `--session <id>`.
|
|
16
|
+
* Pure — input arrays are not mutated.
|
|
17
|
+
*
|
|
18
|
+
* `argv` is in the shape Node provides: `[exec, scriptOrFirstArg, ...rest]`.
|
|
19
|
+
* The caller passes `process.argv.slice(1)` (the args part) and re-prepends
|
|
20
|
+
* `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
|
|
21
|
+
*/
|
|
22
|
+
export declare function sanitizeArgvForResume(args: ReadonlyArray<string>, sessionId: string): string[];
|
|
23
|
+
export interface RelaunchOptions {
|
|
24
|
+
/** Override process.argv (tests). Defaults to live process.argv. */
|
|
25
|
+
argv?: ReadonlyArray<string>;
|
|
26
|
+
/** Override the exit hook (tests). Defaults to process.exit. */
|
|
27
|
+
onExit?: (code: number) => void;
|
|
28
|
+
/** Injected spawn for tests. Defaults to the real node:child_process spawn. */
|
|
29
|
+
spawnFn?: typeof spawn;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Spawn a fresh CLI process bound to {sessionId} and exit the current one.
|
|
33
|
+
* Cross-platform: uses `stdio: "inherit"` so the child takes over the TTY,
|
|
34
|
+
* and `detached: false` so killing the parent's terminal kills the child
|
|
35
|
+
* (the user expects "close window = kill" semantics).
|
|
36
|
+
*
|
|
37
|
+
* NOTE: the caller should disconnect/teardown the current TUI before invoking
|
|
38
|
+
* this — the spawn happens immediately and the parent exit is on next tick,
|
|
39
|
+
* so any open file handles / MCP transports must be released first.
|
|
40
|
+
*/
|
|
41
|
+
export declare function relaunchWithSession(sessionId: string, opts?: RelaunchOptions): void;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ui/utils/relaunch.ts
|
|
3
|
+
*
|
|
4
|
+
* Helpers for "relaunch the CLI with a different session" — used by the
|
|
5
|
+
* /sessions picker so the user does not have to remember the id + restart
|
|
6
|
+
* the binary manually (the whole motivation of the picker).
|
|
7
|
+
*
|
|
8
|
+
* The argv mangling is a PURE function so it is unit-testable in isolation
|
|
9
|
+
* from the spawn side effects. `relaunchWithSession` glues argv mangling +
|
|
10
|
+
* child_process spawn + parent exit; it returns nothing (process replaces).
|
|
11
|
+
*/
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
|
+
/**
|
|
14
|
+
* Strip any existing `-s <id>` / `--session <id>` / `--session=<id>` from
|
|
15
|
+
* argv (kept indices intact otherwise) and append a fresh `--session <id>`.
|
|
16
|
+
* Pure — input arrays are not mutated.
|
|
17
|
+
*
|
|
18
|
+
* `argv` is in the shape Node provides: `[exec, scriptOrFirstArg, ...rest]`.
|
|
19
|
+
* The caller passes `process.argv.slice(1)` (the args part) and re-prepends
|
|
20
|
+
* `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
|
|
21
|
+
*/
|
|
22
|
+
export function sanitizeArgvForResume(args, sessionId) {
|
|
23
|
+
if (!sessionId || !sessionId.trim()) {
|
|
24
|
+
throw new Error("sanitizeArgvForResume: sessionId is required");
|
|
25
|
+
}
|
|
26
|
+
const out = [];
|
|
27
|
+
for (let i = 0; i < args.length; i++) {
|
|
28
|
+
const a = args[i];
|
|
29
|
+
if (a === "-s" || a === "--session") {
|
|
30
|
+
// skip the flag AND its value (if present and not another flag)
|
|
31
|
+
const next = args[i + 1];
|
|
32
|
+
if (next !== undefined && !next.startsWith("-"))
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (a.startsWith("--session=")) {
|
|
37
|
+
continue; // skip the combined form
|
|
38
|
+
}
|
|
39
|
+
out.push(a);
|
|
40
|
+
}
|
|
41
|
+
out.push("--session", sessionId);
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Spawn a fresh CLI process bound to {sessionId} and exit the current one.
|
|
46
|
+
* Cross-platform: uses `stdio: "inherit"` so the child takes over the TTY,
|
|
47
|
+
* and `detached: false` so killing the parent's terminal kills the child
|
|
48
|
+
* (the user expects "close window = kill" semantics).
|
|
49
|
+
*
|
|
50
|
+
* NOTE: the caller should disconnect/teardown the current TUI before invoking
|
|
51
|
+
* this — the spawn happens immediately and the parent exit is on next tick,
|
|
52
|
+
* so any open file handles / MCP transports must be released first.
|
|
53
|
+
*/
|
|
54
|
+
export function relaunchWithSession(sessionId, opts = {}) {
|
|
55
|
+
const argv = opts.argv ?? process.argv;
|
|
56
|
+
const exec = argv[0];
|
|
57
|
+
if (!exec) {
|
|
58
|
+
throw new Error("relaunchWithSession: process.argv[0] is empty — cannot relaunch");
|
|
59
|
+
}
|
|
60
|
+
const exit = opts.onExit ?? ((code) => process.exit(code));
|
|
61
|
+
const spawnImpl = opts.spawnFn ?? spawn;
|
|
62
|
+
const args = sanitizeArgvForResume(argv.slice(1), sessionId);
|
|
63
|
+
const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
|
|
64
|
+
child.once("error", (err) => {
|
|
65
|
+
console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
|
|
66
|
+
exit(1);
|
|
67
|
+
});
|
|
68
|
+
// Hand the TTY to the child and exit cleanly. The child takes over rendering.
|
|
69
|
+
child.once("spawn", () => exit(0));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=relaunch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { relaunchWithSession, sanitizeArgvForResume } from "./relaunch.js";
|
|
4
|
+
describe("sanitizeArgvForResume", () => {
|
|
5
|
+
it("appends --session when no prior session flag exists", () => {
|
|
6
|
+
expect(sanitizeArgvForResume(["-m", "grok-build-0.1"], "abc-123")).toEqual([
|
|
7
|
+
"-m",
|
|
8
|
+
"grok-build-0.1",
|
|
9
|
+
"--session",
|
|
10
|
+
"abc-123",
|
|
11
|
+
]);
|
|
12
|
+
});
|
|
13
|
+
it("strips an existing `-s <id>` and replaces it", () => {
|
|
14
|
+
expect(sanitizeArgvForResume(["-s", "old-id", "-m", "grok-build-0.1"], "new-id")).toEqual([
|
|
15
|
+
"-m",
|
|
16
|
+
"grok-build-0.1",
|
|
17
|
+
"--session",
|
|
18
|
+
"new-id",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
it("strips an existing `--session <id>` (long form)", () => {
|
|
22
|
+
expect(sanitizeArgvForResume(["--session", "old-id", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
23
|
+
});
|
|
24
|
+
it("strips the combined `--session=<id>` form", () => {
|
|
25
|
+
expect(sanitizeArgvForResume(["--session=old-id", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
26
|
+
});
|
|
27
|
+
it("strips `--session` even when its value looks like another flag (treats value as missing)", () => {
|
|
28
|
+
// edge: user typed `--session --batch-api` — we don't eat the next flag
|
|
29
|
+
expect(sanitizeArgvForResume(["--session", "--batch-api", "-y"], "new-id")).toEqual([
|
|
30
|
+
"--batch-api",
|
|
31
|
+
"-y",
|
|
32
|
+
"--session",
|
|
33
|
+
"new-id",
|
|
34
|
+
]);
|
|
35
|
+
});
|
|
36
|
+
it("removes multiple stray session flags (defensive — last wins)", () => {
|
|
37
|
+
expect(sanitizeArgvForResume(["-s", "a", "--session", "b", "--session=c"], "z")).toEqual(["--session", "z"]);
|
|
38
|
+
});
|
|
39
|
+
it("throws when sessionId is empty or whitespace", () => {
|
|
40
|
+
expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
|
|
41
|
+
expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
describe("relaunchWithSession", () => {
|
|
45
|
+
it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
|
|
46
|
+
const exitMock = vi.fn();
|
|
47
|
+
const child = new EventEmitter();
|
|
48
|
+
const spawnMock = vi.fn(() => child);
|
|
49
|
+
relaunchWithSession("sess-xyz", {
|
|
50
|
+
argv: ["/usr/local/bin/muonroi-cli", "-m", "grok-build-0.1"],
|
|
51
|
+
onExit: exitMock,
|
|
52
|
+
spawnFn: spawnMock,
|
|
53
|
+
});
|
|
54
|
+
expect(spawnMock).toHaveBeenCalledTimes(1);
|
|
55
|
+
expect(spawnMock).toHaveBeenCalledWith("/usr/local/bin/muonroi-cli", ["-m", "grok-build-0.1", "--session", "sess-xyz"], { stdio: "inherit", detached: false });
|
|
56
|
+
// exit fires on the child's "spawn" event
|
|
57
|
+
child.emit("spawn");
|
|
58
|
+
expect(exitMock).toHaveBeenCalledWith(0);
|
|
59
|
+
});
|
|
60
|
+
it("exits 1 if the child spawn errors before starting", () => {
|
|
61
|
+
const exitMock = vi.fn();
|
|
62
|
+
const errMock = vi.spyOn(console, "error").mockImplementation(() => { });
|
|
63
|
+
const child = new EventEmitter();
|
|
64
|
+
const spawnMock = vi.fn(() => child);
|
|
65
|
+
relaunchWithSession("sess-xyz", {
|
|
66
|
+
argv: ["/bin/muonroi", "-y"],
|
|
67
|
+
onExit: exitMock,
|
|
68
|
+
spawnFn: spawnMock,
|
|
69
|
+
});
|
|
70
|
+
child.emit("error", new Error("ENOENT"));
|
|
71
|
+
expect(exitMock).toHaveBeenCalledWith(1);
|
|
72
|
+
expect(errMock).toHaveBeenCalled();
|
|
73
|
+
errMock.mockRestore();
|
|
74
|
+
});
|
|
75
|
+
it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
|
|
76
|
+
expect(() => relaunchWithSession("sess", {
|
|
77
|
+
argv: [],
|
|
78
|
+
onExit: () => { },
|
|
79
|
+
spawnFn: (() => new EventEmitter()),
|
|
80
|
+
})).toThrow(/process\.argv\[0\] is empty/);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
//# sourceMappingURL=relaunch.test.js.map
|