codetether_agent/tui/app/
run.rs1use std::sync::Arc;
2
3use crossterm::{
4 event::{EnableBracketedPaste, EnableMouseCapture},
5 execute,
6 terminal::{EnterAlternateScreen, enable_raw_mode},
7};
8use ratatui::{Terminal, backend::CrosstermBackend};
9use tokio::sync::mpsc;
10
11use crate::bus::{AgentBus, s3_sink::spawn_bus_s3_sink};
12use crate::provider::ProviderRegistry;
13use crate::session::{Session, SessionEvent};
14use crate::tui::app::event_loop::run_event_loop;
15use crate::tui::app::message_text::sync_messages_from_session;
16use crate::tui::app::panic_cleanup::install_panic_cleanup_hook;
17use crate::tui::app::state::App;
18use crate::tui::app::terminal_state::{TerminalGuard, restore_terminal_state};
19use crate::tui::ui::main::ui;
20use crate::tui::worker_bridge::TuiWorkerBridge;
21
22enum SessionLoadOutcome {
27 Loaded {
28 msg_count: usize,
29 title: Option<String>,
30 },
31 NewFallback {
32 reason: String,
33 },
34}
35
36async fn init_tui_secrets_manager() {
37 if crate::secrets::secrets_manager().is_some() {
38 return;
39 }
40
41 match crate::secrets::SecretsManager::from_env().await {
42 Ok(secrets_manager) => {
43 if secrets_manager.is_connected() {
44 tracing::info!("Connected to HashiCorp Vault for secrets management");
45 }
46 if let Err(err) = crate::secrets::init_from_manager(secrets_manager) {
47 tracing::debug!(error = %err, "Secrets manager already initialized");
48 }
49 }
50 Err(err) => {
51 tracing::warn!(error = %err, "Vault not configured for TUI startup");
52 tracing::warn!("Set VAULT_ADDR and VAULT_TOKEN environment variables to connect");
53 }
54 }
55}
56
57pub async fn run(project: Option<std::path::PathBuf>, allow_network: bool) -> anyhow::Result<()> {
58 if allow_network {
59 unsafe {
60 std::env::set_var("CODETETHER_SANDBOX_BASH_ALLOW_NETWORK", "1");
61 }
62 }
63
64 if let Some(project) = project {
65 std::env::set_current_dir(&project)?;
66 }
67
68 restore_terminal_state();
69 enable_raw_mode()?;
70 let _guard = TerminalGuard;
71 let _panic_guard = install_panic_cleanup_hook();
72
73 let mut stdout = std::io::stdout();
74 execute!(
75 stdout,
76 EnterAlternateScreen,
77 EnableMouseCapture,
78 EnableBracketedPaste
79 )?;
80
81 let backend = CrosstermBackend::new(stdout);
82 let mut terminal = Terminal::new(backend)?;
83 terminal.clear()?;
84
85 let cwd = std::env::current_dir().unwrap_or_default();
86 let bus = AgentBus::new().into_arc();
87 crate::bus::set_global(bus.clone());
88 spawn_bus_s3_sink(bus.clone());
89 let mut session = Session::new().await?.with_bus(bus.clone());
90 let mut app = App::default();
91 app.state.cwd_display = cwd.display().to_string();
92 app.state.allow_network = allow_network;
93 app.state.session_id = Some(session.id.clone());
94 app.state.status = "Loading providers and workspace…".to_string();
95 terminal.draw(|f| ui(f, &mut app, &session))?;
96
97 init_tui_secrets_manager().await;
98
99 let registry = ProviderRegistry::from_vault().await.ok().map(Arc::new);
100 let mut bus_handle = bus.handle("tui");
101 let worker_bridge = TuiWorkerBridge::spawn(None, None, None, Arc::clone(&bus))
102 .await
103 .ok()
104 .flatten();
105
106 let (loaded_session, session_load_outcome) = match Session::last_for_directory(Some(&cwd)).await
107 {
108 Ok(existing) => {
109 let msg_count = existing.messages.len();
110 let title = existing.title.clone();
111 (
112 existing.with_bus(bus.clone()),
113 SessionLoadOutcome::Loaded { msg_count, title },
114 )
115 }
116 Err(err) => (
117 Session::new().await?.with_bus(bus.clone()),
118 SessionLoadOutcome::NewFallback {
119 reason: err.to_string(),
120 },
121 ),
122 };
123 session = loaded_session;
124
125 if let Ok(cfg) = crate::config::Config::load().await {
128 session.apply_config(&cfg, None);
129 }
130
131 let (event_tx, event_rx) = mpsc::channel::<SessionEvent>(256);
132 let (result_tx, result_rx) = mpsc::channel::<anyhow::Result<Session>>(8);
133
134 app.state.workspace = crate::tui::models::WorkspaceSnapshot::capture(&cwd, 18);
135 app.state.auto_apply_edits = session.metadata.auto_apply_edits;
136 app.state.allow_network = session.metadata.allow_network || allow_network;
137 app.state.slash_autocomplete = session.metadata.slash_autocomplete;
138 app.state.use_worktree = session.metadata.use_worktree;
139 app.state.session_id = Some(session.id.clone());
140 session.metadata.allow_network = app.state.allow_network;
141 sync_messages_from_session(&mut app, &session);
142 if let Some(bridge) = worker_bridge.as_ref() {
143 app.state
144 .set_worker_bridge(bridge.worker_id.clone(), bridge.worker_name.clone());
145 app.state.register_worker_agent("tui".to_string());
146 }
147 app.state.refresh_slash_suggestions();
148 app.state.move_cursor_end();
149 app.state.status = match &session_load_outcome {
150 SessionLoadOutcome::Loaded { msg_count: 0, .. } => format!(
151 "Loaded session {} (empty — type a message to start)",
152 session.id
153 ),
154 SessionLoadOutcome::Loaded { msg_count, title } => {
155 let label = title.as_deref().unwrap_or("(untitled)");
156 format!("Loaded previous session: {label} — {msg_count} messages")
157 }
158 SessionLoadOutcome::NewFallback { reason } => {
159 format!("New session (no prior session for this workspace: {reason})")
160 }
161 };
162
163 run_event_loop(
164 &mut terminal,
165 &mut app,
166 &cwd,
167 registry,
168 &mut session,
169 &mut bus_handle,
170 worker_bridge,
171 event_tx,
172 event_rx,
173 result_tx,
174 result_rx,
175 )
176 .await?;
177
178 terminal.show_cursor()?;
179 Ok(())
180}