mocode-ai 0.5.5 → 0.5.6
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/dist/agent/core.js +5 -4
- package/dist/agent/index.js +16 -3
- package/dist/agent/spawn.js +7 -2
- package/dist/project-skill/initializer.js +83 -83
- package/dist/project-snapshot/llm-snapshot.js +69 -69
- package/dist/repl/index.js +1 -1
- package/dist/session/compact.js +17 -16
- package/dist/session/index.js +1 -1
- package/dist/session/scheduler.js +6 -6
- package/dist/ui/content.js +16 -0
- package/dist/ui/layout.js +26 -6
- package/dist/ui/markdown.js +4 -3
- package/package.json +1 -1
package/dist/agent/core.js
CHANGED
|
@@ -138,6 +138,7 @@ function pushToolResult(history, tc, output, pruner, lifecycle, scheduler) {
|
|
|
138
138
|
*/
|
|
139
139
|
export async function runAgentCore(opts) {
|
|
140
140
|
const { history, userInput, signal, onContextUpdate, hooks, skipRollback } = opts;
|
|
141
|
+
const runtimeContextState = opts.contextState ?? contextState;
|
|
141
142
|
const maxSteps = opts.maxSteps ?? config.maxSteps;
|
|
142
143
|
// 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
|
|
143
144
|
// 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
|
|
@@ -187,7 +188,7 @@ export async function runAgentCore(opts) {
|
|
|
187
188
|
// 预算调度器:每个 runAgentCore 实例一个,步前 evaluateBudget + scheduleActions。
|
|
188
189
|
// 决策按 ROI 分发(cold tools 优先 / history 摘要最后);contextBudget 开关关闭时为 null。
|
|
189
190
|
const scheduler = config.contextBudget !== false
|
|
190
|
-
? createBudgetScheduler() // 在 step 循环之外实例化一次,跨步持有 lastRunLog
|
|
191
|
+
? createBudgetScheduler(runtimeContextState) // 在 step 循环之外实例化一次,跨步持有 lastRunLog
|
|
191
192
|
: null;
|
|
192
193
|
// 本轮流式状态:首个正文 token 到达即停 spinner(思考期间 spinner 持续转「思考中…」,不写思考内容)。
|
|
193
194
|
let mode = 'idle';
|
|
@@ -235,7 +236,7 @@ export async function runAgentCore(opts) {
|
|
|
235
236
|
await scheduler.runStep(history, step);
|
|
236
237
|
}
|
|
237
238
|
else {
|
|
238
|
-
await maybeCompact(history);
|
|
239
|
+
await maybeCompact(history, undefined, undefined, runtimeContextState);
|
|
239
240
|
}
|
|
240
241
|
hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
|
|
241
242
|
mode = 'idle';
|
|
@@ -260,7 +261,7 @@ export async function runAgentCore(opts) {
|
|
|
260
261
|
}
|
|
261
262
|
throw e;
|
|
262
263
|
}
|
|
263
|
-
|
|
264
|
+
runtimeContextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
|
|
264
265
|
addUsage(result.usage); // 本轮累计:onDone 摘要行 + AgentRunResult.usage 透传
|
|
265
266
|
// 校正系数:API 实测 prompt_tokens / 估算 token。
|
|
266
267
|
// 每次 chat 响应后刷新,让下次 evaluateBudget 用更接近真实的 actual。
|
|
@@ -269,7 +270,7 @@ export async function runAgentCore(opts) {
|
|
|
269
270
|
const estimated = estimateMessagesTokens(history) + estimateToolSchemaTokens();
|
|
270
271
|
if (estimated > 100) {
|
|
271
272
|
const raw = result.usage.promptTokens / estimated;
|
|
272
|
-
|
|
273
|
+
runtimeContextState.correction = Math.max(0.5, Math.min(2.0, raw));
|
|
273
274
|
}
|
|
274
275
|
}
|
|
275
276
|
hooks.onChatDone?.(); // 主 agent:spinner.stop()
|
package/dist/agent/index.js
CHANGED
|
@@ -66,8 +66,11 @@ function flushToolBatch(expandSingleEntry = false) {
|
|
|
66
66
|
batch.endBatch(id, layout);
|
|
67
67
|
if (expandSingleEntry)
|
|
68
68
|
batch.expandSingleEntryFully(id, layout);
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// 普通摘要只有一个“当前空行”,再 break 一次把它提交为分隔空行。
|
|
70
|
+
// mutation 自动展开时 content.insertAfter 已先把该当前空行提交到 rows;若这里仍补 \n,
|
|
71
|
+
// diff 后就会固定出现两条空白行。
|
|
72
|
+
if (!expandSingleEntry)
|
|
73
|
+
layout.contentWrite('\n');
|
|
71
74
|
}
|
|
72
75
|
/**
|
|
73
76
|
* agent 核心循环(主 agent,TUI 渲染版):
|
|
@@ -104,6 +107,7 @@ onContextUpdate) {
|
|
|
104
107
|
// 不能按普通字符串的“两个换行才有一个空行”来计算。
|
|
105
108
|
let textBoundaryNewlines = 0;
|
|
106
109
|
let hasPendingTextBoundary = false;
|
|
110
|
+
let toolBatchFollowsText = false;
|
|
107
111
|
const hooks = {
|
|
108
112
|
onText: (s) => {
|
|
109
113
|
// 纯空白 chunk 在视觉上不是正文:既不切 batch,也不写入 markdown 缓冲。
|
|
@@ -130,6 +134,7 @@ onContextUpdate) {
|
|
|
130
134
|
// 文本/思考已流完,模型转而生成 tool_call 参数(可能很长,如 write_file 整篇内容):
|
|
131
135
|
// 补换行(让随后的 ● 行与 diff 不黏在正文末尾)+ 启「生成中」内联 spinner,内容区不再干等。
|
|
132
136
|
if (hasPendingTextBoundary) {
|
|
137
|
+
toolBatchFollowsText = true;
|
|
133
138
|
if (textBoundaryNewlines < 1) {
|
|
134
139
|
layout.contentWrite('\n');
|
|
135
140
|
}
|
|
@@ -150,7 +155,15 @@ onContextUpdate) {
|
|
|
150
155
|
textBoundaryNewlines = 1;
|
|
151
156
|
}
|
|
152
157
|
},
|
|
153
|
-
onToolHeader: (tc) =>
|
|
158
|
+
onToolHeader: (tc) => {
|
|
159
|
+
// mutation 的自动展开会把 current/committed 空行状态互相转换;在首摘要真正
|
|
160
|
+
// 落屏前按视觉行归一,避免同样的文本→edit 边界偶发 1 行或 2 行。
|
|
161
|
+
if (toolBatchFollowsText && isMutationTool(tc.name)) {
|
|
162
|
+
layout.normalizeMutationBoundary();
|
|
163
|
+
}
|
|
164
|
+
toolBatchFollowsText = false;
|
|
165
|
+
writeToolHeader(tc);
|
|
166
|
+
},
|
|
154
167
|
onToolStart: (name) => spinner.start(`执行 ${name}`),
|
|
155
168
|
onToolDone: () => spinner.stop(),
|
|
156
169
|
onToolResult: (tc, output, parsed, preWriteOld, editStartLine) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine),
|
package/dist/agent/spawn.js
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
// - 独立 history:不共享主对话,避免子任务的工具噪声污染主上下文。
|
|
8
8
|
// - 系统提示复用主 agent 组装链(config.systemPrompt + memory 段 + skills 段)+ 子 agent 角色后缀。
|
|
9
9
|
// - 工具子集:按白名单从 chatTools 过滤;无白名单 = 全量(但 task 工具调用方通常会限定只读)。
|
|
10
|
-
// - 不调 beginTurn(不进主回滚链)
|
|
11
|
-
//
|
|
10
|
+
// - 不调 beginTurn(不进主回滚链);skipRollback=true 使文件 mutation 跳过 recordMutation,
|
|
11
|
+
// 子 agent 的改动不进主回滚快照链(靠 git 兜底,见下方 skipRollback 注释)。
|
|
12
12
|
// - 步数上限默认更低(config.subAgentMaxSteps ?? 50),防子任务失控耗尽配额。
|
|
13
13
|
// - 中断透传:opts.signal(主 agent 的 abort signal)透传给 runAgentCore → chat/executeTool,
|
|
14
14
|
// 主 Ctrl+C 树杀子 agent(chat 流式 abort + run_command/web_fetch 即时取消)。
|
|
@@ -21,6 +21,7 @@ import { buildMemorySection, buildMemoryIndexSection } from '../memory/index.js'
|
|
|
21
21
|
import { ui } from '../ui/theme.js';
|
|
22
22
|
import { runAgentCore } from './core.js';
|
|
23
23
|
import { summarizeToolCall, summarizeToolResult, truncateDisplay } from '../ui/render.js';
|
|
24
|
+
import { createContextState } from '../session/compact.js';
|
|
24
25
|
/** 子 agent 系统提示后缀:角色与约束。 */
|
|
25
26
|
const SUBAGENT_SUFFIX = `
|
|
26
27
|
|
|
@@ -115,6 +116,9 @@ export async function spawnAgent(opts) {
|
|
|
115
116
|
// onStepStart / onChatDone / onToolStart / onToolDone / onAbort:子 agent 静默,无需 spinner / 中断渲染。
|
|
116
117
|
// abort 还原(history 还原 + 模式还原)由 core 的 abortRestore 处理,hooks 只管展示。
|
|
117
118
|
};
|
|
119
|
+
// 每个子 agent 独享统计/预算状态。不能保存再恢复模块级单例:多个 task 并发时
|
|
120
|
+
// save/restore 会竞态,且 lastEstimate / schedulerLog 仍会污染主 agent。
|
|
121
|
+
const localContextState = createContextState();
|
|
118
122
|
const result = await runAgentCore({
|
|
119
123
|
history,
|
|
120
124
|
userInput: opts.prompt,
|
|
@@ -123,6 +127,7 @@ export async function spawnAgent(opts) {
|
|
|
123
127
|
maxSteps,
|
|
124
128
|
toolsOverride,
|
|
125
129
|
skipRollback: true, // 逻辑隔离:子 agent 文件改动不进主回滚快照链,主 /rollback 不撤销(靠 git 兜底)
|
|
130
|
+
contextState: localContextState,
|
|
126
131
|
});
|
|
127
132
|
return {
|
|
128
133
|
summary: result.finalText,
|
|
@@ -5,63 +5,63 @@ import { spawnAgent } from '../agent/spawn.js';
|
|
|
5
5
|
/**
|
|
6
6
|
* 子 agent 的系统提示后缀:角色与输出格式约束
|
|
7
7
|
*/
|
|
8
|
-
const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise "Project Skill" document.
|
|
9
|
-
|
|
10
|
-
## ⚠️ Complementary to Snapshot — CRITICAL
|
|
11
|
-
The following information is ALREADY provided by Project Snapshot — **do NOT repeat**:
|
|
12
|
-
- ✗ Project one-liner description (快照已有)
|
|
13
|
-
- ✗ Module names and file locations (快照 srcTree 已有)
|
|
14
|
-
- ✗ Tech stack names and versions (快照 techStack 已有)
|
|
15
|
-
- ✗ Build/test commands (快照 keyCommands 已有)
|
|
16
|
-
- ✗ File lists and directory structure (快照 srcTree 已有)
|
|
17
|
-
|
|
18
|
-
**Your job is to capture INSIGHTS only:**
|
|
19
|
-
- **WHY** — design decisions, trade-offs, reasons for choosing X over Y
|
|
20
|
-
- **HOW** — module behaviors, data flows, call chains, non-obvious interactions
|
|
21
|
-
- **GOTCHAS** — pitfalls, edge cases, non-intuitive behaviors
|
|
22
|
-
- **CONVENTIONS** — naming patterns, code style, unwritten rules
|
|
23
|
-
|
|
24
|
-
## Exploration Strategy
|
|
25
|
-
1. Use \`codegraph\` to trace call chains and understand module interactions
|
|
26
|
-
2. Read core modules to understand **behaviors** (not just names)
|
|
27
|
-
3. Look for complex logic, error handling patterns, unusual configs
|
|
28
|
-
4. Identify naming conventions, common abstractions
|
|
29
|
-
|
|
30
|
-
## Output Format
|
|
31
|
-
Output the skill document between these exact delimiters:
|
|
32
|
-
|
|
33
|
-
\`\`\`skill-start
|
|
34
|
-
## 架构要点
|
|
35
|
-
### 核心模块及职责
|
|
36
|
-
- **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
|
|
37
|
-
...
|
|
38
|
-
|
|
39
|
-
### 关键设计决策
|
|
40
|
-
- 决策:原因
|
|
41
|
-
...
|
|
42
|
-
|
|
43
|
-
## 项目约定
|
|
44
|
-
- 约定(具体的,有例子的)
|
|
45
|
-
...
|
|
46
|
-
|
|
47
|
-
## 开发流程
|
|
48
|
-
- 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
|
|
49
|
-
...
|
|
50
|
-
|
|
51
|
-
## 常见坑点
|
|
52
|
-
- 坑:解法
|
|
53
|
-
...
|
|
54
|
-
\`\`\`skill-end
|
|
55
|
-
|
|
56
|
-
## Rules — MUST FOLLOW
|
|
57
|
-
1. 禁止列出文件名清单或目录结构
|
|
58
|
-
2. 禁止列出依赖名和版本
|
|
59
|
-
3. 禁止写项目概述/一句话描述
|
|
60
|
-
4. 禁止只写命令本身(快照已有),只写注意事项
|
|
61
|
-
5. 每个模块描述必须包含行为(做什么 + 怎么协作),不只是名称
|
|
62
|
-
6. **总长度目标 3000-4000 字符,硬上限 4000**
|
|
63
|
-
7. 写可操作的内容,用具体路径和例子
|
|
64
|
-
8. 如果某个 section 没有实质内容,省略它
|
|
8
|
+
const SKILL_INIT_SUFFIX = `You are a project exploration agent. Your task is to deeply understand this project and generate a concise "Project Skill" document.
|
|
9
|
+
|
|
10
|
+
## ⚠️ Complementary to Snapshot — CRITICAL
|
|
11
|
+
The following information is ALREADY provided by Project Snapshot — **do NOT repeat**:
|
|
12
|
+
- ✗ Project one-liner description (快照已有)
|
|
13
|
+
- ✗ Module names and file locations (快照 srcTree 已有)
|
|
14
|
+
- ✗ Tech stack names and versions (快照 techStack 已有)
|
|
15
|
+
- ✗ Build/test commands (快照 keyCommands 已有)
|
|
16
|
+
- ✗ File lists and directory structure (快照 srcTree 已有)
|
|
17
|
+
|
|
18
|
+
**Your job is to capture INSIGHTS only:**
|
|
19
|
+
- **WHY** — design decisions, trade-offs, reasons for choosing X over Y
|
|
20
|
+
- **HOW** — module behaviors, data flows, call chains, non-obvious interactions
|
|
21
|
+
- **GOTCHAS** — pitfalls, edge cases, non-intuitive behaviors
|
|
22
|
+
- **CONVENTIONS** — naming patterns, code style, unwritten rules
|
|
23
|
+
|
|
24
|
+
## Exploration Strategy
|
|
25
|
+
1. Use \`codegraph\` to trace call chains and understand module interactions
|
|
26
|
+
2. Read core modules to understand **behaviors** (not just names)
|
|
27
|
+
3. Look for complex logic, error handling patterns, unusual configs
|
|
28
|
+
4. Identify naming conventions, common abstractions
|
|
29
|
+
|
|
30
|
+
## Output Format
|
|
31
|
+
Output the skill document between these exact delimiters:
|
|
32
|
+
|
|
33
|
+
\`\`\`skill-start
|
|
34
|
+
## 架构要点
|
|
35
|
+
### 核心模块及职责
|
|
36
|
+
- **\`path/to/module\`** — 行为描述(做什么 + 怎么做)
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
### 关键设计决策
|
|
40
|
+
- 决策:原因
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
## 项目约定
|
|
44
|
+
- 约定(具体的,有例子的)
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
## 开发流程
|
|
48
|
+
- 命令 + 注意事项/坑点(命令本身快照已有,这里只写注意什么)
|
|
49
|
+
...
|
|
50
|
+
|
|
51
|
+
## 常见坑点
|
|
52
|
+
- 坑:解法
|
|
53
|
+
...
|
|
54
|
+
\`\`\`skill-end
|
|
55
|
+
|
|
56
|
+
## Rules — MUST FOLLOW
|
|
57
|
+
1. 禁止列出文件名清单或目录结构
|
|
58
|
+
2. 禁止列出依赖名和版本
|
|
59
|
+
3. 禁止写项目概述/一句话描述
|
|
60
|
+
4. 禁止只写命令本身(快照已有),只写注意事项
|
|
61
|
+
5. 每个模块描述必须包含行为(做什么 + 怎么协作),不只是名称
|
|
62
|
+
6. **总长度目标 3000-4000 字符,硬上限 4000**
|
|
63
|
+
7. 写可操作的内容,用具体路径和例子
|
|
64
|
+
8. 如果某个 section 没有实质内容,省略它
|
|
65
65
|
`;
|
|
66
66
|
/**
|
|
67
67
|
* 生成初始项目 Skill(使用子 agent 深度探索)
|
|
@@ -72,35 +72,35 @@ export async function generateInitialSkill(existingSkill, signal) {
|
|
|
72
72
|
const hasExisting = !!existingSkill;
|
|
73
73
|
let prompt;
|
|
74
74
|
if (hasExisting) {
|
|
75
|
-
prompt = `以下是当前的项目 Skill 内容,请深度探索项目并优化/完善它:
|
|
76
|
-
|
|
77
|
-
\`\`\`markdown
|
|
78
|
-
${existingSkill}
|
|
79
|
-
\`\`\`
|
|
80
|
-
|
|
81
|
-
## 优化方向
|
|
82
|
-
1. 补充缺失的信息(架构要点、设计决策、调用链等)
|
|
83
|
-
2. 修正过时或不准确的内容
|
|
84
|
-
3. 让描述更具体(用实际路径和例子)
|
|
85
|
-
4. 删除冗余或空泛的描述
|
|
86
|
-
5. 如果发现新的坑点或约定,添加进去
|
|
87
|
-
|
|
75
|
+
prompt = `以下是当前的项目 Skill 内容,请深度探索项目并优化/完善它:
|
|
76
|
+
|
|
77
|
+
\`\`\`markdown
|
|
78
|
+
${existingSkill}
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
## 优化方向
|
|
82
|
+
1. 补充缺失的信息(架构要点、设计决策、调用链等)
|
|
83
|
+
2. 修正过时或不准确的内容
|
|
84
|
+
3. 让描述更具体(用实际路径和例子)
|
|
85
|
+
4. 删除冗余或空泛的描述
|
|
86
|
+
5. 如果发现新的坑点或约定,添加进去
|
|
87
|
+
|
|
88
88
|
记住:保持精简,总长度控制在 4000-5000 字符。输出完整的优化后版本。`;
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
|
-
prompt = `请深度探索当前项目,生成一份项目专属 Skill 文档。
|
|
92
|
-
|
|
93
|
-
## 探索步骤
|
|
94
|
-
1. 先用 glob 扫描项目结构(**/*.ts, **/*.json 等)
|
|
95
|
-
2. 读取 package.json、README.md、tsconfig.json 等关键文件
|
|
96
|
-
3. 用 codegraph 或 grep 找到主入口、核心模块、关键接口
|
|
97
|
-
4. 理解架构模式、数据流向、调用关系
|
|
98
|
-
5. 识别命名约定、代码风格、测试策略
|
|
99
|
-
6. 找出可能的坑点(复杂配置、特殊依赖、构建陷阱)
|
|
100
|
-
|
|
101
|
-
## 输出
|
|
102
|
-
探索完成后,将完整的 skill 文档输出在 \`\`\`skill-start 和 \`\`\`skill-end 之间。
|
|
103
|
-
|
|
91
|
+
prompt = `请深度探索当前项目,生成一份项目专属 Skill 文档。
|
|
92
|
+
|
|
93
|
+
## 探索步骤
|
|
94
|
+
1. 先用 glob 扫描项目结构(**/*.ts, **/*.json 等)
|
|
95
|
+
2. 读取 package.json、README.md、tsconfig.json 等关键文件
|
|
96
|
+
3. 用 codegraph 或 grep 找到主入口、核心模块、关键接口
|
|
97
|
+
4. 理解架构模式、数据流向、调用关系
|
|
98
|
+
5. 识别命名约定、代码风格、测试策略
|
|
99
|
+
6. 找出可能的坑点(复杂配置、特殊依赖、构建陷阱)
|
|
100
|
+
|
|
101
|
+
## 输出
|
|
102
|
+
探索完成后,将完整的 skill 文档输出在 \`\`\`skill-start 和 \`\`\`skill-end 之间。
|
|
103
|
+
|
|
104
104
|
记住:内容要精简、可操作、有具体路径和例子。总长度控制在 4000-5000 字符。`;
|
|
105
105
|
}
|
|
106
106
|
try {
|
|
@@ -2,64 +2,64 @@ import { spawnAgent } from '../agent/spawn.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* 子 agent 系统提示:让 agent 自主探索项目并生成 markdown 快照
|
|
4
4
|
*/
|
|
5
|
-
const SNAPSHOT_LLM_SUFFIX = `You are a project analysis agent. Your task is to explore this project and generate a concise markdown snapshot for an AI coding assistant.
|
|
6
|
-
|
|
7
|
-
## Your Mission
|
|
8
|
-
Explore the project using available tools (read_file, glob, grep) and generate a structured markdown snapshot that captures:
|
|
9
|
-
- WHAT exists (project description, tech stack, commands, modules, directory structure)
|
|
10
|
-
- WHERE it is (paths, locations)
|
|
11
|
-
|
|
12
|
-
Do NOT explain WHY or HOW (that's Project Skill's job).
|
|
13
|
-
|
|
14
|
-
## Exploration Strategy
|
|
15
|
-
1. Read package.json, README.md, tsconfig.json (or equivalent for other languages)
|
|
16
|
-
2. List top-level directories to identify modules
|
|
17
|
-
3. Scan src/ directory structure (depth ≤3, directories only)
|
|
18
|
-
4. Extract key commands from package.json scripts
|
|
19
|
-
5. Identify tech stack from dependencies
|
|
20
|
-
|
|
21
|
-
## Output Format
|
|
22
|
-
Output a markdown document between these exact delimiters:
|
|
23
|
-
|
|
24
|
-
\`\`\`snapshot-md
|
|
25
|
-
# Project Snapshot
|
|
26
|
-
|
|
27
|
-
## Description
|
|
28
|
-
(一句话描述项目,≤80字,中文)
|
|
29
|
-
|
|
30
|
-
## Tech Stack
|
|
31
|
-
(关键技术栈,逗号分隔,含版本,≤15项)
|
|
32
|
-
|
|
33
|
-
## Commands
|
|
34
|
-
| Command | Description |
|
|
35
|
-
|---------|-------------|
|
|
36
|
-
| \`npm run build\` | 构建项目 |
|
|
37
|
-
| \`npm test\` | 运行测试 |
|
|
38
|
-
| ... | ... |
|
|
39
|
-
|
|
40
|
-
## Modules
|
|
41
|
-
- **module-name** — 一句话职责描述(≤20字)
|
|
42
|
-
- **module-name** — 一句话职责描述
|
|
43
|
-
- ...
|
|
44
|
-
|
|
45
|
-
## Source Tree
|
|
46
|
-
\`\`\`
|
|
47
|
-
src/
|
|
48
|
-
agent/
|
|
49
|
-
tools/
|
|
50
|
-
ui/
|
|
51
|
-
\`\`\`
|
|
52
|
-
\`\`\`snapshot-md
|
|
53
|
-
|
|
54
|
-
## Rules
|
|
55
|
-
1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
|
|
56
|
-
2. **Tech Stack**: ≤15项,逗号分隔,含版本。格式如 "TypeScript 5.x, Node.js ≥18, openai@^4"。
|
|
57
|
-
3. **Commands**: 5-8个最重要的命令,从 package.json scripts 提取。Description ≤15字。用表格格式。
|
|
58
|
-
4. **Modules**: 每个顶层模块目录一条。职责≤20字,只写 WHAT(做什么)。
|
|
59
|
-
5. **Source Tree**: 只列目录(不列文件),深度≤3,用缩进表示层级,放在代码块中。
|
|
60
|
-
6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
|
|
61
|
-
7. 总 markdown ≤ 2500 字符。
|
|
62
|
-
8. 使用中文。
|
|
5
|
+
const SNAPSHOT_LLM_SUFFIX = `You are a project analysis agent. Your task is to explore this project and generate a concise markdown snapshot for an AI coding assistant.
|
|
6
|
+
|
|
7
|
+
## Your Mission
|
|
8
|
+
Explore the project using available tools (read_file, glob, grep) and generate a structured markdown snapshot that captures:
|
|
9
|
+
- WHAT exists (project description, tech stack, commands, modules, directory structure)
|
|
10
|
+
- WHERE it is (paths, locations)
|
|
11
|
+
|
|
12
|
+
Do NOT explain WHY or HOW (that's Project Skill's job).
|
|
13
|
+
|
|
14
|
+
## Exploration Strategy
|
|
15
|
+
1. Read package.json, README.md, tsconfig.json (or equivalent for other languages)
|
|
16
|
+
2. List top-level directories to identify modules
|
|
17
|
+
3. Scan src/ directory structure (depth ≤3, directories only)
|
|
18
|
+
4. Extract key commands from package.json scripts
|
|
19
|
+
5. Identify tech stack from dependencies
|
|
20
|
+
|
|
21
|
+
## Output Format
|
|
22
|
+
Output a markdown document between these exact delimiters:
|
|
23
|
+
|
|
24
|
+
\`\`\`snapshot-md
|
|
25
|
+
# Project Snapshot
|
|
26
|
+
|
|
27
|
+
## Description
|
|
28
|
+
(一句话描述项目,≤80字,中文)
|
|
29
|
+
|
|
30
|
+
## Tech Stack
|
|
31
|
+
(关键技术栈,逗号分隔,含版本,≤15项)
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
| Command | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| \`npm run build\` | 构建项目 |
|
|
37
|
+
| \`npm test\` | 运行测试 |
|
|
38
|
+
| ... | ... |
|
|
39
|
+
|
|
40
|
+
## Modules
|
|
41
|
+
- **module-name** — 一句话职责描述(≤20字)
|
|
42
|
+
- **module-name** — 一句话职责描述
|
|
43
|
+
- ...
|
|
44
|
+
|
|
45
|
+
## Source Tree
|
|
46
|
+
\`\`\`
|
|
47
|
+
src/
|
|
48
|
+
agent/
|
|
49
|
+
tools/
|
|
50
|
+
ui/
|
|
51
|
+
\`\`\`
|
|
52
|
+
\`\`\`snapshot-md
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
|
|
56
|
+
2. **Tech Stack**: ≤15项,逗号分隔,含版本。格式如 "TypeScript 5.x, Node.js ≥18, openai@^4"。
|
|
57
|
+
3. **Commands**: 5-8个最重要的命令,从 package.json scripts 提取。Description ≤15字。用表格格式。
|
|
58
|
+
4. **Modules**: 每个顶层模块目录一条。职责≤20字,只写 WHAT(做什么)。
|
|
59
|
+
5. **Source Tree**: 只列目录(不列文件),深度≤3,用缩进表示层级,放在代码块中。
|
|
60
|
+
6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
|
|
61
|
+
7. 总 markdown ≤ 2500 字符。
|
|
62
|
+
8. 使用中文。
|
|
63
63
|
`;
|
|
64
64
|
/**
|
|
65
65
|
* 生成 LLM 快照(markdown 格式)
|
|
@@ -67,17 +67,17 @@ src/
|
|
|
67
67
|
* @param signal AbortSignal
|
|
68
68
|
*/
|
|
69
69
|
export async function generateLLMSnapshot(root, signal) {
|
|
70
|
-
const prompt = `请探索项目 ${root},生成项目快照(markdown 格式)。
|
|
71
|
-
|
|
72
|
-
## 探索步骤
|
|
73
|
-
1. 读取 package.json(或等效配置文件)
|
|
74
|
-
2. 读取 README.md
|
|
75
|
-
3. 列出顶层目录,识别模块
|
|
76
|
-
4. 扫描 src/ 目录结构(深度≤3,只列目录)
|
|
77
|
-
5. 从 package.json scripts 提取关键命令
|
|
78
|
-
6. 从 dependencies 识别技术栈
|
|
79
|
-
|
|
80
|
-
## 输出
|
|
70
|
+
const prompt = `请探索项目 ${root},生成项目快照(markdown 格式)。
|
|
71
|
+
|
|
72
|
+
## 探索步骤
|
|
73
|
+
1. 读取 package.json(或等效配置文件)
|
|
74
|
+
2. 读取 README.md
|
|
75
|
+
3. 列出顶层目录,识别模块
|
|
76
|
+
4. 扫描 src/ 目录结构(深度≤3,只列目录)
|
|
77
|
+
5. 从 package.json scripts 提取关键命令
|
|
78
|
+
6. 从 dependencies 识别技术栈
|
|
79
|
+
|
|
80
|
+
## 输出
|
|
81
81
|
严格按照系统提示的 markdown 格式输出,不要添加额外解释。`;
|
|
82
82
|
try {
|
|
83
83
|
const result = await spawnAgent({
|
package/dist/repl/index.js
CHANGED
|
@@ -169,7 +169,7 @@ function refreshStatusBase(history, lastTurnUsage) {
|
|
|
169
169
|
model: config.model,
|
|
170
170
|
contextBar: renderContextBarInline(history),
|
|
171
171
|
cwd: process.cwd(),
|
|
172
|
-
modeTag: getAgentMode() === 'plan' ? '
|
|
172
|
+
modeTag: getAgentMode() === 'plan' ? 'Plan' : 'Auto',
|
|
173
173
|
planSummary: hasActivePlan() ? getActivePlanSummary(process.stdout.columns ?? 80) : '',
|
|
174
174
|
lastTurnUsage,
|
|
175
175
|
});
|
package/dist/session/compact.js
CHANGED
|
@@ -6,10 +6,9 @@ import { Spinner } from '../ui/spinner.js';
|
|
|
6
6
|
import * as layout from '../ui/layout.js';
|
|
7
7
|
import { pruneAfterCompaction } from '../rollback/index.js';
|
|
8
8
|
import { toText } from '../context/utils.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
* 由 agent/core.ts 在每次 chat 响应后更新;compact/repl 在 usage 失效时同步重置。 */
|
|
9
|
+
export function createContextState() {
|
|
10
|
+
return { lastEstimate: 0, correction: 1 };
|
|
11
|
+
}
|
|
13
12
|
export const contextState = {
|
|
14
13
|
lastEstimate: 0,
|
|
15
14
|
correction: 1,
|
|
@@ -240,9 +239,10 @@ async function defaultSummarize(older, focus) {
|
|
|
240
239
|
* 不检查阈值——调用方(maybeCompact)决定是否调;/compact 直接调以强制压缩。
|
|
241
240
|
*/
|
|
242
241
|
export async function compactHistory(history, opts) {
|
|
242
|
+
const state = opts.contextState ?? contextState;
|
|
243
243
|
const schemaTokens = estimateToolSchemaTokens();
|
|
244
244
|
const estimateBefore = estimateMessagesTokens(history) + schemaTokens;
|
|
245
|
-
|
|
245
|
+
state.lastEstimate = estimateBefore;
|
|
246
246
|
const groups = groupFromEnd(history);
|
|
247
247
|
// 保近期:从尾向前累积直到预算花完(至少保 1 组),永不劈开 group。
|
|
248
248
|
const keepBudget = Math.floor(opts.window * 0.4);
|
|
@@ -313,9 +313,9 @@ export async function compactHistory(history, opts) {
|
|
|
313
313
|
history.push(...rebuilt);
|
|
314
314
|
pruneAfterCompaction(history);
|
|
315
315
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
316
|
+
state.lastEstimate = estimateAfter;
|
|
317
|
+
state.lastUsage = undefined;
|
|
318
|
+
state.correction = 1;
|
|
319
319
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
320
320
|
return {
|
|
321
321
|
compacted: true,
|
|
@@ -400,9 +400,9 @@ export async function compactHistory(history, opts) {
|
|
|
400
400
|
history.push(...rebuilt);
|
|
401
401
|
pruneAfterCompaction(history); // 摘要删了旧轮次 → 按存活轮次裁剪回滚日志
|
|
402
402
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
403
|
+
state.lastEstimate = estimateAfter;
|
|
404
|
+
state.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
|
|
405
|
+
state.correction = 1;
|
|
406
406
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
407
407
|
// 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
|
|
408
408
|
if (estimateAfter >= opts.threshold * opts.window) {
|
|
@@ -418,9 +418,9 @@ export async function compactHistory(history, opts) {
|
|
|
418
418
|
}
|
|
419
419
|
// 摘要失败:回退仅微压缩(tool content 已原地改),结构不动
|
|
420
420
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
421
|
+
state.lastEstimate = estimateAfter;
|
|
422
|
+
state.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
|
|
423
|
+
state.correction = 1;
|
|
424
424
|
if (microcompactDone) {
|
|
425
425
|
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
426
426
|
return {
|
|
@@ -458,10 +458,10 @@ export async function compactHistory(history, opts) {
|
|
|
458
458
|
* 强制走 compactHistory(manual/force 参数透传)。返 CompactResult 给 caller 文案展示。
|
|
459
459
|
* 默认 manual=false 自动路径完全不变。
|
|
460
460
|
*/
|
|
461
|
-
export async function maybeCompact(history, report, manualOpts) {
|
|
461
|
+
export async function maybeCompact(history, report, manualOpts, state = contextState) {
|
|
462
462
|
const schemaTokens = estimateToolSchemaTokens();
|
|
463
463
|
const est = estimateMessagesTokens(history) + schemaTokens;
|
|
464
|
-
|
|
464
|
+
state.lastEstimate = est;
|
|
465
465
|
const isManual = manualOpts?.manual === true;
|
|
466
466
|
// 手动路径:旁路 autoCompact / report / 总阈三重门
|
|
467
467
|
if (!isManual) {
|
|
@@ -485,6 +485,7 @@ export async function maybeCompact(history, report, manualOpts) {
|
|
|
485
485
|
focus: manualOpts?.focus,
|
|
486
486
|
manual: isManual,
|
|
487
487
|
force: manualOpts?.force,
|
|
488
|
+
contextState: state,
|
|
488
489
|
});
|
|
489
490
|
if (isManual)
|
|
490
491
|
return r;
|
package/dist/session/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - persist.ts:history 序列化到磁盘 + --resume / /resume
|
|
5
5
|
* 依赖方向:session → {llm(摘要复用 chat), config, ui};llm 不反向依赖 session。
|
|
6
6
|
*/
|
|
7
|
-
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, } from './compact.js';
|
|
7
|
+
export { compactHistory, maybeCompact, capToolResultForHistory, truncateMid, contextState, createContextState, } from './compact.js';
|
|
8
8
|
// ── Context Budget Scheduler 接缝 ────────────────────────────────────────
|
|
9
9
|
// agent/core.ts 步前调 runScheduler(history, step):评估五区预算 → 按 ROI 调度
|
|
10
10
|
// shrink_cold_tools / cap_hot_tools / compact_history。开关关闭时退化为 maybeCompact。
|
|
@@ -38,14 +38,14 @@ import { ui } from '../ui/theme.js';
|
|
|
38
38
|
/** 创建 runAgentCore 闭包持有的 scheduler(每次 agent 启动一个新实例)。
|
|
39
39
|
* observePush 当前只是占位:真正 L1/L2/L3 已由 cap / pruner / lifecycle 在 push 时跑;
|
|
40
40
|
* 保留接口为后续「调度器注入 hotBoundary 给 lifecycle」演进留接缝。 */
|
|
41
|
-
export function createBudgetScheduler() {
|
|
41
|
+
export function createBudgetScheduler(state = contextState) {
|
|
42
42
|
const obs = {
|
|
43
43
|
lastRunLog: null,
|
|
44
44
|
observePush(_history, _idx) {
|
|
45
45
|
// 占位:push-time 三闸(cap / pruner / lifecycle)已自动跑;此接缝供将来演进。
|
|
46
46
|
},
|
|
47
47
|
async runStep(history, step) {
|
|
48
|
-
const report = evaluateBudget(history, config.contextWindowTokens, step,
|
|
48
|
+
const report = evaluateBudget(history, config.contextWindowTokens, step, state.correction);
|
|
49
49
|
const actions = scheduleActions(report);
|
|
50
50
|
let compactHistoryCalled = false;
|
|
51
51
|
for (const a of actions) {
|
|
@@ -55,7 +55,7 @@ export function createBudgetScheduler() {
|
|
|
55
55
|
}
|
|
56
56
|
else if (a.kind === 'compact_history') {
|
|
57
57
|
// 路由到 maybeCompact(history, report)——按 ROI 调度(只有 history 超 / totalOver 才真压)
|
|
58
|
-
await maybeCompact(history, report);
|
|
58
|
+
await maybeCompact(history, report, undefined, state);
|
|
59
59
|
compactHistoryCalled = true;
|
|
60
60
|
}
|
|
61
61
|
// shrink_cold_tools L1/L2/L3 与 cap_hot_tools:已由 push-time 闸在每次 push 自动跑
|
|
@@ -71,14 +71,14 @@ export function createBudgetScheduler() {
|
|
|
71
71
|
};
|
|
72
72
|
obs.lastRunLog = log;
|
|
73
73
|
// 暴露给 /context 共享读(repl / context 命令)
|
|
74
|
-
|
|
74
|
+
state.schedulerLog = log;
|
|
75
75
|
},
|
|
76
76
|
};
|
|
77
77
|
return obs;
|
|
78
78
|
}
|
|
79
79
|
/** 便捷:agent/core.ts 不需要每次 createBudgetScheduler,直接 runScheduler(history, step)。 */
|
|
80
|
-
export async function runScheduler(history, step) {
|
|
81
|
-
const s = createBudgetScheduler();
|
|
80
|
+
export async function runScheduler(history, step, state = contextState) {
|
|
81
|
+
const s = createBudgetScheduler(state);
|
|
82
82
|
await s.runStep(history, step);
|
|
83
83
|
}
|
|
84
84
|
/** 手动 /compact 入口(repl):与自动路径完全一致——五区 ROI 调度,但 history 摘要强制执行。
|
package/dist/ui/content.js
CHANGED
|
@@ -136,6 +136,22 @@ export function totalRows() {
|
|
|
136
136
|
export function committedRows() {
|
|
137
137
|
return rows.length;
|
|
138
138
|
}
|
|
139
|
+
/** 把缓冲尾部的视觉空白行归一化为恰好 count 条,并结束在“无当前行”状态。
|
|
140
|
+
* 供 markdown→mutation 边界使用;ANSI reset/颜色码和空格均按视觉空白处理。 */
|
|
141
|
+
export function normalizeTrailingBlankRows(count) {
|
|
142
|
+
if (hasCurrent) {
|
|
143
|
+
rows.push(rowStartSgr + curRaw + '\x1B[0m');
|
|
144
|
+
}
|
|
145
|
+
const isBlank = (line) => line.replace(/\x1b\[[0-9;]*m/g, '').trim().length === 0;
|
|
146
|
+
while (rows.length > 0 && isBlank(rows[rows.length - 1]))
|
|
147
|
+
rows.pop();
|
|
148
|
+
for (let i = 0; i < Math.max(0, count); i++)
|
|
149
|
+
rows.push('\x1B[0m');
|
|
150
|
+
curSgr = '';
|
|
151
|
+
rowStartSgr = '';
|
|
152
|
+
curRaw = '';
|
|
153
|
+
hasCurrent = false;
|
|
154
|
+
}
|
|
139
155
|
/** 取绝对行索引(0-based,含当前行)的原始自洽行;越界返 null。供鼠标选区文本提取。 */
|
|
140
156
|
export function lineAt(abs) {
|
|
141
157
|
const all = snapshot();
|
package/dist/ui/layout.js
CHANGED
|
@@ -3,6 +3,7 @@ import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncate
|
|
|
3
3
|
import { ui } from './theme.js';
|
|
4
4
|
import * as content from './content.js';
|
|
5
5
|
import * as mouse from './mouse.js';
|
|
6
|
+
import { shiftBatchesAfter } from './batch.js';
|
|
6
7
|
import { copyToClipboard, readClipboard } from './clipboard.js';
|
|
7
8
|
import { renderMarkdown } from './markdown.js';
|
|
8
9
|
// ── 内部状态 ──
|
|
@@ -541,8 +542,9 @@ export function contentInsertAfter(after, lines) {
|
|
|
541
542
|
scrollOffset = Math.min(delta, Math.max(0, content.totalRows() - g.contentBottom));
|
|
542
543
|
}
|
|
543
544
|
}
|
|
544
|
-
// batch
|
|
545
|
-
|
|
545
|
+
// 必须同步平移 batch 索引。若延迟到 dynamic import.then,下一条 mutation 可能已经
|
|
546
|
+
// 按插入后的 buffer 创建;旧回调会再平移它一次,导致详情插到空行之后。
|
|
547
|
+
shiftBatchesAfter(after, delta);
|
|
546
548
|
repaintViewport();
|
|
547
549
|
}
|
|
548
550
|
/**
|
|
@@ -570,14 +572,32 @@ export function contentDeleteFrom(startIdx, n) {
|
|
|
570
572
|
if (scrolled) {
|
|
571
573
|
scrollOffset = Math.max(0, scrollOffset - delta);
|
|
572
574
|
}
|
|
573
|
-
// batch 摘要行索引平移(用 -delta 表示后段索引前移)
|
|
574
|
-
|
|
575
|
+
// batch 摘要行索引平移(用 -delta 表示后段索引前移),同插入路径必须同步。
|
|
576
|
+
shiftBatchesAfter(startIdx, -delta);
|
|
575
577
|
repaintViewport();
|
|
576
578
|
}
|
|
577
579
|
/** 当前内容物理行总数(含当前未提交行)。供 batch 渲染器在 endBatch 时定位摘要行索引。 */
|
|
578
580
|
export function totalRows() {
|
|
579
581
|
return content.totalRows();
|
|
580
582
|
}
|
|
583
|
+
/** 正文→mutation 首摘要前,把尾部间距强制归一为一条视觉空行。 */
|
|
584
|
+
export function normalizeMutationBoundary() {
|
|
585
|
+
if (!active || !ui.isTTY)
|
|
586
|
+
return;
|
|
587
|
+
if (mdActive)
|
|
588
|
+
commitMd();
|
|
589
|
+
const totalBefore = content.totalRows();
|
|
590
|
+
content.normalizeTrailingBlankRows(1);
|
|
591
|
+
const g = getGeo();
|
|
592
|
+
const delta = content.totalRows() - totalBefore;
|
|
593
|
+
if (scrollOffset > 0 && delta !== 0) {
|
|
594
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, Math.max(0, content.totalRows() - g.contentBottom)));
|
|
595
|
+
}
|
|
596
|
+
contentRow = Math.min(content.committedRows() + 1, g.contentBottom);
|
|
597
|
+
contentCol = 1;
|
|
598
|
+
if (scrollOffset === 0)
|
|
599
|
+
repaintViewport();
|
|
600
|
+
}
|
|
581
601
|
/** 原地刷新一条内容行(行数不变),用于运行中的工具 batch 更新计数。 */
|
|
582
602
|
export function contentReplaceLine(absIdx, line) {
|
|
583
603
|
if (!active)
|
|
@@ -1273,11 +1293,11 @@ function composeModelLine(status, cols) {
|
|
|
1273
1293
|
const ctxW = ansiDisplayWidth(ctx);
|
|
1274
1294
|
// 左段:模式标识 + 切换提示(灰)+ 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1275
1295
|
const modeTag = status.modeTag ?? '';
|
|
1276
|
-
const modeColor = modeTag === '
|
|
1296
|
+
const modeColor = modeTag === 'Plan' ? ui.yellow : ui.accent;
|
|
1277
1297
|
const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
|
|
1278
1298
|
const modeW = modeTag ? displayWidth(modeTag) : 0;
|
|
1279
1299
|
// 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
|
|
1280
|
-
const HINT = 'Shift+Tab
|
|
1300
|
+
const HINT = 'Shift+Tab 切换';
|
|
1281
1301
|
const hintW = modeTag ? displayWidth(HINT) : 0;
|
|
1282
1302
|
const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
|
|
1283
1303
|
const tokChip = formatTurnTokenChip(status.lastTurnUsage);
|
package/dist/ui/markdown.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// clipAnsiLine 兜底,绝不溢出。颜色一律嵌入式 ANSI(ui.*)写进字符串。
|
|
14
14
|
import { highlight, supportsLanguage } from 'cli-highlight';
|
|
15
15
|
import { ui, getThemeVersion } from './theme.js';
|
|
16
|
-
import { charWidth, displayWidth, ansiDisplayWidth } from './render.js';
|
|
16
|
+
import { charWidth, displayWidth, ansiDisplayWidth, stripAnsi } from './render.js';
|
|
17
17
|
const RESET = '\x1b[0m';
|
|
18
18
|
// theme 无 italic/strike,自备(TTY 感知:!isTTY 退化为空串,同 ui.* 契约)。
|
|
19
19
|
const ITALIC = ui.isTTY ? '\x1b[3m' : '';
|
|
@@ -577,13 +577,14 @@ function renderMarkdownImpl(text, cols) {
|
|
|
577
577
|
// 独立 markdown 段的外部间距由 agent/batch 边界统一管理。流式后端可能把
|
|
578
578
|
// "\n\n" 与首段正文拆成不同 chunk;仅清洗首个正文 chunk 不够,因为 mdBuf
|
|
579
579
|
// 累积重渲染时这些换行仍会变成段首空行。这里最终兜底,正文段永不自带前导空行。
|
|
580
|
-
|
|
580
|
+
const isVisualBlank = (line) => stripAnsi(line).trim().length === 0;
|
|
581
|
+
while (out.length > 0 && isVisualBlank(out[0]))
|
|
581
582
|
out.shift();
|
|
582
583
|
// 末尾不留空行:agent onText 后接 onToolCall 的 contentWrite('\n') 会补 1 空行分隔正文与 ● 行;
|
|
583
584
|
// 若 md 末尾自带空行(段落/代码块后)则叠成 2 空行。裁掉末尾连续空行,让 onToolCall / 轮末
|
|
584
585
|
// contentWrite('\n') 恰好补 1 行(与改造前 raw 文本行为一致)。块间空行(flushPara/flushCode 中段
|
|
585
586
|
// push 的)不受影响——只裁末尾。
|
|
586
|
-
while (out.length > 0 && out[out.length - 1]
|
|
587
|
+
while (out.length > 0 && isVisualBlank(out[out.length - 1]))
|
|
587
588
|
out.pop();
|
|
588
589
|
return out;
|
|
589
590
|
}
|