mocode-ai 0.1.9 → 0.2.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 +139 -127
- package/README.zh-CN.md +202 -0
- package/dist/agent/index.js +5 -10
- package/dist/config/index.js +9 -6
- package/dist/repl/index.js +12 -10
- package/dist/tools/builtins/codegraph.js +5 -3
- package/dist/ui/clipboard.js +118 -0
- package/dist/ui/content.js +5 -0
- package/dist/ui/intervention.js +7 -10
- package/dist/ui/layout.js +401 -93
- package/dist/ui/mouse.js +67 -70
- package/dist/ui/prompt.js +95 -31
- package/dist/ui/render.js +17 -0
- package/package.json +40 -40
package/dist/ui/mouse.js
CHANGED
|
@@ -1,93 +1,90 @@
|
|
|
1
|
-
// SGR 鼠标报表重组器(
|
|
1
|
+
// SGR 鼠标报表重组器 + 事件分发(叶子:仅正则 + 回调,无 UI 依赖)。
|
|
2
2
|
//
|
|
3
|
-
// 背景:layout 进 alt 屏时发 \x1B[?1000h + \x1B[?
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// 喂入 consumeMouse,内部状态机把 fragment 拼回完整报表,期间所有 fragment 返回 suppress=true
|
|
8
|
-
// 让调用方吞掉,完整报表解出滚轮方向后返回 wheel(±1)由调用方调 layout.scrollBy(wheel*5)。
|
|
3
|
+
// 背景:layout 进 alt 屏时发 \x1B[?1000h + \x1B[?1002h + \x1B[?1006h,启用:
|
|
4
|
+
// 1000 = 按键事件追踪(按下 / 释放),1002 = 拖动追踪(按住键移动时上报 motion),
|
|
5
|
+
// 1006 = SGR 编码 → \x1B[<btn;col;rowM(按下 / 拖动)或 \x1B[<btn;col;rowm(释放)。
|
|
6
|
+
// 有了 1002 的拖动上报,才能实现"按住左键拖过多屏 → 应用层维护选区 → 松开复制"(仿 Claude Code)。
|
|
9
7
|
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
// 但 Node readline 的 emitKeypressEvents 不认 `<` 为 CSI 参数字节,把一条报表拆成 `\x1b[<` +
|
|
9
|
+
// 逐字符共 ≥9 个 keypress(数字 / `;` / `M` 有可打印 name,会被当文本砸进输入框)。故本模块在
|
|
10
|
+
// keypress 层重组:每个 keypress 的 key.sequence 喂入 swallow,内部状态机把 fragment 拼回完整报表,
|
|
11
|
+
// 期间所有 fragment 返回 true(调用方须 return 吞掉,防砸进输入框);报表拼齐后解析成结构化
|
|
12
|
+
// MouseEvent 派发给已注册的 handler(layout 注册,做滚动 / 选区 / 复制)。
|
|
13
|
+
//
|
|
14
|
+
// 触发可靠:首 fragment `\x1b[<` 是 SGR 鼠标独有——真 Esc 的 sequence==='\x1b';人按 Esc 再按 <
|
|
15
|
+
// 得 `\x1b<`(meta-<),不进 CSI 分支。故 `\x1b[<` 开头只能是 SGR 报表。
|
|
16
|
+
/** 单条完整报表(捕获 button / col / row / 终止符 M|m)。 */
|
|
17
|
+
const REPORT_RE = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/;
|
|
18
|
+
/** 全局版:一个 chunk 里可能背靠背多条(拖动 motion 连发),matchAll 取全部。 */
|
|
19
|
+
const REPORT_RE_G = /\x1b\[<(\d+);(\d+);(\d+)([Mm])/g;
|
|
17
20
|
/** 收集中 buf 允许的形状:\x1b[< 后跟 [0-9;] 任意个,末尾可选一个 M/m(未完成或刚完成)。 */
|
|
18
21
|
const PARTIAL_RE = /^\x1b\[<[0-9;]*[Mm]?$/;
|
|
19
22
|
const START = '\x1b[<';
|
|
20
|
-
|
|
21
|
-
function wheelDir(button) {
|
|
22
|
-
if (!(button & 64))
|
|
23
|
-
return 0;
|
|
24
|
-
return button & 1 ? -1 : +1;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* 纯函数:从可能含 ≥1 条完整报表的串里取末条滚轮方向(+1 上 / -1 下 / 0 无或非滚轮)。
|
|
28
|
-
* 供"整体抛出整条报表"的前向兼容分支与测试用(当前 Node 拆碎,正常路径走 consumeMouse)。
|
|
29
|
-
*/
|
|
30
|
-
export function wheelFromReport(seq) {
|
|
31
|
-
let last = 0;
|
|
32
|
-
for (const m of seq.matchAll(REPORT_RE_G)) {
|
|
33
|
-
last = wheelDir(Number(m[1]));
|
|
34
|
-
}
|
|
35
|
-
return last;
|
|
36
|
-
}
|
|
23
|
+
let handler = null;
|
|
37
24
|
let collecting = false;
|
|
38
25
|
let buf = '';
|
|
39
|
-
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
buf = '';
|
|
43
|
-
if (stallTimer) {
|
|
44
|
-
clearTimeout(stallTimer);
|
|
45
|
-
stallTimer = null;
|
|
46
|
-
}
|
|
26
|
+
/** 注册事件回调(layout 进 alt 屏时注册)。 */
|
|
27
|
+
export function setHandler(fn) {
|
|
28
|
+
handler = fn;
|
|
47
29
|
}
|
|
48
30
|
/** 复位内部收集状态(退出 alt 屏 / 测试间清污染)。 */
|
|
49
31
|
export function resetMouse() {
|
|
50
|
-
|
|
32
|
+
collecting = false;
|
|
33
|
+
buf = '';
|
|
34
|
+
}
|
|
35
|
+
/** button 位:64=滚轮(&1 区分上下),32=拖动 motion,低 2 位=键(0=左,1=中,2=右)。 */
|
|
36
|
+
function emit(button, col, row, term) {
|
|
37
|
+
if (!handler)
|
|
38
|
+
return;
|
|
39
|
+
if (button & 64) {
|
|
40
|
+
handler({ type: 'wheel', dir: button & 1 ? -1 : +1 });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const btn = button & 3;
|
|
44
|
+
if (term === 'm') {
|
|
45
|
+
handler({ type: 'release', col, row, button: btn });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (button & 32) {
|
|
49
|
+
handler({ type: 'drag', col, row, button: btn });
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
handler({ type: 'press', col, row, button: btn });
|
|
51
53
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
t.unref();
|
|
58
|
-
stallTimer = t;
|
|
54
|
+
/** 解出串中所有完整报表并按序派发(拖动 motion 一个 chunk 多条时全处理)。 */
|
|
55
|
+
function dispatchAll(s) {
|
|
56
|
+
for (const m of s.matchAll(REPORT_RE_G)) {
|
|
57
|
+
emit(Number(m[1]), Number(m[2]), Number(m[3]), m[4]);
|
|
58
|
+
}
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
|
-
* 喂入一个 keypress 的 key.sequence
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* - suppress=false:非鼠标,放行正常处理。
|
|
61
|
+
* 喂入一个 keypress 的 key.sequence。
|
|
62
|
+
* 返回 true=本 keypress 是鼠标 fragment(或完整报表),调用方须 return 吞掉;
|
|
63
|
+
* 返回 false=非鼠标,放行正常处理。完整报表拼齐时同步派发 MouseEvent 给 handler。
|
|
65
64
|
*/
|
|
66
|
-
export function
|
|
65
|
+
export function swallow(seq) {
|
|
66
|
+
if (!seq)
|
|
67
|
+
return false;
|
|
67
68
|
if (!collecting) {
|
|
68
69
|
if (!seq.startsWith(START))
|
|
69
|
-
return
|
|
70
|
-
//
|
|
71
|
-
if (REPORT_RE.test(seq))
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
return false;
|
|
71
|
+
// 单 keypress 已含完整报表(未来 Node 可能整体抛):直接解。
|
|
72
|
+
if (REPORT_RE.test(seq)) {
|
|
73
|
+
dispatchAll(seq);
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
74
76
|
collecting = true;
|
|
75
77
|
buf = seq;
|
|
76
|
-
|
|
77
|
-
return { wheel: 0, suppress: true };
|
|
78
|
+
return true;
|
|
78
79
|
}
|
|
79
|
-
// 收集中:追加。
|
|
80
80
|
buf += seq;
|
|
81
81
|
if (REPORT_RE.test(buf)) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
return
|
|
85
|
-
}
|
|
86
|
-
if (PARTIAL_RE.test(buf)) {
|
|
87
|
-
// 合法未完成形状(无 M/m 终止)—— 继续等下一 fragment。
|
|
88
|
-
return { wheel: 0, suppress: true };
|
|
82
|
+
dispatchAll(buf);
|
|
83
|
+
resetMouse();
|
|
84
|
+
return true;
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
86
|
+
if (PARTIAL_RE.test(buf))
|
|
87
|
+
return true; // 合法未完成形状,继续等
|
|
88
|
+
resetMouse(); // 偏离合法形状:畸形垃圾,丢半截,吞掉不打扰
|
|
89
|
+
return true;
|
|
93
90
|
}
|
package/dist/ui/prompt.js
CHANGED
|
@@ -20,8 +20,8 @@ function ensurePasteDetector() {
|
|
|
20
20
|
pasteDetectorInstalled = true;
|
|
21
21
|
stdin.on('data', (chunk) => {
|
|
22
22
|
const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
23
|
-
// 剔除 SGR 鼠标报告(\x1B[<…M/m,连半截也匹配)
|
|
24
|
-
//
|
|
23
|
+
// 剔除 SGR 鼠标报告(\x1B[<…M/m,连半截也匹配):拖拽选区时终端连续发 motion 报表,
|
|
24
|
+
// 一个 chunk 里可能拼多条(22+ 字符 > 16 阈值)误判粘贴 50ms、把后续真按键泄进 pasteParts。
|
|
25
25
|
const text = raw.replace(/\x1b\[<[0-9;]*[Mm]/g, '');
|
|
26
26
|
const hasNL = text.indexOf('\r') >= 0 || text.indexOf('\n') >= 0;
|
|
27
27
|
// 按字符数(码点)判粘贴,非字节:CJK 汉字占 3 UTF-8 字节,旧阈值(len>8 字节)会把 IME 提交的
|
|
@@ -80,6 +80,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
80
80
|
let cc = lines[cl].length; // 光标在该行的字符索引 = 末行末尾
|
|
81
81
|
let justSawCR = false; // \r\n 合一:粘贴的 \r 折行后,紧跟的 \n 吞掉(避免折两行)
|
|
82
82
|
let chip = null; // 原子粘贴块:整段封进 [预览…],不可编辑;提交时拼回全文
|
|
83
|
+
let chipPre = ''; // chip 之前已存在的文本(粘贴发生时光标前的原文,原样多行保留,不截断);随 chip 一起提交/删除
|
|
83
84
|
let menuOpen = false;
|
|
84
85
|
let selected = 0;
|
|
85
86
|
let filtered = [];
|
|
@@ -111,16 +112,30 @@ export async function promptWithSlashMenu(opts) {
|
|
|
111
112
|
function chipPrefix() {
|
|
112
113
|
return chip ? `[${truncateDisplay(chip.split('\n').join(' '), 20)}] ` : '';
|
|
113
114
|
}
|
|
114
|
-
/**
|
|
115
|
+
/** chipPre 按行拆分(粘贴发生前光标之前已有的文本,可能多行,原样保留在 chip 之前)。 */
|
|
116
|
+
function chipPreLines() {
|
|
117
|
+
return chip ? chipPre.split('\n') : [];
|
|
118
|
+
}
|
|
119
|
+
/** 供 layout 画的行:chipPre 的行原样排在前面,最后一行接上 chip 前缀 + suffix 第 0 行
|
|
120
|
+
* (chip 原子显示,不可编辑;chipPre 同样不可直接编辑,光标只活在 suffix)。 */
|
|
115
121
|
function dispLines() {
|
|
116
122
|
if (!chip)
|
|
117
123
|
return lines;
|
|
118
|
-
const pre =
|
|
119
|
-
|
|
124
|
+
const pre = chipPreLines();
|
|
125
|
+
const lastPre = pre[pre.length - 1];
|
|
126
|
+
const mergedRow = lastPre + chipPrefix() + lines[0];
|
|
127
|
+
return [...pre.slice(0, -1), mergedRow, ...lines.slice(1)];
|
|
128
|
+
}
|
|
129
|
+
/** 供 layout 定位光标行:chipPre 多出的行数计入偏移(suffix 的 cl=0 对应 chipPre 最后一行所在的合并行)。 */
|
|
130
|
+
function dispCursorLine() {
|
|
131
|
+
return chip ? chipPreLines().length - 1 + cl : cl;
|
|
120
132
|
}
|
|
121
|
-
/** 供 layout 定位光标列:chip
|
|
133
|
+
/** 供 layout 定位光标列:chip 在合并行(cl===0)时偏移 chipPre 末行宽度 + chipPrefix 宽度。 */
|
|
122
134
|
function dispCursorCol() {
|
|
123
|
-
|
|
135
|
+
if (!chip || cl !== 0)
|
|
136
|
+
return cursorCol();
|
|
137
|
+
const pre = chipPreLines();
|
|
138
|
+
return displayWidth(pre[pre.length - 1]) + displayWidth(chipPrefix()) + cursorCol();
|
|
124
139
|
}
|
|
125
140
|
/** 在光标处插入文本(含换行则拆行)。供短粘贴 finalize 落为可编辑文本。 */
|
|
126
141
|
function insertText(text) {
|
|
@@ -137,21 +152,27 @@ export async function promptWithSlashMenu(opts) {
|
|
|
137
152
|
// 旧值 parts[...].length 漏算 before → 光标落到插入文本内部(行中间)→ 后续打字插到中间(bug)。
|
|
138
153
|
cc = before.length + parts[parts.length - 1].length;
|
|
139
154
|
}
|
|
140
|
-
/**
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const buf = pasteParts.join('');
|
|
147
|
-
pasteParts = [];
|
|
148
|
-
justSawCR = false;
|
|
155
|
+
/** 落一段粘贴文本(长/短判定与 chip 逻辑,finalizePaste 与鼠标点击贴入共用)。
|
|
156
|
+
* 长粘贴落 chip 时不再吞掉用户已打的字:光标前的文本并入 chipPre(原样保留,与 chip 一起原子化),
|
|
157
|
+
* 光标后的文本留在 suffix(lines)——即"前段文字 [长文本…] 后段文字"而非清空覆盖。 */
|
|
158
|
+
function applyPastedText(buf) {
|
|
149
159
|
if (!buf)
|
|
150
160
|
return;
|
|
151
161
|
const isLong = buf.split('\n').length > 8 || buf.length > 400;
|
|
152
162
|
if (isLong) {
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
const before = lines[cl].slice(0, cc);
|
|
164
|
+
const after = lines[cl].slice(cc);
|
|
165
|
+
if (chip == null) {
|
|
166
|
+
// 首次起 chip:光标前的行(含之前的整行)转入 chipPre,光标后的部分留作 suffix 首行。
|
|
167
|
+
chipPre = [...lines.slice(0, cl), before].join('\n');
|
|
168
|
+
chip = buf;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
// 已有 chip:光标前若已打了字(chip 之后、本次粘贴之前),并入 chip 尾部保留(不丢字),
|
|
172
|
+
// 光标后的部分仍留作 suffix 首行。
|
|
173
|
+
chip = before ? chip + '\n' + before + '\n' + buf : chip + '\n' + buf;
|
|
174
|
+
}
|
|
175
|
+
lines = [after, ...lines.slice(cl + 1)];
|
|
155
176
|
cl = 0;
|
|
156
177
|
cc = 0;
|
|
157
178
|
}
|
|
@@ -161,6 +182,23 @@ export async function promptWithSlashMenu(opts) {
|
|
|
161
182
|
computeFiltered();
|
|
162
183
|
redraw();
|
|
163
184
|
}
|
|
185
|
+
/** 粘贴结束:长粘贴(>8 行或 >400 字符)落/并进 chip(原子,整段封预览),短粘贴落为可编辑文本。 */
|
|
186
|
+
function finalizePaste() {
|
|
187
|
+
if (resolved) {
|
|
188
|
+
pasteParts = [];
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const buf = pasteParts.join('');
|
|
192
|
+
pasteParts = [];
|
|
193
|
+
justSawCR = false;
|
|
194
|
+
applyPastedText(buf);
|
|
195
|
+
}
|
|
196
|
+
/** 鼠标右键单击输入框(未拖动)时 layout 读剪贴板后回调:与键盘粘贴走同一落地逻辑(长/短判定)。 */
|
|
197
|
+
function onMousePaste(text) {
|
|
198
|
+
if (resolved)
|
|
199
|
+
return;
|
|
200
|
+
applyPastedText(text.replace(/\r\n/g, '\n').replace(/\r/g, '\n'));
|
|
201
|
+
}
|
|
164
202
|
function computeFiltered() {
|
|
165
203
|
if (cl === 0 && lines[0].startsWith('/')) {
|
|
166
204
|
filtered = opts.commands.filter((c) => c.name.startsWith(lines[0]));
|
|
@@ -177,12 +215,13 @@ export async function promptWithSlashMenu(opts) {
|
|
|
177
215
|
layout.paintInput({
|
|
178
216
|
prompt: opts.prompt,
|
|
179
217
|
lines: dispLines(),
|
|
180
|
-
cursorLine:
|
|
218
|
+
cursorLine: dispCursorLine(),
|
|
181
219
|
cursorCol: dispCursorCol(),
|
|
182
220
|
menu: menuLines().length ? { lines: menuLines() } : null,
|
|
183
221
|
});
|
|
184
222
|
}
|
|
185
223
|
function cleanup() {
|
|
224
|
+
layout.setPasteHandler(null);
|
|
186
225
|
try {
|
|
187
226
|
stdin.setRawMode(false);
|
|
188
227
|
}
|
|
@@ -206,7 +245,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
206
245
|
cleanup();
|
|
207
246
|
resolve(value);
|
|
208
247
|
}
|
|
209
|
-
/** 提交:菜单打开时先补全选中项到第 0 行。chip
|
|
248
|
+
/** 提交:菜单打开时先补全选中项到第 0 行。chipPre + chip + suffix 依次拼回全文(保留粘贴前后原有文字)。 */
|
|
210
249
|
function submit() {
|
|
211
250
|
if (menuOpen && filtered[selected]) {
|
|
212
251
|
lines = [filtered[selected].name];
|
|
@@ -214,7 +253,12 @@ export async function promptWithSlashMenu(opts) {
|
|
|
214
253
|
cc = lines[0].length;
|
|
215
254
|
}
|
|
216
255
|
const suffix = lines.join('\n');
|
|
217
|
-
|
|
256
|
+
let content = suffix;
|
|
257
|
+
if (chip) {
|
|
258
|
+
content = suffix.length > 0 ? chip + '\n' + suffix : chip;
|
|
259
|
+
if (chipPre)
|
|
260
|
+
content = chipPre + '\n' + content;
|
|
261
|
+
}
|
|
218
262
|
finish(content === '' ? [''] : content.split('\n'));
|
|
219
263
|
}
|
|
220
264
|
/** 插换行:在光标处断行。 */
|
|
@@ -235,6 +279,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
235
279
|
cl = 0;
|
|
236
280
|
cc = 0;
|
|
237
281
|
chip = null;
|
|
282
|
+
chipPre = '';
|
|
238
283
|
menuOpen = false;
|
|
239
284
|
filtered = [];
|
|
240
285
|
selected = 0;
|
|
@@ -265,14 +310,10 @@ export async function promptWithSlashMenu(opts) {
|
|
|
265
310
|
function onKey(_str, key) {
|
|
266
311
|
if (resolved || !key)
|
|
267
312
|
return;
|
|
268
|
-
// SGR
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
if (_m.suppress) {
|
|
272
|
-
if (_m.wheel)
|
|
273
|
-
layout.scrollBy(_m.wheel * 5);
|
|
313
|
+
// 鼠标 fragment(SGR 报表被 readline 拆碎):mouse.swallow 重组并派发 MouseEvent 给
|
|
314
|
+
// layout.handleMouseEvent(滚轮/框选/复制全在那处理);此处只需吞掉 fragment 不进 pasteParts/输入框。
|
|
315
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
274
316
|
return;
|
|
275
|
-
}
|
|
276
317
|
// Shift+Tab:循环切换 agent 模式(auto ↔ plan)。不插字符、不提交、不影响输入文本;
|
|
277
318
|
// 回调由 repl 注入(翻 agentMode + 重写 history[0] + 设状态行 modeTag),再 redraw() 经
|
|
278
319
|
// paintInput 重画底栏(状态行 chip 即时刷新 + 光标留输入框)。置于 case 'tab' 之前,故不触发菜单补全。
|
|
@@ -306,7 +347,7 @@ export async function promptWithSlashMenu(opts) {
|
|
|
306
347
|
}
|
|
307
348
|
// 滚动回看键(优先;不触发回尾):PgUp/PgDn 翻页,Ctrl+↑↓ 与 plain ↑/↓ 每次 5 行。
|
|
308
349
|
// plain ↑/↓ 仅在单行输入且菜单关闭时作滚动(多行编辑留给光标移动,菜单打开留给选项);
|
|
309
|
-
//
|
|
350
|
+
// 兼鼠标滚轮——alt 屏内(经 \x1B[?1007h)滚轮转发 ↑/↓,1 行/格太慢故放大到 5。
|
|
310
351
|
const plainArrowScroll = (key.name === 'up' || key.name === 'down') &&
|
|
311
352
|
!key.ctrl &&
|
|
312
353
|
!key.meta &&
|
|
@@ -382,8 +423,14 @@ export async function promptWithSlashMenu(opts) {
|
|
|
382
423
|
redraw();
|
|
383
424
|
}
|
|
384
425
|
else if (chip) {
|
|
385
|
-
// 光标在 suffix 开头(紧贴 ] 后):退格删整个 chip(原子)
|
|
426
|
+
// 光标在 suffix 开头(紧贴 ] 后):退格删整个 chip(原子),chipPre 还原为可编辑行,
|
|
427
|
+
// 光标落在 chipPre 末尾(紧接原来打的字继续编辑)。
|
|
428
|
+
const preLines = chipPre ? chipPre.split('\n') : [''];
|
|
429
|
+
lines = [...preLines.slice(0, -1), preLines[preLines.length - 1] + lines[0], ...lines.slice(1)];
|
|
430
|
+
cl = preLines.length - 1;
|
|
431
|
+
cc = preLines[preLines.length - 1].length;
|
|
386
432
|
chip = null;
|
|
433
|
+
chipPre = '';
|
|
387
434
|
computeFiltered();
|
|
388
435
|
redraw();
|
|
389
436
|
}
|
|
@@ -478,10 +525,11 @@ export async function promptWithSlashMenu(opts) {
|
|
|
478
525
|
rawOk = false;
|
|
479
526
|
}
|
|
480
527
|
if (!rawOk) {
|
|
481
|
-
// isTTY 但 setRawMode 失败(罕见):退化为 readline
|
|
528
|
+
// isTTY 但 setRawMode 失败(罕见):退化为 readline,不注册 setPasteHandler(不走 cleanup,防泄漏)
|
|
482
529
|
questionFallback(opts.prompt).then((a) => res([a]), (e) => rej(e));
|
|
483
530
|
return;
|
|
484
531
|
}
|
|
532
|
+
layout.setPasteHandler(onMousePaste); // 鼠标右键单击输入框(未拖动)→ 读剪贴板贴入;cleanup 时注销
|
|
485
533
|
stdin.resume();
|
|
486
534
|
emitter.on('keypress', onKey);
|
|
487
535
|
computeFiltered();
|
|
@@ -535,6 +583,7 @@ export async function promptTurnPicker(items) {
|
|
|
535
583
|
});
|
|
536
584
|
}
|
|
537
585
|
function cleanup() {
|
|
586
|
+
layout.setMouseEnabled(true); // 恢复鼠标框选/滚轮(面板期间被禁,防拖拽覆盖菜单)
|
|
538
587
|
try {
|
|
539
588
|
stdin.setRawMode(false);
|
|
540
589
|
}
|
|
@@ -554,6 +603,8 @@ export async function promptTurnPicker(items) {
|
|
|
554
603
|
function onKey(_str, key) {
|
|
555
604
|
if (resolved || !key)
|
|
556
605
|
return;
|
|
606
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
607
|
+
return; // 鼠标 fragment 吞掉(框选已禁,滚轮 handleMouseEvent no-op)
|
|
557
608
|
if (key.ctrl && key.name === 'c') {
|
|
558
609
|
cleanup();
|
|
559
610
|
reject(new Error('SIGINT'));
|
|
@@ -584,6 +635,7 @@ export async function promptTurnPicker(items) {
|
|
|
584
635
|
return new Promise((res, rej) => {
|
|
585
636
|
resolve = res;
|
|
586
637
|
reject = rej;
|
|
638
|
+
layout.setMouseEnabled(false); // 面板期间禁鼠标框选/滚轮(防拖拽 viewport 重画覆盖菜单)
|
|
587
639
|
ensurePasteDetector();
|
|
588
640
|
readline.emitKeypressEvents(stdin);
|
|
589
641
|
let rawOk = true;
|
|
@@ -672,6 +724,7 @@ export async function promptSessionPicker(items, recentCap = 10) {
|
|
|
672
724
|
});
|
|
673
725
|
}
|
|
674
726
|
function cleanup() {
|
|
727
|
+
layout.setMouseEnabled(true);
|
|
675
728
|
try {
|
|
676
729
|
stdin.setRawMode(false);
|
|
677
730
|
}
|
|
@@ -691,6 +744,8 @@ export async function promptSessionPicker(items, recentCap = 10) {
|
|
|
691
744
|
function onKey(_str, key) {
|
|
692
745
|
if (resolved || !key)
|
|
693
746
|
return;
|
|
747
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
748
|
+
return;
|
|
694
749
|
if (key.ctrl && key.name === 'c') {
|
|
695
750
|
cleanup();
|
|
696
751
|
reject(new Error('SIGINT'));
|
|
@@ -731,6 +786,7 @@ export async function promptSessionPicker(items, recentCap = 10) {
|
|
|
731
786
|
return new Promise((res, rej) => {
|
|
732
787
|
resolve = res;
|
|
733
788
|
reject = rej;
|
|
789
|
+
layout.setMouseEnabled(false);
|
|
734
790
|
ensurePasteDetector();
|
|
735
791
|
readline.emitKeypressEvents(stdin);
|
|
736
792
|
let rawOk = true;
|
|
@@ -806,6 +862,7 @@ export async function promptThemePicker(items) {
|
|
|
806
862
|
});
|
|
807
863
|
}
|
|
808
864
|
function cleanup() {
|
|
865
|
+
layout.setMouseEnabled(true);
|
|
809
866
|
try {
|
|
810
867
|
stdin.setRawMode(false);
|
|
811
868
|
}
|
|
@@ -825,6 +882,8 @@ export async function promptThemePicker(items) {
|
|
|
825
882
|
function onKey(_str, key) {
|
|
826
883
|
if (resolved || !key)
|
|
827
884
|
return;
|
|
885
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
886
|
+
return;
|
|
828
887
|
if (key.ctrl && key.name === 'c') {
|
|
829
888
|
cleanup();
|
|
830
889
|
reject(new Error('SIGINT'));
|
|
@@ -856,6 +915,7 @@ export async function promptThemePicker(items) {
|
|
|
856
915
|
return new Promise((res, rej) => {
|
|
857
916
|
resolve = res;
|
|
858
917
|
reject = rej;
|
|
918
|
+
layout.setMouseEnabled(false);
|
|
859
919
|
ensurePasteDetector();
|
|
860
920
|
readline.emitKeypressEvents(stdin);
|
|
861
921
|
let rawOk = true;
|
|
@@ -916,6 +976,7 @@ export async function promptRevertChoice(fileCount) {
|
|
|
916
976
|
});
|
|
917
977
|
}
|
|
918
978
|
function cleanup() {
|
|
979
|
+
layout.setMouseEnabled(true);
|
|
919
980
|
try {
|
|
920
981
|
stdin.setRawMode(false);
|
|
921
982
|
}
|
|
@@ -935,6 +996,8 @@ export async function promptRevertChoice(fileCount) {
|
|
|
935
996
|
function onKey(_str, key) {
|
|
936
997
|
if (resolved || !key)
|
|
937
998
|
return;
|
|
999
|
+
if (mouse.swallow(key.sequence ?? ''))
|
|
1000
|
+
return;
|
|
938
1001
|
if (key.ctrl && key.name === 'c') {
|
|
939
1002
|
cleanup();
|
|
940
1003
|
reject(new Error('SIGINT'));
|
|
@@ -965,6 +1028,7 @@ export async function promptRevertChoice(fileCount) {
|
|
|
965
1028
|
return new Promise((res, rej) => {
|
|
966
1029
|
resolve = res;
|
|
967
1030
|
reject = rej;
|
|
1031
|
+
layout.setMouseEnabled(false);
|
|
968
1032
|
ensurePasteDetector();
|
|
969
1033
|
readline.emitKeypressEvents(stdin);
|
|
970
1034
|
let rawOk = true;
|
package/dist/ui/render.js
CHANGED
|
@@ -82,6 +82,23 @@ export function fmtElapsed(ms) {
|
|
|
82
82
|
export function stripAnsi(s) {
|
|
83
83
|
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
84
84
|
}
|
|
85
|
+
/** 按显示列范围 [start, end) 截取纯文本(不含 ANSI,按 charWidth 计位,CJK/宽字符占 2)。
|
|
86
|
+
* 跨宽字符边界时整字符归入(w+cw>start 即含入),供鼠标选区文本提取用。 */
|
|
87
|
+
export function sliceByDisplayCol(str, start, end) {
|
|
88
|
+
if (start >= end)
|
|
89
|
+
return '';
|
|
90
|
+
let w = 0;
|
|
91
|
+
let out = '';
|
|
92
|
+
for (const ch of str) {
|
|
93
|
+
if (w >= end)
|
|
94
|
+
break;
|
|
95
|
+
const cw = charWidth(ch.codePointAt(0) ?? 0);
|
|
96
|
+
if (w + cw > start)
|
|
97
|
+
out += ch;
|
|
98
|
+
w += cw;
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
85
102
|
/** 带色串的可见显示宽度(先去 ANSI 再按 displayWidth 度量)。 */
|
|
86
103
|
export function ansiDisplayWidth(s) {
|
|
87
104
|
return displayWidth(stripAnsi(s));
|
package/package.json
CHANGED
|
@@ -1,40 +1,40 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "mocode-ai",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"mocode": "bin/mocode.js"
|
|
8
|
-
},
|
|
9
|
-
"files": [
|
|
10
|
-
"dist",
|
|
11
|
-
"bin",
|
|
12
|
-
"README.md"
|
|
13
|
-
],
|
|
14
|
-
"engines": {
|
|
15
|
-
"node": ">=18"
|
|
16
|
-
},
|
|
17
|
-
"scripts": {
|
|
18
|
-
"start": "tsx src/index.ts",
|
|
19
|
-
"build": "tsc -p tsconfig.build.json",
|
|
20
|
-
"typecheck": "tsc --noEmit",
|
|
21
|
-
"prepare": "npm run build"
|
|
22
|
-
},
|
|
23
|
-
"dependencies": {
|
|
24
|
-
"cli-highlight": "^2.1.11",
|
|
25
|
-
"dotenv": "^16.0.0",
|
|
26
|
-
"fast-glob": "^3.0.0",
|
|
27
|
-
"openai": "^4.0.0"
|
|
28
|
-
},
|
|
29
|
-
"devDependencies": {
|
|
30
|
-
"@types/node": "^22.0.0",
|
|
31
|
-
"tsx": "^4.0.0",
|
|
32
|
-
"typescript": "^5.0.0"
|
|
33
|
-
},
|
|
34
|
-
"repository": {
|
|
35
|
-
"type": "git",
|
|
36
|
-
"url": "git+https://github.com/wanxunyang/mocode.git"
|
|
37
|
-
},
|
|
38
|
-
"author": "wxy",
|
|
39
|
-
"license": "MIT"
|
|
40
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "mocode-ai",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"mocode": "bin/mocode.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bin",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "tsx src/index.ts",
|
|
19
|
+
"build": "tsc -p tsconfig.build.json",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"prepare": "npm run build"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"cli-highlight": "^2.1.11",
|
|
25
|
+
"dotenv": "^16.0.0",
|
|
26
|
+
"fast-glob": "^3.0.0",
|
|
27
|
+
"openai": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^22.0.0",
|
|
31
|
+
"tsx": "^4.0.0",
|
|
32
|
+
"typescript": "^5.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/wanxunyang/mocode.git"
|
|
37
|
+
},
|
|
38
|
+
"author": "wxy",
|
|
39
|
+
"license": "MIT"
|
|
40
|
+
}
|