@datalabrotterdam/nova-ai-cli 1.2.0-alpha.3 → 1.2.0-alpha.5
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
# [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.5](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.5) (2026-10-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **cli:** keep the full-screen frame one row short of the terminal ([37355f4](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/37355f4efb8a237f204c061ad7008faf54a0dd9e))
|
|
7
|
+
* **cli:** pick the account's default model when none is set ([fb0f34c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/fb0f34c7bac26f1b59c4b6b2245e6fb8365f99f1))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* **cli:** full-screen TUI with a pinned prompt and mouse-wheel scrolling ([a68d693](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/a68d6930945bd6440de22c709fc14ae32a9c9f65))
|
|
13
|
+
|
|
14
|
+
# [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.4](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.3...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.4) (2026-10-05)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **cli:** depend on the agent version the CLI was released with ([4931439](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/4931439d106063b81e24367d6aa9c3f285f4ad15))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **cli:** offer npm updates at start-up, remind until installed, /update ([18defca](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/18defcaea988698f9d0909419bb2aa1fdedfa5a9))
|
|
25
|
+
|
|
1
26
|
# [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.3](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.2...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.3) (2026-10-04)
|
|
2
27
|
|
|
3
28
|
|
package/README.md
CHANGED
|
@@ -44,10 +44,17 @@ nova-ai # start the terminal UI in your project
|
|
|
44
44
|
Or without installing: `npx @datalabrotterdam/nova-ai-cli login`, then
|
|
45
45
|
`npx @datalabrotterdam/nova-ai-cli`.
|
|
46
46
|
|
|
47
|
-
When a newer version is on npm, the terminal UI
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
When a newer version is on npm, `nova-ai` asks before the terminal UI opens:
|
|
48
|
+
Enter installs it (`npm install -g`) and restarts, `n` asks again tomorrow, `s`
|
|
49
|
+
skips that version. Until it is installed, the status line keeps reminding you,
|
|
50
|
+
and `/update` installs it and restarts in the same session. `-p` and
|
|
51
|
+
`--version` print a notice (on stderr, only in a terminal). npm is asked at
|
|
52
|
+
most once a day; an alpha install also follows the `alpha` channel.
|
|
53
|
+
`NOVA_NO_UPDATE_CHECK=1` turns it off (it is off in CI).
|
|
54
|
+
|
|
55
|
+
nova-ai installs updates itself only when it was installed with
|
|
56
|
+
`npm install -g`; with npx, another package manager or a project dependency it
|
|
57
|
+
shows the command to run instead.
|
|
51
58
|
|
|
52
59
|
```
|
|
53
60
|
nova-ai [options] Interactive terminal UI
|
|
@@ -64,9 +71,11 @@ Interactive options:
|
|
|
64
71
|
|
|
65
72
|
## Terminal UI
|
|
66
73
|
|
|
67
|
-
The UI
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
The UI runs full screen (the terminal's alternate screen, like `less` or
|
|
75
|
+
`vim`) and gives your shell its screen back on exit. The mouse wheel or
|
|
76
|
+
PgUp/PgDn scroll the transcript, Esc jumps back to the newest output, and
|
|
77
|
+
while you read an answer the prompt that asked for it stays pinned on the top
|
|
78
|
+
row. To select text, hold Shift (Option in some macOS terminals) while dragging.
|
|
70
79
|
|
|
71
80
|
- **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
|
|
72
81
|
questions and builds a task list but changes nothing; `ask` only answers.
|
|
@@ -41,6 +41,7 @@ function chooseUpdate(currentVersion, distTags) {
|
|
|
41
41
|
return {
|
|
42
42
|
currentVersion,
|
|
43
43
|
latestVersion: best.version,
|
|
44
|
+
tag: best.tag,
|
|
44
45
|
command: `npm install -g ${CLI_PACKAGE_NAME}@${best.tag}`
|
|
45
46
|
};
|
|
46
47
|
}
|
|
@@ -61,7 +62,7 @@ async function readDistTags(options) {
|
|
|
61
62
|
if (!response.ok) return null;
|
|
62
63
|
const payload = await response.json();
|
|
63
64
|
if (!isDistTags(payload)) return null;
|
|
64
|
-
if (cachePath) writeCache(cachePath, { checkedAt: now, distTags: payload });
|
|
65
|
+
if (cachePath) writeCache(cachePath, { ...cached, checkedAt: now, distTags: payload });
|
|
65
66
|
return payload;
|
|
66
67
|
}
|
|
67
68
|
function readCache(path) {
|
|
@@ -74,6 +75,26 @@ function readCache(path) {
|
|
|
74
75
|
}
|
|
75
76
|
return null;
|
|
76
77
|
}
|
|
78
|
+
var SNOOZE_MS = 24 * 60 * 60 * 1e3;
|
|
79
|
+
function shouldAskAboutUpdate(update, options = {}) {
|
|
80
|
+
const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
|
|
81
|
+
const cached = cachePath ? readCache(cachePath) : null;
|
|
82
|
+
if (!cached) return true;
|
|
83
|
+
if (cached.skippedVersion === update.latestVersion) return false;
|
|
84
|
+
const now = (options.now ?? Date.now)();
|
|
85
|
+
return !(typeof cached.snoozedUntil === "number" && now < cached.snoozedUntil);
|
|
86
|
+
}
|
|
87
|
+
function rememberUpdateAnswer(answer, update, options = {}) {
|
|
88
|
+
const cachePath = options.cachePath === void 0 ? updateCachePath() : options.cachePath;
|
|
89
|
+
if (!cachePath) return;
|
|
90
|
+
const cached = readCache(cachePath);
|
|
91
|
+
const base = cached ?? { checkedAt: 0, distTags: {} };
|
|
92
|
+
const now = (options.now ?? Date.now)();
|
|
93
|
+
writeCache(
|
|
94
|
+
cachePath,
|
|
95
|
+
answer === "skip" ? { ...base, skippedVersion: update.latestVersion, snoozedUntil: void 0 } : { ...base, snoozedUntil: now + SNOOZE_MS }
|
|
96
|
+
);
|
|
97
|
+
}
|
|
77
98
|
function writeCache(path, value) {
|
|
78
99
|
try {
|
|
79
100
|
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
@@ -141,6 +162,8 @@ export {
|
|
|
141
162
|
updateChecksDisabled,
|
|
142
163
|
checkForUpdate,
|
|
143
164
|
chooseUpdate,
|
|
165
|
+
shouldAskAboutUpdate,
|
|
166
|
+
rememberUpdateAnswer,
|
|
144
167
|
readInstalledVersion,
|
|
145
168
|
isNewerVersion,
|
|
146
169
|
formatUpdateNotice
|
package/dist/index.js
CHANGED
|
@@ -89,7 +89,7 @@ function parseTuiArgs(args) {
|
|
|
89
89
|
function startUpdateCheck() {
|
|
90
90
|
if (!process.stderr.isTTY) return null;
|
|
91
91
|
const abort = new AbortController();
|
|
92
|
-
const result = import("./update-check-
|
|
92
|
+
const result = import("./update-check-G3PYCRWM.js").then(({ checkForUpdate }) => checkForUpdate({ signal: abort.signal })).catch(() => null);
|
|
93
93
|
return { result, cancel: () => abort.abort() };
|
|
94
94
|
}
|
|
95
95
|
async function noticeUpdate(check, waitMs) {
|
|
@@ -103,7 +103,7 @@ async function noticeUpdate(check, waitMs) {
|
|
|
103
103
|
clearTimeout(timer);
|
|
104
104
|
check.cancel();
|
|
105
105
|
if (!update) return;
|
|
106
|
-
const { formatUpdateNotice } = await import("./update-check-
|
|
106
|
+
const { formatUpdateNotice } = await import("./update-check-G3PYCRWM.js");
|
|
107
107
|
process.stderr.write(`
|
|
108
108
|
${formatUpdateNotice(update)}
|
|
109
109
|
`);
|
|
@@ -175,7 +175,7 @@ ${MAIN_HELP}
|
|
|
175
175
|
);
|
|
176
176
|
return 2;
|
|
177
177
|
}
|
|
178
|
-
const { default: runChat } = await import("./tui-
|
|
178
|
+
const { default: runChat } = await import("./tui-4XTC6VJP.js");
|
|
179
179
|
return runChat(options);
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
|
-
checkForUpdate
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
checkForUpdate,
|
|
3
|
+
rememberUpdateAnswer,
|
|
4
|
+
shouldAskAboutUpdate
|
|
5
|
+
} from "./chunk-L77KQRGZ.js";
|
|
6
|
+
import {
|
|
7
|
+
CLI_PACKAGE_NAME,
|
|
8
|
+
cliVersion
|
|
9
|
+
} from "./chunk-FMHWW3E6.js";
|
|
5
10
|
import {
|
|
6
11
|
TuiAcpClient,
|
|
7
12
|
createStore,
|
|
@@ -10,14 +15,113 @@ import {
|
|
|
10
15
|
} from "./chunk-NHGIUEIR.js";
|
|
11
16
|
|
|
12
17
|
// src/tui/index.ts
|
|
13
|
-
import { createInterface } from "readline/promises";
|
|
14
|
-
import {
|
|
15
|
-
|
|
18
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
19
|
+
import {
|
|
20
|
+
readCredentials,
|
|
21
|
+
saveDefaultModel as saveDefaultModel2
|
|
22
|
+
} from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
23
|
+
import { resolveDefaultModel as resolveDefaultModel2 } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
24
|
+
import { createNovaClient as createNovaClient2 } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
|
|
25
|
+
import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
16
26
|
import { setWorkspaceTrusted as setWorkspaceTrusted2 } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
17
27
|
|
|
28
|
+
// src/update-install.ts
|
|
29
|
+
import { spawnSync } from "child_process";
|
|
30
|
+
import { existsSync, realpathSync } from "fs";
|
|
31
|
+
import { dirname, join, sep } from "path";
|
|
32
|
+
import { createInterface } from "readline/promises";
|
|
33
|
+
var START_WAIT_MS = 1500;
|
|
34
|
+
var UPDATED_FROM_ENV = "NOVA_UPDATED_FROM";
|
|
35
|
+
function globalNpmPrefix(options = {}) {
|
|
36
|
+
const script = options.scriptPath ?? process.argv[1];
|
|
37
|
+
if (!script) return null;
|
|
38
|
+
const windows = (options.platform ?? process.platform) === "win32";
|
|
39
|
+
const marker = `${sep}node_modules${sep}${CLI_PACKAGE_NAME.split("/").join(sep)}${sep}`;
|
|
40
|
+
const resolved = realPath(script);
|
|
41
|
+
const at = resolved.lastIndexOf(marker);
|
|
42
|
+
if (at < 0) return null;
|
|
43
|
+
const nodeModulesParent = resolved.slice(0, at);
|
|
44
|
+
const prefix = windows ? nodeModulesParent : dirname(nodeModulesParent);
|
|
45
|
+
if (!windows && nodeModulesParent !== join(prefix, "lib")) return null;
|
|
46
|
+
return existsSync(join(prefix, windows ? "nova-ai.cmd" : join("bin", "nova-ai"))) ? prefix : null;
|
|
47
|
+
}
|
|
48
|
+
async function withInstallability(update, options = {}) {
|
|
49
|
+
if (!update) return null;
|
|
50
|
+
return { ...update, installable: globalNpmPrefix(options) !== null };
|
|
51
|
+
}
|
|
52
|
+
function npmInvocation(args) {
|
|
53
|
+
return process.platform === "win32" ? [`npm ${args.map((arg) => /\s/.test(arg) ? `"${arg}"` : arg).join(" ")}`, [], true] : ["npm", args, false];
|
|
54
|
+
}
|
|
55
|
+
function realPath(path) {
|
|
56
|
+
try {
|
|
57
|
+
return realpathSync(path);
|
|
58
|
+
} catch {
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function parseUpdateAnswer(answer) {
|
|
63
|
+
const value = answer.trim().toLowerCase();
|
|
64
|
+
if (value === "" || value === "y" || value === "yes") return "update";
|
|
65
|
+
if (value === "s" || value === "skip") return "skip";
|
|
66
|
+
return "later";
|
|
67
|
+
}
|
|
68
|
+
async function askToUpdate(update) {
|
|
69
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
70
|
+
try {
|
|
71
|
+
console.log(`
|
|
72
|
+
Update available: nova-ai ${update.currentVersion} \u2192 ${update.latestVersion}`);
|
|
73
|
+
return parseUpdateAnswer(
|
|
74
|
+
await prompt.question("Update now? [Y/n/s] (n: not today, s: skip this version) ")
|
|
75
|
+
);
|
|
76
|
+
} finally {
|
|
77
|
+
prompt.close();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function offerUpdateAtStart(check, restartArgs) {
|
|
81
|
+
let timer;
|
|
82
|
+
const update = await Promise.race([
|
|
83
|
+
check,
|
|
84
|
+
new Promise((resolve2) => {
|
|
85
|
+
timer = setTimeout(() => resolve2(null), START_WAIT_MS);
|
|
86
|
+
})
|
|
87
|
+
]);
|
|
88
|
+
clearTimeout(timer);
|
|
89
|
+
if (!update?.installable || !shouldAskAboutUpdate(update)) return null;
|
|
90
|
+
const answer = await askToUpdate(update);
|
|
91
|
+
if (answer !== "update") {
|
|
92
|
+
rememberUpdateAnswer(answer, update);
|
|
93
|
+
console.log(
|
|
94
|
+
answer === "skip" ? `Skipping ${update.latestVersion}; you'll be asked again for a newer version.` : "Not now. /update installs it any time."
|
|
95
|
+
);
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return installAndRestart(update, restartArgs);
|
|
99
|
+
}
|
|
100
|
+
function installAndRestart(update, restartArgs) {
|
|
101
|
+
const prefix = globalNpmPrefix();
|
|
102
|
+
if (!prefix) return null;
|
|
103
|
+
console.log(`
|
|
104
|
+
Installing nova-ai ${update.latestVersion}\u2026`);
|
|
105
|
+
const [file, args, shell] = npmInvocation(["install", "-g", "--prefix", prefix, `${CLI_PACKAGE_NAME}@${update.tag}`]);
|
|
106
|
+
const install = spawnSync(file, args, { stdio: "inherit", shell });
|
|
107
|
+
if (install.status !== 0) {
|
|
108
|
+
console.error(`
|
|
109
|
+
The update did not install. Run it yourself: ${update.command}
|
|
110
|
+
`);
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
const script = process.argv[1];
|
|
114
|
+
if (!script) return null;
|
|
115
|
+
const restarted = spawnSync(process.execPath, [...process.execArgv, script, ...restartArgs], {
|
|
116
|
+
stdio: "inherit",
|
|
117
|
+
env: { ...process.env, [UPDATED_FROM_ENV]: update.currentVersion }
|
|
118
|
+
});
|
|
119
|
+
return restarted.status ?? 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
18
122
|
// src/tui/ink/app.tsx
|
|
19
|
-
import { Box as Box6, render,
|
|
20
|
-
import { useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
123
|
+
import { Box as Box6, render, Text as Text6, useBoxMetrics, useInput as useInput3, useWindowSize } from "ink";
|
|
124
|
+
import { memo, useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
21
125
|
import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
|
|
22
126
|
|
|
23
127
|
// src/tui/session/session-runner.ts
|
|
@@ -46,7 +150,7 @@ import {
|
|
|
46
150
|
|
|
47
151
|
// src/tui/files/file-mentions.ts
|
|
48
152
|
import { statSync } from "fs";
|
|
49
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
153
|
+
import { isAbsolute, relative, resolve, sep as sep2 } from "path";
|
|
50
154
|
var MENTION_RE = /@(?:"([^"]+)"|'([^']+)'|([^\s]+))/g;
|
|
51
155
|
var TRAILING_PUNCTUATION = /[.,;:!?\)\]\}]+$/;
|
|
52
156
|
function findWorkspaceFileMentions(text, cwd) {
|
|
@@ -56,10 +160,10 @@ function findWorkspaceFileMentions(text, cwd) {
|
|
|
56
160
|
const raw = match[1] ?? match[2] ?? match[3] ?? "";
|
|
57
161
|
const candidates = [raw, raw.replace(TRAILING_PUNCTUATION, "")].filter((value, index, all) => value && all.indexOf(value) === index);
|
|
58
162
|
for (const candidate of candidates) {
|
|
59
|
-
const normalized = candidate.replace(/[\\/]/g,
|
|
163
|
+
const normalized = candidate.replace(/[\\/]/g, sep2);
|
|
60
164
|
const absolutePath = resolve(workspaceRoot, normalized);
|
|
61
165
|
const relativePath = relative(workspaceRoot, absolutePath);
|
|
62
|
-
if (relativePath.startsWith(`..${
|
|
166
|
+
if (relativePath.startsWith(`..${sep2}`) || relativePath === ".." || isAbsolute(relativePath)) continue;
|
|
63
167
|
try {
|
|
64
168
|
if (!statSync(absolutePath).isFile()) continue;
|
|
65
169
|
} catch {
|
|
@@ -67,7 +171,7 @@ function findWorkspaceFileMentions(text, cwd) {
|
|
|
67
171
|
}
|
|
68
172
|
mentions.set(absolutePath.toLowerCase(), {
|
|
69
173
|
absolutePath,
|
|
70
|
-
relativePath: relativePath.split(
|
|
174
|
+
relativePath: relativePath.split(sep2).join("/")
|
|
71
175
|
});
|
|
72
176
|
break;
|
|
73
177
|
}
|
|
@@ -1054,6 +1158,17 @@ function kittyKeyboardOverride(env = process.env) {
|
|
|
1054
1158
|
function isLateTerminalReply(input) {
|
|
1055
1159
|
return /^\u001b?\[\?[\d;]*[uc]$/.test(input);
|
|
1056
1160
|
}
|
|
1161
|
+
var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1006h";
|
|
1162
|
+
var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1006l";
|
|
1163
|
+
var MOUSE_INPUT = /^\u001b?\[<(\d+);\d+;\d+[Mm]$/;
|
|
1164
|
+
function isMouseInput(input) {
|
|
1165
|
+
return MOUSE_INPUT.test(input);
|
|
1166
|
+
}
|
|
1167
|
+
function wheelRows(input) {
|
|
1168
|
+
const button = Number(MOUSE_INPUT.exec(input)?.[1] ?? -1);
|
|
1169
|
+
if (button < 0 || (button & 64) === 0) return 0;
|
|
1170
|
+
return button & 1 ? -3 : 3;
|
|
1171
|
+
}
|
|
1057
1172
|
|
|
1058
1173
|
// src/tui/ink/components/prompt-input.tsx
|
|
1059
1174
|
import { Box, Text, useInput, usePaste } from "ink";
|
|
@@ -1071,7 +1186,8 @@ var palette = {
|
|
|
1071
1186
|
faint: "#64748b",
|
|
1072
1187
|
code: "#fcd34d",
|
|
1073
1188
|
diffAddBg: "#1b3a24",
|
|
1074
|
-
diffRemoveBg: "#3d1f1f"
|
|
1189
|
+
diffRemoveBg: "#3d1f1f",
|
|
1190
|
+
stickyBackground: "#1e293b"
|
|
1075
1191
|
};
|
|
1076
1192
|
var colors = {
|
|
1077
1193
|
primary: chalk.hex(palette.primary),
|
|
@@ -1098,7 +1214,7 @@ function PromptInput({
|
|
|
1098
1214
|
}) {
|
|
1099
1215
|
useSyncExternalStore(editor.subscribe, editor.getVersion);
|
|
1100
1216
|
useInput((input, key) => {
|
|
1101
|
-
if (onKey(input, key)) return;
|
|
1217
|
+
if (isMouseInput(input) || onKey(input, key)) return;
|
|
1102
1218
|
editor.handleKey(input, key);
|
|
1103
1219
|
});
|
|
1104
1220
|
usePaste((text) => editor.paste(text));
|
|
@@ -1384,12 +1500,13 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
|
|
|
1384
1500
|
function StatusLine({
|
|
1385
1501
|
ui,
|
|
1386
1502
|
model,
|
|
1387
|
-
mcp
|
|
1503
|
+
mcp,
|
|
1504
|
+
scroll = 0
|
|
1388
1505
|
}) {
|
|
1389
1506
|
const separator = /* @__PURE__ */ jsx2(Text2, { color: palette.faint, children: " | " });
|
|
1390
1507
|
const usage = ui.contextUsage ? `ctx:${formatTokenCount(ui.contextUsage.totalTokens)}${ui.contextUsage.contextWindow ? `/${formatTokenCount(ui.contextUsage.contextWindow)}` : ""}` : null;
|
|
1391
1508
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1392
|
-
ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). Run: ${ui.updateAvailable.command}` }) : null,
|
|
1509
|
+
ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). ${ui.updateAvailable.installable ? "Type /update to install it." : `Run: ${ui.updateAvailable.command}`}` }) : null,
|
|
1393
1510
|
ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
|
|
1394
1511
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
1395
1512
|
/* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
|
|
@@ -1412,6 +1529,10 @@ function StatusLine({
|
|
|
1412
1529
|
`mcp:${mcp.connected}/${mcp.configured}`,
|
|
1413
1530
|
mcp.failed ? /* @__PURE__ */ jsx2(Text2, { color: palette.danger, children: ` !${mcp.failed}` }) : null
|
|
1414
1531
|
] }) : null,
|
|
1532
|
+
scroll ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1533
|
+
separator,
|
|
1534
|
+
/* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `\u2191${scroll} lines \xB7 PgDn/Esc` })
|
|
1535
|
+
] }) : null,
|
|
1415
1536
|
mcp.skills ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1416
1537
|
separator,
|
|
1417
1538
|
`skills:${mcp.skills}`
|
|
@@ -1819,6 +1940,14 @@ var builtinCommands = [
|
|
|
1819
1940
|
ctx.exit();
|
|
1820
1941
|
}
|
|
1821
1942
|
},
|
|
1943
|
+
{
|
|
1944
|
+
name: "update",
|
|
1945
|
+
description: "Install the newer nova-ai from npm and restart in this session",
|
|
1946
|
+
run(ctx) {
|
|
1947
|
+
const message = ctx.requestUpdate();
|
|
1948
|
+
if (message) ctx.print(message);
|
|
1949
|
+
}
|
|
1950
|
+
},
|
|
1822
1951
|
{
|
|
1823
1952
|
name: "mode",
|
|
1824
1953
|
description: "Switch interaction mode (/mode agent|ask|plan)",
|
|
@@ -2198,10 +2327,10 @@ console.log(ObjC.unwrap(png.base64EncodedStringWithOptions(0)));
|
|
|
2198
2327
|
`;
|
|
2199
2328
|
|
|
2200
2329
|
// src/tui/files/find-fd.ts
|
|
2201
|
-
import { spawnSync } from "child_process";
|
|
2330
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2202
2331
|
var FD_CANDIDATES = ["fd", "fdfind"];
|
|
2203
2332
|
var cachedPath;
|
|
2204
|
-
function resolveFdPath(run = (command, args) =>
|
|
2333
|
+
function resolveFdPath(run = (command, args) => spawnSync2(command, args, { stdio: "ignore" })) {
|
|
2205
2334
|
if (cachedPath !== void 0) return cachedPath;
|
|
2206
2335
|
for (const candidate of FD_CANDIDATES) {
|
|
2207
2336
|
const result = run(candidate, ["--version"]);
|
|
@@ -2216,7 +2345,7 @@ function resolveFdPath(run = (command, args) => spawnSync(command, args, { stdio
|
|
|
2216
2345
|
|
|
2217
2346
|
// src/tui/files/list-workspace-files.ts
|
|
2218
2347
|
import { readdirSync } from "fs";
|
|
2219
|
-
import { join, relative as relative2, sep as
|
|
2348
|
+
import { join as join2, relative as relative2, sep as sep3 } from "path";
|
|
2220
2349
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".nova-ai", "out", "coverage", ".next", ".turbo"]);
|
|
2221
2350
|
function listWorkspaceFiles(cwd, max = 2e3) {
|
|
2222
2351
|
const results = [];
|
|
@@ -2232,12 +2361,12 @@ function listWorkspaceFiles(cwd, max = 2e3) {
|
|
|
2232
2361
|
for (const entry of entries) {
|
|
2233
2362
|
if (results.length >= max) break;
|
|
2234
2363
|
if (entry.isSymbolicLink()) continue;
|
|
2235
|
-
const full =
|
|
2364
|
+
const full = join2(dir, entry.name);
|
|
2236
2365
|
if (entry.isDirectory()) {
|
|
2237
2366
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2238
2367
|
stack.push(full);
|
|
2239
2368
|
} else if (entry.isFile()) {
|
|
2240
|
-
results.push(relative2(cwd, full).split(
|
|
2369
|
+
results.push(relative2(cwd, full).split(sep3).join("/"));
|
|
2241
2370
|
}
|
|
2242
2371
|
}
|
|
2243
2372
|
}
|
|
@@ -2268,7 +2397,7 @@ var DraftImageAttachments = class {
|
|
|
2268
2397
|
import { spawn } from "child_process";
|
|
2269
2398
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2270
2399
|
import { homedir } from "os";
|
|
2271
|
-
import { basename, dirname, join as
|
|
2400
|
+
import { basename, dirname as dirname2, join as join3 } from "path";
|
|
2272
2401
|
var PATH_DELIMITERS = /* @__PURE__ */ new Set([" ", " ", '"', "'", "="]);
|
|
2273
2402
|
var MAX_SUGGESTIONS2 = 20;
|
|
2274
2403
|
var WorkspaceAutocompleteProvider = class {
|
|
@@ -2349,14 +2478,14 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2349
2478
|
pathSuggestions(prefix) {
|
|
2350
2479
|
const quoted = prefix.startsWith('"');
|
|
2351
2480
|
const raw = quoted ? prefix.slice(1) : prefix;
|
|
2352
|
-
const expanded = raw === "~" || raw.startsWith("~/") ?
|
|
2481
|
+
const expanded = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
|
|
2353
2482
|
const absolute = expanded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(expanded);
|
|
2354
2483
|
const separator = Math.max(raw.lastIndexOf("/"), raw.lastIndexOf("\\"));
|
|
2355
2484
|
const listsDirectory = raw === "" || raw === "~" || separator === raw.length - 1;
|
|
2356
|
-
const directoryPart = listsDirectory ? expanded :
|
|
2485
|
+
const directoryPart = listsDirectory ? expanded : dirname2(expanded);
|
|
2357
2486
|
const namePrefix = listsDirectory ? "" : basename(expanded);
|
|
2358
2487
|
const displayDirectory = raw === "~" ? "~/" : separator >= 0 ? raw.slice(0, separator + 1) : "";
|
|
2359
|
-
const searchDirectory = absolute ? directoryPart :
|
|
2488
|
+
const searchDirectory = absolute ? directoryPart : join3(this.cwd, directoryPart);
|
|
2360
2489
|
let entries;
|
|
2361
2490
|
try {
|
|
2362
2491
|
entries = readdirSync2(searchDirectory, { withFileTypes: true });
|
|
@@ -2370,7 +2499,7 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2370
2499
|
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2371
2500
|
try {
|
|
2372
2501
|
isDirectory = statSync2(
|
|
2373
|
-
|
|
2502
|
+
join3(searchDirectory, entry.name)
|
|
2374
2503
|
).isDirectory();
|
|
2375
2504
|
} catch {
|
|
2376
2505
|
}
|
|
@@ -3494,17 +3623,11 @@ var TranscriptCommitter = class {
|
|
|
3494
3623
|
planSignature = null;
|
|
3495
3624
|
snapshot = null;
|
|
3496
3625
|
lastState = null;
|
|
3497
|
-
redrawRequested = false;
|
|
3498
|
-
/** The next sync starts a new epoch, e.g. after the terminal width changed. */
|
|
3499
|
-
requestRedraw() {
|
|
3500
|
-
this.redrawRequested = true;
|
|
3501
|
-
this.lastState = null;
|
|
3502
|
-
}
|
|
3503
3626
|
sync(state, expanded) {
|
|
3504
3627
|
if (this.snapshot && state === this.lastState && expanded === this.expanded)
|
|
3505
3628
|
return this.snapshot;
|
|
3506
3629
|
this.lastState = state;
|
|
3507
|
-
if (
|
|
3630
|
+
if (state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
|
|
3508
3631
|
this.startEpoch(state, expanded);
|
|
3509
3632
|
}
|
|
3510
3633
|
const messages = state.messages;
|
|
@@ -3657,7 +3780,6 @@ var TranscriptCommitter = class {
|
|
|
3657
3780
|
return true;
|
|
3658
3781
|
}
|
|
3659
3782
|
startEpoch(state, expanded) {
|
|
3660
|
-
this.redrawRequested = false;
|
|
3661
3783
|
this.epoch++;
|
|
3662
3784
|
this.sessionId = state.sessionId;
|
|
3663
3785
|
this.expanded = expanded;
|
|
@@ -3703,7 +3825,6 @@ function safeSplitOffset(text, from) {
|
|
|
3703
3825
|
}
|
|
3704
3826
|
|
|
3705
3827
|
// src/tui/ink/controller.ts
|
|
3706
|
-
var CLEAR_SCREEN_AND_SCROLLBACK = "\x1B[2J\x1B[3J\x1B[H";
|
|
3707
3828
|
var EXIT_CONFIRM_MS = 2e3;
|
|
3708
3829
|
var TuiController = class {
|
|
3709
3830
|
constructor(store, runner, io) {
|
|
@@ -3728,6 +3849,8 @@ var TuiController = class {
|
|
|
3728
3849
|
exiting = false;
|
|
3729
3850
|
view;
|
|
3730
3851
|
lastEpoch = 0;
|
|
3852
|
+
scroll = 0;
|
|
3853
|
+
viewport = { rows: 10, contentRows: 0 };
|
|
3731
3854
|
exitArmed = false;
|
|
3732
3855
|
exitTimer;
|
|
3733
3856
|
animationTimer;
|
|
@@ -3739,6 +3862,8 @@ var TuiController = class {
|
|
|
3739
3862
|
unsubscribeStore;
|
|
3740
3863
|
resolveExit = () => {
|
|
3741
3864
|
};
|
|
3865
|
+
/** Set by /update: the caller installs and restarts after the TUI exits. */
|
|
3866
|
+
updateRequested = false;
|
|
3742
3867
|
/** Settles once the user asked to quit; `run` then tears the view down. */
|
|
3743
3868
|
exitRequested = new Promise((resolve2) => {
|
|
3744
3869
|
this.resolveExit = resolve2;
|
|
@@ -3784,8 +3909,17 @@ var TuiController = class {
|
|
|
3784
3909
|
this.runner.cancel();
|
|
3785
3910
|
return true;
|
|
3786
3911
|
}
|
|
3912
|
+
if (this.scroll > 0) {
|
|
3913
|
+
this.scrollTo(0);
|
|
3914
|
+
return true;
|
|
3915
|
+
}
|
|
3787
3916
|
return false;
|
|
3788
3917
|
}
|
|
3918
|
+
if (key.pageUp || key.pageDown) {
|
|
3919
|
+
const page = Math.max(1, this.viewport.rows - 2);
|
|
3920
|
+
this.scrollBy(key.pageUp ? page : -page);
|
|
3921
|
+
return true;
|
|
3922
|
+
}
|
|
3789
3923
|
if (key.tab && key.shift) {
|
|
3790
3924
|
this.cycleInteractionMode();
|
|
3791
3925
|
return true;
|
|
@@ -4007,14 +4141,23 @@ ${skill.path}`
|
|
|
4007
4141
|
}
|
|
4008
4142
|
};
|
|
4009
4143
|
/**
|
|
4010
|
-
*
|
|
4011
|
-
*
|
|
4012
|
-
* erased exactly (Ink leaves stale prompt borders), and old scrollback keeps
|
|
4013
|
-
* the old wrapping.
|
|
4144
|
+
* The view reports its transcript size after each layout. While scrolled
|
|
4145
|
+
* up, new output below grows the offset so the rows being read stay put.
|
|
4014
4146
|
*/
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
this.
|
|
4147
|
+
setViewport(rows, contentRows) {
|
|
4148
|
+
const grown = contentRows - this.viewport.contentRows;
|
|
4149
|
+
this.viewport = { rows, contentRows };
|
|
4150
|
+
this.scrollTo(this.scroll > 0 && grown > 0 ? this.scroll + grown : this.scroll);
|
|
4151
|
+
}
|
|
4152
|
+
/** Positive scrolls toward older output. */
|
|
4153
|
+
scrollBy(rows) {
|
|
4154
|
+
this.scrollTo(this.scroll + rows);
|
|
4155
|
+
}
|
|
4156
|
+
scrollTo(offset) {
|
|
4157
|
+
const max = Math.max(0, this.viewport.contentRows - this.viewport.rows);
|
|
4158
|
+
const next = Math.max(0, Math.min(offset, max));
|
|
4159
|
+
if (next === this.scroll) return;
|
|
4160
|
+
this.scroll = next;
|
|
4018
4161
|
this.refresh();
|
|
4019
4162
|
}
|
|
4020
4163
|
toggleToolDetails() {
|
|
@@ -4063,6 +4206,19 @@ ${skill.path}`
|
|
|
4063
4206
|
clear: () => void this.runner.startNewSession(),
|
|
4064
4207
|
newSession: () => this.runner.startNewSession(),
|
|
4065
4208
|
exit: () => this.requestExit(),
|
|
4209
|
+
requestUpdate: () => {
|
|
4210
|
+
const update = this.store.getState().updateAvailable;
|
|
4211
|
+
if (!update) return "No newer nova-ai found on npm.";
|
|
4212
|
+
if (!update.installable) {
|
|
4213
|
+
return `nova-ai ${update.latestVersion} is available, but this copy wasn't installed with npm -g, so it can't update itself. Run: ${update.command}`;
|
|
4214
|
+
}
|
|
4215
|
+
if (this.store.getState().busy) {
|
|
4216
|
+
return "Wait for the current request to finish (or press Esc), then run /update again.";
|
|
4217
|
+
}
|
|
4218
|
+
this.updateRequested = true;
|
|
4219
|
+
this.requestExit();
|
|
4220
|
+
return null;
|
|
4221
|
+
},
|
|
4066
4222
|
resumeSession: (sessionId) => {
|
|
4067
4223
|
const id = sessionId ?? listSessionsForCwd(this.runner.cwd)[0]?.sessionId;
|
|
4068
4224
|
return id ? this.runner.resumeFrom(id) : false;
|
|
@@ -4102,6 +4258,7 @@ ${skill.path}`
|
|
|
4102
4258
|
submit = async (text, images = [], pastes = []) => {
|
|
4103
4259
|
const value = text.trim();
|
|
4104
4260
|
if (!value) return;
|
|
4261
|
+
this.scroll = 0;
|
|
4105
4262
|
this.editor.addToHistory(value);
|
|
4106
4263
|
this.store.setState((state) => ({
|
|
4107
4264
|
inputHistory: [...state.inputHistory, value],
|
|
@@ -4203,7 +4360,10 @@ ${skill.path}`
|
|
|
4203
4360
|
this.view = this.buildView();
|
|
4204
4361
|
if (this.view.transcript.epoch !== this.lastEpoch) {
|
|
4205
4362
|
this.lastEpoch = this.view.transcript.epoch;
|
|
4206
|
-
this.
|
|
4363
|
+
if (this.scroll) {
|
|
4364
|
+
this.scroll = 0;
|
|
4365
|
+
this.view = this.buildView();
|
|
4366
|
+
}
|
|
4207
4367
|
}
|
|
4208
4368
|
for (const listener of this.listeners) listener();
|
|
4209
4369
|
}
|
|
@@ -4217,7 +4377,8 @@ ${skill.path}`
|
|
|
4217
4377
|
model: this.runner.model,
|
|
4218
4378
|
mcp: mcpCounts(this.runner),
|
|
4219
4379
|
expandedTools: this.expandedTools,
|
|
4220
|
-
exiting: this.exiting
|
|
4380
|
+
exiting: this.exiting,
|
|
4381
|
+
scroll: this.scroll
|
|
4221
4382
|
};
|
|
4222
4383
|
}
|
|
4223
4384
|
questionState(pending) {
|
|
@@ -4287,7 +4448,7 @@ function useDialogInput(handle, isActive) {
|
|
|
4287
4448
|
const [, setTick] = useState(0);
|
|
4288
4449
|
useInput2(
|
|
4289
4450
|
(input, key) => {
|
|
4290
|
-
if (handle(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4451
|
+
if (!isMouseInput(input) && handle(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4291
4452
|
},
|
|
4292
4453
|
{ isActive }
|
|
4293
4454
|
);
|
|
@@ -4429,7 +4590,7 @@ function QuestionDialog({
|
|
|
4429
4590
|
const [, setTick] = useState(0);
|
|
4430
4591
|
useInput2(
|
|
4431
4592
|
(input, key) => {
|
|
4432
|
-
if (state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4593
|
+
if (!isMouseInput(input) && state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4433
4594
|
},
|
|
4434
4595
|
{ isActive }
|
|
4435
4596
|
);
|
|
@@ -4511,7 +4672,7 @@ function PermissionDialog({
|
|
|
4511
4672
|
}) {
|
|
4512
4673
|
useInput2(
|
|
4513
4674
|
(input, key) => {
|
|
4514
|
-
if (permissionKey(request, input, key) === "closed") onClose();
|
|
4675
|
+
if (!isMouseInput(input) && permissionKey(request, input, key) === "closed") onClose();
|
|
4515
4676
|
},
|
|
4516
4677
|
{ isActive }
|
|
4517
4678
|
);
|
|
@@ -4562,9 +4723,8 @@ function wrapPlain(text, width) {
|
|
|
4562
4723
|
}
|
|
4563
4724
|
|
|
4564
4725
|
// src/tui/ink/app.tsx
|
|
4565
|
-
import {
|
|
4726
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
4566
4727
|
var FRAME_MS = 120;
|
|
4567
|
-
var RESIZE_SETTLE_MS = 100;
|
|
4568
4728
|
async function runInkTui(credentials, cwd, args, options = {}) {
|
|
4569
4729
|
const store = createStore({
|
|
4570
4730
|
messages: [],
|
|
@@ -4606,22 +4766,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
|
|
|
4606
4766
|
// Ink otherwise turns non-interactive whenever CI is set in the
|
|
4607
4767
|
// environment; the TUI only runs on a terminal anyway.
|
|
4608
4768
|
interactive: Boolean(stdout.isTTY),
|
|
4769
|
+
// Full screen: the transcript scrolls inside the app (PgUp/PgDn) and the
|
|
4770
|
+
// terminal gets its own screen back on exit.
|
|
4771
|
+
alternateScreen: Boolean(stdout.isTTY),
|
|
4609
4772
|
kittyKeyboard: { mode: kittyKeyboard ? "enabled" : "disabled" }
|
|
4610
4773
|
});
|
|
4611
4774
|
controller.start();
|
|
4775
|
+
if (stdout.isTTY) stdout.write(MOUSE_REPORTING_ON);
|
|
4612
4776
|
const onSignal = () => controller.requestExit();
|
|
4613
4777
|
process.once("SIGTERM", onSignal);
|
|
4614
|
-
let printedColumns = stdout.columns;
|
|
4615
|
-
let resizeTimer;
|
|
4616
|
-
const onResize = () => {
|
|
4617
|
-
if (resizeTimer) clearTimeout(resizeTimer);
|
|
4618
|
-
resizeTimer = setTimeout(() => {
|
|
4619
|
-
if (stdout.columns === printedColumns) return;
|
|
4620
|
-
printedColumns = stdout.columns;
|
|
4621
|
-
controller.redrawAll();
|
|
4622
|
-
}, RESIZE_SETTLE_MS);
|
|
4623
|
-
};
|
|
4624
|
-
stdout.on("resize", onResize);
|
|
4625
4778
|
void (options.checkForUpdate ?? checkForUpdate)().then((updateAvailable) => {
|
|
4626
4779
|
if (updateAvailable) store.setState({ updateAvailable });
|
|
4627
4780
|
});
|
|
@@ -4631,45 +4784,99 @@ async function runInkTui(credentials, cwd, args, options = {}) {
|
|
|
4631
4784
|
await controller.stop();
|
|
4632
4785
|
} finally {
|
|
4633
4786
|
process.off("SIGTERM", onSignal);
|
|
4634
|
-
stdout.
|
|
4635
|
-
if (resizeTimer) clearTimeout(resizeTimer);
|
|
4787
|
+
if (stdout.isTTY) stdout.write(MOUSE_REPORTING_OFF);
|
|
4636
4788
|
instance.unmount();
|
|
4637
4789
|
await instance.waitUntilExit().catch(() => {
|
|
4638
4790
|
});
|
|
4639
4791
|
}
|
|
4640
4792
|
stdout.write(`Resume this session with: nova-ai --resume ${runner.sessionId}
|
|
4641
4793
|
`);
|
|
4794
|
+
return { sessionId: runner.sessionId, updateRequested: controller.updateRequested };
|
|
4642
4795
|
}
|
|
4643
4796
|
function App({ controller }) {
|
|
4644
4797
|
const view = useSyncExternalStore2(controller.subscribe, controller.getView);
|
|
4645
4798
|
const { rows, columns } = useWindowSize();
|
|
4646
4799
|
const bottomRef = useRef(null);
|
|
4647
4800
|
const bottom = useBoxMetrics(bottomRef);
|
|
4801
|
+
const contentRef = useRef(null);
|
|
4802
|
+
const content = useBoxMetrics(contentRef);
|
|
4648
4803
|
const working = useWorking(view);
|
|
4804
|
+
const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
|
|
4805
|
+
useInput3((input) => {
|
|
4806
|
+
const rows2 = wheelRows(input);
|
|
4807
|
+
if (rows2) controller.scrollBy(rows2);
|
|
4808
|
+
});
|
|
4809
|
+
const height = Math.max(4, rows - 1);
|
|
4810
|
+
const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
|
|
4811
|
+
useEffect(() => {
|
|
4812
|
+
if (content.hasMeasured) controller.setViewport(viewportRows, content.height);
|
|
4813
|
+
}, [controller, viewportRows, content.hasMeasured, content.height]);
|
|
4814
|
+
if (view.exiting) return null;
|
|
4649
4815
|
const { transcript } = view;
|
|
4650
|
-
const
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4657
|
-
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4816
|
+
const blocks = [...transcript.committed, ...transcript.live];
|
|
4817
|
+
const liveKeys = new Set(transcript.live.map((block) => block.key));
|
|
4818
|
+
const onUserTop = (key, top) => setUserTops((tops) => tops.get(key) === top ? tops : new Map(tops).set(key, top));
|
|
4819
|
+
const firstVisibleRow = content.height - viewportRows - view.scroll;
|
|
4820
|
+
let sticky = null;
|
|
4821
|
+
for (const block of blocks) {
|
|
4822
|
+
if (block.kind !== "user") continue;
|
|
4823
|
+
const top = userTops.get(block.key);
|
|
4824
|
+
if (top !== void 0 && top < firstVisibleRow) sticky = block.text;
|
|
4825
|
+
}
|
|
4826
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height, children: [
|
|
4827
|
+
/* @__PURE__ */ jsxs6(
|
|
4828
|
+
Box6,
|
|
4829
|
+
{
|
|
4830
|
+
flexDirection: "column",
|
|
4831
|
+
height: viewportRows,
|
|
4832
|
+
flexShrink: 0,
|
|
4833
|
+
overflowY: "hidden",
|
|
4834
|
+
justifyContent: "flex-end",
|
|
4835
|
+
children: [
|
|
4836
|
+
/* @__PURE__ */ jsxs6(Box6, { ref: contentRef, flexDirection: "column", flexShrink: 0, marginBottom: -view.scroll, children: [
|
|
4837
|
+
blocks.map(
|
|
4838
|
+
(block) => liveKeys.has(block.key) ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key) : /* @__PURE__ */ jsx6(CommittedBlock, { block, onUserTop }, block.key)
|
|
4839
|
+
),
|
|
4840
|
+
working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
4841
|
+
/* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
|
|
4842
|
+
/* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
|
|
4843
|
+
] }) }) : null
|
|
4844
|
+
] }),
|
|
4845
|
+
sticky !== null ? /* @__PURE__ */ jsx6(StickyPrompt, { text: sticky, columns }) : null
|
|
4846
|
+
]
|
|
4847
|
+
}
|
|
4848
|
+
),
|
|
4849
|
+
/* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
|
|
4850
|
+
/* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
|
|
4851
|
+
/* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp, scroll: view.scroll })
|
|
4670
4852
|
] })
|
|
4671
4853
|
] });
|
|
4672
4854
|
}
|
|
4855
|
+
var CommittedBlock = memo(function CommittedBlock2({
|
|
4856
|
+
block,
|
|
4857
|
+
onUserTop
|
|
4858
|
+
}) {
|
|
4859
|
+
if (block.kind !== "user") {
|
|
4860
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
4861
|
+
}
|
|
4862
|
+
return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
4863
|
+
}, (before, after) => before.block === after.block);
|
|
4864
|
+
function UserAnchor({
|
|
4865
|
+
block,
|
|
4866
|
+
onUserTop,
|
|
4867
|
+
children
|
|
4868
|
+
}) {
|
|
4869
|
+
const ref = useRef(null);
|
|
4870
|
+
const { top, hasMeasured } = useBoxMetrics(ref);
|
|
4871
|
+
useEffect(() => {
|
|
4872
|
+
if (hasMeasured) onUserTop(block.key, top);
|
|
4873
|
+
}, [block.key, top, hasMeasured]);
|
|
4874
|
+
return /* @__PURE__ */ jsx6(Box6, { ref, marginTop: 1, children });
|
|
4875
|
+
}
|
|
4876
|
+
function StickyPrompt({ text, columns }) {
|
|
4877
|
+
const line = `> ${text.replace(/\s+/g, " ").trim()}`;
|
|
4878
|
+
return /* @__PURE__ */ jsx6(Box6, { position: "absolute", top: 0, left: 0, width: columns, children: /* @__PURE__ */ jsx6(Text6, { color: palette.primary, backgroundColor: palette.stickyBackground, wrap: "truncate-end", children: line.padEnd(columns) }) });
|
|
4879
|
+
}
|
|
4673
4880
|
function Surface({
|
|
4674
4881
|
controller,
|
|
4675
4882
|
view,
|
|
@@ -4736,7 +4943,23 @@ async function runChat(options) {
|
|
|
4736
4943
|
console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
|
|
4737
4944
|
return 1;
|
|
4738
4945
|
}
|
|
4739
|
-
const
|
|
4946
|
+
const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL ?? await pickDefaultModel(stored);
|
|
4947
|
+
if (!model) {
|
|
4948
|
+
console.error("nova-ai has no model to use yet. Start it with one, for example:");
|
|
4949
|
+
console.error(" nova-ai --model <model-id> (or set NOVA_MODEL)");
|
|
4950
|
+
console.error("Then Ctrl+P in nova-ai switches and remembers the model.");
|
|
4951
|
+
return 1;
|
|
4952
|
+
}
|
|
4953
|
+
const credentials = { ...stored, defaultModel: model };
|
|
4954
|
+
const updateCheck = checkForUpdate().then((update) => withInstallability(update));
|
|
4955
|
+
const updatedFrom = process.env[UPDATED_FROM_ENV];
|
|
4956
|
+
if (updatedFrom) {
|
|
4957
|
+
delete process.env[UPDATED_FROM_ENV];
|
|
4958
|
+
console.log(`Updated nova-ai ${updatedFrom} \u2192 ${cliVersion() ?? "?"}.`);
|
|
4959
|
+
} else {
|
|
4960
|
+
const restartedCode = await offerUpdateAtStart(updateCheck, process.argv.slice(2));
|
|
4961
|
+
if (restartedCode !== null) return restartedCode;
|
|
4962
|
+
}
|
|
4740
4963
|
const cwd = process.cwd();
|
|
4741
4964
|
let resume = options.resume;
|
|
4742
4965
|
if (options.continueLast) {
|
|
@@ -4748,11 +4971,35 @@ async function runChat(options) {
|
|
|
4748
4971
|
}
|
|
4749
4972
|
if (workspaceNeedsTrust(cwd)) await askToTrust(cwd);
|
|
4750
4973
|
const args = resume ? ["--resume", resume] : [];
|
|
4751
|
-
await runInkTui(credentials, cwd, args);
|
|
4974
|
+
const result = await runInkTui(credentials, cwd, args, { checkForUpdate: () => updateCheck });
|
|
4975
|
+
if (result.updateRequested) {
|
|
4976
|
+
const update = await updateCheck;
|
|
4977
|
+
if (update) {
|
|
4978
|
+
const restartArgs = [
|
|
4979
|
+
...loadStoredSession2(result.sessionId) ? ["--resume", result.sessionId] : [],
|
|
4980
|
+
...options.model ? ["--model", options.model] : []
|
|
4981
|
+
];
|
|
4982
|
+
const restartedCode = installAndRestart(update, restartArgs);
|
|
4983
|
+
if (restartedCode !== null) return restartedCode;
|
|
4984
|
+
}
|
|
4985
|
+
}
|
|
4752
4986
|
return 0;
|
|
4753
4987
|
}
|
|
4988
|
+
async function pickDefaultModel(credentials) {
|
|
4989
|
+
try {
|
|
4990
|
+
const { data } = await createNovaClient2(credentials.apiKey).models.list({ limit: 100 });
|
|
4991
|
+
const model = resolveDefaultModel2(data, void 0);
|
|
4992
|
+
if (model) {
|
|
4993
|
+
saveDefaultModel2(model);
|
|
4994
|
+
console.log(`Using model ${model} (Ctrl+P switches).`);
|
|
4995
|
+
}
|
|
4996
|
+
return model;
|
|
4997
|
+
} catch {
|
|
4998
|
+
return void 0;
|
|
4999
|
+
}
|
|
5000
|
+
}
|
|
4754
5001
|
async function askToTrust(cwd) {
|
|
4755
|
-
const prompt =
|
|
5002
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
4756
5003
|
try {
|
|
4757
5004
|
console.log(`
|
|
4758
5005
|
${cwd}`);
|
|
@@ -4,9 +4,11 @@ import {
|
|
|
4
4
|
formatUpdateNotice,
|
|
5
5
|
isNewerVersion,
|
|
6
6
|
readInstalledVersion,
|
|
7
|
+
rememberUpdateAnswer,
|
|
8
|
+
shouldAskAboutUpdate,
|
|
7
9
|
updateCachePath,
|
|
8
10
|
updateChecksDisabled
|
|
9
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-L77KQRGZ.js";
|
|
10
12
|
import "./chunk-FMHWW3E6.js";
|
|
11
13
|
export {
|
|
12
14
|
checkForUpdate,
|
|
@@ -14,6 +16,8 @@ export {
|
|
|
14
16
|
formatUpdateNotice,
|
|
15
17
|
isNewerVersion,
|
|
16
18
|
readInstalledVersion,
|
|
19
|
+
rememberUpdateAnswer,
|
|
20
|
+
shouldAskAboutUpdate,
|
|
17
21
|
updateCachePath,
|
|
18
22
|
updateChecksDisabled
|
|
19
23
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalabrotterdam/nova-ai-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.5",
|
|
4
4
|
"description": "Agentic coding with DataLab Rotterdam's Nova AI in your terminal: interactive TUI, headless mode, and an ACP agent for editors",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "DataLab Rotterdam",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@agentclientprotocol/sdk": "^0.29.0",
|
|
42
|
-
"@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.
|
|
42
|
+
"@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.2",
|
|
43
43
|
"@datalabrotterdam/nova-sdk": "^1.3.2",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"cli-highlight": "^2.1.11",
|