Skip to content

Commit 814f149

Browse files
authored
feat: add context usage percentage indicator to prompt (#2994)
Add experimental feature to show context window usage as a percentage in the chat prompt (e.g., "[rust-agent] 6% >"). The percentage is color-coded: green (<50%), yellow (50-89%), red (90-100%). The feature is disabled by default and can be enabled via /experiment.
1 parent daed93c commit 814f149

File tree

6 files changed

+215
-76
lines changed

6 files changed

+215
-76
lines changed

crates/chat-cli/src/cli/chat/cli/experiment.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,11 @@ static AVAILABLE_EXPERIMENTS: &[Experiment] = &[
5050
description: "Enables Q to create todo lists that can be viewed and managed using /todos",
5151
setting_key: Setting::EnabledTodoList,
5252
},
53+
Experiment {
54+
name: "Context Usage Indicator",
55+
description: "Shows context usage percentage in the prompt (e.g., [rust-agent] 6% >)",
56+
setting_key: Setting::EnabledContextUsageIndicator,
57+
},
5358
];
5459

5560
#[derive(Debug, PartialEq, Args)]

crates/chat-cli/src/cli/chat/cli/usage.rs

Lines changed: 99 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,60 @@ use crate::cli::chat::{
2121
};
2222
use crate::os::Os;
2323

24+
/// Detailed usage data for context window analysis
25+
#[derive(Debug)]
26+
pub struct DetailedUsageData {
27+
pub total_tokens: TokenCount,
28+
pub context_tokens: TokenCount,
29+
pub assistant_tokens: TokenCount,
30+
pub user_tokens: TokenCount,
31+
pub tools_tokens: TokenCount,
32+
pub context_window_size: usize,
33+
pub dropped_context_files: Vec<(String, String)>,
34+
}
35+
36+
/// Calculate usage percentage from token counts
37+
pub fn calculate_usage_percentage(tokens: TokenCount, context_window_size: usize) -> f32 {
38+
(tokens.value() as f32 / context_window_size as f32) * 100.0
39+
}
40+
41+
/// Get detailed usage data for context window analysis
42+
pub async fn get_detailed_usage_data(session: &mut ChatSession, os: &Os) -> Result<DetailedUsageData, ChatError> {
43+
let context_window_size = context_window_tokens(session.conversation.model_info.as_ref());
44+
45+
let state = session
46+
.conversation
47+
.backend_conversation_state(os, true, &mut std::io::stderr())
48+
.await?;
49+
50+
let data = state.calculate_conversation_size();
51+
let tool_specs_json: String = state
52+
.tools
53+
.values()
54+
.filter_map(|s| serde_json::to_string(s).ok())
55+
.collect::<Vec<String>>()
56+
.join("");
57+
let tools_char_count: CharCount = tool_specs_json.len().into();
58+
let total_tokens: TokenCount =
59+
(data.context_messages + data.user_messages + data.assistant_messages + tools_char_count).into();
60+
61+
Ok(DetailedUsageData {
62+
total_tokens,
63+
context_tokens: data.context_messages.into(),
64+
assistant_tokens: data.assistant_messages.into(),
65+
user_tokens: data.user_messages.into(),
66+
tools_tokens: tools_char_count.into(),
67+
context_window_size,
68+
dropped_context_files: state.dropped_context_files,
69+
})
70+
}
71+
72+
/// Get total usage percentage (simple interface for prompt generation)
73+
pub async fn get_total_usage_percentage(session: &mut ChatSession, os: &Os) -> Result<f32, ChatError> {
74+
let data = get_detailed_usage_data(session, os).await?;
75+
Ok(calculate_usage_percentage(data.total_tokens, data.context_window_size))
76+
}
77+
2478
/// Arguments for the usage command that displays token usage statistics and context window
2579
/// information.
2680
///
@@ -32,12 +86,9 @@ pub struct UsageArgs;
3286

