@noobdemon/noob-cli 1.13.1 → 1.13.3
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 +22 -0
- package/README.md +139 -106
- package/package.json +3 -2
- package/src/agent.js +9 -5
- package/src/api.js +11 -1
- package/src/diff.js +40 -6
- package/src/i18n.js +0 -14
- package/src/repl/commands/goal.js +37 -0
- package/src/repl/commands/kg.js +147 -0
- package/src/repl.js +36 -147
- package/src/tools.js +11 -1
- package/src/tui.js +128 -29
package/src/tui.js
CHANGED
|
@@ -13,6 +13,20 @@ import { c } from './ui.js';
|
|
|
13
13
|
const ESC = '\x1b';
|
|
14
14
|
const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
15
15
|
const visLen = (s) => s.replace(ANSI_RE, '').length;
|
|
16
|
+
// Trích chuỗi mã ANSI còn "mở" tính tới cuối `text`: replay mọi SGR escape,
|
|
17
|
+
// reset (\x1b[0m / \x1b[m) xoá sạch, mã khác tích luỹ. Trả về chuỗi để TÁI MỞ
|
|
18
|
+
// màu ở đầu dòng wrap kế tiếp — chống mất màu khi soft-wrap cắt giữa vùng màu.
|
|
19
|
+
function openAnsi(text) {
|
|
20
|
+
let codes = [];
|
|
21
|
+
const re = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
22
|
+
let m;
|
|
23
|
+
while ((m = re.exec(text))) {
|
|
24
|
+
const seq = m[0];
|
|
25
|
+
if (seq === '\x1b[0m' || seq === '\x1b[m') codes = [];
|
|
26
|
+
else codes.push(seq);
|
|
27
|
+
}
|
|
28
|
+
return codes.join('');
|
|
29
|
+
}
|
|
16
30
|
function readClipboardImageDataUrl() {
|
|
17
31
|
if (process.platform !== 'win32') return null;
|
|
18
32
|
try {
|
|
@@ -72,16 +86,20 @@ function softWrapLine(text, width) {
|
|
|
72
86
|
if (visLen(text) <= width) return [close(text)];
|
|
73
87
|
const lines = [];
|
|
74
88
|
let remaining = text;
|
|
89
|
+
let carry = ''; // mã ANSI còn mở từ dòng trước → tái mở ở đầu dòng kế.
|
|
75
90
|
while (remaining) {
|
|
76
|
-
|
|
77
|
-
|
|
91
|
+
const piece = carry + remaining;
|
|
92
|
+
if (visLen(piece) <= width) {
|
|
93
|
+
lines.push(piece);
|
|
78
94
|
break;
|
|
79
95
|
}
|
|
80
|
-
let cutPos = findVisPos(remaining, width);
|
|
96
|
+
let cutPos = findVisPos(remaining, width - visLen(carry));
|
|
81
97
|
const slice = remaining.slice(0, cutPos);
|
|
82
98
|
const lastSpace = slice.lastIndexOf(' ');
|
|
83
99
|
if (lastSpace > width * 0.3) cutPos = lastSpace;
|
|
84
|
-
|
|
100
|
+
const head = remaining.slice(0, cutPos).trimEnd();
|
|
101
|
+
lines.push(carry + head);
|
|
102
|
+
carry = hasAnsi ? openAnsi(carry + head) : '';
|
|
85
103
|
remaining = remaining.slice(cutPos).trimStart();
|
|
86
104
|
}
|
|
87
105
|
return lines.map(close);
|
|
@@ -97,17 +115,21 @@ function wrapText(text, width, maxLines) {
|
|
|
97
115
|
// nằm ở đâu trong tổng stream.
|
|
98
116
|
const lines = [];
|
|
99
117
|
let remaining = text;
|
|
118
|
+
let carry = ''; // mã ANSI còn mở từ dòng trước → tái mở ở đầu dòng kế.
|
|
100
119
|
while (remaining) {
|
|
101
|
-
|
|
102
|
-
|
|
120
|
+
const piece = carry + remaining;
|
|
121
|
+
if (visLen(piece) <= width) {
|
|
122
|
+
lines.push(piece);
|
|
103
123
|
remaining = '';
|
|
104
124
|
break;
|
|
105
125
|
}
|
|
106
|
-
let cutPos = findVisPos(remaining, width);
|
|
126
|
+
let cutPos = findVisPos(remaining, width - visLen(carry));
|
|
107
127
|
const slice = remaining.slice(0, cutPos);
|
|
108
128
|
const lastSpace = slice.lastIndexOf(' ');
|
|
109
129
|
if (lastSpace > width * 0.3) cutPos = lastSpace;
|
|
110
|
-
|
|
130
|
+
const head = remaining.slice(0, cutPos).trimEnd();
|
|
131
|
+
lines.push(carry + head);
|
|
132
|
+
carry = hasAnsi ? openAnsi(carry + head) : '';
|
|
111
133
|
remaining = remaining.slice(cutPos).trimStart();
|
|
112
134
|
}
|
|
113
135
|
if (lines.length > maxLines) {
|
|
@@ -221,6 +243,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
221
243
|
let frameTimer = null;
|
|
222
244
|
let prevRows = 0;
|
|
223
245
|
let drawn = false;
|
|
246
|
+
let lastUpBy = 0; // số dòng con trỏ nằm TRÊN dòng cuối đã vẽ (placeCursor set);
|
|
247
|
+
// eraseSeq dùng để biết con trỏ đang ở đâu mà nhảy lên đúng dòng đầu.
|
|
224
248
|
|
|
225
249
|
let promptLabel = '';
|
|
226
250
|
// Thanh nhập = mảng "ô" + con trỏ. Mỗi ô là 1 ký tự {c} hoặc 1 khối dán
|
|
@@ -288,18 +312,30 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
288
312
|
return preview ? `[pasted ${x.lines} lines: "${preview}"]` : `[pasted ${x.lines} lines]`;
|
|
289
313
|
};
|
|
290
314
|
const fullText = () => cells.map(cellStr).join('');
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
.
|
|
315
|
+
// Con trỏ ẢO: ô tại vị trí `cur` được đảo màu (reverse video \x1b[7m). Vẽ con
|
|
316
|
+
// trỏ trong CHÍNH text thay vì dựa con trỏ phần cứng → không phụ thuộc terminal
|
|
317
|
+
// có tôn trọng DECSCUSR / vẽ block-cursor đè sai ô hay không. cur == len → con
|
|
318
|
+
// trỏ sau ký tự cuối: đảo màu 1 space ở cuối.
|
|
319
|
+
const REV = `${ESC}[7m`;
|
|
320
|
+
const UNREV = `${ESC}[27m`;
|
|
321
|
+
const cellColored = (x) => {
|
|
322
|
+
if (x.image) return c.dim(imageLabel(x));
|
|
323
|
+
if (x.paste === undefined) return x.c;
|
|
324
|
+
const preview = pastePreview(x.paste);
|
|
325
|
+
const label = preview
|
|
326
|
+
? `[pasted ${x.lines} lines: "${preview}"]`
|
|
327
|
+
: `[pasted ${x.lines} lines]`;
|
|
328
|
+
return c.dim(label);
|
|
329
|
+
};
|
|
330
|
+
const coloredInput = () => {
|
|
331
|
+
let out = '';
|
|
332
|
+
for (let i = 0; i < cells.length; i++) {
|
|
333
|
+
const rendered = cellColored(cells[i]);
|
|
334
|
+
out += i === cur ? `${REV}${rendered}${UNREV}` : rendered;
|
|
335
|
+
}
|
|
336
|
+
if (cur >= cells.length) out += `${REV} ${UNREV}`; // con trỏ ở cuối
|
|
337
|
+
return out;
|
|
338
|
+
};
|
|
303
339
|
|
|
304
340
|
// Dựng thanh nhập + tính vị trí con trỏ trên màn (`cursorScreenCol` +
|
|
305
341
|
// `cursorScreenRow`). Vừa khung → tô màu đầy đủ, 1 dòng. Tràn khung → soft-
|
|
@@ -308,6 +344,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
308
344
|
// thêm "…".
|
|
309
345
|
let cursorScreenCol = 0;
|
|
310
346
|
let cursorScreenRow = 0;
|
|
347
|
+
let trailingRows = 0; // số dòng trang trí SAU bar (thanh chắn dưới); placeCursor nhảy lên qua chúng
|
|
311
348
|
let barRows = 1; // số dòng bar hiện tại; cập nhật bởi renderBar. placeCursor
|
|
312
349
|
// dùng để tính upBy — KHÔNG dùng totalRows (gồm cả top/menu
|
|
313
350
|
// rows phía trên) vì sẽ kéo cursor lên quá cao, lần commit
|
|
@@ -377,7 +414,21 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
377
414
|
cursorScreenCol = promptW + colInLine;
|
|
378
415
|
barRows = lines.length;
|
|
379
416
|
|
|
380
|
-
|
|
417
|
+
// Con trỏ ẢO cho nhánh wrap: chèn ô đảo màu (reverse video) tại `colInLine`
|
|
418
|
+
// của dòng `curLine`. Con trỏ ở cuối ô (colInLine == lineLen) → đảo màu 1
|
|
419
|
+
// space append; giữa dòng → đảo màu ký tự tại đó. KHÔNG dùng con trỏ phần
|
|
420
|
+
// cứng (đã ẩn suốt phiên) nên nhánh này PHẢI tự vẽ con trỏ, nếu không wrap
|
|
421
|
+
// sẽ mất con trỏ.
|
|
422
|
+
return lines
|
|
423
|
+
.map((l, i) => {
|
|
424
|
+
let body = l;
|
|
425
|
+
if (i === curLine) {
|
|
426
|
+
if (colInLine >= l.length) body = l + `${REV} ${UNREV}`;
|
|
427
|
+
else body = l.slice(0, colInLine) + `${REV}${l[colInLine]}${UNREV}` + l.slice(colInLine + 1);
|
|
428
|
+
}
|
|
429
|
+
return (i === 0 ? promptLabel : indent) + body;
|
|
430
|
+
})
|
|
431
|
+
.join('\n');
|
|
381
432
|
}
|
|
382
433
|
function topRow() {
|
|
383
434
|
if (liveOut) {
|
|
@@ -521,13 +572,29 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
521
572
|
if (top !== null) r.push(...top);
|
|
522
573
|
for (const mr of menuRows()) r.push(mr);
|
|
523
574
|
}
|
|
575
|
+
// Khung nhập: kẻ 2 thanh chắn (trên + dưới) quanh thanh nhập để khu gõ luôn
|
|
576
|
+
// rõ ràng, KỂ CẢ khi model đang chạy (rows() chạy cùng path busy/rảnh). Thanh
|
|
577
|
+
// chắn là dòng trang trí tĩnh — KHÔNG nằm trong barRows (renderBar set), nên
|
|
578
|
+
// placeCursor phải nhảy lên qua thanh chắn DƯỚI: cộng nó vào trailingRows.
|
|
579
|
+
const rule = c.dim('─'.repeat(Math.max(8, cols())));
|
|
524
580
|
const bar = renderBar();
|
|
525
|
-
if (bar)
|
|
581
|
+
if (bar) {
|
|
582
|
+
r.push(rule); // thanh chắn TRÊN (phía trên bar — không ảnh hưởng cursor)
|
|
583
|
+
r.push(...bar.split('\n'));
|
|
584
|
+
r.push(rule); // thanh chắn DƯỚI
|
|
585
|
+
trailingRows = 1; // 1 dòng sau bar → placeCursor nhảy lên thêm 1
|
|
586
|
+
} else {
|
|
587
|
+
trailingRows = 0;
|
|
588
|
+
}
|
|
526
589
|
return r;
|
|
527
590
|
}
|
|
528
591
|
function eraseSeq() {
|
|
529
592
|
if (!drawn) return '\r';
|
|
530
|
-
|
|
593
|
+
// Sau lần vẽ trước, placeCursor đã đưa con trỏ về dòng nhập logic — nằm
|
|
594
|
+
// TRÊN dòng cuối cùng đã vẽ `lastUpBy` dòng (qua thanh chắn dưới + phần bar).
|
|
595
|
+
// Để về dòng ĐẦU rồi ESC[J xóa xuống, chỉ nhảy lên (prevRows-1) - lastUpBy.
|
|
596
|
+
const up = Math.max(0, prevRows - 1 - lastUpBy);
|
|
597
|
+
return '\r' + (up > 0 ? `${ESC}[${up}A` : '') + `${ESC}[J`;
|
|
531
598
|
}
|
|
532
599
|
// Sau khi vẽ xong các hàng, con trỏ đang ở CUỐI thanh (hàng cuối, cột cuối).
|
|
533
600
|
// Đưa về đúng (row, col) của con trỏ logic: \r về cột 0 → đi lên `upBy` hàng
|
|
@@ -535,7 +602,11 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
535
602
|
// upBy dùng `barRows` (số dòng BAR, set bởi renderBar) — KHÔNG dùng totalRows
|
|
536
603
|
// (gồm cả top/menu rows phía trên bar) vì sẽ kéo cursor lên quá cao.
|
|
537
604
|
const placeCursor = () => {
|
|
538
|
-
|
|
605
|
+
// upBy: số dòng cần nhảy LÊN từ dòng cuối cùng đã vẽ về dòng con trỏ logic.
|
|
606
|
+
// Dòng cuối giờ là thanh chắn DƯỚI (trailingRows), nên cộng nó vào: nhảy qua
|
|
607
|
+
// thanh chắn rồi mới tính trong phạm vi bar như cũ.
|
|
608
|
+
const upBy = trailingRows + (barRows - 1 - cursorScreenRow);
|
|
609
|
+
lastUpBy = upBy; // ghi lại để eraseSeq lần sau biết con trỏ đang ở đâu
|
|
539
610
|
let s = '\r';
|
|
540
611
|
if (upBy > 0) s += `${ESC}[${upBy}A`;
|
|
541
612
|
if (cursorScreenCol > 0) s += `${ESC}[${cursorScreenCol}C`;
|
|
@@ -543,7 +614,10 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
543
614
|
};
|
|
544
615
|
function draw() {
|
|
545
616
|
const rs = rows(); // rows() → renderBar() cập nhật cursorScreenRow/Col + barRows
|
|
546
|
-
|
|
617
|
+
// Con trỏ ảo (ô đảo màu trong coloredInput) là con trỏ DUY NHẤT — giữ con trỏ
|
|
618
|
+
// phần cứng ẩn (?25l) suốt phiên, không bật lại ở cuối frame. placeCursor vẫn
|
|
619
|
+
// đưa con trỏ phần cứng về dòng nhập để eraseSeq lần sau tính đúng vùng xóa.
|
|
620
|
+
w(`${ESC}[?25l` + eraseSeq() + rs.join('\n') + placeCursor());
|
|
547
621
|
prevRows = rs.length;
|
|
548
622
|
drawn = true;
|
|
549
623
|
}
|
|
@@ -560,7 +634,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
560
634
|
let s = `${ESC}[?25l` + eraseSeq();
|
|
561
635
|
s += block;
|
|
562
636
|
if (!block.endsWith('\n')) s += '\n';
|
|
563
|
-
|
|
637
|
+
// Con trỏ ảo là con trỏ duy nhất — không bật con trỏ phần cứng ở cuối frame.
|
|
638
|
+
s += rs.join('\n') + placeCursor();
|
|
564
639
|
w(s);
|
|
565
640
|
prevRows = rs.length;
|
|
566
641
|
drawn = true;
|
|
@@ -577,7 +652,27 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
577
652
|
}
|
|
578
653
|
liveOut = buf;
|
|
579
654
|
if (done.length) commit(done.join('\n'));
|
|
580
|
-
|
|
655
|
+
// Phần dở (chưa có \n) sống trong liveOut → topRow() đẩy nó lên TOP vùng vẽ
|
|
656
|
+
// lại mỗi frame. Nếu liveOut phình dài hơn 1 dòng terminal, wrapText cho nhiều
|
|
657
|
+
// dòng → tổng rows() cao dần, vượt chiều cao terminal → terminal CUỘN, prevRows
|
|
658
|
+
// không còn khớp vị trí vật lý con trỏ → eraseSeq lần kế xóa nhầm, bar (thanh
|
|
659
|
+
// chat) bị đẩy khuất/mất. Fix: khi liveOut tràn 1 dòng, FLUSH phần đã đầy dòng
|
|
660
|
+
// thành output vĩnh viễn (commit), chỉ giữ lại đuôi ngắn (< cols) trong liveOut.
|
|
661
|
+
else if (visLen(liveOut) > cols()) {
|
|
662
|
+
const w0 = cols();
|
|
663
|
+
const wrapped = softWrapLine(liveOut, w0);
|
|
664
|
+
// Giữ dòng cuối (đuôi đang gõ tiếp) trong liveOut, commit các dòng đã đầy.
|
|
665
|
+
const tail = wrapped[wrapped.length - 1];
|
|
666
|
+
const head = wrapped.slice(0, -1).join('\n');
|
|
667
|
+
if (head) {
|
|
668
|
+
liveOut = tail.replace(ANSI_RE, '');
|
|
669
|
+
commit(head);
|
|
670
|
+
} else {
|
|
671
|
+
draw();
|
|
672
|
+
}
|
|
673
|
+
} else {
|
|
674
|
+
draw();
|
|
675
|
+
}
|
|
581
676
|
}
|
|
582
677
|
|
|
583
678
|
// ----- bàn phím: paste (bracketed) + heuristic chunk nhiều dòng -----
|
|
@@ -941,7 +1036,9 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
941
1036
|
process.stdin.setEncoding('utf8');
|
|
942
1037
|
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
943
1038
|
process.stdin.resume();
|
|
944
|
-
|
|
1039
|
+
// Bar cursor (DECSCUSR 6): vạch đứng hiển thị GIỮA hai ô, không đè màu lên
|
|
1040
|
+
// ký tự như block cursor → tránh ảo giác '|a' (block phủ ô trống sau text).
|
|
1041
|
+
w(`${ESC}[?2004h${ESC}[6 q`); // bật bracketed paste + bar cursor
|
|
945
1042
|
process.stdin.on('data', onData); // Shift+Tab (\x1b[Z) + mũi tên xử lý trong feedKeys
|
|
946
1043
|
// vá stdout: mọi output → commit phía trên thanh
|
|
947
1044
|
process.stdout.write = (chunk, enc, cb) => {
|
|
@@ -1046,7 +1143,9 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, com
|
|
|
1046
1143
|
try {
|
|
1047
1144
|
if (frameTimer) clearInterval(frameTimer);
|
|
1048
1145
|
process.stdout.write = realWrite;
|
|
1049
|
-
|
|
1146
|
+
// Tắt bracketed paste + reset cursor style về mặc định (DECSCUSR 0) +
|
|
1147
|
+
// hiện con trỏ. Không reset → bar cursor dính lại ở shell sau khi thoát.
|
|
1148
|
+
w(`${ESC}[?2004l${ESC}[0 q${ESC}[?25h\n`);
|
|
1050
1149
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
|
1051
1150
|
process.stdin.pause();
|
|
1052
1151
|
} catch {}
|