feat: implement background API connectivity checks and update state based on connectivity status
This commit is contained in:
@@ -300,6 +300,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
state.misc.tick_count = state.misc.tick_count.wrapping_add(1);
|
||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||
state.misc.drain_expired_toasts(now_ms);
|
||||
// Background API connectivity check — runs on a background thread
|
||||
// every ~1s while disconnected, every ~30s while connected, so the
|
||||
// status bar reflects real API availability without user input.
|
||||
let check_interval = if state.misc.api_connected { 600 } else { 20 };
|
||||
if state.misc.tick_count % check_interval == 0 {
|
||||
spawn_api_connectivity_check(state);
|
||||
}
|
||||
crate::app::review::maybe_run_staleness_sweep(state);
|
||||
if let Some(ref rt) = state.session_runtime {
|
||||
let _ = crate::app::review::process_pending_lessons(&rt.session_dir, &state.memory_dir);
|
||||
@@ -392,6 +399,8 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
|
||||
if let Some(ref mut rt) = state.session_runtime {
|
||||
rt.push_message(crate::dto::chat::message::ChatMessage::system(message.clone()));
|
||||
}
|
||||
} else if kind == "connectivity" {
|
||||
state.misc.api_connected = message == "connected";
|
||||
} else {
|
||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
||||
}
|
||||
@@ -1142,6 +1151,54 @@ fn run_oauth_flow(provider: &str) -> anyhow::Result<String> {
|
||||
Ok(format!("Successfully authenticated with {}.", provider))
|
||||
}
|
||||
|
||||
/// Spawn a background thread that checks API reachability via a lightweight HEAD
|
||||
/// request to `<base_url>/models`, pushing the result as a `SystemNote` so the
|
||||
/// next `Tick` handler updates `api_connected`.
|
||||
///
|
||||
/// Flow: resolve the provider's base URL → build a short-lived reqwest client
|
||||
/// with 3s connect / 5s total timeout → HEAD the `/models` endpoint → push
|
||||
/// a `connectivity` SystemNote with the result.
|
||||
///
|
||||
/// Why: runs off the event loop so a slow/TIMEOUT network does not block the TUI.
|
||||
fn spawn_api_connectivity_check(state: &AppStateRest) {
|
||||
let base_url = state
|
||||
.app_config
|
||||
.providers
|
||||
.get(&state.settings.provider)
|
||||
.map(|p| p.api_base.clone())
|
||||
.unwrap_or_else(|| crate::service::provider::DEFAULT_BASE_URL.to_string());
|
||||
let turn_events = state.turn_events.clone();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let url = format!("{}/models", base_url.trim_end_matches('/'));
|
||||
let connected = match reqwest::blocking::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(5))
|
||||
.connect_timeout(std::time::Duration::from_secs(3))
|
||||
.build()
|
||||
{
|
||||
Ok(client) => match client.head(&url).send() {
|
||||
Ok(resp) => {
|
||||
let s = resp.status();
|
||||
// 401/403 means the server is reachable (just auth is wrong)
|
||||
s.is_success() || s.as_u16() == 401 || s.as_u16() == 403
|
||||
}
|
||||
Err(_) => false,
|
||||
},
|
||||
Err(_) => false,
|
||||
};
|
||||
if let Ok(mut q) = turn_events.lock() {
|
||||
q.push_back(TurnEvent::SystemNote {
|
||||
kind: "connectivity".to_string(),
|
||||
message: if connected {
|
||||
"connected".to_string()
|
||||
} else {
|
||||
"disconnected".to_string()
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Generate `n` pseudo-random bytes from the system clock mixed with a monotonic
|
||||
/// counter, providing sufficient unpredictability for a per-flow OAuth state
|
||||
/// token without a `rand` dependency.
|
||||
|
||||
Reference in New Issue
Block a user