feat: implement background API connectivity checks and update state based on connectivity status

This commit is contained in:
asepharyana
2026-07-12 12:09:59 +07:00
parent 55bcfda0d6
commit 40108defc0
2 changed files with 58 additions and 1 deletions
+57
View File
@@ -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.
+1 -1
View File
@@ -9,7 +9,7 @@ use crate::app::runtime::stream::turn::StreamedTurn;
use crate::dto::chat::message::ChatMessage;
use crate::dto::provider::request::{ChatRequest, StreamOptions, ToolDef};
const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1";
pub(crate) const DEFAULT_BASE_URL: &str = "https://opencode.ai/zen/v1";
const DEFAULT_MODEL: &str = "deepseek-v4-flash-free";
pub const DEFAULT_API_KEY: &str = "sk-5dd268d88adb496b-818beb-6bc7498e";
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);