mocode-ai 0.4.5 → 0.4.7
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 +219 -219
- package/dist/config/index.js +2 -1
- package/dist/config/presets.js +177 -0
- package/dist/llm/index.js +5 -1
- package/dist/memory/reflect.js +5 -2
- package/dist/repl/index.js +216 -7
- package/dist/tools/builtins/ask-human.js +70 -43
- package/dist/ui/batch.js +21 -4
- package/dist/ui/diff.js +41 -4
- package/dist/ui/intervention.js +59 -19
- package/dist/ui/layout.js +258 -59
- package/dist/ui/theme.js +22 -0
- package/dist/updater/index.js +4 -2
- package/package.json +1 -1
package/dist/ui/layout.js
CHANGED
|
@@ -49,6 +49,9 @@ let selection = null;
|
|
|
49
49
|
let selecting = false; // 左键按下中(press→release 之间)
|
|
50
50
|
let mouseEnabled = true; // 导航菜单(picker)期间置 false:只吞报表不做选区/滚动,防菜单被 viewport 重画覆盖
|
|
51
51
|
const WHEEL_LINES = 3; // 滚轮每格滚动行数
|
|
52
|
+
let inputSelection = null;
|
|
53
|
+
/** 上次画过输入框的 lastView 签名(lines 数 + 每行 display_w 累加);变了说明 prompt.ts 改了文本 → 选区锚点失效,清掉。 */
|
|
54
|
+
let lastInputSig = '';
|
|
52
55
|
// 点击输入框粘贴:当前文本输入方(prompt.ts / repl 运行态 typeahead)注册回调,接收剪贴板文本贴入。
|
|
53
56
|
// 只在输入区(底栏输入行)内右键单击(未拖动)时触发——与内容区选区互不干扰(内容区左键=框选,右键=复制)。
|
|
54
57
|
let pasteHandler = null;
|
|
@@ -618,9 +621,15 @@ export function scrollBy(delta) {
|
|
|
618
621
|
}
|
|
619
622
|
/** 清活跃选区(不复制),若原有选区则重画去掉反白。供 ESC / 点击别处 / 退出滚动态等场景调。 */
|
|
620
623
|
export function clearSelection() {
|
|
621
|
-
if (!selection)
|
|
624
|
+
if (!selection && !inputSelection)
|
|
622
625
|
return;
|
|
623
626
|
selection = null;
|
|
627
|
+
if (inputSelection) {
|
|
628
|
+
inputSelection = null;
|
|
629
|
+
if (active && base && lastView)
|
|
630
|
+
paintInput(lastView);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
624
633
|
if (scrollOffset >= 0)
|
|
625
634
|
repaintViewport();
|
|
626
635
|
}
|
|
@@ -648,6 +657,11 @@ export function setMouseEnabled(v) {
|
|
|
648
657
|
selection = null;
|
|
649
658
|
repaintViewport();
|
|
650
659
|
}
|
|
660
|
+
if (inputSelection) {
|
|
661
|
+
inputSelection = null;
|
|
662
|
+
if (active && base && lastView)
|
|
663
|
+
paintInput(lastView);
|
|
664
|
+
}
|
|
651
665
|
}
|
|
652
666
|
}
|
|
653
667
|
/** 从归一化选区抠出纯文本(去 ANSI,按显示列裁切,行间 \n 拼接)。越界行跳过(缓冲被 trim 等边界情况)。 */
|
|
@@ -665,6 +679,47 @@ function extractSelectionText(sel) {
|
|
|
665
679
|
}
|
|
666
680
|
return out.join('\n');
|
|
667
681
|
}
|
|
682
|
+
/** 输入框选区归一化:按 (line, col) 字典序排成 start/end。无选区返 null。 */
|
|
683
|
+
function normalizeInputSelection() {
|
|
684
|
+
if (!inputSelection)
|
|
685
|
+
return null;
|
|
686
|
+
const a = inputSelection.anchor;
|
|
687
|
+
const b = inputSelection.end;
|
|
688
|
+
const aFirst = a.line < b.line || (a.line === b.line && a.col <= b.col);
|
|
689
|
+
return aFirst
|
|
690
|
+
? { startLine: a.line, startCol: a.col, endLine: b.line, endCol: b.col }
|
|
691
|
+
: { startLine: b.line, startCol: b.col, endLine: a.line, endCol: a.col };
|
|
692
|
+
}
|
|
693
|
+
/** 从归一化输入框选区抠出纯文本(lines 上不带 ANSI,直接按 display_col 切)。越界行跳到该行末尾。 */
|
|
694
|
+
function extractInputSelectionText(sel) {
|
|
695
|
+
if (!lastView)
|
|
696
|
+
return '';
|
|
697
|
+
const out = [];
|
|
698
|
+
for (let l = sel.startLine; l <= sel.endLine; l++) {
|
|
699
|
+
const raw = lastView.lines[l];
|
|
700
|
+
if (raw == null)
|
|
701
|
+
continue;
|
|
702
|
+
const lineW = displayWidth(raw);
|
|
703
|
+
const colStart = l === sel.startLine ? sel.startCol : 0;
|
|
704
|
+
const colEnd = l === sel.endLine ? sel.endCol : lineW;
|
|
705
|
+
out.push(sliceByDisplayCol(raw, colStart, colEnd));
|
|
706
|
+
}
|
|
707
|
+
return out.join('\n');
|
|
708
|
+
}
|
|
709
|
+
/** 输入框 lastView 签名:lines 数 + 每行 display_w 累加;任何键改动 lines 即变化,用作 inputSelection 失效判据。 */
|
|
710
|
+
function inputViewSig(view) {
|
|
711
|
+
if (!view)
|
|
712
|
+
return '';
|
|
713
|
+
return view.lines.length + '|' + view.lines.map((l) => displayWidth(l).toString()).join(',');
|
|
714
|
+
}
|
|
715
|
+
/** 清输入框反白选区并重画(若 active)。不复制,仅清。供 clearSelection / setMouseEnabled 复用。 */
|
|
716
|
+
function clearInputSelection() {
|
|
717
|
+
if (!inputSelection)
|
|
718
|
+
return;
|
|
719
|
+
inputSelection = null;
|
|
720
|
+
if (active && base && lastView)
|
|
721
|
+
paintInput(lastView);
|
|
722
|
+
}
|
|
668
723
|
/** 屏行是否落在底栏输入行范围内(paintInput 的 firstInputRow..firstInputRow+inputRowsAvail-1)。 */
|
|
669
724
|
function isInputRow(row) {
|
|
670
725
|
const g = getGeo();
|
|
@@ -706,6 +761,33 @@ function setInputCursorFromClick(screenRow, screenCol) {
|
|
|
706
761
|
if (mode !== 'input') {
|
|
707
762
|
enterInputMode(statusText);
|
|
708
763
|
}
|
|
764
|
+
const pos = inputScreenToInputPos(screenRow, screenCol);
|
|
765
|
+
if (!pos) {
|
|
766
|
+
paintInput(lastView);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
// 三步原子:
|
|
770
|
+
// 1) lastView.cursorLine = targetLine — view.cursorLine 单位 = lines 行号 in dispLines(供 paintInput)
|
|
771
|
+
// 2) lastView.cursorCol = targetDisplayCol — view.cursorCol 单位 = display_col(供 paintInput,
|
|
772
|
+
// paintInput 内部 wrap/flat 后用 cursorCol 算 visLine + cursorVisCol)
|
|
773
|
+
// 3) paintInput 重画——末步 cup 把真光标移到点击位的可视位置
|
|
774
|
+
// 4) cursorChangeHandler(dispatchFlatIdx, dispatchInSegVis) — 给 prompt 的"屏 tap 原坐标",
|
|
775
|
+
// prompt 自己重做 flat 算 cl/cc(扣 chip prefix,与 layout 端的 stamp 时 dispLines 一致即可)。
|
|
776
|
+
// 未注册 handler(非输入态)→ 仅步骤 3 可见真光标移动;步骤 4 不动 prompt 的 cl/cc。
|
|
777
|
+
lastView = { ...lastView, cursorLine: pos.line, cursorCol: pos.displayCol };
|
|
778
|
+
paintInput(lastView);
|
|
779
|
+
if (cursorChangeHandler)
|
|
780
|
+
cursorChangeHandler(pos.flatIdx, pos.inSegVis);
|
|
781
|
+
}
|
|
782
|
+
/**
|
|
783
|
+
* 把屏(行,列)反推到 (lines 行号, 行内 display_col, flatIdx, 段内显示列),
|
|
784
|
+
* 算法与 setInputCursorFromClick 抽出前等价——基于 lastView 的"折行 + 滚动窗"几何,
|
|
785
|
+
* 与 paintInput 的 windowInputVis 同源(startVis 同锚),保证 press/drag 落点 = 画上高亮位置。
|
|
786
|
+
* 越界点(空行)→ 落末行末段末。
|
|
787
|
+
*/
|
|
788
|
+
function inputScreenToInputPos(screenRow, screenCol) {
|
|
789
|
+
if (!lastView)
|
|
790
|
+
return null;
|
|
709
791
|
const g = getGeo();
|
|
710
792
|
const promptW = displayWidth(lastView.prompt);
|
|
711
793
|
const firstInputRow = g.contentBottom + 4;
|
|
@@ -717,7 +799,7 @@ function setInputCursorFromClick(screenRow, screenCol) {
|
|
|
717
799
|
const visCol = Math.max(0, screenCol - 1 - promptW);
|
|
718
800
|
// 复刻 windowInputVis 的折行 + 滚动窗(输入态, lines 全量参与)。
|
|
719
801
|
// lastView.lines 在 prompt 的 redraw() 中为 dispLines()(含 chip pre 行 / chip prefix 列);
|
|
720
|
-
// chip 模式下点击 chip
|
|
802
|
+
// chip 模式下点击 chip 区域也走同一算法,与 paintInput 的视协议(段接缝除外)一致。
|
|
721
803
|
const cols = Math.max(1, g.cols - promptW);
|
|
722
804
|
const lineVis = lastView.lines.map((l) => wrapByDisplayWidth(l, cols));
|
|
723
805
|
const flat = [];
|
|
@@ -726,7 +808,6 @@ function setInputCursorFromClick(screenRow, screenCol) {
|
|
|
726
808
|
flat.push(r);
|
|
727
809
|
const totalVis = flat.length;
|
|
728
810
|
const maxInputRows = Math.max(1, Math.floor(g.rows * 0.4));
|
|
729
|
-
const showCount = Math.min(maxInputRows, totalVis);
|
|
730
811
|
// 起窗偏移:用 lastView 当前光标位置作为锚(与 paintInput 同算法)。
|
|
731
812
|
let curVisLine = 0;
|
|
732
813
|
{
|
|
@@ -748,10 +829,6 @@ function setInputCursorFromClick(screenRow, screenCol) {
|
|
|
748
829
|
const startVis = totalVis > maxInputRows
|
|
749
830
|
? Math.max(0, Math.min(curAbs - maxInputRows + 1, totalVis - maxInputRows))
|
|
750
831
|
: 0;
|
|
751
|
-
let targetLine;
|
|
752
|
-
let targetDisplayCol;
|
|
753
|
-
let dispatchFlatIdx; // 传给 prompt 的屏 tap 原坐标(flat 中段号);超尾点边界用 totalVis - 1
|
|
754
|
-
let dispatchInSegVis; // 段内显示列
|
|
755
832
|
// 点到可视区末行之下(空行)→ 等价"光标在最末可视行的字符串末尾"。
|
|
756
833
|
if (startVis + visRow >= totalVis) {
|
|
757
834
|
const lastFlatIdx = totalVis - 1;
|
|
@@ -766,55 +843,44 @@ function setInputCursorFromClick(screenRow, screenCol) {
|
|
|
766
843
|
lastLine = lineVis.length - 1;
|
|
767
844
|
lastSegInLine = lineVis[lastLine].length - 1;
|
|
768
845
|
}
|
|
769
|
-
//
|
|
846
|
+
// lines[lastLine] 内 display_col = 前 segInLine 段 display_w + 末段 display_w
|
|
770
847
|
let inLineDisplayCol = 0;
|
|
771
848
|
for (let s = 0; s <= lastSegInLine; s++)
|
|
772
849
|
inLineDisplayCol += displayWidth(lineVis[lastLine][s] ?? '');
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
// 1) lastView.cursorLine = targetLine — view.cursorLine 单位 = lines 行号 in dispLines(供 paintInput)
|
|
808
|
-
// 2) lastView.cursorCol = targetDisplayCol — view.cursorCol 单位 = display_col(供 paintInput,
|
|
809
|
-
// paintInput 内部 wrap/flat 后用 cursorCol 算 visLine + cursorVisCol)
|
|
810
|
-
// 3) paintInput 重画——末步 cup 把真光标移到点击位的可视位置
|
|
811
|
-
// 4) cursorChangeHandler(dispatchFlatIdx, dispatchInSegVis) — 给 prompt 的"屏 tap 原坐标",
|
|
812
|
-
// prompt 自己重做 flat 算 cl/cc(扣 chip prefix,与 layout 端的 stamp 时 dispLines 一致即可)。
|
|
813
|
-
// 未注册 handler(非输入态)→ 仅步骤 3 可见真光标移动;步骤 4 不动 prompt 的 cl/cc。
|
|
814
|
-
lastView = { ...lastView, cursorLine: targetLine, cursorCol: targetDisplayCol };
|
|
815
|
-
paintInput(lastView);
|
|
816
|
-
if (cursorChangeHandler)
|
|
817
|
-
cursorChangeHandler(dispatchFlatIdx, dispatchInSegVis);
|
|
850
|
+
return {
|
|
851
|
+
line: lastLine,
|
|
852
|
+
displayCol: inLineDisplayCol,
|
|
853
|
+
flatIdx: lastFlatIdx,
|
|
854
|
+
inSegVis: displayWidth(flat[lastFlatIdx]), // 段末
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
// flatIdx = 点击可视段在 flat 中的绝对索引(startVis + visRow)
|
|
858
|
+
const flatIdx = startVis + visRow;
|
|
859
|
+
// 把 flatIdx 反推到 (line, segInLine) — line 给 paintInput 用(同 lines[cursorLine] 行号),
|
|
860
|
+
// segInLine 给 prompt 做 cl/cc 反推用。
|
|
861
|
+
let line = 0;
|
|
862
|
+
let segInLine = flatIdx;
|
|
863
|
+
while (line < lineVis.length && segInLine >= lineVis[line].length) {
|
|
864
|
+
segInLine -= lineVis[line].length;
|
|
865
|
+
line++;
|
|
866
|
+
}
|
|
867
|
+
if (line >= lineVis.length) {
|
|
868
|
+
line = lineVis.length - 1;
|
|
869
|
+
segInLine = lineVis[line].length - 1;
|
|
870
|
+
}
|
|
871
|
+
// lines[line] 内 display_col = 前 segInLine 段 display_w 累加 + 段内显示列。
|
|
872
|
+
let inLineDisplayCol = 0;
|
|
873
|
+
for (let s = 0; s < segInLine; s++)
|
|
874
|
+
inLineDisplayCol += displayWidth(lineVis[line][s] ?? '');
|
|
875
|
+
const seg = flat[flatIdx];
|
|
876
|
+
const segW = displayWidth(seg);
|
|
877
|
+
const inSeg = Math.min(visCol, segW); // 段尾点击容许越界 clip 到段末
|
|
878
|
+
return {
|
|
879
|
+
line,
|
|
880
|
+
displayCol: inLineDisplayCol + inSeg,
|
|
881
|
+
flatIdx,
|
|
882
|
+
inSegVis: inSeg,
|
|
883
|
+
};
|
|
818
884
|
}
|
|
819
885
|
/**
|
|
820
886
|
* 鼠标事件分发(mouse.setHandler 注册)。
|
|
@@ -840,9 +906,20 @@ function handleMouseEvent(e) {
|
|
|
840
906
|
return;
|
|
841
907
|
const g = getGeo();
|
|
842
908
|
const col = Math.max(0, e.col - 1); // SGR 报表列 1-based → 显示列 0-based
|
|
843
|
-
// 右键释放:落输入行 →
|
|
909
|
+
// 右键释放:落输入行 → 优先复制当前输入框选区(若有),退化为贴入;落内容区 → 复制选区后清高亮。
|
|
844
910
|
if (e.type === 'release' && e.button === 2) {
|
|
845
911
|
if (isInputRow(e.row)) {
|
|
912
|
+
// 输入框选区存在且真拖动过:复制(不动 text 光标,不改 cl/cc),与内容区"copy 后清高亮"对齐
|
|
913
|
+
const inpSel = normalizeInputSelection();
|
|
914
|
+
if (inpSel) {
|
|
915
|
+
const text = extractInputSelectionText(inpSel);
|
|
916
|
+
inputSelection = null;
|
|
917
|
+
if (lastView)
|
|
918
|
+
paintInput(lastView); // 立刻擦反白,视觉反馈
|
|
919
|
+
if (text)
|
|
920
|
+
copyToClipboard(text);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
846
923
|
pasteIntoInput();
|
|
847
924
|
return;
|
|
848
925
|
}
|
|
@@ -861,20 +938,48 @@ function handleMouseEvent(e) {
|
|
|
861
938
|
return; // 其余中/右键 press/drag 不处理(终端原生右键菜单等不受影响)
|
|
862
939
|
if (e.type === 'press') {
|
|
863
940
|
if (isInputRow(e.row)) {
|
|
864
|
-
// 左键落输入行:进输入态 + 光标定位到点击位 +
|
|
865
|
-
//
|
|
941
|
+
// 左键落输入行:进输入态 + 光标定位到点击位 + 开输入框反白选区(起点 = 终点 = 点击位)。
|
|
942
|
+
// 不再 return——后续 drag/release 分支也要知道选区在输入框。点击未拖动时 release 端会清掉选区,
|
|
943
|
+
// 与"点一下就定位光标、起手打字"的习惯一致。
|
|
866
944
|
setInputCursorFromClick(e.row, e.col);
|
|
945
|
+
const pos = inputScreenToInputPos(e.row, e.col);
|
|
946
|
+
if (pos) {
|
|
947
|
+
// 清掉内容区旧选区(若有):按一下输入框不应同时保留内容区反白,视觉混淆。
|
|
948
|
+
if (selection) {
|
|
949
|
+
selection = null;
|
|
950
|
+
}
|
|
951
|
+
inputSelection = {
|
|
952
|
+
anchor: { line: pos.line, col: pos.displayCol },
|
|
953
|
+
end: { line: pos.line, col: pos.displayCol },
|
|
954
|
+
dragged: false,
|
|
955
|
+
};
|
|
956
|
+
paintInput(lastView);
|
|
957
|
+
}
|
|
867
958
|
return;
|
|
868
959
|
}
|
|
869
960
|
selecting = true;
|
|
870
961
|
const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
|
|
871
962
|
const absLine = screenRowToAbsLine(rowInContent);
|
|
872
963
|
selection = { anchorLine: absLine, anchorCol: col, endLine: absLine, endCol: col, dragged: false };
|
|
964
|
+
// 切内容区选区时清掉输入框旧选区(若有)。
|
|
965
|
+
inputSelection = null;
|
|
873
966
|
repaintViewport();
|
|
874
967
|
repaint(); // 补一次输入区重画:INPUT 态 repaintViewport 把真光标留在内容区续写位,须靠 repaint 把它带回输入框(否则点内容区会现假闪烁光标,见 issue)
|
|
875
968
|
return;
|
|
876
969
|
}
|
|
877
970
|
if (e.type === 'drag') {
|
|
971
|
+
if (inputSelection) {
|
|
972
|
+
// 输入框拖动选区:更新 end,触边不动 scroll(输入框行数有限,无外滚概念);重画底栏。
|
|
973
|
+
const pos = inputScreenToInputPos(e.row, e.col);
|
|
974
|
+
if (pos) {
|
|
975
|
+
if (pos.line !== inputSelection.end.line || pos.displayCol !== inputSelection.end.col) {
|
|
976
|
+
inputSelection.dragged = true;
|
|
977
|
+
inputSelection.end = { line: pos.line, col: pos.displayCol };
|
|
978
|
+
}
|
|
979
|
+
paintInput(lastView);
|
|
980
|
+
}
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
878
983
|
if (!selecting || !selection)
|
|
879
984
|
return;
|
|
880
985
|
// 触边自动翻页:motion 事件持续到达时靠此逐步滚动,把选区扩展到滚出去的历史行。
|
|
@@ -894,6 +999,16 @@ function handleMouseEvent(e) {
|
|
|
894
999
|
}
|
|
895
1000
|
// release(左键)
|
|
896
1001
|
selecting = false;
|
|
1002
|
+
// 输入框 release 优先:未拖动 → 清选区(光标已定好);拖动过 → 留高亮等右键复制
|
|
1003
|
+
if (inputSelection) {
|
|
1004
|
+
if (!inputSelection.dragged) {
|
|
1005
|
+
inputSelection = null;
|
|
1006
|
+
if (lastView)
|
|
1007
|
+
paintInput(lastView);
|
|
1008
|
+
}
|
|
1009
|
+
// 拖动过:不操作,留高亮
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
897
1012
|
if (!selection)
|
|
898
1013
|
return;
|
|
899
1014
|
if (!selection.dragged) {
|
|
@@ -1261,6 +1376,38 @@ function windowInputVis(lines, cursorLine, cursorCol, cols, promptW, rows) {
|
|
|
1261
1376
|
startVis,
|
|
1262
1377
|
};
|
|
1263
1378
|
}
|
|
1379
|
+
/** 把 lines 按 W 做软折,返回 lineVis;供 paintInput 高亮预算用。与 windowInputVis 复用 wrap 算法。 */
|
|
1380
|
+
function visInputLines(lines, W) {
|
|
1381
|
+
return lines.map((l) => wrapByDisplayWidth(l, W));
|
|
1382
|
+
}
|
|
1383
|
+
/**
|
|
1384
|
+
* 给纯文本(无 ANSI,来自 wrap 后的可视段)做"显示列区间"高亮,返回插入 SEL_OPEN/SEL_OFF 后的字符串。
|
|
1385
|
+
* 输入字段 vis 段宽 = displayWidth(seg),clipStart/clipEnd 单位 = display_col。
|
|
1386
|
+
* 与 highlightRange 等价语义但省去 ANSI split(纯文本无 SGR),更省。
|
|
1387
|
+
*/
|
|
1388
|
+
function highlightWithinRow(seg, clipStart, clipEnd) {
|
|
1389
|
+
if (clipEnd <= clipStart)
|
|
1390
|
+
return seg;
|
|
1391
|
+
let out = '';
|
|
1392
|
+
let w = 0;
|
|
1393
|
+
let opened = false;
|
|
1394
|
+
for (const ch of seg) {
|
|
1395
|
+
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
1396
|
+
if (!opened && w >= clipStart && w < clipEnd) {
|
|
1397
|
+
out += SEL_OPEN;
|
|
1398
|
+
opened = true;
|
|
1399
|
+
}
|
|
1400
|
+
if (opened && w >= clipEnd) {
|
|
1401
|
+
out += SEL_OFF;
|
|
1402
|
+
opened = false;
|
|
1403
|
+
}
|
|
1404
|
+
out += ch;
|
|
1405
|
+
w += cw;
|
|
1406
|
+
}
|
|
1407
|
+
if (opened)
|
|
1408
|
+
out += SEL_OFF;
|
|
1409
|
+
return out;
|
|
1410
|
+
}
|
|
1264
1411
|
/**
|
|
1265
1412
|
* 画输入区:擦旧菜单 → (必要时)setRegion → 画状态行 + 输入行 + 向上菜单,光标留输入框(dim 时回续写位)。
|
|
1266
1413
|
* prompt.ts 每次按键调;enterInputMode / enterRunningMode 也调(空 / dim)。
|
|
@@ -1269,6 +1416,18 @@ export function paintInput(view) {
|
|
|
1269
1416
|
if (!active || !base)
|
|
1270
1417
|
return;
|
|
1271
1418
|
lastView = view;
|
|
1419
|
+
// 失效检测:lastView 文本若有变化(键输入/AI 续写)→ 选区锚点不再有效,清掉避免「错位反白」。
|
|
1420
|
+
// 内容区选区用 content 自己的锚,与 lastView 无关,不动。
|
|
1421
|
+
if (inputSelection) {
|
|
1422
|
+
const sig = inputViewSig(view);
|
|
1423
|
+
if (sig !== lastInputSig) {
|
|
1424
|
+
inputSelection = null;
|
|
1425
|
+
}
|
|
1426
|
+
lastInputSig = sig;
|
|
1427
|
+
}
|
|
1428
|
+
else {
|
|
1429
|
+
lastInputSig = inputViewSig(view);
|
|
1430
|
+
}
|
|
1272
1431
|
const preGeo = getGeo();
|
|
1273
1432
|
// 累积本帧所有 cup/clearLine/文本,末尾一次写出——避免「擦旧菜单」与「重画」分多次 write 时,
|
|
1274
1433
|
// 终端把中间空白渲染出来 → 菜单/面板切换时闪烁(↑↓ 导航每次 redraw 都全帧擦后重画)。
|
|
@@ -1333,13 +1492,53 @@ export function paintInput(view) {
|
|
|
1333
1492
|
// 光标:不画反白块/假光标——输入框走终端真光标(WT / VSCode 终端默认竖线/闪烁块,
|
|
1334
1493
|
// 各终端表现略不同但都贴合 IME 候选气泡且不再"挡住字符")。真光标位置由下方第 6 步 cup 写。
|
|
1335
1494
|
// 保留 view.caret=false 路径给 picker 等非文本输入:把真光标定位到 hint 末尾。
|
|
1495
|
+
const inpSel = normalizeInputSelection(); // 已签名检测失效,这里看到的要么有效要么 null
|
|
1496
|
+
// 预算 (visRow -> line) 用于高亮:与 inputScreenToInputPos 的反推同源(flat → line + segInLine)
|
|
1497
|
+
const colW = Math.max(1, preGeo.cols - promptW);
|
|
1498
|
+
const lineVisArr = visInputLines(view.lines, colW);
|
|
1499
|
+
const flatInput = [];
|
|
1500
|
+
for (const lv of lineVisArr)
|
|
1501
|
+
for (const r of lv)
|
|
1502
|
+
flatInput.push(r);
|
|
1503
|
+
let cumLine = 0;
|
|
1504
|
+
const flatToLine = [];
|
|
1505
|
+
for (let li = 0; li < lineVisArr.length; li++) {
|
|
1506
|
+
const segs = lineVisArr[li];
|
|
1507
|
+
let acc = 0;
|
|
1508
|
+
for (let s = 0; s < segs.length; s++) {
|
|
1509
|
+
flatToLine.push({ line: li, segInLine: s, inLineDisplayCol: acc });
|
|
1510
|
+
acc += displayWidth(segs[s]);
|
|
1511
|
+
}
|
|
1512
|
+
if (segs.length === 0)
|
|
1513
|
+
flatToLine.push({ line: li, segInLine: 0, inLineDisplayCol: 0 });
|
|
1514
|
+
}
|
|
1336
1515
|
for (let i = 0; i < inputRowsAvail; i++) {
|
|
1337
1516
|
const line = vis.visRows[i] ?? '';
|
|
1338
1517
|
const r = firstInputRow + i;
|
|
1339
1518
|
const prefix = vis.startVis === 0 && i === 0 ? view.prompt : indent;
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1519
|
+
let text;
|
|
1520
|
+
if (view.dim) {
|
|
1521
|
+
text = renderDimInputRow(view.prompt, line, view.placeholder ?? '', g.cols);
|
|
1522
|
+
}
|
|
1523
|
+
else {
|
|
1524
|
+
text = `${prefix}${line}`;
|
|
1525
|
+
// 输入框反白叠层:仅当 paintInput 不是 dim(运行态 typeahead 不参与选区,避免干扰 IME 气泡),
|
|
1526
|
+
// 且该可视段所属的逻辑行落在 inpSel 区间内 → 高亮行内 [colStart,colEnd) 段。
|
|
1527
|
+
if (inpSel) {
|
|
1528
|
+
const flatIdx = vis.startVis + i;
|
|
1529
|
+
const m = flatIdx < flatToLine.length ? flatToLine[flatIdx] : flatToLine[flatToLine.length - 1];
|
|
1530
|
+
if (m.line >= inpSel.startLine && m.line <= inpSel.endLine) {
|
|
1531
|
+
const seg = flatInput[flatIdx] ?? '';
|
|
1532
|
+
const segW = displayWidth(seg);
|
|
1533
|
+
const colStart = m.line === inpSel.startLine ? inpSel.startCol - m.inLineDisplayCol : 0;
|
|
1534
|
+
const colEnd = m.line === inpSel.endLine ? inpSel.endCol - m.inLineDisplayCol : segW;
|
|
1535
|
+
if (colStart < segW && colEnd > colStart) {
|
|
1536
|
+
// 对 seg(纯文本无 ANSI)做行内高亮:在 [clipStart, clipEnd) 之间插 SEL_OPEN/SEL_OFF。
|
|
1537
|
+
text = `${prefix}${highlightWithinRow(seg, Math.max(0, colStart), Math.min(segW, colEnd))}`;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1343
1542
|
buf += cup(r, 1) + esc.clearLine + text;
|
|
1344
1543
|
}
|
|
1345
1544
|
// 4b. 下线(输入框底):满屏宽细线 ─(cyan),在 model 行上一行(model 行占屏底 rows)
|
package/dist/ui/theme.js
CHANGED
|
@@ -29,6 +29,9 @@ const DEFAULT = {
|
|
|
29
29
|
brightCyan: '\x1B[38;2;86;182;194m',
|
|
30
30
|
brightMagenta: '\x1B[38;2;198;120;221m',
|
|
31
31
|
userBg: '\x1B[48;2;44;49;60m', // One Dark current-line bg,深色终端上微妙可辨
|
|
32
|
+
// diff 行底色:One Dark bg #282c34 上加 ~14% 亮度的对应色,够辨识但不刺眼
|
|
33
|
+
addBg: '\x1B[48;2;44;62;42m', // 偏暗绿(One Dark green(152,195,121)暗化)
|
|
34
|
+
delBg: '\x1B[48;2;62;38;42m', // 偏暗红(One Dark red(224,108,117)暗化)
|
|
32
35
|
};
|
|
33
36
|
/**
|
|
34
37
|
* 内建主题表。default=One Dark、light=Solarized Light、solarized=Solarized Dark、
|
|
@@ -49,6 +52,10 @@ const THEMES = {
|
|
|
49
52
|
brightCyan: '\x1B[38;2;42;161;152m',
|
|
50
53
|
brightMagenta: '\x1B[38;2;108;113;196m',
|
|
51
54
|
userBg: '\x1B[48;2;238;232;213m',
|
|
55
|
+
// diff 行底色:Solarized Light base2(238,232,213)上贴同色族浅 tint,
|
|
56
|
+
// 比直接用 base1 更柔,跟深 fg(red/green)对比充足
|
|
57
|
+
addBg: '\x1B[48;2;220;235;205m',
|
|
58
|
+
delBg: '\x1B[48;2;245;218;215m',
|
|
52
59
|
},
|
|
53
60
|
solarized: {
|
|
54
61
|
red: '\x1B[38;2;220;50;47m',
|
|
@@ -61,6 +68,9 @@ const THEMES = {
|
|
|
61
68
|
brightCyan: '\x1B[38;2;147;161;161m',
|
|
62
69
|
brightMagenta: '\x1B[38;2;108;113;196m',
|
|
63
70
|
userBg: '\x1B[48;2;7;54;66m',
|
|
71
|
+
// diff 行底色:Solarized Dark base03(0,43,54)上加对应色族暗 tint
|
|
72
|
+
addBg: '\x1B[48;2;20;50;38m',
|
|
73
|
+
delBg: '\x1B[48;2;55;30;30m',
|
|
64
74
|
},
|
|
65
75
|
gruvbox: {
|
|
66
76
|
red: '\x1B[38;2;251;73;52m',
|
|
@@ -73,6 +83,9 @@ const THEMES = {
|
|
|
73
83
|
brightCyan: '\x1B[38;2;142;192;124m',
|
|
74
84
|
brightMagenta: '\x1B[38;2;211;134;155m',
|
|
75
85
|
userBg: '\x1B[48;2;40;40;40m',
|
|
86
|
+
// diff 行底色:Gruvbox dark bg(40,40,40)上贴暗 bg0_a 风格
|
|
87
|
+
addBg: '\x1B[48;2;40;55;30m',
|
|
88
|
+
delBg: '\x1B[48;2;70;35;30m',
|
|
76
89
|
},
|
|
77
90
|
nord: {
|
|
78
91
|
red: '\x1B[38;2;191;97;106m',
|
|
@@ -85,6 +98,9 @@ const THEMES = {
|
|
|
85
98
|
brightCyan: '\x1B[38;2;136;192;208m',
|
|
86
99
|
brightMagenta: '\x1B[38;2;180;142;173m',
|
|
87
100
|
userBg: '\x1B[48;2;67;76;94m',
|
|
101
|
+
// diff 行底色:Nord polar night(46,52,64)上贴对应色族暗 tint
|
|
102
|
+
addBg: '\x1B[48;2;46;66;52m',
|
|
103
|
+
delBg: '\x1B[48;2;72;46;52m',
|
|
88
104
|
},
|
|
89
105
|
};
|
|
90
106
|
let currentName = 'default';
|
|
@@ -155,4 +171,10 @@ export const ui = {
|
|
|
155
171
|
get userBg() {
|
|
156
172
|
return color('userBg');
|
|
157
173
|
},
|
|
174
|
+
get addBg() {
|
|
175
|
+
return color('addBg');
|
|
176
|
+
},
|
|
177
|
+
get delBg() {
|
|
178
|
+
return color('delBg');
|
|
179
|
+
},
|
|
158
180
|
};
|
package/dist/updater/index.js
CHANGED
|
@@ -126,13 +126,15 @@ export function compareSemver(a, b) {
|
|
|
126
126
|
}
|
|
127
127
|
return 0;
|
|
128
128
|
}
|
|
129
|
-
/** 纯决策:据当前版本 / 缓存 / 时间,算是否提示 / spawn / 后台刷新。无副作用,可离线测。
|
|
129
|
+
/** 纯决策:据当前版本 / 缓存 / 时间,算是否提示 / spawn / 后台刷新。无副作用,可离线测。
|
|
130
|
+
* notice 同时携带当前版本(`current`)与最新版本(`latest`),让 REPL 提示能向用户
|
|
131
|
+
* 清晰展示"你正在用的 mocode 是 X,最新是 Y",不只是单调的"有新版本"。 */
|
|
130
132
|
export function evaluateUpdate(current, cache, now) {
|
|
131
133
|
const refresh = !cache.latest || now - (cache.lastCheck ?? 0) > CHECK_INTERVAL_MS;
|
|
132
134
|
let notice = null;
|
|
133
135
|
let spawn = false;
|
|
134
136
|
if (cache.latest && compareSemver(cache.latest, current) > 0) {
|
|
135
|
-
notice =
|
|
137
|
+
notice = `当前 mocode v${current} 已是较旧版本,最新为 v${cache.latest},后台更新中,下次启动生效。`;
|
|
136
138
|
spawn = now - (cache.lastSpawn ?? 0) > SPAWN_INTERVAL_MS;
|
|
137
139
|
}
|
|
138
140
|
return { notice, spawn, refresh };
|