pub mod client;
pub mod dashboard;
mod event_loop;
pub mod health;
pub mod iterm2;
use crossterm::{
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{
Frame, Terminal,
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout},
style::{Modifier, Style},
text::Line,
widgets::Paragraph,
};
use client::DaemonClient;
use dashboard::{ChatMessage, DashboardState};
use event_loop::run_loop;
use health::{Daemon, HealthScreen, HealthUpdate};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Screen {
#[default]
Chat,
Health,
}
pub const HEALTH_KEY_HINT: &str =
"[1]health [2]logs [3]search [Tab]svc [↑↓]nav [r]refresh [S]start [X]stop [c]chat [q]quit";
pub async fn run(url: String, interval_ms: u64) -> anyhow::Result<()> {
run_focused(url, interval_ms, None).await
}
fn rediscover_daemon(client: &mut DaemonClient, reachable: bool) -> bool {
if reachable {
return false;
}
let resolved = crate::core::resolve_daemon_url(None);
if resolved != client.base_url() {
client.set_base_url(resolved);
true
} else {
false
}
}
pub async fn run_focused(
url: String,
interval_ms: u64,
focus_id: Option<String>,
) -> anyhow::Result<()> {
let mut client = DaemonClient::new(url);
enable_raw_mode()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = run_loop(&mut terminal, &mut client, interval_ms, focus_id).await;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
pub(crate) async fn poll_daemon(state: &mut DashboardState, client: &mut DaemonClient) {
state.daemon_reachable = client.is_healthy().await;
if rediscover_daemon(client, state.daemon_reachable) {
state.daemon_reachable = client.is_healthy().await;
}
if state.daemon_reachable {
match client.coordinator_context().await {
Ok(context) => {
state.sessions = context
.sessions
.into_iter()
.map(coordinator_session_to_row)
.collect();
}
Err(_) => state.daemon_reachable = false,
}
} else {
state.sessions.clear();
}
state.clamp_selection();
}
fn coordinator_session_to_row(s: crate::client::CoordinatorSession) -> client::SessionRow {
use crate::core::session::{SessionId, SessionStatus};
let id = uuid::Uuid::parse_str(&s.id)
.map(SessionId)
.unwrap_or_else(|_| SessionId(uuid::Uuid::nil()));
let status = match s.status.as_str() {
"Starting" => SessionStatus::Starting,
"Active" => SessionStatus::Active,
"AwaitingApproval" => SessionStatus::AwaitingApproval,
"Detached" => SessionStatus::Detached,
"Paused" => SessionStatus::Paused,
_ => SessionStatus::Stopped,
};
client::SessionRow {
id,
workdir: s.workdir,
status,
active_delegations: s.active_delegations,
tmux_name: s.name,
last_seen: Default::default(),
}
}
pub(crate) fn spawn_health_pollers(
search_url: String,
memory_url: String,
tx: tokio::sync::mpsc::Sender<HealthUpdate>,
) {
for (daemon, url) in [(Daemon::Search, search_url), (Daemon::Memory, memory_url)] {
let tx = tx.clone();
tokio::spawn(async move {
let client = health::client_for(daemon, &url);
loop {
let state = client.poll().await;
if tx.send(HealthUpdate { daemon, state }).await.is_err() {
break; }
tokio::time::sleep(health::POLL_INTERVAL).await;
}
});
}
}
pub(crate) async fn coordinator_send(
state: &mut DashboardState,
client: &DaemonClient,
message: &str,
) {
state.push_chat(ChatMessage::user(message));
match client.coordinator_chat(message, &state.coord_history).await {
Ok(Some(outcome)) => {
let reply = match outcome.command_output {
Some(output) => format!("{}\n{output}", outcome.reply),
None => outcome.reply.clone(),
};
state.push_chat(ChatMessage::coordinator(reply));
if outcome.routed_to_session.is_some() {
state.coord_history.clear();
} else {
state
.coord_history
.push(crate::client::ChatMessage::user(message.to_string()));
state
.coord_history
.push(crate::client::ChatMessage::assistant(outcome.reply));
}
state.last_action = Some("message sent".to_string());
}
Ok(None) => {
state.push_chat(ChatMessage::coordinator(
"coordinator chat is not configured — set OPENROUTER_API_KEY and enable the overseer",
));
state.last_action = Some("LLM not configured".to_string());
}
Err(e) => {
state.push_chat(ChatMessage::coordinator(format!("daemon error: {e}")));
state.last_action = Some("daemon error".to_string());
}
}
}
pub(crate) fn render_screen(
frame: &mut Frame,
screen: Screen,
chat: &DashboardState,
hp: &HealthScreen,
) {
match screen {
Screen::Chat => dashboard::render(frame, chat),
Screen::Health => {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(6), Constraint::Length(1)])
.split(frame.area());
health::render(frame, hp);
frame.render_widget(
Paragraph::new(Line::from(HEALTH_KEY_HINT)).style(
Style::default()
.add_modifier(Modifier::BOLD)
.add_modifier(Modifier::REVERSED),
),
chunks[1],
);
}
}
}
#[cfg(test)]
mod tests;