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

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,18 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.7](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.6...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.7) (2026-10-24)
2
+
3
+
4
+ ### Features
5
+
6
+ * **agent:** Claude Code compatible hooks ([227fe8b](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/227fe8b273ec2e211e9d57674e76af5c42a473bd))
7
+ * **agent:** install the shared Nova skills ([64139dd](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/64139dd612933343f04f165de1a9ab78832d6864))
8
+ * **agent:** mark and protect bundled skills ([6ef52eb](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/6ef52eb6d048f8a3b4e75686244aabcfd0f62d6e))
9
+ * **agent:** thinking effort for reasoning models ([8551fea](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/8551feacff3e1675f369030e20fb2b5071f0db7f))
10
+ * **agent:** web_search tool ([12985eb](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/12985eb9ca06d6c814d4e0162465c651889d9370))
11
+ * **cli:** /effort slider for thinking models ([8ca67f2](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/8ca67f2f5dcc8f6bff8c36409a2baba5d43c4a02))
12
+ * **cli:** remember the thinking level ([883196c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/883196c9fdb089f4f26ac82b557cc87dfc0b1c0b))
13
+ * OAuth sign-in for MCP servers ([ba57c79](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/ba57c79642f902ab2bc20f59e4627fb2b774bb4a))
14
+ * the user's agent files as session modes ([3386de8](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/3386de88d07b0351bd1e44960fcacfef26e3d45b))
15
+
1
16
  # [@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
17
 
3
18
 
package/README.md CHANGED
@@ -79,6 +79,8 @@ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
79
79
 
80
80
  - **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
81
81
  questions and builds a task list but changes nothing; `ask` only answers.
82
+ Your own agents (`~/.nova-ai/agents/<name>.agent.md`, the format the VS Code
83
+ extension uses) are extra modes after these; `/mode` lists them.
82
84
  - **Permissions**: before an edit or command you choose once, for this
83
85
  session, always (saved as an exact rule) or deny. Ctrl+K switches between
84
86
  asking, accepting edits, and bypass (never remembered).
