Skip to content

feat(windows): run Kriyon on Windows - #48

Open
THPubs wants to merge 18 commits into
devfrom
feat/windows
Open

THPubs wants to merge 18 commits into
devfrom
feat/windows

Conversation

@THPubs

@THPubs THPubs commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Summary

Kriyon now builds, installs and runs on Windows 10/11. It was tested by hand on a real Windows 11 machine: the NSIS installer, the first-run screen, sessions (start, resume, close, crash recovery), live status and usage, the tray, notifications, the Files view (browse, edit, save, AI changes), the activity log, the structured view, the command palette and the app tour. Reinstalling over a running session also worked, and the session survived.

The design is recorded in docs/adr/0055-windows-support.md. In short:

  • Same daemon and protocol. The session socket stays a Unix-domain socket; Windows gets it from uds_windows behind kriyon_core::uds.
  • A Job Object is the Windows tree stop. The shell runs in a kill-on-close job, so a stop, a crash or the app's reaper ends the whole tree. The job also gives the busy signal.
  • ConPTY's opening cursor query is answered by the daemon, because nothing is attached when the shell starts.
  • PowerShell is the session shell. Claude launch lines are written in PowerShell syntax.
  • Daemons run from a per-build copy in the run dir, so an update can replace kriyon-ptyd.exe while sessions are open.
  • The process table comes from a Toolhelp snapshot plus each process's command line, shaped like ps rows.
  • Paths: canonicalize goes through dunce (clippy now bans the std one), repo-relative paths use /, and the frontend reads C:\ paths.
  • No menu bar on Windows. The tray icon is in colour, shortcut labels say Ctrl, and the copy says "this PC" and "File Explorer" there.
  • Helper programs (git, installs, setup commands) start without console windows.

Fixes that also matter on macOS:

  • Companion files are only written when they change, so an app launch no longer reloads the plugin ("plugin.json changed") in every live session.
  • A file save closes the target before the atomic rename.

Known gaps

  • Status hooks need Python on Windows. Kriyon uses a real python.exe when there is one. With none, it leaves the hook entries out instead of failing on every event; the companion still loads. Rewriting the hook scripts as a small Rust program would remove the dependency.
  • Releases are Mac-only. There's no Windows signing or update feed yet, and a few repo tooling tests (the release script, a Claude hook filter) fail when run on Windows.

Test plan

  • pnpm check on macOS (run by the commit and push hooks)
  • On Windows: clippy clean; 702 core, 13 daemon, 398 app and 2,607 frontend tests pass
  • Hand-tested on Windows 11 with the installed release build

Checklist

  • The PR targets dev.
  • I wrote or updated tests, and pnpm check passes.
  • I read and tested every line, and I can explain it in review.
  • AI tools wrote most of this change. (Tick if true. See Using AI tools.)

🤖 Generated with Claude Code

THPubs and others added 16 commits November 1, 2026 13:31
Port the session daemon, the app's process and socket handling, and the
frontend's paths and shortcut labels so Kriyon builds, runs and hosts Claude
Code on Windows 10/11.

- The session socket is a Unix-domain socket on both platforms; Windows gets
  it from uds_windows behind kriyon_core::uds.
- The daemon puts its shell in a Job Object that kills on close: the Windows
  tree stop, and the busy signal (more than the shell in the job). It answers
  ConPTY's opening cursor query itself, since no terminal is attached yet.
- The app spawns the daemon detached with no console, claims its run dir with
  an unshared lock file, reads the process table from a Toolhelp snapshot plus
  each process's own command line, and ends daemons with TerminateProcess.
- Hooks run a real python.exe on Windows (never the Store stub), with paths
  Git Bash can read. The shell is PowerShell; no shell PATH probe is needed.
- No menu bar off macOS; the window fits a short screen; tray accelerators
  use CmdOrCtrl.
- Frontend: Windows-aware path helpers and Ctrl-spelled shortcut labels.
- Test binaries on Windows embed the Common Controls manifest so they start.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- Resolve paths with dunce (no \\?\ verbatim paths), so file watching and
  path comparisons match what Windows reports; clippy now bans the std
  canonicalize.
- Close the target before the atomic rename, so saving a file works.
- Write relative paths with `/` (git pathspecs, @-mention completion,
  nested command names).
- Recognise Windows transcript paths for the main-session lock, and the
  Windows temp folder for handoffs and one-shot detection.
