@modelstatus/cli 0.1.46 → 0.1.48
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/package.json +1 -1
- package/src/tui/app.js +41 -2
- package/src/tui/game/launch.js +5 -2
- package/src/tui/game/loop.js +2 -2
- package/src/tui/game/term.js +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.48",
|
|
4
4
|
"description": "Track which AI models you use, where, and never get surprised by a retirement. Free offline model-health for any repo (mm status), browser sign-in for cloud inventory + alerts.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"llm",
|
package/src/tui/app.js
CHANGED
|
@@ -47,9 +47,22 @@ function useTermDims() {
|
|
|
47
47
|
const [dims, setDims] = React.useState({ cols: (stdout && stdout.columns) || 80, rows: (stdout && stdout.rows) || 24 });
|
|
48
48
|
React.useEffect(() => {
|
|
49
49
|
if (!stdout) return undefined;
|
|
50
|
-
|
|
50
|
+
// Functional update that returns the SAME object when nothing changed, so a
|
|
51
|
+
// re-measure that finds identical dims doesn't trigger a needless re-render.
|
|
52
|
+
const on = () => setDims((prev) => {
|
|
53
|
+
const cols = stdout.columns || 80, rows = stdout.rows || 24;
|
|
54
|
+
return prev.cols === cols && prev.rows === rows ? prev : { cols, rows };
|
|
55
|
+
});
|
|
51
56
|
stdout.on("resize", on);
|
|
52
|
-
|
|
57
|
+
// Re-measure a few beats after mount. When the TUI REMOUNTS right after the
|
|
58
|
+
// game leaves the alternate screen, some terminals (notably Warp's block
|
|
59
|
+
// model) re-flow the viewport a moment LATER and report stdout.{columns,rows}
|
|
60
|
+
// stale at mount-time — so the first frame is too SHORT and Ink falls back to
|
|
61
|
+
// cursor-up diffing, which drifts/scrolls the window off the top (and Ctrl-L
|
|
62
|
+
// can't fix it because it redraws at the same wrong height). These one-shot
|
|
63
|
+
// re-reads pick up the settled size and snap the window back to full screen.
|
|
64
|
+
const timers = [60, 250, 600].map((ms) => setTimeout(on, ms));
|
|
65
|
+
return () => { stdout.off("resize", on); timers.forEach(clearTimeout); };
|
|
53
66
|
}, [stdout]);
|
|
54
67
|
const fw = Number(process.env.MM_TUI_WIDTH);
|
|
55
68
|
const fh = Number(process.env.MM_TUI_HEIGHT);
|
|
@@ -256,12 +269,37 @@ export const appController = {
|
|
|
256
269
|
remount(next = {}) {
|
|
257
270
|
const opts = { ...(this._opts || {}), ...next };
|
|
258
271
|
this._opts = opts;
|
|
272
|
+
// We're still in the alt screen (entered by runApp; the game kept it). Clear +
|
|
273
|
+
// home so the fresh Ink tree paints the whole screen from the top with no
|
|
274
|
+
// leftover game frame and no inline-cursor drift.
|
|
275
|
+
try { process.stdout.write("\x1b[2J\x1b[H"); } catch { /* ignore */ }
|
|
259
276
|
this._instance = render(h(Bootstrap, opts));
|
|
260
277
|
return this._instance;
|
|
261
278
|
},
|
|
262
279
|
};
|
|
263
280
|
|
|
264
281
|
export function runApp(opts) {
|
|
282
|
+
// Run the TUI in the ALTERNATE SCREEN BUFFER (a clean full-screen surface, like
|
|
283
|
+
// the game). Ink's default INLINE rendering drifts after the game's alt-screen
|
|
284
|
+
// round-trip in block-model terminals (Warp) — the window scrolls off the top
|
|
285
|
+
// and Ctrl-L can't recover it. Owning the alt screen ourselves makes every
|
|
286
|
+
// (re)mount paint a clean full screen, and leaving it on quit restores the
|
|
287
|
+
// host's scrollback. The in-TUI game keeps this alt screen (keepAlt) instead of
|
|
288
|
+
// toggling its own, so there's no unreliable nested-alt-screen handling.
|
|
289
|
+
const out = process.stdout;
|
|
290
|
+
const leaveAlt = () => {
|
|
291
|
+
if (!appController._inAlt) return;
|
|
292
|
+
try { out.write("\x1b[?1049l\x1b[?25h"); } catch { /* ignore */ }
|
|
293
|
+
appController._inAlt = false;
|
|
294
|
+
};
|
|
295
|
+
appController._leaveAlt = leaveAlt;
|
|
296
|
+
// Never strand the terminal in the alt buffer on a crash/signal.
|
|
297
|
+
process.once("exit", leaveAlt);
|
|
298
|
+
process.once("SIGINT", () => { leaveAlt(); process.exit(130); });
|
|
299
|
+
process.once("SIGTERM", () => { leaveAlt(); process.exit(143); });
|
|
300
|
+
try { out.write("\x1b[?1049h\x1b[2J\x1b[H"); } catch { /* ignore */ }
|
|
301
|
+
appController._inAlt = true;
|
|
302
|
+
|
|
265
303
|
appController._opts = opts;
|
|
266
304
|
const app = render(h(Bootstrap, opts));
|
|
267
305
|
appController._instance = app;
|
|
@@ -276,6 +314,7 @@ export function runApp(opts) {
|
|
|
276
314
|
if (appController._instance && appController._instance !== inst) {
|
|
277
315
|
arm(appController._instance);
|
|
278
316
|
} else {
|
|
317
|
+
leaveAlt(); // real quit → restore the host screen + scrollback
|
|
279
318
|
resolve();
|
|
280
319
|
}
|
|
281
320
|
});
|
package/src/tui/game/launch.js
CHANGED
|
@@ -67,6 +67,7 @@ export async function playGameInTui({ dir, width, height, initialView = "scan",
|
|
|
67
67
|
height: process.stdout.rows || height,
|
|
68
68
|
level: 1,
|
|
69
69
|
scanStore: handle,
|
|
70
|
+
inTui: true, // render in the TUI's alt screen; don't toggle a nested one
|
|
70
71
|
});
|
|
71
72
|
} finally {
|
|
72
73
|
try { handle && handle.abort(); } catch { /* ignore */ }
|
|
@@ -80,8 +81,10 @@ export async function playGameInTui({ dir, width, height, initialView = "scan",
|
|
|
80
81
|
// TUI comes back shifted down / not full-height until the next resize. Reset
|
|
81
82
|
// the scroll region (\x1b[r), clear the screen, and home the cursor so the
|
|
82
83
|
// remounted tree fills the whole terminal from the top (same as Ctrl-L).
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
// The game kept the TUI's alt screen intact (inTui → keepAlt), so we never
|
|
85
|
+
// left it — just let the game's restore() flush, then remount. remount() clears
|
|
86
|
+
// + homes inside the alt screen so the fresh tree paints a clean full screen.
|
|
87
|
+
await new Promise((r) => setImmediate(r));
|
|
85
88
|
appController.remount({ initialView, fresh: false });
|
|
86
89
|
} catch (e) {
|
|
87
90
|
if (onError) onError(e); else throw e;
|
package/src/tui/game/loop.js
CHANGED
|
@@ -74,7 +74,7 @@ const fmtNum = (n) => (n == null ? "0" : Number(n).toLocaleString("en-US"));
|
|
|
74
74
|
* @param {object} [o._inject] test seams: { out, inp, proc, now, schedule, autoExitAfterMs }
|
|
75
75
|
* @returns {Promise<{score:number, level:number, best:number, status:string}>}
|
|
76
76
|
*/
|
|
77
|
-
export function runGame({ width, height, level = 1, scanStore = null, onExit, _inject = {} } = {}) {
|
|
77
|
+
export function runGame({ width, height, level = 1, scanStore = null, onExit, inTui = false, _inject = {} } = {}) {
|
|
78
78
|
return new Promise((resolve) => {
|
|
79
79
|
const proc = _inject.proc || process;
|
|
80
80
|
const out = _inject.out || proc.stdout || process.stdout;
|
|
@@ -89,7 +89,7 @@ export function runGame({ width, height, level = 1, scanStore = null, onExit, _i
|
|
|
89
89
|
try { best = Number(loadConfig().dkHighScore) || 0; } catch { best = 0; }
|
|
90
90
|
|
|
91
91
|
const dims = { width, height };
|
|
92
|
-
const term = new Term({ out, inp, proc, color: COLOR_ON });
|
|
92
|
+
const term = new Term({ out, inp, proc, color: COLOR_ON, keepAlt: inTui });
|
|
93
93
|
const input = new InputState({ now });
|
|
94
94
|
|
|
95
95
|
// Allocate the buffer for the WHOLE frame: HUD rows + board + key row, BOARD_W
|
package/src/tui/game/term.js
CHANGED
|
@@ -216,11 +216,17 @@ export class Term {
|
|
|
216
216
|
inp = process.stdin,
|
|
217
217
|
proc = process,
|
|
218
218
|
color = COLOR_ON,
|
|
219
|
+
// When launched from inside the TUI (which now OWNS the alternate screen),
|
|
220
|
+
// don't toggle the alt buffer ourselves — render into the TUI's alt screen and
|
|
221
|
+
// leave it intact on exit so the remounted TUI stays on a clean full screen
|
|
222
|
+
// (toggling a nested alt screen is unreliable, esp. in Warp's block model).
|
|
223
|
+
keepAlt = false,
|
|
219
224
|
} = {}) {
|
|
220
225
|
this.out = out;
|
|
221
226
|
this.inp = inp;
|
|
222
227
|
this.proc = proc;
|
|
223
228
|
this.color = color;
|
|
229
|
+
this.keepAlt = keepAlt;
|
|
224
230
|
this.started = false;
|
|
225
231
|
this.restored = false;
|
|
226
232
|
this._wasRaw = false;
|
|
@@ -241,8 +247,9 @@ export class Term {
|
|
|
241
247
|
|
|
242
248
|
// Enter alt screen + hide cursor ONCE (never per frame — that's the anti-
|
|
243
249
|
// flicker invariant). Clear the alt buffer so a stale prior frame can't show
|
|
244
|
-
// through before the first diff paints.
|
|
245
|
-
|
|
250
|
+
// through before the first diff paints. keepAlt → the TUI already owns the alt
|
|
251
|
+
// screen, so only hide-cursor + clear (don't re-enter / nest the alt buffer).
|
|
252
|
+
this.write((this.keepAlt ? "" : ALT_ENTER) + CURSOR_HIDE + CLEAR_SCREEN);
|
|
246
253
|
|
|
247
254
|
// Raw mode so we get bytes immediately (no line buffering / no echo). Save
|
|
248
255
|
// the prior state so restore() returns the terminal exactly as it was.
|
|
@@ -300,8 +307,10 @@ export class Term {
|
|
|
300
307
|
this.restored = true;
|
|
301
308
|
|
|
302
309
|
// Order matters: reset SGR + show cursor + leave alt LAST so the chrome
|
|
303
|
-
// lands back on the normal buffer with a visible cooked cursor.
|
|
304
|
-
|
|
310
|
+
// lands back on the normal buffer with a visible cooked cursor. keepAlt → the
|
|
311
|
+
// TUI owns the alt screen and will remount into it, so only reset SGR (don't
|
|
312
|
+
// show the cursor or leave the alt buffer).
|
|
313
|
+
try { this.write(this.keepAlt ? SGR_RESET : SGR_RESET + CURSOR_SHOW + ALT_LEAVE); } catch {}
|
|
305
314
|
|
|
306
315
|
const inp = this.inp;
|
|
307
316
|
try {
|