mocode-ai 0.5.5 → 0.5.7

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.
@@ -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
- contextState.lastUsage = result.usage; // 供 /context 与状态行显示实测 token
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
- contextState.correction = Math.max(0.5, Math.min(2.0, raw));
273
+ runtimeContextState.correction = Math.max(0.5, Math.min(2.0, raw));
273
274
  }
274
275
  }
275
276
  hooks.onChatDone?.(); // 主 agent:spinner.stop()
@@ -482,7 +483,7 @@ export async function runAgentCore(opts) {
482
483
  return { completed: true, finalText: null, usage: turnUsage };
483
484
  }
484
485
  finally {
485
- // 跑完(正常 / 达上限)在回复末尾打耗时摘要行(仿 Claude Code);中断 done=false 不打。
486
+ // 跑完(正常 / 达上限)在回复末尾打耗时摘要行;中断 done=false 不打。
486
487
  if (done) {
487
488
  hooks.onDone?.(Date.now() - t0, turnUsage);
488
489
  }
@@ -34,7 +34,7 @@ function writeToolHeader(tc) {
34
34
  // 第一条工具开始时立即落摘要;后续调用加入同一 batch,并原地刷新计数。
35
35
  batch.showLiveBatch(currentBatchId, layout);
36
36
  }
37
- /** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮,仿 Claude Code);其余走一行 preview。
37
+ /** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮);其余走一行 preview。
38
38
  * 同 writeToolHeader,改为累积到 BatchRenderer(只缓存字符串,不写屏)。 */
39
39
  function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
40
40
  if (!currentBatchId)
@@ -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
- layout.contentWrite('\n');
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) => writeToolHeader(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),
@@ -7,8 +7,8 @@
7
7
  // - 独立 history:不共享主对话,避免子任务的工具噪声污染主上下文。
8
8
  // - 系统提示复用主 agent 组装链(config.systemPrompt + memory 段 + skills 段)+ 子 agent 角色后缀。
9
9
  // - 工具子集:按白名单从 chatTools 过滤;无白名单 = 全量(但 task 工具调用方通常会限定只读)。
10
- // - 不调 beginTurn(不进主回滚链);但文件 mutation 仍经 executeTool→recordMutation,
11
- // 归入当前主轮次(语义:子 agent 的改动属于当前主轮,可随 /rollback 一起撤销)。
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,
@@ -4,8 +4,6 @@
4
4
  // 单一入口 runScheduler(agent/core.ts 步前调)接管"何时调用哪一闸"的调度。
5
5
  // 不调 LLM、不碰 Tool Calling schema / executeTool / tool_call_id 配对 / TUI 渲染
6
6
  // (叶子级:仅 stdlib + tools/constants + session/compact 的 capToolResultForHistory 兜底 + config 开关)。
7
- //
8
- // 见 CLAUDE.md「Context Optimization Pipeline」节 +「Context Budget Scheduler」节。
9
7
  export { optimizeToolResult } from './pipeline.js';
10
8
  export { classify, knownToolKinds } from './classifier.js';
11
9
  export { registerEncoder, registerAll, getEncoder, registeredKinds, } from './registry.js';
@@ -1,6 +1,6 @@
1
1
  // Context Optimization Pipeline 的类型契约。
2
2
  //
3
- // 设计原则(见 CLAUDE.md context/ 子系统):
3
+ // 设计原则:
4
4
  // - Tool Calling 的 JSON schema 与 executeTool 不动;本层只接管"工具结果进 LLM 前"的表示。
5
5
  // - 不设计统一 DSL,针对不同数据类型各做最优 encoder。
6
6
  // - 所有 encoder 是纯函数(无 LLM 调用 / 无 IO / 无副作用),永不抛错(pipeline 层 try/catch,
@@ -1,8 +1,7 @@
1
1
  // memory 发现子系统:加载项目记忆 MOCODE.md(对标 skills/discover.ts 的叶子模式)。
2
2
  // 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools/skills,避免环。
3
3
  //
4
- // 约定:纯 MOCODE.md(不读 CLAUDE.md——mocode 是独立工具,接非 Claude 后端,
5
- // 有自己的工具集与约定,叫 CLAUDE.md 名不副实且可能读到 Claude 专属内容)。
4
+ // 约定:纯 MOCODE.md(mocode 是独立工具,有自己的工具集与约定)。
6
5
  // 项目级从 cwd 向上逐级找,全局 ~/.mocode/MOCODE.md。全量注入 systemPrompt(超长截断);
