mocode-ai 1.3.0 → 1.3.2
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 +1 -0
- package/README.zh-CN.md +1 -0
- package/dist/i18n/index.js +60 -6
- package/dist/llm/index.js +26 -0
- package/dist/permissions/index.js +15 -1
- package/dist/repl/index.js +110 -11
- package/dist/session/query-history.js +45 -0
- package/dist/ui/batch.js +62 -11
- package/dist/ui/clipboard.js +24 -6
- package/dist/ui/composer.js +785 -0
- package/dist/ui/fuzzy.js +81 -0
- package/dist/ui/history-picker.js +214 -0
- package/dist/ui/layout.js +80 -8
- package/dist/ui/prompt.js +141 -3
- package/dist/ui/theme.js +7 -18
- package/package.json +1 -1
package/dist/ui/batch.js
CHANGED
|
@@ -275,14 +275,20 @@ function buildEntryDetailLines(e, indent = ' ') {
|
|
|
275
275
|
}
|
|
276
276
|
return lines;
|
|
277
277
|
}
|
|
278
|
+
/** entry 第一层明细行的结果指纹:结果/失败态/diff 有变时指纹变,驱动运行态原位刷新。 */
|
|
279
|
+
function entryLineDigest(e) {
|
|
280
|
+
return `${e.resultSummary}\u0001${e.diffBlock ? '1' : '0'}\u0001${e.failed ? '1' : '0'}`;
|
|
281
|
+
}
|
|
282
|
+
/** 单条第一层明细行。isLast 决定分支符(└─/├─);结果回填原位替换时也要用对分支符。 */
|
|
283
|
+
function buildEntryLine(e, index, isLast, extraIndent = '') {
|
|
284
|
+
const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
|
|
285
|
+
const branch = isLast ? '└─' : '├─';
|
|
286
|
+
const failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
|
|
287
|
+
return sanitizeRow(`${extraIndent} ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`);
|
|
288
|
+
}
|
|
278
289
|
/** 第一层只展示有哪些调用及其简短结果,不展开完整输出。extraIndent 供子批嵌套加深缩进。 */
|
|
279
290
|
function buildExpandedLines(entries, extraIndent = '') {
|
|
280
|
-
return entries.map((e, index) =>
|
|
281
|
-
const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
|
|
282
|
-
const branch = index === entries.length - 1 ? '└─' : '├─';
|
|
283
|
-
const failure = e.failed ? `${ui.red}×${ui.reset} ` : '';
|
|
284
|
-
return sanitizeRow(`${extraIndent} ${ui.dim}${branch}${ui.reset} ${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`);
|
|
285
|
-
});
|
|
291
|
+
return entries.map((e, index) => buildEntryLine(e, index, index === entries.length - 1, extraIndent));
|
|
286
292
|
}
|
|
287
293
|
function entryDetailIndent(entries, index) {
|
|
288
294
|
return index < entries.length - 1 ? ' │ ' : ' ';
|
|
@@ -295,7 +301,8 @@ export function endBatch(id, layout) {
|
|
|
295
301
|
b.finishedAt ??= Date.now();
|
|
296
302
|
if (b.summaryAbsIdx >= 0) {
|
|
297
303
|
layout.contentReplaceLine?.(b.summaryAbsIdx, buildSummaryLine(b));
|
|
298
|
-
//
|
|
304
|
+
// 点击命中在 showLiveBatch 首次落盘时即已登记(运行态可展开);这里幂等重登,
|
|
305
|
+
// 并把摘要行刷成最终完成态文案。
|
|
299
306
|
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
300
307
|
return;
|
|
301
308
|
}
|
|
@@ -323,7 +330,9 @@ export function endBatch(id, layout) {
|
|
|
323
330
|
absLineToBatchId.set(b.summaryAbsIdx, b.id);
|
|
324
331
|
}
|
|
325
332
|
/**
|
|
326
|
-
*
|
|
333
|
+
* 工具执行中立即显示/刷新摘要,并登记点击命中——运行态即可展开/折叠(此前只在
|
|
334
|
+
* endBatch 收尾后开放,导致「连续无正文的工具链」在整轮结束前无法展开,用户报告痛点)。
|
|
335
|
+
* 展开态下由 syncLiveExpanded 保持明细视图与新 entry/结果同步。
|
|
327
336
|
*/
|
|
328
337
|
export function showLiveBatch(id, layout) {
|
|
329
338
|
const b = batches.get(id);
|
|
@@ -360,10 +369,12 @@ export function showLiveBatch(id, layout) {
|
|
|
360
369
|
}
|
|
361
370
|
layout.contentInsertAfter(anchor, [sanitizeRow(summary)], false);
|
|
362
371
|
b.summaryAbsIdx = anchor + 1;
|
|
372
|
+
absLineToBatchId.set(b.summaryAbsIdx, b.id); // 运行态同样开放点击(endBatch 幂等重登)
|
|
363
373
|
return;
|
|
364
374
|
}
|
|
365
375
|
layout.contentWrite(summary + '\n');
|
|
366
376
|
b.summaryAbsIdx = Math.max(0, layout.totalRows() - 2);
|
|
377
|
+
absLineToBatchId.set(b.summaryAbsIdx, b.id); // 运行态同样开放点击
|
|
367
378
|
// 首条摘要通过增量 contentWrite 落屏时,markdown→普通内容的边界可能只更新了
|
|
368
379
|
// buffer/续写位;直到第二个 header 的 contentReplaceLine 或后续正文重绘才完全可见。
|
|
369
380
|
// 立即按 buffer 原子重画,确保慢工具执行期间摘要前的空行已经显示。
|
|
@@ -377,6 +388,30 @@ export function showLiveBatch(id, layout) {
|
|
|
377
388
|
else {
|
|
378
389
|
layout.contentReplaceLine(b.summaryAbsIdx, summary);
|
|
379
390
|
}
|
|
391
|
+
// 展开态的运行态同步:已渲染 entry 的结果回填原位替换 + 新 entry 追加。
|
|
392
|
+
syncLiveExpanded(b, layout);
|
|
393
|
+
}
|
|
394
|
+
/** 展开态 batch 的运行态同步(每次 showLiveBatch 后调):不展开/未落盘时 no-op。 */
|
|
395
|
+
function syncLiveExpanded(b, layout) {
|
|
396
|
+
if (!expandedBatches.has(b.id) || b.summaryAbsIdx < 0)
|
|
397
|
+
return;
|
|
398
|
+
// ① 已渲染 entry 的结果晚于渲染到达 → 原位替换该明细行(否则展开视图永远停在「无结果」)。
|
|
399
|
+
for (let i = 0; i < b.renderedCount && i < b.entries.length; i++) {
|
|
400
|
+
const e = b.entries[i];
|
|
401
|
+
const digest = entryLineDigest(e);
|
|
402
|
+
if (e.renderedDigest === digest)
|
|
403
|
+
continue;
|
|
404
|
+
const abs = findParentEntryAbsLine(b.id, i);
|
|
405
|
+
if (abs == null)
|
|
406
|
+
continue;
|
|
407
|
+
layout.contentReplaceLine(abs, buildEntryLine(e, i, i === b.entries.length - 1, b.indent ?? ''));
|
|
408
|
+
e.renderedDigest = digest;
|
|
409
|
+
}
|
|
410
|
+
// ② 新 entry(运行态 recordCall 后)追加到明细区末尾。
|
|
411
|
+
const ins = layout.contentInsertAfter;
|
|
412
|
+
if (ins && b.entries.length > b.renderedCount) {
|
|
413
|
+
refreshBatchExpanded(b.id, { contentInsertAfter: ins });
|
|
414
|
+
}
|
|
380
415
|
}
|
|
381
416
|
/** 绝对行索引 → 命中 batch id(用于鼠标 release 反查);非摘要行返 null。 */
|
|
382
417
|
export function findBatchByAbsLine(absLine) {
|
|
@@ -412,12 +447,25 @@ export function refreshBatchExpanded(id, layout) {
|
|
|
412
447
|
// 实时追加:不锚定视口,让新明细行自然出现在屏底。
|
|
413
448
|
// 组容器批的 entry 与子批摘要行交错,新 entry 必须插在当前块末尾,
|
|
414
449
|
// 不能简单用 summaryAbsIdx+renderedCount(否则 entry 会插到前一个子批摘要行之前)。
|
|
450
|
+
// 块末还必须计入已渲染 entry 的二层展开明细行(toggleEntry 把明细插在 entry 行下方,
|
|
451
|
+
// 会把后续 entry 行整体下移;运行态可交互后这条路径高频,漏算会插错位)。
|
|
415
452
|
let anchor = b.summaryAbsIdx + b.renderedCount;
|
|
453
|
+
for (const j of b.expandedEntries) {
|
|
454
|
+
if (j < b.renderedCount) {
|
|
455
|
+
anchor += buildEntryDetailLines(b.entries[j], entryDetailIndent(b.entries, j)).length;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
416
458
|
if (b.groupParent) {
|
|
417
|
-
let maxIdx =
|
|
459
|
+
let maxIdx = anchor;
|
|
418
460
|
for (const [idx, target] of absLineToEntry) {
|
|
419
|
-
if (target.batchId
|
|
420
|
-
|
|
461
|
+
if (target.batchId !== b.id)
|
|
462
|
+
continue;
|
|
463
|
+
// entry 行自身(含其二层明细)的块末位置
|
|
464
|
+
const end = b.expandedEntries.has(target.entryIndex)
|
|
465
|
+
? idx + buildEntryDetailLines(b.entries[target.entryIndex], entryDetailIndent(b.entries, target.entryIndex)).length
|
|
466
|
+
: idx;
|
|
467
|
+
if (end > maxIdx)
|
|
468
|
+
maxIdx = end;
|
|
421
469
|
}
|
|
422
470
|
for (const child of batches.values()) {
|
|
423
471
|
if (child.parentId === b.id && child.summaryAbsIdx >= 0) {
|
|
@@ -438,6 +486,7 @@ export function refreshBatchExpanded(id, layout) {
|
|
|
438
486
|
// 登记新增明细行的点击命中(按实际插入位置)
|
|
439
487
|
for (let i = 0; i < newEntries.length; i++) {
|
|
440
488
|
absLineToEntry.set(anchor + 1 + i, { batchId: b.id, entryIndex: b.renderedCount + i });
|
|
489
|
+
newEntries[i].renderedDigest = entryLineDigest(newEntries[i]);
|
|
441
490
|
}
|
|
442
491
|
b.renderedCount = b.entries.length;
|
|
443
492
|
}
|
|
@@ -468,6 +517,7 @@ function expand(b, layout, live = false) {
|
|
|
468
517
|
b.renderedCount = b.entries.length;
|
|
469
518
|
for (let i = 0; i < b.entries.length; i++) {
|
|
470
519
|
absLineToEntry.set(b.summaryAbsIdx + 1 + i, { batchId: b.id, entryIndex: i });
|
|
520
|
+
b.entries[i].renderedDigest = entryLineDigest(b.entries[i]); // 记指纹,供运行态结果回填原位刷新
|
|
471
521
|
}
|
|
472
522
|
// 组容器批展开时:把之前被折叠隐藏的子批摘要行重新插回对应 entry 下方,
|
|
473
523
|
// 否则子批摘要行会留在父批摘要行之后、造成明细重复/错位。
|
|
@@ -507,6 +557,7 @@ export function expandSingleEntryFully(id, layout) {
|
|
|
507
557
|
layout.contentWrite('\n');
|
|
508
558
|
expandedBatches.add(id);
|
|
509
559
|
b.expandedEntries.add(0);
|
|
560
|
+
b.entries[0].renderedDigest = entryLineDigest(b.entries[0]);
|
|
510
561
|
absLineToEntry.set(b.summaryAbsIdx + 1, { batchId: id, entryIndex: 0 });
|
|
511
562
|
}
|
|
512
563
|
function collapse(b, layout) {
|
package/dist/ui/clipboard.js
CHANGED
|
@@ -6,8 +6,9 @@ import { spawn } from 'node:child_process';
|
|
|
6
6
|
* 写入(copyToClipboard):选区复制(layout 鼠标选择)与 /copy 等命令共用,双通道:
|
|
7
7
|
* 1. OSC 52(`\x1B]52;c;<base64>\x07`):请求终端把文本写入系统剪贴板。经终端转发,SSH / 远程也生效;
|
|
8
8
|
* Windows Terminal ≥1.16、iTerm2、kitty、WezTerm 等支持(部分终端默认关,需开"允许应用访问剪贴板")。
|
|
9
|
-
* 2. 本地原生工具(best-effort,失败静默):win32=
|
|
10
|
-
*
|
|
9
|
+
* 2. 本地原生工具(best-effort,失败静默):win32=PowerShell Set-Clipboard(stdin 显式 Unicode 编码,
|
|
10
|
+
* 保证剪贴板正文无 BOM/杂字符——clip.exe 会把 BOM 字符 U+FEFF 留进正文,终端级粘贴会把它敲进输入框)、
|
|
11
|
+
* darwin=pbcopy、linux=wl-copy / xclip / xsel。覆盖 OSC52 被禁用的场景(如部分 VS Code 集成终端配置)。
|
|
11
12
|
* 两条都发:哪条生效由环境决定,重复写同一内容无副作用。
|
|
12
13
|
*
|
|
13
14
|
* 读取(readClipboard):OSC 52 是单向的(终端不会把剪贴板内容回传给应用,即便发 `\x1B]52;c;?\x07`
|
|
@@ -41,9 +42,23 @@ function pipeTo(cmd, args, buf) {
|
|
|
41
42
|
/** 本地原生剪贴板(best-effort)。 */
|
|
42
43
|
function nativeCopy(text) {
|
|
43
44
|
if (platform === 'win32') {
|
|
44
|
-
// clip.exe
|
|
45
|
-
//
|
|
46
|
-
|
|
45
|
+
// 不走 clip.exe 直写:BOM-less UTF-16LE 会被按控制台代码页(GBK)解码成乱码,
|
|
46
|
+
// 而 BOM 版虽能正确解码,clip.exe 却会把 BOM 字符(U+FEFF)留进剪贴板正文——
|
|
47
|
+
// 终端级粘贴(Ctrl+V 由终端直接灌键盘输入)绕过 readClipboard 的 FEFF 剥离,
|
|
48
|
+
// FEFF 会敲进输入框渲染成缺字形方块。故改走 PowerShell stdin:显式 Unicode
|
|
49
|
+
// 输入编码,无 BOM、无杂字符;PowerShell 拉不起(spawn error)才兜底 clip.exe+BOM。
|
|
50
|
+
const buf = Buffer.from(text, 'utf16le');
|
|
51
|
+
const bom = Buffer.from([0xff, 0xfe]);
|
|
52
|
+
try {
|
|
53
|
+
const p = spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command',
|
|
54
|
+
'[Console]::InputEncoding=[System.Text.Encoding]::Unicode; Set-Clipboard -Value ([Console]::In.ReadToEnd())'], { stdio: ['pipe', 'ignore', 'ignore'], windowsHide: true });
|
|
55
|
+
p.on('error', () => pipeTo('clip', [], Buffer.concat([bom, buf]))); // 无 PowerShell:退 clip.exe(乱码/FEFF 风险自负,OSC52 兜底)
|
|
56
|
+
p.stdin.on('error', () => { });
|
|
57
|
+
p.stdin.end(buf);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
pipeTo('clip', [], Buffer.concat([bom, buf]));
|
|
61
|
+
}
|
|
47
62
|
}
|
|
48
63
|
else if (platform === 'darwin') {
|
|
49
64
|
pipeTo('pbcopy', [], Buffer.from(text, 'utf8'));
|
|
@@ -101,7 +116,10 @@ export async function readClipboard() {
|
|
|
101
116
|
// Get-Clipboard 默认按控制台代码页输出;显式转 UTF8 避免中文乱码。
|
|
102
117
|
const out = await captureOutput('powershell', ['-NoProfile', '-NonInteractive', '-Command',
|
|
103
118
|
'[Console]::OutputEncoding=[System.Text.Encoding]::UTF8; Get-Clipboard -Raw'], 'utf8');
|
|
104
|
-
|
|
119
|
+
// 末尾 strip 一个 \n:PowerShell 把字符串写到 stdout 时会追加行终止符(实验证实
|
|
120
|
+
// clipboard=abc 时读回 abc\r\n),属输出伪影而非剪贴板内容;真以换行结尾的剪贴板
|
|
121
|
+
// 内容多出的一个 \n 恰好被抵消,不受影响。
|
|
122
|
+
return out?.replace(/^\uFEFF/, '').replace(/\r\n/g, '\n').replace(/\r$/, '').replace(/\n$/, '') ?? '';
|
|
105
123
|
}
|
|
106
124
|
if (platform === 'darwin') {
|
|
107
125
|
return (await captureOutput('pbpaste', [], 'utf8')) ?? '';
|