use serde::Deserialize;
use serde_json::{json, Value};
use uuid::Uuid;
use crate::service::load_user_config;
use crate::transport::api_error::{open_handle, ApiError};
use crate::{ActivitySource, AppState, DaemonEvent};
use super::{CallerParams, NoParams};
pub async fn chat_providers(state: &AppState, _params: NoParams) -> Result<Value, ApiError> {
let cfg = load_user_config().unwrap_or_default();
let ollama_available = if cfg.local_model.enabled {
trusty_common::auto_detect_local_provider(&cfg.local_model.base_url)
.await
.is_some()
} else {
false
};
let openrouter_available = !cfg.openrouter_api_key.is_empty();
let active = state.chat_provider().await.map(|p| p.name().to_string());
Ok(json!({
"providers": [
{
"name": "ollama",
"model": cfg.local_model.model,
"available": ollama_available,
},
{
"name": "openrouter",
"model": cfg.openrouter_model,
"available": openrouter_available,
}
],
"active": active,
}))
}
#[derive(Debug, Deserialize)]
pub struct ListMessagesParams {
pub palace: String,
#[serde(default)]
pub unread_only: Option<bool>,
}
pub async fn messages_list(
state: &AppState,
params: ListMessagesParams,
) -> Result<Value, ApiError> {
let handle = open_handle(state, ¶ms.palace)?;
let unread_only = params.unread_only.unwrap_or(false);
let payload: Vec<Value> = crate::messaging::list_messages(&handle, unread_only)
.into_iter()
.map(|m| {
let formatted = m.to_injection_block();
json!({
"id": m.id.to_string(),
"from_palace": m.from_palace,
"to_palace": m.to_palace,
"purpose": m.purpose,
"sent_at": m.sent_at.to_rfc3339(),
"read": m.read,
"content": m.content,
"formatted": formatted,
})
})
.collect();
Ok(Value::Array(payload))
}
#[derive(Debug, Deserialize)]
pub struct SendMessageParams {
pub to_palace: String,
pub purpose: String,
pub content: String,
#[serde(default)]
pub from_palace: Option<String>,
#[serde(flatten)]
pub caller: CallerParams,
}
pub async fn message_send(state: &AppState, params: SendMessageParams) -> Result<Value, ApiError> {
let from_palace = params
.from_palace
.or_else(|| state.default_palace.clone())
.unwrap_or_else(|| "<unknown>".to_string());
let drawer_id = crate::messaging::send_message_to_palace(
&state.registry,
&state.data_root,
&from_palace,
¶ms.to_palace,
¶ms.purpose,
params.content,
params.caller.creator(),
)
.await
.map_err(|e| ApiError::internal(format!("send_message: {e:#}")))?;
let drawer_count = open_handle(state, ¶ms.to_palace)
.map(|h| h.drawers.read().len())
.unwrap_or(0);
state.emit(DaemonEvent::DrawerAdded {
palace_id: params.to_palace.clone(),
palace_name: params.to_palace.clone(),
drawer_count,
timestamp: chrono::Utc::now(),
content_preview: format!("[msg from {from_palace}] {}", params.purpose),
source: ActivitySource::Http,
});
Ok(json!({
"drawer_id": drawer_id.to_string(),
"from_palace": from_palace,
"to_palace": params.to_palace,
"purpose": params.purpose,
"status": "sent",
}))
}
#[derive(Debug, Deserialize)]
pub struct MarkReadParams {
pub palace: String,
pub drawer_id: String,
}
pub async fn message_mark_read(
state: &AppState,
params: MarkReadParams,
) -> Result<Value, ApiError> {
let uuid = Uuid::parse_str(¶ms.drawer_id)
.map_err(|_| ApiError::bad_request("drawer_id must be a UUID"))?;
let handle = open_handle(state, ¶ms.palace)?;
let flipped = crate::messaging::mark_message_read(&handle, uuid)
.await
.map_err(|e| ApiError::internal(format!("mark_read: {e:#}")))?;
Ok(json!({ "flipped": flipped }))
}