mirror of
https://github.com/openai/codex.git
synced 2026-05-24 21:14:51 +00:00
Add workspace headline polling client
This commit is contained in:
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
@@ -2104,6 +2104,7 @@ name = "codex-backend-client"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"codex-api",
|
||||
"codex-backend-openapi-models",
|
||||
"codex-client",
|
||||
@@ -3634,6 +3635,7 @@ dependencies = [
|
||||
"codex-app-server-client",
|
||||
"codex-app-server-protocol",
|
||||
"codex-arg0",
|
||||
"codex-backend-client",
|
||||
"codex-chatgpt",
|
||||
"codex-cli",
|
||||
"codex-cloud-requirements",
|
||||
|
||||
@@ -13,6 +13,7 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::types::CodeTaskDetailsResponse;
|
||||
use crate::types::CodexWorkspaceMessagesResponse;
|
||||
use crate::types::ConfigFileResponse;
|
||||
use crate::types::PaginatedListTaskListItem;
|
||||
use crate::types::RateLimitReachedKind as BackendRateLimitReachedKind;
|
||||
@@ -408,6 +409,16 @@ impl Client {
|
||||
.map_err(RequestError::from)
|
||||
}
|
||||
|
||||
pub async fn list_workspace_messages(
|
||||
&self,
|
||||
) -> std::result::Result<CodexWorkspaceMessagesResponse, RequestError> {
|
||||
let url = self.workspace_messages_url();
|
||||
let req = self.http.get(&url).headers(self.headers());
|
||||
let (body, ct) = self.exec_request_detailed(req, "GET", &url).await?;
|
||||
self.decode_json::<CodexWorkspaceMessagesResponse>(&url, &ct, &body)
|
||||
.map_err(RequestError::from)
|
||||
}
|
||||
|
||||
/// Create a new task (user turn) by POSTing to the appropriate backend path
|
||||
/// based on `path_style`. Returns the created task id.
|
||||
pub async fn create_task(&self, request_body: serde_json::Value) -> Result<String> {
|
||||
@@ -539,6 +550,13 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_messages_url(&self) -> String {
|
||||
match self.path_style {
|
||||
PathStyle::CodexApi => format!("{}/api/codex/workspace-messages", self.base_url),
|
||||
PathStyle::ChatGptApi => format!("{}/wham/workspace-messages", self.base_url),
|
||||
}
|
||||
}
|
||||
|
||||
fn map_rate_limit_window(
|
||||
window: Option<Option<Box<crate::types::RateLimitWindowSnapshot>>>,
|
||||
) -> Option<RateLimitWindow> {
|
||||
@@ -862,4 +880,35 @@ mod tests {
|
||||
serde_json::json!({ "credit_type": "usage_limit" })
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_messages_uses_expected_paths() {
|
||||
let codex_client = Client {
|
||||
base_url: "https://example.test".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
path_style: PathStyle::CodexApi,
|
||||
};
|
||||
assert_eq!(
|
||||
codex_client.workspace_messages_url(),
|
||||
"https://example.test/api/codex/workspace-messages"
|
||||
);
|
||||
|
||||
let chatgpt_client = Client {
|
||||
base_url: "https://chatgpt.com/backend-api".to_string(),
|
||||
http: reqwest::Client::new(),
|
||||
auth_provider: codex_model_provider::unauthenticated_auth_provider(),
|
||||
user_agent: None,
|
||||
chatgpt_account_id: None,
|
||||
chatgpt_account_is_fedramp: false,
|
||||
path_style: PathStyle::ChatGptApi,
|
||||
};
|
||||
assert_eq!(
|
||||
chatgpt_client.workspace_messages_url(),
|
||||
"https://chatgpt.com/backend-api/wham/workspace-messages"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@ pub use client::Client;
|
||||
pub use client::RequestError;
|
||||
pub use types::CodeTaskDetailsResponse;
|
||||
pub use types::CodeTaskDetailsResponseExt;
|
||||
pub use types::CodexWorkspaceMessage;
|
||||
pub use types::CodexWorkspaceMessageType;
|
||||
pub use types::CodexWorkspaceMessagesResponse;
|
||||
pub use types::ConfigFileResponse;
|
||||
pub use types::PaginatedListTaskListItem;
|
||||
pub use types::TaskListItem;
|
||||
|
||||
@@ -8,11 +8,51 @@ pub use codex_backend_openapi_models::models::RateLimitStatusPayload;
|
||||
pub use codex_backend_openapi_models::models::RateLimitWindowSnapshot;
|
||||
pub use codex_backend_openapi_models::models::TaskListItem;
|
||||
|
||||
use chrono::DateTime;
|
||||
use chrono::Utc;
|
||||
use serde::Deserialize;
|
||||
use serde::de::Deserializer;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct CodexWorkspaceMessagesResponse {
|
||||
#[serde(default)]
|
||||
pub messages: Vec<CodexWorkspaceMessage>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq)]
|
||||
pub struct CodexWorkspaceMessage {
|
||||
pub message_id: String,
|
||||
pub message_type: CodexWorkspaceMessageType,
|
||||
pub message_body: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CodexWorkspaceMessageType {
|
||||
Headline,
|
||||
Announcement,
|
||||
#[serde(other)]
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CodexWorkspaceMessagesResponse {
|
||||
pub fn headlines(&self) -> impl Iterator<Item = &CodexWorkspaceMessage> {
|
||||
self.messages
|
||||
.iter()
|
||||
.filter(|message| message.message_type == CodexWorkspaceMessageType::Headline)
|
||||
}
|
||||
|
||||
pub fn announcements(&self) -> impl Iterator<Item = &CodexWorkspaceMessage> {
|
||||
self.messages
|
||||
.iter()
|
||||
.filter(|message| message.message_type == CodexWorkspaceMessageType::Announcement)
|
||||
}
|
||||
}
|
||||
|
||||
/// Hand-rolled models for the Cloud Tasks task-details response.
|
||||
/// The generated OpenAPI models are pretty bad. This is a half-step
|
||||
/// towards hand-rolling them.
|
||||
|
||||
@@ -29,6 +29,7 @@ codex-ansi-escape = { workspace = true }
|
||||
codex-app-server-client = { workspace = true }
|
||||
codex-app-server-protocol = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
codex-backend-client = { workspace = true }
|
||||
codex-install-context = { workspace = true }
|
||||
codex-chatgpt = { workspace = true }
|
||||
codex-cloud-requirements = { workspace = true }
|
||||
|
||||
@@ -190,6 +190,7 @@ mod version;
|
||||
mod voice;
|
||||
mod width;
|
||||
mod workspace_command;
|
||||
mod workspace_messages;
|
||||
#[cfg(target_os = "linux")]
|
||||
#[allow(dead_code)]
|
||||
mod voice {
|
||||
@@ -1104,6 +1105,7 @@ async fn run_ratatui_app(
|
||||
color_eyre::install()?;
|
||||
|
||||
tooltips::announcement::prewarm();
|
||||
workspace_messages::prewarm_headline(&initial_config);
|
||||
|
||||
// Forward panic reports through tracing so they appear in the UI status
|
||||
// line, but do not swallow the default/color-eyre panic handler.
|
||||
|
||||
39
codex-rs/tui/src/workspace_messages.rs
Normal file
39
codex-rs/tui/src/workspace_messages.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use crate::legacy_core::config::Config;
|
||||
use codex_backend_client::Client as BackendClient;
|
||||
use codex_backend_client::CodexWorkspaceMessage;
|
||||
use codex_login::AuthManager;
|
||||
use std::sync::OnceLock;
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
const HEADLINE_FETCH_TIMEOUT: Duration = Duration::from_millis(1000);
|
||||
|
||||
static WORKSPACE_HEADLINE: OnceLock<Option<CodexWorkspaceMessage>> = OnceLock::new();
|
||||
|
||||
pub(crate) fn prewarm_headline(config: &Config) {
|
||||
if WORKSPACE_HEADLINE.get().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let config = config.clone();
|
||||
tokio::spawn(async move {
|
||||
let headline = timeout(HEADLINE_FETCH_TIMEOUT, fetch_headline(config))
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
let _ = WORKSPACE_HEADLINE.set(headline);
|
||||
});
|
||||
}
|
||||
|
||||
async fn fetch_headline(config: Config) -> Option<CodexWorkspaceMessage> {
|
||||
let auth_manager =
|
||||
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await;
|
||||
let auth = auth_manager.auth().await?;
|
||||
if !auth.uses_codex_backend() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let client = BackendClient::from_auth(config.chatgpt_base_url, &auth).ok()?;
|
||||
let messages = client.list_workspace_messages().await.ok()?;
|
||||
messages.headlines().next().cloned()
|
||||
}
|
||||
Reference in New Issue
Block a user