Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions app/src/ai/agent/conversation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,18 @@ impl AIConversation {
&self.conversation_usage_metadata.context_window_segments
}

/// Total absolute token count currently used in the context window,
/// computed by summing all segment token counts. Returns None when
/// the server did not emit segments.
pub fn context_window_tokens(&self) -> Option<u32> {
let segments = self.context_window_segments();
if segments.is_empty() {
None
} else {
Some(segments.iter().map(|s| s.token_count).sum())
}
}

/// Total credits spent in the conversation, including both LLM inference
/// and platform credits.
pub fn credits_spent(&self) -> f32 {
Expand Down
46 changes: 43 additions & 3 deletions app/src/ai/blocklist/agent_view/agent_input_footer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2059,17 +2059,57 @@ impl AgentInputFooter {
let icon = icon_for_context_window_usage(usage);
let remaining_pct = ((1.0 - usage) * 100.0).round() as i32;

// Build tooltip with absolute token count when available.
// Examples: "41% context remaining (112k / 1M)", "41% context remaining"
let token_info = conversation.context_window_tokens().map(|tokens| {
let limit = AIExecutionProfilesModel::as_ref(ctx)
.active_profile()
.context_window_limit_for_request(ctx);
let tokens_str = if tokens < 1_000 {
tokens.to_string()
} else if tokens < 1_000_000 {
let k = tokens as f64 / 1_000.0;
if k.fract() == 0.0 {
format!("{}k", k as u64)
} else {
format!("{:.1}k", k)
}
} else {
let m = tokens as f64 / 1_000_000.0;
if m.fract() == 0.0 {
format!("{}M", m as u64)
} else {
format!("{:.1}M", m)
}
};
if let Some(limit) = limit {
let limit_str = if limit < 1_000_000 {
format!("{}k", limit / 1_000)
} else {
format!("{}M", limit / 1_000_000)
};
format!("{tokens_str} / {limit_str}")
} else {
tokens_str
}
});

let context_remaining_text = if let Some(token_info) = token_info {
format!("{remaining_pct}% context remaining ({token_info})")
} else {
format!("{remaining_pct}% context remaining")
};

let expiry = conversation.latest_exchange().and_then(|exchange| {
let output = exchange.output_status.output()?;
output.get().model_info.as_ref()?.prompt_cache_expires_at
});
let is_cache_expired = FeatureFlag::PromptCacheExpiryWarning.is_enabled()
&& expiry.is_some_and(|expiry| expiry <= Local::now());
let context_remaining_tooltip = format!("{remaining_pct}% context remaining");
let tooltip = if is_cache_expired {
format!("{context_remaining_tooltip} · prompt cache expired")
format!("{context_remaining_text} · prompt cache expired")
} else {
context_remaining_tooltip
context_remaining_text
};

self.prompt_cache_expired = is_cache_expired;
Expand Down
67 changes: 62 additions & 5 deletions app/src/ai/blocklist/usage/conversation_usage_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub struct ConversationUsageInfo {
pub tool_calls: i32,
pub models: Vec<ModelTokenUsage>,
pub context_window_usage: f32,
/// Absolute token count currently used in the context window.
pub context_window_tokens: Option<u32>,
/// The configured context window limit (e.g. 1M tokens).
pub context_window_limit: Option<u32>,
/// Per-segment breakdown of the context window. Scaled so the segments
/// sum to `context_window_usage`. Empty when the server did not emit it.
pub context_window_segments: Vec<ContextWindowSegment>,
Expand Down Expand Up @@ -466,19 +470,51 @@ impl ConversationUsageView {
}

labels.push(render_label_text("Context window used", appearance));

// Build display string: absolute tokens as primary, percentage as secondary.
// Examples: "112k / 1M (11%)", "112k (41%)"
let context_usage_pct = self.usage_info.context_window_usage * 100.;
let context_usage_str = if context_window_breakdown_enabled && self.context_window_expanded
{
format!("{context_usage_pct:.2}%")
let pct_str = if context_usage_pct.fract() == 0.0 {
format!("{}%", context_usage_pct as u32)
} else {
format!("{}%", context_usage_pct.round())
format!("{:.1}%", context_usage_pct)
};

let context_display_str =
if let (Some(tokens), Some(limit)) =
(self.usage_info.context_window_tokens, self.usage_info.context_window_limit)
{
// Show absolute tokens with limit: "112k / 1M (41%)"
let limit_str = format_token_count(limit);
let tokens_str = format_token_count(tokens);
if context_window_breakdown_enabled && self.context_window_expanded {
format!("{tokens_str} / {limit_str} ({context_usage_pct:.2}%)")
} else {
format!("{tokens_str} / {limit_str} ({pct_str})")
}
} else if let Some(tokens) = self.usage_info.context_window_tokens {
// Show absolute tokens without limit: "112k (41%)"
let tokens_str = format_token_count(tokens);
if context_window_breakdown_enabled && self.context_window_expanded {
format!("{tokens_str} ({context_usage_pct:.2}%)")
} else {
format!("{tokens_str} ({pct_str})")
}
} else {
// Fallback to percentage only when no segment data is available.
if context_window_breakdown_enabled && self.context_window_expanded {
format!("{context_usage_pct:.2}%")
} else {
pct_str
}
};

let mut context_window_row = Flex::row()
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_size(MainAxisSize::Min)
.with_spacing(4.)
.with_child(
Text::new(context_usage_str, appearance.ui_font_family(), font_size)
Text::new(context_display_str, appearance.ui_font_family(), font_size)
.with_color(text_color)
.finish(),
)
Expand Down Expand Up @@ -1182,6 +1218,27 @@ const CONTEXT_WINDOW_SEGMENT_PERCENT_DECIMAL_PLACES: usize = 2;
/// Maximum width of the context-window "Other" tooltip before wrapping.
const CONTEXT_WINDOW_OTHER_TOOLTIP_MAX_WIDTH: f32 = 280.;

/// Formats a token count into a human-readable string (e.g. 112k, 1.2M).
fn format_token_count(tokens: u32) -> String {
if tokens < 1_000 {
tokens.to_string()
} else if tokens < 1_000_000 {
let k = tokens as f64 / 1_000.0;
if k.fract() == 0.0 {
format!("{}k", k as u64)
} else {
format!("{:.1}k", k)
}
} else {
let m = tokens as f64 / 1_000_000.0;
if m.fract() == 0.0 {
format!("{}M", m as u64)
} else {
format!("{:.1}M", m)
}
}
}

#[cfg(test)]
#[path = "conversation_usage_view_tests.rs"]
mod tests;
2 changes: 2 additions & 0 deletions app/src/ai/blocklist/usage/conversation_usage_view_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ fn placeholder_usage_info() -> ConversationUsageInfo {
tool_calls: 0,
models: Vec::new(),
context_window_usage: 0.0,
context_window_tokens: None,
context_window_limit: None,
context_window_segments: Vec::new(),
files_changed: 0,
lines_added: 0,
Expand Down
4 changes: 4 additions & 0 deletions app/src/terminal/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6895,6 +6895,10 @@ impl TerminalView {
tool_calls: tool_usage.total_tool_calls(),
models: conversation.token_usage().to_vec(),
context_window_usage: conversation.context_window_usage(),
context_window_tokens: conversation.context_window_tokens(),
context_window_limit: AIExecutionProfilesModel::as_ref(ctx)
.active_profile()
.context_window_limit_for_request(ctx),
context_window_segments: conversation.context_window_segments().to_vec(),
files_changed: tool_usage.apply_file_diff_stats.files_changed,
lines_added: tool_usage.apply_file_diff_stats.lines_added,
Expand Down
10 changes: 10 additions & 0 deletions app/src/workspaces/gql_convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,13 +320,23 @@ impl From<&gql_usage::ConversationUsage> for ConversationUsageInfo {
context_window_segments,
..
} = (&gql.usage_metadata).into();

// Compute absolute context window tokens from segments when available.
let context_window_tokens = if context_window_segments.is_empty() {
None
} else {
Some(context_window_segments.iter().map(|s| s.token_count).sum())
};

ConversationUsageInfo {
credits_spent,
platform_credits_spent,
credits_spent_for_last_block: None,
tool_calls: tool.total_tool_calls(),
models,
context_window_usage,
context_window_tokens,
context_window_limit: None,
context_window_segments,
files_changed: tool.apply_file_diff_stats.files_changed,
lines_added: tool.apply_file_diff_stats.lines_added,
Expand Down