use crate::render::utils::sanitize_terminal_text;
use std::path::Path;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MemoryStatus {
#[default]
Disabled,
Unknown,
Connected,
Degraded,
}
impl MemoryStatus {
pub(crate) fn label(self) -> &'static str {
match self {
Self::Disabled => "OFF",
Self::Unknown => "UNKNOWN",
Self::Connected => "OK",
Self::Degraded => "DEGRADED",
}
}
}
pub struct StatusPanel {
pub model_name: String,
pub provider_name: String,
pub workspace_display: String,
pub backend_display: String,
pub total_tokens: u64,
pub prompt_tokens: u64,
pub completion_tokens: u64,
pub last_latency_ms: u64,
pub is_connected: bool,
pub input_context_tokens: u64,
pub input_context_tokens_estimated: bool,
pub memory_status: MemoryStatus,
}
impl Default for StatusPanel {
fn default() -> Self {
Self {
model_name: String::new(),
provider_name: String::new(),
workspace_display: String::new(),
backend_display: "Local".to_string(),
total_tokens: 0,
prompt_tokens: 0,
completion_tokens: 0,
last_latency_ms: 0,
is_connected: false,
input_context_tokens: 0,
input_context_tokens_estimated: false,
memory_status: MemoryStatus::Disabled,
}
}
}
impl StatusPanel {
pub fn new() -> Self {
Self::default()
}
pub fn set_provider(&mut self, provider: &str, model: &str) {
self.provider_name = provider.to_string();
self.model_name = model.to_string();
self.is_connected = true;
}
pub fn set_workspace(&mut self, path: &Path) {
self.workspace_display = shorten_path_display(path, 26);
}
pub fn set_remote_workspace(&mut self, base_url: &str) {
self.workspace_display = format!("remote:{base_url}");
}
pub fn set_backend(&mut self, backend: impl Into<String>) {
self.backend_display = backend.into();
}
pub fn update_metrics(
&mut self,
prompt_tokens: u64,
completion_tokens: u64,
latency_ms: u64,
input_context_tokens: u64,
input_context_tokens_estimated: bool,
) {
self.input_context_tokens = input_context_tokens;
self.input_context_tokens_estimated = input_context_tokens_estimated;
self.prompt_tokens = self.prompt_tokens.saturating_add(prompt_tokens);
self.completion_tokens = self.completion_tokens.saturating_add(completion_tokens);
self.total_tokens = self
.total_tokens
.saturating_add(prompt_tokens.saturating_add(completion_tokens));
self.last_latency_ms = latency_ms;
}
pub(crate) fn format_token_count(tokens: u64) -> String {
if tokens < 1000 {
format!("{}", tokens)
} else if tokens < 1_000_000 {
format!("{:.1}K", tokens as f64 / 1000.0)
} else {
format!("{:.1}M", tokens as f64 / 1_000_000.0)
}
}
pub(crate) fn format_context_usage(input_tokens: u64, input_tokens_estimated: bool) -> String {
let prefix = if input_tokens_estimated { "~" } else { "" };
let usage = Self::format_token_count(input_tokens);
format!("{prefix}{usage}")
}
}
fn shorten_path_display(path: &Path, max_chars: usize) -> String {
let s = path.to_string_lossy();
let count = s.chars().count();
if count <= max_chars {
return s.into_owned();
}
let prefix = sanitize_terminal_text("…");
let keep = max_chars.saturating_sub(prefix.chars().count());
let skip = count.saturating_sub(keep);
format!("{prefix}{}", s.chars().skip(skip).collect::<String>())
}
#[cfg(test)]
mod tests {
use super::{MemoryStatus, StatusPanel};
#[test]
fn memory_status_labels_are_operator_readable() {
assert_eq!(MemoryStatus::Unknown.label(), "UNKNOWN");
assert_eq!(MemoryStatus::Connected.label(), "OK");
assert_eq!(MemoryStatus::Degraded.label(), "DEGRADED");
}
#[test]
fn format_context_usage_marks_estimated_values() {
assert_eq!(StatusPanel::format_context_usage(24_000, true), "~24.0K");
}
#[test]
fn format_context_usage_leaves_reported_values_unmarked() {
assert_eq!(StatusPanel::format_context_usage(24_000, false), "24.0K");
}
}