fix(approval): clarify limited-scope Enter action

This commit is contained in:
creatixchu committed 2026-10-20 13:09:26 +08:00
1 parent eb5150a87b
commit b2dd5b8d98
6 files changed
+25 -14

No files matched your search

+2 -2
View File
@@ -12,8 +12,8 @@
en: 8d6c7c6e63f849f6
zh: d89fa273e30b99d3
/deepseek-ai-dsh-client-ui-approval/table-of-contents:
en: 4d8f80fd7fb1b422
zh: 286100dd6b607ed7
en: 7b5184a35d7607f9
zh: 55ebfea4d5ad26ed
/deepseek-ai-dsh-client-ui-approval/model-experience:
en: 8161dce49e9f899c
zh: a17401e1e78ba6be
+1 -1
View File
@@ -20,7 +20,7 @@ Browser approval presentation over the Agent-scoped Remote Event waterfall. The
Focus the approval detail region to approve with Enter or reject with Escape. The mounted plugin reserves both keys against editable shortcuts. Enter on the focused Reject button retains its native reject action. Input controls and IME candidates keep their own keys. Keyboard and pointer actions share one pending-request lock; a withdrawn or replaced request cannot accept another answer, and an earlier failed answer cannot unlock its replacement.
When offered, **Always allow** is the highlighted primary button. Otherwise, the current-request allow action is primary.
When offered, **Always allow** is the highlighted primary button. Otherwise, the current-request allow action is primary. Enter in the detail region always grants the limited scope: once for a tool approval, or the current session for CUA. Persistent access requires explicitly activating Always allow. The shortcut reference uses Allow because the caller supplies that scope.
<a id="model-experience"></a>
## Model Experience
+1 -1
View File
@@ -20,7 +20,7 @@ kind: "package-reference"
聚焦审批详情区域后,Enter 批准,Esc 拒绝。插件挂载期间,这两个按键不能分配给可编辑快捷键。聚焦“拒绝”按钮后,Enter 保留按钮原生拒绝操作。输入控件与输入法候选保留各自的按键。键盘和指针操作共用同一待处理请求锁;已撤销或替换的请求不能再次作答,较早请求的失败也不会解锁替代请求。
提供“始终允许”时,将它显示为高亮主按钮;否则高亮当前请求的允许按钮。
提供“始终允许”时,将它显示为高亮主按钮;否则高亮当前请求的允许按钮。在详情区域按 Enter 始终授予有限范围的许可:工具审批为单次,CUA 为当前会话。持久授权需要显式操作“始终允许”按钮。快捷键说明使用“允许”,具体范围由调用方提供。
<a id="model-experience"></a>
## 模型体验
@@ -81,7 +81,7 @@ export function apply(ctx: ClientContext): void {
ctx.inject(['shortcuts'], (scope) => {
const t = ctx.locale.bind(NS)
scope.effect(() => scope.shortcuts.registerFixed({
id: 'approval.allow' as ShortcutCommandId, label: () => t('allowOnce'), keys: ['Enter'], bindings: [{ code: 'Enter', modifiers: [] }], group: 'approval',
id: 'approval.allow' as ShortcutCommandId, label: () => t('allow'), keys: ['Enter'], bindings: [{ code: 'Enter', modifiers: [] }], group: 'approval',
}), 'ui-approval: fixed allow reference')
scope.effect(() => scope.shortcuts.registerFixed({
id: 'approval.reject' as ShortcutCommandId, label: () => t('reject'), keys: ['Esc'], bindings: [{ code: 'Escape', modifiers: [] }], group: 'approval',
@@ -6,6 +6,7 @@ export const zh = {
'detail.aria': '审批详情',
escalation: '工具 {toolName} 请求越权执行',
reject: '拒绝',
allow: '允许',
allowOnce: '允许一次',
allowAlways: '始终允许',
} satisfies Record<string, string>
@@ -19,6 +20,7 @@ export const en = {
'detail.aria': 'Approval details',
escalation: 'Tool {toolName} requests privileged execution',
reject: 'Reject',
allow: 'Allow',
allowOnce: 'Allow once',
allowAlways: 'Always allow',
} satisfies Record<ApprovalKey, string>
@@ -218,7 +218,7 @@ describe('approval Remote Event consumer', () => {
expect(next).toHaveBeenCalledOnce()
expect(bench.pending.getSnapshot()).toEqual([])
expect(bench.register).toHaveBeenCalledOnce()
expect(bench.shortcuts.mock.calls.map(([entry]) => entry.label())).toEqual(['allowOnce', 'reject'])
expect(bench.shortcuts.mock.calls.map(([entry]) => entry.label())).toEqual(['allow', 'reject'])
})
it('publishes one scoped takeover, returns the answer, and keeps stable registrations', async () => {
@@ -514,21 +514,30 @@ describe('package entries', () => {
})
})
it.each(['allow', 'reject', 'always'] as const)('settles a fixed application decision through %s without free text', async (choice) => {
it.each(['allow', 'reject', 'always', 'keyboard'] as const)('settles a fixed application decision through %s without free text', async (choice) => {
const onAllow = vi.fn(async () => {}), onReject = vi.fn(async () => {}), onAlways = vi.fn(async () => {})
const actions = { allow: onAllow, reject: onReject, always: onAlways }
render(<ApprovalCard {...{
requestKey: 'app-approval', title: 'Control this app?', description: 'Only for this task.',
subject: { name: 'Calculator', icon: 'data:image/png;base64,aWNvbg==' }, allowLabel: 'Allow for this task',
subject: { name: 'Calculator', ...choice === 'keyboard' ? {} : { icon: 'data:image/png;base64,aWNvbg==' } }, allowLabel: 'Allow for this task',
canAnswer: () => true, onAllow, onReject, onAlways, renderSlot: vi.fn(() => null),
t: (key: string) => ({ reject: 'Reject', allowAlways: 'Always allow' })[key] ?? key,
} as FactoryComponentPropsOf<'conversation.approval.card'>} />)
expect(screen.getByText('Calculator')).toBeTruthy()
expect(document.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,aWNvbg==')
if (choice === 'keyboard') expect(document.querySelector('img')).toBeNull()
else expect(document.querySelector('img')?.getAttribute('src')).toBe('data:image/png;base64,aWNvbg==')
expect(screen.queryByRole('textbox')).toBeNull()
const label = { allow: 'Allow for this task', reject: 'Reject', always: 'Always allow' }[choice]
fireEvent.click(screen.getByRole('button', { name: label }))
fireEvent.click(screen.getByRole('button', { name: label }))
await waitFor(() => { expect(actions[choice]).toHaveBeenCalledOnce() })
expect(Object.values(actions).filter(action => action.mock.calls.length > 0)).toEqual([actions[choice]])
const action = choice === 'keyboard' ? 'allow' : choice
if (choice === 'keyboard') {
const detail = screen.getByRole('group')
detail.focus()
fireEvent.keyDown(detail, { key: 'Enter', code: 'Enter' })
fireEvent.keyDown(detail, { key: 'Enter', code: 'Enter' })
} else {
const label = { allow: 'Allow for this task', reject: 'Reject', always: 'Always allow' }[choice]
fireEvent.click(screen.getByRole('button', { name: label }))
fireEvent.click(screen.getByRole('button', { name: label }))
}
await waitFor(() => { expect(actions[action]).toHaveBeenCalledOnce() })
expect(Object.values(actions).filter(callback => callback.mock.calls.length > 0)).toEqual([actions[action]])
})