mocode-ai 1.6.4 → 1.6.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.
Files changed (64) hide show
  1. package/README.md +376 -365
  2. package/README.zh-CN.md +13 -2
  3. package/dist/agent/model-turn.js +14 -1
  4. package/dist/agent/run-coordinator.js +5 -0
  5. package/dist/agent/spawn.js +38 -5
  6. package/dist/agent/stages/model-runner.js +1 -1
  7. package/dist/agent/stages/tool-dispatcher.js +1 -0
  8. package/dist/agent/work-discipline.js +3 -1
  9. package/dist/config/index.js +52 -2
  10. package/dist/config/presets.js +40 -1
  11. package/dist/config/profiles.js +9 -1
  12. package/dist/context/clearing.js +53 -0
  13. package/dist/context/relevance.js +6 -0
  14. package/dist/context/text-search.js +113 -0
  15. package/dist/llm/index.js +35 -8
  16. package/dist/llm/providers/anthropic.js +14 -4
  17. package/dist/llm/reasoning.js +141 -0
  18. package/dist/memory/reflect.js +1 -1
  19. package/dist/memory/store.js +31 -28
  20. package/dist/models/budgets.js +11 -0
  21. package/dist/models/catalog.js +130 -0
  22. package/dist/models/map-preset.js +50 -0
  23. package/dist/models/model-panel.js +227 -0
  24. package/dist/models/protocol.js +44 -0
  25. package/dist/models/reasoning-cap.js +88 -0
  26. package/dist/models/search.js +58 -0
  27. package/dist/models/types.js +8 -0
  28. package/dist/repl/commands/effort.js +81 -0
  29. package/dist/repl/commands/model-actions.js +138 -0
  30. package/dist/repl/commands/model-catalog.js +67 -0
  31. package/dist/repl/commands/model-panel-ui.js +107 -0
  32. package/dist/repl/commands/model.js +17 -0
  33. package/dist/repl/commands/registry.js +6 -0
  34. package/dist/repl/commands/session.js +54 -0
  35. package/dist/repl/commands/stats.js +28 -0
  36. package/dist/repl/commands.js +3 -0
  37. package/dist/repl/runtime.js +5 -1
  38. package/dist/repl/status-bar.js +4 -2
  39. package/dist/rollback/store.js +17 -0
  40. package/dist/runtime/runtime.js +31 -1
  41. package/dist/session/compact.js +84 -3
  42. package/dist/session/index.js +2 -0
  43. package/dist/session/retention.js +297 -0
  44. package/dist/session/scheduler.js +30 -3
  45. package/dist/session/store.js +108 -6
  46. package/dist/session/usage-stats.js +90 -0
  47. package/dist/skills/activation.js +2 -0
  48. package/dist/skills/builtin-skills.js +153 -0
  49. package/dist/skills/discover.js +3 -1
  50. package/dist/skills/runner.js +4 -1
  51. package/dist/tools/builtins/index.js +4 -0
  52. package/dist/tools/builtins/read-file.js +19 -1
  53. package/dist/tools/builtins/session-search.js +33 -0
  54. package/dist/tools/builtins/task.js +2 -0
  55. package/dist/tools/constants.js +8 -0
  56. package/dist/tools/read-dedup.js +62 -0
  57. package/dist/tools/tool-runtime.js +1 -0
  58. package/dist/ui/batch.js +109 -103
  59. package/dist/ui/fuzzy-picker.js +227 -0
  60. package/dist/ui/hierarchical-picker.js +302 -0
  61. package/dist/ui/intervention.js +11 -1
  62. package/dist/ui/layout-internal/statusbar.js +18 -7
  63. package/dist/ui/render.js +2 -0
  64. package/package.json +1 -1
