@datalabrotterdam/nova-ai-cli 1.2.0-alpha.10 → 1.2.0-alpha.12

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,24 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.12](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.11...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.12) (2026-10-24)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** show the model's thinking as one block ([4479cac](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/4479cac70bf1f9973401bdfd420c401e00c11099))
7
+
8
+
9
+ ### Features
10
+
11
+ * **cli:** collapse the model's thinking by default ([28f04c1](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/28f04c1c5141aac2468c0aa8ba86dc38d885bb4e))
12
+ * **cli:** say when a collapsed tool row has output ([51274a5](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/51274a597700cf3208d48d54c2a32262e8947a96))
13
+ * token and cost totals per session ([8e9aef8](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/8e9aef81b6345832f38d8a2639c35d4cb6fb3b47))
14
+
15
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.11](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.10...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.11) (2026-10-24)
16
+
17
+
18
+ ### Features
19
+
20
+ * **agent:** skills and MCP prompts as slash commands ([95bb049](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/95bb04932c9edc12f964832b3e5274b3100387d1))
21
+
1
22
  # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.10](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.9...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.10) (2026-10-24)
2
23
 
3
24
 
package/README.md CHANGED
@@ -101,16 +101,18 @@ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
101
101
  | Ctrl+C twice | Exit; prints `nova-ai --resume <id>` |
102
102
  | Shift+Tab | Cycle agent / ask / plan / autopilot and your own agents |
103
103
  | Ctrl+R / Ctrl+P / Ctrl+K | Sessions / models / permission mode |
104
- | Ctrl+O / Ctrl+T / Ctrl+B | Verbose tool output / tool inspector / background jobs |
104
+ | Ctrl+O / Ctrl+T / Ctrl+B | Thinking and verbose tool output / tool inspector / background jobs |
105
105
  | Alt+V | Paste a clipboard image |
106
106
 
107
107
  Slash commands: `/help`, `/model`, `/effort` (thinking models), `/mode`, `/permission`, `/session`,
108
- `/resume`, `/clear`, `/compact`, `/rewind [n]`, `/queue`, `/steer`, `/usage`,
108
+ `/resume`, `/clear`, `/compact`, `/rewind [n]`, `/queue`, `/steer`, `/usage` (context, and the session's tokens and estimated cost; the status line shows `≈ €0.0123` once a priced model answered),
109
109
  `/tools`, `/mcp`, `/skills`, `/shell` and `/agent` (background jobs), `/trust`
110
110
  and `/exit`. Your own commands are markdown files in `~/.nova-ai/commands/` or
111
111
  the repository's `.nova-ai/commands/` (Claude Code's format, so
112
112
  `.claude/commands` works too): the file is the prompt, `$ARGUMENTS` what you
113
- type after the command, and they show up in the `/` menu. Details:
113
+ type after the command, and they show up in the `/` menu, together with your
114
+ skills (`/<skill-name> <request>`) and your MCP servers' prompts
115
+ (`/mcp__<server>__<prompt>`). Details:
114
116
  [`src/tui/README.md`](https://github.com/Datalab-Rotterdam/nova-ai-cli/blob/main/packages/cli/src/tui/README.md).
115
117
 
116
118
  ## Headless
@@ -1,4 +1,5 @@
1
1
  // src/tui/session/tui-acp-client.ts
2
+ import { parseUsageTotals } from "@datalabrotterdam/nova-ai-agent/core/usage.js";
2
3
  import { NOVA_NOTIFICATIONS } from "@datalabrotterdam/nova-ai-agent/acp/extensions.js";
3
4
  import { openBrowser } from "@datalabrotterdam/nova-ai-agent/acp/auth-server.js";
4
5
  import { readFile, writeFile, mkdir } from "fs/promises";
@@ -310,16 +311,15 @@ ${err.message}`;
310
311
  this.appendStreamingText(update.content.text);
311
312
  return;
312
313
  case "agent_thought_chunk":
313
- if (update.content.type === "text" && update.content.text.trim()) {
314
- this.resetStreaming();
315
- this.appendMessage({
316
- id: uid(),
317
- role: "assistant",
318
- text: update.content.text,
319
- streaming: false
320
- });
314
+ if (update.content.type === "text" && update.content.text) {
315
+ this.appendStreamingText(update.content.text, true);
321
316
  }
322
317
  return;
318
+ case "usage_update": {
319
+ const totals = parseUsageTotals(update._meta?.["nova-ai-cli/usageTotals"]);
320
+ if (totals) this.store.setState({ usageTotals: totals });
321
+ return;
322
+ }
323
323
  case "available_commands_update":
324
324
  this.agentCommands = update.availableCommands.map(({ name, description, input }) => ({
325
325
  name,
@@ -488,11 +488,13 @@ ${err.message}`;
488
488
  if (record.toolCallId && record.output)
489
489
  this.patchToolCall(record.toolCallId, { output: record.output });
490
490
  }
