@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/src/repl.js CHANGED
@@ -5,8 +5,22 @@ import chalk from 'chalk';
5
5
  import { createTui } from './tui.js';
6
6
  import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from './agent.js';
7
7
  import { spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from './subagent.js';
8
- import { TokenMeter, countMessages, CONTEXT_WINDOW, countTokens } from './tokens.js';
9
- import { stream, usage, cachedUsage, resetUsageCache, evaluateQuotaWarning, ApiError, resetMemoryToken } from './api.js';
8
+ import {
9
+ TokenMeter,
10
+ countMessages,
11
+ CONTEXT_WINDOW,
12
+ SEND_LIMIT_TOKENS,
13
+ countTokens,
14
+ } from './tokens.js';
15
+ import {
16
+ stream,
17
+ usage,
18
+ cachedUsage,
19
+ resetUsageCache,
20
+ evaluateQuotaWarning,
21
+ ApiError,
22
+ resetMemoryToken,
23
+ } from './api.js';
10
24
  import {
11
25
  runTool,
12
26
  describe,
@@ -26,10 +40,7 @@ import {
26
40
  askAddRoot as _askAddRoot,
27
41
  askWorkflowAgentMode as _askWorkflowAgentMode,
28
42
  } from './repl/permission.js';
29
- import {
30
- runImprove as _runImprove,
31
- runKarpathy as _runKarpathy,
32
- } from './repl/commands/prompts.js';
43
+ import { runImprove as _runImprove, runKarpathy as _runKarpathy } from './repl/commands/prompts.js';
33
44
  import { config } from './config.js';
34
45
  import { loadMemory, memoryPath, memoryStats } from './memory.js';
35
46
  import { t } from './i18n.js';
@@ -79,7 +90,14 @@ import {
79
90
  } from './repl/workflow-commands.js';
80
91
  import { createState } from './repl/state.js';
81
92
  import {
82
- shortCwd, shortPath, relTime, firstLine, truncate, fmtTime, fmtK, preview,
93
+ shortCwd,
94
+ shortPath,
95
+ relTime,
96
+ firstLine,
97
+ truncate,
98
+ fmtTime,
99
+ fmtK,
100
+ preview,
83
101
  } from './repl/utils.js';
84
102
  import { createAgentDispatcher } from './repl/agent-dispatch.js';
85
103
  import { createBgRegistry } from './workflow-bg.js';
@@ -142,6 +160,14 @@ export async function startRepl(opts = {}) {
142
160
  tui.print(state.yolo ? c.err(' ' + t.yoloOn) : c.ok(' ' + t.yoloOff));
143
161
  tui.setPrompt(promptStr(false));
144
162
  },
163
+ onCtrlT: () => {
164
+ // Cycle build → plan → compose. UI hint mode (chỉ system-prompt hint).
165
+ const order = ['build', 'plan', 'compose'];
166
+ const next = order[(order.indexOf(state.agentUiMode) + 1) % order.length];
167
+ state.agentUiMode = next;
168
+ tui.setAgentMode(next);
169
+ tui.print(c.accent(' ◆ mode: ') + next);
170
+ },
145
171
  completer: completeInput,
146
172
  });
147
173
 