- Worktree installs run through cmd and setup commands through PowerShell,
  with no console window; every git spawn hides its console too.
- A closed client connection no longer hangs on Windows when the daemon
  never accepted it.
- Recycle Bin delete, editor launch, memory and CPU readings, and the
  orphan-daemon reaper on Windows.
- Claude launch commands in PowerShell syntax on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Tests that drive a POSIX shell, read a terminal device or use POSIX path
fixtures run on Unix only, each next to a Windows counterpart where the
behaviour differs. Hook-script tests use a real Python and point
USERPROFILE at the test home, which is what Windows Python reads for `~`.
Shortcut labels in tests come from the platform helper.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- The tray uses the colour app icon on Windows (a template silhouette
  vanishes on a dark taskbar) and drops the menu-bar-only Ctrl+N hint.
- Touched-file paths and git's toplevel share one spelling for Windows
  paths, so Claude's edits are credited in AI changes.
- Copy names the computer and file manager per platform: this Mac or this
  PC, Finder or File Explorer.
- The usage-snapshot test spaces its rewrites past one file-time tick.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…design

- Windows daemons run from a per-build copy in the run dir, so an update
  can replace the installed kriyon-ptyd.exe while sessions are open.
- With no real Python on Windows the hook entries are left out (the
  companion still loads) instead of failing on every event.
- Companion files are written only when their contents change, so a launch
  no longer reloads the plugin in every live session.
- ADR 0054 records how Kriyon runs on Windows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Off macOS the tour spells its shortcuts with Ctrl, leaves out ⌘N (a menu
bar shortcut there is no menu bar for), and no longer points at a Help menu
Windows doesn't have; Settings' tour hint does the same.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A delete on a drive without a Recycle Bin used to be silently permanent:
FOF_NOCONFIRMATION answered Windows' own warning. FOF_WANTNUKEWARNING
brings that warning back, and saying no fails the delete.

New and renamed entries now refuse names Windows reads as something else:
a trailing dot or space (.git. would become .git), a colon (a file
stream), the characters Windows forbids, and device names such as NUL.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Force-stopping a stuck daemon no longer walks its children by parent pid.
Windows never updates a parent pid when the parent exits, and pids are
reused, so that walk could end unrelated processes; the daemon's job
already ends its tree. Every Windows terminate now goes through one gated
helper beside signal_gated.

Busy on Windows is one rule, windows_command_of in kriyon-core: the shell
has a live child. The app's sweep and the daemon both use it; the daemon
reads it from its job's members, so a process a finished command left
behind no longer holds a session busy forever.

npm-installed Claude Code (claude.cmd, or node on @anthropic-ai/claude-code)
now reads as claude, so auto-sleep and the attention checks see it. The
test fixture's profile path is a placeholder.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The Windows file identity was the creation time, which NTFS hands on to a
file made at a just-deleted path within about 15 seconds. A stopping daemon
could then delete a newer daemon's bridge socket, and a replaced spool could
keep the old cursor. The identity is now the volume serial number and file
index, the Windows equivalent of device and inode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
create_session now reports the session shell's language (ShellLanguage in
kriyon-core), so the frontend no longer guesses PowerShell from the user
agent. The PowerShell launch line reads the seed prompt as UTF-8, and a
nested session's env is removed again when its claude ends, as a POSIX
NAME=value prefix would be.

Hook and status-line commands quote their paths with sh_quote, so a profile
folder with an apostrophe works. With no Python to run the hooks, each
session now says its status is unavailable because the hooks need Python,
instead of looking applied while nothing ever fires.

Also moves two doc comments back onto their functions.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The frontend now has one Windows-path rule, isWindowsPath, which also knows
network shares. canonicalDir uses it, so a share root matches git's
//server/share spelling. The Rust outside-project check reads shares too,
works out the home folder from a Windows cwd, recognises Windows and Git
Bash paths in commands, and the sandbox compares a long verbatim path in
its ordinary form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bootstrap, the editor launcher and the Python lookup each had their own way
to find a program on the PATH. spawn::on_path is now the one, honouring
PATHEXT. The setup command uses shellinit::WINDOWS_SHELL, and the
CREATE_NO_WINDOW flag is defined once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The tray menu no longer shows Ctrl+N and Ctrl+Q off macOS, where nothing
binds them. The tray popover uses platformKeys for its hint, and copy that
names the Help menu is marked {mac:...} where it is written rather than
matched by a regex over the wording.

