Merge branch 'main' into mcp-codex

Ben Brandt created

Change summary

Cargo.lock                                                                |   4 
assets/icons/ai_claude.svg                                                |   1 
assets/icons/ai_gemini.svg                                                |   4 
assets/icons/new_from_summary.svg                                         |   7 
assets/icons/new_text_thread.svg                                          |   7 
assets/icons/new_thread.svg                                               |   3 
assets/keymaps/default-linux.json                                         |   9 
assets/keymaps/default-macos.json                                         |   9 
assets/keymaps/vim.json                                                   |   9 
crates/activity_indicator/src/activity_indicator.rs                       |   4 
crates/agent/src/thread.rs                                                |  75 
crates/agent_servers/Cargo.toml                                           |   5 
crates/agent_servers/src/claude.rs                                        | 184 
crates/agent_servers/src/codex.rs                                         |   3 
crates/agent_servers/src/e2e_tests.rs                                     |   2 
crates/agent_ui/src/active_thread.rs                                      |  14 
crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs |   8 
crates/agent_ui/src/agent_panel.rs                                        | 260 
crates/agent_ui/src/message_editor.rs                                     |  29 
crates/agent_ui/src/ui.rs                                                 |   2 
crates/agent_ui/src/ui/new_thread_button.rs                               |  75 
crates/ai_onboarding/src/agent_api_keys_onboarding.rs                     | 135 
crates/ai_onboarding/src/agent_panel_onboarding_card.rs                   |   8 
crates/ai_onboarding/src/agent_panel_onboarding_content.rs                | 114 
crates/ai_onboarding/src/ai_onboarding.rs                                 | 279 
crates/ai_onboarding/src/young_account_banner.rs                          |   2 
crates/client/src/zed_urls.rs                                             |  13 
crates/collab/src/rpc.rs                                                  |   7 
crates/context_server/src/context_server.rs                               |   4 
crates/editor/src/actions.rs                                              |   2 
crates/editor/src/editor.rs                                               |  44 
crates/editor/src/element.rs                                              |   9 
crates/extension/src/types.rs                                             |   4 
crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs                   |   4 
crates/gpui/src/elements/div.rs                                           |  29 
crates/gpui/src/elements/list.rs                                          |   6 
crates/gpui/src/platform/linux/platform.rs                                |   7 
crates/icons/src/icons.rs                                                 |   3 
crates/language_model/src/registry.rs                                     |   4 
crates/language_models/src/provider/cloud.rs                              |  12 
crates/language_tools/Cargo.toml                                          |   1 
crates/language_tools/src/lsp_tool.rs                                     | 592 
crates/project/src/context_server_store.rs                                |  18 
crates/project/src/context_server_store/extension.rs                      |   5 
crates/project/src/project_settings.rs                                    |   2 
crates/project/src/terminals.rs                                           |   2 
crates/project_panel/src/project_panel.rs                                 |  16 
crates/terminal_view/src/terminal_scrollbar.rs                            |  11 
crates/theme/src/default_colors.rs                                        |   4 
crates/theme/src/fallback_themes.rs                                       |   9 
crates/theme/src/schema.rs                                                |  34 
crates/theme/src/styles/colors.rs                                         |  10 
crates/ui/src/components/scrollbar.rs                                     |  31 
crates/workspace/src/pane.rs                                              |  11 
crates/zed/src/main.rs                                                    |  74 
crates/zed/src/zed/open_listener.rs                                       |  26 
docs/src/languages/php.md                                                 |   2 
57 files changed, 1,553 insertions(+), 695 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -150,7 +150,9 @@ dependencies = [
  "indoc",
  "itertools 0.14.0",
  "language",
+ "libc",
  "log",
+ "nix 0.29.0",
  "paths",
  "project",
  "schemars",
@@ -163,6 +165,7 @@ dependencies = [
  "tempfile",
  "ui",
  "util",
+ "uuid",
  "watch",
  "which 6.0.3",
  "workspace-hack",
@@ -9166,7 +9169,6 @@ dependencies = [
  "collections",
  "copilot",
  "editor",
- "feature_flags",
  "futures 0.3.31",
  "gpui",
  "itertools 0.14.0",

assets/icons/ai_gemini.svg 🔗

@@ -1 +1,3 @@
-<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.44 12.27C7.81333 13.1217 8 14.0317 8 15C8 14.0317 8.18083 13.1217 8.5425 12.27C8.91583 11.4183 9.4175 10.6775 10.0475 10.0475C10.6775 9.4175 11.4183 8.92167 12.27 8.56C13.1217 8.18667 14.0317 8 15 8C14.0317 8 13.1217 7.81917 12.27 7.4575C11.4411 7.1001 10.6871 6.5895 10.0475 5.9525C9.4105 5.31293 8.8999 4.55891 8.5425 3.73C8.18083 2.87833 8 1.96833 8 1C8 1.96833 7.81333 2.87833 7.44 3.73C7.07833 4.58167 6.5825 5.3225 5.9525 5.9525C5.31293 6.5895 4.55891 7.1001 3.73 7.4575C2.87833 7.81917 1.96833 8 1 8C1.96833 8 2.87833 8.18667 3.73 8.56C4.58167 8.92167 5.3225 9.4175 5.9525 10.0475C6.5825 10.6775 7.07833 11.4183 7.44 12.27Z" fill="black"/>
+</svg>

assets/icons/new_from_summary.svg 🔗

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14 4H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66667 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M4.66667 12H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M8.00016 12C8.41993 12.5597 9.00515 12.9731 9.67294 13.1817C10.3407 13.3903 11.0572 13.3835 11.7209 13.1623C12.3846 12.941 12.9619 12.5166 13.371 11.949C13.78 11.3815 14.0002 10.6996 14.0002 10C14.0002 9.20435 13.6841 8.44129 13.1215 7.87868C12.5589 7.31607 11.7958 7 11.0002 7C10.1135 7 9.30683 7.36 8.72683 7.94L7.3335 9.33333" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M7.3335 6.66669V9.33335H10.0002" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/new_text_thread.svg 🔗

@@ -0,0 +1,7 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7.33333 8H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M10.6667 5H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M9 11H2" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M12 7V11" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M14 9H10" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/icons/new_thread.svg 🔗

@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M6.31254 12.549C7.3841 13.0987 8.61676 13.2476 9.78839 12.9688C10.96 12.6901 11.9936 12.0021 12.7028 11.0287C13.412 10.0554 13.7503 8.8607 13.6566 7.66002C13.5629 6.45934 13.0435 5.33159 12.1919 4.48C11.3403 3.62841 10.2126 3.10898 9.01188 3.01531C7.8112 2.92164 6.61655 3.2599 5.64319 3.96912C4.66984 4.67834 3.9818 5.71188 3.70306 6.88351C3.42432 8.05514 3.5732 9.2878 4.12289 10.3594L3 13.6719L6.31254 12.549Z" stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/keymaps/default-linux.json 🔗

@@ -277,7 +277,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor && !use_modifier_to_send",
+    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
     "bindings": {
       "enter": "agent::Chat",
       "ctrl-enter": "agent::ChatWithFollow",
@@ -288,7 +288,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor && use_modifier_to_send",
+    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
     "bindings": {
       "ctrl-enter": "agent::Chat",
       "enter": "editor::Newline",
@@ -483,9 +483,8 @@
       "ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch  / find_under_expand_skip
       "ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
       "ctrl-k ctrl-i": "editor::Hover",
+      "ctrl-k ctrl-b": "editor::BlameHover",
       "ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
-      "ctrl-u": "editor::UndoSelection",
-      "ctrl-shift-u": "editor::RedoSelection",
       "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
       "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
       "f2": "editor::Rename",
@@ -663,6 +662,8 @@
   {
     "context": "Editor",
     "bindings": {
+      "ctrl-u": "editor::UndoSelection",
+      "ctrl-shift-u": "editor::RedoSelection",
       "ctrl-shift-j": "editor::JoinLines",
       "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
       "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",

assets/keymaps/default-macos.json 🔗

@@ -318,7 +318,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor && !use_modifier_to_send",
+    "context": "MessageEditor && !Picker > Editor && !use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
@@ -330,7 +330,7 @@
     }
   },
   {
-    "context": "MessageEditor > Editor && use_modifier_to_send",
+    "context": "MessageEditor && !Picker > Editor && use_modifier_to_send",
     "use_key_equivalents": true,
     "bindings": {
       "cmd-enter": "agent::Chat",
@@ -537,9 +537,8 @@
       "ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
       "cmd-k ctrl-cmd-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
       "cmd-k cmd-i": "editor::Hover",
+      "cmd-k cmd-b": "editor::BlameHover",
       "cmd-/": ["editor::ToggleComments", { "advance_downwards": false }],
-      "cmd-u": "editor::UndoSelection",
-      "cmd-shift-u": "editor::RedoSelection",
       "f8": ["editor::GoToDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
       "shift-f8": ["editor::GoToPreviousDiagnostic", { "severity": { "min": "hint", "max": "error" } }],
       "f2": "editor::Rename",
@@ -726,6 +725,8 @@
     "context": "Editor",
     "use_key_equivalents": true,
     "bindings": {
+      "cmd-u": "editor::UndoSelection",
+      "cmd-shift-u": "editor::RedoSelection",
       "ctrl-j": "editor::JoinLines",
       "ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
       "ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",

assets/keymaps/vim.json 🔗

@@ -124,6 +124,7 @@
       "g r a": "editor::ToggleCodeActions",
       "g g": "vim::StartOfDocument",
       "g h": "editor::Hover",
+      "g B": "editor::BlameHover",
       "g t": "pane::ActivateNextItem",
       "g shift-t": "pane::ActivatePreviousItem",
       "g d": "editor::GoToDefinition",
@@ -858,6 +859,14 @@
       "shift-n": null
     }
   },
+  {
+    "context": "Picker > Editor",
+    "bindings": {
+      "ctrl-h": "editor::Backspace",
+      "ctrl-u": "editor::DeleteToBeginningOfLine",
+      "ctrl-w": "editor::DeleteToPreviousWordStart"
+    }
+  },
   {
     "context": "GitCommit > Editor && VimControl && vim_mode == normal",
     "bindings": {

crates/activity_indicator/src/activity_indicator.rs 🔗

@@ -231,7 +231,6 @@ impl ActivityIndicator {
                 status,
             } => {
                 let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
-                let project = project.clone();
                 let status = status.clone();
                 let server_name = server_name.clone();
                 cx.spawn_in(window, async move |workspace, cx| {
@@ -247,8 +246,7 @@ impl ActivityIndicator {
                     workspace.update_in(cx, |workspace, window, cx| {
                         workspace.add_item_to_active_pane(
                             Box::new(cx.new(|cx| {
-                                let mut editor =
-                                    Editor::for_buffer(buffer, Some(project.clone()), window, cx);
+                                let mut editor = Editor::for_buffer(buffer, None, window, cx);
                                 editor.set_read_only(true);
                                 editor
                             })),

crates/agent/src/thread.rs 🔗

@@ -51,7 +51,7 @@ use util::{ResultExt as _, debug_panic, post_inc};
 use uuid::Uuid;
 use zed_llm_client::{CompletionIntent, CompletionRequestStatus, UsageLimit};
 
-const MAX_RETRY_ATTEMPTS: u8 = 3;
+const MAX_RETRY_ATTEMPTS: u8 = 4;
 const BASE_RETRY_DELAY: Duration = Duration::from_secs(5);
 
 #[derive(Debug, Clone)]
@@ -2182,8 +2182,8 @@ impl Thread {
 
         // General strategy here:
         // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all.
-        // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), try multiple times with exponential backoff.
-        // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), just retry once.
+        // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff.
+        // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times.
         match error {
             HttpResponseError {
                 status_code: StatusCode::TOO_MANY_REQUESTS,
@@ -2211,8 +2211,8 @@ impl Thread {
                 }
                 StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed {
                     delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
-                    // Internal Server Error could be anything, so only retry once.
-                    max_attempts: 1,
+                    // Internal Server Error could be anything, retry up to 3 times.
+                    max_attempts: 3,
                 }),
                 status => {
                     // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"),
@@ -2223,20 +2223,23 @@ impl Thread {
                             max_attempts: MAX_RETRY_ATTEMPTS,
                         })
                     } else {
-                        None
+                        Some(RetryStrategy::Fixed {
+                            delay: retry_after.unwrap_or(BASE_RETRY_DELAY),
+                            max_attempts: 2,
+                        })
                     }
                 }
             },
             ApiInternalServerError { .. } => Some(RetryStrategy::Fixed {
                 delay: BASE_RETRY_DELAY,
-                max_attempts: 1,
+                max_attempts: 3,
             }),
             ApiReadResponseError { .. }
             | HttpSend { .. }
             | DeserializeResponse { .. }
             | BadRequestFormat { .. } => Some(RetryStrategy::Fixed {
                 delay: BASE_RETRY_DELAY,
-                max_attempts: 1,
+                max_attempts: 3,
             }),
             // Retrying these errors definitely shouldn't help.
             HttpResponseError {
@@ -2244,24 +2247,31 @@ impl Thread {
                     StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED,
                 ..
             }
-            | SerializeRequest { .. }
+            | AuthenticationError { .. }
+            | PermissionError { .. } => None,
+            // These errors might be transient, so retry them
+            SerializeRequest { .. }
             | BuildRequestBody { .. }
             | PromptTooLarge { .. }
-            | AuthenticationError { .. }
-            | PermissionError { .. }
             | ApiEndpointNotFound { .. }
-            | NoApiKey { .. } => None,
+            | NoApiKey { .. } => Some(RetryStrategy::Fixed {
+                delay: BASE_RETRY_DELAY,
+                max_attempts: 2,
+            }),
             // Retry all other 4xx and 5xx errors once.
             HttpResponseError { status_code, .. }
                 if status_code.is_client_error() || status_code.is_server_error() =>
             {
                 Some(RetryStrategy::Fixed {
                     delay: BASE_RETRY_DELAY,
-                    max_attempts: 1,
+                    max_attempts: 3,
                 })
             }
             // Conservatively assume that any other errors are non-retryable
-            HttpResponseError { .. } | Other(..) => None,
+            HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed {
+                delay: BASE_RETRY_DELAY,
+                max_attempts: 2,
+            }),
         }
     }
 
@@ -4352,7 +4362,7 @@ fn main() {{
             let retry_state = thread.retry_state.as_ref().unwrap();
             assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
             assert_eq!(
-                retry_state.max_attempts, 1,
+                retry_state.max_attempts, 3,
                 "Should have correct max attempts"
             );
         });
@@ -4368,8 +4378,9 @@ fn main() {{
                             if let MessageSegment::Text(text) = seg {
                                 text.contains("internal")
                                     && text.contains("Fake")
-                                    && text.contains("Retrying in")
-                                    && !text.contains("attempt")
+                                    && text.contains("Retrying")
+                                    && text.contains("attempt 1 of 3")
+                                    && text.contains("seconds")
                             } else {
                                 false
                             }
@@ -4464,8 +4475,8 @@ fn main() {{
             let retry_state = thread.retry_state.as_ref().unwrap();
             assert_eq!(retry_state.attempt, 1, "Should be first retry attempt");
             assert_eq!(
-                retry_state.max_attempts, 1,
-                "Internal server errors should only retry once"
+                retry_state.max_attempts, 3,
+                "Internal server errors should retry up to 3 times"
             );
         });
 
@@ -4473,7 +4484,15 @@ fn main() {{
         cx.executor().advance_clock(BASE_RETRY_DELAY);
         cx.run_until_parked();
 
-        // Should have scheduled second retry - count retry messages
+        // Advance clock for second retry
+        cx.executor().advance_clock(BASE_RETRY_DELAY);
+        cx.run_until_parked();
+
+        // Advance clock for third retry
+        cx.executor().advance_clock(BASE_RETRY_DELAY);
+        cx.run_until_parked();
+
+        // Should have completed all retries - count retry messages
         let retry_count = thread.update(cx, |thread, _| {
             thread
                 .messages
@@ -4491,24 +4510,24 @@ fn main() {{
                 .count()
         });
         assert_eq!(
-            retry_count, 1,
-            "Should have only one retry for internal server errors"
+            retry_count, 3,
+            "Should have 3 retries for internal server errors"
         );
 
-        // For internal server errors, we only retry once and then give up
-        // Check that retry_state is cleared after the single retry
+        // For internal server errors, we retry 3 times and then give up
+        // Check that retry_state is cleared after all retries
         thread.read_with(cx, |thread, _| {
             assert!(
                 thread.retry_state.is_none(),
-                "Retry state should be cleared after single retry"
+                "Retry state should be cleared after all retries"
             );
         });
 
-        // Verify total attempts (1 initial + 1 retry)
+        // Verify total attempts (1 initial + 3 retries)
         assert_eq!(
             *completion_count.lock(),
-            2,
-            "Should have attempted once plus 1 retry"
+            4,
+            "Should have attempted once plus 3 retries"
         );
     }
 

crates/agent_servers/Cargo.toml 🔗

@@ -37,11 +37,16 @@ strum.workspace = true
 tempfile.workspace = true
 ui.workspace = true
 util.workspace = true
+uuid.workspace = true
 watch.workspace = true
 which.workspace = true
 shlex.workspace = true
 workspace-hack.workspace = true
 
+[target.'cfg(unix)'.dependencies]
+libc.workspace = true
+nix.workspace = true
+
 [dev-dependencies]
 env_logger.workspace = true
 language.workspace = true

crates/agent_servers/src/claude.rs 🔗

@@ -3,10 +3,13 @@ pub mod tools;
 use collections::HashMap;
 use project::Project;
 use settings::SettingsStore;
+use smol::process::Child;
 use std::cell::RefCell;
 use std::fmt::Display;
 use std::path::Path;
+use std::pin::pin;
 use std::rc::Rc;
+use uuid::Uuid;
 
 use agentic_coding_protocol::{
     self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion,
@@ -15,7 +18,7 @@ use agentic_coding_protocol::{
 use anyhow::{Result, anyhow};
 use futures::channel::oneshot;
 use futures::future::LocalBoxFuture;
-use futures::{AsyncBufReadExt, AsyncWriteExt};
+use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt};
 use futures::{
     AsyncRead, AsyncWrite, FutureExt, StreamExt,
     channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
@@ -27,7 +30,7 @@ use serde::{Deserialize, Serialize};
 use util::ResultExt;
 
 use crate::claude::tools::ClaudeTool;
-use crate::mcp_server::{McpConfig, ZedMcpServer};
+use crate::mcp_server::{self, McpConfig, ZedMcpServer};
 use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
 use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection};
 
@@ -97,50 +100,58 @@ impl AgentServer for ClaudeCode {
                 anyhow::bail!("Failed to find claude binary");
             };
 
-            let mut child = util::command::new_smol_command(&command.path)
-                .args(
-                    [
-                        "--input-format",
-                        "stream-json",
-                        "--output-format",
-                        "stream-json",
-                        "--print",
-                        "--verbose",
-                        "--mcp-config",
-                        mcp_config_path.to_string_lossy().as_ref(),
-                        "--permission-prompt-tool",
-                        &format!(
-                            "mcp__{}__{}",
-                            crate::mcp_server::SERVER_NAME,
-                            crate::mcp_server::PERMISSION_TOOL
-                        ),
-                        "--allowedTools",
-                        "mcp__zed__Read,mcp__zed__Edit",
-                        "--disallowedTools",
-                        "Read,Edit",
-                    ]
-                    .into_iter()
-                    .chain(command.args.iter().map(|arg| arg.as_str())),
-                )
-                .current_dir(root_dir)
-                .stdin(std::process::Stdio::piped())
-                .stdout(std::process::Stdio::piped())
-                .stderr(std::process::Stdio::inherit())
-                .kill_on_drop(true)
-                .spawn()?;
-
-            let stdin = child.stdin.take().unwrap();
-            let stdout = child.stdout.take().unwrap();
-
             let (incoming_message_tx, mut incoming_message_rx) = mpsc::unbounded();
             let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
+            let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
+
+            let session_id = Uuid::new_v4();
+
+            log::trace!("Starting session with id: {}", session_id);
 
-            let io_task =
-                ClaudeAgentConnection::handle_io(outgoing_rx, incoming_message_tx, stdin, stdout);
             cx.background_spawn(async move {
-                io_task.await.log_err();
+                let mut outgoing_rx = Some(outgoing_rx);
+                let mut mode = ClaudeSessionMode::Start;
+
+                loop {
+                    let mut child =
+                        spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir)
+                            .await?;
+                    mode = ClaudeSessionMode::Resume;
+
+                    let pid = child.id();
+                    log::trace!("Spawned (pid: {})", pid);
+
+                    let mut io_fut = pin!(
+                        ClaudeAgentConnection::handle_io(
+                            outgoing_rx.take().unwrap(),
+                            incoming_message_tx.clone(),
+                            child.stdin.take().unwrap(),
+                            child.stdout.take().unwrap(),
+                        )
+                        .fuse()
+                    );
+
+                    select_biased! {
+                        done_tx = cancel_rx.next() => {
+                            if let Some(done_tx) = done_tx {
+                                log::trace!("Interrupted (pid: {})", pid);
+                                let result = send_interrupt(pid as i32);
+                                outgoing_rx.replace(io_fut.await?);
+                                done_tx.send(result).log_err();
+                                continue;
+                            }
+                        }
+                        result = io_fut => {
+                            result?;
+                        }
+                    }
+
+                    log::trace!("Stopped (pid: {})", pid);
+                    break;
+                }
+
                 drop(mcp_config_path);
-                drop(child);
+                anyhow::Ok(())
             })
             .detach();
 
@@ -170,6 +181,8 @@ impl AgentServer for ClaudeCode {
                     delegate,
                     outgoing_tx,
                     end_turn_tx,
+                    cancel_tx,
+                    session_id,
                     _handler_task: handler_task,
                     _mcp_server: None,
                 };
@@ -181,6 +194,19 @@ impl AgentServer for ClaudeCode {
     }
 }
 
+#[cfg(unix)]
+fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
+    let pid = nix::unistd::Pid::from_raw(pid);
+
+    nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
+        .map_err(|e| anyhow!("Failed to interrupt process: {}", e))
+}
+
+#[cfg(windows)]
+fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
+    panic!("Cancel not implemented on Windows")
+}
+
 impl AgentConnection for ClaudeAgentConnection {
     /// Send a request to the agent and wait for a response.
     fn request_any(
@@ -190,6 +216,8 @@ impl AgentConnection for ClaudeAgentConnection {
         let delegate = self.delegate.clone();
         let end_turn_tx = self.end_turn_tx.clone();
         let outgoing_tx = self.outgoing_tx.clone();
+        let mut cancel_tx = self.cancel_tx.clone();
+        let session_id = self.session_id;
         async move {
             match params {
                 // todo: consider sending an empty request so we get the init response?
@@ -228,26 +256,83 @@ impl AgentConnection for ClaudeAgentConnection {
                             stop_sequence: None,
                             usage: None,
                         },
-                        session_id: None,
+                        session_id: Some(session_id),
                     })?;
                     rx.await??;
                     Ok(AnyAgentResult::SendUserMessageResponse(
                         acp::SendUserMessageResponse,
                     ))
                 }
-                AnyAgentRequest::CancelSendMessageParams(_) => Ok(
-                    AnyAgentResult::CancelSendMessageResponse(acp::CancelSendMessageResponse),
-                ),
+                AnyAgentRequest::CancelSendMessageParams(_) => {
+                    let (done_tx, done_rx) = oneshot::channel();
+                    cancel_tx.send(done_tx).await?;
+                    done_rx.await??;
+
+                    Ok(AnyAgentResult::CancelSendMessageResponse(
+                        acp::CancelSendMessageResponse,
+                    ))
+                }
             }
         }
         .boxed_local()
     }
 }
 
+#[derive(Clone, Copy)]
+enum ClaudeSessionMode {
+    Start,
+    Resume,
+}
+
+async fn spawn_claude(
+    command: &AgentServerCommand,
+    mode: ClaudeSessionMode,
+    session_id: Uuid,
+    mcp_config_path: &Path,
+    root_dir: &Path,
+) -> Result<Child> {
+    let child = util::command::new_smol_command(&command.path)
+        .args([
+            "--input-format",
+            "stream-json",
+            "--output-format",
+            "stream-json",
+            "--print",
+            "--verbose",
+            "--mcp-config",
+            mcp_config_path.to_string_lossy().as_ref(),
+            "--permission-prompt-tool",
+            &format!(
+                "mcp__{}__{}",
+                mcp_server::SERVER_NAME,
+                mcp_server::PERMISSION_TOOL
+            ),
+            "--allowedTools",
+            "mcp__zed__Read,mcp__zed__Edit",
+            "--disallowedTools",
+            "Read,Edit",
+        ])
+        .args(match mode {
+            ClaudeSessionMode::Start => ["--session-id".to_string(), session_id.to_string()],
+            ClaudeSessionMode::Resume => ["--resume".to_string(), session_id.to_string()],
+        })
+        .args(command.args.iter().map(|arg| arg.as_str()))
+        .current_dir(root_dir)
+        .stdin(std::process::Stdio::piped())
+        .stdout(std::process::Stdio::piped())
+        .stderr(std::process::Stdio::inherit())
+        .kill_on_drop(true)
+        .spawn()?;
+
+    Ok(child)
+}
+
 struct ClaudeAgentConnection {
     delegate: AcpClientDelegate,
+    session_id: Uuid,
     outgoing_tx: UnboundedSender<SdkMessage>,
     end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
+    cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
     _mcp_server: Option<ZedMcpServer>,
     _handler_task: Task<()>,
 }
@@ -349,7 +434,7 @@ impl ClaudeAgentConnection {
         incoming_tx: UnboundedSender<SdkMessage>,
         mut outgoing_bytes: impl Unpin + AsyncWrite,
         incoming_bytes: impl Unpin + AsyncRead,
-    ) -> Result<()> {
+    ) -> Result<UnboundedReceiver<SdkMessage>> {
         let mut output_reader = BufReader::new(incoming_bytes);
         let mut outgoing_line = Vec::new();
         let mut incoming_line = String::new();
@@ -383,7 +468,8 @@ impl ClaudeAgentConnection {
                 }
             }
         }
-        Ok(())
+
+        Ok(outgoing_rx)
     }
 }
 
@@ -506,14 +592,14 @@ enum SdkMessage {
     Assistant {
         message: Message, // from Anthropic SDK
         #[serde(skip_serializing_if = "Option::is_none")]
-        session_id: Option<String>,
+        session_id: Option<Uuid>,
     },
 
     // A user message
     User {
         message: Message, // from Anthropic SDK
         #[serde(skip_serializing_if = "Option::is_none")]
-        session_id: Option<String>,
+        session_id: Option<Uuid>,
     },
 
     // Emitted as the last message in a conversation

crates/agent_servers/src/codex.rs 🔗

@@ -94,8 +94,7 @@ impl AgentServer for Codex {
             let codex_mcp_client: Arc<ContextServer> = ContextServer::stdio(
                 ContextServerId("codex-mcp-server".into()),
                 ContextServerCommand {
-                    // todo! should we change ContextServerCommand to take a PathBuf?
-                    path: command.path.to_string_lossy().to_string(),
+                    path: command.path,
                     args: command.args,
                     env: command.env,
                 },

crates/agent_servers/src/e2e_tests.rs 🔗

@@ -350,6 +350,8 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
                 claude: Some(AgentServerSettings {
                     command: crate::claude::tests::local_command(),
                 }),
+                // todo!
+                codex: None,
                 gemini: Some(AgentServerSettings {
                     command: crate::gemini::tests::local_command(),
                 }),

crates/agent_ui/src/active_thread.rs 🔗

@@ -3724,8 +3724,11 @@ pub(crate) fn open_context(
 
         AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
             if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                panel.update(cx, |panel, cx| {
-                    panel.open_thread(thread_context.thread.clone(), window, cx);
+                let thread = thread_context.thread.clone();
+                window.defer(cx, move |window, cx| {
+                    panel.update(cx, |panel, cx| {
+                        panel.open_thread(thread, window, cx);
+                    });
                 });
             }
         }),
@@ -3733,8 +3736,11 @@ pub(crate) fn open_context(
         AgentContextHandle::TextThread(text_thread_context) => {
             workspace.update(cx, |workspace, cx| {
                 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                    panel.update(cx, |panel, cx| {
-                        panel.open_prompt_editor(text_thread_context.context.clone(), window, cx)
+                    let context = text_thread_context.context.clone();
+                    window.defer(cx, move |window, cx| {
+                        panel.update(cx, |panel, cx| {
+                            panel.open_prompt_editor(context, window, cx)
+                        });
                     });
                 }
             })

crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs 🔗

@@ -1,4 +1,5 @@
 use std::{
+    path::PathBuf,
     sync::{Arc, Mutex},
     time::Duration,
 };
@@ -188,7 +189,7 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
         }
         None => (
             "some-mcp-server".to_string(),
-            "".to_string(),
+            PathBuf::new(),
             "[]".to_string(),
             "{}".to_string(),
         ),
@@ -199,13 +200,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
   /// The name of your MCP server
   "{name}": {{
     /// The command which runs the MCP server
-    "command": "{command}",
+    "command": "{}",
     /// The arguments to pass to the MCP server
     "args": {args},
     /// The environment variables to set
     "env": {env}
   }}
-}}"#
+}}"#,
+        command.display()
     )
 }
 

crates/agent_ui/src/agent_panel.rs 🔗

@@ -12,6 +12,7 @@ use serde::{Deserialize, Serialize};
 use crate::NewExternalAgentThread;
 use crate::agent_diff::AgentDiffThread;
 use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
+use crate::ui::NewThreadButton;
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
     DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -66,8 +67,8 @@ use theme::ThemeSettings;
 use time::UtcOffset;
 use ui::utils::WithRemSize;
 use ui::{
-    Banner, Callout, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu, PopoverMenuHandle,
-    ProgressBar, Tab, Tooltip, prelude::*,
+    Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
+    PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
 };
 use util::ResultExt as _;
 use workspace::{
@@ -1906,16 +1907,39 @@ impl AgentPanel {
                         .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
                             this.header("Zed Agent")
                         })
-                        .action("New Thread", NewThread::default().boxed_clone())
-                        .action("New Text Thread", NewTextThread.boxed_clone())
+                        .item(
+                            ContextMenuEntry::new("New Thread")
+                                .icon(IconName::NewThread)
+                                .icon_color(Color::Muted)
+                                .handler(move |window, cx| {
+                                    window.dispatch_action(NewThread::default().boxed_clone(), cx);
+                                }),
+                        )
+                        .item(
+                            ContextMenuEntry::new("New Text Thread")
+                                .icon(IconName::NewTextThread)
+                                .icon_color(Color::Muted)
+                                .handler(move |window, cx| {
+                                    window.dispatch_action(NewTextThread.boxed_clone(), cx);
+                                }),
+                        )
                         .when_some(active_thread, |this, active_thread| {
                             let thread = active_thread.read(cx);
+
                             if !thread.is_empty() {
-                                this.action(
-                                    "New From Summary",
-                                    Box::new(NewThread {
-                                        from_thread_id: Some(thread.id().clone()),
-                                    }),
+                                let thread_id = thread.id().clone();
+                                this.item(
+                                    ContextMenuEntry::new("New From Summary")
+                                        .icon(IconName::NewFromSummary)
+                                        .icon_color(Color::Muted)
+                                        .handler(move |window, cx| {
+                                            window.dispatch_action(
+                                                Box::new(NewThread {
+                                                    from_thread_id: Some(thread_id.clone()),
+                                                }),
+                                                cx,
+                                            );
+                                        }),
                                 )
                             } else {
                                 this
@@ -1924,19 +1948,33 @@ impl AgentPanel {
                         .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
                             this.separator()
                                 .header("External Agents")
-                                .action(
-                                    "New Gemini Thread",
-                                    NewExternalAgentThread {
-                                        agent: Some(crate::ExternalAgent::Gemini),
-                                    }
-                                    .boxed_clone(),
+                                .item(
+                                    ContextMenuEntry::new("New Gemini Thread")
+                                        .icon(IconName::AiGemini)
+                                        .icon_color(Color::Muted)
+                                        .handler(move |window, cx| {
+                                            window.dispatch_action(
+                                                NewExternalAgentThread {
+                                                    agent: Some(crate::ExternalAgent::Gemini),
+                                                }
+                                                .boxed_clone(),
+                                                cx,
+                                            );
+                                        }),
                                 )
-                                .action(
-                                    "New Claude Code Thread",
-                                    NewExternalAgentThread {
-                                        agent: Some(crate::ExternalAgent::ClaudeCode),
-                                    }
-                                    .boxed_clone(),
+                                .item(
+                                    ContextMenuEntry::new("New Claude Code Thread")
+                                        .icon(IconName::AiClaude)
+                                        .icon_color(Color::Muted)
+                                        .handler(move |window, cx| {
+                                            window.dispatch_action(
+                                                NewExternalAgentThread {
+                                                    agent: Some(crate::ExternalAgent::ClaudeCode),
+                                                }
+                                                .boxed_clone(),
+                                                cx,
+                                            );
+                                        }),
                                 )
                                 .action(
                                     "New Codex Thread",
@@ -2269,7 +2307,20 @@ impl AgentPanel {
             return None;
         }
 
-        Some(div().size_full().child(self.onboarding.clone()))
+        let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
+        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
+
+        Some(
+            div()
+                .size_full()
+                .when(thread_view, |this| {
+                    this.bg(cx.theme().colors().panel_background)
+                })
+                .when(text_thread_view, |this| {
+                    this.bg(cx.theme().colors().editor_background)
+                })
+                .child(self.onboarding.clone()),
+        )
     }
 
     fn render_trial_end_upsell(
@@ -2292,6 +2343,28 @@ impl AgentPanel {
         })))
     }
 
+    fn render_empty_state_section_header(
+        &self,
+        label: impl Into<SharedString>,
+        action_slot: Option<AnyElement>,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement {
+        h_flex()
+            .mt_2()
+            .pl_1p5()
+            .pb_1()
+            .w_full()
+            .justify_between()
+            .border_b_1()
+            .border_color(cx.theme().colors().border_variant)
+            .child(
+                Label::new(label.into())
+                    .size(LabelSize::Small)
+                    .color(Color::Muted),
+            )
+            .children(action_slot)
+    }
+
     fn render_thread_empty_state(
         &self,
         window: &mut Window,
@@ -2414,19 +2487,9 @@ impl AgentPanel {
                     .justify_end()
                     .gap_1()
                     .child(
-                        h_flex()
-                            .pl_1p5()
-                            .pb_1()
-                            .w_full()
-                            .justify_between()
-                            .border_b_1()
-                            .border_color(cx.theme().colors().border_variant)
-                            .child(
-                                Label::new("Recent")
-                                    .size(LabelSize::Small)
-                                    .color(Color::Muted),
-                            )
-                            .child(
+                        self.render_empty_state_section_header(
+                            "Recent",
+                            Some(
                                 Button::new("view-history", "View All")
                                     .style(ButtonStyle::Subtle)
                                     .label_size(LabelSize::Small)
@@ -2441,8 +2504,11 @@ impl AgentPanel {
                                     )
                                     .on_click(move |_event, window, cx| {
                                         window.dispatch_action(OpenHistory.boxed_clone(), cx);
-                                    }),
+                                    })
+                                    .into_any_element(),
                             ),
+                            cx,
+                        ),
                     )
                     .child(
                         v_flex()
@@ -2470,6 +2536,113 @@ impl AgentPanel {
                                 },
                             )),
                     )
+                    .child(self.render_empty_state_section_header("Start", None, cx))
+                    .child(
+                        v_flex()
+                            .p_1()
+                            .gap_2()
+                            .child(
+                                h_flex()
+                                    .w_full()
+                                    .gap_2()
+                                    .child(
+                                        NewThreadButton::new(
+                                            "new-thread-btn",
+                                            "New Thread",
+                                            IconName::NewThread,
+                                        )
+                                        .keybinding(KeyBinding::for_action_in(
+                                            &NewThread::default(),
+                                            &self.focus_handle(cx),
+                                            window,
+                                            cx,
+                                        ))
+                                        .on_click(
+                                            |window, cx| {
+                                                window.dispatch_action(
+                                                    NewThread::default().boxed_clone(),
+                                                    cx,
+                                                )
+                                            },
+                                        ),
+                                    )
+                                    .child(
+                                        NewThreadButton::new(
+                                            "new-text-thread-btn",
+                                            "New Text Thread",
+                                            IconName::NewTextThread,
+                                        )
+                                        .keybinding(KeyBinding::for_action_in(
+                                            &NewTextThread,
+                                            &self.focus_handle(cx),
+                                            window,
+                                            cx,
+                                        ))
+                                        .on_click(
+                                            |window, cx| {
+                                                window.dispatch_action(Box::new(NewTextThread), cx)
+                                            },
+                                        ),
+                                    ),
+                            )
+                            .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
+                                this.child(
+                                    h_flex()
+                                        .w_full()
+                                        .gap_2()
+                                        .child(
+                                            NewThreadButton::new(
+                                                "new-gemini-thread-btn",
+                                                "New Gemini Thread",
+                                                IconName::AiGemini,
+                                            )
+                                            // .keybinding(KeyBinding::for_action_in(
+                                            //     &OpenHistory,
+                                            //     &self.focus_handle(cx),
+                                            //     window,
+                                            //     cx,
+                                            // ))
+                                            .on_click(
+                                                |window, cx| {
+                                                    window.dispatch_action(
+                                                        Box::new(NewExternalAgentThread {
+                                                            agent: Some(
+                                                                crate::ExternalAgent::Gemini,
+                                                            ),
+                                                        }),
+                                                        cx,
+                                                    )
+                                                },
+                                            ),
+                                        )
+                                        .child(
+                                            NewThreadButton::new(
+                                                "new-claude-thread-btn",
+                                                "New Claude Code Thread",
+                                                IconName::AiClaude,
+                                            )
+                                            // .keybinding(KeyBinding::for_action_in(
+                                            //     &OpenHistory,
+                                            //     &self.focus_handle(cx),
+                                            //     window,
+                                            //     cx,
+                                            // ))
+                                            .on_click(
+                                                |window, cx| {
+                                                    window.dispatch_action(
+                                                        Box::new(NewExternalAgentThread {
+                                                            agent: Some(
+                                                                crate::ExternalAgent::ClaudeCode,
+                                                            ),
+                                                        }),
+                                                        cx,
+                                                    )
+                                                },
+                                            ),
+                                        ),
+                                )
+                            }),
+                    )
                     .when_some(configuration_error.as_ref(), |this, err| {
                         this.child(self.render_configuration_error(err, &focus_handle, window, cx))
                     })
@@ -3084,7 +3257,20 @@ impl Render for AgentPanel {
                                 .into_any(),
                         )
                     })
-                    .child(h_flex().child(message_editor.clone()))
+                    .child(h_flex().relative().child(message_editor.clone()).when(
+                        !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
+                        |this| {
+                            this.child(
+                                div()
+                                    .size_full()
+                                    .absolute()
+                                    .inset_0()
+                                    .bg(cx.theme().colors().panel_background)
+                                    .opacity(0.8)
+                                    .block_mouse_except_scroll(),
+                            )
+                        },
+                    ))
                     .child(self.render_drag_target(cx)),
                 ActiveView::ExternalAgentThread { thread_view, .. } => parent
                     .relative()

crates/agent_ui/src/message_editor.rs 🔗

@@ -14,6 +14,7 @@ use agent::{
     context_store::ContextStoreEvent,
 };
 use agent_settings::{AgentSettings, CompletionMode};
+use ai_onboarding::ApiKeysWithProviders;
 use buffer_diff::BufferDiff;
 use client::UserStore;
 use collections::{HashMap, HashSet};
@@ -33,7 +34,8 @@ use gpui::{
 };
 use language::{Buffer, Language, Point};
 use language_model::{
-    ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
+    ConfiguredModel, LanguageModelRegistry, LanguageModelRequestMessage, MessageContent,
+    ZED_CLOUD_PROVIDER_ID,
 };
 use multi_buffer;
 use project::Project;
@@ -1655,9 +1657,34 @@ impl Render for MessageEditor {
 
         let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
 
+        let in_pro_trial = matches!(
+            self.user_store.read(cx).current_plan(),
+            Some(proto::Plan::ZedProTrial)
+        );
+
+        let pro_user = matches!(
+            self.user_store.read(cx).current_plan(),
+            Some(proto::Plan::ZedPro)
+        );
+
+        let configured_providers: Vec<(IconName, SharedString)> =
+            LanguageModelRegistry::read_global(cx)
+                .providers()
+                .iter()
+                .filter(|provider| {
+                    provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
+                })
+                .map(|provider| (provider.icon(), provider.name().0.clone()))
+                .collect();
+        let has_existing_providers = configured_providers.len() > 0;
+
         v_flex()
             .size_full()
             .bg(cx.theme().colors().panel_background)
+            .when(
+                has_existing_providers && !in_pro_trial && !pro_user,
+                |this| this.child(cx.new(ApiKeysWithProviders::new)),
+            )
             .when(changed_buffers.len() > 0, |parent| {
                 parent.child(self.render_edits_bar(&changed_buffers, window, cx))
             })

crates/agent_ui/src/ui.rs 🔗

@@ -2,6 +2,7 @@ mod agent_notification;
 mod burn_mode_tooltip;
 mod context_pill;
 mod end_trial_upsell;
+mod new_thread_button;
 mod onboarding_modal;
 pub mod preview;
 mod upsell;
@@ -10,4 +11,5 @@ pub use agent_notification::*;
 pub use burn_mode_tooltip::*;
 pub use context_pill::*;
 pub use end_trial_upsell::*;
+pub use new_thread_button::*;
 pub use onboarding_modal::*;

crates/agent_ui/src/ui/new_thread_button.rs 🔗

@@ -0,0 +1,75 @@
+use gpui::{ClickEvent, ElementId, IntoElement, ParentElement, Styled};
+use ui::prelude::*;
+
+#[derive(IntoElement)]
+pub struct NewThreadButton {
+    id: ElementId,
+    label: SharedString,
+    icon: IconName,
+    keybinding: Option<ui::KeyBinding>,
+    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
+}
+
+impl NewThreadButton {
+    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>, icon: IconName) -> Self {
+        Self {
+            id: id.into(),
+            label: label.into(),
+            icon,
+            keybinding: None,
+            on_click: None,
+        }
+    }
+
+    pub fn keybinding(mut self, keybinding: Option<ui::KeyBinding>) -> Self {
+        self.keybinding = keybinding;
+        self
+    }
+
+    pub fn on_click<F>(mut self, handler: F) -> Self
+    where
+        F: Fn(&mut Window, &mut App) + 'static,
+    {
+        self.on_click = Some(Box::new(
+            move |_: &ClickEvent, window: &mut Window, cx: &mut App| handler(window, cx),
+        ));
+        self
+    }
+}
+
+impl RenderOnce for NewThreadButton {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        h_flex()
+            .id(self.id)
+            .w_full()
+            .py_1p5()
+            .px_2()
+            .gap_1()
+            .justify_between()
+            .rounded_md()
+            .border_1()
+            .border_color(cx.theme().colors().border.opacity(0.4))
+            .bg(cx.theme().colors().element_active.opacity(0.2))
+            .hover(|style| {
+                style
+                    .bg(cx.theme().colors().element_hover)
+                    .border_color(cx.theme().colors().border)
+            })
+            .child(
+                h_flex()
+                    .gap_1p5()
+                    .child(
+                        Icon::new(self.icon)
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .child(Label::new(self.label).size(LabelSize::Small)),
+            )
+            .when_some(self.keybinding, |this, keybinding| {
+                this.child(keybinding.size(rems_from_px(10.)))
+            })
+            .when_some(self.on_click, |this, on_click| {
+                this.on_click(move |event, window, cx| on_click(event, window, cx))
+            })
+    }
+}

crates/ai_onboarding/src/agent_api_keys_onboarding.rs 🔗

@@ -0,0 +1,135 @@
+use gpui::{Action, IntoElement, ParentElement, RenderOnce, point};
+use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
+use ui::{Divider, List, prelude::*};
+
+use crate::BulletItem;
+
+pub struct ApiKeysWithProviders {
+    configured_providers: Vec<(IconName, SharedString)>,
+}
+
+impl ApiKeysWithProviders {
+    pub fn new(cx: &mut Context<Self>) -> Self {
+        cx.subscribe(
+            &LanguageModelRegistry::global(cx),
+            |this: &mut Self, _registry, event: &language_model::Event, cx| match event {
+                language_model::Event::ProviderStateChanged
+                | language_model::Event::AddedProvider(_)
+                | language_model::Event::RemovedProvider(_) => {
+                    this.configured_providers = Self::compute_configured_providers(cx)
+                }
+                _ => {}
+            },
+        )
+        .detach();
+
+        Self {
+            configured_providers: Self::compute_configured_providers(cx),
+        }
+    }
+
+    fn compute_configured_providers(cx: &App) -> Vec<(IconName, SharedString)> {
+        LanguageModelRegistry::read_global(cx)
+            .providers()
+            .iter()
+            .filter(|provider| {
+                provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
+            })
+            .map(|provider| (provider.icon(), provider.name().0.clone()))
+            .collect()
+    }
+
+    pub fn has_providers(&self) -> bool {
+        !self.configured_providers.is_empty()
+    }
+}
+
+impl Render for ApiKeysWithProviders {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let configured_providers_list =
+            self.configured_providers
+                .iter()
+                .cloned()
+                .map(|(icon, name)| {
+                    h_flex()
+                        .gap_1p5()
+                        .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
+                        .child(Label::new(name))
+                });
+
+        h_flex()
+            .mx_2p5()
+            .p_1()
+            .pb_0()
+            .gap_2()
+            .rounded_t_lg()
+            .border_t_1()
+            .border_x_1()
+            .border_color(cx.theme().colors().border.opacity(0.5))
+            .bg(cx.theme().colors().background.alpha(0.5))
+            .shadow(vec![gpui::BoxShadow {
+                color: gpui::black().opacity(0.15),
+                offset: point(px(1.), px(-1.)),
+                blur_radius: px(3.),
+                spread_radius: px(0.),
+            }])
+            .child(
+                h_flex()
+                    .px_2p5()
+                    .py_1p5()
+                    .gap_2()
+                    .flex_wrap()
+                    .rounded_t(px(5.))
+                    .overflow_hidden()
+                    .border_t_1()
+                    .border_x_1()
+                    .border_color(cx.theme().colors().border)
+                    .bg(cx.theme().colors().panel_background)
+                    .child(Icon::new(IconName::Info).size(IconSize::XSmall).color(Color::Muted))
+                    .child(Label::new("Or start now using API keys from your environment for the following providers:").color(Color::Muted))
+                    .children(configured_providers_list)
+            )
+    }
+}
+
+#[derive(IntoElement)]
+pub struct ApiKeysWithoutProviders;
+
+impl ApiKeysWithoutProviders {
+    pub fn new() -> Self {
+        Self
+    }
+}
+
+impl RenderOnce for ApiKeysWithoutProviders {
+    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
+        v_flex()
+            .mt_2()
+            .gap_1()
+            .child(
+                h_flex()
+                    .gap_2()
+                    .child(
+                        Label::new("API Keys")
+                            .size(LabelSize::Small)
+                            .color(Color::Muted)
+                            .buffer_font(cx),
+                    )
+                    .child(Divider::horizontal()),
+            )
+            .child(List::new().child(BulletItem::new(
+                "You can also use AI in Zed by bringing your own API keys",
+            )))
+            .child(
+                Button::new("configure-providers", "Configure Providers")
+                    .full_width()
+                    .style(ButtonStyle::Outlined)
+                    .on_click(move |_, window, cx| {
+                        window.dispatch_action(
+                            zed_actions::agent::OpenConfiguration.boxed_clone(),
+                            cx,
+                        );
+                    }),
+            )
+    }
+}

crates/ai_onboarding/src/agent_panel_onboarding_card.rs 🔗

@@ -24,7 +24,7 @@ impl ParentElement for AgentPanelOnboardingCard {
 impl RenderOnce for AgentPanelOnboardingCard {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
         div()
-            .m_4()
+            .m_2p5()
             .p(px(3.))
             .elevation_2(cx)
             .rounded_lg()
@@ -49,6 +49,7 @@ impl RenderOnce for AgentPanelOnboardingCard {
                             .right_0()
                             .w(px(400.))
                             .h(px(92.))
+                            .rounded_md()
                             .child(
                                 Vector::new(
                                     VectorName::AiGrid,
@@ -61,11 +62,12 @@ impl RenderOnce for AgentPanelOnboardingCard {
                     .child(
                         div()
                             .absolute()
-                            .top_0()
-                            .right_0()
+                            .top_0p5()
+                            .right_0p5()
                             .w(px(660.))
                             .h(px(401.))
                             .overflow_hidden()
+                            .rounded_md()
                             .bg(linear_gradient(
                                 75.,
                                 linear_color_stop(

crates/ai_onboarding/src/agent_panel_onboarding_content.rs 🔗

@@ -1,12 +1,11 @@
 use std::sync::Arc;
 
 use client::{Client, UserStore};
-use gpui::{Action, ClickEvent, Entity, IntoElement, ParentElement};
+use gpui::{Entity, IntoElement, ParentElement};
 use language_model::{LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
-use ui::{Divider, List, prelude::*};
-use zed_actions::agent::{OpenConfiguration, ToggleModelSelector};
+use ui::prelude::*;
 
-use crate::{AgentPanelOnboardingCard, BulletItem, ZedAiOnboarding};
+use crate::{AgentPanelOnboardingCard, ApiKeysWithoutProviders, ZedAiOnboarding};
 
 pub struct AgentPanelOnboarding {
     user_store: Entity<UserStore>,
@@ -53,93 +52,34 @@ impl AgentPanelOnboarding {
             .map(|provider| (provider.icon(), provider.name().0.clone()))
             .collect()
     }
-
-    fn configure_providers(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
-        window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
-        cx.notify();
-    }
-
-    fn render_api_keys_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
-        let has_existing_providers = self.configured_providers.len() > 0;
-        let configure_provider_label = if has_existing_providers {
-            "Configure Other Provider"
-        } else {
-            "Configure Providers"
-        };
-
-        let content = if has_existing_providers {
-            List::new()
-                    .child(BulletItem::new(
-                        "Or start now using API keys from your environment for the following providers:"
-                    ))
-                    .child(
-                        h_flex()
-                            .px_5()
-                            .gap_2()
-                            .flex_wrap()
-                            .children(self.configured_providers.iter().cloned().map(|(icon, name)|
-                                h_flex()
-                                    .gap_1p5()
-                                    .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
-                                    .child(Label::new(name))
-                            ))
-                    )
-                    .child(BulletItem::new(
-                        "No need for any of the plans or even to sign in",
-                    ))
-        } else {
-            List::new()
-                .child(BulletItem::new(
-                    "You can also use AI in Zed by bringing your own API keys",
-                ))
-                .child(BulletItem::new(
-                    "No need for any of the plans or even to sign in",
-                ))
-        };
-
-        v_flex()
-            .mt_2()
-            .gap_1()
-            .child(
-                h_flex()
-                    .gap_2()
-                    .child(
-                        Label::new("API Keys")
-                            .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .buffer_font(cx),
-                    )
-                    .child(Divider::horizontal()),
-            )
-            .child(content)
-            .when(has_existing_providers, |this| {
-                this.child(
-                    Button::new("pick-model", "Choose Model")
-                        .full_width()
-                        .style(ButtonStyle::Outlined)
-                        .on_click(|_event, window, cx| {
-                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
-                        }),
-                )
-            })
-            .child(
-                Button::new("configure-providers", configure_provider_label)
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .on_click(cx.listener(Self::configure_providers)),
-            )
-    }
 }
 
 impl Render for AgentPanelOnboarding {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let enrolled_in_trial = matches!(
+            self.user_store.read(cx).current_plan(),
+            Some(proto::Plan::ZedProTrial)
+        );
+
         AgentPanelOnboardingCard::new()
-            .child(ZedAiOnboarding::new(
-                self.client.clone(),
-                &self.user_store,
-                self.continue_with_zed_ai.clone(),
-                cx,
-            ))
-            .child(self.render_api_keys_section(cx))
+            .child(
+                ZedAiOnboarding::new(
+                    self.client.clone(),
+                    &self.user_store,
+                    self.continue_with_zed_ai.clone(),
+                    cx,
+                )
+                .with_dismiss({
+                    let callback = self.continue_with_zed_ai.clone();
+                    move |window, cx| callback(window, cx)
+                }),
+            )
+            .map(|this| {
+                if enrolled_in_trial || self.configured_providers.len() >= 1 {
+                    this
+                } else {
+                    this.child(ApiKeysWithoutProviders::new())
+                }
+            })
     }
 }

crates/ai_onboarding/src/ai_onboarding.rs 🔗

@@ -1,8 +1,10 @@
+mod agent_api_keys_onboarding;
 mod agent_panel_onboarding_card;
 mod agent_panel_onboarding_content;
 mod edit_prediction_onboarding_content;
 mod young_account_banner;
 
+pub use agent_api_keys_onboarding::{ApiKeysWithProviders, ApiKeysWithoutProviders};
 pub use agent_panel_onboarding_card::AgentPanelOnboardingCard;
 pub use agent_panel_onboarding_content::AgentPanelOnboarding;
 pub use edit_prediction_onboarding_content::EditPredictionOnboarding;
@@ -12,7 +14,7 @@ use std::sync::Arc;
 
 use client::{Client, UserStore, zed_urls};
 use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
-use ui::{Divider, List, ListItem, RegisterComponent, TintColor, prelude::*};
+use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
 
 pub struct BulletItem {
     label: SharedString,
@@ -69,6 +71,7 @@ pub struct ZedAiOnboarding {
     pub continue_with_zed_ai: Arc<dyn Fn(&mut Window, &mut App)>,
     pub sign_in: Arc<dyn Fn(&mut Window, &mut App)>,
     pub accept_terms_of_service: Arc<dyn Fn(&mut Window, &mut App)>,
+    pub dismiss_onboarding: Option<Arc<dyn Fn(&mut Window, &mut App)>>,
 }
 
 impl ZedAiOnboarding {
@@ -80,6 +83,7 @@ impl ZedAiOnboarding {
     ) -> Self {
         let store = user_store.read(cx);
         let status = *client.status().borrow();
+
         Self {
             sign_in_status: status.into(),
             has_accepted_terms_of_service: store.current_user_has_accepted_terms().unwrap_or(false),
@@ -102,14 +106,22 @@ impl ZedAiOnboarding {
                 })
                 .detach();
             }),
+            dismiss_onboarding: None,
         }
     }
 
-    fn render_free_plan_section(&self, cx: &mut App) -> impl IntoElement {
+    pub fn with_dismiss(
+        mut self,
+        dismiss_callback: impl Fn(&mut Window, &mut App) + 'static,
+    ) -> Self {
+        self.dismiss_onboarding = Some(Arc::new(dismiss_callback));
+        self
+    }
+
+    fn free_plan_definition(&self, cx: &mut App) -> impl IntoElement {
         v_flex()
             .mt_2()
             .gap_1()
-            .when(self.account_too_young, |this| this.opacity(0.4))
             .child(
                 h_flex()
                     .gap_2()
@@ -119,6 +131,12 @@ impl ZedAiOnboarding {
                             .color(Color::Muted)
                             .buffer_font(cx),
                     )
+                    .child(
+                        Label::new("(Current Plan)")
+                            .size(LabelSize::Small)
+                            .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
+                            .buffer_font(cx),
+                    )
                     .child(Divider::horizontal()),
             )
             .child(
@@ -130,65 +148,89 @@ impl ZedAiOnboarding {
                         "2000 accepted edit predictions using our open-source Zeta model",
                     )),
             )
-            .child(
-                Button::new("continue", "Continue Free")
-                    .disabled(self.account_too_young)
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .on_click({
-                        let callback = self.continue_with_zed_ai.clone();
-                        move |_, window, cx| callback(window, cx)
-                    }),
-            )
     }
 
-    fn render_pro_plan_section(&self, cx: &mut App) -> impl IntoElement {
-        let (button_label, button_url) = if self.account_too_young {
-            ("Start with Pro", zed_urls::upgrade_to_zed_pro_url(cx))
-        } else {
-            ("Start Pro Trial", zed_urls::account_url(cx))
-        };
+    fn pro_trial_definition(&self) -> impl IntoElement {
+        List::new()
+            .child(BulletItem::new(
+                "150 prompts per month with the Claude models",
+            ))
+            .child(BulletItem::new(
+                "Unlimited accepted edit predictions using our open-source Zeta model",
+            ))
+    }
 
-        v_flex()
-            .mt_2()
-            .gap_1()
-            .child(
-                h_flex()
-                    .gap_2()
-                    .child(
-                        Label::new("Pro")
-                            .size(LabelSize::Small)
-                            .color(Color::Accent)
-                            .buffer_font(cx),
-                    )
-                    .child(Divider::horizontal()),
-            )
-            .child(
-                List::new()
-                    .child(BulletItem::new("500 prompts per month with Claude models"))
-                    .child(BulletItem::new("Unlimited edit predictions"))
-                    .when(!self.account_too_young, |this| {
-                        this.child(BulletItem::new(
-                            "Try it out for 14 days with no charge, no credit card required",
+    fn pro_plan_definition(&self, cx: &mut App) -> impl IntoElement {
+        v_flex().mt_2().gap_1().map(|this| {
+            if self.account_too_young {
+                this.child(
+                    h_flex()
+                        .gap_2()
+                        .child(
+                            Label::new("Pro")
+                                .size(LabelSize::Small)
+                                .color(Color::Accent)
+                                .buffer_font(cx),
+                        )
+                        .child(Divider::horizontal()),
+                )
+                .child(
+                    List::new()
+                        .child(BulletItem::new("500 prompts per month with Claude models"))
+                        .child(BulletItem::new(
+                            "Unlimited accepted edit predictions using our open-source Zeta model",
                         ))
-                    }),
-            )
-            .child(
-                Button::new("pro", button_label)
-                    .full_width()
-                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
-                    .on_click(move |_, _window, cx| cx.open_url(&button_url)),
-            )
+                        .child(BulletItem::new("USD $20 per month")),
+                )
+                .child(
+                    Button::new("pro", "Start with Pro")
+                        .full_width()
+                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                        .on_click(move |_, _window, cx| {
+                            cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
+                        }),
+                )
+            } else {
+                this.child(
+                    h_flex()
+                        .gap_2()
+                        .child(
+                            Label::new("Pro Trial")
+                                .size(LabelSize::Small)
+                                .color(Color::Accent)
+                                .buffer_font(cx),
+                        )
+                        .child(Divider::horizontal()),
+                )
+                .child(
+                    List::new()
+                        .child(self.pro_trial_definition())
+                        .child(BulletItem::new(
+                            "Try it out for 14 days with no charge and no credit card required",
+                        )),
+                )
+                .child(
+                    Button::new("pro", "Start Pro Trial")
+                        .full_width()
+                        .style(ButtonStyle::Tinted(ui::TintColor::Accent))
+                        .on_click(move |_, _window, cx| {
+                            cx.open_url(&zed_urls::start_trial_url(cx))
+                        }),
+                )
+            }
+        })
     }
 
-    fn render_accept_terms_of_service(&self) -> Div {
+    fn render_accept_terms_of_service(&self) -> AnyElement {
         v_flex()
-            .w_full()
             .gap_1()
+            .w_full()
             .child(Headline::new("Before starting…"))
-            .child(Label::new(
-                "Make sure you have read and accepted Zed AI's terms of service.",
-            ))
+            .child(
+                Label::new("Make sure you have read and accepted Zed AI's terms of service.")
+                    .color(Color::Muted)
+                    .mb_2(),
+            )
             .child(
                 Button::new("terms_of_service", "View and Read the Terms of Service")
                     .full_width()
@@ -196,9 +238,7 @@ impl ZedAiOnboarding {
                     .icon(IconName::ArrowUpRight)
                     .icon_color(Color::Muted)
                     .icon_size(IconSize::XSmall)
-                    .on_click(move |_, _window, cx| {
-                        cx.open_url("https://zed.dev/terms-of-service")
-                    }),
+                    .on_click(move |_, _window, cx| cx.open_url(&zed_urls::terms_of_service(cx))),
             )
             .child(
                 Button::new("accept_terms", "I've read it and accept it")
@@ -209,23 +249,23 @@ impl ZedAiOnboarding {
                         move |_, window, cx| (callback)(window, cx)
                     }),
             )
+            .into_any_element()
     }
 
-    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> Div {
-        const SIGN_IN_DISCLAIMER: &str =
-            "To start using AI in Zed with our hosted models, sign in and subscribe to a plan.";
+    fn render_sign_in_disclaimer(&self, _cx: &mut App) -> AnyElement {
         let signing_in = matches!(self.sign_in_status, SignInStatus::SigningIn);
 
         v_flex()
-            .gap_2()
+            .gap_1()
             .child(Headline::new("Welcome to Zed AI"))
-            .child(div().w_full().child(Label::new(SIGN_IN_DISCLAIMER)))
             .child(
-                Button::new("sign_in", "Sign In with GitHub")
-                    .icon(IconName::Github)
-                    .icon_position(IconPosition::Start)
-                    .icon_size(IconSize::Small)
-                    .icon_color(Color::Muted)
+                Label::new("Sign in to start using AI in Zed with a free trial of the Pro plan, which includes:")
+                    .color(Color::Muted)
+                    .mb_2(),
+            )
+            .child(self.pro_trial_definition())
+            .child(
+                Button::new("sign_in", "Sign in to Start Trial")
                     .disabled(signing_in)
                     .full_width()
                     .style(ButtonStyle::Tinted(ui::TintColor::Accent))
@@ -234,36 +274,55 @@ impl ZedAiOnboarding {
                         move |_, window, cx| callback(window, cx)
                     }),
             )
+            .into_any_element()
     }
 
-    fn render_free_plan_onboarding(&self, cx: &mut App) -> Div {
-        const PLANS_DESCRIPTION: &str = "Choose how you want to start.";
+    fn render_free_plan_state(&self, cx: &mut App) -> AnyElement {
         let young_account_banner = YoungAccountBanner;
 
         v_flex()
+            .relative()
+            .gap_1()
             .child(Headline::new("Welcome to Zed AI"))
             .child(
-                Label::new(PLANS_DESCRIPTION)
-                    .size(LabelSize::Small)
+                Label::new("Choose how you want to start.")
                     .color(Color::Muted)
-                    .mt_1()
-                    .mb_3(),
+                    .mb_2(),
             )
-            .when(self.account_too_young, |this| {
-                this.child(young_account_banner)
+            .map(|this| {
+                if self.account_too_young {
+                    this.child(young_account_banner)
+                } else {
+                    this.child(self.free_plan_definition(cx)).when_some(
+                        self.dismiss_onboarding.as_ref(),
+                        |this, dismiss_callback| {
+                            let callback = dismiss_callback.clone();
+
+                            this.child(
+                                h_flex().absolute().top_0().right_0().child(
+                                    IconButton::new("dismiss_onboarding", IconName::Close)
+                                        .icon_size(IconSize::Small)
+                                        .tooltip(Tooltip::text("Dismiss"))
+                                        .on_click(move |_, window, cx| callback(window, cx)),
+                                ),
+                            )
+                        },
+                    )
+                }
             })
-            .child(self.render_free_plan_section(cx))
-            .child(self.render_pro_plan_section(cx))
+            .child(self.pro_plan_definition(cx))
+            .into_any_element()
     }
 
-    fn render_trial_onboarding(&self, _cx: &mut App) -> Div {
+    fn render_trial_state(&self, _cx: &mut App) -> AnyElement {
         v_flex()
-            .child(Headline::new("Welcome to the trial of Zed Pro"))
+            .relative()
+            .gap_1()
+            .child(Headline::new("Welcome to the Zed Pro free trial"))
             .child(
                 Label::new("Here's what you get for the next 14 days:")
-                    .size(LabelSize::Small)
                     .color(Color::Muted)
-                    .mt_1(),
+                    .mb_2(),
             )
             .child(
                 List::new()
@@ -272,25 +331,31 @@ impl ZedAiOnboarding {
                         "Unlimited edit predictions with Zeta, our open-source model",
                     )),
             )
-            .child(
-                Button::new("trial", "Start Trial")
-                    .full_width()
-                    .style(ButtonStyle::Outlined)
-                    .on_click({
-                        let callback = self.continue_with_zed_ai.clone();
-                        move |_, window, cx| callback(window, cx)
-                    }),
+            .when_some(
+                self.dismiss_onboarding.as_ref(),
+                |this, dismiss_callback| {
+                    let callback = dismiss_callback.clone();
+                    this.child(
+                        h_flex().absolute().top_0().right_0().child(
+                            IconButton::new("dismiss_onboarding", IconName::Close)
+                                .icon_size(IconSize::Small)
+                                .tooltip(Tooltip::text("Dismiss"))
+                                .on_click(move |_, window, cx| callback(window, cx)),
+                        ),
+                    )
+                },
             )
+            .into_any_element()
     }
 
-    fn render_pro_plan_onboarding(&self, _cx: &mut App) -> Div {
+    fn render_pro_plan_state(&self, _cx: &mut App) -> AnyElement {
         v_flex()
+            .gap_1()
             .child(Headline::new("Welcome to Zed Pro"))
             .child(
                 Label::new("Here's what you get:")
-                    .size(LabelSize::Small)
                     .color(Color::Muted)
-                    .mt_1(),
+                    .mb_2(),
             )
             .child(
                 List::new()
@@ -306,6 +371,7 @@ impl ZedAiOnboarding {
                         move |_, window, cx| callback(window, cx)
                     }),
             )
+            .into_any_element()
     }
 }
 
@@ -314,9 +380,9 @@ impl RenderOnce for ZedAiOnboarding {
         if matches!(self.sign_in_status, SignInStatus::SignedIn) {
             if self.has_accepted_terms_of_service {
                 match self.plan {
-                    None | Some(proto::Plan::Free) => self.render_free_plan_onboarding(cx),
-                    Some(proto::Plan::ZedProTrial) => self.render_trial_onboarding(cx),
-                    Some(proto::Plan::ZedPro) => self.render_pro_plan_onboarding(cx),
+                    None | Some(proto::Plan::Free) => self.render_free_plan_state(cx),
+                    Some(proto::Plan::ZedProTrial) => self.render_trial_state(cx),
+                    Some(proto::Plan::ZedPro) => self.render_pro_plan_state(cx),
                 }
             } else {
                 self.render_accept_terms_of_service()
@@ -339,18 +405,17 @@ impl Component for ZedAiOnboarding {
             plan: Option<proto::Plan>,
             account_too_young: bool,
         ) -> AnyElement {
-            div()
-                .w(px(800.))
-                .child(ZedAiOnboarding {
-                    sign_in_status,
-                    has_accepted_terms_of_service,
-                    plan,
-                    account_too_young,
-                    continue_with_zed_ai: Arc::new(|_, _| {}),
-                    sign_in: Arc::new(|_, _| {}),
-                    accept_terms_of_service: Arc::new(|_, _| {}),
-                })
-                .into_any_element()
+            ZedAiOnboarding {
+                sign_in_status,
+                has_accepted_terms_of_service,
+                plan,
+                account_too_young,
+                continue_with_zed_ai: Arc::new(|_, _| {}),
+                sign_in: Arc::new(|_, _| {}),
+                accept_terms_of_service: Arc::new(|_, _| {}),
+                dismiss_onboarding: None,
+            }
+            .into_any_element()
         }
 
         Some(
@@ -368,7 +433,7 @@ impl Component for ZedAiOnboarding {
                     ),
                     single_example(
                         "Account too young",
-                        onboarding(SignInStatus::SignedIn, true, None, true),
+                        onboarding(SignInStatus::SignedIn, false, None, true),
                     ),
                     single_example(
                         "Free Plan",

crates/ai_onboarding/src/young_account_banner.rs 🔗

@@ -6,7 +6,7 @@ pub struct YoungAccountBanner;
 
 impl RenderOnce for YoungAccountBanner {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        const YOUNG_ACCOUNT_DISCLAIMER: &str = "Given your GitHub account was created less than 30 days ago, we cannot put you in the Free plan or offer you a free trial of the Pro plan. We hope you'll understand, as this is unfortunately required to prevent abuse of our service. To continue, upgrade to Pro or use your own API keys for other providers.";
+        const YOUNG_ACCOUNT_DISCLAIMER: &str = "To prevent abuse of our service, we cannot offer plans to GitHub accounts created fewer than 30 days ago. To request an exception, reach out to billing@zed.dev.";
 
         let label = div()
             .w_full()

crates/client/src/zed_urls.rs 🔗

@@ -18,7 +18,20 @@ pub fn account_url(cx: &App) -> String {
     format!("{server_url}/account", server_url = server_url(cx))
 }
 
+/// Returns the URL to the start trial page on zed.dev.
+pub fn start_trial_url(cx: &App) -> String {
+    format!(
+        "{server_url}/account/start-trial",
+        server_url = server_url(cx)
+    )
+}
+
 /// Returns the URL to the upgrade page on zed.dev.
 pub fn upgrade_to_zed_pro_url(cx: &App) -> String {
     format!("{server_url}/account/upgrade", server_url = server_url(cx))
 }
+
+/// Returns the URL to Zed's terms of service.
+pub fn terms_of_service(cx: &App) -> String {
+    format!("{server_url}/terms-of-service", server_url = server_url(cx))
+}

crates/collab/src/rpc.rs 🔗

@@ -4167,6 +4167,13 @@ async fn accept_terms_of_service(
     response.send(proto::AcceptTermsOfServiceResponse {
         accepted_tos_at: accepted_tos_at.timestamp() as u64,
     })?;
+
+    // When the user accepts the terms of service, we want to refresh their LLM
+    // token to grant access.
+    session
+        .peer
+        .send(session.connection_id, proto::RefreshLlmToken {})?;
+
     Ok(())
 }
 

crates/context_server/src/context_server.rs 🔗

@@ -6,9 +6,9 @@ pub mod test;
 pub mod transport;
 pub mod types;
 
-use std::fmt::Display;
 use std::path::Path;
 use std::sync::Arc;
+use std::{fmt::Display, path::PathBuf};
 
 use anyhow::Result;
 use client::Client;
@@ -31,7 +31,7 @@ impl Display for ContextServerId {
 #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
 pub struct ContextServerCommand {
     #[serde(rename = "command")]
-    pub path: String,
+    pub path: PathBuf,
     pub args: Vec<String>,
     pub env: Option<HashMap<String, String>>,
 }

crates/editor/src/actions.rs 🔗

@@ -322,6 +322,8 @@ actions!(
         ApplyDiffHunk,
         /// Deletes the character before the cursor.
         Backspace,
+        /// Shows git blame information for the current line.
+        BlameHover,
         /// Cancels the current operation.
         Cancel,
         /// Cancels the running flycheck operation.

crates/editor/src/editor.rs 🔗

@@ -950,6 +950,7 @@ struct InlineBlamePopover {
     hide_task: Option<Task<()>>,
     popover_bounds: Option<Bounds<Pixels>>,
     popover_state: InlineBlamePopoverState,
+    keyboard_grace: bool,
 }
 
 enum SelectionDragState {
@@ -6517,21 +6518,55 @@ impl Editor {
         }
     }
 
+    pub fn blame_hover(&mut self, _: &BlameHover, window: &mut Window, cx: &mut Context<Self>) {
+        let snapshot = self.snapshot(window, cx);
+        let cursor = self.selections.newest::<Point>(cx).head();
+        let Some((buffer, point, _)) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)
+        else {
+            return;
+        };
+
+        let Some(blame) = self.blame.as_ref() else {
+            return;
+        };
+
+        let row_info = RowInfo {
+            buffer_id: Some(buffer.remote_id()),
+            buffer_row: Some(point.row),
+            ..Default::default()
+        };
+        let Some(blame_entry) = blame
+            .update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
+            .flatten()
+        else {
+            return;
+        };
+
+        let anchor = self.selections.newest_anchor().head();
+        let position = self.to_pixel_point(anchor, &snapshot, window);
+        if let (Some(position), Some(last_bounds)) = (position, self.last_bounds) {
+            self.show_blame_popover(&blame_entry, position + last_bounds.origin, true, cx);
+        };
+    }
+
     fn show_blame_popover(
         &mut self,
         blame_entry: &BlameEntry,
         position: gpui::Point<Pixels>,
+        ignore_timeout: bool,
         cx: &mut Context<Self>,
     ) {
         if let Some(state) = &mut self.inline_blame_popover {
             state.hide_task.take();
         } else {
-            let delay = EditorSettings::get_global(cx).hover_popover_delay;
+            let blame_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
             let blame_entry = blame_entry.clone();
             let show_task = cx.spawn(async move |editor, cx| {
-                cx.background_executor()
-                    .timer(std::time::Duration::from_millis(delay))
-                    .await;
+                if !ignore_timeout {
+                    cx.background_executor()
+                        .timer(std::time::Duration::from_millis(blame_popover_delay))
+                        .await;
+                }
                 editor
                     .update(cx, |editor, cx| {
                         editor.inline_blame_popover_show_task.take();
@@ -6560,6 +6595,7 @@ impl Editor {
                                 commit_message: details,
                                 markdown,
                             },
+                            keyboard_grace: ignore_timeout,
                         });
                         cx.notify();
                     })

crates/editor/src/element.rs 🔗

@@ -216,6 +216,7 @@ impl EditorElement {
         register_action(editor, window, Editor::newline_above);
         register_action(editor, window, Editor::newline_below);
         register_action(editor, window, Editor::backspace);
+        register_action(editor, window, Editor::blame_hover);
         register_action(editor, window, Editor::delete);
         register_action(editor, window, Editor::tab);
         register_action(editor, window, Editor::backtab);
@@ -1143,10 +1144,14 @@ impl EditorElement {
                 .as_ref()
                 .and_then(|state| state.popover_bounds)
                 .map_or(false, |bounds| bounds.contains(&event.position));
+            let keyboard_grace = editor
+                .inline_blame_popover
+                .as_ref()
+                .map_or(false, |state| state.keyboard_grace);
 
             if mouse_over_inline_blame || mouse_over_popover {
-                editor.show_blame_popover(&blame_entry, event.position, cx);
-            } else {
+                editor.show_blame_popover(&blame_entry, event.position, false, cx);
+            } else if !keyboard_grace {
                 editor.hide_blame_popover(cx);
             }
         } else {

crates/extension/src/types.rs 🔗

@@ -3,7 +3,7 @@ mod dap;
 mod lsp;
 mod slash_command;
 
-use std::ops::Range;
+use std::{ops::Range, path::PathBuf};
 
 use util::redact::should_redact;
 
@@ -18,7 +18,7 @@ pub type EnvVars = Vec<(String, String)>;
 /// A command.
 pub struct Command {
     /// The command to execute.
-    pub command: String,
+    pub command: PathBuf,
     /// The arguments to pass to the command.
     pub args: Vec<String>,
     /// The environment variables to set for the command.

crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs 🔗

@@ -75,7 +75,7 @@ impl From<Range> for std::ops::Range<usize> {
 impl From<Command> for extension::Command {
     fn from(value: Command) -> Self {
         Self {
-            command: value.command,
+            command: value.command.into(),
             args: value.args,
             env: value.env,
         }
@@ -958,7 +958,7 @@ impl ExtensionImports for WasmState {
                                 command,
                             } => Ok(serde_json::to_string(&settings::ContextServerSettings {
                                 command: Some(settings::CommandSettings {
-                                    path: Some(command.path),
+                                    path: command.path.to_str().map(|path| path.to_string()),
                                     arguments: Some(command.args),
                                     env: command.env.map(|env| env.into_iter().collect()),
                                 }),

crates/gpui/src/elements/div.rs 🔗

@@ -1664,6 +1664,11 @@ impl Interactivity {
         window: &mut Window,
         _cx: &mut App,
     ) -> Point<Pixels> {
+        fn round_to_two_decimals(pixels: Pixels) -> Pixels {
+            const ROUNDING_FACTOR: f32 = 100.0;
+            (pixels * ROUNDING_FACTOR).round() / ROUNDING_FACTOR
+        }
+
         if let Some(scroll_offset) = self.scroll_offset.as_ref() {
             let mut scroll_to_bottom = false;
             let mut tracked_scroll_handle = self
@@ -1678,8 +1683,16 @@ impl Interactivity {
             let rem_size = window.rem_size();
             let padding = style.padding.to_pixels(bounds.size.into(), rem_size);
             let padding_size = size(padding.left + padding.right, padding.top + padding.bottom);
+            // The floating point values produced by Taffy and ours often vary
+            // slightly after ~5 decimal places. This can lead to cases where after
+            // subtracting these, the container becomes scrollable for less than
+            // 0.00000x pixels. As we generally don't benefit from a precision that
+            // high for the maximum scroll, we round the scroll max to 2 decimal
+            // places here.
             let padded_content_size = self.content_size + padding_size;
-            let scroll_max = (padded_content_size - bounds.size).max(&Size::default());
+            let scroll_max = (padded_content_size - bounds.size)
+                .map(round_to_two_decimals)
+                .max(&Default::default());
             // Clamp scroll offset in case scroll max is smaller now (e.g., if children
             // were removed or the bounds became larger).
             let mut scroll_offset = scroll_offset.borrow_mut();
@@ -1692,7 +1705,7 @@ impl Interactivity {
             }
 
             if let Some(mut scroll_handle_state) = tracked_scroll_handle {
-                scroll_handle_state.padded_content_size = padded_content_size;
+                scroll_handle_state.max_offset = scroll_max;
             }
 
             *scroll_offset
@@ -2936,7 +2949,7 @@ impl ScrollAnchor {
 struct ScrollHandleState {
     offset: Rc<RefCell<Point<Pixels>>>,
     bounds: Bounds<Pixels>,
-    padded_content_size: Size<Pixels>,
+    max_offset: Size<Pixels>,
     child_bounds: Vec<Bounds<Pixels>>,
     scroll_to_bottom: bool,
     overflow: Point<Overflow>,
@@ -2965,6 +2978,11 @@ impl ScrollHandle {
         *self.0.borrow().offset.borrow()
     }
 
+    /// Get the maximum scroll offset.
+    pub fn max_offset(&self) -> Size<Pixels> {
+        self.0.borrow().max_offset
+    }
+
     /// Get the top child that's scrolled into view.
     pub fn top_item(&self) -> usize {
         let state = self.0.borrow();
@@ -2999,11 +3017,6 @@ impl ScrollHandle {
         self.0.borrow().child_bounds.get(ix).cloned()
     }
 
-    /// Get the size of the content with padding of the container.
-    pub fn padded_content_size(&self) -> Size<Pixels> {
-        self.0.borrow().padded_content_size
-    }
-
     /// scroll_to_item scrolls the minimal amount to ensure that the child is
     /// fully visible
     pub fn scroll_to_item(&self, ix: usize) {

crates/gpui/src/elements/list.rs 🔗

@@ -411,9 +411,9 @@ impl ListState {
         self.0.borrow_mut().set_offset_from_scrollbar(point);
     }
 
-    /// Returns the size of items we have measured.
+    /// Returns the maximum scroll offset according to the items we have measured.
     /// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
-    pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
+    pub fn max_offset_for_scrollbar(&self) -> Size<Pixels> {
         let state = self.0.borrow();
         let bounds = state.last_layout_bounds.unwrap_or_default();
 
@@ -421,7 +421,7 @@ impl ListState {
             .scrollbar_drag_start_height
             .unwrap_or_else(|| state.items.summary().height);
 
-        Size::new(bounds.size.width, height)
+        Size::new(Pixels::ZERO, Pixels::ZERO.max(height - bounds.size.height))
     }
 
     /// Returns the current scroll offset adjusted for the scrollbar

crates/gpui/src/platform/linux/platform.rs 🔗

@@ -828,6 +828,13 @@ impl crate::Keystroke {
             Keysym::Delete => "delete".to_owned(),
             Keysym::Escape => "escape".to_owned(),
 
+            Keysym::Left => "left".to_owned(),
+            Keysym::Right => "right".to_owned(),
+            Keysym::Up => "up".to_owned(),
+            Keysym::Down => "down".to_owned(),
+            Keysym::Home => "home".to_owned(),
+            Keysym::End => "end".to_owned(),
+
             _ => {
                 let name = xkb::keysym_get_name(key_sym).to_lowercase();
                 if key_sym.is_keypad_key() {

crates/icons/src/icons.rs 🔗

@@ -181,6 +181,9 @@ pub enum IconName {
     MicMute,
     Microscope,
     Minimize,
+    NewFromSummary,
+    NewTextThread,
+    NewThread,
     Option,
     PageDown,
     PageUp,

crates/language_model/src/registry.rs 🔗

@@ -206,8 +206,8 @@ impl LanguageModelRegistry {
         None
     }
 
-    /// Check that we have at least one provider that is authenticated.
-    fn has_authenticated_provider(&self, cx: &App) -> bool {
+    /// Returns `true` if at least one provider that is authenticated.
+    pub fn has_authenticated_provider(&self, cx: &App) -> bool {
         self.providers.values().any(|p| p.is_authenticated(cx))
     }
 

crates/language_models/src/provider/cloud.rs 🔗

@@ -1140,19 +1140,19 @@ impl RenderOnce for ZedAiConfiguration {
         let is_pro = self.plan == Some(proto::Plan::ZedPro);
         let subscription_text = match (self.plan, self.subscription_period) {
             (Some(proto::Plan::ZedPro), Some(_)) => {
-                "You have access to Zed's hosted LLMs through your Pro subscription."
+                "You have access to Zed's hosted models through your Pro subscription."
             }
             (Some(proto::Plan::ZedProTrial), Some(_)) => {
-                "You have access to Zed's hosted LLMs through your Pro trial."
+                "You have access to Zed's hosted models through your Pro trial."
             }
             (Some(proto::Plan::Free), Some(_)) => {
-                "You have basic access to Zed's hosted LLMs through the Free plan."
+                "You have basic access to Zed's hosted models through the Free plan."
             }
             _ => {
                 if self.eligible_for_trial {
-                    "Subscribe for access to Zed's hosted LLMs. Start with a 14 day free trial."
+                    "Subscribe for access to Zed's hosted models. Start with a 14 day free trial."
                 } else {
-                    "Subscribe for access to Zed's hosted LLMs."
+                    "Subscribe for access to Zed's hosted models."
                 }
             }
         };
@@ -1166,7 +1166,7 @@ impl RenderOnce for ZedAiConfiguration {
             Button::new("start_trial", "Start 14-day Free Pro Trial")
                 .style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
                 .full_width()
-                .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
+                .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
                 .into_any_element()
         } else {
             Button::new("upgrade", "Upgrade to Pro")

crates/language_tools/Cargo.toml 🔗

@@ -18,7 +18,6 @@ client.workspace = true
 collections.workspace = true
 copilot.workspace = true
 editor.workspace = true
-feature_flags.workspace = true
 futures.workspace = true
 gpui.workspace = true
 itertools.workspace = true

crates/language_tools/src/lsp_tool.rs 🔗

@@ -1,13 +1,17 @@
-use std::{collections::hash_map, path::PathBuf, rc::Rc, time::Duration};
+use std::{
+    collections::{BTreeMap, HashMap},
+    path::{Path, PathBuf},
+    rc::Rc,
+    time::Duration,
+};
 
 use client::proto;
-use collections::{HashMap, HashSet};
+use collections::HashSet;
 use editor::{Editor, EditorEvent};
-use feature_flags::FeatureFlagAppExt as _;
 use gpui::{Corner, Entity, Subscription, Task, WeakEntity, actions};
-use language::{BinaryStatus, BufferId, LocalFile, ServerHealth};
+use language::{BinaryStatus, BufferId, ServerHealth};
 use lsp::{LanguageServerId, LanguageServerName, LanguageServerSelector};
-use project::{LspStore, LspStoreEvent, project_settings::ProjectSettings};
+use project::{LspStore, LspStoreEvent, Worktree, project_settings::ProjectSettings};
 use settings::{Settings as _, SettingsStore};
 use ui::{
     Context, ContextMenu, ContextMenuEntry, ContextMenuItem, DocumentationAside, DocumentationSide,
@@ -36,8 +40,7 @@ pub struct LspTool {
 
 #[derive(Debug)]
 struct LanguageServerState {
-    items: Vec<LspItem>,
-    other_servers_start_index: Option<usize>,
+    items: Vec<LspMenuItem>,
     workspace: WeakEntity<Workspace>,
     lsp_store: WeakEntity<LspStore>,
     active_editor: Option<ActiveEditor>,
@@ -63,8 +66,13 @@ impl std::fmt::Debug for ActiveEditor {
 struct LanguageServers {
     health_statuses: HashMap<LanguageServerId, LanguageServerHealthStatus>,
     binary_statuses: HashMap<LanguageServerName, LanguageServerBinaryStatus>,
-    servers_per_buffer_abs_path:
-        HashMap<PathBuf, HashMap<LanguageServerId, Option<LanguageServerName>>>,
+    servers_per_buffer_abs_path: HashMap<PathBuf, ServersForPath>,
+}
+
+#[derive(Debug, Clone)]
+struct ServersForPath {
+    servers: HashMap<LanguageServerId, Option<LanguageServerName>>,
+    worktree: Option<WeakEntity<Worktree>>,
 }
 
 #[derive(Debug, Clone)]
@@ -120,8 +128,8 @@ impl LanguageServerState {
         };
 
         let mut first_button_encountered = false;
-        for (i, item) in self.items.iter().enumerate() {
-            if let LspItem::ToggleServersButton { restart } = item {
+        for item in &self.items {
+            if let LspMenuItem::ToggleServersButton { restart } = item {
                 let label = if *restart {
                     "Restart All Servers"
                 } else {
@@ -140,22 +148,19 @@ impl LanguageServerState {
                                     };
                                     let project = workspace.read(cx).project().clone();
                                     let buffer_store = project.read(cx).buffer_store().clone();
-                                    let worktree_store = project.read(cx).worktree_store();
-
                                     let buffers = state
                                         .read(cx)
                                         .language_servers
                                         .servers_per_buffer_abs_path
-                                        .keys()
-                                        .filter_map(|abs_path| {
-                                            worktree_store.read(cx).find_worktree(abs_path, cx)
-                                        })
-                                        .filter_map(|(worktree, relative_path)| {
-                                            let entry =
-                                                worktree.read(cx).entry_for_path(&relative_path)?;
-                                            project.read(cx).path_for_entry(entry.id, cx)
-                                        })
-                                        .filter_map(|project_path| {
+                                        .iter()
+                                        .filter_map(|(abs_path, servers)| {
+                                            let worktree =
+                                                servers.worktree.as_ref()?.upgrade()?.read(cx);
+                                            let relative_path =
+                                                abs_path.strip_prefix(&worktree.abs_path()).ok()?;
+                                            let entry = worktree.entry_for_path(&relative_path)?;
+                                            let project_path =
+                                                project.read(cx).path_for_entry(entry.id, cx)?;
                                             buffer_store.read(cx).get_by_path(&project_path)
                                         })
                                         .collect();
@@ -165,13 +170,16 @@ impl LanguageServerState {
                                         .iter()
                                         // Do not try to use IDs as we have stopped all servers already, when allowing to restart them all
                                         .flat_map(|item| match item {
-                                            LspItem::ToggleServersButton { .. } => None,
-                                            LspItem::WithHealthCheck(_, status, ..) => Some(
-                                                LanguageServerSelector::Name(status.name.clone()),
-                                            ),
-                                            LspItem::WithBinaryStatus(_, server_name, ..) => Some(
-                                                LanguageServerSelector::Name(server_name.clone()),
+                                            LspMenuItem::Header { .. } => None,
+                                            LspMenuItem::ToggleServersButton { .. } => None,
+                                            LspMenuItem::WithHealthCheck { health, .. } => Some(
+                                                LanguageServerSelector::Name(health.name.clone()),
                                             ),
+                                            LspMenuItem::WithBinaryStatus {
+                                                server_name, ..
+                                            } => Some(LanguageServerSelector::Name(
+                                                server_name.clone(),
+                                            )),
                                         })
                                         .collect();
                                     lsp_store.restart_language_servers_for_buffers(
@@ -190,13 +198,17 @@ impl LanguageServerState {
                 }
                 menu = menu.item(button);
                 continue;
-            };
+            } else if let LspMenuItem::Header { header, separator } = item {
+                menu = menu
+                    .when(*separator, |menu| menu.separator())
+                    .when_some(header.as_ref(), |menu, header| menu.header(header));
+                continue;
+            }
 
             let Some(server_info) = item.server_info() else {
                 continue;
             };
 
-            let workspace = self.workspace.clone();
             let server_selector = server_info.server_selector();
             // TODO currently, Zed remote does not work well with the LSP logs
             // https://github.com/zed-industries/zed/issues/28557
@@ -205,6 +217,7 @@ impl LanguageServerState {
 
             let status_color = server_info
                 .binary_status
+                .as_ref()
                 .and_then(|binary_status| match binary_status.status {
                     BinaryStatus::None => None,
                     BinaryStatus::CheckingForUpdate
@@ -223,17 +236,20 @@ impl LanguageServerState {
                 })
                 .unwrap_or(Color::Success);
 
-            if self
-                .other_servers_start_index
-                .is_some_and(|index| index == i)
-            {
-                menu = menu.separator().header("Other Buffers");
-            }
-
-            if i == 0 && self.other_servers_start_index.is_some() {
-                menu = menu.header("Current Buffer");
-            }
+            let message = server_info
+                .message
+                .as_ref()
+                .or_else(|| server_info.binary_status.as_ref()?.message.as_ref())
+                .cloned();
+            let hover_label = if has_logs {
+                Some("View Logs")
+            } else if message.is_some() {
+                Some("View Message")
+            } else {
+                None
+            };
 
+            let server_name = server_info.name.clone();
             menu = menu.item(ContextMenuItem::custom_entry(
                 move |_, _| {
                     h_flex()
@@ -245,42 +261,99 @@ impl LanguageServerState {
                             h_flex()
                                 .gap_2()
                                 .child(Indicator::dot().color(status_color))
-                                .child(Label::new(server_info.name.0.clone())),
-                        )
-                        .child(
-                            h_flex()
-                                .visible_on_hover("menu_item")
-                                .child(
-                                    Label::new("View Logs")
-                                        .size(LabelSize::Small)
-                                        .color(Color::Muted),
-                                )
-                                .child(
-                                    Icon::new(IconName::ChevronRight)
-                                        .size(IconSize::Small)
-                                        .color(Color::Muted),
-                                ),
+                                .child(Label::new(server_name.0.clone())),
                         )
+                        .when_some(hover_label, |div, hover_label| {
+                            div.child(
+                                h_flex()
+                                    .visible_on_hover("menu_item")
+                                    .child(
+                                        Label::new(hover_label)
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
+                                    )
+                                    .child(
+                                        Icon::new(IconName::ChevronRight)
+                                            .size(IconSize::Small)
+                                            .color(Color::Muted),
+                                    ),
+                            )
+                        })
                         .into_any_element()
                 },
                 {
                     let lsp_logs = lsp_logs.clone();
+                    let message = message.clone();
+                    let server_selector = server_selector.clone();
+                    let server_name = server_info.name.clone();
+                    let workspace = self.workspace.clone();
                     move |window, cx| {
-                        if !has_logs {
+                        if has_logs {
+                            lsp_logs.update(cx, |lsp_logs, cx| {
+                                lsp_logs.open_server_trace(
+                                    workspace.clone(),
+                                    server_selector.clone(),
+                                    window,
+                                    cx,
+                                );
+                            });
+                        } else if let Some(message) = &message {
+                            let Some(create_buffer) = workspace
+                                .update(cx, |workspace, cx| {
+                                    workspace
+                                        .project()
+                                        .update(cx, |project, cx| project.create_buffer(cx))
+                                })
+                                .ok()
+                            else {
+                                return;
+                            };
+
+                            let window = window.window_handle();
+                            let workspace = workspace.clone();
+                            let message = message.clone();
+                            let server_name = server_name.clone();
+                            cx.spawn(async move |cx| {
+                                let buffer = create_buffer.await?;
+                                buffer.update(cx, |buffer, cx| {
+                                    buffer.edit(
+                                        [(
+                                            0..0,
+                                            format!("Language server {server_name}:\n\n{message}"),
+                                        )],
+                                        None,
+                                        cx,
+                                    );
+                                    buffer.set_capability(language::Capability::ReadOnly, cx);
+                                })?;
+
+                                workspace.update(cx, |workspace, cx| {
+                                    window.update(cx, |_, window, cx| {
+                                        workspace.add_item_to_active_pane(
+                                            Box::new(cx.new(|cx| {
+                                                let mut editor =
+                                                    Editor::for_buffer(buffer, None, window, cx);
+                                                editor.set_read_only(true);
+                                                editor
+                                            })),
+                                            None,
+                                            true,
+                                            window,
+                                            cx,
+                                        );
+                                    })
+                                })??;
+
+                                anyhow::Ok(())
+                            })
+                            .detach();
+                        } else {
                             cx.propagate();
                             return;
                         }
-                        lsp_logs.update(cx, |lsp_logs, cx| {
-                            lsp_logs.open_server_trace(
-                                workspace.clone(),
-                                server_selector.clone(),
-                                window,
-                                cx,
-                            );
-                        });
                     }
                 },
-                server_info.message.map(|server_message| {
+                message.map(|server_message| {
                     DocumentationAside::new(
                         DocumentationSide::Right,
                         Rc::new(move |_| Label::new(server_message.clone()).into_any_element()),
@@ -345,81 +418,95 @@ impl LanguageServers {
 
 #[derive(Debug)]
 enum ServerData<'a> {
-    WithHealthCheck(
-        LanguageServerId,
-        &'a LanguageServerHealthStatus,
-        Option<&'a LanguageServerBinaryStatus>,
-    ),
-    WithBinaryStatus(
-        Option<LanguageServerId>,
-        &'a LanguageServerName,
-        &'a LanguageServerBinaryStatus,
-    ),
+    WithHealthCheck {
+        server_id: LanguageServerId,
+        health: &'a LanguageServerHealthStatus,
+        binary_status: Option<&'a LanguageServerBinaryStatus>,
+    },
+    WithBinaryStatus {
+        server_id: Option<LanguageServerId>,
+        server_name: &'a LanguageServerName,
+        binary_status: &'a LanguageServerBinaryStatus,
+    },
 }
 
 #[derive(Debug)]
-enum LspItem {
-    WithHealthCheck(
-        LanguageServerId,
-        LanguageServerHealthStatus,
-        Option<LanguageServerBinaryStatus>,
-    ),
-    WithBinaryStatus(
-        Option<LanguageServerId>,
-        LanguageServerName,
-        LanguageServerBinaryStatus,
-    ),
+enum LspMenuItem {
+    WithHealthCheck {
+        server_id: LanguageServerId,
+        health: LanguageServerHealthStatus,
+        binary_status: Option<LanguageServerBinaryStatus>,
+    },
+    WithBinaryStatus {
+        server_id: Option<LanguageServerId>,
+        server_name: LanguageServerName,
+        binary_status: LanguageServerBinaryStatus,
+    },
     ToggleServersButton {
         restart: bool,
     },
+    Header {
+        header: Option<SharedString>,
+        separator: bool,
+    },
 }
 
-impl LspItem {
+impl LspMenuItem {
     fn server_info(&self) -> Option<ServerInfo> {
         match self {
-            LspItem::ToggleServersButton { .. } => None,
-            LspItem::WithHealthCheck(
-                language_server_id,
-                language_server_health_status,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_health_status.name.clone(),
-                id: Some(*language_server_id),
-                health: language_server_health_status.health(),
-                binary_status: language_server_binary_status.clone(),
-                message: language_server_health_status.message(),
+            Self::Header { .. } => None,
+            Self::ToggleServersButton { .. } => None,
+            Self::WithHealthCheck {
+                server_id,
+                health,
+                binary_status,
+                ..
+            } => Some(ServerInfo {
+                name: health.name.clone(),
+                id: Some(*server_id),
+                health: health.health(),
+                binary_status: binary_status.clone(),
+                message: health.message(),
             }),
-            LspItem::WithBinaryStatus(
+            Self::WithBinaryStatus {
                 server_id,
-                language_server_name,
-                language_server_binary_status,
-            ) => Some(ServerInfo {
-                name: language_server_name.clone(),
+                server_name,
+                binary_status,
+                ..
+            } => Some(ServerInfo {
+                name: server_name.clone(),
                 id: *server_id,
                 health: None,
-                binary_status: Some(language_server_binary_status.clone()),
-                message: language_server_binary_status.message.clone(),
+                binary_status: Some(binary_status.clone()),
+                message: binary_status.message.clone(),
             }),
         }
     }
 }
 
 impl ServerData<'_> {
-    fn name(&self) -> &LanguageServerName {
-        match self {
-            Self::WithHealthCheck(_, state, _) => &state.name,
-            Self::WithBinaryStatus(_, name, ..) => name,
-        }
-    }
-
-    fn into_lsp_item(self) -> LspItem {
+    fn into_lsp_item(self) -> LspMenuItem {
         match self {
-            Self::WithHealthCheck(id, name, status) => {
-                LspItem::WithHealthCheck(id, name.clone(), status.cloned())
-            }
-            Self::WithBinaryStatus(server_id, name, status) => {
-                LspItem::WithBinaryStatus(server_id, name.clone(), status.clone())
-            }
+            Self::WithHealthCheck {
+                server_id,
+                health,
+                binary_status,
+                ..
+            } => LspMenuItem::WithHealthCheck {
+                server_id,
+                health: health.clone(),
+                binary_status: binary_status.cloned(),
+            },
+            Self::WithBinaryStatus {
+                server_id,
+                server_name,
+                binary_status,
+                ..
+            } => LspMenuItem::WithBinaryStatus {
+                server_id,
+                server_name: server_name.clone(),
+                binary_status: binary_status.clone(),
+            },
         }
     }
 }
@@ -452,7 +539,6 @@ impl LspTool {
         let state = cx.new(|_| LanguageServerState {
             workspace: workspace.weak_handle(),
             items: Vec::new(),
-            other_servers_start_index: None,
             lsp_store: lsp_store.downgrade(),
             active_editor: None,
             language_servers: LanguageServers::default(),
@@ -542,13 +628,28 @@ impl LspTool {
                 message: proto::update_language_server::Variant::RegisteredForBuffer(update),
                 ..
             } => {
-                self.server_state.update(cx, |state, _| {
-                    state
+                self.server_state.update(cx, |state, cx| {
+                    let Ok(worktree) = state.workspace.update(cx, |workspace, cx| {
+                        workspace
+                            .project()
+                            .read(cx)
+                            .find_worktree(Path::new(&update.buffer_abs_path), cx)
+                            .map(|(worktree, _)| worktree.downgrade())
+                    }) else {
+                        return;
+                    };
+                    let entry = state
                         .language_servers
                         .servers_per_buffer_abs_path
                         .entry(PathBuf::from(&update.buffer_abs_path))
-                        .or_default()
-                        .insert(*language_server_id, name.clone());
+                        .or_insert_with(|| ServersForPath {
+                            servers: HashMap::default(),
+                            worktree: worktree.clone(),
+                        });
+                    entry.servers.insert(*language_server_id, name.clone());
+                    if worktree.is_some() {
+                        entry.worktree = worktree;
+                    }
                 });
                 updated = true;
             }
@@ -562,94 +663,95 @@ impl LspTool {
 
     fn regenerate_items(&mut self, cx: &mut App) {
         self.server_state.update(cx, |state, cx| {
-            let editor_buffers = state
+            let active_worktrees = state
                 .active_editor
                 .as_ref()
-                .map(|active_editor| active_editor.editor_buffers.clone())
-                .unwrap_or_default();
-            let editor_buffer_paths = editor_buffers
-                .iter()
-                .filter_map(|buffer_id| {
-                    let buffer_path = state
-                        .lsp_store
-                        .update(cx, |lsp_store, cx| {
-                            Some(
-                                project::File::from_dyn(
-                                    lsp_store
-                                        .buffer_store()
-                                        .read(cx)
-                                        .get(*buffer_id)?
-                                        .read(cx)
-                                        .file(),
-                                )?
-                                .abs_path(cx),
-                            )
+                .into_iter()
+                .flat_map(|active_editor| {
+                    active_editor
+                        .editor
+                        .upgrade()
+                        .into_iter()
+                        .flat_map(|active_editor| {
+                            active_editor
+                                .read(cx)
+                                .buffer()
+                                .read(cx)
+                                .all_buffers()
+                                .into_iter()
+                                .filter_map(|buffer| {
+                                    project::File::from_dyn(buffer.read(cx).file())
+                                })
+                                .map(|buffer_file| buffer_file.worktree.clone())
                         })
-                        .ok()??;
-                    Some(buffer_path)
                 })
-                .collect::<Vec<_>>();
+                .collect::<HashSet<_>>();
 
-            let mut servers_with_health_checks = HashSet::default();
-            let mut server_ids_with_health_checks = HashSet::default();
-            let mut buffer_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let mut other_servers =
-                Vec::with_capacity(state.language_servers.health_statuses.len());
-            let buffer_server_ids = editor_buffer_paths
-                .iter()
-                .filter_map(|buffer_path| {
-                    state
-                        .language_servers
-                        .servers_per_buffer_abs_path
-                        .get(buffer_path)
-                })
-                .flatten()
-                .fold(HashMap::default(), |mut acc, (server_id, name)| {
-                    match acc.entry(*server_id) {
-                        hash_map::Entry::Occupied(mut o) => {
-                            let old_name: &mut Option<&LanguageServerName> = o.get_mut();
-                            if old_name.is_none() {
-                                *old_name = name.as_ref();
-                            }
-                        }
-                        hash_map::Entry::Vacant(v) => {
-                            v.insert(name.as_ref());
+            let mut server_ids_to_worktrees =
+                HashMap::<LanguageServerId, Entity<Worktree>>::default();
+            let mut server_names_to_worktrees = HashMap::<
+                LanguageServerName,
+                HashSet<(Entity<Worktree>, LanguageServerId)>,
+            >::default();
+            for servers_for_path in state.language_servers.servers_per_buffer_abs_path.values() {
+                if let Some(worktree) = servers_for_path
+                    .worktree
+                    .as_ref()
+                    .and_then(|worktree| worktree.upgrade())
+                {
+                    for (server_id, server_name) in &servers_for_path.servers {
+                        server_ids_to_worktrees.insert(*server_id, worktree.clone());
+                        if let Some(server_name) = server_name {
+                            server_names_to_worktrees
+                                .entry(server_name.clone())
+                                .or_default()
+                                .insert((worktree.clone(), *server_id));
                         }
                     }
-                    acc
+                }
+            }
+
+            let mut servers_per_worktree = BTreeMap::<SharedString, Vec<ServerData>>::new();
+            let mut servers_without_worktree = Vec::<ServerData>::new();
+            let mut servers_with_health_checks = HashSet::default();
+
+            for (server_id, health) in &state.language_servers.health_statuses {
+                let worktree = server_ids_to_worktrees.get(server_id).or_else(|| {
+                    let worktrees = server_names_to_worktrees.get(&health.name)?;
+                    worktrees
+                        .iter()
+                        .find(|(worktree, _)| active_worktrees.contains(worktree))
+                        .or_else(|| worktrees.iter().next())
+                        .map(|(worktree, _)| worktree)
                 });
-            for (server_id, server_state) in &state.language_servers.health_statuses {
-                let binary_status = state
-                    .language_servers
-                    .binary_statuses
-                    .get(&server_state.name);
-                servers_with_health_checks.insert(&server_state.name);
-                server_ids_with_health_checks.insert(*server_id);
-                if buffer_server_ids.contains_key(server_id) {
-                    buffer_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithHealthCheck(
-                        *server_id,
-                        server_state,
-                        binary_status,
-                    ));
+                servers_with_health_checks.insert(&health.name);
+                let worktree_name =
+                    worktree.map(|worktree| SharedString::new(worktree.read(cx).root_name()));
+
+                let binary_status = state.language_servers.binary_statuses.get(&health.name);
+                let server_data = ServerData::WithHealthCheck {
+                    server_id: *server_id,
+                    health,
+                    binary_status,
+                };
+                match worktree_name {
+                    Some(worktree_name) => servers_per_worktree
+                        .entry(worktree_name.clone())
+                        .or_default()
+                        .push(server_data),
+                    None => servers_without_worktree.push(server_data),
                 }
             }
 
             let mut can_stop_all = !state.language_servers.health_statuses.is_empty();
             let mut can_restart_all = state.language_servers.health_statuses.is_empty();
-            for (server_name, status) in state
+            for (server_name, binary_status) in state
                 .language_servers
                 .binary_statuses
                 .iter()
                 .filter(|(name, _)| !servers_with_health_checks.contains(name))
             {
-                match status.status {
+                match binary_status.status {
                     BinaryStatus::None => {
                         can_restart_all = false;
                         can_stop_all |= true;
@@ -674,52 +776,73 @@ impl LspTool {
                     BinaryStatus::Failed { .. } => {}
                 }
 
-                let matching_server_id = state
-                    .language_servers
-                    .servers_per_buffer_abs_path
-                    .iter()
-                    .filter(|(path, _)| editor_buffer_paths.contains(path))
-                    .flat_map(|(_, server_associations)| server_associations.iter())
-                    .find_map(|(id, name)| {
-                        if name.as_ref() == Some(server_name) {
-                            Some(*id)
-                        } else {
-                            None
+                match server_names_to_worktrees.get(server_name) {
+                    Some(worktrees_for_name) => {
+                        match worktrees_for_name
+                            .iter()
+                            .find(|(worktree, _)| active_worktrees.contains(worktree))
+                            .or_else(|| worktrees_for_name.iter().next())
+                        {
+                            Some((worktree, server_id)) => {
+                                let worktree_name =
+                                    SharedString::new(worktree.read(cx).root_name());
+                                servers_per_worktree
+                                    .entry(worktree_name.clone())
+                                    .or_default()
+                                    .push(ServerData::WithBinaryStatus {
+                                        server_name,
+                                        binary_status,
+                                        server_id: Some(*server_id),
+                                    });
+                            }
+                            None => servers_without_worktree.push(ServerData::WithBinaryStatus {
+                                server_name,
+                                binary_status,
+                                server_id: None,
+                            }),
                         }
-                    });
-                if let Some(server_id) = matching_server_id {
-                    buffer_servers.push(ServerData::WithBinaryStatus(
-                        Some(server_id),
+                    }
+                    None => servers_without_worktree.push(ServerData::WithBinaryStatus {
                         server_name,
-                        status,
-                    ));
-                } else {
-                    other_servers.push(ServerData::WithBinaryStatus(None, server_name, status));
+                        binary_status,
+                        server_id: None,
+                    }),
                 }
             }
 
-            buffer_servers.sort_by_key(|data| data.name().clone());
-            other_servers.sort_by_key(|data| data.name().clone());
-
-            let mut other_servers_start_index = None;
             let mut new_lsp_items =
-                Vec::with_capacity(buffer_servers.len() + other_servers.len() + 1);
-            new_lsp_items.extend(buffer_servers.into_iter().map(ServerData::into_lsp_item));
-            if !new_lsp_items.is_empty() {
-                other_servers_start_index = Some(new_lsp_items.len());
+                Vec::with_capacity(servers_per_worktree.len() + servers_without_worktree.len() + 2);
+            for (worktree_name, worktree_servers) in servers_per_worktree {
+                if worktree_servers.is_empty() {
+                    continue;
+                }
+                new_lsp_items.push(LspMenuItem::Header {
+                    header: Some(worktree_name),
+                    separator: false,
+                });
+                new_lsp_items.extend(worktree_servers.into_iter().map(ServerData::into_lsp_item));
+            }
+            if !servers_without_worktree.is_empty() {
+                new_lsp_items.push(LspMenuItem::Header {
+                    header: Some(SharedString::from("Unknown worktree")),
+                    separator: false,
+                });
+                new_lsp_items.extend(
+                    servers_without_worktree
+                        .into_iter()
+                        .map(ServerData::into_lsp_item),
+                );
             }
-            new_lsp_items.extend(other_servers.into_iter().map(ServerData::into_lsp_item));
             if !new_lsp_items.is_empty() {
                 if can_stop_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: false });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: false });
                 } else if can_restart_all {
-                    new_lsp_items.push(LspItem::ToggleServersButton { restart: true });
+                    new_lsp_items.push(LspMenuItem::ToggleServersButton { restart: true });
                 }
             }
 
             state.items = new_lsp_items;
-            state.other_servers_start_index = other_servers_start_index;
         });
     }
 
@@ -841,10 +964,7 @@ impl StatusItemView for LspTool {
 
 impl Render for LspTool {
     fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
-        if !cx.is_staff()
-            || self.server_state.read(cx).language_servers.is_empty()
-            || self.lsp_menu.is_none()
-        {
+        if self.server_state.read(cx).language_servers.is_empty() || self.lsp_menu.is_none() {
             return div();
         }
 
@@ -852,12 +972,12 @@ impl Render for LspTool {
         let mut has_warnings = false;
         let mut has_other_notifications = false;
         let state = self.server_state.read(cx);
-        for server in state.language_servers.health_statuses.values() {
-            if let Some(binary_status) = &state.language_servers.binary_statuses.get(&server.name) {
-                has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
-                has_other_notifications |= binary_status.message.is_some();
-            }
+        for binary_status in state.language_servers.binary_statuses.values() {
+            has_errors |= matches!(binary_status.status, BinaryStatus::Failed { .. });
+            has_other_notifications |= binary_status.message.is_some();
+        }
 
+        for server in state.language_servers.health_statuses.values() {
             if let Some((message, health)) = &server.health {
                 has_other_notifications |= message.is_some();
                 match health {

crates/project/src/context_server_store.rs 🔗

@@ -610,7 +610,7 @@ mod tests {
     use context_server::test::create_fake_transport;
     use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
     use serde_json::json;
-    use std::{cell::RefCell, rc::Rc};
+    use std::{cell::RefCell, path::PathBuf, rc::Rc};
     use util::path;
 
     #[gpui::test]
@@ -931,7 +931,7 @@ mod tests {
                         ContextServerSettings::Custom {
                             enabled: true,
                             command: ContextServerCommand {
-                                path: "somebinary".to_string(),
+                                path: "somebinary".into(),
                                 args: vec!["arg".to_string()],
                                 env: None,
                             },
@@ -971,7 +971,7 @@ mod tests {
                         ContextServerSettings::Custom {
                             enabled: true,
                             command: ContextServerCommand {
-                                path: "somebinary".to_string(),
+                                path: "somebinary".into(),
                                 args: vec!["anotherArg".to_string()],
                                 env: None,
                             },
@@ -1053,7 +1053,7 @@ mod tests {
                 ContextServerSettings::Custom {
                     enabled: true,
                     command: ContextServerCommand {
-                        path: "somebinary".to_string(),
+                        path: "somebinary".into(),
                         args: vec!["arg".to_string()],
                         env: None,
                     },
@@ -1104,7 +1104,7 @@ mod tests {
                     ContextServerSettings::Custom {
                         enabled: false,
                         command: ContextServerCommand {
-                            path: "somebinary".to_string(),
+                            path: "somebinary".into(),
                             args: vec!["arg".to_string()],
                             env: None,
                         },
@@ -1132,7 +1132,7 @@ mod tests {
                     ContextServerSettings::Custom {
                         enabled: true,
                         command: ContextServerCommand {
-                            path: "somebinary".to_string(),
+                            path: "somebinary".into(),
                             args: vec!["arg".to_string()],
                             env: None,
                         },
@@ -1184,7 +1184,7 @@ mod tests {
         ContextServerSettings::Custom {
             enabled: true,
             command: ContextServerCommand {
-                path: "somebinary".to_string(),
+                path: "somebinary".into(),
                 args: vec!["arg".to_string()],
                 env: None,
             },
@@ -1256,11 +1256,11 @@ mod tests {
     }
 
     struct FakeContextServerDescriptor {
-        path: String,
+        path: PathBuf,
     }
 
     impl FakeContextServerDescriptor {
-        fn new(path: impl Into<String>) -> Self {
+        fn new(path: impl Into<PathBuf>) -> Self {
             Self { path: path.into() }
         }
     }

crates/project/src/context_server_store/extension.rs 🔗

@@ -61,10 +61,7 @@ impl registry::ContextServerDescriptor for ContextServerDescriptor {
             let mut command = extension
                 .context_server_command(id.clone(), extension_project.clone())
                 .await?;
-            command.command = extension
-                .path_from_extension(command.command.as_ref())
-                .to_string_lossy()
-                .to_string();
+            command.command = extension.path_from_extension(&command.command);
 
             log::info!("loaded command for context server {id}: {command:?}");
 

crates/project/src/project_settings.rs 🔗

@@ -581,7 +581,7 @@ impl Settings for ProjectSettings {
 
         #[derive(Deserialize)]
         struct VsCodeContextServerCommand {
-            command: String,
+            command: PathBuf,
             args: Option<Vec<String>>,
             env: Option<HashMap<String, String>>,
             // note: we don't support envFile and type

crates/project/src/terminals.rs 🔗

@@ -662,7 +662,7 @@ pub fn wrap_for_ssh(
 
             format!("cd \"$HOME/{trimmed_path}\"; {env_changes} {to_run}")
         } else {
-            format!("cd {path}; {env_changes} {to_run}")
+            format!("cd \"{path}\"; {env_changes} {to_run}")
         }
     } else {
         format!("cd; {env_changes} {to_run}")

crates/project_panel/src/project_panel.rs 🔗

@@ -384,12 +384,20 @@ struct ItemColors {
     focused: Hsla,
 }
 
-fn get_item_color(cx: &App) -> ItemColors {
+fn get_item_color(is_sticky: bool, cx: &App) -> ItemColors {
     let colors = cx.theme().colors();
 
     ItemColors {
-        default: colors.panel_background,
-        hover: colors.element_hover,
+        default: if is_sticky {
+            colors.panel_overlay_background
+        } else {
+            colors.panel_background
+        },
+        hover: if is_sticky {
+            colors.panel_overlay_hover
+        } else {
+            colors.element_hover
+        },
         marked: colors.element_selected,
         focused: colors.panel_focused_border,
         drag_over: colors.drop_target_background,
@@ -3903,7 +3911,7 @@ impl ProjectPanel {
 
         let filename_text_color = details.filename_text_color;
         let diagnostic_severity = details.diagnostic_severity;
-        let item_colors = get_item_color(cx);
+        let item_colors = get_item_color(is_sticky, cx);
 
         let canonical_path = details
             .canonical_path

crates/terminal_view/src/terminal_scrollbar.rs 🔗

@@ -46,9 +46,16 @@ impl TerminalScrollHandle {
 }
 
 impl ScrollableHandle for TerminalScrollHandle {
-    fn content_size(&self) -> Size<Pixels> {
+    fn max_offset(&self) -> Size<Pixels> {
         let state = self.state.borrow();
-        size(Pixels::ZERO, state.total_lines as f32 * state.line_height)
+        size(
+            Pixels::ZERO,
+            state
+                .total_lines
+                .checked_sub(state.viewport_lines)
+                .unwrap_or(0) as f32
+                * state.line_height,
+        )
     }
 
     fn offset(&self) -> Point<Pixels> {

crates/theme/src/default_colors.rs 🔗

@@ -83,6 +83,8 @@ impl ThemeColors {
             panel_indent_guide: neutral().light_alpha().step_5(),
             panel_indent_guide_hover: neutral().light_alpha().step_6(),
             panel_indent_guide_active: neutral().light_alpha().step_6(),
+            panel_overlay_background: neutral().light().step_2(),
+            panel_overlay_hover: neutral().light_alpha().step_4(),
             pane_focused_border: blue().light().step_5(),
             pane_group_border: neutral().light().step_6(),
             scrollbar_thumb_background: neutral().light_alpha().step_3(),
@@ -206,6 +208,8 @@ impl ThemeColors {
             panel_indent_guide: neutral().dark_alpha().step_4(),
             panel_indent_guide_hover: neutral().dark_alpha().step_6(),
             panel_indent_guide_active: neutral().dark_alpha().step_6(),
+            panel_overlay_background: neutral().dark().step_2(),
+            panel_overlay_hover: neutral().dark_alpha().step_4(),
             pane_focused_border: blue().dark().step_5(),
             pane_group_border: neutral().dark().step_6(),
             scrollbar_thumb_background: neutral().dark_alpha().step_3(),

crates/theme/src/fallback_themes.rs 🔗

@@ -59,6 +59,7 @@ pub(crate) fn zed_default_dark() -> Theme {
     let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.);
     let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.);
     let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.);
+    let hover = hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0);
 
     let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0);
     let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0);
@@ -108,14 +109,14 @@ pub(crate) fn zed_default_dark() -> Theme {
                 surface_background: bg,
                 background: bg,
                 element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0),
-                element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
+                element_hover: hover,
                 element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
                 element_disabled: SystemColors::default().transparent,
                 element_selection_background: player.local().selection.alpha(0.25),
                 drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0),
                 ghost_element_background: SystemColors::default().transparent,
-                ghost_element_hover: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
+                ghost_element_hover: hover,
                 ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0),
                 ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0),
                 ghost_element_disabled: SystemColors::default().transparent,
@@ -202,10 +203,12 @@ pub(crate) fn zed_default_dark() -> Theme {
                 panel_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.),
                 panel_indent_guide_hover: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
                 panel_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
+                panel_overlay_background: bg,
+                panel_overlay_hover: hover,
                 pane_focused_border: blue,
                 pane_group_border: hsla(225. / 360., 13. / 100., 12. / 100., 1.),
                 scrollbar_thumb_background: gpui::transparent_black(),
-                scrollbar_thumb_hover_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0),
+                scrollbar_thumb_hover_background: hover,
                 scrollbar_thumb_active_background: hsla(
                     225.0 / 360.,
                     11.8 / 100.,

crates/theme/src/schema.rs 🔗

@@ -351,6 +351,12 @@ pub struct ThemeColorsContent {
     #[serde(rename = "panel.indent_guide_active")]
     pub panel_indent_guide_active: Option<String>,
 
+    #[serde(rename = "panel.overlay_background")]
+    pub panel_overlay_background: Option<String>,
+
+    #[serde(rename = "panel.overlay_hover")]
+    pub panel_overlay_hover: Option<String>,
+
     #[serde(rename = "pane.focused_border")]
     pub pane_focused_border: Option<String>,
 
@@ -674,6 +680,14 @@ impl ThemeColorsContent {
             .scrollbar_thumb_border
             .as_ref()
             .and_then(|color| try_parse_color(color).ok());
+        let element_hover = self
+            .element_hover
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok());
+        let panel_background = self
+            .panel_background
+            .as_ref()
+            .and_then(|color| try_parse_color(color).ok());
         ThemeColorsRefinement {
             border,
             border_variant: self
@@ -712,10 +726,7 @@ impl ThemeColorsContent {
                 .element_background
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            element_hover: self
-                .element_hover
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
+            element_hover,
             element_active: self
                 .element_active
                 .as_ref()
@@ -832,10 +843,7 @@ impl ThemeColorsContent {
                 .search_match_background
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
-            panel_background: self
-                .panel_background
-                .as_ref()
-                .and_then(|color| try_parse_color(color).ok()),
+            panel_background,
             panel_focused_border: self
                 .panel_focused_border
                 .as_ref()
@@ -852,6 +860,16 @@ impl ThemeColorsContent {
                 .panel_indent_guide_active
                 .as_ref()
                 .and_then(|color| try_parse_color(color).ok()),
+            panel_overlay_background: self
+                .panel_overlay_background
+                .as_ref()
+                .and_then(|color| try_parse_color(color).ok())
+                .or(panel_background),
+            panel_overlay_hover: self
+                .panel_overlay_hover
+                .as_ref()
+                .and_then(|color| try_parse_color(color).ok())
+                .or(element_hover),
             pane_focused_border: self
                 .pane_focused_border
                 .as_ref()

crates/theme/src/styles/colors.rs 🔗

@@ -131,6 +131,12 @@ pub struct ThemeColors {
     pub panel_indent_guide: Hsla,
     pub panel_indent_guide_hover: Hsla,
     pub panel_indent_guide_active: Hsla,
+
+    /// The color of the overlay surface on top of panel.
+    pub panel_overlay_background: Hsla,
+    /// The color of the overlay surface on top of panel when hovered over.
+    pub panel_overlay_hover: Hsla,
+
     pub pane_focused_border: Hsla,
     pub pane_group_border: Hsla,
     /// The color of the scrollbar thumb.
@@ -326,6 +332,8 @@ pub enum ThemeColorField {
     PanelIndentGuide,
     PanelIndentGuideHover,
     PanelIndentGuideActive,
+    PanelOverlayBackground,
+    PanelOverlayHover,
     PaneFocusedBorder,
     PaneGroupBorder,
     ScrollbarThumbBackground,
@@ -438,6 +446,8 @@ impl ThemeColors {
             ThemeColorField::PanelIndentGuide => self.panel_indent_guide,
             ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover,
             ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active,
+            ThemeColorField::PanelOverlayBackground => self.panel_overlay_background,
+            ThemeColorField::PanelOverlayHover => self.panel_overlay_hover,
             ThemeColorField::PaneFocusedBorder => self.pane_focused_border,
             ThemeColorField::PaneGroupBorder => self.pane_group_border,
             ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background,

crates/ui/src/components/scrollbar.rs 🔗

@@ -29,8 +29,8 @@ impl ThumbState {
 }
 
 impl ScrollableHandle for UniformListScrollHandle {
-    fn content_size(&self) -> Size<Pixels> {
-        self.0.borrow().base_handle.content_size()
+    fn max_offset(&self) -> Size<Pixels> {
+        self.0.borrow().base_handle.max_offset()
     }
 
     fn set_offset(&self, point: Point<Pixels>) {
@@ -47,8 +47,8 @@ impl ScrollableHandle for UniformListScrollHandle {
 }
 
 impl ScrollableHandle for ListState {
-    fn content_size(&self) -> Size<Pixels> {
-        self.content_size_for_scrollbar()
+    fn max_offset(&self) -> Size<Pixels> {
+        self.max_offset_for_scrollbar()
     }
 
     fn set_offset(&self, point: Point<Pixels>) {
@@ -73,8 +73,8 @@ impl ScrollableHandle for ListState {
 }
 
 impl ScrollableHandle for ScrollHandle {
-    fn content_size(&self) -> Size<Pixels> {
-        self.padded_content_size()
+    fn max_offset(&self) -> Size<Pixels> {
+        self.max_offset()
     }
 
     fn set_offset(&self, point: Point<Pixels>) {
@@ -91,7 +91,10 @@ impl ScrollableHandle for ScrollHandle {
 }
 
 pub trait ScrollableHandle: Any + Debug {
-    fn content_size(&self) -> Size<Pixels>;
+    fn content_size(&self) -> Size<Pixels> {
+        self.viewport().size + self.max_offset()
+    }
+    fn max_offset(&self) -> Size<Pixels>;
     fn set_offset(&self, point: Point<Pixels>);
     fn offset(&self) -> Point<Pixels>;
     fn viewport(&self) -> Bounds<Pixels>;
@@ -149,17 +152,17 @@ impl ScrollbarState {
 
     fn thumb_range(&self, axis: ScrollbarAxis) -> Option<Range<f32>> {
         const MINIMUM_THUMB_SIZE: Pixels = px(25.);
-        let content_size = self.scroll_handle.content_size().along(axis);
+        let max_offset = self.scroll_handle.max_offset().along(axis);
         let viewport_size = self.scroll_handle.viewport().size.along(axis);
-        if content_size.is_zero() || viewport_size.is_zero() || content_size <= viewport_size {
+        if max_offset.is_zero() || viewport_size.is_zero() {
             return None;
         }
+        let content_size = viewport_size + max_offset;
         let visible_percentage = viewport_size / content_size;
         let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage);
         if thumb_size > viewport_size {
             return None;
         }
-        let max_offset = content_size - viewport_size;
         let current_offset = self
             .scroll_handle
             .offset()
@@ -307,7 +310,7 @@ impl Element for Scrollbar {
 
             let compute_click_offset =
                 move |event_position: Point<Pixels>,
-                      item_size: Size<Pixels>,
+                      max_offset: Size<Pixels>,
                       event_type: ScrollbarMouseEvent| {
                     let viewport_size = padded_bounds.size.along(axis);
 
@@ -323,7 +326,7 @@ impl Element for Scrollbar {
                         - thumb_offset)
                         .clamp(px(0.), viewport_size - thumb_size);
 
-                    let max_offset = (item_size.along(axis) - viewport_size).max(px(0.));
+                    let max_offset = max_offset.along(axis);
                     let percentage = if viewport_size > thumb_size {
                         thumb_start / (viewport_size - thumb_size)
                     } else {
@@ -347,7 +350,7 @@ impl Element for Scrollbar {
                     } else {
                         let click_offset = compute_click_offset(
                             event.position,
-                            scroll.content_size(),
+                            scroll.max_offset(),
                             ScrollbarMouseEvent::GutterClick,
                         );
                         scroll.set_offset(scroll.offset().apply_along(axis, |_| click_offset));
@@ -373,7 +376,7 @@ impl Element for Scrollbar {
                     ThumbState::Dragging(drag_state) if event.dragging() => {
                         let drag_offset = compute_click_offset(
                             event.position,
-                            scroll.content_size(),
+                            scroll.max_offset(),
                             ScrollbarMouseEvent::ThumbDrag(drag_state),
                         );
                         scroll.set_offset(scroll.offset().apply_along(axis, |_| drag_offset));

crates/workspace/src/pane.rs 🔗

@@ -18,7 +18,7 @@ use futures::{StreamExt, stream::FuturesUnordered};
 use gpui::{
     Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
     DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
-    Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
+    Focusable, IsZero, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point,
     PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window,
     actions, anchored, deferred, prelude::*,
 };
@@ -46,8 +46,8 @@ use theme::ThemeSettings;
 use ui::{
     ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton,
     IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label,
-    PopoverMenu, PopoverMenuHandle, ScrollableHandle, Tab, TabBar, TabPosition, Tooltip,
-    prelude::*, right_click_menu,
+    PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*,
+    right_click_menu,
 };
 use util::{ResultExt, debug_panic, maybe, truncate_and_remove_front};
 
@@ -2865,10 +2865,9 @@ impl Pane {
                 }
             })
             .children(pinned_tabs.len().ne(&0).then(|| {
-                let content_width = self.tab_bar_scroll_handle.content_size().width;
-                let viewport_width = self.tab_bar_scroll_handle.viewport().size.width;
+                let max_scroll = self.tab_bar_scroll_handle.max_offset().width;
                 // We need to check both because offset returns delta values even when the scroll handle is not scrollable
-                let is_scrollable = content_width > viewport_width;
+                let is_scrollable = !max_scroll.is_zero();
                 let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.);
                 let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count;
                 h_flex()

crates/zed/src/main.rs 🔗

@@ -1,6 +1,7 @@
 mod reliability;
 mod zed;
 
+use agent_ui::AgentPanel;
 use anyhow::{Context as _, Result};
 use clap::{Parser, command};
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
@@ -14,7 +15,7 @@ use extension_host::ExtensionStore;
 use fs::{Fs, RealFs};
 use futures::{StreamExt, channel::oneshot, future};
 use git::GitHostingProviderRegistry;
-use gpui::{App, AppContext as _, Application, AsyncApp, UpdateGlobal as _};
+use gpui::{App, AppContext as _, Application, AsyncApp, Focusable as _, UpdateGlobal as _};
 
 use gpui_tokio::Tokio;
 use http_client::{Url, read_proxy_from_env};
@@ -24,7 +25,6 @@ use reqwest_client::ReqwestClient;
 
 use assets::Assets;
 use node_runtime::{NodeBinaryOptions, NodeRuntime};
-use onboarding::show_onboarding_view;
 use parking_lot::Mutex;
 use project::project_settings::ProjectSettings;
 use recent_projects::{SshSettings, open_ssh_project};
@@ -56,6 +56,8 @@ use zed::{
     inline_completion_registry, open_paths_with_positions,
 };
 
+use crate::zed::OpenRequestKind;
+
 #[cfg(feature = "mimalloc")]
 #[global_allocator]
 static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@@ -747,32 +749,46 @@ pub fn main() {
 }
 
 fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut App) {
-    if let Some(connection) = request.cli_connection {
-        let app_state = app_state.clone();
-        cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await)
-            .detach();
-        return;
-    }
-
-    if let Some(action_index) = request.dock_menu_action {
-        cx.perform_dock_menu_action(action_index);
-        return;
-    }
+    if let Some(kind) = request.kind {
+        match kind {
+            OpenRequestKind::CliConnection(connection) => {
+                let app_state = app_state.clone();
+                cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await)
+                    .detach();
+            }
+            OpenRequestKind::Extension { extension_id } => {
+                cx.spawn(async move |cx| {
+                    let workspace =
+                        workspace::get_any_active_workspace(app_state, cx.clone()).await?;
+                    workspace.update(cx, |_, window, cx| {
+                        window.dispatch_action(
+                            Box::new(zed_actions::Extensions {
+                                category_filter: None,
+                                id: Some(extension_id),
+                            }),
+                            cx,
+                        );
+                    })
+                })
+                .detach_and_log_err(cx);
+            }
+            OpenRequestKind::AgentPanel => {
+                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::<AgentPanel>(cx) {
+                            panel.focus_handle(cx).focus(window);
+                        }
+                    })
+                })
+                .detach_and_log_err(cx);
+            }
+            OpenRequestKind::DockMenuAction { index } => {
+                cx.perform_dock_menu_action(index);
+            }
+        }
 
-    if let Some(extension) = request.extension_id {
-        cx.spawn(async move |cx| {
-            let workspace = workspace::get_any_active_workspace(app_state, cx.clone()).await?;
-            workspace.update(cx, |_, window, cx| {
-                window.dispatch_action(
-                    Box::new(zed_actions::Extensions {
-                        category_filter: None,
-                        id: Some(extension),
-                    }),
-                    cx,
-                );
-            })
-        })
-        .detach_and_log_err(cx);
         return;
     }
 
@@ -1041,10 +1057,6 @@ async fn restore_or_create_workspace(app_state: Arc<AppState>, cx: &mut AsyncApp
                 );
             }
         }
-    } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
-        let state = app_state.clone();
-        cx.update(|cx| show_onboarding_view(state, cx))?.await?;
-        // cx.update(|cx| show_welcome_view(app_state, cx))?.await?;
     } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
         cx.update(|cx| show_welcome_view(app_state, cx))?.await?;
     } else {

crates/zed/src/zed/open_listener.rs 🔗

@@ -30,14 +30,20 @@ use workspace::{AppState, OpenOptions, SerializedWorkspaceLocation, Workspace};
 
 #[derive(Default, Debug)]
 pub struct OpenRequest {
-    pub cli_connection: Option<(mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)>,
+    pub kind: Option<OpenRequestKind>,
     pub open_paths: Vec<String>,
     pub diff_paths: Vec<[String; 2]>,
     pub open_channel_notes: Vec<(u64, Option<String>)>,
     pub join_channel: Option<u64>,
     pub ssh_connection: Option<SshConnectionOptions>,
-    pub dock_menu_action: Option<usize>,
-    pub extension_id: Option<String>,
+}
+
+#[derive(Debug)]
+pub enum OpenRequestKind {
+    CliConnection((mpsc::Receiver<CliRequest>, IpcSender<CliResponse>)),
+    Extension { extension_id: String },
+    AgentPanel,
+    DockMenuAction { index: usize },
 }
 
 impl OpenRequest {
@@ -45,9 +51,11 @@ impl OpenRequest {
         let mut this = Self::default();
         for url in request.urls {
             if let Some(server_name) = url.strip_prefix("zed-cli://") {
-                this.cli_connection = Some(connect_to_cli(server_name)?);
+                this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?));
             } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") {
-                this.dock_menu_action = Some(action_index.parse()?);
+                this.kind = Some(OpenRequestKind::DockMenuAction {
+                    index: action_index.parse()?,
+                });
             } else if let Some(file) = url.strip_prefix("file://") {
                 this.parse_file_path(file)
             } else if let Some(file) = url.strip_prefix("zed://file") {
@@ -55,8 +63,12 @@ impl OpenRequest {
             } else if let Some(file) = url.strip_prefix("zed://ssh") {
                 let ssh_url = "ssh:/".to_string() + file;
                 this.parse_ssh_file_path(&ssh_url, cx)?
-            } else if let Some(file) = url.strip_prefix("zed://extension/") {
-                this.extension_id = Some(file.to_string())
+            } else if let Some(extension_id) = url.strip_prefix("zed://extension/") {
+                this.kind = Some(OpenRequestKind::Extension {
+                    extension_id: extension_id.to_string(),
+                });
+            } else if url == "zed://agent" {
+                this.kind = Some(OpenRequestKind::AgentPanel);
             } else if url.starts_with("ssh://") {
                 this.parse_ssh_file_path(&url, cx)?
             } else if let Some(request_path) = parse_zed_link(&url, cx) {

docs/src/languages/php.md 🔗

@@ -15,7 +15,7 @@ The PHP extension offers both `phpactor` and `intelephense` language server supp
 
 ## Phpactor
 
-The Zed PHP Extension can install `phpactor` automatically but requires `php` to installed and available in your path:
+The Zed PHP Extension can install `phpactor` automatically but requires `php` to be installed and available in your path:
 
 ```sh
 # brew install php            # macOS