Skip to content

Commit 6786a77

Browse files
committed
refactor(ui): 重做确认弹窗信息架构,添加 Diff 展开/折叠
- ConfirmationPrompt: 去掉重复标题,统一中文文案,移除普通详情的 bordered box, 仅方案审核保留边框,选项标签全部中文化 - DiffRenderer: 新增 useState + useInput 交互式展开/折叠(按 E 切换), isFocused prop 控制键盘激活,统计信息显示 +N/-M 变更行数 - ExitPlanModeTool/EnterPlanModeTool: 确认消息精简为中文
1 parent 150a48f commit 6786a77

4 files changed

Lines changed: 96 additions & 57 deletions

File tree

packages/cli/src/tools/builtin/plan/EnterPlanModeTool.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,8 @@ User: "What files handle routing?"
101101
try {
102102
const response = await context.confirmationHandler.requestConfirmation({
103103
type: 'enterPlanMode',
104-
message:
105-
'The assistant requests to enter Plan mode for this complex task. In Plan mode, the assistant will:\n\n' +
106-
'1. Research the codebase thoroughly (read-only)\n' +
107-
'2. Understand existing patterns and architecture\n' +
108-
'3. Design an implementation approach\n' +
109-
'4. Present a detailed plan for your approval\n\n' +
110-
'Do you want to enter Plan mode?',
111-
details: 'Plan mode enables systematic research before implementation',
104+
message: '助手建议先制定实施方案再执行。',
105+
details: '规划模式下仅使用只读工具进行调研,完成后提交方案供审核。',
112106
});
113107

114108
if (response.approved) {

packages/cli/src/tools/builtin/plan/ExitPlanModeTool.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -84,17 +84,9 @@ Before using this tool, ensure your plan is clear and unambiguous. If there are
8484
try {
8585
const response = await context.confirmationHandler.requestConfirmation({
8686
type: 'exitPlanMode',
87-
message:
88-
'The assistant has finished planning and is ready for your review.\n\n' +
89-
'[WARN] Before approving, please verify:\n' +
90-
'1. The assistant has written a detailed plan to the plan file\n' +
91-
'2. The plan includes implementation steps, affected files, and testing methods\n' +
92-
'3. You have seen text explanations from the assistant (not just tool calls)\n\n' +
93-
'If the assistant only made tool calls without presenting a plan summary,\n' +
94-
'please reject and ask for a proper plan.',
95-
details:
96-
'After approval, the assistant will exit Plan mode and begin implementation.',
97-
planContent: planContent || undefined, // 传递 plan 内容给 UI
87+
message: '助手已完成方案规划,请审核后选择执行方式。',
88+
details: '批准后将退出规划模式,开始实施。',
89+
planContent: planContent || undefined,
9890
});
9991

10092
if (response.approved) {

packages/cli/src/ui/components/ConfirmationPrompt.tsx

Lines changed: 33 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,19 @@ interface ConfirmationContentProps {
3030
const ConfirmationContent = React.memo<ConfirmationContentProps>(
3131
({ details, headerColor, isPlanModeExit, isPlanModeEnter, terminalWidth }) => (
3232
<>
33+
{/* 副标题:显示工具签名等上下文信息(如 "Bash(git status)") */}
3334
{details.title && (
3435
<Box marginBottom={1}>
35-
<Text bold>{details.title}</Text>
36+
<Text dimColor>{details.title}</Text>
3637
</Box>
3738
)}
3839

3940
<Box marginBottom={1}>
4041
<Text>{details.message}</Text>
4142
</Box>
4243

43-
{(details.planContent || details.details) && (
44+
{/* 方案审核:保留 bordered box */}
45+
{details.planContent && (
4446
<Box
4547
flexDirection="column"
4648
marginBottom={1}
@@ -49,26 +51,33 @@ const ConfirmationContent = React.memo<ConfirmationContentProps>(
4951
padding={1}
5052
>
5153
<Text bold color={headerColor}>
52-
{isPlanModeExit
53-
? 'Implementation Plan:'
54-
: isPlanModeEnter
55-
? 'Details:'
56-
: 'Operation Details:'}
54+
实施方案
5755
</Text>
5856
<Box marginTop={1}>
5957
<MessageRenderer
60-
content={details.planContent || details.details || ''}
58+
content={details.planContent}
6159
role="assistant"
6260
terminalWidth={terminalWidth - 4}
6361
/>
6462
</Box>
6563
</Box>
6664
)}
6765

66+
{/* 普通操作详情:无边框,轻量渲染 */}
67+
{!details.planContent && details.details && (
68+
<Box flexDirection="column" marginBottom={1}>
69+
<MessageRenderer
70+
content={details.details}
71+
role="assistant"
72+
terminalWidth={terminalWidth}
73+
/>
74+
</Box>
75+
)}
76+
6877
{details.risks && details.risks.length > 0 && (
6978
<Box flexDirection="column" marginBottom={1}>
7079
<Text color="red" bold>
71-
[WARN] 风险提示:
80+
风险提示:
7281
</Text>
7382
{details.risks.map((risk, index) => (
7483
<Box key={index} marginLeft={2}>
@@ -207,17 +216,17 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
207216
return [
208217
{
209218
key: 'approve-auto',
210-
label: '[Y] Yes, execute with auto-edit mode',
219+
label: '[Y] 批准,自动执行',
211220
value: { approved: true, targetMode: PermissionMode.AUTO_EDIT },
212221
},
213222
{
214223
key: 'approve-default',
215-
label: '[S] Yes, execute with default mode (ask for each operation)',
224+
label: '[S] 批准,逐步确认',
216225
value: { approved: true, targetMode: PermissionMode.DEFAULT },
217226
},
218227
{
219228
key: 'reject',
220-
label: '[N] No, keep planning',
229+
label: '[N] 继续优化方案',
221230
value: { approved: false, reason: '方案需要改进' },
222231
},
223232
];
@@ -227,12 +236,12 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
227236
return [
228237
{
229238
key: 'approve',
230-
label: '[Y] Yes, enter Plan mode',
239+
label: '[Y] 进入规划模式',
231240
value: { approved: true },
232241
},
233242
{
234243
key: 'reject',
235-
label: '[N] No, proceed directly',
244+
label: '[N] 直接执行',
236245
value: { approved: false, reason: '用户拒绝进入 Plan 模式' },
237246
},
238247
];
@@ -242,12 +251,12 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
242251
return [
243252
{
244253
key: 'continue',
245-
label: '[Y] Yes, continue',
254+
label: '[Y] 继续执行',
246255
value: { approved: true },
247256
},
248257
{
249258
key: 'stop',
250-
label: '[N] No, stop here',
259+
label: '[N] 停止',
251260
value: { approved: false, reason: '用户选择停止' },
252261
},
253262
];
@@ -256,17 +265,17 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
256265
return [
257266
{
258267
key: 'approve-once',
259-
label: '[Y] Yes (once only)',
268+
label: '[Y] 允许(仅本次)',
260269
value: { approved: true, scope: 'once' },
261270
},
262271
{
263272
key: 'approve-session',
264-
label: '[S] Yes, remember for this project (Shift+Tab)',
273+
label: '[S] 允许(记住本项目)',
265274
value: { approved: true, scope: 'session' },
266275
},
267276
{
268277
key: 'reject',
269-
label: '[N] No',
278+
label: '[N] 拒绝',
270279
value: { approved: false, reason: '用户拒绝' },
271280
},
272281
];
@@ -275,18 +284,15 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
275284
// Header 样式(memo 化)
276285
const headerStyle = useMemo(() => {
277286
if (isPlanModeExit) {
278-
return {
279-
color: 'cyan' as const,
280-
title: 'Plan Mode - Review Implementation Plan',
281-
};
287+
return { color: 'cyan' as const, title: '方案审核' };
282288
}
283289
if (isPlanModeEnter) {
284-
return { color: 'magenta' as const, title: 'Enter Plan Mode?' };
290+
return { color: 'magenta' as const, title: '进入规划模式' };
285291
}
286292
if (isMaxTurnsExceeded) {
287-
return { color: 'yellow' as const, title: 'Max Turns Exceeded' };
293+
return { color: 'yellow' as const, title: '已达最大轮次' };
288294
}
289-
return { color: 'yellow' as const, title: 'Confirmation Required' };
295+
return { color: 'yellow' as const, title: '操作确认' };
290296
}, [isPlanModeExit, isPlanModeEnter, isMaxTurnsExceeded]);
291297

292298
return (
@@ -313,7 +319,7 @@ export const ConfirmationPrompt: React.FC<ConfirmationPromptProps> = React.memo(
313319

314320
<Box flexDirection="column">
315321
<Text color="gray">
316-
使用 Up/Down 选择,回车确认(支持 Y/S/N 快捷键,ESC 取消)
322+
使用 ↑↓ 选择,回车确认 · Y/S/N 快捷键 · Esc 取消
317323
</Text>
318324
<SelectInput
319325
items={options}

packages/cli/src/ui/components/DiffRenderer.tsx

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
/**
22
* Diff 渲染组件 - 渲染 unified diff 格式的差异
3+
* 支持交互式展开/折叠
34
*/
45

5-
import { Box, Text } from 'ink';
6-
import React from 'react';
6+
import { Box, Text, useInput } from 'ink';
7+
import React, { useState } from 'react';
78
import { useTheme } from '../../store/selectors/index.js';
89

910
interface DiffRendererProps {
@@ -12,8 +13,12 @@ interface DiffRendererProps {
1213
matchLine?: number; // 变更所在行号(保留用于向后兼容,但不再显示)
1314
terminalWidth: number;
1415
maxLines?: number; // 默认显示的最大行数(默认 20 行)
16+
isFocused?: boolean; // 是否激活键盘监听(避免多实例冲突)
1517
}
1618

19+
/** 展开时显示的最大行数上限,防止性能问题 */
20+
const MAX_EXPANDED_LINES = 400;
21+
1722
/**
1823
* 解析 unified diff 格式的 patch
1924
*/
@@ -99,20 +104,46 @@ export const DiffRenderer: React.FC<DiffRendererProps> = React.memo(
99104
startLine,
100105
matchLine,
101106
terminalWidth,
102-
maxLines = 20, // 默认显示 20 行
107+
maxLines = 20,
108+
isFocused = false,
103109
}) => {
104110
const theme = useTheme();
105111
const parsedLines = parsePatch(patch);
112+
const [isExpanded, setIsExpanded] = useState(false);
113+
114+
// 键盘交互:按 E 切换展开/折叠
115+
useInput(
116+
(input) => {
117+
if (input.toLowerCase() === 'e') {
118+
setIsExpanded((prev) => !prev);
119+
}
120+
},
121+
{ isActive: isFocused && parsedLines.length > maxLines }
122+
);
106123

107124
// 计算行号列宽度
108125
const maxLineNum = Math.max(...parsedLines.map((l) => l.lineNumber || 0));
109126
const lineNumWidth = maxLineNum.toString().length + 1;
110127

128+
// 统计变更行数
129+
const addedCount = parsedLines.filter((l) => l.type === 'add').length;
130+
const removedCount = parsedLines.filter((l) => l.type === 'remove').length;
131+
111132
// 判断是否需要折叠
112133
const totalLines = parsedLines.length;
113134
const needsCollapse = totalLines > maxLines;
114-
const displayLines = needsCollapse ? parsedLines.slice(0, maxLines) : parsedLines;
115-
const hiddenLines = totalLines - maxLines;
135+
136+
// 根据展开状态决定显示行数
137+
let displayLines: typeof parsedLines;
138+
if (!needsCollapse) {
139+
displayLines = parsedLines;
140+
} else if (isExpanded) {
141+
displayLines = parsedLines.slice(0, MAX_EXPANDED_LINES);
142+
} else {
143+
displayLines = parsedLines.slice(0, maxLines);
144+
}
145+
146+
const hiddenLines = totalLines - displayLines.length;
116147

117148
return (
118149
<Box flexDirection="column" marginTop={1} marginBottom={1}>
@@ -124,7 +155,13 @@ export const DiffRenderer: React.FC<DiffRendererProps> = React.memo(
124155
{/* diff 统计信息 */}
125156
{needsCollapse && (
126157
<Text color={theme.colors.info}>
127-
显示前 {maxLines} 行,共 {totalLines} 行 diff
158+
{isExpanded
159+
? `已展开 ${displayLines.length}/${totalLines} 行`
160+
: `显示前 ${maxLines} 行,共 ${totalLines} 行`}
161+
{' · '}
162+
<Text color={theme.colors.success}>+{addedCount}</Text>
163+
{' '}
164+
<Text color={theme.colors.error}>-{removedCount}</Text>
128165
</Text>
129166
)}
130167

@@ -158,7 +195,7 @@ export const DiffRenderer: React.FC<DiffRendererProps> = React.memo(
158195
if (line.type === 'add') {
159196
prefix = '+';
160197
fgColor = theme.colors.success;
161-
bgColor = undefined; // Ink 不支持背景色,使用前景色区分
198+
bgColor = undefined;
162199
} else if (line.type === 'remove') {
163200
prefix = '-';
164201
fgColor = theme.colors.error;
@@ -183,12 +220,22 @@ export const DiffRenderer: React.FC<DiffRendererProps> = React.memo(
183220
);
184221
})}
185222

186-
{/* 折叠提示 */}
223+
{/* 折叠/展开提示 */}
187224
{needsCollapse && (
188225
<Box marginTop={1}>
189-
<Text color={theme.colors.warning} dimColor>
190-
已隐藏剩余 {hiddenLines} 行 diff(总共 {totalLines} 行)
191-
</Text>
226+
{isExpanded ? (
227+
<Text color={theme.colors.info} dimColor>
228+
{hiddenLines > 0
229+
? `已达显示上限 ${MAX_EXPANDED_LINES} 行,仍有 ${hiddenLines} 行未显示`
230+
: '已显示全部内容'}
231+
{isFocused ? ' · 按 E 折叠' : ''}
232+
</Text>
233+
) : (
234+
<Text color={theme.colors.info} dimColor>
235+
已隐藏剩余 {hiddenLines}
236+
{isFocused ? ' · 按 E 展开全部' : ''}
237+
</Text>
238+
)}
192239
</Box>
193240
)}
194241

0 commit comments

Comments
 (0)