fix(desktop): forward Windows PowerShell choice to agent sidecar

- 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>
This commit is contained in:
程序员阿江(Relakkes) 2026-06-03 14:05:29 +08:00
parent f070d16d38
commit 7a882d1858
4 changed files with 68 additions and 4 deletions

View File

@ -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 {

View File

@ -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<NodeJS.ProcessEnv> {
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
}
}

View File

@ -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()
})
})

View File

@ -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,