use std::io::IsTerminal;
use anyhow::{Context, Result};
use super::daemon;
use crate::{formatter, socket_client, tui};
fn resolve_session_arg(s: &str) -> (uuid::Uuid, bool) {
if let Ok(uuid) = uuid::Uuid::parse_str(s) {
return (uuid, true);
}
let ns = uuid::Uuid::NAMESPACE_URL;
(uuid::Uuid::new_v5(&ns, s.as_bytes()), false)
}
pub(crate) async fn run_snapshot_tui(
prompt: String,
auto_approve: bool,
session_name: Option<String>,
width: u16,
height: u16,
) -> Result<()> {
use astrid_core::SessionId;
daemon::ensure_daemon("snapshot-tui").await?;
let session_id = if let Some(s) = session_name.as_deref() {
SessionId::from_uuid(resolve_session_arg(s).0)
} else {
SessionId::from_uuid(uuid::Uuid::new_v4())
};
let mut client =
socket_client::connect_for_workspace(session_id.clone(), crate::principal::current(), None)
.await
.context("Failed to connect to daemon")?;
let workspace = std::env::current_dir().ok();
tui::headless::run(tui::headless::HeadlessConfig {
client: &mut client,
session_id: &session_id,
workspace,
model_name: "",
prompt: &prompt,
width,
height,
auto_approve,
})
.await
}
pub(crate) async fn run_headless(
prompt: String,
format: formatter::OutputFormat,
auto_approve: bool,
session_name: Option<String>,
print_session: bool,
) -> Result<()> {
use astrid_core::SessionId;
daemon::ensure_daemon("headless").await?;
let session_id = if let Some(s) = session_name.as_deref() {
let (id, was_uuid) = resolve_session_arg(s);
if print_session {
if was_uuid {
eprintln!("[headless] Session: {id}");
} else {
eprintln!("[headless] Session: {s} ({id})");
}
}
SessionId::from_uuid(id)
} else {
let id = uuid::Uuid::new_v4();
if print_session {
eprintln!("[headless] Session: {id}");
}
SessionId::from_uuid(id)
};
let mut client =
socket_client::connect_for_workspace(session_id.clone(), crate::principal::current(), None)
.await
.context("Failed to connect to daemon")?;
let full_prompt = if std::io::stdin().is_terminal() {
prompt
} else {
let mut stdin_text = String::new();
std::io::Read::read_to_string(&mut std::io::stdin(), &mut stdin_text)?;
if stdin_text.is_empty() {
prompt
} else {
format!("{stdin_text}\n\n{prompt}")
}
};
crate::socket_client::send_input_as_active_agent(&mut client, full_prompt).await?;
let (response_text, tool_calls) =
collect_response(&mut client, &session_id, format, auto_approve).await?;
match format {
formatter::OutputFormat::Pretty => {
if !response_text.ends_with('\n') {
println!();
}
},
formatter::OutputFormat::Json => {
let output = serde_json::json!({
"response": response_text,
"tool_calls": tool_calls,
});
println!("{}", serde_json::to_string_pretty(&output)?);
},
}
let disconnect = astrid_types::ipc::IpcMessage::new(
astrid_types::Topic::client_disconnect(),
astrid_types::ipc::IpcPayload::Disconnect {
reason: Some("headless".to_string()),
},
session_id.0,
);
let _ = client.send_message(disconnect).await;
Ok(())
}
async fn collect_response(
client: &mut socket_client::SocketClient,
session_id: &astrid_core::SessionId,
format: formatter::OutputFormat,
auto_approve: bool,
) -> Result<(String, Vec<serde_json::Value>)> {
let mut response_text = String::new();
let mut tool_calls: Vec<serde_json::Value> = Vec::new();
let timeout_duration = std::time::Duration::from_mins(2);
loop {
let message = match tokio::time::timeout(timeout_duration, client.read_message()).await {
Ok(Ok(Some(msg))) => msg,
Ok(Ok(None)) => break,
Ok(Err(e)) => return Err(e.context("Failed to read from daemon")),
Err(_) => {
eprintln!("[headless] Timed out waiting for response (120s)");
std::process::exit(53);
},
};
match &message.payload {
astrid_types::ipc::IpcPayload::AgentResponse { text, is_final, .. } => {
if format == formatter::OutputFormat::Pretty {
print!("{text}");
let _ = std::io::Write::flush(&mut std::io::stdout());
}
response_text.push_str(text);
if *is_final {
break;
}
},
astrid_types::ipc::IpcPayload::LlmStreamEvent {
event: astrid_types::llm::StreamEvent::ToolCallStart { id, name },
..
} => {
tool_calls.push(serde_json::json!({
"type": "tool_call",
"id": id,
"name": name,
}));
},
astrid_types::ipc::IpcPayload::ToolExecuteResult { call_id, result } => {
tool_calls.push(serde_json::json!({
"type": "tool_result",
"call_id": call_id,
"content": result.content,
"is_error": result.is_error,
}));
},
astrid_types::ipc::IpcPayload::ApprovalRequired {
request_id, action, ..
} => {
let decision = if auto_approve { "approve" } else { "deny" };
eprintln!(
"[headless] Auto-{} approval for: {action}",
if auto_approve { "approved" } else { "denied" }
);
let response = astrid_types::ipc::IpcPayload::ApprovalResponse {
request_id: request_id.clone(),
decision: decision.to_string(),
reason: Some(
if auto_approve {
"headless --yes mode"
} else {
"headless mode"
}
.to_string(),
),
};
let topic = astrid_types::Topic::approval_response(request_id);
let msg = astrid_types::ipc::IpcMessage::new(topic, response, session_id.0);
client.send_message(msg).await?;
},
_ => {},
}
}
Ok((response_text, tool_calls))
}