mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-18 13:23:33 +08:00
- serverRuntime injects CLAUDE_CODE_POWERSHELL_PATH from the user's chosen shell (readDesktopTerminalConfig + resolveDesktopTerminalShell) on Windows, so the agent PowerShellTool honors the same shell as the UI terminal. Best-effort: never blocks startup, never overrides an explicit env var, only forwards pwsh/powershell selections (not cmd/custom). Regression from the Tauri build. - document that notificationPermissionState reflects OS capability, not macOS authorization (no Electron API exists; the 'failed' lifecycle is the real signal) instead of calling the non-existent systemPreferences.getNotificationSettings Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import type { DesktopNotificationOptions, NotificationPermissionState } from '../../src/lib/desktopHost/types'
|
|
|
|
export type ElectronNotificationInstance = {
|
|
show(): void
|
|
on(event: 'click' | 'close' | 'failed', handler: () => void): ElectronNotificationInstance
|
|
}
|
|
|
|
export type ElectronNotificationConstructor = {
|
|
new(options: { title: string, body?: string, icon?: string }): ElectronNotificationInstance
|
|
isSupported(): boolean
|
|
}
|
|
|
|
const activeNotifications = new Set<ElectronNotificationInstance>()
|
|
|
|
export function validateNotificationOptions(value: unknown): value is DesktopNotificationOptions {
|
|
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
const record = value as Record<string, unknown>
|
|
return typeof record.title === 'string'
|
|
&& record.title.trim().length > 0
|
|
&& (record.body === undefined || typeof record.body === 'string')
|
|
&& (record.icon === undefined || typeof record.icon === 'string')
|
|
&& (record.id === undefined || typeof record.id === 'number')
|
|
&& (record.extra === undefined || (typeof record.extra === 'object' && record.extra !== null && !Array.isArray(record.extra)))
|
|
}
|
|
|
|
// NOTE: Electron has no API to read the macOS user authorization status from the
|
|
// main process (`systemPreferences.getNotificationSettings()` does not exist; the
|
|
// only option is the native `macos-notification-state` module, which we avoid to
|
|
// keep the cross-platform build dependency-free). `Notification.isSupported()`
|
|
// reflects OS capability, NOT whether the user granted permission, so this can
|
|
// report 'granted' even when the user denied notifications in System Settings.
|
|
// The authoritative delivery signal is the 'failed' lifecycle event handled in
|
|
// sendDesktopNotification(); on macOS, events only emit correctly for code-signed
|
|
// builds (unsigned/ad-hoc builds emit 'failed').
|
|
export function notificationPermissionState(
|
|
NotificationClass: ElectronNotificationConstructor,
|
|
): NotificationPermissionState {
|
|
return NotificationClass.isSupported() ? 'granted' : 'denied'
|
|
}
|
|
|
|
export function requestNotificationPermission(
|
|
NotificationClass: ElectronNotificationConstructor,
|
|
): NotificationPermissionState {
|
|
return notificationPermissionState(NotificationClass)
|
|
}
|
|
|
|
export function sendDesktopNotification({
|
|
NotificationClass,
|
|
options,
|
|
onAction,
|
|
onLifecycle,
|
|
}: {
|
|
NotificationClass: ElectronNotificationConstructor
|
|
options: unknown
|
|
onAction: (payload: unknown) => void
|
|
onLifecycle?: (event: 'close' | 'failed') => void
|
|
}): boolean {
|
|
if (!validateNotificationOptions(options)) {
|
|
throw new Error('Invalid Electron notification payload')
|
|
}
|
|
if (!NotificationClass.isSupported()) return false
|
|
|
|
const notification = new NotificationClass({
|
|
title: options.title,
|
|
body: options.body,
|
|
icon: options.icon,
|
|
})
|
|
|
|
activeNotifications.add(notification)
|
|
const cleanup = () => {
|
|
activeNotifications.delete(notification)
|
|
}
|
|
notification.on('click', () => {
|
|
onAction({
|
|
id: options.id,
|
|
extra: options.extra,
|
|
target: options.target,
|
|
action: 'click',
|
|
})
|
|
cleanup()
|
|
})
|
|
notification.on('close', () => {
|
|
onLifecycle?.('close')
|
|
cleanup()
|
|
})
|
|
notification.on('failed', () => {
|
|
onLifecycle?.('failed')
|
|
cleanup()
|
|
})
|
|
notification.show()
|
|
return true
|
|
}
|