mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-23 14:33:36 +08:00
fix: restore Windows desktop notifications
This commit is contained in:
parent
dfb69976fd
commit
a0624eeb21
@ -830,6 +830,26 @@ async fn macos_send_notification(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn open_windows_notification_settings() -> Result<bool, String> {
|
||||||
|
open_windows_notification_settings_impl()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn open_windows_notification_settings_impl() -> Result<bool, String> {
|
||||||
|
StdCommand::new("explorer.exe")
|
||||||
|
.arg("ms-settings:notifications")
|
||||||
|
.spawn()
|
||||||
|
.map_err(|err| format!("open Windows notification settings: {err}"))?;
|
||||||
|
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn open_windows_notification_settings_impl() -> Result<bool, String> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
async fn run_notification_bridge<T, F>(operation: F) -> Result<T, String>
|
async fn run_notification_bridge<T, F>(operation: F) -> Result<T, String>
|
||||||
where
|
where
|
||||||
T: Send + 'static,
|
T: Send + 'static,
|
||||||
@ -1533,7 +1553,8 @@ pub fn run() {
|
|||||||
terminal_kill,
|
terminal_kill,
|
||||||
macos_notification_permission_state,
|
macos_notification_permission_state,
|
||||||
macos_request_notification_permission,
|
macos_request_notification_permission,
|
||||||
macos_send_notification
|
macos_send_notification,
|
||||||
|
open_windows_notification_settings
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// macOS: native menu bar (traffic-light overlay style)
|
// macOS: native menu bar (traffic-light overlay style)
|
||||||
|
|||||||
@ -218,7 +218,7 @@ describe('Settings > General tab', () => {
|
|||||||
})
|
})
|
||||||
expect(desktopNotificationsMock.notifyDesktop).toHaveBeenCalledWith({
|
expect(desktopNotificationsMock.notifyDesktop).toHaveBeenCalledWith({
|
||||||
title: 'Claude Code Haha notifications are enabled',
|
title: 'Claude Code Haha notifications are enabled',
|
||||||
body: 'Permission prompts and completed agent replies will now use macOS notifications.',
|
body: 'Permission prompts and completed agent replies will now use system notifications.',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -655,7 +655,7 @@ export const en = {
|
|||||||
'settings.general.notificationsAuthorize': 'Authorize',
|
'settings.general.notificationsAuthorize': 'Authorize',
|
||||||
'settings.general.notificationsOpenSettings': 'Open Settings',
|
'settings.general.notificationsOpenSettings': 'Open Settings',
|
||||||
'settings.general.notificationsTestTitle': 'Claude Code Haha notifications are enabled',
|
'settings.general.notificationsTestTitle': 'Claude Code Haha notifications are enabled',
|
||||||
'settings.general.notificationsTestBody': 'Permission prompts and completed agent replies will now use macOS notifications.',
|
'settings.general.notificationsTestBody': 'Permission prompts and completed agent replies will now use system notifications.',
|
||||||
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
|
'settings.general.webFetchPreflightTitle': 'WebFetch Preflight',
|
||||||
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
|
'settings.general.webFetchPreflightDescription': 'Desktop sessions skip Claude\'s domain preflight by default to avoid false failures on third-party providers and restricted networks.',
|
||||||
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',
|
'settings.general.webFetchPreflightEnabled': 'Skip WebFetch domain preflight',
|
||||||
|
|||||||
@ -657,7 +657,7 @@ export const zh: Record<TranslationKey, string> = {
|
|||||||
'settings.general.notificationsAuthorize': '授权通知',
|
'settings.general.notificationsAuthorize': '授权通知',
|
||||||
'settings.general.notificationsOpenSettings': '打开系统设置',
|
'settings.general.notificationsOpenSettings': '打开系统设置',
|
||||||
'settings.general.notificationsTestTitle': 'Claude Code Haha 通知已启用',
|
'settings.general.notificationsTestTitle': 'Claude Code Haha 通知已启用',
|
||||||
'settings.general.notificationsTestBody': '后续授权确认和 Agent 回复完成都会通过 macOS 系统通知提醒。',
|
'settings.general.notificationsTestBody': '后续授权确认和 Agent 回复完成都会通过系统通知提醒。',
|
||||||
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
|
'settings.general.webFetchPreflightTitle': 'WebFetch 预检',
|
||||||
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
|
'settings.general.webFetchPreflightDescription': '桌面端默认跳过 Claude 的域名预检,避免第三方服务商或受限网络下出现误报失败。',
|
||||||
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',
|
'settings.general.webFetchPreflightEnabled': '跳过 WebFetch 域名预检',
|
||||||
|
|||||||
@ -12,6 +12,9 @@ const coreApiMock = vi.hoisted(() => ({
|
|||||||
const eventApiMock = vi.hoisted(() => ({
|
const eventApiMock = vi.hoisted(() => ({
|
||||||
listen: vi.fn(),
|
listen: vi.fn(),
|
||||||
}))
|
}))
|
||||||
|
const shellApiMock = vi.hoisted(() => ({
|
||||||
|
open: vi.fn(),
|
||||||
|
}))
|
||||||
const requestUserAttentionMock = vi.hoisted(() => vi.fn())
|
const requestUserAttentionMock = vi.hoisted(() => vi.fn())
|
||||||
const windowApiMock = vi.hoisted(() => ({
|
const windowApiMock = vi.hoisted(() => ({
|
||||||
requestUserAttention: requestUserAttentionMock,
|
requestUserAttention: requestUserAttentionMock,
|
||||||
@ -28,11 +31,13 @@ vi.mock('@tauri-apps/plugin-notification', () => notificationPluginMock)
|
|||||||
vi.mock('@tauri-apps/api/core', () => coreApiMock)
|
vi.mock('@tauri-apps/api/core', () => coreApiMock)
|
||||||
vi.mock('@tauri-apps/api/event', () => eventApiMock)
|
vi.mock('@tauri-apps/api/event', () => eventApiMock)
|
||||||
vi.mock('@tauri-apps/api/window', () => windowApiMock)
|
vi.mock('@tauri-apps/api/window', () => windowApiMock)
|
||||||
|
vi.mock('@tauri-apps/plugin-shell', () => shellApiMock)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDesktopNotificationPermission,
|
getDesktopNotificationPermission,
|
||||||
installDesktopNotificationClickListener,
|
installDesktopNotificationClickListener,
|
||||||
notifyDesktop,
|
notifyDesktop,
|
||||||
|
openDesktopNotificationSettings,
|
||||||
requestDesktopNotificationPermission,
|
requestDesktopNotificationPermission,
|
||||||
resetDesktopNotificationsForTests,
|
resetDesktopNotificationsForTests,
|
||||||
setNativeNotificationSenderForTests,
|
setNativeNotificationSenderForTests,
|
||||||
@ -45,6 +50,7 @@ describe('desktopNotifications', () => {
|
|||||||
resetDesktopNotificationsForTests()
|
resetDesktopNotificationsForTests()
|
||||||
coreApiMock.invoke.mockReset()
|
coreApiMock.invoke.mockReset()
|
||||||
eventApiMock.listen.mockReset()
|
eventApiMock.listen.mockReset()
|
||||||
|
shellApiMock.open.mockReset()
|
||||||
notificationPluginMock.isPermissionGranted.mockReset()
|
notificationPluginMock.isPermissionGranted.mockReset()
|
||||||
notificationPluginMock.requestPermission.mockReset()
|
notificationPluginMock.requestPermission.mockReset()
|
||||||
notificationPluginMock.sendNotification.mockReset()
|
notificationPluginMock.sendNotification.mockReset()
|
||||||
@ -231,6 +237,57 @@ describe('desktopNotifications', () => {
|
|||||||
expect(notificationPluginMock.requestPermission).toHaveBeenCalledTimes(1)
|
expect(notificationPluginMock.requestPermission).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('reads and requests Windows notification permission through the native plugin command', async () => {
|
||||||
|
Object.defineProperty(navigator, 'platform', {
|
||||||
|
configurable: true,
|
||||||
|
value: 'Win32',
|
||||||
|
})
|
||||||
|
coreApiMock.invoke
|
||||||
|
.mockResolvedValueOnce(true)
|
||||||
|
.mockResolvedValueOnce('granted')
|
||||||
|
|
||||||
|
await expect(getDesktopNotificationPermission()).resolves.toBe('granted')
|
||||||
|
await expect(requestDesktopNotificationPermission()).resolves.toBe('granted')
|
||||||
|
|
||||||
|
expect(coreApiMock.invoke).toHaveBeenNthCalledWith(1, 'plugin:notification|is_permission_granted')
|
||||||
|
expect(coreApiMock.invoke).toHaveBeenNthCalledWith(2, 'plugin:notification|request_permission')
|
||||||
|
expect(notificationPluginMock.isPermissionGranted).not.toHaveBeenCalled()
|
||||||
|
expect(notificationPluginMock.requestPermission).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends Windows notifications when the native plugin reports permission granted', async () => {
|
||||||
|
Object.defineProperty(navigator, 'platform', {
|
||||||
|
configurable: true,
|
||||||
|
value: 'Win32',
|
||||||
|
})
|
||||||
|
coreApiMock.invoke.mockResolvedValueOnce(true)
|
||||||
|
|
||||||
|
await expect(notifyDesktop({
|
||||||
|
title: 'Permission required',
|
||||||
|
body: 'Approve command execution',
|
||||||
|
})).resolves.toBe(true)
|
||||||
|
|
||||||
|
expect(coreApiMock.invoke).toHaveBeenCalledWith('plugin:notification|is_permission_granted')
|
||||||
|
expect(notificationPluginMock.isPermissionGranted).not.toHaveBeenCalled()
|
||||||
|
expect(notificationPluginMock.sendNotification).toHaveBeenCalledWith({
|
||||||
|
title: 'Permission required',
|
||||||
|
body: 'Approve command execution',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('opens Windows notification settings through the native command', async () => {
|
||||||
|
Object.defineProperty(navigator, 'platform', {
|
||||||
|
configurable: true,
|
||||||
|
value: 'Win32',
|
||||||
|
})
|
||||||
|
coreApiMock.invoke.mockResolvedValueOnce(true)
|
||||||
|
|
||||||
|
await expect(openDesktopNotificationSettings()).resolves.toBe(true)
|
||||||
|
|
||||||
|
expect(coreApiMock.invoke).toHaveBeenCalledWith('open_windows_notification_settings')
|
||||||
|
expect(shellApiMock.open).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('reports and requests macOS notification permission through the native bridge', async () => {
|
it('reports and requests macOS notification permission through the native bridge', async () => {
|
||||||
Object.defineProperty(navigator, 'platform', {
|
Object.defineProperty(navigator, 'platform', {
|
||||||
configurable: true,
|
configurable: true,
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type NativeNotificationPayload = {
|
|||||||
|
|
||||||
type NativeNotificationSender = (options: NativeNotificationPayload) => Promise<boolean> | boolean
|
type NativeNotificationSender = (options: NativeNotificationPayload) => Promise<boolean> | boolean
|
||||||
export type DesktopNotificationPermission = NotificationPermission | 'unsupported'
|
export type DesktopNotificationPermission = NotificationPermission | 'unsupported'
|
||||||
|
type PluginPermissionState = DesktopNotificationPermission | 'prompt' | 'prompt-with-rationale'
|
||||||
|
|
||||||
const TARGET_EXTRA_KEY = 'ccHahaTarget'
|
const TARGET_EXTRA_KEY = 'ccHahaTarget'
|
||||||
const notifiedKeys = new Set<string>()
|
const notifiedKeys = new Set<string>()
|
||||||
@ -46,10 +47,12 @@ function readBrowserNotificationPermission(): DesktopNotificationPermission {
|
|||||||
function detectPlatform(): 'darwin' | 'win32' | 'linux' | 'unknown' {
|
function detectPlatform(): 'darwin' | 'win32' | 'linux' | 'unknown' {
|
||||||
const platform = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase() : ''
|
const platform = typeof navigator !== 'undefined' ? navigator.platform.toLowerCase() : ''
|
||||||
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''
|
const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent.toLowerCase() : ''
|
||||||
const raw = `${platform} ${userAgent}`
|
if (platform.includes('mac')) return 'darwin'
|
||||||
if (raw.includes('mac')) return 'darwin'
|
if (platform.includes('win')) return 'win32'
|
||||||
if (raw.includes('win')) return 'win32'
|
if (platform.includes('linux')) return 'linux'
|
||||||
if (raw.includes('linux')) return 'linux'
|
if (userAgent.includes('mac')) return 'darwin'
|
||||||
|
if (userAgent.includes('win')) return 'win32'
|
||||||
|
if (userAgent.includes('linux')) return 'linux'
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,6 +67,46 @@ function getNotificationSettingsUrl(): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizePermission(value: unknown): DesktopNotificationPermission {
|
||||||
|
if (value === true) return 'granted'
|
||||||
|
if (value === false) return 'denied'
|
||||||
|
if (value === null) return 'default'
|
||||||
|
if (value === 'prompt' || value === 'prompt-with-rationale') return 'default'
|
||||||
|
return ['default', 'denied', 'granted', 'unsupported'].includes(value as string)
|
||||||
|
? value as DesktopNotificationPermission
|
||||||
|
: 'unsupported'
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeWindowsNotificationPermissionState(): Promise<DesktopNotificationPermission | null> {
|
||||||
|
if (detectPlatform() !== 'win32') return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { invoke } = await import('@tauri-apps/api/core')
|
||||||
|
const granted = await invoke<boolean | null>('plugin:notification|is_permission_granted')
|
||||||
|
return normalizePermission(granted)
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof console !== 'undefined') {
|
||||||
|
console.warn('[desktopNotifications] failed to read Windows notification permission:', err)
|
||||||
|
}
|
||||||
|
return 'unsupported'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function invokeWindowsNotificationPermissionRequest(): Promise<DesktopNotificationPermission | null> {
|
||||||
|
if (detectPlatform() !== 'win32') return null
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { invoke } = await import('@tauri-apps/api/core')
|
||||||
|
const permission = await invoke<PluginPermissionState>('plugin:notification|request_permission')
|
||||||
|
return normalizePermission(permission)
|
||||||
|
} catch (err) {
|
||||||
|
if (typeof console !== 'undefined') {
|
||||||
|
console.warn('[desktopNotifications] failed to request Windows notification permission:', err)
|
||||||
|
}
|
||||||
|
return 'unsupported'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function invokeMacNotificationPermissionState(): Promise<DesktopNotificationPermission | null> {
|
async function invokeMacNotificationPermissionState(): Promise<DesktopNotificationPermission | null> {
|
||||||
if (detectPlatform() !== 'darwin') return null
|
if (detectPlatform() !== 'darwin') return null
|
||||||
|
|
||||||
@ -187,6 +230,9 @@ export async function getDesktopNotificationPermission(): Promise<DesktopNotific
|
|||||||
const macPermission = await invokeMacNotificationPermissionState()
|
const macPermission = await invokeMacNotificationPermissionState()
|
||||||
if (macPermission) return macPermission
|
if (macPermission) return macPermission
|
||||||
|
|
||||||
|
const windowsPermission = await invokeWindowsNotificationPermissionState()
|
||||||
|
if (windowsPermission) return windowsPermission
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { isPermissionGranted } = await import('@tauri-apps/plugin-notification')
|
const { isPermissionGranted } = await import('@tauri-apps/plugin-notification')
|
||||||
if (await isPermissionGranted()) return 'granted'
|
if (await isPermissionGranted()) return 'granted'
|
||||||
@ -200,6 +246,9 @@ export async function requestDesktopNotificationPermission(): Promise<DesktopNot
|
|||||||
const macPermission = await invokeMacNotificationPermissionRequest()
|
const macPermission = await invokeMacNotificationPermissionRequest()
|
||||||
if (macPermission) return macPermission
|
if (macPermission) return macPermission
|
||||||
|
|
||||||
|
const windowsPermission = await invokeWindowsNotificationPermissionRequest()
|
||||||
|
if (windowsPermission) return windowsPermission
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
isPermissionGranted,
|
isPermissionGranted,
|
||||||
@ -217,6 +266,16 @@ export async function openDesktopNotificationSettings(): Promise<boolean> {
|
|||||||
const url = getNotificationSettingsUrl()
|
const url = getNotificationSettingsUrl()
|
||||||
if (!url) return false
|
if (!url) return false
|
||||||
|
|
||||||
|
if (detectPlatform() === 'win32') {
|
||||||
|
try {
|
||||||
|
const { invoke } = await import('@tauri-apps/api/core')
|
||||||
|
const opened = await invoke<boolean>('open_windows_notification_settings')
|
||||||
|
if (opened) return true
|
||||||
|
} catch {
|
||||||
|
// Fall back to shell.open/window.open below.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { open } = await import('@tauri-apps/plugin-shell')
|
const { open } = await import('@tauri-apps/plugin-shell')
|
||||||
await open(url)
|
await open(url)
|
||||||
@ -240,7 +299,9 @@ async function sendNativeNotification(options: { title: string; body?: string; t
|
|||||||
sendNotification,
|
sendNotification,
|
||||||
} = await import('@tauri-apps/plugin-notification')
|
} = await import('@tauri-apps/plugin-notification')
|
||||||
|
|
||||||
if (!(await isPermissionGranted())) {
|
const windowsPermission = await invokeWindowsNotificationPermissionState()
|
||||||
|
const permissionGranted = windowsPermission ? windowsPermission === 'granted' : await isPermissionGranted()
|
||||||
|
if (!permissionGranted) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user