codemini-cli 0.3.9 → 0.4.1

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} 个工具调用`,
@@ -169,6 +180,10 @@ const TUI_COPY = {
169
180
  doingGlob: '正在按模式查找文件',
170
181
  doneGrep: '已搜索关键词',
171
182
  doingGrep: '正在搜索关键词',
183
+ doneWebFetch: '已抓取网页',
184
+ doingWebFetch: '正在抓取网页',
185
+ doneWebSearch: '已搜索网页',
186
+ doingWebSearch: '正在搜索网页',
172
187
  doneCommand: '已执行命令',
173
188
  doingCommand: '正在执行命令',
174
189
  doneUpdateTodos: '已更新待办',
@@ -264,6 +279,30 @@ const TUI_COPY = {
264
279
  answerLabel: '确认输入(yes/no)',
265
280
  answerPlaceholder: 'yes 或 no'
266
281
  },
282
+ runApproval: {
283
+ title: '确认执行命令?',
284
+ commandLabel: '命令',
285
+ riskLabel: '风险等级',
286
+ descriptionLabel: '说明',
287
+ sideEffectsLabel: '副作用',
288
+ lowRisk: '低',
289
+ mediumRisk: '中',
290
+ highRisk: '高',
291
+ prompt: '输入 yes 执行,输入 no 取消。',
292
+ invalidAnswer: '请输入 yes 或 no。',
293
+ inputLocked: '命令审批进行中,请输入 yes 或 no',
294
+ answerLabel: '审批输入(yes/no)',
295
+ answerPlaceholder: 'yes 或 no'
296
+ },
297
+ fileChangeSummary: {
298
+ title: '文件改动',
299
+ fileLabel: '文件',
300
+ statusLabel: '状态',
301
+ editStatus: '编辑',
302
+ createStatus: '新建',
303
+ deleteStatus: '删除',
304
+ changesLabel: '改动'
305
+ },
267
306
  planApproval: {
268
307
  title: '确认执行计划?',
269
308
  goalLabel: '目标',
@@ -305,7 +344,18 @@ const TUI_COPY = {
305
344
  pendingQueue: 'pending queue',
306
345
  commandPaletteGroupedSelect: 'command palette | grouped select mode',
307
346
  commandPaletteGroupedSuggestions: 'command palette | grouped suggestions',
308
- startupHint: 'Use /help, /commands, /compact, /stop, /exit, !<shell>. Tab for slash autocomplete.',
347
+ startupHints: [
348
+ '🧭 Use /help to view command help. Tab for slash autocomplete.',
349
+ '📋 Try /plan mode for complex tasks — let the AI propose a plan before coding.',
350
+ '⏫ Use ↑↓ arrow keys to browse input history and repeat previous actions.',
351
+ '🐚 Type !<shell command> to run local terminal commands, e.g. !ls, !git status.',
352
+ '🔧 Ctrl+T toggles tool call detail expansion/collapse.',
353
+ '📊 Try /status to check current session mode, model, and token usage.',
354
+ '🧩 Use /mode plan to switch to planning mode — AI proposes a plan before coding.',
355
+ '🆕 /new starts a fresh session to begin a clean slate.',
356
+ '🧠 /memory lets you view and manage the AI\'s persistent memory for better personalization.',
357
+ '💤 CodeMini auto-"dreams" to rest, consolidate errors, and self-optimize — it gets smarter over time~'
358
+ ],
309
359
  toolSummaryExpanded: 'Tool summary: expanded',
310
360
  toolSummaryCollapsed: 'Tool summary: collapsed',
311
361
  toolChainCollapsed: (count) => `${count} earlier tool calls hidden`,
@@ -342,6 +392,10 @@ const TUI_COPY = {
342
392
  doingGlob: 'Matching files by pattern',
343
393
  doneGrep: 'Searched keywords',
344
394
  doingGrep: 'Searching keywords',
395
+ doneWebFetch: 'Fetched page',
396
+ doingWebFetch: 'Fetching page',
397
+ doneWebSearch: 'Searched web',
398
+ doingWebSearch: 'Searching web',
345
399
  doneCommand: 'Ran command',
346
400
  doingCommand: 'Running command',
347
401
  doneUpdateTodos: 'Updated todos',
@@ -437,6 +491,30 @@ const TUI_COPY = {
437
491
  answerLabel: 'Approval input (yes/no)',
438
492
  answerPlaceholder: 'yes or no'
439
493
  },
494
+ runApproval: {
495
+ title: 'Confirm command execution?',
496
+ commandLabel: 'Command',
497
+ riskLabel: 'Risk Level',
498
+ descriptionLabel: 'Description',
499
+ sideEffectsLabel: 'Side Effects',
500
+ lowRisk: 'Low',
501
+ mediumRisk: 'Medium',
502
+ highRisk: 'High',
503
+ prompt: 'Type yes to execute, or no to cancel.',
504
+ invalidAnswer: 'Please enter yes or no.',
505
+ inputLocked: 'Command approval is active; type yes or no',
506
+ answerLabel: 'Approval input (yes/no)',
507
+ answerPlaceholder: 'yes or no'
508
+ },
509
+ fileChangeSummary: {
510
+ title: 'File Changes',
511
+ fileLabel: 'File',
512
+ statusLabel: 'Status',
513
+ editStatus: 'Edit',
514
+ createStatus: 'Create',
515
+ deleteStatus: 'Delete',
516
+ changesLabel: 'Changes'
517
+ },
440
518
  planApproval: {
441
519
  title: 'Approve this plan?',
442
520
  goalLabel: 'Goal',
@@ -993,6 +1071,25 @@ export function parseDeleteApprovalAnswer(value) {
993
1071
  return normalized ? 'invalid' : 'empty';
994
1072
  }
995
1073
 
1074
+ export function normalizeRunApprovalRequest(request) {
1075
+ if (!request || String(request?.name || '').trim() !== 'run') return null;
1076
+ const details =
1077
+ request?.approvalDetails && typeof request.approvalDetails === 'object' && !Array.isArray(request.approvalDetails)
1078
+ ? request.approvalDetails
1079
+ : {};
1080
+ const command = String(details.command || request?.arguments?.command || '').trim();
1081
+ if (!command) return null;
1082
+ return {
1083
+ id: String(request?.id || '').trim(),
1084
+ toolName: 'run',
1085
+ command,
1086
+ risk: details.risk || 'high',
1087
+ description: details.evaluation?.description || '',
1088
+ sideEffects: details.evaluation?.sideEffects || '',
1089
+ recommendation: details.evaluation?.recommendation || 'deny'
1090
+ };
1091
+ }
1092
+
996
1093
  export function parsePlanApprovalAnswer(value) {
997
1094
  const raw = String(value || '').trim();
998
1095
  if (!raw) return { action: 'empty', command: '' };
@@ -1039,36 +1136,42 @@ export function formatDeleteApprovalLines(copy, request) {
1039
1136
  ];
1040
1137
  }
1041
1138
 
1139
+ export function formatPlanApprovalLines(copy, request) {
1140
+ if (!request) return [];
1141
+ return [String(copy?.planApproval?.title || '').trim()].filter(Boolean);
1142
+ }
1143
+
1042
1144
  function getActivityDisplayParts(activity) {
1043
1145
  if (isCodeGenerationActivityName(activity?.name)) {
1044
1146
  return {
1045
- primary: 'Code',
1147
+ primary: '🧠 Code',
1046
1148
  secondary: ' (generation)'
1047
1149
  };
1048
1150
  }
1049
1151
  const parsed = parseToolDisplayName(activity?.name);
1050
- if (parsed.base === 'run') {
1152
+ const base = String(parsed.base || '').toLowerCase();
1153
+ if (base === 'run') {
1051
1154
  const intent = classifyCommandIntent(parsed.target);
1052
1155
  return {
1053
- primary: getIntentLabel(intent.kind),
1156
+ primary: `${getIntentEmoji(intent.kind)} ${getIntentLabel(intent.kind)}`,
1054
1157
  secondary: parsed.target ? `(${parsed.target})` : ''
1055
1158
  };
1056
1159
  }
1057
1160
  if ((activity?.type || 'tool') === 'skill') {
1058
1161
  return {
1059
- primary: `Skill`,
1162
+ primary: '🧩 Skill',
1060
1163
  secondary: `(${activity?.name || 'unknown'})`
1061
1164
  };
1062
1165
  }
1063
1166
  if ((activity?.type || 'tool') === 'system_tool') {
1064
- if (parsed.base === 'project_index') {
1065
- return { primary: 'Project Index', secondary: '' };
1167
+ if (base === 'project_index') {
1168
+ return { primary: '🗂️ Project Index', secondary: '' };
1066
1169
  }
1067
- if (parsed.base === 'file_index') {
1068
- return { primary: 'File Index', secondary: parsed.target ? `(${parsed.target})` : '' };
1170
+ if (base === 'file_index') {
1171
+ return { primary: '🗂️ File Index', secondary: parsed.target ? `(${parsed.target})` : '' };
1069
1172
  }
1070
1173
  return {
1071
- primary: 'Index',
1174
+ primary: '🗂️ Index',
1072
1175
  secondary: parsed.target ? `(${parsed.target})` : parsed.base ? `(${parsed.base})` : ''
1073
1176
  };
1074
1177
  }
@@ -1080,6 +1183,8 @@ function getActivityDisplayParts(activity) {
1080
1183
  patch: 'Patch',
1081
1184
  run: 'Run',
1082
1185
  grep: 'Search',
1186
+ web_fetch: 'Fetch',
1187
+ web_search: 'Web Search',
1083
1188
  glob: 'Glob',
1084
1189
  list: 'List',
1085
1190
  list_background_tasks: 'Tasks',
@@ -1090,19 +1195,55 @@ function getActivityDisplayParts(activity) {
1090
1195
  read_plan: 'Read Plan',
1091
1196
  update_plan: 'Update Plan'
1092
1197
  };
1198
+ const emojis = {
1199
+ read: '📖',
1200
+ edit: '✏️',
1201
+ write: '📝',
1202
+ delete: '🗑️',
1203
+ patch: '🩹',
1204
+ run: '⚙️',
1205
+ grep: '🔍',
1206
+ web_fetch: '🌐',
1207
+ web_search: '🌐',
1208
+ glob: '🧭',
1209
+ list: '📂',
1210
+ list_background_tasks: '🗃️',
1211
+ get_background_task: '📌',
1212
+ stop_background_task: '⏹️',
1213
+ list_files: '🧭',
1214
+ update_todos: '✅',
1215
+ read_plan: '📋',
1216
+ update_plan: '🗓️'
1217
+ };
1093
1218
  return {
1094
- primary: labels[parsed.base] || parsed.base || 'Tool',
1219
+ primary: `${emojis[base] || '🔧'} ${labels[base] || parsed.base || 'Tool'}`,
1095
1220
  secondary: parsed.target ? `(${parsed.target})` : ''
1096
1221
  };
1097
1222
  }
1098
1223
 
1224
+ function getIntentEmoji(kind) {
1225
+ const map = {
1226
+ test: '🧪',
1227
+ install: '📦',
1228
+ build: '🏗️',
1229
+ frontend: '🖥️',
1230
+ backend: '🛰️',
1231
+ database: '🗄️',
1232
+ docker: '🐳',
1233
+ command: '⚙️'
1234
+ };
1235
+ return map[kind] || '⚙️';
1236
+ }
1237
+
1099
1238
  export function isIndexSystemToolName(name) {
1100
1239
  const parsed = parseToolDisplayName(name);
1101
1240
  return parsed.base === 'project_index' || parsed.base === 'file_index';
1102
1241
  }
1103
1242
 
1104
1243
  export function shouldShowCompletionFooter(msg) {
1105
- return Boolean(msg && msg.label === 'coder' && !msg.loading && !(msg.phase || '').trim());
1244
+ if (!msg || msg.loading || (msg.phase || '').trim()) return false;
1245
+ const label = (msg.label || '').toLowerCase();
1246
+ return label === 'coder' || label === 'planner' || label === 'reviewer' || label === 'tester';
1106
1247
  }
1107
1248
 
1108
1249
  function describeToolActivity(name, copy, { done = false, blocked = false } = {}) {
@@ -1183,7 +1324,6 @@ function stageDescriptor(inputStage, busy, runtimeStatus, copy) {
1183
1324
 
1184
1325
  function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
1185
1326
  const status = normalizeRuntimeStatus(runtimeStatus, copy);
1186
- const spinnerChar = SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length];
1187
1327
  return h(
1188
1328
  Box,
1189
1329
  {
@@ -1195,7 +1335,7 @@ function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
1195
1335
  },
1196
1336
  h(Text, { color: busy ? 'greenBright' : 'gray' }, busy ? copy.generic.live : copy.generic.idle),
1197
1337
  h(Text, { color: 'gray' }, ' '),
1198
- h(Text, { color: busy ? 'cyanBright' : 'gray' }, spinnerChar),
1338
+ h(Text, { color: busy ? 'cyanBright' : 'gray' }, busy ? '●' : '○'),
1199
1339
  h(Text, { color: 'gray' }, ' '),
1200
1340
  h(Text, { color: busy ? 'white' : 'gray' }, status.title || copy.generic.waitingForInput)
1201
1341
  );
@@ -2273,7 +2413,17 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
2273
2413
  } else {
2274
2414
  pushTextRows(msg?.text || '');
2275
2415
  const toolCalls = Array.isArray(msg?.toolCalls) ? msg.toolCalls : [];
2276
- toolCalls.forEach((tool, idx) => pushActivityRows(tool, idx, toolCalls.length));
2416
+ const pendingToolCalls = Array.isArray(msg?.pendingToolCalls) ? msg.pendingToolCalls : [];
2417
+ const visibleCalls = [
2418
+ ...toolCalls,
2419
+ ...pendingToolCalls.filter((pending) => {
2420
+ if (!pending) return false;
2421
+ if (pending.id && toolCalls.some((tool) => tool?.id && tool.id === pending.id)) return false;
2422
+ const pendingBase = parseToolDisplayName(pending.name).base;
2423
+ return !toolCalls.some((tool) => parseToolDisplayName(tool?.name).base === pendingBase && tool?.status === 'running');
2424
+ })
2425
+ ];
2426
+ visibleCalls.forEach((tool, idx) => pushActivityRows(tool, idx, visibleCalls.length));
2277
2427
  }
2278
2428
 
2279
2429
  const codeGenerationRows = getCodeGenerationActivityRows(msg);
@@ -2320,15 +2470,20 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2320
2470
  ? 'redBright'
2321
2471
  : 'cyanBright';
2322
2472
  const durationText = formatActivityDurationText(row);
2473
+ const trailingLoader =
2474
+ row.status === 'running'
2475
+ ? h(Text, { color: 'gray' }, ` ${SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length]}`)
2476
+ : null;
2323
2477
  return h(
2324
2478
  Box,
2325
2479
  { key: `row-tool-${msg.id}-${idx}` },
2326
2480
  h(Text, { color: 'gray' }, ' '),
2327
- h(Text, { color: dotColor }, row.status === 'running' ? SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length] : '●'),
2481
+ h(Text, { color: dotColor }, '●'),
2328
2482
  h(Text, { color: 'gray' }, ' '),
2329
2483
  h(Text, { color: textColor }, display.primary),
2330
2484
  h(Text, { color: 'gray' }, display.secondary),
2331
- durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null
2485
+ durationText ? h(Text, { color: row.statusColor }, ` ${durationText}`) : null,
2486
+ trailingLoader
2332
2487
  );
2333
2488
  }
2334
2489
  if (row.kind === 'activity-summary') {
@@ -2594,8 +2749,11 @@ const MessageBubble = React.memo(function MessageBubble({ msg, loaderTick, showT
2594
2749
  shouldShowCompletionFooter(msg)
2595
2750
  ? h(
2596
2751
  Box,
2597
- { marginTop: 1, marginLeft: 1, key: `row-completion-${msg.id}` },
2598
- h(Text, { color: 'gray', dimColor: true }, copy.generic.taskCompleted)
2752
+ { marginTop: 1, flexDirection: 'column', key: `row-completion-${msg.id}` },
2753
+ h(FileChangeSummary, { segments: msg.segments, copy }),
2754
+ h(Box, { marginLeft: 1, marginTop: 1 },
2755
+ h(Text, { color: 'gray', dimColor: true }, copy.generic.taskCompleted)
2756
+ )
2599
2757
  )
2600
2758
  : null
2601
2759
  )
@@ -2868,12 +3026,102 @@ function DeleteApprovalPanel({ request, inputValue, errorText, copy, cursorVisib
2868
3026
  );
2869
3027
  }
2870
3028
 
3029
+ function RunApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
3030
+ if (!request) return null;
3031
+ const details = request?.toolName === 'run' ? request : normalizeRunApprovalRequest(request);
3032
+ if (!details) return null;
3033
+ const c = copy.runApproval || {};
3034
+ const riskColor = details.risk === 'low' ? 'green' : details.risk === 'medium' ? 'yellow' : 'redBright';
3035
+ const borderColor = details.risk === 'medium' ? 'yellow' : 'redBright';
3036
+ const riskLabel = details.risk === 'low' ? c.lowRisk : details.risk === 'medium' ? c.mediumRisk : c.highRisk;
3037
+ const placeholder = String(c.answerPlaceholder || '').trim();
3038
+ return h(
3039
+ Box,
3040
+ {
3041
+ marginTop: 1,
3042
+ flexDirection: 'column',
3043
+ borderStyle: 'round',
3044
+ borderColor,
3045
+ paddingX: 1,
3046
+ paddingY: 0
3047
+ },
3048
+ h(Text, { color: borderColor }, c.title),
3049
+ h(Text, { color: 'white' }, `${c.commandLabel}: ${details.command}`),
3050
+ h(Text, null, `${c.riskLabel}: `, h(Text, { color: riskColor, bold: true }, riskLabel || details.risk)),
3051
+ details.description ? h(Text, { color: 'gray' }, `${c.descriptionLabel}: ${details.description}`) : null,
3052
+ details.sideEffects ? h(Text, { color: 'gray' }, `${c.sideEffectsLabel}: ${details.sideEffects}`) : null,
3053
+ h(Text, { color: 'gray' }, c.prompt),
3054
+ h(
3055
+ Box,
3056
+ { marginTop: 1 },
3057
+ h(Text, { color: borderColor }, `${c.answerLabel}: `),
3058
+ h(ApprovalCursorLine, {
3059
+ inputValue,
3060
+ placeholder: placeholder || ' ',
3061
+ cursorVisible,
3062
+ accent: borderColor
3063
+ })
3064
+ ),
3065
+ errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
3066
+ );
3067
+ }
3068
+
3069
+ function FileChangeSummary({ segments, copy }) {
3070
+ if (!Array.isArray(segments) || segments.length === 0) return null;
3071
+ const c = copy.fileChangeSummary || {};
3072
+ const changes = new Map();
3073
+ for (const seg of segments) {
3074
+ if (!seg.fileChange) continue;
3075
+ const p = seg.fileChange.path;
3076
+ if (!p) continue;
3077
+ const existing = changes.get(p);
3078
+ if (existing) {
3079
+ /* 同一文件多次编辑,合并行数,取最高 action */
3080
+ existing.linesAdded += seg.fileChange.linesAdded || 0;
3081
+ existing.linesRemoved += seg.fileChange.linesRemoved || 0;
3082
+ const ACTION_ORDER = { delete: 3, create: 2, edit: 1 };
3083
+ if ((ACTION_ORDER[seg.fileChange.action] || 0) > (ACTION_ORDER[existing.action] || 0)) {
3084
+ existing.action = seg.fileChange.action;
3085
+ }
3086
+ } else {
3087
+ changes.set(p, { path: p, action: seg.fileChange.action, linesAdded: seg.fileChange.linesAdded || 0, linesRemoved: seg.fileChange.linesRemoved || 0 });
3088
+ }
3089
+ }
3090
+ if (changes.size === 0) return null;
3091
+ const entries = [...changes.values()];
3092
+ return h(
3093
+ Box,
3094
+ { marginTop: 1, flexDirection: 'column', borderStyle: 'round', borderColor: 'gray', paddingX: 1 },
3095
+ h(Text, { color: 'cyan', bold: true }, c.title || 'File Changes'),
3096
+ ...entries.map((entry) => {
3097
+ const statusMap = { edit: c.editStatus || 'Edit', create: c.createStatus || 'Create', delete: c.deleteStatus || 'Delete' };
3098
+ const statusColor = entry.action === 'create' ? 'green' : entry.action === 'delete' ? 'red' : 'yellow';
3099
+ const statusText = statusMap[entry.action] || entry.action;
3100
+ let changesText = '';
3101
+ if (entry.action !== 'delete') {
3102
+ const parts = [];
3103
+ if (entry.linesAdded > 0) parts.push(h(Text, { color: 'green' }, `+${entry.linesAdded}`));
3104
+ if (entry.linesRemoved > 0) parts.push(h(Text, { color: 'red' }, `-${entry.linesRemoved}`));
3105
+ if (parts.length > 0) {
3106
+ changesText = parts.reduce((acc, el, i) => i === 0 ? [el] : [...acc, ' ', el], []);
3107
+ }
3108
+ }
3109
+ return h(
3110
+ Box,
3111
+ { key: entry.path },
3112
+ h(Text, { color: 'white' }, ` ${entry.path}`),
3113
+ h(Text, { color: 'gray' }, ' '),
3114
+ h(Text, { color: statusColor }, statusText),
3115
+ changesText ? h(Text, null, ' ', changesText) : null
3116
+ );
3117
+ })
3118
+ );
3119
+ }
3120
+
2871
3121
  function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
2872
3122
  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
3123
  const placeholder = String(copy.planApproval.answerPlaceholder || '').trim();
3124
+ const lines = formatPlanApprovalLines(copy, request);
2877
3125
  return h(
2878
3126
  Box,
2879
3127
  {
@@ -2884,11 +3132,9 @@ function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible
2884
3132
  paddingX: 1,
2885
3133
  paddingY: 0
2886
3134
  },
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),
3135
+ ...lines.map((line, index) =>
3136
+ h(Text, { key: `plan-approval-line-${index}`, color: 'yellowBright' }, line)
3137
+ ),
2892
3138
  h(
2893
3139
  Box,
2894
3140
  { marginTop: 1 },
@@ -2988,10 +3234,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2988
3234
  const [pendingDeleteApproval, setPendingDeleteApproval] = useState(null);
2989
3235
  const [deleteApprovalInput, setDeleteApprovalInput] = useState('');
2990
3236
  const [deleteApprovalError, setDeleteApprovalError] = useState('');
3237
+ const [pendingRunApproval, setPendingRunApproval] = useState(null);
3238
+ const [runApprovalInput, setRunApprovalInput] = useState('');
3239
+ const [runApprovalError, setRunApprovalError] = useState('');
2991
3240
  const [pendingPlanApproval, setPendingPlanApproval] = useState(null);
2992
3241
  const [planApprovalInput, setPlanApprovalInput] = useState('');
2993
3242
  const [planApprovalError, setPlanApprovalError] = useState('');
2994
- const approvalLockActive = Boolean(pendingDeleteApproval || pendingPlanApproval);
3243
+ const approvalLockActive = Boolean(pendingDeleteApproval || pendingRunApproval || pendingPlanApproval);
2995
3244
  const activeAssistantIdRef = useRef(null);
2996
3245
  const activeAssistantAutoSkillNamesRef = useRef([]);
2997
3246
  const streamedAssistantHandledRef = useRef(false);
@@ -3006,6 +3255,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3006
3255
  const activePlanStepInfoRef = useRef(null);
3007
3256
  const activePlanStepTitleRef = useRef('');
3008
3257
  const deleteApprovalResolverRef = useRef(null);
3258
+ const runApprovalResolverRef = useRef(null);
3009
3259
 
3010
3260
  useEffect(() => {
3011
3261
  const rawStartupActivities = runtime.consumeStartupEvents?.();
@@ -3031,27 +3281,42 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3031
3281
  useEffect(() => {
3032
3282
  if (typeof runtime.setRequestToolApproval !== 'function') return () => {};
3033
3283
  runtime.setRequestToolApproval((request) => {
3034
- const normalized = normalizeDeleteApprovalRequest(request);
3035
- if (!normalized) {
3036
- return Promise.resolve({ approved: false });
3284
+ const deleteNorm = normalizeDeleteApprovalRequest(request);
3285
+ if (deleteNorm) {
3286
+ setDeleteApprovalInput('');
3287
+ setDeleteApprovalError('');
3288
+ setPendingDeleteApproval(deleteNorm);
3289
+ return new Promise((resolve) => {
3290
+ deleteApprovalResolverRef.current = resolve;
3291
+ });
3037
3292
  }
3038
- setDeleteApprovalInput('');
3039
- setDeleteApprovalError('');
3040
- setPendingDeleteApproval(normalized);
3041
- return new Promise((resolve) => {
3042
- deleteApprovalResolverRef.current = resolve;
3043
- });
3293
+ const runNorm = normalizeRunApprovalRequest(request);
3294
+ if (runNorm) {
3295
+ setRunApprovalInput('');
3296
+ setRunApprovalError('');
3297
+ setPendingRunApproval(runNorm);
3298
+ return new Promise((resolve) => {
3299
+ runApprovalResolverRef.current = resolve;
3300
+ });
3301
+ }
3302
+ return Promise.resolve({ approved: false });
3044
3303
  });
3045
3304
  return () => {
3046
3305
  runtime.setRequestToolApproval(null);
3047
3306
  deleteApprovalResolverRef.current = null;
3307
+ runApprovalResolverRef.current = null;
3048
3308
  };
3049
3309
  }, [runtime]);
3050
3310
 
3051
3311
  useEffect(() => {
3052
3312
  messagesRef.current = messages;
3053
3313
  }, [messages]);
3054
- const startupHint = copy.generic.startupHint;
3314
+ const startupHints = copy.generic.startupHints;
3315
+ const startupHint = useMemo(() => {
3316
+ const arr = Array.isArray(startupHints) ? startupHints : [];
3317
+ if (arr.length === 0) return '';
3318
+ return arr[Math.floor(Math.random() * arr.length)];
3319
+ }, [startupHints]);
3055
3320
  const isBackspaceKey = (value, key) =>
3056
3321
  Boolean(key?.backspace) || value === '\u0008' || value === '\u007f' || (key?.ctrl && value === 'h');
3057
3322
  const isDeleteKey = (value, key) =>
@@ -3239,7 +3504,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3239
3504
  ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
3240
3505
  ...(startedAt ? { startedAt } : {}),
3241
3506
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
3242
- ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
3507
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {}),
3508
+ ...(toolEvent.fileChange ? { fileChange: toolEvent.fileChange } : {})
3243
3509
  });
3244
3510
  } else {
3245
3511
  toolCalls[idx] = {
@@ -3265,7 +3531,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3265
3531
  ...(toolEvent.arguments !== undefined ? { arguments: toolEvent.arguments } : {}),
3266
3532
  ...(startedAt ? { startedAt } : {}),
3267
3533
  ...(toolEvent.durationMs !== undefined ? { durationMs: toolEvent.durationMs } : {}),
3268
- ...(toolEvent.summary ? { summary: toolEvent.summary } : {})
3534
+ ...(toolEvent.summary ? { summary: toolEvent.summary } : {}),
3535
+ ...(toolEvent.fileChange ? { fileChange: toolEvent.fileChange } : {})
3269
3536
  };
3270
3537
  if (segmentIdx === -1) {
3271
3538
  segments.push(patch);
@@ -3716,7 +3983,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3716
3983
  status: 'done',
3717
3984
  durationMs: event.durationMs,
3718
3985
  summary: event.summary,
3719
- arguments: event.arguments
3986
+ arguments: event.arguments,
3987
+ fileChange: event.fileChange || null
3720
3988
  });
3721
3989
  }
3722
3990
  if (event?.type === 'tool:blocked') {
@@ -4137,6 +4405,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4137
4405
  return;
4138
4406
  }
4139
4407
 
4408
+ if (pendingRunApproval) {
4409
+ if (key.return) {
4410
+ const answer = parseDeleteApprovalAnswer(runApprovalInput);
4411
+ if (answer === 'approve' || answer === 'deny') {
4412
+ const resolver = runApprovalResolverRef.current;
4413
+ runApprovalResolverRef.current = null;
4414
+ setPendingRunApproval(null);
4415
+ setRunApprovalInput('');
4416
+ setRunApprovalError('');
4417
+ if (resolver) resolver({ approved: answer === 'approve' });
4418
+ } else {
4419
+ setRunApprovalError(copy.runApproval.invalidAnswer);
4420
+ }
4421
+ return;
4422
+ }
4423
+
4424
+ if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
4425
+ setRunApprovalInput((prev) => prev.slice(0, -1));
4426
+ setRunApprovalError('');
4427
+ return;
4428
+ }
4429
+
4430
+ if (isPrintableInput(value, key)) {
4431
+ setRunApprovalInput((prev) => `${prev}${value}`);
4432
+ setRunApprovalError('');
4433
+ return;
4434
+ }
4435
+
4436
+ if (key.ctrl && value === 'c') {
4437
+ if (busy && typeof runtime.abort === 'function') {
4438
+ runtime.abort();
4439
+ return;
4440
+ }
4441
+ exit();
4442
+ return;
4443
+ }
4444
+
4445
+ return;
4446
+ }
4447
+
4140
4448
  if (pendingPlanApproval) {
4141
4449
  if (key.return) {
4142
4450
  const parsed = parsePlanApprovalAnswer(planApprovalInput);
@@ -4488,9 +4796,11 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4488
4796
  );
4489
4797
  const activeApprovalLock = pendingDeleteApproval
4490
4798
  ? { text: copy.deleteApproval.inputLocked }
4491
- : pendingPlanApproval
4492
- ? { text: copy.planApproval.inputLocked }
4493
- : null;
4799
+ : pendingRunApproval
4800
+ ? { text: copy.runApproval.inputLocked }
4801
+ : pendingPlanApproval
4802
+ ? { text: copy.planApproval.inputLocked }
4803
+ : null;
4494
4804
 
4495
4805
  return h(
4496
4806
  Box,
@@ -4526,6 +4836,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
4526
4836
  copy,
4527
4837
  cursorVisible
4528
4838
  }),
4839
+ h(RunApprovalPanel, {
4840
+ request: pendingRunApproval,
4841
+ inputValue: runApprovalInput,
4842
+ errorText: runApprovalError,
4843
+ copy,
4844
+ cursorVisible
4845
+ }),
4529
4846
  h(PlanApprovalPanel, {
4530
4847
  request: pendingPlanApproval,
4531
4848
  inputValue: planApprovalInput,
@@ -12,5 +12,19 @@ export function describeMiscToolActivity(copy, parsed, rawName, { done = false,
12
12
  if (parsed.base === 'update_todos') {
13
13
  return blocked ? makeBlocked(copy, 'update_todos') : done ? copy.toolActivity.doneUpdateTodos : copy.toolActivity.doingUpdateTodos;
14
14
  }
15
+ if (parsed.base === 'web_fetch') {
16
+ const target = parsed.target || parsed.raw;
17
+ const label = done
18
+ ? (copy.toolActivity.doneWebFetch || copy.toolActivity.doneGeneric)
19
+ : (copy.toolActivity.doingWebFetch || copy.toolActivity.doingGeneric);
20
+ return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
21
+ }
22
+ if (parsed.base === 'web_search') {
23
+ const target = parsed.target || parsed.raw;
24
+ const label = done
25
+ ? (copy.toolActivity.doneWebSearch || copy.toolActivity.doneGeneric)
26
+ : (copy.toolActivity.doingWebSearch || copy.toolActivity.doingGeneric);
27
+ return blocked ? makeBlocked(copy, target) : `${label}: ${target}`;
28
+ }
15
29
  return blocked ? `${copy.toolActivity.blocked}: ${parsed.raw}` : done ? `${copy.toolActivity.doneGeneric}: ${parsed.raw}` : `${copy.toolActivity.doingGeneric}: ${parsed.raw}`;
16
30
  }
@@ -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
  }