Skip to main content

schwab_cli/ui/
agent_health.rs

1use std::sync::{Arc, Mutex};
2use std::time::Instant;
3
4/// Shared between the embedded agent task and the watch TUI.
5#[derive(Debug, Clone)]
6pub struct AgentWatchHealth {
7    pub loop_running: bool,
8    pub started_at: Instant,
9    pub ticks_completed: u64,
10    pub last_error: Option<String>,
11    /// Set when Schwab OAuth cannot be refreshed — user must `schwab auth login`.
12    pub auth_required: bool,
13}
14
15impl AgentWatchHealth {
16    pub fn starting() -> Self {
17        Self {
18            loop_running: true,
19            started_at: Instant::now(),
20            ticks_completed: 0,
21            last_error: None,
22            auth_required: false,
23        }
24    }
25
26    pub fn record_tick(&mut self) {
27        self.ticks_completed += 1;
28        self.last_error = None;
29        self.auth_required = false;
30    }
31
32    pub fn record_error(&mut self, err: &str) {
33        self.last_error = Some(err.to_string());
34        if is_fatal_auth_error(err) {
35            self.auth_required = true;
36            self.loop_running = false;
37        }
38    }
39}
40
41pub type SharedAgentHealth = Arc<Mutex<AgentWatchHealth>>;
42
43pub fn new_shared_health() -> SharedAgentHealth {
44    Arc::new(Mutex::new(AgentWatchHealth::starting()))
45}
46
47pub fn format_tick_error(err: &str) -> String {
48    if is_fatal_auth_error(err) {
49        return "Schwab login required — run: schwab auth login".to_string();
50    }
51    let needs_login = err.contains("OAuth error");
52    if needs_login && !err.contains("auth login") {
53        format!("{err} → run: schwab auth login")
54    } else {
55        err.to_string()
56    }
57}
58
59pub fn is_fatal_auth_error(err: &str) -> bool {
60    err.contains("invalid_grant")
61        || err.contains("Refresh token is invalid")
62        || err.contains("No refresh token on disk")
63        || err.contains("Run `schwab auth login`")
64        || err.contains("Not authenticated")
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn detects_invalid_grant() {
73        let err = r#"OAuth error: HTTP 400: {"error":"invalid_grant"}"#;
74        assert!(is_fatal_auth_error(err));
75        assert!(format_tick_error(err).contains("schwab auth login"));
76    }
77}