mocode-ai 0.4.9 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -0
- package/dist/agent/core.js +23 -2
- package/dist/agent/index.js +1 -1
- package/dist/config/index.js +17 -7
- package/dist/llm/index.js +22 -1
- package/dist/project-skill/index.js +81 -30
- package/dist/project-skill/initializer.js +45 -46
- package/dist/project-snapshot/index.js +39 -131
- package/dist/project-snapshot/llm-snapshot.js +127 -0
- package/dist/repl/index.js +163 -46
- package/dist/session/compact.js +3 -3
- package/dist/tools/builtins/project-skill-update.js +38 -7
- package/dist/tools/builtins/read-file.js +1 -17
- package/dist/tools/builtins/todolist.js +124 -8
- package/dist/ui/batch.js +23 -6
- package/dist/ui/content.js +96 -0
- package/dist/ui/diff.js +1 -1
- package/dist/ui/intervention.js +32 -4
- package/dist/ui/layout.js +166 -41
- package/dist/ui/prompt.js +11 -11
- package/dist/ui/render.js +44 -18
- package/dist/ui/spinner.js +1 -1
- package/dist/ui/theme.js +117 -0
- package/package.json +5 -4
package/dist/ui/layout.js
CHANGED
|
@@ -34,6 +34,8 @@ let runningFrame = -1;
|
|
|
34
34
|
let userActiveUntil = 0; // 打字活跃截止时刻(Date.now()+PAUSE);0=未活跃
|
|
35
35
|
let flushTimer = null; // 用户停手后 flush 缓冲内容(repaintViewport)
|
|
36
36
|
const USER_ACTIVE_PAUSE_MS = 1500;
|
|
37
|
+
/** Sticky banner 的 ❯ 前缀(同 repl 的 PROMPT;常量统一视觉)。 */
|
|
38
|
+
const BANNER_PROMPT = '❯ ';
|
|
37
39
|
let lastView = null;
|
|
38
40
|
let lastMenuStartRow = 0; // 上次菜单起始屏行(供擦除)
|
|
39
41
|
let lastMenuRows = 0;
|
|
@@ -320,6 +322,67 @@ function commitMd() {
|
|
|
320
322
|
mdBuf = '';
|
|
321
323
|
content.commitSegment();
|
|
322
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
|
+
}
|
|
323
386
|
/**
|
|
324
387
|
* markdown 正文写(替代 contentWrite 用于 agent onText):累积 chunk 到 mdBuf,每 chunk 把整段
|
|
325
388
|
* mdBuf 经 renderMarkdown 渲成自洽 ANSI 行,replace 缓冲段(content.setLines 截旧 + 写新),
|
|
@@ -456,6 +519,19 @@ export function contentInsertAfter(after, lines) {
|
|
|
456
519
|
if (scrolled) {
|
|
457
520
|
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, Math.max(0, content.totalRows() - g.contentBottom)));
|
|
458
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
|
+
}
|
|
459
535
|
// batch 摘要行索引平移
|
|
460
536
|
void import('./batch.js').then((m) => m.shiftBatchesAfter(after, delta));
|
|
461
537
|
repaintViewport();
|
|
@@ -572,6 +648,12 @@ function highlightRange(line, colStart, colEnd) {
|
|
|
572
648
|
* 重画内容区 viewport:按 scrollOffset 取缓冲尾窗,逐行 cup+clearline+rowtext 映射到屏 1..contentBottom。
|
|
573
649
|
* offset=0 即尾窗(== 实时屏,resize / 回尾时用)。清行含 contentBottom——顺带擦 WT 边距漏影(状态行重复)。
|
|
574
650
|
* 有活跃选区(鼠标拖拽中)时,对选中范围套反白——纯视觉,不影响缓冲内容。
|
|
651
|
+
*
|
|
652
|
+
* **滚动回看 sticky banner(输入框上方固定标题的姊妹需求)**:
|
|
653
|
+
* 仅在 scrollOffset > 0 时,在 viewport 第 1 行顶部叠一行横幅,显示「当前 viewport 顶上
|
|
654
|
+
* 那条用户消息」的预览。用户上滑翻历史时,横幅内容随滚动到的 user→agent 对应关系变化;
|
|
655
|
+
* 滑到底(offset===0)自动消失(实时屏可见,无需 banner)。满宽 padding + 底色对比反色
|
|
656
|
+
* 提示「↑ 这是上方滚走的内容」,不与内容区行内 SGR 冲突。
|
|
575
657
|
*/
|
|
576
658
|
export function repaintViewport() {
|
|
577
659
|
if (!active)
|
|
@@ -580,11 +662,27 @@ export function repaintViewport() {
|
|
|
580
662
|
const h = g.contentBottom;
|
|
581
663
|
const slice = content.sliceFromEnd(scrollOffset, h);
|
|
582
664
|
const sel = normalizeSelection();
|
|
583
|
-
|
|
665
|
+
// viewportAbsStart() 无选区时也用:sticky banner 必须按真实窗口头算,不能降级 0(否则 banner 永不出现)。
|
|
666
|
+
const absStart = viewportAbsStart();
|
|
667
|
+
// sticky banner:仅 scrollOffset>0 时显示;offset=0 即实时屏,user 气泡本来就在视口内,无需 banner。
|
|
668
|
+
const bannerText = scrollOffset > 0
|
|
669
|
+
? content.lastUserMessageBefore(absStart)
|
|
670
|
+
: null;
|
|
671
|
+
const BANNER_ROW = 1; // 横幅占 viewport 第 1 行(会把原第 1 行内容遮住 —— 1 行换"我在看啥"的可读性,可接受)
|
|
584
672
|
let p = '';
|
|
585
673
|
for (let r = 1; r <= h; r++) {
|
|
586
674
|
let line = slice[r - 1] ?? '';
|
|
587
|
-
if (
|
|
675
|
+
if (bannerText && r === BANNER_ROW) {
|
|
676
|
+
// banner 行:满宽 userBg + ❯ + 单行截断(多行 ⏎ 折叠)+ userBg 补到底 + 底线分隔短横
|
|
677
|
+
const cols = g.cols;
|
|
678
|
+
const oneLine = bannerText.replace(/\s*\n\s*/g, ' ⏎ ');
|
|
679
|
+
const promptW = displayWidth(BANNER_PROMPT);
|
|
680
|
+
const avail = Math.max(1, cols - promptW);
|
|
681
|
+
const truncated = truncateDisplayHead(oneLine, avail);
|
|
682
|
+
const padCount = Math.max(0, cols - promptW - displayWidth(truncated));
|
|
683
|
+
line = `${ui.userBg}${BANNER_PROMPT}${truncated}${' '.repeat(padCount)}${ui.reset}`;
|
|
684
|
+
}
|
|
685
|
+
else if (sel) {
|
|
588
686
|
const absLine = absStart + r - 1;
|
|
589
687
|
if (absLine >= sel.startLine && absLine <= sel.endLine) {
|
|
590
688
|
const lineW = ansiDisplayWidth(line);
|
|
@@ -912,6 +1010,24 @@ function handleMouseEvent(e) {
|
|
|
912
1010
|
scrollBy(e.dir * WHEEL_LINES);
|
|
913
1011
|
return;
|
|
914
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
|
+
}
|
|
915
1031
|
if (!mouseEnabled)
|
|
916
1032
|
return;
|
|
917
1033
|
const g = getGeo();
|
|
@@ -947,26 +1063,8 @@ function handleMouseEvent(e) {
|
|
|
947
1063
|
if (e.button !== 0)
|
|
948
1064
|
return; // 其余中/右键 press/drag 不处理(终端原生右键菜单等不受影响)
|
|
949
1065
|
if (e.type === 'press') {
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
// 不再 return——后续 drag/release 分支也要知道选区在输入框。点击未拖动时 release 端会清掉选区,
|
|
953
|
-
// 与"点一下就定位光标、起手打字"的习惯一致。
|
|
954
|
-
setInputCursorFromClick(e.row, e.col);
|
|
955
|
-
const pos = inputScreenToInputPos(e.row, e.col);
|
|
956
|
-
if (pos) {
|
|
957
|
-
// 清掉内容区旧选区(若有):按一下输入框不应同时保留内容区反白,视觉混淆。
|
|
958
|
-
if (selection) {
|
|
959
|
-
selection = null;
|
|
960
|
-
}
|
|
961
|
-
inputSelection = {
|
|
962
|
-
anchor: { line: pos.line, col: pos.displayCol },
|
|
963
|
-
end: { line: pos.line, col: pos.displayCol },
|
|
964
|
-
dragged: false,
|
|
965
|
-
};
|
|
966
|
-
paintInput(lastView);
|
|
967
|
-
}
|
|
968
|
-
return;
|
|
969
|
-
}
|
|
1066
|
+
// 输入行左键已在上方 mouseEnabled 检查之前处理(支持 intervention 面板期间点击定位光标)。
|
|
1067
|
+
// 此处只处理内容区选区。
|
|
970
1068
|
selecting = true;
|
|
971
1069
|
const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
|
|
972
1070
|
const absLine = screenRowToAbsLine(rowInContent);
|
|
@@ -1105,26 +1203,26 @@ function composeSpinnerLine(status, cols) {
|
|
|
1105
1203
|
// 滚动回看:左段 = 符号(◆ 或 心跳帧) + 状态名(灰,无走时);右段 = 历史指示。
|
|
1106
1204
|
// 跟非回看的 INPUT/RUNNING 态保持一致——避免「◆ 留下、状态字蒸发」的视觉错觉。
|
|
1107
1205
|
const symbol = spinning
|
|
1108
|
-
? `${ui.
|
|
1109
|
-
: `${ui.
|
|
1206
|
+
? `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset}`
|
|
1207
|
+
: `${ui.accent}◆${ui.reset}`;
|
|
1110
1208
|
lead = `${symbol} ${ui.dim}${status.status}${ui.reset}`;
|
|
1111
1209
|
leadW = 1 + 1 + displayWidth(status.status);
|
|
1112
1210
|
}
|
|
1113
1211
|
else if (hasSpinner) {
|
|
1114
1212
|
// spinner 激活(思考中/执行工具…):帧 + 状态 + 走时
|
|
1115
1213
|
const ePart = elapsed ? ` ${ui.dim}${elapsed}${ui.reset}` : '';
|
|
1116
|
-
lead = `${ui.
|
|
1214
|
+
lead = `${ui.bold}${ui.accent}${status.spinnerFrame}${ui.reset} ${ui.dim}${status.status}${ui.reset}${ePart}`;
|
|
1117
1215
|
leadW = 1 + 1 + displayWidth(status.status) + (elapsed ? 1 + displayWidth(elapsed) : 0);
|
|
1118
1216
|
}
|
|
1119
1217
|
else if (spinning) {
|
|
1120
1218
|
// 运行态心跳帧(流式输出中):帧 + 走时
|
|
1121
1219
|
const ePart = elapsed ? ` ${ui.dim}${elapsed}${ui.reset}` : '';
|
|
1122
|
-
lead = `${ui.
|
|
1220
|
+
lead = `${ui.bold}${ui.accent}${RUNNING_FRAMES[runningFrame]}${ui.reset}${ePart}`;
|
|
1123
1221
|
leadW = 1 + (elapsed ? 1 + displayWidth(elapsed) : 0);
|
|
1124
1222
|
}
|
|
1125
1223
|
else {
|
|
1126
1224
|
// INPUT 态:◆ + 状态文字(无走时)
|
|
1127
|
-
lead = `${ui.
|
|
1225
|
+
lead = `${ui.accent}◆${ui.reset} ${ui.dim}${status.status}${ui.reset}`;
|
|
1128
1226
|
leadW = 1 + 1 + displayWidth(status.status);
|
|
1129
1227
|
}
|
|
1130
1228
|
// ── 右段:仅滚动回看时显历史指示 ──
|
|
@@ -1143,28 +1241,50 @@ function composeSpinnerLine(status, cols) {
|
|
|
1143
1241
|
function composeModelLine(status, cols) {
|
|
1144
1242
|
const ctx = status.contextBar; // 已带色
|
|
1145
1243
|
const ctxW = ansiDisplayWidth(ctx);
|
|
1146
|
-
// 左段:模式标识 + 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1244
|
+
// 左段:模式标识 + 切换提示(灰)+ 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
1147
1245
|
const modeTag = status.modeTag ?? '';
|
|
1148
|
-
const modeColor = modeTag === 'plan' ? ui.yellow : ui.
|
|
1246
|
+
const modeColor = modeTag === 'plan' ? ui.yellow : ui.accent;
|
|
1149
1247
|
const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
|
|
1150
1248
|
const modeW = modeTag ? displayWidth(modeTag) : 0;
|
|
1249
|
+
// 切换提示:告诉用户怎么切模式。灰(dim)降优先级,不与 modeTag 抢色;只在有 modeTag 时出现。
|
|
1250
|
+
const HINT = 'Shift+Tab 切换模式';
|
|
1251
|
+
const hintW = modeTag ? displayWidth(HINT) : 0;
|
|
1252
|
+
const hintPart = modeTag ? `${ui.dim}${HINT}${ui.reset}` : '';
|
|
1151
1253
|
const tokChip = formatTurnTokenChip(status.lastTurnUsage);
|
|
1152
1254
|
const tokW = displayWidth(stripAnsi(tokChip));
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
const
|
|
1156
|
-
const
|
|
1255
|
+
// 合并左段:段间留 2 空格分隔。极窄时 hint 与 chip 都可能藏掉。
|
|
1256
|
+
// 优先级:modeTag(必) > chip(提示累计 token,有信息量)> hint(纯说明性,窄时最先省)。
|
|
1257
|
+
const sepMH = modePart && (hintPart || tokChip) ? ' ' : '';
|
|
1258
|
+
const sepHT = (hintPart && tokChip) ? ' ' : '';
|
|
1259
|
+
const leftStr = `${modePart}${sepMH}${hintPart}${sepHT}${tokChip}`;
|
|
1260
|
+
const leftW = modeW
|
|
1261
|
+
+ (modePart && (hintPart || tokChip) ? sepMH.length : 0)
|
|
1262
|
+
+ hintW
|
|
1263
|
+
+ ((hintPart && tokChip) ? sepHT.length : 0)
|
|
1264
|
+
+ tokW;
|
|
1157
1265
|
// 右段:ctx + sep + cwd,右端对齐。cwd 按预算截断,极窄(<6)隐藏。
|
|
1158
|
-
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd
|
|
1266
|
+
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd、再按 hint→chip 顺序省。
|
|
1159
1267
|
const minGap = 2;
|
|
1160
1268
|
let cwdBudget = cols - leftW - minGap - ctxW - STATUS_SEP_W - 1;
|
|
1161
1269
|
let cwd = cwdBudget >= 6 ? truncateDisplay(status.cwd, cwdBudget) : '';
|
|
1162
1270
|
let cwdW = displayWidth(cwd);
|
|
1163
1271
|
let rightStr = `${ctx}${STATUS_SEP}${ui.dim}${cwd}${ui.reset}`;
|
|
1164
1272
|
let rightW = ctxW + STATUS_SEP_W + cwdW;
|
|
1165
|
-
//
|
|
1166
|
-
|
|
1167
|
-
|
|
1273
|
+
// 极窄:逐步降级——先藏 hint,再藏 token chip,只剩 modeTag 与右段挤。
|
|
1274
|
+
// 这样 80 列宽终端下 hint 和 chip 都能稳住,只 <50 列才退化到只剩 modeTag。
|
|
1275
|
+
if (leftW + minGap + rightW > cols) {
|
|
1276
|
+
// 优先藏 hint(纯说明,信息密度最低):仅留 modePart + sep + tokChip
|
|
1277
|
+
if (modePart && tokChip && hintPart) {
|
|
1278
|
+
const leftStr2 = `${modePart}${sepMH}${tokChip}`;
|
|
1279
|
+
const leftW2 = modeW + sepMH.length + tokW;
|
|
1280
|
+
if (leftW2 + minGap + rightW <= cols) {
|
|
1281
|
+
return twoColumn(leftStr2, leftW2, rightStr, rightW, cols);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
// 再藏 token chip:仅留 modePart
|
|
1285
|
+
if (modePart && tokChip) {
|
|
1286
|
+
return twoColumn(modePart, modeW, rightStr, rightW, cols);
|
|
1287
|
+
}
|
|
1168
1288
|
}
|
|
1169
1289
|
return twoColumn(leftStr, leftW, rightStr, rightW, cols);
|
|
1170
1290
|
}
|
|
@@ -1247,13 +1367,18 @@ function startTurnTimer() {
|
|
|
1247
1367
|
}, 200);
|
|
1248
1368
|
turnTimer.unref();
|
|
1249
1369
|
}
|
|
1250
|
-
/** 停走时计时器。enterInputMode / exitAltScreen
|
|
1251
|
-
function stopTurnTimer() {
|
|
1370
|
+
/** 停走时计时器。enterInputMode / exitAltScreen 调;intervention 面板进入时也调(防 drawStatusBar 200ms 心跳把光标拉到 runningCaretPos,覆盖 paintInput 的正确光标位)。 */
|
|
1371
|
+
export function stopTurnTimer() {
|
|
1252
1372
|
if (turnTimer) {
|
|
1253
1373
|
clearInterval(turnTimer);
|
|
1254
1374
|
turnTimer = null;
|
|
1255
1375
|
}
|
|
1256
1376
|
}
|
|
1377
|
+
/** 恢复走时计时器(若 RUNNING 态)。intervention 面板退出时调,恢复状态行走时心跳。 */
|
|
1378
|
+
export function startTurnTimerIfRunning() {
|
|
1379
|
+
if (active && mode === 'running')
|
|
1380
|
+
startTurnTimer();
|
|
1381
|
+
}
|
|
1257
1382
|
/**
|
|
1258
1383
|
* 在续写位画一行瞬时活动文本(spinner 帧):不进缓冲、不推进续写位,逐行 clearLine 重画。
|
|
1259
1384
|
* 仅 TTY + offset=0(实时尾)+ 非打字暂停态时物理写屏;滚动态跳过(由状态行 spinner 兜底,且避免覆盖 viewport 历史行)。
|
|
@@ -1494,7 +1619,7 @@ export function paintInput(view) {
|
|
|
1494
1619
|
buf += cup(spinnerRow, 1) + esc.clearLine + composeSpinnerLine(status, g.cols);
|
|
1495
1620
|
buf += cup(modelRow, 1) + esc.clearLine + composeModelLine(status, g.cols);
|
|
1496
1621
|
// 3b. 上线(输入框顶):满屏宽细线 ─(cyan),框住输入区上边界
|
|
1497
|
-
buf += cup(g.contentBottom + 3, 1) + esc.clearLine + ui.
|
|
1622
|
+
buf += cup(g.contentBottom + 3, 1) + esc.clearLine + ui.accent + '─'.repeat(g.cols) + ui.reset;
|
|
1498
1623
|
// 4. 输入行(g.contentBottom+4 .. rows-1)——按可视行画,首行带 prompt、其余缩进 promptW
|
|
1499
1624
|
const firstInputRow = g.contentBottom + 4;
|
|
1500
1625
|
const inputRowsAvail = g.footerH - 5; // 去掉虚拟空/spinner行/上线/下线/model行,留输入行
|
|
@@ -1552,7 +1677,7 @@ export function paintInput(view) {
|
|
|
1552
1677
|
buf += cup(r, 1) + esc.clearLine + text;
|
|
1553
1678
|
}
|
|
1554
1679
|
// 4b. 下线(输入框底):满屏宽细线 ─(cyan),在 model 行上一行(model 行占屏底 rows)
|
|
1555
|
-
buf += cup(g.rows - 1, 1) + esc.clearLine + ui.
|
|
1680
|
+
buf += cup(g.rows - 1, 1) + esc.clearLine + ui.accent + '─'.repeat(g.cols) + ui.reset;
|
|
1556
1681
|
// 5. 向上菜单(画在内容区底,底栏正上方)
|
|
1557
1682
|
if (view.menu && view.menu.lines.length > 0) {
|
|
1558
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 =
|
|
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.
|
|
112
|
-
const marker = isSel ? `${ui.
|
|
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.
|
|
703
|
-
const marker = isSel ? `${ui.
|
|
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.
|
|
830
|
-
const marker = isSel ? `${ui.
|
|
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.
|
|
969
|
-
const marker = isSel ? `${ui.
|
|
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.
|
|
1093
|
-
const marker = isSel ? `${ui.
|
|
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
|
-
// ──
|
|
235
|
-
const LOGO_W =
|
|
236
|
-
const LOGO_GAP =
|
|
237
|
-
|
|
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
|
-
'
|
|
239
|
+
'█▀█▀█ █▀▀▀█ █ ',
|
|
240
|
+
'█ █ █ █▀▀█ █ █▀▀█ █▀▀█ █▀▀█',
|
|
241
|
+
'█ █ █ █ █ █ ▄ █ █ █ █ █▀▀▀',
|
|
242
|
+
'▀ ▀ ▀ ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀',
|
|
243
243
|
];
|
|
244
|
-
/** 取第 idx 行 logo(着色 + 补宽)
|
|
244
|
+
/** 取第 idx 行 logo(着色 + 补宽 + LOGO_GAP 间隔),与 info 行直接拼接(neofetch 风)。 */
|
|
245
245
|
function logoLine(idx) {
|
|
246
|
-
|
|
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.
|
|
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
|
|
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
|
-
/**
|
|
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 {
|
package/dist/ui/spinner.js
CHANGED
|
@@ -53,7 +53,7 @@ export class Spinner {
|
|
|
53
53
|
}
|
|
54
54
|
render() {
|
|
55
55
|
const f = FRAMES[this.frame];
|
|
56
|
-
stdout.write(`\r${ui.
|
|
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
|
},
|