use serde_json::{Value, json};
use crate::daemon_client;
pub fn spawn(
project_name: &str,
ws_name: Option<&str>,
agent_type: Option<&str>,
host: Option<&str>,
) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({"project": project_name});
if let Some(ws) = ws_name {
params["ws"] = json!(ws);
}
if let Some(t) = agent_type {
params["type"] = json!(t);
}
if let Some(h) = host {
params["host"] = json!(h);
}
match client.request("agent.spawn", params) {
Ok(result) => {
let agent_id = result
.get("agent_id")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let context = ws_name.unwrap_or(project_name);
println!("Spawned agent '{agent_id}' in '{context}'");
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
pub fn prompt(agent_id: &str, text: &str, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({"agent_id": agent_id, "text": text});
if let Some(h) = host {
params["host"] = json!(h);
}
match client.request("agent.prompt", params) {
Ok(_) => println!("Prompt sent to agent '{agent_id}'"),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
pub fn kill(agent_id: &str, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({"agent_id": agent_id});
if let Some(h) = host {
params["host"] = json!(h);
}
match client.request("agent.kill", params) {
Ok(_) => println!("Killed agent '{agent_id}'"),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
pub fn attach_shell(agent_id: &str, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({"agent_id": agent_id});
if let Some(h) = host {
params["host"] = json!(h);
}
let shell_id = match client.request("agent.shell-id", params) {
Ok(result) => result
.get("shell_id")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
drop(client);
crate::shell::attach(&shell_id, host);
}
pub fn conversation(agent_id: &str, watch: bool, verbose: bool, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let dim = "\x1b[2m";
let reset = "\x1b[0m";
let blue = "\x1b[34m";
let green = "\x1b[32m";
let yellow = "\x1b[33m";
let cyan = "\x1b[36m";
let mut status_params = json!({"agent_id": agent_id});
if let Some(h) = host {
status_params["host"] = json!(h);
}
if let Ok(status) = client.request("agent.status", status_params)
&& let Some(session) = status.get("session_file").and_then(|v| v.as_str())
{
eprintln!("{dim}session: {session}{reset}");
}
if watch {
eprintln!("{dim}[watching — press Ctrl+C to stop]{reset}");
}
eprintln!();
let mut seen = 0usize;
loop {
let mut msg_params = json!({"agent_id": agent_id});
if let Some(h) = host {
msg_params["host"] = json!(h);
}
let messages = match client.request("agent.messages", msg_params) {
Ok(val) => val.as_array().cloned().unwrap_or_default(),
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
};
for msg in messages.iter().skip(seen) {
let role = msg.get("role").and_then(|v| v.as_str()).unwrap_or("");
match role {
"user" => {
let content = msg.get("content").and_then(|v| v.as_str()).unwrap_or("");
println!("{blue}━━━ User ━━━{reset}");
println!("{content}");
println!();
}
"assistant" => {
if let Some(blocks) = msg.get("content").and_then(|v| v.as_array()) {
let has_text = blocks
.iter()
.any(|b| b.get("type").and_then(|v| v.as_str()) == Some("text"));
if verbose {
println!("{green}━━━ Assistant ━━━{reset}");
for block in blocks {
render_block(block, dim, reset, yellow, cyan);
}
println!();
} else if has_text {
println!("{green}━━━ Assistant ━━━{reset}");
for block in blocks {
if block.get("type").and_then(|v| v.as_str()) == Some("text") {
render_block(block, dim, reset, yellow, cyan);
}
}
println!();
}
} else if let Some(content) = msg.get("content").and_then(|v| v.as_str()) {
println!("{green}━━━ Assistant ━━━{reset}");
println!("{content}");
println!();
}
}
"tool_result" if verbose => {
if let Some(blocks) = msg.get("content").and_then(|v| v.as_array()) {
for block in blocks {
render_tool_result(block, dim, reset, cyan);
}
}
}
_ => {}
}
}
seen = messages.len();
if !watch {
if seen == 0 {
println!("{dim}No messages yet.{reset}");
}
break;
}
std::thread::sleep(std::time::Duration::from_secs(2));
}
}
fn render_block(block: &Value, dim: &str, reset: &str, yellow: &str, cyan: &str) {
let btype = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
match btype {
"text" => {
let text = block.get("text").and_then(|v| v.as_str()).unwrap_or("");
println!("{text}");
}
"thinking" => {
let thinking = block.get("thinking").and_then(|v| v.as_str()).unwrap_or("");
let preview = if thinking.len() > 200 {
&thinking[..200]
} else {
thinking
};
println!("{dim} [thinking] {preview}...{reset}");
}
"tool_use" => {
let name = block.get("name").and_then(|v| v.as_str()).unwrap_or("?");
let input = block
.get("input")
.map(|v| {
let s = v.to_string();
if s.len() > 120 {
format!("{}...", &s[..120])
} else {
s
}
})
.unwrap_or_default();
println!("{yellow} [{cyan}{name}{yellow}]{reset} {dim}{input}{reset}");
}
"tool_result" => {
render_tool_result_block(block, dim, reset, cyan);
}
_ => {}
}
}
fn render_tool_result(block: &Value, dim: &str, reset: &str, cyan: &str) {
render_tool_result_block(block, dim, reset, cyan);
}
fn render_tool_result_block(block: &Value, dim: &str, reset: &str, cyan: &str) {
let tool_use_id = block
.get("tool_use_id")
.and_then(|v| v.as_str())
.unwrap_or("?");
let id_short = if tool_use_id.len() > 12 {
&tool_use_id[tool_use_id.len() - 8..]
} else {
tool_use_id
};
let content = &block["content"];
let preview = match content {
Value::String(s) => {
if s.len() > 300 {
format!("{}…", &s[..300])
} else {
s.clone()
}
}
Value::Array(arr) => {
let texts: Vec<&str> = arr
.iter()
.filter_map(|v| v.get("text").and_then(|t| t.as_str()))
.collect();
let joined = texts.join("\n");
if joined.len() > 300 {
format!("{}…", &joined[..300])
} else {
joined
}
}
_ => content.to_string(),
};
let lines: Vec<&str> = preview.lines().collect();
if lines.len() <= 4 {
println!("{dim} ⏎ {cyan}{id_short}{reset} {dim}{preview}{reset}");
} else {
println!(
"{dim} ⏎ {cyan}{id_short}{reset} {dim}{} (+{} more lines){reset}",
lines[..3].join("\n "),
lines.len() - 3
);
}
}
pub fn list(project_name: Option<&str>, ws_name: Option<&str>, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({});
if let Some(h) = host {
params["host"] = json!(h);
}
match client.request("agent.list", params) {
Ok(result) => {
let agents = result.as_array().unwrap_or(&Vec::new()).clone();
let filtered: Vec<&Value> = agents
.iter()
.filter(|a| {
if let Some(ws) = ws_name {
a.get("workstream_id")
.and_then(|v| v.as_str())
.is_some_and(|id| id == ws)
} else {
true
}
})
.collect();
if filtered.is_empty() {
if let Some(ws) = ws_name {
let project = project_name.unwrap_or("?");
println!("No agents running for '{project}/{ws}'.");
} else {
println!("No agents running.");
}
return;
}
println!(
"{:<12} {:<15} {:<10} {:<15} {:<10} SESSION",
"HOST", "ID", "TYPE", "STATE", "SHELL"
);
println!("{}", "─".repeat(90));
for agent in &filtered {
let session = agent
.get("session_file")
.and_then(|v| v.as_str())
.unwrap_or("-");
let session_short = session.rsplit('/').next().unwrap_or(session);
println!(
"{:<12} {:<15} {:<10} {:<15} {:<10} {}",
agent.get("host").and_then(|v| v.as_str()).unwrap_or("-"),
agent.get("id").and_then(|v| v.as_str()).unwrap_or("-"),
agent.get("type").and_then(|v| v.as_str()).unwrap_or("-"),
agent.get("state").and_then(|v| v.as_str()).unwrap_or("-"),
agent
.get("shell_id")
.and_then(|v| v.as_str())
.unwrap_or("-"),
session_short,
);
}
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}
pub fn status(agent_id: &str, host: Option<&str>) {
let mut client = daemon_client::connect_to_hub_or_exit();
let mut params = json!({"agent_id": agent_id});
if let Some(h) = host {
params["host"] = json!(h);
}
match client.request("agent.status", params) {
Ok(result) => {
println!(
"Agent: {}",
result
.get("id")
.and_then(|v| v.as_str())
.unwrap_or(agent_id)
);
println!(
" Type: {}",
result.get("type").and_then(|v| v.as_str()).unwrap_or("-")
);
println!(
" State: {}",
result.get("state").and_then(|v| v.as_str()).unwrap_or("-")
);
println!(
" Shell: {}",
result
.get("shell_id")
.and_then(|v| v.as_str())
.unwrap_or("-")
);
println!(
" Session: {}",
result
.get("session_file")
.and_then(|v| v.as_str())
.unwrap_or("-")
);
println!(
" Messages: {}",
result
.get("message_count")
.and_then(|v| v.as_u64())
.unwrap_or(0)
);
println!(
" Spawned: {}",
result
.get("spawned_at")
.and_then(|v| v.as_str())
.unwrap_or("-")
);
println!(
" Last activity: {}",
result
.get("last_activity")
.and_then(|v| v.as_str())
.unwrap_or("-")
);
}
Err(e) => {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
}