@@ -95,12 +97,12 @@ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
95
97
  | Enter / Shift+Enter | Send / new line (Shift+Enter where the terminal supports the kitty keyboard protocol: VS Code, kitty, WezTerm, Ghostty, foot, Alacritty, recent iTerm2; Alt+Enter, Ctrl+J or a trailing `\` work everywhere) |
96
98
  | Esc | Cancel the running turn |
97
99
  | Ctrl+C twice | Exit; prints `nova-ai --resume <id>` |
98
- | Shift+Tab | Cycle agent / ask / plan |
100
+ | Shift+Tab | Cycle agent / ask / plan and your own agents |
99
101
  | Ctrl+R / Ctrl+P / Ctrl+K | Sessions / models / permission mode |
100
102
  | Ctrl+O / Ctrl+T / Ctrl+B | Verbose tool output / tool inspector / background jobs |
101
103
  | Alt+V | Paste a clipboard image |
102
104
 
103
- Slash commands: `/help`, `/model`, `/mode`, `/permission`, `/session`,
105
+ Slash commands: `/help`, `/model`, `/effort` (thinking models), `/mode`, `/permission`, `/session`,
104
106
  `/resume`, `/clear`, `/compact`, `/rewind [n]`, `/queue`, `/steer`, `/usage`,
105
107
  `/tools`, `/mcp`, `/skills`, `/shell` and `/agent` (background jobs), `/trust`
106
108
  and `/exit`. Details:
@@ -169,7 +171,10 @@ need ACP. Everything is documented in
169
171
  and nested shells need an exact rule.
170
172
  - **Web pages are fetched only with approval.** `fetch_url` asks every time
171
173
  unless a rule allows that URL (a URL can carry data out), and a redirect to
172
- another site needs its own approval.
174
+ another site needs its own approval. `web_search` asks the same way, since
175
+ the query goes to the search providers (You.com, DuckDuckGo; your own
176
+ SearXNG server first when `~/.nova-ai/settings.json` has
177
+ `"webSearch": { "searxngUrl": "https://…" }`).
173
178
  - **File access stays in the workspace** (symlinks resolved); commands run in
174
179
  their own process group with a timeout, and the whole tree is stopped on
175
180
  cancel.
@@ -194,7 +199,22 @@ VS Code extension: settings, per-project trust, sessions and memory.
194
199
  skills folders are read too). `/skills` lists them; `/skills off <name>`
195
200
  switches one off in this project (`--global`: everywhere). Only names and
196
201
  short descriptions go into the prompt; Nova loads a skill when it fits.
202
+ Nova installs its own skills for Word, Excel, PowerPoint and PDF files
203
+ (`nova-docx`, `nova-xlsx`, `nova-pptx`, `nova-pdf`, from
204
+ [nova-ai-skills](https://github.com/Datalab-Rotterdam/nova-ai-skills)) in
205
+ `~/.nova-ai/skills` and keeps them up to date; they need Python 3.
206
+ - **Hooks**: Claude Code's `hooks` format in the same settings files (a
207
+ repository's own only once you trust the workspace): `PreToolUse`,
208
+ `PostToolUse`, `UserPromptSubmit`, `SessionStart` and `Stop`, with Claude
209
+ Code's tool names as matchers, so hooks from `.claude/settings.json` work
210
+ when copied over.
197
211
  - **MCP servers**: `.mcp.json` or `mcpServers` in `.nova-ai/settings.json`.
212
+ HTTP servers that use OAuth show "needs sign-in" in `/mcp`; `/mcp login
213
+ <server>` opens the sign-in page in your browser (tokens are kept in
214
+ `~/.nova-ai/mcp-auth`, refreshed automatically), `/mcp logout <server>`
215
+ forgets them. A server that wants a client registered by hand takes
216
+ `"oauth": { "clientId", "clientSecret", "scope", "callbackPort" }` in
217
+ `.mcp.json`.
198
218
 
199
219
  The complete layout and rule syntax: [docs/NOVA_HOME.md](https://github.com/Datalab-Rotterdam/nova-ai-cli/blob/main/docs/NOVA_HOME.md).
200
220
 
@@ -207,6 +227,12 @@ to a text protocol for models that don't support it. When the context window
207
227
  fills up, older messages are summarized automatically (`/compact` does it by
208
228
  hand).
209
229
 
230
+ Thinking models are marked ◆ in the model list. For those with levels the
231
+ status line shows the current one (`· thinking medium`) and `/effort` opens a
232
+ slider (←/→, Enter), or sets one directly (`/effort high`, `/effort off`). The
233
+ choice is remembered like the model; a model without that level uses its own
234
+ default.
235
+
210
236
  | Environment | |
211
237
  |---|---|
212
238
  | `NOVA_API_KEY`, `NOVA_MODEL` | Use this key / model instead of the stored ones |
@@ -1,5 +1,6 @@
1
1
  // src/tui/session/tui-acp-client.ts
2
2
  import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
3
+ import { openBrowser } from "@datalabrotterdam/nova-ai-agent/acp/auth-server.js";
3
4
  import { readFile, writeFile, mkdir } from "fs/promises";
4
5
  import { dirname } from "path";
5
6
  import {
@@ -7,13 +8,11 @@ import {
7
8
  terminateProcessTree
8
9
  } from "@datalabrotterdam/nova-ai-agent/core/process-tree.js";
9
10
  import { resolveWorkspaceFile } from "@datalabrotterdam/nova-ai-agent/core/tools/workspace-paths.js";
10
- import {
11
- isInteractionMode
12
- } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
13
11
  import {
14
12
  fromElicitationRequest,
15
13
  toElicitationResponse
16
14
  } from "@datalabrotterdam/nova-ai-agent/acp/user-questions.js";
15
+ import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
17
16
  var DEFAULT_OUTPUT_LIMIT = 1e5;
18
17
  var nextMessageId = 0;
19
18
  function uid() {
@@ -31,6 +30,8 @@ var TuiAcpClient = class {
31
30
  cwd;
32
31
  onBackgroundLifecycle;
33
32
  onInteractionModeChange;
33
+ /** The modes the agent offers (built-in, then the user's agent files); set by the runner. */
34
+ modeIds = [...INTERACTION_MODES];
34
35
  capabilities = {
35
36
  fs: { readTextFile: true, writeTextFile: true },
36
37
  terminal: true,
@@ -91,6 +92,29 @@ var TuiAcpClient = class {
91
92
  appendUserMessage(text) {
92
93
  this.appendMessage({ id: uid(), role: "user", text });
93
94
  }
95
+ /** An MCP server's sign-in page: opened in the browser, and printed in case that fails. */
96
+ mcpAuthUrl(params) {
97
+ const { serverName, url } = params ?? {};
98
+ if (typeof serverName !== "string" || typeof url !== "string") return;
99
+ let page;
100
+ try {
101
+ page = new URL(url);
102
+ } catch {
103
+ return;
104
+ }
105
+ if (page.protocol !== "https:" && page.protocol !== "http:") {
106
+ this.appendError(`MCP server "${serverName}" sent a sign-in address that is not a web page; not opened.`);
107
+ return;
108
+ }
109
+ openBrowser(page.toString());
110
+ this.appendMessage({
111
+ id: uid(),
112
+ role: "assistant",
113
+ streaming: false,
114
+ text: `Sign in to MCP server "${serverName}" in your browser. If it did not open, visit:
115
+ ${page}`
116
+ });
117
+ }
94
118
  appendError(text) {
95
119
  this.appendMessage({ id: uid(), role: "error", text });
96
120
  }
@@ -295,7 +319,7 @@ ${err.message}`;
295
319
  }
296
320
  return;
297
321
  case "current_mode_update":
