Expand ~ in warp://action/new_tab?path= URLs (#9277)

## Description

Fixes #9178. `warp://action/new_tab?path=~/foo` was opening the current
directory instead of `~/foo`. The query value was being passed straight
into `PathBuf::from_str`, which doesn't expand `~`, so the resulting
literal `~/foo` couldn't be resolved by `open_file` and the new tab
silently fell back to the current location.

Routed the `path` query through `shellexpand::tilde` (already a
workspace dependency) before constructing the `PathBuf`. The same call
site serves both `/new_tab` and `/new_window`, so both URLs benefit.
Absolute and relative paths are unchanged.

## Testing

Added 6 unit tests in `app/src/uri/uri_test.rs`:

- `~/Projects` expands to `$HOME/Projects`
- URL-encoded `%7E%2FProjects` expands the same way
- absolute path `/tmp/foo` unchanged
- relative path `relative/dir` unchanged
- missing `path=` returns `None`
- bare `~` expands to `$HOME`

Also verified the helper in a standalone harness against extra edge
cases: empty value, non-leading `~` (unchanged, correct shell
semantics), `~user/path` (unchanged, a shellexpand limitation), and
URL-encoded spaces.

Couldn't run the in-tree test suite locally because the Metal toolchain
isn't installed, so relying on CI for the full clippy / nextest pass.

## Agent Mode
- [ ] Warp Agent Mode - This PR was created via Warp's AI Agent Mode

## Changelog Entries for Stable

CHANGELOG-BUG-FIX: `warp://action/new_tab?path=~/foo` (and
`/new_window`) now expand `~` to your home directory.

---------

Co-authored-by: seemeroland <roland@warp.dev>
This commit is contained in:
Faizan Qureshi
2026-04-28 16:00:24 -07:00
committed by GitHub
co-authored by seemeroland
parent 389716a905
commit c0feac21db
2 changed files with 48 additions and 5 deletions
+8 -5
View File
@@ -657,6 +657,13 @@ fn find_matching_config_name<'a>(
.find(|&config| config.name.to_lowercase() == target_name_lower)
}
/// 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()))
}
#[derive(Debug)]
enum Action {
NewTab,
@@ -706,11 +713,7 @@ impl Action {
} else {
None
};
let Some(Ok(path)) = url
.query_pairs()
.find(|(k, _v)| k == "path")
.map(|(_, path)| PathBuf::from_str(&path))
else {
let Some(path) = parse_tab_path(url) else {
log::warn!("Could not parse path to open a new tab/window");
return;
};
+40
View File
@@ -533,3 +533,43 @@ fn validate_custom_uri_errors_do_not_leak_query_string() {
assert!(!msg.contains("refresh_token"), "{msg}");
assert!(!msg.contains("LEAKED"), "{msg}");
}
#[test]
fn test_parse_tab_path_expands_tilde() {
let url = Url::parse("warp://action/new_tab?path=~/Projects").unwrap();
let home = dirs::home_dir().expect("HOME must be set for this test");
assert_eq!(parse_tab_path(&url), Some(home.join("Projects")));
}
#[test]
fn test_parse_tab_path_expands_url_encoded_tilde() {
// `%7E` and `%2F` are URL-encoded `~` and `/`.
let url = Url::parse("warp://action/new_tab?path=%7E%2FProjects").unwrap();
let home = dirs::home_dir().expect("HOME must be set for this test");
assert_eq!(parse_tab_path(&url), Some(home.join("Projects")));
}
#[test]
fn test_parse_tab_path_absolute_path_unchanged() {
let url = Url::parse("warp://action/new_tab?path=/tmp/foo").unwrap();
assert_eq!(parse_tab_path(&url), Some(PathBuf::from("/tmp/foo")));
}
#[test]
fn test_parse_tab_path_relative_path_unchanged() {
let url = Url::parse("warp://action/new_tab?path=relative/dir").unwrap();
assert_eq!(parse_tab_path(&url), Some(PathBuf::from("relative/dir")));
}
#[test]
fn test_parse_tab_path_missing_returns_none() {
let url = Url::parse("warp://action/new_tab").unwrap();
assert_eq!(parse_tab_path(&url), None);
}
#[test]
fn test_parse_tab_path_bare_tilde() {
let url = Url::parse("warp://action/new_tab?path=~").unwrap();
let home = dirs::home_dir().expect("HOME must be set for this test");
assert_eq!(parse_tab_path(&url), Some(home));
}