@datalabrotterdam/nova-ai-cli 1.2.0-alpha.4 → 1.2.0-alpha.6

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.6](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.5...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.6) (2026-10-23)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** start the session before the first prompt ([0c26456](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/0c26456b8126e3b42a6de2e38212213ae5380e62))
7
+
8
+
9
+ ### Features
10
+
11
+ * **agent:** set workspace trust over ACP ([cec4a89](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/cec4a8949a4232f46448219e4d5363289651215d))
12
+
13
+ # [@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)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **cli:** keep the full-screen frame one row short of the terminal ([37355f4](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/37355f4efb8a237f204c061ad7008faf54a0dd9e))
19
+ * **cli:** pick the account's default model when none is set ([fb0f34c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/fb0f34c7bac26f1b59c4b6b2245e6fb8365f99f1))
20
+
21
+
22
+ ### Features
23
+
24
+ * **cli:** full-screen TUI with a pinned prompt and mouse-wheel scrolling ([a68d693](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/a68d6930945bd6440de22c709fc14ae32a9c9f65))
25
+
1
26
  # [@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)
2
27
 
3
28
 
package/README.md CHANGED
@@ -71,9 +71,11 @@ Interactive options:
71
71
 
72
72
  ## Terminal UI
73
73
 
74
- The UI renders inline below your shell prompt. Finished messages go to the
75
- terminal's own scrollback (scroll, search and copy as usual); only the
76
- 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.
77
79
 
78
80
  - **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
79
81
  questions and builds a task list but changes nothing; `ask` only answers.
@@ -1,147 +1,3 @@
1
- // src/tui/settings/workspace-mcp.ts
2
- import { readFileSync as readFileSync2 } from "fs";
3
- import { join as join2 } from "path";
4
- import { isWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
5
- import { readSettings } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
6
-
7
- // src/tui/settings/workspace-settings.ts
8
- import { readFileSync } from "fs";
9
- import { join } from "path";
10
- function settingsPath(cwd) {
11
- return join(cwd, ".nova-ai", "settings.json");
12
- }
13
- function readWorkspaceSettings(cwd) {
14
- try {
15
- const raw = readFileSync(settingsPath(cwd), "utf8");
16
- return JSON.parse(raw);
17
- } catch {
18
- return {};
19
- }
20
- }
21
-
22
- // src/tui/settings/workspace-mcp.ts
23
- var UNTRUSTED_MCP_MESSAGE = "Not started: MCP servers declared by this workspace only run once you trust it (/trust in the TUI, --trust-workspace headless).";
24
- function readWorkspaceMcpConfiguration(cwd, options = {}) {
25
- const configured = readWorkspaceSettings(cwd).mcpServers ?? [];
26
- const project = readProjectMcpConfiguration(cwd);
27
- const servers = new Map(configured.map((server) => [server.name, server]));
28
- for (const server of project.servers) servers.set(server.name, server);
29
- const declared = [...servers.values()];
30
- if (declared.length && !(options.trusted ?? isWorkspaceTrusted(cwd))) {
31
- return {
32
- servers: [],
33
- failures: [
34
- ...project.failures,
35
- ...declared.map((server) => ({ serverName: server.name, message: UNTRUSTED_MCP_MESSAGE }))
36
- ],
37
- untrusted: declared.map((server) => server.name)
38
- };
39
- }
40
- return { servers: declared, failures: project.failures };
41
- }
42
- function workspaceNeedsTrust(cwd) {
43
- if (isWorkspaceTrusted(cwd)) return false;
44
- if (readWorkspaceMcpConfiguration(cwd, { trusted: true }).servers.length) return true;
45
- return loadWorkspaceAllowRules(cwd).length > 0;
46
- }
47
- function loadWorkspaceAllowRules(cwd) {
48
- return ["settings.json", "settings.local.json"].flatMap((name) => {
49
- const allow = readSettings(join2(cwd, ".nova-ai", name))?.permissions?.allow;
50
- return Array.isArray(allow) ? allow : [];
51
- });
52
- }
53
- function readProjectMcpConfiguration(cwd) {
54
- const path = join2(cwd, ".mcp.json");
55
- let raw;
56
- try {
57
- raw = readFileSync2(path, "utf8");
58
- } catch (error) {
59
- if (error.code === "ENOENT") return { servers: [], failures: [] };
60
- return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Could not read file") }] };
61
- }
62
- let document;
63
- try {
64
- document = JSON.parse(raw);
65
- } catch (error) {
66
- return { servers: [], failures: [{ serverName: ".mcp.json", message: errorMessage(error, "Invalid JSON") }] };
67
- }
68
- const root = asRecord(document);
69
- const definitions = root ? asRecord(root.mcpServers) : null;
70
- if (!definitions) {
71
- return {
72
- servers: [],
73
- failures: [{ serverName: ".mcp.json", message: 'Expected an "mcpServers" object.' }]
74
- };
75
- }
76
- const servers = [];
77
- const failures = [];
78
- for (const [name, value] of Object.entries(definitions)) {
79
- const config = asRecord(value);
80
- if (config?.disabled === true || config?.enabled === false) continue;
81
- try {
82
- servers.push(normalizeProjectServer(name, config));
83
- } catch (error) {
84
- failures.push({ serverName: `.mcp.json:${name}`, message: errorMessage(error, "Invalid server configuration") });
85
- }
86
- }
87
- return { servers, failures };
88
- }
89
- function normalizeProjectServer(name, config) {
90
- if (!config) throw new Error("Server definition must be an object.");
91
- const type = typeof config.type === "string" ? config.type : config.url ? "http" : "stdio";
92
- if (type === "http" || type === "sse") {
93
- if (typeof config.url !== "string" || !config.url) throw new Error(`${type} server requires a URL.`);
94
- try {
95
- new URL(config.url);
96
- } catch {
97
- throw new Error(`${type} server requires a valid URL.`);
98
- }
99
- return {
100
- name,
101
- type,
102
- url: config.url,
103
- headers: normalizePairs(config.headers, "headers")
104
- };
105
- }
106
- if (type !== "stdio") throw new Error(`Unsupported transport type: ${type}.`);
107
- if (typeof config.command !== "string" || !config.command) throw new Error("stdio server requires a command.");
108
- if (config.args !== void 0 && (!Array.isArray(config.args) || !config.args.every((arg) => typeof arg === "string"))) {
109
- throw new Error("args must be an array of strings.");
110
- }
111
- return {
112
- name,
113
- command: config.command,
114
- args: config.args ?? [],
115
- env: normalizePairs(config.env, "env")
116
- };
117
- }
118
- function normalizePairs(value, label) {
119
- if (value === void 0) return [];
120
- if (Array.isArray(value)) {
121
- if (!value.every((entry) => {
122
- const pair = asRecord(entry);
123
- return typeof pair?.name === "string" && typeof pair.value === "string";
124
- })) {
125
- throw new Error(`${label} must contain name/value strings.`);
126
- }
127
- return value.map((entry) => {
128
- const pair = entry;
129
- return { name: pair.name, value: pair.value };
130
- });
131
- }
132
- const record = asRecord(value);
133
- if (!record || !Object.values(record).every((entry) => typeof entry === "string")) {
134
- throw new Error(`${label} must be an object of string values.`);
135
- }
136
- return Object.entries(record).map(([name, entry]) => ({ name, value: entry }));
137
- }
138
- function asRecord(value) {
139
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
140
- }
141
- function errorMessage(error, fallback) {
142
- return error instanceof Error && error.message ? `${fallback}: ${error.message}` : fallback;
143
- }
144
-
145
1
  // src/tui/session/tui-acp-client.ts
146
2
  import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
147
3
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -746,8 +602,6 @@ function createStore(initial) {
746
602
  }
747
603
 
748
604
  export {
749
- readWorkspaceMcpConfiguration,
750
- workspaceNeedsTrust,
751
605
  TuiAcpClient,
752
606
  choosePermissionOption,
753
607
  createStore
@@ -1,9 +1,8 @@
1
1
  import {
2
2
  TuiAcpClient,
3
3
  choosePermissionOption,
4
- createStore,
5
- readWorkspaceMcpConfiguration
6
- } from "./chunk-NHGIUEIR.js";
4
+ createStore
5
+ } from "./chunk-QSNU4H55.js";
7
6
 
8
7
  // src/headless/index.ts
9
8
  import { resolve } from "path";
@@ -12,7 +11,6 @@ import * as acp from "@agentclientprotocol/sdk";
12
11
  import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
13
12
  import { clientFromContext } from "@datalabrotterdam/nova-ai-agent/client/context-client.js";
14
13
  import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
15
- import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
16
14
  import { listStoredSessions, loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
17
15
 
18
16
  // src/headless/client.ts
@@ -191,15 +189,19 @@ ${HEADLESS_HELP}
191
189
  dependencies.agent ?? new NovaAgent()
192
190
  );
193
191
  const agent = connection.client;
194
- if (options.trustWorkspace) setWorkspaceTrusted(options.cwd, true);
195
- const mcpConfiguration = options.mcp ? readWorkspaceMcpConfiguration(options.cwd) : { servers: [], failures: [] };
196
- for (const failure of mcpConfiguration.failures) {
197
- output.write({
198
- type: "mcp.configuration_error",
199
- serverName: failure.serverName,
200
- message: failure.message
201
- });
202
- }
192
+ const sessionMeta = { "nova-ai-cli/workspaceMcp": options.mcp };
193
+ const reportMcpFailures = (meta) => {
194
+ const failures = toRecord(toRecord(meta)?.["nova-ai-cli/mcp"])?.failures;
195
+ for (const failure of Array.isArray(failures) ? failures : []) {
196
+ const item = toRecord(failure);
197
+ if (typeof item?.serverName !== "string" || typeof item.message !== "string") continue;
198
+ output.write({
199
+ type: "mcp.configuration_error",
200
+ serverName: item.serverName,
201
+ message: item.message
202
+ });
203
+ }
204
+ };
203
205
  let sessionId = null;
204
206
  let interrupted = false;
205
207
  const onInterrupt = () => {
@@ -215,18 +217,23 @@ ${HEADLESS_HELP}
215
217
  clientCapabilities: client.capabilities,
216
218
  clientInfo: { name: "nova-ai-cli-headless", version: "1" }
217
219
  });
