From cde0e242346c43790eb3aa65f9ac3cb31a0eb550 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 21 Jan 2026 14:23:16 -0700 Subject: [PATCH 1/4] feat: add Windows platform support - terminal.rs: Use PowerShell on Windows, keep $SHELL/zsh on Unix - Make -i flag conditional (Unix only) - app_server.rs: Platform-aware PATH handling - Use ; separator on Windows, : on Unix - Add Windows tool paths (nvm-windows, npm, Volta, pnpm, Scoop, Git, Node.js) - Check both HOME and USERPROFILE for home directory - workspaces.rs: Platform-specific app launching - macOS: open -a - Windows: Map app names to executables (code, cursor, etc.) - Linux: Use lowercase command names Co-Authored-By: Claude Opus 4.5 --- src-tauri/src/backend/app_server.rs | 162 ++++++++++++++++++++++------ src-tauri/src/terminal.rs | 11 ++ src-tauri/src/workspaces.rs | 68 ++++++++++-- 3 files changed, 199 insertions(+), 42 deletions(-) diff --git a/src-tauri/src/backend/app_server.rs b/src-tauri/src/backend/app_server.rs index 1d3ccba6..89dc5a59 100644 --- a/src-tauri/src/backend/app_server.rs +++ b/src-tauri/src/backend/app_server.rs @@ -72,54 +72,152 @@ impl WorkspaceSession { } } -pub(crate) fn build_codex_path_env(codex_bin: Option<&str>) -> Option { - let mut paths: Vec = env::var("PATH") - .unwrap_or_default() - .split(':') - .filter(|value| !value.is_empty()) - .map(|value| value.to_string()) - .collect(); - let mut extras = vec![ - "/opt/homebrew/bin", - "/usr/local/bin", - "/usr/bin", - "/bin", - "/usr/sbin", - "/sbin", - ] - .into_iter() - .map(|value| value.to_string()) - .collect::>(); - if let Ok(home) = env::var("HOME") { - extras.push(format!("{home}/.local/bin")); - extras.push(format!("{home}/.local/share/mise/shims")); - extras.push(format!("{home}/.cargo/bin")); - extras.push(format!("{home}/.bun/bin")); - let nvm_root = Path::new(&home).join(".nvm/versions/node"); - if let Ok(entries) = std::fs::read_dir(nvm_root) { - for entry in entries.flatten() { - let bin_path = entry.path().join("bin"); - if bin_path.is_dir() { - extras.push(bin_path.to_string_lossy().to_string()); +/// Platform-specific PATH separator +#[cfg(windows)] +const PATH_SEPARATOR: char = ';'; +#[cfg(not(windows))] +const PATH_SEPARATOR: char = ':'; + +/// Resolve home directory using platform-appropriate environment variables +fn resolve_home_dir() -> Option { + if let Ok(value) = env::var("HOME") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + if let Ok(value) = env::var("USERPROFILE") { + let trimmed = value.trim(); + if !trimmed.is_empty() { + return Some(trimmed.to_string()); + } + } + None +} + +/// Build platform-specific extra paths for common tool locations +fn build_extra_paths(codex_bin: Option<&str>) -> Vec { + let mut extras = Vec::new(); + + #[cfg(not(windows))] + { + extras.extend([ + "/opt/homebrew/bin".to_string(), + "/usr/local/bin".to_string(), + "/usr/bin".to_string(), + "/bin".to_string(), + "/usr/sbin".to_string(), + "/sbin".to_string(), + ]); + } + + if let Some(home) = resolve_home_dir() { + #[cfg(not(windows))] + { + extras.push(format!("{home}/.local/bin")); + extras.push(format!("{home}/.local/share/mise/shims")); + extras.push(format!("{home}/.cargo/bin")); + extras.push(format!("{home}/.bun/bin")); + + let nvm_root = Path::new(&home).join(".nvm/versions/node"); + if let Ok(entries) = std::fs::read_dir(&nvm_root) { + for entry in entries.flatten() { + let bin_path = entry.path().join("bin"); + if bin_path.is_dir() { + extras.push(bin_path.to_string_lossy().to_string()); + } } } } + + #[cfg(windows)] + { + extras.push(format!("{home}/.cargo/bin")); + extras.push(format!("{home}/.bun/bin")); + + if let Ok(appdata) = env::var("APPDATA") { + let nvm_root = Path::new(&appdata).join("nvm"); + if nvm_root.is_dir() { + extras.push(nvm_root.to_string_lossy().to_string()); + if let Ok(entries) = std::fs::read_dir(&nvm_root) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_dir() && path.join("node.exe").exists() { + extras.push(path.to_string_lossy().to_string()); + } + } + } + } + let npm_bin = Path::new(&appdata).join("npm"); + if npm_bin.is_dir() { + extras.push(npm_bin.to_string_lossy().to_string()); + } + } + + if let Ok(localappdata) = env::var("LOCALAPPDATA") { + let volta_bin = Path::new(&localappdata).join("Volta/bin"); + if volta_bin.is_dir() { + extras.push(volta_bin.to_string_lossy().to_string()); + } + let pnpm_home = Path::new(&localappdata).join("pnpm"); + if pnpm_home.is_dir() { + extras.push(pnpm_home.to_string_lossy().to_string()); + } + } + + let scoop_shims = Path::new(&home).join("scoop/shims"); + if scoop_shims.is_dir() { + extras.push(scoop_shims.to_string_lossy().to_string()); + } + } } + + #[cfg(windows)] + { + if let Ok(program_files) = env::var("ProgramFiles") { + let git_cmd = Path::new(&program_files).join("Git/cmd"); + if git_cmd.is_dir() { + extras.push(git_cmd.to_string_lossy().to_string()); + } + let nodejs = Path::new(&program_files).join("nodejs"); + if nodejs.is_dir() { + extras.push(nodejs.to_string_lossy().to_string()); + } + } + if let Ok(systemroot) = env::var("SystemRoot") { + extras.push(format!("{systemroot}/System32")); + } + } + if let Some(bin_path) = codex_bin.filter(|value| !value.trim().is_empty()) { - let parent = Path::new(bin_path).parent(); - if let Some(parent) = parent { + if let Some(parent) = Path::new(bin_path).parent() { extras.push(parent.to_string_lossy().to_string()); } } + + extras +} + +pub(crate) fn build_codex_path_env(codex_bin: Option<&str>) -> Option { + let mut paths: Vec = env::var("PATH") + .unwrap_or_default() + .split(PATH_SEPARATOR) + .filter(|value| !value.is_empty()) + .map(|value| value.to_string()) + .collect(); + + let extras = build_extra_paths(codex_bin); + for extra in extras { if !paths.contains(&extra) { paths.push(extra); } } + if paths.is_empty() { None } else { - Some(paths.join(":")) + Some(paths.join(&PATH_SEPARATOR.to_string())) } } diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index dba7f987..cafb24ad 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -27,10 +27,19 @@ fn terminal_key(workspace_id: &str, terminal_id: &str) -> String { format!("{workspace_id}:{terminal_id}") } +/// Returns the default shell path for the current platform. +#[cfg(not(target_os = "windows"))] fn shell_path() -> String { std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()) } +/// Returns the default shell path for Windows (PowerShell). +#[cfg(target_os = "windows")] +fn shell_path() -> String { + // PowerShell is available on all modern Windows (Win 7+) + "powershell.exe".to_string() +} + fn spawn_terminal_reader( event_sink: impl EventSink, workspace_id: String, @@ -104,6 +113,8 @@ pub(crate) async fn terminal_open( let mut cmd = CommandBuilder::new(shell_path()); cmd.cwd(cwd); + // Unix shells use -i for interactive mode; Windows PowerShell doesn't need it + #[cfg(not(target_os = "windows"))] cmd.arg("-i"); cmd.env("TERM", "xterm-256color"); diff --git a/src-tauri/src/workspaces.rs b/src-tauri/src/workspaces.rs index fba6be49..1d2c7f27 100644 --- a/src-tauri/src/workspaces.rs +++ b/src-tauri/src/workspaces.rs @@ -1384,16 +1384,64 @@ pub(crate) async fn open_workspace_in( path: String, app: String, ) -> Result<(), String> { - let status = std::process::Command::new("open") - .arg("-a") - .arg(app) - .arg(path) - .status() - .map_err(|error| format!("Failed to open app: {error}"))?; - if status.success() { - Ok(()) - } else { - Err("Failed to open app".to_string()) + #[cfg(target_os = "macos")] + { + let status = std::process::Command::new("open") + .arg("-a") + .arg(&app) + .arg(&path) + .status() + .map_err(|error| format!("Failed to open app: {error}"))?; + if status.success() { + return Ok(()); + } else { + return Err("Failed to open app".to_string()); + } + } + + #[cfg(target_os = "windows")] + { + // Map display names to Windows executables/commands + let (exe, args): (&str, Vec<&str>) = match app.as_str() { + "Visual Studio Code" => ("cmd", vec!["/c", "code", &path]), + "Cursor" => ("cmd", vec!["/c", "cursor", &path]), + "Zed" => ("zed", vec![&path]), + "Ghostty" => ("ghostty", vec![&path]), + "Antigravity" => ("antigravity", vec![&path]), + _ => return Err(format!("Unknown application: {app}")), + }; + + let status = std::process::Command::new(exe) + .args(&args) + .status() + .map_err(|error| format!("Failed to open app: {error}"))?; + if status.success() { + return Ok(()); + } else { + return Err("Failed to open app".to_string()); + } + } + + #[cfg(target_os = "linux")] + { + let exe = match app.as_str() { + "Visual Studio Code" => "code", + "Cursor" => "cursor", + "Zed" => "zed", + "Ghostty" => "ghostty", + "Antigravity" => "antigravity", + _ => return Err(format!("Unknown application: {app}")), + }; + + let status = std::process::Command::new(exe) + .arg(&path) + .status() + .map_err(|error| format!("Failed to open app: {error}"))?; + if status.success() { + Ok(()) + } else { + Err("Failed to open app".to_string()) + } } } From 552ec65f698852fbafc4bae37b8fa425f491c28a Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 21 Jan 2026 14:35:43 -0700 Subject: [PATCH 2/4] chore: bump version to 0.7.13 Co-Authored-By: Claude Opus 4.5 --- package.json | 2 +- src-tauri/tauri.conf.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index e0295555..0ce44fe6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "codex-monitor", "private": true, - "version": "0.7.12", + "version": "0.7.13", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 39a73dd5..d944b423 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "CodexMonitor", - "version": "0.7.12", + "version": "0.7.13", "identifier": "com.dimillian.codexmonitor", "build": { "beforeDevCommand": "npm run dev", From 15925f0bd392b546b0db71b1160054d40024763c Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 21 Jan 2026 14:54:22 -0700 Subject: [PATCH 3/4] ci: add Windows build to release workflow - Add build_windows job for MSI and NSIS installers - Download Windows artifacts in release job - Include Windows installers in GitHub release Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6025a9f9..a3bdb495 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -160,6 +160,59 @@ jobs: release-artifacts/CodexMonitor.app.tar.gz release-artifacts/CodexMonitor.app.tar.gz.sig + build_windows: + runs-on: windows-latest + environment: release + env: + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + TAURI_SIGNING_PRIVATE_KEY_B64: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_B64 }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + fetch-tags: true + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: swatinem/rust-cache@v2 + with: + workspaces: './src-tauri -> target' + + - name: Install dependencies + run: npm ci + + - name: Write Tauri signing key + shell: bash + run: | + set -euo pipefail + mkdir -p "$HOME/.tauri" + echo "$TAURI_SIGNING_PRIVATE_KEY_B64" | base64 --decode > "$HOME/.tauri/codexmonitor.key" + + - name: Build Windows installer + shell: bash + run: | + set -euo pipefail + export TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY="$(cat "$HOME/.tauri/codexmonitor.key")" + npm run tauri -- build -c src-tauri/tauri.windows.conf.json + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v4 + with: + name: windows-artifacts + path: | + src-tauri/target/release/bundle/msi/*.msi + src-tauri/target/release/bundle/nsis/*.exe + build_linux: name: appimage (${{ matrix.arch }}) runs-on: ${{ matrix.platform }} @@ -224,6 +277,7 @@ jobs: environment: release needs: - build_macos + - build_windows - build_linux steps: - name: Checkout @@ -243,6 +297,11 @@ jobs: pattern: appimage-* path: release-artifacts merge-multiple: true + - name: Download Windows artifacts + uses: actions/download-artifact@v4 + with: + name: windows-artifacts + path: release-artifacts/windows - name: Build latest.json run: | @@ -378,6 +437,8 @@ jobs: release-artifacts/CodexMonitor.app.tar.gz \ release-artifacts/CodexMonitor.app.tar.gz.sig \ release-artifacts/*.AppImage* \ + release-artifacts/windows/*.msi \ + release-artifacts/windows/*.exe \ release-artifacts/latest.json - name: Bump version and open PR From df19c293587f34d0a08434cf55e32d65be0afe45 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 21 Jan 2026 15:24:47 -0700 Subject: [PATCH 4/4] feat: enable dictation support on Windows - Remove Windows exclusion from dictation module in lib.rs - Move cpal, whisper-rs, sha2 dependencies to main [dependencies] section - Add CMake installation step to Windows CI build Dictation now uses the same whisper-rs based speech-to-text on all platforms. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 3 +++ src-tauri/Cargo.lock | 14 +++++++------- src-tauri/Cargo.toml | 7 +++---- src-tauri/src/lib.rs | 5 ----- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3bdb495..41de71e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -187,6 +187,9 @@ jobs: with: workspaces: './src-tauri -> target' + - name: Install CMake + run: choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System' -y + - name: Install dependencies run: npm ci diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 085728e9..485c3511 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -914,7 +914,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1088,7 +1088,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2606,7 +2606,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" dependencies = [ - "proc-macro-crate 3.4.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.114", @@ -3798,7 +3798,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", + "windows-sys 0.52.0", ] [[package]] @@ -3811,7 +3811,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.11.0", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4846,7 +4846,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix 1.1.3", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5656,7 +5656,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 1f986312..c042483b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -37,15 +37,14 @@ portable-pty = "0.8" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } libc = "0.2" chrono = { version = "0.4", features = ["clock"] } +cpal = "0.15" +whisper-rs = "0.12" +sha2 = "0.10" [target."cfg(not(any(target_os = \"android\", target_os = \"ios\")))".dependencies] tauri-plugin-updater = "2" tauri-plugin-window-state = "2" -[target."cfg(not(target_os = \"windows\"))".dependencies] -cpal = "0.15" -whisper-rs = "0.12" -sha2 = "0.10" [target."cfg(target_os = \"macos\")".dependencies] objc2 = "0.6" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1509f0a0..900dada2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -4,11 +4,6 @@ mod backend; mod codex; mod codex_home; mod codex_config; -#[cfg(not(target_os = "windows"))] -#[path = "dictation.rs"] -mod dictation; -#[cfg(target_os = "windows")] -#[path = "dictation_stub.rs"] mod dictation; mod event_sink; mod git;