diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 1b92b5afd0..5c82ba04f1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3645,6 +3645,7 @@ dependencies = [ "codex-app-server-client", "codex-app-server-protocol", "codex-arg0", + "codex-backend-client", "codex-chatgpt", "codex-cli", "codex-cloud-requirements", diff --git a/codex-rs/backend-client/src/types.rs b/codex-rs/backend-client/src/types.rs index ca081d57bc..c31f428d08 100644 --- a/codex-rs/backend-client/src/types.rs +++ b/codex-rs/backend-client/src/types.rs @@ -49,6 +49,43 @@ impl CodexWorkspaceMessagesResponse { } } +#[cfg(test)] +mod workspace_message_tests { + use super::*; + use pretty_assertions::assert_eq; + + #[test] + fn workspace_messages_response_deserializes_headlines_and_announcements() { + let response: CodexWorkspaceMessagesResponse = serde_json::from_value(serde_json::json!({ + "messages": [ + { + "message_id": "headline-id", + "message_type": "headline", + "message_body": "Headline body" + }, + { + "message_id": "announcement-id", + "message_type": "announcement", + "message_body": "Announcement body" + } + ] + })) + .expect("workspace messages response should deserialize"); + + let headlines = response + .headlines() + .map(|message| message.message_id.as_str()) + .collect::>(); + let announcements = response + .announcements() + .map(|message| message.message_id.as_str()) + .collect::>(); + + assert_eq!(headlines, vec!["headline-id"]); + assert_eq!(announcements, vec!["announcement-id"]); + } +} + /// 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. diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index c5538c02ed..2532b12d4b 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -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 } diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index b2e92a19b4..3f4aec7c0c 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -42,6 +42,7 @@ use codex_config::format_config_error_with_source; use codex_exec_server::EnvironmentManager; use codex_exec_server::EnvironmentManagerArgs; use codex_exec_server::ExecServerRuntimePaths; +use codex_features::Feature; use codex_login::AuthConfig; use codex_login::default_client::set_default_client_residency_requirement; use codex_login::enforce_login_restrictions; @@ -190,6 +191,7 @@ mod version; mod voice; mod width; mod workspace_command; +mod workspace_messages; #[cfg(target_os = "linux")] #[allow(dead_code)] mod voice { @@ -1106,6 +1108,9 @@ async fn run_ratatui_app( color_eyre::install()?; tooltips::announcement::prewarm(); + if initial_config.features.enabled(Feature::WorkspaceMessages) { + workspace_messages::prewarm_workspace_messages(&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. diff --git a/codex-rs/tui/src/workspace_messages.rs b/codex-rs/tui/src/workspace_messages.rs new file mode 100644 index 0000000000..0d4defb26f --- /dev/null +++ b/codex-rs/tui/src/workspace_messages.rs @@ -0,0 +1,42 @@ +use crate::legacy_core::config::Config; +use codex_backend_client::Client as BackendClient; +use codex_backend_client::CodexWorkspaceMessagesResponse; +use codex_login::AuthManager; +use std::sync::OnceLock; +use std::time::Duration; +use tokio::time::timeout; + +const WORKSPACE_MESSAGES_FETCH_TIMEOUT: Duration = Duration::from_millis(1000); + +static WORKSPACE_MESSAGES: OnceLock> = OnceLock::new(); +static WORKSPACE_MESSAGES_FETCH_STARTED: OnceLock<()> = OnceLock::new(); + +pub(crate) fn prewarm_workspace_messages(config: &Config) { + if WORKSPACE_MESSAGES_FETCH_STARTED.set(()).is_err() { + return; + } + + let config = config.clone(); + tokio::spawn(async move { + let messages = timeout( + WORKSPACE_MESSAGES_FETCH_TIMEOUT, + fetch_workspace_messages(config), + ) + .await + .ok() + .flatten(); + let _ = WORKSPACE_MESSAGES.set(messages); + }); +} + +async fn fetch_workspace_messages(config: Config) -> Option { + 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()?; + client.list_workspace_messages().await.ok() +}