From 7e643e974bbe3c66b0851983cae048cddbf78d15 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Tue, 12 May 2026 15:43:12 +0530 Subject: [PATCH 1/4] fix(windows): wire CEF keyboard input routing on cold launch On Windows + CEF, `tauri.conf.json` declares the main window with `visible: false` (so we can restore window state before the first paint). When `setup()` later calls `window.show()`, Windows transitions the window to the foreground but does not synthesize the `WM_SETFOCUS` that CEF's window subclass needs in order to fire `BrowserHost::OnSetFocus(true)` and propagate the focused state to the renderer. The renderer's `is_keyboard_input_target` flag is left in its initial `false` state. The user-visible symptom: cold launch -> click chat textarea -> cursor blinks (mouse routing works) but typing is silently dropped. The only known workaround was to click outside the app window and click back, which produces a real `WM_KILLFOCUS`+`WM_SETFOCUS` cycle that CEF's window handler observes as a state transition. Calling `webview.set_focus()` from Rust does not fix this: it dispatches `WebviewMessage::SetFocus` -> `host.set_focus(1)`, which is idempotent from CEF's host point of view (the host already considers itself focused because the OS window is foreground). CEF's renderer only wires keyboard routing on an observed state *transition*, not a state assertion. The fix mimics the manual click-outside / click-back gesture in code: 300ms after `window.show()`, spawn an async task that `minimize()`s the window (forces `WM_KILLFOCUS` -> `host.set_focus(0)`), waits 80ms for Windows to process the message, then `unminimize()`s (forces `WM_SETFOCUS` -> `host.set_focus(1)`). Trailing explicit `window.set_focus()` + `webview.set_focus()` calls serve as belt-and-suspenders in case the minimize/restore raced ahead of CEF's browser-create. Side effect: a brief minimize/restore animation (~120ms) immediately after the window first appears on cold launch. A cleaner fix would expose `BrowserHost::SetFocus(false)` in the vendored tauri-cef so we could cycle focus without touching window state, but that requires vendor surgery; this is the minimum viable change. Also pulls in the openhuman-1475 worktree's PATH/MSVC improvements to `scripts/run-dev-win.sh` so a clean checkout can `pnpm dev:app:win` without a pre-warm cache: probes Git-for-Windows install for cygpath when not on PATH, restores the Windows-side PATH that `/etc/profile` would otherwise wipe, and prepends the VS Installer dir to cmd's PATH before invoking vcvars64 (so vswhere is reachable and the Windows SDK LIB/INCLUDE entries land in the captured env). Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/src/lib.rs | 90 ++++++++++ scripts/run-dev-win.sh | 347 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 430 insertions(+), 7 deletions(-) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 7b15471b40..f86b0c52d0 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -835,6 +835,25 @@ fn show_main_window(app: &AppHandle) -> Result<(), String> { window .set_focus() .map_err(|err| format!("failed to focus main window: {err}"))?; + // `WebviewWindow::set_focus` only dispatches `WindowMessage::SetFocus` + // (vendor/tauri-cef cef_impl.rs `WindowMessage::SetFocus` → `window.request_focus()`), + // which lifts the OS window but does NOT call `CefBrowserHost::SetFocus(true)`. + // Without that CEF-level focus call the renderer never gets wired as the + // keyboard input target on cold launch: the chat textarea accepts focus + // (cursor blinks) but `WM_KEYDOWN` messages aren't forwarded to it, so + // typing is silently dead until the user click-outside / click-back + // triggers `WM_KILLFOCUS`+`WM_SETFOCUS` and CEF's window handler routes + // through `host.set_focus(1)` internally. + // + // Explicitly dispatch `WebviewMessage::SetFocus` (cef_impl.rs line ~2081), + // which is what actually invokes `CefBrowserHost::SetFocus(true)`. + let webview: &tauri::Webview = window.as_ref(); + if let Err(err) = webview.set_focus() { + log::warn!( + "[show_main_window] CEF webview set_focus failed (non-fatal — \ + keyboard routing may not initialize until user click-outside-and-back): {err}" + ); + } Ok(()) } #[cfg(target_os = "linux")] @@ -1595,6 +1614,77 @@ pub fn run() { if let Err(err) = window.show() { log::warn!("[window-state] show main window failed: {err}"); } + // CEF keyboard routing fix — cold launch: + // + // `window.show()` does not wire the renderer as the + // keyboard input target. `Window::set_focus` only + // dispatches `WindowMessage::SetFocus` → `request_focus`, + // which lifts the OS window but does not call + // `CefBrowserHost::SetFocus(true)`. Without that + // CEF-level focus, the textarea accepts focus on cold + // launch (cursor blinks) but `WM_KEYDOWN` messages + // never reach the renderer — typing is silently dead + // until the user click-outside / click-back triggers + // `WM_KILLFOCUS`+`WM_SETFOCUS`, which CEF's window + // handler routes through `host.set_focus(1)` internally. + // + // We need to call `webview.set_focus()` (which dispatches + // `WebviewMessage::SetFocus` → `host.set_focus(1)`) + // *after* CEF has finished creating the browser — too + // early and `browser()`/`host()` return None and the + // call silently no-ops. Defer the call to a spawned + // task with a small delay so CEF's browser-create + // settles. Then call it again after another delay as + // belt-and-suspenders for slower init paths. + // Previous attempts at calling `webview.set_focus()` alone + // confirmed the dispatch reaches CEF (both returned Ok), + // but keyboard routing stayed broken. `host.set_focus(1)` + // alone is insufficient — CEF's internal focus state + // needs a blur-then-focus *cycle* to wire keyboard + // routing on cold launch (matches the user-discovered + // workaround: click outside the window, then click back). + // + // The vendored tauri-cef doesn't expose `set_focus(false)`, + // so we mimic the cycle at the OS-window level: + // minimize triggers `WM_KILLFOCUS` (CEF's window handler + // propagates this to `host.set_focus(0)`), unminimize + // restores the window and triggers `WM_SETFOCUS` → + // `host.set_focus(1)`. Pair with explicit `set_focus` + // calls on both Window and Webview to cover the case + // where minimize/unminimize raced ahead of CEF's + // browser-create. + log::info!("[focus-fix] scheduling deferred CEF focus-cycle"); + let webview_window_clone = window.clone(); + tauri::async_runtime::spawn(async move { + // Wait for CEF to finish creating the browser host + // (synchronous setup() returns before this completes). + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + // Blur-then-focus cycle via minimize/unminimize. + // This is what the manual click-outside / click-back + // workaround does at the Win32 level. + log::info!("[focus-fix] starting minimize→unminimize focus cycle"); + if let Err(err) = webview_window_clone.minimize() { + log::warn!("[focus-fix] minimize failed: {err}"); + } + // Tiny pause so Windows actually processes the + // minimize before we ask to restore. + tokio::time::sleep(std::time::Duration::from_millis(80)).await; + if let Err(err) = webview_window_clone.unminimize() { + log::warn!("[focus-fix] unminimize failed: {err}"); + } + // Belt-and-suspenders: explicit Window + Webview focus + // after the cycle in case the minimize→restore path + // didn't propagate. + tokio::time::sleep(std::time::Duration::from_millis(40)).await; + if let Err(err) = webview_window_clone.set_focus() { + log::warn!("[focus-fix] post-cycle window.set_focus failed: {err}"); + } + let webview: &tauri::Webview = webview_window_clone.as_ref(); + if let Err(err) = webview.set_focus() { + log::warn!("[focus-fix] post-cycle webview.set_focus failed: {err}"); + } + log::info!("[focus-fix] focus cycle complete"); + }); } } diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh index a32ec8390e..93da0d7c2e 100644 --- a/scripts/run-dev-win.sh +++ b/scripts/run-dev-win.sh @@ -12,8 +12,24 @@ cd "$APP_DIR" # shellcheck source=../scripts/load-dotenv.sh source "$REPO_ROOT/scripts/load-dotenv.sh" +# When pnpm/PowerShell/cmd launch `bash.exe` directly, the spawned shell +# inherits the parent PATH and the MSYS utility directory (`Git\usr\bin`) +# may be absent — bash runs, but `cygpath`, `mktemp`, `grep`, `sort`, etc. +# are missing. Probe known Git-for-Windows install locations and prepend +# `usr/bin` so the rest of the script works regardless of launcher. if ! command -v cygpath >/dev/null 2>&1; then - echo "[run-dev-win] cygpath not found. Run this script from Git Bash or MSYS2." + for git_root in "/c/Program Files/Git" "/c/Program Files (x86)/Git"; do + if [[ -x "$git_root/usr/bin/cygpath.exe" ]]; then + export PATH="$git_root/usr/bin:$PATH" + break + fi + done +fi + +if ! command -v cygpath >/dev/null 2>&1; then + echo "[run-dev-win] cygpath not found. Run this script from Git Bash or MSYS2," + echo "[run-dev-win] or install Git for Windows so cygpath.exe is available at" + echo "[run-dev-win] 'C:\\Program Files\\Git\\usr\\bin\\cygpath.exe'." exit 1 fi @@ -22,6 +38,38 @@ if [[ -z "${LOCALAPPDATA:-}" ]]; then exit 1 fi +# ───────────────────────────────────────────────────────────────────────────── +# Restore the real Windows-side PATH. +# +# Git for Windows' bash sources /etc/profile + /etc/profile.d/* on every +# spawn, which REPLACES the inherited Windows PATH with an MSYS-only +# default (/usr/local/bin:/usr/bin:/bin:…). That wipes every tool the +# parent shell saw — node, cargo, pnpm, ninja, cmake, etc. — and breaks +# any downstream script that assumes PATH inheritance. +# +# Pull the full machine + user PATH from a cmd.exe subprocess (which DOES +# inherit the unaltered Windows PATH from its parent), convert each entry +# to MSYS form, and append it to the current PATH. We append (not prepend) +# so MSYS coreutils (cygpath, grep, sed, mktemp) still resolve first. +# ───────────────────────────────────────────────────────────────────────────── +cmd_exe_for_path="$(command -v cmd.exe 2>/dev/null || command -v cmd 2>/dev/null || echo /c/Windows/System32/cmd.exe)" +if [[ -x "$cmd_exe_for_path" ]]; then + windows_path_raw="$("$cmd_exe_for_path" //c "echo %PATH%" 2>/dev/null | tr -d '\r' | head -n1 || true)" + if [[ -n "$windows_path_raw" ]]; then + windows_path_unix="" + IFS=';' read -ra _wpe <<< "$windows_path_raw" + for _entry in "${_wpe[@]}"; do + [[ -z "$_entry" ]] && continue + _u="$(cygpath -u "$_entry" 2>/dev/null || printf '%s' "$_entry")" + windows_path_unix="${windows_path_unix}${windows_path_unix:+:}${_u}" + done + if [[ -n "$windows_path_unix" ]]; then + export PATH="$PATH:$windows_path_unix" + echo "[run-dev-win] appended Windows-side PATH (node/cargo/pnpm/… now findable)" + fi + fi +fi + export LIBCLANG_PATH="/c/Program Files/LLVM/bin" # Bootstrap the MSVC C++ build environment in this shell so cl.exe / link.exe / @@ -53,22 +101,46 @@ if ! command -v cl.exe >/dev/null 2>&1; then # file, then have cmd execute the file. Avoids inner quoting entirely. vcvars_launcher="$(mktemp --suffix=.bat)" vcvars_launcher_win="$(cygpath -w "$vcvars_launcher")" + # vcvarsall.bat (called by vcvars64.bat) shells out to `vswhere` by bare + # name to locate Windows SDK / MSVC component versions. If vswhere isn't + # on cmd.exe's PATH, vcvarsall silently degrades — it sets `cl.exe` on + # PATH but skips the Windows SDK `LIB` / `INCLUDE` entries, which then + # fails the link step downstream with `LNK1181: cannot open input file + # 'kernel32.lib'`. The VS Installer dir holding vswhere is rarely on the + # system PATH (Microsoft expects you to invoke vswhere by absolute path), + # so we prepend it inside the launcher .bat before calling vcvars. + vswhere_dir_win="$(cygpath -w "$(dirname "$vswhere_exe")")" # Note: we deliberately do NOT redirect vcvars64.bat's stdout to NUL — MSYS # would rewrite `NUL` to `/dev/null` while writing the .bat. Instead we let # vcvars64 print its banner and filter for `KEY=VALUE` lines below. - printf '@echo off\r\ncall "%s"\r\nset\r\n' "$vcvars_bat" > "$vcvars_launcher" + printf '@echo off\r\nset "PATH=%s;%%PATH%%"\r\ncall "%s"\r\nset\r\n' \ + "$vswhere_dir_win" "$vcvars_bat" > "$vcvars_launcher" # Note: do NOT set MSYS_NO_PATHCONV here — disabling path conversion stops # MSYS from rewriting `//c` to `/c`, leaving cmd to treat `//c` as an # unknown switch and open an interactive shell instead of executing the # launcher. - msvc_env="$(cmd //c "$vcvars_launcher_win" 2>&1 || true)" + # `cmd` may be missing from PATH when bash.exe is spawned by pnpm/PowerShell + # with a stripped environment. Fall back to the well-known absolute path. + cmd_exe="$(command -v cmd.exe 2>/dev/null || command -v cmd 2>/dev/null || echo /c/Windows/System32/cmd.exe)" + if [[ ! -x "$cmd_exe" ]]; then + echo "[run-dev-win] cmd.exe not found on PATH and /c/Windows/System32/cmd.exe missing" >&2 + rm -f "$vcvars_launcher" + exit 1 + fi + msvc_env_raw="$("$cmd_exe" //c "$vcvars_launcher_win" 2>&1 || true)" rm -f "$vcvars_launcher" # Strip lines that aren't key=value (vcvars banner, blank lines). - msvc_env="$(printf '%s\n' "$msvc_env" | grep -E '^[A-Za-z_][A-Za-z0-9_()]*=' || true)" + msvc_env="$(printf '%s\n' "$msvc_env_raw" | grep -E '^[A-Za-z_][A-Za-z0-9_()]*=' || true)" if [[ -z "$msvc_env" ]]; then echo "[run-dev-win] failed to capture MSVC env from vcvars64.bat" >&2 + echo "[run-dev-win] cmd.exe used: $cmd_exe" >&2 + echo "[run-dev-win] launcher: $vcvars_launcher_win" >&2 + echo "[run-dev-win] --- cmd output (first 40 lines) ---" >&2 + printf '%s\n' "$msvc_env_raw" | head -n 40 >&2 + echo "[run-dev-win] --- end cmd output ---" >&2 exit 1 fi + pre_vcvars_path="$PATH" while IFS='=' read -r key value; do case "$key" in PATH) @@ -80,7 +152,11 @@ if ! command -v cl.exe >/dev/null 2>&1; then unix_entry="$(cygpath -u "$entry" 2>/dev/null || printf '%s' "$entry")" new_path="${new_path}${new_path:+:}${unix_entry}" done - export PATH="$new_path" + # Prepend vcvars' PATH so MSVC tools win, but append the pre-vcvars + # PATH so node, pnpm, git, etc. remain findable. vcvars64.bat ships a + # MSVC-only PATH; without re-adding the original, downstream tools + # (pnpm.cmd invoking node, etc.) blow up with "node is not recognized". + export PATH="$new_path:$pre_vcvars_path" ;; INCLUDE|LIB|LIBPATH) # Compiler/linker want Windows-style ;-separated paths — leave as-is. @@ -98,6 +174,65 @@ if ! command -v cl.exe >/dev/null 2>&1; then echo "[run-dev-win] MSVC env loaded (cl.exe at $(command -v cl.exe))" fi +# Windows SDK self-discovery fallback. +# +# vcvars64.bat can silently "succeed" while only setting up the MSVC half +# of the toolchain — when vswhere is missing from PATH at the time +# vcvars runs, or when the Windows SDK isn't registered in the way +# vcvarsall expects, it skips setting `WindowsSdkDir` / `WindowsSDKVersion` +# and only appends MSVC's own libs to `LIB`. The linker then fails with +# `LNK1181: cannot open input file 'kernel32.lib'` because the SDK's +# `um\x64\kernel32.lib` isn't on the search list. +# +# This block runs unconditionally (whether or not we just bootstrapped +# vcvars) and patches in the SDK paths if they're missing. Detects the +# latest installed SDK on disk under `Windows Kits\10\Lib` and appends +# both lib and include trees. +if [[ -z "${WindowsSdkDir:-}" || "${WindowsSDKVersion:-}" == "\\" || -z "${WindowsSDKVersion:-}" ]]; then + sdk_root_unix="/c/Program Files (x86)/Windows Kits/10" + if [[ -d "$sdk_root_unix/Lib" ]]; then + sdk_version="$(ls -d "$sdk_root_unix"/Lib/*/ 2>/dev/null \ + | sort -V | tail -n1 \ + | xargs -I{} basename {})" + if [[ -n "$sdk_version" && -f "$sdk_root_unix/Lib/$sdk_version/um/x64/kernel32.lib" ]]; then + sdk_root_win="$(cygpath -w "$sdk_root_unix")" + export WindowsSdkDir="${sdk_root_win}\\" + export WindowsSDKVersion="${sdk_version}\\" + sdk_lib_um="${sdk_root_win}\\Lib\\${sdk_version}\\um\\x64" + sdk_lib_ucrt="${sdk_root_win}\\Lib\\${sdk_version}\\ucrt\\x64" + sdk_inc_shared="${sdk_root_win}\\Include\\${sdk_version}\\shared" + sdk_inc_um="${sdk_root_win}\\Include\\${sdk_version}\\um" + sdk_inc_ucrt="${sdk_root_win}\\Include\\${sdk_version}\\ucrt" + sdk_inc_winrt="${sdk_root_win}\\Include\\${sdk_version}\\winrt" + export LIB="${LIB:+$LIB;}${sdk_lib_um};${sdk_lib_ucrt}" + export INCLUDE="${INCLUDE:+$INCLUDE;}${sdk_inc_shared};${sdk_inc_um};${sdk_inc_ucrt};${sdk_inc_winrt}" + # Prepend the SDK bin dir to PATH so `rc.exe` (Windows Resource + # Compiler) is findable. CMake-driven native crates (cef-dll-sys + # via cmake-rs, whisper-rs-sys, etc.) invoke `rc` by bare name + # during their try-compile probe; vcvars usually adds this dir + # but doesn't when its SDK detection degraded. + sdk_bin_unix="$sdk_root_unix/bin/$sdk_version/x64" + if [[ -x "$sdk_bin_unix/rc.exe" ]]; then + export PATH="$sdk_bin_unix:$PATH" + echo "[run-dev-win] SDK bin dir (with rc.exe) prepended to PATH: $sdk_bin_unix" + else + echo "[run-dev-win] WARNING: rc.exe not found at $sdk_bin_unix — CMake-driven crates will fail" >&2 + fi + echo "[run-dev-win] Windows SDK discovered manually (vcvars degraded): version ${sdk_version}" + else + echo "[run-dev-win] WARNING: Windows SDK version dir or kernel32.lib not found under $sdk_root_unix/Lib" >&2 + echo "[run-dev-win] linker will likely fail with LNK1181." >&2 + fi + else + echo "[run-dev-win] WARNING: Windows SDK not installed at $sdk_root_unix" >&2 + echo "[run-dev-win] Install via Visual Studio Build Tools and retry." >&2 + fi +fi + +echo "[run-dev-win] LIB = ${LIB:-}" +echo "[run-dev-win] WindowsSdkDir = ${WindowsSdkDir:-}" +echo "[run-dev-win] WindowsSDKVersion = ${WindowsSDKVersion:-}" + # Pin the linker by absolute path — runs whether or not we just bootstrapped # the MSVC env. PATH ordering alone isn't reliable: the bash-side reorder # doesn't always survive into the Windows-side %PATH% that rustc sees when @@ -167,7 +302,54 @@ find_pnpm() { command -v pnpm return 0 fi - find_winget_exe "pnpm.pnpm" "pnpm.exe" + # WinGet (preferred on a fresh contributor machine). + if winget_pnpm="$(find_winget_exe "pnpm.pnpm" "pnpm.exe")"; then + printf '%s\n' "$winget_pnpm" + return 0 + fi + # npm-global install — `npm i -g pnpm` drops a shim under %APPDATA%\npm. + # The shim is a `.cmd` on Windows; bash invokes .cmd via the same path. + local appdata_unix="" + if [[ -n "${APPDATA:-}" ]]; then + appdata_unix="$(to_unix_path "$APPDATA" 2>/dev/null || true)" + fi + if [[ -z "$appdata_unix" && -n "${USERPROFILE:-}" ]]; then + local userprofile_unix + userprofile_unix="$(to_unix_path "$USERPROFILE" 2>/dev/null || true)" + if [[ -n "$userprofile_unix" ]]; then + appdata_unix="$userprofile_unix/AppData/Roaming" + fi + fi + # Ordering matters: prefer the bare shebang shim (a `#!/bin/sh` script) + # over `pnpm.cmd`. The .cmd shim invokes `node` through cmd.exe, which + # ignores the bash-side PATH after vcvars rewriting and blows up with + # `'"node"' is not recognized`. The bash shim execs node directly using + # bash's PATH, which we've taken care to keep node on. + # + # NB: MSYS does NOT set the execute bit on .cmd files (only on .exe and + # shebang-prefixed scripts), so we test with `-f` (regular file) rather + # than `-x`. + if [[ -n "$appdata_unix" ]]; then + for candidate in \ + "$appdata_unix/npm/pnpm" \ + "$appdata_unix/npm/pnpm.cmd" \ + "$appdata_unix/npm/pnpm.exe"; do + if [[ -f "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + fi + # Chocolatey shim — same pattern as find_ninja above. + for choco_pnpm in \ + "/c/ProgramData/chocolatey/bin/pnpm.cmd" \ + "/c/ProgramData/chocolatey/bin/pnpm.exe"; do + if [[ -f "$choco_pnpm" ]]; then + printf '%s\n' "$choco_pnpm" + return 0 + fi + done + return 1 } find_ninja() { @@ -175,14 +357,133 @@ find_ninja() { command -v ninja return 0 fi - find_winget_exe "Ninja-build.Ninja" "ninja.exe" + # WinGet (preferred on a fresh contributor machine). + if winget_ninja="$(find_winget_exe "Ninja-build.Ninja" "ninja.exe")"; then + printf '%s\n' "$winget_ninja" + return 0 + fi + # Chocolatey shim — common on engineering desktops that pre-date WinGet. + # `-f` rather than `-x` because MSYS leaves .cmd files unmarked-executable. + for choco_ninja in \ + "/c/ProgramData/chocolatey/bin/ninja.exe" \ + "/c/ProgramData/chocolatey/bin/ninja.cmd" \ + "/c/ProgramData/chocolatey/lib/ninja/tools/ninja.exe"; do + if [[ -f "$choco_ninja" ]]; then + printf '%s\n' "$choco_ninja" + return 0 + fi + done + # CMake's own bundled ninja, if a recent CMake install dropped one alongside. + local bundled="/c/Program Files/CMake/bin/ninja.exe" + if [[ -f "$bundled" ]]; then + printf '%s\n' "$bundled" + return 0 + fi + return 1 +} + +# pnpm.cmd / the bare pnpm shim both ultimately `exec node ...`. When +# PowerShell launches pnpm which launches bash.exe, the inherited PATH +# does NOT reliably include Node.js — and vcvars wipes the rest. Probe +# the common Windows install locations and prepend whatever we find so +# downstream `exec node` calls in pnpm shims and Tauri scripts succeed. +find_nodejs_dir() { + # 1) Already on PATH (unlikely if we got here, but cheap to check). + if command -v node >/dev/null 2>&1 || command -v node.exe >/dev/null 2>&1; then + dirname "$(command -v node 2>/dev/null || command -v node.exe)" + return 0 + fi + # 2) Standard installer locations. + for nodejs_dir in \ + "/c/Program Files/nodejs" \ + "/c/Program Files (x86)/nodejs"; do + if [[ -f "$nodejs_dir/node.exe" ]]; then + printf '%s\n' "$nodejs_dir" + return 0 + fi + done + # 3) nvm-for-windows: %LOCALAPPDATA%\nvm\v. Pick the highest. + if [[ -n "${LOCALAPPDATA:-}" ]]; then + local nvm_root + nvm_root="$(to_unix_path "$LOCALAPPDATA" 2>/dev/null || true)/nvm" + if [[ -d "$nvm_root" ]]; then + local nvm_pick + nvm_pick="$(ls -d "$nvm_root"/v* 2>/dev/null | sort -V | tail -n1)" + if [[ -n "$nvm_pick" && -f "$nvm_pick/node.exe" ]]; then + printf '%s\n' "$nvm_pick" + return 0 + fi + fi + fi + # 4) Chocolatey shim. + if [[ -f "/c/ProgramData/chocolatey/bin/node.exe" ]]; then + printf '%s\n' "/c/ProgramData/chocolatey/bin" + return 0 + fi + return 1 +} + +NODEJS_DIR="$(find_nodejs_dir || true)" +if [[ -z "$NODEJS_DIR" ]]; then + echo "[run-dev-win] node.exe not found on PATH or in common Windows install dirs." >&2 + echo "[run-dev-win] Install Node.js (https://nodejs.org/) and retry." >&2 + exit 1 +fi +export PATH="$NODEJS_DIR:$PATH" +echo "[run-dev-win] nodejs dir prepended to PATH: $NODEJS_DIR" + +# Same trick for cargo. Git Bash's /etc/profile.d scripts wipe the parent +# Windows PATH and re-install a MSYS-default one; rustup's +# `~/.cargo/bin` (or `$CARGO_HOME/bin`) doesn't survive that. We need +# cargo for the vendored tauri-cli install (`ensure-tauri-cli.sh`), +# `core:stage`, and `cargo tauri dev` itself. +find_cargo_dir() { + if command -v cargo >/dev/null 2>&1 || command -v cargo.exe >/dev/null 2>&1; then + dirname "$(command -v cargo 2>/dev/null || command -v cargo.exe)" + return 0 + fi + # 1) Honour CARGO_HOME (rustup, workspace .env conventions). + if [[ -n "${CARGO_HOME:-}" ]]; then + local ch + ch="$(to_unix_path "$CARGO_HOME" 2>/dev/null || printf '%s' "$CARGO_HOME")" + if [[ -f "$ch/bin/cargo.exe" ]]; then + printf '%s\n' "$ch/bin" + return 0 + fi + fi + # 2) Default rustup install at %USERPROFILE%\.cargo\bin. + if [[ -n "${USERPROFILE:-}" ]]; then + local up + up="$(to_unix_path "$USERPROFILE" 2>/dev/null || true)" + if [[ -n "$up" && -f "$up/.cargo/bin/cargo.exe" ]]; then + printf '%s\n' "$up/.cargo/bin" + return 0 + fi + fi + # 3) Same path via $HOME (Git Bash sometimes only sets HOME, not USERPROFILE). + if [[ -n "${HOME:-}" && -f "$HOME/.cargo/bin/cargo.exe" ]]; then + printf '%s\n' "$HOME/.cargo/bin" + return 0 + fi + return 1 } +CARGO_DIR="$(find_cargo_dir || true)" +if [[ -z "$CARGO_DIR" ]]; then + echo "[run-dev-win] cargo.exe not found. Install Rust via rustup (https://rustup.rs/) and retry." >&2 + exit 1 +fi +export PATH="$CARGO_DIR:$PATH" +echo "[run-dev-win] cargo dir prepended to PATH: $CARGO_DIR" + PNPM_EXE="$(find_pnpm || true)" if [[ -z "$PNPM_EXE" ]]; then echo "[run-dev-win] pnpm not found. Install pnpm and retry." exit 1 fi +echo "[run-dev-win] pnpm resolved to: $PNPM_EXE" +echo "[run-dev-win] node on bash PATH: $(command -v node 2>/dev/null || echo '')" +echo "[run-dev-win] node.exe on bash PATH: $(command -v node.exe 2>/dev/null || echo '')" NINJA_EXE="$(find_ninja || true)" if [[ -z "$NINJA_EXE" ]]; then @@ -204,6 +505,38 @@ export PATH="$PATH_PREFIX:$PATH" "$PNPM_EXE" tauri:ensure "$PNPM_EXE" core:stage + +# ───────────────────────────────────────────────────────────────────────────── +# Stage the CEF runtime next to the dev OpenHuman.exe. +# +# `cargo tauri build` (release) copies CEF into the bundle automatically, but +# `cargo tauri dev` doesn't — the dev .exe lands at /debug/OpenHuman.exe +# alone, and Windows can't find libcef.dll. The .exe panics during boot with +# `cef::library_loader::LibraryLoader::new` errors (or just refuses to launch +# with "libcef.dll not found"). Without this step every fresh contributor +# session hits the wall. +# +# We stage by copying (not symlinking) so the script runs without admin / +# Developer-Mode privileges. `cp -ru` only copies entries newer than the +# destination, so subsequent dev runs are essentially free. +# ───────────────────────────────────────────────────────────────────────────── +if [[ -n "${CEF_RUNTIME_PATH:-}" && -f "$CEF_RUNTIME_PATH/libcef.dll" ]]; then + CARGO_TARGET_DIR_UNIX="$(to_unix_path "${CARGO_TARGET_DIR:-$REPO_ROOT/target}" 2>/dev/null || printf '%s' "${CARGO_TARGET_DIR:-$REPO_ROOT/target}")" + CEF_STAGE_DIR="$CARGO_TARGET_DIR_UNIX/debug" + mkdir -p "$CEF_STAGE_DIR" + if [[ ! -f "$CEF_STAGE_DIR/libcef.dll" \ + || "$CEF_RUNTIME_PATH/libcef.dll" -nt "$CEF_STAGE_DIR/libcef.dll" ]]; then + echo "[run-dev-win] staging CEF runtime → $CEF_STAGE_DIR (first run only — copies ~270MB)" + cp -ru "$CEF_RUNTIME_PATH"/. "$CEF_STAGE_DIR/" + echo "[run-dev-win] CEF runtime staged" + else + echo "[run-dev-win] CEF runtime already staged at $CEF_STAGE_DIR (libcef.dll up to date)" + fi +else + echo "[run-dev-win] WARNING: CEF_RUNTIME_PATH not set or libcef.dll missing — the dev exe will fail to load" >&2 + echo "[run-dev-win] expected: $CEF_PATH//cef_windows_x86_64/libcef.dll" >&2 +fi + # Use the vendored tauri-cef CLI (via the pnpm tauri script) so the # CEF runtime is correctly bundled. APPLE_SIGNING_IDENTITY is macOS-only # and is intentionally omitted here. From ca79b68f0e75e218f326c2496f3fbca369bffd4a Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Tue, 12 May 2026 16:02:07 +0530 Subject: [PATCH 2/4] fix(windows): gate focus-cycle to target_os = "windows" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The minimize/unminimize CEF focus-recovery cycle only addresses the Windows CEF integration's host-renderer focus desync (cold launch via `visible: false` window). macOS and Linux CEF route focus differently and don't exhibit the symptom, so running the cycle there would just flicker the window with no benefit. Wrap the deferred focus-cycle spawn in `#[cfg(target_os = "windows")]` per CodeRabbit feedback on #1528. The `webview.set_focus()` addition in `show_main_window` stays unconditional — it's a single non-fatal call with no visible side effect, idempotent on platforms where the host already considers itself focused. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/src/lib.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index f86b0c52d0..192094592e 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -1653,6 +1653,17 @@ pub fn run() { // calls on both Window and Webview to cover the case // where minimize/unminimize raced ahead of CEF's // browser-create. + // Windows-only: the bug class (CEF host-renderer focus + // desync after a `visible: false` → `show()` transition + // without a real `WM_KILLFOCUS`+`WM_SETFOCUS` edge) + // manifests on the Windows CEF integration. macOS and + // Linux CEF use different focus propagation paths and + // don't exhibit the symptom, so running the + // minimize/unminimize cycle there would just be a + // visible flicker for no benefit. (Per CodeRabbit + // review on PR #1528.) + #[cfg(target_os = "windows")] + { log::info!("[focus-fix] scheduling deferred CEF focus-cycle"); let webview_window_clone = window.clone(); tauri::async_runtime::spawn(async move { @@ -1685,6 +1696,7 @@ pub fn run() { } log::info!("[focus-fix] focus cycle complete"); }); + } } } From 100b50f74ee0f1ddc214676ac6331d114e997d76 Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Tue, 12 May 2026 16:06:30 +0530 Subject: [PATCH 3/4] review: address graycyrus inline comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `run-dev-win.sh`: cmd.exe-not-found path was a silent no-op that skipped the entire Windows-PATH restoration block — contributors hitting "node/cargo not found" downstream had no log trail pointing here. Add WARNING stderr logs to all three failure branches (cmd.exe missing, query returned empty, no entries after conversion) so the downstream symptom is traceable to its actual cause. - `lib.rs`: replace `cef_impl.rs line ~2081` reference with a message-name reference (`WebviewMessage::SetFocus` handler) so the comment doesn't drift as the vendor file evolves. Skipping the minor `xargs/basename → sed` nit (SC2012 lint area also flagged by CodeRabbit as "Low value" — Windows SDK dirs are strictly `10.0.x.0` so the practical risk is negligible). Focus-cycle Windows gate (graycyrus's other [major] comment) already landed in ca79b68f. Co-Authored-By: Claude Opus 4.7 (1M context) --- app/src-tauri/src/lib.rs | 5 +++-- scripts/run-dev-win.sh | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 192094592e..0a4d0ab478 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -845,8 +845,9 @@ fn show_main_window(app: &AppHandle) -> Result<(), String> { // triggers `WM_KILLFOCUS`+`WM_SETFOCUS` and CEF's window handler routes // through `host.set_focus(1)` internally. // - // Explicitly dispatch `WebviewMessage::SetFocus` (cef_impl.rs line ~2081), - // which is what actually invokes `CefBrowserHost::SetFocus(true)`. + // Explicitly dispatch `WebviewMessage::SetFocus` (cef_impl.rs handler + // for that variant), which is what actually invokes + // `CefBrowserHost::SetFocus(true)`. let webview: &tauri::Webview = window.as_ref(); if let Err(err) = webview.set_focus() { log::warn!( diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh index 93da0d7c2e..7f0184a85e 100644 --- a/scripts/run-dev-win.sh +++ b/scripts/run-dev-win.sh @@ -66,8 +66,14 @@ if [[ -x "$cmd_exe_for_path" ]]; then if [[ -n "$windows_path_unix" ]]; then export PATH="$PATH:$windows_path_unix" echo "[run-dev-win] appended Windows-side PATH (node/cargo/pnpm/… now findable)" + else + echo "[run-dev-win] WARNING: cmd.exe PATH query returned no entries — node/cargo may be missing downstream" >&2 fi + else + echo "[run-dev-win] WARNING: cmd.exe PATH query returned empty — node/cargo may be missing downstream" >&2 fi +else + echo "[run-dev-win] WARNING: cmd.exe not found at '$cmd_exe_for_path' — Windows PATH restoration skipped; node/cargo may be missing downstream" >&2 fi export LIBCLANG_PATH="/c/Program Files/LLVM/bin" From 47abaa37cb862295e16df7d315f5494663bba0eb Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Tue, 12 May 2026 16:28:15 +0530 Subject: [PATCH 4/4] =?UTF-8?q?review:=20xargs/basename=20=E2=86=92=20sed?= =?UTF-8?q?=20in=20SDK=20version=20discovery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address graycyrus's repeated minor flag on the SDK-version-pick line. Behavior unchanged: still picks the highest-version `10.0.x.0` dir under `Windows Kits/10/Lib`, just trims the trailing slash and prefix via sed instead of piping into `xargs -I{} basename {}`. Co-Authored-By: Claude Opus 4.7 (1M context) --- scripts/run-dev-win.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh index 7f0184a85e..3840409237 100644 --- a/scripts/run-dev-win.sh +++ b/scripts/run-dev-win.sh @@ -199,7 +199,7 @@ if [[ -z "${WindowsSdkDir:-}" || "${WindowsSDKVersion:-}" == "\\" || -z "${Windo if [[ -d "$sdk_root_unix/Lib" ]]; then sdk_version="$(ls -d "$sdk_root_unix"/Lib/*/ 2>/dev/null \ | sort -V | tail -n1 \ - | xargs -I{} basename {})" + | sed 's|.*/||; s|/||g')" if [[ -n "$sdk_version" && -f "$sdk_root_unix/Lib/$sdk_version/um/x64/kernel32.lib" ]]; then sdk_root_win="$(cygpath -w "$sdk_root_unix")" export WindowsSdkDir="${sdk_root_win}\\"