@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 +20 -0
- package/README.md +4 -0
- package/dist/index.js +1 -1
- package/dist/{tui-NBVDJQIA.js → tui-W6ZQ2SWR.js} +283 -75
- package/package.json +2 -2
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
|
@@ -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+)
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1683
|
-
...rows.map((row) =>
|
|
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] -
|
|
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 (
|
|
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
|
|
2517
|
-
const mention = extractMentionPrefix(
|
|
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 &&
|
|
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,
|
|
2522
|
-
return items2.length ? { items: items2, prefix:
|
|
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(
|
|
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
|
|
2532
|
-
return !(
|
|
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
|
|
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] =
|
|
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 = [
|
|
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(
|
|
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,
|
|
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
|
|
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 ===
|
|
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
|
|
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
|
|
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
|
|
3517
|
-
if (item &&
|
|
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
|
|
3556
|
-
const relevant = force || extractMentionPrefix(
|
|
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
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 ?
|
|
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,
|
|
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 (
|
|
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
|
|
4956
|
-
|
|
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,
|
|
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__ */
|
|
4961
|
-
/* @__PURE__ */ jsx5(Text5, { children:
|
|
4962
|
-
|
|
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 =
|
|
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
|
-
}, (
|
|
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(
|
|
5166
|
-
|
|
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.
|
|
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.
|
|
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",
|