zed: Improve zed:// URL handling (#57047)
Some checks are pending
Congratsbot / check-author (push) Waiting to run
Congratsbot / congrats (push) Blocked by required conditions
deploy_nightly_docs / deploy_docs (push) Has been skipped
run_tests / orchestrate (push) Waiting to run
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / miri_scheduler (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions

Release Notes:

- Improved `zed://` and `zed://agent` URL handling
This commit is contained in:
Neel 2026-05-18 16:09:21 +01:00 committed by GitHub
parent 988f083fc5
commit d3d5fb0d15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 7 deletions

View file

@ -670,7 +670,11 @@ fn prompt_and_open_paths(
create_new_window: bool,
cx: &mut App,
) {
if let Some(workspace_window) = local_workspace_windows(cx).into_iter().next() {
if let Some(workspace_window) =
workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
.into_iter()
.next()
{
workspace_window
.update(cx, |multi_workspace, window, cx| {
let workspace = multi_workspace.workspace().clone();
@ -9471,7 +9475,7 @@ pub async fn get_any_active_multi_workspace(
activate_any_workspace_window(&mut cx).context("could not open zed")
}
fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
pub fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<MultiWorkspace>> {
cx.update(|cx| {
if let Some(workspace_window) = cx
.active_window()
@ -9492,10 +9496,6 @@ fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option<WindowHandle<Multi
})
}
pub fn local_workspace_windows(cx: &App) -> Vec<WindowHandle<MultiWorkspace>> {
workspace_windows_for_location(&SerializedWorkspaceLocation::Local, cx)
}
pub fn workspace_windows_for_location(
serialized_location: &SerializedWorkspaceLocation,
cx: &App,

View file

@ -925,6 +925,14 @@ fn main() {
.ok()
.and_then(|request| OpenRequest::parse(request, cx).log_err())
{
Some(request) if request.is_focus_app_only() => cx.spawn({
let app_state = app_state.clone();
async move |cx| {
if let Err(e) = restore_or_create_workspace(app_state, cx).await {
fail_to_open_window_async(e, cx)
}
}
}),
Some(request) => {
handle_open_request(request, app_state.clone(), cx);
Task::ready(())
@ -978,6 +986,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await)
.detach();
}
OpenRequestKind::FocusApp => {
cx.spawn(async move |cx| {
if workspace::activate_any_workspace_window(cx).is_some() {
return anyhow::Ok(());
}
restore_or_create_workspace(app_state, cx).await
})
.detach_and_log_err(cx);
}
OpenRequestKind::Extension { extension_id } => {
cx.spawn(async move |cx| {
let workspace =
@ -1001,6 +1018,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
let multi_workspace =
workspace::get_any_active_multi_workspace(app_state, cx.clone()).await?;
let panels_task = multi_workspace.update(cx, |multi_workspace, _, cx| {
multi_workspace
.workspace()
.update(cx, |workspace, _| workspace.take_panels_task())
})?;
if let Some(task) = panels_task {
task.await.log_err();
}
multi_workspace.update(cx, |multi_workspace, window, cx| {
multi_workspace.workspace().update(cx, |workspace, cx| {
if let Some(panel) = workspace.focus_panel::<AgentPanel>(window, cx) {
@ -1011,6 +1037,11 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
cx,
);
});
} else {
log::warn!(
"zed://agent received but the AgentPanel is not registered \
(is `disable_ai` enabled?)"
);
}
});
})

View file

@ -51,6 +51,7 @@ pub enum OpenRequestKind {
Box<dyn CliResponseSink>,
),
),
FocusApp,
Extension {
extension_id: String,
},
@ -82,6 +83,7 @@ impl std::fmt::Debug for OpenRequestKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::CliConnection(_) => write!(f, "CliConnection(..)"),
Self::FocusApp => write!(f, "FocusApp"),
Self::Extension { extension_id } => f
.debug_struct("Extension")
.field("extension_id", extension_id)
@ -118,6 +120,15 @@ impl std::fmt::Debug for OpenRequestKind {
}
impl OpenRequest {
pub fn is_focus_app_only(&self) -> bool {
matches!(self.kind, Some(OpenRequestKind::FocusApp))
&& self.open_paths.is_empty()
&& self.diff_paths.is_empty()
&& self.remote_connection.is_none()
&& self.join_channel.is_none()
&& self.open_channel_notes.is_empty()
}
pub fn parse(request: RawOpenRequest, cx: &App) -> Result<Self> {
let mut this = Self::default();
@ -167,6 +178,8 @@ impl OpenRequest {
}
} else if let Some(agent_path) = url.strip_prefix("zed://agent") {
this.parse_agent_url(agent_path)
} else if url == "zed://" || url == "zed://open" || url == "zed://open/" {
this.kind = Some(OpenRequestKind::FocusApp);
} else if let Some(schema_path) = url.strip_prefix("zed://schemas/") {
this.kind = Some(OpenRequestKind::BuiltinJsonSchema {
schema_path: schema_path.to_string(),
@ -210,7 +223,8 @@ impl OpenRequest {
}
fn parse_agent_url(&mut self, agent_path: &str) {
// Format: "" or "?prompt=<text>"
// Format: "" or "?prompt=<text>".
let agent_path = agent_path.strip_prefix('/').unwrap_or(agent_path);
let external_source_prompt = agent_path.strip_prefix('?').and_then(|query| {
url::form_urlencoded::parse(query.as_bytes())
.find_map(|(key, value)| (key == "prompt").then_some(value))
@ -1230,6 +1244,63 @@ mod tests {
}
}
#[gpui::test]
fn test_parse_agent_url_with_trailing_slash(cx: &mut TestAppContext) {
let _app_state = init_test(cx);
let request = cx.update(|cx| {
OpenRequest::parse(
RawOpenRequest {
urls: vec!["zed://agent/?prompt=hello".into()],
..Default::default()
},
cx,
)
.unwrap()
});
match request.kind {
Some(OpenRequestKind::AgentPanel {
external_source_prompt,
}) => {
assert_eq!(
external_source_prompt
.as_ref()
.map(ExternalSourcePrompt::as_str),
Some("hello")
);
}
_ => panic!("Expected AgentPanel kind"),
}
}
#[gpui::test]
fn test_parse_focus_app_url(cx: &mut TestAppContext) {
let _app_state = init_test(cx);
for url in ["zed://", "zed://open", "zed://open/"] {
let request = cx.update(|cx| {
OpenRequest::parse(
RawOpenRequest {
urls: vec![url.into()],
..Default::default()
},
cx,
)
.unwrap()
});
assert!(
matches!(request.kind, Some(OpenRequestKind::FocusApp)),
"expected FocusApp for {url}, got {:?}",
request.kind
);
assert!(
request.is_focus_app_only(),
"expected is_focus_app_only for {url}"
);
}
}
#[gpui::test]
fn test_parse_agent_url_with_empty_prompt(cx: &mut TestAppContext) {
let _app_state = init_test(cx);