diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96d2421..6a076df 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,8 +11,8 @@ name: release # # Release notes come from the matching CHANGELOG.md section (see that file). # -# macOS ships unsigned (Gatekeeper warning); Linux AppImage needs no signing. -# Windows is intentionally excluded until Milestone 2 (windows.rs is stubbed). +# macOS ships unsigned (Gatekeeper warning); Linux AppImage needs no signing; +# Windows ships an unsigned NSIS installer (SmartScreen warning accepted). on: push: @@ -91,6 +91,7 @@ jobs: - { os: macos-latest, target: aarch64-apple-darwin } - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu } - { os: ubuntu-22.04-arm, target: aarch64-unknown-linux-gnu } + - { os: windows-latest, target: x86_64-pc-windows-msvc } runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/windows-check.yml b/.github/workflows/windows-check.yml new file mode 100644 index 0000000..3fed9d6 --- /dev/null +++ b/.github/workflows/windows-check.yml @@ -0,0 +1,33 @@ +name: windows-check +# Fast Windows compile gate: type-checks every Windows #[cfg] branch on a real +# windows-latest runner (macOS/Linux `cargo check` never compiles the Windows +# arms). No signing secrets, no bundling — just `cargo check`. +on: + push: + branches: [main] + paths: ["src-tauri/**", ".github/workflows/windows-check.yml", "package.json", "package-lock.json"] + pull_request: + paths: ["src-tauri/**", ".github/workflows/windows-check.yml", "package.json", "package-lock.json"] + workflow_dispatch: + +# Least-privilege GITHUB_TOKEN: this workflow only reads the repo. +permissions: + contents: read + +jobs: + cargo-check: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { targets: "x86_64-pc-windows-msvc" } + - uses: actions/setup-node@v4 + with: { node-version: 20 } + - name: Build frontend + # generate_context!() embeds the frontend dist/, so it must exist + # before cargo check even though we don't bundle here. + run: | + npm ci + npm run build + - name: cargo check + run: cargo check --manifest-path src-tauri/Cargo.toml --target x86_64-pc-windows-msvc diff --git a/CHANGELOG.md b/CHANGELOG.md index b81ee72..009850b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ short and user-facing: `- **Thing** — what the user gets.` ## [Unreleased] +## [0.5.0] - 2026-07-22 + +- **Windows support (x64)** — Vector now builds, runs, and self-updates on Windows 10/11 via an NSIS installer. Agent directory tracking, reveal-in-Explorer, open-in-default-app, clipboard file paste, editor discovery, and PowerShell/pwsh shells are all wired up. +- **Known limitation** — on Windows, a *shell* pane's directory doesn't update live as you `cd` (it refreshes when you open the shell panel); agent panes are unaffected. Live shell tracking is a planned follow-up. +- **Note** — Windows builds are unsigned; SmartScreen shows a "Windows protected your PC" prompt on first launch (click *More info → Run anyway*). + ## [0.4.2] - 2026-07-21 - **Security fixes** — updated dependencies to close all known advisories (markdown/diff preview XSS via DOMPurify/Mermaid, a Tauri IPC issue, and a TLS certificate-parsing bug). diff --git a/CLAUDE.md b/CLAUDE.md index 2fbbbac..1c9ccbb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## What Vector is -A Tauri v2 desktop app (macOS + Linux; Windows in progress) that replaces the shell inside terminal tabs with AI coding agents (Claude Code, Codex, etc.). Each tab owns a PTY running the agent's CLI, rendered into xterm.js. See `README.md` for user-facing behavior and the full agent list. OS-divergent logic lives behind `src-tauri/src/platform/` (compiler-enforced per-OS surface); the Linux port is documented in `docs/superpowers/plans/2026-07-20-linux-port-milestone-1.md`. +A Tauri v2 desktop app (macOS, Linux, Windows) that replaces the shell inside terminal tabs with AI coding agents (Claude Code, Codex, etc.). Each tab owns a PTY running the agent's CLI, rendered into xterm.js. See `README.md` for user-facing behavior and the full agent list. OS-divergent logic lives behind `src-tauri/src/platform/` (compiler-enforced per-OS surface); the Linux port is documented in `docs/superpowers/plans/2026-07-20-linux-port-milestone-1.md`. ## Build / run / release @@ -76,6 +76,7 @@ A prior attempt to rewrite CUF (`ESC[nC`) into literal spaces with SGR-inverse/b - **Bundle identifier** `dev.vector.app` ends in `.app` (Tauri warns); changing it invalidates the updater for existing installs. - **Pinned versions**: all `package.json` deps are pinned exactly (no `^`/`~`). Dependabot alerts have been addressed by pinning. Don't loosen constraints to quiet a warning. - **License**: PolyForm Noncommercial 1.0.0. Commercial use requires a separate license. +- **Windows shell cwd**: Windows ships without the OSC-7 shell trampoline (macOS/Linux use zsh ZDOTDIR / bash PROMPT_COMMAND). `read_agent_cwd` (PEB walk in `platform/windows.rs::process_cwd`) covers agent panes and shell-panel-expand; a shell pane's live cwd does not follow `cd`. Deferred follow-up: a PowerShell `prompt`-function trampoline. ## Updating this file (meta-rule) diff --git a/README.md b/README.md index 5841457..358f002 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,8 @@ not a shell prompt. **Linux** (x86_64/aarch64, WebKitGTK) — grab the `.AppImage` or `.deb` from [Releases](https://github.com/avram19/vector/releases); for the AppImage, `chmod +x Vector_*.AppImage` and run it. +**Windows** (x64) — download the `.exe` installer from [Releases](https://github.com/avram19/vector/releases) and run it. The build is unsigned, so SmartScreen warns on first launch — click **More info → Run anyway**. + Full install steps and first-run notes: [docs → Getting Started → Install](https://avram19.github.io/vector/getting-started/install/). ## Build from source diff --git a/docs/superpowers/plans/2026-07-22-windows-port-milestone-2.md b/docs/superpowers/plans/2026-07-22-windows-port-milestone-2.md new file mode 100644 index 0000000..19de7b1 --- /dev/null +++ b/docs/superpowers/plans/2026-07-22-windows-port-milestone-2.md @@ -0,0 +1,867 @@ +# Windows Port — Milestone 2 Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make Vector build, run, and self-update on Windows (x64) with feature parity to macOS/Linux, verified CI-only (no local Windows machine), by filling the six stubbed `platform/windows.rs` functions and wiring the Windows bundle + release leg. + +**Architecture:** Vector already has a compiler-enforced platform module (`src-tauri/src/platform/{mod,macos,linux,windows}.rs`). Milestone 1 (Linux) landed the frontend + the module surface; `windows.rs` is six `todo!()` stubs. This milestone implements those leaf ops using raw `extern "system"` FFI (no new crates — the same technique the reference fork used and proved), fixes one load-bearing `which_path` ordering bug that would otherwise break agent launch on Windows, makes the shell panel and default shell Windows-aware, suppresses console-window flashing on child-process spawns, and adds the NSIS bundle target + a `windows-latest` release leg and a fast `windows-check` compile gate. + +**Tech Stack:** Rust (Tauri v2, portable-pty→ConPTY), raw Win32 FFI (`ntdll`/`kernel32`/`user32`/`shell32`), GitHub Actions (`tauri-action`, NSIS), no new Cargo dependencies. + +**Reference implementation:** The fork `github.com/prateekraj3711-alt/vector-windows` (cloned to the session scratchpad) ported an *older* Vector to Windows with the scattered-`#[cfg]` approach. It has **no** `platform/` module, so we do not copy its structure — but its `read_cwd_windows` PEB walk, `silent_command` helper, `which_path` exe-ordering fix, and `explorer /select` + `cmd /C start` reveal/open are proven and are transcribed (adapted to our module layout) below. + +## Global Constraints + +- **No new Cargo dependencies.** All Windows OS calls use raw `extern "system"` FFI + `#[link(...)]`. The `windows` crate is NOT added. +- **Target triple:** `x86_64-pc-windows-msvc` only (x64). PEB offsets below are x64 ABI. No 32-bit support. +- **Never spawn a bare binary name from Rust** — always resolve through `config::which_path` first (macOS/Windows GUI apps start with a minimal PATH). (from `CLAUDE.md`) +- **Never regress macOS or Linux.** Windows code is `#[cfg(target_os = "windows")]`-gated via `platform/mod.rs`; shared-file edits (`config.rs`, `main.rs`, `sidebar.rs`) must keep macOS/Linux arms byte-identical in behavior. +- **Console flash:** every `std::process::Command` that runs a *console* program on Windows must go through `config::silent_command` (sets `CREATE_NO_WINDOW`), or a console window flashes per call. +- **Verification is CI-only.** No local Windows runtime. Compile-verification = `windows-check` job (`cargo check` on `windows-latest`). Runtime behavior is smoke-tested on a real Windows box post-merge (checklist in Task 10). "It compiles" is the only automated gate; do not claim runtime behavior works. +- **Bundle identifier stays `dev.vector.app`** (changing it breaks the updater for existing installs). +- **Version files move together** — `package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json` must always agree (the release `gate` job fails otherwise). + +## Deferred / documented degradation (NOT in this milestone) + +- **PowerShell `prompt`-function trampoline (live shell cwd on `cd`).** Vector tracks live cwd via OSC-7 emitted by a shell trampoline (zsh ZDOTDIR / bash PROMPT_COMMAND), parsed by `src/shell/cwdSniffer`. On Windows we ship **without** the PowerShell trampoline: `read_agent_cwd` (the PEB walk) still returns correct cwd on shell-panel expand, and agent sessions are unaffected, but a Windows *shell* pane's cwd will not update live on every `cd`. This is the spec's sanctioned degradation (design doc Milestone 2, item 2 "falls back to `process_cwd`"). Documented in `CLAUDE.md` and `CHANGELOG.md` in Task 9. Revisit as a follow-up milestone. + +## File map + +- **Modify** `src-tauri/src/config.rs` — add `silent_command` (Task 2); fix `which_path` Windows exe-ordering (Task 2). +- **Modify** `src-tauri/src/platform/windows.rs` — implement all six stubs + `open_path` polish (Tasks 3–5). +- **Modify** `src-tauri/src/main.rs` — `default_shell` pwsh preference + `start_shell_session` Windows arm (Task 6). +- **Modify** `src-tauri/src/sidebar.rs` — Windows editor discovery in `installed_editors` + `open_in_editor` (Task 6). +- **Modify** `src-tauri/src/git.rs`, `src-tauri/src/github/client.rs` — route git/gh spawns through `silent_command` (Task 7). +- **Modify** `src-tauri/tauri.conf.json` — add `"nsis"` bundle target (Task 8). +- **Create** `.github/workflows/windows-check.yml` — fast `cargo check` gate on `windows-latest` (Task 1). +- **Modify** `.github/workflows/release.yml` — add the `windows-latest` matrix leg (Task 8). +- **Modify** `CHANGELOG.md`, `CLAUDE.md` — release notes + the documented cwd degradation (Task 9). + +--- + +### Task 1: Windows compile-verification gate (`windows-check.yml`) + +Establishes the ONLY automated verification available for every subsequent task: `cargo check` on a real `windows-latest` runner. Modeled on the existing `linux-check.yml` (no apt deps; Windows runners ship MSVC + WebView2). Fast (no bundle, no signing). + +**Files:** +- Create: `.github/workflows/windows-check.yml` + +**Interfaces:** +- Produces: a CI job named `windows-check / cargo-check` that runs on every push/PR touching `src-tauri/**` or the workflow, giving green/red compile status for the `#[cfg(target_os="windows")]` arms this milestone adds. + +- [ ] **Step 1: Write the workflow** + +Create `.github/workflows/windows-check.yml`: + +```yaml +name: windows-check +# Fast Windows compile gate: type-checks every Windows #[cfg] branch on a real +# windows-latest runner (macOS/Linux `cargo check` never compiles the Windows +# arms). No signing secrets, no bundling — just `cargo check`. +on: + push: + branches: [main] + paths: ["src-tauri/**", ".github/workflows/windows-check.yml", "package.json", "package-lock.json"] + pull_request: + paths: ["src-tauri/**", ".github/workflows/windows-check.yml", "package.json", "package-lock.json"] + workflow_dispatch: + +# Least-privilege GITHUB_TOKEN: this workflow only reads the repo. +permissions: + contents: read + +jobs: + cargo-check: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: { targets: "x86_64-pc-windows-msvc" } + - uses: actions/setup-node@v4 + with: { node-version: 20 } + - name: Build frontend + # generate_context!() embeds the frontend dist/, so it must exist + # before cargo check even though we don't bundle here. + run: | + npm ci + npm run build + - name: cargo check + run: cargo check --manifest-path src-tauri/Cargo.toml --target x86_64-pc-windows-msvc +``` + +- [ ] **Step 2: Validate YAML locally** + +Run: `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/windows-check.yml')); print('ok')"` +Expected: `ok` + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/windows-check.yml +git commit -m "ci: add windows-check cargo-check gate" +``` + +- [ ] **Step 4: Push the branch and confirm the job runs** + +This is the harness every later task relies on. Push and confirm `windows-check` appears and runs (it will currently FAIL to compile because `windows.rs` still has `todo!()` bodies referencing undefined helpers only if they don't typecheck — `todo!()` itself compiles, so expect it to PASS on the current stubs). A PASS here proves the runner + frontend build + cross-check pipeline works. + +Run: `git push -u origin HEAD` then `gh run watch $(gh run list --workflow windows-check.yml -L1 --json databaseId -q '.[0].databaseId')` +Expected: `windows-check` completes (PASS on current `todo!()` stubs). + +--- + +### Task 2: `config.rs` — `silent_command` + `which_path` Windows exe-ordering fix + +Two shared-file changes. **`which_path` fix is load-bearing:** npm installs a bare extensionless `claude` (a Git-Bash shell shim) next to `claude.cmd`. Our current loop returns the bare `claude` first — unrunnable via `CreateProcess` — so **every npm-installed agent fails to launch on Windows** until we probe `.exe/.cmd/.bat` first. Both edits compile on macOS (the new code is `#[cfg(windows)]`-gated or macOS-neutral). + +**Files:** +- Modify: `src-tauri/src/config.rs` — `which_path` (currently ~line 191), add `silent_command` after `which`. + +**Interfaces:** +- Produces: `pub fn silent_command>(program: S) -> std::process::Command` — a `Command` with `CREATE_NO_WINDOW` set on Windows, plain `Command::new` elsewhere. Consumed by Tasks 3, 6, 7. +- Produces: `which_path` returning the `.exe/.cmd/.bat` variant before the bare name on Windows. + +- [ ] **Step 1: Fix `which_path` Windows ordering** + +Replace the body of the `for dir in std::env::split_paths(&path)` loop in `which_path` (the current form probes bare-name first, then exts) with exe-first ordering: + +```rust + for dir in std::env::split_paths(&path) { + // On Windows, prefer the executable-extension variants. An extensionless + // file is not runnable via CreateProcess — and npm ships a bare shell + // shim (`claude`) alongside `claude.cmd`, so matching the bare name first + // would hand back the unrunnable sh script. Skip the ext probe when `bin` + // already carries an executable extension. + #[cfg(windows)] + { + let has_exec_ext = std::path::Path::new(bin) + .extension() + .and_then(|e| e.to_str()) + .map(|e| matches!(e.to_ascii_lowercase().as_str(), "exe" | "cmd" | "bat" | "com")) + .unwrap_or(false); + if !has_exec_ext { + for ext in ["exe", "cmd", "bat"] { + let f = dir.join(format!("{bin}.{ext}")); + if f.is_file() { return Some(f); } + } + } + } + let full = dir.join(bin); + if full.is_file() { return Some(full); } + } + None +} +``` + +- [ ] **Step 2: Add `silent_command` immediately after the `which` fn** + +```rust +/// Build a `std::process::Command` that does NOT flash a console window on +/// Windows. A GUI-subsystem app (Vector) spawning a console program — `git`, +/// `gh`, `cmd`, `explorer` — pops a visible console window per call unless +/// `CREATE_NO_WINDOW` is set. Worktree discovery alone fires many `git` +/// invocations on startup (× every auto-resumed tab), so without this the +/// window flickers with console pop-ups for a few seconds. No-op off Windows. +pub fn silent_command>(program: S) -> std::process::Command { + #[allow(unused_mut)] + let mut cmd = std::process::Command::new(program); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} +``` + +- [ ] **Step 3: Verify macOS still compiles** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS (0 errors; a `dead_code` warning on `silent_command` is acceptable until Task 3/7 use it — or add `#[allow(dead_code)]` if the warning is denied). + +- [ ] **Step 4: Commit** + +```bash +git add src-tauri/src/config.rs +git commit -m "fix(windows): probe .exe/.cmd before bare name in which_path; add silent_command" +``` + +--- + +### Task 3: `windows.rs` — reveal / open / open_path / extra_path_dirs + +The mechanical leaf ops. `explorer.exe` returns nonzero even on success, so we don't check its status. All spawns go through `config::silent_command` (Task 2) so no console flashes. + +**Files:** +- Modify: `src-tauri/src/platform/windows.rs` — replace `open_path`, `reveal_in_file_manager`, `open_default_app`, `extra_path_dirs` stubs. + +**Interfaces:** +- Consumes: `crate::config::silent_command` (Task 2). +- Produces (unchanged signatures, matching `linux.rs`): `open_path(&str) -> std::io::Result<()>`, `reveal_in_file_manager(&Path) -> Result<(),String>`, `open_default_app(&Path) -> Result<(),String>`, `extra_path_dirs(&Path) -> Vec`. + +- [ ] **Step 1: Rewrite the four functions** + +In `src-tauri/src/platform/windows.rs`, replace the current `open_path`, `reveal_in_file_manager`, `open_default_app`, and `extra_path_dirs` stubs: + +```rust +pub fn open_path(target: &str) -> std::io::Result<()> { + // The empty "" is `start`'s title argument — required so a quoted path + // isn't consumed as the window title. + crate::config::silent_command("cmd") + .args(["/C", "start", "", target]) + .spawn() + .map(|_| ()) +} + +/// Windows: `explorer /select,` highlights the file in a new Explorer +/// window. explorer.exe returns a nonzero exit code even on success, so we +/// spawn-and-forget without checking status. +pub fn reveal_in_file_manager(path: &Path) -> Result<(), String> { + crate::config::silent_command("explorer.exe") + .arg(format!("/select,{}", path.display())) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +/// Windows: `cmd /C start "" ` opens the path in its default handler. +pub fn open_default_app(path: &Path) -> Result<(), String> { + crate::config::silent_command("cmd") + .args(["/C", "start", ""]) + .arg(path) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +/// Extra PATH dirs where Windows package managers drop agent/git shims. macOS +/// GUI apps start with a minimal PATH; Windows GUI apps inherit the fuller user +/// PATH, but these cover common setups where a shim dir isn't on PATH yet. +pub fn extra_path_dirs(home: &Path) -> Vec { + let mut dirs = Vec::new(); + if let Some(appdata) = std::env::var_os("APPDATA") { + dirs.push(PathBuf::from(appdata).join("npm")); // npm global bin (.cmd shims) + } + dirs.push(home.join(".cargo").join("bin")); + dirs.push(home.join(".bun").join("bin")); + dirs.push(home.join("scoop").join("shims")); + if let Some(programdata) = std::env::var_os("ProgramData") { + dirs.push(PathBuf::from(programdata).join("chocolatey").join("bin")); + } + if let Some(pf) = std::env::var_os("ProgramFiles") { + dirs.push(PathBuf::from(pf).join("Git").join("cmd")); // Git for Windows + } + dirs +} +``` + +- [ ] **Step 2: Verify (macOS check is a no-op for these arms; rely on windows-check after push)** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS (these functions are `#[cfg(windows)]` via `mod.rs`, so macOS check only confirms the shared tree still builds). + +- [ ] **Step 3: Commit** + +```bash +git add src-tauri/src/platform/windows.rs +git commit -m "feat(windows): implement reveal/open/open_path/extra_path_dirs" +``` + +--- + +### Task 4: `windows.rs` — `process_cwd` (PEB walk) + +The highest-risk item, transcribed from the fork's proven `read_cwd_windows` (adapted to the `process_cwd(pid) -> Option` signature and forward-slash normalization our callers expect is NOT required — macOS/Linux return native separators, so we keep backslashes). Reads another process's cwd by walking its PEB across the process boundary. x64 offsets only. Best-effort: any failure → `None` (caller falls back to spawn-time cwd). + +**Files:** +- Modify: `src-tauri/src/platform/windows.rs` — replace the `process_cwd` stub. + +**Interfaces:** +- Produces: `process_cwd(pid: u32) -> Option` (same signature as `linux.rs`/`macos.rs`). Consumed by `main.rs::read_agent_cwd`. + +- [ ] **Step 1: Replace the `process_cwd` stub** + +```rust +/// Read another process's current working directory by walking its PEB. There +/// is no public Win32 API for this, so we use the documented approach: +/// `NtQueryInformationProcess(ProcessBasicInformation)` for the PEB base, then +/// `ReadProcessMemory` to follow PEB → RTL_USER_PROCESS_PARAMETERS → +/// CurrentDirectory.DosPath (a UNICODE_STRING). Offsets are the x64 ABI (our +/// only target). Best-effort — any failure returns None. +pub fn process_cwd(pid: u32) -> Option { + use std::os::raw::c_void; + + type Handle = *mut c_void; + const PROCESS_QUERY_INFORMATION: u32 = 0x0400; + const PROCESS_VM_READ: u32 = 0x0010; + + #[repr(C)] + struct ProcessBasicInformation { + _reserved1: *mut c_void, + peb_base_address: *mut c_void, + _reserved2: [*mut c_void; 2], + _unique_process_id: usize, + _reserved3: *mut c_void, + } + + extern "system" { + fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle; + fn CloseHandle(h: Handle) -> i32; + fn ReadProcessMemory( + h: Handle, + base: *const c_void, + buf: *mut c_void, + size: usize, + read: *mut usize, + ) -> i32; + } + #[link(name = "ntdll")] + extern "system" { + fn NtQueryInformationProcess( + h: Handle, + class: u32, + info: *mut c_void, + len: u32, + ret_len: *mut u32, + ) -> i32; + } + + unsafe { + let h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, pid); + if h.is_null() { + return None; + } + // Closure so we CloseHandle on every exit path. + let out = (|| -> Option { + let read_at = |addr: usize, buf: *mut c_void, size: usize| -> bool { + let mut got = 0usize; + ReadProcessMemory(h, addr as *const c_void, buf, size, &mut got) != 0 && got == size + }; + + let mut pbi: ProcessBasicInformation = std::mem::zeroed(); + let mut ret_len = 0u32; + // ProcessBasicInformation class = 0. + if NtQueryInformationProcess( + h, + 0, + &mut pbi as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + &mut ret_len, + ) != 0 + { + return None; + } + let peb = pbi.peb_base_address as usize; + if peb == 0 { + return None; + } + + let ptr_size = std::mem::size_of::(); + // x64 offsets: + // PEB.ProcessParameters @ 0x20 + // RTL_USER_PROCESS_PARAMETERS.CurrentDirectory.DosPath @ 0x38 + // UNICODE_STRING { u16 Length; u16 MaxLength; PWSTR Buffer; } + // Length @ +0x00, Buffer ptr @ +0x08 + let mut params: usize = 0; + if !read_at(peb + 0x20, &mut params as *mut _ as *mut c_void, ptr_size) || params == 0 { + return None; + } + let mut len_u16: u16 = 0; + if !read_at(params + 0x38, &mut len_u16 as *mut _ as *mut c_void, 2) { + return None; + } + if len_u16 == 0 || len_u16 > 0x7ffe { + return None; + } + let mut buf_ptr: usize = 0; + if !read_at(params + 0x40, &mut buf_ptr as *mut _ as *mut c_void, ptr_size) || buf_ptr == 0 { + return None; + } + let n_chars = (len_u16 as usize) / 2; + let mut wbuf: Vec = vec![0u16; n_chars]; + if !read_at(buf_ptr, wbuf.as_mut_ptr() as *mut c_void, len_u16 as usize) { + return None; + } + let s = String::from_utf16_lossy(&wbuf); + let trimmed = s.trim_end_matches(['\\', '/']); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + })(); + CloseHandle(h); + out + } +} +``` + +- [ ] **Step 2: Verify shared tree still builds on macOS** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add src-tauri/src/platform/windows.rs +git commit -m "feat(windows): process_cwd via NtQueryInformationProcess PEB walk" +``` + +--- + +### Task 5: `windows.rs` — `clipboard_file_paths` (CF_HDROP) + `read_claude_credential` + +`clipboard_file_paths` reads file paths a user copied in Explorer, via raw `CF_HDROP` + `DragQueryFileW` FFI (no `windows` crate — same style as Task 4). `read_claude_credential` mirrors `linux.rs` exactly (Claude Code stores plaintext `.credentials.json` on Windows too; no keychain). If clipboard FFI returns empty at runtime the feature degrades gracefully (paste falls through to text). + +**Files:** +- Modify: `src-tauri/src/platform/windows.rs` — replace `clipboard_file_paths` and `read_claude_credential` stubs. + +**Interfaces:** +- Consumes: `super::creds::extract_access_token`, `crate::config::profile_config_dir` (both exist). +- Produces: `clipboard_file_paths() -> Vec`, `read_claude_credential(Option<&str>) -> Option` (signatures match `linux.rs`). + +- [ ] **Step 1: Replace the two stubs** + +```rust +/// Read file paths off the clipboard via the shell `CF_HDROP` format (what +/// Explorer puts there on Ctrl-C of files). Raw Win32 FFI — no `windows` crate. +/// Best-effort: returns an empty vec on any failure, so paste falls through to +/// text handling. +pub fn clipboard_file_paths() -> Vec { + use std::os::raw::c_void; + type Handle = *mut c_void; + const CF_HDROP: u32 = 15; + + #[link(name = "user32")] + extern "system" { + fn OpenClipboard(hwnd: Handle) -> i32; + fn CloseClipboard() -> i32; + fn GetClipboardData(format: u32) -> Handle; + } + #[link(name = "shell32")] + extern "system" { + fn DragQueryFileW(hdrop: Handle, index: u32, buf: *mut u16, cch: u32) -> u32; + } + + let mut paths = Vec::new(); + unsafe { + if OpenClipboard(std::ptr::null_mut()) == 0 { + return paths; + } + let hdrop = GetClipboardData(CF_HDROP); + if !hdrop.is_null() { + // index 0xFFFFFFFF returns the file count. + let count = DragQueryFileW(hdrop, 0xFFFF_FFFF, std::ptr::null_mut(), 0); + for i in 0..count { + // First call with null buffer returns length in chars (excl NUL). + let len = DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0); + if len == 0 { + continue; + } + let mut buf: Vec = vec![0u16; (len + 1) as usize]; + let got = DragQueryFileW(hdrop, i, buf.as_mut_ptr(), len + 1); + if got == 0 { + continue; + } + buf.truncate(got as usize); + paths.push(String::from_utf16_lossy(&buf)); + } + } + CloseClipboard(); + } + paths +} + +/// On Windows, Claude Code stores credentials as plaintext `.credentials.json` +/// inside the profile config dir (or `~/.claude`). No keychain — identical to +/// the Linux path. +pub fn read_claude_credential(profile_id: Option<&str>) -> Option { + let dir = match profile_id { + None | Some("") | Some("__default__") => dirs::home_dir()?.join(".claude"), + Some(id) => crate::config::profile_config_dir(id)?, + }; + let raw = std::fs::read_to_string(dir.join(".credentials.json")).ok()?; + super::creds::extract_access_token(&raw) +} +``` + +- [ ] **Step 2: Confirm `windows.rs` no longer imports anything unused** + +The `use std::path::{Path, PathBuf};` at the top of `windows.rs` is still needed (Path in reveal/open, PathBuf in extra_path_dirs). Leave it. No `todo!()` should remain. + +Run: `grep -c 'todo!' src-tauri/src/platform/windows.rs` +Expected: `0` + +- [ ] **Step 3: Verify shared tree builds on macOS** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src-tauri/src/platform/windows.rs +git commit -m "feat(windows): clipboard_file_paths via CF_HDROP; plaintext credential reader" +``` + +--- + +### Task 6: Windows shell + editor discovery + +Three Windows arms so the shell panel spawns PowerShell (not `/bin/zsh`), new tabs default to `pwsh`/`powershell`, and the editor picker works. `default_shell` and `start_shell_session` are in `main.rs`; editor discovery is in `sidebar.rs`. + +**Files:** +- Modify: `src-tauri/src/main.rs` — `default_shell` (~line 64), `start_shell_session` (~line 230). +- Modify: `src-tauri/src/sidebar.rs` — `installed_editors` (Windows arm ~line 296), `open_in_editor` (Windows arm ~line 328). + +**Interfaces:** +- Consumes: `crate::config::which`, `crate::config::silent_command`. +- Produces: no signature changes — only new `#[cfg(target_os = "windows")]` / `#[cfg(windows)]` branches. + +- [ ] **Step 1: `default_shell` — prefer pwsh, then powershell** + +Replace the `if cfg!(windows)` arm of `default_shell`: + +```rust +fn default_shell() -> Vec { + if cfg!(windows) { + // PowerShell 7 (pwsh) if installed, else Windows PowerShell 5. + if config::which("pwsh.exe") { + vec!["pwsh.exe".into()] + } else { + vec!["powershell.exe".into()] + } + } else if cfg!(target_os = "linux") { + vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into())] + } else { + vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".into())] + } +} +``` + +- [ ] **Step 2: `start_shell_session` — Windows branch (no `/bin/zsh -l`, no trampoline)** + +`start_shell_session` currently hardcodes `std::env::var("SHELL")` + `"-l"` + the zsh ZDOTDIR trampoline — all Unix-only. Gate the existing body as non-Windows and add a Windows branch. Wrap the current program/env/trampoline block: + +Find the start of the function body: + +```rust + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); + let program = vec![shell.clone(), "-l".to_string()]; + let path = config::augmented_path(); +``` + +Replace with a Windows-gated split. On Windows, `program = default_shell()` (no login flag, no OSC-7 trampoline — see Deferred section), and `SHELL`/`TERM_PROGRAM` env is Unix-flavored so we set only PATH + TERM basics: + +```rust + #[cfg(windows)] + let (program, mut env): (Vec, Vec<(String, String)>) = { + let path = config::augmented_path(); + ( + default_shell(), + vec![ + ("TERM".into(), "xterm-256color".into()), + ("COLORTERM".into(), "truecolor".into()), + ("PATH".into(), path.to_string_lossy().to_string()), + ], + ) + }; + + #[cfg(not(windows))] + let (program, mut env) = { + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); + let program = vec![shell.clone(), "-l".to_string()]; + let path = config::augmented_path(); + let mut env: Vec<(String, String)> = vec![ + ("TERM".into(), "xterm-256color".into()), + ("COLORTERM".into(), "truecolor".into()), + ("TERM_PROGRAM".into(), "iTerm.app".into()), + ("TERM_PROGRAM_VERSION".into(), "3.6.6".into()), + ("PATH".into(), path.to_string_lossy().to_string()), + ]; + // ZDOTDIR / PROMPT_COMMAND OSC-7 trampoline (zsh + bash) … + // <-- keep the ENTIRE existing trampoline block here, unchanged, then: + (program, env) + }; +``` + +**Note to implementer:** move the existing zsh/bash trampoline code verbatim inside the `#[cfg(not(windows))]` block before its `(program, env)` tail. Do not alter the trampoline logic. Everything after the env/trampoline setup (the `.spawn(...)` call) stays shared. Verify the `mut` on `env` is still warranted on non-Windows (the trampoline pushes to it) and that Windows `env` is `mut` only if later code mutates it — if the shared tail pushes to `env`, keep `mut` on both; otherwise drop `mut` on the Windows binding to avoid an `unused_mut` warning. + +- [ ] **Step 3: `installed_editors` — Windows arm** + +In `sidebar.rs`, the current `#[cfg(not(any(target_os = "macos", target_os = "linux")))] let found: Vec = vec![];` is the Windows fallback. Replace it with a real probe mirroring the Linux list (VS Code etc. ship `.cmd` launchers on PATH; `which_path` resolves them via the Task 2 ext-probe): + +```rust + #[cfg(target_os = "windows")] + let found = tauri::async_runtime::spawn_blocking(|| -> Vec { + // (binary_name, display_name) — resolved against the augmented PATH + // (which_path probes .cmd/.exe). bundle_id carries the binary name. + const WINDOWS_EDITORS: &[(&str, &str)] = &[ + ("code", "VS Code"), ("cursor", "Cursor"), ("windsurf", "Windsurf"), + ("zed", "Zed"), ("subl", "Sublime Text"), ("nvim", "Neovim"), + ("code-insiders", "VS Code Insiders"), + ]; + WINDOWS_EDITORS.iter().filter_map(|&(bin, name)| { + config::which_path(bin).map(|_| EditorInfo { + bundle_id: bin.to_string(), + display_name: name.to_string(), + }) + }).collect() + }).await.map_err(|e| e.to_string())?; + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + let found: Vec = vec![]; +``` + +- [ ] **Step 4: `open_in_editor` — Windows arm** + +Add a Windows branch (spawn via `silent_command` so a console editor launcher doesn't flash), and narrow the final catch-all: + +```rust + #[cfg(target_os = "windows")] + { + let bin = config::which_path(&bundle_id).ok_or_else(|| format!("{bundle_id} not found"))?; + config::silent_command(bin).arg(&path).spawn().map_err(|e| e.to_string())?; + return Ok(()); + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] + { + Err("open_in_editor is not supported on this platform".to_string()) + } +``` + +- [ ] **Step 5: Verify macOS builds** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS (macOS arms untouched). + +- [ ] **Step 6: Commit** + +```bash +git add src-tauri/src/main.rs src-tauri/src/sidebar.rs +git commit -m "feat(windows): pwsh default shell, Windows shell panel, editor discovery" +``` + +--- + +### Task 7: Console-flash suppression on git/gh spawns + +`git` fires many times on startup (worktree discovery × auto-resumed tabs) and `gh` on every GitHub-tab action. Each is a console program → a flashing black window per call on Windows unless spawned with `CREATE_NO_WINDOW`. Route the three choke points through `config::silent_command`. No-op on macOS/Linux. + +**Files:** +- Modify: `src-tauri/src/git.rs` — `run_git` (~line 40), `run_git_capture_stdout` (~line 60). +- Modify: `src-tauri/src/github/client.rs` — `run_gh` (~line 15). + +**Interfaces:** +- Consumes: `crate::config::silent_command` (Task 2). + +- [ ] **Step 1: `git.rs` — both runners** + +In `run_git` and `run_git_capture_stdout`, replace `Command::new(&git)` with `crate::config::silent_command(&git)`. (Both already resolve `git` via `which_path` into the `git` binding, so only the constructor changes.) Remove/adjust any now-unused `use std::process::Command;` only if nothing else in the file uses it — otherwise leave it. + +- [ ] **Step 2: `github/client.rs` — `run_gh`** + +Replace `Command::new(&gh)` with `crate::config::silent_command(&gh)`. + +- [ ] **Step 3: Verify macOS builds (silent_command is a no-op transform there)** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS. + +- [ ] **Step 4: Commit** + +```bash +git add src-tauri/src/git.rs src-tauri/src/github/client.rs +git commit -m "fix(windows): suppress console flash on git/gh spawns via silent_command" +``` + +--- + +### Task 8: Bundle + release leg (NSIS + windows-latest) + +Add the NSIS installer target and the `windows-latest` release matrix leg. `tauri-action` builds NSIS + updater artifacts (`.exe`, `.nsis.zip`, `.sig`) and merges the `windows-x86_64` key into `latest.json` automatically. Unsigned (SmartScreen warning accepted). + +**Files:** +- Modify: `src-tauri/tauri.conf.json` — `bundle.targets`. +- Modify: `.github/workflows/release.yml` — matrix + stale comment. + +**Interfaces:** +- Produces: a `windows-latest / x86_64-pc-windows-msvc` release job; NSIS artifacts on the GitHub release; a `windows-x86_64` entry in `latest.json`. + +- [ ] **Step 1: Add `"nsis"` to bundle targets** + +In `tauri.conf.json`, change: + +```json + "targets": ["deb", "appimage", "app", "dmg"], +``` +to: +```json + "targets": ["deb", "appimage", "app", "dmg", "nsis"], +``` + +(`icon.ico` is already in the icon array; no NSIS-specific config needed — Tauri defaults produce a per-user installer.) + +- [ ] **Step 2: Add the Windows matrix leg + update the stale comment in `release.yml`** + +Change the matrix include list from: + +```yaml + include: + - { os: macos-latest, target: aarch64-apple-darwin } + - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu } + - { os: ubuntu-22.04-arm, target: aarch64-unknown-linux-gnu } +``` +to: +```yaml + include: + - { os: macos-latest, target: aarch64-apple-darwin } + - { os: ubuntu-22.04, target: x86_64-unknown-linux-gnu } + - { os: ubuntu-22.04-arm, target: aarch64-unknown-linux-gnu } + - { os: windows-latest, target: x86_64-pc-windows-msvc } +``` + +And delete the now-false top-of-file comment `# Windows is intentionally excluded until Milestone 2 (windows.rs is stubbed).` (the `Linux deps` step is already `if: startsWith(matrix.os, 'ubuntu')`, so it correctly skips on Windows; the shared `npm ci` + `tauri-action` steps run on Windows unchanged). + +- [ ] **Step 3: Validate both files** + +Run: `python3 -c "import json; json.load(open('src-tauri/tauri.conf.json')); print('json ok')"` +Expected: `json ok` +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/release.yml')); print('yaml ok')"` +Expected: `yaml ok` + +- [ ] **Step 4: Commit** + +```bash +git add src-tauri/tauri.conf.json .github/workflows/release.yml +git commit -m "ci(windows): add NSIS bundle target and windows-latest release leg" +``` + +--- + +### Task 9: Docs — CHANGELOG, CLAUDE.md cwd degradation note + +Record the milestone in the CHANGELOG (release notes are extracted from it by the `gate` job) and document the one parity gap in `CLAUDE.md`. + +**Files:** +- Modify: `CHANGELOG.md` — new top section for the release version. +- Modify: `CLAUDE.md` — note the Windows shell-cwd degradation. + +**Interfaces:** +- Produces: a `## [X.Y.Z]` CHANGELOG section the release `gate` job's awk extractor will pick up as `notes`. + +- [ ] **Step 1: Add CHANGELOG section** + +Prepend a section under the CHANGELOG header (the version is bumped in Task 10; use the same number there — implementer uses the value the controller supplies): + +```markdown +## [X.Y.Z] + +### Added +- **Windows support (x64).** Vector now builds, runs, and self-updates on Windows 10/11 via an NSIS installer. Agent cwd tracking (PEB walk), file reveal in Explorer, open-in-default-app, clipboard file paste, editor discovery, and PowerShell/pwsh shells are all wired up. + +### Known limitations +- On Windows, a **shell** pane's directory does not update live on `cd` (the cwd shown refreshes when the shell panel is opened). Agent panes are unaffected. Live shell-cwd tracking (a PowerShell prompt trampoline) is a planned follow-up. +- Windows builds are **unsigned**; SmartScreen shows a "Windows protected your PC" warning on first launch (click *More info → Run anyway*). +``` + +- [ ] **Step 2: Add the CLAUDE.md note** + +Under the `## Gotchas` section of `CLAUDE.md`, add one bullet: + +```markdown +- **Windows shell cwd**: Windows ships without the OSC-7 shell trampoline (macOS/Linux use zsh ZDOTDIR / bash PROMPT_COMMAND). `read_agent_cwd` (PEB walk in `platform/windows.rs::process_cwd`) covers agent panes and shell-panel-expand; a shell pane's live cwd does not follow `cd`. Deferred follow-up: a PowerShell `prompt`-function trampoline. +``` + +- [ ] **Step 3: Commit** + +```bash +git add CHANGELOG.md CLAUDE.md +git commit -m "docs: Windows support changelog + shell-cwd degradation note" +``` + +--- + +### Task 10: Version bump, ship, and CI verification + +Bump the version across the three files, confirm `windows-check` is green on the branch, then (after merge) confirm the release job produces Windows artifacts + a `windows-x86_64` `latest.json` key. Runtime smoke happens on a real Windows box. + +**Files:** +- Modify: `package.json`, `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json` — version. +- Modify: `CHANGELOG.md` — replace `X.Y.Z` placeholder with the real version. + +- [ ] **Step 1: Pick the version and bump all three files + lockfiles** + +Bump `package.json`, `src-tauri/tauri.conf.json`, and `src-tauri/Cargo.toml` `version` to the chosen `X.Y.Z` (minor bump for a new platform, e.g. `0.5.0`), replace the `X.Y.Z` in the CHANGELOG heading, then sync lockfiles: + +```bash +npm install --package-lock-only +cargo update -p vector --manifest-path src-tauri/Cargo.toml +``` + +- [ ] **Step 2: Confirm the three versions agree (the gate job's guard)** + +Run: +```bash +node -p "require('./package.json').version" +node -p "require('./src-tauri/tauri.conf.json').version" +grep -m1 '^version' src-tauri/Cargo.toml +``` +Expected: all three print the same `X.Y.Z`. + +- [ ] **Step 3: Full macOS typecheck of the whole change** + +Run: `cargo check --manifest-path src-tauri/Cargo.toml` +Expected: PASS. + +- [ ] **Step 4: Commit + push, then confirm `windows-check` is green** + +```bash +git add package.json package-lock.json src-tauri/Cargo.toml src-tauri/tauri.conf.json CHANGELOG.md +git commit -m "release: prepare vX.Y.Z (Windows support)" +git push +``` +Run: `gh run watch $(gh run list --workflow windows-check.yml -L1 --json databaseId -q '.[0].databaseId')` +Expected: `windows-check` **PASS** — this is the milestone's key automated gate (all Windows `#[cfg]` arms compile on a real Windows toolchain). + +- [ ] **Step 5: After merge to main, confirm the release job produced Windows artifacts** + +Once merged (the version bump triggers `release.yml`): +Run: `gh release view vX.Y.Z --json assets -q '.assets[].name'` +Expected: includes a Windows NSIS `.exe`, a `.nsis.zip`, and a `.sig`; and `latest.json` contains a `windows-x86_64` platform key (verify: `gh release download vX.Y.Z -p latest.json -O - | python3 -c "import json,sys; print('windows-x86_64' in json.load(sys.stdin)['platforms'])"` → `True`). + +- [ ] **Step 6: Windows runtime smoke checklist (manual, on a real Windows box)** + +Not automatable here. Record results before declaring the milestone done: +- App launches (past SmartScreen); a tab spawns Claude and it renders without VT corruption (ConPTY + aggressive filter). +- `cd` in an agent pane updates the tab cwd (PEB walk). +- Right-click → Reveal opens Explorer with the file selected; Open uses the default app. +- Copy files in Explorer → paste into a pane types the paths (CF_HDROP). +- Editor picker lists installed editors; open-in-editor launches one. +- No console windows flash during startup/GitHub actions. +- The in-app updater sees the next release and updates. + +--- + +## Self-Review + +**Spec coverage (design doc Milestone 2 items 1–8):** +1. `process_cwd` PEB walk → Task 4. ✅ +2. Live cwd trampoline → **deferred** (documented degradation, Deferred section + Task 9). ✅ (sanctioned by spec fallback clause) +3. `augmented_path` Windows dirs → Task 3 (`extra_path_dirs`). ✅ +4. Clipboard CF_HDROP → Task 5. ✅ (raw FFI instead of `windows` crate — Global Constraint) +5. reveal/open → Task 3. ✅ +6. `default_shell` pwsh→powershell → Task 6. ✅ +7. PTY/ConPTY re-verify → Task 10 Step 6 (filter unchanged; the fork confirmed no filter change needed — re-verified at smoke). ✅ +8. NSIS bundle + latest.json windows key → Task 8 + Task 10 Step 5. ✅ +- Load-bearing extra not in spec: `which_path` exe-ordering (Task 2) — without it npm agents don't launch on Windows. ✅ +- Console-flash suppression (Task 2 + 7) — Windows UX. ✅ +- Windows shell panel + editor discovery (Task 6) — parity. ✅ + +**Placeholder scan:** `X.Y.Z` in Tasks 9–10 is intentional (controller supplies the concrete version at execution); every code block is complete. No TBD/TODO. + +**Type consistency:** `silent_command` signature identical in Tasks 2/3/6/7. `process_cwd`/`clipboard_file_paths`/`read_claude_credential`/`reveal_in_file_manager`/`open_default_app`/`open_path`/`extra_path_dirs` match the `linux.rs` signatures the module surface requires. `EditorInfo { bundle_id, display_name }` matches the Linux arm. +``` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 04c4bb2..b4c2810 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "vector", - "version": "0.4.2", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "vector", - "version": "0.4.2", + "version": "0.5.0", "dependencies": { "@iconify/react": "6.0.2", "@tauri-apps/api": "2.11.1", diff --git a/package.json b/package.json index b893b79..461fd5f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vector", "private": true, - "version": "0.4.2", + "version": "0.5.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index af7838d..cc19402 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -5629,7 +5629,7 @@ dependencies = [ [[package]] name = "vector" -version = "0.4.2" +version = "0.5.0" dependencies = [ "anyhow", "dirs 5.0.1", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0f9f27d..ed5e807 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vector" -version = "0.4.2" +version = "0.5.0" description = "Vector — agent-first terminal" edition = "2021" diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 291aaef..09c1927 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -214,15 +214,27 @@ pub fn which_path(bin: &str) -> Option { } let path = augmented_path(); for dir in std::env::split_paths(&path) { - let full = dir.join(bin); - if full.is_file() { return Some(full); } + // On Windows, prefer the executable-extension variants. An extensionless + // file is not runnable via CreateProcess — and npm ships a bare shell + // shim (`claude`) alongside `claude.cmd`, so matching the bare name first + // would hand back the unrunnable sh script. Skip the ext probe when `bin` + // already carries an executable extension. #[cfg(windows)] { - for ext in ["exe", "cmd", "bat"] { - let f = dir.join(format!("{bin}.{ext}")); - if f.is_file() { return Some(f); } + let has_exec_ext = std::path::Path::new(bin) + .extension() + .and_then(|e| e.to_str()) + .map(|e| matches!(e.to_ascii_lowercase().as_str(), "exe" | "cmd" | "bat" | "com")) + .unwrap_or(false); + if !has_exec_ext { + for ext in ["exe", "cmd", "bat"] { + let f = dir.join(format!("{bin}.{ext}")); + if f.is_file() { return Some(f); } + } } } + let full = dir.join(bin); + if full.is_file() { return Some(full); } } None } @@ -231,6 +243,25 @@ pub fn which(bin: &str) -> bool { which_path(bin).is_some() } +/// Build a `std::process::Command` that does NOT flash a console window on +/// Windows. A GUI-subsystem app (Vector) spawning a console program — `git`, +/// `gh`, `cmd`, `explorer` — pops a visible console window per call unless +/// `CREATE_NO_WINDOW` is set. Worktree discovery alone fires many `git` +/// invocations on startup (× every auto-resumed tab), so without this the +/// window flickers with console pop-ups for a few seconds. No-op off Windows. +#[allow(dead_code)] +pub fn silent_command>(program: S) -> std::process::Command { + #[allow(unused_mut)] + let mut cmd = std::process::Command::new(program); + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + cmd.creation_flags(CREATE_NO_WINDOW); + } + cmd +} + /// Whether an agent caches terminal colors at startup and needs a process /// restart to pick up a Vector theme change. The frontend shows a banner /// offering a one-click restart for these agents. diff --git a/src-tauri/src/git.rs b/src-tauri/src/git.rs index 4fb1898..9f7e281 100644 --- a/src-tauri/src/git.rs +++ b/src-tauri/src/git.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::process::Command; use serde::Serialize; #[derive(Debug, Clone, Serialize)] @@ -37,7 +36,7 @@ fn git_bin() -> Result { fn run_git(args: &[&str], cwd: &Path) -> Result { let git = git_bin()?; - let out = Command::new(&git) + let out = crate::config::silent_command(&git) .args(args) .current_dir(cwd) .output() @@ -57,7 +56,7 @@ fn run_git(args: &[&str], cwd: &Path) -> Result { /// `git diff --no-index` exits 1 when files differ, with the diff on stdout. fn run_git_capture_stdout(args: &[&str], cwd: &Path) -> Result { let git = git_bin()?; - let out = Command::new(&git) + let out = crate::config::silent_command(&git) .args(args) .current_dir(cwd) .output() diff --git a/src-tauri/src/github/client.rs b/src-tauri/src/github/client.rs index 75eaaeb..869a007 100644 --- a/src-tauri/src/github/client.rs +++ b/src-tauri/src/github/client.rs @@ -1,5 +1,3 @@ -use std::process::Command; - use crate::config; /// Resolve the `gh` binary against Vector's augmented PATH (GUI apps start with @@ -12,7 +10,7 @@ pub fn gh_path() -> Option { /// `Err(stderr)`. PATH is augmented so gh's own child processes (git) resolve. pub fn run_gh(args: &[&str]) -> Result { let gh = gh_path().ok_or_else(|| "gh CLI not found on PATH".to_string())?; - let out = Command::new(&gh) + let out = crate::config::silent_command(&gh) .args(args) .env("PATH", config::augmented_path()) .output() diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index f48e50a..aa0d6df 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -63,7 +63,12 @@ async fn default_agent(state: State<'_, AppState>) -> Result { fn default_shell() -> Vec { if cfg!(windows) { - vec!["powershell.exe".into()] + // PowerShell 7 (pwsh) if installed, else Windows PowerShell 5. + if config::which("pwsh.exe") { + vec!["pwsh.exe".into()] + } else { + vec!["powershell.exe".into()] + } } else if cfg!(target_os = "linux") { vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into())] } else { @@ -235,30 +240,48 @@ async fn start_shell_session( cols: u16, rows: u16, ) -> Result<(), String> { - let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); - let program = vec![shell.clone(), "-l".to_string()]; - let path = config::augmented_path(); - let mut env: Vec<(String, String)> = vec![ - ("TERM".into(), "xterm-256color".into()), - ("COLORTERM".into(), "truecolor".into()), - ("TERM_PROGRAM".into(), "iTerm.app".into()), - ("TERM_PROGRAM_VERSION".into(), "3.6.6".into()), - ("PATH".into(), path.to_string_lossy().to_string()), - ]; - - // ZDOTDIR trampoline: inject OSC 7 precmd silently for zsh only. - // For bash/fish we skip this for v0.3.3 — live cwd tracking won't update past initial. - if shell.ends_with("/zsh") || shell == "zsh" { - let zdotdir = std::env::temp_dir() - .join(format!("vector-zdotdir-{}", std::process::id())); - std::fs::create_dir_all(&zdotdir).map_err(|e| e.to_string())?; - - // Capture the user's existing ZDOTDIR (or $HOME) so we can chain to their real .zshrc. - let user_zdotdir = std::env::var("ZDOTDIR") - .unwrap_or_else(|_| dirs::home_dir().unwrap_or_default().to_string_lossy().to_string()); + // On Windows the shell is PowerShell (no `-l`, and no OSC-7 trampoline — + // live shell-cwd tracking is a deferred feature; see CLAUDE.md). Elsewhere + // the login shell gets the zsh/bash OSC-7 trampoline, unchanged. + #[cfg(windows)] + let (program, env): (Vec, Vec<(String, String)>) = { + let path = config::augmented_path(); + ( + default_shell(), + vec![ + ("TERM".into(), "xterm-256color".into()), + ("COLORTERM".into(), "truecolor".into()), + ("PATH".into(), path.to_string_lossy().to_string()), + ], + ) + }; - let zshrc_content = format!( - r#"# Vector shell trampoline — emit OSC 7 on every prompt so the host can track cwd. + #[cfg(not(windows))] + let (program, env) = { + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string()); + let program = vec![shell.clone(), "-l".to_string()]; + let path = config::augmented_path(); + let mut env: Vec<(String, String)> = vec![ + ("TERM".into(), "xterm-256color".into()), + ("COLORTERM".into(), "truecolor".into()), + ("TERM_PROGRAM".into(), "iTerm.app".into()), + ("TERM_PROGRAM_VERSION".into(), "3.6.6".into()), + ("PATH".into(), path.to_string_lossy().to_string()), + ]; + + // ZDOTDIR trampoline: inject OSC 7 precmd silently for zsh only. + // For bash/fish we skip this for v0.3.3 — live cwd tracking won't update past initial. + if shell.ends_with("/zsh") || shell == "zsh" { + let zdotdir = std::env::temp_dir() + .join(format!("vector-zdotdir-{}", std::process::id())); + std::fs::create_dir_all(&zdotdir).map_err(|e| e.to_string())?; + + // Capture the user's existing ZDOTDIR (or $HOME) so we can chain to their real .zshrc. + let user_zdotdir = std::env::var("ZDOTDIR") + .unwrap_or_else(|_| dirs::home_dir().unwrap_or_default().to_string_lossy().to_string()); + + let zshrc_content = format!( + r#"# Vector shell trampoline — emit OSC 7 on every prompt so the host can track cwd. _vector_osc7() {{ printf '\033]7;file://%s%s\007' "$HOST" "$PWD" }} typeset -ga precmd_functions precmd_functions+=(_vector_osc7) @@ -266,25 +289,28 @@ _vector_osc7 # Source the user's real .zshrc if present [ -f "{user_zdotdir}/.zshrc" ] && source "{user_zdotdir}/.zshrc" "# - ); - let zshrc_path = zdotdir.join(".zshrc"); - std::fs::write(&zshrc_path, &zshrc_content).map_err(|e| e.to_string())?; + ); + let zshrc_path = zdotdir.join(".zshrc"); + std::fs::write(&zshrc_path, &zshrc_content).map_err(|e| e.to_string())?; - env.push(("ZDOTDIR".into(), zdotdir.to_string_lossy().to_string())); - env.push(("VECTOR_USER_ZDOTDIR".into(), user_zdotdir)); - } - // bash: PROMPT_COMMAND is read from the environment, so we can inject OSC-7 - // without a temp rcfile. Prepend so the user's own PROMPT_COMMAND still runs. - else if shell.ends_with("/bash") || shell == "bash" { - let existing = std::env::var("PROMPT_COMMAND").unwrap_or_default(); - let osc7 = r#"printf '\033]7;file://%s%s\007' "$HOSTNAME" "$PWD""#; - let combined = if existing.is_empty() { - osc7.to_string() - } else { - format!("{osc7}; {existing}") - }; - env.push(("PROMPT_COMMAND".into(), combined)); - } + env.push(("ZDOTDIR".into(), zdotdir.to_string_lossy().to_string())); + env.push(("VECTOR_USER_ZDOTDIR".into(), user_zdotdir)); + } + // bash: PROMPT_COMMAND is read from the environment, so we can inject OSC-7 + // without a temp rcfile. Prepend so the user's own PROMPT_COMMAND still runs. + else if shell.ends_with("/bash") || shell == "bash" { + let existing = std::env::var("PROMPT_COMMAND").unwrap_or_default(); + let osc7 = r#"printf '\033]7;file://%s%s\007' "$HOSTNAME" "$PWD""#; + let combined = if existing.is_empty() { + osc7.to_string() + } else { + format!("{osc7}; {existing}") + }; + env.push(("PROMPT_COMMAND".into(), combined)); + } + + (program, env) + }; let cwd_path = cwd .map(std::path::PathBuf::from) diff --git a/src-tauri/src/platform/windows.rs b/src-tauri/src/platform/windows.rs index 842ec53..44befcb 100644 --- a/src-tauri/src/platform/windows.rs +++ b/src-tauri/src/platform/windows.rs @@ -1,11 +1,226 @@ use std::path::{Path, PathBuf}; pub fn open_path(target: &str) -> std::io::Result<()> { - std::process::Command::new("cmd").args(["/C", "start", "", target]).spawn().map(|_| ()) -} -pub fn reveal_in_file_manager(_path: &Path) -> Result<(), String> { todo!("Milestone 2: explorer /select") } -pub fn open_default_app(_path: &Path) -> Result<(), String> { todo!("Milestone 2: ShellExecute") } -pub fn extra_path_dirs(_home: &Path) -> Vec { todo!("Milestone 2: %APPDATA%\\npm, scoop, etc.") } -pub fn process_cwd(_pid: u32) -> Option { todo!("Milestone 2: NtQueryInformationProcess PEB walk") } -pub fn clipboard_file_paths() -> Vec { todo!("Milestone 2: CF_HDROP") } -pub fn read_claude_credential(_profile_id: Option<&str>) -> Option { todo!("Milestone 2: plaintext .credentials.json") } + // The empty "" is `start`'s title argument — required so a quoted path + // isn't consumed as the window title. + crate::config::silent_command("cmd") + .args(["/C", "start", "", target]) + .spawn() + .map(|_| ()) +} + +/// Windows: `explorer /select,` highlights the file in a new Explorer +/// window. explorer.exe returns a nonzero exit code even on success, so we +/// spawn-and-forget without checking status. +pub fn reveal_in_file_manager(path: &Path) -> Result<(), String> { + crate::config::silent_command("explorer.exe") + .arg(format!("/select,{}", path.display())) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +/// Windows: `cmd /C start "" ` opens the path in its default handler. +pub fn open_default_app(path: &Path) -> Result<(), String> { + crate::config::silent_command("cmd") + .args(["/C", "start", ""]) + .arg(path) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +/// Extra PATH dirs where Windows package managers drop agent/git shims. macOS +/// GUI apps start with a minimal PATH; Windows GUI apps inherit the fuller user +/// PATH, but these cover common setups where a shim dir isn't on PATH yet. +pub fn extra_path_dirs(home: &Path) -> Vec { + let mut dirs = Vec::new(); + if let Some(appdata) = std::env::var_os("APPDATA") { + dirs.push(PathBuf::from(appdata).join("npm")); // npm global bin (.cmd shims) + } + dirs.push(home.join(".cargo").join("bin")); + dirs.push(home.join(".bun").join("bin")); + dirs.push(home.join("scoop").join("shims")); + if let Some(programdata) = std::env::var_os("ProgramData") { + dirs.push(PathBuf::from(programdata).join("chocolatey").join("bin")); + } + if let Some(pf) = std::env::var_os("ProgramFiles") { + dirs.push(PathBuf::from(pf).join("Git").join("cmd")); // Git for Windows + } + dirs +} +/// Read another process's current working directory by walking its PEB. There +/// is no public Win32 API for this, so we use the documented approach: +/// `NtQueryInformationProcess(ProcessBasicInformation)` for the PEB base, then +/// `ReadProcessMemory` to follow PEB → RTL_USER_PROCESS_PARAMETERS → +/// CurrentDirectory.DosPath (a UNICODE_STRING). Offsets are the x64 ABI (our +/// only target). Best-effort — any failure returns None. +pub fn process_cwd(pid: u32) -> Option { + use std::os::raw::c_void; + + type Handle = *mut c_void; + const PROCESS_QUERY_INFORMATION: u32 = 0x0400; + const PROCESS_VM_READ: u32 = 0x0010; + + #[repr(C)] + struct ProcessBasicInformation { + _reserved1: *mut c_void, + peb_base_address: *mut c_void, + _reserved2: [*mut c_void; 2], + _unique_process_id: usize, + _reserved3: *mut c_void, + } + + extern "system" { + fn OpenProcess(access: u32, inherit: i32, pid: u32) -> Handle; + fn CloseHandle(h: Handle) -> i32; + fn ReadProcessMemory( + h: Handle, + base: *const c_void, + buf: *mut c_void, + size: usize, + read: *mut usize, + ) -> i32; + } + #[link(name = "ntdll")] + extern "system" { + fn NtQueryInformationProcess( + h: Handle, + class: u32, + info: *mut c_void, + len: u32, + ret_len: *mut u32, + ) -> i32; + } + + unsafe { + let h = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 0, pid); + if h.is_null() { + return None; + } + // Closure so we CloseHandle on every exit path. + let out = (|| -> Option { + let read_at = |addr: usize, buf: *mut c_void, size: usize| -> bool { + let mut got = 0usize; + ReadProcessMemory(h, addr as *const c_void, buf, size, &mut got) != 0 && got == size + }; + + let mut pbi: ProcessBasicInformation = std::mem::zeroed(); + let mut ret_len = 0u32; + // ProcessBasicInformation class = 0. + if NtQueryInformationProcess( + h, + 0, + &mut pbi as *mut _ as *mut c_void, + std::mem::size_of::() as u32, + &mut ret_len, + ) != 0 + { + return None; + } + let peb = pbi.peb_base_address as usize; + if peb == 0 { + return None; + } + + let ptr_size = std::mem::size_of::(); + // x64 offsets: + // PEB.ProcessParameters @ 0x20 + // RTL_USER_PROCESS_PARAMETERS.CurrentDirectory.DosPath @ 0x38 + // UNICODE_STRING { u16 Length; u16 MaxLength; PWSTR Buffer; } + // Length @ +0x00, Buffer ptr @ +0x08 + let mut params: usize = 0; + if !read_at(peb + 0x20, &mut params as *mut _ as *mut c_void, ptr_size) || params == 0 { + return None; + } + let mut len_u16: u16 = 0; + if !read_at(params + 0x38, &mut len_u16 as *mut _ as *mut c_void, 2) { + return None; + } + // Reject 0, over-long, or odd Length: a UTF-16 byte length must be + // even, and an odd value would make `wbuf` (sized len/2 u16s) one + // byte short of the `len_u16`-byte ReadProcessMemory below. + if len_u16 == 0 || len_u16 > 0x7ffe || len_u16 % 2 != 0 { + return None; + } + let mut buf_ptr: usize = 0; + if !read_at(params + 0x40, &mut buf_ptr as *mut _ as *mut c_void, ptr_size) || buf_ptr == 0 { + return None; + } + let n_chars = (len_u16 as usize) / 2; + let mut wbuf: Vec = vec![0u16; n_chars]; + if !read_at(buf_ptr, wbuf.as_mut_ptr() as *mut c_void, len_u16 as usize) { + return None; + } + let s = String::from_utf16_lossy(&wbuf); + let trimmed = s.trim_end_matches(['\\', '/']); + if trimmed.is_empty() { + None + } else { + Some(trimmed.to_string()) + } + })(); + CloseHandle(h); + out + } +} +/// Read file paths off the clipboard via the shell `CF_HDROP` format (what +/// Explorer puts there on Ctrl-C of files). Raw Win32 FFI — no `windows` crate. +/// Best-effort: returns an empty vec on any failure, so paste falls through to +/// text handling. +pub fn clipboard_file_paths() -> Vec { + use std::os::raw::c_void; + type Handle = *mut c_void; + const CF_HDROP: u32 = 15; + + #[link(name = "user32")] + extern "system" { + fn OpenClipboard(hwnd: Handle) -> i32; + fn CloseClipboard() -> i32; + fn GetClipboardData(format: u32) -> Handle; + } + #[link(name = "shell32")] + extern "system" { + fn DragQueryFileW(hdrop: Handle, index: u32, buf: *mut u16, cch: u32) -> u32; + } + + let mut paths = Vec::new(); + unsafe { + if OpenClipboard(std::ptr::null_mut()) == 0 { + return paths; + } + let hdrop = GetClipboardData(CF_HDROP); + if !hdrop.is_null() { + // index 0xFFFFFFFF returns the file count. + let count = DragQueryFileW(hdrop, 0xFFFF_FFFF, std::ptr::null_mut(), 0); + for i in 0..count { + // First call with null buffer returns length in chars (excl NUL). + let len = DragQueryFileW(hdrop, i, std::ptr::null_mut(), 0); + if len == 0 { + continue; + } + let mut buf: Vec = vec![0u16; (len + 1) as usize]; + let got = DragQueryFileW(hdrop, i, buf.as_mut_ptr(), len + 1); + if got == 0 { + continue; + } + buf.truncate(got as usize); + paths.push(String::from_utf16_lossy(&buf)); + } + } + CloseClipboard(); + } + paths +} + +/// On Windows, Claude Code stores credentials as plaintext `.credentials.json` +/// inside the profile config dir (or `~/.claude`). No keychain — identical to +/// the Linux path. +pub fn read_claude_credential(profile_id: Option<&str>) -> Option { + let dir = match profile_id { + None | Some("") | Some("__default__") => dirs::home_dir()?.join(".claude"), + Some(id) => crate::config::profile_config_dir(id)?, + }; + let raw = std::fs::read_to_string(dir.join(".credentials.json")).ok()?; + super::creds::extract_access_token(&raw) +} diff --git a/src-tauri/src/pty.rs b/src-tauri/src/pty.rs index 7b9a493..a445e12 100644 --- a/src-tauri/src/pty.rs +++ b/src-tauri/src/pty.rs @@ -240,6 +240,23 @@ impl PtyRegistry { })?; let (cmd_name, rest) = program.split_first().ok_or_else(|| anyhow::anyhow!("empty command"))?; + // On Windows, batch shims (`.cmd`/`.bat` — e.g. npm's `claude.cmd`) are + // not PE images and cannot be launched via ConPTY's CreateProcessW + // directly; they must run through `cmd.exe /c`. Native `.exe`s spawn + // as-is. (which_path resolves agents to their `.cmd` shim on Windows.) + #[cfg(windows)] + let mut cmd = { + let lower = cmd_name.to_ascii_lowercase(); + if lower.ends_with(".cmd") || lower.ends_with(".bat") { + let mut c = CommandBuilder::new("cmd.exe"); + c.arg("/c"); + c.arg(cmd_name); + c + } else { + CommandBuilder::new(cmd_name) + } + }; + #[cfg(not(windows))] let mut cmd = CommandBuilder::new(cmd_name); for a in rest { cmd.arg(a); } if let Some(cwd) = cwd { cmd.cwd(cwd); } diff --git a/src-tauri/src/sidebar.rs b/src-tauri/src/sidebar.rs index f60e41d..0d80ddf 100644 --- a/src-tauri/src/sidebar.rs +++ b/src-tauri/src/sidebar.rs @@ -293,7 +293,24 @@ pub async fn installed_editors(state: State<'_, AppState>) -> Result Vec { + // (binary_name, display_name) — resolved against the augmented PATH + // (which_path probes .cmd/.exe). bundle_id carries the binary name. + const WINDOWS_EDITORS: &[(&str, &str)] = &[ + ("code", "VS Code"), ("cursor", "Cursor"), ("windsurf", "Windsurf"), + ("zed", "Zed"), ("subl", "Sublime Text"), ("nvim", "Neovim"), + ("code-insiders", "VS Code Insiders"), + ]; + WINDOWS_EDITORS.iter().filter_map(|&(bin, name)| { + config::which_path(bin).map(|_| EditorInfo { + bundle_id: bin.to_string(), + display_name: name.to_string(), + }) + }).collect() + }).await.map_err(|e| e.to_string())?; + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] let found: Vec = vec![]; *state.installed_editors.lock() = Some(found.clone()); @@ -325,7 +342,14 @@ pub fn open_in_editor(bundle_id: String, path: PathBuf) -> Result<(), String> { return Ok(()); } - #[cfg(not(any(target_os = "macos", target_os = "linux")))] + #[cfg(target_os = "windows")] + { + let bin = config::which_path(&bundle_id).ok_or_else(|| format!("{bundle_id} not found"))?; + config::silent_command(bin).arg(&path).spawn().map_err(|e| e.to_string())?; + return Ok(()); + } + + #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))] { Err("open_in_editor is not supported on this platform".to_string()) } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ed44a9d..8905504 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": "Vector", - "version": "0.4.2", + "version": "0.5.0", "identifier": "dev.vector.app", "build": { "beforeDevCommand": "npm run dev", @@ -38,7 +38,7 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": ["deb", "appimage", "app", "dmg"], + "targets": ["deb", "appimage", "app", "dmg", "nsis"], "icon": [ "icons/32x32.png", "icons/128x128.png", diff --git a/website/src/components/DownloadCTA.astro b/website/src/components/DownloadCTA.astro index 17ebb2f..d85709e 100644 --- a/website/src/components/DownloadCTA.astro +++ b/website/src/components/DownloadCTA.astro @@ -6,7 +6,8 @@ const releases = "https://github.com/avram19/vector/releases/latest"; -

