agent_trace/commands/
open.rs1use 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 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 {
39 let m = manifest.lock().unwrap();
40 banner::print_banner(&store_root, &config, &m, ascii, output)?;
41 }
42
43 let original_hook = std::panic::take_hook();
45 std::panic::set_hook(Box::new(move |panic_info| {
46 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 let git = GitStore::open(&store_root)?;
58 let initial_log = git.log(changelog_limit).unwrap_or_default();
59
60 let history = load_command_history(&store_root);
62
63 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 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 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 disable_raw_mode()?;
101 execute!(
102 terminal.backend_mut(),
103 LeaveAlternateScreen,
104 crossterm::event::DisableMouseCapture,
105 )?;
106 terminal.show_cursor()?;
107
108 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 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}