Remove account billing UI from AI settings

This commit is contained in:
2026-08-06 16:40:48 -05:00
parent 7ab381289e
commit af520193a9
2 changed files with 2 additions and 340 deletions
-340
View File
@@ -74,9 +74,6 @@ use crate::ai::llms::{
}; };
use crate::ai::mcp::TemplatableMCPServerManager; use crate::ai::mcp::TemplatableMCPServerManager;
use crate::ai::paths::host_native_absolute_path; use crate::ai::paths::host_native_absolute_path;
use crate::auth::auth_manager::{AuthManager, LoginGatedFeature};
use crate::auth::auth_view_modal::AuthViewVariant;
use crate::auth::AuthStateProvider;
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent}; use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
use crate::cloud_object::GenericStringObjectFormat::Json; use crate::cloud_object::GenericStringObjectFormat::Json;
use crate::cloud_object::{JsonObjectType, ObjectType}; use crate::cloud_object::{JsonObjectType, ObjectType};
@@ -151,7 +148,6 @@ use std::sync::LazyLock;
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine}; use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
use crate::ai::{AIRequestUsageModel, AIRequestUsageModelEvent};
use crate::appearance::{Appearance, AppearanceEvent}; use crate::appearance::{Appearance, AppearanceEvent};
use crate::editor::{EditorView, Event as EditorEvent, TextOptions}; use crate::editor::{EditorView, Event as EditorEvent, TextOptions};
use crate::menu::{MenuItem, MenuItemFields}; use crate::menu::{MenuItem, MenuItemFields};
@@ -1142,11 +1138,6 @@ impl AISettingsPageView {
}, },
); );
let request_usage_model = AIRequestUsageModel::handle(ctx);
ctx.subscribe_to_model(&request_usage_model, |_, _, _, ctx| {
ctx.notify();
});
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _handle, _event, ctx| { ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), |me, _handle, _event, ctx| {
// Re-render if teams-related data changed that may affect whether features such as voice input are enabled. // Re-render if teams-related data changed that may affect whether features such as voice input are enabled.
Self::refresh_base_model_menu(&me.base_model_dropdown, ctx); Self::refresh_base_model_menu(&me.base_model_dropdown, ctx);
@@ -1742,17 +1733,6 @@ impl AISettingsPageView {
} }
}); });
let ai_request_model = AIRequestUsageModel::handle(ctx);
ctx.subscribe_to_model(&ai_request_model, |me, _, event, ctx| {
match event {
AIRequestUsageModelEvent::RequestUsageUpdated => ctx.notify(),
AIRequestUsageModelEvent::RequestBonusRefunded { .. } => ctx.notify(),
AIRequestUsageModelEvent::AmbientCreditsBannerDismissed => {}
}
Self::refresh_base_model_menu(&me.base_model_dropdown, ctx);
Self::refresh_coding_model_menu(&me.coding_model_dropdown, ctx);
});
let profile_views = Self::create_profile_views(ctx); let profile_views = Self::create_profile_views(ctx);
// Custom model router views // Custom model router views
@@ -2159,9 +2139,6 @@ impl AISettingsPageView {
None => { None => {
// Full page: all widgets (legacy behavior) // Full page: all widgets (legacy behavior)
widgets.push(Box::new(GlobalAIWidget::default())); widgets.push(Box::new(GlobalAIWidget::default()));
if !FeatureFlag::UsageBasedPricing.is_enabled() {
widgets.push(Box::new(UsageWidget::default()));
}
if ai_settings if ai_settings
.intelligent_autosuggestions_enabled_internal .intelligent_autosuggestions_enabled_internal
.is_supported_on_current_platform() .is_supported_on_current_platform()
@@ -2243,9 +2220,6 @@ impl AISettingsPageView {
widgets.push(Box::new(OtherAIWidget::default())); widgets.push(Box::new(OtherAIWidget::default()));
} }
Some(AISubpage::Profiles) => { Some(AISubpage::Profiles) => {
if !FeatureFlag::UsageBasedPricing.is_enabled() {
widgets.push(Box::new(UsageWidget::default()));
}
widgets.push(Box::new(AgentsWidget::default())); widgets.push(Box::new(AgentsWidget::default()));
} }
Some(AISubpage::Knowledge) => { Some(AISubpage::Knowledge) => {
@@ -2981,7 +2955,6 @@ pub enum AISettingsPageAction {
SetOrchestrationMessageDisplayMode(OrchestrationMessageDisplayMode), SetOrchestrationMessageDisplayMode(OrchestrationMessageDisplayMode),
SetPromptSubmissionMode(PromptSubmissionMode), SetPromptSubmissionMode(PromptSubmissionMode),
SetLongRunningCommandSubmissionMode(LongRunningCommandSubmissionMode), SetLongRunningCommandSubmissionMode(LongRunningCommandSubmissionMode),
AttemptLoginGatedUpgrade,
RemoveCLIAgentToolbarEnabledCommand(String), RemoveCLIAgentToolbarEnabledCommand(String),
RemoveFromCommandExecutionAllowlist(AgentModeCommandExecutionPredicate), RemoveFromCommandExecutionAllowlist(AgentModeCommandExecutionPredicate),
RemoveFromCommandExecutionDenylist(AgentModeCommandExecutionPredicate), RemoveFromCommandExecutionDenylist(AgentModeCommandExecutionPredicate),
@@ -3057,16 +3030,6 @@ pub enum AISettingsPageAction {
ToggleCrosscheckEnabled, ToggleCrosscheckEnabled,
} }
impl From<&AISettingsPageAction> for LoginGatedFeature {
fn from(val: &AISettingsPageAction) -> LoginGatedFeature {
use AISettingsPageAction::*;
match val {
AttemptLoginGatedUpgrade => "Upgrade AI Usage",
_ => "Unknown reason",
}
}
}
impl TypedActionView for AISettingsPageView { impl TypedActionView for AISettingsPageView {
type Action = AISettingsPageAction; type Action = AISettingsPageAction;
@@ -3459,15 +3422,6 @@ impl TypedActionView for AISettingsPageView {
}); });
ctx.notify(); ctx.notify();
} }
AISettingsPageAction::AttemptLoginGatedUpgrade => {
AuthManager::handle(ctx).update(ctx, |auth_manager, ctx| {
auth_manager.attempt_login_gated_feature(
action.into(),
AuthViewVariant::RequireLoginCloseable,
ctx,
)
});
}
AISettingsPageAction::RemoveCLIAgentToolbarEnabledCommand(command) => { AISettingsPageAction::RemoveCLIAgentToolbarEnabledCommand(command) => {
AISettings::handle(ctx).update(ctx, |settings, ctx| { AISettings::handle(ctx).update(ctx, |settings, ctx| {
settings.remove_cli_agent_footer_enabled_command(command, ctx); settings.remove_cli_agent_footer_enabled_command(command, ctx);
@@ -3976,12 +3930,6 @@ impl SettingsPageMeta for AISettingsPageView {
FeatureFlag::AgentMode.is_enabled() FeatureFlag::AgentMode.is_enabled()
} }
fn on_page_selected(&mut self, _: bool, ctx: &mut ViewContext<Self>) {
AIRequestUsageModel::handle(ctx).update(ctx, |ai_request_usage_model, ctx| {
ai_request_usage_model.refresh_request_usage_async(ctx)
});
}
fn update_filter(&mut self, query: &str, ctx: &mut ViewContext<Self>) -> MatchData { fn update_filter(&mut self, query: &str, ctx: &mut ViewContext<Self>) -> MatchData {
self.page.update_filter(query, ctx) self.page.update_filter(query, ctx)
} }
@@ -4255,294 +4203,6 @@ impl SettingsWidget for GlobalAIWidget {
} }
} }
#[derive(Default)]
struct UsageWidget {
requests_highlight_index: HighlightedHyperlink,
}
impl UsageWidget {
fn render_request_usage_count(
&self,
used: usize,
limit: usize,
is_unlimited: bool,
workspace_is_delinquent_due_to_payment_issue: bool,
appearance: &Appearance,
) -> Box<dyn galaxyui::Element> {
let mut row = Flex::row();
if used >= limit || workspace_is_delinquent_due_to_payment_issue {
row.add_child(
ConstrainedBox::new(
Icon::AlertTriangle
.to_galaxyui_icon(appearance.theme().ui_error_color().into())
.finish(),
)
.with_height(16.)
.with_width(16.)
.finish(),
)
}
let request_count_label = if workspace_is_delinquent_due_to_payment_issue {
"Restricted due to billing issue".to_string()
} else if is_unlimited {
"Unlimited".to_string()
} else {
format!("{used}/{limit}")
};
row.add_child(
appearance
.ui_builder()
.paragraph(request_count_label)
.with_style(UiComponentStyles {
font_color: {
if used >= limit {
Some(appearance.theme().ui_error_color())
} else {
Some(blended_colors::text_sub(
appearance.theme(),
appearance.theme().surface_1(),
))
}
},
font_size: Some(16.),
margin: Some(Coords {
top: 0.,
bottom: 0.,
left: 8.,
right: 0.,
}),
..Default::default()
})
.build()
.finish(),
);
row.finish()
}
/// Renders a row of what is being limited, along with the current used/limit.
#[allow(clippy::too_many_arguments)]
fn render_ai_usage_limit_row(
&self,
header: impl Into<Cow<'static, str>>,
description: impl Into<Cow<'static, str>>,
used: usize,
limit: usize,
is_unlimited: bool,
workspace_is_delinquent_due_to_payment_issue: bool,
appearance: &Appearance,
) -> Box<dyn galaxyui::Element> {
let request_usage_details = Flex::column()
.with_cross_axis_alignment(CrossAxisAlignment::End)
.with_child(self.render_request_usage_count(
used,
limit,
is_unlimited,
workspace_is_delinquent_due_to_payment_issue,
appearance,
));
let request_usage_description = FormattedTextElement::from_str(
description,
appearance.ui_font_family(),
appearance.ui_font_size(),
)
.with_color(blended_colors::text_sub(
appearance.theme(),
appearance.theme().surface_1(),
));
Flex::row()
.with_child(
Shrinkable::new(
2.,
Container::new(
Flex::column()
.with_child(
appearance
.ui_builder()
.paragraph(header)
.with_style(UiComponentStyles {
font_color: Some(blended_colors::text_main(
appearance.theme(),
appearance.theme().surface_1(),
)),
margin: Some(Coords {
top: 0.,
bottom: 4.,
left: 0.,
right: 0.,
}),
..Default::default()
})
.build()
.finish(),
)
.with_child(request_usage_description.finish())
.finish(),
)
.with_margin_bottom(16.)
.finish(),
)
.finish(),
)
.with_child(
Shrinkable::new(
1.,
Container::new(request_usage_details.finish())
.with_margin_bottom(16.)
.finish(),
)
.finish(),
)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_main_axis_size(MainAxisSize::Max)
.finish()
}
}
impl SettingsWidget for UsageWidget {
type View = AISettingsPageView;
fn search_terms(&self) -> &str {
"a.i. ai usage limit plan"
}
fn render(
&self,
_view: &Self::View,
appearance: &Appearance,
app: &AppContext,
) -> Box<dyn Element> {
let ai_request_usage_model = AIRequestUsageModel::as_ref(app);
let next_refresh_time = ai_request_usage_model.next_refresh_time();
let formatted_next_refresh_time = next_refresh_time.format("%b %d").to_string();
let workspace_is_delinquent_due_to_payment_issue = UserWorkspaces::as_ref(app)
.current_team()
.map(|team| team.billing_metadata.is_delinquent_due_to_payment_issue())
.unwrap_or_default();
let usage_header = Container::new(
Flex::row()
.with_main_axis_size(MainAxisSize::Max)
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
.with_cross_axis_alignment(CrossAxisAlignment::Center)
.with_child(
build_sub_header(
appearance,
"Usage",
Some(styles::header_font_color(true, app)),
)
.finish(),
)
.with_child(
appearance
.ui_builder()
.paragraph(format!("Resets {formatted_next_refresh_time}"))
.with_style(UiComponentStyles {
font_color: Some(blended_colors::text_sub(
appearance.theme(),
appearance.theme().surface_1(),
)),
..Default::default()
})
.build()
.finish(),
)
.finish(),
)
.with_padding_bottom(HEADER_PADDING)
.finish();
let request_limit_description = format!(
"This is the {} limit of AI credits for your account.",
ai_request_usage_model.refresh_duration_to_string()
);
let request_usage_row = self.render_ai_usage_limit_row(
"Credits",
request_limit_description,
ai_request_usage_model.requests_used(),
ai_request_usage_model.request_limit(),
ai_request_usage_model.is_unlimited(),
workspace_is_delinquent_due_to_payment_issue,
appearance,
);
let auth_state = AuthStateProvider::as_ref(app).get();
let upgrade_cta_text_fragments = if let Some(team) =
UserWorkspaces::as_ref(app).current_team()
{
let current_user_email = auth_state.user_email().unwrap_or_default();
let has_admin_permissions = team.has_admin_permissions(&current_user_email);
if team.billing_metadata.can_upgrade_to_higher_tier_plan() {
let upgrade_url = UserWorkspaces::upgrade_link_for_team(team.uid);
if has_admin_permissions {
vec![
FormattedTextFragment::hyperlink("Upgrade", upgrade_url),
FormattedTextFragment::plain_text(" to get more AI usage."),
]
} else {
// The /upgrade page says to contact their administrator.
vec![
FormattedTextFragment::hyperlink("Compare plans", upgrade_url),
FormattedTextFragment::plain_text(" for more AI usage."),
]
}
} else {
vec![
FormattedTextFragment::hyperlink("Contact support", "mailto:support@warp.dev"),
FormattedTextFragment::plain_text(" for more AI usage."),
]
}
} else {
let user_id = auth_state.user_id().unwrap_or_default();
let upgrade_url = UserWorkspaces::upgrade_link(user_id);
vec![
FormattedTextFragment::hyperlink("Upgrade", upgrade_url),
FormattedTextFragment::plain_text(" to get more AI usage."),
]
};
let mut upgrade_cta = FormattedTextElement::new(
FormattedText::new([FormattedTextLine::Line(upgrade_cta_text_fragments)]),
appearance.ui_font_size(),
appearance.ui_font_family(),
appearance.ui_font_family(),
styles::description_font_color(true, app).into(),
self.requests_highlight_index.clone(),
)
.with_hyperlink_font_color(appearance.theme().accent().into_solid());
if AuthStateProvider::as_ref(app)
.get()
.is_anonymous_or_logged_out()
{
upgrade_cta = upgrade_cta.register_default_click_handlers(|_, ctx, _| {
ctx.dispatch_typed_action(AISettingsPageAction::AttemptLoginGatedUpgrade);
});
} else {
upgrade_cta = upgrade_cta.register_default_click_handlers(|url, ctx, _| {
ctx.dispatch_typed_action(AISettingsPageAction::HyperlinkClick(url));
})
}
Flex::column()
.with_children([
render_separator(appearance),
usage_header,
request_usage_row,
Container::new(upgrade_cta.finish())
.with_margin_bottom(16.)
.finish(),
])
.finish()
}
}
#[derive(Default)] #[derive(Default)]
struct ActiveAIWidget { struct ActiveAIWidget {
active_ai_toggle: SwitchStateHandle, active_ai_toggle: SwitchStateHandle,
+2
View File
@@ -407,6 +407,8 @@ metadata.
Drive remains usable while logged out; team-only actions retain their separate restrictions. Drive remains usable while logged out; team-only actions retain their separate restrictions.
- [x] Remove the stale account/signup gate from the Global Agent control so configured local - [x] Remove the stale account/signup gate from the Global Agent control so configured local
providers and runtimes remain usable while logged out. providers and runtimes remain usable while logged out.
- [x] Remove the inherited account-credit, billing-status, and upgrade CTA widget from AI
settings; configured local providers own their credentials and usage limits.
- [x] Move the retained Drive import flow onto local persistence for personal targets, including - [x] Move the retained Drive import flow onto local persistence for personal targets, including
local folder/notebook/workflow creation and progress reporting without remote `UpdateManager` local folder/notebook/workflow creation and progress reporting without remote `UpdateManager`
or `SyncQueue` dependencies; shared/team imports retain their remote path. or `SyncQueue` dependencies; shared/team imports retain their remote path.