mod docker; pub mod parse_url_paths; pub mod web_intent_parser; #[cfg(target_family = "wasm")] pub mod browser_url_handler; use crate::ai::active_agent_views_model::{ActiveAgentViewsModel, ConversationOrTaskId}; use crate::ai::agent::api::ServerConversationToken; use crate::drive::OpenWarpDriveObjectSettings; use crate::launch_configs::launch_config::LaunchConfig; use crate::linear::{LinearAction, LinearIssueWork}; use crate::root_view::{open_new_window_get_handles, OpenLaunchConfigArg}; use crate::server::ids::ServerId; use crate::server::telemetry::{LaunchConfigUiLocation, TelemetryEvent}; use crate::util::openable_file_type::{is_file_openable_in_warp, is_markdown_file}; use crate::workspace::{Workspace, WorkspaceAction, WorkspaceRegistry}; use crate::{cloud_object::ObjectType, workspace::ToastStack}; use crate::{drive::OpenWarpDriveObjectArgs, view_components::DismissibleToast}; use crate::{features::FeatureFlag, workspace::active_terminal_in_window}; use crate::ai::ambient_agents::github_auth_notifier::GitHubAuthNotifier; use crate::settings_view::{OpenTeamsSettingsModalArgs, SettingsSection}; use crate::user_config::load_launch_configs; use crate::{ quake_mode_window_id, quake_mode_window_is_open, safe_info, send_telemetry_from_app_ctx, ChannelState, OpenPath, }; use anyhow::{anyhow, ensure, Result}; use itertools::Itertools; use session_sharing_protocol::common::SessionId; use std::collections::HashMap; use std::path::PathBuf; use std::str::FromStr; use url::Url; use warpui::notification::UserNotification; use warpui::{platform::TerminationMode, SingletonEntity as _, TypedActionView}; use warpui::{AppContext, EntityId, ViewHandle, WindowId}; use self::docker::open_docker_container; const DESKTOP_REDIRECT_URI_PATH: &str = "/desktop_redirect"; /// Args for opening the MCP settings page via deeplink, with optional auto-install. /// The `autoinstall` value is the raw query param string; it is matched case-insensitively /// against gallery titles in `autoinstall_from_gallery`. pub struct OpenMCPSettingsArgs { pub autoinstall: Option, } /// Source query parameter value indicating auth was initiated from cloud agent setup. /// Used to skip opening settings page after GitHub auth completes. pub const CLOUD_SETUP_SOURCE: &str = "cloud_setup"; #[derive(Debug, PartialEq, Eq)] pub enum UriHost { Auth, Team, /// A host prefix for all actions (e.g.: new tab, new window). Action, /// A host prefix for all actions that involve launch configurations Launch, /// Supports joining shared sessions via a warp:// URI. SharedSession, /// Supports viewing AI conversations via a warp:// URI. Conversation, /// Supports WD object actions Drive, /// Supports opening warp's settings panel via URI Settings, /// A host prefix for a general-purpose home/landing page. Unlike other intent URIs, the home /// page behavior may change over time and vary from platform to platform. Home, /// Actions related to MCP servers (e.g.: oauth callbacks). Mcp, /// Opens a new tab with the Codex model and starts a conversation. Codex, /// Actions triggered from Linear integrations (e.g. work on issue). Linear, } impl FromStr for UriHost { type Err = anyhow::Error; fn from_str(s: &str) -> Result { match s { "auth" => Ok(Self::Auth), "team" => Ok(Self::Team), "action" => Ok(Self::Action), "launch" => Ok(Self::Launch), "shared_session" if FeatureFlag::ViewingSharedSessions.is_enabled() => { Ok(Self::SharedSession) } "conversation" => Ok(Self::Conversation), "drive" => Ok(Self::Drive), "settings" => Ok(Self::Settings), "home" => Ok(Self::Home), "mcp" => Ok(Self::Mcp), "codex" => Ok(Self::Codex), "linear" => Ok(Self::Linear), _ => Err(anyhow!("Received url with unexpected host: {}", s)), } } } impl UriHost { fn handle(&self, primary_window_id: Option, url: &Url, ctx: &mut AppContext) { // Handle host match self { UriHost::Auth => { ctx.window_ids() .collect_vec() .into_iter() .for_each(|window_id| { let Some(root_view_id) = ctx.root_view_id(window_id) else { return; }; safe_info!( safe: ("Dispatched auth url to window {window_id}"), full: ("Dispatched auth url {url} to window {window_id}") ); ctx.dispatch_action( window_id, &[root_view_id], "root_view:handle_incoming_auth_url", &url.clone(), log::Level::Info, ); }); } UriHost::Team => { match url.path_segments().into_iter().flatten().last() { // If the last segment of the URL is "settings", open the team settings page. Some("settings") => { open_window_with_action( primary_window_id, "root_view:open_team_settings_page", ctx, ); } // Otherwise default to previous behavior. _ => { // TODO: Parse URL to ensure the user is logged into the right account // Shows the user the settings view of their newly joined team within the app. open_window_with_action( primary_window_id, "root_view:handle_team_intent_link_action", ctx, ); } }; send_telemetry_from_app_ctx!(TelemetryEvent::OpenTeamFromURI, ctx); } UriHost::Action => { match Action::parse(url) { Ok(action) => action.handle(primary_window_id, url, ctx), Err(err) => { log::warn!("{err}"); } }; } UriHost::Launch => { if let Some(desired_config_path) = get_launch_config_path(url.path()) { let configs = load_launch_configs(&crate::user_config::launch_configs_dir()); if let Some(config) = find_matching_config(desired_config_path.as_str(), &configs) { ctx.dispatch_global_action( "root_view:open_launch_config", &OpenLaunchConfigArg { launch_config: config.clone(), ui_location: LaunchConfigUiLocation::Uri, open_in_active_window: false, }, ) } else { log::warn!( "couldn't find a matching file path for '{}'", desired_config_path.as_str() ); } } else { log::warn!("couldn't turn launch link '{}' into path", url.path()); } } UriHost::SharedSession => { // We expect the uri to have the ID of the session to join as the last segment. // e.g. warp://shared_session/{id} let session_id = url .path_segments() .into_iter() .flatten() .last() .and_then(|id| SessionId::from_str(id).ok()); if let Some(session_id) = session_id { // If there's an existing window, join the session inc a new tab. Otherwise, open a new window. match primary_window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }) { Some((primary_window_id, root_view_id)) => { ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:join_shared_session_in_existing_window", &session_id, log::Level::Info, ); } None => { ctx.dispatch_global_action("root_view:join_shared_session", &session_id) } } } else { log::warn!("Failed to join shared session with uri={url}"); } } UriHost::Conversation => { // We expect the uri to have the conversation ID as the last segment. // e.g. warp://conversation/{conversation_id} let conversation_id: Option = url .path_segments() .into_iter() .flatten() .last() .map(|s| ServerConversationToken::new(s.to_owned())); if let Some(conversation_id) = conversation_id { // If there's an existing window, open the conversation in a new tab. Otherwise, open a new window. match primary_window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }) { Some((primary_window_id, root_view_id)) => { ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:open_cloud_conversation_in_existing_window", &conversation_id, log::Level::Info, ); } None => ctx.dispatch_global_action( "root_view:open_conversation_viewer", &conversation_id, ), } } else { log::warn!("Failed to open conversation with uri={url}"); } } UriHost::Drive => { // We expect the uri to have the ID of the object we are trying to open and the object_type. // e.g. warp://drive/{object_type}?id={UID} // For folder links, we expect an additional query parameter primary_object_id which refers to the id object // that should be opened // When the user is directed here via the request access flow, we expect an additional query parameter invitee_email // If this paramter is present, we will open the sharing dialog with the email filled in. let object_type = url .path_segments() .into_iter() .flatten() .last() .and_then(|object_type| ObjectType::from_str(object_type).ok()); let query_string: HashMap<_, _> = url.query_pairs().collect(); let object_server_id: Option = query_string.get("id").map(ServerId::from_string_lossy); let focused_folder_id: Option = query_string .get("focused_folder_id") .map(ServerId::from_string_lossy); let invitee_email: Option = query_string.get("invitee_email").map(|s| s.to_string()); if let Some((object_type, server_id)) = object_type.zip(object_server_id) { let primary_window_and_view = primary_window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }); let args = OpenWarpDriveObjectArgs { object_type, server_id, settings: OpenWarpDriveObjectSettings { focused_folder_id, invitee_email, }, }; // If there's an existing window, open the object in that window, otherwise open a new window if let Some((primary_window_id, root_view_id)) = primary_window_and_view { // `args` may contain user-identifiable fields // (e.g. `invitee_email`), so avoid writing the full // debug representation to `warp.log` on non-dogfood // release channels. safe_info!( safe: ( "Opening drive object in existing window: object_type={:?} server_id={}", args.object_type, args.server_id, ), full: ("Opening drive object in existing window: {args:?}") ); ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:open_drive_object_existing_window", &args, log::Level::Info, ); } else { ctx.dispatch_global_action("root_view:open_drive_object_new_window", &args) } } else { log::warn!("Failed to open drive object with uri={url}"); } } UriHost::Settings => { // We support opening different settings pages through URI: // - warp://settings/teams?invite={email} - opens team settings with invite modal // - warp://settings/billing_and_usage - opens billing and usage settings page // - warp://settings/environments - opens environments settings page // - warp://settings/mcp - opens MCP servers settings page // - warp://settings/platform - opens platform settings page // - warp://settings/appearance - opens appearance settings page (themes, fonts, etc.) let settings_sub_page: Option = url .path_segments() .into_iter() .flatten() .last() .map(|s| s.to_string()); let query_string: HashMap<_, _> = url.query_pairs().collect(); if let Some(settings_sub_page) = settings_sub_page { match settings_sub_page.as_str() { "teams" => { let invite_email = query_string.get("invite").map(|s| s.to_string()); let args = OpenTeamsSettingsModalArgs { invite_email }; dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_team_settings_with_email_invite_in_existing_window", "root_view:open_team_settings_with_email_invite_in_new_window", &args, ctx, ); } "billing_and_usage" => { dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_settings_page_in_existing_window", "root_view:open_settings_page_in_new_window", &SettingsSection::BillingAndUsage, ctx, ); } "environments" => { // Notify that GitHub auth completed so views can refresh GitHubAuthNotifier::handle(ctx).update(ctx, |notifier, ctx| { notifier.notify_auth_completed(ctx); }); // Open settings page unless auth was initiated from cloud setup // (cloud setup users should stay on their current page) let source = query_string.get("source").map(|s| s.as_ref()); let skip_settings = source == Some(CLOUD_SETUP_SOURCE); if !skip_settings { dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_settings_page_in_existing_window", "root_view:open_settings_page_in_new_window", &SettingsSection::CloudEnvironments, ctx, ); } } "mcp" => { // warp://settings/mcp?autoinstall= auto-installs a gallery MCP server. // The value is matched case-insensitively against gallery titles. let autoinstall = query_string.get("autoinstall").map(|v| v.to_string()); let args = OpenMCPSettingsArgs { autoinstall }; dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_mcp_settings_in_existing_window", "root_view:open_mcp_settings_in_new_window", &args, ctx, ); } "platform" => { dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_settings_page_in_existing_window", "root_view:open_settings_page_in_new_window", &SettingsSection::OzCloudAPIKeys, ctx, ); } "appearance" => { dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_settings_page_in_existing_window", "root_view:open_settings_page_in_new_window", &SettingsSection::Appearance, ctx, ); } _ => { log::warn!("Failed to open settings pane with uri={url}"); } } } else { log::warn!("Failed to open settings pane with uri={url}"); } } UriHost::Home => { ctx.dispatch_global_action("root_view::open_new", &()); } UriHost::Mcp => { #[cfg(not(target_family = "wasm"))] { let result = crate::ai::mcp::TemplatableMCPServerManager::handle(ctx) .update(ctx, |manager, _ctx| manager.handle_oauth_callback(url)); if let Err(e) = result { log::error!("Failed to handle MCP OAuth callback: {e:?}"); } } } UriHost::Codex => { dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_codex_in_existing_window", "root_view:open_codex_in_new_window", &(), ctx, ); } UriHost::Linear => match LinearAction::parse(url) { Ok(LinearAction::WorkOnIssue) => { let args = LinearIssueWork::from_url(url); dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_linear_issue_work_in_existing_window", "root_view:open_linear_issue_work_in_new_window", &args, ctx, ); } Err(err) => { log::warn!("{err}"); } }, } } /// When handling this URI action, determine which window(s) should be focused. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn window_behavior_hint(&self) -> WindowBehaviorHint { use WindowBehaviorHint as W; match self { Self::Auth => W::ShowPrimaryWindow(WindowActivationFallbackBehavior::NewWindow { replace_existing: true, }), Self::Team | Self::Drive | Self::Settings => W::default(), // These URLs always open new windows. Self::Launch | Self::SharedSession | Self::Conversation | Self::Home => W::Nothing, // This will actually be handled by [`Action::window_behavior_hint`]. Self::Action => W::Nothing, // TODO(vorporeal): probably want to focus the window with the MCP pane open Self::Mcp => W::Nothing, // Codex opens a new tab with AI mode, use default behavior Self::Codex => W::default(), // Linear deeplink opens a new tab with agent view Self::Linear => W::default(), } } } /// This determines which windows, if any, will become visible on handling a URI. This is a "hint" /// because it is platform-dependent, and not all platforms can conform. For example, MacOS /// automatically shows the frontmost window, and so the Nothing variant of this is impossible on /// MacOS. #[derive(Clone, Debug)] enum WindowBehaviorHint { /// Determined by the [`get_primary_window`] function. ShowPrimaryWindow(WindowActivationFallbackBehavior), Nothing, } impl Default for WindowBehaviorHint { fn default() -> Self { Self::ShowPrimaryWindow(WindowActivationFallbackBehavior::NewWindow { replace_existing: false, }) } } impl WindowBehaviorHint { /// Perform the desired window focus behavior for the URI being handled. This may change the /// "primary window" if a new one has to be created. Return the new primary WindowId. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn resolve( self, primary_window_id: Option, ctx: &mut AppContext, ) -> Option { match self { Self::ShowPrimaryWindow(fallback_behavior) => { if let Some(window_id) = primary_window_id { match ctx.windows().windowing_system() { Some(windowing_system) if windowing_system.allows_programmatic_window_activation() => { ctx.windows().show_window_and_focus_app(window_id); } _ => { return fallback_behavior.resolve(window_id, ctx); } } } } Self::Nothing => {} }; primary_window_id } } /// If we're in an environment where we can't fulfill [`WindowBehaviorHint`], and the OS default /// behavior isn't acceptable/reliable, e.g. Wayland doesn't allow windows to programmatically show /// themselves, try this fallback behavior instead. #[derive(Clone, Debug)] enum WindowActivationFallbackBehavior { /// If the primary window picked to handle the URL is not the active one, send a native push /// notification. Notify { title: String, description: String }, /// Create a new window to handle the URI. NewWindow { /// Close the former "primary window" as determined by [`get_primary_window`]. This should /// generally default to `false` to avoid closing a window with information that the user /// may still want. One exception is the Auth route where the old window just showed the /// auth page. replace_existing: bool, }, } impl WindowActivationFallbackBehavior { /// Perform the desired window fallback behavior for the URI being handled. This may change the /// "primary window" if a new one has to be created. Return the new primary WindowId. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn resolve(self, primary_window_id: WindowId, ctx: &mut AppContext) -> Option { match self { WindowActivationFallbackBehavior::Notify { title, description } => { if ctx .windows() .active_window() .is_some_and(|active_window| active_window == primary_window_id) { return Some(primary_window_id); } if let Some(view_handle) = ctx .views_of_type::(primary_window_id) .filter(|views| !views.is_empty()) .map(|mut views| views.swap_remove(0)) { view_handle.update(ctx, |_, ctx| { ctx.send_desktop_notification( UserNotification::new(title, description, None), |_, err, ctx| { log::warn!( "Error showing URL intent notification on {:?}: {err:?}", ctx.window_id() ) }, ); }); } Some(primary_window_id) } WindowActivationFallbackBehavior::NewWindow { replace_existing } => { let new_window_id = open_new_window_get_handles(None, ctx).0; if replace_existing { ctx.windows() .close_window(primary_window_id, TerminationMode::Cancellable); } Some(new_window_id) } } } } /// Turn the launch config URL into a filename. /// "/hello%20world" --> "hello world" fn get_launch_config_path(path: &str) -> Option { // Remove the leading slash before the filename. let (_, config_path) = path.split_once('/')?; // URL-decode the filename to recover spaces and // other non-URL-friendly characters let decoded = serde_urlencoded::from_str::>(config_path).ok()?; // serde_urlencoded::from_str tries to find a vector key-value pairs, // so we'll take the first tuple in the vector... let decoded_config_name = decoded.first()?; // ... and read the first member of the tuple. let validated_path = validate_launch_config_path(decoded_config_name.0.as_str())?; // Finally, return the validated path. Some(validated_path.to_string()) } /// Remove file extension, which consists of the last '.' in the filename /// and whatever characters follow it. fn remove_extension(full_path: &str) -> Option<&str> { let (no_extension, _) = full_path.rsplit_once('.')?; Some(no_extension) } /// Ensure that a path is relative and doesn't contain '/../', /// to prevent launch config links from escaping the launch config directory. fn validate_launch_config_path(path: &str) -> Option<&str> { if path.starts_with('/') || path.starts_with("../") || path.contains("/../") || path.ends_with("/..") { None } else { Some(path) } } /// Given a config path, find a matching launch config file fn find_matching_config<'a>( target_path: &str, configs: &'a [LaunchConfig], ) -> Option<&'a LaunchConfig> { // first, try to match the exact filename. if let Some(matched_config) = find_matching_config_name(target_path, configs) { return Some(matched_config); } // next, try to match the filename without the extension let no_extension = remove_extension(target_path)?; find_matching_config_name(no_extension, configs) } /// Case-insensitive matching on the config's name /// (field in the YAML file). fn find_matching_config_name<'a>( target_name: &str, configs: &'a [LaunchConfig], ) -> Option<&'a LaunchConfig> { let target_name_lower = target_name.to_lowercase(); configs .iter() .find(|&config| config.name.to_lowercase() == target_name_lower) } #[derive(Debug)] enum Action { NewTab, NewWindow, Docker, OpenRepo, CloudAgentSetup, NewCloudAgentConversation, NewAgentConversation, CreateEnvironment { repos: Vec }, FocusCloudMode, } impl Action { fn parse(url: &Url) -> Result { match url.path() { "/new_tab" => Ok(Self::NewTab), "/new_window" => Ok(Self::NewWindow), "/docker/open_subshell" => Ok(Self::Docker), "/open-repo" => Ok(Self::OpenRepo), "/cloud_agent_setup" => Ok(Self::CloudAgentSetup), "/new_cloud_agent_conversation" => Ok(Self::NewCloudAgentConversation), "/new_agent_conversation" => Ok(Self::NewAgentConversation), "/create_environment" => { let repos = url .query_pairs() .filter_map(|(k, v)| (k == "repo").then(|| v.into_owned())) .collect::>(); Ok(Self::CreateEnvironment { repos }) } "/focus_cloud_mode" => Ok(Self::FocusCloudMode), _ => Err(anyhow!( "Received \"action\" intent with unexpected action: {}", url.path() )), } } fn handle(&self, primary_window_id: Option, url: &Url, ctx: &mut AppContext) { #[cfg(target_os = "linux")] let primary_window_id = self.window_behavior_hint().resolve(primary_window_id, ctx); match self { Self::NewTab | Self::NewWindow => { let window_id = if let Self::NewTab = self { primary_window_id } else { None }; let Some(Ok(path)) = url .query_pairs() .find(|(k, _v)| k == "path") .map(|(_, path)| PathBuf::from_str(&path)) else { log::warn!("Could not parse path to open a new tab/window"); return; }; open_file(window_id, path, ctx); } Action::Docker => { if let Err(err) = open_docker_container(url, ctx) { if let Some(window_id) = primary_window_id { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error("Custom URI is invalid.".to_owned()); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } log::warn!("error opening docker container: {err}"); } } Action::OpenRepo => { let window_id = primary_window_id.or_else(|| Some(open_new_window_get_handles(None, ctx).0)); let Some(window_id) = window_id else { log::warn!("unable to determine window for open repo action"); return; }; let Some(mut workspaces) = ctx.views_of_type::(window_id) else { log::warn!("no workspace found in window {window_id} for open repo action"); return; }; if let Some(workspace) = workspaces.pop() { workspace.update(ctx, |workspace, ctx| { workspace .handle_action(&WorkspaceAction::OpenRepository { path: None }, ctx); }); } else { log::warn!("no workspace views in window {window_id} for open repo action"); } } Action::CloudAgentSetup => { let window_id = primary_window_id.or_else(|| Some(open_new_window_get_handles(None, ctx).0)); let Some(window_id) = window_id else { log::warn!("unable to determine window for cloud agent setup action"); return; }; let Some(mut workspaces) = ctx.views_of_type::(window_id) else { log::warn!( "no workspace found in window {window_id} for cloud agent setup action" ); return; }; if let Some(workspace) = workspaces.pop() { workspace.update(ctx, |workspace, ctx| { workspace.handle_action(&WorkspaceAction::OpenCloudAgentSetupGuide, ctx); }); } else { log::warn!( "no workspace views in window {window_id} for cloud agent setup action" ); } } Action::NewCloudAgentConversation => { let window_id = primary_window_id.or_else(|| Some(open_new_window_get_handles(None, ctx).0)); let Some(window_id) = window_id else { log::warn!( "unable to determine window for new cloud agent conversation action" ); return; }; let Some(mut workspaces) = ctx.views_of_type::(window_id) else { log::warn!( "no workspace found in window {window_id} for new cloud agent conversation action" ); return; }; if let Some(workspace) = workspaces.pop() { workspace.update(ctx, |workspace, ctx| { workspace.handle_action(&WorkspaceAction::AddAmbientAgentTab, ctx); }); } else { log::warn!( "no workspace views in window {window_id} for new cloud agent conversation action" ); } } Action::NewAgentConversation => { let window_id = primary_window_id.or_else(|| Some(open_new_window_get_handles(None, ctx).0)); let Some(window_id) = window_id else { log::warn!("unable to determine window for new agent conversation action"); return; }; let Some(workspace) = WorkspaceRegistry::as_ref(ctx).get(window_id, ctx) else { log::warn!( "no workspace found in window {window_id} for new agent conversation action" ); return; }; workspace.update(ctx, |workspace, ctx| { workspace.handle_action(&WorkspaceAction::AddAgentTab, ctx); }); } Action::CreateEnvironment { repos } => { use crate::root_view::CreateEnvironmentArg; let arg = CreateEnvironmentArg { repos: repos.clone(), }; let primary_window_and_view = primary_window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }); if let Some((primary_window_id, root_view_id)) = primary_window_and_view { ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:create_environment_in_existing_window", &arg, log::Level::Info, ); } else { ctx.dispatch_global_action("root_view:create_environment", &arg); } } Action::FocusCloudMode => { // Notify that GitHub auth completed so views can refresh GitHubAuthNotifier::handle(ctx).update(ctx, |notifier, ctx| { notifier.notify_auth_completed(ctx); }); let active_agent_views = ActiveAgentViewsModel::as_ref(ctx); let focused_conversation = primary_window_id .and_then(|wid| active_agent_views.get_focused_conversation(wid)); let mut terminal_view_id = match focused_conversation { Some(ConversationOrTaskId::TaskId(task_id)) => { active_agent_views.get_terminal_view_id_for_ambient_task(task_id) } Some(ConversationOrTaskId::ConversationId(conversation_id)) => { active_agent_views .get_terminal_view_id_for_conversation(conversation_id, ctx) } None => None, }; if terminal_view_id.is_none() { terminal_view_id = find_cloud_mode_terminal_view_id(primary_window_id, ctx); } if terminal_view_id.is_none() { terminal_view_id = active_agent_views.get_last_focused_terminal_id(); } if terminal_view_id.is_none() { terminal_view_id = primary_window_id .and_then(|window_id| active_terminal_view_id_in_window(window_id, ctx)); } if let Some(terminal_view_id) = terminal_view_id { if let Some((window_id, workspace)) = find_workspace_for_terminal_view(terminal_view_id, ctx) { ctx.windows().show_window_and_focus_app(window_id); workspace.update(ctx, |workspace, ctx| { workspace.handle_action( &WorkspaceAction::FocusTerminalViewInWorkspace { terminal_view_id }, ctx, ); }); return; } } dispatch_action_in_new_or_existing_window( primary_window_id, "root_view:open_settings_page_in_existing_window", "root_view:open_settings_page_in_new_window", &SettingsSection::CloudEnvironments, ctx, ); } } } /// When handling this URI action, determine which window(s) should be focused. #[cfg_attr(not(target_os = "linux"), allow(dead_code))] fn window_behavior_hint(&self) -> WindowBehaviorHint { use WindowBehaviorHint as W; match self { Self::Docker | Self::CreateEnvironment { .. } | Self::OpenRepo | Self::CloudAgentSetup | Self::NewCloudAgentConversation | Self::NewAgentConversation | Self::FocusCloudMode => W::default(), Self::NewTab => W::ShowPrimaryWindow(WindowActivationFallbackBehavior::Notify { title: "New tab created".to_owned(), description: "Go to Warp to see your new tab.".to_owned(), }), Self::NewWindow => W::Nothing, } } } /// Handles all incoming urls. These urls are file urls, auth urls for login, /// and team urls for opening team settings. pub fn handle_incoming_uri(url: &Url, ctx: &mut AppContext) { // Non-dogfood builds must never log the full URL here: URLs routed to this // handler can carry secrets in their query string (for example, the // Firebase `refresh_token` on `warp://auth/desktop_redirect?...`). Log // only the non-sensitive components (scheme, host, path) on release // channels; dogfood builds retain the full URL for local debugging. safe_info!( safe: ("received url {}", safe_url_log_fields(url)), full: ("received url {:?}", &url) ); // Pick the window that should be handling the URI. This has some // additional logic to handle the hotkey window and there being no // currently-active window. let primary_window_id = get_primary_window(ctx.windows().frontmost_window_id(), ctx); // If we're running on a platform where we can spawn local TTYs, // check if this is a file:// URL and if so, spawn a new session // with an initial working directory based on the provided path. #[cfg(feature = "local_tty")] if url.scheme() == "file" { if let Ok(path) = url.to_file_path() { open_file(primary_window_id, path, ctx); } return; } match validate_custom_uri(url) { Ok(host) => { #[cfg(any(target_os = "linux", windows))] let primary_window_id = host.window_behavior_hint().resolve(primary_window_id, ctx); host.handle(primary_window_id, url, ctx); } Err(e) => { if let Some(window_id) = primary_window_id { ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| { let toast = DismissibleToast::error(format!("Custom URI is invalid: {e:?}")); toast_stack.add_ephemeral_toast(toast, window_id, ctx); }); } log::warn!("Custom URI is invalid: {e:?}"); } } } /// Gets the primary window ID, and returns `None` if it does not exist. /// A primary window is the foregrounded window, or one of the inactive non-quake windows. /// A closed quake window is not counted. fn get_primary_window( active_window_id: Option, ctx: &mut AppContext, ) -> Option { // Return quake mode window if it's open if let Some(window_id) = quake_mode_window_id() .filter(|window_id| quake_mode_window_is_open() && ctx.is_window_open(*window_id)) { return Some(window_id); } // Otherwise, return active window if let Some(window_id) = active_window_id { return Some(window_id); } let mut non_quake_mode_windows = ctx .window_ids() .filter(|window_id| Some(*window_id) != quake_mode_window_id()); // There's no active window, return first non-quake mode window or None if none exist. non_quake_mode_windows.next() } /// Handle an incoming `file://` URL. /// * Markdown files are opened as notebook panes. /// * For directories, open a new session at the directory path. /// * For other files, open a new session at the parent directory path, then possibly execute the /// file. fn open_file(window_id: Option, path: PathBuf, ctx: &mut AppContext) { let primary_window_and_view = window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }); if is_markdown_file(&path) { if let Some((primary_window_id, root_view_id)) = primary_window_and_view { ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:add_file_pane", &path, log::Level::Info, ); } else { ctx.dispatch_global_action("root_view:open_new_with_file_notebook", &path); } } else if path.is_file() && is_file_openable_in_warp(&path).is_some() { #[cfg(feature = "local_fs")] { use crate::code::editor_management::CodeSource; use crate::root_view::{open_new_with_workspace_source, NewWorkspaceSource}; use crate::util::{ file::external_editor::EditorSettings, openable_file_type::resolve_file_target_to_open_in_warp, }; // Open text/code files in Warp's code editor, respecting the user's layout preference. let editor_settings = EditorSettings::as_ref(ctx); let target = resolve_file_target_to_open_in_warp(&path, editor_settings, None); let window_id = if let Some((wid, _)) = primary_window_and_view { wid } else { open_new_with_workspace_source( NewWorkspaceSource::Session { options: Box::default(), }, ctx, ) .0 }; ctx.windows().show_window_and_focus_app(window_id); if let Some(workspaces) = ctx.views_of_type::(window_id) { if let Some(workspace) = workspaces.into_iter().next() { workspace.update(ctx, |workspace, ctx| { let source = CodeSource::Finder { path: path.clone() }; workspace.open_file_with_target(path, target, None, source, ctx); }); } } } } else { let directory_path = if path.is_file() { match path.parent() { Some(parent) => parent.to_path_buf(), None => PathBuf::new(), } } else { path.clone() }; if let Some((primary_window_id, root_view_id)) = primary_window_and_view { ctx.dispatch_action( primary_window_id, &[root_view_id], "root_view:add_session_at_path", &directory_path, log::Level::Info, ); // Run command after session has been added if path.is_file() { if let Some(path_str) = path.to_str() { execute_file(primary_window_id, path_str, ctx); } } } else { let open_path = OpenPath { path: directory_path, }; ctx.dispatch_global_action("root_view:open_new_from_path", &open_path); // Run command after window has been added if path.is_file() { let active_window_id = ctx.windows().active_window(); if let Some(primary_window_id) = get_primary_window(active_window_id, ctx) { if let Some(path_str) = path.to_str() { execute_file(primary_window_id, path_str, ctx); } } } } send_telemetry_from_app_ctx!(TelemetryEvent::OpenNewSessionFromFilePath, ctx); } } fn execute_file(window_id: WindowId, path_str: &str, ctx: &mut AppContext) { active_terminal_in_window(window_id, ctx, |term, t_ctx| { let path_str = term.shell_family(t_ctx).shell_escape(path_str); term.input().update(t_ctx, |input, i_ctx| { input.set_pending_command(&path_str, i_ctx); }) }); send_telemetry_from_app_ctx!(TelemetryEvent::CommandFileRun, ctx); } fn open_window_with_action(active_window_id: Option, action: &str, ctx: &mut AppContext) { if let Some(primary_window_id) = active_window_id { // Dispatch action to primary window if let Some(root_view_id) = ctx.root_view_id(primary_window_id) { ctx.dispatch_action( primary_window_id, &[root_view_id], action, &(), log::Level::Info, ); } } else { log::warn!("no primary window id to dispatch action to"); // Open a new window and dispatch action there ctx.dispatch_global_action("root_view:open_new", &()); // TODO: Note we cannot just dispatch here as it will be a no-op. // Need to send a callback once window is fully open. } } fn find_workspace_for_terminal_view( terminal_view_id: EntityId, ctx: &mut AppContext, ) -> Option<(WindowId, ViewHandle)> { for window_id in ctx.window_ids() { let Some(workspaces) = ctx.views_of_type::(window_id) else { continue; }; for workspace in workspaces { let contains_terminal = workspace .as_ref(ctx) .list_tab_pane_groups(ctx) .iter() .any(|group| group.terminal_ids.contains(&terminal_view_id)); if contains_terminal { return Some((window_id, workspace)); } } } None } fn active_terminal_view_id_in_window(window_id: WindowId, ctx: &AppContext) -> Option { let workspaces = ctx.views_of_type::(window_id)?; let workspace = workspaces.first()?; workspace.read(ctx, |workspace, w_ctx| { let pane_group = workspace.active_tab_pane_group().as_ref(w_ctx); pane_group .active_session_view(w_ctx) .map(|terminal_view| terminal_view.id()) .or_else(|| { pane_group .terminal_views(w_ctx) .first() .map(|view| view.id()) }) }) } fn find_cloud_mode_terminal_view_id( primary_window_id: Option, ctx: &AppContext, ) -> Option { let mut window_ids = Vec::new(); if let Some(primary_window_id) = primary_window_id { window_ids.push(primary_window_id); } window_ids.extend( ctx.window_ids() .filter(|window_id| Some(*window_id) != primary_window_id), ); for window_id in window_ids { let Some(workspaces) = ctx.views_of_type::(window_id) else { continue; }; for workspace in workspaces { if let Some(terminal_view_id) = workspace.read(ctx, |workspace, w_ctx| { find_cloud_mode_terminal_in_workspace(workspace, w_ctx) }) { return Some(terminal_view_id); } } } None } fn find_cloud_mode_terminal_in_workspace( workspace: &Workspace, ctx: &AppContext, ) -> Option { let mut fallback_ambient_terminal_id = None; for pane_group_handle in workspace.tab_views() { let pane_group = pane_group_handle.as_ref(ctx); let ambient_terminal_id = pane_group .terminal_views(ctx) .into_iter() .find_map(|terminal_view| { terminal_view .as_ref(ctx) .ambient_agent_view_model() .as_ref(ctx) .is_ambient_agent() .then_some(terminal_view.id()) }); let Some(ambient_terminal_id) = ambient_terminal_id else { continue; }; let has_environment_management_pane = pane_group .pane_ids() .any(|pane_id| pane_id.is_environment_management_pane()); if has_environment_management_pane { return Some(ambient_terminal_id); } if fallback_ambient_terminal_id.is_none() { fallback_ambient_terminal_id = Some(ambient_terminal_id); } } fallback_ambient_terminal_id } /// Helper function to dispatch an action to an existing window /// or create new window if none exist. fn dispatch_action_in_new_or_existing_window( primary_window_id: Option, existing_window_action: &str, new_window_action: &str, args: &T, ctx: &mut AppContext, ) { let primary_window_and_view = primary_window_id.and_then(|window_id| { ctx.root_view_id(window_id) .map(|view_id| (window_id, view_id)) }); if let Some((primary_window_id, root_view_id)) = primary_window_and_view { ctx.dispatch_action( primary_window_id, &[root_view_id], existing_window_action, args, log::Level::Info, ); } else { ctx.dispatch_global_action(new_window_action, args); } } /// Validates an incoming custom URI for security and returns the host. fn validate_custom_uri(url: &Url) -> Result { // For now the only scheme we support is `[scheme_name]://[host_str]/... // Ignore all other urls that don't match this scheme for security purposes. if url.scheme() != ChannelState::url_scheme() { return Err(anyhow!( "Received url with unexpected scheme: {} ", url.scheme() )); } let host_str = url .host_str() .ok_or_else(|| anyhow!("Received url with no host str"))?; let host = UriHost::from_str(host_str)?; // Check if this host is allowed to have arbitrary paths. let host_allows_arbitrary_path = match host { UriHost::Action | UriHost::Launch | UriHost::SharedSession | UriHost::Conversation | UriHost::Drive | UriHost::Team | UriHost::Settings | UriHost::Mcp | UriHost::Codex | UriHost::Linear => true, // Auth and Home only allow the desktop redirect path UriHost::Auth | UriHost::Home => false, }; ensure!( host_allows_arbitrary_path || url.path() == DESKTOP_REDIRECT_URI_PATH, "Received url with unexpected path: {} ", url.path() ); Ok(host) } /// Formats the non-sensitive components of an incoming URL for logging on /// release channels. /// /// The returned string contains only the URL's scheme, host, and path — never /// its query string, fragment, or userinfo component. URLs that reach /// [`handle_incoming_uri`] can carry secrets in their query (for example, the /// Firebase refresh token in `warp://auth/desktop_redirect?refresh_token=...`), /// so this helper exists to give [`safe_info!`] a redacted representation that /// still preserves enough signal for triage. /// /// `url.host_str()` can return `None` for schemes that don't require a host /// (e.g. some `file://` URLs on certain platforms); the literal `-` is used /// as a placeholder in that case so the formatter never panics. fn safe_url_log_fields(url: &Url) -> String { format!( "scheme={} host={} path={}", url.scheme(), url.host_str().unwrap_or("-"), url.path(), ) } #[cfg(test)] #[path = "uri_test.rs"] mod tests;