mocode-ai 1.1.8 → 1.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,8 +8,6 @@ A terminal coding agent: give it a goal, and it **completes it autonomously**
8
8
 
9
9
  MoCode explores your code, reads/writes/edits files, runs shell commands, and searches the web on its own, driving the task forward through a loop of "think → call a tool → observe the result → think again." It works with any OpenAI-compatible endpoint (GLM, DeepSeek, Qwen, local Ollama / vLLM, etc.), runs as a full-screen TUI with streaming output and visible reasoning.
10
10
 
11
- ## Architecture
12
-
13
11
  ## Engineering discipline
14
12
 
15
13
  MoCode keeps code-level control light and leaves task strategy to the agent:
@@ -82,7 +80,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
82
80
  - **Pressure-driven context compression** — Normal history keeps full tool evidence. At 80% occupancy, one scheduler event runs all enabled cleanup and always follows with a history summary. `/context` shows live usage and `/compact` remains an explicit manual override.
83
81
  - **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.
84
82
  - **Project context (`MOCODE.md`)** — A single project-level memory file at `MOCODE.md` captures both static facts (project description, commands, module list, directory tree) and human/AI-written insights (conventions, architectural decisions, pitfalls). Generate it once with `/init`, then keep it up to date by hand or by asking the agent to refresh it. Loaded automatically into the system prompt on every turn.
85
- - **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.
83
+ - **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 records the execution plan with the dedicated `plan_update` tool — a three-state step machine (`pending`/`in_progress`/`completed`, at most one `in_progress`) that auto-settles to `## Done:` when finished. The active plan is re-injected into the system prompt after compaction and re-synced into context whenever notes.md changes, and a gentle reminder nudges the agent if it goes several tool-steps without updating the plan. A live progress chip in the TUI status bar shows `plan: [title] (3/7) ▸ [current step]`.
86
84
  - **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.
87
85
  - **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.
88
86
 
@@ -210,6 +208,7 @@ The agent operates in **the working directory it was launched from** — to have
210
208
  | `web_fetch` | Fetch a URL, cleaning HTML into plain text |
211
209
  | `use_skill` | Load the full SKILL.md instructions for a given skill |
212
210
  | `ask_human` | Pop up a Q&A panel at decision points; user picks a preset or types freely (blocks until answered) |
211
+ | `plan_update` | Record/update the session execution plan (the `## Plan:` block in notes.md); three-state steps, at most one in_progress, auto-settles to `## Done:` when all complete |
213
212
  | `switch_mode` | Switch between `plan` (read-only planning) and `auto` (full execution); the agent can call this itself to explore before acting |
214
213
  | `sub-agent` | Spawn a capable isolated worker; read tasks can run concurrently and writes use overlay + ChangeSet safe merge |
215
214
 
@@ -221,12 +220,31 @@ The agent operates in **the working directory it was launched from** — to have
221
220
 
222
221
  The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle at runtime with `/memory_switch` (REPL restart required, by design — see Skills section for the difference between Tier-1 `MOCODE.md` and Tier-2 memory).
223
222
 
