mocode-ai 0.4.4 → 0.4.6
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 -209
- package/README.zh-CN.md +10 -0
- package/dist/agent/index.js +30 -13
- package/dist/config/index.js +13 -11
- package/dist/config/presets.js +177 -0
- package/dist/llm/capabilities.js +4 -0
- package/dist/llm/index.js +5 -1
- package/dist/memory/reflect.js +5 -2
- package/dist/repl/index.js +268 -10
- package/dist/tools/builtins/ask-human.js +71 -43
- package/dist/tools/builtins/codegraph.js +1 -1
- package/dist/ui/batch.js +235 -0
- package/dist/ui/content.js +51 -0
- package/dist/ui/diff.js +41 -4
- package/dist/ui/intervention.js +59 -20
- package/dist/ui/layout.js +449 -66
- package/dist/ui/prompt.js +99 -5
- package/dist/ui/render.js +22 -0
- package/dist/ui/theme.js +22 -0
- package/package.json +1 -1
package/dist/ui/layout.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
1
|
import { stdin, stdout } from 'node:process';
|
|
2
|
-
import { appendFileSync } from 'node:fs';
|
|
3
|
-
import { homedir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
2
|
import { charWidth, displayWidth, truncateDisplay, truncateDisplayHead, ansiDisplayWidth, wrapByDisplayWidth, fmtElapsed, stripAnsi, sliceByDisplayCol, } from './render.js';
|
|
6
3
|
import { ui } from './theme.js';
|
|
7
4
|
import * as content from './content.js';
|
|
@@ -52,9 +49,16 @@ let selection = null;
|
|
|
52
49
|
let selecting = false; // 左键按下中(press→release 之间)
|
|
53
50
|
let mouseEnabled = true; // 导航菜单(picker)期间置 false:只吞报表不做选区/滚动,防菜单被 viewport 重画覆盖
|
|
54
51
|
const WHEEL_LINES = 3; // 滚轮每格滚动行数
|
|
52
|
+
let inputSelection = null;
|
|
53
|
+
/** 上次画过输入框的 lastView 签名(lines 数 + 每行 display_w 累加);变了说明 prompt.ts 改了文本 → 选区锚点失效,清掉。 */
|
|
54
|
+
let lastInputSig = '';
|
|
55
55
|
// 点击输入框粘贴:当前文本输入方(prompt.ts / repl 运行态 typeahead)注册回调,接收剪贴板文本贴入。
|
|
56
56
|
// 只在输入区(底栏输入行)内右键单击(未拖动)时触发——与内容区选区互不干扰(内容区左键=框选,右键=复制)。
|
|
57
57
|
let pasteHandler = null;
|
|
58
|
+
/** 鼠标点击输入框 → prompt.ts(文本 source of truth)提供的光标应用回调;
|
|
59
|
+
* layout 只做"点击 → 算新位置"的映射,实际改 cl/cc 归 prompt(同 pasteHandler 套路)。
|
|
60
|
+
* 若未注册(非输入态/未进 prompt):仅做视觉反馈,cl/cc 不变。 */
|
|
61
|
+
let cursorChangeHandler = null;
|
|
58
62
|
const esc = {
|
|
59
63
|
altOn: '\x1B[?1049h',
|
|
60
64
|
altOff: '\x1B[?1049l',
|
|
@@ -394,6 +398,7 @@ export function clearContent() {
|
|
|
394
398
|
mdActive = false;
|
|
395
399
|
mdBuf = '';
|
|
396
400
|
content.reset();
|
|
401
|
+
notifyContentReset(); // batch 渲染器同步重置(batch 摘要行索引全部失效)
|
|
397
402
|
stdout.write(esc.home);
|
|
398
403
|
}
|
|
399
404
|
/**
|
|
@@ -420,6 +425,69 @@ export function rewindContent(rowsToRewind) {
|
|
|
420
425
|
// repaintViewport 会按新 buffer 重画整片,旧 frame 自然被覆盖。
|
|
421
426
|
repaintViewport();
|
|
422
427
|
}
|
|
428
|
+
/**
|
|
429
|
+
* 在绝对行索引 after 后插入自洽行(详情展开用)。
|
|
430
|
+
* 滚动回看冻结:scrollOffset>0 时 offset += delta 把窗口冻在原绝对行,
|
|
431
|
+
* 与 contentWrite/contentWriteMd 同模式。插入后 repaintViewport 重画 viewport。
|
|
432
|
+
*
|
|
433
|
+
* 续写位(contentRow):若原写头在插入点之后,前移 lines.length,保持相对位置;
|
|
434
|
+
* 若原写头 ≤ after,不变(新行在写头之后)。非 TTY 直接调 content.insertAfter。
|
|
435
|
+
*/
|
|
436
|
+
export function contentInsertAfter(after, lines) {
|
|
437
|
+
if (!active || lines.length === 0)
|
|
438
|
+
return;
|
|
439
|
+
const g = getGeo();
|
|
440
|
+
const totalBefore = content.totalRows();
|
|
441
|
+
const scrolled = scrollOffset > 0;
|
|
442
|
+
content.insertAfter(after, lines);
|
|
443
|
+
const delta = content.totalRows() - totalBefore;
|
|
444
|
+
if (delta === 0)
|
|
445
|
+
return;
|
|
446
|
+
// 续写位:原写头绝对行 = viewport 起点 + (contentRow-1) [offset=0];偏移后才一致
|
|
447
|
+
if (scrollOffset === 0 && contentRow > after + 1) {
|
|
448
|
+
contentRow = Math.min(contentRow + delta, g.contentBottom);
|
|
449
|
+
}
|
|
450
|
+
// 滚动回看冻结(同 contentWrite)
|
|
451
|
+
if (scrolled) {
|
|
452
|
+
scrollOffset = Math.max(0, Math.min(scrollOffset + delta, Math.max(0, content.totalRows() - g.contentBottom)));
|
|
453
|
+
}
|
|
454
|
+
// batch 摘要行索引平移
|
|
455
|
+
void import('./batch.js').then((m) => m.shiftBatchesAfter(after, delta));
|
|
456
|
+
repaintViewport();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* 从绝对行索引 startIdx 起删 n 行(折叠回退用)。
|
|
460
|
+
* 滚动回看冻结同 insertAfter:offset -= delta,钳 ≥ 0。续写位若在删区后前移 delta。
|
|
461
|
+
*/
|
|
462
|
+
export function contentDeleteFrom(startIdx, n) {
|
|
463
|
+
if (!active || n <= 0)
|
|
464
|
+
return;
|
|
465
|
+
const g = getGeo();
|
|
466
|
+
const totalBefore = content.totalRows();
|
|
467
|
+
const scrolled = scrollOffset > 0;
|
|
468
|
+
content.deleteFrom(startIdx, n);
|
|
469
|
+
const delta = totalBefore - content.totalRows();
|
|
470
|
+
if (delta === 0)
|
|
471
|
+
return;
|
|
472
|
+
if (scrollOffset === 0 && contentRow > startIdx + 1) {
|
|
473
|
+
contentRow = Math.max(1, contentRow - delta);
|
|
474
|
+
}
|
|
475
|
+
if (scrolled) {
|
|
476
|
+
scrollOffset = Math.max(0, scrollOffset - delta);
|
|
477
|
+
}
|
|
478
|
+
// batch 摘要行索引平移(用 -delta 表示后段索引前移)
|
|
479
|
+
void import('./batch.js').then((m) => m.shiftBatchesAfter(startIdx, -delta));
|
|
480
|
+
repaintViewport();
|
|
481
|
+
}
|
|
482
|
+
/** 当前内容物理行总数(含当前未提交行)。供 batch 渲染器在 endBatch 时定位摘要行索引。 */
|
|
483
|
+
export function totalRows() {
|
|
484
|
+
return content.totalRows();
|
|
485
|
+
}
|
|
486
|
+
/** 清空内容区时通知 batch 渲染器重置(摘要行映射与展开态)。 */
|
|
487
|
+
export function notifyContentReset() {
|
|
488
|
+
// 动态 import 避免循环;模块级 reset() 只清映射,不动 batch 内部数据(id 与 entries 仍可重用)
|
|
489
|
+
void import('./batch.js').then((m) => m.reset());
|
|
490
|
+
}
|
|
423
491
|
// ── viewport 滚动回看(Phase 2)──
|
|
424
492
|
/** 是否处于滚动回看态(offset>0,内容区显历史)。prompt 据此在非滚动键时回尾。 */
|
|
425
493
|
export function isScrolled() {
|
|
@@ -553,9 +621,15 @@ export function scrollBy(delta) {
|
|
|
553
621
|
}
|
|
554
622
|
/** 清活跃选区(不复制),若原有选区则重画去掉反白。供 ESC / 点击别处 / 退出滚动态等场景调。 */
|
|
555
623
|
export function clearSelection() {
|
|
556
|
-
if (!selection)
|
|
624
|
+
if (!selection && !inputSelection)
|
|
557
625
|
return;
|
|
558
626
|
selection = null;
|
|
627
|
+
if (inputSelection) {
|
|
628
|
+
inputSelection = null;
|
|
629
|
+
if (active && base && lastView)
|
|
630
|
+
paintInput(lastView);
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
559
633
|
if (scrollOffset >= 0)
|
|
560
634
|
repaintViewport();
|
|
561
635
|
}
|
|
@@ -567,6 +641,13 @@ export function clearSelection() {
|
|
|
567
641
|
export function setPasteHandler(fn) {
|
|
568
642
|
pasteHandler = fn;
|
|
569
643
|
}
|
|
644
|
+
/** 注册"点击输入框 → 改光标"回调:prompt.ts 等文本所有者挂此回调后,
|
|
645
|
+
* 鼠标左键落输入行算出的新 (line, col) 会在 prompt 下次 redraw 前喂入此回调
|
|
646
|
+
* (prompt 用它写自己的 cl / cc,真正改 source of truth)。
|
|
647
|
+
* 传 null 注销(在 cleanup / 退出输入态时调,防下个 prompt 实例被旧回调污染)。 */
|
|
648
|
+
export function setCursorChangeHandler(fn) {
|
|
649
|
+
cursorChangeHandler = fn;
|
|
650
|
+
}
|
|
570
651
|
/** picker / 介入面板期间禁用鼠标选区与拖拽(避免 viewport 重画覆盖菜单);滚轮仍可用。面板退出后恢复。 */
|
|
571
652
|
export function setMouseEnabled(v) {
|
|
572
653
|
mouseEnabled = v;
|
|
@@ -576,6 +657,11 @@ export function setMouseEnabled(v) {
|
|
|
576
657
|
selection = null;
|
|
577
658
|
repaintViewport();
|
|
578
659
|
}
|
|
660
|
+
if (inputSelection) {
|
|
661
|
+
inputSelection = null;
|
|
662
|
+
if (active && base && lastView)
|
|
663
|
+
paintInput(lastView);
|
|
664
|
+
}
|
|
579
665
|
}
|
|
580
666
|
}
|
|
581
667
|
/** 从归一化选区抠出纯文本(去 ANSI,按显示列裁切,行间 \n 拼接)。越界行跳过(缓冲被 trim 等边界情况)。 */
|
|
@@ -593,6 +679,47 @@ function extractSelectionText(sel) {
|
|
|
593
679
|
}
|
|
594
680
|
return out.join('\n');
|
|
595
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
|
+
}
|
|
596
723
|
/** 屏行是否落在底栏输入行范围内(paintInput 的 firstInputRow..firstInputRow+inputRowsAvail-1)。 */
|
|
597
724
|
function isInputRow(row) {
|
|
598
725
|
const g = getGeo();
|
|
@@ -612,11 +739,154 @@ function pasteIntoInput() {
|
|
|
612
739
|
})
|
|
613
740
|
.catch(() => { });
|
|
614
741
|
}
|
|
742
|
+
/** 鼠标左键落输入行:进输入态(若是 running)→ 用统一的 display_col 单位算出"目标光标位"
|
|
743
|
+
* → paintInput 用新光标重画(末步 cup 把终端真光标移到点击位,默认即竖线/闪烁块),
|
|
744
|
+
* 并同步调 cursorChangeHandler 让 prompt.ts 改自己的 cl/cc(source of truth),下次按键按新位写字。
|
|
745
|
+
*
|
|
746
|
+
* 单位说明:
|
|
747
|
+
* - paintInput 的 view.cursorCol 单位 = display_col(同 paintInput 的 dispCursorCol,见 prompt.ts:186)
|
|
748
|
+
* - prompt 的 cc 单位 = char_idx(字符索引)
|
|
749
|
+
* 本函数同时给出 targetLine、targetDisplayCol(paintInput 用)、targetCharCol(prompt 用),
|
|
750
|
+
* 故 visitColToCharCol 那一套反推只是用于把 click 屏幕列换成字符偏移,与 paintInput 的视协议完全对齐。
|
|
751
|
+
*
|
|
752
|
+
* 点击行为:
|
|
753
|
+
* - 屏幕 visRow/visCol 用 charWidth 计列(中文/Emoji 全角占 2);落在宽字符中段归到字符左边界。
|
|
754
|
+
* - 累加 flat[0..flatIdx-1] 的 display_w + 字符长度 → targetDisplayCol/targetCharCol,与 paintInput 渲染一致。
|
|
755
|
+
* - 段接缝点击因 paintInput 协议限制(<= 还是 <),会落到前段末;这是 paintInput 全局行为,不归本函数。
|
|
756
|
+
* - 未注册 handler(非输入态)→ 仅视觉真光标可见;cl/cc 不变。 */
|
|
757
|
+
function setInputCursorFromClick(screenRow, screenCol) {
|
|
758
|
+
if (!active || !base || !lastView)
|
|
759
|
+
return;
|
|
760
|
+
// running 态:先收归输入态(enterInputMode 会 paintInput 整帧重画;之后我们再按新光标重画一次覆盖即可)
|
|
761
|
+
if (mode !== 'input') {
|
|
762
|
+
enterInputMode(statusText);
|
|
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;
|
|
791
|
+
const g = getGeo();
|
|
792
|
+
const promptW = displayWidth(lastView.prompt);
|
|
793
|
+
const firstInputRow = g.contentBottom + 4;
|
|
794
|
+
const inputRowsAvail = Math.max(0, g.footerH - 5);
|
|
795
|
+
// 输入框可视区的 (visRow, visCol) 屏幕坐标 → 0-based。
|
|
796
|
+
// 点到可视区末行之下(空白区)→ 落到最末可视行(光标归最后一段);isInputRow 已挡可视区之上的点击。
|
|
797
|
+
const visRow = Math.max(0, Math.min(screenRow - firstInputRow, Math.max(0, inputRowsAvail - 1)));
|
|
798
|
+
// 屏幕列 (1-based SGR) → 显示列 (0-based),再扣 prompt 宽;尾点容许越界 clip 到 >=0。
|
|
799
|
+
const visCol = Math.max(0, screenCol - 1 - promptW);
|
|
800
|
+
// 复刻 windowInputVis 的折行 + 滚动窗(输入态, lines 全量参与)。
|
|
801
|
+
// lastView.lines 在 prompt 的 redraw() 中为 dispLines()(含 chip pre 行 / chip prefix 列);
|
|
802
|
+
// chip 模式下点击 chip 区域也走同一算法,与 paintInput 的视协议(段接缝除外)一致。
|
|
803
|
+
const cols = Math.max(1, g.cols - promptW);
|
|
804
|
+
const lineVis = lastView.lines.map((l) => wrapByDisplayWidth(l, cols));
|
|
805
|
+
const flat = [];
|
|
806
|
+
for (const lv of lineVis)
|
|
807
|
+
for (const r of lv)
|
|
808
|
+
flat.push(r);
|
|
809
|
+
const totalVis = flat.length;
|
|
810
|
+
const maxInputRows = Math.max(1, Math.floor(g.rows * 0.4));
|
|
811
|
+
// 起窗偏移:用 lastView 当前光标位置作为锚(与 paintInput 同算法)。
|
|
812
|
+
let curVisLine = 0;
|
|
813
|
+
{
|
|
814
|
+
const clRows = lineVis[lastView.cursorLine] ?? [''];
|
|
815
|
+
let acc = 0;
|
|
816
|
+
for (let i = 0; i < clRows.length; i++) {
|
|
817
|
+
const rw = displayWidth(clRows[i]);
|
|
818
|
+
if (lastView.cursorCol <= acc + rw) {
|
|
819
|
+
curVisLine = i;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
acc += rw;
|
|
823
|
+
curVisLine = i;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
let curAbs = curVisLine;
|
|
827
|
+
for (let i = 0; i < lastView.cursorLine; i++)
|
|
828
|
+
curAbs += lineVis[i].length;
|
|
829
|
+
const startVis = totalVis > maxInputRows
|
|
830
|
+
? Math.max(0, Math.min(curAbs - maxInputRows + 1, totalVis - maxInputRows))
|
|
831
|
+
: 0;
|
|
832
|
+
// 点到可视区末行之下(空行)→ 等价"光标在最末可视行的字符串末尾"。
|
|
833
|
+
if (startVis + visRow >= totalVis) {
|
|
834
|
+
const lastFlatIdx = totalVis - 1;
|
|
835
|
+
// 把 lastFlatIdx 反推到 (line, segInLine)
|
|
836
|
+
let lastLine = 0;
|
|
837
|
+
let lastSegInLine = lastFlatIdx;
|
|
838
|
+
while (lastLine < lineVis.length && lastSegInLine >= lineVis[lastLine].length) {
|
|
839
|
+
lastSegInLine -= lineVis[lastLine].length;
|
|
840
|
+
lastLine++;
|
|
841
|
+
}
|
|
842
|
+
if (lastLine >= lineVis.length) {
|
|
843
|
+
lastLine = lineVis.length - 1;
|
|
844
|
+
lastSegInLine = lineVis[lastLine].length - 1;
|
|
845
|
+
}
|
|
846
|
+
// lines[lastLine] 内 display_col = 前 segInLine 段 display_w + 末段 display_w
|
|
847
|
+
let inLineDisplayCol = 0;
|
|
848
|
+
for (let s = 0; s <= lastSegInLine; s++)
|
|
849
|
+
inLineDisplayCol += displayWidth(lineVis[lastLine][s] ?? '');
|
|
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
|
+
};
|
|
884
|
+
}
|
|
615
885
|
/**
|
|
616
886
|
* 鼠标事件分发(mouse.setHandler 注册)。
|
|
617
887
|
* - 左键(button 0):内容区按下开选区、拖动扩展(触边自动翻页)、释放只更新高亮**不复制**;
|
|
618
|
-
* 纯点击(未拖动)清空旧选区(点别处的常见预期)
|
|
619
|
-
*
|
|
888
|
+
* 纯点击(未拖动)清空旧选区(点别处的常见预期);落在输入行 → 自动进入输入态 + 光标定位到点击位
|
|
889
|
+
* (沿用 windowInputVis 的软折规则做逆解,支持多行 / 中文宽字符;右键 release 落输入行仍贴入)。
|
|
620
890
|
* - 右键(button 2,单击 = press→release 未拖动):落在输入行 → 读剪贴板贴入(仿常见终端"右键粘贴");
|
|
621
891
|
* 落在内容区 → 复制当前选区(若有)到剪贴板后立即清空高亮(视觉确认"已复制",不留旧选区误导),
|
|
622
892
|
* 静默不弹提示;都无对应状态则 no-op。
|
|
@@ -636,9 +906,20 @@ function handleMouseEvent(e) {
|
|
|
636
906
|
return;
|
|
637
907
|
const g = getGeo();
|
|
638
908
|
const col = Math.max(0, e.col - 1); // SGR 报表列 1-based → 显示列 0-based
|
|
639
|
-
// 右键释放:落输入行 →
|
|
909
|
+
// 右键释放:落输入行 → 优先复制当前输入框选区(若有),退化为贴入;落内容区 → 复制选区后清高亮。
|
|
640
910
|
if (e.type === 'release' && e.button === 2) {
|
|
641
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
|
+
}
|
|
642
923
|
pasteIntoInput();
|
|
643
924
|
return;
|
|
644
925
|
}
|
|
@@ -656,17 +937,49 @@ function handleMouseEvent(e) {
|
|
|
656
937
|
if (e.button !== 0)
|
|
657
938
|
return; // 其余中/右键 press/drag 不处理(终端原生右键菜单等不受影响)
|
|
658
939
|
if (e.type === 'press') {
|
|
659
|
-
if (isInputRow(e.row))
|
|
660
|
-
|
|
940
|
+
if (isInputRow(e.row)) {
|
|
941
|
+
// 左键落输入行:进输入态 + 光标定位到点击位 + 开输入框反白选区(起点 = 终点 = 点击位)。
|
|
942
|
+
// 不再 return——后续 drag/release 分支也要知道选区在输入框。点击未拖动时 release 端会清掉选区,
|
|
943
|
+
// 与"点一下就定位光标、起手打字"的习惯一致。
|
|
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
|
+
}
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
661
960
|
selecting = true;
|
|
662
961
|
const rowInContent = Math.max(1, Math.min(e.row, g.contentBottom));
|
|
663
962
|
const absLine = screenRowToAbsLine(rowInContent);
|
|
664
963
|
selection = { anchorLine: absLine, anchorCol: col, endLine: absLine, endCol: col, dragged: false };
|
|
964
|
+
// 切内容区选区时清掉输入框旧选区(若有)。
|
|
965
|
+
inputSelection = null;
|
|
665
966
|
repaintViewport();
|
|
666
967
|
repaint(); // 补一次输入区重画:INPUT 态 repaintViewport 把真光标留在内容区续写位,须靠 repaint 把它带回输入框(否则点内容区会现假闪烁光标,见 issue)
|
|
667
968
|
return;
|
|
668
969
|
}
|
|
669
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
|
+
}
|
|
670
983
|
if (!selecting || !selection)
|
|
671
984
|
return;
|
|
672
985
|
// 触边自动翻页:motion 事件持续到达时靠此逐步滚动,把选区扩展到滚出去的历史行。
|
|
@@ -686,13 +999,44 @@ function handleMouseEvent(e) {
|
|
|
686
999
|
}
|
|
687
1000
|
// release(左键)
|
|
688
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
|
+
}
|
|
689
1012
|
if (!selection)
|
|
690
1013
|
return;
|
|
691
1014
|
if (!selection.dragged) {
|
|
692
|
-
// 内容区纯点击(未拖动)
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
1015
|
+
// 内容区纯点击(未拖动):若落在工具 batch 摘要行上 → 切换展开/折叠(仿 Claude Code);
|
|
1016
|
+
// 否则原行为:清选区。batch 反查通过 content lineAt + dynamic import(避免 layout↔batch 循环依赖)。
|
|
1017
|
+
const absClick = selection.anchorLine; // 起止同行同列,取任一;未拖动时 line = anchor = end
|
|
1018
|
+
void (async () => {
|
|
1019
|
+
try {
|
|
1020
|
+
const m = await import('./batch.js');
|
|
1021
|
+
const id = m.findBatchByAbsLine(absClick);
|
|
1022
|
+
if (id) {
|
|
1023
|
+
m.toggleBatch(id, {
|
|
1024
|
+
contentInsertAfter: (after, lines) => contentInsertAfter(after, lines),
|
|
1025
|
+
contentDeleteFrom: (start, n) => contentDeleteFrom(start, n),
|
|
1026
|
+
});
|
|
1027
|
+
selection = null;
|
|
1028
|
+
repaintViewport();
|
|
1029
|
+
repaint();
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
catch {
|
|
1034
|
+
// batch 不可用(非 TTY 等)→ 走默认清选区路径
|
|
1035
|
+
}
|
|
1036
|
+
selection = null;
|
|
1037
|
+
repaintViewport();
|
|
1038
|
+
repaint();
|
|
1039
|
+
})();
|
|
696
1040
|
return;
|
|
697
1041
|
}
|
|
698
1042
|
// 拖动过:保留高亮选区供右键复制(不在此处复制,复制交给右键释放分支)。
|
|
@@ -735,7 +1079,7 @@ function twoColumn(leftStr, leftW, rightStr, rightW, cols) {
|
|
|
735
1079
|
* INPUT: ◆ 空闲
|
|
736
1080
|
* 思考中: ⠹ 思考中… 0.5s
|
|
737
1081
|
* 运行心跳: ♥ 0.5s
|
|
738
|
-
* 滚动回看: ◆
|
|
1082
|
+
* 滚动回看: ◆ 空闲 历史 ↑3 (PgDn 回底) */
|
|
739
1083
|
function composeSpinnerLine(status, cols) {
|
|
740
1084
|
const spinning = mode === 'running' && runningFrame >= 0;
|
|
741
1085
|
const hasSpinner = !!status.spinnerFrame;
|
|
@@ -748,9 +1092,13 @@ function composeSpinnerLine(status, cols) {
|
|
|
748
1092
|
let lead;
|
|
749
1093
|
let leadW;
|
|
750
1094
|
if (scrolled) {
|
|
751
|
-
//
|
|
752
|
-
|
|
753
|
-
|
|
1095
|
+
// 滚动回看:左段 = 符号(◆ 或 心跳帧) + 状态名(灰,无走时);右段 = 历史指示。
|
|
1096
|
+
// 跟非回看的 INPUT/RUNNING 态保持一致——避免「◆ 留下、状态字蒸发」的视觉错觉。
|
|
1097
|
+
const symbol = spinning
|
|
1098
|
+
? `${ui.brightMagenta}${RUNNING_FRAMES[runningFrame]}${ui.reset}`
|
|
1099
|
+
: `${ui.brightCyan}◆${ui.reset}`;
|
|
1100
|
+
lead = `${symbol} ${ui.dim}${status.status}${ui.reset}`;
|
|
1101
|
+
leadW = 1 + 1 + displayWidth(status.status);
|
|
754
1102
|
}
|
|
755
1103
|
else if (hasSpinner) {
|
|
756
1104
|
// spinner 激活(思考中/执行工具…):帧 + 状态 + 走时
|
|
@@ -896,20 +1244,6 @@ function stopTurnTimer() {
|
|
|
896
1244
|
turnTimer = null;
|
|
897
1245
|
}
|
|
898
1246
|
}
|
|
899
|
-
// ── 临时诊断:spinner frame 泄漏追踪(定位 557e678 后的间歇性泄漏后删除)──
|
|
900
|
-
// 记 paintLiveAtCursor/clearLiveAtCursor 的可疑时序:续写位漂移、clear 被守卫跳过、清错行。
|
|
901
|
-
// 同步追加到 ~/.mocode/spinner-debug.log,全 try/catch 不抛、不阻塞、不抢屏。
|
|
902
|
-
let _dbgSpinnerPath = '';
|
|
903
|
-
function dbgSpinner(msg) {
|
|
904
|
-
try {
|
|
905
|
-
if (!_dbgSpinnerPath)
|
|
906
|
-
_dbgSpinnerPath = join(homedir(), '.mocode', 'spinner-debug.log');
|
|
907
|
-
appendFileSync(_dbgSpinnerPath, `[${new Date().toISOString()}] ${msg}\n`);
|
|
908
|
-
}
|
|
909
|
-
catch {
|
|
910
|
-
// 诊断日志失败不影响渲染
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
1247
|
/**
|
|
914
1248
|
* 在续写位画一行瞬时活动文本(spinner 帧):不进缓冲、不推进续写位,逐行 clearLine 重画。
|
|
915
1249
|
* 仅 TTY + offset=0(实时尾)+ 非打字暂停态时物理写屏;滚动态跳过(由状态行 spinner 兜底,且避免覆盖 viewport 历史行)。
|
|
@@ -951,10 +1285,6 @@ export function paintLiveAtCursor(text) {
|
|
|
951
1285
|
if (scrollOffset === 0 && contentRow < g.contentBottom) {
|
|
952
1286
|
contentRow = Math.min(total, g.contentBottom);
|
|
953
1287
|
}
|
|
954
|
-
// 临时诊断:续写位 != 上次画帧位置 = spinner 运行期间续写位漂移(泄漏根因嫌疑)
|
|
955
|
-
if (frameRow && (frameRow !== contentRow || frameCol !== contentCol)) {
|
|
956
|
-
dbgSpinner(`DRIFT-PAINT old=(${frameRow},${frameCol}) cur=(${contentRow},${contentCol}) mode=${mode} off=${scrollOffset}`);
|
|
957
|
-
}
|
|
958
1288
|
let out = '';
|
|
959
1289
|
if (frameRow && (frameRow !== contentRow || frameCol !== contentCol)) {
|
|
960
1290
|
// 清旧帧同样钳到可视区(resize 后 frameRow 可能 > contentBottom)
|
|
@@ -1046,30 +1376,37 @@ function windowInputVis(lines, cursorLine, cursorCol, cols, promptW, rows) {
|
|
|
1046
1376
|
startVis,
|
|
1047
1377
|
};
|
|
1048
1378
|
}
|
|
1379
|
+
/** 把 lines 按 W 做软折,返回 lineVis;供 paintInput 高亮预算用。与 windowInputVis 复用 wrap 算法。 */
|
|
1380
|
+
function visInputLines(lines, W) {
|
|
1381
|
+
return lines.map((l) => wrapByDisplayWidth(l, W));
|
|
1382
|
+
}
|
|
1049
1383
|
/**
|
|
1050
|
-
*
|
|
1051
|
-
*
|
|
1052
|
-
*
|
|
1053
|
-
*
|
|
1054
|
-
* 光标列恒落在字符边界:cursorCol = 显示宽度(slice 整字累加),不进字符内部,故按 acc===col 取字符即可。
|
|
1055
|
-
* 宽字符(CJK=2)在行尾放不下时整字折下行,光标列仍在边界,acc 逐字累加 displayWidth 与终端光标一致。
|
|
1384
|
+
* 给纯文本(无 ANSI,来自 wrap 后的可视段)做"显示列区间"高亮,返回插入 SEL_OPEN/SEL_OFF 后的字符串。
|
|
1385
|
+
* 输入字段 vis 段宽 = displayWidth(seg),clipStart/clipEnd 单位 = display_col。
|
|
1386
|
+
* 与 highlightRange 等价语义但省去 ANSI split(纯文本无 SGR),更省。
|
|
1056
1387
|
*/
|
|
1057
|
-
function
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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) {
|
|
1061
1395
|
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
1062
|
-
if (
|
|
1063
|
-
|
|
1064
|
-
|
|
1396
|
+
if (!opened && w >= clipStart && w < clipEnd) {
|
|
1397
|
+
out += SEL_OPEN;
|
|
1398
|
+
opened = true;
|
|
1065
1399
|
}
|
|
1066
|
-
if (
|
|
1067
|
-
|
|
1400
|
+
if (opened && w >= clipEnd) {
|
|
1401
|
+
out += SEL_OFF;
|
|
1402
|
+
opened = false;
|
|
1068
1403
|
}
|
|
1069
|
-
|
|
1070
|
-
|
|
1404
|
+
out += ch;
|
|
1405
|
+
w += cw;
|
|
1071
1406
|
}
|
|
1072
|
-
|
|
1407
|
+
if (opened)
|
|
1408
|
+
out += SEL_OFF;
|
|
1409
|
+
return out;
|
|
1073
1410
|
}
|
|
1074
1411
|
/**
|
|
1075
1412
|
* 画输入区:擦旧菜单 → (必要时)setRegion → 画状态行 + 输入行 + 向上菜单,光标留输入框(dim 时回续写位)。
|
|
@@ -1079,6 +1416,18 @@ export function paintInput(view) {
|
|
|
1079
1416
|
if (!active || !base)
|
|
1080
1417
|
return;
|
|
1081
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
|
+
}
|
|
1082
1431
|
const preGeo = getGeo();
|
|
1083
1432
|
// 累积本帧所有 cup/clearLine/文本,末尾一次写出——避免「擦旧菜单」与「重画」分多次 write 时,
|
|
1084
1433
|
// 终端把中间空白渲染出来 → 菜单/面板切换时闪烁(↑↓ 导航每次 redraw 都全帧擦后重画)。
|
|
@@ -1140,7 +1489,29 @@ export function paintInput(view) {
|
|
|
1140
1489
|
const firstInputRow = g.contentBottom + 4;
|
|
1141
1490
|
const inputRowsAvail = g.footerH - 5; // 去掉虚拟空/spinner行/上线/下线/model行,留输入行
|
|
1142
1491
|
const indent = ' '.repeat(promptW);
|
|
1143
|
-
|
|
1492
|
+
// 光标:不画反白块/假光标——输入框走终端真光标(WT / VSCode 终端默认竖线/闪烁块,
|
|
1493
|
+
// 各终端表现略不同但都贴合 IME 候选气泡且不再"挡住字符")。真光标位置由下方第 6 步 cup 写。
|
|
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
|
+
}
|
|
1144
1515
|
for (let i = 0; i < inputRowsAvail; i++) {
|
|
1145
1516
|
const line = vis.visRows[i] ?? '';
|
|
1146
1517
|
const r = firstInputRow + i;
|
|
@@ -1149,13 +1520,24 @@ export function paintInput(view) {
|
|
|
1149
1520
|
if (view.dim) {
|
|
1150
1521
|
text = renderDimInputRow(view.prompt, line, view.placeholder ?? '', g.cols);
|
|
1151
1522
|
}
|
|
1152
|
-
else if (showCaret && i === vis.visLine) {
|
|
1153
|
-
// 块状光标:反白光标右侧字符(cur),行末(无字符)反白一个空格——示"现在在哪输入"
|
|
1154
|
-
const { before, cur, after } = splitAtVisCol(line, vis.cursorVisCol);
|
|
1155
|
-
text = `${prefix}${before}${ui.reverse}${cur || ' '}${ui.reset}${after}`;
|
|
1156
|
-
}
|
|
1157
1523
|
else {
|
|
1158
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
|
+
}
|
|
1159
1541
|
}
|
|
1160
1542
|
buf += cup(r, 1) + esc.clearLine + text;
|
|
1161
1543
|
}
|
|
@@ -1195,16 +1577,17 @@ export function paintInput(view) {
|
|
|
1195
1577
|
*/
|
|
1196
1578
|
function renderDimInputRow(prompt, text, placeholder, cols) {
|
|
1197
1579
|
const promptW = displayWidth(prompt);
|
|
1198
|
-
|
|
1199
|
-
|
|
1580
|
+
// dim 态(运行中打字):不画反白块——反白块视觉占位是 INPUT 态真光标的旧版,这里真光标本
|
|
1581
|
+
// 就被隐起来(IME 锚定需要,见 contentMode),在 dim 文本后硬塞个反白空格会"白块闪烁"。
|
|
1582
|
+
// 用户读 dim 文本本身就能定位打字边界;真光标在 INPUT 态显形(竖线/闪烁块,跟终端默认一致)。
|
|
1583
|
+
const contentW = Math.max(0, cols - promptW);
|
|
1200
1584
|
if (text.length > 0) {
|
|
1201
|
-
// 有打字:❯ dim + 文本(dim
|
|
1202
|
-
|
|
1203
|
-
return `${ui.dim}${prompt}${truncateDisplayHead(text, contentW)}${ui.reset}${caret}`;
|
|
1585
|
+
// 有打字:❯ dim + 文本(dim,超长从头部截断保留尾部——光标恒在末尾,须始终看到刚打的字)
|
|
1586
|
+
return `${ui.dim}${prompt}${truncateDisplayHead(text, contentW)}${ui.reset}`;
|
|
1204
1587
|
}
|
|
1205
|
-
// 空:❯ dim +
|
|
1588
|
+
// 空:❯ dim + dim 占位 ghost(taking the full row)
|
|
1206
1589
|
const p = placeholder ? truncateDisplay(placeholder, contentW) : '';
|
|
1207
|
-
return `${ui.dim}${prompt}${
|
|
1590
|
+
return `${ui.dim}${prompt}${p}${ui.reset}`;
|
|
1208
1591
|
}
|
|
1209
1592
|
/**
|
|
1210
1593
|
* 运行态 typeahead 回显:定向写输入行(底栏输入框),把 dim 占位换成已打字文本 + 反白块状光标(无打字时光标在起点)。
|