@noobdemon/noob-cli 1.9.5 → 1.9.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.9.5",
3
+ "version": "1.9.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
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 as your very first tool call using write_file to a temp block in your response (format: "- [ ] item"). Then WORK THROUGH EVERY ITEM, checking them off ("- [x]") as you complete each. BEFORE summarizing or claiming "done", mentally verify: "Have I checked off ALL items? Is there anything left unchecked?" If ANY item remains unchecked, CONTINUE — do not stop. If the user's request implies multiple deliverables, treat each as a TODO item. NEVER stop mid-plan. NEVER assume something is done without a tool result proving it.
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.
@@ -416,6 +416,25 @@ function buildUserMessage(history) {
416
416
  return parts.join("\n");
417
417
  }
418
418
 
419
+ // Detect câu trả lời bị cắt giữa chừng — KHÔNG phải câu hoàn chỉnh.
420
+ // Trả true nếu text kết thúc đột ngột (thiếu dấu câu, list chưa đóng, v.v.).
421
+ function isIncompleteResponse(text) {
422
+ if (!text) return false;
423
+ const t = text.trimEnd();
424
+ // Kết thúc bằng dấu cuối danh sách chưa đóng: "A.", "B.", "1.", "(1)" mà không có gì sau
425
+ if (/[A-Z]\.\s*$/.test(t) || /\(\d+\)\s*$/.test(t) || /^\d+\.\s*$/m.test(t)) return true;
426
+ // Kết thúc giữa câu — không có dấu câu cuối cùng (. ! ? : ; ) và không phải markdown/code
427
+ const lastChar = t.slice(-1);
428
+ if (lastChar && !/[.!?:;)\]"'`#>\n]/.test(lastChar) && t.length > 50) {
429
+ // Kiểm thêm: dòng cuối chứa từ khóa bị cắt (ví dụ, vd, hay, hoặc, và, nhưng, mà)
430
+ const lastLine = t.split("\n").pop().trim();
431
+ if (/\s(ví|vd|hay|hoặc|và|nhưng|mà|hoac|or|and|but|e\.g|i\.e)\s*$/i.test(lastLine)) return true;
432
+ // Dòng cuối là 1 câu bắt đầu nhưng chưa xong (có chủ ngữ nhưng không có vị ngữ hoàn chỉnh)
433
+ if (/\s(Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn|Bạn)\s+(muốn|có|thấy|nên|cần|đã|đang|sẽ|chọn|chọn|chọn)\s*$/i.test(lastLine)) return true;
434
+ }
435
+ return false;
436
+ }
437
+
419
438
  // Extract a single tool call from an assistant message, if present.
420
439
  // NOTE: we do NOT match up to a closing ``` fence — write_file content routinely
421
440
  // contains its own ```code``` fences (e.g. a README), and the first inner fence
@@ -523,6 +542,16 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
523
542
  });
524
543
  continue;
525
544
  }
545
+ // Phát hiện câu trả lời bị cắt giữa chừng (truncated stream): text kết thúc
546
+ // đột ngột không có dấu câu/đóng danh sách/tool block → nudge model viết tiếp.
547
+ if (isIncompleteResponse(text)) {
548
+ history.push({
549
+ role: "tool",
550
+ name: "stream_recovery",
551
+ content: "[STREAM CUT] Lượt vừa bị cắt giữa chừng. Hãy viết TIẾP liền mạch — nếu đang hỏi user thì hoàn tất câu hỏi, nếu đang list thì đóng danh sách, nếu đang viết tool block thì đóng đúng JSON.",
552
+ });
553
+ continue;
554
+ }
526
555
  return text; // final answer
527
556
  }
528
557
 
package/src/repl.js CHANGED
@@ -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)
@@ -1326,6 +1330,9 @@ NGUYÊN TẮC:
1326
1330
  // Lượt cuối không stream ra gì (vd model suy luận trả 1 cục) → in fallback.
1327
1331
  if ((!printer || !printer.started) && answer?.trim())
1328
1332
  printAnswer(answer, state.model.name, providerColor(state.model.provider));
1333
+ // Parse todo từ model output → render trên status bar.
1334
+ state.todos = parseTodosFromHistory(state.history);
1335
+ tui.setTodos(state.todos);
1329
1336
  return answer; // vòng ULTRA cần text này để dò token hoàn thành
1330
1337
  } catch (err) {
1331
1338
  stopSpin();
@@ -1386,6 +1393,29 @@ NGUYÊN TẮC:
1386
1393
  }
1387
1394
  }
1388
1395
 
1396
+ // ── Todo parser ────────────────────────────────────────────────────────────
1397
+ // Scan history assistant messages cho pattern `- [ ] task` và `- [x] task`.
1398
+ // Trả về [{text, done}] — items mới nhất ở cuối. Dùng state mới nhất (cuối
1399
+ // history) để反映todo hiện tại của model.
1400
+ function parseTodosFromHistory(history) {
1401
+ const todos = [];
1402
+ for (const m of history) {
1403
+ if (m.role !== "assistant" || typeof m.content !== "string") continue;
1404
+ // Match todo items: `- [ ] task` hoặc `- [x] task` (case-insensitive)
1405
+ const lines = m.content.split("\n");
1406
+ for (const line of lines) {
1407
+ const doneMatch = line.match(/^[\s]*-\s*\[x\]\s+(.+)/i);
1408
+ if (doneMatch) { todos.push({ text: doneMatch[1].trim(), done: true }); continue; }
1409
+ const todoMatch = line.match(/^[\s]*-\s*\[\s?\]\s+(.+)/);
1410
+ if (todoMatch) { todos.push({ text: todoMatch[1].trim(), done: false }); }
1411
+ }
1412
+ }
1413
+ // Dedupe: giữ item CUỐI cùng cho mỗi text (model có thể lặp todo)
1414
+ const seen = new Map();
1415
+ for (const t of todos) seen.set(t.text, t);
1416
+ return [...seen.values()];
1417
+ }
1418
+
1389
1419
  async function execTool(name, input) {
1390
1420
  const desc = describe(name, input);
1391
1421
  const color = name === "run_command" ? "#ef4444" : "#f59e0b";
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();