mocode-ai 0.4.4 → 0.4.5

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 CHANGED
@@ -150,6 +150,8 @@ The agent operates in **the working directory it was launched from** — to have
150
150
  | `memory_update` | Edit a memory in place (id unchanged; correct stale facts / update summary / toggle pin) |
151
151
  | `memory_forget` | Forget a memory: archived by default (recoverable), `mode=delete` for a hard delete (pinned memories can't be deleted) |
152
152
 
153
+ The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle at runtime with `/memory_switch` (REPL restart required, by design — see Skills section for the difference between Tier-1 `MOCODE.md` and Tier-2 memory).
154
+
153
155
  ## Slash commands
154
156
 
155
157
  | Command | Purpose |
@@ -162,6 +164,7 @@ The agent operates in **the working directory it was launched from** — to have
162
164
  | `/resume` | Resume a saved session |
163
165
  | `/rollback` | Menu to pick a turn to roll back to (↑↓ · Enter) |
164
166
  | `/memory` | Show memory library: entry count + recent index |
167
+ | `/memory_switch` | Toggle Tier-2 memory on/off (REPL restart required — by design) |
165
168
  | `/reflect` | Manually trigger a background memory reflection pass |
166
169
  | `/model` | Configure the LLM (baseURL / apiKey / model / context window), applied immediately + persisted |
167
170
  | `/init` | Scan the project and generate `MOCODE.md` project memory (dispatched to the agent) |
@@ -198,6 +201,13 @@ MoCode automatically scans the following directories for skills (each skill is a
198
201
 
199
202
  A skill's `description` is injected into the system prompt (progressive disclosure, tier 1); the model calls `use_skill` to load the full body (tier 2) only when the task is relevant. Use `/skills` to see discovered skills.
200
203
 
204
+ ## Project memory (MOCODE.md)
205
+
206
+ MoCode has a **two-tier memory** model distinct from skills:
207
+
208
+ - **Tier-1 — `MOCODE.md` (auto-loaded every session):** Markdown project memory that gets concatenated into the system prompt on every turn. Discovery walks `~/.mocode/MOCODE.md` → every `MOCODE.md` from the cwd up to the filesystem root (far→near, near wins). On overflow the body is truncated with a marker pointing back at the files. Generate or refresh one with `/init`, or write it by hand — it's plain Markdown, no schema. `MOCODE.md` is also where the agent itself persists "next-session facts" it deduces (architecture, conventions, pitfalls).
209
+ - **Tier-2 — `memory_*` tool library (agent-driven, opt-in):** Discrete tagged records (`decision` / `fact` / `pitfall` / `reference` / `feedback`) with recall-count-based decay (30-day → archived; 90-day → GC). The agent saves / searches / updates / forgets via tools; titles go in the system-prompt index (≤50), bodies fetched on demand via `memory_search`. Off by default; toggle with `MEMORY_ENABLED=true` at startup or `/memory_switch` (REPL restart required).
210
+
201
211
  ## Type checking
202
212
 
203
213
  ```bash
package/README.zh-CN.md CHANGED
@@ -150,6 +150,8 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
150
150
  | `memory_update` | 原地改一条记忆(id 不变;纠正过时事实 / 改摘要 / 改 pin) |
151
151
  | `memory_forget` | 遗忘记忆:默认归档(可复活),`mode=delete` 硬删(pinned 拒删) |
152
152
 
153
+ 5 个 `memory_*` 工具受启动时 `MEMORY_ENABLED=true` 总开关控制;运行时切换用 `/memory_switch`(需重启 REPL,刻意为之,见下「项目记忆」小节区分 Tier-1 / Tier-2)。
154
+
153
155
  ## 斜杠命令
154
156
 
155
157
  | 命令 | 作用 |
@@ -162,6 +164,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
162
164
  | `/resume` | 续接已保存的会话 |
163
165
  | `/rollback` | 菜单选轮次回滚(↑↓ · Enter) |
164
166
  | `/memory` | 看记忆库:条目数 + 近期索引 |
167
+ | `/memory_switch` | 切换 Tier-2 记忆开关(需重启 REPL,刻意为之) |
165
168
  | `/reflect` | 手动触发一次后台记忆反思 pass |
166
169
  | `/model` | 配置大模型(baseURL / apiKey / model / 上下文窗口),即时生效 + 持久化 |
167
170
  | `/init` | 扫描项目生成 `MOCODE.md` 项目记忆(发给 agent 执行) |
@@ -198,6 +201,13 @@ mocode 自动扫描以下目录的 skill(每个 skill 是 `<name>/SKILL.md`,带
198
201
 
199
202
  skill 的 `description` 注入系统提示(渐进式披露第①层),模型只在任务相关时调 `use_skill` 加载完整正文(第②层)。用 `/skills` 查看已发现的 skill。
200
203
 
204
+ ## 项目记忆(MOCODE.md)
205
+
206
+ mocode 的**双层记忆**模型,跟 Skills 是两件事:
207
+
208
+ - **Tier-1 — `MOCODE.md`(每轮自动加载):** Markdown 项目记忆,每轮拼进 system prompt。发现路径:`~/.mocode/MOCODE.md` → 从 cwd 往上逐级 `MOCODE.md`(远→近拼接,近的覆盖更突出);超长截断并标注原始文件。运行 `/init` 生成或刷新,纯 Markdown,可手写,无 schema。agent 自己推得的「下次要记住的事实」(架构/约定/坑位)也写在这里。
209
+ - **Tier-2 — `memory_*` 工具库(agent 主导,需启用):** 离散带标签条目(`decision` / `fact` / `pitfall` / `reference` / `feedback`),按召回计数衰减(30 天 → archived,90 天 → 硬删 GC)。agent 用工具存 / 搜 / 改 / 删;索引(标题)进系统提示(≤50 条),正文按需 `memory_search` 取。默认关,启动 `MEMORY_ENABLED=true` 或 REPL 内 `/memory_switch`(需重启 REPL,刻意为之)。
210
+
201
211
  ## 类型检查
202
212
 
203
213
  ```bash
@@ -7,10 +7,13 @@ import { Spinner } from '../ui/spinner.js';
7
7
  import { summarizeToolCall, summarizeToolResult, truncateDisplay, fmtElapsed, } from '../ui/render.js';
8
8
  import { renderFileChange } from '../ui/diff.js';
9
9
  import * as layout from '../ui/layout.js';
10
+ import * as batch from '../ui/batch.js';
10
11
  import { beginTurn } from '../rollback/index.js';
11
12
  import { config } from '../config/index.js';
12
13
  import { runAgentCore, isMutationTool, } from './core.js';
13
14
  import { createPetHooks } from '../pet/state.js';
15
+ /** 当前 turn 的 batch id(runAgent 内闭包变量;一条 turn 一轮 tool batch 结束即清空)。 */
16
+ let currentBatchId = null;
14
17
  /** 取 userInput 的首行:字符串直接 split;多模态 parts 找首个 text part 再 split。 */
15
18
  function firstLineOf(ui) {
16
19
  if (typeof ui === 'string')
@@ -18,15 +21,22 @@ function firstLineOf(ui) {
18
21
  const first = ui.find((p) => p.type === 'text');
19
22
  return first?.text.split('\n')[0] ?? '';
20
23
  }
21
- /** 工具调用 ● 头:工具名 + 参数摘要(按 tool_calls 原顺序打印,让用户看到本轮跑哪些工具)。 */
24
+ /** 工具调用 ● 头:工具名 + 参数摘要(按 tool_calls 原顺序打印,让用户看到本轮跑哪些工具)。
25
+ * 重构后改为累积到 BatchRenderer,onToolBatchEnd 时统一打摘要行;
26
+ * 展开/折叠由 BatchRenderer + 鼠标 release 决定,本函数不再直接写屏。 */
22
27
  function writeToolHeader(tc) {
23
- const summary = summarizeToolCall(tc.name, tc.arguments);
24
- layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${tc.name}${ui.reset} ${ui.dim}${summary}${ui.reset}\n`);
28
+ if (!currentBatchId)
29
+ currentBatchId = batch.beginBatch();
30
+ batch.recordCall(currentBatchId, tc.name, summarizeToolCall(tc.name, tc.arguments));
25
31
  }
26
- /** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮,仿 Claude Code);其余走一行 preview。 */
32
+ /** 渲染工具结果:mutation 成功走 diff 块(行号 + 语法高亮,仿 Claude Code);其余走一行 preview。
33
+ * 同 writeToolHeader,改为累积到 BatchRenderer(只缓存字符串,不写屏)。 */
27
34
  function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
35
+ if (!currentBatchId)
36
+ return;
37
+ let diff = null;
28
38
  if (isMutationTool(tc.name) && parsed && !output.startsWith('错误')) {
29
- layout.contentWrite(renderFileChange({
39
+ diff = renderFileChange({
30
40
  path: String(parsed.path ?? ''),
31
41
  kind: tc.name === 'edit_file' ? 'edit' : 'write',
32
42
  oldStr: tc.name === 'edit_file'
@@ -34,14 +44,10 @@ function writeToolResult(tc, output, parsed, preWriteOld, editStartLine) {
34
44
  : preWriteOld,
35
45
  newStr: String((tc.name === 'edit_file' ? parsed.new_string : parsed.content) ?? ''),
36
46
  startLine: tc.name === 'edit_file' ? editStartLine : 1,
37
- }));
38
- }
39
- else {
40
- const preview = summarizeToolResult(tc.name, output);
41
- if (preview) {
42
- layout.contentWrite(` ${ui.gray}↳ ${preview}${ui.reset}\n`);
43
- }
47
+ });
44
48
  }
49
+ const preview = diff ? '' : summarizeToolResult(tc.name, output);
50
+ batch.recordResult(currentBatchId, tc.name, preview, diff);
45
51
  }
46
52
  /**
47
53
  * agent 核心循环(主 agent,TUI 渲染版):
@@ -63,6 +69,7 @@ onContextUpdate) {
63
69
  // 开新轮次(回滚用):首行截断 40,供 /rollback 轮次菜单展示。
64
70
  beginTurn(truncateDisplay(firstLineOf(userInput), 40));
65
71
  layout.contentMode(); // 防御性:运行态光标归输入框光标位供 IME 锚定(enterRunningMode 已置,这里兜底)
72
+ currentBatchId = null; // 新 turn 清旧 batch id(防上 turn 残留)
66
73
  // spinner:状态行最前面转圈(思考中 / 生成 / 执行 工具时,状态栏 lead 位显帧 + 文字)。
67
74
  // 经 setStatus 注入状态行(spinnerFrame + statusText),composeStatus 把帧 + 文字放 lead 位;
68
75
  // 不画内容区续写位——内容区在等待期间保持干净,首 token 到达即从续写位开始写正文。
@@ -101,7 +108,16 @@ onContextUpdate) {
101
108
  onToolStart: (name) => spinner.start(`执行 ${name}`),
102
109
  onToolDone: () => spinner.stop(),
103
110
  onToolResult: (tc, output, parsed, preWriteOld, editStartLine) => writeToolResult(tc, output, parsed, preWriteOld, editStartLine),
104
- onToolBatchEnd: () => layout.contentWrite('\n'),
111
+ onToolBatchEnd: () => {
112
+ // 收尾:把累积的 batch 渲染成单行摘要(批内 N 个 tool 调用共用一行,
113
+ // 鼠标点击该行可展开完整明细——见 ui/batch.ts)。无 batch(模型未调工具)则补空行保持间距。
114
+ if (currentBatchId) {
115
+ const id = currentBatchId;
116
+ currentBatchId = null;
117
+ batch.endBatch(id, layout);
118
+ }
119
+ layout.contentWrite('\n');
120
+ },
105
121
  onNoReply: () => layout.contentWrite(`${ui.dim}(无回复)${ui.reset}\n`),
106
122
  onMaxSteps: () => layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}达到最大步数(${config.maxSteps}),本轮停止。${ui.reset}\n`),
107
123
  onAbort: () => {
@@ -109,6 +125,7 @@ onContextUpdate) {
109
125
  if (lastChar && lastChar !== '\n')
110
126
  layout.contentWrite('\n');
111
127
  layout.contentWrite(`${ui.dim}(已中断)${ui.reset}\n`);
128
+ currentBatchId = null; // 丢弃未收尾 batch
112
129
  },
113
130
  onDone: (elapsedMs, usage) => {
114
131
  const tok = formatTurnTokens(usage);
@@ -99,7 +99,7 @@ function buildPlanModeSuffix() {
99
99
  ## ⛯ PLAN MODE (active now)
100
100
  You are in PLAN mode: investigate and design only — do NOT execute or change anything.
101
101
  - Your editing / command tools (write_file, edit_file, run_command) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human) to investigate.
102
- - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. (Codegraph is the default first action for code exploration — see Workflow in the base prompt.)
102
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. (Codegraph is the default first action for code exploration — see Workflow in the base prompt. But first check whether this conversation already covers it — don't re-explore something already retrieved earlier in this session.)
103
103
  - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
104
104
  - When the plan is complete and ready for review, you MUST call the \`ask_human\` tool to surface the plan to the user for approval — do NOT just output the plan as plain text and STOP. ask_human renders a real interactive selection panel inside the TUI; plain-text approval questions in your reply are hard to see and easy to miss.
105
105
  - Pass the \`ask_human\` tool a concise plan summary (goal + files/areas to change + key risks + verification) and these three options so the user can decide in one click:
@@ -115,7 +115,7 @@ You are in PLAN mode: investigate and design only — do NOT execute or change a
115
115
  ## ⛯ PLAN MODE (active now)
116
116
  You are in PLAN mode: investigate and design only — do NOT execute or change anything.
117
117
  - Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
118
- - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. (Codegraph is the default first action for code exploration — see Workflow in the base prompt.)
118
+ - Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. (Codegraph is the default first action for code exploration — see Workflow in the base prompt. But first check whether this conversation already covers it — don't re-explore something already retrieved earlier in this session.)
119
119
  - Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
120
120
  - When the plan is complete and ready for review, you MUST call the \`ask_human\` tool to surface the plan to the user for approval — do NOT just output the plan as plain text and STOP. ask_human renders a real interactive selection panel inside the TUI; plain-text approval questions in your reply are hard to see and easy to miss.
121
121
  - Pass the \`ask_human\` tool a concise plan summary (goal + files/areas to change + key risks + verification) and these three options so the user can decide in one click:
@@ -145,6 +145,9 @@ ${PLATFORM_NOTE}
145
145
 
146
146
  ## Step / Turn Economy (read this first — saves LLM calls)
147
147
  - **Minimize turns**: each user message costs at least one LLM call, and history grows every step until threshold-triggered compact fires (extra call). If a request contains ≥2 independent sub-goals (e.g. "改 X 然后再优化 Y"), ask the user to split them into separate turns rather than chaining both in one go. State this politely: "这条包含 N 个独立目标,建议拆成 N 次对话,以避免上下文膨胀。"
148
+ - **Context check before any call — do this before the batching rule below**: before planning tool calls for this turn, first check whether the current conversation, an earlier tool result, or a file/symbol already read in this session already answers it. If it does, skip the call and answer directly. Only call a tool when the info is genuinely missing, may be stale (the underlying file/state changed since you last read it), or was never retrieved. This applies to every tool — codegraph, grep, web_search, run_command — not just read_file.
149
+ - ✅ already have it: user asks "刚才那个函数在哪个文件" after codegraph_explore returned it two turns ago → answer from that result, no new call.
150
+ - ❌ wasteful: re-running grep/codegraph for a symbol whose location this same conversation already returned, "just to be sure".
148
151
  - **Plan the full turn, then emit it as one batch — this is the single biggest step-saver**: before emitting anything, enumerate every read / edit / command you'll need for this sub-goal, then return them together as one set of tool_calls (reads run in parallel, writes/commands run in the order given). Don't emit one call, observe, then emit the next in a follow-up turn when you could have planned both upfront.
149
152
  - ✅ one turn: \`[read_file A, read_file B, edit_file A, run_command 'npm test']\`
150
153
  - ❌ four turns: \`[read_file A]\` → \`[read_file B]\` → \`[edit_file A]\` → \`[run_command 'npm test']\`
@@ -160,18 +163,17 @@ ${PLATFORM_NOTE}
160
163
  - **Code exploration first action**: before reading files with read_file or searching with grep, check if a .codegraph/ index exists. If it does, use the codegraph tool (explore for questions/features, node for a specific symbol) as your FIRST step — it returns source + call paths in one shot. Only fall back to read_file/grep when codegraph misses, you need just-changed content, or you're editing a known small file. Build the index with \`codegraph init\` if none exists.
161
164
  - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
162
165
  - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
166
+ - **Web search when freshness matters**: for tasks involving UI/interaction/copy/visual design, new SDKs or APIs, CVE/version upgrades, or anything likely past your training cutoff, web_search FIRST to ground your work in current material — don't fall back on stale templates (gradient+emoji defaults, "I hope this message finds you well" openers, generic AI-flavored phrasing). Routine coding (bug fixes, refactors, tests, internal docs) doesn't need it.
163
167
 
164
168
  ## Tool Guidelines
165
169
  - See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
166
170
  - **If the user gave a precise path or symbol, go directly**: read_file or codegraph node it — don't pre-validate with glob/grep.
167
- - Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory.
171
+ - Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory. (Skip if you already read this exact content earlier in this session and nothing has changed it since — see Step Economy above.)
168
172
  - For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
169
- - Use glob to find file paths, grep to search content. **Don't use run_command for file-level checks** (existence / listing / type) — those have no clean cmd.exe equivalent and Windows path escaping fails often. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string). The earlier rule against \`run_command\` for cat/sed/find/grep still applies.
170
- - run_command runs per platform (cmd on Windows, bash elsewhere); state intent before running commands with side effects (deleting files, installing packages, git push, resets, etc.).
171
- - Use web_search for information beyond training data (new versions, news, real-time data, latest APIs); don't answer potentially outdated info from memory.
172
- - Use web_fetch to read a specific URL (a link from search results, or a URL given by the user); it only fetches static HTML — if a JS-rendered page yields no body, switch to web_search (its results include cleaned body text).
173
+ - Use glob to find file paths, grep to search content. **Don't use run_command for file-level checks** (existence / listing / type) — those have no clean cmd.exe equivalent and Windows path escaping fails often. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string).
174
+ - run_command has side effects on the host state intent before invoking (delete, install, push, reset, etc.).
173
175
  - Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) — list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
174
- - **Trim context when stale**: when an old tool result is dead weight (sub-goal done, no downstream consumer, or superseded by a later read), call drop_context to stub it. Otherwise rely on automatic pruning — don't carry stale reads into new sub-goals.
176
+ - **Trim context when stale**: when an old tool result is dead weight (sub-goal done, no downstream consumer, or superseded by a later read), call drop_context to stub it; otherwise rely on automatic pruning.
175
177
  - **Batch writes and commands too, not just reads**: the executor runs ALL returned tool_calls (reads, writes, commands) before the next LLM call. Emit independent edit_file / write_file / run_command in one response when the chain is clear — don't serialize them across turns just because they have side effects. (The read-only batching note in Step Economy applies to writes the same way.)
176
178
  - **Chain shell workflows in a single \`run_command\`**: use \`&&\`, \`;\`, \`|\`, \`>\`, heredocs to fold multi-step scripts (\`mkdir -p x && cat > x/file.ts <<'EOF' ... EOF && npm test\`) into one call. Only emit a follow-up turn when the result forces a decision (error, ambiguous output, branching logic).
177
179
 
@@ -193,9 +195,8 @@ ${PLATFORM_NOTE}
193
195
  ${memorySection}
194
196
 
195
197
  ## Working notepad (todolist) — for multi-step tasks
196
- - For tasks spanning **≥2 independent modules** OR when the user asks for stepwise progress ("先计划再执行" / "plan then do" / "按步骤来"), call \`todolist create\` first to write the plan to \`.mocode/plans/<id>.md\`, then \`todolist update\` to mark progress as you go. For single-file edits or quick lookups, skip it.
197
- - The plan is file-backed (survives context compression; user can see/edit), and the active plan summary is auto-injected into this system prompt each turn. Re-read via \`todolist read\` when unsure of your place.
198
- - See the \`todolist\` tool description for the full action set (create / read / update / add_step / finish / list / unarchive / delete) and lifecycle.
198
+ - For tasks spanning **≥2 independent modules** OR when the user asks for stepwise progress ("先计划再执行" / "plan then do" / "按步骤来"), call \`todolist create\` first; update as you go. Skip for single-file edits or quick lookups.
199
+ - Plan is file-backed (\`todolist read\` to re-orient). See the tool description for the full action set.
199
200
 
200
201
  ## Termination & Reporting
201
202
  - Stop immediately when no more tools are needed; give conclusions directly.
@@ -26,6 +26,9 @@ const KNOWN_TEXT_ONLY_PREFIXES = [
26
26
  'babbage-',
27
27
  'davinci-',
28
28
  'gpt-4o-mini-search', // 搜索专用,无视觉入口
29
+ // MiniMax M2 系列(M2 / M2.1 / M2.5 / M2.7,含各自 -highspeed 变体):纯文本,无视觉输入。
30
+ // 官方文档明确仅 MiniMax-M3 支持 image/video content parts;M2.x 传 image_url 会被拒。
31
+ 'minimax-m2',
29
32
  ];
30
33
  const KNOWN_VISION_FAMILIES = [
31
34
  'gpt-4o',
@@ -54,6 +57,7 @@ const KNOWN_VISION_FAMILIES = [
54
57
  'minicpm-v',
55
58
  'glm-4v',
56
59
  'yi-vl',
60
+ 'minimax-m3', // 官方文档:仅 M3 支持 image_url/video_url content parts
57
61
  ];
58
62
  /** 归一化:小写、去空白;用于前缀比较。 */
59
63
  function normalize(model) {
@@ -11,11 +11,13 @@ import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js'
11
11
  import { bannerString, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
12
12
  import * as layout from '../ui/layout.js';
13
13
  import * as mouse from '../ui/mouse.js';
14
+ import * as batch from '../ui/batch.js';
14
15
  import { promptWithSlashMenu, promptTurnPicker, promptSessionPicker, promptThemePicker, promptRevertChoice, } from '../ui/prompt.js';
15
16
  import { promptIntervention } from '../ui/intervention.js';
16
17
  import { tools } from '../tools/registry.js';
17
18
  import { estimateMessagesTokens, reconfigureClient, } from '../llm/index.js';
18
19
  import { loadImageAttachment, renderChip, MAX_INLINE_BYTES_DEFAULT, } from '../attachments/image.js';
20
+ import { modelSupportsVision } from '../llm/capabilities.js';
19
21
  import { manualCompact, contextState, newSessionId, saveSession, loadSession, listSessions, } from '../session/index.js';
20
22
  import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
21
23
  import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
@@ -63,6 +65,9 @@ const MODEL_PRESETS = [
63
65
  { label: 'GLM(智谱)', baseURL: 'https://open.bigmodel.cn/api/v3', model: 'glm-4.6', window: 128000 },
64
66
  { label: 'DeepSeek', baseURL: 'https://api.deepseek.com', model: 'deepseek-chat', window: 64000 },
65
67
  { label: 'Qwen(阿里)', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model: 'qwen-plus', window: 128000 },
68
+ // MiniMax OpenAI 兼容端点(https://platform.minimax.io/docs/api-reference/text-openai-api)。
69
+ // MiniMax-M3 为唯一支持图片/视频输入的模型;M2 系列纯文本(见 llm/capabilities.ts KNOWN_TEXT_ONLY_PREFIXES)。
70
+ { label: 'MiniMax', baseURL: 'https://api.minimax.io/v1', model: 'MiniMax-M3', window: 1000000 },
66
71
  { label: '本地 Ollama', baseURL: 'http://localhost:11434/v1', model: 'qwen2.5:7b', window: 32768 },
67
72
  { label: '本地 vLLM', baseURL: 'http://localhost:8000/v1', model: 'default', window: 32768 },
68
73
  { label: '自定义 base_url', baseURL: '', model: '', window: 128000 },
@@ -418,19 +423,33 @@ function textOf(c) {
418
423
  }
419
424
  /**
420
425
  * 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文,仿 Claude Code):
421
- * user→❯ 回显、assistant→正文(+ tool_calls )、tool→↳ 结果预览;system 跳过。
426
+ * user→❯ 回显、assistant→正文(+ tool_calls 折叠成摘要行)、tool→↳ 结果预览;system 跳过。
422
427
  * 思考段不持久(history 只存正文),故无思考折叠。渲染后续写位在末尾,紧接 enterInputMode 画输入框。
423
428
  * 内容长于屏时 viewport 显尾(最近轮次),PgUp 可看更早——与流式态一致。
424
429
  * user 多模态:用 textOf 取 text parts;若侧 channel messageAttachments 有原文件名则追加 chip 行
425
430
  * (避免 base64 解码不可逆,旧 session 没侧 channel 时只显文本,文件名 fallback 到 image/* mime)。
431
+ *
432
+ * 折叠策略:遇到 assistant + tool_calls 不立即打 ● 行,而是累积到 batchEntries;
433
+ * 跟随的连续 tool 消息按 tool_call_id 反查填 resultSummary;遇下一个非 tool 消息(或末尾)时,
434
+ * 用 batch.writeSummaryOnly 出单行摘要(与实时 runAgent 同一渲染器,UI 一致)。
435
+ * 回放默认全折叠;用户可鼠标点击摘要行展开(由 BatchRenderer 接管,见 ui/batch.ts)。
426
436
  */
427
437
  export function renderHistory(history) {
428
438
  const idToName = new Map();
439
+ // 当前累积的 batch(assistant.tool_calls + 后续 tool 消息);一旦遇到非 tool 消息即收尾出摘要
440
+ let pendingBatch = [];
441
+ const flushBatch = () => {
442
+ if (pendingBatch.length === 0)
443
+ return;
444
+ batch.writeSummaryOnly(pendingBatch, layout);
445
+ pendingBatch = [];
446
+ };
429
447
  for (let idx = 0; idx < history.length; idx++) {
430
448
  const m = history[idx];
431
449
  if (m.role === 'system')
432
450
  continue;
433
451
  if (m.role === 'user') {
452
+ flushBatch(); // 上一轮 batch(若有)收尾
434
453
  const lines = textOf(m.content).split('\n');
435
454
  layout.contentWrite(formatUserMessage(lines));
436
455
  const atts = messageAttachments.get(idx);
@@ -455,31 +474,53 @@ export function renderHistory(history) {
455
474
  if (m.role === 'assistant') {
456
475
  const text = textOf(m.content);
457
476
  if (text) {
477
+ flushBatch(); // 文本前若有累积 batch 先收尾(罕见:连续两个 assistant tool_calls 文本间)
458
478
  layout.contentWriteMdOnce(text);
459
479
  if (!text.endsWith('\n'))
460
480
  layout.contentWrite('\n');
461
481
  }
462
482
  const tcs = m.tool_calls;
463
- if (Array.isArray(tcs)) {
483
+ if (Array.isArray(tcs) && tcs.length > 0) {
484
+ // 累积到 pendingBatch,顺序 = tool_calls 序
464
485
  for (const tc of tcs) {
465
486
  const name = tc?.function?.name ?? '';
466
487
  const args = tc?.function?.arguments ?? '';
467
488
  if (tc?.id && name)
468
489
  idToName.set(tc.id, name);
469
- layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${name}${ui.reset} ${ui.dim}${summarizeToolCall(name, args)}${ui.reset}\n`);
490
+ pendingBatch.push({
491
+ name,
492
+ callSummary: summarizeToolCall(name, args),
493
+ resultSummary: '',
494
+ diffBlock: null,
495
+ });
470
496
  }
497
+ continue; // 跳过后续 tool 消息处理循环(由下一分支填 result)
471
498
  }
499
+ // 无 tool_calls:若有 pending batch(文本+无 tool_calls 的 assistant),不常见,先收尾
500
+ flushBatch();
472
501
  continue;
473
502
  }
474
503
  if (m.role === 'tool') {
475
504
  const id = m.tool_call_id ?? '';
476
505
  const name = idToName.get(id) ?? '';
477
- const preview = summarizeToolResult(name, textOf(m.content));
478
- if (preview)
479
- layout.contentWrite(` ${ui.gray}↳ ${preview}${ui.reset}\n`);
506
+ const output = textOf(m.content);
507
+ const preview = summarizeToolResult(name, output);
508
+ // 匹配 pendingBatch 中尚未填 result 的同名 entry;同名前缀 tool 较罕见(并行工具同 id 不同名)
509
+ let target;
510
+ for (let i = pendingBatch.length - 1; i >= 0; i--) {
511
+ const e = pendingBatch[i];
512
+ if (e.name === name && !e.resultSummary) {
513
+ target = e;
514
+ break;
515
+ }
516
+ }
517
+ if (target)
518
+ target.resultSummary = preview;
519
+ // 不直接写屏——等 flushBatch 时出单行摘要
480
520
  continue;
481
521
  }
482
522
  }
