use std::{
thread::JoinHandle,
time::{Duration, Instant},
};
use super::MissionControlApp;
use crate::{
providers::claude_usage::{ClaudeUsage, connected_account, load_claude_usage, quota_label},
tui::{TuiEvent, state::MissionControlState},
};
const REFRESH_INTERVAL: Duration = Duration::from_secs(300);
const CONNECTION_CHECK_INTERVAL: Duration = Duration::from_secs(5);
#[derive(Default)]
pub(super) struct ClaudeQuotaRefresh {
worker: Option<JoinHandle<QuotaCheck>>,
account: Option<[u8; 32]>,
last_attempt: Option<Instant>,
usage: Option<ClaudeUsage>,
last_label_update: Option<Instant>,
last_check: Option<Instant>,
manual_refresh: bool,
}
struct QuotaCheck {
account: Option<[u8; 32]>,
usage: Option<ClaudeUsage>,
attempted_at: Option<Instant>,
}
impl ClaudeQuotaRefresh {
pub(super) fn request_refresh(&mut self) {
self.manual_refresh = true;
}
pub(super) fn cleanup(&mut self) -> Option<String> {
let worker = self.worker.take()?;
match crate::thread_join::join_with_timeout(worker, Duration::from_millis(50)) {
Ok(Err(_)) => Some("Claude usage worker panicked".into()),
_ => None,
}
}
}
impl MissionControlApp {
pub(super) fn poll_claude_quota(
&mut self,
state: &mut MissionControlState,
now: Instant,
) -> bool {
let quota = &mut self.claude_quota;
let mut changed = false;
if quota
.worker
.as_ref()
.is_some_and(|worker| worker.is_finished())
{
let check = quota
.worker
.take()
.expect("finished worker")
.join()
.unwrap_or(QuotaCheck {
account: None,
usage: None,
attempted_at: Some(now),
});
let old_label = state.claude_quota_label.clone();
if quota.account != check.account {
quota.account = check.account;
quota.last_attempt = None;
quota.usage = None;
state.claude_quota_label = None;
}
if let Some(attempted_at) = check.attempted_at {
quota.last_attempt = Some(attempted_at);
quota.usage = check.usage;
}
state.claude_quota_label = quota
.usage
.as_ref()
.and_then(|usage| quota_label(usage, chrono::Utc::now()));
quota.last_label_update = Some(now);
changed = old_label != state.claude_quota_label;
}
if let Some(usage) = quota.usage.as_ref()
&& quota
.last_label_update
.is_none_or(|last| now.duration_since(last) >= Duration::from_secs(1))
{
quota.last_label_update = Some(now);
let label = quota_label(usage, chrono::Utc::now());
if label != state.claude_quota_label {
state.claude_quota_label = label;
changed = true;
}
}
let refresh_due = quota.account.is_some()
&& quota
.last_attempt
.is_none_or(|last| now.duration_since(last) >= REFRESH_INTERVAL)
&& state
.last_agent_activity
.is_some_and(|last| now.duration_since(last) < REFRESH_INTERVAL);
let check_due = quota
.last_check
.is_none_or(|last| now.duration_since(last) >= CONNECTION_CHECK_INTERVAL);
if quota.worker.is_none() && (check_due || refresh_due || quota.manual_refresh) {
let previous = quota.account;
let force = quota.manual_refresh || refresh_due;
quota.manual_refresh = false;
quota.last_check = Some(now);
let sender = self.events.clone();
quota.worker = Some(std::thread::spawn(move || {
let account = connected_account();
let mut check = QuotaCheck {
account,
usage: None,
attempted_at: None,
};
if account.is_some() && (force || previous.is_none()) {
check.attempted_at = Some(Instant::now());
let usage = load_claude_usage();
check.account = connected_account();
if check.account == account {
check.usage = usage.ok();
}
}
let _ = sender.try_send(TuiEvent::WorkerOutcomeReady);
check
}));
}
changed
}
}