@noobdemon/noob-cli 1.12.14 → 1.12.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 +6 -0
- package/package.json +1 -1
- package/src/agent.js +24 -2
- package/src/api.js +7 -3
- package/src/diff.js +13 -4
- package/src/i18n.js +2 -1
- package/src/repl/agent-dispatch.js +30 -21
- package/src/repl/complete.js +1 -0
- package/src/repl/permission.js +1 -2
- package/src/repl/state.js +1 -0
- package/src/repl/stream-printer.js +2 -11
- package/src/repl/workflow-commands.js +3 -1
- package/src/repl.js +220 -56
- package/src/tokens.js +13 -6
- package/src/tools.js +47 -8
- package/src/tui.js +173 -22
- package/src/ui.js +8 -5
- package/src/workflow-bg.js +33 -5
- package/src/workflow-runs.js +4 -1
package/src/tools.js
CHANGED
|
@@ -110,6 +110,13 @@ export function removeRoot(p) {
|
|
|
110
110
|
export function listRoots() {
|
|
111
111
|
return [cwd(), ...extraRoots];
|
|
112
112
|
}
|
|
113
|
+
// Xoá toàn bộ extraRoots khỏi bộ nhớ phiên (KHÔNG đụng dirs.json). Dùng cho test
|
|
114
|
+
// isolation: extraRoots là module-global, loadWorkspaceRoots() ở import nạp sẵn
|
|
115
|
+
// nội dung dirs.json → leak sang test. Gọi trong beforeEach để mỗi test thấy scope
|
|
116
|
+
// sạch (chỉ cwd đã chdir). KHÔNG persist — production không nên gọi.
|
|
117
|
+
export function clearRoots() {
|
|
118
|
+
extraRoots.clear();
|
|
119
|
+
}
|
|
113
120
|
function within(root, full) {
|
|
114
121
|
if (full === root) return true;
|
|
115
122
|
const rel = path.relative(root, full);
|
|
@@ -180,7 +187,10 @@ function htmlToText(html) {
|
|
|
180
187
|
s = s.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
181
188
|
s = s.replace(/<(script|style|head|noscript|svg)\b[\s\S]*?<\/\1>/gi, ' ');
|
|
182
189
|
// Block-level tag → newline để giữ cấu trúc đoạn.
|
|
183
|
-
s = s.replace(
|
|
190
|
+
s = s.replace(
|
|
191
|
+
/<\/(p|div|section|article|header|footer|li|tr|h[1-6]|ul|ol|table|blockquote)>/gi,
|
|
192
|
+
'\n'
|
|
193
|
+
);
|
|
184
194
|
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
185
195
|
s = s.replace(/<[^>]+>/g, ' '); // gỡ mọi tag còn lại
|
|
186
196
|
// Decode entity phổ biến.
|
|
@@ -541,15 +551,45 @@ export const TOOLS = {
|
|
|
541
551
|
let out = '';
|
|
542
552
|
let timedOut = false;
|
|
543
553
|
let aborted = false;
|
|
554
|
+
let settled = false;
|
|
555
|
+
|
|
556
|
+
// Resolve ĐÚNG 1 lần. Không phụ thuộc child 'close' để thoát: gradle daemon
|
|
557
|
+
// / JVM grandchildren có thể giữ stdio pipe sống sau taskkill → 'close'
|
|
558
|
+
// không bao giờ bắn → Promise treo vĩnh viễn → tool kẹt → model không
|
|
559
|
+
// được gọi lại. Đây là gốc bug treo CLI khi chạy compileJava.
|
|
560
|
+
const finish = (text) => {
|
|
561
|
+
if (settled) return;
|
|
562
|
+
settled = true;
|
|
563
|
+
clearTimeout(killer);
|
|
564
|
+
clearTimeout(graceTimer);
|
|
565
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
566
|
+
resolve(text);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Sau khi ra lệnh kill, không chờ 'close' mãi. Cho cây tiến trình 2s ân
|
|
570
|
+
// hạn để thoát sạch; quá hạn → resolve cưỡng bức để giải phóng tool.
|
|
571
|
+
let graceTimer = null;
|
|
572
|
+
const forceResolveAfterKill = () => {
|
|
573
|
+
if (graceTimer || settled) return;
|
|
574
|
+
graceTimer = setTimeout(() => {
|
|
575
|
+
const tail = aborted
|
|
576
|
+
? `\n[aborted by user (Ctrl+C) — killed (tiến trình con có thể còn sống nền).]`
|
|
577
|
+
: `\n[timed out after ${Math.round(timeout / 1000)}s — killed (tiến trình con có thể còn sống nền). Nếu là server / task chạy lâu, chạy lại với {"background": true}.]`;
|
|
578
|
+
finish(clip((out.trim() || '(no output)') + tail));
|
|
579
|
+
}, 2000);
|
|
580
|
+
};
|
|
581
|
+
|
|
544
582
|
const killer = setTimeout(() => {
|
|
545
583
|
timedOut = true;
|
|
546
584
|
killBgTree(child);
|
|
585
|
+
forceResolveAfterKill();
|
|
547
586
|
}, timeout);
|
|
548
587
|
// Ctrl+C trong lúc command đang chạy → kill cây tiến trình con (Windows
|
|
549
588
|
// dùng taskkill /T để diệt cả grand-children, vd npm spawn node).
|
|
550
589
|
const onAbort = () => {
|
|
551
590
|
aborted = true;
|
|
552
591
|
killBgTree(child);
|
|
592
|
+
forceResolveAfterKill();
|
|
553
593
|
};
|
|
554
594
|
if (signal) {
|
|
555
595
|
if (signal.aborted) onAbort();
|
|
@@ -558,19 +598,15 @@ export const TOOLS = {
|
|
|
558
598
|
child.stdout.on('data', (d) => (out += d));
|
|
559
599
|
child.stderr.on('data', (d) => (out += d));
|
|
560
600
|
child.on('error', (e) => {
|
|
561
|
-
|
|
562
|
-
signal?.removeEventListener?.('abort', onAbort);
|
|
563
|
-
resolve(`Failed to start command: ${e.message}`);
|
|
601
|
+
finish(`Failed to start command: ${e.message}`);
|
|
564
602
|
});
|
|
565
603
|
child.on('close', (code) => {
|
|
566
|
-
clearTimeout(killer);
|
|
567
|
-
signal?.removeEventListener?.('abort', onAbort);
|
|
568
604
|
const tail = aborted
|
|
569
605
|
? `\n[aborted by user (Ctrl+C) — killed.]`
|
|
570
606
|
: timedOut
|
|
571
607
|
? `\n[timed out after ${Math.round(timeout / 1000)}s — killed. If this is a server or other long-running task, re-run with {"background": true} instead.]`
|
|
572
608
|
: `\n[exit code ${code}]`;
|
|
573
|
-
|
|
609
|
+
finish(clip((out.trim() || '(no output)') + tail));
|
|
574
610
|
});
|
|
575
611
|
});
|
|
576
612
|
},
|
|
@@ -652,7 +688,10 @@ export const TOOLS = {
|
|
|
652
688
|
const out = await kgAddObservations([{ entityName, contents }]);
|
|
653
689
|
const added = out[0]?.addedObservations || [];
|
|
654
690
|
if (!added.length) return `Không có observation mới cho '${entityName}' (đã trùng)`;
|
|
655
|
-
return
|
|
691
|
+
return (
|
|
692
|
+
`Đã thêm ${added.length} observation cho '${entityName}':\n` +
|
|
693
|
+
added.map((o) => ` - ${o}`).join('\n')
|
|
694
|
+
);
|
|
656
695
|
},
|
|
657
696
|
};
|
|
658
697
|
|
package/src/tui.js
CHANGED
|
@@ -107,7 +107,7 @@ function truncStr(s, max) {
|
|
|
107
107
|
return s.length > max ? s.slice(0, max - 1) + '…' : s;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer } = {}) {
|
|
110
|
+
export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, completer } = {}) {
|
|
111
111
|
const tty = !!(process.stdout.isTTY && process.stdin.isTTY) && process.env.NOOB_TUI !== '0';
|
|
112
112
|
const cols = () => process.stdout.columns || 80;
|
|
113
113
|
|
|
@@ -153,6 +153,12 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
153
153
|
setBusy() {},
|
|
154
154
|
setMeta() {},
|
|
155
155
|
setTodos() {},
|
|
156
|
+
setUltra() {},
|
|
157
|
+
setAgentMode() {},
|
|
158
|
+
setGoal() {},
|
|
159
|
+
setBgAgents() {},
|
|
160
|
+
pushAgentLog() {},
|
|
161
|
+
clearAgentLog() {},
|
|
156
162
|
setPrompt() {},
|
|
157
163
|
read() {
|
|
158
164
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
@@ -180,6 +186,16 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
180
186
|
let busyLabel = '';
|
|
181
187
|
let busyMeta = ''; // chuỗi phụ (elapsed + token) repl đẩy vào qua setMeta()
|
|
182
188
|
let todos = []; // [{text, done}] — danh sách todo đang chạy, repl parse từ model output
|
|
189
|
+
let ultra = false; // ULTRA mode đang bật? → segment trong status bar
|
|
190
|
+
let bgAgents = 0; // số sub-agent / workflow chạy nền → segment + gợi ý Ctrl+O
|
|
191
|
+
let agentMode = ''; // build | plan | compose — UI hint mode → segment status bar (Ctrl+T / /mode)
|
|
192
|
+
let goalText = ''; // HARD GOAL hiện tại (/goal) → segment 🎯 status bar (truncate)
|
|
193
|
+
// Ring buffer log sub-agent nền: KHÔNG in ra UI chính, chỉ vào đây. Ctrl+O mở
|
|
194
|
+
// overlay xem. Mỗi entry: chuỗi đã format (có thể kèm màu). Giữ tối đa MAX_LOG.
|
|
195
|
+
const MAX_AGENT_LOG = 500;
|
|
196
|
+
const agentLog = [];
|
|
197
|
+
let overlayOpen = false; // overlay log đang mở?
|
|
198
|
+
let overlayScroll = 0; // offset cuộn từ ĐÁY (0 = xem dòng mới nhất)
|
|
183
199
|
let frame = 0;
|
|
184
200
|
let frameTimer = null;
|
|
185
201
|
let prevRows = 0;
|
|
@@ -354,38 +370,71 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
354
370
|
return wrapText(liveOut, cols(), 2);
|
|
355
371
|
}
|
|
356
372
|
const spin = FRAMES[frame % FRAMES.length];
|
|
357
|
-
//
|
|
373
|
+
// Badge ghép vào mọi dòng status: ULTRA bật/tắt + số sub-agent nền (gợi ý
|
|
374
|
+
// Ctrl+O xem log). Reset ANSI từng đoạn (chalk tự đóng) → không bleed.
|
|
375
|
+
const modeBadge = agentMode ? c.tool(`◆ ${agentMode}`) : '';
|
|
376
|
+
const goalBadge = goalText ? c.accent('🎯 ') + c.dim(truncStr(goalText, 28)) : '';
|
|
377
|
+
const ultraBadge = ultra ? c.err('🚀 ULTRA') : '';
|
|
378
|
+
const bgBadge = bgAgents ? c.accent(`⊕ ${bgAgents} nền`) + c.dim(' (Ctrl+O)') : '';
|
|
379
|
+
const badges = [modeBadge, goalBadge, ultraBadge, bgBadge].filter(Boolean).join(c.dim(' · '));
|
|
380
|
+
const badgeTail = badges ? c.dim(' · ') + badges : '';
|
|
381
|
+
// Todo progress: 1 dòng gọn dùng chung (📋 done/total bar pct%). Dựng SẴN ở
|
|
382
|
+
// đây để ghép vào status line theo trạng thái — KHÔNG return sớm nuốt status.
|
|
383
|
+
// Rảnh → chỉ dòng này. Busy → ghép cạnh spinner. Mất todo không nuốt spinner.
|
|
384
|
+
let progress = null;
|
|
385
|
+
let taskLine = null;
|
|
358
386
|
if (todos.length) {
|
|
359
387
|
const done = todos.filter((t) => t.done).length;
|
|
360
388
|
const total = todos.length;
|
|
361
389
|
const current = todos.find((t) => !t.done);
|
|
362
390
|
const pct = Math.round((done / total) * 100);
|
|
363
|
-
// Thanh progress:
|
|
364
|
-
const barW = Math.min(
|
|
391
|
+
// Thanh progress dày hơn, màu phân biệt: phần done = ember, còn lại = dim.
|
|
392
|
+
const barW = Math.min(16, Math.max(8, Math.floor(cols() * 0.2)));
|
|
365
393
|
const filled = Math.round((done / total) * barW);
|
|
366
|
-
const bar = '█'.repeat(filled) + '░'.repeat(barW - filled);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const tail = busy ? c.dim(' · Ctrl+C') : '';
|
|
374
|
-
return [progress + meta + tail, taskLine];
|
|
394
|
+
const bar = c.accent('█'.repeat(filled)) + c.dim('░'.repeat(barW - filled));
|
|
395
|
+
progress =
|
|
396
|
+
c.accent('📋 ') + c.ok(`${done}`) + c.dim(`/${total}`) + ' ' + bar + c.dim(` ${pct}%`);
|
|
397
|
+
// Task đang làm (▸ amber) — CHỈ dùng lúc busy (dòng 2). Rảnh bỏ để gọn 1 dòng.
|
|
398
|
+
taskLine = current
|
|
399
|
+
? c.tool(' ▸ ') + c.user(truncStr(current.text, cols() - 8))
|
|
400
|
+
: c.ok(' ✓ tất cả todo đã xong!');
|
|
375
401
|
}
|
|
402
|
+
|
|
376
403
|
// Khi busy, LUÔN hiện elapsed+tokens (busyMeta) cạnh statusText/busyLabel để
|
|
377
404
|
// người dùng thấy phiên đang sống — kể cả lúc model im giữa các bước.
|
|
378
405
|
if (statusText) {
|
|
379
406
|
const meta = busy && busyMeta ? c.dim(' · ' + busyMeta) : '';
|
|
380
407
|
const tail = busy ? c.dim(' · Ctrl+C để dừng') : '';
|
|
381
|
-
|
|
408
|
+
// Busy + todos → spinner + progress CHUNG dòng 1, task line dòng 2.
|
|
409
|
+
if (busy && progress) {
|
|
410
|
+
const line = c.dim(spin + ' ') + progress + meta + tail + badgeTail;
|
|
411
|
+
const out = wrapText(line, cols(), 2);
|
|
412
|
+
if (taskLine) out.push(taskLine);
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
const line = c.dim(spin + ' ') + statusText + meta + tail + badgeTail;
|
|
382
416
|
return wrapText(line, cols(), 2);
|
|
383
417
|
}
|
|
384
418
|
if (busy) {
|
|
385
|
-
const meta = busyMeta ? ' · ' + busyMeta : '';
|
|
386
|
-
|
|
419
|
+
const meta = busyMeta ? c.dim(' · ' + busyMeta) : '';
|
|
420
|
+
// Busy + todos → spinner + progress chung dòng 1, task line dòng 2.
|
|
421
|
+
if (progress) {
|
|
422
|
+
const line = c.dim(spin + ' ') + progress + meta + c.dim(' · Ctrl+C') + badgeTail;
|
|
423
|
+
const out = wrapText(line, cols(), 2);
|
|
424
|
+
if (taskLine) out.push(taskLine);
|
|
425
|
+
return out;
|
|
426
|
+
}
|
|
427
|
+
const line =
|
|
428
|
+
c.dim(spin + ' ' + (busyLabel || 'đang chạy')) +
|
|
429
|
+
meta +
|
|
430
|
+
c.dim(' · Ctrl+C để dừng') +
|
|
431
|
+
badgeTail;
|
|
387
432
|
return wrapText(line, cols(), 2);
|
|
388
433
|
}
|
|
434
|
+
// Rảnh + todos → 1 dòng progress gọn (KHÔNG task/ctx line, không nuốt ô nhập).
|
|
435
|
+
if (progress) return wrapText(progress + badgeTail, cols(), 2);
|
|
436
|
+
// Rảnh nhưng có ULTRA / sub-agent nền → vẫn hiện badge để user biết trạng thái.
|
|
437
|
+
if (badges) return wrapText(' ' + badges, cols(), 2);
|
|
389
438
|
return null;
|
|
390
439
|
}
|
|
391
440
|
function menuRows() {
|
|
@@ -405,11 +454,45 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
405
454
|
);
|
|
406
455
|
});
|
|
407
456
|
}
|
|
457
|
+
// Overlay log sub-agent nền (Ctrl+O). Thay thế vùng output chính (top/menu)
|
|
458
|
+
// bằng panel cuộn được: header + N dòng log gần nhất theo overlayScroll. KHÔNG
|
|
459
|
+
// đụng renderBar/cursor math — overlay chỉ là các dòng phía TRÊN thanh nhập.
|
|
460
|
+
const OVERLAY_LINES = 12;
|
|
461
|
+
function overlayRows() {
|
|
462
|
+
const head =
|
|
463
|
+
c.accent('┄┄ log sub-agent nền ') +
|
|
464
|
+
c.dim(`(${agentLog.length} dòng · ↑/↓ cuộn · Ctrl+O đóng)`);
|
|
465
|
+
if (!agentLog.length) {
|
|
466
|
+
return [head, c.dim(' (chưa có log — sub-agent nền sẽ ghi vào đây)')];
|
|
467
|
+
}
|
|
468
|
+
// Cửa sổ [start, end) tính từ ĐÁY theo overlayScroll (0 = đáy/mới nhất).
|
|
469
|
+
const end = Math.max(0, agentLog.length - overlayScroll);
|
|
470
|
+
const start = Math.max(0, end - OVERLAY_LINES);
|
|
471
|
+
const body = [];
|
|
472
|
+
const width = Math.max(8, cols() - 2);
|
|
473
|
+
for (const raw of agentLog.slice(start, end)) {
|
|
474
|
+
// Mỗi entry có thể dài → soft-wrap, nhưng chỉ lấy dòng đầu + '…' để panel
|
|
475
|
+
// gọn (1 entry = 1 dòng panel). User cuộn để xem từng entry.
|
|
476
|
+
const oneLine = String(raw).replace(/\n/g, ' ');
|
|
477
|
+
const wrapped = softWrapLine(oneLine, width);
|
|
478
|
+
body.push(
|
|
479
|
+
' ' + (wrapped.length > 1 ? wrapped[0].replace(ANSI_RE, '') + c.dim('…') : wrapped[0])
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
const more = overlayScroll > 0 ? c.dim(` ↓ còn ${overlayScroll} dòng mới hơn`) : '';
|
|
483
|
+
return more ? [head, ...body, more] : [head, ...body];
|
|
484
|
+
}
|
|
408
485
|
function rows() {
|
|
409
486
|
const r = [];
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
487
|
+
if (overlayOpen) {
|
|
488
|
+
// Overlay mở: thay top/menu bằng panel log. Thanh nhập vẫn vẽ bên dưới để
|
|
489
|
+
// user gõ /lệnh (vd Ctrl+O đóng) mà không mất con trỏ.
|
|
490
|
+
r.push(...overlayRows());
|
|
491
|
+
} else {
|
|
492
|
+
const top = topRow();
|
|
493
|
+
if (top !== null) r.push(...top);
|
|
494
|
+
for (const mr of menuRows()) r.push(mr);
|
|
495
|
+
}
|
|
413
496
|
const bar = renderBar();
|
|
414
497
|
if (bar) r.push(...bar.split('\n'));
|
|
415
498
|
return r;
|
|
@@ -532,9 +615,31 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
532
615
|
refreshMenu();
|
|
533
616
|
draw();
|
|
534
617
|
}
|
|
618
|
+
// Echo tin nhắn user vừa gửi thành KHỐI nền màu, gutter ember mỗi dòng. Plain
|
|
619
|
+
// text (không phải coloredInput) để bg phủ đều; mỗi dòng tự reset (tránh bleed
|
|
620
|
+
// màu sang hết chiều rộng terminal). Paste chip → 1 dòng tóm tắt [pasted...].
|
|
621
|
+
function echoUserBlock() {
|
|
622
|
+
const plain = cells.map(cellPlain).join('');
|
|
623
|
+
if (!plain.trim()) return promptLabel + coloredInput();
|
|
624
|
+
const gutter = c.userGutter('▌') + ' ';
|
|
625
|
+
const gutterW = 2; // '▌' + space (▌ là 1 cột)
|
|
626
|
+
const budget = Math.max(8, cols() - gutterW);
|
|
627
|
+
const wrapped = plain
|
|
628
|
+
.split('\n')
|
|
629
|
+
.flatMap((ln) => softWrapLine(ln, budget))
|
|
630
|
+
.map((ln) => {
|
|
631
|
+
// softWrapLine có thể đính RESET cuối khi text gốc có ANSI; strip để bg
|
|
632
|
+
// phủ liền, rồi pad tới budget cho khối nền chữ nhật đều mép phải.
|
|
633
|
+
const body = ln.replace(ANSI_RE, '');
|
|
634
|
+
const padded = body + ' '.repeat(Math.max(0, budget - visLen(body)));
|
|
635
|
+
return gutter + c.userBg(' ' + padded + ' ');
|
|
636
|
+
});
|
|
637
|
+
return wrapped.join('\n');
|
|
638
|
+
}
|
|
639
|
+
|
|
535
640
|
function submit() {
|
|
536
641
|
const full = fullText();
|
|
537
|
-
const echo =
|
|
642
|
+
const echo = echoUserBlock();
|
|
538
643
|
if (full.trim() && submitHistory[submitHistory.length - 1] !== full) {
|
|
539
644
|
submitHistory.push(full);
|
|
540
645
|
if (submitHistory.length > 200) submitHistory.shift();
|
|
@@ -616,8 +721,13 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
616
721
|
if (fin === 'Z')
|
|
617
722
|
onShiftTab?.(); // Shift+Tab = bật/tắt yolo
|
|
618
723
|
else if (fin === 'A' || fin === 'B') {
|
|
619
|
-
// ↑/↓:
|
|
620
|
-
if (
|
|
724
|
+
// ↑/↓: overlay mở → cuộn log; menu mở → điều hướng menu; còn lại → lịch sử.
|
|
725
|
+
if (overlayOpen) {
|
|
726
|
+
// ↑ = lùi về dòng cũ (tăng scroll), ↓ = về đáy (giảm scroll).
|
|
727
|
+
if (fin === 'A') overlayScroll = Math.min(agentLog.length, overlayScroll + 1);
|
|
728
|
+
else overlayScroll = Math.max(0, overlayScroll - 1);
|
|
729
|
+
draw();
|
|
730
|
+
} else if (menu.length) {
|
|
621
731
|
menuIdx =
|
|
622
732
|
fin === 'A' ? (menuIdx - 1 + menu.length) % menu.length : (menuIdx + 1) % menu.length;
|
|
623
733
|
draw();
|
|
@@ -689,6 +799,18 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
689
799
|
i++;
|
|
690
800
|
continue;
|
|
691
801
|
} // Ctrl+L: clear screen
|
|
802
|
+
if (ch === '\x0f') {
|
|
803
|
+
overlayOpen = !overlayOpen;
|
|
804
|
+
overlayScroll = 0; // mở lại = xem đáy (dòng mới nhất)
|
|
805
|
+
draw();
|
|
806
|
+
i++;
|
|
807
|
+
continue;
|
|
808
|
+
} // Ctrl+O: bật/tắt overlay log sub-agent nền
|
|
809
|
+
if (ch === '\x14') {
|
|
810
|
+
onCtrlT?.();
|
|
811
|
+
i++;
|
|
812
|
+
continue;
|
|
813
|
+
} // Ctrl+T: cycle build/plan/compose mode
|
|
692
814
|
if (ch === '\x01') {
|
|
693
815
|
cur = 0;
|
|
694
816
|
draw();
|
|
@@ -818,6 +940,35 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
818
940
|
todos = Array.isArray(items) ? items : [];
|
|
819
941
|
draw();
|
|
820
942
|
},
|
|
943
|
+
setUltra(on) {
|
|
944
|
+
ultra = !!on;
|
|
945
|
+
draw();
|
|
946
|
+
},
|
|
947
|
+
setAgentMode(mode) {
|
|
948
|
+
agentMode = mode || '';
|
|
949
|
+
draw();
|
|
950
|
+
},
|
|
951
|
+
setGoal(text) {
|
|
952
|
+
goalText = text || '';
|
|
953
|
+
draw();
|
|
954
|
+
},
|
|
955
|
+
setBgAgents(n) {
|
|
956
|
+
bgAgents = Math.max(0, n | 0);
|
|
957
|
+
draw();
|
|
958
|
+
},
|
|
959
|
+
// Đẩy 1 dòng log sub-agent nền vào ring buffer (KHÔNG in ra UI chính). Nếu
|
|
960
|
+
// overlay đang mở + đang xem đáy (scroll=0) → redraw để thấy dòng mới ngay.
|
|
961
|
+
pushAgentLog(line) {
|
|
962
|
+
if (line == null) return;
|
|
963
|
+
agentLog.push(String(line));
|
|
964
|
+
if (agentLog.length > MAX_AGENT_LOG) agentLog.shift();
|
|
965
|
+
if (overlayOpen && overlayScroll === 0) draw();
|
|
966
|
+
},
|
|
967
|
+
clearAgentLog() {
|
|
968
|
+
agentLog.length = 0;
|
|
969
|
+
overlayScroll = 0;
|
|
970
|
+
if (overlayOpen) draw();
|
|
971
|
+
},
|
|
821
972
|
setPrompt(label) {
|
|
822
973
|
promptLabel = label || '';
|
|
823
974
|
draw();
|
package/src/ui.js
CHANGED
|
@@ -26,6 +26,10 @@ export const c = {
|
|
|
26
26
|
ok: chalk.hex('#84a98c'),
|
|
27
27
|
err: chalk.hex('#c0524b'),
|
|
28
28
|
accent: chalk.hex('#ff7a45'),
|
|
29
|
+
// Tin nhắn user gửi: nền warm-gray đậm + chữ paper-white → khối phân biệt rõ
|
|
30
|
+
// trong scrollback (không lẫn với prose model amber). Gutter ember ▌ ở mép trái.
|
|
31
|
+
userBg: chalk.bgHex('#3a342c').hex('#f5f1e8'),
|
|
32
|
+
userGutter: chalk.hex('#ff7a45'),
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
// Single-tone "gradient" giữ tương thích API gradient-string nội bộ. Banner
|
|
@@ -42,15 +46,14 @@ const term = () => process.stdout.columns || 80;
|
|
|
42
46
|
// phân cách bằng ◆ ember nhỏ thay vì dấu · xám.
|
|
43
47
|
export function banner() {
|
|
44
48
|
const sidebar = chalk.hex('#ff7a45')('▌');
|
|
45
|
-
const wordmark = [
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'╹ ╹┗━┛┗━┛┗━┛',
|
|
49
|
-
].map((row) => sidebar + ' ' + brand(row)).join('\n');
|
|
49
|
+
const wordmark = ['┏┓╻┏━┓┏━┓┏┓ ', '┃┗┫┃ ┃┃ ┃┣┻┓', '╹ ╹┗━┛┗━┛┗━┛']
|
|
50
|
+
.map((row) => sidebar + ' ' + brand(row))
|
|
51
|
+
.join('\n');
|
|
50
52
|
const diamond = chalk.hex('#ff7a45')('◆');
|
|
51
53
|
const tag = chalk.hex('#e8e3d8').italic(t.tagline);
|
|
52
54
|
const meta = ' ' + c.dim('agentic terminal') + ' ' + diamond + ' ' + tag;
|
|
53
55
|
console.log('\n' + wordmark);
|
|
56
|
+
console.log('');
|
|
54
57
|
console.log(meta + '\n');
|
|
55
58
|
}
|
|
56
59
|
|
package/src/workflow-bg.js
CHANGED
|
@@ -30,6 +30,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
30
30
|
execTool,
|
|
31
31
|
spawnAgentToolsDoc,
|
|
32
32
|
cwd = process.cwd(),
|
|
33
|
+
tui = null, // route log sub-agent nền + cập nhật badge số agent nền (optional; null trong test)
|
|
33
34
|
} = deps;
|
|
34
35
|
const _runAgent = deps.runAgent || runAgent;
|
|
35
36
|
const _createRun = deps.createRun || createRun;
|
|
@@ -40,8 +41,14 @@ export function createBgRegistry(deps = {}) {
|
|
|
40
41
|
// Hàng đợi run đã settle, chờ main loop drain.
|
|
41
42
|
const completions = [];
|
|
42
43
|
|
|
43
|
-
const activeCount = () =>
|
|
44
|
-
|
|
44
|
+
const activeCount = () => [...runs.values()].filter((r) => r.status === 'running').length;
|
|
45
|
+
|
|
46
|
+
// Cập nhật badge số agent nền trên status bar (no-op nếu không có tui).
|
|
47
|
+
const syncBadge = () => {
|
|
48
|
+
try {
|
|
49
|
+
tui?.setBgAgents?.(activeCount());
|
|
50
|
+
} catch {}
|
|
51
|
+
};
|
|
45
52
|
|
|
46
53
|
// Kick off 1 bg workflow. Trả về { ok, id } hoặc { ok:false, error }.
|
|
47
54
|
function startBgWorkflow({ prompt, name = null } = {}) {
|
|
@@ -71,6 +78,10 @@ export function createBgRegistry(deps = {}) {
|
|
|
71
78
|
error: null,
|
|
72
79
|
};
|
|
73
80
|
runs.set(run.id, entry);
|
|
81
|
+
try {
|
|
82
|
+
tui?.pushAgentLog?.(`🎼 workflow nền bắt đầu — run ${run.id}${name ? ' · ' + name : ''}`);
|
|
83
|
+
} catch {}
|
|
84
|
+
syncBadge();
|
|
74
85
|
|
|
75
86
|
// workflowRun riêng cho bg dispatcher: journal cache dùng run.data này, KHÔNG
|
|
76
87
|
// đụng state.workflowRun của turn (turn có thể đang chạy /workflow blocking).
|
|
@@ -90,8 +101,9 @@ export function createBgRegistry(deps = {}) {
|
|
|
90
101
|
stopSpin: noop,
|
|
91
102
|
startSpin: noop,
|
|
92
103
|
execTool,
|
|
93
|
-
tui
|
|
104
|
+
tui, // sub-agent log → ring buffer (Ctrl+O), KHÔNG in ra UI chính
|
|
94
105
|
c: null,
|
|
106
|
+
bgAgentLog: true, // cờ: dispatcher route onLog → tui.pushAgentLog thay console.log
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
// Chạy headless. runAgent options TUI-bound (onStatus/onDelta/onSteer) bỏ —
|
|
@@ -111,18 +123,32 @@ export function createBgRegistry(deps = {}) {
|
|
|
111
123
|
.then((answer) => {
|
|
112
124
|
entry.status = 'done';
|
|
113
125
|
entry.result = typeof answer === 'string' ? answer : String(answer ?? '');
|
|
114
|
-
try {
|
|
126
|
+
try {
|
|
127
|
+
_closeRun(run.data, 'done', cwd);
|
|
128
|
+
} catch {}
|
|
129
|
+
try {
|
|
130
|
+
tui?.pushAgentLog?.(`✓ workflow nền xong — run ${run.id}`);
|
|
131
|
+
} catch {}
|
|
115
132
|
completions.push({ id: run.id, name, status: 'done', result: entry.result });
|
|
133
|
+
syncBadge();
|
|
116
134
|
})
|
|
117
135
|
.catch((err) => {
|
|
118
136
|
const isAbort = err?.message === 'aborted' || err?.name === 'AbortError';
|
|
119
137
|
entry.status = isAbort ? 'interrupted' : 'failed';
|
|
120
138
|
entry.error = err?.message || String(err);
|
|
121
|
-
try {
|
|
139
|
+
try {
|
|
140
|
+
_closeRun(run.data, entry.status, cwd);
|
|
141
|
+
} catch {}
|
|
142
|
+
try {
|
|
143
|
+
tui?.pushAgentLog?.(
|
|
144
|
+
`${isAbort ? '⊘ huỷ' : '✗ lỗi'} workflow nền — run ${run.id}${entry.error ? ': ' + entry.error : ''}`
|
|
145
|
+
);
|
|
146
|
+
} catch {}
|
|
122
147
|
// Abort do stopBg/sweepOnExit → KHÔNG enqueue completion (user chủ động huỷ).
|
|
123
148
|
if (!isAbort) {
|
|
124
149
|
completions.push({ id: run.id, name, status: entry.status, error: entry.error });
|
|
125
150
|
}
|
|
151
|
+
syncBadge();
|
|
126
152
|
});
|
|
127
153
|
entry.promise = promise;
|
|
128
154
|
|
|
@@ -143,6 +169,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
143
169
|
const entry = runs.get(id);
|
|
144
170
|
if (!entry || entry.status !== 'running') return false;
|
|
145
171
|
entry.controller.abort();
|
|
172
|
+
syncBadge();
|
|
146
173
|
return true;
|
|
147
174
|
}
|
|
148
175
|
|
|
@@ -157,6 +184,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
157
184
|
n++;
|
|
158
185
|
}
|
|
159
186
|
}
|
|
187
|
+
if (n) syncBadge();
|
|
160
188
|
return n;
|
|
161
189
|
}
|
|
162
190
|
|
package/src/workflow-runs.js
CHANGED
|
@@ -70,7 +70,10 @@ export function hashTask({ task, context, model }) {
|
|
|
70
70
|
|
|
71
71
|
// Tên file an toàn — chỉ [a-z0-9-_], thay ký tự không hợp lệ bằng `_`.
|
|
72
72
|
function sanitizeId(s) {
|
|
73
|
-
return String(s || '')
|
|
73
|
+
return String(s || '')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
76
|
+
.slice(0, 80);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function makeId(name) {
|