@datalabrotterdam/nova-ai-cli 1.2.0-alpha.12 → 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,10 @@
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
+
1
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)
2
9
 
3
10
 
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;
@@ -102,10 +104,11 @@ row. To select text, hold Shift (Option in some macOS terminals) while dragging.
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
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` (context, and the session's tokens and estimated cost; the status line shows `≈ €0.0123` once a priced model answered),
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
package/dist/index.js CHANGED
@@ -185,7 +185,7 @@ ${MAIN_HELP}
185
185
  );
186
186
  return 2;
187
187
  }
188
- const { default: runChat } = await import("./tui-RR2LCTL7.js");
188
+ const { default: runChat } = await import("./tui-NBVDJQIA.js");
189
189
  return runChat(options);
190
190
  }
191
191
  }
@@ -1554,6 +1554,10 @@ function StatusLine({
1554
1554
  ui.permissionMode,
1555
1555
  separator,
1556
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,
1557
1561
  usage ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1558
1562
  separator,
1559
1563
  usage
@@ -2092,6 +2096,13 @@ var builtinCommands = [
2092
2096
  ctx.print(await ctx.switchSkill(name, action === "on", global));
2093
2097
  }
2094
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
+ },
2095
2106
  {
2096
2107
  name: "usage",
2097
2108
  description: "Inspect estimated context usage by category",
@@ -2386,6 +2397,35 @@ const png = bitmap.representationUsingTypeProperties($.NSBitmapImageFileTypePNG,
2386
2397
  console.log(ObjC.unwrap(png.base64EncodedStringWithOptions(0)));
2387
2398
  `;
2388
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
+
2389
2429
  // src/tui/files/find-fd.ts
2390
2430
  import { spawnSync as spawnSync2 } from "child_process";
2391
2431
  var FD_CANDIDATES = ["fd", "fdfind"];
@@ -2454,7 +2494,7 @@ var DraftImageAttachments = class {
2454
2494
  };
2455
2495
 
2456
2496
  // src/tui/files/workspace-autocomplete.ts
2457
- import { spawn } from "child_process";
2497
+ import { spawn as spawn2 } from "child_process";
2458
2498
  import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2459
2499
  import { homedir } from "os";
2460
2500
  import { basename, dirname as dirname2, join as join3 } from "path";
@@ -2665,7 +2705,7 @@ function walkWithFd(cwd, fdPath, query, signal) {
2665
2705
  resolve2([]);
2666
2706
  return;
2667
2707
  }
2668
- const child = spawn(fdPath, args, { stdio: ["ignore", "pipe", "ignore"] });
2708
+ const child = spawn2(fdPath, args, { stdio: ["ignore", "pipe", "ignore"] });
2669
2709
  let stdout = "";
2670
2710
  let settled = false;
2671
2711
  const finish = (entries) => {
@@ -4051,6 +4091,9 @@ var TuiController = class {
4051
4091
  case "o":
4052
4092
  this.toggleToolDetails();
4053
4093
  return true;
4094
+ case "s":
4095
+ this.toggleSelectMode();
4096
+ return true;
4054
4097
  case "t":
4055
4098
  this.openToolInspector();
4056
4099
  return true;
@@ -4325,6 +4368,27 @@ ${skill.path}`
4325
4368
  this.scroll = next;
4326
4369
  this.refresh();
4327
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
+ }
4328
4392
  toggleToolDetails() {
4329
4393
  this.expandedTools = !this.expandedTools;
4330
4394
  this.store.setState({
@@ -4397,6 +4461,7 @@ ${skill.path}`
4397
4461
  setModel: (model) => this.setModel(model),
4398
4462
  getInteractionMode: () => this.runner.interactionMode,
4399
4463
  getInteractionModes: () => this.runner.modeIds,
4464
+ copyLastAnswer: () => this.copyLastAnswer(),
4400
4465
  agentCommands: () => this.agentCommands(),
4401
4466
  setInteractionMode: (mode) => this.setInteractionMode(mode),
4402
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.12",
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",