codemini-cli 0.2.7 → 0.2.9

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.
@@ -2,6 +2,20 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { shouldCaptureEscapeSequence } from './input-escape.js';
4
4
  import { classifyCommandIntent } from '../core/shell.js';
5
+ import {
6
+ buildInterToolNotice as buildRegisteredInterToolNotice,
7
+ buildPreToolNotice as buildRegisteredPreToolNotice,
8
+ buildSyntheticCompletionText as buildRegisteredSyntheticCompletionText
9
+ } from './tool-narration.js';
10
+ import {
11
+ describeToolActivity as describeRegisteredToolActivity,
12
+ isCodeGenerationActivityName
13
+ } from './tool-activity/index.js';
14
+ import {
15
+ describeAutoSkillActivity as describeRegisteredAutoSkillActivity,
16
+ describeSkillActivity as describeRegisteredSkillActivity,
17
+ formatAutoSkillBadge as formatRegisteredAutoSkillBadge
18
+ } from './skill-activity/index.js';
5
19
 
6
20
  const h = React.createElement;
7
21
  const SUGGESTION_PAGE_SIZE = 8;
@@ -88,6 +102,7 @@ const TUI_COPY = {
88
102
  startupHint: '使用 /help、/commands、/compact、/exit、!<shell>。Tab 可自动补全 slash 命令。',
89
103
  toolSummaryExpanded: '工具摘要:已展开',
90
104
  toolSummaryCollapsed: '工具摘要:已收起',
105
+ toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用,按 Ctrl+T 展开全部`,
91
106
  toggleToolSummary: 'Ctrl+T 切换',
92
107
  scrollHint: '使用终端自己的滚动条或 scrollback',
93
108
  keyboardDebugEnabled: '键盘调试已开启',
@@ -219,6 +234,7 @@ const TUI_COPY = {
219
234
  startupHint: 'Use /help, /commands, /compact, /exit, !<shell>. Tab for slash autocomplete.',
220
235
  toolSummaryExpanded: 'Tool summary: expanded',
221
236
  toolSummaryCollapsed: 'Tool summary: collapsed',
237
+ toolChainCollapsed: (count) => `${count} earlier tool calls hidden, press Ctrl+T to expand`,
222
238
  toggleToolSummary: 'Ctrl+T to toggle',
223
239
  scrollHint: 'Scroll with your terminal scrollbar or scrollback',
224
240
  keyboardDebugEnabled: 'Keyboard debug enabled',
@@ -353,6 +369,308 @@ function trimText(value, maxLen = 88) {
353
369
  return `${text.slice(0, maxLen - 3)}...`;
354
370
  }
355
371
 
372
+ export function splitMarkdownTableCells(line) {
373
+ const text = String(line || '').trim();
374
+ if (!text.includes('|')) return [];
375
+ return text
376
+ .replace(/^\|/, '')
377
+ .replace(/\|$/, '')
378
+ .split('|')
379
+ .map((cell) => String(cell || '').trim());
380
+ }
381
+
382
+ export function isMarkdownTableSeparator(line) {
383
+ const cells = splitMarkdownTableCells(line);
384
+ return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell));
385
+ }
386
+
387
+ export function isMarkdownTableHeader(line, nextLine) {
388
+ const cells = splitMarkdownTableCells(line);
389
+ return cells.length > 1 && isMarkdownTableSeparator(nextLine);
390
+ }
391
+
392
+ function getMarkdownTableAlignments(separatorLine, columnCount) {
393
+ const cells = splitMarkdownTableCells(separatorLine);
394
+ return Array.from({ length: columnCount }, (_, index) => {
395
+ const cell = String(cells[index] || '').trim();
396
+ if (/^:-{3,}:$/.test(cell)) return 'center';
397
+ if (/^-{3,}:$/.test(cell)) return 'right';
398
+ return 'left';
399
+ });
400
+ }
401
+
402
+ function stringWidthLite(value) {
403
+ return Array.from(String(value || '')).reduce((sum, ch) => sum + charDisplayWidth(ch), 0);
404
+ }
405
+
406
+ function splitTableWrapUnits(text) {
407
+ return String(text || '')
408
+ .split(/([\s,.;:!?/\\|()[\]{}<>,。;:!?、()【】《》]+)/)
409
+ .filter(Boolean);
410
+ }
411
+
412
+ function wrapPlainText(text, width, hard = false) {
413
+ const normalized = String(text || '').replace(/\s+/g, ' ').trim();
414
+ if (!normalized) return [''];
415
+ if (width <= 1) return [normalized];
416
+
417
+ const words = splitTableWrapUnits(normalized);
418
+ const lines = [];
419
+ let current = '';
420
+
421
+ const pushWord = (word) => {
422
+ if (stringWidthLite(word) <= width) {
423
+ if (!current) {
424
+ current = word;
425
+ return;
426
+ }
427
+ const needsSpacer =
428
+ !/\s$/.test(current) &&
429
+ !/^\s/.test(word) &&
430
+ !/^[,.;:!?/\\|)\]},。;:!?、】【》]/.test(word);
431
+ const next = needsSpacer ? `${current} ${word}` : `${current}${word}`;
432
+ if (stringWidthLite(next) <= width) {
433
+ current = next;
434
+ } else {
435
+ lines.push(current);
436
+ current = word;
437
+ }
438
+ return;
439
+ }
440
+
441
+ if (!hard) {
442
+ if (current) {
443
+ lines.push(current);
444
+ current = '';
445
+ }
446
+ lines.push(word);
447
+ return;
448
+ }
449
+
450
+ if (current) {
451
+ lines.push(current);
452
+ current = '';
453
+ }
454
+ let rest = word;
455
+ while (stringWidthLite(rest) > width) {
456
+ lines.push(Array.from(rest).slice(0, width).join(''));
457
+ rest = Array.from(rest).slice(width).join('');
458
+ }
459
+ current = rest;
460
+ };
461
+
462
+ for (const word of words) pushWord(word);
463
+ if (current) lines.push(current);
464
+ return lines.length > 0 ? lines : [''];
465
+ }
466
+
467
+ function padAlignedText(text, width, align = 'left') {
468
+ const value = String(text || '');
469
+ const visible = stringWidthLite(value);
470
+ if (visible >= width) return value;
471
+ const gap = width - visible;
472
+ if (align === 'right') return `${' '.repeat(gap)}${value}`;
473
+ if (align === 'center') {
474
+ const left = Math.floor(gap / 2);
475
+ const right = gap - left;
476
+ return `${' '.repeat(left)}${value}${' '.repeat(right)}`;
477
+ }
478
+ return `${value}${' '.repeat(gap)}`;
479
+ }
480
+
481
+ function normalizeTableCellText(value) {
482
+ return String(value || '')
483
+ .replace(/`([^`]+)`/g, '$1')
484
+ .replace(/\*\*([^*]+)\*\*/g, '$1')
485
+ .replace(/\*([^*]+)\*/g, '$1')
486
+ .trim();
487
+ }
488
+
489
+ export function formatMarkdownTableBlock(lines, contentWidth = 72) {
490
+ const sourceLines = Array.isArray(lines) ? lines : [];
491
+ if (sourceLines.length < 2) return [];
492
+
493
+ const headerCells = splitMarkdownTableCells(sourceLines[0]);
494
+ const separatorLine = sourceLines[1];
495
+ const bodyRows = sourceLines.slice(2).map(splitMarkdownTableCells).filter((cells) => cells.length > 0);
496
+ if (headerCells.length === 0) return [];
497
+
498
+ const columnCount = Math.max(headerCells.length, ...bodyRows.map((cells) => cells.length));
499
+ const headers = Array.from({ length: columnCount }, (_, index) => normalizeTableCellText(headerCells[index] || ''));
500
+ const rows = bodyRows.map((cells) =>
501
+ Array.from({ length: columnCount }, (_, index) => normalizeTableCellText(cells[index] || ''))
502
+ );
503
+ const alignments = getMarkdownTableAlignments(separatorLine, columnCount);
504
+
505
+ const minColumnWidth = 3;
506
+ const maxRowLines = 6;
507
+ const safetyMargin = 4;
508
+ const borderOverhead = 1 + columnCount * 3;
509
+ const availableWidth = Math.max(contentWidth - borderOverhead - safetyMargin, columnCount * minColumnWidth);
510
+
511
+ const getMinWidth = (text) => {
512
+ const words = splitTableWrapUnits(String(text || '')).filter((word) => !/^\s+$/.test(word));
513
+ if (words.length === 0) return minColumnWidth;
514
+ return Math.max(...words.map((word) => stringWidthLite(word)), minColumnWidth);
515
+ };
516
+
517
+ const getIdealWidth = (text) => Math.max(stringWidthLite(String(text || '').trim()), minColumnWidth);
518
+
519
+ const minWidths = headers.map((header, index) =>
520
+ Math.max(getMinWidth(header), ...rows.map((row) => getMinWidth(row[index])))
521
+ );
522
+ const idealWidths = headers.map((header, index) =>
523
+ Math.max(getIdealWidth(header), ...rows.map((row) => getIdealWidth(row[index])))
524
+ );
525
+
526
+ const totalMin = minWidths.reduce((sum, width) => sum + width, 0);
527
+ const totalIdeal = idealWidths.reduce((sum, width) => sum + width, 0);
528
+ let needsHardWrap = false;
529
+ let columnWidths;
530
+
531
+ if (totalIdeal <= availableWidth) {
532
+ columnWidths = idealWidths.slice();
533
+ } else if (totalMin <= availableWidth) {
534
+ const extraSpace = availableWidth - totalMin;
535
+ const overflows = idealWidths.map((ideal, index) => ideal - minWidths[index]);
536
+ const totalOverflow = overflows.reduce((sum, width) => sum + width, 0);
537
+ columnWidths = minWidths.map((min, index) => {
538
+ if (totalOverflow === 0) return min;
539
+ return min + Math.floor((overflows[index] / totalOverflow) * extraSpace);
540
+ });
541
+ } else {
542
+ needsHardWrap = true;
543
+ const scale = availableWidth / Math.max(totalMin, 1);
544
+ columnWidths = minWidths.map((width) => Math.max(Math.floor(width * scale), minColumnWidth));
545
+ }
546
+
547
+ const wrapCell = (text, width) => wrapPlainText(text, width, needsHardWrap);
548
+
549
+ const computeMaxWrappedLines = () => {
550
+ let maxLines = 1;
551
+ for (let index = 0; index < headers.length; index += 1) {
552
+ maxLines = Math.max(maxLines, wrapCell(headers[index], columnWidths[index]).length);
553
+ }
554
+ for (const row of rows) {
555
+ for (let index = 0; index < columnCount; index += 1) {
556
+ maxLines = Math.max(maxLines, wrapCell(row[index], columnWidths[index]).length);
557
+ }
558
+ }
559
+ return maxLines;
560
+ };
561
+
562
+ const renderVerticalRows = () => {
563
+ const rendered = [];
564
+ const separatorWidth = Math.min(Math.max(contentWidth - 2, 12), 40);
565
+ const separator = '─'.repeat(separatorWidth);
566
+ rows.forEach((row, rowIndex) => {
567
+ if (rowIndex > 0) rendered.push({ kind: 'table-vertical-separator', text: separator });
568
+ row.forEach((cell, cellIndex) => {
569
+ const label = headers[cellIndex] || `Column ${cellIndex + 1}`;
570
+ const firstWidth = Math.max(contentWidth - stringWidthLite(label) - 3, 10);
571
+ const nextWidth = Math.max(contentWidth - 3, 10);
572
+ const firstPass = wrapPlainText(cell, firstWidth, true);
573
+ const firstLine = firstPass[0] || '';
574
+ const remaining = firstPass.slice(1).join(' ');
575
+ const rest = remaining ? wrapPlainText(remaining, nextWidth, true) : [];
576
+ const wrapped = [firstLine, ...rest].filter((line, idx) => idx === 0 || line.trim());
577
+ rendered.push({
578
+ kind: 'table-vertical',
579
+ label,
580
+ text: wrapped[0] || ''
581
+ });
582
+ for (const line of wrapped.slice(1)) {
583
+ rendered.push({
584
+ kind: 'table-vertical-continuation',
585
+ text: line
586
+ });
587
+ }
588
+ });
589
+ });
590
+ return rendered;
591
+ };
592
+
593
+ if (computeMaxWrappedLines() > maxRowLines && contentWidth < 80) {
594
+ return renderVerticalRows();
595
+ }
596
+
597
+ const renderBorder = (type) => {
598
+ const chars = {
599
+ top: ['┌', '─', '┬', '┐'],
600
+ middle: ['├', '─', '┼', '┤'],
601
+ bottom: ['└', '─', '┴', '┘']
602
+ }[type];
603
+ let line = chars[0];
604
+ columnWidths.forEach((width, index) => {
605
+ line += chars[1].repeat(width + 2);
606
+ line += index < columnWidths.length - 1 ? chars[2] : chars[3];
607
+ });
608
+ return line;
609
+ };
610
+
611
+ const renderRowLines = (cells, isHeader = false) => {
612
+ const wrappedColumns = cells.map((cell, index) => wrapCell(cell, columnWidths[index]));
613
+ const maxLines = Math.max(...wrappedColumns.map((entry) => entry.length), 1);
614
+ const verticalOffsets = wrappedColumns.map((entry) => Math.floor((maxLines - entry.length) / 2));
615
+ const rendered = [];
616
+ for (let lineIndex = 0; lineIndex < maxLines; lineIndex += 1) {
617
+ let line = '│';
618
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
619
+ const wrapped = wrappedColumns[columnIndex];
620
+ const offset = verticalOffsets[columnIndex];
621
+ const contentIndex = lineIndex - offset;
622
+ const text = contentIndex >= 0 && contentIndex < wrapped.length ? wrapped[contentIndex] : '';
623
+ const align = isHeader ? 'center' : alignments[columnIndex];
624
+ line += ` ${padAlignedText(text, columnWidths[columnIndex], align)} │`;
625
+ }
626
+ rendered.push({
627
+ kind: 'table',
628
+ text: line,
629
+ isHeader
630
+ });
631
+ }
632
+ return rendered;
633
+ };
634
+
635
+ const tableLines = [
636
+ { kind: 'table-separator', text: renderBorder('top') },
637
+ ...renderRowLines(headers, true),
638
+ { kind: 'table-separator', text: renderBorder('middle') }
639
+ ];
640
+
641
+ rows.forEach((row, index) => {
642
+ tableLines.push(...renderRowLines(row, false));
643
+ if (index < rows.length - 1) {
644
+ tableLines.push({ kind: 'table-separator', text: renderBorder('middle') });
645
+ }
646
+ });
647
+ tableLines.push({ kind: 'table-separator', text: renderBorder('bottom') });
648
+
649
+ const maxLineWidth = Math.max(...tableLines.map((entry) => stringWidthLite(entry.text)));
650
+ if (maxLineWidth > contentWidth - safetyMargin) {
651
+ return renderVerticalRows();
652
+ }
653
+
654
+ return tableLines;
655
+ }
656
+
657
+ function parseRichTextSegments(line, baseColor) {
658
+ const parts = String(line || '').split(/(`[^`]+`|\*\*[^*]+\*\*)/g);
659
+ return parts.map((part, idx) => {
660
+ if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
661
+ return h(
662
+ Text,
663
+ { key: `ic-${idx}`, color: 'black', backgroundColor: 'yellow' },
664
+ part.slice(1, -1)
665
+ );
666
+ }
667
+ if (part.startsWith('**') && part.endsWith('**') && part.length >= 4) {
668
+ return h(Text, { key: `bd-${idx}`, color: 'cyanBright', bold: true }, part.slice(2, -2));
669
+ }
670
+ return h(Text, { key: `tx-${idx}`, color: baseColor }, part);
671
+ });
672
+ }
673
+
356
674
  function safeJsonParse(raw) {
357
675
  try {
358
676
  return JSON.parse(String(raw || '{}'));
@@ -371,33 +689,8 @@ function parseToolDisplayName(name) {
371
689
  };
372
690
  }
373
691
 
374
- function isCodeGenerationActivityName(name) {
375
- return String(name || '').trim() === 'Code generation';
376
- }
377
-
378
692
  export function buildPreToolNotice(name, copy) {
379
- const parsed = parseToolDisplayName(name);
380
- const base = parsed.base;
381
- const target = parsed.target ? trimText(parsed.target, 48) : '';
382
- const isEnglish = String(copy?.roleLabels?.coder || '').trim() === 'CODER' && String(copy?.roleLabels?.you || '').trim() === 'YOU';
383
-
384
- if (isEnglish) {
385
- if (base === 'read') return target ? `I'll inspect ${target} first.` : `I'll inspect the relevant file first.`;
386
- if (base === 'list' || base === 'glob') return target ? `I'll inspect the ${target} directory first.` : `I'll inspect the relevant directory first.`;
387
- if (base === 'grep') return `I'll search the relevant code first.`;
388
- if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') {
389
- return `I'll inspect the current code first, then make the change.`;
390
- }
391
- if (base === 'run') return `I'll verify the current project state first.`;
392
- return `I'll check the relevant project context first.`;
393
- }
394
-
395
- if (base === 'read') return target ? `我先查看 ${target} 的内容。` : '我先查看相关文件内容。';
396
- if (base === 'list' || base === 'glob') return target ? `我先查看 ${target} 目录里的内容。` : '我先查看相关目录内容。';
397
- if (base === 'grep') return '我先搜索相关代码位置。';
398
- if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') return '我先确认当前代码上下文,再动手修改。';
399
- if (base === 'run') return '我先检查当前项目状态。';
400
- return '我先查看相关上下文。';
693
+ return buildRegisteredPreToolNotice(name, copy);
401
694
  }
