@modelstatus/cli 0.1.37 → 0.1.39
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 +14 -1
- package/src/tui/game/dk-core.js +32 -36
- package/src/tui/game/dk-render.js +3 -2
- package/src/tui/game/launch.js +83 -0
- package/src/tui/views/scan.js +13 -67
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modelstatus/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.39",
|
|
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
|
@@ -17,6 +17,7 @@ import { AlertsView, meta as alertsMeta } from "./views/alerts.js";
|
|
|
17
17
|
import { AccountView, meta as accountMeta } from "./views/account.js";
|
|
18
18
|
import { IntegrationsView, meta as integrationsMeta } from "./views/integrations.js";
|
|
19
19
|
import { SignIn } from "./signin.js";
|
|
20
|
+
import { playGameInTui, gameFitsTerminal } from "./game/launch.js";
|
|
20
21
|
|
|
21
22
|
// `needsAuth: true` views show a sign-in card when there's no apiKey; Local +
|
|
22
23
|
// What's New are public (signed registry over HTTP, no account needed).
|
|
@@ -141,6 +142,18 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
141
142
|
if (handlesBackRef.current) return undefined;
|
|
142
143
|
return setIdx((i) => (i - 1 + VIEWS.length) % VIEWS.length);
|
|
143
144
|
}
|
|
145
|
+
// Shift-P: play Donkey Kong from ANY tab, any time. The Scan tab owns its
|
|
146
|
+
// own P (same launcher) so it isn't double-fired here.
|
|
147
|
+
if (input === "P" && curKeyRef.current !== "scan") {
|
|
148
|
+
const w = (stdout && stdout.columns) || outer;
|
|
149
|
+
const hh = (stdout && stdout.rows) || termRows;
|
|
150
|
+
if (!gameFitsTerminal(w, hh)) return showToast("terminal too small for the game — resize a bit", "#d97706");
|
|
151
|
+
playGameInTui({
|
|
152
|
+
dir, width: w, height: hh, initialView: curKeyRef.current, scanPhase: "play",
|
|
153
|
+
onError: (e) => showToast(`couldn't start the game: ${e?.message || e}`, "#dc2626"),
|
|
154
|
+
});
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
144
157
|
if (input === "q") return exit();
|
|
145
158
|
},
|
|
146
159
|
{ isActive: !prompt && !capturing },
|
|
@@ -209,7 +222,7 @@ export function App({ apiBase, apiKey, dir, initialView, onSignedIn, fresh }) {
|
|
|
209
222
|
h(Text, {}, ""),
|
|
210
223
|
toast ? h(Text, { color: toast.color }, ` ${toast.msg}`) : h(Text, {}, ""),
|
|
211
224
|
h(StatusBar, { segsLeft, segsRight, width: W }),
|
|
212
|
-
h(KeyBar, { keys, width: W }),
|
|
225
|
+
h(KeyBar, { keys: keys.some((k) => k.k === "P") ? keys : [...keys, { k: "P", label: "play" }], width: W }),
|
|
213
226
|
);
|
|
214
227
|
}
|
|
215
228
|
|
package/src/tui/game/dk-core.js
CHANGED
|
@@ -57,7 +57,7 @@ export const AIR_MAX = 130;
|
|
|
57
57
|
export const GRAVITY = 14; // fx/tick added to vy while airborne (full jump → apex ≈ 3 cells)
|
|
58
58
|
export const JUMP_CUT_GRAVITY = 42; // stronger gravity while RISING after release → short hop (~1.5 cells)
|
|
59
59
|
export const TERMINAL_VY = 220; // fx/tick max fall speed
|
|
60
|
-
export const JUMP_VY = -
|
|
60
|
+
export const JUMP_VY = -180; // initial up-velocity → apex ≈ 4.5 cells (higher, snappier)
|
|
61
61
|
export const COYOTE_TICKS = 5; // jump still allowed N ticks after walking off
|
|
62
62
|
export const JUMP_BUFFER_TICKS = 6; // a press N ticks before landing still fires on land
|
|
63
63
|
|
|
@@ -96,10 +96,6 @@ export function cooldownForLevel(level) {
|
|
|
96
96
|
export function barrelSpeedForLevel(level) {
|
|
97
97
|
return Math.min(170, BARREL_ROLL + (level - 1) * 12);
|
|
98
98
|
}
|
|
99
|
-
/** Probability a rolling barrel takes a ladder down per eligibility check. */
|
|
100
|
-
export function ladderChanceForLevel(level) {
|
|
101
|
-
return Math.min(0.6, 0.18 + (level - 1) * 0.1);
|
|
102
|
-
}
|
|
103
99
|
/** By construction the jump apex (~3 cells) clears a 1-cell barrel + player at
|
|
104
100
|
* every level — exposed so a test can assert the invariant numerically. */
|
|
105
101
|
export function jumpClearsBarrel(/* level */) {
|
|
@@ -157,9 +153,11 @@ function girderIndexAt(platforms, x, y) {
|
|
|
157
153
|
return -1;
|
|
158
154
|
}
|
|
159
155
|
|
|
160
|
-
/**
|
|
156
|
+
/** Ladders are DOUBLE-WIDE: they occupy columns [col, col+1]. The ladder at
|
|
157
|
+
* column x within [yTop..yBottom] at row y, or null. The 2-col catch makes
|
|
158
|
+
* mounting forgiving (a single-col ladder was too narrow to latch reliably). */
|
|
161
159
|
function ladderAt(ladders, x, y) {
|
|
162
|
-
for (const l of ladders) if (l.col
|
|
160
|
+
for (const l of ladders) if ((x === l.col || x === l.col + 1) && y >= l.yTop && y <= l.yBottom) return l;
|
|
163
161
|
return null;
|
|
164
162
|
}
|
|
165
163
|
|
|
@@ -184,7 +182,10 @@ export function initGame({
|
|
|
184
182
|
const { BOARD_W, BOARD_H } = boardSize(width, height);
|
|
185
183
|
if (BOARD_W < MIN_W || BOARD_H < MIN_H) return { tooSmall: true, BOARD_W, BOARD_H };
|
|
186
184
|
|
|
187
|
-
|
|
185
|
+
// Fewer girders → bigger vertical gaps → real jump headroom (a jump head-bonks
|
|
186
|
+
// the girder overhead, so girders ~3 rows apart capped jumps at ~1 cell; ~4+
|
|
187
|
+
// rows lets the higher JUMP_VY actually read as a tall jump).
|
|
188
|
+
const N = Math.max(3, Math.min(4, Math.floor(BOARD_H / 4)));
|
|
188
189
|
const gap = Math.floor((BOARD_H - 2) / N);
|
|
189
190
|
const amp = Math.max(0, Math.min(2, Math.floor((gap - 1) / 2)));
|
|
190
191
|
const platforms = [];
|
|
@@ -335,7 +336,7 @@ function stepDying(s, _dt) {
|
|
|
335
336
|
p.vy = Math.min(p.vy + GRAVITY, TERMINAL_VY);
|
|
336
337
|
p.py = clampFx(p.py + p.vy, 0, toFx(s.BOARD_H - 1));
|
|
337
338
|
syncCell(p);
|
|
338
|
-
advanceBarrels(s
|
|
339
|
+
advanceBarrels(s); // juice: hazards stay live during the death beat
|
|
339
340
|
s.statusTimer -= 1;
|
|
340
341
|
if (s.statusTimer <= 0) {
|
|
341
342
|
if (s.lives > 0) {
|
|
@@ -405,7 +406,9 @@ function stepPlaying(s, input, dt) {
|
|
|
405
406
|
if ((wantUp || wantDown) && !input.jump) {
|
|
406
407
|
p.onLadder = true;
|
|
407
408
|
p.vx = 0; p.vy = 0;
|
|
408
|
-
|
|
409
|
+
// Snap to whichever of the two rails the player mounted (double-wide ladder).
|
|
410
|
+
const railCol = cell(p.px) >= onLad.col + 1 ? onLad.col + 1 : onLad.col;
|
|
411
|
+
p.px = toFx(railCol);
|
|
409
412
|
if (wantUp) p.py = Math.max(toFx(onLad.yTop), p.py - CLIMB_SPEED);
|
|
410
413
|
else p.py = Math.min(toFx(onLad.yBottom), p.py + CLIMB_SPEED);
|
|
411
414
|
// Reached an endpoint EXACTLY → step off onto that girder (off the ladder).
|
|
@@ -413,7 +416,7 @@ function stepPlaying(s, input, dt) {
|
|
|
413
416
|
else if (p.py >= toFx(onLad.yBottom)) { p.onLadder = false; p.onGround = true; p.py = toFx(onLad.yBottom); }
|
|
414
417
|
} else if (p.onLadder) {
|
|
415
418
|
// Left the column, pressed jump, or no climb input → unlatch.
|
|
416
|
-
const stillOn = onLad && cell(p.px) === onLad.col;
|
|
419
|
+
const stillOn = onLad && (cell(p.px) === onLad.col || cell(p.px) === onLad.col + 1);
|
|
417
420
|
if (!stillOn || input.jump || !(input.up || input.down)) {
|
|
418
421
|
p.onLadder = false;
|
|
419
422
|
// settle onto a girder if one is at/below this cell; else gravity takes over.
|
|
@@ -535,7 +538,7 @@ function stepPlaying(s, input, dt) {
|
|
|
535
538
|
}
|
|
536
539
|
|
|
537
540
|
// 6/7. BARRELS -------------------------------------------------------------
|
|
538
|
-
advanceBarrels(s
|
|
541
|
+
advanceBarrels(s);
|
|
539
542
|
|
|
540
543
|
// 8. COLLISIONS + SCORING --------------------------------------------------
|
|
541
544
|
const pCol = cell(p.px), pRow = cell(p.py);
|
|
@@ -594,10 +597,11 @@ function stepPlaying(s, input, dt) {
|
|
|
594
597
|
return finalize(s, s.rngSeed);
|
|
595
598
|
}
|
|
596
599
|
|
|
597
|
-
/** Advance every barrel one tick (roll / fall
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
600
|
+
/** Advance every barrel one tick (roll along a girder / fall to the one below) in
|
|
601
|
+
* fx. Barrels NEVER take ladders — random mid-board drops were unreadable/unfair;
|
|
602
|
+
* a barrel rolls to the wall, bounces, and falls, a predictable zig-zag cascade. */
|
|
603
|
+
function advanceBarrels(s) {
|
|
604
|
+
const { platforms, BOARD_W, BOARD_H } = s;
|
|
601
605
|
const maxPx = toFx(BOARD_W - 1);
|
|
602
606
|
const surviving = [];
|
|
603
607
|
for (const b of s.barrels) {
|
|
@@ -620,33 +624,25 @@ function advanceBarrels(s, rnd) {
|
|
|
620
624
|
b.py = nextPy;
|
|
621
625
|
}
|
|
622
626
|
} else {
|
|
623
|
-
// On a girder:
|
|
624
|
-
|
|
625
|
-
|
|
627
|
+
// On a girder: roll along it (never down a ladder). Reaching a wall bounces
|
|
628
|
+
// the barrel and drops it to the girder below.
|
|
629
|
+
const nextPx = b.px + b.vx;
|
|
630
|
+
const nc = cell(nextPx);
|
|
631
|
+
if (nextPx < 0 || nextPx > maxPx) {
|
|
632
|
+
b.vx = -b.vx; // bounce off the wall and fall to the girder below
|
|
626
633
|
b.falling = true;
|
|
627
|
-
b.onLadder = true;
|
|
628
|
-
b.px = toFx(lad.col);
|
|
629
634
|
b.vy = GRAVITY;
|
|
630
635
|
b.py = b.py + b.vy;
|
|
631
636
|
} else {
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
b.
|
|
637
|
+
const giHere = girderIndexAt(platforms, c, cell(b.py) + 1);
|
|
638
|
+
if (giHere >= 0 && nc >= 0 && nc < BOARD_W && platforms[giHere].slopeOffsets[nc] !== undefined) {
|
|
639
|
+
b.px = nextPx;
|
|
640
|
+
b.py = toFx(platforms[giHere].slopeOffsets[nc] - 1); // follow slope
|
|
641
|
+
} else {
|
|
642
|
+
b.px = nextPx; // rolled off the end → fall
|
|
636
643
|
b.falling = true;
|
|
637
644
|
b.vy = GRAVITY;
|
|
638
645
|
b.py = b.py + b.vy;
|
|
639
|
-
} else {
|
|
640
|
-
const giHere = girderIndexAt(platforms, c, cell(b.py) + 1);
|
|
641
|
-
if (giHere >= 0 && nc >= 0 && nc < BOARD_W && platforms[giHere].slopeOffsets[nc] !== undefined) {
|
|
642
|
-
b.px = nextPx;
|
|
643
|
-
b.py = toFx(platforms[giHere].slopeOffsets[nc] - 1); // follow slope
|
|
644
|
-
} else {
|
|
645
|
-
b.px = nextPx; // rolled off the end → fall
|
|
646
|
-
b.falling = true;
|
|
647
|
-
b.vy = GRAVITY;
|
|
648
|
-
b.py = b.py + b.vy;
|
|
649
|
-
}
|
|
650
646
|
}
|
|
651
647
|
}
|
|
652
648
|
}
|
|
@@ -62,9 +62,10 @@ function buildGrid(state, glyph) {
|
|
|
62
62
|
for (const p of platforms) {
|
|
63
63
|
for (let x = 0; x < BOARD_W; x++) put(x, p.slopeOffsets[x], glyph.girder, "girder");
|
|
64
64
|
}
|
|
65
|
-
// ladders
|
|
65
|
+
// ladders — DOUBLE-WIDE: draw both rails (col and col+1) to match the 2-col
|
|
66
|
+
// climb hit-test in dk-core.ladderAt (easier to mount, reads as a real ladder).
|
|
66
67
|
for (const l of ladders) {
|
|
67
|
-
for (let y = l.yTop; y <= l.yBottom; y++) put(l.col, y, glyph.ladder, "ladder");
|
|
68
|
+
for (let y = l.yTop; y <= l.yBottom; y++) { put(l.col, y, glyph.ladder, "ladder"); put(l.col + 1, y, glyph.ladder, "ladder"); }
|
|
68
69
|
}
|
|
69
70
|
// goal + actors
|
|
70
71
|
put(princess.x, princess.y, glyph.princess, "princess");
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* Shared TUI -> game handoff. Used by the global Shift-P shortcut (app.js, any
|
|
2
|
+
* tab) and the Scan tab. Unmounts Ink (releasing raw mode + stdin), runs the
|
|
3
|
+
* direct-ANSI game loop (which owns its own raw mode + alt screen), then remounts
|
|
4
|
+
* the TUI at the requested tab. A TRUE background scan SUBPROCESS feeds the game
|
|
5
|
+
* HUD and survives the unmount because it's a separate OS process. */
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import os from "node:os";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { track } from "../../telemetry.js";
|
|
10
|
+
import { boardSize, MIN_W, MIN_H } from "./dk-core.js";
|
|
11
|
+
|
|
12
|
+
let launching = false; // module-level re-entrancy guard (one game at a time)
|
|
13
|
+
|
|
14
|
+
/** Does the board fit this terminal? (mirrors the loop's too-small refusal). */
|
|
15
|
+
export function gameFitsTerminal(width, height) {
|
|
16
|
+
const { BOARD_W, BOARD_H } = boardSize(width, height);
|
|
17
|
+
return BOARD_W >= MIN_W && BOARD_H >= MIN_H;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Launch Donkey Kong from inside the running TUI.
|
|
22
|
+
* { dir, width, height, initialView, scanPhase, onError }
|
|
23
|
+
* Returns when the player quits the game and the TUI has been remounted.
|
|
24
|
+
*/
|
|
25
|
+
export async function playGameInTui({ dir, width, height, initialView = "scan", scanPhase = "idle", onError } = {}) {
|
|
26
|
+
if (launching) return;
|
|
27
|
+
launching = true;
|
|
28
|
+
track("game_opened", { game: "donkey_kong", scan_phase: scanPhase });
|
|
29
|
+
try {
|
|
30
|
+
const [{ runGame }, { startScanProcess }, { appController }, { writeDiskScan, loadRegistry }] = await Promise.all([
|
|
31
|
+
import("./loop.js"),
|
|
32
|
+
import("../../sources/scan-process.js"),
|
|
33
|
+
import("../app.js"),
|
|
34
|
+
import("../scan-stream.js"),
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
// (1) Start a TRUE background scan SUBPROCESS over `dir`. It survives the Ink
|
|
38
|
+
// unmount (separate OS process). Pre-fetch + cache the registry so the worker
|
|
39
|
+
// skips the network; on done, persist candidates to the disk cache so the
|
|
40
|
+
// remounted view loads fresh results.
|
|
41
|
+
let cacheFile = null;
|
|
42
|
+
try {
|
|
43
|
+
const snapshot = await loadRegistry().catch(() => null);
|
|
44
|
+
if (snapshot) {
|
|
45
|
+
cacheFile = path.join(os.tmpdir(), `mm-reg-game-${process.pid}.json`);
|
|
46
|
+
fs.writeFileSync(cacheFile, JSON.stringify(snapshot));
|
|
47
|
+
}
|
|
48
|
+
} catch { /* worker fetches itself */ }
|
|
49
|
+
|
|
50
|
+
let handle = null;
|
|
51
|
+
try {
|
|
52
|
+
handle = startScanProcess(
|
|
53
|
+
{ root: dir, registryCachePath: cacheFile || undefined },
|
|
54
|
+
{ onDone: (r) => { try { if (r?.candidates?.length) writeDiskScan(dir, r.candidates); } catch { /* best effort */ } } },
|
|
55
|
+
);
|
|
56
|
+
} catch { handle = null; }
|
|
57
|
+
|
|
58
|
+
// (2) Unmount the Ink tree (releases raw mode + stdin). Let teardown settle.
|
|
59
|
+
appController.unmount();
|
|
60
|
+
await new Promise((r) => setImmediate(r));
|
|
61
|
+
|
|
62
|
+
// (3) Run the game. Its loop owns raw mode / alt screen / the diff renderer
|
|
63
|
+
// and restores the terminal (cooked, main buffer, cursor shown) on exit.
|
|
64
|
+
try {
|
|
65
|
+
await runGame({
|
|
66
|
+
width: process.stdout.columns || width,
|
|
67
|
+
height: process.stdout.rows || height,
|
|
68
|
+
level: 1,
|
|
69
|
+
scanStore: handle,
|
|
70
|
+
});
|
|
71
|
+
} finally {
|
|
72
|
+
try { handle && handle.abort(); } catch { /* ignore */ }
|
|
73
|
+
try { if (cacheFile) fs.unlinkSync(cacheFile); } catch { /* ignore */ }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// (4) Remount the TUI where the player was (so quitting returns you there).
|
|
77
|
+
appController.remount({ initialView, fresh: false });
|
|
78
|
+
} catch (e) {
|
|
79
|
+
if (onError) onError(e); else throw e;
|
|
80
|
+
} finally {
|
|
81
|
+
launching = false;
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/tui/views/scan.js
CHANGED
|
@@ -45,7 +45,8 @@ export const meta = {
|
|
|
45
45
|
{ k: "/", label: "search" },
|
|
46
46
|
{ k: "g", label: "rescan" },
|
|
47
47
|
{ k: "u", label: "upload all" },
|
|
48
|
-
{ k: "P", label: "play" }, //
|
|
48
|
+
{ k: "P", label: "play" }, // launches anytime (also the global Shift-P)
|
|
49
|
+
{ k: "N", label: "new project" },
|
|
49
50
|
],
|
|
50
51
|
};
|
|
51
52
|
|
|
@@ -79,68 +80,16 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
79
80
|
if (launchingRef.current) return;
|
|
80
81
|
if (!canPlay) return ui?.showToast?.("terminal too small for the game — resize a bit", "#d97706");
|
|
81
82
|
launchingRef.current = true;
|
|
82
|
-
track("game_opened", { game: "donkey_kong", scan_phase: scan.phase });
|
|
83
83
|
// Test seam: a caller may inject the launcher so the Ink↔game handoff (which
|
|
84
84
|
// grabs the real terminal + spawns a subprocess) can be asserted without
|
|
85
|
-
// either. Production passes nothing → the
|
|
86
|
-
if (launchGameImpl) {
|
|
87
|
-
try { await launchGameImpl({ dir, width, height }); }
|
|
88
|
-
finally { launchingRef.current = false; }
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
85
|
+
// either. Production passes nothing → the shared launcher runs.
|
|
91
86
|
try {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
// (1) Start a TRUE background scan SUBPROCESS over the same dir. It survives
|
|
100
|
-
// the Ink unmount (separate OS process). Pre-fetch + cache the registry so
|
|
101
|
-
// the worker skips the network; on done, persist candidates to the disk
|
|
102
|
-
// cache so the remounted Scan view loads the fresh results.
|
|
103
|
-
let cacheFile = null;
|
|
104
|
-
try {
|
|
105
|
-
const snapshot = await loadRegistry().catch(() => null);
|
|
106
|
-
if (snapshot) {
|
|
107
|
-
cacheFile = path.join(os.tmpdir(), `mm-reg-game-${process.pid}.json`);
|
|
108
|
-
fs.writeFileSync(cacheFile, JSON.stringify(snapshot));
|
|
109
|
-
}
|
|
110
|
-
} catch { /* worker fetches itself */ }
|
|
111
|
-
|
|
112
|
-
let handle = null;
|
|
113
|
-
try {
|
|
114
|
-
handle = startScanProcess(
|
|
115
|
-
{ root: dir, registryCachePath: cacheFile || undefined },
|
|
116
|
-
{ onDone: (r) => { try { if (r?.candidates?.length) writeDiskScan(dir, r.candidates); } catch { /* best effort */ } } },
|
|
117
|
-
);
|
|
118
|
-
} catch { handle = null; }
|
|
119
|
-
|
|
120
|
-
// (2) Unmount the Ink tree (releases raw mode + stdin). Wait a tick so the
|
|
121
|
-
// teardown fully settles before the game grabs raw mode + alt screen.
|
|
122
|
-
appController.unmount();
|
|
123
|
-
await new Promise((r) => setImmediate(r));
|
|
124
|
-
|
|
125
|
-
// (3) Run the game. Its loop owns raw mode / alt screen / the diff renderer
|
|
126
|
-
// and restores the terminal (cooked, main buffer, cursor shown) on exit.
|
|
127
|
-
try {
|
|
128
|
-
await runGame({
|
|
129
|
-
width: process.stdout.columns || width,
|
|
130
|
-
height: process.stdout.rows || height,
|
|
131
|
-
level: 1,
|
|
132
|
-
scanStore: handle,
|
|
133
|
-
});
|
|
134
|
-
} finally {
|
|
135
|
-
try { handle && handle.abort(); } catch { /* ignore */ }
|
|
136
|
-
try { if (cacheFile) fs.unlinkSync(cacheFile); } catch { /* ignore */ }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// (4) Remount the TUI at the Scan tab. fresh:false → the view loads the
|
|
140
|
-
// (now-updated) disk scan cache on its fast path.
|
|
141
|
-
appController.remount({ initialView: "scan", fresh: false });
|
|
142
|
-
} catch (e) {
|
|
143
|
-
ui?.showToast?.(`couldn't start the game: ${e?.message || e}`, "#dc2626");
|
|
87
|
+
if (launchGameImpl) { await launchGameImpl({ dir, width, height }); return; }
|
|
88
|
+
const { playGameInTui } = await import("../game/launch.js");
|
|
89
|
+
await playGameInTui({
|
|
90
|
+
dir, width, height, initialView: "scan", scanPhase: scan.phase,
|
|
91
|
+
onError: (e) => ui?.showToast?.(`couldn't start the game: ${e?.message || e}`, "#dc2626"),
|
|
92
|
+
});
|
|
144
93
|
} finally {
|
|
145
94
|
launchingRef.current = false;
|
|
146
95
|
}
|
|
@@ -322,13 +271,10 @@ export function ScanView({ client, dir, ui, active, width = 78, height = 14, fre
|
|
|
322
271
|
if (input === "a") return setItems((its) => its.map((it) => ({ ...it, selected: true })));
|
|
323
272
|
if (input === "x") return setItems((its) => its.map((it) => ({ ...it, selected: false })));
|
|
324
273
|
if (input === "p") return setProjectIdx((i) => (i + 1) % projOptions.length);
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (running) { launchGame(); return; }
|
|
330
|
-
return ui.askPrompt("New project", { onSubmit: createProject });
|
|
331
|
-
}
|
|
274
|
+
// P always launches Donkey Kong (any time, not just mid-scan) to match the
|
|
275
|
+
// global Shift-P shortcut. "New project" moved to N.
|
|
276
|
+
if (input === "P") { launchGame(); return; }
|
|
277
|
+
if (input === "N") return ui.askPrompt("New project", { onSubmit: createProject });
|
|
332
278
|
if (input === "g") return scan.reload();
|
|
333
279
|
if (input === "u") return upload();
|
|
334
280
|
if (input === "l") {
|