220
+ if (options.trustWorkspace) await agent.workspaceTrust({ cwd: options.cwd, trusted: true });
218
221
  if (options.resume) {
219
- await agent.resumeSession({
222
+ const resumed = await agent.resumeSession({
220
223
  sessionId: options.resume,
221
224
  cwd: options.cwd,
222
- mcpServers: mcpConfiguration.servers
225
+ mcpServers: [],
226
+ _meta: sessionMeta
223
227
  });
228
+ reportMcpFailures(resumed._meta);
224
229
  sessionId = options.resume;
225
230
  } else {
226
231
  const created = await agent.newSession({
227
232
  cwd: options.cwd,
228
- mcpServers: mcpConfiguration.servers
233
+ mcpServers: [],
234
+ _meta: sessionMeta
229
235
  });
236
+ reportMcpFailures(created._meta);
230
237
  sessionId = created.sessionId;
231
238
  }
232
239
  await agent.setSessionConfigOption({
package/dist/index.js CHANGED
@@ -140,7 +140,7 @@ ${MAIN_HELP}
140
140
  }
141
141
  case "print": {
142
142
  const update = startUpdateCheck();
143
- const { default: runHeadless } = await import("./headless-OFJFVJ3O.js");
143
+ const { default: runHeadless } = await import("./headless-C36E4XSQ.js");
144
144
  const code = await runHeadless(command.args);
145
145
  if (update) await noticeUpdate(update, 500);
146
146
  return code;
@@ -175,7 +175,7 @@ ${MAIN_HELP}
175
175
  );
176
176
  return 2;
177
177
  }
178
- const { default: runChat } = await import("./tui-OWU4YGTT.js");
178
+ const { default: runChat } = await import("./tui-TRADKND7.js");
179
179
  return runChat(options);
180
180
  }
181
181
  }
@@ -9,16 +9,14 @@ import {
9
9
  } from "./chunk-FMHWW3E6.js";
10
10
  import {
11
11
  TuiAcpClient,
12
- createStore,
13
- readWorkspaceMcpConfiguration,
14
- workspaceNeedsTrust
15
- } from "./chunk-NHGIUEIR.js";
12
+ createStore
13
+ } from "./chunk-QSNU4H55.js";
16
14
 
17
15
  // src/tui/index.ts
18
16
  import { createInterface as createInterface2 } from "readline/promises";
19
17
  import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
20
18
  import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
21
- import { setWorkspaceTrusted as setWorkspaceTrusted2 } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
19
+ import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
22
20
 
23
21
  // src/update-install.ts
24
22
  import { spawnSync } from "child_process";
@@ -115,16 +113,12 @@ The update did not install. Run it yourself: ${update.command}
115
113
  }
116
114
 
117
115
  // src/tui/ink/app.tsx
