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:
2026-06-15 15:28:19 +07:00
parent e37faf49d3
commit 4419f5660e
18 changed files with 365 additions and 42 deletions
+92
View File
@@ -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> {