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.
- package/README.md +20 -18
- package/package.json +6 -6
- package/souls/anime.md +5 -2
- package/src/cli.js +3 -1
- package/src/commands/run.js +229 -16
- package/src/core/agent-loop.js +159 -47
- package/src/core/ast.js +40 -0
- package/src/core/chat-runtime.js +712 -126
- package/src/core/command-policy.js +56 -0
- package/src/core/config-store.js +0 -3
- package/src/core/crypto-utils.js +6 -2
- package/src/core/memory-store.js +3 -3
- package/src/core/project-index.js +4 -18
- package/src/core/provider/anthropic.js +15 -2
- package/src/core/provider/openai-compatible.js +15 -2
- package/src/core/session-store.js +82 -25
- package/src/core/shell-profile.js +17 -1
- package/src/core/string-utils.js +37 -0
- package/src/core/tools.js +152 -393
- package/src/tui/chat-app.js +461 -147
- package/src/tui/tool-activity/presenters/files.js +2 -2
- package/src/tui/tool-narration.js +0 -3
- package/src/tui/tool-narration/presenters/patch.js +0 -3
package/src/tui/chat-app.js
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
|
1076
|
-
const
|
|
1077
|
-
const
|
|
1078
|
-
const
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
1107
|
-
h(Text, { color: 'black', backgroundColor:
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
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
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
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'
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
2212
|
-
|
|
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
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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 -
|
|
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(
|
|
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(
|
|
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
|
-
|
|
2992
|
+
label: normalizedRole,
|
|
2993
|
+
planStepInfo: { current, total },
|
|
2994
|
+
planStep: `${current}/${total} · ${title}`
|
|
2801
2995
|
});
|
|
2802
2996
|
setPlanState((prev) => {
|
|
2803
|
-
|
|
2804
|
-
.map((step) => (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:
|
|
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'
|
|
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
|
|
3303
|
+
label,
|
|
3096
3304
|
text: '',
|
|
3097
|
-
color:
|
|
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'
|
|
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
|
|
3493
|
-
setPlanState((prev) =>
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
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.
|
|
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,
|