@@ -165,8 +191,7 @@ export async function startRepl(opts = {}) {
165
191
  _askPermission(name, { tui, ask, pending, c, t, truncate, targetPath });
166
192
  const askAddRoot = (root, targetPath) =>
167
193
  _askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
168
- const askWorkflowAgentMode = () =>
169
- _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
194
+ const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
170
195
 
171
196
  // ── quota soft-cap guard ───────────────────────────────────────────
172
197
  // Trước khi start ULTRA / loop / workflow (3 lệnh ăn nhiều request nhất),
@@ -232,6 +257,7 @@ export async function startRepl(opts = {}) {
232
257
  abort = null;
233
258
  if (state.ultra) {
234
259
  state.ultra = false; // Ctrl+C cũng dừng vòng tự hành, không chỉ lượt hiện tại
260
+ tui.setUltra(false); // tắt badge ULTRA trên status bar
235
261
  console.log(c.tool(' ' + t.ultraStopped));
236
262
  }
237
263
  if (pending.length) {
@@ -256,7 +282,14 @@ export async function startRepl(opts = {}) {
256
282
  // Bg workflow đang chạy → abort + mark interrupted (journal resume-able).
257
283
  const swept = bgRegistry.sweepOnExit();
258
284
  if (swept > 0)
259
- console.log(c.dim(' ' + (t.bgWorkflowSweep ? t.bgWorkflowSweep(swept) : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)));
285
+ console.log(
286
+ c.dim(
287
+ ' ' +
288
+ (t.bgWorkflowSweep
289
+ ? t.bgWorkflowSweep(swept)
290
+ : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
291
+ )
292
+ );
260
293
  persist();
261
294
  console.log(c.dim('\n ' + t.bye));
262
295
  tui.close(); // khôi phục terminal (raw mode/paste/stdout) trước khi thoát
@@ -313,6 +346,7 @@ export async function startRepl(opts = {}) {
313
346
  state.history = s.history || [];
314
347
  state.mode = 'chat';
315
348
  state.goal = s.goal || null; // khôi phục HARD GOAL nếu phiên cũ có
349
+ tui.setGoal(state.goal || '');
316
350
  if (s.model) {
317
351
  const m = findModel(s.model);
318
352
  if (m) state.model = m;
@@ -608,19 +642,29 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
608
642
  const active = bgRegistry.activeRuns();
609
643
  if (!active.length) return console.log(c.dim(' không có workflow nền nào đang chạy.'));
610
644
  console.log(c.err(' cần id: /workflow stop <id> | all'));
611
- for (const r of active) console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : ''));
645
+ for (const r of active)
646
+ console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : ''));
612
647
  return;
613
648
  }
614
649
  if (id === 'all') {
615
650
  const n = bgRegistry.sweepOnExit();
616
651
  return console.log(
617
- c.tool(' 🎼 ' + (n ? `đã huỷ ${n} workflow nền — resume bằng /workflow resume <id>` : 'không có workflow nền nào đang chạy.'))
652
+ c.tool(
653
+ ' 🎼 ' +
654
+ (n
655
+ ? `đã huỷ ${n} workflow nền — resume bằng /workflow resume <id>`
656
+ : 'không có workflow nền nào đang chạy.')
657
+ )
618
658
  );
619
659
  }
620
660
  const ok = bgRegistry.stopBg(id);
621
661
  console.log(
622
662
  ok
623
- ? c.tool(' 🎼 đã huỷ workflow nền ' + c.accent(id) + c.dim(' — resume bằng /workflow resume ' + id))
663
+ ? c.tool(
664
+ ' 🎼 đã huỷ workflow nền ' +
665
+ c.accent(id) +
666
+ c.dim(' — resume bằng /workflow resume ' + id)
667
+ )
624
668
  : c.err(' không tìm thấy workflow nền đang chạy với id: ' + id)
625
669
  );
626
670
  }
@@ -633,7 +677,9 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
633
677
  const now = Date.now();
634
678
  for (const r of active) {
635
679
  const secs = Math.round((now - r.startedAt) / 1000);
636
- console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : '') + c.dim(` · ${secs}s`));
680
+ console.log(
681
+ ' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : '') + c.dim(` · ${secs}s`)
682
+ );
637
683
  }
638
684
  }
639
685
 
@@ -802,16 +848,23 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
802
848
  const items = listWorkflowRuns(20);
803
849
  if (!items.length) {
804
850
  console.log(c.dim(' (chưa có workflow run nào trong workspace này)'));
805
- console.log(c.dim(' Run đầu tiên được tạo khi bạn /workflow <yêu cầu> hoặc /workflow run <name>.'));
851
+ console.log(
852
+ c.dim(' Run đầu tiên được tạo khi bạn /workflow <yêu cầu> hoặc /workflow run <name>.')
853
+ );
806
854
  return;
807
855
  }
