use super::CommandExecutor;
use crate::client::http_client::SessionRow;
use crate::client::resolver::resolve_target;
use crate::client::result::{
CommandResult, DecisionCounts, DiscoveredProjectSummary, RecommendationSummary, SessionSummary,
TmuxSessionSummary,
};
pub(crate) const MAX_OUTPUT_CHARS: usize = 4000;
impl CommandExecutor {
pub(super) async fn sessions(&self) -> CommandResult {
match self.client().sessions().await {
Ok(rows) => CommandResult::Sessions(
rows.into_iter()
.map(|s| SessionSummary {
id: s.id.0.to_string(),
status: status_label(s.status),
workdir: s.workdir,
})
.collect(),
),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn status(&self, session_id: &str) -> CommandResult {
match self.client().session_events(session_id).await {
Ok(events) => {
let names: Vec<String> = events
.iter()
.rev()
.take(5)
.rev()
.map(|e| e.event.wire_name().to_string())
.collect();
CommandResult::SessionDetail {
id: session_id.to_string(),
status: "active".to_string(),
events: names,
}
}
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn decide(&self, session_id: &str, approved: bool) -> CommandResult {
let managed = match self.client().list_managed_sessions().await {
Ok(managed) => managed,
Err(e) => return CommandResult::Error(format!("daemon unreachable: {e}")),
};
if let Some(found) = resolve_target(&managed, session_id) {
let answer = decide_answer(approved, found.proposed_default.as_deref());
return match self
.client()
.answer_managed_session(&found.id, &answer)
.await
{
Ok(_) if approved => CommandResult::Approved {
session_id: found.id.clone(),
},
Ok(_) => CommandResult::Denied {
session_id: found.id.clone(),
},
Err(e) => CommandResult::Error(format!("answer failed: {e}")),
};
}
let exists = match self.client().sessions().await {
Ok(rows) => rows.iter().any(|s| s.id.0.to_string() == session_id),
Err(e) => return CommandResult::Error(format!("daemon unreachable: {e}")),
};
if !exists {
return CommandResult::Error(format!("session {session_id} not found"));
}
if approved {
CommandResult::Approved {
session_id: session_id.to_string(),
}
} else {
CommandResult::Denied {
session_id: session_id.to_string(),
}
}
}
pub(super) async fn overseer(&self) -> CommandResult {
match self.client().overseer_status().await {
Ok(snap) => CommandResult::OverseerStatus {
enabled: snap.enabled,
handler: snap.handler,
decisions: DecisionCounts {
allow: snap.decisions.0,
block: snap.decisions.1,
flag: snap.decisions.2,
},
},
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn tmux(&self) -> CommandResult {
match self.client().tmux_sessions().await {
Ok(rows) => CommandResult::TmuxSessions(
rows.into_iter()
.map(|r| TmuxSessionSummary {
name: r.name,
managed: r.managed,
})
.collect(),
),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn projects(&self) -> CommandResult {
match self.client().discover_projects().await {
Ok(rows) => CommandResult::DiscoveredProjects(
rows.into_iter()
.map(|r| DiscoveredProjectSummary {
path: r.path,
session_count: r.session_count,
last_session: r.last_session,
})
.collect(),
),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn discover(&self) -> CommandResult {
match self.client().discover_sessions().await {
Ok(count) => CommandResult::Discovered { count },
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn adopt(&self, session: &str) -> CommandResult {
match self.client().adopt_tmux_session(session).await {
Ok(true) => CommandResult::Adopted {
session: session.to_string(),
},
Ok(false) => CommandResult::Error(format!("tmux session {session} not found")),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub async fn register_project(&self, path: &str) -> CommandResult {
match self.client().register_project(path).await {
Ok(()) => CommandResult::ProjectRegistered {
path: path.to_string(),
},
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn config(&self, project: &str) -> CommandResult {
match self.client().analyze_config(project).await {
Ok(recs) => CommandResult::ConfigAnalysis {
project: project.to_string(),
recommendations: recs
.into_iter()
.map(|r| RecommendationSummary {
id: r.id,
message: r.message,
})
.collect(),
},
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn snapshot(&self, session: &str) -> CommandResult {
match self.client().snapshot_tmux_session(session).await {
Ok(Some(output)) => CommandResult::Snapshot {
session: session.to_string(),
output,
},
Ok(None) => CommandResult::Error(format!("tmux session {session} not found")),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn kill(&self, session_id: &str) -> CommandResult {
match self.client().kill_session(session_id).await {
Ok(true) => CommandResult::Killed {
session_id: session_id.to_string(),
},
Ok(false) => CommandResult::Error(format!("session {session_id} not found")),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn send(&self, session: &str, prompt: &str) -> CommandResult {
if prompt.trim().is_empty() {
return CommandResult::Error("send: a prompt is required".to_string());
}
let rows = match self.client().sessions().await {
Ok(rows) => rows,
Err(e) => return CommandResult::Error(format!("daemon unreachable: {e}")),
};
let Some(target) = resolve_session(&rows, session) else {
return CommandResult::Error(format!("session {session} not found"));
};
match self.client().send_session_command(&target, prompt).await {
Ok(Some(output)) => CommandResult::CommandSent {
session: target,
output: truncate_output(&output),
},
Ok(None) => CommandResult::Error(format!("session {session} not found")),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn launch(&self, project: &std::path::Path) -> CommandResult {
let workdir = project.to_string_lossy().to_string();
match self.client().launch_session(&workdir).await {
Ok(session) => CommandResult::SessionStarted {
session,
workdir,
deployed: true,
},
Err(e) => CommandResult::Error(format!("launch failed: {e}")),
}
}
pub(super) async fn connect(&self, project: &std::path::Path) -> CommandResult {
let workdir = project.to_string_lossy().to_string();
match self.client().connect_session(&workdir).await {
Ok(session) => CommandResult::SessionStarted {
session,
workdir,
deployed: false,
},
Err(e) => CommandResult::Error(format!("connect failed: {e}")),
}
}
pub(super) async fn doctor(&self) -> CommandResult {
let cwd = std::env::current_dir()
.ok()
.map(|p| p.display().to_string());
match self.client().doctor(cwd.as_deref()).await {
Ok(report) => CommandResult::Doctor(report),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
pub(super) async fn coordinator_chat(&self, message: &str) -> CommandResult {
match self.client().coordinator_chat(message, &[], false).await {
Ok(Some(outcome)) => {
let reply = match outcome.command_output {
Some(output) => format!("{}\n{output}", outcome.reply),
None => outcome.reply,
};
CommandResult::ChatReply { reply }
}
Ok(None) => CommandResult::Error(
"coordinator chat is not configured (no OpenRouter API key)".to_string(),
),
Err(e) => CommandResult::Error(format!("daemon unreachable: {e}")),
}
}
}
pub(crate) fn resolve_session(rows: &[SessionRow], query: &str) -> Option<String> {
resolve_target(rows, query).map(target_of)
}
fn target_of(row: &SessionRow) -> String {
if row.tmux_name.is_empty() {
row.id.0.to_string()
} else {
row.tmux_name.clone()
}
}
pub(crate) fn decide_answer(approved: bool, proposed_default: Option<&str>) -> String {
if !approved {
return "no".to_string();
}
proposed_default
.map(str::to_string)
.unwrap_or_else(|| "yes".to_string())
}
pub(crate) fn truncate_output(text: &str) -> String {
if text.chars().count() <= MAX_OUTPUT_CHARS {
return text.to_string();
}
let head: String = text.chars().take(MAX_OUTPUT_CHARS).collect();
format!("{head}\n… (output truncated)")
}
fn status_label(status: crate::core::session::SessionStatus) -> String {
serde_json::to_value(status)
.ok()
.and_then(|v| v.as_str().map(str::to_string))
.unwrap_or_else(|| "unknown".to_string())
}