codemini-cli 0.2.8 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemini-cli",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Coding CLI optimized for small-model workflows and Windows PowerShell",
5
5
  "keywords": [
6
6
  "cli",
package/src/cli.js CHANGED
@@ -4,7 +4,7 @@ import { handleConfig } from './commands/config.js';
4
4
  import { handleDoctor } from './commands/doctor.js';
5
5
  import { handleSkill } from './commands/skill.js';
6
6
 
7
- const VERSION = '0.2.8';
7
+ const VERSION = '0.2.9';
8
8
 
9
9
  function printHelp() {
10
10
  console.log(`codemini ${VERSION}
@@ -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;
@@ -675,33 +689,8 @@ function parseToolDisplayName(name) {
675
689
  };
676
690
  }
677
691
 
678
- function isCodeGenerationActivityName(name) {
679
- return String(name || '').trim() === 'Code generation';
680
- }
681
-
682
692
  export function buildPreToolNotice(name, copy) {
683
- const parsed = parseToolDisplayName(name);
684
- const base = parsed.base;
685
- const target = parsed.target ? trimText(parsed.target, 48) : '';
686
- const isEnglish = String(copy?.roleLabels?.coder || '').trim() === 'CODER' && String(copy?.roleLabels?.you || '').trim() === 'YOU';
687
-
688
- if (isEnglish) {
689
- if (base === 'read') return target ? `I'll inspect ${target} first.` : `I'll inspect the relevant file first.`;
690
- if (base === 'list' || base === 'glob') return target ? `I'll inspect the ${target} directory first.` : `I'll inspect the relevant directory first.`;
691
- if (base === 'grep') return `I'll search the relevant code first.`;
692
- if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') {
693
- return `I'll inspect the current code first, then make the change.`;
694
- }
695
- if (base === 'run') return `I'll verify the current project state first.`;
696
- return `I'll check the relevant project context first.`;
697
- }
698
-
699
- if (base === 'read') return target ? `我先查看 ${target} 的内容。` : '我先查看相关文件内容。';
700
- if (base === 'list' || base === 'glob') return target ? `我先查看 ${target} 目录里的内容。` : '我先查看相关目录内容。';
701
- if (base === 'grep') return '我先搜索相关代码位置。';
702
- if (base === 'edit' || base === 'write' || base === 'patch' || base === 'generate_diff') return '我先确认当前代码上下文,再动手修改。';
703
- if (base === 'run') return '我先检查当前项目状态。';
704
- return '我先查看相关上下文。';
693
+ return buildRegisteredPreToolNotice(name, copy);
705
694
  }
706
695
 
707
696
  export function shouldInjectPreToolNotice(msg) {
@@ -712,6 +701,33 @@ export function shouldInjectPreToolNotice(msg) {
712
701
  return !text && !hasTextSegment;
713
702
  }
714
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
+
715
731
  function formatDurationMs(ms) {
716
732
  const safeMs = Math.max(0, Number(ms) || 0);
717
733
  return `${(safeMs / 1000).toFixed(1)}s`;
@@ -818,168 +834,19 @@ export function shouldShowCompletionFooter(msg) {
818
834
  }
819
835
 
820
836
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
821
- const parsed = parseToolDisplayName(name);
822
- if (parsed.base === 'project_index') {
823
- return blocked
824
- ? `${copy.toolActivity.blocked}: project index`
825
- : done
826
- ? copy.toolActivity.doneProjectIndex
827
- : copy.toolActivity.doingProjectIndex;
828
- }
829
- if (parsed.base === 'file_index') {
830
- const safeTarget = trimText(parsed.target || '.codemini-project/file-index.json', 72);
831
- return blocked
832
- ? `${copy.toolActivity.blocked}: ${safeTarget}`
833
- : done
834
- ? `${copy.toolActivity.doneFileIndex}: ${safeTarget}`
835
- : `${copy.toolActivity.doingFileIndex}: ${safeTarget}`;
836
- }
837
- if (parsed.base === 'run' || parsed.base === 'start_service') {
838
- const intent = classifyCommandIntent(parsed.target);
839
- const target = parsed.target || intent.kind || 'command';
840
- if (intent.kind === 'install') {
841
- return blocked
842
- ? `${copy.toolActivity.blocked}: ${target}`
843
- : done
844
- ? `${copy.toolActivity.doneInstall}: ${target}`
845
- : `${copy.toolActivity.doingInstall}: ${target}`;
846
- }
847
- if (intent.kind === 'build') {
848
- return blocked
849
- ? `${copy.toolActivity.blocked}: ${target}`
850
- : done
851
- ? `${copy.toolActivity.doneBuild}: ${target}`
852
- : `${copy.toolActivity.doingBuild}: ${target}`;
853
- }
854
- if (intent.kind === 'test') {
855
- return blocked
856
- ? `${copy.toolActivity.blocked}: ${target}`
857
- : done
858
- ? `${copy.toolActivity.doneTest}: ${target}`
859
- : `${copy.toolActivity.doingTest}: ${target}`;
860
- }
861
- if (intent.kind === 'frontend-service') {
862
- return blocked
863
- ? `${copy.toolActivity.blocked}: ${target}`
864
- : done
865
- ? `${copy.toolActivity.doneFrontend}: ${target}`
866
- : `${copy.toolActivity.doingFrontend}: ${target}`;
867
- }
868
- if (intent.kind === 'backend-service') {
869
- return blocked
870
- ? `${copy.toolActivity.blocked}: ${target}`
871
- : done
872
- ? `${copy.toolActivity.doneBackend}: ${target}`
873
- : `${copy.toolActivity.doingBackend}: ${target}`;
874
- }
875
- if (intent.kind === 'database-service') {
876
- return blocked
877
- ? `${copy.toolActivity.blocked}: ${target}`
878
- : done
879
- ? `${copy.toolActivity.doneDatabase}: ${target}`
880
- : `${copy.toolActivity.doingDatabase}: ${target}`;
881
- }
882
- if (intent.kind === 'docker-service') {
883
- return blocked
884
- ? `${copy.toolActivity.blocked}: ${target}`
885
- : done
886
- ? `${copy.toolActivity.doneDocker}: ${target}`
887
- : `${copy.toolActivity.doingDocker}: ${target}`;
888
- }
889
- if (intent.kind === 'service') {
890
- return blocked
891
- ? `${copy.toolActivity.blocked}: ${target}`
892
- : done
893
- ? `${copy.toolActivity.doneGeneric}: ${target}`
894
- : `${copy.toolActivity.doingGeneric}: ${target}`;
895
- }
896
- }
897
- if (isCodeGenerationActivityName(name)) {
898
- return blocked
899
- ? `${copy.toolActivity.blocked}: code generation`
900
- : done
901
- ? copy.toolActivity.doneCodeGeneration
902
- : copy.toolActivity.doingCodeGeneration;
903
- }
904
- const { raw, base, target } = parseToolDisplayName(name);
905
- const safeTarget = trimText(target, 72);
906
- if (base === 'read') {
907
- return blocked
908
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
909
- : done
910
- ? `${copy.toolActivity.doneRead}: ${safeTarget || '.'}`
911
- : `${copy.toolActivity.doingRead}: ${safeTarget || '.'}`;
912
- }
913
- if (base === 'edit') {
914
- return blocked
915
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
916
- : done
917
- ? `${copy.toolActivity.doneEdit}: ${safeTarget || '.'}`
918
- : `${copy.toolActivity.doingEdit}: ${safeTarget || '.'}`;
919
- }
920
- if (base === 'write') {
921
- return blocked
922
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
923
- : done
924
- ? `${copy.toolActivity.doneWrite}: ${safeTarget || '.'}`
925
- : `${copy.toolActivity.doingWrite}: ${safeTarget || '.'}`;
926
- }
927
- if (base === 'patch') {
928
- return blocked
929
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
930
- : done
931
- ? `${copy.toolActivity.donePatch}: ${safeTarget || '.'}`
932
- : `${copy.toolActivity.doingPatch}: ${safeTarget || '.'}`;
933
- }
934
- if (base === 'list' || base === 'glob' || base === 'grep') {
935
- return blocked
936
- ? `${copy.toolActivity.blocked}: ${base}(${safeTarget || '.'})`
937
- : done
938
- ? `${copy.toolActivity.doneList}: ${safeTarget || '.'}`
939
- : `${copy.toolActivity.doingList}: ${safeTarget || '.'}`;
940
- }
941
- if (base === 'run') {
942
- return blocked
943
- ? `${copy.toolActivity.blocked}: ${safeTarget || base}`
944
- : done
945
- ? `${copy.toolActivity.doneCommand}: ${safeTarget || base}`
946
- : `${copy.toolActivity.doingCommand}: ${safeTarget || base}`;
947
- }
948
- if (base === 'start_service' || base === 'list_services' || base === 'get_service_status' || base === 'get_service_logs' || base === 'stop_service') {
949
- return blocked
950
- ? `${copy.toolActivity.blocked}: ${safeTarget || base}`
951
- : done
952
- ? `${copy.toolActivity.doneGeneric}: ${safeTarget || base}`
953
- : `${copy.toolActivity.doingGeneric}: ${safeTarget || base}`;
954
- }
955
- if (base === 'create_task') {
956
- return blocked ? `${copy.toolActivity.blocked}: create_task` : done ? copy.toolActivity.doneCreateTask : copy.toolActivity.doingCreateTask;
957
- }
958
- if (base === 'update_task') {
959
- return blocked ? `${copy.toolActivity.blocked}: update_task` : done ? copy.toolActivity.doneUpdateTask : copy.toolActivity.doingUpdateTask;
960
- }
961
- return blocked ? `${copy.toolActivity.blocked}: ${raw}` : done ? `${copy.toolActivity.doneGeneric}: ${raw}` : `${copy.toolActivity.doingGeneric}: ${raw}`;
837
+ return describeRegisteredToolActivity(copy, name, { done, blocked });
962
838
  }
963
839
 
964
840
  function describeSkillActivity(name, copy, { done = false, failed = false } = {}) {
965
- if (failed) return `${copy.runtime.skillFailed}: /${name}`;
966
- if (done) return `${copy.toolActivity.doneSkill}: /${name}`;
967
- return `${copy.toolActivity.doingSkill}: /${name}`;
841
+ return describeRegisteredSkillActivity(copy, name, { done, failed });
968
842
  }
969
843
 
970
844
  function describeAutoSkillActivity(names, copy) {
971
- const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
972
- if (safeNames.length === 0) return '';
973
- return copy.runtime.autoSkillInjected(safeNames);
845
+ return describeRegisteredAutoSkillActivity(copy, names);
974
846
  }
975
847
 
976
848
  function formatAutoSkillBadge(names, copy) {
977
- const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
978
- if (safeNames.length === 0) return '';
979
- const [first, ...rest] = safeNames;
980
- const suffix = rest.length > 0 ? ` +${rest.length}` : '';
981
- const prefix = copy?.roleLabels?.system === 'SYSTEM' ? 'AUTO' : '自动';
982
- return `${prefix} /${first}${suffix}`;
849
+ return formatRegisteredAutoSkillBadge(copy, names);
983
850
  }
984
851
 
985
852
  function normalizeRuntimeStatus(status, copy) {
@@ -2967,6 +2834,26 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
2967
2834
  return aid;
2968
2835
  };
2969
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
+
2970
2857
  const runSubmission = (line, userMessageId = null) => {
2971
2858
  inFlightRef.current = true;
2972
2859
  activeUserMessageIdRef.current = userMessageId;
@@ -3060,9 +2947,15 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3060
2947
  setMessages((prev) =>
3061
2948
  prev.map((m) => {
3062
2949
  if (m.id !== targetId) return m;
2950
+ const responseText = typeof event.text === 'string' ? event.text.trim() : '';
2951
+ const shouldSynthesizeCompletion = !responseText && m.syntheticPrelude;
3063
2952
  return {
3064
2953
  ...m,
3065
- ...(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
+ : {}),
3066
2959
  loading: false,
3067
2960
  phase: undefined,
3068
2961
  liveStatus: undefined,
@@ -3086,6 +2979,7 @@ export function ChatApp({ runtime, sessionId, model, language = 'zh', shellName
3086
2979
  }
3087
2980
  if (event?.type === 'tool:start') {
3088
2981
  ensureActiveAssistant();
2982
+ maybeRefreshSyntheticNarration(event.name);
3089
2983
  const detail = describeToolActivity(event.name, copy);
3090
2984
  setRuntimeStatus(makeStatus(copy.runtime.toolRunning, detail, 'magentaBright'));
3091
2985
  setInputStage('tooling');
@@ -0,0 +1,20 @@
1
+ export function describeSkillActivity(copy, name, { done = false, failed = false } = {}) {
2
+ if (failed) return `${copy.runtime.skillFailed}: /${name}`;
3
+ if (done) return `${copy.toolActivity.doneSkill}: /${name}`;
4
+ return `${copy.toolActivity.doingSkill}: /${name}`;
5
+ }
6
+
7
+ export function describeAutoSkillActivity(copy, names) {
8
+ const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
9
+ if (safeNames.length === 0) return '';
10
+ return copy.runtime.autoSkillInjected(safeNames);
11
+ }
12
+
13
+ export function formatAutoSkillBadge(copy, names) {
14
+ const safeNames = Array.isArray(names) ? names.filter(Boolean) : [];
15
+ if (safeNames.length === 0) return '';
16
+ const [first, ...rest] = safeNames;
17
+ const suffix = rest.length > 0 ? ` +${rest.length}` : '';
18
+ const prefix = copy?.roleLabels?.system === 'SYSTEM' ? 'AUTO' : '自动';
19
+ return `${prefix} /${first}${suffix}`;
20
+ }
@@ -0,0 +1,29 @@
1
+ import { classifyCommandIntent } from '../../core/shell.js';
2
+
3
+ export function parseToolDisplayName(name) {
4
+ const raw = String(name || '').trim();
5
+ const match = raw.match(/^([^(]+)\((.*)\)$/);
6
+ return {
7
+ raw,
8
+ base: match ? match[1] : raw,
9
+ target: match ? match[2] : ''
10
+ };
11
+ }
12
+
13
+ export function trimText(text, max = 72) {
14
+ const value = String(text || '').trim();
15
+ if (!value) return '';
16
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...` : value;
17
+ }
18
+
19
+ export function makeBlocked(copy, target) {
20
+ return `${copy.toolActivity.blocked}: ${target}`;
21
+ }
22
+
23
+ export function makePhase(copy, doneLabel, doingLabel, target) {
24
+ return target ? `${doneLabel}: ${target}` : doneLabel || doingLabel;
25
+ }
26
+
27
+ export function classifyRunIntent(target) {
28
+ return classifyCommandIntent(target);
29
+ }
@@ -0,0 +1,17 @@
1
+ import { parseToolDisplayName } from './common.js';
2
+ import { describeCommandToolActivity } from './presenters/command.js';
3
+ import { describeFileToolActivity } from './presenters/files.js';
4
+ import { describeMiscToolActivity } from './presenters/misc.js';
5
+ import { describeSystemToolActivity } from './presenters/system.js';
6
+
7
+ export { isCodeGenerationActivityName } from './presenters/misc.js';
8
+
9
+ export function describeToolActivity(copy, name, options = {}) {
10
+ const parsed = parseToolDisplayName(name);
11
+ return (
12
+ describeSystemToolActivity(copy, parsed, options) ||
13
+ describeCommandToolActivity(copy, parsed, options) ||
14
+ describeFileToolActivity(copy, parsed, options) ||
15
+ describeMiscToolActivity(copy, parsed, name, options)
16
+ );
17
+ }
@@ -0,0 +1,29 @@
1
+ import { classifyRunIntent, makeBlocked, trimText } from '../common.js';
2
+
3
+ function phaseText(copy, blocked, done, target, doingLabel, doneLabel) {
4
+ if (blocked) return makeBlocked(copy, target);
5
+ return done ? `${doneLabel}: ${target}` : `${doingLabel}: ${target}`;
6
+ }
7
+
8
+ export function describeCommandToolActivity(copy, parsed, { done = false, blocked = false } = {}) {
9
+ const target = parsed.target || 'command';
10
+ const intent = classifyRunIntent(parsed.target);
11
+
12
+ if (parsed.base === 'run' || parsed.base === 'start_service') {
13
+ if (intent.kind === 'install') return phaseText(copy, blocked, done, target, copy.toolActivity.doingInstall, copy.toolActivity.doneInstall);
14
+ if (intent.kind === 'build') return phaseText(copy, blocked, done, target, copy.toolActivity.doingBuild, copy.toolActivity.doneBuild);
15
+ if (intent.kind === 'test') return phaseText(copy, blocked, done, target, copy.toolActivity.doingTest, copy.toolActivity.doneTest);
16
+ if (intent.kind === 'frontend-service') return phaseText(copy, blocked, done, target, copy.toolActivity.doingFrontend, copy.toolActivity.doneFrontend);
17
+ if (intent.kind === 'backend-service') return phaseText(copy, blocked, done, target, copy.toolActivity.doingBackend, copy.toolActivity.doneBackend);
18
+ if (intent.kind === 'database-service') return phaseText(copy, blocked, done, target, copy.toolActivity.doingDatabase, copy.toolActivity.doneDatabase);
19
+ if (intent.kind === 'docker-service') return phaseText(copy, blocked, done, target, copy.toolActivity.doingDocker, copy.toolActivity.doneDocker);
20
+ if (intent.kind === 'service') return phaseText(copy, blocked, done, target, copy.toolActivity.doingGeneric, copy.toolActivity.doneGeneric);
21
+ if (parsed.base === 'run') return phaseText(copy, blocked, done, trimText(target, 72) || parsed.base, copy.toolActivity.doingCommand, copy.toolActivity.doneCommand);
22
+ }
23
+
24
+ if (parsed.base === 'start_service' || parsed.base === 'list_services' || parsed.base === 'get_service_status' || parsed.base === 'get_service_logs' || parsed.base === 'stop_service') {
25
+ return phaseText(copy, blocked, done, trimText(parsed.target, 72) || parsed.base, copy.toolActivity.doingGeneric, copy.toolActivity.doneGeneric);
26
+ }
27
+
28
+ return '';
29
+ }
@@ -0,0 +1,26 @@
1
+ import { makeBlocked, trimText } from '../common.js';
2
+
3
+ function describePathTool(copy, parsed, labels, { done = false, blocked = false } = {}) {
4
+ const safeTarget = trimText(parsed.target, 72) || '.';
5
+ if (blocked) return makeBlocked(copy, `${parsed.base}(${safeTarget})`);
6
+ return done ? `${labels.done}: ${safeTarget}` : `${labels.doing}: ${safeTarget}`;
7
+ }
8
+
9
+ export function describeFileToolActivity(copy, parsed, options = {}) {
10
+ if (parsed.base === 'read') {
11
+ return describePathTool(copy, parsed, { done: copy.toolActivity.doneRead, doing: copy.toolActivity.doingRead }, options);
12
+ }
13
+ if (parsed.base === 'edit') {
14
+ return describePathTool(copy, parsed, { done: copy.toolActivity.doneEdit, doing: copy.toolActivity.doingEdit }, options);
15
+ }
16
+ if (parsed.base === 'write') {
17
+ return describePathTool(copy, parsed, { done: copy.toolActivity.doneWrite, doing: copy.toolActivity.doingWrite }, options);
18
+ }
19
+ if (parsed.base === 'patch') {
20
+ return describePathTool(copy, parsed, { done: copy.toolActivity.donePatch, doing: copy.toolActivity.doingPatch }, options);
21
+ }
22
+ if (parsed.base === 'list' || parsed.base === 'glob' || parsed.base === 'grep') {
23
+ return describePathTool(copy, parsed, { done: copy.toolActivity.doneList, doing: copy.toolActivity.doingList }, options);
24
+ }
25
+ return '';
26
+ }
@@ -0,0 +1,19 @@
1
+ import { makeBlocked } from '../common.js';
2
+
3
+ export function isCodeGenerationActivityName(name) {
4
+ return String(name || '').trim() === 'Code generation';
5
+ }
6
+
7
+ export function describeMiscToolActivity(copy, parsed, rawName, { done = false, blocked = false } = {}) {
8
+ if (isCodeGenerationActivityName(rawName)) {
9
+ if (blocked) return `${copy.toolActivity.blocked}: code generation`;
10
+ return done ? copy.toolActivity.doneCodeGeneration : copy.toolActivity.doingCodeGeneration;
11
+ }
12
+ if (parsed.base === 'create_task') {
13
+ return blocked ? makeBlocked(copy, 'create_task') : done ? copy.toolActivity.doneCreateTask : copy.toolActivity.doingCreateTask;
14
+ }
15
+ if (parsed.base === 'update_task') {
16
+ return blocked ? makeBlocked(copy, 'update_task') : done ? copy.toolActivity.doneUpdateTask : copy.toolActivity.doingUpdateTask;
17
+ }
18
+ return blocked ? `${copy.toolActivity.blocked}: ${parsed.raw}` : done ? `${copy.toolActivity.doneGeneric}: ${parsed.raw}` : `${copy.toolActivity.doingGeneric}: ${parsed.raw}`;
19
+ }
@@ -0,0 +1,14 @@
1
+ import { makeBlocked, trimText } from '../common.js';
2
+
3
+ export function describeSystemToolActivity(copy, parsed, { done = false, blocked = false } = {}) {
4
+ if (parsed.base === 'project_index') {
5
+ if (blocked) return `${copy.toolActivity.blocked}: project index`;
6
+ return done ? copy.toolActivity.doneProjectIndex : copy.toolActivity.doingProjectIndex;
7
+ }
8
+ if (parsed.base === 'file_index') {
9
+ const safeTarget = trimText(parsed.target || '.codemini-project/file-index.json', 72);
10
+ if (blocked) return makeBlocked(copy, safeTarget);
11
+ return done ? `${copy.toolActivity.doneFileIndex}: ${safeTarget}` : `${copy.toolActivity.doingFileIndex}: ${safeTarget}`;
12
+ }
13
+ return '';
14
+ }
@@ -0,0 +1,37 @@
1
+ export function trimText(text, max = 48) {
2
+ const value = String(text || '').trim();
3
+ if (!value) return '';
4
+ return value.length > max ? `${value.slice(0, Math.max(0, max - 3)).trimEnd()}...` : value;
5
+ }
6
+
7
+ export function parseToolDisplayName(name) {
8
+ const raw = String(name || '').trim();
9
+ const match = raw.match(/^([^(]+)\((.*)\)$/);
10
+ return {
11
+ raw,
12
+ base: match ? match[1] : raw,
13
+ target: match ? match[2] : ''
14
+ };
15
+ }
16
+
17
+ export function isEnglishCopy(copy) {
18
+ return String(copy?.roleLabels?.coder || '').trim() === 'CODER' && String(copy?.roleLabels?.you || '').trim() === 'YOU';
19
+ }
20
+
21
+ export function renderLocalizedEntry(entry, copy, context) {
22
+ if (!entry) return '';
23
+ const locale = isEnglishCopy(copy) ? 'en' : 'zh';
24
+ const renderer = entry[locale];
25
+ return typeof renderer === 'function' ? renderer(context) : '';
26
+ }
27
+
28
+ export function getLastToolActivity(msg, statuses = []) {
29
+ const allowed = new Set((Array.isArray(statuses) ? statuses : []).map((status) => String(status)));
30
+ const segments = Array.isArray(msg?.segments) ? msg.segments : [];
31
+ for (let idx = segments.length - 1; idx >= 0; idx -= 1) {
32
+ const segment = segments[idx];
33
+ if (segment?.type !== 'tool' && segment?.type !== 'system_tool') continue;
34
+ if (allowed.size === 0 || allowed.has(String(segment.status || ''))) return segment;
35
+ }
36
+ return null;
37
+ }
@@ -0,0 +1,109 @@
1
+ function buildPreludeEntry() {
2
+ return {
3
+ en: ({ changeKind, target, verb }) => {
4
+ if (changeKind === 'readme') return `I'll inspect the project structure first, then write the README.`;
5
+ if (changeKind === 'doc') return `I'll inspect the existing context first, then update the document.`;
6
+ if (target) return `I'll inspect ${target} first, then ${verb} it.`;
7
+ return `I'll inspect the current code first, then make the change.`;
8
+ },
9
+ zh: ({ changeKind, target, verbZh }) => {
10
+ if (changeKind === 'readme') return '我先看一下项目内容和结构,再开始写 README。';
11
+ if (changeKind === 'doc') return '我先看一下现有内容,再开始整理这份文档。';
12
+ if (target) return `我先看一下 ${target} 的上下文,再开始${verbZh}。`;
13
+ return '我先确认当前代码上下文,再动手修改。';
14
+ }
15
+ };
16
+ }
17
+
18
+ function buildCompletionEntry() {
19
+ return {
20
+ en: ({ changeKind, target }) => {
21
+ if (changeKind === 'readme') return 'The README is in place. If you want, I can also add a quick start, feature summary, or usage example.';
22
+ if (changeKind === 'doc') return 'The document is updated. If you want, I can also polish the wording, structure, or add an example section.';
23
+ if (changeKind === 'test') {
24
+ return target
25
+ ? `${target} is ready. If you want, I can keep going with verification, more test coverage, or a quick review of edge cases.`
26
+ : 'That test-related change is ready. If you want, I can keep going with verification or edge-case review.';
27
+ }
28
+ return target
29
+ ? `${target} is ready. If you want, I can also add tests, update docs, or do a quick edge-case pass.`
30
+ : 'That part is ready. If you want, I can also add tests, update docs, or do a quick edge-case pass.';
31
+ },
32
+ zh: ({ changeKind, target }) => {
33
+ if (changeKind === 'readme') return 'README 已经写好了。要不要我顺手再补一个快速开始、功能概览,或者使用示例?';
34
+ if (changeKind === 'doc') return '文档已经更新好了。要不要我继续顺一下语气、结构,或者补一段示例?';
35
+ if (changeKind === 'test') {
36
+ return target
37
+ ? `${target} 已经处理好了。要不要我继续补验证、扩一下测试覆盖,或者再过一遍边界情况?`
38
+ : '这部分测试相关修改已经处理好了。要不要我继续补验证,或者再过一遍边界情况?';
39
+ }
40
+ return target
41
+ ? `${target} 已经处理好了。要不要我继续补测试、更新文档,或者再检查一遍边界情况?`
42
+ : '这部分已经处理好了。要不要我继续补测试、更新文档,或者再检查一遍边界情况?';
43
+ }
44
+ };
45
+ }
46
+
47
+ function buildBridgeEntry() {
48
+ return {
49
+ en: ({ changeKind, nextTarget, verb, hasContext }) => {
50
+ if (changeKind === 'readme') return hasContext ? 'I have enough context now, so I can write the README.' : 'I can write the README next.';
51
+ if (changeKind === 'doc') return hasContext ? 'I have enough context now, so I can update the document.' : 'I can update the document next.';
52
+ if (nextTarget) return hasContext ? `I have enough context now, so I'll ${verb} ${nextTarget}.` : `I'll ${verb} ${nextTarget} next.`;
53
+ return hasContext ? 'I have enough context now, so I can make the change.' : 'I can make the change next.';
54
+ },
55
+ zh: ({ changeKind, nextTarget, verbZh, hasContext }) => {
56
+ if (changeKind === 'readme') return hasContext ? '相关内容我已经看过了,现在开始写 README。' : '现在开始写 README。';
57
+ if (changeKind === 'doc') return hasContext ? '需要的上下文我已经看过了,现在开始整理这份文档。' : '现在开始整理这份文档。';
58
+ if (nextTarget) return hasContext ? `需要的上下文我已经看过了,现在${verbZh} ${nextTarget}。` : `现在${verbZh} ${nextTarget}。`;
59
+ return hasContext ? '需要的上下文我已经看过了,现在开始修改。' : '现在开始修改。';
60
+ }
61
+ };
62
+ }
63
+
64
+ export function inferChangeKind(target) {
65
+ const lowerTarget = String(target || '').toLowerCase();
66
+ if (lowerTarget.includes('readme')) return 'readme';
67
+ if (lowerTarget.endsWith('.md')) return 'doc';
68
+ if (/test|spec/i.test(lowerTarget)) return 'test';
69
+ return 'generic';
70
+ }
71
+
72
+ export function createChangePresenter({ verb, verbZh }) {
73
+ return {
74
+ prelude: buildPreludeEntry(),
75
+ completion: buildCompletionEntry(),
76
+ bridges: {
77
+ inspect: buildBridgeEntry(),
78
+ search: {
79
+ en: ({ changeKind, nextTarget, verb }) => {
80
+ if (changeKind === 'readme') return 'I found what I needed, so I can write the README.';
81
+ if (changeKind === 'doc') return 'I found what I needed, so I can update the document.';
82
+ if (nextTarget) return `I found the right spot, so I'll ${verb} ${nextTarget}.`;
83
+ return 'I found the right spot, so I can make the change.';
84
+ },
85
+ zh: ({ changeKind, nextTarget, verbZh }) => {
86
+ if (changeKind === 'readme') return '相关位置我已经找到了,现在开始写 README。';
87
+ if (changeKind === 'doc') return '相关位置我已经找到了,现在开始整理这份文档。';
88
+ if (nextTarget) return `相关位置我已经找到了,现在${verbZh} ${nextTarget}。`;
89
+ return '相关位置我已经找到了,现在开始修改。';
90
+ }
91
+ },
92
+ run: {
93
+ en: ({ changeKind, nextTarget, verb }) => {
94
+ if (changeKind === 'readme') return 'I have the result I needed, so I can write the README next.';
95
+ if (changeKind === 'doc') return 'I have the result I needed, so I can update the document next.';
96
+ if (nextTarget) return `I have the result I needed, so I'll ${verb} ${nextTarget}.`;
97
+ return 'I have the result I needed, so I can make the follow-up change.';
98
+ },
99
+ zh: ({ changeKind, nextTarget, verbZh }) => {
100
+ if (changeKind === 'readme') return '结果我已经拿到了,现在开始写 README。';
101
+ if (changeKind === 'doc') return '结果我已经拿到了,现在开始整理这份文档。';
102
+ if (nextTarget) return `结果我已经拿到了,现在${verbZh} ${nextTarget}。`;
103
+ return '结果我已经拿到了,现在继续做后续修改。';
104
+ }
105
+ }
106
+ },
107
+ meta: { verb, verbZh }
108
+ };
109
+ }
@@ -0,0 +1,3 @@
1
+ import { createChangePresenter } from './change.js';
2
+
3
+ export const editPresenter = createChangePresenter({ verb: 'update', verbZh: '修改' });
@@ -0,0 +1,10 @@
1
+ export const genericPresenter = {
2
+ prelude: {
3
+ en: () => `I'll check the relevant project context first.`,
4
+ zh: () => '我先查看相关上下文。'
5
+ },
6
+ completion: {
7
+ en: () => 'The requested work is done. If you want, I can keep going with tests, docs, or a quick review.',
8
+ zh: () => '这部分已经处理好了。你要是愿意,我可以继续补测试、文档,或者再帮你快速检查一遍。'
9
+ }
10
+ };
@@ -0,0 +1,11 @@
1
+ export const globPresenter = {
2
+ prelude: {
3
+ en: ({ target }) => (target ? `I'll inspect the ${target} directory first.` : 'I\'ll inspect the relevant directory first.'),
4
+ zh: ({ target }) => (target ? `我先查看 ${target} 目录里的内容。` : '我先查看相关目录内容。')
5
+ },
6
+ completion: {
7
+ en: () => 'I have the relevant context now. Do you want me to make the change next, or summarize the findings first?',
8
+ zh: () => '相关上下文我已经看完了。接下来你要我直接动手改,还是先把结论整理给你?'
9
+ },
10
+ meta: { bridgeGroup: 'inspect' }
11
+ };
@@ -0,0 +1,11 @@
1
+ export const grepPresenter = {
2
+ prelude: {
3
+ en: () => `I'll search the relevant code first.`,
4
+ zh: () => '我先搜索相关代码位置。'
5
+ },
6
+ completion: {
7
+ en: () => 'I found the relevant spots. Do you want me to make the change next, or summarize the findings first?',
8
+ zh: () => '相关位置我已经找到了。接下来你要我直接动手改,还是先把结论整理给你?'
9
+ },
10
+ meta: { bridgeGroup: 'search' }
11
+ };
@@ -0,0 +1,11 @@
1
+ export const listPresenter = {
2
+ prelude: {
3
+ en: ({ target }) => (target ? `I'll inspect the ${target} directory first.` : 'I\'ll inspect the relevant directory first.'),
4
+ zh: ({ target }) => (target ? `我先查看 ${target} 目录里的内容。` : '我先查看相关目录内容。')
5
+ },
6
+ completion: {
7
+ en: () => 'I have the relevant context now. Do you want me to make the change next, or summarize the findings first?',
8
+ zh: () => '相关上下文我已经看完了。接下来你要我直接动手改,还是先把结论整理给你?'
9
+ },
10
+ meta: { bridgeGroup: 'inspect' }
11
+ };
@@ -0,0 +1,3 @@
1
+ import { createChangePresenter } from './change.js';
2
+
3
+ export const patchPresenter = createChangePresenter({ verb: 'patch', verbZh: '修改' });
@@ -0,0 +1,11 @@
1
+ export const readPresenter = {
2
+ prelude: {
3
+ en: ({ target }) => (target ? `I'll inspect ${target} first.` : 'I\'ll inspect the relevant file first.'),
4
+ zh: ({ target }) => (target ? `我先查看 ${target} 的内容。` : '我先查看相关文件内容。')
5
+ },
6
+ completion: {
7
+ en: () => 'I have the relevant context now. Do you want me to make the change next, or summarize the findings first?',
8
+ zh: () => '相关上下文我已经看完了。接下来你要我直接动手改,还是先把结论整理给你?'
9
+ },
10
+ meta: { bridgeGroup: 'inspect' }
11
+ };
@@ -0,0 +1,29 @@
1
+ export const runPresenter = {
2
+ prelude: {
3
+ en: () => `I'll verify the current project state first.`,
4
+ zh: () => '我先检查当前项目状态。'
5
+ },
6
+ completion: {
7
+ en: () => 'That step is finished. Do you want me to act on the result next, or summarize what it means first?',
8
+ zh: () => '这一步已经跑完了。接下来要我根据结果继续处理,还是先把结论整理给你?'
9
+ },
10
+ bridges: {
11
+ 'generic-change': {
12
+ en: () => 'I have the result I needed, so I can make the follow-up change.',
13
+ zh: () => '结果我已经拿到了,现在继续做后续修改。'
14
+ },
15
+ 'doc-change': {
16
+ en: () => 'I have the result I needed, so I can make the follow-up change.',
17
+ zh: () => '结果我已经拿到了,现在继续做后续修改。'
18
+ },
19
+ 'readme-change': {
20
+ en: () => 'I have the result I needed, so I can make the follow-up change.',
21
+ zh: () => '结果我已经拿到了,现在继续做后续修改。'
22
+ },
23
+ 'test-change': {
24
+ en: () => 'I have the result I needed, so I can make the follow-up change.',
25
+ zh: () => '结果我已经拿到了,现在继续做后续修改。'
26
+ }
27
+ },
28
+ meta: { bridgeGroup: 'run' }
29
+ };
@@ -0,0 +1,3 @@
1
+ import { createChangePresenter } from './change.js';
2
+
3
+ export const writePresenter = createChangePresenter({ verb: 'write', verbZh: '写' });
@@ -0,0 +1,67 @@
1
+ import { getLastToolActivity, parseToolDisplayName, renderLocalizedEntry, trimText } from './tool-narration/common.js';
2
+ import { inferChangeKind } from './tool-narration/presenters/change.js';
3
+ import { editPresenter } from './tool-narration/presenters/edit.js';
4
+ import { genericPresenter } from './tool-narration/presenters/generic.js';
5
+ import { globPresenter } from './tool-narration/presenters/glob.js';
6
+ import { grepPresenter } from './tool-narration/presenters/grep.js';
7
+ import { listPresenter } from './tool-narration/presenters/list.js';
8
+ import { patchPresenter } from './tool-narration/presenters/patch.js';
9
+ import { readPresenter } from './tool-narration/presenters/read.js';
10
+ import { runPresenter } from './tool-narration/presenters/run.js';
11
+ import { writePresenter } from './tool-narration/presenters/write.js';
12
+
13
+ const BASE_PRESENTERS = {
14
+ read: readPresenter,
15
+ list: listPresenter,
16
+ glob: globPresenter,
17
+ grep: grepPresenter,
18
+ write: writePresenter,
19
+ edit: editPresenter,
20
+ patch: patchPresenter,
21
+ generate_diff: patchPresenter,
22
+ run: runPresenter
23
+ };
24
+
25
+ function resolveNarrationContext(name) {
26
+ const { base, target } = parseToolDisplayName(name);
27
+ const presenter = BASE_PRESENTERS[base] || genericPresenter;
28
+ const changeKind = inferChangeKind(target);
29
+ return {
30
+ base,
31
+ target: trimText(target, 48),
32
+ presenter,
33
+ bridgeGroup: presenter?.meta?.bridgeGroup || 'generic',
34
+ verb: presenter?.meta?.verb || 'update',
35
+ verbZh: presenter?.meta?.verbZh || '修改',
36
+ changeKind
37
+ };
38
+ }
39
+
40
+ export function buildPreToolNotice(name, copy) {
41
+ const context = resolveNarrationContext(name);
42
+ return renderLocalizedEntry(context.presenter.prelude, copy, context);
43
+ }
44
+
45
+ export function buildInterToolNotice(previousActivity, nextToolName, copy) {
46
+ const previousContext = resolveNarrationContext(previousActivity?.name);
47
+ const nextContext = resolveNarrationContext(nextToolName);
48
+ const bridge = nextContext.presenter?.bridges?.[previousContext.bridgeGroup];
49
+ return renderLocalizedEntry(bridge, copy, {
50
+ ...nextContext,
51
+ nextTarget: nextContext.target,
52
+ hasContext: previousContext.bridgeGroup !== 'generic'
53
+ });
54
+ }
55
+
56
+ export function buildSyntheticCompletionText(msg, copy) {
57
+ const activity = getLastToolActivity(msg, ['done', 'running']);
58
+ if (!activity) {
59
+ return renderLocalizedEntry(genericPresenter.completion, copy, {});
60
+ }
61
+
62
+ const context = resolveNarrationContext(activity.name);
63
+ return renderLocalizedEntry(context.presenter.completion, copy, {
64
+ ...context,
65
+ target: trimText(context.target, 56)
66
+ });
67
+ }