118
- import { Box as Box6, render, Static, Text as Text6, useBoxMetrics, useWindowSize } from "ink";
119
- import { useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
116
+ import { Box as Box6, render, Text as Text6, useBoxMetrics, useInput as useInput3, useWindowSize } from "ink";
117
+ import { memo, useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
120
118
  import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
121
119
 
122
120
  // src/tui/session/session-runner.ts
123
121
  import * as acp from "@agentclientprotocol/sdk";
124
- import {
125
- NovaAIError
126
- } from "@datalabrotterdam/nova-sdk";
127
- import { createNovaClient } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
128
122
  import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
129
123
  import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
130
124
  import { replayHistory } from "@datalabrotterdam/nova-ai-agent/core/history.js";
@@ -133,15 +127,6 @@ import {
133
127
  savePermissionMode
134
128
  } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
135
129
  import { loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
136
- import { discoverSkills } from "@datalabrotterdam/nova-ai-agent/core/skills.js";
137
- import {
138
- resolveModelContextWindow
139
- } from "@datalabrotterdam/nova-ai-agent/core/context-compaction.js";
140
- import {
141
- chatModels,
142
- resolveDefaultModel,
143
- resolveModelSupportsImageInput
144
- } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
145
130
 
146
131
  // src/tui/files/file-mentions.ts
147
132
  import { statSync } from "fs";
@@ -286,15 +271,6 @@ var SessionRunner = class {
286
271
  constructor(store, credentials, cwd) {
287
272
  this.store = store;
288
273
  this.credentials = credentials;
289
- this.loadMcpConfiguration(cwd);
290
- this.skills = discoverSkills(cwd).map(
291
- ({ name, description, source, path }) => ({
292
- name,
293
- description,
294
- source,
295
- path
296
- })
297
- );
298
274
  this.cwd = cwd;
299
275
  this.acpClient = new TuiAcpClient(
300
276
  store,
@@ -309,13 +285,7 @@ var SessionRunner = class {
309
285
  clientCapabilities: this.acpClient.capabilities,
310
286
  clientInfo: { name: "nova-ai-cli-tui", version: "1" }
311
287
  });
312
- this.novaClient = createNovaClient(credentials.apiKey);
313
- const model = credentials.defaultModel ?? process.env.NOVA_MODEL;
314
- if (!model)
315
- throw new Error(
316
- "No Nova model configured. Re-run authentication or set NOVA_MODEL."
317
- );
318
- this.model = model;
288
+ this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
319
289
  this.sessionId = crypto.randomUUID();
320
290
  this.sessionReady = Promise.resolve();
321
291
  }
@@ -332,15 +302,12 @@ var SessionRunner = class {
332
302
  connection;
333
303
  nova;
334
304
  initialized;
335
- novaClient;
336
- mcpServers = [];
337
- mcpConfigurationFailures = [];
338
305
  mcpStatus = {
339
306
  configured: [],
340
307
  connected: [],
341
308
  failures: []
342
309
  };
343
- skills;
310
+ skills = [];
344
311
  sessionReady;
345
312
  agentSessionLoaded = false;
346
313
  promptActive = false;
@@ -349,9 +316,6 @@ var SessionRunner = class {
349
316
  queuedAttachments = /* @__PURE__ */ new Map();
350
317
  editingQueuedMessageId = null;
351
318
  imageSupportByModel = /* @__PURE__ */ new Map();
352
- contextWindow = null;
353
- contextWindowModel = null;
354
- contextWindowRequest = 0;
355
319
  contextUsageRequest = 0;
356
320
  modelCheck = null;
357
321
  resumeFrom(storedSessionId) {
@@ -364,15 +328,7 @@ var SessionRunner = class {
364
328
  this.sessionId = stored.sessionId;
365
329
  this.cwd = stored.cwd;
366
330
  this.title = stored.title;
367
- this.loadMcpConfiguration(stored.cwd);
368
- this.skills = discoverSkills(stored.cwd).map(
369
- ({ name, description, source, path }) => ({
370
- name,
371
- description,
372
- source,
373
- path
374
- })
375
- );
331
+ this.skills = [];
376
332
  const messages = restoreSessionMessages(stored.messages);
377
333
  this.store.setState({
378
334
  messages,
@@ -391,7 +347,7 @@ var SessionRunner = class {
391
347
  return await this.nova.loadSession({
392
348
  sessionId: stored.sessionId,
393
349
  cwd: stored.cwd,
394
- mcpServers: this.mcpServers
350
+ mcpServers: []
395
351
  });
396
352
  } finally {
397
353
  this.acpClient.muteSessionUpdates = false;
@@ -516,9 +472,6 @@ var SessionRunner = class {
516
472
  void this.enqueueQueuedPrompt(value, [], [], "steer", true);
517
473
  return true;
518
474
  }
519
- configuredMcpServers() {
520
- return [...this.mcpServers];
521
- }
522
475
  mcpSessionStatus() {
523
476
  return {
524
477
  configured: this.mcpStatus.configured.map((server) => ({ ...server })),
@@ -529,27 +482,29 @@ var SessionRunner = class {
529
482
  skillSessionStatus() {
530
483
  return this.skills.map((skill) => ({ ...skill }));
531
484
  }
485
+ async trustWorkspace() {
486
+ await this.initialized;
487
+ await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
488
+ }
489
+ /** Every skill of the workspace with its on/off state, from the agent. */
490
+ async listSkills() {
491
+ await this.initialized;
492
+ return (await this.nova.skillsList({ cwd: this.cwd })).skills;
493
+ }
494
+ async setSkillEnabled(name, enabled, global) {
495
+ await this.initialized;
496
+ return (await this.nova.skillsSetEnabled({
497
+ cwd: this.cwd,
498
+ name,
499
+ enabled,
500
+ scope: global ? "global" : "project"
501
+ })).skills;
502
+ }
532
503
  contextUsageSnapshot() {
533
504
  return this.store.getState().contextUsage;
534
505
  }
535
506
  async refreshContextUsage() {
536
507
  await this.ensureSession();
537
- const sessionId = this.sessionId;
538
- const model = this.model;
539
- if (this.contextWindowModel !== model) {
540
- const request = ++this.contextWindowRequest;
541
- const contextWindow = await resolveModelContextWindow(
542
- this.novaClient,
543
- model
544
- );
545
- if (request === this.contextWindowRequest && model === this.model) {
546
- this.contextWindow = contextWindow;
547
- this.contextWindowModel = model;
548
- }
549
- }
550
- if (sessionId !== this.sessionId || !this.agentSessionLoaded) {
551
- return this.refreshContextUsage();
552
- }
553
508
  const usage = await this.updateContextUsage();
554
509
  if (!usage)
555
510
  throw new Error(
@@ -565,13 +520,8 @@ var SessionRunner = class {
565
520
  }
566
521
  setModel(model) {
567
522
  this.model = model;
568
- this.contextWindow = null;
569
- this.contextWindowModel = null;
570
- this.contextWindowRequest++;
571
523
  saveDefaultModel(model);
572
524
  if (this.agentSessionLoaded) void this.updateContextUsage();
573
- void this.refreshContextUsage().catch(() => {
574
- });
575
525
  }
576
526
  /**
577
527
  * Hands the mode to the agent's permission policy (which decides) and
@@ -629,27 +579,30 @@ var SessionRunner = class {
629
579
  }
630
580
  /** Chat models only: embedding, speech and transcription models can't run a session. */
631
581
  async listModels() {
632
- const { data } = await this.novaClient.models.list({ limit: 100 });
633
- return chatModels(data).map((model) => ({
634
- id: model.id,
635
- name: model.name ?? null
636
- }));
582
+ await this.initialized;
583
+ const { models } = await this.nova.modelsList();
584
+ for (const model of models) this.imageSupportByModel.set(model.id, model.imageInput);
585
+ return models.map((model) => ({ id: model.id, name: model.name }));
637
586
  }
638
587
  /**
639
- * Replaces a saved model that cannot chat (older logins saved the first
640
- * model of the list, e.g. an embedding model) and saves the replacement.
588
+ * Takes the account's default model when none is set, and replaces a saved
589
+ * one that cannot chat (older logins saved the first model of the list,
590
+ * e.g. an embedding model); the agent decides, the choice is saved.
641
591
  * Runs once; a failed lookup keeps the model and the prompt reports errors.
642
592
  */
643
593
  ensureUsableModel() {
644
594
  this.modelCheck ??= (async () => {
645
595
  try {
646
- const { data } = await this.novaClient.models.list({ limit: 100 });
647
- const model = resolveDefaultModel(data, this.model);
648
- if (!model || model === this.model) return;
596
+ await this.initialized;
597
+ const { models, defaultModel } = await this.nova.modelsList(
598
+ this.model ? { preferred: this.model } : {}
599
+ );
600
+ for (const model of models) this.imageSupportByModel.set(model.id, model.imageInput);
601
+ if (!defaultModel || defaultModel === this.model) return;
649
602
  const previous = this.model;
650
- this.setModel(model);
603
+ this.setModel(defaultModel);
651
604
  this.store.setState({
652
- statusLine: `${previous} is not a chat model; switched to ${model} (Ctrl+P to pick another).`
605
+ statusLine: previous ? `${previous} is not a chat model; switched to ${defaultModel} (Ctrl+P to pick another).` : `Using model ${defaultModel} (Ctrl+P switches).`
653
606
  });
654
607
  } catch {
655
608
  }
@@ -657,14 +610,8 @@ var SessionRunner = class {
657
610
  return this.modelCheck;
658
611
  }
659
612
  async supportsImageInput(model = this.model) {
660
- const cached = this.imageSupportByModel.get(model);
661
- if (cached !== void 0) return cached;
662
- const supported = await resolveModelSupportsImageInput(
663
- this.novaClient,
664
- model
665
- );
666
- this.imageSupportByModel.set(model, supported);
667
- return supported;
613
+ if (!this.imageSupportByModel.has(model)) await this.listModels();
614
+ return this.imageSupportByModel.get(model) ?? false;
668
615
  }
669
616
  async compactContext() {
670
617
  if (this.store.getState().busy || this.promptActive) {
@@ -788,7 +735,7 @@ var SessionRunner = class {
788
735
  });
789
736
  }
790
737
  } catch (err) {
791
- const message = err instanceof NovaAIError ? `Nova AI request failed (status ${err.status}, requestId ${err.requestId}): ${err.message}` : err instanceof Error ? err.message : "Unknown error.";
738
+ const message = err instanceof Error ? err.message : "Unknown error.";
792
739
  pendingToolFailure = `Request failed: ${message}`;
793
740
  this.acpClient.appendError(message);
794
741
  this.store.setState({ statusLine: "Request failed." });
@@ -942,11 +889,10 @@ var SessionRunner = class {
942
889
  ];
943
890
  }
944
891
  async createSession() {
945
- this.loadMcpConfiguration(this.cwd);
946
892
  await this.initialized;
947
893
  const { sessionId, _meta } = await this.nova.newSession({
948
894
  cwd: this.cwd,
949
- mcpServers: this.mcpServers
895
+ mcpServers: []
950
896
  });
951
897
  this.sessionId = sessionId;
952
898
  this.applyMcpStatus(_meta);
@@ -970,10 +916,20 @@ var SessionRunner = class {
970
916
  const value = meta?.["nova-ai-cli/mcp"];
971
917
  if (!value || typeof value !== "object") return;
972
918
  const record = value;
919
+ const strings = (value2) => Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : void 0;
973
920
  const configured = Array.isArray(record.configured) ? record.configured.flatMap((entry) => {
974
921
  if (!entry || typeof entry !== "object") return [];
975
922
  const item = entry;
976
- return typeof item.name === "string" && typeof item.transport === "string" ? [{ name: item.name, transport: item.transport }] : [];
923
+ return typeof item.name === "string" && typeof item.transport === "string" ? [
924
+ {
925
+ name: item.name,
926
+ transport: item.transport,
927
+ ...typeof item.url === "string" ? { url: item.url } : {},
928
+ ...typeof item.command === "string" ? { command: item.command } : {},
929
+ ...strings(item.args) ? { args: strings(item.args) } : {},
930
+ ...strings(item.env) ? { env: strings(item.env) } : {}
931
+ }
932
+ ] : [];
977
933
  }) : [];
978
934
  const connected = Array.isArray(record.connected) ? record.connected.filter(
979
935
  (entry) => typeof entry === "string"
@@ -983,24 +939,7 @@ var SessionRunner = class {
983
939
  const item = entry;
984
940
  return typeof item.serverName === "string" && typeof item.message === "string" ? [{ serverName: item.serverName, message: item.message }] : [];
985
941
  }) : [];
986
- this.mcpStatus = {
987
- configured,
988
- connected,
989
- failures: [...this.mcpConfigurationFailures, ...failures]
990
- };
991
- }
992
- loadMcpConfiguration(cwd) {
993
- const configuration = readWorkspaceMcpConfiguration(cwd);
994
- this.mcpServers = configuration.servers;
995
- this.mcpConfigurationFailures = configuration.failures;
996
- this.mcpStatus = {
997
- configured: this.mcpServers.map((server) => ({
998
- name: server.name,
999
- transport: "type" in server ? server.type : "stdio"
1000
- })),
1001
- connected: [],
1002
- failures: [...this.mcpConfigurationFailures]
1003
- };
942
+ this.mcpStatus = { configured, connected, failures };
1004
943
  }
1005
944
  applySkillStatus(meta) {
1006
945
  const value = meta?.["nova-ai-cli/skills"];
@@ -1018,12 +957,28 @@ var SessionRunner = class {
1018
957
  ] : [];
1019
958
  });
1020
959
  }
960
+ /**
961
+ * Starts the session ahead of the first prompt, so the agent's MCP and
962
+ * skill status is there for /mcp and the status line. A failure is left to
963
+ * the first prompt, which tries again and reports it.
964
+ */
965
+ async prepareSession() {
966
+ try {
967
+ await this.ensureSession();
968
+ } catch {
969
+ this.sessionReady = Promise.resolve();
970
+ }
971
+ }
972
+ get sessionLoaded() {
973
+ return this.agentSessionLoaded;
974
+ }
1021
975
  async ensureSession() {
1022
976
  await this.ensureUsableModel();
1023
- await this.sessionReady;
1024
- if (!this.agentSessionLoaded) {
1025
- this.sessionReady = this.createSession();
1026
- await this.sessionReady;
977
+ for (; ; ) {
978
+ const ready = this.sessionReady;
979
+ await ready;
980
+ if (this.agentSessionLoaded) return;
981
+ if (this.sessionReady === ready) this.sessionReady = this.createSession();
1027
982
  }
1028
983
  }
1029
984
  applyInteractionMode(mode, statusLine) {
@@ -1039,7 +994,7 @@ var SessionRunner = class {
1039
994
  try {
1040
995
  const usage = await this.nova.sessionContextUsage({
1041
996
  sessionId,
1042
- contextWindow: this.contextWindow,
997
+ ...this.model ? { model: this.model } : {},
1043
998
  mode: this.interactionMode
1044
999
  });
1045
1000
  if (request === this.contextUsageRequest && sessionId === this.sessionId) {
@@ -1153,6 +1108,17 @@ function kittyKeyboardOverride(env = process.env) {
1153
1108
  function isLateTerminalReply(input) {
1154
1109
  return /^\u001b?\[\?[\d;]*[uc]$/.test(input);
1155
1110
  }
1111
+ var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1006h";
1112
+ var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1006l";
1113
+ var MOUSE_INPUT = /^\u001b?\[<(\d+);\d+;\d+[Mm]$/;
1114
+ function isMouseInput(input) {
1115
+ return MOUSE_INPUT.test(input);
1116
+ }
1117
+ function wheelRows(input) {
1118
+ const button = Number(MOUSE_INPUT.exec(input)?.[1] ?? -1);
1119
+ if (button < 0 || (button & 64) === 0) return 0;
1120
+ return button & 1 ? -3 : 3;
1121
+ }
1156
1122
 
1157
1123
  // src/tui/ink/components/prompt-input.tsx
1158
1124
  import { Box, Text, useInput, usePaste } from "ink";
@@ -1170,7 +1136,8 @@ var palette = {
1170
1136
  faint: "#64748b",
1171
1137
  code: "#fcd34d",
1172
1138
  diffAddBg: "#1b3a24",
1173
- diffRemoveBg: "#3d1f1f"
1139
+ diffRemoveBg: "#3d1f1f",
1140
+ stickyBackground: "#1e293b"
1174
1141
  };
1175
1142
  var colors = {
1176
1143
  primary: chalk.hex(palette.primary),
@@ -1197,7 +1164,7 @@ function PromptInput({
1197
1164
  }) {
1198
1165
  useSyncExternalStore(editor.subscribe, editor.getVersion);
1199
1166
  useInput((input, key) => {
1200
- if (onKey(input, key)) return;
1167
+ if (isMouseInput(input) || onKey(input, key)) return;
1201
1168
  editor.handleKey(input, key);
1202
1169
  });
1203
1170
  usePaste((text) => editor.paste(text));
@@ -1483,7 +1450,8 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
1483
1450
  function StatusLine({
1484
1451
  ui,
1485
1452
  model,
1486
- mcp
1453
+ mcp,
1454
+ scroll = 0
1487
1455
  }) {
1488
1456
  const separator = /* @__PURE__ */ jsx2(Text2, { color: palette.faint, children: " | " });
1489
1457
  const usage = ui.contextUsage ? `ctx:${formatTokenCount(ui.contextUsage.totalTokens)}${ui.contextUsage.contextWindow ? `/${formatTokenCount(ui.contextUsage.contextWindow)}` : ""}` : null;
@@ -1511,6 +1479,10 @@ function StatusLine({
1511
1479
  `mcp:${mcp.connected}/${mcp.configured}`,
1512
1480
  mcp.failed ? /* @__PURE__ */ jsx2(Text2, { color: palette.danger, children: ` !${mcp.failed}` }) : null
1513
1481
  ] }) : null,
1482
+ scroll ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1483
+ separator,
1484
+ /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `\u2191${scroll} lines \xB7 PgDn/Esc` })
1485
+ ] }) : null,
1514
1486
  mcp.skills ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1515
1487
  separator,
1516
1488
  `skills:${mcp.skills}`
@@ -1831,12 +1803,6 @@ function PlanView({
1831
1803
  }
1832
1804
 
1833
1805
  // src/tui/ink/controller.ts
1834
- import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
1835
- import {
1836
- disabledSkillNames,
1837
- discoverSkills as discoverSkills2,
1838
- setSkillEnabled
1839
- } from "@datalabrotterdam/nova-ai-agent/core/skills.js";
1840
1806
  import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1841
1807
 
1842
1808
  // src/tui/commands/builtins.ts
@@ -1961,8 +1927,8 @@ var builtinCommands = [
1961
1927
  {
1962
1928
  name: "trust",
1963
1929
  description: "Trust this workspace: start the MCP servers it declares and apply its .nova-ai allow rules",
1964
- run(ctx) {
1965
- ctx.print(ctx.trustWorkspace());
1930
+ async run(ctx) {
1931
+ ctx.print(await ctx.trustWorkspace());
1966
1932
  }
1967
1933
  },
1968
1934
  {
@@ -1989,16 +1955,16 @@ var builtinCommands = [
1989
1955
  name: "mcp",
1990
1956
  description: "Inspect configured MCP servers",
1991
1957
  run(ctx) {
1992
- ctx.openMcpInspector();
1958
+ return ctx.openMcpInspector();
1993
1959
  }
1994
1960
  },
1995
1961
  {
1996
1962
  name: "skills",
1997
1963
  description: "List skills, or switch one: /skills on|off <name> [--global]",
1998
- run(ctx, args) {
1964
+ async run(ctx, args) {
1999
1965
  const words = args.trim().split(/\s+/).filter(Boolean);
2000
1966
  if (!words.length) {
2001
- ctx.openSkillInspector();
1967
+ await ctx.openSkillInspector();
2002
1968
  return;
2003
1969
  }
2004
1970
  const global = words.includes("--global");
@@ -2007,7 +1973,7 @@ var builtinCommands = [
2007
1973
  ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
2008
1974
  return;
2009
1975
  }
2010
- ctx.print(ctx.switchSkill(name, action === "on", global));
1976
+ ctx.print(await ctx.switchSkill(name, action === "on", global));
2011
1977
  }
2012
1978
  },
2013
1979
  {
@@ -3601,17 +3567,11 @@ var TranscriptCommitter = class {
3601
3567
  planSignature = null;
3602
3568
  snapshot = null;
3603
3569
  lastState = null;
3604
- redrawRequested = false;
3605
- /** The next sync starts a new epoch, e.g. after the terminal width changed. */
3606
- requestRedraw() {
3607
- this.redrawRequested = true;
3608
- this.lastState = null;
3609
- }
3610
3570
  sync(state, expanded) {
3611
3571
  if (this.snapshot && state === this.lastState && expanded === this.expanded)
3612
3572
  return this.snapshot;
3613
3573
  this.lastState = state;
3614
- if (this.redrawRequested || state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
3574
+ if (state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
3615
3575
  this.startEpoch(state, expanded);
3616
3576
  }
3617
3577
  const messages = state.messages;
@@ -3764,7 +3724,6 @@ var TranscriptCommitter = class {
3764
3724
  return true;
3765
3725
  }
3766
3726
  startEpoch(state, expanded) {
3767
- this.redrawRequested = false;
3768
3727
  this.epoch++;
3769
3728
  this.sessionId = state.sessionId;
3770
3729
  this.expanded = expanded;
@@ -3810,7 +3769,6 @@ function safeSplitOffset(text, from) {
3810
3769
  }
3811
3770
 
3812
3771
  // src/tui/ink/controller.ts
3813
- var CLEAR_SCREEN_AND_SCROLLBACK = "\x1B[2J\x1B[3J\x1B[H";
3814
3772
  var EXIT_CONFIRM_MS = 2e3;
3815
3773
  var TuiController = class {
3816
3774
  constructor(store, runner, io) {
@@ -3835,6 +3793,8 @@ var TuiController = class {
3835
3793
  exiting = false;
3836
3794
  view;
3837
3795
  lastEpoch = 0;
3796
+ scroll = 0;
3797
+ viewport = { rows: 10, contentRows: 0 };
3838
3798
  exitArmed = false;
3839
3799
  exitTimer;
3840
3800
  animationTimer;
@@ -3858,7 +3818,7 @@ var TuiController = class {
3858
3818
  };
3859
3819
  getView = () => this.view;
3860
3820
  start() {
3861
- void this.runner.ensureUsableModel().then(() => this.refresh());
3821
+ void this.runner.prepareSession().then(() => this.refresh());
3862
3822
  this.syncWindowTitle();
3863
3823
  this.animationTimer = setInterval(() => this.syncWindowTitle(true), 120);
3864
3824
  this.animationTimer.unref?.();
@@ -3893,8 +3853,17 @@ var TuiController = class {
3893
3853
  this.runner.cancel();
3894
3854
  return true;
3895
3855
  }
3856
+ if (this.scroll > 0) {
3857
+ this.scrollTo(0);
3858
+ return true;
3859
+ }
3896
3860
  return false;
3897
3861
  }
3862
+ if (key.pageUp || key.pageDown) {
3863
+ const page = Math.max(1, this.viewport.rows - 2);
3864
+ this.scrollBy(key.pageUp ? page : -page);
3865
+ return true;
3866
+ }
3898
3867
  if (key.tab && key.shift) {
3899
3868
  this.cycleInteractionMode();
3900
3869
  return true;
@@ -4059,9 +4028,13 @@ ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
4059
4028
  state: new ToolInspectorState(() => this.toolCalls(), this.closeOverlay)
4060
4029
  });
4061
4030
  };
4062
- openMcpInspector = () => {
4063
- const servers = this.runner.configuredMcpServers();
4031
+ openMcpInspector = async () => {
4032
+ if (!this.runner.sessionLoaded) {
4033
+ this.showText("MCP servers", "Connecting MCP servers...");
4034
+ await this.runner.prepareSession();
4035
+ }
4064
4036
  const connection = this.runner.mcpSessionStatus();
4037
+ const servers = connection.configured;
4065
4038
  const configuredNames = new Set(servers.map((server) => server.name));
4066
4039
  const configurationFailures = connection.failures.filter((failure) => !configuredNames.has(failure.serverName)).map((failure) => `${failure.serverName}
4067
4040
  status: failed - ${failure.message}`);
@@ -4075,16 +4048,20 @@ status: failed - ${failure.message}`);
4075
4048
  );
4076
4049
  };
4077
4050
  /** Every discovered skill with its state; switched-off skills stay listed. */
4078
- openSkillInspector = () => {
4079
- const cwd = this.runner.cwd;
4080
- const skills = discoverSkills2(cwd);
4081
- const disabled = disabledSkillNames(cwd);
4082
- const on = skills.filter((skill) => !disabled.has(skill.name)).length;
4051
+ openSkillInspector = async () => {
4052
+ let skills;
4053
+ try {
4054
+ skills = await this.runner.listSkills();
4055
+ } catch (error) {
4056
+ this.showText("Skills", errorMessage(error, "Could not list the skills."));
4057
+ return;
4058
+ }
4059
+ const on = skills.filter((skill) => skill.enabled).length;
4083
4060
  this.showText(
4084
4061
  `Skills (${on} on, ${skills.length - on} off)`,
4085
4062
  skills.length ? [
4086
4063
  ...skills.map(
4087
- (skill) => `${skill.name} [${skill.source}]${disabled.has(skill.name) ? " (off)" : ""}
4064
+ (skill) => `${skill.name} [${skill.source}]${skill.enabled ? "" : " (off)"}
4088
4065
  ${skill.description}
4089
4066
  ${skill.path}`
4090
4067
  ),
@@ -4093,17 +4070,15 @@ ${skill.path}`
4093
4070
  );
4094
4071
  };
4095
4072
  /** /skills on|off <name> [--global]: applies from the next message. */
4096
- switchSkill = (name, enabled, global) => {
4097
- const cwd = this.runner.cwd;
4098
- if (!discoverSkills2(cwd).some((skill) => skill.name === name)) {
4099
- return `No skill named "${name}". /skills lists them.`;
4100
- }
4073
+ switchSkill = async (name, enabled, global) => {
4074
+ let skills;
4101
4075
  try {
4102
- setSkillEnabled(cwd, global ? "global" : "project", name, enabled);
4076
+ skills = await this.runner.setSkillEnabled(name, enabled, global);
4103
4077
  } catch (error) {
4104
- return errorMessage(error, "Could not change the skill.");
4078
+ const message = errorMessage(error, "Could not change the skill.");
4079
+ return message.startsWith("No skill named") ? `${message} /skills lists them.` : message;
4105
4080
  }
4106
- const stillOff = enabled && disabledSkillNames(cwd).has(name);
4081
+ const stillOff = enabled && skills.some((skill) => skill.name === name && !skill.enabled);
4107
4082
  return stillOff ? `Switched "${name}" on ${global ? "globally" : "in this project"}, but it is still off ${global ? "in this project" : "globally"} (use ${global ? "/skills on " + name : "/skills on " + name + " --global"}).` : `Skill "${name}" is ${enabled ? "on" : "off"} ${global ? "everywhere" : "in this project"} from the next message.`;
4108
4083
  };
4109
4084
  openUsageInspector = async () => {
@@ -4116,14 +4091,23 @@ ${skill.path}`
4116
4091
  }
4117
4092
  };
4118
4093
  /**
4119
- * Prints the whole transcript again. After the width changed, the terminal
4120
- * re-wrapped what is on screen on its own and the live area can't be
4121
- * erased exactly (Ink leaves stale prompt borders), and old scrollback keeps
4122
- * the old wrapping.
4094
+ * The view reports its transcript size after each layout. While scrolled
4095
+ * up, new output below grows the offset so the rows being read stay put.
4123
4096
  */
4124
- redrawAll() {
4125
- if (this.stopped || this.exiting) return;
4126
- this.committer.requestRedraw();
4097
+ setViewport(rows, contentRows) {
4098
+ const grown = contentRows - this.viewport.contentRows;
4099
+ this.viewport = { rows, contentRows };
4100
+ this.scrollTo(this.scroll > 0 && grown > 0 ? this.scroll + grown : this.scroll);
4101
+ }
4102
+ /** Positive scrolls toward older output. */
4103
+ scrollBy(rows) {
4104
+ this.scrollTo(this.scroll + rows);
4105
+ }
4106
+ scrollTo(offset) {
4107
+ const max = Math.max(0, this.viewport.contentRows - this.viewport.rows);
4108
+ const next = Math.max(0, Math.min(offset, max));
4109
+ if (next === this.scroll) return;
4110
+ this.scroll = next;
4127
4111
  this.refresh();
4128
4112
  }
4129
4113
  toggleToolDetails() {
@@ -4198,8 +4182,12 @@ ${skill.path}`
4198
4182
  getPermissionMode: () => this.store.getState().permissionMode,
4199
4183
  setPermissionMode: (mode) => this.setPermissionMode(mode),
4200
4184
  openPermissionPicker: this.openPermissionModes,
4201
- trustWorkspace: () => {
4202
- setWorkspaceTrusted(this.runner.cwd, true);
4185
+ trustWorkspace: async () => {
4186
+ try {
4187
+ await this.runner.trustWorkspace();
4188
+ } catch (error) {
4189
+ return errorMessage(error, "Could not trust the workspace.");
4190
+ }
4203
4191
  return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
4204
4192
  },
4205
4193
  openToolInspector: this.openToolInspector,
@@ -4224,6 +4212,7 @@ ${skill.path}`
4224
4212
  submit = async (text, images = [], pastes = []) => {
4225
4213
  const value = text.trim();
4226
4214
  if (!value) return;
4215
+ this.scroll = 0;
4227
4216
  this.editor.addToHistory(value);
4228
4217
  this.store.setState((state) => ({
4229
4218
  inputHistory: [...state.inputHistory, value],
@@ -4325,7 +4314,10 @@ ${skill.path}`
4325
4314
  this.view = this.buildView();
4326
4315
  if (this.view.transcript.epoch !== this.lastEpoch) {
4327
4316
  this.lastEpoch = this.view.transcript.epoch;
4328
- this.io.write(CLEAR_SCREEN_AND_SCROLLBACK);
4317
+ if (this.scroll) {
4318
+ this.scroll = 0;
4319
+ this.view = this.buildView();
4320
+ }
4329
4321
  }
4330
4322
  for (const listener of this.listeners) listener();
4331
4323
  }
@@ -4339,7 +4331,8 @@ ${skill.path}`
4339
4331
  model: this.runner.model,
4340
4332
  mcp: mcpCounts(this.runner),
4341
4333
  expandedTools: this.expandedTools,
4342
- exiting: this.exiting
4334
+ exiting: this.exiting,
4335
+ scroll: this.scroll
4343
4336
  };
4344
4337
  }
4345
4338
  questionState(pending) {
@@ -4371,14 +4364,12 @@ ${skill.path}`
4371
4364
  }
4372
4365
  };
4373
4366
  function formatMcpServer(server, status) {
4374
- const type = "type" in server ? server.type : "stdio";
4375
- const lines = [`${server.name} [${type}]`];
4376
- if ("url" in server) lines.push(`url: ${server.url}`);
4377
- if ("command" in server) {
4367
+ const lines = [`${server.name} [${server.transport}]`];
4368
+ if (server.url) lines.push(`url: ${server.url}`);
4369
+ if (server.command) {
4378
4370
  lines.push(`command: ${server.command}`);
4379
- if (server.args.length) lines.push(`args: ${server.args.join(" ")}`);
4380
- if (server.env.length)
4381
- lines.push(`env: ${server.env.map((entry) => entry.name).join(", ")} (values hidden)`);
4371
+ if (server.args?.length) lines.push(`args: ${server.args.join(" ")}`);
4372
+ if (server.env?.length) lines.push(`env: ${server.env.join(", ")} (values hidden)`);
4382
4373
  }
4383
4374
  const failure = status.failures.find((item) => item.serverName === server.name);
4384
4375
  lines.push(
@@ -4409,7 +4400,7 @@ function useDialogInput(handle, isActive) {
4409
4400
  const [, setTick] = useState(0);
4410
4401
  useInput2(
4411
4402
  (input, key) => {
4412
- if (handle(input, key) === "changed") setTick((tick) => tick + 1);
4403
+ if (!isMouseInput(input) && handle(input, key) === "changed") setTick((tick) => tick + 1);
4413
4404
  },
4414
4405
  { isActive }
4415
4406
  );
@@ -4551,7 +4542,7 @@ function QuestionDialog({
4551
4542
  const [, setTick] = useState(0);
4552
4543
  useInput2(
4553
4544
  (input, key) => {
4554
- if (state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
4545
+ if (!isMouseInput(input) && state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
4555
4546
  },
4556
4547
  { isActive }
4557
4548
  );
@@ -4633,7 +4624,7 @@ function PermissionDialog({
4633
4624
  }) {
4634
4625
  useInput2(
4635
4626
  (input, key) => {
4636
- if (permissionKey(request, input, key) === "closed") onClose();
4627
+ if (!isMouseInput(input) && permissionKey(request, input, key) === "closed") onClose();
4637
4628
  },
4638
4629
  { isActive }
4639
4630
  );
@@ -4684,9 +4675,8 @@ function wrapPlain(text, width) {
4684
4675
  }
4685
4676
 
4686
4677
  // src/tui/ink/app.tsx
4687
- import { Fragment as Fragment4, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4678
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
4688
4679
  var FRAME_MS = 120;
4689
- var RESIZE_SETTLE_MS = 100;
4690
4680
  async function runInkTui(credentials, cwd, args, options = {}) {
4691
4681
  const store = createStore({
4692
4682
  messages: [],
@@ -4728,22 +4718,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
4728
4718
  // Ink otherwise turns non-interactive whenever CI is set in the
4729
4719
  // environment; the TUI only runs on a terminal anyway.
4730
4720
  interactive: Boolean(stdout.isTTY),
4721
+ // Full screen: the transcript scrolls inside the app (PgUp/PgDn) and the
4722
+ // terminal gets its own screen back on exit.
4723
+ alternateScreen: Boolean(stdout.isTTY),
4731
4724
  kittyKeyboard: { mode: kittyKeyboard ? "enabled" : "disabled" }
4732
4725
  });
4733
4726
  controller.start();
4727
+ if (stdout.isTTY) stdout.write(MOUSE_REPORTING_ON);
4734
4728
  const onSignal = () => controller.requestExit();
4735
4729
  process.once("SIGTERM", onSignal);
4736
- let printedColumns = stdout.columns;
4737
- let resizeTimer;
4738
- const onResize = () => {
4739
- if (resizeTimer) clearTimeout(resizeTimer);
4740
- resizeTimer = setTimeout(() => {
4741
- if (stdout.columns === printedColumns) return;
4742
- printedColumns = stdout.columns;
4743
- controller.redrawAll();
4744
- }, RESIZE_SETTLE_MS);
4745
- };
4746
- stdout.on("resize", onResize);
4747
4730
  void (options.checkForUpdate ?? checkForUpdate)().then((updateAvailable) => {
4748
4731
  if (updateAvailable) store.setState({ updateAvailable });
4749
4732
  });
@@ -4753,8 +4736,7 @@ async function runInkTui(credentials, cwd, args, options = {}) {
4753
4736
  await controller.stop();
4754
4737
  } finally {
4755
4738
  process.off("SIGTERM", onSignal);
4756
- stdout.off("resize", onResize);
4757
- if (resizeTimer) clearTimeout(resizeTimer);
4739
+ if (stdout.isTTY) stdout.write(MOUSE_REPORTING_OFF);
4758
4740
  instance.unmount();
4759
4741
  await instance.waitUntilExit().catch(() => {
4760
4742
  });
@@ -4768,31 +4750,85 @@ function App({ controller }) {
4768
4750
  const { rows, columns } = useWindowSize();
4769
4751
  const bottomRef = useRef(null);
4770
4752
  const bottom = useBoxMetrics(bottomRef);
4753
+ const contentRef = useRef(null);
4754
+ const content = useBoxMetrics(contentRef);
4771
4755
  const working = useWorking(view);
4756
+ const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
4757
+ useInput3((input) => {
4758
+ const rows2 = wheelRows(input);
4759
+ if (rows2) controller.scrollBy(rows2);
4760
+ });
4761
+ const height = Math.max(4, rows - 1);
4762
+ const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
4763
+ useEffect(() => {
4764
+ if (content.hasMeasured) controller.setViewport(viewportRows, content.height);
4765
+ }, [controller, viewportRows, content.hasMeasured, content.height]);
4766
+ if (view.exiting) return null;
4772
4767
  const { transcript } = view;
4773
- const scrollback = (
4774
- // <Static> is laid out as an absolute box; without a width its text
4775
- // wraps against its content and runs past the terminal edge.
4776
- /* @__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)
4777
- );
4778
- if (view.exiting) return scrollback;
4779
- const liveRows = Math.max(3, rows - (bottom.hasMeasured ? bottom.height : 6) - 2);
4780
- const dialogRows = Math.max(6, rows - 4);
4781
- return /* @__PURE__ */ jsxs6(Fragment4, { children: [
4782
- scrollback,
4783
- /* @__PURE__ */ jsx6(Box6, { flexDirection: "column", maxHeight: liveRows, overflowY: "hidden", justifyContent: "flex-end", children: /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", flexShrink: 0, children: [
4784
- transcript.live.map((block) => /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key)),
4785
- working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
4786
- /* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
4787
- /* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
4788
- ] }) }) : null
4789
- ] }) }),
4790
- /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", marginTop: 1, children: [
4791
- /* @__PURE__ */ jsx6(Surface, { controller, view, rows: dialogRows, columns }),
4792
- /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp })
4768
+ const blocks = [...transcript.committed, ...transcript.live];
4769
+ const liveKeys = new Set(transcript.live.map((block) => block.key));
4770
+ const onUserTop = (key, top) => setUserTops((tops) => tops.get(key) === top ? tops : new Map(tops).set(key, top));
4771
+ const firstVisibleRow = content.height - viewportRows - view.scroll;
4772
+ let sticky = null;
4773
+ for (const block of blocks) {
4774
+ if (block.kind !== "user") continue;
4775
+ const top = userTops.get(block.key);
4776
+ if (top !== void 0 && top < firstVisibleRow) sticky = block.text;
4777
+ }
4778
+ return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height, children: [
4779
+ /* @__PURE__ */ jsxs6(
4780
+ Box6,
4781
+ {
4782
+ flexDirection: "column",
4783
+ height: viewportRows,
4784
+ flexShrink: 0,
4785
+ overflowY: "hidden",
4786
+ justifyContent: "flex-end",
4787
+ children: [
4788
+ /* @__PURE__ */ jsxs6(Box6, { ref: contentRef, flexDirection: "column", flexShrink: 0, marginBottom: -view.scroll, children: [
4789
+ blocks.map(
4790
+ (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)
4791
+ ),
4792
+ working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
4793
+ /* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
4794
+ /* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
4795
+ ] }) }) : null
4796
+ ] }),
4797
+ sticky !== null ? /* @__PURE__ */ jsx6(StickyPrompt, { text: sticky, columns }) : null
4798
+ ]
4799
+ }
4800
+ ),
4801
+ /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
4802
+ /* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
4803
+ /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp, scroll: view.scroll })
4793
4804
  ] })
4794
4805
  ] });
4795
4806
  }
4807
+ var CommittedBlock = memo(function CommittedBlock2({
4808
+ block,
4809
+ onUserTop
4810
+ }) {
4811
+ if (block.kind !== "user") {
4812
+ return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
4813
+ }
4814
+ return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
4815
+ }, (before, after) => before.block === after.block);
4816
+ function UserAnchor({
4817
+ block,
4818
+ onUserTop,
4819
+ children
4820
+ }) {
4821
+ const ref = useRef(null);
4822
+ const { top, hasMeasured } = useBoxMetrics(ref);
4823
+ useEffect(() => {
4824
+ if (hasMeasured) onUserTop(block.key, top);
4825
+ }, [block.key, top, hasMeasured]);
4826
+ return /* @__PURE__ */ jsx6(Box6, { ref, marginTop: 1, children });
4827
+ }
4828
+ function StickyPrompt({ text, columns }) {
4829
+ const line = `> ${text.replace(/\s+/g, " ").trim()}`;
4830
+ 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) }) });
4831
+ }
4796
4832
  function Surface({
4797
4833
  controller,
4798
4834
  view,
@@ -4853,13 +4889,15 @@ function useWorking(view) {
4853
4889
  }
4854
4890
 
4855
4891
  // src/tui/index.ts
4892
+ import { workspaceNeedsTrust } from "@datalabrotterdam/nova-ai-agent/core/workspace-mcp.js";
4856
4893
  async function runChat(options) {
4857
4894
  const stored = readCredentials();
4858
4895
  if (!stored) {
4859
4896
  console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
4860
4897
  return 1;
4861
4898
  }
4862
- const credentials = options.model ? { ...stored, defaultModel: options.model } : stored;
4899
+ const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL;
4900
+ const credentials = { ...stored, defaultModel: model };
4863
4901
  const updateCheck = checkForUpdate().then((update) => withInstallability(update));
4864
4902
  const updatedFrom = process.env[UPDATED_FROM_ENV];
4865
4903
  if (updatedFrom) {
@@ -4903,7 +4941,7 @@ ${cwd}`);
4903
4941
  console.log("Trusting it starts those servers and applies those rules. Only trust code you know.");
4904
4942
  const answer = (await prompt.question("Trust this workspace? [y/N] ")).trim().toLowerCase();
4905
4943
  if (answer === "y" || answer === "yes") {
4906
- setWorkspaceTrusted2(cwd, true);
4944
+ setWorkspaceTrusted(cwd, true);
4907
4945
  } else {
4908
4946
  console.log("Not trusted: its MCP servers stay off and its allow rules are ignored. Use /trust later to change this.");
4909
4947
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.4",
3
+ "version": "1.2.0-alpha.6",
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,8 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@agentclientprotocol/sdk": "^0.29.0",
42
- "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.2",
43
- "@datalabrotterdam/nova-sdk": "^1.3.2",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.3",
44
43
  "chalk": "^5.3.0",
45
44
  "cli-highlight": "^2.1.11",
46
45
  "diff": "^9.0.0",
@@ -75,5 +74,8 @@
75
74
  }
76
75
  ]
77
76
  ]
77
+ },
78
+ "devDependencies": {
79
+ "@datalabrotterdam/nova-sdk": "^1.3.2"
78
80
  }
79
81
  }