wip: in-progress changes (grid, config, wizard, settings, pty) before session-persistence
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,98 @@ pub fn default_shell() -> String {
|
||||
"/bin/sh".into()
|
||||
}
|
||||
|
||||
/// The user's full PATH for spawning panels: their login-shell PATH (which sources
|
||||
/// `.zprofile`/`.zshrc`), merged with the daemon's current PATH and common install
|
||||
/// dirs. Cached for the daemon's lifetime.
|
||||
///
|
||||
/// Why: when the GUI launches the daemon (Finder/launchd), the inherited PATH is
|
||||
/// minimal (`/usr/bin:/bin:…`), so agents like `claude`, `codex`, `gemini` — installed
|
||||
/// in `~/.local/bin`, npm-global, or Homebrew — aren't found and the panel exits
|
||||
/// immediately with "Process exited". A bare `/bin/zsh` still works, which is why
|
||||
/// shells launched fine but agents didn't.
|
||||
pub fn enriched_path() -> String {
|
||||
use std::collections::HashSet;
|
||||
use std::sync::OnceLock;
|
||||
static CACHE: OnceLock<String> = OnceLock::new();
|
||||
CACHE
|
||||
.get_or_init(|| {
|
||||
let mut dirs: Vec<String> = Vec::new();
|
||||
let mut seen: HashSet<String> = HashSet::new();
|
||||
let mut merge = |src: &str| {
|
||||
for d in src.split(':') {
|
||||
if !d.is_empty() && seen.insert(d.to_string()) {
|
||||
dirs.push(d.to_string());
|
||||
}
|
||||
}
|
||||
};
|
||||
if let Some(p) = login_shell_path() {
|
||||
merge(&p);
|
||||
}
|
||||
if let Ok(p) = std::env::var("PATH") {
|
||||
merge(&p);
|
||||
}
|
||||
merge(&fallback_path_dirs());
|
||||
dirs.join(":")
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Whether `cmd` is an executable resolvable on the spawn PATH (or an existing path
|
||||
/// if it contains a slash). Used to only offer agents the user actually has installed.
|
||||
pub fn is_installed(cmd: &str) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
if cmd.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let is_exec = |p: &std::path::Path| {
|
||||
p.metadata().map(|m| m.is_file() && m.permissions().mode() & 0o111 != 0).unwrap_or(false)
|
||||
};
|
||||
if cmd.contains('/') {
|
||||
return is_exec(std::path::Path::new(cmd));
|
||||
}
|
||||
enriched_path().split(':').any(|dir| !dir.is_empty() && is_exec(&std::path::Path::new(dir).join(cmd)))
|
||||
}
|
||||
|
||||
/// Common locations user-installed CLIs land in, as a colon-joined fallback.
|
||||
fn fallback_path_dirs() -> String {
|
||||
let mut v = vec![
|
||||
"/opt/homebrew/bin".to_string(),
|
||||
"/usr/local/bin".to_string(),
|
||||
"/usr/bin".to_string(),
|
||||
"/bin".to_string(),
|
||||
"/usr/sbin".to_string(),
|
||||
"/sbin".to_string(),
|
||||
];
|
||||
if let Ok(home) = std::env::var("HOME") {
|
||||
if !home.is_empty() {
|
||||
for d in [".local/bin", ".npm-global/bin", ".cargo/bin", ".bun/bin", ".deno/bin", ".volta/bin", "go/bin"] {
|
||||
v.push(format!("{home}/{d}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
v.join(":")
|
||||
}
|
||||
|
||||
/// Capture PATH from the user's login+interactive shell so rc-file PATH edits apply.
|
||||
/// Parses `env` output (the exported PATH is colon-joined regardless of shell, so this
|
||||
/// works for fish too, where `$PATH` would otherwise print space-separated).
|
||||
#[cfg(unix)]
|
||||
fn login_shell_path() -> Option<String> {
|
||||
let shell = login_shell().or_else(|| std::env::var("SHELL").ok())?;
|
||||
let out = std::process::Command::new(&shell)
|
||||
.args(["-lic", "env"])
|
||||
.output()
|
||||
.ok()?;
|
||||
String::from_utf8_lossy(&out.stdout)
|
||||
.lines()
|
||||
.find_map(|l| l.strip_prefix("PATH="))
|
||||
.map(|p| p.trim().to_string())
|
||||
.filter(|p| !p.is_empty())
|
||||
}
|
||||
|
||||
#[cfg(not(unix))]
|
||||
fn login_shell_path() -> Option<String> { None }
|
||||
|
||||
/// The current user's login shell from the passwd database (`getpwuid`).
|
||||
#[cfg(unix)]
|
||||
fn login_shell() -> Option<String> {
|
||||
|
||||
Reference in New Issue
Block a user