dave-code 1.0.4 → 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 +39 -5
- package/bin/aiClient.js +878 -159
- package/bin/check.js +11 -0
- package/bin/cliMenu.js +251 -172
- package/bin/commandRouter.js +70 -0
- package/bin/configManager.js +153 -64
- package/bin/contextManager.js +167 -0
- package/bin/index.js +2103 -573
- package/bin/markdownRenderer.js +264 -0
- package/bin/memoryManager.js +182 -0
- package/bin/planManager.js +291 -0
- package/bin/projectNotebookManager.js +839 -0
- package/bin/runtimeEvents.js +104 -0
- package/bin/scanManager.js +561 -0
- package/bin/sessionManager.js +182 -0
- package/bin/terminalRenderer.js +701 -0
- 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 +1607 -0
- package/package.json +6 -5
package/bin/index.js
CHANGED
|
@@ -10,12 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import fs from 'fs';
|
|
12
12
|
import path from 'path';
|
|
13
|
+
import crypto from 'crypto';
|
|
13
14
|
|
|
14
15
|
const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
16
|
const packageVersion = pkg.version;
|
|
16
|
-
import {
|
|
17
|
+
import { spawn } from 'child_process';
|
|
17
18
|
import { getLogoLines } from './logoRenderer.js';
|
|
18
|
-
import { hasApiKey, getAIResponse, sessionTokenUsage, resetTokenUsage } from './aiClient.js';
|
|
19
|
+
import { hasApiKey, getAIResponse, streamAIResponse, generateTitle, extractMemoryCandidates, summarizeForCompaction, sessionTokenUsage, resetTokenUsage } from './aiClient.js';
|
|
20
|
+
import { listSessions, saveSession, deleteSession } from './sessionManager.js';
|
|
19
21
|
import {
|
|
20
22
|
CONFIG_FILE,
|
|
21
23
|
getProfiles,
|
|
@@ -23,11 +25,10 @@ import {
|
|
|
23
25
|
setActiveProfile,
|
|
24
26
|
updateProfile,
|
|
25
27
|
openConfigFileInEditor,
|
|
26
|
-
|
|
27
|
-
setActiveEffort,
|
|
28
|
-
EFFORT_PRESETS,
|
|
28
|
+
inferContextWindowTokens,
|
|
29
29
|
isFirstLaunch,
|
|
30
|
-
setInitialized
|
|
30
|
+
setInitialized,
|
|
31
|
+
lastConfigError
|
|
31
32
|
} from './configManager.js';
|
|
32
33
|
import { runWelcomeAnimation } from './install.js';
|
|
33
34
|
import {
|
|
@@ -35,10 +36,73 @@ import {
|
|
|
35
36
|
secureInputPrompt,
|
|
36
37
|
chatInputPrompt,
|
|
37
38
|
waitForEnter,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
confirmPrompt
|
|
39
|
+
selectWorkModeMenu,
|
|
40
|
+
runThunderActivationAnimation,
|
|
41
|
+
confirmPrompt,
|
|
42
|
+
permissionPrompt,
|
|
43
|
+
textInputPrompt
|
|
41
44
|
} from './cliMenu.js';
|
|
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';
|
|
68
|
+
import { createRuntimeEvents } from './runtimeEvents.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';
|
|
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';
|
|
96
|
+
import {
|
|
97
|
+
deletePlan,
|
|
98
|
+
findPlan,
|
|
99
|
+
listPlans,
|
|
100
|
+
planStatusPanel,
|
|
101
|
+
renamePlan,
|
|
102
|
+
updatePlan,
|
|
103
|
+
upsertPlan,
|
|
104
|
+
validatePlanContent
|
|
105
|
+
} from './planManager.js';
|
|
42
106
|
|
|
43
107
|
// Localization Dictionary
|
|
44
108
|
const locales = {
|
|
@@ -71,13 +135,21 @@ const locales = {
|
|
|
71
135
|
helpLines: [
|
|
72
136
|
' /help - 显示此帮助信息',
|
|
73
137
|
' /clear - 清屏并重置主面板',
|
|
74
|
-
' /reset -
|
|
75
|
-
' /
|
|
76
|
-
' /
|
|
138
|
+
' /reset - 重置对话历史并新建会话',
|
|
139
|
+
' /history - 查看并载入历史聊天记录',
|
|
140
|
+
' /stats - 显示当前会话的 Token 使用统计 (别名: /tokens)',
|
|
141
|
+
' /lang - 切换中英文界面 (别名: /language)',
|
|
77
142
|
' /config - 编辑本地配置文件 (~/.dave-code-config.json)',
|
|
78
143
|
' /model - 快捷切换当前激活的服务配置',
|
|
79
144
|
' /api - 快捷修改当前配置的 API 密钥',
|
|
80
|
-
' /
|
|
145
|
+
' /mode highway|thunder - 切换单 Agent 或办公室团队工作流',
|
|
146
|
+
' /thunder <需求> - 切换到 Thunder 并开始团队规划',
|
|
147
|
+
' /agents - 查看当前工作区的 Thunder 团队',
|
|
148
|
+
' /memory - 查看、开关或删除当前工作区的跨聊天记忆',
|
|
149
|
+
' /note status|refresh|rebuild|clear - 管理 Highway 后台项目笔记',
|
|
150
|
+
' /plan 名称: 需求 - 创建只读实施计划;/plan 管理已有计划',
|
|
151
|
+
' /code <需求或计划名> - 单次授权编程修改,仍逐项确认',
|
|
152
|
+
' /undo - 回滚本进程内最近一次文件变更',
|
|
81
153
|
' /open - 打开文件或文件夹并在其中工作',
|
|
82
154
|
' /exit - 退出 Dave Code 代理'
|
|
83
155
|
],
|
|
@@ -97,7 +169,7 @@ const locales = {
|
|
|
97
169
|
保存并关闭文件后,请在下方按 \x1b[1;32m[回车 (Enter)]\x1b[0m 键重载配置。`,
|
|
98
170
|
reloadingConfigMsg: '\n\x1b[32m正在重载配置...\x1b[0m\n',
|
|
99
171
|
openDirSuccess: '已切换工作目录至: ',
|
|
100
|
-
openFileSuccess: '
|
|
172
|
+
openFileSuccess: '已在编辑器中打开现有文件。Dave 将按需读取最新内容。',
|
|
101
173
|
openPathError: '路径不存在或无法访问:',
|
|
102
174
|
openUsagePrompt: '使用方法:/open <文件或文件夹路径>'
|
|
103
175
|
},
|
|
@@ -130,13 +202,21 @@ const locales = {
|
|
|
130
202
|
helpLines: [
|
|
131
203
|
' /help - Show this help message',
|
|
132
204
|
' /clear - Clear screen and refresh home panel',
|
|
133
|
-
' /reset - Reset conversation history',
|
|
134
|
-
' /
|
|
135
|
-
' /
|
|
205
|
+
' /reset - Reset conversation history and start a new session',
|
|
206
|
+
' /history - View and resume past chat sessions',
|
|
207
|
+
' /stats - Show session token usage statistics (alias: /tokens)',
|
|
208
|
+
' /lang - Switch language between Chinese and English (alias: /language)',
|
|
136
209
|
' /config - Open and edit config file (~/.dave-code-config.json)',
|
|
137
210
|
' /model - Interactively switch active service profile',
|
|
138
211
|
' /api - Interactively set API key for active profile',
|
|
139
|
-
' /
|
|
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',
|
|
217
|
+
' /plan name: request - Create a read-only implementation plan; /plan manages plans',
|
|
218
|
+
' /code <request or plan> - Authorize one confirmed coding turn',
|
|
219
|
+
' /undo - Undo the most recent file mutation in this process',
|
|
140
220
|
' /open - Open a file or directory to work in',
|
|
141
221
|
' /exit - Exit the Dave Code agent'
|
|
142
222
|
],
|
|
@@ -156,7 +236,7 @@ Please edit and save the file in your editor.
|
|
|
156
236
|
Once done, press \x1b[1;32m[Enter]\x1b[0m below in this terminal to reload configuration.`,
|
|
157
237
|
reloadingConfigMsg: '\n\x1b[32mReloading configuration...\x1b[0m\n',
|
|
158
238
|
openDirSuccess: 'Switched working directory to: ',
|
|
159
|
-
openFileSuccess: 'Opened file
|
|
239
|
+
openFileSuccess: 'Opened the existing file. Dave will read current content on demand.',
|
|
160
240
|
openPathError: 'Path does not exist or is not accessible: ',
|
|
161
241
|
openUsagePrompt: 'Usage: /open <file or folder path>'
|
|
162
242
|
}
|
|
@@ -165,38 +245,69 @@ Once done, press \x1b[1;32m[Enter]\x1b[0m below in this terminal to reload confi
|
|
|
165
245
|
let currentLang = 'cn';
|
|
166
246
|
let messages = [];
|
|
167
247
|
let activeOpenFile = null;
|
|
248
|
+
let workspaceRoot = process.cwd();
|
|
249
|
+
let workspaceReady = true;
|
|
250
|
+
let currentSessionId = null;
|
|
251
|
+
let currentSessionTitle = 'New Chat';
|
|
252
|
+
let workMode = 'highway';
|
|
253
|
+
let currentContextPlan = null;
|
|
254
|
+
let currentScanSummary = '';
|
|
255
|
+
let currentReadBrief = '';
|
|
256
|
+
|
|
257
|
+
function initSession() {
|
|
258
|
+
currentSessionId = crypto.randomUUID();
|
|
259
|
+
currentSessionTitle = currentLang === 'cn' ? '新会话' : 'New Chat';
|
|
260
|
+
messages = [];
|
|
261
|
+
workMode = 'highway';
|
|
262
|
+
currentContextPlan = null;
|
|
263
|
+
currentScanSummary = '';
|
|
264
|
+
currentReadBrief = '';
|
|
265
|
+
saveCurrentSession();
|
|
266
|
+
}
|
|
168
267
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
return width;
|
|
268
|
+
function saveCurrentSession(allowEmpty = false) {
|
|
269
|
+
if (!currentSessionId) return;
|
|
270
|
+
if (!allowEmpty && messages.length === 0) return;
|
|
271
|
+
const sessionData = {
|
|
272
|
+
id: currentSessionId,
|
|
273
|
+
title: currentSessionTitle,
|
|
274
|
+
timestamp: Date.now(),
|
|
275
|
+
messages: messages,
|
|
276
|
+
workspaceRoot: workspaceReady ? workspaceRoot : null,
|
|
277
|
+
activeOpenFile: workspaceReady && activeOpenFile ? activeOpenFile : null,
|
|
278
|
+
workMode,
|
|
279
|
+
contextPlan: currentContextPlan
|
|
280
|
+
};
|
|
281
|
+
saveSession(currentSessionId, sessionData);
|
|
186
282
|
}
|
|
187
283
|
|
|
188
|
-
function
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
284
|
+
function redrawScreen(messageText = '') {
|
|
285
|
+
clearConsole();
|
|
286
|
+
drawHeader();
|
|
287
|
+
if (messageText) {
|
|
288
|
+
console.log(messageText);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const cleanHistory = messages.filter(m => m.role !== 'tool' && !m.toolCall && !m.content.includes('[System Context:') && !m.content.includes('[Tool Response for'));
|
|
292
|
+
if (cleanHistory.length > 0) {
|
|
293
|
+
console.log(currentLang === 'cn' ? '\x1b[90m--- 当前对话内容 (Current Chat) ---\x1b[0m' : '\x1b[90m--- Current Chat Context ---\x1b[0m');
|
|
294
|
+
cleanHistory.forEach(m => {
|
|
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}`);
|
|
301
|
+
});
|
|
302
|
+
console.log('\x1b[90m─────────────────────────────────\x1b[0m\n');
|
|
303
|
+
}
|
|
194
304
|
}
|
|
195
305
|
|
|
196
|
-
function
|
|
197
|
-
const
|
|
198
|
-
if (
|
|
199
|
-
|
|
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;
|
|
200
311
|
}
|
|
201
312
|
|
|
202
313
|
function maskKey(key) {
|
|
@@ -209,143 +320,351 @@ function clearConsole() {
|
|
|
209
320
|
process.stdout.write('\x1b[2J\x1b[0f');
|
|
210
321
|
}
|
|
211
322
|
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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 || '')}`);
|
|
225
402
|
}
|
|
226
|
-
|
|
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`);
|
|
227
407
|
}
|
|
228
408
|
|
|
229
|
-
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|
|
435
|
+
}
|
|
239
436
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
437
|
+
return sections.join('\n\n');
|
|
438
|
+
}
|
|
439
|
+
|
|
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 };
|
|
263
513
|
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
`\r\x1b[K\x1b[35m[Ultracode Workflow]\x1b[0m Digesting ${path.basename(filePath)}: ` +
|
|
267
|
-
`${barColor}[${filledBar}${emptyBar}]\x1b[0m ${percentage}% ` +
|
|
268
|
-
`(${completedCount}/${totalChunks} chunks) ${completedCount === totalChunks ? '✔' : '\x1b[36m' + spinnerFrame + '\x1b[0m'}${suffix}`
|
|
269
|
-
);
|
|
514
|
+
} finally {
|
|
515
|
+
renderer?.resume();
|
|
270
516
|
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
+
}))
|
|
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
|
+
}
|
|
546
|
+
|
|
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' };
|
|
301
570
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
results.sort((a, b) => a.idx - b.idx);
|
|
309
|
-
|
|
310
|
-
let report = `=== ULTRACODE DIGESTION REPORT FOR "${path.basename(filePath)}" ===\n`;
|
|
311
|
-
report += `Total Lines: ${totalLines}\n`;
|
|
312
|
-
report += `Resolved Path: ${resolvedPath}\n\n`;
|
|
313
|
-
|
|
314
|
-
results.forEach(res => {
|
|
315
|
-
report += `--- Segment ${res.idx + 1} (Lines ${res.start}-${res.end}) ---\n`;
|
|
316
|
-
report += `${res.summary}\n\n`;
|
|
317
|
-
});
|
|
318
|
-
|
|
319
|
-
report += `=========================================================\n`;
|
|
320
|
-
return report;
|
|
571
|
+
if (choice === 3) return { action: 'finish' };
|
|
572
|
+
return { action: 'cancel' };
|
|
573
|
+
} finally {
|
|
574
|
+
renderer?.resume();
|
|
575
|
+
}
|
|
321
576
|
}
|
|
322
577
|
|
|
323
578
|
function drawHeader() {
|
|
324
579
|
const logoLines = getLogoLines();
|
|
325
580
|
const t = locales[currentLang];
|
|
581
|
+
const active = getActiveProfile();
|
|
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);
|
|
593
|
+
const leftColWidth = 45;
|
|
594
|
+
const rightColWidth = Math.max(38, boxWidth - leftColWidth - 3);
|
|
595
|
+
const activeFileLabel = workspaceReady && activeOpenFile ? truncateMiddle(sanitizeUntrustedText(path.relative(workspaceRoot, activeOpenFile)), rightColWidth - 13) : (currentLang === 'cn' ? '未选择' : 'None');
|
|
596
|
+
const workspaceLabel = workspaceReady
|
|
597
|
+
? truncateMiddle(sanitizeUntrustedText(workspaceRoot), rightColWidth - 11)
|
|
598
|
+
: (currentLang === 'cn' ? '未关联,请使用 /open' : 'Not linked; use /open');
|
|
599
|
+
const contextLabel = `${contextStats.modelMessages}/${contextStats.savedMessages} msgs · ~${contextStats.estimatedTokens}/${contextStats.budgetTokens} tokens${contextStats.compacted ? ' · compacted' : ''}`;
|
|
600
|
+
|
|
601
|
+
if (terminalWidth < 100) {
|
|
602
|
+
const contentWidth = Math.max(32, terminalWidth - 2);
|
|
603
|
+
const rule = '─'.repeat(contentWidth);
|
|
604
|
+
const compactStatus = [
|
|
605
|
+
`${currentLang === 'cn' ? '模式' : 'Mode'}: ${workMode === 'thunder' ? '⚡ Thunder' : '➜ Highway'}`,
|
|
606
|
+
`${currentLang === 'cn' ? '模型' : 'Model'}: ${active ? active.model : '(not configured)'}`,
|
|
607
|
+
`${currentLang === 'cn' ? '上下文窗口' : 'Context window'}: ${inferContextWindowTokens(active).toLocaleString()} tokens`,
|
|
608
|
+
`${currentLang === 'cn' ? '目录' : 'Workspace'}: ${workspaceReady ? truncateMiddle(sanitizeUntrustedText(workspaceRoot), contentWidth - 5) : (currentLang === 'cn' ? '未关联,请使用 /open' : 'Not linked; use /open')}`,
|
|
609
|
+
`${currentLang === 'cn' ? '文件' : 'File'}: ${workspaceReady && activeOpenFile ? truncateMiddle(sanitizeUntrustedText(path.relative(workspaceRoot, activeOpenFile)), contentWidth - 5) : (currentLang === 'cn' ? '未选择' : 'None')}`,
|
|
610
|
+
`${currentLang === 'cn' ? '上下文' : 'Context'}: ${contextStats.modelMessages}/${contextStats.savedMessages} · ~${contextStats.estimatedTokens}/${contextStats.budgetTokens}${contextStats.compacted ? (currentLang === 'cn' ? ' · 已压缩' : ' · compacted') : ''}`,
|
|
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')}`
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
console.log(`\x1b[38;2;250;100;30m${rule}\x1b[0m`);
|
|
619
|
+
console.log(centerLine(`\x1b[1mDave Code v${packageVersion}\x1b[0m`, contentWidth));
|
|
620
|
+
for (const logoLine of logoLines) console.log(centerLine(logoLine, contentWidth));
|
|
621
|
+
console.log(`\x1b[38;2;250;100;30m${rule}\x1b[0m`);
|
|
622
|
+
for (const line of compactStatus) console.log(truncateMiddle(line, contentWidth));
|
|
623
|
+
console.log(`\x1b[90m${rule}\x1b[0m\n`);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
326
626
|
|
|
327
627
|
const rightLines = [
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
628
|
+
currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m状态\x1b[0m' : '\x1b[1;38;2;250;100;30mStatus\x1b[0m',
|
|
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`] : []),
|
|
334
641
|
'',
|
|
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`] : []),
|
|
335
648
|
'',
|
|
649
|
+
currentLang === 'cn' ? '\x1b[1;38;2;250;100;30m常用命令\x1b[0m' : '\x1b[1;38;2;250;100;30mCommands\x1b[0m',
|
|
650
|
+
'/mode /plan /code',
|
|
651
|
+
'/agents /open',
|
|
652
|
+
'/help /history /model',
|
|
653
|
+
currentLang === 'cn' ? 'Ctrl+C 或 /exit 退出' : 'Ctrl+C or /exit to quit',
|
|
336
654
|
'',
|
|
337
|
-
''
|
|
655
|
+
currentLang === 'cn' ? '文件内容会按需读取并压缩上下文。' : 'Files are read on demand and compacted.'
|
|
338
656
|
];
|
|
339
657
|
|
|
340
|
-
const leftColWidth = 45;
|
|
341
|
-
const rightColWidth = 47;
|
|
342
|
-
const boxWidth = 95;
|
|
343
|
-
|
|
344
658
|
const boxColor = '\x1b[38;2;250;100;30m';
|
|
345
659
|
const resetColor = '\x1b[0m';
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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}`;
|
|
349
668
|
|
|
350
669
|
console.log(topBorder);
|
|
351
670
|
|
|
@@ -366,18 +685,16 @@ function drawHeader() {
|
|
|
366
685
|
leftContent = centerLine(`\x1b[90m(No Models / 未配置)\x1b[0m`, leftColWidth);
|
|
367
686
|
}
|
|
368
687
|
} else if (i === 19) {
|
|
369
|
-
|
|
370
|
-
const truncatedCwd = cwd.length > leftColWidth - 2 ? '...' + cwd.slice(-(leftColWidth - 5)) : cwd;
|
|
371
|
-
leftContent = centerLine(`\x1b[90m${truncatedCwd}\x1b[0m`, leftColWidth);
|
|
688
|
+
leftContent = centerLine(`\x1b[90m${truncateMiddle(process.cwd(), leftColWidth - 2)}\x1b[0m`, leftColWidth);
|
|
372
689
|
}
|
|
373
690
|
|
|
374
|
-
const rightContent = padLine(rightLines[i] || '', rightColWidth);
|
|
691
|
+
const rightContent = padLine(truncateMiddle(rightLines[i] || '', rightColWidth), rightColWidth);
|
|
375
692
|
|
|
376
693
|
console.log(
|
|
377
|
-
boxColor + '│ ' + resetColor +
|
|
378
|
-
leftContent +
|
|
379
|
-
boxColor + ' │ ' + resetColor +
|
|
380
|
-
rightContent +
|
|
694
|
+
boxColor + '│ ' + resetColor +
|
|
695
|
+
leftContent +
|
|
696
|
+
boxColor + ' │ ' + resetColor +
|
|
697
|
+
rightContent +
|
|
381
698
|
boxColor + ' │' + resetColor
|
|
382
699
|
);
|
|
383
700
|
}
|
|
@@ -387,90 +704,39 @@ function drawHeader() {
|
|
|
387
704
|
|
|
388
705
|
}
|
|
389
706
|
|
|
390
|
-
|
|
391
|
-
constructor() {
|
|
392
|
-
this.actionsCount = 0;
|
|
393
|
-
this.actionTypes = {};
|
|
394
|
-
this.lastActionText = '';
|
|
395
|
-
this.spinnerIdx = 0;
|
|
396
|
-
this.spinnerInterval = null;
|
|
397
|
-
this.statusColor = '\x1b[35m';
|
|
398
|
-
}
|
|
707
|
+
let promptLoopRunning = false;
|
|
399
708
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const cleanActionText = this.lastActionText.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
428
|
-
|
|
429
|
-
let displayAction = cleanActionText;
|
|
430
|
-
if (displayAction.length > 50) {
|
|
431
|
-
displayAction = displayAction.slice(0, 47) + '...';
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
process.stdout.write(
|
|
435
|
-
`\r\x1b[K\x1b[35m⚙ [Agent Actions]\x1b[0m ${this.actionsCount} actions | Last: ${displayAction} \x1b[36m${spinnerFrame}\x1b[0m`
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
stop() {
|
|
440
|
-
if (this.spinnerInterval) {
|
|
441
|
-
clearInterval(this.spinnerInterval);
|
|
442
|
-
this.spinnerInterval = null;
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
finish() {
|
|
447
|
-
this.stop();
|
|
448
|
-
if (this.actionsCount > 0) {
|
|
449
|
-
const typesStr = Object.entries(this.actionTypes)
|
|
450
|
-
.map(([type, count]) => `${count} ${type.toLowerCase()}${count > 1 ? 's' : ''}`)
|
|
451
|
-
.join(', ');
|
|
452
|
-
const summaryText = `Completed ${this.actionsCount} actions (${typesStr})`;
|
|
453
|
-
process.stdout.write(`\r\x1b[K\x1b[32m✔ [Agent Actions]\x1b[0m ${summaryText}\n`);
|
|
709
|
+
async function promptUser() {
|
|
710
|
+
if (promptLoopRunning) return;
|
|
711
|
+
promptLoopRunning = true;
|
|
712
|
+
|
|
713
|
+
while (true) {
|
|
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}`;
|
|
722
|
+
const promptWidth = Math.max(32, process.stdout.columns || 80);
|
|
723
|
+
const padding = ' '.repeat(Math.max(1, promptWidth - getStringWidth(statusText) - 1));
|
|
724
|
+
const colorCode = '\x1b[90m';
|
|
725
|
+
console.log(padding + colorCode + statusText + '\x1b[0m');
|
|
726
|
+
const activeFileBase = activeOpenFile ? path.basename(activeOpenFile) : '';
|
|
727
|
+
const placeholder = activeOpenFile
|
|
728
|
+
? (currentLang === 'cn' ? `正在针对 [${activeFileBase}] 工作...` : `Working on [${activeFileBase}]...`)
|
|
729
|
+
: (currentLang === 'cn' ? '输入问题或命令,例如 /help ...' : 'Enter a prompt or command, e.g. /help ...');
|
|
730
|
+
const input = await chatInputPrompt(placeholder, currentLang);
|
|
731
|
+
try {
|
|
732
|
+
await handleInput(input);
|
|
733
|
+
} catch (error) {
|
|
734
|
+
process.stdout.write('\x1b[?25h');
|
|
735
|
+
console.log(`\n\x1b[31mUnexpected error:\x1b[0m ${error.message}\n`);
|
|
454
736
|
}
|
|
455
|
-
process.stdout.write('\x1b[?25h'); // Show cursor
|
|
456
737
|
}
|
|
457
738
|
}
|
|
458
739
|
|
|
459
|
-
async function promptUser() {
|
|
460
|
-
const effort = getActiveEffort();
|
|
461
|
-
const effortText = `● ${effort} · /effort`;
|
|
462
|
-
const N = effort.length;
|
|
463
|
-
const padding = ' '.repeat(Math.max(1, 80 - N));
|
|
464
|
-
const colorCode = effort === 'ultracode' ? '\x1b[1;38;2;140;90;240m' : '\x1b[90m';
|
|
465
|
-
console.log(padding + colorCode + effortText + '\x1b[0m');
|
|
466
|
-
const activeFileBase = activeOpenFile ? path.basename(activeOpenFile) : '';
|
|
467
|
-
const placeholder = activeOpenFile
|
|
468
|
-
? (currentLang === 'cn' ? `正在针对 [${activeFileBase}] 工作...` : `Working on [${activeFileBase}]...`)
|
|
469
|
-
: (currentLang === 'cn' ? 'Try "how do I log an error?" / 输入提示词或命令...' : 'Try "how do I log an error?" / Enter prompt or command...');
|
|
470
|
-
const input = await chatInputPrompt(placeholder, currentLang);
|
|
471
|
-
handleInput(input);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
740
|
// ── Interactive Config Handlers ──
|
|
475
741
|
|
|
476
742
|
async function triggerProfileSwitch(t) {
|
|
@@ -491,13 +757,396 @@ async function triggerProfileSwitch(t) {
|
|
|
491
757
|
const chosenProfile = profiles[selectedIdx];
|
|
492
758
|
setActiveProfile(chosenProfile.model);
|
|
493
759
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
760
|
+
redrawScreen(`\n\x1b[32mActive model set to: ${chosenProfile.model}\x1b[0m\n`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function describeToolRequest(toolName, toolArg, lang) {
|
|
764
|
+
const cn = lang === 'cn';
|
|
765
|
+
const structuredValue = toolArg && typeof toolArg === 'object'
|
|
766
|
+
? (toolArg.path || toolArg.filePath || toolArg.source || toolArg.query || toolArg.command || '')
|
|
767
|
+
: toolArg;
|
|
768
|
+
const firstLine = String(structuredValue || '').split('\n', 1)[0].trim();
|
|
769
|
+
const target = firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
770
|
+
const labels = {
|
|
771
|
+
LIST_DIR: cn ? `准备查看 ${target || '.'}` : `Preparing to list ${target || '.'}`,
|
|
772
|
+
INSPECT_FILE: cn ? `准备分析 ${target} 的结构` : `Preparing to inspect ${target}`,
|
|
773
|
+
READ_FILE: cn ? `准备读取 ${target}` : `Preparing to read ${target}`,
|
|
774
|
+
READ_NOTEBOOK: cn ? '准备按需读取项目笔记' : 'Preparing to read the project notebook',
|
|
775
|
+
EDIT_FILE: cn ? `准备局部修改 ${target}` : `Preparing to edit ${target}`,
|
|
776
|
+
WRITE_FILE: cn ? `准备修改 ${target}` : `Preparing to change ${target}`,
|
|
777
|
+
SEARCH_GREP: cn ? `准备搜索 “${target}”` : `Preparing to search "${target}"`,
|
|
778
|
+
MAKE_DIR: cn ? `准备创建目录 ${target}` : `Preparing to create directory ${target}`,
|
|
779
|
+
MOVE_PATH: cn ? `准备移动 ${target}` : `Preparing to move ${target}`,
|
|
780
|
+
DELETE_PATH: cn ? `准备删除 ${target}` : `Preparing to delete ${target}`,
|
|
781
|
+
RUN_COMMAND: cn ? `准备运行命令 ${target}` : `Preparing to run ${target}`
|
|
782
|
+
};
|
|
783
|
+
return labels[toolName] || (cn ? `准备执行 ${toolName}` : `Preparing ${toolName}`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function launchEditor(filePath) {
|
|
787
|
+
const command = process.platform === 'win32'
|
|
788
|
+
? { file: 'explorer.exe', args: [filePath] }
|
|
789
|
+
: process.platform === 'darwin'
|
|
790
|
+
? { file: 'open', args: [filePath] }
|
|
791
|
+
: { file: 'xdg-open', args: [filePath] };
|
|
792
|
+
const child = spawn(command.file, command.args, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
793
|
+
child.on('error', () => {});
|
|
794
|
+
child.unref();
|
|
795
|
+
}
|
|
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
|
+
|
|
850
|
+
async function handlePlanMenu() {
|
|
851
|
+
while (true) {
|
|
852
|
+
const plans = listPlans(workspaceRoot);
|
|
853
|
+
if (plans.length === 0) {
|
|
854
|
+
console.log(currentLang === 'cn'
|
|
855
|
+
? '\n\x1b[90m暂无计划。使用 /plan 名称: 需求 创建。\x1b[0m\n'
|
|
856
|
+
: '\n\x1b[90mNo plans. Use /plan name: request to create one.\x1b[0m\n');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
const selected = await selectMenu(
|
|
860
|
+
currentLang === 'cn' ? '工作区计划' : 'Workspace Plans',
|
|
861
|
+
currentLang === 'cn' ? '选择一个计划进行查看、重命名或删除。' : 'Select a plan to view, rename, or delete.',
|
|
862
|
+
plans.map(plan => ({ name: plan.name, desc: `${plan.status} · ${new Date(plan.updatedAt).toLocaleString()}` })),
|
|
863
|
+
0
|
|
864
|
+
);
|
|
865
|
+
if (selected === -1) return;
|
|
866
|
+
const plan = plans[selected];
|
|
867
|
+
const action = await selectMenu(
|
|
868
|
+
plan.name,
|
|
869
|
+
currentLang === 'cn' ? `状态: ${plan.status}` : `Status: ${plan.status}`,
|
|
870
|
+
currentLang === 'cn'
|
|
871
|
+
? [
|
|
872
|
+
{ name: '查看计划', desc: '显示完整的做什么与怎么做' },
|
|
873
|
+
{ name: '重命名', desc: '修改计划名称' },
|
|
874
|
+
{ name: '删除', desc: '永久删除计划' },
|
|
875
|
+
{ name: '返回', desc: '返回计划列表' }
|
|
876
|
+
]
|
|
877
|
+
: [
|
|
878
|
+
{ name: 'View Plan', desc: 'Show the complete what-and-how plan' },
|
|
879
|
+
{ name: 'Rename', desc: 'Change the plan name' },
|
|
880
|
+
{ name: 'Delete', desc: 'Permanently delete the plan' },
|
|
881
|
+
{ name: 'Back', desc: 'Return to the plan list' }
|
|
882
|
+
],
|
|
883
|
+
0
|
|
884
|
+
);
|
|
885
|
+
if (action === -1 || action === 3) continue;
|
|
886
|
+
if (action === 0) {
|
|
887
|
+
console.log(`\n\x1b[1;36m${sanitizeUntrustedText(plan.name)}\x1b[0m`);
|
|
888
|
+
console.log(`${sanitizeUntrustedText(plan.content)}\n`);
|
|
889
|
+
await waitForEnter(currentLang === 'cn' ? '按 Enter 返回计划列表。' : 'Press Enter to return to plans.');
|
|
890
|
+
} else if (action === 1) {
|
|
891
|
+
const nextName = await textInputPrompt(currentLang === 'cn' ? '新计划名称: ' : 'New plan name: ');
|
|
892
|
+
if (nextName) {
|
|
893
|
+
try {
|
|
894
|
+
renamePlan(workspaceRoot, plan.id, nextName);
|
|
895
|
+
} catch (error) {
|
|
896
|
+
console.log(`\n\x1b[31m${sanitizeUntrustedText(error.message)}\x1b[0m\n`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
} else if (action === 2) {
|
|
900
|
+
const allowed = await confirmPrompt(currentLang === 'cn'
|
|
901
|
+
? `删除计划 "${sanitizeUntrustedText(plan.name)}"?(y/n): `
|
|
902
|
+
: `Delete plan "${sanitizeUntrustedText(plan.name)}"? (y/n): `);
|
|
903
|
+
if (allowed) deletePlan(workspaceRoot, plan.id);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function sessionMatchesWorkspace(session, candidateRoot) {
|
|
909
|
+
const referencedFiles = new Set();
|
|
910
|
+
for (const message of session.messages || []) {
|
|
911
|
+
if (message.role !== 'assistant') continue;
|
|
912
|
+
const parsed = message.toolCall
|
|
913
|
+
? { toolName: message.toolCall.name, toolArg: message.toolCall.arguments }
|
|
914
|
+
: parseToolCall(message.content || '');
|
|
915
|
+
if (!parsed || parsed.error || !['READ_FILE', 'EDIT_FILE', 'WRITE_FILE'].includes(parsed.toolName)) continue;
|
|
916
|
+
const rawPath = parsed.toolArg && typeof parsed.toolArg === 'object'
|
|
917
|
+
? (parsed.toolArg.path || parsed.toolArg.filePath || '')
|
|
918
|
+
: parsed.toolArg;
|
|
919
|
+
let filePath = String(rawPath || '').split('\n', 1)[0].replace(/:(\d+)-(\d+)$/, '').trim();
|
|
920
|
+
if (filePath) referencedFiles.add(filePath);
|
|
921
|
+
if (referencedFiles.size >= 8) break;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
let matches = 0;
|
|
925
|
+
for (const filePath of referencedFiles) {
|
|
926
|
+
try {
|
|
927
|
+
const resolved = resolveExistingWorkspacePath(filePath, candidateRoot);
|
|
928
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) matches++;
|
|
929
|
+
if (matches >= 2) return true;
|
|
930
|
+
} catch (error) {
|
|
931
|
+
// A missing reference simply means this is probably not the old workspace.
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
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');
|
|
497
943
|
}
|
|
498
944
|
|
|
499
945
|
// ── Main REPL Input Handler ──
|
|
500
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
|
+
|
|
501
1150
|
async function handleInput(input) {
|
|
502
1151
|
const trimmed = input.trim();
|
|
503
1152
|
const t = locales[currentLang];
|
|
@@ -525,23 +1174,271 @@ async function handleInput(input) {
|
|
|
525
1174
|
}
|
|
526
1175
|
|
|
527
1176
|
if (trimmed === '/reset') {
|
|
528
|
-
|
|
1177
|
+
initSession();
|
|
529
1178
|
resetTokenUsage();
|
|
530
1179
|
console.log(`\x1b[32m${t.resetMsg}\x1b[0m\n`);
|
|
531
1180
|
promptUser();
|
|
532
1181
|
return;
|
|
533
1182
|
}
|
|
534
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
|
+
|
|
1260
|
+
if (trimmed === '/history') {
|
|
1261
|
+
while (true) {
|
|
1262
|
+
const sessions = listSessions();
|
|
1263
|
+
if (sessions.length === 0) {
|
|
1264
|
+
console.log(currentLang === 'cn' ? '\n\x1b[31m暂无历史聊天记录!\x1b[0m\n' : '\n\x1b[31mNo chat history found!\x1b[0m\n');
|
|
1265
|
+
promptUser();
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
const menuOptions = sessions.map(s => {
|
|
1270
|
+
const dateStr = new Date(s.timestamp || Date.now()).toLocaleString(currentLang === 'cn' ? 'zh-CN' : 'en-US', {
|
|
1271
|
+
month: 'short',
|
|
1272
|
+
day: 'numeric',
|
|
1273
|
+
hour: '2-digit',
|
|
1274
|
+
minute: '2-digit'
|
|
1275
|
+
});
|
|
1276
|
+
const cleanMsgs = (s.messages || []).filter(m => m.role !== 'tool' && !m.toolCall && !m.content.includes('[System Context:') && !m.content.includes('[Tool Response for'));
|
|
1277
|
+
const msgCount = cleanMsgs.length;
|
|
1278
|
+
return {
|
|
1279
|
+
name: sanitizeUntrustedText(s.title || (currentLang === 'cn' ? '未命名会话' : 'Untitled Session')),
|
|
1280
|
+
desc: `${dateStr} | ${msgCount} ${currentLang === 'cn' ? '条对话' : 'messages'}`
|
|
1281
|
+
};
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
const titleText = currentLang === 'cn' ? '历史聊天记录 (Chat History)' : 'Chat History';
|
|
1285
|
+
const descText = currentLang === 'cn' ? '选择一个历史聊天记录以继续或删除:' : 'Select a chat history to resume or delete:';
|
|
1286
|
+
|
|
1287
|
+
const currentIdx = sessions.findIndex(s => s.id === currentSessionId);
|
|
1288
|
+
const selectedIdx = await selectMenu(titleText, descText, menuOptions, currentIdx !== -1 ? currentIdx : 0);
|
|
1289
|
+
|
|
1290
|
+
if (selectedIdx === -1) {
|
|
1291
|
+
redrawScreen();
|
|
1292
|
+
promptUser();
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const chosenSession = sessions[selectedIdx];
|
|
1297
|
+
|
|
1298
|
+
const safeChosenTitle = sanitizeUntrustedText(chosenSession.title);
|
|
1299
|
+
const actionTitle = currentLang === 'cn' ? `选择操作: "${safeChosenTitle}"` : `Actions for: "${safeChosenTitle}"`;
|
|
1300
|
+
const actionDesc = currentLang === 'cn' ? '请选择您想要进行的操作:' : 'Please select an action:';
|
|
1301
|
+
|
|
1302
|
+
const actionOptions = currentLang === 'cn' ? [
|
|
1303
|
+
{ name: '载入并继续对话 (Resume)', desc: '加载此会话的历史聊天内容并继续' },
|
|
1304
|
+
{ name: '删除此会话 (Delete)', desc: '从本地磁盘永久删除此聊天记录' },
|
|
1305
|
+
{ name: '取消并返回 (Cancel)', desc: '返回上级历史记录列表' }
|
|
1306
|
+
] : [
|
|
1307
|
+
{ name: 'Resume Conversation', desc: 'Load the messages and continue chatting' },
|
|
1308
|
+
{ name: 'Delete Session', desc: 'Permanently remove this chat file from disk' },
|
|
1309
|
+
{ name: 'Cancel & Back', desc: 'Go back to the sessions list' }
|
|
1310
|
+
];
|
|
1311
|
+
|
|
1312
|
+
const actionIdx = await selectMenu(actionTitle, actionDesc, actionOptions, 0);
|
|
1313
|
+
|
|
1314
|
+
if (actionIdx === 0) {
|
|
1315
|
+
currentSessionId = chosenSession.id;
|
|
1316
|
+
currentSessionTitle = chosenSession.title;
|
|
1317
|
+
messages = chosenSession.messages || [];
|
|
1318
|
+
workMode = chosenSession.workMode === 'thunder' ? 'thunder' : 'highway';
|
|
1319
|
+
currentContextPlan = chosenSession.contextPlan || null;
|
|
1320
|
+
currentScanSummary = '';
|
|
1321
|
+
|
|
1322
|
+
const savedWorkspace = chosenSession.workspaceRoot ? path.resolve(chosenSession.workspaceRoot) : null;
|
|
1323
|
+
const savedWorkspaceUsable = savedWorkspace && fs.existsSync(savedWorkspace) && fs.statSync(savedWorkspace).isDirectory()
|
|
1324
|
+
? savedWorkspace
|
|
1325
|
+
: null;
|
|
1326
|
+
const inferredWorkspace = !savedWorkspaceUsable && sessionMatchesWorkspace(chosenSession, workspaceRoot)
|
|
1327
|
+
? path.resolve(workspaceRoot)
|
|
1328
|
+
: null;
|
|
1329
|
+
const restorableWorkspace = savedWorkspaceUsable || inferredWorkspace;
|
|
1330
|
+
let workspaceMessage;
|
|
1331
|
+
if (restorableWorkspace && fs.existsSync(restorableWorkspace) && fs.statSync(restorableWorkspace).isDirectory()) {
|
|
1332
|
+
process.chdir(restorableWorkspace);
|
|
1333
|
+
workspaceRoot = restorableWorkspace;
|
|
1334
|
+
workspaceReady = true;
|
|
1335
|
+
activeOpenFile = null;
|
|
1336
|
+
if (chosenSession.activeOpenFile) {
|
|
1337
|
+
try {
|
|
1338
|
+
const restoredFile = resolveWorkspacePath(chosenSession.activeOpenFile, workspaceRoot);
|
|
1339
|
+
if (fs.existsSync(restoredFile) && fs.statSync(restoredFile).isFile()) activeOpenFile = restoredFile;
|
|
1340
|
+
} catch (error) {
|
|
1341
|
+
// The saved active file may have been moved or deleted.
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
workspaceMessage = currentLang === 'cn'
|
|
1345
|
+
? `\x1b[90m工作区: ${workspaceRoot}${inferredWorkspace ? '(已从历史验证)' : ''}\x1b[0m`
|
|
1346
|
+
: `\x1b[90mWorkspace: ${workspaceRoot}${inferredWorkspace ? ' (verified from history)' : ''}\x1b[0m`;
|
|
1347
|
+
saveCurrentSession();
|
|
1348
|
+
} else {
|
|
1349
|
+
workspaceReady = false;
|
|
1350
|
+
activeOpenFile = null;
|
|
1351
|
+
workspaceMessage = currentLang === 'cn'
|
|
1352
|
+
? `\x1b[33m此旧会话没有可恢复的工作区。请先使用 /open <项目目录>。\x1b[0m`
|
|
1353
|
+
: `\x1b[33mThis older session has no restorable workspace. Use /open <project-directory> first.\x1b[0m`;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
redrawScreen(currentLang === 'cn'
|
|
1357
|
+
? `\n\x1b[32m已成功载入会话: "${sanitizeUntrustedText(currentSessionTitle)}"\x1b[0m\n${workspaceMessage}\n`
|
|
1358
|
+
: `\n\x1b[32mSuccessfully loaded session: "${sanitizeUntrustedText(currentSessionTitle)}"\x1b[0m\n${workspaceMessage}\n`
|
|
1359
|
+
);
|
|
1360
|
+
|
|
1361
|
+
const cleanHistory = messages.filter(m => m.role !== 'tool' && !m.toolCall && !m.content.includes('[System Context:') && !m.content.includes('[Tool Response for'));
|
|
1362
|
+
const lastMessages = cleanHistory.slice(-6);
|
|
1363
|
+
|
|
1364
|
+
if (lastMessages.length > 0) {
|
|
1365
|
+
console.log(currentLang === 'cn' ? '\x1b[90m--- 最近聊天内容预览 ---\x1b[0m' : '\x1b[90m--- Recent Chat Preview ---\x1b[0m');
|
|
1366
|
+
lastMessages.forEach(m => {
|
|
1367
|
+
let previewContent = sanitizeUntrustedText(stripReasoningBlocks(m.displayContent || m.content));
|
|
1368
|
+
if (previewContent.length > 150) {
|
|
1369
|
+
previewContent = previewContent.slice(0, 147) + '...';
|
|
1370
|
+
}
|
|
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
|
+
}));
|
|
1375
|
+
});
|
|
1376
|
+
console.log('\x1b[90m────────────────────────\x1b[0m\n');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
promptUser();
|
|
1380
|
+
return;
|
|
1381
|
+
} else if (actionIdx === 1) {
|
|
1382
|
+
const confirmMsg = currentLang === 'cn'
|
|
1383
|
+
? `\x1b[31m警告:您确定要删除此会话 "${safeChosenTitle}" 吗?此操作无法撤销! (y/n): \x1b[0m`
|
|
1384
|
+
: `\x1b[31mWarning: Are you sure you want to delete session "${safeChosenTitle}"? This cannot be undone! (y/n): \x1b[0m`;
|
|
1385
|
+
|
|
1386
|
+
const confirmed = await confirmPrompt(confirmMsg);
|
|
1387
|
+
if (confirmed) {
|
|
1388
|
+
deleteSession(chosenSession.id);
|
|
1389
|
+
|
|
1390
|
+
if (chosenSession.id === currentSessionId) {
|
|
1391
|
+
initSession();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
console.log(currentLang === 'cn' ? '\n\x1b[32m✔ 会话已成功删除!\x1b[0m\n' : '\n\x1b[32m✔ Session deleted successfully!\x1b[0m\n');
|
|
1395
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
535
1401
|
if (trimmed === '/stats' || trimmed === '/tokens') {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
+
}
|
|
545
1442
|
promptUser();
|
|
546
1443
|
return;
|
|
547
1444
|
}
|
|
@@ -549,9 +1446,7 @@ async function handleInput(input) {
|
|
|
549
1446
|
if (trimmed === '/lang' || trimmed === '/language') {
|
|
550
1447
|
currentLang = currentLang === 'cn' ? 'en' : 'cn';
|
|
551
1448
|
const nextT = locales[currentLang];
|
|
552
|
-
|
|
553
|
-
drawHeader();
|
|
554
|
-
console.log(`\x1b[32m${nextT.switchMsg}\x1b[0m\n`);
|
|
1449
|
+
redrawScreen(`\x1b[32m${nextT.switchMsg}\x1b[0m\n`);
|
|
555
1450
|
promptUser();
|
|
556
1451
|
return;
|
|
557
1452
|
}
|
|
@@ -571,19 +1466,11 @@ async function handleInput(input) {
|
|
|
571
1466
|
return;
|
|
572
1467
|
}
|
|
573
1468
|
|
|
574
|
-
//
|
|
575
|
-
if (trimmed === '/effort') {
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
setActiveEffort(selected);
|
|
580
|
-
clearConsole();
|
|
581
|
-
drawHeader();
|
|
582
|
-
console.log(`\n\x1b[32mThinking effort set to: ${selected}\x1b[0m\n`);
|
|
583
|
-
} else {
|
|
584
|
-
clearConsole();
|
|
585
|
-
drawHeader();
|
|
586
|
-
}
|
|
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');
|
|
587
1474
|
promptUser();
|
|
588
1475
|
return;
|
|
589
1476
|
}
|
|
@@ -610,17 +1497,22 @@ async function handleInput(input) {
|
|
|
610
1497
|
|
|
611
1498
|
// Pause prompt and wait for Enter key
|
|
612
1499
|
await waitForEnter(t.openingConfigMsg);
|
|
613
|
-
|
|
614
|
-
//
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
1500
|
+
|
|
1501
|
+
// Give the editor a brief moment to flush the saved file, then read it again from disk.
|
|
1502
|
+
await new Promise(resolve => setTimeout(resolve, 150));
|
|
1503
|
+
const reloadedProfile = getActiveProfile();
|
|
1504
|
+
const reloadMessage = reloadedProfile
|
|
1505
|
+
? (currentLang === 'cn'
|
|
1506
|
+
? `\n\x1b[32m配置已重载\x1b[0m\n\x1b[90m当前模型: ${reloadedProfile.model}\n配置文件: ${CONFIG_FILE}\x1b[0m\n`
|
|
1507
|
+
: `\n\x1b[32mConfiguration reloaded\x1b[0m\n\x1b[90mActive model: ${reloadedProfile.model}\nConfig file: ${CONFIG_FILE}\x1b[0m\n`)
|
|
1508
|
+
: `\n\x1b[31m${t.noActiveError}\x1b[0m\n\x1b[90m${CONFIG_FILE}\x1b[0m\n`;
|
|
1509
|
+
redrawScreen(reloadMessage);
|
|
618
1510
|
promptUser();
|
|
619
1511
|
return;
|
|
620
1512
|
}
|
|
621
1513
|
|
|
622
1514
|
// Handle /open <path>
|
|
623
|
-
if (
|
|
1515
|
+
if (/^\/open(?:\s|$)/.test(trimmed)) {
|
|
624
1516
|
const args = trimmed.slice(5).trim();
|
|
625
1517
|
if (!args) {
|
|
626
1518
|
console.log(`\n\x1b[31m${t.openUsagePrompt}\x1b[0m\n`);
|
|
@@ -639,10 +1531,11 @@ async function handleInput(input) {
|
|
|
639
1531
|
if (stats && stats.isDirectory()) {
|
|
640
1532
|
try {
|
|
641
1533
|
process.chdir(resolvedPath);
|
|
1534
|
+
workspaceRoot = process.cwd();
|
|
1535
|
+
workspaceReady = true;
|
|
642
1536
|
activeOpenFile = null; // Switch to directory mode clears active file focus
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
console.log(`\n\x1b[32m${t.openDirSuccess}${resolvedPath}\x1b[0m\n`);
|
|
1537
|
+
saveCurrentSession();
|
|
1538
|
+
redrawScreen(`\n\x1b[32m${t.openDirSuccess}${resolvedPath}\x1b[0m\n`);
|
|
646
1539
|
} catch (err) {
|
|
647
1540
|
console.log(`\n\x1b[31m${t.openPathError}${resolvedPath} (${err.message})\x1b[0m\n`);
|
|
648
1541
|
}
|
|
@@ -650,75 +1543,36 @@ async function handleInput(input) {
|
|
|
650
1543
|
return;
|
|
651
1544
|
}
|
|
652
1545
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
fs.writeFileSync(resolvedPath, '', 'utf8');
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
activeOpenFile = resolvedPath; // Track currently open file
|
|
662
|
-
|
|
663
|
-
// Read content
|
|
664
|
-
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
665
|
-
const lines = content.split('\n');
|
|
666
|
-
const activeEffort = getActiveEffort();
|
|
667
|
-
const preset = EFFORT_PRESETS[activeEffort] || EFFORT_PRESETS.high;
|
|
668
|
-
|
|
669
|
-
let contextContent = '';
|
|
670
|
-
if (activeEffort === 'ultracode') {
|
|
671
|
-
if (lines.length > 1000) {
|
|
672
|
-
const totalChunks = Math.ceil(lines.length / 2000);
|
|
673
|
-
const confirmMsg = `\n\x1b[35m[Ultracode]\x1b[0m File "${path.basename(resolvedPath)}" has ${lines.length} lines. Digesting it will take ${totalChunks} API calls. Proceed? (y/n): `;
|
|
674
|
-
const proceed = await confirmPrompt(confirmMsg);
|
|
675
|
-
|
|
676
|
-
if (proceed) {
|
|
677
|
-
const report = await runFileDigestionWorkflow(resolvedPath);
|
|
678
|
-
const relPath = path.relative(process.cwd(), resolvedPath);
|
|
679
|
-
contextContent = `[System Context: User opened file "${resolvedPath}" (${lines.length} lines). The file has been processed using the chunked digestion workflow. Below is the full digestion report. If you need to view raw code or text of specific lines, use the <<READ_FILE: ${relPath}:start-end>> tool (e.g., <<READ_FILE: ${relPath}:1200-1350>>).]\n\n${report}`;
|
|
680
|
-
} else {
|
|
681
|
-
const relPath = path.relative(process.cwd(), resolvedPath);
|
|
682
|
-
contextContent = `[System Context: User opened file "${resolvedPath}" (${lines.length} lines). Since the file is large, digestion was skipped by user. Use the <<READ_FILE: filePath:start-end>> tool (e.g. <<READ_FILE: ${relPath}:1-500>>) to read other segments of the file if needed.]`;
|
|
683
|
-
console.log(`\n\x1b[33m[Warning] Skipped digestion. AI will read file sections on demand.\x1b[0m`);
|
|
684
|
-
}
|
|
685
|
-
} else if (lines.length > preset.maxReadLines) {
|
|
686
|
-
const truncatedContent = lines.slice(0, 100).join('\n');
|
|
687
|
-
const relPath = path.relative(process.cwd(), resolvedPath);
|
|
688
|
-
contextContent = `[System Context: User opened file "${resolvedPath}" (${lines.length} lines). Since the file is large, only the first 100 lines are shown below. Use the <<READ_FILE: filePath:start-end>> tool (e.g. <<READ_FILE: ${relPath}:101-300>>) to read other segments of the file if needed.]\n\n${truncatedContent}`;
|
|
689
|
-
console.log(`\n\x1b[33m[Warning] File is large (${lines.length} lines). Truncated first 100 lines for AI context.\x1b[0m`);
|
|
690
|
-
} else {
|
|
691
|
-
contextContent = `[System Context: User opened file "${resolvedPath}" for editing and working. Current file contents:\n\n${content}]`;
|
|
692
|
-
}
|
|
1546
|
+
if (!workspaceReady) {
|
|
1547
|
+
if (stats && stats.isFile()) {
|
|
1548
|
+
workspaceRoot = path.dirname(resolvedPath);
|
|
1549
|
+
process.chdir(workspaceRoot);
|
|
1550
|
+
workspaceReady = true;
|
|
693
1551
|
} else {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
} else {
|
|
700
|
-
contextContent = `[System Context: User opened file "${resolvedPath}" for editing and working. Current file contents:\n\n${content}]`;
|
|
701
|
-
}
|
|
1552
|
+
console.log(currentLang === 'cn'
|
|
1553
|
+
? '\n\x1b[33m此会话尚未关联工作区,请先使用 /open <项目目录>。\x1b[0m\n'
|
|
1554
|
+
: '\n\x1b[33mThis session is not linked to a workspace. Use /open <project-directory> first.\x1b[0m\n');
|
|
1555
|
+
promptUser();
|
|
1556
|
+
return;
|
|
702
1557
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (!stats || !stats.isFile()) {
|
|
1561
|
+
console.log(`\n\x1b[31m${t.openPathError}${sanitizeUntrustedText(resolvedPath)}\x1b[0m\n`);
|
|
1562
|
+
promptUser();
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
709
1565
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1566
|
+
// Existing files can be focused, but /open never creates workspace content.
|
|
1567
|
+
try {
|
|
1568
|
+
const filePath = resolveWorkspacePath(args, workspaceRoot);
|
|
1569
|
+
activeOpenFile = filePath; // Track currently open file
|
|
1570
|
+
workspaceReady = true;
|
|
1571
|
+
saveCurrentSession();
|
|
714
1572
|
|
|
715
|
-
|
|
716
|
-
if (err && process.platform === 'win32') {
|
|
717
|
-
exec(`notepad "${resolvedPath}"`);
|
|
718
|
-
}
|
|
719
|
-
});
|
|
1573
|
+
launchEditor(filePath);
|
|
720
1574
|
|
|
721
|
-
console.log(`\n\x1b[32m${t.openFileSuccess}\n\x1b[90mPath: ${
|
|
1575
|
+
console.log(`\n\x1b[32m${t.openFileSuccess}\n\x1b[90mPath: ${filePath}\x1b[0m\n`);
|
|
722
1576
|
} catch (err) {
|
|
723
1577
|
console.log(`\n\x1b[31m${t.openPathError}${resolvedPath} (${err.message})\x1b[0m\n`);
|
|
724
1578
|
}
|
|
@@ -727,270 +1581,942 @@ async function handleInput(input) {
|
|
|
727
1581
|
return;
|
|
728
1582
|
}
|
|
729
1583
|
|
|
1584
|
+
let agentMode = 'chat';
|
|
1585
|
+
let agentPrompt = trimmed;
|
|
1586
|
+
let historyDisplay = trimmed;
|
|
1587
|
+
let executingPlan = null;
|
|
1588
|
+
let pendingPlan = null;
|
|
1589
|
+
let thunderPlanningRequest = null;
|
|
1590
|
+
|
|
1591
|
+
if (/^\/(?:plan|code)(?:\s|$)/.test(trimmed)) {
|
|
1592
|
+
if (!workspaceReady) {
|
|
1593
|
+
console.log(currentLang === 'cn'
|
|
1594
|
+
? '\n\x1b[33m请先使用 /open <项目目录> 关联工作区。\x1b[0m\n'
|
|
1595
|
+
: '\n\x1b[33mUse /open <project-directory> before planning or coding.\x1b[0m\n');
|
|
1596
|
+
promptUser();
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
const command = parseInputCommand(trimmed, { findPlan: name => findPlan(workspaceRoot, name) });
|
|
1600
|
+
if (command.type === 'error') {
|
|
1601
|
+
console.log(`\n\x1b[31m${sanitizeUntrustedText(command.error)}\x1b[0m\n`);
|
|
1602
|
+
promptUser();
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
if (command.type === 'code.usage') {
|
|
1606
|
+
console.log(currentLang === 'cn'
|
|
1607
|
+
? '\n用法: /code <需求或计划名>,或 /code --prompt <需求>\n'
|
|
1608
|
+
: '\nUsage: /code <request or plan name>, or /code --prompt <request>\n');
|
|
1609
|
+
promptUser();
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
if (command.type === 'plan.menu') {
|
|
1613
|
+
await handlePlanMenu();
|
|
1614
|
+
redrawScreen();
|
|
1615
|
+
promptUser();
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
if (command.type === 'plan.create') {
|
|
1619
|
+
const existing = findPlan(workspaceRoot, command.name);
|
|
1620
|
+
let replace = false;
|
|
1621
|
+
if (existing) {
|
|
1622
|
+
replace = await confirmPrompt(currentLang === 'cn'
|
|
1623
|
+
? `计划 "${sanitizeUntrustedText(existing.name)}" 已存在,替换它?(y/n): `
|
|
1624
|
+
: `Plan "${sanitizeUntrustedText(existing.name)}" exists. Replace it? (y/n): `);
|
|
1625
|
+
if (!replace) {
|
|
1626
|
+
promptUser();
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
if (workMode === 'thunder') {
|
|
1631
|
+
thunderPlanningRequest = { request: command.request, name: command.name, replace };
|
|
1632
|
+
}
|
|
1633
|
+
agentMode = 'plan';
|
|
1634
|
+
pendingPlan = { name: command.name, request: command.request, replace };
|
|
1635
|
+
agentPrompt = `Create the named implementation plan "${command.name}" for this request:\n${command.request}`;
|
|
1636
|
+
} else if (command.type === 'agent' && command.mode === 'code') {
|
|
1637
|
+
agentMode = 'code';
|
|
1638
|
+
executingPlan = command.plan;
|
|
1639
|
+
if (executingPlan?.workflowMode === 'thunder') workMode = 'thunder';
|
|
1640
|
+
agentPrompt = executingPlan
|
|
1641
|
+
? `Execute the saved plan "${executingPlan.name}". Re-read current files before changing them.\n\n${executingPlan.content}`
|
|
1642
|
+
: command.prompt;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
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
|
+
|
|
1661
|
+
if (!workspaceReady) {
|
|
1662
|
+
console.log(currentLang === 'cn'
|
|
1663
|
+
? '\n\x1b[33m当前历史会话没有关联项目目录。请先输入 /open <项目目录>,再继续修改。\x1b[0m\n'
|
|
1664
|
+
: '\n\x1b[33mThis history session is not linked to a project directory. Run /open <project-directory> before continuing.\x1b[0m\n');
|
|
1665
|
+
promptUser();
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
730
1669
|
// Check if active profile and API key are configured
|
|
731
1670
|
if (!hasApiKey()) {
|
|
732
1671
|
console.log(`\n${t.noKeyError}\n`);
|
|
1672
|
+
if (lastConfigError) console.log(`\x1b[31m${sanitizeUntrustedText(lastConfigError)}\x1b[0m\n`);
|
|
733
1673
|
promptUser();
|
|
734
1674
|
return;
|
|
735
1675
|
}
|
|
736
1676
|
|
|
737
|
-
|
|
738
|
-
|
|
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
|
+
}
|
|
739
1684
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1685
|
+
// Push user message to history
|
|
1686
|
+
currentLang = detectInputLanguage(historyDisplay || agentPrompt, currentLang);
|
|
1687
|
+
messages.push({ role: 'user', content: agentPrompt, displayContent: historyDisplay });
|
|
1688
|
+
saveCurrentSession();
|
|
1689
|
+
|
|
1690
|
+
if (executingPlan) {
|
|
1691
|
+
updatePlan(workspaceRoot, executingPlan.id, { status: 'running', lastRun: { startedAt: Date.now() } });
|
|
1692
|
+
redrawScreen(currentLang === 'cn'
|
|
1693
|
+
? `\x1b[90m正在执行计划: ${sanitizeUntrustedText(executingPlan.name)}\x1b[0m\n`
|
|
1694
|
+
: `\x1b[90mExecuting plan: ${sanitizeUntrustedText(executingPlan.name)}\x1b[0m\n`);
|
|
1695
|
+
}
|
|
743
1696
|
|
|
744
|
-
const
|
|
1697
|
+
const recalledMemories = workspaceReady ? retrieveMemories(workspaceRoot, agentPrompt, 8) : [];
|
|
1698
|
+
const recalledMemoryText = formatMemoriesForPrompt(recalledMemories);
|
|
1699
|
+
const turnTokenStart = {
|
|
1700
|
+
input: sessionTokenUsage.inputTokens,
|
|
1701
|
+
output: sessionTokenUsage.outputTokens
|
|
1702
|
+
};
|
|
1703
|
+
|
|
1704
|
+
const runtime = createRuntimeEvents();
|
|
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 });
|
|
1730
|
+
const unsubscribe = runtime.subscribe(event => renderer.handle(event));
|
|
1731
|
+
const abortController = new AbortController();
|
|
1732
|
+
let interrupted = false;
|
|
1733
|
+
const onTurnInterrupt = () => {
|
|
1734
|
+
interrupted = true;
|
|
1735
|
+
abortController.abort();
|
|
1736
|
+
};
|
|
1737
|
+
process.once('SIGINT', onTurnInterrupt);
|
|
745
1738
|
let loopCount = 0;
|
|
746
|
-
let
|
|
1739
|
+
let turnCompleted = false;
|
|
1740
|
+
let terminalEventSent = false;
|
|
1741
|
+
let compactionShown = false;
|
|
1742
|
+
let compactionSummary = '';
|
|
1743
|
+
let summarizedMessageCount = 0;
|
|
1744
|
+
let turnOutcome = 'stopped';
|
|
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
|
+
}
|
|
747
1781
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
activeReply = await getAIResponse(messages, activeOpenFile, preset.maxReadLines);
|
|
755
|
-
stopSpinner();
|
|
756
|
-
} catch (error) {
|
|
757
|
-
stopSpinner();
|
|
758
|
-
console.log(`\n\x1b[31mAPI Error:\x1b[0m ${error.message}\n`);
|
|
759
|
-
if (loopCount === 0) messages.pop();
|
|
760
|
-
break;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
// Check if the reply contains any tool tags
|
|
764
|
-
const match = activeReply.match(/<<([A-Z_]+):\s*([\s\S]*?)>>/);
|
|
765
|
-
if (!match) {
|
|
766
|
-
// Final response from Dave
|
|
767
|
-
let finalReply = activeReply;
|
|
768
|
-
const thinkMatch = finalReply.match(/<think>([\s\S]*?)<\/think>/);
|
|
769
|
-
if (thinkMatch) {
|
|
770
|
-
const thinkContent = thinkMatch[1].trim();
|
|
771
|
-
if (thinkContent) {
|
|
772
|
-
console.log(`\n\x1b[90m💭 Dave (Thinking):\n${thinkContent}\x1b[0m`);
|
|
773
|
-
}
|
|
774
|
-
finalReply = finalReply.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
775
|
-
}
|
|
776
|
-
if (finalReply === '') {
|
|
777
|
-
finalReply = '(Finished reasoning / 已思考完毕但未返回额外文本)';
|
|
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] });
|
|
778
1788
|
}
|
|
779
|
-
|
|
780
|
-
messages.push({ role: 'assistant', content: activeReply });
|
|
781
|
-
break;
|
|
1789
|
+
yield event;
|
|
782
1790
|
}
|
|
1791
|
+
}
|
|
783
1792
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
1793
|
+
runtime.emit('turn.started', {
|
|
1794
|
+
label: currentLang === 'cn' ? '准备项目工作流' : 'Preparing project workflow'
|
|
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
|
+
}
|
|
1805
|
+
|
|
1806
|
+
try {
|
|
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.
|
|
792
1902
|
}
|
|
793
|
-
thoughtText = thoughtText.replace(/<think>[\s\S]*?<\/think>/, '').trim();
|
|
794
1903
|
}
|
|
795
|
-
|
|
796
|
-
|
|
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]'));
|
|
1926
|
+
if (!compactionShown && contextWasCompacted) {
|
|
1927
|
+
runtime.emit('context.compacted', {
|
|
1928
|
+
omittedMessages: Math.max(1, messages.length - modelMessages.length + 1),
|
|
1929
|
+
keptMessages: modelMessages.length
|
|
1930
|
+
});
|
|
1931
|
+
compactionShown = true;
|
|
797
1932
|
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Save Dave's intermediate tool call message to history
|
|
801
|
-
messages.push({ role: 'assistant', content: activeReply });
|
|
802
1933
|
|
|
803
|
-
|
|
804
|
-
|
|
1934
|
+
let activeReply = '';
|
|
1935
|
+
const nativeToolCalls = [];
|
|
1936
|
+
let modelWasTruncated = false;
|
|
1937
|
+
const finishWithoutTools = forceBudgetFinish || forceNoProgressFinish;
|
|
1938
|
+
for await (const modelEvent of trackedModelRunner(modelMessages, {
|
|
1939
|
+
activeOpenFile,
|
|
1940
|
+
signal: abortController.signal,
|
|
1941
|
+
mode: agentMode,
|
|
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
|
+
}) : ''
|
|
1967
|
+
})) {
|
|
1968
|
+
if (modelEvent.type === 'model.delta') {
|
|
1969
|
+
activeReply += modelEvent.data.text || '';
|
|
1970
|
+
} else if (modelEvent.type === 'model.tool_call') {
|
|
1971
|
+
nativeToolCalls.push(modelEvent.data);
|
|
1972
|
+
} else if (modelEvent.type === 'model.completed') {
|
|
1973
|
+
modelWasTruncated = modelEvent.data.truncated === true;
|
|
1974
|
+
runtime.emit(modelEvent.type, modelEvent.data);
|
|
1975
|
+
} else if (modelEvent.type === 'model.started') {
|
|
1976
|
+
runtime.emit('model.started', {
|
|
1977
|
+
...modelEvent.data,
|
|
1978
|
+
label: loopCount === 0
|
|
1979
|
+
? (currentLang === 'cn' ? '思考中 · 正在理解问题' : 'Thinking · understanding the request')
|
|
1980
|
+
: (currentLang === 'cn' ? '思考中 · 正在检查工具结果' : 'Thinking · checking tool results')
|
|
1981
|
+
});
|
|
1982
|
+
} else {
|
|
1983
|
+
runtime.emit(modelEvent.type, modelEvent.data);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
805
1986
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
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
|
+
});
|
|
832
2019
|
}
|
|
2020
|
+
}
|
|
833
2021
|
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
2022
|
+
if (modelWasTruncated) {
|
|
2023
|
+
runtime.emit('model.retry', {
|
|
2024
|
+
reason: 'output-truncated',
|
|
2025
|
+
label: currentLang === 'cn' ? '模型输出达到上限,正在要求其缩短并继续' : 'Model output was truncated; requesting a concise continuation'
|
|
2026
|
+
});
|
|
2027
|
+
messages.push({
|
|
2028
|
+
role: 'user',
|
|
2029
|
+
content: '[System notice: The previous model response reached its output limit and was not accepted. Continue concisely. If a tool is needed, request exactly one structured tool.]'
|
|
2030
|
+
});
|
|
2031
|
+
saveCurrentSession();
|
|
2032
|
+
loopCount++;
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
const activeProfile = getActiveProfile();
|
|
2037
|
+
let parsedTool = null;
|
|
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
|
+
})) };
|
|
2050
|
+
} else if (nativeToolCalls.length === 1) {
|
|
2051
|
+
const call = nativeToolCalls[0];
|
|
2052
|
+
parsedTool = SUPPORTED_TOOLS.has(call.name)
|
|
2053
|
+
? { toolName: call.name, toolArg: call.arguments || {}, native: true, id: call.id }
|
|
2054
|
+
: { error: `Unsupported structured tool: ${call.name}` };
|
|
2055
|
+
} else if ((activeProfile?.toolMode || 'native') === 'legacy') {
|
|
2056
|
+
parsedTool = parseToolCall(activeReply);
|
|
2057
|
+
if (parsedTool && !parsedTool.error && typeof parsedTool.toolArg === 'string' && parsedTool.toolArg.trim().startsWith('{')) {
|
|
2058
|
+
try {
|
|
2059
|
+
parsedTool.toolArg = JSON.parse(parsedTool.toolArg);
|
|
2060
|
+
} catch {
|
|
2061
|
+
parsedTool = { error: 'Legacy tool arguments must be valid JSON.' };
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
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
|
+
}
|
|
2074
|
+
let finalReply = sanitizeUntrustedText(stripReasoningBlocks(activeReply));
|
|
2075
|
+
if (!finalReply) {
|
|
2076
|
+
finalReply = currentLang === 'cn' ? '模型未返回可显示内容。' : 'The model returned no displayable content.';
|
|
2077
|
+
}
|
|
2078
|
+
if (pendingPlan) {
|
|
2079
|
+
const missingSections = validatePlanContent(finalReply);
|
|
2080
|
+
if (missingSections.length > 0) {
|
|
2081
|
+
planValidationAttempts++;
|
|
2082
|
+
if (planValidationAttempts >= 3) {
|
|
2083
|
+
throw new Error(`Plan validation failed: missing ${missingSections.join(', ')}.`);
|
|
878
2084
|
}
|
|
2085
|
+
messages.push({ role: 'assistant', content: finalReply });
|
|
2086
|
+
messages.push({ role: 'user', content: `[Plan validation failed. Rewrite the complete plan. Missing: ${missingSections.join(', ')}. Every implementation step must state WHAT changes, HOW it is implemented, affected files/interfaces, and validation.]` });
|
|
2087
|
+
saveCurrentSession();
|
|
2088
|
+
loopCount++;
|
|
2089
|
+
continue;
|
|
879
2090
|
}
|
|
880
2091
|
}
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
2092
|
+
runtime.emit('phase.changed', { phase: 'act', label: currentLang === 'cn' ? 'ACT · 整理结果' : 'ACT · organizing result' });
|
|
2093
|
+
runtime.emit('model.delta', { text: finalReply });
|
|
2094
|
+
messages.push({ role: 'assistant', content: finalReply });
|
|
2095
|
+
saveCurrentSession();
|
|
2096
|
+
if (pendingPlan) {
|
|
2097
|
+
upsertPlan(workspaceRoot, {
|
|
2098
|
+
name: pendingPlan.name,
|
|
2099
|
+
request: pendingPlan.request,
|
|
2100
|
+
content: finalReply
|
|
2101
|
+
}, { replace: pendingPlan.replace });
|
|
2102
|
+
}
|
|
2103
|
+
turnCompleted = true;
|
|
2104
|
+
|
|
2105
|
+
if (currentSessionTitle === 'New Chat' || currentSessionTitle === '新会话' || currentSessionTitle === 'New Session') {
|
|
2106
|
+
const titleSessionId = currentSessionId;
|
|
2107
|
+
const titleMessages = prepareModelMessages(messages.map(message => ({ ...message })), currentContextPlan);
|
|
2108
|
+
generateTitle(titleMessages).then(newTitle => {
|
|
2109
|
+
if (currentSessionId === titleSessionId && newTitle && newTitle !== 'New Session') {
|
|
2110
|
+
currentSessionTitle = sanitizeUntrustedText(newTitle).slice(0, 80);
|
|
2111
|
+
saveCurrentSession();
|
|
2112
|
+
}
|
|
2113
|
+
}).catch(() => {});
|
|
2114
|
+
}
|
|
2115
|
+
break;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
if (parsedTool.error) {
|
|
2119
|
+
const requestedSeveralTools = /multiple tools at once|exactly one .*invocation/i.test(parsedTool.error);
|
|
2120
|
+
runtime.emit('tool.failed', {
|
|
2121
|
+
tool: 'TOOL_PARSE',
|
|
2122
|
+
displaySummary: requestedSeveralTools
|
|
2123
|
+
? (currentLang === 'cn' ? '模型一次请求了多个工具,已要求逐个调用' : 'The model requested several tools; retrying one at a time')
|
|
2124
|
+
: (currentLang === 'cn' ? '工具格式无法解析' : 'Could not parse tool call'),
|
|
2125
|
+
error: parsedTool.error
|
|
2126
|
+
});
|
|
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
|
+
});
|
|
2133
|
+
saveCurrentSession();
|
|
2134
|
+
loopCount++;
|
|
2135
|
+
continue;
|
|
2136
|
+
}
|
|
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`);
|
|
935
2181
|
}
|
|
2182
|
+
return await (request.allowSession ? permissionPrompt : confirmPrompt)(`\x1b[33m${sanitizeUntrustedText(request.prompt)}\x1b[0m`);
|
|
2183
|
+
} finally {
|
|
2184
|
+
renderer.resume();
|
|
936
2185
|
}
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
|
|
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
|
+
});
|
|
940
2220
|
}
|
|
2221
|
+
messages.push({ role: 'tool', content: toolResult, toolCallId: call.id, toolName: call.toolName });
|
|
941
2222
|
}
|
|
2223
|
+
saveCurrentSession();
|
|
2224
|
+
if (interrupted) throw new Error('Request cancelled.');
|
|
2225
|
+
loopCount += calls.length;
|
|
2226
|
+
continue;
|
|
2227
|
+
}
|
|
942
2228
|
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
2229
|
+
const toolName = parsedTool.toolName;
|
|
2230
|
+
const toolArg = parsedTool.toolArg;
|
|
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.'}]`
|
|
953
2253
|
});
|
|
954
|
-
toolResult = outputText;
|
|
955
2254
|
} else {
|
|
956
|
-
|
|
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
|
+
});
|
|
957
2266
|
}
|
|
958
|
-
|
|
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
|
+
}
|
|
2278
|
+
runtime.emit('tool.requested', {
|
|
2279
|
+
tool: toolName,
|
|
2280
|
+
toolCallId,
|
|
2281
|
+
displaySummary: describeToolRequest(toolName, toolArg, currentLang)
|
|
2282
|
+
});
|
|
2283
|
+
|
|
2284
|
+
messages.push({
|
|
2285
|
+
role: 'assistant',
|
|
2286
|
+
content: '',
|
|
2287
|
+
toolCall: { id: toolCallId, name: toolName, arguments: toolArg }
|
|
2288
|
+
});
|
|
2289
|
+
saveCurrentSession();
|
|
2290
|
+
|
|
2291
|
+
const toolResponse = await executeToolCall({
|
|
2292
|
+
toolName,
|
|
2293
|
+
toolArg,
|
|
2294
|
+
toolCallId,
|
|
2295
|
+
workspaceRoot,
|
|
2296
|
+
emit: runtime.emit,
|
|
2297
|
+
requestPermission: async request => {
|
|
2298
|
+
renderer.pause();
|
|
2299
|
+
try {
|
|
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`);
|
|
2306
|
+
} finally {
|
|
2307
|
+
renderer.resume();
|
|
2308
|
+
}
|
|
2309
|
+
},
|
|
2310
|
+
lang: currentLang,
|
|
2311
|
+
mode: agentMode,
|
|
2312
|
+
signal: abortController.signal,
|
|
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
|
|
2325
|
+
});
|
|
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++;
|
|
959
2340
|
else {
|
|
960
|
-
|
|
2341
|
+
repeatedToolCount = 1;
|
|
2342
|
+
blockedRepeatedToolSignature = '';
|
|
961
2343
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
2344
|
+
repeatedToolSignature = signature;
|
|
2345
|
+
repeatedToolResultHash = resultHash;
|
|
2346
|
+
if (toolResponse.cancelled) planActionCancelled = true;
|
|
2347
|
+
messages.push({ role: 'tool', content: String(toolResult || ''), toolCallId, toolName });
|
|
2348
|
+
saveCurrentSession();
|
|
2349
|
+
if (interrupted) throw new Error('Request cancelled.');
|
|
2350
|
+
loopCount++;
|
|
965
2351
|
}
|
|
966
2352
|
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
2353
|
+
if (turnCompleted) {
|
|
2354
|
+
runtime.emit('phase.changed', { phase: 'verify', label: currentLang === 'cn' ? 'VERIFY · 完成检查' : 'VERIFY · final checks' });
|
|
2355
|
+
runtime.emit('turn.completed', { steps: loopCount });
|
|
2356
|
+
terminalEventSent = true;
|
|
2357
|
+
turnOutcome = planActionCancelled ? 'cancelled' : 'completed';
|
|
2358
|
+
}
|
|
2359
|
+
} catch (error) {
|
|
2360
|
+
if (readPlanCancelled || budgetCancelled || interrupted || abortController.signal.aborted) {
|
|
2361
|
+
runtime.emit('turn.cancelled', {
|
|
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')
|
|
2367
|
+
});
|
|
2368
|
+
terminalEventSent = true;
|
|
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
|
+
}
|
|
2377
|
+
} else {
|
|
2378
|
+
if (loopCount === 0) {
|
|
2379
|
+
messages.pop();
|
|
2380
|
+
saveCurrentSession();
|
|
2381
|
+
}
|
|
2382
|
+
runtime.emit('turn.failed', {
|
|
2383
|
+
error: currentLang === 'cn' ? `请求失败:${error.message}` : `Request failed: ${error.message}`
|
|
2384
|
+
});
|
|
2385
|
+
terminalEventSent = true;
|
|
2386
|
+
turnOutcome = 'blocked';
|
|
2387
|
+
}
|
|
2388
|
+
} finally {
|
|
2389
|
+
process.removeListener('SIGINT', onTurnInterrupt);
|
|
2390
|
+
if (!terminalEventSent) runtime.emit('turn.cancelled', { reason: currentLang === 'cn' ? '本轮已停止' : 'Turn stopped' });
|
|
2391
|
+
renderer.dispose();
|
|
2392
|
+
unsubscribe();
|
|
2393
|
+
runtime.close();
|
|
2394
|
+
}
|
|
972
2395
|
|
|
973
|
-
|
|
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
|
+
}
|
|
974
2476
|
|
|
975
|
-
|
|
2477
|
+
if (executingPlan) {
|
|
2478
|
+
updatePlan(workspaceRoot, executingPlan.id, {
|
|
2479
|
+
status: turnOutcome === 'completed' ? 'completed' : turnOutcome === 'cancelled' ? 'ready' : 'blocked',
|
|
2480
|
+
lastRun: {
|
|
2481
|
+
...(executingPlan.lastRun || {}),
|
|
2482
|
+
finishedAt: Date.now(),
|
|
2483
|
+
outcome: turnOutcome,
|
|
2484
|
+
reviews: thunderReviews
|
|
2485
|
+
},
|
|
2486
|
+
...(thunderTeam ? { teamSnapshot: getThunderTeam(workspaceRoot, thunderTeam.id) || thunderTeam } : {})
|
|
2487
|
+
});
|
|
976
2488
|
}
|
|
977
2489
|
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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(() => {});
|
|
981
2497
|
}
|
|
982
2498
|
|
|
2499
|
+
sessionTokenUsage.lastInputTokens = Math.max(0, sessionTokenUsage.inputTokens - turnTokenStart.input);
|
|
2500
|
+
sessionTokenUsage.lastOutputTokens = Math.max(0, sessionTokenUsage.outputTokens - turnTokenStart.output);
|
|
2501
|
+
sessionTokenUsage.lastTotalTokens = sessionTokenUsage.lastInputTokens + sessionTokenUsage.lastOutputTokens;
|
|
2502
|
+
sessionTokenUsage.lastPhaseUsage = {
|
|
2503
|
+
...phaseUsage,
|
|
2504
|
+
approvedPostScanBudget: Number(currentContextPlan?.turnBudgetTokens) || 0
|
|
2505
|
+
};
|
|
2506
|
+
|
|
983
2507
|
if (sessionTokenUsage.lastTotalTokens > 0) {
|
|
984
|
-
console.log(
|
|
985
|
-
`\x1b[
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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`);
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
if (pendingPlan || executingPlan) {
|
|
2514
|
+
redrawScreen(currentLang === 'cn'
|
|
2515
|
+
? '\x1b[90m计划状态已更新。\x1b[0m\n'
|
|
2516
|
+
: '\x1b[90mPlan status updated.\x1b[0m\n');
|
|
992
2517
|
}
|
|
993
2518
|
|
|
2519
|
+
await closeWorkspaceShell(workspaceRoot);
|
|
994
2520
|
promptUser();
|
|
995
2521
|
}
|
|
996
2522
|
|
|
@@ -1000,12 +2526,16 @@ async function startApp() {
|
|
|
1000
2526
|
clearConsole();
|
|
1001
2527
|
await runWelcomeAnimation();
|
|
1002
2528
|
setInitialized();
|
|
1003
|
-
|
|
1004
|
-
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
2529
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1005
2530
|
}
|
|
2531
|
+
initSession();
|
|
1006
2532
|
clearConsole();
|
|
2533
|
+
// The octopus workspace header remains part of every normal startup.
|
|
1007
2534
|
drawHeader();
|
|
1008
2535
|
promptUser();
|
|
1009
2536
|
}
|
|
1010
2537
|
|
|
1011
|
-
startApp().catch(
|
|
2538
|
+
startApp().catch(error => {
|
|
2539
|
+
process.stdout.write('\x1b[?25h');
|
|
2540
|
+
console.error(error);
|
|
2541
|
+
});
|