@datalabrotterdam/nova-ai-cli 1.2.0-alpha.13 → 1.2.0-alpha.15

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,23 @@
1
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.15](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.14...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.15) (2026-10-25)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **cli:** Markdown rules span the row in a streamed block of their own ([7ab846c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/7ab846c86ff83ce07046f8c366dbab8d019b17b8))
7
+
8
+
9
+ ### Features
10
+
11
+ * **cli:** compact permission dialog with a details view ([63e0d97](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/63e0d9740152b8e2874ecde62b2ba2dd88899423))
12
+ * **cli:** drag to select text, click to copy; paste markers delete as one unit ([160238e](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/160238eb8b02cfcb209bc92ee81a7c508932e4ba)), closes [#ImageN](https://github.com/Datalab-Rotterdam/nova-ai-cli/issues/ImageN)
13
+
14
+ # [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.14](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.13...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.14) (2026-10-24)
15
+
16
+
17
+ ### Features
18
+
19
+ * GitHub Action and GitLab template for nova-ai in CI ([2352e97](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/2352e97327ae3e3c65be238bb2d3134db13abeb3))
20
+
1
21
  # [@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
22
 
3
23
 
package/README.md CHANGED
@@ -139,6 +139,10 @@ nova-ai -p -c "and now add a changelog entry"
139
139
  Exit codes: `0` success, `1` configuration or runtime error, `2` cancelled,
140
140
  `3` tool-turn safety limit, `130` interrupted.
141
141
 
142
+ In CI: a GitHub Action (`uses: Datalab-Rotterdam/nova-ai-cli@main`) and a GitLab
143
+ template review pull requests and post the answer as a comment; see
144
+ [docs/CI.md](https://github.com/Datalab-Rotterdam/nova-ai-cli/blob/main/docs/CI.md).
145
+
142
146
  ## Editors (ACP)
143
147
 
144
148
  Configure `nova-ai --acp` as a custom agent. For Zed:
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-NBVDJQIA.js");
188
+ const { default: runChat } = await import("./tui-W6ZQ2SWR.js");
189
189
  return runChat(options);
190
190
  }
191
191
  }
