mocode-ai 1.1.10 → 1.2.0

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.
@@ -163,11 +163,18 @@ export function endPathMutation(capture, op) {
163
163
  if (changed)
164
164
  mutationVersion += 1;
165
165
  }
166
+ // 构建产物 / 临时 / 缓存目录:可再生运行时状态,扫描它们既昂贵(dist/ 含大量 .js bundle,
167
+ // 全量 readFileSync 会同步卡死事件循环,表现为 run_command 期间滚轮划不动、spinner 冻结),
168
+ // 也易把后台 daemon / 打包器的写入误判成模型改动。回滚本就只应覆盖源码,构建产物可再生。
169
+ const EXCLUDED_WORKSPACE_DIRS = new Set([
170
+ '.git', '.codegraph', 'node_modules',
171
+ 'dist', 'build', 'out', 'coverage', '.tmp', 'tmp',
172
+ '.output', '.next', '.vite', '.turbo', '.svelte-kit',
173
+ ]);
166
174
  function isWorkspaceExcluded(full) {
167
175
  const base = path.basename(full).toLowerCase();
168
- // Git 元数据必须永久排除以保护 index;依赖树/代码索引是可再生运行时状态,
169
- // 扫描它们既昂贵,也可能把后台 daemon 的写入误判成模型改动。
170
- if (base === '.git' || base === '.codegraph' || base === 'node_modules')
176
+ // Git 元数据必须永久排除以保护 index;依赖树/代码索引/构建产物/临时目录是可再生运行时状态。
177
+ if (EXCLUDED_WORKSPACE_DIRS.has(base))
171
178
  return true;
172
179
  const sessionDir = path.resolve(config.sessionDir);
173
180
  return isInside(sessionDir, full);
@@ -0,0 +1,43 @@
1
+ // 可执行 skill runner:把 skill 的「工作流」封装成隔离子 agent 执行。
2
+ // 对齐 Claude Code Agent Skills 的 context: fork 模型——skill 内容成为驱动子 agent
3
+ // 的 prompt(协议/操作规范),子 agent 用受控工具子集在隔离上下文里执行,结果摘要回灌。
4
+ //
5
+ // 依赖:agent/spawn.ts 的 spawnAgent(已具备隔离 history / 工具白名单 / maxSteps /
6
+ // read-write overlay / abort 透传 / usage 统计),这里只做「渲染 + 参数映射」,不重复造执行器。
7
+ import { spawnAgent } from '../agent/spawn.js';
8
+ /** 子 agent(Explore/Plan 等)类型 → 只读/写模式。缺省按 read 保守处理。 */
9
+ const AGENT_READ_MODE = new Set(['explore', 'plan', 'read', 'research']);
10
+ /**
11
+ * 把参数渲染进 skill 正文:替换 $ARGUMENTS / ${} / $1 / ${1} 等占位符。
12
+ * 仅替换存在的占位符,无占位符正文原样返回(兼容纯文本 skill)。
13
+ */
14
+ export function renderBody(skill, args) {
15
+ const body = skill.body?.trim() || '';
16
+ if (!body)
17
+ return body;
18
+ const arg = args ?? {};
19
+ const named = JSON.stringify(arg, null, 2) || '{}';
20
+ const positional = Array.isArray(arg)
21
+ ? arg.map((v) => String(v))
22
+ : (Object.values(arg).map((v) => String(v)));
23
+ const at = (i) => positional[i] ?? '';
24
+ return body
25
+ .replace(/\$ARGUMENTS\b/gi, named)
26
+ .replace(/\$\{?(\d+)\}?/g, (_m, idx) => at(Number(idx) - 1))
27
+ .replace(/\$\{ARGUMENTS\}/gi, named);
28
+ }
29
+ /**
30
+ * fork 执行:把 skill 作为隔离子 agent 的协议,派生受控子 agent 执行其工作流。
31
+ * skill.allowed_tools → 工具白名单;skill.agent → 只读/写模式;args 序列化进用户 prompt。
32
+ */
33
+ export async function runSkillForked(skill, args, ctx) {
34
+ const rendered = renderBody(skill, args);
35
+ const mode = skill.agent && AGENT_READ_MODE.has(skill.agent.trim().toLowerCase()) ? 'read' : 'write';
36
+ return spawnAgent({
37
+ prompt: `Execute the "${skill.name}" skill workflow now. Follow its protocol exactly, use the available tools, and when done return a concise summary of what you did, key findings, any files changed, and blockers.\n\n--- Skill protocol ---\n\n${rendered || '(skill body is empty; follow the skill contract described in the list above)'}`,
38
+ tools: skill.allowed_tools,
39
+ mode,
40
+ signal: ctx?.signal,
41
+ context: args ? `Skill arguments:\n${JSON.stringify(args, null, 2)}` : undefined,
42
+ });
43
+ }
@@ -1,6 +1,10 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import fg from 'fast-glob';
3
3
  import { MAX_RESULTS, IGNORE } from '../constants.js';
4
+ // 二进制文件探测:头部 4KB 含 C0 控制字符(NUL/BEL 等)即视为二进制,跳过。
5
+ // 否则 grep 扫到 SQLite/压缩文件等会产出「单行数 KB + 控制字符」的匹配行,
6
+ // 这类行进 TUI 展开后被终端 auto-wrap,物理行与缓冲行失配导致整屏错乱。
7
+ const BINARY_PROBE_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
4
8
  import { getSandboxRoot, isInsideRoot, jailResolve } from '../../sandbox/index.js';
5
9
  // ---------- grep ----------
6
10
  export const grepTool = {
@@ -55,6 +59,10 @@ export const grepTool = {
55
59
  continue; // 跳过无法读的文件(二进制/权限/沙箱越界)
56
60
  }
57
61
  scanned++;
62
+ // 二进制文件(如 .codegraph/codegraph.db 这类 SQLite)跳过:其「行」是数 KB 的
63
+ // 序列化记录 + 控制字符,匹配行对 LLM 无意义,还会污染 TUI 展开渲染。
64
+ if (BINARY_PROBE_RE.test(content.slice(0, 4096)))
65
+ continue;
58
66
  const lines = content.split(/\r?\n/);
59
67
  const lineNos = [];
60
68
  for (let i = 0; i < lines.length; i++) {
@@ -22,7 +22,9 @@ export const DECAY_DAYS = 30;
22
22
  export const GC_DAYS = 90;
23
23
  /** memory_search 结果(召回的记忆正文)的放宽上限:指令性内容,中截破坏语义,对齐 use_skill。 */
24
24
  export const MAX_MEMORY_RESULT = 64000;
25
- export const IGNORE = ['**/node_modules/**', '**/.git/**'];
25
+ // .codegraph:codegraph 索引目录(codegraph.db SQLite 二进制 + daemon.log),
26
+ // grep/glob 扫它无意义且会产出数 KB 的超长「行」,污染 TUI 展开渲染。
27
+ export const IGNORE = ['**/node_modules/**', '**/.git/**', '**/.codegraph/**'];
26
28
  // ── 前端工具簇(默认关闭,显式开启)─────────────────────────────────────────
27
29
  /**
28
30
  * 前端开发相关工具簇:browser / dev_server 依赖 playwright 二进制且拉起长驻进程,
package/dist/ui/batch.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
  import { ui } from './theme.js';
15
15
  import { t } from '../i18n/index.js';
16
+ import { truncateAnsi } from './render.js';
16
17
  const batches = new Map();
17
18
  /** 绝对行索引 → 所属 batch id(仅记录 summary 行;用于鼠标点击反查)。
18
19
  * buffer 行数变化时本表可能漂移——但只在 insertAfter/deleteFrom 后由本模块同步更新,
@@ -24,6 +25,31 @@ const absLineToEntry = new Map();
24
25
  const expandedBatches = new Set();
25
26
  /** 展开时完整输出的最大行数;超出截断,避免巨型输出撑爆 viewport。 */
26
27
  const MAX_EXPAND_LINES = 200;
28
+ /** 自洽行允许的最大显示宽(= 终端 cols)。buffer 行超 cols 会被终端 auto-wrap,
29
+ * 物理行与缓冲行失配 → repaintViewport 的 CUP 寻址全错(屏幕错乱)。 */
30
+ let maxCols = 200;
31
+ /** layout 在进 alt 屏 / SIGWINCH 时调,同步当前终端列宽供行宽钳制。 */
32
+ export function setMaxCols(n) {
33
+ if (Number.isFinite(n) && n >= 8)
34
+ maxCols = Math.floor(n);
35
+ }
36
+ /** 行内控制字符(NUL/BEL/TAB 等,常见于 grep 扫到二进制)替换为可见替代符。
37
+ * 必须保护 SGR 序列:行已带 ui.* 颜色码,裸 replace 会把 \x1B 一并替换、
38
+ * 毁掉转义序列(显示成字面 "[90m")。按 SGR 切分后只清洗文本段。 */
39
+ function visibleControl(s) {
40
+ return s
41
+ .split(/(\x1b\[[0-9;]*m)/)
42
+ .map((part, i) => (i % 2 === 1 ? part : part.replace(/[\x00-\x1f\x7f]/g, '·')))
43
+ .join('');
44
+ }
45
+ /** 自洽行统一收尾:行宽钳到 maxCols + 行末补 reset。
46
+ * 超宽行若直接入 rows[],repaintViewport 逐行 cup+clearLine 直出时终端会
47
+ * auto-wrap 成多条物理行,把后续所有行的屏位打乱(用户报告:展开含超长行的
48
+ * 工具输出后整屏错乱)。truncateAnsi 保留行内 SGR 且断尾补 reset,再统一 \x1B[0m 收尾。 */
49
+ function sanitizeRow(s) {
50
+ const cleaned = visibleControl(s);
51
+ return truncateAnsi(cleaned, maxCols) + '\x1B[0m';
52
+ }
27
53
  export function isMutationToolName(name) {
28
54
  return name === 'write_file' || name === 'edit_file';
29
55
  }
@@ -129,23 +155,25 @@ function buildEntryDetailLines(e, indent = ' ') {
129
155
  if (line === '' && lines.length > 0)
130
156
  continue; // 跳过首尾空行(diff 头/尾换行)
131
157
  const prefixed = `${ui.dim}${indent}${ui.reset}${line}`;
132
- lines.push(prefixed.endsWith('\x1B[0m') ? prefixed : prefixed + '\x1B[0m');
158
+ lines.push(sanitizeRow(prefixed));
133
159
  }
134
160
  }
135
161
  else if (e.fullOutput) {
136
- // 完整工具输出(纯文本):按行展开,每行缩进 + dim 样式;长输出截断到 MAX_EXPAND_LINES
162
+ // 完整工具输出(纯文本):按行展开,每行缩进 + dim 样式;长输出截断到 MAX_EXPAND_LINES 行。
163
+ // 每行经 sanitizeRow 钳宽:fullOutput 可能含 grep 扫二进制(db/压缩文件)得到的
164
+ // 超长行 + 控制字符,不钳会让终端 auto-wrap 打乱屏位。
137
165
  const rawLines = e.fullOutput.split('\n');
138
166
  const truncated = rawLines.length > MAX_EXPAND_LINES;
139
167
  const displayLines = truncated ? rawLines.slice(0, MAX_EXPAND_LINES) : rawLines;
140
168
  for (const line of displayLines) {
141
- lines.push(`${indent}${ui.gray}${line}${ui.reset}\x1B[0m`);
169
+ lines.push(sanitizeRow(`${indent}${ui.gray}${line}${ui.reset}`));
142
170
  }
143
171
  if (truncated) {
144
- lines.push(`${indent}${ui.dim}… (${rawLines.length - MAX_EXPAND_LINES} more lines)${ui.reset}\x1B[0m`);
172
+ lines.push(sanitizeRow(`${indent}${ui.dim}… (${rawLines.length - MAX_EXPAND_LINES} more lines)${ui.reset}`));
145
173
  }
146
174
  }
147
175
  else if (e.resultSummary) {
148
- lines.push(`${indent}${ui.gray}↳ ${e.resultSummary}${ui.reset}\x1B[0m`);
176
+ lines.push(sanitizeRow(`${indent}${ui.gray}↳ ${e.resultSummary}${ui.reset}`));
149
177
  }
150
178
  return lines;
151
179
  }
@@ -155,7 +183,7 @@ function buildExpandedLines(entries) {
155
183
  const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
156
184
  const branch = index === entries.length - 1 ? '└─' : '├─';
157
185
  const failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
158
- return ` ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}\x1B[0m`;
186
+ return sanitizeRow(` ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`);
159
187
  });
160
188
  }
