Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 90 additions & 3 deletions app/src/uri/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ 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::root_view::{open_new_window_get_handles, workspace_for_window, 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};
Expand All @@ -21,7 +21,7 @@ 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::user_config::{load_launch_configs, load_tab_configs, tab_configs_dir};
use crate::{
quake_mode_window_id, quake_mode_window_is_open, safe_info, send_telemetry_from_app_ctx,
ChannelState, OpenPath,
Expand Down Expand Up @@ -78,6 +78,8 @@ pub enum UriHost {
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,
}

impl FromStr for UriHost {
Expand All @@ -99,6 +101,7 @@ impl FromStr for UriHost {
"mcp" => Ok(Self::Mcp),
"codex" => Ok(Self::Codex),
"linear" => Ok(Self::Linear),
"tabconfig" if FeatureFlag::TabConfigs.is_enabled() => Ok(Self::TabConfig),
_ => Err(anyhow!("Received url with unexpected host: {}", s)),
}
}
Expand Down Expand Up @@ -184,6 +187,9 @@ impl UriHost {
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}
Expand Down Expand Up @@ -472,6 +478,8 @@ impl UriHost {
Self::Codex => W::default(),
// Linear deeplink opens a new tab with agent view
Self::Linear => W::default(),
// Tab config deeplink prefers existing window unless ?new_window=true.
Self::TabConfig => W::default(),
Comment thread
haha1903 marked this conversation as resolved.
Outdated
}
}
}
Expand Down Expand Up @@ -657,6 +665,84 @@ fn find_matching_config_name<'a>(
.find(|&config| config.name.to_lowercase() == target_name_lower)
}

/// Handles `warp://tabconfig/<name>` deeplinks.
///
/// Resolution rules:
/// - `<name>` is matched case-insensitively against each tab config's `name`
/// field, then against the file stem as a fallback.
/// - 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 config = config.clone();

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| workspace_for_window(*id, ctx).is_some())
};

let workspace = match target_window_id {
Some(window_id) => workspace_for_window(window_id, ctx),
None => {
let new_window_id = open_new_window_get_handles(None, ctx).0;
workspace_for_window(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 lookup that matches the tab config's `name` first, then
/// the file stem (so both `warp://tabconfig/My%20Tab` and
/// `warp://tabconfig/my_tab.toml` work).
fn find_matching_tab_config<'a>(
target: &str,
configs: &'a [crate::tab_configs::TabConfig],
) -> Option<&'a crate::tab_configs::TabConfig> {
let target_lower = target.to_lowercase();
if let Some(matched) = configs
.iter()
.find(|c| c.name.to_lowercase() == target_lower)
{
return Some(matched);
}

let stem = remove_extension(target).unwrap_or(target).to_lowercase();
configs.iter().find(|c| {
c.source_path
.as_ref()
.and_then(|p| p.file_stem())
.and_then(|s| s.to_str())
.map(|s| s.to_lowercase() == stem)
.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> {
Expand Down Expand Up @@ -1306,7 +1392,8 @@ fn validate_custom_uri(url: &Url) -> Result<UriHost> {
| UriHost::Settings
| UriHost::Mcp
| UriHost::Codex
| UriHost::Linear => true,
| UriHost::Linear
| UriHost::TabConfig => true,
// Auth and Home only allow the desktop redirect path
UriHost::Auth | UriHost::Home => false,
};
Expand Down
1 change: 0 additions & 1 deletion app/src/user_config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ use std::path::PathBuf;
use warp_core::ui::theme::WarpTheme;
use warpui::{Entity, ModelContext, SingletonEntity};

#[cfg(test)]
pub(crate) use imp::load_tab_configs;
#[cfg(feature = "local_fs")]
pub use imp::load_workflows;
Expand Down
8 changes: 8 additions & 0 deletions app/src/user_config/wasm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::path::Path;
use warpui::ModelContext;

use crate::launch_configs::launch_config::LaunchConfig;
use crate::tab_configs::{TabConfig, TabConfigError};
use crate::themes::theme::WarpThemeConfig;
use crate::workflows::workflow::Workflow;

Expand Down Expand Up @@ -39,3 +40,10 @@ pub fn load_launch_configs(_launch_config_path: &Path) -> Vec<LaunchConfig> {
// launch configs from any path.
Default::default()
}

/// Loads all tab configs relative to the `tab_config_path`.
pub(crate) fn load_tab_configs(_tab_config_path: &Path) -> (Vec<TabConfig>, Vec<TabConfigError>) {
// There's no local filesystem for wasm, so we'll never be able to retrieve
// tab configs from any path.
Default::default()
}
2 changes: 1 addition & 1 deletion app/src/workspace/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6306,7 +6306,7 @@ impl Workspace {

/// Opens a tab config, showing the param-fill modal when the config has parameters,
/// or opening the tab directly when there are no parameters.
fn open_tab_config(
pub(crate) fn open_tab_config(
&mut self,
tab_config: crate::tab_configs::TabConfig,
ctx: &mut ViewContext<Self>,
Expand Down