mocode-ai 0.5.10 → 0.6.1

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.
@@ -140,9 +140,6 @@ export async function runAgentCore(opts) {
140
140
  const { history, userInput, signal, onContextUpdate, hooks, skipRollback } = opts;
141
141
  const runtimeContextState = opts.contextState ?? contextState;
142
142
  const maxSteps = opts.maxSteps ?? config.maxSteps;
143
- // 中断回滚快照:入口(本 turn push 任何消息前)整段浅拷贝。abort 时 length=0;push(...saved) 还原。
144
- // 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
145
- const savedHistory = history.slice();
146
143
  // 中断还原:LLM 中途可能调 switch_mode 切了模式,abort 时连同模式一起还原回轮首。
147
144
  const savedMode = getAgentMode();
148
145
  // 本轮计时:从入口到完毕(正常 return / 达上限),供 finally 打 ✻ Worked for 摘要行。
@@ -174,6 +171,11 @@ export async function runAgentCore(opts) {
174
171
  return thrashHint(name, args, c);
175
172
  };
176
173
  history.push({ role: 'user', content: userInput });
174
+ // 中断回滚快照:push 用户消息后整段浅拷贝。abort 时 length=0;push(...saved) 还原。
175
+ // 这样中断时至少保留用户消息(及之前的历史);每步工具全部执行完毕后刷新快照,
176
+ // 保留已完成的 assistant+tool_calls+tool 结果,只丢弃当前未完成步骤的消息。
177
+ // 用 slice() 而非 length:maybeCompact 会原地重建(length=0;push(...rebuilt)),savedLen 会失效。
178
+ let savedHistory = history.slice();
177
179
  // drop_context 工具的上下文剔除回调:闭包捕获 history,原地剔除无关旧 tool 结果。
178
180
  // 保护由 dropContextFromHistory 内部保证:history[0](system)+ 当前轮(最后 user 及其后)永不剔除。
179
181
  // 子 agent 也在自己的 history 上操作(子 agent 独立 history);skipRollback 不影响此行为。
@@ -454,6 +456,9 @@ export async function runAgentCore(opts) {
454
456
  // 工具步末尾补一空行:与下一轮的思考 / 正文分隔(否则 ↳ 后紧接 ▎ 思考,无空行不好看;
455
457
  // 与正文→● 的 1 空行对称)。工具结果已以 \n 收尾,此处再补 \n 恰好 1 空行。
456
458
  hooks.onToolBatchEnd?.();
459
+ // 刷新中断快照:工具全部执行完毕后,history 处于一致状态(assistant+tool_calls+tool 结果完整),
460
+ // 此时中断可安全保留这些已完成的消息,只丢弃下一轮未完成的 chat() 响应。
461
+ savedHistory = history.slice();
457
462
  continue; // 带着工具结果再调一次 LLM
458
463
  }
459
464
  if (mode !== 'idle' && lastChar !== '\n')
@@ -216,7 +216,7 @@ ${PLATFORM_NOTE}
216
216
  - Batch only independent read-only calls. After their results arrive, make the dependent edit in the next turn; then batch independent edits and one final verification when their exact inputs are already known.
217
217
  - Read only what supports the next decision; verify once after a related edit set, not after every edit.
218
218
  - Do not repeat an unchanged failing call; after three unproductive attempts, change tools or ask for the missing decision.
219
- - **Don't re-read a file you already have, unless it may have changed**: if you (or an earlier step in this session) already read a file's relevant content and nothing has touched it since, edit directly from that content instead of calling read_file again "to be safe". This does NOT apply when the file was edited (by you or externally) since your last read, when a prior edit may have shifted line numbers you're about to target, or right after a compact where you're unsure the surviving context is accurate in those cases re-reading is expected and correct, not wasteful.
219
+ - For \`edit_file\`, derive \`old_string\` by copying the exact relevant lines from the latest successful \`read_file\` of that same path; never reconstruct it from memory, a summary, grep output, or a previous diff. That read becomes stale after any edit/write to the path, compaction/resume, or a possible external change. On an \`old_string\` mismatch, re-read the exact region and retry once with the newly returned text; never retry identical arguments.
220
220
 
221
221
  ## Workflow
222
222
  - Understand requirements and current code before acting; do not guess.
@@ -226,7 +226,7 @@ ${PLATFORM_NOTE}
226
226
 
227
227
  ## Tool rules
228
228
  - Precise path/symbol → go directly to \`read_file\` or \`codegraph node\`; use \`glob\`/\`grep\` only for discovery.
229
- - Before editing, confirm the relevant content unless it is already current in this conversation. Use \`edit_file\` for unique local replacements and \`write_file\` for new/full files.
229
+ - Before editing, read the exact target region and use its verbatim text as \`old_string\`. Use \`edit_file\` for unique local replacements and \`write_file\` for new/full files.
230
230
  - Local edits require an exact unique match; use \`write_file\` for new/full files.
231
231
  - Use \`glob\`/\`grep\` for discovery and \`run_command\` for execution or verification, not file existence checks. State intent before side effects.
232
232
  - Call \`ask_human\` only when a real user decision is required; otherwise decide and proceed.
@@ -8,7 +8,7 @@ import { runAgent } from '../agent/index.js';
8
8
  import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
9
9
  import { togglePet, killPetProcess, listSkins, setSkin, sendState } from '../pet/bridge.js';
10
10
  import { setSandboxRoot } from '../sandbox/root.js';
11
- import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js';
11
+ import { ui, setTheme, getTheme, listThemes, themeExists, applyTerminalBackground } from '../ui/theme.js';
12
12
  import { bannerLines, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
13
13
  import * as layout from '../ui/layout.js';
14
14
  import * as mouse from '../ui/mouse.js';
@@ -1310,6 +1310,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1310
1310
  // 切:setTheme → 重算状态行(新色)→ 清内容重绘(历史 / 横幅,镜像启动 + /resume)→ 确认 → 持久化。
1311
1311
  // markdown MEMO 按 themeVersion 自动失效,故 renderHistory 取新色;状态栏 / 输入框由 continue 回 INPUT 态时读 getter 刷。
1312
1312
  setTheme(name);
1313
+ applyTerminalBackground();
1313
1314
  refreshStatusBase(history);
1314
1315
  layout.clearContent();
1315
1316
  if (history.some((m) => m.role === 'user')) {
@@ -88,8 +88,9 @@ export function loadSession(id) {
88
88
  }
89
89
  /** 列出最近会话,按 createdAt 降序。损坏文件跳过。
90
90
  * - limit?: 仅返回前 N 条。会话目录名是 YYYYMMDD-HHmmss,字典序=时间序;
91
- * 先按目录名降序取前 N,再解析 session.json,避免
92
- * /resume 在 sessions 目录堆了几百个子目录时 readdirSync + 全量 JSON.parse 慢。
91
+ * 按目录名降序逐个解析,收集到 N 个有效会话就停止,避免 /resume 在
92
+ * sessions 目录堆了几百个子目录时全量 JSON.parse;同时不让只有笔记或
93
+ * 快照、没有 session.json 的目录占掉最近 N 条的名额。
93
94
  * - 不传 limit 时读全部(向后兼容,供裸 --resume 列全表用)。
94
95
  * - 向后兼容:同时扫描旧式 <id>.json 文件(扁平结构),优先读新式目录。
95
96
  */
@@ -108,9 +109,11 @@ export function listSessions(limit) {
108
109
  }
109
110
  }
110
111
  const all = ids.sort().reverse(); // 降序:最新在前
111
- const toRead = typeof limit === 'number' ? all.slice(0, Math.max(0, limit)) : all;
112
+ const maxResults = typeof limit === 'number' ? Math.max(0, limit) : Infinity;
112
113
  const out = [];
113
- for (const id of toRead) {
114
+ for (const id of all) {
115
+ if (out.length >= maxResults)
116
+ break;
114
117
  try {
115
118
  // 优先新式目录,回退旧式文件
116
119
  const newPath = path.join(config.sessionDir, id, 'session.json');
@@ -3,7 +3,7 @@ import { resolve } from 'node:path';
3
3
  // ---------- edit_file ----------
4
4
  export const editFileTool = {
5
5
  name: 'edit_file',
6
- description: 'Replace a string in a file. old_string must occur exactly once and match exactly (including indentation/newlines). Use write_file for new files.',
6
+ description: 'Replace a string in a file. old_string must occur exactly once and match exactly (including indentation/newlines). Copy old_string verbatim from a fresh read_file result for this path; do not reconstruct it from memory or summaries. Use write_file for new files.',
7
7
  risk: 'confirm',
8
8
  parameters: {
9
9
  type: 'object',
@@ -29,7 +29,7 @@ export const editFileTool = {
29
29
  const normNew = newStr.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
30
30
  const count = norm.split(normOld).length - 1;
31
31
  if (count === 0) {
32
- return `错误:在 ${path} 中未找到 old_string。请先 read_file 确认实际内容。`;
32
+ return `错误:在 ${path} 中未找到 old_string。不要重试相同参数;请先 read_file 读取目标区域,再从返回内容逐字复制新的 old_string 后重试。`;
33
33
  }
34
34
  if (count > 1) {
35
35
  return `错误:old_string 在 ${path} 中出现 ${count} 次,不唯一。请加入更多上下文使其唯一。`;
package/dist/ui/layout.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { stdin, stdout } from 'node:process';
2
2
  import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncateAnsi, ansiDisplayWidth, wrapByDisplayWidth, fmtElapsed, stripAnsi, sliceByDisplayCol, } from './render.js';
3
- import { ui } from './theme.js';
3
+ import { ui, applyTerminalBackground, resetTerminalBackground } from './theme.js';
4
4
  import * as content from './content.js';
5
5
  import * as mouse from './mouse.js';
6
6
  import { shiftBatchesAfter } from './batch.js';
@@ -1886,6 +1886,7 @@ export function enterAltScreen() {
1886
1886
  return;
1887
1887
  active = true;
1888
1888
  stdout.write(esc.altOn);
1889
+ applyTerminalBackground();
1889
1890
  stdout.write(esc.mouseOn); // 完整鼠标追踪(按下/拖动/释放/滚轮)→ mouse.swallow 重组 → handleMouseEvent
1890
1891
  mouse.setHandler(handleMouseEvent);
1891
1892
  setRegion(6); // 1 虚拟空 + 1 spinner行 + 1 上线 + 1 输入 + 1 下线 + 1 model行(两行式底栏)
@@ -1943,6 +1944,7 @@ export function exitAltScreen() {
1943
1944
  if (!active)
1944
1945
  return;
1945
1946
  active = false;
1947
+ resetTerminalBackground();
1946
1948
  stopTurnTimer(); // 兜底清走时计时器(防异常退出泄漏)
1947
1949
  turnStart = null;
1948
1950
  scrollLockUntil = 0; // 清轮首滚动锁(防状态泄漏到下次进 alt 屏)
package/dist/ui/theme.js CHANGED
@@ -33,6 +33,7 @@ const DEFAULT = {
33
33
  // diff 行底色:One Dark bg #282c34 上加 ~14% 亮度的对应色,够辨识但不刺眼
34
34
  addBg: '\x1B[48;2;44;62;42m', // 偏暗绿(One Dark green(152,195,121)暗化)
35
35
  delBg: '\x1B[48;2;62;38;42m', // 偏暗红(One Dark red(224,108,117)暗化)
36
+ terminalBg: 'rgb:28/2c/34', // One Dark 经典深底,偏蓝灰的黑
36
37
  };
37
38
  /**
38
39
  * 内建主题表。default=One Dark、light=Solarized Light、solarized=Solarized Dark、
@@ -58,6 +59,7 @@ const THEMES = {
58
59
  // 比直接用 base1 更柔,跟深 fg(red/green)对比充足
59
60
  addBg: '\x1B[48;2;220;235;205m',
60
61
  delBg: '\x1B[48;2;245;218;215m',
62
+ terminalBg: 'rgb:1a/1e/24', // 深黑底带微蓝,与 light 主题文字反差大
61
63
  },
62
64
  solarized: {
63
65
  red: '\x1B[38;2;220;50;47m',
@@ -74,6 +76,7 @@ const THEMES = {
74
76
  // diff 行底色:Solarized Dark base03(0,43,54)上加对应色族暗 tint
75
77
  addBg: '\x1B[48;2;20;50;38m',
76
78
  delBg: '\x1B[48;2;55;30;30m',
79
+ terminalBg: 'rgb:00/2b/36', // Solarized Dark base03,深青黑
77
80
  },
78
81
  gruvbox: {
79
82
  red: '\x1B[38;2;251;73;52m',
@@ -90,6 +93,7 @@ const THEMES = {
90
93
  // diff 行底色:Gruvbox dark bg(40,40,40)上贴暗 bg0_a 风格
91
94
  addBg: '\x1B[48;2;40;55;30m',
92
95
  delBg: '\x1B[48;2;70;35;30m',
96
+ terminalBg: 'rgb:28/28/28', // Gruvbox 深黑底,带微暖色调
93
97
  },
94
98
  nord: {
95
99
  red: '\x1B[38;2;191;97;106m',
@@ -106,6 +110,7 @@ const THEMES = {
106
110
  // diff 行底色:Nord polar night(46,52,64)上贴对应色族暗 tint
107
111
  addBg: '\x1B[48;2;46;66;52m',
108
112
  delBg: '\x1B[48;2;72;46;52m',
113
+ terminalBg: 'rgb:2e/34/40', // Nord Polar Night 深灰蓝
109
114
  },
110
115
  orange: {
111
116
  // 秋季/南瓜橙:深暖底(#231c16 系)+ accent 主强调走橙色;yellow 槽也偏橙
@@ -125,6 +130,7 @@ const THEMES = {
125
130
  // diff 行底色:暖深棕底上贴对应色族暗 tint,跟 fg 配对柔和可辨
126
131
  addBg: '\x1B[48;2;55;70;35m',
127
132
  delBg: '\x1B[48;2;78;42;32m',
133
+ terminalBg: 'rgb:23/1c/16', // 深暖棕黑,带橙色调
128
134
  },
129
135
  rose: {
130
136
  // 玫红:深紫底 + accent 玫粉作主强调;magenta 槽与 accent 同源(主题色),
@@ -143,6 +149,7 @@ const THEMES = {
143
149
  // diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏紫红
144
150
  addBg: '\x1B[48;2;50;60;42m',
145
151
  delBg: '\x1B[48;2;75;40;52m',
152
+ terminalBg: 'rgb:1e/14/1c', // 深紫黑底,带玫红暖调
146
153
  },
147
154
  emerald: {
148
155
  // 翡翠绿:深绿底 + accent 翡翠绿作主强调;green 槽与 accent 同源(主题色),
@@ -161,6 +168,7 @@ const THEMES = {
161
168
  // diff 行底色:深绿底贴对应色族暗 tint,绿暗化偏深绿、红暗化偏暗红
162
169
  addBg: '\x1B[48;2;30;55;40m',
163
170
  delBg: '\x1B[48;2;60;40;40m',
171
+ terminalBg: 'rgb:10/1e/18', // 深绿黑底,翡翠色调
164
172
  },
165
173
  amber: {
166
174
  // 琥珀金黄:深棕底 + accent 琥珀金黄作主强调;yellow 槽与 accent 同源(主题色),
@@ -179,6 +187,7 @@ const THEMES = {
179
187
  // diff 行底色:深棕底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
180
188
  addBg: '\x1B[48;2;50;55;25m',
181
189
  delBg: '\x1B[48;2;70;40;28m',
190
+ terminalBg: 'rgb:1e/18/0e', // 深棕黑底,琥珀金黄调
182
191
  },
183
192
  lavender: {
184
193
  // 薰衣草淡紫:深紫底 + accent 淡紫作主强调;magenta 槽与 accent 同源(主题色),
@@ -197,6 +206,7 @@ const THEMES = {
197
206
  // diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏冷绿、红暗化偏冷紫红
198
207
  addBg: '\x1B[48;2;38;46;42m',
199
208
  delBg: '\x1B[48;2;60;40;55m',
209
+ terminalBg: 'rgb:18/14/24', // 深紫黑底,薰衣草冷调
200
210
  },
201
211
  sunset: {
202
212
  // 日落珊瑚:深棕红底 + accent 珊瑚红作主强调;red 槽与 accent 同源(主题色);
@@ -215,6 +225,7 @@ const THEMES = {
215
225
  // diff 行底色:深棕红底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
216
226
  addBg: '\x1B[48;2;50;55;30m',
217
227
  delBg: '\x1B[48;2;75;32;30m',
228
+ terminalBg: 'rgb:20/12/10', // 深棕红黑底,珊瑚暖调
218
229
  },
219
230
  };
220
231
  let currentName = 'default';
@@ -222,6 +233,18 @@ let version = 0;
222
233
  function currentPalette() {
223
234
  return THEMES[currentName] ?? DEFAULT;
224
235
  }
236
+ /** Apply the active theme's dark background to the terminal window (OSC 11). */
237
+ export function applyTerminalBackground() {
238
+ if (!isTTY)
239
+ return;
240
+ stdout.write(`\x1B]11;${currentPalette().terminalBg}\x07`);
241
+ }
242
+ /** Ask the terminal to restore its default background color. */
243
+ export function resetTerminalBackground() {
244
+ if (!isTTY)
245
+ return;
246
+ stdout.write('\x1B]111\x07');
247
+ }
225
248
  /** 取某颜色字段的当前 ANSI 码(经 isTTY 门控)。 */
226
249
  function color(key) {
227
250
  return wrap(currentPalette()[key] ?? DEFAULT[key]);
@@ -0,0 +1,80 @@
1
+ import { renderMarkdown } from './markdown.js';
2
+ import { ui } from './theme.js';
3
+ const records = new Map();
4
+ const absLineToId = new Map();
5
+ let liveRaw = '';
6
+ let nextId = 0;
7
+ /** 流式写入推理;首次写入会结束此前的正文 Markdown 段,确保两者能独立折叠。 */
8
+ export function writeThinking(delta, layout) {
9
+ if (!delta)
10
+ return;
11
+ if (!liveRaw)
12
+ layout.beginMarkdownSegment();
13
+ liveRaw += delta;
14
+ layout.contentWriteMd(delta);
15
+ }
16
+ /** 推理流结束:将实时 Markdown 段替换为可点击的一行摘要。 */
17
+ export function finishThinking(layout) {
18
+ if (!liveRaw)
19
+ return;
20
+ const raw = liveRaw;
21
+ liveRaw = '';
22
+ const lineCount = raw.split('\n').length;
23
+ const summary = ` ${ui.bold}${ui.accent}▸${ui.reset} ${ui.dim}Thought · ${lineCount} lines (click to expand)${ui.reset}`;
24
+ const summaryAbsIdx = layout.collapseMarkdownSegment(summary);
25
+ if (summaryAbsIdx == null)
26
+ return;
27
+ const id = `t${++nextId}`;
28
+ records.set(id, { id, summaryAbsIdx, raw, expanded: false, expandedLineCount: 0 });
29
+ absLineToId.set(summaryAbsIdx, id);
30
+ }
31
+ /** 返回指定缓冲绝对行命中的推理记录;非摘要行返回 null。 */
32
+ export function findThinkingByAbsLine(absLine) {
33
+ return absLineToId.get(absLine) ?? null;
34
+ }
35
+ /** 切换摘要行下的完整推理内容。 */
36
+ export function toggleThinking(id, layout) {
37
+ const record = records.get(id);
38
+ if (!record)
39
+ return;
40
+ if (record.expanded) {
41
+ layout.contentDeleteFrom(record.summaryAbsIdx + 1, record.expandedLineCount);
42
+ record.expanded = false;
43
+ record.expandedLineCount = 0;
44
+ return;
45
+ }
46
+ const rendered = renderMarkdown(record.raw, layout.contentCols());
47
+ const maxLines = 200;
48
+ const lines = rendered.slice(0, maxLines);
49
+ if (rendered.length > maxLines) {
50
+ lines.push(`${ui.dim} … (${rendered.length - maxLines} more lines)${ui.reset}`);
51
+ }
52
+ if (lines.length === 0)
53
+ return;
54
+ layout.contentInsertAfter(record.summaryAbsIdx, lines);
55
+ record.expanded = true;
56
+ record.expandedLineCount = lines.length;
57
+ }
58
+ /** 缓冲中段插删后同步摘要绝对行索引。 */
59
+ export function shiftThinkingsAfter(absIdx, delta) {
60
+ if (delta === 0)
61
+ return;
62
+ const next = new Map();
63
+ for (const [idx, id] of absLineToId) {
64
+ const shifted = idx > absIdx ? idx + delta : idx;
65
+ if (shifted >= 0)
66
+ next.set(shifted, id);
67
+ const record = records.get(id);
68
+ if (record && idx > absIdx)
69
+ record.summaryAbsIdx = shifted;
70
+ }
71
+ absLineToId.clear();
72
+ for (const [idx, id] of next)
73
+ absLineToId.set(idx, id);
74
+ }
75
+ /** 内容区整体清空时清掉折叠状态与尚未结束的流。 */
76
+ export function resetThinkings() {
77
+ records.clear();
78
+ absLineToId.clear();
79
+ liveRaw = '';
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.5.10",
3
+ "version": "0.6.1",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {