nou-cli 0.5.4 → 0.5.10
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 +18 -7
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli.js +69 -0
- package/dist/src/config/index.js +20 -0
- package/dist/src/conversations/external.js +35 -13
- 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 +75 -0
- package/dist/src/ui/App.js +281 -36
- package/dist/src/ui/Chat.js +63 -11
- package/dist/src/ui/CodeView.js +71 -20
- package/dist/src/ui/Editor.js +1 -1
- package/dist/src/ui/FileTree.js +3 -1
- package/dist/src/ui/GamePanel.js +42 -14
- package/dist/src/ui/Header.js +32 -4
- package/dist/src/ui/Pane.js +2 -2
- package/dist/src/ui/ProblemsView.js +3 -1
- package/dist/src/ui/QuickOpen.js +8 -3
- package/dist/src/ui/Settings.js +21 -7
- package/dist/src/ui/Terminal.js +7 -1
- package/dist/src/ui/background.js +58 -0
- package/dist/src/ui/commands.js +49 -7
- 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/dist/src/ui/theme.js +59 -13
- package/dist/src/workspaces/client.js +69 -0
- package/dist/src/workspaces/host.js +144 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -50,9 +50,11 @@ 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
|
+
| 🪟 **Workspaces** | Several projects in one window, a tab each in the header: click **+** (or `/workspace new ../api`) and the other folder opens beside this one with its own files, chats and agents. Click a tab (or `/workspace 2`) to switch; the ones in the background keep working, and a tab shows ● while its agent is busy. Quitting one (Ctrl+C twice) closes its tab. The first time, Nou installs a small pseudo-terminal package ([@lydell/node-pty](https://www.npmjs.com/package/@lydell/node-pty), prebuilt for macOS, Linux and Windows) and reopens as a workspaces window, with your conversation in the first tab. Or start that way: `nou --workspaces . ../api`. |
|
|
53
54
|
| 🗂️ **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
|
-
|
|
|
55
|
-
|
|
|
55
|
+
| 👀 **Show me** | Ask how something works or where it happens (`/showme where do we check the login?`, or just ask) and Nou explains it with the code open: each place it talks about opens in the code view, highlighted, and they're listed in the chat to click. `/showme next` and `/showme back` step through them. Works with every engine: Nou's points at places with a tool, and Claude Code, Codex and pi are asked to list them at the end of their reply, which Nou reads and hides. |
|
|
56
|
+
| 🧑🏫 **Teach me** | `/teach 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. `/teach next` skips a step, `/teach stop` ends it. Works with every engine (Nou's, Claude Code, Codex and pi): any edit one makes anyway is put back. |
|
|
57
|
+
| 🎮 **Games for a break** | Add Pong, Snake, Breakout (it gets faster each wall you clear), Dodge, Hop and Pairs 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. Your best score in each is kept, and a panel too small for a game says how much room it needs. |
|
|
56
58
|
| 🧠 **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`). |
|
|
57
59
|
| 🔎 **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). |
|
|
58
60
|
| ↶ **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. |
|
|
@@ -64,9 +66,12 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
64
66
|
| 👀 **Knows what you're looking at** | The open file, visible lines, your cursor and unsaved edits ride along with each message. "Fix this function" just works. |
|
|
65
67
|
| 🌿 **Git aware** | Sees your branch, uncommitted changes and recent commits, and can diff on its own. Press <kbd>v</kbd> to diff any file against `HEAD`. |
|
|
66
68
|
| 🔌 **Any model** | Local (Ollama, LM Studio, llama.cpp) or cloud (Claude, OpenAI, Gemini, OpenRouter, Groq, DeepSeek). Switch mid-conversation. |
|
|
69
|
+
| 🎚️ **Speed vs smarts** | Pick how hard the model thinks in plain words: **Fastest** and **Fast** (cheaper + faster), **Balanced** (the default), **Smarter** and **Smartest** (thinks longer, costs more). In <kbd>s</kbd> settings, or `/effort smarter`. |
|
|
67
70
|
| 🛞 **Pick your agent** | Nou's own, or Claude Code, Codex or pi, switchable like models, with the same diffs, approvals and undo. |
|
|
68
71
|
| 🧩 **Plugins** | Add your own tools and slash commands with a single file. |
|
|
69
72
|
| 🔎 **Quick open** | <kbd>Ctrl</kbd>+<kbd>P</kbd> fuzzy-finds any file. |
|
|
73
|
+
| 🔋 **Easy on your battery** | Idle, Nou uses next to no CPU. When its window is in the background (in terminals that report focus: iTerm2, Terminal.app, kitty, Ghostty, WezTerm, Windows Terminal, VS Code), spinners and git polling pause until you come back, and TypeScript checks are incremental. |
|
|
74
|
+
| 👓 **Easy to read** | Hints and secondary text keep at least 4.5:1 contrast. Nou asks your terminal whether its background is light or dark and picks matching colors (code switches to GitHub Light on a light background); set it yourself in <kbd>s</kbd> settings → **Nou's colors**. |
|
|
70
75
|
|
|
71
76
|
Every change is reviewed like a code review:
|
|
72
77
|
|
|
@@ -100,12 +105,14 @@ Every change is reviewed like a code review:
|
|
|
100
105
|
| <kbd>c</kbd> | in the file tree: show only changed files |
|
|
101
106
|
| <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) |
|
|
102
107
|
| <kbd><</kbd> / <kbd>></kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
|
|
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 |
|
|
108
|
+
| <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, × or `exit` 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 |
|
|
109
|
+
| <kbd>z</kbd> | in the code view: fold every function and block to its first line, or open them all again (also `/fold`, `/unfold`); click a ▾/▸ beside a line number to fold or open just that one |
|
|
110
|
+
| <kbd>Ctrl</kbd>+<kbd>L</kbd> | draw the whole screen again (also `/redraw`), if it ever looks shifted or clicks land a row off. Output from libraries goes to `~/.nou/nou.log` instead of the screen |
|
|
104
111
|
| <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) |
|
|
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) |
|
|
112
|
+
| <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; double-click copies a word, path or link, triple-click a paragraph) |
|
|
106
113
|
| <kbd>s</kbd> | settings: model, preferences, plugins |
|
|
107
114
|
| <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) |
|
|
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
|
|
115
|
+
| <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. To send now instead of waiting: <kbd>Alt</kbd>+<kbd>Enter</kbd> (stops Nou and sends it), click the queued message, or `/now`; or turn on **Send while Nou works** in settings so <kbd>Enter</kbd> sends right away (<kbd>Alt</kbd>+<kbd>Enter</kbd> then queues) |
|
|
109
116
|
| <kbd>Esc</kbd> | stop Nou (anything queued goes back in the input) · close a diff · back to chat |
|
|
110
117
|
| <kbd>Ctrl</kbd>+<kbd>C</kbd> | stop Nou while it works · press twice to quit |
|
|
111
118
|
|
|
@@ -123,11 +130,13 @@ In the editor:
|
|
|
123
130
|
| <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
|
|
124
131
|
| <kbd>Esc</kbd> | clear the selection, or leave the editor |
|
|
125
132
|
|
|
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 <
|
|
133
|
+
Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/effort [fastest|fast|balanced|smarter|smartest]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/workspace [new <folder> | n]` · `/now` · `/showme <question>` · `/teach <change>` · `/help`
|
|
134
|
+
|
|
135
|
+
A small typo in a command still works when it only changes the screen (`/termnial` opens the terminal); for others Nou asks "Did you mean...?" instead of guessing.
|
|
127
136
|
|
|
128
137
|
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.
|
|
129
138
|
|
|
130
|
-
Conversations are saved per folder after each reply. `/resume`
|
|
139
|
+
Conversations are saved per folder after each reply. `/resume` opens a picker (↑↓ and Enter, type to filter, or type its number) and `/resume 2` (or `nou --continue` for the last one, `nou --resume` for the list) picks one back up with the engine it was on: Nou's own engine restores its messages, Claude Code its session and Codex its thread; pi is handed the conversation so far. Sessions you started in Claude Code's or Codex's own CLI in this folder are listed too (marked "CLI"), and resuming one carries it on in that engine inside Nou.
|
|
131
140
|
|
|
132
141
|
## 🧠 Models
|
|
133
142
|
|
|
@@ -170,6 +179,8 @@ The engine is the agent that answers in the chat. Nou's own works with every mod
|
|
|
170
179
|
| `pi` | your Nou model, local or cloud | ✅ |
|
|
171
180
|
| `codex` (Codex SDK) | OpenAI; your API key or ChatGPT login | ❌ edits in its sandbox; review and undo after |
|
|
172
181
|
|
|
182
|
+
Codex's sandbox can write to the project and the repo's `.git` (so it can commit, stash and pull) and reach the network.
|
|
183
|
+
|
|
173
184
|
Other engines install on first use into `~/.nou/engines/`, so Nou itself stays small. Switching engines starts a new conversation. Compare them on the same tasks with `npm run eval -- --engine nou --engine claude-code`.
|
|
174
185
|
|
|
175
186
|
## 🔄 Updating
|
package/dist/src/agent/index.js
CHANGED
|
@@ -49,6 +49,7 @@ How to work:
|
|
|
49
49
|
- Don't commit, push, install packages or delete files unless the user asked. Don't add comments the code didn't need.
|
|
50
50
|
- Before saying something works, check it (run the tests or the code). If you couldn't, say so.
|
|
51
51
|
- When the user asks where something is, or to show, open or find something, search for it (grep, glob), read enough to be sure, then call open_file on the exact place (with line and end_line) so it opens in their code view, and answer in a sentence or two with path:line.
|
|
52
|
+
- When you explain how something works and it happens in more than one place, call show_places with each part (file, line range, a short note) in the order to read them: Nou opens them highlighted, one after another. Then explain in that order.
|
|
52
53
|
- If the user rejects an edit or command, don't retry it: ask what they would like instead.
|
|
53
54
|
- Keep replies short. Refer to code as path:line.
|
|
54
55
|
|
package/dist/src/cli.js
CHANGED
|
@@ -14,11 +14,45 @@ import { lsp } from "./lsp/index.js";
|
|
|
14
14
|
import { App } from "./ui/App.js";
|
|
15
15
|
import { installFastOutput } from "./ui/fast-output.js";
|
|
16
16
|
import { RENDER_OPTIONS } from "./ui/render-options.js";
|
|
17
|
+
import { setScreenClear } from "./ui/redraw.js";
|
|
18
|
+
import { detectBackground } from "./ui/background.js";
|
|
19
|
+
import { setPalette } from "./ui/theme.js";
|
|
20
|
+
import { runHost, workspaceFolder } from "./workspaces/host.js";
|
|
21
|
+
import { pendingWorkspaces } from "./workspaces/client.js";
|
|
22
|
+
import { spawn } from "child_process";
|
|
23
|
+
import { appendFileSync, mkdirSync } from "fs";
|
|
24
|
+
import path from "path";
|
|
25
|
+
import { format } from "util";
|
|
26
|
+
import { NOU_HOME } from "./config/index.js";
|
|
27
|
+
/**
|
|
28
|
+
* Sends console output and stderr to `file` instead of the terminal, until the returned function puts them
|
|
29
|
+
* back. Nou draws the whole screen; a stray line from a library would scroll it out from under the mouse.
|
|
30
|
+
*/
|
|
31
|
+
function quietConsole(file) {
|
|
32
|
+
const saved = { log: console.log, info: console.info, warn: console.warn, error: console.error, debug: console.debug, stderr: process.stderr.write };
|
|
33
|
+
const toFile = (text) => {
|
|
34
|
+
try {
|
|
35
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
36
|
+
appendFileSync(file, text);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// nowhere to keep it: dropping it still beats scrambling the screen
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
for (const name of ["log", "info", "warn", "error", "debug"])
|
|
43
|
+
console[name] = (...args) => toFile(`${new Date().toISOString()} ${name}: ${format(...args)}\n`);
|
|
44
|
+
process.stderr.write = ((chunk) => (toFile(String(chunk)), true));
|
|
45
|
+
return () => {
|
|
46
|
+
Object.assign(console, { log: saved.log, info: saved.info, warn: saved.warn, error: saved.error, debug: saved.debug });
|
|
47
|
+
process.stderr.write = saved.stderr;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
17
50
|
const HELP = `nou ${currentVersion()}: an AI coding assistant for your terminal
|
|
18
51
|
|
|
19
52
|
Usage:
|
|
20
53
|
nou open Nou in the current folder
|
|
21
54
|
nou --continue pick up the last conversation in this folder (-c)
|
|
55
|
+
nou --workspaces . ../api several projects in one window, a tab each
|
|
22
56
|
nou --resume list earlier conversations here to pick one up (-r)
|
|
23
57
|
nou update update to the latest version
|
|
24
58
|
nou --version print the version
|
|
@@ -48,6 +82,24 @@ async function main() {
|
|
|
48
82
|
console.log("If that was a permissions error, run: sudo npm install -g nou-cli@latest");
|
|
49
83
|
process.exit(1);
|
|
50
84
|
}
|
|
85
|
+
// nou --workspaces <folder>... : several Nous in this window, a tab each (see src/workspaces).
|
|
86
|
+
const wsAt = args.indexOf("--workspaces");
|
|
87
|
+
if (wsAt >= 0) {
|
|
88
|
+
if (!process.stdin.isTTY) {
|
|
89
|
+
console.error("Nou needs an interactive terminal.");
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const asked = args.slice(wsAt + 1).filter((a) => !a.startsWith("-"));
|
|
93
|
+
const folders = asked.map((f) => workspaceFolder(f, process.cwd())).filter((f) => !!f);
|
|
94
|
+
const missing = asked.filter((f) => !workspaceFolder(f, process.cwd()));
|
|
95
|
+
if (missing.length)
|
|
96
|
+
console.error(`Not a folder, so not opened: ${missing.join(", ")}`);
|
|
97
|
+
const uiTheme = loadSettings().uiTheme;
|
|
98
|
+
// Every workspace uses the colors found for this terminal (they can't ask it themselves from the background).
|
|
99
|
+
process.env.NOU_PALETTE = uiTheme === "dark" || uiTheme === "light" ? uiTheme : (await detectBackground()) ?? "dark";
|
|
100
|
+
const firstArgs = args.includes("--continue") || args.includes("-c") ? ["--continue"] : [];
|
|
101
|
+
process.exit(await runHost(folders.length ? folders : [process.cwd()], firstArgs));
|
|
102
|
+
}
|
|
51
103
|
if (!process.stdin.isTTY) {
|
|
52
104
|
console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
|
|
53
105
|
process.exit(1);
|
|
@@ -108,9 +160,26 @@ async function main() {
|
|
|
108
160
|
if ((settings.engine ?? "nou") === "nou")
|
|
109
161
|
await ensureProjectContext(choice);
|
|
110
162
|
const permContext = await loadPermanentContext();
|
|
163
|
+
// Colors that are readable on this terminal: a dark or a light set, from the setting or the background.
|
|
164
|
+
const uiTheme = loadSettings().uiTheme;
|
|
165
|
+
const fromHost = process.env.NOU_PALETTE;
|
|
166
|
+
setPalette(uiTheme === "dark" || uiTheme === "light" ? uiTheme : fromHost === "light" || fromHost === "dark" ? fromHost : (await detectBackground()) ?? "dark");
|
|
111
167
|
await installFastOutput();
|
|
168
|
+
// While Nou is on screen, anything a library prints (console.log, warnings, stderr) goes to a log file:
|
|
169
|
+
// written into the terminal, it would scroll the screen out from under the mouse.
|
|
170
|
+
const restoreOutput = quietConsole(path.join(NOU_HOME, "nou.log"));
|
|
112
171
|
const app = render(createElement(App, { initialModel: choice, permContext, ...(args.includes("--continue") || args.includes("-c") ? { resume: "last" } : args.includes("--resume") || args.includes("-r") ? { resume: "list" } : {}) }), RENDER_OPTIONS);
|
|
172
|
+
setScreenClear(() => app.clear());
|
|
113
173
|
await app.waitUntilExit();
|
|
174
|
+
restoreOutput();
|
|
175
|
+
// /workspace new: Nou closes and opens again as a workspaces window, this conversation in the first tab.
|
|
176
|
+
const folders = pendingWorkspaces();
|
|
177
|
+
if (folders) {
|
|
178
|
+
lsp.stopAll();
|
|
179
|
+
const child = spawn(process.execPath, [process.argv[1], "--workspaces", ...folders, "--continue"], { stdio: "inherit" });
|
|
180
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
114
183
|
lsp.stopAll();
|
|
115
184
|
p.outro(chalk.cyan("Bye bye!"));
|
|
116
185
|
}
|
package/dist/src/config/index.js
CHANGED
|
@@ -4,6 +4,23 @@ import path from "path";
|
|
|
4
4
|
export const NOU_HOME = path.join(os.homedir(), ".nou");
|
|
5
5
|
const CONFIG_PATH = path.join(NOU_HOME, "config.json");
|
|
6
6
|
export const EFFORTS = ["low", "medium", "high", "xhigh", "max"];
|
|
7
|
+
/**
|
|
8
|
+
* What each effort means for you, since "xhigh" says little: a short name for the picker and what it trades.
|
|
9
|
+
* The saved value stays the effort itself (low...max), which is what the engines are given.
|
|
10
|
+
*/
|
|
11
|
+
export const EFFORT_LABELS = {
|
|
12
|
+
low: { name: "Fastest", tradeoff: "Cheapest + fastest: quick answers, little thinking. Good for small edits and questions." },
|
|
13
|
+
medium: { name: "Fast", tradeoff: "Cheaper + faster: some thinking, for everyday changes." },
|
|
14
|
+
high: { name: "Balanced", tradeoff: "Balanced: thinks things through without taking long. The default." },
|
|
15
|
+
xhigh: { name: "Smarter", tradeoff: "Smarter: thinks longer, for trickier bugs and bigger changes. Slower, costs more." },
|
|
16
|
+
max: { name: "Smartest", tradeoff: "Smartest: thinks as long as it needs, for the hardest problems. Slowest, costs the most." },
|
|
17
|
+
};
|
|
18
|
+
/** An effort from its name or its label ("smarter", "xhigh"), any case. */
|
|
19
|
+
export function effortFrom(text) {
|
|
20
|
+
const t = text.trim().toLowerCase();
|
|
21
|
+
return EFFORTS.find((e) => e === t || EFFORT_LABELS[e].name.toLowerCase() === t) ?? null;
|
|
22
|
+
}
|
|
23
|
+
export const UI_THEMES = ["auto", "dark", "light"];
|
|
7
24
|
export const AUTO_SAVE = ["delay", "focus", "off"];
|
|
8
25
|
/** How fast follow mode replays what the agent does: a step every so often, or as it happens. */
|
|
9
26
|
export const FOLLOW_SPEEDS = ["slow", "normal", "fast", "instant"];
|
|
@@ -18,8 +35,10 @@ export const DEFAULT_SETTINGS = {
|
|
|
18
35
|
autoApproveEdits: false,
|
|
19
36
|
autoApproveCommands: false,
|
|
20
37
|
gitContext: true,
|
|
38
|
+
sendNowWhileBusy: false,
|
|
21
39
|
autoSave: "delay",
|
|
22
40
|
codeTheme: "one-dark",
|
|
41
|
+
uiTheme: "auto",
|
|
23
42
|
mouse: true,
|
|
24
43
|
reuseToolConfig: true,
|
|
25
44
|
paneWidths: null,
|
|
@@ -28,6 +47,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
28
47
|
gameDock: "chat",
|
|
29
48
|
terminalRows: null,
|
|
30
49
|
gameRows: null,
|
|
50
|
+
gameBest: {},
|
|
31
51
|
plugins: {},
|
|
32
52
|
trustedPlugins: {},
|
|
33
53
|
};
|
|
@@ -29,11 +29,27 @@ function jsonLines(file, limit = Infinity) {
|
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
31
31
|
/** The start of a file, for the line that says which folder a Codex session ran in. */
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The file's first line, however long (up to `max`): Codex puts its whole system prompt in a session's first
|
|
34
|
+
* line, tens of KB, and reading a fixed 8 KB cut it short, so no Codex session was ever found.
|
|
35
|
+
*/
|
|
36
|
+
function firstLine(file, max = 4 * 1024 * 1024) {
|
|
33
37
|
const fd = openSync(file, "r");
|
|
34
38
|
try {
|
|
35
|
-
const
|
|
36
|
-
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let read = 0;
|
|
41
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
42
|
+
while (read < max) {
|
|
43
|
+
const n = readSync(fd, buf, 0, buf.length, read);
|
|
44
|
+
if (n <= 0)
|
|
45
|
+
break;
|
|
46
|
+
const end = buf.subarray(0, n).indexOf(10); // \n
|
|
47
|
+
chunks.push(Buffer.from(buf.subarray(0, end >= 0 ? end : n)));
|
|
48
|
+
read += n;
|
|
49
|
+
if (end >= 0)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
37
53
|
}
|
|
38
54
|
finally {
|
|
39
55
|
closeSync(fd);
|
|
@@ -90,28 +106,34 @@ function codexSessions(home, cwd) {
|
|
|
90
106
|
if (!existsSync(root))
|
|
91
107
|
return [];
|
|
92
108
|
const files = [];
|
|
109
|
+
// By date: sessions/YYYY/MM/DD/rollout-*.jsonl. Anything else in there (a .DS_Store, a folder that can't
|
|
110
|
+
// be read) is skipped, rather than hiding every session.
|
|
93
111
|
const walk = (dir, depth) => {
|
|
94
|
-
|
|
112
|
+
let names;
|
|
113
|
+
try {
|
|
114
|
+
names = readdirSync(dir).sort().reverse();
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const name of names) {
|
|
95
120
|
if (files.length >= SCAN)
|
|
96
121
|
return;
|
|
97
122
|
const full = path.join(dir, name);
|
|
98
|
-
if (depth < 3)
|
|
99
|
-
|
|
123
|
+
if (depth < 3) {
|
|
124
|
+
if (!name.startsWith("."))
|
|
125
|
+
walk(full, depth + 1);
|
|
126
|
+
}
|
|
100
127
|
else if (name.startsWith("rollout-") && name.endsWith(".jsonl"))
|
|
101
128
|
files.push(full);
|
|
102
129
|
}
|
|
103
130
|
};
|
|
104
|
-
|
|
105
|
-
walk(root, 0);
|
|
106
|
-
}
|
|
107
|
-
catch {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
131
|
+
walk(root, 0);
|
|
110
132
|
const out = [];
|
|
111
133
|
for (const file of files) {
|
|
112
134
|
let meta;
|
|
113
135
|
try {
|
|
114
|
-
meta = JSON.parse(
|
|
136
|
+
meta = JSON.parse(firstLine(file));
|
|
115
137
|
}
|
|
116
138
|
catch {
|
|
117
139
|
continue;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { existsSync, mkdirSync } from "fs";
|
|
2
3
|
import path from "path";
|
|
4
|
+
import { NOU_HOME } from "../config/index.js";
|
|
3
5
|
import { onPath, projectBin, run } from "./process.js";
|
|
4
6
|
const rel = (file) => path.relative(process.cwd(), path.resolve(process.cwd(), file)).replace(/\\/g, "/");
|
|
5
7
|
/** ESLint, using the project's own install and config, so results match `npm run lint`. */
|
|
@@ -38,6 +40,18 @@ export const eslintProvider = {
|
|
|
38
40
|
},
|
|
39
41
|
};
|
|
40
42
|
/** The TypeScript compiler over the whole project (type errors need the full picture). */
|
|
43
|
+
/**
|
|
44
|
+
* tsc runs after every edit, and a full check of a mid-sized project is many CPU-seconds (on a laptop,
|
|
45
|
+
* battery). Incremental checks reuse what didn't change. The build info lives in ~/.nou/cache, one file per
|
|
46
|
+
* project, so the project's own files (and any tsBuildInfoFile it sets) are left alone.
|
|
47
|
+
*/
|
|
48
|
+
let incremental = true;
|
|
49
|
+
function buildInfoFile() {
|
|
50
|
+
const id = createHash("sha1").update(process.cwd()).digest("hex").slice(0, 16);
|
|
51
|
+
const dir = path.join(NOU_HOME, "cache", "tsc");
|
|
52
|
+
mkdirSync(dir, { recursive: true });
|
|
53
|
+
return path.join(dir, `${id}.tsbuildinfo`);
|
|
54
|
+
}
|
|
41
55
|
export const typescriptProvider = {
|
|
42
56
|
id: "typescript",
|
|
43
57
|
name: "TypeScript",
|
|
@@ -49,7 +63,14 @@ export const typescriptProvider = {
|
|
|
49
63
|
const bin = projectBin("typescript", "tsc");
|
|
50
64
|
if (!bin)
|
|
51
65
|
return [];
|
|
52
|
-
const
|
|
66
|
+
const args = [bin, "--noEmit", "-p", ".", "--pretty", "false"];
|
|
67
|
+
let result = await run(process.execPath, incremental ? [...args, "--incremental", "--tsBuildInfoFile", buildInfoFile()] : args, { timeout: 180_000 });
|
|
68
|
+
// An option this project's tsconfig rules out (a TS5xxx/TS6xxx error that names no file): check it the
|
|
69
|
+
// plain way from now on.
|
|
70
|
+
if (incremental && /^error TS[56]\d{3}:/m.test(result.stdout)) {
|
|
71
|
+
incremental = false;
|
|
72
|
+
result = await run(process.execPath, args, { timeout: 180_000 });
|
|
73
|
+
}
|
|
53
74
|
const diagnostics = [];
|
|
54
75
|
for (const line of result.stdout.split(/\r?\n/)) {
|
|
55
76
|
const m = line.match(/^(.+?)\((\d+),(\d+)\): (error|warning) (TS\d+): (.*)$/);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "child_process";
|
|
2
|
-
import { statSync } from "fs";
|
|
2
|
+
import { realpathSync, statSync } from "fs";
|
|
3
|
+
import path from "path";
|
|
3
4
|
import { transientMessage } from "../agent/index.js";
|
|
4
5
|
import { listAllFiles } from "../ui/QuickOpen.js";
|
|
5
6
|
import { ExternalEngine } from "./external.js";
|
|
@@ -9,6 +10,31 @@ const MAX_FILES = 5000;
|
|
|
9
10
|
const MAX_BYTES = 512 * 1024;
|
|
10
11
|
/** Times a turn that failed for a passing reason (a dropped stream, a failing server) is picked up again. */
|
|
11
12
|
const RESUMES = 5;
|
|
13
|
+
/**
|
|
14
|
+
* The repo's git folders. Codex's "workspace-write" sandbox keeps `.git` read-only inside the folder it may
|
|
15
|
+
* write to, so `git commit`, `git stash` and `git pull` fail ("cannot write to .git"). Handing it the git folder
|
|
16
|
+
* as a folder of its own makes it writable (the read-only rule is for a `.git` inside a writable folder).
|
|
17
|
+
* Worktrees keep their state in the main repo's git folder, so that one goes too.
|
|
18
|
+
*/
|
|
19
|
+
export function gitDirs(cwd = process.cwd()) {
|
|
20
|
+
try {
|
|
21
|
+
const out = execFileSync("git", ["rev-parse", "--absolute-git-dir", "--git-common-dir"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
22
|
+
// Real paths, so one folder reached two ways (macOS's /var is /private/var, Windows' short RUNNER~1 names)
|
|
23
|
+
// is listed once.
|
|
24
|
+
const real = (d) => { try {
|
|
25
|
+
return realpathSync.native(d);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return d;
|
|
29
|
+
} };
|
|
30
|
+
const dirs = out.split("\n").map((d) => d.trim()).filter(Boolean).map((d) => real(path.resolve(cwd, d)));
|
|
31
|
+
const key = (d) => (process.platform === "win32" ? d.toLowerCase() : d);
|
|
32
|
+
return dirs.filter((d, i) => dirs.findIndex((e) => key(e) === key(d)) === i);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return []; // not a git repo, or git isn't installed
|
|
36
|
+
}
|
|
37
|
+
}
|
|
12
38
|
/** Project files small enough to keep a copy of (git's list when there is one, so ignored files are skipped). */
|
|
13
39
|
function projectFiles() {
|
|
14
40
|
let files;
|
|
@@ -79,6 +105,7 @@ export class CodexEngine extends ExternalEngine {
|
|
|
79
105
|
// resolve registry.npmjs.org"); Nou's other engines can reach it, so Codex can too.
|
|
80
106
|
networkAccessEnabled: true,
|
|
81
107
|
approvalPolicy: "never",
|
|
108
|
+
additionalDirectories: gitDirs(),
|
|
82
109
|
...(this.choice.provider.id === "openai" ? { model: this.choice.model } : {}),
|
|
83
110
|
...(this.effort && EFFORTS.has(this.effort) ? { modelReasoningEffort: this.effort } : {}),
|
|
84
111
|
};
|