mod fetch;
use crate::{
core::{
extensions::{
DockLine, DockSpan, Extension, ExtensionCommand, ExtensionHook, ExtensionMode,
ExtensionTool, ExtensionUiRequest, ForkProjectInfo, OverlayEvent, OverlaySize,
OverlayView, next_ui_id, request_ui,
},
runtime,
},
extensions::{
ACCENT, DIM, ERROR, EXTENSION_FACTORIES, ExtensionFactory, MUTED, PRIORITY_USAGE, SUCCESS,
command_arg, util::truncate_chars,
},
modes::interactive::{
app::{App, MsgLevel},
handlers::register_slash_command,
},
utils::{
glyphs::{DEF_ARROW_UP_DOWN, DEF_BAR_EMPTY, DEF_BAR_FULL},
time::{now_ms, rel_time},
},
};
use fetch::{ProviderReport, QuotaWindow};
use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
const EXT: &str = "usage";
const BAR_WIDTH: usize = 24;
const MAX_PANEL_ROWS: usize = 24;
const REPORT_TTL_MS: u64 = 120_000;
#[linkme::distributed_slice(EXTENSION_FACTORIES)]
static USAGE_FACTORY: ExtensionFactory = ExtensionFactory {
priority: PRIORITY_USAGE,
make: || -> Arc<dyn Extension> { Arc::new(Usage) },
};
#[derive(Default)]
struct State {
overlay_id: Option<u64>,
reports: Option<Vec<ProviderReport>>,
unqueried: Vec<String>,
loading: bool,
refresh_pending: bool,
fetched_at: u64,
skip_reason: Option<String>,
}
fn state() -> &'static Mutex<State> {
static S: OnceLock<Mutex<State>> = OnceLock::new();
S.get_or_init(|| Mutex::new(State::default()))
}
fn lock() -> MutexGuard<'static, State> {
state().lock().unwrap_or_else(|e| e.into_inner())
}
pub struct Usage;
impl Extension for Usage {
fn name(&self) -> &str {
EXT
}
fn description(&self) -> &str {
"Show provider quota / rate-limit usage and reset times (/usage)"
}
fn default_enabled(&self) -> bool {
false
}
fn modes(&self) -> Vec<ExtensionMode> {
vec![ExtensionMode::Dev, ExtensionMode::Creator]
}
fn fork_project(&self) -> Option<ForkProjectInfo> {
Some(ForkProjectInfo {
plugin_name: "jcode".to_string(),
plugin_version: "0.94.0".to_string(),
url: "https://github.com/1jehuang/jcode".to_string(),
})
}
fn tools(&self) -> Vec<ExtensionTool> {
Vec::new()
}
fn commands(&self) -> Vec<ExtensionCommand> {
vec![ExtensionCommand {
name: "usage".to_string(),
description: "/usage (provider quota / rate-limit usage and reset times)".to_string(),
busy_safe: true, subcommands: Vec::new(),
}]
}
fn on_registered(&self) {
register_slash_command(EXT, "usage", command_usage);
}
fn on_enabled_changed(&self, enabled: bool) {
if !enabled {
clear(None);
}
}
fn hooks(&self) -> Vec<ExtensionHook> {
if lock().overlay_id.is_some() {
vec![ExtensionHook::Overlay]
} else {
Vec::new()
}
}
fn wants_redraw(&self) -> bool {
let st = lock();
st.loading || st.refresh_pending
}
fn overlay_view(&self, id: u64, _cols: u16) -> Option<OverlayView> {
let mut st = lock();
if st.overlay_id != Some(id) {
return None;
}
st.refresh_pending = false;
let lines = match (&st.reports, &st.skip_reason) {
(_, Some(reason)) => vec![vec![DockSpan::new(DIM.0, DIM.1, reason.clone())]],
(None, None) => vec![vec![DockSpan::new(DIM.0, DIM.1, "loading quotas…")]],
(Some(reports), None) => report_lines(reports, &st.unqueried, st.fetched_at),
};
Some(OverlayView {
title: "Usage".to_string(),
header: Vec::new(),
lines,
messages: Vec::new(),
footer: vec![
(DEF_ARROW_UP_DOWN.to_string(), "scroll".to_string()),
("Esc".to_string(), "close".to_string()),
],
input: None,
editor: None,
size: OverlaySize::Panel {
max_rows: MAX_PANEL_ROWS as u16,
},
selected: None,
input_focus: false,
})
}
fn on_overlay_event(&self, id: u64, ev: &OverlayEvent) -> bool {
match ev {
OverlayEvent::Closed => clear(Some(id)),
_ => false,
}
}
}
fn clear(id: Option<u64>) -> bool {
let mut st = lock();
if let Some(id) = id
&& st.overlay_id != Some(id)
{
return false;
}
st.overlay_id = None;
st.reports = None;
st.unqueried.clear();
st.loading = false;
st.refresh_pending = false;
st.fetched_at = 0;
st.skip_reason = None;
true
}
fn command_usage(st: &mut App, raw: &str) -> bool {
let arg = command_arg(raw).trim();
if !arg.is_empty() {
st.push_msg(
format!("Usage: /usage (unexpected argument: {arg})"),
MsgLevel::Warning,
);
return false;
}
start_report();
false
}
fn start_report() {
let id = next_ui_id();
let offline = runtime::offline();
let now = now_ms();
let (needs_fetch, cached) = {
let st = lock();
let fresh = st
.reports
.as_ref()
.is_some_and(|_| now.saturating_sub(st.fetched_at) < REPORT_TTL_MS);
(st.reports.is_none() || !fresh, st.reports.clone())
};
{
let mut st = lock();
st.overlay_id = Some(id);
st.reports = cached;
st.skip_reason = offline.then(|| "offline mode: quota requests skipped".to_string());
st.loading = !offline && needs_fetch;
st.refresh_pending = false;
if offline {
st.fetched_at = 0;
st.unqueried.clear();
}
}
request_ui(ExtensionUiRequest::ShowOverlay { id });
if offline || !needs_fetch {
return;
}
match tokio::runtime::Handle::try_current() {
Ok(handle) => {
handle.spawn(async move {
let reports = fetch::fetch_all().await;
let unqueried = fetch::unqueried_providers();
finish_report(id, reports, unqueried);
});
}
Err(_) => {
finish_report(
id,
Vec::new(),
vec!["no async runtime: quota requests skipped".to_string()],
);
}
}
}
fn finish_report(id: u64, reports: Vec<ProviderReport>, unqueried: Vec<String>) {
let mut st = lock();
if st.overlay_id != Some(id) {
return;
}
st.reports = Some(reports);
st.unqueried = unqueried;
st.loading = false;
st.refresh_pending = true;
st.fetched_at = now_ms();
}
fn report_lines(
reports: &[ProviderReport],
unqueried: &[String],
fetched_at: u64,
) -> Vec<DockLine> {
let mut lines: Vec<DockLine> = Vec::new();
lines.push(vec![DockSpan::new(
ACCENT.0,
ACCENT.1,
format!(
"Usage · {} providers · fetched {}",
reports.len(),
rel_time(Some(fetched_at))
),
)]);
if reports.is_empty() {
lines.push(Vec::new());
lines.push(vec![DockSpan::new(
DIM.0,
DIM.1,
"no configured provider exposes a quota endpoint",
)]);
}
for report in reports {
lines.push(Vec::new());
lines.extend(provider_lines(report));
}
if !unqueried.is_empty() {
lines.push(Vec::new());
lines.push(vec![DockSpan::new(
MUTED.0,
MUTED.1,
format!("no quota endpoint: {}", unqueried.join(", ")),
)]);
}
lines
}
fn provider_lines(report: &ProviderReport) -> Vec<DockLine> {
let mut lines: Vec<DockLine> = Vec::new();
let status_span = match (&report.error, report.windows.is_empty()) {
(Some(_), _) => DockSpan::new(ERROR.0, ERROR.1, " error"),
(None, true) => DockSpan::new(MUTED.0, MUTED.1, " (balance only)"),
(None, false) => DockSpan::new(SUCCESS.0, SUCCESS.1, ""),
};
lines.push(vec![
DockSpan::new(ACCENT.0, ACCENT.1, &report.label),
DockSpan::new(MUTED.0, MUTED.1, format!(" · {}", report.provider)),
status_span,
]);
if let Some(error) = &report.error {
lines.push(vec![DockSpan::new(ERROR.0, ERROR.1, format!(" {error}"))]);
}
for window in &report.windows {
lines.push(window_line(window));
}
for (key, value) in &report.extra {
lines.push(vec![DockSpan::new(
MUTED.0,
MUTED.1,
format!(" {key}: {value}"),
)]);
}
lines
}
fn window_line(window: &QuotaWindow) -> DockLine {
const NAME_WIDTH: usize = 20;
let name = truncate_chars(&window.name, NAME_WIDTH);
let percent = window.percent as f64;
let mut spans = vec![
DockSpan::plain(format!(" {name:<NAME_WIDTH$} ")),
DockSpan::new(ACCENT.0, ACCENT.1, percent_bar(percent, BAR_WIDTH)),
DockSpan::new(MUTED.0, MUTED.1, format!(" {percent:5.1}%")),
];
if let Some(resets_at) = window.resets_at {
spans.push(DockSpan::new(
DIM.0,
DIM.1,
format!(" resets {}", rel_time(Some(resets_at))),
));
}
spans
}
fn percent_bar(percent: f64, width: usize) -> String {
let ratio = (percent / 100.0).clamp(0.0, 1.0);
let filled = ((ratio * width as f64).round() as usize).min(width);
let mut out = String::with_capacity(width);
for index in 0..width {
out.push(if index < filled {
DEF_BAR_FULL
} else {
DEF_BAR_EMPTY
});
}
out
}
#[cfg(test)]
mod tests {
use super::fetch::ProviderReport;
use super::*;
use crate::{
core::auth,
extensions::WARNING,
test_support::{AUTH_TEST_LOCK, AgentDirGuard},
utils::time::now_ms,
};
fn window_level(percent: f32) -> (&'static str, &'static str) {
if percent >= 90.0 {
ERROR
} else if percent >= 70.0 {
WARNING
} else {
SUCCESS
}
}
fn lock_global() -> MutexGuard<'static, ()> {
AUTH_TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
}
fn drain_ui() {
while crate::core::extensions::take_pending_ui().is_some() {}
}
fn text(lines: &[DockLine]) -> String {
lines
.iter()
.map(|line| line.iter().map(|s| s.text.as_str()).collect::<String>())
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn percent_bar_rounds_and_clamps() {
assert_eq!(percent_bar(0.0, 4), "░░░░");
assert_eq!(percent_bar(50.0, 4), "██░░");
assert_eq!(percent_bar(100.0, 4), "████");
assert_eq!(percent_bar(150.0, 4), "████");
}
#[test]
fn report_lines_render_windows_extras_and_unqueried() {
let report = ProviderReport {
provider: "anthropic".to_string(),
label: "Anthropic".to_string(),
windows: vec![fetch::QuotaWindow {
name: "5-hour".to_string(),
percent: 42.0,
resets_at: None,
}],
extra: vec![("Extra usage".to_string(), "enabled".to_string())],
error: None,
};
let lines = report_lines(&[report], &["groq".to_string()], now_ms());
let plain = text(&lines);
assert!(plain.contains("Usage · 1 providers"), "{plain}");
assert!(plain.contains("Anthropic"), "{plain}");
assert!(plain.contains("5-hour"), "{plain}");
assert!(plain.contains("42.0%"), "{plain}");
assert!(plain.contains("Extra usage: enabled"), "{plain}");
assert!(plain.contains("no quota endpoint: groq"), "{plain}");
}
#[test]
fn report_lines_show_errors_and_empty_state() {
let report = ProviderReport::failed("openai", "HTTP 401: nope");
let plain = text(&report_lines(&[report], &[], now_ms()));
assert!(plain.contains("OpenAI"), "{plain}");
assert!(plain.contains("HTTP 401: nope"), "{plain}");
assert!(plain.contains("error"), "{plain}");
let plain = text(&report_lines(&[], &[], now_ms()));
assert!(
plain.contains("no configured provider exposes a quota endpoint"),
"{plain}"
);
}
#[test]
fn window_level_flags_high_usage() {
assert_eq!(window_level(10.0), SUCCESS);
assert_eq!(window_level(75.0), WARNING);
assert_eq!(window_level(95.0), ERROR);
}
#[test]
fn extension_declares_command_and_defaults_off() {
assert_eq!(Usage.name(), EXT);
assert!(!Usage.default_enabled(), "usage 应默认不启用");
assert_eq!(
Usage.modes(),
vec![ExtensionMode::Dev, ExtensionMode::Creator]
);
assert!(Usage.tools().is_empty());
let commands = Usage.commands();
assert_eq!(commands.len(), 1);
assert_eq!(commands[0].name, "usage");
assert!(commands[0].busy_safe);
assert!(commands[0].subcommands.is_empty());
}
#[test]
fn fork_project_reports_jcode_source() {
let info = Usage.fork_project().expect("usage 应声明 fork 来源");
assert_eq!(info.plugin_name, "jcode");
assert_eq!(info.plugin_version, "0.94.0");
assert_eq!(info.url, "https://github.com/1jehuang/jcode");
}
#[test]
fn command_rejects_extra_arguments() {
let _g = lock_global();
let _ad = AgentDirGuard::temp();
drain_ui();
let mut app = App::new();
assert!(!command_usage(&mut app, "usage bogus"));
assert!(!app.system_messages.is_empty(), "多余参数应提示用法");
assert_eq!(lock().overlay_id, None, "报错时不应打开面板");
}
#[test]
fn offline_mode_skips_requests_and_clears_state() {
let _g = lock_global();
let _ad = AgentDirGuard::temp();
drain_ui();
crate::core::runtime::set_offline(true);
let mut app = App::new();
assert!(!command_usage(&mut app, "usage"));
assert!(app.system_messages.is_empty());
let st = lock();
assert!(st.overlay_id.is_some(), "离线也应打开面板");
assert!(!st.loading, "离线不进入 loading");
assert!(
st.skip_reason
.as_deref()
.is_some_and(|r| r.contains("offline")),
"{:?}",
st.skip_reason
);
drop(st);
let id = lock().overlay_id.expect("离线也应打开面板");
let view = Usage.overlay_view(id, 80).expect("当前面板应有内容");
let plain = text(&view.lines);
assert!(plain.contains("offline mode"), "{plain}");
Usage.on_enabled_changed(false);
assert_eq!(lock().overlay_id, None);
crate::core::runtime::set_offline(false);
}
#[test]
fn finish_report_ignores_stale_panels() {
let _g = lock_global();
let _ad = AgentDirGuard::temp();
clear(None);
let id = next_ui_id();
{
let mut st = lock();
st.overlay_id = Some(id);
st.loading = true;
}
finish_report(id, Vec::new(), Vec::new());
assert!(lock().reports.is_some());
assert!(!lock().loading);
assert!(lock().refresh_pending);
let stale = id + 1;
finish_report(stale, Vec::new(), vec!["x".to_string()]);
assert!(lock().unqueried.is_empty());
Usage.on_enabled_changed(false);
}
#[test]
fn overlay_renders_report_and_closes() {
let _g = lock_global();
let _ad = AgentDirGuard::temp();
drain_ui();
clear(None);
let id = next_ui_id();
{
let mut st = lock();
st.overlay_id = Some(id);
st.reports = Some(vec![ProviderReport {
provider: "anthropic".to_string(),
label: "Anthropic".to_string(),
windows: vec![fetch::QuotaWindow {
name: "5-hour".to_string(),
percent: 42.0,
resets_at: None,
}],
extra: Vec::new(),
error: None,
}]);
st.fetched_at = now_ms();
}
let view = Usage.overlay_view(id, 80).expect("当前面板应有内容");
let plain = text(&view.lines);
assert!(plain.contains("5-hour"), "{plain}");
assert!(plain.contains("42.0%"), "{plain}");
assert!(Usage.on_overlay_event(id, &OverlayEvent::Closed));
assert_eq!(lock().overlay_id, None);
assert!(lock().reports.is_none());
}
#[test]
fn configured_providers_without_endpoint_are_listed_separately() {
let _g = lock_global();
let _ad = AgentDirGuard::temp();
let _ek = crate::test_support::env_key_lock();
auth::write_auth_key("groq", "gsk-test").unwrap();
let unqueried = fetch::unqueried_providers();
assert!(unqueried.contains(&"groq".to_string()), "{unqueried:?}");
assert!(
!unqueried.iter().any(|id| id == "anthropic"),
"有端点的 provider 不应出现在未查询清单里"
);
}
}