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/dist/ui/prompt.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import readline from 'node:readline';
2
2
  import { stdin, stdout } from 'node:process';
3
3
  import { ui } from './theme.js';
4
- import { displayWidth, padEndDisplay, truncateDisplay } from './render.js';
4
+ import { displayWidth, padEndDisplay, truncateDisplay, visColToCharCol, wrapByDisplayWidth } from './render.js';
5
5
  import * as layout from './layout.js';
6
6
  import * as mouse from './mouse.js';
7
7
  // ── 粘贴检测(块级 + 时间窗)──
@@ -249,8 +249,105 @@ export async function promptWithSlashMenu(opts) {
249
249
  menu: menuLines().length ? { lines: menuLines() } : null,
250
250
  });
251
251
  }
252
+ /** 鼠标点击输入框:layout 已把"屏 tap 位置"以 (flatIdx, inSegVis) 形式传进来
253
+ * (flatIdx = flat 中的绝对段号;inSegVis = 段内显示列)。
254
+ * 本函数在 prompt 自己的 (lines + chip) 上复刻 wrap/flat 还原 (cl, cc)→ redraw。
255
+ * chip 模式自动扣 chip prefix:
256
+ * - dispLines() 把 chipPre 末行 + chipPrefix + suffix[0] 拼成一行 "mergedRow"
257
+ * - flat 中对应的"合并行段"是 mergedRow 的折行段,段内 inChar 累加从 mergedRow 起
258
+ * - 要得到 lines[cl=0] 内的字符索引,扣掉 mergedRow 起始到 lines[0] 起始的字符数
259
+ * = chipPre 末行字符数 + chipPrefix 字符数。 */
260
+ function applyExternalCursor(flatIdx, inSegVis) {
261
+ const g = layout.getGeo();
262
+ const promptW = displayWidth(opts.prompt);
263
+ const W = Math.max(1, g.cols - promptW);
264
+ const dispLs = dispLines();
265
+ const lineVis = dispLs.map((l) => wrapByDisplayWidth(l, W));
266
+ const flat = [];
267
+ for (const lv of lineVis)
268
+ for (const r of lv)
269
+ flat.push(r);
270
+ let newCl;
271
+ let newCc;
272
+ if (flat.length === 0) {
273
+ newCl = 0;
274
+ newCc = 0;
275
+ }
276
+ else {
277
+ const safeIdx = Math.max(0, Math.min(flatIdx, flat.length - 1));
278
+ const seg = flat[safeIdx];
279
+ const segW = displayWidth(seg);
280
+ const safeInSeg = Math.max(0, Math.min(inSegVis, segW));
281
+ const inChar = visColToCharCol(seg, safeInSeg);
282
+ // safeIdx → (dispLine, segInLine)
283
+ let dispLine = 0;
284
+ let segInLine = safeIdx;
285
+ while (dispLine < lineVis.length && segInLine >= lineVis[dispLine].length) {
286
+ segInLine -= lineVis[dispLine].length;
287
+ dispLine++;
288
+ }
289
+ if (dispLine >= lineVis.length) {
290
+ dispLine = lineVis.length - 1;
291
+ segInLine = lineVis[dispLine].length - 1;
292
+ }
293
+ // dispLine → cl(chip-aware)
294
+ if (chip) {
295
+ const preLen = chipPreLines().length;
296
+ if (dispLine < preLen - 1) {
297
+ // 落在 chipPre 内(非末行)→ 走出 chip 到 suffix 起点
298
+ newCl = 0;
299
+ }
300
+ else if (dispLine === preLen - 1) {
301
+ // 合并行 = suffix 第 0 行(cl=0)
302
+ newCl = 0;
303
+ }
304
+ else {
305
+ newCl = dispLine - (preLen - 1);
306
+ }
307
+ }
308
+ else {
309
+ newCl = dispLine;
310
+ }
311
+ newCl = Math.max(0, Math.min(newCl, lines.length - 1));
312
+ const lineText = lines[newCl] ?? '';
313
+ // cc 必须在 lines[newCl] 内。flat 中 seg 是 dispLs[dispLine] 的某折行段,
314
+ // seg 内 inChar 是段内字符偏移,但 lines[newCl] 的字符是按 (dispLs 前段累加) + (本段 inChar) 算的。
315
+ // ——非 chip 模式下:dispLine = newCl, segStartChars = sum(lineVis[dispLine][0..segInLine-1].length)
316
+ // ccInLine = segStartChars + inChar
317
+ // ——chip 模式下 newCl=0(合并行):dispLine 是合并行 dispLine,segStartChars 同上,
318
+ // 但 lines[0] 在合并行中"起始处"在 (mergedRow.length - lines[0].length) 字符处,
319
+ // 故 ccInLine = segStartChars + inChar - chipOverheadChars。
320
+ let segStartChars = 0;
321
+ const segs = lineVis[dispLine];
322
+ for (let s = 0; s < segInLine; s++)
323
+ segStartChars += segs[s]?.length ?? 0;
324
+ let ccInLine = segStartChars + inChar;
325
+ if (chip && newCl === 0) {
326
+ const pre = chipPreLines();
327
+ const mergedRow = pre[pre.length - 1] ?? '';
328
+ const prefix = chipPrefix();
329
+ // mergedRow 起始到 lines[0] 起始的字符数 = (chipPreLast + chipPrefix) 字符数
330
+ const chipOverheadChars = mergedRow.length + prefix.length - lineText.length;
331
+ ccInLine = ccInLine - chipOverheadChars;
332
+ }
333
+ newCc = Math.max(0, Math.min(ccInLine, lineText.length));
334
+ }
335
+ cl = newCl;
336
+ cc = newCc;
337
+ // 收起菜单/过滤态:点击输入框关闭菜单,回到纯文本编辑。
338
+ if (menuOpen) {
339
+ menuOpen = false;
340
+ filtered = [];
341
+ selected = 0;
342
+ menuTop = 0;
343
+ }
344
+ justSawCR = false;
345
+ computeFiltered();
346
+ redraw();
347
+ }
252
348
  function cleanup() {
253
349
  layout.setPasteHandler(null);
350
+ layout.setCursorChangeHandler(null);
254
351
  try {
255
352
  stdin.setRawMode(false);
256
353
  }
@@ -566,6 +663,7 @@ export async function promptWithSlashMenu(opts) {
566
663
  return;
567
664
  }
568
665
  layout.setPasteHandler(onMousePaste); // 鼠标右键单击输入框(未拖动)→ 读剪贴板贴入;cleanup 时注销
666
+ layout.setCursorChangeHandler(applyExternalCursor); // 鼠标左键单击输入框(未拖动)→ 改 cl/cc 到点击位;cleanup 时注销
569
667
  stdin.resume();
570
668
  emitter.on('keypress', onKey);
571
669
  computeFiltered();
@@ -615,7 +713,6 @@ export async function promptTurnPicker(items) {
615
713
  cursorLine: 0,
616
714
  cursorCol: displayWidth(hint),
617
715
  menu: { lines: menuLines() },
618
- caret: false, // 纯导航菜单(非文本输入):不画输入框块状光标,聚焦由菜单 ▸ 标记
619
716
  });
