mocode-ai 0.5.9 → 0.6.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 +2 -2
- package/README.zh-CN.md +2 -2
- package/dist/agent/core.js +1 -1
- package/dist/config/index.js +155 -67
- package/dist/context/classifier.js +0 -2
- package/dist/repl/index.js +44 -17
- package/dist/rollback/index.js +7 -2
- package/dist/sandbox/policy.js +0 -1
- package/dist/session/persist.js +31 -15
- package/dist/session/state.js +33 -0
- package/dist/tools/builtins/index.js +0 -2
- package/dist/ui/layout.js +3 -1
- package/dist/ui/theme.js +23 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
|
|
|
19
19
|
- **Automatic context compression** — As the context window fills up, a three-tier compression kicks in (trim individual results → compact older tool results in place → summarize older turns), so long sessions never overflow. `/context` shows live token usage; `/compact` triggers manual compression (optionally with a focus hint to preserve what matters).
|
|
20
20
|
- **Cross-session long-term memory** — The agent can save project architecture, conventions, and lessons learned as long-term memory, auto-loaded in future sessions. A background process periodically reflects on conversations to mine things worth remembering. Memories can be created, searched, updated, and forgotten, with recall-based decay.
|
|
21
21
|
- **Project context (Snapshot + Skill)** — Two complementary systems help the agent understand your project: **Project Snapshot** automatically scans files and generates LLM-enhanced summaries (project description, tech stack, commands, module responsibilities, directory tree); **Project Skill** is a manually maintained knowledge base capturing design decisions, architectural insights, pitfalls, and conventions. Snapshot provides *what/where* (facts), Skill provides *why/how* (insights) — no duplication, ~46% token savings.
|
|
22
|
-
- **
|
|
22
|
+
- **Session notepad (notes.md)** — For complex multi-step tasks (≥3 file changes / ≥5 tool calls), the agent maintains a working notepad at `.mocode/sessions/<sessionId>/notes.md` (file-based, survives context compression). It can record intermediate findings, design decisions, open questions, and structured plans. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]` when a `## Plan:` section is present. The agent manages the file directly with write_file/edit_file/read_file.
|
|
23
23
|
- **Interruptible and reversible** — Ctrl+C interrupts the current turn at any time (kills child processes recursively, rolls history back to before the turn started, leaves no half-finished tool calls). `/rollback` restores file changes from per-turn snapshots, with a per-file keep/undo choice — no git dependency required.
|
|
24
24
|
- **Sandbox protection** — File reads/writes go through a sandbox that blocks out-of-bounds paths (`../../`, absolute paths outside the root, symlink escapes, etc.), so the agent never touches files outside your working directory.
|
|
25
25
|
|
|
@@ -144,7 +144,7 @@ The agent operates in **the working directory it was launched from** — to have
|
|
|
144
144
|
| `switch_mode` | Switch between `plan` (read-only planning) and `auto` (full execution); the agent can call this itself to explore before acting |
|
|
145
145
|
| `drop_context` | Replace irrelevant old tool results in history with stubs to free up context (preserves tool_call_id pairing, leaves system prompt and current turn untouched, idempotent) |
|
|
146
146
|
| `task` | Spawn a sub-agent for an independent subtask (isolated history, optional restricted toolset, optional step cap); consecutive calls run in parallel automatically, returning only a summary |
|
|
147
|
-
|
|
147
|
+
|
|
148
148
|
| `memory_save` | Save a piece of cross-session long-term memory (title indexed, body fetched on demand) |
|
|
149
149
|
| `memory_search` | Search memory bodies by keyword; hits boost the recall count (affects forgetting decay) |
|
|
150
150
|
| `memory_list` | List the memory index (id/title/summary, no body) |
|
package/README.zh-CN.md
CHANGED
|
@@ -18,7 +18,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
|
|
|
18
18
|
- **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
|
|
19
19
|
- **上下文自动压缩** — 接近窗口上限时三层压缩(单条结果裁剪 → 旧工具结果原地微压缩 → 旧对话摘要),长会话也不爆窗口;`/context` 实时显示 token 用量,`/compact` 可手动压缩(能带焦点指令聚焦保留)。
|
|
20
20
|
- **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
|
|
21
|
-
-
|
|
21
|
+
- **会话记事本(notes.md)** — 复杂多步任务(≥3 处文件改动 / ≥5 步工具调用)时,agent 在 `.mocode/sessions/<sessionId>/notes.md` 维护一个工作记事本(落盘抗压缩),可记录中间发现、设计决策、待验证问题和结构化计划。TUI 状态栏实时显示进度 chip:`plan: [标题] (3/7) ▸ [当前步]`(当存在 `## Plan:` 段时)。agent 直接用 write_file/edit_file/read_file 管理此文件。
|
|
22
22
|
- **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
|
|
23
23
|
- **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
|
|
24
24
|
|
|
@@ -143,7 +143,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
|
|
|
143
143
|
| `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
|
|
144
144
|
| `drop_context` | 把历史里无关的旧工具结果替换为存根释放上下文(保 tool_call_id 配对,不动 system 与当前轮;幂等) |
|
|
145
145
|
| `task` | 派生子 agent 执行独立子任务(独立历史、可受限工具集、可设步数上限);连续多个自动并行,只回摘要 |
|
|
146
|
-
|
|
146
|
+
|
|
147
147
|
| `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
|
|
148
148
|
| `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
|
|
149
149
|
| `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
|
package/dist/agent/core.js
CHANGED
|
@@ -304,7 +304,7 @@ export async function runAgentCore(opts) {
|
|
|
304
304
|
if (READ_TOOL_NAMES.has(calls[i].name)) {
|
|
305
305
|
// 收集连续只读组(≥1),并发执行:先渲染所有 header,再一次性启动所有
|
|
306
306
|
// (executeTool 调用即开始 I/O),最后按原顺序逐个 await + 回灌。
|
|
307
|
-
// 必须先 header 后 execute:
|
|
307
|
+
// 必须先 header 后 execute:grep 等同步快速工具会在 executeTool 返回 Promise 前
|
|
308
308
|
// 已经完成;若先 started.map,用户只能在工具完成后才看到摘要与其前面的换行。
|
|
309
309
|
// 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
|
|
310
310
|
let j = i;
|
package/dist/config/index.js
CHANGED
|
@@ -4,6 +4,8 @@ import path from 'node:path';
|
|
|
4
4
|
import dotenv from 'dotenv';
|
|
5
5
|
import { loadSnapshot } from '../project-snapshot/index.js';
|
|
6
6
|
import { buildProjectSkillSection } from '../project-skill/index.js';
|
|
7
|
+
import { getSandboxRoot } from '../sandbox/root.js';
|
|
8
|
+
import { getCurrentSessionId } from '../session/state.js';
|
|
7
9
|
/**
|
|
8
10
|
* 按优先级加载配置文件并回填 process.env:
|
|
9
11
|
* 候选(后者覆盖前者,优先级升序):<cwd>/.env(兼容旧用法,最低)→ ~/.mocode/config(全局)→ <cwd>/.mocode/config(项目级覆盖,最高)。
|
|
@@ -103,6 +105,49 @@ function buildSnapshotSection() {
|
|
|
103
105
|
export function isProjectSnapshotEnabled() {
|
|
104
106
|
return config.projectSnapshotEnabled;
|
|
105
107
|
}
|
|
108
|
+
/**
|
|
109
|
+
* Session notepad 段落:读取 .mocode/sessions/<sessionId>/notes.md,只注入 ## 标题行作为目录摘要。
|
|
110
|
+
* Agent 用 write_file/edit_file/read_file 维护此文件,抗 compact(在 context window 之外)。
|
|
111
|
+
* 文件不存在或为空时返空串(零开销)。
|
|
112
|
+
*/
|
|
113
|
+
function buildNotepadSection() {
|
|
114
|
+
const sessionId = getCurrentSessionId();
|
|
115
|
+
if (!sessionId)
|
|
116
|
+
return '';
|
|
117
|
+
const root = getSandboxRoot() ?? process.cwd();
|
|
118
|
+
const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
|
|
119
|
+
if (!fs.existsSync(p))
|
|
120
|
+
return '';
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(p, 'utf8').trim();
|
|
123
|
+
if (!content)
|
|
124
|
+
return '';
|
|
125
|
+
// 1) 提取 ## 标题行(最多 15 个),用作目录摘要
|
|
126
|
+
const headers = content.split('\n')
|
|
127
|
+
.filter(l => /^##\s/.test(l))
|
|
128
|
+
.slice(0, 15);
|
|
129
|
+
// 2) 提取 Plan 段进度(仅当存在 "## Plan: ..." 时)
|
|
130
|
+
const planMatch = content.match(/^## Plan:\s*(.+)$/m);
|
|
131
|
+
const stepsTotal = (content.match(/^\s*-\s*\[[ xX]\]\s*\d+\./gm) || []).length;
|
|
132
|
+
const stepsDone = (content.match(/^\s*-\s*\[[xX]\]\s*\d+\./gm) || []).length;
|
|
133
|
+
const planChip = planMatch
|
|
134
|
+
? `\nPlan: ${planMatch[1].trim()} (${stepsDone}/${stepsTotal})`
|
|
135
|
+
: '';
|
|
136
|
+
if (headers.length === 0 && !planChip)
|
|
137
|
+
return '';
|
|
138
|
+
return [
|
|
139
|
+
'',
|
|
140
|
+
`## Session Notepad (your working notes — use read_file(".mocode/sessions/${sessionId}/notes.md") for details)`,
|
|
141
|
+
'Sections:',
|
|
142
|
+
...headers,
|
|
143
|
+
planChip,
|
|
144
|
+
'',
|
|
145
|
+
].join('\n');
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return '';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
106
151
|
const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
107
152
|
## Memory (cross-session long-term facts)
|
|
108
153
|
- A "memory index" (id/title/summary only) is injected into the system prompt. Retrieve full body via memory_search (pass id or keyword); use memory_list to see the entire index.
|
|
@@ -118,39 +163,31 @@ const SYSTEM_PROMPT_MEMORY_SECTION = `
|
|
|
118
163
|
* 句都不出现,且 read-only 列表里的 memory_search/memory_list 也移除——避免提示词里出现
|
|
119
164
|
* 根本不存在的工具名引起 LLM 调不到。
|
|
120
165
|
*/
|
|
166
|
+
const PLAN_RESEARCH_RULES = `
|
|
167
|
+
- Research enough to locate relevant code, trace call paths, and understand existing conventions. Use the codegraph-first Workflow, but do not repeat information already retrieved in this session.
|
|
168
|
+
- Produce an actionable plan: files and reasons, ordered steps, edge cases, and verification (typecheck / tests / build).
|
|
169
|
+
- When ready, MUST call the \`ask_human\` tool with a concise summary and exactly these options:
|
|
170
|
+
1. "按计划执行 (switch to auto and implement)" — call \`switch_mode("auto")\` in the same turn, then implement.
|
|
171
|
+
2. "继续细化方案 (stay in plan, refine)" — remain in plan and refine.
|
|
172
|
+
3. "取消 / 暂不执行 (abort)" — stop without switching mode.
|
|
173
|
+
- Never silently switch or stop. Do not ask approval in plain text; \`ask_human\` is the approval channel.
|
|
174
|
+
- The REPL approval prompt is only a fallback; do not rely on it.`;
|
|
121
175
|
function buildPlanModeSuffix() {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
-
|
|
136
|
-
|
|
137
|
-
- Note: the REPL may still show its own approval prompt (\`promptIntervention\`) as a defense-in-depth fallback if you somehow STOP without calling ask_human — do not rely on it; the primary path is ask_human.`;
|
|
138
|
-
}
|
|
139
|
-
return `
|
|
140
|
-
|
|
141
|
-
## ⛯ PLAN MODE (active now)
|
|
142
|
-
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
143
|
-
- Your editing / command / memory-write tools (write_file, edit_file, run_command, memory_save, memory_update, memory_forget) have been REMOVED from your tool list. Use only the read-only tools available to you (read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list) to investigate.
|
|
144
|
-
- Research thoroughly: locate the relevant code, trace call paths, and understand existing patterns and conventions before designing. (Codegraph is the default first action for code exploration — see Workflow in the base prompt. But first check whether this conversation already covers it — don't re-explore something already retrieved earlier in this session.)
|
|
145
|
-
- Then produce a clear, actionable implementation plan: files to change (with paths), what to change in each and why, the ordered steps, edge cases to handle, and how to verify (typecheck / tests / build). Be specific enough to execute against.
|
|
146
|
-
- When the plan is complete and ready for review, you MUST call the \`ask_human\` tool to surface the plan to the user for approval — do NOT just output the plan as plain text and STOP. ask_human renders a real interactive selection panel inside the TUI; plain-text approval questions in your reply are hard to see and easy to miss.
|
|
147
|
-
- Pass the \`ask_human\` tool a concise plan summary (goal + files/areas to change + key risks + verification) and these three options so the user can decide in one click:
|
|
148
|
-
1. "按计划执行 (switch to auto and implement)" — user approves; you then call \`switch_mode("auto")\` IN THE SAME turn and proceed.
|
|
149
|
-
2. "继续细化方案 (stay in plan, refine)" — user wants more detail / alternatives; stay in plan, iterate, and re-ask via \`ask_human\` when ready.
|
|
150
|
-
3. "取消 / 暂不执行 (abort)" — user wants to stop; STOP, do NOT call \`switch_mode\`.
|
|
151
|
-
- This applies to BOTH paths: whether the user said "先 plan 再 auto" (autonomous) or entered plan mode manually (via /plan or Shift+Tab) for safety review. The single rule is: never silently self-switch and never silently STOP — always route through \`ask_human\` so the user has an explicit chance to approve, refine, or cancel.
|
|
152
|
-
- Do NOT in your text reply ask rhetorical confirmation questions like "shall I proceed? / 是否同意 / 需要你确认吗" — that bypasses the panel and forces the user to type free-text feedback, which is strictly worse than picking from the 3 options. ask_human is the only sanctioned approval channel in plan mode.
|
|
153
|
-
- Note: the REPL may still show its own approval prompt (\`promptIntervention\`) as a defense-in-depth fallback if you somehow STOP without calling ask_human — do not rely on it; the primary path is ask_human.`;
|
|
176
|
+
const memoryTools = isMemoryEnabled()
|
|
177
|
+
? 'memory_save, memory_update, memory_forget'
|
|
178
|
+
: '';
|
|
179
|
+
const readOnlyTools = isMemoryEnabled()
|
|
180
|
+
? 'read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human, memory_search, memory_list'
|
|
181
|
+
: 'read_file, glob, grep, codegraph, web_search, web_fetch, use_skill, ask_human';
|
|
182
|
+
const removed = ['write_file', 'edit_file', 'run_command', memoryTools]
|
|
183
|
+
.filter(Boolean)
|
|
184
|
+
.join(', ');
|
|
185
|
+
return `
|
|
186
|
+
|
|
187
|
+
## ⛯ PLAN MODE (active now)
|
|
188
|
+
You are in PLAN mode: investigate and design only — do NOT execute or change anything.
|
|
189
|
+
- Removed from your tool list: ${removed}. Use only these read-only tools: ${readOnlyTools}.
|
|
190
|
+
${PLAN_RESEARCH_RULES}`;
|
|
154
191
|
}
|
|
155
192
|
/** 兼容旧名字:repl 的 buildSystemMessage 仍引 PLAN_MODE_SUFFIX(变量)。运行时按需现拼。 */
|
|
156
193
|
export function buildBasePrompt() {
|
|
@@ -161,7 +198,8 @@ export function buildBasePrompt() {
|
|
|
161
198
|
const planLine = isMemoryEnabled()
|
|
162
199
|
? '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command/memory-write tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.'
|
|
163
200
|
: '- For complex or multi-step tasks, the user may switch to PLAN mode (Shift+Tab): your editing/command tools are then removed from your tool list, and you must research with read-only tools only and produce a step-by-step plan (no execution). On approval the session returns to auto mode to execute the plan.';
|
|
164
|
-
return
|
|
201
|
+
return `## Core behavior
|
|
202
|
+
You are mocode, a terminal coding agent. Complete programming tasks through a "think → call tool → observe result → think again" loop until solved. Reply to the user in Chinese.
|
|
165
203
|
|
|
166
204
|
## 模式 (Modes)
|
|
167
205
|
${autoAllToolsLine}
|
|
@@ -169,40 +207,31 @@ ${planLine}
|
|
|
169
207
|
|
|
170
208
|
${PLATFORM_NOTE}
|
|
171
209
|
|
|
172
|
-
##
|
|
173
|
-
|
|
174
|
-
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
- **Batch read-only tools in parallel**: consecutive read-only tools (read_file, glob, grep, codegraph, web_search, web_fetch) auto-execute in parallel within one turn — this is the concrete read-side case of the rule above. Do NOT call them serially across turns when you could emit them together.
|
|
182
|
-
- **Decide before reading**: do not read files "just to see"; plan the 2-3 file paths you actually need, then emit them as one batched tool_calls turn.
|
|
183
|
-
- **Chain read→edit→verify in one turn**: when the edit is obvious after a read, call edit_file (and verify with run_command) in the SAME response — don't split into 3 separate turns.
|
|
184
|
-
- **Verify once at the end of an edit chain, not after every edit**: after batching a set of related edits, run a single typecheck / test / build command to verify the whole change together. Running a verify command after each individual edit_file call wastes turns — batch the edits, then verify once.
|
|
185
|
-
- **Don't repeat failed calls**: if the same tool call fails or returns the same content 3 times in this turn, switch strategy (use a different tool, ask the user, or re-read the tool description) — don't keep retrying the same shape.
|
|
210
|
+
## Tool details
|
|
211
|
+
### Token-efficient execution
|
|
212
|
+
- First check whether the answer is already in this conversation or a previous tool result. If yes, answer directly; do not re-run tools "to be safe".
|
|
213
|
+
- Plan the complete sub-task before calling tools. Batch independent reads in one turn. Because tool calls in one response execute without intermediate model reasoning, never batch a read with an edit that depends on its result.
|
|
214
|
+
- Do not read "just to see". Read only what supports the next decision. Re-read after a change, compaction, stale state, or uncertain line context.
|
|
215
|
+
- Prefer one precise call over overlapping searches. If a call fails, inspect the error and change the approach instead of repeating it unchanged.
|
|
216
|
+
- Batch only independent read-only calls. After their results arrive, make the dependent edit in the next turn; then batch independent edits and one final verification when their exact inputs are already known.
|
|
217
|
+
- Read only what supports the next decision; verify once after a related edit set, not after every edit.
|
|
218
|
+
- Do not repeat an unchanged failing call; after three unproductive attempts, change tools or ask for the missing decision.
|
|
186
219
|
- **Don't re-read a file you already have, unless it may have changed**: if you (or an earlier step in this session) already read a file's relevant content and nothing has touched it since, edit directly from that content instead of calling read_file again "to be safe". This does NOT apply when the file was edited (by you or externally) since your last read, when a prior edit may have shifted line numbers you're about to target, or right after a compact where you're unsure the surviving context is accurate — in those cases re-reading is expected and correct, not wasteful.
|
|
187
220
|
|
|
188
|
-
## Workflow
|
|
189
|
-
- Understand
|
|
190
|
-
-
|
|
191
|
-
-
|
|
192
|
-
-
|
|
193
|
-
- **Web search when freshness matters**: for tasks involving UI/interaction/copy/visual design, new SDKs or APIs, CVE/version upgrades, or anything likely past your training cutoff, web_search FIRST to ground your work in current material — don't fall back on stale templates (gradient+emoji defaults, "I hope this message finds you well" openers, generic AI-flavored phrasing). Routine coding (bug fixes, refactors, tests, internal docs) doesn't need it.
|
|
221
|
+
## Workflow
|
|
222
|
+
- Understand requirements and current code before acting; do not guess.
|
|
223
|
+
- If \`.codegraph/\` exists, use \`codegraph\` first for unfamiliar code questions. Use direct reads for known or recently changed files.
|
|
224
|
+
- After modifications, run the smallest relevant verification, then typecheck/build when appropriate. Never claim success without evidence.
|
|
225
|
+
- Use web search only when freshness materially affects the answer (new APIs, versions, security, current UI conventions).
|
|
194
226
|
|
|
195
|
-
## Tool
|
|
196
|
-
-
|
|
197
|
-
-
|
|
198
|
-
-
|
|
199
|
-
-
|
|
200
|
-
-
|
|
201
|
-
-
|
|
202
|
-
-
|
|
203
|
-
- **Trim context when stale**: when an old tool result is dead weight (sub-goal done, no downstream consumer, or superseded by a later read), call drop_context to stub it; otherwise rely on automatic pruning. **When your context gets too long, don't hesitate to use drop_context proactively** — it's cheap and designed to be called, not saved for emergencies.
|
|
204
|
-
- **Batch writes and commands too, not just reads**: the executor runs ALL returned tool_calls (reads, writes, commands) before the next LLM call. Emit independent edit_file / write_file / run_command in one response when the chain is clear — don't serialize them across turns just because they have side effects. (The read-only batching note in Step Economy applies to writes the same way.)
|
|
205
|
-
- **Chain shell workflows in a single \`run_command\`**: use \`&&\`, \`;\`, \`|\`, \`>\`, heredocs to fold multi-step scripts (\`mkdir -p x && cat > x/file.ts <<'EOF' ... EOF && npm test\`) into one call. Only emit a follow-up turn when the result forces a decision (error, ambiguous output, branching logic).
|
|
227
|
+
## Tool rules
|
|
228
|
+
- Precise path/symbol → go directly to \`read_file\` or \`codegraph node\`; use \`glob\`/\`grep\` only for discovery.
|
|
229
|
+
- Before editing, confirm the relevant content unless it is already current in this conversation. Use \`edit_file\` for unique local replacements and \`write_file\` for new/full files.
|
|
230
|
+
- Local edits require an exact unique match; use \`write_file\` for new/full files.
|
|
231
|
+
- Use \`glob\`/\`grep\` for discovery and \`run_command\` for execution or verification, not file existence checks. State intent before side effects.
|
|
232
|
+
- Call \`ask_human\` only when a real user decision is required; otherwise decide and proceed.
|
|
233
|
+
- Drop stale tool output when it no longer supports the current sub-task. Keep only evidence needed for the next decision.
|
|
234
|
+
- Batch independent writes only when each input is already known and their order does not matter. Keep dependent mutations sequential. Combine a clear shell workflow in one command; follow up when its result creates a decision.
|
|
206
235
|
|
|
207
236
|
## Large file writes (avoid token-cap truncation)
|
|
208
237
|
- \`write_file\` / \`edit_file\` arguments are part of the model's JSON output — a single tool call's content > ~5K tokens risks mid-stream truncation when the model's max output (default 8K–16K tokens) is exceeded, producing a "arguments 不是合法 JSON" error. Even with \`MAX_TOKENS=32000\` set, huge files still risk truncation.
|
|
@@ -219,10 +248,69 @@ ${PLATFORM_NOTE}
|
|
|
219
248
|
- Confirm with the user before irreversible or outward-facing operations (delete, overwrite existing files, push, request external services), unless explicitly authorized.
|
|
220
249
|
- Operate only within authorized scope; when unsure, ask — don't guess.
|
|
221
250
|
|
|
222
|
-
|
|
251
|
+
## Project context (dynamic reference)
|
|
252
|
+
${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}${buildNotepadSection()}
|
|
253
|
+
|
|
254
|
+
## Session Notepad — working notes file
|
|
255
|
+
${getCurrentSessionId()
|
|
256
|
+
? `You maintain a working notepad at \`.mocode/sessions/${getCurrentSessionId()}/notes.md\` using write_file / edit_file / read_file.`
|
|
257
|
+
: 'You maintain a working notepad (path will be shown after the session starts).'}
|
|
258
|
+
This is your private working surface — write intermediate findings, decisions, open questions,
|
|
259
|
+
and anything you might need to recall later. The file survives context compaction.
|
|
260
|
+
|
|
261
|
+
### WHEN TO WRITE
|
|
262
|
+
- After exploring code and discovering key constraints → add a section
|
|
263
|
+
- Before making a design decision → record reasoning and alternatives considered
|
|
264
|
+
- When accumulating data across tool calls → store intermediates
|
|
265
|
+
- When you realize something you might forget after compaction → write it down
|
|
266
|
+
- After completing a phase → summarize what you learned
|
|
267
|
+
|
|
268
|
+
### FORMAT (markdown, section-based)
|
|
269
|
+
Use \`## <topic>\` headers to organize. Each section is self-contained.
|
|
270
|
+
Example:
|
|
271
|
+
|
|
272
|
+
## Auth Module
|
|
273
|
+
- JWT TTL: 86400s, hardcoded at src/auth/jwt.ts:42
|
|
274
|
+
- Config path: config.auth.jwt.ttl (does not exist yet)
|
|
275
|
+
- Migration: read from config with fallback to 86400
|
|
276
|
+
|
|
277
|
+
## Decision: Schema Validation
|
|
278
|
+
- Chose: zod over joi
|
|
279
|
+
- Why: project already uses zod (config/index.ts:8), joi would add a dep
|
|
280
|
+
- Risk: none — zod already in dependency tree
|
|
281
|
+
|
|
282
|
+
## Open Questions
|
|
283
|
+
- [ ] Does the refresh token flow need TTL config too?
|
|
284
|
+
- [ ] Check if rate limiter interacts with auth middleware
|
|
285
|
+
|
|
286
|
+
### RULES
|
|
287
|
+
${getCurrentSessionId()
|
|
288
|
+
? `- Your notepad file path is: \`.mocode/sessions/${getCurrentSessionId()}/notes.md\`. Use this exact path for all read_file/write_file/edit_file operations on your notes.`
|
|
289
|
+
: '- Your notepad file path will be available after the session starts.'}
|
|
290
|
+
- Use write_file to create/overwrite; use edit_file to append or modify sections
|
|
291
|
+
- Keep the file concise — summarize, don't dump raw tool output
|
|
292
|
+
- At task completion, the file can be deleted or left for the user's reference
|
|
293
|
+
- Do NOT use this for cross-session knowledge (use memory_save for that)
|
|
294
|
+
|
|
295
|
+
### PLAN FORMAT (use for any task with ≥3 steps)
|
|
296
|
+
Write the plan as a top-level \`## Plan:\` section. The system extracts this for the status bar chip, so follow the format exactly.
|
|
297
|
+
|
|
298
|
+
## Plan: <task title>
|
|
299
|
+
|
|
300
|
+
Goal: <one-line goal>
|
|
301
|
+
|
|
302
|
+
### Steps
|
|
303
|
+
- [ ] 1. <step 1>
|
|
304
|
+
- [x] 2. <step 2>
|
|
305
|
+
- [ ] 3. <step 3>
|
|
306
|
+
|
|
307
|
+
### Progress
|
|
308
|
+
- <what you learned / did in this phase>
|
|
223
309
|
|
|
224
|
-
|
|
225
|
-
-
|
|
310
|
+
Rules:
|
|
311
|
+
- Only ONE active \`## Plan:\` section at a time.
|
|
312
|
+
- Mark steps \`[x]\` as you complete them; append a line to \`### Progress\` after each phase.
|
|
313
|
+
- When the plan is done, either delete the section or rename it to \`## Done: <title>\` so the status bar chip clears automatically.
|
|
226
314
|
|
|
227
315
|
## Termination & Reporting
|
|
228
316
|
- Stop immediately when no more tools are needed; give conclusions directly.
|
package/dist/repl/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { runAgent } from '../agent/index.js';
|
|
|
8
8
|
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
|
-
import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js';
|
|
11
|
+
import { ui, setTheme, getTheme, listThemes, themeExists, applyTerminalBackground } from '../ui/theme.js';
|
|
12
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';
|
|
@@ -23,8 +23,11 @@ import { manualCompact, contextState, newSessionId, saveSession, loadSession, li
|
|
|
23
23
|
import { listTurns, planRollback, applyRollback, persistSnapshots, loadSnapshots, rebuildFromHistory, resetState, } from '../rollback/index.js';
|
|
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
|
-
import { buildActivePlanSection, onActivePlanChange, hasActivePlan, getActivePlanSummary, } from '../plan/index.js';
|
|
27
26
|
import { buildSnapshot, clearSnapshotCache } from '../project-snapshot/index.js';
|
|
27
|
+
import fs from 'node:fs';
|
|
28
|
+
import path from 'node:path';
|
|
29
|
+
import { getSandboxRoot } from '../sandbox/root.js';
|
|
30
|
+
import { setCurrentSessionId, getCurrentSessionId } from '../session/state.js';
|
|
28
31
|
/**
|
|
29
32
|
* readline 的 prompt 必须是纯文本(无 ANSI):readline 按字符数算光标位置,
|
|
30
33
|
* 颜色码会让光标错位、编辑时漂移。颜色只用在直接 stdout.write 的横幅 / 工具行 / 回复。
|
|
@@ -162,6 +165,28 @@ function renderContextBarInline(history) {
|
|
|
162
165
|
const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
|
|
163
166
|
return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
|
|
164
167
|
}
|
|
168
|
+
/** 从 .mocode/sessions/<sessionId>/notes.md 读取活跃 plan 摘要(## Plan: 段)。每次状态栏刷新时同步读,文件小开销可忽略。 */
|
|
169
|
+
function readPlanFromNotes() {
|
|
170
|
+
const sessionId = getCurrentSessionId();
|
|
171
|
+
if (!sessionId)
|
|
172
|
+
return '';
|
|
173
|
+
const root = getSandboxRoot() ?? process.cwd();
|
|
174
|
+
const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
|
|
175
|
+
try {
|
|
176
|
+
const c = fs.readFileSync(p, 'utf8');
|
|
177
|
+
const title = c.match(/^## Plan:\s*(.+)$/m)?.[1].trim();
|
|
178
|
+
if (!title)
|
|
179
|
+
return '';
|
|
180
|
+
const total = (c.match(/^\s*-\s*\[[ xX]\]\s*\d+\./gm) || []).length;
|
|
181
|
+
const done = (c.match(/^\s*-\s*\[[xX]\]\s*\d+\./gm) || []).length;
|
|
182
|
+
const current = c.match(/^\s*-\s*\[ \]\s*\d+\.\s*(.+)$/m)?.[1].trim();
|
|
183
|
+
const summary = `plan: ${title} (${done}/${total})`;
|
|
184
|
+
return current ? `${summary} ▸ ${current}` : summary;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
165
190
|
/** 状态行基线:模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token。repl 在轮次边界、切模式、plan 变更时调。 */
|
|
166
191
|
function refreshStatusBase(history, lastTurnUsage) {
|
|
167
192
|
layout.setStatusBase({
|
|
@@ -169,7 +194,7 @@ function refreshStatusBase(history, lastTurnUsage) {
|
|
|
169
194
|
contextBar: renderContextBarInline(history),
|
|
170
195
|
cwd: process.cwd(),
|
|
171
196
|
modeTag: getAgentMode() === 'plan' ? 'Plan' : 'Auto',
|
|
172
|
-
planSummary:
|
|
197
|
+
planSummary: readPlanFromNotes(),
|
|
173
198
|
lastTurnUsage,
|
|
174
199
|
});
|
|
175
200
|
}
|
|
@@ -565,6 +590,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
565
590
|
// 异步触发 LLM 快照生成(不阻塞 REPL 启动)
|
|
566
591
|
buildSnapshot().catch(() => { });
|
|
567
592
|
}
|
|
593
|
+
// --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
594
|
+
let currentSessionId = sessionId;
|
|
595
|
+
if (!currentSessionId)
|
|
596
|
+
currentSessionId = newSessionId(); // 新会话立即分配 ID,确保 prompt 里有正确路径
|
|
597
|
+
setCurrentSessionId(currentSessionId, process.cwd()); // 必须在 buildBasePrompt() 之前,让 prompt 能读到 sessionId
|
|
568
598
|
// 构造系统提示:auto 用 base;plan 在 base 后追加按当前开关现拼的 plan suffix。
|
|
569
599
|
// 切模式时 applyMode 重算 history[0](history[0] 恒 system,compaction 保它,不破坏)。
|
|
570
600
|
// 活跃 plan 摘要拼在 memory 段后(systemPrompt 的尾段),todo 工具变更后 listener 重写 history[0]。
|
|
@@ -575,8 +605,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
575
605
|
const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt() +
|
|
576
606
|
(planMode ? getPlanModeSuffix() : '') +
|
|
577
607
|
buildMemorySection() +
|
|
578
|
-
buildMemoryIndexSection(isMemoryEnabled())
|
|
579
|
-
buildActivePlanSection());
|
|
608
|
+
buildMemoryIndexSection(isMemoryEnabled()));
|
|
580
609
|
// 有预加载(--resume)则用它,并把 history[0] 刷成当前 system prompt(config 可能已变);
|
|
581
610
|
// 否则新会话只塞 system 提示(默认 auto)。
|
|
582
611
|
const history = initialHistory && initialHistory.length
|
|
@@ -587,12 +616,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
587
616
|
history[0]?.role === 'system') {
|
|
588
617
|
history[0] = { role: 'system', content: buildSystemMessage(false) };
|
|
589
618
|
}
|
|
590
|
-
// --resume:读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
591
619
|
if (sessionId && initialHistory && initialHistory.length) {
|
|
592
620
|
if (!loadSnapshots(sessionId))
|
|
593
621
|
rebuildFromHistory(history);
|
|
594
622
|
}
|
|
595
|
-
let currentSessionId = sessionId;
|
|
596
623
|
// 反思 cadence 计数:每 reflectEveryN 轮 fire-and-forget 一次后台反思 pass。
|
|
597
624
|
let turnCount = 0;
|
|
598
625
|
// 本轮 token 累计:runAgent 返回后写入,供底栏模式 chip 右边显示。undefined=无实测
|
|
@@ -666,16 +693,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
666
693
|
applyMode(m === 'plan');
|
|
667
694
|
refreshStatusBase(history);
|
|
668
695
|
});
|
|
669
|
-
// 注册活跃 plan 变更监听器:todolist 工具每次 create/update/add_step/finish 后调 setActivePlan,
|
|
670
|
-
// 触发本 listener 重写 history[0](plan 摘要段刷新)+ 刷状态行 plan chip。
|
|
671
|
-
// 不调 drawStatusBar:INPUT 态靠 prompt.ts redraw;RUNNING 态靠 200ms turnTimer 兜底。
|
|
672
|
-
// listener 内访问 history 是闭包捕获,保持同一引用(repl 持有)。
|
|
673
|
-
onActivePlanChange(() => {
|
|
674
|
-
if (history[0]?.role === 'system') {
|
|
675
|
-
history[0] = { role: 'system', content: buildSystemMessage(getAgentMode() === 'plan') };
|
|
676
|
-
}
|
|
677
|
-
refreshStatusBase(history);
|
|
678
|
-
});
|
|
679
696
|
/**
|
|
680
697
|
* 回滚子流程(由 /rollback 触发):菜单(↑/↓)选轮次 → 选中第 X 轮 = 删第 X 轮及之后 + 预填第 X 轮 user 输入
|
|
681
698
|
* (Enter 重新跑该轮);被删轮次的文件改动走二选一菜单(promptRevertChoice:
|
|
@@ -739,6 +756,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
739
756
|
applyRollback(plan, history, revertPaths);
|
|
740
757
|
if (!currentSessionId)
|
|
741
758
|
currentSessionId = newSessionId();
|
|
759
|
+
setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
|
|
742
760
|
try {
|
|
743
761
|
saveSession(history, currentSessionId);
|
|
744
762
|
}
|
|
@@ -749,6 +767,11 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
749
767
|
// 复显剩余对话(无提示行),输入框预填该轮 user 输入 → 下轮 Enter 重新跑
|
|
750
768
|
layout.clearContent();
|
|
751
769
|
renderHistory(history);
|
|
770
|
+
// 末尾补空行:与后续用户消息(❯ bubble)之间分隔。runTurn 在每个 agent 轮结束后
|
|
771
|
+
// contentWrite('\n') 做轮次分隔,/resume 后接 \n\n,/theme·/model 后接 \n;
|
|
772
|
+
// rollbackFlow 原本漏了这一行,renderHistory 末尾的 batch 摘要行 / assistant 文本
|
|
773
|
+
// 收口后,续写位未稳到新空行,下一次 echoInput 的 ❯ 气泡会黏在最后一条输出后面。
|
|
774
|
+
layout.contentWrite('\n');
|
|
752
775
|
pendingPrefill = prefillText.split('\n');
|
|
753
776
|
};
|
|
754
777
|
/**
|
|
@@ -799,6 +822,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
799
822
|
// 成功轮次自动落盘(崩溃也保住上一轮);新会话首轮分配 id
|
|
800
823
|
if (!currentSessionId)
|
|
801
824
|
currentSessionId = newSessionId();
|
|
825
|
+
setCurrentSessionId(currentSessionId, process.cwd()); // 同步到 session/state,确保 notes.md 存在
|
|
802
826
|
try {
|
|
803
827
|
saveSession(history, currentSessionId);
|
|
804
828
|
}
|
|
@@ -852,6 +876,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
852
876
|
history.push(...loaded.history);
|
|
853
877
|
setAgentMode('auto'); // 续接重置为 auto(mode 不落盘;listener 重写 history[0] 回 auto,与 loaded 幂等)
|
|
854
878
|
currentSessionId = loaded.id;
|
|
879
|
+
setCurrentSessionId(loaded.id, process.cwd()); // 切换会话:确保该会话的 notes.md 存在
|
|
855
880
|
// 读回该会话的轮次/快照;无文件则从 history 重建 turns(无快照→旧轮次文件改动不可撤销)
|
|
856
881
|
if (!loadSnapshots(loaded.id))
|
|
857
882
|
rebuildFromHistory(history);
|
|
@@ -988,6 +1013,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
988
1013
|
history.length = 1; // 保留 system 提示
|
|
989
1014
|
resetState(); // 同步清空回滚轮次/快照
|
|
990
1015
|
currentSessionId = undefined; // 下轮起新会话文件
|
|
1016
|
+
setCurrentSessionId(undefined, process.cwd()); // 同步清空 session/state
|
|
991
1017
|
turnCount = 0; // 反思 cadence 重新计数
|
|
992
1018
|
contextState.lastUsage = undefined;
|
|
993
1019
|
contextState.correction = 1;
|
|
@@ -1284,6 +1310,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
|
|
|
1284
1310
|
// 切:setTheme → 重算状态行(新色)→ 清内容重绘(历史 / 横幅,镜像启动 + /resume)→ 确认 → 持久化。
|
|
1285
1311
|
// markdown MEMO 按 themeVersion 自动失效,故 renderHistory 取新色;状态栏 / 输入框由 continue 回 INPUT 态时读 getter 刷。
|
|
1286
1312
|
setTheme(name);
|
|
1313
|
+
applyTerminalBackground();
|
|
1287
1314
|
refreshStatusBase(history);
|
|
1288
1315
|
layout.clearContent();
|
|
1289
1316
|
if (history.some((m) => m.role === 'user')) {
|
package/dist/rollback/index.js
CHANGED
|
@@ -168,6 +168,10 @@ export function rebuildFromHistory(history) {
|
|
|
168
168
|
currentTurnId = 0;
|
|
169
169
|
}
|
|
170
170
|
function snapshotsPath(id) {
|
|
171
|
+
// 新式目录,回退旧式文件
|
|
172
|
+
const newPath = path.join(config.sessionDir, id, 'snapshots.json');
|
|
173
|
+
if (existsSync(newPath))
|
|
174
|
+
return newPath;
|
|
171
175
|
return path.join(config.sessionDir, `${id}.snapshots.json`);
|
|
172
176
|
}
|
|
173
177
|
/** 随 saveSession 调:把 turns + snapshots 落盘(turns 为空则跳过,不写空文件)。 */
|
|
@@ -175,8 +179,9 @@ export function persistSnapshots(id) {
|
|
|
175
179
|
if (turns.length === 0)
|
|
176
180
|
return;
|
|
177
181
|
try {
|
|
178
|
-
|
|
179
|
-
|
|
182
|
+
const dir = path.join(config.sessionDir, id);
|
|
183
|
+
mkdirSync(dir, { recursive: true });
|
|
184
|
+
writeFileSync(path.join(dir, 'snapshots.json'), JSON.stringify({ version: 1, turns, snapshots }), 'utf8');
|
|
180
185
|
}
|
|
181
186
|
catch {
|
|
182
187
|
// 落盘失败不阻断(回滚仅失去跨重启能力)
|
package/dist/sandbox/policy.js
CHANGED
package/dist/session/persist.js
CHANGED
|
@@ -42,7 +42,7 @@ function firstUserOf(history) {
|
|
|
42
42
|
return '';
|
|
43
43
|
}
|
|
44
44
|
function sessionPath(id) {
|
|
45
|
-
return path.join(config.sessionDir,
|
|
45
|
+
return path.join(config.sessionDir, id, 'session.json');
|
|
46
46
|
}
|
|
47
47
|
/** 保存会话到磁盘(history.length<=1 时跳过写盘,只返 meta)。 */
|
|
48
48
|
export function saveSession(history, id) {
|
|
@@ -54,14 +54,19 @@ export function saveSession(history, id) {
|
|
|
54
54
|
};
|
|
55
55
|
if (history.length <= 1)
|
|
56
56
|
return meta; // 仅 system,不落盘
|
|
57
|
-
sessionDir
|
|
57
|
+
const dir = path.join(config.sessionDir, id);
|
|
58
|
+
mkdirSync(dir, { recursive: true });
|
|
58
59
|
const record = { ...meta, history };
|
|
59
60
|
writeFileSync(sessionPath(id), JSON.stringify(record), 'utf8');
|
|
60
61
|
return meta;
|
|
61
62
|
}
|
|
62
|
-
/** 加载会话;不存在 / 损坏返 null(不抛)
|
|
63
|
+
/** 加载会话;不存在 / 损坏返 null(不抛)。优先新式目录,回退旧式文件。 */
|
|
63
64
|
export function loadSession(id) {
|
|
64
|
-
|
|
65
|
+
// 新式: .mocode/sessions/<id>/session.json
|
|
66
|
+
const newPath = path.join(config.sessionDir, id, 'session.json');
|
|
67
|
+
// 旧式: .mocode/sessions/<id>.json
|
|
68
|
+
const oldPath = path.join(config.sessionDir, `${id}.json`);
|
|
69
|
+
const p = existsSync(newPath) ? newPath : oldPath;
|
|
65
70
|
if (!existsSync(p))
|
|
66
71
|
return null;
|
|
67
72
|
try {
|
|
@@ -82,25 +87,36 @@ export function loadSession(id) {
|
|
|
82
87
|
}
|
|
83
88
|
}
|
|
84
89
|
/** 列出最近会话,按 createdAt 降序。损坏文件跳过。
|
|
85
|
-
* - limit?: 仅返回前 N
|
|
86
|
-
*
|
|
87
|
-
* /resume 在 sessions
|
|
90
|
+
* - limit?: 仅返回前 N 条。会话目录名是 YYYYMMDD-HHmmss,字典序=时间序;
|
|
91
|
+
* 先按目录名降序取前 N,再解析 session.json,避免
|
|
92
|
+
* /resume 在 sessions 目录堆了几百个子目录时 readdirSync + 全量 JSON.parse 慢。
|
|
88
93
|
* - 不传 limit 时读全部(向后兼容,供裸 --resume 列全表用)。
|
|
94
|
+
* - 向后兼容:同时扫描旧式 <id>.json 文件(扁平结构),优先读新式目录。
|
|
89
95
|
*/
|
|
90
96
|
export function listSessions(limit) {
|
|
91
97
|
if (!existsSync(config.sessionDir))
|
|
92
98
|
return [];
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
+
const entries = readdirSync(config.sessionDir, { withFileTypes: true });
|
|
100
|
+
const ids = [];
|
|
101
|
+
for (const e of entries) {
|
|
102
|
+
if (e.isDirectory() && /^\d{8}-\d{6}$/.test(e.name)) {
|
|
103
|
+
ids.push(e.name);
|
|
104
|
+
}
|
|
105
|
+
else if (e.isFile() && e.name.endsWith('.json') && !e.name.endsWith('.snapshots.json')) {
|
|
106
|
+
// 旧式扁平文件:兼容读取
|
|
107
|
+
ids.push(e.name.replace(/\.json$/, ''));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const all = ids.sort().reverse(); // 降序:最新在前
|
|
99
111
|
const toRead = typeof limit === 'number' ? all.slice(0, Math.max(0, limit)) : all;
|
|
100
112
|
const out = [];
|
|
101
|
-
for (const
|
|
113
|
+
for (const id of toRead) {
|
|
102
114
|
try {
|
|
103
|
-
|
|
115
|
+
// 优先新式目录,回退旧式文件
|
|
116
|
+
const newPath = path.join(config.sessionDir, id, 'session.json');
|
|
117
|
+
const oldPath = path.join(config.sessionDir, `${id}.json`);
|
|
118
|
+
const p = existsSync(newPath) ? newPath : oldPath;
|
|
119
|
+
const rec = JSON.parse(readFileSync(p, 'utf8'));
|
|
104
120
|
if (rec && typeof rec.id === 'string') {
|
|
105
121
|
out.push({
|
|
106
122
|
id: rec.id,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// session/state.ts - 会话状态跟踪模块
|
|
2
|
+
// 提供当前活跃会话 ID 的全局访问点,供 config/buildNotepadSection 等读取会话级 notes.md。
|
|
3
|
+
// 避免 repl/index.ts ↔ config/index.ts 循环依赖。
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
let currentSessionId;
|
|
7
|
+
/** 获取当前活跃会话 ID(供 buildNotepadSection 等使用)。 */
|
|
8
|
+
export function getCurrentSessionId() {
|
|
9
|
+
return currentSessionId;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 设置当前活跃会话 ID,并确保该会话的 notes.md 文件存在(不存在则创建空文件)。
|
|
13
|
+
* 由 repl/index.ts 在会话启动 / /resume 切换时调用。
|
|
14
|
+
*/
|
|
15
|
+
export function setCurrentSessionId(id, cwd) {
|
|
16
|
+
currentSessionId = id;
|
|
17
|
+
if (id)
|
|
18
|
+
ensureSessionNotes(id, cwd);
|
|
19
|
+
}
|
|
20
|
+
/** 确保 .mocode/sessions/<id>/notes.md 存在,不存在则创建空文件。 */
|
|
21
|
+
function ensureSessionNotes(id, cwd) {
|
|
22
|
+
const dir = path.join(cwd, '.mocode', 'sessions', id);
|
|
23
|
+
const file = path.join(dir, 'notes.md');
|
|
24
|
+
if (existsSync(file))
|
|
25
|
+
return;
|
|
26
|
+
try {
|
|
27
|
+
mkdirSync(dir, { recursive: true });
|
|
28
|
+
writeFileSync(file, '', 'utf8');
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// 创建失败不影响 REPL 主流程
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -17,7 +17,6 @@ import { memoryListTool } from './memory-list.js';
|
|
|
17
17
|
import { memoryUpdateTool } from './memory-update.js';
|
|
18
18
|
import { memoryForgetTool } from './memory-forget.js';
|
|
19
19
|
import { taskTool } from './task.js';
|
|
20
|
-
import { todolistTool } from './todolist.js';
|
|
21
20
|
import { projectSkillUpdateTool } from './project-skill-update.js';
|
|
22
21
|
/**
|
|
23
22
|
* 所有内置工具,按注册顺序排列。
|
|
@@ -64,5 +63,4 @@ export const builtinTools = [
|
|
|
64
63
|
..._memoryTools,
|
|
65
64
|
..._projectSkillTools,
|
|
66
65
|
taskTool, // 派生子 agent(独立 history + 可受限工具集);plan 模式禁用(见 PLAN_DISABLED_TOOLS)
|
|
67
|
-
todolistTool, // 工作记事本(plan 文件:复杂任务 checklist,落盘抗压缩);plan 模式可用(便于「先 plan 再 auto」时落地执行清单)
|
|
68
66
|
];
|
package/dist/ui/layout.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { stdin, stdout } from 'node:process';
|
|
2
2
|
import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, truncateAnsi, ansiDisplayWidth, wrapByDisplayWidth, fmtElapsed, stripAnsi, sliceByDisplayCol, } from './render.js';
|
|
3
|
-
import { ui } from './theme.js';
|
|
3
|
+
import { ui, applyTerminalBackground, resetTerminalBackground } from './theme.js';
|
|
4
4
|
import * as content from './content.js';
|
|
5
5
|
import * as mouse from './mouse.js';
|
|
6
6
|
import { shiftBatchesAfter } from './batch.js';
|
|
@@ -1886,6 +1886,7 @@ export function enterAltScreen() {
|
|
|
1886
1886
|
return;
|
|
1887
1887
|
active = true;
|
|
1888
1888
|
stdout.write(esc.altOn);
|
|
1889
|
+
applyTerminalBackground();
|
|
1889
1890
|
stdout.write(esc.mouseOn); // 完整鼠标追踪(按下/拖动/释放/滚轮)→ mouse.swallow 重组 → handleMouseEvent
|
|
1890
1891
|
mouse.setHandler(handleMouseEvent);
|
|
1891
1892
|
setRegion(6); // 1 虚拟空 + 1 spinner行 + 1 上线 + 1 输入 + 1 下线 + 1 model行(两行式底栏)
|
|
@@ -1943,6 +1944,7 @@ export function exitAltScreen() {
|
|
|
1943
1944
|
if (!active)
|
|
1944
1945
|
return;
|
|
1945
1946
|
active = false;
|
|
1947
|
+
resetTerminalBackground();
|
|
1946
1948
|
stopTurnTimer(); // 兜底清走时计时器(防异常退出泄漏)
|
|
1947
1949
|
turnStart = null;
|
|
1948
1950
|
scrollLockUntil = 0; // 清轮首滚动锁(防状态泄漏到下次进 alt 屏)
|
package/dist/ui/theme.js
CHANGED
|
@@ -33,6 +33,7 @@ const DEFAULT = {
|
|
|
33
33
|
// diff 行底色:One Dark bg #282c34 上加 ~14% 亮度的对应色,够辨识但不刺眼
|
|
34
34
|
addBg: '\x1B[48;2;44;62;42m', // 偏暗绿(One Dark green(152,195,121)暗化)
|
|
35
35
|
delBg: '\x1B[48;2;62;38;42m', // 偏暗红(One Dark red(224,108,117)暗化)
|
|
36
|
+
terminalBg: 'rgb:28/2c/34', // One Dark 经典深底,偏蓝灰的黑
|
|
36
37
|
};
|
|
37
38
|
/**
|
|
38
39
|
* 内建主题表。default=One Dark、light=Solarized Light、solarized=Solarized Dark、
|
|
@@ -58,6 +59,7 @@ const THEMES = {
|
|
|
58
59
|
// 比直接用 base1 更柔,跟深 fg(red/green)对比充足
|
|
59
60
|
addBg: '\x1B[48;2;220;235;205m',
|
|
60
61
|
delBg: '\x1B[48;2;245;218;215m',
|
|
62
|
+
terminalBg: 'rgb:1a/1e/24', // 深黑底带微蓝,与 light 主题文字反差大
|
|
61
63
|
},
|
|
62
64
|
solarized: {
|
|
63
65
|
red: '\x1B[38;2;220;50;47m',
|
|
@@ -74,6 +76,7 @@ const THEMES = {
|
|
|
74
76
|
// diff 行底色:Solarized Dark base03(0,43,54)上加对应色族暗 tint
|
|
75
77
|
addBg: '\x1B[48;2;20;50;38m',
|
|
76
78
|
delBg: '\x1B[48;2;55;30;30m',
|
|
79
|
+
terminalBg: 'rgb:00/2b/36', // Solarized Dark base03,深青黑
|
|
77
80
|
},
|
|
78
81
|
gruvbox: {
|
|
79
82
|
red: '\x1B[38;2;251;73;52m',
|
|
@@ -90,6 +93,7 @@ const THEMES = {
|
|
|
90
93
|
// diff 行底色:Gruvbox dark bg(40,40,40)上贴暗 bg0_a 风格
|
|
91
94
|
addBg: '\x1B[48;2;40;55;30m',
|
|
92
95
|
delBg: '\x1B[48;2;70;35;30m',
|
|
96
|
+
terminalBg: 'rgb:28/28/28', // Gruvbox 深黑底,带微暖色调
|
|
93
97
|
},
|
|
94
98
|
nord: {
|
|
95
99
|
red: '\x1B[38;2;191;97;106m',
|
|
@@ -106,6 +110,7 @@ const THEMES = {
|
|
|
106
110
|
// diff 行底色:Nord polar night(46,52,64)上贴对应色族暗 tint
|
|
107
111
|
addBg: '\x1B[48;2;46;66;52m',
|
|
108
112
|
delBg: '\x1B[48;2;72;46;52m',
|
|
113
|
+
terminalBg: 'rgb:2e/34/40', // Nord Polar Night 深灰蓝
|
|
109
114
|
},
|
|
110
115
|
orange: {
|
|
111
116
|
// 秋季/南瓜橙:深暖底(#231c16 系)+ accent 主强调走橙色;yellow 槽也偏橙
|
|
@@ -125,6 +130,7 @@ const THEMES = {
|
|
|
125
130
|
// diff 行底色:暖深棕底上贴对应色族暗 tint,跟 fg 配对柔和可辨
|
|
126
131
|
addBg: '\x1B[48;2;55;70;35m',
|
|
127
132
|
delBg: '\x1B[48;2;78;42;32m',
|
|
133
|
+
terminalBg: 'rgb:23/1c/16', // 深暖棕黑,带橙色调
|
|
128
134
|
},
|
|
129
135
|
rose: {
|
|
130
136
|
// 玫红:深紫底 + accent 玫粉作主强调;magenta 槽与 accent 同源(主题色),
|
|
@@ -143,6 +149,7 @@ const THEMES = {
|
|
|
143
149
|
// diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏紫红
|
|
144
150
|
addBg: '\x1B[48;2;50;60;42m',
|
|
145
151
|
delBg: '\x1B[48;2;75;40;52m',
|
|
152
|
+
terminalBg: 'rgb:1e/14/1c', // 深紫黑底,带玫红暖调
|
|
146
153
|
},
|
|
147
154
|
emerald: {
|
|
148
155
|
// 翡翠绿:深绿底 + accent 翡翠绿作主强调;green 槽与 accent 同源(主题色),
|
|
@@ -161,6 +168,7 @@ const THEMES = {
|
|
|
161
168
|
// diff 行底色:深绿底贴对应色族暗 tint,绿暗化偏深绿、红暗化偏暗红
|
|
162
169
|
addBg: '\x1B[48;2;30;55;40m',
|
|
163
170
|
delBg: '\x1B[48;2;60;40;40m',
|
|
171
|
+
terminalBg: 'rgb:10/1e/18', // 深绿黑底,翡翠色调
|
|
164
172
|
},
|
|
165
173
|
amber: {
|
|
166
174
|
// 琥珀金黄:深棕底 + accent 琥珀金黄作主强调;yellow 槽与 accent 同源(主题色),
|
|
@@ -179,6 +187,7 @@ const THEMES = {
|
|
|
179
187
|
// diff 行底色:深棕底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
|
|
180
188
|
addBg: '\x1B[48;2;50;55;25m',
|
|
181
189
|
delBg: '\x1B[48;2;70;40;28m',
|
|
190
|
+
terminalBg: 'rgb:1e/18/0e', // 深棕黑底,琥珀金黄调
|
|
182
191
|
},
|
|
183
192
|
lavender: {
|
|
184
193
|
// 薰衣草淡紫:深紫底 + accent 淡紫作主强调;magenta 槽与 accent 同源(主题色),
|
|
@@ -197,6 +206,7 @@ const THEMES = {
|
|
|
197
206
|
// diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏冷绿、红暗化偏冷紫红
|
|
198
207
|
addBg: '\x1B[48;2;38;46;42m',
|
|
199
208
|
delBg: '\x1B[48;2;60;40;55m',
|
|
209
|
+
terminalBg: 'rgb:18/14/24', // 深紫黑底,薰衣草冷调
|
|
200
210
|
},
|
|
201
211
|
sunset: {
|
|
202
212
|
// 日落珊瑚:深棕红底 + accent 珊瑚红作主强调;red 槽与 accent 同源(主题色);
|
|
@@ -215,6 +225,7 @@ const THEMES = {
|
|
|
215
225
|
// diff 行底色:深棕红底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
|
|
216
226
|
addBg: '\x1B[48;2;50;55;30m',
|
|
217
227
|
delBg: '\x1B[48;2;75;32;30m',
|
|
228
|
+
terminalBg: 'rgb:20/12/10', // 深棕红黑底,珊瑚暖调
|
|
218
229
|
},
|
|
219
230
|
};
|
|
220
231
|
let currentName = 'default';
|
|
@@ -222,6 +233,18 @@ let version = 0;
|
|
|
222
233
|
function currentPalette() {
|
|
223
234
|
return THEMES[currentName] ?? DEFAULT;
|
|
224
235
|
}
|
|
236
|
+
/** Apply the active theme's dark background to the terminal window (OSC 11). */
|
|
237
|
+
export function applyTerminalBackground() {
|
|
238
|
+
if (!isTTY)
|
|
239
|
+
return;
|
|
240
|
+
stdout.write(`\x1B]11;${currentPalette().terminalBg}\x07`);
|
|
241
|
+
}
|
|
242
|
+
/** Ask the terminal to restore its default background color. */
|
|
243
|
+
export function resetTerminalBackground() {
|
|
244
|
+
if (!isTTY)
|
|
245
|
+
return;
|
|
246
|
+
stdout.write('\x1B]111\x07');
|
|
247
|
+
}
|
|
225
248
|
/** 取某颜色字段的当前 ANSI 码(经 isTTY 门控)。 */
|
|
226
249
|
function color(key) {
|
|
227
250
|
return wrap(currentPalette()[key] ?? DEFAULT[key]);
|