mocode-ai 0.4.10 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ MoCode isn't a chat box with a coat of paint — it's an agent that actually get
18
18
  - **Plan / Auto dual mode** — In `plan` mode the agent is read-only (reads code, queries indexes, searches — never writes to disk, runs commands, or spawns sub-agents) and produces a plan; `auto` mode unlocks the full toolset. The agent can switch between the two on its own — scope out an unfamiliar codebase first, then start making changes.
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
- - **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. See [docs/USAGE_SNAPSHOT_SKILL.md](./docs/USAGE_SNAPSHOT_SKILL.md) for details.
21
+ - **Project context (Snapshot + Skill)** — Two complementary systems help the agent understand your project: **Project Snapshot** automatically scans files and generates LLM-enhanced summaries (project description, tech stack, commands, module responsibilities, directory tree); **Project Skill** is a manually maintained knowledge base capturing design decisions, architectural insights, pitfalls, and conventions. Snapshot provides *what/where* (facts), Skill provides *why/how* (insights) — no duplication, ~46% token savings.
22
22
  - **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.
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.
@@ -193,6 +193,10 @@ export async function runAgentCore(opts) {
193
193
  let mode = 'idle';
194
194
  let gotText = false;
195
195
  let lastChar = '';
196
+ // 早退重探:本 turn 已执行过工具但模型突然返回无工具调用 + 极短/空文本 → 推一条提示让模型继续,
197
+ // 而非直接退出。每 turn 最多触发 1 次,防死循环。弱模型在探索中途偶尔"说完"即此兜底。
198
+ let nudgeCount = 0;
199
+ let hadToolsThisTurn = false;
196
200
  const onText = (s) => {
197
201
  hooks.onText?.(s); // 主 agent:走 markdown 渲染写内容区
198
202
  mode = 'text';
@@ -262,6 +266,7 @@ export async function runAgentCore(opts) {
262
266
  // lastUsage 已更新:触发状态行 context 用量条重算+重画,运行中不再冻结在轮首。
263
267
  onContextUpdate?.();
264
268
  if (result.toolCalls.length > 0) {
269
+ hadToolsThisTurn = true;
265
270
  // 流式正文末尾补换行(若 onToolCall 已补则 lastChar='\n',此处 no-op);防 ● 行黏在正文行尾
266
271
  if (mode !== 'idle' && lastChar !== '\n')
267
272
  hooks.onTextEnd?.();
@@ -438,6 +443,19 @@ export async function runAgentCore(opts) {
438
443
  }
439
444
  if (mode !== 'idle' && lastChar !== '\n')
440
445
  hooks.onTextEnd?.(); // 流式末尾补换行
446
+ // 早退保护:本 turn 已执行过工具调用,但模型突然返回无工具 + 极短/空文本 → 很可能是在探索中途
447
+ // 提前"说完了"。此时推一条 user 提示消息让模型继续探索,而非直接退出。每 turn 最多 1 次,防死循环。
448
+ // 判定标准:无 gotText(完全没输出)或正文极短(< 80 字符,通常是一句"我需要更多信息"级别的截断)。
449
+ const textLen = result.content?.trim().length ?? 0;
450
+ if (hadToolsThisTurn && nudgeCount < 1 && (!gotText || textLen < 80)) {
451
+ nudgeCount++;
452
+ history.push({ role: 'assistant', content: result.content });
453
+ history.push({
454
+ role: 'user',
455
+ content: 'You stopped before completing the task. Please continue investigating — call more tools if needed, or provide a complete answer based on what you have gathered so far.',
456
+ });
457
+ continue; // 带着提示再调一次 LLM
458
+ }
441
459
  // 没有工具调用:流式正文即最终回复(已实时打印)
442
460
  if (!gotText)
443
461
  hooks.onNoReply?.();
@@ -226,6 +226,7 @@ ${buildSnapshotSection()}${config.projectSkillEnabled ? buildProjectSkillSection
226
226
 
227
227
  ## Termination & Reporting
228
228
  - Stop immediately when no more tools are needed; give conclusions directly.
229
+ - **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.
229
230
  - **No flattery / no preamble in conclusions**: skip "Sure", "好的", "我已经完成了" and similar no-information prefixes — jump straight to substance.
230
231
  - 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.`;
231
232
  }
@@ -9,7 +9,7 @@ import { getAgentMode, setAgentMode, onModeChange } from '../agent/mode.js';
9
9
  import { togglePet, killPetProcess, listSkins, setSkin, sendState } from '../pet/bridge.js';
10
10
  import { setSandboxRoot } from '../sandbox/root.js';
11
11
  import { ui, setTheme, getTheme, listThemes, themeExists } from '../ui/theme.js';
12
- import { bannerString, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
12
+ import { bannerLines, displayWidth, padEndDisplay, summarizeToolCall, summarizeToolResult } from '../ui/render.js';
13
13
  import * as layout from '../ui/layout.js';
14
14
  import * as mouse from '../ui/mouse.js';
15
15
  import * as batch from '../ui/batch.js';
@@ -67,6 +67,12 @@ const THEME_DESCRIPTIONS = {
67
67
  solarized: 'Solarized 强调色',
68
68
  gruvbox: 'Gruvbox 暖色',
69
69
  nord: 'Nord 冷色',
70
+ orange: '南瓜橙(暖)',
71
+ rose: '玫红(暖紫)',
72
+ emerald: '翡翠绿(冷)',
73
+ amber: '琥珀金黄(暖)',
74
+ lavender: '薰衣草淡紫(冷)',
75
+ sunset: '日落珊瑚红(暖)',
70
76
  };
71
77
  /** /model 预设后端:选一个预填 baseURL,仍可逐项改。base_url 取自 README 常见表。 */
72
78
  const MODEL_PRESETS = [
@@ -139,7 +145,7 @@ function renderContextBar(history) {
139
145
  const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
140
146
  const src = contextState.lastUsage ? '实测' : '估算';
141
147
  const k = (n) => `${Math.round(n / 1000)}k`;
142
- const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
148
+ const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
143
149
  return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${Math.round(pct * 100)}% ${k(est)}/${k(win)} tokens · ${history.length} 条消息 (${src})${ui.reset}`;
144
150
  }
145
151
  /** 状态行用量条(精简版,进底栏):[bar] pct% k/k。
@@ -154,7 +160,7 @@ function renderContextBarInline(history) {
154
160
  const filled = Math.round(pct * W);
155
161
  const bar = '█'.repeat(filled) + '░'.repeat(W - filled);
156
162
  const k = (n) => `${Math.round(n / 1000)}k`;
157
- const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.cyan;
163
+ const pctCol = pct >= config.compactThreshold ? ui.yellow : ui.accent;
158
164
  return `${ui.gray}[${pctCol}${bar}${ui.reset}] ${pctCol}${Math.round(pct * 100)}%${ui.reset} ${ui.dim}${k(est)}/${k(win)}${ui.reset}`;
159
165
  }
160
166
  /** 状态行基线:模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token。repl 在轮次边界、切模式、plan 变更时调。 */
@@ -595,6 +601,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
595
601
  baseURL: config.baseURL,
596
602
  cwd: process.cwd(),
597
603
  tools: toolsLine,
604
+ memoryEnabled: isMemoryEnabled(),
598
605
  });
599
606
  // 开场:按 config.theme 切主题(横幅 / 状态行 / 后续渲染皆用新色),再进 alt screen + 状态基线 + 清内容区。
600
607
  // --resume 有历史则渲染对话(仿 Claude Code),否则横幅。
@@ -607,7 +614,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
607
614
  renderHistory(history);
608
615
  }
609
616
  else {
610
- layout.contentWrite(bannerString(banner()));
617
+ layout.writeBanner(bannerLines(banner()));
611
618
  }
612
619
  if (!isModelConfigured()) {
613
620
  // 未配置 baseURL/apiKey:醒目提示引导 /model(不退出,REPL 仍可用;发消息会失败但不崩)。
@@ -633,7 +640,6 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
633
640
  }
634
641
  }
635
642
  }
