@datalabrotterdam/nova-ai-cli 1.2.0-alpha.11 → 1.2.0-alpha.13
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.13](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.12...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.13) (2026-10-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **cli:** Ctrl+S select mode and /copy ([bcb7e02](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/bcb7e02f05739caae7a6155d2c37b7d2e54d01fa))
|
|
7
|
+
|
|
8
|
+
# [@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)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **cli:** show the model's thinking as one block ([4479cac](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/4479cac70bf1f9973401bdfd420c401e00c11099))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **cli:** collapse the model's thinking by default ([28f04c1](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/28f04c1c5141aac2468c0aa8ba86dc38d885bb4e))
|
|
19
|
+
* **cli:** say when a collapsed tool row has output ([51274a5](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/51274a597700cf3208d48d54c2a32262e8947a96))
|
|
20
|
+
* token and cost totals per session ([8e9aef8](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/8e9aef81b6345832f38d8a2639c35d4cb6fb3b47))
|
|
21
|
+
|
|
1
22
|
# [@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)
|
|
2
23
|
|
|
3
24
|
|
package/README.md
CHANGED
|
@@ -75,7 +75,9 @@ The UI runs full screen (the terminal's alternate screen, like `less` or
|
|
|
75
75
|
`vim`) and gives your shell its screen back on exit. The mouse wheel or
|
|
76
76
|
PgUp/PgDn scroll the transcript, Esc jumps back to the newest output, and
|
|
77
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
|
|
78
|
+
row. To select text, hold Shift (Option in some macOS terminals) while dragging,
|
|
79
|
+
or press Ctrl+S to turn the mouse over to your terminal (select mode; Ctrl+S
|
|
80
|
+
again gives the wheel back). `/copy` copies the last answer.
|
|
79
81
|
|
|
80
82
|
- **Modes** (Shift+Tab): `agent` does the work; `plan` reads the code, asks
|
|
81
83
|
questions and builds a task list but changes nothing; `ask` only answers;
|
|
@@ -101,11 +103,12 @@ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
|
|
|
101
103
|
| Ctrl+C twice | Exit; prints `nova-ai --resume <id>` |
|
|
102
104
|
| Shift+Tab | Cycle agent / ask / plan / autopilot and your own agents |
|
|
103
105
|
| Ctrl+R / Ctrl+P / Ctrl+K | Sessions / models / permission mode |
|
|
104
|
-
| Ctrl+O / Ctrl+T / Ctrl+B |
|
|
106
|
+
| Ctrl+O / Ctrl+T / Ctrl+B | Thinking and verbose tool output / tool inspector / background jobs |
|
|
107
|
+
| Ctrl+S | Select mode: the mouse selects text instead of scrolling (again to switch back) |
|
|
105
108
|
| Alt+V | Paste a clipboard image |
|
|
106
109
|
|
|
107
110
|
Slash commands: `/help`, `/model`, `/effort` (thinking models), `/mode`, `/permission`, `/session`,
|
|
108
|
-
`/resume`, `/clear`, `/compact`, `/rewind [n]`, `/queue`, `/steer`, `/usage
|
|
111
|
+
`/resume`, `/clear`, `/compact`, `/copy`, `/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
112
|
`/tools`, `/mcp`, `/skills`, `/shell` and `/agent` (background jobs), `/trust`
|
|
110
113
|
and `/exit`. Your own commands are markdown files in `~/.nova-ai/commands/` or
|
|
111
114
|
the repository's `.nova-ai/commands/` (Claude Code's format, so
|
|
@@ -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
|
|
314
|
-
this.
|
|
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) => ({
|
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-
|
|
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-
|
|
188
|
+
const { default: runChat } = await import("./tui-NBVDJQIA.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-
|
|
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;
|
|
@@ -1543,10 +1554,18 @@ function StatusLine({
|
|
|
1543
1554
|
ui.permissionMode,
|
|
1544
1555
|
separator,
|
|
1545
1556
|
ui.busy ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: "working" }) : /* @__PURE__ */ jsx2(Text2, { color: palette.success, children: "ready" }),
|
|
1557
|
+
ui.selectMode ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1558
|
+
separator,
|
|
1559
|
+
/* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: "select \xB7 Ctrl+S" })
|
|
1560
|
+
] }) : null,
|
|
1546
1561
|
usage ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1547
1562
|
separator,
|
|
1548
1563
|
usage
|
|
1549
1564
|
] }) : null,
|
|
1565
|
+
ui.usageTotals?.cost !== void 0 ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1566
|
+
separator,
|
|
1567
|
+
`\u2248 ${formatCost(ui.usageTotals.cost, ui.usageTotals.currency)}`
|
|
1568
|
+
] }) : null,
|
|
1550
1569
|
ui.queuedCount ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1551
1570
|
separator,
|
|
1552
1571
|
`queued:${ui.queuedCount}`
|
|
@@ -1741,6 +1760,10 @@ function TranscriptBlockView({ block, frame = 0 }) {
|
|
|
1741
1760
|
}
|
|
1742
1761
|
case "assistant": {
|
|
1743
1762
|
const text = block.first ? block.text.replace(/^\s+/, "") : block.text;
|
|
1763
|
+
if (block.thinking) {
|
|
1764
|
+
const lines = text.split("\n").filter((line) => line.trim()).length;
|
|
1765
|
+
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` }) });
|
|
1766
|
+
}
|
|
1744
1767
|
return /* @__PURE__ */ jsx4(Prefixed, { prefix: block.first ? `${ASSISTANT_MARKER} ` : " ", color: palette.accent, children: /* @__PURE__ */ jsx4(Markdown, { text: text.trimEnd() }) });
|
|
1745
1768
|
}
|
|
1746
1769
|
case "error":
|
|
@@ -1808,7 +1831,10 @@ function ToolCallBlock({
|
|
|
1808
1831
|
path: call.diff.path,
|
|
1809
1832
|
limit: expanded ? void 0 : COLLAPSED_DIFF_LINES
|
|
1810
1833
|
}
|
|
1811
|
-
) : expanded && !isCompactTool(call) ? /* @__PURE__ */ jsx4(Box4, { paddingLeft: 2, children: /* @__PURE__ */ jsx4(Text4, { color: palette.muted, children: formatToolCallDetails(call) }) }) :
|
|
1834
|
+
) : 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 ? (
|
|
1835
|
+
// Collapsed output says it is there, like collapsed thinking.
|
|
1836
|
+
/* @__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"}` }) })
|
|
1837
|
+
) : null
|
|
1812
1838
|
] });
|
|
1813
1839
|
}
|
|
1814
1840
|
function ActivityBlock({
|
|
@@ -2070,6 +2096,13 @@ var builtinCommands = [
|
|
|
2070
2096
|
ctx.print(await ctx.switchSkill(name, action === "on", global));
|
|
2071
2097
|
}
|
|
2072
2098
|
},
|
|
2099
|
+
{
|
|
2100
|
+
name: "copy",
|
|
2101
|
+
description: "Copy the last answer to the clipboard (Ctrl+S selects text with the mouse)",
|
|
2102
|
+
async run(ctx) {
|
|
2103
|
+
ctx.print(await ctx.copyLastAnswer());
|
|
2104
|
+
}
|
|
2105
|
+
},
|
|
2073
2106
|
{
|
|
2074
2107
|
name: "usage",
|
|
2075
2108
|
description: "Inspect estimated context usage by category",
|
|
@@ -2364,6 +2397,35 @@ const png = bitmap.representationUsingTypeProperties($.NSBitmapImageFileTypePNG,
|
|
|
2364
2397
|
console.log(ObjC.unwrap(png.base64EncodedStringWithOptions(0)));
|
|
2365
2398
|
`;
|
|
2366
2399
|
|
|
2400
|
+
// src/tui/files/clipboard-text.ts
|
|
2401
|
+
import { spawn } from "child_process";
|
|
2402
|
+
async function copyText(text, write, tools = systemTools()) {
|
|
2403
|
+
write(`\x1B]52;c;${Buffer.from(text, "utf8").toString("base64")}\x07`);
|
|
2404
|
+
for (const [command, args] of tools) {
|
|
2405
|
+
if (await pipeTo(command, args, text)) return command;
|
|
2406
|
+
}
|
|
2407
|
+
return null;
|
|
2408
|
+
}
|
|
2409
|
+
function systemTools() {
|
|
2410
|
+
if (process.platform === "darwin") return [["pbcopy", []]];
|
|
2411
|
+
if (process.platform === "win32") return [["clip", []]];
|
|
2412
|
+
return [["wl-copy", []], ["xclip", ["-selection", "clipboard"]]];
|
|
2413
|
+
}
|
|
2414
|
+
function pipeTo(command, args, text) {
|
|
2415
|
+
return new Promise((resolve2) => {
|
|
2416
|
+
let child;
|
|
2417
|
+
try {
|
|
2418
|
+
child = spawn(command, args, { stdio: ["pipe", "ignore", "ignore"] });
|
|
2419
|
+
} catch {
|
|
2420
|
+
resolve2(false);
|
|
2421
|
+
return;
|
|
2422
|
+
}
|
|
2423
|
+
child.on("error", () => resolve2(false));
|
|
2424
|
+
child.on("close", (code) => resolve2(code === 0));
|
|
2425
|
+
child.stdin.end(text);
|
|
2426
|
+
});
|
|
2427
|
+
}
|
|
2428
|
+
|
|
2367
2429
|
// src/tui/files/find-fd.ts
|
|
2368
2430
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2369
2431
|
var FD_CANDIDATES = ["fd", "fdfind"];
|
|
@@ -2432,7 +2494,7 @@ var DraftImageAttachments = class {
|
|
|
2432
2494
|
};
|
|
2433
2495
|
|
|
2434
2496
|
// src/tui/files/workspace-autocomplete.ts
|
|
2435
|
-
import { spawn } from "child_process";
|
|
2497
|
+
import { spawn as spawn2 } from "child_process";
|
|
2436
2498
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2437
2499
|
import { homedir } from "os";
|
|
2438
2500
|
import { basename, dirname as dirname2, join as join3 } from "path";
|
|
@@ -2643,7 +2705,7 @@ function walkWithFd(cwd, fdPath, query, signal) {
|
|
|
2643
2705
|
resolve2([]);
|
|
2644
2706
|
return;
|
|
2645
2707
|
}
|
|
2646
|
-
const child =
|
|
2708
|
+
const child = spawn2(fdPath, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
2647
2709
|
let stdout = "";
|
|
2648
2710
|
let settled = false;
|
|
2649
2711
|
const finish = (entries) => {
|
|
@@ -3770,7 +3832,7 @@ var TranscriptCommitter = class {
|
|
|
3770
3832
|
);
|
|
3771
3833
|
break;
|
|
3772
3834
|
case "assistant":
|
|
3773
|
-
this.emitAssistant(message, committing, emit);
|
|
3835
|
+
this.emitAssistant(message, committing, emit, expanded);
|
|
3774
3836
|
if (message.streaming) committing = false;
|
|
3775
3837
|
break;
|
|
3776
3838
|
}
|
|
@@ -3782,14 +3844,22 @@ var TranscriptCommitter = class {
|
|
|
3782
3844
|
this.snapshot = { epoch: this.epoch, committed: this.committedView, live };
|
|
3783
3845
|
return this.snapshot;
|
|
3784
3846
|
}
|
|
3785
|
-
emitAssistant(message, committing, emit) {
|
|
3847
|
+
emitAssistant(message, committing, emit, expanded) {
|
|
3848
|
+
if (message.thinking) {
|
|
3849
|
+
const block2 = { key: message.id, kind: "assistant", text: message.text, first: true, streaming: message.streaming, thinking: true, expanded };
|
|
3850
|
+
if (message.streaming) emit(block2, false, []);
|
|
3851
|
+
else if (message.text.trim()) emit(block2, true, [message.id]);
|
|
3852
|
+
else if (committing) this.committedIds.push(message.id);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3786
3855
|
let offset = this.partial?.id === message.id ? this.partial.offset : 0;
|
|
3787
3856
|
const block = (text, streaming) => ({
|
|
3788
3857
|
key: `${message.id}:${offset}`,
|
|
3789
3858
|
kind: "assistant",
|
|
3790
3859
|
text,
|
|
3791
3860
|
first: offset === 0,
|
|
3792
|
-
streaming
|
|
3861
|
+
streaming,
|
|
3862
|
+
...message.thinking ? { thinking: true } : {}
|
|
3793
3863
|
});
|
|
3794
3864
|
if (!message.streaming) {
|
|
3795
3865
|
const rest2 = message.text.slice(offset);
|
|
@@ -4021,6 +4091,9 @@ var TuiController = class {
|
|
|
4021
4091
|
case "o":
|
|
4022
4092
|
this.toggleToolDetails();
|
|
4023
4093
|
return true;
|
|
4094
|
+
case "s":
|
|
4095
|
+
this.toggleSelectMode();
|
|
4096
|
+
return true;
|
|
4024
4097
|
case "t":
|
|
4025
4098
|
this.openToolInspector();
|
|
4026
4099
|
return true;
|
|
@@ -4269,7 +4342,8 @@ ${skill.path}`
|
|
|
4269
4342
|
this.showText("Context usage", "Calculating context usage...");
|
|
4270
4343
|
try {
|
|
4271
4344
|
const usage = await this.runner.refreshContextUsage();
|
|
4272
|
-
this.
|
|
4345
|
+
const totals = this.store.getState().usageTotals;
|
|
4346
|
+
this.showText("Context usage", [formatContextUsage(usage), ...totals ? [formatUsageTotals(totals)] : []].join("\n\n"));
|
|
4273
4347
|
} catch (error) {
|
|
4274
4348
|
this.showText("Context usage", errorMessage(error, "Failed to calculate context usage."));
|
|
4275
4349
|
}
|
|
@@ -4294,10 +4368,31 @@ ${skill.path}`
|
|
|
4294
4368
|
this.scroll = next;
|
|
4295
4369
|
this.refresh();
|
|
4296
4370
|
}
|
|
4371
|
+
/**
|
|
4372
|
+
* Select mode: mouse reporting off, so the terminal selects and copies text
|
|
4373
|
+
* natively (the wheel no longer scrolls the transcript; PgUp/PgDn still do).
|
|
4374
|
+
* For terminals where Shift/Option + drag does not get past mouse reporting.
|
|
4375
|
+
*/
|
|
4376
|
+
toggleSelectMode() {
|
|
4377
|
+
const selectMode = !this.store.getState().selectMode;
|
|
4378
|
+
this.io.write(selectMode ? MOUSE_REPORTING_OFF : MOUSE_REPORTING_ON);
|
|
4379
|
+
this.store.setState({
|
|
4380
|
+
selectMode,
|
|
4381
|
+
statusLine: selectMode ? "Select mode: drag to select text, your terminal copies it. PgUp/PgDn scroll; Ctrl+S turns the mouse wheel back on." : "Mouse wheel scrolls the transcript again. Ctrl+S to select text."
|
|
4382
|
+
});
|
|
4383
|
+
}
|
|
4384
|
+
/** /copy: the last answer (not its thinking) to the clipboard. */
|
|
4385
|
+
async copyLastAnswer() {
|
|
4386
|
+
const answer = [...this.store.getState().messages].reverse().find((message) => message.role === "assistant" && !message.thinking && message.text.trim());
|
|
4387
|
+
if (answer?.role !== "assistant") return "There is no answer to copy yet.";
|
|
4388
|
+
const tool = await copyText(answer.text.trim(), (data) => this.io.write(data));
|
|
4389
|
+
const size = `${answer.text.trim().length} characters`;
|
|
4390
|
+
return tool ? `Copied the last answer (${size}).` : `Sent the last answer (${size}) to the terminal's clipboard; if nothing was copied, use Ctrl+S to select it.`;
|
|
4391
|
+
}
|
|
4297
4392
|
toggleToolDetails() {
|
|
4298
4393
|
this.expandedTools = !this.expandedTools;
|
|
4299
4394
|
this.store.setState({
|
|
4300
|
-
statusLine: this.expandedTools ? "
|
|
4395
|
+
statusLine: this.expandedTools ? "Thinking and verbose tool details expanded; read/search stay compact." : "Thinking and verbose tool details collapsed."
|
|
4301
4396
|
});
|
|
4302
4397
|
}
|
|
4303
4398
|
toolCalls() {
|
|
@@ -4366,6 +4461,7 @@ ${skill.path}`
|
|
|
4366
4461
|
setModel: (model) => this.setModel(model),
|
|
4367
4462
|
getInteractionMode: () => this.runner.interactionMode,
|
|
4368
4463
|
getInteractionModes: () => this.runner.modeIds,
|
|
4464
|
+
copyLastAnswer: () => this.copyLastAnswer(),
|
|
4369
4465
|
agentCommands: () => this.agentCommands(),
|
|
4370
4466
|
setInteractionMode: (mode) => this.setInteractionMode(mode),
|
|
4371
4467
|
getPermissionMode: () => this.store.getState().permissionMode,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@datalabrotterdam/nova-ai-cli",
|
|
3
|
-
"version": "1.2.0-alpha.
|
|
3
|
+
"version": "1.2.0-alpha.13",
|
|
4
4
|
"description": "Agentic coding with DataLab Rotterdam's Nova AI in your terminal: interactive TUI, headless mode, and an ACP agent for editors",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "DataLab Rotterdam",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@agentclientprotocol/sdk": "^0.29.0",
|
|
42
|
-
"@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.
|
|
42
|
+
"@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.9",
|
|
43
43
|
"@datalabrotterdam/nova-ai-browser-host": "^1.0.0",
|
|
44
44
|
"chalk": "^5.3.0",
|
|
45
45
|
"cli-highlight": "^2.1.11",
|