diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index f75565c..a7a444e 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -53,11 +53,16 @@ jobs: -p vt -p input -p workspace -p theme -p config -p macros -p mcp -p cast -p plugin + # The terminal application and its backend: pty (ConPTY), terminal, notes, + # and app itself all build on Windows. + - name: Build terminal app + if: always() + run: cargo build -p pty -p terminal -p notes -p app + # Informational only: compile the whole workspace so the log captures the - # full remaining Windows surface (pty/terminal are #![cfg(unix)] and app - # depends on them unconditionally). `--keep-going` enumerates every - # failing crate in one run instead of stopping at the first. Expected to - # fail today; never gates. + # remaining Windows surface. `relay` (the standalone sidecar) is still + # Unix-only; `--keep-going` enumerates every failing crate in one run. + # Expected to fail on relay today; never gates. - name: Full workspace surface (informational) if: always() continue-on-error: true diff --git a/crates/app/src/ipc/mod.rs b/crates/app/src/ipc/mod.rs new file mode 100644 index 0000000..fa5037f --- /dev/null +++ b/crates/app/src/ipc/mod.rs @@ -0,0 +1,16 @@ +//! Single-instance IPC: `prompt --toggle-quick` summons the quick terminal and +//! `prompt mcp` bridges Model Context Protocol tool calls into the running GUI. +//! +//! Unix carries this over a per-user domain socket (`unix`). Windows has no +//! named-pipe transport yet, so it falls back to graceful stubs (`windows`) +//! that keep the CLI surface intact while the feature is unavailable. + +#[cfg(unix)] +mod unix; +#[cfg(unix)] +pub use unix::*; + +#[cfg(windows)] +mod windows; +#[cfg(windows)] +pub use windows::*; diff --git a/crates/app/src/ipc.rs b/crates/app/src/ipc/unix.rs similarity index 100% rename from crates/app/src/ipc.rs rename to crates/app/src/ipc/unix.rs diff --git a/crates/app/src/ipc/windows.rs b/crates/app/src/ipc/windows.rs new file mode 100644 index 0000000..564a240 --- /dev/null +++ b/crates/app/src/ipc/windows.rs @@ -0,0 +1,37 @@ +//! Windows IPC stubs. +//! +//! The single-instance control channel — quick-terminal summon and the MCP +//! bridge — is a Unix-domain socket. A Windows named-pipe transport is not yet +//! implemented, so these degrade gracefully: the client calls report the +//! feature is unavailable and the server never binds. + +use gpui::App; +use serde_json::Value; + +/// The value injected as `PROMPT_SOCKET` into spawned sessions. Empty on +/// Windows: there is no control channel for external tooling to reach yet. +pub fn socket_env() -> String { + String::new() +} + +/// Client: ask a running instance to toggle the quick terminal. +pub fn send_toggle() -> bool { + eprintln!("prompt: quick-terminal summon is not supported on Windows yet"); + false +} + +/// Client: send one op to the running instance. Always fails on Windows — there +/// is no transport to reach it. +pub fn request(_op: &str, _args: &Value) -> Result { + Err("prompt: IPC is not supported on Windows yet".to_string()) +} + +/// Server: own the control channel. A no-op on Windows. +pub fn listen(_cx: &mut App) {} + +/// Dev-only CLI (`prompt ipc `): unavailable on Windows. +#[cfg(debug_assertions)] +pub fn run_cli(_args: &[String]) -> i32 { + eprintln!("prompt ipc: not supported on Windows"); + 2 +} diff --git a/crates/app/src/main.rs b/crates/app/src/main.rs index a7e9156..a80e5a6 100644 --- a/crates/app/src/main.rs +++ b/crates/app/src/main.rs @@ -235,6 +235,12 @@ pub(crate) fn open_window( { options.window_decorations = Some(gpui::WindowDecorations::Client); } + // Windows draws its own caption and frame (native decorations); our tab bar + // sits in the client area below it. + #[cfg(target_os = "windows")] + { + options.window_decorations = Some(gpui::WindowDecorations::Server); + } let handle = cx .open_window( options, diff --git a/crates/app/src/pluginhost.rs b/crates/app/src/pluginhost.rs index 72d870f..7d2b495 100644 --- a/crates/app/src/pluginhost.rs +++ b/crates/app/src/pluginhost.rs @@ -111,6 +111,23 @@ pub enum Block { Unknown, } +/// Force-terminate the process `pid` (a plugin runtime that blew its timeout). +#[cfg(unix)] +fn force_kill(pid: u32) { + unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) }; +} + +/// Force-terminate the process `pid` via `taskkill /F /T` (also reaps its +/// child tree). Dependency-free; `taskkill` ships with Windows. +#[cfg(windows)] +fn force_kill(pid: u32) { + let _ = Command::new("taskkill") + .args(["/PID", &pid.to_string(), "/F", "/T"]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); +} + /// Invoke `plugin`'s runtime with `req`, returning the parsed response. The /// process runs in the plugin's own directory (so relative entrypoints like /// `plugin.ts` resolve); the target directory travels in `req.cwd`. @@ -167,7 +184,7 @@ pub fn invoke(plugin: &plugin::Plugin, req: &Request) -> Result Result<(u16, String), St let mut parts = command.split_whitespace(); let program = resolve_program(parts.next().ok_or("empty service command")?); let args: Vec<&str> = parts.collect(); - use std::os::unix::process::CommandExt; - std::process::Command::new(&program) - .args(&args) + let mut cmd = std::process::Command::new(&program); + cmd.args(&args) .current_dir(dir) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .process_group(0) // outlive the app that spawned it - .spawn() + .stderr(std::process::Stdio::null()); + // Detach so the service outlives the app that spawned it. + #[cfg(unix)] + { + use std::os::unix::process::CommandExt; + cmd.process_group(0); + } + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + // CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS: a new group with no + // inherited console, so it survives the parent exiting. + const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200; + const DETACHED_PROCESS: u32 = 0x0000_0008; + cmd.creation_flags(CREATE_NEW_PROCESS_GROUP | DETACHED_PROCESS); + } + cmd.spawn() .map_err(|e| format!("spawn `{program}`: {e}"))?; for _ in 0..60 { std::thread::sleep(std::time::Duration::from_millis(50)); diff --git a/crates/app/src/root/render.rs b/crates/app/src/root/render.rs index 5b7b2d4..71380ca 100644 --- a/crates/app/src/root/render.rs +++ b/crates/app/src/root/render.rs @@ -60,8 +60,9 @@ impl Render for WorkspaceView { // No separate titlebar: the pane group's top-row tab bar *is* the // titlebar (it reserves the traffic-light inset and drags the window). - // Platforms without native controls overlay their own at the top-right. - #[cfg(not(target_os = "macos"))] + // macOS uses native traffic lights and Windows native caption controls; + // only Linux (client-side decorations) overlays its own at the top-right. + #[cfg(target_os = "linux")] { base = base.child(crate::titlebar::window_controls_overlay(&self.colors)); } diff --git a/crates/notes/src/token.rs b/crates/notes/src/token.rs index 864c39a..5168791 100644 --- a/crates/notes/src/token.rs +++ b/crates/notes/src/token.rs @@ -59,8 +59,11 @@ pub fn write_info(port: u16, token: &str) { let body = serde_json::json!({ "port": port, "pid": std::process::id(), "token": token }).to_string(); if std::fs::write(&path, body).is_ok() { - use std::os::unix::fs::PermissionsExt; - let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600)); + } } } diff --git a/crates/pty/src/windows/spawn.rs b/crates/pty/src/windows/spawn.rs index 014d092..748d015 100644 --- a/crates/pty/src/windows/spawn.rs +++ b/crates/pty/src/windows/spawn.rs @@ -171,18 +171,18 @@ fn quote(arg: &str) -> String { '\\' => backslashes += 1, '"' => { // Escape the run of backslashes preceding the quote, then the quote. - out.extend(std::iter::repeat('\\').take(backslashes * 2 + 1)); + out.push_str(&"\\".repeat(backslashes * 2 + 1)); out.push('"'); backslashes = 0; } _ => { - out.extend(std::iter::repeat('\\').take(backslashes)); + out.push_str(&"\\".repeat(backslashes)); backslashes = 0; out.push(c); } } } - out.extend(std::iter::repeat('\\').take(backslashes * 2)); + out.push_str(&"\\".repeat(backslashes * 2)); out.push('"'); out }