Remove account billing UI from AI settings
This commit is contained in:
@@ -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(¤t_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,
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user