@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 says so, and so do `-p` and
48
- `--version` (on stderr, only in a terminal). npm is asked at most once a day;
49
- an alpha install also follows the `alpha` channel. `NOVA_NO_UPDATE_CHECK=1`
50
- turns it off (it is off in CI).
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 renders inline below your shell prompt. Finished messages go to the
68
- terminal's own scrollback (scroll, search and copy as usual); only the
69
- streaming answer, running tools, the prompt and the status rows are redrawn.
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-OX6WAPNN.js").then(({ checkForUpdate }) => checkForUpdate({ signal: abort.signal })).catch(() => null);
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-OX6WAPNN.js");
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-LNFSHL4Y.js");
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
- } from "./chunk-37PMUZ34.js";
4
- import "./chunk-FMHWW3E6.js";
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 { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
15
- import { listStoredSessions as listStoredSessions2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
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, Static, Text as Text6, useBoxMetrics, useWindowSize } from "ink";
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, sep);
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(`..${sep}`) || relativePath === ".." || isAbsolute(relativePath)) continue;
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(sep).join("/")
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) => spawnSync(command, args, { stdio: "ignore" })) {
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 sep2 } from "path";
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 = join(dir, entry.name);
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(sep2).join("/"));
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 join2 } from "path";
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("~/") ? join2(homedir(), raw.slice(1)) : raw;
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 : dirname(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 : join2(this.cwd, 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
- join2(searchDirectory, entry.name)
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 (this.redrawRequested || state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
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
- * Prints the whole transcript again. After the width changed, the terminal
4011
- * re-wrapped what is on screen on its own and the live area can't be
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
- redrawAll() {
4016
- if (this.stopped || this.exiting) return;
4017
- this.committer.requestRedraw();
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.io.write(CLEAR_SCREEN_AND_SCROLLBACK);
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 { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
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.off("resize", onResize);
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 scrollback = (
4651
- // <Static> is laid out as an absolute box; without a width its text
4652
- // wraps against its content and runs past the terminal edge.
4653
- /* @__PURE__ */ jsx6(Static, { items: [...transcript.committed], style: { width: columns }, children: (block) => /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) }, block.key) }, transcript.epoch)
4654
- );
4655
- if (view.exiting) return scrollback;
4656
- const liveRows = Math.max(3, rows - (bottom.hasMeasured ? bottom.height : 6) - 2);
4657
- const dialogRows = Math.max(6, rows - 4);
4658
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
4659
- scrollback,
4660
- /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", maxHeight: liveRows, overflowY: "hidden", justifyContent: "flex-end", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexShrink: 0, children: [
4661
- transcript.live.map((block) => /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key)),
4662
- working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
4663
- /* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
4664
- /* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
4665
- ] }) }) : null
4666
- ] }) }),
4667
- /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", marginTop: 1, children: [
4668
- /* @__PURE__ */ jsx6(Surface, { controller, view, rows: dialogRows, columns }),
4669
- /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp })
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 credentials = options.model ? { ...stored, defaultModel: options.model } : stored;
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 = createInterface({ input: process.stdin, output: process.stdout });
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-37PMUZ34.js";
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",
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.1",
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",