mocode-ai 0.4.8 → 0.4.10
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 +18 -0
- package/dist/agent/core.d.ts +112 -0
- package/dist/agent/core.js +56 -13
- package/dist/agent/index.d.ts +19 -0
- package/dist/agent/index.js +1 -1
- package/dist/agent/mode.d.ts +29 -0
- package/dist/agent/spawn.d.ts +36 -0
- package/dist/attachments/image.d.ts +22 -0
- package/dist/commands/config.d.ts +8 -0
- package/dist/config/file.d.ts +23 -0
- package/dist/config/index.d.ts +114 -0
- package/dist/config/index.js +61 -5
- package/dist/config/presets.d.ts +57 -0
- package/dist/context/budget.d.ts +79 -0
- package/dist/context/classifier.d.ts +14 -0
- package/dist/context/encoders/_util.d.ts +11 -0
- package/dist/context/encoders/code.d.ts +2 -0
- package/dist/context/encoders/doc.d.ts +14 -0
- package/dist/context/encoders/graph.d.ts +11 -0
- package/dist/context/encoders/index.d.ts +2 -0
- package/dist/context/encoders/log.d.ts +2 -0
- package/dist/context/encoders/memory.d.ts +2 -0
- package/dist/context/encoders/passthrough.d.ts +11 -0
- package/dist/context/encoders/search.d.ts +2 -0
- package/dist/context/encoders/summary.d.ts +12 -0
- package/dist/context/encoders/table.d.ts +2 -0
- package/dist/context/encoders/tree.d.ts +2 -0
- package/dist/context/index.d.ts +6 -0
- package/dist/context/lifecycle.d.ts +51 -0
- package/dist/context/pipeline.d.ts +9 -0
- package/dist/context/registry.d.ts +9 -0
- package/dist/context/relevance.d.ts +65 -0
- package/dist/context/types.d.ts +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/llm/capabilities.d.ts +19 -0
- package/dist/llm/index.d.ts +111 -0
- package/dist/llm/index.js +22 -1
- package/dist/memory/discover.d.ts +15 -0
- package/dist/memory/index.d.ts +13 -0
- package/dist/memory/reflect.d.ts +34 -0
- package/dist/memory/store.d.ts +124 -0
- package/dist/permissions/index.js +138 -0
- package/dist/pet/bridge.d.ts +73 -0
- package/dist/pet/protocol.d.ts +83 -0
- package/dist/pet/state.d.ts +28 -0
- package/dist/plan/active.d.ts +23 -0
- package/dist/plan/index.d.ts +13 -0
- package/dist/plan/state.d.ts +2 -0
- package/dist/plan/store.d.ts +78 -0
- package/dist/project-skill/index.js +180 -0
- package/dist/project-skill/initializer.js +178 -0
- package/dist/project-snapshot/index.js +85 -0
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/project-snapshot/static-files.js +87 -0
- package/dist/repl/index.d.ts +22 -0
- package/dist/repl/index.js +268 -2
- package/dist/rollback/index.d.ts +79 -0
- package/dist/sandbox/command.d.ts +3 -0
- package/dist/sandbox/index.d.ts +4 -0
- package/dist/sandbox/jail.d.ts +17 -0
- package/dist/sandbox/policy.d.ts +21 -0
- package/dist/sandbox/root.d.ts +5 -0
- package/dist/session/compact.d.ts +92 -0
- package/dist/session/drop.d.ts +19 -0
- package/dist/session/index.d.ts +13 -0
- package/dist/session/persist.d.ts +30 -0
- package/dist/session/scheduler.d.ts +53 -0
- package/dist/skills/discover.d.ts +33 -0
- package/dist/skills/index.d.ts +13 -0
- package/dist/tools/builtins/ask-human.d.ts +9 -0
- package/dist/tools/builtins/codegraph.d.ts +2 -0
- package/dist/tools/builtins/codegraph.js +17 -4
- package/dist/tools/builtins/drop-context.d.ts +18 -0
- package/dist/tools/builtins/edit-file.d.ts +2 -0
- package/dist/tools/builtins/edit-file.js +1 -0
- package/dist/tools/builtins/glob.d.ts +2 -0
- package/dist/tools/builtins/grep.d.ts +2 -0
- package/dist/tools/builtins/index.d.ts +2 -0
- package/dist/tools/builtins/index.js +7 -0
- package/dist/tools/builtins/memory-forget.d.ts +2 -0
- package/dist/tools/builtins/memory-forget.js +1 -0
- package/dist/tools/builtins/memory-list.d.ts +2 -0
- package/dist/tools/builtins/memory-save.d.ts +2 -0
- package/dist/tools/builtins/memory-save.js +1 -0
- package/dist/tools/builtins/memory-search.d.ts +2 -0
- package/dist/tools/builtins/memory-update.d.ts +2 -0
- package/dist/tools/builtins/memory-update.js +1 -0
- package/dist/tools/builtins/project-skill-update.js +104 -0
- package/dist/tools/builtins/read-file.d.ts +2 -0
- package/dist/tools/builtins/run-command.d.ts +2 -0
- package/dist/tools/builtins/run-command.js +1 -0
- package/dist/tools/builtins/switch-mode.d.ts +2 -0
- package/dist/tools/builtins/task.d.ts +2 -0
- package/dist/tools/builtins/task.js +1 -0
- package/dist/tools/builtins/todolist.d.ts +2 -0
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/tools/builtins/use-skill.d.ts +2 -0
- package/dist/tools/builtins/web-fetch.d.ts +2 -0
- package/dist/tools/builtins/web-search.d.ts +2 -0
- package/dist/tools/builtins/web-search.js +12 -1
- package/dist/tools/builtins/write-file.d.ts +2 -0
- package/dist/tools/builtins/write-file.js +1 -0
- package/dist/tools/constants.d.ts +52 -0
- package/dist/tools/registry.d.ts +18 -0
- package/dist/tools/types.d.ts +42 -0
- package/dist/ui/batch.d.ts +67 -0
- package/dist/ui/batch.js +19 -2
- package/dist/ui/clipboard.d.ts +9 -0
- package/dist/ui/content.d.ts +55 -0
- package/dist/ui/content.js +68 -3
- package/dist/ui/diff.d.ts +34 -0
- package/dist/ui/diff.js +15 -24
- package/dist/ui/intervention.d.ts +46 -0
- package/dist/ui/layout.d.ts +225 -0
- package/dist/ui/layout.js +67 -11
- package/dist/ui/markdown.d.ts +46 -0
- package/dist/ui/mouse.d.ts +29 -0
- package/dist/ui/prompt.d.ts +61 -0
- package/dist/ui/render.d.ts +60 -0
- package/dist/ui/spinner.d.ts +26 -0
- package/dist/ui/theme.d.ts +47 -0
- package/dist/updater/index.d.ts +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { spawnAgent } from '../agent/spawn.js';
|
|
2
|
+
/**
|
|
3
|
+
* 子 agent 系统提示:让 agent 自主探索项目并生成 markdown 快照
|
|
4
|
+
*/
|
|
5
|
+
const SNAPSHOT_LLM_SUFFIX = `You are a project analysis agent. Your task is to explore this project and generate a concise markdown snapshot for an AI coding assistant.
|
|
6
|
+
|
|
7
|
+
## Your Mission
|
|
8
|
+
Explore the project using available tools (read_file, glob, grep) and generate a structured markdown snapshot that captures:
|
|
9
|
+
- WHAT exists (project description, tech stack, commands, modules, directory structure)
|
|
10
|
+
- WHERE it is (paths, locations)
|
|
11
|
+
|
|
12
|
+
Do NOT explain WHY or HOW (that's Project Skill's job).
|
|
13
|
+
|
|
14
|
+
## Exploration Strategy
|
|
15
|
+
1. Read package.json, README.md, tsconfig.json (or equivalent for other languages)
|
|
16
|
+
2. List top-level directories to identify modules
|
|
17
|
+
3. Scan src/ directory structure (depth ≤3, directories only)
|
|
18
|
+
4. Extract key commands from package.json scripts
|
|
19
|
+
5. Identify tech stack from dependencies
|
|
20
|
+
|
|
21
|
+
## Output Format
|
|
22
|
+
Output a markdown document between these exact delimiters:
|
|
23
|
+
|
|
24
|
+
\`\`\`snapshot-md
|
|
25
|
+
# Project Snapshot
|
|
26
|
+
|
|
27
|
+
## Description
|
|
28
|
+
(一句话描述项目,≤80字,中文)
|
|
29
|
+
|
|
30
|
+
## Tech Stack
|
|
31
|
+
(关键技术栈,逗号分隔,含版本,≤15项)
|
|
32
|
+
|
|
33
|
+
## Commands
|
|
34
|
+
| Command | Description |
|
|
35
|
+
|---------|-------------|
|
|
36
|
+
| \`npm run build\` | 构建项目 |
|
|
37
|
+
| \`npm test\` | 运行测试 |
|
|
38
|
+
| ... | ... |
|
|
39
|
+
|
|
40
|
+
## Modules
|
|
41
|
+
- **module-name** — 一句话职责描述(≤20字)
|
|
42
|
+
- **module-name** — 一句话职责描述
|
|
43
|
+
- ...
|
|
44
|
+
|
|
45
|
+
## Source Tree
|
|
46
|
+
\`\`\`
|
|
47
|
+
src/
|
|
48
|
+
agent/
|
|
49
|
+
tools/
|
|
50
|
+
ui/
|
|
51
|
+
\`\`\`
|
|
52
|
+
\`\`\`snapshot-md
|
|
53
|
+
|
|
54
|
+
## Rules
|
|
55
|
+
1. **Description**: ≤80字,中文,从 README 和 package.json description 提炼。不要"一个..."开头。
|
|
56
|
+
2. **Tech Stack**: ≤15项,逗号分隔,含版本。格式如 "TypeScript 5.x, Node.js ≥18, openai@^4"。
|
|
57
|
+
3. **Commands**: 5-8个最重要的命令,从 package.json scripts 提取。Description ≤15字。用表格格式。
|
|
58
|
+
4. **Modules**: 每个顶层模块目录一条。职责≤20字,只写 WHAT(做什么)。
|
|
59
|
+
5. **Source Tree**: 只列目录(不列文件),深度≤3,用缩进表示层级,放在代码块中。
|
|
60
|
+
6. 不要包含:设计决策、注意事项、坑点、约定 → 这些是 Skill 的职责。
|
|
61
|
+
7. 总 markdown ≤ 2500 字符。
|
|
62
|
+
8. 使用中文。
|
|
63
|
+
`;
|
|
64
|
+
/**
|
|
65
|
+
* 生成 LLM 快照(markdown 格式)
|
|
66
|
+
* @param root 项目根目录
|
|
67
|
+
* @param signal AbortSignal
|
|
68
|
+
*/
|
|
69
|
+
export async function generateLLMSnapshot(root, signal) {
|
|
70
|
+
const prompt = `请探索项目 ${root},生成项目快照(markdown 格式)。
|
|
71
|
+
|
|
72
|
+
## 探索步骤
|
|
73
|
+
1. 读取 package.json(或等效配置文件)
|
|
74
|
+
2. 读取 README.md
|
|
75
|
+
3. 列出顶层目录,识别模块
|
|
76
|
+
4. 扫描 src/ 目录结构(深度≤3,只列目录)
|
|
77
|
+
5. 从 package.json scripts 提取关键命令
|
|
78
|
+
6. 从 dependencies 识别技术栈
|
|
79
|
+
|
|
80
|
+
## 输出
|
|
81
|
+
严格按照系统提示的 markdown 格式输出,不要添加额外解释。`;
|
|
82
|
+
try {
|
|
83
|
+
const result = await spawnAgent({
|
|
84
|
+
prompt,
|
|
85
|
+
systemPromptSuffix: SNAPSHOT_LLM_SUFFIX,
|
|
86
|
+
tools: ['read_file', 'glob', 'grep'], // 允许探索工具
|
|
87
|
+
maxSteps: 15, // 给足够的探索步数
|
|
88
|
+
signal,
|
|
89
|
+
});
|
|
90
|
+
if (!result.completed) {
|
|
91
|
+
return {
|
|
92
|
+
ok: false,
|
|
93
|
+
error: '子 agent 未完成(可能中断或超时)',
|
|
94
|
+
transcript: result.transcript,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (!result.summary) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: '子 agent 未返回内容',
|
|
101
|
+
transcript: result.transcript,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// 从 summary 中提取 markdown
|
|
105
|
+
const mdMatch = result.summary.match(/```snapshot-md\n([\s\S]*?)\n```snapshot-md/);
|
|
106
|
+
if (!mdMatch || !mdMatch[1]) {
|
|
107
|
+
// fallback: 尝试普通 markdown 代码块
|
|
108
|
+
const fallbackMatch = result.summary.match(/```markdown\n([\s\S]*?)\n```/);
|
|
109
|
+
if (!fallbackMatch || !fallbackMatch[1]) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: '无法从子 agent 输出中提取 markdown',
|
|
113
|
+
transcript: result.transcript,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { ok: true, content: fallbackMatch[1].trim(), transcript: result.transcript };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, content: mdMatch[1].trim(), transcript: result.transcript };
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
error: e instanceof Error ? e.message : '未知错误',
|
|
124
|
+
transcript: '',
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* 静态项目文件白名单 + 扫描逻辑。
|
|
5
|
+
* 这些文件通常是项目元信息,变动频率低、体积小、跨 session 稳定。
|
|
6
|
+
*/
|
|
7
|
+
/** 静态文件白名单(文件名或模式) */
|
|
8
|
+
const STATIC_FILE_PATTERNS = [
|
|
9
|
+
// 文档
|
|
10
|
+
/^README(\.[a-z]+)?(\.[a-z]+)?$/i, // README, README.md, README.zh-CN.md
|
|
11
|
+
'CHANGELOG.md',
|
|
12
|
+
'CONTRIBUTING.md',
|
|
13
|
+
'LICENSE',
|
|
14
|
+
'LICENSE.md',
|
|
15
|
+
'LICENSE.txt',
|
|
16
|
+
// Node.js
|
|
17
|
+
'package.json',
|
|
18
|
+
'tsconfig.json',
|
|
19
|
+
'jsconfig.json',
|
|
20
|
+
// Python
|
|
21
|
+
'pyproject.toml',
|
|
22
|
+
'setup.py',
|
|
23
|
+
'setup.cfg',
|
|
24
|
+
'requirements.txt',
|
|
25
|
+
'Pipfile',
|
|
26
|
+
// Go
|
|
27
|
+
'go.mod',
|
|
28
|
+
// Rust
|
|
29
|
+
'Cargo.toml',
|
|
30
|
+
// 环境变量模板
|
|
31
|
+
'.env.example',
|
|
32
|
+
'.env.template',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* 判断文件名是否匹配白名单
|
|
36
|
+
*/
|
|
37
|
+
function matchesPattern(filename) {
|
|
38
|
+
for (const p of STATIC_FILE_PATTERNS) {
|
|
39
|
+
if (typeof p === 'string') {
|
|
40
|
+
if (filename === p)
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
if (p.test(filename))
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 扫描项目根目录,收集匹配白名单的静态文件。
|
|
52
|
+
* 只扫顶层(不递归),避免 node_modules 等大目录。
|
|
53
|
+
* @param root 项目根目录(绝对路径)
|
|
54
|
+
* @returns key = 相对路径(正斜杠),val = { mtime, content }
|
|
55
|
+
*/
|
|
56
|
+
export function scanStaticFiles(root) {
|
|
57
|
+
const result = {};
|
|
58
|
+
let items;
|
|
59
|
+
try {
|
|
60
|
+
items = readdirSync(root, { withFileTypes: true });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return result;
|
|
64
|
+
}
|
|
65
|
+
for (const item of items) {
|
|
66
|
+
if (!item.isFile())
|
|
67
|
+
continue;
|
|
68
|
+
if (!matchesPattern(item.name))
|
|
69
|
+
continue;
|
|
70
|
+
const absPath = path.join(root, item.name);
|
|
71
|
+
try {
|
|
72
|
+
const stat = statSync(absPath);
|
|
73
|
+
// 体积限制:单文件 > 64KB 跳过(避免 README 巨长拖慢快照)
|
|
74
|
+
if (stat.size > 64 * 1024)
|
|
75
|
+
continue;
|
|
76
|
+
const content = readFileSync(absPath, 'utf8');
|
|
77
|
+
result[item.name] = {
|
|
78
|
+
mtime: stat.mtimeMs,
|
|
79
|
+
content,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// 读取失败:跳过
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ChatMessage } from '../llm/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 把会话历史渲染成静态文本进内容区(回滚 / 续接 / --resume 后复显上下文,仿 Claude Code):
|
|
4
|
+
* user→❯ 回显、assistant→正文(+ tool_calls 折叠成 ● 摘要行)、tool→↳ 结果预览;system 跳过。
|
|
5
|
+
* 思考段不持久(history 只存正文),故无思考折叠。渲染后续写位在末尾,紧接 enterInputMode 画输入框。
|
|
6
|
+
* 内容长于屏时 viewport 显尾(最近轮次),PgUp 可看更早——与流式态一致。
|
|
7
|
+
* user 多模态:用 textOf 取 text parts;若侧 channel messageAttachments 有原文件名则追加 chip 行
|
|
8
|
+
* (避免 base64 解码不可逆,旧 session 没侧 channel 时只显文本,文件名 fallback 到 image/* mime)。
|
|
9
|
+
*
|
|
10
|
+
* 折叠策略:遇到 assistant + tool_calls 不立即打 ● 行,而是累积到 batchEntries;
|
|
11
|
+
* 跟随的连续 tool 消息按 tool_call_id 反查填 resultSummary;遇下一个非 tool 消息(或末尾)时,
|
|
12
|
+
* 用 batch.writeSummaryOnly 出单行摘要(与实时 runAgent 同一渲染器,UI 一致)。
|
|
13
|
+
* 回放默认全折叠;用户可鼠标点击摘要行展开(由 BatchRenderer 接管,见 ui/batch.ts)。
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderHistory(history: ChatMessage[]): void;
|
|
16
|
+
/**
|
|
17
|
+
* 交互式 REPL:全屏 TUI(alt screen + 固定底栏)。INPUT 态底栏=状态行+输入框(raw mode 等按键);
|
|
18
|
+
* 提交后 enterRunningMode(底栏改 dim 占位、光标回内容续写位),命令分发与 runAgent 的流式输出经
|
|
19
|
+
* contentWrite 落入内容区(滚动区域内自动滚动,底栏不动)。history 由本模块持有,在轮次间持久;
|
|
20
|
+
* agent 只读取并追加(+ 经 session/ 压缩)。每轮成功结束后自动落盘,退出后可用 --resume / /resume 续接。
|
|
21
|
+
*/
|
|
22
|
+
export declare function startRepl(initialHistory?: ChatMessage[], sessionId?: string, updateNotice?: string | null, sandboxRootOverride?: string): Promise<void>;
|
package/dist/repl/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import readline from 'node:readline/promises';
|
|
2
2
|
import { emitKeypressEvents } from 'node:readline';
|
|
3
3
|
import { stdin, stdout } from 'node:process';
|
|
4
|
-
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
4
|
+
import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isProjectSkillEnabled, isProjectSnapshotEnabled, updateProjectSkillConfig, updateSnapshotConfig, buildBasePrompt, getPlanModeSuffix, } from '../config/index.js';
|
|
5
5
|
import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
|
|
6
6
|
import { deletePreset, getPreset, isValidPresetName, listPresets, migrateCurrentToPreset, savePreset, } from '../config/presets.js';
|
|
7
7
|
import { runAgent } from '../agent/index.js';
|
|
@@ -24,6 +24,7 @@ import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots
|
|
|
24
24
|
import { listSkills, effectiveSystemPrompt, } from '../skills/index.js';
|
|
25
25
|
import { buildMemorySection, buildMemoryIndexSection, kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, loadAll, } from '../memory/index.js';
|
|
26
26
|
import { buildActivePlanSection, onActivePlanChange, hasActivePlan, getActivePlanSummary, } from '../plan/index.js';
|
|
27
|
+
import { buildSnapshot, clearSnapshotCache } from '../project-snapshot/index.js';
|
|
27
28
|
/**
|
|
28
29
|
* readline 的 prompt 必须是纯文本(无 ANSI):readline 按字符数算光标位置,
|
|
29
30
|
* 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
|
|
@@ -41,6 +42,8 @@ const SLASH_COMMANDS = [
|
|
|
41
42
|
{ name: '/rollback', desc: '菜单选轮次回滚(↑↓·Enter)' },
|
|
42
43
|
{ name: '/memory', desc: '记忆库:条目计数与近期索引(关闭时提示先开 /memory_switch)' },
|
|
43
44
|
{ name: '/memory_switch', desc: '切换记忆子系统开关(无参=切换;/on 或 /off 显式;持久化 MEMORY_ENABLED)' },
|
|
45
|
+
{ name: '/project_skill', desc: '项目专属 Skill 开关 + 查看/初始化(无参=切换;/on·/off·/view·/init)' },
|
|
46
|
+
{ name: '/snapshot', desc: '切换项目快照开关(无参=切换;/on·/off·/status;持久化 MOCODE_PROJECT_SNAPSHOT)' },
|
|
44
47
|
{ name: '/memory_status', desc: '查看记忆子系统当前开关与原理' },
|
|
45
48
|
{ name: '/reflect', desc: '手动触发后台记忆反思 pass(需先开启记忆)' },
|
|
46
49
|
{ name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆(需先开启记忆)' },
|
|
@@ -49,6 +52,7 @@ const SLASH_COMMANDS = [
|
|
|
49
52
|
{ name: '/model switch', desc: '↑↓·Enter 在已配置预设间切换' },
|
|
50
53
|
{ name: '/model list', desc: '列出已经配置的模型' },
|
|
51
54
|
{ name: '/model delete <name>', desc: '删除已配置的模型' },
|
|
55
|
+
{ name: '/snapshot_refresh', desc: '刷新项目快照(重新扫描静态文件,重新生成 LLM 摘要)' },
|
|
52
56
|
{ name: '/plan', desc: '切到 plan 模式(只读探查+产出计划)' },
|
|
53
57
|
{ name: '/auto', desc: '切回 auto 模式(全工具执行)' },
|
|
54
58
|
{ name: '/pet', desc: '开关桌宠(独立悬浮窗,展示 agent 状态动画)' },
|
|
@@ -175,6 +179,8 @@ function runningStateFor(cmd) {
|
|
|
175
179
|
return { status: '回滚', placeholder: '选择轮次…' };
|
|
176
180
|
case '/init':
|
|
177
181
|
return { status: '初始化', placeholder: '生成 MOCODE.md…' };
|
|
182
|
+
case '/snapshot_refresh':
|
|
183
|
+
return { status: '刷新快照', placeholder: '扫描项目文件中…' };
|
|
178
184
|
case '/plan':
|
|
179
185
|
return { status: '切 plan', placeholder: '…' };
|
|
180
186
|
case '/auto':
|
|
@@ -191,6 +197,10 @@ function runningStateFor(cmd) {
|
|
|
191
197
|
return { status: '切记忆开关', placeholder: '切换中…' };
|
|
192
198
|
case '/memory_status':
|
|
193
199
|
return { status: '查记忆状态', placeholder: '…' };
|
|
200
|
+
case '/project_skill':
|
|
201
|
+
return { status: '项目 Skill', placeholder: '处理中…' };
|
|
202
|
+
case '/snapshot':
|
|
203
|
+
return { status: '切快照开关', placeholder: '切换中…' };
|
|
194
204
|
default:
|
|
195
205
|
// 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
|
|
196
206
|
// 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
|
|
@@ -518,8 +528,10 @@ export function renderHistory(history) {
|
|
|
518
528
|
break;
|
|
519
529
|
}
|
|
520
530
|
}
|
|
521
|
-
if (target)
|
|
531
|
+
if (target) {
|
|
522
532
|
target.resultSummary = preview;
|
|
533
|
+
target.fullOutput = output;
|
|
534
|
+
}
|
|
523
535
|
// 不直接写屏——等 flushBatch 时出单行摘要
|
|
524
536
|
continue;
|
|
525
537
|
}
|
|
@@ -538,6 +550,12 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
538
550
|
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
539
551
|
// 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
|
|
540
552
|
setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
|
|
553
|
+
// 项目快照:sandboxRoot 设定后异步构建(完全由 LLM 生成)。
|
|
554
|
+
// 构建失败不阻断 REPL:snapshot 内部 catch 所有异常。
|
|
555
|
+
if (config.projectSnapshotEnabled) {
|
|
556
|
+
// 异步触发 LLM 快照生成(不阻塞 REPL 启动)
|
|
557
|
+
buildSnapshot().catch(() => { });
|
|
558
|
+
}
|
|
541
559
|
// 构造系统提示:auto 用 base;plan 在 base 后追加按当前开关现拼的 plan suffix。
|
|
542
560
|
// 切模式时 applyMode 重算 history[0](history[0] 恒 system,compaction 保它,不破坏)。
|
|
543
561
|
// 活跃 plan 摘要拼在 memory 段后(systemPrompt 的尾段),todo 工具变更后 listener 重写 history[0]。
|
|
@@ -1135,6 +1153,40 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1135
1153
|
}
|
|
1136
1154
|
continue;
|
|
1137
1155
|
}
|
|
1156
|
+
if (line === '/snapshot_refresh') {
|
|
1157
|
+
if (!config.projectSnapshotEnabled) {
|
|
1158
|
+
layout.contentWrite(`${ui.dim}(项目快照功能已关闭,MOCODE_PROJECT_SNAPSHOT=false)${ui.reset}\n`);
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
// buildSnapshot 是异步操作(完全由 LLM 生成),显示进度提示
|
|
1162
|
+
layout.contentWrite(`${ui.dim}正在重新生成项目快照 (LLM 分析中)...${ui.reset}\n`);
|
|
1163
|
+
try {
|
|
1164
|
+
// 清除缓存,强制重新生成
|
|
1165
|
+
clearSnapshotCache();
|
|
1166
|
+
const result = await buildSnapshot(undefined, true);
|
|
1167
|
+
if (result.snapshot) {
|
|
1168
|
+
layout.contentWrite(`${ui.cyan}✓ 项目快照已刷新${ui.reset} (${result.snapshot.builtAt})\n`);
|
|
1169
|
+
layout.contentWrite(`${ui.dim}已生成 markdown 快照,注入到系统提示词中${ui.reset}\n`);
|
|
1170
|
+
// 刷新 history[0]:buildSnapshotSection 内部 loadSnapshot() 会拿到新快照,
|
|
1171
|
+
// buildSystemMessage 重新拼 system prompt,快照段落就更新了。
|
|
1172
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1173
|
+
layout.contentWrite(`${ui.dim}(system prompt 已同步刷新)${ui.reset}\n`);
|
|
1174
|
+
}
|
|
1175
|
+
else {
|
|
1176
|
+
layout.contentWrite(`${ui.red}✗ 快照生成失败${ui.reset}: ${result.error || '未知错误'}\n`);
|
|
1177
|
+
if (result.transcript) {
|
|
1178
|
+
layout.contentWrite(`${ui.dim}--- 子 agent 日志 ---${ui.reset}\n`);
|
|
1179
|
+
layout.contentWrite(`${ui.dim}${result.transcript}${ui.reset}\n`);
|
|
1180
|
+
layout.contentWrite(`${ui.dim}--- 日志结束 ---${ui.reset}\n`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
catch (e) {
|
|
1185
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
1186
|
+
layout.contentWrite(`${ui.red}[错误]${ui.reset} 刷新快照失败: ${msg}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1138
1190
|
if (line === '/sessions') {
|
|
1139
1191
|
// /sessions:浏览全部已保存会话(慢路径,readdir+全量 JSON.parse,目录 N 大时会有可感知卡顿)。
|
|
1140
1192
|
// 默认走 /resume(仅最近 10 条,瞬开);要翻历史续接更早的会话才用这条。
|
|
@@ -1665,6 +1717,220 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1665
1717
|
}
|
|
1666
1718
|
continue;
|
|
1667
1719
|
}
|
|
1720
|
+
if (line === '/project_skill' ||
|
|
1721
|
+
line.startsWith('/project_skill ')) {
|
|
1722
|
+
// /project_skill — 项目专属 Skill 总开关 + 查看/编辑。
|
|
1723
|
+
// 用法:
|
|
1724
|
+
// /project_skill 切换 on/off
|
|
1725
|
+
// /project_skill on|off 显式开关
|
|
1726
|
+
// /project_skill view 查看当前内容
|
|
1727
|
+
// /project_skill edit 打开编辑器修改
|
|
1728
|
+
try {
|
|
1729
|
+
const arg = line.startsWith('/project_skill ')
|
|
1730
|
+
? line.slice('/project_skill '.length).trim().toLowerCase()
|
|
1731
|
+
: '';
|
|
1732
|
+
// /project_skill view — 查看当前内容
|
|
1733
|
+
if (arg === 'view') {
|
|
1734
|
+
const enabled = isProjectSkillEnabled();
|
|
1735
|
+
layout.contentWrite(`${ui.cyan}项目专属 Skill:${ui.reset} ${enabled ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
1736
|
+
if (enabled) {
|
|
1737
|
+
const { readProjectSkill } = await import('../project-skill/index.js');
|
|
1738
|
+
const content = readProjectSkill();
|
|
1739
|
+
if (content) {
|
|
1740
|
+
layout.contentWrite(`${ui.dim}--- 内容 ---${ui.reset}\n${content}\n${ui.dim}--- 结束 ---${ui.reset}\n`);
|
|
1741
|
+
}
|
|
1742
|
+
else {
|
|
1743
|
+
layout.contentWrite(`${ui.dim}(尚未创建,可用 /project_skill init 生成初始内容)${ui.reset}\n`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
else {
|
|
1747
|
+
layout.contentWrite(`${ui.dim}(功能已关闭,用 /project_skill on 开启)${ui.reset}\n`);
|
|
1748
|
+
}
|
|
1749
|
+
continue;
|
|
1750
|
+
}
|
|
1751
|
+
// /project_skill init — 扫描项目并生成/优化 skill
|
|
1752
|
+
if (arg === 'init') {
|
|
1753
|
+
if (!isProjectSkillEnabled()) {
|
|
1754
|
+
layout.contentWrite(`${ui.yellow}⚠ 项目专属 Skill 尚未开启${ui.reset},请先运行 ${ui.cyan}/project_skill on${ui.reset}\n`);
|
|
1755
|
+
continue;
|
|
1756
|
+
}
|
|
1757
|
+
const { readProjectSkill, writeProjectSkill } = await import('../project-skill/index.js');
|
|
1758
|
+
const existing = readProjectSkill();
|
|
1759
|
+
if (existing) {
|
|
1760
|
+
layout.contentWrite(`${ui.cyan}⏳ 正在深度探索项目并优化现有 skill...${ui.reset}\n`);
|
|
1761
|
+
}
|
|
1762
|
+
else {
|
|
1763
|
+
layout.contentWrite(`${ui.cyan}⏳ 正在深度探索项目(子 agent 将使用工具扫描代码、架构、配置等)...${ui.reset}\n`);
|
|
1764
|
+
}
|
|
1765
|
+
layout.contentWrite(`${ui.dim}提示: 按 Ctrl+C 可中断${ui.reset}\n\n`);
|
|
1766
|
+
// 派生子 agent 深度探索项目
|
|
1767
|
+
const { generateInitialSkill } = await import('../project-skill/initializer.js');
|
|
1768
|
+
const result = await generateInitialSkill(existing ?? undefined, currentAbort?.signal);
|
|
1769
|
+
// 显示探索过程日志
|
|
1770
|
+
if (result.transcript) {
|
|
1771
|
+
layout.contentWrite(`${ui.dim}--- 探索过程 ---${ui.reset}\n`);
|
|
1772
|
+
layout.contentWrite(result.transcript);
|
|
1773
|
+
layout.contentWrite(`${ui.dim}--- 探索结束 ---${ui.reset}\n\n`);
|
|
1774
|
+
}
|
|
1775
|
+
if (result.ok && result.content) {
|
|
1776
|
+
const writeResult = writeProjectSkill(result.content);
|
|
1777
|
+
if (writeResult.ok) {
|
|
1778
|
+
layout.contentWrite(`${ui.green}✓ 项目 Skill 已生成${ui.reset}\n`);
|
|
1779
|
+
layout.contentWrite(`${ui.dim}内容预览:${ui.reset}\n${result.content.slice(0, 500)}${result.content.length > 500 ? '...' : ''}\n`);
|
|
1780
|
+
layout.contentWrite(`\n${ui.dim}文件: ${ui.cyan}.mocode/project-skill.md${ui.reset} (${result.content.length} 字符)\n`);
|
|
1781
|
+
layout.contentWrite(`${ui.dim}下次启动时会自动注入到系统提示词。Agent 也会在开发过程中持续更新。${ui.reset}\n`);
|
|
1782
|
+
layout.contentWrite(`${ui.dim}提示: 可用 ${ui.cyan}/project_skill view${ui.reset} 查看完整内容,或手动编辑文件完善。${ui.dim}${ui.reset}\n`);
|
|
1783
|
+
}
|
|
1784
|
+
else {
|
|
1785
|
+
layout.contentWrite(`${ui.red}✗ 写入失败:${ui.reset} ${writeResult.error}\n`);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
layout.contentWrite(`${ui.red}✗ 探索失败:${ui.reset} ${result.error}\n`);
|
|
1790
|
+
if (result.transcript) {
|
|
1791
|
+
layout.contentWrite(`${ui.dim}子 agent 输出了部分内容(见上方),但未能生成完整的 skill 文档。${ui.reset}\n`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
continue;
|
|
1795
|
+
}
|
|
1796
|
+
// 开关切换
|
|
1797
|
+
let nextEnabled;
|
|
1798
|
+
if (arg === '') {
|
|
1799
|
+
nextEnabled = !isProjectSkillEnabled();
|
|
1800
|
+
}
|
|
1801
|
+
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(arg)) {
|
|
1802
|
+
nextEnabled = true;
|
|
1803
|
+
}
|
|
1804
|
+
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(arg)) {
|
|
1805
|
+
nextEnabled = false;
|
|
1806
|
+
}
|
|
1807
|
+
else {
|
|
1808
|
+
layout.contentWrite(`${ui.yellow}/project_skill 用法:${ui.reset}\n` +
|
|
1809
|
+
` /project_skill 切换(开↔关)\n` +
|
|
1810
|
+
` /project_skill on|off 显式设值\n` +
|
|
1811
|
+
` /project_skill view 查看当前内容\n` +
|
|
1812
|
+
` /project_skill init 扫描项目生成/优化 skill(可重复运行)\n`);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
const prev = isProjectSkillEnabled();
|
|
1816
|
+
if (nextEnabled === prev) {
|
|
1817
|
+
layout.contentWrite(`${ui.dim}(已是 ${nextEnabled ? '开启' : '关闭'},未变更 — 持久化字段未写入)${ui.reset}\n`);
|
|
1818
|
+
continue;
|
|
1819
|
+
}
|
|
1820
|
+
updateProjectSkillConfig(nextEnabled);
|
|
1821
|
+
updateConfigKey('MOCODE_PROJECT_SKILL', nextEnabled ? 'true' : 'false');
|
|
1822
|
+
const note = nextEnabled
|
|
1823
|
+
? `${ui.green}已开启项目专属 Skill${ui.reset} — project_skill_update 工具进入工具表;` +
|
|
1824
|
+
`项目 Skill 内容会在下次拼 system message 时注入。工具表快照需重启 REPL 才完整刷新。`
|
|
1825
|
+
: `${ui.yellow}已关闭项目专属 Skill${ui.reset} — project_skill_update 工具将在下次拼 system message 时从工具表过滤;` +
|
|
1826
|
+
`项目 Skill 内容不再注入。重启 REPL 后工具表完全不出现。`;
|
|
1827
|
+
layout.contentWrite(`${note}\n`);
|
|
1828
|
+
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MOCODE_PROJECT_SKILL=${nextEnabled ? 'true' : 'false'};${ui.reset}` +
|
|
1829
|
+
(process.env.MOCODE_PROJECT_SKILL
|
|
1830
|
+
? `${ui.dim}同 session shell 未 export,文件写入即时生效)${ui.reset}\n`
|
|
1831
|
+
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
1832
|
+
}
|
|
1833
|
+
catch (e) {
|
|
1834
|
+
layout.contentWrite(`${ui.red}/project_skill 失败:${ui.reset} ${e.message}\n`);
|
|
1835
|
+
}
|
|
1836
|
+
continue;
|
|
1837
|
+
}
|
|
1838
|
+
if (line === '/snapshot' ||
|
|
1839
|
+
line.startsWith('/snapshot ')) {
|
|
1840
|
+
// /snapshot — 项目快照总开关。无参切换 on/off;/on 或 /off 显式;/status 只读。
|
|
1841
|
+
//
|
|
1842
|
+
// 设计原则(与 /project_skill / /memory_switch 对齐):
|
|
1843
|
+
// - 单一来源 config.projectSnapshotEnabled(被 buildSnapshotSection() 现拼 +
|
|
1844
|
+
// read_file 工具每次 execute 现读);改完下一轮 chat 的 system prompt 即时反映。
|
|
1845
|
+
// - 持久化字段 MOCODE_PROJECT_SNAPSHOT,默认值 true(默认开启,启动 lazy build)。
|
|
1846
|
+
// - 关闭时:system prompt 不注入快照段,read_file 不查 cache 直接走真实 readFile。
|
|
1847
|
+
// - 开启时:若还没有 snapshot.json,主动 buildProjectSnapshot() 一次(仿 startRepl
|
|
1848
|
+
// 655-658 的 lazy build 模式),否则 buildSnapshotSection() 会因 loadSnapshot=null
|
|
1849
|
+
// 返空串,用户感知不到自己刚打开的开关已经生效。
|
|
1850
|
+
try {
|
|
1851
|
+
const arg = line.startsWith('/snapshot ')
|
|
1852
|
+
? line.slice('/snapshot '.length).trim().toLowerCase()
|
|
1853
|
+
: '';
|
|
1854
|
+
// /snapshot status — 只读查询
|
|
1855
|
+
if (arg === 'status') {
|
|
1856
|
+
const on = isProjectSnapshotEnabled();
|
|
1857
|
+
layout.contentWrite(`${ui.cyan}项目快照:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
1858
|
+
layout.contentWrite(`${ui.dim} 单一来源 config.projectSnapshotEnabled(${on ? 'true' : 'false'});` +
|
|
1859
|
+
`持久化 ${ui.cyan}MOCODE_PROJECT_SNAPSHOT${ui.dim};` +
|
|
1860
|
+
`配置文件 ${CONFIG_PATH}${ui.reset}\n`);
|
|
1861
|
+
layout.contentWrite(`${ui.dim} 关闭时:buildBasePrompt() 不含「## Project Snapshot」段;` +
|
|
1862
|
+
`read_file 工具直接走真实 readFile,不走 snapshot cache。${ui.reset}\n`);
|
|
1863
|
+
layout.contentWrite(`${ui.dim} 切换后下次 chat 即时反映(本轮已发出的请求不会回滚)。${ui.reset}\n`);
|
|
1864
|
+
continue;
|
|
1865
|
+
}
|
|
1866
|
+
// /snapshot(无参=切换;有参=按值设)
|
|
1867
|
+
let nextEnabled;
|
|
1868
|
+
if (arg === '') {
|
|
1869
|
+
nextEnabled = !isProjectSnapshotEnabled();
|
|
1870
|
+
}
|
|
1871
|
+
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(arg)) {
|
|
1872
|
+
nextEnabled = true;
|
|
1873
|
+
}
|
|
1874
|
+
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(arg)) {
|
|
1875
|
+
nextEnabled = false;
|
|
1876
|
+
}
|
|
1877
|
+
else {
|
|
1878
|
+
layout.contentWrite(`${ui.yellow}/snapshot 用法:${ui.reset}\n` +
|
|
1879
|
+
` /snapshot 切换(开↔关)\n` +
|
|
1880
|
+
` /snapshot on|off 显式设值\n` +
|
|
1881
|
+
` /snapshot status 查看当前状态\n`);
|
|
1882
|
+
continue;
|
|
1883
|
+
}
|
|
1884
|
+
const prev = isProjectSnapshotEnabled();
|
|
1885
|
+
if (nextEnabled === prev) {
|
|
1886
|
+
layout.contentWrite(`${ui.dim}(已是 ${nextEnabled ? '开启' : '关闭'},未变更 — 持久化字段未写入)${ui.reset}\n`);
|
|
1887
|
+
continue;
|
|
1888
|
+
}
|
|
1889
|
+
updateSnapshotConfig(nextEnabled);
|
|
1890
|
+
updateConfigKey('MOCODE_PROJECT_SNAPSHOT', nextEnabled ? 'true' : 'false');
|
|
1891
|
+
// 开启时异步 build 一次,避免 buildSnapshotSection 因 loadSnapshot=null 返空串。
|
|
1892
|
+
let built = false;
|
|
1893
|
+
if (nextEnabled) {
|
|
1894
|
+
try {
|
|
1895
|
+
clearSnapshotCache();
|
|
1896
|
+
const result = await buildSnapshot();
|
|
1897
|
+
if (result.snapshot) {
|
|
1898
|
+
built = true;
|
|
1899
|
+
}
|
|
1900
|
+
else {
|
|
1901
|
+
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${result.error || '未知错误'} ` +
|
|
1902
|
+
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
catch (e) {
|
|
1906
|
+
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${e.message} ` +
|
|
1907
|
+
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
// 刷新 history[0] 使 system prompt 立即反映新值(仿 /snapshot_refresh)。
|
|
1911
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1912
|
+
const note = nextEnabled
|
|
1913
|
+
? `${ui.green}已开启项目快照${ui.reset} — ` +
|
|
1914
|
+
`system prompt 将在下次 chat 注入「## Project Snapshot」段;` +
|
|
1915
|
+
`read_file 工具优先走 snapshot cache(mtime 校验失败时回退真实 readFile)。`
|
|
1916
|
+
: `${ui.yellow}已关闭项目快照${ui.reset} — ` +
|
|
1917
|
+
`system prompt 不再注入「## Project Snapshot」段;` +
|
|
1918
|
+
`read_file 工具直接走真实 readFile,不再查 cache。`;
|
|
1919
|
+
let extra = '';
|
|
1920
|
+
if (nextEnabled && built) {
|
|
1921
|
+
extra = `${ui.dim}(已构建快照;如需刷新用 /snapshot_refresh)${ui.reset}\n`;
|
|
1922
|
+
}
|
|
1923
|
+
layout.contentWrite(`${note}\n${extra}`);
|
|
1924
|
+
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MOCODE_PROJECT_SNAPSHOT=${nextEnabled ? 'true' : 'false'};` +
|
|
1925
|
+
(process.env.MOCODE_PROJECT_SNAPSHOT
|
|
1926
|
+
? `${ui.dim}同 session shell 已 export MOCODE_PROJECT_SNAPSHOT,文件写入下次启动仍以 shell 值为准;取消 shell 设置后生效)${ui.reset}\n`
|
|
1927
|
+
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
1928
|
+
}
|
|
1929
|
+
catch (e) {
|
|
1930
|
+
layout.contentWrite(`${ui.red}/snapshot 失败:${ui.reset} ${e.message}\n`);
|
|
1931
|
+
}
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1668
1934
|
const bubbleRows = input.length + 2 + pendingAttachments.length; // N 行 message + 2 行尾随空(含 \n\n 留下的 open current 行) + 每附件 1 行
|
|
1669
1935
|
const shouldCommit = await awaitPendingRecall(input, pendingAttachments.length, placeholder);
|
|
1670
1936
|
if (!shouldCommit) {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ChatMessage } from '../llm/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* 回滚子系统:`/rollback` 菜单(↑/↓)选轮次 → 选中第 X 轮 = 删该轮及之后 + 预填该轮 user 输入(仿 Claude Code
|
|
4
|
+
* rewind,Enter 重新跑);撤销被删轮次的文件改动(逐个「保留/撤销」询问)。
|
|
5
|
+
*
|
|
6
|
+
* 语义:选中下标 picked(0-based,= 第 picked+1 轮)→ `planRollback(picked)` 保 1..picked(删 picked+1 轮及之后),
|
|
7
|
+
* 预填 userTexts[picked](= 第 picked+1 轮 user 输入)。**不是**保 1..picked+1——选中第 X 轮即"从第 X 轮重跑"。
|
|
8
|
+
*
|
|
9
|
+
* 撤销方案 = 落盘快照(不用 git):write_file/edit_file 执行前由 executeTool 调
|
|
10
|
+
* recordMutation,把 before 内容存入快照(按轮次 turnId 打标)。回滚到第 n 轮时,
|
|
11
|
+
* 对每条选「撤销」的路径,恢复 turnId > cutoffTurnId 的最小 turnId 快照的 before
|
|
12
|
+
* (= 所选轮末状态;before===null = 当时不存在 → 删文件)。同文件多轮改动也能
|
|
13
|
+
* 精确回到所选轮边界,不误伤更早轮次。
|
|
14
|
+
*
|
|
15
|
+
* 快照 + 轮次日志随 saveSession 落盘到 <sessionDir>/<id>.snapshots.json,/resume 读回
|
|
16
|
+
* → 重启后仍可撤销。叶子模块:运行时只依赖 config(+ node:fs/path);ChatMessage
|
|
17
|
+
* 用 import type 擦除,不拉 llm 运行时;不反向依赖任何业务。
|
|
18
|
+
*
|
|
19
|
+
* v1 偏离 / 局限:
|
|
20
|
+
* - 撤销粒度 = 路径(非逐条 tool_call)——快照只能整路径回到轮末,无法只撤一条 edit。
|
|
21
|
+
* - run_command 不撤销(shell 可任意改文件,无法跟踪)。
|
|
22
|
+
* - 跨 /resume 后若文件被外部改过,恢复会用 before 覆盖(边缘情况,不处理)。
|
|
23
|
+
* - 快照文件随大文件多次改动膨胀;/clear 重置;日后可按「turn 已不可回滚」裁剪。
|
|
24
|
+
*/
|
|
25
|
+
export interface Turn {
|
|
26
|
+
turnId: number;
|
|
27
|
+
firstLine: string;
|
|
28
|
+
}
|
|
29
|
+
export interface Snapshot {
|
|
30
|
+
turnId: number;
|
|
31
|
+
path: string;
|
|
32
|
+
before: string | null;
|
|
33
|
+
}
|
|
34
|
+
export interface FileChange {
|
|
35
|
+
path: string;
|
|
36
|
+
ops: string[];
|
|
37
|
+
snapshotAvailable: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface RollbackPlan {
|
|
40
|
+
n: number;
|
|
41
|
+
cutoffIndex: number;
|
|
42
|
+
cutoffTurnId: number;
|
|
43
|
+
changes: FileChange[];
|
|
44
|
+
}
|
|
45
|
+
/** agent:runAgent 入口调——开新轮次。firstLine 已由调用方截断到 40。 */
|
|
46
|
+
export declare function beginTurn(firstLine: string): void;
|
|
47
|
+
/** tools/registry:write_file/edit_file 执行前调——记 before 快照(在 tool.execute 之前读)。 */
|
|
48
|
+
export declare function recordMutation(p: string): void;
|
|
49
|
+
/** 列出当前可回滚的轮次(1-based 序号由调用方显示)。 */
|
|
50
|
+
export declare function listTurns(): Turn[];
|
|
51
|
+
/**
|
|
52
|
+
* 规划回滚到第 n 轮(1-based):算截断点 + 被删轮次里涉及的文件改动(按 path 去重)。
|
|
53
|
+
* 调用方据 changes 逐 path 问保留 / 撤销;snapshotAvailable=false 的项无法撤销。
|
|
54
|
+
*/
|
|
55
|
+
export declare function planRollback(n: number, history: ChatMessage[]): RollbackPlan;
|
|
56
|
+
/**
|
|
57
|
+
* 执行回滚:原地截断 history 到 cutoffIndex + 按选择恢复文件 + 裁剪 turns/snapshots。
|
|
58
|
+
* revertPaths 为选「撤销」的相对路径集合。返回删除消息数 + 撤销文件列表。
|
|
59
|
+
*/
|
|
60
|
+
export declare function applyRollback(plan: RollbackPlan, history: ChatMessage[], revertPaths: Set<string>): {
|
|
61
|
+
deletedMsgs: number;
|
|
62
|
+
revertedFiles: string[];
|
|
63
|
+
};
|
|
64
|
+
/** compact 摘要成功后调:按存活轮次数裁剪(M = 新 history 里 user 消息数)。 */
|
|
65
|
+
export declare function pruneAfterCompaction(history: ChatMessage[]): void;
|
|
66
|
+
/** /clear 调:清空全部状态。 */
|
|
67
|
+
export declare function resetState(): void;
|
|
68
|
+
/**
|
|
69
|
+
* 无 snapshots 文件时(/resume 旧会话)从 history 重建 turns(扫 user 消息,1..M,
|
|
70
|
+
* 无快照 → 那些轮次的文件改动不可撤销)。turnIdCounter = M,后续新轮次从 M+1 续。
|
|
71
|
+
*/
|
|
72
|
+
export declare function rebuildFromHistory(history: ChatMessage[]): void;
|
|
73
|
+
/** 随 saveSession 调:把 turns + snapshots 落盘(turns 为空则跳过,不写空文件)。 */
|
|
74
|
+
export declare function persistSnapshots(id: string): void;
|
|
75
|
+
/**
|
|
76
|
+
* /resume / --resume 加载会话后调:读回 turns + snapshots。成功返 true(状态已覆盖);
|
|
77
|
+
* 失败 / 无文件返 false,调用方应改调 rebuildFromHistory(history) 兜底。
|
|
78
|
+
*/
|
|
79
|
+
export declare function loadSnapshots(id: string): boolean;
|