223
+ The four frontend tools — `browser`, `dev_server`, `screenshot`, `view_image` — are **off by default** (they depend on the Playwright binary, spawn long-lived processes, or capture the desktop). Enable the whole cluster at runtime with `/fe on`; the model only sees them once enabled. Toggle with `/fe on|off|status`.
224
+
225
+ ### Frontend / UI loop
226
+
227
+ `dev_server` + `browser` form a loop of "start it → open the page → see the rendered result":
228
+
229
+ ```
230
+ dev_server start command="npm run dev" readyUrl="http://localhost:5173"
231
+ browser open → navigate → click / fill → screenshot
232
+ dev_server stop id=srv-xxxx
233
+ ```
234
+
235
+ - `dev_server` processes survive across tool calls (`run_command` can't — it tree-kills children on timeout or when the turn is interrupted). Readiness waiting supports `readyUrl` (loopback only) or `readyPattern` (matches startup logs); logs go to `.mocode/dev-servers/<id>.log` and support incremental reads via `offset`.
236
+ - `browser` page sessions also persist across calls; screenshots feed back to the model through the multimodal channel, along with recent console output, page errors, and failed requests.
237
+ - Safe defaults: `browser` only allows `http/https` on `localhost / 127.0.0.1 / ::1`, rejecting `file:` and credentialed URLs; set `MOCODE_BROWSER_ALLOW_REMOTE=true` to reach remote hosts. `dev_server` runs arbitrary commands and shares `run_command`'s `dangerous` risk class — requires user confirmation before execution.
238
+ - Both are disabled in plan mode; on exit mocode tree-kills background processes and closes the browser.
239
+ - The browser binary is not bundled with the npm package; run `npx playwright install chromium` before first use.
240
+
224
241
  ## Slash commands
225
242
 
226
243
  | Command | Purpose |
227
244
  | ------------------ | ----------------------------------------------------------------------- |
228
245
  | `/exit` `/quit` | Exit MoCode |
229
246
  | `/clear` | Clear history (keeps the system prompt) + clear screen |
247
+ | `/image` | Attach a local image to the next message; supports `attach <path>` / `list` / `clear` |
230
248
  | `/context` | Show a context usage bar (tokens / message count, estimated or measured) |
231
249
  | `/skills` | List discovered skills |
232
250
  | `/compact` | Compress history (optionally with a focus hint: `/compact …`) |
@@ -241,6 +259,7 @@ The five `memory_*` tools are gated on `MEMORY_ENABLED=true` at startup; toggle
241
259
  | `/plan` | Switch to plan mode (read-only exploration + plan output, approve to switch to auto) |
242
260
  | `/auto` | Switch back to auto mode (full toolset execution) |
243
261
  | `/pet` | Toggle the optional desktop pet (floating window mirroring agent state) |
262
+ | `/fe` | Toggle the frontend tool cluster `browser` / `dev_server` / `screenshot` / `view_image` on/off (off by default) |
244
263
  | `/pet skin` | Pick a pet skin (↑↓ · Enter) |
245
264
  | `/pet quit` | Fully shut down the pet process (not just disconnect) |
246
265
 
package/README.zh-CN.md CHANGED
@@ -60,8 +60,6 @@ mocode 不会在任务结束时暗中启动验证瀑布。agent 可以根据任
60
60
 
61
61
  <p align="center"><img src="./assets/architecture/pet-bridge-zh-CN.svg" alt="MoCode 桌宠桥:hooks、事件帧、Electron 客户端" width="100%"></p>
62
62
 
63
- ## 为什么用 mocode
64
-
65
63
  ## 工程化纪律
66
64
 
67
65
  mocode 把代码层控制保持得尽量轻,把任务策略交给 agent:
@@ -81,7 +79,7 @@ mocode 不是一个套壳聊天框,而是一个能真正动手干活的 agent:
81
79
  - **计划 / 执行双模式** — `plan` 模式下只读探查(读代码、查索引、搜索,绝不写盘、不跑命令、不派生子 agent),产出计划;`auto` 模式全量工具放开。agent 还能在两者间自切换——先把陌生代码库摸清,再动手改。
82
80
  - **统一压力驱动压缩** — 正常 history 保留完整工具证据;达到 80% 后由一次调度事件运行所有已启用的清理,并始终继续 history 摘要。`/context` 显示实时用量,`/compact` 仍是用户显式覆盖。
83
81
  - **跨会话长期记忆** — agent 能把项目架构、约定、踩过的坑存成长期记忆,下次会话自动加载;后台还会定期从对话里反思挖掘值得记住的事。记忆可增删改、带召回衰减。
84
- - **会话记事本(notes.md)** — 复杂多步任务(≥3 处文件改动 / ≥5 步工具调用)时,agent 在 `.mocode/sessions/<sessionId>/notes.md` 维护一个工作记事本(落盘抗压缩),可记录中间发现、设计决策、待验证问题和结构化计划。TUI 状态栏实时显示进度 chip:`plan: [标题] (3/7) ▸ [当前步]`(当存在 `## Plan:` 段时)。agent 直接用 write_file/edit_file/read_file 管理此文件。
82
+ - **会话记事本(notes.md)** — 复杂多步任务(≥3 处文件改动 / ≥5 步工具调用)时,agent 在 `.mocode/sessions/<sessionId>/notes.md` 维护一个工作记事本(落盘抗压缩),可记录中间发现、设计决策、待验证问题和结构化计划。执行计划由专用 `plan_update` 工具维护——三态步骤机(`pending`/`in_progress`/`completed`,同一时刻至多一个 `in_progress`),全部完成自动结算为 `## Done:`。活跃 plan 在压缩后重注入系统提示、notes.md 一变就重同步进上下文,若连续多步未更新还会有温和提醒。TUI 状态栏实时显示进度 chip:`plan: [标题] (3/7) ▸ [当前步]`。
85
83
  - **可中断、可回滚** — Ctrl+C 随时打断当前轮次(树杀子进程,历史还原到本轮开始前,不留残半的工具调用);`/rollback` 按轮次快照恢复文件改动,逐个文件「保留/撤销」,不依赖 git。
86
84
  - **沙箱防护** — 文件读写经沙箱拦截,挡掉越界路径(`../../`、绝对外圈、软链出圈等),不碰工作目录之外的文件。
87
85
 
@@ -213,6 +211,7 @@ agent 工作在**启动时所在的工作目录**——想让它操作某个项
213
211
  | `web_fetch` | 抓取指定 URL,HTML 清洗成纯文本 |
214
212
  | `use_skill` | 加载某 skill 的完整 SKILL.md 指令 |
215
213
  | `ask_human` | 决策点弹终端问答面板,用户选预设项或自由输入(阻塞至回应) |
214
+ | `plan_update` | 记录/更新会话执行计划(notes.md 的 `## Plan:` 段);三态步骤机,同一时刻至多一个 in_progress,全部完成自动结算为 `## Done:` |
216
215
  | `switch_mode` | 在 `plan`(只读规划)与 `auto`(全量执行)间切换;agent 可自行调用,先探查再动手 |
217
216
  | `sub-agent` | 派生具备完整能力的隔离子 Agent;只读任务可并发,写任务通过 overlay + ChangeSet 安全合并 |
218
217
 
@@ -238,6 +237,8 @@ dev_server stop id=srv-xxxx
238
237
  - 两者在 plan 模式下均被禁用;mocode 退出时会树杀后台进程并关闭浏览器。
239
238
  - 浏览器二进制不随 npm 包分发,首次使用前需 `npx playwright install chromium`。
240
239
 
240
+ 这 4 个前端工具 —— `browser`、`dev_server`、`screenshot`、`view_image` —— **默认关闭**(依赖 Playwright 二进制、会拉起长驻进程或截取桌面)。运行时用 `/fe on` 整体开启,开启后模型才看得到;用 `/fe on|off|status` 切换。
241
+
241
242
  5 个 `memory_*` 工具受启动时 `MEMORY_ENABLED=true` 总开关控制;运行时切换用 `/memory_switch`(需重启 REPL,刻意为之,见下「项目记忆」小节区分 Tier-1 / Tier-2)。
242
243
 
243
244
  ## 斜杠命令
@@ -261,6 +262,7 @@ dev_server stop id=srv-xxxx
261
262
  | `/plan` | 切到 plan 模式(只读探查 + 产出计划,审批后切 auto 执行) |
262
263
  | `/auto` | 切回 auto 模式(全量工具执行) |
263
264
  | `/pet` | 开关桌宠(独立悬浮窗,镜像 agent 状态动画) |
265
+ | `/fe` | 切换前端工具簇 `browser` / `dev_server` / `screenshot` / `view_image` 的开关(默认关闭) |
264
266
  | `/pet skin` | 选桌宠皮肤(↑↓ · Enter) |
265
267
  | `/pet quit` | 完全关闭桌宠进程(而非仅断开本连接) |
266
268
 
@@ -5,6 +5,7 @@
5
5
  // 与 index.ts 的关系:index.ts 的 runAgent = runAgentCore + TUI hooks 薄封装(行为不变)。
6
6
  // spawn.ts 的 spawnAgent = runAgentCore + 静默 hooks(子 agent)。
7
7
  import { readFileSync } from 'node:fs';
8
+ import { getNotesMtime } from '../session/notes.js';
8
9
  import { chat, estimatePromptTokens, planChatTools, chatTools, } from '../llm/index.js';
9
10
  import { executeToolOutcome, getToolCapabilities, isFileMutationTool, tools, } from '../tools/registry.js';
10
11
  import { checkPermission } from '../permissions/index.js';
@@ -17,13 +18,19 @@ import { createBudgetScheduler } from '../session/scheduler.js';
17
18
  import { recordArtifact, invalidateArtifacts, rehydrateArtifacts, } from '../context/index.js';
18
19
  import { createRelevancePruner } from '../context/relevance.js';
19
20
  import { isToolResultSuccess } from '../context/utils.js';
20
- import { config } from '../config/index.js';
21
+ import { config, extractActivePlanSection, reinjectActivePlanIntoSystem } from '../config/index.js';
21
22
  import { t } from '../i18n/index.js';
22
23
  import { jailResolve } from '../sandbox/index.js';
23
24
  import { createLifecycleEngine } from '../context/lifecycle.js';
24
25
  import { getTokenCalibration, updateTokenCalibration, } from '../context/token-calibration.js';
25
26
  import { getCurrentTurnId, getCurrentTurnMutationState } from '../rollback/index.js';
26
27
  import { getCurrentSessionId } from '../session/state.js';
28
+ /** nag 提醒阈值:连续 N 个"执行了工具但没更新 notes.md"的步后提醒一次(对齐 Claude Code TodoWrite 的 3 轮)。 */
29
+ const PLAN_NAG_THRESHOLD = 3;
30
+ /** nag 提醒文本:注入到当前步第一条 tool_result 内容前(与最新工具输出同批被模型看到,而非单独一条易被冲淡)。 */
31
+ const PLAN_NAG_TEXT = '[mocode] Reminder: you have an active plan in notes.md but have not updated it recently. ' +
32
+ 'If you finished a step, call plan_update to check it off (keep at most one in_progress); ' +
33
+ 'if the whole plan is done, let plan_update settle it to ## Done:. If the plan changed scope, update it to match reality.';
27
34
  /** 解析工具 arguments JSON;非法或空返 null(调用方据此降级到普通 preview)。 */
28
35
  function parseArgs(raw) {
29
36
  try {
@@ -176,6 +183,10 @@ export async function runAgentCore(opts) {
176
183
  let done = false; // 正常完毕 / 达上限 true;中断 false(不显摘要)
177
184
  let traceStatus = 'error';
178
185
  let toolCallCount = 0;
186
+ // A+B(plan 可靠性):跨步计数"执行了工具但没改动 notes.md"的连续步数。
187
+ // 本步写了 notes.md(plan_update 或直接 write/edit)→ 清零并重同步 history[0];
188
+ // 否则累计,达阈值则在当前步 tool_result 前注入 nag 提醒。
189
+ let stepsSincePlanTouch = 0;
179
190
  // 本轮 token 累计:每步 chat() 返回后把 result.usage 累加,供 onDone 摘要行 + AgentRunResult.usage
180
191
  // 透传给 repl(显示在底栏模式 chip 右边)。未开启 include_usage 或全失败时为 undefined。
181
192
  let turnUsage;
@@ -315,6 +326,8 @@ export async function runAgentCore(opts) {
315
326
  runtimeContextState.lifecycleStats = lifecycle.stats();
316
327
  }
317
328
  rehydrateArtifacts(runtimeContextState, history);
329
+ // ② compact 后把活跃 plan 重注入系统提示,避免 agent 因上下文压缩丢失执行计划。
330
+ reinjectActivePlanIntoSystem(history);
318
331
  }
319
332
  hooks.onStepStart?.(); // 主 agent:spinner.start('思考中')
320
333
  mode = 'idle';
@@ -450,6 +463,10 @@ export async function runAgentCore(opts) {
450
463
  function: { name: tc.name, arguments: tc.arguments },
451
464
  })),
452
465
  });