491
- appendStreamingText(text) {
491
+ appendStreamingText(text, thinking = false) {
492
+ const current = this.streamingMessageId ? this.store.getState().messages.find((message) => message.id === this.streamingMessageId) : void 0;
493
+ if (current?.role === "assistant" && Boolean(current.thinking) !== thinking) this.resetStreaming();
492
494
  if (!this.streamingMessageId) {
493
495
  const id = uid();
494
496
  this.streamingMessageId = id;
495
- this.appendMessage({ id, role: "assistant", text, streaming: true });
497
+ this.appendMessage({ id, role: "assistant", text, streaming: true, ...thinking ? { thinking: true } : {} });
496
498
  return;
497
499
  }
498
500
  this.store.setState((state) => ({
@@ -2,7 +2,7 @@ import {
2
2
  TuiAcpClient,
3
3
  choosePermissionOption,
4
4
  createStore
5
- } from "./chunk-KDAUUQBE.js";
5
+ } from "./chunk-KRC3RH3Y.js";
6
6
 
7
7
  // src/headless/index.ts
8
8
  import { resolve } from "path";
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ ${MAIN_HELP}
144
144
  }
145
145
  case "print": {
146
146
  const update = startUpdateCheck();
147
- const { default: runHeadless } = await import("./headless-BQXK7TNC.js");
147
+ const { default: runHeadless } = await import("./headless-XRIRZB7Z.js");
148
148
  const code = await runHeadless(command.args);
149
149
  if (update) await noticeUpdate(update, 500);
150
150
  return code;
@@ -185,7 +185,7 @@ ${MAIN_HELP}
185
185
  );
186
186
  return 2;
187
187
  }
188
- const { default: runChat } = await import("./tui-TJFV6NC6.js");
188
+ const { default: runChat } = await import("./tui-RR2LCTL7.js");
189
189
  return runChat(options);
190
190
  }
191
191
  }
@@ -10,7 +10,7 @@ import {
10
10
  import {
11
11
  TuiAcpClient,
12
12
  createStore
13
- } from "./chunk-KDAUUQBE.js";
13
+ } from "./chunk-KRC3RH3Y.js";
14
14
 
15
15
  // src/tui/index.ts
16
16
  import { createInterface as createInterface2 } from "readline/promises";
@@ -591,7 +591,7 @@ var SessionRunner = class {
591
591
  this.title = null;
592
592
  this.promptActive = false;
593
593
  this.agentSessionLoaded = false;
594
- this.store.setState({ contextUsage: null, plan: [] });
594
+ this.store.setState({ contextUsage: null, usageTotals: null, plan: [] });
595
595
  if (previousSessionId)
596
596
  await this.nova.closeSession({ sessionId: previousSessionId });
597
597
  this.sessionReady = this.createSession();
@@ -1351,6 +1351,17 @@ function formatElapsedTime(elapsedMs) {
1351
1351
  if (minutes > 0) return `${minutes}m ${seconds}s`;
1352
1352
  return `${seconds}s`;
1353
1353
  }
1354
+ function formatCost(amount, currency = "EUR") {
1355
+ try {
1356
+ return new Intl.NumberFormat("en-US", { style: "currency", currency, maximumFractionDigits: amount < 1 ? 4 : 2 }).format(amount);
1357
+ } catch {
1358
+ return `${amount.toFixed(4)} ${currency}`;
1359
+ }
1360
+ }
1361
+ function formatUsageTotals(totals) {
1362
+ const cost = totals.cost !== void 0 ? ` \xB7 \u2248 ${formatCost(totals.cost, totals.currency)} (from the models' prices per 1M tokens; cached tokens count as input)` : "";
1363
+ return `This session: ${formatTokenCount(totals.promptTokens)} in${totals.cachedTokens ? ` (${formatTokenCount(totals.cachedTokens)} cached)` : ""} \xB7 ${formatTokenCount(totals.completionTokens)} out${cost}`;
1364
+ }
1354
1365
  function formatTokenCount(value) {
1355
1366
  if (value >= 1e6) {
1356
1367
  const millions = value / 1e6;
@@ -1547,6 +1558,10 @@ function StatusLine({
1547
1558
  separator,
1548
1559
  usage
1549
1560
  ] }) : null,
1561
+ ui.usageTotals?.cost !== void 0 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1562
+ separator,
1563
+ `\u2248 ${formatCost(ui.usageTotals.cost, ui.usageTotals.currency)}`
1564
+ ] }) : null,
1550
1565
  ui.queuedCount ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1551
1566
  separator,
1552
1567
  `queued:${ui.queuedCount}`
@@ -1741,6 +1756,10 @@ function TranscriptBlockView({ block, frame = 0 }) {
1741
1756
  }
