first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,42 +1,50 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ai::LLMId;
|
||||
use instant::Instant;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::image_cache::ImageType;
|
||||
use galaxyui_core::windowing::state::{ApplicationStage, StateEvent};
|
||||
use galaxyui_core::windowing::WindowManager;
|
||||
|
||||
use crate::components::feature_optout_dialog::{render_feature_optout_dialog, FeatureOptOutDialog};
|
||||
use crate::model::{
|
||||
OnboardingAuthState, OnboardingStateEvent, OnboardingStateModel, OnboardingStep,
|
||||
SelectedSettings,
|
||||
};
|
||||
use crate::slides::{
|
||||
AgentSlide, AgentSlideEvent, CustomizeUISlide, FreeUserNoAiSlide, IntentionSlide, IntroSlide,
|
||||
IntroSlideEvent, OnboardingModelInfo, OnboardingSlide, ProjectSlide, ThemePickerSlide,
|
||||
ThemePickerSlideEvent, ThirdPartySlide,
|
||||
AgentSlide, AiAccessSlide, AiAccessSlideEvent, AiSetupSlide, CustomizeUISlide, IntentionSlide,
|
||||
IntroSlide, IntroSlideEvent, OnboardingModelInfo, OnboardingSlide, ProjectSlide,
|
||||
ThemePickerSlide, ThemePickerSlideEvent, ThirdPartySlide,
|
||||
};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use ai::LLMId;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui::assets::asset_cache::AssetSource;
|
||||
use galaxyui::image_cache::ImageType;
|
||||
use galaxyui::windowing::{
|
||||
state::{ApplicationStage, StateEvent},
|
||||
WindowManager,
|
||||
};
|
||||
use instant::Instant;
|
||||
use std::time::Duration;
|
||||
|
||||
const APP_BECAME_ACTIVE_DEBOUNCE: Duration = Duration::from_secs(15);
|
||||
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::GalaxyTheme};
|
||||
use galaxyui::elements::Rect;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
CacheOption, ChildAnchor, Container, Empty, Image, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Shrinkable, Stack,
|
||||
},
|
||||
keymap::Keystroke,
|
||||
keymap::{macros::*, FixedBinding},
|
||||
presenter::ChildView,
|
||||
const PLAN_ACTIVATED_TOAST_DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::{Fill, WarpTheme};
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Align, CacheOption, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Dismiss, Empty,
|
||||
Flex, Image, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
ParentAnchor, ParentElement, ParentOffsetBounds, Rect, Shrinkable, Stack,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::macros::*;
|
||||
use galaxyui_core::keymap::{FixedBinding, Keystroke};
|
||||
use galaxyui_core::presenter::ChildView;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext, ViewHandle,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum AgentOnboardingEvent {
|
||||
@@ -68,14 +76,21 @@ pub struct AgentOnboardingView {
|
||||
intro_slide: ViewHandle<IntroSlide>,
|
||||
theme_picker_slide: ViewHandle<ThemePickerSlide>,
|
||||
intention_slide: ViewHandle<IntentionSlide>,
|
||||
ai_setup_slide: ViewHandle<AiSetupSlide>,
|
||||
customize_slide: ViewHandle<CustomizeUISlide>,
|
||||
free_user_no_ai_slide: ViewHandle<FreeUserNoAiSlide>,
|
||||
agent_slide: ViewHandle<AgentSlide>,
|
||||
ai_access_slide: ViewHandle<AiAccessSlide>,
|
||||
third_party_slide: ViewHandle<ThirdPartySlide>,
|
||||
project_slide: ViewHandle<ProjectSlide>,
|
||||
skippable: bool,
|
||||
close_button: button::Button,
|
||||
no_ai_confirm_button: button::Button,
|
||||
no_ai_cancel_button: button::Button,
|
||||
no_ai_close_button: button::Button,
|
||||
last_model_refresh: Option<Instant>,
|
||||
show_plan_activated_toast: bool,
|
||||
last_auth_state: OnboardingAuthState,
|
||||
plan_activated_close_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -88,6 +103,10 @@ pub enum AgentOnboardingAction {
|
||||
EnterKey,
|
||||
CmdOrCtrlEnterKey,
|
||||
Escape,
|
||||
NoAiConfirm,
|
||||
NoAiCancel,
|
||||
NoAiDismiss,
|
||||
DismissPlanActivatedToast,
|
||||
}
|
||||
|
||||
fn dispatch_onboarding_action_to_slide<V: OnboardingSlide>(
|
||||
@@ -104,6 +123,11 @@ fn dispatch_onboarding_action_to_slide<V: OnboardingSlide>(
|
||||
AgentOnboardingAction::EnterKey => slide.on_enter(ctx),
|
||||
AgentOnboardingAction::CmdOrCtrlEnterKey => slide.on_cmd_or_ctrl_enter(ctx),
|
||||
AgentOnboardingAction::Escape => slide.on_escape(ctx),
|
||||
// Parent-level actions are handled by the parent view, never routed to a slide.
|
||||
AgentOnboardingAction::NoAiConfirm
|
||||
| AgentOnboardingAction::NoAiCancel
|
||||
| AgentOnboardingAction::NoAiDismiss
|
||||
| AgentOnboardingAction::DismissPlanActivatedToast => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,8 +141,6 @@ impl AgentOnboardingView {
|
||||
default_model_id: LLMId,
|
||||
workspace_enforces_autonomy: bool,
|
||||
agent_modality_enabled: bool,
|
||||
free_user_no_ai_experiment: bool,
|
||||
agent_price_cents: Option<i32>,
|
||||
auth_state: OnboardingAuthState,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
@@ -128,8 +150,6 @@ impl AgentOnboardingView {
|
||||
default_model_id,
|
||||
workspace_enforces_autonomy,
|
||||
agent_modality_enabled,
|
||||
free_user_no_ai_experiment,
|
||||
agent_price_cents,
|
||||
auth_state,
|
||||
)
|
||||
});
|
||||
@@ -147,7 +167,13 @@ impl AgentOnboardingView {
|
||||
OnboardingStateEvent::UpgradeRequested => {
|
||||
ctx.emit(AgentOnboardingEvent::UpgradeRequested);
|
||||
}
|
||||
_ => {}
|
||||
OnboardingStateEvent::AuthStateChanged => {
|
||||
me.handle_auth_state_changed(ctx);
|
||||
}
|
||||
OnboardingStateEvent::ModelsUpdated
|
||||
| OnboardingStateEvent::SelectedSlideChanged
|
||||
| OnboardingStateEvent::IntentionChanged
|
||||
| OnboardingStateEvent::NoAiConfirmationChanged => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,14 +201,15 @@ impl AgentOnboardingView {
|
||||
ctx.add_typed_action_view(move |_| IntentionSlide::new(onboarding_state))
|
||||
};
|
||||
|
||||
let ai_setup_slide = {
|
||||
let onboarding_state = onboarding_state.clone();
|
||||
ctx.add_typed_action_view(move |_| AiSetupSlide::new(onboarding_state))
|
||||
};
|
||||
|
||||
let customize_slide = {
|
||||
let onboarding_state = onboarding_state.clone();
|
||||
ctx.add_typed_action_view(move |ctx| CustomizeUISlide::new(onboarding_state, ctx))
|
||||
};
|
||||
let free_user_no_ai_slide = {
|
||||
let onboarding_state = onboarding_state.clone();
|
||||
ctx.add_typed_action_view(move |_| FreeUserNoAiSlide::new(onboarding_state))
|
||||
};
|
||||
|
||||
ctx.subscribe_to_view(&theme_picker_slide, |me, _view, event, ctx| {
|
||||
me.handle_theme_picker_slide_event(event, ctx);
|
||||
@@ -193,11 +220,16 @@ impl AgentOnboardingView {
|
||||
ctx.add_typed_action_view(move |ctx| AgentSlide::new(onboarding_state, ctx))
|
||||
};
|
||||
|
||||
ctx.subscribe_to_view(&agent_slide, |_me, _view, event, ctx| match event {
|
||||
AgentSlideEvent::CopyUpgradeUrlRequested => {
|
||||
let ai_access_slide = {
|
||||
let onboarding_state = onboarding_state.clone();
|
||||
ctx.add_typed_action_view(move |_| AiAccessSlide::new(onboarding_state))
|
||||
};
|
||||
|
||||
ctx.subscribe_to_view(&ai_access_slide, |_me, _view, event, ctx| match event {
|
||||
AiAccessSlideEvent::CopyUpgradeUrlRequested => {
|
||||
ctx.emit(AgentOnboardingEvent::UpgradeCopyUrlRequested);
|
||||
}
|
||||
AgentSlideEvent::PasteAuthTokenFromClipboardRequested => {
|
||||
AiAccessSlideEvent::PasteAuthTokenFromClipboardRequested => {
|
||||
ctx.emit(AgentOnboardingEvent::UpgradePasteTokenFromClipboardRequested);
|
||||
}
|
||||
});
|
||||
@@ -236,14 +268,21 @@ impl AgentOnboardingView {
|
||||
intro_slide,
|
||||
theme_picker_slide,
|
||||
intention_slide,
|
||||
ai_setup_slide,
|
||||
customize_slide,
|
||||
free_user_no_ai_slide,
|
||||
agent_slide,
|
||||
ai_access_slide,
|
||||
third_party_slide,
|
||||
project_slide,
|
||||
skippable,
|
||||
close_button: button::Button::default(),
|
||||
no_ai_confirm_button: button::Button::default(),
|
||||
no_ai_cancel_button: button::Button::default(),
|
||||
no_ai_close_button: button::Button::default(),
|
||||
last_model_refresh: None,
|
||||
show_plan_activated_toast: false,
|
||||
last_auth_state: auth_state,
|
||||
plan_activated_close_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,12 +313,6 @@ impl AgentOnboardingView {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn free_user_no_ai_experiment(&self, ctx: &AppContext) -> bool {
|
||||
self.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.free_user_no_ai_experiment()
|
||||
}
|
||||
|
||||
/// The current `use_vertical_tabs` value on the onboarding UI customization.
|
||||
/// This reflects the intention's default (agent = vertical, terminal = horizontal)
|
||||
/// and any change the user made on the customize slide, and is what the
|
||||
@@ -291,32 +324,6 @@ impl AgentOnboardingView {
|
||||
.use_vertical_tabs
|
||||
}
|
||||
|
||||
pub fn set_agent_price_cents(&mut self, cents: Option<i32>, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |state, ctx| {
|
||||
state.set_agent_price_cents(cents, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn set_free_user_no_ai_experiment(&mut self, value: bool, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |state, ctx| {
|
||||
state.set_free_user_no_ai_experiment(value, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// When the user upgrades during the FreeUserNoAi experiment, advance directly
|
||||
/// to the Agent setup step (skipping the intention slide — they've already chosen).
|
||||
pub fn advance_to_agent_step(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let step = self.onboarding_state.as_ref(ctx).step();
|
||||
if matches!(step, OnboardingStep::Intention) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.set_intention_agent_driven_development(ctx);
|
||||
model.next(ctx); // Intention → Agent
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_onboarding(&self, ctx: &mut ViewContext<Self>) {
|
||||
// Focus the onboarding view so key bindings (Enter, arrow keys, etc.) are routed here
|
||||
// instead of to other views (e.g. the editor).
|
||||
@@ -339,7 +346,7 @@ impl AgentOnboardingView {
|
||||
/// Eagerly loads all onboarding slide images into the asset cache
|
||||
/// so they display instantly when the user navigates between slides.
|
||||
fn preload_onboarding_images(ctx: &mut ViewContext<Self>) {
|
||||
let asset_cache = galaxyui::assets::asset_cache::AssetCache::as_ref(ctx);
|
||||
let asset_cache = galaxyui_core::assets::asset_cache::AssetCache::as_ref(ctx);
|
||||
// Preload the shared background image used on all right panels.
|
||||
asset_cache.load_asset::<ImageType>(AssetSource::Bundled {
|
||||
path: crate::slides::layout::ONBOARDING_BG_PATH,
|
||||
@@ -347,6 +354,12 @@ impl AgentOnboardingView {
|
||||
for path in IntentionSlide::VISUAL_IMAGE_PATHS {
|
||||
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
|
||||
}
|
||||
for path in AiSetupSlide::VISUAL_IMAGE_PATHS {
|
||||
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
|
||||
}
|
||||
for path in AiAccessSlide::VISUAL_IMAGE_PATHS {
|
||||
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
|
||||
}
|
||||
for path in CustomizeUISlide::VISUAL_IMAGE_PATHS {
|
||||
asset_cache.load_asset::<ImageType>(AssetSource::Bundled { path });
|
||||
}
|
||||
@@ -360,11 +373,169 @@ impl AgentOnboardingView {
|
||||
// which are already in CustomizeUISlide::VISUAL_IMAGE_PATHS.
|
||||
}
|
||||
|
||||
fn render_no_ai_dialog(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let escape = Keystroke::parse("escape").unwrap_or_default();
|
||||
let close_button = self.no_ai_close_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Icon(Icon::X),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
keystroke: Some(escape),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiDismiss);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let cancel_button = self.no_ai_cancel_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Give me AI features".into()),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiCancel);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let enter = Keystroke::parse("enter").unwrap_or_default();
|
||||
let confirm_button = self.no_ai_confirm_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("I don't want AI".into()),
|
||||
theme: &button::themes::Primary,
|
||||
options: button::Options {
|
||||
keystroke: Some(enter),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiConfirm);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
render_feature_optout_dialog(
|
||||
appearance,
|
||||
FeatureOptOutDialog {
|
||||
title: "Are you sure you don't want AI?",
|
||||
body: "Without AI, you'll still get Warp's terminal experience, but you'll miss \
|
||||
our agentic features like automatic fixes for terminal errors.",
|
||||
features: &[],
|
||||
close_button,
|
||||
cancel_button,
|
||||
confirm_button,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn handle_onboarding_completed(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let settings = self.onboarding_state.as_ref(ctx).settings();
|
||||
ctx.emit(AgentOnboardingEvent::OnboardingCompleted(settings));
|
||||
}
|
||||
|
||||
/// Reacts to a billing/auth transition. When the user becomes a paying user
|
||||
/// we show a success toast; if they're still on the AI-access slide we also
|
||||
/// advance them, since selecting a plan was the remaining action there.
|
||||
fn handle_auth_state_changed(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let new_state = self.onboarding_state.as_ref(ctx).auth_state();
|
||||
let became_paying = new_state == OnboardingAuthState::PayingUser
|
||||
&& self.last_auth_state != OnboardingAuthState::PayingUser;
|
||||
self.last_auth_state = new_state;
|
||||
if !became_paying {
|
||||
return;
|
||||
}
|
||||
|
||||
let on_ai_access = self.onboarding_state.as_ref(ctx).step() == OnboardingStep::AiAccess;
|
||||
if on_ai_access {
|
||||
self.onboarding_state
|
||||
.update(ctx, |model, ctx| model.next(ctx));
|
||||
}
|
||||
|
||||
self.show_plan_activated_toast = true;
|
||||
let _ = ctx.spawn(
|
||||
galaxyui_core::r#async::Timer::after(PLAN_ACTIVATED_TOAST_DURATION),
|
||||
|me: &mut Self, _, ctx| {
|
||||
if me.show_plan_activated_toast {
|
||||
me.show_plan_activated_toast = false;
|
||||
ctx.notify();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Green success pill shown after billing succeeds. Hosted at the view level
|
||||
/// (not the slide) so it survives the auto-advance off the AI-access slide.
|
||||
fn render_plan_activated_toast(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const TOAST_MIN_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const CLOSE_SIZE: f32 = 16.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let toast_bg: Fill = theme.ansi_fg_green().into();
|
||||
let text_color: ColorU = theme.font_color(toast_bg.into_solid()).into();
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let check_icon = ConstrainedBox::new(Box::new(
|
||||
Icon::CheckSkinny.to_warpui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let text = ui_builder
|
||||
.span("Plan successfully activated!")
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let close_button = ui_builder
|
||||
.close_button(CLOSE_SIZE, self.plan_activated_close_mouse_state.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentOnboardingAction::DismissPlanActivatedToast);
|
||||
})
|
||||
.finish();
|
||||
|
||||
let left = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(check_icon)
|
||||
.with_child(Container::new(text).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(close_button)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(toast_bg)
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(TOAST_MIN_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn handle_theme_picker_slide_event(
|
||||
&mut self,
|
||||
event: &ThemePickerSlideEvent,
|
||||
@@ -432,19 +603,11 @@ impl View for AgentOnboardingView {
|
||||
let slide = match selected_slide {
|
||||
OnboardingStep::Intro => ChildView::new(&self.intro_slide).finish(),
|
||||
OnboardingStep::ThemePicker => ChildView::new(&self.theme_picker_slide).finish(),
|
||||
OnboardingStep::Intention => {
|
||||
if self
|
||||
.onboarding_state
|
||||
.as_ref(app)
|
||||
.free_user_no_ai_experiment()
|
||||
{
|
||||
ChildView::new(&self.free_user_no_ai_slide).finish()
|
||||
} else {
|
||||
ChildView::new(&self.intention_slide).finish()
|
||||
}
|
||||
}
|
||||
OnboardingStep::Intention => ChildView::new(&self.intention_slide).finish(),
|
||||
OnboardingStep::AiSetup => ChildView::new(&self.ai_setup_slide).finish(),
|
||||
OnboardingStep::Customize => ChildView::new(&self.customize_slide).finish(),
|
||||
OnboardingStep::Agent => ChildView::new(&self.agent_slide).finish(),
|
||||
OnboardingStep::AiAccess => ChildView::new(&self.ai_access_slide).finish(),
|
||||
OnboardingStep::ThirdParty => ChildView::new(&self.third_party_slide).finish(),
|
||||
OnboardingStep::Project => ChildView::new(&self.project_slide).finish(),
|
||||
};
|
||||
@@ -481,6 +644,35 @@ impl View for AgentOnboardingView {
|
||||
);
|
||||
}
|
||||
|
||||
if self
|
||||
.onboarding_state
|
||||
.as_ref(app)
|
||||
.no_ai_confirmation()
|
||||
.is_some()
|
||||
{
|
||||
let dialog = self.render_no_ai_dialog(appearance);
|
||||
stack.add_child(
|
||||
Rect::new()
|
||||
.with_background(Fill::Solid(ColorU::black()).with_opacity(60))
|
||||
.finish(),
|
||||
);
|
||||
stack.add_child(
|
||||
Dismiss::new(Align::new(dialog).finish())
|
||||
.on_dismiss(|ctx, _app| {
|
||||
ctx.dispatch_typed_action(AgentOnboardingAction::NoAiDismiss);
|
||||
})
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
if self.show_plan_activated_toast {
|
||||
stack.add_child(
|
||||
Align::new(self.render_plan_activated_toast(appearance))
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
@@ -489,11 +681,41 @@ impl TypedActionView for AgentOnboardingView {
|
||||
type Action = AgentOnboardingAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
if self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.no_ai_confirmation()
|
||||
.is_some()
|
||||
{
|
||||
match action {
|
||||
AgentOnboardingAction::NoAiConfirm | AgentOnboardingAction::EnterKey => {
|
||||
self.onboarding_state
|
||||
.update(ctx, |model, ctx| model.confirm_no_ai(ctx));
|
||||
}
|
||||
AgentOnboardingAction::NoAiCancel => {
|
||||
self.onboarding_state
|
||||
.update(ctx, |model, ctx| model.cancel_no_ai(ctx));
|
||||
}
|
||||
AgentOnboardingAction::NoAiDismiss | AgentOnboardingAction::Escape => {
|
||||
self.onboarding_state
|
||||
.update(ctx, |model, ctx| model.dismiss_no_ai(ctx));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(action, AgentOnboardingAction::Escape) && self.skippable {
|
||||
ctx.emit(AgentOnboardingEvent::OnboardingSkipped);
|
||||
return;
|
||||
}
|
||||
|
||||
if matches!(action, AgentOnboardingAction::DismissPlanActivatedToast) {
|
||||
self.show_plan_activated_toast = false;
|
||||
ctx.notify();
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_slide = self.onboarding_state.as_ref(ctx).step();
|
||||
|
||||
match selected_slide {
|
||||
@@ -503,27 +725,21 @@ impl TypedActionView for AgentOnboardingView {
|
||||
OnboardingStep::ThemePicker => self.theme_picker_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::Intention => {
|
||||
if self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.free_user_no_ai_experiment()
|
||||
{
|
||||
self.free_user_no_ai_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
})
|
||||
} else {
|
||||
self.intention_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
})
|
||||
}
|
||||
}
|
||||
OnboardingStep::Intention => self.intention_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::AiSetup => self.ai_setup_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::Customize => self.customize_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::Agent => self.agent_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::AiAccess => self.ai_access_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
OnboardingStep::ThirdParty => self.third_party_slide.update(ctx, |slide, ctx| {
|
||||
dispatch_onboarding_action_to_slide(slide, *action, ctx)
|
||||
}),
|
||||
|
||||
@@ -1,29 +1,34 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
use ai::LLMId;
|
||||
use anyhow::Result;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::{AnsiColor, AnsiColors, Details, Fill, Image, TerminalColors};
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::GalaxyTheme};
|
||||
use galaxyui::assets::asset_cache::AssetSource;
|
||||
use galaxyui::platform;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Container, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize, ParentElement,
|
||||
},
|
||||
fonts::{Cache, FamilyId, Weight},
|
||||
presenter::ChildView,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AddWindowOptions, AppContext, AssetProvider, Element, Entity, SingletonEntity as _,
|
||||
TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use onboarding::slides::OnboardingModelInfo;
|
||||
use onboarding::{
|
||||
AgentOnboardingEvent, AgentOnboardingView, MockTelemetryContextProvider, SelectedSettings,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use rust_embed::RustEmbed;
|
||||
use std::borrow::Cow;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::{
|
||||
AnsiColor, AnsiColors, Details, Fill, Image, TerminalColors, WarpTheme,
|
||||
};
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::elements::{
|
||||
Container, CrossAxisAlignment, Flex, MainAxisAlignment, MainAxisSize, ParentElement,
|
||||
};
|
||||
use galaxyui_core::fonts::{Cache, FamilyId, Weight};
|
||||
use galaxyui_core::presenter::ChildView;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
platform, AddWindowOptions, AppContext, AssetProvider, Element, Entity, SingletonEntity as _,
|
||||
TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use onboarding::{
|
||||
AgentOnboardingEvent, AgentOnboardingView, MockTelemetryContextProvider, SelectedSettings,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, RustEmbed)]
|
||||
#[folder = "../../app/assets"]
|
||||
@@ -44,10 +49,14 @@ fn main() -> Result<()> {
|
||||
galaxy_logging::init(galaxy_logging::LogConfig {
|
||||
is_cli: false,
|
||||
log_destination: None,
|
||||
..Default::default()
|
||||
})?;
|
||||
|
||||
let app_builder =
|
||||
platform::AppBuilder::new(platform::AppCallbacks::default(), Box::new(ASSETS), None);
|
||||
let app_builder = warpui::platform::AppBuilder::new(
|
||||
platform::AppCallbacks::default(),
|
||||
Box::new(ASSETS),
|
||||
None,
|
||||
);
|
||||
let _ = app_builder.run(move |ctx| {
|
||||
// Register Appearance singleton so views can access Appearance::handle(ctx).
|
||||
ctx.add_singleton_model(|ctx| build_appearance(phenomenon(), ctx));
|
||||
@@ -84,26 +93,23 @@ impl OnboardingMainView {
|
||||
id: LLMId::from("auto"),
|
||||
title: "Auto".to_string(),
|
||||
icon: Icon::Oz,
|
||||
requires_upgrade: false,
|
||||
is_default: true,
|
||||
},
|
||||
OnboardingModelInfo {
|
||||
id: LLMId::from("claude-sonnet"),
|
||||
title: "Claude Sonnet".to_string(),
|
||||
icon: Icon::ClaudeLogo,
|
||||
requires_upgrade: false,
|
||||
is_default: false,
|
||||
},
|
||||
OnboardingModelInfo {
|
||||
id: LLMId::from("gpt-4o"),
|
||||
title: "GPT-4o".to_string(),
|
||||
icon: Icon::OpenAILogo,
|
||||
requires_upgrade: true,
|
||||
is_default: false,
|
||||
},
|
||||
];
|
||||
let onboarding_view = ctx.add_typed_action_view(move |ctx| {
|
||||
// agent_modality_enabled and no_ai_experiment are false for demo purposes
|
||||
// agent_modality_enabled is false for demo purposes
|
||||
AgentOnboardingView::new(
|
||||
themes.clone(),
|
||||
true,
|
||||
@@ -111,8 +117,6 @@ impl OnboardingMainView {
|
||||
default_model_id.clone(),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
onboarding::OnboardingAuthState::LoggedOut,
|
||||
ctx,
|
||||
)
|
||||
@@ -266,7 +270,7 @@ impl View for OnboardingMainView {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_focus(&mut self, focus_ctx: &galaxyui::FocusContext, ctx: &mut ViewContext<Self>) {
|
||||
fn on_focus(&mut self, focus_ctx: &galaxyui_core::FocusContext, ctx: &mut ViewContext<Self>) {
|
||||
if let OnboardingMainState::Onboarding(view) = &self.state {
|
||||
if focus_ctx.is_self_focused() {
|
||||
ctx.focus(view);
|
||||
|
||||
@@ -4,6 +4,6 @@ mod view;
|
||||
pub use model::{FinalState, OnboardingQuery};
|
||||
pub use view::{OnboardingCalloutView, OnboardingCalloutViewEvent, OnboardingKeybindings};
|
||||
|
||||
pub fn init(app: &mut galaxyui::AppContext) {
|
||||
pub fn init(app: &mut galaxyui_core::AppContext) {
|
||||
view::init(app);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui_core::{Entity, ModelContext};
|
||||
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::OnboardingIntention;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui::{Entity, ModelContext};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum FinalState {
|
||||
@@ -64,14 +65,10 @@ pub(super) enum UniversalInputCalloutState {
|
||||
pub(super) enum AgentModalityCalloutState {
|
||||
#[default]
|
||||
Off,
|
||||
/// Step 1: "Meet your terminal input" / "Meet your updated terminal input"
|
||||
MeetTerminalInput,
|
||||
/// Step 2: "Natural language support" with checkbox
|
||||
NaturalLanguageSupport,
|
||||
/// Step 3: "Introducing Warp's new agent experience" (Agent intention only)
|
||||
IntroducingAgentExperience,
|
||||
/// Step 4: "Updated agent input" (Agent intention only)
|
||||
UpdatedAgentInput,
|
||||
/// Step 1: terminal input with natural language support.
|
||||
TerminalMode,
|
||||
/// Step 2: "Agent Mode" (Agent intention only).
|
||||
AgentMode,
|
||||
/// Terminal state
|
||||
Complete(FinalState),
|
||||
}
|
||||
@@ -187,13 +184,9 @@ impl OnboardingCalloutModel {
|
||||
) {
|
||||
let (next_state, emit_enter_agent_modality) = match state {
|
||||
AgentModalityCalloutState::Off => {
|
||||
(Some(AgentModalityCalloutState::MeetTerminalInput), false)
|
||||
(Some(AgentModalityCalloutState::TerminalMode), false)
|
||||
}
|
||||
AgentModalityCalloutState::MeetTerminalInput => (
|
||||
Some(AgentModalityCalloutState::NaturalLanguageSupport),
|
||||
false,
|
||||
),
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
AgentModalityCalloutState::TerminalMode => {
|
||||
// For Terminal intention, finish here
|
||||
// For Agent intention, continue to IntroducingAgentExperience
|
||||
match self.intention {
|
||||
@@ -203,17 +196,11 @@ impl OnboardingCalloutModel {
|
||||
),
|
||||
OnboardingIntention::AgentDrivenDevelopment => {
|
||||
// Signal to enter agent modality when showing the agent experience slide
|
||||
(
|
||||
Some(AgentModalityCalloutState::IntroducingAgentExperience),
|
||||
true,
|
||||
)
|
||||
(Some(AgentModalityCalloutState::AgentMode), true)
|
||||
}
|
||||
}
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => {
|
||||
(Some(AgentModalityCalloutState::UpdatedAgentInput), false)
|
||||
}
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
AgentModalityCalloutState::AgentMode => {
|
||||
// For Agent with project: Initialize
|
||||
// For Agent without project: Finish
|
||||
let final_state = if self.has_project {
|
||||
@@ -246,7 +233,9 @@ impl OnboardingCalloutModel {
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::AgentMode)
|
||||
if self.has_project =>
|
||||
{
|
||||
// Skip initialization
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
@@ -272,9 +261,7 @@ impl OnboardingCalloutModel {
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::NaturalLanguageSupport,
|
||||
) => {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::TerminalMode) => {
|
||||
// Terminal intention finishes here
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
@@ -283,7 +270,9 @@ impl OnboardingCalloutModel {
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::AgentMode)
|
||||
if !self.has_project =>
|
||||
{
|
||||
// Agent without project finishes here
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
@@ -296,10 +285,10 @@ impl OnboardingCalloutModel {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle "Back to terminal" action (ESC in UpdatedAgentInput without project)
|
||||
/// Handle "Back to terminal" action (ESC in IntroducingAgentExperience).
|
||||
pub fn back_to_terminal(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
match &self.state {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::AgentMode) => {
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::Complete(
|
||||
FinalState::BackToTerminal,
|
||||
@@ -342,17 +331,11 @@ impl OnboardingCalloutModel {
|
||||
OnboardingCalloutState::UniversalInput(UniversalInputCalloutState::TalkToAgent) => {
|
||||
Some("talk_to_agent")
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::MeetTerminalInput) => {
|
||||
Some("meet_terminal_input")
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::TerminalMode) => {
|
||||
Some("natural_language_support")
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::NaturalLanguageSupport,
|
||||
) => Some("natural_language_support"),
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::IntroducingAgentExperience,
|
||||
) => Some("introducing_agent_experience"),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput) => {
|
||||
Some("updated_agent_input")
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::AgentMode) => {
|
||||
Some("introducing_agent_experience")
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
@@ -435,16 +418,10 @@ impl OnboardingCalloutModel {
|
||||
fn prompt_for_agent_modality(&self, state: AgentModalityCalloutState) -> OnboardingQuery {
|
||||
match state {
|
||||
AgentModalityCalloutState::Off => OnboardingQuery::None,
|
||||
AgentModalityCalloutState::MeetTerminalInput => {
|
||||
AgentModalityCalloutState::TerminalMode => {
|
||||
OnboardingQuery::TerminalCommand("Run a command...".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
OnboardingQuery::AgentPrompt("help me terraform my Gcloud setup".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => {
|
||||
OnboardingQuery::AgentPrompt("Tell the agent what to build...".to_string())
|
||||
}
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
AgentModalityCalloutState::AgentMode => {
|
||||
if self.has_project {
|
||||
OnboardingQuery::AgentPrompt("/init".to_string())
|
||||
} else {
|
||||
@@ -470,11 +447,9 @@ impl OnboardingCalloutModel {
|
||||
);
|
||||
}
|
||||
OnboardingCalloutState::AgentModality(_) => {
|
||||
log::info!("Transitioning to AgentModality::MeetTerminalInput");
|
||||
log::info!("Transitioning to AgentModality::NaturalLanguageSupport");
|
||||
self.set_state(
|
||||
OnboardingCalloutState::AgentModality(
|
||||
AgentModalityCalloutState::MeetTerminalInput,
|
||||
),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::TerminalMode),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use ui_components::Component;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
elements::Empty,
|
||||
keymap::{macros::*, FixedBinding, Keystroke},
|
||||
use galaxyui_core::elements::Empty;
|
||||
use galaxyui_core::keymap::macros::*;
|
||||
use galaxyui_core::keymap::{FixedBinding, Keystroke};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, EventContext, ModelHandle, SingletonEntity, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::Component;
|
||||
|
||||
/// Display strings for keybindings shown in the onboarding callout.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -16,16 +17,16 @@ pub struct OnboardingKeybindings {
|
||||
pub submit_to_local_agent: String,
|
||||
/// Display string for submitting to cloud agent (e.g., "⌘⌥⏎")
|
||||
pub submit_to_cloud_agent: String,
|
||||
/// Display string for returning to terminal mode (e.g., "Esc")
|
||||
pub return_to_terminal_mode: String,
|
||||
}
|
||||
|
||||
use crate::{
|
||||
callout::model::{
|
||||
AgentModalityCalloutState, FinalState, OnboardingCalloutModel, OnboardingCalloutModelEvent,
|
||||
OnboardingCalloutState, OnboardingQuery, UniversalInputCalloutState,
|
||||
},
|
||||
components::onboarding_callout::{self, Button, StepStatus},
|
||||
OnboardingIntention,
|
||||
use crate::callout::model::{
|
||||
AgentModalityCalloutState, FinalState, OnboardingCalloutModel, OnboardingCalloutModelEvent,
|
||||
OnboardingCalloutState, OnboardingQuery, UniversalInputCalloutState,
|
||||
};
|
||||
use crate::components::onboarding_callout::{self, Button, StepStatus};
|
||||
use crate::OnboardingIntention;
|
||||
|
||||
/// Options for rendering a callout.
|
||||
struct CalloutOptions {
|
||||
@@ -105,46 +106,23 @@ fn get_agent_modality_callout_options(
|
||||
keybindings: &OnboardingKeybindings,
|
||||
) -> Option<CalloutOptions> {
|
||||
let total_steps = match intention {
|
||||
OnboardingIntention::Terminal => 2,
|
||||
OnboardingIntention::AgentDrivenDevelopment => 4,
|
||||
OnboardingIntention::Terminal => 1,
|
||||
OnboardingIntention::AgentDrivenDevelopment => 2,
|
||||
};
|
||||
|
||||
match state {
|
||||
AgentModalityCalloutState::MeetTerminalInput => {
|
||||
let title: &'static str = if has_project || intention == OnboardingIntention::Terminal {
|
||||
"Meet your terminal input"
|
||||
} else {
|
||||
"Meet your updated terminal input"
|
||||
};
|
||||
Some(CalloutOptions {
|
||||
title,
|
||||
text: format!(
|
||||
"Run commands from the terminal, or use {} or {} to start or send to a local or cloud agent respectively.",
|
||||
keybindings.submit_to_local_agent,
|
||||
keybindings.submit_to_cloud_agent
|
||||
),
|
||||
step: StepStatus::new(0, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: "Next",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
})
|
||||
}
|
||||
AgentModalityCalloutState::NaturalLanguageSupport => {
|
||||
AgentModalityCalloutState::TerminalMode => {
|
||||
let is_final_step = intention == OnboardingIntention::Terminal;
|
||||
// Show different callout content based on initial NL detection state
|
||||
if initial_natural_language_detection_enabled {
|
||||
// NL detection was already enabled - show simpler "overrides" callout without checkbox
|
||||
Some(CalloutOptions {
|
||||
title: "Natural language overrides",
|
||||
title: "Welcome to terminal mode",
|
||||
text: format!(
|
||||
"You can always override any auto-detection using {}.",
|
||||
"Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.",
|
||||
keybindings.toggle_input_mode
|
||||
),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
step: StepStatus::new(0, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: if is_final_step { "Finish" } else { "Next" },
|
||||
@@ -156,12 +134,12 @@ fn get_agent_modality_callout_options(
|
||||
} else {
|
||||
// NL detection was disabled - show full explanation with checkbox to enable
|
||||
Some(CalloutOptions {
|
||||
title: "Natural language support",
|
||||
title: "You’re in terminal mode",
|
||||
text: format!(
|
||||
"Natural language input is off by default. If enabled, you can type requests in plain English and Warp will autodetect queries for the agent. You can always override them using {}.",
|
||||
"Run commands here, just like a regular terminal. If you type a question or task using natural language, Warp can suggest opening it in agent mode. You can always override using {}.",
|
||||
keybindings.toggle_input_mode
|
||||
),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
step: StepStatus::new(0, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: if is_final_step { "Finish" } else { "Next" },
|
||||
@@ -175,24 +153,12 @@ fn get_agent_modality_callout_options(
|
||||
})
|
||||
}
|
||||
}
|
||||
AgentModalityCalloutState::IntroducingAgentExperience => Some(CalloutOptions {
|
||||
title: "Introducing Warp's new agent experience",
|
||||
text: "Agent conversations are now their own scoped view outside of your terminal. Simply hit ESC to return to the terminal at any point.".to_string(),
|
||||
step: StepStatus::new(2, total_steps),
|
||||
left_button: None,
|
||||
right_button: ButtonOptions {
|
||||
text: "Next",
|
||||
action: OnboardingCalloutViewAction::NextClicked,
|
||||
keystroke: Some(Keystroke::parse("enter").unwrap_or_default()),
|
||||
},
|
||||
checkbox: None,
|
||||
}),
|
||||
AgentModalityCalloutState::UpdatedAgentInput => {
|
||||
AgentModalityCalloutState::AgentMode => {
|
||||
if has_project {
|
||||
Some(CalloutOptions {
|
||||
title: "Updated agent input",
|
||||
text: "Your agent input will detect natural language as well as commands by default. Use ! to lock the input in bash mode to write commands.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!".to_string(),
|
||||
step: StepStatus::new(3, total_steps),
|
||||
title: "You're in agent mode",
|
||||
text: "Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow.\n\nSubmit the query below to have the agent initialize this project, or ⊗ to clear the input and start your own!".to_string(),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
left_button: Some(ButtonOptions {
|
||||
text: "Skip initialization",
|
||||
action: OnboardingCalloutViewAction::SkipClicked,
|
||||
@@ -207,9 +173,12 @@ fn get_agent_modality_callout_options(
|
||||
})
|
||||
} else {
|
||||
Some(CalloutOptions {
|
||||
title: "Updated agent input",
|
||||
text: "Your agent input will detect natural language as well as commands by default. Use ! to lock the input in bash mode to write commands.".to_string(),
|
||||
step: StepStatus::new(3, total_steps),
|
||||
title: "You're in agent mode",
|
||||
text: format!(
|
||||
"Agent mode gives your questions and tasks their own conversation, so you can ask follow-ups without leaving your terminal workflow. Press {} to return to terminal mode at any point.",
|
||||
keybindings.return_to_terminal_mode
|
||||
),
|
||||
step: StepStatus::new(1, total_steps),
|
||||
left_button: Some(ButtonOptions {
|
||||
text: "Back to terminal",
|
||||
action: OnboardingCalloutViewAction::BackToTerminalClicked,
|
||||
@@ -378,11 +347,11 @@ impl OnboardingCalloutView {
|
||||
}
|
||||
|
||||
/// Returns true if the callout should be positioned above the zero state.
|
||||
/// For UpdatedAgentInput state, always position relative to the input box instead.
|
||||
/// For the agent experience state, always position relative to the input box instead.
|
||||
pub fn should_position_above_zero_state(&self, app: &AppContext) -> bool {
|
||||
!matches!(
|
||||
self.model.as_ref(app).state(),
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::UpdatedAgentInput)
|
||||
OnboardingCalloutState::AgentModality(AgentModalityCalloutState::AgentMode)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, MainAxisAlignment, MainAxisSize, ParentElement, Radius, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
/// Content for a "you'll lose these features" opt-out confirmation dialog.
|
||||
pub struct FeatureOptOutDialog {
|
||||
pub title: &'static str,
|
||||
pub body: &'static str,
|
||||
pub features: &'static [&'static str],
|
||||
pub close_button: Box<dyn Element>,
|
||||
pub cancel_button: Box<dyn Element>,
|
||||
pub confirm_button: Box<dyn Element>,
|
||||
}
|
||||
|
||||
pub fn render_feature_optout_dialog(
|
||||
appearance: &Appearance,
|
||||
dialog: FeatureOptOutDialog,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let dialog_surface = theme.surface_1();
|
||||
let dialog_surface_solid = dialog_surface.into_solid();
|
||||
let border_color = internal_colors::neutral_4(theme);
|
||||
|
||||
let title = FormattedTextElement::from_str(dialog.title, appearance.ui_font_family(), 16.)
|
||||
.with_color(internal_colors::text_main(theme, dialog_surface_solid))
|
||||
.with_weight(Weight::Bold)
|
||||
.with_line_height_ratio(1.25)
|
||||
.finish();
|
||||
|
||||
let title_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(Shrinkable::new(1., title).finish())
|
||||
.with_child(dialog.close_button)
|
||||
.finish();
|
||||
|
||||
let body_text = FormattedTextElement::from_str(dialog.body, appearance.ui_font_family(), 14.)
|
||||
.with_color(internal_colors::text_main(theme, dialog_surface_solid))
|
||||
.with_weight(Weight::Normal)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let mut body_section = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(body_text);
|
||||
|
||||
// The list is optional: callers that only want a warning (e.g. the no-AI
|
||||
// onboarding modal) pass an empty slice, in which case we skip it entirely.
|
||||
if !dialog.features.is_empty() {
|
||||
let feature_row_color: ColorU = theme.foreground().into();
|
||||
let feature_x_fill = Fill::Solid(theme.ansi_fg_red());
|
||||
let mut feature_list =
|
||||
Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
for &item in dialog.features {
|
||||
let icon_el = ConstrainedBox::new(Icon::X.to_warpui_icon(feature_x_fill).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish();
|
||||
let text_el = FormattedTextElement::from_str(item, appearance.ui_font_family(), 14.)
|
||||
.with_color(feature_row_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
let row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon_el)
|
||||
.with_child(Container::new(text_el).with_margin_left(4.).finish())
|
||||
.finish();
|
||||
feature_list = feature_list.with_child(
|
||||
Container::new(row)
|
||||
.with_padding_top(4.)
|
||||
.with_padding_bottom(4.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
body_section = body_section.with_child(
|
||||
Container::new(feature_list.finish())
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
let body_section = body_section.finish();
|
||||
|
||||
let footer = Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::End)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(dialog.cancel_button)
|
||||
.with_child(
|
||||
Container::new(dialog.confirm_button)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_border(Border::top(1.).with_border_color(border_color))
|
||||
.with_horizontal_padding(24.)
|
||||
.with_vertical_padding(12.)
|
||||
.finish();
|
||||
|
||||
let dialog_body = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(title_row)
|
||||
.with_horizontal_padding(24.)
|
||||
.with_padding_top(24.)
|
||||
.with_padding_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(body_section)
|
||||
.with_horizontal_padding(24.)
|
||||
.with_padding_bottom(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(footer)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(dialog_body)
|
||||
.with_background(dialog_surface)
|
||||
.with_border(Border::all(1.).with_border_color(border_color))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.finish(),
|
||||
)
|
||||
.with_width(460.)
|
||||
.finish()
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod feature_optout_dialog;
|
||||
pub mod onboarding_callout;
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance,
|
||||
color::{coloru_with_opacity, contrast::relative_luminance},
|
||||
theme::{phenomenon::PhenomenonStyle, Fill},
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Rect,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
prelude::*,
|
||||
ui_components::checkbox::Checkbox as WarpCheckbox,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{
|
||||
button, button::Button as ButtonComponent, Component, MouseEventHandler, Options as _,
|
||||
use ui_components::button::Button as ButtonComponent;
|
||||
use ui_components::{button, Component, MouseEventHandler, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::color::coloru_with_opacity;
|
||||
use galaxy_core::ui::color::contrast::relative_luminance;
|
||||
use galaxy_core::ui::theme::phenomenon::PhenomenonStyle;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DropShadow, Flex,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, ParentElement, Radius, Rect,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::prelude::*;
|
||||
use galaxyui_core::ui_components::checkbox::Checkbox as WarpCheckbox;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
|
||||
const CALLOUT_WIDTH: f32 = 480.;
|
||||
const CALLOUT_BORDER_WIDTH: f32 = 1.;
|
||||
|
||||
@@ -24,17 +24,16 @@ impl std::fmt::Display for OnboardingIntention {
|
||||
|
||||
pub use callout::{OnboardingCalloutView, OnboardingKeybindings};
|
||||
|
||||
/// User-facing names of the AI features enabled when the agent intention is selected.
|
||||
/// User-facing descriptions of the AI features enabled when the agent intention is selected.
|
||||
/// Shared by the intention slide's agent card checklist and the login slide's
|
||||
/// skip-login confirmation dialog so the two always stay in sync.
|
||||
pub const AI_FEATURES: &[&str] = &[
|
||||
"Warp agents",
|
||||
"Oz cloud agents platform",
|
||||
"Next command predictions",
|
||||
"Prompt suggestions",
|
||||
"Codebase context",
|
||||
"Remote control with Claude Code, Codex, and other agents",
|
||||
"Agents over SSH",
|
||||
"Use frontier and open-weight models with Warp Agent",
|
||||
"Hand off agent work to cloud agents",
|
||||
"Automatically diagnose and fix terminal errors",
|
||||
"Agentic control of long-running commands and TUIs",
|
||||
"Review code diffs and send comments directly to agents",
|
||||
"Remote control for Claude Code, Codex, and other agents",
|
||||
];
|
||||
|
||||
/// User-facing names of the Warp Drive features enabled when the terminal
|
||||
@@ -76,7 +75,7 @@ pub use model::{OnboardingAuthState, SelectedSettings, UICustomizationSettings};
|
||||
pub use slides::ProjectOnboardingSettings;
|
||||
pub use telemetry::OnboardingEvent;
|
||||
|
||||
pub fn init(app: &mut galaxyui::AppContext) {
|
||||
pub fn init(app: &mut galaxyui_core::AppContext) {
|
||||
agent_onboarding_view::init(app);
|
||||
callout::init(app);
|
||||
}
|
||||
|
||||
+316
-81
@@ -1,11 +1,12 @@
|
||||
use ai::LLMId;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui_core::{Entity, ModelContext};
|
||||
|
||||
use crate::slides::{
|
||||
AgentAutonomy, AgentDevelopmentSettings, OnboardingModelInfo, ProjectOnboardingSettings,
|
||||
};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::OnboardingIntention;
|
||||
use ai::LLMId;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxyui::{Entity, ModelContext};
|
||||
|
||||
/// UI customization settings chosen during the "Customize your UI" onboarding slide.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -80,9 +81,12 @@ impl SelectedSettings {
|
||||
pub fn is_ai_enabled(&self) -> bool {
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
match self {
|
||||
SelectedSettings::AgentDrivenDevelopment { agent_settings, .. } => {
|
||||
!agent_settings.disable_oz
|
||||
}
|
||||
// Agent-driven development always means "I want AI" (including the
|
||||
// bring-your-own-agents `disable_oz` path). This reflects intent and
|
||||
// is used to decide that an account/login is required; whether AI is
|
||||
// actually enabled is applied later based on whether the user has an
|
||||
// account (see `apply_onboarding_settings`).
|
||||
SelectedSettings::AgentDrivenDevelopment { .. } => true,
|
||||
SelectedSettings::Terminal { .. } => {
|
||||
// With old onboarding (no OpenWarpNewSettingsModes), Terminal
|
||||
// intent still leaves AI enabled; with new onboarding,
|
||||
@@ -114,13 +118,57 @@ impl SelectedSettings {
|
||||
pub(crate) enum OnboardingStep {
|
||||
Intro,
|
||||
Intention,
|
||||
AiSetup,
|
||||
Customize,
|
||||
Agent,
|
||||
AiAccess,
|
||||
ThirdParty,
|
||||
Project,
|
||||
ThemePicker,
|
||||
}
|
||||
|
||||
/// The AI setup selected on the "Choose your AI setup" slide.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum AiSetupChoice {
|
||||
#[default]
|
||||
WarpAgent,
|
||||
ThirdParty,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AiSetupChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AiSetupChoice::WarpAgent => write!(f, "warp_agent"),
|
||||
AiSetupChoice::ThirdParty => write!(f, "third_party"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The access method selected on the "Choose how to access AI" slide (Warp Agent path).
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum AiAccessChoice {
|
||||
#[default]
|
||||
Subscription,
|
||||
SetUpLater,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AiAccessChoice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AiAccessChoice::Subscription => write!(f, "subscription"),
|
||||
AiAccessChoice::SetUpLater => write!(f, "set_up_later"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Which opt-out entry point opened the "Are you sure you don't want AI?" modal.
|
||||
/// Determines where "Give me AI features" routes the user on cancel.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum NoAiConfirmationSource {
|
||||
/// Triggered from the intention slide via "Just use the terminal" + Next.
|
||||
Intention,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum OnboardingStateEvent {
|
||||
ModelsUpdated,
|
||||
@@ -129,6 +177,7 @@ pub(crate) enum OnboardingStateEvent {
|
||||
Completed,
|
||||
UpgradeRequested,
|
||||
AuthStateChanged,
|
||||
NoAiConfirmationChanged,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -143,15 +192,15 @@ pub(crate) struct OnboardingStateModel {
|
||||
workspace_enforces_autonomy: bool,
|
||||
/// Whether the AgentView feature flag is enabled.
|
||||
agent_modality_enabled: bool,
|
||||
/// Whether the user is in the FreeUserNoAi experiment group (and is free tier).
|
||||
/// When true, the Agent Driven Development option on the intention slide is locked
|
||||
/// behind an upgrade CTA.
|
||||
free_user_no_ai_experiment: bool,
|
||||
/// Yearly price per month in USD cents for the agent plan badge.
|
||||
/// When `None`, falls back to a hardcoded default ($18/mo).
|
||||
agent_price_cents: Option<i32>,
|
||||
/// The AI setup selected on the "Choose your AI setup" slide.
|
||||
ai_setup_choice: AiSetupChoice,
|
||||
/// The access method selected on the "Choose how to access AI" slide.
|
||||
ai_access_choice: AiAccessChoice,
|
||||
/// Auth / billing state of the user.
|
||||
auth_state: OnboardingAuthState,
|
||||
/// When set, the "Are you sure you don't want AI?" confirmation modal is
|
||||
/// shown; the value records which entry point triggered it.
|
||||
no_ai_confirmation: Option<NoAiConfirmationSource>,
|
||||
}
|
||||
|
||||
impl OnboardingStateModel {
|
||||
@@ -161,8 +210,6 @@ impl OnboardingStateModel {
|
||||
default_model_id: LLMId,
|
||||
workspace_enforces_autonomy: bool,
|
||||
agent_modality_enabled: bool,
|
||||
free_user_no_ai_experiment: bool,
|
||||
agent_price_cents: Option<i32>,
|
||||
auth_state: OnboardingAuthState,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -174,9 +221,10 @@ impl OnboardingStateModel {
|
||||
models,
|
||||
workspace_enforces_autonomy,
|
||||
agent_modality_enabled,
|
||||
free_user_no_ai_experiment,
|
||||
agent_price_cents,
|
||||
ai_setup_choice: AiSetupChoice::default(),
|
||||
ai_access_choice: AiAccessChoice::default(),
|
||||
auth_state,
|
||||
no_ai_confirmation: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,7 +245,6 @@ impl OnboardingStateModel {
|
||||
}
|
||||
|
||||
pub(crate) fn settings(&self) -> SelectedSettings {
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
let ui_customization = if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
Some(self.ui_customization.clone())
|
||||
} else {
|
||||
@@ -256,6 +303,111 @@ impl OnboardingStateModel {
|
||||
self.agent_modality_enabled
|
||||
}
|
||||
|
||||
/// Whether the DES-816 V3 onboarding flow (the "Choose your AI setup" fork on the
|
||||
/// AI-first path) is active. True for all users when the new settings-modes flow
|
||||
/// is enabled, since new users always enter a world where Warp-provided AI is not free.
|
||||
pub(crate) fn ai_setup_flow_active(&self) -> bool {
|
||||
FeatureFlag::OpenWarpNewSettingsModes.is_enabled()
|
||||
}
|
||||
|
||||
pub(crate) fn ai_setup_choice(&self) -> AiSetupChoice {
|
||||
self.ai_setup_choice
|
||||
}
|
||||
|
||||
pub(crate) fn ai_access_choice(&self) -> AiAccessChoice {
|
||||
self.ai_access_choice
|
||||
}
|
||||
|
||||
pub(crate) fn set_ai_setup_choice(
|
||||
&mut self,
|
||||
choice: AiSetupChoice,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.ai_setup_choice == choice {
|
||||
return;
|
||||
}
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SettingChanged {
|
||||
setting: "ai_setup".to_string(),
|
||||
value: choice.to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
self.ai_setup_choice = choice;
|
||||
self.agent_settings.disable_oz = matches!(choice, AiSetupChoice::ThirdParty);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn set_ai_access_choice(
|
||||
&mut self,
|
||||
choice: AiAccessChoice,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.ai_access_choice == choice {
|
||||
return;
|
||||
}
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SettingChanged {
|
||||
setting: "ai_access".to_string(),
|
||||
value: choice.to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
self.ai_access_choice = choice;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn no_ai_confirmation(&self) -> Option<NoAiConfirmationSource> {
|
||||
self.no_ai_confirmation
|
||||
}
|
||||
|
||||
/// Shows the "Are you sure you don't want AI?" confirmation modal, recording
|
||||
/// which opt-out entry point triggered it so cancel can route appropriately.
|
||||
pub(crate) fn request_no_ai_confirmation(
|
||||
&mut self,
|
||||
source: NoAiConfirmationSource,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::NoAiConfirmationShown, ctx);
|
||||
self.no_ai_confirmation = Some(source);
|
||||
ctx.emit(OnboardingStateEvent::NoAiConfirmationChanged);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// "I don't want AI": commit to the terminal-only path (AI features off) and
|
||||
/// continue the flow there, so declining AI never dead-ends onboarding.
|
||||
pub(crate) fn confirm_no_ai(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::NoAiConfirmed, ctx);
|
||||
self.no_ai_confirmation = None;
|
||||
self.set_intention(OnboardingIntention::Terminal, ctx);
|
||||
self.set_step(OnboardingStep::Customize, ctx);
|
||||
}
|
||||
|
||||
/// "Give me AI features": abort the opt-out. The only trigger is the
|
||||
/// intention slide's "Just use the terminal", which is an explicit request
|
||||
/// for AI, so route onto the AI path.
|
||||
pub(crate) fn cancel_no_ai(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::NoAiConfirmationCancelled, ctx);
|
||||
match self.no_ai_confirmation.take() {
|
||||
Some(NoAiConfirmationSource::Intention) => {
|
||||
self.set_intention(OnboardingIntention::AgentDrivenDevelopment, ctx);
|
||||
self.set_step(OnboardingStep::AiSetup, ctx);
|
||||
}
|
||||
None => {
|
||||
ctx.emit(OnboardingStateEvent::NoAiConfirmationChanged);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the confirmation modal without changing the user's path (ESC / X).
|
||||
pub(crate) fn dismiss_no_ai(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
if self.no_ai_confirmation.take().is_some() {
|
||||
ctx.emit(OnboardingStateEvent::NoAiConfirmationChanged);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_customization(&self) -> &UICustomizationSettings {
|
||||
&self.ui_customization
|
||||
}
|
||||
@@ -290,28 +442,6 @@ impl OnboardingStateModel {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn free_user_no_ai_experiment(&self) -> bool {
|
||||
self.free_user_no_ai_experiment
|
||||
}
|
||||
|
||||
pub(crate) fn agent_price_badge(&self) -> String {
|
||||
const DEFAULT_AGENT_PRICE_CENTS: i32 = 1800;
|
||||
let cents = self.agent_price_cents.unwrap_or(DEFAULT_AGENT_PRICE_CENTS);
|
||||
format!("Starting at ${}/mo", cents / 100)
|
||||
}
|
||||
|
||||
pub(crate) fn set_agent_price_cents(
|
||||
&mut self,
|
||||
cents: Option<i32>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.agent_price_cents == cents {
|
||||
return;
|
||||
}
|
||||
self.agent_price_cents = cents;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn set_show_conversation_history(
|
||||
&mut self,
|
||||
value: bool,
|
||||
@@ -448,18 +578,6 @@ impl OnboardingStateModel {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn set_free_user_no_ai_experiment(
|
||||
&mut self,
|
||||
value: bool,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
if self.free_user_no_ai_experiment == value {
|
||||
return;
|
||||
}
|
||||
self.free_user_no_ai_experiment = value;
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub(crate) fn set_workspace_enforces_autonomy(
|
||||
&mut self,
|
||||
value: bool,
|
||||
@@ -512,13 +630,6 @@ impl OnboardingStateModel {
|
||||
self.set_intention(OnboardingIntention::AgentDrivenDevelopment, ctx);
|
||||
}
|
||||
|
||||
pub(crate) fn is_model_disabled(&self, model_id: &LLMId) -> bool {
|
||||
self.models
|
||||
.iter()
|
||||
.find(|m| &m.id == model_id)
|
||||
.is_some_and(|m| m.requires_upgrade)
|
||||
}
|
||||
|
||||
pub(crate) fn request_upgrade(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
ctx.emit(OnboardingStateEvent::UpgradeRequested);
|
||||
}
|
||||
@@ -528,10 +639,6 @@ impl OnboardingStateModel {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.is_model_disabled(&model_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SettingChanged {
|
||||
setting: "model".to_string(),
|
||||
@@ -551,7 +658,6 @@ impl OnboardingStateModel {
|
||||
default_model_id: LLMId,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) {
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
|
||||
// If the user is past the agent slide, don't change the agent model from underneath them.
|
||||
// When the new settings modes flag is on, ThemePicker comes after the agent slides
|
||||
@@ -631,12 +737,13 @@ impl OnboardingStateModel {
|
||||
}
|
||||
|
||||
fn send_completion_telemetry(&self, ctx: &mut ModelContext<Self>) {
|
||||
let (intention, model, autonomy) = match &self.intention {
|
||||
OnboardingIntention::Terminal => (self.intention.to_string(), None, None),
|
||||
let (intention, model, autonomy, ai_access) = match &self.intention {
|
||||
OnboardingIntention::Terminal => (self.intention.to_string(), None, None, None),
|
||||
OnboardingIntention::AgentDrivenDevelopment => (
|
||||
self.intention.to_string(),
|
||||
Some(self.agent_settings.selected_model_id.to_string()),
|
||||
self.agent_settings.autonomy.map(|x| x.to_string()),
|
||||
Some(self.ai_setup_choice.to_string()),
|
||||
),
|
||||
};
|
||||
|
||||
@@ -651,6 +758,7 @@ impl OnboardingStateModel {
|
||||
model,
|
||||
autonomy,
|
||||
has_project_path,
|
||||
ai_access,
|
||||
},
|
||||
ctx
|
||||
);
|
||||
@@ -663,27 +771,45 @@ impl OnboardingStateModel {
|
||||
}
|
||||
|
||||
pub(crate) fn back(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
let theme_picker_last = FeatureFlag::OpenWarpNewSettingsModes.is_enabled();
|
||||
let ai_setup_flow = self.ai_setup_flow_active();
|
||||
let agent_intention = matches!(self.intention, OnboardingIntention::AgentDrivenDevelopment);
|
||||
|
||||
let prev = if theme_picker_last {
|
||||
match self.step {
|
||||
OnboardingStep::Intro => None,
|
||||
OnboardingStep::Intention => Some(OnboardingStep::Intro),
|
||||
OnboardingStep::Customize => Some(OnboardingStep::Intention),
|
||||
OnboardingStep::Agent => Some(OnboardingStep::Customize),
|
||||
OnboardingStep::ThirdParty => match self.intention {
|
||||
OnboardingIntention::Terminal => Some(OnboardingStep::Customize),
|
||||
OnboardingIntention::AgentDrivenDevelopment => Some(OnboardingStep::Agent),
|
||||
},
|
||||
OnboardingStep::AiSetup => Some(OnboardingStep::Intention),
|
||||
OnboardingStep::Customize => {
|
||||
if ai_setup_flow && agent_intention {
|
||||
match self.ai_setup_choice {
|
||||
AiSetupChoice::WarpAgent => Some(OnboardingStep::AiAccess),
|
||||
AiSetupChoice::ThirdParty => Some(OnboardingStep::ThirdParty),
|
||||
}
|
||||
} else {
|
||||
Some(OnboardingStep::Intention)
|
||||
}
|
||||
}
|
||||
OnboardingStep::AiAccess => Some(OnboardingStep::Agent),
|
||||
OnboardingStep::Agent => {
|
||||
if ai_setup_flow {
|
||||
Some(OnboardingStep::AiSetup)
|
||||
} else {
|
||||
Some(OnboardingStep::Customize)
|
||||
}
|
||||
}
|
||||
OnboardingStep::ThirdParty => Some(OnboardingStep::AiSetup),
|
||||
OnboardingStep::Project => Some(OnboardingStep::ThirdParty),
|
||||
OnboardingStep::ThemePicker => Some(OnboardingStep::ThirdParty),
|
||||
OnboardingStep::ThemePicker => Some(OnboardingStep::Customize),
|
||||
}
|
||||
} else {
|
||||
match self.step {
|
||||
OnboardingStep::Intro => None,
|
||||
OnboardingStep::ThemePicker => Some(OnboardingStep::Intro),
|
||||
OnboardingStep::Intention => Some(OnboardingStep::ThemePicker),
|
||||
// Unreachable in the legacy flow.
|
||||
OnboardingStep::AiSetup => None,
|
||||
OnboardingStep::AiAccess => None,
|
||||
OnboardingStep::Customize => None,
|
||||
OnboardingStep::ThirdParty => None,
|
||||
OnboardingStep::Agent => Some(OnboardingStep::Intention),
|
||||
@@ -698,7 +824,6 @@ impl OnboardingStateModel {
|
||||
}
|
||||
|
||||
pub(crate) fn next(&mut self, ctx: &mut ModelContext<Self>) {
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
let theme_picker_last = FeatureFlag::OpenWarpNewSettingsModes.is_enabled();
|
||||
|
||||
let is_last_step = if theme_picker_last {
|
||||
@@ -711,17 +836,52 @@ impl OnboardingStateModel {
|
||||
}
|
||||
|
||||
if theme_picker_last {
|
||||
let ai_setup_flow = self.ai_setup_flow_active();
|
||||
match self.step {
|
||||
OnboardingStep::Intro => self.set_step(OnboardingStep::Intention, ctx),
|
||||
OnboardingStep::Intention => self.set_step(OnboardingStep::Customize, ctx),
|
||||
OnboardingStep::Customize => match self.intention {
|
||||
OnboardingIntention::Terminal => self.set_step(OnboardingStep::ThirdParty, ctx),
|
||||
OnboardingStep::Intention => match self.intention {
|
||||
OnboardingIntention::Terminal => self.set_step(OnboardingStep::Customize, ctx),
|
||||
OnboardingIntention::AgentDrivenDevelopment => {
|
||||
self.set_step(OnboardingStep::Agent, ctx)
|
||||
if ai_setup_flow {
|
||||
self.set_step(OnboardingStep::AiSetup, ctx)
|
||||
} else {
|
||||
self.set_step(OnboardingStep::Customize, ctx)
|
||||
}
|
||||
}
|
||||
},
|
||||
OnboardingStep::Agent => self.set_step(OnboardingStep::ThirdParty, ctx),
|
||||
OnboardingStep::ThirdParty => self.set_step(OnboardingStep::ThemePicker, ctx),
|
||||
OnboardingStep::AiSetup => match self.ai_setup_choice {
|
||||
AiSetupChoice::WarpAgent => self.set_step(OnboardingStep::Agent, ctx),
|
||||
AiSetupChoice::ThirdParty => self.set_step(OnboardingStep::ThirdParty, ctx),
|
||||
},
|
||||
OnboardingStep::Customize => match self.intention {
|
||||
OnboardingIntention::Terminal => {
|
||||
self.set_step(OnboardingStep::ThemePicker, ctx)
|
||||
}
|
||||
OnboardingIntention::AgentDrivenDevelopment => {
|
||||
if ai_setup_flow {
|
||||
self.set_step(OnboardingStep::ThemePicker, ctx)
|
||||
} else {
|
||||
self.set_step(OnboardingStep::Agent, ctx)
|
||||
}
|
||||
}
|
||||
},
|
||||
OnboardingStep::Agent => {
|
||||
if ai_setup_flow {
|
||||
self.set_step(OnboardingStep::AiAccess, ctx)
|
||||
} else {
|
||||
self.set_step(OnboardingStep::ThirdParty, ctx)
|
||||
}
|
||||
}
|
||||
OnboardingStep::AiAccess => self.set_step(OnboardingStep::Customize, ctx),
|
||||
OnboardingStep::ThirdParty => {
|
||||
if ai_setup_flow
|
||||
&& matches!(self.intention, OnboardingIntention::AgentDrivenDevelopment)
|
||||
{
|
||||
self.set_step(OnboardingStep::Customize, ctx)
|
||||
} else {
|
||||
self.set_step(OnboardingStep::ThemePicker, ctx)
|
||||
}
|
||||
}
|
||||
OnboardingStep::Project => self.set_step(OnboardingStep::ThemePicker, ctx),
|
||||
OnboardingStep::ThemePicker => {}
|
||||
}
|
||||
@@ -730,6 +890,9 @@ impl OnboardingStateModel {
|
||||
OnboardingStep::Intro => self.set_step(OnboardingStep::ThemePicker, ctx),
|
||||
OnboardingStep::ThemePicker => self.set_step(OnboardingStep::Intention, ctx),
|
||||
OnboardingStep::Intention => self.set_step(OnboardingStep::Agent, ctx),
|
||||
// Unreachable in the legacy flow.
|
||||
OnboardingStep::AiSetup => {}
|
||||
OnboardingStep::AiAccess => {}
|
||||
OnboardingStep::Customize => {}
|
||||
OnboardingStep::ThirdParty => {}
|
||||
OnboardingStep::Agent => self.set_step(OnboardingStep::Project, ctx),
|
||||
@@ -770,6 +933,22 @@ impl OnboardingStateModel {
|
||||
ctx
|
||||
);
|
||||
}
|
||||
OnboardingStep::AiSetup => {
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SlideViewed {
|
||||
slide_name: "ai_setup".to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
OnboardingStep::AiAccess => {
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SlideViewed {
|
||||
slide_name: "ai_access".to_string(),
|
||||
},
|
||||
ctx
|
||||
);
|
||||
}
|
||||
OnboardingStep::Customize => {
|
||||
send_telemetry_from_ctx!(
|
||||
OnboardingEvent::SlideViewed {
|
||||
@@ -807,8 +986,64 @@ impl OnboardingStateModel {
|
||||
ctx.emit(OnboardingStateEvent::SelectedSlideChanged);
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// The `(step_index, step_count)` shown by the bottom-nav progress dots for the
|
||||
/// current step, intention, and flow variant.
|
||||
pub(crate) fn progress(&self) -> (usize, usize) {
|
||||
|
||||
let is_terminal = matches!(self.intention, OnboardingIntention::Terminal);
|
||||
if !FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
// Legacy flow: ThemePicker → Intention → Agent → Project.
|
||||
return match self.step {
|
||||
OnboardingStep::Intro | OnboardingStep::ThemePicker => (0, 4),
|
||||
OnboardingStep::Intention | OnboardingStep::AiSetup | OnboardingStep::Customize => {
|
||||
(1, 4)
|
||||
}
|
||||
OnboardingStep::Agent | OnboardingStep::ThirdParty | OnboardingStep::AiAccess => {
|
||||
(2, 4)
|
||||
}
|
||||
OnboardingStep::Project => (3, 4),
|
||||
};
|
||||
}
|
||||
|
||||
// The Warp Agent path has the extra "Choose how to access AI" step, so it
|
||||
// is one longer than the third-party-agent path.
|
||||
let is_warp_agent_path =
|
||||
!is_terminal && matches!(self.ai_setup_choice, AiSetupChoice::WarpAgent);
|
||||
let step_count = if is_terminal {
|
||||
3
|
||||
} else if is_warp_agent_path {
|
||||
6
|
||||
} else {
|
||||
5
|
||||
};
|
||||
let step_index = match self.step {
|
||||
OnboardingStep::Intro | OnboardingStep::Intention => 0,
|
||||
OnboardingStep::AiSetup => 1,
|
||||
OnboardingStep::Agent => 2,
|
||||
OnboardingStep::AiAccess => 3,
|
||||
OnboardingStep::Customize => {
|
||||
if is_terminal {
|
||||
1
|
||||
} else if is_warp_agent_path {
|
||||
4
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
OnboardingStep::ThirdParty => 2,
|
||||
// Unreachable in the new flow; keep the legacy position.
|
||||
OnboardingStep::Project => 3,
|
||||
OnboardingStep::ThemePicker => step_count - 1,
|
||||
};
|
||||
(step_index, step_count)
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for OnboardingStateModel {
|
||||
type Event = OnboardingStateEvent;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "model_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
use ai::LLMId;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::telemetry::testing::MockTelemetryContextProvider;
|
||||
use galaxyui_core::{App, ModelHandle};
|
||||
|
||||
use crate::model::{
|
||||
AiSetupChoice, NoAiConfirmationSource, OnboardingAuthState, OnboardingStateModel,
|
||||
OnboardingStep, SelectedSettings,
|
||||
};
|
||||
use crate::OnboardingIntention;
|
||||
|
||||
fn add_test_model(app: &mut App) -> ModelHandle<OnboardingStateModel> {
|
||||
app.update(MockTelemetryContextProvider::register);
|
||||
app.add_model(|_| {
|
||||
OnboardingStateModel::new(
|
||||
Vec::new(),
|
||||
LLMId::from("auto"),
|
||||
false,
|
||||
true,
|
||||
OnboardingAuthState::FreeUser,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn step(app: &App, model: &ModelHandle<OnboardingStateModel>) -> OnboardingStep {
|
||||
model.read(app, |model, _| model.step())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_path_routes_through_ai_setup() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
// Default intention is agent-driven development.
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiSetup);
|
||||
|
||||
// The default AI setup choice is the Warp agent.
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Agent);
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiAccess);
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::ThemePicker);
|
||||
|
||||
// Back navigation mirrors the forward path.
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiAccess);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Agent);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiSetup);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_party_choice_routes_to_third_party_slide() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.next(ctx); // Intro → Intention
|
||||
model.next(ctx); // Intention → AiSetup
|
||||
model.set_ai_setup_choice(AiSetupChoice::ThirdParty, ctx);
|
||||
model.next(ctx); // AiSetup → ThirdParty
|
||||
});
|
||||
assert_eq!(step(&app, &model), OnboardingStep::ThirdParty);
|
||||
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
|
||||
// Back from Customize returns to the chosen AI-setup slide.
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::ThirdParty);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiSetup);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_no_ai_switches_to_terminal_path() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.next(ctx); // Intro → Intention
|
||||
model.set_intention_terminal(ctx);
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
});
|
||||
|
||||
// The confirmation modal is shown without leaving the intention slide yet.
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
model.read(&app, |model, _| {
|
||||
assert_eq!(
|
||||
model.no_ai_confirmation(),
|
||||
Some(NoAiConfirmationSource::Intention)
|
||||
);
|
||||
});
|
||||
|
||||
// Confirming "I don't want AI" lands on the terminal path, never a dead end.
|
||||
model.update(&mut app, |model, ctx| model.confirm_no_ai(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
model.read(&app, |model, _| {
|
||||
assert_eq!(model.no_ai_confirmation(), None);
|
||||
assert_eq!(*model.intention(), OnboardingIntention::Terminal);
|
||||
assert!(!model.settings().is_ai_enabled());
|
||||
});
|
||||
|
||||
// The terminal path continues to completion, skipping the third-party slide.
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::ThemePicker);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirm_no_ai_from_intention_then_back_returns_to_intention() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.next(ctx); // Intro → Intention
|
||||
model.set_intention_terminal(ctx);
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
});
|
||||
|
||||
// "Just use the terminal" + Next does not advance until the user confirms.
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
|
||||
model.update(&mut app, |model, ctx| model.confirm_no_ai(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
|
||||
// Back from Customize goes to the intention fork, not the AI-setup slide.
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_no_ai_from_intention_routes_to_ai_setup() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.next(ctx); // Intro → Intention
|
||||
model.set_intention_terminal(ctx);
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
});
|
||||
|
||||
// "Give me AI features" switches onto the AI path and opens the AI-setup slide.
|
||||
model.update(&mut app, |model, ctx| model.cancel_no_ai(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::AiSetup);
|
||||
model.read(&app, |model, _| {
|
||||
assert_eq!(model.no_ai_confirmation(), None);
|
||||
assert_eq!(
|
||||
*model.intention(),
|
||||
OnboardingIntention::AgentDrivenDevelopment
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dismiss_no_ai_closes_without_changing_path() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.next(ctx); // Intro → Intention
|
||||
model.set_intention_terminal(ctx);
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
model.dismiss_no_ai(ctx);
|
||||
});
|
||||
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
model.read(&app, |model, _| {
|
||||
assert_eq!(model.no_ai_confirmation(), None);
|
||||
assert_eq!(*model.intention(), OnboardingIntention::Terminal);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_settings_disable_ai() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
model.update(&mut app, |model, ctx| model.set_intention_terminal(ctx));
|
||||
model.read(&app, |model, _| {
|
||||
assert!(matches!(
|
||||
model.settings(),
|
||||
SelectedSettings::Terminal { .. }
|
||||
));
|
||||
assert!(!model.settings().is_ai_enabled());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_intent_keeps_ai_enabled_for_any_setup_choice() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
// Default agent intention + "Use Warp Agent" enables AI.
|
||||
model.read(&app, |model, _| assert!(model.settings().is_ai_enabled()));
|
||||
|
||||
// "Use third party agents" still keeps AI enabled: agent intent always
|
||||
// means the user wants AI, even when bringing their own agents.
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.set_ai_setup_choice(AiSetupChoice::ThirdParty, ctx)
|
||||
});
|
||||
model.read(&app, |model, _| assert!(model.settings().is_ai_enabled()));
|
||||
|
||||
// Switching back to Warp Agent also keeps AI enabled.
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.set_ai_setup_choice(AiSetupChoice::WarpAgent, ctx)
|
||||
});
|
||||
model.read(&app, |model, _| assert!(model.settings().is_ai_enabled()));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_path_skips_third_party() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
model.update(&mut app, |model, ctx| model.set_intention_terminal(ctx));
|
||||
|
||||
// Terminal goes Intention → Customize → ThemePicker; the "Customize third
|
||||
// party agents" slide is only for the agent → third-party choice.
|
||||
for expected in [
|
||||
OnboardingStep::Intention,
|
||||
OnboardingStep::Customize,
|
||||
OnboardingStep::ThemePicker,
|
||||
] {
|
||||
model.update(&mut app, |model, ctx| model.next(ctx));
|
||||
assert_eq!(step(&app, &model), expected);
|
||||
}
|
||||
|
||||
// Back navigation mirrors the forward path, also skipping third-party.
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Customize);
|
||||
model.update(&mut app, |model, ctx| model.back(ctx));
|
||||
assert_eq!(step(&app, &model), OnboardingStep::Intention);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_reports_v3_positions_for_agent_path() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
|
||||
// Warp Agent path: Intention → AiSetup → Agent → AiAccess → Customize → ThemePicker.
|
||||
let cases = [
|
||||
(OnboardingStep::Intention, (0, 6)),
|
||||
(OnboardingStep::AiSetup, (1, 6)),
|
||||
(OnboardingStep::Agent, (2, 6)),
|
||||
(OnboardingStep::AiAccess, (3, 6)),
|
||||
(OnboardingStep::Customize, (4, 6)),
|
||||
(OnboardingStep::ThemePicker, (5, 6)),
|
||||
];
|
||||
for (target, expected) in cases {
|
||||
model.update(&mut app, |model, ctx| model.set_step(target, ctx));
|
||||
let progress = model.read(&app, |model, _| model.progress());
|
||||
assert_eq!(progress, expected, "unexpected dots for {target:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_reports_v3_positions_for_third_party_path() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
model.update(&mut app, |model, ctx| {
|
||||
model.set_ai_setup_choice(AiSetupChoice::ThirdParty, ctx)
|
||||
});
|
||||
|
||||
// Third-party path has no "Choose how to access AI" step, so it is one
|
||||
// dot shorter than the Warp Agent path.
|
||||
let cases = [
|
||||
(OnboardingStep::Intention, (0, 5)),
|
||||
(OnboardingStep::AiSetup, (1, 5)),
|
||||
(OnboardingStep::ThirdParty, (2, 5)),
|
||||
(OnboardingStep::Customize, (3, 5)),
|
||||
(OnboardingStep::ThemePicker, (4, 5)),
|
||||
];
|
||||
for (target, expected) in cases {
|
||||
model.update(&mut app, |model, ctx| model.set_step(target, ctx));
|
||||
let progress = model.read(&app, |model, _| model.progress());
|
||||
assert_eq!(progress, expected, "unexpected dots for {target:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn progress_reports_terminal_path_uses_three_dot_variant() {
|
||||
let _flag = FeatureFlag::OpenWarpNewSettingsModes.override_enabled(true);
|
||||
App::test((), |mut app| async move {
|
||||
let model = add_test_model(&mut app);
|
||||
model.update(&mut app, |model, ctx| model.set_intention_terminal(ctx));
|
||||
let cases = [
|
||||
(OnboardingStep::Intention, (0, 3)),
|
||||
(OnboardingStep::Customize, (1, 3)),
|
||||
(OnboardingStep::ThemePicker, (2, 3)),
|
||||
];
|
||||
for (target, expected) in cases {
|
||||
model.update(&mut app, |model, ctx| model.set_step(target, ctx));
|
||||
let progress = model.read(&app, |model, _| model.progress());
|
||||
assert_eq!(progress, expected, "unexpected dots for {target:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,74 +1,33 @@
|
||||
use super::two_line_button::{render_two_line_button, TwoLineButtonSpec};
|
||||
use crate::model::{OnboardingAuthState, OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::visuals::agent_visual;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance,
|
||||
theme::{color::internal_colors, Fill},
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
AnchorPair, Border, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
||||
CornerRadius, CrossAxisAlignment, Dismiss, Empty, Flex, FormattedTextElement, Hoverable,
|
||||
Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
OffsetType, ParentElement, ParentOffsetBounds, PositioningAxis, Radius, SavePosition,
|
||||
ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Stack, Text, XAxisAnchor, YAxisAnchor,
|
||||
},
|
||||
fonts::Properties,
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
scene::DropShadow,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, Gradient, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
use ai::LLMId;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::button::State as ButtonState;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
AnchorPair, Border, ClippedScrollStateHandle, ClippedScrollable, ConstrainedBox, Container,
|
||||
CornerRadius, CrossAxisAlignment, Dismiss, Empty, Flex, FormattedTextElement, Hoverable,
|
||||
Icon as WarpUiIcon, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
OffsetType, ParentElement, ParentOffsetBounds, PositioningAxis, Radius, SavePosition,
|
||||
ScrollTarget, ScrollToPositionMode, ScrollbarWidth, Stack, Text, XAxisAnchor, YAxisAnchor,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::scene::DropShadow;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, SingletonEntity as _, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
/// high-contrast "inverted" fill (foreground color)
|
||||
struct UpgradeButtonTheme;
|
||||
|
||||
impl button::Theme for UpgradeButtonTheme {
|
||||
fn background(
|
||||
&self,
|
||||
button_state: ButtonState,
|
||||
appearance: &Appearance,
|
||||
) -> Option<galaxy_core::ui::theme::Fill> {
|
||||
use galaxy_core::ui::color::blend::Blend;
|
||||
let theme = appearance.theme();
|
||||
let base = theme.foreground();
|
||||
match button_state {
|
||||
ButtonState::Default => Some(base),
|
||||
// Blend a little of the theme background back in to dim on hover /
|
||||
// press. Opacities are relative to the foreground fill.
|
||||
ButtonState::Hovered => Some(base.blend(&theme.background().with_opacity(15))),
|
||||
ButtonState::Pressed => Some(base.blend(&theme.background().with_opacity(30))),
|
||||
}
|
||||
}
|
||||
|
||||
fn text_color(
|
||||
&self,
|
||||
background: Option<galaxy_core::ui::theme::Fill>,
|
||||
appearance: &Appearance,
|
||||
) -> ColorU {
|
||||
let bg = background
|
||||
.unwrap_or_else(|| appearance.theme().background())
|
||||
.into_solid();
|
||||
appearance.theme().font_color(bg).into()
|
||||
}
|
||||
}
|
||||
use super::two_line_button::{render_two_line_button, TwoLineButtonSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::agent_visual;
|
||||
|
||||
/// Information about a model displayed on the onboarding slide.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -76,7 +35,6 @@ pub struct OnboardingModelInfo {
|
||||
pub id: LLMId,
|
||||
pub title: String,
|
||||
pub icon: Icon,
|
||||
pub requires_upgrade: bool,
|
||||
pub is_default: bool,
|
||||
}
|
||||
|
||||
@@ -139,20 +97,10 @@ pub enum AgentSlideAction {
|
||||
ToggleDisableOz,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
UpgradeClicked,
|
||||
CopyUpgradeUrlClicked,
|
||||
PasteAuthTokenFromClipboardClicked,
|
||||
DismissPlanActivatedToast,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AgentSlideEvent {
|
||||
CopyUpgradeUrlRequested,
|
||||
PasteAuthTokenFromClipboardRequested,
|
||||
}
|
||||
|
||||
pub struct AgentSlide {
|
||||
onboarding_state: galaxyui::ModelHandle<OnboardingStateModel>,
|
||||
onboarding_state: galaxyui_core::ModelHandle<OnboardingStateModel>,
|
||||
|
||||
/// Mouse state handles for each model row.
|
||||
model_mouse_states: Vec<MouseStateHandle>,
|
||||
@@ -164,25 +112,14 @@ pub struct AgentSlide {
|
||||
autonomy_partial_mouse_state: MouseStateHandle,
|
||||
autonomy_none_mouse_state: MouseStateHandle,
|
||||
|
||||
disable_oz_mouse: MouseStateHandle,
|
||||
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
upgrade_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
dropdown_scroll_state: ClippedScrollStateHandle,
|
||||
is_model_list_expanded: bool,
|
||||
highlighted_model_id: Option<LLMId>,
|
||||
show_auth_prompt_bar: bool,
|
||||
copy_url_mouse_state: MouseStateHandle,
|
||||
paste_token_mouse_state: MouseStateHandle,
|
||||
show_plan_activated_toast: bool,
|
||||
last_auth_state: OnboardingAuthState,
|
||||
plan_activated_close_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
const PLAN_ACTIVATED_TOAST_DURATION: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
|
||||
/// Produces the `SavePosition` id for the model row at `index` in the
|
||||
/// dropdown list. Used by `scroll_to_position` to scroll a specific row into
|
||||
/// view as the keyboard highlight moves.
|
||||
@@ -190,18 +127,9 @@ fn model_row_position_id(index: usize) -> String {
|
||||
format!("agent_slide_model_row_{index}")
|
||||
}
|
||||
|
||||
/// Returns the slide's view of the model list: free-tier before premium,
|
||||
/// with server order preserved within each tier. The slide owns this sort so
|
||||
/// state storage can stay in server order.
|
||||
fn sorted_models(models: &[OnboardingModelInfo]) -> Vec<OnboardingModelInfo> {
|
||||
let (free, premium): (Vec<_>, Vec<_>) =
|
||||
models.iter().cloned().partition(|m| !m.requires_upgrade);
|
||||
free.into_iter().chain(premium).collect()
|
||||
}
|
||||
|
||||
impl AgentSlide {
|
||||
pub(crate) fn new(
|
||||
onboarding_state: galaxyui::ModelHandle<OnboardingStateModel>,
|
||||
onboarding_state: galaxyui_core::ModelHandle<OnboardingStateModel>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let model_count = onboarding_state.as_ref(ctx).models().len();
|
||||
@@ -209,8 +137,6 @@ impl AgentSlide {
|
||||
.map(|_| MouseStateHandle::default())
|
||||
.collect();
|
||||
|
||||
let initial_auth_state = onboarding_state.as_ref(ctx).auth_state();
|
||||
|
||||
ctx.subscribe_to_model(&onboarding_state, |me, model, event, ctx| {
|
||||
match event {
|
||||
OnboardingStateEvent::ModelsUpdated => {
|
||||
@@ -226,30 +152,12 @@ impl AgentSlide {
|
||||
let model_count = state.models().len();
|
||||
me.ensure_mouse_states_for_models(model_count, ctx);
|
||||
}
|
||||
OnboardingStateEvent::AuthStateChanged => {
|
||||
let new_state = model.as_ref(ctx).auth_state();
|
||||
if new_state == OnboardingAuthState::PayingUser
|
||||
&& me.last_auth_state != OnboardingAuthState::PayingUser
|
||||
{
|
||||
me.show_plan_activated_toast = true;
|
||||
// Auto-dismiss after the configured duration.
|
||||
let _ = ctx.spawn(
|
||||
galaxyui::r#async::Timer::after(PLAN_ACTIVATED_TOAST_DURATION),
|
||||
|me: &mut Self, _, ctx| {
|
||||
if me.show_plan_activated_toast {
|
||||
me.show_plan_activated_toast = false;
|
||||
ctx.notify();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
me.last_auth_state = new_state;
|
||||
ctx.notify();
|
||||
}
|
||||
OnboardingStateEvent::SelectedSlideChanged
|
||||
OnboardingStateEvent::AuthStateChanged
|
||||
| OnboardingStateEvent::SelectedSlideChanged
|
||||
| OnboardingStateEvent::IntentionChanged
|
||||
| OnboardingStateEvent::Completed
|
||||
| OnboardingStateEvent::UpgradeRequested => {}
|
||||
| OnboardingStateEvent::UpgradeRequested
|
||||
| OnboardingStateEvent::NoAiConfirmationChanged => {}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -260,20 +168,12 @@ impl AgentSlide {
|
||||
autonomy_full_mouse_state: MouseStateHandle::default(),
|
||||
autonomy_partial_mouse_state: MouseStateHandle::default(),
|
||||
autonomy_none_mouse_state: MouseStateHandle::default(),
|
||||
disable_oz_mouse: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
upgrade_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
dropdown_scroll_state: ClippedScrollStateHandle::new(),
|
||||
is_model_list_expanded: false,
|
||||
highlighted_model_id: None,
|
||||
show_auth_prompt_bar: false,
|
||||
copy_url_mouse_state: MouseStateHandle::default(),
|
||||
paste_token_mouse_state: MouseStateHandle::default(),
|
||||
show_plan_activated_toast: false,
|
||||
last_auth_state: initial_auth_state,
|
||||
plan_activated_close_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,7 +208,7 @@ impl AgentSlide {
|
||||
// state is a floating overlay (built in `View::render`) that sits *on top
|
||||
// of* this content, so the underlying layout never shifts between the two
|
||||
// states. That keeps the header + picker chip pinned in place.
|
||||
let bottom_nav = self.render_bottom_nav(appearance);
|
||||
let bottom_nav = self.render_bottom_nav(appearance, app);
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
self.render_header(appearance),
|
||||
@@ -333,7 +233,7 @@ impl AgentSlide {
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Select your in-app agent's defaults.",
|
||||
"Select your Warp Agent's defaults.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
@@ -389,19 +289,10 @@ impl AgentSlide {
|
||||
upper_col.finish()
|
||||
};
|
||||
|
||||
let mut col = Flex::column()
|
||||
let col = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(upper_sections);
|
||||
|
||||
if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
let disable_oz_section = self.render_disable_oz_section(appearance, settings);
|
||||
col = col.with_child(
|
||||
Container::new(disable_oz_section)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
Container::new(col.finish()).with_margin_top(40.).finish()
|
||||
}
|
||||
|
||||
@@ -458,31 +349,15 @@ impl AgentSlide {
|
||||
);
|
||||
}
|
||||
|
||||
let has_disabled = self
|
||||
.onboarding_state
|
||||
.as_ref(app)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.requires_upgrade);
|
||||
|
||||
let mut col = Flex::column()
|
||||
Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(header)
|
||||
.with_child(
|
||||
Container::new(chip_stack.finish())
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if has_disabled {
|
||||
col = col.with_child(
|
||||
Container::new(self.render_upgrade_banner(appearance))
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
col.finish()
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Renders the single-row collapsed picker button: provider icon, selected title,
|
||||
@@ -595,9 +470,8 @@ impl AgentSlide {
|
||||
}
|
||||
|
||||
/// Renders the vertical list of model rows shown inside the floating dropdown
|
||||
/// overlay. Each row: provider icon + title on the left, pill on the right
|
||||
/// (Premium for paywalled rows). Disabled rows are rendered dimmed and are
|
||||
/// not clickable or hover-selectable.
|
||||
/// overlay. Each row shows a provider icon + title on the left, with a
|
||||
/// "Recommended" pill on the right for the default model.
|
||||
fn render_model_list_rows(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
@@ -609,7 +483,7 @@ impl AgentSlide {
|
||||
let state = self.onboarding_state.as_ref(app);
|
||||
let highlighted_id = self.highlighted_model_id.clone();
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
let models = sorted_models(state.models());
|
||||
let models = state.models();
|
||||
|
||||
let mut col = Flex::column().with_cross_axis_alignment(CrossAxisAlignment::Stretch);
|
||||
|
||||
@@ -695,18 +569,11 @@ impl AgentSlide {
|
||||
let background_for_text = theme.background().into_solid();
|
||||
let ui_font_family = appearance.ui_font_family();
|
||||
|
||||
let is_disabled = model.requires_upgrade;
|
||||
|
||||
let title_color: ColorU = if is_disabled {
|
||||
internal_colors::text_disabled(theme, background_for_text)
|
||||
} else {
|
||||
internal_colors::text_main(theme, background_for_text)
|
||||
};
|
||||
let title_color: ColorU = internal_colors::text_main(theme, background_for_text);
|
||||
|
||||
let row_id = model.id.clone();
|
||||
let title = model.title.clone();
|
||||
let icon = model.icon;
|
||||
let requires_upgrade = model.requires_upgrade;
|
||||
let is_default = model.is_default;
|
||||
|
||||
let hoverable_body = Hoverable::new(mouse_state, move |_| {
|
||||
@@ -730,10 +597,7 @@ impl AgentSlide {
|
||||
.with_child(Container::new(title_el).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
|
||||
// Trailing pills: "Recommended" on the server-designated default
|
||||
// model, "Premium" on paywalled rows. In practice a single row is
|
||||
// at most one of these, but both can be shown side-by-side if the
|
||||
// default is also premium for any reason.
|
||||
// "Recommended" pill on the server-designated default model.
|
||||
let make_pill = |label: &'static str| -> Box<dyn Element> {
|
||||
let badge = Text::new(label.to_string(), ui_font_family, 11.0)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
@@ -755,8 +619,6 @@ impl AgentSlide {
|
||||
|
||||
let trailing: Box<dyn Element> = if is_default {
|
||||
make_pill("Recommended")
|
||||
} else if requires_upgrade {
|
||||
make_pill("Premium")
|
||||
} else {
|
||||
Empty::new().finish()
|
||||
};
|
||||
@@ -769,7 +631,7 @@ impl AgentSlide {
|
||||
.with_child(trailing)
|
||||
.finish();
|
||||
|
||||
let background = if is_highlighted && !is_disabled {
|
||||
let background = if is_highlighted {
|
||||
Some(Fill::Solid(internal_colors::neutral_2(theme)))
|
||||
} else {
|
||||
None
|
||||
@@ -787,26 +649,19 @@ impl AgentSlide {
|
||||
.finish()
|
||||
});
|
||||
|
||||
if is_disabled {
|
||||
// Disabled rows: no click, no hover-updates-highlight, muted.
|
||||
hoverable_body.finish()
|
||||
} else {
|
||||
let click_id = row_id.clone();
|
||||
let hover_id = row_id;
|
||||
hoverable_body
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_hover(move |is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::HighlightModel(
|
||||
hover_id.clone(),
|
||||
));
|
||||
}
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::SelectModel(click_id.clone()));
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
let click_id = row_id.clone();
|
||||
let hover_id = row_id;
|
||||
hoverable_body
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_hover(move |is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::HighlightModel(hover_id.clone()));
|
||||
}
|
||||
})
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::SelectModel(click_id.clone()));
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_autonomy_workspace_enforced(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
@@ -881,19 +736,19 @@ impl AgentSlide {
|
||||
(
|
||||
AgentAutonomy::Full,
|
||||
"Full",
|
||||
"Runs commands, writes code, and reads files without asking.",
|
||||
"Warp Agent runs commands, writes code, and reads files without asking.",
|
||||
self.autonomy_full_mouse_state.clone(),
|
||||
),
|
||||
(
|
||||
AgentAutonomy::Partial,
|
||||
"Partial",
|
||||
"Can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.",
|
||||
"Warp Agent can plan, read files, and execute low-risk commands. Asks before making any changes or executing sensitive commands.",
|
||||
self.autonomy_partial_mouse_state.clone(),
|
||||
),
|
||||
(
|
||||
AgentAutonomy::None,
|
||||
"None",
|
||||
"Takes no actions without your approval.",
|
||||
"Warp Agent takes no actions without your approval.",
|
||||
self.autonomy_none_mouse_state.clone(),
|
||||
),
|
||||
];
|
||||
@@ -939,39 +794,7 @@ impl AgentSlide {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_disable_oz_section(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
settings: &AgentDevelopmentSettings,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let background_for_text = theme.background().into_solid();
|
||||
|
||||
let checkbox = appearance
|
||||
.ui_builder()
|
||||
.checkbox(self.disable_oz_mouse.clone(), Some(12.))
|
||||
.check(settings.disable_oz)
|
||||
.build()
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(AgentSlideAction::ToggleDisableOz))
|
||||
.finish();
|
||||
|
||||
let label = Text::new("Disable Warp Agent", appearance.ui_font_family(), 14.0)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Normal,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(checkbox)
|
||||
.with_child(Container::new(label).with_margin_left(8.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -1002,13 +825,7 @@ impl AgentSlide {
|
||||
},
|
||||
);
|
||||
|
||||
let step_index = 2;
|
||||
let step_count =
|
||||
if galaxy_core::features::FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
5
|
||||
} else {
|
||||
4
|
||||
};
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -1018,98 +835,6 @@ impl AgentSlide {
|
||||
)
|
||||
}
|
||||
|
||||
fn render_upgrade_banner(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
// Diagonal magenta → yellow gradient (top-left to bottom-right). Chosen
|
||||
// to match the "premium" glow styling in the Figma mocks.
|
||||
const GRADIENT_START_MAGENTA: ColorU = ColorU {
|
||||
r: 0xE2,
|
||||
g: 0x48,
|
||||
b: 0xBC,
|
||||
a: 0xFF,
|
||||
};
|
||||
const GRADIENT_END_YELLOW: ColorU = ColorU {
|
||||
r: 0xF5,
|
||||
g: 0xB7,
|
||||
b: 0x00,
|
||||
a: 0xFF,
|
||||
};
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background_for_text = theme.background().into_solid();
|
||||
let ui_font_family = appearance.ui_font_family();
|
||||
|
||||
// Primary "heading" line: bolder, full-contrast.
|
||||
let title = Text::new(
|
||||
"Upgrade for access to premium models.",
|
||||
ui_font_family,
|
||||
13.0,
|
||||
)
|
||||
.with_color(internal_colors::text_main(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Medium,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
// Secondary subtext: muted, normal weight.
|
||||
let subtitle = Text::new(
|
||||
"State-of-the-art models require paid plans.",
|
||||
ui_font_family,
|
||||
12.0,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(theme, background_for_text))
|
||||
.with_style(Properties {
|
||||
weight: Weight::Normal,
|
||||
..Default::default()
|
||||
})
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let text_col = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(title)
|
||||
.with_child(Container::new(subtitle).with_margin_top(4.).finish())
|
||||
.finish();
|
||||
|
||||
let upgrade_button = self.upgrade_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Upgrade".into()),
|
||||
theme: &UpgradeButtonTheme,
|
||||
options: button::Options {
|
||||
size: button::Size::Small,
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::UpgradeClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_col)
|
||||
.with_child(upgrade_button)
|
||||
.finish();
|
||||
|
||||
Container::new(row)
|
||||
.with_uniform_padding(12.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(8.)))
|
||||
.with_border(Border::all(1.).with_border_gradient(
|
||||
vec2f(0., 0.),
|
||||
vec2f(1., 1.),
|
||||
Gradient {
|
||||
start: GRADIENT_START_MAGENTA,
|
||||
end: GRADIENT_END_YELLOW,
|
||||
},
|
||||
))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_visual(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
@@ -1138,187 +863,10 @@ impl AgentSlide {
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// Full-width bar pinned below the slide's two-column layout. Shown after
|
||||
/// the user clicks the Upgrade button, so they can fall back to copying
|
||||
/// the upgrade URL (or pasting the returned auth token) if the browser
|
||||
/// didn't launch automatically.
|
||||
fn render_auth_prompt_bar(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const BAR_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let bar_bg = theme.surface_1();
|
||||
let bar_bg_solid = bar_bg.into_solid();
|
||||
let text_color = internal_colors::text_sub(theme, bar_bg_solid);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let text_styles = UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
let link_styles = UiComponentStyles {
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let icon = ConstrainedBox::new(Box::new(
|
||||
Icon::AlertCircle.to_galaxyui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let copy_url_link = ui_builder
|
||||
.link(
|
||||
"copy the URL".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::CopyUpgradeUrlClicked);
|
||||
})),
|
||||
self.copy_url_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let paste_token_link = ui_builder
|
||||
.link(
|
||||
"Click here".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::PasteAuthTokenFromClipboardClicked);
|
||||
})),
|
||||
self.paste_token_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let text_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ui_builder
|
||||
.span("If your browser hasn't launched, ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(copy_url_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" and open the page manually. ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(paste_token_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" to paste your token from the browser.")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_row)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(bar_bg)
|
||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_4(theme)))
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(BAR_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Green success pill shown when the user's `OnboardingAuthState`
|
||||
/// transitions into `PayingUser`. Auto-dismisses after
|
||||
/// `PLAN_ACTIVATED_TOAST_DURATION`; also dismissable via the close X.
|
||||
fn render_plan_activated_toast(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const TOAST_MIN_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const CLOSE_SIZE: f32 = 16.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let toast_bg: Fill = theme.ansi_fg_green().into();
|
||||
let text_color: ColorU = theme.font_color(toast_bg.into_solid()).into();
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let check_icon = ConstrainedBox::new(Box::new(
|
||||
Icon::CheckSkinny.to_galaxyui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let text = ui_builder
|
||||
.span("Plan successfully activated. All premium models are available.")
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let close_button = ui_builder
|
||||
.close_button(CLOSE_SIZE, self.plan_activated_close_mouse_state.clone())
|
||||
.with_style(UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.on_click(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(AgentSlideAction::DismissPlanActivatedToast);
|
||||
})
|
||||
.finish();
|
||||
|
||||
let left = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(check_icon)
|
||||
.with_child(Container::new(text).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(close_button)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(toast_bg)
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(TOAST_MIN_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AgentSlide {
|
||||
type Event = AgentSlideEvent;
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for AgentSlide {
|
||||
@@ -1334,39 +882,10 @@ impl View for AgentSlide {
|
||||
// The floating dropdown overlay is built inside `render_model_section`
|
||||
// so it inherits the column width naturally. Here we only need the
|
||||
// base two-column layout.
|
||||
let slide = layout::static_left(
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, settings, workspace_enforces_autonomy, app),
|
||||
|| self.render_visual(appearance, app),
|
||||
);
|
||||
|
||||
// Upgrade-prompt bar: shown after the user clicks Upgrade, as long as
|
||||
// they aren't yet on a paid plan. Overlays the bottom of the slide
|
||||
// (doesn't bump slide content up) so the slide layout stays stable
|
||||
// whether or not the bar is visible.
|
||||
//
|
||||
// The plan-activated success toast supersedes the bar (and any other
|
||||
// bottom overlay) while it's visible.
|
||||
let auth_state = self.onboarding_state.as_ref(app).auth_state();
|
||||
let show_bar =
|
||||
self.show_auth_prompt_bar && !matches!(auth_state, OnboardingAuthState::PayingUser);
|
||||
if !show_bar && !self.show_plan_activated_toast {
|
||||
return slide;
|
||||
}
|
||||
|
||||
let bottom_overlay = if self.show_plan_activated_toast {
|
||||
self.render_plan_activated_toast(appearance)
|
||||
} else {
|
||||
self.render_auth_prompt_bar(appearance)
|
||||
};
|
||||
|
||||
let mut stack = Stack::new();
|
||||
stack.add_child(slide);
|
||||
stack.add_child(
|
||||
galaxyui::elements::Align::new(bottom_overlay)
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
);
|
||||
stack.finish()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1388,10 +907,7 @@ impl AgentSlide {
|
||||
// starts on the selected row.
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
if let Some(index) = sorted_models(state.models())
|
||||
.iter()
|
||||
.position(|m| m.id == selected_id)
|
||||
{
|
||||
if let Some(index) = state.models().iter().position(|m| m.id == selected_id) {
|
||||
self.dropdown_scroll_state.scroll_to_position(ScrollTarget {
|
||||
position_id: model_row_position_id(index),
|
||||
mode: ScrollToPositionMode::FullyIntoView,
|
||||
@@ -1402,55 +918,33 @@ impl AgentSlide {
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
/// Finds the next enabled model index in the given direction, wrapping
|
||||
/// around. Indices are into the slide's sorted view of the model list.
|
||||
/// Returns `None` if all models are paywalled.
|
||||
fn next_enabled_model_index(
|
||||
&self,
|
||||
start: usize,
|
||||
forward: bool,
|
||||
ctx: &AppContext,
|
||||
) -> Option<usize> {
|
||||
let models = sorted_models(self.onboarding_state.as_ref(ctx).models());
|
||||
let count = models.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
for offset in 1..=count {
|
||||
let idx = if forward {
|
||||
(start + offset) % count
|
||||
} else {
|
||||
(start + count - offset) % count
|
||||
};
|
||||
if !models[idx].requires_upgrade {
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Advances the highlight cursor to the next/previous enabled model, wrapping.
|
||||
/// The origin of the walk is the currently-highlighted id (if any), else the
|
||||
/// Advances the highlight cursor to the next/previous model, wrapping. The
|
||||
/// origin of the walk is the currently-highlighted id (if any), else the
|
||||
/// currently-selected id. Also scrolls the dropdown so the newly-highlighted
|
||||
/// row stays visible — same `SavePosition` + `scroll_to_position` pattern
|
||||
/// used by `VerticalTabsPanelState::scroll_to_tab`.
|
||||
fn advance_highlighted_model(&mut self, forward: bool, ctx: &mut ViewContext<Self>) {
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let sorted = sorted_models(state.models());
|
||||
let selected_id = state.agent_settings().selected_model_id.clone();
|
||||
let (model_ids, selected_id) = {
|
||||
let state = self.onboarding_state.as_ref(ctx);
|
||||
let ids: Vec<LLMId> = state.models().iter().map(|m| m.id.clone()).collect();
|
||||
(ids, state.agent_settings().selected_model_id.clone())
|
||||
};
|
||||
let count = model_ids.len();
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
let start_index = self
|
||||
.highlighted_model_id
|
||||
.as_ref()
|
||||
.and_then(|id| sorted.iter().position(|m| &m.id == id))
|
||||
.or_else(|| sorted.iter().position(|m| m.id == selected_id))
|
||||
.and_then(|id| model_ids.iter().position(|m| m == id))
|
||||
.or_else(|| model_ids.iter().position(|m| *m == selected_id))
|
||||
.unwrap_or(0);
|
||||
let Some(next_index) = self.next_enabled_model_index(start_index, forward, ctx) else {
|
||||
return;
|
||||
let next_index = if forward {
|
||||
(start_index + 1) % count
|
||||
} else {
|
||||
(start_index + count - 1) % count
|
||||
};
|
||||
let Some(next_id) = sorted.get(next_index).map(|m| m.id.clone()) else {
|
||||
return;
|
||||
};
|
||||
self.highlighted_model_id = Some(next_id);
|
||||
self.highlighted_model_id = Some(model_ids[next_index].clone());
|
||||
// Scroll the dropdown so the new highlight is visible. `FullyIntoView`
|
||||
// is a no-op when the row is already fully in view, otherwise it
|
||||
// scrolls the minimum amount to show it.
|
||||
@@ -1532,16 +1026,7 @@ impl OnboardingSlide for AgentSlide {
|
||||
// and collapses the list. Does NOT advance to the next slide.
|
||||
if self.is_model_list_expanded {
|
||||
if let Some(id) = self.highlighted_model_id.clone() {
|
||||
// Only select if the highlighted row is still enabled.
|
||||
let enabled = self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.id == id && !m.requires_upgrade);
|
||||
if enabled {
|
||||
self.select_model(id, ctx);
|
||||
}
|
||||
self.select_model(id, ctx);
|
||||
}
|
||||
self.set_model_list_expanded(false, ctx);
|
||||
return;
|
||||
@@ -1578,16 +1063,7 @@ impl TypedActionView for AgentSlide {
|
||||
self.set_model_list_expanded(!self.is_model_list_expanded, ctx);
|
||||
}
|
||||
AgentSlideAction::HighlightModel(model_id) => {
|
||||
// Only update if the id corresponds to an enabled row. Callers
|
||||
// (hover handlers) already filter this out, but we defend against
|
||||
// stale actions fired while the list was re-rendering.
|
||||
let enabled = self
|
||||
.onboarding_state
|
||||
.as_ref(ctx)
|
||||
.models()
|
||||
.iter()
|
||||
.any(|m| m.id == *model_id && !m.requires_upgrade);
|
||||
if enabled && self.highlighted_model_id.as_ref() != Some(model_id) {
|
||||
if self.highlighted_model_id.as_ref() != Some(model_id) {
|
||||
self.highlighted_model_id = Some(model_id.clone());
|
||||
ctx.notify();
|
||||
}
|
||||
@@ -1616,29 +1092,6 @@ impl TypedActionView for AgentSlide {
|
||||
AgentSlideAction::NextClicked => {
|
||||
self.next(ctx);
|
||||
}
|
||||
AgentSlideAction::UpgradeClicked => {
|
||||
send_telemetry_from_ctx!(OnboardingEvent::AgentSlideUpgradeClicked, ctx);
|
||||
if !matches!(
|
||||
self.onboarding_state.as_ref(ctx).auth_state(),
|
||||
OnboardingAuthState::PayingUser,
|
||||
) {
|
||||
self.show_auth_prompt_bar = true;
|
||||
ctx.notify();
|
||||
}
|
||||
self.onboarding_state.update(ctx, |state, ctx| {
|
||||
state.request_upgrade(ctx);
|
||||
});
|
||||
}
|
||||
AgentSlideAction::CopyUpgradeUrlClicked => {
|
||||
ctx.emit(AgentSlideEvent::CopyUpgradeUrlRequested);
|
||||
}
|
||||
AgentSlideAction::PasteAuthTokenFromClipboardClicked => {
|
||||
ctx.emit(AgentSlideEvent::PasteAuthTokenFromClipboardRequested);
|
||||
}
|
||||
AgentSlideAction::DismissPlanActivatedToast => {
|
||||
self.show_plan_activated_toast = false;
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,622 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Stack,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{AiAccessChoice, OnboardingAuthState, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiAccessSlideAction {
|
||||
SelectSubscription,
|
||||
SelectSetUpLater,
|
||||
CopyUpgradeUrlClicked,
|
||||
PasteAuthTokenFromClipboardClicked,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
}
|
||||
|
||||
/// Emitted to the parent onboarding view so the (app-crate) upgrade fallback
|
||||
/// actions can be handled at the root level — the onboarding crate can't
|
||||
/// reference them directly.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiAccessSlideEvent {
|
||||
CopyUpgradeUrlRequested,
|
||||
PasteAuthTokenFromClipboardRequested,
|
||||
}
|
||||
|
||||
/// The "Choose how to access AI" slide (Warp Agent path). Forks between a paid
|
||||
/// subscription and a "Set up later" option that lets the user explore Warp's
|
||||
/// built-in AI before committing to a plan.
|
||||
pub struct AiAccessSlide {
|
||||
onboarding_state: ModelHandle<OnboardingStateModel>,
|
||||
subscription_mouse_state: MouseStateHandle,
|
||||
set_up_later_mouse_state: MouseStateHandle,
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
show_auth_prompt_bar: bool,
|
||||
copy_url_mouse_state: MouseStateHandle,
|
||||
paste_token_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
impl AiAccessSlide {
|
||||
pub(crate) fn new(onboarding_state: ModelHandle<OnboardingStateModel>) -> Self {
|
||||
Self {
|
||||
onboarding_state,
|
||||
subscription_mouse_state: MouseStateHandle::default(),
|
||||
set_up_later_mouse_state: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
show_auth_prompt_bar: false,
|
||||
copy_url_mouse_state: MouseStateHandle::default(),
|
||||
paste_token_mouse_state: MouseStateHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// The final DES-816 visual exports have not landed yet, so the right panel
|
||||
// reuses the existing bundled agent welcome image.
|
||||
pub(crate) const VISUAL_IMAGE_PATHS: &'static [&'static str] =
|
||||
&["async/png/onboarding/welcome_agent.png"];
|
||||
|
||||
fn choice(&self, app: &AppContext) -> AiAccessChoice {
|
||||
self.onboarding_state.as_ref(app).ai_access_choice()
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
choice: AiAccessChoice,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
Align::new(self.render_header(appearance)).left().finish(),
|
||||
Align::new(self.render_options(appearance, choice)).finish(),
|
||||
],
|
||||
bottom_nav,
|
||||
self.scroll_state.clone(),
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Get AI access")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(36.),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Save with a recurring plan, or explore Warp's AI before committing.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(
|
||||
theme,
|
||||
theme.background().into_solid(),
|
||||
))
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(title)
|
||||
.with_child(Container::new(subtitle).with_margin_top(16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_options(&self, appearance: &Appearance, choice: AiAccessChoice) -> Box<dyn Element> {
|
||||
let subscription_card = self
|
||||
.render_subscription_card(appearance, matches!(choice, AiAccessChoice::Subscription));
|
||||
|
||||
let set_up_later_card =
|
||||
self.render_set_up_later_card(appearance, matches!(choice, AiAccessChoice::SetUpLater));
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(subscription_card)
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(set_up_later_card)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(38.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Shared chrome for an option card: selected/unselected background + border,
|
||||
/// hover/click to select.
|
||||
fn render_card_chrome(
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
select_action: AiAccessSlideAction,
|
||||
content: Box<dyn Element>,
|
||||
) -> Box<dyn Element> {
|
||||
const RADIUS: f32 = 8.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background = if is_selected {
|
||||
Some(internal_colors::accent_overlay_1(theme))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
theme.accent()
|
||||
} else {
|
||||
Fill::Solid(internal_colors::neutral_4(theme))
|
||||
};
|
||||
|
||||
Hoverable::new(mouse_state, move |_| {
|
||||
let mut container = Container::new(content)
|
||||
.with_uniform_padding(24.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(RADIUS)))
|
||||
.with_border(Border::all(1.).with_border_fill(border_color));
|
||||
if let Some(bg) = background {
|
||||
container = container.with_background(bg);
|
||||
}
|
||||
container.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(select_action.clone());
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_subscription_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Subscription")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let badge = {
|
||||
let green = theme.ansi_fg_green();
|
||||
let badge_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Best value")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(green),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
Container::new(badge_text)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(11.)))
|
||||
.with_border(Border::all(1.).with_border_fill(Fill::Solid(green)))
|
||||
.with_horizontal_padding(8.)
|
||||
.with_vertical_padding(3.)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let header_row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(label)
|
||||
.with_child(badge)
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Starting at $18 / mo, available with monthly or annual plans. Includes base credits, \
|
||||
frontier models, cloud agents, collaboration, and more.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(header_row)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
self.subscription_mouse_state.clone(),
|
||||
AiAccessSlideAction::SelectSubscription,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_set_up_later_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Set up later")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Explore Warp's built-in AI features before committing to a plan, or bring your own \
|
||||
inference.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
.with_child(label)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
self.set_up_later_mouse_state.clone(),
|
||||
AiAccessSlideAction::SelectSetUpLater,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Back".into()),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::BackClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let enter = Keystroke::parse("enter").unwrap_or_default();
|
||||
let next_button = self.next_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Next".into()),
|
||||
theme: &button::themes::Primary,
|
||||
options: button::Options {
|
||||
keystroke: Some(enter),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::NextClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
step_count,
|
||||
Some(back_button),
|
||||
Some(next_button),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_visual(&self) -> Box<dyn Element> {
|
||||
layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[0],
|
||||
layout::FOREGROUND_LAYOUT_DEFAULT,
|
||||
)
|
||||
}
|
||||
|
||||
/// Full-width bar pinned below the slide's two-column layout. Shown after
|
||||
/// the user picks Subscription and clicks "Next", so they can fall back to
|
||||
/// copying the upgrade URL (or pasting the returned auth token) if the
|
||||
/// browser didn't launch automatically.
|
||||
fn render_auth_prompt_bar(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
const BAR_HEIGHT: f32 = 40.;
|
||||
const ICON_SIZE: f32 = 14.;
|
||||
const FONT_SIZE: f32 = 12.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let bar_bg = theme.surface_1();
|
||||
let bar_bg_solid = bar_bg.into_solid();
|
||||
let text_color = internal_colors::text_sub(theme, bar_bg_solid);
|
||||
let ui_builder = appearance.ui_builder();
|
||||
|
||||
let text_styles = UiComponentStyles {
|
||||
font_color: Some(text_color),
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
let link_styles = UiComponentStyles {
|
||||
font_size: Some(FONT_SIZE),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let icon = ConstrainedBox::new(Box::new(
|
||||
Icon::AlertCircle.to_warpui_icon(Fill::Solid(text_color)),
|
||||
))
|
||||
.with_width(ICON_SIZE)
|
||||
.with_height(ICON_SIZE)
|
||||
.finish();
|
||||
|
||||
let copy_url_link = ui_builder
|
||||
.link(
|
||||
"copy the URL".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(AiAccessSlideAction::CopyUpgradeUrlClicked);
|
||||
})),
|
||||
self.copy_url_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let paste_token_link = ui_builder
|
||||
.link(
|
||||
"Click here".into(),
|
||||
None,
|
||||
Some(Box::new(|ctx| {
|
||||
ctx.dispatch_typed_action(
|
||||
AiAccessSlideAction::PasteAuthTokenFromClipboardClicked,
|
||||
);
|
||||
})),
|
||||
self.paste_token_mouse_state.clone(),
|
||||
)
|
||||
.soft_wrap(false)
|
||||
.with_style(link_styles)
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let text_row = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon)
|
||||
.with_child(
|
||||
Container::new(
|
||||
ui_builder
|
||||
.span("If your browser hasn't launched, ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(copy_url_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" and open the page manually. ")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_child(paste_token_link)
|
||||
.with_child(
|
||||
ui_builder
|
||||
.span(" to paste your token from the browser.")
|
||||
.with_style(text_styles)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(text_row)
|
||||
.finish();
|
||||
|
||||
ConstrainedBox::new(
|
||||
Container::new(row)
|
||||
.with_background(bar_bg)
|
||||
.with_border(Border::top(1.).with_border_color(internal_colors::neutral_4(theme)))
|
||||
.with_horizontal_padding(16.)
|
||||
.finish(),
|
||||
)
|
||||
.with_min_height(BAR_HEIGHT)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AiAccessSlide {
|
||||
type Event = AiAccessSlideEvent;
|
||||
}
|
||||
|
||||
impl View for AiAccessSlide {
|
||||
fn ui_name() -> &'static str {
|
||||
"AiAccessSlide"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let choice = self.choice(app);
|
||||
|
||||
let slide = layout::static_left(
|
||||
|| self.render_content(appearance, choice, app),
|
||||
|| self.render_visual(),
|
||||
);
|
||||
|
||||
// Overlay the fallback bar at the bottom (rather than adding it to the
|
||||
// column) so the slide layout stays stable whether or not it's shown.
|
||||
let show_bar = self.show_auth_prompt_bar
|
||||
&& !matches!(
|
||||
self.onboarding_state.as_ref(app).auth_state(),
|
||||
OnboardingAuthState::PayingUser,
|
||||
);
|
||||
if !show_bar {
|
||||
return slide;
|
||||
}
|
||||
|
||||
let mut stack = Stack::new();
|
||||
stack.add_child(slide);
|
||||
stack.add_child(
|
||||
Align::new(self.render_auth_prompt_bar(appearance))
|
||||
.bottom_center()
|
||||
.finish(),
|
||||
);
|
||||
stack.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl AiAccessSlide {
|
||||
fn select_choice(&mut self, choice: AiAccessChoice, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.set_ai_access_choice(choice, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.next(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
/// Primary "Next" action. On the subscription path this advances when the
|
||||
/// user already has a plan, otherwise it launches checkout in the browser
|
||||
/// (the slide auto-advances once billing flips to a paying plan). The "Set
|
||||
/// up later" path always advances.
|
||||
fn advance_or_upgrade(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
match self.choice(ctx) {
|
||||
AiAccessChoice::Subscription => {
|
||||
if matches!(
|
||||
self.onboarding_state.as_ref(ctx).auth_state(),
|
||||
OnboardingAuthState::PayingUser
|
||||
) {
|
||||
self.next(ctx);
|
||||
} else {
|
||||
// Surface the manual-fallback bar in case the browser
|
||||
// doesn't launch; it's hidden again once billing flips to
|
||||
// PayingUser.
|
||||
self.show_auth_prompt_bar = true;
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.request_upgrade(ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
AiAccessChoice::SetUpLater => self.next(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OnboardingSlide for AiAccessSlide {
|
||||
fn on_up(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiAccessChoice::Subscription, ctx);
|
||||
}
|
||||
|
||||
fn on_down(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiAccessChoice::SetUpLater, ctx);
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.advance_or_upgrade(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for AiAccessSlide {
|
||||
type Action = AiAccessSlideAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
AiAccessSlideAction::SelectSubscription => {
|
||||
self.select_choice(AiAccessChoice::Subscription, ctx);
|
||||
}
|
||||
AiAccessSlideAction::SelectSetUpLater => {
|
||||
self.select_choice(AiAccessChoice::SetUpLater, ctx);
|
||||
}
|
||||
AiAccessSlideAction::CopyUpgradeUrlClicked => {
|
||||
ctx.emit(AiAccessSlideEvent::CopyUpgradeUrlRequested);
|
||||
}
|
||||
AiAccessSlideAction::PasteAuthTokenFromClipboardClicked => {
|
||||
ctx.emit(AiAccessSlideEvent::PasteAuthTokenFromClipboardRequested);
|
||||
}
|
||||
AiAccessSlideAction::BackClicked => {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.back(ctx);
|
||||
});
|
||||
}
|
||||
AiAccessSlideAction::NextClicked => {
|
||||
self.advance_or_upgrade(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,520 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{AiSetupChoice, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
|
||||
/// Checklist shown on the "Use Warp agent" card.
|
||||
const WARP_AGENT_FEATURES: &[&str] = &[
|
||||
"Best harness for terminal tasks and agentic coding",
|
||||
"Frontier models from OpenAI, Anthropic, and Google",
|
||||
"Model routing across frontier and open-weight models",
|
||||
"Multi-agent orchestration",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AiSetupSlideAction {
|
||||
SelectWarpAgent,
|
||||
SelectThirdParty,
|
||||
BackClicked,
|
||||
NextClicked,
|
||||
}
|
||||
|
||||
/// The "Choose your AI setup" slide (DES-816 V3), shown on the AI-first path for
|
||||
/// users enrolled in the FREE_AI_REMOVAL experiment arm. Forks between the Warp
|
||||
/// agent (paid-plan path) and third-party agents (works on Free).
|
||||
pub struct AiSetupSlide {
|
||||
onboarding_state: ModelHandle<OnboardingStateModel>,
|
||||
warp_agent_mouse_state: MouseStateHandle,
|
||||
third_party_mouse_state: MouseStateHandle,
|
||||
back_button: button::Button,
|
||||
next_button: button::Button,
|
||||
scroll_state: ClippedScrollStateHandle,
|
||||
}
|
||||
|
||||
impl AiSetupSlide {
|
||||
pub(crate) fn new(onboarding_state: ModelHandle<OnboardingStateModel>) -> Self {
|
||||
Self {
|
||||
onboarding_state,
|
||||
warp_agent_mouse_state: MouseStateHandle::default(),
|
||||
third_party_mouse_state: MouseStateHandle::default(),
|
||||
back_button: button::Button::default(),
|
||||
next_button: button::Button::default(),
|
||||
scroll_state: ClippedScrollStateHandle::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// The final DES-816 visual exports have not landed yet, so the right panel
|
||||
// reuses existing bundled assets that match each choice: the agent
|
||||
// experience for "Use Warp agent" and the CLI-agent toolbar for
|
||||
// "Use third party agents".
|
||||
pub(crate) const VISUAL_IMAGE_PATHS: &'static [&'static str] = &[
|
||||
"async/png/onboarding/welcome_agent.png",
|
||||
"async/png/onboarding/thirdparty_toolbar_enabled_vertical.png",
|
||||
];
|
||||
|
||||
fn choice(&self, app: &AppContext) -> AiSetupChoice {
|
||||
self.onboarding_state.as_ref(app).ai_setup_choice()
|
||||
}
|
||||
|
||||
fn render_content(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
choice: AiSetupChoice,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
Align::new(self.render_header(appearance)).left().finish(),
|
||||
Align::new(self.render_options(appearance, choice)).finish(),
|
||||
],
|
||||
bottom_nav,
|
||||
self.scroll_state.clone(),
|
||||
appearance,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
let logo_fill = internal_colors::fg_overlay_4(theme);
|
||||
let logo = ConstrainedBox::new(Icon::WarpLogoLight.to_warpui_icon(logo_fill).finish())
|
||||
.with_width(64.)
|
||||
.with_height(64.)
|
||||
.finish();
|
||||
|
||||
let title = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Choose your AI setup")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(36.),
|
||||
font_weight: Some(Weight::Medium),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let subtitle = FormattedTextElement::from_str(
|
||||
"Choose if you'd like to use Warp Agent or third party agents.",
|
||||
appearance.ui_font_family(),
|
||||
16.,
|
||||
)
|
||||
.with_color(internal_colors::text_sub(
|
||||
theme,
|
||||
theme.background().into_solid(),
|
||||
))
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.0)
|
||||
.finish();
|
||||
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start)
|
||||
// Offset icon built in padding to left align icon with title.
|
||||
.with_child(Container::new(logo).with_margin_left(-7.).finish())
|
||||
.with_child(Container::new(title).with_margin_top(11.).finish())
|
||||
.with_child(Container::new(subtitle).with_margin_top(16.).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_options(&self, appearance: &Appearance, choice: AiSetupChoice) -> Box<dyn Element> {
|
||||
let warp_agent_card = self.render_warp_agent_card(
|
||||
appearance,
|
||||
matches!(choice, AiSetupChoice::WarpAgent),
|
||||
self.warp_agent_mouse_state.clone(),
|
||||
);
|
||||
|
||||
let third_party_card = self.render_third_party_card(
|
||||
appearance,
|
||||
matches!(choice, AiSetupChoice::ThirdParty),
|
||||
self.third_party_mouse_state.clone(),
|
||||
);
|
||||
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(warp_agent_card)
|
||||
.with_margin_bottom(12.)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(third_party_card)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(38.)
|
||||
.finish()
|
||||
}
|
||||
|
||||
/// Shared chrome for an AI-setup option card. Applies the selected/unselected
|
||||
/// background + border + rounded corners, wires up hover/click, and emits the
|
||||
/// provided select action.
|
||||
fn render_card_chrome(
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
select_action: AiSetupSlideAction,
|
||||
content: Box<dyn Element>,
|
||||
) -> Box<dyn Element> {
|
||||
const RADIUS: f32 = 8.;
|
||||
|
||||
let theme = appearance.theme();
|
||||
let background = if is_selected {
|
||||
Some(internal_colors::accent_overlay_1(theme))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let border_color = if is_selected {
|
||||
theme.accent()
|
||||
} else {
|
||||
Fill::Solid(internal_colors::neutral_4(theme))
|
||||
};
|
||||
|
||||
Hoverable::new(mouse_state, move |_| {
|
||||
let mut container = Container::new(content)
|
||||
.with_uniform_padding(24.)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(RADIUS)))
|
||||
.with_border(Border::all(1.).with_border_fill(border_color));
|
||||
if let Some(bg) = background {
|
||||
container = container.with_background(bg);
|
||||
}
|
||||
container.finish()
|
||||
})
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(select_action.clone());
|
||||
})
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_warp_agent_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let label_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
let description_color = internal_colors::text_sub(theme, bg_solid);
|
||||
|
||||
let header_row = {
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Use Warp Agent")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let badge = {
|
||||
let green = theme.ansi_fg_green();
|
||||
let badge_text = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Access more models")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(12.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(green),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
Container::new(badge_text)
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(11.)))
|
||||
.with_border(Border::all(1.).with_border_fill(Fill::Solid(green)))
|
||||
.with_horizontal_padding(8.)
|
||||
.with_vertical_padding(3.)
|
||||
.finish()
|
||||
};
|
||||
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_main_axis_alignment(MainAxisAlignment::SpaceBetween)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(label)
|
||||
.with_child(badge)
|
||||
.finish()
|
||||
};
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"State of the art agent harness deeply integrated into the terminal.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(description_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let checklist = {
|
||||
// When the card is selected, use the theme's green to match the
|
||||
// "Blended ANSI/green_fg" token in the design.
|
||||
let check_fill = if is_selected {
|
||||
Fill::Solid(theme.ansi_fg_green())
|
||||
} else {
|
||||
Fill::Solid(label_color)
|
||||
};
|
||||
let mut col = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Start);
|
||||
for &item in WARP_AGENT_FEATURES {
|
||||
let icon_el = ConstrainedBox::new(Icon::Check.to_warpui_icon(check_fill).finish())
|
||||
.with_width(16.)
|
||||
.with_height(16.)
|
||||
.finish();
|
||||
let text_el = appearance
|
||||
.ui_builder()
|
||||
.paragraph(item.to_string())
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(14.),
|
||||
font_weight: Some(Weight::Normal),
|
||||
font_color: Some(label_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
let row = Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(icon_el)
|
||||
.with_child(Container::new(text_el).with_margin_left(8.).finish())
|
||||
.finish();
|
||||
col = col.with_child(
|
||||
Container::new(row)
|
||||
.with_padding_top(4.)
|
||||
.with_padding_bottom(4.)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
col.finish()
|
||||
};
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(header_row)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.with_child(Container::new(checklist).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
mouse_state,
|
||||
AiSetupSlideAction::SelectWarpAgent,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_third_party_card(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
is_selected: bool,
|
||||
mouse_state: MouseStateHandle,
|
||||
) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
let bg_solid = theme.background().into_solid();
|
||||
let text_color = if is_selected {
|
||||
internal_colors::text_main(theme, bg_solid)
|
||||
} else {
|
||||
internal_colors::text_sub(theme, bg_solid)
|
||||
};
|
||||
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Use third party agents")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
font_color: Some(text_color),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.finish();
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"Use agents like Claude Code, Codex, and Gemini.",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
.with_color(text_color)
|
||||
.with_weight(Weight::Normal)
|
||||
.with_alignment(TextAlignment::Left)
|
||||
.with_line_height_ratio(1.2)
|
||||
.finish();
|
||||
|
||||
let content = Flex::column()
|
||||
.with_main_axis_size(MainAxisSize::Min)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(label)
|
||||
.with_child(Container::new(description).with_margin_top(12.).finish())
|
||||
.finish();
|
||||
|
||||
Self::render_card_chrome(
|
||||
appearance,
|
||||
is_selected,
|
||||
mouse_state,
|
||||
AiSetupSlideAction::SelectThirdParty,
|
||||
content,
|
||||
)
|
||||
}
|
||||
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Back".into()),
|
||||
theme: &button::themes::Naked,
|
||||
options: button::Options {
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiSetupSlideAction::BackClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let enter = Keystroke::parse("enter").unwrap_or_default();
|
||||
let next_button = self.next_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
content: button::Content::Label("Next".into()),
|
||||
theme: &button::themes::Primary,
|
||||
options: button::Options {
|
||||
keystroke: Some(enter),
|
||||
on_click: Some(Box::new(|ctx, _app, _pos| {
|
||||
ctx.dispatch_typed_action(AiSetupSlideAction::NextClicked);
|
||||
})),
|
||||
..button::Options::default(appearance)
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
step_count,
|
||||
Some(back_button),
|
||||
Some(next_button),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_visual(&self, choice: AiSetupChoice) -> Box<dyn Element> {
|
||||
match choice {
|
||||
AiSetupChoice::WarpAgent => layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[0],
|
||||
layout::FOREGROUND_LAYOUT_DEFAULT,
|
||||
),
|
||||
AiSetupChoice::ThirdParty => layout::onboarding_right_panel_with_bg(
|
||||
Self::VISUAL_IMAGE_PATHS[1],
|
||||
layout::FOREGROUND_LAYOUT_THIRD_PARTY,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for AiSetupSlide {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for AiSetupSlide {
|
||||
fn ui_name() -> &'static str {
|
||||
"AiSetupSlide"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
let choice = self.choice(app);
|
||||
|
||||
// Background is rendered by the parent onboarding view (including background images).
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, choice, app),
|
||||
|| self.render_visual(choice),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl AiSetupSlide {
|
||||
fn select_choice(&mut self, choice: AiSetupChoice, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.set_ai_setup_choice(choice, ctx);
|
||||
});
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.next(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl OnboardingSlide for AiSetupSlide {
|
||||
fn on_up(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiSetupChoice::WarpAgent, ctx);
|
||||
}
|
||||
|
||||
fn on_down(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.select_choice(AiSetupChoice::ThirdParty, ctx);
|
||||
}
|
||||
|
||||
fn on_enter(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.next(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for AiSetupSlide {
|
||||
type Action = AiSetupSlideAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
AiSetupSlideAction::SelectWarpAgent => {
|
||||
self.select_choice(AiSetupChoice::WarpAgent, ctx);
|
||||
}
|
||||
AiSetupSlideAction::SelectThirdParty => {
|
||||
self.select_choice(AiSetupChoice::ThirdParty, ctx);
|
||||
}
|
||||
AiSetupSlideAction::BackClicked => {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
model.back(ctx);
|
||||
});
|
||||
}
|
||||
AiSetupSlideAction::NextClicked => {
|
||||
self.next(ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
use crate::slides::progress_dots;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Container, CrossAxisAlignment, Empty, Flex, MainAxisSize, ParentElement, Shrinkable,
|
||||
},
|
||||
Element,
|
||||
use galaxyui_core::elements::{
|
||||
Align, CrossAxisAlignment, Empty, Flex, MainAxisSize, ParentElement, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use crate::slides::progress_dots;
|
||||
|
||||
pub fn onboarding_bottom_nav(
|
||||
appearance: &Appearance,
|
||||
@@ -19,19 +18,16 @@ pub fn onboarding_bottom_nav(
|
||||
let back_button = back_button.unwrap_or_else(|| Empty::new().finish());
|
||||
let next_button = next_button.unwrap_or_else(|| Empty::new().finish());
|
||||
|
||||
// Use equal-size flex slots on the left and right so the dots remain centered regardless of the
|
||||
// button widths.
|
||||
// Equal-weight side slots push Back to the far left and Next to the far
|
||||
// right, leaving the natural-width dots centered between them on one row.
|
||||
let left = Shrinkable::new(1., Align::new(back_button).left().finish()).finish();
|
||||
let right = Shrinkable::new(1., Align::new(next_button).right().finish()).finish();
|
||||
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(dots)
|
||||
.with_child(right)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
Flex::row()
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(left)
|
||||
.with_child(dots)
|
||||
.with_child(right)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::toggle_card::{render_toggle_card, ChipSpec, ToggleCardSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel, UICustomizationSettings};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::OnboardingIntention;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
/// Which setting card is currently selected (expanded).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
@@ -127,8 +129,9 @@ impl CustomizeUISlide {
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
ui: &UICustomizationSettings,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, intention)).finish();
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
slide_content::onboarding_slide_content(
|
||||
vec![
|
||||
@@ -255,6 +258,24 @@ impl CustomizeUISlide {
|
||||
let mut chips = vec![];
|
||||
|
||||
if ui.tools_panel_enabled(&intention) {
|
||||
chips.push(ChipSpec {
|
||||
label: "File explorer",
|
||||
is_enabled: ui.show_project_explorer,
|
||||
mouse_state: self.chip_file_explorer_mouse.clone(),
|
||||
on_click: Box::new(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::ToggleToolsSubSetting {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}),
|
||||
on_hover: Some(Box::new(|is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::HoverToolsChip {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}
|
||||
})),
|
||||
});
|
||||
|
||||
// Conversation history chip is only shown for the agent intention.
|
||||
if is_agent {
|
||||
chips.push(ChipSpec {
|
||||
@@ -276,24 +297,6 @@ impl CustomizeUISlide {
|
||||
});
|
||||
}
|
||||
|
||||
chips.push(ChipSpec {
|
||||
label: "File explorer",
|
||||
is_enabled: ui.show_project_explorer,
|
||||
mouse_state: self.chip_file_explorer_mouse.clone(),
|
||||
on_click: Box::new(|ctx, _, _| {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::ToggleToolsSubSetting {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}),
|
||||
on_hover: Some(Box::new(|is_hovered, ctx, _, _| {
|
||||
if is_hovered {
|
||||
ctx.dispatch_typed_action(CustomizeSlideAction::HoverToolsChip {
|
||||
setting: ToolsPanelSubSetting::ProjectExplorer,
|
||||
});
|
||||
}
|
||||
})),
|
||||
});
|
||||
|
||||
chips.push(ChipSpec {
|
||||
label: "Global file search",
|
||||
is_enabled: ui.show_global_search,
|
||||
@@ -402,11 +405,7 @@ impl CustomizeUISlide {
|
||||
|
||||
// --- Bottom nav ---
|
||||
|
||||
fn render_bottom_nav(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -437,8 +436,7 @@ impl CustomizeUISlide {
|
||||
},
|
||||
);
|
||||
|
||||
let is_terminal = matches!(intention, OnboardingIntention::Terminal);
|
||||
let (step_index, step_count) = if is_terminal { (1, 4) } else { (1, 5) };
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -537,12 +535,8 @@ impl CustomizeUISlide {
|
||||
"async/png/onboarding/terminal_intention/terminal_customize_horizontal_tabs.png"
|
||||
}
|
||||
} else {
|
||||
// Default chip: conversation for agent, file explorer for terminal.
|
||||
let default_chip = if is_agent {
|
||||
ToolsPanelSubSetting::ConversationHistory
|
||||
} else {
|
||||
ToolsPanelSubSetting::ProjectExplorer
|
||||
};
|
||||
// Default chip: file explorer for both intents (matches the new tools panel order).
|
||||
let default_chip = ToolsPanelSubSetting::ProjectExplorer;
|
||||
let chip = hovered_chip.unwrap_or(default_chip);
|
||||
if is_agent {
|
||||
match (chip, vertical) {
|
||||
@@ -644,7 +638,7 @@ impl View for CustomizeUISlide {
|
||||
let ui = self.model_ui_customization(app);
|
||||
|
||||
layout::static_left(
|
||||
|| self.render_content(appearance, intention, &ui),
|
||||
|| self.render_content(appearance, intention, &ui, app),
|
||||
|| self.render_visual(appearance, intention, &ui),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::{OnboardingIntention, AI_FEATURES};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, Icon};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, ParentElement, Radius,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{NoAiConfirmationSource, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::visuals::{intention_terminal_visual, intention_visual};
|
||||
use crate::{OnboardingIntention, AI_FEATURES};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum IntentionSlideAction {
|
||||
@@ -199,7 +202,7 @@ impl IntentionSlide {
|
||||
let header_row = {
|
||||
let label = appearance
|
||||
.ui_builder()
|
||||
.paragraph("Build faster with AI agents")
|
||||
.paragraph("Build faster with agents")
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Semibold),
|
||||
@@ -237,7 +240,7 @@ impl IntentionSlide {
|
||||
};
|
||||
|
||||
let description = FormattedTextElement::from_str(
|
||||
"An agent-first experience with best in class terminal support. Get terminal and agent driven development AI features like:",
|
||||
"Get AI features to accelerate terminal and agent-driven workflows:",
|
||||
appearance.ui_font_family(),
|
||||
14.,
|
||||
)
|
||||
@@ -521,8 +524,14 @@ impl IntentionSlide {
|
||||
fn next(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.onboarding_state.update(ctx, |model, ctx| {
|
||||
if FeatureFlag::OpenWarpNewSettingsModes.is_enabled() {
|
||||
// Always advance to Customize slide; both intentions continue the flow.
|
||||
model.next(ctx);
|
||||
match model.intention() {
|
||||
// "Just use the terminal" confirms leaving AI behind before advancing.
|
||||
OnboardingIntention::Terminal => {
|
||||
model.request_no_ai_confirmation(NoAiConfirmationSource::Intention, ctx);
|
||||
}
|
||||
// Agent intention routes to the next step (the AI-setup fork).
|
||||
OnboardingIntention::AgentDrivenDevelopment => model.next(ctx),
|
||||
}
|
||||
} else {
|
||||
match model.intention() {
|
||||
OnboardingIntention::Terminal => {
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::OnboardingEvent;
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, Icon};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
shimmering_text::{ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle},
|
||||
Align, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning,
|
||||
ParentAnchor, ParentElement, ParentOffsetBounds, Stack,
|
||||
},
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::shimmering_text::{
|
||||
ShimmerConfig, ShimmeringTextElement, ShimmeringTextStateHandle,
|
||||
};
|
||||
use galaxyui_core::elements::{
|
||||
Align, ChildAnchor, ConstrainedBox, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Stack,
|
||||
};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::OnboardingEvent;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum IntroSlideEvent {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use galaxyui::{
|
||||
assets::asset_cache::AssetSource,
|
||||
elements::{
|
||||
Align, CacheOption, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Empty,
|
||||
Expanded, Flex, Image, MainAxisSize, ParentElement, Point, Shrinkable,
|
||||
SizeConstraintCondition, SizeConstraintSwitch, Stack,
|
||||
},
|
||||
event::DispatchedEvent,
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
use galaxyui_core::assets::asset_cache::AssetSource;
|
||||
use galaxyui_core::elements::{
|
||||
Align, CacheOption, Clipped, ConstrainedBox, Container, CrossAxisAlignment, Empty, Expanded,
|
||||
Flex, Image, MainAxisSize, ParentElement, Point, Shrinkable, SizeConstraintCondition,
|
||||
SizeConstraintSwitch, Stack,
|
||||
};
|
||||
use galaxyui_core::event::DispatchedEvent;
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
|
||||
SizeConstraint,
|
||||
};
|
||||
use pathfinder_geometry::vector::{vec2f, Vector2F};
|
||||
|
||||
// Onboarding images live under `app/assets/async/` so they are excluded from the WASM
|
||||
// binary (RustEmbed excludes `async/**` on wasm targets). They are still bundled normally
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
mod agent_slide;
|
||||
mod ai_access_slide;
|
||||
mod ai_setup_slide;
|
||||
mod bottom_nav;
|
||||
mod customize_slide;
|
||||
mod free_user_no_ai_slide;
|
||||
mod intention_slide;
|
||||
mod intro_slide;
|
||||
pub mod layout;
|
||||
@@ -14,12 +15,11 @@ mod third_party_slide;
|
||||
mod toggle_card;
|
||||
mod two_line_button;
|
||||
|
||||
pub use agent_slide::{
|
||||
AgentAutonomy, AgentDevelopmentSettings, AgentSlide, AgentSlideEvent, OnboardingModelInfo,
|
||||
};
|
||||
pub use agent_slide::{AgentAutonomy, AgentDevelopmentSettings, AgentSlide, OnboardingModelInfo};
|
||||
pub use ai_access_slide::{AiAccessSlide, AiAccessSlideEvent};
|
||||
pub use ai_setup_slide::AiSetupSlide;
|
||||
pub use bottom_nav::onboarding_bottom_nav;
|
||||
pub use customize_slide::CustomizeUISlide;
|
||||
pub use free_user_no_ai_slide::FreeUserNoAiSlide;
|
||||
pub use intention_slide::IntentionSlide;
|
||||
pub use intro_slide::{IntroSlide, IntroSlideEvent};
|
||||
pub use onboarding_slide::OnboardingSlide;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use galaxyui::{View, ViewContext};
|
||||
use galaxyui_core::{View, ViewContext};
|
||||
|
||||
pub trait OnboardingSlide: View {
|
||||
fn on_up(&mut self, _ctx: &mut ViewContext<Self>) {}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ConstrainedBox, Container, CornerRadius, Empty, Flex, MainAxisSize, ParentElement, Radius,
|
||||
},
|
||||
Element,
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ConstrainedBox, Container, CornerRadius, Empty, Flex, MainAxisSize, ParentElement, Radius,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
/// Render `n` dots with 4px radius and 8px spacing. `k` is the 0-based active dot index.
|
||||
pub(crate) fn progress_dots(n: usize, k: usize, appearance: &Appearance) -> Box<dyn Element> {
|
||||
|
||||
@@ -1,28 +1,29 @@
|
||||
use ui_components::{button, keyboard_shortcut, Component as _, Options as _};
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::color::coloru_with_opacity;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui_core::elements::{
|
||||
Align, ClippedScrollStateHandle, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
MouseStateHandle, ParentElement, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::file_picker::{FilePickerConfiguration, FilePickerError};
|
||||
use galaxyui_core::prelude::{MainAxisAlignment, MainAxisSize, Vector2F};
|
||||
use galaxyui_core::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::OnboardingStateModel;
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::visuals::project_visual;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance, color::coloru_with_opacity, theme::color::internal_colors, Icon,
|
||||
};
|
||||
use galaxyui::prelude::{MainAxisAlignment, MainAxisSize, Vector2F};
|
||||
use galaxyui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ClippedScrollStateHandle, ConstrainedBox, Container, CrossAxisAlignment, Flex,
|
||||
MouseStateHandle, ParentElement, Shrinkable,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
platform::file_picker::{FilePickerConfiguration, FilePickerError},
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, keyboard_shortcut, Component as _, Options as _};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
|
||||
const LEFT_COLUMN_W: f32 = 428.;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, ClippedScrollStateHandle, ClippedScrollable, Container, CrossAxisAlignment, Flex,
|
||||
MainAxisSize, ParentElement, ScrollbarWidth, Shrinkable,
|
||||
},
|
||||
Element,
|
||||
use galaxyui_core::elements::{
|
||||
Align, ClippedScrollStateHandle, ClippedScrollable, Container, CrossAxisAlignment, Flex,
|
||||
MainAxisSize, ParentElement, ScrollbarWidth, Shrinkable,
|
||||
};
|
||||
use galaxyui_core::Element;
|
||||
|
||||
pub fn onboarding_slide_content(
|
||||
children: Vec<Box<dyn Element>>,
|
||||
@@ -57,7 +55,7 @@ pub fn onboarding_slide_content(
|
||||
.with_child(Shrinkable::new(1., scrollable).finish())
|
||||
.with_child(
|
||||
Container::new(bottom_nav)
|
||||
.with_margin_top(24.)
|
||||
.with_margin_top(16.)
|
||||
.with_padding_right(PADDING)
|
||||
.finish(),
|
||||
)
|
||||
@@ -67,7 +65,7 @@ pub fn onboarding_slide_content(
|
||||
// inside the scrollable and bottom nav so the scrollbar stays at the edge.
|
||||
Container::new(outer)
|
||||
.with_padding_top(PADDING)
|
||||
.with_padding_bottom(PADDING)
|
||||
.with_padding_bottom(PADDING - 16.)
|
||||
.with_padding_left(PADDING)
|
||||
.finish()
|
||||
}
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::WarpTheme;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseStateHandle, ParentElement, Radius, Text,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::telemetry::OnboardingEvent;
|
||||
use crate::visuals::theme_picker_visual;
|
||||
use crate::OnboardingIntention;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors, theme::GalaxyTheme};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ClippedScrollStateHandle, ConstrainedBox, Container, CornerRadius,
|
||||
CrossAxisAlignment, Empty, Flex, FormattedTextElement, Hoverable, MainAxisAlignment,
|
||||
MainAxisSize, MouseStateHandle, ParentElement, Radius, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
keymap::Keystroke,
|
||||
platform::Cursor,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ThemePickerSlideEvent {
|
||||
@@ -134,12 +137,15 @@ impl ThemePickerSlide {
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
// The option "chrome" (background, borders, text) should be styled using the currently
|
||||
// selected theme.
|
||||
let selected_theme = self
|
||||
.theme_options
|
||||
.get(self.selected_theme_index)
|
||||
.map(|option| option.theme.clone())
|
||||
.unwrap_or_else(|| self.theme_options[0].theme.clone());
|
||||
// selected theme, if sync_with_os is not selected.
|
||||
let selected_theme = if self.sync_with_os {
|
||||
appearance.theme().clone()
|
||||
} else {
|
||||
self.theme_options
|
||||
.get(self.selected_theme_index)
|
||||
.map(|option| option.theme.clone())
|
||||
.unwrap_or_else(|| self.theme_options[0].theme.clone())
|
||||
};
|
||||
|
||||
let bottom_nav = self.render_bottom_nav(appearance, app);
|
||||
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::keymap::Keystroke;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::ui_components::components::{UiComponent as _, UiComponentStyles};
|
||||
use galaxyui_core::{
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
use super::toggle_card::{render_toggle_card, ToggleCardSpec};
|
||||
use super::OnboardingSlide;
|
||||
use crate::model::{OnboardingStateEvent, OnboardingStateModel};
|
||||
use crate::slides::{bottom_nav, layout, slide_content};
|
||||
use crate::OnboardingIntention;
|
||||
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
ClippedScrollStateHandle, Container, CrossAxisAlignment, Flex, FormattedTextElement,
|
||||
MainAxisSize, MouseStateHandle, ParentElement,
|
||||
},
|
||||
fonts::Weight,
|
||||
keymap::Keystroke,
|
||||
text_layout::TextAlignment,
|
||||
ui_components::components::{UiComponent as _, UiComponentStyles},
|
||||
AppContext, Element, Entity, ModelHandle, SingletonEntity as _, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
use ui_components::{button, Component as _, Options as _};
|
||||
|
||||
/// Which setting card is currently expanded.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SettingCard {
|
||||
@@ -111,8 +111,9 @@ impl ThirdPartySlide {
|
||||
cli_toolbar_enabled: bool,
|
||||
show_agent_notifications: bool,
|
||||
intention: OnboardingIntention,
|
||||
app: &AppContext,
|
||||
) -> Box<dyn Element> {
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, intention)).finish();
|
||||
let bottom_nav = Align::new(self.render_bottom_nav(appearance, app)).finish();
|
||||
|
||||
let mut sections = vec![
|
||||
self.render_header(appearance),
|
||||
@@ -263,11 +264,7 @@ impl ThirdPartySlide {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_bottom_nav(
|
||||
&self,
|
||||
appearance: &Appearance,
|
||||
intention: OnboardingIntention,
|
||||
) -> Box<dyn Element> {
|
||||
fn render_bottom_nav(&self, appearance: &Appearance, app: &AppContext) -> Box<dyn Element> {
|
||||
let back_button = self.back_button.render(
|
||||
appearance,
|
||||
button::Params {
|
||||
@@ -298,8 +295,7 @@ impl ThirdPartySlide {
|
||||
},
|
||||
);
|
||||
|
||||
let is_terminal = matches!(intention, OnboardingIntention::Terminal);
|
||||
let (step_index, step_count) = if is_terminal { (2, 4) } else { (3, 5) };
|
||||
let (step_index, step_count) = self.onboarding_state.as_ref(app).progress();
|
||||
bottom_nav::onboarding_bottom_nav(
|
||||
appearance,
|
||||
step_index,
|
||||
@@ -361,6 +357,7 @@ impl View for ThirdPartySlide {
|
||||
cli_toolbar_enabled,
|
||||
show_agent_notifications,
|
||||
intention,
|
||||
app,
|
||||
)
|
||||
},
|
||||
|| self.render_visual(cli_toolbar_enabled, show_agent_notifications, vertical),
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::prelude::Align;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
presenter::EventContext,
|
||||
text_layout::TextAlignment,
|
||||
AppContext, Element,
|
||||
};
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
FormattedTextElement, Hoverable, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Shrinkable, Text, Wrap,
|
||||
};
|
||||
use galaxyui_core::fonts::Weight;
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::prelude::Align;
|
||||
use galaxyui_core::presenter::EventContext;
|
||||
use galaxyui_core::text_layout::TextAlignment;
|
||||
use galaxyui_core::{AppContext, Element};
|
||||
|
||||
pub(super) type ClickCallback = Box<dyn FnMut(&mut EventContext, &AppContext, Vector2F) + 'static>;
|
||||
pub(super) type HoverCallback =
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
use super::agent_slide::AgentSlideAction;
|
||||
use galaxy_core::ui::{
|
||||
appearance::Appearance,
|
||||
icons::Icon,
|
||||
theme::{color::internal_colors, Fill},
|
||||
};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Flex, Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Radius, Stack, Text,
|
||||
},
|
||||
fonts::{Properties, Weight},
|
||||
platform::Cursor,
|
||||
Element,
|
||||
};
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::Fill;
|
||||
use galaxyui_core::elements::{
|
||||
Align, Border, ChildAnchor, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
Hoverable, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor, ParentElement,
|
||||
ParentOffsetBounds, Radius, Stack, Text,
|
||||
};
|
||||
use galaxyui_core::fonts::{Properties, Weight};
|
||||
use galaxyui_core::platform::Cursor;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::agent_slide::AgentSlideAction;
|
||||
|
||||
pub(super) struct TwoLineButtonSpec {
|
||||
pub(super) is_selected: bool,
|
||||
|
||||
@@ -20,6 +20,9 @@ pub enum OnboardingEvent {
|
||||
model: Option<String>,
|
||||
autonomy: Option<String>,
|
||||
has_project_path: bool,
|
||||
/// How the user is accessing AI when intention is agent_driven:
|
||||
/// "warp_agent" or "third_party". None when intention is not agent_driven.
|
||||
ai_access: Option<String>,
|
||||
},
|
||||
/// The user clicked the "Get Started" button.
|
||||
GetStartedClicked,
|
||||
@@ -37,8 +40,12 @@ pub enum OnboardingEvent {
|
||||
SlideNavigatedNext,
|
||||
/// The user navigated to the previous slide.
|
||||
SlideNavigatedBack,
|
||||
/// The user clicked the upgrade/subscribe button on the FreeUserNoAi experiment slide.
|
||||
FreeUserNoAiUpgradeClicked,
|
||||
/// The user was shown the "Are you sure you don't want AI?" confirmation modal.
|
||||
NoAiConfirmationShown,
|
||||
/// The user confirmed they don't want AI in the confirmation modal.
|
||||
NoAiConfirmed,
|
||||
/// The user chose to keep AI ("Give me AI features") in the confirmation modal.
|
||||
NoAiConfirmationCancelled,
|
||||
/// The user clicked the "Upgrade" button on the "Customize your agent" slide.
|
||||
AgentSlideUpgradeClicked,
|
||||
/// The user clicked the "Log in" link on the welcome/intro slide.
|
||||
@@ -60,9 +67,9 @@ impl TelemetryEvent for OnboardingEvent {
|
||||
OnboardingEvent::CalloutCompleted { .. } => "onboarding_callout_completed",
|
||||
OnboardingEvent::SlideNavigatedNext => "onboarding_slide_navigated_next",
|
||||
OnboardingEvent::SlideNavigatedBack => "onboarding_slide_navigated_back",
|
||||
OnboardingEvent::FreeUserNoAiUpgradeClicked => {
|
||||
"onboarding_free_user_no_ai_upgrade_clicked"
|
||||
}
|
||||
OnboardingEvent::NoAiConfirmationShown => "onboarding_no_ai_confirmation_shown",
|
||||
OnboardingEvent::NoAiConfirmed => "onboarding_no_ai_confirmed",
|
||||
OnboardingEvent::NoAiConfirmationCancelled => "onboarding_no_ai_confirmation_cancelled",
|
||||
OnboardingEvent::AgentSlideUpgradeClicked => "onboarding_agent_slide_upgrade_clicked",
|
||||
OnboardingEvent::WelcomeLoginClicked => "onboarding_welcome_login_clicked",
|
||||
}
|
||||
@@ -83,11 +90,13 @@ impl TelemetryEvent for OnboardingEvent {
|
||||
model,
|
||||
autonomy,
|
||||
has_project_path,
|
||||
ai_access,
|
||||
} => Some(json!({
|
||||
"intention": intention,
|
||||
"model": model,
|
||||
"autonomy": autonomy,
|
||||
"has_project_path": has_project_path,
|
||||
"ai_access": ai_access,
|
||||
})),
|
||||
OnboardingEvent::GetStartedClicked => None,
|
||||
OnboardingEvent::FolderSelectionStarted => None,
|
||||
@@ -101,7 +110,9 @@ impl TelemetryEvent for OnboardingEvent {
|
||||
})),
|
||||
OnboardingEvent::SlideNavigatedNext => None,
|
||||
OnboardingEvent::SlideNavigatedBack => None,
|
||||
OnboardingEvent::FreeUserNoAiUpgradeClicked => None,
|
||||
OnboardingEvent::NoAiConfirmationShown => None,
|
||||
OnboardingEvent::NoAiConfirmed => None,
|
||||
OnboardingEvent::NoAiConfirmationCancelled => None,
|
||||
OnboardingEvent::AgentSlideUpgradeClicked => None,
|
||||
OnboardingEvent::WelcomeLoginClicked => None,
|
||||
}
|
||||
@@ -123,8 +134,12 @@ impl TelemetryEvent for OnboardingEvent {
|
||||
OnboardingEvent::CalloutCompleted { .. } => "User completed the callout flow",
|
||||
OnboardingEvent::SlideNavigatedNext => "User navigated to the next slide",
|
||||
OnboardingEvent::SlideNavigatedBack => "User navigated to the previous slide",
|
||||
OnboardingEvent::FreeUserNoAiUpgradeClicked => {
|
||||
"User clicked the upgrade button on the free-user no-AI experiment slide"
|
||||
OnboardingEvent::NoAiConfirmationShown => "User was shown the no-AI confirmation modal",
|
||||
OnboardingEvent::NoAiConfirmed => {
|
||||
"User confirmed they don't want AI in the confirmation modal"
|
||||
}
|
||||
OnboardingEvent::NoAiConfirmationCancelled => {
|
||||
"User chose to keep AI in the confirmation modal"
|
||||
}
|
||||
OnboardingEvent::AgentSlideUpgradeClicked => {
|
||||
"User clicked the Upgrade button on the Customize your agent slide"
|
||||
@@ -165,8 +180,12 @@ impl TelemetryEventDesc for OnboardingEventDiscriminant {
|
||||
OnboardingEventDiscriminant::CalloutCompleted => "onboarding_callout_completed",
|
||||
OnboardingEventDiscriminant::SlideNavigatedNext => "onboarding_slide_navigated_next",
|
||||
OnboardingEventDiscriminant::SlideNavigatedBack => "onboarding_slide_navigated_back",
|
||||
OnboardingEventDiscriminant::FreeUserNoAiUpgradeClicked => {
|
||||
"onboarding_free_user_no_ai_upgrade_clicked"
|
||||
OnboardingEventDiscriminant::NoAiConfirmationShown => {
|
||||
"onboarding_no_ai_confirmation_shown"
|
||||
}
|
||||
OnboardingEventDiscriminant::NoAiConfirmed => "onboarding_no_ai_confirmed",
|
||||
OnboardingEventDiscriminant::NoAiConfirmationCancelled => {
|
||||
"onboarding_no_ai_confirmation_cancelled"
|
||||
}
|
||||
OnboardingEventDiscriminant::AgentSlideUpgradeClicked => {
|
||||
"onboarding_agent_slide_upgrade_clicked"
|
||||
@@ -197,8 +216,14 @@ impl TelemetryEventDesc for OnboardingEventDiscriminant {
|
||||
OnboardingEventDiscriminant::SlideNavigatedBack => {
|
||||
"User navigated to the previous slide"
|
||||
}
|
||||
OnboardingEventDiscriminant::FreeUserNoAiUpgradeClicked => {
|
||||
"User clicked the upgrade button on the free-user no-AI experiment slide"
|
||||
OnboardingEventDiscriminant::NoAiConfirmationShown => {
|
||||
"User was shown the no-AI confirmation modal"
|
||||
}
|
||||
OnboardingEventDiscriminant::NoAiConfirmed => {
|
||||
"User confirmed they don't want AI in the confirmation modal"
|
||||
}
|
||||
OnboardingEventDiscriminant::NoAiConfirmationCancelled => {
|
||||
"User chose to keep AI in the confirmation modal"
|
||||
}
|
||||
OnboardingEventDiscriminant::AgentSlideUpgradeClicked => {
|
||||
"User clicked the Upgrade button on the Customize your agent slide"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use galaxy_core::telemetry::{TelemetryContextModel, TelemetryContextProvider};
|
||||
use galaxyui::{AppContext, ModelContext};
|
||||
use galaxyui_core::{AppContext, ModelContext};
|
||||
|
||||
/// A mock telemetry context provider for the onboarding binary that logs events
|
||||
/// instead of sending them to a server.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use galaxyui::elements::Align;
|
||||
use galaxyui::Element;
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui_core::elements::Align;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::onboarding_visual::{OnboardingVisual, Pill, RectPct};
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use galaxyui::elements::Align;
|
||||
use galaxyui::Element;
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
use crate::visuals::onboarding_visual::Rect;
|
||||
use galaxyui_core::elements::Align;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::onboarding_visual::{OnboardingVisual, Pill, RectPct};
|
||||
use crate::visuals::onboarding_visual::Rect;
|
||||
|
||||
pub(crate) fn intention_terminal_visual(
|
||||
panel_background: ColorU,
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
use galaxyui::elements::Align;
|
||||
use galaxyui::Element;
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui_core::elements::Align;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::onboarding_visual::{OnboardingVisual, Pill, RectPct};
|
||||
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxy_core::ui::Icon;
|
||||
use galaxyui::assets::asset_cache::{AssetCache, AssetSource, AssetState};
|
||||
use galaxyui::geometry::rect::RectF;
|
||||
use galaxyui::geometry::vector::{vec2f, Vector2F};
|
||||
use galaxyui::image_cache::{AnimatedImageBehavior, CacheOption, FitType, Image, ImageCache};
|
||||
use galaxyui::{
|
||||
elements::{CornerRadius, Fill, Point, Radius},
|
||||
event::DispatchedEvent,
|
||||
use galaxyui_core::assets::asset_cache::{AssetCache, AssetSource, AssetState};
|
||||
use galaxyui_core::elements::{CornerRadius, Fill, Point, Radius};
|
||||
use galaxyui_core::event::DispatchedEvent;
|
||||
use galaxyui_core::geometry::rect::RectF;
|
||||
use galaxyui_core::geometry::vector::{vec2f, Vector2F};
|
||||
use galaxyui_core::image_cache::{AnimatedImageBehavior, CacheOption, FitType, Image, ImageCache};
|
||||
use galaxyui_core::{
|
||||
AfterLayoutContext, AppContext, Element, EventContext, LayoutContext, PaintContext,
|
||||
SingletonEntity as _, SizeConstraint,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub(crate) struct RectPct {
|
||||
|
||||
@@ -2,6 +2,8 @@ use galaxy_core::ui::Icon;
|
||||
use galaxyui::elements::Align;
|
||||
use galaxyui::Element;
|
||||
use pathfinder_color::ColorU;
|
||||
use galaxyui_core::elements::Align;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::onboarding_visual::{IconPct, OnboardingVisual, Pill, RectPct};
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui_core::elements::Align;
|
||||
use galaxyui_core::Element;
|
||||
|
||||
use super::onboarding_visual::{OnboardingVisual, Pill, RectPct};
|
||||
use galaxy_core::ui::{appearance::Appearance, theme::color::internal_colors};
|
||||
use galaxyui::{elements::Align, Element};
|
||||
|
||||
pub(crate) fn theme_picker_visual(appearance: &Appearance) -> Box<dyn Element> {
|
||||
let theme = appearance.theme();
|
||||
|
||||
Reference in New Issue
Block a user