dave-code 1.1.0 → 1.2.0
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/README.md +25 -6
- package/bin/aiClient.js +292 -67
- package/bin/check.js +11 -0
- package/bin/cliMenu.js +205 -68
- package/bin/commandRouter.js +20 -0
- package/bin/configManager.js +71 -47
- package/bin/contextManager.js +107 -91
- package/bin/index.js +1413 -243
- package/bin/markdownRenderer.js +264 -0
- package/bin/memoryManager.js +182 -0
- package/bin/planManager.js +61 -9
- package/bin/projectNotebookManager.js +839 -0
- package/bin/runtimeEvents.js +42 -0
- package/bin/scanManager.js +561 -0
- package/bin/sessionManager.js +17 -1
- package/bin/terminalRenderer.js +543 -125
- package/bin/textWidth.js +194 -0
- package/bin/thunderManager.js +302 -0
- package/bin/thunderOrchestrator.js +263 -0
- package/bin/thunderPrompts.js +53 -0
- package/bin/thunderRenderer.js +200 -0
- package/bin/toolRuntime.js +688 -133
- package/package.json +3 -5
package/bin/index.js
CHANGED
|
@@ -16,7 +16,7 @@ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.ur
|
|
|
16
16
|
const packageVersion = pkg.version;
|
|
17
17
|
import { spawn } from 'child_process';
|
|
18
18
|
import { getLogoLines } from './logoRenderer.js';
|
|
19
|
-
import { hasApiKey, getAIResponse, streamAIResponse, generateTitle, sessionTokenUsage, resetTokenUsage } from './aiClient.js';
|
|
19
|
+
import { hasApiKey, getAIResponse, streamAIResponse, generateTitle, extractMemoryCandidates, summarizeForCompaction, sessionTokenUsage, resetTokenUsage } from './aiClient.js';
|
|
20
20
|
import { listSessions, saveSession, deleteSession } from './sessionManager.js';
|
|
21
21
|
import {
|
|
22
22
|
CONFIG_FILE,
|
|
@@ -25,9 +25,7 @@ import {
|
|
|
25
25
|
setActiveProfile,
|
|
26
26
|
updateProfile,
|
|
27
27
|
openConfigFileInEditor,
|
|
28
|
-
|
|
29
|
-
setActiveEffort,
|
|
30
|
-
EFFORT_PRESETS,
|
|
28
|
+
inferContextWindowTokens,
|
|
31
29
|
isFirstLaunch,
|
|
32
30
|
setInitialized,
|
|
33
31
|
lastConfigError
|
|
@@ -38,20 +36,68 @@ import {
|
|
|
38
36
|
secureInputPrompt,
|
|
39
37
|
chatInputPrompt,
|
|
40
38
|
waitForEnter,
|
|
41
|
-
|
|
39
|
+
selectWorkModeMenu,
|
|
40
|
+
runThunderActivationAnimation,
|
|
42
41
|
confirmPrompt,
|
|
42
|
+
permissionPrompt,
|
|
43
43
|
textInputPrompt
|
|
44
44
|
} from './cliMenu.js';
|
|
45
|
-
import { prepareModelMessages, stripReasoningBlocks, getContextStats } from './contextManager.js';
|
|
46
|
-
import {
|
|
45
|
+
import { estimateTokens, prepareModelMessages, shouldCompact, stripReasoningBlocks, getContextStats } from './contextManager.js';
|
|
46
|
+
import { buildScanSnapshot, planHighwayNotebookBuild, runAdaptiveScan } from './scanManager.js';
|
|
47
|
+
import {
|
|
48
|
+
clearProjectNotebook,
|
|
49
|
+
ensureHighwayProjectNotebook,
|
|
50
|
+
getProjectNotebookStatus,
|
|
51
|
+
loadProjectNotebook,
|
|
52
|
+
markProjectNotebookStale,
|
|
53
|
+
readProjectNotebook
|
|
54
|
+
} from './projectNotebookManager.js';
|
|
55
|
+
import {
|
|
56
|
+
parseToolCall,
|
|
57
|
+
executeToolCall,
|
|
58
|
+
createToolCallSignature,
|
|
59
|
+
resolveBudgetFinishResponse,
|
|
60
|
+
resolveRepeatedToolRequest,
|
|
61
|
+
resolveExistingWorkspacePath,
|
|
62
|
+
resolveWorkspacePath,
|
|
63
|
+
undoLastMutation,
|
|
64
|
+
closeWorkspaceShell,
|
|
65
|
+
SUPPORTED_TOOLS,
|
|
66
|
+
MUTATING_TOOLS
|
|
67
|
+
} from './toolRuntime.js';
|
|
47
68
|
import { createRuntimeEvents } from './runtimeEvents.js';
|
|
48
|
-
import { createTerminalRenderer, sanitizeUntrustedText } from './terminalRenderer.js';
|
|
69
|
+
import { createTerminalRenderer, formatConversationMessage, sanitizeUntrustedText } from './terminalRenderer.js';
|
|
70
|
+
import { displayWidth, displayWidth as getStringWidth, padCenter as centerLine, padEnd as padLine, truncateMiddle } from './textWidth.js';
|
|
71
|
+
import { createThunderRenderer } from './thunderRenderer.js';
|
|
49
72
|
import { parseInputCommand } from './commandRouter.js';
|
|
73
|
+
import {
|
|
74
|
+
formatThunderResourceProposal,
|
|
75
|
+
runThunderPlanning,
|
|
76
|
+
runThunderPostReview,
|
|
77
|
+
suggestThunderPlanName
|
|
78
|
+
} from './thunderOrchestrator.js';
|
|
79
|
+
import {
|
|
80
|
+
addThunderMessage,
|
|
81
|
+
findThunderTeamForPlan,
|
|
82
|
+
getThunderTeam,
|
|
83
|
+
thunderSummary,
|
|
84
|
+
updateThunderTeam
|
|
85
|
+
} from './thunderManager.js';
|
|
86
|
+
import { buildThunderSystemPrompt } from './thunderPrompts.js';
|
|
87
|
+
import {
|
|
88
|
+
clearMemories,
|
|
89
|
+
deleteMemory,
|
|
90
|
+
formatMemoriesForPrompt,
|
|
91
|
+
memorySummary,
|
|
92
|
+
mergeMemoryCandidates,
|
|
93
|
+
retrieveMemories,
|
|
94
|
+
setMemoryEnabled
|
|
95
|
+
} from './memoryManager.js';
|
|
50
96
|
import {
|
|
51
97
|
deletePlan,
|
|
52
98
|
findPlan,
|
|
53
99
|
listPlans,
|
|
54
|
-
|
|
100
|
+
planStatusPanel,
|
|
55
101
|
renamePlan,
|
|
56
102
|
updatePlan,
|
|
57
103
|
upsertPlan,
|
|
@@ -96,9 +142,14 @@ const locales = {
|
|
|
96
142
|
' /config - 编辑本地配置文件 (~/.dave-code-config.json)',
|
|
97
143
|
' /model - 快捷切换当前激活的服务配置',
|
|
98
144
|
' /api - 快捷修改当前配置的 API 密钥',
|
|
99
|
-
' /
|
|
145
|
+
' /mode highway|thunder - 切换单 Agent 或办公室团队工作流',
|
|
146
|
+
' /thunder <需求> - 切换到 Thunder 并开始团队规划',
|
|
147
|
+
' /agents - 查看当前工作区的 Thunder 团队',
|
|
148
|
+
' /memory - 查看、开关或删除当前工作区的跨聊天记忆',
|
|
149
|
+
' /note status|refresh|rebuild|clear - 管理 Highway 后台项目笔记',
|
|
100
150
|
' /plan 名称: 需求 - 创建只读实施计划;/plan 管理已有计划',
|
|
101
151
|
' /code <需求或计划名> - 单次授权编程修改,仍逐项确认',
|
|
152
|
+
' /undo - 回滚本进程内最近一次文件变更',
|
|
102
153
|
' /open - 打开文件或文件夹并在其中工作',
|
|
103
154
|
' /exit - 退出 Dave Code 代理'
|
|
104
155
|
],
|
|
@@ -158,9 +209,14 @@ const locales = {
|
|
|
158
209
|
' /config - Open and edit config file (~/.dave-code-config.json)',
|
|
159
210
|
' /model - Interactively switch active service profile',
|
|
160
211
|
' /api - Interactively set API key for active profile',
|
|
161
|
-
' /
|
|
212
|
+
' /mode highway|thunder - Switch between single-agent and office-team workflow',
|
|
213
|
+
' /thunder <request> - Switch to Thunder and start team planning',
|
|
214
|
+
' /agents - View Thunder teams for this workspace',
|
|
215
|
+
' /memory - View, toggle, or delete cross-chat workspace memories',
|
|
216
|
+
' /note status|refresh|rebuild|clear - Manage the private Highway project notebook',
|
|
162
217
|
' /plan name: request - Create a read-only implementation plan; /plan manages plans',
|
|
163
218
|
' /code <request or plan> - Authorize one confirmed coding turn',
|
|
219
|
+
' /undo - Undo the most recent file mutation in this process',
|
|
164
220
|
' /open - Open a file or directory to work in',
|
|
165
221
|
' /exit - Exit the Dave Code agent'
|
|
166
222
|
],
|
|
@@ -193,24 +249,34 @@ let workspaceRoot = process.cwd();
|
|
|
193
249
|
let workspaceReady = true;
|
|
194
250
|
let currentSessionId = null;
|
|
195
251
|
let currentSessionTitle = 'New Chat';
|
|
252
|
+
let workMode = 'highway';
|
|
253
|
+
let currentContextPlan = null;
|
|
254
|
+
let currentScanSummary = '';
|
|
255
|
+
let currentReadBrief = '';
|
|
196
256
|
|
|
197
257
|
function initSession() {
|
|
198
258
|
currentSessionId = crypto.randomUUID();
|
|
199
259
|
currentSessionTitle = currentLang === 'cn' ? '新会话' : 'New Chat';
|
|
200
260
|
messages = [];
|
|
261
|
+
workMode = 'highway';
|
|
262
|
+
currentContextPlan = null;
|
|
263
|
+
currentScanSummary = '';
|
|
264
|
+
currentReadBrief = '';
|
|
201
265
|
saveCurrentSession();
|
|
202
266
|
}
|
|
203
267
|
|
|
204
|
-
function saveCurrentSession() {
|
|
268
|
+
function saveCurrentSession(allowEmpty = false) {
|
|
205
269
|
if (!currentSessionId) return;
|
|
206
|
-
if (messages.length === 0) return;
|
|
270
|
+
if (!allowEmpty && messages.length === 0) return;
|
|
207
271
|
const sessionData = {
|
|
208
272
|
id: currentSessionId,
|
|
209
273
|
title: currentSessionTitle,
|
|
210
274
|
timestamp: Date.now(),
|
|
211
275
|
messages: messages,
|
|
212
276
|
workspaceRoot: workspaceReady ? workspaceRoot : null,
|
|
213
|
-
activeOpenFile: workspaceReady && activeOpenFile ? activeOpenFile : null
|
|
277
|
+
activeOpenFile: workspaceReady && activeOpenFile ? activeOpenFile : null,
|
|
278
|
+
workMode,
|
|
279
|
+
contextPlan: currentContextPlan
|
|
214
280
|
};
|
|
215
281
|
saveSession(currentSessionId, sessionData);
|
|
216
282
|
}
|
|
@@ -226,61 +292,22 @@ function redrawScreen(messageText = '') {
|
|
|
226
292
|
if (cleanHistory.length > 0) {
|
|
227
293
|
console.log(currentLang === 'cn' ? '\x1b[90m--- 当前对话内容 (Current Chat) ---\x1b[0m' : '\x1b[90m--- Current Chat Context ---\x1b[0m');
|
|
228
294
|
cleanHistory.forEach(m => {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.log(`\x1b[1;38;2;250;100;30mDave\x1b[0m: ${displayContent}`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
295
|
+
const displayContent = stripReasoningBlocks(m.displayContent || m.content);
|
|
296
|
+
const rendered = formatConversationMessage({ ...m, displayContent }, {
|
|
297
|
+
color: Boolean(process.stdout.isTTY && !process.env.NO_COLOR),
|
|
298
|
+
width: Math.max(24, Math.min(process.stdout.columns || 96, 120))
|
|
299
|
+
});
|
|
300
|
+
if (rendered) console.log(`\n${rendered}`);
|
|
238
301
|
});
|
|
239
302
|
console.log('\x1b[90m─────────────────────────────────\x1b[0m\n');
|
|
240
303
|
}
|
|
241
304
|
}
|
|
242
305
|
|
|
243
|
-
function
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
250
|
-
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
251
|
-
(code >= 0x3000 && code <= 0x303f) ||
|
|
252
|
-
(code >= 0xff00 && code <= 0xffef)
|
|
253
|
-
) {
|
|
254
|
-
width += 2;
|
|
255
|
-
} else {
|
|
256
|
-
width += 1;
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
return width;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function centerLine(str, width) {
|
|
263
|
-
const strWidth = getStringWidth(str);
|
|
264
|
-
if (strWidth >= width) return str;
|
|
265
|
-
const leftPad = Math.floor((width - strWidth) / 2);
|
|
266
|
-
const rightPad = width - strWidth - leftPad;
|
|
267
|
-
return ' '.repeat(leftPad) + str + ' '.repeat(rightPad);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function padLine(str, width) {
|
|
271
|
-
const strWidth = getStringWidth(str);
|
|
272
|
-
if (strWidth >= width) return str;
|
|
273
|
-
return str + ' '.repeat(width - strWidth);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function truncateMiddle(str, width) {
|
|
277
|
-
const text = String(str || '');
|
|
278
|
-
if (getStringWidth(text) <= width) return text;
|
|
279
|
-
if (width <= 6) return text.slice(0, width);
|
|
280
|
-
const keep = width - 3;
|
|
281
|
-
const left = Math.ceil(keep / 2);
|
|
282
|
-
const right = Math.floor(keep / 2);
|
|
283
|
-
return text.slice(0, left) + '...' + text.slice(-right);
|
|
306
|
+
function detectInputLanguage(text, fallback = currentLang) {
|
|
307
|
+
const value = String(text || '');
|
|
308
|
+
if (/[\u3400-\u9fff]/.test(value)) return 'cn';
|
|
309
|
+
if (/[A-Za-z]{2,}/.test(value)) return 'en';
|
|
310
|
+
return fallback;
|
|
284
311
|
}
|
|
285
312
|
|
|
286
313
|
function maskKey(key) {
|
|
@@ -293,125 +320,299 @@ function clearConsole() {
|
|
|
293
320
|
process.stdout.write('\x1b[2J\x1b[0f');
|
|
294
321
|
}
|
|
295
322
|
|
|
296
|
-
|
|
297
|
-
const
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
323
|
+
function formatPlanPanelItem(plan, width, compact = false) {
|
|
324
|
+
const meta = {
|
|
325
|
+
ready: { icon: '○', color: '\x1b[36m', cn: '待执行', en: 'READY' },
|
|
326
|
+
running: { icon: '▶', color: '\x1b[33m', cn: '执行中', en: 'RUNNING' },
|
|
327
|
+
completed: { icon: '✓', color: '\x1b[32m', cn: '已完成', en: 'DONE' },
|
|
328
|
+
blocked: { icon: '!', color: '\x1b[31m', cn: '受阻', en: 'BLOCKED' }
|
|
329
|
+
}[plan.status] || { icon: '○', color: '\x1b[36m', cn: '待执行', en: 'READY' };
|
|
330
|
+
const name = sanitizeUntrustedText(plan.name);
|
|
331
|
+
if (compact) return `${meta.icon} ${name}`;
|
|
332
|
+
const label = currentLang === 'cn' ? meta.cn : meta.en;
|
|
333
|
+
const available = Math.max(6, width - getStringWidth(meta.icon) - getStringWidth(label) - 3);
|
|
334
|
+
const shownName = truncateMiddle(name, available);
|
|
335
|
+
const padding = ' '.repeat(Math.max(1, width - getStringWidth(meta.icon) - getStringWidth(shownName) - getStringWidth(label) - 2));
|
|
336
|
+
return `${meta.color}${meta.icon}\x1b[0m ${shownName}${padding}\x1b[90m${label}\x1b[0m`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function notebookStatusLabel(status, lang = currentLang) {
|
|
340
|
+
const cn = lang === 'cn';
|
|
341
|
+
const labels = {
|
|
342
|
+
missing: cn ? '未建立' : 'missing', building: cn ? '建库中' : 'building',
|
|
343
|
+
partial: cn ? '待续建' : 'partial', ready: cn ? '就绪' : 'ready',
|
|
344
|
+
drift: cn ? '有漂移' : 'drift', stale: cn ? '待修复' : 'stale', failed: cn ? '失败' : 'failed'
|
|
345
|
+
};
|
|
346
|
+
return labels[status] || status;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function currentNotebookStatus() {
|
|
350
|
+
if (!workspaceReady || workMode !== 'highway') return null;
|
|
351
|
+
return getProjectNotebookStatus(workspaceRoot);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function formatNotebookAge(updatedAt) {
|
|
355
|
+
if (!updatedAt) return currentLang === 'cn' ? '从未' : 'never';
|
|
356
|
+
const seconds = Math.max(0, Math.floor((Date.now() - updatedAt) / 1000));
|
|
357
|
+
if (seconds < 60) return currentLang === 'cn' ? '刚刚' : 'just now';
|
|
358
|
+
const minutes = Math.floor(seconds / 60);
|
|
359
|
+
if (minutes < 60) return currentLang === 'cn' ? `${minutes} 分钟前` : `${minutes}m ago`;
|
|
360
|
+
const hours = Math.floor(minutes / 60);
|
|
361
|
+
if (hours < 48) return currentLang === 'cn' ? `${hours} 小时前` : `${hours}h ago`;
|
|
362
|
+
const days = Math.floor(hours / 24);
|
|
363
|
+
return currentLang === 'cn' ? `${days} 天前` : `${days}d ago`;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function confirmLargeNotebookBuild(info, renderer = null) {
|
|
367
|
+
renderer?.pause();
|
|
368
|
+
try {
|
|
369
|
+
const sizeMb = (info.bytes / 1024 / 1024).toFixed(1);
|
|
370
|
+
const prompt = currentLang === 'cn'
|
|
371
|
+
? `首次项目读取将处理 ${info.files} 个有效文本文件(${sizeMb} MB,约 ${info.estimatedBatches} 个读取批次),并在后台缓存项目认知。是否继续?`
|
|
372
|
+
: `The first project read will process ${info.files} effective text files (${sizeMb} MB, about ${info.estimatedBatches} read batches) and cache project knowledge in the background. Continue?`;
|
|
373
|
+
return await confirmPrompt(`\x1b[33m${prompt}\x1b[0m`);
|
|
374
|
+
} finally { renderer?.resume(); }
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function cloneReadPlan(plan) {
|
|
378
|
+
return JSON.parse(JSON.stringify(plan));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function planTokenTotal(plan) {
|
|
382
|
+
return (plan.files || []).reduce((sum, file) => sum + (Number(file.estimatedTokens) || 0), 0)
|
|
383
|
+
+ (Number(plan.notebook?.notebookBudgetTokens) || 0);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function printReadPlan(plan, scanUsage = {}) {
|
|
387
|
+
const cn = currentLang === 'cn';
|
|
388
|
+
console.log(cn ? '\n\x1b[1;36mHighway 阅读计划\x1b[0m' : '\n\x1b[1;36mHighway read plan\x1b[0m');
|
|
389
|
+
console.log(cn
|
|
390
|
+
? ` Scan:${Number(scanUsage.inputTokens || 0).toLocaleString()} 输入 / ${Number(scanUsage.outputTokens || 0).toLocaleString()} 输出`
|
|
391
|
+
: ` Scan: ${Number(scanUsage.inputTokens || 0).toLocaleString()} in / ${Number(scanUsage.outputTokens || 0).toLocaleString()} out`);
|
|
392
|
+
console.log(cn
|
|
393
|
+
? ` 整轮软预算:${Number(plan.turnBudgetTokens || 0).toLocaleString()} Token · 单次上下文:${Number(plan.contextBudgetTokens || plan.budgetTokens || 0).toLocaleString()}`
|
|
394
|
+
: ` Post-Scan soft budget: ${Number(plan.turnBudgetTokens || 0).toLocaleString()} tokens · per-call context: ${Number(plan.contextBudgetTokens || plan.budgetTokens || 0).toLocaleString()}`);
|
|
395
|
+
console.log(cn
|
|
396
|
+
? ` 笔记:${plan.notebook?.action || 'reuse'} · ${Number(plan.notebook?.notebookBudgetTokens || 0).toLocaleString()} Token`
|
|
397
|
+
: ` Notebook: ${plan.notebook?.action || 'reuse'} · ${Number(plan.notebook?.notebookBudgetTokens || 0).toLocaleString()} tokens`);
|
|
398
|
+
const files = plan.files || [];
|
|
399
|
+
if (!files.length) console.log(cn ? ' 文件:仅按需读取笔记' : ' Files: notebook only unless evidence is needed');
|
|
400
|
+
for (const [index, file] of files.slice(0, 20).entries()) {
|
|
401
|
+
console.log(` ${index + 1}. [${file.strategy}] ${sanitizeUntrustedText(file.path)}${file.required ? ' *' : ''} · ${Number(file.estimatedTokens || 0).toLocaleString()} · ${sanitizeUntrustedText(file.reason || '')}`);
|
|
402
|
+
}
|
|
403
|
+
if (files.length > 20) console.log(cn ? ` …另有 ${files.length - 20} 个文件` : ` ...and ${files.length - 20} more files`);
|
|
404
|
+
console.log(cn
|
|
405
|
+
? ` 计划证据估算:约 ${planTokenTotal(plan).toLocaleString()} Token\n`
|
|
406
|
+
: ` Planned evidence estimate: about ${planTokenTotal(plan).toLocaleString()} tokens\n`);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function readPlanMenuDescription(plan, scanUsage = {}) {
|
|
410
|
+
const lines = [
|
|
411
|
+
currentLang === 'cn'
|
|
412
|
+
? `Scan ${Number(scanUsage.inputTokens || 0).toLocaleString()} 输入 / ${Number(scanUsage.outputTokens || 0).toLocaleString()} 输出`
|
|
413
|
+
: `Scan ${Number(scanUsage.inputTokens || 0).toLocaleString()} in / ${Number(scanUsage.outputTokens || 0).toLocaleString()} out`,
|
|
414
|
+
currentLang === 'cn'
|
|
415
|
+
? `整轮 ${Number(plan.turnBudgetTokens || 0).toLocaleString()} · 单次 ${Number(plan.contextBudgetTokens || plan.budgetTokens || 0).toLocaleString()} · 笔记 ${plan.notebook?.action || 'reuse'} ${Number(plan.notebook?.notebookBudgetTokens || 0).toLocaleString()}`
|
|
416
|
+
: `Turn ${Number(plan.turnBudgetTokens || 0).toLocaleString()} · per-call ${Number(plan.contextBudgetTokens || plan.budgetTokens || 0).toLocaleString()} · notebook ${plan.notebook?.action || 'reuse'} ${Number(plan.notebook?.notebookBudgetTokens || 0).toLocaleString()}`,
|
|
417
|
+
...(plan.files || []).slice(0, 12).map((file, index) =>
|
|
418
|
+
`${index + 1}. [${file.strategy}] ${file.path}${file.required ? ' *' : ''} · ${Number(file.estimatedTokens || 0).toLocaleString()} · ${file.reason || ''}`),
|
|
419
|
+
...((plan.files || []).length > 12
|
|
420
|
+
? [currentLang === 'cn' ? `…另有 ${plan.files.length - 12} 个文件` : `...and ${plan.files.length - 12} more files`]
|
|
421
|
+
: [])
|
|
422
|
+
];
|
|
423
|
+
return lines.join('\n');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function loadProjectInstructions(root) {
|
|
427
|
+
const sections = [];
|
|
428
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md', '.cursorrules']) {
|
|
429
|
+
const target = path.join(root, name);
|
|
430
|
+
try {
|
|
431
|
+
if (!fs.statSync(target).isFile()) continue;
|
|
432
|
+
sections.push(`## ${name}\n${fs.readFileSync(target, 'utf8').slice(0, 8192)}`);
|
|
433
|
+
} catch {
|
|
434
|
+
// Missing or unreadable convention files are ignored.
|
|
308
435
|
}
|
|
309
436
|
}
|
|
310
|
-
return
|
|
437
|
+
return sections.join('\n\n');
|
|
311
438
|
}
|
|
312
439
|
|
|
313
|
-
async function
|
|
314
|
-
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
440
|
+
async function reviewHighwayReadPlan(plan, snapshot, scanUsage, renderer) {
|
|
441
|
+
if (process.env.DAVE_CODE_REVIEW_PLAN !== '1') return { approved: true, plan: cloneReadPlan(plan) };
|
|
442
|
+
const original = cloneReadPlan(plan);
|
|
443
|
+
let current = cloneReadPlan(plan);
|
|
444
|
+
const recommended = Number(original.turnBudgetTokens) || Number(original.budgetTokens) * 3 || 24000;
|
|
445
|
+
const known = new Map((snapshot.files || []).map(file => [file.path.toLowerCase(), file]));
|
|
446
|
+
renderer?.pause();
|
|
447
|
+
try {
|
|
448
|
+
while (true) {
|
|
449
|
+
const selected = await selectMenu(
|
|
450
|
+
currentLang === 'cn' ? '确认 Highway 阅读计划' : 'Confirm Highway read plan',
|
|
451
|
+
readPlanMenuDescription(current, scanUsage),
|
|
452
|
+
[
|
|
453
|
+
{ name: currentLang === 'cn' ? '按当前计划开始' : 'Start with this plan', desc: currentLang === 'cn' ? '进入正式 READ' : 'Enter formal READ' },
|
|
454
|
+
{ name: currentLang === 'cn' ? '经济预算(70%)' : 'Economy budget (70%)', desc: Math.round(recommended * 0.7).toLocaleString() },
|
|
455
|
+
{ name: currentLang === 'cn' ? '推荐预算(100%)' : 'Recommended budget (100%)', desc: recommended.toLocaleString() },
|
|
456
|
+
{ name: currentLang === 'cn' ? '充分预算(150%)' : 'Thorough budget (150%)', desc: Math.round(recommended * 1.5).toLocaleString() },
|
|
457
|
+
{ name: currentLang === 'cn' ? '自定义预算' : 'Custom budget', desc: currentLang === 'cn' ? '输入累计 Token' : 'Enter cumulative tokens' },
|
|
458
|
+
{ name: currentLang === 'cn' ? '修改文件策略' : 'Edit file strategy', desc: 'outline / targeted / full' },
|
|
459
|
+
{ name: currentLang === 'cn' ? '增加文件' : 'Add file', desc: currentLang === 'cn' ? '添加快照中的路径' : 'Add a path from the snapshot' },
|
|
460
|
+
{ name: currentLang === 'cn' ? '排除文件' : 'Exclude file', desc: currentLang === 'cn' ? '从本轮计划移除' : 'Remove from this turn' },
|
|
461
|
+
{ name: currentLang === 'cn' ? '恢复 Scan 推荐' : 'Restore Scan plan', desc: currentLang === 'cn' ? '撤销所有调整' : 'Undo all adjustments' },
|
|
462
|
+
{ name: currentLang === 'cn' ? '取消本轮' : 'Cancel turn', desc: currentLang === 'cn' ? '不进入 READ' : 'Do not enter READ' }
|
|
463
|
+
]
|
|
464
|
+
);
|
|
465
|
+
if (selected === 0) return { approved: true, plan: current };
|
|
466
|
+
if (selected === 1) current.turnBudgetTokens = Math.max(current.contextBudgetTokens || current.budgetTokens, Math.round(recommended * 0.7));
|
|
467
|
+
else if (selected === 2) current.turnBudgetTokens = Math.max(current.contextBudgetTokens || current.budgetTokens, Math.round(recommended));
|
|
468
|
+
else if (selected === 3) current.turnBudgetTokens = Math.max(current.contextBudgetTokens || current.budgetTokens, Math.round(recommended * 1.5));
|
|
469
|
+
else if (selected === 4) {
|
|
470
|
+
const value = Number(await textInputPrompt(currentLang === 'cn' ? '整轮累计预算 Token:' : 'Post-Scan cumulative token budget: '));
|
|
471
|
+
if (Number.isFinite(value) && value >= (current.contextBudgetTokens || current.budgetTokens)) current.turnBudgetTokens = Math.round(value);
|
|
472
|
+
} else if (selected === 5 && current.files.length) {
|
|
473
|
+
const index = await selectMenu(
|
|
474
|
+
currentLang === 'cn' ? '选择文件' : 'Select file',
|
|
475
|
+
currentLang === 'cn' ? '选择后切换读取策略' : 'Choose a new read strategy',
|
|
476
|
+
current.files.map(file => ({ name: file.path, desc: `${file.strategy}${file.required ? ' · required' : ''}` }))
|
|
477
|
+
);
|
|
478
|
+
if (index >= 0) {
|
|
479
|
+
const strategy = await selectMenu('Read strategy', current.files[index].path, [
|
|
480
|
+
{ name: 'outline', desc: currentLang === 'cn' ? '仅结构大纲' : 'Structure only' },
|
|
481
|
+
{ name: 'targeted', desc: currentLang === 'cn' ? '按需局部读取' : 'Targeted ranges' },
|
|
482
|
+
{ name: 'full', desc: currentLang === 'cn' ? '完整读取' : 'Full file' }
|
|
483
|
+
]);
|
|
484
|
+
if (strategy >= 0) current.files[index].strategy = ['outline', 'targeted', 'full'][strategy];
|
|
485
|
+
}
|
|
486
|
+
} else if (selected === 6) {
|
|
487
|
+
const candidate = (await textInputPrompt(currentLang === 'cn' ? '工作区相对路径:' : 'Workspace-relative path: ')).replace(/\\/g, '/');
|
|
488
|
+
const file = known.get(candidate.toLowerCase());
|
|
489
|
+
if (file && !current.files.some(item => item.path.toLowerCase() === candidate.toLowerCase())) {
|
|
490
|
+
current.files.push({
|
|
491
|
+
path: file.path,
|
|
492
|
+
reason: currentLang === 'cn' ? '用户添加' : 'Added by user',
|
|
493
|
+
strategy: 'targeted',
|
|
494
|
+
required: true,
|
|
495
|
+
estimatedTokens: Math.max(100, Math.ceil(Math.min(file.size, 24000) / 4))
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
} else if (selected === 7 && current.files.length) {
|
|
499
|
+
const index = await selectMenu(
|
|
500
|
+
currentLang === 'cn' ? '排除文件' : 'Exclude file',
|
|
501
|
+
currentLang === 'cn' ? '选择要从计划中移除的文件' : 'Choose a file to remove from this plan',
|
|
502
|
+
current.files.map(file => ({ name: file.path, desc: file.required ? 'required' : file.strategy }))
|
|
503
|
+
);
|
|
504
|
+
if (index >= 0) {
|
|
505
|
+
const target = current.files[index];
|
|
506
|
+
const allowed = !target.required || await confirmPrompt(currentLang === 'cn'
|
|
507
|
+
? `这是 Scan 标记的必需文件,仍要排除 ${target.path}?(y/n): `
|
|
508
|
+
: `Scan marked this file required. Exclude ${target.path} anyway? (y/n): `);
|
|
509
|
+
if (allowed) current.files.splice(index, 1);
|
|
510
|
+
}
|
|
511
|
+
} else if (selected === 8) current = cloneReadPlan(original);
|
|
512
|
+
else if (selected === 9 || selected < 0) return { approved: false, plan: current };
|
|
513
|
+
}
|
|
514
|
+
} finally {
|
|
515
|
+
renderer?.resume();
|
|
323
516
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
: `Digesting ${path.basename(filePath)} · ${completedCount}/${totalChunks} chunks`
|
|
342
|
-
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function buildReadBrief(plan, notebook) {
|
|
520
|
+
const brief = {
|
|
521
|
+
budget: {
|
|
522
|
+
perCall: plan.contextBudgetTokens || plan.budgetTokens,
|
|
523
|
+
postScan: plan.turnBudgetTokens
|
|
524
|
+
},
|
|
525
|
+
notebook: {
|
|
526
|
+
action: plan.notebook?.action,
|
|
527
|
+
status: notebook?.status || plan.notebook?.status || 'missing',
|
|
528
|
+
maturity: notebook?.maturity || plan.notebook?.maturity || 'baseline',
|
|
529
|
+
sections: Object.keys(notebook?.sections || {}).slice(0, 12)
|
|
530
|
+
},
|
|
531
|
+
files: (plan.files || []).slice(0, 40).map(file => ({
|
|
532
|
+
path: file.path, strategy: file.strategy, required: file.required, reason: file.reason
|
|
533
|
+
}))
|
|
343
534
|
};
|
|
535
|
+
const text = JSON.stringify(brief);
|
|
536
|
+
if (estimateTokens(text) <= 1200) return text;
|
|
537
|
+
let low = 0;
|
|
538
|
+
let high = text.length;
|
|
539
|
+
while (low < high) {
|
|
540
|
+
const middle = Math.ceil((low + high) / 2);
|
|
541
|
+
if (estimateTokens(text.slice(0, middle)) <= 1200) low = middle;
|
|
542
|
+
else high = middle - 1;
|
|
543
|
+
}
|
|
544
|
+
return `${text.slice(0, low)}\n[Read brief capped at 1200 estimated tokens]`;
|
|
545
|
+
}
|
|
344
546
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
emitProgress();
|
|
369
|
-
return { idx: task.idx, start, end, summary: `[Error reading this segment: ${err.message}]` };
|
|
547
|
+
async function resolveTurnBudgetPressure(plan, usedTokens, renderer) {
|
|
548
|
+
renderer?.pause();
|
|
549
|
+
try {
|
|
550
|
+
const choice = await selectMenu(
|
|
551
|
+
currentLang === 'cn' ? 'Highway Token 预算接近上限' : 'Highway token budget is nearly exhausted',
|
|
552
|
+
currentLang === 'cn'
|
|
553
|
+
? `已使用 ${usedTokens.toLocaleString()} / ${plan.turnBudgetTokens.toLocaleString()} Token`
|
|
554
|
+
: `Used ${usedTokens.toLocaleString()} / ${plan.turnBudgetTokens.toLocaleString()} tokens`,
|
|
555
|
+
[
|
|
556
|
+
{ name: currentLang === 'cn' ? '增加 25%' : 'Add 25%', desc: currentLang === 'cn' ? '继续当前工作流' : 'Continue the workflow' },
|
|
557
|
+
{ name: currentLang === 'cn' ? '增加 50%' : 'Add 50%', desc: currentLang === 'cn' ? '为复杂任务增加余量' : 'Add room for complex work' },
|
|
558
|
+
{ name: currentLang === 'cn' ? '自定义追加' : 'Custom extension', desc: currentLang === 'cn' ? '输入新增 Token' : 'Enter additional tokens' },
|
|
559
|
+
{ name: currentLang === 'cn' ? '基于现有证据收尾' : 'Finish with current evidence', desc: currentLang === 'cn' ? '停止调用工具并简洁回答' : 'Stop tools and answer concisely' },
|
|
560
|
+
{ name: currentLang === 'cn' ? '取消本轮' : 'Cancel turn', desc: currentLang === 'cn' ? '保留已经落盘的修改' : 'Keep any already-applied changes' }
|
|
561
|
+
]
|
|
562
|
+
);
|
|
563
|
+
if (choice === 0) return { action: 'extend', tokens: Math.max(1000, Math.round(plan.turnBudgetTokens * 0.25)) };
|
|
564
|
+
if (choice === 1) return { action: 'extend', tokens: Math.max(1000, Math.round(plan.turnBudgetTokens * 0.5)) };
|
|
565
|
+
if (choice === 2) {
|
|
566
|
+
const tokens = Number(await textInputPrompt(currentLang === 'cn' ? '追加 Token:' : 'Additional tokens: '));
|
|
567
|
+
return Number.isFinite(tokens) && tokens > 0
|
|
568
|
+
? { action: 'extend', tokens: Math.round(tokens) }
|
|
569
|
+
: { action: 'finish' };
|
|
370
570
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
report += `Total Lines: ${totalLines}\n`;
|
|
377
|
-
report += `Resolved Path: ${resolvedPath}\n\n`;
|
|
378
|
-
|
|
379
|
-
results.forEach(res => {
|
|
380
|
-
report += `--- Segment ${res.idx + 1} (Lines ${res.start}-${res.end}) ---\n`;
|
|
381
|
-
report += `${res.summary}\n\n`;
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
report += `=========================================================\n`;
|
|
385
|
-
return report;
|
|
571
|
+
if (choice === 3) return { action: 'finish' };
|
|
572
|
+
return { action: 'cancel' };
|
|
573
|
+
} finally {
|
|
574
|
+
renderer?.resume();
|
|
575
|
+
}
|
|
386
576
|
}
|
|
387
577
|
|
|
388
578
|
function drawHeader() {
|
|
389
579
|
const logoLines = getLogoLines();
|
|
390
580
|
const t = locales[currentLang];
|
|
391
581
|
const active = getActiveProfile();
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const
|
|
582
|
+
const contextStats = getContextStats(messages, currentContextPlan);
|
|
583
|
+
const memory = workspaceReady ? memorySummary(workspaceRoot) : { enabled: true, total: 0 };
|
|
584
|
+
const note = currentNotebookStatus();
|
|
585
|
+
const planLimit = process.stdout.columns && process.stdout.columns < 100 ? 2 : 4;
|
|
586
|
+
const planPanel = workspaceReady
|
|
587
|
+
? planStatusPanel(workspaceRoot, planLimit)
|
|
588
|
+
: { items: [], total: 0, hidden: 0, counts: { active: 0, completed: 0 } };
|
|
589
|
+
const terminalWidth = Math.max(36, process.stdout.columns || 100);
|
|
590
|
+
// The two-column header scales with the terminal instead of assuming 95
|
|
591
|
+
// columns, so 101-119 column windows no longer leave a ragged right edge.
|
|
592
|
+
const boxWidth = Math.min(terminalWidth - 2, 118);
|
|
395
593
|
const leftColWidth = 45;
|
|
396
|
-
const rightColWidth =
|
|
397
|
-
const boxWidth = 95;
|
|
594
|
+
const rightColWidth = Math.max(38, boxWidth - leftColWidth - 3);
|
|
398
595
|
const activeFileLabel = workspaceReady && activeOpenFile ? truncateMiddle(sanitizeUntrustedText(path.relative(workspaceRoot, activeOpenFile)), rightColWidth - 13) : (currentLang === 'cn' ? '未选择' : 'None');
|
|
399
596
|
const workspaceLabel = workspaceReady
|
|
400
597
|
? truncateMiddle(sanitizeUntrustedText(workspaceRoot), rightColWidth - 11)
|
|
401
598
|
: (currentLang === 'cn' ? '未关联,请使用 /open' : 'Not linked; use /open');
|
|
402
599
|
const contextLabel = `${contextStats.modelMessages}/${contextStats.savedMessages} msgs · ~${contextStats.estimatedTokens}/${contextStats.budgetTokens} tokens${contextStats.compacted ? ' · compacted' : ''}`;
|
|
403
|
-
const terminalWidth = Math.max(36, process.stdout.columns || 100);
|
|
404
600
|
|
|
405
601
|
if (terminalWidth < 100) {
|
|
406
602
|
const contentWidth = Math.max(32, terminalWidth - 2);
|
|
407
603
|
const rule = '─'.repeat(contentWidth);
|
|
408
604
|
const compactStatus = [
|
|
605
|
+
`${currentLang === 'cn' ? '模式' : 'Mode'}: ${workMode === 'thunder' ? '⚡ Thunder' : '➜ Highway'}`,
|
|
409
606
|
`${currentLang === 'cn' ? '模型' : 'Model'}: ${active ? active.model : '(not configured)'}`,
|
|
410
|
-
|
|
607
|
+
`${currentLang === 'cn' ? '上下文窗口' : 'Context window'}: ${inferContextWindowTokens(active).toLocaleString()} tokens`,
|
|
411
608
|
`${currentLang === 'cn' ? '目录' : 'Workspace'}: ${workspaceReady ? truncateMiddle(sanitizeUntrustedText(workspaceRoot), contentWidth - 5) : (currentLang === 'cn' ? '未关联,请使用 /open' : 'Not linked; use /open')}`,
|
|
412
609
|
`${currentLang === 'cn' ? '文件' : 'File'}: ${workspaceReady && activeOpenFile ? truncateMiddle(sanitizeUntrustedText(path.relative(workspaceRoot, activeOpenFile)), contentWidth - 5) : (currentLang === 'cn' ? '未选择' : 'None')}`,
|
|
413
610
|
`${currentLang === 'cn' ? '上下文' : 'Context'}: ${contextStats.modelMessages}/${contextStats.savedMessages} · ~${contextStats.estimatedTokens}/${contextStats.budgetTokens}${contextStats.compacted ? (currentLang === 'cn' ? ' · 已压缩' : ' · compacted') : ''}`,
|
|
414
|
-
`${currentLang === 'cn' ? '
|
|
611
|
+
`${currentLang === 'cn' ? '记忆' : 'Memory'}: ${memory.total} ${currentLang === 'cn' ? '条' : 'saved'} · ${memory.enabled ? (currentLang === 'cn' ? '已开启' : 'on') : (currentLang === 'cn' ? '已关闭' : 'off')}`,
|
|
612
|
+
...(note ? [`${currentLang === 'cn' ? '项目笔记' : 'Note'}: ${notebookStatusLabel(note.status)} · ${note.completed}/${note.files}${note.drift ? ` · ${note.drift} ${currentLang === 'cn' ? '处漂移' : 'drift'}` : ''}`] : []),
|
|
613
|
+
`${currentLang === 'cn' ? '计划' : 'Plans'}: ${planPanel.items.length
|
|
614
|
+
? `${planPanel.items.map(plan => formatPlanPanelItem(plan, contentWidth, true)).join(' | ')}${planPanel.hidden ? ` | +${planPanel.hidden}` : ''}`
|
|
615
|
+
: (currentLang === 'cn' ? '无' : 'None')}`
|
|
415
616
|
];
|
|
416
617
|
|
|
417
618
|
console.log(`\x1b[38;2;250;100;30m${rule}\x1b[0m`);
|
|
@@ -425,17 +626,29 @@ function drawHeader() {
|
|
|
425
626
|
|
|
426
627
|
const rightLines = [
|
|
427
628
|
currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m状态\x1b[0m' : '\x1b[1;38;2;250;100;30mStatus\x1b[0m',
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
629
|
+
`${currentLang === 'cn' ? '模式' : 'Mode'}: ${workMode === 'thunder' ? '\x1b[1;33m⚡ Thunder\x1b[0m' : '\x1b[36m➜ Highway\x1b[0m'}`,
|
|
630
|
+
`${currentLang === 'cn' ? '模型' : 'Model'}: ${active ? active.model : (currentLang === 'cn' ? '未配置' : 'not configured')}`,
|
|
631
|
+
`${currentLang === 'cn' ? '阶段' : 'Stage'}: ${currentContextPlan ? 'SCAN ✓ · READ' : (currentLang === 'cn' ? '等待任务' : 'Idle')}`,
|
|
632
|
+
`${currentLang === 'cn' ? '工作区' : 'Workspace'}: ${workspaceLabel}`,
|
|
633
|
+
`${currentLang === 'cn' ? '当前文件' : 'Active file'}: ${activeFileLabel}`,
|
|
634
|
+
`${currentLang === 'cn' ? '上下文' : 'Context'}: ${contextLabel}`,
|
|
635
|
+
currentLang === 'cn'
|
|
636
|
+
? `记忆: ${memory.total} 条 · ${memory.enabled ? '已开启' : '已关闭'} · /memory`
|
|
637
|
+
: `Memory: ${memory.total} saved · ${memory.enabled ? 'on' : 'off'} · /memory`,
|
|
638
|
+
...(note ? [currentLang === 'cn'
|
|
639
|
+
? `笔记: ${notebookStatusLabel(note.status)} · ${note.completed}/${note.files} 文件 · /note`
|
|
640
|
+
: `Note: ${notebookStatusLabel(note.status)} · ${note.completed}/${note.files} files · /note`] : []),
|
|
433
641
|
'',
|
|
434
|
-
currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m计划\x1b[0m' : '\x1b[1;38;2;250;100;30mPlans\x1b[0m'
|
|
435
|
-
|
|
642
|
+
`${currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m计划\x1b[0m' : '\x1b[1;38;2;250;100;30mPlans\x1b[0m'} ` +
|
|
643
|
+
`\x1b[90m${planPanel.counts.active} ${currentLang === 'cn' ? '未完成' : 'active'} · ${planPanel.counts.completed} ${currentLang === 'cn' ? '已完成' : 'done'}\x1b[0m`,
|
|
644
|
+
...(planPanel.items.length
|
|
645
|
+
? planPanel.items.map(plan => formatPlanPanelItem(plan, rightColWidth))
|
|
646
|
+
: [currentLang === 'cn' ? '\x1b[90m暂无计划 · 使用 /plan 创建\x1b[0m' : '\x1b[90mNo plans · create one with /plan\x1b[0m']),
|
|
647
|
+
...(planPanel.hidden > 0 ? [`\x1b[90m↳ +${planPanel.hidden} more · /plan\x1b[0m`] : []),
|
|
436
648
|
'',
|
|
437
649
|
currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m常用命令\x1b[0m' : '\x1b[1;38;2;250;100;30mCommands\x1b[0m',
|
|
438
|
-
'/plan /code
|
|
650
|
+
'/mode /plan /code',
|
|
651
|
+
'/agents /open',
|
|
439
652
|
'/help /history /model',
|
|
440
653
|
currentLang === 'cn' ? 'Ctrl+C 或 /exit 退出' : 'Ctrl+C or /exit to quit',
|
|
441
654
|
'',
|
|
@@ -444,9 +657,14 @@ function drawHeader() {
|
|
|
444
657
|
|
|
445
658
|
const boxColor = '\x1b[38;2;250;100;30m';
|
|
446
659
|
const resetColor = '\x1b[0m';
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
660
|
+
|
|
661
|
+
// Every row is "│ " + left + " │ " + right + " │", so the border must match
|
|
662
|
+
// that exact inner width or the box corners drift as the terminal resizes.
|
|
663
|
+
const innerWidth = leftColWidth + rightColWidth + 3;
|
|
664
|
+
const titleText = `Dave Code v${packageVersion}`;
|
|
665
|
+
const topFill = Math.max(1, innerWidth - displayWidth(titleText) - 4);
|
|
666
|
+
const topBorder = `${boxColor}┌── ${resetColor}\x1b[1m${titleText}\x1b[0m${boxColor} ${'─'.repeat(topFill)}┐${resetColor}`;
|
|
667
|
+
const bottomBorder = `${boxColor}└${'─'.repeat(innerWidth + 2)}┘${resetColor}`;
|
|
450
668
|
|
|
451
669
|
console.log(topBorder);
|
|
452
670
|
|
|
@@ -467,18 +685,16 @@ function drawHeader() {
|
|
|
467
685
|
leftContent = centerLine(`\x1b[90m(No Models / 未配置)\x1b[0m`, leftColWidth);
|
|
468
686
|
}
|
|
469
687
|
} else if (i === 19) {
|
|
470
|
-
|
|
471
|
-
const truncatedCwd = cwd.length > leftColWidth - 2 ? '...' + cwd.slice(-(leftColWidth - 5)) : cwd;
|
|
472
|
-
leftContent = centerLine(`\x1b[90m${truncatedCwd}\x1b[0m`, leftColWidth);
|
|
688
|
+
leftContent = centerLine(`\x1b[90m${truncateMiddle(process.cwd(), leftColWidth - 2)}\x1b[0m`, leftColWidth);
|
|
473
689
|
}
|
|
474
690
|
|
|
475
691
|
const rightContent = padLine(truncateMiddle(rightLines[i] || '', rightColWidth), rightColWidth);
|
|
476
692
|
|
|
477
693
|
console.log(
|
|
478
|
-
boxColor + '│ ' + resetColor +
|
|
479
|
-
leftContent +
|
|
480
|
-
boxColor + ' │ ' + resetColor +
|
|
481
|
-
rightContent +
|
|
694
|
+
boxColor + '│ ' + resetColor +
|
|
695
|
+
leftContent +
|
|
696
|
+
boxColor + ' │ ' + resetColor +
|
|
697
|
+
rightContent +
|
|
482
698
|
boxColor + ' │' + resetColor
|
|
483
699
|
);
|
|
484
700
|
}
|
|
@@ -495,12 +711,18 @@ async function promptUser() {
|
|
|
495
711
|
promptLoopRunning = true;
|
|
496
712
|
|
|
497
713
|
while (true) {
|
|
498
|
-
const
|
|
499
|
-
const
|
|
714
|
+
const note = currentNotebookStatus();
|
|
715
|
+
const noteText = note
|
|
716
|
+
? ` · ${currentLang === 'cn' ? '笔记' : 'Note'} ${notebookStatusLabel(note.status)}`
|
|
717
|
+
: '';
|
|
718
|
+
const statusText = currentContextPlan
|
|
719
|
+
? `${workMode === 'thunder' ? '⚡ Thunder' : '➜ Highway'} · ${currentLang === 'cn' ? '上下文' : 'Context'} ~${getContextStats(messages, currentContextPlan).estimatedTokens}/${currentContextPlan.budgetTokens}`
|
|
720
|
+
+ noteText
|
|
721
|
+
: `${workMode === 'thunder' ? '⚡ Thunder' : '➜ Highway'} · ${currentLang === 'cn' ? '扫描就绪' : 'Scan ready'}${noteText}`;
|
|
500
722
|
const promptWidth = Math.max(32, process.stdout.columns || 80);
|
|
501
|
-
const padding = ' '.repeat(Math.max(1, promptWidth - getStringWidth(
|
|
502
|
-
const colorCode =
|
|
503
|
-
console.log(padding + colorCode +
|
|
723
|
+
const padding = ' '.repeat(Math.max(1, promptWidth - getStringWidth(statusText) - 1));
|
|
724
|
+
const colorCode = '\x1b[90m';
|
|
725
|
+
console.log(padding + colorCode + statusText + '\x1b[0m');
|
|
504
726
|
const activeFileBase = activeOpenFile ? path.basename(activeOpenFile) : '';
|
|
505
727
|
const placeholder = activeOpenFile
|
|
506
728
|
? (currentLang === 'cn' ? `正在针对 [${activeFileBase}] 工作...` : `Working on [${activeFileBase}]...`)
|
|
@@ -547,7 +769,9 @@ function describeToolRequest(toolName, toolArg, lang) {
|
|
|
547
769
|
const target = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
548
770
|
const labels = {
|
|
549
771
|
LIST_DIR: cn ? `准备查看 ${target || '.'}` : `Preparing to list ${target || '.'}`,
|
|
772
|
+
INSPECT_FILE: cn ? `准备分析 ${target} 的结构` : `Preparing to inspect ${target}`,
|
|
550
773
|
READ_FILE: cn ? `准备读取 ${target}` : `Preparing to read ${target}`,
|
|
774
|
+
READ_NOTEBOOK: cn ? '准备按需读取项目笔记' : 'Preparing to read the project notebook',
|
|
551
775
|
EDIT_FILE: cn ? `准备局部修改 ${target}` : `Preparing to edit ${target}`,
|
|
552
776
|
WRITE_FILE: cn ? `准备修改 ${target}` : `Preparing to change ${target}`,
|
|
553
777
|
SEARCH_GREP: cn ? `准备搜索 “${target}”` : `Preparing to search "${target}"`,
|
|
@@ -570,6 +794,59 @@ function launchEditor(filePath) {
|
|
|
570
794
|
child.unref();
|
|
571
795
|
}
|
|
572
796
|
|
|
797
|
+
async function handleMemoryMenu() {
|
|
798
|
+
while (true) {
|
|
799
|
+
const summary = memorySummary(workspaceRoot);
|
|
800
|
+
const options = [
|
|
801
|
+
{
|
|
802
|
+
name: currentLang === 'cn'
|
|
803
|
+
? `自动记忆:${summary.enabled ? '开启' : '关闭'}`
|
|
804
|
+
: `Auto memory: ${summary.enabled ? 'on' : 'off'}`,
|
|
805
|
+
desc: currentLang === 'cn' ? '切换跨聊天记忆提取' : 'Toggle cross-chat memory extraction'
|
|
806
|
+
},
|
|
807
|
+
...summary.memories.map(memory => ({
|
|
808
|
+
name: `[${memory.category}] ${memory.text}`,
|
|
809
|
+
desc: new Date(memory.updatedAt).toLocaleString(currentLang === 'cn' ? 'zh-CN' : 'en-US')
|
|
810
|
+
})),
|
|
811
|
+
...(summary.memories.length ? [{
|
|
812
|
+
name: currentLang === 'cn' ? '清空全部记忆' : 'Clear all memories',
|
|
813
|
+
desc: currentLang === 'cn' ? '永久删除当前工作区的自动记忆' : 'Permanently delete workspace memories'
|
|
814
|
+
}] : [])
|
|
815
|
+
];
|
|
816
|
+
const selected = await selectMenu(
|
|
817
|
+
currentLang === 'cn' ? '工作区记忆' : 'Workspace Memory',
|
|
818
|
+
currentLang === 'cn'
|
|
819
|
+
? `${summary.total} 条记忆 · 仅保存可跨聊天复用的偏好、约定和项目事实`
|
|
820
|
+
: `${summary.total} memories · durable preferences, conventions, and project facts only`,
|
|
821
|
+
options,
|
|
822
|
+
0
|
|
823
|
+
);
|
|
824
|
+
if (selected === -1) return;
|
|
825
|
+
if (selected === 0) {
|
|
826
|
+
setMemoryEnabled(workspaceRoot, !summary.enabled);
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
const memoryIndex = selected - 1;
|
|
830
|
+
if (memoryIndex < summary.memories.length) {
|
|
831
|
+
const memory = summary.memories[memoryIndex];
|
|
832
|
+
const action = await selectMenu(
|
|
833
|
+
currentLang === 'cn' ? '记忆详情' : 'Memory detail',
|
|
834
|
+
memory.text,
|
|
835
|
+
currentLang === 'cn'
|
|
836
|
+
? [{ name: '删除', desc: '永久删除这条记忆' }, { name: '返回', desc: '保留记忆' }]
|
|
837
|
+
: [{ name: 'Delete', desc: 'Permanently delete this memory' }, { name: 'Back', desc: 'Keep this memory' }],
|
|
838
|
+
1
|
|
839
|
+
);
|
|
840
|
+
if (action === 0) deleteMemory(workspaceRoot, memory.id);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const confirmed = await confirmPrompt(currentLang === 'cn'
|
|
844
|
+
? '确认清空当前工作区的全部记忆?(y/n): '
|
|
845
|
+
: 'Clear every memory for this workspace? (y/n): ');
|
|
846
|
+
if (confirmed) clearMemories(workspaceRoot);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
573
850
|
async function handlePlanMenu() {
|
|
574
851
|
while (true) {
|
|
575
852
|
const plans = listPlans(workspaceRoot);
|
|
@@ -657,8 +934,219 @@ function sessionMatchesWorkspace(session, candidateRoot) {
|
|
|
657
934
|
return false;
|
|
658
935
|
}
|
|
659
936
|
|
|
937
|
+
function recentConversationForScan(history, maxMessages = 8) {
|
|
938
|
+
return history
|
|
939
|
+
.filter(message => ['user', 'assistant'].includes(message.role) && !message.toolCall)
|
|
940
|
+
.slice(-maxMessages)
|
|
941
|
+
.map(message => `[${message.role}] ${sanitizeUntrustedText(stripReasoningBlocks(message.displayContent || message.content || '')).slice(0, 1200)}`)
|
|
942
|
+
.join('\n');
|
|
943
|
+
}
|
|
944
|
+
|
|
660
945
|
// ── Main REPL Input Handler ──
|
|
661
946
|
|
|
947
|
+
async function chooseWorkMode() {
|
|
948
|
+
return selectWorkModeMenu(workMode, currentLang);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async function showThunderTeams() {
|
|
952
|
+
if (!workspaceReady) {
|
|
953
|
+
console.log(currentLang === 'cn' ? '\n请先使用 /open 关联工作区。\n' : '\nUse /open to attach a workspace first.\n');
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
const summary = thunderSummary(workspaceRoot);
|
|
957
|
+
if (!summary.latest) {
|
|
958
|
+
console.log(currentLang === 'cn' ? '\n暂无活跃 Thunder 团队。\n' : '\nNo active Thunder team.\n');
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
const team = summary.latest;
|
|
962
|
+
console.log(`\n\x1b[1;33m⚡ Thunder · ${sanitizeUntrustedText(team.planName)}\x1b[0m`);
|
|
963
|
+
console.log(currentLang === 'cn'
|
|
964
|
+
? `阶段 ${team.phase} · ${team.members.length} 人 · ${team.resourceProposal.tier} · 并发 ${team.resourceProposal.concurrency}`
|
|
965
|
+
: `Phase ${team.phase} · ${team.members.length} members · ${team.resourceProposal.tier} · concurrency ${team.resourceProposal.concurrency}`);
|
|
966
|
+
for (const member of team.members) {
|
|
967
|
+
const used = (member.contextUsage?.usedTokens || 0).toLocaleString();
|
|
968
|
+
const budget = (member.contextUsage?.budgetTokens || currentContextPlan?.budgetTokens || 0).toLocaleString();
|
|
969
|
+
console.log(` ${member.status === 'done' ? '✓' : member.status === 'blocked' ? '!' : '○'} ${member.name} · ${member.phase || 'reading'} · ${used}/${budget} · ${sanitizeUntrustedText(member.latestReport || member.currentTask || '')}`);
|
|
970
|
+
}
|
|
971
|
+
console.log('');
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async function runThunderPlanningFlow({ request, requestedName = null, replace = false }) {
|
|
975
|
+
const planName = requestedName || suggestThunderPlanName(request, currentLang);
|
|
976
|
+
const finalName = !requestedName && findPlan(workspaceRoot, planName) ? `${planName} ${new Date().toLocaleTimeString(currentLang === 'cn' ? 'zh-CN' : 'en-US', { hour: '2-digit', minute: '2-digit' })}` : planName;
|
|
977
|
+
const recalled = retrieveMemories(workspaceRoot, request, 8);
|
|
978
|
+
const memoryText = formatMemoriesForPrompt(recalled);
|
|
979
|
+
const runtime = createRuntimeEvents();
|
|
980
|
+
let renderer = createTerminalRenderer({ stdout: process.stdout, lang: currentLang });
|
|
981
|
+
const unsubscribe = runtime.subscribe(event => renderer?.handle(event));
|
|
982
|
+
const abortController = new AbortController();
|
|
983
|
+
const onInterrupt = () => abortController.abort();
|
|
984
|
+
process.once('SIGINT', onInterrupt);
|
|
985
|
+
messages.push({ role: 'user', content: request, displayContent: request });
|
|
986
|
+
saveCurrentSession();
|
|
987
|
+
try {
|
|
988
|
+
runtime.emit('turn.started', { label: currentLang === 'cn' ? '准备 Thunder 工作流' : 'Preparing Thunder workflow' });
|
|
989
|
+
runtime.emit('phase.changed', { phase: 'scan', label: currentLang === 'cn' ? 'SCAN · 扫描项目' : 'SCAN · scanning project' });
|
|
990
|
+
const scan = await runAdaptiveScan({
|
|
991
|
+
workspaceRoot, request, memories: memoryText, conversationContext: recentConversationForScan(messages),
|
|
992
|
+
profile: getActiveProfile(), emit: runtime.emit, signal: abortController.signal, lang: currentLang
|
|
993
|
+
});
|
|
994
|
+
currentContextPlan = scan.contextPlan;
|
|
995
|
+
currentScanSummary = scan.summary;
|
|
996
|
+
saveCurrentSession();
|
|
997
|
+
renderer.dispose();
|
|
998
|
+
renderer = null;
|
|
999
|
+
const result = await runThunderPlanning({
|
|
1000
|
+
workspaceRoot, request, planName: finalName, lang: currentLang, memories: memoryText,
|
|
1001
|
+
contextPlan: scan.contextPlan, scanSummary: scan.summary, scanSnapshot: scan.snapshot,
|
|
1002
|
+
emit: runtime.emit,
|
|
1003
|
+
requestPermission: async request => {
|
|
1004
|
+
renderer?.pause();
|
|
1005
|
+
try {
|
|
1006
|
+
if (request.preview) {
|
|
1007
|
+
const lines = sanitizeUntrustedText(request.preview).split('\n');
|
|
1008
|
+
console.log(lines.slice(0, 30).join('\n'));
|
|
1009
|
+
if (lines.length > 30) console.log(currentLang === 'cn' ? `… 已折叠 ${lines.length - 30} 行预览` : `… ${lines.length - 30} preview lines collapsed`);
|
|
1010
|
+
}
|
|
1011
|
+
return await (request.allowSession ? permissionPrompt : confirmPrompt)(`\x1b[33m${sanitizeUntrustedText(request.prompt)}\x1b[0m`);
|
|
1012
|
+
}
|
|
1013
|
+
finally { renderer?.resume(); }
|
|
1014
|
+
},
|
|
1015
|
+
approveResources: async (proposal, team) => {
|
|
1016
|
+
renderer?.dispose();
|
|
1017
|
+
renderer = createThunderRenderer({
|
|
1018
|
+
stdout: process.stdout, lang: currentLang, team,
|
|
1019
|
+
onMessageRequested: async member => {
|
|
1020
|
+
renderer?.pause();
|
|
1021
|
+
try {
|
|
1022
|
+
const message = await textInputPrompt(currentLang === 'cn'
|
|
1023
|
+
? `给 ${member.name} 的消息(由 PM 记录和路由): `
|
|
1024
|
+
: `Message for ${member.name} (recorded and routed by PM): `);
|
|
1025
|
+
if (message?.trim()) {
|
|
1026
|
+
addThunderMessage(workspaceRoot, team.id, {
|
|
1027
|
+
from: 'user', to: member.id, type: 'question', summary: message.trim(), refs: [], requiresResponse: true
|
|
1028
|
+
});
|
|
1029
|
+
runtime.emit('message.sent', { teamId: team.id, message: { from: 'user', to: member.id, type: 'question', summary: message.trim() } });
|
|
1030
|
+
}
|
|
1031
|
+
} finally {
|
|
1032
|
+
renderer?.resume();
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
});
|
|
1036
|
+
renderer.pause();
|
|
1037
|
+
console.log(`\n\x1b[1;33m${sanitizeUntrustedText(formatThunderResourceProposal(proposal, currentLang))}\x1b[0m\n`);
|
|
1038
|
+
const approved = await confirmPrompt(currentLang === 'cn'
|
|
1039
|
+
? '批准此资源方案并开始只读团队规划?(y/n): '
|
|
1040
|
+
: 'Approve this resource proposal and start read-only team planning? (y/n): ');
|
|
1041
|
+
if (approved) renderer.resume();
|
|
1042
|
+
return approved;
|
|
1043
|
+
},
|
|
1044
|
+
approvePerformance: async () => confirmPrompt(currentLang === 'cn'
|
|
1045
|
+
? 'PM 判断这是高难度任务。是否批准高性能档(最多 9 个 Agent、6 个并发请求)?(y/n): '
|
|
1046
|
+
: 'The PM classified this as high difficulty. Approve performance tier (up to 9 agents and 6 concurrent requests)? (y/n): '),
|
|
1047
|
+
signal: abortController.signal
|
|
1048
|
+
});
|
|
1049
|
+
if (result.cancelled) {
|
|
1050
|
+
messages.push({ role: 'assistant', content: currentLang === 'cn' ? 'Thunder 资源方案未获批准,未启动规划。' : 'The Thunder resource proposal was not approved; planning did not start.' });
|
|
1051
|
+
saveCurrentSession();
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
const plan = upsertPlan(workspaceRoot, {
|
|
1055
|
+
name: finalName, request, content: result.planContent, workflowMode: 'thunder', teamId: result.team.id,
|
|
1056
|
+
resourceProposal: result.team.resourceProposal, teamSnapshot: result.team,
|
|
1057
|
+
taskGraph: result.team.tasks, decisions: result.team.decisions
|
|
1058
|
+
}, { replace });
|
|
1059
|
+
updateThunderTeam(workspaceRoot, result.team.id, { planId: plan.id, planName: plan.name, phase: 'awaiting_code' });
|
|
1060
|
+
messages.push({ role: 'assistant', content: result.planContent });
|
|
1061
|
+
saveCurrentSession();
|
|
1062
|
+
renderer?.dispose();
|
|
1063
|
+
renderer = null;
|
|
1064
|
+
redrawScreen(currentLang === 'cn'
|
|
1065
|
+
? `\n\x1b[32mThunder 团队计划“${sanitizeUntrustedText(plan.name)}”已保存。输入 /code ${sanitizeUntrustedText(plan.name)} 开始执行。\x1b[0m\n`
|
|
1066
|
+
: `\n\x1b[32mThunder team plan “${sanitizeUntrustedText(plan.name)}” was saved. Run /code ${sanitizeUntrustedText(plan.name)} to execute.\x1b[0m\n`);
|
|
1067
|
+
return plan;
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
renderer?.dispose();
|
|
1070
|
+
renderer = null;
|
|
1071
|
+
if (abortController.signal.aborted) console.log(currentLang === 'cn' ? '\nThunder 规划已取消。\n' : '\nThunder planning cancelled.\n');
|
|
1072
|
+
else console.log(currentLang === 'cn' ? `\nThunder 规划失败:${sanitizeUntrustedText(error.message)}\n` : `\nThunder planning failed: ${sanitizeUntrustedText(error.message)}\n`);
|
|
1073
|
+
return null;
|
|
1074
|
+
} finally {
|
|
1075
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
1076
|
+
renderer?.dispose();
|
|
1077
|
+
unsubscribe();
|
|
1078
|
+
runtime.close();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
async function handleNoteCommand(action) {
|
|
1083
|
+
if (workMode !== 'highway') {
|
|
1084
|
+
console.log(currentLang === 'cn'
|
|
1085
|
+
? '\n\x1b[33m项目笔记暂仅支持 Highway;Thunder 工作流保持不变。\x1b[0m\n'
|
|
1086
|
+
: '\n\x1b[33mProject notebooks currently support Highway only; Thunder is unchanged.\x1b[0m\n');
|
|
1087
|
+
return;
|
|
1088
|
+
}
|
|
1089
|
+
if (!workspaceReady) {
|
|
1090
|
+
console.log(currentLang === 'cn' ? '\n请先使用 /open 关联工作区。\n' : '\nUse /open to attach a workspace first.\n');
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
if (action === 'status') {
|
|
1094
|
+
const note = getProjectNotebookStatus(workspaceRoot);
|
|
1095
|
+
if (!note.exists) {
|
|
1096
|
+
console.log(currentLang === 'cn'
|
|
1097
|
+
? '\n\x1b[90m项目笔记尚未建立;下一次 Highway 项目任务会自动建库。\x1b[0m\n'
|
|
1098
|
+
: '\n\x1b[90mNo project notebook exists; the next Highway project task will build it.\x1b[0m\n');
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const coverage = note.files ? Math.round(note.completed / note.files * 100) : 0;
|
|
1102
|
+
console.log(currentLang === 'cn'
|
|
1103
|
+
? `\n\x1b[1;36m项目笔记\x1b[0m\n 状态:${notebookStatusLabel(note.status)}\n 覆盖:${note.completed}/${note.files} 文件(${coverage}%)\n 漂移:${note.drift} 个路径\n 更新:${formatNotebookAge(note.updatedAt)}\n`
|
|
1104
|
+
: `\n\x1b[1;36mProject notebook\x1b[0m\n Status: ${notebookStatusLabel(note.status)}\n Coverage: ${note.completed}/${note.files} files (${coverage}%)\n Drift: ${note.drift} paths\n Updated: ${formatNotebookAge(note.updatedAt)}\n`);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (action === 'clear') {
|
|
1108
|
+
const confirmed = await confirmPrompt(currentLang === 'cn'
|
|
1109
|
+
? '\x1b[31m确认删除此工作区的后台项目笔记?下一次 Highway Scan 将重新建库。\x1b[0m'
|
|
1110
|
+
: '\x1b[31mDelete this workspace notebook? The next Highway Scan will rebuild it.\x1b[0m');
|
|
1111
|
+
if (confirmed) clearProjectNotebook(workspaceRoot);
|
|
1112
|
+
console.log(confirmed
|
|
1113
|
+
? (currentLang === 'cn' ? '\n\x1b[32m项目笔记已删除。\x1b[0m\n' : '\n\x1b[32mProject notebook deleted.\x1b[0m\n')
|
|
1114
|
+
: (currentLang === 'cn' ? '\n已取消。\n' : '\nCancelled.\n'));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (!hasApiKey()) {
|
|
1118
|
+
console.log(`\n${locales[currentLang].noKeyError}\n`);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
const runtime = createRuntimeEvents();
|
|
1122
|
+
const renderer = createTerminalRenderer({ stdout: process.stdout, lang: currentLang });
|
|
1123
|
+
const unsubscribe = runtime.subscribe(event => renderer.handle(event));
|
|
1124
|
+
const abortController = new AbortController();
|
|
1125
|
+
const onInterrupt = () => abortController.abort();
|
|
1126
|
+
process.once('SIGINT', onInterrupt);
|
|
1127
|
+
try {
|
|
1128
|
+
runtime.emit('turn.started', { label: currentLang === 'cn' ? '维护后台项目笔记' : 'Maintaining private project notebook' });
|
|
1129
|
+
const snapshot = await buildScanSnapshot(workspaceRoot, '', { emit: runtime.emit, includeOutlines: false, signal: abortController.signal });
|
|
1130
|
+
const existingNote = getProjectNotebookStatus(workspaceRoot);
|
|
1131
|
+
const buildPlan = action === 'rebuild' || !existingNote.exists
|
|
1132
|
+
? await planHighwayNotebookBuild({
|
|
1133
|
+
snapshot, request: '', emit: runtime.emit, signal: abortController.signal
|
|
1134
|
+
})
|
|
1135
|
+
: null;
|
|
1136
|
+
await ensureHighwayProjectNotebook({
|
|
1137
|
+
workspaceRoot, snapshot, request: '', lang: currentLang, profile: getActiveProfile(), emit: runtime.emit, signal: abortController.signal,
|
|
1138
|
+
mode: action, reason: `manual-${action}`, modelRunner: streamAIResponse,
|
|
1139
|
+
confirmLarge: info => confirmLargeNotebookBuild(info, renderer), buildPlan
|
|
1140
|
+
});
|
|
1141
|
+
runtime.emit('turn.completed', { steps: 1 });
|
|
1142
|
+
} catch (error) {
|
|
1143
|
+
runtime.emit('turn.failed', { error: sanitizeUntrustedText(error.message) });
|
|
1144
|
+
} finally {
|
|
1145
|
+
process.removeListener('SIGINT', onInterrupt);
|
|
1146
|
+
renderer.dispose(); unsubscribe(); runtime.close();
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
|
|
662
1150
|
async function handleInput(input) {
|
|
663
1151
|
const trimmed = input.trim();
|
|
664
1152
|
const t = locales[currentLang];
|
|
@@ -693,6 +1181,82 @@ async function handleInput(input) {
|
|
|
693
1181
|
return;
|
|
694
1182
|
}
|
|
695
1183
|
|
|
1184
|
+
if (trimmed === '/undo') {
|
|
1185
|
+
if (!workspaceReady) console.log(currentLang === 'cn' ? '\n请先使用 /open 关联工作区。\n' : '\nUse /open to attach a workspace first.\n');
|
|
1186
|
+
else {
|
|
1187
|
+
try {
|
|
1188
|
+
const undone = await undoLastMutation(workspaceRoot);
|
|
1189
|
+
console.log(`\n${undone.ok ? '\x1b[32m' : '\x1b[33m'}${sanitizeUntrustedText(undone.message)}\x1b[0m\n`);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.log(`\n\x1b[31m${sanitizeUntrustedText(error.message)}\x1b[0m\n`);
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
promptUser();
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
if (/^\/mode(?:\s|$)/.test(trimmed)) {
|
|
1199
|
+
const command = parseInputCommand(trimmed);
|
|
1200
|
+
if (command.type === 'error') {
|
|
1201
|
+
console.log(`\n\x1b[31m${sanitizeUntrustedText(command.error)}\x1b[0m\n`);
|
|
1202
|
+
} else {
|
|
1203
|
+
const selected = command.type === 'mode.menu' ? await chooseWorkMode() : command.mode;
|
|
1204
|
+
if (selected) {
|
|
1205
|
+
const enteringThunder = selected === 'thunder' && workMode !== 'thunder';
|
|
1206
|
+
workMode = selected;
|
|
1207
|
+
saveCurrentSession(true);
|
|
1208
|
+
if (enteringThunder) await runThunderActivationAnimation({ currentLang });
|
|
1209
|
+
redrawScreen(currentLang === 'cn'
|
|
1210
|
+
? `\n\x1b[32m工作模式已切换为 ${selected === 'thunder' ? '⚡ Thunder' : '➜ Highway'}。\x1b[0m\n`
|
|
1211
|
+
: `\n\x1b[32mWork mode switched to ${selected === 'thunder' ? '⚡ Thunder' : '➜ Highway'}.\x1b[0m\n`);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
promptUser();
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
if (/^\/thunder(?:\s|$)/.test(trimmed)) {
|
|
1219
|
+
const command = parseInputCommand(trimmed);
|
|
1220
|
+
if (command.type === 'thunder.usage') {
|
|
1221
|
+
console.log(currentLang === 'cn' ? '\n用法:/thunder <需求>\n' : '\nUsage: /thunder <request>\n');
|
|
1222
|
+
promptUser();
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
const enteringThunder = workMode !== 'thunder';
|
|
1226
|
+
workMode = 'thunder';
|
|
1227
|
+
saveCurrentSession(true);
|
|
1228
|
+
if (enteringThunder) await runThunderActivationAnimation({ currentLang });
|
|
1229
|
+
await handleInput(command.prompt);
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
if (trimmed === '/agents') {
|
|
1234
|
+
await showThunderTeams();
|
|
1235
|
+
promptUser();
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
if (/^\/note(?:\s|$)/.test(trimmed)) {
|
|
1240
|
+
const command = parseInputCommand(trimmed);
|
|
1241
|
+
if (command.type === 'error') console.log(currentLang === 'cn'
|
|
1242
|
+
? '\n用法:/note status|refresh|rebuild|clear\n'
|
|
1243
|
+
: '\nUsage: /note status|refresh|rebuild|clear\n');
|
|
1244
|
+
else await handleNoteCommand(command.action);
|
|
1245
|
+
promptUser();
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (trimmed === '/memory') {
|
|
1250
|
+
if (!workspaceReady) {
|
|
1251
|
+
console.log(currentLang === 'cn' ? '\n请先使用 /open 关联工作区。\n' : '\nUse /open to attach a workspace first.\n');
|
|
1252
|
+
} else {
|
|
1253
|
+
await handleMemoryMenu();
|
|
1254
|
+
redrawScreen();
|
|
1255
|
+
}
|
|
1256
|
+
promptUser();
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
696
1260
|
if (trimmed === '/history') {
|
|
697
1261
|
while (true) {
|
|
698
1262
|
const sessions = listSessions();
|
|
@@ -751,6 +1315,9 @@ async function handleInput(input) {
|
|
|
751
1315
|
currentSessionId = chosenSession.id;
|
|
752
1316
|
currentSessionTitle = chosenSession.title;
|
|
753
1317
|
messages = chosenSession.messages || [];
|
|
1318
|
+
workMode = chosenSession.workMode === 'thunder' ? 'thunder' : 'highway';
|
|
1319
|
+
currentContextPlan = chosenSession.contextPlan || null;
|
|
1320
|
+
currentScanSummary = '';
|
|
754
1321
|
|
|
755
1322
|
const savedWorkspace = chosenSession.workspaceRoot ? path.resolve(chosenSession.workspaceRoot) : null;
|
|
756
1323
|
const savedWorkspaceUsable = savedWorkspace && fs.existsSync(savedWorkspace) && fs.statSync(savedWorkspace).isDirectory()
|
|
@@ -801,11 +1368,10 @@ async function handleInput(input) {
|
|
|
801
1368
|
if (previewContent.length > 150) {
|
|
802
1369
|
previewContent = previewContent.slice(0, 147) + '...';
|
|
803
1370
|
}
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
1371
|
+
console.log(formatConversationMessage({ ...m, displayContent: previewContent }, {
|
|
1372
|
+
color: Boolean(process.stdout.isTTY && !process.env.NO_COLOR),
|
|
1373
|
+
width: Math.max(24, Math.min(process.stdout.columns || 96, 100))
|
|
1374
|
+
}));
|
|
809
1375
|
});
|
|
810
1376
|
console.log('\x1b[90m────────────────────────\x1b[0m\n');
|
|
811
1377
|
}
|
|
@@ -833,15 +1399,46 @@ async function handleInput(input) {
|
|
|
833
1399
|
}
|
|
834
1400
|
|
|
835
1401
|
if (trimmed === '/stats' || trimmed === '/tokens') {
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
1402
|
+
if (currentLang === 'cn') {
|
|
1403
|
+
console.log(`\n\x1b[1;36mToken 使用统计\x1b[0m`);
|
|
1404
|
+
console.log(` \x1b[90m本次请求\x1b[0m`);
|
|
1405
|
+
console.log(` 输入:${sessionTokenUsage.lastInputTokens.toLocaleString()}`);
|
|
1406
|
+
console.log(` 输出:${sessionTokenUsage.lastOutputTokens.toLocaleString()}`);
|
|
1407
|
+
console.log(` 总计:${sessionTokenUsage.lastTotalTokens.toLocaleString()}`);
|
|
1408
|
+
console.log(` \x1b[90m当前会话累计\x1b[0m`);
|
|
1409
|
+
console.log(` 输入:${sessionTokenUsage.inputTokens.toLocaleString()}`);
|
|
1410
|
+
console.log(` 输出:${sessionTokenUsage.outputTokens.toLocaleString()}`);
|
|
1411
|
+
console.log(` 总计:${sessionTokenUsage.totalTokens.toLocaleString()}\n`);
|
|
1412
|
+
console.log(` \x1b[90m后台记忆提取:${sessionTokenUsage.memoryTotalTokens.toLocaleString()} Token\x1b[0m\n`);
|
|
1413
|
+
console.log(` \x1b[90m上下文压缩:${sessionTokenUsage.compactionTotalTokens.toLocaleString()} Token · 缓存读取:${sessionTokenUsage.cacheReadInputTokens.toLocaleString()} · 缓存创建:${sessionTokenUsage.cacheCreationInputTokens.toLocaleString()}\x1b[0m\n`);
|
|
1414
|
+
} else {
|
|
1415
|
+
console.log(`\n\x1b[1;36mToken usage\x1b[0m`);
|
|
1416
|
+
console.log(` \x1b[90mLast request\x1b[0m`);
|
|
1417
|
+
console.log(` Input: ${sessionTokenUsage.lastInputTokens.toLocaleString()}`);
|
|
1418
|
+
console.log(` Output: ${sessionTokenUsage.lastOutputTokens.toLocaleString()}`);
|
|
1419
|
+
console.log(` Total: ${sessionTokenUsage.lastTotalTokens.toLocaleString()}`);
|
|
1420
|
+
console.log(` \x1b[90mCurrent session\x1b[0m`);
|
|
1421
|
+
console.log(` Input: ${sessionTokenUsage.inputTokens.toLocaleString()}`);
|
|
1422
|
+
console.log(` Output: ${sessionTokenUsage.outputTokens.toLocaleString()}`);
|
|
1423
|
+
console.log(` Total: ${sessionTokenUsage.totalTokens.toLocaleString()}\n`);
|
|
1424
|
+
console.log(` \x1b[90mBackground memory extraction: ${sessionTokenUsage.memoryTotalTokens.toLocaleString()} tokens\x1b[0m\n`);
|
|
1425
|
+
console.log(` \x1b[90mContext compaction: ${sessionTokenUsage.compactionTotalTokens.toLocaleString()} tokens · cache read: ${sessionTokenUsage.cacheReadInputTokens.toLocaleString()} · cache creation: ${sessionTokenUsage.cacheCreationInputTokens.toLocaleString()}\x1b[0m\n`);
|
|
1426
|
+
}
|
|
1427
|
+
const phases = sessionTokenUsage.lastPhaseUsage;
|
|
1428
|
+
if (phases) {
|
|
1429
|
+
const label = currentLang === 'cn' ? '阶段明细' : 'Phase breakdown';
|
|
1430
|
+
console.log(` \x1b[90m${label}\x1b[0m`);
|
|
1431
|
+
for (const [name, usage] of [
|
|
1432
|
+
['Scan', phases.scan],
|
|
1433
|
+
['Notebook', phases.notebook],
|
|
1434
|
+
['Post-Scan', phases.postScan]
|
|
1435
|
+
]) {
|
|
1436
|
+
console.log(` ${name}: ${Number(usage?.inputTokens || 0).toLocaleString()} in / ${Number(usage?.outputTokens || 0).toLocaleString()} out / ${Number(usage?.totalTokens || 0).toLocaleString()} total`);
|
|
1437
|
+
}
|
|
1438
|
+
if (phases.approvedPostScanBudget) {
|
|
1439
|
+
console.log(` ${currentLang === 'cn' ? '批准的整轮预算' : 'Approved post-Scan budget'}: ${Number(phases.approvedPostScanBudget).toLocaleString()}\n`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
845
1442
|
promptUser();
|
|
846
1443
|
return;
|
|
847
1444
|
}
|
|
@@ -869,16 +1466,11 @@ async function handleInput(input) {
|
|
|
869
1466
|
return;
|
|
870
1467
|
}
|
|
871
1468
|
|
|
872
|
-
//
|
|
873
|
-
if (trimmed === '/effort') {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
setActiveEffort(selected);
|
|
878
|
-
redrawScreen(`\n\x1b[32mThinking effort set to: ${selected}\x1b[0m\n`);
|
|
879
|
-
} else {
|
|
880
|
-
redrawScreen();
|
|
881
|
-
}
|
|
1469
|
+
// Compatibility notice for the removed effort workflow.
|
|
1470
|
+
if (trimmed === '/effort' || trimmed.startsWith('/effort ')) {
|
|
1471
|
+
console.log(currentLang === 'cn'
|
|
1472
|
+
? '\n\x1b[36m/effort 已移除:上下文预算现在由每轮自适应 Scan 根据项目和需求决定。\x1b[0m\n'
|
|
1473
|
+
: '\n\x1b[36m/effort was removed. Adaptive Scan now chooses the context budget for each project task.\x1b[0m\n');
|
|
882
1474
|
promptUser();
|
|
883
1475
|
return;
|
|
884
1476
|
}
|
|
@@ -994,6 +1586,7 @@ async function handleInput(input) {
|
|
|
994
1586
|
let historyDisplay = trimmed;
|
|
995
1587
|
let executingPlan = null;
|
|
996
1588
|
let pendingPlan = null;
|
|
1589
|
+
let thunderPlanningRequest = null;
|
|
997
1590
|
|
|
998
1591
|
if (/^\/(?:plan|code)(?:\s|$)/.test(trimmed)) {
|
|
999
1592
|
if (!workspaceReady) {
|
|
@@ -1034,18 +1627,37 @@ async function handleInput(input) {
|
|
|
1034
1627
|
return;
|
|
1035
1628
|
}
|
|
1036
1629
|
}
|
|
1630
|
+
if (workMode === 'thunder') {
|
|
1631
|
+
thunderPlanningRequest = { request: command.request, name: command.name, replace };
|
|
1632
|
+
}
|
|
1037
1633
|
agentMode = 'plan';
|
|
1038
1634
|
pendingPlan = { name: command.name, request: command.request, replace };
|
|
1039
1635
|
agentPrompt = `Create the named implementation plan "${command.name}" for this request:\n${command.request}`;
|
|
1040
1636
|
} else if (command.type === 'agent' && command.mode === 'code') {
|
|
1041
1637
|
agentMode = 'code';
|
|
1042
1638
|
executingPlan = command.plan;
|
|
1639
|
+
if (executingPlan?.workflowMode === 'thunder') workMode = 'thunder';
|
|
1043
1640
|
agentPrompt = executingPlan
|
|
1044
1641
|
? `Execute the saved plan "${executingPlan.name}". Re-read current files before changing them.\n\n${executingPlan.content}`
|
|
1045
1642
|
: command.prompt;
|
|
1046
1643
|
}
|
|
1047
1644
|
}
|
|
1048
1645
|
|
|
1646
|
+
if (agentMode === 'chat' && /^(?:你好|您好|嗨|哈喽|hello|hi|hey)[!!。,.\s]*$/i.test(agentPrompt)) {
|
|
1647
|
+
currentLang = detectInputLanguage(agentPrompt, currentLang);
|
|
1648
|
+
const reply = currentLang === 'cn'
|
|
1649
|
+
? '你好,我在。你可以直接告诉我想了解、规划或修改项目中的什么。'
|
|
1650
|
+
: 'Hello, I’m here. Tell me what you want to understand, plan, or change in the project.';
|
|
1651
|
+
messages.push({ role: 'user', content: agentPrompt, displayContent: historyDisplay });
|
|
1652
|
+
messages.push({ role: 'assistant', content: reply });
|
|
1653
|
+
saveCurrentSession();
|
|
1654
|
+
console.log(formatConversationMessage({ role: 'assistant', content: reply }, {
|
|
1655
|
+
color: Boolean(process.stdout.isTTY && !process.env.NO_COLOR), width: Math.max(24, process.stdout.columns || 80)
|
|
1656
|
+
}));
|
|
1657
|
+
promptUser();
|
|
1658
|
+
return;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1049
1661
|
if (!workspaceReady) {
|
|
1050
1662
|
console.log(currentLang === 'cn'
|
|
1051
1663
|
? '\n\x1b[33m当前历史会话没有关联项目目录。请先输入 /open <项目目录>,再继续修改。\x1b[0m\n'
|
|
@@ -1062,7 +1674,16 @@ async function handleInput(input) {
|
|
|
1062
1674
|
return;
|
|
1063
1675
|
}
|
|
1064
1676
|
|
|
1677
|
+
if (workMode === 'thunder' && (thunderPlanningRequest || agentMode === 'chat')) {
|
|
1678
|
+
const target = thunderPlanningRequest || { request: agentPrompt, name: null, replace: false };
|
|
1679
|
+
currentLang = detectInputLanguage(target.request, currentLang);
|
|
1680
|
+
await runThunderPlanningFlow({ request: target.request, requestedName: target.name, replace: target.replace });
|
|
1681
|
+
promptUser();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1065
1685
|
// Push user message to history
|
|
1686
|
+
currentLang = detectInputLanguage(historyDisplay || agentPrompt, currentLang);
|
|
1066
1687
|
messages.push({ role: 'user', content: agentPrompt, displayContent: historyDisplay });
|
|
1067
1688
|
saveCurrentSession();
|
|
1068
1689
|
|
|
@@ -1073,16 +1694,39 @@ async function handleInput(input) {
|
|
|
1073
1694
|
: `\x1b[90mExecuting plan: ${sanitizeUntrustedText(executingPlan.name)}\x1b[0m\n`);
|
|
1074
1695
|
}
|
|
1075
1696
|
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
const maxLoops = preset.maxLoops;
|
|
1697
|
+
const recalledMemories = workspaceReady ? retrieveMemories(workspaceRoot, agentPrompt, 8) : [];
|
|
1698
|
+
const recalledMemoryText = formatMemoriesForPrompt(recalledMemories);
|
|
1079
1699
|
const turnTokenStart = {
|
|
1080
1700
|
input: sessionTokenUsage.inputTokens,
|
|
1081
1701
|
output: sessionTokenUsage.outputTokens
|
|
1082
1702
|
};
|
|
1083
1703
|
|
|
1084
1704
|
const runtime = createRuntimeEvents();
|
|
1085
|
-
const
|
|
1705
|
+
const thunderTeam = executingPlan?.workflowMode === 'thunder'
|
|
1706
|
+
? (findThunderTeamForPlan(workspaceRoot, executingPlan.id) || (executingPlan.teamId ? getThunderTeam(workspaceRoot, executingPlan.teamId) : null))
|
|
1707
|
+
: null;
|
|
1708
|
+
if (thunderTeam) updateThunderTeam(workspaceRoot, thunderTeam.id, { phase: 'executing' });
|
|
1709
|
+
const pendingThunderDirectives = [];
|
|
1710
|
+
let renderer;
|
|
1711
|
+
renderer = thunderTeam
|
|
1712
|
+
? createThunderRenderer({
|
|
1713
|
+
stdout: process.stdout, lang: currentLang, team: { ...thunderTeam, phase: 'executing' },
|
|
1714
|
+
onMessageRequested: async member => {
|
|
1715
|
+
renderer?.pause();
|
|
1716
|
+
try {
|
|
1717
|
+
const message = await textInputPrompt(currentLang === 'cn'
|
|
1718
|
+
? `给 ${member.name} 的消息(由 PM 记录,将在下一协调边界生效): `
|
|
1719
|
+
: `Message for ${member.name} (recorded by PM and applied at the next coordination boundary): `);
|
|
1720
|
+
if (message?.trim()) {
|
|
1721
|
+
addThunderMessage(workspaceRoot, thunderTeam.id, {
|
|
1722
|
+
from: 'user', to: member.id, type: 'question', summary: message.trim(), refs: [], requiresResponse: true
|
|
1723
|
+
});
|
|
1724
|
+
pendingThunderDirectives.push(`[PM-routed user message for ${member.name}] ${message.trim()}`);
|
|
1725
|
+
}
|
|
1726
|
+
} finally { renderer?.resume(); }
|
|
1727
|
+
}
|
|
1728
|
+
})
|
|
1729
|
+
: createTerminalRenderer({ stdout: process.stdout, lang: currentLang });
|
|
1086
1730
|
const unsubscribe = runtime.subscribe(event => renderer.handle(event));
|
|
1087
1731
|
const abortController = new AbortController();
|
|
1088
1732
|
let interrupted = false;
|
|
@@ -1095,17 +1739,190 @@ async function handleInput(input) {
|
|
|
1095
1739
|
let turnCompleted = false;
|
|
1096
1740
|
let terminalEventSent = false;
|
|
1097
1741
|
let compactionShown = false;
|
|
1742
|
+
let compactionSummary = '';
|
|
1743
|
+
let summarizedMessageCount = 0;
|
|
1098
1744
|
let turnOutcome = 'stopped';
|
|
1099
1745
|
let planActionCancelled = false;
|
|
1746
|
+
const readTracker = { files: new Map() };
|
|
1747
|
+
const configuredPermission = getActiveProfile()?.permissionPolicy || {};
|
|
1748
|
+
const permissionPolicy = {
|
|
1749
|
+
mode: ['ask', 'acceptEdits', 'allowlist'].includes(configuredPermission.mode) ? configuredPermission.mode : 'ask',
|
|
1750
|
+
rules: Array.isArray(configuredPermission.rules) ? configuredPermission.rules : [],
|
|
1751
|
+
allowedTools: new Set()
|
|
1752
|
+
};
|
|
1753
|
+
let repeatedToolSignature = '';
|
|
1754
|
+
let repeatedToolResultHash = '';
|
|
1755
|
+
let repeatedToolCount = 0;
|
|
1756
|
+
let blockedRepeatedToolSignature = '';
|
|
1757
|
+
const batchToolHistory = new Map();
|
|
1758
|
+
const blockedBatchSignatures = new Set();
|
|
1759
|
+
let planValidationAttempts = 0;
|
|
1760
|
+
let workspaceMayHaveChanged = false;
|
|
1761
|
+
const mutationJournal = [];
|
|
1762
|
+
const phaseUsage = {
|
|
1763
|
+
scan: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
1764
|
+
notebook: { inputTokens: 0, outputTokens: 0, totalTokens: 0 },
|
|
1765
|
+
postScan: { inputTokens: 0, outputTokens: 0, totalTokens: 0 }
|
|
1766
|
+
};
|
|
1767
|
+
let readPlanCancelled = false;
|
|
1768
|
+
let budgetCancelled = false;
|
|
1769
|
+
let forceBudgetFinish = false;
|
|
1770
|
+
let forceNoProgressFinish = false;
|
|
1771
|
+
let toolFreeFinishRepairAttempts = 0;
|
|
1772
|
+
let lastBudgetPromptAt = -1;
|
|
1773
|
+
|
|
1774
|
+
function addPhaseUsage(bucket, data = {}) {
|
|
1775
|
+
const target = phaseUsage[bucket];
|
|
1776
|
+
if (!target) return;
|
|
1777
|
+
target.inputTokens += Number(data.inputTokens) || 0;
|
|
1778
|
+
target.outputTokens += Number(data.outputTokens) || 0;
|
|
1779
|
+
target.totalTokens = target.inputTokens + target.outputTokens;
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1782
|
+
async function* trackedModelRunner(modelMessages, options = {}) {
|
|
1783
|
+
for await (const event of streamAIResponse(modelMessages, options)) {
|
|
1784
|
+
if (event.type === 'model.completed') {
|
|
1785
|
+
const bucket = options.usagePhase === 'notebook' ? 'notebook' : 'postScan';
|
|
1786
|
+
addPhaseUsage(bucket, event.data);
|
|
1787
|
+
runtime.emit('usage.phase', { phase: bucket, ...event.data, cumulative: phaseUsage[bucket] });
|
|
1788
|
+
}
|
|
1789
|
+
yield event;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1100
1792
|
|
|
1101
1793
|
runtime.emit('turn.started', {
|
|
1102
|
-
label: currentLang === 'cn' ? '
|
|
1794
|
+
label: currentLang === 'cn' ? '准备项目工作流' : 'Preparing project workflow'
|
|
1103
1795
|
});
|
|
1796
|
+
if (thunderTeam) {
|
|
1797
|
+
runtime.emit('team.started', { team: { ...thunderTeam, phase: 'executing' } });
|
|
1798
|
+
runtime.emit('team.phase', { teamId: thunderTeam.id, phase: 'executing' });
|
|
1799
|
+
const lead = thunderTeam.members.find(member => member.role === 'techLead');
|
|
1800
|
+
if (lead) runtime.emit('member.updated', { teamId: thunderTeam.id, member: { ...lead, status: 'working', currentTask: currentLang === 'cn' ? '执行已批准计划并管理单写入队列' : 'Execute approved plan through the single write queue' } });
|
|
1801
|
+
}
|
|
1802
|
+
if (recalledMemories.length > 0) {
|
|
1803
|
+
runtime.emit('memory.recalled', { count: recalledMemories.length });
|
|
1804
|
+
}
|
|
1104
1805
|
|
|
1105
1806
|
try {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1807
|
+
runtime.emit('phase.changed', { phase: 'scan', label: currentLang === 'cn' ? 'SCAN · 扫描项目' : 'SCAN · scanning project' });
|
|
1808
|
+
const scan = await runAdaptiveScan({
|
|
1809
|
+
workspaceRoot,
|
|
1810
|
+
request: agentPrompt,
|
|
1811
|
+
memories: recalledMemoryText,
|
|
1812
|
+
conversationContext: recentConversationForScan(messages),
|
|
1813
|
+
profile: getActiveProfile(),
|
|
1814
|
+
lang: currentLang,
|
|
1815
|
+
emit: runtime.emit,
|
|
1816
|
+
signal: abortController.signal,
|
|
1817
|
+
notebookPolicy: workMode === 'highway' ? 'highway' : 'metadata',
|
|
1818
|
+
confirmLargeNotebook: info => confirmLargeNotebookBuild(info, renderer)
|
|
1819
|
+
});
|
|
1820
|
+
currentContextPlan = scan.contextPlan;
|
|
1821
|
+
phaseUsage.scan = { ...phaseUsage.scan, ...(scan.scanUsage || {}) };
|
|
1822
|
+
currentScanSummary = workMode === 'thunder' ? scan.summary : '';
|
|
1823
|
+
if (workMode === 'highway') {
|
|
1824
|
+
runtime.emit('read.plan.presented', {
|
|
1825
|
+
contextPlan: currentContextPlan,
|
|
1826
|
+
scanUsage: scan.scanUsage || {}
|
|
1827
|
+
});
|
|
1828
|
+
const reviewed = await reviewHighwayReadPlan(currentContextPlan, scan.snapshot, scan.scanUsage, renderer);
|
|
1829
|
+
runtime.emit('read.plan.resolved', {
|
|
1830
|
+
approved: reviewed.approved,
|
|
1831
|
+
contextPlan: reviewed.plan
|
|
1832
|
+
});
|
|
1833
|
+
if (!reviewed.approved) {
|
|
1834
|
+
readPlanCancelled = true;
|
|
1835
|
+
throw new Error('READ_PLAN_CANCELLED');
|
|
1836
|
+
}
|
|
1837
|
+
currentContextPlan = reviewed.plan;
|
|
1838
|
+
currentContextPlan.budgetTokens = currentContextPlan.contextBudgetTokens || currentContextPlan.budgetTokens;
|
|
1839
|
+
runtime.emit('phase.changed', {
|
|
1840
|
+
phase: 'read',
|
|
1841
|
+
label: currentLang === 'cn' ? 'READ · 制作并读取项目笔记' : 'READ · preparing project notebook'
|
|
1842
|
+
});
|
|
1843
|
+
const groups = ['full', 'targeted', 'outline'].map((strategy, index) => ({
|
|
1844
|
+
id: `approved-${strategy}`,
|
|
1845
|
+
strategy: strategy === 'targeted' ? 'read' : strategy,
|
|
1846
|
+
paths: (currentContextPlan.files || []).filter(file => file.strategy === strategy).map(file => file.path),
|
|
1847
|
+
reason: 'User-approved Highway read plan.',
|
|
1848
|
+
priority: 100 - index * 20
|
|
1849
|
+
})).filter(group => group.paths.length);
|
|
1850
|
+
const notebookResult = await ensureHighwayProjectNotebook({
|
|
1851
|
+
workspaceRoot,
|
|
1852
|
+
snapshot: scan.snapshot,
|
|
1853
|
+
request: agentPrompt,
|
|
1854
|
+
lang: currentLang,
|
|
1855
|
+
profile: getActiveProfile(),
|
|
1856
|
+
emit: runtime.emit,
|
|
1857
|
+
signal: abortController.signal,
|
|
1858
|
+
confirmLarge: info => confirmLargeNotebookBuild(info, renderer),
|
|
1859
|
+
mode: currentContextPlan.notebook?.action === 'build' && scan.notebook ? 'rebuild'
|
|
1860
|
+
: currentContextPlan.notebook?.action === 'update' ? 'refresh' : 'auto',
|
|
1861
|
+
reason: 'read',
|
|
1862
|
+
notebookBudgetTokens: currentContextPlan.notebook?.notebookBudgetTokens || 2000,
|
|
1863
|
+
notebookSections: currentContextPlan.notebook?.sections || [],
|
|
1864
|
+
buildPlan: {
|
|
1865
|
+
rationale: currentContextPlan.rationale,
|
|
1866
|
+
defaultStrategy: 'outline',
|
|
1867
|
+
groups,
|
|
1868
|
+
validation: {
|
|
1869
|
+
requiredCoveragePercent: 100,
|
|
1870
|
+
requireEntries: true,
|
|
1871
|
+
requireConfigs: true,
|
|
1872
|
+
requireDependencies: true
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
modelRunner: trackedModelRunner
|
|
1876
|
+
});
|
|
1877
|
+
currentReadBrief = buildReadBrief(currentContextPlan, notebookResult?.notebook || scan.notebook);
|
|
1878
|
+
} else {
|
|
1879
|
+
currentReadBrief = buildReadBrief(currentContextPlan, null);
|
|
1880
|
+
}
|
|
1881
|
+
if (thunderTeam) updateThunderTeam(workspaceRoot, thunderTeam.id, { scanSnapshot: scan.snapshot, contextPlan: scan.contextPlan });
|
|
1882
|
+
saveCurrentSession();
|
|
1883
|
+
runtime.emit('phase.changed', { phase: 'read', label: currentLang === 'cn' ? 'READ · 正式读取' : 'READ · formal reading' });
|
|
1884
|
+
|
|
1885
|
+
while (!abortController.signal.aborted) {
|
|
1886
|
+
if (loopCount >= 100) {
|
|
1887
|
+
forceNoProgressFinish = true;
|
|
1888
|
+
messages.push({ role: 'user', content: '[Safety limit: 100 agent steps reached. Tools are now disabled. Finish with the best verified result and list any remaining work.]' });
|
|
1889
|
+
}
|
|
1890
|
+
while (pendingThunderDirectives.length) {
|
|
1891
|
+
messages.push({ role: 'user', content: pendingThunderDirectives.shift() });
|
|
1892
|
+
}
|
|
1893
|
+
const activeContextProfile = getActiveProfile();
|
|
1894
|
+
const compacting = shouldCompact(messages, activeContextProfile || currentContextPlan);
|
|
1895
|
+
const archivedCount = Math.max(0, messages.length - 20);
|
|
1896
|
+
if (compacting && (!compactionSummary || archivedCount >= summarizedMessageCount + 10)) {
|
|
1897
|
+
try {
|
|
1898
|
+
compactionSummary = await summarizeForCompaction(messages.slice(0, archivedCount), activeContextProfile, { signal: abortController.signal });
|
|
1899
|
+
summarizedMessageCount = archivedCount;
|
|
1900
|
+
} catch {
|
|
1901
|
+
// prepareModelMessages supplies a deterministic structured fallback.
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
const contextOptions = { profile: activeContextProfile, summary: compactionSummary };
|
|
1905
|
+
const modelMessages = prepareModelMessages(messages, currentContextPlan, contextOptions);
|
|
1906
|
+
const contextStats = getContextStats(messages, currentContextPlan, contextOptions);
|
|
1907
|
+
runtime.emit('context.usage', {
|
|
1908
|
+
usedTokens: contextStats.estimatedTokens,
|
|
1909
|
+
rawTokens: contextStats.rawEstimatedTokens,
|
|
1910
|
+
budgetTokens: contextStats.budgetTokens,
|
|
1911
|
+
compacted: contextStats.compacted
|
|
1912
|
+
});
|
|
1913
|
+
const postScanUsed = phaseUsage.notebook.totalTokens + phaseUsage.postScan.totalTokens;
|
|
1914
|
+
const turnBudget = Number(currentContextPlan.turnBudgetTokens) || Number(currentContextPlan.budgetTokens) || 24000;
|
|
1915
|
+
const predictedNextTokens = Math.max(500, Math.min(
|
|
1916
|
+
contextStats.estimatedTokens + (Number(getActiveProfile()?.maxOutputTokens) || 4096),
|
|
1917
|
+
Number(currentContextPlan.contextBudgetTokens || currentContextPlan.budgetTokens) || 24000
|
|
1918
|
+
));
|
|
1919
|
+
if (!forceBudgetFinish && !forceNoProgressFinish
|
|
1920
|
+
&& (postScanUsed >= Math.floor(turnBudget * 0.9) || postScanUsed + predictedNextTokens > turnBudget)
|
|
1921
|
+
&& lastBudgetPromptAt < 0) {
|
|
1922
|
+
runtime.emit('budget.warning', { usedTokens: postScanUsed, budgetTokens: turnBudget });
|
|
1923
|
+
lastBudgetPromptAt = postScanUsed;
|
|
1924
|
+
}
|
|
1925
|
+
const contextWasCompacted = modelMessages.some(message => String(message.content || '').startsWith('[Context summary]'));
|
|
1109
1926
|
if (!compactionShown && contextWasCompacted) {
|
|
1110
1927
|
runtime.emit('context.compacted', {
|
|
1111
1928
|
omittedMessages: Math.max(1, messages.length - modelMessages.length + 1),
|
|
@@ -1117,12 +1934,36 @@ async function handleInput(input) {
|
|
|
1117
1934
|
let activeReply = '';
|
|
1118
1935
|
const nativeToolCalls = [];
|
|
1119
1936
|
let modelWasTruncated = false;
|
|
1120
|
-
|
|
1937
|
+
const finishWithoutTools = forceBudgetFinish || forceNoProgressFinish;
|
|
1938
|
+
for await (const modelEvent of trackedModelRunner(modelMessages, {
|
|
1121
1939
|
activeOpenFile,
|
|
1122
|
-
maxReadLines: preset.maxReadLines,
|
|
1123
1940
|
signal: abortController.signal,
|
|
1124
1941
|
mode: agentMode,
|
|
1125
|
-
|
|
1942
|
+
usagePhase: 'postScan',
|
|
1943
|
+
disableTools: finishWithoutTools,
|
|
1944
|
+
disableToolsReason: forceNoProgressFinish ? 'no-progress' : 'budget',
|
|
1945
|
+
maxOutputTokens: finishWithoutTools
|
|
1946
|
+
? Math.min(1200, Number(getActiveProfile()?.maxOutputTokens) || 1200)
|
|
1947
|
+
: undefined,
|
|
1948
|
+
notebookEnabled: workMode === 'highway',
|
|
1949
|
+
workspaceRoot,
|
|
1950
|
+
lang: currentLang,
|
|
1951
|
+
projectInstructions: loadProjectInstructions(workspaceRoot),
|
|
1952
|
+
workspaceMemories: recalledMemoryText,
|
|
1953
|
+
contextPlan: currentContextPlan,
|
|
1954
|
+
...(workMode === 'highway'
|
|
1955
|
+
? { readBrief: currentReadBrief }
|
|
1956
|
+
: { scanSummary: currentScanSummary }),
|
|
1957
|
+
thunderPrompt: thunderTeam ? buildThunderSystemPrompt('techLead', {
|
|
1958
|
+
language: currentLang,
|
|
1959
|
+
capability: 'code',
|
|
1960
|
+
userRequest: executingPlan.request,
|
|
1961
|
+
approvedPlan: executingPlan.content,
|
|
1962
|
+
assignedTask: currentLang === 'cn' ? '按批准计划执行;统一管理所有文件写入并在完成前验证。' : 'Execute the approved plan, own every workspace write, and validate before completion.',
|
|
1963
|
+
decisions: executingPlan.decisions || [],
|
|
1964
|
+
relevantMemory: recalledMemoryText,
|
|
1965
|
+
resourceLimits: `Tier ${thunderTeam.resourceProposal.tier}; concurrency ${thunderTeam.resourceProposal.concurrency}; single workspace write queue.`
|
|
1966
|
+
}) : ''
|
|
1126
1967
|
})) {
|
|
1127
1968
|
if (modelEvent.type === 'model.delta') {
|
|
1128
1969
|
activeReply += modelEvent.data.text || '';
|
|
@@ -1143,6 +1984,41 @@ async function handleInput(input) {
|
|
|
1143
1984
|
}
|
|
1144
1985
|
}
|
|
1145
1986
|
|
|
1987
|
+
if (finishWithoutTools) {
|
|
1988
|
+
const finishResolution = resolveBudgetFinishResponse({
|
|
1989
|
+
reply: activeReply,
|
|
1990
|
+
nativeToolCalls,
|
|
1991
|
+
repairAttempts: toolFreeFinishRepairAttempts,
|
|
1992
|
+
lang: currentLang,
|
|
1993
|
+
reason: forceNoProgressFinish ? 'no-progress' : 'budget'
|
|
1994
|
+
});
|
|
1995
|
+
if (finishResolution.action === 'retry') {
|
|
1996
|
+
toolFreeFinishRepairAttempts++;
|
|
1997
|
+
runtime.emit('model.retry', {
|
|
1998
|
+
reason: 'tool-free-finish-protocol',
|
|
1999
|
+
label: currentLang === 'cn'
|
|
2000
|
+
? '收尾响应错误地请求了工具,正在重新生成纯文本结论'
|
|
2001
|
+
: 'The finish response requested a tool; regenerating a plain-text conclusion'
|
|
2002
|
+
});
|
|
2003
|
+
messages.push({
|
|
2004
|
+
role: 'user',
|
|
2005
|
+
content: '[System notice: The previous finish response was rejected because it attempted a tool call. Tools are unavailable. Do not continue reading and do not output any protocol or markup. Answer now in plain language using only evidence already present in the conversation. Clearly state any limits caused by missing evidence.]'
|
|
2006
|
+
});
|
|
2007
|
+
saveCurrentSession();
|
|
2008
|
+
loopCount++;
|
|
2009
|
+
continue;
|
|
2010
|
+
}
|
|
2011
|
+
if (finishResolution.action === 'fallback') {
|
|
2012
|
+
activeReply = finishResolution.reply;
|
|
2013
|
+
nativeToolCalls.length = 0;
|
|
2014
|
+
modelWasTruncated = false;
|
|
2015
|
+
runtime.emit('model.protocol_blocked', {
|
|
2016
|
+
reason: 'tool-free-finish-protocol',
|
|
2017
|
+
attempts: toolFreeFinishRepairAttempts + 1
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1146
2022
|
if (modelWasTruncated) {
|
|
1147
2023
|
runtime.emit('model.retry', {
|
|
1148
2024
|
reason: 'output-truncated',
|
|
@@ -1159,8 +2035,18 @@ async function handleInput(input) {
|
|
|
1159
2035
|
|
|
1160
2036
|
const activeProfile = getActiveProfile();
|
|
1161
2037
|
let parsedTool = null;
|
|
1162
|
-
if (
|
|
1163
|
-
parsedTool =
|
|
2038
|
+
if (finishWithoutTools) {
|
|
2039
|
+
parsedTool = null;
|
|
2040
|
+
} else if (nativeToolCalls.length > 1) {
|
|
2041
|
+
const unsupported = nativeToolCalls.find(call => !SUPPORTED_TOOLS.has(call.name));
|
|
2042
|
+
parsedTool = unsupported
|
|
2043
|
+
? { error: `Unsupported structured tool: ${unsupported.name}` }
|
|
2044
|
+
: { batch: nativeToolCalls.map((call, index) => ({
|
|
2045
|
+
toolName: call.name,
|
|
2046
|
+
toolArg: call.arguments || {},
|
|
2047
|
+
native: true,
|
|
2048
|
+
id: call.id || `${runtime.turnId}-tool-${loopCount + 1}-${index + 1}`
|
|
2049
|
+
})) };
|
|
1164
2050
|
} else if (nativeToolCalls.length === 1) {
|
|
1165
2051
|
const call = nativeToolCalls[0];
|
|
1166
2052
|
parsedTool = SUPPORTED_TOOLS.has(call.name)
|
|
@@ -1177,6 +2063,14 @@ async function handleInput(input) {
|
|
|
1177
2063
|
}
|
|
1178
2064
|
}
|
|
1179
2065
|
if (!parsedTool) {
|
|
2066
|
+
if (pendingThunderDirectives.length > 0) {
|
|
2067
|
+
const interruptedReply = sanitizeUntrustedText(stripReasoningBlocks(activeReply));
|
|
2068
|
+
if (interruptedReply) messages.push({ role: 'assistant', content: interruptedReply });
|
|
2069
|
+
while (pendingThunderDirectives.length) messages.push({ role: 'user', content: pendingThunderDirectives.shift() });
|
|
2070
|
+
saveCurrentSession();
|
|
2071
|
+
loopCount++;
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
1180
2074
|
let finalReply = sanitizeUntrustedText(stripReasoningBlocks(activeReply));
|
|
1181
2075
|
if (!finalReply) {
|
|
1182
2076
|
finalReply = currentLang === 'cn' ? '模型未返回可显示内容。' : 'The model returned no displayable content.';
|
|
@@ -1184,7 +2078,8 @@ async function handleInput(input) {
|
|
|
1184
2078
|
if (pendingPlan) {
|
|
1185
2079
|
const missingSections = validatePlanContent(finalReply);
|
|
1186
2080
|
if (missingSections.length > 0) {
|
|
1187
|
-
|
|
2081
|
+
planValidationAttempts++;
|
|
2082
|
+
if (planValidationAttempts >= 3) {
|
|
1188
2083
|
throw new Error(`Plan validation failed: missing ${missingSections.join(', ')}.`);
|
|
1189
2084
|
}
|
|
1190
2085
|
messages.push({ role: 'assistant', content: finalReply });
|
|
@@ -1194,6 +2089,7 @@ async function handleInput(input) {
|
|
|
1194
2089
|
continue;
|
|
1195
2090
|
}
|
|
1196
2091
|
}
|
|
2092
|
+
runtime.emit('phase.changed', { phase: 'act', label: currentLang === 'cn' ? 'ACT · 整理结果' : 'ACT · organizing result' });
|
|
1197
2093
|
runtime.emit('model.delta', { text: finalReply });
|
|
1198
2094
|
messages.push({ role: 'assistant', content: finalReply });
|
|
1199
2095
|
saveCurrentSession();
|
|
@@ -1208,7 +2104,7 @@ async function handleInput(input) {
|
|
|
1208
2104
|
|
|
1209
2105
|
if (currentSessionTitle === 'New Chat' || currentSessionTitle === '新会话' || currentSessionTitle === 'New Session') {
|
|
1210
2106
|
const titleSessionId = currentSessionId;
|
|
1211
|
-
const titleMessages = prepareModelMessages(messages.map(message => ({ ...message })),
|
|
2107
|
+
const titleMessages = prepareModelMessages(messages.map(message => ({ ...message })), currentContextPlan);
|
|
1212
2108
|
generateTitle(titleMessages).then(newTitle => {
|
|
1213
2109
|
if (currentSessionId === titleSessionId && newTitle && newTitle !== 'New Session') {
|
|
1214
2110
|
currentSessionTitle = sanitizeUntrustedText(newTitle).slice(0, 80);
|
|
@@ -1220,20 +2116,165 @@ async function handleInput(input) {
|
|
|
1220
2116
|
}
|
|
1221
2117
|
|
|
1222
2118
|
if (parsedTool.error) {
|
|
2119
|
+
const requestedSeveralTools = /multiple tools at once|exactly one .*invocation/i.test(parsedTool.error);
|
|
1223
2120
|
runtime.emit('tool.failed', {
|
|
1224
2121
|
tool: 'TOOL_PARSE',
|
|
1225
|
-
displaySummary:
|
|
2122
|
+
displaySummary: requestedSeveralTools
|
|
2123
|
+
? (currentLang === 'cn' ? '模型一次请求了多个工具,已要求逐个调用' : 'The model requested several tools; retrying one at a time')
|
|
2124
|
+
: (currentLang === 'cn' ? '工具格式无法解析' : 'Could not parse tool call'),
|
|
1226
2125
|
error: parsedTool.error
|
|
1227
2126
|
});
|
|
1228
|
-
messages.push({
|
|
2127
|
+
messages.push({
|
|
2128
|
+
role: 'user',
|
|
2129
|
+
content: requestedSeveralTools
|
|
2130
|
+
? '[Tool protocol correction: You requested multiple tools in one response. Request exactly one tool next. Choose the single call most likely to add new evidence; do not repeat a call that already succeeded.]'
|
|
2131
|
+
: `[Tool Response for TOOL_PARSE:\n${parsedTool.error}]`
|
|
2132
|
+
});
|
|
1229
2133
|
saveCurrentSession();
|
|
1230
2134
|
loopCount++;
|
|
1231
2135
|
continue;
|
|
1232
2136
|
}
|
|
1233
2137
|
|
|
2138
|
+
if (parsedTool.batch) {
|
|
2139
|
+
const calls = parsedTool.batch;
|
|
2140
|
+
const executable = [];
|
|
2141
|
+
const completed = [];
|
|
2142
|
+
for (const call of calls) {
|
|
2143
|
+
const signature = createToolCallSignature(call.toolName, call.toolArg);
|
|
2144
|
+
const history = batchToolHistory.get(signature);
|
|
2145
|
+
if (history?.count >= 2) {
|
|
2146
|
+
if (blockedBatchSignatures.has(signature)) forceNoProgressFinish = true;
|
|
2147
|
+
blockedBatchSignatures.add(signature);
|
|
2148
|
+
completed.push({ call, response: { ok: false }, result: `No-progress guard: duplicate ${call.toolName} was not executed; use the cached earlier result or request different evidence.` });
|
|
2149
|
+
} else executable.push(call);
|
|
2150
|
+
}
|
|
2151
|
+
const readOnly = executable.filter(call => !MUTATING_TOOLS.has(call.toolName));
|
|
2152
|
+
const mutations = executable.filter(call => MUTATING_TOOLS.has(call.toolName));
|
|
2153
|
+
for (const call of calls) {
|
|
2154
|
+
runtime.emit('tool.requested', {
|
|
2155
|
+
tool: call.toolName,
|
|
2156
|
+
toolCallId: call.id,
|
|
2157
|
+
displaySummary: describeToolRequest(call.toolName, call.toolArg, currentLang)
|
|
2158
|
+
});
|
|
2159
|
+
}
|
|
2160
|
+
messages.push({
|
|
2161
|
+
role: 'assistant',
|
|
2162
|
+
content: '',
|
|
2163
|
+
toolCalls: calls.map(call => ({ id: call.id, name: call.toolName, arguments: call.toolArg }))
|
|
2164
|
+
});
|
|
2165
|
+
saveCurrentSession();
|
|
2166
|
+
|
|
2167
|
+
const runBatchCall = async call => {
|
|
2168
|
+
const response = await executeToolCall({
|
|
2169
|
+
toolName: call.toolName,
|
|
2170
|
+
toolArg: call.toolArg,
|
|
2171
|
+
toolCallId: call.id,
|
|
2172
|
+
workspaceRoot,
|
|
2173
|
+
emit: runtime.emit,
|
|
2174
|
+
requestPermission: async request => {
|
|
2175
|
+
renderer.pause();
|
|
2176
|
+
try {
|
|
2177
|
+
if (thunderTeam && request.preview) {
|
|
2178
|
+
const lines = sanitizeUntrustedText(request.preview).split('\n');
|
|
2179
|
+
console.log(lines.slice(0, 30).join('\n'));
|
|
2180
|
+
if (lines.length > 30) console.log(currentLang === 'cn' ? `… 已折叠 ${lines.length - 30} 行预览` : `… ${lines.length - 30} preview lines collapsed`);
|
|
2181
|
+
}
|
|
2182
|
+
return await (request.allowSession ? permissionPrompt : confirmPrompt)(`\x1b[33m${sanitizeUntrustedText(request.prompt)}\x1b[0m`);
|
|
2183
|
+
} finally {
|
|
2184
|
+
renderer.resume();
|
|
2185
|
+
}
|
|
2186
|
+
},
|
|
2187
|
+
lang: currentLang,
|
|
2188
|
+
mode: agentMode,
|
|
2189
|
+
signal: abortController.signal,
|
|
2190
|
+
readTracker,
|
|
2191
|
+
permissionPolicy,
|
|
2192
|
+
contextPlan: currentContextPlan,
|
|
2193
|
+
remainingBudgetTokens: Math.max(100, (Number(currentContextPlan.turnBudgetTokens) || 24000) - phaseUsage.notebook.totalTokens - phaseUsage.postScan.totalTokens),
|
|
2194
|
+
readNotebook: workMode === 'highway' ? options => readProjectNotebook(workspaceRoot, options) : async () => null
|
|
2195
|
+
});
|
|
2196
|
+
return { call, response, result: response.modelResult || response.result };
|
|
2197
|
+
};
|
|
2198
|
+
|
|
2199
|
+
completed.push(...await Promise.all(readOnly.map(runBatchCall)));
|
|
2200
|
+
for (const call of mutations) completed.push(await runBatchCall(call));
|
|
2201
|
+
const byId = new Map(completed.map(item => [item.call.id, item]));
|
|
2202
|
+
for (const call of calls) {
|
|
2203
|
+
const item = byId.get(call.id);
|
|
2204
|
+
const toolResult = String(item?.result || 'Tool returned no result.');
|
|
2205
|
+
const signature = createToolCallSignature(call.toolName, call.toolArg);
|
|
2206
|
+
if (!toolResult.startsWith('No-progress guard:')) {
|
|
2207
|
+
const resultHash = crypto.createHash('sha256').update(toolResult).digest('hex');
|
|
2208
|
+
const previous = batchToolHistory.get(signature);
|
|
2209
|
+
batchToolHistory.set(signature, { hash: resultHash, count: previous?.hash === resultHash ? previous.count + 1 : 1 });
|
|
2210
|
+
}
|
|
2211
|
+
if (item?.response?.cancelled) planActionCancelled = true;
|
|
2212
|
+
if (agentMode === 'code' && MUTATING_TOOLS.has(call.toolName) && !item?.response?.cancelled && (item?.response?.ok || call.toolName === 'RUN_COMMAND')) {
|
|
2213
|
+
workspaceMayHaveChanged = true;
|
|
2214
|
+
mutationJournal.push({
|
|
2215
|
+
tool: call.toolName,
|
|
2216
|
+
path: call.toolArg?.path || call.toolArg?.filePath || call.toolArg?.source || '',
|
|
2217
|
+
destination: call.toolArg?.destination || call.toolArg?.to || '',
|
|
2218
|
+
at: Date.now()
|
|
2219
|
+
});
|
|
2220
|
+
}
|
|
2221
|
+
messages.push({ role: 'tool', content: toolResult, toolCallId: call.id, toolName: call.toolName });
|
|
2222
|
+
}
|
|
2223
|
+
saveCurrentSession();
|
|
2224
|
+
if (interrupted) throw new Error('Request cancelled.');
|
|
2225
|
+
loopCount += calls.length;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1234
2229
|
const toolName = parsedTool.toolName;
|
|
1235
2230
|
const toolArg = parsedTool.toolArg;
|
|
1236
2231
|
const toolCallId = parsedTool.id || `${runtime.turnId}-tool-${loopCount + 1}`;
|
|
2232
|
+
const requestedToolSignature = createToolCallSignature(toolName, toolArg);
|
|
2233
|
+
const repeatedRequest = resolveRepeatedToolRequest({
|
|
2234
|
+
signature: requestedToolSignature,
|
|
2235
|
+
previousSignature: repeatedToolSignature,
|
|
2236
|
+
identicalResultCount: repeatedToolCount,
|
|
2237
|
+
blockedSignature: blockedRepeatedToolSignature
|
|
2238
|
+
});
|
|
2239
|
+
if (repeatedRequest.action !== 'allow') {
|
|
2240
|
+
blockedRepeatedToolSignature = repeatedRequest.blockedSignature;
|
|
2241
|
+
if (repeatedRequest.action === 'redirect') {
|
|
2242
|
+
runtime.emit('model.retry', {
|
|
2243
|
+
reason: 'repeated-tool-call',
|
|
2244
|
+
label: currentLang === 'cn'
|
|
2245
|
+
? `已阻止重复调用 ${toolName},正在要求改用新的证据路径`
|
|
2246
|
+
: `Blocked repeated ${toolName}; requesting a new evidence path`
|
|
2247
|
+
});
|
|
2248
|
+
messages.push({
|
|
2249
|
+
role: 'user',
|
|
2250
|
+
content: `[No-progress guard: The exact ${toolName} call was not executed because it already returned identical evidence twice. Do not request it again. ${toolName === 'INSPECT_FILE'
|
|
2251
|
+
? 'Use READ_FILE with a task-relevant line range from the existing outline, inspect a different file, or answer from current evidence.'
|
|
2252
|
+
: 'Change the arguments, choose a different tool, or answer from current evidence.'}]`
|
|
2253
|
+
});
|
|
2254
|
+
} else {
|
|
2255
|
+
forceNoProgressFinish = true;
|
|
2256
|
+
runtime.emit('model.retry', {
|
|
2257
|
+
reason: 'repeated-tool-call-finish',
|
|
2258
|
+
label: currentLang === 'cn'
|
|
2259
|
+
? '模型仍在重复同一调用,将基于现有证据收尾'
|
|
2260
|
+
: 'The model repeated the same call again; finishing from current evidence'
|
|
2261
|
+
});
|
|
2262
|
+
messages.push({
|
|
2263
|
+
role: 'user',
|
|
2264
|
+
content: '[No-progress guard: You repeated the blocked tool call again. Tools are now disabled for this turn. Give the best concise final answer using only evidence already returned, and identify anything not verified.]'
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
saveCurrentSession();
|
|
2268
|
+
loopCount++;
|
|
2269
|
+
continue;
|
|
2270
|
+
}
|
|
2271
|
+
if (toolName === 'RUN_COMMAND') {
|
|
2272
|
+
runtime.emit('phase.changed', { phase: 'verify', label: currentLang === 'cn' ? 'VERIFY · 运行验证' : 'VERIFY · running validation' });
|
|
2273
|
+
} else if (MUTATING_TOOLS.has(toolName)) {
|
|
2274
|
+
runtime.emit('phase.changed', { phase: 'act', label: currentLang === 'cn' ? 'ACT · 准备更改' : 'ACT · preparing change' });
|
|
2275
|
+
} else {
|
|
2276
|
+
runtime.emit('phase.changed', { phase: 'read', label: currentLang === 'cn' ? 'READ · 正式读取' : 'READ · formal reading' });
|
|
2277
|
+
}
|
|
1237
2278
|
runtime.emit('tool.requested', {
|
|
1238
2279
|
tool: toolName,
|
|
1239
2280
|
toolCallId,
|
|
@@ -1253,10 +2294,15 @@ async function handleInput(input) {
|
|
|
1253
2294
|
toolCallId,
|
|
1254
2295
|
workspaceRoot,
|
|
1255
2296
|
emit: runtime.emit,
|
|
1256
|
-
requestPermission: async
|
|
2297
|
+
requestPermission: async request => {
|
|
1257
2298
|
renderer.pause();
|
|
1258
2299
|
try {
|
|
1259
|
-
|
|
2300
|
+
if (thunderTeam && request.preview) {
|
|
2301
|
+
const lines = sanitizeUntrustedText(request.preview).split('\n');
|
|
2302
|
+
console.log(lines.slice(0, 30).join('\n'));
|
|
2303
|
+
if (lines.length > 30) console.log(currentLang === 'cn' ? `… 已折叠 ${lines.length - 30} 行预览` : `… ${lines.length - 30} preview lines collapsed`);
|
|
2304
|
+
}
|
|
2305
|
+
return await (request.allowSession ? permissionPrompt : confirmPrompt)(`\x1b[33m${sanitizeUntrustedText(request.prompt)}\x1b[0m`);
|
|
1260
2306
|
} finally {
|
|
1261
2307
|
renderer.resume();
|
|
1262
2308
|
}
|
|
@@ -1264,11 +2310,39 @@ async function handleInput(input) {
|
|
|
1264
2310
|
lang: currentLang,
|
|
1265
2311
|
mode: agentMode,
|
|
1266
2312
|
signal: abortController.signal,
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
2313
|
+
readTracker,
|
|
2314
|
+
permissionPolicy,
|
|
2315
|
+
contextPlan: currentContextPlan,
|
|
2316
|
+
remainingBudgetTokens: Math.max(
|
|
2317
|
+
100,
|
|
2318
|
+
(Number(currentContextPlan.turnBudgetTokens) || 24000)
|
|
2319
|
+
- phaseUsage.notebook.totalTokens
|
|
2320
|
+
- phaseUsage.postScan.totalTokens
|
|
2321
|
+
),
|
|
2322
|
+
readNotebook: workMode === 'highway'
|
|
2323
|
+
? options => readProjectNotebook(workspaceRoot, options)
|
|
2324
|
+
: async () => null
|
|
1270
2325
|
});
|
|
1271
2326
|
const toolResult = toolResponse.modelResult || toolResponse.result;
|
|
2327
|
+
if (agentMode === 'code' && MUTATING_TOOLS.has(toolName) && !toolResponse.cancelled
|
|
2328
|
+
&& (toolResponse.ok || toolName === 'RUN_COMMAND')) {
|
|
2329
|
+
workspaceMayHaveChanged = true;
|
|
2330
|
+
mutationJournal.push({
|
|
2331
|
+
tool: toolName,
|
|
2332
|
+
path: toolArg?.path || toolArg?.filePath || toolArg?.source || '',
|
|
2333
|
+
destination: toolArg?.destination || toolArg?.to || '',
|
|
2334
|
+
at: Date.now()
|
|
2335
|
+
});
|
|
2336
|
+
}
|
|
2337
|
+
const signature = requestedToolSignature;
|
|
2338
|
+
const resultHash = crypto.createHash('sha256').update(String(toolResult || '')).digest('hex');
|
|
2339
|
+
if (signature === repeatedToolSignature && resultHash === repeatedToolResultHash) repeatedToolCount++;
|
|
2340
|
+
else {
|
|
2341
|
+
repeatedToolCount = 1;
|
|
2342
|
+
blockedRepeatedToolSignature = '';
|
|
2343
|
+
}
|
|
2344
|
+
repeatedToolSignature = signature;
|
|
2345
|
+
repeatedToolResultHash = resultHash;
|
|
1272
2346
|
if (toolResponse.cancelled) planActionCancelled = true;
|
|
1273
2347
|
messages.push({ role: 'tool', content: String(toolResult || ''), toolCallId, toolName });
|
|
1274
2348
|
saveCurrentSession();
|
|
@@ -1277,25 +2351,29 @@ async function handleInput(input) {
|
|
|
1277
2351
|
}
|
|
1278
2352
|
|
|
1279
2353
|
if (turnCompleted) {
|
|
2354
|
+
runtime.emit('phase.changed', { phase: 'verify', label: currentLang === 'cn' ? 'VERIFY · 完成检查' : 'VERIFY · final checks' });
|
|
1280
2355
|
runtime.emit('turn.completed', { steps: loopCount });
|
|
1281
2356
|
terminalEventSent = true;
|
|
1282
2357
|
turnOutcome = planActionCancelled ? 'cancelled' : 'completed';
|
|
1283
|
-
} else if (loopCount >= maxLoops) {
|
|
1284
|
-
runtime.emit('turn.failed', {
|
|
1285
|
-
error: currentLang === 'cn'
|
|
1286
|
-
? `已达到 ${maxLoops} 步上限,请提高 effort 或缩小任务范围。`
|
|
1287
|
-
: `Reached the ${maxLoops}-step limit. Raise effort or narrow the task.`
|
|
1288
|
-
});
|
|
1289
|
-
terminalEventSent = true;
|
|
1290
|
-
turnOutcome = 'blocked';
|
|
1291
2358
|
}
|
|
1292
2359
|
} catch (error) {
|
|
1293
|
-
if (interrupted || abortController.signal.aborted) {
|
|
2360
|
+
if (readPlanCancelled || budgetCancelled || interrupted || abortController.signal.aborted) {
|
|
1294
2361
|
runtime.emit('turn.cancelled', {
|
|
1295
|
-
reason:
|
|
2362
|
+
reason: readPlanCancelled
|
|
2363
|
+
? (currentLang === 'cn' ? '阅读计划已取消,未进入正式读取' : 'Read plan cancelled before formal reading')
|
|
2364
|
+
: budgetCancelled
|
|
2365
|
+
? (currentLang === 'cn' ? '已按 Token 预算选择取消本轮' : 'Turn cancelled at the token budget boundary')
|
|
2366
|
+
: (currentLang === 'cn' ? '已取消当前操作' : 'Current operation cancelled')
|
|
1296
2367
|
});
|
|
1297
2368
|
terminalEventSent = true;
|
|
1298
2369
|
turnOutcome = 'cancelled';
|
|
2370
|
+
if (readPlanCancelled) {
|
|
2371
|
+
messages.push({
|
|
2372
|
+
role: 'assistant',
|
|
2373
|
+
content: currentLang === 'cn' ? '阅读计划已取消,未进入正式读取。' : 'The read plan was cancelled before formal reading.'
|
|
2374
|
+
});
|
|
2375
|
+
saveCurrentSession();
|
|
2376
|
+
}
|
|
1299
2377
|
} else {
|
|
1300
2378
|
if (loopCount === 0) {
|
|
1301
2379
|
messages.pop();
|
|
@@ -1315,30 +2393,121 @@ async function handleInput(input) {
|
|
|
1315
2393
|
runtime.close();
|
|
1316
2394
|
}
|
|
1317
2395
|
|
|
2396
|
+
// Code changes are already authorized and durable at this point. Notebook
|
|
2397
|
+
// maintenance is best-effort and must run even after a later failure/cancel.
|
|
2398
|
+
if (workMode === 'highway' && agentMode === 'code' && workspaceMayHaveChanged
|
|
2399
|
+
&& getProjectNotebookStatus(workspaceRoot).exists) {
|
|
2400
|
+
const noteRuntime = createRuntimeEvents();
|
|
2401
|
+
const noteRenderer = createTerminalRenderer({ stdout: process.stdout, lang: currentLang });
|
|
2402
|
+
const stopNoteRender = noteRuntime.subscribe(event => noteRenderer.handle(event));
|
|
2403
|
+
const noteAbortController = new AbortController();
|
|
2404
|
+
const onNoteInterrupt = () => noteAbortController.abort();
|
|
2405
|
+
process.once('SIGINT', onNoteInterrupt);
|
|
2406
|
+
try {
|
|
2407
|
+
noteRuntime.emit('turn.started', { label: currentLang === 'cn' ? '同步后台项目笔记' : 'Synchronizing project notebook' });
|
|
2408
|
+
const snapshot = await buildScanSnapshot(workspaceRoot, '', { emit: () => {}, includeOutlines: false, signal: noteAbortController.signal });
|
|
2409
|
+
await ensureHighwayProjectNotebook({
|
|
2410
|
+
workspaceRoot, snapshot, request: agentPrompt, lang: currentLang, profile: getActiveProfile(),
|
|
2411
|
+
emit: noteRuntime.emit, mode: 'refresh', reason: 'dave-code', modelRunner: trackedModelRunner,
|
|
2412
|
+
notebookBudgetTokens: Math.max(400, Math.min(
|
|
2413
|
+
2000,
|
|
2414
|
+
(Number(currentContextPlan?.turnBudgetTokens) || 24000)
|
|
2415
|
+
- phaseUsage.notebook.totalTokens
|
|
2416
|
+
- phaseUsage.postScan.totalTokens
|
|
2417
|
+
)),
|
|
2418
|
+
mutationJournal, signal: noteAbortController.signal
|
|
2419
|
+
});
|
|
2420
|
+
noteRuntime.emit('turn.completed', { steps: 1 });
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
markProjectNotebookStale(workspaceRoot, `Dave code update failed: ${error.message}`);
|
|
2423
|
+
noteRuntime.emit('note.failed', { error: sanitizeUntrustedText(error.message), status: 'stale' });
|
|
2424
|
+
noteRuntime.emit('turn.failed', { error: currentLang === 'cn' ? '代码已保存,但项目笔记更新失败;下次 Scan 会优先修复。' : 'Code was saved, but notebook update failed; the next Scan will repair it.' });
|
|
2425
|
+
} finally {
|
|
2426
|
+
process.removeListener('SIGINT', onNoteInterrupt);
|
|
2427
|
+
noteRenderer.dispose(); stopNoteRender(); noteRuntime.close();
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
let thunderReviews = [];
|
|
2432
|
+
if (thunderTeam) {
|
|
2433
|
+
if (turnOutcome === 'completed') {
|
|
2434
|
+
const reviewRuntime = createRuntimeEvents();
|
|
2435
|
+
const reviewRenderer = createThunderRenderer({ stdout: process.stdout, lang: currentLang, team: { ...thunderTeam, phase: 'reviewing' } });
|
|
2436
|
+
const stopReviewRender = reviewRuntime.subscribe(event => reviewRenderer.handle(event));
|
|
2437
|
+
try {
|
|
2438
|
+
const executionSummary = messages.slice(-12).map(message => {
|
|
2439
|
+
if (message.role === 'tool') return `[${message.toolName || 'tool'}] ${String(message.content || '').slice(0, 1200)}`;
|
|
2440
|
+
return `[${message.role}] ${String(message.content || '').slice(0, 1600)}`;
|
|
2441
|
+
}).join('\n\n');
|
|
2442
|
+
const reviewResult = await runThunderPostReview({
|
|
2443
|
+
workspaceRoot, team: getThunderTeam(workspaceRoot, thunderTeam.id) || thunderTeam,
|
|
2444
|
+
planContent: executingPlan.content, executionSummary, lang: currentLang,
|
|
2445
|
+
memories: recalledMemoryText, emit: reviewRuntime.emit,
|
|
2446
|
+
contextPlan: currentContextPlan, scanSummary: currentScanSummary,
|
|
2447
|
+
requestPermission: async request => {
|
|
2448
|
+
reviewRenderer.pause();
|
|
2449
|
+
try {
|
|
2450
|
+
if (request.preview) console.log(sanitizeUntrustedText(request.preview).split('\n').slice(0, 30).join('\n'));
|
|
2451
|
+
return await (request.allowSession ? permissionPrompt : confirmPrompt)(`\x1b[33m${sanitizeUntrustedText(request.prompt)}\x1b[0m`);
|
|
2452
|
+
}
|
|
2453
|
+
finally { reviewRenderer.resume(); }
|
|
2454
|
+
}
|
|
2455
|
+
});
|
|
2456
|
+
thunderReviews = reviewResult.reports;
|
|
2457
|
+
if (reviewResult.blocked) turnOutcome = 'blocked';
|
|
2458
|
+
} catch (error) {
|
|
2459
|
+
thunderReviews = [{ role: 'review', report: error.message }];
|
|
2460
|
+
updateThunderTeam(workspaceRoot, thunderTeam.id, { phase: 'blocked' });
|
|
2461
|
+
turnOutcome = 'blocked';
|
|
2462
|
+
} finally {
|
|
2463
|
+
reviewRenderer.dispose();
|
|
2464
|
+
stopReviewRender();
|
|
2465
|
+
reviewRuntime.close();
|
|
2466
|
+
}
|
|
2467
|
+
if (thunderReviews.length) {
|
|
2468
|
+
console.log(currentLang === 'cn' ? '\n\x1b[1;33mThunder 最终复核\x1b[0m' : '\n\x1b[1;33mThunder final review\x1b[0m');
|
|
2469
|
+
for (const review of thunderReviews) console.log(` ${review.role}: ${sanitizeUntrustedText(review.report).replace(/\s+/g, ' ').slice(0, 300)}`);
|
|
2470
|
+
console.log('');
|
|
2471
|
+
}
|
|
2472
|
+
} else {
|
|
2473
|
+
updateThunderTeam(workspaceRoot, thunderTeam.id, { phase: turnOutcome === 'cancelled' ? 'cancelled' : 'blocked' });
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
|
|
1318
2477
|
if (executingPlan) {
|
|
1319
2478
|
updatePlan(workspaceRoot, executingPlan.id, {
|
|
1320
2479
|
status: turnOutcome === 'completed' ? 'completed' : turnOutcome === 'cancelled' ? 'ready' : 'blocked',
|
|
1321
2480
|
lastRun: {
|
|
1322
2481
|
...(executingPlan.lastRun || {}),
|
|
1323
2482
|
finishedAt: Date.now(),
|
|
1324
|
-
outcome: turnOutcome
|
|
1325
|
-
|
|
2483
|
+
outcome: turnOutcome,
|
|
2484
|
+
reviews: thunderReviews
|
|
2485
|
+
},
|
|
2486
|
+
...(thunderTeam ? { teamSnapshot: getThunderTeam(workspaceRoot, thunderTeam.id) || thunderTeam } : {})
|
|
1326
2487
|
});
|
|
1327
2488
|
}
|
|
1328
2489
|
|
|
2490
|
+
if (turnOutcome === 'completed' && workspaceReady && memorySummary(workspaceRoot).enabled) {
|
|
2491
|
+
const memoryWorkspace = workspaceRoot;
|
|
2492
|
+
const memorySessionId = currentSessionId;
|
|
2493
|
+
const memoryMessages = messages.map(message => ({ ...message }));
|
|
2494
|
+
extractMemoryCandidates(memoryMessages)
|
|
2495
|
+
.then(candidates => mergeMemoryCandidates(memoryWorkspace, candidates, memorySessionId))
|
|
2496
|
+
.catch(() => {});
|
|
2497
|
+
}
|
|
2498
|
+
|
|
1329
2499
|
sessionTokenUsage.lastInputTokens = Math.max(0, sessionTokenUsage.inputTokens - turnTokenStart.input);
|
|
1330
2500
|
sessionTokenUsage.lastOutputTokens = Math.max(0, sessionTokenUsage.outputTokens - turnTokenStart.output);
|
|
1331
2501
|
sessionTokenUsage.lastTotalTokens = sessionTokenUsage.lastInputTokens + sessionTokenUsage.lastOutputTokens;
|
|
2502
|
+
sessionTokenUsage.lastPhaseUsage = {
|
|
2503
|
+
...phaseUsage,
|
|
2504
|
+
approvedPostScanBudget: Number(currentContextPlan?.turnBudgetTokens) || 0
|
|
2505
|
+
};
|
|
1332
2506
|
|
|
1333
2507
|
if (sessionTokenUsage.lastTotalTokens > 0) {
|
|
1334
|
-
console.log(
|
|
1335
|
-
`\x1b[
|
|
1336
|
-
|
|
1337
|
-
`(${sessionTokenUsage.lastTotalTokens.toLocaleString()} total) | ` +
|
|
1338
|
-
`Session Total: ${sessionTokenUsage.inputTokens.toLocaleString()} input, ` +
|
|
1339
|
-
`${sessionTokenUsage.outputTokens.toLocaleString()} output ` +
|
|
1340
|
-
`(${sessionTokenUsage.totalTokens.toLocaleString()} total)\x1b[0m\n`
|
|
1341
|
-
);
|
|
2508
|
+
console.log(currentLang === 'cn'
|
|
2509
|
+
? `\x1b[90m本次:${sessionTokenUsage.lastInputTokens.toLocaleString()} 输入 · ${sessionTokenUsage.lastOutputTokens.toLocaleString()} 输出 · ${sessionTokenUsage.lastTotalTokens.toLocaleString()} 总计 │ 会话:${sessionTokenUsage.totalTokens.toLocaleString()} 总计\x1b[0m\n`
|
|
2510
|
+
: `\x1b[90mTurn: ${sessionTokenUsage.lastInputTokens.toLocaleString()} in · ${sessionTokenUsage.lastOutputTokens.toLocaleString()} out · ${sessionTokenUsage.lastTotalTokens.toLocaleString()} total │ Session: ${sessionTokenUsage.totalTokens.toLocaleString()} total\x1b[0m\n`);
|
|
1342
2511
|
}
|
|
1343
2512
|
|
|
1344
2513
|
if (pendingPlan || executingPlan) {
|
|
@@ -1347,6 +2516,7 @@ async function handleInput(input) {
|
|
|
1347
2516
|
: '\x1b[90mPlan status updated.\x1b[0m\n');
|
|
1348
2517
|
}
|
|
1349
2518
|
|
|
2519
|
+
await closeWorkspaceShell(workspaceRoot);
|
|
1350
2520
|
promptUser();
|
|
1351
2521
|
}
|
|
1352
2522
|
|