7
6
  import { existsSync, readFileSync } from 'node:fs';
8
7
  import os from 'node:os';
@@ -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({
@@ -95,8 +95,7 @@ function maskKey(k) {
95
95
  return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
96
96
  }
97
97
  /**
98
- * /init 指令:发给 agent 扫描项目并生成 MOCODE.md(对标 Claude Code /init 生成 CLAUDE.md,
99
- * 但 mocode 读 MOCODE.md)。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
98
+ * /init 指令:发给 agent 扫描项目并生成 MOCODE.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
100
99
  */
101
100
  const INIT_PROMPT = `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
102
101
 
@@ -169,7 +168,7 @@ function refreshStatusBase(history, lastTurnUsage) {
169
168
  model: config.model,
170
169
  contextBar: renderContextBarInline(history),
171
170
  cwd: process.cwd(),
172
- modeTag: getAgentMode() === 'plan' ? 'plan' : 'auto',
171
+ modeTag: getAgentMode() === 'plan' ? 'Plan' : 'Auto',
173
172
  planSummary: hasActivePlan() ? getActivePlanSummary(process.stdout.columns ?? 80) : '',
174
173
  lastTurnUsage,
175
174
  });
@@ -332,7 +331,7 @@ function stopRunningListener() {
332
331
  * 把用户消息格式化为带满宽背景色的文本(上滑时易辨认用户消息)。
333
332
  * 每行用 padEndDisplay 填充到终端宽度(含 ❯ / 缩进),背景色 SGR 包裹整行 + 行末 reset。
334
333
  * 满宽 pad 使终端背景色覆盖整行(含行尾空单元格),上滑滚动时用户消息呈连续色块、与 assistant 正文区分。
335
- * 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)之间空一行,仿 Claude Code。
334
+ * 末尾多留一空行(\n\n 收尾):用户消息与后续(agent 流式输出 / 下条消息)之间空一行。
336
335
  */
337
336
  function formatUserMessage(lines) {
338
337
  const cols = layout.getGeo().cols;
@@ -442,7 +441,7 @@ function textOf(c) {
442
441
  return String(c);
443
442
  }
444
443
  /**
445
- * 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文,仿 Claude Code):
444
+ * 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文):
446
445
  * user→❯ 回显、assistant→正文(+ tool_calls 折叠成 ● 摘要行)、tool→↳ 结果预览;system 跳过。
447
446
  * 思考段不持久(history 只存正文),故无思考折叠。渲染后续写位在末尾,紧接 enterInputMode 画输入框。
448
447
  * 内容长于屏时 viewport 显尾(最近轮次),PgUp 可看更早——与流式态一致。
@@ -608,7 +607,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
608
607
  memoryEnabled: isMemoryEnabled(),
609
608
  });
610
609
  // 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
611
- // --resume 有历史则渲染对话(仿 Claude Code),否则横幅。
610
+ // --resume 有历史则渲染对话,否则横幅。
612
611
  setTheme(config.theme);
613
612
  layout.enterAltScreen();
614
613
  refreshStatusBase(history);
@@ -679,7 +678,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
679
678
  });
680
679
  /**
681
680
  * 回滚子流程(由 /rollback 触发):菜单(↑/↓)选轮次 → 选中第 X 轮 = 删第 X 轮及之后 + 预填第 X 轮 user 输入
682
- * (仿 Claude Code rewind,Enter 重新跑该轮);被删轮次的文件改动走二选一菜单(promptRevertChoice:
681
+ * (Enter 重新跑该轮);被删轮次的文件改动走二选一菜单(promptRevertChoice:
683
682
  * 撤销文件 / 只撤销消息)。选轮 + 方式菜单均走 raw mode。预填经 pendingPrefill 注入下轮 INPUT。
684
683
  */
685
684
  const rollbackFlow = async () => {
@@ -861,7 +860,8 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
861
860
  lastTurnUsage = undefined; // 续接:旧会话的 token 累计已无意义,清空等下轮覆写
862
861
  layout.clearContent();
863
862
  renderHistory(history);
864
- layout.contentWrite(`${ui.dim}(已续接会话 ${loaded.id})${ui.reset}\n`);
863
+ // 末尾 \n\n:与后续用户消息(❯ bubble)之间空一行。
864
+ layout.contentWrite(`${ui.dim}(已续接会话 ${loaded.id})${ui.reset}\n\n`);
865
865
  }
866
866
  while (true) {
867
867
  // INPUT 态:画底栏输入框 + 状态行,光标入输入框
@@ -3,7 +3,7 @@
3
3
  import { jailResolve, jailGlobPattern } from './jail.js';
4
4
  /**
5
5
  * 豁免 cwd 牢笼的工具:
6
- * - memory_*:操作 ~/.mocode 与 <cwd>/.mocode(CLAUDE.md 明确不进回滚、在外圈),本就该在外圈
6
+ * - memory_*:操作 ~/.mocode 与 <cwd>/.mocode,本就该在外圈
7
7
  * - use_skill:读 ~/.claude/skills、~/.mocode/skills、<cwd>/.mocode/skills,部分在外圈
8
8
  * - web_*:跨网络,非文件路径
9
9
  * - ask_human / switch_mode:无文件路径
@@ -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
- /** 跨模块共享的上下文状态:agent lastUsage,compact 写 lastEstimate,repl 的 /context 读。
10
- * scheduler.ts 写最近一次调度日志(可选,repl 可读不到时 no-op)。
11
- * correction:API 实测 token / 估算 token 的校正系数(1.0 = 无偏差;>1 = 低估;<1 = 高估)。
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
- contextState.lastEstimate = estimateBefore;
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
- contextState.lastEstimate = estimateAfter;
317
- contextState.lastUsage = undefined;
318
- contextState.correction = 1;
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
- contextState.lastEstimate = estimateAfter;
404
- contextState.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
405
- contextState.correction = 1;
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
- contextState.lastEstimate = estimateAfter;
422
- contextState.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
423
- contextState.correction = 1;
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
- contextState.lastEstimate = est;
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;
@@ -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, contextState.correction);
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
- contextState.schedulerLog = log;
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 摘要强制执行。
@@ -1,4 +1,4 @@
1
- // skills 发现子系统(对标 Claude Code 的 skill 自动发现)。
1
+ // skills 发现子系统。
2
2
  // 仅依赖 node 标准库,是叶子模块:不依赖 config/agent/llm/tools,避免环。
3
3
  //
4
4
  // 约定:每个 skill 是一个目录 <skill-name>/SKILL.md,顶部 YAML frontmatter
package/dist/ui/batch.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 工具调用批量折叠渲染(仿 Claude Code):
2
+ * 工具调用批量折叠渲染:
3
3
  * agent 一轮返回 N 个 tool_calls 时,不逐个打印 `● name ↳ result`,
4
4
  * 改输出一行摘要 `● Ran N tools · read 3, grep 1, glob 1`;
5
5
  * 鼠标点击该摘要行 → 展开完整明细(● 头 + ↳ preview / diff 块),再点折回。
@@ -70,7 +70,7 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
70
70
  }
71
71
  }
72
72
  // ── 摘要行文本生成 ──
73
- /** 把 entry 列表压缩成一行摘要(Claude Code 风格)。 */
73
+ /** 把 entry 列表压缩成一行摘要。 */
74
74
  function buildSummaryLine(entries) {
75
75
  if (entries.length === 0) {
76
76
  return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.dim}No tools${ui.reset}`;
@@ -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
  // ── 内部状态 ──
@@ -64,7 +65,7 @@ let cursorChangeHandler = null;
64
65
  const esc = {
65
66
  altOn: '\x1B[?1049h',
66
67
  altOff: '\x1B[?1049l',
67
- // 完整鼠标追踪(仿 Claude Code 全屏渲染):1000=按键(按下/释放)+ 1002=拖动 motion + 1006=SGR 编码。
68
+ // 完整鼠标追踪:1000=按键(按下/释放)+ 1002=拖动 motion + 1006=SGR 编码。
68
69
  // 拿到按下/拖动/释放的坐标后,由 layout 在应用层维护选区(mouse.ts 重组报表 → handleMouseEvent):
69
70
  // - 左键:内容区按下开选区、拖动扩展(触边自动翻页跨屏)、释放只留高亮,不自动复制;输入框不响应
70
71
  // (不干扰正常打字/焦点)。
@@ -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
- void import('./batch.js').then((m) => m.shiftBatchesAfter(after, delta));
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
- void import('./batch.js').then((m) => m.shiftBatchesAfter(startIdx, -delta));
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)
@@ -1139,7 +1159,7 @@ function handleMouseEvent(e) {
1139
1159
  if (!selection)
1140
1160
  return;
1141
1161
  if (!selection.dragged) {
1142
- // 内容区纯点击(未拖动):若落在工具 batch 摘要行上 → 切换展开/折叠(仿 Claude Code);
1162
+ // 内容区纯点击(未拖动):若落在工具 batch 摘要行上 → 切换展开/折叠;
1143
1163
  // 否则原行为:清选区。batch 反查通过 content lineAt + dynamic import(避免 layout↔batch 循环依赖)。
1144
1164
  const absClick = selection.anchorLine; // 起止同行同列,取任一;未拖动时 line = anchor = end
1145
1165
  void (async () => {
@@ -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 === 'plan' ? ui.yellow : ui.accent;
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);
@@ -5,7 +5,7 @@
5
5
  // 整段(text 可能停在未闭合 ``` fence 中段,状态机扫到 EOF 仍 inFence 时把已累积代码行
6
6
  // 当「进行中代码块」照常 emit,边生成边显)。
7
7
  //
8
- // 样式:代码块 Flat(Claude Code 风)——语言标签 dim 置顶 + 2 空格 gutter + cli-highlight
8
+ // 样式:代码块 Flat——语言标签 dim 置顶 + 2 空格 gutter + cli-highlight
9
9
  // 语法高亮 + 软折行,无边框。其余:标题/列表(嵌套)/行内代码/粗体/斜体/删除线/链接/引用块/分隔线。
10
10
  //
11
11
  // 硬约束:每行 ansiDisplayWidth ≤ cols(layout.repaintViewport 直出行,超宽会让终端 auto-wrap
@@ -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
- while (out.length > 0 && out[0] === '')
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
  }
package/dist/ui/mouse.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // 背景:layout 进 alt 屏时发 \x1B[?1000h + \x1B[?1002h + \x1B[?1006h,启用:
4
4
  // 1000 = 按键事件追踪(按下 / 释放),1002 = 拖动追踪(按住键移动时上报 motion),
5
5
  // 1006 = SGR 编码 → \x1B[<btn;col;rowM(按下 / 拖动)或 \x1B[<btn;col;rowm(释放)。
6
- // 有了 1002 的拖动上报,才能实现"按住左键拖过多屏 → 应用层维护选区 → 松开复制"(仿 Claude Code)
6
+ // 有了 1002 的拖动上报,才能实现"按住左键拖过多屏 → 应用层维护选区 → 松开复制"。
7
7
  //
8
8
  // 但 Node readline 的 emitKeypressEvents 不认 `<` 为 CSI 参数字节,把一条报表拆成 `\x1b[<` +
9
9
  // 逐字符共 ≥9 个 keypress(数字 / `;` / `M` 有可打印 name,会被当文本砸进输入框)。故本模块在
package/dist/ui/prompt.js CHANGED
@@ -420,7 +420,7 @@ export async function promptWithSlashMenu(opts) {
420
420
  computeFiltered();
421
421
  redraw();
422
422
  }
423
- /** Ctrl+C:有内容(已打字 / 多行 / chip / 菜单草稿 / 粘贴缓冲 / 粘贴中)则清空,再按一次(空)才退出(仿 fish / Claude Code)。 */
423
+ /** Ctrl+C:有内容(已打字 / 多行 / chip / 菜单草稿 / 粘贴缓冲 / 粘贴中)则清空,再按一次(空)才退出(仿 fish)。 */
424
424
  function onCtrlC() {
425
425
  const hasContent = chip != null ||
426
426
  lines.length > 1 ||
package/dist/ui/render.js CHANGED
@@ -61,7 +61,7 @@ export function displayWidth(str) {
61
61
  return w;
62
62
  }
63
63
  /**
64
- * 毫秒 → Claude Code 式耗时串:运行中状态行走时与轮次结束摘要行共用。
64
+ * 毫秒 → 耗时串:运行中状态行走时与轮次结束摘要行共用。
65
65
  * <10s 显 1 位小数(3.2s);10-59s 整数(12s);≥60s m+s(3m 3s);≥1h h+m(1h 2m)。
66
66
  */
67
67
  export function fmtElapsed(ms) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {