logiguard fork: GPUI xdg-activation keyboard-focus serial fix
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Some checks failed
Update All Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Triage Project Sync (#84) / Sync triage project (push) Has been cancelled
release_nightly / notify_on_failure (push) Has been cancelled
release_nightly / check_style (push) Has been cancelled
release_nightly / run_tests_windows (push) Has been cancelled
release_nightly / clippy_windows (push) Has been cancelled
release_nightly / bundle_linux_aarch64 (push) Has been cancelled
release_nightly / bundle_linux_x86_64 (push) Has been cancelled
release_nightly / bundle_mac_aarch64 (push) Has been cancelled
release_nightly / bundle_mac_x86_64 (push) Has been cancelled
release_nightly / bundle_windows_aarch64 (push) Has been cancelled
release_nightly / bundle_windows_x86_64 (push) Has been cancelled
release_nightly / build_nix_linux_x86_64 (push) Has been cancelled
release_nightly / build_nix_mac_aarch64 (push) Has been cancelled
release_nightly / update_nightly_tag (push) Has been cancelled
Hotfix Review Monitor / check-hotfix-reviews (push) Has been cancelled
Stale PR Review Reminder / check-stale-prs (push) Has been cancelled
Update Weekly Top Ranking Issues / update_top_ranking_issues (push) Has been cancelled
Bump collab-staging Tag / update-collab-staging-tag (push) Has been cancelled
compliance_check / scheduled_compliance_check (push) Has been cancelled
Single-commit orphan branch: full zed-industries/zed @ 8c74db0 source tree with a 3-file patch applied (no upstream history). Patch (crates/gpui_linux/src/linux/wayland/): - serial.rs: add SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; add latest_serial_of() - window.rs: activate() uses keyboard-enter serial (Mutter focus gate) Mutter honors window activation only when the token carries the keyboard- focus serial from wl_keyboard.enter; GPUI used a stale mouse-press serial. See docs/tray-window-focus-wayland.md in logiguard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
368
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
368
crates/cloud_api_client/src/cloud_api_client.rs
Normal file
@@ -0,0 +1,368 @@
|
||||
mod llm_token;
|
||||
mod websocket;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use cloud_api_types::websocket_protocol::{PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER_NAME};
|
||||
pub use cloud_api_types::*;
|
||||
use futures::AsyncReadExt as _;
|
||||
use gpui::{App, Task};
|
||||
use gpui_tokio::Tokio;
|
||||
use http_client::http::request;
|
||||
use http_client::{
|
||||
AsyncBody, HttpClientWithUrl, HttpRequestExt, Json, Method, Request, StatusCode,
|
||||
};
|
||||
use parking_lot::RwLock;
|
||||
use thiserror::Error;
|
||||
use yawc::WebSocket;
|
||||
|
||||
use crate::websocket::Connection;
|
||||
|
||||
pub use llm_token::LlmApiToken;
|
||||
|
||||
struct Credentials {
|
||||
user_id: u32,
|
||||
access_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ClientApiError {
|
||||
/// 401 — credentials are invalid or expired.
|
||||
#[error("Unauthorized")]
|
||||
Unauthorized,
|
||||
/// No credentials have been set on the client.
|
||||
#[error("not signed in")]
|
||||
NotSignedIn,
|
||||
/// Connection-level failure: DNS, TCP, TLS, timeout, etc.
|
||||
/// The HTTP request never received a response.
|
||||
#[error("connection to {host} failed")]
|
||||
ConnectionFailed {
|
||||
host: String,
|
||||
#[source]
|
||||
source: anyhow::Error,
|
||||
},
|
||||
/// Server returned a non-success HTTP status (other than 401).
|
||||
#[error("{host} returned {status}")]
|
||||
ServerError {
|
||||
host: String,
|
||||
status: StatusCode,
|
||||
body: String,
|
||||
},
|
||||
/// Failed to read or parse the response body after a successful HTTP status.
|
||||
#[error("invalid response")]
|
||||
InvalidResponse(#[source] anyhow::Error),
|
||||
/// Failed to build the HTTP request (URL construction, serialization, etc.).
|
||||
/// This typically indicates a programming error.
|
||||
#[error("failed to build request")]
|
||||
RequestBuildFailed(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
pub struct CloudApiClient {
|
||||
credentials: RwLock<Option<Credentials>>,
|
||||
http_client: Arc<HttpClientWithUrl>,
|
||||
}
|
||||
|
||||
impl CloudApiClient {
|
||||
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||
Self {
|
||||
credentials: RwLock::new(None),
|
||||
http_client,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_credentials(&self) -> bool {
|
||||
self.credentials.read().is_some()
|
||||
}
|
||||
|
||||
pub fn set_credentials(&self, user_id: u32, access_token: String) {
|
||||
*self.credentials.write() = Some(Credentials {
|
||||
user_id,
|
||||
access_token,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn clear_credentials(&self) {
|
||||
*self.credentials.write() = None;
|
||||
}
|
||||
|
||||
fn cloud_host(&self) -> String {
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/")
|
||||
.ok()
|
||||
.and_then(|url| url.host_str().map(String::from))
|
||||
.unwrap_or_else(|| "cloud.zed.dev".into())
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
&self,
|
||||
req: request::Builder,
|
||||
body: impl Into<AsyncBody>,
|
||||
) -> Result<Request<AsyncBody>, ClientApiError> {
|
||||
let credentials = self.credentials.read();
|
||||
let credentials = credentials.as_ref().ok_or(ClientApiError::NotSignedIn)?;
|
||||
build_request(req, body, credentials).map_err(ClientApiError::RequestBuildFailed)
|
||||
}
|
||||
|
||||
pub async fn get_authenticated_user(
|
||||
&self,
|
||||
system_id: Option<String>,
|
||||
) -> Result<GetAuthenticatedUserResponse, ClientApiError> {
|
||||
let host = self.cloud_host();
|
||||
let request_builder = Request::builder()
|
||||
.method(Method::GET)
|
||||
.uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/users/me")
|
||||
.map_err(ClientApiError::RequestBuildFailed)?
|
||||
.as_ref(),
|
||||
)
|
||||
.when_some(system_id, |builder, system_id| {
|
||||
builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id)
|
||||
});
|
||||
|
||||
let request = self.build_request(request_builder, AsyncBody::default())?;
|
||||
|
||||
let mut response = self.http_client.send(request).await.map_err(|source| {
|
||||
ClientApiError::ConnectionFailed {
|
||||
host: host.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
if response.status() == StatusCode::UNAUTHORIZED {
|
||||
return Err(ClientApiError::Unauthorized);
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await.ok();
|
||||
|
||||
return Err(ClientApiError::ServerError {
|
||||
host,
|
||||
status: response.status(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(|e| ClientApiError::InvalidResponse(e.into()))?;
|
||||
|
||||
serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into()))
|
||||
}
|
||||
|
||||
pub fn connect(&self, cx: &App) -> Result<Task<Result<Connection>>> {
|
||||
let mut connect_url = self
|
||||
.http_client
|
||||
.build_zed_cloud_url("/client/users/connect")?;
|
||||
connect_url
|
||||
.set_scheme(match connect_url.scheme() {
|
||||
"https" => "wss",
|
||||
"http" => "ws",
|
||||
scheme => Err(anyhow!("invalid URL scheme: {scheme}"))?,
|
||||
})
|
||||
.map_err(|_| anyhow!("failed to set URL scheme"))?;
|
||||
|
||||
let credentials = self.credentials.read();
|
||||
let credentials = credentials.as_ref().context("no credentials provided")?;
|
||||
let authorization_header = format!("{} {}", credentials.user_id, credentials.access_token);
|
||||
|
||||
Ok(Tokio::spawn_result(cx, async move {
|
||||
let ws = WebSocket::connect(connect_url)
|
||||
.with_request(
|
||||
request::Builder::new()
|
||||
.header("Authorization", authorization_header)
|
||||
.header(PROTOCOL_VERSION_HEADER_NAME, PROTOCOL_VERSION.to_string()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Connection::new(ws))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn create_llm_token(
|
||||
&self,
|
||||
system_id: Option<String>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
) -> Result<CreateLlmTokenResponse, ClientApiError> {
|
||||
let host = self.cloud_host();
|
||||
let request_builder = Request::builder()
|
||||
.method(Method::POST)
|
||||
.uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/llm_tokens")
|
||||
.map_err(ClientApiError::RequestBuildFailed)?
|
||||
.as_ref(),
|
||||
)
|
||||
.when_some(system_id, |builder, system_id| {
|
||||
builder.header(ZED_SYSTEM_ID_HEADER_NAME, system_id)
|
||||
});
|
||||
|
||||
let request = self.build_request(
|
||||
request_builder,
|
||||
Json(CreateLlmTokenBody { organization_id }),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await.map_err(|source| {
|
||||
ClientApiError::ConnectionFailed {
|
||||
host: host.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
if response.status() == StatusCode::UNAUTHORIZED {
|
||||
return Err(ClientApiError::Unauthorized);
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await.ok();
|
||||
|
||||
return Err(ClientApiError::ServerError {
|
||||
host,
|
||||
status: response.status(),
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_string(&mut body)
|
||||
.await
|
||||
.map_err(|e| ClientApiError::InvalidResponse(e.into()))?;
|
||||
|
||||
serde_json::from_str(&body).map_err(|e| ClientApiError::InvalidResponse(e.into()))
|
||||
}
|
||||
|
||||
pub async fn validate_credentials(&self, user_id: u32, access_token: &str) -> Result<bool> {
|
||||
let request = build_request(
|
||||
Request::builder().method(Method::GET).uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/users/me")?
|
||||
.as_ref(),
|
||||
),
|
||||
AsyncBody::default(),
|
||||
&Credentials {
|
||||
user_id,
|
||||
access_token: access_token.into(),
|
||||
},
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if response.status().is_success() {
|
||||
Ok(true)
|
||||
} else {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
if response.status() == StatusCode::UNAUTHORIZED {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Failed to get authenticated user.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn submit_agent_feedback(&self, body: SubmitAgentThreadFeedbackBody) -> Result<()> {
|
||||
let request = self.build_request(
|
||||
Request::builder().method(Method::POST).uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/feedback/agent_thread")?
|
||||
.as_ref(),
|
||||
),
|
||||
AsyncBody::from(serde_json::to_string(&body)?),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
anyhow::bail!(
|
||||
"Failed to submit agent feedback.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn submit_agent_feedback_comments(
|
||||
&self,
|
||||
body: SubmitAgentThreadFeedbackCommentsBody,
|
||||
) -> Result<()> {
|
||||
let request = self.build_request(
|
||||
Request::builder().method(Method::POST).uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/feedback/agent_thread_comments")?
|
||||
.as_ref(),
|
||||
),
|
||||
AsyncBody::from(serde_json::to_string(&body)?),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
anyhow::bail!(
|
||||
"Failed to submit agent feedback comments.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn submit_edit_prediction_feedback(
|
||||
&self,
|
||||
body: SubmitEditPredictionFeedbackBody,
|
||||
) -> Result<()> {
|
||||
let request = self.build_request(
|
||||
Request::builder().method(Method::POST).uri(
|
||||
self.http_client
|
||||
.build_zed_cloud_url("/client/feedback/edit_prediction")?
|
||||
.as_ref(),
|
||||
),
|
||||
AsyncBody::from(serde_json::to_string(&body)?),
|
||||
)?;
|
||||
|
||||
let mut response = self.http_client.send(request).await?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
let mut body = String::new();
|
||||
response.body_mut().read_to_string(&mut body).await?;
|
||||
|
||||
anyhow::bail!(
|
||||
"Failed to submit edit prediction feedback.\nStatus: {:?}\nBody: {body}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request(
|
||||
req: request::Builder,
|
||||
body: impl Into<AsyncBody>,
|
||||
credentials: &Credentials,
|
||||
) -> Result<Request<AsyncBody>> {
|
||||
Ok(req
|
||||
.header("Content-Type", "application/json")
|
||||
.header(
|
||||
"Authorization",
|
||||
format!("{} {}", credentials.user_id, credentials.access_token),
|
||||
)
|
||||
.body(body.into())?)
|
||||
}
|
||||
78
crates/cloud_api_client/src/llm_token.rs
Normal file
78
crates/cloud_api_client/src/llm_token.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_lock::{RwLock, RwLockUpgradableReadGuard, RwLockWriteGuard};
|
||||
use cloud_api_types::OrganizationId;
|
||||
|
||||
use crate::{ClientApiError, CloudApiClient};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct LlmApiToken(Arc<RwLock<Option<String>>>);
|
||||
|
||||
impl LlmApiToken {
|
||||
/// Returns the cached LLM token, fetching a fresh one only if none has
|
||||
/// been cached yet. The returned token is not validated; callers must
|
||||
/// be prepared to refresh it (via [`LlmApiToken::refresh`]) if the
|
||||
/// server rejects it.
|
||||
pub async fn cached(
|
||||
&self,
|
||||
client: &CloudApiClient,
|
||||
system_id: Option<String>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
) -> Result<String, ClientApiError> {
|
||||
let lock = self.0.upgradable_read().await;
|
||||
if let Some(token) = lock.as_ref() {
|
||||
Ok(token.to_string())
|
||||
} else {
|
||||
Self::fetch(
|
||||
RwLockUpgradableReadGuard::upgrade(lock).await,
|
||||
client,
|
||||
system_id,
|
||||
organization_id,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn refresh(
|
||||
&self,
|
||||
client: &CloudApiClient,
|
||||
system_id: Option<String>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
) -> Result<String, ClientApiError> {
|
||||
Self::fetch(self.0.write().await, client, system_id, organization_id).await
|
||||
}
|
||||
|
||||
/// Clears the existing token before attempting to fetch a new one.
|
||||
///
|
||||
/// Used when switching organizations so that a failed refresh doesn't
|
||||
/// leave a token for the wrong organization.
|
||||
pub async fn clear_and_refresh(
|
||||
&self,
|
||||
client: &CloudApiClient,
|
||||
system_id: Option<String>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
) -> Result<String, ClientApiError> {
|
||||
let mut lock = self.0.write().await;
|
||||
*lock = None;
|
||||
Self::fetch(lock, client, system_id, organization_id).await
|
||||
}
|
||||
|
||||
async fn fetch(
|
||||
mut lock: RwLockWriteGuard<'_, Option<String>>,
|
||||
client: &CloudApiClient,
|
||||
system_id: Option<String>,
|
||||
organization_id: Option<OrganizationId>,
|
||||
) -> Result<String, ClientApiError> {
|
||||
let result = client.create_llm_token(system_id, organization_id).await;
|
||||
match result {
|
||||
Ok(response) => {
|
||||
*lock = Some(response.token.0.clone());
|
||||
Ok(response.token.0)
|
||||
}
|
||||
Err(err) => {
|
||||
*lock = None;
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
crates/cloud_api_client/src/websocket.rs
Normal file
73
crates/cloud_api_client/src/websocket.rs
Normal file
@@ -0,0 +1,73 @@
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use cloud_api_types::websocket_protocol::MessageToClient;
|
||||
use futures::channel::mpsc::unbounded;
|
||||
use futures::stream::{SplitSink, SplitStream};
|
||||
use futures::{FutureExt as _, SinkExt as _, Stream, StreamExt as _, TryStreamExt as _, pin_mut};
|
||||
use gpui::{App, BackgroundExecutor, Task};
|
||||
use yawc::WebSocket;
|
||||
use yawc::frame::{FrameView, OpCode};
|
||||
|
||||
const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1);
|
||||
|
||||
pub type MessageStream = Pin<Box<dyn Stream<Item = Result<MessageToClient>>>>;
|
||||
|
||||
pub struct Connection {
|
||||
tx: SplitSink<WebSocket, FrameView>,
|
||||
rx: SplitStream<WebSocket>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn new(ws: WebSocket) -> Self {
|
||||
let (tx, rx) = ws.split();
|
||||
|
||||
Self { tx, rx }
|
||||
}
|
||||
|
||||
pub fn spawn(self, cx: &App) -> (MessageStream, Task<()>) {
|
||||
let (mut tx, rx) = (self.tx, self.rx);
|
||||
|
||||
let (message_tx, message_rx) = unbounded();
|
||||
|
||||
let handle_io = |executor: BackgroundExecutor| async move {
|
||||
// Send messages on this frequency so the connection isn't closed.
|
||||
let keepalive_timer = executor.timer(KEEPALIVE_INTERVAL).fuse();
|
||||
futures::pin_mut!(keepalive_timer);
|
||||
|
||||
let rx = rx.fuse();
|
||||
pin_mut!(rx);
|
||||
|
||||
loop {
|
||||
futures::select_biased! {
|
||||
_ = keepalive_timer => {
|
||||
let _ = tx.send(FrameView::ping(Vec::new())).await;
|
||||
|
||||
keepalive_timer.set(executor.timer(KEEPALIVE_INTERVAL).fuse());
|
||||
}
|
||||
frame = rx.next() => {
|
||||
let Some(frame) = frame else {
|
||||
break;
|
||||
};
|
||||
|
||||
match frame.opcode {
|
||||
OpCode::Binary => {
|
||||
let message_result = MessageToClient::deserialize(&frame.payload);
|
||||
message_tx.unbounded_send(message_result).ok();
|
||||
}
|
||||
OpCode::Close => {
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let task = cx.spawn(async move |cx| handle_io(cx.background_executor().clone()).await);
|
||||
|
||||
(message_rx.into_stream().boxed(), task)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user