Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d466953
fix: suppress console window flashes on Windows subprocess spawns
XiaoLrrr Oct 15, 2026
4e2d1b5
feat: add Windows desktop support (window config + close behavior)
XiaoLrrr Oct 15, 2026
93dc4ae
fix(windows): resolve home dir via HARNESSKIT_HOME + cross-platform p…
XiaoLrrr Oct 19, 2026
79a7b4c
fix(windows): route remaining git spawns and data dir through hk_core…
XiaoLrrr Oct 19, 2026
e3ad10e
chore: drop unused @tauri-apps/cli devDependency
RealZST Oct 24, 2026
c18ba31
refactor(process): hide console windows only when the desktop opts in
RealZST Oct 24, 2026
1f249be
test(kits): restore HARNESSKIT_HOME after each test and use it end to…
RealZST Oct 24, 2026
fa5bec6
test(web): inject the allowlist home into WebState instead of an env var
RealZST Oct 24, 2026
fa2dc41
style: drop no-op Option wrappers and stray blank lines
RealZST Oct 24, 2026
a4b4df7
ci: run the PR checks on Windows too
RealZST Oct 24, 2026
513810e
docs: note Windows desktop development and the HARNESSKIT_HOME test o…
RealZST Oct 24, 2026
fcbb2d6
test: serialize the remaining tests that read the home directory twice
RealZST Oct 24, 2026
3caf8ac
ci(temp): build an unsigned Windows NSIS installer for manual testing
RealZST Oct 24, 2026
3675c80
fix(ui): paint the canvas and stop root overscroll on the Windows des…
RealZST Oct 24, 2026
bf46132
fix(ui): show the app icon picker only on the macOS desktop
RealZST Oct 24, 2026
6ce2f81
refactor(ui): replace isMacDesktop/hasWindowVibrancy with isDesktop()…
RealZST Oct 24, 2026
1bb0dff
feat(desktop): give the Windows title bar the sidebar's colors
RealZST Oct 24, 2026
9516e3a
style: biome formatting for the title bar change
RealZST Oct 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,35 @@ jobs:

- name: Rust tests
run: cargo test --workspace

test-windows:
# HarnessKit ships a Windows CLI, and the desktop app now builds on Windows,
# so run the same checks there: path separators, process spawning and home
# resolution all differ, and macOS-only CI cannot catch regressions in them.
# Pinned to windows-2025 like release.yml; never windows-latest.
runs-on: windows-2025
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2

- name: Install frontend dependencies
run: npm ci

- name: Frontend tests
run: npm test

- name: Build frontend
run: npm run build

- name: Rust tests
run: cargo test --workspace
45 changes: 45 additions & 0 deletions .github/workflows/windows-desktop-preview.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Temporary: builds an unsigned Windows NSIS installer of the desktop app for
# manual testing of PR #158. Not meant to be merged; the release pipeline for
# Windows desktop builds will be a separate change.
name: Windows Desktop Preview

on:
push:
branches: [wip/pr-158-windows-preview]
workflow_dispatch:

jobs:
build:
runs-on: windows-2025
steps:
- uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22

- name: Setup Rust
uses: dtolnay/rust-toolchain@stable

- name: Rust cache
uses: swatinem/rust-cache@v2

- name: Install Tauri CLI
run: cargo install tauri-cli --locked

- name: Install frontend dependencies
run: npm ci

# NSIS only (no WiX/MSI), no updater artifacts so no signing key is
# needed. Tauri runs `npm run build` as the beforeBuildCommand.
- name: Build NSIS installer
shell: bash
run: cargo tauri build --ci --config '{"bundle":{"targets":["nsis"],"createUpdaterArtifacts":false}}'

