Remove legacy ModelInfo and merge it with ModelFamily (#7748)

This is a step towards removing the need to know `model` when
constructing config. We firstly don't need to know `model_info` and just
respect if the user has already set it. Next step, we don't need to know
`model` unless the user explicitly set it in `config.toml`
This commit is contained in:
Ahmed Ibrahim
2025-12-08 15:29:37 -08:00
committed by GitHub
Unverified
parent ac5fa6baf8
commit 382f047a10
10 changed files with 205 additions and 225 deletions
+2 -11
View File
@@ -48,7 +48,6 @@ use crate::error::Result;
use crate::flags::CODEX_RS_SSE_FIXTURE;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::WireApi;
use crate::openai_model_info::get_model_info;
use crate::openai_models::model_family::ModelFamily;
use crate::tools::spec::create_tools_json_for_chat_completions_api;
use crate::tools::spec::create_tools_json_for_responses_api;
@@ -95,19 +94,11 @@ impl ModelClient {
pub fn get_model_context_window(&self) -> Option<i64> {
let model_family = self.get_model_family();
let effective_context_window_percent = model_family.effective_context_window_percent;
self.config
.model_context_window
.or_else(|| get_model_info(&model_family).map(|info| info.context_window))
model_family
.context_window
.map(|w| w.saturating_mul(effective_context_window_percent) / 100)
}
pub fn get_auto_compact_token_limit(&self) -> Option<i64> {
let model_family = self.get_model_family();
self.config.model_auto_compact_token_limit.or_else(|| {
get_model_info(&model_family).and_then(|info| info.auto_compact_token_limit)
})
}
pub fn config(&self) -> Arc<Config> {
Arc::clone(&self.config)
}
+3 -10
View File
@@ -80,7 +80,6 @@ use crate::exec::StreamOutput;
use crate::exec_policy::ExecPolicyUpdateError;
use crate::mcp::auth::compute_auth_statuses;
use crate::mcp_connection_manager::McpConnectionManager;
use crate::openai_model_info::get_model_info;
use crate::project_doc::get_user_instructions;
use crate::protocol::AgentMessageContentDeltaEvent;
use crate::protocol::AgentReasoningSectionBreakEvent;
@@ -415,15 +414,11 @@ impl Session {
otel_event_manager: &OtelEventManager,
provider: ModelProviderInfo,
session_configuration: &SessionConfiguration,
mut per_turn_config: Config,
per_turn_config: Config,
model_family: ModelFamily,
conversation_id: ConversationId,
sub_id: String,
) -> TurnContext {
if let Some(model_info) = get_model_info(&model_family) {
per_turn_config.model_context_window = Some(model_info.context_window);
}
let otel_event_manager = otel_event_manager.clone().with_model(
session_configuration.model.as_str(),
model_family.slug.as_str(),
@@ -1955,9 +1950,6 @@ async fn spawn_review_thread(
per_turn_config.model_reasoning_effort = Some(ReasoningEffortConfig::Low);
per_turn_config.model_reasoning_summary = ReasoningSummaryConfig::Detailed;
per_turn_config.features = review_features.clone();
if let Some(model_info) = get_model_info(&model_family) {
per_turn_config.model_context_window = Some(model_info.context_window);
}
let otel_event_manager = parent_turn_context
.client
@@ -2097,7 +2089,8 @@ pub(crate) async fn run_task(
} = turn_output;
let limit = turn_context
.client
.get_auto_compact_token_limit()
.get_model_family()
.auto_compact_token_limit()
.unwrap_or(i64::MAX);
let total_usage_tokens = sess.get_total_token_usage().await;
let token_limit_reached = total_usage_tokens >= limit;
+11 -24
View File
@@ -26,8 +26,6 @@ use crate::model_provider_info::LMSTUDIO_OSS_PROVIDER_ID;
use crate::model_provider_info::ModelProviderInfo;
use crate::model_provider_info::OLLAMA_OSS_PROVIDER_ID;
use crate::model_provider_info::built_in_model_providers;
use crate::openai_model_info::get_model_info;
use crate::openai_models::model_family::find_family_for_model;
use crate::project_doc::DEFAULT_PROJECT_DOC_FILENAME;
use crate::project_doc::LOCAL_PROJECT_DOC_FILENAME;
use crate::protocol::AskForApproval;
@@ -1106,23 +1104,12 @@ impl Config {
let forced_login_method = cfg.forced_login_method;
// todo(aibrahim): make model optional
let model = model
.or(config_profile.model)
.or(cfg.model)
.unwrap_or_else(default_model);
let model_family = find_family_for_model(&model);
let openai_model_info = get_model_info(&model_family);
let model_context_window = cfg
.model_context_window
.or_else(|| openai_model_info.as_ref().map(|info| info.context_window));
let model_auto_compact_token_limit = cfg.model_auto_compact_token_limit.or_else(|| {
openai_model_info
.as_ref()
.and_then(|info| info.auto_compact_token_limit)
});
let compact_prompt = compact_prompt.or(cfg.compact_prompt).and_then(|value| {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -1168,8 +1155,8 @@ impl Config {
let config = Self {
model,
review_model,
model_context_window,
model_auto_compact_token_limit,
model_context_window: cfg.model_context_window,
model_auto_compact_token_limit: cfg.model_auto_compact_token_limit,
model_provider_id,
model_provider,
cwd: resolved_cwd,
@@ -2950,8 +2937,8 @@ model_verbosity = "high"
Config {
model: "o3".to_string(),
review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(),
model_context_window: Some(200_000),
model_auto_compact_token_limit: Some(180_000),
model_context_window: None,
model_auto_compact_token_limit: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::Never,
@@ -3025,8 +3012,8 @@ model_verbosity = "high"
let expected_gpt3_profile_config = Config {
model: "gpt-3.5-turbo".to_string(),
review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(),
model_context_window: Some(16_385),
model_auto_compact_token_limit: Some(14_746),
model_context_window: None,
model_auto_compact_token_limit: None,
model_provider_id: "openai-chat-completions".to_string(),
model_provider: fixture.openai_chat_completions_provider.clone(),
approval_policy: AskForApproval::UnlessTrusted,
@@ -3115,8 +3102,8 @@ model_verbosity = "high"
let expected_zdr_profile_config = Config {
model: "o3".to_string(),
review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(),
model_context_window: Some(200_000),
model_auto_compact_token_limit: Some(180_000),
model_context_window: None,
model_auto_compact_token_limit: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::OnFailure,
@@ -3191,8 +3178,8 @@ model_verbosity = "high"
let expected_gpt5_profile_config = Config {
model: "gpt-5.1".to_string(),
review_model: OPENAI_DEFAULT_REVIEW_MODEL.to_string(),
model_context_window: Some(272_000),
model_auto_compact_token_limit: Some(244_800),
model_context_window: None,
model_auto_compact_token_limit: None,
model_provider_id: "openai".to_string(),
model_provider: fixture.openai_provider.clone(),
approval_policy: AskForApproval::OnFailure,
-1
View File
@@ -67,7 +67,6 @@ pub use conversation_manager::NewConversation;
pub use auth::AuthManager;
pub use auth::CodexAuth;
pub mod default_client;
mod openai_model_info;
pub mod project_doc;
mod rollout;
pub(crate) mod safety;
-83
View File
@@ -1,83 +0,0 @@
use crate::openai_models::model_family::ModelFamily;
// Shared constants for commonly used window/token sizes.
pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000;
/// Metadata about a model, particularly OpenAI models.
/// We may want to consider including details like the pricing for
/// input tokens, output tokens, etc., though users will need to be able to
/// override this in config.toml, as this information can get out of date.
/// Though this would help present more accurate pricing information in the UI.
#[derive(Debug)]
pub(crate) struct ModelInfo {
/// Size of the context window in tokens. This is the maximum size of the input context.
pub(crate) context_window: i64,
/// Token threshold where we should automatically compact conversation history. This considers
/// input tokens + output tokens of this turn.
pub(crate) auto_compact_token_limit: Option<i64>,
}
impl ModelInfo {
const fn new(context_window: i64) -> Self {
Self {
context_window,
auto_compact_token_limit: Some(Self::default_auto_compact_limit(context_window)),
}
}
const fn default_auto_compact_limit(context_window: i64) -> i64 {
(context_window * 9) / 10
}
}
pub(crate) fn get_model_info(model_family: &ModelFamily) -> Option<ModelInfo> {
let slug = model_family.slug.as_str();
match slug {
// OSS models have a 128k shared token pool.
// Arbitrarily splitting it: 3/4 input context, 1/4 output.
// https://openai.com/index/gpt-oss-model-card/
"gpt-oss-20b" => Some(ModelInfo::new(96_000)),
"gpt-oss-120b" => Some(ModelInfo::new(96_000)),
// https://platform.openai.com/docs/models/o3
"o3" => Some(ModelInfo::new(200_000)),
// https://platform.openai.com/docs/models/o4-mini
"o4-mini" => Some(ModelInfo::new(200_000)),
// https://platform.openai.com/docs/models/codex-mini-latest
"codex-mini-latest" => Some(ModelInfo::new(200_000)),
// As of Jun 25, 2025, gpt-4.1 defaults to gpt-4.1-2025-04-14.
// https://platform.openai.com/docs/models/gpt-4.1
"gpt-4.1" | "gpt-4.1-2025-04-14" => Some(ModelInfo::new(1_047_576)),
// As of Jun 25, 2025, gpt-4o defaults to gpt-4o-2024-08-06.
// https://platform.openai.com/docs/models/gpt-4o
"gpt-4o" | "gpt-4o-2024-08-06" => Some(ModelInfo::new(128_000)),
// https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-05-13
"gpt-4o-2024-05-13" => Some(ModelInfo::new(128_000)),
// https://platform.openai.com/docs/models/gpt-4o?snapshot=gpt-4o-2024-11-20
"gpt-4o-2024-11-20" => Some(ModelInfo::new(128_000)),
// https://platform.openai.com/docs/models/gpt-3.5-turbo
"gpt-3.5-turbo" => Some(ModelInfo::new(16_385)),
_ if slug.starts_with("gpt-5-codex")
|| slug.starts_with("gpt-5.1-codex")
|| slug.starts_with("gpt-5.1-codex-max") =>
{
Some(ModelInfo::new(CONTEXT_WINDOW_272K))
}
_ if slug.starts_with("gpt-5") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)),
_ if slug.starts_with("codex-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)),
_ if slug.starts_with("exp-") => Some(ModelInfo::new(CONTEXT_WINDOW_272K)),
_ => None,
}
}
@@ -15,6 +15,7 @@ const BASE_INSTRUCTIONS: &str = include_str!("../../prompt.md");
const GPT_5_CODEX_INSTRUCTIONS: &str = include_str!("../../gpt_5_codex_prompt.md");
const GPT_5_1_INSTRUCTIONS: &str = include_str!("../../gpt_5_1_prompt.md");
const GPT_5_1_CODEX_MAX_INSTRUCTIONS: &str = include_str!("../../gpt-5.1-codex-max_prompt.md");
pub(crate) const CONTEXT_WINDOW_272K: i64 = 272_000;
/// A model family is a group of models that share certain characteristics.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -23,14 +24,20 @@ pub struct ModelFamily {
/// "gpt-4.1-2025-04-14".
pub slug: String,
/// The model family name, e.g. "gpt-4.1". Note this should able to be used
/// with [`crate::openai_model_info::get_model_info`].
/// The model family name, e.g. "gpt-4.1". This string is used when deriving
/// default metadata for the family, such as context windows.
pub family: String,
/// True if the model needs additional instructions on how to use the
/// "virtual" `apply_patch` CLI.
pub needs_special_apply_patch_instructions: bool,
/// Maximum supported context window, if known.
pub context_window: Option<i64>,
/// Token threshold for automatic compaction if config does not override it.
auto_compact_token_limit: Option<i64>,
// Whether the `reasoning` field can be set when making a request to this
// model family. Note it has `effort` and `summary` subfields (though
// `summary` is optional).
@@ -82,6 +89,12 @@ impl ModelFamily {
if let Some(reasoning_summary_format) = config.model_reasoning_summary_format.as_ref() {
self.reasoning_summary_format = reasoning_summary_format.clone();
}
if let Some(context_window) = config.model_context_window {
self.context_window = Some(context_window);
}
if let Some(auto_compact_token_limit) = config.model_auto_compact_token_limit {
self.auto_compact_token_limit = Some(auto_compact_token_limit);
}
self
}
pub fn with_remote_overrides(mut self, remote_models: Vec<ModelInfo>) -> Self {
@@ -93,6 +106,15 @@ impl ModelFamily {
}
self
}
pub fn auto_compact_token_limit(&self) -> Option<i64> {
self.auto_compact_token_limit
.or(self.context_window.map(Self::default_auto_compact_limit))
}
const fn default_auto_compact_limit(context_window: i64) -> i64 {
(context_window * 9) / 10
}
}
macro_rules! model_family {
@@ -105,6 +127,8 @@ macro_rules! model_family {
slug: $slug.to_string(),
family: $family.to_string(),
needs_special_apply_patch_instructions: false,
context_window: Some(CONTEXT_WINDOW_272K),
auto_compact_token_limit: None,
supports_reasoning_summaries: false,
reasoning_summary_format: ReasoningSummaryFormat::None,
supports_parallel_tool_calls: false,
@@ -136,12 +160,14 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
slug, "o3",
supports_reasoning_summaries: true,
needs_special_apply_patch_instructions: true,
context_window: Some(200_000),
)
} else if slug.starts_with("o4-mini") {
model_family!(
slug, "o4-mini",
supports_reasoning_summaries: true,
needs_special_apply_patch_instructions: true,
context_window: Some(200_000),
)
} else if slug.starts_with("codex-mini-latest") {
model_family!(
@@ -149,18 +175,32 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
supports_reasoning_summaries: true,
needs_special_apply_patch_instructions: true,
shell_type: ConfigShellToolType::Local,
context_window: Some(200_000),
)
} else if slug.starts_with("gpt-4.1") {
model_family!(
slug, "gpt-4.1",
needs_special_apply_patch_instructions: true,
context_window: Some(1_047_576),
)
} else if slug.starts_with("gpt-oss") || slug.starts_with("openai/gpt-oss") {
model_family!(slug, "gpt-oss", apply_patch_tool_type: Some(ApplyPatchToolType::Function))
model_family!(
slug, "gpt-oss",
apply_patch_tool_type: Some(ApplyPatchToolType::Function),
context_window: Some(96_000),
)
} else if slug.starts_with("gpt-4o") {
model_family!(slug, "gpt-4o", needs_special_apply_patch_instructions: true)
model_family!(
slug, "gpt-4o",
needs_special_apply_patch_instructions: true,
context_window: Some(128_000),
)
} else if slug.starts_with("gpt-3.5") {
model_family!(slug, "gpt-3.5", needs_special_apply_patch_instructions: true)
model_family!(
slug, "gpt-3.5",
needs_special_apply_patch_instructions: true,
context_window: Some(16_385),
)
} else if slug.starts_with("test-gpt-5") {
model_family!(
slug, slug,
@@ -196,6 +236,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
supports_parallel_tool_calls: true,
support_verbosity: true,
truncation_policy: TruncationPolicy::Tokens(10_000),
context_window: Some(CONTEXT_WINDOW_272K),
)
} else if slug.starts_with("exp-") {
model_family!(
@@ -209,6 +250,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
truncation_policy: TruncationPolicy::Bytes(10_000),
shell_type: ConfigShellToolType::UnifiedExec,
supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K),
)
// Production models.
@@ -223,6 +265,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
supports_parallel_tool_calls: true,
support_verbosity: false,
truncation_policy: TruncationPolicy::Tokens(10_000),
context_window: Some(CONTEXT_WINDOW_272K),
)
} else if slug.starts_with("gpt-5-codex")
|| slug.starts_with("gpt-5.1-codex")
@@ -238,6 +281,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
supports_parallel_tool_calls: true,
support_verbosity: false,
truncation_policy: TruncationPolicy::Tokens(10_000),
context_window: Some(CONTEXT_WINDOW_272K),
)
} else if slug.starts_with("gpt-5.1") {
model_family!(
@@ -251,6 +295,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
truncation_policy: TruncationPolicy::Bytes(10_000),
shell_type: ConfigShellToolType::ShellCommand,
supports_parallel_tool_calls: true,
context_window: Some(CONTEXT_WINDOW_272K),
)
} else if slug.starts_with("gpt-5") {
model_family!(
@@ -260,6 +305,7 @@ pub fn find_family_for_model(slug: &str) -> ModelFamily {
shell_type: ConfigShellToolType::Default,
support_verbosity: true,
truncation_policy: TruncationPolicy::Bytes(10_000),
context_window: Some(CONTEXT_WINDOW_272K),
)
} else {
derive_default_model_family(slug)
@@ -271,6 +317,8 @@ fn derive_default_model_family(model: &str) -> ModelFamily {
slug: model.to_string(),
family: model.to_string(),
needs_special_apply_patch_instructions: false,
context_window: None,
auto_compact_token_limit: None,
supports_reasoning_summaries: false,
reasoning_summary_format: ReasoningSummaryFormat::None,
supports_parallel_tool_calls: false,
+2 -1
View File
@@ -550,7 +550,7 @@ impl ChatWidget {
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
info.model_context_window
.or(self.config.model_context_window)
.or(self.model_family.context_window)
.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
@@ -2024,6 +2024,7 @@ impl ChatWidget {
self.add_to_history(crate::status::new_status_output(
&self.config,
self.auth_manager.as_ref(),
&self.model_family,
total_usage,
context_usage,
&self.conversation_id,
+94 -89
View File
@@ -98,7 +98,7 @@ fn snapshot(percent: f64) -> RateLimitSnapshot {
#[test]
fn resumed_initial_messages_render_history() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let (mut chat, mut rx, _ops) = make_chatwidget_manual(None);
let conversation_id = ConversationId::new();
let rollout_file = NamedTempFile::new().unwrap();
@@ -154,7 +154,7 @@ fn resumed_initial_messages_render_history() {
/// Entering review mode uses the hint provided by the review request.
#[test]
fn entered_review_mode_uses_request_hint() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let (mut chat, mut rx, _ops) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "review-start".into(),
@@ -175,7 +175,7 @@ fn entered_review_mode_uses_request_hint() {
/// Entering review mode renders the current changes banner when requested.
#[test]
fn entered_review_mode_defaults_to_current_changes_banner() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let (mut chat, mut rx, _ops) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "review-start".into(),
@@ -195,7 +195,7 @@ fn entered_review_mode_defaults_to_current_changes_banner() {
/// the closing banner while clearing review mode state.
#[test]
fn exited_review_mode_emits_results_and_finishes() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let (mut chat, mut rx, _ops) = make_chatwidget_manual(None);
let review = ReviewOutputEvent {
findings: vec![ReviewFinding {
@@ -229,7 +229,7 @@ fn exited_review_mode_emits_results_and_finishes() {
/// Exiting review restores the pre-review context window indicator.
#[test]
fn review_restores_context_window_indicator() {
let (mut chat, mut rx, _ops) = make_chatwidget_manual();
let (mut chat, mut rx, _ops) = make_chatwidget_manual(None);
let context_window = 13_000;
let pre_review_tokens = 12_700; // ~30% remaining after subtracting baseline.
@@ -278,7 +278,7 @@ fn review_restores_context_window_indicator() {
/// Receiving a TokenCount event without usage clears the context indicator.
#[test]
fn token_count_none_resets_context_indicator() {
let (mut chat, _rx, _ops) = make_chatwidget_manual();
let (mut chat, _rx, _ops) = make_chatwidget_manual(None);
let context_window = 13_000;
let pre_compact_tokens = 12_700;
@@ -304,7 +304,7 @@ fn token_count_none_resets_context_indicator() {
#[test]
fn context_indicator_shows_used_tokens_when_window_unknown() {
let (mut chat, _rx, _ops) = make_chatwidget_manual();
let (mut chat, _rx, _ops) = make_chatwidget_manual(Some("unknown-model"));
chat.config.model_context_window = None;
let auto_compact_limit = 200_000;
@@ -371,7 +371,9 @@ async fn helpers_are_available_and_do_not_panic() {
}
// --- Helpers for tests that need direct construction and event draining ---
fn make_chatwidget_manual() -> (
fn make_chatwidget_manual(
model_override: Option<&str>,
) -> (
ChatWidget,
tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
tokio::sync::mpsc::UnboundedReceiver<Op>,
@@ -379,7 +381,10 @@ fn make_chatwidget_manual() -> (
let (tx_raw, rx) = unbounded_channel::<AppEvent>();
let app_event_tx = AppEventSender::new(tx_raw);
let (op_tx, op_rx) = unbounded_channel::<Op>();
let cfg = test_config();
let mut cfg = test_config();
if let Some(model) = model_override {
cfg.model = model.to_string();
}
let bottom = BottomPane::new(BottomPaneParams {
app_event_tx: app_event_tx.clone(),
frame_requester: FrameRequester::test_dummy(),
@@ -447,7 +452,7 @@ pub(crate) fn make_chatwidget_manual_with_sender() -> (
tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
tokio::sync::mpsc::UnboundedReceiver<Op>,
) {
let (widget, rx, op_rx) = make_chatwidget_manual();
let (widget, rx, op_rx) = make_chatwidget_manual(None);
let app_event_tx = widget.app_event_tx.clone();
(widget, app_event_tx, rx, op_rx)
}
@@ -543,7 +548,7 @@ fn test_rate_limit_warnings_monthly() {
#[test]
fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
primary: None,
@@ -592,7 +597,7 @@ fn rate_limit_snapshot_keeps_prior_credits_when_missing_from_headers() {
#[test]
fn rate_limit_snapshot_updates_and_retains_plan_type() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.on_rate_limit_snapshot(Some(RateLimitSnapshot {
primary: Some(RateLimitWindow {
@@ -645,7 +650,7 @@ fn rate_limit_snapshot_updates_and_retains_plan_type() {
#[test]
fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
let (mut chat, _, _) = make_chatwidget_manual();
let (mut chat, _, _) = make_chatwidget_manual(None);
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.config.model = NUDGE_MODEL_SLUG.to_string();
@@ -661,7 +666,7 @@ fn rate_limit_switch_prompt_skips_when_on_lower_cost_model() {
#[test]
fn rate_limit_switch_prompt_shows_once_per_session() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual();
let (mut chat, _, _) = make_chatwidget_manual(None);
chat.config.model = "gpt-5".to_string();
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
@@ -686,7 +691,7 @@ fn rate_limit_switch_prompt_shows_once_per_session() {
#[test]
fn rate_limit_switch_prompt_respects_hidden_notice() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual();
let (mut chat, _, _) = make_chatwidget_manual(None);
chat.config.model = "gpt-5".to_string();
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
chat.config.notices.hide_rate_limit_model_nudge = Some(true);
@@ -702,7 +707,7 @@ fn rate_limit_switch_prompt_respects_hidden_notice() {
#[test]
fn rate_limit_switch_prompt_defers_until_task_complete() {
let auth = CodexAuth::create_dummy_chatgpt_auth_for_testing();
let (mut chat, _, _) = make_chatwidget_manual();
let (mut chat, _, _) = make_chatwidget_manual(None);
chat.config.model = "gpt-5".to_string();
chat.auth_manager = AuthManager::from_auth_for_testing(auth);
@@ -723,7 +728,7 @@ fn rate_limit_switch_prompt_defers_until_task_complete() {
#[test]
fn rate_limit_switch_prompt_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.auth_manager =
AuthManager::from_auth_for_testing(CodexAuth::create_dummy_chatgpt_auth_for_testing());
chat.config.model = "gpt-5".to_string();
@@ -739,7 +744,7 @@ fn rate_limit_switch_prompt_popup_snapshot() {
#[test]
fn exec_approval_emits_proposed_command_and_decision_history() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Trigger an exec approval request with a short, single-line command
let ev = ExecApprovalRequestEvent {
@@ -784,7 +789,7 @@ fn exec_approval_emits_proposed_command_and_decision_history() {
#[test]
fn exec_approval_decision_truncates_multiline_and_long_commands() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Multiline command: modal should show full command, history records decision only
let ev_multi = ExecApprovalRequestEvent {
@@ -969,7 +974,7 @@ fn get_available_model(chat: &ChatWidget, model: &str) -> ModelPreset {
#[test]
fn empty_enter_during_task_does_not_queue() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Simulate running task so submissions would normally be queued.
chat.bottom_pane.set_task_running(true);
@@ -983,7 +988,7 @@ fn empty_enter_during_task_does_not_queue() {
#[test]
fn alt_up_edits_most_recent_queued_message() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Simulate a running task so messages would normally be queued.
chat.bottom_pane.set_task_running(true);
@@ -1016,7 +1021,7 @@ fn alt_up_edits_most_recent_queued_message() {
/// is queued repeatedly.
#[test]
fn enqueueing_history_prompt_multiple_times_is_stable() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Submit an initial prompt to seed history.
chat.bottom_pane.set_composer_text("repeat me".to_string());
@@ -1042,7 +1047,7 @@ fn enqueueing_history_prompt_multiple_times_is_stable() {
#[test]
fn streaming_final_answer_keeps_task_running_state() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None);
chat.on_task_started();
chat.on_agent_message_delta("Final answer line\n".to_string());
@@ -1072,7 +1077,7 @@ fn streaming_final_answer_keeps_task_running_state() {
#[test]
fn ctrl_c_shutdown_ignores_caps_lock() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None);
chat.handle_key_event(KeyEvent::new(KeyCode::Char('C'), KeyModifiers::CONTROL));
@@ -1084,7 +1089,7 @@ fn ctrl_c_shutdown_ignores_caps_lock() {
#[test]
fn ctrl_c_cleared_prompt_is_recoverable_via_history() {
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(None);
chat.bottom_pane.insert_str("draft message ");
chat.bottom_pane
@@ -1118,7 +1123,7 @@ fn ctrl_c_cleared_prompt_is_recoverable_via_history() {
#[test]
fn exec_history_cell_shows_working_then_completed() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Begin command
let begin = begin_exec(&mut chat, "call-1", "echo done");
@@ -1148,7 +1153,7 @@ fn exec_history_cell_shows_working_then_completed() {
#[test]
fn exec_history_cell_shows_working_then_failed() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Begin command
let begin = begin_exec(&mut chat, "call-2", "false");
@@ -1172,7 +1177,7 @@ fn exec_history_cell_shows_working_then_failed() {
#[test]
fn exec_history_shows_unified_exec_startup_commands() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let begin = begin_exec_with_source(
&mut chat,
@@ -1200,7 +1205,7 @@ fn exec_history_shows_unified_exec_startup_commands() {
/// OpenReviewCustomPrompt to the app event channel.
#[test]
fn review_popup_custom_prompt_action_sends_event() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Open the preset selection popup
chat.open_review_popup();
@@ -1225,7 +1230,7 @@ fn review_popup_custom_prompt_action_sends_event() {
#[test]
fn slash_init_skips_when_project_doc_exists() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None);
let tempdir = tempdir().unwrap();
let existing_path = tempdir.path().join(DEFAULT_PROJECT_DOC_FILENAME);
std::fs::write(&existing_path, "existing instructions").unwrap();
@@ -1257,7 +1262,7 @@ fn slash_init_skips_when_project_doc_exists() {
#[test]
fn slash_quit_requests_exit() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.dispatch_command(SlashCommand::Quit);
@@ -1266,7 +1271,7 @@ fn slash_quit_requests_exit() {
#[test]
fn slash_exit_requests_exit() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.dispatch_command(SlashCommand::Exit);
@@ -1275,7 +1280,7 @@ fn slash_exit_requests_exit() {
#[test]
fn slash_resume_opens_picker() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.dispatch_command(SlashCommand::Resume);
@@ -1284,7 +1289,7 @@ fn slash_resume_opens_picker() {
#[test]
fn slash_undo_sends_op() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.dispatch_command(SlashCommand::Undo);
@@ -1296,7 +1301,7 @@ fn slash_undo_sends_op() {
#[test]
fn slash_rollout_displays_current_path() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let rollout_path = PathBuf::from("/tmp/codex-test-rollout.jsonl");
chat.current_rollout_path = Some(rollout_path.clone());
@@ -1313,7 +1318,7 @@ fn slash_rollout_displays_current_path() {
#[test]
fn slash_rollout_handles_missing_path() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.dispatch_command(SlashCommand::Rollout);
@@ -1332,7 +1337,7 @@ fn slash_rollout_handles_missing_path() {
#[test]
fn undo_success_events_render_info_messages() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "turn-1".to_string(),
@@ -1369,7 +1374,7 @@ fn undo_success_events_render_info_messages() {
#[test]
fn undo_failure_events_render_error_message() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "turn-2".to_string(),
@@ -1404,7 +1409,7 @@ fn undo_failure_events_render_error_message() {
#[test]
fn undo_started_hides_interrupt_hint() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "turn-hint".to_string(),
@@ -1424,7 +1429,7 @@ fn undo_started_hides_interrupt_hint() {
/// The commit picker shows only commit subjects (no timestamps).
#[test]
fn review_commit_picker_shows_subjects_without_timestamps() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Open the Review presets parent popup.
chat.open_review_popup();
@@ -1486,7 +1491,7 @@ fn review_commit_picker_shows_subjects_without_timestamps() {
/// and uses the same text for the user-facing hint.
#[test]
fn custom_prompt_submit_sends_review_op() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.show_review_custom_prompt();
// Paste prompt text via ChatWidget handler, then submit
@@ -1514,7 +1519,7 @@ fn custom_prompt_submit_sends_review_op() {
/// Hitting Enter on an empty custom prompt view does not submit.
#[test]
fn custom_prompt_enter_empty_does_not_send() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.show_review_custom_prompt();
// Enter without any text
@@ -1526,7 +1531,7 @@ fn custom_prompt_enter_empty_does_not_send() {
#[test]
fn view_image_tool_call_adds_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let image_path = chat.config.cwd.join("example.png");
chat.handle_codex_event(Event {
@@ -1547,7 +1552,7 @@ fn view_image_tool_call_adds_history_cell() {
// marker (replacing the spinner) and flushes it into history.
#[test]
fn interrupt_exec_marks_failed_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Begin a long-running command so we have an active exec cell with a spinner.
begin_exec(&mut chat, "call-int", "sleep 1");
@@ -1576,7 +1581,7 @@ fn interrupt_exec_marks_failed_snapshot() {
// suggesting the user to tell the model what to do differently and to use /feedback.
#[test]
fn interrupted_turn_error_message_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Simulate an in-progress task so the widget is in a running state.
chat.handle_codex_event(Event {
@@ -1607,7 +1612,7 @@ fn interrupted_turn_error_message_snapshot() {
/// parent popup, pressing Esc again dismisses all panels (back to normal mode).
#[test]
fn review_custom_prompt_escape_navigates_back_then_dismisses() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Open the Review presets parent popup.
chat.open_review_popup();
@@ -1642,7 +1647,7 @@ fn review_custom_prompt_escape_navigates_back_then_dismisses() {
/// parent popup, pressing Esc again dismisses all panels (back to normal mode).
#[tokio::test]
async fn review_branch_picker_escape_navigates_back_then_dismisses() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Open the Review presets parent popup.
chat.open_review_popup();
@@ -1729,7 +1734,7 @@ fn render_bottom_popup(chat: &ChatWidget, width: u16) -> String {
#[test]
fn model_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.config.model = "gpt-5-codex".to_string();
chat.open_model_popup();
@@ -1740,7 +1745,7 @@ fn model_selection_popup_snapshot() {
#[test]
fn approvals_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.config.notices.hide_full_access_warning = None;
chat.open_approvals_popup();
@@ -1779,7 +1784,7 @@ fn preset_matching_ignores_extra_writable_roots() {
#[test]
fn full_access_confirmation_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
let preset = builtin_approval_presets()
.into_iter()
@@ -1794,7 +1799,7 @@ fn full_access_confirmation_popup_snapshot() {
#[cfg(target_os = "windows")]
#[test]
fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
let preset = builtin_approval_presets()
.into_iter()
@@ -1812,7 +1817,7 @@ fn windows_auto_mode_prompt_requests_enabling_sandbox_feature() {
#[cfg(target_os = "windows")]
#[test]
fn startup_prompts_for_windows_sandbox_when_agent_requested() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
set_windows_sandbox_enabled(false);
chat.config.forced_auto_mode_downgraded_on_windows = true;
@@ -1834,7 +1839,7 @@ fn startup_prompts_for_windows_sandbox_when_agent_requested() {
#[test]
fn model_reasoning_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
set_chatgpt_auth(&mut chat);
chat.config.model = "gpt-5.1-codex-max".to_string();
@@ -1849,7 +1854,7 @@ fn model_reasoning_selection_popup_snapshot() {
#[test]
fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
set_chatgpt_auth(&mut chat);
chat.config.model = "gpt-5.1-codex-max".to_string();
@@ -1864,7 +1869,7 @@ fn model_reasoning_selection_popup_extra_high_warning_snapshot() {
#[test]
fn reasoning_popup_shows_extra_high_with_space() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
set_chatgpt_auth(&mut chat);
chat.config.model = "gpt-5.1-codex-max".to_string();
@@ -1885,7 +1890,7 @@ fn reasoning_popup_shows_extra_high_with_space() {
#[test]
fn single_reasoning_option_skips_selection() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let single_effort = vec![ReasoningEffortPreset {
effort: ReasoningEffortConfig::High,
@@ -1925,7 +1930,7 @@ fn single_reasoning_option_skips_selection() {
#[test]
fn feedback_selection_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Open the feedback category selection popup via slash command.
chat.dispatch_command(SlashCommand::Feedback);
@@ -1936,7 +1941,7 @@ fn feedback_selection_popup_snapshot() {
#[test]
fn feedback_upload_consent_popup_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Open the consent popup directly for a chosen category.
chat.open_feedback_consent(crate::app_event::FeedbackCategory::Bug);
@@ -1947,7 +1952,7 @@ fn feedback_upload_consent_popup_snapshot() {
#[test]
fn reasoning_popup_escape_returns_to_model_popup() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.config.model = "gpt-5.1".to_string();
chat.open_model_popup();
@@ -1967,7 +1972,7 @@ fn reasoning_popup_escape_returns_to_model_popup() {
#[test]
fn exec_history_extends_previous_when_consecutive() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// 1) Start "ls -la" (List)
let begin_ls = begin_exec(&mut chat, "call-ls", "ls -la");
@@ -1998,7 +2003,7 @@ fn exec_history_extends_previous_when_consecutive() {
#[test]
fn user_shell_command_renders_output_not_exploring() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let begin_ls = begin_exec_with_source(
&mut chat,
@@ -2021,7 +2026,7 @@ fn user_shell_command_renders_output_not_exploring() {
#[test]
fn disabled_slash_command_while_task_running_snapshot() {
// Build a chat widget and simulate an active task
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.bottom_pane.set_task_running(true);
// Dispatch a command that is unavailable while a task runs (e.g., /model)
@@ -2045,7 +2050,7 @@ fn disabled_slash_command_while_task_running_snapshot() {
#[test]
fn approval_modal_exec_snapshot() {
// Build a chat widget with manual channels to avoid spawning the agent.
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Ensure policy allows surfacing approvals explicitly (not strictly required for direct event).
chat.config.approval_policy = AskForApproval::OnRequest;
// Inject an exec approval request to display the approval modal.
@@ -2100,7 +2105,7 @@ fn approval_modal_exec_snapshot() {
// Ensures spacing looks correct when no reason text is provided.
#[test]
fn approval_modal_exec_without_reason_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.config.approval_policy = AskForApproval::OnRequest;
let ev = ExecApprovalRequestEvent {
@@ -2139,7 +2144,7 @@ fn approval_modal_exec_without_reason_snapshot() {
// Snapshot test: patch approval modal
#[test]
fn approval_modal_patch_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.config.approval_policy = AskForApproval::OnRequest;
// Build a small changeset and a reason/grant_root to exercise the prompt text.
@@ -2178,7 +2183,7 @@ fn approval_modal_patch_snapshot() {
#[test]
fn interrupt_restores_queued_messages_into_composer() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None);
// Simulate a running task to enable queuing of user inputs.
chat.bottom_pane.set_task_running(true);
@@ -2217,7 +2222,7 @@ fn interrupt_restores_queued_messages_into_composer() {
#[test]
fn interrupt_prepends_queued_messages_before_existing_composer_text() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None);
chat.bottom_pane.set_task_running(true);
chat.bottom_pane
@@ -2255,7 +2260,7 @@ fn interrupt_prepends_queued_messages_before_existing_composer_text() {
fn ui_snapshots_small_heights_idle() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (chat, _rx, _op_rx) = make_chatwidget_manual();
let (chat, _rx, _op_rx) = make_chatwidget_manual(None);
for h in [1u16, 2, 3] {
let name = format!("chat_small_idle_h{h}");
let mut terminal = Terminal::new(TestBackend::new(40, h)).expect("create terminal");
@@ -2272,7 +2277,7 @@ fn ui_snapshots_small_heights_idle() {
fn ui_snapshots_small_heights_task_running() {
use ratatui::Terminal;
use ratatui::backend::TestBackend;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Activate status line
chat.handle_codex_event(Event {
id: "task-1".into(),
@@ -2303,7 +2308,7 @@ fn ui_snapshots_small_heights_task_running() {
fn status_widget_and_approval_modal_snapshot() {
use codex_core::protocol::ExecApprovalRequestEvent;
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Begin a running task so the status indicator would be active.
chat.handle_codex_event(Event {
id: "task-1".into(),
@@ -2356,7 +2361,7 @@ fn status_widget_and_approval_modal_snapshot() {
// Ensures the VT100 rendering of the status indicator is stable when active.
#[test]
fn status_widget_active_snapshot() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Activate the status indicator by simulating a task start.
chat.handle_codex_event(Event {
id: "task-1".into(),
@@ -2383,7 +2388,7 @@ fn status_widget_active_snapshot() {
#[test]
fn background_event_updates_status_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "bg-1".into(),
@@ -2399,7 +2404,7 @@ fn background_event_updates_status_header() {
#[test]
fn apply_patch_events_emit_history_cells() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// 1) Approval request -> proposed patch summary cell
let mut changes = HashMap::new();
@@ -2497,7 +2502,7 @@ fn apply_patch_events_emit_history_cells() {
#[test]
fn apply_patch_manual_approval_adjusts_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let mut proposed_changes = HashMap::new();
proposed_changes.insert(
@@ -2546,7 +2551,7 @@ fn apply_patch_manual_approval_adjusts_header() {
#[test]
fn apply_patch_manual_flow_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let mut proposed_changes = HashMap::new();
proposed_changes.insert(
@@ -2599,7 +2604,7 @@ fn apply_patch_manual_flow_snapshot() {
#[test]
fn apply_patch_approval_sends_op_with_submission_id() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Simulate receiving an approval request with a distinct submission id and call id
let mut changes = HashMap::new();
changes.insert(
@@ -2638,7 +2643,7 @@ fn apply_patch_approval_sends_op_with_submission_id() {
#[test]
fn apply_patch_full_flow_integration_like() {
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(None);
// 1) Backend requests approval
let mut changes = HashMap::new();
@@ -2716,7 +2721,7 @@ fn apply_patch_full_flow_integration_like() {
#[test]
fn apply_patch_untrusted_shows_approval_modal() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
// Ensure approval policy is untrusted (OnRequest)
chat.config.approval_policy = AskForApproval::OnRequest;
@@ -2761,7 +2766,7 @@ fn apply_patch_untrusted_shows_approval_modal() {
#[test]
fn apply_patch_request_shows_diff_summary() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Ensure we are in OnRequest so an approval is surfaced
chat.config.approval_policy = AskForApproval::OnRequest;
@@ -2827,7 +2832,7 @@ fn apply_patch_request_shows_diff_summary() {
#[test]
fn plan_update_renders_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
let update = UpdatePlanArgs {
explanation: Some("Adapting plan".to_string()),
plan: vec![
@@ -2863,7 +2868,7 @@ fn plan_update_renders_history_cell() {
#[test]
fn stream_error_updates_status_indicator() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.bottom_pane.set_task_running(true);
let msg = "Reconnecting... 2/5";
chat.handle_codex_event(Event {
@@ -2888,7 +2893,7 @@ fn stream_error_updates_status_indicator() {
#[test]
fn warning_event_adds_warning_history_cell() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "sub-1".into(),
msg: EventMsg::Warning(WarningEvent {
@@ -2907,7 +2912,7 @@ fn warning_event_adds_warning_history_cell() {
#[test]
fn stream_recovery_restores_previous_status_header() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "task".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
@@ -2940,7 +2945,7 @@ fn stream_recovery_restores_previous_status_header() {
#[test]
fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Begin turn
chat.handle_codex_event(Event {
@@ -2994,7 +2999,7 @@ fn multiple_agent_messages_in_single_turn_emit_multiple_headers() {
#[test]
fn final_reasoning_then_message_without_deltas_are_rendered() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// No deltas; only final reasoning followed by final message.
chat.handle_codex_event(Event {
@@ -3021,7 +3026,7 @@ fn final_reasoning_then_message_without_deltas_are_rendered() {
#[test]
fn deltas_then_same_final_message_are_rendered_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Stream some reasoning deltas first.
chat.handle_codex_event(Event {
@@ -3085,7 +3090,7 @@ fn deltas_then_same_final_message_are_rendered_snapshot() {
// then the exec block, another blank line, the status line, a blank line, and the composer.
#[test]
fn chatwidget_exec_and_status_layout_vt100_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "t1".into(),
msg: EventMsg::AgentMessage(AgentMessageEvent { message: "Im going to search the repo for where “Change Approved” is rendered to update that view.".into() }),
@@ -3177,7 +3182,7 @@ fn chatwidget_exec_and_status_layout_vt100_snapshot() {
// E2E vt100 snapshot for complex markdown with indented and nested fenced code blocks
#[test]
fn chatwidget_markdown_code_blocks_vt100_snapshot() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None);
// Simulate a final agent message via streaming deltas instead of a single message
@@ -3268,7 +3273,7 @@ printf 'fenced within fenced\n'
#[test]
fn chatwidget_tall() {
let (mut chat, _rx, _op_rx) = make_chatwidget_manual();
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(None);
chat.handle_codex_event(Event {
id: "t1".into(),
msg: EventMsg::TaskStarted(TaskStartedEvent {
+5 -1
View File
@@ -7,6 +7,7 @@ use chrono::DateTime;
use chrono::Local;
use codex_common::create_config_summary_entries;
use codex_core::config::Config;
use codex_core::openai_models::model_family::ModelFamily;
use codex_core::protocol::SandboxPolicy;
use codex_core::protocol::TokenUsage;
use codex_protocol::ConversationId;
@@ -70,6 +71,7 @@ struct StatusHistoryCell {
pub(crate) fn new_status_output(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
@@ -81,6 +83,7 @@ pub(crate) fn new_status_output(
let card = StatusHistoryCell::new(
config,
auth_manager,
model_family,
total_usage,
context_usage,
session_id,
@@ -97,6 +100,7 @@ impl StatusHistoryCell {
fn new(
config: &Config,
auth_manager: &AuthManager,
model_family: &ModelFamily,
total_usage: &TokenUsage,
context_usage: Option<&TokenUsage>,
session_id: &Option<ConversationId>,
@@ -119,7 +123,7 @@ impl StatusHistoryCell {
let agents_summary = compose_agents_summary(config);
let account = compose_account_display(auth_manager, plan_type);
let session_id = session_id.as_ref().map(std::string::ToString::to_string);
let context_window = config.model_context_window.and_then(|window| {
let context_window = model_family.context_window.and_then(|window| {
context_usage.map(|usage| StatusContextWindowData {
percent_remaining: usage.percent_of_context_window_remaining(window),
tokens_in_context: usage.tokens_in_context_window(),
+35
View File
@@ -8,6 +8,8 @@ use codex_core::AuthManager;
use codex_core::config::Config;
use codex_core::config::ConfigOverrides;
use codex_core::config::ConfigToml;
use codex_core::openai_models::model_family::ModelFamily;
use codex_core::openai_models::models_manager::ModelsManager;
use codex_core::protocol::CreditsSnapshot;
use codex_core::protocol::RateLimitSnapshot;
use codex_core::protocol::RateLimitWindow;
@@ -37,6 +39,10 @@ fn test_auth_manager(config: &Config) -> AuthManager {
)
}
fn test_model_family(config: &Config) -> ModelFamily {
ModelsManager::construct_model_family_offline(config.model.as_str(), config)
}
fn render_lines(lines: &[Line<'static>]) -> Vec<String> {
lines
.iter()
@@ -124,9 +130,12 @@ fn status_snapshot_includes_reasoning_details() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -177,9 +186,11 @@ fn status_snapshot_includes_monthly_limit() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -218,9 +229,11 @@ fn status_snapshot_shows_unlimited_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -258,9 +271,11 @@ fn status_snapshot_shows_positive_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -298,9 +313,11 @@ fn status_snapshot_hides_zero_credits() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -336,9 +353,11 @@ fn status_snapshot_hides_when_has_no_credits_flag() {
plan_type: None,
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -374,9 +393,11 @@ fn status_card_token_usage_excludes_cached_tokens() {
.single()
.expect("timestamp");
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -427,9 +448,11 @@ fn status_snapshot_truncates_in_narrow_terminal() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -469,9 +492,11 @@ fn status_snapshot_shows_missing_limits_message() {
.single()
.expect("timestamp");
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -529,9 +554,11 @@ fn status_snapshot_includes_credits_and_limits() {
};
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -577,9 +604,11 @@ fn status_snapshot_shows_empty_limits_message() {
.expect("timestamp");
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -634,9 +663,11 @@ fn status_snapshot_shows_stale_limits_message() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -695,9 +726,11 @@ fn status_snapshot_cached_limits_hide_credits_without_flag() {
let rate_display = rate_limit_snapshot_display(&snapshot, captured_at);
let now = captured_at + ChronoDuration::minutes(20);
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&usage,
Some(&usage),
&None,
@@ -742,9 +775,11 @@ fn status_context_window_uses_last_usage() {
.single()
.expect("timestamp");
let model_family = test_model_family(&config);
let composite = new_status_output(
&config,
&auth_manager,
&model_family,
&total_usage,
Some(&last_usage),
&None,