codemini-cli 0.3.4 → 0.3.6

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',
@@ -961,6 +1069,17 @@ function normalizeRuntimeStatus(status, copy) {
961
1069
  };
962
1070
  }
963
1071
 
1072
+ export function shouldRefreshRuntimeStateForEvent(event) {
1073
+ const type = String(event?.type || '');
1074
+ return (
1075
+ type === 'assistant:start' ||
1076
+ type === 'assistant:delta' ||
1077
+ type === 'assistant:response' ||
1078
+ type === 'tool:result' ||
1079
+ type === 'compact:auto'
1080
+ );
1081
+ }
1082
+
964
1083
  function stageDescriptor(inputStage, busy, runtimeStatus, copy) {
965
1084
  const normalized = normalizeRuntimeStatus(runtimeStatus, copy);
966
1085
  const tag =
@@ -1025,7 +1144,6 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1025
1144
  const pct = Math.min(100, Math.max(0, pctRaw));
1026
1145
  const filled = Math.min(12, Math.max(0, Math.round((pct / 100) * 12)));
1027
1146
  const activeColor = pct < 40 ? 'greenBright' : pct < 75 ? 'yellowBright' : 'redBright';
1028
- const statusColor = runtimeStatus?.color || activeColor;
1029
1147
  const chunks = Array.from({ length: 12 }, (_, idx) => {
1030
1148
  const zoneColor = idx < 5 ? 'greenBright' : idx < 9 ? 'yellowBright' : 'redBright';
1031
1149
  const color = idx < filled ? zoneColor : 'gray';
@@ -1037,7 +1155,7 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1037
1155
  Box,
1038
1156
  { justifyContent: 'flex-end', alignItems: 'center' },
1039
1157
  h(Text, { color: 'gray' }, '上下文 '),
1040
- h(Text, { color: statusColor }, `${Math.round(pct)}% `),
1158
+ h(Text, { color: activeColor }, `${Math.round(pct)}% `),
1041
1159
  h(
1042
1160
  Box,
1043
1161
  { flexDirection: 'row' },
@@ -1061,78 +1179,58 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
1061
1179
 
1062
1180
  function PlanStrip({ planState, copy }) {
1063
1181
  if (!planState || !planState.total) return null;
1064
- const progress = `${planState.current}/${planState.total}`;
1065
- const stripComplete = Boolean(planState.completed) && !planState.failed;
1066
- const statusLabel = planState.failed ? copy.generic.attention : stripComplete ? copy.generic.taskCompleted : copy.generic.active;
1067
- const statusColor = planState.failed ? 'redBright' : stripComplete ? 'cyanBright' : 'greenBright';
1068
- const roleLabel =
1069
- planState.resultStatus || stripComplete || planState.failed
1070
- ? copy?.roleLabels?.system === 'SYSTEM'
1071
- ? 'RESULT'
1072
- : '结果'
1073
- : String(planState.role || 'agent').toUpperCase();
1074
- const titleLabel =
1075
- planState.resultStatus || stripComplete || planState.failed
1076
- ? copy?.roleLabels?.system === 'SYSTEM'
1077
- ? 'Plan execution result'
1078
- : '计划执行结果'
1079
- : 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' : '已完成';
1080
1186
  return h(
1081
1187
  Box,
1082
- {
1083
- marginBottom: 1,
1084
- borderStyle: 'round',
1085
- borderColor: planState.failed ? 'red' : 'cyan',
1086
- paddingX: 1,
1087
- paddingY: 0,
1088
- flexDirection: 'column'
1089
- },
1188
+ { marginBottom: 1, flexDirection: 'row' },
1189
+ h(Box, { width: 2 }, h(Text, { color: borderColor }, '│')),
1090
1190
  h(
1091
1191
  Box,
1092
- { justifyContent: 'space-between' },
1192
+ {
1193
+ flexDirection: 'column',
1194
+ borderStyle: 'round',
1195
+ borderColor,
1196
+ paddingX: 1,
1197
+ paddingY: 0,
1198
+ width: '100%'
1199
+ },
1093
1200
  h(
1094
1201
  Box,
1095
- null,
1096
- h(Text, { color: 'black', backgroundColor: planState.failed ? 'red' : 'cyanBright' }, ` ${copy.generic.plan} ${progress} `),
1097
- h(Text, { color: 'gray' }, ' '),
1098
- 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
1099
1207
  ),
1100
- h(Text, { color: statusColor }, statusLabel)
1101
- ),
1102
- h(Text, { color: 'white' }, titleLabel),
1103
- planState.resultVerified
1104
- ? h(
1105
- Box,
1106
- { marginTop: 1 },
1107
- h(Text, { color: 'gray' }, trimText(planState.resultVerified, 120))
1108
- )
1109
- : null,
1110
- planState.steps.length > 0
1111
- ? h(
1112
- Box,
1113
- { marginTop: 1, flexDirection: 'column' },
1114
- ...planState.steps.slice(-4).map((step, idx) =>
1115
- h(
1116
- Box,
1117
- { key: `plan-step-${idx}`, marginTop: idx === 0 ? 0 : 1 },
1118
- h(Text, { color: step.status === 'active' ? 'cyanBright' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.status === 'active' ? '>' : step.status === 'failed' ? 'x' : '·'} `),
1119
- h(Text, { color: step.status === 'active' ? 'yellowBright' : step.status === 'failed' ? 'redBright' : 'gray' }, `${step.index}/${step.total}`),
1120
- h(Text, { color: 'gray' }, ' '),
1121
- h(Text, { color: step.status === 'active' ? 'magentaBright' : step.status === 'failed' ? 'redBright' : 'gray' }, String(step.role || 'agent').toUpperCase()),
1122
- h(Text, { color: 'gray' }, ' '),
1123
- 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
+ }
1124
1230
  )
1125
1231
  )
1126
- )
1127
- : null,
1128
- planState.resultNext
1129
- ? h(
1130
- Box,
1131
- { marginTop: 1 },
1132
- h(Text, { color: 'black', backgroundColor: 'yellowBright' }, ' NEXT '),
1133
- h(Text, { color: 'gray' }, ` ${trimText(planState.resultNext, 108)}`)
1134
- )
1135
- : null
1232
+ : null
1233
+ )
1136
1234
  );
1137
1235
  }
1138
1236
 
@@ -1231,21 +1329,43 @@ export function parseAutoPlanSummaryMessage(text) {
1231
1329
  warnings: '',
1232
1330
  failed: '',
1233
1331
  warningSteps: '',
1234
- failedSteps: ''
1332
+ failedSteps: '',
1333
+ planSteps: []
1235
1334
  };
1236
1335
 
1336
+ let inPlanSteps = false;
1237
1337
  for (const line of lines.slice(1)) {
1238
- if (line.startsWith('File: ')) parsed.filePath = line.slice('File: '.length).trim();
1239
- else if (line.startsWith('Plan File: ')) parsed.filePath = line.slice('Plan File: '.length).trim();
1240
- else if (line.startsWith('Plan Summary: ')) parsed.planSummary = line.slice('Plan Summary: '.length).trim();
1241
- else if (line.startsWith('Final Summary: ')) parsed.finalSummary = line.slice('Final Summary: '.length).trim();
1242
- else if (line.startsWith('Approval: ')) parsed.approval = line.slice('Approval: '.length).trim();
1243
- else if (line.startsWith('Steps: ')) parsed.stepsTotal = line.slice('Steps: '.length).trim();
1244
- else if (line.startsWith('Completed: ')) parsed.completed = line.slice('Completed: '.length).trim();
1245
- else if (line.startsWith('Warnings: ')) parsed.warnings = line.slice('Warnings: '.length).trim();
1246
- else if (line.startsWith('Failed: ')) parsed.failed = line.slice('Failed: '.length).trim();
1247
- else if (line.startsWith('Warning steps: ')) parsed.warningSteps = line.slice('Warning steps: '.length).trim();
1248
- 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
+ }
1249
1369
  }
1250
1370
 
1251
1371
  return parsed;
@@ -1394,7 +1514,7 @@ function extractPreviewLinesFromTool(tool, maxLines = 3) {
1394
1514
  typeof tool?.arguments === 'string'
1395
1515
  ? (() => {
1396
1516
  const parsedArguments = safeJsonParse(tool.arguments);
1397
- if (parsedArguments) {
1517
+ if (parsedArguments && !parsedArguments._invalid_json) {
1398
1518
  const parsedPreview = collectPreviewStrings(parsedArguments);
1399
1519
  if (parsedPreview.length > 0) return parsedPreview;
1400
1520
  }
@@ -1413,7 +1533,7 @@ function extractPreviewLinesFromTool(tool, maxLines = 3) {
1413
1533
  }
1414
1534
 
1415
1535
  function getLatestToolPreviewLines(msg, maxLines = 3) {
1416
- const codeTools = new Set(['edit', 'write', 'patch', 'generate_diff']);
1536
+ const codeTools = new Set(['edit', 'write']);
1417
1537
  const extractFromCalls = (calls) => {
1418
1538
  for (let index = calls.length - 1; index >= 0; index -= 1) {
1419
1539
  const tool = calls[index];
@@ -1451,7 +1571,7 @@ export function getGeneratingCodePlaceholderRows(msg, copy, contentWidth = 72) {
1451
1571
  const previewWindow = getLatestToolPreviewLines(msg, 3);
1452
1572
  if (previewWindow.lines.length === 0) return [];
1453
1573
  const hasRunningCodeTool = (Array.isArray(msg?.toolCalls) ? msg.toolCalls : []).some(
1454
- (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)
1455
1575
  );
1456
1576
  const isCodeGenerationStatus = liveStatus === String(copy?.runtime?.generatingCode || '').trim();
1457
1577
  if (!isCodeGenerationStatus && !(msg?.phase === 'tooling' && hasRunningCodeTool)) return [];
@@ -1749,13 +1869,11 @@ function isCodeActivityName(name) {
1749
1869
  'edit',
1750
1870
  'write',
1751
1871
  'write_file',
1752
- 'patch',
1753
1872
  'replace_text',
1754
1873
  'replace_block',
1755
1874
  'insert_before',
1756
1875
  'insert_after',
1757
- 'validate_edit',
1758
- 'generate_diff'
1876
+ 'validate_edit'
1759
1877
  ]).has(parsed.base);
1760
1878
  }
1761
1879
 
@@ -1829,6 +1947,10 @@ export function normalizeActivitySpacingRows(inputRows) {
1829
1947
  }
1830
1948
  }
1831
1949
 
1950
+ if (isTodoRow(row) && !isTodoRow(next) && next && !isBlankTextRow(next) && next.kind !== 'status') {
1951
+ normalized.push({ kind: 'todo-gap' });
1952
+ }
1953
+
1832
1954
  if (isBlankTextRow(row) && isBlankTextRow(prev)) {
1833
1955
  normalized.pop();
1834
1956
  }
@@ -1964,13 +2086,7 @@ export function buildMessageRows(msg, showToolDetails, contentWidth = 72, copy)
1964
2086
  const trimmed = line.trim();
1965
2087
  const planProgress = parsePlanProgressLine(trimmed);
1966
2088
  if (planProgress) {
1967
- rows.push({
1968
- kind: 'plan-progress',
1969
- current: planProgress.current,
1970
- total: planProgress.total,
1971
- role: planProgress.role,
1972
- title: trimText(planProgress.title, Math.max(12, contentWidth - 18))
1973
- });
2089
+ // Skip rendering plan progress lines inline — shown in header badge instead
1974
2090
  continue;
1975
2091
  }
1976
2092
  if (trimmed.startsWith('```')) {
@@ -2146,6 +2262,9 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2146
2262
  h(Text, { color, dimColor }, row.text || row.activeForm || ' ')
2147
2263
  );
2148
2264
  }
2265
+ if (row.kind === 'todo-gap') {
2266
+ return h(Box, { key: `row-todo-gap-${msg.id}-${idx}`, marginTop: 1 }, h(Text, { color: 'gray' }, ' '));
2267
+ }
2149
2268
  if (row.kind === 'table') {
2150
2269
  return h(
2151
2270
  Box,
@@ -2190,16 +2309,8 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
2190
2309
  );
2191
2310
  }
2192
2311
  if (row.kind === 'plan-progress') {
2193
- return h(
2194
- Box,
2195
- { key: `row-plan-progress-${msg.id}-${idx}`, marginTop: 1, marginBottom: 1 },
2196
- h(Text, { color: 'cyanBright' }, '[plan] '),
2197
- h(Text, { color: 'yellowBright' }, `Step ${row.current}/${row.total}`),
2198
- h(Text, { color: 'gray' }, ' -> '),
2199
- h(Text, { color: 'magentaBright' }, String(row.role || 'agent').toUpperCase()),
2200
- h(Text, { color: 'gray' }, ': '),
2201
- h(Text, { color: 'white' }, row.title)
2202
- );
2312
+ // Already shown in header badge — skip inline rendering
2313
+ return null;
2203
2314
  }
2204
2315
  if (row.kind === 'status') {
2205
2316
  const dots = '.'.repeat((loaderTick % 3) + 1);
@@ -2348,11 +2459,7 @@ export function moveSuggestionSelection(currentIndex, itemCount, direction, page
2348
2459
 
2349
2460
  function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
2350
2461
  if (msg?.planStrip) {
2351
- return h(
2352
- Box,
2353
- { marginBottom: 1 },
2354
- h(PlanStrip, { planState: msg.planState, copy })
2355
- );
2462
+ return h(PlanStrip, { planState: msg.planState, copy });
2356
2463
  }
2357
2464
  if (msg?.planSummary || parseAutoPlanSummaryMessage(msg?.text)) {
2358
2465
  return h(PlanSummaryBubble, { msg, copy });
@@ -2385,7 +2492,8 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
2385
2492
  h(
2386
2493
  Box,
2387
2494
  null,
2388
- 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
2389
2497
  ),
2390
2498
  autoSkillBadge
2391
2499
  ? h(Text, { color: 'blueBright' }, autoSkillBadge)
@@ -2536,6 +2644,8 @@ function InputBar({
2536
2644
  afterCursor,
2537
2645
  cursorVisible,
2538
2646
  busy,
2647
+ disabled = false,
2648
+ disabledText = '',
2539
2649
  inputStage,
2540
2650
  pendingQueueLength,
2541
2651
  showToolDetails,
@@ -2577,20 +2687,60 @@ function InputBar({
2577
2687
  Box,
2578
2688
  null,
2579
2689
  h(Text, { color: 'cyan' }, 'codemini> '),
2580
- h(Text, { color: 'white' }, beforeCursor),
2581
- h(
2582
- Text,
2583
- {
2584
- color: cursorVisible ? 'black' : 'white',
2585
- backgroundColor: cursorVisible ? 'cyanBright' : undefined
2586
- },
2587
- underCursor || ' '
2588
- ),
2589
- 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
+ ]
2590
2705
  )
2591
2706
  );
2592
2707
  }
2593
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
+
2594
2744
  function SignatureBar({ version = '' }) {
2595
2745
  return h(
2596
2746
  Box,
@@ -2672,6 +2822,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2672
2822
  const [debugKeys, setDebugKeys] = useState(false);
2673
2823
  const [lastKeyDebug, setLastKeyDebug] = useState('');
2674
2824
  const [showToolDetails, setShowToolDetails] = useState(false);
2825
+ const [pendingDeleteApproval, setPendingDeleteApproval] = useState(null);
2826
+ const [deleteApprovalInput, setDeleteApprovalInput] = useState('');
2827
+ const [deleteApprovalError, setDeleteApprovalError] = useState('');
2675
2828
  const activeAssistantIdRef = useRef(null);
2676
2829
  const activeAssistantAutoSkillNamesRef = useRef([]);
2677
2830
  const streamedAssistantHandledRef = useRef(false);
@@ -2681,6 +2834,11 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2681
2834
  const messagesRef = useRef([]);
2682
2835
  const pendingQueueRef = useRef([]);
2683
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);
2684
2842
 
2685
2843
  useEffect(() => {
2686
2844
  const rawStartupActivities = runtime.consumeStartupEvents?.();
@@ -2703,6 +2861,26 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2703
2861
  const planTextBufferRef = useRef('');
2704
2862
  const { exit } = useApp();
2705
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
+
2706
2884
  useEffect(() => {
2707
2885
  messagesRef.current = messages;
2708
2886
  }, [messages]);
@@ -2739,7 +2917,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2739
2917
  : [];
2740
2918
  const hasTransientPanels =
2741
2919
  commandSuggestions.length > 0 || pendingQueue.length > 0 || debugKeys || Boolean(planState?.total);
2742
- const messageContentWidth = Math.max(24, stdoutCols - 18);
2920
+ const messageContentWidth = Math.max(24, stdoutCols - 8);
2743
2921
 
2744
2922
  const syncRuntimeVisualState = (variant = 'ready') => {
2745
2923
  const snapshot = runtime.getRuntimeState?.();
@@ -2747,10 +2925,35 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2747
2925
  setDisplaySessionId(snapshot.sessionId || sessionId);
2748
2926
  setDisplayModel(snapshot.model || model);
2749
2927
  setDisplaySdkProvider(snapshot.sdkProvider || sdkProvider);
2750
- 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
+ });
2751
2937
  setRuntimeStatus(makeIdleStatus(copy, snapshot, variant));
2752
2938
  };
2753
2939
 
2940
+ const refreshRuntimeSnapshot = () => {
2941
+ const snapshot = runtime.getRuntimeState?.();
2942
+ if (!snapshot) return;
2943
+ setDisplaySessionId(snapshot.sessionId || sessionId);
2944
+ setDisplayModel(snapshot.model || model);
2945
+ setDisplaySdkProvider(snapshot.sdkProvider || sdkProvider);
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
+ });
2955
+ };
2956
+
2754
2957
  useEffect(() => {
2755
2958
  syncRuntimeVisualState('ready');
2756
2959
  }, []);
@@ -2767,15 +2970,34 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2767
2970
  if (!last) return;
2768
2971
  const current = Number(last[1]);
2769
2972
  const total = Number(last[2]);
2770
- 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';
2771
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;
2772
2991
  setActiveAssistantMeta({
2773
- planStep: `${current}/${total} · ${role}: ${title}`
2992
+ label: normalizedRole,
2993
+ planStepInfo: { current, total },
2994
+ planStep: `${current}/${total} · ${title}`
2774
2995
  });
2775
2996
  setPlanState((prev) => {
2776
- const steps = (prev.steps || [])
2777
- .map((step) => (step.index === current ? { ...step, status: 'done' } : step))
2997
+ let steps = (prev.steps || [])
2998
+ .map((step) => (step.status === 'active' ? { ...step, status: 'done' } : step))
2778
2999
  .filter((step, idx, arr) => arr.findIndex((x) => x.index === step.index) === idx);
3000
+
2779
3001
  const withoutCurrent = steps.filter((step) => step.index !== current);
2780
3002
  return {
2781
3003
  current,
@@ -2966,13 +3188,16 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
2966
3188
  }
2967
3189
  const parsedPlanSummary = result.type === 'system' ? parseAutoPlanSummaryMessage(result.text || '') : null;
2968
3190
  if (parsedPlanSummary?.approval === 'pending') {
3191
+ const preSteps = Array.isArray(parsedPlanSummary.planSteps) && parsedPlanSummary.planSteps.length > 0
3192
+ ? parsedPlanSummary.planSteps
3193
+ : [];
2969
3194
  setPlanState({
2970
3195
  current: 0,
2971
- total: 0,
3196
+ total: preSteps.length,
2972
3197
  role: '',
2973
3198
  title: '',
2974
3199
  failed: false,
2975
- steps: [],
3200
+ steps: preSteps,
2976
3201
  pendingApproval: true,
2977
3202
  completed: false,
2978
3203
  resultStatus: '',
@@ -3008,7 +3233,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3008
3233
  const nextCall = {
3009
3234
  id: toolCall.id || '',
3010
3235
  name: toolCall.name || '',
3011
- 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,
3012
3239
  status: 'pending',
3013
3240
  type: 'tool'
3014
3241
  };
@@ -3021,11 +3248,13 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3021
3248
  };
3022
3249
 
3023
3250
  const finalizeActiveAssistant = () => {
3251
+ activePlanStepRoleRef.current = null;
3252
+ activePlanStepInfoRef.current = null;
3253
+ activePlanStepTitleRef.current = '';
3024
3254
  setActiveAssistantMeta({
3025
3255
  loading: false,
3026
3256
  phase: undefined,
3027
3257
  liveStatus: undefined,
3028
- planStep: undefined,
3029
3258
  pendingToolCalls: [],
3030
3259
  codeGenerationEndedAt: undefined,
3031
3260
  autoSkillNames: activeAssistantAutoSkillNamesRef.current
@@ -3061,19 +3290,27 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3061
3290
  if (activeAssistantIdRef.current) return activeAssistantIdRef.current;
3062
3291
  const aid = nextId();
3063
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;
3064
3299
  setMessages((prev) => [
3065
3300
  ...prev,
3066
3301
  {
3067
3302
  id: aid,
3068
- label: 'coder',
3303
+ label,
3069
3304
  text: '',
3070
- color: 'greenBright',
3305
+ color: style.text,
3071
3306
  toolCalls: [],
3072
3307
  segments: [],
3073
3308
  loading: true,
3074
3309
  phase: 'thinking',
3075
3310
  liveStatus: copy.runtime.modelThinking,
3076
- autoSkillNames: activeAssistantAutoSkillNamesRef.current
3311
+ autoSkillNames: activeAssistantAutoSkillNamesRef.current,
3312
+ ...(planStepInfo ? { planStepInfo } : {}),
3313
+ ...(planStepDisplay ? { planStep: planStepDisplay } : {})
3077
3314
  }
3078
3315
  ]);
3079
3316
  return aid;
@@ -3119,6 +3356,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3119
3356
  resultNext: ''
3120
3357
  });
3121
3358
  planTextBufferRef.current = '';
3359
+ activePlanStepNumberRef.current = 0;
3122
3360
  activeAssistantIdRef.current = null;
3123
3361
  activeAssistantAutoSkillNamesRef.current = [];
3124
3362
  streamedAssistantHandledRef.current = false;
@@ -3126,6 +3364,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3126
3364
 
3127
3365
  runtime
3128
3366
  .submit(line, (event) => {
3367
+ if (shouldRefreshRuntimeStateForEvent(event)) {
3368
+ refreshRuntimeSnapshot();
3369
+ }
3129
3370
  if (event?.type === 'assistant:start') {
3130
3371
  streamedAssistantHandledRef.current = true;
3131
3372
  setRuntimeStatus(makeStatus(copy.runtime.modelThinking, copy.runtime.requestDelivered, 'cyanBright'));
@@ -3160,7 +3401,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3160
3401
  if (event?.type === 'assistant:tool_call_delta') {
3161
3402
  ensureActiveAssistant();
3162
3403
  const parsed = parseToolDisplayName(event.toolCall?.name);
3163
- const isCodeTool = new Set(['write', 'edit', 'patch', 'generate_diff']).has(parsed.base);
3404
+ const isCodeTool = new Set(['write', 'edit']).has(parsed.base);
3164
3405
  if (isCodeTool) {
3165
3406
  setRuntimeStatus(makeStatus(copy.runtime.generatingCode, copy.runtime.streamingReply, 'greenBright'));
3166
3407
  setInputStage('streaming');
@@ -3219,7 +3460,6 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3219
3460
  loading: false,
3220
3461
  phase: undefined,
3221
3462
  liveStatus: undefined,
3222
- planStep: undefined,
3223
3463
  pendingToolCalls: [],
3224
3464
  autoSkillNames: activeAssistantAutoSkillNamesRef.current,
3225
3465
  ...(m.codeGenerationStartedAt && !m.codeGenerationEndedAt ? { codeGenerationEndedAt: Date.now() } : {})
@@ -3227,7 +3467,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3227
3467
  })
3228
3468
  );
3229
3469
  }
3230
- if (!hasPlannedTools) {
3470
+ if (!hasPlannedTools && !activePlanStepInfoRef.current) {
3231
3471
  activeAssistantIdRef.current = null;
3232
3472
  }
3233
3473
  if (!hadActiveAssistant && !hasPlannedTools && event.text) {
@@ -3380,6 +3620,22 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3380
3620
  liveStatus: copy.toolActivity.waitingModelAdjust(detail)
3381
3621
  });
3382
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
+ }
3383
3639
  if (event?.type === 'skill:start') {
3384
3640
  ensureActiveAssistant();
3385
3641
  const detail = describeSkillActivity(event.name, copy);
@@ -3459,16 +3715,28 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3459
3715
  return;
3460
3716
  }
3461
3717
  if (result.type !== 'noop') setInputStage('idle');
3462
- if (planTextBufferRef.current && planState.total > 0) {
3463
- setPlanState((prev) => ({
3464
- ...prev,
3465
- steps: (prev.steps || []).map((step) =>
3466
- step.index === prev.current && step.status === 'active' ? { ...step, status: prev.failed ? 'failed' : 'done' } : step
3467
- )
3468
- }));
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
+ });
3469
3729
  }
3470
3730
  syncRuntimeVisualState(result.type === 'noop' ? 'ready' : 'after');
3471
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
+ }
3472
3740
  if (!shouldAppendAssistantResult(result, activeAssistantIdRef.current, streamedAssistantHandledRef.current)) return;
3473
3741
  appendResultMessage(result);
3474
3742
  })
@@ -3485,7 +3753,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3485
3753
  ...prev,
3486
3754
  failed: prev.total > 0,
3487
3755
  steps: (prev.steps || []).map((step) =>
3488
- step.index === prev.current ? { ...step, status: 'failed' } : step
3756
+ step.status === 'active' ? { ...step, status: 'failed' } : step
3489
3757
  )
3490
3758
  }));
3491
3759
  setMessages((prev) => [
@@ -3497,6 +3765,7 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3497
3765
  flushAssistantDelta();
3498
3766
  finalizeActiveAssistant();
3499
3767
  activeAssistantIdRef.current = null;
3768
+ activePlanStepNumberRef.current = 0;
3500
3769
  streamedAssistantHandledRef.current = false;
3501
3770
  activeUserMessageIdRef.current = null;
3502
3771
  if (deltaFlushTimerRef.current) {
@@ -3552,6 +3821,19 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3552
3821
  appendResultMessage(result);
3553
3822
  })
3554
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
+ }
3555
3837
  const message = sanitizeRenderableText(err?.message || String(err));
3556
3838
  updateMessageMeta(userMessageId, {
3557
3839
  loading: false,
@@ -3599,6 +3881,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3599
3881
  escSeqRef.current = '';
3600
3882
  }
3601
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
+
3602
3924
  if (key.upArrow) {
3603
3925
  if (suggestionNav && commandSuggestions.length > 0) {
3604
3926
  setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'up'));
@@ -3703,6 +4025,16 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3703
4025
  setCursorIndex(0);
3704
4026
  if (!line) return;
3705
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
+
3706
4038
  setHistory((prev) => [...prev, line]);
3707
4039
  setHistoryIndex(null);
3708
4040
  setDraftBeforeHistory('');
@@ -3772,6 +4104,10 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3772
4104
  }
3773
4105
 
3774
4106
  if (key.ctrl && value === 'c') {
4107
+ if (busy && typeof runtime.abort === 'function') {
4108
+ runtime.abort();
4109
+ return;
4110
+ }
3775
4111
  exit();
3776
4112
  return;
3777
4113
  }
@@ -3911,6 +4247,12 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3911
4247
  ),
3912
4248
  h(SuggestionPanel, { commandSuggestions, suggestionNav, menuIndex, copy }),
3913
4249
  h(PendingPanel, { pendingQueue, copy }),
4250
+ h(DeleteApprovalPanel, {
4251
+ request: pendingDeleteApproval,
4252
+ inputValue: deleteApprovalInput,
4253
+ errorText: deleteApprovalError,
4254
+ copy
4255
+ }),
3914
4256
  debugKeys
3915
4257
  ? h(
3916
4258
  Box,
@@ -3924,6 +4266,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
3924
4266
  afterCursor,
3925
4267
  cursorVisible,
3926
4268
  busy,
4269
+ disabled: Boolean(pendingDeleteApproval),
4270
+ disabledText: pendingDeleteApproval ? copy.deleteApproval.inputLocked : '',
3927
4271
  inputStage,
3928
4272
  pendingQueueLength: pendingQueue.length,
3929
4273
  showToolDetails,