@noobdemon/noob-cli 1.11.1 → 1.12.0
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 +30 -0
- package/package.json +1 -1
- package/src/agent.js +7 -4
- package/src/api.js +61 -5
- package/src/diff.js +152 -0
- package/src/repl/commands/prompts.js +45 -0
- package/src/repl/permission.js +116 -0
- package/src/repl/state.js +33 -0
- package/src/repl/utils.js +64 -0
- package/src/repl/workflow-commands.js +3 -0
- package/src/repl.js +372 -202
- package/src/tokens.js +2 -2
- package/src/ui.js +92 -5
- package/src/workflow-runs.js +222 -0
package/src/repl.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createTui } from './tui.js';
|
|
|
6
6
|
import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from './agent.js';
|
|
7
7
|
import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from './subagent.js';
|
|
8
8
|
import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from './tokens.js';
|
|
9
|
-
import { stream, usage, ApiError, resetMemoryToken } from './api.js';
|
|
9
|
+
import { stream, usage, cachedUsage, resetUsageCache, ApiError, resetMemoryToken } from './api.js';
|
|
10
10
|
import {
|
|
11
11
|
runTool,
|
|
12
12
|
describe,
|
|
@@ -18,7 +18,17 @@ import {
|
|
|
18
18
|
nearestExistingDir,
|
|
19
19
|
} from './tools.js';
|
|
20
20
|
import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from './models.js';
|
|
21
|
-
import { c, banner, modelBadge, renderMarkdown, box } from './ui.js';
|
|
21
|
+
import { c, banner, modelBadge, renderMarkdown, renderInline, renderHeadingLine, renderBulletPrefix, box, formatQuota } from './ui.js';
|
|
22
|
+
import { renderUnifiedDiff, renderNewFilePreview } from './diff.js';
|
|
23
|
+
import {
|
|
24
|
+
askPermission as _askPermission,
|
|
25
|
+
askAddRoot as _askAddRoot,
|
|
26
|
+
askWorkflowAgentMode as _askWorkflowAgentMode,
|
|
27
|
+
} from './repl/permission.js';
|
|
28
|
+
import {
|
|
29
|
+
runImprove as _runImprove,
|
|
30
|
+
runKarpathy as _runKarpathy,
|
|
31
|
+
} from './repl/commands/prompts.js';
|
|
22
32
|
import { config } from './config.js';
|
|
23
33
|
import { loadMemory, memoryPath, memoryStats } from './memory.js';
|
|
24
34
|
import { t } from './i18n.js';
|
|
@@ -26,6 +36,17 @@ import { checkLatest, runUpdate, CURRENT } from './update.js';
|
|
|
26
36
|
import * as sessions from './sessions.js';
|
|
27
37
|
import { loadSkill, listSkills } from './skills.js';
|
|
28
38
|
import { saveWorkflow, loadWorkflow } from './workflows.js';
|
|
39
|
+
import {
|
|
40
|
+
createRun as createWorkflowRun,
|
|
41
|
+
loadRun as loadWorkflowRun,
|
|
42
|
+
listRuns as listWorkflowRuns,
|
|
43
|
+
closeRun as closeWorkflowRun,
|
|
44
|
+
hashTask as hashWorkflowTask,
|
|
45
|
+
lookupTaskResult as lookupWorkflowTaskResult,
|
|
46
|
+
recordTaskStart as recordWorkflowTaskStart,
|
|
47
|
+
recordTaskDone as recordWorkflowTaskDone,
|
|
48
|
+
recordTaskFailed as recordWorkflowTaskFailed,
|
|
49
|
+
} from './workflow-runs.js';
|
|
29
50
|
import { getBuiltinWorkflow, loadBuiltinPrompt } from './workflows-builtin.js';
|
|
30
51
|
import { SLASH, completeInput, mentionedFiles } from './repl/complete.js';
|
|
31
52
|
import { parseTodosFromHistory } from './repl/todos.js';
|
|
@@ -45,22 +66,12 @@ import {
|
|
|
45
66
|
workflowLoad as _workflowLoad,
|
|
46
67
|
workflowDelete as _workflowDelete,
|
|
47
68
|
} from './repl/workflow-commands.js';
|
|
69
|
+
import { createState } from './repl/state.js';
|
|
70
|
+
import {
|
|
71
|
+
shortCwd, shortPath, relTime, firstLine, truncate, fmtTime, fmtK, preview,
|
|
72
|
+
} from './repl/utils.js';
|
|
48
73
|
export async function startRepl(opts = {}) {
|
|
49
|
-
const state =
|
|
50
|
-
model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
|
|
51
|
-
mode: 'chat', // chat | merge | search
|
|
52
|
-
history: [],
|
|
53
|
-
autoApprove: new Set(),
|
|
54
|
-
yolo: !!opts.yolo || config.yoloDefault, // cờ --yolo HOẶC mặc định đã lưu (/auto-yolo)
|
|
55
|
-
ultra: false, // chế độ tự hành (self-quest) đang chạy?
|
|
56
|
-
agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
|
|
57
|
-
goal: null, // HARD GOAL (set qua /goal <text>) — inject vào mọi prompt tới khi /goal clear
|
|
58
|
-
loop: null, // /loop — {intervalMs, intervalStr, task, timer, ticks, startedAt} hoặc null
|
|
59
|
-
extraRoots: [], // thư mục ngoài cwd được /add-dir cấp quyền (UX display only;
|
|
60
|
-
// source of truth là extraRoots trong src/tools.js)
|
|
61
|
-
_longSessionWarned: false, // đã in cảnh báo phiên dài chưa (reset khi /clear)
|
|
62
|
-
todos: [], // [{text, done}] — todo list parse từ model output, render trên status bar
|
|
63
|
-
};
|
|
74
|
+
const state = createState(opts, config);
|
|
64
75
|
const tokenMeter = new TokenMeter();
|
|
65
76
|
|
|
66
77
|
// Set terminal title bar — hiện trên CMD/PowerShell.
|
|
@@ -367,7 +378,7 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
367
378
|
// match quan trọng: `help` / `?` / `patterns` / `builtins` / `list|ls` /
|
|
368
379
|
// `load` / `delete|rm` / `save` / `run`. Ad-hoc default = phần còn lại.
|
|
369
380
|
const m = trimmed.match(
|
|
370
|
-
/^(help|\?|patterns|builtins|list|ls|load|delete|rm|save|run)\b\s*([\s\S]*)$/i
|
|
381
|
+
/^(help|\?|patterns|builtins|list|ls|load|delete|rm|save|run|runs|log|resume)\b\s*([\s\S]*)$/i
|
|
371
382
|
);
|
|
372
383
|
if (m) {
|
|
373
384
|
const sub = m[1].toLowerCase();
|
|
@@ -380,6 +391,9 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
380
391
|
if (sub === 'delete' || sub === 'rm') return workflowDelete(rest);
|
|
381
392
|
if (sub === 'save') return workflowSave(rest);
|
|
382
393
|
if (sub === 'run') return workflowRun(rest);
|
|
394
|
+
if (sub === 'runs') return workflowRuns();
|
|
395
|
+
if (sub === 'log') return workflowLog(rest);
|
|
396
|
+
if (sub === 'resume') return workflowResume(rest);
|
|
383
397
|
}
|
|
384
398
|
// Default: ad-hoc workflow (giữ behavior cũ — model design workflow từ request).
|
|
385
399
|
await workflowExecute(trimmed);
|
|
@@ -388,39 +402,12 @@ Thực thi: đọc/tạo file cần thiết bằng tool, viết code production-
|
|
|
388
402
|
// Hỏi quyền bật agent mode để chạy workflow. CHỈ chấp nhận y/n (Enter = yes).
|
|
389
403
|
// Nếu nhận dòng lạ & dài (paste nhầm tin nhắn) → xếp hàng + hỏi lại (y hệt
|
|
390
404
|
// askPermission / askAddRoot) để user khỏi phải gõ lại /workflow.
|
|
391
|
-
|
|
392
|
-
tui.setBusy(false);
|
|
393
|
-
console.log(
|
|
394
|
-
c.tool(
|
|
395
|
-
' ' +
|
|
396
|
-
(t.workflowAgentAskHint || '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
397
|
-
)
|
|
398
|
-
);
|
|
399
|
-
try {
|
|
400
|
-
while (true) {
|
|
401
|
-
const raw = await ask(
|
|
402
|
-
c.tool(' bật agent mode và chạy workflow? ') +
|
|
403
|
-
c.dim('[y] có, bật & chạy / [n] huỷ (gõ /agent rồi chạy lại nếu muốn) › ')
|
|
404
|
-
);
|
|
405
|
-
if (raw == null) return 'n'; // stdin đóng thật
|
|
406
|
-
const a = raw.trim().toLowerCase();
|
|
407
|
-
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
408
|
-
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
409
|
-
if (raw.trim().length > 3) {
|
|
410
|
-
pending.push(raw);
|
|
411
|
-
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
412
|
-
}
|
|
413
|
-
console.log(c.dim(' → gõ y hoặc n'));
|
|
414
|
-
}
|
|
415
|
-
} finally {
|
|
416
|
-
tui.setBusy(true, t.thinking);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
405
|
+
const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
419
406
|
|
|
420
407
|
// Chạy thật workflow prompt — chia sẻ giữa ad-hoc và `run <name>`.
|
|
421
408
|
// `builtInName` (optional): nếu có thì SKIP loadSkill dynamic-workflows (prompt
|
|
422
409
|
// built-in đã hardcode pattern + step cụ thể rồi, không cần model design lại).
|
|
423
|
-
async function workflowExecute(userRequest, { builtInName = null } = {}) {
|
|
410
|
+
async function workflowExecute(userRequest, { builtInName = null, resumeRun = null } = {}) {
|
|
424
411
|
let prompt;
|
|
425
412
|
if (builtInName) {
|
|
426
413
|
// Built-in prompt đã đầy đủ PLAN + 4 bước thực thi — KHÔNG wrap thêm skill.
|
|
@@ -472,8 +459,57 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
472
459
|
c.tool(' ✓ ' + (t.workflowAgentEnabled || 'đã bật agent mode cho workflow này.'))
|
|
473
460
|
);
|
|
474
461
|
}
|
|
462
|
+
// Mở workflow run journal — track mỗi sub-agent task để resume sau interrupt.
|
|
463
|
+
// resumeRun: data đã load sẵn từ /workflow resume <id> → tái dùng id cũ.
|
|
464
|
+
// Còn lại: tạo run mới (ad-hoc hoặc built-in/saved).
|
|
465
|
+
try {
|
|
466
|
+
if (resumeRun) {
|
|
467
|
+
state.workflowRun = { id: resumeRun.id, data: resumeRun, path: null };
|
|
468
|
+
// Đánh dấu lại running (trước đó là interrupted/failed).
|
|
469
|
+
resumeRun.status = 'running';
|
|
470
|
+
const doneCount = (resumeRun.tasks || []).filter((tk) => tk.status === 'done').length;
|
|
471
|
+
console.log(
|
|
472
|
+
c.tool(
|
|
473
|
+
' 🎼 resume run ' +
|
|
474
|
+
c.accent(resumeRun.id) +
|
|
475
|
+
c.dim(` · ${doneCount}/${(resumeRun.tasks || []).length} task đã done sẽ được skip\n`)
|
|
476
|
+
)
|
|
477
|
+
);
|
|
478
|
+
} else {
|
|
479
|
+
const run = createWorkflowRun({
|
|
480
|
+
name: builtInName || 'adhoc',
|
|
481
|
+
workflowPrompt: prompt,
|
|
482
|
+
});
|
|
483
|
+
state.workflowRun = run;
|
|
484
|
+
console.log(c.dim(' 📓 workflow run journal: ' + run.id));
|
|
485
|
+
}
|
|
486
|
+
} catch (e) {
|
|
487
|
+
// Journal lỗi (vd EACCES /.noob/) → vẫn chạy workflow như cũ, mất tính năng resume thôi.
|
|
488
|
+
state.workflowRun = null;
|
|
489
|
+
console.log(c.dim(' (không khởi tạo được workflow journal: ' + (e?.message || e) + ')'));
|
|
490
|
+
}
|
|
475
491
|
console.log(c.tool(' 🎼 ' + (t.workflowRunning || 'Dynamic workflow running…')));
|
|
476
|
-
|
|
492
|
+
try {
|
|
493
|
+
await handle(prompt);
|
|
494
|
+
// handle() xong sạch → đóng journal với status done.
|
|
495
|
+
if (state.workflowRun) closeWorkflowRun(state.workflowRun.data, 'done');
|
|
496
|
+
} catch (err) {
|
|
497
|
+
// Interrupt (Ctrl+C) hoặc lỗi khác → đánh dấu interrupted, journal vẫn đầy đủ để resume.
|
|
498
|
+
if (state.workflowRun) {
|
|
499
|
+
const isAbort = err?.message === 'aborted' || err?.name === 'AbortError';
|
|
500
|
+
closeWorkflowRun(state.workflowRun.data, isAbort ? 'interrupted' : 'failed');
|
|
501
|
+
console.log(
|
|
502
|
+
c.tool(
|
|
503
|
+
' 📓 run ' +
|
|
504
|
+
c.accent(state.workflowRun.id) +
|
|
505
|
+
c.dim(' đã lưu — resume bằng `/workflow resume ' + state.workflowRun.id + '`')
|
|
506
|
+
)
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
throw err;
|
|
510
|
+
} finally {
|
|
511
|
+
state.workflowRun = null;
|
|
512
|
+
}
|
|
477
513
|
persist();
|
|
478
514
|
}
|
|
479
515
|
|
|
@@ -662,39 +698,83 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
662
698
|
await workflowExecute(userRequest);
|
|
663
699
|
}
|
|
664
700
|
|
|
665
|
-
// /
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
if (!
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
console.log(
|
|
674
|
-
|
|
675
|
-
|
|
701
|
+
// /workflow runs — list các workflow run journal trong workspace hiện tại.
|
|
702
|
+
function workflowRuns() {
|
|
703
|
+
const items = listWorkflowRuns(20);
|
|
704
|
+
if (!items.length) {
|
|
705
|
+
console.log(c.dim(' (chưa có workflow run nào trong workspace này)'));
|
|
706
|
+
console.log(c.dim(' Run đầu tiên được tạo khi bạn /workflow <yêu cầu> hoặc /workflow run <name>.'));
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
console.log('\n' + chalk.bold(' ' + (t.workflowRunsTitle || '📓 Workflow runs (workspace này)')));
|
|
710
|
+
for (const r of items) {
|
|
711
|
+
const statusColor =
|
|
712
|
+
r.status === 'done' ? c.ok :
|
|
713
|
+
r.status === 'interrupted' ? c.tool :
|
|
714
|
+
r.status === 'failed' ? c.err :
|
|
715
|
+
c.accent;
|
|
716
|
+
console.log(
|
|
717
|
+
c.dim(' ') +
|
|
718
|
+
c.accent(r.id.padEnd(40)) +
|
|
719
|
+
statusColor(r.status.padEnd(13)) +
|
|
720
|
+
c.dim(`${r.done}/${r.total} task done · ${relTime(r.updatedAt)}`)
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
console.log(
|
|
724
|
+
c.dim('\n /workflow log <id> xem chi tiết\n /workflow resume <id> chạy lại, skip task đã done\n')
|
|
725
|
+
);
|
|
676
726
|
}
|
|
677
727
|
|
|
678
|
-
// /
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
728
|
+
// /workflow log <id> — xem chi tiết 1 run: prompt, tasks, kết quả mỗi sub-agent.
|
|
729
|
+
function workflowLog(arg) {
|
|
730
|
+
if (!arg) return console.log(c.err(' Cách dùng: /workflow log <id> (xem id bằng /workflow runs)'));
|
|
731
|
+
const r = loadWorkflowRun(arg);
|
|
732
|
+
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
733
|
+
const d = r.data;
|
|
734
|
+
console.log('\n' + chalk.bold(' 📓 ' + d.id));
|
|
735
|
+
console.log(c.dim(` name: ${d.name || '(adhoc)'} · status: ${d.status} · started: ${relTime(d.startedAt)} · updated: ${relTime(d.updatedAt)}`));
|
|
736
|
+
console.log(c.dim(' workflow prompt: ' + truncate(d.workflowPrompt || '', 120)));
|
|
737
|
+
if (!d.tasks?.length) {
|
|
738
|
+
console.log(c.dim(' (chưa có sub-agent task nào được ghi nhận)'));
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
console.log('\n' + c.dim(' ── sub-agent tasks ──'));
|
|
742
|
+
d.tasks.forEach((tk, i) => {
|
|
743
|
+
const statusColor =
|
|
744
|
+
tk.status === 'done' ? c.ok :
|
|
745
|
+
tk.status === 'failed' ? c.err :
|
|
746
|
+
c.tool;
|
|
747
|
+
console.log(
|
|
748
|
+
c.dim(` #${i + 1} `) +
|
|
749
|
+
statusColor(tk.status.padEnd(8)) +
|
|
750
|
+
c.dim(tk.hash + ' ') +
|
|
751
|
+
c.accent(truncate(tk.task, 80))
|
|
752
|
+
);
|
|
753
|
+
if (tk.context) console.log(c.dim(' context: ' + truncate(tk.context, 80)));
|
|
754
|
+
if (tk.model) console.log(c.dim(' model: ' + tk.model));
|
|
755
|
+
if (tk.result) console.log(c.dim(' result: ' + truncate(tk.result.replace(/\s+/g, ' '), 120)));
|
|
756
|
+
if (tk.error) console.log(c.err(' error: ' + truncate(tk.error, 120)));
|
|
757
|
+
});
|
|
758
|
+
console.log('');
|
|
696
759
|
}
|
|
697
760
|
|
|
761
|
+
// /workflow resume <id> — chạy lại workflow với cùng prompt; dispatchTool tự
|
|
762
|
+
// hit cache trong journal cho mọi task đã done lần trước → tiết kiệm token.
|
|
763
|
+
async function workflowResume(arg) {
|
|
764
|
+
if (!arg) return console.log(c.err(' Cách dùng: /workflow resume <id> (xem id bằng /workflow runs)'));
|
|
765
|
+
const r = loadWorkflowRun(arg);
|
|
766
|
+
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
767
|
+
const d = r.data;
|
|
768
|
+
if (!d.workflowPrompt) return console.log(c.err(' Run này không có workflow prompt — không thể resume.'));
|
|
769
|
+
if (d.status === 'done') {
|
|
770
|
+
console.log(c.tool(' ⚠ run đã ở trạng thái DONE — vẫn resume nhưng có thể chạy lại task mới nếu model spawn khác.'));
|
|
771
|
+
}
|
|
772
|
+
await workflowExecute(d.workflowPrompt, { resumeRun: d });
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const runImprove = (arg) => _runImprove(arg, { config, c, t, handle, persist });
|
|
776
|
+
const runKarpathy = (arg) => _runKarpathy(arg, { config, c, t, handle, persist });
|
|
777
|
+
|
|
698
778
|
// ── ULTRA: chế độ tự hành (self-quest) ────────────────────────────────────
|
|
699
779
|
// Constants + helpers thuần + prompt templates đã tách sang src/repl/ultra.js.
|
|
700
780
|
// Phần state-heavy (runUltra loop) ở dưới giữ ở đây vì cần closure handle/persist/state.
|
|
@@ -1062,6 +1142,22 @@ NGUYÊN TẮC:
|
|
|
1062
1142
|
.catch(() => {});
|
|
1063
1143
|
}
|
|
1064
1144
|
|
|
1145
|
+
// Prefetch usage để status bar có quota ngay từ phút đầu. Cache TTL 90s ở
|
|
1146
|
+
// api.js → status bar tick mỗi giây chỉ đọc cachedUsage() sync, không gọi
|
|
1147
|
+
// gateway. Refresh nền mỗi 90s để số luôn tươi mà không spam.
|
|
1148
|
+
// Chạy hoàn toàn non-blocking; lỗi (offline, bad key) → bỏ qua, status bar
|
|
1149
|
+
// chỉ thiếu phần quota tail, không ảnh hưởng phần còn lại.
|
|
1150
|
+
let usageRefreshTimer = null;
|
|
1151
|
+
const refreshUsage = () => {
|
|
1152
|
+
if (!config.apiKey) return;
|
|
1153
|
+
usage({ force: true }).catch(() => {});
|
|
1154
|
+
};
|
|
1155
|
+
if (config.apiKey) {
|
|
1156
|
+
refreshUsage();
|
|
1157
|
+
usageRefreshTimer = setInterval(refreshUsage, 90_000);
|
|
1158
|
+
if (typeof usageRefreshTimer.unref === 'function') usageRefreshTimer.unref();
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1065
1161
|
// Khôi phục phiên theo cờ dòng lệnh, hoặc mở phiên mới.
|
|
1066
1162
|
if (opts.continue) {
|
|
1067
1163
|
const s = sessions.latest(process.cwd()); // phiên gần nhất CỦA workspace này
|
|
@@ -1120,6 +1216,7 @@ NGUYÊN TẮC:
|
|
|
1120
1216
|
}
|
|
1121
1217
|
}
|
|
1122
1218
|
exiting = true;
|
|
1219
|
+
if (usageRefreshTimer) clearInterval(usageRefreshTimer);
|
|
1123
1220
|
tui.close();
|
|
1124
1221
|
process.exit(0);
|
|
1125
1222
|
|
|
@@ -1140,7 +1237,9 @@ NGUYÊN TẮC:
|
|
|
1140
1237
|
// Nhờ vậy người dùng LUÔN thấy đồng hồ + token đang chạy, kể cả khi treo chờ y/n.
|
|
1141
1238
|
const tickMeta = () => {
|
|
1142
1239
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
1143
|
-
|
|
1240
|
+
const quota = formatQuota(cachedUsage());
|
|
1241
|
+
const tail = quota ? ` · ${quota}` : '';
|
|
1242
|
+
tui.setMeta(`${elapsed}s · ${tokenMeter.formatWithPct()}${tail}`);
|
|
1144
1243
|
};
|
|
1145
1244
|
const tick = (label) => {
|
|
1146
1245
|
tui.status(c.dim(`${label}…`));
|
|
@@ -1238,6 +1337,7 @@ NGUYÊN TẮC:
|
|
|
1238
1337
|
);
|
|
1239
1338
|
startSpin(t.thinking);
|
|
1240
1339
|
try {
|
|
1340
|
+
const runData = state.workflowRun?.data || null;
|
|
1241
1341
|
const results = await Promise.all(
|
|
1242
1342
|
tasks.map((task, i) => {
|
|
1243
1343
|
// Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
|
|
@@ -1252,10 +1352,59 @@ NGUYÊN TẮC:
|
|
|
1252
1352
|
} else
|
|
1253
1353
|
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
1254
1354
|
}
|
|
1255
|
-
|
|
1355
|
+
const taskBody = task?.task || task?.prompt || '';
|
|
1356
|
+
const taskCtx = task?.context || '';
|
|
1357
|
+
// Workflow journal: nếu đang trong run + task đã done lần trước → return
|
|
1358
|
+
// cached result, tiết kiệm token. Hash = crc32(task+ctx+model).
|
|
1359
|
+
if (runData) {
|
|
1360
|
+
const hash = hashWorkflowTask({ task: taskBody, context: taskCtx, model: subModel });
|
|
1361
|
+
const cached = lookupWorkflowTaskResult(runData, hash);
|
|
1362
|
+
if (cached !== null) {
|
|
1363
|
+
stopSpin();
|
|
1364
|
+
console.log(
|
|
1365
|
+
chalk.hex('#8b5cf6')(
|
|
1366
|
+
` ⊘ sub-agent #${i + 1}${modelTag} skip — đã done trong run trước (cached)`
|
|
1367
|
+
)
|
|
1368
|
+
);
|
|
1369
|
+
startSpin(t.thinking);
|
|
1370
|
+
return Promise.resolve(
|
|
1371
|
+
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
1372
|
+
);
|
|
1373
|
+
}
|
|
1374
|
+
recordWorkflowTaskStart(runData, {
|
|
1375
|
+
hash,
|
|
1376
|
+
task: taskBody,
|
|
1377
|
+
context: taskCtx,
|
|
1378
|
+
model: subModel,
|
|
1379
|
+
});
|
|
1380
|
+
// [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
|
|
1381
|
+
return runSubAgent({
|
|
1382
|
+
task: taskBody,
|
|
1383
|
+
context: taskCtx,
|
|
1384
|
+
depth: depth + 1,
|
|
1385
|
+
model: subModel,
|
|
1386
|
+
signal: abort.signal,
|
|
1387
|
+
tokenMeter,
|
|
1388
|
+
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
1389
|
+
onLog: (msg) => {
|
|
1390
|
+
stopSpin();
|
|
1391
|
+
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
1392
|
+
startSpin(t.thinking);
|
|
1393
|
+
},
|
|
1394
|
+
})
|
|
1395
|
+
.then((r) => {
|
|
1396
|
+
recordWorkflowTaskDone(runData, hash, r);
|
|
1397
|
+
return `── sub-agent #${i + 1}${modelTag} ──\n${r}`;
|
|
1398
|
+
})
|
|
1399
|
+
.catch((e) => {
|
|
1400
|
+
recordWorkflowTaskFailed(runData, hash, e);
|
|
1401
|
+
return `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`;
|
|
1402
|
+
});
|
|
1403
|
+
}
|
|
1404
|
+
// Không có active workflow run → behavior cũ.
|
|
1256
1405
|
return runSubAgent({
|
|
1257
|
-
task:
|
|
1258
|
-
context:
|
|
1406
|
+
task: taskBody,
|
|
1407
|
+
context: taskCtx,
|
|
1259
1408
|
depth: depth + 1,
|
|
1260
1409
|
model: subModel,
|
|
1261
1410
|
signal: abort.signal,
|
|
@@ -1335,15 +1484,18 @@ NGUYÊN TẮC:
|
|
|
1335
1484
|
abort = null;
|
|
1336
1485
|
tui.setBusy(false);
|
|
1337
1486
|
// Auto-compact dựa trên context tokens thay vì chars.
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1340
|
-
//
|
|
1487
|
+
// Với CONTEXT_WINDOW = 2M tokens (xem src/tokens.js):
|
|
1488
|
+
// 75% (1.5M tokens) → auto compact
|
|
1489
|
+
// 60% (1.2M tokens) → cảnh báo mạnh
|
|
1490
|
+
// 40% (800k tokens) → nhắc nhẹ
|
|
1491
|
+
// Ngưỡng kéo xuống vì model context dài hiện tại để 80% mới compact thì
|
|
1492
|
+
// mỗi lượt cuối đã ăn 200k+ tokens — auto-compact sớm hơn giữ phiên mượt.
|
|
1341
1493
|
try {
|
|
1342
1494
|
const totalTokens = countMessages(state.history);
|
|
1343
1495
|
const k = Math.round(totalTokens / 1000);
|
|
1344
1496
|
const pct = Math.round((totalTokens / CONTEXT_WINDOW) * 100);
|
|
1345
|
-
// Mốc 3 (≥
|
|
1346
|
-
if (totalTokens >= CONTEXT_WINDOW * 0.
|
|
1497
|
+
// Mốc 3 (≥75% — 1.5M tokens): TỰ ĐỘNG compact.
|
|
1498
|
+
if (totalTokens >= CONTEXT_WINDOW * 0.75 && !state._autoCompacting) {
|
|
1347
1499
|
state._autoCompacting = true;
|
|
1348
1500
|
console.log(c.accent(` ⚡ ${t.autoCompactTrigger(k)} (${pct}% context)`));
|
|
1349
1501
|
tui.setBusy(true, t.compactRunning);
|
|
@@ -1371,12 +1523,12 @@ NGUYÊN TẮC:
|
|
|
1371
1523
|
} finally {
|
|
1372
1524
|
state._autoCompacting = false;
|
|
1373
1525
|
}
|
|
1374
|
-
} else if (totalTokens >= CONTEXT_WINDOW * 0.
|
|
1375
|
-
// Mốc 2 (≥
|
|
1526
|
+
} else if (totalTokens >= CONTEXT_WINDOW * 0.6) {
|
|
1527
|
+
// Mốc 2 (≥60% — 1.2M tokens): cảnh báo mạnh.
|
|
1376
1528
|
console.log(c.err(` ⚠ ${t.veryLongSession(k)} (${pct}% context)`));
|
|
1377
1529
|
state._longSessionWarned = true;
|
|
1378
|
-
} else if (totalTokens >= CONTEXT_WINDOW * 0.
|
|
1379
|
-
// Mốc 1 (≥
|
|
1530
|
+
} else if (totalTokens >= CONTEXT_WINDOW * 0.4 && !state._longSessionWarned) {
|
|
1531
|
+
// Mốc 1 (≥40% — 800k tokens): nhắc nhẹ một lần.
|
|
1380
1532
|
console.log(c.dim(` ⓘ ${t.longSession(k)} (${pct}% context)`));
|
|
1381
1533
|
state._longSessionWarned = true;
|
|
1382
1534
|
}
|
|
@@ -1393,9 +1545,52 @@ NGUYÊN TẮC:
|
|
|
1393
1545
|
const color = name === 'run_command' ? '#ef4444' : '#f59e0b';
|
|
1394
1546
|
console.log('\n' + chalk.hex(color)(' ⚙ ' + name) + c.dim(' ' + desc));
|
|
1395
1547
|
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1548
|
+
// Diff preview — show user CHÍNH XÁC dòng nào bị xoá/thêm trước khi approve.
|
|
1549
|
+
// Tránh tin model mù: bắt được hallucinate nội dung sớm.
|
|
1550
|
+
if (name === 'edit_file' && typeof input.old_string === 'string' && typeof input.new_string === 'string') {
|
|
1551
|
+
try {
|
|
1552
|
+
const filePath = path.resolve(process.cwd(), input.path || '');
|
|
1553
|
+
let oldText = input.old_string;
|
|
1554
|
+
let newText = input.new_string;
|
|
1555
|
+
// Cố gắng đọc file gốc + locate old_string để show diff CỦA CẢ FILE
|
|
1556
|
+
// (vài dòng context xung quanh). Nếu old_string không match đúng (vd model
|
|
1557
|
+
// dùng escape khác), fallback về diff trực tiếp old_string vs new_string.
|
|
1558
|
+
if (fs.existsSync(filePath)) {
|
|
1559
|
+
const fileContent = fs.readFileSync(filePath, 'utf8');
|
|
1560
|
+
const idx = fileContent.indexOf(input.old_string);
|
|
1561
|
+
if (idx !== -1) {
|
|
1562
|
+
// Full file before + after thay đổi → diff sẽ render đúng hunk
|
|
1563
|
+
// có vài dòng context xung quanh sửa đổi (chuẩn unified diff).
|
|
1564
|
+
oldText = fileContent;
|
|
1565
|
+
if (input.replace_all) {
|
|
1566
|
+
newText = fileContent.split(input.old_string).join(input.new_string);
|
|
1567
|
+
} else {
|
|
1568
|
+
const before = fileContent.slice(0, idx);
|
|
1569
|
+
const after = fileContent.slice(idx + input.old_string.length);
|
|
1570
|
+
newText = before + input.new_string + after;
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
console.log(renderUnifiedDiff(oldText, newText, { label: input.path || '(unknown path)' }));
|
|
1575
|
+
} catch (e) {
|
|
1576
|
+
// Fallback nếu đọc file lỗi: ít nhất show old vs new raw.
|
|
1577
|
+
console.log(renderUnifiedDiff(input.old_string, input.new_string, { label: input.path || '(unknown path)' }));
|
|
1578
|
+
}
|
|
1579
|
+
} else if (name === 'write_file' && typeof input.content === 'string') {
|
|
1580
|
+
try {
|
|
1581
|
+
const filePath = path.resolve(process.cwd(), input.path || '');
|
|
1582
|
+
if (fs.existsSync(filePath)) {
|
|
1583
|
+
// File đã tồn tại → write_file là OVERWRITE → show diff old vs new.
|
|
1584
|
+
const oldContent = fs.readFileSync(filePath, 'utf8');
|
|
1585
|
+
console.log(renderUnifiedDiff(oldContent, input.content, { label: input.path + ' (OVERWRITE)', maxLines: 60 }));
|
|
1586
|
+
} else {
|
|
1587
|
+
// File mới → preview top 20 dòng + tổng số dòng.
|
|
1588
|
+
console.log(renderNewFilePreview(input.content, { label: input.path || '(unknown path)', maxLines: 20 }));
|
|
1589
|
+
}
|
|
1590
|
+
} catch {
|
|
1591
|
+
console.log(renderNewFilePreview(input.content, { label: input.path || '(unknown path)', maxLines: 20 }));
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1399
1594
|
|
|
1400
1595
|
if (DESTRUCTIVE.has(name) && !state.yolo && !state.autoApprove.has(name)) {
|
|
1401
1596
|
const a = await askPermission(name);
|
|
@@ -1450,66 +1645,14 @@ NGUYÊN TẮC:
|
|
|
1450
1645
|
// Hỏi user có muốn cấp quyền folder ngoài workspace cho tool call hiện tại
|
|
1451
1646
|
// hay không. Trả về "y" | "n" | "a" (luôn). y = chỉ lần này; a = auto-approve
|
|
1452
1647
|
// mọi add-root trong phiên. Cùng style retry với askPermission (lạc → queue).
|
|
1453
|
-
|
|
1454
|
-
tui.setBusy(false);
|
|
1455
|
-
console.log(c.tool(' ⏸ Cần cấp quyền folder: ') + c.accent(root));
|
|
1456
|
-
console.log(c.dim(' (model muốn truy cập: ' + targetPath + ')'));
|
|
1457
|
-
try {
|
|
1458
|
-
while (true) {
|
|
1459
|
-
const raw = await ask(
|
|
1460
|
-
c.tool(' cho phép? ') +
|
|
1461
|
-
c.dim('[y] thêm vào scope lần này / [a] luôn thêm / [n] từ chối › ')
|
|
1462
|
-
);
|
|
1463
|
-
if (raw == null) return 'n';
|
|
1464
|
-
const a = raw.trim().toLowerCase();
|
|
1465
|
-
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
1466
|
-
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
1467
|
-
if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
|
|
1468
|
-
if (raw.trim().length > 3) {
|
|
1469
|
-
pending.push(raw);
|
|
1470
|
-
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
1471
|
-
}
|
|
1472
|
-
console.log(c.dim(' → gõ y / n / a'));
|
|
1473
|
-
}
|
|
1474
|
-
} finally {
|
|
1475
|
-
tui.setBusy(true, t.thinking);
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1648
|
+
const askAddRoot = (root, targetPath) => _askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
1478
1649
|
|
|
1479
1650
|
// Đọc câu trả lời cho phép một cách CHẮC CHẮN. Chỉ chấp nhận y/n/a (hoặc
|
|
1480
1651
|
// Enter = đồng ý). Nếu nhận một dòng lạ & dài — gần như chắc là một tin nhắn
|
|
1481
1652
|
// bị paste/gõ nhầm vào đúng lúc prompt hiện ra (đây là thủ phạm làm hỏng &
|
|
1482
1653
|
// "tự tắt" trước đây) — thì KHÔNG coi là từ chối: xếp nó vào hàng đợi tin
|
|
1483
1654
|
// nhắn rồi HỎI LẠI. Nhờ vậy không thao tác nào bị quyết định bởi rác.
|
|
1484
|
-
|
|
1485
|
-
// Tắt spinner "đang chạy" trong lúc chờ duyệt. Nếu để nguyên, thanh dưới hiện
|
|
1486
|
-
// "đang chạy · Ctrl+C để dừng" → người dùng tưởng đang bận, không biết phải gõ
|
|
1487
|
-
// y/n nên lượt TREO. Báo bằng 1 dòng cố định (vào scrollback, không bị vẽ đè)
|
|
1488
|
-
// + bỏ spinner để prompt nổi bật. finally khôi phục trạng thái chạy.
|
|
1489
|
-
tui.setBusy(false);
|
|
1490
|
-
console.log(
|
|
1491
|
-
c.tool(' ⏸ Cần quyền: ' + name) + c.dim(' — gõ y (đồng ý) / n (từ chối) / a (luôn cho phép)')
|
|
1492
|
-
);
|
|
1493
|
-
try {
|
|
1494
|
-
while (true) {
|
|
1495
|
-
const raw = await ask(
|
|
1496
|
-
c.tool(' cho phép? ') + c.dim('[y] có / [n] không / [a] luôn ' + name + ' › ')
|
|
1497
|
-
);
|
|
1498
|
-
if (raw == null) return 'n'; // stdin đóng thật
|
|
1499
|
-
const a = raw.trim().toLowerCase();
|
|
1500
|
-
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
1501
|
-
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
1502
|
-
if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
|
|
1503
|
-
if (raw.trim().length > 3) {
|
|
1504
|
-
pending.push(raw); // tin nhắn lạc → xếp hàng, gửi sau khi xong lượt
|
|
1505
|
-
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
1506
|
-
}
|
|
1507
|
-
console.log(c.dim(' ' + t.permRetry));
|
|
1508
|
-
}
|
|
1509
|
-
} finally {
|
|
1510
|
-
tui.setBusy(true, t.thinking); // khôi phục "đang chạy" cho phần còn lại của lượt
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1655
|
+
const askPermission = (name) => _askPermission(name, { tui, ask, pending, c, t, truncate });
|
|
1513
1656
|
|
|
1514
1657
|
// ── slash commands ─────────────────────────────────────────────────────
|
|
1515
1658
|
async function command(input) {
|
|
@@ -1638,6 +1781,7 @@ NGUYÊN TẮC:
|
|
|
1638
1781
|
break;
|
|
1639
1782
|
case 'logout':
|
|
1640
1783
|
config.clearKey();
|
|
1784
|
+
resetUsageCache();
|
|
1641
1785
|
console.log(c.ok(' ' + t.loggedOut));
|
|
1642
1786
|
break;
|
|
1643
1787
|
case 'usage':
|
|
@@ -1770,6 +1914,7 @@ NGUYÊN TẮC:
|
|
|
1770
1914
|
function doLogin(key) {
|
|
1771
1915
|
if (!key) return console.log(c.err(' ' + t.needKeyArg));
|
|
1772
1916
|
config.setKey(key);
|
|
1917
|
+
resetUsageCache();
|
|
1773
1918
|
console.log(c.ok(' ✓ ') + c.dim(t.loginSaved(config.path)));
|
|
1774
1919
|
showUsage().catch(() => {});
|
|
1775
1920
|
}
|
|
@@ -1778,7 +1923,7 @@ NGUYÊN TẮC:
|
|
|
1778
1923
|
if (!config.apiKey) return console.log(c.tool(' ' + t.notLoggedIn));
|
|
1779
1924
|
tui.status(c.dim(' ...'));
|
|
1780
1925
|
try {
|
|
1781
|
-
const u = await usage();
|
|
1926
|
+
const u = await usage({ force: true });
|
|
1782
1927
|
tui.status(null);
|
|
1783
1928
|
if (!u.ok) return printError(new ApiError(t.errInvalidKey, { code: u.error }));
|
|
1784
1929
|
printUsage(u);
|
|
@@ -1857,13 +2002,6 @@ NGUYÊN TẮC:
|
|
|
1857
2002
|
}
|
|
1858
2003
|
|
|
1859
2004
|
// ── presentation helpers ───────────────────────────────────────────────────
|
|
1860
|
-
function fmtK(n) {
|
|
1861
|
-
return n >= 1000000
|
|
1862
|
-
? (n / 1000000).toFixed(1) + 'M'
|
|
1863
|
-
: n >= 1000
|
|
1864
|
-
? (n / 1000).toFixed(1) + 'k'
|
|
1865
|
-
: String(n);
|
|
1866
|
-
}
|
|
1867
2005
|
function printAnswer(text, name, color) {
|
|
1868
2006
|
if (!text?.trim()) return;
|
|
1869
2007
|
console.log('\n' + chalk.hex(color).bold(' ● ' + name));
|
|
@@ -1875,26 +2013,61 @@ function printAnswer(text, name, color) {
|
|
|
1875
2013
|
);
|
|
1876
2014
|
}
|
|
1877
2015
|
|
|
1878
|
-
//
|
|
1879
|
-
//
|
|
1880
|
-
//
|
|
1881
|
-
//
|
|
2016
|
+
// Stream printer với inline markdown rendering. Chiến lược:
|
|
2017
|
+
// - Tích đầy đủ MỘT dòng (\n) rồi mới render & write — vậy `**bold**`, backtick,
|
|
2018
|
+
// và heading `## foo` không bị cắt giữa chừng giữa các token.
|
|
2019
|
+
// - Trên dòng đang dở (chưa thấy \n), giữ trong buffer; cuối cùng `flush()` xử lý.
|
|
2020
|
+
// - Block code fence (```lang ... ```) bypass renderer: in nguyên xi giữa cặp ```.
|
|
2021
|
+
// - ```tool trở đi: nuốt sạch (sẽ render riêng qua tool dispatcher).
|
|
2022
|
+
function renderStreamLine(line, inCodeFence) {
|
|
2023
|
+
if (inCodeFence) return line; // code fence body: in raw, không parse markdown.
|
|
2024
|
+
const heading = renderHeadingLine(line);
|
|
2025
|
+
if (heading !== null) return heading;
|
|
2026
|
+
return renderInline(renderBulletPrefix(line));
|
|
2027
|
+
}
|
|
1882
2028
|
function makeStreamPrinter(name, color) {
|
|
1883
|
-
let buf = '';
|
|
1884
|
-
let printed = 0;
|
|
2029
|
+
let buf = ''; // toàn bộ delta đã nhận (chưa cắt)
|
|
2030
|
+
let printed = 0; // offset đã in trong buf
|
|
1885
2031
|
let suppress = false;
|
|
1886
2032
|
let started = false;
|
|
1887
2033
|
let header = false;
|
|
1888
|
-
|
|
1889
|
-
const
|
|
2034
|
+
let inCodeFence = false;
|
|
2035
|
+
const HOLD = 8; // giữ đuôi 8 char để phát hiện sớm "```tool"
|
|
2036
|
+
const emit = (s) => {
|
|
1890
2037
|
if (!s) return;
|
|
1891
2038
|
if (!header) {
|
|
1892
2039
|
process.stdout.write('\n' + chalk.hex(color).bold(' ● ' + name) + '\n ');
|
|
1893
2040
|
header = true;
|
|
1894
2041
|
}
|
|
2042
|
+
// s có thể chứa \n từ render line + leading newline gốc. Thay \n → \n để indent.
|
|
1895
2043
|
process.stdout.write(s.replace(/\n/g, '\n '));
|
|
1896
2044
|
started = true;
|
|
1897
2045
|
};
|
|
2046
|
+
// Render & emit từ printed → end. Chỉ flush các dòng đã HOÀN CHỈNH (có \n).
|
|
2047
|
+
// Dòng cuối chưa có \n: chừa lại trong buf cho lần push tiếp theo.
|
|
2048
|
+
const flushCompleteLines = (end) => {
|
|
2049
|
+
const slice = buf.slice(printed, end);
|
|
2050
|
+
if (!slice) return;
|
|
2051
|
+
const lastNl = slice.lastIndexOf('\n');
|
|
2052
|
+
if (lastNl === -1) return; // chưa có dòng nào hoàn chỉnh trong khoảng này
|
|
2053
|
+
const complete = slice.slice(0, lastNl + 1); // bao gồm \n cuối
|
|
2054
|
+
printed += lastNl + 1;
|
|
2055
|
+
// Render từng dòng (split giữ \n)
|
|
2056
|
+
const lines = complete.split('\n');
|
|
2057
|
+
// split với chuỗi kết thúc \n → mảng cuối là '' — bỏ qua
|
|
2058
|
+
const rendered = lines.map((ln, i) => {
|
|
2059
|
+
if (i === lines.length - 1 && ln === '') return '';
|
|
2060
|
+
// Toggle code fence khi gặp ``` ở đầu dòng (allow indent).
|
|
2061
|
+
const fenceMatch = ln.match(/^\s*```/);
|
|
2062
|
+
if (fenceMatch) {
|
|
2063
|
+
const out = renderStreamLine(ln, inCodeFence);
|
|
2064
|
+
inCodeFence = !inCodeFence;
|
|
2065
|
+
return out;
|
|
2066
|
+
}
|
|
2067
|
+
return renderStreamLine(ln, inCodeFence);
|
|
2068
|
+
}).join('\n');
|
|
2069
|
+
emit(rendered);
|
|
2070
|
+
};
|
|
1898
2071
|
return {
|
|
1899
2072
|
get started() {
|
|
1900
2073
|
return started;
|
|
@@ -1907,19 +2080,49 @@ function makeStreamPrinter(name, color) {
|
|
|
1907
2080
|
if (suppress) return;
|
|
1908
2081
|
const f = buf.indexOf('```tool');
|
|
1909
2082
|
if (f !== -1) {
|
|
1910
|
-
|
|
2083
|
+
// Flush mọi thứ TRƯỚC ```tool (full lines + phần dở của dòng cuối cũng in luôn
|
|
2084
|
+
// vì sắp suppress, không cần chừa buffer nữa).
|
|
2085
|
+
const beforeTool = buf.slice(printed, f);
|
|
2086
|
+
if (beforeTool) {
|
|
2087
|
+
// Render từng dòng kể cả dòng dở cuối
|
|
2088
|
+
const parts = beforeTool.split('\n');
|
|
2089
|
+
const rendered = parts.map((ln) => {
|
|
2090
|
+
const fenceMatch = ln.match(/^\s*```/);
|
|
2091
|
+
if (fenceMatch) {
|
|
2092
|
+
const out = renderStreamLine(ln, inCodeFence);
|
|
2093
|
+
inCodeFence = !inCodeFence;
|
|
2094
|
+
return out;
|
|
2095
|
+
}
|
|
2096
|
+
return renderStreamLine(ln, inCodeFence);
|
|
2097
|
+
}).join('\n');
|
|
2098
|
+
emit(rendered);
|
|
2099
|
+
}
|
|
1911
2100
|
printed = buf.length;
|
|
1912
2101
|
suppress = true;
|
|
1913
2102
|
return;
|
|
1914
2103
|
}
|
|
2104
|
+
// Chỉ flush các dòng đã hoàn chỉnh; chừa đuôi HOLD char để phát hiện ```tool.
|
|
1915
2105
|
const safeEnd = Math.max(printed, buf.length - HOLD);
|
|
1916
|
-
if (safeEnd > printed)
|
|
1917
|
-
write(buf.slice(printed, safeEnd));
|
|
1918
|
-
printed = safeEnd;
|
|
1919
|
-
}
|
|
2106
|
+
if (safeEnd > printed) flushCompleteLines(safeEnd);
|
|
1920
2107
|
},
|
|
1921
2108
|
flush() {
|
|
1922
|
-
if (!suppress && printed < buf.length)
|
|
2109
|
+
if (!suppress && printed < buf.length) {
|
|
2110
|
+
// Render phần còn lại (kể cả dòng cuối chưa có \n).
|
|
2111
|
+
const tail = buf.slice(printed);
|
|
2112
|
+
const parts = tail.split('\n');
|
|
2113
|
+
const rendered = parts.map((ln, i) => {
|
|
2114
|
+
// Toggle fence
|
|
2115
|
+
const fenceMatch = ln.match(/^\s*```/);
|
|
2116
|
+
if (fenceMatch) {
|
|
2117
|
+
const out = renderStreamLine(ln, inCodeFence);
|
|
2118
|
+
inCodeFence = !inCodeFence;
|
|
2119
|
+
return out;
|
|
2120
|
+
}
|
|
2121
|
+
return renderStreamLine(ln, inCodeFence);
|
|
2122
|
+
}).join('\n');
|
|
2123
|
+
emit(rendered);
|
|
2124
|
+
printed = buf.length;
|
|
2125
|
+
}
|
|
1923
2126
|
if (started) process.stdout.write('\n');
|
|
1924
2127
|
},
|
|
1925
2128
|
};
|
|
@@ -2027,37 +2230,4 @@ function listModels() {
|
|
|
2027
2230
|
console.log('\n' + lines.join('\n') + c.dim('\n\n ' + t.modelListHint) + '\n');
|
|
2028
2231
|
}
|
|
2029
2232
|
|
|
2030
|
-
const shortCwd = () => {
|
|
2031
|
-
const p = process.cwd();
|
|
2032
|
-
return p.length > 48 ? '…' + p.slice(-47) : p;
|
|
2033
|
-
};
|
|
2034
|
-
const shortPath = (p = '') => (p.length > 30 ? '…' + p.slice(-29) : p);
|
|
2035
|
-
const relTime = (ts) => {
|
|
2036
|
-
const m = Math.round((Date.now() - ts) / 60000);
|
|
2037
|
-
if (m < 1) return 'vừa xong';
|
|
2038
|
-
if (m < 60) return m + ' phút trước';
|
|
2039
|
-
const h = Math.round(m / 60);
|
|
2040
|
-
if (h < 24) return h + ' giờ trước';
|
|
2041
|
-
return Math.round(h / 24) + ' ngày trước';
|
|
2042
|
-
};
|
|
2043
|
-
const firstLine = (s) => (s.split('\n')[0] || '').slice(0, 100);
|
|
2044
|
-
const truncate = (s = '', n = 120) => (s.length > n ? s.slice(0, n) + '…' : s).replace(/\n/g, '⏎');
|
|
2045
|
-
const fmtTime = (iso) => {
|
|
2046
|
-
try {
|
|
2047
|
-
return new Date(iso).toLocaleString('vi-VN');
|
|
2048
|
-
} catch {
|
|
2049
|
-
return iso;
|
|
2050
|
-
}
|
|
2051
|
-
};
|
|
2052
2233
|
|
|
2053
|
-
function preview(content, label) {
|
|
2054
|
-
const lines = content.split('\n').slice(0, 12);
|
|
2055
|
-
const more = content.split('\n').length - lines.length;
|
|
2056
|
-
console.log(
|
|
2057
|
-
c.dim(' ┌─ ' + (label || '')) +
|
|
2058
|
-
'\n' +
|
|
2059
|
-
lines.map((l) => c.dim(' │ ') + l.slice(0, 110)).join('\n') +
|
|
2060
|
-
(more > 0 ? c.dim(`\n │ … +${more} dòng nữa`) : '') +
|
|
2061
|
-
c.dim('\n └─')
|
|
2062
|
-
);
|
|
2063
|
-
}
|