From 57c3632a9e79a6208ea82b0b5115a1cf35ccabfa Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 10:47:28 -0800 Subject: [PATCH 01/15] fetching rate limits when calling /status --- codex-rs/Cargo.lock | 1 + codex-rs/tui/Cargo.toml | 1 + codex-rs/tui/config.toml | 2 ++ codex-rs/tui/src/chatwidget.rs | 52 ++++++++++++++++++++++++++++++++-- 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index cca5ca0b43..2b0080e049 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1470,6 +1470,7 @@ dependencies = [ "codex-ansi-escape", "codex-app-server-protocol", "codex-arg0", + "codex-backend-client", "codex-common", "codex-core", "codex-feedback", diff --git a/codex-rs/tui/Cargo.toml b/codex-rs/tui/Cargo.toml index b524d8bfd4..b8b2ae2623 100644 --- a/codex-rs/tui/Cargo.toml +++ b/codex-rs/tui/Cargo.toml @@ -29,6 +29,7 @@ clap = { workspace = true, features = ["derive"] } codex-ansi-escape = { workspace = true } codex-app-server-protocol = { workspace = true } codex-arg0 = { workspace = true } +codex-backend-client = { workspace = true } codex-common = { workspace = true, features = [ "cli", "elapsed", diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml new file mode 100644 index 0000000000..ca4c341c5d --- /dev/null +++ b/codex-rs/tui/config.toml @@ -0,0 +1,2 @@ +[projects."/Users/zhao/code/codex"] +trust_level = "untrusted" diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 79781f3352..475e73898f 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2,7 +2,10 @@ use std::collections::HashMap; use std::collections::VecDeque; use std::path::PathBuf; use std::sync::Arc; +use std::time::Duration; +use codex_app_server_protocol::AuthMode; +use codex_backend_client::Client as BackendClient; use codex_core::config::Config; use codex_core::config::types::Notifications; use codex_core::git_info::current_branch_name; @@ -1727,16 +1730,61 @@ impl ChatWidget { } else { (&default_usage, Some(&default_usage)) }; + let rate_limit_snapshot = self + .fetch_rate_limits_from_usage() + .or_else(|| self.rate_limit_snapshot.clone()); + + if let Some(snapshot) = rate_limit_snapshot.clone() { + self.rate_limit_snapshot = Some(snapshot); + } + + let now = Local::now(); self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), total_usage, context_usage, &self.conversation_id, - self.rate_limit_snapshot.as_ref(), - Local::now(), + rate_limit_snapshot.as_ref(), + now, )); } + fn fetch_rate_limits_from_usage(&self) -> Option { + let auth = self.auth_manager.auth()?; + if auth.mode != AuthMode::ChatGPT { + return None; + } + + let base_url = self.config.chatgpt_base_url.clone(); + let auth = auth; + let result = tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + let fetch = async { + let client = BackendClient::from_auth(base_url, &auth).await?; + client.get_rate_limits().await + }; + tokio::time::timeout(Duration::from_secs(1), fetch).await + }) + }); + + match result { + Ok(Ok(snapshot)) => Some(crate::status::rate_limit_snapshot_display( + &snapshot, + Local::now(), + )), + Ok(Err(err)) => { + tracing::debug!( + error = ?err, + "failed to fetch rate limits from /usage within timeout" + ); + None + } + Err(_) => { + tracing::debug!("timed out fetching rate limits from /usage"); + None + } + } + } fn lower_cost_preset(&self) -> Option { let auth_mode = self.auth_manager.auth().map(|auth| auth.mode); From bfa6fbc7007349b79630c9c34da1ddd76c1c2d6d Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 10:56:14 -0800 Subject: [PATCH 02/15] unecessary diff --- codex-rs/tui/src/chatwidget.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 475e73898f..1c67d87b68 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1738,7 +1738,6 @@ impl ChatWidget { self.rate_limit_snapshot = Some(snapshot); } - let now = Local::now(); self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), @@ -1746,7 +1745,7 @@ impl ChatWidget { context_usage, &self.conversation_id, rate_limit_snapshot.as_ref(), - now, + Local::now(), )); } fn fetch_rate_limits_from_usage(&self) -> Option { From f8e3c57975a91da700a1fc19e4b0c789d30bffdd Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 11:01:18 -0800 Subject: [PATCH 03/15] remove unecessary config.toml + fmt --- codex-rs/tui/config.toml | 2 -- codex-rs/tui/src/chatwidget.rs | 11 ++++------- 2 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml deleted file mode 100644 index ca4c341c5d..0000000000 --- a/codex-rs/tui/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[projects."/Users/zhao/code/codex"] -trust_level = "untrusted" diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1c67d87b68..c2b04f020c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1730,13 +1730,10 @@ impl ChatWidget { } else { (&default_usage, Some(&default_usage)) }; - let rate_limit_snapshot = self - .fetch_rate_limits_from_usage() - .or_else(|| self.rate_limit_snapshot.clone()); - if let Some(snapshot) = rate_limit_snapshot.clone() { - self.rate_limit_snapshot = Some(snapshot); - } + self.rate_limit_snapshot = self + .fetch_rate_limits_from_usage() + .or(self.rate_limit_snapshot.clone()); self.add_to_history(crate::status::new_status_output( &self.config, @@ -1744,7 +1741,7 @@ impl ChatWidget { total_usage, context_usage, &self.conversation_id, - rate_limit_snapshot.as_ref(), + self.rate_limit_snapshot.as_ref(), Local::now(), )); } From 7ffdcecb8f8936aec1dbc8886ad532db0e1440ee Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 11:35:51 -0800 Subject: [PATCH 04/15] fetching even if timeout --- codex-rs/tui/src/app.rs | 3 ++ codex-rs/tui/src/app_event.rs | 4 ++ codex-rs/tui/src/chatwidget.rs | 91 ++++++++++++++++++++-------------- 3 files changed, 61 insertions(+), 37 deletions(-) diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index 4aa295a323..9f996007f8 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -496,6 +496,9 @@ impl App { AppEvent::FileSearchResult { query, matches } => { self.chat_widget.apply_file_search_result(query, matches); } + AppEvent::RateLimitSnapshotFetched(snapshot) => { + self.chat_widget.on_rate_limit_snapshot(Some(snapshot)); + } AppEvent::UpdateReasoningEffort(effort) => { self.on_update_reasoning_effort(effort); } diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 39485faa93..98a18cb30c 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -4,6 +4,7 @@ use codex_common::approval_presets::ApprovalPreset; use codex_common::model_presets::ModelPreset; use codex_core::protocol::ConversationPathResponseEvent; use codex_core::protocol::Event; +use codex_core::protocol::RateLimitSnapshot; use codex_file_search::FileMatch; use crate::bottom_pane::ApprovalRequest; @@ -41,6 +42,9 @@ pub(crate) enum AppEvent { matches: Vec, }, + /// Result of refreshing rate limits (e.g., after `/status`). + RateLimitSnapshotFetched(RateLimitSnapshot), + /// Result of computing a `/diff` command. DiffResult(String), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c2b04f020c..a5b9f6670d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -65,6 +65,7 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::oneshot; use tracing::debug; use crate::app_event::AppEvent; @@ -119,6 +120,7 @@ use codex_common::approval_presets::builtin_approval_presets; use codex_common::model_presets::ModelPreset; use codex_common::model_presets::builtin_model_presets; use codex_core::AuthManager; +use codex_core::CodexAuth; use codex_core::ConversationManager; use codex_core::protocol::AskForApproval; use codex_core::protocol::SandboxPolicy; @@ -497,7 +499,7 @@ impl ChatWidget { } } - fn on_rate_limit_snapshot(&mut self, snapshot: Option) { + pub(crate) fn on_rate_limit_snapshot(&mut self, snapshot: Option) { if let Some(snapshot) = snapshot { let warnings = self.rate_limit_warnings.take_warnings( snapshot @@ -1724,62 +1726,61 @@ impl ChatWidget { } pub(crate) fn add_status_output(&mut self) { - let default_usage = TokenUsage::default(); let (total_usage, context_usage) = if let Some(ti) = &self.token_info { - (&ti.total_token_usage, Some(&ti.last_token_usage)) + ( + ti.total_token_usage.clone(), + Some(ti.last_token_usage.clone()), + ) } else { - (&default_usage, Some(&default_usage)) + let usage = TokenUsage::default(); + (usage.clone(), Some(usage)) }; - self.rate_limit_snapshot = self - .fetch_rate_limits_from_usage() - .or(self.rate_limit_snapshot.clone()); + let snapshot_for_display = self + .refresh_rate_limits_from_usage() + .or_else(|| self.rate_limit_snapshot.clone()); self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), - total_usage, - context_usage, + &total_usage, + context_usage.as_ref(), &self.conversation_id, - self.rate_limit_snapshot.as_ref(), + snapshot_for_display.as_ref(), Local::now(), )); } - fn fetch_rate_limits_from_usage(&self) -> Option { + fn refresh_rate_limits_from_usage(&mut self) -> Option { let auth = self.auth_manager.auth()?; if auth.mode != AuthMode::ChatGPT { return None; } let base_url = self.config.chatgpt_base_url.clone(); - let auth = auth; - let result = tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - let fetch = async { - let client = BackendClient::from_auth(base_url, &auth).await?; - client.get_rate_limits().await - }; - tokio::time::timeout(Duration::from_secs(1), fetch).await - }) + let app_event_tx = self.app_event_tx.clone(); + let (tx, rx) = oneshot::channel(); + + tokio::spawn(async move { + let snapshot = fetch_rate_limits(base_url, auth).await; + if let Some(snapshot) = snapshot { + app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot.clone())); + let _ = tx.send(Some(snapshot)); + } else { + let _ = tx.send(None); + } }); - match result { - Ok(Ok(snapshot)) => Some(crate::status::rate_limit_snapshot_display( - &snapshot, - Local::now(), - )), - Ok(Err(err)) => { - tracing::debug!( - error = ?err, - "failed to fetch rate limits from /usage within timeout" - ); - None - } - Err(_) => { - tracing::debug!("timed out fetching rate limits from /usage"); - None - } - } + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(async { + match tokio::time::timeout(Duration::from_secs(1), rx).await { + Ok(Ok(Some(snapshot))) => Some(crate::status::rate_limit_snapshot_display( + &snapshot, + Local::now(), + )), + Ok(Ok(None)) | Ok(Err(_)) | Err(_) => None, + } + }) + }) } fn lower_cost_preset(&self) -> Option { @@ -2936,6 +2937,22 @@ fn extract_first_bold(s: &str) -> Option { None } +async fn fetch_rate_limits(base_url: String, auth: CodexAuth) -> Option { + match BackendClient::from_auth(base_url, &auth).await { + Ok(client) => match client.get_rate_limits().await { + Ok(snapshot) => Some(snapshot), + Err(err) => { + debug!(error = ?err, "failed to fetch rate limits from /usage"); + None + } + }, + Err(err) => { + debug!(error = ?err, "failed to construct backend client for rate limits"); + None + } + } +} + #[cfg(test)] pub(crate) fn show_review_commit_picker_with_entries( chat: &mut ChatWidget, From fc6aa8406acdee872769fb6c2417713dba1ffce0 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 11:37:11 -0800 Subject: [PATCH 05/15] simplifying --- codex-rs/tui/config.toml | 2 ++ codex-rs/tui/src/chatwidget.rs | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml new file mode 100644 index 0000000000..ca4c341c5d --- /dev/null +++ b/codex-rs/tui/config.toml @@ -0,0 +1,2 @@ +[projects."/Users/zhao/code/codex"] +trust_level = "untrusted" diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a5b9f6670d..2d47caaecf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1732,8 +1732,7 @@ impl ChatWidget { Some(ti.last_token_usage.clone()), ) } else { - let usage = TokenUsage::default(); - (usage.clone(), Some(usage)) + (TokenUsage::default(), Some(TokenUsage::default())) }; let snapshot_for_display = self From a1db68171c5eadfbe6e74c40b1e2c1b31fa24a66 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 11:38:41 -0800 Subject: [PATCH 06/15] delete config.toml --- codex-rs/tui/config.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml deleted file mode 100644 index ca4c341c5d..0000000000 --- a/codex-rs/tui/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[projects."/Users/zhao/code/codex"] -trust_level = "untrusted" From 21ae11276787876a5175388e60cbb12ea633a281 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 12:44:26 -0800 Subject: [PATCH 07/15] rate limit prefetching --- codex-rs/tui/src/chatwidget.rs | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 2d47caaecf..4b012d9de3 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1039,7 +1039,7 @@ impl ChatWidget { let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string(); let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager); - Self { + let widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), codex_op_tx, @@ -1081,7 +1081,11 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, - } + }; + + widget.prefetch_rate_limits(); + + widget } /// Create a ChatWidget attached to an existing conversation (e.g., a fork). @@ -1106,7 +1110,7 @@ impl ChatWidget { let codex_op_tx = spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone()); - Self { + let widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), codex_op_tx, @@ -1148,7 +1152,11 @@ impl ChatWidget { last_rendered_width: std::cell::Cell::new(None), feedback, current_rollout_path: None, - } + }; + + widget.prefetch_rate_limits(); + + widget } pub(crate) fn handle_key_event(&mut self, key_event: KeyEvent) { @@ -1782,6 +1790,24 @@ impl ChatWidget { }) } + fn prefetch_rate_limits(&self) { + let Some(auth) = self.auth_manager.auth() else { + return; + }; + if auth.mode != AuthMode::ChatGPT { + return; + } + + let base_url = self.config.chatgpt_base_url.clone(); + let app_event_tx = self.app_event_tx.clone(); + + tokio::spawn(async move { + if let Some(snapshot) = fetch_rate_limits(base_url, auth).await { + app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); + } + }); + } + fn lower_cost_preset(&self) -> Option { let auth_mode = self.auth_manager.auth().map(|auth| auth.mode); builtin_model_presets(auth_mode) From 6343be110068ec8993e87ed9ca667d5531a0fca9 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:08:09 -0800 Subject: [PATCH 08/15] background fetch --- codex-rs/tui/config.toml | 2 ++ codex-rs/tui/src/chatwidget.rs | 49 ++++++++-------------------------- 2 files changed, 13 insertions(+), 38 deletions(-) create mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml new file mode 100644 index 0000000000..ca4c341c5d --- /dev/null +++ b/codex-rs/tui/config.toml @@ -0,0 +1,2 @@ +[projects."/Users/zhao/code/codex"] +trust_level = "untrusted" diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 4b012d9de3..d586b88bd2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -65,7 +65,6 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; use tokio::sync::mpsc::UnboundedSender; -use tokio::sync::oneshot; use tracing::debug; use crate::app_event::AppEvent; @@ -1743,9 +1742,7 @@ impl ChatWidget { (TokenUsage::default(), Some(TokenUsage::default())) }; - let snapshot_for_display = self - .refresh_rate_limits_from_usage() - .or_else(|| self.rate_limit_snapshot.clone()); + let snapshot_for_display = self.rate_limit_snapshot.clone(); self.add_to_history(crate::status::new_status_output( &self.config, @@ -1757,39 +1754,6 @@ impl ChatWidget { Local::now(), )); } - fn refresh_rate_limits_from_usage(&mut self) -> Option { - let auth = self.auth_manager.auth()?; - if auth.mode != AuthMode::ChatGPT { - return None; - } - - let base_url = self.config.chatgpt_base_url.clone(); - let app_event_tx = self.app_event_tx.clone(); - let (tx, rx) = oneshot::channel(); - - tokio::spawn(async move { - let snapshot = fetch_rate_limits(base_url, auth).await; - if let Some(snapshot) = snapshot { - app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot.clone())); - let _ = tx.send(Some(snapshot)); - } else { - let _ = tx.send(None); - } - }); - - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(async { - match tokio::time::timeout(Duration::from_secs(1), rx).await { - Ok(Ok(Some(snapshot))) => Some(crate::status::rate_limit_snapshot_display( - &snapshot, - Local::now(), - )), - Ok(Ok(None)) | Ok(Err(_)) | Err(_) => None, - } - }) - }) - } - fn prefetch_rate_limits(&self) { let Some(auth) = self.auth_manager.auth() else { return; @@ -1802,9 +1766,18 @@ impl ChatWidget { let app_event_tx = self.app_event_tx.clone(); tokio::spawn(async move { - if let Some(snapshot) = fetch_rate_limits(base_url, auth).await { + let mut interval = tokio::time::interval(Duration::from_secs(60)); + + if let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth.clone()).await { app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); } + + loop { + interval.tick().await; + if let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth.clone()).await { + app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); + } + } }); } From c8ecc494414a650cda81f59bc17464bb20a732c5 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:12:37 -0800 Subject: [PATCH 09/15] undo diff --- codex-rs/tui/src/chatwidget.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index d586b88bd2..9fccd34d08 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1733,16 +1733,14 @@ impl ChatWidget { } pub(crate) fn add_status_output(&mut self) { + let default_usage = TokenUsage::default(); let (total_usage, context_usage) = if let Some(ti) = &self.token_info { - ( - ti.total_token_usage.clone(), - Some(ti.last_token_usage.clone()), - ) + (&ti.total_token_usage, Some(&ti.last_token_usage)) } else { - (TokenUsage::default(), Some(TokenUsage::default())) + (&default_usage, Some(&default_usage)) }; - let snapshot_for_display = self.rate_limit_snapshot.clone(); + let snapshot_for_display = self.rate_limit_snapshot.as_ref(); self.add_to_history(crate::status::new_status_output( &self.config, @@ -1750,7 +1748,7 @@ impl ChatWidget { &total_usage, context_usage.as_ref(), &self.conversation_id, - snapshot_for_display.as_ref(), + snapshot_for_display, Local::now(), )); } From 82639ec81b7bd43a7e38d3fde0c83df4d761bf75 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:19:13 -0800 Subject: [PATCH 10/15] . --- codex-rs/tui/src/chatwidget.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9fccd34d08..356d90a762 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1740,15 +1740,13 @@ impl ChatWidget { (&default_usage, Some(&default_usage)) }; - let snapshot_for_display = self.rate_limit_snapshot.as_ref(); - self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), - &total_usage, - context_usage.as_ref(), + total_usage, + context_usage, &self.conversation_id, - snapshot_for_display, + self.rate_limit_snapshot.as_ref(), Local::now(), )); } From 296c98b15984b535498e3eabe64bd869323c4259 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:20:02 -0800 Subject: [PATCH 11/15] . --- codex-rs/tui/src/chatwidget.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 356d90a762..ea6437ba46 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1739,7 +1739,6 @@ impl ChatWidget { } else { (&default_usage, Some(&default_usage)) }; - self.add_to_history(crate::status::new_status_output( &self.config, self.auth_manager.as_ref(), From 58ce6bcfb5ea7ebc420bd1d240ee37e66739cb6b Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:22:02 -0800 Subject: [PATCH 12/15] fix comment --- codex-rs/tui/src/app_event.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs index 98a18cb30c..0c3033c5c9 100644 --- a/codex-rs/tui/src/app_event.rs +++ b/codex-rs/tui/src/app_event.rs @@ -42,7 +42,7 @@ pub(crate) enum AppEvent { matches: Vec, }, - /// Result of refreshing rate limits (e.g., after `/status`). + /// Result of refreshing rate limits RateLimitSnapshotFetched(RateLimitSnapshot), /// Result of computing a `/diff` command. From 82f49f4b4e0f708d4434d8209f5b1317a00d2811 Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:23:05 -0800 Subject: [PATCH 13/15] simplify prefetch_rate_limits --- codex-rs/tui/src/chatwidget.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index ea6437ba46..5d6a5fa105 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1763,15 +1763,11 @@ impl ChatWidget { tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(60)); - if let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth.clone()).await { - app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); - } - loop { - interval.tick().await; if let Some(snapshot) = fetch_rate_limits(base_url.clone(), auth.clone()).await { app_event_tx.send(AppEvent::RateLimitSnapshotFetched(snapshot)); } + interval.tick().await; } }); } From 189afb052f6735951a55fb0541d902118c147dba Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 13:23:40 -0800 Subject: [PATCH 14/15] del config.toml --- codex-rs/tui/config.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 codex-rs/tui/config.toml diff --git a/codex-rs/tui/config.toml b/codex-rs/tui/config.toml deleted file mode 100644 index ca4c341c5d..0000000000 --- a/codex-rs/tui/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[projects."/Users/zhao/code/codex"] -trust_level = "untrusted" From 6259f91e5d10b759bf90fb1d9aa8ae79357a020a Mon Sep 17 00:00:00 2001 From: kevin zhao Date: Mon, 17 Nov 2025 14:29:00 -0800 Subject: [PATCH 15/15] adding drop logic --- codex-rs/tui/src/chatwidget.rs | 28 ++++++++++++++++++++++++---- codex-rs/tui/src/chatwidget/tests.rs | 1 + 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 5d6a5fa105..83fea1e90e 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -65,6 +65,7 @@ use ratatui::text::Line; use ratatui::widgets::Paragraph; use ratatui::widgets::Wrap; use tokio::sync::mpsc::UnboundedSender; +use tokio::task::JoinHandle; use tracing::debug; use crate::app_event::AppEvent; @@ -259,6 +260,7 @@ pub(crate) struct ChatWidget { rate_limit_snapshot: Option, rate_limit_warnings: RateLimitWarningState, rate_limit_switch_prompt: RateLimitSwitchPromptState, + rate_limit_poller: Option>, // Stream lifecycle controller stream_controller: Option, running_commands: HashMap, @@ -1038,7 +1040,7 @@ impl ChatWidget { let placeholder = EXAMPLE_PROMPTS[rng.random_range(0..EXAMPLE_PROMPTS.len())].to_string(); let codex_op_tx = spawn_agent(config.clone(), app_event_tx.clone(), conversation_manager); - let widget = Self { + let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), codex_op_tx, @@ -1062,6 +1064,7 @@ impl ChatWidget { rate_limit_snapshot: None, rate_limit_warnings: RateLimitWarningState::default(), rate_limit_switch_prompt: RateLimitSwitchPromptState::default(), + rate_limit_poller: None, stream_controller: None, running_commands: HashMap::new(), task_complete_pending: false, @@ -1109,7 +1112,7 @@ impl ChatWidget { let codex_op_tx = spawn_agent_from_existing(conversation, session_configured, app_event_tx.clone()); - let widget = Self { + let mut widget = Self { app_event_tx: app_event_tx.clone(), frame_requester: frame_requester.clone(), codex_op_tx, @@ -1133,6 +1136,7 @@ impl ChatWidget { rate_limit_snapshot: None, rate_limit_warnings: RateLimitWarningState::default(), rate_limit_switch_prompt: RateLimitSwitchPromptState::default(), + rate_limit_poller: None, stream_controller: None, running_commands: HashMap::new(), task_complete_pending: false, @@ -1749,7 +1753,15 @@ impl ChatWidget { Local::now(), )); } - fn prefetch_rate_limits(&self) { + fn stop_rate_limit_poller(&mut self) { + if let Some(handle) = self.rate_limit_poller.take() { + handle.abort(); + } + } + + fn prefetch_rate_limits(&mut self) { + self.stop_rate_limit_poller(); + let Some(auth) = self.auth_manager.auth() else { return; }; @@ -1760,7 +1772,7 @@ impl ChatWidget { let base_url = self.config.chatgpt_base_url.clone(); let app_event_tx = self.app_event_tx.clone(); - tokio::spawn(async move { + let handle = tokio::spawn(async move { let mut interval = tokio::time::interval(Duration::from_secs(60)); loop { @@ -1770,6 +1782,8 @@ impl ChatWidget { interval.tick().await; } }); + + self.rate_limit_poller = Some(handle); } fn lower_cost_preset(&self) -> Option { @@ -2808,6 +2822,12 @@ impl ChatWidget { } } +impl Drop for ChatWidget { + fn drop(&mut self) { + self.stop_rate_limit_poller(); + } +} + impl Renderable for ChatWidget { fn render(&self, area: Rect, buf: &mut Buffer) { self.as_renderable().render(area, buf); diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index bdaae93353..c875862bbc 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -333,6 +333,7 @@ fn make_chatwidget_manual() -> ( rate_limit_snapshot: None, rate_limit_warnings: RateLimitWarningState::default(), rate_limit_switch_prompt: RateLimitSwitchPromptState::default(), + rate_limit_poller: None, stream_controller: None, running_commands: HashMap::new(), task_complete_pending: false,