first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+244 -295
View File
@@ -1,15 +1,60 @@
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use anyhow::Result;
use cfg_if::cfg_if;
use itertools::Itertools;
use lazy_static::lazy_static;
use onboarding::{
AgentOnboardingEvent, AgentOnboardingView, OnboardingIntention, SelectedSettings,
};
use parking_lot::Mutex;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde::{Deserialize, Serialize};
use session_sharing_protocol::common::SessionId;
use settings::Setting as _;
use url::Url;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::safe_error;
use galaxy_core::user_preferences::GetUserPreferences as _;
use warpui::clipboard::ClipboardContent;
use warpui::elements::{
Border, ChildAnchor, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Stack,
};
use warpui::keymap::{EditableBinding, FixedBinding};
use warpui::platform::{WindowBounds, WindowStyle};
use warpui::presenter::ChildView;
use warpui::rendering::OnGPUDeviceSelected;
use warpui::windowing::WindowManager;
use warpui::{
id, AddWindowOptions, AppContext, DisplayId, Element, Entity, EntityId, FocusContext,
NextNewWindowsHasThisWindowsBoundsUponClose, SingletonEntity, TypedActionView, View,
ViewContext, ViewHandle, WindowId,
};
use crate::ai::agent::api::ServerConversationToken;
use crate::ai::blocklist::SerializedBlockListItem;
use crate::ai::llms::{LLMPreferences, LLMPreferencesEvent};
use crate::ai::onboarding::{build_onboarding_models, current_onboarding_auth_state};
use crate::app_state::{AppState, PaneUuid, WindowSnapshot};
use crate::appearance::Appearance;
use crate::auth::auth_manager::{AuthManager, AuthManagerEvent};
use crate::auth::auth_override_warning_modal::AuthOverrideWarningModalVariant;
use crate::auth::auth_override_warning_modal::{
AuthOverrideWarningModal, AuthOverrideWarningModalEvent, AuthOverrideWarningModalVariant,
};
use crate::auth::auth_state::AuthState;
use crate::auth::auth_view_modal::AuthRedirectPayload;
use crate::auth::auth_view_modal::{AuthRedirectPayload, AuthView, AuthViewVariant};
use crate::auth::login_slide::{LoginSlideEvent, LoginSlideSource, LoginSlideView};
use crate::auth::needs_sso_link_view::NeedsSsoLinkView;
use crate::auth::paste_auth_token_modal::{PasteAuthTokenModalEvent, PasteAuthTokenModalView};
#[cfg(target_family = "wasm")]
use crate::auth::web_handoff::{WebHandoffEvent, WebHandoffView};
use crate::auth::{AuthStateProvider, LoginFailureReason};
use crate::autoupdate::{AutoupdateState, AutoupdateStateEvent};
use crate::autoupdate::{AutoupdateState, AutoupdateStateEvent, RequestType, UpdateReady};
use crate::changelog_model::ChangelogRequestType;
use crate::cloud_object::model::persistence::CloudModel;
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType, ObjectType};
use crate::drive::export::ExportManager;
@@ -18,110 +63,47 @@ use crate::drive::{
CloudObjectTypeAndId, OpenGalaxyDriveObjectArgs, OpenGalaxyDriveObjectSettings,
};
use crate::experiments::{BlockOnboarding, Experiment};
use crate::features::FeatureFlag;
use crate::interval_timer::IntervalTimer;
use crate::launch_configs::launch_config;
use crate::linear::LinearIssueWork;
use crate::notebooks::manager::NotebookSource;
use crate::settings::apply_onboarding_settings;
use crate::pane_group::{NewTerminalOptions, PanesLayout};
use crate::persistence::ModelEvent;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::ids::SyncId;
use crate::server::server_api::auth::UserAuthenticationError;
use crate::server::server_api::{ServerApi, ServerApiProvider, ServerTime};
use crate::server::telemetry::{LaunchConfigUiLocation, TelemetryEvent};
use crate::settings::cloud_preferences_syncer::{
CloudPreferencesSyncer, CloudPreferencesSyncerEvent,
};
use crate::settings::AISettings;
use crate::workspace::tab_settings::TabSettings;
use onboarding::{
AgentOnboardingEvent, AgentOnboardingView, OnboardingIntention, SelectedSettings,
};
use crate::persistence::ModelEvent;
use crate::report_if_error;
use crate::server::cloud_objects::update_manager::UpdateManager;
use crate::server::experiments::is_free_user_no_ai_experiment_active;
use crate::server::ids::SyncId;
use crate::server::server_api::auth::UserAuthenticationError;
use crate::server::server_api::ServerApiProvider;
use crate::server::telemetry::LaunchConfigUiLocation;
use crate::settings::QuakeModeSettings;
use crate::settings::ThemeSettings;
use crate::settings_view::flags;
use crate::settings::{apply_onboarding_settings, AISettings, QuakeModeSettings, ThemeSettings};
use crate::settings_view::mcp_servers_page::MCPServersSettingsPage;
use crate::settings_view::{OpenTeamsSettingsModalArgs, SettingsSection};
use crate::settings_view::{flags, OpenTeamsSettingsModalArgs, SettingsSection};
use crate::terminal::available_shells::AvailableShell;
use crate::terminal::general_settings::GeneralSettings;
use crate::terminal::keys_settings::KeysSettings;
use crate::terminal::shell::ShellType;
use crate::terminal::view::{cell_size_and_padding, TerminalAction};
use crate::themes::onboarding_theme_picker_themes;
use crate::themes::theme::{AnsiColorIdentifier, Blend, Fill, GalaxyThemeConfig, ThemeKind};
use crate::uri::OpenMCPSettingsArgs;
use crate::themes::theme::{AnsiColorIdentifier, Blend, Fill, ThemeKind, WarpThemeConfig};
use crate::uri::{OpenMCPSettingsArgs, OpenSettingsArgs};
use crate::util::bindings::{self, is_binding_pty_compliant};
use crate::util::traffic_lights::{traffic_light_data, TrafficLightData, TrafficLightMouseStates};
use crate::view_components::DismissibleToast;
use crate::window_settings::WindowSettings;
use crate::workspace::hoa_onboarding::mark_hoa_onboarding_completed;
use crate::workspace::WorkspaceAction;
use crate::workspace::tab_settings::TabSettings;
use crate::workspace::view::OnboardingTutorial;
use crate::workspace::{PaneViewLocator, Workspace, WorkspaceAction, WorkspaceRegistry};
use crate::workspaces::team_tester::TeamTesterStatus;
use crate::workspaces::update_manager::TeamUpdateManager;
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
use crate::{
app_state::{AppState, PaneUuid, WindowSnapshot},
autoupdate::{RequestType, UpdateReady},
changelog_model::ChangelogRequestType,
pane_group::{NewTerminalOptions, PanesLayout},
send_telemetry_from_ctx,
server::{server_api::ServerTime, telemetry::TelemetryEvent},
UpdateQuakeModeEventArg,
report_if_error, send_telemetry_from_app_ctx, send_telemetry_from_ctx, ChannelState,
GlobalResourceHandles, GlobalResourceHandlesProvider, UpdateQuakeModeEventArg,
};
use crate::{
auth::auth_override_warning_modal::{AuthOverrideWarningModal, AuthOverrideWarningModalEvent},
auth::auth_view_modal::{AuthView, AuthViewVariant},
server::server_api::ServerApi,
workspace::{view::OnboardingTutorial, PaneViewLocator, Workspace},
};
use crate::{features::FeatureFlag, ChannelState};
use crate::{send_telemetry_from_app_ctx, GlobalResourceHandles, GlobalResourceHandlesProvider};
use anyhow::Result;
use cfg_if::cfg_if;
use galaxy_core::context_flag::ContextFlag;
use galaxy_core::user_preferences::GetUserPreferences as _;
use galaxyui::clipboard::ClipboardContent;
use galaxyui::keymap::{EditableBinding, FixedBinding};
use galaxyui::windowing::WindowManager;
use itertools::Itertools;
use lazy_static::lazy_static;
use parking_lot::Mutex;
use pathfinder_geometry::rect::RectF;
use pathfinder_geometry::vector::{vec2f, Vector2F};
use serde::{Deserialize, Serialize};
use session_sharing_protocol::common::SessionId;
use settings::Setting as _;
use std::path::Path;
use std::sync::mpsc::SyncSender;
use std::sync::Arc;
use std::{collections::HashMap, path::PathBuf};
use url::Url;
use crate::ai::llms::{LLMPreferences, LLMPreferencesEvent};
use crate::ai::onboarding::{
apply_free_tier_default_model_override, build_onboarding_models, current_onboarding_auth_state,
};
use crate::pricing::{PricingInfoModel, PricingInfoModelEvent};
use galaxy_graphql::billing::StripeSubscriptionPlan;
use galaxyui::elements::{
Border, ChildAnchor, OffsetPositioning, ParentAnchor, ParentElement, ParentOffsetBounds, Stack,
};
use galaxyui::rendering::OnGPUDeviceSelected;
use galaxyui::{id, AddWindowOptions, DisplayId, SingletonEntity};
use galaxyui::{
platform::{WindowBounds, WindowStyle},
presenter::ChildView,
AppContext, Element, Entity, EntityId, TypedActionView, View, ViewContext, ViewHandle,
WindowId,
};
use galaxyui::{FocusContext, NextNewWindowsHasThisWindowsBoundsUponClose};
#[cfg(target_family = "wasm")]
use crate::auth::web_handoff::{WebHandoffEvent, WebHandoffView};
const WINDOW_TITLE: &str = "Galaxy";
@@ -245,29 +227,6 @@ pub struct CreateEnvironmentArg {
pub repos: Vec<String>,
}
/// Arguments for the immediate tab detach action dispatched during drag.
/// This contains minimal info needed to identify which tab to detach.
pub struct DetachTabImmediateArg {
/// Index of the tab to detach
pub tab_index: usize,
/// Pre-calculated window position for the new window (in screen coordinates).
/// This is calculated to position the window so the mouse is in the tab bar region.
pub window_position: Option<Vector2F>,
/// Source window ID - the window containing the tab to detach.
/// We need this because the active window might be the preview window.
pub source_window_id: WindowId,
}
/// Pre-gathered information for creating a transferred window.
/// This is used when the caller already has access to the workspace (e.g., from within a view method)
/// and cannot rely on workspace lookup (which fails during view updates).
pub struct TabTransferInfo {
pub transferred_tab: crate::workspace::view::TransferredTab,
pub window_size: Vector2F,
pub window_position: Vector2F,
pub source_window_id: WindowId,
}
impl CreateEnvironmentArg {
/// Formats the `/create-environment` slash command invocation.
pub fn to_query(&self) -> String {
@@ -314,9 +273,6 @@ pub fn init(app: &mut AppContext) {
);
app.add_global_action("root_view:open_launch_config", open_launch_config);
app.add_global_action("root_view:send_feedback", send_feedback);
app.add_global_action("root_view:detach_tab_immediate", |arg, ctx| {
let _ = detach_tab_with_transfer(arg, ctx);
});
app.add_global_action(
"root_view:toggle_quake_mode_window",
toggle_quake_mode_window,
@@ -362,6 +318,10 @@ pub fn init(app: &mut AppContext) {
"root_view:handle_pane_navigation_event",
RootView::focus_pane,
);
app.add_action(
"root_view:activate_tab_by_pane_group_id",
RootView::activate_tab_by_pane_group_id,
);
app.add_action("root_view:close_window", RootView::close_window);
app.add_action("root_view:minimize_window", RootView::minimize_window);
app.add_action(
@@ -430,6 +390,15 @@ pub fn init(app: &mut AppContext) {
RootView::open_settings_page_in_existing_window,
);
app.add_global_action(
"root_view:open_settings_in_new_window",
open_settings_in_new_window,
);
app.add_action(
"root_view:open_settings_in_existing_window",
RootView::open_settings_in_existing_window,
);
app.add_global_action(
"root_view:open_mcp_settings_in_new_window",
open_mcp_settings_in_new_window,
@@ -544,17 +513,7 @@ fn maybe_register_global_window_shortcuts(
/// Find the root [`Workspace`] view for the active window.
fn active_workspace(ctx: &mut AppContext) -> Option<ViewHandle<Workspace>> {
let window_id = ctx.windows().active_window()?;
ctx.views_of_type::<Workspace>(window_id)
.and_then(|views| views.first().cloned())
}
/// Find the root [`Workspace`] view for a specific window.
pub fn workspace_for_window(
window_id: WindowId,
ctx: &mut AppContext,
) -> Option<ViewHandle<Workspace>> {
ctx.views_of_type::<Workspace>(window_id)
.and_then(|views| views.first().cloned())
WorkspaceRegistry::as_ref(ctx).get(window_id, ctx)
}
fn open_launch_config(arg: &OpenLaunchConfigArg, ctx: &mut AppContext) {
@@ -625,73 +584,29 @@ fn send_feedback(_: &(), ctx: &mut AppContext) {
}
}
/// Handler for tab detachment using the transferable views framework.
/// Instead of extracting and recreating views, this transfers the PaneGroup view tree directly.
/// Returns the new window ID if successful.
pub fn detach_tab_with_transfer(
arg: &DetachTabImmediateArg,
ctx: &mut AppContext,
) -> Option<WindowId> {
let Some(source_workspace) = workspace_for_window(arg.source_window_id, ctx) else {
log::warn!(
"No workspace found for source window {:?}",
arg.source_window_id
);
return None;
};
let transferred_tab = source_workspace.read(ctx, |workspace, ctx| {
workspace.get_tab_transfer_info(arg.tab_index, ctx)
})?;
let window_size = ctx
.windows()
.platform_window(arg.source_window_id)
.map(|window| window.as_ctx().size())
.unwrap_or(*FALLBACK_WINDOW_SIZE);
let window_position = arg.window_position.unwrap_or_default();
let info = TabTransferInfo {
transferred_tab,
window_size,
window_position,
source_window_id: arg.source_window_id,
};
let (new_window_id, _transferred_view_ids) = create_transferred_window(info, false, ctx);
source_workspace.update(ctx, |workspace, ctx| {
workspace.remove_tab_without_undo(arg.tab_index, ctx);
});
Some(new_window_id)
}
/// Creates a new window with the transferred pane group.
/// This function takes pre-gathered TabTransferInfo, allowing it to be called
/// from within a view method where workspace lookup would fail.
///
/// If `for_drag` is true, the window is created without stealing focus (for drag preview).
/// If `is_tab_drag_preview` is true, the window is created without stealing
/// focus so it can follow the cursor during a tab drag.
///
/// Returns the new window ID and the list of transferred view entity IDs.
/// The transferred view IDs are needed by `tab_drag::on_tab_drag` to track which
/// views must follow the tab during subsequent handoff/reverse-handoff cycles.
/// Returns the new window ID.
pub fn create_transferred_window(
info: TabTransferInfo,
for_drag: bool,
transferred_tab: crate::workspace::view::TransferredTab,
source_window_id: WindowId,
window_size: Vector2F,
window_position: Vector2F,
is_tab_drag_preview: bool,
ctx: &mut AppContext,
) -> (WindowId, Vec<EntityId>) {
) -> WindowId {
let global_resource_handles = GlobalResourceHandlesProvider::handle(ctx)
.as_ref(ctx)
.get()
.clone();
let window_settings = WindowSettings::handle(ctx).as_ref(ctx);
let window_bounds =
WindowBounds::ExactPosition(RectF::new(info.window_position, info.window_size));
let window_bounds = WindowBounds::ExactPosition(RectF::new(window_position, window_size));
let window_style = if for_drag {
let window_style = if is_tab_drag_preview {
WindowStyle::PositionedNoFocus
} else {
WindowStyle::Normal
@@ -711,35 +626,34 @@ pub fn create_transferred_window(
let mut view = RootView::new(
global_resource_handles.clone(),
NewWorkspaceSource::TransferredTab {
tab_color: info.transferred_tab.color,
custom_title: info.transferred_tab.custom_title.clone(),
left_panel_open: info.transferred_tab.left_panel_open,
vertical_tabs_panel_open: info.transferred_tab.vertical_tabs_panel_open,
right_panel_open: info.transferred_tab.right_panel_open,
is_right_panel_maximized: info.transferred_tab.is_right_panel_maximized,
for_drag_preview: for_drag,
tab_color: transferred_tab.color,
custom_title: transferred_tab.custom_title.clone(),
left_panel_open: transferred_tab.left_panel_open,
vertical_tabs_panel_open: transferred_tab.vertical_tabs_panel_open,
right_panel_open: transferred_tab.right_panel_open,
is_right_panel_maximized: transferred_tab.is_right_panel_maximized,
is_tab_drag_preview,
},
ctx,
);
if !for_drag {
if !is_tab_drag_preview {
view.focus(ctx);
}
view
},
);
let pane_group_id = info.transferred_tab.pane_group.id();
let transferred_view_ids =
ctx.transfer_view_tree_to_window(pane_group_id, info.source_window_id, new_window_id);
let pane_group_id = transferred_tab.pane_group.id();
ctx.transfer_view_tree_to_window(pane_group_id, source_window_id, new_window_id);
if let Some(new_workspace) = workspace_for_window(new_window_id, ctx) {
if let Some(new_workspace) = WorkspaceRegistry::as_ref(ctx).get(new_window_id, ctx) {
new_workspace.update(ctx, |workspace, ctx| {
workspace.adopt_transferred_pane_group(info.transferred_tab.pane_group.clone(), ctx);
workspace.adopt_transferred_pane_group(transferred_tab.pane_group.clone(), ctx);
});
} else {
log::warn!("Failed to find workspace in newly created window {new_window_id:?}");
}
(new_window_id, transferred_view_ids)
new_window_id
}
#[cfg(feature = "crash_reporting")]
@@ -1080,6 +994,34 @@ fn open_settings_page_in_new_window(section: &SettingsSection, ctx: &mut AppCont
});
}
/// Maps a `warp://settings` deeplink to the workspace action that opens it.
fn workspace_action_for_open_settings(args: &OpenSettingsArgs) -> WorkspaceAction {
match args {
OpenSettingsArgs::Default => WorkspaceAction::ShowSettings,
OpenSettingsArgs::Search { query } => WorkspaceAction::ShowSettingsPageWithSearch {
search_query: query.clone(),
section: None,
},
OpenSettingsArgs::Widget { page, widget_id } => WorkspaceAction::ScrollToSettingsWidget {
page: *page,
widget_id,
},
}
}
fn open_settings_in_new_window(args: &OpenSettingsArgs, ctx: &mut AppContext) {
let action = workspace_action_for_open_settings(args);
let root_handle = open_new_window_get_handles(None, ctx).1;
root_handle.update(ctx, |root_view, ctx| {
if let AuthOnboardingState::Terminal(workspace_view_handle) =
&root_view.auth_onboarding_state
{
let window_id = ctx.window_id();
ctx.dispatch_typed_action_for_view(window_id, workspace_view_handle.id(), &action);
}
});
}
/// MCP servers need to wait for initial load to complete, so we have this action in addition
/// to the general-purpose [`open_settings_page_in_new_window`].
fn open_mcp_settings_in_new_window(args: &OpenMCPSettingsArgs, ctx: &mut AppContext) {
@@ -1224,7 +1166,7 @@ fn open_new_with_shell(shell: &Option<AvailableShell>, ctx: &mut AppContext) {
/// Global action that performs a few steps:
/// 1. Open a new tab, or open a window if there is none.
/// 2. Set the terminal input buffer to a command that should open a subshell
/// 3. Set a flag that we should automatically bootstrap that subshell if its we can boostrap its
/// 3. Set a flag that we should automatically bootstrap that subshell if its we can bootstrap its
/// [`ShellType`].
fn open_new_tab_insert_subshell_command_and_bootstrap_if_supported(
arg: &SubshellCommandArg,
@@ -1595,6 +1537,8 @@ pub enum NewWorkspaceSource {
options: Box<NewTerminalOptions>,
initial_query: Option<String>,
},
/// Starts the workspace with the Cloud Agent setup tab.
AmbientAgent,
/// A tab is being transferred from another window via the transferable views framework.
/// The workspace will create a placeholder tab, which will be replaced by the transferred
/// PaneGroup after window creation.
@@ -1612,7 +1556,7 @@ pub enum NewWorkspaceSource {
/// Whether the right panel was maximized in the source tab
is_right_panel_maximized: bool,
/// Whether this transferred tab window is currently being used as a drag preview.
for_drag_preview: bool,
is_tab_drag_preview: bool,
},
}
@@ -1884,9 +1828,23 @@ impl RootView {
ctx.focus(onboarding_view);
}
// For users who bypass onboarding (already logged in, or onboarding flags not active),
// start autoupdate polling immediately. For new users in onboarding, this is a no-op;
// polling will be started once onboarding completes.
root_view.start_autoupdate_polling(ctx);
root_view
}
/// Starts the autoupdate polling loop, but only if we are already in the `Terminal` state
/// (i.e. onboarding has completed or was not shown). Safe to call unconditionally — it is
/// a no-op when still in a pre-terminal state.
fn start_autoupdate_polling(&self, ctx: &mut ViewContext<Self>) {
if matches!(self.auth_onboarding_state, AuthOnboardingState::Terminal(_)) {
AutoupdateState::handle(ctx).update(ctx, |state, ctx| state.start_polling(ctx));
}
}
/// Used for integration tests.
pub fn workspace_view(&self) -> Option<&ViewHandle<Workspace>> {
match &self.auth_onboarding_state {
@@ -1986,12 +1944,6 @@ impl RootView {
true
}
fn build_plan_yearly_price_cents(ctx: &AppContext) -> Option<i32> {
PricingInfoModel::as_ref(ctx)
.plan_pricing(&StripeSubscriptionPlan::Build)
.map(|p| p.yearly_plan_price_per_month_usd_cents)
}
fn create_agent_onboarding_view(
ctx: &mut ViewContext<Self>,
) -> ViewHandle<AgentOnboardingView> {
@@ -2001,17 +1953,13 @@ impl RootView {
let themes = onboarding_theme_picker_themes();
let onboarding_view = ctx.add_typed_action_view(move |ctx| {
let (mut models, default_model_id) =
build_onboarding_models(LLMPreferences::as_ref(ctx));
let default_model_id =
apply_free_tier_default_model_override(&mut models, default_model_id, ctx);
let (models, default_model_id) =
build_onboarding_models(LLMPreferences::as_ref(ctx), ctx);
let workspace_enforces_autonomy = UserWorkspaces::as_ref(ctx)
.ai_autonomy_settings()
.has_any_overrides();
let agent_price_cents = Self::build_plan_yearly_price_cents(ctx);
let auth_state = current_onboarding_auth_state(ctx);
AgentOnboardingView::new(
@@ -2021,36 +1969,18 @@ impl RootView {
default_model_id,
workspace_enforces_autonomy,
FeatureFlag::AgentView.is_enabled(),
is_free_user_no_ai_experiment_active(ctx),
agent_price_cents,
auth_state,
ctx,
)
});
// Subscribe to pricing updates so the badge stays current.
let onboarding_view_for_pricing = onboarding_view.clone();
ctx.subscribe_to_model(
&PricingInfoModel::handle(ctx),
move |_, _, event, ctx| match event {
PricingInfoModelEvent::PricingInfoUpdated => {
let cents = Self::build_plan_yearly_price_cents(ctx);
onboarding_view_for_pricing.update(ctx, |view, ctx| {
view.set_agent_price_cents(cents, ctx);
});
}
},
);
let onboarding_view_clone = onboarding_view.clone();
ctx.subscribe_to_model(
&LLMPreferences::handle(ctx),
move |_, llm_preferences, event, ctx| match event {
LLMPreferencesEvent::UpdatedAvailableLLMs => {
let (mut models, default_model_id) =
build_onboarding_models(llm_preferences.as_ref(ctx));
let default_model_id =
apply_free_tier_default_model_override(&mut models, default_model_id, ctx);
let (models, default_model_id) =
build_onboarding_models(llm_preferences.as_ref(ctx), ctx);
onboarding_view_clone.update(ctx, |onboarding_view, ctx| {
onboarding_view.set_onboarding_models(models, default_model_id, ctx);
})
@@ -2061,42 +1991,21 @@ impl RootView {
},
);
// Subscribe to workspace changes to update autonomy enforcement state and detect upgrades.
// TeamsChanged fires whenever the workspace/billing metadata poll returns, which is also
// when a free→paid upgrade would be reflected (customer_type changes).
// Subscribe to workspace changes to update autonomy enforcement state and auth/billing
// state (e.g. a free→paid upgrade reflected by the workspace/billing metadata poll).
let onboarding_view_for_workspaces = onboarding_view.clone();
ctx.subscribe_to_model(
&UserWorkspaces::handle(ctx),
move |_, user_workspaces, event, ctx| {
match event {
UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess => {
let workspace_enforces_autonomy = user_workspaces
.as_ref(ctx)
.ai_autonomy_settings()
.has_any_overrides();
onboarding_view_for_workspaces.update(ctx, |onboarding_view, ctx| {
onboarding_view
.set_workspace_enforces_autonomy(workspace_enforces_autonomy, ctx);
});
}
UserWorkspacesEvent::TeamsChanged => {
let new_locked = is_free_user_no_ai_experiment_active(ctx);
let was_locked = onboarding_view_for_workspaces
.as_ref(ctx)
.free_user_no_ai_experiment(ctx);
if was_locked && !new_locked {
// User upgraded — skip the intention slide.
onboarding_view_for_workspaces.update(ctx, |view, ctx| {
view.set_free_user_no_ai_experiment(false, ctx);
view.advance_to_agent_step(ctx);
});
} else {
onboarding_view_for_workspaces.update(ctx, |view, ctx| {
view.set_free_user_no_ai_experiment(new_locked, ctx);
});
}
}
_ => {}
if matches!(event, UserWorkspacesEvent::UpdateWorkspaceSettingsSuccess) {
let workspace_enforces_autonomy = user_workspaces
.as_ref(ctx)
.ai_autonomy_settings()
.has_any_overrides();
onboarding_view_for_workspaces.update(ctx, |onboarding_view, ctx| {
onboarding_view
.set_workspace_enforces_autonomy(workspace_enforces_autonomy, ctx);
});
}
let auth_state = current_onboarding_auth_state(ctx);
onboarding_view_for_workspaces.update(ctx, |onboarding_view, ctx| {
@@ -2192,12 +2101,14 @@ impl RootView {
};
let workspace = target.to_workspace(ctx);
// User opted out of login: apply locally (no cloud race).
// Skipping leaves the user without an account, so AI is disabled.
if let Some(selected_settings) = self.pending_post_auth_onboarding_settings.take() {
apply_onboarding_settings(&selected_settings, ctx);
apply_onboarding_settings(&selected_settings, false, ctx);
}
self.auth_onboarding_state = AuthOnboardingState::Terminal(workspace);
ctx.emit(RootViewEvent::AuthOnboardingStateChanged);
self.start_pending_tutorial(ctx);
self.start_autoupdate_polling(ctx);
self.focus(ctx);
ctx.notify();
}
@@ -2243,16 +2154,6 @@ impl RootView {
mark_hoa_onboarding_completed(ctx);
}
// Terminal-intent users should not see the conversation list
// auto-opened for discoverability.
if matches!(selected_settings, SelectedSettings::Terminal { .. }) {
AISettings::handle(ctx).update(ctx, |settings, ctx| {
report_if_error!(settings
.has_auto_opened_conversation_list
.set_value(true, ctx));
});
}
let is_logged_in = AuthStateProvider::as_ref(ctx).get().is_logged_in();
// If the user isn't logged in, only require login if the applied
// settings need an account (AI or Warp Drive enabled).
@@ -2272,30 +2173,36 @@ impl RootView {
.theme()
.name()
.unwrap_or_else(|| "Dark".to_string());
let (use_vertical_tabs, intention) = match selected_settings {
SelectedSettings::AgentDrivenDevelopment {
ui_customization, ..
} => (
ui_customization
.as_ref()
.map(|c| c.use_vertical_tabs)
.unwrap_or(true),
OnboardingIntention::AgentDrivenDevelopment,
),
SelectedSettings::Terminal {
ui_customization, ..
} => (
ui_customization
.as_ref()
.map(|c| c.use_vertical_tabs)
.unwrap_or(false),
OnboardingIntention::Terminal,
),
};
let (use_vertical_tabs, intention, uses_third_party_agents) =
match selected_settings {
SelectedSettings::AgentDrivenDevelopment {
ui_customization,
agent_settings,
..
} => (
ui_customization
.as_ref()
.map(|c| c.use_vertical_tabs)
.unwrap_or(true),
OnboardingIntention::AgentDrivenDevelopment,
agent_settings.disable_oz,
),
SelectedSettings::Terminal {
ui_customization, ..
} => (
ui_customization
.as_ref()
.map(|c| c.use_vertical_tabs)
.unwrap_or(false),
OnboardingIntention::Terminal,
false,
),
};
let login_slide_view = ctx.add_typed_action_view(|ctx| {
LoginSlideView::new(
ai_enabled,
uses_third_party_agents,
&theme_name,
use_vertical_tabs,
intention,
@@ -2319,7 +2226,7 @@ impl RootView {
return;
}
apply_onboarding_settings(selected_settings, ctx);
apply_onboarding_settings(selected_settings, is_logged_in, ctx);
if is_logged_in {
AuthManager::handle(ctx)
@@ -2332,6 +2239,7 @@ impl RootView {
self.auth_onboarding_state = AuthOnboardingState::Terminal(workspace);
ctx.emit(RootViewEvent::AuthOnboardingStateChanged);
self.start_pending_tutorial(ctx);
self.start_autoupdate_polling(ctx);
ctx.notify();
}
AgentOnboardingEvent::OnboardingSkipped => {
@@ -2353,6 +2261,7 @@ impl RootView {
let workspace = target.to_workspace(ctx);
self.auth_onboarding_state = AuthOnboardingState::Terminal(workspace);
ctx.emit(RootViewEvent::AuthOnboardingStateChanged);
self.start_autoupdate_polling(ctx);
ctx.notify();
}
AgentOnboardingEvent::UpgradeRequested => {
@@ -2416,6 +2325,8 @@ impl RootView {
let login_slide_view = ctx.add_typed_action_view(|ctx| {
LoginSlideView::new(
ai_enabled,
// Terminal intention is never the third-party-agent path.
false,
&theme_name,
use_vertical_tabs,
OnboardingIntention::Terminal,
@@ -2464,6 +2375,9 @@ impl RootView {
let login_slide_view = ctx.add_typed_action_view(|ctx| {
LoginSlideView::new(
ai_enabled,
// No agent setup choice has been made yet; default to the
// Warp Agent login screen rather than the third-party copy.
false,
&theme_name,
use_vertical_tabs,
// Existing-user login from the welcome slide happens before the user
@@ -2531,6 +2445,20 @@ impl RootView {
true
}
fn activate_tab_by_pane_group_id(
&mut self,
pane_group_id: &EntityId,
ctx: &mut ViewContext<Self>,
) -> bool {
ctx.windows().show_window_and_focus_app(ctx.window_id());
if let AuthOnboardingState::Terminal(workspace) = &self.auth_onboarding_state {
workspace.update(ctx, |view, ctx| {
view.activate_tab_by_pane_group_id(*pane_group_id, ctx);
});
}
true
}
fn handle_notification_click(
&mut self,
pane_view_locator: &PaneViewLocator,
@@ -2551,7 +2479,10 @@ impl RootView {
});
}
Err(error) => {
log::error!("Unable to parse AuthResult from url: {error}");
safe_error!(
safe: ("Unable to parse AuthResult from url"),
full: ("Unable to parse AuthResult from url: {error}")
);
self.auth_view.update(ctx, |view, ctx| {
view.last_login_failure_reason =
Some(LoginFailureReason::InvalidRedirectUrl { was_pasted: false });
@@ -2906,6 +2837,22 @@ impl RootView {
true
}
pub fn open_settings_in_existing_window(
&mut self,
args: &OpenSettingsArgs,
ctx: &mut ViewContext<Self>,
) -> bool {
let window_id = ctx.window_id();
if let AuthOnboardingState::Terminal(handle) = &self.auth_onboarding_state {
let action = workspace_action_for_open_settings(args);
ctx.dispatch_typed_action_for_view(window_id, handle.id(), &action);
ctx.windows().show_window_and_focus_app(window_id);
} else {
log::error!("Auth not complete before trying to open settings");
}
true
}
/// Opens the MCP servers settings page in an existing window, optionally triggering auto-install.
/// Waits for `initial_load_complete` before opening so gallery data is available for autoinstall.
pub fn open_mcp_settings_in_existing_window(
@@ -3036,6 +2983,7 @@ impl RootView {
.complete_auth_and_create_workspace(ctx);
}
self.start_autoupdate_polling(ctx);
self.focus(ctx);
}
AuthManagerEvent::AuthFailed(err) => match err {
@@ -3081,12 +3029,14 @@ impl RootView {
if let Some(selected_settings) =
self.pending_post_auth_onboarding_settings.take()
{
apply_onboarding_settings(&selected_settings, ctx);
// Skipped login → no account → AI disabled.
apply_onboarding_settings(&selected_settings, false, ctx);
}
self.auth_onboarding_state = AuthOnboardingState::Terminal(workspace);
ctx.emit(RootViewEvent::AuthOnboardingStateChanged);
self.start_pending_tutorial(ctx);
}
self.start_autoupdate_polling(ctx);
self.focus(ctx);
}
AuthManagerEvent::LoginOverrideDetected(interrupted_auth_payload) => {
@@ -3230,10 +3180,10 @@ impl RootView {
key_code: &galaxyui::platform::keyboard::KeyCode,
ctx: &mut ViewContext<Self>,
) -> bool {
use crate::settings::AISettings;
use galaxyui::event::KeyState;
use voice_input::{VoiceInput, VoiceInputState, VoiceInputToggledFrom};
use crate::settings::AISettings;
// Check that the released key matches the configured voice input toggle key.
let ai_settings = AISettings::as_ref(ctx);
if let Some(configured_key_code) = ai_settings.voice_input_toggle_key.value().to_key_code()
@@ -3284,7 +3234,8 @@ impl RootView {
let Some(selected_settings) = self.pending_post_auth_onboarding_settings.take() else {
return;
};
apply_onboarding_settings(&selected_settings, ctx);
// Reached only after a successful login, so the user has an account.
apply_onboarding_settings(&selected_settings, true, ctx);
}
/// If onboarding stored a pending tutorial (because login was required first),
@@ -3302,12 +3253,10 @@ impl RootView {
&& FeatureFlag::TabConfigs.is_enabled()
{
let intention = tutorial.intention();
// Terminal-intent users skip the session config modal.
if matches!(intention, OnboardingIntention::AgentDrivenDevelopment) {
workspace.update(ctx, |view, ctx| {
view.set_pending_onboarding_intention(intention);
view.open_vertical_tabs_panel_if_enabled(ctx);
view.show_session_config_modal(ctx);
view.start_agent_onboarding_tutorial(tutorial, ctx);
});
} else {
workspace.update(ctx, |view, ctx| {