use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeySource {
NikaDaemon,
OsKeychain,
EnvVar,
NotConfigured,
}
impl KeySource {
pub fn label(&self) -> &'static str {
match self {
KeySource::NikaDaemon => "daemon",
KeySource::OsKeychain => "keychain",
KeySource::EnvVar => "env",
KeySource::NotConfigured => "none",
}
}
pub fn icon(&self) -> &'static str {
match self {
KeySource::NikaDaemon => "🔐",
KeySource::OsKeychain => "🔑",
KeySource::EnvVar => "📦",
KeySource::NotConfigured => "❌",
}
}
pub fn is_secure(&self) -> bool {
matches!(self, KeySource::NikaDaemon | KeySource::OsKeychain)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConnectionStatus {
Connected { latency_ms: u64 },
NotConfigured,
Failed { error: String },
Checking,
Unknown,
}
impl ConnectionStatus {
pub fn icon(&self) -> &'static str {
match self {
ConnectionStatus::Connected { .. } => "✅",
ConnectionStatus::NotConfigured => "⚪",
ConnectionStatus::Failed { .. } => "❌",
ConnectionStatus::Checking => "🔄",
ConnectionStatus::Unknown => "❓",
}
}
pub fn label(&self) -> &'static str {
match self {
ConnectionStatus::Connected { .. } => "connected",
ConnectionStatus::NotConfigured => "not configured",
ConnectionStatus::Failed { .. } => "failed",
ConnectionStatus::Checking => "checking...",
ConnectionStatus::Unknown => "unknown",
}
}
pub fn is_usable(&self) -> bool {
matches!(self, ConnectionStatus::Connected { .. })
}
}
#[derive(Debug, Clone)]
pub struct ProviderStatus {
pub id: &'static str,
pub connection: ConnectionStatus,
pub key_source: KeySource,
pub last_checked: Option<Instant>,
}
impl ProviderStatus {
pub fn new(id: &'static str) -> Self {
Self {
id,
connection: ConnectionStatus::Unknown,
key_source: KeySource::NotConfigured,
last_checked: None,
}
}
pub fn is_ready(&self) -> bool {
self.connection.is_usable() && self.key_source != KeySource::NotConfigured
}
}
#[derive(Debug, Clone, Default)]
pub struct ProviderStatusCache {
pub providers: Vec<ProviderStatus>,
pub last_refresh: Option<Instant>,
pub refreshing: bool,
}
impl ProviderStatusCache {
pub fn new() -> Self {
Self::default()
}
pub fn llm_configured_count(&self) -> usize {
self.providers
.iter()
.filter(|p| {
let cat = super::icons::provider_category(p.id);
(cat == "LLM" || cat == "Local") && p.key_source != KeySource::NotConfigured
})
.count()
}
pub fn mcp_configured_count(&self) -> usize {
self.providers
.iter()
.filter(|p| {
super::icons::provider_category(p.id) == "MCP"
&& p.key_source != KeySource::NotConfigured
})
.count()
}
pub fn summary(&self) -> String {
format!(
"LLM: {}/7 | MCP: {}/6",
self.llm_configured_count(),
self.mcp_configured_count()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_key_source_secure() {
assert!(KeySource::NikaDaemon.is_secure());
assert!(KeySource::OsKeychain.is_secure());
assert!(!KeySource::EnvVar.is_secure());
assert!(!KeySource::NotConfigured.is_secure());
}
#[test]
fn test_connection_status_usable() {
assert!(ConnectionStatus::Connected { latency_ms: 100 }.is_usable());
assert!(!ConnectionStatus::NotConfigured.is_usable());
assert!(!ConnectionStatus::Failed {
error: "test".into()
}
.is_usable());
}
#[test]
fn test_provider_status_ready() {
let mut status = ProviderStatus::new("anthropic");
assert!(!status.is_ready());
status.connection = ConnectionStatus::Connected { latency_ms: 50 };
assert!(!status.is_ready());
status.key_source = KeySource::NikaDaemon;
assert!(status.is_ready());
}
#[test]
fn test_cache_summary() {
let cache = ProviderStatusCache::new();
assert_eq!(cache.summary(), "LLM: 0/7 | MCP: 0/6");
}
}