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.
@@ -1,2 +1,2 @@
1
- export declare const PACKAGE_VERSION = "1.6.5";
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.5";
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
@@ -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
- // List recent sessions in this workspace so the user can pick one
3925
- // to resume on next launch (`muonroi-cli --session <id>`).
3926
- let body = "No prior sessions found in this workspace.";
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 store = new SessionStore(agent.getCwd());
3930
- const sessions = store.listRecentSessions(15);
3931
- if (sessions.length > 0) {
3932
- const lines = sessions.map((s, idx) => {
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
- body = `Failed to list sessions: ${err instanceof Error ? err.message : String(err)}`;
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
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.6.5",
6
+ "version": "1.6.6",
7
7
  "description": "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.",
8
8
  "repository": {
9
9
  "type": "git",