diff --git a/desktop/electron/services/notifications.ts b/desktop/electron/services/notifications.ts index 68156ab4..e028fee6 100644 --- a/desktop/electron/services/notifications.ts +++ b/desktop/electron/services/notifications.ts @@ -23,6 +23,15 @@ export function validateNotificationOptions(value: unknown): value is DesktopNot && (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 { diff --git a/desktop/electron/services/serverRuntime.ts b/desktop/electron/services/serverRuntime.ts index a3c1e815..b4a6475e 100644 --- a/desktop/electron/services/serverRuntime.ts +++ b/desktop/electron/services/serverRuntime.ts @@ -5,6 +5,7 @@ import { formatStartupError, killSidecar, mergeProxyEnv, + POWERSHELL_PATH_OVERRIDE_ENV, proxyUrlFromElectronProxyRules, pushStartupLog, reserveLocalPort, @@ -12,8 +13,10 @@ import { SERVER_CONTROL_HOST, spawnSidecar, waitForServer, + windowsPowerShellOverride, type SidecarChild, } from './sidecarManager' +import { readDesktopTerminalConfig, resolveDesktopTerminalShell } from './terminal' type ServerRuntimeOptions = { desktopRoot: string @@ -157,17 +160,33 @@ export class ElectronServerRuntime { } private async resolveSidecarBaseEnvOnce(): Promise { - if (!this.resolveSystemProxy) return process.env + if (!this.resolveSystemProxy) return this.applyPowerShellOverride(process.env) try { const rules = await this.resolveSystemProxy('https://auth.openai.com/') - return mergeProxyEnv( + return this.applyPowerShellOverride(mergeProxyEnv( process.env, proxyUrlFromElectronProxyRules(rules), - ) + )) } catch (error) { console.error('[desktop] failed to resolve system proxy for sidecars', error) - return process.env + return this.applyPowerShellOverride(process.env) } } + + // On Windows, forward the user's chosen PowerShell to the agent sidecar so its + // PowerShellTool honors the same shell as the UI terminal (regression from the + // Tauri build, where this lived in src-tauri/src/lib.rs). Best-effort: never + // block sidecar startup, and never override an explicitly set env var. + private applyPowerShellOverride(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + if (process.platform !== 'win32' || env[POWERSHELL_PATH_OVERRIDE_ENV]) return env + try { + const shell = resolveDesktopTerminalShell('win32', readDesktopTerminalConfig(env)) + const override = windowsPowerShellOverride(shell, 'win32') + if (override) return { ...env, [POWERSHELL_PATH_OVERRIDE_ENV]: override } + } catch { + // Misconfigured custom shell etc. — fall through to the unmodified env. + } + return env + } } diff --git a/desktop/electron/services/sidecarManager.test.ts b/desktop/electron/services/sidecarManager.test.ts index d8d7c3c3..6c1f5150 100644 --- a/desktop/electron/services/sidecarManager.test.ts +++ b/desktop/electron/services/sidecarManager.test.ts @@ -12,6 +12,7 @@ import { proxyUrlFromElectronProxyRules, pushStartupLog, resolveHostTriple, + windowsPowerShellOverride, type SidecarChild, } from './sidecarManager' @@ -148,4 +149,17 @@ describe('Electron sidecar manager', () => { expect(spawnSyncFn).toHaveBeenCalledWith('taskkill', ['/F', '/T', '/PID', '777'], { stdio: 'ignore' }) expect(spawnAsync).not.toHaveBeenCalled() }) + + it('forwards a PowerShell shell choice to the sidecar only on Windows', () => { + expect(windowsPowerShellOverride('pwsh.exe', 'win32')).toBe('pwsh.exe') + expect(windowsPowerShellOverride('powershell.exe', 'win32')).toBe('powershell.exe') + expect(windowsPowerShellOverride('C:\\tools\\PowerShell\\pwsh.exe', 'win32')).toBe('C:\\tools\\PowerShell\\pwsh.exe') + // non-PowerShell selections must not be reported as a PowerShell override + expect(windowsPowerShellOverride('cmd.exe', 'win32')).toBeNull() + expect(windowsPowerShellOverride('C:\\bin\\bash.exe', 'win32')).toBeNull() + expect(windowsPowerShellOverride(null, 'win32')).toBeNull() + // never applies off Windows + expect(windowsPowerShellOverride('pwsh', 'darwin')).toBeNull() + expect(windowsPowerShellOverride('powershell.exe', 'linux')).toBeNull() + }) }) diff --git a/desktop/electron/services/sidecarManager.ts b/desktop/electron/services/sidecarManager.ts index e78e63ab..3e8f9b0f 100644 --- a/desktop/electron/services/sidecarManager.ts +++ b/desktop/electron/services/sidecarManager.ts @@ -140,6 +140,28 @@ export function mergeProxyEnv( } } +// The agent's PowerShellTool reads this env var to honor the user's chosen shell +// (mirrors src/utils/shell/powershellDetection.ts). Without it the agent would +// re-autodetect PowerShell instead of using the shell the user picked in the UI. +export const POWERSHELL_PATH_OVERRIDE_ENV = 'CLAUDE_CODE_POWERSHELL_PATH' + +/** + * Map a resolved Windows shell path to a PowerShell override for the sidecar env. + * Returns the path only on Windows when it points at pwsh/powershell, so that a + * cmd.exe or non-PowerShell custom shell selection does not get misreported as a + * PowerShell override. Matches the consumer's isPowerShellExecutablePath check. + */ +export function windowsPowerShellOverride( + shellPath: string | null | undefined, + platform: NodeJS.Platform = process.platform, +): string | null { + if (platform !== 'win32') return null + const trimmed = shellPath?.trim() + if (!trimmed) return null + const base = trimmed.split(/[\\/]/).pop()?.toLowerCase().replace(/\.exe$/, '') + return base === 'pwsh' || base === 'powershell' ? trimmed : null +} + export function buildSidecarEnv(baseEnv: NodeJS.ProcessEnv, h5DistDir: string): NodeJS.ProcessEnv { const env: NodeJS.ProcessEnv = { ...baseEnv,