Read Claude Code's own quota endpoint, not just its status line
Decision 0015 rejected the account usage endpoint because AGENTS.md forbade reading a harness's credential. The rule was written to stop one program helping itself to another's secrets, and it was catching a legitimate use with it: the user asking about their own subscription, through software they installed to do that. AGENTS.md now states the narrow allowance instead of an absolute the project does not hold, and 0016 records it. The status line stays. It is free and it speaks every turn. What it cannot do is report the per-model weekly limits a Max plan meters separately, or answer at all before a session has taken a turn. The first live reading found the account-wide seven-day window at 38% left and a per-model weekly window at 77% left — a second ceiling the footer previously could not see. Constraints the credential is read under, all enforced in code: access token only, never the refresh token; zeroed on drop, along with the file buffer it was borrowed out of; unprintable by construction, since HarnessError carries no owned strings and AccessToken's Debug is hand-written; identified as lumbridge/<version>, because sending claude-code/2.1.0 would make our traffic indistinguishable from the harness's in Anthropic's logs; and off entirely under LUMBRIDGE_CLAUDE_OAUTH=0. The request runs on a detached thread with a slow refresh and a 429 backoff, so a ten-second round trip cannot stall the transcript follower or make quitting wait on the network, and one surface failing does not fault the other two. Footer polish on top: the harness name prints once per group instead of in front of each of its four windows, each quota carries a short scope pill (5h, 7d, Fable wk, tokens) where an invisible BORDER-weight label used to be, quotas sort ahead of spend, and a window under ten percent turns its headline amber — value colour on the number, provenance colour on the meter, never mixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ef52aa7ce2
commit
219c674aea
Generated
+149
-3
@@ -48,7 +48,7 @@ version = "0.26.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bda177466b9524d59f1b12f0dd30b68696788e9992a7e959021c4a0ed96fcf59"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.13.1",
|
||||
"home",
|
||||
"libc",
|
||||
@@ -500,6 +500,12 @@ version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||
|
||||
[[package]]
|
||||
name = "bindgen"
|
||||
version = "0.71.1"
|
||||
@@ -1015,6 +1021,35 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||
dependencies = [
|
||||
"cookie",
|
||||
"document-features",
|
||||
"idna",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"time",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
@@ -1294,6 +1329,12 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.5.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.20"
|
||||
@@ -1406,6 +1447,15 @@ dependencies = [
|
||||
"libloading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "document-features"
|
||||
version = "0.2.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||
dependencies = [
|
||||
"litrs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast-rs"
|
||||
version = "1.2.1"
|
||||
@@ -3002,6 +3052,12 @@ version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||
|
||||
[[package]]
|
||||
name = "litrs"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.14"
|
||||
@@ -3052,6 +3108,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.20",
|
||||
"ureq",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3480,6 +3537,12 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||
|
||||
[[package]]
|
||||
name = "num-derive"
|
||||
version = "0.4.2"
|
||||
@@ -3974,6 +4037,12 @@ dependencies = [
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.21"
|
||||
@@ -4631,6 +4700,7 @@ version = "0.23.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"ring",
|
||||
"rustls-pki-types",
|
||||
@@ -5589,6 +5659,36 @@ dependencies = [
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde_core",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-keccak"
|
||||
version = "2.0.2"
|
||||
@@ -5983,6 +6083,37 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||
|
||||
[[package]]
|
||||
name = "ureq"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"cookie_store",
|
||||
"log",
|
||||
"percent-encoding",
|
||||
"rustls",
|
||||
"rustls-pki-types",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"ureq-proto",
|
||||
"utf8-zero",
|
||||
"webpki-roots",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ureq-proto"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||
dependencies = [
|
||||
"base64 0.23.1",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.8"
|
||||
@@ -6002,7 +6133,7 @@ version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"data-url",
|
||||
"flate2",
|
||||
"fontdb 0.23.0",
|
||||
@@ -6029,6 +6160,12 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-zero"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
@@ -6365,6 +6502,15 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||
dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
@@ -7184,7 +7330,7 @@ version = "0.12.15-zed"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac2d05756ff48539950c3282ad7acf3817ad3f08797c205ad1c34a2ce03b9970"
|
||||
dependencies = [
|
||||
"base64",
|
||||
"base64 0.22.1",
|
||||
"bytes",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
|
||||
@@ -2126,10 +2126,21 @@ impl LumbridgeShell {
|
||||
.child("no harness on this pane"),
|
||||
)
|
||||
})
|
||||
.children(segments.into_iter().map(|segment| {
|
||||
let selected = active.as_ref() == Some(&segment.id);
|
||||
usage_segment(segment, selected)
|
||||
}))
|
||||
// The harness name is printed once per group. Repeating
|
||||
// "CLAUDE CODE" in front of four of its own windows spends the
|
||||
// strip's width on a word the eye has already read.
|
||||
.children({
|
||||
let mut previous: Option<String> = None;
|
||||
segments
|
||||
.into_iter()
|
||||
.map(|segment| {
|
||||
let repeats = previous.as_deref() == Some(segment.label.as_str());
|
||||
previous = Some(segment.label.clone());
|
||||
let selected = active.as_ref() == Some(&segment.id);
|
||||
usage_segment(segment, selected, repeats)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2176,9 +2187,20 @@ fn usage_meter(consumed_permille: Option<u64>, color: u32) -> impl IntoElement {
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn usage_segment(segment: UsageSegment, selected: bool) -> impl IntoElement {
|
||||
/// One quota in the strip.
|
||||
///
|
||||
/// `continues_group` means the harness above this one is the same, so its name
|
||||
/// is left off and only the window is named.
|
||||
fn usage_segment(segment: UsageSegment, selected: bool, continues_group: bool) -> impl IntoElement {
|
||||
let color = provenance_color(segment.provenance);
|
||||
let name_color = if selected { TEXT } else { MUTED };
|
||||
let headline_color = if segment.headline.is_none() {
|
||||
MUTED
|
||||
} else if segment.critical {
|
||||
ATTENTION
|
||||
} else {
|
||||
TEXT
|
||||
};
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
@@ -2186,21 +2208,32 @@ fn usage_segment(segment: UsageSegment, selected: bool) -> impl IntoElement {
|
||||
.when(selected, |view| {
|
||||
view.px_2().py_1().rounded(px(4.0)).bg(rgb(PANEL_ACTIVE))
|
||||
})
|
||||
.when(!continues_group, |view| {
|
||||
view.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(name_color))
|
||||
.child(segment.label),
|
||||
)
|
||||
})
|
||||
// A quiet pill rather than more running text. The window's name is a
|
||||
// label on the number, not another number, and at BORDER weight it was
|
||||
// simply invisible.
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(name_color))
|
||||
.child(segment.label),
|
||||
.px(px(5.0))
|
||||
.py(px(1.0))
|
||||
.rounded(px(3.0))
|
||||
.bg(rgb(if selected { BORDER } else { BORDER_QUIET }))
|
||||
.text_color(rgb(if selected { TEXT } else { MUTED }))
|
||||
.child(segment.scope),
|
||||
)
|
||||
.child(usage_meter(segment.consumed_permille, color))
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_color(rgb(if segment.headline.is_some() {
|
||||
TEXT
|
||||
} else {
|
||||
MUTED
|
||||
}))
|
||||
.text_color(rgb(headline_color))
|
||||
.child(segment.headline.unwrap_or_else(|| "no reading".to_owned())),
|
||||
)
|
||||
.when_some(segment.reset, |view, reset| {
|
||||
|
||||
@@ -183,6 +183,17 @@ impl UsageFeed {
|
||||
let outcome = probe.poll();
|
||||
let health = outcome.health();
|
||||
for profile in probe.profiles() {
|
||||
// A probe may announce a profile after it starts: the per-model
|
||||
// weekly limits are named by the provider's response, so they
|
||||
// cannot be declared up front. Without this they would report
|
||||
// readings the strip has no profile to render them against.
|
||||
if self
|
||||
.profiles
|
||||
.insert(profile.id().clone(), profile.clone())
|
||||
.is_none()
|
||||
{
|
||||
changed = true;
|
||||
}
|
||||
if self.health.insert(profile.id().clone(), health) != Some(health) {
|
||||
changed = true;
|
||||
}
|
||||
@@ -255,28 +266,46 @@ impl UsageFeed {
|
||||
.into_iter()
|
||||
.filter_map(|id| self.segment(id))
|
||||
.collect();
|
||||
// Two windows on one account would otherwise both read "CODEX". A
|
||||
// strip that names two different quotas the same thing is worse than
|
||||
// a longer label.
|
||||
let duplicated: Vec<String> = segments
|
||||
.iter()
|
||||
.filter(|segment| {
|
||||
segments
|
||||
.iter()
|
||||
.filter(|other| other.label == segment.label)
|
||||
.count()
|
||||
> 1
|
||||
})
|
||||
.map(|segment| segment.label.clone())
|
||||
.collect();
|
||||
for segment in &mut segments {
|
||||
if duplicated.contains(&segment.label) {
|
||||
segment.label = format!("{} {}", segment.label, segment.model.to_uppercase());
|
||||
// Keep one harness's quotas together. Claude Code alone reports four,
|
||||
// and a strip that interleaves them with Codex reads as eight unrelated
|
||||
// numbers instead of two accounts. Stable within a group, so a segment
|
||||
// never moves under the pointer.
|
||||
let mut order: Vec<String> = Vec::new();
|
||||
for segment in &segments {
|
||||
if !order.contains(&segment.label) {
|
||||
order.push(segment.label.clone());
|
||||
}
|
||||
}
|
||||
segments.sort_by_key(|segment| {
|
||||
(
|
||||
order
|
||||
.iter()
|
||||
.position(|label| *label == segment.label)
|
||||
.unwrap_or(usize::MAX),
|
||||
// Quotas before spend within a harness. "How much is left" is
|
||||
// the question; "how much was used" is the footnote.
|
||||
usize::from(segment.consumed_permille.is_none()),
|
||||
)
|
||||
});
|
||||
segments
|
||||
}
|
||||
|
||||
/// The shortest unambiguous name for a quota window.
|
||||
///
|
||||
/// The provider's own wording — "five-hour window", "Fable weekly" — is
|
||||
/// right in a detail view and far too long in a strip that has to hold six
|
||||
/// of them.
|
||||
fn short_scope(model: &str) -> String {
|
||||
match model {
|
||||
"five-hour window" => "5h".to_owned(),
|
||||
"seven-day window" => "7d".to_owned(),
|
||||
"session transcripts" => "tokens".to_owned(),
|
||||
other => other
|
||||
.strip_suffix(" weekly")
|
||||
.map_or_else(|| other.to_owned(), |name| format!("{name} wk")),
|
||||
}
|
||||
}
|
||||
|
||||
fn segment(&self, id: &AccountProfileId) -> Option<UsageSegment> {
|
||||
let profile = self.profiles.get(id)?;
|
||||
let projection = self.projection(id);
|
||||
@@ -285,7 +314,7 @@ impl UsageFeed {
|
||||
Some(UsageSegment {
|
||||
id: id.clone(),
|
||||
label: profile.harness().to_uppercase(),
|
||||
model: profile.model().to_owned(),
|
||||
scope: Self::short_scope(profile.model()),
|
||||
consumed_permille,
|
||||
// "How much do I have left" is the question an engineer actually
|
||||
// asks. Consumption stays available in the expanded detail.
|
||||
@@ -310,6 +339,10 @@ impl UsageFeed {
|
||||
let unit = projection.unit()?;
|
||||
Some(format!("{} used", unit.format_amount(consumed)))
|
||||
}),
|
||||
// Ten percent of a window is the point at which the number stops
|
||||
// being background information and starts being a decision about
|
||||
// what to run next.
|
||||
critical: consumed_permille.is_some_and(|permille| permille >= 900),
|
||||
// A profile that reports spend but no ceiling and no reset should
|
||||
// say so where the reset would go, rather than leaving a silent
|
||||
// gap that reads as "we just haven't shown it yet".
|
||||
@@ -361,11 +394,18 @@ impl Drop for UsageFeed {
|
||||
pub(crate) struct UsageSegment {
|
||||
pub(crate) id: AccountProfileId,
|
||||
pub(crate) label: String,
|
||||
pub(crate) model: String,
|
||||
/// The window this segment is about, in the shortest form that stays
|
||||
/// unambiguous: `5h`, `7d`, `fable wk`, `tokens`. One account has several
|
||||
/// quotas and the strip has to name which one it is showing.
|
||||
pub(crate) scope: String,
|
||||
pub(crate) consumed_permille: Option<u64>,
|
||||
/// What to lead with: how much is left when a ceiling is known, how much
|
||||
/// was spent when it is not, and nothing at all when there is no reading.
|
||||
pub(crate) headline: Option<String>,
|
||||
/// Nearly gone. Drives the headline's colour, never the meter's: the meter
|
||||
/// carries provenance, and mixing the two would make a trustworthy reading
|
||||
/// and an alarming one look the same.
|
||||
pub(crate) critical: bool,
|
||||
pub(crate) reset: Option<String>,
|
||||
pub(crate) burn: String,
|
||||
pub(crate) burn_provenance: UsageProvenance,
|
||||
|
||||
Reference in New Issue
Block a user