From 0a82efdcd9cdb8fcdbffa3a38d0b91182cc1d0be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 4 May 2026 19:03:46 +0800 Subject: [PATCH] fix: keep desktop permission prompts visible Desktop notification permission can appear granted while macOS still suppresses a foreground banner, so blocking permission prompts now also request OS-level window attention. The notification sender keeps the native notification path, but prompt callers can ask the window manager for attention when silence would block user progress. The desktop package also declares the existing qrcode runtime dependency used by AdapterSettings so production desktop builds can resolve that import. Constraint: Tauri window attention requires an explicit capability grant in packaged builds. Constraint: macOS may suppress foreground notification banners even when notification APIs report success. Rejected: Replace native notifications with a custom in-app sound or overlay | the notification feature should stay OS-owned and already has an in-app permission card. Confidence: high Scope-risk: narrow Directive: Keep requestAttention limited to blocking prompts unless another flow truly needs persistent OS attention. Tested: bun run test -- src/lib/desktopNotifications.test.ts src/stores/chatStore.test.ts Tested: bun run check:desktop Tested: bun run check:native Tested: git diff --check Not-tested: bun run quality:pr | blocked by existing branch-wide CLI core policy requiring allow-cli-core-change approval --- desktop/src-tauri/capabilities/default.json | 1 + desktop/src/lib/desktopNotifications.test.ts | 29 ++++++++++++++++++++ desktop/src/lib/desktopNotifications.ts | 15 ++++++++++ desktop/src/stores/chatStore.test.ts | 2 ++ desktop/src/stores/chatStore.ts | 2 ++ 5 files changed, 49 insertions(+) diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json index 60f575e3..02123d46 100644 --- a/desktop/src-tauri/capabilities/default.json +++ b/desktop/src-tauri/capabilities/default.json @@ -7,6 +7,7 @@ "core:default", "core:window:allow-close", "core:window:allow-minimize", + "core:window:allow-request-user-attention", "core:window:allow-start-dragging", "core:window:allow-toggle-maximize", "shell:allow-open", diff --git a/desktop/src/lib/desktopNotifications.test.ts b/desktop/src/lib/desktopNotifications.test.ts index bf6ddaec..c64b6336 100644 --- a/desktop/src/lib/desktopNotifications.test.ts +++ b/desktop/src/lib/desktopNotifications.test.ts @@ -5,8 +5,20 @@ const notificationPluginMock = vi.hoisted(() => ({ requestPermission: vi.fn(), sendNotification: vi.fn(), })) +const requestUserAttentionMock = vi.hoisted(() => vi.fn()) +const windowApiMock = vi.hoisted(() => ({ + requestUserAttention: requestUserAttentionMock, + getCurrentWindow: vi.fn(() => ({ + requestUserAttention: requestUserAttentionMock, + })), + UserAttentionType: { + Critical: 1, + Informational: 2, + }, +})) vi.mock('@tauri-apps/plugin-notification', () => notificationPluginMock) +vi.mock('@tauri-apps/api/window', () => windowApiMock) import { getDesktopNotificationPermission, @@ -24,6 +36,8 @@ describe('desktopNotifications', () => { notificationPluginMock.isPermissionGranted.mockReset() notificationPluginMock.requestPermission.mockReset() notificationPluginMock.sendNotification.mockReset() + windowApiMock.getCurrentWindow.mockClear() + windowApiMock.requestUserAttention.mockReset() useSettingsStore.setState({ desktopNotificationsEnabled: true }) }) @@ -110,6 +124,21 @@ describe('desktopNotifications', () => { }) }) + it('requests OS-level window attention for blocking prompts', async () => { + const sender = vi.fn(async () => true) + setNativeNotificationSenderForTests(sender) + + notifyDesktop({ + requestAttention: true, + title: 'Permission required', + body: 'Approve command execution', + }) + + await vi.waitFor(() => expect(sender).toHaveBeenCalledTimes(1)) + await vi.waitFor(() => expect(windowApiMock.requestUserAttention).toHaveBeenCalledTimes(1)) + expect(windowApiMock.requestUserAttention).toHaveBeenCalledWith(windowApiMock.UserAttentionType.Critical) + }) + it('throttles bursts within the same cooldown scope', async () => { vi.useFakeTimers() const sender = vi.fn(async () => true) diff --git a/desktop/src/lib/desktopNotifications.ts b/desktop/src/lib/desktopNotifications.ts index 6e5b06a7..8a2fa5a6 100644 --- a/desktop/src/lib/desktopNotifications.ts +++ b/desktop/src/lib/desktopNotifications.ts @@ -8,6 +8,7 @@ export type DesktopNotificationOptions = { dedupeKey?: string cooldownScope?: string cooldownMs?: number + requestAttention?: boolean } type NativeNotificationSender = (options: { title: string; body?: string }) => Promise | boolean @@ -108,6 +109,16 @@ async function sendNativeNotification(options: { title: string; body?: string }) return true } +async function requestWindowAttention(): Promise { + try { + const { getCurrentWindow, UserAttentionType } = await import('@tauri-apps/api/window') + await getCurrentWindow().requestUserAttention(UserAttentionType.Critical) + return true + } catch { + return false + } +} + export function notifyDesktop(options: DesktopNotificationOptions): void { if (!useSettingsStore.getState().desktopNotificationsEnabled) { return @@ -131,6 +142,10 @@ export function notifyDesktop(options: DesktopNotificationOptions): void { notifiedKeys.add(options.dedupeKey) } + if (options.requestAttention) { + void requestWindowAttention() + } + const sender = overrideNativeNotificationSender ?? sendNativeNotification void Promise.resolve(sender({ title: options.title, body: options.body })).then((sent) => { if (!sent && typeof console !== 'undefined') { diff --git a/desktop/src/stores/chatStore.test.ts b/desktop/src/stores/chatStore.test.ts index 7eeb9094..31853243 100644 --- a/desktop/src/stores/chatStore.test.ts +++ b/desktop/src/stores/chatStore.test.ts @@ -696,6 +696,7 @@ describe('chatStore history mapping', () => { expect(notifyDesktopMock).toHaveBeenCalledWith({ dedupeKey: 'permission:perm-ask-1', cooldownScope: 'permission-prompt', + requestAttention: true, title: 'Claude Code Haha 需要你的确认', body: 'AskUserQuestion 请求执行,正在等待允许。', }) @@ -1009,6 +1010,7 @@ describe('chatStore history mapping', () => { expect(notifyDesktopMock).toHaveBeenCalledWith({ dedupeKey: 'computer-use-permission:cu-1', cooldownScope: 'permission-prompt', + requestAttention: true, title: 'Claude Code Haha 需要你的确认', body: 'Open Finder and inspect a file', }) diff --git a/desktop/src/stores/chatStore.ts b/desktop/src/stores/chatStore.ts index 00550aae..5aacb2a0 100644 --- a/desktop/src/stores/chatStore.ts +++ b/desktop/src/stores/chatStore.ts @@ -674,6 +674,7 @@ export const useChatStore = create((set, get) => ({ notifyDesktop({ dedupeKey: `permission:${msg.requestId}`, cooldownScope: 'permission-prompt', + requestAttention: true, title: 'Claude Code Haha 需要你的确认', body: msg.toolName ? `${msg.toolName} 请求执行,正在等待允许。` @@ -710,6 +711,7 @@ export const useChatStore = create((set, get) => ({ notifyDesktop({ dedupeKey: `computer-use-permission:${msg.requestId}`, cooldownScope: 'permission-prompt', + requestAttention: true, title: 'Claude Code Haha 需要你的确认', body: msg.request.reason || 'Computer Use 正在等待允许。', })