466
+ // A+B:记录本步第一条 tool_result 的下标 + 执行前 notes.md 的 mtime,
467
+ // 工具全部执行完后据此判断"本步是否改动了 notes.md"(重同步 / nag)。
468
+ const toolResultStartIdx = history.length;
469
+ const notesMtimeBefore = getNotesMtime();
453
470
  // Record interstitial narration for observability only. It never changes tool output
454
471
  // or injects instructions back into the model context.
455
472
  const narration = result.content?.trim() ?? '';
@@ -733,6 +750,25 @@ export async function runAgentCore(opts) {
733
750
  i++;
734
751
  }
735
752
  }
753
+ // A(事件驱动重同步):本步若改动了 notes.md,把最新 plan 块刷回 history[0],
754
+ // 让模型上下文镜像当前勾选态(不再停留在轮首的旧副本)。只在 mtime 变化时触发,零额外 churn。
755
+ // B(nag 提醒):连续 N 步有工具活动但没更新 plan,在当前步第一条 tool_result 前注入提醒。
756
+ const notesMtimeAfter = getNotesMtime();
757
+ if (notesMtimeAfter !== notesMtimeBefore) {
758
+ reinjectActivePlanIntoSystem(history);
759
+ stepsSincePlanTouch = 0;
760
+ }
761
+ else {
762
+ stepsSincePlanTouch += 1;
763
+ if (stepsSincePlanTouch >= PLAN_NAG_THRESHOLD) {
764
+ const activePlan = extractActivePlanSection();
765
+ const firstToolMsg = history[toolResultStartIdx];
766
+ if (activePlan && firstToolMsg && firstToolMsg.role === 'tool' && typeof firstToolMsg.content === 'string') {
767
+ firstToolMsg.content = `${PLAN_NAG_TEXT}\n\n${firstToolMsg.content}`;
768
+ }
769
+ stepsSincePlanTouch = 0;
770
+ }
771
+ }
736
772
  if (modelAttachments.length > 0) {
737
773
  const names = modelAttachments.map((attachment) => attachment.name).join(', ');
738
774
  const content = [
@@ -117,9 +117,9 @@ function flushToolBatch(expandSingleEntry = false) {
117
117
  batch.endBatch(id, layout);
118
118
  if (expandSingleEntry)
119
119
  batch.expandSingleEntryFully(id, layout);
120
- // 普通摘要只有一个“当前空行”,再 break 一次把它提交为分隔空行。
121
- // mutation 自动展开时 content.insertAfter 已先把该当前空行提交到 rows;若这里仍补 \n,
122
- // diff 后就会固定出现两条空白行。
120
+ // 普通批:endBatch 留了 1 个 hasCurrent 空行,break 一次把它提交为分隔空行。
121
+ // mutation 自动展开:expandSingleEntryFully 自己补 separator (\n),这里不能再补 \n,
122
+ // 不然 diff 后面就会出现两条空白行。
123
123
  if (!expandSingleEntry)
124
124
  layout.contentWrite('\n');
125
125
  }
@@ -68,6 +68,8 @@ export async function spawnAgent(opts) {
68
68
  const requested = opts.tools?.length ? new Set(opts.tools) : null;
69
69
  const readOnly = new Set(['read_file', 'glob', 'grep', 'web_search', 'web_fetch', 'use_skill', 'memory_search', 'memory_list']);
70
70
  toolsOverride = chatTools.filter((tool) => tool.function.name !== 'sub-agent' &&
71
+ // plan_update 直写主会话 notes.md(不走 overlay),子代理不应改动主计划——统一排除。
72
+ tool.function.name !== 'plan_update' &&
71
73
  (!requested || requested.has(tool.function.name)) &&
72
74
  (mode === 'write' || readOnly.has(tool.function.name)));
73
75
  // 独立 history(子 agent 自己持有,不共享主对话)。
@@ -20,12 +20,11 @@ export function inferModelFamily(model) {
20
20
  * 4 阶段核心纪律(英文)。4 个 model family 共用此文本,只在首句与标题
21
21
  * 标签上做轻量变体。保持短小,详细的完成检查由动态 checklist 按需注入。
22
22
  */
23
- const CORE_SECTION = `## Working discipline coding tasks
24
-
25
- Use your judgment to choose the shortest reliable path from the request to a useful result.
23
+ const CORE_SECTION = `Use your judgment to choose the shortest reliable path from the request to a useful result.
26
24
 
27
25
  - Inspect only the code and context needed for the next decision.
28
26
  - Make the smallest coherent change and avoid unrelated refactors.
27
+ - Preserve existing behavior and public API compatibility unless the task explicitly requires a change.
29
28
  - Decide whether validation is useful based on risk, scope, available commands, and the user's request. Validation is optional, not a completion gate.
30
29
  - When validation is useful, choose the smallest relevant check yourself; do not run broad test/build suites by default.
31
30
  - Re-read or rerun only when evidence is stale or the next edit depends on exact current content.
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import dotenv from 'dotenv';
5
- import { getSandboxRoot } from '../sandbox/root.js';
6
5
  import { getCurrentSessionId } from '../session/state.js';
6
+ import { getNotesFilePath } from '../session/notes.js';
7
7
  import { buildWorkDisciplineSection, inferModelFamily } from '../agent/work-discipline.js';
8
8
  import { detectLanguage, setLanguage, t, } from '../i18n/index.js';
9
9
  /**
@@ -62,17 +62,52 @@ export function isModelConfigured() {
62
62
  }
63
63
  const PLATFORM_NOTE = (() => {
64
64
  if (process.platform === 'win32') {
65
- return `## Environment (Windows)
66
- - \`run_command\` uses \`cmd.exe /c\`: use cmd syntax and \`%VAR%\`; Unix builtins and command substitution are unavailable.
65
+ return `- This is Windows: \`run_command\` uses \`cmd.exe /c\` — use cmd syntax and \`%VAR%\`; Unix builtins and command substitution are unavailable.
67
66
  - Prefer read_file/glob/grep for file discovery and reading. When shell is necessary, use forward-slash paths or invoke PowerShell explicitly.`;
68
67
  }
69
68
  if (process.platform === 'darwin') {
70
- return `## Environment (macOS)
71
- - \`run_command\` uses bash with BSD utilities. Prefer read_file/glob/grep; account for BSD/GNU differences when shell commands are necessary.`;
69
+ return `- This is macOS: \`run_command\` uses bash with BSD utilities. Prefer read_file/glob/grep; account for BSD/GNU differences when shell commands are necessary.`;
72
70
  }
73
- return `## Environment (Linux/Unix)
74
- - \`run_command\` uses bash. Prefer read_file/glob/grep when they fit; otherwise use standard POSIX/GNU syntax.`;
71
+ return `- This is Linux/Unix: \`run_command\` uses bash. Prefer read_file/glob/grep when they fit; otherwise use standard POSIX/GNU syntax.`;
75
72
  })();
73
+ /**
74
+ * 默认「声音」(Voice):给 mocode 一点人情味与性格,贴近 ChatGPT / 豆包的语感——
75
+ * 简洁但有温度、有观点、不谄媚、不啰嗦。这是性格的"底座"。
76
+ * 性格主要来自**身段/语气约束**,而非长篇指令,所以这段文字很短,不撑爆系统提示。
77
+ * 用户可用下列方式整段替换(自定义品牌声音):
78
+ * 1. `<cwd>/.mocode/persona.md`(项目级,最高)或 `~/.mocode/persona.md`(全局)
79
+ * 2. 环境变量 `MOCODE_PERSONA`(整段覆盖)
80
+ * 两者皆无则用本默认底座。
81
+ */
82
+ const DEFAULT_VOICE = `## Voice
83
+ - Act as a skilled engineering partner: clear, concise, practical. Avoid generic chatbot behavior.
84
+ - Give technical recommendations with brief trade-off reasoning when choices exist.
85
+ - Focus on useful information. Avoid unnecessary greetings, apologies, repetition, or filler.
86
+ - Match the user's style and language while staying task-focused.
87
+ - For long operations, briefly state the plan and expected result. Avoid step-by-step narration.
88
+ - State assumptions and ask when uncertain. Do not guess.`;
89
+ /** 解析用户自定义声音:persona.md 文件优先(项目级 > 全局),其次 env MOCODE_PERSONA。无则返回 ''。 */
90
+ function readPersonaFile() {
91
+ const candidates = [
92
+ path.join(process.cwd(), '.mocode', 'persona.md'),
93
+ path.join(os.homedir(), '.mocode', 'persona.md'),
94
+ ];
95
+ for (const p of candidates) {
96
+ try {
97
+ const txt = fs.readFileSync(p, 'utf8').trim();
98
+ if (txt)
99
+ return txt;
100
+ }
101
+ catch {
102
+ // 不存在/不可读:跳过
103
+ }
104
+ }
105
+ return process.env.MOCODE_PERSONA?.trim() ?? '';
106
+ }
107
+ /** 解析最终注入的 Voice 段:用户自定义优先,否则用默认底座。 */
108
+ function buildVoiceSection() {
109
+ return readPersonaFile() || DEFAULT_VOICE;
110
+ }
76
111
  /**
77
112
  * 基础系统提示的"记忆段落":开 isMemoryEnabled() 时才拼。
78
113
  * 默认关(新用户零侵入):这段 + 工具表里的 5 个 memory_* + 系统提示尾部的 Memory Index
@@ -89,11 +124,8 @@ const PLATFORM_NOTE = (() => {
89
124
  * 这样比纯目录列表更显眼,降低 agent 在长上下文里扫过去就忘了的概率。
90
125
  */
91
126
  export function buildNotepadSection(sessionId = getCurrentSessionId()) {
92
- if (!sessionId)
93
- return '';
94
- const root = getSandboxRoot() ?? process.cwd();
95
- const p = path.join(root, '.mocode', 'sessions', sessionId, 'notes.md');
96
- if (!fs.existsSync(p))
127
+ const p = getNotesFilePath(sessionId);
128
+ if (!p || !fs.existsSync(p))
97
129
  return '';
98
130
  try {
99
131
  const content = fs.readFileSync(p, 'utf8').trim();
@@ -133,6 +165,55 @@ export function buildNotepadSection(sessionId = getCurrentSessionId()) {
133
165
  return '';
134
166
  }
135
167
  }
168
+ /**
169
+ * 抽取 notes.md 中**唯一活跃**的 `## Plan:` 段原文(含标题行到下一个 `## ` 之前)。
170
+ * 用于 compact 后把计划重注入系统提示,避免 agent 因上下文压缩丢失执行计划。
171
+ * 已结算(`## Done:`)或无 plan 时返回 null。
172
+ */
173
+ export function extractActivePlanSection(sessionId = getCurrentSessionId()) {
174
+ const p = getNotesFilePath(sessionId);
175
+ if (!p || !fs.existsSync(p))
176
+ return null;
177
+ try {
178
+ const normalized = fs.readFileSync(p, 'utf8').replace(/\r\n?/g, '\n');
179
+ const lines = normalized.split('\n');
180
+ const start = lines.findIndex((l) => /^## Plan:\s*.+$/.test(l));
181
+ if (start < 0)
182
+ return null;
183
+ const endOffset = lines.slice(start + 1).findIndex((l) => /^##\s/.test(l));
184
+ const end = endOffset < 0 ? lines.length : start + 1 + endOffset;
185
+ return lines.slice(start, end).join('\n').trimEnd();
186
+ }
187
+ catch {
188
+ return null;
189
+ }
190
+ }
191
+ /** compact 重注入用的幂等标记:history[0] 中夹住活跃 plan 块,重复注入只替换不累积。 */
192
+ const ACTIVE_PLAN_MARKER = '\n\n<!-- mocode:active-plan -->\n';
193
+ /**
194
+ * 把活跃 `## Plan:` 段重注入系统提示(history[0])。compact 后调用:
195
+ * 若 notes.md 有活跃 plan,则覆盖旧标记块写入最新内容;若无,则清掉残留标记块。
196
+ * 直接改 history[0].content(compact 不破坏 index 0),幂等,返回是否改动。
197
+ */
198
+ export function reinjectActivePlanIntoSystem(history) {
199
+ const sys = history[0];
200
+ if (!sys || sys.role !== 'system' || typeof sys.content !== 'string')
201
+ return false;
202
+ let content = sys.content;
203
+ const markerIdx = content.indexOf(ACTIVE_PLAN_MARKER);
204
+ if (markerIdx >= 0) {
205
+ content = content.slice(0, markerIdx).replace(/\s+$/, '');
206
+ }
207
+ const plan = extractActivePlanSection();
208
+ if (!plan) {
209
+ if (markerIdx < 0)
210
+ return false;
211
+ sys.content = content;
212
+ return true;
213
+ }
214
+ sys.content = `${content}${ACTIVE_PLAN_MARKER}${plan}\n`;
215
+ return true;
216
+ }
136
217
  const SYSTEM_PROMPT_MEMORY_SECTION = `
137
218
  ## Memory (cross-session facts)
138
219
  - The prompt may contain a title/summary index; retrieve details with memory_search or inspect all with memory_list.
@@ -179,28 +260,33 @@ export function buildBasePrompt(sessionId = getCurrentSessionId()) {
179
260
  const memorySection = buildMemoryPromptSection();
180
261
  const notepadSection = buildNotepadSection(sessionId);
181
262
  // 静态主体:稳定段落集中在前,让支持 prompt caching 的后端能命中前缀缓存(#12)。
182
- // 约束:staticBody 的前缀段(尤其 ## Core behavior 第一行)必须是纯静态文本,
263
+ // 约束:staticBody 的前缀段(尤其 ## Identity 第一行)必须是纯静态文本,
183
264
  // 不得嵌入会话级可变函数调用(如 t()/config.model)。否则 /language、/model
184
265
  // 切换会让最敏感的前缀变化,破坏自动前缀缓存命中。可变值统一放到
185
- // ## Termination & Reporting 段末尾(仍在切片边界之前,子 agent 仍能拿到)。
186
- const staticBody = `## Core behavior
187
- You are mocode, a terminal coding agent. Complete programming tasks through a "think → call tool → observe result → think again" loop until solved.
266
+ // ## Reporting 段末尾(仍在切片边界之前,子 agent 仍能拿到)。
267
+ const staticBody = `## Identity
268
+ You are mocode, a terminal coding agent.
269
+
270
+ ## Core behavior
271
+ Complete programming tasks through an "analyze → call tool → observe result → decide next step" loop until solved.
188
272
 
189
273
  ## Modes
190
274
  - AUTO is the default: investigate and complete the task with the tools currently exposed.
191
275
  - PLAN is read-only research and design; do not make changes until the user approves and switches back to AUTO.
192
276
 
193
- ${PLATFORM_NOTE}
194
-
195
- ${buildWorkDisciplineSection(inferModelFamily(config.model))}
196
-
197
277
  ## Workflow
198
- - Use existing conversation and tool evidence before gathering more. Inspect only what supports the next decision; do not guess.
199
- - Keep changes focused. Decide for yourself whether a check is worth running; prefer the smallest relevant check and avoid broad test/build suites unless the task or risk justifies them.
278
+ - Understand: use existing conversation and tool evidence before gathering more; inspect only what supports the next decision, do not guess.
279
+ - Plan: for tasks with 3+ steps or context-loss risk, record the plan with the \`plan_update\` tool (see Session state); keep each step self-contained.
280
+ - Implement: make the smallest coherent change; edit against a fresh read (see Tool policy); avoid unrelated refactors.
281
+ - Verify: decide whether validation is useful by risk and scope; run the smallest relevant check, not broad test/build suites by default.
282
+ - Report: stop when done and give honest conclusions with path:line references (see Reporting).
200
283
  - Use web search only when freshness materially affects the answer.
201
284
  ${buildCodegraphSection()}
202
285
 
203
- ## Tool use
286
+ ## Engineering principles
287
+ ${buildWorkDisciplineSection(inferModelFamily(config.model))}
288
+
289
+ ## Tool policy
204
290
  - During tool-calling turns, stay silent unless something important enough must reach the user — otherwise just call the tool and let it run.
205
291
  - Go directly to a known path or symbol; use discovery tools only when the location is unknown.
206
292
  - Edit against a FRESH read: before any edit_file/write_file, call read_file on the exact path and copy both its latest hash and the exact target text. Never reconstruct old_string from a grep/summary/diff — those lose whitespace and indentation and cause edit failures.
@@ -211,47 +297,52 @@ ${buildCodegraphSection()}
211
297
  - For generated content over roughly 200 lines or 5K tokens, use small staged writes rather than one oversized tool argument.
212
298
  - Use \`ask_human\` only for a genuinely user-owned decision; otherwise choose the safest reversible option and proceed.
213
299
 
214
- ## Safety & Boundaries
300
+ ## Environment
301
+ ${PLATFORM_NOTE}
302
+
303
+ ## Safety
215
304
  - Get confirmation before irreversible or outward-facing actions such as deletion, push, production changes, or external requests, unless explicitly authorized.
216
305
  - Stay within the authorized workspace and disclose anything skipped or unverifiable.
217
306
 
218
- ## Termination & Reporting
307
+ ${buildVoiceSection()}
308
+
309
+ ## Reporting
219
310
  - Stop immediately when no more tools are needed; give conclusions directly.
220
311
  - **Do not stop prematurely during exploration**: if you started investigating but haven't gathered enough information to answer the user's question, keep calling tools. Only stop when you have sufficient evidence or hit a dead end.
221
312
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
222
313
  - Report honestly: say success when successful, say where you're stuck when failing, and mention anything skipped. Reference code in "path:line" format (e.g., src/index.ts:42). Keep it concise.
223
314
  ${t('assistant.languageInstruction')}`;
224
- // 动态段(置于末尾):memory 索引 + notepad 目录与使用说明。
225
- // 按需注入(#13):仅当有内容才拼对应标题/说明,避免空标题与无文件时的模板噪声。
226
- // - "## Project context" 仅当 memorySection/notepadSection 非空;
227
- // - notepad 使用说明仅当 notes.md 文件存在(notepadSection 非空)—
228
- // 无文件时连 marker 都不拼,既省 token 也让前缀缓存更稳。core 切片
229
- // 回退到 MARKER_DYNAMIC_SECTION 或整段(见 buildMocodeCorePrompt)。
315
+ // 动态段(置于末尾):memory 索引 + notepad 索引 + notepad 使用说明。
316
+ // 按需注入(#13):有内容的索引才拼对应标题,避免空标题噪声。
317
+ // - "## Project context" 仅当 memorySection/notepadSection 非空(notepad 索引依赖 notes.md 存在);
318
+ // - "## Session state" 使用说明**无条件**注入(放在动态尾段首位):否则会陷入"说明依赖 notes.md 存在 → 模型不知要建 → 文件永不存在"的鸡生蛋循环,功能对模型不可见。动态段在静态前缀之后,不影响 prompt 缓存。
230
319
  const dynamicParts = [];
320
+ // 会话级私有尾段(子 agent 切片会丢弃):Session state 说明无条件注入在前,Project context 按需在后。
321
+ dynamicParts.push(`## Session state (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
322
+ 'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
323
+ 'Record and update the execution plan with the `plan_update` tool (preferred over editing checkboxes by hand); it keeps at most one active plan as a `## Plan:` section:\n' +
324
+ '```\n' +
325
+ '## Plan: <title>\n' +
326
+ 'Goal: <outcome>\n' +
327
+ '### Steps\n' +
328
+ '- [ ] 1. <self-contained step: target file/symbol, the change, and how to verify>\n' +
329
+ '### Progress\n' +
330
+ '- <completed/total>\n' +
331
+ '```\n' +
332
+ 'Keep at most one step in_progress, and mark a step completed as soon as its work is done — do not batch updates to the end of the turn. ' +
333
+ 'Write each step so a teammate who lost the conversation could pick it up cold: name the file or symbol, the exact change, and the verification, so the plan survives context compaction. ' +
334
+ 'plan_update creates notes.md for you when the task warrants it; read_file the full notes.md whenever you need to recover context after compaction. ' +
335
+ 'When every step is completed, plan_update settles the plan to `## Done:` automatically. Keep other notes concise and session-specific; use memory for stable cross-session facts.');
231
336
  const ctxContent = `${memorySection}${notepadSection}`.trimEnd();
232
337
  if (ctxContent) {
233
- dynamicParts.push(`## Project context (dynamic reference)\n${ctxContent}`);
234
- }
235
- if (notepadSection) {
236
- dynamicParts.push(`## Session Notepad (\`.mocode/sessions/${sessionId ?? '<id>'}/notes.md\`)\n` +
237
- 'Use this compact, persistent working surface for tasks with at least three steps or context-loss risk; skip it for simple work.\n\n' +
238
- 'Keep at most one active plan:\n' +
239
- '```\n' +
240
- '## Plan: <title>\n' +
241
- 'Goal: <outcome>\n' +
242
- '### Steps\n' +
243
- '- [ ] 1. <verifiable step>\n' +
244
- '### Progress\n' +
245
- '- <completed phase and evidence>\n' +
246
- '```\n' +
247
- 'Update checkboxes and Progress after each completed phase. Before the final reply, reconcile the plan with actual work, then rename it to `## Done:` or remove it. Keep other notes concise and session-specific; use memory for stable cross-session facts.');
338
+ dynamicParts.push(`## Project context\n${ctxContent}`);
248
339
  }
249
340
  return `${staticBody}\n\n${dynamicParts.join('\n\n')}`;
250
341
  }
251
342
  /** 静态主体结束 + 会话私有段起点标记,供 buildMocodeCorePrompt 稳健切片(#17)。 */
252
- const MARKER_STATIC_END = '## Termination & Reporting';
253
- const MARKER_DYNAMIC_SECTION = '## Project context (dynamic reference)';
254
- const MARKER_DROPPABLE_SECTION = '## Session Notepad (';
343
+ const MARKER_STATIC_END = '## Reporting';
344
+ const MARKER_DYNAMIC_SECTION = '## Project context';
345
+ const MARKER_DROPPABLE_SECTION = '## Session state';
255
346
  /**
256
347
  * Stable, production-grade behavior shared by main and sub agents.
257
348
  * It intentionally excludes the trailing session-specific payload (notepad
@@ -312,6 +403,7 @@ export const config = {
312
403
  maxSteps: Number(process.env.MAX_STEPS) || 1000,
313
404
  subAgentEnabled: process.env.MOCODE_SUBAGENT_ENABLED === 'true',
314
405
  subAgentMaxSteps: Number(process.env.SUB_AGENT_MAX_STEPS) || Number(process.env.MAX_STEPS) || 1000,
406
+ frontendToolsEnabled: process.env.MOCODE_FRONTEND_TOOLS_ENABLED === 'true',
315
407
  sessionDir: path.join(process.cwd(), '.mocode', 'sessions'),
316
408
  searchApiKey: process.env.ANYSEARCH_API_KEY,
317
409
  sandboxRoot: process.env.SANDBOX_ROOT || undefined,
@@ -360,6 +452,15 @@ export function updateSubAgentConfig(enabled) {
360
452
  config.subAgentEnabled = enabled;
361
453
  process.env.MOCODE_SUBAGENT_ENABLED = enabled ? 'true' : 'false';
362
454
  }
455
+ /** 前端工具簇总开关;默认 false,关闭时 browser/dev_server/screenshot/view_image 不进入模型工具表。 */
456
+ export function isFrontendToolsEnabled() {
457
+ return config.frontendToolsEnabled;
458
+ }
459
+ /** 运行时切换前端工具簇;工具 schema 刷新与持久化由 REPL 调用方完成。 */
460
+ export function updateFrontendToolsConfig(enabled) {
461
+ config.frontendToolsEnabled = enabled;
462
+ process.env.MOCODE_FRONTEND_TOOLS_ENABLED = enabled ? 'true' : 'false';
463
+ }
363
464
  /**
364
465
  * 记忆子系统总开关:单一来源。/memory_switch、/memory_status、buildSystemPrompt、
365
466
  * tools/builtins/index.ts、tools/constants.ts 的 plan-mode 列表都从这里查。
@@ -23,6 +23,10 @@ const zhCN = {
23
23
  'commands.subagentOn': '开启子 Agent',
24
24
  'commands.subagentOff': '关闭子 Agent',
25
25
  'commands.subagentStatus': '查看子 Agent 状态',
26
+ 'commands.fe': '前端工具簇开关 browser/dev_server/screenshot/view_image(默认关闭)',
27
+ 'commands.feOn': '开启前端工具簇',
28
+ 'commands.feOff': '关闭前端工具簇',
29
+ 'commands.feStatus': '查看前端工具簇状态',
26
30
  'commands.theme': '切换颜色主题(↑↓·Enter)',
27
31
  'commands.model': '模型配置与预设管理',
28
32
  'commands.modelConfigure': '配置新模型(向导)',
@@ -211,6 +215,12 @@ const zhCN = {
211
215
  'subagent.changedOn': '已开启子 Agent;sub-agent 将从下一次模型请求起可用。',
212
216
  'subagent.changedOff': '已关闭子 Agent;sub-agent 已从模型工具表移除。',
213
217
  'subagent.usage': '用法:/subagent on|off|status',
218
+ 'fe.status': '前端工具簇:{state}',
219
+ 'fe.stateOn': '开启',
220
+ 'fe.stateOff': '关闭',
221
+ 'fe.changedOn': '已开启前端工具簇;browser / dev_server / screenshot / view_image 将从下一次模型请求起可用。',
222
+ 'fe.changedOff': '已关闭前端工具簇;browser / dev_server / screenshot / view_image 已从模型工具表移除。',
223
+ 'fe.usage': '用法:/fe on|off|status',
214
224
  'plan.ready': '计划已就绪',
215
225
  'plan.approvalDetail': '切换到 auto 模式按上述计划执行?(plan 模式只读探查,执行需切 auto)',
216
226
  'plan.execute': '切 auto 执行',
@@ -265,6 +275,10 @@ const en = {
265
275
  'commands.subagentOn': 'Enable sub-agents',
266
276
  'commands.subagentOff': 'Disable sub-agents',
267
277
  'commands.subagentStatus': 'Show sub-agent status',
278
+ 'commands.fe': 'Frontend tools: browser/dev_server/screenshot/view_image (disabled by default)',
279
+ 'commands.feOn': 'Enable frontend tools',
280
+ 'commands.feOff': 'Disable frontend tools',
281
+ 'commands.feStatus': 'Show frontend tools status',
268
282
  'commands.theme': 'Switch color theme (↑↓·Enter)',
269
283
  'commands.model': 'Model configuration and presets',
270
284
  'commands.modelConfigure': 'Configure a new model (wizard)',
@@ -453,6 +467,12 @@ const en = {
453
467
  'subagent.changedOn': 'Sub-agents enabled; sub-agent will be available from the next model request.',
454
468
  'subagent.changedOff': 'Sub-agents disabled; sub-agent has been removed from the model tool list.',
455
469
  'subagent.usage': 'Usage: /subagent on|off|status',
470
+ 'fe.status': 'Frontend tools: {state}',
471
+ 'fe.stateOn': 'enabled',
472
+ 'fe.stateOff': 'disabled',
473
+ 'fe.changedOn': 'Frontend tools enabled; browser / dev_server / screenshot / view_image will be available from the next model request.',
474
+ 'fe.changedOff': 'Frontend tools disabled; browser / dev_server / screenshot / view_image have been removed from the model tool list.',
475
+ 'fe.usage': 'Usage: /fe on|off|status',
456
476
  'plan.ready': 'Plan ready',
457
477
  'plan.approvalDetail': 'Switch to auto mode and execute the plan above? (Plan mode is read-only; execution requires auto mode.)',
458
478
  'plan.execute': 'Switch to auto and execute',
package/dist/llm/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import OpenAI from 'openai';
2
- import { config, isSubAgentEnabled } from '../config/index.js';
2
+ import { config, isSubAgentEnabled, isFrontendToolsEnabled } from '../config/index.js';
3
3
  import { tools } from '../tools/registry.js';
4
- import { getPlanDisabledTools } from '../tools/constants.js';
4
+ import { getPlanDisabledTools, FRONTEND_TOOLS } from '../tools/constants.js';
5
5
  import { ThinkTagFilter } from './think-filter.js';
6
6
  // 强制关闭第三方调试日志泄漏:openai SDK 在 process.env.DEBUG === 'true' 时用裸
7
7
  // console.log 把请求/响应直写 stdout,会污染 TUI 输入框(并泄露 headers/URL)。
@@ -159,10 +159,14 @@ export function __setChatCreateImpl(impl) {
159
159
  export const chatTools = [];
160
160
  export const planChatTools = [];
161
161
  export function refreshChatTools() {
162
- // sub-agent 常驻内部 registry,运行时开关只控制模型可见 schema,因而 on/off 可即时生效。
163
- const visibleTools = isSubAgentEnabled()
164
- ? tools
165
- : tools.filter((tool) => tool.name !== 'sub-agent');
162
+ // sub-agent 与前端工具簇常驻内部 registry,运行时开关只控制模型可见 schema,因而 on/off 可即时生效。
163
+ const visibleTools = tools.filter((tool) => {
164
+ if (tool.name === 'sub-agent')
165
+ return isSubAgentEnabled();
166
+ if (FRONTEND_TOOLS.has(tool.name))
167
+ return isFrontendToolsEnabled();
168
+ return true;
169
+ });
166
170
  const next = visibleTools.map((t) => ({
167
171
  type: 'function',
168
172
  function: {