@@ -1183,18 +1183,137 @@ function kittyKeyboardOverride(env = process.env) {
1183
1183
  function isLateTerminalReply(input) {
1184
1184
  return /^\u001b?\[\?[\d;]*[uc]$/.test(input);
1185
1185
  }
1186
- var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1006h";
1187
- var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1006l";
1188
- var MOUSE_INPUT = /^\u001b?\[<(\d+);\d+;\d+[Mm]$/;
1186
+ var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1002h\x1B[?1006h";
1187
+ var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1006l";
1188
+ var MOUSE_INPUT = /^\u001b?\[<(\d+);(\d+);(\d+)([Mm])$/;
1189
1189
  function isMouseInput(input) {
1190
1190
  return MOUSE_INPUT.test(input);
1191
1191
  }
1192
+ function parseMouse(input) {
1193
+ const match = MOUSE_INPUT.exec(input);
1194
+ if (!match) return null;
1195
+ return {
1196
+ button: Number(match[1]),
1197
+ col: Number(match[2]) - 1,
1198
+ row: Number(match[3]) - 1,
1199
+ release: match[4] === "m"
1200
+ };
1201
+ }
1192
1202
  function wheelRows(input) {
1193
1203
  const button = Number(MOUSE_INPUT.exec(input)?.[1] ?? -1);
1194
1204
  if (button < 0 || (button & 64) === 0) return 0;
1195
1205
  return button & 1 ? -3 : 3;
1196
1206
  }
1197
1207
 
1208
+ // src/tui/ink/screen-selection.ts
1209
+ import { stripVTControlCharacters } from "util";
1210
+ import stringWidth from "string-width";
1211
+ var CONTROL_RUN = "(?:\\u001b\\[[\\d;?]*[A-LN-Za-ln-z]|\\u001b[78c])*";
1212
+ var LEADING_CONTROLS = new RegExp(`^${CONTROL_RUN}`);
1213
+ var TRAILING_CONTROLS = new RegExp(`${CONTROL_RUN}$`);
1214
+ var graphemes = new Intl.Segmenter();
1215
+ var ScreenSelection = class {
1216
+ constructor(out) {
1217
+ this.out = out;
1218
+ }
1219
+ out;
1220
+ rawLines = [];
1221
+ plainLines = [];
1222
+ anchor = null;
1223
+ focus = { row: 0, col: 0 };
1224
+ /** The stream to hand Ink: writes go through, frames are kept and painted over. */
1225
+ wrap(stdout) {
1226
+ return new Proxy(stdout, {
1227
+ get: (target, key) => {
1228
+ if (key === "write") return this.write(target);
1229
+ const value = Reflect.get(target, key, target);
1230
+ return typeof value === "function" ? value.bind(target) : value;
1231
+ }
1232
+ });
1233
+ }
1234
+ get active() {
1235
+ return this.anchor !== null;
1236
+ }
1237
+ set(anchor, focus) {
1238
+ this.repaint(() => {
1239
+ this.anchor = anchor;
1240
+ this.focus = focus;
1241
+ });
1242
+ }
1243
+ /** Shift+arrows: moves the selection's moving end, wrapping at the row edges. */
1244
+ extend(rows, cols) {
1245
+ if (!this.anchor) return;
1246
+ const width = this.out.columns ?? 80;
1247
+ let { row, col } = this.focus;
1248
+ col += cols;
1249
+ if (col < 0) [row, col] = [row - 1, width - 1];
1250
+ if (col >= width) [row, col] = [row + 1, 0];
1251
+ row = Math.max(0, Math.min(row + rows, this.plainLines.length - 1));
1252
+ this.set(this.anchor, { row, col });
1253
+ }
1254
+ clear() {
1255
+ if (this.anchor) this.repaint(() => this.anchor = null);
1256
+ }
1257
+ text() {
1258
+ return this.segments().map(({ text }) => text.trimEnd()).join("\n");
1259
+ }
1260
+ write(target) {
1261
+ return (chunk, ...rest) => {
1262
+ const result = target.write.call(target, chunk, ...rest);
1263
+ if (typeof chunk === "string" && this.capture(chunk) && this.anchor) this.out.write(this.overlay());
1264
+ return result;
1265
+ };
1266
+ }
1267
+ capture(chunk) {
1268
+ const frame = chunk.replace(LEADING_CONTROLS, "").replace(TRAILING_CONTROLS, "");
1269
+ const plain = stripVTControlCharacters(frame);
1270
+ if (!plain.trim()) return false;
1271
+ this.rawLines = frame.split("\n");
1272
+ this.plainLines = plain.split("\n");
1273
+ return true;
1274
+ }
1275
+ /** Restores the rows under the old selection, then paints the new one. */
1276
+ repaint(change) {
1277
+ const restore = this.segments().map(({ row }) => `\x1B[${row + 1};1H\x1B[0m${this.rawLines[row] ?? ""}\x1B[0m`).join("");
1278
+ change();
1279
+ this.out.write(`\x1B7${restore}${this.overlay(false)}\x1B8`);
1280
+ }
1281
+ overlay(saveCursor = true) {
1282
+ const paint = this.segments().filter(({ text }) => text).map(({ row, col, text }) => `\x1B[${row + 1};${col + 1}H\x1B[7m${text}\x1B[27m`).join("");
1283
+ return saveCursor ? `\x1B7${paint}\x1B8` : paint;
1284
+ }
1285
+ /** Each selected row's text and the cell it starts at, top to bottom. */
1286
+ segments() {
1287
+ if (!this.anchor) return [];
1288
+ const [start, end] = before(this.anchor, this.focus) ? [this.anchor, this.focus] : [this.focus, this.anchor];
1289
+ const segments = [];
1290
+ for (let row = start.row; row <= Math.min(end.row, this.plainLines.length - 1); row++) {
1291
+ const from = row === start.row ? start.col : 0;
1292
+ const to = row === end.row ? end.col : Infinity;
1293
+ segments.push({ row, ...sliceCells(this.plainLines[row] ?? "", from, to) });
1294
+ }
1295
+ return segments;
1296
+ }
1297
+ };
1298
+ function before(a, b) {
1299
+ return a.row < b.row || a.row === b.row && a.col <= b.col;
1300
+ }
1301
+ function sliceCells(line, from, to) {
1302
+ let col = 0;
1303
+ let start = -1;
1304
+ let text = "";
1305
+ for (const { segment } of graphemes.segment(line)) {
1306
+ if (col > to) break;
1307
+ const width = stringWidth(segment);
1308
+ if (col + Math.max(width, 1) - 1 >= from) {
1309
+ if (start < 0) start = col;
1310
+ text += segment;
1311
+ }
1312
+ col += width;
1313
+ }
1314
+ return { col: start < 0 ? from : start, text };
1315
+ }
1316
+
1198
1317
  // src/tui/ink/components/prompt-input.tsx
1199
1318
  import { Box, Text, useInput, usePaste } from "ink";
1200
1319
  import { useSyncExternalStore } from "react";
@@ -1292,13 +1411,13 @@ function Suggestions({ view }) {
1292
1411
  ] });
1293
1412
  }
1294
1413
  function withCursor(line, col) {
1295
- const before = line.slice(0, col);
1414
+ const before2 = line.slice(0, col);
1296
1415
  const rest = line.slice(col);
1297
1416
  const first = segmenter.segment(rest)[Symbol.iterator]().next();
1298
1417
  const current = first.done ? " " : first.value.segment;
1299
1418
  const after = first.done ? "" : rest.slice(current.length);
1300
1419
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1301
- before,
1420
+ before2,
1302
1421
  /* @__PURE__ */ jsx(Text, { inverse: true, children: current }),
1303
1422
  after
1304
1423
  ] });
