nou-cli 0.5.4 → 0.5.10
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/README.md +18 -7
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli.js +69 -0
- package/dist/src/config/index.js +20 -0
- package/dist/src/conversations/external.js +35 -13
- package/dist/src/diagnostics/providers.js +23 -2
- package/dist/src/engines/codex.js +28 -1
- package/dist/src/games/index.js +220 -30
- package/dist/src/guide/index.js +75 -0
- package/dist/src/ui/App.js +281 -36
- package/dist/src/ui/Chat.js +63 -11
- package/dist/src/ui/CodeView.js +71 -20
- package/dist/src/ui/Editor.js +1 -1
- package/dist/src/ui/FileTree.js +3 -1
- package/dist/src/ui/GamePanel.js +42 -14
- package/dist/src/ui/Header.js +32 -4
- package/dist/src/ui/Pane.js +2 -2
- package/dist/src/ui/ProblemsView.js +3 -1
- package/dist/src/ui/QuickOpen.js +8 -3
- package/dist/src/ui/Settings.js +21 -7
- package/dist/src/ui/Terminal.js +7 -1
- package/dist/src/ui/background.js +58 -0
- package/dist/src/ui/commands.js +49 -7
- package/dist/src/ui/focus.js +59 -0
- package/dist/src/ui/redraw.js +16 -0
- package/dist/src/ui/render-options.js +3 -0
- package/dist/src/ui/text.js +45 -0
- package/dist/src/ui/theme.js +59 -13
- package/dist/src/workspaces/client.js +69 -0
- package/dist/src/workspaces/host.js +144 -0
- package/package.json +2 -1
package/dist/src/games/index.js
CHANGED
|
@@ -12,8 +12,13 @@ function rng(seed) {
|
|
|
12
12
|
return (s >>> 0) / 4294967296;
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
|
-
const blank = (w, h) => Array.from({ length: h }, () => Array(w).fill(" "));
|
|
15
|
+
const blank = (w, h) => Array.from({ length: Math.max(0, h) }, () => Array(Math.max(0, w)).fill(" "));
|
|
16
16
|
const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
|
|
17
|
+
/** Draws a character if (x, y) is on the grid: games never write outside it, whatever its size. */
|
|
18
|
+
const put = (grid, x, y, ch) => {
|
|
19
|
+
if (grid[y]?.[x] !== undefined)
|
|
20
|
+
grid[y][x] = ch;
|
|
21
|
+
};
|
|
17
22
|
const PADDLE = 3;
|
|
18
23
|
const PONG_WIN = 7;
|
|
19
24
|
function serve(s, toward) {
|
|
@@ -26,8 +31,11 @@ export const pong = {
|
|
|
26
31
|
blurb: "The classic: you on the left, the computer on the right. First to 7.",
|
|
27
32
|
controls: "↑↓ or w/s move",
|
|
28
33
|
tickMs: 50,
|
|
34
|
+
minWidth: 16,
|
|
35
|
+
minHeight: 6,
|
|
29
36
|
colors: { "█": "#61afef", "●": "#e5c07b", "┊": "#5c6370" },
|
|
30
37
|
create(w, h, seed = Date.now()) {
|
|
38
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
31
39
|
const s = { w, h, you: Math.floor((h - PADDLE) / 2), them: Math.floor((h - PADDLE) / 2), bx: 0, by: 0, vx: 0, vy: 0, scores: [0, 0], random: rng(seed) };
|
|
32
40
|
return serve(s, -1);
|
|
33
41
|
},
|
|
@@ -65,18 +73,16 @@ export const pong = {
|
|
|
65
73
|
view(s) {
|
|
66
74
|
const grid = blank(s.w, s.h);
|
|
67
75
|
for (let y = 0; y < s.h; y += 2)
|
|
68
|
-
grid
|
|
76
|
+
put(grid, Math.floor(s.w / 2), y, "┊");
|
|
69
77
|
for (let k = 0; k < PADDLE; k++) {
|
|
70
|
-
grid
|
|
71
|
-
grid
|
|
78
|
+
put(grid, 1, s.you + k, "█");
|
|
79
|
+
put(grid, s.w - 2, Math.round(s.them) + k, "█");
|
|
72
80
|
}
|
|
73
|
-
|
|
74
|
-
const by = Math.round(s.by);
|
|
75
|
-
if (grid[by]?.[bx] !== undefined)
|
|
76
|
-
grid[by][bx] = "●";
|
|
81
|
+
put(grid, Math.round(s.bx), Math.round(s.by), "●");
|
|
77
82
|
const [a, b] = s.scores;
|
|
78
83
|
return {
|
|
79
84
|
lines: grid.map((r) => r.join("")),
|
|
85
|
+
score: a,
|
|
80
86
|
status: `you ${a} · ${b} computer ↑↓ move`,
|
|
81
87
|
...(a >= PONG_WIN ? { over: "You win! ✦" } : b >= PONG_WIN ? { over: "The computer wins." } : {}),
|
|
82
88
|
};
|
|
@@ -98,8 +104,13 @@ export const snake = {
|
|
|
98
104
|
blurb: "Eat, grow, don't bite yourself or the walls.",
|
|
99
105
|
controls: "arrows or wasd steer",
|
|
100
106
|
tickMs: 110,
|
|
107
|
+
minWidth: 10,
|
|
108
|
+
minHeight: 5,
|
|
109
|
+
// A little faster with every few eaten, down to a floor.
|
|
110
|
+
speed: (s) => Math.max(55, 110 - Math.floor(s.score / 3) * 8),
|
|
101
111
|
colors: { "█": "#98c379", "▓": "#7ec16e", "◆": "#e06c75" },
|
|
102
112
|
create(w, h, seed = Date.now()) {
|
|
113
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
103
114
|
const y = Math.floor(h / 2);
|
|
104
115
|
const s = { w, h, body: [{ x: 4, y }, { x: 3, y }, { x: 2, y }], dir: "right", food: { x: 0, y: 0 }, score: 0, dead: false, random: rng(seed) };
|
|
105
116
|
return { ...s, food: placeFood(s) };
|
|
@@ -110,7 +121,7 @@ export const snake = {
|
|
|
110
121
|
let dir = s.dir;
|
|
111
122
|
// One turn per tick (the first that isn't straight back), so quick double taps don't fold it onto itself.
|
|
112
123
|
for (const i of inputs) {
|
|
113
|
-
if (i !== OPPOSITE[dir] && i !== dir) {
|
|
124
|
+
if (i !== "action" && i !== OPPOSITE[dir] && i !== dir) {
|
|
114
125
|
dir = i;
|
|
115
126
|
break;
|
|
116
127
|
}
|
|
@@ -126,36 +137,42 @@ export const snake = {
|
|
|
126
137
|
},
|
|
127
138
|
view(s) {
|
|
128
139
|
const grid = blank(s.w, s.h);
|
|
129
|
-
grid
|
|
130
|
-
s.body.forEach((b, i) =>
|
|
131
|
-
|
|
132
|
-
grid[b.y][b.x] = i === 0 ? "█" : "▓";
|
|
133
|
-
});
|
|
134
|
-
return { lines: grid.map((r) => r.join("")), status: `length ${s.body.length} · score ${s.score}`, ...(s.dead ? { over: `Game over: ${s.score} eaten.` } : {}) };
|
|
140
|
+
put(grid, s.food.x, s.food.y, "◆");
|
|
141
|
+
s.body.forEach((b, i) => put(grid, b.x, b.y, i === 0 ? "█" : "▓"));
|
|
142
|
+
return { lines: grid.map((r) => r.join("")), score: s.score, status: `length ${s.body.length} · score ${s.score}`, ...(s.dead ? { over: `Game over: ${s.score} eaten.` } : {}) };
|
|
135
143
|
},
|
|
136
144
|
};
|
|
137
145
|
const PAD = 7;
|
|
138
146
|
const BRICK_ROWS = 4;
|
|
139
147
|
function launch(s) {
|
|
140
|
-
|
|
148
|
+
// Each level the ball starts a little faster.
|
|
149
|
+
const pace = 0.6 * (1 + (s.level - 1) * 0.12);
|
|
150
|
+
return { ...s, bx: s.paddle + PAD / 2, by: s.h - 3, vx: (s.random() < 0.5 ? -1 : 1) * pace, vy: -pace };
|
|
141
151
|
}
|
|
152
|
+
const wall = (w) => Array.from({ length: BRICK_ROWS }, () => Array(Math.floor(w / 3)).fill(true));
|
|
142
153
|
export const breakout = {
|
|
143
154
|
id: "breakout",
|
|
144
155
|
title: "Breakout",
|
|
145
|
-
blurb: "Knock out every brick with the ball. Three lives.",
|
|
156
|
+
blurb: "Knock out every brick with the ball; each wall you clear brings a faster one. Three lives.",
|
|
146
157
|
controls: "←→ or a/d move",
|
|
147
158
|
tickMs: 45,
|
|
159
|
+
minWidth: 18,
|
|
160
|
+
minHeight: 9,
|
|
148
161
|
colors: { "▀": "#e06c75", "▄": "#d19a66", "▬": "#61afef", "●": "#e5c07b" },
|
|
149
162
|
create(w, h, seed = Date.now()) {
|
|
163
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
150
164
|
const s = {
|
|
151
165
|
w, h, paddle: Math.floor((w - PAD) / 2), bx: 0, by: 0, vx: 0, vy: 0,
|
|
152
|
-
bricks:
|
|
166
|
+
bricks: wall(w), lives: 3, score: 0, level: 1, random: rng(seed),
|
|
153
167
|
};
|
|
154
168
|
return launch(s);
|
|
155
169
|
},
|
|
156
170
|
tick(s, inputs) {
|
|
157
|
-
if (s.lives <= 0
|
|
171
|
+
if (s.lives <= 0)
|
|
158
172
|
return s;
|
|
173
|
+
// A cleared wall: the next level, a little faster.
|
|
174
|
+
if (s.bricks.every((r) => r.every((b) => !b)))
|
|
175
|
+
return launch({ ...s, bricks: wall(s.w), level: s.level + 1 });
|
|
159
176
|
let paddle = s.paddle;
|
|
160
177
|
for (const i of inputs)
|
|
161
178
|
paddle += i === "left" ? -2 : i === "right" ? 2 : 0;
|
|
@@ -195,23 +212,196 @@ export const breakout = {
|
|
|
195
212
|
s.bricks.forEach((r, y) => r.forEach((alive, x) => {
|
|
196
213
|
if (alive)
|
|
197
214
|
for (let k = 0; k < 3; k++)
|
|
198
|
-
|
|
199
|
-
grid[y + 1][x * 3 + k] = y % 2 ? "▄" : "▀";
|
|
215
|
+
put(grid, x * 3 + k, y + 1, y % 2 ? "▄" : "▀");
|
|
200
216
|
}));
|
|
201
217
|
for (let k = 0; k < PAD; k++)
|
|
202
|
-
grid
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (s.lives > 0 && grid[by]?.[bx] !== undefined)
|
|
206
|
-
grid[by][bx] = "●";
|
|
207
|
-
const cleared = s.bricks.every((r) => r.every((b) => !b));
|
|
218
|
+
put(grid, s.paddle + k, s.h - 1, "▬");
|
|
219
|
+
if (s.lives > 0)
|
|
220
|
+
put(grid, Math.round(s.bx), Math.round(s.by), "●");
|
|
208
221
|
return {
|
|
209
222
|
lines: grid.map((r) => r.join("")),
|
|
210
|
-
|
|
211
|
-
|
|
223
|
+
score: s.score,
|
|
224
|
+
status: `level ${s.level} · score ${s.score} · lives ${"♥".repeat(Math.max(0, s.lives))}`,
|
|
225
|
+
...(s.lives <= 0 ? { over: `Game over: ${s.score} points, level ${s.level}.` } : {}),
|
|
212
226
|
};
|
|
213
227
|
},
|
|
214
228
|
};
|
|
229
|
+
/** Rocks fall; slide along the bottom to dodge them and catch the stars. It gets busier as you last. */
|
|
230
|
+
export const dodge = {
|
|
231
|
+
id: "dodge",
|
|
232
|
+
title: "Dodge",
|
|
233
|
+
blurb: "Rocks fall faster and thicker the longer you last. Catch ★ for points.",
|
|
234
|
+
controls: "←→ or a/d move",
|
|
235
|
+
tickMs: 90,
|
|
236
|
+
minWidth: 12,
|
|
237
|
+
minHeight: 6,
|
|
238
|
+
speed: (s) => Math.max(45, 90 - Math.floor(s.ticks / 60) * 5),
|
|
239
|
+
colors: { "▲": "#61afef", "●": "#d19a66", "★": "#e5c07b" },
|
|
240
|
+
create(w, h, seed = Date.now()) {
|
|
241
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
242
|
+
return { w, h, x: Math.floor(w / 2), rocks: [], stars: [], score: 0, ticks: 0, dead: false, random: rng(seed) };
|
|
243
|
+
},
|
|
244
|
+
tick(s, inputs) {
|
|
245
|
+
if (s.dead)
|
|
246
|
+
return s;
|
|
247
|
+
let x = s.x;
|
|
248
|
+
for (const i of inputs)
|
|
249
|
+
x += i === "left" ? -1 : i === "right" ? 1 : 0;
|
|
250
|
+
x = clamp(x, 0, s.w - 1);
|
|
251
|
+
const fall = (p) => ({ x: p.x, y: p.y + 1 });
|
|
252
|
+
const rocks = s.rocks.map(fall).filter((p) => p.y < s.h);
|
|
253
|
+
let stars = s.stars.map(fall).filter((p) => p.y < s.h);
|
|
254
|
+
// More rocks per row as the game goes on.
|
|
255
|
+
const chance = Math.min(0.55, 0.18 + s.ticks / 900);
|
|
256
|
+
if (s.random() < chance)
|
|
257
|
+
rocks.push({ x: Math.floor(s.random() * s.w), y: 0 });
|
|
258
|
+
if (s.random() < 0.05)
|
|
259
|
+
stars.push({ x: Math.floor(s.random() * s.w), y: 0 });
|
|
260
|
+
const at = (p) => p.x === x && p.y === s.h - 1;
|
|
261
|
+
let score = s.score + (s.ticks % 10 === 9 ? 1 : 0);
|
|
262
|
+
if (stars.some(at)) {
|
|
263
|
+
score += 10;
|
|
264
|
+
stars = stars.filter((p) => !at(p));
|
|
265
|
+
}
|
|
266
|
+
const dead = rocks.some(at);
|
|
267
|
+
return { ...s, x, rocks, stars, score, ticks: s.ticks + 1, dead };
|
|
268
|
+
},
|
|
269
|
+
view(s) {
|
|
270
|
+
const grid = blank(s.w, s.h);
|
|
271
|
+
for (const r of s.rocks)
|
|
272
|
+
put(grid, r.x, r.y, "●");
|
|
273
|
+
for (const r of s.stars)
|
|
274
|
+
put(grid, r.x, r.y, "★");
|
|
275
|
+
put(grid, s.x, s.h - 1, "▲");
|
|
276
|
+
return { lines: grid.map((r) => r.join("")), score: s.score, status: `score ${s.score}`, ...(s.dead ? { over: `Hit! ${s.score} points.` } : {}) };
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
const HOP_X = 3;
|
|
280
|
+
/** Run along the ground and hop the walls that come at you; they come faster as you go. */
|
|
281
|
+
export const hop = {
|
|
282
|
+
id: "hop",
|
|
283
|
+
title: "Hop",
|
|
284
|
+
blurb: "Run and hop over the walls coming at you. One hit and it's over.",
|
|
285
|
+
controls: "↑ or Enter hops",
|
|
286
|
+
tickMs: 70,
|
|
287
|
+
minWidth: 20,
|
|
288
|
+
minHeight: 6,
|
|
289
|
+
speed: (s) => Math.max(35, 70 - Math.floor(s.score / 5) * 4),
|
|
290
|
+
colors: { "◉": "#98c379", "█": "#c678dd", "▔": "#5c6370" },
|
|
291
|
+
create(w, h, seed = Date.now()) {
|
|
292
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
293
|
+
return { w, h, y: 0, vy: 0, walls: [], score: 0, ticks: 0, dead: false, random: rng(seed) };
|
|
294
|
+
},
|
|
295
|
+
tick(s, inputs) {
|
|
296
|
+
if (s.dead)
|
|
297
|
+
return s;
|
|
298
|
+
// Height above the ground; a hop goes up and comes back down.
|
|
299
|
+
let { y, vy } = s;
|
|
300
|
+
if (y === 0 && inputs.some((i) => i === "up" || i === "action"))
|
|
301
|
+
vy = 1.15;
|
|
302
|
+
y = Math.max(0, y + vy);
|
|
303
|
+
vy = y === 0 ? 0 : vy - 0.22;
|
|
304
|
+
const walls = s.walls.map((wl) => ({ ...wl, x: wl.x - 1 })).filter((wl) => wl.x >= 0);
|
|
305
|
+
const passed = s.walls.length - walls.length;
|
|
306
|
+
const last = walls[walls.length - 1];
|
|
307
|
+
// A new wall, far enough from the last that every gap can be cleared.
|
|
308
|
+
if ((!last || last.x < s.w - 9) && s.random() < 0.12)
|
|
309
|
+
walls.push({ x: s.w - 1, tall: 1 + Math.floor(s.random() * Math.min(2, s.h - 3)) });
|
|
310
|
+
const dead = walls.some((wl) => wl.x === HOP_X && Math.round(y) < wl.tall);
|
|
311
|
+
return { ...s, y, vy, walls, score: s.score + passed, ticks: s.ticks + 1, dead };
|
|
312
|
+
},
|
|
313
|
+
view(s) {
|
|
314
|
+
const grid = blank(s.w, s.h);
|
|
315
|
+
const ground = s.h - 1;
|
|
316
|
+
for (let x = 0; x < s.w; x++)
|
|
317
|
+
put(grid, x, ground, "▔");
|
|
318
|
+
for (const wl of s.walls)
|
|
319
|
+
for (let k = 1; k <= wl.tall; k++)
|
|
320
|
+
put(grid, wl.x, ground - k, "█");
|
|
321
|
+
put(grid, HOP_X, ground - 1 - Math.round(s.y), "◉");
|
|
322
|
+
return { lines: grid.map((r) => r.join("")), score: s.score, status: `walls hopped ${s.score}`, ...(s.dead ? { over: `Bonk! ${s.score} hopped.` } : {}) };
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
const SYMBOLS = ["◆", "●", "▲", "■", "★", "♥", "♣", "♠", "☀", "☂", "♪", "✿"];
|
|
326
|
+
/** Cards face down: flip two at a time and find every matching pair in as few moves as you can. */
|
|
327
|
+
export const pairs = {
|
|
328
|
+
id: "pairs",
|
|
329
|
+
title: "Pairs",
|
|
330
|
+
blurb: "Flip two cards at a time; find every matching pair in as few moves as you can.",
|
|
331
|
+
controls: "arrows move · Enter flips",
|
|
332
|
+
tickMs: 100,
|
|
333
|
+
minWidth: 16,
|
|
334
|
+
minHeight: 5,
|
|
335
|
+
colors: { "◆": "#e06c75", "●": "#e5c07b", "▲": "#61afef", "■": "#98c379", "★": "#e5c07b", "♥": "#e06c75", "♣": "#98c379", "♠": "#c678dd", "☀": "#d19a66", "☂": "#56b6c2", "♪": "#c678dd", "✿": "#56b6c2", "▒": "#5c6370", "▸": "#e5c07b", "◂": "#e5c07b" },
|
|
336
|
+
create(w, h, seed = Date.now()) {
|
|
337
|
+
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
338
|
+
// As many cards as fit (each 4 wide, rows 2 apart), an even number, up to every symbol twice.
|
|
339
|
+
const cols = Math.max(2, Math.min(6, Math.floor(w / 4)));
|
|
340
|
+
const rows = Math.max(2, Math.min(4, Math.floor((h + 1) / 2)));
|
|
341
|
+
const count = Math.min(SYMBOLS.length * 2, cols * rows - ((cols * rows) % 2));
|
|
342
|
+
const random = rng(seed);
|
|
343
|
+
const deck = SYMBOLS.slice(0, count / 2).flatMap((sym) => [sym, sym]);
|
|
344
|
+
for (let i = deck.length - 1; i > 0; i--) {
|
|
345
|
+
const j = Math.floor(random() * (i + 1));
|
|
346
|
+
[deck[i], deck[j]] = [deck[j], deck[i]];
|
|
347
|
+
}
|
|
348
|
+
return { cols, rows, w, h, cards: deck, found: deck.map(() => false), open: [], cursor: 0, moves: 0, wait: 0 };
|
|
349
|
+
},
|
|
350
|
+
tick(s, inputs) {
|
|
351
|
+
if (s.found.every(Boolean))
|
|
352
|
+
return s;
|
|
353
|
+
let { cursor, open, found, moves, wait } = s;
|
|
354
|
+
// Two face-up cards that don't match stay shown a moment, then turn back over.
|
|
355
|
+
if (open.length === 2 && wait > 0) {
|
|
356
|
+
wait--;
|
|
357
|
+
if (wait === 0)
|
|
358
|
+
open = [];
|
|
359
|
+
return { ...s, open, wait };
|
|
360
|
+
}
|
|
361
|
+
const n = s.cards.length;
|
|
362
|
+
for (const i of inputs) {
|
|
363
|
+
if (i === "left")
|
|
364
|
+
cursor = (cursor + n - 1) % n;
|
|
365
|
+
else if (i === "right")
|
|
366
|
+
cursor = (cursor + 1) % n;
|
|
367
|
+
else if (i === "up")
|
|
368
|
+
cursor = cursor - s.cols >= 0 ? cursor - s.cols : cursor;
|
|
369
|
+
else if (i === "down")
|
|
370
|
+
cursor = cursor + s.cols < n ? cursor + s.cols : cursor;
|
|
371
|
+
else if (i === "action" && !found[cursor] && !open.includes(cursor) && open.length < 2) {
|
|
372
|
+
open = [...open, cursor];
|
|
373
|
+
if (open.length === 2) {
|
|
374
|
+
moves++;
|
|
375
|
+
if (s.cards[open[0]] === s.cards[open[1]]) {
|
|
376
|
+
found = found.map((f, k) => f || open.includes(k));
|
|
377
|
+
open = [];
|
|
378
|
+
}
|
|
379
|
+
else
|
|
380
|
+
wait = 8;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
return { ...s, cursor, open, found, moves, wait };
|
|
385
|
+
},
|
|
386
|
+
view(s) {
|
|
387
|
+
const grid = blank(s.w, s.h);
|
|
388
|
+
s.cards.forEach((card, i) => {
|
|
389
|
+
const x = (i % s.cols) * 4;
|
|
390
|
+
const y = Math.floor(i / s.cols) * 2;
|
|
391
|
+
const shown = s.found[i] || s.open.includes(i);
|
|
392
|
+
if (i === s.cursor) {
|
|
393
|
+
put(grid, x, y, "▸");
|
|
394
|
+
put(grid, x + 2, y, "◂");
|
|
395
|
+
}
|
|
396
|
+
put(grid, x + 1, y, s.found[i] ? card : shown ? card : "▒");
|
|
397
|
+
});
|
|
398
|
+
const done = s.found.every(Boolean);
|
|
399
|
+
const pairsLeft = s.found.filter((f) => !f).length / 2;
|
|
400
|
+
// Fewer moves is better: the score counts what you saved.
|
|
401
|
+
const score = Math.max(0, s.cards.length * 2 - s.moves);
|
|
402
|
+
return { lines: grid.map((r) => r.join("")), score, status: `moves ${s.moves} · ${pairsLeft} pairs left`, ...(done ? { over: `All found in ${s.moves} moves ✦` } : {}) };
|
|
403
|
+
},
|
|
404
|
+
};
|
|
215
405
|
/** Every game Nou has, in the order settings lists them. */
|
|
216
|
-
export const GAMES = [pong, snake, breakout];
|
|
406
|
+
export const GAMES = [pong, snake, breakout, dodge, hop, pairs];
|
|
217
407
|
export const gameById = (id) => GAMES.find((g) => g.id === id);
|
package/dist/src/guide/index.js
CHANGED
|
@@ -98,3 +98,78 @@ export function guideProgress(typed, expected) {
|
|
|
98
98
|
}
|
|
99
99
|
return j;
|
|
100
100
|
}
|
|
101
|
+
export const SHOW_PLACES = "show_places";
|
|
102
|
+
/** Stops as the model gave them, kept when usable: a file in the project and a line. */
|
|
103
|
+
export function tidyStops(raw) {
|
|
104
|
+
return (Array.isArray(raw) ? raw : [])
|
|
105
|
+
.filter((s) => typeof s?.file === "string" && s.file.trim() && Number(s.line) >= 1)
|
|
106
|
+
.map((s) => {
|
|
107
|
+
const line = Math.floor(Number(s.line));
|
|
108
|
+
const end = Math.max(line, Math.floor(Number(s.end_line ?? s.end ?? line)) || line);
|
|
109
|
+
return { file: path.relative(process.cwd(), path.resolve(s.file)).split(path.sep).join("/"), line, end, note: String(s.note ?? s.why ?? "").trim() };
|
|
110
|
+
})
|
|
111
|
+
.filter((s) => !s.file.startsWith(".."))
|
|
112
|
+
.slice(0, 12);
|
|
113
|
+
}
|
|
114
|
+
/** The tool Nou's own engine points at places with. `onStops` gets them. */
|
|
115
|
+
export function showPlacesTool(onStops) {
|
|
116
|
+
return {
|
|
117
|
+
definition: {
|
|
118
|
+
name: SHOW_PLACES,
|
|
119
|
+
description: "When you explain how something works or where something happens in this project, point at the exact places so the user sees them: Nou opens each one in their code view, highlighted, in the order given. Call it once, after you've read the code, with the parts that matter (usually 1-6).",
|
|
120
|
+
parameters: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
places: {
|
|
124
|
+
type: "array",
|
|
125
|
+
items: {
|
|
126
|
+
type: "object",
|
|
127
|
+
properties: {
|
|
128
|
+
file: { type: "string", description: "Project-relative path." },
|
|
129
|
+
line: { type: "number", description: "First line of the part (1-based)." },
|
|
130
|
+
end_line: { type: "number", description: "Last line of the part." },
|
|
131
|
+
note: { type: "string", description: "One or two plain sentences: what this part does, in terms of the explanation." },
|
|
132
|
+
},
|
|
133
|
+
required: ["file", "line", "end_line", "note"],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
required: ["places"],
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
readOnly: true,
|
|
141
|
+
source: "nou",
|
|
142
|
+
run: (args) => {
|
|
143
|
+
const stops = tidyStops(args.places);
|
|
144
|
+
if (!stops.length)
|
|
145
|
+
return "No usable places: each needs a project file and a line. Call show_places again.";
|
|
146
|
+
onStops(stops);
|
|
147
|
+
return `Shown to the user: ${stops.length} place${stops.length > 1 ? "s" : ""}, opening the first. Now explain in the order of the places, naming each as path:line.`;
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** The fenced block other engines (which can't call Nou's tools) list their places in. */
|
|
152
|
+
const SHOW_BLOCK = /```nou-show\s*\n([\s\S]*?)\n```/;
|
|
153
|
+
/**
|
|
154
|
+
* For Claude Code, Codex and pi, which can't call show_places: how to point at places, added to what they
|
|
155
|
+
* know about the project, so explaining "where does X happen" opens the files on any engine.
|
|
156
|
+
*/
|
|
157
|
+
export const SHOW_PLACES_NOTE = "When you explain how something in this project works or where something happens, end your reply with the exact places, in the order to read them, as a JSON array in a fenced block marked nou-show. The user's editor opens each one highlighted:\n```nou-show\n[{\"file\": \"src/auth/session.ts\", \"line\": 40, \"end_line\": 58, \"note\": \"Checks the token and refuses expired ones.\"}]\n```\nOnly for places in this project you've actually read; leave it out when nothing needs showing.";
|
|
158
|
+
/** The message /showme sends: explain, and point at the places. */
|
|
159
|
+
export function explainPrompt(question, withTool) {
|
|
160
|
+
const how = withTool ? `call ${SHOW_PLACES} with them` : "list them in a nou-show block at the end, as described";
|
|
161
|
+
return `[Show me] ${question}\n\nExplain this to me and show me where it happens in the code. Don't change any files. Read what you need, find the exact places (file and line range) that matter, ${how}, in the order to read them, and explain in that order, naming each as path:line.`;
|
|
162
|
+
}
|
|
163
|
+
/** Places from a reply's ```nou-show block, and the reply without it; null when there's none. */
|
|
164
|
+
export function stopsFromReply(text) {
|
|
165
|
+
const m = SHOW_BLOCK.exec(text);
|
|
166
|
+
if (!m)
|
|
167
|
+
return null;
|
|
168
|
+
try {
|
|
169
|
+
const stops = tidyStops(JSON.parse(m[1]));
|
|
170
|
+
return { stops, rest: text.replace(m[0], "").trim() };
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
}
|