mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
## What changed - Load the `/experimental` catalog from the running server for the active thread, with loading, empty, and failure states in the popup. - Show beta features in server order and preserve server-reported names, descriptions, and configured values. Mark features as read-only when the local config writer cannot safely update them. - Persist only changed, writable feature flags. Bound discovery with page-size and page-count limits, repeated-cursor detection, deduplication, cancellation, and a timeout. ## Testing - Cover paginated discovery, malformed or unresponsive servers, popup cancellation, read-only controls, selective writes, and the new popup states. GitOrigin-RevId: 1f49eb961c8ab220a466e15f032c1dc61ed86d38
75 lines
3.2 KiB
Rust
75 lines
3.2 KiB
Rust
//! Bounded, popup-scoped experimental-feature discovery through the existing RPC.
|
|
//! Dropping a popup stops pagination. One reserved request ID bounds abandoned
|
|
//! requests until the server replies or the connection closes.
|
|
|
|
use codex_app_server_client::AppServerRequestHandle;
|
|
use codex_app_server_protocol::ClientRequest;
|
|
use codex_app_server_protocol::ExperimentalFeature;
|
|
use codex_app_server_protocol::ExperimentalFeatureListParams;
|
|
use codex_app_server_protocol::ExperimentalFeatureListResponse;
|
|
use codex_app_server_protocol::RequestId;
|
|
use codex_protocol::ThreadId;
|
|
use std::collections::HashSet;
|
|
use std::time::Duration;
|
|
use tokio::sync::oneshot;
|
|
|
|
pub(crate) fn fetch(
|
|
request_handle: AppServerRequestHandle,
|
|
thread_id: ThreadId,
|
|
mut response_tx: oneshot::Sender<Result<Vec<ExperimentalFeature>, String>>,
|
|
) {
|
|
tokio::spawn(async move {
|
|
let discovery = async {
|
|
let mut features = Vec::new();
|
|
let mut cursor = None;
|
|
let mut cursors = HashSet::new();
|
|
let mut names = HashSet::new();
|
|
for _ in 0..10 {
|
|
let response = request_handle
|
|
.request_typed::<ExperimentalFeatureListResponse>(
|
|
ClientRequest::ExperimentalFeatureList {
|
|
// The client rejects duplicate pending IDs, bounding unanswered
|
|
// requests across popup cancellation and timeout/retry cycles.
|
|
request_id: RequestId::String("tui-experimental-features".to_string()),
|
|
params: ExperimentalFeatureListParams {
|
|
cursor,
|
|
limit: Some(100),
|
|
thread_id: Some(thread_id.to_string()),
|
|
},
|
|
},
|
|
)
|
|
.await
|
|
.map_err(|_| "Experimental feature request failed".to_string())?;
|
|
if response.data.len() > 100 {
|
|
return Err("Experimental feature page exceeds requested limit".to_string());
|
|
}
|
|
features.extend(
|
|
response
|
|
.data
|
|
.into_iter()
|
|
.filter(|feature| names.insert(feature.name.clone())),
|
|
);
|
|
cursor = response.next_cursor;
|
|
let Some(next) = cursor.as_ref() else {
|
|
return Ok(features);
|
|
};
|
|
if !cursors.insert(next.clone()) {
|
|
return Err("Experimental feature pagination repeated a cursor".to_string());
|
|
}
|
|
}
|
|
Err("Experimental feature discovery exceeded 10 pages".to_string())
|
|
};
|
|
tokio::select! {
|
|
_ = response_tx.closed() => {},
|
|
result = tokio::time::timeout(Duration::from_secs(/*secs*/ 5), discovery) => {
|
|
let result = result.unwrap_or_else(|_| Err("Experimental feature discovery timed out".to_string()));
|
|
let _ = response_tx.send(result);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "experimental_features_tests.rs"]
|
|
mod tests;
|