Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ permissions:
contents: read

jobs:
# The Rust pty host (ADR 0004, phase 1): its own checks, then the parity suite against the
# Node host. Linux only until phase 3; the runner's preinstalled Rust toolchain.
# The Rust pty host (ADR 0004): its own checks, then the parity suite against the Node host,
# on Linux here and on macOS in the build job (phase 3); the runner's preinstalled Rust.
rust-host:
runs-on: ubuntu-22.04
steps:
Expand Down Expand Up @@ -91,6 +91,14 @@ jobs:
- name: E2E smoke test
if: runner.os == 'Linux'
run: xvfb-run -a bun run test:e2e
# The Rust host on macOS (ADR 0004 phase 3): its tests and the parity suite against the
# Node host, as the rust-host job runs them on Linux.
- name: Rust host on macOS
if: runner.os == 'macOS'
run: |
cargo test --locked --manifest-path host-rs/Cargo.toml
bun run test:host
bash scripts/build-mac-host.sh
- if: inputs.package
run: bun run ${{ matrix.dist }}
env:
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 2026-10-27

- [host] Rust host phase 3a (#76, ADR 0004): macOS builds carry it too: `bun run dist:mac` builds one universal (arm64 + x64) `wraithgrid-host` (`scripts/build-mac-host.sh`, lipo) bundled in both apps and ad-hoc signed with them; Settings → Advanced "Use the Rust host" shows on macOS; CI's macOS job runs `cargo test` and the host parity suite. Windows is phase 3b: portable-pty always quotes arguments, so the ready `cmd.exe /d /s /c "…"` line of an npm `.cmd` CLI needs a ConPTY spawn of our own (files: scripts/build-mac-host.sh, package.json, electron-builder.yml, .github/workflows/build.yml, src/main/app.ts, src/renderer/components/SettingsView.tsx, docs/CONTEXT.md)
- [chat] A ↑ recall or a `/` pick no longer moves the caret a frame later (cause of the intermittent `chat-history.spec.ts` CI failures, e.g. `second promptline one`): the frame could land between a select-all and the typing that replaces it, so the text was added instead; the caret is now placed in a layout effect right after the text is in the box (files: src/renderer/components/chat/Composer.tsx)
- [chat] A sideways swipe over the chat no longer pauses follow (the cause of the intermittent `chat.spec.ts:299` CI failure): Chromium sometimes scrolls the chat by the swipe's few px of vertical noise, and that upward move read as the reader going back; for 150 ms after a mostly sideways wheel the chat's own upward scroll is ignored (files: src/renderer/components/chat/ChatView.tsx)
- [git] Changes panel: commit message, push/pull confirm and History "Load more" belong to their pane (keyed by pane); a diff past git's 8 MB buffer shows its start marked cut, not the unstaged-only diff; too many changes to list still reads as a repo; paths with spaces in binary/mode-only entries are right (#140) (files: src/renderer/components/DiffPanel.tsx, src/renderer/lib/diff.ts, src/main/git.ts, src/main/app.ts)
Expand Down
4 changes: 2 additions & 2 deletions docs/CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ react-resizable-panels, @dnd-kit. Tests: Vitest (unit), Playwright (Electron e2e
- `bun run test:host` — build both pty hosts (electron-vite + `cargo build --release` in host-rs) and run the parity suite (`tests/host`, Linux/macOS); `cargo test`/`cargo clippy` in `host-rs/`
- `bun run test:e2e` — build, then Playwright (`tests/e2e`; needs a display, CI uses xvfb-run)
- `bun run build` — production build into `out/`
- `bun run dist:linux:portable` — Linux packages in Ubuntu 22.04 Docker; `bun run dist:win` on Windows; `bun run dist:mac` on a Mac
- `bun run dist:linux:portable` — Linux packages in Ubuntu 22.04 Docker; `bun run dist:win` on Windows; `bun run dist:mac` on a Mac (builds a universal Rust host first, `scripts/build-mac-host.sh`)
- Release: `bun pm version <patch|minor|major> -m "chore: release %s" && git push --follow-tags`

## Directory map
Expand All @@ -27,7 +27,7 @@ react-resizable-panels, @dnd-kit. Tests: Vitest (unit), Playwright (Electron e2e
- `src/main/host-start.ts`: start a detached host (fd masking) + socket connect; used by app and bridge `src/main/pane-router.ts`: routes pane ops to the local host or a machine's (by workspace `machineId`)
- `src/main/spawn-resolve.ts`: pane spec → exact process (binary, args, cwd, env); local in ipc, remote in the bridge `src/main/ssh.ts`: `~/.ssh/config` hosts, ssh args, ssh child as a duplex stream
- `src/main/ipc.ts`: all IPC handlers; validates every payload with zod contracts `src/main/pane-host.ts`: GUI side of the pty host: find/start it, hello + token, create/attach/write/kill, version mismatch
- `host-rs/`: the Rust pty host (ADR 0004; phase 2: Settings → Advanced "Use the Rust host", off by default, Linux; `settings.rustHost` or `WRAITHGRID_RUST_HOST=1` → app.ts `startHost` runs the binary from resources (packaged) or `host-rs/target/release` (dev) instead of host.js; Linux packages bundle it via `extraResources`): `main.rs` lock/token/socket/signals, `server.rs` clients+panes (same frames as `src/host/protocol.ts`), `pty.rs` portable-pty + waitpid, `screen.rs` vt100 snapshot/text, `protocol.rs` frame validation; phase 5: `bridge.rs` = `wraithgrid-host stdio <ver>`, the ssh end on a Linux machine (starts/joins a host under `~/.wraithgrid/remote`, injects the token, turns `plan` frames into `create`), `plan.rs` = login-shell PATH, binary, folder, env on that machine; GUI side `src/main/remote-plan.ts` (spec → plan: the rules) + `remote-bin.ts` (preflight `uname -sm`, upload once per version to `~/.wraithgrid/bin`, fallback `wraithgrid host-stdio`), static musl binaries from `scripts/build-remote-hosts.sh` → `host-rs/target/remote` → resources `remote/`; `tests/host/` = parity suite vs the Node host (`bun run test:host`)
- `host-rs/`: the Rust pty host (ADR 0004; phase 2/3a: Settings → Advanced "Use the Rust host", off by default, Linux and macOS (Windows: phase 3b); `settings.rustHost` or `WRAITHGRID_RUST_HOST=1` → app.ts `startHost` runs the binary from resources (packaged) or `host-rs/target/release` (dev) instead of host.js; Linux packages bundle it via `extraResources`): `main.rs` lock/token/socket/signals, `server.rs` clients+panes (same frames as `src/host/protocol.ts`), `pty.rs` portable-pty + waitpid, `screen.rs` vt100 snapshot/text, `protocol.rs` frame validation; phase 5: `bridge.rs` = `wraithgrid-host stdio <ver>`, the ssh end on a Linux machine (starts/joins a host under `~/.wraithgrid/remote`, injects the token, turns `plan` frames into `create`), `plan.rs` = login-shell PATH, binary, folder, env on that machine; GUI side `src/main/remote-plan.ts` (spec → plan: the rules) + `remote-bin.ts` (preflight `uname -sm`, upload once per version to `~/.wraithgrid/bin`, fallback `wraithgrid host-stdio`), static musl binaries from `scripts/build-remote-hosts.sh` → `host-rs/target/remote` → resources `remote/`; `tests/host/` = parity suite vs the Node host (`bun run test:host`)
- `src/host/`: background pty host (ADR 0001), built as `out/main/host.js`, never imports electron: `index.ts` entry (lock, token, socket), `server.ts` panes + clients, `protocol.ts` NDJSON frames (zod), `paths.ts` socket/token paths per user-data dir, `screen.ts` headless xterm + serialize, `fds.ts` inherited-fd masking, `pty-manager.ts` spawns/kills pane processes (per-OS KillPolicy) `src/main/chat/`: chat panes (ADR 0002): `sessions.ts` runs one headless CLI per pane (pipes, JSON lines), `adapter.ts` registry, `claude.ts` stream-json ↔ ChatEvent (+ approvals), `agy.ts` agy's stream-json (no approvals/interrupt: `controls` event → the view restarts it), `codex.ts` `codex app-server` JSON-RPC, `images.ts` pasted images → temp files, `spawn.ts` masked fds/process group, `attach.ts` files from the attach picker, `transcript.ts` copies a conversation to another account's dir (claude transcript, or codex rollout under `sessions/YYYY/MM/DD`, #60) (switch account, #29); add a CLI = one adapter + `chat: true` in agent-clis
- `src/main/login.ts`: sign-in dialog backend, per CLI (`LOGIN_FLOWS`): claude `auth login` + `auth status --json`; codex `login --device-auth` (one-time code) + `login status`; `DIALOG_LOGIN_CLIS` in agent-clis says who gets the dialog `src/main/clipboard-image.ts`: clipboard image → temp file → text to paste (path, or `@path` for gemini/agy)
- `src/main/pane-env.ts`: builds a pane's env (per-CLI config dir var for agent panes; mod folders as `CLAUDE_CODE_PLUGIN_DIRS` for claude; strips the launching terminal's vars; UTF-8 locale fallback) `src/main/config-store.ts`: config.json load/save, atomic write, bad-file quarantine
Expand Down
1 change: 1 addition & 0 deletions docs/DECISIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,3 +141,4 @@ Entries dated 2026-09-29 are reconstructed from the code and commit history duri
- 2026-10-26 | Rust pty host phase 1 (#76, ADR 0004 Proposed): `host-rs/` crate, same socket/frames, `vt100` for the reattach screen, parity suite against the Node host | No native rebuilds per Electron, ~5 MB idle vs ~110 MB, one binary for remote hosts; parity is tested, not described | Tauri move (webview risks, e2e rewrite); `alacritty_terminal` (no row formatting/scrollback capture; kept as fallback)
- 2026-10-26 | Rust host phase 5 (#76): the window sends a `plan` (rules as data: candidates, env drop/set, login-shell probe) and the remote Rust bridge only does the machine-specific lookups; the binary is copied over ssh per version and must run (`--version`) before it is used, else `wraithgrid host-stdio` | One set of rules in TS, no Wraithgrid install needed on Linux machines, and a binary that can't run there (untested arm64) falls back instead of breaking the machine | Porting spawn-resolve/pane-env to Rust (two copies of the rules); a separate remote user-data dir shared with an installed Wraithgrid (its host may be another version)
- 2026-10-26 | A chat's live view is capped at 1000 items, dropping to 800 (#141) | Bounds memory and per-render work of a pane left open for days; the saved view already keeps only 400, so nothing a restart would show is lost | No cap (memory grows until the pane closes); virtualized list (bigger change, breaks find-in-page and follow)
- 2026-10-27 | Rust host phase 3 split: macOS first (3a), Windows after (3b) with a ConPTY spawn of our own | portable-pty always quotes each argument on Windows, so the ready `cmd.exe /d /s /c "…"` line for npm `.cmd` CLIs (platform.ts) can't pass through it; macOS needs no code change. One universal mac binary (lipo) serves both the arm64 and x64 apps | Splitting the line into argv (cmd.exe doesn't read `\"` escapes); a per-arch mac binary (two extraResources configs for one value)
2 changes: 1 addition & 1 deletion docs/TODO.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# TODO

## Now
- #76 Rust host: phase 3 (macOS + Windows) and 4 (default on, drop node-pty) left; phase 5 arm64 binary never run on real arm64; old `~/.wraithgrid/bin/wraithgrid-host-*` versions on remote machines are never removed
- #76 Rust host: 3a macOS done (never run on a real Mac: only CI's parity suite); 3b Windows next (own ConPTY spawn with a raw command line, named pipe, taskkill tree, Ctrl-C), then 4 (default on, drop node-pty); phase 5 arm64 binary never run on real arm64; old `~/.wraithgrid/bin/wraithgrid-host-*` versions on remote machines are never removed
- CI flakes fixed 2026-10-27 (PR #152): chat.spec.ts:299 (sideways-wheel noise paused follow), chat-history.spec.ts (caret placed a frame late collapsed a select-all). Still seen once locally: responsive.spec.ts:131 (Settings nav heading not hidden in a small window), passed 3/3 on rerun
- Audit (2026-10-26) bugs: all fixed (#135–#138 PR #148, #139 PR #150, #140 PR #151). Not seen live: #138 cancelled pkexec prompt, #140 renderer-crash reattach, codex cancel/stop paths, agy first-start effort
- Audit (2026-10-26) perf: #141 done (PR #149); not measured live yet (a DevTools profile of a long streaming chat before/after would confirm)
Expand Down
5 changes: 5 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ mac:
artifactName: ${productName}-${version}-mac-${arch}.${ext}
extendInfo:
LSMinimumSystemVersion: '12.0'
# The Rust background host (ADR 0004 phase 3), off unless Settings → Advanced turns it on:
# one universal binary for both apps (`bun run dist:mac` runs scripts/build-mac-host.sh).
extraResources:
- from: host-rs/target/release/wraithgrid-host
to: wraithgrid-host
dmg:
artifactName: ${productName}-${version}-mac-${arch}.${ext}
# In-app updates (electron-updater) read latest.yml / latest-linux.yml from this repo's
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"dist:linux": "electron-vite build && cargo build --release --manifest-path host-rs/Cargo.toml && bash scripts/build-remote-hosts.sh && electron-builder --linux --publish never",
"dist:linux:portable": "bash scripts/dist-linux-docker.sh",
"dist:win": "electron-vite build && electron-builder --win --publish never",
"dist:mac": "electron-vite build && electron-builder --mac --publish never",
"dist:mac": "electron-vite build && bash scripts/build-mac-host.sh && electron-builder --mac --publish never",
"postinstall": "electron-builder install-app-deps"
},
"devDependencies": {
Expand Down
15 changes: 15 additions & 0 deletions scripts/build-mac-host.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# The Rust host for the macOS app (ADR 0004 phase 3): one universal binary (Apple Silicon and
# Intel) at host-rs/target/release/wraithgrid-host, where electron-builder takes it from for
# both the arm64 and the x64 app. Run on a Mac.
set -euo pipefail
cd "$(dirname "$0")/../host-rs"
targets=(aarch64-apple-darwin x86_64-apple-darwin)
for t in "${targets[@]}"; do
if command -v rustup >/dev/null; then rustup target add "$t" >/dev/null 2>&1 || true; fi
cargo build --release --locked --target "$t"
done
mkdir -p target/release
lipo -create -output target/release/wraithgrid-host \
"target/${targets[0]}/release/wraithgrid-host" "target/${targets[1]}/release/wraithgrid-host"
lipo -info target/release/wraithgrid-host
3 changes: 2 additions & 1 deletion src/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ const hostFiles = hostPaths({
* when packaged, the cargo release build next to `out/` in development; null when there is none.
*/
const rustHostBin = ((): string | null => {
if (process.platform !== 'linux') return null
// Linux and macOS (ADR 0004 phases 2, 3); Windows waits for its own ConPTY spawn.
if (process.platform !== 'linux' && process.platform !== 'darwin') return null
const bin = app.isPackaged
? path.join(process.resourcesPath, 'wraithgrid-host')
: path.join(__dirname, '../../host-rs/target/release/wraithgrid-host')
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -631,13 +631,13 @@ function CliRow() {
)
}

/** The Rust background host (ADR 0004 phase 2): off by default, Linux builds that have it. */
/** The Rust background host (ADR 0004): off by default, Linux and macOS builds that have it. */
function RustHostRow() {
const actions = useActions()
const on = useApp((s) => s.config.settings.rustHost)
const available = useApp((s) => s.info.rustHost)
const linux = useApp((s) => s.info.platform === 'linux')
if (!linux) return null
const unix = useApp((s) => s.info.platform === 'linux' || s.info.platform === 'darwin')
if (!unix) return null
return (
<div className="srow">
<div>
Expand Down
Loading