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