use std::sync::{Mutex, OnceLock, PoisonError};
use std::time::{Duration, Instant};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HudPlacement {
#[default]
Auto,
InBand,
StatusLine,
}
impl HudPlacement {
pub fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::InBand => "in_band",
Self::StatusLine => "status_line",
}
}
pub(super) fn redaction_counts_in_band(self) -> bool {
match self {
Self::InBand => true,
Self::StatusLine => false,
Self::Auto => !claude_status_line_active(),
}
}
}
#[must_use]
pub fn compact_metric(
receipt: &lean_ctx_protocol::context_gateway::ContextDecisionReceiptV1,
) -> Option<String> {
let tokens = receipt.tokens;
if tokens.delivered == 0 && tokens.original == 0 {
return None;
}
let mut line = format!(
"LeanCTX ๐ก {} โ {}",
kilo(tokens.original),
kilo(tokens.delivered)
);
if tokens.original > tokens.delivered && tokens.original > 0 {
let saved = (tokens.original - tokens.delivered) * 100 / tokens.original;
line.push_str(&format!(" โ{saved}%"));
}
let mut parts = Vec::new();
let security = receipt.security;
if security.redactions > 0 {
parts.push(format!("{} redacted", security.redactions));
}
if security.blocked_objects > 0 {
parts.push(format!("{} withheld", security.blocked_objects));
}
if security.injection_signals > 0 {
parts.push(format!(
"{} injection signal(s)",
security.injection_signals
));
}
let sources = receipt.sources;
if sources.inspected > 1 {
parts.push(format!(
"{}/{} sources used",
sources.selected, sources.inspected
));
}
if !parts.is_empty() {
line.push_str(" ยท ");
line.push_str(&parts.join(" ยท "));
}
Some(line)
}
fn kilo(tokens: u64) -> String {
if tokens >= 1000 {
format!("{:.1}k", tokens as f64 / 1000.0)
} else {
tokens.to_string()
}
}
const REFRESH: Duration = Duration::from_mins(1);
fn claude_status_line_active() -> bool {
if std::env::var_os("CLAUDECODE").is_none() {
return false;
}
static CACHE: OnceLock<Mutex<Option<(Instant, bool)>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| Mutex::new(None));
let mut cached = cache.lock().unwrap_or_else(PoisonError::into_inner);
if let Some((at, active)) = *cached
&& at.elapsed() < REFRESH
{
return active;
}
let active = dirs::home_dir().is_some_and(|home| {
let path = crate::core::editor_registry::claude_state_dir(&home).join("settings.json");
std::fs::read_to_string(path)
.ok()
.and_then(|content| crate::core::jsonc::parse_jsonc(&content).ok())
.and_then(|settings| {
settings
.get("statusLine")
.and_then(|line| line.get("command"))
.and_then(serde_json::Value::as_str)
.map(crate::core::claude_host::is_lean_ctx_statusline)
})
.unwrap_or(false)
});
*cached = Some((Instant::now(), active));
active
}