Skip to content

Commit 25a36e6

Browse files
author
sikongyue
committed
feat(ai): Show context window usage as absolute tokens (e.g. 112k / 1M)
Display absolute token counts alongside percentages in AI Agent context window usage UI, so users can judge when to compact or hand off based on actual token counts rather than relative percentages. - Add context_window_tokens and context_window_limit fields to ConversationUsageInfo - Add context_window_tokens() method in conversation.rs, computed by summing segment token counts - Pass context window token data through terminal/view.rs - Update conversation_usage_view.rs display format: - With limit: "112k / 1M (41%)" - Without limit: "112k (41%)" - Fallback: "41%" (when no segment data available) - Update agent input footer tooltip: "41% context remaining (112k / 1M)" - Fix struct initialization in gql_convert.rs and test files Closes issue: Show context window usage as absolute tokens
1 parent 2b4a66f commit 25a36e6

6 files changed

Lines changed: 133 additions & 8 deletions

File tree

app/src/ai/agent/conversation.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,18 @@ impl AIConversation {
764764
&self.conversation_usage_metadata.context_window_segments
765765
}
766766

767+
/// Total absolute token count currently used in the context window,
768+
/// computed by summing all segment token counts. Returns None when
769+
/// the server did not emit segments.
770+
pub fn context_window_tokens(&self) -> Option<u32> {
771+
let segments = self.context_window_segments();
772+
if segments.is_empty() {
773+
None
774+
} else {
775+
Some(segments.iter().map(|s| s.token_count).sum())
776+
}
777+
}
778+
767779
/// Total credits spent in the conversation, including both LLM inference
768780
/// and platform credits.
769781
pub fn credits_spent(&self) -> f32 {

app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2059,17 +2059,57 @@ impl AgentInputFooter {
20592059
let icon = icon_for_context_window_usage(usage);
20602060
let remaining_pct = ((1.0 - usage) * 100.0).round() as i32;
20612061

2062+
// Build tooltip with absolute token count when available.
2063+
// Examples: "41% context remaining (112k / 1M)", "41% context remaining"
2064+
let token_info = conversation.context_window_tokens().map(|tokens| {
2065+
let limit = AIExecutionProfilesModel::as_ref(ctx)
2066+
.active_profile()
2067+
.context_window_limit_for_request(ctx);
2068+
let tokens_str = if tokens < 1_000 {
2069+
tokens.to_string()
2070+
} else if tokens < 1_000_000 {
2071+
let k = tokens as f64 / 1_000.0;
2072+
if k.fract() == 0.0 {
2073+
format!("{}k", k as u64)
2074+
} else {
2075+
format!("{:.1}k", k)
2076+
}
2077+
} else {
2078+
let m = tokens as f64 / 1_000_000.0;
2079+
if m.fract() == 0.0 {
2080+
format!("{}M", m as u64)
2081+
} else {
2082+
format!("{:.1}M", m)
2083+
}
2084+
};
2085+
if let Some(limit) = limit {
2086+
let limit_str = if limit < 1_000_000 {
2087+
format!("{}k", limit / 1_000)
2088+
} else {
2089+
format!("{}M", limit / 1_000_000)
2090+
};
2091+
format!("{tokens_str} / {limit_str}")
2092+
} else {
2093+
tokens_str
2094+
}
2095+
});
2096+
2097+
let context_remaining_text = if let Some(token_info) = token_info {
2098+
format!("{remaining_pct}% context remaining ({token_info})")
2099+
} else {
2100+
format!("{remaining_pct}% context remaining")
2101+
};
2102+
20622103
let expiry = conversation.latest_exchange().and_then(|exchange| {
20632104
let output = exchange.output_status.output()?;
20642105
output.get().model_info.as_ref()?.prompt_cache_expires_at
20652106
});
20662107
let is_cache_expired = FeatureFlag::PromptCacheExpiryWarning.is_enabled()
20672108
&& expiry.is_some_and(|expiry| expiry <= Local::now());
2068-
let context_remaining_tooltip = format!("{remaining_pct}% context remaining");
20692109
let tooltip = if is_cache_expired {
2070-
format!("{context_remaining_tooltip} · prompt cache expired")
2110+
format!("{context_remaining_text} · prompt cache expired")
20712111
} else {
2072-
context_remaining_tooltip
2112+
context_remaining_text
20732113
};
20742114

20752115
self.prompt_cache_expired = is_cache_expired;

app/src/ai/blocklist/usage/conversation_usage_view.rs

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ pub struct ConversationUsageInfo {
4949
pub tool_calls: i32,
5050
pub models: Vec<ModelTokenUsage>,
5151
pub context_window_usage: f32,
52+
/// Absolute token count currently used in the context window.
53+
pub context_window_tokens: Option<u32>,
54+
/// The configured context window limit (e.g. 1M tokens).
55+
pub context_window_limit: Option<u32>,
5256
/// Per-segment breakdown of the context window. Scaled so the segments
5357
/// sum to `context_window_usage`. Empty when the server did not emit it.
5458
pub context_window_segments: Vec<ContextWindowSegment>,
@@ -466,19 +470,51 @@ impl ConversationUsageView {
466470
}
467471

468472
labels.push(render_label_text("Context window used", appearance));
473+
474+
// Build display string: absolute tokens as primary, percentage as secondary.
475+
// Examples: "112k / 1M (11%)", "112k (41%)"
469476
let context_usage_pct = self.usage_info.context_window_usage * 100.;
470-
let context_usage_str = if context_window_breakdown_enabled && self.context_window_expanded
471-
{
472-
format!("{context_usage_pct:.2}%")
477+
let pct_str = if context_usage_pct.fract() == 0.0 {
478+
format!("{}%", context_usage_pct as u32)
473479
} else {
474-
format!("{}%", context_usage_pct.round())
480+
format!("{:.1}%", context_usage_pct)
475481
};
482+
483+
let context_display_str =
484+
if let (Some(tokens), Some(limit)) =
485+
(self.usage_info.context_window_tokens, self.usage_info.context_window_limit)
486+
{
487+
// Show absolute tokens with limit: "112k / 1M (41%)"
488+
let limit_str = format_token_count(limit);
489+
let tokens_str = format_token_count(tokens);
490+
if context_window_breakdown_enabled && self.context_window_expanded {
491+
format!("{tokens_str} / {limit_str} ({context_usage_pct:.2}%)")
492+
} else {
493+
format!("{tokens_str} / {limit_str} ({pct_str})")
494+
}
495+
} else if let Some(tokens) = self.usage_info.context_window_tokens {
496+
// Show absolute tokens without limit: "112k (41%)"
497+
let tokens_str = format_token_count(tokens);
498+
if context_window_breakdown_enabled && self.context_window_expanded {
499+
format!("{tokens_str} ({context_usage_pct:.2}%)")
500+
} else {
501+
format!("{tokens_str} ({pct_str})")
502+
}
503+
} else {
504+
// Fallback to percentage only when no segment data is available.
505+
if context_window_breakdown_enabled && self.context_window_expanded {
506+
format!("{context_usage_pct:.2}%")
507+
} else {
508+
pct_str
509+
}
510+
};
511+
476512
let mut context_window_row = Flex::row()
477513
.with_cross_axis_alignment(CrossAxisAlignment::Center)
478514
.with_main_axis_size(MainAxisSize::Min)
479515
.with_spacing(4.)
480516
.with_child(
481-
Text::new(context_usage_str, appearance.ui_font_family(), font_size)
517+
Text::new(context_display_str, appearance.ui_font_family(), font_size)
482518
.with_color(text_color)
483519
.finish(),
484520
)
@@ -1182,6 +1218,27 @@ const CONTEXT_WINDOW_SEGMENT_PERCENT_DECIMAL_PLACES: usize = 2;
11821218
/// Maximum width of the context-window "Other" tooltip before wrapping.
11831219
const CONTEXT_WINDOW_OTHER_TOOLTIP_MAX_WIDTH: f32 = 280.;
11841220

1221+
/// Formats a token count into a human-readable string (e.g. 112k, 1.2M).
1222+
fn format_token_count(tokens: u32) -> String {
1223+
if tokens < 1_000 {
1224+
tokens.to_string()
1225+
} else if tokens < 1_000_000 {
1226+
let k = tokens as f64 / 1_000.0;
1227+
if k.fract() == 0.0 {
1228+
format!("{}k", k as u64)
1229+
} else {
1230+
format!("{:.1}k", k)
1231+
}
1232+
} else {
1233+
let m = tokens as f64 / 1_000_000.0;
1234+
if m.fract() == 0.0 {
1235+
format!("{}M", m as u64)
1236+
} else {
1237+
format!("{:.1}M", m)
1238+
}
1239+
}
1240+
}
1241+
11851242
#[cfg(test)]
11861243
#[path = "conversation_usage_view_tests.rs"]
11871244
mod tests;

app/src/ai/blocklist/usage/conversation_usage_view_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ fn placeholder_usage_info() -> ConversationUsageInfo {
3939
tool_calls: 0,
4040
models: Vec::new(),
4141
context_window_usage: 0.0,
42+
context_window_tokens: None,
43+
context_window_limit: None,
4244
context_window_segments: Vec::new(),
4345
files_changed: 0,
4446
lines_added: 0,

app/src/terminal/view.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6895,6 +6895,10 @@ impl TerminalView {
68956895
tool_calls: tool_usage.total_tool_calls(),
68966896
models: conversation.token_usage().to_vec(),
68976897
context_window_usage: conversation.context_window_usage(),
6898+
context_window_tokens: conversation.context_window_tokens(),
6899+
context_window_limit: AIExecutionProfilesModel::as_ref(ctx)
6900+
.active_profile()
6901+
.context_window_limit_for_request(ctx),
68986902
context_window_segments: conversation.context_window_segments().to_vec(),
68996903
files_changed: tool_usage.apply_file_diff_stats.files_changed,
69006904
lines_added: tool_usage.apply_file_diff_stats.lines_added,

app/src/workspaces/gql_convert.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,13 +320,23 @@ impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo {
320320
context_window_segments,
321321
..
322322
} = (&gql.usage_metadata).into();
323+
324+
// Compute absolute context window tokens from segments when available.
325+
let context_window_tokens = if context_window_segments.is_empty() {
326+
None
327+
} else {
328+
Some(context_window_segments.iter().map(|s| s.token_count).sum())
329+
};
330+
323331
ConversationUsageInfo {
324332
credits_spent,
325333
platform_credits_spent,
326334
credits_spent_for_last_block: None,
327335
tool_calls: tool.total_tool_calls(),
328336
models,
329337
context_window_usage,
338+
context_window_tokens,
339+
context_window_limit: None,
330340
context_window_segments,
331341
files_changed: tool.apply_file_diff_stats.files_changed,
332342
lines_added: tool.apply_file_diff_stats.lines_added,

0 commit comments

Comments
 (0)