523
+ flushBatch(); // 末尾兜底
483
524
  }
484
525
  /**
485
526
  * 交互式 REPL:全屏 TUI(alt screen + 固定底栏)。INPUT 态底栏=状态行+输入框(raw mode 等按键);
@@ -674,9 +715,13 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
674
715
  ? input
675
716
  : [
676
717
  { type: 'text', text: input },
718
+ // detail 故意不设(留 undefined,JSON.stringify 时被丢弃):OpenAI 认 'auto'/'low'/'high',
719
+ // 但 MiniMax 只认 'low'/'default'/'high'——'auto' 不是合法枚举值,某些后端会 400。
720
+ // 不传 detail 让各 provider 用自己的默认值(OpenAI 默认视为 auto,MiniMax 默认 default),
721
+ // 是唯一在两边都不出错的写法。
677
722
  ...imgs.map((a) => ({
678
723
  type: 'image_url',
679
- image_url: { url: a.dataUrl, detail: 'auto' },
724
+ image_url: { url: a.dataUrl },
680
725
  })),
681
726
  ];
682
727
  const msgIndex = history.length; // runAgent push 后 = 这个 index
@@ -939,6 +984,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
939
984
  pendingAttachments.push(r.att);
940
985
  }
941
986
  layout.contentWrite(` ${ui.dim}${renderChip(r.att)} — will attach to next message${ui.reset}\n`);
987
+ // 提前警告(不阻断附加):当前模型已知不支持视觉(如 MiniMax M2.x / gpt-3.5 等)时,
988
+ // 附加时就提示,而不是等发送后才在 catch 块里翻译 API 报错——减少一轮无意义请求。
989
+ if (!modelSupportsVision(config.model)) {
990
+ layout.contentWrite(` ${ui.yellow}⚠ 当前模型 ${config.model} 已知不支持视觉输入,发送图片可能会失败。可用 /model 切换。${ui.reset}\n`);
991
+ }
942
992
  continue;
943
993
  }
944
994
  if (line === '/context') {
@@ -79,11 +79,12 @@ export function coerceOptions(raw) {
79
79
  export const askHumanTool = {
80
80
  name: 'ask_human',
81
81
  description: [
82
- 'Ask the user for input at a decision point (blocks until they respond).',
83
- ' Use when: multiple approaches need a user decision, intent is unclear, or extra info is needed.',
84
- ' Don\'t call when the task is clear and you can decide it interrupts the user.',
85
- ' Options (2-6) let the user pick; omit/empty for free-text input. Their answer is returned as the result.',
86
- ].join(''),
82
+ 'Present the user with a menu of choices to pick from not a generic "ask" tool.',
83
+ ' DEFAULT: pass 2–6 concrete options via `options`; the user picks one and the pick comes back.',
84
+ ' FREE-TEXT (omit `options`): only when the answer truly cannot be reduced to a few choices',
85
+ ' (e.g. "paste the error message", "enter the exact URL") this blocks with a text input.',
86
+ ' DO NOT call when the task is clear and you can pick a sensible default — decide and proceed.',
87
+ ].join(' '),
87
88
  parameters: {
88
89
  type: 'object',
89
90
  properties: {
@@ -94,7 +95,7 @@ export const askHumanTool = {
94
95
  options: {
95
96
  type: 'array',
96
97
  items: { type: 'string' },
97
- description: 'Options for the user to choose from (2~6). May be omitted when omitted or empty, it becomes free-text input instead',
98
+ description: '2–6 concrete choices the user can pick with one click. Required in most cases; omit only when free-form text is genuinely needed.',
98
99
  },
99
100
  context: {
100
101
  type: 'string',
@@ -53,7 +53,7 @@ export const codegraphTool = {
53
53
  description: 'FIRST CHOICE for understanding/locating code, tracing call chains, or assessing the impact of changes — use this before read_file/grep when a .codegraph/ index exists.' +
54
54
  ' Returns symbol source + call paths in one shot — more accurate and economical than piecing together read_file/grep.' +
55
55
  ' When to use: starting any code exploration; locating a symbol; understanding how a feature works; seeing what calls a function or what a change affects.' +
56
- ' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file.' +
56
+ ' When NOT to use: no .codegraph/ index (build it first with `codegraph init`); reading a file you just edited; editing a single known small file — in those cases go straight to read_file/edit_file; or the symbol/area was already retrieved earlier in this session — reuse that result instead of calling again.' +
57
57
  ' Fallback: if codegraph misses or the result is incomplete, then use read_file/grep/glob to fill the gaps.',
58
58
  parameters: {
59
59
  type: 'object',