808
- console.log('\n' + chalk.bold(' ' + (t.workflowRunsTitle || '📓 Workflow runs (workspace này)')));
856
+ console.log(
857
+ '\n' + chalk.bold(' ' + (t.workflowRunsTitle || '📓 Workflow runs (workspace này)'))
858
+ );
809
859
  for (const r of items) {
810
860
  const statusColor =
811
- r.status === 'done' ? c.ok :
812
- r.status === 'interrupted' ? c.tool :
813
- r.status === 'failed' ? c.err :
814
- c.accent;
861
+ r.status === 'done'
862
+ ? c.ok
863
+ : r.status === 'interrupted'
864
+ ? c.tool
865
+ : r.status === 'failed'
866
+ ? c.err
867
+ : c.accent;
815
868
  console.log(
816
869
  c.dim(' ') +
817
870
  c.accent(r.id.padEnd(40)) +
@@ -820,18 +873,25 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
820
873
  );
821
874
  }
822
875
  console.log(
823
- c.dim('\n /workflow log <id> xem chi tiết\n /workflow resume <id> chạy lại, skip task đã done\n')
876
+ c.dim(
877
+ '\n /workflow log <id> xem chi tiết\n /workflow resume <id> chạy lại, skip task đã done\n'
878
+ )
824
879
  );
825
880
  }
826
881
 
827
882
  // /workflow log <id> — xem chi tiết 1 run: prompt, tasks, kết quả mỗi sub-agent.
