@@ -109,6 +109,13 @@ interface ConfirmationResponse {
109109
110110 /** 拒绝原因(如果未批准) */
111111 reason? : string ;
112+
113+ /**
114+ * 授权范围
115+ * - once: 仅本次执行
116+ * - session: 记住至项目本地配置(settings.local.json)
117+ */
118+ scope? : ' once' | ' session' ;
112119}
113120```
114121
@@ -176,17 +183,53 @@ const useConfirmation = () => {
176183
177184``` typescript
178185const ConfirmationPrompt: React .FC <Props > = ({ details , onResponse }) => {
179- // 使用 Ink 的 useInput hook 处理键盘输入
186+ const { isFocused } = useFocus ({ autoFocus: true });
187+
180188 useInput ((input , key ) => {
181- if (input === ' y' || input === ' Y' ) {
182- onResponse ({ approved: true });
183- } else if (input === ' n' || input === ' N' ) {
184- onResponse ({ approved: false , reason: ' 用户拒绝' });
185- } else if (key .escape ) {
189+ if (! isFocused ) return ;
190+
191+ if (key .escape ) {
186192 onResponse ({ approved: false , reason: ' 用户取消' });
193+ return ;
194+ }
195+
196+ const normalized = input ?.toLowerCase ();
197+ if (normalized === ' y' ) {
198+ onResponse ({ approved: true , scope: ' once' });
199+ } else if (normalized === ' s' || (key .shift && key .tab )) {
200+ onResponse ({ approved: true , scope: ' session' });
201+ } else if (normalized === ' n' ) {
202+ onResponse ({ approved: false , reason: ' 用户拒绝' });
187203 }
188204 });
189205
206+ const ItemComponent: React .FC <{ label: string ; isSelected? : boolean }> = ({
207+ label ,
208+ isSelected ,
209+ }) => <Text color ={isSelected ? ' yellow' : undefined }>{label }< / Text > ;
210+
211+ const options = useMemo <
212+ Array < { label : string ; key : string ; value : ConfirmationResponse }>
213+ > (() => {
214+ return [
215+ {
216+ key: ' approve-once' ,
217+ label: ' [Y] Yes (once only)' ,
218+ value: { approved: true , scope: ' once' },
219+ },
220+ {
221+ key: ' approve-session' ,
222+ label: ' [S] Yes, remember for this project (Shift+Tab)' ,
223+ value: { approved: true , scope: ' session' },
224+ },
225+ {
226+ key: ' reject' ,
227+ label: ' [N] No' ,
228+ value: { approved: false , reason: ' 用户拒绝' },
229+ },
230+ ];
231+ }, []);
232+
190233 return (
191234 < Box flexDirection = " column" borderStyle = " round" borderColor = " yellow" >
192235 < Text bold color = " yellow" > 🔔 需要用户确认< / Text >
@@ -202,10 +245,15 @@ const ConfirmationPrompt: React.FC<Props> = ({ details, onResponse }) => {
202245 </Box >
203246 )}
204247
205- <Text >
206- < Text color = " green" bold > [Y ]< / Text> 批准 /
207- < Text color = " red" bold > [N ]< / Text > 拒绝
208- < / Text >
248+ < Box flexDirection = " column" >
249+ < Text color = " gray" > 使用 ↑ ↓ 选择,回车确认(支持 Y / S / N 快捷键,ESC 取消)< / Text >
250+ < SelectInput
251+ isFocused = {isFocused }
252+ items = {options }
253+ itemComponent = {ItemComponent }
254+ onSelect = {(item) => onResponse(item.value)}
255+ / >
256+ < / Box >
209257 < / Box >
210258 );
211259};
@@ -317,9 +365,10 @@ await agent.chat('删除所有测试文件', context);
317365// 4. ConfirmationPrompt 组件渲染
318366// → 显示确认对话框
319367
320- // 5. 用户按键响应
321- // → 按 'Y': onResponse({ approved: true })
322- // → 按 'N': onResponse({ approved: false, reason: '用户拒绝' })
368+ // 5. 用户选择确认选项
369+ // → 选择“仅此一次允许”: onResponse({ approved: true, scope: 'once' })
370+ // → 选择“本会话允许”: onResponse({ approved: true, scope: 'session' })
371+ // → 选择“拒绝”: onResponse({ approved: false, reason: '用户拒绝' })
323372
324373// 6. handleResponse 调用 resolver
325374// → Promise 被 resolve
@@ -329,6 +378,11 @@ await agent.chat('删除所有测试文件', context);
329378// → 如果拒绝: 调用 execution.abort()
330379```
331380
381+ ## 授权记忆
382+
383+ - 当用户选择 ` scope: 'session' ` 时,权限阶段会缓存当前工具调用签名,并将规则追加到 ` .blade/settings.local.json ` ,在当前项目中长期生效。
384+ - 命中缓存时,权限阶段会直接返回允许结果,并附带原因说明,便于日志审计。
385+
332386## 最佳实践
333387
334388### 工具开发者
@@ -368,8 +422,8 @@ await agent.chat('删除所有测试文件', context);
368422 - 易于理解的操作提示
369423
3704242 . ** 良好的键盘交互**
371- - 使用 Ink 的 ` useInput ` 而非直接监听 stdin
372- - 支持多种确认方式(Y/y, N/n, ESC)
425+ - 使用 Ink 的 ` useInput ` 捕获 ESC 等基础事件
426+ - 借助 ` ink-select-input ` 提供箭头选择 + Enter 的确认体验
373427 - 防止与其他输入冲突
374428
3754293 . ** 状态管理**
@@ -451,10 +505,12 @@ const deleteFilesTool: ToolConfig = {
451505 │ • debug .log │
452506 │ ... 还有 12 个文件 │
453507 │ │
454- │ [Y ] 批准 / [N ] 拒绝 │
508+ │ › [Y ] Yes (once only ) │
509+ │ [S ] Yes , remember for this project │
510+ │ [N ] No │
455511 └─────────────────────────────────┘
456- → 用户按 Y : 继续删除
457- → 用户按 N : 中止操作,向 AI 返回 " 用户拒绝执行 "
512+ → 用户通过方向键选择对应项并按回车确认
513+ → 选择“记住至本项目”会立刻写入 settings . local . json
458514```
459515
460516### 场景 2: 网络请求
0 commit comments