Skip to content

Commit 3e8b31e

Browse files
committed
added tui improvements
1 parent b6644fb commit 3e8b31e

5 files changed

Lines changed: 189 additions & 24 deletions

File tree

rust_tui_coder/src/agent.rs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,7 +1066,7 @@ TOOL: {"name": "RUN_COMMAND", "parameters": {"command": "cargo build --release"}
10661066
None
10671067
}
10681068

1069-
pub async fn run(&mut self, config: &LlmConfig, user_prompt: String) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> {
1069+
pub async fn run(&mut self, config: &LlmConfig, user_prompt: String, app: &mut crate::app::App) -> Result<(String, Vec<String>), Box<dyn std::error::Error>> {
10701070
// Add system message if this is the first interaction
10711071
if self.messages.is_empty() {
10721072
self.messages.push(Message {
@@ -1116,7 +1116,9 @@ TOOL: {"name": "RUN_COMMAND", "parameters": {"command": "cargo build --release"}
11161116
attempts += 1;
11171117

11181118
// Get response from LLM
1119-
let response = llm::ask_llm_with_messages(config, &self.messages).await?;
1119+
let (response, tokens_used) = llm::ask_llm_with_messages(config, &self.messages).await?;
1120+
app.increment_tokens(tokens_used);
1121+
app.increment_requests();
11201122

11211123
// Check if response contains a tool call
11221124
if let Some(tool) = self.parse_tool_call(&response) {
@@ -1152,6 +1154,7 @@ TOOL: {"name": "RUN_COMMAND", "parameters": {"command": "cargo build --release"}
11521154
// Execute the tool
11531155
let tool_result = match tool.execute() {
11541156
Ok(result) => {
1157+
app.increment_tools_executed();
11551158
tool_logs.push(format!("✅ Success: {}", result));
11561159
result
11571160
}
@@ -1178,12 +1181,15 @@ TOOL: {"name": "RUN_COMMAND", "parameters": {"command": "cargo build --release"}
11781181
// Check if we should continue or if the task is complete
11791182
if attempts >= MAX_ATTEMPTS {
11801183
// Get final response after max attempts
1181-
let final_response = llm::ask_llm_with_messages(config, &self.messages).await?;
1184+
let (final_response, final_tokens) = llm::ask_llm_with_messages(config, &self.messages).await?;
1185+
app.increment_tokens(final_tokens);
1186+
app.increment_requests();
1187+
11821188
self.messages.push(Message {
11831189
role: "assistant".to_string(),
11841190
content: final_response.clone(),
11851191
});
1186-
1192+
11871193
all_tool_logs.push(format!("⚠️ Reached maximum attempts ({})", MAX_ATTEMPTS));
11881194
return Ok((final_response, all_tool_logs));
11891195
}

rust_tui_coder/src/app.rs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,75 @@ pub struct App {
55
pub tool_logs: Vec<String>,
66
pub is_executing_tool: bool,
77
pub current_tool: String,
8+
// Usage tracking
9+
pub session_start_time: std::time::Instant,
10+
pub tokens_used: u64,
11+
pub total_requests: u64,
12+
pub total_tools_executed: u64,
813
}
914

1015
impl App {
1116
pub fn new() -> Self {
1217
Self {
1318
user_input: String::new(),
1419
conversation: Vec::new(),
15-
status_message: "Type in /quit to exit".to_string(),
20+
status_message: "Commands: /quit (exit), /stats (usage stats)".to_string(),
1621
tool_logs: Vec::new(),
1722
is_executing_tool: false,
1823
current_tool: String::new(),
24+
// Initialize usage tracking
25+
session_start_time: std::time::Instant::now(),
26+
tokens_used: 0,
27+
total_requests: 0,
28+
total_tools_executed: 0,
1929
}
2030
}
2131

2232
pub fn add_tool_log(&mut self, log: String) {
2333
self.tool_logs.push(log);
2434
}
35+
36+
// Usage tracking methods
37+
pub fn increment_tokens(&mut self, tokens: u64) {
38+
self.tokens_used += tokens;
39+
}
40+
41+
pub fn increment_requests(&mut self) {
42+
self.total_requests += 1;
43+
}
44+
45+
pub fn increment_tools_executed(&mut self) {
46+
self.total_tools_executed += 1;
47+
}
48+
49+
pub fn get_session_duration(&self) -> std::time::Duration {
50+
self.session_start_time.elapsed()
51+
}
52+
53+
pub fn get_usage_summary(&self) -> String {
54+
let duration = self.get_session_duration();
55+
let hours = duration.as_secs() / 3600;
56+
let minutes = (duration.as_secs() % 3600) / 60;
57+
let seconds = duration.as_secs() % 60;
58+
59+
format!(
60+
"Session Summary:\n\
61+
• Duration: {:02}:{:02}:{:02}\n\
62+
• Tokens Used: {}\n\
63+
• LLM Requests: {}\n\
64+
• Tools Executed: {}\n\
65+
• Average Tokens/Request: {}",
66+
hours,
67+
minutes,
68+
seconds,
69+
self.tokens_used,
70+
self.total_requests,
71+
self.total_tools_executed,
72+
if self.total_requests > 0 {
73+
self.tokens_used / self.total_requests
74+
} else {
75+
0
76+
}
77+
)
78+
}
2579
}

rust_tui_coder/src/llm.rs

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ use crate::config::LlmConfig;
44
use std::error::Error;
55
use std::fmt;
66

7+
// Token estimation function (rough approximation based on GPT tokenization)
8+
pub fn estimate_token_count(text: &str) -> u64 {
9+
// Rough approximation: ~4 characters per token for English text
10+
// This is not exact but provides a reasonable estimate
11+
let char_count = text.chars().count() as f64;
12+
let estimated_tokens = (char_count / 4.0).ceil() as u64;
13+
14+
// Ensure minimum of 1 token for non-empty text
15+
if estimated_tokens == 0 && !text.trim().is_empty() {
16+
1
17+
} else {
18+
estimated_tokens
19+
}
20+
}
21+
722
#[derive(Debug)]
823
pub enum LlmError {
924
RequestFailed(reqwest::Error),
@@ -61,10 +76,17 @@ struct Choice {
6176
message: Message,
6277
}
6378

64-
pub async fn ask_llm_with_messages(config: &LlmConfig, messages: &[Message]) -> Result<String, LlmError> {
79+
pub async fn ask_llm_with_messages(config: &LlmConfig, messages: &[Message]) -> Result<(String, u64), LlmError> {
6580
let client = Client::new();
6681
let _provider = config.provider.as_deref().unwrap_or("openai");
6782

83+
// Calculate input tokens
84+
let mut input_tokens = 0u64;
85+
for message in messages {
86+
input_tokens += estimate_token_count(&message.role);
87+
input_tokens += estimate_token_count(&message.content);
88+
}
89+
6890
// Determine model to use (support AUTODETECT from /models endpoint for OpenAI-compatible APIs)
6991
let model_to_use = if config.model_name.eq_ignore_ascii_case("AUTODETECT") || config.model_name.trim().is_empty() {
7092
// Fetch available models and use the first one
@@ -133,9 +155,15 @@ pub async fn ask_llm_with_messages(config: &LlmConfig, messages: &[Message]) ->
133155
match serde_json::from_str::<ChatCompletionResponse>(&response_text) {
134156
Ok(parsed_response) => {
135157
if let Some(choice) = parsed_response.choices.into_iter().next() {
136-
Ok(choice.message.content)
158+
let response_content = choice.message.content;
159+
let output_tokens = estimate_token_count(&response_content);
160+
let total_tokens = input_tokens + output_tokens;
161+
Ok((response_content, total_tokens))
137162
} else {
138-
Ok("No response content available.".to_string())
163+
let response_content = "No response content available.".to_string();
164+
let output_tokens = estimate_token_count(&response_content);
165+
let total_tokens = input_tokens + output_tokens;
166+
Ok((response_content, total_tokens))
139167
}
140168
}
141169
Err(e) => {

rust_tui_coder/src/main.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,29 @@ async fn run_app<B: Backend>(
7373

7474
// Check for quit command
7575
if user_input.trim() == "/quit" {
76+
// Display usage summary before quitting
77+
let summary = app.get_usage_summary();
78+
app.conversation.push(format!("System: {}", summary));
79+
terminal.draw(|f| ui::ui(f, &app))?;
80+
81+
// Give user a moment to see the summary
82+
std::thread::sleep(std::time::Duration::from_secs(2));
7683
return Ok(());
7784
}
85+
86+
// Check for stats command
87+
if user_input.trim() == "/stats" {
88+
let summary = app.get_usage_summary();
89+
app.conversation.push(format!("System: {}", summary));
90+
continue;
91+
}
7892

7993
app.conversation.push(format!("User: {}", user_input));
8094

8195
app.status_message = "Thinking...".to_string();
8296
terminal.draw(|f| ui::ui(f, &app))?;
8397

84-
match agent.run(&config.llm, user_input).await {
98+
match agent.run(&config.llm, user_input, &mut app).await {
8599
Ok((response, tool_logs)) => {
86100
// Add tool logs to the app
87101
for log in tool_logs {

rust_tui_coder/src/ui.rs

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,86 @@ use ratatui::{
77
};
88
use crate::app::App;
99

10-
// Helper function to wrap text to fit within a given width
10+
// Enhanced helper function to wrap text to fit within a given width
1111
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
12-
let words: Vec<&str> = text.split_whitespace().collect();
12+
if max_width == 0 {
13+
return vec![text.to_string()];
14+
}
15+
1316
let mut lines = Vec::new();
1417
let mut current_line = String::new();
15-
16-
for word in words {
17-
if current_line.len() + word.len() + 1 <= max_width {
18-
if !current_line.is_empty() {
19-
current_line.push(' ');
20-
}
21-
current_line.push_str(word);
22-
} else {
18+
19+
// Handle empty text
20+
if text.trim().is_empty() {
21+
return vec![text.to_string()];
22+
}
23+
24+
for line in text.lines() {
25+
if line.trim().is_empty() {
26+
// Handle empty lines by preserving them
2327
if !current_line.is_empty() {
2428
lines.push(current_line.clone());
29+
current_line.clear();
30+
}
31+
lines.push(String::new());
32+
continue;
33+
}
34+
35+
let words: Vec<&str> = line.split_whitespace().collect();
36+
37+
for word in words {
38+
// Handle words longer than max_width by breaking them
39+
if word.len() > max_width {
40+
if !current_line.is_empty() {
41+
lines.push(current_line.clone());
42+
current_line.clear();
43+
}
44+
// Break long word into chunks
45+
let mut remaining_word = word;
46+
while !remaining_word.is_empty() {
47+
let chunk_size = std::cmp::min(max_width, remaining_word.len());
48+
let chunk = &remaining_word[..chunk_size];
49+
lines.push(chunk.to_string());
50+
remaining_word = &remaining_word[chunk_size..];
51+
}
52+
continue;
53+
}
54+
55+
// Check if word fits on current line
56+
let space_needed = if current_line.is_empty() { word.len() } else { current_line.len() + 1 + word.len() };
57+
58+
if space_needed <= max_width {
59+
if !current_line.is_empty() {
60+
current_line.push(' ');
61+
}
62+
current_line.push_str(word);
63+
} else {
64+
// Word doesn't fit, start new line
65+
if !current_line.is_empty() {
66+
lines.push(current_line.clone());
67+
current_line.clear();
68+
}
69+
current_line.push_str(word);
2570
}
26-
current_line = word.to_string();
71+
}
72+
73+
// Add the current line if it's not empty
74+
if !current_line.is_empty() {
75+
lines.push(current_line.clone());
76+
current_line.clear();
2777
}
2878
}
29-
79+
80+
// Handle case where we have pending content
3081
if !current_line.is_empty() {
3182
lines.push(current_line);
3283
}
33-
84+
85+
// Ensure we always return at least the original text if no wrapping occurred
3486
if lines.is_empty() {
3587
lines.push(text.to_string());
3688
}
37-
89+
3890
lines
3991
}
4092

@@ -152,8 +204,19 @@ pub fn ui(f: &mut Frame, app: &App) {
152204
.position(tool_scroll_position);
153205
f.render_stateful_widget(tool_scrollbar, chunks[1], &mut tool_scrollbar_state);
154206

207+
// Enhanced input field with text wrapping
155208
let input_block = Block::default().title("Input").borders(Borders::ALL);
156-
let input = Paragraph::new(app.user_input.as_str())
209+
let input_max_width = chunks[2].width.saturating_sub(4) as usize; // Account for borders and padding
210+
211+
// Wrap the input text for better display
212+
let wrapped_input = if app.user_input.is_empty() {
213+
vec![String::new()]
214+
} else {
215+
wrap_text(&app.user_input, input_max_width)
216+
};
217+
218+
let input_text = wrapped_input.join("\n");
219+
let input = Paragraph::new(input_text)
157220
.block(input_block);
158221
f.render_widget(input, chunks[2]);
159222

0 commit comments

Comments
 (0)