mocode-ai 0.4.1 → 0.4.3
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/dist/agent/core.js +64 -10
- package/dist/agent/index.js +40 -2
- package/dist/agent/spawn.js +18 -9
- package/dist/config/index.js +116 -29
- package/dist/llm/index.js +71 -3
- package/dist/memory/index.js +8 -1
- package/dist/memory/store.js +51 -9
- package/dist/repl/index.js +191 -11
- package/dist/tools/builtins/ask-human.js +40 -3
- package/dist/tools/builtins/index.js +20 -5
- package/dist/tools/constants.js +16 -0
- package/dist/ui/content.js +27 -0
- package/dist/ui/layout.js +77 -18
- package/dist/ui/prompt.js +15 -5
- package/package.json +1 -1
package/dist/ui/layout.js
CHANGED
|
@@ -396,6 +396,30 @@ export function clearContent() {
|
|
|
396
396
|
content.reset();
|
|
397
397
|
stdout.write(esc.home);
|
|
398
398
|
}
|
|
399
|
+
/**
|
|
400
|
+
* 撤销(rewind)内容区末尾 n 物理行 —— 供 "刚 echoInput 写下用户气泡、用户撤回"
|
|
401
|
+
* 把气泡从可视区与 buffer 一起拿掉。content.rewind 弹出 buffer 末 n 行;layout 这边
|
|
402
|
+
* 同步上溯 contentRow(contentTop 钳位,防下溢)、scrollOffset 钳到新 totalRows、
|
|
403
|
+
* 最后 repaintViewport 重画可视区(running 态回 runningCaretPos,input 态回续写位)。
|
|
404
|
+
*
|
|
405
|
+
* 与 clearContent 不同:只擦最近一段、保留更早内容与 buffer;光标归内容区底,
|
|
406
|
+
* 不是 (1,1)。常发生在 RUNNING 态的 pending 窗口内 recall —— recall 后调用方
|
|
407
|
+
* 通常再 enterInputMode 切回 INPUT 视觉,光标由后续 paintInput 重画。
|
|
408
|
+
*/
|
|
409
|
+
export function rewindContent(rowsToRewind) {
|
|
410
|
+
if (!active || rowsToRewind <= 0)
|
|
411
|
+
return;
|
|
412
|
+
content.rewind(rowsToRewind);
|
|
413
|
+
const g = getGeo();
|
|
414
|
+
contentRow = Math.max(g.contentTop, contentRow - rowsToRewind);
|
|
415
|
+
contentCol = 1;
|
|
416
|
+
// 钳 scrollOffset:不能让 viewport 视点超出新 totalRows
|
|
417
|
+
const maxOff = Math.max(0, content.totalRows() - g.contentBottom);
|
|
418
|
+
scrollOffset = Math.min(scrollOffset, maxOff);
|
|
419
|
+
// 末段 frameRow/frameCol 是 spinner 的画位,撤回若跨过 frame 行也不必清——
|
|
420
|
+
// repaintViewport 会按新 buffer 重画整片,旧 frame 自然被覆盖。
|
|
421
|
+
repaintViewport();
|
|
422
|
+
}
|
|
399
423
|
// ── viewport 滚动回看(Phase 2)──
|
|
400
424
|
/** 是否处于滚动回看态(offset>0,内容区显历史)。prompt 据此在非滚动键时回尾。 */
|
|
401
425
|
export function isScrolled() {
|
|
@@ -423,7 +447,16 @@ function normalizeSelection() {
|
|
|
423
447
|
? { startLine: a.line, startCol: a.col, endLine: b.line, endCol: b.col }
|
|
424
448
|
: { startLine: b.line, startCol: b.col, endLine: a.line, endCol: a.col };
|
|
425
449
|
}
|
|
426
|
-
/** 给自洽带色行的显示列区间 [colStart,colEnd)
|
|
450
|
+
/** 给自洽带色行的显示列区间 [colStart,colEnd) 套「统一亮黄底 + 黑字」。
|
|
451
|
+
* 强制重设前/背景,无视原 SGR:md 字符常带 ui.dim / ui.cyan / ui.gray 等,
|
|
452
|
+
* 仅靠 SGR 7 反转对比度极弱;改用显式「亮黄底 SGR 103 + 黑前景 SGR 30」,
|
|
453
|
+
* 跨终端一致。退反白用 0 清 SGR,行末 active SGR 自然续接。
|
|
454
|
+
* 关键:反白 active 期间**吃掉所有行内 SGR**(不让前景色干扰)——否则
|
|
455
|
+
* 行内首个 \x1B[2m(dim)/\x1B[36m(cyan)/\x1B[1m(bold) 进选区后仍生效,
|
|
456
|
+
* 前景被压回原色,整片亮黄底被切割、看着「花」;直穿则带 dim 等,对比不足。
|
|
457
|
+
* 反白外 SGR 直穿,保留原色。 */
|
|
458
|
+
const SEL_OPEN = '\x1B[30;103m'; // 30:黑前景 | 103:亮黄背景
|
|
459
|
+
const SEL_OFF = '\x1B[0m'; // 全清 SGR,行末 active 状态续接
|
|
427
460
|
function highlightRange(line, colStart, colEnd) {
|
|
428
461
|
if (colEnd <= colStart)
|
|
429
462
|
return line;
|
|
@@ -433,17 +466,20 @@ function highlightRange(line, colStart, colEnd) {
|
|
|
433
466
|
let opened = false;
|
|
434
467
|
for (let i = 0; i < parts.length; i++) {
|
|
435
468
|
if (i % 2 === 1) {
|
|
436
|
-
|
|
469
|
+
// SGR 段:反白内直接吃,不让行内前景色进选区;反白外直穿,保留原色。
|
|
470
|
+
if (opened)
|
|
471
|
+
continue;
|
|
472
|
+
out += parts[i];
|
|
437
473
|
continue;
|
|
438
474
|
}
|
|
439
475
|
for (const ch of parts[i]) {
|
|
440
476
|
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
441
477
|
if (!opened && w >= colStart && w < colEnd) {
|
|
442
|
-
out +=
|
|
478
|
+
out += SEL_OPEN;
|
|
443
479
|
opened = true;
|
|
444
480
|
}
|
|
445
481
|
if (opened && w >= colEnd) {
|
|
446
|
-
out +=
|
|
482
|
+
out += SEL_OFF;
|
|
447
483
|
opened = false;
|
|
448
484
|
}
|
|
449
485
|
out += ch;
|
|
@@ -451,7 +487,7 @@ function highlightRange(line, colStart, colEnd) {
|
|
|
451
487
|
}
|
|
452
488
|
}
|
|
453
489
|
if (opened)
|
|
454
|
-
out +=
|
|
490
|
+
out += SEL_OFF;
|
|
455
491
|
return out;
|
|
456
492
|
}
|
|
457
493
|
/**
|
|
@@ -742,29 +778,52 @@ function composeSpinnerLine(status, cols) {
|
|
|
742
778
|
const rightStr = tail ? `${ui.yellow}${tail}${ui.reset}` : '';
|
|
743
779
|
return twoColumn(lead, leadW, rightStr, tailW, cols);
|
|
744
780
|
}
|
|
745
|
-
/** 下线之下那行(model 行):左 =
|
|
781
|
+
/** 下线之下那行(model 行):左 = 模式标识 + 本轮 token chip;右 = context + cwd,右端对齐。
|
|
746
782
|
* 活跃 plan chip 不再放这里,改放 spinner 行上方的「虚拟空行」(contentBottom+1,见 drawStatusBar),
|
|
747
783
|
* 既不挤 model 行,又给输入区上方留出可视分隔带。 */
|
|
748
784
|
function composeModelLine(status, cols) {
|
|
749
785
|
const ctx = status.contextBar; // 已带色
|
|
750
786
|
const ctxW = ansiDisplayWidth(ctx);
|
|
751
|
-
//
|
|
787
|
+
// 左段:模式标识 + 本轮 token chip。token chip 仅展示总量,用 mid 灰,不抢主色。
|
|
752
788
|
const modeTag = status.modeTag ?? '';
|
|
753
789
|
const modeColor = modeTag === 'plan' ? ui.yellow : ui.brightCyan;
|
|
754
|
-
const modePart = modeTag
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
const
|
|
758
|
-
|
|
790
|
+
const modePart = modeTag ? `${modeColor}${modeTag}${ui.reset}` : '';
|
|
791
|
+
const modeW = modeTag ? displayWidth(modeTag) : 0;
|
|
792
|
+
const tokChip = formatTurnTokenChip(status.lastTurnUsage);
|
|
793
|
+
const tokW = displayWidth(stripAnsi(tokChip));
|
|
794
|
+
// 合并左段:chip 前留 2 空格分隔,无 modeTag 也允许仅显示 chip(兜底边角)。
|
|
795
|
+
const sep = modePart && tokChip ? ' ' : '';
|
|
796
|
+
const leftStr = `${modePart}${sep}${tokChip}`;
|
|
797
|
+
const leftW = modeW + (modePart && tokChip ? sep.length : 0) + tokW;
|
|
759
798
|
// 右段:ctx + sep + cwd,右端对齐。cwd 按预算截断,极窄(<6)隐藏。
|
|
799
|
+
// 任一 chip 极宽时收紧 cwd(toolbar 列挤压场景),先从 cwd 砍、再隐藏 cwd、再隐藏 token chip。
|
|
760
800
|
const minGap = 2;
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
801
|
+
let cwdBudget = cols - leftW - minGap - ctxW - STATUS_SEP_W - 1;
|
|
802
|
+
let cwd = cwdBudget >= 6 ? truncateDisplay(status.cwd, cwdBudget) : '';
|
|
803
|
+
let cwdW = displayWidth(cwd);
|
|
804
|
+
let rightStr = `${ctx}${STATUS_SEP}${ui.dim}${cwd}${ui.reset}`;
|
|
805
|
+
let rightW = ctxW + STATUS_SEP_W + cwdW;
|
|
806
|
+
// 极窄(<24 列含 ctx):藏 token chip
|
|
807
|
+
if (modePart && tokChip && rightW + minGap + leftW > cols) {
|
|
808
|
+
return twoColumn(modePart, modeW, rightStr, rightW, cols);
|
|
809
|
+
}
|
|
766
810
|
return twoColumn(leftStr, leftW, rightStr, rightW, cols);
|
|
767
811
|
}
|
|
812
|
+
/** 把本轮 token 总量格式化成 chip 文本(纯字符串,带 ANSI 色)。无 usage 返空串。
|
|
813
|
+
* 显示策略:chip 信息密度有限,只显示总量 + 一个 ↻ 标记表示有 cache 命中。
|
|
814
|
+
* 详细分项(↑↓ 计费/↻ 缓存/reasoning)在 turn 末 summary 行展示,不在此处展开。 */
|
|
815
|
+
function formatTurnTokenChip(usage) {
|
|
816
|
+
if (!usage || !usage.totalTokens)
|
|
817
|
+
return '';
|
|
818
|
+
const n = usage.totalTokens;
|
|
819
|
+
const text = n < 1000 ? `${n}` : `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k`;
|
|
820
|
+
const cached = usage.cachedTokens ?? 0;
|
|
821
|
+
const cacheTag = cached > 0
|
|
822
|
+
? ` ↻${cached < 1000 ? cached : `${(cached / 1000).toFixed(cached >= 10000 ? 0 : 1)}k`}`
|
|
823
|
+
: '';
|
|
824
|
+
// chip 用 mid 灰(降优先级)— 模式仍是主色
|
|
825
|
+
return `${ui.dim}${text} tokens${cacheTag}${ui.reset}`;
|
|
826
|
+
}
|
|
768
827
|
/** spinner 行上方的「虚拟空行」(contentBottom+1)。
|
|
769
828
|
* - 有活跃 plan:显「plan: <summary> ▸ N. step」整行左对齐(yellow + dim)
|
|
770
829
|
* - 无活跃 plan:空(保留原分隔视觉,避免内容贴输入区)
|
|
@@ -929,7 +988,7 @@ export function clearLiveAtCursor() {
|
|
|
929
988
|
frameRow = 0;
|
|
930
989
|
frameCol = 0;
|
|
931
990
|
}
|
|
932
|
-
/** 更新状态行基线(模型 / context / cwd / 模式标识 / 活跃 plan chip)。repl 在轮次边界与切模式时调。 */
|
|
991
|
+
/** 更新状态行基线(模型 / context / cwd / 模式标识 / 活跃 plan chip / 本轮 token chip)。repl 在轮次边界与切模式时调。 */
|
|
933
992
|
export function setStatusBase(b) {
|
|
934
993
|
base = b;
|
|
935
994
|
}
|
package/dist/ui/prompt.js
CHANGED
|
@@ -128,9 +128,15 @@ export async function promptWithSlashMenu(opts) {
|
|
|
128
128
|
function cursorCol() {
|
|
129
129
|
return displayWidth(lines[cl].slice(0, cc));
|
|
130
130
|
}
|
|
131
|
-
/** chip
|
|
131
|
+
/** chip 预览前缀:行数 + 字符数(字符 <1K 直出,≥1K 缩为 X.XK)。比"前 20 字符截断"对 CJK 更友好
|
|
132
|
+
* ——后者在中文里 20 列只够 10 个汉字就截没,几乎看不到内容;元信息密度更高、长度可预测。 */
|
|
132
133
|
function chipPrefix() {
|
|
133
|
-
|
|
134
|
+
if (!chip)
|
|
135
|
+
return '';
|
|
136
|
+
const lines = chip.split('\n').length;
|
|
137
|
+
const chars = chip.length;
|
|
138
|
+
const charStr = chars < 1000 ? `${chars} 字符` : `${(chars / 1000).toFixed(1)}K 字符`;
|
|
139
|
+
return `[📋 ${lines} 行 · ${charStr}] `;
|
|
134
140
|
}
|
|
135
141
|
/** chipPre 按行拆分(粘贴发生前光标之前已有的文本,可能多行,原样保留在 chip 之前)。 */
|
|
136
142
|
function chipPreLines() {
|
|
@@ -178,7 +184,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
178
184
|
function applyPastedText(buf) {
|
|
179
185
|
if (!buf)
|
|
180
186
|
return;
|
|
181
|
-
const isLong = buf.split('\n').length > 8 || buf.length >
|
|
187
|
+
const isLong = buf.split('\n').length > 8 || buf.length > 200;
|
|
182
188
|
if (isLong) {
|
|
183
189
|
const before = lines[cl].slice(0, cc);
|
|
184
190
|
const after = lines[cl].slice(cc);
|
|
@@ -202,7 +208,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
202
208
|
computeFiltered();
|
|
203
209
|
redraw();
|
|
204
210
|
}
|
|
205
|
-
/** 粘贴结束:长粘贴(>8 行或 >
|
|
211
|
+
/** 粘贴结束:长粘贴(>8 行或 >200 字符)落/并进 chip(原子,整段封预览),短粘贴落为可编辑文本。 */
|
|
206
212
|
function finalizePaste() {
|
|
207
213
|
if (resolved) {
|
|
208
214
|
pasteParts = [];
|
|
@@ -411,9 +417,13 @@ export async function promptWithSlashMenu(opts) {
|
|
|
411
417
|
return;
|
|
412
418
|
}
|
|
413
419
|
const isReturn = key.name === 'return' || key.name === 'enter';
|
|
414
|
-
// 换行:Ctrl+J / Alt+Enter(meta)/ Shift+Enter(终端区分时)/ lone LF
|
|
420
|
+
// 换行:Ctrl+J / Ctrl+Enter / Alt+Enter(meta)/ Shift+Enter(终端区分时)/ lone LF
|
|
415
421
|
// (粘贴的 CR/LF 已在上方 pasting 分支累积进 pasteParts,不会到此)
|
|
422
|
+
// Ctrl+Enter:readline 解析得到 key.ctrl=true && isReturn 走这条路;少数老 xterm 把
|
|
423
|
+
// Ctrl+Enter 当裸 \r 发(没 ctrl flag)→ 落到下方「提交」分支做 Enter 处理,
|
|
424
|
+
// 此时用 Ctrl+J 兜底。
|
|
416
425
|
const wantNewline = (key.ctrl && key.name === 'j') ||
|
|
426
|
+
(key.ctrl && isReturn) ||
|
|
417
427
|
(key.meta && isReturn) ||
|
|
418
428
|
(key.shift && isReturn) ||
|
|
419
429
|
(key.sequence === '\n' && !key.ctrl);
|