copilot: Un-globalify copilot + handle it more directly with EditPredictionStore (#46618)

Piotr Osiewicz created

- **copilot: Fix double lease panic when signing out**
- **Extract copilot_chat into a separate crate**
- **Do not use re-exports from copilot**
- **Use new SignIn API**
- **Extract copilot_ui out of copilot**

Closes #7501

Release Notes:

- Fixed Copilot providing suggestions from different Zed windows.
- Copilot edit predictions now support jumping to unresolved
diagnostics.

Change summary

Cargo.lock                                                     |  54 
Cargo.toml                                                     |   3 
crates/copilot/Cargo.toml                                      |  11 
crates/copilot/src/copilot.rs                                  | 400 ++-
crates/copilot/src/copilot_edit_prediction_delegate.rs         |  42 
crates/copilot/src/request.rs                                  | 180 +
crates/copilot_chat/Cargo.toml                                 |  41 
crates/copilot_chat/LICENSE-GPL                                |   1 
crates/copilot_chat/src/copilot_chat.rs                        |   3 
crates/copilot_chat/src/responses.rs                           |   3 
crates/copilot_ui/Cargo.toml                                   |  32 
crates/copilot_ui/LICENSE-GPL                                  |   1 
crates/copilot_ui/src/copilot_ui.rs                            |  25 
crates/copilot_ui/src/sign_in.rs                               |  87 
crates/edit_prediction/Cargo.toml                              |   1 
crates/edit_prediction/src/edit_prediction.rs                  |  36 
crates/edit_prediction/src/onboarding_modal.rs                 |  10 
crates/edit_prediction_ui/Cargo.toml                           |   2 
crates/edit_prediction_ui/src/edit_prediction_button.rs        |  32 
crates/language_models/Cargo.toml                              |   2 
crates/language_models/src/provider/copilot_chat.rs            |  95 
crates/language_tools/Cargo.toml                               |   2 
crates/language_tools/src/lsp_log_view.rs                      |  83 
crates/project/src/lsp_store/log_store.rs                      |  13 
crates/settings_ui/Cargo.toml                                  |   2 
crates/settings_ui/src/pages/edit_prediction_provider_setup.rs |  56 
crates/zed/Cargo.toml                                          |   2 
crates/zed/src/main.rs                                         |  15 
crates/zed/src/zed.rs                                          |   5 
crates/zed/src/zed/edit_prediction_registry.rs                 |  11 
30 files changed, 877 insertions(+), 373 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -3669,13 +3669,12 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-std",
- "chrono",
  "client",
  "clock",
  "collections",
  "command_palette_hooks",
+ "copilot_chat",
  "ctor",
- "dirs 4.0.0",
  "edit_prediction_types",
  "editor",
  "fs",
@@ -3683,11 +3682,9 @@ dependencies = [
  "gpui",
  "http_client",
  "indoc",
- "itertools 0.14.0",
  "language",
  "log",
  "lsp",
- "menu",
  "node_runtime",
  "parking_lot",
  "paths",
@@ -3698,13 +3695,45 @@ dependencies = [
  "serde_json",
  "settings",
  "sum_tree",
- "task",
  "theme",
+ "util",
+ "zlog",
+]
+
+[[package]]
+name = "copilot_chat"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "collections",
+ "dirs 4.0.0",
+ "fs",
+ "futures 0.3.31",
+ "gpui",
+ "http_client",
+ "itertools 0.14.0",
+ "log",
+ "paths",
+ "serde",
+ "serde_json",
+ "settings",
+]
+
+[[package]]
+name = "copilot_ui"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "copilot",
+ "gpui",
+ "log",
+ "lsp",
+ "menu",
+ "serde_json",
  "ui",
- "url",
  "util",
  "workspace",
- "zlog",
 ]
 
 [[package]]