620
717
  }
621
718
  function cleanup() {
@@ -756,7 +853,6 @@ export async function promptSessionPicker(items, recentCap = 10) {
756
853
  cursorLine: 0,
757
854
  cursorCol: displayWidth(h),
758
855
  menu: { lines: menuLines() },
759
- caret: false, // 纯导航菜单(非文本输入):不画输入框块状光标,聚焦由菜单 ▸ 标记
760
856
  });
761
857
  }
762
858
  function cleanup() {
@@ -894,7 +990,6 @@ export async function promptThemePicker(items) {
894
990
  cursorLine: 0,
895
991
  cursorCol: displayWidth(hint),
896
992
  menu: { lines: menuLines() },
897
- caret: false, // 纯导航菜单:不画输入框块状光标,聚焦由菜单 ▸ 标记
898
993
  });
899
994
  }
900
995
  function cleanup() {
@@ -1008,7 +1103,6 @@ export async function promptRevertChoice(fileCount) {
1008
1103
  cursorLine: 0,
1009
1104
  cursorCol: displayWidth(hint),
1010
1105
  menu: { lines: menuLines() },
1011
- caret: false, // 纯导航菜单:不画输入框块状光标,聚焦由菜单 ▸ 标记
1012
1106
  });
1013
1107
  }
1014
1108
  function cleanup() {
package/dist/ui/render.js CHANGED
@@ -99,6 +99,28 @@ export function sliceByDisplayCol(str, start, end) {
99
99
  }
100
100
  return out;
101
101
  }
102
+ /** 把可视列(0-based)反推为 str 内的字符偏移(JS 字符串下标,UTF-16 code unit)。
103
+ * 点击落在宽字符的"列中段"时归到该字符之前的偏移(停在它左侧边界)——与终端光标行为一致。
104
+ * visCol 大于串的显示宽度时返回 str.length(点行末的"右边")。
105
+ * 输入框点击定位专用;与 sliceByDisplayCol 互为逆,但语义只关心单点、不关心区间。 */
106
+ export function visColToCharCol(str, visCol) {
107
+ if (visCol <= 0)
108
+ return 0;
109
+ let w = 0;
110
+ let i = 0;
111
+ for (const ch of str) {
112
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
113
+ if (cw <= 0) {
114
+ i += ch.length;
115
+ continue;
116
+ } // 零宽(组合符):不占列
117
+ if (w + cw > visCol)
118
+ break; // 落在该字符显示区间内 → 停在其左侧
119
+ w += cw;
120
+ i += ch.length;
121
+ }
122
+ return i;
123
+ }
102
124
  /** 带色串的可见显示宽度(先去 ANSI 再按 displayWidth 度量)。 */
103
125
  export function ansiDisplayWidth(s) {
104
126
  return displayWidth(stripAnsi(s));
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {