Files
galaxy/app/src/uri/mod.rs
T
Ryan Ward 856a6cd6f3 Remove Warp cloud features and OpenTelemetry
Settings cleanup:
- Remove billing_and_usage, main_page, referrals_page, show_blocks_view,
  environments_page, handoff_environment_creation_modal,
  custom_inference_modal, remove_custom_endpoint_confirmation_dialog,
  transfer_ownership_confirmation_modal, delete_environment_confirmation_dialog
- Remove SettingsSection variants: Account, BillingAndUsage, Referrals,
  SharedBlocks, CloudEnvironments, OzCloudAPIKeys
- Remove SettingsPageViewHandle variants: Main, BillingAndUsage, Referrals,
  CloudEnvironments, OzCloudAPIKeys, SharedBlocks
- Add Platform variant for PlatformPageView
- Gut custom inference endpoint UI from ai_page.rs
- Gut transfer ownership modal from teams_page.rs
- Stub environment_management_pane as dead code
- Remove create_discount_badge usage
- Remove handle_experiment_change call

OpenTelemetry removal:
- Remove opentelemetry, opentelemetry-http, opentelemetry-otlp,
  opentelemetry_sdk, tracing-opentelemetry dependencies
- Replace tracing module with no-op stub
- Delete native.rs and cloud_agent_auth.rs

Bug fixes (prior work):
- Fix apply_diffs() to use markdown_unescaped(ctx)
- Fix notebook executor AIDocumentId handling
- Fix margin/corner-radius consistency in requested_command.rs
- Add document tool handlers to extract_tool_result_content()
- Fix deprecated from_byte_stream in MCP SSE transport
- Fix Cargo.toml profile package spec
- Upgrade rust-toolchain to 1.94.1
2026-07-23 11:14:49 -05:00

1704 lines
68 KiB
Rust

