muonroi-cli 1.7.1 → 1.7.2
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/index.js +88 -44
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +11 -4
- package/dist/src/ui/app.d.ts +1 -1
- package/dist/src/ui/app.js +27 -7
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +27 -3
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/package.json +1 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const PACKAGE_VERSION = "1.7.
|
|
1
|
+
export declare const PACKAGE_VERSION = "1.7.2";
|
|
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.7.
|
|
3
|
+
export const PACKAGE_VERSION = "1.7.2";
|
|
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/index.js
CHANGED
|
@@ -391,6 +391,7 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
391
391
|
const { createRoot } = await import("@opentui/react");
|
|
392
392
|
const { createElement } = await import("react");
|
|
393
393
|
const { App } = await import("./ui/app.js");
|
|
394
|
+
const { relaunchWithSession } = await import("./ui/utils/relaunch.js");
|
|
394
395
|
const renderer = await createCliRenderer({
|
|
395
396
|
exitOnCtrlC: false,
|
|
396
397
|
// We manage SIGINT ourselves (orchestrator abort → agent cleanup → renderer destroy).
|
|
@@ -441,64 +442,106 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
441
442
|
process.env.TERM_PROGRAM === "WezTerm" ||
|
|
442
443
|
process.env.TERM_PROGRAM === "wezterm" ||
|
|
443
444
|
process.env.MUONROI_FORCE_SHELL_HOLD === "1";
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
/* fall back to async */
|
|
460
|
-
try {
|
|
461
|
-
process.stdout.write(seq);
|
|
462
|
-
}
|
|
463
|
-
catch {
|
|
464
|
-
/* noop */
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
};
|
|
445
|
+
// Restore the terminal to a clean main-screen state: undo Kitty keyboard,
|
|
446
|
+
// raw mode, mouse tracking, bracketed paste, and alt-screen. Shared by the
|
|
447
|
+
// /exit path AND the session-resume relaunch — a relaunch that skips this
|
|
448
|
+
// leaves the child (and, once the parent exits, the shell) with a terminal
|
|
449
|
+
// still in mouse-tracking/alt-screen mode, so stray escape bytes get parsed
|
|
450
|
+
// by the shell as a bogus command ("'…' is not recognized … operable
|
|
451
|
+
// program or batch file").
|
|
452
|
+
function restoreTerminalForHandoff() {
|
|
453
|
+
// Restore terminal state from JS before the native destroyRenderer runs
|
|
454
|
+
restoreTerminalSync();
|
|
455
|
+
renderer.destroy();
|
|
456
|
+
// Use synchronous writes to stdout's underlying fd so escape codes flush
|
|
457
|
+
// before the child inherits stdin — async process.stdout.write() can buffer
|
|
458
|
+
// past the spawn() boundary, leaving the terminal in mouse-tracking mode.
|
|
459
|
+
const writeSync = (seq) => {
|
|
468
460
|
try {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
}
|
|
475
|
-
catch {
|
|
476
|
-
/* noop */
|
|
477
|
-
}
|
|
478
|
-
}
|
|
461
|
+
const fs = require("node:fs");
|
|
462
|
+
fs.writeSync(1, seq);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
/* fall back to async */
|
|
479
466
|
try {
|
|
480
|
-
process.
|
|
467
|
+
process.stdout.write(seq);
|
|
481
468
|
}
|
|
482
469
|
catch {
|
|
483
470
|
/* noop */
|
|
484
471
|
}
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
try {
|
|
475
|
+
// 1. Take stdin out of raw mode + pause it so no buffered keystrokes
|
|
476
|
+
// (or in-flight mouse-event bytes) hit the child.
|
|
477
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
485
478
|
try {
|
|
486
|
-
process.stdin.
|
|
479
|
+
process.stdin.setRawMode(false);
|
|
487
480
|
}
|
|
488
481
|
catch {
|
|
489
482
|
/* noop */
|
|
490
483
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
process.stdin.pause();
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
/* noop */
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
process.stdin.unref?.();
|
|
498
493
|
}
|
|
499
494
|
catch {
|
|
500
|
-
|
|
495
|
+
/* noop */
|
|
501
496
|
}
|
|
497
|
+
// 2. Disable extended-coords first (1006/1015/1005), then the basic
|
|
498
|
+
// tracking modes (1003→1002→1000). Wrong order leaves the terminal
|
|
499
|
+
// emitting SGR-formatted events after the base modes are off.
|
|
500
|
+
writeSync("\x1B[?1006l\x1B[?1015l\x1B[?1005l");
|
|
501
|
+
writeSync("\x1B[?1003l\x1B[?1002l\x1B[?1000l");
|
|
502
|
+
writeSync("\x1B[?2004l\x1B[?25h"); // bracketed paste off, cursor on
|
|
503
|
+
writeSync("\x1B[?1049l\x1B[0m\x1B[!p"); // exit alt-screen, reset SGR, soft reset
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
// best-effort
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
// Resume a different session by restarting the CLI bound to --session <id>.
|
|
510
|
+
// Routes through the SAME teardown as /exit, then supervises the child so the
|
|
511
|
+
// shell never reclaims the foreground mid-restart (which corrupted the
|
|
512
|
+
// terminal and dropped the user back to a broken prompt).
|
|
513
|
+
const onRelaunch = (sessionId) => {
|
|
514
|
+
void agent.cleanup().finally(() => {
|
|
515
|
+
restoreTerminalForHandoff();
|
|
516
|
+
// Let the terminal process the restore sequences before the child takes
|
|
517
|
+
// over stdin (mirrors the /exit shell-hold timing).
|
|
518
|
+
setTimeout(() => {
|
|
519
|
+
try {
|
|
520
|
+
relaunchWithSession(sessionId, { supervise: true });
|
|
521
|
+
}
|
|
522
|
+
catch (err) {
|
|
523
|
+
console.error(`[relaunch] failed: ${err?.message ?? err}`);
|
|
524
|
+
process.exit(1);
|
|
525
|
+
}
|
|
526
|
+
}, 80);
|
|
527
|
+
});
|
|
528
|
+
};
|
|
529
|
+
const onExit = () => {
|
|
530
|
+
void agent.cleanup().finally(() => {
|
|
531
|
+
restoreTerminalForHandoff();
|
|
532
|
+
const writeSync = (seq) => {
|
|
533
|
+
try {
|
|
534
|
+
require("node:fs").writeSync(1, seq);
|
|
535
|
+
}
|
|
536
|
+
catch {
|
|
537
|
+
try {
|
|
538
|
+
process.stdout.write(seq);
|
|
539
|
+
}
|
|
540
|
+
catch {
|
|
541
|
+
/* noop */
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
};
|
|
502
545
|
// WezTerm (and similar single-pane terminals) closes the window when
|
|
503
546
|
// the foreground process exits. To keep the pane usable after `/exit`,
|
|
504
547
|
// we hand control over to an interactive shell instead of exiting.
|
|
@@ -547,6 +590,7 @@ async function startInteractive(apiKey, baseURL, model, maxToolRounds, batchApi,
|
|
|
547
590
|
},
|
|
548
591
|
initialMessage,
|
|
549
592
|
onExit,
|
|
593
|
+
onRelaunch,
|
|
550
594
|
}));
|
|
551
595
|
}
|
|
552
596
|
async function runHeadless(prompt, apiKey, baseURL, model, maxToolRounds, batchApi, sandboxMode, sandboxSettings, format, session, permissionMode = "safe", councilAutoAnswer) {
|
|
@@ -8,6 +8,12 @@ export declare class SessionStore {
|
|
|
8
8
|
/**
|
|
9
9
|
* List recent sessions in this workspace (most recently updated first).
|
|
10
10
|
* Used by the `/sessions` slash command for resume picking.
|
|
11
|
+
*
|
|
12
|
+
* Only sessions that actually have a persisted message are returned. Every
|
|
13
|
+
* CLI launch WITHOUT `--session` opens a fresh empty session, and test /
|
|
14
|
+
* harness runs leave behind title-only stubs — without this filter the picker
|
|
15
|
+
* fills with empty rows that resume to a blank screen, so a user picking one
|
|
16
|
+
* thinks resume is broken.
|
|
11
17
|
*/
|
|
12
18
|
listRecentSessions(limit?: number): SessionInfo[];
|
|
13
19
|
getLatestSession(): SessionInfo | null;
|
|
@@ -64,14 +64,21 @@ export class SessionStore {
|
|
|
64
64
|
/**
|
|
65
65
|
* List recent sessions in this workspace (most recently updated first).
|
|
66
66
|
* Used by the `/sessions` slash command for resume picking.
|
|
67
|
+
*
|
|
68
|
+
* Only sessions that actually have a persisted message are returned. Every
|
|
69
|
+
* CLI launch WITHOUT `--session` opens a fresh empty session, and test /
|
|
70
|
+
* harness runs leave behind title-only stubs — without this filter the picker
|
|
71
|
+
* fills with empty rows that resume to a blank screen, so a user picking one
|
|
72
|
+
* thinks resume is broken.
|
|
67
73
|
*/
|
|
68
74
|
listRecentSessions(limit = 20) {
|
|
69
75
|
const rows = getDatabase()
|
|
70
76
|
.prepare(`
|
|
71
|
-
SELECT id, workspace_id, title, model, mode, cwd_at_start, cwd_last, status, created_at, updated_at
|
|
72
|
-
FROM sessions
|
|
73
|
-
WHERE workspace_id = ?
|
|
74
|
-
|
|
77
|
+
SELECT s.id, s.workspace_id, s.title, s.model, s.mode, s.cwd_at_start, s.cwd_last, s.status, s.created_at, s.updated_at
|
|
78
|
+
FROM sessions s
|
|
79
|
+
WHERE s.workspace_id = ?
|
|
80
|
+
AND EXISTS (SELECT 1 FROM messages m WHERE m.session_id = s.id)
|
|
81
|
+
ORDER BY s.updated_at DESC
|
|
75
82
|
LIMIT ?
|
|
76
83
|
`)
|
|
77
84
|
.all(this.workspace.id, limit);
|
package/dist/src/ui/app.d.ts
CHANGED
|
@@ -16,6 +16,6 @@ import "./slash/export.js";
|
|
|
16
16
|
import "./slash/status.js";
|
|
17
17
|
import type { AppProps } from "./types.js";
|
|
18
18
|
export type { AppStartupConfig } from "./types.js";
|
|
19
|
-
export declare function App({ agent, startupConfig, initialMessage, onExit }: AppProps): import("react").ReactNode;
|
|
19
|
+
export declare function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }: AppProps): import("react").ReactNode;
|
|
20
20
|
export type { McpRunInfo } from "./components/message-view.js";
|
|
21
21
|
export { computeMcpRunInfo } from "./components/message-view.js";
|
package/dist/src/ui/app.js
CHANGED
|
@@ -408,7 +408,7 @@ ${prompt}`;
|
|
|
408
408
|
// user cannot enable a provider we are not actively maintaining UX for.
|
|
409
409
|
// Hoisted to module-level so React useEffect deps stay stable across renders.
|
|
410
410
|
const SPLASH_PROVIDERS = ["deepseek", "siliconflow"];
|
|
411
|
-
export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
411
|
+
export function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }) {
|
|
412
412
|
const t = dark;
|
|
413
413
|
const renderer = useRenderer();
|
|
414
414
|
// Set initial status bar values synchronously before first render
|
|
@@ -3929,9 +3929,15 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
3929
3929
|
case "update":
|
|
3930
3930
|
setIsUpdating(true);
|
|
3931
3931
|
setUpdateOutput(null);
|
|
3932
|
+
// Always echo into the message log — the home-screen `updateOutput`
|
|
3933
|
+
// banner only renders on the splash screen, so an /update run inside an
|
|
3934
|
+
// active chat would otherwise produce no visible output at all.
|
|
3935
|
+
setMessages((prev) => [...prev, buildAssistantEntry("⟳ Checking for updates…")]);
|
|
3932
3936
|
runUpdate(startupConfig.version).then((result) => {
|
|
3933
3937
|
setIsUpdating(false);
|
|
3934
|
-
|
|
3938
|
+
const text = result.success ? result.output : `Update failed: ${result.output}`;
|
|
3939
|
+
setUpdateOutput(text);
|
|
3940
|
+
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
3935
3941
|
});
|
|
3936
3942
|
break;
|
|
3937
3943
|
case "clear":
|
|
@@ -4830,9 +4836,14 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
4830
4836
|
if (key.name === "return") {
|
|
4831
4837
|
setIsUpdating(true);
|
|
4832
4838
|
setShowUpdateModal(false);
|
|
4839
|
+
// Echo into the message log too — same reason as the /update command:
|
|
4840
|
+
// the updateOutput banner only renders on the home/splash screen.
|
|
4841
|
+
setMessages((prev) => [...prev, buildAssistantEntry("⟳ Checking for updates…")]);
|
|
4833
4842
|
runUpdate(startupConfig.version).then((result) => {
|
|
4834
4843
|
setIsUpdating(false);
|
|
4835
|
-
|
|
4844
|
+
const text = result.success ? result.output : `Update failed: ${result.output}`;
|
|
4845
|
+
setUpdateOutput(text);
|
|
4846
|
+
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
4836
4847
|
});
|
|
4837
4848
|
return;
|
|
4838
4849
|
}
|
|
@@ -5228,12 +5239,21 @@ export function App({ agent, startupConfig, initialMessage, onExit }) {
|
|
|
5228
5239
|
}
|
|
5229
5240
|
// Close the modal first so the toast renders before the spawn.
|
|
5230
5241
|
setShowSessionPicker(false);
|
|
5231
|
-
pushToast("info", `Resuming session ${picked.id
|
|
5232
|
-
// Defer to the next tick so OpenTUI flushes the toast frame
|
|
5233
|
-
//
|
|
5242
|
+
pushToast("info", `Resuming session ${picked.id}… restarting CLI`);
|
|
5243
|
+
// Defer to the next tick so OpenTUI flushes the toast frame, then hand
|
|
5244
|
+
// off to onRelaunch — which tears down the renderer + restores the
|
|
5245
|
+
// terminal (raw mode / mouse tracking / alt-screen) and supervises the
|
|
5246
|
+
// child. Doing the spawn here directly (the old relaunchWithSession
|
|
5247
|
+
// call) skipped that teardown and dropped the user to a corrupted
|
|
5248
|
+
// shell prompt. Fall back to the legacy path only if no handler wired.
|
|
5234
5249
|
setTimeout(() => {
|
|
5235
5250
|
try {
|
|
5236
|
-
|
|
5251
|
+
if (onRelaunch) {
|
|
5252
|
+
onRelaunch(picked.id);
|
|
5253
|
+
}
|
|
5254
|
+
else {
|
|
5255
|
+
relaunchWithSession(picked.id);
|
|
5256
|
+
}
|
|
5237
5257
|
}
|
|
5238
5258
|
catch (err) {
|
|
5239
5259
|
console.error(`[session-picker] relaunch failed: ${err?.message ?? err}`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { Semantic } from "@muonroi/agent-harness-opentui";
|
|
2
3
|
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
4
|
/**
|
|
4
5
|
* Recent-sessions picker. Opened by `/sessions` or `/session`. Selecting a
|
|
@@ -14,15 +15,19 @@ export function SessionPickerModal({ t, sessions, focusIndex, width, height, })
|
|
|
14
15
|
const panelHeight = Math.min(contentHeight, maxH);
|
|
15
16
|
const top = bottomAlignedModalTop(height, panelHeight);
|
|
16
17
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
18
|
+
return (_jsx(Semantic, { id: "session-picker", role: "dialog", name: "Resume session", isModal: true, children: _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) => {
|
|
19
|
+
const focused = idx === focusIndex;
|
|
20
|
+
const ts = formatTimestamp(s.updatedAt);
|
|
21
|
+
const titleRaw = s.title?.trim() || "(untitled)";
|
|
22
|
+
const titleMax = Math.max(8, panelWidth - 38);
|
|
23
|
+
const title = titleRaw.length > titleMax ? `${titleRaw.slice(0, titleMax - 1)}…` : titleRaw;
|
|
24
|
+
// Show the FULL session id (not slice(-8)). The resumed-session
|
|
25
|
+
// header displays the full id, so a truncated id here made users
|
|
26
|
+
// think they had resumed a "different" session when it was the
|
|
27
|
+
// same one shown in two formats.
|
|
28
|
+
const idLabel = s.id;
|
|
29
|
+
return (_jsx(Semantic, { id: `session-item-${idx}`, role: "listitem", name: `${title} ${idLabel}`, value: s.id, selected: focused || undefined, children: _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} ${idLabel}` })] }) }, s.id));
|
|
30
|
+
})) }), _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
31
|
}
|
|
27
32
|
/**
|
|
28
33
|
* Compact MM-DD HH:MM timestamp for the picker rows. Trades the year for
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "@opentui/react/jsx-runtime";
|
|
2
|
+
import { Semantic } from "@muonroi/agent-harness-opentui";
|
|
2
3
|
import { bottomAlignedModalTop } from "../utils/modal.js";
|
|
3
4
|
export function UpdateModal({ t, width, height, currentVersion, latestVersion, }) {
|
|
4
5
|
const overlayBg = "#000000cc";
|
|
5
6
|
const panelWidth = Math.min(60, width - 6);
|
|
6
7
|
const panelHeight = 9;
|
|
7
8
|
const top = bottomAlignedModalTop(height, panelHeight);
|
|
8
|
-
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: "#f59e0b", children: _jsx("b", { children: "Update Available" }) }), _jsx("text", { fg: t.textMuted, children: "esc to dismiss" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsxs("text", { fg: t.text, children: ["A new version of muonroi-cli is available: ", _jsxs("span", { style: { fg: t.textMuted }, children: ["v", currentVersion] }), " → ", _jsxs("span", { style: { fg: "#22c55e" }, children: ["v", latestVersion] })] }) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "Press enter to update now, or esc to dismiss" }) })] }) }));
|
|
9
|
+
return (_jsx(Semantic, { id: "update-modal", role: "dialog", name: "Update Available", isModal: true, children: _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: "#f59e0b", children: _jsx("b", { children: "Update Available" }) }), _jsx("text", { fg: t.textMuted, children: "esc to dismiss" })] }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsxs("text", { fg: t.text, children: ["A new version of muonroi-cli is available: ", _jsxs("span", { style: { fg: t.textMuted }, children: ["v", currentVersion] }), " → ", _jsxs("span", { style: { fg: "#22c55e" }, children: ["v", latestVersion] })] }) }), _jsx("box", { flexShrink: 0, paddingLeft: 2, paddingRight: 2, paddingTop: 1, children: _jsx("text", { fg: t.textMuted, children: "Press enter to update now, or esc to dismiss" }) })] }) }) }));
|
|
9
10
|
}
|
|
10
11
|
//# sourceMappingURL=update-modal.js.map
|
package/dist/src/ui/types.d.ts
CHANGED
|
@@ -69,6 +69,13 @@ export interface AppProps {
|
|
|
69
69
|
startupConfig: AppStartupConfig;
|
|
70
70
|
initialMessage?: string;
|
|
71
71
|
onExit?: () => void;
|
|
72
|
+
/**
|
|
73
|
+
* Restart the CLI bound to a different session id (used by the /sessions
|
|
74
|
+
* picker). Routes through the same terminal teardown as onExit and supervises
|
|
75
|
+
* the child process, so resuming never strands the user at a corrupted shell
|
|
76
|
+
* prompt.
|
|
77
|
+
*/
|
|
78
|
+
onRelaunch?: (sessionId: string) => void;
|
|
72
79
|
}
|
|
73
80
|
export interface ActiveTurnState {
|
|
74
81
|
kind: "local" | "telegram";
|
|
@@ -27,6 +27,23 @@ export interface RelaunchOptions {
|
|
|
27
27
|
onExit?: (code: number) => void;
|
|
28
28
|
/** Injected spawn for tests. Defaults to the real node:child_process spawn. */
|
|
29
29
|
spawnFn?: typeof spawn;
|
|
30
|
+
/**
|
|
31
|
+
* Run immediately before the spawn — used to tear down the current TUI
|
|
32
|
+
* (renderer.destroy, restore raw mode / mouse tracking / alt-screen) so the
|
|
33
|
+
* child inherits a CLEAN terminal. Without this the child (and the shell, if
|
|
34
|
+
* the parent exits) inherit a terminal still in mouse-tracking/alt-screen
|
|
35
|
+
* mode, and stray escape bytes get parsed by the shell as a command
|
|
36
|
+
* ("'…' is not recognized as an internal or external command").
|
|
37
|
+
*/
|
|
38
|
+
beforeSpawn?: () => void;
|
|
39
|
+
/**
|
|
40
|
+
* When true, the parent stays alive and supervises the child: it exits with
|
|
41
|
+
* the child's exit code instead of exiting the instant the child spawns.
|
|
42
|
+
* This keeps any intermediate launcher (a `bun run` wrapper, the bun bin
|
|
43
|
+
* shim) alive so the shell does NOT regain the terminal foreground while the
|
|
44
|
+
* child is running. Required for a correct in-terminal restart on Windows.
|
|
45
|
+
*/
|
|
46
|
+
supervise?: boolean;
|
|
30
47
|
}
|
|
31
48
|
/**
|
|
32
49
|
* Spawn a fresh CLI process bound to {sessionId} and exit the current one.
|
|
@@ -26,16 +26,22 @@ export function sanitizeArgvForResume(args, sessionId) {
|
|
|
26
26
|
const out = [];
|
|
27
27
|
for (let i = 0; i < args.length; i++) {
|
|
28
28
|
const a = args[i];
|
|
29
|
-
if (a === "-s" || a === "--session") {
|
|
29
|
+
if (a === "-s" || a === "--session" || a === "--mock-llm") {
|
|
30
30
|
// skip the flag AND its value (if present and not another flag)
|
|
31
31
|
const next = args[i + 1];
|
|
32
32
|
if (next !== undefined && !next.startsWith("-"))
|
|
33
33
|
i++;
|
|
34
34
|
continue;
|
|
35
35
|
}
|
|
36
|
-
if (a.startsWith("--session=")) {
|
|
36
|
+
if (a.startsWith("--session=") || a.startsWith("--mock-llm=")) {
|
|
37
37
|
continue; // skip the combined form
|
|
38
38
|
}
|
|
39
|
+
// Transient launch-mode flags must NOT survive a resume — re-entering
|
|
40
|
+
// agent-mode (named-pipe transport) on a user-driven resume strands the
|
|
41
|
+
// child with no harness server. Drop them.
|
|
42
|
+
if (a === "--agent-mode") {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
39
45
|
out.push(a);
|
|
40
46
|
}
|
|
41
47
|
out.push("--session", sessionId);
|
|
@@ -60,12 +66,30 @@ export function relaunchWithSession(sessionId, opts = {}) {
|
|
|
60
66
|
const exit = opts.onExit ?? ((code) => process.exit(code));
|
|
61
67
|
const spawnImpl = opts.spawnFn ?? spawn;
|
|
62
68
|
const args = sanitizeArgvForResume(argv.slice(1), sessionId);
|
|
69
|
+
// Tear down the current TUI and restore the terminal BEFORE spawning so the
|
|
70
|
+
// child inherits a clean TTY (no raw mode / mouse tracking / alt-screen).
|
|
71
|
+
if (opts.beforeSpawn) {
|
|
72
|
+
try {
|
|
73
|
+
opts.beforeSpawn();
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
console.error(`[relaunch] beforeSpawn teardown failed: ${err?.message ?? err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
63
79
|
const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
|
|
64
80
|
child.once("error", (err) => {
|
|
65
81
|
console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
|
|
66
82
|
exit(1);
|
|
67
83
|
});
|
|
68
|
-
|
|
84
|
+
if (opts.supervise) {
|
|
85
|
+
// Stay alive until the child exits so any intermediate launcher (bun run
|
|
86
|
+
// wrapper / bin shim) keeps the terminal foreground for the child instead
|
|
87
|
+
// of letting the shell reclaim it mid-restart.
|
|
88
|
+
child.once("exit", (code) => exit(code ?? 0));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
// Legacy fast-handoff: exit the instant the child spawns. Correct only when
|
|
92
|
+
// there is no intervening launcher waiting on this process.
|
|
69
93
|
child.once("spawn", () => exit(0));
|
|
70
94
|
}
|
|
71
95
|
//# sourceMappingURL=relaunch.js.map
|
|
@@ -40,6 +40,24 @@ describe("sanitizeArgvForResume", () => {
|
|
|
40
40
|
expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
|
|
41
41
|
expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
|
|
42
42
|
});
|
|
43
|
+
it("strips transient launch-mode flags (--agent-mode) so resume does not re-enter agent-mode", () => {
|
|
44
|
+
expect(sanitizeArgvForResume(["--agent-mode", "-m", "grok-build-0.1"], "new-id")).toEqual([
|
|
45
|
+
"-m",
|
|
46
|
+
"grok-build-0.1",
|
|
47
|
+
"--session",
|
|
48
|
+
"new-id",
|
|
49
|
+
]);
|
|
50
|
+
});
|
|
51
|
+
it("strips `--mock-llm <dir>` and the combined form", () => {
|
|
52
|
+
expect(sanitizeArgvForResume(["--mock-llm", "/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
53
|
+
expect(sanitizeArgvForResume(["--mock-llm=/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
|
|
54
|
+
});
|
|
55
|
+
it("strips agent-mode + mock-llm together (harness-launched resume → clean user CLI)", () => {
|
|
56
|
+
expect(sanitizeArgvForResume(["--agent-mode", "--mock-llm", "/fx", "--session", "old"], "new")).toEqual([
|
|
57
|
+
"--session",
|
|
58
|
+
"new",
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
43
61
|
});
|
|
44
62
|
describe("relaunchWithSession", () => {
|
|
45
63
|
it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
|
|
@@ -72,6 +90,38 @@ describe("relaunchWithSession", () => {
|
|
|
72
90
|
expect(errMock).toHaveBeenCalled();
|
|
73
91
|
errMock.mockRestore();
|
|
74
92
|
});
|
|
93
|
+
it("runs beforeSpawn teardown before spawning", () => {
|
|
94
|
+
const calls = [];
|
|
95
|
+
const child = new EventEmitter();
|
|
96
|
+
const spawnMock = vi.fn(() => {
|
|
97
|
+
calls.push("spawn");
|
|
98
|
+
return child;
|
|
99
|
+
});
|
|
100
|
+
relaunchWithSession("sess", {
|
|
101
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
102
|
+
onExit: () => { },
|
|
103
|
+
spawnFn: spawnMock,
|
|
104
|
+
beforeSpawn: () => calls.push("teardown"),
|
|
105
|
+
});
|
|
106
|
+
expect(calls).toEqual(["teardown", "spawn"]);
|
|
107
|
+
});
|
|
108
|
+
it("supervise: stays alive until the child exits, then propagates the exit code", () => {
|
|
109
|
+
const exitMock = vi.fn();
|
|
110
|
+
const child = new EventEmitter();
|
|
111
|
+
const spawnMock = vi.fn(() => child);
|
|
112
|
+
relaunchWithSession("sess", {
|
|
113
|
+
argv: ["/bin/bun", "src/index.ts"],
|
|
114
|
+
onExit: exitMock,
|
|
115
|
+
spawnFn: spawnMock,
|
|
116
|
+
supervise: true,
|
|
117
|
+
});
|
|
118
|
+
// Does NOT exit on spawn when supervising.
|
|
119
|
+
child.emit("spawn");
|
|
120
|
+
expect(exitMock).not.toHaveBeenCalled();
|
|
121
|
+
// Exits with the child's code when the child finally exits.
|
|
122
|
+
child.emit("exit", 3);
|
|
123
|
+
expect(exitMock).toHaveBeenCalledWith(3);
|
|
124
|
+
});
|
|
75
125
|
it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
|
|
76
126
|
expect(() => relaunchWithSession("sess", {
|
|
77
127
|
argv: [],
|