From ad69357968abf8d06ce7fd34b2554f70143cebe9 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 30 Jan 2026 00:52:47 +0530 Subject: [PATCH] agent: Support initial prompt via zed://agent URL schema (#47959) Adds `zed://agent?prompt=` URL support to open the Agent Panel with a pre-filled prompt. Release Notes: - Added support for opening the Agent Panel with an initial prompt via `zed://agent?prompt=` URL. --- crates/agent_ui/src/acp/thread_view.rs | 25 +++++++++++++++------ crates/agent_ui/src/agent_panel.rs | 30 ++++++++++++++++++++------ crates/agent_ui/src/agent_ui.rs | 6 ++++++ crates/zed/src/main.rs | 8 ++++--- crates/zed/src/zed/open_listener.rs | 19 +++++++++++++--- 5 files changed, 69 insertions(+), 19 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index b739823be0a168d51c0a36cc9e07d49da9277345..2fd5e043e76bb26dfccc96199cd7a740863dbd5a 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -78,10 +78,10 @@ use crate::user_slash_command::{ }; use crate::{ AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue, - CycleFavoriteModels, CycleModeSelector, EditFirstQueuedMessage, ExpandMessageEditor, Follow, - KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, OpenHistory, RejectAll, RejectOnce, - RemoveFirstQueuedMessage, SelectPermissionGranularity, SendImmediately, SendNextQueuedMessage, - ToggleProfileSelector, ToggleThinkingMode, + CycleFavoriteModels, CycleModeSelector, EditFirstQueuedMessage, ExpandMessageEditor, + ExternalAgentInitialContent, Follow, KeepAll, NewThread, OpenAddContextMenu, OpenAgentDiff, + OpenHistory, RejectAll, RejectOnce, RemoveFirstQueuedMessage, SelectPermissionGranularity, + SendImmediately, SendNextQueuedMessage, ToggleProfileSelector, ToggleThinkingMode, }; const STOPWATCH_THRESHOLD: Duration = Duration::from_secs(30); @@ -388,7 +388,7 @@ impl AcpThreadView { pub fn new( agent: Rc, resume_thread: Option, - summarize_thread: Option, + initial_content: Option, workspace: WeakEntity, project: Entity, thread_store: Option>, @@ -430,8 +430,19 @@ impl AcpThreadView { window, cx, ); - if let Some(entry) = summarize_thread { - editor.insert_thread_summary(entry, window, cx); + if let Some(content) = initial_content { + match content { + ExternalAgentInitialContent::ThreadSummary(entry) => { + editor.insert_thread_summary(entry, window, cx); + } + ExternalAgentInitialContent::Text(prompt) => { + editor.set_message( + vec![acp::ContentBlock::Text(acp::TextContent::new(prompt))], + window, + cx, + ); + } + } } editor }); diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 772e7aeefb5bae93b6556782e4701b4201411adb..9ed119d5a63e7a3687304cd4c79bb7294c931f92 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -30,7 +30,10 @@ use crate::{ acp::{AcpThreadHistory, ThreadHistoryEvent}, text_thread_history::{TextThreadHistory, TextThreadHistoryEvent}, }; -use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary}; +use crate::{ + ExternalAgent, ExternalAgentInitialContent, NewExternalAgentThread, + NewNativeAgentThreadFromSummary, +}; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Result, anyhow}; @@ -742,7 +745,7 @@ impl AgentPanel { self.external_thread( Some(ExternalAgent::NativeAgent), None, - Some(thread), + Some(ExternalAgentInitialContent::ThreadSummary(thread)), window, cx, ); @@ -795,7 +798,7 @@ impl AgentPanel { &mut self, agent_choice: Option, resume_thread: Option, - summarize_thread: Option, + initial_content: Option, window: &mut Window, cx: &mut Context, ) { @@ -857,7 +860,7 @@ impl AgentPanel { agent_panel._external_thread( server, resume_thread, - summarize_thread, + initial_content, workspace, project, ext_agent, @@ -1414,6 +1417,21 @@ impl AgentPanel { } } + pub fn new_external_thread_with_text( + &mut self, + initial_text: Option, + window: &mut Window, + cx: &mut Context, + ) { + self.external_thread( + None, + None, + initial_text.map(ExternalAgentInitialContent::Text), + window, + cx, + ); + } + pub fn new_agent_thread( &mut self, agent: AgentType, @@ -1476,7 +1494,7 @@ impl AgentPanel { &mut self, server: Rc, resume_thread: Option, - summarize_thread: Option, + initial_content: Option, workspace: WeakEntity, project: Entity, ext_agent: ExternalAgent, @@ -1498,7 +1516,7 @@ impl AgentPanel { crate::acp::AcpThreadView::new( server, resume_thread, - summarize_thread, + initial_content, workspace.clone(), project, thread_store, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 0c4b8d6df8135093c6f0edfe742fb50bacd3c161..10b8e9f0d61fa4b4ea95e0618c14c551d041787c 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -217,6 +217,12 @@ impl ExternalAgent { } } +/// Content to initialize new external agent with. +pub enum ExternalAgentInitialContent { + ThreadSummary(acp_thread::AgentSessionInfo), + Text(String), +} + /// Opens the profile management interface for configuring agent tools and settings. #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] #[action(namespace = agent)] diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index c65e4aa98584d22ddd12d92eed46e4a29d206ee3..d664b65641ce53f208472a09c84e827feb0573d5 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -846,13 +846,15 @@ fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut }) .detach_and_log_err(cx); } - OpenRequestKind::AgentPanel => { + OpenRequestKind::AgentPanel { initial_prompt } => { cx.spawn(async move |cx| { let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?; workspace.update(cx, |workspace, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.focus_handle(cx).focus(window, cx); + if let Some(panel) = workspace.focus_panel::(window, cx) { + panel.update(cx, |panel, cx| { + panel.new_external_thread_with_text(initial_prompt, window, cx); + }); } }) }) diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 987a755c586f038a33a5eca295aab27e6f99dcbd..06cf6a90fa17f3a5d33b538c4e27b666102e1f0e 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -48,7 +48,9 @@ pub enum OpenRequestKind { Extension { extension_id: String, }, - AgentPanel, + AgentPanel { + initial_prompt: Option, + }, SharedAgentThread { session_id: String, }, @@ -108,8 +110,8 @@ impl OpenRequest { this.kind = Some(OpenRequestKind::Extension { extension_id: extension_id.to_string(), }); - } else if url == "zed://agent" { - this.kind = Some(OpenRequestKind::AgentPanel); + } else if let Some(agent_path) = url.strip_prefix("zed://agent") { + this.parse_agent_url(agent_path) } else if let Some(session_id_str) = url.strip_prefix("zed://agent/shared/") { if uuid::Uuid::parse_str(session_id_str).is_ok() { this.kind = Some(OpenRequestKind::SharedAgentThread { @@ -160,6 +162,17 @@ impl OpenRequest { } } + fn parse_agent_url(&mut self, agent_path: &str) { + // Format: "" or "?prompt=" + let initial_prompt = agent_path.strip_prefix('?').and_then(|query| { + url::form_urlencoded::parse(query.as_bytes()) + .find_map(|(key, value)| (key == "prompt").then_some(value)) + .filter(|s| !s.is_empty()) + .map(|s| s.into_owned()) + }); + self.kind = Some(OpenRequestKind::AgentPanel { initial_prompt }); + } + fn parse_git_clone_url(&mut self, clone_path: &str) -> Result<()> { // Format: /?repo= or ?repo= let clone_path = clone_path.strip_prefix('/').unwrap_or(clone_path);