diff --git a/app/src/main/hl/engines/pathEnrich.ts b/app/src/main/hl/engines/pathEnrich.ts index 6f47b2e8..e918afe9 100644 --- a/app/src/main/hl/engines/pathEnrich.ts +++ b/app/src/main/hl/engines/pathEnrich.ts @@ -246,14 +246,15 @@ function findOnWindowsPath(name: string, env: NodeJS.ProcessEnv): string | null // PATHEXT is the canonical search order. We always check `.exe` first so // a native binary wins over an npm shim with the same stem. const pathExt = (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').map((e) => e.toLowerCase()).filter(Boolean); - // Always include .ps1 since npm-installed global CLIs may ship as PowerShell - // scripts on Windows (e.g. Claude Code's claude.ps1). Insert it before .bat/.cmd - // so native binaries still win, but working .ps1 shims take precedence over - // potentially broken/dummy .cmd stubs generated by older npm versions. + // npm-installed global CLIs typically ship .cmd, .ps1, AND a sh wrapper + // (e.g. claude.cmd + claude.ps1). When both .cmd and .ps1 are present we + // route through cmd.exe — it's already on COMSPEC, doesn't depend on + // env.Path-based powershell.exe lookup (which has been observed to fail + // with ENOENT inside Electron's main process), and avoids PowerShell's + // cold-start cost (~1-2s + Defender script scan). .ps1 stays as a + // fallback for packages that ship only PowerShell shims. if (!pathExt.includes('.ps1')) { - const batIdx = pathExt.findIndex((e) => e === '.bat' || e === '.cmd'); - if (batIdx >= 0) pathExt.splice(batIdx, 0, '.ps1'); - else pathExt.push('.ps1'); + pathExt.push('.ps1'); } const exts = name.includes('.') && pathExt.includes(path.win32.extname(name).toLowerCase()) ? [''] @@ -313,8 +314,16 @@ export function resolveCliSpawn( if (ext === '.ps1') { // PowerShell scripts: route through powershell.exe so execution policy // and profile loading don't block npm-installed CLIs like claude.ps1. + // Use the absolute path under %SystemRoot% rather than relying on + // env.Path / Path: Electron's main process has been observed to spawn + // with `spawn powershell.exe ENOENT` even when the WindowsPowerShell + // dir is on the system PATH, because libuv on Windows reads the env + // block's `Path` key (capitalized) and our enriched env may only + // expose `PATH`. The absolute path sidesteps that lookup entirely. + const systemRoot = env.SystemRoot ?? env.SYSTEMROOT ?? 'C:\\Windows'; + const absPwsh = path.win32.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'); return { - command: 'powershell.exe', + command: absPwsh, args: ['-ExecutionPolicy', 'Bypass', '-NoProfile', '-File', resolved, ...args], viaCmdShell: false, spawnOptions: {}, diff --git a/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd b/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd new file mode 100644 index 00000000..e91e699d --- /dev/null +++ b/app/src/main/hl/stock/browser-harness-js/sdk/browser-harness-js.cmd @@ -0,0 +1,44 @@ +@echo off +rem browser-harness-js Windows launcher — runs the bundled bash script under +rem Git for Windows' bash.exe. The script is POSIX-only (curl, nohup, /tmp, +rem bun bootstrap) so we delegate to bash rather than re-implement in cmd.exe. +rem +rem Discovery order: +rem 1. %BROWSER_HARNESS_JS_BASH% (explicit env override) +rem 2. %ProgramFiles%\Git\bin\bash.exe +rem 3. %ProgramFiles(x86)%\Git\bin\bash.exe +rem 4. %LocalAppData%\Programs\Git\bin\bash.exe (per-user install) +rem +rem Without Git for Windows, exits 1 with a stderr hint so the agent sees a +rem clean error instead of triggering Windows' "Open with..." association +rem dialog on the extensionless bash script next to this file. + +setlocal + +set "SCRIPT_DIR=%~dp0" +set "BASH_SCRIPT=%SCRIPT_DIR%browser-harness-js" + +rem `exit /b` (no arg) propagates the *current* ERRORLEVEL. Inside a +rem parenthesized block, `exit /b %errorlevel%` would expand at parse time +rem and silently report 0 even if bash failed. + +if defined BROWSER_HARNESS_JS_BASH ( + if exist "%BROWSER_HARNESS_JS_BASH%" ( + "%BROWSER_HARNESS_JS_BASH%" "%BASH_SCRIPT%" %* + exit /b + ) +) + +for %%P in ( + "%ProgramFiles%\Git\bin\bash.exe" + "%ProgramFiles(x86)%\Git\bin\bash.exe" + "%LocalAppData%\Programs\Git\bin\bash.exe" +) do ( + if exist %%P ( + %%P "%BASH_SCRIPT%" %* + exit /b + ) +) + +>&2 echo browser-harness-js: bash.exe not found. Install Git for Windows from https://gitforwindows.org/ or set BROWSER_HARNESS_JS_BASH to a bash.exe path. +exit /b 1 diff --git a/app/src/renderer/hub/ConnectionsPane.tsx b/app/src/renderer/hub/ConnectionsPane.tsx index 417cd7ec..881d0497 100644 --- a/app/src/renderer/hub/ConnectionsPane.tsx +++ b/app/src/renderer/hub/ConnectionsPane.tsx @@ -1181,6 +1181,15 @@ export function ConnectionsPane({ + {/* + Cookie sync is unsupported on Windows: Chromium 127+ uses App-Bound + Encryption (v20) tied to the original user-data-dir, so a temp-copy + profile decrypts to nothing, and launching headless against the real + profile is blocked by the Chromium DevTools hardening that refuses + --remote-debugging-port for the default user-data-dir. Hide the + section entirely on win32 until we have a native v20 decryption path. + */} + {window.electronAPI?.shell?.platform !== 'win32' && (
)}
+ )} ); } diff --git a/app/src/renderer/hub/SettingsPane.tsx b/app/src/renderer/hub/SettingsPane.tsx index 629f5307..8d1fe693 100644 --- a/app/src/renderer/hub/SettingsPane.tsx +++ b/app/src/renderer/hub/SettingsPane.tsx @@ -418,6 +418,11 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding, const scrollerRef = useRef(null); const [activeSection, setActiveSection] = useState('settings-application'); const platform = window.electronAPI?.shell?.platform ?? fallbackShortcutPlatform(); + // Cookie sync is unsupported on Windows (Chromium ABE + DevTools hardening), + // so the Browser Sync tab + section are hidden on win32. + const tabs = platform === 'win32' + ? SETTINGS_TABS.filter((tab) => tab.id !== 'settings-browser-sync') + : SETTINGS_TABS; const scrollToSection = useCallback((id: SettingsSectionId, behavior: ScrollBehavior = 'smooth') => { const scroller = scrollerRef.current; @@ -434,14 +439,14 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding, const updateActiveFromScroll = useCallback(() => { const scroller = scrollerRef.current; if (!scroller) return; - let next = SETTINGS_TABS[0].id; + let next = tabs[0].id; const threshold = scroller.scrollTop + 112; - for (const tab of SETTINGS_TABS) { + for (const tab of tabs) { const section = scroller.querySelector(`#${tab.id}`); if (section && section.offsetTop <= threshold) next = tab.id; } setActiveSection(next); - }, []); + }, [tabs]); useEffect(() => { const sectionId = intent?.sectionId ?? ( @@ -467,7 +472,7 @@ export function SettingsPane({ intent, keybindings, overrides, onUpdateBinding,