use std::sync::Arc;
use axum::{Json, extract::State};
use crate::coordinator::{
CoordinatorContext, build_coordinator_context, coordinator_system_prompt, parse_session_prefix,
};
use crate::error::DaemonError;
use crate::llm_overseer::ChatMessage;
use crate::services::{SessionService, TmuxService};
use crate::state::DaemonState;
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
pub struct CoordinatorChatRequest {
pub message: String,
#[serde(default)]
#[schema(value_type = Vec<Object>)]
pub history: Vec<ChatMessage>,
}
#[derive(Debug, serde::Serialize, utoipa::ToSchema)]
pub struct CoordinatorChatResponse {
pub reply: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub routed_to_session: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub command_output: Option<String>,
}
#[utoipa::path(
get,
path = "/api/v1/coordinator/context",
tag = "config",
responses((status = 200, description = "Cross-session activity snapshot"))
)]
pub async fn coordinator_context(
State(state): State<Arc<DaemonState>>,
) -> Json<CoordinatorContext> {
Json(build_coordinator_context(&state))
}
#[utoipa::path(
post,
path = "/api/v1/coordinator/chat",
tag = "config",
request_body = CoordinatorChatRequest,
responses(
(status = 200, description = "Coordinator reply, or routed command output"),
(status = 503, description = "LLM chat is not configured (non-prefixed message)"),
)
)]
pub async fn coordinator_chat(
State(state): State<Arc<DaemonState>>,
Json(body): Json<CoordinatorChatRequest>,
) -> Result<Json<CoordinatorChatResponse>, DaemonError> {
let context = build_coordinator_context(&state);
if let Some((session_name, command)) = parse_session_prefix(&body.message, &context.sessions) {
let session = SessionService::new(&state).command_target(&session_name)?;
TmuxService::send_command(&session, &command);
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
let output = TmuxService::capture(&session, 100);
return Ok(Json(CoordinatorChatResponse {
reply: format!("Sent to {session_name}: {command}"),
routed_to_session: Some(session_name),
command_output: Some(output),
}));
}
let overseer = state.llm_overseer().ok_or_else(|| {
DaemonError::ServiceUnavailable(
"LLM chat is not configured (no OpenRouter API key)".to_string(),
)
})?;
let mut history = vec![ChatMessage::user(coordinator_system_prompt(&context))];
history.push(ChatMessage::assistant(
"Understood — I have the current session context.".to_string(),
));
history.extend(body.history);
let reply = overseer
.chat(&mut history, &body.message)
.await
.map_err(|e| DaemonError::Internal(e.to_string()))?;
Ok(Json(CoordinatorChatResponse {
reply,
routed_to_session: None,
command_output: None,
}))
}