Skip to content

Commit 6f465ad

Browse files
committed
feat: add token tracking for local and cloud requests; update UI to display token usage
1 parent 46152f2 commit 6f465ad

File tree

3 files changed

+111
-46
lines changed

3 files changed

+111
-46
lines changed

crates/agentic-tui/src/ui/app.rs

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ pub struct App {
125125
cloud_response: Option<AtomicNote>,
126126
synthesis_scroll: u16,
127127
coaching_tip: (String, String),
128+
local_tokens_used: u32, // Token count for current local request
129+
cloud_tokens_used: u32, // Token count for current cloud request
128130
}
129131

130132
impl App {
@@ -154,6 +156,8 @@ impl App {
154156
cloud_response: None,
155157
synthesis_scroll: 0,
156158
coaching_tip: (String::new(), String::new()),
159+
local_tokens_used: 0,
160+
cloud_tokens_used: 0,
157161
}
158162
}
159163

@@ -345,6 +349,8 @@ impl App {
345349
&self.theme,
346350
self.agent_status,
347351
&self.settings,
352+
self.local_tokens_used,
353+
self.cloud_tokens_used,
348354
);
349355
render_footer(
350356
frame,
@@ -473,10 +479,10 @@ impl App {
473479
.alignment(ratatui::prelude::Alignment::Center)
474480
};
475481

476-
// Create a centered area for the synthesis (70% width, centered vertically)
482+
// Create a compact area for the synthesis (60% width, ~12 lines height, centered)
477483
let main_area = app_chunks[1];
478-
let synthesis_width = (main_area.width * 70 / 100).max(40).min(main_area.width);
479-
let synthesis_height = 10; // Fixed height for the synthesis box
484+
let synthesis_width = (main_area.width * 60 / 100).max(40).min(80);
485+
let synthesis_height = 12.min(main_area.height - 6);
480486

481487
let synthesis_area = Rect::new(
482488
main_area.x + (main_area.width.saturating_sub(synthesis_width)) / 2,
@@ -655,6 +661,9 @@ impl App {
655661
if self.agent_status == AgentStatus::ValidatingCloud {
656662
self.agent_status = AgentStatus::Ready;
657663
self.start_agent_services();
664+
// Automatically enter chat mode after successful validation
665+
self.mode = AppMode::Chat;
666+
self.edit_buffer.clear();
658667
}
659668
}
660669

@@ -904,6 +913,8 @@ impl App {
904913
self.proposals.clear();
905914
self.current_proposal_index = 0;
906915
self.original_user_query.clear();
916+
self.local_tokens_used = 0;
917+
self.cloud_tokens_used = 0;
907918
}
908919
_ => {}
909920
},
@@ -919,6 +930,8 @@ impl App {
919930
self.cloud_response = None;
920931
self.synthesis_scroll = 0;
921932
self.agent_status = AgentStatus::Ready;
933+
self.local_tokens_used = 0;
934+
self.cloud_tokens_used = 0;
922935
}
923936
KeyCode::Down => {
924937
// Discard synthesis (negative action)
@@ -931,10 +944,18 @@ impl App {
931944
self.synthesis_scroll = 0;
932945
self.agent_status = AgentStatus::Ready;
933946
self.edit_buffer.clear();
947+
self.local_tokens_used = 0;
948+
self.cloud_tokens_used = 0;
934949
}
935-
KeyCode::Left | KeyCode::Right => {
936-
// Keep horizontal scrolling for long content
937-
// Currently no horizontal scroll implemented
950+
KeyCode::Left => {
951+
// Scroll up through synthesis content
952+
if self.synthesis_scroll > 0 {
953+
self.synthesis_scroll -= 1;
954+
}
955+
}
956+
KeyCode::Right => {
957+
// Scroll down through synthesis content
958+
self.synthesis_scroll += 1;
938959
}
939960
KeyCode::Enter | KeyCode::Esc => {
940961
// Fallback: return to normal without saving
@@ -973,6 +994,10 @@ impl App {
973994
// Store the original user query for metadata
974995
self.original_user_query = message.clone();
975996

997+
// Estimate tokens for local request (rough: chars/4 + prompt overhead)
998+
self.local_tokens_used = (message.len() / 4) as u32 + 500; // ~500 tokens for prompt template
999+
self.cloud_tokens_used = 0; // Reset cloud tokens for new session
1000+
9761001
self.agent_status = AgentStatus::Orchestrating;
9771002
let settings = self.settings.clone();
9781003
let tx = self.agent_tx.clone();
@@ -1016,17 +1041,37 @@ impl App {
10161041
// Use proposal text directly since the new prompt ensures proper format
10171042
let clean_proposal = proposal_text;
10181043

1044+
// Get model names for usage metadata
1045+
let local_model = if self.settings.local_model.is_empty() || self.settings.local_model == "[SELECT]" {
1046+
"unknown"
1047+
} else {
1048+
&self.settings.local_model
1049+
};
1050+
1051+
let cloud_model = if self.settings.cloud_model.is_empty() || self.settings.cloud_model == "[SELECT]" {
1052+
"anthropic/claude-3.5-sonnet"
1053+
} else {
1054+
&self.settings.cloud_model
1055+
};
1056+
1057+
// Estimate token breakdown (rough estimates)
1058+
let local_prompt_tokens = (self.original_user_query.len() / 4) as u32 + 200; // Query + template
1059+
let local_completion_tokens = self.local_tokens_used.saturating_sub(local_prompt_tokens);
1060+
let cloud_prompt_tokens = (self.final_prompt.len() / 4) as u32 + 150; // Proposal + synthesis template
1061+
let cloud_completion_tokens = self.cloud_tokens_used.saturating_sub(cloud_prompt_tokens);
1062+
10191063
let markdown_content = format!(
1020-
"---\ndate: {}\nprovider: \"OPENROUTER\"\nmodel: \"{}\"\nquery: \"{}\"\nproposal: \"{}\"\ntags: [{}]\n---\n\n# {}\n\n{}\n",
1064+
"---\ndate: {}\nprovider: \"OPENROUTER\"\nquery: \"{}\"\nproposal: \"{}\"\ntags: [{}]\n\nusage:\n local_model: \"{}\"\n local_prompt_tokens: {}\n local_completion_tokens: {}\n cloud_model: \"{}\"\n cloud_prompt_tokens: {}\n cloud_completion_tokens: {}\n---\n\n# {}\n\n{}\n",
10211065
chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"),
1022-
if self.settings.cloud_model.is_empty() || self.settings.cloud_model == "[SELECT]" {
1023-
"anthropic/claude-3.5-sonnet"
1024-
} else {
1025-
&self.settings.cloud_model
1026-
},
10271066
self.original_user_query.replace("\"", "\\\""),
10281067
clean_proposal.replace("\"", "\\\""),
10291068
note.header_tags.iter().map(|tag| format!("\"{}\"", tag)).collect::<Vec<_>>().join(", "),
1069+
local_model,
1070+
local_prompt_tokens,
1071+
local_completion_tokens,
1072+
cloud_model,
1073+
cloud_prompt_tokens,
1074+
cloud_completion_tokens,
10301075
note.header_tags.join(" • "),
10311076
note.body_text
10321077
);
@@ -1048,6 +1093,9 @@ impl App {
10481093
fn handle_cloud_synthesis(&mut self) {
10491094
// Set status to searching and trigger cloud API call
10501095
self.agent_status = AgentStatus::Searching;
1096+
1097+
// Estimate tokens for cloud request (prompt + synthesis template)
1098+
self.cloud_tokens_used = (self.final_prompt.len() / 4) as u32 + 300; // ~300 tokens for synthesis template
10511099

10521100
let prompt = self.final_prompt.clone();
10531101
let api_key = self.settings.api_key.clone();

crates/agentic-tui/src/ui/footer.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ pub fn render_footer(
2121

2222
let content = match mode {
2323
AppMode::Complete => {
24-
// Save/Discard navigation for synthesis results
24+
// Save/Discard navigation for synthesis results with scroll controls
2525
Line::from(vec![
2626
Span::raw("[↑] "),
27-
Span::styled("Save Synthesis", theme.ratatui_style(Element::Accent)),
27+
Span::styled("Save", theme.ratatui_style(Element::Accent)),
2828
Span::raw(" | "),
2929
Span::raw("[↓] "),
30-
Span::styled(
31-
"Discard & New Query",
32-
theme.ratatui_style(Element::Inactive),
33-
),
30+
Span::styled("Discard", theme.ratatui_style(Element::Inactive)),
31+
Span::raw(" | "),
32+
Span::raw("[←→] "),
33+
Span::styled("Scroll", theme.ratatui_style(Element::Text)),
3434
])
3535
.alignment(Alignment::Center)
3636
}

crates/agentic-tui/src/ui/header.rs

Lines changed: 45 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ pub fn render_header(
1616
theme: &Theme,
1717
status: AgentStatus,
1818
settings: &Settings,
19+
local_tokens: u32,
20+
cloud_tokens: u32,
1921
) {
2022
// Dynamic title based on what's actually configured - much smarter!
2123
let title = Title::from(" Agentic v0.1.0 ").alignment(Alignment::Left);
2224

23-
let (status_text, status_color) = build_smart_status_with_color(status, settings);
25+
let (status_text, status_color) = build_smart_status_with_color(status, settings, local_tokens, cloud_tokens);
2426

2527
let status_span = Span::styled(status_text, Style::default().fg(status_color));
2628

@@ -37,8 +39,8 @@ pub fn render_header(
3739
frame.render_widget(header_paragraph, area);
3840
}
3941

40-
fn build_smart_status_with_color(status: AgentStatus, settings: &Settings) -> (String, Color) {
41-
// Show actual configuration state - much smarter than generic messages!
42+
fn build_smart_status_with_color(status: AgentStatus, settings: &Settings, local_tokens: u32, cloud_tokens: u32) -> (String, Color) {
43+
// Show actual configuration state with model names always visible
4244
let local_configured = settings.local_model != "[SELECT]";
4345
let cloud_configured =
4446
settings.cloud_model != "[SELECT]" && settings.api_key != "sk-or-v1-982...b52";
@@ -55,59 +57,74 @@ fn build_smart_status_with_color(status: AgentStatus, settings: &Settings) -> (S
5557
"NOT-READY"
5658
};
5759

60+
// Helper function to format token count for display
61+
let format_single_tokens = |tokens: u32| -> String {
62+
if tokens < 1000 {
63+
format!("{}", tokens)
64+
} else {
65+
format!("{:.1}k", tokens as f32 / 1000.0)
66+
}
67+
};
68+
69+
let format_token_display = |local: u32, cloud: u32, status: AgentStatus| -> String {
70+
match status {
71+
AgentStatus::Orchestrating if local > 0 => {
72+
format!(" | ({})", format_single_tokens(local))
73+
}
74+
AgentStatus::Searching if local > 0 && cloud > 0 => {
75+
format!(" | ({}) + ({})", format_single_tokens(local), format_single_tokens(cloud))
76+
}
77+
AgentStatus::Complete if local > 0 => {
78+
let total = local + cloud;
79+
if cloud > 0 {
80+
format!(" | ({})", format_single_tokens(total))
81+
} else {
82+
format!(" | ({})", format_single_tokens(local))
83+
}
84+
}
85+
_ => String::new()
86+
}
87+
};
88+
5889
let text = match status {
5990
AgentStatus::Ready => {
60-
// Both configured - show actual model names
6191
format!("Ruixen :: {} :: {}", local_display, cloud_display)
6292
}
6393
AgentStatus::NotReady => {
64-
// Default state - show what we have so far
6594
format!("Ruixen :: {} :: {}", local_display, cloud_display)
6695
}
6796
AgentStatus::CheckLocalModel => {
68-
// Highlight the local model issue
6997
format!("Ruixen :: [CONFIGURE LOCAL] :: {}", cloud_display)
7098
}
7199
AgentStatus::CheckCloudModel => {
72-
// Highlight the cloud model issue
73100
format!("Ruixen :: {} :: [CONFIGURE CLOUD]", local_display)
74101
}
75102
AgentStatus::CheckApiKey => {
76-
// Highlight the API key issue
77103
format!("Ruixen :: {} :: [CONFIGURE API KEY]", local_display)
78104
}
79105
AgentStatus::ValidatingLocal => {
80-
format!(
81-
"Ruixen :: [CHECKING {}] :: {}",
82-
local_display, cloud_display
83-
)
106+
format!("Ruixen :: [CHECKING {}] :: {}", local_display, cloud_display)
84107
}
85108
AgentStatus::ValidatingCloud => {
86-
format!(
87-
"Ruixen :: {} :: [CHECKING {}]",
88-
local_display, cloud_display
89-
)
109+
format!("Ruixen :: {} :: [CHECKING {}]", local_display, cloud_display)
90110
}
91111
AgentStatus::LocalEndpointError => {
92-
format!(
93-
"Ruixen :: [ERROR: {} UNREACHABLE] :: {}",
94-
local_display, cloud_display
95-
)
112+
format!("Ruixen :: [ERROR: {} UNREACHABLE] :: {}", local_display, cloud_display)
96113
}
97114
AgentStatus::CloudEndpointError => {
98-
format!(
99-
"Ruixen :: {} :: [ERROR: {} UNREACHABLE]",
100-
local_display, cloud_display
101-
)
115+
format!("Ruixen :: {} :: [ERROR: {} UNREACHABLE]", local_display, cloud_display)
102116
}
103117
AgentStatus::Orchestrating => {
104-
format!("Ruixen :: [ORCHESTRATING] :: {}", cloud_display)
118+
// Show local tokens during orchestration
119+
format!("Ruixen :: {} :: {}{}", local_display, cloud_display, format_token_display(local_tokens, cloud_tokens, status))
105120
}
106121
AgentStatus::Searching => {
107-
format!("Ruixen :: ☁️🔍 :: [SEARCHING] :: {}", cloud_display)
122+
// Show local + cloud tokens during synthesis
123+
format!("Ruixen :: {} :: {}{}", local_display, cloud_display, format_token_display(local_tokens, cloud_tokens, status))
108124
}
109125
AgentStatus::Complete => {
110-
format!("Ruixen :: ✨ :: [SYNTHESIS COMPLETE] :: {}", cloud_display)
126+
// Show total bill (red color applied later)
127+
format!("Ruixen :: {} :: {}{}", local_display, cloud_display, format_token_display(local_tokens, cloud_tokens, status))
111128
}
112129
};
113130

@@ -124,7 +141,7 @@ fn build_smart_status_with_color(status: AgentStatus, settings: &Settings) -> (S
124141
AgentStatus::ValidatingLocal | AgentStatus::ValidatingCloud => Color::Yellow, // Testing in progress
125142
AgentStatus::Orchestrating => Color::Cyan,
126143
AgentStatus::Searching => Color::Blue, // Cloud synthesis in progress
127-
AgentStatus::Complete => Color::Green, // Success!
144+
AgentStatus::Complete => Color::Green, // Success! (token styling handled separately)
128145
AgentStatus::LocalEndpointError | AgentStatus::CloudEndpointError => Color::Red, // Connection failed
129146
_ => Color::Red, // Other validation failed
130147
};

0 commit comments

Comments
 (0)