diff --git a/cli/src/commands/agent/run/mode_interactive.rs b/cli/src/commands/agent/run/mode_interactive.rs index 80a8d78f..7e773399 100644 --- a/cli/src/commands/agent/run/mode_interactive.rs +++ b/cli/src/commands/agent/run/mode_interactive.rs @@ -219,8 +219,9 @@ pub async fn run_interactive( let data = client.get_my_account().await?; send_input_event(&input_tx, InputEvent::GetStatus(data.to_text())).await?; - // Fetch billing info (only when Stakpak API key is present) - if has_stakpak_key { + // Fetch billing info (only when Stakpak API key is present AND not using local provider) + // When using local provider auth (e.g., Claude API key in auth.toml), skip billing + if has_stakpak_key && !ctx_clone.is_using_local_provider() { refresh_billing_info(client.as_ref(), &input_tx).await; } // Load available profiles and send to TUI diff --git a/cli/src/config/app.rs b/cli/src/config/app.rs index 44b541f7..b94dcd58 100644 --- a/cli/src/config/app.rs +++ b/cli/src/config/app.rs @@ -730,17 +730,49 @@ impl AppConfig { None } - /// Get auth display info for the TUI. - pub fn get_auth_display_info(&self) -> (Option, Option, Option) { - if matches!(self.provider, ProviderType::Remote) { - return (None, None, None); + /// Check if using a local LLM provider (via auth.toml or config). + /// Returns true if any local provider (anthropic, openai, gemini) has auth configured, + /// even if the profile's provider setting is Remote. + pub fn is_using_local_provider(&self) -> bool { + // If explicitly set to Local, it's local + if matches!(self.provider, ProviderType::Local) { + return true; } - let config_provider = Some("Local".to_string()); + // Check if any local provider has auth configured in auth.toml let builtin_providers = ["anthropic", "openai", "gemini"]; + for provider_name in builtin_providers { + if self.resolve_provider_auth(provider_name).is_some() { + return true; + } + } + // Check custom providers in config + for name in self.providers.keys() { + if !builtin_providers.contains(&name.as_str()) + && name != "stakpak" + && self + .providers + .get(name) + .is_some_and(|p| p.api_key().is_some()) + { + return true; + } + } + + false + } + + /// Get auth display info for the TUI. + /// Returns (config_provider, auth_provider, subscription_name). + /// Now checks for local provider auth even when provider=Remote. + pub fn get_auth_display_info(&self) -> (Option, Option, Option) { + let builtin_providers = ["anthropic", "openai", "gemini"]; + + // Check if any local provider has auth - if so, treat as "Local" for display for provider_name in builtin_providers { if let Some(auth) = self.resolve_provider_auth(provider_name) { + let config_provider = Some("Local".to_string()); let base_name = match provider_name { "anthropic" => "Anthropic", "openai" => "OpenAI", @@ -769,12 +801,19 @@ impl AppConfig { // Check custom providers for name in self.providers.keys() { - if !builtin_providers.contains(&name.as_str()) { - return (config_provider, Some(name.clone()), None); + if !builtin_providers.contains(&name.as_str()) + && name != "stakpak" + && self + .providers + .get(name) + .is_some_and(|p| p.api_key().is_some()) + { + return (Some("Local".to_string()), Some(name.clone()), None); } } - (config_provider, None, None) + // No local provider auth found - return None for all (Remote mode) + (None, None, None) } } diff --git a/libs/shared/src/models/billing.rs b/libs/shared/src/models/billing.rs index 3b0c101c..bf7583fb 100644 --- a/libs/shared/src/models/billing.rs +++ b/libs/shared/src/models/billing.rs @@ -45,21 +45,27 @@ pub struct Product { pub id: String, pub is_add_on: bool, pub is_default: bool, - pub items: Vec, + #[serde(default)] + pub items: Option>, pub name: String, + #[serde(default)] pub quantity: u32, pub started_at: u64, pub status: String, + #[serde(default)] pub version: u32, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct BillingResponse { pub created_at: u64, + #[serde(default)] pub env: String, pub features: HashMap, pub id: String, pub name: String, + #[serde(default)] pub products: Vec, + #[serde(default)] pub stripe_id: Option, } diff --git a/tui/src/services/side_panel.rs b/tui/src/services/side_panel.rs index b84f8b9f..5867710e 100644 --- a/tui/src/services/side_panel.rs +++ b/tui/src/services/side_panel.rs @@ -42,7 +42,18 @@ pub fn render_side_panel(f: &mut Frame, state: &mut AppState, area: Rect) { // Calculate section heights let collapsed_height = 1; // Height when collapsed (just header) - let footer_height = 4; // For version+profile, empty line, shortcuts (2 lines) + // Calculate footer height - check if version+profile needs to wrap + let version = env!("CARGO_PKG_VERSION"); + let profile = &state.current_profile_name; + let available_width = padded_area.width as usize; + let left_part = format!("{}v{}", LEFT_PADDING, version); + let right_part = format!("profile {} ", profile); + let total_content = left_part.len() + right_part.len() + 1; + let footer_height = if total_content > available_width { + 5 + } else { + 4 + }; // All sections are expanded by default (no collapsing) let context_collapsed = state @@ -72,8 +83,11 @@ pub fn render_side_panel(f: &mut Frame, state: &mut AppState, area: Rect) { 5 // Header + Tokens + Model + Provider }; - // Billing section is hidden when billing_info is None (local mode) - let billing_height = if state.billing_info.is_none() { + // Billing section is hidden when billing_info is None OR when using local provider + // Check if auth_display_info indicates local provider (first element is Some("Local")) + let is_local_provider = + matches!(&state.auth_display_info, (Some(provider), _, _) if provider == "Local"); + let billing_height = if state.billing_info.is_none() || is_local_provider { 0 } else if billing_collapsed { collapsed_height @@ -640,21 +654,37 @@ fn render_footer_section(f: &mut Frame, state: &AppState, area: Rect) { let profile = &state.current_profile_name; let available_width = area.width as usize; - // Line 1: Version (left) and Profile (right) + // Line 1: Version (left) and Profile (right) - wrap if too long let left_part = format!("{}v{}", LEFT_PADDING, version); let right_part = format!("profile {} ", profile); - let total_content = left_part.len() + right_part.len(); - let spacing = available_width.saturating_sub(total_content).max(1); - - lines.push(Line::from(vec![ - Span::styled( + let total_content = left_part.len() + right_part.len() + 1; // +1 for min spacing + + if total_content <= available_width { + // Fits on one line + let spacing = available_width.saturating_sub(total_content).max(1); + lines.push(Line::from(vec![ + Span::styled( + format!("{}v{}", LEFT_PADDING, version), + Style::default().fg(Color::DarkGray), + ), + Span::raw(" ".repeat(spacing)), + Span::styled("profile ", Style::default().fg(Color::DarkGray)), + Span::styled(profile, Style::default().fg(Color::Reset)), + ])); + } else { + // Wrap: version on first line, profile on second line + lines.push(Line::from(vec![Span::styled( format!("{}v{}", LEFT_PADDING, version), Style::default().fg(Color::DarkGray), - ), - Span::raw(" ".repeat(spacing)), - Span::styled("profile ", Style::default().fg(Color::DarkGray)), - Span::styled(profile, Style::default().fg(Color::Reset)), - ])); + )])); + lines.push(Line::from(vec![ + Span::styled( + format!("{}profile ", LEFT_PADDING), + Style::default().fg(Color::DarkGray), + ), + Span::styled(profile, Style::default().fg(Color::Reset)), + ])); + } // Empty line between version/profile and shortcuts lines.push(Line::from(""));