Skip to main content

codetether_agent/tui/app/
run.rs

1use 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
22/// Outcome of trying to resume the most recent workspace session at startup.
23///
24/// Used to populate an informative status line so the user can distinguish
25/// "no prior session existed" from "prior session loaded with 0 messages".
26enum 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    // Seed session metadata from the user's config so RLM settings
126    // (threshold, iteration limits, subcall model) take effect.
127    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}