codemini-cli 0.3.7 → 0.3.8
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/OPERATIONS.md +18 -0
- package/README.md +194 -151
- package/package.json +1 -1
- package/src/commands/chat.js +24 -0
- package/src/commands/run.js +4 -4
- package/src/core/agent-loop.js +11 -1
- package/src/core/chat-runtime.js +239 -79
- package/src/core/plan-state.js +32 -0
- package/src/core/session-store.js +3 -19
- package/src/core/shell-profile.js +1 -0
- package/src/core/tools.js +158 -1
- package/src/tui/chat-app.js +265 -19
package/src/core/tools.js
CHANGED
|
@@ -19,6 +19,7 @@ import { checkReadDedup } from './agent-loop.js';
|
|
|
19
19
|
import { TOOL_SKIP_DIRS as SKIP_DIRS, TEXT_EXTENSIONS, CODE_WRITE_GUARD_EXTENSIONS, LANGUAGE_FILE_TYPES } from './constants.js';
|
|
20
20
|
import { sha256Prefixed as sha256, sha256 as sha256Hash } from './crypto-utils.js';
|
|
21
21
|
import { forgetMemory, listMemories, rememberMemory, searchMemories } from './memory-store.js';
|
|
22
|
+
import { normalizePlanState } from './plan-state.js';
|
|
22
23
|
import { normalizeTodos } from './todo-state.js';
|
|
23
24
|
const BACKGROUND_TASK_RECENT_OUTPUT_LIMIT = 80;
|
|
24
25
|
const BACKGROUND_TASK_POLL_MS = 150;
|
|
@@ -1534,7 +1535,7 @@ async function editTarget(root, args) {
|
|
|
1534
1535
|
throw new Error(`edit does not support kind: ${kind}`);
|
|
1535
1536
|
}
|
|
1536
1537
|
|
|
1537
|
-
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate }) {
|
|
1538
|
+
export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSystemEvent, getTodos, onTodosUpdate, getPlanState, onPlanStateUpdate }) {
|
|
1538
1539
|
const emitSystemTool = (event) => {
|
|
1539
1540
|
if (typeof onSystemEvent === 'function' && event) onSystemEvent(event);
|
|
1540
1541
|
};
|
|
@@ -1791,6 +1792,76 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
1791
1792
|
}
|
|
1792
1793
|
}
|
|
1793
1794
|
},
|
|
1795
|
+
{
|
|
1796
|
+
type: 'function',
|
|
1797
|
+
function: {
|
|
1798
|
+
name: 'read_plan',
|
|
1799
|
+
description:
|
|
1800
|
+
'Read the structured plan state for the current session. Use this to recover plan progress after transient model/tool errors before continuing implementation.',
|
|
1801
|
+
parameters: {
|
|
1802
|
+
type: 'object',
|
|
1803
|
+
properties: {
|
|
1804
|
+
include_steps: { type: 'boolean', description: 'Include normalized plan steps in the output (default: true)' }
|
|
1805
|
+
},
|
|
1806
|
+
required: []
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
type: 'function',
|
|
1812
|
+
function: {
|
|
1813
|
+
name: 'update_plan',
|
|
1814
|
+
description:
|
|
1815
|
+
'Create, replace, or clear the structured plan state for the current session. Use clear=true to remove plan state.',
|
|
1816
|
+
parameters: {
|
|
1817
|
+
type: 'object',
|
|
1818
|
+
properties: {
|
|
1819
|
+
clear: { type: 'boolean', description: 'Set true to clear current plan state' },
|
|
1820
|
+
plan: {
|
|
1821
|
+
type: 'object',
|
|
1822
|
+
properties: {
|
|
1823
|
+
status: { type: 'string', description: 'Plan lifecycle status (for example pending_approval, approved, completed, failed)' },
|
|
1824
|
+
source: { type: 'string', description: 'Plan source such as auto/manual/tool' },
|
|
1825
|
+
goal: { type: 'string', description: 'Original user goal for this plan' },
|
|
1826
|
+
filePath: { type: 'string', description: 'Plan markdown file path' },
|
|
1827
|
+
summary: { type: 'string', description: 'Short plan summary' },
|
|
1828
|
+
finalSummary: { type: 'string', description: 'Final planning summary shown for approval' },
|
|
1829
|
+
steps: {
|
|
1830
|
+
type: 'array',
|
|
1831
|
+
items: {
|
|
1832
|
+
type: 'object',
|
|
1833
|
+
properties: {
|
|
1834
|
+
title: { type: 'string' },
|
|
1835
|
+
role: { type: 'string' },
|
|
1836
|
+
task: { type: 'string' }
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
},
|
|
1842
|
+
status: { type: 'string', description: 'Top-level alias for plan.status when plan is omitted' },
|
|
1843
|
+
source: { type: 'string', description: 'Top-level alias for plan.source when plan is omitted' },
|
|
1844
|
+
goal: { type: 'string', description: 'Top-level alias for plan.goal when plan is omitted' },
|
|
1845
|
+
filePath: { type: 'string', description: 'Top-level alias for plan.filePath when plan is omitted' },
|
|
1846
|
+
summary: { type: 'string', description: 'Top-level alias for plan.summary when plan is omitted' },
|
|
1847
|
+
finalSummary: { type: 'string', description: 'Top-level alias for plan.finalSummary when plan is omitted' },
|
|
1848
|
+
steps: {
|
|
1849
|
+
type: 'array',
|
|
1850
|
+
description: 'Top-level alias for plan.steps when plan is omitted',
|
|
1851
|
+
items: {
|
|
1852
|
+
type: 'object',
|
|
1853
|
+
properties: {
|
|
1854
|
+
title: { type: 'string' },
|
|
1855
|
+
role: { type: 'string' },
|
|
1856
|
+
task: { type: 'string' }
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
},
|
|
1861
|
+
required: []
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
},
|
|
1794
1865
|
{
|
|
1795
1866
|
type: 'function',
|
|
1796
1867
|
function: {
|
|
@@ -2164,6 +2235,42 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2164
2235
|
newTodos: nextTodos
|
|
2165
2236
|
};
|
|
2166
2237
|
},
|
|
2238
|
+
read_plan: async (args = {}) => {
|
|
2239
|
+
const includeSteps = args?.include_steps !== false;
|
|
2240
|
+
const currentPlan = normalizePlanState(typeof getPlanState === 'function' ? getPlanState() : null);
|
|
2241
|
+
if (!includeSteps && currentPlan && Array.isArray(currentPlan.steps)) {
|
|
2242
|
+
const { steps, ...rest } = currentPlan;
|
|
2243
|
+
return {
|
|
2244
|
+
ok: true,
|
|
2245
|
+
plan: rest,
|
|
2246
|
+
hasPendingApproval: rest.status === 'pending_approval'
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
return {
|
|
2250
|
+
ok: true,
|
|
2251
|
+
plan: currentPlan,
|
|
2252
|
+
hasPendingApproval: currentPlan?.status === 'pending_approval'
|
|
2253
|
+
};
|
|
2254
|
+
},
|
|
2255
|
+
update_plan: async (args = {}) => {
|
|
2256
|
+
const oldPlan = normalizePlanState(typeof getPlanState === 'function' ? getPlanState() : null);
|
|
2257
|
+
const shouldClear = args?.clear === true || args?.plan === null;
|
|
2258
|
+
const nextRaw = shouldClear
|
|
2259
|
+
? null
|
|
2260
|
+
: args?.plan && typeof args.plan === 'object'
|
|
2261
|
+
? args.plan
|
|
2262
|
+
: args;
|
|
2263
|
+
const nextPlan = normalizePlanState(nextRaw);
|
|
2264
|
+
if (typeof onPlanStateUpdate === 'function') {
|
|
2265
|
+
onPlanStateUpdate(nextPlan);
|
|
2266
|
+
}
|
|
2267
|
+
return {
|
|
2268
|
+
ok: true,
|
|
2269
|
+
oldPlan,
|
|
2270
|
+
newPlan: nextPlan,
|
|
2271
|
+
hasPendingApproval: nextPlan?.status === 'pending_approval'
|
|
2272
|
+
};
|
|
2273
|
+
},
|
|
2167
2274
|
run: (args) => runCommand(workspaceRoot, config, args),
|
|
2168
2275
|
remember_user: async (args = {}) => {
|
|
2169
2276
|
const saved = await rememberMemory({
|
|
@@ -2312,6 +2419,56 @@ export function getBuiltinTools({ workspaceRoot = process.cwd(), config, onSyste
|
|
|
2312
2419
|
return ['Updated todo list:', ...lines].join('\n');
|
|
2313
2420
|
},
|
|
2314
2421
|
|
|
2422
|
+
read_plan(result) {
|
|
2423
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2424
|
+
const plan = normalizePlanState(result.plan);
|
|
2425
|
+
if (!plan) return 'No active plan state.';
|
|
2426
|
+
const lines = [
|
|
2427
|
+
'Current plan state:',
|
|
2428
|
+
`- status: ${plan.status || '-'}`,
|
|
2429
|
+
`- source: ${plan.source || '-'}`,
|
|
2430
|
+
`- goal: ${plan.goal || '-'}`,
|
|
2431
|
+
`- filePath: ${plan.filePath || '-'}`,
|
|
2432
|
+
`- summary: ${plan.summary || '-'}`,
|
|
2433
|
+
`- finalSummary: ${plan.finalSummary || '-'}`
|
|
2434
|
+
];
|
|
2435
|
+
const steps = Array.isArray(plan.steps) ? plan.steps : [];
|
|
2436
|
+
if (steps.length > 0) {
|
|
2437
|
+
lines.push('- steps:');
|
|
2438
|
+
for (let i = 0; i < Math.min(steps.length, 8); i += 1) {
|
|
2439
|
+
const step = steps[i];
|
|
2440
|
+
lines.push(` ${i + 1}. [${step.role || '-'}] ${step.title || '-'} :: ${step.task || '-'}`);
|
|
2441
|
+
}
|
|
2442
|
+
if (steps.length > 8) lines.push(` ... and ${steps.length - 8} more step(s)`);
|
|
2443
|
+
}
|
|
2444
|
+
return lines.join('\n');
|
|
2445
|
+
},
|
|
2446
|
+
|
|
2447
|
+
update_plan(result) {
|
|
2448
|
+
if (!result || typeof result !== 'object') return String(result);
|
|
2449
|
+
const nextPlan = normalizePlanState(result.newPlan);
|
|
2450
|
+
if (!nextPlan) return 'Plan state cleared.';
|
|
2451
|
+
const lines = [
|
|
2452
|
+
'Current plan state:',
|
|
2453
|
+
`- status: ${nextPlan.status || '-'}`,
|
|
2454
|
+
`- source: ${nextPlan.source || '-'}`,
|
|
2455
|
+
`- goal: ${nextPlan.goal || '-'}`,
|
|
2456
|
+
`- filePath: ${nextPlan.filePath || '-'}`,
|
|
2457
|
+
`- summary: ${nextPlan.summary || '-'}`,
|
|
2458
|
+
`- finalSummary: ${nextPlan.finalSummary || '-'}`
|
|
2459
|
+
];
|
|
2460
|
+
const steps = Array.isArray(nextPlan.steps) ? nextPlan.steps : [];
|
|
2461
|
+
if (steps.length > 0) {
|
|
2462
|
+
lines.push('- steps:');
|
|
2463
|
+
for (let i = 0; i < Math.min(steps.length, 8); i += 1) {
|
|
2464
|
+
const step = steps[i];
|
|
2465
|
+
lines.push(` ${i + 1}. [${step.role || '-'}] ${step.title || '-'} :: ${step.task || '-'}`);
|
|
2466
|
+
}
|
|
2467
|
+
if (steps.length > 8) lines.push(` ... and ${steps.length - 8} more step(s)`);
|
|
2468
|
+
}
|
|
2469
|
+
return lines.join('\n');
|
|
2470
|
+
},
|
|
2471
|
+
|
|
2315
2472
|
query_project_index(result) {
|
|
2316
2473
|
if (!result || typeof result !== 'object') return String(result);
|
|
2317
2474
|
const lines = [];
|
package/src/tui/chat-app.js
CHANGED
|
@@ -27,6 +27,7 @@ const BANNER = [
|
|
|
27
27
|
' ██████ ██████ ██████ ███████ ██ ██ ██ ██ ████ ██ '
|
|
28
28
|
];
|
|
29
29
|
const BANNER_COLORS = ['magentaBright', 'redBright', 'yellowBright', 'cyanBright', 'magentaBright'];
|
|
30
|
+
const SPINNER_FRAMES = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
30
31
|
const ROLE_STYLES = {
|
|
31
32
|
you: {
|
|
32
33
|
accent: 'blueBright',
|
|
@@ -258,6 +259,18 @@ const TUI_COPY = {
|
|
|
258
259
|
inputLocked: '删除确认进行中,请输入 yes 或 no',
|
|
259
260
|
answerLabel: '确认输入(yes/no)',
|
|
260
261
|
answerPlaceholder: 'yes 或 no'
|
|
262
|
+
},
|
|
263
|
+
planApproval: {
|
|
264
|
+
title: '确认执行计划?',
|
|
265
|
+
goalLabel: '目标',
|
|
266
|
+
summaryLabel: '摘要',
|
|
267
|
+
fileLabel: '文件',
|
|
268
|
+
prompt: '输入 /yes 执行,输入 /edit <反馈> 修改,输入 /reject 拒绝。',
|
|
269
|
+
invalidAnswer: '请输入 /yes、/edit <反馈> 或 /reject。',
|
|
270
|
+
missingFeedback: '请在 /edit 后提供反馈内容。',
|
|
271
|
+
inputLocked: '计划审批进行中,请在审批框输入 /yes、/edit 或 /reject',
|
|
272
|
+
answerLabel: '审批输入',
|
|
273
|
+
answerPlaceholder: '/yes | /edit <反馈> | /reject'
|
|
261
274
|
}
|
|
262
275
|
},
|
|
263
276
|
en: {
|
|
@@ -415,6 +428,18 @@ const TUI_COPY = {
|
|
|
415
428
|
inputLocked: 'Delete approval is active; type yes or no',
|
|
416
429
|
answerLabel: 'Approval input (yes/no)',
|
|
417
430
|
answerPlaceholder: 'yes or no'
|
|
431
|
+
},
|
|
432
|
+
planApproval: {
|
|
433
|
+
title: 'Approve this plan?',
|
|
434
|
+
goalLabel: 'Goal',
|
|
435
|
+
summaryLabel: 'Summary',
|
|
436
|
+
fileLabel: 'File',
|
|
437
|
+
prompt: 'Type /yes to execute, /edit <feedback> to revise, or /reject to discard.',
|
|
438
|
+
invalidAnswer: 'Please enter /yes, /edit <feedback>, or /reject.',
|
|
439
|
+
missingFeedback: 'Please provide feedback after /edit.',
|
|
440
|
+
inputLocked: 'Plan approval is active; type /yes, /edit <feedback>, or /reject',
|
|
441
|
+
answerLabel: 'Approval input',
|
|
442
|
+
answerPlaceholder: '/yes | /edit <feedback> | /reject'
|
|
418
443
|
}
|
|
419
444
|
}
|
|
420
445
|
};
|
|
@@ -960,6 +985,38 @@ export function parseDeleteApprovalAnswer(value) {
|
|
|
960
985
|
return normalized ? 'invalid' : 'empty';
|
|
961
986
|
}
|
|
962
987
|
|
|
988
|
+
export function parsePlanApprovalAnswer(value) {
|
|
989
|
+
const raw = String(value || '').trim();
|
|
990
|
+
if (!raw) return { action: 'empty', command: '' };
|
|
991
|
+
const normalized = raw.toLowerCase();
|
|
992
|
+
if (normalized === '/yes' || normalized === 'yes') {
|
|
993
|
+
return { action: 'approve', command: '/yes' };
|
|
994
|
+
}
|
|
995
|
+
if (normalized === '/reject' || normalized === 'reject' || normalized === 'no') {
|
|
996
|
+
return { action: 'reject', command: '/reject' };
|
|
997
|
+
}
|
|
998
|
+
const editMatch = raw.match(/^\/?edit(?:\s+(.+))?$/i);
|
|
999
|
+
if (editMatch) {
|
|
1000
|
+
const feedback = String(editMatch[1] || '').trim();
|
|
1001
|
+
if (!feedback) return { action: 'missing_feedback', command: '' };
|
|
1002
|
+
return { action: 'edit', feedback, command: `/edit ${feedback}` };
|
|
1003
|
+
}
|
|
1004
|
+
return { action: 'invalid', command: '' };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
export function parsePendingPlanApprovalMessage(text = '') {
|
|
1008
|
+
const raw = String(text || '');
|
|
1009
|
+
if (!/^Plan approval is still pending\./i.test(raw.trim())) return null;
|
|
1010
|
+
const lines = raw.split(/\r?\n/);
|
|
1011
|
+
const out = { goal: '', summary: '', filePath: '' };
|
|
1012
|
+
for (const line of lines) {
|
|
1013
|
+
if (line.startsWith('Goal: ')) out.goal = line.slice('Goal: '.length).trim();
|
|
1014
|
+
else if (line.startsWith('Summary: ')) out.summary = line.slice('Summary: '.length).trim();
|
|
1015
|
+
else if (line.startsWith('Plan File: ')) out.filePath = line.slice('Plan File: '.length).trim();
|
|
1016
|
+
}
|
|
1017
|
+
return out;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
963
1020
|
export function formatDeleteApprovalLines(copy, request) {
|
|
964
1021
|
const details = normalizeDeleteApprovalRequest(request);
|
|
965
1022
|
if (!details) return [];
|
|
@@ -1021,7 +1078,9 @@ function getActivityDisplayParts(activity) {
|
|
|
1021
1078
|
get_background_task: 'Task',
|
|
1022
1079
|
stop_background_task: 'Stop',
|
|
1023
1080
|
list_files: 'Glob',
|
|
1024
|
-
update_todos: 'Update Todos'
|
|
1081
|
+
update_todos: 'Update Todos',
|
|
1082
|
+
read_plan: 'Read Plan',
|
|
1083
|
+
update_plan: 'Update Plan'
|
|
1025
1084
|
};
|
|
1026
1085
|
return {
|
|
1027
1086
|
primary: labels[parsed.base] || parsed.base || 'Tool',
|
|
@@ -1114,7 +1173,7 @@ function stageDescriptor(inputStage, busy, runtimeStatus, copy) {
|
|
|
1114
1173
|
|
|
1115
1174
|
function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
|
|
1116
1175
|
const status = normalizeRuntimeStatus(runtimeStatus, copy);
|
|
1117
|
-
const
|
|
1176
|
+
const spinnerChar = SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length];
|
|
1118
1177
|
return h(
|
|
1119
1178
|
Box,
|
|
1120
1179
|
{
|
|
@@ -1126,7 +1185,7 @@ function RuntimeStrip({ busy, runtimeStatus, loaderTick, copy }) {
|
|
|
1126
1185
|
},
|
|
1127
1186
|
h(Text, { color: busy ? 'greenBright' : 'gray' }, busy ? copy.generic.live : copy.generic.idle),
|
|
1128
1187
|
h(Text, { color: 'gray' }, ' '),
|
|
1129
|
-
h(Text, { color: busy ? 'cyanBright' : 'gray' },
|
|
1188
|
+
h(Text, { color: busy ? 'cyanBright' : 'gray' }, spinnerChar),
|
|
1130
1189
|
h(Text, { color: 'gray' }, ' '),
|
|
1131
1190
|
h(Text, { color: busy ? 'white' : 'gray' }, status.title || copy.generic.waitingForInput)
|
|
1132
1191
|
);
|
|
@@ -1168,7 +1227,7 @@ function ContextProgressMeter({ runtimeState, runtimeStatus, compact = false })
|
|
|
1168
1227
|
Box,
|
|
1169
1228
|
{ justifyContent: 'flex-end', alignItems: 'center' },
|
|
1170
1229
|
h(Text, { color: 'gray' }, '上下文 '),
|
|
1171
|
-
h(Text, { color:
|
|
1230
|
+
h(Text, { color: activeColor }, `${Math.round(pct)}% `),
|
|
1172
1231
|
h(
|
|
1173
1232
|
Box,
|
|
1174
1233
|
null,
|
|
@@ -1347,8 +1406,13 @@ export function parseAutoPlanSummaryMessage(text) {
|
|
|
1347
1406
|
index: Number(stepMatch[1]),
|
|
1348
1407
|
role: String(stepMatch[2] || '').trim().toLowerCase(),
|
|
1349
1408
|
title: String(stepMatch[3] || '').trim(),
|
|
1409
|
+
task: '',
|
|
1350
1410
|
status: 'pending'
|
|
1351
1411
|
});
|
|
1412
|
+
} else if (/^-\s*task\s*:\s*/i.test(line) && parsed.planSteps.length > 0) {
|
|
1413
|
+
parsed.planSteps[parsed.planSteps.length - 1].task = line.replace(/^-\s*task\s*:\s*/i, '').trim();
|
|
1414
|
+
} else if (/^Next:\s*/i.test(line)) {
|
|
1415
|
+
inPlanSteps = false;
|
|
1352
1416
|
} else {
|
|
1353
1417
|
inPlanSteps = false;
|
|
1354
1418
|
}
|
|
@@ -1708,6 +1772,7 @@ function PlanSummaryBubble({ msg, copy }) {
|
|
|
1708
1772
|
summary.failed ? `${labels.fail} ${summary.failed}` : ''
|
|
1709
1773
|
].filter(Boolean);
|
|
1710
1774
|
const shortFile = summary.filePath ? trimText(summary.filePath, 96) : '';
|
|
1775
|
+
const planSteps = Array.isArray(summary.planSteps) ? summary.planSteps : [];
|
|
1711
1776
|
|
|
1712
1777
|
return h(
|
|
1713
1778
|
Box,
|
|
@@ -1746,11 +1811,27 @@ function PlanSummaryBubble({ msg, copy }) {
|
|
|
1746
1811
|
summary.planSummary
|
|
1747
1812
|
? h(
|
|
1748
1813
|
Box,
|
|
1749
|
-
{ marginBottom: summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
|
|
1814
|
+
{ marginBottom: planSteps.length > 0 || summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
|
|
1750
1815
|
h(Text, { color: 'cyanBright' }, labels.plan),
|
|
1751
1816
|
h(Text, { color: 'gray' }, summary.planSummary)
|
|
1752
1817
|
)
|
|
1753
1818
|
: null,
|
|
1819
|
+
planSteps.length > 0
|
|
1820
|
+
? h(
|
|
1821
|
+
Box,
|
|
1822
|
+
{ marginBottom: summary.approval || metaItems.length > 0 || summary.warningSteps || summary.failedSteps || shortFile ? 1 : 0, flexDirection: 'column' },
|
|
1823
|
+
h(Text, { color: 'cyanBright' }, labels.steps),
|
|
1824
|
+
...planSteps.flatMap((step, idx) => {
|
|
1825
|
+
const roleTag = String(step?.role || '').trim().toUpperCase() || 'CODER';
|
|
1826
|
+
const titleText = String(step?.title || '-').trim() || '-';
|
|
1827
|
+
const taskText = String(step?.task || '').trim();
|
|
1828
|
+
const titleRow = h(Text, { key: `plan-step-title-${idx}`, color: 'gray' }, `${idx + 1}. [${roleTag}] ${titleText}`);
|
|
1829
|
+
if (!taskText) return [titleRow];
|
|
1830
|
+
const taskRow = h(Text, { key: `plan-step-task-${idx}`, color: 'gray' }, ` - task: ${taskText}`);
|
|
1831
|
+
return [titleRow, taskRow];
|
|
1832
|
+
})
|
|
1833
|
+
)
|
|
1834
|
+
: null,
|
|
1754
1835
|
summary.approval
|
|
1755
1836
|
? h(
|
|
1756
1837
|
Box,
|
|
@@ -2233,7 +2314,7 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
|
|
|
2233
2314
|
Box,
|
|
2234
2315
|
{ key: `row-tool-${msg.id}-${idx}` },
|
|
2235
2316
|
h(Text, { color: 'gray' }, ' '),
|
|
2236
|
-
h(Text, { color: dotColor }, '●'),
|
|
2317
|
+
h(Text, { color: dotColor }, row.status === 'running' ? SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length] : '●'),
|
|
2237
2318
|
h(Text, { color: 'gray' }, ' '),
|
|
2238
2319
|
h(Text, { color: textColor }, display.primary),
|
|
2239
2320
|
h(Text, { color: 'gray' }, display.secondary),
|
|
@@ -2313,12 +2394,12 @@ export function renderMessageRow(msg, row, idx, loaderTick) {
|
|
|
2313
2394
|
return null;
|
|
2314
2395
|
}
|
|
2315
2396
|
if (row.kind === 'status') {
|
|
2316
|
-
const
|
|
2397
|
+
const spinnerChar = SPINNER_FRAMES[loaderTick % SPINNER_FRAMES.length];
|
|
2317
2398
|
return h(
|
|
2318
2399
|
Box,
|
|
2319
2400
|
{ key: `row-status-${msg.id}-${idx}`, marginTop: 1 },
|
|
2320
2401
|
h(Text, { color: 'gray' }, ' '),
|
|
2321
|
-
h(Text, { color: 'gray', dimColor: true }, `${row.text}${
|
|
2402
|
+
h(Text, { color: 'gray', dimColor: true }, `${row.text} ${spinnerChar}`)
|
|
2322
2403
|
);
|
|
2323
2404
|
}
|
|
2324
2405
|
if (row.kind === 'quote') {
|
|
@@ -2457,7 +2538,7 @@ export function moveSuggestionSelection(currentIndex, itemCount, direction, page
|
|
|
2457
2538
|
return safeIndex;
|
|
2458
2539
|
}
|
|
2459
2540
|
|
|
2460
|
-
function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
|
|
2541
|
+
const MessageBubble = React.memo(function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, contentWidth = 72, copy }) {
|
|
2461
2542
|
if (msg?.planStrip) {
|
|
2462
2543
|
return h(PlanStrip, { planState: msg.planState, copy });
|
|
2463
2544
|
}
|
|
@@ -2509,7 +2590,13 @@ function MessageBubble({ msg, loaderTick, showToolDetails, rowWindow = null, con
|
|
|
2509
2590
|
: null
|
|
2510
2591
|
)
|
|
2511
2592
|
);
|
|
2512
|
-
}
|
|
2593
|
+
}, (prev, next) => {
|
|
2594
|
+
if (prev.msg === next.msg &&
|
|
2595
|
+
prev.showToolDetails === next.showToolDetails &&
|
|
2596
|
+
prev.contentWidth === next.contentWidth &&
|
|
2597
|
+
prev.copy === next.copy) return true;
|
|
2598
|
+
return false;
|
|
2599
|
+
});
|
|
2513
2600
|
|
|
2514
2601
|
function MessageList({ messages, loaderTick, showToolDetails, contentWidth = 72, copy }) {
|
|
2515
2602
|
return h(
|
|
@@ -2524,7 +2611,7 @@ function MessageList({ messages, loaderTick, showToolDetails, contentWidth = 72,
|
|
|
2524
2611
|
h(MessageBubble, {
|
|
2525
2612
|
key: message.id,
|
|
2526
2613
|
msg: message,
|
|
2527
|
-
loaderTick,
|
|
2614
|
+
loaderTick: message.loading ? loaderTick : 0,
|
|
2528
2615
|
showToolDetails,
|
|
2529
2616
|
contentWidth,
|
|
2530
2617
|
copy
|
|
@@ -2706,7 +2793,32 @@ function InputBar({
|
|
|
2706
2793
|
);
|
|
2707
2794
|
}
|
|
2708
2795
|
|
|
2709
|
-
function
|
|
2796
|
+
function ApprovalCursorLine({ inputValue, placeholder, cursorVisible, accent }) {
|
|
2797
|
+
if (inputValue) {
|
|
2798
|
+
return h(
|
|
2799
|
+
Box,
|
|
2800
|
+
null,
|
|
2801
|
+
h(Text, { color: 'white' }, inputValue),
|
|
2802
|
+
h(
|
|
2803
|
+
Text,
|
|
2804
|
+
{ color: cursorVisible ? 'black' : accent, backgroundColor: cursorVisible ? accent : undefined },
|
|
2805
|
+
' '
|
|
2806
|
+
)
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
return h(
|
|
2810
|
+
Box,
|
|
2811
|
+
null,
|
|
2812
|
+
h(
|
|
2813
|
+
Text,
|
|
2814
|
+
{ color: cursorVisible ? 'black' : accent, backgroundColor: cursorVisible ? accent : undefined },
|
|
2815
|
+
' '
|
|
2816
|
+
),
|
|
2817
|
+
placeholder ? h(Text, { color: 'gray' }, placeholder) : null
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
function DeleteApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
|
|
2710
2822
|
if (!request) return null;
|
|
2711
2823
|
const details =
|
|
2712
2824
|
request?.toolName === 'delete'
|
|
@@ -2735,7 +2847,48 @@ function DeleteApprovalPanel({ request, inputValue, errorText, copy }) {
|
|
|
2735
2847
|
Box,
|
|
2736
2848
|
{ marginTop: 1 },
|
|
2737
2849
|
h(Text, { color: 'redBright' }, `${copy.deleteApproval.answerLabel}: `),
|
|
2738
|
-
h(
|
|
2850
|
+
h(ApprovalCursorLine, {
|
|
2851
|
+
inputValue,
|
|
2852
|
+
placeholder: placeholder || ' ',
|
|
2853
|
+
cursorVisible,
|
|
2854
|
+
accent: 'redBright'
|
|
2855
|
+
})
|
|
2856
|
+
),
|
|
2857
|
+
errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
|
|
2858
|
+
);
|
|
2859
|
+
}
|
|
2860
|
+
|
|
2861
|
+
function PlanApprovalPanel({ request, inputValue, errorText, copy, cursorVisible }) {
|
|
2862
|
+
if (!request) return null;
|
|
2863
|
+
const goal = String(request.goal || '').trim();
|
|
2864
|
+
const summary = String(request.summary || '').trim();
|
|
2865
|
+
const filePath = String(request.filePath || '').trim();
|
|
2866
|
+
const placeholder = String(copy.planApproval.answerPlaceholder || '').trim();
|
|
2867
|
+
return h(
|
|
2868
|
+
Box,
|
|
2869
|
+
{
|
|
2870
|
+
marginTop: 1,
|
|
2871
|
+
flexDirection: 'column',
|
|
2872
|
+
borderStyle: 'round',
|
|
2873
|
+
borderColor: 'yellowBright',
|
|
2874
|
+
paddingX: 1,
|
|
2875
|
+
paddingY: 0
|
|
2876
|
+
},
|
|
2877
|
+
h(Text, { color: 'yellowBright' }, copy.planApproval.title),
|
|
2878
|
+
goal ? h(Text, { color: 'white' }, `${copy.planApproval.goalLabel}: ${goal}`) : null,
|
|
2879
|
+
summary ? h(Text, { color: 'white' }, `${copy.planApproval.summaryLabel}: ${summary}`) : null,
|
|
2880
|
+
filePath ? h(Text, { color: 'gray' }, `${copy.planApproval.fileLabel}: ${filePath}`) : null,
|
|
2881
|
+
h(Text, { color: 'gray' }, copy.planApproval.prompt),
|
|
2882
|
+
h(
|
|
2883
|
+
Box,
|
|
2884
|
+
{ marginTop: 1 },
|
|
2885
|
+
h(Text, { color: 'yellowBright' }, `${copy.planApproval.answerLabel}: `),
|
|
2886
|
+
h(ApprovalCursorLine, {
|
|
2887
|
+
inputValue,
|
|
2888
|
+
placeholder: placeholder || ' ',
|
|
2889
|
+
cursorVisible,
|
|
2890
|
+
accent: 'yellowBright'
|
|
2891
|
+
})
|
|
2739
2892
|
),
|
|
2740
2893
|
errorText ? h(Text, { color: 'yellowBright' }, errorText) : null
|
|
2741
2894
|
);
|
|
@@ -2825,6 +2978,10 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
2825
2978
|
const [pendingDeleteApproval, setPendingDeleteApproval] = useState(null);
|
|
2826
2979
|
const [deleteApprovalInput, setDeleteApprovalInput] = useState('');
|
|
2827
2980
|
const [deleteApprovalError, setDeleteApprovalError] = useState('');
|
|
2981
|
+
const [pendingPlanApproval, setPendingPlanApproval] = useState(null);
|
|
2982
|
+
const [planApprovalInput, setPlanApprovalInput] = useState('');
|
|
2983
|
+
const [planApprovalError, setPlanApprovalError] = useState('');
|
|
2984
|
+
const approvalLockActive = Boolean(pendingDeleteApproval || pendingPlanApproval);
|
|
2828
2985
|
const activeAssistantIdRef = useRef(null);
|
|
2829
2986
|
const activeAssistantAutoSkillNamesRef = useRef([]);
|
|
2830
2987
|
const streamedAssistantHandledRef = useRef(false);
|
|
@@ -3204,6 +3361,25 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
3204
3361
|
resultVerified: '',
|
|
3205
3362
|
resultNext: ''
|
|
3206
3363
|
});
|
|
3364
|
+
setPendingPlanApproval({
|
|
3365
|
+
goal: parsedPlanSummary.planSummary || '',
|
|
3366
|
+
summary: parsedPlanSummary.finalSummary || parsedPlanSummary.planSummary || '',
|
|
3367
|
+
filePath: parsedPlanSummary.filePath || ''
|
|
3368
|
+
});
|
|
3369
|
+
setPlanApprovalInput('');
|
|
3370
|
+
setPlanApprovalError('');
|
|
3371
|
+
} else if (result.type === 'system') {
|
|
3372
|
+
const pendingMeta = parsePendingPlanApprovalMessage(result.text || '');
|
|
3373
|
+
if (pendingMeta) {
|
|
3374
|
+
setPendingPlanApproval({
|
|
3375
|
+
goal: pendingMeta.goal || '',
|
|
3376
|
+
summary: pendingMeta.summary || '',
|
|
3377
|
+
filePath: pendingMeta.filePath || ''
|
|
3378
|
+
});
|
|
3379
|
+
setPlanState((prev) => ({ ...prev, pendingApproval: true }));
|
|
3380
|
+
setPlanApprovalInput('');
|
|
3381
|
+
setPlanApprovalError('');
|
|
3382
|
+
}
|
|
3207
3383
|
}
|
|
3208
3384
|
setMessages((prev) => [
|
|
3209
3385
|
...prev,
|
|
@@ -3342,6 +3518,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
3342
3518
|
setBusy(true);
|
|
3343
3519
|
setInputStage('sending');
|
|
3344
3520
|
setRuntimeStatus(makeStatus(copy.runtime.sendingToGateway, copy.runtime.preparingRequest, 'yellowBright'));
|
|
3521
|
+
setPendingPlanApproval(null);
|
|
3522
|
+
setPlanApprovalInput('');
|
|
3523
|
+
setPlanApprovalError('');
|
|
3345
3524
|
setPlanState({
|
|
3346
3525
|
current: 0,
|
|
3347
3526
|
total: 0,
|
|
@@ -3623,6 +3802,9 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
3623
3802
|
if (event?.type === 'plan:steps') {
|
|
3624
3803
|
const planSteps = Array.isArray(event.steps) ? event.steps : [];
|
|
3625
3804
|
if (planSteps.length > 0) {
|
|
3805
|
+
setPendingPlanApproval(null);
|
|
3806
|
+
setPlanApprovalInput('');
|
|
3807
|
+
setPlanApprovalError('');
|
|
3626
3808
|
setPlanState((prev) => ({
|
|
3627
3809
|
...prev,
|
|
3628
3810
|
total: planSteps.length,
|
|
@@ -3921,6 +4103,46 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
3921
4103
|
return;
|
|
3922
4104
|
}
|
|
3923
4105
|
|
|
4106
|
+
if (pendingPlanApproval) {
|
|
4107
|
+
if (key.return) {
|
|
4108
|
+
const parsed = parsePlanApprovalAnswer(planApprovalInput);
|
|
4109
|
+
if (parsed.action === 'approve' || parsed.action === 'reject' || parsed.action === 'edit') {
|
|
4110
|
+
setPendingPlanApproval(null);
|
|
4111
|
+
setPlanApprovalInput('');
|
|
4112
|
+
setPlanApprovalError('');
|
|
4113
|
+
runSubmission(parsed.command);
|
|
4114
|
+
} else if (parsed.action === 'missing_feedback') {
|
|
4115
|
+
setPlanApprovalError(copy.planApproval.missingFeedback);
|
|
4116
|
+
} else {
|
|
4117
|
+
setPlanApprovalError(copy.planApproval.invalidAnswer);
|
|
4118
|
+
}
|
|
4119
|
+
return;
|
|
4120
|
+
}
|
|
4121
|
+
|
|
4122
|
+
if (isBackspaceKey(value, key) || isDeleteKey(value, key)) {
|
|
4123
|
+
setPlanApprovalInput((prev) => prev.slice(0, -1));
|
|
4124
|
+
setPlanApprovalError('');
|
|
4125
|
+
return;
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
if (isPrintableInput(value, key)) {
|
|
4129
|
+
setPlanApprovalInput((prev) => `${prev}${value}`);
|
|
4130
|
+
setPlanApprovalError('');
|
|
4131
|
+
return;
|
|
4132
|
+
}
|
|
4133
|
+
|
|
4134
|
+
if (key.ctrl && value === 'c') {
|
|
4135
|
+
if (busy && typeof runtime.abort === 'function') {
|
|
4136
|
+
runtime.abort();
|
|
4137
|
+
return;
|
|
4138
|
+
}
|
|
4139
|
+
exit();
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
return;
|
|
4144
|
+
}
|
|
4145
|
+
|
|
3924
4146
|
if (key.upArrow) {
|
|
3925
4147
|
if (suggestionNav && commandSuggestions.length > 0) {
|
|
3926
4148
|
setMenuIndex((prev) => moveSuggestionSelection(prev, commandSuggestions.length, 'up'));
|
|
@@ -4171,13 +4393,24 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4171
4393
|
}, []);
|
|
4172
4394
|
|
|
4173
4395
|
useEffect(() => {
|
|
4174
|
-
|
|
4175
|
-
if (!busy && !hasLoadingMessage) return () => {};
|
|
4396
|
+
if (!busy) return () => {};
|
|
4176
4397
|
const timer = setInterval(() => {
|
|
4177
4398
|
setLoaderTick((prev) => prev + 1);
|
|
4178
4399
|
}, 500);
|
|
4179
4400
|
return () => clearInterval(timer);
|
|
4180
|
-
}, [busy
|
|
4401
|
+
}, [busy]);
|
|
4402
|
+
|
|
4403
|
+
useEffect(() => {
|
|
4404
|
+
const pending = Boolean(runtimeState?.pendingPlanApproval);
|
|
4405
|
+
if (!pending) {
|
|
4406
|
+
setPendingPlanApproval(null);
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
// Startup/recovery fallback only while idle; do not resurrect the panel mid-execution.
|
|
4410
|
+
if (!busy) {
|
|
4411
|
+
setPendingPlanApproval((prev) => prev || { goal: '', summary: '', filePath: '' });
|
|
4412
|
+
}
|
|
4413
|
+
}, [runtimeState?.pendingPlanApproval, busy]);
|
|
4181
4414
|
|
|
4182
4415
|
useEffect(() => {
|
|
4183
4416
|
if (commandSuggestions.length === 0) {
|
|
@@ -4219,6 +4452,11 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4219
4452
|
activeUserMessageIdRef.current,
|
|
4220
4453
|
activeAssistantIdRef.current
|
|
4221
4454
|
);
|
|
4455
|
+
const activeApprovalLock = pendingDeleteApproval
|
|
4456
|
+
? { text: copy.deleteApproval.inputLocked }
|
|
4457
|
+
: pendingPlanApproval
|
|
4458
|
+
? { text: copy.planApproval.inputLocked }
|
|
4459
|
+
: null;
|
|
4222
4460
|
|
|
4223
4461
|
return h(
|
|
4224
4462
|
Box,
|
|
@@ -4251,7 +4489,15 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4251
4489
|
request: pendingDeleteApproval,
|
|
4252
4490
|
inputValue: deleteApprovalInput,
|
|
4253
4491
|
errorText: deleteApprovalError,
|
|
4254
|
-
copy
|
|
4492
|
+
copy,
|
|
4493
|
+
cursorVisible
|
|
4494
|
+
}),
|
|
4495
|
+
h(PlanApprovalPanel, {
|
|
4496
|
+
request: pendingPlanApproval,
|
|
4497
|
+
inputValue: planApprovalInput,
|
|
4498
|
+
errorText: planApprovalError,
|
|
4499
|
+
copy,
|
|
4500
|
+
cursorVisible
|
|
4255
4501
|
}),
|
|
4256
4502
|
debugKeys
|
|
4257
4503
|
? h(
|
|
@@ -4266,8 +4512,8 @@ export function ChatApp({ runtime, sessionId, model, sdkProvider = 'openai-compa
|
|
|
4266
4512
|
afterCursor,
|
|
4267
4513
|
cursorVisible,
|
|
4268
4514
|
busy,
|
|
4269
|
-
disabled: Boolean(
|
|
4270
|
-
disabledText:
|
|
4515
|
+
disabled: Boolean(activeApprovalLock),
|
|
4516
|
+
disabledText: activeApprovalLock ? activeApprovalLock.text : '',
|
|
4271
4517
|
inputStage,
|
|
4272
4518
|
pendingQueueLength: pendingQueue.length,
|
|
4273
4519
|
showToolDetails,
|