mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
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
This commit is contained in:
parent
f1c5f86b80
commit
0a82efdcd9
@ -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",
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -8,6 +8,7 @@ export type DesktopNotificationOptions = {
|
||||
dedupeKey?: string
|
||||
cooldownScope?: string
|
||||
cooldownMs?: number
|
||||
requestAttention?: boolean
|
||||
}
|
||||
|
||||
type NativeNotificationSender = (options: { title: string; body?: string }) => Promise<boolean> | boolean
|
||||
@ -108,6 +109,16 @@ async function sendNativeNotification(options: { title: string; body?: string })
|
||||
return true
|
||||
}
|
||||
|
||||
async function requestWindowAttention(): Promise<boolean> {
|
||||
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') {
|
||||
|
||||
@ -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',
|
||||
})
|
||||
|
||||
@ -674,6 +674,7 @@ export const useChatStore = create<ChatStore>((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<ChatStore>((set, get) => ({
|
||||
notifyDesktop({
|
||||
dedupeKey: `computer-use-permission:${msg.requestId}`,
|
||||
cooldownScope: 'permission-prompt',
|
||||
requestAttention: true,
|
||||
title: 'Claude Code Haha 需要你的确认',
|
||||
body: msg.request.reason || 'Computer Use 正在等待允许。',
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user