298
- if (isInteractionMode(update.currentModeId)) {
322
+ if (this.modeIds.includes(update.currentModeId)) {
299
323
  this.onInteractionModeChange?.(update.currentModeId);
300
324
  }
301
325
  return;
@@ -415,6 +439,8 @@ ${err.message}`;
415
439
  return this.backgroundUpdate(params);
416
440
  case NOVA_NOTIFICATIONS.queueChanged:
417
441
  return this.queueChanged(params);
442
+ case NOVA_NOTIFICATIONS.mcpAuthUrl:
443
+ return this.mcpAuthUrl(params);
418
444
  default:
419
445
  return;
420
446
  }
@@ -2,7 +2,7 @@ import {
2
2
  TuiAcpClient,
3
3
  choosePermissionOption,
4
4
  createStore
5
- } from "./chunk-QSNU4H55.js";
5
+ } from "./chunk-EOPM4RRE.js";
6
6
 
7
7
  // src/headless/index.ts
8
8
  import { resolve } from "path";
package/dist/index.js CHANGED
@@ -17,8 +17,7 @@ var MODE_FLAGS = {
17
17
  "--headless": "print",
18
18
  "--setup": "login",
19
19
  "--version": "version",
20
- "-v": "version",
21
- "--web": "web"
20
+ "-v": "version"
22
21
  };
23
22
  function parseCli(argv) {
24
23
  const first = argv[0];
@@ -34,7 +33,7 @@ function parseCli(argv) {
34
33
  const mode = [...modes][0];
35
34
  const wantsHelp = options.includes("--help") || options.includes("-h");
36
35
  if (!mode) return wantsHelp ? { mode: "help", args: [] } : { mode: "tui", args: argv };
37
- const drop = /* @__PURE__ */ new Set(["--acp", "--headless", "--setup", "--version", "-v", "--web"]);
36
+ const drop = /* @__PURE__ */ new Set(["--acp", "--headless", "--setup", "--version", "-v"]);
38
37
  return { mode, args: argv.filter((arg, index) => !(drop.has(arg) && (end < 0 || index < end))) };
39
38
  }
40
39
  function versionText() {
@@ -140,7 +139,7 @@ ${MAIN_HELP}
140
139
  }
141
140
  case "print": {
142
141
  const update = startUpdateCheck();
143
- const { default: runHeadless } = await import("./headless-C36E4XSQ.js");
142
+ const { default: runHeadless } = await import("./headless-QLCJRT3X.js");
144
143
  const code = await runHeadless(command.args);
145
144
  if (update) await noticeUpdate(update, 500);
146
145
  return code;
@@ -153,11 +152,6 @@ ${MAIN_HELP}
153
152
  const { runLogout } = await import("@datalabrotterdam/nova-ai-agent/commands/login.js");
154
153
  return runLogout();
155
154
  }
156
- case "web": {
157
- const { default: runWebUi } = await import("./webui-L4GGMXJ3.js");
158
- await runWebUi();
159
- return 0;
160
- }
161
155
  case "tui": {
162
156
  let options;
163
157
  try {
@@ -175,7 +169,7 @@ ${MAIN_HELP}
175
169
  );
176
170
  return 2;
177
171
  }
178
- const { default: runChat } = await import("./tui-TRADKND7.js");
172
+ const { default: runChat } = await import("./tui-HJQOYGHL.js");
179
173
  return runChat(options);
180
174
  }
181
175
  }
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  TuiAcpClient,
12
12
  createStore
13
- } from "./chunk-QSNU4H55.js";
13
+ } from "./chunk-EOPM4RRE.js";
14
14
 
15
15
  // src/tui/index.ts
16
16
  import { createInterface as createInterface2 } from "readline/promises";
@@ -122,11 +122,15 @@ import * as acp from "@agentclientprotocol/sdk";
122
122
  import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
123
123
  import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
124
124
  import { replayHistory } from "@datalabrotterdam/nova-ai-agent/core/history.js";
125
- import { saveDefaultModel } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
125
+ import {
126
+ saveDefaultEffort,
127
+ saveDefaultModel
128
+ } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
126
129
  import {
127
130
  savePermissionMode
128
131
  } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
129
132
  import { loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
133
+ import { resolveEffort } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
130
134
 
131
135
  // src/tui/files/file-mentions.ts
132
136
  import { statSync } from "fs";
@@ -286,6 +290,7 @@ var SessionRunner = class {
286
290
  clientInfo: { name: "nova-ai-cli-tui", version: "1" }
287
291
  });
288
292
  this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
293
+ this.effort = credentials.defaultEffort ?? null;
289
294
  this.sessionId = crypto.randomUUID();
290
295
  this.sessionReady = Promise.resolve();
291
296
  }
@@ -296,6 +301,10 @@ var SessionRunner = class {
296
301
  title = null;
297
302
  model;
298
303
  interactionMode = "agent";
304
+ /** The modes the agent offers: the built-in ones, then the user's agent files. */
305
+ get modeIds() {
306
+ return this.acpClient.modeIds;
307
+ }
299
308
  /** The agent, served in-process over a real ACP connection (tests may stub it). */
300
309
  agent = new NovaAgent();
301
310
  acpClient;
@@ -315,7 +324,10 @@ var SessionRunner = class {
315
324
  handoffActive = false;
316
325
  queuedAttachments = /* @__PURE__ */ new Map();
317
326
  editingQueuedMessageId = null;
318
- imageSupportByModel = /* @__PURE__ */ new Map();
327
+ /** From the agent's model list: image input, thinking levels. */
328
+ modelInfo = /* @__PURE__ */ new Map();
329
+ /** The thinking level the user chose (saved); null: each model's default. */
330
+ effort;
319
331
  contextUsageRequest = 0;
320
332
  modelCheck = null;
321
333
  resumeFrom(storedSessionId) {
@@ -353,6 +365,7 @@ var SessionRunner = class {
353
365
  this.acpClient.muteSessionUpdates = false;
354
366
  }
355
367
  }).then((response) => {
368
+ this.applyModes(response.modes);
356
369
  this.applyMcpStatus(response._meta);
357
370
  this.applySkillStatus(response._meta);
358
371
  this.agentSessionLoaded = true;
@@ -360,6 +373,7 @@ var SessionRunner = class {
360
373
  void this.nova.setSessionMode({ sessionId: this.sessionId, modeId: this.interactionMode }).catch(() => {
361
374
  });
362
375
  this.applyPermissionMode(this.sessionId);
376
+ this.applyEffort(this.sessionId);
363
377
  void this.updateContextUsage();
364
378
  void this.refreshContextUsage().catch(() => {
365
379
  });
@@ -482,6 +496,14 @@ var SessionRunner = class {
482
496
  skillSessionStatus() {
483
497
  return this.skills.map((skill) => ({ ...skill }));
484
498
  }
499
+ /** Signs in to (or out of) an HTTP MCP server; the agent reconnects it and reports the new status. */
500
+ async mcpSignIn(serverName, signIn) {
501
+ await this.ensureSession();
502
+ const params = { sessionId: this.sessionId, serverName };
503
+ const meta = signIn ? await this.nova.mcpLogin(params) : await this.nova.mcpLogout(params);
504
+ this.applyMcpStatus(meta);
505
+ return this.mcpSessionStatus();
506
+ }
485
507
  async trustWorkspace() {
486
508
  await this.initialized;
487
509
  await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
@@ -581,8 +603,12 @@ var SessionRunner = class {
581
603
  async listModels() {
582
604
  await this.initialized;
583
605
  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 }));
606
+ for (const model of models) this.modelInfo.set(model.id, model);
607
+ return models.map((model) => ({ id: model.id, name: model.name, thinking: model.thinking }));
608
+ }
609
+ /** Sync, from the loaded model list: false until it has loaded. */
610
+ hasThinkingLevels(model = this.model) {
611
+ return (this.modelInfo.get(model)?.efforts.length ?? 0) > 0;
586
612
  }
587
613
  /**
588
614
  * Takes the account's default model when none is set, and replaces a saved
@@ -597,7 +623,7 @@ var SessionRunner = class {
597
623
  const { models, defaultModel } = await this.nova.modelsList(
598
624
  this.model ? { preferred: this.model } : {}
599
625
  );
600
- for (const model of models) this.imageSupportByModel.set(model.id, model.imageInput);
626
+ for (const model of models) this.modelInfo.set(model.id, model);
601
627
  if (!defaultModel || defaultModel === this.model) return;
602
628
  const previous = this.model;
603
629
  this.setModel(defaultModel);
@@ -610,8 +636,41 @@ var SessionRunner = class {
610
636
  return this.modelCheck;
611
637
  }
612
638
  async supportsImageInput(model = this.model) {
613
- if (!this.imageSupportByModel.has(model)) await this.listModels();
614
- return this.imageSupportByModel.get(model) ?? false;
639
+ return (await this.modelDetails(model))?.imageInput ?? false;
640
+ }
641
+ /** What the agent's model list says about `model`; undefined when it isn't listed. */
642
+ async modelDetails(model = this.model) {
643
+ if (!this.modelInfo.has(model)) await this.listModels();
644
+ return this.modelInfo.get(model);
645
+ }
646
+ /**
647
+ * The level the current model thinks at, as the agent picks it (the
648
+ * chosen one when the model has it); null for a model without levels
649
+ * or before the model list has loaded.
650
+ */
651
+ currentEffort() {
652
+ const info = this.modelInfo.get(this.model);
653
+ if (!info?.efforts.length) return null;
654
+ return resolveEffort(
655
+ { reasoning_efforts: info.efforts, reasoning_default: info.reasoningDefault },
656
+ this.effort
657
+ ) ?? null;
658
+ }
659
+ /** The current model's levels from the loaded list; empty when it has none. */
660
+ modelEffortLevels() {
661
+ return this.modelInfo.get(this.model)?.efforts ?? [];
662
+ }
663
+ /** Saved and kept across models and sessions; a model without the level uses its default. */
664
+ setEffort(level) {
665
+ this.effort = level;
666
+ saveDefaultEffort(level);
667
+ if (this.agentSessionLoaded) this.applyEffort(this.sessionId);
668
+ }
669
+ applyEffort(sessionId) {
670
+ if (!this.effort) return;
671
+ void this.nova.setSessionConfigOption({ sessionId, configId: "thought_level", value: this.effort }).catch(() => {
672
+ this.acpClient.appendError("Could not change the thinking level.");
673
+ });
615
674
  }
616
675
  async compactContext() {
617
676
  if (this.store.getState().busy || this.promptActive) {
@@ -890,11 +949,12 @@ var SessionRunner = class {
890
949
  }
891
950
  async createSession() {
892
951
  await this.initialized;
893
- const { sessionId, _meta } = await this.nova.newSession({
952
+ const { sessionId, _meta, modes } = await this.nova.newSession({
894
953
  cwd: this.cwd,
895
954
  mcpServers: []
896
955
  });
897
956
  this.sessionId = sessionId;
957
+ this.applyModes(modes);
898
958
  this.applyMcpStatus(_meta);
899
959
  this.applySkillStatus(_meta);
900
960
  this.agentSessionLoaded = true;
@@ -902,6 +962,7 @@ var SessionRunner = class {
902
962
  void this.nova.setSessionMode({ sessionId, modeId: this.interactionMode }).catch(() => {
903
963
  });
904
964
  this.applyPermissionMode(sessionId);
965
+ this.applyEffort(sessionId);
905
966
  void this.updateContextUsage();
906
967
  void this.refreshContextUsage().catch(() => {
907
968
  });
@@ -937,7 +998,13 @@ var SessionRunner = class {
937
998
  const failures = Array.isArray(record.failures) ? record.failures.flatMap((entry) => {
938
999
  if (!entry || typeof entry !== "object") return [];
939
1000
  const item = entry;
940
- return typeof item.serverName === "string" && typeof item.message === "string" ? [{ serverName: item.serverName, message: item.message }] : [];
1001
+ return typeof item.serverName === "string" && typeof item.message === "string" ? [
1002
+ {
1003
+ serverName: item.serverName,
1004
+ message: item.message,
1005
+ ...item.needsAuth === true ? { needsAuth: true } : {}
1006
+ }
1007
+ ] : [];
941
1008
  }) : [];
942
1009
  this.mcpStatus = { configured, connected, failures };
943
1010
  }
@@ -981,6 +1048,10 @@ var SessionRunner = class {
981
1048
  if (this.sessionReady === ready) this.sessionReady = this.createSession();
982
1049
  }
983
1050
  }
1051
+ applyModes(modes) {
1052
+ if (modes?.availableModes.length) this.acpClient.modeIds = modes.availableModes.map((mode) => mode.id);
1053
+ if (!this.modeIds.includes(this.interactionMode)) this.applyInteractionMode("agent", "Mode: agent");
1054
+ }
984
1055
  applyInteractionMode(mode, statusLine) {
985
1056
  this.interactionMode = mode;
986
1057
  this.store.setState({ interactionMode: mode, statusLine });
@@ -1450,6 +1521,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
1450
1521
  function StatusLine({
1451
1522
  ui,
1452
1523
  model,
1524
+ effort = null,
1453
1525
  mcp,
1454
1526
  scroll = 0
1455
1527
  }) {
@@ -1460,6 +1532,7 @@ function StatusLine({
1460
1532
  ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
1461
1533
  /* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
1462
1534
  /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
1535
+ effort ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ` \xB7 thinking ${effort.toLowerCase()}` }) : null,
1463
1536
  separator,
1464
1537
  ui.interactionMode,
1465
1538
  separator,
@@ -1802,18 +1875,14 @@ function PlanView({
1802
1875
  ] });
1803
1876
  }
1804
1877
 
1805
- // src/tui/ink/controller.ts
1806
- import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1807
-
1808
1878
  // src/tui/commands/builtins.ts
1809
- import { isInteractionMode } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
1810
1879
  var permissionModes = ["ask", "acceptEdits", "bypassAll"];
1811
1880
  var builtinCommands = [
1812
1881
  {
1813
1882
  name: "help",
1814
1883
  description: "Show available commands",
1815
1884
  run(ctx) {
1816
- const lines = builtinCommands.map((c) => `/${c.name} \u2014 ${c.description}`);
1885
+ const lines = builtinCommands.filter((c) => c.available?.(ctx) ?? true).map((c) => `/${c.name} \u2014 ${c.description}`);
1817
1886
  ctx.print(lines.join("\n"));
1818
1887
  }
1819
1888
  },
@@ -1894,15 +1963,15 @@ var builtinCommands = [
1894
1963
  },
1895
1964
  {
1896
1965
  name: "mode",
1897
- description: "Switch interaction mode (/mode agent|ask|plan)",
1966
+ description: "Switch interaction mode (/mode agent|ask|plan|<agent file>)",
1898
1967
  run(ctx, args) {
1899
1968
  const mode = args.trim();
1900
1969
  if (!mode) {
1901
- ctx.print(`Current mode: ${ctx.getInteractionMode()}`);
1970
+ ctx.print(`Current mode: ${ctx.getInteractionMode()} (available: ${ctx.getInteractionModes().join(", ")})`);
1902
1971
  return;
1903
1972
  }
1904
- if (!isInteractionMode(mode)) {
1905
- ctx.print("Usage: /mode agent|ask|plan");
1973
+ if (!ctx.getInteractionModes().includes(mode)) {
1974
+ ctx.print(`Usage: /mode ${ctx.getInteractionModes().join("|")}`);
1906
1975
  return;
1907
1976
  }
1908
1977
  ctx.setInteractionMode(mode);
@@ -1944,6 +2013,16 @@ var builtinCommands = [
1944
2013
  ctx.openModelPicker();
1945
2014
  }
1946
2015
  },
2016
+ {
2017
+ name: "effort",
2018
+ description: "Set how hard the model thinks (/effort <level>, or no args for a slider)",
2019
+ available: (ctx) => ctx.hasThinkingLevels(),
2020
+ async run(ctx, args) {
2021
+ const level = args.trim();
2022
+ if (level) ctx.print(await ctx.setEffort(level));
2023
+ else await ctx.openEffortPicker();
2024
+ }
2025
+ },
1947
2026
  {
1948
2027
  name: "tools",
1949
2028
  description: "Inspect tool calls from this session",
@@ -1953,9 +2032,20 @@ var builtinCommands = [
1953
2032
  },
1954
2033
  {
1955
2034
  name: "mcp",
1956
- description: "Inspect configured MCP servers",
1957
- run(ctx) {
1958
- return ctx.openMcpInspector();
2035
+ description: "Inspect MCP servers, or sign in to one: /mcp login|logout <server>",
2036
+ async run(ctx, args) {
2037
+ const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
2038
+ const name = rest.join(" ");
2039
+ if (!action) {
2040
+ await ctx.openMcpInspector();
2041
+ return;
2042
+ }
2043
+ if (action !== "login" && action !== "logout" || !name) {
2044
+ ctx.print("Usage: /mcp, or /mcp login|logout <server>");
2045
+ return;
2046
+ }
2047
+ if (action === "login") ctx.print(`Signing in to "${name}"\u2026 (waits up to 5 minutes for the browser)`);
2048
+ ctx.print(await ctx.mcpSignIn(name, action === "login"));
1959
2049
  }
1960
2050
  },
1961
2051
  {
@@ -2142,8 +2232,8 @@ function formatJob(job) {
2142
2232
 
2143
2233
  // src/tui/commands/index.ts
2144
2234
  var COMMANDS = [...builtinCommands];
2145
- function allCommands() {
2146
- return COMMANDS;
2235
+ function allCommands(ctx) {
2236
+ return ctx ? COMMANDS.filter((c) => c.available?.(ctx) ?? true) : COMMANDS;
2147
2237
  }
2148
2238
  function findCommand(name) {
2149
2239
  return COMMANDS.find((c) => c.name === name);
@@ -2361,7 +2451,8 @@ var WorkspaceAutocompleteProvider = class {
2361
2451
  const mention = extractMentionPrefix(before);
2362
2452
  if (mention) return this.mentionSuggestions(mention, options.signal);
2363
2453
  if (cursorLine === 0 && before.startsWith("/") && !/\s/.test(before)) {
2364
- const items2 = filterCommands(this.commands, before.slice(1));
2454
+ const commands = typeof this.commands === "function" ? this.commands() : this.commands;
2455
+ const items2 = filterCommands(commands, before.slice(1));
2365
2456
  return items2.length ? { items: items2, prefix: before } : null;
2366
2457
  }
2367
2458
  if (!options.force) return null;
@@ -2884,6 +2975,40 @@ var SelectionState = class {
2884
2975
  return "changed";
2885
2976
  }
2886
2977
  };
2978
+ var SliderState = class {
2979
+ constructor(levels, selected, onSelect, onCancel) {
2980
+ this.levels = levels;
2981
+ this.selected = selected;
2982
+ this.onSelect = onSelect;
2983
+ this.onCancel = onCancel;
2984
+ }
2985
+ levels;
2986
+ selected;
2987
+ onSelect;
2988
+ onCancel;
2989
+ handleKey(input, key) {
2990
+ if (isCancel(input, key)) {
2991
+ this.onCancel();
2992
+ return "closed";
2993
+ }
2994
+ if (key.return) {
2995
+ const level = this.levels[this.selected];
2996
+ if (!level) return "ignored";
2997
+ this.onSelect(level);
2998
+ return "closed";
2999
+ }
3000
+ const last = this.levels.length - 1;
3001
+ let next = this.selected;
3002
+ if (key.leftArrow || key.downArrow) next = Math.max(0, this.selected - 1);
3003
+ else if (key.rightArrow || key.upArrow) next = Math.min(last, this.selected + 1);
3004
+ else if (key.home) next = 0;
3005
+ else if (key.end) next = last;
3006
+ else return "ignored";
3007
+ if (next === this.selected) return "ignored";
3008
+ this.selected = next;
3009
+ return "changed";
3010
+ }
3011
+ };
2887
3012
  var ScrollState = class {
2888
3013
  constructor(onClose) {
2889
3014
  this.onClose = onClose;
@@ -3132,6 +3257,9 @@ function permissionKey(request, input, key) {
3132
3257
  return "closed";
3133
3258
  }
3134
3259
 
3260
+ // src/tui/ink/controller.ts
3261
+ import { effortLabel } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
3262
+
3135
3263
  // src/tui/ink/editor/prompt-editor.ts
3136
3264
  var MAX_HISTORY = 200;
3137
3265
  var PromptEditor = class {
@@ -3965,8 +4093,8 @@ var TuiController = class {
3965
4093
  "Models",
3966
4094
  models.map((model) => ({
3967
4095
  value: model.id,
3968
- label: model.id,
3969
- description: model.name ?? void 0
4096
+ label: model.thinking ? `${model.id} \u25C6` : model.id,
4097
+ description: [model.name, model.thinking ? "thinking" : null].filter(Boolean).join(" \xB7 ") || void 0
3970
4098
  })),
3971
4099
  "model-picker",
3972
4100
  (model) => this.setModel(model)
@@ -3975,6 +4103,58 @@ var TuiController = class {
3975
4103
  this.showText("Models", errorMessage(error, "Failed to list models."));
3976
4104
  }
3977
4105
  };
4106
+ effortName() {
4107
+ const level = this.runner.currentEffort();
4108
+ const efforts = level ? this.runner.modelEffortLevels() : [];
4109
+ return level ? effortLabel(level, efforts) : null;
4110
+ }
4111
+ /** The selected model's levels and current one, or why it has none. */
4112
+ async effortLevels() {
4113
+ let info;
4114
+ try {
4115
+ info = await this.runner.modelDetails();
4116
+ } catch (error) {
4117
+ return errorMessage(error, "Could not load the model list.");
4118
+ }
4119
+ const model = this.runner.model || "This model";
4120
+ if (!info?.efforts.length) {
4121
+ return info?.thinking ? `${model} thinks at a fixed depth; there is no level to choose.` : `${model} is not a thinking model; /model lists the ones marked \u25C6.`;
4122
+ }
4123
+ return { efforts: info.efforts, current: this.runner.currentEffort() ?? info.efforts[0] };
4124
+ }
4125
+ /** /effort: a slider over the selected model's levels. */
4126
+ openEffort = async () => {
4127
+ const levels = await this.effortLevels();
4128
+ if (typeof levels === "string") {
4129
+ this.showText("Thinking effort", levels);
4130
+ return;
4131
+ }
4132
+ const state = new SliderState(
4133
+ levels.efforts.map((value) => ({ value, label: effortLabel(value, levels.efforts) })),
4134
+ Math.max(0, levels.efforts.indexOf(levels.current)),
4135
+ (item) => {
4136
+ this.closeOverlay();
4137
+ this.runner.setEffort(item.value);
4138
+ this.store.setState({ statusLine: `Thinking effort: ${item.label}.` });
4139
+ },
4140
+ this.closeOverlay
4141
+ );
4142
+ this.showOverlay({ kind: "slider", title: `Thinking effort \xB7 ${this.runner.model}`, state });
4143
+ };
4144
+ /** /effort <level>: by id ("high") or name ("Extra high", "off"). */
4145
+ setEffort = async (level) => {
4146
+ const levels = await this.effortLevels();
4147
+ if (typeof levels === "string") return levels;
4148
+ const wanted = level.trim().toLowerCase();
4149
+ const value = levels.efforts.find(
4150
+ (candidate) => candidate === wanted || effortLabel(candidate, levels.efforts).toLowerCase() === wanted
4151
+ );
4152
+ const names = levels.efforts.map((candidate) => effortLabel(candidate, levels.efforts).toLowerCase());
4153
+ if (!value) return `${this.runner.model} has these levels: ${names.join(", ")}.`;
4154
+ this.runner.setEffort(value);
4155
+ this.refresh();
4156
+ return `Thinking effort: ${effortLabel(value, levels.efforts)}.`;
4157
+ };
3978
4158
  openPermissionModes = () => {
3979
4159
  const modes = [
3980
4160
  { value: "ask", label: "ask", description: "Prompt before tools make changes" },
@@ -4061,7 +4241,7 @@ status: failed - ${failure.message}`);
4061
4241
  `Skills (${on} on, ${skills.length - on} off)`,
