@noobdemon/noob-cli 1.9.6 → 1.9.8
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/package.json +1 -1
- package/src/agent.js +1 -1
- package/src/repl.js +55 -29
- package/src/tokens.js +22 -0
- package/src/tui.js +30 -0
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -33,7 +33,7 @@ Available tools (each is self-contained; pick the SMALLEST tool that answers the
|
|
|
33
33
|
Context is finite. Don't slurp the whole repo up front. Discover information progressively: list_dir/glob to map → grep to locate → read_file (with offset+limit for big files) to inspect only what matters. Each tool result spends your attention budget — make every call earn it. When a tool returns a huge blob, extract the few facts you need, then move on; don't re-read it later (the result stays in history).
|
|
34
34
|
|
|
35
35
|
# Rules
|
|
36
|
-
- TODO-BASED EXECUTION: For any multi-step task (3+ actions), CREATE a todo list FIRST
|
|
36
|
+
- TODO-BASED EXECUTION: For any multi-step task (3+ actions), CREATE a todo list FIRST in your response text (NOT in a file — just write them as "- [ ] task name" in your reply). Then WORK THROUGH EVERY ITEM, checking them off ("- [x] task name") as you complete each. The runtime parses your text for these markers and shows a progress bar to the user. BEFORE summarizing or claiming "done", verify ALL items are "- [x]". If ANY remain unchecked, CONTINUE — do not stop. NEVER stop mid-plan.
|
|
37
37
|
- GROUND TRUTH = real TOOL RESULTs in this conversation, not your memory or what you intended to do. A file changed only if a write_file/edit_file result confirms it (see the FILES CHANGED list). A test passed / build succeeded / command worked only if a run_command result above shows it. Never narrate outcomes you didn't observe; if you haven't checked, say so and check now (read_file / list_dir / run the command). Before any "done/summary" reply, reconcile every file and result you're about to claim against the actual tool results above — if it isn't there, you didn't do it yet.
|
|
38
38
|
- Investigate before editing: read the relevant files first; never invent file contents.
|
|
39
39
|
- Make the smallest change that fully solves the task. Match the surrounding code style.
|
package/src/repl.js
CHANGED
|
@@ -5,7 +5,7 @@ import chalk from "chalk";
|
|
|
5
5
|
import { createTui } from "./tui.js";
|
|
6
6
|
import { runAgent, maybeSummarize } from "./agent.js";
|
|
7
7
|
import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from "./subagent.js";
|
|
8
|
-
import { TokenMeter } from "./tokens.js";
|
|
8
|
+
import { TokenMeter, countMessages, CONTEXT_WINDOW } from "./tokens.js";
|
|
9
9
|
import { stream, usage, ApiError, resetMemoryToken } from "./api.js";
|
|
10
10
|
import { runTool, describe, DESTRUCTIVE, addRoot, removeRoot, listRoots, OutOfScopeError, nearestExistingDir } from "./tools.js";
|
|
11
11
|
import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from "./models.js";
|
|
@@ -146,6 +146,7 @@ export async function startRepl(opts = {}) {
|
|
|
146
146
|
extraRoots: [], // thư mục ngoài cwd được /add-dir cấp quyền (UX display only;
|
|
147
147
|
// source of truth là extraRoots trong src/tools.js)
|
|
148
148
|
_longSessionWarned: false, // đã in cảnh báo phiên dài chưa (reset khi /clear)
|
|
149
|
+
todos: [], // [{text, done}] — todo list parse từ model output, render trên status bar
|
|
149
150
|
};
|
|
150
151
|
const tokenMeter = new TokenMeter();
|
|
151
152
|
|
|
@@ -365,6 +366,9 @@ export async function startRepl(opts = {}) {
|
|
|
365
366
|
session = sessions.newSession({ cwd: process.cwd(), model: state.model.id });
|
|
366
367
|
// Reset per-session upstream memory token so the next chat starts fresh.
|
|
367
368
|
resetMemoryToken();
|
|
369
|
+
// Clear todo list khi phiên mới.
|
|
370
|
+
state.todos = [];
|
|
371
|
+
tui.setTodos([]);
|
|
368
372
|
};
|
|
369
373
|
|
|
370
374
|
// /frontend-design <yêu cầu> — vận dụng skill frontend-design (skills/frontend-design/SKILL.md)
|
|
@@ -1182,7 +1186,7 @@ NGUYÊN TẮC:
|
|
|
1182
1186
|
// Nhờ vậy người dùng LUÔN thấy đồng hồ + token đang chạy, kể cả khi treo chờ y/n.
|
|
1183
1187
|
const tickMeta = () => {
|
|
1184
1188
|
const elapsed = ((Date.now() - t0) / 1000).toFixed(0);
|
|
1185
|
-
tui.setMeta(`${elapsed}s · ${tokenMeter.
|
|
1189
|
+
tui.setMeta(`${elapsed}s · ${tokenMeter.formatWithPct()}`);
|
|
1186
1190
|
};
|
|
1187
1191
|
const tick = (label) => {
|
|
1188
1192
|
tui.status(c.dim(`${label}…`));
|
|
@@ -1233,6 +1237,8 @@ NGUYÊN TẮC:
|
|
|
1233
1237
|
? text + `\n\n[File người dùng nhắc tới bằng @: ${files.join(", ")} — đọc bằng read_file nếu cần.]`
|
|
1234
1238
|
: text;
|
|
1235
1239
|
state.history.push({ role: "user", content });
|
|
1240
|
+
// Tính context tokens realtime — đếm system prompt + history trước khi gửi.
|
|
1241
|
+
tokenMeter.setContext(countMessages(state.history));
|
|
1236
1242
|
if (process.stdin.isTTY && !state.steerHintShown) {
|
|
1237
1243
|
console.log(c.dim(" " + t.steerHint));
|
|
1238
1244
|
state.steerHintShown = true;
|
|
@@ -1326,6 +1332,9 @@ NGUYÊN TẮC:
|
|
|
1326
1332
|
// Lượt cuối không stream ra gì (vd model suy luận trả 1 cục) → in fallback.
|
|
1327
1333
|
if ((!printer || !printer.started) && answer?.trim())
|
|
1328
1334
|
printAnswer(answer, state.model.name, providerColor(state.model.provider));
|
|
1335
|
+
// Parse todo từ model output → render trên status bar.
|
|
1336
|
+
state.todos = parseTodosFromHistory(state.history);
|
|
1337
|
+
tui.setTodos(state.todos);
|
|
1329
1338
|
return answer; // vòng ULTRA cần text này để dò token hoàn thành
|
|
1330
1339
|
} catch (err) {
|
|
1331
1340
|
stopSpin();
|
|
@@ -1334,34 +1343,28 @@ NGUYÊN TẮC:
|
|
|
1334
1343
|
} finally {
|
|
1335
1344
|
abort = null;
|
|
1336
1345
|
tui.setBusy(false);
|
|
1337
|
-
//
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1346
|
+
// Auto-compact dựa trên context tokens thay vì chars.
|
|
1347
|
+
// 80% context window (160k tokens) → auto compact.
|
|
1348
|
+
// 70% (140k tokens) → cảnh báo mạnh.
|
|
1349
|
+
// 60% (120k tokens) → nhắc nhẹ.
|
|
1340
1350
|
try {
|
|
1341
|
-
const
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
);
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
// giữ model chạy mượt khi user mải làm việc, không để phiên phình mãi.
|
|
1348
|
-
// Dùng cờ _autoCompacting chống re-entrant (nếu compact lâu, lượt sau
|
|
1349
|
-
// tới trước khi xong thì bỏ qua).
|
|
1350
|
-
if (totalChars > 240000 && !state._autoCompacting) {
|
|
1351
|
+
const totalTokens = countMessages(state.history);
|
|
1352
|
+
tokenMeter.setContext(totalTokens);
|
|
1353
|
+
const k = Math.round(totalTokens / 1000);
|
|
1354
|
+
const pct = Math.round((totalTokens / CONTEXT_WINDOW) * 100);
|
|
1355
|
+
// Mốc 3 (≥80% — 160k tokens): TỰ ĐỘNG compact.
|
|
1356
|
+
if (totalTokens >= CONTEXT_WINDOW * 0.8 && !state._autoCompacting) {
|
|
1351
1357
|
state._autoCompacting = true;
|
|
1352
|
-
console.log(c.accent(
|
|
1358
|
+
console.log(c.accent(` ⚡ ${t.autoCompactTrigger(k)} (${pct}% context)`));
|
|
1353
1359
|
tui.setBusy(true, t.compactRunning);
|
|
1354
1360
|
try {
|
|
1355
1361
|
const ok = await maybeSummarize(state.history, { model: state.model, force: true });
|
|
1356
1362
|
tui.setBusy(false);
|
|
1357
1363
|
if (ok) {
|
|
1358
|
-
const
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
);
|
|
1362
|
-
const aK = Math.round(afterChars / 1000);
|
|
1363
|
-
const pct = totalChars > 0 ? Math.round(((totalChars - afterChars) / totalChars) * 100) : 0;
|
|
1364
|
-
console.log(c.ok(" " + t.autoCompactDone(k, aK, pct)));
|
|
1364
|
+
const afterTokens = countMessages(state.history);
|
|
1365
|
+
const aK = Math.round(afterTokens / 1000);
|
|
1366
|
+
const saved = totalTokens > 0 ? Math.round(((totalTokens - afterTokens) / totalTokens) * 100) : 0;
|
|
1367
|
+
console.log(c.ok(` ${t.autoCompactDone(k, aK, saved)} (${Math.round((afterTokens / CONTEXT_WINDOW) * 100)}% context)`));
|
|
1365
1368
|
state._longSessionWarned = false;
|
|
1366
1369
|
persist();
|
|
1367
1370
|
} else {
|
|
@@ -1373,19 +1376,42 @@ NGUYÊN TẮC:
|
|
|
1373
1376
|
} finally {
|
|
1374
1377
|
state._autoCompacting = false;
|
|
1375
1378
|
}
|
|
1376
|
-
} else if (
|
|
1377
|
-
// Mốc 2 (
|
|
1378
|
-
console.log(c.err(
|
|
1379
|
+
} else if (totalTokens >= CONTEXT_WINDOW * 0.7) {
|
|
1380
|
+
// Mốc 2 (≥70% — 140k tokens): cảnh báo mạnh.
|
|
1381
|
+
console.log(c.err(` ⚠ ${t.veryLongSession(k)} (${pct}% context)`));
|
|
1379
1382
|
state._longSessionWarned = true;
|
|
1380
|
-
} else if (
|
|
1381
|
-
// Mốc 1 (120k
|
|
1382
|
-
console.log(c.dim(
|
|
1383
|
+
} else if (totalTokens >= CONTEXT_WINDOW * 0.6 && !state._longSessionWarned) {
|
|
1384
|
+
// Mốc 1 (≥60% — 120k tokens): nhắc nhẹ một lần.
|
|
1385
|
+
console.log(c.dim(` ⓘ ${t.longSession(k)} (${pct}% context)`));
|
|
1383
1386
|
state._longSessionWarned = true;
|
|
1384
1387
|
}
|
|
1385
1388
|
} catch {}
|
|
1386
1389
|
}
|
|
1387
1390
|
}
|
|
1388
1391
|
|
|
1392
|
+
// ── Todo parser ────────────────────────────────────────────────────────────
|
|
1393
|
+
// Scan history assistant messages cho pattern `- [ ] task` và `- [x] task`.
|
|
1394
|
+
// Trả về [{text, done}] — items mới nhất ở cuối. Dùng state mới nhất (cuối
|
|
1395
|
+
// history) để反映todo hiện tại của model.
|
|
1396
|
+
function parseTodosFromHistory(history) {
|
|
1397
|
+
const todos = [];
|
|
1398
|
+
for (const m of history) {
|
|
1399
|
+
if (m.role !== "assistant" || typeof m.content !== "string") continue;
|
|
1400
|
+
// Match todo items: `- [ ] task` hoặc `- [x] task` (case-insensitive)
|
|
1401
|
+
const lines = m.content.split("\n");
|
|
1402
|
+
for (const line of lines) {
|
|
1403
|
+
const doneMatch = line.match(/^[\s]*-\s*\[x\]\s+(.+)/i);
|
|
1404
|
+
if (doneMatch) { todos.push({ text: doneMatch[1].trim(), done: true }); continue; }
|
|
1405
|
+
const todoMatch = line.match(/^[\s]*-\s*\[\s?\]\s+(.+)/);
|
|
1406
|
+
if (todoMatch) { todos.push({ text: todoMatch[1].trim(), done: false }); }
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
// Dedupe: giữ item CUỐI cùng cho mỗi text (model có thể lặp todo)
|
|
1410
|
+
const seen = new Map();
|
|
1411
|
+
for (const t of todos) seen.set(t.text, t);
|
|
1412
|
+
return [...seen.values()];
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1389
1415
|
async function execTool(name, input) {
|
|
1390
1416
|
const desc = describe(name, input);
|
|
1391
1417
|
const color = name === "run_command" ? "#ef4444" : "#f59e0b";
|
package/src/tokens.js
CHANGED
|
@@ -32,11 +32,16 @@ export function countMessages(messages = []) {
|
|
|
32
32
|
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k_base
|
|
33
33
|
// (token dài nhất ~ vài chục byte).
|
|
34
34
|
const TAIL_WINDOW = 256;
|
|
35
|
+
// Context window tối đa của model (200k tokens). Dùng để tính % usage realtime.
|
|
36
|
+
export const CONTEXT_WINDOW = 200000;
|
|
35
37
|
|
|
36
38
|
export class TokenMeter {
|
|
37
39
|
constructor() {
|
|
38
40
|
this.input = 0;
|
|
39
41
|
this.output = 0;
|
|
42
|
+
// Context size (tokens) hiện tại của messages sắp gửi lên API.
|
|
43
|
+
// Được set từ repl.js trước mỗi lượt gọi, cập nhật realtime.
|
|
44
|
+
this.contextTokens = 0;
|
|
40
45
|
// Phần đầu output đã "commit" — không đụng vào nữa.
|
|
41
46
|
this._committedChars = 0; // số ký tự đã đẩy qua khỏi tail window
|
|
42
47
|
this._committedTokens = 0; // tổng token tương ứng đã cộng vào this.output
|
|
@@ -92,9 +97,26 @@ export class TokenMeter {
|
|
|
92
97
|
const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
|
|
93
98
|
return `↑${fmt(this.input)} ↓${fmt(this.output)} (${fmt(this.total)})`;
|
|
94
99
|
}
|
|
100
|
+
// Định dạng kèm context usage: "↑1.2k ↓340 (1.5k) · ctx 45%".
|
|
101
|
+
formatWithPct() {
|
|
102
|
+
const base = this.format();
|
|
103
|
+
const pct = this.contextPct();
|
|
104
|
+
if (pct === null) return base;
|
|
105
|
+
return `${base} · ctx ${pct}%`;
|
|
106
|
+
}
|
|
107
|
+
// Phần trăm context window đã dùng (0–100). Trả null nếu chưa có data.
|
|
108
|
+
contextPct() {
|
|
109
|
+
if (!this.contextTokens) return null;
|
|
110
|
+
return Math.min(100, Math.round((this.contextTokens / CONTEXT_WINDOW) * 100));
|
|
111
|
+
}
|
|
112
|
+
// Đặt context size (tokens) — gọi từ repl.js trước mỗi lượt API.
|
|
113
|
+
setContext(n) {
|
|
114
|
+
this.contextTokens = Math.max(0, n | 0);
|
|
115
|
+
}
|
|
95
116
|
reset() {
|
|
96
117
|
this.input = 0;
|
|
97
118
|
this.output = 0;
|
|
119
|
+
this.contextTokens = 0;
|
|
98
120
|
this._committedChars = 0;
|
|
99
121
|
this._committedTokens = 0;
|
|
100
122
|
this._tail = "";
|
package/src/tui.js
CHANGED
|
@@ -73,6 +73,10 @@ function wrapText(text, width, maxLines) {
|
|
|
73
73
|
return lines.map(close);
|
|
74
74
|
}
|
|
75
75
|
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
76
|
+
function truncStr(s, max) {
|
|
77
|
+
if (!s) return "";
|
|
78
|
+
return s.length > max ? s.slice(0, max - 1) + "…" : s;
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer } = {}) {
|
|
78
82
|
const tty = !!(process.stdout.isTTY && process.stdin.isTTY) && process.env.NOOB_TUI !== "0";
|
|
@@ -141,6 +145,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
141
145
|
let busy = false;
|
|
142
146
|
let busyLabel = "";
|
|
143
147
|
let busyMeta = ""; // chuỗi phụ (elapsed + token) repl đẩy vào qua setMeta()
|
|
148
|
+
let todos = []; // [{text, done}] — danh sách todo đang chạy, repl parse từ model output
|
|
144
149
|
let frame = 0;
|
|
145
150
|
let frameTimer = null;
|
|
146
151
|
let prevRows = 0;
|
|
@@ -311,6 +316,25 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
311
316
|
return wrapText(liveOut, cols(), 2);
|
|
312
317
|
}
|
|
313
318
|
const spin = FRAMES[frame % FRAMES.length];
|
|
319
|
+
// Todo progress bar: hiện khi có todos, thay thế statusText/busyLabel
|
|
320
|
+
if (todos.length) {
|
|
321
|
+
const done = todos.filter((t) => t.done).length;
|
|
322
|
+
const total = todos.length;
|
|
323
|
+
const current = todos.find((t) => !t.done);
|
|
324
|
+
const pct = Math.round((done / total) * 100);
|
|
325
|
+
// Thanh progress: ████░░░░ 2/5 (40%)
|
|
326
|
+
const barW = Math.min(12, Math.floor(cols() * 0.15));
|
|
327
|
+
const filled = Math.round((done / total) * barW);
|
|
328
|
+
const bar = "█".repeat(filled) + "░".repeat(barW - filled);
|
|
329
|
+
const progress = c.ok(`${done}/${total}`) + c.dim(` ${bar} ${pct}%`);
|
|
330
|
+
// Task đang làm: ▸ ...
|
|
331
|
+
const taskLine = current
|
|
332
|
+
? c.accent("▸ ") + c.dim(truncStr(current.text, cols() - 12))
|
|
333
|
+
: c.ok("✓ hoàn thành!");
|
|
334
|
+
const meta = busy && busyMeta ? c.dim(" · " + busyMeta) : "";
|
|
335
|
+
const tail = busy ? c.dim(" · Ctrl+C") : "";
|
|
336
|
+
return [progress + meta + tail, taskLine];
|
|
337
|
+
}
|
|
314
338
|
// Khi busy, LUÔN hiện elapsed+tokens (busyMeta) cạnh statusText/busyLabel để
|
|
315
339
|
// người dùng thấy phiên đang sống — kể cả lúc model im giữa các bước.
|
|
316
340
|
if (statusText) {
|
|
@@ -637,6 +661,12 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
637
661
|
busyMeta = next;
|
|
638
662
|
if (busy) draw();
|
|
639
663
|
},
|
|
664
|
+
setTodos(items) {
|
|
665
|
+
// repl parse todo từ model output, đẩy vào TUI để render progress bar.
|
|
666
|
+
// items: [{text: string, done: boolean}] — empty array = ẩn todo bar.
|
|
667
|
+
todos = Array.isArray(items) ? items : [];
|
|
668
|
+
draw();
|
|
669
|
+
},
|
|
640
670
|
setPrompt(label) {
|
|
641
671
|
promptLabel = label || "";
|
|
642
672
|
draw();
|