Files
galaxy/app/src/lib.rs
T

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();
}