nou-cli 0.5.3 → 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 +13 -5
- package/dist/src/cli.js +33 -0
- package/dist/src/config/index.js +3 -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/guide/index.js +100 -0
- package/dist/src/syntax/index.js +17 -17
- package/dist/src/ui/App.js +398 -54
- package/dist/src/ui/Chat.js +103 -19
- package/dist/src/ui/CodeView.js +70 -19
- package/dist/src/ui/Editor.js +44 -7
- package/dist/src/ui/FileTree.js +4 -2
- package/dist/src/ui/GamePanel.js +42 -14
- package/dist/src/ui/Header.js +5 -2
- package/dist/src/ui/ProblemsView.js +4 -2
- package/dist/src/ui/Settings.js +10 -3
- package/dist/src/ui/Terminal.js +5 -2
- package/dist/src/ui/clipboard.js +19 -0
- package/dist/src/ui/commands.js +21 -1
- 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 +76 -3
- package/dist/src/ui/theme.js +2 -0
- package/package.json +1 -1
|
@@ -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");
|