nou-cli 0.5.20 → 0.5.21
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 -2
- package/dist/src/ui/App.js +13 -7
- package/dist/src/ui/QuickOpen.js +15 -8
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,8 +69,7 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
69
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`. |
|
|
70
70
|
| 🛞 **Pick your agent** | Nou's own, or Claude Code, Codex or pi, switchable like models, with the same diffs, approvals and undo. |
|
|
71
71
|
| 🧩 **Plugins** | Add your own tools and slash commands with a single file. |
|
|
72
|
-
| 🔎 **Quick open** | <kbd>Ctrl</kbd>+<kbd>P</kbd> (or `/files`)
|
|
73
|
-
| 🔍 **Search every file** | `/search <text>` (or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd> in terminals that report it) finds the text in every file as you type, each line as `file:line` with the match highlighted; <kbd>Enter</kbd> opens that line. Lowercase ignores case; a capital letter matches case exactly. |
|
|
72
|
+
| 🔎 **Quick open and search** | <kbd>Ctrl</kbd>+<kbd>P</kbd> (or `/files`) finds a file by name; <kbd>Tab</kbd> switches the same popup to searching the text inside every file (and back), keeping what you typed: each line as `file:line` with the match highlighted, <kbd>Enter</kbd> opens it. `/search <text>` (or <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>F</kbd> where the terminal reports it) opens it straight on searching inside. Lowercase ignores case; a capital matches case exactly. <kbd>?</kbd> (or `/actions`) lists every action. |
|
|
74
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. |
|
|
75
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**. |
|
|
76
75
|
|
package/dist/src/ui/App.js
CHANGED
|
@@ -55,8 +55,6 @@ const MIN_CODE = 20;
|
|
|
55
55
|
const MIN_CHAT = 24;
|
|
56
56
|
const NO_LINES = new Set();
|
|
57
57
|
const NO_FILES = new Set();
|
|
58
|
-
const NO_SEARCH_FILES = [];
|
|
59
|
-
const NO_SEARCH_ACTIONS = [];
|
|
60
58
|
const DOUBLE_CLICK_MS = 500;
|
|
61
59
|
/** How long each follow step stays on screen before the next. */
|
|
62
60
|
const FOLLOW_STEP_MS = { slow: 2500, normal: 1200, fast: 450, instant: 0 };
|
|
@@ -126,8 +124,12 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
126
124
|
const [setupReason, setSetupReason] = useState(null);
|
|
127
125
|
/** The file finder / command palette: closed (null), or open with this query (">" for commands). */
|
|
128
126
|
const [quickOpen, setQuickOpen] = useState(null);
|
|
129
|
-
/**
|
|
130
|
-
const [
|
|
127
|
+
/** The quick-open popup opens on searching inside files (/search, Ctrl+Shift+F) rather than file names. */
|
|
128
|
+
const [quickText, setQuickText] = useState(false);
|
|
129
|
+
const setSearching = (query) => {
|
|
130
|
+
setQuickText(true);
|
|
131
|
+
setQuickOpen(query);
|
|
132
|
+
};
|
|
131
133
|
/** /resume's picker: the conversations, in the palette's place while it's open. */
|
|
132
134
|
const [resumePicker, setResumePicker] = useState(null);
|
|
133
135
|
/** What the open picker is for, said in its empty search box. */
|
|
@@ -2700,24 +2702,28 @@ ${snippet}
|
|
|
2700
2702
|
clickable.current = { status: zones, modelFrom: columns - (" LOCAL ".length + ` ${modelLabel} `.length + 1) };
|
|
2701
2703
|
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Header, { width: columns, branch: branch, changes: git.size, model: modelLabel, local: choice.provider.local, busy: busy, update: update, tabs: tabsOfWorkspaces, project: projectName }), _jsxs(Box, { flexDirection: "row", height: height, children: [_jsx(FileTree, { width: treeWidth, height: heights.tree, focused: focus === "tree", openFile: openFile, reveal: reveal, version: treeVersion, changed: edited, working: working, ...(tourFiles ? { showing: tourFiles } : {}), git: git, problemCounts: problemCounts, wheelPane: "tree", scrollTo: scrollFor("tree"), click: treeClick, onOpen: onTreeOpen, onHover: onTreeHover }), codePanel === "problems" && !pendingChange ? (_jsx(ProblemsView, { width: codeWidth, height: heights.code, focused: focus === "code", problems: diagnostics.all(), checking: checking, wheelPane: "code", onJump: onJump, onClose: onCloseProblems })) : showEditor ? (_jsx(Editor, { ...(tabStrip ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, initial: draft.current?.path === openFile ? draft.current.text : content, baseline: draft.current?.path === openFile ? draft.current.baseline : content, startLine: editorStart.line, ...(editorStart.col ? { startCol: editorStart.col } : {}), problems: fileProblems, autoSave: settings.autoSave, syntaxVersion: syntaxVersion, wheelPane: "code", scrollTo: scrollFor("code"), mouse: editorMouse, onHover: onEditorHover, onDefinition: onEditorDefinition, onSave: onEditorSave, onExit: onEditorExit, onState: onEditorState, ...(guideStep && openFile === guideStep.file ? { guide: { row: guideStep.line - 1, code: guideStep.code }, onGuideDone } : {}) }, `${openFile}:${editorStart.nonce}`)) : (_jsx(CodeView, { ...(tour && tour.stops[tour.at]?.file === openFile && !diff ? { showing: `${tour.at + 1}/${tour.stops.length}` } : {}), ...(tabStrip && !diff ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, content: content, diff: diff, diffLabel: diffMode === "git" ? "vs HEAD" : diffMode === "session" ? sessionView?.label ?? "this session" : "Nou's edit", pending: !!pendingChange && diff === pendingChange, marked: record?.lines ?? NO_LINES, problems: fileProblems, focusLine: focusLine, syntaxVersion: syntaxVersion, diffNote: pendingChange && diff === pendingChange ? pendingNote?.short ?? (checking.length ? "checking…" : null) : null, wheelPane: "code", scrollTo: scrollFor("code"), onViewport: onViewport, defineAt: codeDefine, onDefine: onCodeDefine, onFinding: onCodeFinding, findRequest: findRequest, foldClick: foldClick, foldAll: foldAll })), _jsx(Chat, { width: chatWidth, height: heights.chat, focused: focus === "chat", entries: entries, streaming: streaming, busy: busy, approval: approval?.request ?? null, approvalNote: pendingChange ? pendingNote?.detail ?? null : null, contextChip: contextChip, wheelPane: "chat", scrollTo: scrollFor("chat"), prefill: chatPrefill, extraCommands: pluginCommands, onMenu: onChatMenu, onSubmit: onSend, queued: queued, sendNowFirst: settings.sendNowWhileBusy, onSendNow: sendNow, onSelecting: onChatSelecting, onAnswer: onAnswer, click: chatClick, select: chatSelect, tabs: chatTabsProp(), onOpenRef: openRef })] }), _jsx(Box, { children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { backgroundColor: mode.color, color: C.onBadge, bold: true, children: ` ${mode.label} ` }), _jsx(Text, { backgroundColor: C.panel, color: autoMode === "ask" ? C.dim : C.orange, bold: autoMode !== "ask", children: ` ${autoMode === "ask" ? "ask" : autoMode} ` }), _jsx(Text, { backgroundColor: C.panel, color: follow ? C.green : C.dim, children: followChip }), _jsx(Text, { backgroundColor: C.panel, color: totals.errors ? C.red : C.dim, children: ` ✗ ${totals.errors}` }), _jsx(Text, { backgroundColor: C.panel, color: totals.warnings ? C.yellow : C.dim, children: ` ▲ ${totals.warnings} ` }), checking.length > 0 && _jsxs(Text, { color: C.accent2, children: [" \u27F3 ", checking.join(", ")] }), exitArmed
|
|
2702
2704
|
? _jsxs(Text, { color: C.yellow, bold: true, children: [" ", "Press again to quit", draft.current && settings.autoSave === "off" ? ` (unsaved changes in ${draft.current.path} will be lost)` : ""] })
|
|
2703
|
-
: shownHints.map(([key, action], i) => (_jsxs(Text, { children: [i === 0 ? " " : _jsx(Text, { color: C.dim, children: " \u00B7 " }), _jsx(Text, { color: C.text, children: key }), _jsxs(Text, { color: C.dim, children: [" ", action] })] }, key)))] }) }), layout.current.terminal && shellRef.current && (_jsx(Box, { position: "absolute", marginLeft: layout.current.terminal.x, marginTop: layout.current.terminal.y, children: _jsx(Terminal, { shell: shellRef.current, version: termVersion, width: layout.current.terminal.width, height: layout.current.terminal.height, focused: focus === "terminal", wheelPane: "terminal", onLeave: leaveTerminal, onClose: () => toggleTerminal(false), onResize: resizeTerminal }) })), layout.current.game && (_jsx(Box, { position: "absolute", marginLeft: layout.current.game.x, marginTop: layout.current.game.y, children: _jsx(GameGuard, { children: _jsx(GamePanel, { width: layout.current.game.width, height: layout.current.game.height, focused: focus === "game", moving: !!gameDrag, installed: settings.games, best: settings.gameBest ?? {}, onScore: onGameScore, onLeave: leaveGames, onOpenSettings: gamesSettings }) }) })),
|
|
2704
|
-
|
|
2705
|
+
: shownHints.map(([key, action], i) => (_jsxs(Text, { children: [i === 0 ? " " : _jsx(Text, { color: C.dim, children: " \u00B7 " }), _jsx(Text, { color: C.text, children: key }), _jsxs(Text, { color: C.dim, children: [" ", action] })] }, key)))] }) }), layout.current.terminal && shellRef.current && (_jsx(Box, { position: "absolute", marginLeft: layout.current.terminal.x, marginTop: layout.current.terminal.y, children: _jsx(Terminal, { shell: shellRef.current, version: termVersion, width: layout.current.terminal.width, height: layout.current.terminal.height, focused: focus === "terminal", wheelPane: "terminal", onLeave: leaveTerminal, onClose: () => toggleTerminal(false), onResize: resizeTerminal }) })), layout.current.game && (_jsx(Box, { position: "absolute", marginLeft: layout.current.game.x, marginTop: layout.current.game.y, children: _jsx(GameGuard, { children: _jsx(GamePanel, { width: layout.current.game.width, height: layout.current.game.height, focused: focus === "game", moving: !!gameDrag, installed: settings.games, best: settings.gameBest ?? {}, onScore: onGameScore, onLeave: leaveGames, onOpenSettings: gamesSettings }) }) })), quickOpen !== null && (_jsx(QuickOpen, { width: columns, top: 3, files: allFiles, recent: recent, actions: resumePicker ?? paletteActions, initialQuery: quickOpen, ...(resumePicker ? { placeholder: "type to filter", title: pickerTitle } : { searchText, startInText: quickText }), onPickHit: (hit) => {
|
|
2706
|
+
setQuickOpen(null);
|
|
2707
|
+
setQuickText(false);
|
|
2705
2708
|
clearFollow();
|
|
2706
2709
|
// Opens the line with the match, highlighted, and puts the keyboard there.
|
|
2707
2710
|
openAt(hit.file, hit.line, false, hit.line);
|
|
2708
2711
|
setFocus("code");
|
|
2709
|
-
},
|
|
2712
|
+
}, onPick: (file) => {
|
|
2710
2713
|
setQuickOpen(null);
|
|
2714
|
+
setQuickText(false);
|
|
2711
2715
|
clearFollow();
|
|
2712
2716
|
open(file);
|
|
2713
2717
|
setReveal(file);
|
|
2714
2718
|
setFocus("code");
|
|
2715
2719
|
}, onRun: (action) => {
|
|
2716
2720
|
setQuickOpen(null);
|
|
2721
|
+
setQuickText(false);
|
|
2717
2722
|
setResumePicker(null);
|
|
2718
2723
|
action.run();
|
|
2719
2724
|
}, onClose: () => {
|
|
2720
2725
|
setQuickOpen(null);
|
|
2726
|
+
setQuickText(false);
|
|
2721
2727
|
setResumePicker(null);
|
|
2722
2728
|
} }))] }));
|
|
2723
2729
|
}
|
package/dist/src/ui/QuickOpen.js
CHANGED
|
@@ -58,18 +58,20 @@ export function score(file, query) {
|
|
|
58
58
|
return { score: total - file.length * 0.05, hits };
|
|
59
59
|
}
|
|
60
60
|
/** Ctrl+P: find a file. Start with ">" (or press ?) for every action Nou has, with its shortcut. */
|
|
61
|
-
export function QuickOpen({ width, top, files, recent = [], actions, initialQuery = "", placeholder = "Go to file… (> for commands)", title, onPick, searchText, onPickHit, onRun, onClose }) {
|
|
61
|
+
export function QuickOpen({ width, top, files, recent = [], actions, initialQuery = "", placeholder = "Go to file… (> for commands)", title, onPick, searchText, startInText = false, onPickHit, onRun, onClose }) {
|
|
62
62
|
const [query, setQuery] = useState(initialQuery);
|
|
63
63
|
const [cursor, setCursor] = useState(0);
|
|
64
|
-
//
|
|
65
|
-
|
|
64
|
+
// One popup for both: file names, or (Tab) the text inside every file. Searching inside reads every file,
|
|
65
|
+
// so it waits for a pause in the typing (each search is one pass, cached).
|
|
66
|
+
const [inText, setInText] = useState(startInText);
|
|
67
|
+
const textMode = !!searchText && inText;
|
|
66
68
|
const [searched, setSearched] = useState({ query: "", hits: [], more: false });
|
|
67
69
|
useEffect(() => {
|
|
68
|
-
if (!searchText)
|
|
70
|
+
if (!searchText || !textMode)
|
|
69
71
|
return;
|
|
70
72
|
const timer = setTimeout(() => setSearched({ query, ...searchText(query) }), query === searched.query ? 0 : 120);
|
|
71
73
|
return () => clearTimeout(timer);
|
|
72
|
-
}, [query,
|
|
74
|
+
}, [query, textMode]);
|
|
73
75
|
const boxWidth = Math.min(80, Math.max(40, width - 10));
|
|
74
76
|
const inner = boxWidth - 2; // fill every cell so the screen underneath doesn't show through
|
|
75
77
|
const commandMode = !textMode && query.startsWith(">");
|
|
@@ -101,6 +103,11 @@ export function QuickOpen({ width, top, files, recent = [], actions, initialQuer
|
|
|
101
103
|
useInput((_input, key) => {
|
|
102
104
|
if (key.escape)
|
|
103
105
|
onClose();
|
|
106
|
+
else if (key.tab && searchText && !query.startsWith(">")) {
|
|
107
|
+
// Tab: file names <-> the text inside files, keeping what's typed.
|
|
108
|
+
setInText((t) => !t);
|
|
109
|
+
setCursor(0);
|
|
110
|
+
}
|
|
104
111
|
else if (key.upArrow)
|
|
105
112
|
setCursor((c) => (c - 1 + Math.max(1, count)) % Math.max(1, count));
|
|
106
113
|
else if (key.downArrow)
|
|
@@ -114,7 +121,7 @@ export function QuickOpen({ width, top, files, recent = [], actions, initialQuer
|
|
|
114
121
|
/** Cut at the end (a title reads from the start). */
|
|
115
122
|
const fit = (s, n) => (s.length > n ? `${s.slice(0, n - 1)}…` : s.padEnd(n));
|
|
116
123
|
const pad = (s) => (s.length > inner ? `…${s.slice(-(inner - 1))}` : s.padEnd(inner));
|
|
117
|
-
return (_jsxs(Box, { position: "absolute", top: top, left: Math.floor((width - boxWidth) / 2), width: boxWidth, flexDirection: "column", borderStyle: "round", borderColor: C.accent2, children: [_jsxs(Text, { backgroundColor: C.panel, children: [_jsx(Text, { color: C.accent2, bold: true, children: fit(` ${title ?? (textMode ? "Search
|
|
124
|
+
return (_jsxs(Box, { position: "absolute", top: top, left: Math.floor((width - boxWidth) / 2), width: boxWidth, flexDirection: "column", borderStyle: "round", borderColor: C.accent2, children: [_jsxs(Text, { backgroundColor: C.panel, children: [_jsx(Text, { color: C.accent2, bold: true, children: fit(` ${title ?? (textMode ? "Search inside files · Tab: file names instead" : commandMode ? "Every action · type to filter (? or /actions)" : searchText ? "Find a file by name · Tab: search inside files" : "Find a file · type part of its name")}`, inner - 6) }), _jsx(Text, { color: C.dim, children: "Esc ×".padStart(6) })] }), _jsxs(Text, { backgroundColor: C.panel, children: [_jsx(Text, { color: C.accent2, bold: true, children: " \u2315 " }), _jsx(TextInput, { value: query, onChange: (v) => {
|
|
118
125
|
setQuery(v);
|
|
119
126
|
setCursor(0);
|
|
120
127
|
}, onSubmit: () => {
|
|
@@ -164,6 +171,6 @@ export function QuickOpen({ width, top, files, recent = [], actions, initialQuer
|
|
|
164
171
|
const hits = new Set(r.m.hits.map((h) => h + 1));
|
|
165
172
|
return (_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▌" : " " }), _jsx(Text, { color: color, children: icon }), [...label].map((ch, j) => (_jsx(Text, { color: hits.has(j) ? C.yellow : selected ? C.text : C.dim, bold: hits.has(j), children: ch }, j))), _jsx(Text, { color: C.dim, children: tag })] }, r.file));
|
|
166
173
|
}), _jsx(Text, { color: C.dim, children: pad(textMode
|
|
167
|
-
? ` ↑↓ select · Enter opens the line · Esc close ${searched.query === query && query.trim() ? `${hits.length}${searched.more ? "+" : ""} lines` : ""}`
|
|
168
|
-
: commandMode ? ` ↑↓ select · Enter run · Esc close ${found.length} commands` :
|
|
174
|
+
? ` ↑↓ select · Enter opens the line · Tab names · Esc close ${searched.query === query && query.trim() ? `${hits.length}${searched.more ? "+" : ""} lines` : ""}`
|
|
175
|
+
: commandMode ? ` ↑↓ select · Enter run · Esc close ${found.length} commands` : ` ↑↓ select · Enter open${searchText ? " · Tab inside files" : ""} · > commands · Esc close`) })] }));
|
|
169
176
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.21",
|
|
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": {
|