402
695
 
403
696
  export function shouldInjectPreToolNotice(msg) {
@@ -408,6 +701,33 @@ export function shouldInjectPreToolNotice(msg) {
408
701
  return !text && !hasTextSegment;
409
702
  }
410
703
 
704
+ function getLastToolActivity(msg, statuses = []) {
705
+ const allowed = new Set((Array.isArray(statuses) ? statuses : []).map((status) => String(status)));
706
+ const segments = Array.isArray(msg?.segments) ? msg.segments : [];
707
+ for (let idx = segments.length - 1; idx >= 0; idx -= 1) {
708
+ const segment = segments[idx];
709
+ if (segment?.type !== 'tool' && segment?.type !== 'system_tool') continue;
710
+ if (allowed.size === 0 || allowed.has(String(segment.status || ''))) return segment;
711
+ }
712
+ return null;
713
+ }
714
+
715
+ function hasOnlySyntheticNarration(msg) {
716
+ if (!msg?.syntheticPrelude) return false;
717
+ const segments = Array.isArray(msg?.segments) ? msg.segments : [];
718
+ const hasToolRows = segments.some((segment) => segment?.type === 'tool' || segment?.type === 'system_tool');
719
+ const textSegments = segments.filter((segment) => segment?.type === 'text' && String(segment.text || '').trim());
720
+ return hasToolRows && textSegments.length <= 1;
721
+ }
722
+
723
+ export function buildInterToolNotice(previousActivity, nextToolName, copy) {
724
+ return buildRegisteredInterToolNotice(previousActivity, nextToolName, copy);
725
+ }
726
+
727
+ export function buildSyntheticCompletionText(msg, copy) {
728
+ return buildRegisteredSyntheticCompletionText(msg, copy);
729
+ }
730
+
411
731
  function formatDurationMs(ms) {
412
732
  const safeMs = Math.max(0, Number(ms) || 0);
413
733
  return `${(safeMs / 1000).toFixed(1)}s`;
@@ -514,168 +834,19 @@ export function shouldShowCompletionFooter(msg) {
514
834
  }
515
835
 
516
836
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
517
- const parsed = parseToolDisplayName(name);
518
- if (parsed.base === 'project_index') {
519
- return blocked
520
- ? `${copy.toolActivity.blocked}: project index`
521
- : done
522
- ? copy.toolActivity.doneProjectIndex
523
- : copy.toolActivity.doingProjectIndex;
524
- }
525
- if (parsed.base === 'file_index') {
526
- const safeTarget = trimText(parsed.target || '.codemini-project/file-index.json', 72);
527
- return blocked
528
- ? `${copy.toolActivity.blocked}: ${safeTarget}`
529
- : done
530
- ? `${copy.toolActivity.doneFileIndex}: ${safeTarget}`
531
- : `${copy.toolActivity.doingFileIndex}: ${safeTarget}`;
532
- }
533
- if (parsed.base === 'run' || parsed.base === 'start_service') {
534
- const intent = classifyCommandIntent(parsed.target);
535
- const target = parsed.target || intent.kind || 'command';
536
- if (intent.kind === 'install') {
537
- return blocked
538
- ? `${copy.toolActivity.blocked}: ${target}`
539
- : done
540
- ? `${copy.toolActivity.doneInstall}: ${target}`
541
- : `${copy.toolActivity.doingInstall}: ${target}`;
542
- }
543
- if (intent.kind === 'build') {
544
- return blocked
545
- ? `${copy.toolActivity.blocked}: ${target}`
546
- : done
547
- ? `${copy.toolActivity.doneBuild}: ${target}`
548
- : `${copy.toolActivity.doingBuild}: ${target}`;
549
- }
550
- if (intent.kind === 'test') {
551
- return blocked
552
- ? `${copy.toolActivity.blocked}: ${target}`
553
- : done
554
- ? `${copy.toolActivity.doneTest}: ${target}`
555
- : `${copy.toolActivity.doingTest}: ${target}`;
556
- }
557
- if (intent.kind === 'frontend-service') {
558
- return blocked
559
- ? `${copy.toolActivity.blocked}: ${target}`
560
- : done
561
- ? `${copy.toolActivity.doneFrontend}: ${target}`
562
- : `${copy.toolActivity.doingFrontend}: ${target}`;
563
- }
564
- if (intent.kind === 'backend-service') {
565
- return blocked
566
- ? `${copy.toolActivity.blocked}: ${target}`
567
- : done
568
- ? `${copy.toolActivity.doneBackend}: ${target}`
569
- : `${copy.toolActivity.doingBackend}: ${target}`;
570
- }
571
- if (intent.kind === 'database-service') {
572
- return blocked
573
- ? `${copy.toolActivity.blocked}: ${target}`
574
- : done
575
- ? `${copy.toolActivity.doneDatabase}: ${target}`
576
- : `${copy.toolActivity.doingDatabase}: ${target}`;
577
- }
578
- if (intent.kind === 'docker-service') {
579
- return blocked
580
- ? `${copy.toolActivity.blocked}: ${target}`
581
- : done
582
- ? `${copy.toolActivity.doneDocker}: ${target}`
583
- : `${copy.toolActivity.doingDocker}: ${target}`;
584
- }
585
- if (intent.kind === 'service') {
586
- return blocked
587
- ? `${copy.toolActivity.blocked}: ${target}`
588
- : done
589
- ? `${copy.toolActivity.doneGeneric}: ${target}`
590
- : `${copy.toolActivity.doingGeneric}: ${target}`;
591
- }
592
- }
593
- if (isCodeGenerationActivityName(name)) {
594
- return blocked
595
- ? `${copy.toolActivity.blocked}: code generation`
596
- : done
597
- ? copy.toolActivity.doneCodeGeneration
598
- : copy.toolActivity.doingCodeGeneration;
599
- }
600
- const { raw, base, target } = parseToolDisplayName(name);
601
- const safeTarget = trimText(target, 72);
602
- if (base === 'read') {
603
- return blocked
604
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
605
- : done
606
- ? `${copy.toolActivity.doneRead}: ${safeTarget || '.'}`
607
- : `${copy.toolActivity.doingRead}: ${safeTarget || '.'}`;
608
- }
609
- if (base === 'edit') {
610
- return blocked
611
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
612
- : done
613
- ? `${copy.toolActivity.doneEdit}: ${safeTarget || '.'}`
614
- : `${copy.toolActivity.doingEdit}: ${safeTarget || '.'}`;
615
- }
616
- if (base === 'write') {
617
- return blocked
618
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
619
- : done
620
- ? `${copy.toolActivity.doneWrite}: ${safeTarget || '.'}`
621
- : `${copy.toolActivity.doingWrite}: ${safeTarget || '.'}`;
622
- }
623
- if (base === 'patch') {
624
- return blocked
625
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
626
- : done
627
- ? `${copy.toolActivity.donePatch}: ${safeTarget || '.'}`
628
- : `${copy.toolActivity.doingPatch}: ${safeTarget || '.'}`;
629
- }
630
- if (base === 'list' || base === 'glob' || base === 'grep') {
631
- return blocked
632
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
633
- : done
634
- ? `${copy.toolActivity.doneList}: ${safeTarget || '.'}`
635
- : `${copy.toolActivity.doingList}: ${safeTarget || '.'}`;
636
- }
637
- if (base === 'run') {
638
- return blocked
639
- ? `${copy.toolActivity.blocked}: ${safeTarget || base}`
640
- : done
641
- ? `${copy.toolActivity.doneCommand}: ${safeTarget || base}`
642
- : `${copy.toolActivity.doingCommand}: ${safeTarget || base}`;
643
- }
644
- if (base === 'start_service' || base === 'list_services' || base === 'get_service_status' || base === 'get_service_logs' || base === 'stop_service') {
645
- return blocked
646
- ? `${copy.toolActivity.blocked}: ${safeTarget || base}`
647
- : done
648
- ? `${copy.toolActivity.doneGeneric}: ${safeTarget || base}`
649
- : `${copy.toolActivity.doingGeneric}: ${safeTarget || base}`;
650
- }
651
- if (base === 'create_task') {
652
- return blocked ? `${copy.toolActivity.blocked}: create_task` : done ? copy.toolActivity.doneCreateTask : copy.toolActivity.doingCreateTask;
653
- }
654
- if (base === 'update_task') {
655
- return blocked ? `${copy.toolActivity.blocked}: update_task` : done ? copy.toolActivity.doneUpdateTask : copy.toolActivity.doingUpdateTask;
656
- }
657
- return blocked ? `${copy.toolActivity.blocked}: ${raw}` : done ? `${copy.toolActivity.doneGeneric}: ${raw}` : `${copy.toolActivity.doingGeneric}: ${raw}`;
837
+ return describeRegisteredToolActivity(copy, name, { done, blocked });
658
838
  }
659
839
 
660
840
  function describeSkillActivity(name, copy, { done = false, failed = false } = {}) {
661
- if (failed) return `${copy.runtime.skillFailed}: /${name}`;
662
- if (done) return `${copy.toolActivity.doneSkill}: /${name}`;
663
- return `${copy.toolActivity.doingSkill}: /${name}`;
841
+ return describeRegisteredSkillActivity(copy, name, { done, failed });
664
842
  }
665
843
 
666
844
  function describeAutoSkillActivity(names, copy) {
667
- const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
668
- if (safeNames.length === 0) return '';
669
- return copy.runtime.autoSkillInjected(safeNames);
845
+ return describeRegisteredAutoSkillActivity(copy, names);
670
846
  }
671
847
 
672
848
  function formatAutoSkillBadge(names, copy) {
673
- const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
674
- if (safeNames.length === 0) return '';
675
- const [first, ...rest] = safeNames;
676
- const suffix = rest.length > 0 ? ` +${rest.length}` : '';
677
- const prefix = copy?.roleLabels?.system === 'SYSTEM' ? 'AUTO' : '自动';
678
- return `${prefix} /${first}${suffix}`;
849
+ return formatRegisteredAutoSkillBadge(copy, names);
679
850
  }
680
851
 
681
852
  function normalizeRuntimeStatus(status, copy) {
@@ -880,20 +1051,31 @@ function Header({ sessionId, model, shellName, safeMode = true }) {
880
1051
  }
881
1052
 
882
1053
  function renderInlineCode(line, baseColor) {
883
- const parts = line.split(/(`[^`]+`)/g);
884
- return parts.map((part, idx) => {
885
- if (part.startsWith('`') && part.endsWith('`') && part.length >= 2) {
886
- return h(
887
- Text,
888
- { key: `ic-${idx}`, color: 'black', backgroundColor: 'yellow' },
889
- part.slice(1, -1)
890
- );
891
- }
892
- return h(Text, { key: `tx-${idx}`, color: baseColor }, part);
893
- });
1054
+ return parseRichTextSegments(line, baseColor);
894
1055
  }
895
1056
 
896
1057
  function renderTextLine(msg, line, idx, color) {
1058
+ const headingMatch = String(line || '').match(/^\s{0,3}(#{1,3})\s+(.*)$/);
1059
+ if (headingMatch) {
1060
+ const level = headingMatch[1].length;
1061
+ const title = headingMatch[2].trim();
1062
+ const accent = level === 1 ? 'cyanBright' : level === 2 ? 'greenBright' : 'yellowBright';
1063
+ return h(
1064
+ Box,
1065
+ { key: `ln-wrap-${msg.id}-${idx}` },
1066
+ h(Text, { color: accent, bold: true }, title)
1067
+ );
1068
+ }
1069
+
1070
+ const boldTitleMatch = String(line || '').match(/^\s*\*\*(.+)\*\*\s*$/);
1071
+ if (boldTitleMatch) {
1072
+ return h(
1073
+ Box,
1074
+ { key: `ln-wrap-${msg.id}-${idx}` },
1075
+ h(Text, { key: `ln-${msg.id}-${idx}`, color: 'cyanBright', bold: true }, boldTitleMatch[1].trim())
1076
+ );
1077
+ }
1078
+
897
1079
  return h(
898
1080
  Box,
899
1081
  { key: `ln-wrap-${msg.id}-${idx}` },
@@ -1544,12 +1726,70 @@ export function mergeActivitySummary(previousSummary, nextSummary, activityName)
1544
1726
  return lines.join('\n');
1545
1727
  }
1546
1728
 
1729
+ export function collapseActivityChainRows(inputRows, showToolDetails, copy, maxVisibleActivities = 3) {
1730
+ const rows = Array.isArray(inputRows) ? inputRows : [];
1731
+ if (showToolDetails) return rows.slice();
1732
+ const maxVisible = Math.max(1, Number(maxVisibleActivities) || 3);
1733
+ const collapsed = [];
1734
+
1735
+ const isCollapsibleActivity = (row) =>
1736
+ row?.kind === 'activity' &&
1737
+ ['tool', 'skill', 'system_tool'].includes(String(row?.activityType || 'tool'));
1738
+
1739
+ let index = 0;
1740
+ while (index < rows.length) {
1741
+ const row = rows[index];
1742
+ if (!isCollapsibleActivity(row)) {
1743
+ collapsed.push(row);
1744
+ index += 1;
1745
+ continue;
1746
+ }
1747
+
1748
+ const group = [];
1749
+ while (index < rows.length) {
1750
+ const next = rows[index];
1751
+ if (isCollapsibleActivity(next)) {
1752
+ group.push([next]);
1753
+ index += 1;
1754
+ continue;
1755
+ }
1756
+ if (next?.kind === 'activity-summary' && group.length > 0) {
1757
+ group[group.length - 1].push(next);
1758
+ index += 1;
1759
+ continue;
1760
+ }
1761
+ break;
1762
+ }
1763
+
1764
+ if (group.length <= maxVisible) {
1765
+ for (const item of group) collapsed.push(...item);
1766
+ continue;
1767
+ }
1768
+
1769
+ const hiddenCount = group.length - maxVisible;
1770
+ collapsed.push({
1771
+ kind: 'activity-collapsed',
1772
+ hiddenCount,
1773
+ text:
1774
+ copy?.generic?.toolChainCollapsed != null
1775
+ ? copy.generic.toolChainCollapsed(hiddenCount)
1776
+ : `${hiddenCount} earlier tool calls hidden`
1777
+ });
1778
+ for (const item of group.slice(-maxVisible)) {
1779
+ collapsed.push(...item);
1780
+ }
1781
+ }
1782
+
1783
+ return collapsed;
1784
+ }
1785
+
1547
1786
  function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
1548
1787
  const rows = [];
1549
1788
  const pushTextRows = (text) => {
1550
1789
  const lines = String(text || '').split('\n');
1551
1790
  let codeFence = false;
1552
- for (const line of lines) {
1791
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
1792
+ const line = lines[lineIndex];
1553
1793
  const trimmed = line.trim();
1554
1794
  const planProgress = parsePlanProgressLine(trimmed);
1555
1795
  if (planProgress) {
@@ -1570,6 +1810,16 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
1570
1810
  pushWrappedRow(rows, { kind: 'code', text: line || ' ', color: 'gray' }, contentWidth);
1571
1811
  continue;
1572
1812
  }
1813
+ if (isMarkdownTableHeader(line, lines[lineIndex + 1])) {
1814
+ const tableLines = [line];
1815
+ lineIndex += 1; // skip separator
1816
+ while (lineIndex + 1 < lines.length && splitMarkdownTableCells(lines[lineIndex + 1]).length > 1) {
1817
+ tableLines.push(lines[lineIndex + 1]);
1818
+ lineIndex += 1;
1819
+ }
1820
+ rows.push(...formatMarkdownTableBlock(tableLines, contentWidth));
1821
+ continue;
1822
+ }
1573
1823
  let color = msg.color || roleStyle(msg.label).text || 'white';
1574
1824
  if (line.startsWith('#')) color = 'cyanBright';
1575
1825
  else if (trimmed.startsWith('- ') || trimmed.startsWith('* ')) color = 'magentaBright';
@@ -1651,7 +1901,9 @@ function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy) {
1651
1901
  syntheticRows.push(...statusRows);
1652
1902
  }
1653
1903
 
1654
- return normalizeActivitySpacingRows(insertRowsAfterLastCodeRow(rows, syntheticRows));
1904
+ return normalizeActivitySpacingRows(
1905
+ insertRowsAfterLastCodeRow(collapseActivityChainRows(rows, showToolDetails, copy), syntheticRows)
1906
+ );
1655
1907
  }
1656
1908
 
1657
1909
  function renderMessageRow(msg, row, idx, loaderTick) {
@@ -1695,6 +1947,49 @@ function renderMessageRow(msg, row, idx, loaderTick) {
1695
1947
  h(Text, { color: 'gray' }, `└ ${row.text}`)
1696
1948
  );
1697
1949
  }
1950
+ if (row.kind === 'table') {
1951
+ return h(
1952
+ Box,
1953
+ { key: `row-table-${msg.id}-${idx}`, marginLeft: 1 },
1954
+ h(Text, { color: row.isHeader ? 'cyanBright' : 'gray', bold: Boolean(row.isHeader) }, row.text)
1955
+ );
1956
+ }
1957
+ if (row.kind === 'table-separator') {
1958
+ return h(
1959
+ Box,
1960
+ { key: `row-table-sep-${msg.id}-${idx}`, marginLeft: 1 },
1961
+ h(Text, { color: 'gray' }, row.text)
1962
+ );
1963
+ }
1964
+ if (row.kind === 'table-vertical') {
1965
+ return h(
1966
+ Box,
1967
+ { key: `row-table-v-${msg.id}-${idx}`, marginLeft: 1 },
1968
+ h(Text, { color: 'cyanBright', bold: true }, `${row.label}:`),
1969
+ h(Text, { color: 'gray' }, row.text ? ` ${row.text}` : '')
1970
+ );
1971
+ }
1972
+ if (row.kind === 'table-vertical-continuation') {
1973
+ return h(
1974
+ Box,
1975
+ { key: `row-table-vc-${msg.id}-${idx}`, marginLeft: 3 },
1976
+ h(Text, { color: 'gray' }, row.text)
1977
+ );
1978
+ }
1979
+ if (row.kind === 'table-vertical-separator') {
1980
+ return h(
1981
+ Box,
1982
+ { key: `row-table-vs-${msg.id}-${idx}`, marginLeft: 1 },
1983
+ h(Text, { color: 'gray' }, row.text)
1984
+ );
1985
+ }
1986
+ if (row.kind === 'activity-collapsed') {
1987
+ return h(
1988
+ Box,
1989
+ { key: `row-tool-collapsed-${msg.id}-${idx}`, marginLeft: 1 },
1990
+ h(Text, { color: 'gray' }, `└ ${row.text}`)
1991
+ );
1992
+ }
1698
1993
  if (row.kind === 'plan-progress') {
1699
1994
  return h(
1700
1995
  Box,
@@ -2539,6 +2834,26 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2539
2834
  return aid;
2540
2835
  };
2541
2836
 
2837
+ const maybeRefreshSyntheticNarration = (nextToolName) => {
2838
+ const targetId = activeAssistantIdRef.current;
2839
+ if (!targetId || !nextToolName) return;
2840
+ setMessages((prev) =>
2841
+ prev.map((m) => {
2842
+ if (m.id !== targetId) return m;
2843
+ if (!hasOnlySyntheticNarration(m)) return m;
2844
+ const previousActivity = getLastToolActivity(m, ['done', 'running']);
2845
+ if (!previousActivity) return m;
2846
+ const bridge = buildInterToolNotice(previousActivity, nextToolName, copy);
2847
+ if (!bridge) return m;
2848
+ return {
2849
+ ...m,
2850
+ text: bridge,
2851
+ syntheticPrelude: true
2852
+ };
2853
+ })
2854
+ );
2855
+ };
2856
+
2542
2857
  const runSubmission = (line, userMessageId = null) => {
2543
2858
  inFlightRef.current = true;
2544
2859
  activeUserMessageIdRef.current = userMessageId;
@@ -2632,9 +2947,15 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2632
2947
  setMessages((prev) =>
2633
2948
  prev.map((m) => {
2634
2949
  if (m.id !== targetId) return m;
2950
+ const responseText = typeof event.text === 'string' ? event.text.trim() : '';
2951
+ const shouldSynthesizeCompletion = !responseText && m.syntheticPrelude;
2635
2952
  return {
2636
2953
  ...m,
2637
- ...(typeof event.text === 'string' && event.text.length > 0 ? { text: event.text } : {}),
2954
+ ...(responseText
2955
+ ? { text: event.text, syntheticPrelude: false }
2956
+ : shouldSynthesizeCompletion
2957
+ ? { text: buildSyntheticCompletionText(m, copy), syntheticPrelude: false }
2958
+ : {}),
2638
2959
  loading: false,
2639
2960
  phase: undefined,
2640
2961
  liveStatus: undefined,
@@ -2658,6 +2979,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2658
2979
  }
2659
2980
  if (event?.type === 'tool:start') {
2660
2981
  ensureActiveAssistant();
2982
+ maybeRefreshSyntheticNarration(event.name);
2661
2983
  const detail = describeToolActivity(event.name, copy);
2662
2984
  setRuntimeStatus(makeStatus(copy.runtime.toolRunning, detail, 'magentaBright'));
2663
2985
  setInputStage('tooling');