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 +7 -4
- package/dist/src/config/index.js +2 -0
- package/dist/src/guide/index.js +100 -0
- package/dist/src/syntax/index.js +17 -17
- package/dist/src/ui/App.js +336 -42
- package/dist/src/ui/Chat.js +100 -19
- package/dist/src/ui/Editor.js +44 -7
- package/dist/src/ui/FileTree.js +2 -2
- package/dist/src/ui/GamePanel.js +1 -1
- package/dist/src/ui/Header.js +2 -2
- package/dist/src/ui/ProblemsView.js +2 -2
- package/dist/src/ui/Settings.js +2 -2
- package/dist/src/ui/Terminal.js +5 -2
- package/dist/src/ui/clipboard.js +19 -0
- package/dist/src/ui/commands.js +17 -0
- package/dist/src/ui/text.js +31 -3
- package/dist/src/ui/theme.js +2 -0
- package/package.json +1 -1
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
|
-
|
|
|
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><</kbd> / <kbd>></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
|
|
package/dist/src/config/index.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/src/syntax/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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 (
|
|
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 ||
|
|
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) &&
|
|
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 (
|
|
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
|
-
|
|
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");
|