636
- layout.contentWrite(`${ui.dim} /plan · /auto · Shift+Tab 切换模式(plan:只读探查 + 产出计划,审批后切 auto 执行)${ui.reset}\n`);
637
643
  if (updateNotice) {
638
644
  // 自更新提示:放 /plan · /auto 行下方,黄色(警示色)突出"你正在用的版本较旧",与 dim 提示区分。
639
645
  // 开场静态段(进 INPUT 态前),一行,纯文本不与流式 / 输入争用。
@@ -814,7 +820,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
814
820
  /不支持(视觉|图片|图像|多模态)/.test(msg) ||
815
821
  (/图片|图像|视觉|多模态/.test(msg) && /不支持|invalid|reject|fail/i.test(lower));
816
822
  if (looksLikeImageError) {
817
- layout.contentWrite(`${ui.red}[错误]${ui.reset} 当前模型 ${ui.cyan}${config.model}${ui.reset} 不支持视觉输入。${ui.dim}原始:${msg}${ui.reset}\n`);
823
+ layout.contentWrite(`${ui.red}[错误]${ui.reset} 当前模型 ${ui.accent}${config.model}${ui.reset} 不支持视觉输入。${ui.dim}原始:${msg}${ui.reset}\n`);
818
824
  layout.contentWrite(`${ui.dim}提示:运行 /model 切换到支持视觉的模型(如 gpt-4o / claude-3.5-sonnet / gemini-1.5-pro)。${ui.reset}\n`);
819
825
  }
820
826
  else {
@@ -982,7 +988,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
982
988
  lastTurnUsage = undefined; // 清空旧轮的 token 累计
983
989
  pendingAttachments = []; // 一并清空待发图片
984
990
  layout.clearContent();
985
- layout.contentWrite(bannerString(banner()));
991
+ layout.writeBanner(bannerLines(banner()));
986
992
  layout.contentWrite(`${ui.dim}(历史已清空,保留系统提示)${ui.reset}\n`);
987
993
  continue;
988
994
  }
@@ -1054,7 +1060,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1054
1060
  .sort((a, b) => (b.updatedAt || '').localeCompare(a.updatedAt || ''))
1055
1061
  .slice(0, 10);
1056
1062
  for (const e of recent) {
1057
- layout.contentWrite(` ${ui.cyan}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
1063
+ layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
1058
1064
  }
1059
1065
  if (active.length === 0)
1060
1066
  layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 MOCODE.md)${ui.reset}\n`);
@@ -1080,7 +1086,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1080
1086
  else {
1081
1087
  layout.contentWrite(`${ui.dim}已发现 ${skills.length} 个 skill:${ui.reset}\n`);
1082
1088
  for (const s of skills) {
1083
- layout.contentWrite(` ${ui.cyan}${s.name}${ui.reset} ${ui.dim}${s.description}${ui.reset}\n`);
1089
+ layout.contentWrite(` ${ui.accent}${s.name}${ui.reset} ${ui.dim}${s.description}${ui.reset}\n`);
1084
1090
  }
1085
1091
  layout.contentWrite(`${ui.dim}(用 use_skill 工具加载某 skill 的完整指令)${ui.reset}\n`);
1086
1092
  }
@@ -1259,7 +1265,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1259
1265
  else if (arg === 'list' || !themeExists(arg)) {
1260
1266
  layout.contentWrite(`${ui.dim}可用主题:${ui.reset}\n`);
1261
1267
  for (const t of listThemes()) {
1262
- layout.contentWrite(` ${ui.cyan}${t}${ui.reset} ${ui.dim}${THEME_DESCRIPTIONS[t] ?? ''}${ui.reset}\n`);
1268
+ layout.contentWrite(` ${ui.accent}${t}${ui.reset} ${ui.dim}${THEME_DESCRIPTIONS[t] ?? ''}${ui.reset}\n`);
1263
1269
  }
1264
1270
  layout.contentWrite(`${ui.dim}(当前:${getTheme()};用 /theme <名称> 切换)${ui.reset}\n`);
1265
1271
  continue;
@@ -1278,7 +1284,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1278
1284
  renderHistory(history);
1279
1285
  }
1280
1286
  else {
1281
- layout.contentWrite(bannerString(banner()));
1287
+ layout.writeBanner(bannerLines(banner()));
1282
1288
  }
1283
1289
  layout.contentWrite(`${ui.dim}(已切换主题 ${name})${ui.reset}\n`);
1284
1290
  updateConfigKey('MOCODE_THEME', name);
@@ -1314,7 +1320,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1314
1320
  renderHistory(history);
1315
1321
  }
1316
1322
  else {
1317
- layout.contentWrite(bannerString(banner()));
1323
+ layout.writeBanner(bannerLines(banner()));
1318
1324
  }
1319
1325
  layout.contentWrite(`${ui.dim}(已切换到预设 “${target.name}” → ${target.model} @ ${target.baseURL})${ui.reset}\n`);
1320
1326
  if (config.llmKeysFromShell.length > 0) {
@@ -1393,7 +1399,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1393
1399
  layout.contentWrite(`${ui.dim}已配置 ${ps.length} 个预设:${ui.reset}\n`);
1394
1400
  for (const p of ps) {
1395
1401
  const star = p.baseURL === config.baseURL && p.apiKey === config.apiKey && p.model === config.model ? ' ★' : '';
1396
- layout.contentWrite(` ${ui.cyan}${p.name}${ui.reset}${star} ${ui.dim}${p.model} @ ${p.baseURL}${ui.reset}\n`);
1402
+ layout.contentWrite(` ${ui.accent}${p.name}${ui.reset}${star} ${ui.dim}${p.model} @ ${p.baseURL}${ui.reset}\n`);
1397
1403
  }
1398
1404
  layout.contentWrite(`${ui.dim}(★ = 与当前一致;切换用 /model switch)${ui.reset}\n`);
1399
1405
  continue;
@@ -1401,10 +1407,10 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1401
1407
  // /model show:显示当前四项配置(apiKey 脱敏)。
1402
1408
  if (arg === 'show') {
1403
1409
  layout.contentWrite(`${ui.dim}当前模型配置:${ui.reset}\n`);
1404
- layout.contentWrite(` ${ui.cyan}baseURL${ui.reset} ${config.baseURL}\n`);
1405
- layout.contentWrite(` ${ui.cyan}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
1406
- layout.contentWrite(` ${ui.cyan}model ${ui.reset} ${config.model}\n`);
1407
- layout.contentWrite(` ${ui.cyan}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
1410
+ layout.contentWrite(` ${ui.accent}baseURL${ui.reset} ${config.baseURL}\n`);
1411
+ layout.contentWrite(` ${ui.accent}apiKey ${ui.reset} ${maskKey(config.apiKey)}\n`);
1412
+ layout.contentWrite(` ${ui.accent}model ${ui.reset} ${config.model}\n`);
1413
+ layout.contentWrite(` ${ui.accent}窗口 ${ui.reset} ${config.contextWindowTokens} tokens\n`);
1408
1414
  layout.contentWrite(`${ui.dim}(配置文件: ${CONFIG_PATH})${ui.reset}\n`);
1409
1415
  continue;
1410
1416
  }
@@ -1627,7 +1633,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1627
1633
  renderHistory(history);
1628
1634
  }
1629
1635
  else {
1630
- layout.contentWrite(bannerString(banner()));
1636
+ layout.writeBanner(bannerLines(banner()));
1631
1637
  }
1632
1638
  layout.contentWrite(`${ui.dim}(已切换模型 → ${model} @ ${baseURL})${ui.reset}\n`);
1633
1639
  if (savedName) {
@@ -1662,9 +1668,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1662
1668
  try {
1663
1669
  if (line === '/memory_status' || line.startsWith('/memory_status ')) {
1664
1670
  const on = isMemoryEnabled();
1665
- layout.contentWrite(`${ui.cyan}记忆子系统:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1671
+ layout.contentWrite(`${ui.accent}记忆子系统:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1666
1672
  layout.contentWrite(`${ui.dim} 单一来源 isMemoryEnabled()(${config.memoryEnabled});` +
1667
- `持久化 ${ui.cyan}MEMORY_ENABLED${ui.dim};` +
1673
+ `持久化 ${ui.accent}MEMORY_ENABLED${ui.dim};` +
1668
1674
  `配置文件 ${CONFIG_PATH}${ui.reset}\n`);
1669
1675
  layout.contentWrite(`${ui.dim} 关闭时:memory_*_save/_search/_list/_update/_forget 五个工具整体不进工具表;` +
1670
1676
  `buildBasePrompt() 不含「## Memory」段;` +
@@ -1711,6 +1717,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1711
1717
  (process.env.MEMORY_ENABLED
1712
1718
  ? `${ui.dim}同 session shell 未 export,文件写入即时生效)${ui.reset}\n`
1713
1719
  : `${ui.dim}下次启动仍生效)${ui.reset}\n`));
1720
+ // 即时刷 banner(原地替换顶部 bannerH 行,不留副本):banner() 闭包实时读
1721
+ // isMemoryEnabled(),无需重启 REPL。buffer 中 bannerH 之下的对话历史位置不动。
1722
+ layout.rewriteBanner(bannerLines(banner()));
1714
1723
  }
1715
1724
  catch (e) {
1716
1725
  layout.contentWrite(`${ui.red}/memory_switch 失败:${ui.reset} ${e.message}\n`);
@@ -1732,7 +1741,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1732
1741
  // /project_skill view — 查看当前内容
1733
1742
  if (arg === 'view') {
1734
1743
  const enabled = isProjectSkillEnabled();
1735
- layout.contentWrite(`${ui.cyan}项目专属 Skill:${ui.reset} ${enabled ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1744
+ layout.contentWrite(`${ui.accent}项目专属 Skill:${ui.reset} ${enabled ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1736
1745
  if (enabled) {
1737
1746
  const { readProjectSkill } = await import('../project-skill/index.js');
1738
1747
  const content = readProjectSkill();
@@ -1777,7 +1786,7 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1777
1786
  if (writeResult.ok) {
1778
1787
  layout.contentWrite(`${ui.green}✓ 项目 Skill 已生成${ui.reset}\n`);
1779
1788
  layout.contentWrite(`${ui.dim}内容预览:${ui.reset}\n${result.content.slice(0, 500)}${result.content.length > 500 ? '...' : ''}\n`);
1780
- layout.contentWrite(`\n${ui.dim}文件: ${ui.cyan}.mocode/project-skill.md${ui.reset} (${result.content.length} 字符)\n`);
1789
+ layout.contentWrite(`\n${ui.dim}文件: ${ui.accent}.mocode/project-skill.md${ui.reset} (${result.content.length} 字符)\n`);
1781
1790
  layout.contentWrite(`${ui.dim}下次启动时会自动注入到系统提示词。Agent 也会在开发过程中持续更新。${ui.reset}\n`);
1782
1791
  layout.contentWrite(`${ui.dim}提示: 可用 ${ui.cyan}/project_skill view${ui.reset} 查看完整内容,或手动编辑文件完善。${ui.dim}${ui.reset}\n`);
1783
1792
  }
@@ -1854,9 +1863,9 @@ export async function startRepl(initialHistory, sessionId, updateNotice = null,
1854
1863
  // /snapshot status — 只读查询
1855
1864
  if (arg === 'status') {
1856
1865
  const on = isProjectSnapshotEnabled();
1857
- layout.contentWrite(`${ui.cyan}项目快照:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1866
+ layout.contentWrite(`${ui.accent}项目快照:${ui.reset} ${on ? `${ui.green}开启` : `${ui.yellow}关闭`}${ui.reset}\n`);
1858
1867
  layout.contentWrite(`${ui.dim} 单一来源 config.projectSnapshotEnabled(${on ? 'true' : 'false'});` +
1859
- `持久化 ${ui.cyan}MOCODE_PROJECT_SNAPSHOT${ui.dim};` +
1868
+ `持久化 ${ui.accent}MOCODE_PROJECT_SNAPSHOT${ui.dim};` +
1860
1869
  `配置文件 ${CONFIG_PATH}${ui.reset}\n`);
1861
1870
  layout.contentWrite(`${ui.dim} 关闭时:buildBasePrompt() 不含「## Project Snapshot」段;` +
1862
1871
  `read_file 工具直接走真实 readFile,不走 snapshot cache。${ui.reset}\n`);
@@ -323,7 +323,7 @@ export async function compactHistory(history, opts) {
323
323
  const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
324
324
  contextState.lastEstimate = estimateAfter;
325
325
  contextState.lastUsage = undefined;
326
- layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
326
+ layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}强制压缩(focus on early history)${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
327
327
  return {
328
328
  compacted: true,
329
329
  summarized: false,
@@ -409,7 +409,7 @@ export async function compactHistory(history, opts) {
409
409
  const estimateAfter = estimateMessagesTokens(history) + schemaTokens;
410
410
  contextState.lastEstimate = estimateAfter;
411
411
  contextState.lastUsage = undefined; // 压缩后旧 usage 失效,/context 改用估算
412
- layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
412
+ layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}压缩上下文${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
413
413
  // 抖动保护:压缩后仍超阈 → 提示 /clear,不死循环
414
414
  if (estimateAfter >= opts.threshold * opts.window) {
415
415
  layout.contentWrite(` ${ui.yellow}●${ui.reset} ${ui.yellow}压缩后仍超阈,可能存在超大单条;建议 /clear。${ui.reset}\n`);
@@ -427,7 +427,7 @@ export async function compactHistory(history, opts) {
427
427
  contextState.lastEstimate = estimateAfter;
428
428
  contextState.lastUsage = undefined; // 结构虽未变,但 token 数已变,旧 usage 失效
429
429
  if (microcompactDone) {
430
- layout.contentWrite(` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
430
+ layout.contentWrite(` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}微压缩旧工具结果${ui.reset} ${ui.dim}${estimateBefore} → ${estimateAfter} tokens${ui.reset}\n`);
431
431
  return {
432
432
  compacted: true,
433
433
  summarized: false,
package/dist/ui/batch.js CHANGED
@@ -74,11 +74,11 @@ export function recordResult(id, name, resultSummary, diffBlock, fullOutput) {
74
74
  /** 把 entry 列表压缩成一行摘要(Claude Code 风格)。 */
75
75
  function buildSummaryLine(entries) {
76
76
  if (entries.length === 0) {
77
- return ` ${ui.brightMagenta}●${ui.reset} ${ui.dim}No tools${ui.reset}`;
77
+ return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.dim}No tools${ui.reset}`;
78
78
  }
79
79
  if (entries.length === 1) {
80
80
  const e = entries[0];
81
- return ` ${ui.brightMagenta}●${ui.reset} ${ui.cyan}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}`;
81
+ return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}`;
82
82
  }
83
83
  // N>1:同类合并 "read_file 3, glob 1, grep 1"
84
84
  const counts = new Map();
@@ -87,7 +87,7 @@ function buildSummaryLine(entries) {
87
87
  const parts = [];
88
88
  for (const [n, c] of counts)
89
89
  parts.push(`${n} ${c}`);
90
- return ` ${ui.brightMagenta}●${ui.reset} ${ui.dim}Ran ${entries.length} tools · ${parts.join(', ')}${ui.reset}`;
90
+ return ` ${ui.bold}${ui.accent}●${ui.reset} ${ui.dim}Ran ${entries.length} tools · ${parts.join(', ')}${ui.reset}`;
91
91
  }
92
92
  // ── 展开/折叠 ──
93
93
  /** 把 batch 的详情行展开成自洽行数组(供 layout.contentInsertAfter 走 mid-buffer 插入)。
@@ -96,7 +96,7 @@ function buildSummaryLine(entries) {
96
96
  function buildExpandedLines(entries, indent = ' ') {
97
97
  const lines = [];
98
98
  for (const e of entries) {
99
- lines.push(`${indent}${ui.brightMagenta}●${ui.reset} ${ui.cyan}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}\x1B[0m`);
99
+ lines.push(`${indent}${ui.bold}${ui.accent}●${ui.reset} ${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}\x1B[0m`);
100
100
  if (e.diffBlock) {
101
101
  // diff 块多行文本(由 renderFileChange 渲染);按 \n 拆成物理行,
102
102
  // 每行单独入 rows[]。行末 reset 由本函数统一追加(若原行已带 reset,终端合并即可)。
@@ -252,3 +252,45 @@ export function deleteFrom(startIdx, n) {
252
252
  segMark.rowIdx -= actual;
253
253
  }
254
254
  }
255
+ /**
256
+ * 在绝对行索引 startIdx(0-based,已 commit)起**等长替换**为新行(lines.length 必须等于
257
+ * 原区间行数;调用方负责等长,以维持 banner 等「顶部固定行」语义不变)。
258
+ *
259
+ * 用途:layout.writeBanner / rewriteBanner 把 buffer [0, bannerH) 替换为新的自洽行,
260
+ * repaintViewport 自然从 rows[] 头部读出新版 banner,无需 layout 介入。
261
+ *
262
+ * 不动 hasCurrent(行已 commit);若 segMark 在替换区间内,segMark.rowIdx 减至 startIdx
263
+ * (段起点被覆盖)。segMark 在区间前方不动(不在此类用法出现)。
264
+ *
265
+ * 长度不一致直接报错并 no-op(避免静默错位):允许调用方传不同长度时改用 insertAfter + deleteFrom。
266
+ */
267
+ export function replaceHead(startIdx, lines) {
268
+ if (!Number.isInteger(startIdx) || startIdx < 0) {
269
+ throw new Error(`replaceHead: startIdx 必须 ≥ 0 整数,实得 ${startIdx}`);
270
+ }
271
+ if (lines.length === 0)
272
+ return; // 空替换 = no-op(0 行删 0 行)
273
+ if (hasCurrent) {
274
+ rows.push(rowStartSgr + curRaw + '\x1B[0m');
275
+ curRaw = '';
276
+ rowStartSgr = curSgr;
277
+ hasCurrent = false;
278
+ }
279
+ const committed = rows.length;
280
+ if (startIdx >= committed) {
281
+ throw new Error(`replaceHead: startIdx=${startIdx} 超出已 commit 行数 ${committed}(调用方必须保证等长替换且 startIdx 在 buffer 内)`);
282
+ }
283
+ const end = Math.min(startIdx + lines.length, committed);
284
+ const actualOld = end - startIdx;
285
+ if (actualOld !== lines.length) {
286
+ throw new Error(`replaceHead: 行数不匹配(startIdx=${startIdx},新区间 ${actualOld} 行 ≠ 新行 ${lines.length} 行)`);
287
+ }
288
+ // splice 等长替换:rows.length 不变,segMark 若在区间前方不受影响,区间内被覆盖时平移到 startIdx
289
+ rows.splice(startIdx, lines.length, ...lines);
290
+ if (segMark) {
291
+ const m = segMark.rowIdx;
292
+ if (m >= startIdx && m < end) {
293
+ segMark.rowIdx = startIdx; // 段起点被覆盖 → 重锚到区间头
294
+ }
295
+ }
296
+ }
package/dist/ui/diff.js CHANGED
@@ -215,7 +215,7 @@ export function renderFileChange(opts) {
215
215
  const pathDisp = path; // 路径原样展示(不做截断,长路径由 contentWrite 折行)
216
216
  const verb = oldStr === null ? 'Create' : 'Update';
217
217
  // 头行:Update(path) / Create(path)
218
- const head = `${HEAD_INDENT}${ui.brightMagenta}${verb}${ui.reset}${ui.gray}(${ui.reset}${ui.cyan}${pathDisp}${ui.reset}${ui.gray})${ui.reset}`;
218
+ const head = `${HEAD_INDENT}${ui.bold}${ui.accent}${verb}${ui.reset}${ui.gray}(${ui.reset}${ui.accent}${pathDisp}${ui.reset}${ui.gray})${ui.reset}`;
219
219
  // 新建:整文件作 + 行,行号从 1
220
220
  if (oldStr === null) {
221
221
  const lines = splitLines(newStr);
@@ -1,7 +1,7 @@
1
1
  import readline from 'node:readline';
2
2
  import { stdin, stderr } from 'node:process';
3
3
  import { ui } from './theme.js';
4
- import { displayWidth, truncateDisplay, wrapByDisplayWidth } from './render.js';
4
+ import { displayWidth, truncateDisplay, visColToCharCol, wrapByDisplayWidth } from './render.js';
5
5
  import * as layout from './layout.js';
6
6
  import * as mouse from './mouse.js';
7
7
  import { Spinner } from './spinner.js';
@@ -92,8 +92,8 @@ export async function promptIntervention(req) {
92
92
  break;
93
93
  // 选中项:▸ 与正文均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
94
94
  const isSel = idx === selected;
95
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
96
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
95
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
96
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
97
97
  // 数字前缀:只标真实选项(1-9,与 onKeyChoice 的数字直选对应);"自定义"项不占号。
98
98
  const numStr = idx < options.length ? `${idx + 1}. ` : '';
99
99
  const prefixWidth = 2 + numStr.length; // marker(1)+空格(1)+numStr
@@ -132,6 +132,28 @@ export async function promptIntervention(req) {
132
132
  lines.push(`${ui.dim}Enter 提交 · Esc ${cameFromChoice ? '返回选项' : '取消'} · Ctrl+C 取消${ui.reset}`);
133
133
  return lines;
134
134
  }
135
+ /** 鼠标左键点击输入框 → 把 cursor 移到点击位置(与 prompt.ts 的 applyExternalCursor 同源逻辑)。
136
+ * intervention 的 input 只有一行 text(无 chip / 多行),算法简化:flatIdx=0,段内 visCol → charCol。 */
137
+ function applyExternalCursor(_flatIdx, inSegVis) {
138
+ if (mode !== 'input')
139
+ return; // choice 模式下点击输入框不移动光标(无文本可定位)
140
+ // 把屏幕 visCol 映射到 text 的字符偏移:wrapByDisplayWidth 复刻 paintInput 的折行,
141
+ // visColToCharCol 把显示列反推到字符索引(处理 CJK 全角字符)。
142
+ const g = layout.getGeo();
143
+ const promptW = displayWidth('❯ ');
144
+ const W = Math.max(1, g.cols - promptW);
145
+ const segs = wrapByDisplayWidth(text, W);
146
+ // _flatIdx 由 layout 端算出(点击落在哪段),这里用它定位段;越界兜底末段。
147
+ const seg = segs[Math.min(_flatIdx, segs.length - 1)] ?? '';
148
+ const inChar = visColToCharCol(seg, inSegVis);
149
+ // 累加前面段的字符数 → text 中的绝对偏移。
150
+ let offset = 0;
151
+ for (let i = 0; i < Math.min(_flatIdx, segs.length); i++)
152
+ offset += segs[i].length;
153
+ offset += inChar;
154
+ cursor = Math.max(0, Math.min(offset, text.length));
155
+ redraw();
156
+ }
135
157
  function redraw() {
136
158
  if (mode === 'choice') {
137
159
  const hint = '↑↓ 选择 · Enter 确认 · 数字键直选 · Esc 取消';
@@ -163,6 +185,7 @@ export async function promptIntervention(req) {
163
185
  /** 退出:摘自己的监听 + 恢复快照监听 + 擦菜单恢复内容区。不 setRawMode(false)/pause stdin(运行态由 repl 接管)。 */
164
186
  function cleanup() {
165
187
  layout.setMouseEnabled(true); // 恢复鼠标框选(面板期间禁,防拖拽覆盖菜单)
188
+ layout.setCursorChangeHandler(null); // 注销光标变更处理器
166
189
  emitter.removeListener('keypress', onKey);
167
190
  for (const l of savedListeners)
168
191
  emitter.on('keypress', l);
@@ -183,6 +206,8 @@ export async function promptIntervention(req) {
183
206
  layout.resetScroll();
184
207
  else
185
208
  layout.repaintViewport();
209
+ // 恢复走时计时器(RUNNING 态):面板期间 stopTurnTimer 停了心跳,退出后恢复状态行 200ms 刷新。
210
+ layout.startTurnTimerIfRunning();
186
211
  }
187
212
  function onKey(_str, key) {
188
213
  if (resolved || !key)
@@ -310,8 +335,10 @@ export async function promptIntervention(req) {
310
335
  return new Promise((res, rej) => {
311
336
  resolve = res;
312
337
  try {
313
- // 进入面板:停 spinner(避免 onFrame 覆盖)+ 禁鼠标框选(防拖拽 viewport 重画覆盖菜单)+ 回尾(若用户正滚动回看)
338
+ // 进入面板:停 spinner(避免 onFrame 覆盖)+ 停走时计时器(避免 drawStatusBar 200ms 心跳
339
+ // 把真光标拉到 runningCaretPos 覆盖 paintInput 的正确光标位)+ 禁鼠标框选(防拖拽 viewport 重画覆盖菜单)+ 回尾(若用户正滚动回看)
314
340
  Spinner.pauseCurrent();
341
+ layout.stopTurnTimer();
315
342
  layout.setMouseEnabled(false);
316
343
  layout.resetScroll();
317
344
  // 快照现有 keypress 监听(运行态的 onRunningKey)并摘掉,挂自己的 onKey
@@ -328,6 +355,7 @@ export async function promptIntervention(req) {
328
355
  }
329
356
  stdin.resume();
330
357
  emitter.on('keypress', onKey);
358
+ layout.setCursorChangeHandler(applyExternalCursor); // 鼠标左键单击输入框 → 移光标到点击位
331
359
  redraw();
332
360
  }
333
361
  catch (e) {
package/dist/ui/layout.js CHANGED
@@ -322,6 +322,67 @@ function commitMd() {
322
322
  mdBuf = '';
323
323
  content.commitSegment();
324
324
  }
325
+ // ── Banner(启动横幅/模式切换横幅)固定顶部行 ──
326
+ // 与 contentWrite 的「续写式追加」不同:banner 自带模式行集合(MoCode logo + 信息行),
327
+ // 反复 writeBanner(同一启动会话内同一份 banner 反复画 — 启动后、换 model 后、刷新 snapshot 后)
328
+ // 应让 buffer 顶部始终是「最新的一份 banner」,而不是堆 5 份。
329
+ // rewriteBanner 在 banner 已建好时原地等长替换顶部 bannerH 行,viewport 自动从 rows[] 头读新版。
330
+ //
331
+ // bannerH = bannerLines(banner()) 的行数(目前 5:4 行 logo+info + 1 空行分隔)。如未来
332
+ // bannerString 变化,调用方需重调 writeBanner 重设 bannerH。
333
+ let bannerH = 0;
334
+ let bannerRows = []; // 最新写入的 banner 行(供 rewriteBanner 复用 / 调试)
335
+ /**
336
+ * 在 content 缓冲顶部建一份 banner;**首次**调用建好(bannerH = lines.length),**重复**调用
337
+ * 等价 rewriteBanner(lines)(覆盖式刷新,供启动后再写 banner 也安全)。
338
+ *
339
+ * lines 必须与首次写入等长(否则 content.replaceHead 抛错)— 调用方应统一传 bannerLines(banner())。
340
+ * 滚动回看冻结(scrollOffset+=delta / 改 offset 不变):行数不变 offset 不动 → 视图冻结,无须 repaint 风暴。
341
+ * 仅当 scrollOffset === 0 时 repaintViewport 让新版 banner 立刻进 viewport 顶部。
342
+ */
343
+ export function writeBanner(lines) {
344
+ if (bannerH === 0) {
345
+ // 首次:buffer 通常空(启动最早时),用 setLines 灌入;若 buffer 非空(如 reseed)说明 banner
346
+ // 已被前置写过,throw 阻止静默错位 — 调用方应先 reset banner 然后 writeBanner。
347
+ if (mdActive)
348
+ commitMd();
349
+ const existed = content.totalRows();
350
+ if (existed !== 0) {
351
+ throw new Error(`writeBanner: 首次调用时 buffer 已有 ${existed} 行,需先 clearContent 再 writeBanner(否则会覆盖已有内容)`);
352
+ }
353
+ content.setLines(lines);
354
+ bannerH = lines.length;
355
+ bannerRows = lines;
356
+ if (scrollOffset === 0)
357
+ repaintViewport();
358
+ return;
359
+ }
360
+ // 后续:等价 rewriteBanner
361
+ rewriteBanner(lines);
362
+ }
363
+ /**
364
+ * 把 banner 顶部 bannerH 行**等长替换**为新行;banner 后的内容(对话历史)位置不动。
365
+ * 行数与首次 writeBanner 不一致 → 抛错(避免 banner 区错位);如必须改,先清空 content 后再 writeBanner。
366
+ * 滚动回看冻结:行数不变,offset 不动。等 scrollOffset 回到 0 时 viewport 自动显新版 banner。
367
+ */
368
+ export function rewriteBanner(lines) {
369
+ if (bannerH === 0) {
370
+ // 没建过 banner:等价首次 writeBanner(允许首次调用直接进 rewriteBanner 的别名入口)
371
+ writeBanner(lines);
372
+ return;
373
+ }
374
+ if (lines.length !== bannerH) {
375
+ throw new Error(`rewriteBanner: 行数 ${lines.length} ≠ 已建 bannerH ${bannerH}(调用方需统一行数)`);
376
+ }
377
+ content.replaceHead(0, lines);
378
+ bannerRows = lines;
379
+ if (scrollOffset === 0)
380
+ repaintViewport();
381
+ }
382
+ /** 测试 / 调试用:当前 banner 占多少行;0 表示未启用。 */
383
+ export function bannerHeight() {
384
+ return bannerH;
385
+ }
325
386
  /**
326
387
  * markdown 正文写(替代 contentWrite 用于 agent onText):累积 chunk 到 mdBuf,每 chunk 把整段
327
388
  * mdBuf 经 renderMarkdown 渲成自洽 ANSI 行,replace 缓冲段(content.setLines 截旧 + 写新),
@@ -458,6 +519,19 @@ export function contentInsertAfter(after, lines) {
458
519
  if (scrolled) {
459
520
  scrollOffset = Math.max(0, Math.min(scrollOffset + delta, Math.max(0, content.totalRows() - g.contentBottom)));
460
521
  }
522
+ // 展开工具信息时保持视口位置:当 scrollOffset===0(未滚动)且插入点在视口内时,
523
+ // 计算 scrollOffset 让视口锚定在插入前的绝对行位置,展开内容在下面自然展开,
524
+ // 而不是自动跳到展开内容底部(用户体验:点击摘要行,视口不动,详情在下方展开)。
525
+ // 关键:插入点绝对行 = after;插入前视口尾行绝对行 = totalBefore - 1;
526
+ // 插入后要让原视口尾行仍在屏底 → scrollOffset = 插入后新增的、在原视口尾行之后的行数。
527
+ if (!scrolled && after < totalBefore) {
528
+ // 插入点在原缓冲内(非追加到末尾),计算需要滚动的偏移量
529
+ const insertedAfterViewport = after >= (totalBefore - g.contentBottom);
530
+ if (insertedAfterViewport) {
531
+ // 插入点在视口内:保持视口位置,scrollOffset = 插入行数(展开内容在视口下方)
532
+ scrollOffset = Math.min(delta, Math.max(0, content.totalRows() - g.contentBottom));
533
+ }
534
+ }
461
535
  // batch 摘要行索引平移
462
536
  void import('./batch.js').then((m) => m.shiftBatchesAfter(after, delta));
463
537
  repaintViewport();
@@ -936,6 +1010,24 @@ function handleMouseEvent(e) {
936
1010
  scrollBy(e.dir * WHEEL_LINES);
937
1011
  return;
938
1012
  }
1013
+ // 输入行左键点击:即使 mouseEnabled=false(如 intervention 面板期间)也允许定位光标,
1014
+ // 提升用户体验(点哪插哪)。拖拽/右键/内容区点击仍受 mouseEnabled 限制。
1015
+ if (e.button === 0 && e.type === 'press' && isInputRow(e.row)) {
1016
+ setInputCursorFromClick(e.row, e.col);
1017
+ const pos = inputScreenToInputPos(e.row, e.col);
1018
+ if (pos) {
1019
+ if (selection) {
1020
+ selection = null;
1021
+ }
1022
+ inputSelection = {
1023
+ anchor: { line: pos.line, col: pos.displayCol },
1024
+ end: { line: pos.line, col: pos.displayCol },
1025
+ dragged: false,
1026
+ };
1027
+ paintInput(lastView);
1028
+ }
1029
+ return;
1030
+ }
939
1031
  if (!mouseEnabled)
940
1032
  return;
941
1033
  const g = getGeo();
@@ -971,26 +1063,8 @@ function handleMouseEvent(e) {
971
1063
  if (e.button !== 0)
972
1064
  return; // 其余中/右键 press/drag 不处理(终端原生右键菜单等不受影响)
973
1065
  if (e.type === 'press') {
974
- if (isInputRow(e.row)) {
975
- // 左键落输入行:进输入态 + 光标定位到点击位 + 开输入框反白选区(起点 = 终点 = 点击位)。
976
- // 不再 return——后续 drag/release 分支也要知道选区在输入框。点击未拖动时 release 端会清掉选区,
977
- // 与"点一下就定位光标、起手打字"的习惯一致。
978
- setInputCursorFromClick(e.row, e.col);
979
- const pos = inputScreenToInputPos(e.row, e.col);
980
- if (pos) {
981
- // 清掉内容区旧选区(若有):按一下输入框不应同时保留内容区反白,视觉混淆。
982
- if (selection) {
983
- selection = null;
984
- }
985
- inputSelection = {
986
- anchor: { line: pos.line, col: pos.displayCol },
987
- end: { line: pos.line, col: pos.displayCol },
988
- dragged: false,
989
- };
990
- paintInput(lastView);
991
- }
992
- return;
993
- }
1066
+ // 输入行左键已在上方 mouseEnabled 检查之前处理(支持 intervention 面板期间点击定位光标)
1067
+ // 此处只处理内容区选区。
994
1068
  selecting = true;
995
1069
  const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
996
1070
  const absLine = screenRowToAbsLine(rowInContent);
@@ -1129,26 +1203,26 @@ function composeSpinnerLine(status, cols) {
1129
1203
  // 滚动回看:左段 = 符号(◆ 或 心跳帧) + 状态名(灰,无走时);右段 = 历史指示。
1130
1204
  // 跟非回看的 INPUT/RUNNING 态保持一致——避免「◆ 留下、状态字蒸发」的视觉错觉。
1131
1205
  const symbol = spinning
1132
- ? `${ui.brightMagenta}${RUNNING_FRAMES[runningFrame]}${ui.reset}`
1133
- : `${ui.brightCyan}◆${ui.reset}`;
1206
+ ? `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset}`
1207
+ : `${ui.accent}◆${ui.reset}`;
1134
1208
  lead = `${symbol} ${ui.dim}${status.status}${ui.reset}`;
1135
1209
  leadW = 1 + 1 + displayWidth(status.status);
1136
1210
  }
1137
1211
  else if (hasSpinner) {
1138
1212
  // spinner 激活(思考中/执行工具…):帧 + 状态 + 走时
1139
1213
  const ePart = elapsed ? ` ${ui.dim}${elapsed}${ui.reset}` : '';
1140
- lead = `${ui.brightMagenta}${status.spinnerFrame}${ui.reset} ${ui.dim}${status.status}${ui.reset}${ePart}`;
1214
+ lead = `${ui.bold}${ui.accent}${status.spinnerFrame}${ui.reset} ${ui.dim}${status.status}${ui.reset}${ePart}`;
1141
1215
  leadW = 1 + 1 + displayWidth(status.status) + (elapsed ? 1 + displayWidth(elapsed) : 0);
1142
1216
  }
1143
1217
  else if (spinning) {
1144
1218
  // 运行态心跳帧(流式输出中):帧 + 走时
1145
1219
  const ePart = elapsed ? ` ${ui.dim}${elapsed}${ui.reset}` : '';
1146
- lead = `${ui.brightMagenta}${RUNNING_FRAMES[runningFrame]}${ui.reset}${ePart}`;
1220
+ lead = `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset}${ePart}`;
1147
1221
  leadW = 1 + (elapsed ? 1 + displayWidth(elapsed) : 0);
1148
1222
  }
1149
1223
  else {
1150
1224
  // INPUT 态:◆ + 状态文字(无走时)
1151
- lead = `${ui.brightCyan}◆${ui.reset} ${ui.dim}${status.status}${ui.reset}`;
1225
+ lead = `${ui.accent}◆${ui.reset} ${ui.dim}${status.status}${ui.reset}`;
1152
1226
  leadW = 1 + 1 + displayWidth(status.status);
1153
1227
  }
1154
1228
  // ── 右段:仅滚动回看时显历史指示 ──
@@ -1169,7 +1243,7 @@ function composeModelLine(status, cols) {
1169
1243
  const ctxW = ansiDisplayWidth(ctx);
1170
1244
  // 左段:模式标识 + 切换提示(灰)+ 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
1171
1245
  const modeTag = status.modeTag ?? '';
1172
- const modeColor = modeTag === 'plan' ? ui.yellow : ui.brightCyan;
1246
+ const modeColor = modeTag === 'plan' ? ui.yellow : ui.accent;
1173
1247
  const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
1174
1248
  const modeW = modeTag ? displayWidth(modeTag) : 0;
1175
1249
  // 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
@@ -1293,13 +1367,18 @@ function startTurnTimer() {
1293
1367
  }, 200);
1294
1368
  turnTimer.unref();
1295
1369
  }
1296
- /** 停走时计时器。enterInputMode / exitAltScreen 调。 */
1297
- function stopTurnTimer() {
1370
+ /** 停走时计时器。enterInputMode / exitAltScreen 调;intervention 面板进入时也调(防 drawStatusBar 200ms 心跳把光标拉到 runningCaretPos,覆盖 paintInput 的正确光标位)。 */
1371
+ export function stopTurnTimer() {
1298
1372
  if (turnTimer) {
1299
1373
  clearInterval(turnTimer);
1300
1374
  turnTimer = null;
1301
1375
  }
1302
1376
  }
1377
+ /** 恢复走时计时器(若 RUNNING 态)。intervention 面板退出时调,恢复状态行走时心跳。 */
1378
+ export function startTurnTimerIfRunning() {
1379
+ if (active && mode === 'running')
1380
+ startTurnTimer();
1381
+ }
1303
1382
  /**
1304
1383
  * 在续写位画一行瞬时活动文本(spinner 帧):不进缓冲、不推进续写位,逐行 clearLine 重画。
1305
1384
  * 仅 TTY + offset=0(实时尾)+ 非打字暂停态时物理写屏;滚动态跳过(由状态行 spinner 兜底,且避免覆盖 viewport 历史行)。
@@ -1540,7 +1619,7 @@ export function paintInput(view) {
1540
1619
  buf += cup(spinnerRow, 1) + esc.clearLine + composeSpinnerLine(status, g.cols);
1541
1620
  buf += cup(modelRow, 1) + esc.clearLine + composeModelLine(status, g.cols);
1542
1621
  // 3b. 上线(输入框顶):满屏宽细线 ─(cyan),框住输入区上边界
1543
- buf += cup(g.contentBottom + 3, 1) + esc.clearLine + ui.cyan + '─'.repeat(g.cols) + ui.reset;
1622
+ buf += cup(g.contentBottom + 3, 1) + esc.clearLine + ui.accent + '─'.repeat(g.cols) + ui.reset;
1544
1623
  // 4. 输入行(g.contentBottom+4 .. rows-1)——按可视行画,首行带 prompt、其余缩进 promptW
1545
1624
  const firstInputRow = g.contentBottom + 4;
1546
1625
  const inputRowsAvail = g.footerH - 5; // 去掉虚拟空/spinner行/上线/下线/model行,留输入行
@@ -1598,7 +1677,7 @@ export function paintInput(view) {
1598
1677
  buf += cup(r, 1) + esc.clearLine + text;
1599
1678
  }
1600
1679
  // 4b. 下线(输入框底):满屏宽细线 ─(cyan),在 model 行上一行(model 行占屏底 rows)
1601
- buf += cup(g.rows - 1, 1) + esc.clearLine + ui.cyan + '─'.repeat(g.cols) + ui.reset;
1680
+ buf += cup(g.rows - 1, 1) + esc.clearLine + ui.accent + '─'.repeat(g.cols) + ui.reset;
1602
1681
  // 5. 向上菜单(画在内容区底,底栏正上方)
1603
1682
  if (view.menu && view.menu.lines.length > 0) {
1604
1683
  const menuRows = Math.min(view.menu.lines.length, g.contentBottom);
package/dist/ui/prompt.js CHANGED
@@ -84,7 +84,7 @@ export async function promptWithSlashMenu(opts) {
84
84
  let menuOpen = false;
85
85
  let selected = 0;
86
86
  let filtered = [];
87
- const MENU_MAX_VISIBLE = 5;
87
+ const MENU_MAX_VISIBLE = 7;
88
88
  let menuTop = 0; // 窗口首项在 filtered 中的索引,菜单最多显示 MENU_MAX_VISIBLE 条
89
89
  let resolved = false;
90
90
  let resolve;
@@ -108,8 +108,8 @@ export async function promptWithSlashMenu(opts) {
108
108
  const globalIdx = menuTop + i;
109
109
  // 选中项:▸ 与文字均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
110
110
  const isSel = globalIdx === selected;
111
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
112
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
111
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
112
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
113
113
  const name = padEndDisplay(c.name, maxName);
114
114
  // 首/末附加滚动指示(▲/▼)而非替换整行
115
115
  let desc = c.desc;
@@ -699,8 +699,8 @@ export async function promptTurnPicker(items) {
699
699
  const idx = start + i;
700
700
  // 选中项:▸/序号/正文均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
701
701
  const isSel = idx === selected;
702
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
703
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
702
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
703
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
704
704
  const num = `${color}${idx + 1}${ui.reset}`;
705
705
  const text = truncateDisplay(items[idx].firstLine, cols - 6);
706
706
  return `${marker} ${num} ${color}${text}${ui.reset}`;
@@ -826,8 +826,8 @@ export async function promptSessionPicker(items, recentCap = 10) {
826
826
  const idx = start + i;
827
827
  // 选中项:▸/序号/正文/副标题均 cyan+bold(去 dim),未选中项保持 dim——选中行整体高亮。
828
828
  const isSel = idx === selected;
829
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
830
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
829
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
830
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
831
831
  const num = String(idx + 1);
832
832
  const it = vis[idx];
833
833
  const title = it.title || '(无)';
@@ -965,8 +965,8 @@ export async function promptThemePicker(items) {
965
965
  return Array.from({ length: count }, (_, i) => {
966
966
  const idx = start + i;
967
967
  const isSel = idx === selected;
968
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
969
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
968
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
969
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
970
970
  const num = String(idx + 1);
971
971
  const it = items[idx];
972
972
  const title = it.title || '(无)';
@@ -1089,8 +1089,8 @@ export async function promptRevertChoice(fileCount) {
1089
1089
  const cols = layout.getGeo().cols;
1090
1090
  return items.map((text, idx) => {
1091
1091
  const isSel = idx === selected;
1092
- const color = isSel ? `${ui.cyan}${ui.bold}` : ui.dim;
1093
- const marker = isSel ? `${ui.cyan}${ui.bold}▸${ui.reset}` : ' ';
1092
+ const color = isSel ? `${ui.accent}${ui.bold}` : ui.dim;
1093
+ const marker = isSel ? `${ui.accent}${ui.bold}▸${ui.reset}` : ' ';
1094
1094
  const num = `${color}${idx + 1}${ui.reset}`;
1095
1095
  const t = truncateDisplay(text, cols - 6);
1096
1096
  return `${marker} ${num} ${color}${t}${ui.reset}`;
package/dist/ui/render.js CHANGED
@@ -231,42 +231,68 @@ export function wrapByDisplayWidth(text, width) {
231
231
  }
232
232
  const BOX_W = 60; // 内容区显示宽度(logo 区 + 信息区)
233
233
  const MARGIN = ' '; // 盒外左缩进
234
- // ── 坐姿 logo(乌龟风,加双手)──
235
- const LOGO_W = 11; // logo 区显示宽度
236
- const LOGO_GAP = 4; // logo 与信息区之间的间隔
237
- const LOGO_PAD = ' '.repeat(LOGO_W + LOGO_GAP); // logo 行的缩进(对齐信息区)
238
- // 坐着的小人:[●ᴗ●] 头 / | | 身 / ╲| |╱ 双手外撑 / | | 身 / OO 脚
234
+ // ── MoCode 块字符 logo(4 行,neofetch 风)──
235
+ const LOGO_W = 35; // logo 区显示宽度
236
+ const LOGO_GAP = 0; // logo 与信息区之间的间隔
237
+ // 块字符画 4 行:M / o / C / o / d / e,块字符上半/下半合并两个 ASCII 行
239
238
  const LOGO_LINES = [
240
- ' [◕ᴗ◕] ',
241
- ' ●| |● ',
242
- ' OO ',
239
+ '█▀█▀█ █▀▀▀█ █ ',
240
+ ' █ █ █▀▀█ █ █▀▀█ █▀▀█ █▀▀█',
241
+ '█ █ █ █ █ █ ▄ █ █ █ █ █▀▀▀',
242
+ '▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀',
243
243
  ];
244
- /** 取第 idx 行 logo(着色 + 补宽);越界返空格(logo 下方行仍缩进对齐)。 */
244
+ /** 取第 idx 行 logo(着色 + 补宽 + LOGO_GAP 间隔),与 info 行直接拼接(neofetch )。 */
245
245
  function logoLine(idx) {
246
- if (idx >= LOGO_LINES.length)
247
- return LOGO_PAD;
248
- return `${ui.brightCyan}${padEndDisplay(LOGO_LINES[idx], LOGO_W)}${ui.reset}${' '.repeat(LOGO_GAP)}`;
246
+ return `${ui.accent}${padEndDisplay(LOGO_LINES[idx], LOGO_W)}${ui.reset}${' '.repeat(LOGO_GAP)}`;
249
247
  }
250
248
  function labelContent(label, value) {
251
249
  return `${ui.dim}${padEndDisplay(label, 6)}${ui.reset}${value}`;
252
250
  }
251
+ /** 把布尔转成显式中文:banner 上避免「是/否」歧义,用「开/关」。 */
252
+ function memoryLabel(on) {
253
+ return on ? '开启' : '关闭';
254
+ }
255
+ /** 记忆状态染色:开启用 ui.accent(亮),关闭用 ui.dim(弱),让远观有差别。
256
+ * banner 里整体仍走 ui.reset 兜底——染色只贴在值那一列。 */
257
+ function memoryValue(on) {
258
+ return on ? `${ui.accent}${memoryLabel(on)}${ui.reset}` : `${ui.dim}${memoryLabel(on)}${ui.reset}`;
259
+ }
253
260
  /** 横幅纯文本(带 ANSI 颜色,不写出)——供 TUI 经 contentWrite 写入内容区以跟踪续写位。
254
- * 无边框:左侧实心小熊 + 右侧标题/信息(模型/目录),末尾一行提示。 */
261
+ * 布局:大字 logo(4 行,块字符)左对齐,右侧并排放标题/信息(neofetch 风) */
255
262
  export function bannerString(info) {
256
- const title = `${ui.bold}${ui.brightCyan}◆ MoCode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
263
+ const title = `${ui.bold}${ui.accent}◆ MoCode${ui.reset} ${ui.dim}v${VERSION}${ui.reset}`;
264
+ // 信息行 = 标题 + 模型 + 目录 + 记忆(共 4 行,与 4 行 logo 对齐)
257
265
  const rows = [
258
266
  logoLine(0) + title,
259
267
  logoLine(1) + labelContent('模型', info.model),
260
268
  logoLine(2) + labelContent('目录', truncateDisplay(info.cwd, 48)),
269
+ logoLine(3) + labelContent('记忆', memoryValue(info.memoryEnabled)),
261
270
  ];
262
- return (rows.map((r) => MARGIN + r).join('\n') +
263
- '\n\n' +
264
- `${MARGIN}${ui.dim}直接描述任务,agent 会自动读写文件与执行命令。${ui.reset}\n`);
271
+ return rows.map((r) => MARGIN + r).join('\n') + '\n\n';
265
272
  }
266
- /** 启动横幅:小熊 logo + 标题/信息 + 一行提示。纯渲染,不依赖 config / 业务。 */
273
+ /** 启动横幅:大字 logo + 标题/信息。纯渲染,不依赖 config / 业务。 */
267
274
  export function printBanner(info) {
268
275
  stdout.write(bannerString(info));
269
276
  }
277
+ /**
278
+ * 与 bannerString 等价但返**自洽 ANSI 行数组**(每行均以 \x1B[0m 收尾),供 layout.writeBanner
279
+ * 直接灌入 content 缓冲,免去 contentWrite 走字符流再 breakRow 的列宽折行问题。
280
+ *
281
+ * 行集合 = bannerString 解析后的所有物理行。layout 记录此长度作
282
+ * bannerH,rewriteBanner 必须按等长替换(否则 layout 抛错)。
283
+ *
284
+ * 返回前会 normalize:每行若未以 \x1B[0m 收尾则补,统一自洽,避免 setLines / replaceHead
285
+ * 内部 ANSI 状态机把后续 SGR 当 curSgr 残留继承下去。
286
+ */
287
+ export function bannerLines(info) {
288
+ const raw = bannerString(info);
289
+ // bannerString 形如 "行1\n行2\n行3\n行4\n",split('\n') 长度 = 5(末尾 \n 后空串)
290
+ // 行集合真实包含 4 行(4 行 logo+info);filter(Boolean?) 不可 — 空行也要算
291
+ // 仅过滤最后那个 \n 切出的尾空
292
+ const split = raw.split('\n');
293
+ const lines = split.length > 0 && split[split.length - 1] === '' ? split.slice(0, -1) : split;
294
+ return lines.map((s) => (s.endsWith('\x1B[0m') ? s : `${s}\x1B[0m`));
295
+ }
270
296
  // ── 工具调用 / 结果 摘要 ──
271
297
  function tryParse(raw) {
272
298
  try {
@@ -53,7 +53,7 @@ export class Spinner {
53
53
  }
54
54
  render() {
55
55
  const f = FRAMES[this.frame];
56
- stdout.write(`\r${ui.brightMagenta}${f}${ui.reset} ${ui.dim}${this.msg}${ui.reset}\x1B[K`);
56
+ stdout.write(`\r${ui.bold}${ui.accent}${f}${ui.reset} ${ui.dim}${this.msg}${ui.reset}\x1B[K`);
57
57
  }
58
58
  stop() {
59
59
  if (!this.timer)
package/dist/ui/theme.js CHANGED
@@ -28,6 +28,7 @@ const DEFAULT = {
28
28
  gray: '\x1B[38;2;92;99;112m',
29
29
  brightCyan: '\x1B[38;2;86;182;194m',
30
30
  brightMagenta: '\x1B[38;2;198;120;221m',
31
+ accent: '\x1B[38;2;86;182;194m', // 与 cyan 同源(One Dark):logo/标题/输入框顶线/选中项统一承载
31
32
  userBg: '\x1B[48;2;44;49;60m', // One Dark current-line bg,深色终端上微妙可辨
32
33
  // diff 行底色:One Dark bg #282c34 上加 ~14% 亮度的对应色,够辨识但不刺眼
33
34
  addBg: '\x1B[48;2;44;62;42m', // 偏暗绿(One Dark green(152,195,121)暗化)
@@ -51,6 +52,7 @@ const THEMES = {
51
52
  gray: '\x1B[38;2;88;110;117m',
52
53
  brightCyan: '\x1B[38;2;42;161;152m',
53
54
  brightMagenta: '\x1B[38;2;108;113;196m',
55
+ accent: '\x1B[38;2;38;139;210m', // Solarized blue:浅底下更醒目的强调
54
56
  userBg: '\x1B[48;2;238;232;213m',
55
57
  // diff 行底色:Solarized Light base2(238,232,213)上贴同色族浅 tint,
56
58
  // 比直接用 base1 更柔,跟深 fg(red/green)对比充足
@@ -67,6 +69,7 @@ const THEMES = {
67
69
  gray: '\x1B[38;2;88;110;117m',
68
70
  brightCyan: '\x1B[38;2;147;161;161m',
69
71
  brightMagenta: '\x1B[38;2;108;113;196m',
72
+ accent: '\x1B[38;2;42;161;152m', // Solarized cyan(深底版):暗底上跳出
70
73
  userBg: '\x1B[48;2;7;54;66m',
71
74
  // diff 行底色:Solarized Dark base03(0,43,54)上加对应色族暗 tint
72
75
  addBg: '\x1B[48;2;20;50;38m',
@@ -82,6 +85,7 @@ const THEMES = {
82
85
  gray: '\x1B[38;2;146;131;116m',
83
86
  brightCyan: '\x1B[38;2;142;192;124m',
84
87
  brightMagenta: '\x1B[38;2;211;134;155m',
88
+ accent: '\x1B[38;2;250;189;47m', // Gruvbox yellow(主题色)
85
89
  userBg: '\x1B[48;2;40;40;40m',
86
90
  // diff 行底色:Gruvbox dark bg(40,40,40)上贴暗 bg0_a 风格
87
91
  addBg: '\x1B[48;2;40;55;30m',
@@ -97,11 +101,121 @@ const THEMES = {
97
101
  gray: '\x1B[38;2;76;86;106m',
98
102
  brightCyan: '\x1B[38;2;136;192;208m',
99
103
  brightMagenta: '\x1B[38;2;180;142;173m',
104
+ accent: '\x1B[38;2;136;192;208m', // Nord 浅冰蓝(主题色)
100
105
  userBg: '\x1B[48;2;67;76;94m',
101
106
  // diff 行底色:Nord polar night(46,52,64)上贴对应色族暗 tint
102
107
  addBg: '\x1B[48;2;46;66;52m',
103
108
  delBg: '\x1B[48;2;72;46;52m',
104
109
  },
110
+ orange: {
111
+ // 秋季/南瓜橙:深暖底(#231c16 系)+ accent 主强调走橙色;yellow 槽也偏橙
112
+ // 保持调性一致(标题、菜单等关键 UI 元素走 accent);red 取暖珊瑚避免与
113
+ // 橙黄撞色;蓝/紫/青保留冷色锚点维持多色辨识度。
114
+ red: '\x1B[38;2;255;105;90m', // 暖珊瑚红
115
+ green: '\x1B[38;2;150;200;115m', // 黄绿
116
+ yellow: '\x1B[38;2;255;170;60m', // 南瓜橙(主题色)
117
+ blue: '\x1B[38;2;120;170;230m', // 冷蓝锚点
118
+ magenta: '\x1B[38;2;225;130;195m', // 暖粉紫
119
+ cyan: '\x1B[38;2;110;195;195m', // 雾青
120
+ gray: '\x1B[38;2;150;130;110m', // 暖灰
121
+ brightCyan: '\x1B[38;2;160;220;220m',
122
+ brightMagenta: '\x1B[38;2;245;165;215m',
123
+ accent: '\x1B[38;2;255;170;60m', // 南瓜橙(主题主色:logo/标题/输入框顶线/选中项)
124
+ userBg: '\x1B[48;2;55;42;32m', // 暖深棕(略亮于 #231c16 背景)
125
+ // diff 行底色:暖深棕底上贴对应色族暗 tint,跟 fg 配对柔和可辨
126
+ addBg: '\x1B[48;2;55;70;35m',
127
+ delBg: '\x1B[48;2;78;42;32m',
128
+ },
129
+ rose: {
130
+ // 玫红:深紫底 + accent 玫粉作主强调;magenta 槽与 accent 同源(主题色),
131
+ // red 偏暖玫;蓝/青保留冷色锚点维持多色辨识度。
132
+ red: '\x1B[38;2;240;100;120m', // 暖玫红
133
+ green: '\x1B[38;2;160;200;130m', // 黄绿
134
+ yellow: '\x1B[38;2;240;180;140m', // 暖杏
135
+ blue: '\x1B[38;2;130;160;230m', // 冷蓝锚点
136
+ magenta: '\x1B[38;2;230;90;170m', // 主玫红(主题色)
137
+ cyan: '\x1B[38;2;120;200;200m', // 雾青
138
+ gray: '\x1B[38;2;160;140;150m', // 暖灰紫
139
+ brightCyan: '\x1B[38;2;170;220;220m',
140
+ brightMagenta: '\x1B[38;2;250;160;200m',
141
+ accent: '\x1B[38;2;230;90;150m', // 玫粉(主题主色)
142
+ userBg: '\x1B[48;2;50;32;46m', // 深紫底
143
+ // diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏紫红
144
+ addBg: '\x1B[48;2;50;60;42m',
145
+ delBg: '\x1B[48;2;75;40;52m',
146
+ },
147
+ emerald: {
148
+ // 翡翠绿:深绿底 + accent 翡翠绿作主强调;green 槽与 accent 同源(主题色),
149
+ // cyan 偏青绿;蓝/紫保留冷色锚点,red/yellow 保留暖色警示色。
150
+ red: '\x1B[38;2;235;110;110m', // 暖红(警示色)
151
+ green: '\x1B[38;2;80;210;140m', // 主翡翠绿(主题色)
152
+ yellow: '\x1B[38;2;230;200;100m', // 暖黄
153
+ blue: '\x1B[38;2;100;180;210m', // 冷青蓝
154
+ magenta: '\x1B[38;2;190;140;210m', // 冷紫
155
+ cyan: '\x1B[38;2;100;210;190m', // 青绿
156
+ gray: '\x1B[38;2;110;140;130m', // 冷绿灰
157
+ brightCyan: '\x1B[38;2;140;230;210m',
158
+ brightMagenta: '\x1B[38;2;210;170;230m',
159
+ accent: '\x1B[38;2;80;210;140m', // 翡翠绿(主题主色)
160
+ userBg: '\x1B[48;2;18;42;34m', // 深绿底
161
+ // diff 行底色:深绿底贴对应色族暗 tint,绿暗化偏深绿、红暗化偏暗红
162
+ addBg: '\x1B[48;2;30;55;40m',
163
+ delBg: '\x1B[48;2;60;40;40m',
164
+ },
165
+ amber: {
166
+ // 琥珀金黄:深棕底 + accent 琥珀金黄作主强调;yellow 槽与 accent 同源(主题色),
167
+ // 跟 orange 区分(orange 南瓜橙偏橙红,amber 琥珀偏金黄);蓝/紫/青保留冷色锚点。
168
+ red: '\x1B[38;2;230;120;90m', // 暖橙红(警示色)
169
+ green: '\x1B[38;2;180;180;80m', // 黄绿
170
+ yellow: '\x1B[38;2;255;200;80m', // 主琥珀金黄(主题色)
171
+ blue: '\x1B[38;2;120;160;200m', // 冷蓝
172
+ magenta: '\x1B[38;2;220;140;180m', // 暖粉
173
+ cyan: '\x1B[38;2;130;180;170m', // 雾青绿
174
+ gray: '\x1B[38;2;150;130;100m', // 暖灰
175
+ brightCyan: '\x1B[38;2;170;210;200m',
176
+ brightMagenta: '\x1B[38;2;240;180;210m',
177
+ accent: '\x1B[38;2;255;200;80m', // 琥珀金黄(主题主色)
178
+ userBg: '\x1B[48;2;43;31;14m', // 深棕底(比 orange 略浅,避免撞色)
179
+ // diff 行底色:深棕底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
180
+ addBg: '\x1B[48;2;50;55;25m',
181
+ delBg: '\x1B[48;2;70;40;28m',
182
+ },
183
+ lavender: {
184
+ // 薰衣草淡紫:深紫底 + accent 淡紫作主强调;magenta 槽与 accent 同源(主题色),
185
+ // blue 偏冷蓝锚点(略带紫调);red/yellow 保留暖色警示色。
186
+ red: '\x1B[38;2;230;130;150m', // 暖粉红(警示色)
187
+ green: '\x1B[38;2;150;200;140m', // 黄绿
188
+ yellow: '\x1B[38;2;230;200;140m', // 暖杏
189
+ blue: '\x1B[38;2;140;160;230m', // 冷蓝锚点(略带紫)
190
+ magenta: '\x1B[38;2;180;150;230m', // 主薰衣草紫(主题色)
191
+ cyan: '\x1B[38;2;140;210;210m', // 雾青
192
+ gray: '\x1B[38;2;140;130;160m', // 冷灰紫
193
+ brightCyan: '\x1B[38;2;180;230;230m',
194
+ brightMagenta: '\x1B[38;2;210;180;250m',
195
+ accent: '\x1B[38;2;180;150;230m', // 薰衣草紫(主题主色)
196
+ userBg: '\x1B[48;2;31;26;46m', // 深紫底(比 rose 更冷)
197
+ // diff 行底色:深紫底贴对应色族暗 tint,绿暗化偏冷绿、红暗化偏冷紫红
198
+ addBg: '\x1B[48;2;38;46;42m',
199
+ delBg: '\x1B[48;2;60;40;55m',
200
+ },
201
+ sunset: {
202
+ // 日落珊瑚:深棕红底 + accent 珊瑚红作主强调;red 槽与 accent 同源(主题色);
203
+ // 跟 orange 区分(orange 南瓜橙偏橙黄,sunset 珊瑚偏红);蓝/青保留冷色锚点。
204
+ red: '\x1B[38;2;255;120;100m', // 主珊瑚红(主题色)
205
+ green: '\x1B[38;2;180;200;110m', // 黄绿
206
+ yellow: '\x1B[38;2;255;170;80m', // 暖橙黄
207
+ blue: '\x1B[38;2;110;170;220m', // 冷蓝锚点
208
+ magenta: '\x1B[38;2;230;120;180m', // 玫红
209
+ cyan: '\x1B[38;2;120;200;190m', // 雾青
210
+ gray: '\x1B[38;2;160;120;110m', // 暖灰
211
+ brightCyan: '\x1B[38;2;170;225;215m',
212
+ brightMagenta: '\x1B[38;2;250;170;210m',
213
+ accent: '\x1B[38;2;255;120;100m', // 珊瑚红(主题主色)
214
+ userBg: '\x1B[48;2;42;22;20m', // 深棕红底
215
+ // diff 行底色:深棕红底贴对应色族暗 tint,绿暗化偏橄榄、红暗化偏暗棕红
216
+ addBg: '\x1B[48;2;50;55;30m',
217
+ delBg: '\x1B[48;2;75;32;30m',
218
+ },
105
219
  };
106
220
  let currentName = 'default';
107
221
  let version = 0;
@@ -168,6 +282,9 @@ export const ui = {
168
282
  get brightMagenta() {
169
283
  return color('brightMagenta');
170
284
  },
285
+ get accent() {
286
+ return color('accent');
287
+ },
171
288
  get userBg() {
172
289
  return color('userBg');
173
290
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.4.10",
3
+ "version": "0.5.0",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,9 @@
11
11
  "bin",
12
12
  "README.md"
13
13
  ],
14
+ "workspaces": [
15
+ "packages/*"
16
+ ],
14
17
  "engines": {
15
18
  "node": ">=18"
16
19
  },
@@ -27,9 +30,7 @@
27
30
  "openai": "^4.0.0",
28
31
  "ws": "8.21.0"
29
32
  },
30
- "optionalDependencies": {
31
- "mocode-pet-app": "0.1.0"
32
- },
33
+ "optionalDependencies": {},
33
34
  "devDependencies": {
34
35
  "@types/node": "^22.0.0",
35
36
  "@types/ws": "8.5.13",