nou-cli 0.5.0 → 0.5.1
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 +2 -2
- package/dist/src/ui/App.js +20 -9
- package/dist/src/ui/CodeView.js +1 -1
- package/dist/src/ui/Editor.js +20 -4
- package/dist/src/ui/mouse.js +5 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,13 +76,13 @@ Every change is reviewed like a code review:
|
|
|
76
76
|
|
|
77
77
|
| Key | Does |
|
|
78
78
|
| --- | --- |
|
|
79
|
-
| 🖱️ Mouse | wheel scrolls the pane under the pointer (<kbd>Shift</kbd>+wheel or a tilt wheel: sideways) · click or drag a scrollbar to jump · click a file to open it · **click in the code to edit right there
|
|
79
|
+
| 🖱️ Mouse | wheel scrolls the pane under the pointer (<kbd>Shift</kbd>+wheel or a tilt wheel: sideways) · click or drag a scrollbar to jump · click a file to open it · **double-click in the code to edit right there** (in the editor: select a word), drag to select · drag a border between panes to resize them (press <kbd>m</kbd> to turn the mouse off and select text in your terminal) |
|
|
80
80
|
| <kbd>PgUp</kbd>/<kbd>PgDn</kbd>, <kbd>Shift</kbd>+<kbd>↑</kbd>/<kbd>↓</kbd> | scroll the chat; scrolled up, new replies wait below instead of yanking the view (<kbd>Ctrl</kbd>+<kbd>End</kbd> jumps back) |
|
|
81
81
|
| <kbd>/</kbd> | start a command from any pane |
|
|
82
82
|
| <kbd>Tab</kbd> | switch between files, code and chat |
|
|
83
83
|
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | ask → auto-edit (edits apply without asking) → auto (commands too); everything stays undoable |
|
|
84
84
|
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file |
|
|
85
|
-
| <kbd>e</kbd> or a click | edit the open file (see the editor keys below) |
|
|
85
|
+
| <kbd>e</kbd> or a double-click | edit the open file (see the editor keys below) |
|
|
86
86
|
| <kbd>p</kbd> | Problems panel: every error and warning, <kbd>Enter</kbd> jumps to it |
|
|
87
87
|
| <kbd>u</kbd> | undo Nou's last edit |
|
|
88
88
|
| <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 |
|
package/dist/src/ui/App.js
CHANGED
|
@@ -34,6 +34,7 @@ const MIN_TREE = 14;
|
|
|
34
34
|
const MIN_CODE = 20;
|
|
35
35
|
const MIN_CHAT = 24;
|
|
36
36
|
const NO_LINES = new Set();
|
|
37
|
+
const DOUBLE_CLICK_MS = 500;
|
|
37
38
|
const HELP = `Commands:
|
|
38
39
|
/settings models, preferences and plugins
|
|
39
40
|
/model show providers and the current model
|
|
@@ -60,10 +61,10 @@ Keys:
|
|
|
60
61
|
Shift+Tab ask / auto-edit / auto < > resize pane = reset
|
|
61
62
|
m mouse on/off (off: select text to copy; Shift+drag also works in most terminals)
|
|
62
63
|
/ start a command from any pane
|
|
63
|
-
e edit open file (or click in the code) v diff (git / Nou / session) f follow mode
|
|
64
|
+
e edit open file (or double-click in the code) v diff (git / Nou / session) f follow mode
|
|
64
65
|
p problems u undo Nou's edit s settings
|
|
65
66
|
c (in files) show only changed files Ctrl+C twice quit
|
|
66
|
-
Mouse: wheel scrolls (Shift+wheel sideways) · click/drag a scrollbar · click in the code to edit there, drag to select
|
|
67
|
+
Mouse: wheel scrolls (Shift+wheel sideways) · click/drag a scrollbar · double-click in the code to edit there (in the editor: select a word), drag to select
|
|
67
68
|
In the editor:
|
|
68
69
|
Shift+arrows / mouse drag select · Ctrl+A all · Ctrl+C/X/V copy/cut/paste · Esc clear selection or leave
|
|
69
70
|
Ctrl+←/→ word · Ctrl+Backspace or Ctrl+W delete word · Ctrl+Home/End file start/end
|
|
@@ -146,7 +147,7 @@ export function App({ initialModel, permContext }) {
|
|
|
146
147
|
const [update, setUpdate] = useState(null);
|
|
147
148
|
const [wheel, setWheel] = useState(null);
|
|
148
149
|
const [scrollTo, setScrollTo] = useState(null);
|
|
149
|
-
const [editorMouse, setEditorMouse] = useState(
|
|
150
|
+
const [editorMouse, setEditorMouse] = useState([]);
|
|
150
151
|
/** Wheel events waiting for the next frame, added up per pane. */
|
|
151
152
|
const pendingWheel = useRef({ pane: "chat", lines: 0, columns: 0, timer: null });
|
|
152
153
|
const [chatPrefill, setChatPrefill] = useState(null);
|
|
@@ -158,6 +159,7 @@ export function App({ initialModel, permContext }) {
|
|
|
158
159
|
const paneWidthsRef = useRef(paneWidths);
|
|
159
160
|
paneWidthsRef.current = paneWidths;
|
|
160
161
|
/** What a mouse drag is moving: a pane border, a pane's scrollbar, or the editor's selection. */
|
|
162
|
+
const lastClick = useRef(null);
|
|
161
163
|
const dragging = useRef(null);
|
|
162
164
|
const pullAbort = useRef(null);
|
|
163
165
|
const diagnosticsRef = useRef(null);
|
|
@@ -524,8 +526,9 @@ export function App({ initialModel, permContext }) {
|
|
|
524
526
|
return;
|
|
525
527
|
const { treeWidth, codeWidth, paneHeight } = layout.current;
|
|
526
528
|
const codeLeft = treeWidth;
|
|
527
|
-
const editorAt = (x, y, kind) => setEditorMouse((m) => ({ x: x - codeLeft - 1, y: y - 3, kind,
|
|
529
|
+
const editorAt = (x, y, kind) => setEditorMouse((m) => [...m.slice(-15), { x: x - codeLeft - 1, y: y - 3, kind, seq: (m[m.length - 1]?.seq ?? 0) + 1 }]);
|
|
528
530
|
if (event.kind === "drag") {
|
|
531
|
+
lastClick.current = null; // a press that became a drag doesn't start a double-click
|
|
529
532
|
const drag = dragging.current;
|
|
530
533
|
if (drag === "tree" || drag === "chat")
|
|
531
534
|
moveBorder(drag, event.x);
|
|
@@ -536,8 +539,12 @@ export function App({ initialModel, permContext }) {
|
|
|
536
539
|
return;
|
|
537
540
|
}
|
|
538
541
|
if (event.kind === "release") {
|
|
542
|
+
// The release says where the button let go, so a drag lands there even in a terminal that
|
|
543
|
+
// doesn't report the movement in between.
|
|
539
544
|
if (dragging.current === "tree" || dragging.current === "chat")
|
|
540
|
-
updateSettings({ paneWidths:
|
|
545
|
+
updateSettings({ paneWidths: moveBorder(dragging.current, event.x) });
|
|
546
|
+
else if (dragging.current && typeof dragging.current === "object")
|
|
547
|
+
scrollPane(dragging.current.scroll, event.y);
|
|
541
548
|
if (dragging.current === "editor")
|
|
542
549
|
editorAt(event.x, event.y, "release");
|
|
543
550
|
dragging.current = null;
|
|
@@ -566,13 +573,17 @@ export function App({ initialModel, permContext }) {
|
|
|
566
573
|
// Rows start under the pane's top border (y=1) and title (y=2).
|
|
567
574
|
if (pane === "tree" && event.y >= 3)
|
|
568
575
|
setTreeClick((c) => ({ row: event.y - 3, nonce: (c?.nonce ?? 0) + 1 }));
|
|
576
|
+
// Two clicks in the same spot within half a second are a double-click.
|
|
577
|
+
const last = lastClick.current;
|
|
578
|
+
const double = !!last && Date.now() - last.time < DOUBLE_CLICK_MS && Math.abs(last.x - event.x) <= 1 && last.y === event.y;
|
|
579
|
+
lastClick.current = double ? null : { x: event.x, y: event.y, time: Date.now() };
|
|
569
580
|
if (pane === "code" && event.y >= 3 && codePanel === "code") {
|
|
570
581
|
if (showEditor) {
|
|
571
582
|
dragging.current = "editor";
|
|
572
|
-
editorAt(event.x, event.y, "press");
|
|
583
|
+
editorAt(event.x, event.y, double ? "double" : "press");
|
|
573
584
|
}
|
|
574
|
-
else if (openFile && !diff && settleDraft(openFile)) {
|
|
575
|
-
//
|
|
585
|
+
else if (double && openFile && !diff && settleDraft(openFile)) {
|
|
586
|
+
// Double-click in the code: edit right there.
|
|
576
587
|
const gutter = String(content.split("\n").length).length + 1;
|
|
577
588
|
setEditorStart((s) => ({ line: (viewport?.top ?? 1) + event.y - 3, col: Math.max(1, event.x - codeLeft - gutter), nonce: s.nonce + 1 }));
|
|
578
589
|
setDiffMode("off");
|
|
@@ -1160,7 +1171,7 @@ ${snippet}
|
|
|
1160
1171
|
? [["Enter", "open"], ["e", "edit"], ["/", "command"], ["c", "changes"], ["Ctrl+P", "find"], ["u", "undo"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]]
|
|
1161
1172
|
: diff
|
|
1162
1173
|
? [["Esc", "close diff"], ["n/N", "next/prev change"], ...(diffMode === "session" ? [["[ ]", "prev/next file"]] : []), ["v", "cycle diff"]]
|
|
1163
|
-
: [["click/e", "edit"], ["v", "diff"], ["u", "undo"], ["/", "command"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]];
|
|
1174
|
+
: [["double-click/e", "edit"], ["v", "diff"], ["u", "undo"], ["/", "command"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]];
|
|
1164
1175
|
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + 10 + ` ✖ ${totals.errors} ▲ ${totals.warnings} `.length + (checking.length ? 4 + checking.join(", ").length : 0) + 2;
|
|
1165
1176
|
const shownHints = [];
|
|
1166
1177
|
for (let used = statusUsed, i = 0; i < hints.length; i++) {
|
package/dist/src/ui/CodeView.js
CHANGED
|
@@ -111,7 +111,7 @@ export const CodeView = memo(function CodeView({ width, height, focused, filePat
|
|
|
111
111
|
const { icon, color } = fileIcon(filePath);
|
|
112
112
|
const errorCount = problems.filter((d) => d.severity === "error").length;
|
|
113
113
|
const warningCount = problems.filter((d) => d.severity === "warning").length;
|
|
114
|
-
const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath, errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \u2716 ", errorCount] }), warningCount > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", warningCount] }), _jsxs(Text, { color: C.dim, children: [scroll, total > rows ? ` (${percent}%)` : "", focused ? " click or e to edit · v diff" : ""] })] }));
|
|
114
|
+
const title = (_jsxs(_Fragment, { children: [" ", _jsx(Text, { color: color, children: icon }), " ", filePath, errorCount > 0 && _jsxs(Text, { color: C.red, children: [" \u2716 ", errorCount] }), warningCount > 0 && _jsxs(Text, { color: C.yellow, children: [" \u25B2 ", warningCount] }), _jsxs(Text, { color: C.dim, children: [scroll, total > rows ? ` (${percent}%)` : "", focused ? " double-click or e to edit · v diff" : ""] })] }));
|
|
115
115
|
return (_jsx(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: lines.slice(start, start + rows).map((line, i) => {
|
|
116
116
|
const n = start + i + 1;
|
|
117
117
|
const isMarked = marked.has(n);
|
package/dist/src/ui/Editor.js
CHANGED
|
@@ -309,10 +309,17 @@ export const Editor = memo(function Editor({ width, height, focused, filePath, i
|
|
|
309
309
|
view.current.top = topFor(scrollTo.fraction, buf.lines.length, rows);
|
|
310
310
|
rerender((n) => n + 1);
|
|
311
311
|
}, [scrollTo?.nonce]);
|
|
312
|
-
// Click to put the cursor there; drag to select.
|
|
312
|
+
// Click to put the cursor there; double-click to select a word; drag to select.
|
|
313
|
+
const lastMouse = useRef(mouse?.[mouse.length - 1]?.seq ?? 0); // events from before the editor opened are done
|
|
313
314
|
useEffect(() => {
|
|
314
|
-
|
|
315
|
-
|
|
315
|
+
for (const event of mouse ?? []) {
|
|
316
|
+
if (event.seq <= lastMouse.current)
|
|
317
|
+
continue;
|
|
318
|
+
lastMouse.current = event.seq;
|
|
319
|
+
handleMouse(event);
|
|
320
|
+
}
|
|
321
|
+
}, [mouse]);
|
|
322
|
+
const handleMouse = (mouse) => {
|
|
316
323
|
if (mouse.kind === "release") {
|
|
317
324
|
dragFrom.current = null;
|
|
318
325
|
return;
|
|
@@ -327,12 +334,21 @@ export const Editor = memo(function Editor({ width, height, focused, filePath, i
|
|
|
327
334
|
dragFrom.current = { row, col };
|
|
328
335
|
return { ...b, row, col, anchor: null };
|
|
329
336
|
}
|
|
337
|
+
if (mouse.kind === "double") {
|
|
338
|
+
// Double-click selects the word under the pointer.
|
|
339
|
+
const line = b.lines[row];
|
|
340
|
+
const at = isWord(line[col]) ? col : isWord(line[col - 1]) ? col - 1 : -1;
|
|
341
|
+
dragFrom.current = null;
|
|
342
|
+
if (at < 0)
|
|
343
|
+
return { ...b, row, col, anchor: null };
|
|
344
|
+
return { ...b, row, col: wordRight(line, at), anchor: { row, col: wordLeft(line, at + 1) } };
|
|
345
|
+
}
|
|
330
346
|
return { ...b, row, col, anchor: dragFrom.current ?? b.anchor ?? { row: b.row, col: b.col } };
|
|
331
347
|
});
|
|
332
348
|
// Dragging past the top or bottom edge scrolls.
|
|
333
349
|
if (mouse.kind === "drag" && (mouse.y < 0 || mouse.y >= rows))
|
|
334
350
|
reveal.current = true;
|
|
335
|
-
}
|
|
351
|
+
};
|
|
336
352
|
usePaste((text) => {
|
|
337
353
|
if (prompt)
|
|
338
354
|
return setPrompt({ ...prompt, text: prompt.text + text.replace(/\s+/g, " ") });
|
package/dist/src/ui/mouse.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
2
|
import { useStdin } from "ink";
|
|
3
|
-
// Report clicks and wheel (1000)
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// Report clicks and wheel (1000), and movement while a button is held (1002, without it terminals never
|
|
4
|
+
// send drags: no resizing panes, dragging scrollbars or selecting), using SGR coordinates (1006), which
|
|
5
|
+
// work past column 223.
|
|
6
|
+
export const ENABLE = "\x1b[?1000h\x1b[?1002h\x1b[?1006h";
|
|
7
|
+
const DISABLE = "\x1b[?1006l\x1b[?1002l\x1b[?1000l";
|
|
6
8
|
/** Mouse events in a chunk of terminal input (SGR format: ESC [ < button ; x ; y M|m). */
|
|
7
9
|
export function parseMouse(chunk) {
|
|
8
10
|
const events = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nou-cli",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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": {
|