@noobdemon/noob-cli 1.1.2 → 1.1.4

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.1.2",
3
+ "version": "1.1.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -177,8 +177,13 @@ export function parseToolCall(text) {
177
177
  * @param {(msg:string)=>void} opts.onStatus thinking/streaming status
178
178
  * @returns {Promise<string>} the final assistant answer (no tool block)
179
179
  */
180
- export async function runAgent({ history, model, signal, onTool, onStatus, onDelta }) {
180
+ export async function runAgent({ history, model, signal, onTool, onStatus, onDelta, onSteer }) {
181
181
  for (let step = 0; step < MAX_STEPS; step++) {
182
+ // Steering: tin nhắn người dùng gõ GIỮA CHỪNG được chèn vào hội thoại TRƯỚC
183
+ // lần gọi model kế tiếp → model thấy và điều chỉnh ngay trong cùng task.
184
+ const steer = onSteer?.() || [];
185
+ for (const msg of steer) history.push({ role: "user", content: msg });
186
+
182
187
  const prompt = buildPrompt(history);
183
188
  onStatus?.("thinking");
184
189
  onDelta?.({ type: "step-start" });
package/src/i18n.js CHANGED
@@ -13,6 +13,9 @@ export const t = {
13
13
  denied: "đã từ chối",
14
14
  queued: (n, txt) => `⏎ đã xếp hàng [${n}] · gửi khi model xong: ${txt}`,
15
15
  queueCleared: (n) => `(đã xoá ${n} tin đang xếp hàng)`,
16
+ steerHint: "💬 Gõ + Enter bất cứ lúc nào để chèn ý cho AI giữa chừng (không ngắt task đang chạy).",
17
+ steerWillInject: (txt) => `💬 sẽ chèn cho AI ở bước tới: ${txt}`,
18
+ steerInject: (txt) => `💬 đã chèn cho AI: ${txt}`,
16
19
  permRetry: "→ gõ y (đồng ý) · n (từ chối) · a (luôn cho phép)",
17
20
 
18
21
  // auth
package/src/repl.js CHANGED
@@ -59,9 +59,11 @@ export async function startRepl(opts = {}) {
59
59
  w(line);
60
60
  return;
61
61
  }
62
- // Không ai đang hỏi → đây là tin xếp hàng cho lượt kế tiếp.
62
+ // Không ai đang hỏi → tin xếp hàng. Nếu đang chạy task → sẽ CHÈN cho AI
63
+ // bước kế tiếp (steering). Nếu rảnh → gửi như lượt mới khi tới phiên.
63
64
  pending.push(line);
64
- if (process.stdin.isTTY) console.log(c.dim(" " + t.queued(pending.length, truncate(line, 60))));
65
+ if (process.stdin.isTTY)
66
+ console.log(abort ? c.user(" " + t.steerWillInject(truncate(line, 60))) : c.dim(" " + t.queued(pending.length, truncate(line, 60))));
65
67
  }
66
68
  function endInput() {
67
69
  closed = true;
@@ -74,6 +76,10 @@ export async function startRepl(opts = {}) {
74
76
  function buildRl() {
75
77
  const r = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: "" });
76
78
  r.on("line", deliver);
79
+ // readline ở chế độ terminal phát 'SIGINT' của RIÊNG nó (không cho process
80
+ // SIGINT chạy) — nối vào cùng một handler. interrupt() được hoist nên gọi
81
+ // được dù khai báo phía dưới.
82
+ r.on("SIGINT", () => interrupt());
77
83
  r.on("close", () => {
78
84
  if (exiting) return endInput(); // ta chủ động thoát
79
85
  // EOF THẬT: stdin đã end/destroy (Ctrl+Z trên Windows, Ctrl+D trên *nix),
@@ -122,7 +128,10 @@ export async function startRepl(opts = {}) {
122
128
  try {
123
129
  readline.emitKeypressEvents(process.stdin);
124
130
  process.stdin.on("keypress", (_str, key) => {
125
- if (!key || key.name !== "tab" || !key.shift) return;
131
+ if (!key) return;
132
+ // Ctrl+C dạng phím thô — ở raw mode, Ctrl+C KHÔNG tự thành SIGINT nữa.
133
+ if (key.ctrl && key.name === "c") return interrupt();
134
+ if (key.name !== "tab" || !key.shift) return;
126
135
  state.yolo = !state.yolo;
127
136
  console.log(state.yolo ? c.err(" " + t.yoloOn) : c.ok(" " + t.yoloOff));
128
137
  rl.setPrompt(promptStr(false)); // cập nhật dòng trạng thái ngay lập tức
@@ -143,7 +152,19 @@ export async function startRepl(opts = {}) {
143
152
 
144
153
  let abort = null; // active turn controller
145
154
  let sigintArmed = false;
146
- process.on("SIGINT", () => {
155
+ let sigintTimer = null;
156
+ let lastInterrupt = 0;
157
+
158
+ // Ctrl+C — một đường xử lý duy nhất, gọi từ 3 nguồn (xem bên dưới) vì readline
159
+ // ở chế độ terminal/raw NUỐT SIGINT của process trên Windows. Debounce 80ms để
160
+ // gộp các nguồn trùng nhau thành MỘT lần nhấn.
161
+ // • đang chạy task → 1 lần = NGỪNG task (không thoát)
162
+ // • đang rảnh → 1 lần = đếm 1.5s; thêm 1 lần trong 1.5s = THOÁT CLI
163
+ function interrupt() {
164
+ const now = Date.now();
165
+ if (now - lastInterrupt < 80) return; // gộp SIGINT + 'SIGINT' của rl + keypress
166
+ lastInterrupt = now;
167
+
147
168
  if (abort) {
148
169
  abort.abort();
149
170
  abort = null;
@@ -153,17 +174,33 @@ export async function startRepl(opts = {}) {
153
174
  console.log(c.dim(" " + t.queueCleared(n)));
154
175
  }
155
176
  console.log(c.err("\n ✗ " + t.interrupted));
156
- return; // the main loop will redraw the prompt
177
+ sigintArmed = false;
178
+ if (sigintTimer) {
179
+ clearTimeout(sigintTimer);
180
+ sigintTimer = null;
181
+ }
182
+ if (!closed) rl.prompt(true);
183
+ return;
157
184
  }
185
+
158
186
  if (sigintArmed) {
187
+ // lần 2 trong cửa sổ 1.5s → thoát
188
+ if (sigintTimer) clearTimeout(sigintTimer);
189
+ exiting = true;
190
+ persist();
159
191
  console.log(c.dim("\n " + t.bye));
160
192
  process.exit(0);
161
193
  }
194
+ // lần 1 → vũ trang, đếm 1.5s
162
195
  sigintArmed = true;
163
196
  console.log(c.dim("\n " + t.pressAgainToExit));
164
- setTimeout(() => (sigintArmed = false), 2000);
165
- rl.prompt(true);
166
- });
197
+ sigintTimer = setTimeout(() => {
198
+ sigintArmed = false;
199
+ sigintTimer = null;
200
+ }, 1500);
201
+ if (!closed) rl.prompt(true);
202
+ }
203
+ process.on("SIGINT", interrupt);
167
204
 
168
205
  // Đừng để một lỗi bất ngờ làm "tự động tắt" CLI. Nguyên nhân hay gặp:
169
206
  // tiến trình cập nhật nền (spawn npm) phát sự kiện 'error' không ai bắt,
@@ -392,6 +429,10 @@ Cuối cùng: tóm tắt + danh sách sửa theo thứ tự ưu tiên. Thẳng t
392
429
  }
393
430
 
394
431
  state.history.push({ role: "user", content: text });
432
+ if (process.stdin.isTTY && !state.steerHintShown) {
433
+ console.log(c.dim(" " + t.steerHint));
434
+ state.steerHintShown = true;
435
+ }
395
436
  startSpin(t.thinking);
396
437
  let printer = null;
397
438
 
@@ -400,6 +441,14 @@ Cuối cùng: tóm tắt + danh sách sửa theo thứ tự ưu tiên. Thẳng t
400
441
  model: state.model.id,
401
442
  signal: abort.signal,
402
443
  onStatus: () => tick(t.thinking),
444
+ onSteer: () => {
445
+ if (!pending.length) return [];
446
+ const msgs = pending.splice(0);
447
+ stopSpin(); // in sạch dòng chèn rồi cho spinner chạy lại
448
+ for (const msg of msgs) console.log(c.user(" " + t.steerInject(truncate(msg, 70))));
449
+ startSpin(t.thinking);
450
+ return msgs;
451
+ },
403
452
  onDelta: (ev) => {
404
453
  if (ev.type === "step-start") {
405
454
  printer = makeStreamPrinter(state.model.name, providerColor(state.model.provider));