nou-cli 0.5.15 → 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/cli.js CHANGED
@@ -16,9 +16,10 @@ import { installFastOutput } from "./ui/fast-output.js";
16
16
  import { RENDER_OPTIONS } from "./ui/render-options.js";
17
17
  import { setScreenClear } from "./ui/redraw.js";
18
18
  import { detectTerminal } from "./ui/background.js";
19
+ import { setTerminalTitle } from "./ui/title.js";
19
20
  import { setPalette } from "./ui/theme.js";
20
21
  import { runHost, workspaceFolder } from "./workspaces/host.js";
21
- import { pendingWorkspaces } from "./workspaces/client.js";
22
+ import { isHosted, pendingWorkspaces } from "./workspaces/client.js";
22
23
  import { spawn } from "child_process";
23
24
  import { appendFileSync, mkdirSync } from "fs";
24
25
  import path from "path";
@@ -97,6 +98,7 @@ async function main() {
97
98
  const uiTheme = loadSettings().uiTheme;
98
99
  // Every workspace uses the colors found for this terminal (they can't ask it themselves from the background).
99
100
  // The host asks the terminal once and tells every workspace (in the background they can't ask it).
101
+ setTerminalTitle();
100
102
  const term = await detectTerminal();
101
103
  process.env.NOU_PALETTE = uiTheme === "dark" || uiTheme === "light" ? uiTheme : term.palette ?? "dark";
102
104
  if (term.kitty !== null)
@@ -108,6 +110,9 @@ async function main() {
108
110
  console.error("Nou needs an interactive terminal (try Windows Terminal, PowerShell, or Terminal/iTerm on macOS).");
109
111
  process.exit(1);
110
112
  }
113
+ // The tab says Nou CLI, not the shell it runs in (a workspace's host already set it).
114
+ if (!isHosted())
115
+ setTerminalTitle();
111
116
  p.intro(chalk.cyan(`${artWork} \n Hello I am Nou! 🌞 ${chalk.dim(`v${currentVersion()}`)}`));
112
117
  // A folder's files can run things (its MCP servers, plugins, the commands Nou is asked to run), so a
113
118
  // folder is trusted once, like Claude Code and VS Code ask.
@@ -157,6 +157,9 @@ export class ClaudeCodeEngine extends ExternalEngine {
157
157
  if (!signal.aborted)
158
158
  this.hooks.onEntry({ kind: "error", text: `Claude Code: ${detail}` });
159
159
  }
160
+ // The turn is over: don't wait for Claude Code's process to close its stream as well. Something it
161
+ // left running (a background command, an MCP server) kept that open, and Nou "working" forever.
162
+ break;
160
163
  }
161
164
  }
162
165
  if (signal.aborted)
@@ -148,8 +148,25 @@ export function showPlacesTool(onStops) {
148
148
  },
149
149
  };
150
150
  }
