nou-cli 0.5.3 → 0.5.4

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 CHANGED
@@ -50,7 +50,9 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
50
50
  | 🔀 **Live diffs** | Every edit Nou proposes shows up side by side, and the files it is editing shimmer in the file tree while it works. <kbd>y</kbd> accepts, <kbd>n</kbd> rejects. Nothing touches disk without you. |
51
51
  | ✍️ **Built-in editor** | Press <kbd>e</kbd> to edit the open file yourself, with undo/redo and **auto-save**. |
52
52
  | 📍 **Shows you where things are** | Ask "where is the LiveKit integration?" and Nou finds it and opens it in the middle panel, jumped to the exact lines and highlighted. Any file a reply mentions (`src/a.ts:12`) is a link: click it. `/open name[:line]` opens a file yourself. |
53
- | 🎮 **Games for a break** | Add Pong, Snake and Breakout in settings → Games, then `/games` opens a panel docked under the chat: it splits that column instead of covering anything. Drag its title onto the files, code or chat to dock it there, and × at its top right closes it. A game runs only while the panel has the keyboard: Esc goes back to your code and it waits. |
53
+ | 🗂️ **Several agents at once** | `/new` (or **+** in the chat's title) opens another conversation tab while the first keeps working. Each tab has its own agent and history; a working tab shows ●, one waiting for your OK shows !, and a click (or `/tab 2`) switches. `/close` closes one; it stays in `/resume`. |
54
+ | 🧑‍🏫 **Show me how** | `/showme add a --verbose flag` and Nou finds where the change goes, then walks you through typing it yourself: each step opens the file at the right line, says why, and shows the code to type dim ahead of your cursor, like autocomplete (<kbd>Tab</kbd> fills in a line). It moves on when what you typed matches, and saves as it goes. `/showme next` skips a step, `/showme stop` ends it. Works with every engine (Nou's, Claude Code, Codex and pi): any edit one makes anyway is put back. `/teach` does the same. |
55
+ | 🎮 **Games for a break** | Add Pong, Snake and Breakout in settings → Games, then `/games` opens a panel docked under the chat: it splits that column instead of covering anything. Drag its title onto the files, code or chat to dock it there, and × at its top right closes it. Drag its top edge to make it taller or shorter. A game runs only while the panel has the keyboard: Esc goes back to your code and it waits. |
54
56
  | 🧠 **A capable engine** | Reads and searches in parallel, numbered reads for exact line references, edits that survive small whitespace mismatches, several edits to a file as one change, a plan for longer tasks, full command output (stderr too) with exit codes and timeouts, retries when a provider is overloaded or a connection drops (Codex too: it picks the turn back up instead of stopping at "Reconnecting..."), and conversation caching with Claude. It follows your repo's `AGENTS.md`, `CLAUDE.md` and `NOU.md` (and `~/.nou/NOU.md`). |
55
57
  | 🔎 **Finds its way around** | The agent searches your code (`grep`, `glob`) without asking, reads only what it needs, runs your tests to check its work, and summarizes long conversations to stay within the model's context (`/compact` does it on demand). |
56
58
  | ↶ **Undo anything** | Every change, Nou's or yours, is recorded. <kbd>u</kbd> undoes Nou's last edit, `/rewind` undoes its whole last turn, and your own later edits are kept. |
@@ -98,11 +100,12 @@ Every change is reviewed like a code review:
98
100
  | <kbd>c</kbd> | in the file tree: show only changed files |
99
101
  | <kbd>f</kbd> | follow mode (off by default): watch along as Nou works. Each file it reads (the exact lines, highlighted) is shown a step at a time, a little behind it, and its edits as they happen. The status bar shows how many steps behind it is; set the speed in settings → Preferences (slow · normal · fast · instant) |
100
102
  | <kbd>&lt;</kbd> / <kbd>&gt;</kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
101
- | <kbd>`</kbd> | a terminal under the code, like VS Code's (also `/terminal`): Enter runs a command, Ctrl+C stops it, ↑↓ history, Esc back to the code, × closes it. Commands run through your shell one at a time, so full-screen programs like vim don't draw there |
103
+ | <kbd>`</kbd> | a terminal under the code, like VS Code's (also `/terminal`): Enter runs a command, Ctrl+C stops it, ↑↓ history, Esc back to the code, × closes it; drag its top border (or <kbd>Alt</kbd>+↑/↓) to make it taller or shorter. Commands run through your shell one at a time, so full-screen programs like vim don't draw there |
104
+ | <kbd>Ctrl</kbd>+<kbd>O</kbd> | in the chat: open or fold every "▸ N steps" line (what the agent read and ran between replies folds into one; click one to open just it) |
102
105
  | <kbd>m</kbd> | mouse on/off, so you can select and copy text in the terminal (in the chat, just drag over the text: it's copied when you let go) |
103
106
  | <kbd>s</kbd> | settings: model, preferences, plugins |
104
107
  | <kbd>y</kbd> / <kbd>n</kbd> | accept / reject a proposed edit or command, from any pane; the rest of Nou keeps working while it waits (<kbd>o</kbd> shows the edit) |
105
- | <kbd>Enter</kbd> while Nou works | queue the message: it goes when Nou finishes, together with anything else you queued, as one message (<kbd>Alt</kbd>+<kbd>Enter</kbd>: stop Nou and send it now) |
108
+ | <kbd>Enter</kbd> while Nou works | queue the message: it goes when Nou finishes, together with anything else you queued, as one message; commands that only change the screen, like `/terminal`, `/games` or `/open`, run right away (<kbd>Alt</kbd>+<kbd>Enter</kbd>: stop Nou and send it now) |
106
109
  | <kbd>Esc</kbd> | stop Nou (anything queued goes back in the input) · close a diff · back to chat |
107
110
  | <kbd>Ctrl</kbd>+<kbd>C</kbd> | stop Nou while it works · press twice to quit |
108
111
 
@@ -120,7 +123,7 @@ In the editor:
120
123
  | <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
121
124
  | <kbd>Esc</kbd> | clear the selection, or leave the editor |
122
125
 
123
- Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/help`
126
+ Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/showme <what you want>` · `/help`
124
127
 
125
128
  New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol), or type `\` and press <kbd>Enter</kbd> in any terminal.
126
129
 
@@ -26,6 +26,8 @@ export const DEFAULT_SETTINGS = {
26
26
  trustedFolders: [],
27
27
  games: [],
28
28
  gameDock: "chat",
29
+ terminalRows: null,
30
+ gameRows: null,
29
31
  plugins: {},
30
32
  trustedPlugins: {},
31
33
  };
@@ -0,0 +1,100 @@
1
+ import path from "path";
2
+ export const SHOW_STEPS = "show_steps";
3
+ /** The tool the engine hands its steps back with. `onSteps` gets them; the engine is told they're shown. */
4
+ export function showStepsTool(onSteps) {
5
+ return {
6
+ definition: {
7
+ name: SHOW_STEPS,
8
+ description: "Only when the user asked you to show them how ([Show me how] in their message): hand back the change as steps for them to type themselves, instead of editing. Each step adds whole lines of code before a line of a file, with why it's needed. Call it once, with every step in order.",
9
+ parameters: {
10
+ type: "object",
11
+ properties: {
12
+ steps: {
13
+ type: "array",
14
+ items: {
15
+ type: "object",
16
+ properties: {
17
+ file: { type: "string", description: "Project-relative path of the file to add to." },
18
+ line: { type: "number", description: "1-based line the code goes in before (the line after the last one appends)." },
19
+ code: { type: "string", description: "The lines to type, indented as they'll be in the file." },
20
+ why: { type: "string", description: "One or two plain sentences: what this does and why it's needed." },
21
+ },
22
+ required: ["file", "line", "code", "why"],
23
+ },
24
+ },
25
+ },
26
+ required: ["steps"],
27
+ },
28
+ },
29
+ readOnly: true,
30
+ source: "nou",
31
+ run: (args) => {
32
+ const steps = tidySteps(args.steps);
33
+ if (!steps.length)
34
+ return "No usable steps: each needs a file, a line and the code to type. Call show_steps again.";
35
+ onSteps(steps);
36
+ return `Shown to the user as ${steps.length} step${steps.length > 1 ? "s" : ""} to type. Don't edit the files yourself; finish with a one-line summary.`;
37
+ },
38
+ };
39
+ }
40
+ /** Steps as the model gave them, kept when usable: a file, a line, code to type. */
41
+ function tidySteps(raw) {
42
+ return (Array.isArray(raw) ? raw : [])
43
+ .filter((s) => typeof s?.file === "string" && typeof s?.code === "string" && s.code.trim())
44
+ .map((s) => ({ file: path.relative(process.cwd(), path.resolve(s.file)).split(path.sep).join("/"), line: Math.max(1, Math.floor(Number(s.line) || 1)), code: s.code.replace(/\r\n/g, "\n").replace(/\n+$/, ""), why: String(s.why ?? "") }));
45
+ }
46
+ /** The fenced block other engines (which can't call Nou's tools) put their steps in. */
47
+ const BLOCK = /```nou-steps\s*\n([\s\S]*?)\n```/;
48
+ /**
49
+ * The message that asks for steps. Nou's own engine hands them back with the show_steps tool; Claude
50
+ * Code, Codex and pi can't call Nou's tools, so they end their reply with a ```nou-steps block instead.
51
+ */
52
+ export function showMePrompt(task, withTool) {
53
+ const how = withTool
54
+ ? `call ${SHOW_STEPS} once with the steps in order`
55
+ : "end your reply with the steps in order as a JSON array in a fenced block marked nou-steps, like:\n```nou-steps\n[{\"file\": \"src/cli.ts\", \"line\": 12, \"code\": \"const verbose = args.includes(\\\"--verbose\\\");\", \"why\": \"Reads the new flag.\"}]\n```";
56
+ return `[Show me how] ${task}\n\nDon't edit any files: I want to type it myself. Read what you need to find where it goes, then ${how} (each step: file, the line to add the code before, the exact lines of code to type, and why). Keep each step small, a few lines, so it's easy to follow.`;
57
+ }
58
+ /** Steps from a reply's ```nou-steps block, and the reply without it; null when there's none. */
59
+ export function stepsFromReply(text) {
60
+ const m = BLOCK.exec(text);
61
+ if (!m)
62
+ return null;
63
+ try {
64
+ const steps = tidySteps(JSON.parse(m[1]));
65
+ return steps.length ? { steps, rest: text.replace(m[0], "").trim() } : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * How far the text typed so far (`typed`, from where the step starts to the cursor) gets through
73
+ * `expected`: the index reached in `expected`, or -1 when it doesn't match. Leading indentation on each
74
+ * line is ignored either way, since the editor indents new lines itself.
75
+ */
76
+ export function guideProgress(typed, expected) {
77
+ let i = 0;
78
+ let j = 0;
79
+ const skipIndent = (s, k) => {
80
+ while (k < s.length && (s[k] === " " || s[k] === "\t"))
81
+ k++;
82
+ return k;
83
+ };
84
+ i = skipIndent(typed, i);
85
+ j = skipIndent(expected, j);
86
+ while (i < typed.length) {
87
+ if (j >= expected.length || typed[i] !== expected[j])
88
+ return -1;
89
+ const newline = typed[i] === "\n";
90
+ i++;
91
+ j++;
92
+ if (newline) {
93
+ i = skipIndent(typed, i);
94
+ // The rest of the expected indentation counts as typed only once something follows it.
95
+ if (i < typed.length)
96
+ j = skipIndent(expected, j);
97
+ }
98
+ }
99
+ return j;
100
+ }
@@ -101,28 +101,27 @@ const CALLS = /^(call_expression|call|method_invocation|new_expression)$/;
101
101
  /** A named string node (anonymous keyword nodes can also be called "string", e.g. the TS type). */
102
102
  const isString = (n) => n.isNamed && STRINGS.test(n.type);
103
103
  const hasSubstitution = (n) => n.children.some((c) => c !== null && /substitution|interpolation/.test(c.type));
104
- function colorFor(node) {
104
+ /**
105
+ * The color of a leaf. Its `parent` and `grand`parent and whether it's `inString` come from the walk over
106
+ * the tree: asking the node for them (node.parent) crosses into wasm, and every token used to, several times.
107
+ */
108
+ function colorFor(node, parent, grand, inString) {
105
109
  const type = node.type;
106
110
  if (type.includes("comment"))
107
111
  return "comment";
108
112
  // Inside a string (but not inside ${...} substitutions)?
109
- for (let p = node; p; p = p.parent) {
110
- if (/substitution|interpolation/.test(p.type))
111
- break;
112
- if (isString(p))
113
- return "string";
114
- }
113
+ if (inString || isString(node))
114
+ return "string";
115
115
  // Built-in type names (`number`, `string`) and Python annotations (`name: str`).
116
- if (node.parent?.type === "predefined_type" || (node.parent?.type === "type" && /identifier$/.test(type)))
116
+ if (parent?.type === "predefined_type" || (parent?.type === "type" && /identifier$/.test(type)))
117
117
  return "type";
118
118
  if (NUMBERS.test(type))
119
119
  return "number";
120
120
  if (CONSTANTS.test(type) || CONSTANTS.test(node.text))
121
121
  return node.isNamed || /^[a-zA-Z]+$/.test(node.text) ? "constant" : null;
122
- if (TYPES.test(type) && (node.isNamed || node.parent?.type === "predefined_type"))
122
+ if (TYPES.test(type) && (node.isNamed || parent?.type === "predefined_type"))
123
123
  return "type";
124
124
  if (/identifier$/.test(type)) {
125
- const parent = node.parent;
126
125
  if (parent && FUNCTION_PARENTS.test(parent.type) && parent.childForFieldName("name")?.equals(node))
127
126
  return "func";
128
127
  if (parent && CALLS.test(parent.type) && parent.childForFieldName("function")?.equals(node))
@@ -131,7 +130,7 @@ function colorFor(node) {
131
130
  const member = parent && /member_expression|attribute|field_expression|selector_expression/.test(parent.type)
132
131
  ? parent.childForFieldName("property") ?? parent.childForFieldName("attribute") ?? parent.childForFieldName("field")
133
132
  : null;
134
- if (member?.equals(node) && parent.parent && CALLS.test(parent.parent.type) && parent.parent.childForFieldName("function")?.equals(parent))
133
+ if (member?.equals(node) && grand && CALLS.test(grand.type) && grand.childForFieldName("function")?.equals(parent))
135
134
  return "func";
136
135
  }
137
136
  if (PROPERTIES.test(type))
@@ -146,7 +145,7 @@ function colorFor(node) {
146
145
  return null;
147
146
  }
148
147
  if (!node.isNamed) {
149
- if (node.parent?.type === "predefined_type")
148
+ if (parent?.type === "predefined_type")
150
149
  return "type";
151
150
  return /^[a-z_]+$/i.test(type) ? "keyword" : "punctuation";
152
151
  }
@@ -162,14 +161,14 @@ export function highlightTree(file, text) {
162
161
  let out = "";
163
162
  let pos = 0;
164
163
  const cursor = tree.walk();
165
- // Depth-first over leaves, coloring each span.
166
- const visit = () => {
164
+ // Depth-first over leaves, coloring each span; each level knows its parent, grandparent and whether it's in a string.
165
+ const visit = (parent, grand, inString) => {
167
166
  do {
168
167
  const node = cursor.currentNode;
169
168
  if (node.childCount === 0 || node.type.includes("comment") || (isString(node) && !hasSubstitution(node))) {
170
169
  if (node.startIndex >= pos) {
171
170
  out += themeColors().plain(text.slice(pos, node.startIndex));
172
- const color = colorFor(node);
171
+ const color = colorFor(node, parent, grand, inString);
173
172
  const span = text.slice(node.startIndex, node.endIndex);
174
173
  // Color each line separately so styles don't bleed across line breaks.
175
174
  out += span.split("\n").map((part) => (color ? themeColors()[color](part) : themeColors().plain(part))).join("\n");
@@ -177,13 +176,14 @@ export function highlightTree(file, text) {
177
176
  }
178
177
  }
179
178
  else if (cursor.gotoFirstChild()) {
180
- visit();
179
+ // A ${...} substitution inside a string starts code again.
180
+ visit(node, parent, /substitution|interpolation/.test(node.type) ? false : inString || isString(node));
181
181
  cursor.gotoParent();
182
182
  }
183
183
  } while (cursor.gotoNextSibling());
184
184
  };
185
185
  if (cursor.gotoFirstChild())
186
- visit();
186
+ visit(tree.rootNode, null, false);
187
187
  out += themeColors().plain(text.slice(pos));
188
188
  tree.delete();
189
189
  return out.split("\n");