codemini-cli 0.1.11 → 0.1.13

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.
@@ -3,6 +3,7 @@ import { Box, Text, useApp, useInput } from 'ink';
3
3
  import { shouldCaptureEscapeSequence } from './input-escape.js';
4
4
 
5
5
  const h = React.createElement;
6
+ const SUGGESTION_PAGE_SIZE = 8;
6
7
  const BANNER = [
7
8
  ' ██████ ██████ ██████ ███████ ███ ███ ██ ███ ██ ██ ',
8
9
  '██ ██ ██ ██ ██ ██ ████ ████ ██ ████ ██ ██ ',
@@ -122,13 +123,13 @@ const TUI_COPY = {
122
123
  },
123
124
  suggestion: {
124
125
  singleTab: 'Tab 补全当前命令',
125
- navFill: 'Tab 保持切换模式,↑↓选择,Enter 填入',
126
- navEnter: 'Tab 进入切换模式,再用 ↑↓ 选择',
126
+ navFill: 'Tab 保持切换模式,↑↓选择,←→翻页,Enter 填入',
127
+ navEnter: 'Tab 进入切换模式,再用 ↑↓ 选择,←→翻页',
127
128
  noSuggestions: '/ 查看命令,Tab 自动补全,↑↓ 历史,Ctrl+T 展开工具',
128
129
  oneNav: 'Tab 或 Enter 填入当前命令,↑↓ 历史',
129
130
  oneIdle: 'Tab 补全当前唯一候选,Enter 直接发送,↑↓ 历史',
130
- manyNav: (count) => `Tab 切换候选,↑↓选择,Enter 填入 (${count} 项)`,
131
- manyIdle: (count) => `Tab 进入候选切换 (${count} 项),↑↓ 历史`
131
+ manyNav: (count) => `Tab 切换候选,↑↓选择,←→翻页,Enter 填入 (${count} 项)`,
132
+ manyIdle: (count) => `Tab 进入候选切换 (${count} 项),↑↓ 历史,←→翻页`
132
133
  },
133
134
  runtime: {
134
135
  sendingToGateway: '正在发送到网关',
@@ -224,13 +225,13 @@ const TUI_COPY = {
224
225
  },
225
226
  suggestion: {
226
227
  singleTab: 'Tab completes the current command',
227
- navFill: 'Tab stays in pick mode, ↑↓ select, Enter applies',
228
- navEnter: 'Tab enters pick mode, then use ↑↓ to choose',
228
+ navFill: 'Tab stays in pick mode, ↑↓ select, ←→ page, Enter applies',
229
+ navEnter: 'Tab enters pick mode, then use ↑↓ to choose, ←→ page',
229
230
  noSuggestions: '/ shows commands, Tab autocompletes, ↑↓ history, Ctrl+T tools',
230
231
  oneNav: 'Tab or Enter applies the current command, ↑↓ history',
231
232
  oneIdle: 'Tab completes the only candidate, Enter sends, ↑↓ history',
232
- manyNav: (count) => `Tab cycles candidates, ↑↓ select, Enter applies (${count} items)`,
233
- manyIdle: (count) => `Tab enters candidate mode (${count} items), ↑↓ history`
233
+ manyNav: (count) => `Tab cycles candidates, ↑↓ select, ←→ page, Enter applies (${count} items)`,
234
+ manyIdle: (count) => `Tab enters candidate mode (${count} items), ↑↓ history, ←→ page`
234
235
  },
235
236
  runtime: {
236
237
  sendingToGateway: 'sending to gateway',
@@ -546,6 +547,161 @@ function renderTextLine(msg, line, idx, color) {
546
547
  );
547
548
  }
548
549
 
550
+ export function parseAutoPlanSummaryMessage(text) {
551
+ const raw = String(text || '').trim();
552
+ if (!/^Auto plan finished\b/i.test(raw)) return null;
553
+
554
+ const lines = raw
555
+ .split('\n')
556
+ .map((line) => line.trim())
557
+ .filter(Boolean);
558
+ const parsed = {
559
+ statusTitle: lines[0] || '',
560
+ filePath: '',
561
+ planSummary: '',
562
+ finalSummary: '',
563
+ stepsTotal: '',
564
+ completed: '',
565
+ warnings: '',
566
+ failed: '',
567
+ warningSteps: '',
568
+ failedSteps: ''
569
+ };
570
+
571
+ for (const line of lines.slice(1)) {
572
+ if (line.startsWith('File: ')) parsed.filePath = line.slice('File: '.length).trim();
573
+ else if (line.startsWith('Plan Summary: ')) parsed.planSummary = line.slice('Plan Summary: '.length).trim();
574
+ else if (line.startsWith('Final Summary: ')) parsed.finalSummary = line.slice('Final Summary: '.length).trim();
575
+ else if (line.startsWith('Steps: ')) parsed.stepsTotal = line.slice('Steps: '.length).trim();
576
+ else if (line.startsWith('Completed: ')) parsed.completed = line.slice('Completed: '.length).trim();
577
+ else if (line.startsWith('Warnings: ')) parsed.warnings = line.slice('Warnings: '.length).trim();
578
+ else if (line.startsWith('Failed: ')) parsed.failed = line.slice('Failed: '.length).trim();
579
+ else if (line.startsWith('Warning steps: ')) parsed.warningSteps = line.slice('Warning steps: '.length).trim();
580
+ else if (line.startsWith('Failed steps: ')) parsed.failedSteps = line.slice('Failed steps: '.length).trim();
581
+ }
582
+
583
+ return parsed;
584
+ }
585
+
586
+ function PlanSummaryBubble({ msg, copy }) {
587
+ const theme = roleStyle(msg.label);
588
+ const summary = msg.planSummary || parseAutoPlanSummaryMessage(msg.text);
589
+ if (!summary) return null;
590
+
591
+ const statusColor =
592
+ Number(summary.failed || 0) > 0 ? 'redBright' : Number(summary.warnings || 0) > 0 ? 'yellowBright' : 'greenBright';
593
+ const isEnglish = copy?.roleLabels?.system === 'SYSTEM';
594
+ const labels = isEnglish
595
+ ? {
596
+ conclusion: 'Conclusion',
597
+ plan: 'Plan',
598
+ warnings: 'Warnings',
599
+ failed: 'Failed',
600
+ file: 'File',
601
+ steps: 'steps',
602
+ done: 'done',
603
+ warn: 'warn',
604
+ fail: 'fail'
605
+ }
606
+ : {
607
+ conclusion: '结论',
608
+ plan: '计划',
609
+ warnings: '警告',
610
+ failed: '失败',
611
+ file: '文件',
612
+ steps: '步骤',
613
+ done: '完成',
614
+ warn: '警告',
615
+ fail: '失败'
616
+ };
617
+ const metaItems = [
618
+ summary.stepsTotal ? `${labels.steps} ${summary.stepsTotal}` : '',
619
+ summary.completed ? `${labels.done} ${summary.completed}` : '',
620
+ summary.warnings ? `${labels.warn} ${summary.warnings}` : '',
621
+ summary.failed ? `${labels.fail} ${summary.failed}` : ''
622
+ ].filter(Boolean);
623
+ const shortFile = summary.filePath ? trimText(summary.filePath, 96) : '';
624
+
625
+ return h(
626
+ Box,
627
+ { marginBottom: 1, flexDirection: 'row' },
628
+ h(Box, { width: 2 }, h(Text, { color: theme.accent }, '│')),
629
+ h(
630
+ Box,
631
+ {
632
+ flexDirection: 'column',
633
+ borderStyle: 'round',
634
+ borderColor: theme.border,
635
+ paddingX: 1,
636
+ paddingY: 0,
637
+ width: '100%'
638
+ },
639
+ h(
640
+ Box,
641
+ { justifyContent: 'space-between', marginBottom: summary.finalSummary ? 1 : 0 },
642
+ h(
643
+ Box,
644
+ null,
645
+ h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `),
646
+ h(Text, { color: 'gray' }, ' '),
647
+ h(Text, { color: statusColor }, summary.statusTitle)
648
+ ),
649
+ h(Text, { color: theme.chrome }, ' ')
650
+ ),
651
+ summary.finalSummary
652
+ ? h(
653
+ Box,
654
+ { marginBottom: summary.planSummary || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
655
+ h(Text, { color: statusColor }, labels.conclusion),
656
+ h(Text, { color: 'white' }, summary.finalSummary)
657
+ )
658
+ : null,
659
+ summary.planSummary
660
+ ? h(
661
+ Box,
662
+ { marginBottom: metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
663
+ h(Text, { color: 'cyanBright' }, labels.plan),
664
+ h(Text, { color: 'gray' }, summary.planSummary)
665
+ )
666
+ : null,
667
+ metaItems.length > 0
668
+ ? h(
669
+ Box,
670
+ { marginBottom: summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0 },
671
+ ...metaItems.flatMap((item, idx) => [
672
+ idx > 0 ? h(Text, { key: `sep-${idx}`, color: 'gray' }, ' ') : null,
673
+ h(Text, { key: `meta-${idx}`, color: 'gray' }, item)
674
+ ])
675
+ )
676
+ : null,
677
+ summary.warningSteps
678
+ ? h(
679
+ Box,
680
+ { marginBottom: summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
681
+ h(Text, { color: 'yellowBright' }, labels.warnings),
682
+ h(Text, { color: 'gray' }, summary.warningSteps)
683
+ )
684
+ : null,
685
+ summary.failedSteps
686
+ ? h(
687
+ Box,
688
+ { marginBottom: shortFile ? 1 : 0, flexDirection: 'column' },
689
+ h(Text, { color: 'redBright' }, labels.failed),
690
+ h(Text, { color: 'gray' }, summary.failedSteps)
691
+ )
692
+ : null,
693
+ shortFile
694
+ ? h(
695
+ Box,
696
+ { flexDirection: 'column' },
697
+ h(Text, { color: 'gray' }, labels.file),
698
+ h(Text, { color: 'gray' }, shortFile)
699
+ )
700
+ : null
701
+ )
702
+ );
703
+ }
704
+
549
705
  const BUBBLE_CHROME_ROWS = 4;
550
706
 
551
707
  function charDisplayWidth(ch) {
@@ -728,7 +884,70 @@ function getSuggestionDisplay(item) {
728
884
  return typeof item === 'string' ? item : String(item?.display || item?.value || '');
729
885
  }
730
886
 
887
+ function getSuggestionDescription(item) {
888
+ return typeof item === 'string' ? '' : String(item?.description || '');
889
+ }
890
+
891
+ export function formatSuggestionDescription(text, maxChars = 40) {
892
+ const value = String(text || '').replace(/\s+/g, ' ').trim();
893
+ if (!value) return '';
894
+ const limit = Math.max(4, Number(maxChars) || 40);
895
+ if (value.length <= limit) return value;
896
+ return `${value.slice(0, limit - 3)}...`;
897
+ }
898
+
899
+ export function getSuggestionPageState(commandSuggestions, menuIndex, pageSize = SUGGESTION_PAGE_SIZE) {
900
+ const items = Array.isArray(commandSuggestions) ? commandSuggestions : [];
901
+ const normalizedPageSize = Math.max(1, Number(pageSize) || SUGGESTION_PAGE_SIZE);
902
+ const safeIndex = items.length === 0 ? 0 : Math.max(0, Math.min(Number(menuIndex) || 0, items.length - 1));
903
+ const pageIndex = Math.floor(safeIndex / normalizedPageSize);
904
+ const pageCount = Math.max(1, Math.ceil(items.length / normalizedPageSize));
905
+ const pageStart = pageIndex * normalizedPageSize;
906
+ const pageEnd = Math.min(items.length, pageStart + normalizedPageSize);
907
+ return {
908
+ pageSize: normalizedPageSize,
909
+ safeIndex,
910
+ pageIndex,
911
+ pageCount,
912
+ pageStart,
913
+ pageEnd,
914
+ pageItems: items.slice(pageStart, pageEnd)
915
+ };
916
+ }
917
+
918
+ export function moveSuggestionSelection(currentIndex, itemCount, direction, pageSize = SUGGESTION_PAGE_SIZE) {
919
+ const total = Math.max(0, Number(itemCount) || 0);
920
+ if (total <= 0) return 0;
921
+ const normalizedPageSize = Math.max(1, Number(pageSize) || SUGGESTION_PAGE_SIZE);
922
+ const safeIndex = Math.max(0, Math.min(Number(currentIndex) || 0, total - 1));
923
+
924
+ if (direction === 'left') {
925
+ if (safeIndex < normalizedPageSize) return 0;
926
+ return Math.max(0, safeIndex - normalizedPageSize);
927
+ }
928
+
929
+ if (direction === 'right') {
930
+ const currentPageStart = Math.floor(safeIndex / normalizedPageSize) * normalizedPageSize;
931
+ const currentPageEnd = Math.min(total, currentPageStart + normalizedPageSize);
932
+ if (currentPageEnd >= total) return safeIndex;
933
+ return Math.min(total - 1, safeIndex + normalizedPageSize);
934
+ }
935
+
936
+ if (direction === 'up') {
937
+ return Math.max(0, safeIndex - 1);
938
+ }
939
+
940
+ if (direction === 'down') {
941
+ return Math.min(total - 1, safeIndex + 1);
942
+ }
943
+
944
+ return safeIndex;
945
+ }
946
+
731
947
  function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
948
+ if (msg?.planSummary || parseAutoPlanSummaryMessage(msg?.text)) {
949
+ return h(PlanSummaryBubble, { msg, copy });
950
+ }
732
951
  const theme = roleStyle(msg.label);
733
952
  const allRows = buildMessageRows(msg, showToolDetails, contentWidth);
734
953
  const start = rowWindow ? Math.max(0, rowWindow.start || 0) : 0;
@@ -870,7 +1089,8 @@ function MessageList({ messages, loaderTick, showToolDetails, contentWidth = 72,
870
1089
 
871
1090
  function SuggestionPanel({ commandSuggestions, suggestionNav, menuIndex, copy }) {
872
1091
  if (commandSuggestions.length === 0) return null;
873
- const grouped = groupCommandSuggestions(commandSuggestions);
1092
+ const pageState = getSuggestionPageState(commandSuggestions, menuIndex);
1093
+ const grouped = groupCommandSuggestions(pageState.pageItems);
874
1094
  let flatIndex = -1;
875
1095
  const panelHint =
876
1096
  commandSuggestions.length === 1
@@ -892,7 +1112,7 @@ function SuggestionPanel({ commandSuggestions, suggestionNav, menuIndex, copy })
892
1112
  Box,
893
1113
  { marginBottom: 1 },
894
1114
  h(Text, { color: 'magentaBright' }, suggestionNav ? copy.generic.commandPaletteGroupedSelect : copy.generic.commandPaletteGroupedSuggestions),
895
- h(Text, { color: 'gray' }, ` ${panelHint}`)
1115
+ h(Text, { color: 'gray' }, ` ${panelHint} · ${pageState.pageIndex + 1}/${pageState.pageCount}`)
896
1116
  ),
897
1117
  ...grouped.flatMap(([group, items]) => {
898
1118
  const nodes = [
@@ -905,13 +1125,17 @@ function SuggestionPanel({ commandSuggestions, suggestionNav, menuIndex, copy })
905
1125
  ];
906
1126
  items.forEach((c) => {
907
1127
  flatIndex += 1;
908
- const active = suggestionNav && menuIndex === flatIndex;
1128
+ const active = suggestionNav && menuIndex === pageState.pageStart + flatIndex;
909
1129
  const label = getSuggestionDisplay(c);
1130
+ const description = formatSuggestionDescription(getSuggestionDescription(c), 42);
910
1131
  nodes.push(
911
1132
  h(
912
1133
  Box,
913
1134
  { key: `opt-${group}-${getSuggestionValue(c)}` },
914
- h(Text, { color: active ? 'black' : 'magenta', backgroundColor: active ? 'magentaBright' : undefined }, `${active ? ' > ' : ' '}${label}`)
1135
+ h(Text, { color: active ? 'black' : 'magenta', backgroundColor: active ? 'magentaBright' : undefined }, `${active ? ' > ' : ' '}${label}`),
1136
+ description
1137
+ ? h(Text, { color: active ? 'black' : 'gray', backgroundColor: active ? 'magentaBright' : undefined }, ` ${description}`)
1138
+ : null
915
1139
  )
916
1140
  );
917
1141
  });
@@ -1143,7 +1367,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1143
1367
 
1144
1368
  const commandSuggestions =
1145
1369
  inputValue.startsWith('/')
1146
- ? (runtime.getCompletionOptions(inputValue) || []).slice(0, 8)
1370
+ ? runtime.getCompletionOptions(inputValue) || []
1147
1371
  : [];
1148
1372
  const hasTransientPanels =
1149
1373
  commandSuggestions.length > 0 || pendingQueue.length > 0 || debugKeys || Boolean(planState?.total);
@@ -1338,9 +1562,16 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1338
1562
  }
1339
1563
  return;
1340
1564
  }
1565
+ const parsedPlanSummary = result.type === 'system' ? parseAutoPlanSummaryMessage(result.text || '') : null;
1341
1566
  setMessages((prev) => [
1342
1567
  ...prev,
1343
- { id: nextId(), label: 'system', text: result.text || '', color: 'yellowBright' }
1568
+ {
1569
+ id: nextId(),
1570
+ label: 'system',
1571
+ text: result.text || '',
1572
+ color: 'yellowBright',
1573
+ ...(parsedPlanSummary ? { planSummary: parsedPlanSummary } : {})
1574
+ }
1344
1575
  ]);
1345
1576
  };
1346
1577
 
@@ -1688,7 +1919,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1688
1919
 
1689
1920
  if (key.upArrow) {
1690
1921
  if (suggestionNav && commandSuggestions.length > 0) {
1691
- setMenuIndex((prev) => Math.max(0, prev - 1));
1922
+ setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'up'));
1692
1923
  return;
1693
1924
  }
1694
1925
  if (history.length === 0) return;
@@ -1714,7 +1945,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1714
1945
 
1715
1946
  if (key.downArrow) {
1716
1947
  if (suggestionNav && commandSuggestions.length > 0) {
1717
- setMenuIndex((prev) => Math.min(commandSuggestions.length - 1, prev + 1));
1948
+ setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'down'));
1718
1949
  return;
1719
1950
  }
1720
1951
  if (history.length === 0 || historyIndex === null) return;
@@ -1735,6 +1966,10 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1735
1966
  return;
1736
1967
  }
1737
1968
  if (key.leftArrow) {
1969
+ if (suggestionNav && commandSuggestions.length > 0) {
1970
+ setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'left'));
1971
+ return;
1972
+ }
1738
1973
  setSuggestionNav(false);
1739
1974
  const next = Math.max(0, cursorIndexRef.current - 1);
1740
1975
  cursorIndexRef.current = next;
@@ -1742,6 +1977,10 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
1742
1977
  return;
1743
1978
  }
1744
1979
  if (key.rightArrow) {
1980
+ if (suggestionNav && commandSuggestions.length > 0) {
1981
+ setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'right'));
1982
+ return;
1983
+ }
1745
1984
  setSuggestionNav(false);
1746
1985
  const next = Math.min(inputValue.length, cursorIndexRef.current + 1);
1747
1986
  cursorIndexRef.current = next;