@@ -5199,6 +5228,7 @@ dependencies = [
  "cloud_llm_client",
  "collections",
  "copilot",
+ "copilot_ui",
  "ctor",
  "db",
  "edit_prediction_context",
@@ -5349,6 +5379,8 @@ dependencies = [
  "collections",
  "command_palette_hooks",
  "copilot",
+ "copilot_chat",
+ "copilot_ui",
  "edit_prediction",
  "edit_prediction_types",
  "editor",
@@ -8960,6 +8992,8 @@ dependencies = [
  "component",
  "convert_case 0.8.0",
  "copilot",
+ "copilot_chat",
+ "copilot_ui",
  "credentials_provider",
  "deepseek",
  "editor",
@@ -9041,7 +9075,7 @@ dependencies = [
  "client",
  "collections",
  "command_palette_hooks",
- "copilot",
+ "edit_prediction",
  "editor",
  "futures 0.3.31",
  "gpui",
@@ -14939,7 +14973,7 @@ dependencies = [
  "assets",
  "bm25",
  "client",
- "copilot",
+ "copilot_ui",
  "edit_prediction",
  "editor",
  "feature_flags",
@@ -20732,6 +20766,8 @@ dependencies = [
  "component",
  "component_preview",
  "copilot",
+ "copilot_chat",
+ "copilot_ui",
  "crashes",
  "dap",
  "dap_adapters",

Cargo.toml 🔗

@@ -42,6 +42,7 @@ members = [
     "crates/component_preview",
     "crates/context_server",
     "crates/copilot",
+    "crates/copilot_chat",
     "crates/crashes",
     "crates/credentials_provider",
     "crates/dap",
@@ -280,6 +281,8 @@ component = { path = "crates/component" }
 component_preview  = { path = "crates/component_preview" }
 context_server = { path = "crates/context_server" }
 copilot = { path = "crates/copilot" }
+copilot_chat = { path = "crates/copilot_chat" }
+copilot_ui = { path = "crates/copilot_ui" }
 crashes = { path = "crates/crashes" }
 credentials_provider = { path = "crates/credentials_provider" }
 crossbeam = "0.8.4"

crates/copilot/Cargo.toml 🔗

@@ -25,19 +25,16 @@ test-support = [
 
 [dependencies]
 anyhow.workspace = true
-chrono.workspace = true
 collections.workspace = true
 command_palette_hooks.workspace = true
-dirs.workspace = true
+copilot_chat.workspace = true
 fs.workspace = true
 futures.workspace = true
 gpui.workspace = true
-http_client.workspace = true
 edit_prediction_types.workspace = true
 language.workspace = true
 log.workspace = true
 lsp.workspace = true
-menu.workspace = true
 node_runtime.workspace = true
 parking_lot.workspace = true
 paths.workspace = true
@@ -47,12 +44,7 @@ serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true
 sum_tree.workspace = true
-task.workspace = true
-ui.workspace = true
 util.workspace = true
-workspace.workspace = true
-itertools.workspace = true
-url.workspace = true
 
 [target.'cfg(windows)'.dependencies]
 async-std = { version = "1.12.0", features = ["unstable"] }
@@ -76,5 +68,4 @@ serde_json.workspace = true
 settings = { workspace = true, features = ["test-support"] }
 theme = { workspace = true, features = ["test-support"] }
 util = { workspace = true, features = ["test-support"] }
-workspace = { workspace = true, features = ["test-support"] }
 zlog.workspace = true

crates/copilot/src/copilot.rs 🔗

@@ -1,21 +1,19 @@
-pub mod copilot_chat;
 mod copilot_edit_prediction_delegate;
-pub mod copilot_responses;
 pub mod request;
-mod sign_in;
 
-use crate::request::NextEditSuggestions;
-use crate::sign_in::initiate_sign_out;
+use crate::request::{
+    DidFocus, DidFocusParams, FormattingOptions, InlineCompletionContext,
+    InlineCompletionTriggerKind, InlineCompletions, NextEditSuggestions,
+};
 use ::fs::Fs;
 use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet};
 use command_palette_hooks::CommandPaletteFilter;
-use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared};
+use futures::{Future, FutureExt, TryFutureExt, channel::oneshot, future::Shared, select_biased};
 use gpui::{
     App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Global, Task,
     WeakEntity, actions,
 };
-use http_client::HttpClient;
 use language::language_settings::CopilotSettings;
 use language::{
     Anchor, Bias, Buffer, BufferSnapshot, Language, PointUtf16, ToPointUtf16,
@@ -25,8 +23,8 @@ use language::{
 use lsp::{LanguageServer, LanguageServerBinary, LanguageServerId, LanguageServerName};
 use node_runtime::{NodeRuntime, VersionStrategy};
 use parking_lot::Mutex;
-use project::DisableAiSettings;
-use request::StatusNotification;
+use project::{DisableAiSettings, Project};
+use request::DidChangeStatus;
 use semver::Version;
 use serde_json::json;
 use settings::{Settings, SettingsStore};
@@ -42,13 +40,8 @@ use std::{
 };
 use sum_tree::Dimensions;
 use util::{ResultExt, fs::remove_matching};
-use workspace::Workspace;
 
 pub use crate::copilot_edit_prediction_delegate::CopilotEditPredictionDelegate;
-pub use crate::sign_in::{
-    ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
-    reinstall_and_sign_in,
-};
 
 actions!(
     copilot,
@@ -68,50 +61,6 @@ actions!(
     ]
 );
 
-pub fn init(
-    new_server_id: LanguageServerId,
-    fs: Arc<dyn Fs>,
-    http: Arc<dyn HttpClient>,
-    node_runtime: NodeRuntime,
-    cx: &mut App,
-) {
-    let language_settings = all_language_settings(None, cx);
-    let configuration = copilot_chat::CopilotChatConfiguration {
-        enterprise_uri: language_settings
-            .edit_predictions
-            .copilot
-            .enterprise_uri
-            .clone(),
-    };
-    copilot_chat::init(fs.clone(), http.clone(), configuration, cx);
-
-    let copilot = cx.new(move |cx| Copilot::start(new_server_id, fs, node_runtime, cx));
-    Copilot::set_global(copilot.clone(), cx);
-    cx.observe(&copilot, |copilot, cx| {
-        copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
-    })
-    .detach();
-    cx.observe_global::<SettingsStore>(|cx| {
-        if let Some(copilot) = Copilot::global(cx) {
-            copilot.update(cx, |copilot, cx| copilot.update_action_visibilities(cx));
-        }
-    })
-    .detach();
-
-    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
-        workspace.register_action(|_, _: &SignIn, window, cx| {
-            initiate_sign_in(window, cx);
-        });
-        workspace.register_action(|_, _: &Reinstall, window, cx| {
-            reinstall_and_sign_in(window, cx);
-        });
-        workspace.register_action(|_, _: &SignOut, window, cx| {
-            initiate_sign_out(window, cx);
-        });
-    })
-    .detach();
-}
-
 enum CopilotServer {
     Disabled,
     Starting { task: Shared<Task<()>> },
@@ -301,7 +250,7 @@ pub struct Copilot {
     server: CopilotServer,
     buffers: HashSet<WeakEntity<Buffer>>,
     server_id: LanguageServerId,
-    _subscription: gpui::Subscription,
+    _subscriptions: [gpui::Subscription; 2],
 }
 
 pub enum Event {
@@ -316,13 +265,21 @@ struct GlobalCopilot(Entity<Copilot>);
 
 impl Global for GlobalCopilot {}
 
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum CompletionSource {
+    NextEditSuggestion,
+    InlineCompletion,
+}
+
 /// Copilot's NextEditSuggestion response, with coordinates converted to Anchors.
-struct CopilotEditPrediction {
-    buffer: Entity<Buffer>,
-    range: Range<Anchor>,
-    text: String,
-    command: Option<lsp::Command>,
-    snapshot: BufferSnapshot,
+#[derive(Clone)]
+pub(crate) struct CopilotEditPrediction {
+    pub(crate) buffer: Entity<Buffer>,
+    pub(crate) range: Range<Anchor>,
+    pub(crate) text: String,
+    pub(crate) command: Option<lsp::Command>,
+    pub(crate) snapshot: BufferSnapshot,
+    pub(crate) source: CompletionSource,
 }
 
 impl Copilot {
@@ -335,19 +292,37 @@ impl Copilot {
         cx.set_global(GlobalCopilot(copilot));
     }
 
-    fn start(
+    pub fn new(
+        project: Entity<Project>,
         new_server_id: LanguageServerId,
         fs: Arc<dyn Fs>,
         node_runtime: NodeRuntime,
         cx: &mut Context<Self>,
     ) -> Self {
+        let send_focus_notification =
+            cx.subscribe(&project, |this, project, e: &project::Event, cx| {
+                if let project::Event::ActiveEntryChanged(new_entry) = e
+                    && let Ok(running) = this.server.as_authenticated()
+                {
+                    let uri = new_entry
+                        .and_then(|id| project.read(cx).path_for_entry(id, cx))
+                        .and_then(|entry| project.read(cx).absolute_path(&entry, cx))
+                        .and_then(|abs_path| lsp::Uri::from_file_path(abs_path).ok());
+
+                    _ = running.lsp.notify::<DidFocus>(DidFocusParams { uri });
+                }
+            });
+        let _subscriptions = [
+            cx.on_app_quit(Self::shutdown_language_server),
+            send_focus_notification,
+        ];
         let mut this = Self {
             server_id: new_server_id,
             fs,
             node_runtime,
             server: CopilotServer::Disabled,
             buffers: Default::default(),
-            _subscription: cx.on_app_quit(Self::shutdown_language_server),
+            _subscriptions,
         };
         this.start_copilot(true, false, cx);
         cx.observe_global::<SettingsStore>(move |this, cx| {
@@ -357,6 +332,11 @@ impl Copilot {
                     .context("copilot setting change: did change configuration")
                     .log_err();
             }
+            this.update_action_visibilities(cx);
+        })
+        .detach();
+        cx.observe_self(|copilot, cx| {
+            copilot.update_action_visibilities(cx);
         })
         .detach();
         this
@@ -448,6 +428,7 @@ impl Copilot {
     #[cfg(any(test, feature = "test-support"))]
     pub fn fake(cx: &mut gpui::TestAppContext) -> (Entity<Self>, lsp::FakeLanguageServer) {
         use fs::FakeFs;
+        use gpui::Subscription;
         use lsp::FakeLanguageServer;
         use node_runtime::NodeRuntime;
 
@@ -463,6 +444,7 @@ impl Copilot {
             &mut cx.to_async(),
         );
         let node_runtime = NodeRuntime::unavailable();
+        let send_focus_notification = Subscription::new(|| {});
         let this = cx.new(|cx| Self {
             server_id: LanguageServerId(0),
             fs: FakeFs::new(cx.background_executor().clone()),
@@ -472,7 +454,10 @@ impl Copilot {
                 sign_in_status: SignInStatus::Authorized,
                 registered_buffers: Default::default(),
             }),
-            _subscription: cx.on_app_quit(Self::shutdown_language_server),
+            _subscriptions: [
+                send_focus_notification,
+                cx.on_app_quit(Self::shutdown_language_server),
+            ],
             buffers: Default::default(),
         });
         (this, fake_server)
@@ -522,7 +507,51 @@ impl Copilot {
             )?;
 
             server
-                .on_notification::<StatusNotification, _>(|_, _| { /* Silence the notification */ })
+                .on_notification::<DidChangeStatus, _>({
+                    let this = this.clone();
+                    move |params, cx| {
+                        if params.kind == request::StatusKind::Normal {
+                            let this = this.clone();
+                            cx.spawn(async move |cx| {
+                                let lsp = this
+                                    .read_with(cx, |copilot, _| {
+                                        if let CopilotServer::Running(server) = &copilot.server {
+                                            Some(server.lsp.clone())
+                                        } else {
+                                            None
+                                        }
+                                    })
+                                    .ok()
+                                    .flatten();
+                                let Some(lsp) = lsp else { return };
+                                let status = lsp
+                                    .request::<request::CheckStatus>(request::CheckStatusParams {
+                                        local_checks_only: false,
+                                    })
+                                    .await
+                                    .into_response()
+                                    .ok();
+                                if let Some(status) = status {
+                                    this.update(cx, |copilot, cx| {
+                                        copilot.update_sign_in_status(status, cx);
+                                    })
+                                    .ok();
+                                }
+                            })
+                            .detach();
+                        }
+                    }
+                })
+                .detach();
+
+            server
+                .on_request::<lsp::request::ShowDocument, _, _>(move |params, cx| {
+                    if params.external.unwrap_or(false) {
+                        let url = params.uri.to_string();
+                        cx.update(|cx| cx.open_url(&url));
+                    }
+                    async move { Ok(lsp::ShowDocumentResult { success: true }) }
+                })
                 .detach();
 
             let configuration = lsp::DidChangeConfigurationParams {
@@ -545,6 +574,12 @@ impl Copilot {
                 .update(|cx| {
                     let mut params = server.default_initialize_params(false, cx);
                     params.initialization_options = Some(editor_info_json);
+                    params
+                        .capabilities
+                        .window
+                        .get_or_insert_with(Default::default)
+                        .show_document =
+                        Some(lsp::ShowDocumentClientCapabilities { support: true });
                     server.initialize(params, configuration.into(), cx)
                 })
                 .await?;
@@ -615,55 +650,37 @@ impl Copilot {
                 }
                 SignInStatus::SignedOut { .. } | SignInStatus::Unauthorized => {
                     let lsp = server.lsp.clone();
+
                     let task = cx
                         .spawn(async move |this, cx| {
                             let sign_in = async {
-                                let sign_in = lsp
-                                    .request::<request::SignInInitiate>(
-                                        request::SignInInitiateParams {},
-                                    )
+                                let flow = lsp
+                                    .request::<request::SignIn>(request::SignInParams {})
                                     .await
                                     .into_response()
                                     .context("copilot sign-in")?;
-                                match sign_in {
-                                    request::SignInInitiateResult::AlreadySignedIn { user } => {
-                                        Ok(request::SignInStatus::Ok { user: Some(user) })
-                                    }
-                                    request::SignInInitiateResult::PromptUserDeviceFlow(flow) => {
-                                        this.update(cx, |this, cx| {
-                                            if let CopilotServer::Running(RunningCopilotServer {
-                                                sign_in_status: status,
-                                                ..
-                                            }) = &mut this.server
-                                                && let SignInStatus::SigningIn {
-                                                    prompt: prompt_flow,
-                                                    ..
-                                                } = status
-                                            {
-                                                *prompt_flow = Some(flow.clone());
-                                                cx.notify();
-                                            }
-                                        })?;
-                                        let response = lsp
-                                            .request::<request::SignInConfirm>(
-                                                request::SignInConfirmParams {
-                                                    user_code: flow.user_code,
-                                                },
-                                            )
-                                            .await
-                                            .into_response()
-                                            .context("copilot: sign in confirm")?;
-                                        Ok(response)
+
+                                this.update(cx, |this, cx| {
+                                    if let CopilotServer::Running(RunningCopilotServer {
+                                        sign_in_status: status,
+                                        ..
+                                    }) = &mut this.server
+                                        && let SignInStatus::SigningIn {
+                                            prompt: prompt_flow,
+                                            ..
+                                        } = status
+                                    {
+                                        *prompt_flow = Some(flow.clone());
+                                        cx.notify();
                                     }
-                                }
+                                })?;
+
+                                anyhow::Ok(())
                             };
 
                             let sign_in = sign_in.await;
                             this.update(cx, |this, cx| match sign_in {
-                                Ok(status) => {
-                                    this.update_sign_in_status(status, cx);
-                                    Ok(())
-                                }
+                                Ok(()) => Ok(()),
                                 Err(error) => {
                                     this.update_sign_in_status(
                                         request::SignInStatus::NotSignedIn,
@@ -691,7 +708,7 @@ impl Copilot {
         }
     }
 
-    pub(crate) fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
+    pub fn sign_out(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
         self.update_sign_in_status(request::SignInStatus::NotSignedIn, cx);
         match &self.server {
             CopilotServer::Running(RunningCopilotServer { lsp: server, .. }) => {
@@ -713,7 +730,7 @@ impl Copilot {
         }
     }
 
-    pub(crate) fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
+    pub fn reinstall(&mut self, cx: &mut Context<Self>) -> Shared<Task<()>> {
         let language_settings = all_language_settings(None, cx);
         let env = self.build_env(&language_settings.edit_predictions.copilot);
         let start_task = cx
@@ -901,39 +918,127 @@ impl Copilot {
             .registered_buffers
             .get_mut(&buffer.entity_id())
             .unwrap();
-        let snapshot = registered_buffer.report_changes(buffer, cx);
+        let pending_snapshot = registered_buffer.report_changes(buffer, cx);
         let buffer = buffer.read(cx);
         let uri = registered_buffer.uri.clone();
         let position = position.to_point_utf16(buffer);
+        let snapshot = buffer.snapshot();
+        let settings = snapshot.settings_at(0, cx);
+        let tab_size = settings.tab_size.get();
+        let hard_tabs = settings.hard_tabs;
+        drop(settings);
 
         cx.background_spawn(async move {
-            let (version, snapshot) = snapshot.await?;
-            let result = lsp
+            let (version, snapshot) = pending_snapshot.await?;
+            let lsp_position = point_to_lsp(position);
+
+            let nes_request = lsp
                 .request::<NextEditSuggestions>(request::NextEditSuggestionsParams {
-                    text_document: lsp::VersionedTextDocumentIdentifier { uri, version },
-                    position: point_to_lsp(position),
+                    text_document: lsp::VersionedTextDocumentIdentifier {
+                        uri: uri.clone(),
+                        version,
+                    },
+                    position: lsp_position,
                 })
-                .await
-                .into_response()
-                .context("copilot: get completions")?;
-            let completions = result
-                .edits
-                .into_iter()
-                .map(|completion| {
-                    let start = snapshot
-                        .clip_point_utf16(point_from_lsp(completion.range.start), Bias::Left);
-                    let end =
-                        snapshot.clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
-                    CopilotEditPrediction {
-                        buffer: buffer_entity.clone(),
-                        range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
-                        text: completion.text,
-                        command: completion.command,
-                        snapshot: snapshot.clone(),
-                    }
+                .fuse();
+
+            let inline_request = lsp
+                .request::<InlineCompletions>(request::InlineCompletionsParams {
+                    text_document: lsp::VersionedTextDocumentIdentifier {
+                        uri: uri.clone(),
+                        version,
+                    },
+                    position: lsp_position,
+                    context: InlineCompletionContext {
+                        trigger_kind: InlineCompletionTriggerKind::Automatic,
+                    },
+                    formatting_options: Some(FormattingOptions {
+                        tab_size,
+                        insert_spaces: !hard_tabs,
+                    }),
                 })
-                .collect();
-            anyhow::Ok(completions)
+                .fuse();
+
+            futures::pin_mut!(nes_request, inline_request);
+
+            let convert_nes =
+                |result: request::NextEditSuggestionsResult| -> Vec<CopilotEditPrediction> {
+                    result
+                        .edits
+                        .into_iter()
+                        .map(|completion| {
+                            let start = snapshot.clip_point_utf16(
+                                point_from_lsp(completion.range.start),
+                                Bias::Left,
+                            );
+                            let end = snapshot
+                                .clip_point_utf16(point_from_lsp(completion.range.end), Bias::Left);
+                            CopilotEditPrediction {
+                                buffer: buffer_entity.clone(),
+                                range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                text: completion.text,
+                                command: completion.command,
+                                snapshot: snapshot.clone(),
+                                source: CompletionSource::NextEditSuggestion,
+                            }
+                        })
+                        .collect()
+                };
+
+            let convert_inline =
+                |result: request::InlineCompletionsResult| -> Vec<CopilotEditPrediction> {
+                    result
+                        .items
+                        .into_iter()
+                        .map(|item| {
+                            let start = snapshot
+                                .clip_point_utf16(point_from_lsp(item.range.start), Bias::Left);
+                            let end = snapshot
+                                .clip_point_utf16(point_from_lsp(item.range.end), Bias::Left);
+                            CopilotEditPrediction {
+                                buffer: buffer_entity.clone(),
+                                range: snapshot.anchor_before(start)..snapshot.anchor_after(end),
+                                text: item.insert_text,
+                                command: item.command,
+                                snapshot: snapshot.clone(),
+                                source: CompletionSource::InlineCompletion,
+                            }
+                        })
+                        .collect()
+                };
+
+            let mut nes_result: Option<Vec<CopilotEditPrediction>> = None;
+            let mut inline_result: Option<Vec<CopilotEditPrediction>> = None;
+
+            loop {
+                select_biased! {
+                    nes = nes_request => {
+                        let completions = nes.into_response().ok().map(convert_nes).unwrap_or_default();
+                        if !completions.is_empty() {
+                            return Ok(completions);
+                        }
+                        nes_result = Some(completions);
+                    }
+                    inline = inline_request => {
+                        let completions = inline.into_response().ok().map(convert_inline).unwrap_or_default();
+                        if !completions.is_empty() && nes_result.is_some() {
+                            return Ok(completions);
+                        }
+                        inline_result = Some(completions);
+                    }
+                    complete => break,
+                }
+
+                if let (Some(nes), Some(inline)) = (&nes_result, &inline_result) {
+                    return if !nes.is_empty() {
+                        Ok(nes.clone())
+                    } else {
+                        Ok(inline.clone())
+                    };
+                }
+            }
+
+            Ok(nes_result.or(inline_result).unwrap_or_default())
         })
     }
 
@@ -988,7 +1093,11 @@ impl Copilot {
         }
     }
 
-    fn update_sign_in_status(&mut self, lsp_status: request::SignInStatus, cx: &mut Context<Self>) {
+    pub fn update_sign_in_status(
+        &mut self,
+        lsp_status: request::SignInStatus,
+        cx: &mut Context<Self>,
+    ) {
         self.buffers.retain(|buffer| buffer.is_upgradable());
 
         if let Ok(server) = self.server.as_running() {
@@ -1320,9 +1429,14 @@ mod tests {
         );
 
         // Ensure all previously-registered buffers are re-opened when signing in.
-        lsp.set_request_handler::<request::SignInInitiate, _, _>(|_, _| async {
-            Ok(request::SignInInitiateResult::AlreadySignedIn {
-                user: "user-1".into(),
+        lsp.set_request_handler::<request::SignIn, _, _>(|_, _| async {
+            Ok(request::PromptUserDeviceFlow {
+                user_code: "test-code".into(),
+                command: lsp::Command {
+                    title: "Sign in".into(),
+                    command: "github.copilot.finishDeviceFlow".into(),
+                    arguments: None,
+                },
             })
         });
         copilot
@@ -1330,6 +1444,16 @@ mod tests {
             .await
             .unwrap();
 
+        // Simulate auth completion by directly updating sign-in status
+        copilot.update(cx, |copilot, cx| {
+            copilot.update_sign_in_status(
+                request::SignInStatus::Ok {
+                    user: Some("user-1".into()),
+                },
+                cx,
+            );
+        });
+
         assert_eq!(
             lsp.receive_notification::<lsp::notification::DidOpenTextDocument>()
                 .await,

crates/copilot/src/copilot_edit_prediction_delegate.rs 🔗

@@ -1,8 +1,14 @@
-use crate::{Copilot, CopilotEditPrediction};
+use crate::{
+    CompletionSource, Copilot, CopilotEditPrediction,
+    request::{
+        DidShowCompletion, DidShowCompletionParams, DidShowInlineEdit, DidShowInlineEditParams,
+        InlineCompletionItem,
+    },
+};
 use anyhow::Result;
 use edit_prediction_types::{EditPrediction, EditPredictionDelegate, interpolate_edits};
 use gpui::{App, Context, Entity, Task};
-use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt};
+use language::{Anchor, Buffer, BufferSnapshot, EditPreview, OffsetRangeExt, ToPointUtf16};
 use std::{ops::Range, sync::Arc, time::Duration};
 
 pub const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75);
@@ -137,7 +143,37 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate {
         )];
         let edits = interpolate_edits(&completion.snapshot, &buffer.snapshot(), &edits)
             .filter(|edits| !edits.is_empty())?;
-
+        self.copilot.update(cx, |this, _| {
+            if let Ok(server) = this.server.as_authenticated() {
+                match completion.source {
+                    CompletionSource::NextEditSuggestion => {
+                        if let Some(cmd) = completion.command.as_ref() {
+                            _ = server
+                                .lsp
+                                .notify::<DidShowInlineEdit>(DidShowInlineEditParams {
+                                    item: serde_json::json!({"command": {"arguments": cmd.arguments}}),
+                                });
+                        }
+                    }
+                    CompletionSource::InlineCompletion => {
+                        _ = server.lsp.notify::<DidShowCompletion>(DidShowCompletionParams {
+                            item: InlineCompletionItem {
+                                insert_text: completion.text.clone(),
+                                range: lsp::Range::new(
+                                    language::point_to_lsp(
+                                        completion.range.start.to_point_utf16(&completion.snapshot),
+                                    ),
+                                    language::point_to_lsp(
+                                        completion.range.end.to_point_utf16(&completion.snapshot),
+                                    ),
+                                ),
+                                command: completion.command.clone(),
+                            },
+                        });
+                    }
+                }
+            }
+        });
         Some(EditPrediction::Local {
             id: None,
             edits,

crates/copilot/src/request.rs 🔗

@@ -1,4 +1,4 @@
-use lsp::VersionedTextDocumentIdentifier;
+use lsp::{Uri, VersionedTextDocumentIdentifier};
 use serde::{Deserialize, Serialize};
 
 pub enum CheckStatus {}
@@ -15,37 +15,22 @@ impl lsp::request::Request for CheckStatus {
     const METHOD: &'static str = "checkStatus";
 }
 
-pub enum SignInInitiate {}
+pub enum SignIn {}
 
 #[derive(Debug, Serialize, Deserialize)]
-pub struct SignInInitiateParams {}
+pub struct SignInParams {}
 
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(tag = "status")]
-pub enum SignInInitiateResult {
-    AlreadySignedIn { user: String },
-    PromptUserDeviceFlow(PromptUserDeviceFlow),
-}
-
-#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
+#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct PromptUserDeviceFlow {
     pub user_code: String,
-    pub verification_uri: String,
+    pub command: lsp::Command,
 }
 
-impl lsp::request::Request for SignInInitiate {
-    type Params = SignInInitiateParams;
-    type Result = SignInInitiateResult;
-    const METHOD: &'static str = "signInInitiate";
-}
-
-pub enum SignInConfirm {}
-
-#[derive(Debug, Serialize, Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub struct SignInConfirmParams {
-    pub user_code: String,
+impl lsp::request::Request for SignIn {
+    type Params = SignInParams;
+    type Result = PromptUserDeviceFlow;
+    const METHOD: &'static str = "signIn";
 }
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -67,12 +52,6 @@ pub enum SignInStatus {
     NotSignedIn,
 }
 
-impl lsp::request::Request for SignInConfirm {
-    type Params = SignInConfirmParams;
-    type Result = SignInStatus;
-    const METHOD: &'static str = "signInConfirm";
-}
-
 pub enum SignOut {}
 
 #[derive(Debug, Serialize, Deserialize)]
@@ -89,17 +68,26 @@ impl lsp::request::Request for SignOut {
     const METHOD: &'static str = "signOut";
 }
 
-pub enum StatusNotification {}
+pub enum DidChangeStatus {}
 
 #[derive(Debug, Serialize, Deserialize)]
-pub struct StatusNotificationParams {
-    pub message: String,
-    pub status: String, // One of Normal/InProgress
+pub struct DidChangeStatusParams {
+    #[serde(default)]
+    pub message: Option<String>,
+    pub kind: StatusKind,
 }
 
-impl lsp::notification::Notification for StatusNotification {
-    type Params = StatusNotificationParams;
-    const METHOD: &'static str = "statusNotification";
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+pub enum StatusKind {
+    Normal,
+    Error,
+    Warning,
+    Inactive,
+}
+
+impl lsp::notification::Notification for DidChangeStatus {
+    type Params = DidChangeStatusParams;
+    const METHOD: &'static str = "didChangeStatus";
 }
 
 pub enum SetEditorInfo {}
@@ -191,3 +179,121 @@ impl lsp::request::Request for NextEditSuggestions {
 
     const METHOD: &'static str = "textDocument/copilotInlineEdit";
 }
+
+pub(crate) struct DidFocus;
+
+#[derive(Serialize, Deserialize)]
+pub(crate) struct DidFocusParams {
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub(crate) uri: Option<Uri>,
+}
+
+impl lsp::notification::Notification for DidFocus {
+    type Params = DidFocusParams;
+    const METHOD: &'static str = "textDocument/didFocus";
+}
+
+pub(crate) struct DidShowInlineEdit;
+
+#[derive(Serialize, Deserialize)]
+pub(crate) struct DidShowInlineEditParams {
+    pub(crate) item: serde_json::Value,
+}
+
+impl lsp::notification::Notification for DidShowInlineEdit {
+    type Params = DidShowInlineEditParams;
+    const METHOD: &'static str = "textDocument/didShowInlineEdit";
+}
+
+// Inline Completions (non-NES) - textDocument/inlineCompletion
+
+pub enum InlineCompletions {}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InlineCompletionsParams {
+    pub text_document: VersionedTextDocumentIdentifier,
+    pub position: lsp::Position,
+    pub context: InlineCompletionContext,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub formatting_options: Option<FormattingOptions>,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InlineCompletionContext {
+    pub trigger_kind: InlineCompletionTriggerKind,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub enum InlineCompletionTriggerKind {
+    Invoked = 1,
+    Automatic = 2,
+}
+
+impl Serialize for InlineCompletionTriggerKind {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: serde::Serializer,
+    {
+        serializer.serialize_u8(*self as u8)
+    }
+}
+
+impl<'de> Deserialize<'de> for InlineCompletionTriggerKind {
+    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+    where
+        D: serde::Deserializer<'de>,
+    {
+        let value = u8::deserialize(deserializer)?;
+        match value {
+            1 => Ok(InlineCompletionTriggerKind::Invoked),
+            2 => Ok(InlineCompletionTriggerKind::Automatic),
+            _ => Err(serde::de::Error::custom("invalid trigger kind")),
+        }
+    }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FormattingOptions {
+    pub tab_size: u32,
+    pub insert_spaces: bool,
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InlineCompletionsResult {
+    pub items: Vec<InlineCompletionItem>,
+}
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InlineCompletionItem {
+    pub insert_text: String,
+    pub range: lsp::Range,
+    #[serde(skip_serializing_if = "Option::is_none")]
+    pub command: Option<lsp::Command>,
+}
+
+impl lsp::request::Request for InlineCompletions {
+    type Params = InlineCompletionsParams;
+    type Result = InlineCompletionsResult;
+
+    const METHOD: &'static str = "textDocument/inlineCompletion";
+}
+
+// Telemetry notifications for inline completions
+
+pub(crate) struct DidShowCompletion;
+
+#[derive(Debug, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub(crate) struct DidShowCompletionParams {
+    pub(crate) item: InlineCompletionItem,
+}
+
+impl lsp::notification::Notification for DidShowCompletion {
+    type Params = DidShowCompletionParams;
+    const METHOD: &'static str = "textDocument/didShowCompletion";
+}

crates/copilot_chat/Cargo.toml 🔗

@@ -0,0 +1,41 @@
+[package]
+name = "copilot_chat"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/copilot_chat.rs"
+doctest = false
+
+[features]
+default = []
+test-support = [
+    "collections/test-support",
+    "gpui/test-support",
+    "settings/test-support",
+]
+
+[dependencies]
+anyhow.workspace = true
+chrono.workspace = true
+collections.workspace = true
+dirs.workspace = true
+fs.workspace = true
+futures.workspace = true
+gpui.workspace = true
+http_client.workspace = true
+itertools.workspace = true
+log.workspace = true
+paths.workspace = true
+serde.workspace = true
+serde_json.workspace = true
+settings.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+serde_json.workspace = true

crates/copilot/src/copilot_chat.rs → crates/copilot_chat/src/copilot_chat.rs 🔗

@@ -1,3 +1,5 @@
+pub mod responses;
+
 use std::path::PathBuf;
 use std::sync::Arc;
 use std::sync::OnceLock;
@@ -16,7 +18,6 @@ use itertools::Itertools;
 use paths::home_dir;
 use serde::{Deserialize, Serialize};
 
-use crate::copilot_responses as responses;
 use settings::watch_config_dir;
 
 pub const COPILOT_OAUTH_ENV_VAR: &str = "GH_COPILOT_TOKEN";

crates/copilot/src/copilot_responses.rs → crates/copilot_chat/src/responses.rs 🔗

@@ -1,4 +1,5 @@
-use super::*;
+use std::sync::Arc;
+
 use anyhow::{Result, anyhow};
 use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
 use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};

crates/copilot_ui/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "copilot_ui"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/copilot_ui.rs"
+doctest = false
+
+[features]
+default = []
+test-support = [
+    "copilot/test-support",
+    "gpui/test-support",
+]
+
+[dependencies]
+anyhow.workspace = true
+copilot.workspace = true
+gpui.workspace = true
+log.workspace = true
+lsp.workspace = true
+menu.workspace = true
+serde_json.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace.workspace = true

crates/copilot_ui/src/copilot_ui.rs 🔗

@@ -0,0 +1,25 @@
+mod sign_in;
+
+use copilot::{Reinstall, SignIn, SignOut};
+use gpui::App;
+use workspace::Workspace;
+
+pub use sign_in::{
+    ConfigurationMode, ConfigurationView, CopilotCodeVerification, initiate_sign_in,
+    reinstall_and_sign_in,
+};
+
+pub fn init(cx: &mut App) {
+    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
+        workspace.register_action(|_, _: &SignIn, window, cx| {
+            sign_in::initiate_sign_in(window, cx);
+        });
+        workspace.register_action(|_, _: &Reinstall, window, cx| {
+            sign_in::reinstall_and_sign_in(window, cx);
+        });
+        workspace.register_action(|_, _: &SignOut, window, cx| {
+            sign_in::initiate_sign_out(window, cx);
+        });
+    })
+    .detach();
+}

crates/copilot/src/sign_in.rs → crates/copilot_ui/src/sign_in.rs 🔗

@@ -1,12 +1,11 @@
-use crate::{Copilot, Status, request::PromptUserDeviceFlow};
 use anyhow::Context as _;
+use copilot::{Copilot, Status, request, request::PromptUserDeviceFlow};
 use gpui::{
     App, ClipboardItem, Context, DismissEvent, Element, Entity, EventEmitter, FocusHandle,
     Focusable, InteractiveElement, IntoElement, MouseDownEvent, ParentElement, Render, Styled,
     Subscription, Window, WindowBounds, WindowOptions, div, point,
 };
 use ui::{ButtonLike, CommonAnimationExt, ConfiguredApiCard, Vector, VectorName, prelude::*};
-use url::Url;
 use util::ResultExt as _;
 use workspace::{Toast, Workspace, notifications::NotificationId};
 
@@ -187,22 +186,12 @@ impl CopilotCodeVerification {
         .detach();
 
         let status = copilot.read(cx).status();
-        // Determine sign-up URL based on verification_uri domain if available
-        let sign_up_url = if let Status::SigningIn {
-            prompt: Some(ref prompt),
-        } = status
-        {
-            // Extract domain from verification_uri to construct sign-up URL
-            Self::get_sign_up_url_from_verification(&prompt.verification_uri)
-        } else {
-            None
-        };
         Self {
             status,
             connect_clicked: false,
             focus_handle: cx.focus_handle(),
             copilot: copilot.clone(),
-            sign_up_url,
+            sign_up_url: None,
             _subscription: cx.observe(copilot, |this, copilot, cx| {
                 let status = copilot.read(cx).status();
                 match status {
@@ -216,30 +205,10 @@ impl CopilotCodeVerification {
     }
 
     pub fn set_status(&mut self, status: Status, cx: &mut Context<Self>) {
-        // Update sign-up URL if we have a new verification URI
-        if let Status::SigningIn {
-            prompt: Some(ref prompt),
-        } = status
-        {
-            self.sign_up_url = Self::get_sign_up_url_from_verification(&prompt.verification_uri);
-        }
         self.status = status;
         cx.notify();
     }
 
-    fn get_sign_up_url_from_verification(verification_uri: &str) -> Option<String> {
-        // Extract domain from verification URI using url crate
-        if let Ok(url) = Url::parse(verification_uri)
-            && let Some(host) = url.host_str()
-            && !host.contains("github.com")
-        {
-            // For GHE, construct URL from domain
-            Some(format!("https://{}/features/copilot", host))
-        } else {
-            None
-        }
-    }
-
     fn render_device_code(data: &PromptUserDeviceFlow, cx: &mut Context<Self>) -> impl IntoElement {
         let copied = cx
             .read_from_clipboard()
@@ -303,9 +272,49 @@ impl CopilotCodeVerification {
                             .style(ButtonStyle::Outlined)
                             .size(ButtonSize::Medium)
                             .on_click({
-                                let verification_uri = data.verification_uri.clone();
+                                let command = data.command.clone();
                                 cx.listener(move |this, _, _window, cx| {
-                                    cx.open_url(&verification_uri);
+                                    if let Some(copilot) = Copilot::global(cx) {
+                                        let command = command.clone();
+                                        let copilot_clone = copilot.clone();
+                                        copilot.update(cx, |copilot, cx| {
+                                            if let Some(server) = copilot.language_server() {
+                                                let server = server.clone();
+                                                cx.spawn(async move |_, cx| {
+                                                    let result = server
+                                                        .request::<lsp::request::ExecuteCommand>(
+                                                            lsp::ExecuteCommandParams {
+                                                                command: command.command.clone(),
+                                                                arguments: command
+                                                                    .arguments
+                                                                    .clone()
+                                                                    .unwrap_or_default(),
+                                                                ..Default::default()
+                                                            },
+                                                        )
+                                                        .await
+                                                        .into_response()
+                                                        .ok()
+                                                        .flatten();
+                                                    if let Some(value) = result {
+                                                        if let Ok(status) =
+                                                            serde_json::from_value::<
+                                                                request::SignInStatus,
+                                                            >(value)
+                                                        {
+                                                            copilot_clone
+                                                                .update(cx, |copilot, cx| {
+                                                                    copilot.update_sign_in_status(
+                                                                        status, cx,
+                                                                    );
+                                                                });
+                                                        }
+                                                    }
+                                                })
+                                                .detach();
+                                            }
+                                        });
+                                    }
                                     this.connect_clicked = true;
                                 })
                             }),
@@ -450,7 +459,7 @@ impl Render for CopilotCodeVerification {
 
 pub struct ConfigurationView {
     copilot_status: Option<Status>,
-    is_authenticated: fn(cx: &App) -> bool,
+    is_authenticated: Box<dyn Fn(&App) -> bool + 'static>,
     edit_prediction: bool,
     _subscription: Option<Subscription>,
 }
@@ -462,7 +471,7 @@ pub enum ConfigurationMode {
 
 impl ConfigurationView {
     pub fn new(
-        is_authenticated: fn(cx: &App) -> bool,
+        is_authenticated: impl Fn(&App) -> bool + 'static,
         mode: ConfigurationMode,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -470,7 +479,7 @@ impl ConfigurationView {
 
         Self {
             copilot_status: copilot.as_ref().map(|copilot| copilot.read(cx).status()),
-            is_authenticated,
+            is_authenticated: Box::new(is_authenticated),
             edit_prediction: matches!(mode, ConfigurationMode::EditPrediction),
             _subscription: copilot.as_ref().map(|copilot| {
                 cx.observe(copilot, |this, model, cx| {
@@ -669,7 +678,7 @@ impl ConfigurationView {
 
 impl Render for ConfigurationView {
     fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let is_authenticated = self.is_authenticated;
+        let is_authenticated = &self.is_authenticated;
 
         if is_authenticated(cx) {
             return ConfiguredApiCard::new("Authorized")

crates/edit_prediction/Cargo.toml 🔗

@@ -24,6 +24,7 @@ client.workspace = true
 cloud_llm_client.workspace = true
 collections.workspace = true
 copilot.workspace = true
+copilot_ui.workspace = true
 db.workspace = true
 edit_prediction_types.workspace = true
 edit_prediction_context.workspace = true

crates/edit_prediction/src/edit_prediction.rs 🔗

@@ -10,6 +10,7 @@ use cloud_llm_client::{
     PredictEditsRequestTrigger, RejectEditPredictionsBodyRef, ZED_VERSION_HEADER_NAME,
 };
 use collections::{HashMap, HashSet};
+use copilot::Copilot;
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use edit_prediction_context::EditPredictionExcerptOptions;
 use edit_prediction_context::{RelatedExcerptStore, RelatedExcerptStoreEvent, RelatedFile};
@@ -291,6 +292,7 @@ struct ProjectState {
     license_detection_watchers: HashMap<WorktreeId, Rc<LicenseDetectionWatcher>>,
     user_actions: VecDeque<UserActionRecord>,
     _subscription: gpui::Subscription,
+    copilot: Option<Entity<Copilot>>,
 }
 
 impl ProjectState {
@@ -662,6 +664,7 @@ impl EditPredictionStore {
             },
             sweep_ai: SweepAi::new(cx),
             mercury: Mercury::new(cx),
+
             data_collection_choice,
             reject_predictions_tx: reject_tx,
             rated_predictions: Default::default(),
@@ -783,6 +786,38 @@ impl EditPredictionStore {
             .unwrap_or_default()
     }
 
+    pub fn copilot_for_project(&self, project: &Entity<Project>) -> Option<Entity<Copilot>> {
+        self.projects
+            .get(&project.entity_id())
+            .and_then(|project| project.copilot.clone())
+    }
+
+    pub fn start_copilot_for_project(
+        &mut self,
+        project: &Entity<Project>,
+        cx: &mut Context<Self>,
+    ) -> Option<Entity<Copilot>> {
+        let state = self.get_or_init_project(project, cx);
+
+        if state.copilot.is_some() {
+            return state.copilot.clone();
+        }
+        let _project = project.clone();
+        let project = project.read(cx);
+
+        let node = project.node_runtime().cloned();
+        if let Some(node) = node {
+            let next_id = project.languages().next_language_server_id();
+            let fs = project.fs().clone();
+
+            let copilot = cx.new(|cx| Copilot::new(_project, next_id, fs, node, cx));
+            state.copilot = Some(copilot.clone());
+            Some(copilot)
+        } else {
+            None
+        }
+    }
+
     pub fn context_for_project_with_buffers<'a>(
         &'a self,
         project: &Entity<Project>,
@@ -853,6 +888,7 @@ impl EditPredictionStore {
                 license_detection_watchers: HashMap::default(),
                 user_actions: VecDeque::with_capacity(USER_ACTION_HISTORY_SIZE),
                 _subscription: cx.subscribe(&project, Self::handle_project_event),
+                copilot: None,
             })
     }
 

crates/edit_prediction/src/onboarding_modal.rs 🔗

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use crate::ZedPredictUpsell;
+use crate::{EditPredictionStore, ZedPredictUpsell};
 use ai_onboarding::EditPredictionOnboarding;
 use client::{Client, UserStore};
 use db::kvp::Dismissable;
@@ -50,15 +50,17 @@ impl ZedPredictModal {
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) {
+        let project = workspace.project().clone();
         workspace.toggle_modal(window, cx, |_window, cx| {
             let weak_entity = cx.weak_entity();
+            let copilot = EditPredictionStore::try_global(cx)
+                .and_then(|store| store.read(cx).copilot_for_project(&project));
             Self {
                 onboarding: cx.new(|cx| {
                     EditPredictionOnboarding::new(
                         user_store.clone(),
                         client.clone(),
-                        copilot::Copilot::global(cx)
-                            .is_some_and(|copilot| copilot.read(cx).status().is_configured()),
+                        copilot.is_some_and(|copilot| copilot.read(cx).status().is_configured()),
                         Arc::new({
                             let this = weak_entity.clone();
                             move |_window, cx| {
@@ -73,7 +75,7 @@ impl ZedPredictModal {
                                 ZedPredictUpsell::set_dismissed(true, cx);
                                 set_edit_prediction_provider(EditPredictionProvider::Copilot, cx);
                                 this.update(cx, |_, cx| cx.emit(DismissEvent)).ok();
-                                copilot::initiate_sign_in(window, cx);
+                                copilot_ui::initiate_sign_in(window, cx);
                             }
                         }),
                         cx,

crates/edit_prediction_ui/Cargo.toml 🔗

@@ -22,6 +22,8 @@ cloud_llm_client.workspace = true
 codestral.workspace = true
 command_palette_hooks.workspace = true
 copilot.workspace = true
+copilot_chat.workspace = true
+copilot_ui.workspace = true
 edit_prediction_types.workspace = true
 edit_prediction.workspace = true
 editor.workspace = true

crates/edit_prediction_ui/src/edit_prediction_button.rs 🔗

@@ -22,7 +22,7 @@ use language::{
     EditPredictionsMode, File, Language,
     language_settings::{self, AllLanguageSettings, EditPredictionProvider, all_language_settings},
 };
-use project::DisableAiSettings;
+use project::{DisableAiSettings, Project};
 use regex::Regex;
 use settings::{
     EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME,
@@ -75,6 +75,7 @@ pub struct EditPredictionButton {
     fs: Arc<dyn Fs>,
     user_store: Entity<UserStore>,
     popover_menu_handle: PopoverMenuHandle<ContextMenu>,
+    project: WeakEntity<Project>,
 }
 
 enum SupermavenButtonStatus {
@@ -95,7 +96,9 @@ impl Render for EditPredictionButton {
 
         match all_language_settings.edit_predictions.provider {
             EditPredictionProvider::Copilot => {
-                let Some(copilot) = Copilot::global(cx) else {
+                let Some(copilot) = EditPredictionStore::try_global(cx)
+                    .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?))
+                else {
                     return div().hidden();
                 };
                 let status = copilot.read(cx).status();
@@ -129,7 +132,7 @@ impl Render for EditPredictionButton {
                                             .on_click(
                                                 "Reinstall Copilot",
                                                 |window, cx| {
-                                                    copilot::reinstall_and_sign_in(window, cx)
+                                                    copilot_ui::reinstall_and_sign_in(window, cx)
                                                 },
                                             ),
                                             cx,
@@ -143,11 +146,16 @@ impl Render for EditPredictionButton {
                     );
                 }
                 let this = cx.weak_entity();
-
+                let project = self.project.clone();
                 div().child(
                     PopoverMenu::new("copilot")
                         .menu(move |window, cx| {
-                            let current_status = Copilot::global(cx)?.read(cx).status();
+                            let current_status = EditPredictionStore::try_global(cx)
+                                .and_then(|store| {
+                                    store.read(cx).copilot_for_project(&project.upgrade()?)
+                                })?
+                                .read(cx)
+                                .status();
                             match current_status {
                                 Status::Authorized => this.update(cx, |this, cx| {
                                     this.build_copilot_context_menu(window, cx)
@@ -478,6 +486,7 @@ impl EditPredictionButton {
         user_store: Entity<UserStore>,
         popover_menu_handle: PopoverMenuHandle<ContextMenu>,
         client: Arc<Client>,
+        project: Entity<Project>,
         cx: &mut Context<Self>,
     ) -> Self {
         if let Some(copilot) = Copilot::global(cx) {
@@ -514,6 +523,7 @@ impl EditPredictionButton {
             edit_prediction_provider: None,
             user_store,
             popover_menu_handle,
+            project: project.downgrade(),
             fs,
         }
     }
@@ -529,10 +539,10 @@ impl EditPredictionButton {
             ));
         }
 
-        if let Some(copilot) = Copilot::global(cx) {
-            if matches!(copilot.read(cx).status(), Status::Authorized) {
-                providers.push(EditPredictionProvider::Copilot);
-            }
+        if let Some(_) = EditPredictionStore::try_global(cx)
+            .and_then(|store| store.read(cx).copilot_for_project(&self.project.upgrade()?))
+        {
+            providers.push(EditPredictionProvider::Copilot);
         }
 
         if let Some(supermaven) = Supermaven::global(cx) {
@@ -629,7 +639,7 @@ impl EditPredictionButton {
     ) -> Entity<ContextMenu> {
         let fs = self.fs.clone();
         ContextMenu::build(window, cx, |menu, _, _| {
-            menu.entry("Sign In to Copilot", None, copilot::initiate_sign_in)
+            menu.entry("Sign In to Copilot", None, copilot_ui::initiate_sign_in)
                 .entry("Disable Copilot", None, {
                     let fs = fs.clone();
                     move |_window, cx| hide_copilot(fs.clone(), cx)
@@ -931,7 +941,7 @@ impl EditPredictionButton {
         cx: &mut Context<Self>,
     ) -> Entity<ContextMenu> {
         let all_language_settings = all_language_settings(None, cx);
-        let copilot_config = copilot::copilot_chat::CopilotChatConfiguration {
+        let copilot_config = copilot_chat::CopilotChatConfiguration {
             enterprise_uri: all_language_settings
                 .edit_predictions
                 .copilot

crates/language_models/Cargo.toml 🔗

@@ -26,6 +26,8 @@ collections.workspace = true
 component.workspace = true
 convert_case.workspace = true
 copilot.workspace = true
+copilot_chat.workspace = true
+copilot_ui.workspace = true
 credentials_provider.workspace = true
 deepseek = { workspace = true, features = ["schemars"] }
 extension.workspace = true

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

@@ -5,12 +5,13 @@ use std::sync::Arc;
 use anyhow::{Result, anyhow};
 use cloud_llm_client::CompletionIntent;
 use collections::HashMap;
-use copilot::copilot_chat::{
-    ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, ImageUrl,
-    Model as CopilotChatModel, ModelVendor, Request as CopilotChatRequest, ResponseEvent, Tool,
-    ToolCall,
-};
 use copilot::{Copilot, Status};
+use copilot_chat::responses as copilot_responses;
+use copilot_chat::{
+    ChatMessage, ChatMessageContent, ChatMessagePart, CopilotChat, CopilotChatConfiguration,
+    Function, FunctionContent, ImageUrl, Model as CopilotChatModel, ModelVendor,
+    Request as CopilotChatRequest, ResponseEvent, Tool, ToolCall, ToolCallContent, ToolChoice,
+};
 use futures::future::BoxFuture;
 use futures::stream::BoxStream;
 use futures::{FutureExt, Stream, StreamExt};
@@ -60,7 +61,7 @@ impl CopilotChatLanguageModelProvider {
                 _settings_subscription: cx.observe_global::<SettingsStore>(|_, cx| {
                     if let Some(copilot_chat) = CopilotChat::global(cx) {
                         let language_settings = all_language_settings(None, cx);
-                        let configuration = copilot::copilot_chat::CopilotChatConfiguration {
+                        let configuration = CopilotChatConfiguration {
                             enterprise_uri: language_settings
                                 .edit_predictions
                                 .copilot
@@ -178,13 +179,13 @@ impl LanguageModelProvider for CopilotChatLanguageModelProvider {
         cx: &mut App,
     ) -> AnyView {
         cx.new(|cx| {
-            copilot::ConfigurationView::new(
+            copilot_ui::ConfigurationView::new(
                 |cx| {
                     CopilotChat::global(cx)
                         .map(|m| m.read(cx).is_authenticated())
                         .unwrap_or(false)
                 },
-                copilot::ConfigurationMode::Chat,
+                copilot_ui::ConfigurationMode::Chat,
                 cx,
             )
         })
@@ -563,7 +564,7 @@ impl CopilotResponsesEventMapper {
 
     pub fn map_stream(
         mut self,
-        events: Pin<Box<dyn Send + Stream<Item = Result<copilot::copilot_responses::StreamEvent>>>>,
+        events: Pin<Box<dyn Send + Stream<Item = Result<copilot_responses::StreamEvent>>>>,
     ) -> impl Stream<Item = Result<LanguageModelCompletionEvent, LanguageModelCompletionError>>
     {
         events.flat_map(move |event| {
@@ -576,11 +577,11 @@ impl CopilotResponsesEventMapper {
 
     fn map_event(
         &mut self,
-        event: copilot::copilot_responses::StreamEvent,
+        event: copilot_responses::StreamEvent,
     ) -> Vec<Result<LanguageModelCompletionEvent, LanguageModelCompletionError>> {
         match event {
-            copilot::copilot_responses::StreamEvent::OutputItemAdded { item, .. } => match item {
-                copilot::copilot_responses::ResponseOutputItem::Message { id, .. } => {
+            copilot_responses::StreamEvent::OutputItemAdded { item, .. } => match item {
+                copilot_responses::ResponseOutputItem::Message { id, .. } => {
                     vec![Ok(LanguageModelCompletionEvent::StartMessage {
                         message_id: id,
                     })]
@@ -588,7 +589,7 @@ impl CopilotResponsesEventMapper {
                 _ => Vec::new(),
             },
 
-            copilot::copilot_responses::StreamEvent::OutputTextDelta { delta, .. } => {
+            copilot_responses::StreamEvent::OutputTextDelta { delta, .. } => {
                 if delta.is_empty() {
                     Vec::new()
                 } else {
@@ -596,9 +597,9 @@ impl CopilotResponsesEventMapper {
                 }
             }
 
-            copilot::copilot_responses::StreamEvent::OutputItemDone { item, .. } => match item {
-                copilot::copilot_responses::ResponseOutputItem::Message { .. } => Vec::new(),
-                copilot::copilot_responses::ResponseOutputItem::FunctionCall {
+            copilot_responses::StreamEvent::OutputItemDone { item, .. } => match item {
+                copilot_responses::ResponseOutputItem::Message { .. } => Vec::new(),
+                copilot_responses::ResponseOutputItem::FunctionCall {
                     call_id,
                     name,
                     arguments,
@@ -632,7 +633,7 @@ impl CopilotResponsesEventMapper {
                     events.push(Ok(LanguageModelCompletionEvent::Stop(StopReason::ToolUse)));
                     events
                 }
-                copilot::copilot_responses::ResponseOutputItem::Reasoning {
+                copilot_responses::ResponseOutputItem::Reasoning {
                     summary,
                     encrypted_content,
                     ..
@@ -660,7 +661,7 @@ impl CopilotResponsesEventMapper {
                 }
             },
 
-            copilot::copilot_responses::StreamEvent::Completed { response } => {
+            copilot_responses::StreamEvent::Completed { response } => {
                 let mut events = Vec::new();
                 if let Some(usage) = response.usage {
                     events.push(Ok(LanguageModelCompletionEvent::UsageUpdate(TokenUsage {
@@ -676,18 +677,16 @@ impl CopilotResponsesEventMapper {
                 events
             }
 
-            copilot::copilot_responses::StreamEvent::Incomplete { response } => {
+            copilot_responses::StreamEvent::Incomplete { response } => {
                 let reason = response
                     .incomplete_details
                     .as_ref()
                     .and_then(|details| details.reason.as_ref());
                 let stop_reason = match reason {
-                    Some(copilot::copilot_responses::IncompleteReason::MaxOutputTokens) => {
+                    Some(copilot_responses::IncompleteReason::MaxOutputTokens) => {
                         StopReason::MaxTokens
                     }
-                    Some(copilot::copilot_responses::IncompleteReason::ContentFilter) => {
-                        StopReason::Refusal
-                    }
+                    Some(copilot_responses::IncompleteReason::ContentFilter) => StopReason::Refusal,
                     _ => self
                         .pending_stop_reason
                         .take()
@@ -707,7 +706,7 @@ impl CopilotResponsesEventMapper {
                 events
             }
 
-            copilot::copilot_responses::StreamEvent::Failed { response } => {
+            copilot_responses::StreamEvent::Failed { response } => {
                 let provider = PROVIDER_NAME;
                 let (status_code, message) = match response.error {
                     Some(error) => {
@@ -727,18 +726,18 @@ impl CopilotResponsesEventMapper {
                 })]
             }
 
-            copilot::copilot_responses::StreamEvent::GenericError { error } => vec![Err(
+            copilot_responses::StreamEvent::GenericError { error } => vec![Err(
                 LanguageModelCompletionError::Other(anyhow!(format!("{error:?}"))),
             )],
 
-            copilot::copilot_responses::StreamEvent::Created { .. }
-            | copilot::copilot_responses::StreamEvent::Unknown => Vec::new(),
+            copilot_responses::StreamEvent::Created { .. }
+            | copilot_responses::StreamEvent::Unknown => Vec::new(),
         }
     }
 }
 
 fn into_copilot_chat(
-    model: &copilot::copilot_chat::Model,
+    model: &CopilotChatModel,
     request: LanguageModelRequest,
 ) -> Result<CopilotChatRequest> {
     let mut request_messages: Vec<LanguageModelRequestMessage> = Vec::new();
@@ -825,8 +824,8 @@ fn into_copilot_chat(
                     if let MessageContent::ToolUse(tool_use) = content {
                         tool_calls.push(ToolCall {
                             id: tool_use.id.to_string(),
-                            content: copilot::copilot_chat::ToolCallContent::Function {
-                                function: copilot::copilot_chat::FunctionContent {
+                            content: ToolCallContent::Function {
+                                function: FunctionContent {
                                     name: tool_use.name.to_string(),
                                     arguments: serde_json::to_string(&tool_use.input)?,
                                     thought_signature: tool_use.thought_signature.clone(),
@@ -890,7 +889,7 @@ fn into_copilot_chat(
         .tools
         .iter()
         .map(|tool| Tool::Function {
-            function: copilot::copilot_chat::Function {
+            function: Function {
                 name: tool.name.clone(),
                 description: tool.description.clone(),
                 parameters: tool.input_schema.clone(),
@@ -907,18 +906,18 @@ fn into_copilot_chat(
         messages,
         tools,
         tool_choice: request.tool_choice.map(|choice| match choice {
-            LanguageModelToolChoice::Auto => copilot::copilot_chat::ToolChoice::Auto,
-            LanguageModelToolChoice::Any => copilot::copilot_chat::ToolChoice::Any,
-            LanguageModelToolChoice::None => copilot::copilot_chat::ToolChoice::None,
+            LanguageModelToolChoice::Auto => ToolChoice::Auto,
+            LanguageModelToolChoice::Any => ToolChoice::Any,
+            LanguageModelToolChoice::None => ToolChoice::None,
         }),
     })
 }
 
 fn into_copilot_responses(
-    model: &copilot::copilot_chat::Model,
+    model: &CopilotChatModel,
     request: LanguageModelRequest,
-) -> copilot::copilot_responses::Request {
-    use copilot::copilot_responses as responses;
+) -> copilot_responses::Request {
+    use copilot_responses as responses;
 
     let LanguageModelRequest {
         thread_id: _,
@@ -1109,7 +1108,7 @@ fn into_copilot_responses(
         tool_choice: mapped_tool_choice,
         reasoning: None, // We would need to add support for setting from user settings.
         include: Some(vec![
-            copilot::copilot_responses::ResponseIncludable::ReasoningEncryptedContent,
+            copilot_responses::ResponseIncludable::ReasoningEncryptedContent,
         ]),
     }
 }
@@ -1117,7 +1116,7 @@ fn into_copilot_responses(
 #[cfg(test)]
 mod tests {
     use super::*;
-    use copilot::copilot_responses as responses;
+    use copilot_chat::responses;
     use futures::StreamExt;
 
     fn map_events(events: Vec<responses::StreamEvent>) -> Vec<LanguageModelCompletionEvent> {
@@ -1384,20 +1383,22 @@ mod tests {
 
     #[test]
     fn chat_completions_stream_maps_reasoning_data() {
-        use copilot::copilot_chat::ResponseEvent;
+        use copilot_chat::{
+            FunctionChunk, ResponseChoice, ResponseDelta, ResponseEvent, Role, ToolCallChunk,
+        };
 
         let events = vec![
             ResponseEvent {
-                choices: vec![copilot::copilot_chat::ResponseChoice {
+                choices: vec![ResponseChoice {
                     index: Some(0),
                     finish_reason: None,
-                    delta: Some(copilot::copilot_chat::ResponseDelta {
+                    delta: Some(ResponseDelta {
                         content: None,
-                        role: Some(copilot::copilot_chat::Role::Assistant),
-                        tool_calls: vec![copilot::copilot_chat::ToolCallChunk {
+                        role: Some(Role::Assistant),
+                        tool_calls: vec![ToolCallChunk {
                             index: Some(0),
                             id: Some("call_abc123".to_string()),
-                            function: Some(copilot::copilot_chat::FunctionChunk {
+                            function: Some(FunctionChunk {
                                 name: Some("list_directory".to_string()),
                                 arguments: Some("{\"path\":\"test\"}".to_string()),
                                 thought_signature: None,
@@ -1412,10 +1413,10 @@ mod tests {
                 usage: None,
             },
             ResponseEvent {
-                choices: vec![copilot::copilot_chat::ResponseChoice {
+                choices: vec![ResponseChoice {
                     index: Some(0),
                     finish_reason: Some("tool_calls".to_string()),
-                    delta: Some(copilot::copilot_chat::ResponseDelta {
+                    delta: Some(ResponseDelta {
                         content: None,
                         role: None,
                         tool_calls: vec![],

crates/language_tools/Cargo.toml 🔗

@@ -17,8 +17,8 @@ anyhow.workspace = true
 client.workspace = true
 collections.workspace = true
 command_palette_hooks.workspace = true
-copilot.workspace = true
 editor.workspace = true
+edit_prediction.workspace = true
 futures.workspace = true
 gpui.workspace = true
 itertools.workspace = true

crates/language_tools/src/lsp_log_view.rs 🔗

@@ -1,5 +1,5 @@
 use collections::VecDeque;
-use copilot::Copilot;
+use edit_prediction::EditPredictionStore;
 use editor::{Editor, EditorEvent, MultiBufferOffset, actions::MoveToEnd, scroll::Autoscroll};
 use gpui::{
     App, Context, Corner, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement,
@@ -115,46 +115,6 @@ actions!(
 pub fn init(on_headless_host: bool, cx: &mut App) {
     let log_store = log_store::init(on_headless_host, cx);
 
-    log_store.update(cx, |_, cx| {
-        Copilot::global(cx).map(|copilot| {
-            let copilot = &copilot;
-            cx.subscribe(copilot, |log_store, copilot, edit_prediction_event, cx| {
-                if let copilot::Event::CopilotLanguageServerStarted = edit_prediction_event
-                    && let Some(server) = copilot.read(cx).language_server()
-                {
-                    let server_id = server.server_id();
-                    let weak_lsp_store = cx.weak_entity();
-                    log_store.copilot_log_subscription =
-                        Some(server.on_notification::<lsp::notification::LogMessage, _>(
-                            move |params, cx| {
-                                weak_lsp_store
-                                    .update(cx, |lsp_store, cx| {
-                                        lsp_store.add_language_server_log(
-                                            server_id,
-                                            MessageType::LOG,
-                                            &params.message,
-                                            cx,
-                                        );
-                                    })
-                                    .ok();
-                            },
-                        ));
-
-                    let name = LanguageServerName::new_static("copilot");
-                    log_store.add_language_server(
-                        LanguageServerKind::Global,
-                        server.server_id(),
-                        Some(name),
-                        None,
-                        Some(server.clone()),
-                        cx,
-                    );
-                }
-            })
-            .detach();
-        })
-    });
-
     cx.observe_new(move |workspace: &mut Workspace, _, cx| {
         log_store.update(cx, |store, cx| {
             store.add_project(workspace.project(), cx);
@@ -381,8 +341,47 @@ impl LspLogView {
         );
         (editor, vec![editor_subscription, search_subscription])
     }
+    pub(crate) fn try_ensure_copilot_for_project(&self, cx: &mut App) {
+        self.log_store.update(cx, |this, cx| {
+            let copilot = EditPredictionStore::try_global(cx)
+                .and_then(|store| store.read(cx).copilot_for_project(&self.project))?;
+            let server = copilot.read(cx).language_server()?.clone();
+            let log_subscription = this.copilot_state_for_project(&self.project.downgrade());
+            if let Some(subscription_slot @ None) = log_subscription {
+                let weak_lsp_store = cx.weak_entity();
+                let server_id = server.server_id();
+
+                let name = LanguageServerName::new_static("copilot");
+                *subscription_slot =
+                    Some(server.on_notification::<lsp::notification::LogMessage, _>(
+                        move |params, cx| {
+                            weak_lsp_store
+                                .update(cx, |lsp_store, cx| {
+                                    lsp_store.add_language_server_log(
+                                        server_id,
+                                        MessageType::LOG,
+                                        &params.message,
+                                        cx,
+                                    );
+                                })
+                                .ok();
+                        },
+                    ));
+                this.add_language_server(
+                    LanguageServerKind::Global,
+                    server.server_id(),
+                    Some(name),
+                    None,
+                    Some(server.clone()),
+                    cx,
+                );
+            }
 
-    pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
+            Some(())
+        });
+    }
+    pub(crate) fn menu_items(&self, cx: &mut App) -> Option<Vec<LogMenuItem>> {
+        self.try_ensure_copilot_for_project(cx);
         let log_store = self.log_store.read(cx);
 
         let unknown_server = LanguageServerName::new_static("unknown server");

crates/project/src/lsp_store/log_store.rs 🔗

@@ -40,13 +40,13 @@ impl EventEmitter<Event> for LogStore {}
 pub struct LogStore {
     on_headless_host: bool,
     projects: HashMap<WeakEntity<Project>, ProjectState>,
-    pub copilot_log_subscription: Option<lsp::Subscription>,
     pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
     io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>,
 }
 
 struct ProjectState {
     _subscriptions: [Subscription; 2],
+    copilot_log_subscription: Option<lsp::Subscription>,
 }
 
 pub trait Message: AsRef<str> {
@@ -220,7 +220,7 @@ impl LogStore {
         let log_store = Self {
             projects: HashMap::default(),
             language_servers: HashMap::default(),
-            copilot_log_subscription: None,
+
             on_headless_host,
             io_tx,
         };
@@ -350,6 +350,7 @@ impl LogStore {
                         }
                     }),
                 ],
+                copilot_log_subscription: None,
             },
         );
     }
@@ -713,4 +714,12 @@ impl LogStore {
             }
         }
     }
+    pub fn copilot_state_for_project(
+        &mut self,
+        project: &WeakEntity<Project>,
+    ) -> Option<&mut Option<lsp::Subscription>> {
+        self.projects
+            .get_mut(project)
+            .map(|project| &mut project.copilot_log_subscription)
+    }
 }

crates/settings_ui/Cargo.toml 🔗

@@ -18,7 +18,7 @@ test-support = []
 [dependencies]
 anyhow.workspace = true
 bm25 = "2.3.2"
-copilot.workspace = true
+copilot_ui.workspace = true
 edit_prediction.workspace = true
 language_models.workspace = true
 editor.workspace = true

crates/settings_ui/src/pages/edit_prediction_provider_setup.rs 🔗

@@ -1,11 +1,12 @@
 use edit_prediction::{
-    ApiKeyState, MercuryFeatureFlag, SweepFeatureFlag,
+    ApiKeyState, EditPredictionStore, MercuryFeatureFlag, SweepFeatureFlag,
     mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
     sweep_ai::{SWEEP_CREDENTIALS_URL, sweep_api_token},
 };
 use feature_flags::FeatureFlagAppExt as _;
 use gpui::{Entity, ScrollHandle, prelude::*};
 use language_models::provider::mistral::{CODESTRAL_API_URL, codestral_api_key};
+use project::Project;
 use ui::{ButtonLink, ConfiguredApiCard, WithScrollbar, prelude::*};
 
 use crate::{
@@ -30,9 +31,19 @@ impl EditPredictionSetupPage {
 impl Render for EditPredictionSetupPage {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let settings_window = self.settings_window.clone();
-
+        let project = settings_window
+            .read(cx)
+            .original_window
+            .as_ref()
+            .and_then(|window| {
+                window
+                    .read_with(cx, |workspace, _| workspace.project().clone())
+                    .ok()
+            });
         let providers = [
-            Some(render_github_copilot_provider(window, cx).into_any_element()),
+            project.and_then(|project| {
+                Some(render_github_copilot_provider(project, window, cx)?.into_any_element())
+            }),
             cx.has_flag::<MercuryFeatureFlag>().then(|| {
                 render_api_key_provider(
                     IconName::Inception,
@@ -337,29 +348,36 @@ fn codestral_settings() -> Box<[SettingsPageItem]> {
     ])
 }
 
-pub(crate) fn render_github_copilot_provider(
+fn render_github_copilot_provider(
+    project: Entity<Project>,
     window: &mut Window,
     cx: &mut App,
-) -> impl IntoElement {
+) -> Option<impl IntoElement> {
+    let copilot = EditPredictionStore::try_global(cx)?
+        .read(cx)
+        .copilot_for_project(&project);
     let configuration_view = window.use_state(cx, |_, cx| {
-        copilot::ConfigurationView::new(
-            |cx| {
-                copilot::Copilot::global(cx)
+        copilot_ui::ConfigurationView::new(
+            move |cx| {
+                copilot
+                    .as_ref()
                     .is_some_and(|copilot| copilot.read(cx).is_authenticated())
             },
-            copilot::ConfigurationMode::EditPrediction,
+            copilot_ui::ConfigurationMode::EditPrediction,
             cx,
         )
     });
 
-    v_flex()
-        .id("github-copilot")
-        .min_w_0()
-        .gap_1p5()
-        .child(
-            SettingsSectionHeader::new("GitHub Copilot")
-                .icon(IconName::Copilot)
-                .no_padding(true),
-        )
-        .child(configuration_view)
+    Some(
+        v_flex()
+            .id("github-copilot")
+            .min_w_0()
+            .gap_1p5()
+            .child(
+                SettingsSectionHeader::new("GitHub Copilot")
+                    .icon(IconName::Copilot)
+                    .no_padding(true),
+            )
+            .child(configuration_view),
+    )
 }

crates/zed/Cargo.toml 🔗

@@ -87,6 +87,8 @@ command_palette.workspace = true
 component.workspace = true
 component_preview.workspace = true
 copilot.workspace = true
+copilot_chat.workspace = true
+copilot_ui.workspace = true
 crashes.workspace = true
 dap_adapters.workspace = true
 db.workspace = true

crates/zed/src/main.rs 🔗

@@ -590,14 +590,21 @@ fn main() {
             cx.background_executor().clone(),
         );
         command_palette::init(cx);
-        let copilot_language_server_id = app_state.languages.next_language_server_id();
-        copilot::init(
-            copilot_language_server_id,
+        let copilot_chat_configuration = copilot_chat::CopilotChatConfiguration {
+            enterprise_uri: language::language_settings::all_language_settings(None, cx)
+                .edit_predictions
+                .copilot
+                .enterprise_uri
+                .clone(),
+        };
+        copilot_chat::init(
             app_state.fs.clone(),
             app_state.client.http_client(),
-            app_state.node_runtime.clone(),
+            copilot_chat_configuration,
             cx,
         );
+
+        copilot_ui::init(cx);
         supermaven::init(app_state.client.clone(), cx);
         language_model::init(app_state.client.clone(), cx);
         language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx);

crates/zed/src/zed.rs 🔗

@@ -407,6 +407,7 @@ pub fn initialize_workspace(
                 app_state.user_store.clone(),
                 edit_prediction_menu_handle.clone(),
                 app_state.client.clone(),
+                workspace.project().clone(),
                 cx,
             )
         });
@@ -4922,10 +4923,10 @@ mod tests {
             project_panel::init(cx);
             outline_panel::init(cx);
             terminal_view::init(cx);
-            copilot::copilot_chat::init(
+            copilot_chat::init(
                 app_state.fs.clone(),
                 app_state.client.http_client(),
-                copilot::copilot_chat::CopilotChatConfiguration::default(),
+                copilot_chat::CopilotChatConfiguration::default(),
                 cx,
             );
             image_viewer::init(cx);

crates/zed/src/zed/edit_prediction_registry.rs 🔗

@@ -1,7 +1,7 @@
 use client::{Client, UserStore};
 use codestral::CodestralEditPredictionDelegate;
 use collections::HashMap;
-use copilot::{Copilot, CopilotEditPredictionDelegate};
+use copilot::CopilotEditPredictionDelegate;
 use edit_prediction::{
     MercuryFeatureFlag, SweepFeatureFlag, ZedEditPredictionDelegate, Zeta2FeatureFlag,
 };
@@ -165,7 +165,14 @@ fn assign_edit_prediction_provider(
             editor.set_edit_prediction_provider::<ZedEditPredictionDelegate>(None, window, cx);
         }
         EditPredictionProvider::Copilot => {
-            if let Some(copilot) = Copilot::global(cx) {
+            let ep_store = edit_prediction::EditPredictionStore::global(client, &user_store, cx);
+            let Some(project) = editor.project().cloned() else {
+                return;
+            };
+            let copilot =
+                ep_store.update(cx, |this, cx| this.start_copilot_for_project(&project, cx));
+
+            if let Some(copilot) = copilot {
                 if let Some(buffer) = singleton_buffer
                     && buffer.read(cx).file().is_some()
                 {