mocode-ai 1.1.9 → 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.
- package/dist/agent/work-discipline.js +2 -3
- package/dist/config/index.js +74 -28
- package/dist/rollback/index.js +10 -3
- package/dist/skills/runner.js +43 -0
- package/dist/tools/builtins/grep.js +8 -0
- package/dist/tools/constants.js +3 -1
- package/dist/ui/batch.js +34 -6
- package/dist/ui/layout.js +3 -1
- package/package.json +1 -1
|
@@ -20,12 +20,11 @@ export function inferModelFamily(model) {
|
|
|
20
20
|
* 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
|
|
21
21
|
* 标签上做轻量变体。保持短小,详细的完成检查由动态 checklist 按需注入。
|
|
22
22
|
*/
|
|
23
|
-
const CORE_SECTION =
|
|
24
|
-
|
|
25
|
-
Use your judgment to choose the shortest reliable path from the request to a useful result.
|
|
23
|
+
const CORE_SECTION = `Use your judgment to choose the shortest reliable path from the request to a useful result.
|
|
26
24
|
|
|
27
25
|
- Inspect only the code and context needed for the next decision.
|
|
28
26
|
- Make the smallest coherent change and avoid unrelated refactors.
|
|
27
|
+
- Preserve existing behavior and public API compatibility unless the task explicitly requires a change.
|
|
29
28
|
- Decide whether validation is useful based on risk, scope, available commands, and the user's request. Validation is optional, not a completion gate.
|
|
30
29
|
- When validation is useful, choose the smallest relevant check yourself; do not run broad test/build suites by default.
|
|
31
30
|
- Re-read or rerun only when evidence is stale or the next edit depends on exact current content.
|
package/dist/config/index.js
CHANGED
|
@@ -62,17 +62,52 @@ export function isModelConfigured() {
|
|
|
62
62
|
}
|
|
63
63
|
const PLATFORM_NOTE = (() => {
|
|
64
64
|
if (process.platform === 'win32') {
|
|
65
|
-
return
|
|
66
|
-
- \`run_command\` uses \`cmd.exe /c\`: use cmd syntax and \`%VAR%\`; Unix builtins and command substitution are unavailable.
|
|
65
|
+
return `- This is Windows: \`run_command\` uses \`cmd.exe /c\` — use cmd syntax and \`%VAR%\`; Unix builtins and command substitution are unavailable.
|
|
67
66
|
- Prefer read_file/glob/grep for file discovery and reading. When shell is necessary, use forward-slash paths or invoke PowerShell explicitly.`;
|
|
68
67
|
}
|
|
69
68
|
if (process.platform === 'darwin') {
|
|
70
|
-
return
|
|
71
|
-
- \`run_command\` uses bash with BSD utilities. Prefer read_file/glob/grep; account for BSD/GNU differences when shell commands are necessary.`;
|
|
69
|
+
return `- This is macOS: \`run_command\` uses bash with BSD utilities. Prefer read_file/glob/grep; account for BSD/GNU differences when shell commands are necessary.`;
|
|
72
70
|
}
|
|
73
|
-
return
|
|
74
|
-
- \`run_command\` uses bash. Prefer read_file/glob/grep when they fit; otherwise use standard POSIX/GNU syntax.`;
|
|
71
|
+
return `- This is Linux/Unix: \`run_command\` uses bash. Prefer read_file/glob/grep when they fit; otherwise use standard POSIX/GNU syntax.`;
|
|
75
72
|
})();
|
|
73
|
+
/**
|
|
74
|
+
* 默认「声音」(Voice):给 mocode 一点人情味与性格,贴近 ChatGPT / 豆包的语感——
|
|
75
|
+
* 简洁但有温度、有观点、不谄媚、不啰嗦。这是性格的"底座"。
|
|
76
|
+
* 性格主要来自**身段/语气约束**,而非长篇指令,所以这段文字很短,不撑爆系统提示。
|
|
77
|
+
* 用户可用下列方式整段替换(自定义品牌声音):
|
|
78
|
+
* 1. `<cwd>/.mocode/persona.md`(项目级,最高)或 `~/.mocode/persona.md`(全局)
|
|
79
|
+
* 2. 环境变量 `MOCODE_PERSONA`(整段覆盖)
|
|
80
|
+
* 两者皆无则用本默认底座。
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_VOICE = `## Voice
|
|
83
|
+
- Act as a skilled engineering partner: clear, concise, practical. Avoid generic chatbot behavior.
|
|
84
|
+
- Give technical recommendations with brief trade-off reasoning when choices exist.
|
|
85
|
+
- Focus on useful information. Avoid unnecessary greetings, apologies, repetition, or filler.
|
|
86
|
+
- Match the user's style and language while staying task-focused.
|
|
87
|
+
- For long operations, briefly state the plan and expected result. Avoid step-by-step narration.
|
|
88
|
+
- State assumptions and ask when uncertain. Do not guess.`;
|
|
89
|
+
/** 解析用户自定义声音:persona.md 文件优先(项目级 > 全局),其次 env MOCODE_PERSONA。无则返回 ''。 */
|
|
90
|
+
function readPersonaFile() {
|
|
91
|
+
const candidates = [
|
|
92
|
+
path.join(process.cwd(), '.mocode', 'persona.md'),
|
|
93
|
+
path.join(os.homedir(), '.mocode', 'persona.md'),
|
|
94
|
+
];
|
|
95
|
+
for (const p of candidates) {
|
|
96
|
+
try {
|
|
97
|
+
const txt = fs.readFileSync(p, 'utf8').trim();
|
|
98
|
+
if (txt)
|
|
99
|
+
return txt;
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// 不存在/不可读:跳过
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return process.env.MOCODE_PERSONA?.trim() ?? '';
|
|
106
|
+
}
|
|
107
|
+
/** 解析最终注入的 Voice 段:用户自定义优先,否则用默认底座。 */
|
|
108
|
+
function buildVoiceSection() {
|
|
109
|
+
return readPersonaFile() || DEFAULT_VOICE;
|
|
110
|
+
}
|
|
76
111
|
/**
|
|
77
112
|
* 基础系统提示的"记忆段落":开 isMemoryEnabled() 时才拼。
|
|
78
113
|
* 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
|
|
@@ -225,28 +260,33 @@ export function buildBasePrompt(sessionId = getCurrentSessionId()) {
|
|
|
225
260
|
const memorySection = buildMemoryPromptSection();
|
|
226
261
|
const notepadSection = buildNotepadSection(sessionId);
|
|
227
262
|
// 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
|
|
228
|
-
// 约束:staticBody 的前缀段(尤其 ##
|
|
263
|
+
// 约束:staticBody 的前缀段(尤其 ## Identity 第一行)必须是纯静态文本,
|
|
229
264
|
// 不得嵌入会话级可变函数调用(如 t()/config.model)。否则 /language、/model
|
|
230
265
|
// 切换会让最敏感的前缀变化,破坏自动前缀缓存命中。可变值统一放到
|
|
231
|
-
// ##
|
|
232
|
-
const staticBody = `##
|
|
233
|
-
You are mocode, a terminal coding agent.
|
|
266
|
+
// ## Reporting 段末尾(仍在切片边界之前,子 agent 仍能拿到)。
|
|
267
|
+
const staticBody = `## Identity
|
|
268
|
+
You are mocode, a terminal coding agent.
|
|
269
|
+
|
|
270
|
+
## Core behavior
|
|
271
|
+
Complete programming tasks through an "analyze → call tool → observe result → decide next step" loop until solved.
|
|
234
272
|
|
|
235
273
|
## Modes
|
|
236
274
|
- AUTO is the default: investigate and complete the task with the tools currently exposed.
|
|
237
275
|
- PLAN is read-only research and design; do not make changes until the user approves and switches back to AUTO.
|
|
238
276
|
|
|
239
|
-
${PLATFORM_NOTE}
|
|
240
|
-
|
|
241
|
-
${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
242
|
-
|
|
243
277
|
## Workflow
|
|
244
|
-
-
|
|
245
|
-
-
|
|
278
|
+
- Understand: use existing conversation and tool evidence before gathering more; inspect only what supports the next decision, do not guess.
|
|
279
|
+
- Plan: for tasks with 3+ steps or context-loss risk, record the plan with the \`plan_update\` tool (see Session state); keep each step self-contained.
|
|
280
|
+
- Implement: make the smallest coherent change; edit against a fresh read (see Tool policy); avoid unrelated refactors.
|
|
281
|
+
- Verify: decide whether validation is useful by risk and scope; run the smallest relevant check, not broad test/build suites by default.
|
|
282
|
+
- Report: stop when done and give honest conclusions with path:line references (see Reporting).
|
|
246
283
|
- Use web search only when freshness materially affects the answer.
|
|
247
284
|
${buildCodegraphSection()}
|
|
248
285
|
|
|
249
|
-
##
|
|
286
|
+
## Engineering principles
|
|
287
|
+
${buildWorkDisciplineSection(inferModelFamily(config.model))}
|
|
288
|
+
|
|
289
|
+
## Tool policy
|
|
250
290
|
- During tool-calling turns, stay silent unless something important enough must reach the user — otherwise just call the tool and let it run.
|
|
251
291
|
- Go directly to a known path or symbol; use discovery tools only when the location is unknown.
|
|
252
292
|
- Edit against a FRESH read: before any edit_file/write_file, call read_file on the exact path and copy both its latest hash and the exact target text. Never reconstruct old_string from a grep/summary/diff — those lose whitespace and indentation and cause edit failures.
|
|
@@ -257,11 +297,16 @@ ${buildCodegraphSection()}
|
|
|
257
297
|
- For generated content over roughly 200 lines or 5K tokens, use small staged writes rather than one oversized tool argument.
|
|
258
298
|
- Use \`ask_human\` only for a genuinely user-owned decision; otherwise choose the safest reversible option and proceed.
|
|
259
299
|
|
|
260
|
-
##
|
|
300
|
+
## Environment
|
|
301
|
+
${PLATFORM_NOTE}
|
|
302
|
+
|
|
303
|
+
## Safety
|
|
261
304
|
- Get confirmation before irreversible or outward-facing actions such as deletion, push, production changes, or external requests, unless explicitly authorized.
|
|
262
305
|
- Stay within the authorized workspace and disclose anything skipped or unverifiable.
|
|
263
306
|
|
|
264
|
-
|
|
307
|
+
${buildVoiceSection()}
|
|
308
|
+
|
|
309
|
+
## Reporting
|
|
265
310
|
- Stop immediately when no more tools are needed; give conclusions directly.
|
|
266
311
|
- **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
|
|
267
312
|
- **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
|
|
@@ -270,13 +315,10 @@ ${t('assistant.languageInstruction')}`;
|
|
|
270
315
|
// 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
|
|
271
316
|
// 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
|
|
272
317
|
// - "## Project context" 仅当 memorySection/notepadSection 非空(notepad 索引依赖 notes.md 存在);
|
|
273
|
-
// -
|
|
318
|
+
// - "## Session state" 使用说明**无条件**注入(放在动态尾段首位):否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。动态段在静态前缀之后,不影响 prompt 缓存。
|
|
274
319
|
const dynamicParts = [];
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
dynamicParts.push(`## Project context (dynamic reference)\n${ctxContent}`);
|
|
278
|
-
}
|
|
279
|
-
dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
320
|
+
// 会话级私有尾段(子 agent 切片会丢弃):Session state 说明无条件注入在前,Project context 按需在后。
|
|
321
|
+
dynamicParts.push(`## Session state (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
|
|
280
322
|
'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
|
|
281
323
|
'Record and update the execution plan with the `plan_update` tool (preferred over editing checkboxes by hand); it keeps at most one active plan as a `## Plan:` section:\n' +
|
|
282
324
|
'```\n' +
|
|
@@ -291,12 +333,16 @@ ${t('assistant.languageInstruction')}`;
|
|
|
291
333
|
'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
|
|
292
334
|
'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
|
|
293
335
|
'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.');
|
|
336
|
+
const ctxContent = `${memorySection}${notepadSection}`.trimEnd();
|
|
337
|
+
if (ctxContent) {
|
|
338
|
+
dynamicParts.push(`## Project context\n${ctxContent}`);
|
|
339
|
+
}
|
|
294
340
|
return `${staticBody}\n\n${dynamicParts.join('\n\n')}`;
|
|
295
341
|
}
|
|
296
342
|
/** 静态主体结束 + 会话私有段起点标记,供 buildMocodeCorePrompt 稳健切片(#17)。 */
|
|
297
|
-
const MARKER_STATIC_END = '##
|
|
298
|
-
const MARKER_DYNAMIC_SECTION = '## Project context
|
|
299
|
-
const MARKER_DROPPABLE_SECTION = '## Session
|
|
343
|
+
const MARKER_STATIC_END = '## Reporting';
|
|
344
|
+
const MARKER_DYNAMIC_SECTION = '## Project context';
|
|
345
|
+
const MARKER_DROPPABLE_SECTION = '## Session state';
|
|
300
346
|
/**
|
|
301
347
|
* Stable, production-grade behavior shared by main and sub agents.
|
|
302
348
|
* It intentionally excludes the trailing session-specific payload (notepad
|
package/dist/rollback/index.js
CHANGED
|
@@ -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
|
-
|
|
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++) {
|
package/dist/tools/constants.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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}
|
|
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}
|
|
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}
|
|
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}
|
|
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
|