use crate::config::MergedConfig;
use crate::git_store::GitStore;
use crate::manifest::Manifest;
use crate::observability::CliOutput;
use crate::runtime::{ActivityMonitor, InstanceLock, UiEvent};
use crate::session::AgentState;
use crate::tui::app::App;
use crate::tui::banner;
use anyhow::Result;
use crossterm::{
event::EnableMouseCapture,
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io;
use std::path::Path;
use std::sync::{Arc, Mutex};
pub fn run(
store_root: &Path,
agent_name: Option<String>,
ascii: bool,
output: &dyn CliOutput,
) -> Result<()> {
let store_root = store_root
.canonicalize()
.unwrap_or_else(|_| store_root.to_path_buf());
let config = MergedConfig::load(&store_root)?;
let manifest = Manifest::load(&store_root)?;
let manifest = Arc::new(Mutex::new(manifest));
let ascii = ascii || config.ui.ascii_only;
let changelog_limit = config.ui.changelog_limit;
{
let m = manifest.lock().unwrap();
banner::print_banner(&store_root, &config, &m, ascii, output)?;
}
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(
std::io::stderr(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::cursor::Show,
);
original_hook(panic_info);
}));
let git = GitStore::open(&store_root)?;
let initial_log = git.log(changelog_limit).unwrap_or_default();
let history = load_command_history(&store_root);
let _instance_lock = match InstanceLock::acquire(&store_root) {
Ok(lock) => lock,
Err(e) => {
tracing::warn!("TUI instance lock unavailable: {e}");
output.warn("Warning: Another agent-trace TUI is running.")?;
output.warn("Opening in read-only mode.")?;
return run_readonly(&store_root, manifest, agent_name, ascii, changelog_limit);
}
};
let (ui_tx, ui_rx) = tokio::sync::mpsc::channel::<UiEvent>(64);
let agent_state = AgentState::new(agent_name.clone());
let _monitor = ActivityMonitor::try_start(
&store_root,
config.clone(),
manifest.clone(),
agent_state,
Some(ui_tx),
)?;
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(store_root.clone(), manifest, initial_log, history, ui_rx);
let result = app.run(&mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
crossterm::event::DisableMouseCapture,
)?;
terminal.show_cursor()?;
save_command_history(&store_root, &app.chat.history);
result
}
fn run_readonly(
store_root: &Path,
manifest: Arc<Mutex<Manifest>>,
_agent_name: Option<String>,
ascii: bool,
changelog_limit: usize,
) -> Result<()> {
let config = MergedConfig::load(store_root)?;
let git = GitStore::open(store_root)?;
let initial_log = git.log(changelog_limit).unwrap_or_default();
let history = load_command_history(store_root);
let (_tx, rx) = tokio::sync::mpsc::channel::<UiEvent>(1);
{
let m = manifest.lock().unwrap();
let output = crate::observability::NoopOutput;
banner::print_banner(store_root, &config, &m, ascii, &output)?;
}
let original_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |panic_info| {
let _ = crossterm::terminal::disable_raw_mode();
let _ = crossterm::execute!(
std::io::stderr(),
crossterm::terminal::LeaveAlternateScreen,
crossterm::cursor::Show,
);
original_hook(panic_info);
}));
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut app = App::new(store_root.to_path_buf(), manifest, initial_log, history, rx);
let result = app.run(&mut terminal);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
crossterm::event::DisableMouseCapture,
)?;
terminal.show_cursor()?;
result
}
fn load_command_history(store_root: &Path) -> Vec<String> {
let path = store_root.join(".agent-trace").join("command_history.txt");
std::fs::read_to_string(&path)
.unwrap_or_default()
.lines()
.filter(|l| !l.trim().is_empty())
.map(String::from)
.collect()
}
fn save_command_history(store_root: &Path, history: &[String]) {
let path = store_root.join(".agent-trace").join("command_history.txt");
let _ = std::fs::write(path, history.join("\n") + "\n");
}