161
189
  function entryDetailIndent(entries, index) {
package/dist/ui/layout.js CHANGED
@@ -4,7 +4,7 @@ import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncate
4
4
  import { ui, applyTerminalBackground, resetTerminalBackground } from './theme.js';
5
5
  import * as content from './content.js';
6
6
  import * as mouse from './mouse.js';
7
- import { reset as resetBatches, shiftBatchesAfter } from './batch.js';
7
+ import { reset as resetBatches, shiftBatchesAfter, setMaxCols } from './batch.js';
8
8
  import { copyToClipboard, readClipboard } from './clipboard.js';
9
9
  import { renderMarkdown } from './markdown.js';
10
10
  import { t } from '../i18n/index.js';
@@ -2064,6 +2064,7 @@ export function enterAltScreen() {
2064
2064
  if (active || !ui.isTTY)
2065
2065
  return;
2066
2066
  active = true;
2067
+ setMaxCols(getGeo().cols); // 同步 batch 展开行宽钳制,防超宽行 auto-wrap 打乱屏位
2067
2068
  stdout.write(esc.altOn);
2068
2069
  applyTerminalBackground();
2069
2070
  stdout.write(esc.mouseOn); // 完整鼠标追踪(按下/拖动/释放/滚轮)→ mouse.swallow 重组 → handleMouseEvent
@@ -2092,6 +2093,7 @@ export function enterAltScreen() {
2092
2093
  // contentRow 停在旧值、区域未更新,spinner/contentWrite 画到旧行号(「思考中在消息堆里」根因)。
2093
2094
  // 重画 repaintViewport 防抖(下面 timer),避免连续拖动闪烁;但行号/区域必须立即正确。
2094
2095
  const g = getGeo(footerH);
2096
+ setMaxCols(g.cols); // 列宽变 → 展开行钳宽上限同步(后续新展开行生效)
2095
2097
  const total = content.totalRows();
2096
2098
  const committed = content.committedRows();
2097
2099
  // 缩小:contentRow > 新 bottom → 钳到新 bottom
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.1.10",
3
+ "version": "1.2.0",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {