nou-cli 0.5.11 → 0.5.13

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.
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Stands in for Ink's line-update (see fast-width.ts), which works out how much of a changed line can stay
3
+ * on screen. For any line with a character outside plain ASCII (every line with a box border, so nearly all
4
+ * of Nou's) it splits both versions into graphemes, every frame. Lines made only of one-column characters
5
+ * that never combine (fast-width's NARROW) have one grapheme per character, so this answers those directly,
6
+ * with exactly what Ink would, and hands every other line to Ink's own code.
7
+ */
8
+ import { NARROW } from "./fast-width.js";
9
+ // eslint-disable-next-line no-control-regex -- SGR starts with ESC
10
+ const SGR = /\u001b\[[\d;]*m/gu;
11
+ /** Ink's lineUpdate with the fast path in front. Same answers; exported for tests. */
12
+ export function fastLineUpdate(real) {
13
+ return (previous, next, columns) => {
14
+ if (columns === undefined || Number.isNaN(columns) || columns < 1)
15
+ return real(previous, next, columns);
16
+ let common = 0;
17
+ while (common < previous.length && common < next.length && previous[common] === next[common])
18
+ common++;
19
+ if (common === 0)
20
+ return real(previous, next, columns);
21
+ const previousText = previous.replaceAll(SGR, "");
22
+ const nextText = next.replaceAll(SGR, "");
23
+ // Controls are excluded by NARROW too (it starts at the space).
24
+ if (!NARROW.test(previousText) || !NARROW.test(nextText))
25
+ return real(previous, next, columns);
26
+ const codes = [...next.matchAll(SGR)];
27
+ let textLimit = common;
28
+ for (const match of codes) {
29
+ if (match.index >= common)
30
+ break;
31
+ textLimit -= Math.min(match[0].length, common - match.index);
32
+ }
33
+ // One character a grapheme and a column, so this is the grapheme walk Ink does.
34
+ let textLength = 0;
35
+ while (textLength < textLimit && previousText[textLength] === nextText[textLength])
36
+ textLength++;
37
+ const column = textLength;
38
+ const full = `\u001b[1G\u001b[K${next}`;
39
+ if (column === 0 || column >= columns || previousText.length > columns || nextText.length > columns)
40
+ return full;
41
+ let offset = textLength;
42
+ let styles = "";
43
+ for (const match of codes) {
44
+ if (match.index > offset || match.index + match[0].length > common)
45
+ break;
46
+ offset += match[0].length;
47
+ styles += match[0];
48
+ }
49
+ const partial = `\u001b[${column + 1}G\u001b[K${styles}${next.slice(offset)}`;
50
+ return partial.length < full.length ? partial : full;
51
+ };
52
+ }
53
+ const of = new URL(import.meta.url).searchParams.get("of");
54
+ const real = of ? (await import(of)).default : null;
55
+ export default real ? fastLineUpdate(real) : null;
@@ -1,6 +1,33 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from "ink";
3
+ import { useState, useSyncExternalStore } from "react";
3
4
  import { C } from "./theme.js";
5
+ const wheels = new Map();
6
+ const wheelListeners = new Map();
7
+ /**
8
+ * Hands a pane its wheel total. Only that pane draws again, not the whole app: scrolling is the most
9
+ * frequent thing there is, and every pane redrawing for it was most of its cost.
10
+ */
11
+ export function sendWheel(pane, lines, columns) {
12
+ wheels.set(pane, { lines, columns, nonce: (wheels.get(pane)?.nonce ?? 0) + 1 });
13
+ for (const fn of wheelListeners.get(pane) ?? [])
14
+ fn();
15
+ }
16
+ /** The latest wheel total for `pane` (see sendWheel) since this pane appeared; null before that. */
17
+ export function usePaneWheel(pane) {
18
+ // A pane that opens later (the editor, say) mustn't replay a wheel meant for what was there before.
19
+ const [since] = useState(() => (pane ? wheels.get(pane)?.nonce ?? 0 : 0));
20
+ const wheel = useSyncExternalStore((fn) => {
21
+ if (!pane)
22
+ return () => { };
23
+ let set = wheelListeners.get(pane);
24
+ if (!set)
25
+ wheelListeners.set(pane, (set = new Set()));
26
+ set.add(fn);
27
+ return () => set.delete(fn);
28
+ }, () => (pane ? wheels.get(pane) ?? null : null));
29
+ return wheel && wheel.nonce > since ? wheel : null;
30
+ }
4
31
  /** Lines a single wheel notch moves. */
5
32
  export const WHEEL_STEP = 3;
6
33
  /** Where to scroll to for a scrollbar click at `fraction` of the track. */
@@ -274,7 +274,7 @@ export function renderMarkdown(text, width, isFile) {
274
274
  const item = raw.match(/^(\s*)([-*+]|\d+[.)])\s+(\[[ xX]\]\s+)?(.*)/);
275
275
  if (item) {
276
276
  const depth = Math.floor(item[1].length / 2) * 2;
277
- const task = item[3] ? (/x/i.test(item[3]) ? "☑ " : "☐ ") : "";
277
+ const task = item[3] ? (/x/i.test(item[3]) ? "■ " : "□ ") : "";
278
278
  const bullet = `${" ".repeat(depth)}${/\d/.test(item[2]) ? item[2] : "•"} ${task}`;
279
279
  for (const line of wrapSpans([{ text: bullet }, ...spansOf(item[4])], width, bullet.length))
280
280
  out.push({ kind: "text", spans: line });
@@ -79,7 +79,7 @@ const ICONS = Object.fromEntries(Object.entries(iconColors()).map(([ext, color])
79
79
  export function fileIcon(file) {
80
80
  const base = path.basename(file).toLowerCase();
81
81
  if (base.startsWith(".env"))
82
- return { icon: "⚿", color: C.yellow };
82
+ return { icon: "⌁", color: C.yellow };
83
83
  if (base.startsWith(".git"))
84
84
  return { icon: "◌", color: C.orange };
85
85
  if (base === "package.json")
@@ -51,6 +51,8 @@ export function connectToHost(on) {
51
51
  on.redraw();
52
52
  else if (msg.type === "error")
53
53
  on.error(msg.text);
54
+ else if (msg.type === "quit")
55
+ on.quit();
54
56
  }));
55
57
  s.on("error", () => { });
56
58
  s.on("close", () => (socket = null));
@@ -68,6 +68,17 @@ export async function runHost(folders, firstArgs, command = [process.execPath, p
68
68
  if (to && to !== active)
69
69
  activate(to);
70
70
  }
71
+ else if (msg.type === "close") {
72
+ const w = workspaces[msg.index];
73
+ if (!w)
74
+ return;
75
+ // Asked to quit, it saves and exits as when you quit it; one that doesn't is stopped.
76
+ if (w.socket)
77
+ send(w, { type: "quit" });
78
+ else
79
+ w.pty.kill();
80
+ setTimeout(() => workspaces.includes(w) && w.pty.kill(), 3000);
81
+ }
71
82
  else if (msg.type === "new") {
72
83
  const folder = workspaceFolder(msg.folder, from.folder);
73
84
  if (!folder)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nou-cli",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
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": {