diff --git a/packages/client/ui-approval/README.i18n.yaml b/packages/client/ui-approval/README.i18n.yaml
index da9aa9eb7e..ab94e8e499 100644
--- a/packages/client/ui-approval/README.i18n.yaml
+++ b/packages/client/ui-approval/README.i18n.yaml
@@ -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
diff --git a/packages/client/ui-approval/README.md b/packages/client/ui-approval/README.md
index 61389c5000..890c1e8035 100644
--- a/packages/client/ui-approval/README.md
+++ b/packages/client/ui-approval/README.md
@@ -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.
## Model Experience
diff --git a/packages/client/ui-approval/README.zh.md b/packages/client/ui-approval/README.zh.md
index 310eff5b89..76ec67b275 100644
--- a/packages/client/ui-approval/README.zh.md
+++ b/packages/client/ui-approval/README.zh.md
@@ -20,7 +20,7 @@ kind: "package-reference"
聚焦审批详情区域后,Enter 批准,Esc 拒绝。插件挂载期间,这两个按键不能分配给可编辑快捷键。聚焦“拒绝”按钮后,Enter 保留按钮原生拒绝操作。输入控件与输入法候选保留各自的按键。键盘和指针操作共用同一待处理请求锁;已撤销或替换的请求不能再次作答,较早请求的失败也不会解锁替代请求。
-提供“始终允许”时,将它显示为高亮主按钮;否则高亮当前请求的允许按钮。
+提供“始终允许”时,将它显示为高亮主按钮;否则高亮当前请求的允许按钮。在详情区域按 Enter 始终授予有限范围的许可:工具审批为单次,CUA 为当前会话。持久授权需要显式操作“始终允许”按钮。快捷键说明使用“允许”,具体范围由调用方提供。
## 模型体验
diff --git a/packages/client/ui-approval/src/client/index.ts b/packages/client/ui-approval/src/client/index.ts
index fd0b04a599..02e71c91b2 100644
--- a/packages/client/ui-approval/src/client/index.ts
+++ b/packages/client/ui-approval/src/client/index.ts
@@ -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',
diff --git a/packages/client/ui-approval/src/client/locales.ts b/packages/client/ui-approval/src/client/locales.ts
index 2a8aae7ec8..1a3ec25ed2 100644
--- a/packages/client/ui-approval/src/client/locales.ts
+++ b/packages/client/ui-approval/src/client/locales.ts
@@ -6,6 +6,7 @@ export const zh = {
'detail.aria': '审批详情',
escalation: '工具 {toolName} 请求越权执行',
reject: '拒绝',
+ allow: '允许',
allowOnce: '允许一次',
allowAlways: '始终允许',
} satisfies Record
@@ -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
diff --git a/packages/client/ui-approval/tests/ui-approval.client.spec.tsx b/packages/client/ui-approval/tests/ui-approval.client.spec.tsx
index 753258b8f1..1885d6b047 100644
--- a/packages/client/ui-approval/tests/ui-approval.client.spec.tsx
+++ b/packages/client/ui-approval/tests/ui-approval.client.spec.tsx
@@ -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( 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]])
})