828
883
  function workflowLog(arg) {
829
- if (!arg) return console.log(c.err(' Cách dùng: /workflow log <id> (xem id bằng /workflow runs)'));
884
+ if (!arg)
885
+ return console.log(c.err(' Cách dùng: /workflow log <id> (xem id bằng /workflow runs)'));
830
886
  const r = loadWorkflowRun(arg);
831
887
  if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
832
888
  const d = r.data;
833
889
  console.log('\n' + chalk.bold(' 📓 ' + d.id));
834
- console.log(c.dim(` name: ${d.name || '(adhoc)'} · status: ${d.status} · started: ${relTime(d.startedAt)} · updated: ${relTime(d.updatedAt)}`));
890
+ console.log(
891
+ c.dim(
892
+ ` name: ${d.name || '(adhoc)'} · status: ${d.status} · started: ${relTime(d.startedAt)} · updated: ${relTime(d.updatedAt)}`
893
+ )
894
+ );
835
895
  console.log(c.dim(' workflow prompt: ' + truncate(d.workflowPrompt || '', 120)));
836
896
  if (!d.tasks?.length) {
837
897
  console.log(c.dim(' (chưa có sub-agent task nào được ghi nhận)'));
@@ -839,10 +899,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
839
899
  }
840
900
  console.log('\n' + c.dim(' ── sub-agent tasks ──'));
841
901
  d.tasks.forEach((tk, i) => {
842
- const statusColor =
843
- tk.status === 'done' ? c.ok :
844
- tk.status === 'failed' ? c.err :
845
- c.tool;
902
+ const statusColor = tk.status === 'done' ? c.ok : tk.status === 'failed' ? c.err : c.tool;
846
903
  console.log(
847
904
  c.dim(` #${i + 1} `) +
848
905
  statusColor(tk.status.padEnd(8)) +
@@ -851,7 +908,8 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
851
908
  );
852
909
  if (tk.context) console.log(c.dim(' context: ' + truncate(tk.context, 80)));
853
910
  if (tk.model) console.log(c.dim(' model: ' + tk.model));
854
- if (tk.result) console.log(c.dim(' result: ' + truncate(tk.result.replace(/\s+/g, ' '), 120)));
911
+ if (tk.result)
912
+ console.log(c.dim(' result: ' + truncate(tk.result.replace(/\s+/g, ' '), 120)));
855
913
  if (tk.error) console.log(c.err(' error: ' + truncate(tk.error, 120)));
856
914
  });
857
915
  console.log('');
@@ -860,13 +918,19 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
860
918
  // /workflow resume <id> — chạy lại workflow với cùng prompt; dispatchTool tự
861
919
  // hit cache trong journal cho mọi task đã done lần trước → tiết kiệm token.
862
920
  async function workflowResume(arg) {
863
- if (!arg) return console.log(c.err(' Cách dùng: /workflow resume <id> (xem id bằng /workflow runs)'));
921
+ if (!arg)
922
+ return console.log(c.err(' Cách dùng: /workflow resume <id> (xem id bằng /workflow runs)'));
864
923
  const r = loadWorkflowRun(arg);
865
924
  if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
866
925
  const d = r.data;
867
- if (!d.workflowPrompt) return console.log(c.err(' Run này không có workflow prompt — không thể resume.'));
926
+ if (!d.workflowPrompt)
927
+ return console.log(c.err(' Run này không có workflow prompt — không thể resume.'));
868
928
  if (d.status === 'done') {
869
- 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.'));
929
+ console.log(
930
+ c.tool(
931
+ ' ⚠ 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.'
932
+ )
933
+ );
870
934
  }
871
935
  await workflowExecute(d.workflowPrompt, { resumeRun: d });
872
936
  }
@@ -884,6 +948,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
884
948
  if (!(await checkQuotaBeforeHeavy('/ultra'))) return;
885
949
  state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
886
950
  state.ultra = true;
951
+ tui.setUltra(true); // badge 🚀 ULTRA trên status bar
887
952
  console.log(c.accent(' 🚀 ' + t.ultraOn));
888
953
  // Mốc history TRƯỚC khi ULTRA bơm prompt — kết thúc thì cắt về để các lượt sau không bị "dính" mục tiêu cũ.
889
954
  const baseLen = state.history.length;
@@ -929,6 +994,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
929
994
  // token <<ULTRA_DONE>>…) để các yêu cầu SAU đó không bị model coi như vẫn
930
995
  // đang tự hành / vẫn theo đuổi mục tiêu cũ.
931
996
  state.ultra = false;
997
+ tui.setUltra(false); // tắt badge khi thoát ULTRA
932
998
  if (state.history.length > baseLen) state.history.length = baseLen;
933
999
  state.history.push({
934
1000
  role: 'user',
@@ -1211,7 +1277,13 @@ NGUYÊN TẮC:
1211
1277
  const stats = memoryStats() || { rules: 0, notes: 0, mtime: st.mtimeMs };
1212
1278
  console.log(
1213
1279
  c.dim(
1214
- t.memoryStatsMain(mainLines, fmtBytes(mainBytes), stats.rules, stats.notes, relTime(stats.mtime))
1280
+ t.memoryStatsMain(
1281
+ mainLines,
1282
+ fmtBytes(mainBytes),
1283
+ stats.rules,
1284
+ stats.notes,
1285
+ relTime(stats.mtime)
1286
+ )
1215
1287
  )
1216
1288
  );
1217
1289
  } catch {
@@ -1255,6 +1327,8 @@ NGUYÊN TẮC:
1255
1327
  }
1256
1328
 
1257
1329
  tui.start();
1330
+ tui.setAgentMode(state.agentUiMode);
1331
+ tui.setGoal(state.goal || '');
1258
1332
  banner();
1259
1333
  printStatus(state);
1260
1334
  // noob.md status line — cho user thấy model có memory gì (số dòng, rules/notes,
@@ -1345,6 +1419,7 @@ NGUYÊN TẮC:
1345
1419
  execTool,
1346
1420
  spawnAgentToolsDoc,
1347
1421
  cwd: process.cwd(),
1422
+ tui, // route log sub-agent nền → ring buffer (Ctrl+O xem), cập nhật badge số agent nền
1348
1423
  });
1349
1424
 
1350
1425
  // Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
