logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,25 @@
[package]
name = "cloud_api_client"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/cloud_api_client.rs"
[dependencies]
anyhow.workspace = true
cloud_api_types.workspace = true
futures.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
http_client.workspace = true
parking_lot.workspace = true
serde_json.workspace = true
async-lock.workspace = true
thiserror.workspace = true
yawc.workspace = true

View File

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

View 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())?)
}

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

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