mocode-ai 1.3.5 → 1.3.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/llm/index.js CHANGED
@@ -17,12 +17,12 @@ if (process.env.DEBUG === 'true') {
17
17
  * 不重试 → 400 (bad request) / 401 (auth) / 其他 4xx / 用户中断 (AbortError / APIUserAbortError)
18
18
  *
19
19
  * 退避:指数 + ±20% jitter,首等 1s,翻倍,封顶 30s;若后端返回 Retry-After 头则优先按其值。
20
- * 默认 4 次尝试(1 初始 + 3 重试),要调改 RETRY_MAX_ATTEMPTS。
20
+ * 默认 10 次尝试(1 初始 + 9 重试),要调改 RETRY_MAX_ATTEMPTS。
21
21
  *
22
22
  * SDK 内置 maxRetries 默认 2(对所有 5xx+网络错重试),与本策略叠加会双重重试 5xx —— 显式置 0 让
23
23
  * 全部重试由 chat() 外层重试循环统一控,行为可预期。
24
24
  */
25
- const RETRY_MAX_ATTEMPTS = 4;
25
+ const RETRY_MAX_ATTEMPTS = 10;
26
26
  const RETRY_BASE_MS = 1000;
27
27
  const RETRY_MAX_MS = 30000;
28
28
  const RETRY_JITTER = 0.2;
@@ -338,13 +338,18 @@ toolsOverride) {
338
338
  throw err;
339
339
  }
340
340
  const wait = computeBackoff(attempt, getRetryAfterMs(err));
341
- handlers.onRetry?.({
341
+ const retry = {
342
342
  attempt,
343
343
  nextAttempt: attempt + 1,
344
344
  waitMs: wait,
345
345
  code: retryErrorCode(err),
346
- });
347
- logRetry(attempt, err, wait);
346
+ };
347
+ // 交给宿主的 onRetry 负责展示:TUI 可将其作为瞬时状态处理,不能再用 console
348
+ // 追加到内容历史,否则重连成功后会留下过期的失败提示。没有宿主时保留 stderr 日志。
349
+ if (handlers.onRetry)
350
+ handlers.onRetry(retry);
351
+ else
352
+ logRetry(attempt, err, wait);
348
353
  // sleep 自己会在 signal abort 时抛 AbortError——透传,让 runAgentCore 的 catch 按中断处理。
349
354
  await sleep(wait, signal);
350
355
  }
package/dist/ui/batch.js CHANGED
@@ -271,27 +271,84 @@ function buildEntryDetailLines(e, indent = ' ') {
271
271
  }
272
272
  }
273
273
  else if (e.resultSummary) {
274
- lines.push(sanitizeRow(`${indent}${ui.gray}↳ ${e.resultSummary}${ui.reset}`));
274
+ // 用 · 而非 ↳:entry 行尾已内联同一串,详情区再画一遍箭头会被读成「又一条子结果」;
275
+ // · 只表达「这是结论文本」,与 diff 块、fullOutput 原始输出行在视觉上分属三档。
276
+ lines.push(sanitizeRow(`${indent}${ui.dim}${DETAIL_RESULT_MARK}${ui.reset}${ui.gray}${e.resultSummary}${ui.reset}`));
275
277
  }
276
278
  return lines;
277
279
  }
278
- /** entry 第一层明细行的结果指纹:结果/失败态/diff 有变时指纹变,驱动运行态原位刷新。 */
280
+ /**
281
+ * entry 第一层明细行的渲染指纹:指纹变则原位重画该行(驱动运行态结果回填)。
282
+ * 纳入 fullOutput **有无**(而非内容):hasEntryDetail 看它来决定画不画三角——
283
+ * 缺了它,「调用时无结果(空白占位)→ 结果回填」这一步不会重画,三角永远不出现。
284
+ * 只取有无不取内容:完整输出可能上万字符,拼进指纹是浪费,且其内容不影响 entry 行渲染。
285
+ */
279
286
  function entryLineDigest(e) {
280
- return `${e.resultSummary}\u0001${e.diffBlock ? '1' : '0'}\u0001${e.failed ? '1' : '0'}`;
287
+ return `${e.resultSummary}\u0001${e.diffBlock ? '1' : '0'}\u0001${e.fullOutput ? '1' : '0'}\u0001${e.failed ? '1' : '0'}`;
281
288
  }
