You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat(windows): disable OpenFlow on Windows, implement port detection, add NSIS bundle config
OpenFlow depends on bash wrapper scripts that don't have Windows
equivalents yet. Disable at every layer (UI + backend) for v1 so Windows
users see a clear "coming soon" affordance instead of cryptic
wrapper-spawn errors.
- Add get_platform() Tauri command (std::env::consts::OS) + getPlatform()
frontend wrapper + usePlatform() React hook with module-level cache and
in-flight dedupe.
- sidebar-openflow-section.tsx: Windows branch renders a disabled header
with a Tooltip ("OpenFlow is not yet available on Windows"); expand
toggle removed, "+" button disabled, NewRunDialog not mounted.
- spawn_openflow_agents: early-return Err on cfg(windows) as a safety net
for CLI / control-socket callers that bypass the UI.
Port detection and reclamation now work on Windows:
- ports.rs: detect_listening_ports() dispatches on cfg. Windows path
shells out to netstat -ano with CREATE_NO_WINDOW (scan_ports runs every
3 s, so console suppression is non-negotiable), parses TCP + LISTENING
rows for both IPv4 and [::]:port IPv6, applies the existing IGNORED and
is_codemux_internal_port filters. Process names come from one
tasklist /NH /FO csv call per scan (not per port) — PID -> name
HashMap is built once.
- ports.rs: read_ppid() dispatches on cfg. Windows path uses wmic ...
get ParentProcessId /value with graceful None fallback (PPID walk is
nice-to-have, not correctness-critical).
- agent_browser.rs: kill_process_on_port() Windows branch replaced (was a
no-op logging a warning). Now parses netstat -ano in Rust (not via
findstr :port — avoids :9223 matching :92230) and runs taskkill /PID
/F for each owning PID. Also uses CREATE_NO_WINDOW.
- tauri.conf.json: add bundle.windows.nsis so the release pipeline can
eventually produce a .exe installer. No code signing yet (budget
decision); release.yml itself is still intentionally Linux-only.
Out of scope, still tracked: openflow wrapper rewrite, release.yml
Windows matrix, os_input.rs Tier 3 input.
docs/plans/windows-support.md updated with check-offs and a new
"Windows MVP Disable Strategy — OpenFlow" subsection documenting the
disable approach and explicitly-deferred items.
Verified: cargo check, cargo check --release, cargo test (432 lib + 65
git + 12 github), npm run check, npm run build, npm run test (298)
all clean on Linux. Windows cross-compile will run on the existing
windows-latest CI matrix.
Copy file name to clipboardExpand all lines: docs/plans/windows-support.md
+29-5Lines changed: 29 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -106,7 +106,7 @@ Web research conducted 2026-04-12 on the three unknowns that gate the Windows ef
106
106
-[x]**`src-tauri/src/agent_browser.rs:470, 619, 670` — `fuser -k {port}/tcp` for port reclamation**
107
107
- Frees ports stuck after crash
108
108
- Windows: parse `netstat -ano` for PID + `taskkill /PID`, or use `GetExtendedTcpTable` from `windows` crate
109
-
-**Partial (2026-04-12)**: extracted the three call sites into a helper `kill_process_on_port(port)`. Unix impl runs `fuser -k`. Windows impl is a no-op that logs a warning; full `netstat -ano` +`taskkill`port-reclamation is tracked as a follow-up.
109
+
-**Done (2026-04-12)**: `kill_process_on_port(port)` now has a full `#[cfg(windows)]` implementation. Runs `netstat -ano` with `CREATE_NO_WINDOW` (no console flash), parses `TCP <local>:<port> ... LISTENING <pid>` rows (tolerant of both IPv4 `0.0.0.0:9223` and IPv6 `[::]:9223`), then`taskkill /PID <pid> /F` each owning PID. Filtering in Rust (not `findstr :{port}`) avoids false-positives on substring matches like `:92230`. Unix path (`fuser -k`) unchanged.
110
110
111
111
### Browser Automation / Tier 3 Input Injection
112
112
@@ -203,9 +203,10 @@ Web research conducted 2026-04-12 on the three unknowns that gate the Windows ef
203
203
204
204
### Build & Distribution
205
205
206
-
-[]**`src-tauri/tauri.conf.json:31` — `bundle.targets = "all"` but only `linux` section configured**
206
+
-[x]**`src-tauri/tauri.conf.json:31` — `bundle.targets = "all"` but only `linux` section configured**
207
207
- No Windows installer config present
208
208
- Add `"windows": { "nsis": { "languages": ["English"], "displayLanguageSelector": false } }`. Recommend NSIS over MSI for v1 — smaller, simpler, no Windows SDK required
209
+
-**Done (2026-04-12)**: added a `bundle.windows.nsis` section with `languages: ["English"]`, `displayLanguageSelector: false`, and explicit null `installerIcon`/`headerImage`/`sidebarImage` (use Tauri defaults). No code signing yet — that's gated on a budget decision. The release pipeline (`release.yml`) itself is still intentionally Linux-only; this config unblocks the eventual Windows build job so it can produce a `.exe` installer once signing and the release-matrix rewrite land.
- Verify env is passed via `Command::env()` not via shell string; if spawning goes through a shell, rewrite to pass directly
234
235
236
+
### Windows MVP Disable Strategy — OpenFlow
237
+
238
+
OpenFlow depends on the bash wrapper scripts generated in `openflow/prompts.rs` (`ensure_wrapper_exists` / `ensure_claude_wrapper_exists`). Those don't have Windows equivalents yet. Rather than ship a broken feature that fails with cryptic wrapper-spawn errors, OpenFlow is disabled at every layer on Windows for v1. Re-enabling is a follow-up blocked on the wrapper rewrite — Option B under the Agent Integration item above (refactor to spawn adapters directly from Rust, eliminating the wrapper entirely, is the preferred path).
239
+
240
+
-[x]**`src-tauri/src/commands/mod.rs` — new `get_platform()` Tauri command**
241
+
- Returns `std::env::consts::OS` as a `String`. Enables feature gating from the React frontend without needing per-feature cfg flags.
242
+
- Wired into `lib.rs` invoke_handler as `commands::get_platform`.
243
+
-**Done (2026-04-12)**: new module-level command in `commands/mod.rs` just above `get_detected_ports`. Sibling frontend wrapper `getPlatform()` added to `src/tauri/commands.ts`.
- Module-level cache keyed on first successful resolve (OS doesn't change during a session). In-flight invocations are deduped via a shared promise so N concurrent `usePlatform()` consumers share one IPC round-trip. Failure falls back to an empty string so a broken invoke never hides OpenFlow from Linux users (false-positive Windows detection).
246
+
-**Done (2026-04-12)**: exposes `{ os, loading, isWindows }`; second + subsequent mounts return the cached value synchronously on the first render.
247
+
-[x]**`src/components/layout/sidebar-openflow-section.tsx` — disabled header on Windows**
248
+
- Renders a greyed-out header with a `Tooltip` explaining "OpenFlow is not yet available on Windows" when `isWindows`. The `+` button is `disabled` / `aria-disabled`, the expand toggle is removed, and `NewRunDialog` is not mounted at all — nothing to open it from. The section stays visible so Windows users know the feature exists and is coming.
249
+
-**Done (2026-04-12)**: Windows branch is a completely separate `if (isWindows) return ...` at the top of the component, leaving the Linux code path byte-identical.
250
+
-[x]**`src-tauri/src/commands/openflow.rs:498` — `spawn_openflow_agents` Windows guard**
251
+
- Safety net for CLI or socket-driven callers that might bypass the UI (e.g. the `codemux` control socket). Early-returns `Err("OpenFlow is not yet available on Windows")` when `cfg!(windows)`, before any wrapper-script creation attempts.
252
+
-**Done (2026-04-12)**: guard added as the first statement of the command body, with an inline comment pointing at the wrapper dependency and the UI-layer disable.
253
+
254
+
**Explicitly out of scope for this pass**: no refactoring of `src-tauri/src/openflow/` wrapper scripts, no changes to `release.yml`, no changes to `os_input.rs` (Tier 3 input is deferred). Those remain tracked under their own items above.
255
+
235
256
---
236
257
237
258
## Degraded (feature broken, app runs)
238
259
239
260
### Process / Port Detection
240
261
241
-
-[]**`src-tauri/src/ports.rs:38-63` — `detect_listening_ports()` parses `/proc/net/tcp` and `/proc/net/tcp6`**
262
+
-[x]**`src-tauri/src/ports.rs:38-63` — `detect_listening_ports()` parses `/proc/net/tcp` and `/proc/net/tcp6`**
242
263
- Port discovery for UI
243
264
- Windows: `GetExtendedTcpTable()` via `windows` crate, or accept empty list on Windows (users type port manually)
-**Done (2026-04-12)**: `detect_listening_ports()` now dispatches on cfg. Linux path uses the existing `/proc/net/tcp` + `/proc/*/fd/` scan unchanged. Windows path (`windows_impl::detect_listening_ports`) shells out to `netstat -ano` with `CREATE_NO_WINDOW` (no console flash — `scan_ports` runs every 3 s, so the suppression flag is non-negotiable), filters for `TCP ... LISTENING` rows, and parses `<local>:<port> <pid>` for both IPv4 `0.0.0.0:135` and IPv6 `[::]:135` addresses. Existing `IGNORED_PORTS` and `is_codemux_internal_port` filters are applied on both platforms.
266
+
-[x]**`src-tauri/src/ports.rs:120` — `/proc/*/fd/` symlink scan to map sockets → PIDs**
245
267
- Owner process attribution for detected ports
246
268
-`GetExtendedTcpTable` returns PID directly; no fd scan needed
247
-
-[ ]**`src-tauri/src/ports.rs:156-166` — `read_ppid()` reads `/proc/{pid}/stat` for parent PID walk**
269
+
-**Done (2026-04-12)**: the Windows path gets PID directly from `netstat -ano` — no fd scan. Process names come from a single `tasklist /NH /FO csv` call per scan (NOT per port — we build a PID→name `HashMap` once), parsing the first two CSV cells as `name,pid`. Shelling out chose over `GetExtendedTcpTable` to avoid adding the `windows` crate dep for MVP; can swap later if the 3-second-interval `netstat` spawn proves too slow.
270
+
-[x]**`src-tauri/src/ports.rs:156-166` — `read_ppid()` reads `/proc/{pid}/stat` for parent PID walk**
248
271
- Walks up process tree to attribute ports to workspaces
-**Done (2026-04-12)**: `read_ppid` now dispatches on cfg. Linux unchanged. Windows implementation shells out to `wmic process where "ProcessId=<pid>" get ParentProcessId /value` and parses the `ParentProcessId=<n>` line. Deliberately avoids `CreateToolhelp32Snapshot` (would require the `windows` crate) — the PPID walk is a nice-to-have for workspace attribution, not a correctness requirement, so `None` is an acceptable graceful degradation. Follow-up: `wmic` is deprecated on Windows 11 24H2+; if this stops working, swap to PowerShell `Get-CimInstance` or add the `windows` crate dep.
250
274
-[ ]**`src-tauri/src/hooks.rs:195-219` — `shell_is_foreground()` reads `/proc/{shell_pid}/stat` for tpgid vs pgrp**
251
275
- Suppresses hook notifications when shell is backgrounded
252
276
- Already has `#[cfg(not(target_os = "linux"))]` stub returning `false`; hook notifications always fire on Windows (acceptable)
0 commit comments