copilot: Fix onboarding into Copilot requires Zed restart (#26330)

Closes #25594

This PR fixes an issue where signing into Copilot required restarting
Zed.

Copilot depends on an OAuth token that comes from either `hosts.json` or
`apps.json`. Initially, both files don't exist. If neither file is
found, we fallback to watching `hosts.json` for updates. However, if the
auth process creates `apps.json`, we won't receive updates from it,
causing the UI to remain outdated.

This PR fixes that by watching the parent `github-copilot` directory
instead, which will always contain one of those files along with an
additional version file.

I have tested this on macOS and Linux Wayland.

Release Notes:

- Fixed an issue where signing into Copilot required restarting Zed.
This commit is contained in:
Smit Barmase 2025-03-08 21:49:09 +00:00 committed by GitHub
parent 22d9b5d8ca
commit f14d6670ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 62 additions and 20 deletions

View file

@ -4,13 +4,14 @@ use std::sync::OnceLock;
use anyhow::{anyhow, Result};
use chrono::DateTime;
use collections::HashSet;
use fs::Fs;
use futures::{io::BufReader, stream::BoxStream, AsyncBufReadExt, AsyncReadExt, StreamExt};
use gpui::{prelude::*, App, AsyncApp, Global};
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
use paths::home_dir;
use serde::{Deserialize, Serialize};
use settings::watch_config_file;
use settings::watch_config_dir;
use strum::EnumIter;
pub const COPILOT_CHAT_COMPLETION_URL: &str = "https://api.githubcopilot.com/chat/completions";
@ -237,27 +238,18 @@ impl CopilotChat {
}
pub fn new(fs: Arc<dyn Fs>, client: Arc<dyn HttpClient>, cx: &App) -> Self {
let config_paths = copilot_chat_config_paths();
let resolve_config_path = {
let fs = fs.clone();
async move {
for config_path in config_paths.iter() {
if fs.metadata(config_path).await.is_ok_and(|v| v.is_some()) {
return config_path.clone();
}
}
config_paths[0].clone()
}
};
let config_paths: HashSet<PathBuf> = copilot_chat_config_paths().into_iter().collect();
let dir_path = copilot_chat_config_dir();
cx.spawn(|cx| async move {
let config_file = resolve_config_path.await;
let mut config_file_rx = watch_config_file(cx.background_executor(), fs, config_file);
while let Some(contents) = config_file_rx.next().await {
let mut parent_watch_rx = watch_config_dir(
cx.background_executor(),
fs.clone(),
dir_path.clone(),
config_paths,
);
while let Some(contents) = parent_watch_rx.next().await {
let oauth_token = extract_oauth_token(contents);
cx.update(|cx| {
if let Some(this) = Self::global(cx).as_ref() {
this.update(cx, |this, cx| {

View file

@ -1,5 +1,6 @@
use crate::{settings_store::SettingsStore, Settings};
use fs::Fs;
use collections::HashSet;
use fs::{Fs, PathEventKind};
use futures::{channel::mpsc, StreamExt};
use gpui::{App, BackgroundExecutor, ReadGlobal};
use std::{path::PathBuf, sync::Arc, time::Duration};
@ -78,6 +79,55 @@ pub fn watch_config_file(
rx
}
pub fn watch_config_dir(
executor: &BackgroundExecutor,
fs: Arc<dyn Fs>,
dir_path: PathBuf,
config_paths: HashSet<PathBuf>,
) -> mpsc::UnboundedReceiver<String> {
let (tx, rx) = mpsc::unbounded();
executor
.spawn(async move {
for file_path in &config_paths {
if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) {
if let Ok(contents) = fs.load(file_path).await {
if tx.unbounded_send(contents).is_err() {
return;
}
}
}
}
let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await;
futures::pin_mut!(events);
while let Some(event_batch) = events.next().await {
for event in event_batch {
if config_paths.contains(&event.path) {
match event.kind {
Some(PathEventKind::Removed) => {
if tx.unbounded_send(String::new()).is_err() {
return;
}
}
Some(PathEventKind::Created) | Some(PathEventKind::Changed) => {
if let Ok(contents) = fs.load(&event.path).await {
if tx.unbounded_send(contents).is_err() {
return;
}
}
}
_ => {}
}
}
}
}
})
.detach();
rx
}
pub fn update_settings_file<T: Settings>(
fs: Arc<dyn Fs>,
cx: &App,