@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@modelstatus/cli",
3
- "version": "0.1.46",
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
- const on = () => setDims({ cols: stdout.columns || 80, rows: stdout.rows || 24 });
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
- return () => stdout.off("resize", on);
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
  });
@@ -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
- try { process.stdout.write("\x1b[r\x1b[2J\x1b[3J\x1b[H"); } catch { /* ignore */ }
84
- await new Promise((r) => setImmediate(r)); // let the reset flush before Ink mounts
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;
@@ -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
@@ -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
- this.write(ALT_ENTER + CURSOR_HIDE + CLEAR_SCREEN);
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
- try { this.write(SGR_RESET + CURSOR_SHOW + ALT_LEAVE); } catch {}
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 {