@@ -0,0 +1,302 @@
1
+ /**
2
+ * 层级模糊选择器(#模型命令交互·两级浏览)。
3
+ *
4
+ * 一个按层级浏览的选择器(厂商 → 模型),搜索栏是**作用域内过滤**,不跨层:
5
+ * - 顶层(厂商列表):打字只匹配**厂商名**,Enter 钻入选中厂商。
6
+ * - 进入厂商后:打字只匹配**该厂商下的模型名**,Enter 确认模型;Backspace 回厂商。
7
+ * 不会把模型和厂商混在一个搜索结果里。
8
+ *
9
+ * 交互契约:
10
+ * - 可打印字符 追加到过滤词,实时过滤当前层级
11
+ * - ↑/↓ 移动高亮(循环;跳过禁用项)
12
+ * - Enter 分支钻入 / 叶子确认
13
+ * - Backspace 编辑过滤词;词为空时退回上一层
14
+ * - Esc 先清过滤词 → 再退回上一层 → 顶层则取消
15
+ * - Ctrl+C 取消,返回 null
16
+ * - Ctrl+U 清空过滤词
17
+ */
18
+ import { stdin } from 'node:process';
19
+ import { ui } from './theme.js';
20
+ import { displayWidth, truncateDisplay } from './render.js';
21
+ import * as layout from './layout.js';
22
+ import { fuzzyMatch } from './fuzzy.js';
23
+ /** 深度优先拍平整棵树(含分支与叶子)。纯函数,导出供单测/外部复用。 */
24
+ export function flattenTree(roots) {
25
+ const out = [];
26
+ const walk = (nodes, path, ancestorLabels) => {
27
+ nodes.forEach((node, i) => {
28
+ const here = [...path, i];
29
+ out.push({ node, path: here, crumb: ancestorLabels.join(' › ') });
30
+ if (node.children && node.children.length) {
31
+ walk(node.children, here, [...ancestorLabels, node.label]);
32
+ }
33
+ });
34
+ };
35
+ walk(roots, [], []);
36
+ return out;
37
+ }
38
+ export async function promptHierarchicalPicker(opts) {
39
+ if (!layout.isActive())
40
+ return null;
41
+ const maxVisible = opts.maxVisible ?? 12;
42
+ const emitter = stdin;
43
+ let query = '';
44
+ let cursor = 0;
45
+ let path = [];
46
+ let cands = [];
47
+ let selected = 0;
48
+ let resolved = false;
49
+ let resolve;
50
+ /** 沿 path 取当前层级节点列表。 */
51
+ function currentNodes() {
52
+ let nodes = opts.roots;
53
+ for (const idx of path) {
54
+ const next = nodes[idx]?.children;
55
+ if (!next)
56
+ return [];
57
+ nodes = next;
58
+ }
59
+ return nodes;
60
+ }
61
+ function enabledIndices() {
62
+ const out = [];
63
+ cands.forEach((c, i) => {
64
+ if (!c.node.disabled)
65
+ out.push(i);
66
+ });
67
+ return out;
68
+ }
69
+ function clampSelected() {
70
+ const en = enabledIndices();
71
+ if (en.length === 0) {
72
+ selected = 0;
73
+ return;
74
+ }
75
+ if (!en.includes(selected))
76
+ selected = en[0];
77
+ }
78
+ /** 只过滤当前层级:空 query 全显示;有 query 在本层节点 searchText 上模糊匹配。 */
79
+ function recompute() {
80
+ const q = query.trim();
81
+ const enabled = [];
82
+ const disabled = [];
83
+ currentNodes().forEach((node, i) => {
84
+ let m = { score: 0 };
85
+ if (q) {
86
+ m = fuzzyMatch(q, node.searchText);
87
+ if (!m)
88
+ return;
89
+ }
90
+ const c = { node, score: m.score };
91
+ if (node.children && node.children.length)
92
+ c.enterPath = [...path, i];
93
+ else
94
+ c.value = node.value;
95
+ (node.disabled ? disabled : enabled).push(c);
96
+ });
97
+ if (q)
98
+ enabled.sort((a, b) => b.score - a.score);
99
+ cands = [...enabled, ...disabled];
100
+ clampSelected();
101
+ }
102
+ function titleLine() {
103
+ const parts = [opts.title];
104
+ let nodes = opts.roots;
105
+ for (const idx of path) {
106
+ parts.push(nodes[idx]?.label ?? '');
107
+ nodes = nodes[idx]?.children ?? [];
108
+ }
109
+ return parts.join(' › ');
110
+ }
111
+ /** 随层级变化的操作提示。 */
112
+ function hintLine() {
113
+ return path.length === 0
114
+ ? '打字搜索厂商 · ↑↓ 选择 · Enter 进入厂商 · Esc 退出'
115
+ : '打字搜索模型 · ↑↓ 选择 · Enter 确认 · Backspace 返回厂商列表';
116
+ }
117
+ function menuLines() {
118
+ const cols = layout.getGeo().cols;
119
+ const lines = [`${ui.bold}${truncateDisplay(titleLine(), cols)}${ui.reset}`];
120
+ lines.push(`${ui.dim}${truncateDisplay(hintLine(), cols)}${ui.reset}`);
121
+ const en = enabledIndices();
122
+ if (en.length === 0) {
123
+ lines.push(`${ui.dim}${query.trim() ? '无匹配,按 Esc 返回' : '(空列表)'}${ui.reset}`);
124
+ return lines;
125
+ }
126
+ const visibleCount = Math.min(maxVisible, en.length);
127
+ const selPos = en.indexOf(selected);
128
+ const page = Math.floor(selPos / visibleCount);
129
+ const winEn = en.slice(page * visibleCount, page * visibleCount + visibleCount);
130
+ const hasMore = page * visibleCount + visibleCount < en.length;
131
+ const hasAbove = page > 0;
132
+ winEn.forEach((idx, i) => {
133
+ const c = cands[idx];
134
+ const isSel = idx === selected;
135
+ const isBranch = !!c.enterPath;
136
+ const color = isSel ? `${ui.accent}${ui.bold}` : '';
137
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
138
+ let scrollHint = '';
139
+ if (i === 0 && hasAbove)
140
+ scrollHint = ' ▲';
141
+ if (i === winEn.length - 1 && hasMore)
142
+ scrollHint = ' ▼';
143
+ // 分支尾部提示「进入」;叶子 detail 原样。
144
+ const detail = [c.node.detail, isBranch ? '进入 →' : ''].filter(Boolean).join(' · ');
145
+ const detailStr = detail ? ` ${ui.dim}${detail}${ui.reset}` : '';
146
+ const budget = cols - 4 - scrollHint.length - (detail ? displayWidth(detail) + 2 : 0);
147
+ const body = truncateDisplay(c.node.label, Math.max(1, budget));
148
+ lines.push(`${marker} ${color}${body}${ui.reset}${detailStr}${ui.dim}${scrollHint}${ui.reset}`);
149
+ });
150
+ return lines;
151
+ }
152
+ function redraw() {
153
+ layout.paintInput({
154
+ prompt: '❯ ',
155
+ lines: [query],
156
+ cursorLine: 0,
157
+ cursorCol: displayWidth(query.slice(0, cursor)),
158
+ menu: { lines: menuLines() },
159
+ });
160
+ }
161
+ function drill(to) {
162
+ path = to;
163
+ query = '';
164
+ cursor = 0;
165
+ recompute();
166
+ redraw();
167
+ }
168
+ function goUp() {
169
+ if (path.length === 0)
170
+ return false;
171
+ path = path.slice(0, -1);
172
+ recompute();
173
+ redraw();
174
+ return true;
175
+ }
176
+ function finish(v) {
177
+ if (resolved)
178
+ return;
179
+ resolved = true;
180
+ layout.setMouseEnabled(true);
181
+ try {
182
+ stdin.setRawMode(false);
183
+ }
184
+ catch {
185
+ /* 忽略 */
186
+ }
187
+ emitter.removeListener('keypress', onKey);
188
+ stdin.pause();
189
+ layout.paintInput({ prompt: '❯ ', lines: [''], cursorLine: 0, cursorCol: 0, menu: null });
190
+ resolve(v);
191
+ }
192
+ function move(delta) {
193
+ const en = enabledIndices();
194
+ if (en.length === 0)
195
+ return;
196
+ const pos = en.indexOf(selected);
197
+ const nextPos = (pos + delta + en.length) % en.length;
198
+ selected = en[nextPos];
199
+ redraw();
200
+ }
201
+ function onKey(_str, key) {
202
+ if (key.name === 'escape') {
203
+ if (query) {
204
+ query = '';
205
+ cursor = 0;
206
+ recompute();
207
+ redraw();
208
+ return;
209
+ }
210
+ if (goUp())
211
+ return;
212
+ return finish(null);
213
+ }
214
+ if (key.ctrl && key.name === 'c')
215
+ return finish(null);
216
+ if (key.name === 'return' || key.name === 'enter') {
217
+ const c = cands[selected];
218
+ if (!c || c.node.disabled)
219
+ return;
220
+ if (c.enterPath)
221
+ return drill(c.enterPath);
222
+ if (c.value !== undefined)
223
+ return finish(c.value);
224
+ return;
225
+ }
226
+ switch (key.name) {
227
+ case 'up':
228
+ move(-1);
229
+ return;
230
+ case 'down':
231
+ move(1);
232
+ return;
233
+ case 'left':
234
+ if (cursor > 0) {
235
+ cursor--;
236
+ redraw();
237
+ }
238
+ return;
239
+ case 'right':
240
+ if (cursor < query.length) {
241
+ cursor++;
242
+ redraw();
243
+ }
244
+ return;
245
+ case 'home':
246
+ cursor = 0;
247
+ redraw();
248
+ return;
249
+ case 'end':
250
+ cursor = query.length;
251
+ redraw();
252
+ return;
253
+ case 'backspace':
254
+ if (cursor > 0) {
255
+ query = query.slice(0, cursor - 1) + query.slice(cursor);
256
+ cursor--;
257
+ recompute();
258
+ redraw();
259
+ }
260
+ else if (!query) {
261
+ goUp();
262
+ }
263
+ return;
264
+ case 'delete':
265
+ if (cursor < query.length) {
266
+ query = query.slice(0, cursor) + query.slice(cursor + 1);
267
+ recompute();
268
+ redraw();
269
+ }
270
+ return;
271
+ }
272
+ if (key.ctrl && key.name === 'u') {
273
+ query = '';
274
+ cursor = 0;
275
+ recompute();
276
+ redraw();
277
+ return;
278
+ }
279
+ const s = key.sequence ?? '';
280
+ if (s && s >= ' ' && !key.ctrl && !key.meta) {
281
+ query = query.slice(0, cursor) + s + query.slice(cursor);
282
+ cursor += s.length;
283
+ recompute();
284
+ redraw();
285
+ }
286
+ }
287
+ recompute();
288
+ return new Promise((res) => {
289
+ resolve = res;
290
+ layout.setMouseEnabled(false);
291
+ try {
292
+ stdin.setRawMode(true);
293
+ }
294
+ catch {
295
+ res(null);
296
+ return;
297
+ }
298
+ stdin.resume();
299
+ emitter.on('keypress', onKey);
300
+ redraw();
301
+ });
302
+ }
@@ -10,7 +10,17 @@ const emitter = stdin;
10
10
  /** choice 末尾的本地化“自定义输入”项标签。 */