Copy tests now pin the platform and assert literal text, and os.ts has its
own tests.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Brings in the worktree terminals (#47) and the 0.15.0 release (#49). The
one conflict was daemon_rt.rs's imports: it keeps kriyon_core::uds's
UnixStream, which Windows needs, and dev's HashMap.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@THPubs

THPubs commented Nov 1, 2026

Copy link
Copy Markdown
Member Author

Review and fix — pass 1

Full review of the pull request at 7530809, then fixes and a re-check of them, through 0335b4b.

Fixed 25 issues in 2 passes. All checks green on macOS and on a Windows 11 machine.

  • Pushed as 8 fix commits, plus a merge of dev (one import conflict).
  • The template checklist was added to the description.

Notes

  • A macOS CI run failed once on a timing test in watcher.rs that this pull request does not touch; it passed on rerun.

Fixed this pass

  • src-tauri/src/entry_cmd.rs:248 — Stop silent permanent delete when Recycle Bin is unavailable
  • src-tauri/src/daemon_rt.rs:208 — Don't TerminateProcess Toolhelp descendants on Windows
  • src/terminal/launchCommand.ts:47 — Read the seed prompt as UTF-8 in Windows PowerShell
  • crates/kriyon-core/src/proc.rs:1056 — Replace the real Windows profile path with a placeholder
  • crates/kriyon-core/src/private_fs.rs:31 — Use a real file id, not creation time, as Windows identity
  • src/terminal/launchCommand.ts:41 — Scope nested-session env to the one claude launch
  • src-tauri/src/integration.rs:131 — Tell the user when status hooks are left out for lack of Python
  • crates/kriyon-ptyd/src/pty.rs:288 — One Windows busy rule; don't count orphans in the job
  • src-tauri/src/hooks.rs:76 — Quote hook paths safely for apostrophes in Windows profiles
  • src-tauri/src/sandbox.rs:122 — Handle verbatim paths dunce keeps for long Windows paths
  • src-tauri/src/sandbox.rs:449 — Refuse Windows-only unsafe leaf names
  • crates/kriyon-core/src/proc.rs:508 — Recognise npm-installed claude on Windows
  • src-tauri/src/tray.rs:160 — Don't show Ctrl+N / Ctrl+Q in the Windows tray menu
  • crates/kriyon-core/src/activity_log.rs:214 — Teach reaches_outside Windows paths too
  • src/terminal/sessionHost.ts:422 — Take the launch-line shell from Rust, not userAgent
  • src-tauri/src/bootstrap_cmd.rs:127 — Use shellinit::WINDOWS_SHELL for setup commands
  • src/state/paths.ts:27 — One Windows-path rule for the frontend
  • src-tauri/src/sleep_rt.rs:76 — Add a gated Windows terminate beside signal_gated
  • src-tauri/src/links_cmd.rs:202 — One find-on-PATH helper for Windows
  • src-tauri/src/links_cmd.rs:197 — Use spawn::no_console instead of redefining the flag
  • src/terminal/launchCommand.test.ts:49 — Assert the PowerShell native-arg escaping
  • src/platform/os.ts:24 — Test the platform detection the launch line depends on
  • src/platform/shortcut.ts:17 — Don't strip the Help mention by matching exact copy
  • src-tauri/src/handoff_rt.rs:139 — Move note_hook_event's doc back above it
  • src-tauri/src/commands.rs:29 — Move ptyd_binary's doc back above the function

Checked by 5 finders (processes, correctness, boundary, contract, tests, frontend, public text, docs, left behind, step 5 re-check of the fixes) · architecture review · scorer: 55 scored, 25 kept.


Ready to merge, as far as this review can see. Nothing is open from this pass or any pass before it.

The dialog was centred with align-items inside a scrolling page. When it is
taller than the window, a flex-centred child overflows upward, past where the
page can scroll, so its top was cut off. This showed on Windows at the default
820px window height and applies to macOS too. The panel now centres itself
with auto margins, which only centre it while it fits.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The PowerShell launch line escaped the seed prompt's double quotes for
Windows PowerShell 5.1, which passes a native argument's quotes raw. pwsh
7.3 and later, which a Mac or Linux user can have as $SHELL, escapes them
itself ($PSNativeCommandArgumentPassing is Standard), so claude received a
literal backslash before every quote. The line now escapes only when that
variable is unset or Legacy.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant