@datalabrotterdam/nova-ai-cli 1.2.0-alpha.14 → 1.2.0-alpha.16
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/dist/index.js +1 -1
- package/dist/{tui-NBVDJQIA.js → tui-UHITDRFM.js} +315 -76
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [@datalabrotterdam/nova-ai-cli-v1.2.0-alpha.16](https://github.com/Datalab-Rotterdam/nova-ai-cli/compare/@datalabrotterdam/nova-ai-cli@1.2.0-alpha.15...@datalabrotterdam/nova-ai-cli@1.2.0-alpha.16) (2026-10-25)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **cli:** Ctrl+C clears the prompt; Esc asks about running background tasks ([a7cb854](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/a7cb854f4494d88851b672af913d7a1dee666684))
|
|
7
|
+
|
|
8
|
+
# [@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)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **cli:** Markdown rules span the row in a streamed block of their own ([7ab846c](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/7ab846c86ff83ce07046f8c366dbab8d019b17b8))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **cli:** compact permission dialog with a details view ([63e0d97](https://github.com/Datalab-Rotterdam/nova-ai-cli/commit/63e0d9740152b8e2874ecde62b2ba2dd88899423))
|
|
19
|
+
* **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)
|
|
20
|
+
|
|
1
21
|
# [@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)
|
|
2
22
|
|
|
3
23
|
|
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;
|
|
@@ -4052,7 +4179,7 @@ var TuiController = class {
|
|
|
4052
4179
|
if (key.escape) {
|
|
4053
4180
|
if (this.editor.autocomplete) return false;
|
|
4054
4181
|
if (this.store.getState().busy) {
|
|
4055
|
-
this.
|
|
4182
|
+
void this.interrupt();
|
|
4056
4183
|
return true;
|
|
4057
4184
|
}
|
|
4058
4185
|
if (this.scroll > 0) {
|
|
@@ -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;
|
|
@@ -4101,7 +4225,12 @@ var TuiController = class {
|
|
|
4101
4225
|
return false;
|
|
4102
4226
|
}
|
|
4103
4227
|
}
|
|
4228
|
+
/** Ctrl+C clears a typed prompt first; on an empty one it arms exit. */
|
|
4104
4229
|
pressCtrlC() {
|
|
4230
|
+
if (this.editor.getText()) {
|
|
4231
|
+
this.editor.setText("");
|
|
4232
|
+
return;
|
|
4233
|
+
}
|
|
4105
4234
|
if (this.exitArmed) {
|
|
4106
4235
|
this.requestExit();
|
|
4107
4236
|
return;
|
|
@@ -4245,6 +4374,32 @@ var TuiController = class {
|
|
|
4245
4374
|
(value) => this.setPermissionMode(value)
|
|
4246
4375
|
);
|
|
4247
4376
|
};
|
|
4377
|
+
/** Esc during a turn: stop it, then ask whether background tasks it started keep running. */
|
|
4378
|
+
async interrupt() {
|
|
4379
|
+
this.runner.cancel();
|
|
4380
|
+
const running = (await this.runner.listBackgroundJobs().catch(() => [])).filter(
|
|
4381
|
+
(job) => job.status === "running"
|
|
4382
|
+
);
|
|
4383
|
+
if (running.length === 0) return;
|
|
4384
|
+
const titles = running.map((job) => job.title).join(", ");
|
|
4385
|
+
this.showSelection(
|
|
4386
|
+
`Interrupted. ${running.length === 1 ? "1 background task is" : `${running.length} background tasks are`} still running.`,
|
|
4387
|
+
[
|
|
4388
|
+
{ value: "keep", label: "Keep running", description: titles },
|
|
4389
|
+
{ value: "stop", label: running.length === 1 ? "Stop it" : "Stop them", description: titles }
|
|
4390
|
+
],
|
|
4391
|
+
"background-tasks",
|
|
4392
|
+
(value) => {
|
|
4393
|
+
if (value !== "stop") return;
|
|
4394
|
+
void Promise.allSettled(running.map((job) => this.runner.killBackgroundJob(job.jobId))).then(
|
|
4395
|
+
(results) => {
|
|
4396
|
+
const stopped = results.filter((result) => result.status === "fulfilled").length;
|
|
4397
|
+
this.store.setState({ statusLine: `Stopped ${stopped} of ${running.length} background tasks.` });
|
|
4398
|
+
}
|
|
4399
|
+
);
|
|
4400
|
+
}
|
|
4401
|
+
);
|
|
4402
|
+
}
|
|
4248
4403
|
openBackgroundTasks = async () => {
|
|
4249
4404
|
try {
|
|
4250
4405
|
const jobs = await this.runner.listBackgroundJobs();
|
|
@@ -4368,17 +4523,60 @@ ${skill.path}`
|
|
|
4368
4523
|
this.scroll = next;
|
|
4369
4524
|
this.refresh();
|
|
4370
4525
|
}
|
|
4371
|
-
/**
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4526
|
+
/** Wheel scrolls; press + drag selects text; a click while text is selected copies it. */
|
|
4527
|
+
handleMouse(input) {
|
|
4528
|
+
const mouse = parseMouse(input);
|
|
4529
|
+
if (!mouse) return;
|
|
4530
|
+
const rows = wheelRows(input);
|
|
4531
|
+
if (rows) {
|
|
4532
|
+
this.dropSelection();
|
|
4533
|
+
this.scrollBy(rows);
|
|
4534
|
+
return;
|
|
4535
|
+
}
|
|
4536
|
+
const button = mouse.button & ~28;
|
|
4537
|
+
const cell = { row: mouse.row, col: mouse.col };
|
|
4538
|
+
if (button === 0 && !mouse.release) {
|
|
4539
|
+
this.pressedAt = cell;
|
|
4540
|
+
this.dragging = false;
|
|
4541
|
+
} else if (button === 32 && this.pressedAt) {
|
|
4542
|
+
if (!this.dragging)
|
|
4543
|
+
this.store.setState({
|
|
4544
|
+
statusLine: "Selecting: click, Enter or Ctrl+C copies \xB7 Shift+arrows extend \xB7 Esc cancels"
|
|
4545
|
+
});
|
|
4546
|
+
this.dragging = true;
|
|
4547
|
+
this.io.selection.set(this.pressedAt, cell);
|
|
4548
|
+
} else if (button === 0 && mouse.release) {
|
|
4549
|
+
if (!this.dragging && this.io.selection.active) void this.copySelection();
|
|
4550
|
+
this.pressedAt = null;
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
/** Keys while text is selected: Shift+arrows extend, Enter/Ctrl+C copy, Esc cancels; any other key drops it. */
|
|
4554
|
+
handleSelectionKey(input, key) {
|
|
4555
|
+
if (key.shift && (key.leftArrow || key.rightArrow || key.upArrow || key.downArrow)) {
|
|
4556
|
+
this.io.selection.extend(
|
|
4557
|
+
key.upArrow ? -1 : key.downArrow ? 1 : 0,
|
|
4558
|
+
key.leftArrow ? -1 : key.rightArrow ? 1 : 0
|
|
4559
|
+
);
|
|
4560
|
+
return true;
|
|
4561
|
+
}
|
|
4562
|
+
if (key.return || key.ctrl && input === "c") {
|
|
4563
|
+
void this.copySelection();
|
|
4564
|
+
return true;
|
|
4565
|
+
}
|
|
4566
|
+
this.dropSelection();
|
|
4567
|
+
return Boolean(key.escape);
|
|
4568
|
+
}
|
|
4569
|
+
dropSelection() {
|
|
4570
|
+
this.io.selection.clear();
|
|
4571
|
+
if (this.store.getState().statusLine?.startsWith("Selecting:")) this.store.setState({ statusLine: null });
|
|
4572
|
+
}
|
|
4573
|
+
async copySelection() {
|
|
4574
|
+
const text = this.io.selection.text();
|
|
4575
|
+
this.dropSelection();
|
|
4576
|
+
if (!text) return;
|
|
4577
|
+
const tool = await copyText(text, (data) => this.io.write(data));
|
|
4379
4578
|
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."
|
|
4579
|
+
statusLine: tool ? `Copied ${text.length} characters.` : `Sent ${text.length} characters to the terminal's clipboard.`
|
|
4382
4580
|
});
|
|
4383
4581
|
}
|
|
4384
4582
|
/** /copy: the last answer (not its thinking) to the clipboard. */
|
|
@@ -4387,7 +4585,7 @@ ${skill.path}`
|
|
|
4387
4585
|
if (answer?.role !== "assistant") return "There is no answer to copy yet.";
|
|
4388
4586
|
const tool = await copyText(answer.text.trim(), (data) => this.io.write(data));
|
|
4389
4587
|
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,
|
|
4588
|
+
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
4589
|
}
|
|
4392
4590
|
toggleToolDetails() {
|
|
4393
4591
|
this.expandedTools = !this.expandedTools;
|
|
@@ -4697,7 +4895,7 @@ function errorMessage(error, fallback) {
|
|
|
4697
4895
|
|
|
4698
4896
|
// src/tui/ink/dialogs/views.tsx
|
|
4699
4897
|
import { Box as Box5, Text as Text5, useInput as useInput2, usePaste as usePaste2 } from "ink";
|
|
4700
|
-
import
|
|
4898
|
+
import stringWidth3 from "string-width";
|
|
4701
4899
|
import { useMemo, useState } from "react";
|
|
4702
4900
|
import { OTHER_OPTION_ID as OTHER_OPTION_ID2 } from "@datalabrotterdam/nova-ai-agent/core/user-questions.js";
|
|
4703
4901
|
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
@@ -4748,7 +4946,7 @@ function SelectionDialog({
|
|
|
4748
4946
|
const visible = state.items.slice(start, start + rows);
|
|
4749
4947
|
const leadingWidth = Math.min(
|
|
4750
4948
|
14,
|
|
4751
|
-
Math.max(0, ...state.items.map((item) => item.columns ?
|
|
4949
|
+
Math.max(0, ...state.items.map((item) => item.columns ? stringWidth3(item.columns.leading) : 0))
|
|
4752
4950
|
);
|
|
4753
4951
|
return /* @__PURE__ */ jsx5(
|
|
4754
4952
|
Frame,
|
|
@@ -4768,7 +4966,7 @@ function SelectionDialog({
|
|
|
4768
4966
|
}
|
|
4769
4967
|
return /* @__PURE__ */ jsxs5(Box5, { children: [
|
|
4770
4968
|
marker,
|
|
4771
|
-
/* @__PURE__ */ jsx5(Box5, { flexShrink: 0, minWidth: Math.min(32,
|
|
4969
|
+
/* @__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
4970
|
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
4971
|
] }, item.value);
|
|
4774
4972
|
})
|
|
@@ -4944,23 +5142,42 @@ function QuestionDialog({
|
|
|
4944
5142
|
function PermissionDialog({
|
|
4945
5143
|
request,
|
|
4946
5144
|
onClose,
|
|
5145
|
+
cwd,
|
|
5146
|
+
height = 24,
|
|
5147
|
+
width = 80,
|
|
4947
5148
|
isActive = true
|
|
4948
5149
|
}) {
|
|
5150
|
+
const [details, setDetails] = useState(null);
|
|
4949
5151
|
useInput2(
|
|
4950
5152
|
(input, key) => {
|
|
4951
|
-
if (
|
|
5153
|
+
if (isMouseInput(input)) return;
|
|
5154
|
+
if (input === "v") setDetails(new ScrollState(() => setDetails(null)));
|
|
5155
|
+
else if (permissionKey(request, input, key) === "closed") onClose();
|
|
4952
5156
|
},
|
|
4953
|
-
{ isActive }
|
|
5157
|
+
{ isActive: isActive && !details }
|
|
4954
5158
|
);
|
|
4955
|
-
const
|
|
4956
|
-
|
|
5159
|
+
const title = request.title ?? `Permission requested: ${request.toolName}`;
|
|
5160
|
+
if (details)
|
|
5161
|
+
return /* @__PURE__ */ jsx5(
|
|
5162
|
+
ScrollPanel,
|
|
5163
|
+
{
|
|
5164
|
+
title,
|
|
5165
|
+
content: argumentsText(request.args),
|
|
5166
|
+
state: details,
|
|
5167
|
+
height,
|
|
5168
|
+
width,
|
|
5169
|
+
isActive
|
|
5170
|
+
}
|
|
5171
|
+
);
|
|
5172
|
+
const entries = Object.entries(request.args);
|
|
5173
|
+
const nameWidth = Math.max(0, ...entries.map(([name]) => name.length));
|
|
4957
5174
|
return /* @__PURE__ */ jsxs5(Box5, { flexDirection: "column", borderStyle: "round", borderColor: palette.warning, paddingX: 1, children: [
|
|
4958
|
-
/* @__PURE__ */ jsx5(Text5, { color: palette.warning, bold: true,
|
|
5175
|
+
/* @__PURE__ */ jsx5(Text5, { color: palette.warning, bold: true, wrap: "truncate-end", children: title }),
|
|
4959
5176
|
/* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: `${request.toolName} \xB7 ${request.kind} tool` }),
|
|
4960
|
-
/* @__PURE__ */
|
|
4961
|
-
/* @__PURE__ */ jsx5(Text5, { children:
|
|
4962
|
-
|
|
4963
|
-
] }),
|
|
5177
|
+
entries.length ? /* @__PURE__ */ jsx5(Box5, { marginTop: 1, paddingLeft: 1, flexDirection: "column", children: entries.map(([name, value]) => /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate-end", children: [
|
|
5178
|
+
/* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: name.padEnd(nameWidth) }),
|
|
5179
|
+
` ${summarizeArgument(value, cwd)}`
|
|
5180
|
+
] }, name)) }) : null,
|
|
4964
5181
|
/* @__PURE__ */ jsx5(Box5, { marginTop: 1, children: /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
4965
5182
|
/* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[a]" }),
|
|
4966
5183
|
" once ",
|
|
@@ -4970,10 +5187,23 @@ function PermissionDialog({
|
|
|
4970
5187
|
/* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[w]" }),
|
|
4971
5188
|
" always ",
|
|
4972
5189
|
/* @__PURE__ */ jsx5(Text5, { color: palette.danger, children: "[d]" }),
|
|
4973
|
-
" deny"
|
|
5190
|
+
" deny",
|
|
5191
|
+
" ",
|
|
5192
|
+
/* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: "[v]" }),
|
|
5193
|
+
" view details"
|
|
4974
5194
|
] }) })
|
|
4975
5195
|
] });
|
|
4976
5196
|
}
|
|
5197
|
+
function summarizeArgument(value, cwd) {
|
|
5198
|
+
if (typeof value !== "string") return JSON.stringify(value) ?? String(value);
|
|
5199
|
+
if (cwd && value.startsWith(`${cwd}/`)) return value.slice(cwd.length + 1);
|
|
5200
|
+
const lines = value.split("\n").length;
|
|
5201
|
+
return lines > 1 ? `${lines} lines` : value;
|
|
5202
|
+
}
|
|
5203
|
+
function argumentsText(args) {
|
|
5204
|
+
return Object.entries(args).map(([name, value]) => `${name}:
|
|
5205
|
+
${typeof value === "string" ? value : JSON.stringify(value, null, 2)}`).join("\n\n");
|
|
5206
|
+
}
|
|
4977
5207
|
function wrapPlain(text, width) {
|
|
4978
5208
|
const rows = [];
|
|
4979
5209
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -4984,7 +5214,7 @@ function wrapPlain(text, width) {
|
|
|
4984
5214
|
let current = "";
|
|
4985
5215
|
let currentWidth = 0;
|
|
4986
5216
|
for (const character of line) {
|
|
4987
|
-
const characterWidth =
|
|
5217
|
+
const characterWidth = stringWidth3(character);
|
|
4988
5218
|
if (currentWidth + characterWidth > width && current) {
|
|
4989
5219
|
rows.push(current);
|
|
4990
5220
|
current = "";
|
|
@@ -5029,13 +5259,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
|
|
|
5029
5259
|
const stdout = options.stdout ?? process.stdout;
|
|
5030
5260
|
const stdin = options.stdin ?? process.stdin;
|
|
5031
5261
|
const kittyKeyboard = options.kittyKeyboard ?? kittyKeyboardOverride() ?? await detectKittyKeyboard(stdin, stdout);
|
|
5262
|
+
const selection = new ScreenSelection(stdout);
|
|
5032
5263
|
const controller = new TuiController(store, runner, {
|
|
5033
5264
|
write: (data) => {
|
|
5034
5265
|
stdout.write(data);
|
|
5035
|
-
}
|
|
5266
|
+
},
|
|
5267
|
+
selection
|
|
5036
5268
|
});
|
|
5037
5269
|
const instance = render(/* @__PURE__ */ jsx6(App, { controller }), {
|
|
5038
|
-
stdout,
|
|
5270
|
+
stdout: selection.wrap(stdout),
|
|
5039
5271
|
stdin,
|
|
5040
5272
|
exitOnCtrlC: false,
|
|
5041
5273
|
patchConsole: true,
|
|
@@ -5078,10 +5310,7 @@ function App({ controller }) {
|
|
|
5078
5310
|
const content = useBoxMetrics(contentRef);
|
|
5079
5311
|
const working = useWorking(view);
|
|
5080
5312
|
const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
|
|
5081
|
-
useInput3((input) =>
|
|
5082
|
-
const rows2 = wheelRows(input);
|
|
5083
|
-
if (rows2) controller.scrollBy(rows2);
|
|
5084
|
-
});
|
|
5313
|
+
useInput3((input) => controller.handleMouse(input));
|
|
5085
5314
|
const height = Math.max(4, rows - 1);
|
|
5086
5315
|
const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
|
|
5087
5316
|
useEffect(() => {
|
|
@@ -5136,7 +5365,7 @@ var CommittedBlock = memo(function CommittedBlock2({
|
|
|
5136
5365
|
return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
5137
5366
|
}
|
|
5138
5367
|
return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
5139
|
-
}, (
|
|
5368
|
+
}, (before2, after) => before2.block === after.block);
|
|
5140
5369
|
function UserAnchor({
|
|
5141
5370
|
block,
|
|
5142
5371
|
onUserTop,
|
|
@@ -5162,8 +5391,18 @@ function Surface({
|
|
|
5162
5391
|
if (view.question) return /* @__PURE__ */ jsx6(QuestionDialog, { state: view.question }, view.question.pending.id);
|
|
5163
5392
|
const permission = view.ui.pendingPermission;
|
|
5164
5393
|
if (permission)
|
|
5165
|
-
return /* @__PURE__ */ jsx6(
|
|
5166
|
-
|
|
5394
|
+
return /* @__PURE__ */ jsx6(
|
|
5395
|
+
PermissionDialog,
|
|
5396
|
+
{
|
|
5397
|
+
request: permission,
|
|
5398
|
+
onClose: () => {
|
|
5399
|
+
},
|
|
5400
|
+
cwd: view.ui.cwd,
|
|
5401
|
+
height: rows,
|
|
5402
|
+
width: columns
|
|
5403
|
+
},
|
|
5404
|
+
permission.toolCallId
|
|
5405
|
+
);
|
|
5167
5406
|
const overlay = view.overlay;
|
|
5168
5407
|
if (overlay?.kind === "selection")
|
|
5169
5408
|
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.16",
|
|
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",
|