4062
4242
  skills.length ? [
4063
4243
  ...skills.map(
4064
- (skill) => `${skill.name} [${skill.source}]${skill.enabled ? "" : " (off)"}
4244
+ (skill) => `${skill.name} [${skill.bundledBy ? `bundled with ${skill.bundledBy}` : skill.source}]${skill.enabled ? "" : " (off)"}
4065
4245
  ${skill.description}
4066
4246
  ${skill.path}`
4067
4247
  ),
@@ -4132,8 +4312,8 @@ ${skill.path}`
4132
4312
  this.runner.setInteractionMode(mode);
4133
4313
  }
4134
4314
  cycleInteractionMode() {
4135
- const current = INTERACTION_MODES.indexOf(this.runner.interactionMode);
4136
- this.setInteractionMode(INTERACTION_MODES[(current + 1) % INTERACTION_MODES.length]);
4315
+ const modes = this.runner.modeIds;
4316
+ this.setInteractionMode(modes[(modes.indexOf(this.runner.interactionMode) + 1) % modes.length]);
4137
4317
  }
4138
4318
  appendAssistant(text) {
4139
4319
  this.store.setState((state) => ({
@@ -4175,13 +4355,28 @@ ${skill.path}`
4175
4355
  },
4176
4356
  openSessionSwitcher: this.openSessions,
4177
4357
  openModelPicker: () => void this.openModels(),
4358
+ hasThinkingLevels: () => this.runner.hasThinkingLevels(),
4359
+ openEffortPicker: () => this.openEffort(),
4360
+ setEffort: (level) => this.setEffort(level),
4178
4361
  getModel: () => this.runner.model,
4179
4362
  setModel: (model) => this.setModel(model),
4180
4363
  getInteractionMode: () => this.runner.interactionMode,
4364
+ getInteractionModes: () => this.runner.modeIds,
4181
4365
  setInteractionMode: (mode) => this.setInteractionMode(mode),
4182
4366
  getPermissionMode: () => this.store.getState().permissionMode,
4183
4367
  setPermissionMode: (mode) => this.setPermissionMode(mode),
4184
4368
  openPermissionPicker: this.openPermissionModes,
4369
+ mcpSignIn: async (serverName, signIn) => {
4370
+ try {
4371
+ const status = await this.runner.mcpSignIn(serverName, signIn);
4372
+ this.refresh();
4373
+ const failure = status.failures.find((item) => item.serverName === serverName);
4374
+ if (!signIn) return `Signed out of "${serverName}".`;
4375
+ return failure ? `Signed in, but "${serverName}" did not connect: ${failure.message}` : `Signed in to "${serverName}"; its tools are available from the next message.`;
4376
+ } catch (error) {
4377
+ return errorMessage(error, signIn ? "Sign-in failed." : "Sign-out failed.");
4378
+ }
4379
+ },
4185
4380
  trustWorkspace: async () => {
4186
4381
  try {
4187
4382
  await this.runner.trustWorkspace();
@@ -4288,7 +4483,7 @@ ${skill.path}`
4288
4483
  const fdPath = resolveFdPath();
4289
4484
  editor.setAutocompleteProvider(
4290
4485
  new WorkspaceAutocompleteProvider(
4291
- allCommands().map((command) => ({
4486
+ () => allCommands(this.commandContext).map((command) => ({
4292
4487
  name: command.name,
4293
4488
  description: command.description
4294
4489
  })),
@@ -4329,6 +4524,7 @@ ${skill.path}`
4329
4524
  overlay: this.overlay,
4330
4525
  question: this.questionState(ui.pendingQuestion),
4331
4526
  model: this.runner.model,
4527
+ effort: this.effortName(),
4332
4528
  mcp: mcpCounts(this.runner),
4333
4529
  expandedTools: this.expandedTools,
4334
4530
  exiting: this.exiting,
@@ -4373,7 +4569,7 @@ function formatMcpServer(server, status) {
4373
4569
  }
4374
4570
  const failure = status.failures.find((item) => item.serverName === server.name);
4375
4571
  lines.push(
4376
- failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
4572
+ failure?.needsAuth ? `status: needs sign-in - /mcp login ${server.name}` : failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
4377
4573
  );
4378
4574
  return lines.join("\n");
4379
4575
  }
@@ -4412,6 +4608,25 @@ function Frame({ title, children, footer }) {
4412
4608
  footer ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, wrap: "truncate-end", children: footer }) : null
4413
4609
  ] });
4414
4610
  }
4611
+ function SliderDialog({
4612
+ title,
4613
+ state,
4614
+ isActive = true
4615
+ }) {
4616
+ useDialogInput((input, key) => state.handleKey(input, key), isActive);
4617
+ const current = state.levels[state.selected];
4618
+ return /* @__PURE__ */ jsxs5(Frame, { title, footer: "\u2190\u2192 adjust \xB7 Enter apply \xB7 Esc cancel", children: [
4619
+ /* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: state.levels.map((level, index) => {
4620
+ const selected = index === state.selected;
4621
+ return /* @__PURE__ */ jsxs5(Text5, { children: [
4622
+ index > 0 ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, children: " \u2500\u2500 " }) : null,
4623
+ /* @__PURE__ */ jsx5(Text5, { color: selected ? palette.primary : palette.faint, children: selected ? "\u25CF " : "\u25CB " }),
4624
+ /* @__PURE__ */ jsx5(Text5, { bold: selected, color: selected ? void 0 : palette.muted, children: level.label })
4625
+ ] }, level.value);
4626
+ }) }),
4627
+ current?.description ? /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: current.description }) : null
4628
+ ] });
4629
+ }
4415
4630
  function SelectionDialog({
4416
4631
  title,
4417
4632
  state,
@@ -4800,7 +5015,7 @@ function App({ controller }) {
4800
5015
  ),
4801
5016
  /* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
4802
5017
  /* @__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 })
5018
+ /* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, effort: view.effort, mcp: view.mcp, scroll: view.scroll })
4804
5019
  ] })
4805
5020
  ] });
4806
5021
  }
@@ -4843,6 +5058,7 @@ function Surface({
4843
5058
  const overlay = view.overlay;
4844
5059
  if (overlay?.kind === "selection")
4845
5060
  return /* @__PURE__ */ jsx6(SelectionDialog, { title: overlay.title, state: overlay.state, height: rows });
5061
+ if (overlay?.kind === "slider") return /* @__PURE__ */ jsx6(SliderDialog, { title: overlay.title, state: overlay.state });
4846
5062
  if (overlay?.kind === "text")
4847
5063
  return /* @__PURE__ */ jsx6(
4848
5064
  ScrollPanel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.6",
3
+ "version": "1.2.0-alpha.7",
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.3",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.4",
43
43
  "chalk": "^5.3.0",
44
44
  "cli-highlight": "^2.1.11",
45
45
  "diff": "^9.0.0",
@@ -76,6 +76,6 @@
76
76
  ]
77
77
  },
78
78
  "devDependencies": {
79
- "@datalabrotterdam/nova-sdk": "^1.3.2"
79
+ "@datalabrotterdam/nova-sdk": "1.4.0-k8s.3"
80
80
  }
81
81
  }
@@ -1,9 +0,0 @@
1
- // src/webui/index.ts
2
- async function runWebUi() {
3
- console.log("nova-ai --web is a work in progress \u2014 the browser UI isn't built yet.");
4
- console.log("Use `nova-ai` for the terminal UI, `nova-ai -p` headless, or `nova-ai --acp` for editors.");
5
- }
6
- var webui_default = runWebUi;
7
- export {
8
- webui_default as default
9
- };