@@ -1356,12 +1431,23 @@ NGUYÊN TẮC:
1356
1431
  for (const f of fin) {
1357
1432
  const label = f.name ? `${f.name} (${f.id})` : f.id;
1358
1433
  if (f.status === 'done') {
1359
- console.log(c.tool(' 🎼 ' + (t.bgWorkflowDone ? t.bgWorkflowDone(label) : `workflow nền ${label} đã xong`)));
1434
+ console.log(
1435
+ c.tool(
1436
+ ' 🎼 ' + (t.bgWorkflowDone ? t.bgWorkflowDone(label) : `workflow nền ${label} đã xong`)
1437
+ )
1438
+ );
1360
1439
  pending.push(
1361
1440
  `[Kết quả workflow nền "${label}"]\n${f.result}\n\nTổng hợp kết quả trên cho user (ngắn gọn, nêu điểm chính). Đây là output từ workflow chạy nền — KHÔNG chạy lại.`
1362
1441
  );
1363
1442
  } else {
1364
- console.log(c.err(' 🎼 ' + (t.bgWorkflowFailed ? t.bgWorkflowFailed(label, f.error) : `workflow nền ${label} lỗi: ${f.error}`)));
1443
+ console.log(
1444
+ c.err(
1445
+ ' 🎼 ' +
1446
+ (t.bgWorkflowFailed
1447
+ ? t.bgWorkflowFailed(label, f.error)
1448
+ : `workflow nền ${label} lỗi: ${f.error}`)
1449
+ )
1450
+ );
1365
1451
  }
1366
1452
  }
1367
1453
  let input;
@@ -1475,7 +1561,8 @@ NGUYÊN TẮC:
1475
1561
  state.history,
1476
1562
  state.agentMode ? spawnAgentToolsDoc(0) : '',
1477
1563
  state.goal,
1478
- sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id)
1564
+ sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
1565
+ state.agentUiMode
1479
1566
  );
1480
1567
  const userMessage = buildUserMessage(state.history);
1481
1568
  tokenMeter.setContext(countTokens(systemPrompt) + countTokens(userMessage));
@@ -1491,7 +1578,15 @@ NGUYÊN TẮC:
1491
1578
  // src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
1492
1579
  // được rebind trong handle() — không cache.
1493
1580
  const dispatchTool = createAgentDispatcher({
1494
- state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c, bgRegistry,
1581
+ state,
1582
+ abort,
1583
+ tokenMeter,
1584
+ stopSpin,
1585
+ startSpin,
1586
+ execTool,
1587
+ tui,
1588
+ c,
1589
+ bgRegistry,
1495
1590
  });
1496
1591
 
