nou-cli 0.5.4 → 0.5.6
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 +7 -2
- package/dist/src/cli.js +33 -0
- package/dist/src/config/index.js +1 -0
- 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/ui/App.js +64 -14
- package/dist/src/ui/Chat.js +4 -1
- package/dist/src/ui/CodeView.js +70 -19
- package/dist/src/ui/FileTree.js +3 -1
- package/dist/src/ui/GamePanel.js +41 -13
- package/dist/src/ui/Header.js +4 -1
- package/dist/src/ui/ProblemsView.js +3 -1
- package/dist/src/ui/Settings.js +9 -2
- package/dist/src/ui/commands.js +5 -2
- 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/package.json +1 -1
package/dist/src/ui/commands.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Commands that only change what's on screen (or handle a busy Nou themselves): they run right away
|
|
3
3
|
* while Nou works instead of waiting in the queue behind it.
|
|
4
4
|
*/
|
|
5
|
-
const INSTANT = new Set(["new", "tab", "close", "terminal", "games", "open", "settings", "auto", "diff", "sessions", "history", "problems", "copy", "help", "version", "cancel"]);
|
|
5
|
+
const INSTANT = new Set(["fold", "unfold", "redraw", "new", "tab", "close", "terminal", "games", "open", "settings", "auto", "diff", "sessions", "history", "problems", "copy", "help", "version", "cancel"]);
|
|
6
6
|
/** Whether a typed command runs right away while Nou works (see INSTANT); `/showme` counts only to look or step. */
|
|
7
7
|
export function runsWhileBusy(text) {
|
|
8
8
|
const [name, ...rest] = text.slice(1).trim().split(/\s+/);
|
|
@@ -33,12 +33,15 @@ export const SLASH_COMMANDS = [
|
|
|
33
33
|
{ name: "new", description: "a new conversation tab: another agent, working alongside this one" },
|
|
34
34
|
{ name: "tab", args: "[n]", description: "list the conversation tabs, or switch to tab n" },
|
|
35
35
|
{ name: "close", description: "close this conversation tab (it stays saved for /resume)" },
|
|
36
|
+
{ name: "fold", description: "fold every function and block in the open file to its first line (z toggles)" },
|
|
37
|
+
{ name: "unfold", description: "open every fold in the open file" },
|
|
38
|
+
{ name: "redraw", description: "draw the whole screen again (Ctrl+L), if it ever looks shifted or clicks land a row off" },
|
|
36
39
|
{ name: "terminal", description: "a terminal under the code, like VS Code's (or press `)" },
|
|
37
40
|
{ name: "resume", args: "[number|last]", description: "pick up an earlier conversation in this folder (any engine)" },
|
|
38
41
|
{ name: "pull", args: "<model>", description: "download an Ollama model" },
|
|
39
42
|
{ name: "plugins", args: "[add|new <name>]", description: "your plugins and the starter ones" },
|
|
40
43
|
{ name: "mcp", args: "[add|remove|import]", description: "MCP servers: tools from other programs (/mcp add browser for a browser)" },
|
|
41
|
-
{ name: "games", description: "open or close the Games panel (Pong, Snake, Breakout)" },
|
|
44
|
+
{ name: "games", description: "open or close the Games panel (Pong, Snake, Breakout, Dodge, Hop, Pairs)" },
|
|
42
45
|
{ name: "version", description: "which version of Nou this is" },
|
|
43
46
|
{ name: "help", description: "all commands and keys" },
|
|
44
47
|
];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { useStdin } from "ink";
|
|
3
|
+
/**
|
|
4
|
+
* Whether Nou's terminal window is the one in front. Terminals that report focus (xterm's mode 1004:
|
|
5
|
+
* iTerm2, Terminal.app, kitty, Ghostty, WezTerm, Windows Terminal, VS Code's, tmux with focus-events on)
|
|
6
|
+
* send ESC [ I and ESC [ O as you switch windows; Nou then stops its spinners and git polling while you're
|
|
7
|
+
* elsewhere, so it doesn't keep the CPU (and the terminal's redraws) busy for something nobody is watching.
|
|
8
|
+
* Terminals that don't report it never send either, and Nou stays "focused" as before.
|
|
9
|
+
*/
|
|
10
|
+
let focused = true;
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
export function setTerminalFocused(next) {
|
|
13
|
+
if (next === focused)
|
|
14
|
+
return;
|
|
15
|
+
focused = next;
|
|
16
|
+
for (const listener of listeners)
|
|
17
|
+
listener();
|
|
18
|
+
}
|
|
19
|
+
export const terminalFocused = () => focused;
|
|
20
|
+
export function onTerminalFocus(listener) {
|
|
21
|
+
listeners.add(listener);
|
|
22
|
+
return () => listeners.delete(listener);
|
|
23
|
+
}
|
|
24
|
+
export function useTerminalFocused() {
|
|
25
|
+
return useSyncExternalStore(onTerminalFocus, terminalFocused, terminalFocused);
|
|
26
|
+
}
|
|
27
|
+
/** The last focus report in a chunk of terminal input, if there is one. Ink ignores these, so they're never typed. */
|
|
28
|
+
export function parseFocus(chunk) {
|
|
29
|
+
const at = Math.max(chunk.lastIndexOf("\x1b[I"), chunk.lastIndexOf("\x1b[O"));
|
|
30
|
+
return at < 0 ? null : chunk[at + 2] === "I";
|
|
31
|
+
}
|
|
32
|
+
const ENABLE = "\x1b[?1004h";
|
|
33
|
+
const DISABLE = "\x1b[?1004l";
|
|
34
|
+
let exitHookInstalled = false;
|
|
35
|
+
/** Asks the terminal to report focus while mounted, and keeps `useTerminalFocused` up to date. */
|
|
36
|
+
export function useFocusReporting(enabled) {
|
|
37
|
+
const { stdin } = useStdin();
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (!enabled || !stdin)
|
|
40
|
+
return;
|
|
41
|
+
const terminal = process.stdout.isTTY ? process.stdout : null;
|
|
42
|
+
terminal?.write(ENABLE);
|
|
43
|
+
if (terminal && !exitHookInstalled) {
|
|
44
|
+
process.on("exit", () => process.stdout.write(DISABLE));
|
|
45
|
+
exitHookInstalled = true;
|
|
46
|
+
}
|
|
47
|
+
const onData = (data) => {
|
|
48
|
+
const next = parseFocus(String(data));
|
|
49
|
+
if (next !== null)
|
|
50
|
+
setTerminalFocused(next);
|
|
51
|
+
};
|
|
52
|
+
stdin.on("data", onData);
|
|
53
|
+
return () => {
|
|
54
|
+
stdin.off("data", onData);
|
|
55
|
+
terminal?.write(DISABLE);
|
|
56
|
+
setTerminalFocused(true);
|
|
57
|
+
};
|
|
58
|
+
}, [enabled, stdin]);
|
|
59
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A full repaint: the app normally rewrites only the lines that changed, so if anything outside it ever
|
|
3
|
+
* wrote to the terminal, the screen would stay shifted (clicks landing a row off) until something forces
|
|
4
|
+
* every line out again. The CLI hands over Ink's clear here; /redraw and Ctrl+L call it.
|
|
5
|
+
*/
|
|
6
|
+
let clearScreen = null;
|
|
7
|
+
export function setScreenClear(fn) {
|
|
8
|
+
clearScreen = fn;
|
|
9
|
+
}
|
|
10
|
+
/** Forgets what's on screen, so the next frame is drawn in full; false where there's no terminal (tests). */
|
|
11
|
+
export function forgetScreen() {
|
|
12
|
+
if (!clearScreen)
|
|
13
|
+
return false;
|
|
14
|
+
clearScreen();
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
@@ -8,6 +8,9 @@ export const RENDER_OPTIONS = {
|
|
|
8
8
|
incrementalRendering: true,
|
|
9
9
|
// Up to 60 frames a second, so scrolling and typing keep up with the keyboard and mouse.
|
|
10
10
|
maxFps: 60,
|
|
11
|
+
// Library output goes to ~/.nou/nou.log instead (see cli.ts): Ink's console patch would print it into
|
|
12
|
+
// the full-screen app, scrolling it so clicks land a row off.
|
|
13
|
+
patchConsole: false,
|
|
11
14
|
// Terminals that speak the kitty keyboard protocol (kitty, Ghostty, WezTerm, iTerm2, foot...) then tell
|
|
12
15
|
// Enter from Ctrl+Enter, Cmd+Enter and Shift+Enter, so those can add a line in the chat. Others just
|
|
13
16
|
// don't answer the query and keys work as before.
|
package/dist/src/ui/text.js
CHANGED
|
@@ -413,3 +413,48 @@ export function closeOpenMarkdown(text) {
|
|
|
413
413
|
out += "**";
|
|
414
414
|
return out;
|
|
415
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Regions that can fold, from indentation (like VS Code without a language server): a line followed by
|
|
418
|
+
* more-indented lines folds them, and the closing bracket that ends the block. 1-based line -> last line.
|
|
419
|
+
*/
|
|
420
|
+
export function foldRanges(text) {
|
|
421
|
+
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
422
|
+
const indent = (s) => s.match(/^[ \t]*/)[0].replace(/\t/g, " ").length;
|
|
423
|
+
const blank = (s) => !s.trim();
|
|
424
|
+
const out = new Map();
|
|
425
|
+
for (let i = 0; i < lines.length; i++) {
|
|
426
|
+
if (blank(lines[i]))
|
|
427
|
+
continue;
|
|
428
|
+
const base = indent(lines[i]);
|
|
429
|
+
let j = i + 1;
|
|
430
|
+
while (j < lines.length && blank(lines[j]))
|
|
431
|
+
j++;
|
|
432
|
+
if (j >= lines.length || indent(lines[j]) <= base)
|
|
433
|
+
continue;
|
|
434
|
+
let end = j;
|
|
435
|
+
for (let k = j; k < lines.length; k++) {
|
|
436
|
+
if (blank(lines[k]))
|
|
437
|
+
continue;
|
|
438
|
+
if (indent(lines[k]) <= base)
|
|
439
|
+
break;
|
|
440
|
+
end = k;
|
|
441
|
+
}
|
|
442
|
+
// The line that closes the block (`}`, `)`, `]`, `end`...) folds with it.
|
|
443
|
+
const next = end + 1;
|
|
444
|
+
if (next < lines.length && indent(lines[next]) === base && /^\s*([}\])]|end\b|<\/)/.test(lines[next]))
|
|
445
|
+
end = next;
|
|
446
|
+
if (end > i)
|
|
447
|
+
out.set(i + 1, end + 1);
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
/** The file lines (1-based) left showing with `folded` regions (start lines) closed. */
|
|
452
|
+
export function unfoldedLines(count, ranges, folded) {
|
|
453
|
+
const out = [];
|
|
454
|
+
for (let n = 1; n <= count; n++) {
|
|
455
|
+
out.push(n);
|
|
456
|
+
if (folded.has(n) && ranges.has(n))
|
|
457
|
+
n = ranges.get(n);
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.6",
|
|
4
4
|
"description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|