Skip to content

Commit 194fd10

Browse files
authored
feat: /usage (#1177)
1 parent 8c76acd commit 194fd10

File tree

6 files changed

+174
-4
lines changed

6 files changed

+174
-4
lines changed

crates/q_cli/src/cli/chat/command.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ pub enum Command {
4242
Tools {
4343
subcommand: Option<ToolsSubcommand>,
4444
},
45+
Usage,
4546
}
4647

4748
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -696,6 +697,7 @@ impl Command {
696697
},
697698
}
698699
},
700+
"usage" => Self::Usage,
699701
unknown_command => {
700702
// If the command starts with a slash but isn't recognized,
701703
// return an error instead of treating it as a prompt

crates/q_cli/src/cli/chat/conversation_state.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ impl ConversationState {
190190
self.conversation_id.as_ref()
191191
}
192192

193+
/// Returns the conversation history.
194+
pub fn get_chat_history(&self) -> Vec<ChatMessage> {
195+
self.history.iter().cloned().collect()
196+
}
197+
193198
/// Returns the message id associated with the last assistant message, if present.
194199
///
195200
/// This is equivalent to `utterance_id` in the Q API.

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

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ use hooks::{
7373
HookTrigger,
7474
};
7575
use summarization_state::{
76+
CONTEXT_WINDOW_SIZE,
7677
SummarizationState,
7778
TokenWarningLevel,
7879
};
@@ -205,6 +206,7 @@ const HELP_TEXT: &str = color_print::cstr! {"
205206
<em>rm</em> <black!>Remove file(s) from context [--global]</black!>
206207
<em>clear</em> <black!>Clear all files from current context [--global]</black!>
207208
<em>hooks</em> <black!>View and manage context hooks</black!>
209+
<em>/usage</em> <black!>Show current session's context window usage</black!>
208210
209211
<cyan,em>Tips:</cyan,em>
210212
<em>!{command}</em> <black!>Quickly execute a command in your current session</black!>
@@ -1899,6 +1901,157 @@ where
18991901
// during PromptUser.
19001902
execute!(self.output, style::Print("\n\n"),)?;
19011903

1904+
ChatState::PromptUser {
1905+
tool_uses: Some(tool_uses),
1906+
pending_tool_index,
1907+
skip_printing_tools: true,
1908+
}
1909+
},
1910+
Command::Usage => {
1911+
let context_messages = self.conversation_state.context_messages(None).await;
1912+
let chat_history = self.conversation_state.get_chat_history();
1913+
let assistant_messages = chat_history
1914+
.iter()
1915+
.filter_map(|message| {
1916+
if let fig_api_client::model::ChatMessage::AssistantResponseMessage(msg) = message {
1917+
Some(msg)
1918+
} else {
1919+
None
1920+
}
1921+
})
1922+
.collect::<Vec<_>>();
1923+
1924+
let user_messages = chat_history
1925+
.iter()
1926+
.filter_map(|message| {
1927+
if let fig_api_client::model::ChatMessage::UserInputMessage(msg) = message {
1928+
Some(msg)
1929+
} else {
1930+
None
1931+
}
1932+
})
1933+
.collect::<Vec<_>>();
1934+
1935+
let context_token_count = context_messages
1936+
.iter()
1937+
.map(|msg| TokenCounter::count_tokens(&msg.0.content))
1938+
.sum::<usize>();
1939+
1940+
let assistant_token_count = assistant_messages
1941+
.iter()
1942+
.map(|msg| TokenCounter::count_tokens(&msg.content))
1943+
.sum::<usize>();
1944+
1945+
let user_token_count = user_messages
1946+
.iter()
1947+
.map(|msg| TokenCounter::count_tokens(&msg.content))
1948+
.sum::<usize>();
1949+
1950+
let total_token_used: usize = context_token_count + assistant_token_count + user_token_count;
1951+
1952+
let window_width = self.terminal_width();
1953+
let progress_bar_width = std::cmp::min(window_width, 80); // set a max width for the progress bar for better aesthetic
1954+
1955+
let context_width =
1956+
((context_token_count as f64 / CONTEXT_WINDOW_SIZE as f64) * progress_bar_width as f64) as usize;
1957+
let assistant_width =
1958+
((assistant_token_count as f64 / CONTEXT_WINDOW_SIZE as f64) * progress_bar_width as f64) as usize;
1959+
let user_width =
1960+
((user_token_count as f64 / CONTEXT_WINDOW_SIZE as f64) * progress_bar_width as f64) as usize;
1961+
1962+
let left_over_width = progress_bar_width
1963+
- std::cmp::min(context_width + assistant_width + user_width, progress_bar_width);
1964+
1965+
queue!(
1966+
self.output,
1967+
style::Print(format!(
1968+
"\nCurrent context window ({} of {}k tokens used)\n",
1969+
total_token_used,
1970+
CONTEXT_WINDOW_SIZE / 1000
1971+
)),
1972+
style::SetForegroundColor(Color::DarkCyan),
1973+
// add a nice visual to mimic "tiny" progress, so the overral progress bar doesn't look too empty
1974+
style::Print("|".repeat(if context_width == 0 && context_token_count > 0 {
1975+
1
1976+
} else {
1977+
0
1978+
})),
1979+
style::Print("█".repeat(context_width)),
1980+
style::SetForegroundColor(Color::Blue),
1981+
style::Print("|".repeat(if assistant_width == 0 && assistant_token_count > 0 {
1982+
1
1983+
} else {
1984+
0
1985+
})),
1986+
style::Print("█".repeat(assistant_width)),
1987+
style::SetForegroundColor(Color::Magenta),
1988+
style::Print("|".repeat(if user_width == 0 && user_token_count > 0 { 1 } else { 0 })),
1989+
style::Print("█".repeat(user_width)),
1990+
style::SetForegroundColor(Color::DarkGrey),
1991+
style::Print("█".repeat(left_over_width)),
1992+
style::Print(" "),
1993+
style::SetForegroundColor(Color::Reset),
1994+
style::Print(format!(
1995+
"{:.2}%",
1996+
(total_token_used as f32 / CONTEXT_WINDOW_SIZE as f32) * 100.0
1997+
)),
1998+
)?;
1999+
2000+
queue!(self.output, style::Print("\n\n"))?;
2001+
self.output.flush()?;
2002+
2003+
queue!(
2004+
self.output,
2005+
style::SetForegroundColor(Color::DarkCyan),
2006+
style::Print("█ Context files: "),
2007+
style::SetForegroundColor(Color::Reset),
2008+
style::Print(format!(
2009+
"~{} tokens ({:.2}%)\n",
2010+
context_token_count,
2011+
(context_token_count as f32 / CONTEXT_WINDOW_SIZE as f32) * 100.0
2012+
)),
2013+
style::SetForegroundColor(Color::Blue),
2014+
style::Print("█ Q responses: "),
2015+
style::SetForegroundColor(Color::Reset),
2016+
style::Print(format!(
2017+
" ~{} tokens ({:.2}%)\n",
2018+
assistant_token_count,
2019+
(assistant_token_count as f32 / CONTEXT_WINDOW_SIZE as f32) * 100.0
2020+
)),
2021+
style::SetForegroundColor(Color::Magenta),
2022+
style::Print("█ Your prompts: "),
2023+
style::SetForegroundColor(Color::Reset),
2024+
style::Print(format!(
2025+
" ~{} tokens ({:.2}%)\n\n",
2026+
user_token_count,
2027+
(user_token_count as f32 / CONTEXT_WINDOW_SIZE as f32) * 100.0
2028+
)),
2029+
)?;
2030+
2031+
queue!(
2032+
self.output,
2033+
style::SetAttribute(Attribute::Bold),
2034+
style::Print("\n💡 Pro Tips:\n"),
2035+
style::SetAttribute(Attribute::Reset),
2036+
style::SetForegroundColor(Color::DarkGrey),
2037+
style::Print("Run "),
2038+
style::SetForegroundColor(Color::DarkGreen),
2039+
style::Print("/compact"),
2040+
style::SetForegroundColor(Color::DarkGrey),
2041+
style::Print(" to replace the conversation history with its summary\n"),
2042+
style::Print("Run "),
2043+
style::SetForegroundColor(Color::DarkGreen),
2044+
style::Print("/clear"),
2045+
style::SetForegroundColor(Color::DarkGrey),
2046+
style::Print(" to erase the entire chat history\n"),
2047+
style::Print("Run "),
2048+
style::SetForegroundColor(Color::DarkGreen),
2049+
style::Print("/context show"),
2050+
style::SetForegroundColor(Color::DarkGrey),
2051+
style::Print(" to see tokens per context file\n\n"),
2052+
style::SetForegroundColor(Color::Reset),
2053+
)?;
2054+
19022055
ChatState::PromptUser {
19032056
tool_uses: Some(tool_uses),
19042057
pending_tool_index,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ pub const COMMANDS: &[&str] = &[
6767
"/compact",
6868
"/compact help",
6969
"/compact --summary",
70+
"/usage",
7071
];
7172

7273
pub fn generate_prompt(current_profile: Option<&str>, warning: bool) -> String {

crates/q_cli/src/cli/chat/summarization_state.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ use std::collections::VecDeque;
22

33
use fig_api_client::model::ChatMessage;
44

5+
use crate::util::token_counter::TokenCounter;
6+
57
/// Character count warning levels for conversation size
68
#[derive(Debug, Clone, PartialEq, Eq)]
79
pub enum TokenWarningLevel {
810
/// No warning, conversation is within normal limits
911
None,
10-
/// Critical level - at single warning threshold (500K characters)
12+
/// Critical level - at single warning threshold (600K characters)
1113
Critical,
1214
}
1315

1416
/// Constants for character-based warning threshold
15-
pub const MAX_CHARS: usize = 500000; // Character-based warning threshold
17+
pub const CONTEXT_WINDOW_SIZE: usize = 200_000; // tokens
18+
pub const MAX_CHARS: usize = TokenCounter::token_to_chars(CONTEXT_WINDOW_SIZE); // Character-based warning threshold
1619

1720
/// State for tracking summarization process
1821
#[derive(Debug, Clone)]

crates/q_cli/src/util/token_counter.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
pub struct TokenCounter;
22

33
impl TokenCounter {
4+
pub const TOKEN_TO_CHAR_RATIO: usize = 3;
5+
46
/// Estimates the number of tokens in the input content.
5-
/// Currently uses a simple heuristic: content length / 3
7+
/// Currently uses a simple heuristic: content length / TOKEN_TO_CHAR_RATIO
68
///
79
/// Rounds up to the nearest multiple of 10 to avoid giving users a false sense of precision.
810
pub fn count_tokens(content: &str) -> usize {
9-
(content.len() / 3 + 5) / 10 * 10
11+
(content.len() / Self::TOKEN_TO_CHAR_RATIO + 5) / 10 * 10
12+
}
13+
14+
pub const fn token_to_chars(token: usize) -> usize {
15+
token * Self::TOKEN_TO_CHAR_RATIO
1016
}
1117
}
1218

0 commit comments

Comments
 (0)