@noobdemon/noob-cli 1.12.14 → 1.12.15
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/api.js +7 -3
- package/src/diff.js +13 -4
- package/src/i18n.js +2 -1
- package/src/repl/agent-dispatch.js +30 -21
- package/src/repl/permission.js +1 -2
- package/src/repl/stream-printer.js +2 -11
- package/src/repl/workflow-commands.js +3 -1
- package/src/repl.js +190 -55
- package/src/tokens.js +13 -6
- package/src/tools.js +47 -8
- package/src/tui.js +153 -21
- package/src/ui.js +7 -5
- package/src/workflow-bg.js +33 -5
- package/src/workflow-runs.js +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
|
|
4
4
|
|
|
5
|
+
## [1.12.15] - 2026-06-16
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Todo display nuốt thanh status + đè ô nhập** (`src/tui.js` `topRow()`): todo branch return sớm vô điều kiện — lúc rảnh vẫn vẽ full progress + task + ctx (2-3 dòng) đè vùng trên ô nhập; lúc busy mất animated spinner. Fix: gỡ early-return, dựng `progress` (1 dòng) + `taskLine` sẵn rồi ghép theo trạng thái. Rảnh → 1 dòng progress gọn (📋 done/total bar %), không task/ctx line → không nuốt ô nhập. Busy → spinner + progress chung dòng 1, task line ▸ dòng 2 → giữ cả spinner LẪN tiến độ. Smoke `smoke-tui-render` tách 4a (rảnh: progress, KHÔNG task line) + 4b (busy: task line hiện) — 8/8 pass.
|
|
9
|
+
- **Test stale `tests/models.test.js` + leak isolation `tests/tools.test.js`**: models test hardcode catalog cũ (`gateway-claude-opus-4-7`, provider `google`) → cập nhật theo catalog hiện tại (`gateway-claude-opus-4-8`, `deepseek`). grep test fail do `extraRoots` module-global leak từ `loadWorkspaceRoots()` (nạp `.noob/dirs.json` chứa Temp dir) → thêm export `clearRoots()` gọi trong `beforeEach`. grep 17s → 997ms. 98/98 vitest pass.
|
|
10
|
+
|
|
5
11
|
## [1.12.14] - 2026-06-14
|
|
6
12
|
|
|
7
13
|
### Fixed
|
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -391,8 +391,7 @@ async function streamOnce({
|
|
|
391
391
|
// UND_ERR_*). Tự throw nguyên dạng → streamWithRetry KHÔNG retry (vì check
|
|
392
392
|
// err.name === 'ApiError'). Bọc thành ApiError retryable để được backoff.
|
|
393
393
|
const causeCode = err?.cause?.code || err?.code;
|
|
394
|
-
const isNetworkDrop =
|
|
395
|
-
err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
|
|
394
|
+
const isNetworkDrop = err?.name === 'TypeError' && /fetch failed/i.test(err?.message || '');
|
|
396
395
|
const isSocketErr =
|
|
397
396
|
typeof causeCode === 'string' &&
|
|
398
397
|
/^(ECONNRESET|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ENETUNREACH|ENOTFOUND|EPIPE|UND_ERR_)/i.test(
|
|
@@ -422,7 +421,12 @@ const USAGE_CACHE_MS = 90_000;
|
|
|
422
421
|
export async function usage({ force = false } = {}) {
|
|
423
422
|
const now = Date.now();
|
|
424
423
|
const key = config.apiKey || '';
|
|
425
|
-
if (
|
|
424
|
+
if (
|
|
425
|
+
!force &&
|
|
426
|
+
_usageCache.data &&
|
|
427
|
+
_usageCache.key === key &&
|
|
428
|
+
now - _usageCache.at < USAGE_CACHE_MS
|
|
429
|
+
) {
|
|
426
430
|
return _usageCache.data;
|
|
427
431
|
}
|
|
428
432
|
const resp = await fetch(config.gatewayUrl + '/api/usage', { headers: authHeaders() });
|
package/src/diff.js
CHANGED
|
@@ -32,7 +32,8 @@ function diffLines(a, b) {
|
|
|
32
32
|
while (i > 0 && j > 0) {
|
|
33
33
|
if (a[i - 1] === b[j - 1]) {
|
|
34
34
|
ops.push({ type: 'eq', text: a[i - 1] });
|
|
35
|
-
i--;
|
|
35
|
+
i--;
|
|
36
|
+
j--;
|
|
36
37
|
} else if (t[i - 1][j] >= t[i][j - 1]) {
|
|
37
38
|
ops.push({ type: 'del', text: a[i - 1] });
|
|
38
39
|
i--;
|
|
@@ -41,8 +42,12 @@ function diffLines(a, b) {
|
|
|
41
42
|
j--;
|
|
42
43
|
}
|
|
43
44
|
}
|
|
44
|
-
while (i > 0) {
|
|
45
|
-
|
|
45
|
+
while (i > 0) {
|
|
46
|
+
ops.push({ type: 'del', text: a[--i] });
|
|
47
|
+
}
|
|
48
|
+
while (j > 0) {
|
|
49
|
+
ops.push({ type: 'add', text: b[--j] });
|
|
50
|
+
}
|
|
46
51
|
return ops.reverse();
|
|
47
52
|
}
|
|
48
53
|
|
|
@@ -98,7 +103,11 @@ function groupHunks(ops, context = 3) {
|
|
|
98
103
|
|
|
99
104
|
// Render unified diff với màu. `label` là tiêu đề (vd path file).
|
|
100
105
|
// `maxLines` cap tổng số dòng render (mặc định 60) — diff quá dài hiện cuối `... +N dòng`.
|
|
101
|
-
export function renderUnifiedDiff(
|
|
106
|
+
export function renderUnifiedDiff(
|
|
107
|
+
oldText,
|
|
108
|
+
newText,
|
|
109
|
+
{ label = '', context = 3, maxLines = 60 } = {}
|
|
110
|
+
) {
|
|
102
111
|
const a = oldText.split('\n');
|
|
103
112
|
const b = newText.split('\n');
|
|
104
113
|
const ops = diffLines(a, b);
|
package/src/i18n.js
CHANGED
|
@@ -95,7 +95,8 @@ export const t = {
|
|
|
95
95
|
` noob.md : ${lines} dòng · ${bytes} · ${rules} rules · ${notes} notes · cập nhật ${ago}`,
|
|
96
96
|
memoryStatsArchive: (lines, bytes, ago) =>
|
|
97
97
|
` noob-archive.md : ${lines} dòng · ${bytes} · cập nhật ${ago} (không inject vào prompt)`,
|
|
98
|
-
memoryStatsArchiveMissing:
|
|
98
|
+
memoryStatsArchiveMissing:
|
|
99
|
+
' noob-archive.md : (chưa có — tạo bằng tay nếu cần lưu Notes lịch sử)',
|
|
99
100
|
memoryStatsOk: ' ✓ trong ngưỡng khuyến nghị (≤ 200 dòng / ≤ 20KB)',
|
|
100
101
|
memoryStatsWarnLines: (n) =>
|
|
101
102
|
` ⚠ noob.md ${n} dòng > 200 — cân nhắc archive Notes cũ sang noob-archive.md để giảm token bloat mỗi turn`,
|
|
@@ -33,6 +33,22 @@ import { t } from '../i18n.js';
|
|
|
33
33
|
*/
|
|
34
34
|
export function createAgentDispatcher(deps) {
|
|
35
35
|
const { state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c } = deps;
|
|
36
|
+
// bgAgentLog: true → log sub-agent đi vào ring buffer (tui.pushAgentLog, xem
|
|
37
|
+
// bằng Ctrl+O), KHÔNG in ra UI chính. Dùng cho workflow nền (headless). False/
|
|
38
|
+
// thiếu → behavior cũ: in console.log ra UI chính (sub-agent foreground).
|
|
39
|
+
const bgAgentLog = !!deps.bgAgentLog;
|
|
40
|
+
// Route 1 dòng log sub-agent: nền → ring buffer; foreground → console.log.
|
|
41
|
+
const emitAgentLog = (line) => {
|
|
42
|
+
if (bgAgentLog) {
|
|
43
|
+
try {
|
|
44
|
+
tui?.pushAgentLog?.(line);
|
|
45
|
+
} catch {}
|
|
46
|
+
} else {
|
|
47
|
+
stopSpin?.();
|
|
48
|
+
console.log(chalk.hex('#8b5cf6')(' ' + line));
|
|
49
|
+
startSpin?.(t.thinking);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
36
52
|
// bgRegistry: tuỳ chọn — chỉ wire ở dispatcher của TURN (repl.js), KHÔNG ở bg
|
|
37
53
|
// dispatcher (tránh đệ quy: workflow nền spawn workflow nền). Thiếu → tool báo lỗi.
|
|
38
54
|
const bgRegistry = deps.bgRegistry || null;
|
|
@@ -68,7 +84,9 @@ export function createAgentDispatcher(deps) {
|
|
|
68
84
|
// Flag: lượt này model đã dùng write_todos → repl skip parse markdown để
|
|
69
85
|
// không overwrite structured state bằng parser fragile. Reset đầu mỗi turn.
|
|
70
86
|
state._todosFromTool = true;
|
|
71
|
-
try {
|
|
87
|
+
try {
|
|
88
|
+
tui?.setTodos?.(todos);
|
|
89
|
+
} catch {}
|
|
72
90
|
const done = todos.filter((t) => t.done).length;
|
|
73
91
|
// In compact: lần đầu (prev rỗng) hoặc list thay đổi tập text → in full.
|
|
74
92
|
// Nếu cùng tập text + chỉ khác trạng thái done → in diff (dòng vừa toggle).
|
|
@@ -105,7 +123,8 @@ export function createAgentDispatcher(deps) {
|
|
|
105
123
|
if (!bgRegistry)
|
|
106
124
|
return {
|
|
107
125
|
allow: true,
|
|
108
|
-
result:
|
|
126
|
+
result:
|
|
127
|
+
'ERROR: workflow nền không khả dụng ở ngữ cảnh này (vd đang trong 1 workflow nền khác — không lồng).',
|
|
109
128
|
};
|
|
110
129
|
if (!state.agentMode)
|
|
111
130
|
return {
|
|
@@ -122,13 +141,14 @@ export function createAgentDispatcher(deps) {
|
|
|
122
141
|
result: `ERROR: đã đạt ${bgRegistry.MAX_BG} workflow nền song song — chờ một run xong hoặc /workflow stop <id>.`,
|
|
123
142
|
};
|
|
124
143
|
if (r.error === 'empty_prompt')
|
|
125
|
-
return {
|
|
144
|
+
return {
|
|
145
|
+
allow: true,
|
|
146
|
+
result: 'ERROR: workflow cần field "prompt" (yêu cầu cho workflow).',
|
|
147
|
+
};
|
|
126
148
|
return { allow: true, result: 'ERROR khởi tạo workflow nền: ' + r.error };
|
|
127
149
|
}
|
|
128
150
|
stopSpin();
|
|
129
|
-
console.log(
|
|
130
|
-
chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`)
|
|
131
|
-
);
|
|
151
|
+
console.log(chalk.hex('#8b5cf6')(` 🎼 workflow nền bắt đầu — run ${r.id}`));
|
|
132
152
|
startSpin(t.thinking);
|
|
133
153
|
return {
|
|
134
154
|
allow: true,
|
|
@@ -172,8 +192,7 @@ export function createAgentDispatcher(deps) {
|
|
|
172
192
|
if (m) {
|
|
173
193
|
subModel = m.id;
|
|
174
194
|
modelTag = ` [${m.name}]`;
|
|
175
|
-
} else
|
|
176
|
-
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
195
|
+
} else modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
177
196
|
}
|
|
178
197
|
const taskBody = task?.task || task?.prompt || '';
|
|
179
198
|
const taskCtx = task?.context || '';
|
|
@@ -190,9 +209,7 @@ export function createAgentDispatcher(deps) {
|
|
|
190
209
|
)
|
|
191
210
|
);
|
|
192
211
|
startSpin(t.thinking);
|
|
193
|
-
return Promise.resolve(
|
|
194
|
-
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
195
|
-
);
|
|
212
|
+
return Promise.resolve(`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`);
|
|
196
213
|
}
|
|
197
214
|
recordWorkflowTaskStart(runData, {
|
|
198
215
|
hash,
|
|
@@ -209,11 +226,7 @@ export function createAgentDispatcher(deps) {
|
|
|
209
226
|
signal: abort.signal,
|
|
210
227
|
tokenMeter,
|
|
211
228
|
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
212
|
-
onLog: (msg) =>
|
|
213
|
-
stopSpin();
|
|
214
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
215
|
-
startSpin(t.thinking);
|
|
216
|
-
},
|
|
229
|
+
onLog: (msg) => emitAgentLog(msg + modelTag),
|
|
217
230
|
})
|
|
218
231
|
.then((r) => {
|
|
219
232
|
recordWorkflowTaskDone(runData, hash, r);
|
|
@@ -233,11 +246,7 @@ export function createAgentDispatcher(deps) {
|
|
|
233
246
|
signal: abort.signal,
|
|
234
247
|
tokenMeter,
|
|
235
248
|
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
236
|
-
onLog: (msg) =>
|
|
237
|
-
stopSpin();
|
|
238
|
-
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
239
|
-
startSpin(t.thinking);
|
|
240
|
-
},
|
|
249
|
+
onLog: (msg) => emitAgentLog(msg + modelTag),
|
|
241
250
|
})
|
|
242
251
|
.then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
|
|
243
252
|
.catch(
|
package/src/repl/permission.js
CHANGED
|
@@ -102,8 +102,7 @@ export async function askWorkflowAgentMode({ tui, ask, pending, c, t, truncate }
|
|
|
102
102
|
console.log(
|
|
103
103
|
c.tool(
|
|
104
104
|
' ' +
|
|
105
|
-
(t.workflowAgentAskHint ||
|
|
106
|
-
'🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
105
|
+
(t.workflowAgentAskHint || '🎼 /workflow cần spawn sub-agent — agent mode hiện đang TẮT.')
|
|
107
106
|
)
|
|
108
107
|
);
|
|
109
108
|
try {
|
|
@@ -15,12 +15,7 @@
|
|
|
15
15
|
// đa dòng — flushCompleteLines phải HOLD các dòng cuối là table-row/sep
|
|
16
16
|
// candidate để chờ đủ data trước khi parse.
|
|
17
17
|
import chalk from 'chalk';
|
|
18
|
-
import {
|
|
19
|
-
renderMarkdown,
|
|
20
|
-
renderInline,
|
|
21
|
-
renderHeadingLine,
|
|
22
|
-
renderBulletPrefix,
|
|
23
|
-
} from '../ui.js';
|
|
18
|
+
import { renderMarkdown, renderInline, renderHeadingLine, renderBulletPrefix } from '../ui.js';
|
|
24
19
|
|
|
25
20
|
function renderStreamLine(line, inCodeFence) {
|
|
26
21
|
if (inCodeFence) return line; // code fence body: in raw, không parse markdown.
|
|
@@ -62,11 +57,7 @@ export function renderStreamBlock(lines, fenceState) {
|
|
|
62
57
|
continue;
|
|
63
58
|
}
|
|
64
59
|
// Detect table: dòng hiện tại là row + dòng kế tiếp là separator.
|
|
65
|
-
if (
|
|
66
|
-
looksLikeTableRow(ln) &&
|
|
67
|
-
i + 1 < lines.length &&
|
|
68
|
-
TABLE_SEP_RE.test(lines[i + 1])
|
|
69
|
-
) {
|
|
60
|
+
if (looksLikeTableRow(ln) && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1])) {
|
|
70
61
|
// Gom hết các dòng row liên tiếp.
|
|
71
62
|
const tableLines = [ln, lines[i + 1]];
|
|
72
63
|
let j = i + 2;
|
|
@@ -55,7 +55,9 @@ export function workflowHelp({ c, t }) {
|
|
|
55
55
|
console.log(' /workflow run <name> [extra] chạy (built-in HOẶC saved, có thể thêm ngữ cảnh)');
|
|
56
56
|
console.log(' /workflow runs list run journal đã chạy (resume-able)');
|
|
57
57
|
console.log(' /workflow log <id> xem chi tiết 1 run (sub-agent task + kết quả)');
|
|
58
|
-
console.log(
|
|
58
|
+
console.log(
|
|
59
|
+
' /workflow resume <id> chạy lại run, skip task đã done → tiết kiệm token'
|
|
60
|
+
);
|
|
59
61
|
console.log(' /workflow active workflow nền đang chạy');
|
|
60
62
|
console.log(' /workflow stop <id> | all huỷ workflow nền (resume được)');
|
|
61
63
|
console.log(' /workflow delete|rm <name> xoá workflow đã lưu');
|
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 {
|
|
9
|
-
|
|
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,
|
|
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';
|
|
@@ -165,8 +183,7 @@ export async function startRepl(opts = {}) {
|
|
|
165
183
|
_askPermission(name, { tui, ask, pending, c, t, truncate, targetPath });
|
|
166
184
|
const askAddRoot = (root, targetPath) =>
|
|
167
185
|
_askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
168
|
-
const askWorkflowAgentMode = () =>
|
|
169
|
-
_askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
186
|
+
const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
170
187
|
|
|
171
188
|
// ── quota soft-cap guard ───────────────────────────────────────────
|
|
172
189
|
// Trước khi start ULTRA / loop / workflow (3 lệnh ăn nhiều request nhất),
|
|
@@ -232,6 +249,7 @@ export async function startRepl(opts = {}) {
|
|
|
232
249
|
abort = null;
|
|
233
250
|
if (state.ultra) {
|
|
234
251
|
state.ultra = false; // Ctrl+C cũng dừng vòng tự hành, không chỉ lượt hiện tại
|
|
252
|
+
tui.setUltra(false); // tắt badge ULTRA trên status bar
|
|
235
253
|
console.log(c.tool(' ' + t.ultraStopped));
|
|
236
254
|
}
|
|
237
255
|
if (pending.length) {
|
|
@@ -256,7 +274,14 @@ export async function startRepl(opts = {}) {
|
|
|
256
274
|
// Bg workflow đang chạy → abort + mark interrupted (journal resume-able).
|
|
257
275
|
const swept = bgRegistry.sweepOnExit();
|
|
258
276
|
if (swept > 0)
|
|
259
|
-
console.log(
|
|
277
|
+
console.log(
|
|
278
|
+
c.dim(
|
|
279
|
+
' ' +
|
|
280
|
+
(t.bgWorkflowSweep
|
|
281
|
+
? t.bgWorkflowSweep(swept)
|
|
282
|
+
: `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
|
|
283
|
+
)
|
|
284
|
+
);
|
|
260
285
|
persist();
|
|
261
286
|
console.log(c.dim('\n ' + t.bye));
|
|
262
287
|
tui.close(); // khôi phục terminal (raw mode/paste/stdout) trước khi thoát
|
|
@@ -608,19 +633,29 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
608
633
|
const active = bgRegistry.activeRuns();
|
|
609
634
|
if (!active.length) return console.log(c.dim(' không có workflow nền nào đang chạy.'));
|
|
610
635
|
console.log(c.err(' cần id: /workflow stop <id> | all'));
|
|
611
|
-
for (const r of active)
|
|
636
|
+
for (const r of active)
|
|
637
|
+
console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : ''));
|
|
612
638
|
return;
|
|
613
639
|
}
|
|
614
640
|
if (id === 'all') {
|
|
615
641
|
const n = bgRegistry.sweepOnExit();
|
|
616
642
|
return console.log(
|
|
617
|
-
c.tool(
|
|
643
|
+
c.tool(
|
|
644
|
+
' 🎼 ' +
|
|
645
|
+
(n
|
|
646
|
+
? `đã huỷ ${n} workflow nền — resume bằng /workflow resume <id>`
|
|
647
|
+
: 'không có workflow nền nào đang chạy.')
|
|
648
|
+
)
|
|
618
649
|
);
|
|
619
650
|
}
|
|
620
651
|
const ok = bgRegistry.stopBg(id);
|
|
621
652
|
console.log(
|
|
622
653
|
ok
|
|
623
|
-
? c.tool(
|
|
654
|
+
? c.tool(
|
|
655
|
+
' 🎼 đã huỷ workflow nền ' +
|
|
656
|
+
c.accent(id) +
|
|
657
|
+
c.dim(' — resume bằng /workflow resume ' + id)
|
|
658
|
+
)
|
|
624
659
|
: c.err(' không tìm thấy workflow nền đang chạy với id: ' + id)
|
|
625
660
|
);
|
|
626
661
|
}
|
|
@@ -633,7 +668,9 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
633
668
|
const now = Date.now();
|
|
634
669
|
for (const r of active) {
|
|
635
670
|
const secs = Math.round((now - r.startedAt) / 1000);
|
|
636
|
-
console.log(
|
|
671
|
+
console.log(
|
|
672
|
+
' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : '') + c.dim(` · ${secs}s`)
|
|
673
|
+
);
|
|
637
674
|
}
|
|
638
675
|
}
|
|
639
676
|
|
|
@@ -802,16 +839,23 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
802
839
|
const items = listWorkflowRuns(20);
|
|
803
840
|
if (!items.length) {
|
|
804
841
|
console.log(c.dim(' (chưa có workflow run nào trong workspace này)'));
|
|
805
|
-
console.log(
|
|
842
|
+
console.log(
|
|
843
|
+
c.dim(' Run đầu tiên được tạo khi bạn /workflow <yêu cầu> hoặc /workflow run <name>.')
|
|
844
|
+
);
|
|
806
845
|
return;
|
|
807
846
|
}
|
|
808
|
-
console.log(
|
|
847
|
+
console.log(
|
|
848
|
+
'\n' + chalk.bold(' ' + (t.workflowRunsTitle || '📓 Workflow runs (workspace này)'))
|
|
849
|
+
);
|
|
809
850
|
for (const r of items) {
|
|
810
851
|
const statusColor =
|
|
811
|
-
r.status === 'done'
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
852
|
+
r.status === 'done'
|
|
853
|
+
? c.ok
|
|
854
|
+
: r.status === 'interrupted'
|
|
855
|
+
? c.tool
|
|
856
|
+
: r.status === 'failed'
|
|
857
|
+
? c.err
|
|
858
|
+
: c.accent;
|
|
815
859
|
console.log(
|
|
816
860
|
c.dim(' ') +
|
|
817
861
|
c.accent(r.id.padEnd(40)) +
|
|
@@ -820,18 +864,25 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
820
864
|
);
|
|
821
865
|
}
|
|
822
866
|
console.log(
|
|
823
|
-
c.dim(
|
|
867
|
+
c.dim(
|
|
868
|
+
'\n /workflow log <id> xem chi tiết\n /workflow resume <id> chạy lại, skip task đã done\n'
|
|
869
|
+
)
|
|
824
870
|
);
|
|
825
871
|
}
|
|
826
872
|
|
|
827
873
|
// /workflow log <id> — xem chi tiết 1 run: prompt, tasks, kết quả mỗi sub-agent.
|
|
828
874
|
function workflowLog(arg) {
|
|
829
|
-
if (!arg)
|
|
875
|
+
if (!arg)
|
|
876
|
+
return console.log(c.err(' Cách dùng: /workflow log <id> (xem id bằng /workflow runs)'));
|
|
830
877
|
const r = loadWorkflowRun(arg);
|
|
831
878
|
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
832
879
|
const d = r.data;
|
|
833
880
|
console.log('\n' + chalk.bold(' 📓 ' + d.id));
|
|
834
|
-
console.log(
|
|
881
|
+
console.log(
|
|
882
|
+
c.dim(
|
|
883
|
+
` name: ${d.name || '(adhoc)'} · status: ${d.status} · started: ${relTime(d.startedAt)} · updated: ${relTime(d.updatedAt)}`
|
|
884
|
+
)
|
|
885
|
+
);
|
|
835
886
|
console.log(c.dim(' workflow prompt: ' + truncate(d.workflowPrompt || '', 120)));
|
|
836
887
|
if (!d.tasks?.length) {
|
|
837
888
|
console.log(c.dim(' (chưa có sub-agent task nào được ghi nhận)'));
|
|
@@ -839,10 +890,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
839
890
|
}
|
|
840
891
|
console.log('\n' + c.dim(' ── sub-agent tasks ──'));
|
|
841
892
|
d.tasks.forEach((tk, i) => {
|
|
842
|
-
const statusColor =
|
|
843
|
-
tk.status === 'done' ? c.ok :
|
|
844
|
-
tk.status === 'failed' ? c.err :
|
|
845
|
-
c.tool;
|
|
893
|
+
const statusColor = tk.status === 'done' ? c.ok : tk.status === 'failed' ? c.err : c.tool;
|
|
846
894
|
console.log(
|
|
847
895
|
c.dim(` #${i + 1} `) +
|
|
848
896
|
statusColor(tk.status.padEnd(8)) +
|
|
@@ -851,7 +899,8 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
851
899
|
);
|
|
852
900
|
if (tk.context) console.log(c.dim(' context: ' + truncate(tk.context, 80)));
|
|
853
901
|
if (tk.model) console.log(c.dim(' model: ' + tk.model));
|
|
854
|
-
if (tk.result)
|
|
902
|
+
if (tk.result)
|
|
903
|
+
console.log(c.dim(' result: ' + truncate(tk.result.replace(/\s+/g, ' '), 120)));
|
|
855
904
|
if (tk.error) console.log(c.err(' error: ' + truncate(tk.error, 120)));
|
|
856
905
|
});
|
|
857
906
|
console.log('');
|
|
@@ -860,13 +909,19 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
860
909
|
// /workflow resume <id> — chạy lại workflow với cùng prompt; dispatchTool tự
|
|
861
910
|
// hit cache trong journal cho mọi task đã done lần trước → tiết kiệm token.
|
|
862
911
|
async function workflowResume(arg) {
|
|
863
|
-
if (!arg)
|
|
912
|
+
if (!arg)
|
|
913
|
+
return console.log(c.err(' Cách dùng: /workflow resume <id> (xem id bằng /workflow runs)'));
|
|
864
914
|
const r = loadWorkflowRun(arg);
|
|
865
915
|
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
866
916
|
const d = r.data;
|
|
867
|
-
if (!d.workflowPrompt)
|
|
917
|
+
if (!d.workflowPrompt)
|
|
918
|
+
return console.log(c.err(' Run này không có workflow prompt — không thể resume.'));
|
|
868
919
|
if (d.status === 'done') {
|
|
869
|
-
console.log(
|
|
920
|
+
console.log(
|
|
921
|
+
c.tool(
|
|
922
|
+
' ⚠ 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.'
|
|
923
|
+
)
|
|
924
|
+
);
|
|
870
925
|
}
|
|
871
926
|
await workflowExecute(d.workflowPrompt, { resumeRun: d });
|
|
872
927
|
}
|
|
@@ -884,6 +939,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
884
939
|
if (!(await checkQuotaBeforeHeavy('/ultra'))) return;
|
|
885
940
|
state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
|
|
886
941
|
state.ultra = true;
|
|
942
|
+
tui.setUltra(true); // badge 🚀 ULTRA trên status bar
|
|
887
943
|
console.log(c.accent(' 🚀 ' + t.ultraOn));
|
|
888
944
|
// 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
945
|
const baseLen = state.history.length;
|
|
@@ -929,6 +985,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
929
985
|
// token <<ULTRA_DONE>>…) để các yêu cầu SAU đó không bị model coi như vẫn
|
|
930
986
|
// đang tự hành / vẫn theo đuổi mục tiêu cũ.
|
|
931
987
|
state.ultra = false;
|
|
988
|
+
tui.setUltra(false); // tắt badge khi thoát ULTRA
|
|
932
989
|
if (state.history.length > baseLen) state.history.length = baseLen;
|
|
933
990
|
state.history.push({
|
|
934
991
|
role: 'user',
|
|
@@ -1211,7 +1268,13 @@ NGUYÊN TẮC:
|
|
|
1211
1268
|
const stats = memoryStats() || { rules: 0, notes: 0, mtime: st.mtimeMs };
|
|
1212
1269
|
console.log(
|
|
1213
1270
|
c.dim(
|
|
1214
|
-
t.memoryStatsMain(
|
|
1271
|
+
t.memoryStatsMain(
|
|
1272
|
+
mainLines,
|
|
1273
|
+
fmtBytes(mainBytes),
|
|
1274
|
+
stats.rules,
|
|
1275
|
+
stats.notes,
|
|
1276
|
+
relTime(stats.mtime)
|
|
1277
|
+
)
|
|
1215
1278
|
)
|
|
1216
1279
|
);
|
|
1217
1280
|
} catch {
|
|
@@ -1345,6 +1408,7 @@ NGUYÊN TẮC:
|
|
|
1345
1408
|
execTool,
|
|
1346
1409
|
spawnAgentToolsDoc,
|
|
1347
1410
|
cwd: process.cwd(),
|
|
1411
|
+
tui, // route log sub-agent nền → ring buffer (Ctrl+O xem), cập nhật badge số agent nền
|
|
1348
1412
|
});
|
|
1349
1413
|
|
|
1350
1414
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
@@ -1356,12 +1420,23 @@ NGUYÊN TẮC:
|
|
|
1356
1420
|
for (const f of fin) {
|
|
1357
1421
|
const label = f.name ? `${f.name} (${f.id})` : f.id;
|
|
1358
1422
|
if (f.status === 'done') {
|
|
1359
|
-
console.log(
|
|
1423
|
+
console.log(
|
|
1424
|
+
c.tool(
|
|
1425
|
+
' 🎼 ' + (t.bgWorkflowDone ? t.bgWorkflowDone(label) : `workflow nền ${label} đã xong`)
|
|
1426
|
+
)
|
|
1427
|
+
);
|
|
1360
1428
|
pending.push(
|
|
1361
1429
|
`[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
1430
|
);
|
|
1363
1431
|
} else {
|
|
1364
|
-
console.log(
|
|
1432
|
+
console.log(
|
|
1433
|
+
c.err(
|
|
1434
|
+
' 🎼 ' +
|
|
1435
|
+
(t.bgWorkflowFailed
|
|
1436
|
+
? t.bgWorkflowFailed(label, f.error)
|
|
1437
|
+
: `workflow nền ${label} lỗi: ${f.error}`)
|
|
1438
|
+
)
|
|
1439
|
+
);
|
|
1365
1440
|
}
|
|
1366
1441
|
}
|
|
1367
1442
|
let input;
|
|
@@ -1491,7 +1566,15 @@ NGUYÊN TẮC:
|
|
|
1491
1566
|
// src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
|
|
1492
1567
|
// được rebind trong handle() — không cache.
|
|
1493
1568
|
const dispatchTool = createAgentDispatcher({
|
|
1494
|
-
state,
|
|
1569
|
+
state,
|
|
1570
|
+
abort,
|
|
1571
|
+
tokenMeter,
|
|
1572
|
+
stopSpin,
|
|
1573
|
+
startSpin,
|
|
1574
|
+
execTool,
|
|
1575
|
+
tui,
|
|
1576
|
+
c,
|
|
1577
|
+
bgRegistry,
|
|
1495
1578
|
});
|
|
1496
1579
|
|
|
1497
1580
|
const answer = await runAgent({
|
|
@@ -1558,20 +1641,31 @@ NGUYÊN TẮC:
|
|
|
1558
1641
|
// Lý do: auto-compact gián đoạn workflow giữa chừng, summary có thể mất chi
|
|
1559
1642
|
// tiết user cần. Giữ 2 mốc CẢNH BÁO (60% / 80%) để user biết khi nào nên
|
|
1560
1643
|
// chạy /compact, nhưng KHÔNG tự động chạy nữa.
|
|
1561
|
-
//
|
|
1644
|
+
// QUAN TRỌNG: key off SEND_LIMIT_TOKENS (ngưỡng gửi THẬT ~200k), KHÔNG phải
|
|
1645
|
+
// CONTEXT_WINDOW (1M, chỉ để hiển thị ctx%). Nếu dùng CONTEXT_WINDOW thì cảnh
|
|
1646
|
+
// báo 80% = 800k token — gateway đã reject ở ~200k từ lâu, cảnh báo thành
|
|
1647
|
+
// code chết. Với SEND_LIMIT_TOKENS = 200k:
|
|
1562
1648
|
// 60% (120k) → nhắc nhẹ một lần
|
|
1563
1649
|
// 80% (160k) → cảnh báo mạnh — nên /compact ngay trước khi provider reject
|
|
1564
1650
|
try {
|
|
1565
1651
|
const totalTokens = countMessages(state.history);
|
|
1566
1652
|
const k = Math.round(totalTokens / 1000);
|
|
1567
|
-
const pct = Math.round((totalTokens /
|
|
1568
|
-
if (totalTokens >=
|
|
1653
|
+
const pct = Math.round((totalTokens / SEND_LIMIT_TOKENS) * 100);
|
|
1654
|
+
if (totalTokens >= SEND_LIMIT_TOKENS * 0.8) {
|
|
1569
1655
|
// Mốc 2 (≥80% — 160k tokens): cảnh báo mạnh, gợi ý /compact ngay.
|
|
1570
|
-
console.log(
|
|
1656
|
+
console.log(
|
|
1657
|
+
c.err(
|
|
1658
|
+
` ⚠ ${t.veryLongSession(k)} (${pct}% giới hạn gửi) — gõ /compact để tóm tắt, tránh provider reject ở ~200k.`
|
|
1659
|
+
)
|
|
1660
|
+
);
|
|
1571
1661
|
state._longSessionWarned = true;
|
|
1572
|
-
} else if (totalTokens >=
|
|
1662
|
+
} else if (totalTokens >= SEND_LIMIT_TOKENS * 0.6 && !state._longSessionWarned) {
|
|
1573
1663
|
// Mốc 1 (≥60% — 120k tokens): nhắc nhẹ một lần.
|
|
1574
|
-
console.log(
|
|
1664
|
+
console.log(
|
|
1665
|
+
c.dim(
|
|
1666
|
+
` ⓘ ${t.longSession(k)} (${pct}% giới hạn gửi) — cân nhắc /compact nếu phiên còn dài.`
|
|
1667
|
+
)
|
|
1668
|
+
);
|
|
1575
1669
|
state._longSessionWarned = true;
|
|
1576
1670
|
}
|
|
1577
1671
|
} catch {}
|
|
@@ -1589,7 +1683,11 @@ NGUYÊN TẮC:
|
|
|
1589
1683
|
|
|
1590
1684
|
// Diff preview — show user CHÍNH XÁC dòng nào bị xoá/thêm trước khi approve.
|
|
1591
1685
|
// Tránh tin model mù: bắt được hallucinate nội dung sớm.
|
|
1592
|
-
if (
|
|
1686
|
+
if (
|
|
1687
|
+
name === 'edit_file' &&
|
|
1688
|
+
typeof input.old_string === 'string' &&
|
|
1689
|
+
typeof input.new_string === 'string'
|
|
1690
|
+
) {
|
|
1593
1691
|
try {
|
|
1594
1692
|
const filePath = path.resolve(process.cwd(), input.path || '');
|
|
1595
1693
|
let oldText = input.old_string;
|
|
@@ -1616,7 +1714,11 @@ NGUYÊN TẮC:
|
|
|
1616
1714
|
console.log(renderUnifiedDiff(oldText, newText, { label: input.path || '(unknown path)' }));
|
|
1617
1715
|
} catch (e) {
|
|
1618
1716
|
// Fallback nếu đọc file lỗi: ít nhất show old vs new raw.
|
|
1619
|
-
console.log(
|
|
1717
|
+
console.log(
|
|
1718
|
+
renderUnifiedDiff(input.old_string, input.new_string, {
|
|
1719
|
+
label: input.path || '(unknown path)',
|
|
1720
|
+
})
|
|
1721
|
+
);
|
|
1620
1722
|
}
|
|
1621
1723
|
} else if (name === 'write_file' && typeof input.content === 'string') {
|
|
1622
1724
|
try {
|
|
@@ -1624,13 +1726,28 @@ NGUYÊN TẮC:
|
|
|
1624
1726
|
if (fs.existsSync(filePath)) {
|
|
1625
1727
|
// File đã tồn tại → write_file là OVERWRITE → show diff old vs new.
|
|
1626
1728
|
const oldContent = fs.readFileSync(filePath, 'utf8');
|
|
1627
|
-
console.log(
|
|
1729
|
+
console.log(
|
|
1730
|
+
renderUnifiedDiff(oldContent, input.content, {
|
|
1731
|
+
label: input.path + ' (OVERWRITE)',
|
|
1732
|
+
maxLines: 60,
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1628
1735
|
} else {
|
|
1629
1736
|
// File mới → preview top 20 dòng + tổng số dòng.
|
|
1630
|
-
console.log(
|
|
1737
|
+
console.log(
|
|
1738
|
+
renderNewFilePreview(input.content, {
|
|
1739
|
+
label: input.path || '(unknown path)',
|
|
1740
|
+
maxLines: 20,
|
|
1741
|
+
})
|
|
1742
|
+
);
|
|
1631
1743
|
}
|
|
1632
1744
|
} catch {
|
|
1633
|
-
console.log(
|
|
1745
|
+
console.log(
|
|
1746
|
+
renderNewFilePreview(input.content, {
|
|
1747
|
+
label: input.path || '(unknown path)',
|
|
1748
|
+
maxLines: 20,
|
|
1749
|
+
})
|
|
1750
|
+
);
|
|
1634
1751
|
}
|
|
1635
1752
|
}
|
|
1636
1753
|
|
|
@@ -1640,7 +1757,11 @@ NGUYÊN TẮC:
|
|
|
1640
1757
|
const rawPath = typeof input?.path === 'string' && input.path.length > 0 ? input.path : null;
|
|
1641
1758
|
let absPath = null;
|
|
1642
1759
|
if (rawPath) {
|
|
1643
|
-
try {
|
|
1760
|
+
try {
|
|
1761
|
+
absPath = path.resolve(process.cwd(), rawPath);
|
|
1762
|
+
} catch {
|
|
1763
|
+
absPath = null;
|
|
1764
|
+
}
|
|
1644
1765
|
}
|
|
1645
1766
|
const fileKey = absPath ? name + ':' + absPath : null;
|
|
1646
1767
|
|
|
@@ -1777,11 +1898,18 @@ NGUYÊN TẮC:
|
|
|
1777
1898
|
} else {
|
|
1778
1899
|
const [, name, etype, obsRaw] = m2;
|
|
1779
1900
|
const observations = obsRaw
|
|
1780
|
-
? obsRaw
|
|
1901
|
+
? obsRaw
|
|
1902
|
+
.split(';')
|
|
1903
|
+
.map((s) => s.trim())
|
|
1904
|
+
.filter(Boolean)
|
|
1781
1905
|
: [];
|
|
1782
1906
|
const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
|
|
1783
|
-
if (!created.length)
|
|
1784
|
-
|
|
1907
|
+
if (!created.length)
|
|
1908
|
+
console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
|
|
1909
|
+
else
|
|
1910
|
+
console.log(
|
|
1911
|
+
c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`
|
|
1912
|
+
);
|
|
1785
1913
|
}
|
|
1786
1914
|
} else if (sub === 'obs') {
|
|
1787
1915
|
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
@@ -1791,7 +1919,8 @@ NGUYÊN TẮC:
|
|
|
1791
1919
|
const [, name, obs] = m2;
|
|
1792
1920
|
const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
|
|
1793
1921
|
const added = out[0]?.addedObservations || [];
|
|
1794
|
-
if (!added.length)
|
|
1922
|
+
if (!added.length)
|
|
1923
|
+
console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
|
|
1795
1924
|
else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
|
|
1796
1925
|
}
|
|
1797
1926
|
} else if (sub === 'link') {
|
|
@@ -1831,7 +1960,8 @@ NGUYÊN TẮC:
|
|
|
1831
1960
|
} else {
|
|
1832
1961
|
const names = rest.split(/\s+/).filter(Boolean);
|
|
1833
1962
|
const g = await kgOpenNodes(names);
|
|
1834
|
-
if (!g.entities.length)
|
|
1963
|
+
if (!g.entities.length)
|
|
1964
|
+
console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
|
|
1835
1965
|
else console.log(kgFormat(g));
|
|
1836
1966
|
}
|
|
1837
1967
|
} else if (sub === 'search') {
|
|
@@ -2073,7 +2203,14 @@ NGUYÊN TẮC:
|
|
|
2073
2203
|
case 'q': {
|
|
2074
2204
|
const swept = bgRegistry.sweepOnExit();
|
|
2075
2205
|
if (swept > 0)
|
|
2076
|
-
console.log(
|
|
2206
|
+
console.log(
|
|
2207
|
+
c.dim(
|
|
2208
|
+
' ' +
|
|
2209
|
+
(t.bgWorkflowSweep
|
|
2210
|
+
? t.bgWorkflowSweep(swept)
|
|
2211
|
+
: `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
|
|
2212
|
+
)
|
|
2213
|
+
);
|
|
2077
2214
|
persist();
|
|
2078
2215
|
exiting = true;
|
|
2079
2216
|
console.log(c.dim(' ' + t.bye));
|
|
@@ -2294,5 +2431,3 @@ function listModels() {
|
|
|
2294
2431
|
}
|
|
2295
2432
|
console.log('\n' + lines.join('\n') + c.dim('\n\n ' + t.modelListHint) + '\n');
|
|
2296
2433
|
}
|
|
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
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
60
|
+
// Context window cho HIỂN THỊ (ctx% ở status bar). Đặt 1M tokens.
|
|
61
|
+
// QUAN TRỌNG — đây là 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() {
|
package/src/tools.js
CHANGED
|
@@ -110,6 +110,13 @@ export function removeRoot(p) {
|
|
|
110
110
|
export function listRoots() {
|
|
111
111
|
return [cwd(), ...extraRoots];
|
|
112
112
|
}
|
|
113
|
+
// Xoá toàn bộ extraRoots khỏi bộ nhớ phiên (KHÔNG đụng dirs.json). Dùng cho test
|
|
114
|
+
// isolation: extraRoots là module-global, loadWorkspaceRoots() ở import nạp sẵn
|
|
115
|
+
// nội dung dirs.json → leak sang test. Gọi trong beforeEach để mỗi test thấy scope
|
|
116
|
+
// sạch (chỉ cwd đã chdir). KHÔNG persist — production không nên gọi.
|
|
117
|
+
export function clearRoots() {
|
|
118
|
+
extraRoots.clear();
|
|
119
|
+
}
|
|
113
120
|
function within(root, full) {
|
|
114
121
|
if (full === root) return true;
|
|
115
122
|
const rel = path.relative(root, full);
|
|
@@ -180,7 +187,10 @@ function htmlToText(html) {
|
|
|
180
187
|
s = s.replace(/<!--[\s\S]*?-->/g, ' ');
|
|
181
188
|
s = s.replace(/<(script|style|head|noscript|svg)\b[\s\S]*?<\/\1>/gi, ' ');
|
|
182
189
|
// Block-level tag → newline để giữ cấu trúc đoạn.
|
|
183
|
-
s = s.replace(
|
|
190
|
+
s = s.replace(
|
|
191
|
+
/<\/(p|div|section|article|header|footer|li|tr|h[1-6]|ul|ol|table|blockquote)>/gi,
|
|
192
|
+
'\n'
|
|
193
|
+
);
|
|
184
194
|
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
185
195
|
s = s.replace(/<[^>]+>/g, ' '); // gỡ mọi tag còn lại
|
|
186
196
|
// Decode entity phổ biến.
|
|
@@ -541,15 +551,45 @@ export const TOOLS = {
|
|
|
541
551
|
let out = '';
|
|
542
552
|
let timedOut = false;
|
|
543
553
|
let aborted = false;
|
|
554
|
+
let settled = false;
|
|
555
|
+
|
|
556
|
+
// Resolve ĐÚNG 1 lần. Không phụ thuộc child 'close' để thoát: gradle daemon
|
|
557
|
+
// / JVM grandchildren có thể giữ stdio pipe sống sau taskkill → 'close'
|
|
558
|
+
// không bao giờ bắn → Promise treo vĩnh viễn → tool kẹt → model không
|
|
559
|
+
// được gọi lại. Đây là gốc bug treo CLI khi chạy compileJava.
|
|
560
|
+
const finish = (text) => {
|
|
561
|
+
if (settled) return;
|
|
562
|
+
settled = true;
|
|
563
|
+
clearTimeout(killer);
|
|
564
|
+
clearTimeout(graceTimer);
|
|
565
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
566
|
+
resolve(text);
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Sau khi ra lệnh kill, không chờ 'close' mãi. Cho cây tiến trình 2s ân
|
|
570
|
+
// hạn để thoát sạch; quá hạn → resolve cưỡng bức để giải phóng tool.
|
|
571
|
+
let graceTimer = null;
|
|
572
|
+
const forceResolveAfterKill = () => {
|
|
573
|
+
if (graceTimer || settled) return;
|
|
574
|
+
graceTimer = setTimeout(() => {
|
|
575
|
+
const tail = aborted
|
|
576
|
+
? `\n[aborted by user (Ctrl+C) — killed (tiến trình con có thể còn sống nền).]`
|
|
577
|
+
: `\n[timed out after ${Math.round(timeout / 1000)}s — killed (tiến trình con có thể còn sống nền). Nếu là server / task chạy lâu, chạy lại với {"background": true}.]`;
|
|
578
|
+
finish(clip((out.trim() || '(no output)') + tail));
|
|
579
|
+
}, 2000);
|
|
580
|
+
};
|
|
581
|
+
|
|
544
582
|
const killer = setTimeout(() => {
|
|
545
583
|
timedOut = true;
|
|
546
584
|
killBgTree(child);
|
|
585
|
+
forceResolveAfterKill();
|
|
547
586
|
}, timeout);
|
|
548
587
|
// Ctrl+C trong lúc command đang chạy → kill cây tiến trình con (Windows
|
|
549
588
|
// dùng taskkill /T để diệt cả grand-children, vd npm spawn node).
|
|
550
589
|
const onAbort = () => {
|
|
551
590
|
aborted = true;
|
|
552
591
|
killBgTree(child);
|
|
592
|
+
forceResolveAfterKill();
|
|
553
593
|
};
|
|
554
594
|
if (signal) {
|
|
555
595
|
if (signal.aborted) onAbort();
|
|
@@ -558,19 +598,15 @@ export const TOOLS = {
|
|
|
558
598
|
child.stdout.on('data', (d) => (out += d));
|
|
559
599
|
child.stderr.on('data', (d) => (out += d));
|
|
560
600
|
child.on('error', (e) => {
|
|
561
|
-
|
|
562
|
-
signal?.removeEventListener?.('abort', onAbort);
|
|
563
|
-
resolve(`Failed to start command: ${e.message}`);
|
|
601
|
+
finish(`Failed to start command: ${e.message}`);
|
|
564
602
|
});
|
|
565
603
|
child.on('close', (code) => {
|
|
566
|
-
clearTimeout(killer);
|
|
567
|
-
signal?.removeEventListener?.('abort', onAbort);
|
|
568
604
|
const tail = aborted
|
|
569
605
|
? `\n[aborted by user (Ctrl+C) — killed.]`
|
|
570
606
|
: timedOut
|
|
571
607
|
? `\n[timed out after ${Math.round(timeout / 1000)}s — killed. If this is a server or other long-running task, re-run with {"background": true} instead.]`
|
|
572
608
|
: `\n[exit code ${code}]`;
|
|
573
|
-
|
|
609
|
+
finish(clip((out.trim() || '(no output)') + tail));
|
|
574
610
|
});
|
|
575
611
|
});
|
|
576
612
|
},
|
|
@@ -652,7 +688,10 @@ export const TOOLS = {
|
|
|
652
688
|
const out = await kgAddObservations([{ entityName, contents }]);
|
|
653
689
|
const added = out[0]?.addedObservations || [];
|
|
654
690
|
if (!added.length) return `Không có observation mới cho '${entityName}' (đã trùng)`;
|
|
655
|
-
return
|
|
691
|
+
return (
|
|
692
|
+
`Đã thêm ${added.length} observation cho '${entityName}':\n` +
|
|
693
|
+
added.map((o) => ` - ${o}`).join('\n')
|
|
694
|
+
);
|
|
656
695
|
},
|
|
657
696
|
};
|
|
658
697
|
|
package/src/tui.js
CHANGED
|
@@ -153,6 +153,10 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
153
153
|
setBusy() {},
|
|
154
154
|
setMeta() {},
|
|
155
155
|
setTodos() {},
|
|
156
|
+
setUltra() {},
|
|
157
|
+
setBgAgents() {},
|
|
158
|
+
pushAgentLog() {},
|
|
159
|
+
clearAgentLog() {},
|
|
156
160
|
setPrompt() {},
|
|
157
161
|
read() {
|
|
158
162
|
if (queue.length) return Promise.resolve(queue.shift());
|
|
@@ -180,6 +184,14 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
180
184
|
let busyLabel = '';
|
|
181
185
|
let busyMeta = ''; // chuỗi phụ (elapsed + token) repl đẩy vào qua setMeta()
|
|
182
186
|
let todos = []; // [{text, done}] — danh sách todo đang chạy, repl parse từ model output
|
|
187
|
+
let ultra = false; // ULTRA mode đang bật? → segment trong status bar
|
|
188
|
+
let bgAgents = 0; // số sub-agent / workflow chạy nền → segment + gợi ý Ctrl+O
|
|
189
|
+
// Ring buffer log sub-agent nền: KHÔNG in ra UI chính, chỉ vào đây. Ctrl+O mở
|
|
190
|
+
// overlay xem. Mỗi entry: chuỗi đã format (có thể kèm màu). Giữ tối đa MAX_LOG.
|
|
191
|
+
const MAX_AGENT_LOG = 500;
|
|
192
|
+
const agentLog = [];
|
|
193
|
+
let overlayOpen = false; // overlay log đang mở?
|
|
194
|
+
let overlayScroll = 0; // offset cuộn từ ĐÁY (0 = xem dòng mới nhất)
|
|
183
195
|
let frame = 0;
|
|
184
196
|
let frameTimer = null;
|
|
185
197
|
let prevRows = 0;
|
|
@@ -354,38 +366,69 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
354
366
|
return wrapText(liveOut, cols(), 2);
|
|
355
367
|
}
|
|
356
368
|
const spin = FRAMES[frame % FRAMES.length];
|
|
357
|
-
//
|
|
369
|
+
// Badge ghép vào mọi dòng status: ULTRA bật/tắt + số sub-agent nền (gợi ý
|
|
370
|
+
// Ctrl+O xem log). Reset ANSI từng đoạn (chalk tự đóng) → không bleed.
|
|
371
|
+
const ultraBadge = ultra ? c.err('🚀 ULTRA') : '';
|
|
372
|
+
const bgBadge = bgAgents ? c.accent(`⊕ ${bgAgents} nền`) + c.dim(' (Ctrl+O)') : '';
|
|
373
|
+
const badges = [ultraBadge, bgBadge].filter(Boolean).join(c.dim(' · '));
|
|
374
|
+
const badgeTail = badges ? c.dim(' · ') + badges : '';
|
|
375
|
+
// Todo progress: 1 dòng gọn dùng chung (📋 done/total bar pct%). Dựng SẴN ở
|
|
376
|
+
// đây để ghép vào status line theo trạng thái — KHÔNG return sớm nuốt status.
|
|
377
|
+
// Rảnh → chỉ dòng này. Busy → ghép cạnh spinner. Mất todo không nuốt spinner.
|
|
378
|
+
let progress = null;
|
|
379
|
+
let taskLine = null;
|
|
358
380
|
if (todos.length) {
|
|
359
381
|
const done = todos.filter((t) => t.done).length;
|
|
360
382
|
const total = todos.length;
|
|
361
383
|
const current = todos.find((t) => !t.done);
|
|
362
384
|
const pct = Math.round((done / total) * 100);
|
|
363
|
-
// Thanh progress:
|
|
364
|
-
const barW = Math.min(
|
|
385
|
+
// Thanh progress dày hơn, màu phân biệt: phần done = ember, còn lại = dim.
|
|
386
|
+
const barW = Math.min(16, Math.max(8, Math.floor(cols() * 0.2)));
|
|
365
387
|
const filled = Math.round((done / total) * barW);
|
|
366
|
-
const bar = '█'.repeat(filled) + '░'.repeat(barW - filled);
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const tail = busy ? c.dim(' · Ctrl+C') : '';
|
|
374
|
-
return [progress + meta + tail, taskLine];
|
|
388
|
+
const bar = c.accent('█'.repeat(filled)) + c.dim('░'.repeat(barW - filled));
|
|
389
|
+
progress =
|
|
390
|
+
c.accent('📋 ') + c.ok(`${done}`) + c.dim(`/${total}`) + ' ' + bar + c.dim(` ${pct}%`);
|
|
391
|
+
// Task đang làm (▸ amber) — CHỈ dùng lúc busy (dòng 2). Rảnh bỏ để gọn 1 dòng.
|
|
392
|
+
taskLine = current
|
|
393
|
+
? c.tool(' ▸ ') + c.user(truncStr(current.text, cols() - 8))
|
|
394
|
+
: c.ok(' ✓ tất cả todo đã xong!');
|
|
375
395
|
}
|
|
396
|
+
|
|
376
397
|
// Khi busy, LUÔN hiện elapsed+tokens (busyMeta) cạnh statusText/busyLabel để
|
|
377
398
|
// người dùng thấy phiên đang sống — kể cả lúc model im giữa các bước.
|
|
378
399
|
if (statusText) {
|
|
379
400
|
const meta = busy && busyMeta ? c.dim(' · ' + busyMeta) : '';
|
|
380
401
|
const tail = busy ? c.dim(' · Ctrl+C để dừng') : '';
|
|
381
|
-
|
|
402
|
+
// Busy + todos → spinner + progress CHUNG dòng 1, task line dòng 2.
|
|
403
|
+
if (busy && progress) {
|
|
404
|
+
const line = c.dim(spin + ' ') + progress + meta + tail + badgeTail;
|
|
405
|
+
const out = wrapText(line, cols(), 2);
|
|
406
|
+
if (taskLine) out.push(taskLine);
|
|
407
|
+
return out;
|
|
408
|
+
}
|
|
409
|
+
const line = c.dim(spin + ' ') + statusText + meta + tail + badgeTail;
|
|
382
410
|
return wrapText(line, cols(), 2);
|
|
383
411
|
}
|
|
384
412
|
if (busy) {
|
|
385
|
-
const meta = busyMeta ? ' · ' + busyMeta : '';
|
|
386
|
-
|
|
413
|
+
const meta = busyMeta ? c.dim(' · ' + busyMeta) : '';
|
|
414
|
+
// Busy + todos → spinner + progress chung dòng 1, task line dòng 2.
|
|
415
|
+
if (progress) {
|
|
416
|
+
const line = c.dim(spin + ' ') + progress + meta + c.dim(' · Ctrl+C') + badgeTail;
|
|
417
|
+
const out = wrapText(line, cols(), 2);
|
|
418
|
+
if (taskLine) out.push(taskLine);
|
|
419
|
+
return out;
|
|
420
|
+
}
|
|
421
|
+
const line =
|
|
422
|
+
c.dim(spin + ' ' + (busyLabel || 'đang chạy')) +
|
|
423
|
+
meta +
|
|
424
|
+
c.dim(' · Ctrl+C để dừng') +
|
|
425
|
+
badgeTail;
|
|
387
426
|
return wrapText(line, cols(), 2);
|
|
388
427
|
}
|
|
428
|
+
// Rảnh + todos → 1 dòng progress gọn (KHÔNG task/ctx line, không nuốt ô nhập).
|
|
429
|
+
if (progress) return wrapText(progress + badgeTail, cols(), 2);
|
|
430
|
+
// Rảnh nhưng có ULTRA / sub-agent nền → vẫn hiện badge để user biết trạng thái.
|
|
431
|
+
if (badges) return wrapText(' ' + badges, cols(), 2);
|
|
389
432
|
return null;
|
|
390
433
|
}
|
|
391
434
|
function menuRows() {
|
|
@@ -405,11 +448,45 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
405
448
|
);
|
|
406
449
|
});
|
|
407
450
|
}
|
|
451
|
+
// Overlay log sub-agent nền (Ctrl+O). Thay thế vùng output chính (top/menu)
|
|
452
|
+
// bằng panel cuộn được: header + N dòng log gần nhất theo overlayScroll. KHÔNG
|
|
453
|
+
// đụng renderBar/cursor math — overlay chỉ là các dòng phía TRÊN thanh nhập.
|
|
454
|
+
const OVERLAY_LINES = 12;
|
|
455
|
+
function overlayRows() {
|
|
456
|
+
const head =
|
|
457
|
+
c.accent('┄┄ log sub-agent nền ') +
|
|
458
|
+
c.dim(`(${agentLog.length} dòng · ↑/↓ cuộn · Ctrl+O đóng)`);
|
|
459
|
+
if (!agentLog.length) {
|
|
460
|
+
return [head, c.dim(' (chưa có log — sub-agent nền sẽ ghi vào đây)')];
|
|
461
|
+
}
|
|
462
|
+
// Cửa sổ [start, end) tính từ ĐÁY theo overlayScroll (0 = đáy/mới nhất).
|
|
463
|
+
const end = Math.max(0, agentLog.length - overlayScroll);
|
|
464
|
+
const start = Math.max(0, end - OVERLAY_LINES);
|
|
465
|
+
const body = [];
|
|
466
|
+
const width = Math.max(8, cols() - 2);
|
|
467
|
+
for (const raw of agentLog.slice(start, end)) {
|
|
468
|
+
// Mỗi entry có thể dài → soft-wrap, nhưng chỉ lấy dòng đầu + '…' để panel
|
|
469
|
+
// gọn (1 entry = 1 dòng panel). User cuộn để xem từng entry.
|
|
470
|
+
const oneLine = String(raw).replace(/\n/g, ' ');
|
|
471
|
+
const wrapped = softWrapLine(oneLine, width);
|
|
472
|
+
body.push(
|
|
473
|
+
' ' + (wrapped.length > 1 ? wrapped[0].replace(ANSI_RE, '') + c.dim('…') : wrapped[0])
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const more = overlayScroll > 0 ? c.dim(` ↓ còn ${overlayScroll} dòng mới hơn`) : '';
|
|
477
|
+
return more ? [head, ...body, more] : [head, ...body];
|
|
478
|
+
}
|
|
408
479
|
function rows() {
|
|
409
480
|
const r = [];
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
481
|
+
if (overlayOpen) {
|
|
482
|
+
// Overlay mở: thay top/menu bằng panel log. Thanh nhập vẫn vẽ bên dưới để
|
|
483
|
+
// user gõ /lệnh (vd Ctrl+O đóng) mà không mất con trỏ.
|
|
484
|
+
r.push(...overlayRows());
|
|
485
|
+
} else {
|
|
486
|
+
const top = topRow();
|
|
487
|
+
if (top !== null) r.push(...top);
|
|
488
|
+
for (const mr of menuRows()) r.push(mr);
|
|
489
|
+
}
|
|
413
490
|
const bar = renderBar();
|
|
414
491
|
if (bar) r.push(...bar.split('\n'));
|
|
415
492
|
return r;
|
|
@@ -532,9 +609,31 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
532
609
|
refreshMenu();
|
|
533
610
|
draw();
|
|
534
611
|
}
|
|
612
|
+
// Echo tin nhắn user vừa gửi thành KHỐI nền màu, gutter ember mỗi dòng. Plain
|
|
613
|
+
// text (không phải coloredInput) để bg phủ đều; mỗi dòng tự reset (tránh bleed
|
|
614
|
+
// màu sang hết chiều rộng terminal). Paste chip → 1 dòng tóm tắt [pasted...].
|
|
615
|
+
function echoUserBlock() {
|
|
616
|
+
const plain = cells.map(cellPlain).join('');
|
|
617
|
+
if (!plain.trim()) return promptLabel + coloredInput();
|
|
618
|
+
const gutter = c.userGutter('▌') + ' ';
|
|
619
|
+
const gutterW = 2; // '▌' + space (▌ là 1 cột)
|
|
620
|
+
const budget = Math.max(8, cols() - gutterW);
|
|
621
|
+
const wrapped = plain
|
|
622
|
+
.split('\n')
|
|
623
|
+
.flatMap((ln) => softWrapLine(ln, budget))
|
|
624
|
+
.map((ln) => {
|
|
625
|
+
// softWrapLine có thể đính RESET cuối khi text gốc có ANSI; strip để bg
|
|
626
|
+
// phủ liền, rồi pad tới budget cho khối nền chữ nhật đều mép phải.
|
|
627
|
+
const body = ln.replace(ANSI_RE, '');
|
|
628
|
+
const padded = body + ' '.repeat(Math.max(0, budget - visLen(body)));
|
|
629
|
+
return gutter + c.userBg(' ' + padded + ' ');
|
|
630
|
+
});
|
|
631
|
+
return wrapped.join('\n');
|
|
632
|
+
}
|
|
633
|
+
|
|
535
634
|
function submit() {
|
|
536
635
|
const full = fullText();
|
|
537
|
-
const echo =
|
|
636
|
+
const echo = echoUserBlock();
|
|
538
637
|
if (full.trim() && submitHistory[submitHistory.length - 1] !== full) {
|
|
539
638
|
submitHistory.push(full);
|
|
540
639
|
if (submitHistory.length > 200) submitHistory.shift();
|
|
@@ -616,8 +715,13 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
616
715
|
if (fin === 'Z')
|
|
617
716
|
onShiftTab?.(); // Shift+Tab = bật/tắt yolo
|
|
618
717
|
else if (fin === 'A' || fin === 'B') {
|
|
619
|
-
// ↑/↓:
|
|
620
|
-
if (
|
|
718
|
+
// ↑/↓: overlay mở → cuộn log; menu mở → điều hướng menu; còn lại → lịch sử.
|
|
719
|
+
if (overlayOpen) {
|
|
720
|
+
// ↑ = lùi về dòng cũ (tăng scroll), ↓ = về đáy (giảm scroll).
|
|
721
|
+
if (fin === 'A') overlayScroll = Math.min(agentLog.length, overlayScroll + 1);
|
|
722
|
+
else overlayScroll = Math.max(0, overlayScroll - 1);
|
|
723
|
+
draw();
|
|
724
|
+
} else if (menu.length) {
|
|
621
725
|
menuIdx =
|
|
622
726
|
fin === 'A' ? (menuIdx - 1 + menu.length) % menu.length : (menuIdx + 1) % menu.length;
|
|
623
727
|
draw();
|
|
@@ -689,6 +793,13 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
689
793
|
i++;
|
|
690
794
|
continue;
|
|
691
795
|
} // Ctrl+L: clear screen
|
|
796
|
+
if (ch === '\x0f') {
|
|
797
|
+
overlayOpen = !overlayOpen;
|
|
798
|
+
overlayScroll = 0; // mở lại = xem đáy (dòng mới nhất)
|
|
799
|
+
draw();
|
|
800
|
+
i++;
|
|
801
|
+
continue;
|
|
802
|
+
} // Ctrl+O: bật/tắt overlay log sub-agent nền
|
|
692
803
|
if (ch === '\x01') {
|
|
693
804
|
cur = 0;
|
|
694
805
|
draw();
|
|
@@ -818,6 +929,27 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
|
|
|
818
929
|
todos = Array.isArray(items) ? items : [];
|
|
819
930
|
draw();
|
|
820
931
|
},
|
|
932
|
+
setUltra(on) {
|
|
933
|
+
ultra = !!on;
|
|
934
|
+
draw();
|
|
935
|
+
},
|
|
936
|
+
setBgAgents(n) {
|
|
937
|
+
bgAgents = Math.max(0, n | 0);
|
|
938
|
+
draw();
|
|
939
|
+
},
|
|
940
|
+
// Đẩy 1 dòng log sub-agent nền vào ring buffer (KHÔNG in ra UI chính). Nếu
|
|
941
|
+
// overlay đang mở + đang xem đáy (scroll=0) → redraw để thấy dòng mới ngay.
|
|
942
|
+
pushAgentLog(line) {
|
|
943
|
+
if (line == null) return;
|
|
944
|
+
agentLog.push(String(line));
|
|
945
|
+
if (agentLog.length > MAX_AGENT_LOG) agentLog.shift();
|
|
946
|
+
if (overlayOpen && overlayScroll === 0) draw();
|
|
947
|
+
},
|
|
948
|
+
clearAgentLog() {
|
|
949
|
+
agentLog.length = 0;
|
|
950
|
+
overlayScroll = 0;
|
|
951
|
+
if (overlayOpen) draw();
|
|
952
|
+
},
|
|
821
953
|
setPrompt(label) {
|
|
822
954
|
promptLabel = label || '';
|
|
823
955
|
draw();
|
package/src/ui.js
CHANGED
|
@@ -26,6 +26,10 @@ export const c = {
|
|
|
26
26
|
ok: chalk.hex('#84a98c'),
|
|
27
27
|
err: chalk.hex('#c0524b'),
|
|
28
28
|
accent: chalk.hex('#ff7a45'),
|
|
29
|
+
// Tin nhắn user gửi: nền warm-gray đậm + chữ paper-white → khối phân biệt rõ
|
|
30
|
+
// trong scrollback (không lẫn với prose model amber). Gutter ember ▌ ở mép trái.
|
|
31
|
+
userBg: chalk.bgHex('#3a342c').hex('#f5f1e8'),
|
|
32
|
+
userGutter: chalk.hex('#ff7a45'),
|
|
29
33
|
};
|
|
30
34
|
|
|
31
35
|
// Single-tone "gradient" giữ tương thích API gradient-string nội bộ. Banner
|
|
@@ -42,11 +46,9 @@ const term = () => process.stdout.columns || 80;
|
|
|
42
46
|
// phân cách bằng ◆ ember nhỏ thay vì dấu · xám.
|
|
43
47
|
export function banner() {
|
|
44
48
|
const sidebar = chalk.hex('#ff7a45')('▌');
|
|
45
|
-
const wordmark = [
|
|
46
|
-
'
|
|
47
|
-
'
|
|
48
|
-
'╹ ╹┗━┛┗━┛┗━┛',
|
|
49
|
-
].map((row) => sidebar + ' ' + brand(row)).join('\n');
|
|
49
|
+
const wordmark = ['┏┓╻┏━┓┏━┓┏┓ ', '┃┗┫┃ ┃┃ ┃┣┻┓', '╹ ╹┗━┛┗━┛┗━┛']
|
|
50
|
+
.map((row) => sidebar + ' ' + brand(row))
|
|
51
|
+
.join('\n');
|
|
50
52
|
const diamond = chalk.hex('#ff7a45')('◆');
|
|
51
53
|
const tag = chalk.hex('#e8e3d8').italic(t.tagline);
|
|
52
54
|
const meta = ' ' + c.dim('agentic terminal') + ' ' + diamond + ' ' + tag;
|
package/src/workflow-bg.js
CHANGED
|
@@ -30,6 +30,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
30
30
|
execTool,
|
|
31
31
|
spawnAgentToolsDoc,
|
|
32
32
|
cwd = process.cwd(),
|
|
33
|
+
tui = null, // route log sub-agent nền + cập nhật badge số agent nền (optional; null trong test)
|
|
33
34
|
} = deps;
|
|
34
35
|
const _runAgent = deps.runAgent || runAgent;
|
|
35
36
|
const _createRun = deps.createRun || createRun;
|
|
@@ -40,8 +41,14 @@ export function createBgRegistry(deps = {}) {
|
|
|
40
41
|
// Hàng đợi run đã settle, chờ main loop drain.
|
|
41
42
|
const completions = [];
|
|
42
43
|
|
|
43
|
-
const activeCount = () =>
|
|
44
|
-
|
|
44
|
+
const activeCount = () => [...runs.values()].filter((r) => r.status === 'running').length;
|
|
45
|
+
|
|
46
|
+
// Cập nhật badge số agent nền trên status bar (no-op nếu không có tui).
|
|
47
|
+
const syncBadge = () => {
|
|
48
|
+
try {
|
|
49
|
+
tui?.setBgAgents?.(activeCount());
|
|
50
|
+
} catch {}
|
|
51
|
+
};
|
|
45
52
|
|
|
46
53
|
// Kick off 1 bg workflow. Trả về { ok, id } hoặc { ok:false, error }.
|
|
47
54
|
function startBgWorkflow({ prompt, name = null } = {}) {
|
|
@@ -71,6 +78,10 @@ export function createBgRegistry(deps = {}) {
|
|
|
71
78
|
error: null,
|
|
72
79
|
};
|
|
73
80
|
runs.set(run.id, entry);
|
|
81
|
+
try {
|
|
82
|
+
tui?.pushAgentLog?.(`🎼 workflow nền bắt đầu — run ${run.id}${name ? ' · ' + name : ''}`);
|
|
83
|
+
} catch {}
|
|
84
|
+
syncBadge();
|
|
74
85
|
|
|
75
86
|
// workflowRun riêng cho bg dispatcher: journal cache dùng run.data này, KHÔNG
|
|
76
87
|
// đụng state.workflowRun của turn (turn có thể đang chạy /workflow blocking).
|
|
@@ -90,8 +101,9 @@ export function createBgRegistry(deps = {}) {
|
|
|
90
101
|
stopSpin: noop,
|
|
91
102
|
startSpin: noop,
|
|
92
103
|
execTool,
|
|
93
|
-
tui
|
|
104
|
+
tui, // sub-agent log → ring buffer (Ctrl+O), KHÔNG in ra UI chính
|
|
94
105
|
c: null,
|
|
106
|
+
bgAgentLog: true, // cờ: dispatcher route onLog → tui.pushAgentLog thay console.log
|
|
95
107
|
});
|
|
96
108
|
|
|
97
109
|
// Chạy headless. runAgent options TUI-bound (onStatus/onDelta/onSteer) bỏ —
|
|
@@ -111,18 +123,32 @@ export function createBgRegistry(deps = {}) {
|
|
|
111
123
|
.then((answer) => {
|
|
112
124
|
entry.status = 'done';
|
|
113
125
|
entry.result = typeof answer === 'string' ? answer : String(answer ?? '');
|
|
114
|
-
try {
|
|
126
|
+
try {
|
|
127
|
+
_closeRun(run.data, 'done', cwd);
|
|
128
|
+
} catch {}
|
|
129
|
+
try {
|
|
130
|
+
tui?.pushAgentLog?.(`✓ workflow nền xong — run ${run.id}`);
|
|
131
|
+
} catch {}
|
|
115
132
|
completions.push({ id: run.id, name, status: 'done', result: entry.result });
|
|
133
|
+
syncBadge();
|
|
116
134
|
})
|
|
117
135
|
.catch((err) => {
|
|
118
136
|
const isAbort = err?.message === 'aborted' || err?.name === 'AbortError';
|
|
119
137
|
entry.status = isAbort ? 'interrupted' : 'failed';
|
|
120
138
|
entry.error = err?.message || String(err);
|
|
121
|
-
try {
|
|
139
|
+
try {
|
|
140
|
+
_closeRun(run.data, entry.status, cwd);
|
|
141
|
+
} catch {}
|
|
142
|
+
try {
|
|
143
|
+
tui?.pushAgentLog?.(
|
|
144
|
+
`${isAbort ? '⊘ huỷ' : '✗ lỗi'} workflow nền — run ${run.id}${entry.error ? ': ' + entry.error : ''}`
|
|
145
|
+
);
|
|
146
|
+
} catch {}
|
|
122
147
|
// Abort do stopBg/sweepOnExit → KHÔNG enqueue completion (user chủ động huỷ).
|
|
123
148
|
if (!isAbort) {
|
|
124
149
|
completions.push({ id: run.id, name, status: entry.status, error: entry.error });
|
|
125
150
|
}
|
|
151
|
+
syncBadge();
|
|
126
152
|
});
|
|
127
153
|
entry.promise = promise;
|
|
128
154
|
|
|
@@ -143,6 +169,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
143
169
|
const entry = runs.get(id);
|
|
144
170
|
if (!entry || entry.status !== 'running') return false;
|
|
145
171
|
entry.controller.abort();
|
|
172
|
+
syncBadge();
|
|
146
173
|
return true;
|
|
147
174
|
}
|
|
148
175
|
|
|
@@ -157,6 +184,7 @@ export function createBgRegistry(deps = {}) {
|
|
|
157
184
|
n++;
|
|
158
185
|
}
|
|
159
186
|
}
|
|
187
|
+
if (n) syncBadge();
|
|
160
188
|
return n;
|
|
161
189
|
}
|
|
162
190
|
|
package/src/workflow-runs.js
CHANGED
|
@@ -70,7 +70,10 @@ export function hashTask({ task, context, model }) {
|
|
|
70
70
|
|
|
71
71
|
// Tên file an toàn — chỉ [a-z0-9-_], thay ký tự không hợp lệ bằng `_`.
|
|
72
72
|
function sanitizeId(s) {
|
|
73
|
-
return String(s || '')
|
|
73
|
+
return String(s || '')
|
|
74
|
+
.toLowerCase()
|
|
75
|
+
.replace(/[^a-z0-9_-]/g, '_')
|
|
76
|
+
.slice(0, 80);
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
function makeId(name) {
|