@@ -1554,10 +1673,6 @@ function StatusLine({
1554
1673
  ui.permissionMode,
1555
1674
  separator,
1556
1675
  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,
1561
1676
  usage ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
1562
1677
  separator,
1563
1678
  usage
@@ -1594,7 +1709,7 @@ import { Box as Box4, Text as Text4 } from "ink";
1594
1709
  import chalk2 from "chalk";
1595
1710
  import { Box as Box3, Text as Text3 } from "ink";
1596
1711
  import { marked } from "marked";
1597
- import stringWidth from "string-width";
1712
+ import stringWidth2 from "string-width";
1598
1713
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
1599
1714
  function Markdown({ text }) {
1600
1715
  const tokens = marked.lexer(text, { gfm: true });
@@ -1679,12 +1794,12 @@ function Table({ table }) {
1679
1794
  const rows = table.rows.map((row) => row.map((cell) => inline(cell.tokens)));
1680
1795
  const widths = header.map(
1681
1796
  (cell, column) => Math.max(
1682
- stringWidth(cell),
1683
- ...rows.map((row) => stringWidth(row[column] ?? ""))
1797
+ stringWidth2(cell),
1798
+ ...rows.map((row) => stringWidth2(row[column] ?? ""))
1684
1799
  )
1685
1800
  );
1686
1801
  const pad = (value, column) => {
1687
- const space = Math.max(0, widths[column] - stringWidth(value));
1802
+ const space = Math.max(0, widths[column] - stringWidth2(value));
1688
1803
  const align = table.align[column];
1689
1804
  if (align === "right") return " ".repeat(space) + value;
1690
1805
  if (align === "center") {
@@ -1794,7 +1909,7 @@ function Prefixed({
1794
1909
  color,
1795
1910
  children
1796
1911
  }) {
1797
- return /* @__PURE__ */ jsxs4(Box4, { paddingLeft: 1, children: [
1912
+ return /* @__PURE__ */ jsxs4(Box4, { paddingLeft: 1, flexGrow: 1, children: [
1798
1913
  /* @__PURE__ */ jsx4(Box4, { flexShrink: 0, children: /* @__PURE__ */ jsx4(Text4, { color, children: prefix }) }),
1799
1914
  /* @__PURE__ */ jsx4(Box4, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children })
1800
1915
  ] });
@@ -2098,7 +2213,7 @@ var builtinCommands = [
2098
2213
  },
2099
2214
  {
2100
2215
  name: "copy",
2101
- description: "Copy the last answer to the clipboard (Ctrl+S selects text with the mouse)",
2216
+ description: "Copy the last answer to the clipboard (or drag over text with the mouse, then click)",
2102
2217
  async run(ctx) {
2103
2218
  ctx.print(await ctx.copyLastAnswer());
2104
2219
  }
@@ -2513,23 +2628,23 @@ var WorkspaceAutocompleteProvider = class {
2513
2628
  fdPath;
2514
2629
  triggerCharacters = ["@", "/"];
2515
2630
  async getSuggestions(lines, cursorLine, cursorCol, options) {
2516
- const before = (lines[cursorLine] ?? "").slice(0, cursorCol);
2517
- const mention = extractMentionPrefix(before);
2631
+ const before2 = (lines[cursorLine] ?? "").slice(0, cursorCol);
2632
+ const mention = extractMentionPrefix(before2);
2518
2633
  if (mention) return this.mentionSuggestions(mention, options.signal);
2519
- if (cursorLine === 0 && before.startsWith("/") && !/\s/.test(before)) {
2634
+ if (cursorLine === 0 && before2.startsWith("/") && !/\s/.test(before2)) {
2520
2635
  const commands = typeof this.commands === "function" ? this.commands() : this.commands;
2521
- const items2 = filterCommands(commands, before.slice(1));
2522
- return items2.length ? { items: items2, prefix: before } : null;
2636
+ const items2 = filterCommands(commands, before2.slice(1));
2637
+ return items2.length ? { items: items2, prefix: before2 } : null;
2523
2638
  }
2524
2639
  if (!options.force) return null;
2525
- const pathPrefix = extractPathPrefix(before);
2640
+ const pathPrefix = extractPathPrefix(before2);
2526
2641
  if (pathPrefix === null) return null;
2527
2642
  const items = this.pathSuggestions(pathPrefix);
2528
2643
  return items.length ? { items, prefix: pathPrefix } : null;
2529
2644
  }
2530
2645
  shouldTriggerFileCompletion(lines, cursorLine, cursorCol) {
2531
- const before = (lines[cursorLine] ?? "").slice(0, cursorCol).trim();
2532
- return !(before.startsWith("/") && !before.includes(" "));
2646
+ const before2 = (lines[cursorLine] ?? "").slice(0, cursorCol).trim();
2647
+ return !(before2.startsWith("/") && !before2.includes(" "));
2533
2648
  }
2534
2649
  applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
2535
2650
  const line = lines[cursorLine] ?? "";
@@ -2805,6 +2920,14 @@ function previousBoundary(text, offset) {
2805
2920
  }
2806
2921
  return previous;
2807
2922
  }
2923
+ var MARKER = /\[Pasted from clipboard #\d+: \d+ (?:lines|characters)\]|\[#Image\d+\]/g;
2924
+ function markerAt(text, offset) {
2925
+ for (const match of text.matchAll(MARKER)) {
2926
+ const end = match.index + match[0].length;
2927
+ if (match.index <= offset && offset < end) return [match.index, end];
2928
+ }
2929
+ return null;
2930
+ }
2808
2931
  function nextBoundary(text, offset) {
2809
2932
  for (const boundary of boundaries(text)) {
2810
2933
  if (boundary > offset) return boundary;
@@ -2846,15 +2969,15 @@ var TextBuffer = class {
2846
2969
  insert(text) {
2847
2970
  if (!text) return;
2848
2971
  const line = this.lines[this.row];
2849
- const before = line.slice(0, this.col);
2972
+ const before2 = line.slice(0, this.col);
2850
2973
  const after = line.slice(this.col);
2851
2974
  const parts = text.split("\n");
2852
2975
  if (parts.length === 1) {
2853
- this.lines[this.row] = before + text + after;
2976
+ this.lines[this.row] = before2 + text + after;
2854
2977
  this.col += text.length;
2855
2978
  } else {
2856
2979
  const last = parts.at(-1);
2857
- const inserted = [before + parts[0], ...parts.slice(1, -1), last + after];
2980
+ const inserted = [before2 + parts[0], ...parts.slice(1, -1), last + after];
2858
2981
  this.lines.splice(this.row, 1, ...inserted);
2859
2982
  this.row += parts.length - 1;
2860
2983
  this.col = last.length;
@@ -2867,8 +2990,8 @@ var TextBuffer = class {
2867
2990
  backspace() {
2868
2991
  if (this.col > 0) {
2869
2992
  const line = this.lines[this.row];
2870
- const start = previousBoundary(line, this.col);
2871
- this.lines[this.row] = line.slice(0, start) + line.slice(this.col);
2993
+ const [start, end] = markerAt(line, this.col - 1) ?? [previousBoundary(line, this.col), this.col];
2994
+ this.lines[this.row] = line.slice(0, start) + line.slice(end);
2872
2995
  this.col = start;
2873
2996
  } else if (this.row > 0) {
2874
2997
  const previous = this.lines[this.row - 1];
@@ -2884,8 +3007,9 @@ var TextBuffer = class {
2884
3007
  deleteForward() {
2885
3008
  const line = this.lines[this.row];
2886
3009
  if (this.col < line.length) {
2887
- const end = nextBoundary(line, this.col);
2888
- this.lines[this.row] = line.slice(0, this.col) + line.slice(end);
3010
+ const [start, end] = markerAt(line, this.col) ?? [this.col, nextBoundary(line, this.col)];
3011
+ this.lines[this.row] = line.slice(0, start) + line.slice(end);
3012
+ this.col = start;
2889
3013
  } else if (this.row < this.lines.length - 1) {
2890
3014
  this.lines.splice(this.row, 2, line + this.lines[this.row + 1]);
2891
3015
  } else {
@@ -3087,7 +3211,7 @@ var ScrollState = class {
3087
3211
  return "closed";
3088
3212
  }
3089
3213
  const page = Math.max(1, pageRows);
3090
- const before = this.offset;
3214
+ const before2 = this.offset;
3091
3215
  if (key.upArrow) this.offset = Math.max(0, this.offset - 1);
3092
3216
  else if (key.downArrow) this.offset += 1;
3093
3217
  else if (key.pageUp) this.offset = Math.max(0, this.offset - page);
@@ -3095,7 +3219,7 @@ var ScrollState = class {
3095
3219
  else if (key.home) this.offset = 0;
3096
3220
  else if (key.end) this.offset = Number.MAX_SAFE_INTEGER;
3097
3221
  else return "ignored";
3098
- return this.offset === before ? "ignored" : "changed";
3222
+ return this.offset === before2 ? "ignored" : "changed";
3099
3223
  }
3100
3224
  /** Clamps the offset to the content; call before rendering. */
3101
3225
  clamp(totalRows, visibleRows) {
@@ -3125,7 +3249,7 @@ var ToolInspectorState = class {
3125
3249
  }
3126
3250
  const calls = this.calls();
3127
3251
  if (calls.length === 0) return "ignored";
3128
- const before = `${this.selected}:${this.expanded}:${this.detailOffset}`;
3252
+ const before2 = `${this.selected}:${this.expanded}:${this.detailOffset}`;
3129
3253
  if (key.upArrow) this.select(this.selected === 0 ? calls.length - 1 : this.selected - 1);
3130
3254
  else if (key.downArrow)
3131
3255
  this.select(this.selected === calls.length - 1 ? 0 : this.selected + 1);
@@ -3145,7 +3269,7 @@ var ToolInspectorState = class {
3145
3269
  if (this.expanded) this.detailOffset = Number.MAX_SAFE_INTEGER;
3146
3270
  else this.select(calls.length - 1);
3147
3271
  } else return "ignored";
3148
- return before === `${this.selected}:${this.expanded}:${this.detailOffset}` ? "ignored" : "changed";
3272
+ return before2 === `${this.selected}:${this.expanded}:${this.detailOffset}` ? "ignored" : "changed";
3149
3273
  }
3150
3274
  select(index) {
3151
3275
  if (index === this.selected) return;
@@ -3513,8 +3637,8 @@ var PromptEditor = class {
3513
3637
  if (key.return && !key.meta && !key.shift) {
3514
3638
  const item = state.items[state.selected];
3515
3639
  const { row, col } = this.buffer.cursor;
3516
- const before = (this.buffer.lineList[row] ?? "").slice(0, col);
3517
- if (item && before === `/${item.value}`) {
3640
+ const before2 = (this.buffer.lineList[row] ?? "").slice(0, col);
3641
+ if (item && before2 === `/${item.value}`) {
3518
3642
  this.closeAutocomplete();
3519
3643
  return false;
3520
3644
  }
@@ -3552,8 +3676,8 @@ var PromptEditor = class {
3552
3676
  const provider = this.autocompleteProvider;
3553
3677
  const { row, col } = this.buffer.cursor;
3554
3678
  const lines = [...this.buffer.lineList];
3555
- const before = (lines[row] ?? "").slice(0, col);
3556
- const relevant = force || extractMentionPrefix(before) !== null || row === 0 && before.startsWith("/") && !/\s/.test(before);
3679
+ const before2 = (lines[row] ?? "").slice(0, col);
3680
+ const relevant = force || extractMentionPrefix(before2) !== null || row === 0 && before2.startsWith("/") && !/\s/.test(before2);
3557
3681
  if (!provider || !relevant) {
3558
3682
  if (this.autocompleteState) {
3559
3683
  this.closeAutocomplete();
@@ -4004,6 +4128,8 @@ var TuiController = class {
4004
4128
  windowTitleBusy = false;
4005
4129
  windowTitleFrame = 0;
4006
4130
  imagePasteActive = false;
4131
+ pressedAt = null;
4132
+ dragging = false;
4007
4133
  stopped = false;
4008
4134
  unsubscribeStore;
4009
4135
  resolveExit = () => {
@@ -4045,6 +4171,7 @@ var TuiController = class {
4045
4171
  // ---------------------------------------------------------------- keys
4046
4172
  /** Shortcuts that work while the prompt has focus; false lets the editor have the key. */
4047
4173
  handlePromptKey(input, key) {
4174
+ if (this.io.selection.active && this.handleSelectionKey(input, key)) return true;
4048
4175
  if (key.ctrl && input === "c") {
4049
4176
  this.pressCtrlC();
4050
4177
  return true;
@@ -4091,9 +4218,6 @@ var TuiController = class {
4091
4218
  case "o":
4092
4219
  this.toggleToolDetails();
4093
4220
  return true;
4094
- case "s":
4095
- this.toggleSelectMode();
4096
- return true;
4097
4221
  case "t":
4098
4222
  this.openToolInspector();
4099
4223
  return true;
@@ -4368,17 +4492,60 @@ ${skill.path}`
4368
4492
  this.scroll = next;
4369
4493
  this.refresh();
4370
4494
  }
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);
4495
+ /** Wheel scrolls; press + drag selects text; a click while text is selected copies it. */
4496
+ handleMouse(input) {
4497
+ const mouse = parseMouse(input);
4498
+ if (!mouse) return;
4499
+ const rows = wheelRows(input);
4500
+ if (rows) {
4501
+ this.dropSelection();
4502
+ this.scrollBy(rows);
4503
+ return;
4504
+ }
4505
+ const button = mouse.button & ~28;
4506
+ const cell = { row: mouse.row, col: mouse.col };
4507
+ if (button === 0 && !mouse.release) {
4508
+ this.pressedAt = cell;
4509
+ this.dragging = false;
4510
+ } else if (button === 32 && this.pressedAt) {
4511
+ if (!this.dragging)
4512
+ this.store.setState({
4513
+ statusLine: "Selecting: click, Enter or Ctrl+C copies \xB7 Shift+arrows extend \xB7 Esc cancels"
4514
+ });
4515
+ this.dragging = true;
4516
+ this.io.selection.set(this.pressedAt, cell);
4517
+ } else if (button === 0 && mouse.release) {
4518
+ if (!this.dragging && this.io.selection.active) void this.copySelection();
4519
+ this.pressedAt = null;
4520
+ }
4521
+ }
4522
+ /** Keys while text is selected: Shift+arrows extend, Enter/Ctrl+C copy, Esc cancels; any other key drops it. */
4523
+ handleSelectionKey(input, key) {
4524
+ if (key.shift && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow)) {
4525
+ this.io.selection.extend(
4526
+ key.upArrow ? -1 : key.downArrow ? 1 : 0,
4527
+ key.leftArrow ? -1 : key.rightArrow ? 1 : 0
4528
+ );
4529
+ return true;
4530
+ }
4531
+ if (key.return || key.ctrl && input === "c") {
4532
+ void this.copySelection();
4533
+ return true;
4534
+ }
4535
+ this.dropSelection();
4536
+ return Boolean(key.escape);
4537
+ }
4538
+ dropSelection() {
4539
+ this.io.selection.clear();
4540
+ if (this.store.getState().statusLine?.startsWith("Selecting:")) this.store.setState({ statusLine: null });
4541
+ }
4542
+ async copySelection() {
4543
+ const text = this.io.selection.text();
4544
+ this.dropSelection();
4545
+ if (!text) return;
4546
+ const tool = await copyText(text, (data) => this.io.write(data));
4379
4547
  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."
4548
+ statusLine: tool ? `Copied ${text.length} characters.` : `Sent ${text.length} characters to the terminal's clipboard.`
4382
4549
  });
4383
4550
  }
4384
4551
  /** /copy: the last answer (not its thinking) to the clipboard. */
@@ -4387,7 +4554,7 @@ ${skill.path}`
4387
4554
  if (answer?.role !== "assistant") return "There is no answer to copy yet.";
4388
4555
  const tool = await copyText(answer.text.trim(), (data) => this.io.write(data));
4389
4556
  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.`;
4557
+ return tool ? `Copied the last answer (${size}).` : `Sent the last answer (${size}) to the terminal's clipboard; if nothing was copied, drag over it with the mouse and click to copy.`;
4391
4558
  }
4392
4559
  toggleToolDetails() {
4393
4560
  this.expandedTools = !this.expandedTools;
@@ -4697,7 +4864,7 @@ function errorMessage(error, fallback) {
4697
4864
 
4698
4865
  // src/tui/ink/dialogs/views.tsx
4699
4866
  import { Box as Box5, Text as Text5, useInput as useInput2, usePaste as usePaste2 } from "ink";
4700
- import stringWidth2 from "string-width";
4867
+ import stringWidth3 from "string-width";
4701
4868
  import { useMemo, useState } from "react";
4702
4869
  import { OTHER_OPTION_ID as OTHER_OPTION_ID2 } from "@datalabrotterdam/nova-ai-agent/core/user-questions.js";
4703
4870
  import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
@@ -4748,7 +4915,7 @@ function SelectionDialog({
4748
4915
  const visible = state.items.slice(start, start + rows);
4749
4916
  const leadingWidth = Math.min(
4750
4917
  14,
4751
- Math.max(0, ...state.items.map((item) => item.columns ? stringWidth2(item.columns.leading) : 0))
4918
+ Math.max(0, ...state.items.map((item) => item.columns ? stringWidth3(item.columns.leading) : 0))
4752
4919
  );
4753
4920
  return /* @__PURE__ */ jsx5(
4754
4921
  Frame,
@@ -4768,7 +4935,7 @@ function SelectionDialog({
4768
4935
  }
4769
4936
  return /* @__PURE__ */ jsxs5(Box5, { children: [
4770
4937
  marker,
4771
- /* @__PURE__ */ jsx5(Box5, { flexShrink: 0, minWidth: Math.min(32, stringWidth2(item.label)), children: /* @__PURE__ */ jsx5(Text5, { bold: selected, wrap: "truncate-end", children: item.label || item.value }) }),
4938
+ /* @__PURE__ */ jsx5(Box5, { flexShrink: 0, minWidth: Math.min(32, stringWidth3(item.label)), children: /* @__PURE__ */ jsx5(Text5, { bold: selected, wrap: "truncate-end", children: item.label || item.value }) }),
4772
4939
  item.description ? /* @__PURE__ */ jsx5(Box5, { flexGrow: 1, flexShrink: 1, marginLeft: 2, children: /* @__PURE__ */ jsx5(Text5, { color: palette.muted, wrap: "truncate-end", children: item.description.replace(/[\r\n]+/g, " ").trim() }) }) : null
4773
4940
  ] }, item.value);
4774
4941
  })
@@ -4944,23 +5111,42 @@ function QuestionDialog({
4944
5111
  function PermissionDialog({
4945
5112
  request,
4946
5113
  onClose,
5114
+ cwd,
5115
+ height = 24,
5116
+ width = 80,
4947
5117
  isActive = true
4948
5118
  }) {
5119
+ const [details, setDetails] = useState(null);
4949
5120
  useInput2(
4950
5121
  (input, key) => {
4951
- if (!isMouseInput(input) && permissionKey(request, input, key) === "closed") onClose();
5122
+ if (isMouseInput(input)) return;
5123
+ if (input === "v") setDetails(new ScrollState(() => setDetails(null)));
5124
+ else if (permissionKey(request, input, key) === "closed") onClose();
4952
5125
  },
4953
- { isActive }
5126
+ { isActive: isActive && !details }
4954
5127
  );
4955
- const args = JSON.stringify(request.args, null, 2).split("\n");
4956
- const shown = args.slice(0, 12);
5128
+ const title = request.title ?? `Permission requested: ${request.toolName}`;
5129
+ if (details)
5130
+ return /* @__PURE__ */ jsx5(
5131
+ ScrollPanel,
5132
+ {
5133
+ title,
5134
+ content: argumentsText(request.args),
5135
+ state: details,
5136
+ height,
5137
+ width,
5138
+ isActive
5139
+ }
5140
+ );
5141
+ const entries = Object.entries(request.args);
5142
+ const nameWidth = Math.max(0, ...entries.map(([name]) => name.length));
4957
5143
  return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.warning, paddingX: 1, children: [
4958
- /* @__PURE__ */ jsx5(Text5, { color: palette.warning, bold: true, children: request.title ?? `Permission requested: ${request.toolName}` }),
5144
+ /* @__PURE__ */ jsx5(Text5, { color: palette.warning, bold: true, wrap: "truncate-end", children: title }),
4959
5145
  /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: `${request.toolName} \xB7 ${request.kind} tool` }),
4960
- /* @__PURE__ */ jsxs5(Box5, { marginTop: 1, paddingLeft: 1, flexDirection: "column", children: [
4961
- /* @__PURE__ */ jsx5(Text5, { children: shown.join("\n") }),
4962
- args.length > shown.length ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, children: `\u2026 ${args.length - shown.length} more lines` }) : null
4963
- ] }),
5146
+ entries.length ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, paddingLeft: 1, flexDirection: "column", children: entries.map(([name, value]) => /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate-end", children: [
5147
+ /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: name.padEnd(nameWidth) }),
5148
+ ` ${summarizeArgument(value, cwd)}`
5149
+ ] }, name)) }) : null,
4964
5150
  /* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { children: [
4965
5151
  /* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[a]" }),
4966
5152
  " once ",
@@ -4970,10 +5156,23 @@ function PermissionDialog({
4970
5156
  /* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[w]" }),
4971
5157
  " always ",
4972
5158
  /* @__PURE__ */ jsx5(Text5, { color: palette.danger, children: "[d]" }),
4973
- " deny"
5159
+ " deny",
5160
+ " ",
5161
+ /* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[v]" }),
5162
+ " view details"
4974
5163
  ] }) })
4975
5164
  ] });
4976
5165
  }
5166
+ function summarizeArgument(value, cwd) {
5167
+ if (typeof value !== "string") return JSON.stringify(value) ?? String(value);
5168
+ if (cwd && value.startsWith(`${cwd}/`)) return value.slice(cwd.length + 1);
5169
+ const lines = value.split("\n").length;
5170
+ return lines > 1 ? `${lines} lines` : value;
5171
+ }
5172
+ function argumentsText(args) {
5173
+ return Object.entries(args).map(([name, value]) => `${name}:
5174
+ ${typeof value === "string" ? value : JSON.stringify(value, null, 2)}`).join("\n\n");
5175
+ }
4977
5176
  function wrapPlain(text, width) {
4978
5177
  const rows = [];
4979
5178
  for (const line of text.split(/\r?\n/)) {
@@ -4984,7 +5183,7 @@ function wrapPlain(text, width) {
4984
5183
  let current = "";
4985
5184
  let currentWidth = 0;
4986
5185
  for (const character of line) {
4987
- const characterWidth = stringWidth2(character);
5186
+ const characterWidth = stringWidth3(character);
4988
5187
  if (currentWidth + characterWidth > width && current) {
4989
5188
  rows.push(current);
4990
5189
  current = "";
@@ -5029,13 +5228,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
5029
5228
  const stdout = options.stdout ?? process.stdout;
5030
5229
  const stdin = options.stdin ?? process.stdin;
5031
5230
  const kittyKeyboard = options.kittyKeyboard ?? kittyKeyboardOverride() ?? await detectKittyKeyboard(stdin, stdout);
5231
+ const selection = new ScreenSelection(stdout);
5032
5232
  const controller = new TuiController(store, runner, {
5033
5233
  write: (data) => {
5034
5234
  stdout.write(data);
5035
- }
5235
+ },
5236
+ selection
5036
5237
  });
5037
5238
  const instance = render(/* @__PURE__ */ jsx6(App, { controller }), {
5038
- stdout,
5239
+ stdout: selection.wrap(stdout),
5039
5240
  stdin,
5040
5241
  exitOnCtrlC: false,
5041
5242
  patchConsole: true,
@@ -5078,10 +5279,7 @@ function App({ controller }) {
5078
5279
  const content = useBoxMetrics(contentRef);
5079
5280
  const working = useWorking(view);
5080
5281
  const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
5081
- useInput3((input) => {
5082
- const rows2 = wheelRows(input);
5083
- if (rows2) controller.scrollBy(rows2);
5084
- });
5282
+ useInput3((input) => controller.handleMouse(input));
5085
5283
  const height = Math.max(4, rows - 1);
5086
5284
  const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
5087
5285
  useEffect(() => {
@@ -5136,7 +5334,7 @@ var CommittedBlock = memo(function CommittedBlock2({
5136
5334
  return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
5137
5335
  }
5138
5336
  return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
5139
- }, (before, after) => before.block === after.block);
5337
+ }, (before2, after) => before2.block === after.block);
5140
5338
  function UserAnchor({
5141
5339
  block,
5142
5340
  onUserTop,
@@ -5162,8 +5360,18 @@ function Surface({
5162
5360
  if (view.question) return /* @__PURE__ */ jsx6(QuestionDialog, { state: view.question }, view.question.pending.id);
5163
5361
  const permission = view.ui.pendingPermission;
5164
5362
  if (permission)
5165
- return /* @__PURE__ */ jsx6(PermissionDialog, { request: permission, onClose: () => {
5166
- } }, permission.toolCallId);
5363
+ return /* @__PURE__ */ jsx6(
5364
+ PermissionDialog,
5365
+ {
5366
+ request: permission,
5367
+ onClose: () => {
5368
+ },
5369
+ cwd: view.ui.cwd,
5370
+ height: rows,
5371
+ width: columns
5372
+ },
5373
+ permission.toolCallId
5374
+ );
5167
5375
  const overlay = view.overlay;
5168
5376
  if (overlay?.kind === "selection")
5169
5377
  return /* @__PURE__ */ jsx6(SelectionDialog, { title: overlay.title, state: overlay.state, height: rows });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@datalabrotterdam/nova-ai-cli",
3
- "version": "1.2.0-alpha.13",
3
+ "version": "1.2.0-alpha.15",
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.9",
42
+ "@datalabrotterdam/nova-ai-agent": "^1.0.0-alpha.10",
43
43
  "@datalabrotterdam/nova-ai-browser-host": "^1.0.0",
44
44
  "chalk": "^5.3.0",
45
45
  "cli-highlight": "^2.1.11",