nou-cli 0.5.2 → 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 +15 -7
- package/dist/src/agent/index.js +33 -3
- package/dist/src/cli.js +3 -1
- package/dist/src/commands/index.js +1 -1
- package/dist/src/config/index.js +3 -1
- package/dist/src/conversations/external.js +165 -0
- package/dist/src/conversations/index.js +71 -0
- package/dist/src/engines/claude.js +10 -0
- package/dist/src/engines/codex.js +60 -5
- package/dist/src/guide/index.js +100 -0
- package/dist/src/providers/anthropic.js +3 -14
- package/dist/src/syntax/index.js +17 -17
- package/dist/src/terminal/index.js +127 -0
- package/dist/src/ui/App.js +582 -69
- package/dist/src/ui/Chat.js +153 -14
- package/dist/src/ui/ChatInput.js +5 -0
- package/dist/src/ui/CodeView.js +1 -1
- package/dist/src/ui/Editor.js +44 -7
- package/dist/src/ui/FileTree.js +5 -4
- package/dist/src/ui/GamePanel.js +18 -11
- 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/Tabs.js +6 -5
- package/dist/src/ui/Terminal.js +87 -0
- package/dist/src/ui/clipboard.js +19 -0
- package/dist/src/ui/commands.js +20 -0
- package/dist/src/ui/render-options.js +4 -0
- package/dist/src/ui/text.js +47 -3
- package/dist/src/ui/theme.js +2 -0
- package/package.json +1 -1
|
@@ -81,7 +81,7 @@ export function anthropicProvider() {
|
|
|
81
81
|
apiKey: process.env.ANTHROPIC_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || "",
|
|
82
82
|
api: "anthropic-messages",
|
|
83
83
|
}),
|
|
84
|
-
async chat({ model, system, messages, tools, onText, signal, effort: requested }) {
|
|
84
|
+
async chat({ model, system, messages, tools, onText, onThinking, signal, effort: requested }) {
|
|
85
85
|
const fallbacks = FALLBACK_MODELS.test(model);
|
|
86
86
|
const effort = EFFORT_MODELS.test(model) ? process.env.NOU_EFFORT ?? requested ?? "high" : undefined;
|
|
87
87
|
const body = {
|
|
@@ -105,20 +105,9 @@ export function anthropicProvider() {
|
|
|
105
105
|
let text = "";
|
|
106
106
|
try {
|
|
107
107
|
const stream = getClient().beta.messages.stream(body, signal ? { signal } : {});
|
|
108
|
-
// Thinking
|
|
109
|
-
|
|
110
|
-
stream.on("thinking", (delta) => {
|
|
111
|
-
const piece = `${thinking ? "" : "> "}${delta.replace(/\n/g, "\n> ")}`;
|
|
112
|
-
thinking = true;
|
|
113
|
-
text += piece;
|
|
114
|
-
onText(piece);
|
|
115
|
-
});
|
|
108
|
+
// Thinking goes to the progress line, not the reply: it isn't kept, so it mustn't reflow the chat.
|
|
109
|
+
stream.on("thinking", (delta) => onThinking?.(delta));
|
|
116
110
|
stream.on("text", (delta) => {
|
|
117
|
-
if (thinking) {
|
|
118
|
-
thinking = false;
|
|
119
|
-
text += "\n\n";
|
|
120
|
-
onText("\n\n");
|
|
121
|
-
}
|
|
122
111
|
text += delta;
|
|
123
112
|
onText(delta);
|
|
124
113
|
});
|
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");
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawn } from "child_process";
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { stripVTControlCharacters } from "util";
|
|
6
|
+
import { stopProcess } from "../commands/index.js";
|
|
7
|
+
/** Lines of output kept (the oldest go first). */
|
|
8
|
+
const KEEP = 3000;
|
|
9
|
+
/**
|
|
10
|
+
* The terminal panel's shell: runs what you type with your shell in the folder you've `cd`'d to, and
|
|
11
|
+
* keeps the output. Each command is its own process (no native pseudo-terminal), so full-screen programs
|
|
12
|
+
* like vim won't draw, but builds, tests, git and servers work, and what you type while one runs goes
|
|
13
|
+
* to it.
|
|
14
|
+
*/
|
|
15
|
+
export class Shell {
|
|
16
|
+
onChange;
|
|
17
|
+
lines = [];
|
|
18
|
+
cwd = process.cwd();
|
|
19
|
+
child = null;
|
|
20
|
+
/** The line being written (output without its newline yet; a \r starts it over, like progress bars). */
|
|
21
|
+
partial = "";
|
|
22
|
+
constructor(onChange) {
|
|
23
|
+
this.onChange = onChange;
|
|
24
|
+
process.once("exit", () => this.interrupt()); // nothing it started is left running after Nou
|
|
25
|
+
}
|
|
26
|
+
get running() {
|
|
27
|
+
return !!this.child;
|
|
28
|
+
}
|
|
29
|
+
/** Runs a command line; `cd` and `clear` are handled here so they stick. */
|
|
30
|
+
run(command) {
|
|
31
|
+
const line = command.trim();
|
|
32
|
+
if (!line)
|
|
33
|
+
return;
|
|
34
|
+
if (this.child)
|
|
35
|
+
return this.input(command);
|
|
36
|
+
this.push({ text: `${this.prompt()}${line}`, kind: "command" });
|
|
37
|
+
if (line === "clear" || line === "cls") {
|
|
38
|
+
this.lines = [];
|
|
39
|
+
return this.onChange();
|
|
40
|
+
}
|
|
41
|
+
const cd = /^cd(?:\s+(.*))?$/.exec(line);
|
|
42
|
+
if (cd)
|
|
43
|
+
return this.changeDir(cd[1]?.trim().replace(/^["']|["']$/g, "") || os.homedir());
|
|
44
|
+
const posix = process.platform !== "win32";
|
|
45
|
+
const child = spawn(line, {
|
|
46
|
+
cwd: this.cwd,
|
|
47
|
+
shell: posix ? process.env.SHELL || true : true,
|
|
48
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
49
|
+
detached: posix, // its own process group, so Ctrl+C stops everything it started
|
|
50
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
|
|
51
|
+
});
|
|
52
|
+
this.child = child;
|
|
53
|
+
const write = (chunk) => this.write(chunk.toString("utf-8"));
|
|
54
|
+
child.stdout?.on("data", write);
|
|
55
|
+
child.stderr?.on("data", write);
|
|
56
|
+
child.stdin?.on("error", () => { }); // it may exit before reading what you typed
|
|
57
|
+
child.on("error", (error) => this.push({ text: error.message, kind: "error" }));
|
|
58
|
+
child.on("close", (code, signal) => {
|
|
59
|
+
this.flush();
|
|
60
|
+
if (signal)
|
|
61
|
+
this.push({ text: "stopped", kind: "info" });
|
|
62
|
+
else if (code)
|
|
63
|
+
this.push({ text: `exit ${code}`, kind: "error" });
|
|
64
|
+
this.child = null;
|
|
65
|
+
this.onChange();
|
|
66
|
+
});
|
|
67
|
+
this.onChange();
|
|
68
|
+
}
|
|
69
|
+
/** Text for the running command's input (it gets a newline, like pressing Enter in a terminal). */
|
|
70
|
+
input(text) {
|
|
71
|
+
if (!this.child?.stdin?.writable)
|
|
72
|
+
return;
|
|
73
|
+
this.push({ text, kind: "command" });
|
|
74
|
+
this.child.stdin.write(`${text}\n`);
|
|
75
|
+
}
|
|
76
|
+
/** Ctrl+C: stops the running command. */
|
|
77
|
+
interrupt() {
|
|
78
|
+
if (this.child)
|
|
79
|
+
stopProcess(this.child);
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
this.lines = [];
|
|
83
|
+
this.partial = "";
|
|
84
|
+
this.onChange();
|
|
85
|
+
}
|
|
86
|
+
dispose() {
|
|
87
|
+
this.interrupt();
|
|
88
|
+
}
|
|
89
|
+
prompt() {
|
|
90
|
+
const home = os.homedir();
|
|
91
|
+
const where = this.cwd === home ? "~" : this.cwd.startsWith(home + path.sep) ? `~${this.cwd.slice(home.length)}` : this.cwd;
|
|
92
|
+
return `${where.length > 30 ? `…${path.sep}${path.basename(where)}` : where} $ `;
|
|
93
|
+
}
|
|
94
|
+
changeDir(target) {
|
|
95
|
+
const next = path.resolve(this.cwd, target.startsWith("~") ? path.join(os.homedir(), target.slice(1)) : target);
|
|
96
|
+
if (!existsSync(next) || !statSync(next).isDirectory())
|
|
97
|
+
this.push({ text: `cd: no such folder: ${target}`, kind: "error" });
|
|
98
|
+
else
|
|
99
|
+
this.cwd = next;
|
|
100
|
+
this.onChange();
|
|
101
|
+
}
|
|
102
|
+
write(text) {
|
|
103
|
+
const parts = stripVTControlCharacters(text).replace(/\r\n/g, "\n").split("\n");
|
|
104
|
+
for (let i = 0; i < parts.length; i++) {
|
|
105
|
+
const part = parts[i];
|
|
106
|
+
// A carriage return goes back to the start of the line: what follows replaces it.
|
|
107
|
+
this.partial = part.includes("\r") ? part.slice(part.lastIndexOf("\r") + 1) || this.partial : this.partial + part;
|
|
108
|
+
if (i < parts.length - 1)
|
|
109
|
+
this.flush(true);
|
|
110
|
+
}
|
|
111
|
+
this.onChange();
|
|
112
|
+
}
|
|
113
|
+
flush(always = false) {
|
|
114
|
+
if (this.partial || always)
|
|
115
|
+
this.push({ text: this.partial.replace(/\t/g, " "), kind: "out" });
|
|
116
|
+
this.partial = "";
|
|
117
|
+
}
|
|
118
|
+
/** The output so far, with the line still being written. */
|
|
119
|
+
view() {
|
|
120
|
+
return this.partial ? [...this.lines, { text: this.partial, kind: "out" }] : this.lines;
|
|
121
|
+
}
|
|
122
|
+
push(line) {
|
|
123
|
+
this.lines.push(line);
|
|
124
|
+
if (this.lines.length > KEEP)
|
|
125
|
+
this.lines.splice(0, this.lines.length - KEEP);
|
|
126
|
+
}
|
|
127
|
+
}
|