Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/src/commands/agent/run/mode_interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 47 additions & 8 deletions cli/src/config/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,17 +730,49 @@ impl AppConfig {
None
}

/// Get auth display info for the TUI.
pub fn get_auth_display_info(&self) -> (Option<String>, Option<String>, Option<String>) {
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<String>, Option<String>, Option<String>) {
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",
Expand Down Expand Up @@ -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)
}
}

Expand Down
8 changes: 7 additions & 1 deletion libs/shared/src/models/billing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,27 @@ pub struct Product {
pub id: String,
pub is_add_on: bool,
pub is_default: bool,
pub items: Vec<ProductItem>,
#[serde(default)]
pub items: Option<Vec<ProductItem>>,
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<String, Feature>,
pub id: String,
pub name: String,
#[serde(default)]
pub products: Vec<Product>,
#[serde(default)]
pub stripe_id: Option<String>,
}
58 changes: 44 additions & 14 deletions tui/src/services/side_panel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(""));
Expand Down