diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index c9fb0edf..ff7605dd 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -7216,9 +7216,6 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { match result { Ok(()) => { app.set_status("Done"); - if let Ok(mut ds) = dashboard_state.write() { - ds.status_message.clear(); - } } Err(e) => { app.push_system_message(&format!("Error: {e}")); @@ -7235,7 +7232,8 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { break; } tui::TuiReadOutcome::ProviderSwap => { - let _ = app.restore_terminal(); + // Stay in alt screen, disable raw mode for wizard prompts + app.suspend()?; setup_wizard::run_setup_wizard()?; let cwd = std::env::current_dir().unwrap_or_default(); let config = runtime::ConfigLoader::default_for(&cwd).load().ok(); @@ -7244,9 +7242,9 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { { let _ = cli.set_model(Some(new_model)); } - app.push_system_message("Provider updated — restart for full effect"); - // Re-enter TUI - let _ = app.suspend(); + // Re-enter TUI — re-enables raw mode, redraws + app.resume()?; + app.push_system_message("Provider updated"); } tui::TuiReadOutcome::TeamToggle => { let current = std::env::var("CLAWD_AGENT_TEAMS").unwrap_or_default(); @@ -7270,20 +7268,30 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box> { fn update_dashboard(state: &tui::SharedDashboardState, cli: &LiveCli) { let mut session_id = None; let mut turn_count = 0u32; - let mut total_chars = 0usize; + let mut input_tokens = 0u32; + let mut output_tokens = 0u32; + let mut cache_creation_tokens = 0u32; + let mut cache_read_tokens = 0u32; + let mut cost_usd = 0.0f64; if let Some(rt) = cli.runtime.runtime.as_ref() { let session = rt.session(); session_id = Some(session.session_id.clone()); turn_count = session.messages.len() as u32; - total_chars = session.messages.iter().map(|m| { - m.blocks.iter().map(|b| match b { - runtime::ContentBlock::Text { text } => text.len(), - runtime::ContentBlock::ToolUse { input, .. } => input.to_string().len(), - runtime::ContentBlock::ToolResult { output, .. } => output.len(), - _ => 0, - }).sum::() - }).sum(); + + // Aggregate real token usage from all messages + let tracker = runtime::UsageTracker::from_session(session); + let cumulative = tracker.cumulative_usage(); + input_tokens = cumulative.input_tokens; + output_tokens = cumulative.output_tokens; + cache_creation_tokens = cumulative.cache_creation_input_tokens; + cache_read_tokens = cumulative.cache_read_input_tokens; + + // Estimate cost using runtime's pricing model + let pricing = runtime::pricing_for_model(&cli.model) + .unwrap_or_else(runtime::ModelPricing::default_sonnet_tier); + let cost = cumulative.estimate_cost_usd_with_pricing(pricing); + cost_usd = cost.total_cost_usd(); } let update = tui_update::DashboardUpdate { @@ -7291,7 +7299,11 @@ fn update_dashboard(state: &tui::SharedDashboardState, cli: &LiveCli) { permission_mode: &format!("{:?}", cli.permission_mode), session_id: session_id.as_deref(), turn_count, - total_chars, + input_tokens, + output_tokens, + cache_creation_tokens, + cache_read_tokens, + cost_usd, provider: std::env::var("CLAWD_PROVIDER").unwrap_or_else(|_| "anthropic".to_string()), provider_url: std::env::var("ANTHROPIC_BASE_URL") .or_else(|_| std::env::var("OPENAI_BASE_URL")) diff --git a/rust/crates/rusty-claude-cli/src/tui.rs b/rust/crates/rusty-claude-cli/src/tui.rs index 12eddf79..c7c5d68e 100644 --- a/rust/crates/rusty-claude-cli/src/tui.rs +++ b/rust/crates/rusty-claude-cli/src/tui.rs @@ -147,6 +147,7 @@ pub struct TuiApp { } const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; +const MAX_CONVERSATION_LINES: usize = 10_000; /// Key-column width for dashboard `kv()` rows. Values start at this /// column so "Model", "Compactions" etc. all line up. @@ -337,6 +338,17 @@ impl TuiApp { } fn auto_scroll(&mut self) { + // Trim conversation to prevent unbounded memory growth + if self.conversation.len() > MAX_CONVERSATION_LINES { + let drain_count = self.conversation.len() - MAX_CONVERSATION_LINES; + self.conversation.drain(..drain_count); + // Insert trim notice + self.conversation.insert(0, ConversationLine { + text: "... (earlier messages trimmed)".to_string(), + color: Color::DarkGray, + bold: false, + }); + } self.conversation_scroll = 0; self.needs_redraw = true; } diff --git a/rust/crates/rusty-claude-cli/src/tui_update.rs b/rust/crates/rusty-claude-cli/src/tui_update.rs index 12112198..608fea48 100644 --- a/rust/crates/rusty-claude-cli/src/tui_update.rs +++ b/rust/crates/rusty-claude-cli/src/tui_update.rs @@ -114,7 +114,11 @@ pub struct DashboardUpdate<'a> { pub permission_mode: &'a str, pub session_id: Option<&'a str>, pub turn_count: u32, - pub total_chars: usize, + pub input_tokens: u32, + pub output_tokens: u32, + pub cache_creation_tokens: u32, + pub cache_read_tokens: u32, + pub cost_usd: f64, pub provider: String, pub provider_url: String, } @@ -126,8 +130,11 @@ pub fn update_dashboard_from(state: &SharedDashboardState, update: &DashboardUpd ds.permission_mode = update.permission_mode.to_string(); ds.session_id = update.session_id.map(str::to_string); ds.turn_count = update.turn_count; - // Rough token estimate: ~4 chars per token - ds.input_tokens = (update.total_chars / 4) as u32; + ds.input_tokens = update.input_tokens; + ds.output_tokens = update.output_tokens; + ds.cache_creation_tokens = update.cache_creation_tokens; + ds.cache_read_tokens = update.cache_read_tokens; + ds.cost_usd = update.cost_usd; ds.provider = update.provider.clone(); ds.provider_url = update.provider_url.clone(); }