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
13 changes: 9 additions & 4 deletions .github/workflows/windows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions crates/app/src/ipc/mod.rs
Original file line number Diff line number Diff line change
@@ -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::*;
File renamed without changes.
37 changes: 37 additions & 0 deletions crates/app/src/ipc/windows.rs
Original file line number Diff line number Diff line change
@@ -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<Value, String> {
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 <op>`): unavailable on Windows.
#[cfg(debug_assertions)]
pub fn run_cli(_args: &[String]) -> i32 {
eprintln!("prompt ipc: not supported on Windows");
2
}
6 changes: 6 additions & 0 deletions crates/app/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
19 changes: 18 additions & 1 deletion crates/app/src/pluginhost.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -167,7 +184,7 @@ pub fn invoke(plugin: &plugin::Plugin, req: &Request) -> Result<Response, String
waited += POLL;
}
if !flag.load(Ordering::Relaxed) {
unsafe { libc::kill(pid as libc::pid_t, libc::SIGKILL) };
force_kill(pid);
}
});

Expand Down
25 changes: 19 additions & 6 deletions crates/app/src/pluginwebview.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,15 +396,28 @@ fn run_service(command: &str, dir: &std::path::Path) -> 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));
Expand Down
5 changes: 3 additions & 2 deletions crates/app/src/root/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
7 changes: 5 additions & 2 deletions crates/notes/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
}
}

Expand Down
6 changes: 3 additions & 3 deletions crates/pty/src/windows/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
Loading