Skip to main content

agent_trace/commands/
open.rs

1use crate::config::MergedConfig;
2use crate::git_store::GitStore;
3use crate::manifest::Manifest;
4use crate::observability::CliOutput;
5use crate::runtime::{ActivityMonitor, InstanceLock, UiEvent};
6use crate::session::AgentState;
7use crate::tui::app::App;
8use crate::tui::banner;
9use anyhow::Result;
10use crossterm::{
11    event::EnableMouseCapture,
12    execute,
13    terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
14};
15use ratatui::{backend::CrosstermBackend, Terminal};
16use std::io;
17use std::path::Path;
18use std::sync::{Arc, Mutex};
19
20pub fn run(
21    store_root: &Path,
22    agent_name: Option<String>,
23    ascii: bool,
24    output: &dyn CliOutput,
25) -> Result<()> {
26    let store_root = store_root
27        .canonicalize()
28        .unwrap_or_else(|_| store_root.to_path_buf());
29
30    // Load config and manifest.
31    let config = MergedConfig::load(&store_root)?;
32    let manifest = Manifest::load(&store_root)?;
33    let manifest = Arc::new(Mutex::new(manifest));
34    let ascii = ascii || config.ui.ascii_only;
35    let changelog_limit = config.ui.changelog_limit;
36
37    // Print startup banner before entering raw mode.
38    {
39        let m = manifest.lock().unwrap();
40        banner::print_banner(&store_root, &config, &m, ascii, output)?;
41    }
42
43    // Install panic hook to restore terminal on panic.
44    let original_hook = std::panic::take_hook();
45    std::panic::set_hook(Box::new(move |panic_info| {
46        // Restore terminal — best effort, ignore errors.
47        let _ = crossterm::terminal::disable_raw_mode();
48        let _ = crossterm::execute!(
49            std::io::stderr(),
50            crossterm::terminal::LeaveAlternateScreen,
51            crossterm::cursor::Show,
52        );
53        original_hook(panic_info);
54    }));
55
56    // Load initial git log for changelog panel.
57    let git = GitStore::open(&store_root)?;
58    let initial_log = git.log(changelog_limit).unwrap_or_default();
59
60    // Load command history.
61    let history = load_command_history(&store_root);
62
63    // The TUI is the single interactive owner of the store. A second TUI must
64    // open read-only; the poll loop itself is elected separately (PollLock) so
65    // an MCP server can keep monitoring while a read-only TUI observes.
66    let _instance_lock = match InstanceLock::acquire(&store_root) {
67        Ok(lock) => lock,
68        Err(e) => {
69            tracing::warn!("TUI instance lock unavailable: {e}");
70            output.warn("Warning: Another agent-trace TUI is running.")?;
71            output.warn("Opening in read-only mode.")?;
72            return run_readonly(&store_root, manifest, agent_name, ascii, changelog_limit);
73        }
74    };
75
76    // Create UI channel and start the shared activity monitor. When another
77    // process already leads the poll loop this monitor observes HEAD-only.
78    let (ui_tx, ui_rx) = tokio::sync::mpsc::channel::<UiEvent>(64);
79    let agent_state = AgentState::new(agent_name.clone());
80    let _monitor = ActivityMonitor::try_start(
81        &store_root,
82        config.clone(),
83        manifest.clone(),
84        agent_state,
85        Some(ui_tx),
86    )?;
87
88    // Enter TUI.
89    enable_raw_mode()?;
90    let mut stdout = io::stdout();
91    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
92    let backend = CrosstermBackend::new(stdout);
93    let mut terminal = Terminal::new(backend)?;
94
95    let mut app = App::new(store_root.clone(), manifest, initial_log, history, ui_rx);
96
97    let result = app.run(&mut terminal);
98
99    // Restore terminal on exit.
100    disable_raw_mode()?;
101    execute!(
102        terminal.backend_mut(),
103        LeaveAlternateScreen,
104        crossterm::event::DisableMouseCapture,
105    )?;
106    terminal.show_cursor()?;
107
108    // Persist command history.
109    save_command_history(&store_root, &app.chat.history);
110
111    result
112}
113
114fn run_readonly(
115    store_root: &Path,
116    manifest: Arc<Mutex<Manifest>>,
117    _agent_name: Option<String>,
118    ascii: bool,
119    changelog_limit: usize,
120) -> Result<()> {
121    let config = MergedConfig::load(store_root)?;
122    let git = GitStore::open(store_root)?;
123    let initial_log = git.log(changelog_limit).unwrap_or_default();
124    let history = load_command_history(store_root);
125    let (_tx, rx) = tokio::sync::mpsc::channel::<UiEvent>(1);
126
127    {
128        let m = manifest.lock().unwrap();
129        let output = crate::observability::NoopOutput;
130        banner::print_banner(store_root, &config, &m, ascii, &output)?;
131    }
132
133    // Install panic hook to restore terminal on panic.
134    let original_hook = std::panic::take_hook();
135    std::panic::set_hook(Box::new(move |panic_info| {
136        let _ = crossterm::terminal::disable_raw_mode();
137        let _ = crossterm::execute!(
138            std::io::stderr(),
139            crossterm::terminal::LeaveAlternateScreen,
140            crossterm::cursor::Show,
141        );
142        original_hook(panic_info);
143    }));
144
145    enable_raw_mode()?;
146    let mut stdout = io::stdout();
147    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
148    let backend = CrosstermBackend::new(stdout);
149    let mut terminal = Terminal::new(backend)?;
150
151    let mut app = App::new(store_root.to_path_buf(), manifest, initial_log, history, rx);
152    let result = app.run(&mut terminal);
153
154    disable_raw_mode()?;
155    execute!(
156        terminal.backend_mut(),
157        LeaveAlternateScreen,
158        crossterm::event::DisableMouseCapture,
159    )?;
160    terminal.show_cursor()?;
161
162    result
163}
164
165fn load_command_history(store_root: &Path) -> Vec<String> {
166    let path = store_root.join(".agent-trace").join("command_history.txt");
167    std::fs::read_to_string(&path)
168        .unwrap_or_default()
169        .lines()
170        .filter(|l| !l.trim().is_empty())
171        .map(String::from)
172        .collect()
173}
174
175fn save_command_history(store_root: &Path, history: &[String]) {
176    let path = store_root.join(".agent-trace").join("command_history.txt");
177    let _ = std::fs::write(path, history.join("\n") + "\n");
178}