use astrid_core::SessionId;
use colored::Colorize;
use crate::formatter::{OutputFormat, OutputFormatter, create_formatter};
use crate::repl::ReadlineEvent;
use crate::socket_client::SocketClient;
use crate::theme::Theme;
const APPROVAL_UNSUPPORTED_REASON: &str = "approvals not supported in JSON REPL mode";
const SELECTION_UNSUPPORTED_REASON: &str = "interactive selection not supported in JSON REPL mode";
const ELICIT_UNSUPPORTED_REASON: &str = "interactive input not supported in JSON REPL mode";
pub(crate) async fn run_chat(
client: &mut SocketClient,
session_id: &SessionId,
model_name: &str,
format: OutputFormat,
) -> anyhow::Result<()> {
match format {
OutputFormat::Pretty => {
let workspace = std::env::current_dir().ok();
crate::tui::run(client, session_id, workspace, model_name).await?;
},
OutputFormat::Json => {
run_json_chat(client, session_id, format).await?;
},
}
Ok(())
}
async fn run_json_chat(
client: &mut SocketClient,
session_id: &SessionId,
format: OutputFormat,
) -> anyhow::Result<()> {
let mut formatter: Box<dyn OutputFormatter> = create_formatter(format);
println!(
"Session: {} | Type {} to quit, {} for help
",
Theme::session_id(&session_id.0.to_string()),
"exit".cyan(),
"/help".cyan()
);
let mut editor = crate::repl::ReplEditor::new()?;
loop {
let input = match editor.readline() {
ReadlineEvent::Line(line) => line,
ReadlineEvent::Interrupted => continue,
ReadlineEvent::Eof => {
println!("{}", Theme::dimmed("Goodbye!"));
break;
},
};
let input = input.trim();
if input.is_empty() {
continue;
}
if input == "exit" || input == "quit" {
println!("{}", Theme::dimmed("Goodbye!"));
break;
}
if input.starts_with('/') {
handle_slash_command(input, client, session_id);
continue;
}
crate::socket_client::send_input_as_active_agent(client, input.to_string()).await?;
if !drain_agent_response(client, session_id, &mut *formatter).await? {
return Ok(());
}
}
Ok(())
}
async fn drain_agent_response(
client: &mut SocketClient,
session_id: &SessionId,
formatter: &mut dyn OutputFormatter,
) -> anyhow::Result<bool> {
loop {
let Some(message) = client.read_message().await? else {
eprintln!("{}", Theme::error("Connection to daemon lost"));
return Ok(false);
};
match message.payload {
astrid_types::ipc::IpcPayload::AgentResponse { text, is_final, .. } => {
formatter.format_text(&text);
if is_final {
formatter.flush_markdown();
return Ok(true);
}
},
astrid_types::ipc::IpcPayload::LlmStreamEvent {
event: astrid_types::llm::StreamEvent::ToolCallStart { id, name },
..
} => {
formatter.flush_markdown();
formatter.format_tool_start(&id, &name, &serde_json::Value::Null);
},
astrid_types::ipc::IpcPayload::ToolExecuteResult { call_id, result } => {
formatter.flush_markdown();
let res_val = serde_json::to_string(&result.content).unwrap_or_default();
formatter.format_tool_result(&call_id, &res_val, result.is_error);
},
astrid_types::ipc::IpcPayload::ApprovalRequired {
request_id,
action,
resource,
reason,
} => {
formatter.flush_markdown();
auto_deny_approval(client, session_id, &request_id, &action, &resource, &reason)
.await?;
},
astrid_types::ipc::IpcPayload::SelectionRequired {
request_id,
title,
options,
callback_topic,
} => {
formatter.flush_markdown();
auto_skip_selection(
client,
session_id,
&request_id,
&title,
&options,
&callback_topic,
)
.await?;
},
astrid_types::ipc::IpcPayload::ElicitRequest {
request_id,
capsule_id,
field,
} => {
formatter.flush_markdown();
auto_skip_elicit(client, session_id, request_id, &capsule_id, &field).await?;
},
_ => {
},
}
}
}
async fn auto_deny_approval(
client: &mut SocketClient,
session_id: &SessionId,
request_id: &str,
action: &str,
resource: &str,
reason: &str,
) -> anyhow::Result<()> {
println!(
"{}",
Theme::warning(&format!(
"Approval required: {action} on {resource} ({reason})"
))
);
client
.send_message(astrid_types::ipc::IpcMessage::new(
format!("astrid.v1.approval.response.{request_id}"),
astrid_types::ipc::IpcPayload::ApprovalResponse {
request_id: request_id.to_owned(),
decision: "deny".into(),
reason: Some(APPROVAL_UNSUPPORTED_REASON.into()),
},
session_id.0,
))
.await?;
println!(
"{}",
Theme::dimmed(&format!("Auto-denied: {APPROVAL_UNSUPPORTED_REASON}"))
);
Ok(())
}
async fn auto_skip_selection(
client: &mut SocketClient,
session_id: &SessionId,
request_id: &str,
title: &str,
options: &[astrid_types::ipc::SelectionOption],
callback_topic: &str,
) -> anyhow::Result<()> {
println!(
"{}",
Theme::warning(&format!("Selection required: {title}"))
);
for opt in options {
println!(
"{}",
Theme::dimmed(&format!(" - [{}] {}", opt.id, opt.label))
);
}
client
.send_message(astrid_types::ipc::IpcMessage::new(
callback_topic.to_owned(),
astrid_types::ipc::IpcPayload::Custom {
data: serde_json::json!({
"request_id": request_id,
"selected_id": "",
}),
},
session_id.0,
))
.await?;
println!(
"{}",
Theme::dimmed(&format!("Auto-skipped: {SELECTION_UNSUPPORTED_REASON}"))
);
Ok(())
}
async fn auto_skip_elicit(
client: &mut SocketClient,
session_id: &SessionId,
request_id: uuid::Uuid,
capsule_id: &str,
field: &astrid_types::ipc::OnboardingField,
) -> anyhow::Result<()> {
println!(
"{}",
Theme::warning(&format!(
"Input required by capsule '{capsule_id}': {} ({})",
field.prompt, field.key
))
);
client
.send_message(astrid_types::ipc::IpcMessage::new(
format!("astrid.v1.elicit.response.{request_id}"),
astrid_types::ipc::IpcPayload::ElicitResponse {
request_id,
value: None,
values: None,
},
session_id.0,
))
.await?;
println!(
"{}",
Theme::dimmed(&format!("Auto-skipped: {ELICIT_UNSUPPORTED_REASON}"))
);
Ok(())
}
fn handle_slash_command(cmd: &str, _client: &mut SocketClient, _session_id: &SessionId) {
println!("Slash commands temporarily disabled in JSON Mode during microkernel refactor: {cmd}");
}