nou-cli 0.5.10 → 0.5.11
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 +4 -3
- package/dist/src/ui/App.js +66 -18
- package/dist/src/ui/Chat.js +1 -1
- package/dist/src/ui/CodeView.js +6 -5
- package/dist/src/ui/Editor.js +4 -3
- package/dist/src/ui/FileTree.js +6 -5
- package/dist/src/ui/Header.js +13 -2
- package/dist/src/ui/ProblemsView.js +3 -3
- package/dist/src/ui/Settings.js +2 -2
- package/dist/src/ui/commands.js +3 -2
- package/dist/src/ui/keys.js +8 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
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
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`. |
|
|
54
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`. |
|
|
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. |
|
|
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, and `/showme stop` clears the highlights. While it shows, the code view and the file tree take its purple (◉ SHOWING 2/6, and each file's places), as they take green while you edit. 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
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
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. |
|
|
58
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`). |
|
|
@@ -94,8 +94,9 @@ Every change is reviewed like a code review:
|
|
|
94
94
|
| <kbd>Tab</kbd> | switch between files, code and chat |
|
|
95
95
|
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | ask → auto-edit (edits apply without asking) → auto (commands too); everything stays undoable |
|
|
96
96
|
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file; the files you've opened are listed first |
|
|
97
|
+
| <kbd>+</kbd> / <kbd>-</kbd> (in the code) or `/zoom in` / `/zoom out` | make the code panel bigger or smaller, a step at a time: the files and chat give or take the room; <kbd>=</kbd> or `/zoom reset` puts it back (for bigger text, use your terminal's zoom, Cmd/Ctrl and +) |
|
|
97
98
|
| <kbd>b</kbd> | back to the previous file (press again to flip back) |
|
|
98
|
-
| <kbd>Ctrl</kbd>+<kbd>F</kbd> | find in the open file: matches are highlighted and counted; <kbd>Enter</kbd>/<kbd>↓</kbd> next, <kbd>↑</kbd> previous, <kbd>Esc</kbd> close |
|
|
99
|
+
| <kbd>Ctrl</kbd>+<kbd>F</kbd> (<kbd>Cmd</kbd>+<kbd>F</kbd> on a Mac, in terminals that report Cmd: Ghostty, kitty, WezTerm, iTerm2) | find in the open file, from any pane: matches are highlighted and counted; <kbd>Enter</kbd>/<kbd>↓</kbd> next, <kbd>↑</kbd> previous, <kbd>Esc</kbd> close |
|
|
99
100
|
| 🗂️ Tabs | every file you open gets a tab over the code: click one to switch, any tab's × to close it, ‹ › for ones that don't fit · <kbd>[</kbd>/<kbd>]</kbd> previous/next · <kbd>x</kbd> or <kbd>Ctrl</kbd>+<kbd>W</kbd> close |
|
|
100
101
|
| <kbd>Ctrl</kbd>+click | on a name in the code (or <kbd>Option</kbd>+click, or right-click): go to its definition; an imported name goes to its import, and the import's path opens that file |
|
|
101
102
|
| <kbd>e</kbd> or a double-click | edit the open file (see the editor keys below) |
|
|
@@ -130,7 +131,7 @@ In the editor:
|
|
|
130
131
|
| <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
|
|
131
132
|
| <kbd>Esc</kbd> | clear the selection, or leave the editor |
|
|
132
133
|
|
|
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
|
+
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` · `/zoom` · `/workspace [new <folder> | n]` · `/now` · `/showme <question>` · `/teach <change>` · `/help`
|
|
134
135
|
|
|
135
136
|
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.
|
|
136
137
|
|
package/dist/src/ui/App.js
CHANGED
|
@@ -30,6 +30,7 @@ import { ENABLE as MOUSE_ON, useMouse } from "./mouse.js";
|
|
|
30
30
|
import { connectToHost, isHosted, reopenAsWorkspaces, sendToHost, useWorkspaceTabs, workspaceTabs } from "../workspaces/client.js";
|
|
31
31
|
import { PTY_PACKAGE, workspaceFolder } from "../workspaces/host.js";
|
|
32
32
|
import { C, palette } from "./theme.js";
|
|
33
|
+
import { FIND_KEY, isFind } from "./keys.js";
|
|
33
34
|
import { FileTree } from "./FileTree.js";
|
|
34
35
|
import { CodeView } from "./CodeView.js";
|
|
35
36
|
import { Editor } from "./Editor.js";
|
|
@@ -83,7 +84,7 @@ Keys:
|
|
|
83
84
|
m mouse on/off (off: select text to copy; Shift+drag also works in most terminals)
|
|
84
85
|
/ start a command from any pane
|
|
85
86
|
e edit open file (or double-click in the code) v diff (git / Nou / session) f follow mode
|
|
86
|
-
|
|
87
|
+
${FIND_KEY} find in the open file (Enter/↓ next, ↑ previous)
|
|
87
88
|
b back to the previous file (Ctrl+P lists recent files first) [ ] previous/next tab · x close tab
|
|
88
89
|
Ctrl+click, Option+click or right-click a name: go to its definition (an import's path opens the file)
|
|
89
90
|
p problems u undo Nou's edit s settings
|
|
@@ -92,7 +93,7 @@ Mouse: wheel scrolls (Shift+wheel sideways) · click/drag a scrollbar · double-
|
|
|
92
93
|
In the editor:
|
|
93
94
|
Shift+arrows / mouse drag select · Ctrl+A all · Ctrl+C/X/V copy/cut/paste · Esc clear selection or leave
|
|
94
95
|
Ctrl+←/→ word · Ctrl+Backspace or Ctrl+W delete word · Ctrl+Home/End file start/end
|
|
95
|
-
|
|
96
|
+
${FIND_KEY} find · Ctrl+L go to line · Ctrl+/ comment · Ctrl+D duplicate line · Alt+↑/↓ move line
|
|
96
97
|
Tab/Shift+Tab indent/outdent · Ctrl+S save · Ctrl+Z/Y undo/redo · Ctrl+K info · Ctrl+G go to definition`;
|
|
97
98
|
/** A callback that keeps one identity across renders but always runs the latest code, so memoized panes don't re-render for it. */
|
|
98
99
|
function useEvent(fn) {
|
|
@@ -158,6 +159,14 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
158
159
|
*/
|
|
159
160
|
const [tour, setTour] = useState(null);
|
|
160
161
|
const tourFound = useRef(null);
|
|
162
|
+
/** The files on /showme's tour and which of its places are in each, for the file tree. */
|
|
163
|
+
const tourFiles = useMemo(() => {
|
|
164
|
+
if (!tour)
|
|
165
|
+
return null;
|
|
166
|
+
const map = new Map();
|
|
167
|
+
tour.stops.forEach((s, i) => map.set(s.file, [...(map.get(s.file) ?? []), i + 1]));
|
|
168
|
+
return map;
|
|
169
|
+
}, [tour]);
|
|
161
170
|
const shellRef = useRef(null);
|
|
162
171
|
const beforeTerm = useRef("code");
|
|
163
172
|
const [follow, setFollow] = useState(settings.followMode);
|
|
@@ -255,7 +264,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
255
264
|
const [findRequest, setFindRequest] = useState(0);
|
|
256
265
|
const startFind = () => {
|
|
257
266
|
if (!openFile)
|
|
258
|
-
return info(
|
|
267
|
+
return info(`Open a file first (Ctrl+P), then ${FIND_KEY} searches it.`);
|
|
259
268
|
setDiffMode("off");
|
|
260
269
|
setEditing(false);
|
|
261
270
|
setCodePanel("code");
|
|
@@ -270,6 +279,12 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
270
279
|
const layout = useRef({ treeWidth: 0, codeWidth: 0, paneHeight: 0, heights: { tree: 0, code: 0, chat: 0 }, game: null, terminal: null });
|
|
271
280
|
/** Pane widths while a border is dragged (saved on release); the saved ones otherwise. */
|
|
272
281
|
const [paneWidths, setPaneWidths] = useState(settings.paneWidths);
|
|
282
|
+
/**
|
|
283
|
+
* How far the code panel is zoomed (+ and - in the code, /zoom in|out): each step makes it a few columns
|
|
284
|
+
* wider (taken from the files and chat) or narrower (given to them). 0 is the normal layout.
|
|
285
|
+
*/
|
|
286
|
+
const [codeZoom, setCodeZoom] = useState(0);
|
|
287
|
+
const zoomCode = (by) => setCodeZoom((z) => (by === 0 ? 0 : Math.max(-6, Math.min(8, z + by))));
|
|
273
288
|
const paneWidthsRef = useRef(paneWidths);
|
|
274
289
|
paneWidthsRef.current = paneWidths;
|
|
275
290
|
/** What a mouse drag is moving: a pane border, a pane's scrollbar, or the editor's selection. */
|
|
@@ -1479,6 +1494,16 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1479
1494
|
return; // so does the find bar
|
|
1480
1495
|
if (focus === "game" || focus === "terminal")
|
|
1481
1496
|
return; // and the Games panel and the terminal
|
|
1497
|
+
// Cmd+F (Mac) or Ctrl+F from the files or the chat: find in the open file (the code view takes it itself
|
|
1498
|
+
// when it has the keyboard). Other Cmd+letters aren't Nou's: a plain letter is, so Cmd+F never also
|
|
1499
|
+
// toggles follow mode (f).
|
|
1500
|
+
if (isFind(input, key)) {
|
|
1501
|
+
if (focus !== "code")
|
|
1502
|
+
startFind();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
if (key.super)
|
|
1506
|
+
return;
|
|
1482
1507
|
if (key.tab && key.shift)
|
|
1483
1508
|
return cycleAutoMode();
|
|
1484
1509
|
if (key.tab && focus === "chat" && chatMenuOpen.current)
|
|
@@ -1526,9 +1551,13 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1526
1551
|
updateSettings({ paneWidths: moveBorder(focus === "tree" ? "tree" : "chat", (focus === "tree" ? treeWidth - 1 : treeWidth + codeWidth) + step) });
|
|
1527
1552
|
}
|
|
1528
1553
|
else if (input === "=") {
|
|
1554
|
+
setCodeZoom(0);
|
|
1529
1555
|
setPaneWidths(null);
|
|
1530
1556
|
updateSettings({ paneWidths: null });
|
|
1531
1557
|
}
|
|
1558
|
+
else if ((input === "+" || input === "-") && focus === "code") {
|
|
1559
|
+
zoomCode(input === "+" ? 1 : -1);
|
|
1560
|
+
}
|
|
1532
1561
|
else if (input === "v")
|
|
1533
1562
|
cycleDiff();
|
|
1534
1563
|
else if ((input === "]" || input === "[") && diffMode === "session")
|
|
@@ -1619,7 +1648,7 @@ ${snippet}
|
|
|
1619
1648
|
if (action)
|
|
1620
1649
|
return info("Usage: /mcp, /mcp add <name> <command|url>, /mcp add browser, /mcp remove <name>, /mcp import, /mcp reconnect", "error");
|
|
1621
1650
|
const list = mcpRef.current.status();
|
|
1622
|
-
const mark = { ready: "●", connecting: "◌", failed: "
|
|
1651
|
+
const mark = { ready: "●", connecting: "◌", failed: "✗", off: "○" };
|
|
1623
1652
|
const lines = list.map((x) => ` ${mark[x.state]} ${x.name.padEnd(16)} ${x.state === "ready" ? `${x.tools} tools` : x.state}${x.scope === "project" ? " · this project" : ""}${x.error ? ` · ${x.error}` : ""}`);
|
|
1624
1653
|
const others = findOtherMcpServers().filter((f) => !list.some((x) => x.name === f.name));
|
|
1625
1654
|
return info([
|
|
@@ -1656,7 +1685,7 @@ ${snippet}
|
|
|
1656
1685
|
const mark = { on: "●", off: "○", changed: "⚠" };
|
|
1657
1686
|
const lines = allPlugins.map((p) => p.scope === "starter"
|
|
1658
1687
|
? ` + ${p.id.padEnd(16)} ${p.plugin?.description ?? ""}`
|
|
1659
|
-
: ` ${mark[trustState(p, settings)]} ${p.id.padEnd(16)} ${p.scope}${p.error ? ` ·
|
|
1688
|
+
: ` ${mark[trustState(p, settings)]} ${p.id.padEnd(16)} ${p.scope}${p.error ? ` · ✗ ${p.error}` : ""}`);
|
|
1660
1689
|
return info(`Plugins (● on, ○ off, + starter you can add with /plugins add <name>):\n${lines.join("\n")}\nWrite your own: /plugins new <name>`);
|
|
1661
1690
|
};
|
|
1662
1691
|
const runCommand = async (input) => {
|
|
@@ -1819,6 +1848,14 @@ ${snippet}
|
|
|
1819
1848
|
return toggleTerminal();
|
|
1820
1849
|
case "redraw":
|
|
1821
1850
|
return redraw();
|
|
1851
|
+
case "zoom":
|
|
1852
|
+
if (/^(in|\+|bigger)$/.test(arg))
|
|
1853
|
+
return zoomCode(1);
|
|
1854
|
+
if (/^(out|-|smaller)$/.test(arg))
|
|
1855
|
+
return zoomCode(-1);
|
|
1856
|
+
if (/^(reset|0|off)$/.test(arg))
|
|
1857
|
+
return zoomCode(0);
|
|
1858
|
+
return info("/zoom in makes the code panel bigger, /zoom out smaller, /zoom reset puts it back (or + and - with the code focused, = to reset). The files and chat give or take the room. For bigger text, use your terminal's own zoom (Cmd/Ctrl and +).");
|
|
1822
1859
|
case "workspace":
|
|
1823
1860
|
case "workspaces":
|
|
1824
1861
|
case "ws":
|
|
@@ -1973,7 +2010,7 @@ ${snippet}
|
|
|
1973
2010
|
if (!stops.length)
|
|
1974
2011
|
return;
|
|
1975
2012
|
const list = stops.map((s, i) => `${i + 1}. ${s.file}:${s.line}${s.end > s.line ? `-${s.end}` : ""}${s.note ? ` · ${s.note}` : ""}`).join("\n");
|
|
1976
|
-
info(`📍 ${stops.length === 1 ? "Here it is" : `${stops.length} places`}, open in the code view (click one to jump there${stops.length > 1 ? "; /showme next and /showme back step through them" : ""}):\n${list}`);
|
|
2013
|
+
info(`📍 ${stops.length === 1 ? "Here it is" : `${stops.length} places`}, open in the code view (click one to jump there${stops.length > 1 ? "; /showme next and /showme back step through them" : ""}; /showme stop clears the highlights):\n${list}`);
|
|
1977
2014
|
setTour({ stops, at: 0 });
|
|
1978
2015
|
}, [busy]);
|
|
1979
2016
|
// Each place: open it in the code view with its lines highlighted.
|
|
@@ -1986,9 +2023,12 @@ ${snippet}
|
|
|
1986
2023
|
/** /showme <question>: explains it and opens the places in the code; /showme next|back|stop steps through them. */
|
|
1987
2024
|
const showPlaces = (arg) => {
|
|
1988
2025
|
const words = arg.trim();
|
|
1989
|
-
if (/^(stop|end|done)$/.test(words)) {
|
|
2026
|
+
if (/^(stop|end|done|clear)$/.test(words)) {
|
|
2027
|
+
// Done looking: the highlighted lines go back to plain code.
|
|
1990
2028
|
setTour(null);
|
|
1991
|
-
|
|
2029
|
+
const highlighted = !!focusLine?.to;
|
|
2030
|
+
setFocusLine((f) => (f?.to !== undefined ? null : f));
|
|
2031
|
+
return info(tour || highlighted ? "Done showing: the highlights are cleared." : "Nothing is being shown.");
|
|
1992
2032
|
}
|
|
1993
2033
|
if (/^(next|back|prev|previous)$/.test(words)) {
|
|
1994
2034
|
if (!tour)
|
|
@@ -2239,8 +2279,16 @@ ${snippet}
|
|
|
2239
2279
|
} }));
|
|
2240
2280
|
}
|
|
2241
2281
|
const height = Math.max(10, rows - 2);
|
|
2242
|
-
const
|
|
2243
|
-
const
|
|
2282
|
+
const baseTree = paneWidths ? Math.max(MIN_TREE, Math.round(columns * paneWidths.tree)) : Math.max(24, Math.floor(columns * 0.2));
|
|
2283
|
+
const baseChat = paneWidths ? Math.max(MIN_CHAT, Math.min(Math.round(columns * paneWidths.chat), columns - baseTree - MIN_CODE)) : Math.max(34, Math.floor(columns * 0.3));
|
|
2284
|
+
// Zooming the code panel: each step moves a few columns between it and the files and chat, as far as their
|
|
2285
|
+
// minimums (zooming in) or its own (zooming out) allow.
|
|
2286
|
+
const zoomCols = codeZoom * Math.max(4, Math.round(columns * 0.06));
|
|
2287
|
+
const spare = { tree: baseTree - MIN_TREE, chat: baseChat - MIN_CHAT };
|
|
2288
|
+
const taken = zoomCols > 0 ? Math.min(zoomCols, spare.tree + spare.chat) : -Math.min(-zoomCols, Math.max(0, columns - baseTree - baseChat - MIN_CODE));
|
|
2289
|
+
const fromTree = zoomCols > 0 ? Math.round(taken * (spare.tree / Math.max(1, spare.tree + spare.chat))) : Math.round(taken / 2);
|
|
2290
|
+
const treeWidth = baseTree - fromTree;
|
|
2291
|
+
const chatWidth = baseChat - (taken - fromTree);
|
|
2244
2292
|
const codeWidth = Math.max(MIN_CODE, columns - treeWidth - chatWidth);
|
|
2245
2293
|
const tabKey = tabs.length && openFile ? `${tabs.join("\0")}|${openFile}|${tabsWidth(codeWidth)}|${showEditor && !!editorState?.dirty}` : "";
|
|
2246
2294
|
if (tabStripCache.current?.key !== tabKey) {
|
|
@@ -2376,7 +2424,7 @@ ${snippet}
|
|
|
2376
2424
|
// Everything Nou can do, for the command palette (? or Ctrl+P then >), with the key that does it.
|
|
2377
2425
|
const paletteActions = [
|
|
2378
2426
|
{ title: "Find a file", keys: "Ctrl+P", run: () => setQuickOpen("") },
|
|
2379
|
-
{ title: "Find in the open file", keys:
|
|
2427
|
+
{ title: "Find in the open file", keys: FIND_KEY, run: startFind },
|
|
2380
2428
|
{ title: "Open a file at a line", keys: "/open", run: () => typeCommand("/open ") },
|
|
2381
2429
|
{ title: "Go back to the previous file", keys: "b", run: goBack },
|
|
2382
2430
|
{ title: "Cancel your last message (edit it and resend)", keys: "/cancel", run: cancelLast },
|
|
@@ -2442,7 +2490,7 @@ ${snippet}
|
|
|
2442
2490
|
: showEditor && focus === "code"
|
|
2443
2491
|
? editorState?.selection
|
|
2444
2492
|
? [["Ctrl+C", "copy"], ["Ctrl+X", "cut"], ["Tab", "indent"], ["Ctrl+/", "comment"], ["Esc", "deselect"]]
|
|
2445
|
-
: [["Esc", "done"], ["Ctrl+S", settings.autoSave === "off" ? "save" : "save (auto-save on)"], ["Ctrl+Z", "undo"], ["Shift+→", "select"], [
|
|
2493
|
+
: [["Esc", "done"], ["Ctrl+S", settings.autoSave === "off" ? "save" : "save (auto-save on)"], ["Ctrl+Z", "undo"], ["Shift+→", "select"], [FIND_KEY, "find"], ["Ctrl+/", "comment"], ["/help", "all keys"]]
|
|
2446
2494
|
: focus === "chat"
|
|
2447
2495
|
? chatMenu ? [["↑↓", "choose"], ["Tab", "complete"], ["Enter", "run"], ["Esc", "close"]]
|
|
2448
2496
|
: busy
|
|
@@ -2452,9 +2500,9 @@ ${snippet}
|
|
|
2452
2500
|
? [["Enter", "open"], ["e", "edit"], ["/", "command"], ["c", "changes"], ["Ctrl+P", "find"], ["u", "undo"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]]
|
|
2453
2501
|
: diff
|
|
2454
2502
|
? [["Esc", "close diff"], ["n/N", "next/prev change"], ...(diffMode === "session" ? [["[ ]", "prev/next file"]] : []), ["v", "cycle diff"]]
|
|
2455
|
-
: [["double-click/e", "edit"], [
|
|
2503
|
+
: [["double-click/e", "edit"], [FIND_KEY, "find"], ["v", "diff"], ...(recent.length > 1 ? [["b", "back"]] : []), ["u", "undo"], ["/", "command"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]];
|
|
2456
2504
|
const followChip = follow ? ` ◉ follow${followLag ? ` ·${followLag}` : ""} ` : " ○ follow ";
|
|
2457
|
-
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + followChip.length + `
|
|
2505
|
+
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + followChip.length + ` ✗ ${totals.errors} ▲ ${totals.warnings} `.length + (checking.length ? 4 + checking.join(", ").length : 0) + 2;
|
|
2458
2506
|
const shownHints = [];
|
|
2459
2507
|
for (let used = statusUsed, i = 0; i < hints.length; i++) {
|
|
2460
2508
|
used += hints[i][0].length + hints[i][1].length + 4;
|
|
@@ -2467,7 +2515,7 @@ ${snippet}
|
|
|
2467
2515
|
y: () => answer(true), n: approval ? () => answer(false) : undefined, o: openPending,
|
|
2468
2516
|
"Shift+Tab": () => cycleAutoMode(), "/": () => typeCommand("/"), "Ctrl+P": () => setQuickOpen(""),
|
|
2469
2517
|
Tab: () => setFocus((f) => FOCUS_ORDER[(FOCUS_ORDER.indexOf(f) + 1) % FOCUS_ORDER.length]),
|
|
2470
|
-
e: startEditing, "double-click/e": startEditing, b: goBack,
|
|
2518
|
+
e: startEditing, "double-click/e": startEditing, b: goBack, [FIND_KEY]: showEditor ? undefined : startFind, v: cycleDiff, u: undoLast, p: toggleProblems, m: toggleMouse,
|
|
2471
2519
|
s: () => setView("settings"), "/help": () => void runCommand("/help"),
|
|
2472
2520
|
Esc: busy ? stop : diff && !showEditor ? () => setDiffMode("off") : undefined,
|
|
2473
2521
|
};
|
|
@@ -2481,7 +2529,7 @@ ${snippet}
|
|
|
2481
2529
|
};
|
|
2482
2530
|
add(` ${autoMode} `.length, () => cycleAutoMode());
|
|
2483
2531
|
add(followChip.length, () => setFollow((f) => !f));
|
|
2484
|
-
add(`
|
|
2532
|
+
add(` ✗ ${totals.errors} ▲ ${totals.warnings} `.length, toggleProblems);
|
|
2485
2533
|
if (checking.length)
|
|
2486
2534
|
add(3 + checking.join(", ").length);
|
|
2487
2535
|
if (!exitArmed) {
|
|
@@ -2492,8 +2540,8 @@ ${snippet}
|
|
|
2492
2540
|
}
|
|
2493
2541
|
}
|
|
2494
2542
|
const modelLabel = agentRef.current.id === "nou" ? describe(choice) : `${engineInfo(agentRef.current.id).label} · ${engineInfo(agentRef.current.id).model(choice)}`;
|
|
2495
|
-
clickable.current = { status: zones, modelFrom: columns - ("
|
|
2496
|
-
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 }), _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, git: git, problemCounts: problemCounts, wheel: wheelFor("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, wheel: wheelFor("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, wheel: wheelFor("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, { ...(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, wheel: wheelFor("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, wheel: wheelFor("chat"), scrollTo: scrollFor("chat"), prefill: chatPrefill, extraCommands: pluginCommands, onMenu: onChatMenu, onSubmit: onSend, queued: queued, sendNowFirst: settings.sendNowWhileBusy, onSendNow: sendNow, 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: `
|
|
2543
|
+
clickable.current = { status: zones, modelFrom: columns - (" LOCAL ".length + ` ${modelLabel} `.length + 1) };
|
|
2544
|
+
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 }), _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, wheel: wheelFor("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, wheel: wheelFor("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, wheel: wheelFor("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, wheel: wheelFor("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, wheel: wheelFor("chat"), scrollTo: scrollFor("chat"), prefill: chatPrefill, extraCommands: pluginCommands, onMenu: onChatMenu, onSubmit: onSend, queued: queued, sendNowFirst: settings.sendNowWhileBusy, onSendNow: sendNow, 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
|
|
2497
2545
|
? _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)` : ""] })
|
|
2498
2546
|
: 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", wheel: wheelFor("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: "Pick a conversation to resume (type to filter)" } : {}), onPick: (file) => {
|
|
2499
2547
|
setQuickOpen(null);
|
package/dist/src/ui/Chat.js
CHANGED
|
@@ -63,7 +63,7 @@ function entryLines(entry, inner, links = true) {
|
|
|
63
63
|
else {
|
|
64
64
|
const style = entry.kind === "tool"
|
|
65
65
|
? { icon: "⚙", color: C.dim }
|
|
66
|
-
: entry.kind === "error" ? { icon: "
|
|
66
|
+
: entry.kind === "error" ? { icon: "✗", color: C.red } : { icon: "ℹ", color: C.accent };
|
|
67
67
|
wrap(entry.text, inner - 4).forEach((line, i) => {
|
|
68
68
|
const spans = links && entry.kind !== "user" ? linkFileRefs(linkUrls([{ text: ` ${line}` }]), isProjectFile) : [];
|
|
69
69
|
out.push({ text: ` ${line}`, color: style.color, lead: i === 0 ? ` ${style.icon}` : " ", leadColor: style.color, ...(spans.some((sp) => sp.link) ? { spans } : {}) });
|
package/dist/src/ui/CodeView.js
CHANGED
|
@@ -5,6 +5,7 @@ import { SEVERITY_STYLE } from "./ProblemsView.js";
|
|
|
5
5
|
import { artWork } from "../util/index.js";
|
|
6
6
|
import { foldRanges, highlightLines, markAnsi, sideBySide, sliceAnsi, splitLines, unfoldedLines } from "./text.js";
|
|
7
7
|
import { C, fileIcon, gradient } from "./theme.js";
|
|
8
|
+
import { isFind } from "./keys.js";
|
|
8
9
|
import { Pane } from "./Pane.js";
|
|
9
10
|
import { Scrollbar, topFor } from "./scroll.js";
|
|
10
11
|
/** The current match: a yellow background; the others are shown inverted. */
|
|
@@ -12,7 +13,7 @@ const CURRENT_MATCH = ["\x1b[43m\x1b[30m", "\x1b[49m\x1b[39m"];
|
|
|
12
13
|
/** Columns ←/→ and Shift+wheel move sideways. */
|
|
13
14
|
const SIDE_STEP = 8;
|
|
14
15
|
const EDIT_HINT = " double-click or e to edit · v diff";
|
|
15
|
-
export const CodeView = memo(function CodeView({ width, height, focused, filePath, content, diff, diffLabel, pending, marked, problems, focusLine, syntaxVersion, diffNote, wheel, scrollTo, onViewport, defineAt, onDefine, tabs, onFinding, findRequest, foldClick, foldAll }) {
|
|
16
|
+
export const CodeView = memo(function CodeView({ showing, width, height, focused, filePath, content, diff, diffLabel, pending, marked, problems, focusLine, syntaxVersion, diffNote, wheel, scrollTo, onViewport, defineAt, onDefine, tabs, onFinding, findRequest, foldClick, foldAll }) {
|
|
16
17
|
const [top, setTop] = useState(0);
|
|
17
18
|
// Columns scrolled sideways (colors are kept: see sliceAnsi).
|
|
18
19
|
const [left, setLeft] = useState(0);
|
|
@@ -165,7 +166,7 @@ export const CodeView = memo(function CodeView({ width, height, focused, filePat
|
|
|
165
166
|
return setFind((f) => f && { ...f, index: f.index - 1 });
|
|
166
167
|
if (key.backspace || key.delete)
|
|
167
168
|
return setFind((f) => f && { query: f.query.slice(0, -1), index: 0 });
|
|
168
|
-
if (
|
|
169
|
+
if (isFind(input, key))
|
|
169
170
|
return;
|
|
170
171
|
if (input && !key.ctrl && !key.meta && input >= " ")
|
|
171
172
|
setFind((f) => f && { query: f.query + input, index: 0 });
|
|
@@ -173,7 +174,7 @@ export const CodeView = memo(function CodeView({ width, height, focused, filePat
|
|
|
173
174
|
useInput((input, key) => {
|
|
174
175
|
if (find)
|
|
175
176
|
return;
|
|
176
|
-
if (
|
|
177
|
+
if (isFind(input, key) && !diff)
|
|
177
178
|
return setFind({ query: "", index: 0 });
|
|
178
179
|
if (key.upArrow || input === "k")
|
|
179
180
|
setTop((t) => Math.max(0, Math.min(t, maxTop) - 1));
|
|
@@ -238,8 +239,8 @@ export const CodeView = memo(function CodeView({ width, height, focused, filePat
|
|
|
238
239
|
const warningCount = problems.filter((d) => d.severity === "warning").length;
|
|
239
240
|
// The edit hint only when it fits: a cut-off hint reads as noise.
|
|
240
241
|
const titleRoom = tabs ? 0 : width - 4 - (3 + filePath.length + (errorCount ? 5 + String(errorCount).length : 0) + (warningCount ? 5 + String(warningCount).length : 0) + scroll.length + (total > rows ? 7 : 0));
|
|
241
|
-
const title = (_jsxs(_Fragment, { children: [tabs ?? _jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath] }), errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \
|
|
242
|
-
return (_jsxs(Pane, { title: title, width: width, height: height, focused: focused, children: [_jsxs(Box, { flexDirection: "row", height: rows, children: [_jsx(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: shown.slice(start, start + rows).map((n) => {
|
|
242
|
+
const title = (_jsxs(_Fragment, { children: [tabs ?? _jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath] }), showing && _jsxs(_Fragment, { children: [" ", _jsx(Text, { backgroundColor: C.accent2, color: C.onBadge, bold: true, children: ` ◉ SHOWING ${showing} ` })] }), errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \u2717 ", errorCount] }), warningCount > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", warningCount] }), _jsxs(Text, { color: C.dim, children: [scroll, total > rows ? ` (${percent}%)` : "", focused && titleRoom >= EDIT_HINT.length ? EDIT_HINT : ""] })] }));
|
|
243
|
+
return (_jsxs(Pane, { title: title, width: width, height: height, focused: focused, ...(showing ? { accent: C.accent2 } : {}), children: [_jsxs(Box, { flexDirection: "row", height: rows, children: [_jsx(Box, { flexDirection: "column", width: inner, flexShrink: 0, children: shown.slice(start, start + rows).map((n) => {
|
|
243
244
|
const line = lines[n - 1] ?? "";
|
|
244
245
|
const fold = ranges.get(n);
|
|
245
246
|
const isFolded = fold !== undefined && folded.has(n);
|
package/dist/src/ui/Editor.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path";
|
|
|
4
4
|
import { Box, Text, useInput, usePaste } from "ink";
|
|
5
5
|
import { highlightLines, sliceAnsi } from "./text.js";
|
|
6
6
|
import { C, fileIcon } from "./theme.js";
|
|
7
|
+
import { FIND_KEY, isFind } from "./keys.js";
|
|
7
8
|
import { SEVERITY_STYLE } from "./ProblemsView.js";
|
|
8
9
|
import { Pane } from "./Pane.js";
|
|
9
10
|
import { Scrollbar, topFor } from "./scroll.js";
|
|
@@ -401,7 +402,7 @@ export const Editor = memo(function Editor({ width, height, focused, filePath, i
|
|
|
401
402
|
return find(prompt.text, key.upArrow);
|
|
402
403
|
if (key.backspace || key.delete)
|
|
403
404
|
return setPrompt({ ...prompt, text: prompt.text.slice(0, -1) });
|
|
404
|
-
if (
|
|
405
|
+
if (isFind(input, key))
|
|
405
406
|
return find(prompt.text);
|
|
406
407
|
if (input && !key.ctrl && !key.meta && (prompt.kind === "find" || /^\d+$/.test(input))) {
|
|
407
408
|
const text = prompt.text + input;
|
|
@@ -433,7 +434,7 @@ export const Editor = memo(function Editor({ width, height, focused, filePath, i
|
|
|
433
434
|
return step(undo, redo);
|
|
434
435
|
if (key.ctrl && input === "y")
|
|
435
436
|
return step(redo, undo);
|
|
436
|
-
if (
|
|
437
|
+
if (isFind(input, key)) {
|
|
437
438
|
const word = selectedText(buf);
|
|
438
439
|
return setPrompt({ kind: "find", text: word.includes("\n") ? "" : word });
|
|
439
440
|
}
|
|
@@ -631,7 +632,7 @@ export const Editor = memo(function Editor({ width, height, focused, filePath, i
|
|
|
631
632
|
? prompt.kind === "find" ? `Find: ${prompt.text}▏ Enter/↓ next · Shift+Enter/↑ previous · Esc close` : `Go to line: ${prompt.text}▏ Enter go · Esc cancel`
|
|
632
633
|
: flash ?? (cursorProblem
|
|
633
634
|
? `${SEVERITY_STYLE[cursorProblem.severity].icon} ${cursorProblem.message} (${cursorProblem.source})`
|
|
634
|
-
: `Ln ${buf.row + 1}, Col ${buf.col + 1}${sel ? ` (${selectedCount} selected · Ctrl+C copy · Ctrl+X cut)` : ""}${dirty ? " ● modified" : ""}
|
|
635
|
+
: `Ln ${buf.row + 1}, Col ${buf.col + 1}${sel ? ` (${selectedCount} selected · Ctrl+C copy · Ctrl+X cut)` : ""}${dirty ? " ● modified" : ""} ${FIND_KEY} find · Ctrl+/ comment · Esc done`);
|
|
635
636
|
const statusColor = guideStatus && !confirmExit && !prompt ? (progress < 0 ? C.red : C.accent2) : confirmExit ? C.red : prompt ? C.accent : flash ? C.green : cursorProblem ? SEVERITY_STYLE[cursorProblem.severity].color : C.dim;
|
|
636
637
|
const hoverLines = hover ? hover.split("\n").filter((l) => l.trim()).slice(0, 6) : [];
|
|
637
638
|
const cursorVisible = buf.row >= v.top && buf.row < v.top + rows;
|
package/dist/src/ui/FileTree.js
CHANGED
|
@@ -32,7 +32,7 @@ function flatten(expanded) {
|
|
|
32
32
|
walk(".", 0);
|
|
33
33
|
return nodes;
|
|
34
34
|
}
|
|
35
|
-
export const FileTree = memo(function FileTree({ width, height, focused, openFile, reveal, version, changed, working, git, problemCounts, wheel, scrollTo, click, onOpen, onHover }) {
|
|
35
|
+
export const FileTree = memo(function FileTree({ width, height, focused, openFile, reveal, version, changed, working, showing, git, problemCounts, wheel, scrollTo, click, onOpen, onHover }) {
|
|
36
36
|
const [expanded, setExpanded] = useState(() => new Set(["src"]));
|
|
37
37
|
const [cursor, setCursor] = useState(0);
|
|
38
38
|
// The first row shown. The wheel moves it on its own; the keyboard moves the cursor and the view follows.
|
|
@@ -182,11 +182,12 @@ export const FileTree = memo(function FileTree({ width, height, focused, openFil
|
|
|
182
182
|
? { icon: expanded.has(node.path) ? "▾" : "▸", color: C.accent }
|
|
183
183
|
: fileIcon(node.path);
|
|
184
184
|
const counts = problemCounts.get(node.path);
|
|
185
|
-
const problemMark = counts?.errors ? "
|
|
186
|
-
const
|
|
187
|
-
const
|
|
185
|
+
const problemMark = counts?.errors ? "✗" : counts?.warnings ? "▲" : "";
|
|
186
|
+
const stops = showing?.get(node.path);
|
|
187
|
+
const marker = stops ? `◉${stops.join(",")}` : problemMark || code || (changed.has(node.path) ? "●" : dirtyDirs.has(node.path) ? "•" : "");
|
|
188
|
+
const markerColor = stops ? C.accent2 : counts?.errors ? C.red : counts?.warnings ? C.yellow : code ? GIT_COLORS[code] ?? C.yellow : changed.has(node.path) ? C.green : C.yellow;
|
|
188
189
|
const label = `${" ".repeat(node.depth)}${icon} ${node.name}`;
|
|
189
|
-
const nameColor = isOpen ? C.cyan : counts?.errors ? C.red : code ? GIT_COLORS[code] : node.isDir ? C.text : undefined;
|
|
190
|
+
const nameColor = stops ? C.accent2 : isOpen ? C.cyan : counts?.errors ? C.red : code ? GIT_COLORS[code] : node.isDir ? C.text : undefined;
|
|
190
191
|
const room = Math.max(1, inner - marker.length - 1);
|
|
191
192
|
const shown = label.length > room ? `${label.slice(0, room - 1)}…` : label.padEnd(room);
|
|
192
193
|
const iconAt = shown.indexOf(icon);
|
package/dist/src/ui/Header.js
CHANGED
|
@@ -29,12 +29,23 @@ export function headerZones(tabs) {
|
|
|
29
29
|
zones.push({ from: x, to: x + 3, tab: "new" });
|
|
30
30
|
return zones;
|
|
31
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The header's right side: the update notice (when there's room), where the model runs and which. Plain
|
|
34
|
+
* characters only: symbols that are also emoji (☁, ⬆) are drawn two columns wide by some terminals.
|
|
35
|
+
*/
|
|
36
|
+
const rightSide = (width, model, local, update) => ({
|
|
37
|
+
notice: update && width >= 110 ? `↑ v${update} available · run \`nou update\` ` : "",
|
|
38
|
+
badge: ` ${local ? "LOCAL" : "CLOUD"} `,
|
|
39
|
+
model: ` ${model} `,
|
|
40
|
+
});
|
|
32
41
|
export const Header = memo(function Header({ width, branch, changes, model, local, busy, update, tabs = [] }) {
|
|
33
42
|
// The spinner stops while Nou's window is in the background: nobody sees it, and each frame is a redraw.
|
|
34
43
|
const windowFocused = useTerminalFocused();
|
|
35
44
|
const { frame } = useAnimation({ interval: ANIMATION_MS, isActive: !!busy && windowFocused });
|
|
36
45
|
const project = path.basename(process.cwd());
|
|
37
|
-
|
|
46
|
+
const right = rightSide(width, model, local, update);
|
|
47
|
+
const rightWidth = right.notice.length + right.badge.length + right.model.length + 1;
|
|
48
|
+
return (_jsxs(Box, { width: width, justifyContent: "space-between", children: [_jsx(Box, { width: Math.max(0, width - rightWidth - 4), flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate-end", children: [" ", gradient("◆ NOU", busy ? frame : 0).map((g, i) => _jsx(Text, { bold: true, color: g.color, children: g.ch }, i)), _jsxs(Text, { color: C.dim, children: [" ", VERSION] }), _jsx(Text, { color: C.dim, children: " \u2502 " }), tabs.length
|
|
38
49
|
? tabs.map((t, i) => (_jsxs(Text, { children: [_jsx(Text, { ...(t.active ? { backgroundColor: C.panel } : {}), color: t.active ? C.accent : C.text, bold: t.active, children: tabLabel(t, i) }), " "] }, t.id)))
|
|
39
|
-
: _jsxs(Text, { color: C.text, children: [project, " "] }), _jsx(Text, { color: C.accent2, children: " + " }), branch && (_jsxs(_Fragment, { children: [_jsx(Text, { color: C.dim, children: " \u2502 " }), _jsxs(Text, { color: C.accent2, children: ["\u2387 ", branch] }), changes > 0 && _jsxs(Text, { color: C.yellow, children: [" \u00B1", changes] })] }))] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate-end", children: [
|
|
50
|
+
: _jsxs(Text, { color: C.text, children: [project, " "] }), _jsx(Text, { color: C.accent2, children: " + " }), branch && (_jsxs(_Fragment, { children: [_jsx(Text, { color: C.dim, children: " \u2502 " }), _jsxs(Text, { color: C.accent2, children: ["\u2387 ", branch] }), changes > 0 && _jsxs(Text, { color: C.yellow, children: [" \u00B1", changes] })] }))] }) }), _jsx(Box, { flexShrink: 0, children: _jsxs(Text, { wrap: "truncate-end", children: [right.notice && _jsx(Text, { color: C.green, bold: true, children: right.notice }), _jsx(Text, { backgroundColor: local ? C.green : C.accent2, color: C.onBadge, bold: true, children: right.badge }), _jsx(Text, { backgroundColor: C.panel, color: C.text, children: right.model }), " "] }) })] }));
|
|
40
51
|
});
|
|
@@ -5,7 +5,7 @@ import { C, SPINNER, fileIcon, ANIMATION_MS } from "./theme.js";
|
|
|
5
5
|
import { useTerminalFocused } from "./focus.js";
|
|
6
6
|
import { Pane } from "./Pane.js";
|
|
7
7
|
export const SEVERITY_STYLE = {
|
|
8
|
-
error: { icon: "
|
|
8
|
+
error: { icon: "✗", color: C.red },
|
|
9
9
|
warning: { icon: "▲", color: C.yellow },
|
|
10
10
|
info: { icon: "ℹ", color: C.accent },
|
|
11
11
|
};
|
|
@@ -63,13 +63,13 @@ export function ProblemsView({ width, height, focused, problems, checking, wheel
|
|
|
63
63
|
const errors = problems.filter((d) => d.severity === "error").length;
|
|
64
64
|
const warnings = problems.filter((d) => d.severity === "warning").length;
|
|
65
65
|
const start = Math.max(0, Math.min(cursor - Math.floor(visibleRows / 2), rows.length - visibleRows));
|
|
66
|
-
const title = (_jsxs(_Fragment, { children: [" ", _jsxs(Text, { color: C.red, children: ["\
|
|
66
|
+
const title = (_jsxs(_Fragment, { children: [" ", _jsxs(Text, { color: C.red, children: ["\u2717 ", errors] }), " ", _jsxs(Text, { color: C.yellow, children: ["\u25B2 ", warnings] }), " Problems", checking.length > 0 && _jsxs(Text, { color: C.accent2, children: [" ", SPINNER[frame % SPINNER.length], " checking ", checking.join(", ")] }), _jsx(Text, { color: C.dim, children: onlyErrors ? " (errors only)" : "" })] }));
|
|
67
67
|
return (_jsxs(Pane, { title: title, width: width, height: height, focused: focused, ...(focused ? { accent: C.red } : {}), children: [_jsxs(Box, { flexDirection: "column", height: visibleRows, children: [rows.length === 0 && (_jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { color: C.green, children: checking.length ? "Checking…" : "✓ No problems found" }), _jsx(Text, { color: C.dim, children: "Checked by the syntax parser, plus ESLint, TypeScript, Ruff and language servers when your project has them." })] })), rows.slice(start, start + visibleRows).map((row, i) => {
|
|
68
68
|
const selected = focused && start + i === cursor;
|
|
69
69
|
const bg = selected ? { backgroundColor: C.panel } : {};
|
|
70
70
|
if (row.kind === "file") {
|
|
71
71
|
const { icon, color } = fileIcon(row.file);
|
|
72
|
-
return (_jsxs(Text, { wrap: "truncate-end", ...bg, children: [_jsx(Text, { color: color, children: icon }), " ", _jsx(Text, { bold: true, color: C.text, children: row.file }), row.errors > 0 && _jsxs(Text, { color: C.red, children: [" \
|
|
72
|
+
return (_jsxs(Text, { wrap: "truncate-end", ...bg, children: [_jsx(Text, { color: color, children: icon }), " ", _jsx(Text, { bold: true, color: C.text, children: row.file }), row.errors > 0 && _jsxs(Text, { color: C.red, children: [" \u2717 ", row.errors] }), row.warnings > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", row.warnings] })] }, `f${row.file}`));
|
|
73
73
|
}
|
|
74
74
|
const style = SEVERITY_STYLE[row.d.severity];
|
|
75
75
|
return (_jsxs(Text, { wrap: "truncate-end", ...bg, children: [" ", _jsx(Text, { color: style.color, children: style.icon }), _jsxs(Text, { color: C.dim, children: [" ", String(row.d.line).padStart(4), ":", String(row.d.col).padEnd(3), " "] }), _jsx(Text, { ...(selected ? { color: C.text } : {}), children: row.d.message }), _jsxs(Text, { color: C.dim, children: [" ", row.d.source, row.d.code ? ` ${row.d.code}` : ""] })] }, `${row.d.file}:${row.d.line}:${row.d.col}:${i}`));
|
package/dist/src/ui/Settings.js
CHANGED
|
@@ -213,7 +213,7 @@ export function Settings({ width, height, settings, choice, plugins, connectTo,
|
|
|
213
213
|
const selected = i === row;
|
|
214
214
|
const current = p.id === choice.provider.id;
|
|
215
215
|
return (_jsxs(Text, { wrap: "truncate-end", ...(selected && column === "providers" ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: current ? C.green : C.dim, children: current ? " ● " : " " }), _jsx(Text, { color: problem ? C.dim : C.text, bold: selected, children: p.label.padEnd(labelWidth).slice(0, labelWidth) }), _jsx(Text, { color: p.local ? C.green : C.accent2, children: p.local ? " local" : " cloud" }), problem ? _jsx(Text, { color: C.dim, children: " \uD83D\uDD12" }) : null] }, p.id));
|
|
216
|
-
})] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: column === "models" ? C.accent : C.dim, paddingX: 1, children: [_jsxs(Text, { color: C.dim, bold: true, children: ["MODELS \u00B7 ", provider.label] }), reason ? _jsxs(Text, { color: C.red, wrap: "wrap", children: ["\
|
|
216
|
+
})] }), _jsxs(Box, { flexDirection: "column", flexGrow: 1, borderStyle: "round", borderColor: column === "models" ? C.accent : C.dim, paddingX: 1, children: [_jsxs(Text, { color: C.dim, bold: true, children: ["MODELS \u00B7 ", provider.label] }), reason ? _jsxs(Text, { color: C.red, wrap: "wrap", children: ["\u2717 ", reason] }) : null, connecting && provider.connect ? (_jsxs(_Fragment, { children: [_jsxs(Text, { color: C.accent2, bold: true, children: ["Connect ", provider.label] }), provider.connect.map((f, i) => {
|
|
217
217
|
const entered = connecting.values[f.env];
|
|
218
218
|
const shown = (v) => (f.secret ? "•".repeat(Math.min(v.length, 12)) : v);
|
|
219
219
|
if (i < connecting.step) {
|
|
@@ -286,7 +286,7 @@ export function Settings({ width, height, settings, choice, plugins, connectTo,
|
|
|
286
286
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { ...(selected ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { color: selected ? C.accent2 : C.dim, children: selected ? "▸ " : " " }), p.scope === "starter" ? _jsx(Text, { color: C.accent2, bold: true, children: "+ ADD" }) : toggle(enabled), _jsxs(Text, { color: C.text, bold: true, children: [" ", p.plugin?.name ?? p.id] }), _jsxs(Text, { color: C.dim, children: [" ", p.plugin?.version ? `v${p.plugin.version} · ` : "", p.scope === "starter" ? "starter, optional" : p.scope, parts.length ? ` · ${parts.join(", ")}` : ""] })] }), state === "changed"
|
|
287
287
|
? _jsx(Text, { color: C.yellow, wrap: "truncate-end", children: " \u26A0 Changed since you enabled it. Review the code, then press Enter to enable again." })
|
|
288
288
|
: p.error
|
|
289
|
-
? _jsxs(Text, { color: C.red, wrap: "truncate-end", children: [" \
|
|
289
|
+
? _jsxs(Text, { color: C.red, wrap: "truncate-end", children: [" \u2717 ", p.error] })
|
|
290
290
|
: _jsxs(Text, { color: C.dim, wrap: "truncate-end", children: [" ", p.plugin?.description ?? (enabled ? "" : "Enable to load it.")] })] }, p.id));
|
|
291
291
|
}), _jsx(Text, { children: " " }), _jsxs(Text, { color: C.dim, wrap: "truncate-end", children: ["Starters: Enter adds one to ", PLUGIN_DIRS.find((d) => d.scope === "user").dir, ", where you can edit it."] }), _jsxs(Text, { color: C.dim, wrap: "truncate-end", children: ["Write your own: /plugins new <name>, or drop a .js/.mjs file into ", PLUGIN_DIRS.map((d) => d.dir).join(" or "), ", then press r."] })] }));
|
|
292
292
|
};
|
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(["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(["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
|
|
@@ -39,7 +39,7 @@ function editDistance(a, b) {
|
|
|
39
39
|
export function runsWhileBusy(text) {
|
|
40
40
|
const [name, ...rest] = text.slice(1).trim().split(/\s+/);
|
|
41
41
|
if (name === "showme" || name === "teach" || name === "teachme")
|
|
42
|
-
return !rest.length || /^(next|skip|back|prev|previous|stop|end|done)$/.test(rest.join(" "));
|
|
42
|
+
return !rest.length || /^(next|skip|back|prev|previous|stop|end|done|clear)$/.test(rest.join(" "));
|
|
43
43
|
if (!text.startsWith("/") || !name)
|
|
44
44
|
return false;
|
|
45
45
|
return INSTANT.has(name) || INSTANT.has(closestCommand(name, SLASH_COMMANDS.map((c) => c.name)) ?? "");
|
|
@@ -53,6 +53,7 @@ export const SLASH_COMMANDS = [
|
|
|
53
53
|
{ name: "open", args: "<file>[:line]", description: "open a file in the code view (part of its name is enough)" },
|
|
54
54
|
{ name: "effort", args: "[fastest|fast|balanced|smarter|smartest]", description: "speed vs smarts: how hard the model thinks (cheaper + faster, or smarter)" },
|
|
55
55
|
{ name: "workspace", args: "[new <folder> | n]", description: "several projects in this window, a tab each in the header: open one, or switch" },
|
|
56
|
+
{ name: "zoom", args: "in|out|reset", description: "make the code panel bigger or smaller (+ and - with the code focused, = resets)" },
|
|
56
57
|
{ name: "now", description: "stop Nou and send your queued messages right away (or click them)" },
|
|
57
58
|
{ name: "settings", description: "models, engine, preferences and plugins" },
|
|
58
59
|
{ name: "auto", args: "[ask|auto-edit|auto]", description: "how much Nou does without asking" },
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keys named the way the platform names them. On a Mac, Find is Cmd+F: terminals that report the Cmd key
|
|
3
|
+
* (kitty's keyboard protocol, which Nou asks for: Ghostty, kitty, WezTerm, iTerm2) send it as "super". Ctrl+F
|
|
4
|
+
* works everywhere too, for terminals that keep Cmd for themselves.
|
|
5
|
+
*/
|
|
6
|
+
export const FIND_KEY = process.platform === "darwin" ? "Cmd+F" : "Ctrl+F";
|
|
7
|
+
/** Ctrl+F, or Cmd+F where the terminal reports Cmd. */
|
|
8
|
+
export const isFind = (input, key) => input === "f" && (!!key.ctrl || !!key.super);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.11",
|
|
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": {
|