codemini-cli 0.3.5 → 0.3.7

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.
@@ -44,6 +44,38 @@ const ROLE_STYLES = {
44
44
  badgeText: 'black',
45
45
  chrome: 'gray'
46
46
  },
47
+ planner: {
48
+ accent: 'magentaBright',
49
+ border: 'magenta',
50
+ text: 'magentaBright',
51
+ badgeBg: 'magenta',
52
+ badgeText: 'white',
53
+ chrome: 'gray'
54
+ },
55
+ reviewer: {
56
+ accent: 'yellowBright',
57
+ border: 'yellow',
58
+ text: 'yellowBright',
59
+ badgeBg: 'yellow',
60
+ badgeText: 'black',
61
+ chrome: 'gray'
62
+ },
63
+ tester: {
64
+ accent: 'blueBright',
65
+ border: 'blue',
66
+ text: 'blueBright',
67
+ badgeBg: 'blue',
68
+ badgeText: 'white',
69
+ chrome: 'gray'
70
+ },
71
+ summarizer: {
72
+ accent: 'cyanBright',
73
+ border: 'cyan',
74
+ text: 'cyanBright',
75
+ badgeBg: 'cyan',
76
+ badgeText: 'black',
77
+ chrome: 'gray'
78
+ },
47
79
  system: {
48
80
  accent: 'yellowBright',
49
81
  border: 'yellow',
@@ -72,7 +104,7 @@ const ROLE_STYLES = {
72
104
 
73
105
  const TUI_COPY = {
74
106
  zh: {
75
- roleLabels: { you: '你', coder: 'CODER', system: '系统', error: '错误', pending: '等待中' },
107
+ roleLabels: { you: '👤 你', coder: '💻 CODER', planner: '📋 PLANNER', reviewer: '🔍 REVIEWER', tester: '🧪 TESTER', summarizer: '📝 SUMMARIZER', system: '⚙️ 系统', error: '错误', pending: '等待中' },
76
108
  generic: {
77
109
  waitingForInput: '等待输入',
78
110
  ready: '就绪',
@@ -99,7 +131,7 @@ const TUI_COPY = {
99
131
  pendingQueue: '等待队列',
100
132
  commandPaletteGroupedSelect: '命令面板 | 分组选择模式',
101
133
  commandPaletteGroupedSuggestions: '命令面板 | 分组候选',
102
- startupHint: '使用 /help、/commands、/compact、/exit、!<shell>。Tab 可自动补全 slash 命令。',
134
+ startupHint: '使用 /help、/commands、/compact、/exit、/stop、!<shell>。Tab 可自动补全 slash 命令。',
103
135
  toolSummaryExpanded: '工具摘要:已展开',
104
136
  toolSummaryCollapsed: '工具摘要:已收起',
105
137
  toolChainCollapsed: (count) => `已折叠更早的 ${count} 个工具调用`,
@@ -126,6 +158,8 @@ const TUI_COPY = {
126
158
  doingEdit: '正在编辑文件',
127
159
  doneWrite: '已写入文件',
128
160
  doingWrite: '正在写入文件',
161
+ doneDelete: '已删除目标',
162
+ doingDelete: '正在等待删除确认',
129
163
  donePatch: '已应用补丁',
130
164
  doingPatch: '正在应用补丁',
131
165
  doneList: '已列出目录',
@@ -204,16 +238,30 @@ const TUI_COPY = {
204
238
  compactingContext: '正在压缩上下文',
205
239
  autoCompactTriggered: (mode, threshold) => `自动压缩已触发(${mode},阈值 ${threshold}%)`,
206
240
  requestFailed: '请求失败',
241
+ responseStopped: '回答已中止',
207
242
  localCommandRunning: '正在执行本地命令',
208
243
  queuedWaiting: '排队中,等待上一轮完成',
209
244
  idleReady: '等待输入',
210
245
  idleReadyDetail: '就绪',
211
246
  idleAfterTurn: '空闲',
212
247
  idleAfterTurnDetail: '等待下一轮输入'
248
+ },
249
+ deleteApproval: {
250
+ title: '确认删除?',
251
+ pathLabel: '路径',
252
+ nameLabel: '名称',
253
+ typeLabel: '类型',
254
+ fileType: '文件',
255
+ directoryType: '目录',
256
+ prompt: '输入 yes 确认删除,输入 no 取消。',
257
+ invalidAnswer: '请输入 yes 或 no。',
258
+ inputLocked: '删除确认进行中,请输入 yes 或 no',
259
+ answerLabel: '确认输入(yes/no)',
260
+ answerPlaceholder: 'yes 或 no'
213
261
  }
214
262
  },
215
263
  en: {
216
- roleLabels: { you: 'YOU', coder: 'CODER', system: 'SYSTEM', error: 'ERROR', pending: 'PENDING' },
264
+ roleLabels: { you: 'YOU', coder: 'CODER', planner: 'PLANNER', reviewer: 'REVIEWER', tester: 'TESTER', summarizer: 'SUMMARIZER', system: 'SYSTEM', error: 'ERROR', pending: 'PENDING' },
217
265
  generic: {
218
266
  waitingForInput: 'waiting for input',
219
267
  ready: 'ready',
@@ -240,7 +288,7 @@ const TUI_COPY = {
240
288
  pendingQueue: 'pending queue',
241
289
  commandPaletteGroupedSelect: 'command palette | grouped select mode',
242
290
  commandPaletteGroupedSuggestions: 'command palette | grouped suggestions',
243
- startupHint: 'Use /help, /commands, /compact, /exit, !<shell>. Tab for slash autocomplete.',
291
+ startupHint: 'Use /help, /commands, /compact, /stop, /exit, !<shell>. Tab for slash autocomplete.',
244
292
  toolSummaryExpanded: 'Tool summary: expanded',
245
293
  toolSummaryCollapsed: 'Tool summary: collapsed',
246
294
  toolChainCollapsed: (count) => `${count} earlier tool calls hidden`,
@@ -267,6 +315,8 @@ const TUI_COPY = {
267
315
  doingEdit: 'Editing file',
268
316
  doneWrite: 'Wrote file',
269
317
  doingWrite: 'Writing file',
318
+ doneDelete: 'Deleted target',
319
+ doingDelete: 'Waiting for delete approval',
270
320
  donePatch: 'Applied patch',
271
321
  doingPatch: 'Applying patch',
272
322
  doneList: 'Listed directory',
@@ -345,12 +395,26 @@ const TUI_COPY = {
345
395
  compactingContext: 'compacting context',
346
396
  autoCompactTriggered: (mode, threshold) => `auto-compact triggered (${mode}, threshold ${threshold}%)`,
347
397
  requestFailed: 'request failed',
398
+ responseStopped: 'Response stopped',
348
399
  localCommandRunning: 'running local command',
349
400
  queuedWaiting: 'queued, waiting for current turn',
350
401
  idleReady: 'waiting for input',
351
402
  idleReadyDetail: 'ready',
352
403
  idleAfterTurn: 'idle',
353
404
  idleAfterTurnDetail: 'ready for next input'
405
+ },
406
+ deleteApproval: {
407
+ title: 'Confirm deletion?',
408
+ pathLabel: 'Path',
409
+ nameLabel: 'Name',
410
+ typeLabel: 'Type',
411
+ fileType: 'file',
412
+ directoryType: 'directory',
413
+ prompt: 'Type yes to delete, or no to cancel.',
414
+ invalidAnswer: 'Please enter yes or no.',
415
+ inputLocked: 'Delete approval is active; type yes or no',
416
+ answerLabel: 'Approval input (yes/no)',
417
+ answerPlaceholder: 'yes or no'
354
418
  }
355
419
  }
356
420
  };
@@ -747,8 +811,8 @@ function buildUiMessagesFromSessionHistory(sessionMessages, nextId) {
747
811
  function safeJsonParse(raw) {
748
812
  try {
749
813
  return JSON.parse(String(raw || '{}'));
750
- } catch {
751
- return null;
814
+ } catch (parseError) {
815
+ return { _raw: String(raw || ''), _invalid_json: true, _parseError: parseError.message };
752
816
  }
753
817
  }
754
818
 
@@ -867,6 +931,49 @@ export function getPendingUserMessageMeta(copy, { immediateLocal = false, inFlig
867
931
  };
868
932
  }
869
933
 
934
+ export function normalizeDeleteApprovalRequest(request) {
935
+ if (!request || String(request?.name || '').trim() !== 'delete') return null;
936
+ const details =
937
+ request?.approvalDetails && typeof request.approvalDetails === 'object' && !Array.isArray(request.approvalDetails)
938
+ ? request.approvalDetails
939
+ : request?.arguments?.approval && typeof request.arguments.approval === 'object' && !Array.isArray(request.arguments.approval)
940
+ ? request.arguments.approval
941
+ : {};
942
+ const fallbackPath = String(details.path || request?.arguments?.path || '').trim();
943
+ const pathValue = fallbackPath;
944
+ const nameValue = String(details.name || (pathValue ? pathValue.split(/[\\/]/).pop() : '') || '').trim();
945
+ const typeValue = String(details.type || '').trim() === 'directory' ? 'directory' : 'file';
946
+ if (!pathValue) return null;
947
+ return {
948
+ id: String(request?.id || '').trim(),
949
+ toolName: 'delete',
950
+ path: pathValue,
951
+ name: nameValue || pathValue,
952
+ type: typeValue
953
+ };
954
+ }
955
+
956
+ export function parseDeleteApprovalAnswer(value) {
957
+ const normalized = String(value || '').trim().toLowerCase();
958
+ if (normalized === 'yes') return 'approve';
959
+ if (normalized === 'no') return 'deny';
960
+ return normalized ? 'invalid' : 'empty';
961
+ }
962
+
963
+ export function formatDeleteApprovalLines(copy, request) {
964
+ const details = normalizeDeleteApprovalRequest(request);
965
+ if (!details) return [];
966
+ const typeLabel = details.type === 'directory' ? copy.deleteApproval.directoryType : copy.deleteApproval.fileType;
967
+ const pathDisplay = details.path.includes('/') || details.path.includes('\\') ? details.path : `./${details.path}`;
968
+ return [
969
+ copy.deleteApproval.title,
970
+ `${copy.deleteApproval.pathLabel}: ${pathDisplay}`,
971
+ `${copy.deleteApproval.nameLabel}: ${details.name}`,
972
+ `${copy.deleteApproval.typeLabel}: ${typeLabel}`,
973
+ copy.deleteApproval.prompt
974
+ ];
975
+ }
976
+
870
977
  function getActivityDisplayParts(activity) {
871
978
  if (isCodeGenerationActivityName(activity?.name)) {
872
979
  return {
@@ -904,6 +1011,7 @@ function getActivityDisplayParts(activity) {
904
1011
  read: 'Read',
905
1012
  edit: 'Edit',
906
1013
  write: 'Write',
1014
+ delete: 'Delete',
907
1015
  patch: 'Patch',
908
1016
  run: 'Run',
909
1017
  grep: 'Search',
@@ -1036,7 +1144,6 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1036
1144
  const pct = Math.min(100, Math.max(0, pctRaw));
1037
1145
  const filled = Math.min(12, Math.max(0, Math.round((pct / 100) * 12)));
1038
1146
  const activeColor = pct < 40 ? 'greenBright' : pct < 75 ? 'yellowBright' : 'redBright';
1039
- const statusColor = runtimeStatus?.color || activeColor;
1040
1147
  const chunks = Array.from({ length: 12 }, (_, idx) => {
1041
1148
  const zoneColor = idx < 5 ? 'greenBright' : idx < 9 ? 'yellowBright' : 'redBright';
1042
1149
  const color = idx < filled ? zoneColor : 'gray';
@@ -1048,7 +1155,7 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1048
1155
  Box,
1049
1156
  { justifyContent: 'flex-end', alignItems: 'center' },
1050
1157
  h(Text, { color: 'gray' }, '上下文 '),
1051
- h(Text, { color: statusColor }, `${Math.round(pct)}% `),
1158
+ h(Text, { color: activeColor }, `${Math.round(pct)}% `),
1052
1159
  h(
1053
1160
  Box,
1054
1161
  { flexDirection: 'row' },
@@ -1072,78 +1179,58 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1072
1179
 
1073
1180
  function PlanStrip({ planState, copy }) {
1074
1181
  if (!planState || !planState.total) return null;
1075
- const progress = `${planState.current}/${planState.total}`;
1076
- const stripComplete = Boolean(planState.completed) && !planState.failed;
1077
- const statusLabel = planState.failed ? copy.generic.attention : stripComplete ? copy.generic.taskCompleted : copy.generic.active;
1078
- const statusColor = planState.failed ? 'redBright' : stripComplete ? 'cyanBright' : 'greenBright';
1079
- const roleLabel =
1080
- planState.resultStatus || stripComplete || planState.failed
1081
- ? copy?.roleLabels?.system === 'SYSTEM'
1082
- ? 'RESULT'
1083
- : '结果'
1084
- : String(planState.role || 'agent').toUpperCase();
1085
- const titleLabel =
1086
- planState.resultStatus || stripComplete || planState.failed
1087
- ? copy?.roleLabels?.system === 'SYSTEM'
1088
- ? 'Plan execution result'
1089
- : '计划执行结果'
1090
- : planState.title || 'running plan step';
1182
+ const isDone = planState.completed;
1183
+ const borderColor = isDone ? 'green' : planState.failed ? 'red' : 'cyan';
1184
+ const isEnglish = copy?.roleLabels?.system === 'SYSTEM';
1185
+ const completedLabel = isEnglish ? 'DONE' : '已完成';
1091
1186
  return h(
1092
1187
  Box,
1093
- {
1094
- marginBottom: 1,
1095
- borderStyle: 'round',
1096
- borderColor: planState.failed ? 'red' : 'cyan',
1097
- paddingX: 1,
1098
- paddingY: 0,
1099
- flexDirection: 'column'
1100
- },
1188
+ { marginBottom: 1, flexDirection: 'row' },
1189
+ h(Box, { width: 2 }, h(Text, { color: borderColor }, '│')),
1101
1190
  h(
1102
1191
  Box,
1103
- { justifyContent: 'space-between' },
1192
+ {
1193
+ flexDirection: 'column',
1194
+ borderStyle: 'round',
1195
+ borderColor,
1196
+ paddingX: 1,
1197
+ paddingY: 0,
1198
+ width: '100%'
1199
+ },
1104
1200
  h(
1105
1201
  Box,
1106
- null,
1107
- h(Text, { color: 'black', backgroundColor: planState.failed ? 'red' : 'cyanBright' }, ` ${copy.generic.plan} ${progress} `),
1108
- h(Text, { color: 'gray' }, ' '),
1109
- h(Text, { color: 'magentaBright' }, roleLabel)
1202
+ { justifyContent: 'space-between', marginBottom: planState.steps.length > 0 ? 1 : 0 },
1203
+ h(Text, { color: 'black', backgroundColor: isDone ? 'greenBright' : 'cyanBright' }, ' Plan Summary '),
1204
+ isDone
1205
+ ? h(Text, { color: 'black', backgroundColor: 'greenBright' }, ` ${completedLabel} `)
1206
+ : null
1110
1207
  ),
1111
- h(Text, { color: statusColor }, statusLabel)
1112
- ),
1113
- h(Text, { color: 'white' }, titleLabel),
1114
- planState.resultVerified
1115
- ? h(
1116
- Box,
1117
- { marginTop: 1 },
1118
- h(Text, { color: 'gray' }, trimText(planState.resultVerified, 120))
1119
- )
1120
- : null,
1121
- planState.steps.length > 0
1122
- ? h(
1123
- Box,
1124
- { marginTop: 1, flexDirection: 'column' },
1125
- ...planState.steps.slice(-4).map((step, idx) =>
1126
- h(
1127
- Box,
1128
- { key: `plan-step-${idx}`, marginTop: idx === 0 ? 0 : 1 },
1129
- h(Text, { color: step.status === 'active' ? 'cyanBright' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.status === 'active' ? '>' : step.status === 'failed' ? 'x' : '·'} `),
1130
- h(Text, { color: step.status === 'active' ? 'yellowBright' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.index}/${step.total}`),
1131
- h(Text, { color: 'gray' }, ' '),
1132
- h(Text, { color: step.status === 'active' ? 'magentaBright' : step.status === 'failed' ? 'redBright' : 'gray' }, String(step.role || 'agent').toUpperCase()),
1133
- h(Text, { color: 'gray' }, ' '),
1134
- h(Text, { color: step.status === 'active' ? 'white' : step.status === 'failed' ? 'redBright' : 'gray' }, step.title)
1208
+ planState.steps.length > 0
1209
+ ? h(
1210
+ Box,
1211
+ { flexDirection: 'column' },
1212
+ ...planState.steps.map((step, idx) => {
1213
+ const roleKey = step.role ? step.role.toLowerCase() : '';
1214
+ const normalizedRole = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'].includes(roleKey) ? roleKey : 'coder';
1215
+ const stepTheme = roleStyle(normalizedRole);
1216
+ const roleTag = step.role ? step.role.toUpperCase() : '';
1217
+ const stepDone = step.status === 'done' || isDone;
1218
+ const stepFailed = step.status === 'failed';
1219
+ const marker = stepFailed ? '✗' : stepDone ? '✓' : '·';
1220
+ const markerColor = stepFailed ? 'redBright' : stepDone ? 'greenBright' : 'gray';
1221
+ return h(
1222
+ Box,
1223
+ { key: `plan-step-${idx}` },
1224
+ h(Text, { color: markerColor }, `${marker} `),
1225
+ roleTag ? h(Text, { color: stepTheme.badgeText, backgroundColor: stepTheme.badgeBg }, ` ${roleTag} `) : null,
1226
+ roleTag ? h(Text, { color: 'gray' }, ' ') : null,
1227
+ h(Text, { color: stepDone && !stepFailed ? 'gray' : 'white' }, `${step.index}. ${step.title}`)
1228
+ );
1229
+ }
1135
1230
  )
1136
1231
  )
1137
- )
1138
- : null,
1139
- planState.resultNext
1140
- ? h(
1141
- Box,
1142
- { marginTop: 1 },
1143
- h(Text, { color: 'black', backgroundColor: 'yellowBright' }, ' NEXT '),
1144
- h(Text, { color: 'gray' }, ` ${trimText(planState.resultNext, 108)}`)
1145
- )
1146
- : null
1232
+ : null
1233
+ )
1147
1234
  );
1148
1235
  }
1149
1236
 
@@ -1242,21 +1329,43 @@ export function parseAutoPlanSummaryMessage(text) {
1242
1329
  warnings: '',
1243
1330
  failed: '',
1244
1331
  warningSteps: '',
1245
- failedSteps: ''
1332
+ failedSteps: '',
1333
+ planSteps: []
1246
1334
  };
1247
1335
 
1336
+ let inPlanSteps = false;
1248
1337
  for (const line of lines.slice(1)) {
1249
- if (line.startsWith('File: ')) parsed.filePath = line.slice('File: '.length).trim();
1250
- else if (line.startsWith('Plan File: ')) parsed.filePath = line.slice('Plan File: '.length).trim();
1251
- else if (line.startsWith('Plan Summary: ')) parsed.planSummary = line.slice('Plan Summary: '.length).trim();
1252
- else if (line.startsWith('Final Summary: ')) parsed.finalSummary = line.slice('Final Summary: '.length).trim();
1253
- else if (line.startsWith('Approval: ')) parsed.approval = line.slice('Approval: '.length).trim();
1254
- else if (line.startsWith('Steps: ')) parsed.stepsTotal = line.slice('Steps: '.length).trim();
1255
- else if (line.startsWith('Completed: ')) parsed.completed = line.slice('Completed: '.length).trim();
1256
- else if (line.startsWith('Warnings: ')) parsed.warnings = line.slice('Warnings: '.length).trim();
1257
- else if (line.startsWith('Failed: ')) parsed.failed = line.slice('Failed: '.length).trim();
1258
- else if (line.startsWith('Warning steps: ')) parsed.warningSteps = line.slice('Warning steps: '.length).trim();
1259
- else if (line.startsWith('Failed steps: ')) parsed.failedSteps = line.slice('Failed steps: '.length).trim();
1338
+ if (line === 'Plan Steps:') {
1339
+ inPlanSteps = true;
1340
+ continue;
1341
+ }
1342
+ if (inPlanSteps) {
1343
+ // Parse " 1. [role] title"
1344
+ const stepMatch = line.match(/^(\d+)\.\s*\[([^\]]+)\]\s+(.+)$/);
1345
+ if (stepMatch) {
1346
+ parsed.planSteps.push({
1347
+ index: Number(stepMatch[1]),
1348
+ role: String(stepMatch[2] || '').trim().toLowerCase(),
1349
+ title: String(stepMatch[3] || '').trim(),
1350
+ status: 'pending'
1351
+ });
1352
+ } else {
1353
+ inPlanSteps = false;
1354
+ }
1355
+ }
1356
+ if (!inPlanSteps) {
1357
+ if (line.startsWith('File: ')) parsed.filePath = line.slice('File: '.length).trim();
1358
+ else if (line.startsWith('Plan File: ')) parsed.filePath = line.slice('Plan File: '.length).trim();
1359
+ else if (line.startsWith('Plan Summary: ')) parsed.planSummary = line.slice('Plan Summary: '.length).trim();
1360
+ else if (line.startsWith('Final Summary: ')) parsed.finalSummary = line.slice('Final Summary: '.length).trim();
1361
+ else if (line.startsWith('Approval: ')) parsed.approval = line.slice('Approval: '.length).trim();
1362
+ else if (line.startsWith('Steps: ')) parsed.stepsTotal = line.slice('Steps: '.length).trim();
1363
+ else if (line.startsWith('Completed: ')) parsed.completed = line.slice('Completed: '.length).trim();
1364
+ else if (line.startsWith('Warnings: ')) parsed.warnings = line.slice('Warnings: '.length).trim();
1365
+ else if (line.startsWith('Failed: ')) parsed.failed = line.slice('Failed: '.length).trim();
1366
+ else if (line.startsWith('Warning steps: ')) parsed.warningSteps = line.slice('Warning steps: '.length).trim();
1367
+ else if (line.startsWith('Failed steps: ')) parsed.failedSteps = line.slice('Failed steps: '.length).trim();
1368
+ }
1260
1369
  }
1261
1370
 
1262
1371
  return parsed;
@@ -1405,7 +1514,7 @@ function extractPreviewLinesFromTool(tool, maxLines = 3) {
1405
1514
  typeof tool?.arguments === 'string'
1406
1515
  ? (() => {
1407
1516
  const parsedArguments = safeJsonParse(tool.arguments);
1408
- if (parsedArguments) {
1517
+ if (parsedArguments && !parsedArguments._invalid_json) {
1409
1518
  const parsedPreview = collectPreviewStrings(parsedArguments);
1410
1519
  if (parsedPreview.length > 0) return parsedPreview;
1411
1520
  }
@@ -1424,7 +1533,7 @@ function extractPreviewLinesFromTool(tool, maxLines = 3) {
1424
1533
  }
1425
1534
 
1426
1535
  function getLatestToolPreviewLines(msg, maxLines = 3) {
1427
- const codeTools = new Set(['edit', 'write', 'patch', 'generate_diff']);
1536
+ const codeTools = new Set(['edit', 'write']);
1428
1537
  const extractFromCalls = (calls) => {
1429
1538
  for (let index = calls.length - 1; index >= 0; index -= 1) {
1430
1539
  const tool = calls[index];
@@ -1462,7 +1571,7 @@ export function getGeneratingCodePlaceholderRows(msg, copy, contentWidth = 72) {
1462
1571
  const previewWindow = getLatestToolPreviewLines(msg, 3);
1463
1572
  if (previewWindow.lines.length === 0) return [];
1464
1573
  const hasRunningCodeTool = (Array.isArray(msg?.toolCalls) ? msg.toolCalls : []).some(
1465
- (tool) => tool?.status === 'running' && new Set(['edit', 'write', 'patch', 'generate_diff']).has(parseToolDisplayName(tool?.name).base)
1574
+ (tool) => tool?.status === 'running' && new Set(['edit', 'write']).has(parseToolDisplayName(tool?.name).base)
1466
1575
  );
1467
1576
  const isCodeGenerationStatus = liveStatus === String(copy?.runtime?.generatingCode || '').trim();
1468
1577
  if (!isCodeGenerationStatus && !(msg?.phase === 'tooling' && hasRunningCodeTool)) return [];
@@ -1760,13 +1869,11 @@ function isCodeActivityName(name) {
1760
1869
  'edit',
1761
1870
  'write',
1762
1871
  'write_file',
1763
- 'patch',
1764
1872
  'replace_text',
1765
1873
  'replace_block',
1766
1874
  'insert_before',
1767
1875
  'insert_after',
1768
- 'validate_edit',
1769
- 'generate_diff'
1876
+ 'validate_edit'
1770
1877
  ]).has(parsed.base);
1771
1878
  }
1772
1879
 
@@ -1979,13 +2086,7 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
1979
2086
  const trimmed = line.trim();
1980
2087
  const planProgress = parsePlanProgressLine(trimmed);
1981
2088
  if (planProgress) {
1982
- rows.push({
1983
- kind: 'plan-progress',
1984
- current: planProgress.current,
1985
- total: planProgress.total,
1986
- role: planProgress.role,
1987
- title: trimText(planProgress.title, Math.max(12, contentWidth - 18))
1988
- });
2089
+ // Skip rendering plan progress lines inline — shown in header badge instead
1989
2090
  continue;
1990
2091
  }
1991
2092
  if (trimmed.startsWith('```')) {
@@ -2208,16 +2309,8 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2208
2309
  );
2209
2310
  }
2210
2311
  if (row.kind === 'plan-progress') {
2211
- return h(
2212
- Box,
2213
- { key: `row-plan-progress-${msg.id}-${idx}`, marginTop: 1, marginBottom: 1 },
2214
- h(Text, { color: 'cyanBright' }, '[plan] '),
2215
- h(Text, { color: 'yellowBright' }, `Step ${row.current}/${row.total}`),
2216
- h(Text, { color: 'gray' }, ' -> '),
2217
- h(Text, { color: 'magentaBright' }, String(row.role || 'agent').toUpperCase()),
2218
- h(Text, { color: 'gray' }, ': '),
2219
- h(Text, { color: 'white' }, row.title)
2220
- );
2312
+ // Already shown in header badge — skip inline rendering
2313
+ return null;
2221
2314
  }
2222
2315
  if (row.kind === 'status') {
2223
2316
  const dots = '.'.repeat((loaderTick % 3) + 1);
@@ -2366,11 +2459,7 @@ export function moveSuggestionSelection(currentIndex, itemCount, direction, page
2366
2459
 
2367
2460
  function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
2368
2461
  if (msg?.planStrip) {
2369
- return h(
2370
- Box,
2371
- { marginBottom: 1 },
2372
- h(PlanStrip, { planState: msg.planState, copy })
2373
- );
2462
+ return h(PlanStrip, { planState: msg.planState, copy });
2374
2463
  }
2375
2464
  if (msg?.planSummary || parseAutoPlanSummaryMessage(msg?.text)) {
2376
2465
  return h(PlanSummaryBubble, { msg, copy });
@@ -2403,7 +2492,8 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
2403
2492
  h(
2404
2493
  Box,
2405
2494
  null,
2406
- h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `)
2495
+ h(Text, { color: theme.badgeText, backgroundColor: theme.badgeBg }, ` ${messageLabel(msg.label, copy)} `),
2496
+ msg.planStep ? h(Text, { color: 'gray', dimColor: true }, ` ${msg.planStep} `) : null
2407
2497
  ),
2408
2498
  autoSkillBadge
2409
2499
  ? h(Text, { color: 'blueBright' }, autoSkillBadge)
@@ -2554,6 +2644,8 @@ function InputBar({
2554
2644
  afterCursor,
2555
2645
  cursorVisible,
2556
2646
  busy,
2647
+ disabled = false,
2648
+ disabledText = '',
2557
2649
  inputStage,
2558
2650
  pendingQueueLength,
2559
2651
  showToolDetails,
@@ -2595,20 +2687,60 @@ function InputBar({
2595
2687
  Box,
2596
2688
  null,
2597
2689
  h(Text, { color: 'cyan' }, 'codemini> '),
2598
- h(Text, { color: 'white' }, beforeCursor),
2599
- h(
2600
- Text,
2601
- {
2602
- color: cursorVisible ? 'black' : 'white',
2603
- backgroundColor: cursorVisible ? 'cyanBright' : undefined
2604
- },
2605
- underCursor || ' '
2606
- ),
2607
- h(Text, { color: 'white' }, afterCursor)
2690
+ disabled
2691
+ ? h(Text, { color: 'gray' }, disabledText || '')
2692
+ : [
2693
+ h(Text, { key: 'before', color: 'white' }, beforeCursor),
2694
+ h(
2695
+ Text,
2696
+ {
2697
+ key: 'cursor',
2698
+ color: cursorVisible ? 'black' : 'white',
2699
+ backgroundColor: cursorVisible ? 'cyanBright' : undefined
2700
+ },
2701
+ underCursor || ' '
2702
+ ),
2703
+ h(Text, { key: 'after', color: 'white' }, afterCursor)
2704
+ ]
2608
2705
  )
2609
2706
  );
2610
2707
  }
2611
2708
 
2709
+ function DeleteApprovalPanel({ request, inputValue, errorText, copy }) {
2710
+ if (!request) return null;
2711
+ const details =
2712
+ request?.toolName === 'delete'
2713
+ ? request
2714
+ : normalizeDeleteApprovalRequest(request);
2715
+ if (!details) return null;
2716
+ const typeLabel = details.type === 'directory' ? copy.deleteApproval.directoryType : copy.deleteApproval.fileType;
2717
+ const pathDisplay = details.path.includes('/') || details.path.includes('\\') ? details.path : `./${details.path}`;
2718
+ const placeholder = String(copy.deleteApproval.answerPlaceholder || '').trim();
2719
+ return h(
2720
+ Box,
2721
+ {
2722
+ marginTop: 1,
2723
+ flexDirection: 'column',
2724
+ borderStyle: 'round',
2725
+ borderColor: 'redBright',
2726
+ paddingX: 1,
2727
+ paddingY: 0
2728
+ },
2729
+ h(Text, { color: 'redBright' }, copy.deleteApproval.title),
2730
+ h(Text, { color: 'white' }, `${copy.deleteApproval.nameLabel}: ${details.name}`),
2731
+ h(Text, { color: 'white' }, `${copy.deleteApproval.pathLabel}: ${pathDisplay}`),
2732
+ h(Text, { color: 'white' }, `${copy.deleteApproval.typeLabel}: ${typeLabel}`),
2733
+ h(Text, { color: 'gray' }, copy.deleteApproval.prompt),
2734
+ h(
2735
+ Box,
2736
+ { marginTop: 1 },
2737
+ h(Text, { color: 'redBright' }, `${copy.deleteApproval.answerLabel}: `),
2738
+ h(Text, { color: inputValue ? 'white' : 'gray' }, inputValue || placeholder || ' ')
2739
+ ),
2740
+ errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
2741
+ );
2742
+ }
2743
+
2612
2744
  function SignatureBar({ version = '' }) {
2613
2745
  return h(
2614
2746
  Box,
@@ -2690,6 +2822,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2690
2822
  const [debugKeys, setDebugKeys] = useState(false);
2691
2823
  const [lastKeyDebug, setLastKeyDebug] = useState('');
2692
2824
  const [showToolDetails, setShowToolDetails] = useState(false);
2825
+ const [pendingDeleteApproval, setPendingDeleteApproval] = useState(null);
2826
+ const [deleteApprovalInput, setDeleteApprovalInput] = useState('');
2827
+ const [deleteApprovalError, setDeleteApprovalError] = useState('');
2693
2828
  const activeAssistantIdRef = useRef(null);
2694
2829
  const activeAssistantAutoSkillNamesRef = useRef([]);
2695
2830
  const streamedAssistantHandledRef = useRef(false);
@@ -2699,6 +2834,11 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2699
2834
  const messagesRef = useRef([]);
2700
2835
  const pendingQueueRef = useRef([]);
2701
2836
  const deltaBufferRef = useRef('');
2837
+ const activePlanStepNumberRef = useRef(0);
2838
+ const activePlanStepRoleRef = useRef(null);
2839
+ const activePlanStepInfoRef = useRef(null);
2840
+ const activePlanStepTitleRef = useRef('');
2841
+ const deleteApprovalResolverRef = useRef(null);
2702
2842
 
2703
2843
  useEffect(() => {
2704
2844
  const rawStartupActivities = runtime.consumeStartupEvents?.();
@@ -2721,6 +2861,26 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2721
2861
  const planTextBufferRef = useRef('');
2722
2862
  const { exit } = useApp();
2723
2863
 
2864
+ useEffect(() => {
2865
+ if (typeof runtime.setRequestToolApproval !== 'function') return () => {};
2866
+ runtime.setRequestToolApproval((request) => {
2867
+ const normalized = normalizeDeleteApprovalRequest(request);
2868
+ if (!normalized) {
2869
+ return Promise.resolve({ approved: false });
2870
+ }
2871
+ setDeleteApprovalInput('');
2872
+ setDeleteApprovalError('');
2873
+ setPendingDeleteApproval(normalized);
2874
+ return new Promise((resolve) => {
2875
+ deleteApprovalResolverRef.current = resolve;
2876
+ });
2877
+ });
2878
+ return () => {
2879
+ runtime.setRequestToolApproval(null);
2880
+ deleteApprovalResolverRef.current = null;
2881
+ };
2882
+ }, [runtime]);
2883
+
2724
2884
  useEffect(() => {
2725
2885
  messagesRef.current = messages;
2726
2886
  }, [messages]);
@@ -2757,7 +2917,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2757
2917
  : [];
2758
2918
  const hasTransientPanels =
2759
2919
  commandSuggestions.length > 0 || pendingQueue.length > 0 || debugKeys || Boolean(planState?.total);
2760
- const messageContentWidth = Math.max(24, stdoutCols - 18);
2920
+ const messageContentWidth = Math.max(24, stdoutCols - 8);
2761
2921
 
2762
2922
  const syncRuntimeVisualState = (variant = 'ready') => {
2763
2923
  const snapshot = runtime.getRuntimeState?.();
@@ -2765,7 +2925,15 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2765
2925
  setDisplaySessionId(snapshot.sessionId || sessionId);
2766
2926
  setDisplayModel(snapshot.model || model);
2767
2927
  setDisplaySdkProvider(snapshot.sdkProvider || sdkProvider);
2768
- setRuntimeState(snapshot);
2928
+ setRuntimeState((prev) => {
2929
+ if (!prev || !planTextBufferRef.current) return snapshot;
2930
+ const prevTokens = Number(prev.currentContextTokens || 0);
2931
+ const newTokens = Number(snapshot.currentContextTokens || 0);
2932
+ if (newTokens < prevTokens) {
2933
+ return { ...snapshot, currentContextTokens: prevTokens, contextUsagePct: prev.contextUsagePct };
2934
+ }
2935
+ return snapshot;
2936
+ });
2769
2937
  setRuntimeStatus(makeIdleStatus(copy, snapshot, variant));
2770
2938
  };
2771
2939
 
@@ -2775,7 +2943,15 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2775
2943
  setDisplaySessionId(snapshot.sessionId || sessionId);
2776
2944
  setDisplayModel(snapshot.model || model);
2777
2945
  setDisplaySdkProvider(snapshot.sdkProvider || sdkProvider);
2778
- setRuntimeState(snapshot);
2946
+ setRuntimeState((prev) => {
2947
+ if (!prev || !planTextBufferRef.current) return snapshot;
2948
+ const prevTokens = Number(prev.currentContextTokens || 0);
2949
+ const newTokens = Number(snapshot.currentContextTokens || 0);
2950
+ if (newTokens < prevTokens) {
2951
+ return { ...snapshot, currentContextTokens: prevTokens, contextUsagePct: prev.contextUsagePct };
2952
+ }
2953
+ return snapshot;
2954
+ });
2779
2955
  };
2780
2956
 
2781
2957
  useEffect(() => {
@@ -2794,15 +2970,34 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2794
2970
  if (!last) return;
2795
2971
  const current = Number(last[1]);
2796
2972
  const total = Number(last[2]);
2797
- const role = String(last[3] || '').trim();
2973
+ const role = String(last[3] || '').trim().toLowerCase();
2974
+ const normalizedRole = ['planner', 'coder', 'reviewer', 'tester', 'summarizer'].includes(role) ? role : 'coder';
2798
2975
  const title = String(last[4] || '').trim();
2976
+
2977
+ // Detect step transition — finalize old assistant and create a new one
2978
+ if (activePlanStepNumberRef.current > 0 && current !== activePlanStepNumberRef.current) {
2979
+ flushAssistantDelta();
2980
+ const oldId = activeAssistantIdRef.current;
2981
+ if (oldId) {
2982
+ finalizeActiveAssistant();
2983
+ activeAssistantIdRef.current = null;
2984
+ }
2985
+ }
2986
+ activePlanStepNumberRef.current = current;
2987
+
2988
+ activePlanStepRoleRef.current = normalizedRole;
2989
+ activePlanStepInfoRef.current = { current, total };
2990
+ activePlanStepTitleRef.current = title;
2799
2991
  setActiveAssistantMeta({
2800
- planStep: `${current}/${total} · ${role}: ${title}`
2992
+ label: normalizedRole,
2993
+ planStepInfo: { current, total },
2994
+ planStep: `${current}/${total} · ${title}`
2801
2995
  });
2802
2996
  setPlanState((prev) => {
2803
- const steps = (prev.steps || [])
2804
- .map((step) => (step.index === current ? { ...step, status: 'done' } : step))
2997
+ let steps = (prev.steps || [])
2998
+ .map((step) => (step.status === 'active' ? { ...step, status: 'done' } : step))
2805
2999
  .filter((step, idx, arr) => arr.findIndex((x) => x.index === step.index) === idx);
3000
+
2806
3001
  const withoutCurrent = steps.filter((step) => step.index !== current);
2807
3002
  return {
2808
3003
  current,
@@ -2993,13 +3188,16 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2993
3188
  }
2994
3189
  const parsedPlanSummary = result.type === 'system' ? parseAutoPlanSummaryMessage(result.text || '') : null;
2995
3190
  if (parsedPlanSummary?.approval === 'pending') {
3191
+ const preSteps = Array.isArray(parsedPlanSummary.planSteps) && parsedPlanSummary.planSteps.length > 0
3192
+ ? parsedPlanSummary.planSteps
3193
+ : [];
2996
3194
  setPlanState({
2997
3195
  current: 0,
2998
- total: 0,
3196
+ total: preSteps.length,
2999
3197
  role: '',
3000
3198
  title: '',
3001
3199
  failed: false,
3002
- steps: [],
3200
+ steps: preSteps,
3003
3201
  pendingApproval: true,
3004
3202
  completed: false,
3005
3203
  resultStatus: '',
@@ -3035,7 +3233,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3035
3233
  const nextCall = {
3036
3234
  id: toolCall.id || '',
3037
3235
  name: toolCall.name || '',
3038
- arguments: typeof toolCall.arguments === 'string' ? safeJsonParse(toolCall.arguments) ?? toolCall.arguments : toolCall.arguments,
3236
+ arguments: typeof toolCall.arguments === 'string'
3237
+ ? (() => { const p = safeJsonParse(toolCall.arguments); return p._invalid_json ? toolCall.arguments : p; })()
3238
+ : toolCall.arguments,
3039
3239
  status: 'pending',
3040
3240
  type: 'tool'
3041
3241
  };
@@ -3048,11 +3248,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3048
3248
  };
3049
3249
 
3050
3250
  const finalizeActiveAssistant = () => {
3251
+ activePlanStepRoleRef.current = null;
3252
+ activePlanStepInfoRef.current = null;
3253
+ activePlanStepTitleRef.current = '';
3051
3254
  setActiveAssistantMeta({
3052
3255
  loading: false,
3053
3256
  phase: undefined,
3054
3257
  liveStatus: undefined,
3055
- planStep: undefined,
3056
3258
  pendingToolCalls: [],
3057
3259
  codeGenerationEndedAt: undefined,
3058
3260
  autoSkillNames: activeAssistantAutoSkillNamesRef.current
@@ -3088,19 +3290,27 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3088
3290
  if (activeAssistantIdRef.current) return activeAssistantIdRef.current;
3089
3291
  const aid = nextId();
3090
3292
  activeAssistantIdRef.current = aid;
3293
+ const planRole = activePlanStepRoleRef.current;
3294
+ const label = planRole || 'coder';
3295
+ const style = ROLE_STYLES[label] || ROLE_STYLES.coder;
3296
+ const planStepInfo = activePlanStepInfoRef.current;
3297
+ const planStepTitle = activePlanStepTitleRef.current;
3298
+ const planStepDisplay = planStepInfo ? `${planStepInfo.current}/${planStepInfo.total} · ${planStepTitle}` : undefined;
3091
3299
  setMessages((prev) => [
3092
3300
  ...prev,
3093
3301
  {
3094
3302
  id: aid,
3095
- label: 'coder',
3303
+ label,
3096
3304
  text: '',
3097
- color: 'greenBright',
3305
+ color: style.text,
3098
3306
  toolCalls: [],
3099
3307
  segments: [],
3100
3308
  loading: true,
3101
3309
  phase: 'thinking',
3102
3310
  liveStatus: copy.runtime.modelThinking,
3103
- autoSkillNames: activeAssistantAutoSkillNamesRef.current
3311
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current,
3312
+ ...(planStepInfo ? { planStepInfo } : {}),
3313
+ ...(planStepDisplay ? { planStep: planStepDisplay } : {})
3104
3314
  }
3105
3315
  ]);
3106
3316
  return aid;
@@ -3146,6 +3356,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3146
3356
  resultNext: ''
3147
3357
  });
3148
3358
  planTextBufferRef.current = '';
3359
+ activePlanStepNumberRef.current = 0;
3149
3360
  activeAssistantIdRef.current = null;
3150
3361
  activeAssistantAutoSkillNamesRef.current = [];
3151
3362
  streamedAssistantHandledRef.current = false;
@@ -3190,7 +3401,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3190
3401
  if (event?.type === 'assistant:tool_call_delta') {
3191
3402
  ensureActiveAssistant();
3192
3403
  const parsed = parseToolDisplayName(event.toolCall?.name);
3193
- const isCodeTool = new Set(['write', 'edit', 'patch', 'generate_diff']).has(parsed.base);
3404
+ const isCodeTool = new Set(['write', 'edit']).has(parsed.base);
3194
3405
  if (isCodeTool) {
3195
3406
  setRuntimeStatus(makeStatus(copy.runtime.generatingCode, copy.runtime.streamingReply, 'greenBright'));
3196
3407
  setInputStage('streaming');
@@ -3249,7 +3460,6 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3249
3460
  loading: false,
3250
3461
  phase: undefined,
3251
3462
  liveStatus: undefined,
3252
- planStep: undefined,
3253
3463
  pendingToolCalls: [],
3254
3464
  autoSkillNames: activeAssistantAutoSkillNamesRef.current,
3255
3465
  ...(m.codeGenerationStartedAt && !m.codeGenerationEndedAt ? { codeGenerationEndedAt: Date.now() } : {})
@@ -3257,7 +3467,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3257
3467
  })
3258
3468
  );
3259
3469
  }
3260
- if (!hasPlannedTools) {
3470
+ if (!hasPlannedTools && !activePlanStepInfoRef.current) {
3261
3471
  activeAssistantIdRef.current = null;
3262
3472
  }
3263
3473
  if (!hadActiveAssistant && !hasPlannedTools && event.text) {
@@ -3410,6 +3620,22 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3410
3620
  liveStatus: copy.toolActivity.waitingModelAdjust(detail)
3411
3621
  });
3412
3622
  }
3623
+ if (event?.type === 'plan:steps') {
3624
+ const planSteps = Array.isArray(event.steps) ? event.steps : [];
3625
+ if (planSteps.length > 0) {
3626
+ setPlanState((prev) => ({
3627
+ ...prev,
3628
+ total: planSteps.length,
3629
+ steps: planSteps.map((s) => ({
3630
+ index: s.index,
3631
+ total: planSteps.length,
3632
+ role: s.role || '',
3633
+ title: s.title || '',
3634
+ status: 'pending'
3635
+ }))
3636
+ }));
3637
+ }
3638
+ }
3413
3639
  if (event?.type === 'skill:start') {
3414
3640
  ensureActiveAssistant();
3415
3641
  const detail = describeSkillActivity(event.name, copy);
@@ -3489,16 +3715,28 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3489
3715
  return;
3490
3716
  }
3491
3717
  if (result.type !== 'noop') setInputStage('idle');
3492
- if (planTextBufferRef.current && planState.total > 0) {
3493
- setPlanState((prev) => ({
3494
- ...prev,
3495
- steps: (prev.steps || []).map((step) =>
3496
- step.index === prev.current && step.status === 'active' ? { ...step, status: prev.failed ? 'failed' : 'done' } : step
3497
- )
3498
- }));
3718
+ if (planTextBufferRef.current) {
3719
+ setPlanState((prev) => {
3720
+ if (!prev.total) return prev;
3721
+ return {
3722
+ ...prev,
3723
+ completed: !prev.failed,
3724
+ steps: (prev.steps || []).map((step) =>
3725
+ step.status === 'active' ? { ...step, status: prev.failed ? 'failed' : 'done' } : step
3726
+ )
3727
+ };
3728
+ });
3499
3729
  }
3500
3730
  syncRuntimeVisualState(result.type === 'noop' ? 'ready' : 'after');
3501
3731
  if (result.type === 'noop') return;
3732
+ // 被用户中止时显示提示消息
3733
+ if (result.aborted) {
3734
+ setMessages((prev) => [
3735
+ ...prev,
3736
+ { id: nextId(), label: 'system', text: copy.runtime.responseStopped, color: 'yellowBright' }
3737
+ ]);
3738
+ return;
3739
+ }
3502
3740
  if (!shouldAppendAssistantResult(result, activeAssistantIdRef.current, streamedAssistantHandledRef.current)) return;
3503
3741
  appendResultMessage(result);
3504
3742
  })
@@ -3515,7 +3753,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3515
3753
  ...prev,
3516
3754
  failed: prev.total > 0,
3517
3755
  steps: (prev.steps || []).map((step) =>
3518
- step.index === prev.current ? { ...step, status: 'failed' } : step
3756
+ step.status === 'active' ? { ...step, status: 'failed' } : step
3519
3757
  )
3520
3758
  }));
3521
3759
  setMessages((prev) => [
@@ -3527,6 +3765,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3527
3765
  flushAssistantDelta();
3528
3766
  finalizeActiveAssistant();
3529
3767
  activeAssistantIdRef.current = null;
3768
+ activePlanStepNumberRef.current = 0;
3530
3769
  streamedAssistantHandledRef.current = false;
3531
3770
  activeUserMessageIdRef.current = null;
3532
3771
  if (deltaFlushTimerRef.current) {
@@ -3582,6 +3821,19 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3582
3821
  appendResultMessage(result);
3583
3822
  })
3584
3823
  .catch((err) => {
3824
+ // 用户主动中止,不显示为错误
3825
+ if (err?.name === 'AbortError') {
3826
+ updateMessageMeta(userMessageId, {
3827
+ loading: false,
3828
+ phase: undefined,
3829
+ liveStatus: undefined
3830
+ });
3831
+ setMessages((prev) => [
3832
+ ...prev,
3833
+ { id: nextId(), label: 'system', text: copy.runtime.responseStopped, color: 'yellowBright' }
3834
+ ]);
3835
+ return;
3836
+ }
3585
3837
  const message = sanitizeRenderableText(err?.message || String(err));
3586
3838
  updateMessageMeta(userMessageId, {
3587
3839
  loading: false,
@@ -3629,6 +3881,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3629
3881
  escSeqRef.current = '';
3630
3882
  }
3631
3883
 
3884
+ if (pendingDeleteApproval) {
3885
+ if (key.return) {
3886
+ const answer = parseDeleteApprovalAnswer(deleteApprovalInput);
3887
+ if (answer === 'approve' || answer === 'deny') {
3888
+ const resolver = deleteApprovalResolverRef.current;
3889
+ deleteApprovalResolverRef.current = null;
3890
+ setPendingDeleteApproval(null);
3891
+ setDeleteApprovalInput('');
3892
+ setDeleteApprovalError('');
3893
+ if (resolver) resolver({ approved: answer === 'approve' });
3894
+ } else {
3895
+ setDeleteApprovalError(copy.deleteApproval.invalidAnswer);
3896
+ }
3897
+ return;
3898
+ }
3899
+
3900
+ if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
3901
+ setDeleteApprovalInput((prev) => prev.slice(0, -1));
3902
+ setDeleteApprovalError('');
3903
+ return;
3904
+ }
3905
+
3906
+ if (isPrintableInput(value, key)) {
3907
+ setDeleteApprovalInput((prev) => `${prev}${value}`);
3908
+ setDeleteApprovalError('');
3909
+ return;
3910
+ }
3911
+
3912
+ if (key.ctrl && value === 'c') {
3913
+ if (busy && typeof runtime.abort === 'function') {
3914
+ runtime.abort();
3915
+ return;
3916
+ }
3917
+ exit();
3918
+ return;
3919
+ }
3920
+
3921
+ return;
3922
+ }
3923
+
3632
3924
  if (key.upArrow) {
3633
3925
  if (suggestionNav && commandSuggestions.length > 0) {
3634
3926
  setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'up'));
@@ -3733,6 +4025,16 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3733
4025
  setCursorIndex(0);
3734
4026
  if (!line) return;
3735
4027
 
4028
+ // /stop 命令:中止当前正在进行的回答
4029
+ if (line === '/stop' && busy && typeof runtime.abort === 'function') {
4030
+ runtime.abort();
4031
+ setHistory((prev) => [...prev, line]);
4032
+ setHistoryIndex(null);
4033
+ setDraftBeforeHistory('');
4034
+ setHistoryMatches([]);
4035
+ return;
4036
+ }
4037
+
3736
4038
  setHistory((prev) => [...prev, line]);
3737
4039
  setHistoryIndex(null);
3738
4040
  setDraftBeforeHistory('');
@@ -3802,6 +4104,10 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3802
4104
  }
3803
4105
 
3804
4106
  if (key.ctrl && value === 'c') {
4107
+ if (busy && typeof runtime.abort === 'function') {
4108
+ runtime.abort();
4109
+ return;
4110
+ }
3805
4111
  exit();
3806
4112
  return;
3807
4113
  }
@@ -3941,6 +4247,12 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3941
4247
  ),
3942
4248
  h(SuggestionPanel, { commandSuggestions, suggestionNav, menuIndex, copy }),
3943
4249
  h(PendingPanel, { pendingQueue, copy }),
4250
+ h(DeleteApprovalPanel, {
4251
+ request: pendingDeleteApproval,
4252
+ inputValue: deleteApprovalInput,
4253
+ errorText: deleteApprovalError,
4254
+ copy
4255
+ }),
3944
4256
  debugKeys
3945
4257
  ? h(
3946
4258
  Box,
@@ -3954,6 +4266,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3954
4266
  afterCursor,
3955
4267
  cursorVisible,
3956
4268
  busy,
4269
+ disabled: Boolean(pendingDeleteApproval),
4270
+ disabledText: pendingDeleteApproval ? copy.deleteApproval.inputLocked : '',
3957
4271
  inputStage,
3958
4272
  pendingQueueLength: pendingQueue.length,
3959
4273
  showToolDetails,