plannotator_tui_hosts/
lib.rs1pub mod claude;
9pub mod codex;
10pub mod copilot;
11pub mod droid;
12pub mod pi;
13
14use std::path::PathBuf;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Host {
19 ClaudeCode,
20 Codex,
21 Copilot,
23 Droid,
25 Pi,
26}
27
28impl Host {
29 pub fn label(self) -> &'static str {
31 match self {
32 Self::ClaudeCode => "claude",
33 Self::Codex => "codex",
34 Self::Copilot => "copilot",
35 Self::Droid => "droid",
36 Self::Pi => "pi",
37 }
38 }
39
40 pub const ALL: [Host; 5] = [Host::ClaudeCode, Host::Codex, Host::Pi, Host::Copilot, Host::Droid];
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub enum Role {
46 Human,
47 Assistant,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct Message {
53 pub id: String,
54 pub role: Role,
55 pub text: String,
56 pub at: Option<String>,
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct SessionMeta {
63 pub pid: u32,
64 pub session_id: String,
65 pub cwd: PathBuf,
66 pub started_at: u64,
67}
68
69#[derive(Debug)]
70pub enum HostError {
71 NoTranscript(String),
73 NoMessages(String),
75 Unsupported(String),
77 Io(std::io::Error),
78}
79
80impl std::fmt::Display for HostError {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::NoTranscript(msg) | Self::NoMessages(msg) => f.write_str(msg),
84 Self::Unsupported(host) => write!(f, "{host} is not supported yet"),
85 Self::Io(err) => write!(f, "{err}"),
86 }
87 }
88}
89
90impl std::error::Error for HostError {}
91
92impl From<std::io::Error> for HostError {
93 fn from(err: std::io::Error) -> Self {
94 Self::Io(err)
95 }
96}
97
98pub fn detect_host(env: impl Fn(&str) -> Option<String>) -> Result<Host, HostError> {
104 let set = |key: &str| env(key).is_some_and(|v| !v.trim().is_empty());
105 if let Some(name) = env("PLANNOTATOR_TUI_HOST").map(|v| v.trim().to_ascii_lowercase()) {
106 match name.as_str() {
107 "claude" | "claude-code" | "claude_code" => return Ok(Host::ClaudeCode),
108 "codex" => return Ok(Host::Codex),
109 "copilot" | "copilot-cli" | "copilot_cli" => return Ok(Host::Copilot),
110 "droid" | "factory" => return Ok(Host::Droid),
111 "pi" => return Ok(Host::Pi),
112 _ => {}
113 }
114 }
115 if set("CODEX_THREAD_ID") {
116 return Ok(Host::Codex);
117 }
118 if set("COPILOT_CLI") {
119 return Ok(Host::Copilot);
120 }
121 if env("AI_AGENT").is_some_and(|v| v.trim().eq_ignore_ascii_case("pi")) || set("PI_CODING_AGENT") {
123 return Ok(Host::Pi);
124 }
125 for (key, name) in [("OPENCODE", "OpenCode"), ("GEMINI_CLI", "Gemini CLI"), ("OMPCODE", "OMP")] {
126 if set(key) {
127 return Err(HostError::Unsupported(name.to_owned()));
128 }
129 }
130 Ok(Host::ClaudeCode)
131}