151
- /** The fenced block other engines (which can't call Nou's tools) list their places in. */
152
- const SHOW_BLOCK = /```nou-show\s*\n([\s\S]*?)\n```/;
151
+ /**
152
+ * The fenced block other engines (which can't call Nou's tools) list their places in. Models also write it
153
+ * with ~~~, with CRLF, with the closing fence on the JSON's last line, or (unfenced) as a "nou-show" line
154
+ * followed by the JSON array at the end of the reply.
155
+ */
156
+ const SHOW_BLOCK = /(?:```|~~~)[ \t]*nou-show[ \t]*\r?\n([\s\S]*?)\r?\n?[ \t]*(?:```|~~~)|(?:^|\n)[ \t]*nou-show[ \t]*\r?\n[ \t]*(\[[\s\S]*\])\s*$/;
157
+ /**
158
+ * A reply as the chat shows it: Nou's blocks (nou-show, nou-steps) are for Nou, not you, so they're never
159
+ * drawn, including one still arriving mid-stream.
160
+ */
161
+ export function hideNouBlocks(text) {
162
+ if (!text.includes("nou-"))
163
+ return text;
164
+ return text
165
+ .replace(/(?:```|~~~)[ \t]*nou-(?:show|steps)[ \t]*\r?\n[\s\S]*?(?:```|~~~)/g, "")
166
+ .replace(/(?:```|~~~)[ \t]*nou-(?:show|steps)[\s\S]*$/, "")
167
+ .replace(/(?:^|\n)[ \t]*nou-(?:show|steps)[ \t]*\r?\n[ \t]*\[[\s\S]*$/, "")
168
+ .trimEnd();
169
+ }
153
170
  /**
154
171
  * For Claude Code, Codex and pi, which can't call show_places: how to point at places, added to what they
155
172
  * know about the project, so explaining "where does X happen" opens the files on any engine.
@@ -166,7 +183,7 @@ export function stopsFromReply(text) {
166
183
  if (!m)
167
184
  return null;
168
185
  try {
169
- const stops = tidyStops(JSON.parse(m[1]));
186
+ const stops = tidyStops(JSON.parse((m[1] ?? m[2])));
170
187
  return { stops, rest: text.replace(m[0], "").trim() };
171
188
  }
172
189
  catch {
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { hideNouBlocks } from "../guide/index.js";
2
3
  import { memo, useEffect, useMemo, useRef, useState } from "react";
3
4
  import { Box, Text, useAnimation, useInput } from "ink";
4
5
  import { statSync } from "fs";
@@ -54,7 +55,7 @@ function entryLines(entry, inner, links = true) {
54
55
  }
55
56
  else if (entry.kind === "assistant") {
56
57
  out.push({ text: "◆ nou", color: C.accent2, bold: true, lead: BAR, leadColor: C.accent2 });
57
- for (const line of renderMarkdown(entry.text, inner - 3, links ? isProjectFile : undefined)) {
58
+ for (const line of renderMarkdown(hideNouBlocks(entry.text), inner - 3, links ? isProjectFile : undefined)) {
58
59
  const color = line.kind === "heading" ? C.accent2 : line.kind === "quote" ? C.dim : undefined;
59
60
  out.push({ text: "", spans: line.spans, ...(color ? { color } : {}), lead: BAR, leadColor: C.accent2 });
60
61
  }
@@ -192,6 +192,6 @@ export const FileTree = memo(function FileTree({ width, height, focused, openFil
192
192
  const room = Math.max(1, inner - marker.length - 1);
193
193
  const shown = label.length > room ? `${label.slice(0, room - 1)}…` : label.padEnd(room);
194
194
  const iconAt = shown.indexOf(icon);
195
- return (_jsxs(Text, { wrap: "truncate-end", ...(selected && focused ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, children: shown.slice(0, iconAt) }), _jsx(Text, { color: color, children: shown.slice(iconAt, iconAt + icon.length) }), working?.has(node.path) ? (_jsx(Text, { bold: true, inverse: selected && focused, children: gradient(shown.slice(iconAt + icon.length), frame).map((g, j) => _jsx(Text, { color: g.color, children: g.ch }, j)) })) : (_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, inverse: selected && focused, children: shown.slice(iconAt + icon.length) })), _jsxs(Text, { color: markerColor, children: [" ", marker] })] }, node.path));
195
+ return (_jsxs(Text, { wrap: "truncate-end", ...(isOpen ? { backgroundColor: C.openRow } : selected && focused ? { backgroundColor: C.panel } : {}), children: [_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, children: shown.slice(0, iconAt) }), _jsx(Text, { color: color, children: shown.slice(iconAt, iconAt + icon.length) }), working?.has(node.path) ? (_jsx(Text, { bold: true, inverse: selected && focused, children: gradient(shown.slice(iconAt + icon.length), frame).map((g, j) => _jsx(Text, { color: g.color, children: g.ch }, j)) })) : (_jsx(Text, { ...(nameColor ? { color: nameColor } : {}), bold: isOpen, inverse: selected && focused, children: shown.slice(iconAt + icon.length) })), _jsxs(Text, { color: markerColor, children: [" ", marker] })] }, node.path));
196
196
  })] }), _jsx(Scrollbar, { total: nodes.length, top: start, rows: rows, focused: focused })] }) }));
197
197
  });
@@ -16,6 +16,8 @@ const DARK = {
16
16
  /** Lines around panes that aren't focused: quieter than text, since they aren't read. */
17
17
  border: "#565f89",
18
18
  panel: "#1f2335",
19
+ /** The band behind the file that's open, wherever it's listed: every name color stays 4.5:1 on it. */
20
+ openRow: "#2a3352",
19
21
  /** Text on a colored badge (CHAT, CLOUD, EDITING...). */
20
22
  onBadge: "#1a1b26",
21
23
  };
@@ -32,6 +34,7 @@ const LIGHT = {
32
34
  dim: "#5a607a",
33
35
  border: "#a8aecb",
34
36
  panel: "#e6e7ed",
37
+ openRow: "#edf1fc",
35
38
  onBadge: "#ffffff",
36
39
  };
37
40
  /** The colors in use: dark unless the terminal is light (see setPalette). Read at render time, so set it first. */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The terminal's window or tab title. Windows Terminal, iTerm2, Ghostty and the rest show the running shell
3
+ * ("Windows PowerShell") unless the program sets its own with OSC 0, which Nou does: "Nou CLI".
4
+ * The title before is saved (CSI 22;0t) and put back (CSI 23;0t) on the way out, in terminals that keep a
5
+ * title stack (xterm, kitty, Ghostty, WezTerm, iTerm2); the others ignore those codes, and the shell retitles
6
+ * its tab soon after.
7
+ */
8
+ export const TITLE = "Nou CLI";
9
+ let set = false;
10
+ export function setTerminalTitle(title = TITLE) {
11
+ if (set || !process.stdout.isTTY)
12
+ return;
13
+ set = true;
14
+ process.stdout.write(`\x1b[22;0t\x1b]0;${title}\x07`);
15
+ process.on("exit", () => process.stdout.write("\x1b[23;0t"));
16
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nou-cli",
3
- "version": "0.5.15",
3
+ "version": "0.5.16",
4
4
  "description": "Nou: an IDE-style AI coding assistant for your terminal. Local (Ollama, LM Studio) and cloud (Claude, OpenAI, Gemini...) models, live diffs, git-aware, built-in editor.",
5
5
  "type": "module",
6
6
  "bin": {