mod docker;
pub mod parse_url_paths;
pub mod web_intent_parser;
#[cfg(target_family = "wasm")]
pub mod browser_url_handler;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use anyhow::{anyhow, ensure, Result};
use itertools::Itertools;
use session_sharing_protocol::common::SessionId;
use url::Url;
use warp_util::path::LineAndColumnArg;
use warpui::notification::UserNotification;
use warpui::platform::TerminationMode;
use warpui::{AppContext, EntityId, SingletonEntity as _, TypedActionView, ViewHandle, WindowId};
use self::docker::open_docker_container;
use crate::ai::active_agent_views_model::{ActiveAgentViewsModel, ConversationOrTaskId};
use crate::ai::agent::api::ServerConversationToken;
use crate::ai::ambient_agents::github_auth_notifier::GitHubAuthNotifier;
use crate::cloud_object::ObjectType;
use crate::code::editor_management::CodeSource;
use crate::drive::{OpenGalaxyDriveObjectArgs, OpenGalaxyDriveObjectSettings};
use crate::features::FeatureFlag;
use crate::launch_configs::launch_config::LaunchConfig;
use crate::linear::{LinearAction, LinearIssueWork};
use crate::root_view::{
open_new_window_get_handles, open_new_with_workspace_source, NewWorkspaceSource,
OpenLaunchConfigArg,
};
use crate::server::ids::ServerId;
use crate::server::telemetry::{LaunchConfigUiLocation, TelemetryEvent};
use crate::settings_view::{
settings_widget_deeplink_target, OpenTeamsSettingsModalArgs, SettingsSection,
};
use crate::tab_configs::TabConfig;
use crate::user_config::{load_launch_configs, load_tab_configs, tab_configs_dir};
use crate::util::file::external_editor::EditorSettings;
use crate::util::openable_file_type::{
is_file_openable_in_warp, is_markdown_file, is_runnable_shell_script,
resolve_file_target_to_open_in_warp, starts_with_shebang,
};
use crate::view_components::DismissibleToast;
use crate::workspace::auto_handoff::trigger_auto_handoff_to_cloud;
use crate::workspace::util::PaneViewLocator;
use crate::workspace::{
active_terminal_in_window, AutoCloudHandoffTrigger, ToastStack, Workspace, WorkspaceAction,
WorkspaceRegistry,
};
use crate::{
quake_mode_window_id, quake_mode_window_is_open, safe_info, send_telemetry_from_app_ctx,
ChannelState, OpenPath,
};
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<String>,
}
/// Args for the `warp://settings` deeplink family, dispatched to the
/// `root_view:open_settings_in_{existing,new}_window` actions.
pub enum OpenSettingsArgs {
/// `warp://settings` — open a settings tab on the default page.
Default,
/// `warp://settings?q=<query>` — open settings with the search bar pre-filled.
Search { query: String },
/// `warp://settings?widget=<widget_id>` — open settings scrolled to a widget.
Widget {
page: SettingsSection,
widget_id: &'static str,
},
}
/// 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,
/// Opens a saved tab config in an existing window or a new one.
TabConfig,
/// Focuses a specific terminal pane by its persistent session UUID.
Session,
}
impl FromStr for UriHost {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self> {
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),
"tab_config" if FeatureFlag::TabConfigs.is_enabled() => Ok(Self::TabConfig),
"session" => Ok(Self::Session),
_ => Err(anyhow!("Received url with unexpected host: {}", s)),
}
}
}
impl UriHost {
fn handle(&self, primary_window_id: Option<WindowId>, 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::TabConfig => {
handle_tab_config_uri(primary_window_id, url, ctx);
}
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<ServerConversationToken> = 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 parameter 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<ServerId> =
query_string.get("id").map(ServerId::from_string_lossy);
let focused_folder_id: Option<ServerId> = query_string
.get("focused_folder_id")
.map(ServerId::from_string_lossy);
let invitee_email: Option<String> =
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 = OpenGalaxyDriveObjectArgs {
object_type,
server_id,
settings: OpenGalaxyDriveObjectSettings {
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 - opens a settings tab on the default page
// - warp://settings?q={query} - opens settings with the search bar pre-filled
// - warp://settings?widget={widget_id} - opens settings scrolled to a widget
// - 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.)
// - warp://settings/warp_agent - opens the Warp Agent settings page (inference / API keys)
let query_string: HashMap<_, _> = url.query_pairs().collect();
// A bare `warp://settings` (or a trailing slash) yields an empty path
// segment; treat that as "no sub-page" so the query-param routing below
// handles it.
let settings_sub_page: Option<String> = url
.path_segments()
.into_iter()
.flatten()
.last()
.filter(|s| !s.is_empty())
.map(|s| s.to_string());
match settings_sub_page.as_deref() {
Some("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,
);
}
Some("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::About,
ctx,
);
}
}
Some("mcp") => {
// warp://settings/mcp?autoinstall=<name> 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,
);
}
// No special sub-page: route the bare host, the `q` (search) and
// `widget` (scroll-to) query params, and the simple section
// sub-pages (e.g. billing_and_usage, platform, appearance,
// warp_agent) resolved via `settings_section_for_simple_subpage`.
maybe_simple_subpage => {
let simple_section =
maybe_simple_subpage.and_then(settings_section_for_simple_subpage);
// Pull the non-empty `q` search query out of the already
// parsed pairs to pre-fill the settings search bar.
let search_query = query_string
.get("q")
.map(|query| query.to_string())
.filter(|query| !query.is_empty());
let widget_target = query_string
.get("widget")
.and_then(|slug| settings_widget_deeplink_target(slug));
if let Some((page, widget_id)) = widget_target {
// `?widget=` scrolls to a specific widget; it takes
// precedence over `?q=` since searching would filter the
// target widget out of view.
let args = OpenSettingsArgs::Widget { page, widget_id };
dispatch_action_in_new_or_existing_window(
primary_window_id,
"root_view:open_settings_in_existing_window",
"root_view:open_settings_in_new_window",
&args,
ctx,
);
} else if let Some(query) = search_query {
let args = OpenSettingsArgs::Search { query };
dispatch_action_in_new_or_existing_window(
primary_window_id,
"root_view:open_settings_in_existing_window",
"root_view:open_settings_in_new_window",
&args,
ctx,
);
} else if let Some(section) = simple_section {
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",
&section,
ctx,
);
} else if maybe_simple_subpage.is_none() {
// Bare `warp://settings` opens the default settings page.
let args = OpenSettingsArgs::Default;
dispatch_action_in_new_or_existing_window(
primary_window_id,
"root_view:open_settings_in_existing_window",
"root_view:open_settings_in_new_window",
&args,
ctx,
);
} else {
log::warn!("Failed to open settings pane: unrecognized sub-page");
}
}
}
}
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}");
}
},
UriHost::Session => {
let uuid_hex = url
.path_segments()
.into_iter()
.flatten()
.last()
.unwrap_or("");
let Some(uuid_bytes) = decode_uuid_hex(uuid_hex) else {
log::warn!(
"session deep link received invalid UUID hex (safe: len={})",
uuid_hex.len()
);
return;
};
let result = WorkspaceRegistry::as_ref(ctx)
.all_workspaces(ctx)
.iter()
.find_map(|(win_id, workspace)| {
workspace.as_ref(ctx).tab_views().find_map(|pane_group| {
let pane_id = pane_group
.as_ref(ctx)
.find_terminal_pane_by_session_uuid(&uuid_bytes)?;
Some((
*win_id,
PaneViewLocator {
pane_group_id: pane_group.id(),
pane_id,
},
))
})
});
if let Some((window_id, locator)) = result {
ctx.windows().show_window_and_focus_app(window_id);
if let Some(root_view_id) = ctx.root_view_id(window_id) {
ctx.dispatch_action_for_view(
window_id,
root_view_id,
"root_view:handle_pane_navigation_event",
&locator,
);
}
} else {
log::warn!("session deep link could not find pane with given UUID");
}
}
}
}
/// When handling this URI action, determine which window(s) should be focused.
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), 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(),
// Handler picks the window itself based on `?new_window=true`.
Self::TabConfig => W::Nothing,
Self::Session => W::Nothing,
}
}
}
/// 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(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
fn resolve(
self,
primary_window_id: Option<WindowId>,
ctx: &mut AppContext,
) -> Option<WindowId> {
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(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
fn resolve(self, primary_window_id: WindowId, ctx: &mut AppContext) -> Option<WindowId> {
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::<Workspace>(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<String> {
// 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::<Vec<(String, String)>>(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)
}
/// Handles `warp://tab_config/<name>` deeplinks.
///
/// Resolution rules:
/// - `<name>` is matched case-insensitively against each tab config's file
/// stem, so both `warp://tab_config/my_tab` and
/// `warp://tab_config/my_tab.toml` work.
/// - When `?new_window=true` (or no Warp window is open) the tab config opens
/// in a brand-new window. Otherwise it opens as a new tab in the active
/// window.
fn handle_tab_config_uri(primary_window_id: Option<WindowId>, url: &Url, ctx: &mut AppContext) {
let Some(desired) = get_launch_config_path(url.path()) else {
log::warn!("couldn't turn tab config link '{}' into name", url.path());
return;
};
let (configs, _errors) = load_tab_configs(&tab_configs_dir());
let Some(config) = find_matching_tab_config(desired.as_str(), configs) else {
log::warn!("couldn't find a tab config matching '{}'", desired);
return;
};
let force_new_window = url
.query_pairs()
.any(|(k, v)| k == "new_window" && matches!(v.as_ref(), "1" | "true"));
let target_window_id = if force_new_window {
None
} else {
primary_window_id.filter(|id| WorkspaceRegistry::as_ref(ctx).get(*id, ctx).is_some())
};
let workspace = match target_window_id {
Some(window_id) => WorkspaceRegistry::as_ref(ctx).get(window_id, ctx),
None => {
let new_window_id = open_new_window_get_handles(None, ctx).0;
WorkspaceRegistry::as_ref(ctx).get(new_window_id, ctx)
}
};
let Some(workspace) = workspace else {
log::warn!(
"no workspace available to open tab config '{}'",
config.name
);
return;
};
workspace.update(ctx, |workspace, ctx| {
workspace.open_tab_config(config, ctx);
});
}
/// Case-insensitive match against each tab config's file stem. Tab config
/// `name` fields are not unique across files, so we key off the filename.
///
/// Tries the target as-is first, then with the extension stripped, so both
/// `my_tab` and `my_tab.toml` resolve to `my_tab.toml` and dotted stems like
/// `foo.bar` (from `foo.bar.toml`) still work when written without `.toml`.
fn find_matching_tab_config(target: &str, configs: Vec<TabConfig>) -> Option<TabConfig> {
let raw = target.to_lowercase();
let stripped = remove_extension(target).map(str::to_lowercase);
configs.into_iter().find(|c| {
c.source_path
.as_ref()
.and_then(|p| p.file_stem())
.and_then(|s| s.to_str())
.map(|s| {
let stem = s.to_lowercase();
stem == raw || Some(stem.as_str()) == stripped.as_deref()
})
.unwrap_or(false)
})
}
/// Extract the `path` query parameter, expanding a leading `~` to the
/// user's home directory.
fn parse_tab_path(url: &Url) -> Option<PathBuf> {
let raw = url.query_pairs().find(|(k, _)| k == "path")?.1;
Some(PathBuf::from(shellexpand::tilde(&raw).into_owned()))
}
fn parse_positive_usize_query_param(url: &Url, name: &str) -> Result<Option<usize>> {
let Some(raw) = url.query_pairs().find(|(k, _)| k == name).map(|(_, v)| v) else {
return Ok(None);
};
let value = raw.parse::<usize>()?;
ensure!(value > 0, "`{name}` must be greater than 0");
Ok(Some(value))
}
fn parse_open_file_editor_url(url: &Url) -> Result<(PathBuf, Option<LineAndColumnArg>)> {
let raw_path = url
.query_pairs()
.find(|(k, _)| k == "path")
.map(|(_, v)| v)
.ok_or_else(|| anyhow!("missing path for open_file_editor action"))?;
let path = PathBuf::from(shellexpand::tilde(&raw_path).into_owned());
ensure!(
path.is_absolute(),
"`path` must be absolute for open_file_editor action"
);
let line = parse_positive_usize_query_param(url, "line")?;
let column = parse_positive_usize_query_param(url, "column")?;
ensure!(
line.is_some() || column.is_none(),
"`column` requires `line` for open_file_editor action"
);
Ok((
path,
line.map(|line_num| LineAndColumnArg {
line_num,
column_num: column,
}),
))
}
fn parse_auto_handoff_trigger(url: &Url) -> AutoCloudHandoffTrigger {
match url
.query_pairs()
.find(|(k, _)| k == "trigger")
.map(|(_, v)| v)
{
Some(trigger) if matches!(trigger.as_ref(), "sleep" | "macos_sleep" | "macos-sleep") => {
AutoCloudHandoffTrigger::MacOsSleep
}
Some(_) | None => AutoCloudHandoffTrigger::Uri,
}
}
#[derive(Debug)]
enum Action {
NewTab,
NewWindow,
OpenFileEditor {
path: PathBuf,
line_col: Option<LineAndColumnArg>,
},
Docker,
OpenRepo,
CloudAgentSetup,
NewCloudAgentConversation,
NewAgentConversation,
CreateEnvironment {
repos: Vec<String>,
},
FocusCloudMode,
AutoHandoffToCloud {
trigger: AutoCloudHandoffTrigger,
},
}
impl Action {
fn parse(url: &Url) -> Result<Self> {
match url.path() {
"/new_tab" => Ok(Self::NewTab),
"/new_window" => Ok(Self::NewWindow),
"/open_file_editor" => {
let (path, line_col) = parse_open_file_editor_url(url)?;
Ok(Self::OpenFileEditor { path, line_col })
}
"/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::<Vec<_>>();
Ok(Self::CreateEnvironment { repos })
}
"/focus_cloud_mode" => Ok(Self::FocusCloudMode),
"/auto_handoff_to_cloud" | "/auto-handoff-to-cloud" => Ok(Self::AutoHandoffToCloud {
trigger: parse_auto_handoff_trigger(url),
}),
_ => Err(anyhow!(
"Received \"action\" intent with unexpected action: {}",
url.path()
)),
}
}
fn handle(&self, primary_window_id: Option<WindowId>, url: &Url, ctx: &mut AppContext) {
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
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(path) = parse_tab_path(url) else {
log::warn!("Could not parse path to open a new tab/window");
return;
};
open_file(window_id, path, ctx);
}
Self::OpenFileEditor { path, line_col } => {
#[cfg(feature = "local_fs")]
open_file_editor(primary_window_id, path.clone(), *line_col, ctx);
#[cfg(not(feature = "local_fs"))]
{
let _ = (path, line_col);
log::warn!("open_file_editor action requires local_fs support");
}
}
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::<Workspace>(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::<Workspace>(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 Some(window_id) = primary_window_id else {
open_new_with_workspace_source(NewWorkspaceSource::AmbientAgent, ctx);
return;
};
let Some(mut workspaces) = ctx.views_of_type::<Workspace>(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 => {
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,
);
});
// Notify after focusing so Cloud Mode panes can retry in the selected pane.
GitHubAuthNotifier::handle(ctx).update(ctx, |notifier, ctx| {
notifier.notify_auth_completed(ctx);
});
return;
}
}
GitHubAuthNotifier::handle(ctx).update(ctx, |notifier, ctx| {
notifier.notify_auth_completed(ctx);
});
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,
);
}
Action::AutoHandoffToCloud { trigger } => {
trigger_auto_handoff_to_cloud(*trigger, ctx);
}
}
}
/// When handling this URI action, determine which window(s) should be focused.
#[cfg_attr(not(any(target_os = "linux", target_os = "freebsd")), allow(dead_code))]
fn window_behavior_hint(&self) -> WindowBehaviorHint {
use WindowBehaviorHint as W;
match self {
Self::Docker
| Self::OpenFileEditor { .. }
| Self::CreateEnvironment { .. }
| Self::OpenRepo
| Self::CloudAgentSetup
| Self::NewCloudAgentConversation
| Self::NewAgentConversation
| Self::FocusCloudMode
| Self::AutoHandoffToCloud { .. } => 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) {
safe_info!(
safe: ("received 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", target_os = "freebsd", 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<WindowId>,
ctx: &mut AppContext,
) -> Option<WindowId> {
// 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()
}
/// What `open_file` should do with an incoming `file://` URL.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum OpenFileAction {
/// Open in the markdown notebook pane.
Notebook,
/// Open in Warp's code/text editor pane.
Editor,
/// Open a session at the parent directory and queue the file as the pending command,
/// or just open a session at the directory path if `path` is a directory.
ExecuteInSession,
}
/// Pure routing decision for `open_file`. Extracted so it can be unit-tested without
/// standing up a full `AppContext`.
fn classify_open_file_action(path: &Path) -> OpenFileAction {
if is_markdown_file(path) {
OpenFileAction::Notebook
} else if is_runnable_shell_script(path) {
OpenFileAction::ExecuteInSession
} else if path.is_file()
&& (is_file_openable_in_warp(path).is_some() || starts_with_shebang(path))
{
OpenFileAction::Editor
} else {
OpenFileAction::ExecuteInSession
}
}
#[cfg(feature = "local_fs")]
fn can_open_file_editor_path(path: &Path) -> bool {
path.is_file() && is_file_openable_in_warp(path).is_some()
}
/// 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<WindowId>, 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))
});
let action = classify_open_file_action(&path);
if action == OpenFileAction::Notebook {
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 action == OpenFileAction::Editor {
#[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;
use crate::util::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::<Workspace>(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);
}
}
#[cfg(feature = "local_fs")]
fn open_file_editor(
primary_window_id: Option<WindowId>,
path: PathBuf,
line_col: Option<LineAndColumnArg>,
ctx: &mut AppContext,
) {
#[cfg(feature = "local_fs")]
{
if !can_open_file_editor_path(&path) {
log::warn!("open_file_editor action rejected non-openable path: {path:?}");
return;
}
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_id.and_then(|window_id| {
ctx.root_view_id(window_id)
.map(|view_id| (window_id, view_id))
}) {
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::<Workspace>(window_id) {
if let Some(workspace) = workspaces.into_iter().next() {
workspace.update(ctx, |workspace, ctx| {
let source = CodeSource::Link {
path: path.clone(),
range_start: line_col,
range_end: None,
};
workspace.open_file_with_target(path, target, line_col, source, 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<WindowId>, 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<Workspace>)> {
for window_id in ctx.window_ids() {
let Some(workspaces) = ctx.views_of_type::<Workspace>(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<EntityId> {
let workspaces = ctx.views_of_type::<Workspace>(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<WindowId>,
ctx: &AppContext,
) -> Option<EntityId> {
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::<Workspace>(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<EntityId> {
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()
.is_some()
.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<T: 'static>(
primary_window_id: Option<WindowId>,
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);
}
}
fn settings_section_for_simple_subpage(subpage: &str) -> Option<SettingsSection> {
match subpage {
"billing_and_usage" => Some(SettingsSection::About),
"platform" => Some(SettingsSection::About),
"appearance" => Some(SettingsSection::Appearance),
"warp_agent" => Some(SettingsSection::WarpAgent),
_ => None,
}
}
/// Validates an incoming custom URI for security and returns the host.
fn validate_custom_uri(url: &Url) -> Result<UriHost> {
// 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
| UriHost::TabConfig
| UriHost::Session => 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)
}
fn decode_uuid_hex(hex: &str) -> Option<Vec<u8>> {
let hex = hex.as_bytes();
if hex.len() != 32 {
return None;
}
hex.chunks_exact(2)
.map(|pair| {
let high = (pair[0] as char).to_digit(16)?;
let low = (pair[1] as char).to_digit(16)?;
Some(((high << 4) | low) as u8)
})
.collect()
}
#[cfg(test)]
#[path = "uri_tests.rs"]
mod tests;