282
- /** 单条第一层明细行。isLast 决定分支符(└─/├─);结果回填原位替换时也要用对分支符。 */
283
- function buildEntryLine(e, index, isLast, extraIndent = '') {
289
+ /**
290
+ * 第二层展开三角:折叠 / 展开 ▾。替代原先的 ├─ / └─。
291
+ *
292
+ * 根因是**字体实现差异,不是 Unicode 分类差异**——两者 East Asian Width 同为
293
+ * Ambiguous(U+2500–257F Box Drawing 与 U+25A0–25FF Geometric Shapes 整块都是 A):
294
+ * `render.ts: charWidth()` 对 Ambiguous 一律按 1 列算,但部分等宽字体为让竖线连续,
295
+ * 常把 Box Drawing 做成占 2 格的字形(尤其中文字体里 box drawing 取自全角字库)。
296
+ * → sanitizeRow/truncateAnsi 钳宽失准 → 物理行超 cols → 终端 auto-wrap
297
+ * → repaintViewport 的 CUP 寻址全错(整屏错位)。
298
+ *
299
+ * 换 Geometric Shapes 的依据是**实测**:同一终端下 `●`(U+25CF) 与 `◇`(U+25C7)
300
+ * 长期渲染正常,而 `├─`/`└─` 错位——即本机字体对 Geometric Shapes 按 1 列、
301
+ * 对 Box Drawing 按 2 列。故与 `●` 同族的 ▸/▾ 是安全选择。
302
+ *
303
+ * 注:Ambiguous 终究依赖字体。若要零风险,用 Latin-1 的 `·`(U+00B7, EAW=Na)。
304
+ */
305
+ const CARET_COLLAPSED = '▸';
306
+ const CARET_EXPANDED = '▾';
307
+ /** 无详情可展开时占位,保持与 "▸ " 同宽,让工具名列对齐。 */
308
+ const CARET_NONE = ' ';
309
+ /** 详情行缩进:统一 7 空格(对齐 entry 行的 " ▸ " 之后再右移一列,形成层次)。 */
310
+ const DETAIL_INDENT = ' ';
311
+ /**
312
+ * 详情行结果标记:·(U+00B7 MIDDLE DOT, Latin-1 Supplement, **EAW = Narrow**)。
313
+ * 比 ▸/▾ 更稳——Narrow 是 Unicode 层面的硬保证,不依赖字体对 Ambiguous 的取舍。
314
+ *
315
+ * 不用 entry 行尾那个 `↳`:①entry 行已内联同一串结果,详情区再原样输出一遍纯属重复;
316
+ * ②`↳`(U+21B3) 属 Arrows 块、EAW 同样是 Ambiguous。
317
+ */
318
+ const DETAIL_RESULT_MARK = '· ';
319
+ /** entry 是否有可展开的详情行。须与 buildEntryDetailLines 的分支一致,
320
+ * 否则会画出点了没反应的三角(toggleEntry 对空 details 直接 return)。 */
321
+ function hasEntryDetail(e) {
322
+ return Boolean(e.diffBlock || e.fullOutput || e.resultSummary);
323
+ }
324
+ /** entry 行的展开三角(含尾随空格与配色)。折叠 dim、展开 accent,一眼区分当前态。 */
325
+ function entryCaret(e, expanded) {
326
+ if (!hasEntryDetail(e))
327
+ return CARET_NONE;
328
+ return expanded ? `${ui.accent}${CARET_EXPANDED}${ui.reset} ` : `${ui.dim}${CARET_COLLAPSED}${ui.reset} `;
329
+ }
330
+ /**
331
+ * 单条第一层明细行。三角由 entry 自身的详情展开态决定(▸ 可展开 / ▾ 已展开 / 空白占位),
332
+ * 不再依赖 isLast —— 兄弟条目间没有嵌套语义,画分支符(├─/└─)纯属误导且引入宽度歧义。
333
+ */
334
+ function buildEntryLine(b, index, extraIndent = '') {
335
+ const e = b.entries[index];
284
336
  const result = e.resultSummary ? ` ${ui.gray}↳ ${e.resultSummary}${ui.reset}` : '';
285
- const branch = isLast ? '└─' : '├─';
337
+ const caret = entryCaret(e, b.expandedEntries.has(index));
286
338
  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}`);
339
+ return sanitizeRow(`${extraIndent} ${caret}${failure}${ui.accent}${e.name}${ui.reset} ${ui.dim}${e.callSummary}${ui.reset}${result}`);
288
340
  }
289
- /** 第一层只展示有哪些调用及其简短结果,不展开完整输出。extraIndent 供子批嵌套加深缩进。 */
290
- function buildExpandedLines(entries, extraIndent = '') {
291
- return entries.map((e, index) => buildEntryLine(e, index, index === entries.length - 1, extraIndent));
341
+ /** 第一层只展示有哪些调用及其简短结果,不展开完整输出。extraIndent 供子批嵌套加深缩进。
342
+ * fromIndex:运行态追加渲染时只产出 entries[fromIndex, ...) 的行(已渲染的不能重复输出) */
343
+ function buildExpandedLines(b, extraIndent = '', fromIndex = 0) {
344
+ const out = [];
345
+ for (let i = fromIndex; i < b.entries.length; i++)
346
+ out.push(buildEntryLine(b, i, extraIndent));
347
+ return out;
292
348
  }
293
- function entryDetailIndent(entries, index) {
294
- return index < entries.length - 1 ? ' │ ' : ' ';
349
+ /** 详情行缩进(现在与 index 无关,统一常量;保留签名以免调用点全改) */
350
+ function entryDetailIndent(_entries, _index) {
351
+ return DETAIL_INDENT;
295
352
  }
296
353
  /** 在 batch 收尾时(onToolBatchEnd):写摘要行 + 登记 summaryAbsIdx;若已展开(回放场景)立即插详情。 */
297
354
  export function endBatch(id, layout) {
@@ -404,7 +461,7 @@ function syncLiveExpanded(b, layout) {
404
461
  const abs = findParentEntryAbsLine(b.id, i);
405
462
  if (abs == null)
406
463
  continue;
407
- layout.contentReplaceLine(abs, buildEntryLine(e, i, i === b.entries.length - 1, b.indent ?? ''));
464
+ layout.contentReplaceLine(abs, buildEntryLine(b, i, b.indent ?? ''));
408
465
  e.renderedDigest = digest;
409
466
  }
410
467
  // ② 新 entry(运行态 recordCall 后)追加到明细区末尾。
@@ -443,7 +500,7 @@ export function refreshBatchExpanded(id, layout) {
443
500
  if (b.entries.length <= b.renderedCount)
444
501
  return;
445
502
  const newEntries = b.entries.slice(b.renderedCount);
446
- const lines = buildExpandedLines(newEntries, b.indent ?? '');
503
+ const lines = buildExpandedLines(b, b.indent ?? '', b.renderedCount);
447
504
  // 实时追加:不锚定视口,让新明细行自然出现在屏底。
448
505
  // 组容器批的 entry 与子批摘要行交错,新 entry 必须插在当前块末尾,
449
506
  // 不能简单用 summaryAbsIdx+renderedCount(否则 entry 会插到前一个子批摘要行之前)。
@@ -511,7 +568,7 @@ export function toggleBatch(id, layout) {
511
568
  function expand(b, layout, live = false) {
512
569
  if (b.summaryAbsIdx < 0)
513
570
  return; // 摘要行未落盘时展开会把明细插到 buffer 头部
514
- const lines = buildExpandedLines(b.entries, b.indent ?? '');
571
+ const lines = buildExpandedLines(b, b.indent ?? '');
515
572
  layout.contentInsertAfter(b.summaryAbsIdx, lines, !live);
516
573
  expandedBatches.add(b.id);
517
574
  b.renderedCount = b.entries.length;
@@ -548,7 +605,7 @@ export function expandSingleEntryFully(id, layout) {
548
605
  if (!b || b.entries.length !== 1 || expandedBatches.has(id))
549
606
  return;
550
607
  const lines = [
551
- ...buildExpandedLines(b.entries),
608
+ ...buildExpandedLines(b),
552
609
  ...buildEntryDetailLines(b.entries[0], entryDetailIndent(b.entries, 0)),
553
610
  ];
554
611
  layout.contentInsertAfter(b.summaryAbsIdx, lines);
@@ -643,13 +700,19 @@ export function toggleEntry(batchId, entryIndex, layout) {
643
700
  const details = buildEntryDetailLines(b.entries[entryIndex], entryDetailIndent(b.entries, entryIndex));
644
701
  if (details.length === 0)
645
702
  return;
646
- if (b.expandedEntries.has(entryIndex)) {
647
- layout.contentDeleteFrom(headerIdx + 1, details.length);
703
+ const wasExpanded = b.expandedEntries.has(entryIndex);
704
+ // 先翻状态再重画三角:buildEntryLine b.expandedEntries 决定 ▸/▾。
705
+ // 替换 entry 行本身(行数不变)不会触发 shiftBatchesAfter,故可在插/删详情前做。
706
+ if (wasExpanded)
648
707
  b.expandedEntries.delete(entryIndex);
708
+ else
709
+ b.expandedEntries.add(entryIndex);
710
+ layout.contentReplaceLine(headerIdx, buildEntryLine(b, entryIndex, b.indent ?? ''));
711
+ if (wasExpanded) {
712
+ layout.contentDeleteFrom(headerIdx + 1, details.length);
649
713
  }
650
714
  else {
651
715
  layout.contentInsertAfter(headerIdx, details);
652
- b.expandedEntries.add(entryIndex);
653
716
  }
654
717
  }
655
718
  /**
package/dist/ui/layout.js CHANGED
@@ -1332,6 +1332,7 @@ function handleMouseEvent(e) {
1332
1332
  m.toggleEntry(entry.batchId, entry.entryIndex, {
1333
1333
  contentInsertAfter: (after, lines) => contentInsertAfter(after, lines),
1334
1334
  contentDeleteFrom: (start, n) => contentDeleteFrom(start, n),
1335
+ contentReplaceLine: (absIdx, line) => contentReplaceLine(absIdx, line),
1335
1336
  });
1336
1337
  repaint();
1337
1338
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mocode-ai",
3
- "version": "1.3.5",
3
+ "version": "1.3.6",
4
4
  "description": "终端编码 agent:LLM + tool-call 循环 + 流式输出(含思考)+ 16 个工具,接任意 OpenAI 兼容后端。",
5
5
  "type": "module",
6
6
  "bin": {