use std::path::{Path, PathBuf};
use crate::hooks::cline_app::process::ExeProcess;
use crate::model_relay::readiness::{Restart, SurfaceDecl};
fn electron_main(p: &ExeProcess) -> bool {
crate::hooks::cursor::is_main_process(p)
}
fn any(_: &ExeProcess) -> bool {
true
}
fn claude_session(p: &ExeProcess) -> bool {
let line = p.args.join(" ");
let first = p.args.get(1).map(String::as_str);
!line.contains("--bg-pty-host") && first != Some("daemon")
}
fn app_root(binary: &Path) -> Option<PathBuf> {
binary
.ancestors()
.find(|a| a.extension().is_some_and(|e| e == "app"))
.or_else(|| binary.parent())
.map(Path::to_path_buf)
}
pub(crate) fn claude_code(home: &Path) -> Vec<SurfaceDecl> {
let mut roots = vec![home.join(".local").join("share").join("claude")];
if cfg!(windows) {
roots.push(home.join(".local").join("bin"));
}
if cfg!(target_os = "macos") {
roots.push(
home.join("Library")
.join("Application Support")
.join("Claude")
.join("claude-code"),
);
}
if let Some(data) = dirs::data_dir().filter(|_| cfg!(windows)) {
roots.push(data.join("Claude").join("claude-code"));
}
vec![SurfaceDecl {
name: "Claude Code session",
roots,
counts: claude_session,
picks_up_live: false,
restart: Restart::NewSession,
}]
}
pub(crate) fn codex(home: &Path) -> Vec<SurfaceDecl> {
let mut app = Vec::new();
if cfg!(target_os = "macos") {
for root in [PathBuf::from("/Applications"), home.join("Applications")] {
app.push(root.join("ChatGPT.app"));
app.push(root.join("Codex.app"));
}
}
if let Some(local) = dirs::data_local_dir().filter(|_| cfg!(windows)) {
app.push(local.join("OpenAI").join("Codex"));
}
let mut cli = vec![home.join(".codex").join("packages")];
let path = crate::core::user_path::effective_path();
if let Some(found) = crate::hooks::opencode::find_on_path(Some(path.as_os_str()), "codex") {
let real = std::fs::canonicalize(&found).unwrap_or(found);
cli.extend(
real.ancestors()
.find(|a| a.ends_with(Path::new("@openai").join("codex")))
.map(Path::to_path_buf)
.or_else(|| real.parent().map(Path::to_path_buf)),
);
}
vec![
SurfaceDecl {
name: "Codex app",
roots: app,
counts: any,
picks_up_live: false,
restart: Restart::QuitAndReopen,
},
SurfaceDecl {
name: "Codex session",
roots: cli,
counts: any,
picks_up_live: false,
restart: Restart::NewSession,
},
]
}
pub(crate) fn cursor(home: &Path) -> Vec<SurfaceDecl> {
vec![
SurfaceDecl {
name: "Cursor",
roots: crate::hooks::cursor::ide_roots(home),
counts: electron_main,
picks_up_live: false,
restart: Restart::QuitAndReopen,
},
SurfaceDecl {
name: "Cursor CLI session",
roots: crate::hooks::cursor::cli_install_dirs(home),
counts: any,
picks_up_live: false,
restart: Restart::NewSession,
},
]
}
pub(crate) fn cline(home: &Path) -> Vec<SurfaceDecl> {
let roots = crate::hooks::cline_hosts::vscode_binary_candidates(
None,
Some(home),
false,
dirs::data_local_dir().as_deref(),
std::env::var_os("ProgramFiles")
.map(PathBuf::from)
.as_deref(),
)
.iter()
.filter_map(|b| app_root(b))
.collect();
vec![SurfaceDecl {
name: "VS Code",
roots,
counts: electron_main,
picks_up_live: false,
restart: Restart::QuitAndReopen,
}]
}
pub(crate) fn opencode(home: &Path) -> Vec<SurfaceDecl> {
let path = crate::core::user_path::effective_path();
let roots = crate::hooks::opencode::installed_in(home, Some(path.as_os_str()))
.cli
.map(|p| std::fs::canonicalize(&p).unwrap_or(p))
.and_then(|p| p.parent().map(Path::to_path_buf))
.into_iter()
.collect();
vec![SurfaceDecl {
name: "OpenCode session",
roots,
counts: any,
picks_up_live: false,
restart: Restart::NewSession,
}]
}
#[cfg(test)]
mod tests {
use super::*;
fn proc(args: &[&str]) -> ExeProcess {
ExeProcess {
exe: PathBuf::from("/x/claude"),
args: args.iter().map(|a| a.to_string()).collect(),
started: None,
}
}
#[test]
fn a_claude_session_counts_and_its_helpers_do_not() {
assert!(claude_session(&proc(&["claude"])));
assert!(claude_session(&proc(&["claude", "--resume", "x"])));
assert!(!claude_session(&proc(&[
"claude",
"--bg-pty-host",
"/tmp/s.sock"
])));
assert!(!claude_session(&proc(&["claude", "daemon", "run"])));
}
#[test]
fn a_bundle_binary_roots_at_its_app() {
assert_eq!(
app_root(Path::new(
"/Applications/Visual Studio Code.app/Contents/MacOS/Code"
)),
Some(PathBuf::from("/Applications/Visual Studio Code.app"))
);
assert_eq!(
app_root(Path::new("/opt/visual-studio-code/code")),
Some(PathBuf::from("/opt/visual-studio-code"))
);
}
#[test]
fn nothing_picks_up_live_until_a_live_test_says_so() {
let home = Path::new("/home/u");
for d in claude_code(home)
.into_iter()
.chain(codex(home))
.chain(cursor(home))
.chain(cline(home))
.chain(opencode(home))
{
assert!(!d.picks_up_live, "{} needs its live test first", d.name);
}
}
}