11
11
  const customLabel = () => t('intervention.custom');
12
12
  /** 弹出介入面板,阻塞直到用户完成选择。 */
13
- export async function promptIntervention(req) {
13
+ export function promptIntervention(req) {
14
+ // P4 闸1:并发子 agent 可能同时弹面板。面板进出场全是进程级单例(stdin 监听集/
15
+ // 鼠标开关/走时计时器/光标处理器),两个面板交错会导致监听恢复错乱与永久泄漏。
16
+ // 用承诺队列串行化:同一时刻只有一个面板,其余排队;非 TTY 分支不碰单例,排队也无害。
17
+ const run = () => promptInterventionInner(req);
18
+ const result = panelQueue.then(run, run);
19
+ panelQueue = result.then(() => undefined, () => undefined);
20
+ return result;
21
+ }
22
+ let panelQueue = Promise.resolve();
23
+ async function promptInterventionInner(req) {
14
24
  // 非 TTY 降级:不阻塞,自动选默认。日志走 stderr(不污染 stdout 内容流)。
15
25
  if (!layout.isActive()) {
16
26
  const kind = req.type === 'choice' ? t('intervention.defaultChoice') : t('intervention.emptyInput');
@@ -89,15 +89,26 @@ export function composeModelLine(status, cols) {
89
89
  const HINT = t('status.modeSwitch');
90
90
  const hintW = modeTag ? displayWidth(HINT) : 0;
91
91
  const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
92
+ // P3 effort chip:非 auto 时显示,与 modeTag 同级色(运行时状态),优先级在 hint 之上。
93
+ const effortTag = status.effort ?? '';
94
+ // 闪电标识按强度递增:low=1 / medium=2 / high=3,off=0;⚡ 宽度在 charWidth 按 2 格算,
95
+ // 与终端一致,不会再与右侧内容重叠产生残影。标签(low/medium/high)紧随其后明示档位。
96
+ const boltCount = effortTag === 'low' ? 1 : effortTag === 'medium' ? 2 : effortTag === 'high' ? 3 : 0;
97
+ const bolts = '⚡'.repeat(boltCount);
98
+ const effortPart = effortTag ? `${modeColor}${bolts}${effortTag}${ui.reset}` : '';
99
+ const effortW = effortTag ? displayWidth(bolts) + displayWidth(effortTag) : 0;
100
+ const sepME = modePart && effortPart ? STATUS_SEP : '';
92
101
  const tokChip = formatTurnTokenChip(status.lastTurnUsage);
93
102
  const tokW = displayWidth(stripAnsi(tokChip));
94
103
  // 合并左段:段间留 2 空格分隔。极窄时 hint 与 chip 都可能藏掉。
95
104
  // 优先级:modeTag(必) > chip(提示累计 token,有信息量)> hint(纯说明性,窄时最先省)。
96
- const sepMH = modePart && (hintPart || tokChip) ? ' ' : '';
105
+ const sepMH = (modePart || effortPart) && (hintPart || tokChip) ? ' ' : '';
97
106
  const sepHT = hintPart && tokChip ? ' ' : '';
98
- const leftStr = `${modePart}${sepMH}${hintPart}${sepHT}${tokChip}`;
107
+ const leftStr = `${modePart}${sepME}${effortPart}${sepMH}${hintPart}${sepHT}${tokChip}`;
99
108
  const leftW = modeW +
100
- (modePart && (hintPart || tokChip) ? sepMH.length : 0) +
109
+ (sepME ? sepME.length : 0) +
110
+ effortW +
111
+ ((modePart || effortPart) && (hintPart || tokChip) ? sepMH.length : 0) +
101
112
  hintW +
102
113
  (hintPart && tokChip ? sepHT.length : 0) +
103
114
  tokW;
@@ -118,15 +129,15 @@ export function composeModelLine(status, cols) {
118
129
  if (leftW + minGap + rightW > cols) {
119
130
  // 优先藏 hint(纯说明,信息密度最低):仅留 modePart + sep + tokChip
120
131
  if (modePart && tokChip && hintPart) {
121
- const leftStr2 = `${modePart}${sepMH}${tokChip}`;
122
- const leftW2 = modeW + sepMH.length + tokW;
132
+ const leftStr2 = `${modePart}${sepME}${effortPart}${effortPart ? STATUS_SEP : ''}${tokChip}`;
133
+ const leftW2 = modeW + (sepME ? sepME.length : 0) + effortW + (effortPart ? STATUS_SEP_W : 0) + tokW;
123
134
  if (leftW2 + minGap + rightW <= cols) {
124
135
  return twoColumn(leftStr2, leftW2, rightStr, rightW, cols);
125
136
  }
126
137
  }
127
138
  // 再藏 token chip:仅留 modePart
128
- if (modePart && tokChip) {
129
- return twoColumn(modePart, modeW, rightStr, rightW, cols);
139
+ if ((modePart || effortPart) && tokChip) {
140
+ return twoColumn(`${modePart}${sepME}${effortPart}`, modeW + (sepME ? sepME.length : 0) + effortW, rightStr, rightW, cols);
130
141
  }
131
142
  }
132
143
  return twoColumn(leftStr, leftW, rightStr, rightW, cols);
package/dist/ui/render.js CHANGED
@@ -60,6 +60,8 @@ export function charWidth(cp) {
60
60
  return 2; // 表情
61
61
  if (cp >= 0x20000 && cp <= 0x2fffd)
62
62
  return 2; // CJK 扩展 B-F
63
+ if (cp === 0x26a1)
64
+ return 2; // ⚡ 闪电:终端按 emoji 2 格渲染,算 1 会与 chip 重叠产生残影
63
65
  return 1;
64
66
  }
65
67
  export function displayWidth(str) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.6.4",
3
+ "version": "1.6.5",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 25 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {