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);
|
state.misc.tick_count = state.misc.tick_count.wrapping_add(1);
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
state.misc.drain_expired_toasts(now_ms);
|
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);
|
crate::app::review::maybe_run_staleness_sweep(state);
|
||||||
if let Some(ref rt) = state.session_runtime {
|
if let Some(ref rt) = state.session_runtime {
|
||||||
let _ = crate::app::review::process_pending_lessons(&rt.session_dir, &state.memory_dir);
|
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 {
|
if let Some(ref mut rt) = state.session_runtime {
|
||||||
rt.push_message(crate::dto::chat::message::ChatMessage::system(message.clone()));
|
rt.push_message(crate::dto::chat::message::ChatMessage::system(message.clone()));
|
||||||
}
|
}
|
||||||
|
} else if kind == "connectivity" {
|
||||||
|
state.misc.api_connected = message == "connected";
|
||||||
} else {
|
} else {
|
||||||
state.push_toast(Toast::new(ToastKind::Info, message));
|
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))
|
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
|
/// Generate `n` pseudo-random bytes from the system clock mixed with a monotonic
|
||||||
/// counter, providing sufficient unpredictability for a per-flow OAuth state
|
/// counter, providing sufficient unpredictability for a per-flow OAuth state
|
||||||
/// token without a `rand` dependency.
|
/// token without a `rand` dependency.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ use crate::app::runtime::stream::turn::StreamedTurn;
|
|||||||
use crate::dto::chat::message::ChatMessage;
|
use crate::dto::chat::message::ChatMessage;
|
||||||
use crate::dto::provider::request::{ChatRequest, StreamOptions, ToolDef};
|
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";
|
const DEFAULT_MODEL: &str = "deepseek-v4-flash-free";
|
||||||
pub const DEFAULT_API_KEY: &str = "sk-5dd268d88adb496b-818beb-6bc7498e";
|
pub const DEFAULT_API_KEY: &str = "sk-5dd268d88adb496b-818beb-6bc7498e";
|
||||||
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
const CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
|||||||
Reference in New Issue
Block a user