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 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
- - **Working notepad (todolist)** — For complex multi-step tasks (≥3 file changes / ≥5 tool calls), the agent first writes a plan to `.mocode/plans/<id>.md` (file-based, survives context compression), then ticks each step as it goes. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]`. `finish` auto-archives completed plans to `plans/archive/`, with explicit `list / delete / unarchive` actions.
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
- | `todolist` | Working notepad: write a multi-step plan to `.mocode/plans/<id>.md` (survives compression) and tick steps as you go; `finish` auto-archives, with `list / delete / unarchive` for history |
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
- - **工作记事本(todolist)** — 复杂多步任务(≥3 处文件改动 / ≥5 步工具调用)时,agent 先把计划写到 `.mocode/plans/<id>.md`(落盘抗压缩),执行过程逐步勾选;TUI 状态栏实时显示进度 chip:`plan: [标题] (3/7) ▸ [当前步]`。`finish` 自动归档到 `plans/archive/`,`list / delete / unarchive` 管理历史。
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
- | `todolist` | 工作记事本:把多步计划写到 `.mocode/plans/<id>.md`(抗压缩),边执行边勾选;`finish` 自动归档,`list / delete / unarchive` 管历史 |
146
+
147
147
  | `memory_save` | 存一条跨会话长期记忆(标题进索引,正文按需取) |
148
148
  | `memory_search` | 按关键词搜记忆正文,命中即提升召回计数(影响遗忘衰减) |
149
149
  | `memory_list` | 列记忆索引(id/标题/摘要,无正文) |
@@ -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:todolist/grep 等同步快速工具会在 executeTool 返回 Promise 前
307
+ // 必须先 header 后 execute:grep 等同步快速工具会在 executeTool 返回 Promise 前
308
308
  // 已经完成;若先 started.map,用户只能在工具完成后才看到摘要与其前面的换行。
309
309
  // 异步工具(web_fetch 等)并发跑、总耗时 ≈ 最慢一个;同步工具(glob/grep)map 时已顺序跑完,await 即返。
310
310
  let j = i;
@@ -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
- if (!isMemoryEnabled()) {
123
- return `
124
-
125
- ## PLAN MODE (active now)
126
- You are in PLAN mode: investigate and design only do NOT execute or change anything.
127
- - Your editing / command tools (write_file, edit_file, run_command) 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) to investigate.
128
- - 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.)
129
- - 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.
130
- - 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.
131
- - 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:
132
- 1. "按计划执行 (switch to auto and implement)" — user approves; you then call \`switch_mode("auto")\` IN THE SAME turn and proceed.
133
- 2. "继续细化方案 (stay in plan, refine)" — user wants more detail / alternatives; stay in plan, iterate, and re-ask via \`ask_human\` when ready.
134
- 3. "取消 / 暂不执行 (abort)" — user wants to stop; STOP, do NOT call \`switch_mode\`.
135
- - 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.
136
- - 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.
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 `You are mocode, a terminal coding agent. You complete programming tasks through a "think → call tool → observe result → think again" loop until the problem is solved. Reply to the user in Chinese.
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
- ## Step / Turn Economy (read this first — saves LLM calls)
173
- - **Minimize turns**: each user message costs at least one LLM call, and history grows every step until threshold-triggered compact fires (extra call). If a request contains ≥2 independent sub-goals (e.g. "改 X 然后再优化 Y"), ask the user to split them into separate turns rather than chaining both in one go. State this politely: "这条包含 N 个独立目标,建议拆成 N 次对话,以避免上下文膨胀。"
174
- - **Answer directly when you already know the answer — do this before the batching rule below**: before planning any tool calls for this turn, first check whether you can answer from the current conversation, an earlier tool result already in context, a file/symbol already read in this session, or general reasoning/knowledge alone. If so, skip tools entirely and answer directly. Only call a tool when the info is genuinely missing, may be stale (the underlying file/state changed since you last read it), or requires verification you cannot do from context. This applies to every tool — codegraph, grep, web_search, run_command — not just read_file.
175
- - already have it: user asks "刚才那个函数在哪个文件" after codegraph_explore returned it two turns ago answer from that result, no new call.
176
- - pure reasoning: user asks "这个改动会不会影响性能" and the relevant code/logic is already visible in context reason and answer directly, no need to re-run a profiler or re-read the file "just to be safe".
177
- - wasteful: re-running grep/codegraph for a symbol whose location this same conversation already returned, "just to be sure".
178
- - **Plan the full turn, then emit it as one batch — this is the single biggest step-saver**: before emitting anything, enumerate every read / edit / command you'll need for this sub-goal, then return them together as one set of tool_calls (reads run in parallel, writes/commands run in the order given). Don't emit one call, observe, then emit the next in a follow-up turn when you could have planned both upfront.
179
- - one turn: \`[read_file A, read_file B, edit_file A, run_command 'npm test']\`
180
- - four turns: \`[read_file A]\` \`[read_file B]\` \`[edit_file A]\` \`[run_command 'npm test']\`
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 before acting: when unsure about requirements or code state, explore first; don't assume.
190
- - **Code exploration first action**: before reading files with read_file or searching with grep, check if a .codegraph/ index exists. If it does, use the codegraph tool (explore for questions/features, node for a specific symbol) as your FIRST step — it returns source + call paths in one shot. Only fall back to read_file/grep when codegraph misses, you need just-changed content, or you're editing a known small file. Build the index with \`codegraph init\` if none exists.
191
- - Small steps: break tasks into verifiable sub-steps. Before each step, think clearly about what to change and why.
192
- - Verify after change: run typecheck / tests / build via run_command to confirm it works. Never claim done without verification.
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 Guidelines
196
- - See each tool's own description for parameters and usage; this section covers selection strategy and pitfalls only.
197
- - **If the user gave a precise path or symbol, go directly**: read_file or codegraph node it don't pre-validate with glob/grep.
198
- - Before editing code, read_file to confirm actual content (with line numbers); don't guess from memory. (Skip if you already read this exact content earlier in this session and nothing has changed it since — see Step Economy above.)
199
- - For local edits use edit_file: old_string must be unique and match exactly (including indentation/newlines); include surrounding context lines to ensure uniqueness. Use write_file for new files or full rewrites.
200
- - Use glob to find file paths, grep to search content. **Don't use run_command for file-level checks** (existence / listing / type) — those have no clean cmd.exe equivalent and Windows path escaping fails often. Use \`glob\` to list, and just call \`read_file\` to test existence (returns ENOENT as a clean error string).
201
- - run_command has side effects on the host state intent before invoking (delete, install, push, reset, etc.).
202
- - Call ask_human when you hit a decision point requiring user input (multiple implementation approaches, unclear intent, or needing extra info to proceed) list options for the user to pick (they can also choose "custom input" to answer freely). Don't call it frequently when the task is clear and you can decide yourself; if the user cancels, switch approach or proceed with available info — don't re-ask the same question.
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
- ${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection() : ''}${memorySection}
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
- ## Working notepad (todolist)
225
- - For genuinely complex tasks only: explore codebase clarify with user → create plan → execute step by step. See tool description for details.
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.
@@ -38,8 +38,6 @@ const BY_NAME = {
38
38
  memory_forget: 'status',
39
39
  // summary:子 agent 摘要(轻量)
40
40
  task: 'summary',
41
- // status:工作记事本 plan(单 plan/会话,文件级;state 一行 chip 即可)
42
- todolist: 'status',
43
41
  };
44
42
  /**
45
43
  * 形状启发:对未在 BY_NAME 登记的工具输出做模式识别(为 MCP / 未来工具兜底)。
@@ -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: hasActivePlan() ? getActivePlanSummary(process.stdout.columns ?? 80) : '',
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')) {
@@ -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
- mkdirSync(config.sessionDir, { recursive: true });
179
- writeFileSync(snapshotsPath(id), JSON.stringify({ version: 1, turns, snapshots }), 'utf8');
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
  // 落盘失败不阻断(回滚仅失去跨重启能力)
@@ -17,7 +17,6 @@ export const SANDBOX_EXEMPT_TOOLS = new Set([
17
17
  'ask_human', 'switch_mode',
18
18
  'codegraph',
19
19
  'task',
20
- 'todolist', // 写 .mocode/plans/<id>.md(id 服务端生成,无路径注入);操作项目内元数据
21
20
  ]);
22
21
  /**
23
22
  * 路径类工具:enforceSandbox 集中把 args.path 重写为牢内绝对路径(默认安全;工具内
@@ -42,7 +42,7 @@ function firstUserOf(history) {
42
42
  return '';
43
43
  }
44
44
  function sessionPath(id) {
45
- return path.join(config.sessionDir, `${id}.json`);
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
- const p = sessionPath(id);
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 条。会话文件名是 YYYYMMDD-HHmmss.json,字典序=时间序;
86
- * 先按文件名降序取前 N,再只解析这 N 个文件(history 大字段全部跳过不读),避免
87
- * /resume 在 sessions 目录堆了几百个文件时 readdirSync + 全量 JSON.parse 慢。
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
- // 过滤掉 .snapshots.json:ASCII 排序里 's'(115) > 'j'(106),后者排在前面,会让
94
- // slice(0, limit) 取到一堆快照文件(JSON.parse 后 rec.id=undefined 被吞),真会话被挤掉。
95
- const all = readdirSync(config.sessionDir)
96
- .filter((f) => f.endsWith('.json') && !f.endsWith('.snapshots.json'))
97
- .sort() // YYYYMMDD-HHmmss.json 字典序 ≡ 时间序(同 createdAt 升序)
98
- .reverse(); // 降序:最新在前
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 f of toRead) {
113
+ for (const id of toRead) {
102
114
  try {
103
- const rec = JSON.parse(readFileSync(path.join(config.sessionDir, f), 'utf8'));
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]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {