codemini-cli 0.3.9 → 0.4.0

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.
@@ -132,7 +132,18 @@ const TUI_COPY = {
132
132
  pendingQueue: '等待队列',
133
133
  commandPaletteGroupedSelect: '命令面板 | 分组选择模式',
134
134
  commandPaletteGroupedSuggestions: '命令面板 | 分组候选',
135
- startupHint: '使用 /help、/commands、/compact、/exit、/stop、!<shell>。Tab 可自动补全 slash 命令。',
135
+ startupHints: [
136
+ '🧭 使用 /help 可查看命令帮助。Tab 可自动补全 slash 命令。',
137
+ '📋 试试用 /plan 模式来规划复杂任务,让 AI 先给出方案再动手。',
138
+ '⏫ 使用 ↑↓ 键可以浏览历史输入,快速重复之前的操作。',
139
+ '🐚 输入 !<shell命令> 可以直接执行本地终端命令,如 !ls、!git status。',
140
+ '🔧 Ctrl+T 可以切换工具调用详情的展开/收起状态。',
141
+ '📊 试试 /status 查看当前会话模式、模型和 token 用量。',
142
+ '🧩 用 /mode plan 切换到规划模式,让 AI 先出方案再动手。',
143
+ '🆕 /new 可以新建一个干净的会话,重新开始工作。',
144
+ '🧠 /memory 查看和管理 AI 的持久记忆,帮助它更好地理解你的偏好。',
145
+ '💤 CodeMini 会自动"做梦"休息,整理错误信息并自我优化,越用越聪明~'
146
+ ],
136
147
  toolSummaryExpanded: '工具摘要:已展开',
137
148
  toolSummaryCollapsed: '工具摘要:已收起',
138
149
  toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用`,
@@ -264,6 +275,30 @@ const TUI_COPY = {
264
275
  answerLabel: '确认输入(yes/no)',
265
276
  answerPlaceholder: 'yes 或 no'
266
277
  },
278
+ runApproval: {
279
+ title: '确认执行命令?',
280
+ commandLabel: '命令',
281
+ riskLabel: '风险等级',
282
+ descriptionLabel: '说明',
283
+ sideEffectsLabel: '副作用',
284
+ lowRisk: '低',
285
+ mediumRisk: '中',
286
+ highRisk: '高',
287
+ prompt: '输入 yes 执行,输入 no 取消。',
288
+ invalidAnswer: '请输入 yes 或 no。',
289
+ inputLocked: '命令审批进行中,请输入 yes 或 no',
290
+ answerLabel: '审批输入(yes/no)',
291
+ answerPlaceholder: 'yes 或 no'
292
+ },
293
+ fileChangeSummary: {
294
+ title: '文件改动',
295
+ fileLabel: '文件',
296
+ statusLabel: '状态',
297
+ editStatus: '编辑',
298
+ createStatus: '新建',
299
+ deleteStatus: '删除',
300
+ changesLabel: '改动'
301
+ },
267
302
  planApproval: {
268
303
  title: '确认执行计划?',
269
304
  goalLabel: '目标',
@@ -305,7 +340,18 @@ const TUI_COPY = {
305
340
  pendingQueue: 'pending queue',
306
341
  commandPaletteGroupedSelect: 'command palette | grouped select mode',
307
342
  commandPaletteGroupedSuggestions: 'command palette | grouped suggestions',
308
- startupHint: 'Use /help, /commands, /compact, /stop, /exit, !<shell>. Tab for slash autocomplete.',
343
+ startupHints: [
344
+ '🧭 Use /help to view command help. Tab for slash autocomplete.',
345
+ '📋 Try /plan mode for complex tasks — let the AI propose a plan before coding.',
346
+ '⏫ Use ↑↓ arrow keys to browse input history and repeat previous actions.',
347
+ '🐚 Type !<shell command> to run local terminal commands, e.g. !ls, !git status.',
348
+ '🔧 Ctrl+T toggles tool call detail expansion/collapse.',
349
+ '📊 Try /status to check current session mode, model, and token usage.',
350
+ '🧩 Use /mode plan to switch to planning mode — AI proposes a plan before coding.',
351
+ '🆕 /new starts a fresh session to begin a clean slate.',
352
+ '🧠 /memory lets you view and manage the AI\'s persistent memory for better personalization.',
353
+ '💤 CodeMini auto-"dreams" to rest, consolidate errors, and self-optimize — it gets smarter over time~'
354
+ ],
309
355
  toolSummaryExpanded: 'Tool summary: expanded',
310
356
  toolSummaryCollapsed: 'Tool summary: collapsed',
311
357
  toolChainCollapsed: (count) => `${count} earlier tool calls hidden`,
@@ -437,6 +483,30 @@ const TUI_COPY = {
437
483
  answerLabel: 'Approval input (yes/no)',
438
484
  answerPlaceholder: 'yes or no'
439
485
  },
486
+ runApproval: {
487
+ title: 'Confirm command execution?',
488
+ commandLabel: 'Command',
489
+ riskLabel: 'Risk Level',
490
+ descriptionLabel: 'Description',
491
+ sideEffectsLabel: 'Side Effects',
492
+ lowRisk: 'Low',
493
+ mediumRisk: 'Medium',
494
+ highRisk: 'High',
495
+ prompt: 'Type yes to execute, or no to cancel.',
496
+ invalidAnswer: 'Please enter yes or no.',
497
+ inputLocked: 'Command approval is active; type yes or no',
498
+ answerLabel: 'Approval input (yes/no)',
499
+ answerPlaceholder: 'yes or no'
500
+ },
501
+ fileChangeSummary: {
502
+ title: 'File Changes',
503
+ fileLabel: 'File',
504
+ statusLabel: 'Status',
505
+ editStatus: 'Edit',
506
+ createStatus: 'Create',
507
+ deleteStatus: 'Delete',
508
+ changesLabel: 'Changes'
509
+ },
440
510
  planApproval: {
441
511
  title: 'Approve this plan?',
442
512
  goalLabel: 'Goal',
@@ -993,6 +1063,25 @@ export function parseDeleteApprovalAnswer(value) {
993
1063
  return normalized ? 'invalid' : 'empty';
994
1064
  }
995
1065
 
1066
+ export function normalizeRunApprovalRequest(request) {
1067
+ if (!request || String(request?.name || '').trim() !== 'run') return null;
1068
+ const details =
1069
+ request?.approvalDetails && typeof request.approvalDetails === 'object' && !Array.isArray(request.approvalDetails)
1070
+ ? request.approvalDetails
1071
+ : {};
1072
+ const command = String(details.command || request?.arguments?.command || '').trim();
1073
+ if (!command) return null;
1074
+ return {
1075
+ id: String(request?.id || '').trim(),
1076
+ toolName: 'run',
1077
+ command,
1078
+ risk: details.risk || 'high',
1079
+ description: details.evaluation?.description || '',
1080
+ sideEffects: details.evaluation?.sideEffects || '',
1081
+ recommendation: details.evaluation?.recommendation || 'deny'
1082
+ };
1083
+ }
1084
+
996
1085
  export function parsePlanApprovalAnswer(value) {
997
1086
  const raw = String(value || '').trim();
998
1087
  if (!raw) return { action: 'empty', command: '' };
@@ -1039,36 +1128,42 @@ export function formatDeleteApprovalLines(copy, request) {
1039
1128
  ];
1040
1129
  }
1041
1130
 
1131
+ export function formatPlanApprovalLines(copy, request) {
1132
+ if (!request) return [];
1133
+ return [String(copy?.planApproval?.title || '').trim()].filter(Boolean);
1134
+ }
1135
+
1042
1136
  function getActivityDisplayParts(activity) {
1043
1137
  if (isCodeGenerationActivityName(activity?.name)) {
1044
1138
  return {
1045
- primary: 'Code',
1139
+ primary: '🧠 Code',
1046
1140
  secondary: ' (generation)'
1047
1141
  };
1048
1142
  }
1049
1143
  const parsed = parseToolDisplayName(activity?.name);
1050
- if (parsed.base === 'run') {
1144
+ const base = String(parsed.base || '').toLowerCase();
1145
+ if (base === 'run') {
1051
1146
  const intent = classifyCommandIntent(parsed.target);
1052
1147
  return {
1053
- primary: getIntentLabel(intent.kind),
1148
+ primary: `${getIntentEmoji(intent.kind)} ${getIntentLabel(intent.kind)}`,
1054
1149
  secondary: parsed.target ? `(${parsed.target})` : ''
1055
1150
  };
1056
1151
  }
1057
1152
  if ((activity?.type || 'tool') === 'skill') {
1058
1153
  return {
1059
- primary: `Skill`,
1154
+ primary: '🧩 Skill',
1060
1155
  secondary: `(${activity?.name || 'unknown'})`
1061
1156
  };
1062
1157
  }
1063
1158
  if ((activity?.type || 'tool') === 'system_tool') {
1064
- if (parsed.base === 'project_index') {
1065
- return { primary: 'Project Index', secondary: '' };
1159
+ if (base === 'project_index') {
1160
+ return { primary: '🗂️ Project Index', secondary: '' };
1066
1161
  }
1067
- if (parsed.base === 'file_index') {
1068
- return { primary: 'File Index', secondary: parsed.target ? `(${parsed.target})` : '' };
1162
+ if (base === 'file_index') {
1163
+ return { primary: '🗂️ File Index', secondary: parsed.target ? `(${parsed.target})` : '' };
1069
1164
  }
1070
1165
  return {
1071
- primary: 'Index',
1166
+ primary: '🗂️ Index',
1072
1167
  secondary: parsed.target ? `(${parsed.target})` : parsed.base ? `(${parsed.base})` : ''
1073
1168
  };
1074
1169
  }
@@ -1090,19 +1185,53 @@ function getActivityDisplayParts(activity) {
1090
1185
  read_plan: 'Read Plan',
1091
1186
  update_plan: 'Update Plan'
1092
1187
  };
1188
+ const emojis = {
1189
+ read: '📖',
1190
+ edit: '✏️',
1191
+ write: '📝',
1192
+ delete: '🗑️',
1193
+ patch: '🩹',
1194
+ run: '⚙️',
1195
+ grep: '🔍',
1196
+ glob: '🧭',
1197
+ list: '📂',
1198
+ list_background_tasks: '🗃️',
1199
+ get_background_task: '📌',
1200
+ stop_background_task: '⏹️',
1201
+ list_files: '🧭',
1202
+ update_todos: '✅',
1203
+ read_plan: '📋',
1204
+ update_plan: '🗓️'
1205
+ };
1093
1206
  return {
1094
- primary: labels[parsed.base] || parsed.base || 'Tool',
1207
+ primary: `${emojis[base] || '🔧'} ${labels[base] || parsed.base || 'Tool'}`,
1095
1208
  secondary: parsed.target ? `(${parsed.target})` : ''
1096
1209
  };
1097
1210
  }
1098
1211
 
1212
+ function getIntentEmoji(kind) {
1213
+ const map = {
1214
+ test: '🧪',
1215
+ install: '📦',
1216
+ build: '🏗️',
1217
+ frontend: '🖥️',
1218
+ backend: '🛰️',
1219
+ database: '🗄️',
1220
+ docker: '🐳',
1221
+ command: '⚙️'
1222
+ };
1223
+ return map[kind] || '⚙️';
1224
+ }
1225
+
1099
1226
  export function isIndexSystemToolName(name) {
1100
1227
  const parsed = parseToolDisplayName(name);
1101
1228
  return parsed.base === 'project_index' || parsed.base === 'file_index';
1102
1229
  }
1103
1230
 
1104
1231
  export function shouldShowCompletionFooter(msg) {
1105
- return Boolean(msg && msg.label === 'coder' && !msg.loading && !(msg.phase || '').trim());
1232
+ if (!msg || msg.loading || (msg.phase || '').trim()) return false;
1233
+ const label = (msg.label || '').toLowerCase();
1234
+ return label === 'coder' || label === 'planner' || label === 'reviewer' || label === 'tester';
1106
1235
  }
1107
1236
 
1108
1237
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
@@ -1183,7 +1312,6 @@ function stageDescriptor(inputStage, busy, runtimeStatus, copy) {
1183
1312
 
1184
1313
  function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
1185
1314
  const status = normalizeRuntimeStatus(runtimeStatus, copy);
1186
- const spinnerChar = SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length];
1187
1315
  return h(
1188
1316
  Box,
1189
1317
  {
@@ -1195,7 +1323,7 @@ function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
1195
1323
  },
1196
1324
  h(Text, { color: busy ? 'greenBright' : 'gray' }, busy ? copy.generic.live : copy.generic.idle),
1197
1325
  h(Text, { color: 'gray' }, ' '),
1198
- h(Text, { color: busy ? 'cyanBright' : 'gray' }, spinnerChar),
1326
+ h(Text, { color: busy ? 'cyanBright' : 'gray' }, busy ? '●' : '○'),
1199
1327
  h(Text, { color: 'gray' }, ' '),
1200
1328
  h(Text, { color: busy ? 'white' : 'gray' }, status.title || copy.generic.waitingForInput)
1201
1329
  );
@@ -2320,15 +2448,20 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2320
2448
  ? 'redBright'
2321
2449
  : 'cyanBright';
2322
2450
  const durationText = formatActivityDurationText(row);
2451
+ const trailingLoader =
2452
+ row.status === 'running'
2453
+ ? h(Text, { color: 'gray' }, ` ${SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length]}`)
2454
+ : null;
2323
2455
  return h(
2324
2456
  Box,
2325
2457
  { key: `row-tool-${msg.id}-${idx}` },
2326
2458
  h(Text, { color: 'gray' }, ' '),
2327
- h(Text, { color: dotColor }, row.status === 'running' ? SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length] : '●'),
2459
+ h(Text, { color: dotColor }, '●'),
2328
2460
  h(Text, { color: 'gray' }, ' '),
2329
2461
  h(Text, { color: textColor }, display.primary),
2330
2462
  h(Text, { color: 'gray' }, display.secondary),
2331
- durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null
2463
+ durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null,
2464
+ trailingLoader
2332
2465
  );
2333
2466
  }
2334
2467
  if (row.kind === 'activity-summary') {
@@ -2594,8 +2727,11 @@ const MessageBubble = React.memo(function MessageBubble({ msg, loaderTick, showT
2594
2727
  shouldShowCompletionFooter(msg)
2595
2728
  ? h(
2596
2729
  Box,
2597
- { marginTop: 1, marginLeft: 1, key: `row-completion-${msg.id}` },
2598
- h(Text, { color: 'gray', dimColor: true }, copy.generic.taskCompleted)
2730
+ { marginTop: 1, flexDirection: 'column', key: `row-completion-${msg.id}` },
2731
+ h(FileChangeSummary, { segments: msg.segments, copy }),
2732
+ h(Box, { marginLeft: 1, marginTop: 1 },
2733
+ h(Text, { color: 'gray', dimColor: true }, copy.generic.taskCompleted)
2734
+ )
2599
2735
  )
2600
2736
  : null
2601
2737
  )
@@ -2868,12 +3004,102 @@ function DeleteApprovalPanel({ request, inputValue, errorText, copy, cursorVisib
2868
3004
  );
2869
3005
  }
2870
3006
 
3007
+ function RunApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
3008
+ if (!request) return null;
3009
+ const details = request?.toolName === 'run' ? request : normalizeRunApprovalRequest(request);
3010
+ if (!details) return null;
3011
+ const c = copy.runApproval || {};
3012
+ const riskColor = details.risk === 'low' ? 'green' : details.risk === 'medium' ? 'yellow' : 'redBright';
3013
+ const borderColor = details.risk === 'medium' ? 'yellow' : 'redBright';
3014
+ const riskLabel = details.risk === 'low' ? c.lowRisk : details.risk === 'medium' ? c.mediumRisk : c.highRisk;
3015
+ const placeholder = String(c.answerPlaceholder || '').trim();
3016
+ return h(
3017
+ Box,
3018
+ {
3019
+ marginTop: 1,
3020
+ flexDirection: 'column',
3021
+ borderStyle: 'round',
3022
+ borderColor,
3023
+ paddingX: 1,
3024
+ paddingY: 0
3025
+ },
3026
+ h(Text, { color: borderColor }, c.title),
3027
+ h(Text, { color: 'white' }, `${c.commandLabel}: ${details.command}`),
3028
+ h(Text, null, `${c.riskLabel}: `, h(Text, { color: riskColor, bold: true }, riskLabel || details.risk)),
3029
+ details.description ? h(Text, { color: 'gray' }, `${c.descriptionLabel}: ${details.description}`) : null,
3030
+ details.sideEffects ? h(Text, { color: 'gray' }, `${c.sideEffectsLabel}: ${details.sideEffects}`) : null,
3031
+ h(Text, { color: 'gray' }, c.prompt),
3032
+ h(
3033
+ Box,
3034
+ { marginTop: 1 },
3035
+ h(Text, { color: borderColor }, `${c.answerLabel}: `),
3036
+ h(ApprovalCursorLine, {
3037
+ inputValue,
3038
+ placeholder: placeholder || ' ',
3039
+ cursorVisible,
3040
+ accent: borderColor
3041
+ })
3042
+ ),
3043
+ errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
3044
+ );
3045
+ }
3046
+
3047
+ function FileChangeSummary({ segments, copy }) {
3048
+ if (!Array.isArray(segments) || segments.length === 0) return null;
3049
+ const c = copy.fileChangeSummary || {};
3050
+ const changes = new Map();
3051
+ for (const seg of segments) {
3052
+ if (!seg.fileChange) continue;
3053
+ const p = seg.fileChange.path;
3054
+ if (!p) continue;
3055
+ const existing = changes.get(p);
3056
+ if (existing) {
3057
+ /* 同一文件多次编辑,合并行数,取最高 action */
3058
+ existing.linesAdded += seg.fileChange.linesAdded || 0;
3059
+ existing.linesRemoved += seg.fileChange.linesRemoved || 0;
3060
+ const ACTION_ORDER = { delete: 3, create: 2, edit: 1 };
3061
+ if ((ACTION_ORDER[seg.fileChange.action] || 0) > (ACTION_ORDER[existing.action] || 0)) {
3062
+ existing.action = seg.fileChange.action;
3063
+ }
3064
+ } else {
3065
+ changes.set(p, { path: p, action: seg.fileChange.action, linesAdded: seg.fileChange.linesAdded || 0, linesRemoved: seg.fileChange.linesRemoved || 0 });
3066
+ }
3067
+ }
3068
+ if (changes.size === 0) return null;
3069
+ const entries = [...changes.values()];
3070
+ return h(
3071
+ Box,
3072
+ { marginTop: 1, flexDirection: 'column', borderStyle: 'round', borderColor: 'gray', paddingX: 1 },
3073
+ h(Text, { color: 'cyan', bold: true }, c.title || 'File Changes'),
3074
+ ...entries.map((entry) => {
3075
+ const statusMap = { edit: c.editStatus || 'Edit', create: c.createStatus || 'Create', delete: c.deleteStatus || 'Delete' };
3076
+ const statusColor = entry.action === 'create' ? 'green' : entry.action === 'delete' ? 'red' : 'yellow';
3077
+ const statusText = statusMap[entry.action] || entry.action;
3078
+ let changesText = '';
3079
+ if (entry.action !== 'delete') {
3080
+ const parts = [];
3081
+ if (entry.linesAdded > 0) parts.push(h(Text, { color: 'green' }, `+${entry.linesAdded}`));
3082
+ if (entry.linesRemoved > 0) parts.push(h(Text, { color: 'red' }, `-${entry.linesRemoved}`));
3083
+ if (parts.length > 0) {
3084
+ changesText = parts.reduce((acc, el, i) => i === 0 ? [el] : [...acc, ' ', el], []);
3085
+ }
3086
+ }
3087
+ return h(
3088
+ Box,
3089
+ { key: entry.path },
3090
+ h(Text, { color: 'white' }, ` ${entry.path}`),
3091
+ h(Text, { color: 'gray' }, ' '),
3092
+ h(Text, { color: statusColor }, statusText),
3093
+ changesText ? h(Text, null, ' ', changesText) : null
3094
+ );
3095
+ })
3096
+ );
3097
+ }
3098
+
2871
3099
  function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
2872
3100
  if (!request) return null;
2873
- const goal = String(request.goal || '').trim();
2874
- const summary = String(request.summary || '').trim();
2875
- const filePath = String(request.filePath || '').trim();
2876
3101
  const placeholder = String(copy.planApproval.answerPlaceholder || '').trim();
3102
+ const lines = formatPlanApprovalLines(copy, request);
2877
3103
  return h(
2878
3104
  Box,
2879
3105
  {
@@ -2884,11 +3110,9 @@ function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible
2884
3110
  paddingX: 1,
2885
3111
  paddingY: 0
2886
3112
  },
2887
- h(Text, { color: 'yellowBright' }, copy.planApproval.title),
2888
- goal ? h(Text, { color: 'white' }, `${copy.planApproval.goalLabel}: ${goal}`) : null,
2889
- summary ? h(Text, { color: 'white' }, `${copy.planApproval.summaryLabel}: ${summary}`) : null,
2890
- filePath ? h(Text, { color: 'gray' }, `${copy.planApproval.fileLabel}: ${filePath}`) : null,
2891
- h(Text, { color: 'gray' }, copy.planApproval.prompt),
3113
+ ...lines.map((line, index) =>
3114
+ h(Text, { key: `plan-approval-line-${index}`, color: 'yellowBright' }, line)
3115
+ ),
2892
3116
  h(
2893
3117
  Box,
2894
3118
  { marginTop: 1 },
@@ -2988,10 +3212,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2988
3212
  const [pendingDeleteApproval, setPendingDeleteApproval] = useState(null);
2989
3213
  const [deleteApprovalInput, setDeleteApprovalInput] = useState('');
2990
3214
  const [deleteApprovalError, setDeleteApprovalError] = useState('');
3215
+ const [pendingRunApproval, setPendingRunApproval] = useState(null);
3216
+ const [runApprovalInput, setRunApprovalInput] = useState('');
3217
+ const [runApprovalError, setRunApprovalError] = useState('');
2991
3218
  const [pendingPlanApproval, setPendingPlanApproval] = useState(null);
2992
3219
  const [planApprovalInput, setPlanApprovalInput] = useState('');
2993
3220
  const [planApprovalError, setPlanApprovalError] = useState('');
2994
- const approvalLockActive = Boolean(pendingDeleteApproval || pendingPlanApproval);
3221
+ const approvalLockActive = Boolean(pendingDeleteApproval || pendingRunApproval || pendingPlanApproval);
2995
3222
  const activeAssistantIdRef = useRef(null);
2996
3223
  const activeAssistantAutoSkillNamesRef = useRef([]);
2997
3224
  const streamedAssistantHandledRef = useRef(false);
@@ -3006,6 +3233,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3006
3233
  const activePlanStepInfoRef = useRef(null);
3007
3234
  const activePlanStepTitleRef = useRef('');
3008
3235
  const deleteApprovalResolverRef = useRef(null);
3236
+ const runApprovalResolverRef = useRef(null);
3009
3237
 
3010
3238
  useEffect(() => {
3011
3239
  const rawStartupActivities = runtime.consumeStartupEvents?.();
@@ -3031,27 +3259,42 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3031
3259
  useEffect(() => {
3032
3260
  if (typeof runtime.setRequestToolApproval !== 'function') return () => {};
3033
3261
  runtime.setRequestToolApproval((request) => {
3034
- const normalized = normalizeDeleteApprovalRequest(request);
3035
- if (!normalized) {
3036
- return Promise.resolve({ approved: false });
3262
+ const deleteNorm = normalizeDeleteApprovalRequest(request);
3263
+ if (deleteNorm) {
3264
+ setDeleteApprovalInput('');
3265
+ setDeleteApprovalError('');
3266
+ setPendingDeleteApproval(deleteNorm);
3267
+ return new Promise((resolve) => {
3268
+ deleteApprovalResolverRef.current = resolve;
3269
+ });
3037
3270
  }
3038
- setDeleteApprovalInput('');
3039
- setDeleteApprovalError('');
3040
- setPendingDeleteApproval(normalized);
3041
- return new Promise((resolve) => {
3042
- deleteApprovalResolverRef.current = resolve;
3043
- });
3271
+ const runNorm = normalizeRunApprovalRequest(request);
3272
+ if (runNorm) {
3273
+ setRunApprovalInput('');
3274
+ setRunApprovalError('');
3275
+ setPendingRunApproval(runNorm);
3276
+ return new Promise((resolve) => {
3277
+ runApprovalResolverRef.current = resolve;
3278
+ });
3279
+ }
3280
+ return Promise.resolve({ approved: false });
3044
3281
  });
3045
3282
  return () => {
3046
3283
  runtime.setRequestToolApproval(null);
3047
3284
  deleteApprovalResolverRef.current = null;
3285
+ runApprovalResolverRef.current = null;
3048
3286
  };
3049
3287
  }, [runtime]);
3050
3288
 
3051
3289
  useEffect(() => {
3052
3290
  messagesRef.current = messages;
3053
3291
  }, [messages]);
3054
- const startupHint = copy.generic.startupHint;
3292
+ const startupHints = copy.generic.startupHints;
3293
+ const startupHint = useMemo(() => {
3294
+ const arr = Array.isArray(startupHints) ? startupHints : [];
3295
+ if (arr.length === 0) return '';
3296
+ return arr[Math.floor(Math.random() * arr.length)];
3297
+ }, [startupHints]);
3055
3298
  const isBackspaceKey = (value, key) =>
3056
3299
  Boolean(key?.backspace) || value === '\u0008' || value === '\u007f' || (key?.ctrl && value === 'h');
3057
3300
  const isDeleteKey = (value, key) =>
@@ -3239,7 +3482,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3239
3482
  ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
3240
3483
  ...(startedAt ? { startedAt } : {}),
3241
3484
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
3242
- ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
3485
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {}),
3486
+ ...(toolEvent.fileChange ? { fileChange: toolEvent.fileChange } : {})
3243
3487
  });
3244
3488
  } else {
3245
3489
  toolCalls[idx] = {
@@ -3265,7 +3509,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3265
3509
  ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
3266
3510
  ...(startedAt ? { startedAt } : {}),
3267
3511
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
3268
- ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
3512
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {}),
3513
+ ...(toolEvent.fileChange ? { fileChange: toolEvent.fileChange } : {})
3269
3514
  };
3270
3515
  if (segmentIdx === -1) {
3271
3516
  segments.push(patch);
@@ -3716,7 +3961,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3716
3961
  status: 'done',
3717
3962
  durationMs: event.durationMs,
3718
3963
  summary: event.summary,
3719
- arguments: event.arguments
3964
+ arguments: event.arguments,
3965
+ fileChange: event.fileChange || null
3720
3966
  });
3721
3967
  }
3722
3968
  if (event?.type === 'tool:blocked') {
@@ -4137,6 +4383,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4137
4383
  return;
4138
4384
  }
4139
4385
 
4386
+ if (pendingRunApproval) {
4387
+ if (key.return) {
4388
+ const answer = parseDeleteApprovalAnswer(runApprovalInput);
4389
+ if (answer === 'approve' || answer === 'deny') {
4390
+ const resolver = runApprovalResolverRef.current;
4391
+ runApprovalResolverRef.current = null;
4392
+ setPendingRunApproval(null);
4393
+ setRunApprovalInput('');
4394
+ setRunApprovalError('');
4395
+ if (resolver) resolver({ approved: answer === 'approve' });
4396
+ } else {
4397
+ setRunApprovalError(copy.runApproval.invalidAnswer);
4398
+ }
4399
+ return;
4400
+ }
4401
+
4402
+ if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
4403
+ setRunApprovalInput((prev) => prev.slice(0, -1));
4404
+ setRunApprovalError('');
4405
+ return;
4406
+ }
4407
+
4408
+ if (isPrintableInput(value, key)) {
4409
+ setRunApprovalInput((prev) => `${prev}${value}`);
4410
+ setRunApprovalError('');
4411
+ return;
4412
+ }
4413
+
4414
+ if (key.ctrl && value === 'c') {
4415
+ if (busy && typeof runtime.abort === 'function') {
4416
+ runtime.abort();
4417
+ return;
4418
+ }
4419
+ exit();
4420
+ return;
4421
+ }
4422
+
4423
+ return;
4424
+ }
4425
+
4140
4426
  if (pendingPlanApproval) {
4141
4427
  if (key.return) {
4142
4428
  const parsed = parsePlanApprovalAnswer(planApprovalInput);
@@ -4488,9 +4774,11 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4488
4774
  );
4489
4775
  const activeApprovalLock = pendingDeleteApproval
4490
4776
  ? { text: copy.deleteApproval.inputLocked }
4491
- : pendingPlanApproval
4492
- ? { text: copy.planApproval.inputLocked }
4493
- : null;
4777
+ : pendingRunApproval
4778
+ ? { text: copy.runApproval.inputLocked }
4779
+ : pendingPlanApproval
4780
+ ? { text: copy.planApproval.inputLocked }
4781
+ : null;
4494
4782
 
4495
4783
  return h(
4496
4784
  Box,
@@ -4526,6 +4814,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4526
4814
  copy,
4527
4815
  cursorVisible
4528
4816
  }),
4817
+ h(RunApprovalPanel, {
4818
+ request: pendingRunApproval,
4819
+ inputValue: runApprovalInput,
4820
+ errorText: runApprovalError,
4821
+ copy,
4822
+ cursorVisible
4823
+ }),
4529
4824
  h(PlanApprovalPanel, {
4530
4825
  request: pendingPlanApproval,
4531
4826
  inputValue: planApprovalInput,
@@ -6,7 +6,7 @@ export function describeSystemToolActivity(copy, parsed, { done = false, blocked
6
6
  return done ? copy.toolActivity.doneProjectIndex : copy.toolActivity.doingProjectIndex;
7
7
  }
8
8
  if (parsed.base === 'file_index') {
9
- const safeTarget = trimText(parsed.target || '.codemini-project/file-index.json', 72);
9
+ const safeTarget = trimText(parsed.target || '.codemini/file-index.json', 72);
10
10
  if (blocked) return makeBlocked(copy, safeTarget);
11
11
  return done ? `${copy.toolActivity.doneFileIndex}: ${safeTarget}` : `${copy.toolActivity.doingFileIndex}: ${safeTarget}`;
12
12
  }