1497
1592
  const answer = await runAgent({
@@ -1502,6 +1597,7 @@ NGUYÊN TẮC:
1502
1597
  goal: state.goal,
1503
1598
  recentSessions: sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
1504
1599
  extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : '',
1600
+ uiMode: state.agentUiMode,
1505
1601
  // Pending tasks: todo items chưa hoàn thành từ lượt trước → model tiếp tục ngay.
1506
1602
  pendingTasks: (state.todos || []).filter((t) => !t.done).map((t) => t.text),
1507
1603
  onStatus: () => tick(t.thinking),
@@ -1558,20 +1654,31 @@ NGUYÊN TẮC:
1558
1654
  // Lý do: auto-compact gián đoạn workflow giữa chừng, summary có thể mất chi
1559
1655
  // tiết user cần. Giữ 2 mốc CẢNH BÁO (60% / 80%) để user biết khi nào nên
1560
1656
  // chạy /compact, nhưng KHÔNG tự động chạy nữa.
1561
- // Với CONTEXT_WINDOW = 200k tokens:
1657
+ // QUAN TRỌNG: key off SEND_LIMIT_TOKENS (ngưỡng gửi THẬT ~200k), KHÔNG phải
1658
+ // CONTEXT_WINDOW (1M, chỉ để hiển thị ctx%). Nếu dùng CONTEXT_WINDOW thì cảnh
1659
+ // báo 80% = 800k token — gateway đã reject ở ~200k từ lâu, cảnh báo thành
1660
+ // code chết. Với SEND_LIMIT_TOKENS = 200k:
1562
1661
  // 60% (120k) → nhắc nhẹ một lần
1563
1662
  // 80% (160k) → cảnh báo mạnh — nên /compact ngay trước khi provider reject
1564
1663
  try {
1565
1664
  const totalTokens = countMessages(state.history);
1566
1665
  const k = Math.round(totalTokens / 1000);
1567
- const pct = Math.round((totalTokens / CONTEXT_WINDOW) * 100);
1568
- if (totalTokens >= CONTEXT_WINDOW * 0.8) {
1666
+ const pct = Math.round((totalTokens / SEND_LIMIT_TOKENS) * 100);
1667
+ if (totalTokens >= SEND_LIMIT_TOKENS * 0.8) {
1569
1668
  // Mốc 2 (≥80% — 160k tokens): cảnh báo mạnh, gợi ý /compact ngay.
1570
- console.log(c.err(` ⚠ ${t.veryLongSession(k)} (${pct}% context) — gõ /compact để tóm tắt, tránh provider reject ở ~200k.`));
1669
+ console.log(
1670
+ c.err(
1671
+ ` ⚠ ${t.veryLongSession(k)} (${pct}% giới hạn gửi) — gõ /compact để tóm tắt, tránh provider reject ở ~200k.`
1672
+ )
1673
+ );
1571
1674
  state._longSessionWarned = true;
1572
- } else if (totalTokens >= CONTEXT_WINDOW * 0.6 && !state._longSessionWarned) {
1675
+ } else if (totalTokens >= SEND_LIMIT_TOKENS * 0.6 && !state._longSessionWarned) {
1573
1676
  // Mốc 1 (≥60% — 120k tokens): nhắc nhẹ một lần.
1574
- console.log(c.dim(` ⓘ ${t.longSession(k)} (${pct}% context) — cân nhắc /compact nếu phiên còn dài.`));
1677
+ console.log(
1678
+ c.dim(
1679
+ ` ⓘ ${t.longSession(k)} (${pct}% giới hạn gửi) — cân nhắc /compact nếu phiên còn dài.`
1680
+ )
1681
+ );
1575
1682
  state._longSessionWarned = true;
1576
1683
  }
1577
1684
  } catch {}
@@ -1589,7 +1696,11 @@ NGUYÊN TẮC:
1589
1696
 
1590
1697
  // Diff preview — show user CHÍNH XÁC dòng nào bị xoá/thêm trước khi approve.
1591
1698
  // Tránh tin model mù: bắt được hallucinate nội dung sớm.
1592
- if (name === 'edit_file' && typeof input.old_string === 'string' && typeof input.new_string === 'string') {
1699
+ if (
1700
+ name === 'edit_file' &&
1701
+ typeof input.old_string === 'string' &&
1702
+ typeof input.new_string === 'string'
1703
+ ) {
1593
1704
  try {
1594
1705
  const filePath = path.resolve(process.cwd(), input.path || '');
1595
1706
  let oldText = input.old_string;
@@ -1616,7 +1727,11 @@ NGUYÊN TẮC:
1616
1727
  console.log(renderUnifiedDiff(oldText, newText, { label: input.path || '(unknown path)' }));
1617
1728
  } catch (e) {
1618
1729
  // Fallback nếu đọc file lỗi: ít nhất show old vs new raw.
1619
- console.log(renderUnifiedDiff(input.old_string, input.new_string, { label: input.path || '(unknown path)' }));
1730
+ console.log(
1731
+ renderUnifiedDiff(input.old_string, input.new_string, {
1732
+ label: input.path || '(unknown path)',
1733
+ })
1734
+ );
1620
1735
  }
1621
1736
  } else if (name === 'write_file' && typeof input.content === 'string') {
1622
1737
  try {
@@ -1624,13 +1739,28 @@ NGUYÊN TẮC:
1624
1739
  if (fs.existsSync(filePath)) {
1625
1740
  // File đã tồn tại → write_file là OVERWRITE → show diff old vs new.
1626
1741
  const oldContent = fs.readFileSync(filePath, 'utf8');
1627
- console.log(renderUnifiedDiff(oldContent, input.content, { label: input.path + ' (OVERWRITE)', maxLines: 60 }));
1742
+ console.log(
1743
+ renderUnifiedDiff(oldContent, input.content, {
1744
+ label: input.path + ' (OVERWRITE)',
1745
+ maxLines: 60,
1746
+ })
1747
+ );
1628
1748
  } else {
1629
1749
  // File mới → preview top 20 dòng + tổng số dòng.
1630
- console.log(renderNewFilePreview(input.content, { label: input.path || '(unknown path)', maxLines: 20 }));
1750
+ console.log(
1751
+ renderNewFilePreview(input.content, {
1752
+ label: input.path || '(unknown path)',
1753
+ maxLines: 20,
1754
+ })
1755
+ );
1631
1756
  }
1632
1757
  } catch {
1633
- console.log(renderNewFilePreview(input.content, { label: input.path || '(unknown path)', maxLines: 20 }));
1758
+ console.log(
1759
+ renderNewFilePreview(input.content, {
1760
+ label: input.path || '(unknown path)',
1761
+ maxLines: 20,
1762
+ })
1763
+ );
1634
1764
  }
1635
1765
  }
1636
1766
 
@@ -1640,7 +1770,11 @@ NGUYÊN TẮC:
1640
1770
  const rawPath = typeof input?.path === 'string' && input.path.length > 0 ? input.path : null;
1641
1771
  let absPath = null;
1642
1772
  if (rawPath) {
1643
- try { absPath = path.resolve(process.cwd(), rawPath); } catch { absPath = null; }
1773
+ try {
1774
+ absPath = path.resolve(process.cwd(), rawPath);
1775
+ } catch {
1776
+ absPath = null;
1777
+ }
1644
1778
  }
1645
1779
  const fileKey = absPath ? name + ':' + absPath : null;
1646
1780
 
@@ -1753,6 +1887,20 @@ NGUYÊN TẮC:
1753
1887
  );
1754
1888
  break;
1755
1889
  }
1890
+ case 'mode': {
1891
+ const v = arg.toLowerCase().trim();
1892
+ if (!v) {
1893
+ console.log(c.accent(' ◆ mode: ') + state.agentUiMode);
1894
+ console.log(c.dim(' cú pháp: /mode build|plan|compose · hoặc Ctrl+T để cycle'));
1895
+ } else if (v === 'build' || v === 'plan' || v === 'compose') {
1896
+ state.agentUiMode = v;
1897
+ tui.setAgentMode(v);
1898
+ console.log(c.accent(' ◆ mode: ') + v);
1899
+ } else {
1900
+ console.log(c.err(' mode không hợp lệ: ') + v + c.dim(' (chọn build|plan|compose)'));
1901
+ }
1902
+ break;
1903
+ }
1756
1904
  case 'kg': {
1757
1905
  // Knowledge graph CRUD — port từ mcp-knowledge-graph, lưu .noob/kg.jsonl.
1758
1906
  // Sub-cmd: list, path, add, obs, link, unlink, unobs, get, search, rm.
@@ -1777,11 +1925,18 @@ NGUYÊN TẮC:
1777
1925
  } else {
1778
1926
  const [, name, etype, obsRaw] = m2;
1779
1927
  const observations = obsRaw
1780
- ? obsRaw.split(';').map((s) => s.trim()).filter(Boolean)
1928
+ ? obsRaw
1929
+ .split(';')
1930
+ .map((s) => s.trim())
1931
+ .filter(Boolean)
1781
1932
  : [];
1782
1933
  const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
1783
- if (!created.length) console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
1784
- else console.log(c.ok(' ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`);
1934
+ if (!created.length)
1935
+ console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
1936
+ else
1937
+ console.log(
1938
+ c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`
1939
+ );
1785
1940
  }
1786
1941
  } else if (sub === 'obs') {
1787
1942
  const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
@@ -1791,7 +1946,8 @@ NGUYÊN TẮC:
1791
1946
  const [, name, obs] = m2;
1792
1947
  const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
1793
1948
  const added = out[0]?.addedObservations || [];
1794
- if (!added.length) console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
1949
+ if (!added.length)
1950
+ console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
1795
1951
  else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
1796
1952
  }
1797
1953
  } else if (sub === 'link') {
@@ -1831,7 +1987,8 @@ NGUYÊN TẮC:
1831
1987
  } else {
1832
1988
  const names = rest.split(/\s+/).filter(Boolean);
1833
1989
  const g = await kgOpenNodes(names);
1834
- if (!g.entities.length) console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
1990
+ if (!g.entities.length)
1991
+ console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
1835
1992
  else console.log(kgFormat(g));
1836
1993
  }
1837
1994
  } else if (sub === 'search') {
@@ -1882,10 +2039,12 @@ NGUYÊN TẮC:
1882
2039
  v.toLowerCase() === 'xoa'
1883
2040
  ) {
1884
2041
  state.goal = null;
2042
+ tui.setGoal('');
1885
2043
  console.log(c.dim(' đã xoá goal'));
1886
2044
  persist();
1887
2045
  } else {
1888
2046
  state.goal = v;
2047
+ tui.setGoal(v);
1889
2048
  console.log(c.accent(' 🎯 đã đặt goal: ') + v);
1890
2049
  persist();
1891
2050
  }
@@ -2073,7 +2232,14 @@ NGUYÊN TẮC:
2073
2232
  case 'q': {
2074
2233
  const swept = bgRegistry.sweepOnExit();
2075
2234
  if (swept > 0)
2076
- console.log(c.dim(' ' + (t.bgWorkflowSweep ? t.bgWorkflowSweep(swept) : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)));
2235
+ console.log(
2236
+ c.dim(
2237
+ ' ' +
2238
+ (t.bgWorkflowSweep
2239
+ ? t.bgWorkflowSweep(swept)
2240
+ : `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
2241
+ )
2242
+ );
2077
2243
  persist();
2078
2244
  exiting = true;
2079
2245
  console.log(c.dim(' ' + t.bye));
@@ -2294,5 +2460,3 @@ function listModels() {
2294
2460
  }
2295
2461
  console.log('\n' + lines.join('\n') + c.dim('\n\n ' + t.modelListHint) + '\n');
2296
2462
  }
2297
-
2298
-
package/src/tokens.js CHANGED
@@ -57,12 +57,19 @@ export function countMessages(messages = []) {
57
57
  // window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k/o200k
58
58
  // (token dài nhất ~ vài chục byte).
59
59
  const TAIL_WINDOW = 256;
60
- // Context window tối đa của model. Đặt 200k tokens — match Claude 3.5/Opus 4,
61
- // GPT-4o, an toàn cho mọi model phổ biến qua gateway (Gemini 1M, DeepSeek
62
- // 128k, Grok 128k...). Đặt cao hơn 200k là vô nghĩa: provider sẽ reject prompt
63
- // TRƯỚC khi auto-compact của repl.js hội trigger user thấy 'compact
64
- // không hoạt động' logic compact vẫn đúng.
65
- export const CONTEXT_WINDOW = 200_000;
60
+ // Context window cho HIỂN THỊ (ctx% ở status bar). Đặt 1M tokens.
61
+ // QUAN TRỌNG đây con số DISPLAY-ONLY: ctx% = contextTokens / CONTEXT_WINDOW.
62
+ // Ngưỡng nén/tóm tắt THỰC TẾ (MAX_PROMPT_CHARS, SUMMARIZE_THRESHOLD_CHARS trong
63
+ // src/agent.js) GIỮ NGUYÊN mức an toàn (~200k token) để gateway/upstream không
64
+ // reject prompt TRƯỚC khi compact kịp chạy. Nếu sau này xác nhận gateway thật sự
65
+ // nhận ~1M token context, mới nâng các ngưỡng đó cho khớp.
66
+ export const CONTEXT_WINDOW = 1_000_000;
67
+
68
+ // Ngưỡng GỬI THẬT (real ceiling). Cảnh báo phiên dài + logic compact key off CON
69
+ // SỐ NÀY, không phải CONTEXT_WINDOW. Tách 2 hằng: CONTEXT_WINDOW chỉ để hiển thị
70
+ // ctx%; SEND_LIMIT_TOKENS là mức mà vượt qua thì gateway/upstream bắt đầu reject.
71
+ // Nếu nâng mức gửi thật (gateway nhận nhiều hơn), đổi DUY NHẤT hằng này.
72
+ export const SEND_LIMIT_TOKENS = 200_000;
66
73
 
67
74
  export class TokenMeter {
68
75
  constructor() {