first pass of merging in warp (doesn't build)
This commit is contained in:
+244
-295
@@ -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| {
|
||||
|
||||
Reference in New Issue
Block a user