saya-cli 0.3.2

Database-aware AI agent for the terminal: full-screen TUI, schema discovery, and bounded read-only SQL over PostgreSQL, MySQL, SQLite, DuckDB, and Snowflake.
use super::{
    session_commands::SessionAction, session_resume::block_on, session_state::SessionState,
};
use crate::render::{RenderFormat, TerminalEvent, render_event};
use saya_store::{FsSessionStore, SessionStore};

pub(crate) fn emit_action(
    action: SessionAction,
    format: RenderFormat,
    state: &mut SessionState,
    store: &FsSessionStore,
) -> Result<(), Box<dyn std::error::Error>> {
    match action {
        SessionAction::Message(message) => {
            if message != "Conversation context cleared." {
                state.record("system", &message);
            }
            emit(TerminalEvent::Result { message }, format);
        }
        // Doctor is intercepted in the session loop (it needs `runtime`) and
        // never reaches here; the arm keeps the match exhaustive.
        SessionAction::Doctor => {}
        SessionAction::Agent(_)
        | SessionAction::Schema(_)
        | SessionAction::Sql(_)
        | SessionAction::Contracts(_)
        | SessionAction::Resume(_)
        | SessionAction::Exit => {}
        SessionAction::Cancelled => emit(
            TerminalEvent::Diagnostic {
                message: "Request cancelled.".into(),
            },
            format,
        ),
        SessionAction::NotImplemented(feature) => {
            emit(TerminalEvent::NotImplemented { feature }, format)
        }
        SessionAction::Export(_) => emit(
            TerminalEvent::Error {
                message: "export is only available in the interactive TUI".into(),
            },
            format,
        ),
        SessionAction::Chart(_) => emit(
            TerminalEvent::Error {
                message: "chart is only available in the interactive TUI".into(),
            },
            format,
        ),
        SessionAction::Explain(_) => emit(
            TerminalEvent::Error {
                message: "explain is only available in the interactive TUI".into(),
            },
            format,
        ),
        SessionAction::Error(message) => emit(TerminalEvent::Error { message }, format),
        SessionAction::History => history(format, state, store)?,
    }
    Ok(())
}

fn history(
    format: RenderFormat,
    state: &mut SessionState,
    store: &FsSessionStore,
) -> Result<(), Box<dyn std::error::Error>> {
    let entries = block_on(store.history())?;
    let now_ms = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|value| value.as_millis())
        .unwrap_or(0);
    let message = if entries.is_empty() {
        "No saved sessions.".into()
    } else {
        entries
            .into_iter()
            .map(|entry| {
                let age = super::tui::replay::relative_time(
                    now_ms.saturating_sub(entry.modified_unix_ms),
                );
                format!("{}\t{}", entry.id, age)
            })
            .collect::<Vec<_>>()
            .join("\n")
    };
    emit(
        TerminalEvent::Result {
            message: message.clone(),
        },
        format,
    );
    state.record("system", message);
    Ok(())
}

fn emit(event: TerminalEvent, format: RenderFormat) {
    let rendered = render_event(&event, format);
    print!("{}", rendered.stdout);
    eprint!("{}", rendered.stderr);
}