fix(tui): Sprint 1 — ProviderSwap fix, status race, memory bound, token tracking

S1-1: Fix ProviderSwap — use suspend()/resume() instead of restore_terminal()
so TUI re-enters cleanly after the setup wizard runs.

S1-2: Fix status message race — remove immediate ds.status_message.clear()
after set_status("Done") so the message is visible until next turn.

S1-4: Bound conversation memory — add MAX_CONVERSATION_LINES (10,000).
auto_scroll() now drains oldest entries and inserts a trim notice.

S1-5: Wire real token tracking — use runtime::UsageTracker::from_session()
to read cumulative TokenUsage (input, output, cache_creation, cache_read)
and pricing_for_model() to estimate cost. No more char÷4 guess.

Sprint 1 of 7. 218 tests pass.

Authored by TheArchitectit
This commit is contained in:
Claude 2026-06-11 18:49:15 -05:00
parent 7c34488042
commit 755659c916
3 changed files with 51 additions and 20 deletions

View File

@ -7216,9 +7216,6 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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<dyn std::error::Error>> {
{
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<dyn std::error::Error>> {
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::<usize>()
}).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"))

View File

@ -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;
}

View File

@ -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();
}