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

- **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.
This commit is contained in:
Piotr Osiewicz 2026-01-14 15:44:13 +01:00 committed by GitHub
parent cd12d45e4a
commit ca23fa7c7c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 877 additions and 373 deletions

54
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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

View file

@ -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) })
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();
}
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)
}
}
})?;
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,
}),
})
.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);
}
})
.collect();
anyhow::Ok(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,

View file

@ -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,

View file

@ -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";
}

View file

@ -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

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -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";

View file

@ -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};

View file

@ -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

View file

@ -0,0 +1 @@
../../LICENSE-GPL

View file

@ -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();
}

View file

@ -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")

View file

@ -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

View file

@ -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,
})
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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![],

View file

@ -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

View file

@ -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();
pub(crate) fn menu_items<'a>(&'a self, cx: &'a App) -> Option<Vec<LogMenuItem>> {
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,
);
}
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");

View file

@ -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)
}
}

View file

@ -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

View file

@ -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),
)
}

View file

@ -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

View file

@ -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);

View file

@ -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);

View file

@ -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()
{