@modelstatus/cli 0.1.47 → 0.1.49

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.47",
3
+ "version": "0.1.49",
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
@@ -69,11 +69,17 @@ function useTermDims() {
69
69
  const cols = Number.isFinite(fw) && fw > 0 ? fw : dims.cols;
70
70
  const rows = Number.isFinite(fh) && fh > 0 ? fh : dims.rows;
71
71
  // Never exceed the real terminal width (a wider frame soft-wraps every chrome
72
- // line and shatters the window). Fill the FULL terminal height: when the frame
73
- // height ≥ the terminal rows, ink redraws with a full clear each frame; render
74
- // shorter than the screen (the old 80-row cap did this on tall external
75
- // monitors) and ink falls back to cursor-up diffing, which drifts/scrolls.
76
- return { outer: Math.min(cols || 80, 220), termRows: Math.max(8, Math.min(rows || 24, 200)) };
72
+ // line and shatters the window).
73
+ //
74
+ // Height: RESERVE THE LAST ROW (rows - 1). A frame that fills the full height
75
+ // makes ink write+newline the bottom line, which scrolls the buffer up by 1
76
+ // EVERY render; across the handful of renders during load/settle that scroll
77
+ // accumulates and creeps the window's top off-screen (the "not full-screen after
78
+ // the game / in Warp" clip). One row short keeps the last line blank so nothing
79
+ // ever scrolls. This is safe now that the TUI lives in the ALT SCREEN: ink's
80
+ // cursor-up diffing (which it uses when the frame is shorter than the terminal)
81
+ // stays inside the fixed alt buffer and can't drift the way it did inline.
82
+ return { outer: Math.min(cols || 80, 220), termRows: Math.max(8, Math.min((rows || 24) - 1, 200)) };
77
83
  }
78
84
 
79
85
  function PromptRow({ prompt }) {
@@ -269,12 +275,37 @@ export const appController = {
269
275
  remount(next = {}) {
270
276
  const opts = { ...(this._opts || {}), ...next };
271
277
  this._opts = opts;
278
+ // We're still in the alt screen (entered by runApp; the game kept it). Clear +
279
+ // home so the fresh Ink tree paints the whole screen from the top with no
280
+ // leftover game frame and no inline-cursor drift.
281
+ try { process.stdout.write("\x1b[2J\x1b[H"); } catch { /* ignore */ }
272
282
  this._instance = render(h(Bootstrap, opts));
273
283
  return this._instance;
274
284
  },
275
285
  };
276
286
 
277
287
  export function runApp(opts) {
288
+ // Run the TUI in the ALTERNATE SCREEN BUFFER (a clean full-screen surface, like
289
+ // the game). Ink's default INLINE rendering drifts after the game's alt-screen
290
+ // round-trip in block-model terminals (Warp) — the window scrolls off the top
291
+ // and Ctrl-L can't recover it. Owning the alt screen ourselves makes every
292
+ // (re)mount paint a clean full screen, and leaving it on quit restores the
293
+ // host's scrollback. The in-TUI game keeps this alt screen (keepAlt) instead of
294
+ // toggling its own, so there's no unreliable nested-alt-screen handling.
295
+ const out = process.stdout;
296
+ const leaveAlt = () => {
297
+ if (!appController._inAlt) return;
298
+ try { out.write("\x1b[?1049l\x1b[?25h"); } catch { /* ignore */ }
299
+ appController._inAlt = false;
300
+ };
301
+ appController._leaveAlt = leaveAlt;
302
+ // Never strand the terminal in the alt buffer on a crash/signal.
303
+ process.once("exit", leaveAlt);
304
+ process.once("SIGINT", () => { leaveAlt(); process.exit(130); });
305
+ process.once("SIGTERM", () => { leaveAlt(); process.exit(143); });
306
+ try { out.write("\x1b[?1049h\x1b[2J\x1b[H"); } catch { /* ignore */ }
307
+ appController._inAlt = true;
308
+
278
309
  appController._opts = opts;
279
310
  const app = render(h(Bootstrap, opts));
280
311
  appController._instance = app;
@@ -289,6 +320,7 @@ export function runApp(opts) {
289
320
  if (appController._instance && appController._instance !== inst) {
290
321
  arm(appController._instance);
291
322
  } else {
323
+ leaveAlt(); // real quit → restore the host screen + scrollback
292
324
  resolve();
293
325
  }
294
326
  });
@@ -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,12 +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
- // Let the terminal re-flow after leaving the alt screen before Ink measures
85
- // some terminals (Warp's block model) report a stale size for a beat, which
86
- // would make Ink render too short and drift. useTermDims also re-measures a
87
- // few beats post-mount as a backstop.
88
- await new Promise((r) => setTimeout(r, 80));
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));
89
88
  appController.remount({ initialView, fresh: false });
90
89
  } catch (e) {
91
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 {