3387
impl UsageArgs {
3488
pub async fn execute(self, os: &Os, session: &mut ChatSession) -> Result<ChatState, ChatError> {
35-
let state = session
36-
.conversation
37-
.backend_conversation_state(os, true, &mut session.stderr)
38-
.await?;
89+
let usage_data = get_detailed_usage_data(session, os).await?;
3990

40-
if !state.dropped_context_files.is_empty() {
91+
if !usage_data.dropped_context_files.is_empty() {
4192
execute!(
4293
session.stderr,
4394
style::SetForegroundColor(Color::DarkYellow),
@@ -50,33 +101,18 @@ impl UsageArgs {
50101
)?;
51102
}
52103

53-
let data = state.calculate_conversation_size();
54-
let tool_specs_json: String = state
55-
.tools
56-
.values()
57-
.filter_map(|s| serde_json::to_string(s).ok())
58-
.collect::<Vec<String>>()
59-
.join("");
60-
let context_token_count: TokenCount = data.context_messages.into();
61-
let assistant_token_count: TokenCount = data.assistant_messages.into();
62-
let user_token_count: TokenCount = data.user_messages.into();
63-
let tools_char_count: CharCount = tool_specs_json.len().into(); // usize → CharCount
64-
let tools_token_count: TokenCount = tools_char_count.into(); // CharCount → TokenCount
65-
let total_token_used: TokenCount =
66-
(data.context_messages + data.user_messages + data.assistant_messages + tools_char_count).into();
67104
let window_width = session.terminal_width();
68105
// set a max width for the progress bar for better aesthetic
69106
let progress_bar_width = std::cmp::min(window_width, 80);
70107

71-
let context_window_size = context_window_tokens(session.conversation.model_info.as_ref());
72-
let context_width =
73-
((context_token_count.value() as f64 / context_window_size as f64) * progress_bar_width as f64) as usize;
74-
let assistant_width =
75-
((assistant_token_count.value() as f64 / context_window_size as f64) * progress_bar_width as f64) as usize;
76-
let tools_width =
77-
((tools_token_count.value() as f64 / context_window_size as f64) * progress_bar_width as f64) as usize;
78-
let user_width =
79-
((user_token_count.value() as f64 / context_window_size as f64) * progress_bar_width as f64) as usize;
108+
let context_width = ((usage_data.context_tokens.value() as f64 / usage_data.context_window_size as f64)
109+
* progress_bar_width as f64) as usize;
110+
let assistant_width = ((usage_data.assistant_tokens.value() as f64 / usage_data.context_window_size as f64)
111+
* progress_bar_width as f64) as usize;
112+
let tools_width = ((usage_data.tools_tokens.value() as f64 / usage_data.context_window_size as f64)
113+
* progress_bar_width as f64) as usize;
114+
let user_width = ((usage_data.user_tokens.value() as f64 / usage_data.context_window_size as f64)
115+
* progress_bar_width as f64) as usize;
80116

81117
let left_over_width = progress_bar_width
82118
- std::cmp::min(
@@ -86,69 +122,73 @@ impl UsageArgs {
86122

87123
let is_overflow = (context_width + assistant_width + user_width + tools_width) > progress_bar_width;
88124

125+
let total_percentage = calculate_usage_percentage(usage_data.total_tokens, usage_data.context_window_size);
126+
89127
if is_overflow {
90128
queue!(
91129
session.stderr,
92130
style::Print(format!(
93131
"\nCurrent context window ({} of {}k tokens used)\n",
94-
total_token_used,
95-
context_window_size / 1000
132+
usage_data.total_tokens,
133+
usage_data.context_window_size / 1000
96134
)),
97135
style::SetForegroundColor(Color::DarkRed),
98136
style::Print("█".repeat(progress_bar_width)),
99137
style::SetForegroundColor(Color::Reset),
100138
style::Print(" "),
101-
style::Print(format!(
102-
"{:.2}%",
103-
(total_token_used.value() as f32 / context_window_size as f32) * 100.0
104-
)),
139+
style::Print(format!("{:.2}%", total_percentage)),
105140
)?;
106141
} else {
107142
queue!(
108143
session.stderr,
109144
style::Print(format!(
110145
"\nCurrent context window ({} of {}k tokens used)\n",
111-
total_token_used,
112-
context_window_size / 1000
146+
usage_data.total_tokens,
147+
usage_data.context_window_size / 1000
113148
)),
114149
// Context files
115150
style::SetForegroundColor(Color::DarkCyan),
116151
// add a nice visual to mimic "tiny" progress, so the overrall progress bar doesn't look too
117152
// empty
118-
style::Print("|".repeat(if context_width == 0 && *context_token_count > 0 {
119-
1
120-
} else {
121-
0
122-
})),
153+
style::Print(
154+
"|".repeat(if context_width == 0 && usage_data.context_tokens.value() > 0 {
155+
1
156+
} else {
157+
0
158+
})
159+
),
123160
style::Print("█".repeat(context_width)),
124161
// Tools
125162
style::SetForegroundColor(Color::DarkRed),
126-
style::Print("|".repeat(if tools_width == 0 && *tools_token_count > 0 {
163+
style::Print("|".repeat(if tools_width == 0 && usage_data.tools_tokens.value() > 0 {
127164
1
128165
} else {
129166
0
130167
})),
131168
style::Print("█".repeat(tools_width)),
132169
// Assistant responses
133170
style::SetForegroundColor(Color::Blue),
134-
style::Print("|".repeat(if assistant_width == 0 && *assistant_token_count > 0 {
171+
style::Print(
172+
"|".repeat(if assistant_width == 0 && usage_data.assistant_tokens.value() > 0 {
173+
1
174+
} else {
175+
0
176+
})
177+
),
178+
style::Print("█".repeat(assistant_width)),
179+
// User prompts
180+
style::SetForegroundColor(Color::Magenta),
181+
style::Print("|".repeat(if user_width == 0 && usage_data.user_tokens.value() > 0 {
135182
1
136183
} else {
137184
0
138185
})),
139-
style::Print("█".repeat(assistant_width)),
140-
// User prompts
141-
style::SetForegroundColor(Color::Magenta),
142-
style::Print("|".repeat(if user_width == 0 && *user_token_count > 0 { 1 } else { 0 })),
143186
style::Print("█".repeat(user_width)),
144187
style::SetForegroundColor(Color::DarkGrey),
145188
style::Print("█".repeat(left_over_width)),
146189
style::Print(" "),
147190
style::SetForegroundColor(Color::Reset),
148-
style::Print(format!(
149-
"{:.2}%",
150-
(total_token_used.value() as f32 / context_window_size as f32) * 100.0
151-
)),
191+
style::Print(format!("{:.2}%", total_percentage)),
152192
)?;
153193
}
154194

@@ -161,32 +201,32 @@ impl UsageArgs {
161201
style::SetForegroundColor(Color::Reset),
162202
style::Print(format!(
163203
"~{} tokens ({:.2}%)\n",
164-
context_token_count,
165-
(context_token_count.value() as f32 / context_window_size as f32) * 100.0
204+
usage_data.context_tokens,
205+
calculate_usage_percentage(usage_data.context_tokens, usage_data.context_window_size)
166206
)),
167207
style::SetForegroundColor(Color::DarkRed),
168208
style::Print("█ Tools: "),
169209
style::SetForegroundColor(Color::Reset),
170210
style::Print(format!(
171211
" ~{} tokens ({:.2}%)\n",
172-
tools_token_count,
173-
(tools_token_count.value() as f32 / context_window_size as f32) * 100.0
212+
usage_data.tools_tokens,
213+
calculate_usage_percentage(usage_data.tools_tokens, usage_data.context_window_size)
174214
)),
175215
style::SetForegroundColor(Color::Blue),
176216
style::Print("█ Q responses: "),
177217
style::SetForegroundColor(Color::Reset),
178218
style::Print(format!(
179219
" ~{} tokens ({:.2}%)\n",
180-
assistant_token_count,
181-
(assistant_token_count.value() as f32 / context_window_size as f32) * 100.0
220+
usage_data.assistant_tokens,
221+
calculate_usage_percentage(usage_data.assistant_tokens, usage_data.context_window_size)
182222
)),
183223
style::SetForegroundColor(Color::Magenta),
184224
style::Print("█ Your prompts: "),
185225
style::SetForegroundColor(Color::Reset),
186226
style::Print(format!(
187227
" ~{} tokens ({:.2}%)\n\n",
188-
user_token_count,
189-
(user_token_count.value() as f32 / context_window_size as f32) * 100.0
228+
usage_data.user_tokens,
229+
calculate_usage_percentage(usage_data.user_tokens, usage_data.context_window_size)
190230
)),
191231
)?;
192232

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,7 +1920,7 @@ impl ChatSession {
19201920
style::SetForegroundColor(Color::Reset),
19211921
style::SetAttribute(Attribute::Reset)
19221922
)?;
1923-
let prompt = self.generate_tool_trust_prompt();
1923+
let prompt = self.generate_tool_trust_prompt(os).await;
19241924
let user_input = match self.read_user_input(&prompt, false) {
19251925
Some(input) => input,
19261926
None => return Ok(ChatState::Exit),
@@ -3115,11 +3115,25 @@ impl ChatSession {
31153115
}
31163116

31173117
/// Helper function to generate a prompt based on the current context
3118-
fn generate_tool_trust_prompt(&mut self) -> String {
3118+
async fn generate_tool_trust_prompt(&mut self, os: &Os) -> String {
31193119
let profile = self.conversation.current_profile().map(|s| s.to_string());
31203120
let all_trusted = self.all_tools_trusted();
31213121
let tangent_mode = self.conversation.is_in_tangent_mode();
3122-
prompt::generate_prompt(profile.as_deref(), all_trusted, tangent_mode)
3122+
3123+
// Check if context usage indicator is enabled
3124+
let usage_percentage = if os
3125+
.database
3126+
.settings
3127+
.get_bool(crate::database::settings::Setting::EnabledContextUsageIndicator)
3128+
.unwrap_or(false)
3129+
{
3130+
use crate::cli::chat::cli::usage::get_total_usage_percentage;
3131+
get_total_usage_percentage(self, os).await.ok()
3132+
} else {
3133+
None
3134+
};
3135+
3136+
prompt::generate_prompt(profile.as_deref(), all_trusted, tangent_mode, usage_percentage)
31233137
}
31243138

31253139
async fn send_tool_use_telemetry(&mut self, os: &Os) {

crates/chat-cli/src/cli/chat/prompt.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,18 @@ impl Highlighter for ChatHelper {
408408
result.push_str(&format!("[{}] ", profile).cyan().to_string());
409409
}
410410

411+
// Add percentage part if present (colored by usage level)
412+
if let Some(percentage) = components.usage_percentage {
413+
let colored_percentage = if percentage < 50.0 {
414+
format!("{}% ", percentage as u32).green()
415+
} else if percentage < 90.0 {
416+
format!("{}% ", percentage as u32).yellow()
417+
} else {
418+
format!("{}% ", percentage as u32).red()
419+
};
420+
result.push_str(&colored_percentage.to_string());
421+
}
422+
411423
// Add tangent indicator if present (yellow)
412424
if components.tangent_mode {
413425
result.push_str(&"↯ ".yellow().to_string());

0 commit comments

Comments
 (0)