mocode-ai 0.4.9 → 0.5.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/README.md +18 -0
- package/dist/agent/core.js +23 -2
- package/dist/agent/index.js +1 -1
- package/dist/config/index.js +17 -7
- package/dist/llm/index.js +22 -1
- package/dist/project-skill/index.js +81 -30
- package/dist/project-skill/initializer.js +45 -46
- package/dist/project-snapshot/index.js +39 -131
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/repl/index.js +163 -46
- package/dist/session/compact.js +3 -3
- package/dist/tools/builtins/project-skill-update.js +38 -7
- package/dist/tools/builtins/read-file.js +1 -17
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/ui/batch.js +23 -6
- package/dist/ui/content.js +96 -0
- package/dist/ui/diff.js +1 -1
- package/dist/ui/intervention.js +32 -4
- package/dist/ui/layout.js +166 -41
- package/dist/ui/prompt.js +11 -11
- package/dist/ui/render.js +44 -18
- package/dist/ui/spinner.js +1 -1
- package/dist/ui/theme.js +117 -0
- package/package.json +5 -4
|
@@ -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
|
+
}
|
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, isProjectSkillEnabled, updateProjectSkillConfig, 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';
|
|
@@ -9,7 +9,7 @@ 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
11
|
import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js';
|
|
12
|
-
import {
|
|
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';
|
|
15
15
|
import * as batch from '../ui/batch.js';
|
|
@@ -24,7 +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
|
|
27
|
+
import { buildSnapshot, clearSnapshotCache } from '../project-snapshot/index.js';
|
|
28
28
|
/**
|
|
29
29
|
* readline 的 prompt 必须是纯文本(无 ANSI):readline 按字符数算光标位置,
|
|
30
30
|
* 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
|
|
@@ -43,6 +43,7 @@ const SLASH_COMMANDS = [
|
|
|
43
43
|
{ name: '/memory', desc: '记忆库:条目计数与近期索引(关闭时提示先开 /memory_switch)' },
|
|
44
44
|
{ name: '/memory_switch', desc: '切换记忆子系统开关(无参=切换;/on 或 /off 显式;持久化 MEMORY_ENABLED)' },
|
|
45
45
|
{ name: '/project_skill', desc: '项目专属 Skill 开关 + 查看/初始化(无参=切换;/on·/off·/view·/init)' },
|
|
46
|
+
{ name: '/snapshot', desc: '切换项目快照开关(无参=切换;/on·/off·/status;持久化 MOCODE_PROJECT_SNAPSHOT)' },
|
|
46
47
|
{ name: '/memory_status', desc: '查看记忆子系统当前开关与原理' },
|
|
47
48
|
{ name: '/reflect', desc: '手动触发后台记忆反思 pass(需先开启记忆)' },
|
|
48
49
|
{ name: '/init', desc: '扫描项目生成 MOCODE.md 项目记忆(需先开启记忆)' },
|
|
@@ -51,7 +52,7 @@ const SLASH_COMMANDS = [
|
|
|
51
52
|
{ name: '/model switch', desc: '↑↓·Enter 在已配置预设间切换' },
|
|
52
53
|
{ name: '/model list', desc: '列出已经配置的模型' },
|
|
53
54
|
{ name: '/model delete <name>', desc: '删除已配置的模型' },
|
|
54
|
-
{ name: '/
|
|
55
|
+
{ name: '/snapshot_refresh', desc: '刷新项目快照(重新扫描静态文件,重新生成 LLM 摘要)' },
|
|
55
56
|
{ name: '/plan', desc: '切到 plan 模式(只读探查+产出计划)' },
|
|
56
57
|
{ name: '/auto', desc: '切回 auto 模式(全工具执行)' },
|
|
57
58
|
{ name: '/pet', desc: '开关桌宠(独立悬浮窗,展示 agent 状态动画)' },
|
|
@@ -66,6 +67,12 @@ const THEME_DESCRIPTIONS = {
|
|
|
66
67
|
solarized: 'Solarized 强调色',
|
|
67
68
|
gruvbox: 'Gruvbox 暖色',
|
|
68
69
|
nord: 'Nord 冷色',
|
|
70
|
+
orange: '南瓜橙(暖)',
|
|
71
|
+
rose: '玫红(暖紫)',
|
|
72
|
+
emerald: '翡翠绿(冷)',
|
|
73
|
+
amber: '琥珀金黄(暖)',
|
|
74
|
+
lavender: '薰衣草淡紫(冷)',
|
|
75
|
+
sunset: '日落珊瑚红(暖)',
|
|
69
76
|
};
|
|
70
77
|
/** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
|
|
71
78
|
const MODEL_PRESETS = [
|
|
@@ -138,7 +145,7 @@ function renderContextBar(history) {
|
|
|
138
145
|
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
139
146
|
const src = contextState.lastUsage ? '实测' : '估算';
|
|
140
147
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
141
|
-
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.
|
|
148
|
+
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
142
149
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${history.length} 条消息 (${src})${ui.reset}`;
|
|
143
150
|
}
|
|
144
151
|
/** 状态行用量条(精简版,进底栏):[bar] pct% k/k。
|
|
@@ -153,7 +160,7 @@ function renderContextBarInline(history) {
|
|
|
153
160
|
const filled = Math.round(pct * W);
|
|
154
161
|
const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
|
|
155
162
|
const k = (n) => `${Math.round(n / 1000)}k`;
|
|
156
|
-
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.
|
|
163
|
+
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
157
164
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
|
|
158
165
|
}
|
|
159
166
|
/** 状态行基线:模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token。repl 在轮次边界、切模式、plan 变更时调。 */
|
|
@@ -178,7 +185,7 @@ function runningStateFor(cmd) {
|
|
|
178
185
|
return { status: '回滚', placeholder: '选择轮次…' };
|
|
179
186
|
case '/init':
|
|
180
187
|
return { status: '初始化', placeholder: '生成 MOCODE.md…' };
|
|
181
|
-
case '/
|
|
188
|
+
case '/snapshot_refresh':
|
|
182
189
|
return { status: '刷新快照', placeholder: '扫描项目文件中…' };
|
|
183
190
|
case '/plan':
|
|
184
191
|
return { status: '切 plan', placeholder: '…' };
|
|
@@ -198,6 +205,8 @@ function runningStateFor(cmd) {
|
|
|
198
205
|
return { status: '查记忆状态', placeholder: '…' };
|
|
199
206
|
case '/project_skill':
|
|
200
207
|
return { status: '项目 Skill', placeholder: '处理中…' };
|
|
208
|
+
case '/snapshot':
|
|
209
|
+
return { status: '切快照开关', placeholder: '切换中…' };
|
|
201
210
|
default:
|
|
202
211
|
// 输入框留空(运行中可 typeahead 打字,dim 回显);运行状态由内联 spinner 承载(思考中/执行…),
|
|
203
212
|
// 状态行只显走时——故常态 status 留空,不塞「处理」这种与内联重复的泛标签。
|
|
@@ -525,8 +534,10 @@ export function renderHistory(history) {
|
|
|
525
534
|
break;
|
|
526
535
|
}
|
|
527
536
|
}
|
|
528
|
-
if (target)
|
|
537
|
+
if (target) {
|
|
529
538
|
target.resultSummary = preview;
|
|
539
|
+
target.fullOutput = output;
|
|
540
|
+
}
|
|
530
541
|
// 不直接写屏——等 flushBatch 时出单行摘要
|
|
531
542
|
continue;
|
|
532
543
|
}
|
|
@@ -545,13 +556,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
545
556
|
// 沙箱根:文件操作边界。优先级 --sandbox-root > SANDBOX_ROOT env > process.cwd()。
|
|
546
557
|
// 纯边界记录(不 chdir),jail.ts 内部 resolve。子 agent 同进程继承全局 root。
|
|
547
558
|
setSandboxRoot(sandboxRootOverride ?? config.sandboxRoot ?? process.cwd());
|
|
548
|
-
// 项目快照:sandboxRoot
|
|
549
|
-
// 构建失败不阻断 REPL:snapshot 内部 catch
|
|
559
|
+
// 项目快照:sandboxRoot 设定后异步构建(完全由 LLM 生成)。
|
|
560
|
+
// 构建失败不阻断 REPL:snapshot 内部 catch 所有异常。
|
|
550
561
|
if (config.projectSnapshotEnabled) {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
catch { /* ignore */ }
|
|
562
|
+
// 异步触发 LLM 快照生成(不阻塞 REPL 启动)
|
|
563
|
+
buildSnapshot().catch(() => { });
|
|
555
564
|
}
|
|
556
565
|
// 构造系统提示:auto 用 base;plan 在 base 后追加按当前开关现拼的 plan suffix。
|
|
557
566
|
// 切模式时 applyMode 重算 history[0](history[0] 恒 system,compaction 保它,不破坏)。
|
|
@@ -592,6 +601,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
592
601
|
baseURL: config.baseURL,
|
|
593
602
|
cwd: process.cwd(),
|
|
594
603
|
tools: toolsLine,
|
|
604
|
+
memoryEnabled: isMemoryEnabled(),
|
|
595
605
|
});
|
|
596
606
|
// 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
|
|
597
607
|
// --resume 有历史则渲染对话(仿 Claude Code),否则横幅。
|
|
@@ -604,7 +614,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
604
614
|
renderHistory(history);
|
|
605
615
|
}
|
|
606
616
|
else {
|
|
607
|
-
layout.
|
|
617
|
+
layout.writeBanner(bannerLines(banner()));
|
|
608
618
|
}
|
|
609
619
|
if (!isModelConfigured()) {
|
|
610
620
|
// 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
|
|
@@ -630,7 +640,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
630
640
|
}
|
|
631
641
|
}
|
|
632
642
|
}
|
|
633
|
-
layout.contentWrite(`${ui.dim} /plan · /auto · Shift+Tab 切换模式(plan:只读探查 + 产出计划,审批后切 auto 执行)${ui.reset}\n`);
|
|
634
643
|
if (updateNotice) {
|
|
635
644
|
// 自更新提示:放 /plan · /auto 行下方,黄色(警示色)突出"你正在用的版本较旧",与 dim 提示区分。
|
|
636
645
|
// 开场静态段(进 INPUT 态前),一行,纯文本不与流式 / 输入争用。
|
|
@@ -811,7 +820,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
811
820
|
/不支持(视觉|图片|图像|多模态)/.test(msg) ||
|
|
812
821
|
(/图片|图像|视觉|多模态/.test(msg) && /不支持|invalid|reject|fail/i.test(lower));
|
|
813
822
|
if (looksLikeImageError) {
|
|
814
|
-
layout.contentWrite(`${ui.red}[错误]${ui.reset} 当前模型 ${ui.
|
|
823
|
+
layout.contentWrite(`${ui.red}[错误]${ui.reset} 当前模型 ${ui.accent}${config.model}${ui.reset} 不支持视觉输入。${ui.dim}原始:${msg}${ui.reset}\n`);
|
|
815
824
|
layout.contentWrite(`${ui.dim}提示:运行 /model 切换到支持视觉的模型(如 gpt-4o / claude-3.5-sonnet / gemini-1.5-pro)。${ui.reset}\n`);
|
|
816
825
|
}
|
|
817
826
|
else {
|
|
@@ -979,7 +988,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
979
988
|
lastTurnUsage = undefined; // 清空旧轮的 token 累计
|
|
980
989
|
pendingAttachments = []; // 一并清空待发图片
|
|
981
990
|
layout.clearContent();
|
|
982
|
-
layout.
|
|
991
|
+
layout.writeBanner(bannerLines(banner()));
|
|
983
992
|
layout.contentWrite(`${ui.dim}(历史已清空,保留系统提示)${ui.reset}\n`);
|
|
984
993
|
continue;
|
|
985
994
|
}
|
|
@@ -1051,7 +1060,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1051
1060
|
.sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
|
|
1052
1061
|
.slice(0, 10);
|
|
1053
1062
|
for (const e of recent) {
|
|
1054
|
-
layout.contentWrite(` ${ui.
|
|
1063
|
+
layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
|
|
1055
1064
|
}
|
|
1056
1065
|
if (active.length === 0)
|
|
1057
1066
|
layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 MOCODE.md)${ui.reset}\n`);
|
|
@@ -1077,7 +1086,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1077
1086
|
else {
|
|
1078
1087
|
layout.contentWrite(`${ui.dim}已发现 ${skills.length} 个 skill:${ui.reset}\n`);
|
|
1079
1088
|
for (const s of skills) {
|
|
1080
|
-
layout.contentWrite(` ${ui.
|
|
1089
|
+
layout.contentWrite(` ${ui.accent}${s.name}${ui.reset} ${ui.dim}${s.description}${ui.reset}\n`);
|
|
1081
1090
|
}
|
|
1082
1091
|
layout.contentWrite(`${ui.dim}(用 use_skill 工具加载某 skill 的完整指令)${ui.reset}\n`);
|
|
1083
1092
|
}
|
|
@@ -1150,24 +1159,33 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1150
1159
|
}
|
|
1151
1160
|
continue;
|
|
1152
1161
|
}
|
|
1153
|
-
if (line === '/
|
|
1162
|
+
if (line === '/snapshot_refresh') {
|
|
1154
1163
|
if (!config.projectSnapshotEnabled) {
|
|
1155
1164
|
layout.contentWrite(`${ui.dim}(项目快照功能已关闭,MOCODE_PROJECT_SNAPSHOT=false)${ui.reset}\n`);
|
|
1156
1165
|
continue;
|
|
1157
1166
|
}
|
|
1158
|
-
//
|
|
1167
|
+
// buildSnapshot 是异步操作(完全由 LLM 生成),显示进度提示
|
|
1168
|
+
layout.contentWrite(`${ui.dim}正在重新生成项目快照 (LLM 分析中)...${ui.reset}\n`);
|
|
1159
1169
|
try {
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
const
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1170
|
+
// 清除缓存,强制重新生成
|
|
1171
|
+
clearSnapshotCache();
|
|
1172
|
+
const result = await buildSnapshot(undefined, true);
|
|
1173
|
+
if (result.snapshot) {
|
|
1174
|
+
layout.contentWrite(`${ui.cyan}✓ 项目快照已刷新${ui.reset} (${result.snapshot.builtAt})\n`);
|
|
1175
|
+
layout.contentWrite(`${ui.dim}已生成 markdown 快照,注入到系统提示词中${ui.reset}\n`);
|
|
1176
|
+
// 刷新 history[0]:buildSnapshotSection 内部 loadSnapshot() 会拿到新快照,
|
|
1177
|
+
// buildSystemMessage 重新拼 system prompt,快照段落就更新了。
|
|
1178
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1179
|
+
layout.contentWrite(`${ui.dim}(system prompt 已同步刷新)${ui.reset}\n`);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
layout.contentWrite(`${ui.red}✗ 快照生成失败${ui.reset}: ${result.error || '未知错误'}\n`);
|
|
1183
|
+
if (result.transcript) {
|
|
1184
|
+
layout.contentWrite(`${ui.dim}--- 子 agent 日志 ---${ui.reset}\n`);
|
|
1185
|
+
layout.contentWrite(`${ui.dim}${result.transcript}${ui.reset}\n`);
|
|
1186
|
+
layout.contentWrite(`${ui.dim}--- 日志结束 ---${ui.reset}\n`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1171
1189
|
}
|
|
1172
1190
|
catch (e) {
|
|
1173
1191
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -1247,7 +1265,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1247
1265
|
else if (arg === 'list' || !themeExists(arg)) {
|
|
1248
1266
|
layout.contentWrite(`${ui.dim}可用主题:${ui.reset}\n`);
|
|
1249
1267
|
for (const t of listThemes()) {
|
|
1250
|
-
layout.contentWrite(` ${ui.
|
|
1268
|
+
layout.contentWrite(` ${ui.accent}${t}${ui.reset} ${ui.dim}${THEME_DESCRIPTIONS[t] ?? ''}${ui.reset}\n`);
|
|
1251
1269
|
}
|
|
1252
1270
|
layout.contentWrite(`${ui.dim}(当前:${getTheme()};用 /theme <名称> 切换)${ui.reset}\n`);
|
|
1253
1271
|
continue;
|
|
@@ -1266,7 +1284,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1266
1284
|
renderHistory(history);
|
|
1267
1285
|
}
|
|
1268
1286
|
else {
|
|
1269
|
-
layout.
|
|
1287
|
+
layout.writeBanner(bannerLines(banner()));
|
|
1270
1288
|
}
|
|
1271
1289
|
layout.contentWrite(`${ui.dim}(已切换主题 ${name})${ui.reset}\n`);
|
|
1272
1290
|
updateConfigKey('MOCODE_THEME', name);
|
|
@@ -1302,7 +1320,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1302
1320
|
renderHistory(history);
|
|
1303
1321
|
}
|
|
1304
1322
|
else {
|
|
1305
|
-
layout.
|
|
1323
|
+
layout.writeBanner(bannerLines(banner()));
|
|
1306
1324
|
}
|
|
1307
1325
|
layout.contentWrite(`${ui.dim}(已切换到预设 “${target.name}” → ${target.model} @ ${target.baseURL})${ui.reset}\n`);
|
|
1308
1326
|
if (config.llmKeysFromShell.length > 0) {
|
|
@@ -1381,7 +1399,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1381
1399
|
layout.contentWrite(`${ui.dim}已配置 ${ps.length} 个预设:${ui.reset}\n`);
|
|
1382
1400
|
for (const p of ps) {
|
|
1383
1401
|
const star = p.baseURL === config.baseURL && p.apiKey === config.apiKey && p.model === config.model ? ' ★' : '';
|
|
1384
|
-
layout.contentWrite(` ${ui.
|
|
1402
|
+
layout.contentWrite(` ${ui.accent}${p.name}${ui.reset}${star} ${ui.dim}${p.model} @ ${p.baseURL}${ui.reset}\n`);
|
|
1385
1403
|
}
|
|
1386
1404
|
layout.contentWrite(`${ui.dim}(★ = 与当前一致;切换用 /model switch)${ui.reset}\n`);
|
|
1387
1405
|
continue;
|
|
@@ -1389,10 +1407,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1389
1407
|
// /model show:显示当前四项配置(apiKey 脱敏)。
|
|
1390
1408
|
if (arg === 'show') {
|
|
1391
1409
|
layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
|
|
1392
|
-
layout.contentWrite(` ${ui.
|
|
1393
|
-
layout.contentWrite(` ${ui.
|
|
1394
|
-
layout.contentWrite(` ${ui.
|
|
1395
|
-
layout.contentWrite(` ${ui.
|
|
1410
|
+
layout.contentWrite(` ${ui.accent}baseURL${ui.reset} ${config.baseURL}\n`);
|
|
1411
|
+
layout.contentWrite(` ${ui.accent}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
|
|
1412
|
+
layout.contentWrite(` ${ui.accent}model ${ui.reset} ${config.model}\n`);
|
|
1413
|
+
layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
|
|
1396
1414
|
layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
|
|
1397
1415
|
continue;
|
|
1398
1416
|
}
|
|
@@ -1615,7 +1633,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1615
1633
|
renderHistory(history);
|
|
1616
1634
|
}
|
|
1617
1635
|
else {
|
|
1618
|
-
layout.
|
|
1636
|
+
layout.writeBanner(bannerLines(banner()));
|
|
1619
1637
|
}
|
|
1620
1638
|
layout.contentWrite(`${ui.dim}(已切换模型 → ${model} @ ${baseURL})${ui.reset}\n`);
|
|
1621
1639
|
if (savedName) {
|
|
@@ -1650,9 +1668,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1650
1668
|
try {
|
|
1651
1669
|
if (line === '/memory_status' || line.startsWith('/memory_status ')) {
|
|
1652
1670
|
const on = isMemoryEnabled();
|
|
1653
|
-
layout.contentWrite(`${ui.
|
|
1671
|
+
layout.contentWrite(`${ui.accent}记忆子系统:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
1654
1672
|
layout.contentWrite(`${ui.dim} 单一来源 isMemoryEnabled()(${config.memoryEnabled});` +
|
|
1655
|
-
`持久化 ${ui.
|
|
1673
|
+
`持久化 ${ui.accent}MEMORY_ENABLED${ui.dim};` +
|
|
1656
1674
|
`配置文件 ${CONFIG_PATH}${ui.reset}\n`);
|
|
1657
1675
|
layout.contentWrite(`${ui.dim} 关闭时:memory_*_save/_search/_list/_update/_forget 五个工具整体不进工具表;` +
|
|
1658
1676
|
`buildBasePrompt() 不含「## Memory」段;` +
|
|
@@ -1699,6 +1717,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1699
1717
|
(process.env.MEMORY_ENABLED
|
|
1700
1718
|
? `${ui.dim}同 session shell 未 export,文件写入即时生效)${ui.reset}\n`
|
|
1701
1719
|
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
1720
|
+
// 即时刷 banner(原地替换顶部 bannerH 行,不留副本):banner() 闭包实时读
|
|
1721
|
+
// isMemoryEnabled(),无需重启 REPL。buffer 中 bannerH 之下的对话历史位置不动。
|
|
1722
|
+
layout.rewriteBanner(bannerLines(banner()));
|
|
1702
1723
|
}
|
|
1703
1724
|
catch (e) {
|
|
1704
1725
|
layout.contentWrite(`${ui.red}/memory_switch 失败:${ui.reset} ${e.message}\n`);
|
|
@@ -1720,7 +1741,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1720
1741
|
// /project_skill view — 查看当前内容
|
|
1721
1742
|
if (arg === 'view') {
|
|
1722
1743
|
const enabled = isProjectSkillEnabled();
|
|
1723
|
-
layout.contentWrite(`${ui.
|
|
1744
|
+
layout.contentWrite(`${ui.accent}项目专属 Skill:${ui.reset} ${enabled ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
1724
1745
|
if (enabled) {
|
|
1725
1746
|
const { readProjectSkill } = await import('../project-skill/index.js');
|
|
1726
1747
|
const content = readProjectSkill();
|
|
@@ -1765,7 +1786,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1765
1786
|
if (writeResult.ok) {
|
|
1766
1787
|
layout.contentWrite(`${ui.green}✓ 项目 Skill 已生成${ui.reset}\n`);
|
|
1767
1788
|
layout.contentWrite(`${ui.dim}内容预览:${ui.reset}\n${result.content.slice(0, 500)}${result.content.length > 500 ? '...' : ''}\n`);
|
|
1768
|
-
layout.contentWrite(`\n${ui.dim}文件: ${ui.
|
|
1789
|
+
layout.contentWrite(`\n${ui.dim}文件: ${ui.accent}.mocode/project-skill.md${ui.reset} (${result.content.length} 字符)\n`);
|
|
1769
1790
|
layout.contentWrite(`${ui.dim}下次启动时会自动注入到系统提示词。Agent 也会在开发过程中持续更新。${ui.reset}\n`);
|
|
1770
1791
|
layout.contentWrite(`${ui.dim}提示: 可用 ${ui.cyan}/project_skill view${ui.reset} 查看完整内容,或手动编辑文件完善。${ui.dim}${ui.reset}\n`);
|
|
1771
1792
|
}
|
|
@@ -1823,6 +1844,102 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1823
1844
|
}
|
|
1824
1845
|
continue;
|
|
1825
1846
|
}
|
|
1847
|
+
if (line === '/snapshot' ||
|
|
1848
|
+
line.startsWith('/snapshot ')) {
|
|
1849
|
+
// /snapshot — 项目快照总开关。无参切换 on/off;/on 或 /off 显式;/status 只读。
|
|
1850
|
+
//
|
|
1851
|
+
// 设计原则(与 /project_skill / /memory_switch 对齐):
|
|
1852
|
+
// - 单一来源 config.projectSnapshotEnabled(被 buildSnapshotSection() 现拼 +
|
|
1853
|
+
// read_file 工具每次 execute 现读);改完下一轮 chat 的 system prompt 即时反映。
|
|
1854
|
+
// - 持久化字段 MOCODE_PROJECT_SNAPSHOT,默认值 true(默认开启,启动 lazy build)。
|
|
1855
|
+
// - 关闭时:system prompt 不注入快照段,read_file 不查 cache 直接走真实 readFile。
|
|
1856
|
+
// - 开启时:若还没有 snapshot.json,主动 buildProjectSnapshot() 一次(仿 startRepl
|
|
1857
|
+
// 655-658 的 lazy build 模式),否则 buildSnapshotSection() 会因 loadSnapshot=null
|
|
1858
|
+
// 返空串,用户感知不到自己刚打开的开关已经生效。
|
|
1859
|
+
try {
|
|
1860
|
+
const arg = line.startsWith('/snapshot ')
|
|
1861
|
+
? line.slice('/snapshot '.length).trim().toLowerCase()
|
|
1862
|
+
: '';
|
|
1863
|
+
// /snapshot status — 只读查询
|
|
1864
|
+
if (arg === 'status') {
|
|
1865
|
+
const on = isProjectSnapshotEnabled();
|
|
1866
|
+
layout.contentWrite(`${ui.accent}项目快照:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
|
|
1867
|
+
layout.contentWrite(`${ui.dim} 单一来源 config.projectSnapshotEnabled(${on ? 'true' : 'false'});` +
|
|
1868
|
+
`持久化 ${ui.accent}MOCODE_PROJECT_SNAPSHOT${ui.dim};` +
|
|
1869
|
+
`配置文件 ${CONFIG_PATH}${ui.reset}\n`);
|
|
1870
|
+
layout.contentWrite(`${ui.dim} 关闭时:buildBasePrompt() 不含「## Project Snapshot」段;` +
|
|
1871
|
+
`read_file 工具直接走真实 readFile,不走 snapshot cache。${ui.reset}\n`);
|
|
1872
|
+
layout.contentWrite(`${ui.dim} 切换后下次 chat 即时反映(本轮已发出的请求不会回滚)。${ui.reset}\n`);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
// /snapshot(无参=切换;有参=按值设)
|
|
1876
|
+
let nextEnabled;
|
|
1877
|
+
if (arg === '') {
|
|
1878
|
+
nextEnabled = !isProjectSnapshotEnabled();
|
|
1879
|
+
}
|
|
1880
|
+
else if (['on', 'true', '1', 'yes', 'y', 'enable', 'enabled'].includes(arg)) {
|
|
1881
|
+
nextEnabled = true;
|
|
1882
|
+
}
|
|
1883
|
+
else if (['off', 'false', '0', 'no', 'n', 'disable', 'disabled'].includes(arg)) {
|
|
1884
|
+
nextEnabled = false;
|
|
1885
|
+
}
|
|
1886
|
+
else {
|
|
1887
|
+
layout.contentWrite(`${ui.yellow}/snapshot 用法:${ui.reset}\n` +
|
|
1888
|
+
` /snapshot 切换(开↔关)\n` +
|
|
1889
|
+
` /snapshot on|off 显式设值\n` +
|
|
1890
|
+
` /snapshot status 查看当前状态\n`);
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
const prev = isProjectSnapshotEnabled();
|
|
1894
|
+
if (nextEnabled === prev) {
|
|
1895
|
+
layout.contentWrite(`${ui.dim}(已是 ${nextEnabled ? '开启' : '关闭'},未变更 — 持久化字段未写入)${ui.reset}\n`);
|
|
1896
|
+
continue;
|
|
1897
|
+
}
|
|
1898
|
+
updateSnapshotConfig(nextEnabled);
|
|
1899
|
+
updateConfigKey('MOCODE_PROJECT_SNAPSHOT', nextEnabled ? 'true' : 'false');
|
|
1900
|
+
// 开启时异步 build 一次,避免 buildSnapshotSection 因 loadSnapshot=null 返空串。
|
|
1901
|
+
let built = false;
|
|
1902
|
+
if (nextEnabled) {
|
|
1903
|
+
try {
|
|
1904
|
+
clearSnapshotCache();
|
|
1905
|
+
const result = await buildSnapshot();
|
|
1906
|
+
if (result.snapshot) {
|
|
1907
|
+
built = true;
|
|
1908
|
+
}
|
|
1909
|
+
else {
|
|
1910
|
+
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${result.error || '未知错误'} ` +
|
|
1911
|
+
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
catch (e) {
|
|
1915
|
+
layout.contentWrite(`${ui.yellow}⚠ 快照构建失败:${ui.reset} ${e.message} ` +
|
|
1916
|
+
`${ui.dim}(开关已切换,系统提示词中的快照段将为空,稍后可用 /snapshot_refresh 重试)${ui.reset}\n`);
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
// 刷新 history[0] 使 system prompt 立即反映新值(仿 /snapshot_refresh)。
|
|
1920
|
+
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
1921
|
+
const note = nextEnabled
|
|
1922
|
+
? `${ui.green}已开启项目快照${ui.reset} — ` +
|
|
1923
|
+
`system prompt 将在下次 chat 注入「## Project Snapshot」段;` +
|
|
1924
|
+
`read_file 工具优先走 snapshot cache(mtime 校验失败时回退真实 readFile)。`
|
|
1925
|
+
: `${ui.yellow}已关闭项目快照${ui.reset} — ` +
|
|
1926
|
+
`system prompt 不再注入「## Project Snapshot」段;` +
|
|
1927
|
+
`read_file 工具直接走真实 readFile,不再查 cache。`;
|
|
1928
|
+
let extra = '';
|
|
1929
|
+
if (nextEnabled && built) {
|
|
1930
|
+
extra = `${ui.dim}(已构建快照;如需刷新用 /snapshot_refresh)${ui.reset}\n`;
|
|
1931
|
+
}
|
|
1932
|
+
layout.contentWrite(`${note}\n${extra}`);
|
|
1933
|
+
layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MOCODE_PROJECT_SNAPSHOT=${nextEnabled ? 'true' : 'false'};` +
|
|
1934
|
+
(process.env.MOCODE_PROJECT_SNAPSHOT
|
|
1935
|
+
? `${ui.dim}同 session shell 已 export MOCODE_PROJECT_SNAPSHOT,文件写入下次启动仍以 shell 值为准;取消 shell 设置后生效)${ui.reset}\n`
|
|
1936
|
+
: `${ui.dim}下次启动仍生效)${ui.reset}\n`));
|
|
1937
|
+
}
|
|
1938
|
+
catch (e) {
|
|
1939
|
+
layout.contentWrite(`${ui.red}/snapshot 失败:${ui.reset} ${e.message}\n`);
|
|
1940
|
+
}
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1826
1943
|
const bubbleRows = input.length + 2 + pendingAttachments.length; // N 行 message + 2 行尾随空(含 \n\n 留下的 open current 行) + 每附件 1 行
|
|
1827
1944
|
const shouldCommit = await awaitPendingRecall(input, pendingAttachments.length, placeholder);
|
|
1828
1945
|
if (!shouldCommit) {
|
package/dist/session/compact.js
CHANGED
|
@@ -323,7 +323,7 @@ export async function compactHistory(history, opts) {
|
|
|
323
323
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
324
324
|
contextState.lastEstimate = estimateAfter;
|
|
325
325
|
contextState.lastUsage = undefined;
|
|
326
|
-
layout.contentWrite(` ${ui.
|
|
326
|
+
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
327
327
|
return {
|
|
328
328
|
compacted: true,
|
|
329
329
|
summarized: false,
|
|
@@ -409,7 +409,7 @@ export async function compactHistory(history, opts) {
|
|
|
409
409
|
const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
|
|
410
410
|
contextState.lastEstimate = estimateAfter;
|
|
411
411
|
contextState.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
|
|
412
|
-
layout.contentWrite(` ${ui.
|
|
412
|
+
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
413
413
|
// 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
|
|
414
414
|
if (estimateAfter >= opts.threshold * opts.window) {
|
|
415
415
|
layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}压缩后仍超阈,可能存在超大单条;建议 /clear。${ui.reset}\n`);
|
|
@@ -427,7 +427,7 @@ export async function compactHistory(history, opts) {
|
|
|
427
427
|
contextState.lastEstimate = estimateAfter;
|
|
428
428
|
contextState.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
|
|
429
429
|
if (microcompactDone) {
|
|
430
|
-
layout.contentWrite(` ${ui.
|
|
430
|
+
layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
|
|
431
431
|
return {
|
|
432
432
|
compacted: true,
|
|
433
433
|
summarized: false,
|