@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 +1 -1
- package/src/agent.js +6 -1
- package/src/i18n.js +3 -0
- package/src/repl.js +57 -8
package/package.json
CHANGED
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 →
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(() =>
|
|
165
|
-
|
|
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));
|