mod agent;
mod application;
mod atref;
mod clipboard;
mod complete;
mod dispatch;
mod exec;
mod export;
mod fuzzy;
mod history;
mod input;
mod keys;
mod replay;
mod stream_events;
mod table;
mod terminal;
mod transcript;
mod types;
mod ui;
use super::session_resume::block_on;
use super::session_state::SessionState;
use crate::config::runtime::RuntimeConfig;
use crate::render::RenderFormat;
use clipboard::{ClipboardOutcome, clipboard_outcome, copy_to_native_clipboard, osc52_copy};
use dispatch::Dispatch;
use keys::handle_key;
use ratatui::crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyEventKind, MouseEventKind},
execute,
};
use saya_store::{FsSessionStore, SessionStore, SqliteStateStore};
use std::sync::Arc;
use std::time::Duration;
use terminal::TerminalGuard;
use transcript::BlockKind;
use types::{App, ClipboardCopy, SessionSave};
fn start_session_save(app: &mut App, store: &FsSessionStore, session: saya_store::RedactedSession) {
let store = store.clone();
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let result = block_on(store.save(session)).map_err(|error| error.to_string());
let _ = sender.send(result);
});
app.session_save = Some(SessionSave { result: receiver });
}
fn queue_session_save(app: &mut App, store: &FsSessionStore, state: &SessionState) {
let session = state.redacted();
if app.session_save.is_some() {
app.pending_session_save = Some(session);
} else {
start_session_save(app, store, session);
}
}
fn poll_session_save(app: &mut App, store: &FsSessionStore) {
let result = app
.session_save
.as_ref()
.and_then(|save| match save.result.try_recv() {
Ok(result) => Some(result),
Err(std::sync::mpsc::TryRecvError::Disconnected) => {
Some(Err("session save worker stopped unexpectedly".into()))
}
Err(std::sync::mpsc::TryRecvError::Empty) => None,
});
let Some(result) = result else { return };
app.session_save = None;
if let Err(error) = result {
app.transcript.push(
BlockKind::Error,
format!("Could not save this session; your latest changes may be lost: {error}"),
);
}
if let Some(session) = app.pending_session_save.take() {
start_session_save(app, store, session);
}
}
pub(crate) fn run(
runtime: &RuntimeConfig,
store: &FsSessionStore,
state_db: &SqliteStateStore,
format: RenderFormat,
state: &mut SessionState,
) -> Result<i32, Box<dyn std::error::Error>> {
let mut guard = TerminalGuard::new()?;
let profiles = runtime
.connections
.profiles
.keys()
.cloned()
.collect::<Vec<String>>();
let mut app = App::new(profiles, Arc::new(runtime.clone()), state_db.clone());
app.reload_at_refs(state);
app.show_history(state);
let mut mouse_captured = true;
let mut mouse_capture_error_reported = false;
while !app.should_quit {
app.poll_session_picker();
poll_session_save(&mut app, store);
let status = super::session_prompt::status_segments(state);
guard
.terminal
.draw(|frame| ui::draw(frame, &app, &status))?;
if event::poll(Duration::from_millis(60))? {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
handle_key(&mut app, key.code, key.modifiers)
}
Event::Paste(text) => app.paste(&text),
Event::Mouse(mouse) => match mouse.kind {
MouseEventKind::ScrollUp => app.scroll_lines(true, 3),
MouseEventKind::ScrollDown => app.scroll_lines(false, 3),
_ => {}
},
_ => {}
}
}
let want_capture = !app.overlays.selection_mode;
if want_capture != mouse_captured {
let backend = guard.terminal.backend_mut();
let result = if want_capture {
execute!(backend, EnableMouseCapture)
} else {
execute!(backend, DisableMouseCapture)
};
match result {
Ok(()) => {
mouse_captured = want_capture;
mouse_capture_error_reported = false;
}
Err(error) if !mouse_capture_error_reported => {
app.transcript.push(
BlockKind::Error,
format!("Could not update mouse capture: {error}"),
);
mouse_capture_error_reported = true;
}
Err(_) => {}
}
}
if app.clipboard_copy.is_none()
&& let Some(text) = app.pending_clipboard.take()
{
let (sender, receiver) = std::sync::mpsc::channel();
let native_text = text.clone();
std::thread::spawn(move || {
let _ = sender.send(copy_to_native_clipboard(&native_text));
});
app.clipboard_copy = Some(ClipboardCopy {
native_result: receiver,
osc_error: osc52_copy(guard.terminal.backend_mut(), &text)
.err()
.map(|error| error.to_string()),
});
}
let native_result =
app.clipboard_copy
.as_ref()
.and_then(|copy| match copy.native_result.try_recv() {
Ok(result) => Some(result),
Err(std::sync::mpsc::TryRecvError::Disconnected) => Some(false),
Err(std::sync::mpsc::TryRecvError::Empty) => None,
});
if let Some(native_ok) = native_result
&& let Some(copy) = app.clipboard_copy.take()
{
match clipboard_outcome(native_ok, copy.osc_error.as_deref()) {
ClipboardOutcome::Native => app
.transcript
.push(BlockKind::System, "Copied to the system clipboard."),
ClipboardOutcome::Osc52 => app.transcript.push(
BlockKind::System,
"Sent OSC 52 clipboard data; it will copy if your terminal supports it.",
),
ClipboardOutcome::Failed => app.transcript.push(
BlockKind::Error,
format!(
"Could not copy to the system clipboard, and sending OSC 52 failed: {}",
copy.osc_error
.unwrap_or_else(|| "unknown terminal output error".into())
),
),
}
}
if app.is_busy() {
app.spinner = app.spinner.wrapping_add(1);
if app.drain_stream(state) {
queue_session_save(&mut app, store, state);
}
}
if let Some(line) = app.pending.take() {
match dispatch::dispatch(
&line,
&mut app.transcript,
&app.profiles,
state,
runtime,
store,
format,
&mut app.last_query,
) {
Dispatch::Quit => app.should_quit = true,
Dispatch::Handled => app.reload_at_refs(state),
Dispatch::Agent(prompt) => app.start_agent(prompt, state),
Dispatch::OpenSessionPicker => app.open_session_picker(store),
}
queue_session_save(&mut app, store, state);
}
if let Some(id) = app.overlays.pending_resume.take() {
let defaults = super::session_resume::SessionDefaults {
provider: state.provider.clone(),
model: state.model.clone(),
allow_data_sharing: state.allow_data_sharing,
approval_mode: state.approval_mode.clone(),
};
match super::session_resume::resume_session(store, &id, &defaults) {
Ok(Some(loaded)) => {
*state = loaded;
app.reload_at_refs(state);
if state.turns.is_empty() {
app.transcript.clear();
app.transcript.push(
BlockKind::System,
format!("Resumed session {id} (no earlier turns)."),
);
} else {
app.show_history(state);
}
}
Ok(None) => app
.transcript
.push(BlockKind::Error, format!("Session not found: {id}")),
Err(error) => app.transcript.push(BlockKind::Error, error.to_string()),
}
queue_session_save(&mut app, store, state);
}
}
Ok(0)
}