Unsigned builds — see the docs for the first-launch step on macOS. Updates arrive in-app.

+

Unsigned builds — see the docs for the first-launch step on macOS and Windows. Updates arrive in-app.

Total downloads

diff --git a/website/src/content/docs/architecture/build-from-source.md b/website/src/content/docs/architecture/build-from-source.md index 0ed9ba6..ace6f13 100644 --- a/website/src/content/docs/architecture/build-from-source.md +++ b/website/src/content/docs/architecture/build-from-source.md @@ -3,7 +3,7 @@ title: Build from source description: Requirements and commands for building Vector yourself. --- -Requirements: Rust (stable), Node 20+, on macOS, Linux, or Windows (Windows support is in progress). +Requirements: Rust (stable), Node 20+, on macOS, Linux, or Windows. ```bash npm install diff --git a/website/src/content/docs/architecture/how-its-built.md b/website/src/content/docs/architecture/how-its-built.md index 72df780..583b6c6 100644 --- a/website/src/content/docs/architecture/how-its-built.md +++ b/website/src/content/docs/architecture/how-its-built.md @@ -3,7 +3,7 @@ title: How it's built description: A tour of Vector's architecture — Tauri v2, xterm.js, the PTY pipeline, and the cross-platform layer. --- -Vector is a [Tauri v2](https://v2.tauri.app/) desktop app. Each tab replaces the shell inside a terminal with an AI coding agent's CLI (Claude Code, Codex, etc.) — the tab owns a PTY running that agent, rendered into the page with [xterm.js](https://xtermjs.org/). It ships on macOS and Linux, with Windows support in progress. +Vector is a [Tauri v2](https://v2.tauri.app/) desktop app. Each tab replaces the shell inside a terminal with an AI coding agent's CLI (Claude Code, Codex, etc.) — the tab owns a PTY running that agent, rendered into the page with [xterm.js](https://xtermjs.org/). It ships on macOS, Linux, and Windows. ## Frontend diff --git a/website/src/content/docs/getting-started/install.md b/website/src/content/docs/getting-started/install.md index bf1558e..2f24dff 100644 --- a/website/src/content/docs/getting-started/install.md +++ b/website/src/content/docs/getting-started/install.md @@ -1,6 +1,6 @@ --- title: Install -description: Download and install Vector on macOS or Linux. +description: Download and install Vector on macOS, Linux, or Windows. --- ## macOS @@ -15,3 +15,11 @@ Vector runs on x86_64 and aarch64 via WebKitGTK. - **AppImage:** `chmod +x Vector__amd64.AppImage && ./Vector__amd64.AppImage`. If a lib is missing: `sudo apt install libwebkit2gtk-4.1-0 libgtk-3-0 libayatana-appindicator3-1`. Clipboard file-paste uses `wl-clipboard` (Wayland) or `xclip` (X11). + +## Windows + +Vector runs on Windows 10/11 (x64). + +Download the latest `.exe` installer from [Releases](https://github.com/avram19/vector/releases) and run it. Because the build is unsigned, SmartScreen shows a "Windows protected your PC" prompt the first time — click **More info → Run anyway**. Vector installs per-user and updates in-app. + +One current limitation: in a plain **shell** pane the directory shown doesn't follow `cd` live (it refreshes when you open the shell panel); agent panes track the directory normally. diff --git a/website/src/pages/index.astro b/website/src/pages/index.astro index ed59f33..6e7490b 100644 --- a/website/src/pages/index.astro +++ b/website/src/pages/index.astro @@ -14,7 +14,7 @@ const base = import.meta.env.BASE_URL.replace(/\/$/, ""); Vector — an agent-first terminal - +