nou-cli 0.5.14 → 0.5.16
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 +1 -1
- package/dist/src/cli.js +6 -1
- package/dist/src/engines/claude.js +14 -3
- package/dist/src/engines/codex.js +4 -1
- package/dist/src/engines/external.js +48 -1
- package/dist/src/guide/index.js +20 -3
- package/dist/src/ui/App.js +20 -3
- package/dist/src/ui/Chat.js +2 -1
- package/dist/src/ui/FileTree.js +1 -1
- package/dist/src/ui/commands.js +2 -1
- package/dist/src/ui/theme.js +3 -0
- package/dist/src/ui/title.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Every change is reviewed like a code review:
|
|
|
104
104
|
| <kbd>u</kbd> | undo Nou's last edit |
|
|
105
105
|
| <kbd>v</kbd> | diff the open file vs git `HEAD`, Nou's last edit, or this session · <kbd>n</kbd>/<kbd>N</kbd> jump between changes · <kbd>[</kbd>/<kbd>]</kbd> previous/next file in a session diff |
|
|
106
106
|
| <kbd>c</kbd> | in the file tree: show only changed files |
|
|
107
|
-
| <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) |
|
|
107
|
+
| <kbd>f</kbd> or `/follow [on\|off]` | follow mode (off by default, and your choice is remembered): watch along as Nou works, with any engine (Codex's `sed`/`cat` reads count too). 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) |
|
|
108
108
|
| <kbd><</kbd> / <kbd>></kbd> / <kbd>=</kbd> | make the focused pane narrower / wider / reset the layout |
|
|
109
109
|
| <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 |
|
|
110
110
|
| <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 |
|
package/dist/src/cli.js
CHANGED
|
@@ -16,9 +16,10 @@ import { installFastOutput } from "./ui/fast-output.js";
|
|
|
16
16
|
import { RENDER_OPTIONS } from "./ui/render-options.js";
|
|
17
17
|
import { setScreenClear } from "./ui/redraw.js";
|
|
18
18
|
import { detectTerminal } from "./ui/background.js";
|
|
19
|
+
import { setTerminalTitle } from "./ui/title.js";
|
|
19
20
|
import { setPalette } from "./ui/theme.js";
|
|
20
21
|
import { runHost, workspaceFolder } from "./workspaces/host.js";
|
|
21
|
-
import { pendingWorkspaces } from "./workspaces/client.js";
|
|
22
|
+
import { isHosted, pendingWorkspaces } from "./workspaces/client.js";
|
|
22
23
|
import { spawn } from "child_process";
|
|
23
24
|
import { appendFileSync, mkdirSync } from "fs";
|
|
24
25
|
import path from "path";
|
|
@@ -97,6 +98,7 @@ async function main() {
|
|
|
97
98
|
const uiTheme = loadSettings().uiTheme;
|
|
98
99
|
// Every workspace uses the colors found for this terminal (they can't ask it themselves from the background).
|
|
99
100
|
// The host asks the terminal once and tells every workspace (in the background they can't ask it).
|
|
101
|
+
setTerminalTitle();
|
|
100
102
|
const term = await detectTerminal();
|
|
101
103
|
process.env.NOU_PALETTE = uiTheme === "dark" || uiTheme === "light" ? uiTheme : term.palette ?? "dark";
|
|
102
104
|
if (term.kitty !== null)
|
|
@@ -108,6 +110,9 @@ async function main() {
|
|
|
108
110
|
console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
|
|
109
111
|
process.exit(1);
|
|
110
112
|
}
|
|
113
|
+
// The tab says Nou CLI, not the shell it runs in (a workspace's host already set it).
|
|
114
|
+
if (!isHosted())
|
|
115
|
+
setTerminalTitle();
|
|
111
116
|
p.intro(chalk.cyan(`${artWork} \n Hello I am Nou! 🌞 ${chalk.dim(`v${currentVersion()}`)}`));
|
|
112
117
|
// A folder's files can run things (its MCP servers, plugins, the commands Nou is asked to run), so a
|
|
113
118
|
// folder is trusted once, like Claude Code and VS Code ask.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { toRelative } from "../commands/index.js";
|
|
4
|
-
import { ExternalEngine, replaceIn } from "./external.js";
|
|
4
|
+
import { ExternalEngine, readsInCommand, replaceIn } from "./external.js";
|
|
5
5
|
import { loadPackage } from "./index.js";
|
|
6
6
|
const EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
7
7
|
/** Claude Code tools that change files; Nou previews and approves them. */
|
|
@@ -95,8 +95,16 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
95
95
|
? { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: `This edit introduced problems; fix them:\n${problems}` } }
|
|
96
96
|
: {};
|
|
97
97
|
}
|
|
98
|
-
if (tool === "Read" && args.file_path)
|
|
99
|
-
|
|
98
|
+
if (tool === "Read" && args.file_path) {
|
|
99
|
+
// A part of the file (offset and limit, in lines): follow mode shows those lines.
|
|
100
|
+
const from = typeof args.offset === "number" ? Math.max(1, args.offset) : undefined;
|
|
101
|
+
const to = from && typeof args.limit === "number" ? from + args.limit - 1 : undefined;
|
|
102
|
+
this.hooks.onRead(toRelative(args.file_path), from, to);
|
|
103
|
+
}
|
|
104
|
+
if ((tool === "Bash" || tool === "PowerShell") && typeof args.command === "string") {
|
|
105
|
+
for (const read of readsInCommand(args.command))
|
|
106
|
+
this.hooks.onRead(read.path, read.line, read.to);
|
|
107
|
+
}
|
|
100
108
|
this.hooks.onEntry({ kind: "tool", text: tool === "Bash" || tool === "PowerShell" ? `Ran: ${args.command}` : describeTool(tool, args) });
|
|
101
109
|
return {};
|
|
102
110
|
};
|
|
@@ -149,6 +157,9 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
149
157
|
if (!signal.aborted)
|
|
150
158
|
this.hooks.onEntry({ kind: "error", text: `Claude Code: ${detail}` });
|
|
151
159
|
}
|
|
160
|
+
// The turn is over: don't wait for Claude Code's process to close its stream as well. Something it
|
|
161
|
+
// left running (a background command, an MCP server) kept that open, and Nou "working" forever.
|
|
162
|
+
break;
|
|
152
163
|
}
|
|
153
164
|
}
|
|
154
165
|
if (signal.aborted)
|
|
@@ -3,7 +3,7 @@ import { realpathSync, statSync } from "fs";
|
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { transientMessage } from "../agent/index.js";
|
|
5
5
|
import { listAllFiles } from "../ui/QuickOpen.js";
|
|
6
|
-
import { ExternalEngine } from "./external.js";
|
|
6
|
+
import { ExternalEngine, readsInCommand } from "./external.js";
|
|
7
7
|
import { loadPackage } from "./index.js";
|
|
8
8
|
const EFFORTS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
9
9
|
const MAX_FILES = 5000;
|
|
@@ -166,6 +166,9 @@ export class CodexEngine extends ExternalEngine {
|
|
|
166
166
|
}
|
|
167
167
|
else if (event.type === "item.started" && event.item.type === "command_execution") {
|
|
168
168
|
this.hooks.onBusy(`Codex: ${event.item.command}`);
|
|
169
|
+
// Codex reads files with shell commands: follow mode shows them as it would a read.
|
|
170
|
+
for (const read of readsInCommand(event.item.command))
|
|
171
|
+
this.hooks.onRead(read.path, read.line, read.to);
|
|
169
172
|
}
|
|
170
173
|
else if (event.type === "item.completed") {
|
|
171
174
|
const item = event.item;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync } from "fs";
|
|
1
|
+
import { readFileSync, statSync } from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import { newSessionId } from "../agent/index.js";
|
|
4
4
|
import { isInProject, toRelative } from "../commands/index.js";
|
|
@@ -10,6 +10,53 @@ function readOrNull(filePath) {
|
|
|
10
10
|
return null;
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* The files a shell command reads, so follow mode can show them: engines like Codex read with `sed -n
|
|
15
|
+
* '10,80p' f`, `cat f`, `nl -ba f`, `head -n 40 f` instead of a read tool. Only files that exist in the
|
|
16
|
+
* project count; anything it can't make sense of is no read.
|
|
17
|
+
*/
|
|
18
|
+
export function readsInCommand(command) {
|
|
19
|
+
// Codex runs `bash -lc '<script>'`: look inside the quotes.
|
|
20
|
+
const wrapped = command.match(/^\S*(?:bash|sh|zsh)\s+-l?c\s+(['"])([\s\S]*)\1\s*$/);
|
|
21
|
+
const script = wrapped ? wrapped[2] : command;
|
|
22
|
+
const reads = [];
|
|
23
|
+
for (const part of script.split(/\s*(?:&&|\|\||;|\||\n)\s*/)) {
|
|
24
|
+
const words = (part.match(/'[^']*'|"[^"]*"|\S+/g) ?? []).map((w) => w.replace(/^(['"])(.*)\1$/, "$2"));
|
|
25
|
+
const [tool, ...args] = words;
|
|
26
|
+
if (!tool)
|
|
27
|
+
continue;
|
|
28
|
+
const files = args.filter((a) => !a.startsWith("-") && /[./]|\w\.\w/.test(a));
|
|
29
|
+
let range = {};
|
|
30
|
+
if (tool === "sed" && args.includes("-n")) {
|
|
31
|
+
const m = args.join(" ").match(/(\d+)\s*,\s*(\d+)\s*p/) ?? args.join(" ").match(/(\d+)p/);
|
|
32
|
+
if (!m)
|
|
33
|
+
continue;
|
|
34
|
+
range = { line: Number(m[1]), to: Number(m[2] ?? m[1]) };
|
|
35
|
+
files.splice(0, files.length, ...files.filter((f) => !/^\d+(,\d+)?p$/.test(f)));
|
|
36
|
+
// `nl -ba f | sed -n '5,9p'`: the lines are of the file read just before it in the pipe.
|
|
37
|
+
const last = reads.at(-1);
|
|
38
|
+
if (!files.length && last && last.line === undefined)
|
|
39
|
+
Object.assign(last, range);
|
|
40
|
+
}
|
|
41
|
+
else if (tool === "head") {
|
|
42
|
+
const n = Number(args.join(" ").match(/-n\s*(\d+)|-(\d+)/)?.slice(1).find(Boolean) ?? 10);
|
|
43
|
+
range = { line: 1, to: n };
|
|
44
|
+
}
|
|
45
|
+
else if (!["cat", "nl", "bat", "less", "more", "tail"].includes(tool))
|
|
46
|
+
continue;
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
try {
|
|
49
|
+
if (!statSync(path.resolve(process.cwd(), file)).isFile() || !isInProject(file))
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
reads.push({ path: toRelative(file), ...range });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return reads;
|
|
59
|
+
}
|
|
13
60
|
/** Replaces `oldText` with `newText`, as edit tools do; null if it isn't there (the tool will fail on its own). */
|
|
14
61
|
export function replaceIn(text, oldText, newText, all = false) {
|
|
15
62
|
if (!oldText || !text.includes(oldText))
|
package/dist/src/guide/index.js
CHANGED
|
@@ -148,8 +148,25 @@ export function showPlacesTool(onStops) {
|
|
|
148
148
|
},
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
151
|
+
/**
|
|
152
|
+
* The fenced block other engines (which can't call Nou's tools) list their places in. Models also write it
|
|
153
|
+
* with ~~~, with CRLF, with the closing fence on the JSON's last line, or (unfenced) as a "nou-show" line
|
|
154
|
+
* followed by the JSON array at the end of the reply.
|
|
155
|
+
*/
|
|
156
|
+
const SHOW_BLOCK = /(?:```|~~~)[ \t]*nou-show[ \t]*\r?\n([\s\S]*?)\r?\n?[ \t]*(?:```|~~~)|(?:^|\n)[ \t]*nou-show[ \t]*\r?\n[ \t]*(\[[\s\S]*\])\s*$/;
|
|
157
|
+
/**
|
|
158
|
+
* A reply as the chat shows it: Nou's blocks (nou-show, nou-steps) are for Nou, not you, so they're never
|
|
159
|
+
* drawn, including one still arriving mid-stream.
|
|
160
|
+
*/
|
|
161
|
+
export function hideNouBlocks(text) {
|
|
162
|
+
if (!text.includes("nou-"))
|
|
163
|
+
return text;
|
|
164
|
+
return text
|
|
165
|
+
.replace(/(?:```|~~~)[ \t]*nou-(?:show|steps)[ \t]*\r?\n[\s\S]*?(?:```|~~~)/g, "")
|
|
166
|
+
.replace(/(?:```|~~~)[ \t]*nou-(?:show|steps)[\s\S]*$/, "")
|
|
167
|
+
.replace(/(?:^|\n)[ \t]*nou-(?:show|steps)[ \t]*\r?\n[ \t]*\[[\s\S]*$/, "")
|
|
168
|
+
.trimEnd();
|
|
169
|
+
}
|
|
153
170
|
/**
|
|
154
171
|
* For Claude Code, Codex and pi, which can't call show_places: how to point at places, added to what they
|
|
155
172
|
* know about the project, so explaining "where does X happen" opens the files on any engine.
|
|
@@ -166,7 +183,7 @@ export function stopsFromReply(text) {
|
|
|
166
183
|
if (!m)
|
|
167
184
|
return null;
|
|
168
185
|
try {
|
|
169
|
-
const stops = tidyStops(JSON.parse(m[1]));
|
|
186
|
+
const stops = tidyStops(JSON.parse((m[1] ?? m[2])));
|
|
170
187
|
return { stops, rest: text.replace(m[0], "").trim() };
|
|
171
188
|
}
|
|
172
189
|
catch {
|
package/dist/src/ui/App.js
CHANGED
|
@@ -1457,6 +1457,17 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1457
1457
|
if (!followTimer.current)
|
|
1458
1458
|
playFollow();
|
|
1459
1459
|
};
|
|
1460
|
+
/** Turns follow mode on or off (`to`), or flips it; remembered for next time, and the chat says what it does. */
|
|
1461
|
+
const toggleFollow = (to) => {
|
|
1462
|
+
const on = to ?? !followRef.current;
|
|
1463
|
+
followRef.current = on;
|
|
1464
|
+
setFollow(on);
|
|
1465
|
+
if (settingsRef.current.followMode !== on)
|
|
1466
|
+
updateSettings({ followMode: on });
|
|
1467
|
+
info(on
|
|
1468
|
+
? "Follow on: the code view now shows each file Nou reads or edits as it works (f, the follow chip or /follow off to stop)."
|
|
1469
|
+
: "Follow off: the code view stays on what you have open. Edits waiting for your OK still show.");
|
|
1470
|
+
};
|
|
1460
1471
|
const followStepRef = useRef(followStep);
|
|
1461
1472
|
followStepRef.current = followStep;
|
|
1462
1473
|
useEffect(() => {
|
|
@@ -1570,7 +1581,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1570
1581
|
setChatPrefill((p) => ({ text: "/", nonce: (p?.nonce ?? 0) + 1 }));
|
|
1571
1582
|
}
|
|
1572
1583
|
else if (input === "f")
|
|
1573
|
-
|
|
1584
|
+
toggleFollow();
|
|
1574
1585
|
else if (input === "m")
|
|
1575
1586
|
toggleMouse();
|
|
1576
1587
|
else if (input === "`")
|
|
@@ -1752,6 +1763,12 @@ ${snippet}
|
|
|
1752
1763
|
return info(`No file matches "${wanted}". To find where something is in the code, ask in the chat.`, "error");
|
|
1753
1764
|
return openRef({ path: file, ...(m[2] ? { line: Number(m[2]) } : {}), ...(m[3] ? { to: Number(m[3]) } : {}) });
|
|
1754
1765
|
}
|
|
1766
|
+
case "follow": {
|
|
1767
|
+
const want = arg.trim().toLowerCase();
|
|
1768
|
+
if (want && !/^(on|off)$/.test(want))
|
|
1769
|
+
return info("Use /follow on or /follow off (or just /follow to switch).", "error");
|
|
1770
|
+
return toggleFollow(want ? want === "on" : undefined);
|
|
1771
|
+
}
|
|
1755
1772
|
case "effort": {
|
|
1756
1773
|
// Speed vs smarts, by name: /effort smarter (or the effort itself: /effort xhigh).
|
|
1757
1774
|
const options = EFFORTS.map((e) => EFFORT_LABELS[e].name.toLowerCase()).join(", ");
|
|
@@ -2549,7 +2566,7 @@ ${snippet}
|
|
|
2549
2566
|
{ title: "Ask before every edit and command", keys: "Shift+Tab", run: () => cycleAutoMode("ask") },
|
|
2550
2567
|
{ title: "Auto-edit: apply edits without asking", keys: "Shift+Tab", run: () => cycleAutoMode("auto-edit") },
|
|
2551
2568
|
{ title: "Auto: edits and commands without asking", keys: "Shift+Tab", run: () => cycleAutoMode("auto") },
|
|
2552
|
-
{ title: `Follow mode ${follow ? "off" : "on"}: jump to files Nou works on`, keys: "f", run: () =>
|
|
2569
|
+
{ title: `Follow mode ${follow ? "off" : "on"}: jump to files Nou works on`, keys: "f", run: () => toggleFollow() },
|
|
2553
2570
|
{ title: `Replay speed for follow mode: ${settings.followSpeed} (next: ${FOLLOW_SPEEDS[(FOLLOW_SPEEDS.indexOf(settings.followSpeed) + 1) % FOLLOW_SPEEDS.length]})`, keys: "settings", run: () => updateSettings({ followSpeed: FOLLOW_SPEEDS[(FOLLOW_SPEEDS.indexOf(settings.followSpeed) + 1) % FOLLOW_SPEEDS.length] }) },
|
|
2554
2571
|
{ title: `Mouse ${settings.mouse ? "off (select text in the terminal)" : "on"}`, keys: "m", run: toggleMouse },
|
|
2555
2572
|
{ title: "Copy the last reply", keys: "/copy", run: () => void runCommand("/copy") },
|
|
@@ -2625,7 +2642,7 @@ ${snippet}
|
|
|
2625
2642
|
x += width;
|
|
2626
2643
|
};
|
|
2627
2644
|
add(` ${autoMode} `.length, () => cycleAutoMode());
|
|
2628
|
-
add(followChip.length, () =>
|
|
2645
|
+
add(followChip.length, () => toggleFollow());
|
|
2629
2646
|
add(` ✗ ${totals.errors} ▲ ${totals.warnings} `.length, toggleProblems);
|
|
2630
2647
|
if (checking.length)
|
|
2631
2648
|
add(3 + checking.join(", ").length);
|
package/dist/src/ui/Chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { hideNouBlocks } from "../guide/index.js";
|
|
2
3
|
import { memo, useEffect, useMemo, useRef, useState } from "react";
|
|
3
4
|
import { Box, Text, useAnimation, useInput } from "ink";
|
|
4
5
|
import { statSync } from "fs";
|
|
@@ -54,7 +55,7 @@ function entryLines(entry, inner, links = true) {
|
|
|
54
55
|
}
|
|
55
56
|
else if (entry.kind === "assistant") {
|
|
56
57
|
out.push({ text: "◆ nou", color: C.accent2, bold: true, lead: BAR, leadColor: C.accent2 });
|
|
57
|
-
for (const line of renderMarkdown(entry.text, inner - 3, links ? isProjectFile : undefined)) {
|
|
58
|
+
for (const line of renderMarkdown(hideNouBlocks(entry.text), inner - 3, links ? isProjectFile : undefined)) {
|
|
58
59
|
const color = line.kind === "heading" ? C.accent2 : line.kind === "quote" ? C.dim : undefined;
|
|
59
60
|
out.push({ text: "", spans: line.spans, ...(color ? { color } : {}), lead: BAR, leadColor: C.accent2 });
|
|
60
61
|
}
|
package/dist/src/ui/FileTree.js
CHANGED
|
@@ -192,6 +192,6 @@ export const FileTree = memo(function FileTree({ width, height, focused, openFil
|
|
|
192
192
|
const room = Math.max(1, inner - marker.length - 1);
|
|
193
193
|
const shown = label.length > room ? `${label.slice(0, room - 1)}…` : label.padEnd(room);
|
|
194
194
|
const iconAt = shown.indexOf(icon);
|
|
195
|
-
return (_jsxs(Text, { wrap: "truncate-end", ...(selected && focused ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, children: shown.slice(0, iconAt) }), _jsx(Text, { color: color, children: shown.slice(iconAt, iconAt + icon.length) }), working?.has(node.path) ? (_jsx(Text, { bold: true, inverse: selected && focused, children: gradient(shown.slice(iconAt + icon.length), frame).map((g, j) => _jsx(Text, { color: g.color, children: g.ch }, j)) })) : (_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, inverse: selected && focused, children: shown.slice(iconAt + icon.length) })), _jsxs(Text, { color: markerColor, children: [" ", marker] })] }, node.path));
|
|
195
|
+
return (_jsxs(Text, { wrap: "truncate-end", ...(isOpen ? { backgroundColor: C.openRow } : selected && focused ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, children: shown.slice(0, iconAt) }), _jsx(Text, { color: color, children: shown.slice(iconAt, iconAt + icon.length) }), working?.has(node.path) ? (_jsx(Text, { bold: true, inverse: selected && focused, children: gradient(shown.slice(iconAt + icon.length), frame).map((g, j) => _jsx(Text, { color: g.color, children: g.ch }, j)) })) : (_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, inverse: selected && focused, children: shown.slice(iconAt + icon.length) })), _jsxs(Text, { color: markerColor, children: [" ", marker] })] }, node.path));
|
|
196
196
|
})] }), _jsx(Scrollbar, { total: nodes.length, top: start, rows: rows, focused: focused })] }) }));
|
|
197
197
|
});
|
package/dist/src/ui/commands.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Commands that only change what's on screen (or handle a busy Nou themselves): they run right away
|
|
3
3
|
* while Nou works instead of waiting in the queue behind it.
|
|
4
4
|
*/
|
|
5
|
-
const INSTANT = new Set(["files", "actions", "palette", "zoom", "now", "fold", "unfold", "redraw", "new", "tab", "close", "terminal", "games", "open", "settings", "auto", "diff", "sessions", "history", "problems", "copy", "help", "version", "cancel"]);
|
|
5
|
+
const INSTANT = new Set(["follow", "files", "actions", "palette", "zoom", "now", "fold", "unfold", "redraw", "new", "tab", "close", "terminal", "games", "open", "settings", "auto", "diff", "sessions", "history", "problems", "copy", "help", "version", "cancel"]);
|
|
6
6
|
/** Whether a typed command runs right away while Nou works (see INSTANT); `/showme` counts only to look or step. */
|
|
7
7
|
/**
|
|
8
8
|
* The command a mistyped name means: the same letters in another order ("/termnial"), or one letter off
|
|
@@ -51,6 +51,7 @@ export const SLASH_COMMANDS = [
|
|
|
51
51
|
{ name: "models", args: "[provider]", description: "list the models a provider offers" },
|
|
52
52
|
{ name: "connect", args: "<provider>", description: "enter an API key (and URL) for a provider" },
|
|
53
53
|
{ name: "open", args: "<file>[:line]", description: "open a file in the code view (part of its name is enough)" },
|
|
54
|
+
{ name: "follow", args: "[on|off]", description: "the code view shows each file the agent reads or edits as it works" },
|
|
54
55
|
{ name: "effort", args: "[fastest|fast|balanced|smarter|smartest]", description: "speed vs smarts: how hard the model thinks (cheaper + faster, or smarter)" },
|
|
55
56
|
{ name: "workspace", args: "[new <folder> | n]", description: "several projects in this window, a tab each in the header: open one, or switch" },
|
|
56
57
|
{ name: "files", description: "find a file by part of its name (also Ctrl+P)" },
|
package/dist/src/ui/theme.js
CHANGED
|
@@ -16,6 +16,8 @@ const DARK = {
|
|
|
16
16
|
/** Lines around panes that aren't focused: quieter than text, since they aren't read. */
|
|
17
17
|
border: "#565f89",
|
|
18
18
|
panel: "#1f2335",
|
|
19
|
+
/** The band behind the file that's open, wherever it's listed: every name color stays 4.5:1 on it. */
|
|
20
|
+
openRow: "#2a3352",
|
|
19
21
|
/** Text on a colored badge (CHAT, CLOUD, EDITING...). */
|
|
20
22
|
onBadge: "#1a1b26",
|
|
21
23
|
};
|
|
@@ -32,6 +34,7 @@ const LIGHT = {
|
|
|
32
34
|
dim: "#5a607a",
|
|
33
35
|
border: "#a8aecb",
|
|
34
36
|
panel: "#e6e7ed",
|
|
37
|
+
openRow: "#edf1fc",
|
|
35
38
|
onBadge: "#ffffff",
|
|
36
39
|
};
|
|
37
40
|
/** The colors in use: dark unless the terminal is light (see setPalette). Read at render time, so set it first. */
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The terminal's window or tab title. Windows Terminal, iTerm2, Ghostty and the rest show the running shell
|
|
3
|
+
* ("Windows PowerShell") unless the program sets its own with OSC 0, which Nou does: "Nou CLI".
|
|
4
|
+
* The title before is saved (CSI 22;0t) and put back (CSI 23;0t) on the way out, in terminals that keep a
|
|
5
|
+
* title stack (xterm, kitty, Ghostty, WezTerm, iTerm2); the others ignore those codes, and the shell retitles
|
|
6
|
+
* its tab soon after.
|
|
7
|
+
*/
|
|
8
|
+
export const TITLE = "Nou CLI";
|
|
9
|
+
let set = false;
|
|
10
|
+
export function setTerminalTitle(title = TITLE) {
|
|
11
|
+
if (set || !process.stdout.isTTY)
|
|
12
|
+
return;
|
|
13
|
+
set = true;
|
|
14
|
+
process.stdout.write(`\x1b[22;0t\x1b]0;${title}\x07`);
|
|
15
|
+
process.on("exit", () => process.stdout.write("\x1b[23;0t"));
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.16",
|
|
4
4
|
"description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|