nou-cli 0.5.15 → 0.5.17
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 -1
- package/dist/src/cli.js +6 -1
- package/dist/src/engines/claude.js +153 -40
- package/dist/src/guide/index.js +20 -3
- package/dist/src/ui/Chat.js +2 -1
- package/dist/src/ui/FileTree.js +1 -1
- package/dist/src/ui/theme.js +3 -0
- package/dist/src/ui/title.js +16 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,7 +135,7 @@ Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]`
|
|
|
135
135
|
|
|
136
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.
|
|
137
137
|
|
|
138
|
-
New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol, which Nou checks for before it starts; also xterm's modifyOtherKeys form), or type `\` and press <kbd>Enter</kbd> in any terminal.
|
|
138
|
+
New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol, which Nou checks for before it starts; also xterm's modifyOtherKeys form), or type `\` and press <kbd>Enter</kbd> in any terminal. On Windows, <kbd>Ctrl</kbd>+<kbd>Enter</kbd> works in every Windows Terminal and the console; <kbd>Shift</kbd>+<kbd>Enter</kbd> needs Windows Terminal 1.25 or later (older versions send it as a plain Enter).
|
|
139
139
|
|
|
140
140
|
Conversations are saved per folder after each reply. `/resume` opens a picker (↑↓ and Enter, type to filter, or type its number) and `/resume 2` (or `nou --continue` for the last one, `nou --resume` for the list) picks one back up with the engine it was on: Nou's own engine restores its messages, Claude Code its session and Codex its thread; pi is handed the conversation so far. Sessions you started in Claude Code's or Codex's own CLI in this folder are listed too (marked "CLI"), and resuming one carries it on in that engine inside Nou.
|
|
141
141
|
|
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.
|
|
@@ -48,6 +48,7 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
48
48
|
super(choice, hooks, "Claude Code");
|
|
49
49
|
}
|
|
50
50
|
resetConversation() {
|
|
51
|
+
this.closeLive();
|
|
51
52
|
this.claudeSession = undefined;
|
|
52
53
|
}
|
|
53
54
|
snapshot() {
|
|
@@ -57,6 +58,7 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
57
58
|
restore(state) {
|
|
58
59
|
if (typeof state?.claudeSession !== "string")
|
|
59
60
|
return false;
|
|
61
|
+
this.closeLive();
|
|
60
62
|
this.claudeSession = state.claudeSession;
|
|
61
63
|
return true;
|
|
62
64
|
}
|
|
@@ -64,15 +66,45 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
64
66
|
setModel(choice) {
|
|
65
67
|
this.choice = choice;
|
|
66
68
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
dispose() {
|
|
70
|
+
this.closeLive();
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One Claude Code process per conversation, kept running between messages: each message is fed into it
|
|
74
|
+
* and a turn is over at its result. A process per message didn't work: one that left something running
|
|
75
|
+
* (a dev server, a background command) never closed its stream, so the turn never ended, and the next
|
|
76
|
+
* message's process found the session still in use and ended without a word.
|
|
77
|
+
*/
|
|
78
|
+
live = null;
|
|
79
|
+
/** The turn being waited on: settled at its result, or when the process ends. */
|
|
80
|
+
waiting = null;
|
|
81
|
+
streamed = "";
|
|
82
|
+
closeLive() {
|
|
83
|
+
const live = this.live;
|
|
84
|
+
this.live = null;
|
|
85
|
+
live?.close();
|
|
86
|
+
}
|
|
87
|
+
/** What the process was started with; a change (model, effort, project context) means a new one. */
|
|
88
|
+
liveKey() {
|
|
89
|
+
return JSON.stringify([this.choice.provider.id === "anthropic" ? this.choice.model : null, this.effort ?? null, this.extras]);
|
|
90
|
+
}
|
|
91
|
+
openLive(sdk, key) {
|
|
92
|
+
const queue = [];
|
|
93
|
+
let wake = null;
|
|
94
|
+
let closed = false;
|
|
95
|
+
const input = async function* () {
|
|
96
|
+
while (!closed) {
|
|
97
|
+
if (queue.length)
|
|
98
|
+
yield queue.shift();
|
|
99
|
+
else
|
|
100
|
+
await new Promise((resolve) => (wake = resolve));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
71
103
|
const allow = { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" } };
|
|
72
104
|
const deny = (reason) => ({ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason } });
|
|
73
|
-
const preToolUse = async (
|
|
74
|
-
const tool =
|
|
75
|
-
const args = (
|
|
105
|
+
const preToolUse = async (hookInput) => {
|
|
106
|
+
const tool = hookInput.tool_name;
|
|
107
|
+
const args = (hookInput.tool_input ?? {});
|
|
76
108
|
if (EDIT_TOOLS.has(tool) && args.file_path) {
|
|
77
109
|
const ok = await this.tracker.approve(args.file_path, proposedContent(tool, args), tool);
|
|
78
110
|
return ok ? allow : deny("The user rejected this change. Ask them what they would like instead.");
|
|
@@ -86,9 +118,9 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
86
118
|
// Reads and searches follow Claude Code's own rules; anything else that needs asking reaches canUseTool.
|
|
87
119
|
return {};
|
|
88
120
|
};
|
|
89
|
-
const postToolUse = async (
|
|
90
|
-
const tool =
|
|
91
|
-
const args = (
|
|
121
|
+
const postToolUse = async (hookInput) => {
|
|
122
|
+
const tool = hookInput.tool_name;
|
|
123
|
+
const args = (hookInput.tool_input ?? {});
|
|
92
124
|
if (EDIT_TOOLS.has(tool) && args.file_path) {
|
|
93
125
|
const problems = await this.tracker.applied(args.file_path);
|
|
94
126
|
return problems
|
|
@@ -109,10 +141,9 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
109
141
|
return {};
|
|
110
142
|
};
|
|
111
143
|
const query = sdk.query({
|
|
112
|
-
prompt,
|
|
144
|
+
prompt: input(),
|
|
113
145
|
options: {
|
|
114
146
|
cwd: process.cwd(),
|
|
115
|
-
abortController,
|
|
116
147
|
...(this.claudeSession ? { resume: this.claudeSession } : {}),
|
|
117
148
|
...(this.choice.provider.id === "anthropic" ? { model: this.choice.model } : {}),
|
|
118
149
|
...(this.effort && EFFORTS.has(this.effort) ? { effort: this.effort } : {}),
|
|
@@ -122,42 +153,124 @@ export class ClaudeCodeEngine extends ExternalEngine {
|
|
|
122
153
|
settingSources: ["project"],
|
|
123
154
|
systemPrompt: { type: "preset", preset: "claude_code", append: this.extras.join("\n\n") },
|
|
124
155
|
hooks: { PreToolUse: [{ hooks: [preToolUse] }], PostToolUse: [{ hooks: [postToolUse] }] },
|
|
125
|
-
canUseTool: async (tool,
|
|
126
|
-
const ok = await this.hooks.approve({ kind: "tool", name: tool, args:
|
|
156
|
+
canUseTool: async (tool, toolInput) => {
|
|
157
|
+
const ok = await this.hooks.approve({ kind: "tool", name: tool, args: toolInput });
|
|
127
158
|
if (!ok)
|
|
128
|
-
this.hooks.onEntry({ kind: "tool", text: `Denied: ${describeTool(tool,
|
|
129
|
-
return ok ? { behavior: "allow", updatedInput:
|
|
159
|
+
this.hooks.onEntry({ kind: "tool", text: `Denied: ${describeTool(tool, toolInput)}` });
|
|
160
|
+
return ok ? { behavior: "allow", updatedInput: toolInput } : { behavior: "deny", message: "The user denied this." };
|
|
130
161
|
},
|
|
131
162
|
},
|
|
132
163
|
});
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
const live = {
|
|
165
|
+
key,
|
|
166
|
+
query,
|
|
167
|
+
push: (text) => {
|
|
168
|
+
queue.push({ type: "user", message: { role: "user", content: text }, parent_tool_use_id: null, session_id: this.claudeSession ?? "" });
|
|
169
|
+
wake?.();
|
|
170
|
+
wake = null;
|
|
171
|
+
},
|
|
172
|
+
close: () => {
|
|
173
|
+
process.off("exit", live.close);
|
|
174
|
+
closed = true;
|
|
175
|
+
wake?.();
|
|
176
|
+
try {
|
|
177
|
+
query.close?.();
|
|
143
178
|
}
|
|
179
|
+
catch {
|
|
180
|
+
// already gone
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
process.once("exit", live.close); // Claude Code doesn't outlive Nou
|
|
185
|
+
// Reads everything Claude Code says, for as long as the process runs. Between turns that's what a
|
|
186
|
+
// background task it started brings back (it may carry on by itself); it goes to the chat too.
|
|
187
|
+
void (async () => {
|
|
188
|
+
let failure;
|
|
189
|
+
try {
|
|
190
|
+
for await (const message of query)
|
|
191
|
+
this.handle(message, live);
|
|
144
192
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
streamed = "";
|
|
148
|
-
this.hooks.onStream("");
|
|
149
|
-
if (text)
|
|
150
|
-
this.hooks.onEntry({ kind: "assistant", text });
|
|
151
|
-
this.hooks.onBusy("Claude Code is working...");
|
|
193
|
+
catch (error) {
|
|
194
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
152
195
|
}
|
|
153
|
-
|
|
154
|
-
this.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
this.hooks.onEntry({ kind: "error", text: `Claude Code: ${detail}` });
|
|
159
|
-
}
|
|
196
|
+
finally {
|
|
197
|
+
if (this.live === live)
|
|
198
|
+
this.live = null;
|
|
199
|
+
if (this.waiting?.live === live)
|
|
200
|
+
this.waiting.done(failure ?? (closed ? undefined : new Error("Claude Code stopped before it answered.")));
|
|
160
201
|
}
|
|
202
|
+
})();
|
|
203
|
+
return live;
|
|
204
|
+
}
|
|
205
|
+
handle(message, live) {
|
|
206
|
+
if (message.type === "system" && message.subtype === "init") {
|
|
207
|
+
this.claudeSession = message.session_id;
|
|
208
|
+
}
|
|
209
|
+
else if (message.type === "stream_event" && !message.parent_tool_use_id) {
|
|
210
|
+
const event = message.event;
|
|
211
|
+
if (event.type === "content_block_delta" && event.delta?.type === "text_delta") {
|
|
212
|
+
this.streamed += event.delta.text;
|
|
213
|
+
this.hooks.onStream(this.streamed);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
else if (message.type === "assistant" && !message.parent_tool_use_id) {
|
|
217
|
+
const text = message.message.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
218
|
+
this.streamed = "";
|
|
219
|
+
this.hooks.onStream("");
|
|
220
|
+
if (text)
|
|
221
|
+
this.hooks.onEntry({ kind: "assistant", text });
|
|
222
|
+
if (this.waiting)
|
|
223
|
+
this.hooks.onBusy("Claude Code is working...");
|
|
224
|
+
}
|
|
225
|
+
else if (message.type === "result") {
|
|
226
|
+
this.claudeSession = message.session_id ?? this.claudeSession;
|
|
227
|
+
this.streamed = "";
|
|
228
|
+
this.hooks.onStream("");
|
|
229
|
+
if (message.subtype !== "success" || message.is_error) {
|
|
230
|
+
const detail = message.errors?.join("; ") || message.result || message.subtype;
|
|
231
|
+
// A turn you stopped ends with an error result; that isn't news.
|
|
232
|
+
if (!this.stopping)
|
|
233
|
+
this.hooks.onEntry({ kind: "error", text: `Claude Code: ${detail}` });
|
|
234
|
+
}
|
|
235
|
+
this.stopping = false;
|
|
236
|
+
if (this.waiting?.live === live)
|
|
237
|
+
this.waiting.done();
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
stopping = false;
|
|
241
|
+
async run(prompt, signal) {
|
|
242
|
+
const sdk = await loadPackage("@anthropic-ai/claude-agent-sdk");
|
|
243
|
+
const key = this.liveKey();
|
|
244
|
+
if (this.live && this.live.key !== key)
|
|
245
|
+
this.closeLive();
|
|
246
|
+
const live = (this.live ??= this.openLive(sdk, key));
|
|
247
|
+
this.streamed = "";
|
|
248
|
+
let settle;
|
|
249
|
+
const finished = new Promise((resolve, reject) => (settle = (error) => (error ? reject(error) : resolve())));
|
|
250
|
+
const waiting = { live, done: (error) => settle(error) };
|
|
251
|
+
this.waiting = waiting;
|
|
252
|
+
// Stopping: Claude Code interrupts the turn and ends it with a result; if it doesn't promptly, the
|
|
253
|
+
// process goes (the next message starts a new one, resuming the session).
|
|
254
|
+
const onAbort = () => {
|
|
255
|
+
this.stopping = true;
|
|
256
|
+
Promise.resolve(live.query.interrupt?.()).catch(() => { });
|
|
257
|
+
setTimeout(() => {
|
|
258
|
+
if (this.waiting !== waiting)
|
|
259
|
+
return;
|
|
260
|
+
if (this.live === live)
|
|
261
|
+
this.closeLive();
|
|
262
|
+
waiting.done();
|
|
263
|
+
}, 3000);
|
|
264
|
+
};
|
|
265
|
+
signal.addEventListener("abort", onAbort);
|
|
266
|
+
try {
|
|
267
|
+
live.push(prompt);
|
|
268
|
+
await finished;
|
|
269
|
+
}
|
|
270
|
+
finally {
|
|
271
|
+
signal.removeEventListener("abort", onAbort);
|
|
272
|
+
if (this.waiting === waiting)
|
|
273
|
+
this.waiting = null;
|
|
161
274
|
}
|
|
162
275
|
if (signal.aborted)
|
|
163
276
|
throw new Error("aborted");
|
package/dist/src/guide/index.js
CHANGED
|
@@ -148,8 +148,25 @@ export function showPlacesTool(onStops) {
|
|
|
148
148
|
},
|
|
149
149
|
};
|
|
150
150
|
}
|
|
151
|
-
/**
|
|
152
|
-
|
|
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 {
|
package/dist/src/ui/Chat.js
CHANGED
|
@@ -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
|
}
|
package/dist/src/ui/FileTree.js
CHANGED
|
@@ -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
|
});
|
package/dist/src/ui/theme.js
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.5.17",
|
|
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": {
|