Show token used when context window is unknown (#7497)

- Show context window usage in tokens instead of percentage when the
window length is unknown.
This commit is contained in:
Ahmed Ibrahim
2025-12-02 11:45:50 -08:00
committed by GitHub
Unverified
parent 21ad1c1c90
commit 127e307f89
8 changed files with 118 additions and 14 deletions
+2
View File
@@ -76,6 +76,8 @@ pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option<ModelInfo> {
_ if slug.starts_with("codex-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)),
_ if slug.starts_with("exp-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)),
_ => None,
}
}
@@ -113,6 +113,7 @@ pub(crate) struct ChatComposer {
footer_mode: FooterMode,
footer_hint_override: Option<Vec<(String, String)>>,
context_window_percent: Option<i64>,
context_window_used_tokens: Option<i64>,
}
/// Popup state at most one can be visible at any time.
@@ -156,6 +157,7 @@ impl ChatComposer {
footer_mode: FooterMode::ShortcutSummary,
footer_hint_override: None,
context_window_percent: None,
context_window_used_tokens: None,
};
// Apply configuration via the setter to keep side-effects centralized.
this.set_disable_paste_burst(disable_paste_burst);
@@ -1387,6 +1389,7 @@ impl ChatComposer {
use_shift_enter_hint: self.use_shift_enter_hint,
is_task_running: self.is_task_running,
context_window_percent: self.context_window_percent,
context_window_used_tokens: self.context_window_used_tokens,
}
}
@@ -1517,10 +1520,13 @@ impl ChatComposer {
self.is_task_running = running;
}
pub(crate) fn set_context_window_percent(&mut self, percent: Option<i64>) {
if self.context_window_percent != percent {
self.context_window_percent = percent;
pub(crate) fn set_context_window(&mut self, percent: Option<i64>, used_tokens: Option<i64>) {
if self.context_window_percent == percent && self.context_window_used_tokens == used_tokens
{
return;
}
self.context_window_percent = percent;
self.context_window_used_tokens = used_tokens;
}
pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) {
+41 -5
View File
@@ -1,6 +1,7 @@
use crate::key_hint;
use crate::key_hint::KeyBinding;
use crate::render::line_utils::prefix_lines;
use crate::status::format_tokens_compact;
use crate::ui_consts::FOOTER_INDENT_COLS;
use crossterm::event::KeyCode;
use ratatui::buffer::Buffer;
@@ -18,6 +19,7 @@ pub(crate) struct FooterProps {
pub(crate) use_shift_enter_hint: bool,
pub(crate) is_task_running: bool,
pub(crate) context_window_percent: Option<i64>,
pub(crate) context_window_used_tokens: Option<i64>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -81,7 +83,10 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
is_task_running: props.is_task_running,
})],
FooterMode::ShortcutSummary => {
let mut line = context_window_line(props.context_window_percent);
let mut line = context_window_line(
props.context_window_percent,
props.context_window_used_tokens,
);
line.push_span(" · ".dim());
line.extend(vec![
key_hint::plain(KeyCode::Char('?')).into(),
@@ -94,7 +99,10 @@ fn footer_lines(props: FooterProps) -> Vec<Line<'static>> {
esc_backtrack_hint: props.esc_backtrack_hint,
}),
FooterMode::EscHint => vec![esc_hint_line(props.esc_backtrack_hint)],
FooterMode::ContextOnly => vec![context_window_line(props.context_window_percent)],
FooterMode::ContextOnly => vec![context_window_line(
props.context_window_percent,
props.context_window_used_tokens,
)],
}
}
@@ -221,9 +229,18 @@ fn build_columns(entries: Vec<Line<'static>>) -> Vec<Line<'static>> {
.collect()
}
fn context_window_line(percent: Option<i64>) -> Line<'static> {
let percent = percent.unwrap_or(100).clamp(0, 100);
Line::from(vec![Span::from(format!("{percent}% context left")).dim()])
fn context_window_line(percent: Option<i64>, used_tokens: Option<i64>) -> Line<'static> {
if let Some(percent) = percent {
let percent = percent.clamp(0, 100);
return Line::from(vec![Span::from(format!("{percent}% context left")).dim()]);
}
if let Some(tokens) = used_tokens {
let used_fmt = format_tokens_compact(tokens);
return Line::from(vec![Span::from(format!("{used_fmt} used")).dim()]);
}
Line::from(vec![Span::from("100% context left").dim()])
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@@ -400,6 +417,7 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -411,6 +429,7 @@ mod tests {
use_shift_enter_hint: true,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -422,6 +441,7 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -433,6 +453,7 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: true,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -444,6 +465,7 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -455,6 +477,7 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: None,
},
);
@@ -466,6 +489,19 @@ mod tests {
use_shift_enter_hint: false,
is_task_running: true,
context_window_percent: Some(72),
context_window_used_tokens: None,
},
);
snapshot_footer(
"footer_context_tokens_used",
FooterProps {
mode: FooterMode::ShortcutSummary,
esc_backtrack_hint: false,
use_shift_enter_hint: false,
is_task_running: false,
context_window_percent: None,
context_window_used_tokens: Some(123_456),
},
);
}
+13 -3
View File
@@ -76,6 +76,7 @@ pub(crate) struct BottomPane {
/// Queued user messages to show above the composer while a turn is running.
queued_user_messages: QueuedUserMessages,
context_window_percent: Option<i64>,
context_window_used_tokens: Option<i64>,
}
pub(crate) struct BottomPaneParams {
@@ -118,6 +119,7 @@ impl BottomPane {
esc_backtrack_hint: false,
animations_enabled,
context_window_percent: None,
context_window_used_tokens: None,
}
}
@@ -130,6 +132,11 @@ impl BottomPane {
self.context_window_percent
}
#[cfg(test)]
pub(crate) fn context_window_used_tokens(&self) -> Option<i64> {
self.context_window_used_tokens
}
fn active_view(&self) -> Option<&dyn BottomPaneView> {
self.view_stack.last().map(std::convert::AsRef::as_ref)
}
@@ -344,13 +351,16 @@ impl BottomPane {
}
}
pub(crate) fn set_context_window_percent(&mut self, percent: Option<i64>) {
if self.context_window_percent == percent {
pub(crate) fn set_context_window(&mut self, percent: Option<i64>, used_tokens: Option<i64>) {
if self.context_window_percent == percent && self.context_window_used_tokens == used_tokens
{
return;
}
self.context_window_percent = percent;
self.composer.set_context_window_percent(percent);
self.context_window_used_tokens = used_tokens;
self.composer
.set_context_window(percent, self.context_window_used_tokens);
self.request_redraw();
}
@@ -0,0 +1,5 @@
---
source: tui/src/bottom_pane/footer.rs
expression: terminal.backend()
---
" 123K used · ? for shortcuts "
+12 -3
View File
@@ -515,7 +515,7 @@ impl ChatWidget {
match info {
Some(info) => self.apply_token_info(info),
None => {
self.bottom_pane.set_context_window_percent(None);
self.bottom_pane.set_context_window(None, None);
self.token_info = None;
}
}
@@ -523,7 +523,8 @@ impl ChatWidget {
fn apply_token_info(&mut self, info: TokenUsageInfo) {
let percent = self.context_remaining_percent(&info);
self.bottom_pane.set_context_window_percent(percent);
let used_tokens = self.context_used_tokens(&info, percent.is_some());
self.bottom_pane.set_context_window(percent, used_tokens);
self.token_info = Some(info);
}
@@ -536,12 +537,20 @@ impl ChatWidget {
})
}
fn context_used_tokens(&self, info: &TokenUsageInfo, percent_known: bool) -> Option<i64> {
if percent_known {
return None;
}
Some(info.total_token_usage.tokens_in_context_window())
}
fn restore_pre_review_token_info(&mut self) {
if let Some(saved) = self.pre_review_token_info.take() {
match saved {
Some(info) => self.apply_token_info(info),
None => {
self.bottom_pane.set_context_window_percent(None);
self.bottom_pane.set_context_window(None, None);
self.token_info = None;
}
}
+35
View File
@@ -297,6 +297,41 @@ fn token_count_none_resets_context_indicator() {
assert_eq!(chat.bottom_pane.context_window_percent(), None);
}
#[test]
fn context_indicator_shows_used_tokens_when_window_unknown() {
let (mut chat, _rx, _ops) = make_chatwidget_manual();
chat.config.model_context_window = None;
let auto_compact_limit = 200_000;
chat.config.model_auto_compact_token_limit = Some(auto_compact_limit);
// No model window, so the indicator should fall back to showing tokens used.
let total_tokens = 106_000;
let token_usage = TokenUsage {
total_tokens,
..TokenUsage::default()
};
let token_info = TokenUsageInfo {
total_token_usage: token_usage.clone(),
last_token_usage: token_usage,
model_context_window: None,
};
chat.handle_codex_event(Event {
id: "token-usage".into(),
msg: EventMsg::TokenCount(TokenCountEvent {
info: Some(token_info),
rate_limits: None,
}),
});
assert_eq!(chat.bottom_pane.context_window_percent(), None);
assert_eq!(
chat.bottom_pane.context_window_used_tokens(),
Some(total_tokens)
);
}
#[cfg_attr(
target_os = "macos",
ignore = "system configuration APIs are blocked under macOS seatbelt"
+1
View File
@@ -5,6 +5,7 @@ mod helpers;
mod rate_limits;
pub(crate) use card::new_status_output;
pub(crate) use helpers::format_tokens_compact;
pub(crate) use rate_limits::RateLimitSnapshotDisplay;
pub(crate) use rate_limits::rate_limit_snapshot_display;