- name: Upload installer
uses: actions/upload-artifact@v4
with:
name: HarnessKit-windows-x64-setup
path: target/release/bundle/nsis/*.exe
if-no-files-found: error
6 changes: 4 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ cargo run -p hk-cli -- serve # http://127.0.0.1:7070

Open `http://localhost:1420` in your browser. Vite proxies `/api/*` requests to the backend at `:7070`.

### Desktop App Development (macOS only)
### Desktop App Development (macOS / Windows)

```bash
cargo tauri dev
```

Tauri automatically runs `npm run dev` as a before-dev command and launches the native window.
Tauri automatically runs `npm run dev` as a before-dev command and launches the native window. On Windows, the Visual Studio Build Tools ("Desktop development with C++") that rustup asks for are the only extra prerequisite. `cargo tauri build` packages NSIS and MSI installers under `target/release/bundle/`; because `createUpdaterArtifacts` is on, it also needs `TAURI_SIGNING_PRIVATE_KEY` in the environment, or pass `--config '{"bundle":{"createUpdaterArtifacts":false}}'` for a local build without updater artifacts.

## Building Releases

Expand Down Expand Up @@ -89,6 +89,8 @@ npm test # frontend tests (vitest)
cargo test --workspace # Rust tests
```

Tests that need an isolated home directory set `HARNESSKIT_HOME`, which `hk_core::home_dir()` honors ahead of the real home. It exists for test isolation (on Windows the home directory cannot be redirected through `HOME`); it is not a supported way to relocate HarnessKit's data.

## Troubleshooting

### `Found version mismatched Tauri packages`
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/hk-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ fn main() -> Result<()> {
}

fn hk_data_dir() -> PathBuf {
dirs::home_dir().unwrap_or_default().join(".harnesskit")
hk_core::home_dir().join(".harnesskit")
}

/// Resolve the web access token for `hk serve`.
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/antigravity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl Default for AntigravityAdapter {
impl AntigravityAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/claude.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ impl Default for ClaudeAdapter {
impl ClaudeAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ impl Default for CodexAdapter {
impl CodexAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
4 changes: 2 additions & 2 deletions crates/hk-core/src/adapter/copilot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl Default for CopilotAdapter {
impl CopilotAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down Expand Up @@ -520,7 +520,7 @@ mod tests {
&installed,
format!(
r#"{{"version":1,"installed":[{{"pluginUri":"file://{}","marketplace":"user/my-repo"}}]}}"#,
plugin_dir.to_string_lossy()
plugin_dir.to_string_lossy().replace('\\', "/")
),
).unwrap();

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Default for CursorAdapter {
impl CursorAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/dsh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ fn resolve_homes(

impl DshAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
let (dsh_home, agents_home) = resolve_homes(
std::env::var_os("DSH_HOME"),
std::env::var_os("DSH_AGENTS_HOME"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/gemini.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ impl Default for GeminiAdapter {
impl GeminiAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
4 changes: 2 additions & 2 deletions crates/hk-core/src/adapter/grok.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ impl Default for GrokAdapter {

impl GrokAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
grok_home: resolve_grok_home(std::env::var_os("GROK_HOME"), Some(&home))
.unwrap_or_else(|| home.join(".grok")),
Expand All @@ -481,7 +481,7 @@ impl GrokAdapter {
/// moves neither — so don't scan skills or evaluate plugin enablement
/// through an adapter built this way in tests.
pub fn with_grok_home(grok_home: PathBuf) -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
grok_home,
agents_home: home.join(".agents"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/hermes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ impl Default for HermesAdapter {
impl HermesAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/kiro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ impl Default for KiroAdapter {
impl KiroAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/omp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ impl Default for OmpAdapter {
impl OmpAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
8 changes: 6 additions & 2 deletions crates/hk-core/src/adapter/openclaw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ fn user_path(home: &Path, raw: &str) -> Option<PathBuf> {
None => PathBuf::from(raw),
},
};
path.is_absolute().then_some(path)
// `is_absolute()` is false on Windows for POSIX-style absolute paths
// like `/srv/claw` (the gateway's own docs and default examples use
// that shape); treat any path with a root — including a drive-relative
// `\srv` — as resolved, so only truly relative values stay unset.
(path.is_absolute() || path.has_root()).then_some(path)
}

/// Agent ids are normalised before they name a directory
Expand All @@ -94,7 +98,7 @@ impl OpenClawAdapter {
pub fn new() -> Self {
let var = |name| std::env::var_os(name).filter(|v| !v.is_empty());
Self::from_env(
dirs::home_dir().unwrap_or_default(),
crate::home_dir(),
var("OPENCLAW_STATE_DIR"),
var("OPENCLAW_CONFIG_PATH"),
var("OPENCLAW_WORKSPACE_DIR"),
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/opencode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl Default for OpencodeAdapter {
impl OpencodeAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/penguin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ fn subdirs(dir: &Path) -> impl Iterator<Item = PathBuf> + use<> {

impl PenguinAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
Self {
penguin_home: resolve_home(std::env::var_os("PENGUIN_HOME"), &home),
}
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/pi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ impl Default for PiAdapter {

impl PiAdapter {
pub fn new() -> Self {
let home = dirs::home_dir().unwrap_or_default();
let home = crate::home_dir();
let agent_dir = std::env::var_os("PI_CODING_AGENT_DIR")
.map(PathBuf::from)
.unwrap_or_else(|| home.join(".pi").join("agent"));
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/qoder_cn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Default for QoderCnAdapter {
impl QoderCnAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}
#[cfg(test)]
Expand Down
2 changes: 1 addition & 1 deletion crates/hk-core/src/adapter/windsurf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ impl Default for WindsurfAdapter {
impl WindsurfAdapter {
pub fn new() -> Self {
Self {
home: dirs::home_dir().unwrap_or_default(),
home: crate::home_dir(),
}
}

Expand Down
6 changes: 2 additions & 4 deletions crates/hk-core/src/auditor/rules/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,8 @@ impl AuditRule for CliCredentialStorage {
};

if let Some(cred_path) = &meta.credentials_path {
let expanded = if cred_path.starts_with("~/") {
dirs::home_dir()
.map(|h| h.join(&cred_path[2..]).to_string_lossy().to_string())
.unwrap_or_else(|| cred_path.clone())
let expanded = if let Some(rest) = cred_path.strip_prefix("~/") {
crate::home_dir().join(rest).to_string_lossy().to_string()
} else {
cred_path.clone()
};
Expand Down
7 changes: 4 additions & 3 deletions crates/hk-core/src/kits/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ use crate::HkError;
use std::path::PathBuf;

/// Canonical directory holding all Kit zip files: `~/.harnesskit/kits/`.
///
/// The home root is `crate::home_dir()`, which tests redirect through
/// `HARNESSKIT_HOME` (the only override that works on Windows).
pub fn kits_dir() -> Result<PathBuf, HkError> {
let home = dirs::home_dir()
.ok_or_else(|| HkError::Internal("home directory not found".into()))?;
Ok(home.join(".harnesskit").join("kits"))
Ok(crate::home_dir().join(".harnesskit").join("kits"))
}

/// Ensure `~/.harnesskit/kits/` exists; idempotent.
Expand Down
6 changes: 3 additions & 3 deletions crates/hk-core/src/kits/tests/end_to_end.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::adapter::all_adapters;
use crate::kits::service::{create_kit, export_kit, import_kit, sync_kit_to_project, unsync_kit_from_project};
use crate::kits::tests::service::RestoreHome;
use crate::kits::tests::service::{set_test_home, RestoreHome};
use crate::kits::types::{
CreateKitRequest, KitConfigFileRef, SyncKitRequest, UnsyncKitRequest,
};
Expand All @@ -20,7 +20,7 @@ fn open(dir: &std::path::Path) -> Mutex<Store> {
fn full_lifecycle_create_install_uninstall() {
let dir = tempdir().unwrap();
let _restore = RestoreHome::new();
unsafe { std::env::set_var("HOME", dir.path()); }
set_test_home(dir.path());
let store = open(dir.path());
let adapters = all_adapters();
let project = dir.path().join("proj");
Expand Down Expand Up @@ -74,7 +74,7 @@ fn full_lifecycle_create_install_uninstall() {
fn export_import_round_trip_preserves_manifest_bytes() {
let dir = tempdir().unwrap();
let _restore = RestoreHome::new();
unsafe { std::env::set_var("HOME", dir.path()); }
set_test_home(dir.path());
let store = open(dir.path());
let adapters = all_adapters();
let src = dir.path().join("CLAUDE.md");
Expand Down
6 changes: 5 additions & 1 deletion crates/hk-core/src/kits/tests/paths.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
use crate::kits::paths::{kits_dir, zip_path_for};
use serial_test::serial;

#[test]
fn zip_path_for_uses_hk_kit_zip_suffix() {
let p = zip_path_for("abc-123").unwrap();
assert!(p.to_string_lossy().ends_with("abc-123.hk-kit.zip"));
}

/// Reads the home twice (`kits_dir` and the assertion); the kit tests
/// rewrite it, so take the same lock.
#[test]
#[serial(kit_env)]
fn kits_dir_is_under_home() {
let dir = kits_dir().unwrap();
let home = dirs::home_dir().unwrap();
let home = crate::home_dir();
assert!(dir.starts_with(home));
assert!(dir.ends_with(".harnesskit/kits") || dir.ends_with(r".harnesskit\kits"));
}
Loading
Loading