2944 lines
118 KiB
Rust
2944 lines
118 KiB
Rust
#![allow(clippy::doc_lazy_continuation)]
|
|
#![allow(unused)]
|
|
#![allow(unexpected_cfgs)]
|
|
|
|
extern crate galaxy_cli as warp_cli;
|
|
extern crate galaxy_completer as warp_completer;
|
|
extern crate galaxy_editor as warp_editor;
|
|
extern crate galaxy_graphql as warp_graphql;
|
|
extern crate galaxy_isolation_platform as warp_isolation_platform;
|
|
extern crate galaxy_logging as warp_logging;
|
|
extern crate galaxy_managed_secrets as warp_managed_secrets;
|
|
extern crate galaxy_ripgrep as warp_ripgrep;
|
|
extern crate galaxy_server_client as warp_server_client;
|
|
extern crate galaxy_terminal as warp_terminal;
|
|
extern crate galaxy_util as warp_util;
|
|
|
|
#[macro_use]
|
|
extern crate galaxy_core;
|
|
#[macro_use]
|
|
extern crate lazy_static;
|
|
#[macro_use]
|
|
extern crate serde;
|
|
#[macro_use]
|
|
extern crate galaxyui as warpui;
|
|
|
|
mod ai;
|
|
mod alloc;
|
|
mod antivirus;
|
|
#[cfg(target_os = "macos")]
|
|
mod app_menus;
|
|
mod app_services;
|
|
mod app_state;
|
|
mod auth;
|
|
mod autoupdate;
|
|
mod banner;
|
|
#[cfg(feature = "bedrock_smoke_test")]
|
|
mod bedrock_smoke_test;
|
|
mod billing;
|
|
mod build_info;
|
|
mod changelog_model;
|
|
mod chip_configurator;
|
|
mod cloud_object;
|
|
mod code;
|
|
mod code_review;
|
|
mod coding_entrypoints;
|
|
mod coding_panel_enablement_state;
|
|
mod command_palette;
|
|
mod completer;
|
|
#[allow(dead_code)]
|
|
mod context_chips;
|
|
#[cfg(enable_crash_recovery)]
|
|
mod crash_recovery;
|
|
mod debug_dump;
|
|
mod default_terminal;
|
|
mod download_method;
|
|
mod drive;
|
|
#[cfg(windows)]
|
|
mod dynamic_libraries;
|
|
mod env_vars;
|
|
mod experiments;
|
|
mod external_secrets;
|
|
#[cfg(target_family = "wasm")]
|
|
mod font_fallback;
|
|
pub mod galaxy_managed_paths_watcher;
|
|
pub use galaxy_managed_paths_watcher as warp_managed_paths_watcher;
|
|
mod global_resource_handles;
|
|
mod gpu_state;
|
|
mod input_classifier;
|
|
mod interval_timer;
|
|
mod linear;
|
|
#[cfg(feature = "local_fs")]
|
|
mod local_control;
|
|
mod local_object_repository;
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
mod login_item;
|
|
mod menu;
|
|
mod modal;
|
|
mod network;
|
|
mod notebooks;
|
|
mod notification;
|
|
mod palette;
|
|
mod persistence;
|
|
mod platform;
|
|
#[cfg(feature = "plugin_host")]
|
|
mod plugin;
|
|
mod prefix;
|
|
#[cfg(target_os = "macos")]
|
|
mod preview_config_migration;
|
|
mod pricing;
|
|
mod profiling;
|
|
mod projects;
|
|
mod prompt;
|
|
mod quit_warning;
|
|
mod referral_theme_status;
|
|
#[allow(dead_code)]
|
|
mod remote_server;
|
|
mod resource_limits;
|
|
mod reward_view;
|
|
mod safe_triangle;
|
|
mod search_bar;
|
|
mod server;
|
|
mod session_management;
|
|
mod shell_indicator;
|
|
mod suggestions;
|
|
mod system;
|
|
mod tab;
|
|
#[cfg(test)]
|
|
mod test_util;
|
|
mod throttle;
|
|
mod tips;
|
|
mod tracing;
|
|
#[cfg(feature = "tui")]
|
|
mod tui;
|
|
#[cfg(feature = "tui")]
|
|
pub mod tui_export;
|
|
mod ui_components;
|
|
mod undo_close;
|
|
mod uri;
|
|
mod user_config;
|
|
pub mod util;
|
|
mod view_components;
|
|
mod vim_registers;
|
|
mod voice;
|
|
mod voltron;
|
|
#[cfg(target_family = "wasm")]
|
|
mod wasm_nux_dialog;
|
|
mod window_settings;
|
|
mod word_block_editor;
|
|
mod workspaces;
|
|
|
|
// PLEASE DO NOT ADD MORE PUBLIC MODULES!
|
|
//
|
|
// Any modules which we make public outside of the `warp` crate lose dead code
|
|
// checking support, as the compiler cannot make any assumptions about whether
|
|
// or not the function/type is used by another crate that pulls in this one as
|
|
// a dependency.
|
|
//
|
|
// If you feel the need to export a module so that a type or function within it
|
|
// can be used by an integration test, you should define a new assertion function
|
|
// in the galaxy::integration_testing::assertions module (or a sub-module). These
|
|
// functions will allow us to keep types internal to this crate and expose a
|
|
// simpler API for integration tests to consume.
|
|
pub mod ai_assistant;
|
|
pub mod appearance;
|
|
pub mod channel;
|
|
pub mod editor;
|
|
pub mod features;
|
|
pub mod input_suggestions;
|
|
#[cfg(feature = "integration_tests")]
|
|
pub mod integration_testing;
|
|
pub mod keyboard;
|
|
pub mod launch_configs;
|
|
pub mod pane_group;
|
|
pub mod resource_center;
|
|
pub mod root_view;
|
|
pub mod search;
|
|
pub mod settings;
|
|
pub mod settings_view;
|
|
pub mod tab_configs;
|
|
pub mod terminal;
|
|
pub mod themes;
|
|
use ::ai::index::full_source_code_embedding::manager::{
|
|
CodebaseIndexManager, CodebaseIndexManagerConfig,
|
|
};
|
|
#[cfg(feature = "local_fs")]
|
|
use ::ai::index::full_source_code_embedding::SnapshotStorage;
|
|
use ::ai::index::full_source_code_embedding::SyncTask;
|
|
use ::ai::index::DEFAULT_SYNC_REQUESTS_PER_MIN;
|
|
use ::ai::project_context::model::ProjectContextModel;
|
|
pub use ai::agent::todos::AIAgentTodoList;
|
|
pub use ai::agent::{AIAgentActionResultType, FileEdit, TodoOperation};
|
|
use ai::agent_conversations_model::AgentConversationsModel;
|
|
use ai::agent_management::AgentNotificationsModel;
|
|
use ai::ambient_agents::scheduled::ScheduledAgentManager;
|
|
use ai::blocklist::{BlocklistAIHistoryModel, BlocklistAIPermissions};
|
|
use ai::execution_profiles::editor::ExecutionProfileEditorManager;
|
|
use ai::execution_profiles::profiles::AIExecutionProfilesModel;
|
|
use ai::metadata_project_rules::read_project_rule_contents;
|
|
use ai::persisted_workspace::PersistedWorkspace;
|
|
use auth::auth_manager::AuthManager;
|
|
use auth::auth_state::{AuthState, AuthStateProvider};
|
|
use code::editor_management::CodeManager;
|
|
use code::opened_files::OpenedFilesModel;
|
|
use code_review::git_repo_model::GitRepoModels;
|
|
use code_review::GlobalCodeReviewModel;
|
|
use galaxy_cli::agent::AgentCommand;
|
|
use galaxy_cli::{CliCommand, GlobalOptions};
|
|
use quit_warning::UnsavedStateSummary;
|
|
#[cfg(feature = "local_fs")]
|
|
use repo_metadata::{
|
|
repositories::DetectedRepositories, watcher::DirectoryWatcher, RepoMetadataModel,
|
|
};
|
|
use server::network_log_pane_manager::NetworkLogPaneManager;
|
|
use server::voice_transcriber::ServerVoiceTranscriber;
|
|
#[cfg(feature = "local_fs")]
|
|
use settings::import::model::ImportedConfigModel;
|
|
use settings_view::pane_manager::SettingsPaneManager;
|
|
use terminal::general_settings::GeneralSettings;
|
|
use terminal::keys_settings::KeysSettings;
|
|
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
|
|
use terminal::local_shell::LocalShellState;
|
|
pub use util::bindings::cmd_or_ctrl_shift;
|
|
use voice::transcriber::VoiceTranscriber;
|
|
#[cfg(feature = "local_fs")]
|
|
use watcher::HomeDirectoryWatcher;
|
|
|
|
use crate::ai::active_agent_views_model::ActiveAgentViewsModel;
|
|
#[cfg(not(target_family = "wasm"))]
|
|
use crate::ai::aws_credentials::AwsCredentialRefresher as _;
|
|
#[cfg(not(target_family = "wasm"))]
|
|
use crate::ai::geap_credentials::GeapCredentialRefresher as _;
|
|
use crate::ai::mcp::{FileBasedMCPManager, FileMCPWatcher};
|
|
use crate::galaxy_managed_paths_watcher::{
|
|
ensure_galaxy_watch_roots_exist, GalaxyManagedPathsWatcher,
|
|
};
|
|
use crate::uri::web_intent_parser::maybe_rewrite_web_url_to_intent;
|
|
use crate::user_config::GalaxyConfig;
|
|
use crate::view_components::DismissibleToast;
|
|
pub mod workflows;
|
|
pub mod workspace;
|
|
|
|
use std::borrow::Cow;
|
|
use std::collections::HashSet;
|
|
use std::ops::Deref;
|
|
#[cfg(feature = "local_fs")]
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
|
|
use ::settings::{Setting, ToggleableSetting};
|
|
#[cfg(feature = "local_tty")]
|
|
use anyhow::Context;
|
|
use anyhow::{anyhow, Result};
|
|
use appearance::{Appearance, AppearanceManager};
|
|
use channel::ChannelState;
|
|
pub use galaxy_core::errors::{report_error, report_if_error};
|
|
use galaxy_core::execution_mode::{AppExecutionMode, ExecutionMode};
|
|
// Re-export the debounce function to simplify imports.
|
|
pub use galaxy_core::r#async::debounce;
|
|
// Re-export the send_telemetry_from_ctx macro at the crate root level
|
|
pub use galaxy_core::send_telemetry_from_app_ctx;
|
|
pub use galaxy_core::send_telemetry_from_ctx;
|
|
// Re-export the safe logging macros at the crate root level for backwards compatibility
|
|
pub use galaxy_core::{safe_debug, safe_error, safe_info, safe_warn};
|
|
#[cfg(feature = "local_fs")]
|
|
use galaxy_files::FileModel;
|
|
use galaxy_logging::LogDestination;
|
|
use galaxy_managed_secrets::ManagedSecretManager;
|
|
use galaxy_server_client::iap::{IapManager, IapManagerEvent, IapState};
|
|
use galaxy_server_client::network_logging::NetworkLogModel;
|
|
use galaxyui::integration::TestDriver;
|
|
use galaxyui::modals::{AlertDialogWithCallbacks, AppModalCallback};
|
|
use galaxyui::platform::app::{ApproveTerminateResult, TerminationRequestSource};
|
|
use galaxyui::platform::TerminationMode;
|
|
use galaxyui::windowing::state::ApplicationStage;
|
|
use galaxyui::{App, AppContext, Event, SingletonEntity, WindowId};
|
|
use interval_timer::IntervalTimer;
|
|
use itertools::Itertools;
|
|
#[cfg(feature = "integration_tests")]
|
|
pub use persistence::testing as sqlite_testing;
|
|
#[cfg(feature = "plugin_host")]
|
|
pub use plugin::{run_plugin_host, PLUGIN_HOST_FLAG};
|
|
use referral_theme_status::ReferralThemeStatus;
|
|
use server::server_api::ServerApiProvider;
|
|
use settings::{ExtraMetaKeys, PrivacySettings};
|
|
#[cfg(feature = "local_fs")]
|
|
use shellexpand::tilde;
|
|
use terminal::input;
|
|
use terminal::session_settings::SessionSettings;
|
|
use url::Url;
|
|
use window_settings::WindowSettings;
|
|
use workflows::manager::WorkflowManager;
|
|
use workspace::sync_inputs::SyncedInputState;
|
|
|
|
use self::features::FeatureFlag;
|
|
use crate::ai::agent::conversation::AIConversationId;
|
|
use crate::ai::ambient_agents::github_auth_notifier::GitHubAuthNotifier;
|
|
use crate::ai::connected_self_hosted_workers::ConnectedSelfHostedWorkersModel;
|
|
use crate::ai::document::ai_document_model::AIDocumentModel;
|
|
use crate::ai::facts::manager::AIFactManager;
|
|
use crate::ai::harness_availability::HarnessAvailabilityModel;
|
|
use crate::ai::llms::LLMPreferences;
|
|
use crate::ai::mcp::{MCPGalleryManager, TemplatableMCPServerManager};
|
|
use crate::ai::outline::RepoOutlines;
|
|
use crate::ai::restored_conversations::RestoredAgentConversations;
|
|
use crate::ai::skills::SkillManager;
|
|
use crate::ai::AIRequestUsageModel;
|
|
use crate::antivirus::AntivirusInfo;
|
|
use crate::app_state::AppState;
|
|
use crate::autoupdate::{AutoupdateState, RelaunchModel};
|
|
use crate::changelog_model::ChangelogModel;
|
|
use crate::cloud_object::model::actions::{ObjectAction, ObjectActions};
|
|
use crate::cloud_object::model::persistence::CloudModel;
|
|
use crate::cloud_object::model::view::CloudViewModel;
|
|
use crate::code::global_buffer_model::GlobalBufferModel;
|
|
#[cfg(feature = "local_fs")]
|
|
use crate::code::language_server_shutdown_manager::LanguageServerShutdownManager;
|
|
use crate::context_chips::prompt::Prompt;
|
|
use crate::default_terminal::DefaultTerminal;
|
|
use crate::drive::export::ExportManager;
|
|
use crate::drive::CloudObjectTypeAndId;
|
|
use crate::env_vars::manager::EnvVarCollectionManager;
|
|
use crate::experiments::ImprovedPaletteSearch;
|
|
pub use crate::global_resource_handles::{GlobalResourceHandles, GlobalResourceHandlesProvider};
|
|
use crate::gpu_state::GPUState;
|
|
use crate::local_object_repository::LocalObjectRepository;
|
|
use crate::network::NetworkStatus;
|
|
use crate::notebooks::editor::keys::NotebookKeybindings;
|
|
use crate::notebooks::manager::NotebookManager;
|
|
use crate::notebooks::CloudNotebook;
|
|
use crate::notification::NotificationContext;
|
|
use crate::palette::PaletteMode;
|
|
use crate::persistence::model::AgentConversationData;
|
|
use crate::persistence::PersistenceWriter;
|
|
use crate::projects::ProjectManagementModel;
|
|
use crate::root_view::{
|
|
quake_mode_window_id, quake_mode_window_is_open, OpenFromRestoredArg, OpenPath,
|
|
};
|
|
use crate::server::cloud_objects::listener::Listener;
|
|
use crate::server::cloud_objects::update_manager::UpdateManager;
|
|
use crate::server::experiments::ServerExperiments;
|
|
use crate::server::sync_queue::{QueueItem, SyncQueue};
|
|
pub use crate::server::telemetry::{
|
|
AgentModeEntrypoint, AgentModeEntrypointSelectionType, AppStartupInfo, CloseTarget,
|
|
PaletteSource, TelemetryEvent,
|
|
};
|
|
use crate::session_management::{RunningSessionSummary, SessionNavigationData};
|
|
use crate::settings::cloud_preferences_syncer::initialize_cloud_preferences_syncer;
|
|
use crate::settings::manager::SettingsManager;
|
|
use crate::settings::{AISettings, AccessibilitySettings, ScrollSettings, SelectionSettings};
|
|
use crate::settings_view::keybindings::KeybindingChangedNotifier;
|
|
use crate::settings_view::DisplayCount;
|
|
use crate::suggestions::ignored_suggestions_model::IgnoredSuggestionsModel;
|
|
use crate::system::SystemStats;
|
|
use crate::terminal::cli_agent_sessions::CLIAgentSessionsModel;
|
|
use crate::terminal::keys::TerminalKeybindings;
|
|
use crate::terminal::resizable_data::ResizableData;
|
|
use crate::terminal::view::inline_banner::ByoLlmAuthBannerSessionState;
|
|
use crate::terminal::{AudibleBell, CustomSecretRegexUpdater, History};
|
|
#[cfg(feature = "tui")]
|
|
pub use crate::tui::{TuiLoginModel, TuiLoginPhase};
|
|
use crate::undo_close::UndoCloseStack;
|
|
use crate::util::bindings::is_binding_cross_platform;
|
|
use crate::vim_registers::VimRegisters;
|
|
use crate::workflows::aliases::WorkflowAliases;
|
|
use crate::workflows::local_workflows::LocalWorkflows;
|
|
use crate::workspace::{
|
|
ActiveSession, OneTimeModalModel, PaneViewLocator, ToastStack, Workspace, WorkspaceAction,
|
|
};
|
|
use crate::workspaces::team_tester::TeamTesterStatus;
|
|
use crate::workspaces::update_manager::TeamUpdateManager;
|
|
use crate::workspaces::user_profiles::UserProfiles;
|
|
use crate::workspaces::user_workspaces::{UserWorkspaces, UserWorkspacesEvent};
|
|
|
|
/// Our embedded application assets.
|
|
pub static ASSETS: warp_assets::Assets = warp_assets::Assets;
|
|
|
|
fn determine_agent_source(
|
|
launch_mode: &LaunchMode,
|
|
) -> Option<crate::ai::ambient_agents::AgentSource> {
|
|
match launch_mode {
|
|
LaunchMode::CommandLine { .. } => {
|
|
if std::env::var("GITHUB_ACTIONS").ok().as_deref() == Some("true") {
|
|
Some(crate::ai::ambient_agents::AgentSource::GitHubAction)
|
|
} else {
|
|
Some(crate::ai::ambient_agents::AgentSource::Cli)
|
|
}
|
|
}
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. } => {
|
|
Some(crate::ai::ambient_agents::AgentSource::CloudMode)
|
|
}
|
|
// RemoteServerProxy and RemoteServerDaemon are headless server
|
|
// processes that don't use the agent subsystem.
|
|
// TODO: the TUI front-end has no agent harness wired up yet; give it an
|
|
// appropriate `AgentSource` once that lands.
|
|
LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
fn daemon_codebase_index_snapshot_storage(launch_mode: &LaunchMode) -> Option<SnapshotStorage> {
|
|
match launch_mode {
|
|
LaunchMode::RemoteServerDaemon { identity_key } => {
|
|
let data_dir = remote_server::setup::remote_server_daemon_data_dir(identity_key);
|
|
let snapshot_dir = PathBuf::from(tilde(&data_dir).into_owned())
|
|
.join("cache")
|
|
.join("codebase_index_snapshots");
|
|
SnapshotStorage::from_dir(snapshot_dir)
|
|
}
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::CommandLine { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::Tui { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Launch mode for how to start up Warp.
|
|
#[allow(clippy::large_enum_variant)]
|
|
pub(crate) enum LaunchMode {
|
|
/// Run the regular GUI application.
|
|
App {
|
|
args: galaxy_cli::AppArgs,
|
|
/// API key for server authentication, if provided via `--api-key` or `GALAXY_API_KEY`.
|
|
/// Only used on dogfood channels.
|
|
api_key: Option<String>,
|
|
},
|
|
|
|
/// Run the Warp command-line SDK.
|
|
CommandLine {
|
|
command: galaxy_cli::CliCommand,
|
|
global_options: GlobalOptions,
|
|
debug: bool,
|
|
/// Whether this CLI invocation is running in a sandboxed environment.
|
|
is_sandboxed: bool,
|
|
/// Override for computer use permission from CLI flags. If None, uses default behavior.
|
|
computer_use_override: Option<bool>,
|
|
},
|
|
/// Run a test - this may be an integration test or an eval.
|
|
Test {
|
|
driver: Box<Option<TestDriver>>,
|
|
is_integration_test: bool,
|
|
},
|
|
|
|
/// Remote server proxy — bridges SSH stdio to the daemon's Unix socket.
|
|
/// This is a short-lived process that runs for the lifetime of an SSH session.
|
|
#[cfg_attr(target_family = "wasm", allow(dead_code))]
|
|
RemoteServerProxy,
|
|
|
|
/// Remote server daemon — long-lived headless process serving remote
|
|
/// connections via a Unix domain socket.
|
|
#[cfg_attr(not(unix), allow(dead_code))]
|
|
RemoteServerDaemon {
|
|
/// Stable identity key used to partition the daemon's socket/PID
|
|
/// directory on the remote host.
|
|
identity_key: String,
|
|
},
|
|
|
|
/// Run the headless TUI front-end (the `warp-tui` binary in the `warp_tui`
|
|
/// crate). Boots the real headless app so auth/agent state can be reused,
|
|
/// then renders an editor-backed input UI to the terminal (via `mount`)
|
|
/// instead of opening a GUI window.
|
|
#[cfg_attr(not(feature = "tui"), allow(dead_code))]
|
|
Tui {
|
|
/// Builds the root TUI view and starts the TUI driver. Runs after
|
|
/// `initialize_app`; supplied by [`run_tui`]. Carried in the variant
|
|
/// (rather than as a `run_internal` parameter) so it stays scoped to
|
|
/// this mode.
|
|
mount: TuiMountFn,
|
|
},
|
|
}
|
|
|
|
impl LaunchMode {
|
|
fn args(&self) -> Cow<'_, galaxy_cli::AppArgs> {
|
|
match self {
|
|
LaunchMode::App { args, .. } => Cow::Borrowed(args),
|
|
LaunchMode::CommandLine { .. }
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => Cow::Owned(galaxy_cli::AppArgs::default()),
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if this process is running an integration test.
|
|
fn is_integration_test(&self) -> bool {
|
|
match self {
|
|
LaunchMode::Test {
|
|
is_integration_test,
|
|
..
|
|
} => *is_integration_test,
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::CommandLine { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => false,
|
|
}
|
|
}
|
|
|
|
fn take_test_driver(&mut self) -> Option<TestDriver> {
|
|
match self {
|
|
LaunchMode::Test { driver, .. } => driver.take(),
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::CommandLine { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Add an URL to open. Only supported for [`LaunchMode::App`]
|
|
#[allow(dead_code)]
|
|
fn add_url(&mut self, url: Url) {
|
|
if let LaunchMode::App { ref mut args, .. } = self {
|
|
args.urls.push(url);
|
|
}
|
|
}
|
|
|
|
fn execution_mode(&self) -> ExecutionMode {
|
|
match self {
|
|
LaunchMode::App { .. } => ExecutionMode::App,
|
|
LaunchMode::CommandLine { .. } => ExecutionMode::Sdk,
|
|
LaunchMode::Test { .. } => ExecutionMode::App,
|
|
// The TUI front-end is an app-style client, not the SDK.
|
|
LaunchMode::Tui { .. } => ExecutionMode::App,
|
|
// RemoteServerProxy is a thin byte bridge; Sdk is the closest match.
|
|
LaunchMode::RemoteServerProxy => ExecutionMode::Sdk,
|
|
// RemoteServerDaemon gets its own mode for distinct Sentry tagging.
|
|
LaunchMode::RemoteServerDaemon { .. } => ExecutionMode::RemoteServerDaemon,
|
|
}
|
|
}
|
|
|
|
fn is_sandboxed(&self) -> bool {
|
|
match self {
|
|
LaunchMode::CommandLine { is_sandboxed, .. } => *is_sandboxed,
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if Warp should run headlessly, without a visible UI.
|
|
fn is_headless(&self) -> bool {
|
|
match self {
|
|
LaunchMode::CommandLine { command, .. } => match command {
|
|
CliCommand::Agent(AgentCommand::Run(args)) => !args.gui,
|
|
_ => true,
|
|
},
|
|
LaunchMode::RemoteServerProxy | LaunchMode::RemoteServerDaemon { .. } => true,
|
|
// The TUI front-end renders to the terminal, with no GUI window.
|
|
LaunchMode::Tui { .. } => true,
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. } => false,
|
|
}
|
|
}
|
|
|
|
/// Returns `true` if this process can build and sync codebase indices.
|
|
fn supports_indexing(&self) -> bool {
|
|
match self {
|
|
LaunchMode::CommandLine { command, .. } => {
|
|
matches!(command, CliCommand::Agent(AgentCommand::Run { .. }))
|
|
}
|
|
LaunchMode::RemoteServerDaemon { .. } => {
|
|
FeatureFlag::RemoteCodebaseIndexing.is_enabled()
|
|
}
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. } => true,
|
|
LaunchMode::RemoteServerProxy => false,
|
|
// TODO: no agent harness is wired up for the TUI front-end yet;
|
|
// enable indexing once it is.
|
|
LaunchMode::Tui { .. } => false,
|
|
}
|
|
}
|
|
|
|
/// Whether or not to start a crash recovery process (on platforms that support it).
|
|
#[cfg(enable_crash_recovery)]
|
|
pub(crate) fn crash_recovery_enabled(&self) -> bool {
|
|
match self {
|
|
LaunchMode::App { .. } => true,
|
|
LaunchMode::CommandLine { .. }
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::Tui { .. } => false,
|
|
}
|
|
}
|
|
|
|
/// Whether profiling and tracing should be initialized.
|
|
pub(crate) fn needs_profiling(&self) -> bool {
|
|
match self {
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::CommandLine { .. }
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::RemoteServerDaemon { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::Tui { .. } => true,
|
|
}
|
|
}
|
|
|
|
/// Log destination for this mode.
|
|
fn log_destination(&self) -> Option<LogDestination> {
|
|
match self {
|
|
LaunchMode::CommandLine { debug, .. } => {
|
|
if *debug {
|
|
Some(LogDestination::Stderr)
|
|
} else {
|
|
Some(LogDestination::File)
|
|
}
|
|
}
|
|
// Proxy must log to stderr because stdout is the protocol channel.
|
|
LaunchMode::RemoteServerProxy => Some(LogDestination::Stderr),
|
|
LaunchMode::RemoteServerDaemon { .. } => Some(LogDestination::File),
|
|
// A TUI owns the terminal, so logs go to a file; stdout/stderr would
|
|
// corrupt the rendered output and the device-code prompt.
|
|
LaunchMode::Tui { .. } => Some(LogDestination::File),
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. } => None,
|
|
}
|
|
}
|
|
|
|
fn as_str_for_tracing(&self) -> &'static str {
|
|
match self {
|
|
LaunchMode::App { .. } => "app",
|
|
LaunchMode::CommandLine { command, .. } => command.as_str_for_tracing(),
|
|
LaunchMode::Test { .. } => "test",
|
|
LaunchMode::RemoteServerDaemon { .. } => "remote_server_daemon",
|
|
LaunchMode::RemoteServerProxy => "remote_server_proxy",
|
|
LaunchMode::Tui { .. } => "tui",
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
pub(crate) fn new_for_unit_test() -> Self {
|
|
LaunchMode::Test {
|
|
driver: Box::new(None),
|
|
is_integration_test: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// If the given event is a key down event containing alt modifiers, and those
|
|
/// alt modifiers should be treated as meta keys, then remove the alts and
|
|
/// prefix the keys with an escape. See WAR-472.
|
|
fn apply_extra_meta_keys(event: &mut Event, extra_metas: ExtraMetaKeys) {
|
|
if let Event::KeyDown {
|
|
keystroke, details, ..
|
|
} = event
|
|
{
|
|
let left_as_meta = extra_metas.left_alt && details.left_alt;
|
|
let right_as_meta = extra_metas.right_alt && details.right_alt;
|
|
if left_as_meta || right_as_meta {
|
|
let side = match (left_as_meta, right_as_meta) {
|
|
(true, true) => "left+right alt",
|
|
(true, false) => "left alt",
|
|
(false, true) => "right alt",
|
|
(false, false) => unreachable!(),
|
|
};
|
|
log::info!("Treating {side} as meta");
|
|
keystroke.alt = false;
|
|
keystroke.meta = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn apply_scroll_multiplier(event: &mut Event, app: &AppContext) {
|
|
if let Event::ScrollWheel { delta, precise, .. } = event {
|
|
if !*precise {
|
|
let scroll_multiplier = *ScrollSettings::as_ref(app).mouse_scroll_multiplier.value();
|
|
*delta *= scroll_multiplier;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runs the shared Galaxy executable as the app or as one of its command-line modes.
|
|
///
|
|
/// The bundled Galaxy Control wrapper injects `--galaxyctrl`, which is dispatched
|
|
/// before the normal Galaxy command-line parser.
|
|
pub fn run() -> Result<()> {
|
|
// Perform any necessary platform-specific initialization.
|
|
platform::init();
|
|
|
|
// Ensure feature flags are initialized before parsing command-line arguments.
|
|
features::init_feature_flags();
|
|
if let Some(args) = galaxy_cli::local_control::ControlArgs::from_control_mode_env() {
|
|
#[cfg(windows)]
|
|
warp_util::windows::attach_to_parent_console();
|
|
galaxy_cli::local_control::run_and_exit(args);
|
|
}
|
|
|
|
run_app_or_cli()
|
|
}
|
|
|
|
/// Runs normal app and command-line modes after the telemetry-neutral Galaxy
|
|
/// Control dispatch has had an opportunity to exit.
|
|
#[::tracing::instrument(skip_all)]
|
|
fn run_app_or_cli() -> Result<()> {
|
|
// Parse command-line arguments.
|
|
let args = galaxy_cli::Args::from_env();
|
|
|
|
// Server URL overrides are only honored on internal dev channels. Release channels silently
|
|
// ignore `--server-root-url` / `--ws-server-url` / `--session-sharing-server-url` (and their
|
|
// `WARP_*` env-var equivalents) so shipped builds can't be redirected away from their
|
|
// baked-in server URLs. See `Channel::allows_server_url_overrides`.
|
|
if ChannelState::channel().allows_server_url_overrides() {
|
|
if let Some(url) = args.server_root_url() {
|
|
if let Err(e) = ChannelState::override_server_root_url(url.to_owned()) {
|
|
eprintln!("Error: Invalid server root URL: {e:#}");
|
|
}
|
|
}
|
|
|
|
if let Some(url) = args.ws_server_url() {
|
|
if let Err(e) = ChannelState::override_ws_server_url(url.to_owned()) {
|
|
eprintln!("Error: Invalid websocket server URL: {e:#}");
|
|
}
|
|
}
|
|
|
|
if let Some(url) = args.session_sharing_server_url() {
|
|
if let Err(e) = ChannelState::override_session_sharing_server_url(url.to_owned()) {
|
|
eprintln!("Error: Invalid session sharing server URL: {e:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(command) = args.command() {
|
|
#[cfg(windows)]
|
|
if command.prints_to_stdout() {
|
|
// We attach a console to ensure that all standard output gets printed correctly.
|
|
galaxy_util::windows::attach_to_parent_console();
|
|
}
|
|
match command {
|
|
galaxy_cli::Command::Worker(worker) => return run_worker_command(worker),
|
|
galaxy_cli::Command::Completions { shell } => {
|
|
return galaxy_cli::completions::generate_to_stdout(*shell);
|
|
}
|
|
galaxy_cli::Command::CommandLine(cmd) => {
|
|
let (is_sandboxed, computer_use_override) = match cmd.as_ref() {
|
|
galaxy_cli::CliCommand::Agent(galaxy_cli::agent::AgentCommand::Run(
|
|
run_args,
|
|
)) => (
|
|
run_args.sandboxed,
|
|
run_args.computer_use.computer_use_override(),
|
|
),
|
|
_ => (false, None),
|
|
};
|
|
|
|
return run_internal(LaunchMode::CommandLine {
|
|
command: cmd.as_ref().clone(),
|
|
global_options: GlobalOptions {
|
|
output_format: args.output_format(),
|
|
api_key: args.api_key().cloned(),
|
|
},
|
|
debug: args.debug(),
|
|
is_sandboxed,
|
|
computer_use_override,
|
|
});
|
|
}
|
|
galaxy_cli::Command::DumpDebugInfo => {
|
|
return debug_dump::run();
|
|
}
|
|
#[cfg(not(target_family = "wasm"))]
|
|
galaxy_cli::Command::PrintTelemetryEvents => {
|
|
return TelemetryEvent::print_telemetry_events_json();
|
|
}
|
|
}
|
|
}
|
|
|
|
// If running as a standalone CLI binary or through a channel-specific
|
|
// Galaxy AI launcher, print help instead of launching the GUI app. Keep
|
|
// recognizing the former Oz launcher because existing installations may
|
|
// still contain that external symlink.
|
|
let is_cli_binary = cfg!(feature = "standalone")
|
|
|| galaxy_cli::binary_name()
|
|
.is_some_and(|name| name.starts_with("galaxy-ai") || name.starts_with("oz"))
|
|
|| std::env::var_os("GALAXY_CLI_MODE").is_some()
|
|
|| std::env::var_os("WARP_CLI_MODE").is_some();
|
|
if is_cli_binary {
|
|
galaxy_cli::Args::clap_command().print_help()?;
|
|
return Ok(());
|
|
}
|
|
|
|
let api_key = args.api_key().cloned();
|
|
run_internal(LaunchMode::App {
|
|
args: args.into_app_args(),
|
|
api_key,
|
|
})
|
|
}
|
|
|
|
/// Runs a parsed Warp worker command.
|
|
fn run_worker_command(worker: &warp_cli::WorkerCommand) -> Result<()> {
|
|
match worker {
|
|
#[cfg(all(feature = "local_tty", unix))]
|
|
warp_cli::WorkerCommand::TerminalServer(args) => {
|
|
crate::terminal::local_tty::server::run_terminal_server(args);
|
|
Ok(())
|
|
}
|
|
#[cfg(feature = "plugin_host")]
|
|
warp_cli::WorkerCommand::PluginHost { .. } => crate::run_plugin_host(),
|
|
#[cfg(feature = "local_tty")]
|
|
warp_cli::WorkerCommand::MinidumpServer { socket_name } => {
|
|
let _ = socket_name;
|
|
panic!("The minidump server is not supported");
|
|
}
|
|
#[cfg(not(target_family = "wasm"))]
|
|
warp_cli::WorkerCommand::RemoteServerProxy(args) => {
|
|
// Proxy is a thin byte bridge (stdin/stdout ↔ Unix socket).
|
|
// It only needs logging to stderr since stdout is the protocol
|
|
// channel. No crash reporting, no initialize_app.
|
|
let launch_mode = LaunchMode::RemoteServerProxy;
|
|
let mut tracing_initialization = tracing::init()?;
|
|
warp_logging::init(warp_logging::LogConfig {
|
|
is_cli: true,
|
|
log_destination: launch_mode.log_destination(),
|
|
..Default::default()
|
|
})?;
|
|
tracing_initialization.log_initialization_warning();
|
|
crate::remote_server::run_proxy(args.identity_key.clone())
|
|
}
|
|
#[cfg(not(target_family = "wasm"))]
|
|
warp_cli::WorkerCommand::RemoteServerDaemon(args) => {
|
|
// Daemon handles its own full initialization (including
|
|
// initialize_app and crash reporting) inside run_daemon_app.
|
|
crate::remote_server::run_daemon(args.identity_key.clone())
|
|
}
|
|
#[cfg(not(target_family = "wasm"))]
|
|
warp_cli::WorkerCommand::RipgrepSearch {
|
|
parent,
|
|
ignore_case,
|
|
multiline,
|
|
pattern,
|
|
paths,
|
|
} => {
|
|
warp_ripgrep::search::run_search_subprocess(
|
|
std::slice::from_ref(pattern),
|
|
paths.clone(),
|
|
*ignore_case,
|
|
*multiline,
|
|
parent.pid,
|
|
)
|
|
.map_err(|err| anyhow!(err.to_string()))?;
|
|
Ok(())
|
|
}
|
|
#[cfg(not(any(
|
|
feature = "local_tty",
|
|
feature = "plugin_host",
|
|
not(target_family = "wasm")
|
|
)))]
|
|
worker => {
|
|
// On wasm, specifically, we should fail spectacularly if we get here.
|
|
#[cfg(target_family = "wasm")]
|
|
panic!("Worker process not supported on WASM: {worker:?}")
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Runs an integration test using the provided test driver.
|
|
pub fn run_integration_test(driver: TestDriver) -> Result<()> {
|
|
let is_integration_test = std::env::var("WARP_INTEGRATION").is_ok();
|
|
let launch = LaunchMode::Test {
|
|
driver: Box::new(Some(driver)),
|
|
is_integration_test,
|
|
};
|
|
run_internal(launch)
|
|
}
|
|
|
|
/// Runs the headless TUI front-end (the `warp-tui` binary in the `warp_tui`
|
|
/// crate). Bootstraps the real (headless) app and then runs `mount`, which
|
|
/// builds the root TUI view and starts the non-blocking TUI driver.
|
|
///
|
|
/// `mount` is supplied by the `warp_tui` crate (which owns the concrete root
|
|
/// view plus the window/driver bootstrap), so `warp` never has to depend on
|
|
/// `warp_tui`.
|
|
#[cfg(feature = "tui")]
|
|
pub fn run_tui(mount: TuiMountFn) -> Result<()> {
|
|
run_internal(LaunchMode::Tui { mount })
|
|
}
|
|
|
|
/// Dispatches a worker command when the current executable was re-invoked for one.
|
|
#[cfg(feature = "tui")]
|
|
pub fn run_tui_worker_if_requested() -> Option<Result<()>> {
|
|
// Worker spawners always put the worker mode in argv[1]. Do not scan later
|
|
// arguments because a TUI prompt value may legitimately match a worker name.
|
|
let is_worker = std::env::args()
|
|
.nth(1)
|
|
.is_some_and(|arg| warp_cli::is_worker_invocation(&arg));
|
|
if !is_worker {
|
|
return None;
|
|
}
|
|
|
|
features::init_feature_flags();
|
|
let args = warp_cli::Args::from_env();
|
|
let Some(warp_cli::Command::Worker(worker)) = args.command() else {
|
|
return Some(Err(anyhow!(
|
|
"Recognized a Warp worker invocation, but failed to parse its worker command"
|
|
)));
|
|
};
|
|
Some(run_worker_command(worker))
|
|
}
|
|
|
|
/// The headless TUI front-end's mount callback, carried by [`LaunchMode::Tui`].
|
|
/// Supplied to [`run_tui`] by the `warp_tui` crate; it runs after
|
|
/// `initialize_app` to build the root TUI view and start the TUI driver.
|
|
pub type TuiMountFn = Box<dyn FnOnce(&mut warpui::AppContext)>;
|
|
|
|
/// Runs the app (or CLI / daemon). For [`LaunchMode::Tui`] it runs the mount
|
|
/// carried by the variant after `initialize_app` (building the root TUI view and
|
|
/// starting the driver) in place of the GUI/CLI `launch()` path.
|
|
fn run_internal(mut launch_mode: LaunchMode) -> Result<()> {
|
|
let mut timer = IntervalTimer::new();
|
|
|
|
// ── Early initialization (pre-AppBuilder) ──────────────────────
|
|
// These steps run before the platform event loop is started.
|
|
// They must not depend on AppContext.
|
|
|
|
#[cfg(windows)]
|
|
dynamic_libraries::configure_library_loading();
|
|
|
|
if launch_mode.needs_profiling() {
|
|
profiling::init();
|
|
}
|
|
|
|
// The `run` function already initializes feature flags, but ensure they're initialized here
|
|
// for other entrypoints.
|
|
features::init_feature_flags();
|
|
|
|
let mut tracing_initialization = launch_mode
|
|
.needs_profiling()
|
|
.then(tracing::init)
|
|
.transpose()?;
|
|
|
|
// Start the `run_internal` span here - we can't do it before this point
|
|
// because we need the tracing initialization to be complete first.
|
|
let span = ::tracing::info_span!(
|
|
"run_internal",
|
|
tags.cloud_agent = true,
|
|
launch_mode = launch_mode.as_str_for_tracing()
|
|
);
|
|
let _enter = span.enter();
|
|
|
|
// Load the TOML-backed preferences before logging so the opt-in session log can capture the
|
|
// complete application startup. The same backends are registered with AppContext below.
|
|
let private_preferences = settings::init_private_user_preferences();
|
|
let (public_preferences, startup_toml_parse_error) = settings::init_public_user_preferences();
|
|
let session_logs_enabled = FeatureFlag::SettingsFile.is_enabled()
|
|
&& crate::settings::SessionLogsEnabled::read_from_preferences(public_preferences.as_ref())
|
|
.unwrap_or_default();
|
|
|
|
// When the SettingsFile feature flag is enabled, public settings live in
|
|
// the TOML-backed store. When disabled, they live in the platform-native
|
|
// store (same backend as private). Use the correct one for pre-app reads.
|
|
let prefs_for_public_settings: &dyn galaxyui_extras::user_preferences::UserPreferences =
|
|
if FeatureFlag::SettingsFile.is_enabled() {
|
|
public_preferences.as_ref()
|
|
} else {
|
|
private_preferences.deref()
|
|
};
|
|
|
|
let log_destination = launch_mode.log_destination();
|
|
let is_cli = log_destination.is_some();
|
|
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(enable_crash_recovery)] {
|
|
if crash_recovery::is_crash_recovery_process(launch_mode.args().as_ref()) {
|
|
galaxy_logging::init_for_crash_recovery_process()?;
|
|
} else {
|
|
galaxy_logging::init(galaxy_logging::LogConfig {
|
|
is_cli,
|
|
log_destination,
|
|
session_logs_enabled,
|
|
..Default::default()
|
|
})?;
|
|
}
|
|
} else {
|
|
galaxy_logging::init(galaxy_logging::LogConfig {
|
|
is_cli,
|
|
log_destination,
|
|
session_logs_enabled,
|
|
..Default::default()
|
|
})?;
|
|
}
|
|
}
|
|
|
|
if let Some(initialization) = tracing_initialization.as_mut() {
|
|
initialization.log_initialization_warning();
|
|
}
|
|
timer.mark_interval_end("LOG_FILE_SETUP_COMPLETE");
|
|
|
|
if let Some(error) = &startup_toml_parse_error {
|
|
log::warn!("Settings file has syntax errors and could not be parsed: {error}");
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
platform::windows::check_redirection_guard();
|
|
|
|
// Adjust resource limits early, before doing other work, to ensure that
|
|
// any children we spawn (like the terminal server) inherit our adjusted
|
|
// rlimits.
|
|
resource_limits::adjust_resource_limits();
|
|
|
|
// For wasm builds we have this special case to parse out the intent
|
|
// from the url that is used to visite the app on web.
|
|
#[cfg(target_family = "wasm")]
|
|
{
|
|
use uri::web_intent_parser;
|
|
if let Some(intent) = web_intent_parser::parse_web_intent_from_current_url() {
|
|
launch_mode.add_url(intent);
|
|
}
|
|
web_intent_parser::set_context_flags_from_current_url();
|
|
}
|
|
|
|
#[cfg(all(
|
|
feature = "release_bundle",
|
|
any(target_os = "linux", target_os = "freebsd")
|
|
))]
|
|
if let LaunchMode::App { .. } = launch_mode {
|
|
match app_services::linux::pass_startup_args_to_existing_instance(
|
|
launch_mode.args().as_ref(),
|
|
) {
|
|
// If we were able to contact an existing application instance, quit -
|
|
// we only want to run a single instance of Warp at a time.
|
|
Ok(_) => std::process::exit(0),
|
|
// If Warp isn't already running, we're good to go.
|
|
Err(app_services::linux::StartupArgsForwardingError::NoExistingInstance) => {}
|
|
// If we just finished an auto-update, we should continue running.
|
|
Err(app_services::linux::StartupArgsForwardingError::IgnoredAfterAutoUpdate) => {}
|
|
// If we were unable to perform the forwarding for an unknown reason,
|
|
// it's better to run a second instance than potentially end up in a
|
|
// state where Warp refuses to run even a first instance.
|
|
Err(err) => {
|
|
let err = anyhow::Error::from(err).context("Failed to forward startup args");
|
|
log::error!("{err:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(all(feature = "release_bundle", windows))]
|
|
if let LaunchMode::App { .. } = launch_mode {
|
|
match app_services::windows::pass_startup_args_to_existing_instance(
|
|
launch_mode.args().as_ref(),
|
|
) {
|
|
// If we were able to contact an existing application instance, quit -
|
|
// we only want to run a single instance of Warp at a time.
|
|
Ok(_) => std::process::exit(0),
|
|
// If Warp isn't already running, we're good to go.
|
|
Err(app_services::windows::StartupArgsForwardingError::NoExistingInstance) => {}
|
|
// If we just finished an auto-update, we should continue running.
|
|
Err(app_services::windows::StartupArgsForwardingError::IgnoredAfterAutoUpdate) => {}
|
|
// If we were unable to perform the forwarding for an unknown reason,
|
|
// it's better to run a second instance than potentially end up in a
|
|
// state where Warp refuses to run even a first instance.
|
|
Err(err) => {
|
|
let err = anyhow::Error::from(err).context("Failed to forward startup args");
|
|
log::error!("{err:#}");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sets up a Job Object that we associate with the Warp process to handle
|
|
// shared fate with its child processes. This should be called before we
|
|
// start spawning any child processes.
|
|
#[cfg(windows)]
|
|
command::windows::init();
|
|
|
|
#[cfg(enable_crash_recovery)]
|
|
let crash_recovery =
|
|
crash_recovery::CrashRecovery::new(&launch_mode, prefs_for_public_settings);
|
|
|
|
// Set up the pty spawner before doing any meaningful work. We want to
|
|
// ensure that the process is in the cleanest possible state (minimal opened
|
|
// files, modified signal handlers, etc.) to avoid unexpected effects on
|
|
// spawned ptys.
|
|
//
|
|
#[cfg(feature = "local_tty")]
|
|
let pty_spawner =
|
|
terminal::local_tty::spawner::PtySpawner::new().context("Failed to create pty spawner")?;
|
|
|
|
// The TUI front-end skips the GUI lifecycle callbacks (which reach for
|
|
// singletons/windows it never creates), so it uses empty callbacks.
|
|
let callbacks = if matches!(launch_mode, LaunchMode::Tui { .. }) {
|
|
warpui::platform::AppCallbacks::default()
|
|
} else {
|
|
app_callbacks(
|
|
launch_mode.is_integration_test(),
|
|
tracing_initialization.take(),
|
|
)
|
|
};
|
|
let mut app_builder = if launch_mode.is_headless() {
|
|
galaxyui::platform::AppBuilder::new_headless(
|
|
callbacks,
|
|
Box::new(ASSETS),
|
|
launch_mode.take_test_driver(),
|
|
)
|
|
} else {
|
|
galaxyui::platform::AppBuilder::new(
|
|
callbacks,
|
|
Box::new(ASSETS),
|
|
launch_mode.take_test_driver(),
|
|
)
|
|
};
|
|
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use galaxyui::platform::mac::AppExt;
|
|
use galaxyui::AssetProvider as _;
|
|
|
|
let activate_on_launch = !launch_mode.is_integration_test()
|
|
|| std::env::var("WARPUI_USE_REAL_DISPLAY_IN_INTEGRATION_TESTS").is_ok();
|
|
app_builder.set_activate_on_launch(activate_on_launch);
|
|
|
|
let dev_icon = ASSETS.get("bundled/png/galaxy.png")?;
|
|
app_builder.set_dev_icon(dev_icon);
|
|
|
|
let show_dock_icon = crate::settings::app_icon::ShowDockIconState::read_from_preferences(
|
|
prefs_for_public_settings,
|
|
)
|
|
.unwrap_or_else(crate::settings::app_icon::ShowDockIconState::default_value);
|
|
app_builder.set_show_dock_icon_on_launch(show_dock_icon);
|
|
app_builder.set_menu_bar_builder(app_menus::menu_bar);
|
|
app_builder.set_dock_menu_builder(|_| app_menus::dock_menu());
|
|
}
|
|
|
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
{
|
|
use galaxyui::platform::linux::{self, AppBuilderExt};
|
|
|
|
use crate::settings::ForceX11;
|
|
|
|
app_builder.set_window_class(ChannelState::app_id().to_string());
|
|
|
|
let force_x11 = ForceX11::read_from_preferences(prefs_for_public_settings)
|
|
.unwrap_or(ForceX11::default_value());
|
|
// Force use of wayland if the user has passed the `WARP_ENABLE_WAYLAND` env var.
|
|
let allow_wayland = linux::is_wayland_env_var_set() || !force_x11;
|
|
app_builder.force_x11(!allow_wayland);
|
|
}
|
|
|
|
#[cfg(target_os = "windows")]
|
|
{
|
|
use galaxyui::platform::windows::AppBuilderExt;
|
|
app_builder.set_app_user_model_id(ChannelState::app_id().to_string());
|
|
|
|
// Only use DXC for DirectX shader compilation if we're not running in a Parallels VM
|
|
// Parallels VMs can have issues with DXC shader compilation
|
|
let is_parallels_vm = crate::util::vm_detection::is_running_in_windows_parallels_vm();
|
|
if !is_parallels_vm {
|
|
log::info!("Using DXC for DirectX shader compilation");
|
|
use galaxyui::platform::windows::DXCPath;
|
|
|
|
app_builder.use_dxc_for_directx_shader_compilation(DXCPath {
|
|
dxc_path: "dxcompiler.dll".to_string(),
|
|
dxil_path: "dxil.dll".to_string(),
|
|
});
|
|
} else {
|
|
log::info!("Skipping DXC for DirectX shader compilation; running in a Parallels VM");
|
|
}
|
|
}
|
|
|
|
// Override any bindings that have a `Custom` trigger to a `Keystroke`-based trigger. In theory,
|
|
// this should be a noop on Mac (since the keystrokes registered via the Mac menus first
|
|
// intercept the binding), but just to be safe we only enable this in cases where we don't
|
|
// include mac menus.
|
|
#[cfg(not(target_os = "macos"))]
|
|
app_builder.convert_custom_triggers_to_keystroke_triggers(
|
|
crate::util::bindings::custom_tag_to_keystroke,
|
|
);
|
|
|
|
#[cfg(target_os = "macos")]
|
|
app_builder.register_default_keystroke_triggers_for_custom_actions(
|
|
crate::util::bindings::custom_tag_to_keystroke,
|
|
);
|
|
|
|
app_builder.run(move |ctx| {
|
|
#[cfg(not(target_family = "wasm"))]
|
|
// Rotate the log files in the background.
|
|
ctx.background_executor()
|
|
.spawn(galaxy_logging::rotate_log_files())
|
|
.detach();
|
|
|
|
let local_project_indexing_enabled = matches!(
|
|
launch_mode,
|
|
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
|
|
);
|
|
ctx.add_singleton_model(|ctx| {
|
|
AppExecutionMode::new_with_local_project_indexing(
|
|
launch_mode.execution_mode(),
|
|
launch_mode.is_sandboxed(),
|
|
local_project_indexing_enabled,
|
|
ctx,
|
|
)
|
|
});
|
|
// Add the terminal server singleton to the application.
|
|
#[cfg(feature = "local_tty")]
|
|
ctx.add_singleton_model(move |_ctx| pty_spawner);
|
|
|
|
// Register user preferences. This must be done before initializing
|
|
// feature flags or experiments, both of which check user preferences for
|
|
// overrides.
|
|
ctx.add_singleton_model(move |_ctx| ::settings::PublicPreferences::new(public_preferences));
|
|
ctx.add_singleton_model(move |_ctx| private_preferences);
|
|
let startup_toml_parse_error = startup_toml_parse_error;
|
|
|
|
#[cfg(enable_crash_recovery)]
|
|
ctx.add_singleton_model(move |_ctx| crash_recovery);
|
|
|
|
#[cfg(feature = "plugin_host")]
|
|
ctx.add_singleton_model(move |ctx| {
|
|
plugin::PluginHost::new(ctx).expect("Could not instantiate PluginHost")
|
|
});
|
|
let app_state = initialize_app(
|
|
&launch_mode,
|
|
timer,
|
|
startup_toml_parse_error,
|
|
ctx,
|
|
std::iter::empty(),
|
|
);
|
|
|
|
if ImprovedPaletteSearch::improved_search_enabled(ctx) {
|
|
FeatureFlag::UseTantivySearch.set_enabled(true);
|
|
}
|
|
|
|
// The TUI front-end reuses the full `initialize_app` bootstrap above (so
|
|
// auth, `Appearance`, settings, etc. exist), then runs the device-login
|
|
// flow and mounts the TUI (via `crate::tui::init`) instead of the
|
|
// GUI/CLI `launch()` path.
|
|
match launch_mode {
|
|
#[cfg(feature = "tui")]
|
|
LaunchMode::Tui { mount } => crate::tui::init(mount, ctx),
|
|
#[cfg(not(feature = "tui"))]
|
|
LaunchMode::Tui { .. } => {
|
|
unreachable!("the `tui` launch mode requires the `tui` feature")
|
|
}
|
|
other => launch(ctx, app_state, other),
|
|
}
|
|
})
|
|
}
|
|
|
|
pub struct UpdateQuakeModeEventArg {
|
|
active_window_id: Option<WindowId>,
|
|
}
|
|
|
|
#[::tracing::instrument(skip_all, fields(tags.cloud_agent = true))]
|
|
pub(crate) fn initialize_app(
|
|
launch_mode: &LaunchMode,
|
|
mut timer: IntervalTimer,
|
|
startup_toml_parse_error: Option<galaxyui_extras::user_preferences::Error>,
|
|
ctx: &mut galaxyui::AppContext,
|
|
_pre_sentry_errors: impl IntoIterator<Item = anyhow::Error>,
|
|
) -> Option<AppState> {
|
|
let data_domain = ChannelState::data_domain();
|
|
|
|
// Daemon auth arrives through the client handshake, so avoid platform keychains that may
|
|
// require an interactive unlock prompt. Other headless modes still use secure storage for
|
|
// persisted login and BYO provider credentials.
|
|
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
|
galaxyui_extras::secure_storage::register_unavailable(ctx);
|
|
} else {
|
|
// Register an implementation of the secure storage service.
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(feature = "integration_tests")] {
|
|
galaxyui_extras::secure_storage::register_noop(&data_domain, ctx);
|
|
} else if #[cfg(any(target_os = "linux", target_os = "freebsd"))] {
|
|
galaxyui_extras::secure_storage::register_with_fallback(&data_domain, galaxy_core::paths::state_dir(), ctx)
|
|
} else if #[cfg(target_os = "windows")] {
|
|
galaxyui_extras::secure_storage::register_with_dir(&data_domain, galaxy_core::paths::state_dir(), ctx)
|
|
} else {
|
|
galaxyui_extras::secure_storage::register(&data_domain, ctx);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Migrate state and caches out of macOS Application Support before creating
|
|
// the new home-relative data directory.
|
|
galaxy_core::paths::migrate_legacy_macos_data_dir_if_needed();
|
|
|
|
// One-time migration: give Preview its own config directory by
|
|
// symlinking contents from the shared ~/.warp location. Must run
|
|
// before ensure_galaxy_watch_roots_exist() creates the new directory.
|
|
#[cfg(target_os = "macos")]
|
|
preview_config_migration::migrate_preview_config_dir_if_needed();
|
|
|
|
ensure_galaxy_watch_roots_exist();
|
|
ctx.add_singleton_model(GalaxyManagedPathsWatcher::new);
|
|
|
|
ctx.add_singleton_model(GalaxyConfig::new);
|
|
ctx.add_singleton_model(|_ctx| SettingsManager::default());
|
|
|
|
let user_defaults_on_startup = settings::init(startup_toml_parse_error, ctx);
|
|
timer.mark_interval_end("READ_USER_DEFAULTS_AND_INITIALIZE_SETTINGS");
|
|
|
|
if FeatureFlag::UIZoom.is_enabled() {
|
|
ctx.set_zoom_factor(WindowSettings::as_ref(ctx).zoom_level.as_zoom_factor());
|
|
}
|
|
|
|
// Extract API key from command line options, if applicable.
|
|
let api_key = match launch_mode {
|
|
LaunchMode::CommandLine { global_options, .. } => global_options.api_key.clone(),
|
|
LaunchMode::App { api_key, .. } if ChannelState::channel().is_dogfood() => api_key.clone(),
|
|
_ => None,
|
|
};
|
|
let api_key = if FeatureFlag::APIKeyAuthentication.is_enabled() {
|
|
api_key
|
|
} else {
|
|
None
|
|
};
|
|
|
|
// A key supplied to an App launch but dropped here means Warp will start logged out
|
|
// (non-dogfood channel or feature disabled). Surface this loudly so it isn't silent.
|
|
if api_key.is_none()
|
|
&& matches!(
|
|
launch_mode,
|
|
LaunchMode::App {
|
|
api_key: Some(_),
|
|
..
|
|
}
|
|
)
|
|
{
|
|
let channel = ChannelState::channel();
|
|
let warning = format!(
|
|
"WARNING: --api-key/WARP_API_KEY was provided but IGNORED on the '{channel}' channel — Warp is starting LOGGED OUT. API-key auth is only available on internal (dogfood) builds."
|
|
);
|
|
eprintln!("{warning}");
|
|
}
|
|
|
|
let auth_state = Arc::new(AuthState::initialize(ctx, api_key));
|
|
timer.mark_interval_end("AUTH_MANAGER_SET_USER");
|
|
|
|
let agent_source = determine_agent_source(launch_mode);
|
|
|
|
// NetworkLogModel must be registered before ServerApiProvider so that
|
|
// `NetworkLogModel::install_on_clients` can reach it when forwarding items
|
|
// captured by the HTTP client hooks.
|
|
ctx.add_singleton_model(|_ctx| NetworkLogModel::default());
|
|
|
|
// Create a shared IAP state for staging builds. The same `Arc<IapState>`
|
|
// is handed to both `ServerApi` (for sync reads on the request path) and
|
|
// `IapManager` (which owns refresh logic on the main thread).
|
|
#[cfg(not(target_family = "wasm"))]
|
|
let iap_state = ChannelState::iap_config().map(|cfg| Arc::new(IapState::new(&cfg)));
|
|
#[cfg(target_family = "wasm")]
|
|
let iap_state: Option<Arc<IapState>> = None;
|
|
|
|
let server_api_provider = ctx.add_singleton_model({
|
|
let auth_state = auth_state.clone();
|
|
let iap_state = iap_state.clone();
|
|
move |ctx| ServerApiProvider::new(auth_state, agent_source, iap_state, ctx)
|
|
});
|
|
|
|
let server_api = server_api_provider.as_ref(ctx).get();
|
|
#[cfg(not(target_family = "wasm"))]
|
|
if let Ok(run_id) = std::env::var(warp_cli::OZ_RUN_ID_ENV) {
|
|
match run_id.parse() {
|
|
Ok(task_id) => server_api.set_ambient_agent_task_id(Some(task_id)),
|
|
Err(err) => log::warn!("Ignoring invalid {}: {err}", warp_cli::OZ_RUN_ID_ENV),
|
|
}
|
|
}
|
|
let ai_client = server_api_provider.as_ref(ctx).get_ai_client();
|
|
#[cfg(not(target_family = "wasm"))]
|
|
// Refresh starts only after the authenticated server client exists; tracing initialization
|
|
// remains responsible for deciding whether this process opted in to cloud-agent export.
|
|
tracing::start_auth_refresh(
|
|
server_api_provider.as_ref(ctx).get_managed_secrets_client(),
|
|
ctx,
|
|
);
|
|
|
|
ctx.add_singleton_model(|_ctx| AuthStateProvider::new(auth_state.clone()));
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
AuthManager::new(
|
|
server_api.clone(),
|
|
server_api_provider.as_ref(ctx).get_auth_client(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
ctx.add_singleton_model(|_ctx| GPUState::new());
|
|
|
|
PrivacySettings::register_singleton(ctx);
|
|
|
|
// If any part of sqlite initialization fails, we just don't do session restoration (i.e.
|
|
// feature degradation).
|
|
let persistence_scope = match launch_mode {
|
|
LaunchMode::RemoteServerDaemon { identity_key } => {
|
|
persistence::PersistenceScope::RemoteServerDaemon {
|
|
identity_key: identity_key.clone(),
|
|
}
|
|
}
|
|
LaunchMode::App { .. }
|
|
| LaunchMode::CommandLine { .. }
|
|
| LaunchMode::RemoteServerProxy
|
|
| LaunchMode::Test { .. }
|
|
| LaunchMode::Tui { .. } => persistence::PersistenceScope::App,
|
|
};
|
|
let (sqlite_data, writer_handles) = persistence::initialize(ctx, persistence_scope);
|
|
timer.mark_interval_end("SQLITE_INITIALIZED");
|
|
|
|
let persistence_writer = PersistenceWriter::new(writer_handles);
|
|
|
|
let model_event_sender = persistence_writer.sender();
|
|
|
|
let referral_theme_status = ctx.add_model(ReferralThemeStatus::new);
|
|
let tips_handle = ctx.add_model(|_| user_defaults_on_startup.tips_data);
|
|
let user_default_shell_unsupported_banner_model_handle =
|
|
ctx.add_model(|_| user_defaults_on_startup.user_default_shell_unsupported_banner_state);
|
|
// Extract the full-file parse error (if any) before the settings_file_error
|
|
// value is moved below. Only FileParseFailed gates the broken-file guard
|
|
// in `initialize_cloud_preferences_syncer`; InvalidSettings means TOML
|
|
// parsed but individual values were wrong, which doesn't mean local
|
|
// state is unusable.
|
|
let startup_toml_parse_error_for_syncer = user_defaults_on_startup
|
|
.settings_file_error
|
|
.as_ref()
|
|
.and_then(|err| match err {
|
|
settings::SettingsFileError::FileParseFailed(msg) => Some(msg.clone()),
|
|
settings::SettingsFileError::InvalidSettings(_) => None,
|
|
});
|
|
let settings_file_error = user_defaults_on_startup.settings_file_error;
|
|
ctx.add_singleton_model(move |_ctx| {
|
|
GlobalResourceHandlesProvider::new(GlobalResourceHandles {
|
|
model_event_sender,
|
|
tips_completed: tips_handle,
|
|
referral_theme_status,
|
|
user_default_shell_unsupported_banner_model_handle,
|
|
settings_file_error,
|
|
})
|
|
});
|
|
|
|
let (
|
|
mut cloud_objects,
|
|
mut cached_workspaces,
|
|
mut current_workspace_uid,
|
|
mut app_state,
|
|
mut command_history,
|
|
mut restored_user_profiles,
|
|
mut time_of_next_force_object_refresh,
|
|
mut object_actions,
|
|
mut experiments,
|
|
mut ai_queries,
|
|
persisted_workspaces,
|
|
mut workspace_language_servers,
|
|
mut multi_agent_conversations,
|
|
mut persisted_projects,
|
|
mut persisted_project_rules,
|
|
mut persisted_ignored_suggestions,
|
|
mut persisted_mcp_server_installations,
|
|
mut mcp_servers_to_restore,
|
|
) = sqlite_data
|
|
.map(|sqlite_data| {
|
|
(
|
|
sqlite_data.cloud_objects,
|
|
sqlite_data.workspaces,
|
|
sqlite_data.current_workspace_uid,
|
|
Some(sqlite_data.app_state),
|
|
sqlite_data.command_history,
|
|
sqlite_data.user_profiles,
|
|
sqlite_data.time_of_next_force_object_refresh,
|
|
sqlite_data.object_actions,
|
|
sqlite_data.experiments,
|
|
sqlite_data.ai_queries,
|
|
sqlite_data.codebase_indices,
|
|
sqlite_data.workspace_language_servers,
|
|
sqlite_data.multi_agent_conversations,
|
|
sqlite_data.projects,
|
|
sqlite_data.project_rules,
|
|
sqlite_data.ignored_suggestions,
|
|
sqlite_data.mcp_server_installations,
|
|
sqlite_data.mcp_servers_to_restore,
|
|
)
|
|
})
|
|
.unwrap_or_else(|| {
|
|
(
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
Default::default(),
|
|
)
|
|
});
|
|
|
|
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
|
let codebase_index_count = persisted_workspaces.len();
|
|
log::debug!(
|
|
"[Remote codebase indexing] Restored daemon codebase index metadata: metadata_count={codebase_index_count}"
|
|
);
|
|
cloud_objects = Default::default();
|
|
cached_workspaces = Default::default();
|
|
current_workspace_uid = None;
|
|
app_state = None;
|
|
command_history = Default::default();
|
|
restored_user_profiles = Default::default();
|
|
time_of_next_force_object_refresh = None;
|
|
object_actions = Default::default();
|
|
experiments = Default::default();
|
|
ai_queries = Default::default();
|
|
workspace_language_servers = Default::default();
|
|
multi_agent_conversations = Default::default();
|
|
persisted_projects = Default::default();
|
|
persisted_project_rules = Default::default();
|
|
persisted_ignored_suggestions = Default::default();
|
|
persisted_mcp_server_installations = Default::default();
|
|
mcp_servers_to_restore = Default::default();
|
|
}
|
|
|
|
// Initialize a global model to track server-side experiment state.
|
|
// This depends on the [`GlobalResourceHandlesProvider`] and so it must
|
|
// be initialized after it.
|
|
ctx.add_singleton_model(|ctx| ServerExperiments::new_from_cache(experiments, ctx));
|
|
|
|
ctx.add_singleton_model(|ctx| AIRequestUsageModel::new(ai_client, ctx));
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
UserWorkspaces::new(
|
|
server_api_provider.as_ref(ctx).get_team_client(),
|
|
server_api_provider.as_ref(ctx).get_workspace_client(),
|
|
cached_workspaces,
|
|
current_workspace_uid,
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// Initialize ApiKeyManager after UserWorkspaces so it can subscribe to workspace/settings changes
|
|
ctx.add_singleton_model(|ctx| {
|
|
#[cfg_attr(target_family = "wasm", allow(unused_mut))]
|
|
let mut manager = ::ai::api_keys::ApiKeyManager::new(ctx);
|
|
#[cfg(not(target_family = "wasm"))]
|
|
manager.subscribe_to_settings_changes(ctx);
|
|
// Gemini Enterprise (GEAP) credential refresh triggers: workspace
|
|
// settings saves / team changes and the member's enablement toggle.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
if FeatureFlag::GeminiEnterprise.is_enabled() {
|
|
manager.subscribe_to_geap_settings_changes(ctx);
|
|
}
|
|
manager
|
|
});
|
|
|
|
ctx.add_singleton_model(AntivirusInfo::new);
|
|
|
|
if let LaunchMode::App { .. } = launch_mode {
|
|
autoupdate::check_and_report_update_errors(ctx);
|
|
}
|
|
|
|
ctx.set_fallback_font_source_provider(|url| ::asset_cache::url_source(url));
|
|
|
|
ctx.set_default_binding_validator(is_binding_cross_platform);
|
|
|
|
if FeatureFlag::Autoupdate.is_enabled() {
|
|
// Attempt to clean up any old executable, whether or not we were
|
|
// explicitly launched as part of the auto-update process. We may have
|
|
// failed to remove the executable on a previous launch of the app and
|
|
// should try again.
|
|
if let Err(e) = autoupdate::remove_old_executable() {
|
|
log::error!("Failed to remove old executable: {e:?}");
|
|
}
|
|
}
|
|
|
|
experiments::init(ctx);
|
|
|
|
// Initialize timestamp for session id and last active event
|
|
App::record_last_active_timestamp();
|
|
|
|
ctx.add_singleton_model(|_| SettingsPaneManager::new());
|
|
ctx.add_singleton_model(|_| AIFactManager::new());
|
|
ctx.add_singleton_model(|_| ExecutionProfileEditorManager::default());
|
|
ctx.add_singleton_model(|_| NetworkLogPaneManager::default());
|
|
ctx.add_singleton_model(|_| pricing::PricingInfoModel::new());
|
|
ctx.add_singleton_model(|ctx| {
|
|
// Not using the *Provider types isn't ideal, but it's worth it for the ability to move managed secrets to a separate crate.
|
|
ManagedSecretManager::new(
|
|
server_api_provider.as_ref(ctx).get_managed_secrets_client(),
|
|
auth_state.clone(),
|
|
)
|
|
});
|
|
|
|
#[cfg(target_os = "macos")]
|
|
if !launch_mode.is_headless() {
|
|
AppearanceManager::as_ref(ctx).set_app_icon(ctx);
|
|
}
|
|
|
|
#[cfg(feature = "local_tty")]
|
|
terminal::available_shells::register(ctx);
|
|
|
|
// Add truly global actions that don't depend on the existence of any view here
|
|
ctx.add_global_action("app:toggle_user_ps1", move |_args: &(), ctx| {
|
|
SessionSettings::handle(ctx).update(ctx, |session_settings, ctx| {
|
|
report_if_error!(session_settings.honor_ps1.toggle_and_save_value(ctx));
|
|
});
|
|
});
|
|
ctx.add_global_action("app:toggle_copy_on_select", move |_args: &(), ctx| {
|
|
SelectionSettings::handle(ctx).update(ctx, |selection_settings, ctx| {
|
|
report_if_error!(selection_settings.copy_on_select.toggle_and_save_value(ctx));
|
|
});
|
|
});
|
|
|
|
ctx.add_singleton_model(|_ctx| SyncedInputState::new());
|
|
|
|
ctx.add_singleton_model(remote_server::manager::RemoteServerManager::new);
|
|
#[cfg(not(target_family = "wasm"))]
|
|
ctx.add_singleton_model(remote_server::codebase_index_model::RemoteCodebaseIndexModel::new);
|
|
#[cfg(not(target_family = "wasm"))]
|
|
remote_server::wire_auth_token_rotation(ctx);
|
|
|
|
log::info!(
|
|
"Starting Galaxy with channel state {}, version {:?}, profile {}, source commit {}{}",
|
|
ChannelState::debug_str(),
|
|
ChannelState::app_version(),
|
|
build_info::profile(),
|
|
build_info::SOURCE_COMMIT,
|
|
if build_info::source_is_dirty() {
|
|
" (modified)"
|
|
} else {
|
|
""
|
|
}
|
|
);
|
|
|
|
// Teach our app that sometimes option means meta.
|
|
ctx.set_event_munger(move |event, ctx| {
|
|
let extra_meta_keys = *KeysSettings::as_ref(ctx).extra_meta_keys;
|
|
apply_extra_meta_keys(event, extra_meta_keys);
|
|
apply_scroll_multiplier(event, ctx);
|
|
});
|
|
|
|
// Rewrite recognized Warp web URLs (sessions, Drive, settings, home) into local
|
|
// intent URLs when possible so they open directly in the desktop app.
|
|
ctx.set_before_open_url(|url_str, _ctx| {
|
|
if let Ok(url) = Url::parse(url_str) {
|
|
if let Some(intent) = maybe_rewrite_web_url_to_intent(&url) {
|
|
return intent.to_string();
|
|
}
|
|
}
|
|
url_str.to_owned()
|
|
});
|
|
|
|
ctx.set_a11y_verbosity(*AccessibilitySettings::as_ref(ctx).a11y_verbosity);
|
|
|
|
#[cfg(enable_crash_recovery)]
|
|
ctx.on_draw_frame_error(|ctx, window_id| {
|
|
crash_recovery::CrashRecovery::handle(ctx).update(ctx, |crash_recovery, _ctx| {
|
|
crash_recovery.on_draw_frame_error(window_id);
|
|
});
|
|
});
|
|
|
|
let user_is_logged_in = auth_state.is_logged_in();
|
|
|
|
if user_is_logged_in {
|
|
// Skip refresh_user for CLI mode — the CLI handles auth refresh in
|
|
// ensure_auth_state so it can detect invalid credentials before running
|
|
// a command.
|
|
if !matches!(launch_mode, LaunchMode::CommandLine { .. }) {
|
|
AuthManager::handle(ctx).update(ctx, |auth_manager, ctx| {
|
|
auth_manager.refresh_user(ctx);
|
|
});
|
|
}
|
|
|
|
// Set the first frame callback to record the app's startup time.
|
|
// This is only sent for logged-in users so that new users don't skew performance metrics.
|
|
let is_screen_reader_enabled = ctx.is_screen_reader_enabled();
|
|
let from_relaunch = launch_mode.args().finish_update;
|
|
ctx.on_first_frame_drawn(move |ctx| {
|
|
let timing_data = IntervalTimer::handle(ctx).update(ctx, |timer, _| {
|
|
timer.mark_interval_end("FIRST_FRAME_DRAWN");
|
|
timer.compute_stats()
|
|
});
|
|
let event = TelemetryEvent::AppStartup(AppStartupInfo {
|
|
is_session_restoration_on: user_defaults_on_startup.should_restore_session,
|
|
is_screen_reader_enabled,
|
|
from_relaunch,
|
|
is_crash_reporting_enabled: false,
|
|
timing_data,
|
|
});
|
|
|
|
GPUState::handle(ctx).update(ctx, |gpu_state, ctx| {
|
|
gpu_state.set_has_lower_power_gpu(
|
|
galaxyui::rendering::is_low_power_gpu_available(),
|
|
ctx,
|
|
);
|
|
});
|
|
|
|
for window_id in ctx.window_ids().collect_vec() {
|
|
SettingsPaneManager::handle(ctx)
|
|
.read(ctx, |model, _| model.settings_view(window_id))
|
|
.update(ctx, |settings, ctx| {
|
|
settings.refresh_preferred_graphics_backend_dropdown(ctx);
|
|
})
|
|
}
|
|
|
|
send_telemetry_from_app_ctx!(event, ctx);
|
|
});
|
|
|
|
#[cfg(enable_crash_recovery)]
|
|
ctx.on_frame_drawn(|ctx, window_id| {
|
|
crash_recovery::CrashRecovery::handle(ctx).update(ctx, |crash_recovery, ctx| {
|
|
crash_recovery.on_frame_drawn(window_id, ctx);
|
|
});
|
|
})
|
|
} else {
|
|
// If the app was opened while logged out, record an event for measuring new users.
|
|
// This is sent immediately in case they quit the app on the signup screen.
|
|
send_telemetry_sync_from_app_ctx!(TelemetryEvent::LoggedOutStartup, ctx);
|
|
download_method::determine_and_report(
|
|
auth_state.clone(),
|
|
ctx.background_executor().clone(),
|
|
);
|
|
}
|
|
|
|
#[cfg(not(target_family = "wasm"))]
|
|
{
|
|
ctx.add_singleton_model(DirectoryWatcher::new);
|
|
// Register the skill provider directories as force-included paths so
|
|
// the gitignore-pruning watch descend filter still watches gitignored
|
|
// skill directories (e.g. `.agents/skills`) for `Repository`
|
|
// subscribers (LSP, MCP). Registered before any repository begins
|
|
// watching so it gates descent on the very first registration.
|
|
DirectoryWatcher::handle(ctx).update(ctx, |watcher, _| {
|
|
watcher.register_force_included_paths(
|
|
::ai::skills::SKILL_PROVIDER_DEFINITIONS
|
|
.iter()
|
|
.map(|provider| provider.skills_path.clone()),
|
|
);
|
|
});
|
|
ctx.add_singleton_model(|_| DetectedRepositories::default());
|
|
if let Some(home_dir) = dirs::home_dir() {
|
|
ctx.add_singleton_model(|ctx| HomeDirectoryWatcher::new(home_dir, ctx));
|
|
} else {
|
|
log::info!("Home directory not found; skipping HomeDirectoryWatcher registration");
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "local_fs")]
|
|
{
|
|
let imported_config_model = ctx.add_singleton_model(ImportedConfigModel::new);
|
|
|
|
if ChannelState::channel() != galaxy_core::channel::Channel::Integration {
|
|
imported_config_model.update(ctx, |model, ctx| {
|
|
model.search_for_settings_to_import(ctx);
|
|
});
|
|
}
|
|
|
|
let emit_incremental_updates = matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. });
|
|
ctx.add_singleton_model(|ctx| {
|
|
let model = if emit_incremental_updates {
|
|
RepoMetadataModel::new_with_incremental_updates(ctx)
|
|
} else {
|
|
RepoMetadataModel::new(ctx)
|
|
};
|
|
model.register_force_included_paths(
|
|
::ai::skills::SKILL_PROVIDER_DEFINITIONS
|
|
.iter()
|
|
.map(|provider| provider.skills_path.clone()),
|
|
ctx,
|
|
);
|
|
model.set_project_skill_provider_paths(
|
|
::ai::skills::SKILL_PROVIDER_DEFINITIONS
|
|
.iter()
|
|
.map(|provider| provider.skills_path.clone()),
|
|
ctx,
|
|
);
|
|
|
|
// Subscribe to RemoteServerManager push events so that remote repo
|
|
// metadata snapshots and incremental updates populate the remote
|
|
// sub-model and trigger RepoMetadataEvent emissions.
|
|
{
|
|
use remote_server::manager::{RemoteServerManager, RemoteServerManagerEvent};
|
|
let mgr = RemoteServerManager::handle(ctx);
|
|
ctx.subscribe_to_model(&mgr, |me, _, event, ctx| match event {
|
|
RemoteServerManagerEvent::RepoMetadataSnapshot { host_id, update } => {
|
|
me.insert_remote_snapshot(host_id.clone(), update, ctx);
|
|
}
|
|
RemoteServerManagerEvent::RepoMetadataUpdated { host_id, update }
|
|
| RemoteServerManagerEvent::RepoMetadataDirectoryLoaded { host_id, update } => {
|
|
me.apply_remote_incremental_update(host_id, update, ctx);
|
|
}
|
|
RemoteServerManagerEvent::HostDisconnected { host_id } => {
|
|
me.remove_remote_repositories_for_host(host_id, ctx);
|
|
}
|
|
_ => {}
|
|
});
|
|
}
|
|
|
|
model
|
|
});
|
|
}
|
|
|
|
ctx.add_singleton_model(|_| GitRepoModels::new());
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
ProjectManagementModel::new(persisted_projects, persistence_writer.sender(), ctx)
|
|
});
|
|
|
|
ctx.add_singleton_model(move |_| History::new(command_history));
|
|
|
|
ctx.add_singleton_model(CustomSecretRegexUpdater::new);
|
|
|
|
// Register initial keybindings prior to creating menus
|
|
ai::init(ctx);
|
|
app_services::init(ctx);
|
|
// // TODO: Temporarily disabling keybindings for WASM builds. Will be implemented in future WASM support.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
code::editor::find::view::init(ctx);
|
|
workspace::init(ctx);
|
|
pane_group::init(ctx);
|
|
terminal::init(ctx);
|
|
input::init(ctx);
|
|
editor::init(ctx);
|
|
onboarding::init(ctx);
|
|
menu::init(ctx);
|
|
tips::tip_view::init(ctx);
|
|
launch_configs::init(ctx);
|
|
workflows::init(ctx);
|
|
themes::theme_chooser::init(ctx);
|
|
themes::theme_creator_modal::init(ctx);
|
|
themes::theme_deletion_modal::init(ctx);
|
|
root_view::init(ctx);
|
|
voltron::init(ctx);
|
|
auth::init(ctx);
|
|
reward_view::init(ctx);
|
|
crate::view_components::find::init(ctx);
|
|
prompt::editor_modal::init(ctx);
|
|
ai::blocklist::agent_view::editor::init(ctx);
|
|
undo_close::init(ctx);
|
|
billing::shared_objects_creation_denied_modal::init(ctx);
|
|
tab_configs::new_worktree_modal::init(ctx);
|
|
tab_configs::params_modal::init(ctx);
|
|
ai::blocklist::init(ctx);
|
|
ai::blocklist::block::status_bar::init(ctx);
|
|
drive::index::init(ctx);
|
|
drive::sharing::dialog::init(ctx);
|
|
ai_assistant::panel::init(ctx);
|
|
settings_view::update_environment_form::init(ctx);
|
|
env_vars::env_var_collection_block::init(ctx);
|
|
context_chips::display_menu::init(ctx);
|
|
context_chips::node_version_popup::init(ctx);
|
|
env_vars::view::env_var_collection::init(ctx);
|
|
ai::agent::todos::popup::init(ctx);
|
|
terminal::view::init_environment::mode_selector::init(ctx);
|
|
coding_entrypoints::project_buttons::init(ctx);
|
|
if FeatureFlag::CodeReviewSaveChanges.is_enabled() {
|
|
code_review::init(ctx);
|
|
}
|
|
|
|
let display_count = ctx.windows().display_count();
|
|
ctx.add_singleton_model(|_| DisplayCount(display_count));
|
|
|
|
ctx.add_singleton_model(|_| RelaunchModel::new());
|
|
ctx.add_singleton_model(|_| ChangelogModel::new(server_api.clone()));
|
|
ctx.add_singleton_model(|_| GitHubAuthNotifier::new());
|
|
ctx.add_singleton_model(|_| NetworkStatus::new());
|
|
ctx.add_singleton_model(|_| SystemStats::new());
|
|
workspace::auto_handoff::init(ctx);
|
|
ctx.add_singleton_model(|_| KeybindingChangedNotifier::new());
|
|
ctx.add_singleton_model(|_| search::command_palette::SelectedItems::new());
|
|
ctx.add_singleton_model(search::files::model::FileSearchModel::new);
|
|
ctx.add_singleton_model(|_| VimRegisters::new());
|
|
ctx.add_singleton_model(UndoCloseStack::new);
|
|
ctx.add_singleton_model(|_| ToastStack);
|
|
ctx.add_singleton_model(|_| GlobalCodeReviewModel);
|
|
ctx.add_singleton_model(workspace::OneTimeModalModel::new);
|
|
ctx.add_singleton_model(
|
|
workspace::bonus_grant_notification_model::BonusGrantNotificationModel::new,
|
|
);
|
|
#[cfg(feature = "local_fs")]
|
|
ctx.add_singleton_model(FileModel::new);
|
|
ctx.add_singleton_model(GlobalBufferModel::new);
|
|
#[cfg(windows)]
|
|
ctx.add_singleton_model(util::traffic_lights::windows::RendererState::new);
|
|
#[cfg(feature = "local_fs")]
|
|
ctx.add_singleton_model(|_| LanguageServerShutdownManager::new());
|
|
|
|
#[cfg(feature = "voice_input")]
|
|
ctx.add_singleton_model(voice_input::VoiceInput::new);
|
|
ctx.add_singleton_model(|_| {
|
|
VoiceTranscriber::new(Arc::new(ServerVoiceTranscriber::new(server_api.clone())))
|
|
});
|
|
|
|
let notebooks = cloud_objects
|
|
.iter()
|
|
.filter_map(|object| {
|
|
let notebook: Option<&CloudNotebook> = object.into();
|
|
notebook
|
|
})
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut all_queue_items = Vec::new();
|
|
let objects_with_pending_changes = cloud_objects
|
|
.iter()
|
|
.filter(|object| {
|
|
object.metadata().has_pending_content_changes()
|
|
&& !matches!(
|
|
object.object_type(),
|
|
crate::cloud_object::ObjectType::GenericStringObject(
|
|
crate::cloud_object::GenericStringObjectFormat::Json(
|
|
crate::cloud_object::JsonObjectType::AIFact
|
|
)
|
|
)
|
|
)
|
|
})
|
|
.cloned()
|
|
.collect::<Vec<_>>();
|
|
all_queue_items.extend(QueueItem::from_cached_objects(
|
|
objects_with_pending_changes.into_iter(),
|
|
));
|
|
|
|
let cloud_model = ctx.add_singleton_model(|_ctx| {
|
|
CloudModel::new(
|
|
persistence_writer.sender(),
|
|
cloud_objects,
|
|
time_of_next_force_object_refresh,
|
|
)
|
|
});
|
|
|
|
let local_object_sender = persistence_writer.sender();
|
|
let legacy_profile_owner = UserWorkspaces::as_ref(ctx).personal_drive(ctx);
|
|
ctx.add_singleton_model(move |ctx| {
|
|
LocalObjectRepository::new(local_object_sender, legacy_profile_owner, ctx)
|
|
});
|
|
|
|
let unsynced_actions: Vec<(CloudObjectTypeAndId, ObjectAction)> = object_actions
|
|
.iter()
|
|
.filter(|action| action.is_pending())
|
|
.filter_map(|action| {
|
|
cloud_model.read(ctx, |model, _| {
|
|
let object = model.get_by_uid(&action.uid);
|
|
object.and_then(|object| {
|
|
let type_and_id = object.cloud_object_type_and_id();
|
|
if matches!(
|
|
type_and_id,
|
|
CloudObjectTypeAndId::GenericStringObject {
|
|
object_type: crate::cloud_object::GenericStringObjectFormat::Json(
|
|
crate::cloud_object::JsonObjectType::AIFact
|
|
),
|
|
..
|
|
}
|
|
) {
|
|
None
|
|
} else {
|
|
Some((type_and_id, action.clone()))
|
|
}
|
|
})
|
|
})
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
all_queue_items.extend(QueueItem::from_unsynced_actions(
|
|
unsynced_actions.into_iter(),
|
|
));
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
SyncQueue::new(
|
|
all_queue_items,
|
|
server_api_provider.as_ref(ctx).get_cloud_objects_client(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// Seed the orchestration pin set from persisted conversation data
|
|
// before the conversations vec is consumed by the singletons below.
|
|
// Each conversation's `AgentConversationData.pinned` is the source of
|
|
// truth; the singleton mirrors them in memory for fast cross-pane lookups.
|
|
let initial_pinned_conversations: HashSet<AIConversationId> = multi_agent_conversations
|
|
.iter()
|
|
.filter_map(|conv| {
|
|
let data =
|
|
serde_json::from_str::<AgentConversationData>(&conv.conversation.conversation_data)
|
|
.ok()?;
|
|
if !data.pinned {
|
|
return None;
|
|
}
|
|
AIConversationId::try_from(conv.conversation.conversation_id.clone()).ok()
|
|
})
|
|
.collect();
|
|
{
|
|
let conversations = &multi_agent_conversations;
|
|
ctx.add_singleton_model(move |_| BlocklistAIHistoryModel::new(ai_queries, conversations));
|
|
}
|
|
// Per-conversation queued prompts. Registered after the history model
|
|
// since it subscribes to history events for cleanup.
|
|
ctx.add_singleton_model(ai::blocklist::QueuedQueryModel::new);
|
|
// Cross-pane UI state for the orchestration pill bar. Registered
|
|
// after the history model since it subscribes to history events.
|
|
ctx.add_singleton_model(move |ctx| {
|
|
ai::blocklist::agent_view::orchestration_pill_bar_model::OrchestrationPillBarModel::new(
|
|
initial_pinned_conversations,
|
|
ctx,
|
|
)
|
|
});
|
|
ctx.add_singleton_model(move |_| RestoredAgentConversations::new(multi_agent_conversations));
|
|
ctx.add_singleton_model(|_| CLIAgentSessionsModel::new());
|
|
// ActiveAgentViewsModel is used to track active agent conversations and notify listeners when they change.
|
|
ctx.add_singleton_model(|_| ActiveAgentViewsModel::new());
|
|
ctx.add_singleton_model(AgentNotificationsModel::new);
|
|
ctx.add_singleton_model(BlocklistAIPermissions::new);
|
|
ctx.add_singleton_model(ai::blocklist::orchestration_events::OrchestrationEventService::new);
|
|
ctx.add_singleton_model(
|
|
ai::blocklist::local_agent_task_sync_model::LocalAgentTaskSyncModel::new,
|
|
);
|
|
ctx.add_singleton_model(
|
|
ai::blocklist::orchestration_event_streamer::OrchestrationEventStreamer::new,
|
|
);
|
|
|
|
if launch_mode.supports_indexing() {
|
|
ctx.add_singleton_model(RepoOutlines::new);
|
|
} else {
|
|
ctx.add_singleton_model(|ctx| RepoOutlines::new_with_indexing_enabled(false, ctx));
|
|
}
|
|
#[cfg(all(feature = "local_fs", not(target_family = "wasm")))]
|
|
if matches!(
|
|
launch_mode,
|
|
LaunchMode::App { .. } | LaunchMode::CommandLine { .. } | LaunchMode::Test { .. }
|
|
) {
|
|
ctx.add_singleton_model(::ai::index::local_project_index::LocalProjectIndexManager::new);
|
|
}
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
galaxy_core::sync_queue::SyncQueue::<SyncTask>::new_with_rate_limit(
|
|
&ctx.background_executor(),
|
|
Some(DEFAULT_SYNC_REQUESTS_PER_MIN),
|
|
)
|
|
});
|
|
|
|
ctx.add_singleton_model(|_| UserProfiles::new(restored_user_profiles));
|
|
|
|
ctx.add_singleton_model(|_| ObjectActions::new(object_actions));
|
|
|
|
ctx.add_singleton_model(|_| AudibleBell::new());
|
|
|
|
// This model has to be registered after the user workspaces model because it relies on it,
|
|
// and before the UpdateManager models because they rely on the TeamTester model.
|
|
ctx.add_singleton_model(TeamTesterStatus::new);
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
TeamUpdateManager::new(
|
|
server_api_provider.as_ref(ctx).get_team_client(),
|
|
persistence_writer.sender(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
UpdateManager::new(
|
|
persistence_writer.sender(),
|
|
server_api_provider.as_ref(ctx).get_cloud_objects_client(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
let toml_file_path = settings::user_preferences_toml_file_path();
|
|
ctx.add_singleton_model(move |ctx| {
|
|
initialize_cloud_preferences_syncer(
|
|
toml_file_path,
|
|
startup_toml_parse_error_for_syncer.as_deref(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// LogManager must be registered before any subsystem (e.g. MCP, LSP) that creates file-based loggers.
|
|
ctx.add_singleton_model(|_| simple_logger::manager::LogManager::new());
|
|
|
|
let running_mcp_servers = app_state
|
|
.as_ref()
|
|
.map(|app_state| app_state.running_mcp_servers.as_slice())
|
|
.unwrap_or(&[]);
|
|
|
|
// FileMCPWatcher must be registered before FileBasedMCPManager, which subscribes to it.
|
|
ctx.add_singleton_model(FileMCPWatcher::new);
|
|
ctx.add_singleton_model(FileBasedMCPManager::new);
|
|
|
|
// TemplatableMCPServerManager must be registered after UpdateManager and MCPServerManager so it can migrate legacy MCPs on start up
|
|
// It should also be registered after FileBasedMCPManager so it can receive file-based server updates.
|
|
ctx.add_singleton_model(|ctx| {
|
|
TemplatableMCPServerManager::new(
|
|
persisted_mcp_server_installations,
|
|
mcp_servers_to_restore,
|
|
running_mcp_servers,
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// MCPGalleryManager subscribes to UpdateManager so that it can be notified when gallery items are updated.
|
|
// The registration of this singleton must be after UpdateManager is registered.
|
|
ctx.add_singleton_model(MCPGalleryManager::new);
|
|
|
|
// SkillManager is used to cache SKILL.md files for all active terminal views and their working directories
|
|
ctx.add_singleton_model(SkillManager::new);
|
|
|
|
// CloudViewModel subscribes to UpdateManager so that it can be notified when objects are
|
|
// created on the server.
|
|
ctx.add_singleton_model(CloudViewModel::new);
|
|
|
|
// AIDocumentModel subscribes to UpdateManager so that it can be notified when notebooks are created on the server.
|
|
ctx.add_singleton_model(AIDocumentModel::new);
|
|
|
|
// AgentConversationsModel subscribes to UpdateManager for RTC task updates.
|
|
ctx.add_singleton_model(AgentConversationsModel::new);
|
|
|
|
// ByoLlmAuthBannerSessionState tracks dismissal of the BYO LLM auth banner (e.g., AWS Bedrock login).
|
|
ctx.add_singleton_model(ByoLlmAuthBannerSessionState::new);
|
|
|
|
ctx.add_singleton_model(ExportManager::new);
|
|
ctx.add_singleton_model(|ctx| NotebookManager::new(notebooks, ctx));
|
|
ctx.add_singleton_model(|_| CodeManager::default());
|
|
ctx.add_singleton_model(|_| OpenedFilesModel::new());
|
|
ctx.add_singleton_model(NotebookKeybindings::new);
|
|
ctx.add_singleton_model(TerminalKeybindings::new);
|
|
ctx.add_singleton_model(|_| ActiveSession::default());
|
|
ctx.add_singleton_model(|ctx| {
|
|
Listener::new(
|
|
server_api_provider.as_ref(ctx).get_cloud_objects_client(),
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
#[cfg(all(not(target_family = "wasm"), feature = "local_tty"))]
|
|
{
|
|
ctx.add_singleton_model(LocalShellState::new);
|
|
ctx.add_singleton_model(system::SystemInfo::new);
|
|
}
|
|
|
|
// `IapManager` drives gcloud-based IAP token refresh for staging builds.
|
|
// Register it after `LocalShellState`: the Manager needs to know where the gcloud
|
|
// cli lives & thus needs PATH config set by ~/.zshrc et al.
|
|
//
|
|
// Registered on all targets (including wasm) so consumers such as the
|
|
// shared-session viewer network — which compiles and runs on wasm — can
|
|
// read the singleton without panicking. On wasm `iap_state` is always
|
|
// `None`, making this an inert no-op: `IapManager::new` early-returns from
|
|
// its refresh loop and `iap_state()` yields no proxy-auth header.
|
|
ctx.add_singleton_model(move |ctx| {
|
|
let path_resolver = Box::new(|_ctx: &mut AppContext| {
|
|
#[cfg(feature = "local_tty")]
|
|
let path_future = LocalShellState::handle(_ctx).update(_ctx, |shell_state, ctx| {
|
|
shell_state.get_interactive_path_env_var(ctx)
|
|
});
|
|
|
|
#[cfg(not(feature = "local_tty"))]
|
|
let path_future =
|
|
futures::FutureExt::boxed_local(futures::future::ready(None::<String>));
|
|
|
|
path_future
|
|
});
|
|
IapManager::new(iap_state, path_resolver, ctx)
|
|
});
|
|
// Subscribe to IAP manager events to show toasts when refresh fails.
|
|
ctx.subscribe_to_model(&IapManager::handle(ctx), |_, e, ctx| {
|
|
match e {
|
|
IapManagerEvent::RefreshFailed {
|
|
message,
|
|
is_first_failure_of_streak,
|
|
} if *is_first_failure_of_streak => {
|
|
let window_id = ctx
|
|
.windows()
|
|
.active_window()
|
|
.or_else(|| ctx.windows().ordered_window_ids().first().copied());
|
|
let Some(window_id) = window_id else {
|
|
return;
|
|
};
|
|
let toast: DismissibleToast<WorkspaceAction> =
|
|
DismissibleToast::error(format!("IAP credential refresh failed: {message}"));
|
|
ToastStack::handle(ctx).update(ctx, |stack, ctx| {
|
|
stack.add_ephemeral_toast(toast, window_id, ctx);
|
|
});
|
|
}
|
|
_ => {}
|
|
};
|
|
});
|
|
|
|
// Add a singleton model that holds the current prompt configuration.
|
|
ctx.add_singleton_model(Prompt::new);
|
|
|
|
// Add a singleton model for resizable modals whose size should be persisted through restarts.
|
|
ctx.add_singleton_model(|_| ResizableData::default());
|
|
|
|
// Add a singleton model to maintain state of shared session across all windows.
|
|
ctx.add_singleton_model(terminal::shared_session::manager::Manager::new);
|
|
|
|
ctx.add_singleton_model(
|
|
terminal::shared_session::permissions_manager::SessionPermissionsManager::new,
|
|
);
|
|
|
|
ctx.add_singleton_model(EnvVarCollectionManager::new);
|
|
ctx.add_singleton_model(|_| WorkflowManager::new());
|
|
|
|
if FeatureFlag::ScheduledAmbientAgents.is_enabled() {
|
|
ctx.add_singleton_model(ScheduledAgentManager::new);
|
|
}
|
|
|
|
AutoupdateState::register(ctx, server_api.clone());
|
|
|
|
ctx.add_singleton_model(LocalWorkflows::new);
|
|
|
|
ctx.add_singleton_model(LLMPreferences::new);
|
|
ctx.add_singleton_model(HarnessAvailabilityModel::new);
|
|
ctx.add_singleton_model(ConnectedSelfHostedWorkersModel::new);
|
|
|
|
// Seed predefined rules on first launch if no global rules exist.
|
|
// This runs after the local object repository is initialized.
|
|
seed_predefined_rules_if_needed(ctx);
|
|
|
|
let tip_model_handle = ctx.add_singleton_model(|ctx| {
|
|
ai::agent_tips::AITipModel::<ai::AgentTip>::new_for_agent_tips(ctx)
|
|
});
|
|
{
|
|
// Rebuild the tip pool when AI settings change so tips whose applicability
|
|
// depends on AI settings appear/disappear without waiting for the next cooldown cycle.
|
|
let tip_model_handle_for_ai = tip_model_handle.clone();
|
|
ctx.subscribe_to_model(&AISettings::handle(ctx), move |_, _, ctx| {
|
|
tip_model_handle_for_ai.update(ctx, |model, ctx| {
|
|
model.revalidate_tips(ctx);
|
|
});
|
|
});
|
|
// Also revalidate when workspace/team data changes (e.g. voice toggled at
|
|
// the org level). Billing metadata — including `warp_ai_policy.is_voice_enabled`
|
|
// — lives inside the team data, so `TeamsChanged` covers all policy updates.
|
|
let tip_model_handle_for_teams = tip_model_handle.clone();
|
|
ctx.subscribe_to_model(&UserWorkspaces::handle(ctx), move |_, event, ctx| {
|
|
if matches!(event, UserWorkspacesEvent::TeamsChanged) {
|
|
tip_model_handle_for_teams.update(ctx, |model, ctx| {
|
|
model.revalidate_tips(ctx);
|
|
});
|
|
}
|
|
});
|
|
// Revalidate when any keybinding changes so tips with `<keybinding>`
|
|
// placeholders are hidden/shown when the referenced binding is cleared
|
|
// or reassigned.
|
|
ctx.subscribe_to_model(&KeybindingChangedNotifier::handle(ctx), move |_, _, ctx| {
|
|
tip_model_handle.update(ctx, |model, ctx| {
|
|
model.revalidate_tips(ctx);
|
|
});
|
|
});
|
|
}
|
|
|
|
timer.mark_interval_end("SINGLETON_MODELS_REGISTERED");
|
|
|
|
ctx.add_singleton_model(move |_| timer);
|
|
|
|
ctx.add_singleton_model(|ctx| AIExecutionProfilesModel::new(launch_mode, ctx));
|
|
|
|
ctx.add_singleton_model(DefaultTerminal::new);
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
let legacy_indexing_enabled = matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|
|
|| !cfg!(all(feature = "local_fs", not(target_family = "wasm")));
|
|
let should_restore_indices = legacy_indexing_enabled
|
|
&& launch_mode.supports_indexing()
|
|
&& (matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. })
|
|
|| UserWorkspaces::as_ref(ctx).is_codebase_context_enabled(ctx));
|
|
let indices_to_restore = if should_restore_indices {
|
|
persisted_workspaces.clone()
|
|
} else {
|
|
vec![]
|
|
};
|
|
|
|
let codebase_limits = AIRequestUsageModel::as_ref(ctx).codebase_context_limits();
|
|
let mut codebase_index_config = CodebaseIndexManagerConfig::new(
|
|
indices_to_restore,
|
|
codebase_limits.max_indices_allowed,
|
|
codebase_limits.max_files_per_repo,
|
|
codebase_limits.embedding_generation_batch_size,
|
|
server_api_provider.as_ref(ctx).get(),
|
|
launch_mode.supports_indexing() && legacy_indexing_enabled,
|
|
);
|
|
if matches!(launch_mode, LaunchMode::RemoteServerDaemon { .. }) {
|
|
codebase_index_config = codebase_index_config.defer_persisted_index_restore();
|
|
}
|
|
#[cfg(feature = "local_fs")]
|
|
if let Some(snapshot_storage) = daemon_codebase_index_snapshot_storage(launch_mode) {
|
|
return CodebaseIndexManager::new_with_snapshot_storage(
|
|
codebase_index_config,
|
|
Some(snapshot_storage),
|
|
ctx,
|
|
);
|
|
}
|
|
|
|
CodebaseIndexManager::new_with_config(codebase_index_config, ctx)
|
|
});
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
ProjectContextModel::new_from_persisted(
|
|
persisted_project_rules,
|
|
read_project_rule_contents,
|
|
ctx,
|
|
)
|
|
});
|
|
|
|
// Index global rules (e.g. ~/.agents/AGENTS.md) on a background task so
|
|
// they are available to subsequent agent queries.
|
|
ProjectContextModel::handle(ctx).update(ctx, |me, ctx| me.index_global_rules(ctx));
|
|
#[cfg(all(not(target_family = "wasm"), feature = "local_fs"))]
|
|
{
|
|
ctx.add_singleton_model(ai::remote_agent_context::RemoteAgentContext::new);
|
|
}
|
|
|
|
ctx.add_singleton_model(|ctx| {
|
|
PersistedWorkspace::new(
|
|
persisted_workspaces,
|
|
workspace_language_servers,
|
|
persistence_writer.sender(),
|
|
ctx,
|
|
)
|
|
});
|
|
ctx.add_singleton_model(move |_| persistence_writer);
|
|
|
|
ctx.add_singleton_model(input_classifier::InputClassifierModel::new);
|
|
|
|
ctx.add_singleton_model(move |_| IgnoredSuggestionsModel::new(persisted_ignored_suggestions));
|
|
|
|
// Subscribe WorkflowAliases to the UpdateManager so that it can be notified when objects are
|
|
// trashed.
|
|
WorkflowAliases::handle(ctx).update(ctx, |aliases, ctx| {
|
|
aliases.connect(ctx);
|
|
});
|
|
|
|
// When running natively, add the http server singleton to the application.
|
|
#[cfg(not(target_family = "wasm"))]
|
|
ctx.add_singleton_model(move |ctx| {
|
|
let routers = vec![
|
|
app_installation_detection::make_router(),
|
|
profiling::make_router(),
|
|
];
|
|
http_server::HttpServer::new(routers, ctx)
|
|
});
|
|
#[cfg(feature = "local_fs")]
|
|
if matches!(
|
|
launch_mode,
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. }
|
|
) && FeatureFlag::GalaxyControlCli.is_enabled()
|
|
{
|
|
ctx.add_singleton_model(local_control::LocalControlBridge::new);
|
|
ctx.add_singleton_model(local_control::LocalControlServer::new);
|
|
}
|
|
|
|
app_state
|
|
}
|
|
|
|
pub(crate) fn app_callbacks(
|
|
is_integration_test: bool,
|
|
mut tracing_initialization: Option<tracing::Initialization>,
|
|
) -> galaxyui::platform::AppCallbacks {
|
|
galaxyui::platform::AppCallbacks {
|
|
on_internet_reachability_changed: Some(Box::new(move |reachable, ctx| {
|
|
NetworkStatus::handle(ctx)
|
|
.update(ctx, move |me, ctx| me.reachability_changed(reachable, ctx));
|
|
})),
|
|
on_become_active: Some(Box::new(move |ctx| {
|
|
let auth_state = AuthStateProvider::as_ref(ctx).get();
|
|
ctx.record_app_focus(
|
|
auth_state.user_id().map(|uid| uid.as_string()),
|
|
auth_state.anonymous_id(),
|
|
);
|
|
})),
|
|
on_screen_changed: Some(Box::new(move |ctx| {
|
|
ctx.dispatch_global_action(
|
|
"root_view:move_quake_mode_window_from_screen_change",
|
|
&KeysSettings::as_ref(ctx)
|
|
.quake_mode_settings
|
|
.value()
|
|
.clone(),
|
|
);
|
|
|
|
let new_display_count = ctx.windows().display_count();
|
|
DisplayCount::handle(ctx).update(ctx, |display_count, ctx| {
|
|
display_count.0 = new_display_count;
|
|
ctx.notify();
|
|
});
|
|
})),
|
|
on_cpu_awakened: Some(Box::new(move |ctx| {
|
|
SystemStats::handle(ctx).update(ctx, move |system, ctx| {
|
|
log::info!("System has returned from sleep");
|
|
system.dispatch_cpu_was_awakened(ctx);
|
|
});
|
|
})),
|
|
on_cpu_will_sleep: Some(Box::new(move |ctx| {
|
|
SystemStats::handle(ctx).update(ctx, move |system, ctx| {
|
|
log::info!("System is going to sleep...");
|
|
system.dispatch_cpu_will_sleep(ctx);
|
|
});
|
|
})),
|
|
on_resigned_active: Some(Box::new(move |ctx| {
|
|
let active_window_id = ctx.windows().active_window();
|
|
let update_quake_mode_arg = UpdateQuakeModeEventArg { active_window_id };
|
|
|
|
#[cfg(feature = "voice_input")]
|
|
{
|
|
if let voice_input::VoiceInputState::Listening { enabled_from, .. } =
|
|
voice_input::VoiceInput::as_ref(ctx).state()
|
|
{
|
|
// Abort the voice input if it's toggled from a key press, as we cannot listen to key events
|
|
// if the user is focused on a different app - we could miss the release of the key.
|
|
if matches!(
|
|
*enabled_from,
|
|
voice_input::VoiceInputToggledFrom::Key { .. }
|
|
) {
|
|
ctx.dispatch_global_action("root_view:abort_voice_input", &());
|
|
}
|
|
}
|
|
}
|
|
ctx.dispatch_global_action("root_view:update_quake_mode_state", &update_quake_mode_arg);
|
|
|
|
let auth_state = AuthStateProvider::as_ref(ctx).get();
|
|
ctx.record_app_blur(
|
|
auth_state.user_id().map(|uid| uid.as_string()),
|
|
auth_state.anonymous_id(),
|
|
);
|
|
})),
|
|
on_will_terminate: Some(Box::new(move |ctx| {
|
|
// Persist the final app state before tearing down the writer.
|
|
// This ensures the latest session (tabs, CWD, conversations) is saved
|
|
// even if the termination bypassed individual window-close events.
|
|
workspace::save_app_urgently(ctx);
|
|
|
|
NotebookManager::handle(ctx).update(ctx, |manager, ctx| {
|
|
// Notebooks are only saved periodically, so ensure that any pending changes have
|
|
// been sent to the writer thread before terminating.
|
|
manager.close_notebooks(ctx);
|
|
});
|
|
|
|
PersistenceWriter::handle(ctx).update(ctx, |writer, _ctx| {
|
|
writer.terminate();
|
|
});
|
|
|
|
let auth_state = AuthStateProvider::as_ref(ctx).get();
|
|
ctx.try_record_daily_app_focus_duration(
|
|
auth_state.user_id().map(|uid| uid.as_string()),
|
|
auth_state.anonymous_id(),
|
|
);
|
|
// Shutdown all LSP servers gracefully before app termination
|
|
lsp::LspManagerModel::handle(ctx).update(ctx, |manager, ctx| {
|
|
manager.terminate(ctx);
|
|
});
|
|
|
|
// We want to tear down the terminal server before relaunching for
|
|
// autoupdate, to ensure we're not running any extra Warp processes
|
|
// when we bring up the new process. Additionally, this must occur
|
|
// after terminating the persistence writer, so we don't keep track
|
|
// of the fact that the shell sessions terminated.
|
|
#[cfg(feature = "local_tty")]
|
|
terminal::local_tty::spawner::PtySpawner::handle(ctx).update(ctx, |pty_spawner, _| {
|
|
pty_spawner.prepare_for_app_termination();
|
|
});
|
|
|
|
#[cfg(all(feature = "local_tty", windows))]
|
|
terminal::local_tty::shutdown_all_pty_event_loops(ctx);
|
|
|
|
// Tear down app services before spawning the new process, to
|
|
// ensure that the new process doesn't find the old process while
|
|
// attempting to enforce our single-instance policy on Linux.
|
|
app_services::teardown(ctx);
|
|
autoupdate::spawn_child_if_necessary(ctx);
|
|
|
|
// Tear down any application profilers that are running, writing
|
|
// results to disk.
|
|
profiling::teardown();
|
|
|
|
#[cfg(enable_crash_recovery)]
|
|
crash_recovery::CrashRecovery::handle(ctx).update(ctx, |crash_recovery, _ctx| {
|
|
crash_recovery.teardown();
|
|
});
|
|
if let Some(initialization) = tracing_initialization.as_mut() {
|
|
initialization.shutdown();
|
|
}
|
|
})),
|
|
on_should_close_window: Some(Box::new(move |window_id, ctx| {
|
|
let general_settings = GeneralSettings::as_ref(ctx);
|
|
// On Linux or Windows, if we're about to close the final window, we should quit the app instead.
|
|
// On Mac, we do this conditionally based on a user setting.
|
|
let quit_on_last_window_closed =
|
|
cfg!(any(target_os = "linux", target_os = "freebsd", windows))
|
|
|| *general_settings.quit_on_last_window_closed;
|
|
if ctx.window_ids().count() == 1 && quit_on_last_window_closed {
|
|
log::info!("No windows left, terminating app");
|
|
ctx.terminate_app(TerminationMode::Cancellable, None);
|
|
return ApproveTerminateResult::Cancel;
|
|
}
|
|
|
|
let summary = UnsavedStateSummary::for_window(window_id, ctx);
|
|
|
|
send_telemetry_from_app_ctx!(
|
|
TelemetryEvent::UserInitiatedClose {
|
|
initiated_on: CloseTarget::Window,
|
|
},
|
|
ctx
|
|
);
|
|
|
|
// Don't show dialog on integration test. Machine can't press buttons.
|
|
if !is_integration_test && summary.should_display_warning(ctx) {
|
|
let shown = summary
|
|
.dialog()
|
|
.on_confirm(move |ctx| {
|
|
ctx.windows()
|
|
.close_window(window_id, TerminationMode::ForceTerminate);
|
|
})
|
|
.on_cancel(move |ctx| {
|
|
on_close_window_cancelled(window_id, false, ctx);
|
|
})
|
|
.on_show_processes(move |ctx| {
|
|
on_close_window_cancelled(window_id, true, ctx);
|
|
})
|
|
.show(ctx);
|
|
if shown {
|
|
ApproveTerminateResult::Cancel
|
|
} else {
|
|
ApproveTerminateResult::Terminate
|
|
}
|
|
} else {
|
|
ApproveTerminateResult::Terminate
|
|
}
|
|
})),
|
|
on_should_terminate_app: Some(Box::new(move |source, ctx| {
|
|
// Never interrupt a system-initiated termination (logout / restart /
|
|
// scheduled OS update): both cancel paths below return
|
|
// `ApproveTerminateResult::Cancel`, which macOS interprets as Warp
|
|
// refusing to quit. That can abort a scheduled OS update while the
|
|
// quit-warning modal has no visible window to attach to, leaving
|
|
// Warp waiting on a prompt nobody can see (#12441). Skipping
|
|
// `apply_pending_update` here doesn't lose the update: the next
|
|
// update check re-detects it (autoupdate state isn't persisted
|
|
// across restarts, so the artifact may be re-downloaded).
|
|
if source == TerminationRequestSource::System {
|
|
return ApproveTerminateResult::Terminate;
|
|
}
|
|
|
|
send_telemetry_from_app_ctx!(
|
|
TelemetryEvent::UserInitiatedClose {
|
|
initiated_on: CloseTarget::App,
|
|
},
|
|
ctx
|
|
);
|
|
|
|
// If there's a pending autoupdate, apply that before showing the unsaved changes
|
|
// dialog. We apply the update first so that the dialog can force-terminate.
|
|
let applying_update = autoupdate::apply_pending_update(ctx, |ctx| {
|
|
// Once the deferred update is applied, re-terminate the app. This termination is
|
|
// cancellable so that we still show the unsaved changes dialog.
|
|
log::info!("Deferred autoupdate applied, terminating app");
|
|
ctx.terminate_app(TerminationMode::Cancellable, None);
|
|
});
|
|
if applying_update {
|
|
return ApproveTerminateResult::Cancel;
|
|
}
|
|
|
|
let summary = UnsavedStateSummary::for_app(ctx);
|
|
// Don't show dialog on integration test. Machine can't press buttons.
|
|
if !is_integration_test && summary.should_display_warning(ctx) {
|
|
let shown = summary
|
|
.dialog()
|
|
.on_confirm(|ctx| ctx.terminate_app(TerminationMode::ForceTerminate, None))
|
|
.on_show_processes(|ctx| on_close_app_cancelled(true, ctx))
|
|
.on_cancel(|ctx| on_close_app_cancelled(false, ctx))
|
|
.show(ctx);
|
|
if shown {
|
|
return ApproveTerminateResult::Cancel;
|
|
}
|
|
}
|
|
|
|
ApproveTerminateResult::Terminate
|
|
})),
|
|
on_disable_warning_modal: Some(Box::new(move |ctx| {
|
|
GeneralSettings::handle(ctx).update(ctx, |general_settings, ctx| {
|
|
report_if_error!(general_settings
|
|
.show_warning_before_quitting
|
|
.toggle_and_save_value(ctx));
|
|
});
|
|
send_telemetry_from_app_ctx!(TelemetryEvent::QuitModalDisabled, ctx);
|
|
})),
|
|
on_notification_clicked: Some(Box::new(move |notification_response, ctx| {
|
|
if let Some(notification_data) = notification_response.data() {
|
|
let context: serde_json::Result<NotificationContext> =
|
|
serde_json::from_str(notification_data);
|
|
if let Ok(NotificationContext::BlockOrigin {
|
|
window_id,
|
|
pane_group_id,
|
|
pane_id,
|
|
}) = context
|
|
{
|
|
// Ensure the window ID exists, if so dispatch an action to focus
|
|
// the correct pane.
|
|
if ctx.window_ids().contains(&window_id) {
|
|
if let Some(root_view_id) = ctx.root_view_id(window_id) {
|
|
ctx.dispatch_action(
|
|
window_id,
|
|
&[root_view_id],
|
|
"root_view:handle_notification_click",
|
|
&PaneViewLocator {
|
|
pane_group_id,
|
|
pane_id,
|
|
},
|
|
log::Level::Info,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})),
|
|
on_new_window_requested: Some(Box::new(move |ctx| {
|
|
// This one is called when the app is requested to open a new window,
|
|
// e.g. clicking on the Dock icon. It is NOT called from the New Window
|
|
// menu item.
|
|
App::record_last_active_timestamp();
|
|
ctx.dispatch_global_action("root_view:open_new", &());
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
})),
|
|
on_open_urls: Some(Box::new(move |urls, ctx| {
|
|
for url in &urls {
|
|
let parsed_url = Url::parse(url);
|
|
match parsed_url {
|
|
Ok(url) => uri::handle_incoming_uri(&url, ctx),
|
|
Err(e) => log::warn!("Unable to parse received url: {e}"),
|
|
}
|
|
}
|
|
})),
|
|
on_os_appearance_changed: Some(Box::new(move |ctx| {
|
|
AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| {
|
|
appearance_manager.refresh_theme_state(ctx);
|
|
});
|
|
})),
|
|
on_active_window_changed: Some(Box::new(move |ctx| {
|
|
let windowing_model = ctx.windows();
|
|
let active_window_id = windowing_model.active_window();
|
|
let key_window_is_modal_panel = windowing_model.key_window_is_modal_panel();
|
|
|
|
if !key_window_is_modal_panel {
|
|
let update_quake_mode_arg = UpdateQuakeModeEventArg { active_window_id };
|
|
ctx.dispatch_global_action(
|
|
"root_view:update_quake_mode_state",
|
|
&update_quake_mode_arg,
|
|
);
|
|
}
|
|
|
|
if let Some(active_window_id) = active_window_id {
|
|
OneTimeModalModel::handle(ctx).update(ctx, |model, ctx| {
|
|
model.update_target_window_id(active_window_id, ctx);
|
|
});
|
|
}
|
|
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
})),
|
|
on_window_will_close: Some(Box::new(move |closed_window_data, ctx| {
|
|
if ctx.windows().stage() == ApplicationStage::Terminating {
|
|
return;
|
|
}
|
|
|
|
if let Some(window_data) = closed_window_data {
|
|
UndoCloseStack::handle(ctx).update(ctx, |stack, ctx| {
|
|
stack.handle_window_closed(window_data, ctx);
|
|
});
|
|
}
|
|
workspace::save_app_urgently(ctx);
|
|
})),
|
|
on_window_moved: Some(Box::new(move |ctx| {
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
})),
|
|
on_window_resized: Some(Box::new(move |ctx| {
|
|
ctx.dispatch_global_action("workspace:save_app", &());
|
|
})),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
/// Focuses the active window or if there isn't one then a window with a running process
|
|
/// and then shows the native modal.
|
|
fn focus_running_window_and_show_native_modal(
|
|
sessions_summary: RunningSessionSummary,
|
|
dialog_with_callbacks: AlertDialogWithCallbacks<AppModalCallback>,
|
|
ctx: &mut AppContext,
|
|
) {
|
|
let windowing_model = ctx.windows();
|
|
let active_window_id = windowing_model.active_window();
|
|
// Show the nav palette in the active window. If there is no active window,
|
|
// arbitrarily pick one of the windows having a running process.
|
|
let window_id_to_focus = active_window_id.unwrap_or_else(|| {
|
|
*sessions_summary
|
|
.windows_running()
|
|
.iter()
|
|
.next()
|
|
.expect("already checked len > 0")
|
|
});
|
|
ctx.windows().show_window_and_focus_app(window_id_to_focus);
|
|
if let Some(workspaces) = ctx.views_of_type::<Workspace>(window_id_to_focus) {
|
|
if let Some(handle) = workspaces.first() {
|
|
handle.update(ctx, |view, ctx| {
|
|
view.show_native_modal(dialog_with_callbacks, ctx);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_close_app_cancelled(open_navigation_palette: bool, ctx: &mut AppContext) {
|
|
autoupdate::cancel_relaunch(ctx);
|
|
|
|
send_telemetry_from_app_ctx!(
|
|
TelemetryEvent::QuitModalCancel {
|
|
nav_palette: open_navigation_palette,
|
|
modal_for: CloseTarget::App,
|
|
},
|
|
ctx
|
|
);
|
|
|
|
let sessions = SessionNavigationData::all_sessions(ctx).collect_vec();
|
|
let sessions_summary = RunningSessionSummary::new(&sessions);
|
|
|
|
// If open_navigation_palette is false, return early. Otherwise, we honor the open_navigation_palette
|
|
// param which is true if the user clicked the modal button for that. However, if the running
|
|
// processes in this window have finished since the modal popped, there is nothing to do now and we
|
|
// can return early
|
|
if !open_navigation_palette || sessions_summary.long_running_cmds.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let windowing_model = ctx.windows();
|
|
let active_window_id = windowing_model.active_window();
|
|
// show the nav palette in the active window. if there is no active window,
|
|
// arbitrarily pick one of the windows having a running process
|
|
let window_id_to_focus = active_window_id.unwrap_or_else(|| {
|
|
*sessions_summary
|
|
.windows_running()
|
|
.iter()
|
|
.next()
|
|
.expect("already checked len > 0")
|
|
});
|
|
|
|
windowing_model.show_window_and_focus_app(window_id_to_focus);
|
|
|
|
// open the nav palette in the selected window
|
|
if let Some(workspaces) = ctx.views_of_type::<Workspace>(window_id_to_focus) {
|
|
if let Some(handle) = workspaces.first() {
|
|
ctx.dispatch_typed_action_for_view(
|
|
window_id_to_focus,
|
|
handle.id(),
|
|
&WorkspaceAction::OpenPalette {
|
|
mode: PaletteMode::Navigation,
|
|
source: PaletteSource::QuitModal,
|
|
query: Some("running".to_owned()),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn on_close_window_cancelled(
|
|
window_id: WindowId,
|
|
open_navigation_palette: bool,
|
|
ctx: &mut AppContext,
|
|
) {
|
|
send_telemetry_from_app_ctx!(
|
|
TelemetryEvent::QuitModalCancel {
|
|
nav_palette: open_navigation_palette,
|
|
modal_for: CloseTarget::Window,
|
|
},
|
|
ctx
|
|
);
|
|
|
|
let sessions = SessionNavigationData::all_sessions(ctx).collect_vec();
|
|
let sessions_summary = RunningSessionSummary::new(&sessions);
|
|
let num_processes_in_window = sessions_summary.processes_in_window(&window_id).len();
|
|
|
|
// If open_navigation_palette is false, return early. Otherwise, we honor the
|
|
// open_navigation_palette param which is true if the user clicked the modal
|
|
// button for that. However, if the running processes in this window have finished
|
|
// since the modal popped, there is nothing to do now and we can return early
|
|
if !open_navigation_palette || num_processes_in_window == 0 {
|
|
return;
|
|
}
|
|
|
|
ctx.windows().show_window_and_focus_app(window_id);
|
|
|
|
// if we haven't returned early, it means open_navigation_palette is true as the
|
|
// user pressed the modal button for opening the navigation palette to show their
|
|
// running processes
|
|
if let Some(workspaces) = ctx.views_of_type::<Workspace>(window_id) {
|
|
if let Some(handle) = workspaces.first() {
|
|
ctx.dispatch_typed_action_for_view(
|
|
window_id,
|
|
handle.id(),
|
|
&WorkspaceAction::OpenPalette {
|
|
mode: PaletteMode::Navigation,
|
|
source: PaletteSource::QuitModal,
|
|
query: Some("running".to_owned()),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn is_cloud_agent_web_home_launch_url(url: &Url) -> bool {
|
|
url.scheme() == ChannelState::url_scheme()
|
|
&& url.host_str() == Some("action")
|
|
&& url.path() == "/new_cloud_agent_conversation"
|
|
&& url
|
|
.query_pairs()
|
|
.any(|(key, value)| key == "source" && value == "web_home")
|
|
}
|
|
|
|
#[::tracing::instrument(skip_all, fields(tags.cloud_agent = true))]
|
|
fn launch(ctx: &mut galaxyui::AppContext, app_state: Option<AppState>, launch_mode: LaunchMode) {
|
|
IntervalTimer::handle(ctx).update(ctx, |timer, _ctx| {
|
|
timer.mark_interval_end("APP_LAUNCHED");
|
|
});
|
|
|
|
keyboard::load_custom_keybindings(ctx);
|
|
|
|
IntervalTimer::handle(ctx).update(ctx, |timer, _ctx| {
|
|
timer.mark_interval_end("KEYBINDINGS_LOADED");
|
|
});
|
|
|
|
// For now, we only specify application-level fallback fonts on web.
|
|
#[cfg(target_family = "wasm")]
|
|
ctx.set_fallback_font_fn(font_fallback::fallback_font_fn);
|
|
|
|
match launch_mode {
|
|
// The TUI front-end runs its own mount in the run closure and returns
|
|
// before reaching launch().
|
|
LaunchMode::Tui { .. } => unreachable!("LaunchMode::Tui is handled before launch()"),
|
|
LaunchMode::App { .. } | LaunchMode::Test { .. } => {
|
|
let should_skip_restore = launch_mode
|
|
.args()
|
|
.urls
|
|
.iter()
|
|
.any(is_cloud_agent_web_home_launch_url);
|
|
let app_state = if should_skip_restore { None } else { app_state };
|
|
// Attempt to restore windows from the persisted application state.
|
|
let arg = OpenFromRestoredArg { app_state };
|
|
ctx.dispatch_global_action("root_view:open_from_restored", &arg);
|
|
|
|
// Process any URLs that were provided on the command line (which may be
|
|
// file:// URLs or ones using our custom URL scheme).
|
|
for url in launch_mode.args().urls.iter() {
|
|
uri::handle_incoming_uri(url, ctx);
|
|
}
|
|
|
|
// If, after session restoration and command-line argument handling, we
|
|
// haven't opened any windows, open a new window.
|
|
if ctx.window_ids().count() == 0 {
|
|
ctx.dispatch_global_action("root_view:open_new", &());
|
|
}
|
|
|
|
IntervalTimer::handle(ctx).update(ctx, |timer, _| {
|
|
timer.mark_interval_end("WINDOWS_CREATED");
|
|
});
|
|
|
|
// TODO(ben): We should skip this for LaunchMode::Test.
|
|
#[cfg(any(target_os = "macos", target_os = "windows"))]
|
|
{
|
|
use crate::login_item::maybe_register_app_as_login_item;
|
|
use crate::terminal::general_settings::GeneralSettingsChangedEvent;
|
|
// Note that we put this here because it depends on settings already having been initialized.
|
|
ctx.subscribe_to_model(&GeneralSettings::handle(ctx), |_, event, ctx| {
|
|
if matches!(event, GeneralSettingsChangedEvent::LoginItem { .. }) {
|
|
maybe_register_app_as_login_item(ctx);
|
|
}
|
|
});
|
|
maybe_register_app_as_login_item(ctx);
|
|
}
|
|
}
|
|
#[cfg_attr(target_family = "wasm", allow(unused_variables))]
|
|
LaunchMode::CommandLine {
|
|
command,
|
|
global_options,
|
|
..
|
|
} => {
|
|
cfg_if::cfg_if! {
|
|
if #[cfg(target_family = "wasm")] {
|
|
panic!("Cannot execute CLI command {command:?} on the web");
|
|
} else {
|
|
if let Err(err) = crate::ai::agent_sdk::run(ctx, command.clone(), global_options.clone()) {
|
|
eprintln!("{err:#}");
|
|
report_error!(err);
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Proxy should never reach launch() — it's a thin byte bridge.
|
|
LaunchMode::RemoteServerProxy => {
|
|
log::error!("Proxy mode should not use the launch() path");
|
|
std::process::exit(1);
|
|
}
|
|
// Daemon: bind the Unix socket and register the ServerModel.
|
|
// initialize_app already set up everything else including crash
|
|
// reporting.
|
|
#[cfg(unix)]
|
|
LaunchMode::RemoteServerDaemon { identity_key } => {
|
|
remote_server::unix::launch_daemon(&identity_key, ctx);
|
|
}
|
|
#[cfg(not(unix))]
|
|
LaunchMode::RemoteServerDaemon { .. } => {
|
|
log::error!("RemoteServerDaemon is not supported on this platform");
|
|
std::process::exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Initializes the logger before running tests.
|
|
///
|
|
/// The `ctor` attribute here means that this runs BEFORE main(), whenever the
|
|
/// binary is executed. For this reason, we need to ensure that this function
|
|
/// only exists within unit test code. Production bundles and integration tests
|
|
/// also initialize the logging system, and initializing it twice causes a panic.
|
|
///
|
|
/// Additionally, we must not write anything to stdout in this function, as it
|
|
/// can interfere with test harnesses collecting the set of tests to run. (This
|
|
/// Seeds predefined system rules into the local object repository on first launch
|
|
/// (when no global rules exist and they haven't been seeded before).
|
|
/// This ensures rules are available for AI requests without requiring the user
|
|
/// to manually visit the Rules settings page.
|
|
fn seed_predefined_rules_if_needed(ctx: &mut AppContext) {
|
|
use ai::facts::predefined_rules::PREDEFINED_RULES;
|
|
use ai::facts::{AIFact, AIMemory};
|
|
|
|
let settings = AISettings::as_ref(ctx);
|
|
if settings.has_seeded_predefined_rules() {
|
|
return;
|
|
}
|
|
|
|
// Check if any rules already exist
|
|
let has_existing_rules = !LocalObjectRepository::as_ref(ctx).rules(ctx).is_empty();
|
|
|
|
if has_existing_rules {
|
|
// Rules exist (e.g. from a previous session) — mark as seeded and skip
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings.mark_predefined_rules_seeded(ctx);
|
|
});
|
|
return;
|
|
}
|
|
|
|
log::info!(
|
|
"[rules] Seeding {} predefined rules on first launch",
|
|
PREDEFINED_RULES.len()
|
|
);
|
|
|
|
LocalObjectRepository::handle(ctx).update(ctx, |repository, ctx| {
|
|
for rule in PREDEFINED_RULES {
|
|
let ai_fact = AIFact::Memory(AIMemory {
|
|
is_autogenerated: false,
|
|
name: Some(rule.name.to_string()),
|
|
content: rule.content.to_string(),
|
|
suggested_logging_id: None,
|
|
});
|
|
repository.create_rule(ai_fact, ctx);
|
|
}
|
|
});
|
|
|
|
AISettings::handle(ctx).update(ctx, |settings, ctx| {
|
|
settings.mark_predefined_rules_seeded(ctx);
|
|
});
|
|
}
|
|
|
|
/// is why we're not simply calling the init() function above.)
|
|
#[ctor::ctor]
|
|
#[cfg(test)]
|
|
fn init_logging_for_unit_tests_glue() {
|
|
// Initialize terminal-friendly logging for tests from the shared logger crate.
|
|
galaxy_logging::init_logging_for_unit_tests();
|
|
}
|