use anyhow::{Context, Result};
use std::process::{Command, Stdio};
use crate::claude_status;
use crate::config::{Config, Tool};
use crate::state;
use crate::tmux::Tmux;
pub fn upgrade(
tmux: &Tmux,
config: &Config,
harness_name: &str,
harness: &Tool,
model: Option<&str>,
) -> Result<()> {
let sessions = tmux.list_sessions()?;
let mut upgraded = 0;
let mut skipped = 0;
for (name, _path) in &sessions {
if name == "muxr" {
continue;
}
let vertical = name.split('/').next().unwrap_or(name);
let tool = config.resolve_tool(vertical, None);
if tool != harness_name {
continue;
}
if !state::has_harness_process(tmux, name, &harness.bin) {
eprintln!(" {name}: no {harness_name} process, skipping");
skipped += 1;
continue;
}
let session_id = state::discover_session_id(tmux, name, Some(harness));
if session_id.is_none() {
eprintln!(" {name}: could not discover session ID, skipping");
skipped += 1;
continue;
}
let session_id = session_id.unwrap();
eprintln!(" {name}: upgrading (session {session_id})");
let shell_pid = tmux.pane_pid(name).ok().flatten();
let harness_pid = shell_pid.and_then(|sp| {
state::descendant_pids(sp)
.into_iter()
.find(|pid| is_harness_process(*pid, &harness.bin))
});
let target = Tmux::target(name);
let _ = Command::new("tmux")
.args(["send-keys", "-t", &target, "/exit", "Enter"])
.status();
if let Some(pid) = harness_pid {
wait_for_exit(pid, 10);
}
wait_for_prompt(tmux, name, 5);
let cmd = harness.launch_command(Some(name), Some(&session_id), model);
let _ = Command::new("tmux")
.args(["send-keys", "-t", &target, &cmd, "Enter"])
.status();
upgraded += 1;
}
eprintln!(
"\nUpgraded {upgraded} session(s), skipped {skipped}."
);
if let Some(m) = model {
eprintln!("Model: {m}");
}
Ok(())
}
pub fn status(
tmux: &Tmux,
config: &Config,
harness_name: &str,
_harness: &Tool,
) -> Result<()> {
let sessions = tmux.list_sessions()?;
eprintln!("{harness_name} sessions:\n");
eprintln!(
" {:30} {:6} {:10}",
"SESSION", "CTX %", "COST"
);
eprintln!(" {}", "-".repeat(50));
let mut count = 0;
for (name, _path) in &sessions {
if name == "muxr" {
continue;
}
let vertical = name.split('/').next().unwrap_or(name);
let tool = config.resolve_tool(vertical, None);
if tool != harness_name {
continue;
}
let health = claude_status::read_health(name);
let (ctx, cost) = match health {
Some(h) => (
format!("{}%", h.context_pct),
format!("${:.2}", h.cost_usd),
),
None => ("--".to_string(), "--".to_string()),
};
eprintln!(" {:30} {:>6} {:>10}", name, ctx, cost);
count += 1;
}
if count == 0 {
eprintln!(" (no active {harness_name} sessions)");
}
Ok(())
}
pub fn model_switch(
tmux: &Tmux,
config: &Config,
harness_name: &str,
harness: &Tool,
model: Option<&str>,
) -> Result<()> {
let model = model.context("Usage: muxr {harness_name} model <model-name>")?;
let cmd_template = harness
.model_switch_command
.as_ref()
.context("Harness does not support live model switch")?;
let cmd = crate::config::interpolate_raw(cmd_template, "model", model);
let sessions = tmux.list_sessions()?;
let mut switched = 0;
for (name, _) in &sessions {
if name == "muxr" {
continue;
}
let vertical = name.split('/').next().unwrap_or(name);
let tool = config.resolve_tool(vertical, None);
if tool != harness_name {
continue;
}
let target = Tmux::target(name);
let _ = Command::new("tmux")
.args(["send-keys", "-t", &target, &cmd, "Enter"])
.status();
eprintln!(" {name}: sent {cmd}");
switched += 1;
}
eprintln!("\nSwitched {switched} session(s) to {model}.");
Ok(())
}
pub fn compact(
tmux: &Tmux,
config: &Config,
harness_name: &str,
harness: &Tool,
threshold: Option<u32>,
) -> Result<()> {
let threshold = threshold.unwrap_or(80);
let cmd = harness
.compact_command
.as_ref()
.context("Harness does not support compact")?;
let sessions = tmux.list_sessions()?;
let mut compacted = 0;
for (name, _) in &sessions {
if name == "muxr" {
continue;
}
let vertical = name.split('/').next().unwrap_or(name);
let tool = config.resolve_tool(vertical, None);
if tool != harness_name {
continue;
}
let health = crate::claude_status::read_health(name);
let pct = health.as_ref().map(|h| h.context_pct).unwrap_or(0);
if pct >= threshold {
let target = Tmux::target(name);
let _ = Command::new("tmux")
.args(["send-keys", "-t", &target, cmd, "Enter"])
.status();
eprintln!(" {name}: {pct}% -> compacting");
compacted += 1;
} else {
eprintln!(" {name}: {pct}% (under threshold)");
}
}
eprintln!("\nCompacted {compacted} session(s) (threshold: {threshold}%).");
Ok(())
}
fn is_harness_process(pid: u32, bin: &str) -> bool {
let suffix = format!("/{bin}");
Command::new("ps")
.args(["-p", &pid.to_string(), "-o", "args="])
.output()
.map(|o| {
let args_str = String::from_utf8_lossy(&o.stdout);
args_str
.split_whitespace()
.any(|tok| tok == bin || tok.ends_with(&suffix))
})
.unwrap_or(false)
}
fn wait_for_exit(pid: u32, timeout_secs: u32) {
for _ in 0..timeout_secs * 10 {
let alive = Command::new("kill")
.args(["-0", &pid.to_string()])
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if !alive {
return;
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
eprintln!(" process {pid} did not exit, sending SIGKILL");
let _ = Command::new("kill")
.args(["-9", &pid.to_string()])
.status();
}
fn wait_for_prompt(_tmux: &Tmux, session: &str, timeout_secs: u32) {
let target = Tmux::target(session);
for _ in 0..timeout_secs * 10 {
if let Ok(output) = std::process::Command::new("tmux")
.args(["capture-pane", "-p", "-t", &target])
.output()
{
let content = String::from_utf8_lossy(&output.stdout);
let last_line = content.lines().rev().find(|l| !l.is_empty()).unwrap_or("");
if last_line.ends_with('$')
|| last_line.ends_with('%')
|| last_line.ends_with('#')
|| last_line.ends_with('>')
{
return;
}
}
std::thread::sleep(std::time::Duration::from_millis(100));
}
}