1742
1757
  case "assistant": {
1743
1758
  const text = block.first ? block.text.replace(/^\s+/, "") : block.text;
1759
+ if (block.thinking) {
1760
+ const lines = text.split("\n").filter((line) => line.trim()).length;
1761
+ return /* @__PURE__ */ jsx4(Prefixed, { prefix: " ", color: palette.faint, children: /* @__PURE__ */ jsx4(Text4, { color: palette.muted, italic: true, children: block.expanded ? `thinking: ${text.trim()}` : block.streaming ? "\u25B8 thinking\u2026" : `\u25B8 thinking (${lines} ${lines === 1 ? "line" : "lines"}) \xB7 Ctrl+O to show` }) });
1762
+ }
1744
1763
  return /* @__PURE__ */ jsx4(Prefixed, { prefix: block.first ? `${ASSISTANT_MARKER} ` : " ", color: palette.accent, children: /* @__PURE__ */ jsx4(Markdown, { text: text.trimEnd() }) });
1745
1764
  }
1746
1765
  case "error":
@@ -1808,7 +1827,10 @@ function ToolCallBlock({
1808
1827
  path: call.diff.path,
1809
1828
  limit: expanded ? void 0 : COLLAPSED_DIFF_LINES
1810
1829
  }
1811
- ) : expanded && !isCompactTool(call) ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: palette.muted, children: formatToolCallDetails(call) }) }) : null
1830
+ ) : expanded && !isCompactTool(call) ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: palette.muted, children: formatToolCallDetails(call) }) }) : call.status === "completed" && outputLines(call.output).length ? (
1831
+ // Collapsed output says it is there, like collapsed thinking.
1832
+ /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: palette.faint, children: `\u25B8 ${outputLines(call.output).length} ${outputLines(call.output).length === 1 ? "line" : "lines"} of output \xB7 ${isCompactTool(call) ? "Ctrl+T to inspect" : "Ctrl+O to show"}` }) })
1833
+ ) : null
1812
1834
  ] });
1813
1835
  }
1814
1836
  function ActivityBlock({
@@ -3770,7 +3792,7 @@ var TranscriptCommitter = class {
3770
3792
  );
3771
3793
  break;
3772
3794
  case "assistant":
3773
- this.emitAssistant(message, committing, emit);
3795
+ this.emitAssistant(message, committing, emit, expanded);
3774
3796
  if (message.streaming) committing = false;
3775
3797
  break;
3776
3798
  }
@@ -3782,14 +3804,22 @@ var TranscriptCommitter = class {
3782
3804
  this.snapshot = { epoch: this.epoch, committed: this.committedView, live };
3783
3805
  return this.snapshot;
3784
3806
  }
3785
- emitAssistant(message, committing, emit) {
3807
+ emitAssistant(message, committing, emit, expanded) {
3808
+ if (message.thinking) {
3809
+ const block2 = { key: message.id, kind: "assistant", text: message.text, first: true, streaming: message.streaming, thinking: true, expanded };
3810
+ if (message.streaming) emit(block2, false, []);
3811
+ else if (message.text.trim()) emit(block2, true, [message.id]);
3812
+ else if (committing) this.committedIds.push(message.id);
3813
+ return;
3814
+ }
3786
3815
  let offset = this.partial?.id === message.id ? this.partial.offset : 0;
3787
3816
  const block = (text, streaming) => ({
3788
3817
  key: `${message.id}:${offset}`,
3789
3818
  kind: "assistant",
3790
3819
  text,
3791
3820
  first: offset === 0,
3792
- streaming
3821
+ streaming,
3822
+ ...message.thinking ? { thinking: true } : {}
3793
3823
  });
3794
3824
  if (!message.streaming) {
3795
3825
  const rest2 = message.text.slice(offset);
@@ -4269,7 +4299,8 @@ ${skill.path}`
4269
4299
  this.showText("Context usage", "Calculating context usage...");
4270
4300
  try {
4271
4301
  const usage = await this.runner.refreshContextUsage();
4272
- this.showText("Context usage", formatContextUsage(usage));
4302
+ const totals = this.store.getState().usageTotals;
4303
+ this.showText("Context usage", [formatContextUsage(usage), ...totals ? [formatUsageTotals(totals)] : []].join("\n\n"));
4273
4304
  } catch (error) {
4274
4305
  this.showText("Context usage", errorMessage(error, "Failed to calculate context usage."));
4275
4306
  }
@@ -4297,7 +4328,7 @@ ${skill.path}`
4297
4328
  toggleToolDetails() {
4298
4329
  this.expandedTools = !this.expandedTools;
4299
4330
  this.store.setState({
4300
- statusLine: this.expandedTools ? "Verbose tool details expanded; read/search stay compact." : "Verbose tool details collapsed."
4331
+ statusLine: this.expandedTools ? "Thinking and verbose tool details expanded; read/search stay compact." : "Thinking and verbose tool details collapsed."
4301
4332
  });
4302
4333
  }
4303
4334
  toolCalls() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.10",
3
+ "version": "1.2.0-alpha.12",
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.7",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.9",
43
43
  "@datalabrotterdam/nova-ai-browser-host": "^1.0.0",
44
44
  "chalk": "^5.3.0",
45
45
  "cli-highlight": "^2.1.11",