use super::exec;
use super::transcript::{BlockKind, Transcript};
use super::types::LastQuery;
use crate::config::runtime::RuntimeConfig;
use crate::interactive::session_commands::SessionAction;
use crate::interactive::session_resume::{SessionDefaults, block_on, resume_session};
use crate::interactive::session_state::SessionState;
use crate::render::{RenderFormat, TerminalEvent, render_event};
use crate::slash::parse_slash_command;
use saya_store::{FsSessionStore, SessionStore};
use std::path::Path;
pub(crate) enum Dispatch {
Handled,
Quit,
Agent(String),
OpenSessionPicker,
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn dispatch(
line: &str,
transcript: &mut Transcript,
profiles: &[String],
state: &mut SessionState,
runtime: &RuntimeConfig,
store: &FsSessionStore,
format: RenderFormat,
last_query: &mut Option<LastQuery>,
) -> Dispatch {
if line.trim() == "/sessions" {
return Dispatch::OpenSessionPicker;
}
let mut result = Dispatch::Handled;
match parse_slash_command(line) {
Err(error) => transcript.push(BlockKind::Error, error.to_string()),
Ok(Some(command)) => match state.apply(command, profiles) {
SessionAction::Message(message) => transcript.push(BlockKind::System, message),
SessionAction::Error(message) => transcript.push(BlockKind::Error, message),
SessionAction::History => list_sessions(transcript, store),
SessionAction::Resume(id) => resume(transcript, state, store, &id),
SessionAction::Sql(sql) => {
run_sql(transcript, state, runtime, format, &sql, last_query)
}
SessionAction::Export(path) => {
run_export(transcript, runtime, state, last_query, &path)
}
SessionAction::Chart(args) => run_chart(transcript, runtime, state, last_query, &args),
SessionAction::Explain(arg) => {
run_explain(transcript, runtime, state, last_query, &arg)
}
SessionAction::Schema(_) => transcript.push(
BlockKind::System,
"Schema view is available in headless mode; TUI rendering is coming next.",
),
SessionAction::Agent(_) | SessionAction::Cancelled => {}
SessionAction::NotImplemented(feature) => {
transcript.push(BlockKind::System, format!("Not implemented: {feature}"))
}
SessionAction::Exit => result = Dispatch::Quit,
},
Ok(None) => result = Dispatch::Agent(line.to_string()),
}
transcript.scroll_to_bottom();
result
}
fn list_sessions(transcript: &mut Transcript, store: &FsSessionStore) {
match block_on(store.history()) {
Ok(entries) if entries.is_empty() => {
transcript.push(BlockKind::System, "No saved sessions.")
}
Ok(entries) => {
let body = entries
.into_iter()
.map(|entry| format!("{}\t{}", entry.id, entry.modified_unix_ms))
.collect::<Vec<_>>()
.join("\n");
transcript.push(BlockKind::System, body);
}
Err(error) => transcript.push(BlockKind::Error, error.to_string()),
}
}
fn resume(transcript: &mut Transcript, state: &mut SessionState, store: &FsSessionStore, id: &str) {
let defaults = SessionDefaults {
provider: state.provider.clone(),
model: state.model.clone(),
allow_data_sharing: state.allow_data_sharing,
approval_mode: state.approval_mode.clone(),
};
match resume_session(store, id, &defaults) {
Ok(Some(loaded)) => {
*state = loaded;
transcript.push(BlockKind::System, format!("Resumed session {id}"));
}
Ok(None) => transcript.push(BlockKind::Error, format!("Session not found: {id}")),
Err(error) => transcript.push(BlockKind::Error, error.to_string()),
}
}
fn run_sql(
transcript: &mut Transcript,
state: &SessionState,
runtime: &RuntimeConfig,
format: RenderFormat,
sql: &str,
last_query: &mut Option<LastQuery>,
) {
let event = block_on(exec::run_sql(runtime, state.profile.as_deref(), sql));
match event {
TerminalEvent::QueryResult { result } => {
*last_query = Some(LastQuery {
sql: sql.to_string(),
connection: state.profile.clone(),
});
transcript.push(BlockKind::Tool, super::table::format_table(&result));
}
TerminalEvent::Error { message } => {
transcript.push(BlockKind::Error, message);
}
other => {
let rendered = render_event(&other, format);
transcript.push(BlockKind::System, rendered.stdout.trim_end().to_string());
}
}
}
fn run_export(
transcript: &mut Transcript,
runtime: &RuntimeConfig,
state: &SessionState,
last_query: &Option<LastQuery>,
path: &str,
) {
let Some(lq) = last_query.as_ref() else {
transcript.push(
BlockKind::System,
"Nothing to export yet — run a query first.",
);
return;
};
let target = lq.connection.as_deref().or(state.profile.as_deref());
let event = block_on(exec::run_sql(runtime, target, &lq.sql));
match event {
TerminalEvent::QueryResult { result } => {
match super::export::write_result(&result, Path::new(path)) {
Ok(n) => {
let mut msg = format!("Exported {n} row(s) to {path}");
if result.truncated {
msg.push_str(" (result was truncated)");
}
transcript.push(BlockKind::System, msg);
}
Err(msg) => transcript.push(BlockKind::Error, msg),
}
}
TerminalEvent::Error { message } => {
transcript.push(BlockKind::Error, message);
}
_ => {}
}
}
fn run_chart(
transcript: &mut Transcript,
runtime: &RuntimeConfig,
state: &SessionState,
last_query: &Option<LastQuery>,
args: &str,
) {
let Some(lq) = last_query.as_ref() else {
transcript.push(
BlockKind::System,
"Nothing to chart yet — run a query first.",
);
return;
};
let mut tokens = args.split_whitespace();
let (kind, path_arg) = match tokens.next() {
Some(tok) => match crate::chart::ChartKind::parse(tok) {
Some(k) => (Some(k), tokens.next()),
None => (None, Some(tok)),
},
None => (None, None),
};
let target = lq.connection.as_deref().or(state.profile.as_deref());
let result = match block_on(exec::run_sql(runtime, target, &lq.sql)) {
TerminalEvent::QueryResult { result } => result,
TerminalEvent::Error { message } => {
transcript.push(BlockKind::Error, message);
return;
}
_ => return,
};
let mut spec = crate::chart::suggest_spec(&result);
if let Some(k) = kind {
spec.kind = k;
}
let html = match crate::chart::render_html(&result, &spec) {
Ok(html) => html,
Err(msg) => {
transcript.push(BlockKind::System, msg);
return;
}
};
let path = match path_arg {
Some(p) => std::path::PathBuf::from(p),
None => std::env::temp_dir().join("saya-chart.html"),
};
if let Err(msg) = crate::chart::write_html(&html, &path) {
transcript.push(BlockKind::Error, msg);
return;
}
let mut note = format!("Chart written to {}", path.display());
match crate::chart::open_file(&path) {
Ok(()) => note.push_str(" (opening in your browser)"),
Err(e) => note.push_str(&format!(" — open it manually ({e})")),
}
transcript.push(BlockKind::System, note);
}
fn run_explain(
transcript: &mut Transcript,
runtime: &RuntimeConfig,
state: &SessionState,
last_query: &Option<LastQuery>,
arg: &str,
) {
let (sql, connection) = if !arg.trim().is_empty() {
(arg.trim().to_string(), None)
} else if let Some(lq) = last_query.as_ref() {
(lq.sql.clone(), lq.connection.clone())
} else {
transcript.push(
BlockKind::System,
"Nothing to explain — run a query first, or pass SQL: /explain SELECT ...",
);
return;
};
let trimmed = sql.trim().trim_end_matches(';').trim();
let explain_sql = format!("EXPLAIN {trimmed}");
let target = connection.as_deref().or(state.profile.as_deref());
match block_on(exec::run_sql(runtime, target, &explain_sql)) {
TerminalEvent::QueryResult { result } => {
transcript.push(BlockKind::Tool, super::table::format_plan(&result));
}
TerminalEvent::Error { message } => transcript.push(BlockKind::Error, message),
_ => {}
}
}