From bb64b894fd67158be0f874872e6d7bfe717e7368 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Thu, 15 Oct 2026 03:42:31 +0000 Subject: [PATCH] Retire worktree and voice feature flags (#54175) ## What changed - Remove feature-flag checks from managed worktree creation and repository-aware session discovery, while retaining local workspace and trust checks. - Make TUI voice availability follow platform support instead of `features.realtime_conversation`. - Ignore `features.worktrees` and `features.realtime_conversation` configuration overrides, warn when managed requirements reference them, and hide both flags from `/experimental`, including catalogs from older servers. ## Testing Add coverage for ignored configuration overrides and managed requirements, and update session filtering, worktree grouping, and experimental feature discovery tests for the retired flags. GitOrigin-RevId: 1d64e890e6e2f8a78926a9ea163126c0890b068e --- codex-rs/core/src/config/config_tests.rs | 9 ++ codex-rs/core/src/config/managed_features.rs | 9 ++ codex-rs/exec/src/lib.rs | 5 - codex-rs/exec/tests/suite/worktree.rs | 42 ++---- codex-rs/features/src/lib.rs | 10 +- codex-rs/features/src/tests.rs | 19 +++ codex-rs/tui/src/app/agent_center_tests.rs | 4 +- codex-rs/tui/src/app/agents_overview.rs | 9 +- codex-rs/tui/src/app/agents_overview_new.rs | 10 +- codex-rs/tui/src/app/agents_overview_tests.rs | 51 +++---- .../tui/src/app/managed_worktree_creation.rs | 7 +- ...mmand_center__live_center_custom_help.snap | 14 +- .../tests/background_task_defaults_tests.rs | 3 - .../app/tests/session_lifecycle_requests.rs | 1 - codex-rs/tui/src/app/tests/turn_submission.rs | 33 ----- codex-rs/tui/src/app_server_session.rs | 5 +- .../bottom_pane/experimental_features_view.rs | 7 +- .../experimental_features_view_tests.rs | 26 +--- ..._view__tests__voice_runtime_available.snap | 14 -- codex-rs/tui/src/chatwidget/realtime.rs | 6 - .../src/chatwidget/realtime_settings_tests.rs | 4 - .../chatwidget/realtime_tests/lifecycle.rs | 24 ---- codex-rs/tui/src/chatwidget/session_flow.rs | 3 +- codex-rs/tui/src/chatwidget/settings.rs | 23 +--- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 3 +- .../chatwidget/tests/popups_and_settings.rs | 2 +- .../src/chatwidget/tests/slash_commands.rs | 1 - .../chatwidget/tests/worktree_picker_tests.rs | 14 +- .../tui/src/chatwidget/worktree_picker.rs | 12 +- codex-rs/tui/src/daemon_startup.rs | 3 +- codex-rs/tui/src/lib.rs | 47 +------ .../tui/src/managed_worktree_tools_tests.rs | 3 +- codex-rs/tui/src/resume_picker.rs | 125 +++++++----------- .../tui/src/resume_picker/page_loading.rs | 4 +- .../src/resume_picker/page_loading_tests.rs | 5 - ...ps__tests__experimental_voice_tooltip.snap | 6 - codex-rs/tui/src/tooltips.rs | 59 +-------- codex-rs/tui/src/worktree_startup.rs | 5 - 38 files changed, 166 insertions(+), 461 deletions(-) delete mode 100644 codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap delete mode 100644 codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__experimental_voice_tooltip.snap diff --git a/codex-rs/core/src/config/config_tests.rs b/codex-rs/core/src/config/config_tests.rs index aaf2165a6b..5826e93c01 100644 --- a/codex-rs/core/src/config/config_tests.rs +++ b/codex-rs/core/src/config/config_tests.rs @@ -11909,6 +11909,8 @@ shell_tool = false #[test_case::test_case(Feature::Personality; "personality")] #[test_case::test_case(Feature::GuardianThreadContext; "guardian thread context")] +#[test_case::test_case(Feature::Worktrees; "worktrees")] +#[test_case::test_case(Feature::RealtimeConversation; "realtime conversation")] #[test_case::test_case(Feature::ShellZshFork; "shell zsh fork")] #[test_case::test_case(Feature::UnifiedExecZshFork; "unified exec zsh fork")] fn retired_feature_requirements_do_not_pin_configured_values( @@ -11960,6 +11962,13 @@ fn retired_feature_requirements_do_not_pin_configured_values( usize::from(feature != Feature::Personality), ), ); + if matches!(feature, Feature::Worktrees | Feature::RealtimeConversation) { + assert!( + warnings[0].contains(&format!("Ignoring removed `features` requirement `{key}`")) + ); + assert!(warnings[0].contains("enterprise")); + assert!(warnings[0].contains("availability no longer depends on this feature flag")); + } } Ok(()) diff --git a/codex-rs/core/src/config/managed_features.rs b/codex-rs/core/src/config/managed_features.rs index a6519b2a44..dc57ca9216 100644 --- a/codex-rs/core/src/config/managed_features.rs +++ b/codex-rs/core/src/config/managed_features.rs @@ -243,6 +243,15 @@ fn parse_feature_requirements( if key == Feature::Personality.key() { continue; } + if key == Feature::Worktrees.key() || key == Feature::RealtimeConversation.key() { + push_feature_requirement_warning( + &mut startup_warnings, + format!( + "Ignoring removed `features` requirement `{key}` from {source}; availability no longer depends on this feature flag." + ), + ); + continue; + } if matches!(key.as_str(), "shell_zsh_fork" | "unified_exec_zsh_fork") { push_feature_requirement_warning( &mut startup_warnings, diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 4ce2d52855..8d3fcd8861 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -421,11 +421,6 @@ pub async fn run_main(cli: Cli, arg0_paths: Arg0DispatchPaths) -> anyhow::Result .cloud_config_bundle(gate_cloud_config.clone()) .build() .await?; - if !gate_config.features.enabled(Feature::Worktrees) { - anyhow::bail!( - "--worktree requires the worktrees feature; enable it with --enable worktrees" - ); - } if let Some(ExecCommand::Fork(args)) = command.as_mut() { let saved_cwd = worktree::fork_source( args, diff --git a/codex-rs/exec/tests/suite/worktree.rs b/codex-rs/exec/tests/suite/worktree.rs index d2c7afd5d9..94b8492089 100644 --- a/codex-rs/exec/tests/suite/worktree.rs +++ b/codex-rs/exec/tests/suite/worktree.rs @@ -407,35 +407,17 @@ fn worktree_rejects_remote_defaults_before_allocation() -> anyhow::Result<()> { } #[test] -fn worktree_rejects_disabled_features_and_ignored_config_before_allocation() -> anyhow::Result<()> { - for (config, args, expected) in [ - ( - "", - vec!["--ignore-user-config", "prompt"], - "--worktree cannot be combined with --ignore-user-config", - ), - ( - "features.worktrees = false", - vec!["prompt"], - "--enable worktrees", - ), - ( - "features.worktrees = false", - vec!["fork", "missing"], - "--enable worktrees", - ), - ( - "features.worktrees = true", - vec!["-c", "features.worktrees=false", "prompt"], - "--enable worktrees", - ), - ] { - let test = test_codex_exec(); - fs::write(test.home_path().join("config.toml"), config)?; - let output = test.cmd().arg("--worktree").args(args).output()?; - assert!(!output.status.success()); - assert!(String::from_utf8_lossy(&output.stderr).contains(expected)); - assert!(!test.home_path().join("worktrees").exists()); - } +fn worktree_rejects_ignored_config_before_allocation() -> anyhow::Result<()> { + let test = test_codex_exec(); + let output = test + .cmd() + .args(["--worktree", "--ignore-user-config", "prompt"]) + .output()?; + assert!(!output.status.success()); + assert!( + String::from_utf8_lossy(&output.stderr) + .contains("--worktree cannot be combined with --ignore-user-config") + ); + assert!(!test.home_path().join("worktrees").exists()); Ok(()) } diff --git a/codex-rs/features/src/lib.rs b/codex-rs/features/src/lib.rs index 5a3cc5f627..4030e8e4a6 100644 --- a/codex-rs/features/src/lib.rs +++ b/codex-rs/features/src/lib.rs @@ -214,7 +214,7 @@ pub enum Feature { NetworkProxy, /// Mask credentials and inject them through an already enabled network proxy. CredentialMasking, - /// Enable managed worktree creation and repository-aware sessions. + /// Removed compatibility flag. Managed worktrees and repository-aware sessions are always available. Worktrees, /// Respect host system proxy settings for Codex-owned network clients. RespectSystemProxy, @@ -416,7 +416,7 @@ pub enum Feature { UltrafastMode, /// Enable explicitly requested model changes for later step captures. StepModelSwitching, - /// Enable voice conversations in the TUI. + /// Removed compatibility flag. TUI voice availability follows platform support. RealtimeConversation, /// Prevent idle system sleep while a turn is actively running. PreventIdleSleep, @@ -667,7 +667,7 @@ impl Features { self.record_legacy_usage_force("features.transcript_v2", Feature::TranscriptV2); continue; } - "tui_app_server" => { + "tui_app_server" | "worktrees" | "realtime_conversation" => { continue; } "undo" => { @@ -1453,7 +1453,7 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::Worktrees, key: "worktrees", - stage: Stage::Stable, + stage: Stage::Removed, default_enabled: true, }, FeatureSpec { @@ -1999,7 +1999,7 @@ pub const FEATURES: &[FeatureSpec] = &[ FeatureSpec { id: Feature::RealtimeConversation, key: "realtime_conversation", - stage: Stage::Stable, + stage: Stage::Removed, default_enabled: true, }, FeatureSpec { diff --git a/codex-rs/features/src/tests.rs b/codex-rs/features/src/tests.rs index a8a8fca058..0fdfebc50a 100644 --- a/codex-rs/features/src/tests.rs +++ b/codex-rs/features/src/tests.rs @@ -901,3 +901,22 @@ code_mode = true message ); } + +#[test] +fn retired_worktree_and_voice_config_overrides_are_ignored() { + for enabled in [false, true] { + let features_toml = FeaturesToml::from(BTreeMap::from([ + ("worktrees".to_string(), enabled), + ("realtime_conversation".to_string(), enabled), + ])); + let features = Features::from_sources( + FeatureConfigSource { + features: Some(&features_toml), + ..Default::default() + }, + FeatureConfigSource::default(), + FeatureOverrides::default(), + ); + assert_eq!(features, Features::with_defaults()); + } +} diff --git a/codex-rs/tui/src/app/agent_center_tests.rs b/codex-rs/tui/src/app/agent_center_tests.rs index e8c3022b84..952a4d687d 100644 --- a/codex-rs/tui/src/app/agent_center_tests.rs +++ b/codex-rs/tui/src/app/agent_center_tests.rs @@ -513,8 +513,7 @@ async fn live_center_rename_retains_target_when_status_leaves_filter() -> Result #[tokio::test] async fn live_center_navigation_and_complete_hints() { - let mut app = make_test_app().await; - app.config.features.enable(Feature::Worktrees).unwrap(); + let app = make_test_app().await; let ready = ThreadId::from_u128(/*value*/ 42); let needs_you = ThreadId::from_u128(/*value*/ 43); let unloaded = ThreadId::from_u128(/*value*/ 44); @@ -665,7 +664,6 @@ async fn live_center_fixed_shortcuts_yield_to_configured_actions() { #[tokio::test] async fn live_center_hints_follow_configured_bindings() { let mut app = make_test_app().await; - app.config.features.disable(Feature::Worktrees).unwrap(); let config: TuiKeymap = toml::from_str( r#" [list] diff --git a/codex-rs/tui/src/app/agents_overview.rs b/codex-rs/tui/src/app/agents_overview.rs index 00f0633861..8701235d2f 100644 --- a/codex-rs/tui/src/app/agents_overview.rs +++ b/codex-rs/tui/src/app/agents_overview.rs @@ -406,11 +406,10 @@ impl App { let mut view = AgentsOverviewView::new( rows, selected_thread_id, - self.config.features.enabled(Feature::Worktrees) - && !crate::uses_remote_workspace_or_environment( - &self.app_server_target, - self.environment_manager.as_ref(), - ), + !crate::uses_remote_workspace_or_environment( + &self.app_server_target, + self.environment_manager.as_ref(), + ), self.local_settings.tui.status_line_use_colors, self.app_event_tx.clone(), self.keymap.clone(), diff --git a/codex-rs/tui/src/app/agents_overview_new.rs b/codex-rs/tui/src/app/agents_overview_new.rs index 0072408f42..1b96018362 100644 --- a/codex-rs/tui/src/app/agents_overview_new.rs +++ b/codex-rs/tui/src/app/agents_overview_new.rs @@ -260,12 +260,10 @@ impl App { { return; } - if !self.config.features.enabled(Feature::Worktrees) - || crate::uses_remote_workspace_or_environment( - &self.app_server_target, - self.environment_manager.as_ref(), - ) - { + if crate::uses_remote_workspace_or_environment( + &self.app_server_target, + self.environment_manager.as_ref(), + ) { self.add_agents_overview_error( "Managed worktrees require local worktree support.".to_string(), ); diff --git a/codex-rs/tui/src/app/agents_overview_tests.rs b/codex-rs/tui/src/app/agents_overview_tests.rs index b28efb8d17..76f5a2e433 100644 --- a/codex-rs/tui/src/app/agents_overview_tests.rs +++ b/codex-rs/tui/src/app/agents_overview_tests.rs @@ -1132,7 +1132,7 @@ async fn agents_overview_reasoning_uses_existing_events_and_expires_with_attachm } #[tokio::test] -async fn worktrees_overview_grouping_requires_feature() { +async fn worktrees_overview_groups_linked_checkouts() { let mut app = make_test_app().await; let root = tempfile::tempdir().unwrap(); let primary = root.path().join("primary"); @@ -1177,32 +1177,24 @@ async fn worktrees_overview_grouping_requires_feature() { let display_heading = &headings[0]; let grouped_heading = format!("{display_heading} 2"); let width = 160; - for enabled in [false, true] { - app.config - .features - .set_enabled(Feature::Worktrees, enabled) - .unwrap(); - let view = app.agents_overview_view(threads.clone(), /*selected_thread_id*/ None); - app.chat_widget.show_bottom_pane_view(Box::new(view)); - let popup = render_bottom_popup(&app.chat_widget, width); - assert_eq!(popup.contains(&grouped_heading), enabled, "{popup}"); - if enabled { - let grouping = popup - .lines() - .map(|line| line.split('│').next().unwrap_or(line).trim_end()) - .collect::>() - .join("\n") - .replace(&headings[1], "/tmp/worktree-root/primary/subdir/child") - .replace(display_heading, "/tmp/worktree-root/primary") - .replace('\\', "/"); - insta::with_settings!({snapshot_path => "../snapshots"}, { - insta::assert_snapshot!( - "agents_overview_worktree_grouping", - normalize_agent_center_snapshot(grouping) - ); - }); - } - } + let view = app.agents_overview_view(threads, /*selected_thread_id*/ None); + app.chat_widget.show_bottom_pane_view(Box::new(view)); + let popup = render_bottom_popup(&app.chat_widget, width); + assert!(popup.contains(&grouped_heading), "{popup}"); + let grouping = popup + .lines() + .map(|line| line.split('│').next().unwrap_or(line).trim_end()) + .collect::>() + .join("\n") + .replace(&headings[1], "/tmp/worktree-root/primary/subdir/child") + .replace(display_heading, "/tmp/worktree-root/primary") + .replace('\\', "/"); + insta::with_settings!({snapshot_path => "../snapshots"}, { + insta::assert_snapshot!( + "agents_overview_worktree_grouping", + normalize_agent_center_snapshot(grouping) + ); + }); } #[tokio::test] @@ -3024,7 +3016,6 @@ async fn overview_grouping_persists_across_config_reloads() -> Result<()> { #[tokio::test] async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone() { let mut app = make_test_app().await; - app.config.features.enable(Feature::Worktrees).unwrap(); let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); app.app_event_tx = AppEventSender::new(tx); let id = ThreadId::new(); @@ -3077,8 +3068,4 @@ async fn command_center_new_actions_use_selection_and_leave_metadata_text_alone( rx.try_recv(), Ok(AppEvent::NewAgentsOverviewSession { cwd: None }) )); - app.config.features.disable(Feature::Worktrees).unwrap(); - let mut disabled = app.agents_overview_view(vec![target], Some(id)); - disabled.handle_key_event(KeyCode::Char('w').into()); - assert!(rx.try_recv().is_err()); } diff --git a/codex-rs/tui/src/app/managed_worktree_creation.rs b/codex-rs/tui/src/app/managed_worktree_creation.rs index 3fd35cdf7b..7b53366557 100644 --- a/codex-rs/tui/src/app/managed_worktree_creation.rs +++ b/codex-rs/tui/src/app/managed_worktree_creation.rs @@ -64,11 +64,7 @@ impl App { mode: crate::app_event::ManagedWorktreeMode, name: Option, ) { - if !self.config.features.enabled(Feature::Worktrees) { - self.chat_widget.add_error_message( - "Enable worktrees in your Codex configuration to create a worktree.".to_string(), - ); - } else if self.config.active_project.is_untrusted() { + if self.config.active_project.is_untrusted() { self.chat_widget.add_error_message( "Cannot create a worktree from an explicitly untrusted source.".to_string(), ); @@ -230,7 +226,6 @@ impl App { }; let can_continue = !self.reconnect.offline && !self.chat_widget.has_misalignment_policy_violation() - && self.config.features.enabled(Feature::Worktrees) && !crate::uses_remote_workspace_or_environment( &self.app_server_target, self.environment_manager.as_ref(), diff --git a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap index ff08930897..63aae63f5f 100644 --- a/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap +++ b/codex-rs/tui/src/app/snapshots/codex_tui__app__agents_overview__tests__command_center__live_center_custom_help.snap @@ -7,12 +7,13 @@ expression: "screen(&view, 100, 24)" ──────────────────────────────────────────────────────────────────────────────────────────────── Task shortcuts - Navigate Tasks View - k Up n New tab/⇧tab Filter - j Down o Resume f3 Search - enter Open f Fork g Group - pgdn Page down z r Rename - ⌃c Quit x Stop + Navigate Tasks View + k Up n New tab/⇧tab Filter + j Down w New worktree f3 Search + enter Open o Resume g Group + pgdn Page down f Fork + ⌃c Quit z r Rename + x Stop h Hide backspace Delete z p Pin/unpin @@ -24,5 +25,4 @@ expression: "screen(&view, 100, 24)" - esc back diff --git a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs index 438ce7605e..72e79e241a 100644 --- a/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs +++ b/codex-rs/tui/src/app/tests/background_task_defaults_tests.rs @@ -1166,9 +1166,6 @@ async fn command_center_new_checkout_and_worktree_preserve_source_and_default_br app.config.cwd = selected.cwd.clone().abs(); app.chat_widget.windows_sandbox_local_server = cfg!(target_os = "windows"); app.harness_overrides.cwd = Some(selected.cwd.clone()); - app.cli_kv_overrides - .push(("features.worktrees".into(), TomlValue::Boolean(true))); - app.config.features.enable(Feature::Worktrees)?; trust_launch_folder(&mut app); let (mut server, requests, proxy) = start_recording_app_server_with_history( &app.config, diff --git a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs index 5f5d4615ad..9e6984c521 100644 --- a/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs +++ b/codex-rs/tui/src/app/tests/session_lifecycle_requests.rs @@ -1473,7 +1473,6 @@ async fn local_daemon_registers_approval_gated_mcp_tools_for_both_start_paths_sc -> Result<()> { let (mut app, events, _ops) = Box::pin(make_test_app_with_channels()).await; // Invalid optional worktree settings must preserve both daemon start paths. - app.config.features.enable(Feature::Worktrees)?; let codex_home = tempdir()?; app.config.codex_home = codex_home.path().to_path_buf().abs(); app.config.sqlite = SqliteConfig::new_for_testing(codex_home.path().abs()); diff --git a/codex-rs/tui/src/app/tests/turn_submission.rs b/codex-rs/tui/src/app/tests/turn_submission.rs index 15030ebae3..e04fc3da5d 100644 --- a/codex-rs/tui/src/app/tests/turn_submission.rs +++ b/codex-rs/tui/src/app/tests/turn_submission.rs @@ -11,43 +11,11 @@ use codex_protocol::permissions::FileSystemSpecialPath; use codex_protocol::permissions::NetworkSandboxPolicy; use pretty_assertions::assert_eq; -#[tokio::test] -async fn worktree_creation_event_requires_feature() -> Result<()> { - let (mut app, mut events, _op_rx) = make_test_app_with_channels().await; - app.config.features.disable(Feature::Worktrees)?; - let mut tui = crate::tui::test_support::make_test_tui()?; - let mut app_server = Box::pin(crate::start_embedded_app_server_for_picker(&app.config)).await?; - while events.try_recv().is_ok() {} - app.handle_event( - &mut tui, - &mut app_server, - AppEvent::StartManagedWorktree { - mode: crate::app_event::ManagedWorktreeMode::New, - name: None, - }, - ) - .await?; - drain_managed_worktree_start(&mut app, &mut app_server).await; - let message = std::iter::from_fn(|| events.try_recv().ok()) - .find_map(|event| match event { - AppEvent::InsertHistoryCell(cell) => { - Some(lines_to_single_string(&cell.display_lines(/*width*/ 100))) - } - _ => None, - }) - .expect("disabled feature message"); - insta::assert_snapshot!(message, @"■ Enable worktrees in your Codex configuration to create a worktree."); - assert!(!app.config.codex_home.join("worktrees").exists()); - app_server.shutdown().await?; - Ok(()) -} - #[tokio::test] async fn worktree_creation_rejects_untrusted_source_before_allocation() -> Result<()> { use crate::app_event::ManagedWorktreeMode; let (mut app, mut events, _op_rx) = make_test_app_with_channels().await; - app.config.features.enable(Feature::Worktrees)?; app.config.active_project.trust_level = Some(codex_protocol::config_types::TrustLevel::Untrusted); let thread_id = ThreadId::new(); @@ -97,7 +65,6 @@ async fn worktree_creation_rejects_running_agent_before_allocation() -> Result<( let (mut app, mut events, _op_rx) = make_test_app_with_channels().await; let home = tempfile::tempdir()?; app.config.codex_home = home.path().to_path_buf().abs(); - app.config.features.enable(Feature::Worktrees)?; app.config.active_project.trust_level = Some(codex_protocol::config_types::TrustLevel::Trusted); let primary = ThreadId::new(); app.primary_thread_id = Some(primary); diff --git a/codex-rs/tui/src/app_server_session.rs b/codex-rs/tui/src/app_server_session.rs index ccf3767182..ba449c46f4 100644 --- a/codex-rs/tui/src/app_server_session.rs +++ b/codex-rs/tui/src/app_server_session.rs @@ -457,10 +457,7 @@ impl AppServerSession { app_event_tx: AppEventSender, status_updates: tokio::sync::broadcast::Sender, ) -> std::io::Result<()> { - let worktrees = if !self.uses_remote_workspace() - && !config.active_project.is_untrusted() - && config.features.enabled(codex_features::Feature::Worktrees) - { + let worktrees = if !self.uses_remote_workspace() && !config.active_project.is_untrusted() { // Listing a fresh, nonexistent thread is read-only and returns an empty page // on supported stores. Probe before advertising tools or allocating a checkout. let support = self diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs index 805e3eff38..681698d984 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view.rs @@ -1,6 +1,6 @@ //! Experimental controls with popup-owned discovery and configured enablement. //! Failed saves retain intent for explicit retry; cancellation remains available. -//! Server voice discovery is gated by the client package's native runtime. +//! Retired stable features stay hidden even if an older server reports them as beta. use crate::experimental_features::FeatureWriteResult; use codex_app_server_protocol::ExperimentalFeature; @@ -54,7 +54,6 @@ pub(crate) struct ExperimentalFeatureItem { pub(crate) struct ExperimentalFeaturesView { features: Vec, - voice_supported: bool, initial_enabled: Vec, unconfirmed: Vec, catalog_rx: Option, String>>>, @@ -84,7 +83,6 @@ impl ExperimentalFeaturesView { } .to_string(), thread_id, - voice_supported: codex_realtime_webrtc::RealtimeWebrtcSession::is_supported(), write_rx: None, catalog_rx, unconfirmed: Vec::new(), @@ -284,8 +282,7 @@ impl BottomPaneView for ExperimentalFeaturesView { let mut count = 0; for feature in features { if feature.stage != ExperimentalFeatureStage::Beta - || (feature.name == Feature::RealtimeConversation.key() - && !self.voice_supported) + || matches!(feature.name.as_str(), "worktrees" | "realtime_conversation") { continue; } diff --git a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs index 8b8f8a9a31..25a2351fb9 100644 --- a/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs +++ b/codex-rs/tui/src/bottom_pane/experimental_features_view_tests.rs @@ -103,6 +103,8 @@ fn experimental_features_discovery_preserves_server_order_and_accepts_new_keys() .send(Ok(vec![ server_feature("network_proxy"), server_feature("future_feature"), + server_feature("worktrees"), + server_feature("realtime_conversation"), stable, server_feature("prevent_idle_sleep"), ])) @@ -279,27 +281,3 @@ fn snapshot_view(name: &str, view: &ExperimentalFeaturesView) { view.render(area, &mut buffer); insta::assert_snapshot!(name, buffer_text(&buffer)); } - -#[test] -fn voice_discovery_requires_the_client_runtime() { - for supported in [false, true] { - let (app_tx, _app_rx) = tokio::sync::mpsc::unbounded_channel(); - let (catalog_tx, catalog_rx) = oneshot::channel(); - let mut view = ExperimentalFeaturesView::new( - Vec::new(), - ThreadId::new(), - Some(catalog_rx), - AppEventSender::new(app_tx), - crate::keymap::RuntimeKeymap::defaults().list, - ); - view.voice_supported = supported; - catalog_tx - .send(Ok(vec![server_feature("realtime_conversation")])) - .unwrap(); - assert!(view.pre_draw_tick(Instant::now())); - assert_eq!(view.features.len(), usize::from(supported)); - if supported { - snapshot_view("voice_runtime_available", &view); - } - } -} diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap deleted file mode 100644 index 34de22d1e4..0000000000 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__experimental_features_view__tests__voice_runtime_available.snap +++ /dev/null @@ -1,14 +0,0 @@ ---- -source: tui/src/bottom_pane/experimental_features_view_tests.rs -expression: buffer_text(&buffer) ---- - - Experimental features - Checked features are configured on. Some experimental features take effect - only in new tasks or after restarting the Codex server. - - -› [x] Server realtime_conversation Description supplied by the server. - - - space toggle · enter save · esc save/close diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index bb2a48e527..513f40276d 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -23,7 +23,6 @@ use crate::history_cell; use crate::motion::MotionMode; use codex_app_server_protocol::ThreadItem; use codex_app_server_protocol::UserInput; -use codex_features::Feature; use codex_protocol::ThreadId; use codex_protocol::models::MessagePhase; use codex_realtime_webrtc::RealtimeWebrtcSession; @@ -293,11 +292,6 @@ impl ChatWidget { return; } - if !self.config.features.enabled(Feature::RealtimeConversation) { - self.add_error_message("Voice conversations are not enabled.".to_string()); - return; - } - if !RealtimeWebrtcSession::is_supported() { self.add_error_message( "Voice requires macOS, an MSVC-based Windows build, or a glibc-based Linux build." diff --git a/codex-rs/tui/src/chatwidget/realtime_settings_tests.rs b/codex-rs/tui/src/chatwidget/realtime_settings_tests.rs index 2f2f5fed19..e9bcc6d5e6 100644 --- a/codex-rs/tui/src/chatwidget/realtime_settings_tests.rs +++ b/codex-rs/tui/src/chatwidget/realtime_settings_tests.rs @@ -11,10 +11,6 @@ use pretty_assertions::assert_eq; #[tokio::test] async fn voice_settings_selects_a_voice_without_starting_audio() { let (mut chat, _sender, mut events, mut ops) = make_chatwidget_manual_with_sender().await; - chat.config - .features - .enable(Feature::RealtimeConversation) - .unwrap(); chat.config.realtime.voice = Some(RealtimeVoice::Maple); chat.dispatch_command_with_args(SlashCommand::Voice, "settings".to_string(), Vec::new()); assert!( diff --git a/codex-rs/tui/src/chatwidget/realtime_tests/lifecycle.rs b/codex-rs/tui/src/chatwidget/realtime_tests/lifecycle.rs index bbeb21683c..a0bf879b70 100644 --- a/codex-rs/tui/src/chatwidget/realtime_tests/lifecycle.rs +++ b/codex-rs/tui/src/chatwidget/realtime_tests/lifecycle.rs @@ -4,30 +4,6 @@ use super::*; use crate::app_event::RealtimeWebrtcStartupFailure; use pretty_assertions::assert_eq; -#[tokio::test] -async fn enabling_voice_on_an_open_thread_snapshots_the_new_thread_notice() { - let (mut chat, _sender, mut events, _ops) = make_chatwidget_manual_with_sender().await; - chat.set_feature_enabled( - codex_features::Feature::RealtimeConversation, - /*enabled*/ true, - ); - commit_realtime_history_events(&mut chat, &mut events); - let rendered = std::iter::from_fn(|| events.try_recv().ok()) - .filter_map(|event| match event { - AppEvent::InsertHistoryCell(cell) => Some( - cell.display_lines(/*width*/ 80) - .into_iter() - .map(|line| line.to_string()) - .collect::>() - .join("\n"), - ), - _ => None, - }) - .collect::>() - .join("\n"); - insta::assert_snapshot!(rendered, @"• Voice conversations will be available in new threads."); -} - #[tokio::test] async fn voice_cannot_start_in_a_side_conversation() { let (mut chat, _sender, mut events, mut ops) = make_chatwidget_manual_with_sender().await; diff --git a/codex-rs/tui/src/chatwidget/session_flow.rs b/codex-rs/tui/src/chatwidget/session_flow.rs index 3c83e2b951..f34f76ebed 100644 --- a/codex-rs/tui/src/chatwidget/session_flow.rs +++ b/codex-rs/tui/src/chatwidget/session_flow.rs @@ -87,8 +87,7 @@ impl ChatWidget { }); } self.realtime_conversation_available_for_thread = - self.config.features.enabled(Feature::RealtimeConversation) - && codex_realtime_webrtc::RealtimeWebrtcSession::is_supported(); + codex_realtime_webrtc::RealtimeWebrtcSession::is_supported(); self.bottom_pane .set_voice_command_enabled(self.realtime_conversation_available_for_thread); self.bottom_pane diff --git a/codex-rs/tui/src/chatwidget/settings.rs b/codex-rs/tui/src/chatwidget/settings.rs index b0b9674c6a..767548773f 100644 --- a/codex-rs/tui/src/chatwidget/settings.rs +++ b/codex-rs/tui/src/chatwidget/settings.rs @@ -133,9 +133,6 @@ impl ChatWidget { self.refresh_effective_service_tier(); self.sync_service_tier_commands(); } - if feature == Feature::Worktrees { - self.sync_worktrees_enabled(); - } if feature == Feature::Plugins { self.sync_plugins_command_enabled(); self.refresh_plugin_mentions(); @@ -150,21 +147,6 @@ impl ChatWidget { self.update_collaboration_mode_indicator(); } } - if feature == Feature::RealtimeConversation && !enabled { - self.realtime_conversation_available_for_thread = false; - self.bottom_pane - .set_voice_command_enabled(/*enabled*/ false); - self.stop_realtime_conversation(); - } - if feature == Feature::RealtimeConversation - && enabled - && !self.realtime_conversation_available_for_thread - { - self.add_info_message( - "Voice conversations will be available in new threads.".into(), - /*hint*/ None, - ); - } if feature == Feature::MentionsV2 { self.sync_mentions_v2_enabled(); } @@ -353,9 +335,8 @@ impl ChatWidget { } pub(super) fn sync_worktrees_enabled(&mut self) { - self.bottom_pane.set_worktrees_enabled( - self.config.features.enabled(Feature::Worktrees) && self.local_worktree_operations, - ); + self.bottom_pane + .set_worktrees_enabled(self.local_worktree_operations); } pub(super) fn sync_plugins_command_enabled(&mut self) { diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 5327f805cf..79601b9a28 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -1247,8 +1247,7 @@ impl ChatWidget { service_tier_commands_enabled: !self.current_model_service_tier_commands().is_empty(), daybreak_command_description: self.daybreak_command_description(), voice_command_enabled: self.realtime_conversation_available_for_thread, - worktrees_enabled: self.config.features.enabled(Feature::Worktrees) - && self.local_worktree_operations, + worktrees_enabled: self.local_worktree_operations, allow_elevate_sandbox, side_conversation_active: self.active_side_conversation, } diff --git a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs index d64fecc640..c7a0223ed7 100644 --- a/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs +++ b/codex-rs/tui/src/chatwidget/tests/popups_and_settings.rs @@ -3106,7 +3106,7 @@ async fn experimental_features_popup_snapshot() { enabled: true, }, ExperimentalFeatureItem { - key: Feature::RealtimeConversation.key().to_string(), + key: "voice_conversations".to_string(), writable: true, name: "Voice conversations".to_string(), description: "Talk with Codex using /voice.".to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index f867a24d5c..49601e3ccd 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -680,7 +680,6 @@ async fn queued_unknown_slash_reports_error_when_dequeued() { ); assert!(chat.input_queue.queued_user_messages.is_empty()); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); chat.set_local_worktree_operations(/*enabled*/ false); let drain = chat.submit_queued_slash_prompt(UserMessage::from("/worktree extra").into()); assert_matches!(drain, QueueDrain::Continue); diff --git a/codex-rs/tui/src/chatwidget/tests/worktree_picker_tests.rs b/codex-rs/tui/src/chatwidget/tests/worktree_picker_tests.rs index 916fb1e8fd..5c7daae9a2 100644 --- a/codex-rs/tui/src/chatwidget/tests/worktree_picker_tests.rs +++ b/codex-rs/tui/src/chatwidget/tests/worktree_picker_tests.rs @@ -8,7 +8,7 @@ use pretty_assertions::assert_eq; #[tokio::test] async fn slash_new_and_fork_offer_checkout_choices_inside_local_git_repository() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ false); + chat.set_local_worktree_operations(/*enabled*/ false); let checkout = tempdir().expect("temporary checkout"); std::fs::create_dir(checkout.path().join(".git")).expect("git directory"); std::fs::write(checkout.path().join(".git/HEAD"), "ref: refs/heads/main\n").expect("git HEAD"); @@ -23,7 +23,7 @@ async fn slash_new_and_fork_offer_checkout_choices_inside_local_git_repository() chat.dispatch_command(SlashCommand::New); assert_matches!(rx.try_recv(), Ok(AppEvent::NewSession { name: None })); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); chat.dispatch_command(SlashCommand::Fork); let popup = render_bottom_popup(&chat, /*width*/ 80); @@ -63,7 +63,7 @@ async fn slash_new_and_fork_offer_checkout_choices_inside_local_git_repository() (false, "worktrees_command_remote"), (true, "worktrees_command_local"), ] { - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); chat.set_local_worktree_operations(available); chat.bottom_pane .set_composer_text("/work".into(), Vec::new(), Vec::new()); @@ -82,7 +82,7 @@ async fn slash_worktree_offers_current_or_new_conversation() { chat.config.cwd = AbsolutePathBuf::from_absolute_path(checkout.path()).expect("absolute checkout"); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); chat.dispatch_command(SlashCommand::Worktree); let popup = render_bottom_popup(&chat, /*width*/ 80); @@ -105,7 +105,7 @@ async fn worktree_browser_actions_and_stale_results() { std::fs::create_dir(checkout.path().join(".git")).unwrap(); std::fs::write(checkout.path().join(".git/HEAD"), "ref: refs/heads/main\n").unwrap(); chat.config.cwd = AbsolutePathBuf::from_absolute_path(checkout.path()).unwrap(); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); let request = chat.request_managed_worktrees().unwrap(); assert_chatwidget_snapshot!( "worktree_browser_loading", @@ -212,7 +212,7 @@ async fn worktree_browser_names_archived_and_missing_owners() { std::fs::create_dir(checkout.path().join(".git")).unwrap(); std::fs::write(checkout.path().join(".git/HEAD"), "ref: refs/heads/main\n").unwrap(); chat.config.cwd = AbsolutePathBuf::from_absolute_path(checkout.path()).unwrap(); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); let request = chat.request_managed_worktrees().unwrap(); let owner = ThreadId::from_string("00000000-0000-0000-0000-000000000002").unwrap(); chat.on_managed_worktrees_loaded( @@ -268,7 +268,7 @@ async fn worktree_picker_custom_keys_preserve_draft_and_protect_current_checkout std::fs::create_dir(checkout.path().join(".git")).unwrap(); std::fs::write(checkout.path().join(".git/HEAD"), "ref: refs/heads/main\n").unwrap(); chat.config.cwd = AbsolutePathBuf::from_absolute_path(checkout.path()).unwrap(); - chat.set_feature_enabled(Feature::Worktrees, /*enabled*/ true); + chat.set_local_worktree_operations(/*enabled*/ true); let mut keymap = crate::keymap::RuntimeKeymap::defaults(); keymap.list.accept = vec![key_hint::plain(KeyCode::F(/*n*/ 3))]; keymap.list.cancel = vec![key_hint::plain(KeyCode::F(/*n*/ 2))]; diff --git a/codex-rs/tui/src/chatwidget/worktree_picker.rs b/codex-rs/tui/src/chatwidget/worktree_picker.rs index 6ca63f0d8c..d40d2a8406 100644 --- a/codex-rs/tui/src/chatwidget/worktree_picker.rs +++ b/codex-rs/tui/src/chatwidget/worktree_picker.rs @@ -1,4 +1,4 @@ -//! Worktree choices for local, feature-enabled session commands. +//! Worktree choices for local session commands. //! //! Shared picker presentation preserves request identity and action safety checks. @@ -14,9 +14,7 @@ const BROWSER_VIEW_ID: &str = "managed-worktrees"; impl ChatWidget { pub(super) fn managed_worktree_available(&self) -> bool { - self.config.features.enabled(Feature::Worktrees) - && self.local_worktree_operations - && get_git_repo_root(self.config.cwd.as_path()).is_some() + self.local_worktree_operations && get_git_repo_root(self.config.cwd.as_path()).is_some() } pub(super) fn show_session_checkout_picker( @@ -85,12 +83,6 @@ impl ChatWidget { } pub(super) fn show_managed_worktree_picker(&mut self) { - if !self.config.features.enabled(Feature::Worktrees) { - self.add_error_message( - "Enable worktrees in your Codex configuration to create a worktree.".to_string(), - ); - return; - } if !self.managed_worktree_available() { self.add_error_message("Managed worktrees require a local Git repository.".to_string()); return; diff --git a/codex-rs/tui/src/daemon_startup.rs b/codex-rs/tui/src/daemon_startup.rs index 91c38d7837..9e25c09904 100644 --- a/codex-rs/tui/src/daemon_startup.rs +++ b/codex-rs/tui/src/daemon_startup.rs @@ -132,11 +132,12 @@ fn allowed_feature(name: &str) -> bool { matches!( name, // Client gates and per-thread settings already forwarded in thread requests. - "daemon_auto_start" | "worktrees" | "transcript_v2" | "realtime_conversation" | "standalone_web_search" + "daemon_auto_start" | "transcript_v2" | "standalone_web_search" // Shared services and threadless MCP operations need daemon compatibility checks. | "api_key_model_discovery" | "code_mode_host" | "auth_elicitation" | "mcp_oauth_refresh_coordination" // Removed flags still passed by older launch scripts. + | "worktrees" | "realtime_conversation" | "remote_models" | "request_rule" | "responses_websockets_v2" | "workspace_owner_usage_nudge" | "tool_search_always_defer_mcp_tools" | "remote_compaction_v2" | "multi_agent_mode" diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 7aa9aa80f0..e4ab86c347 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -850,7 +850,6 @@ async fn lookup_latest_session_target_with_app_server( .thread_list(latest_session_lookup_params( uses_remote_filesystem, model_provider.clone(), - config, cwd_filter, include_non_interactive, lookup_mode, @@ -886,7 +885,6 @@ enum LatestSessionLookupMode { fn latest_session_lookup_params( uses_remote_filesystem: bool, model_provider: Option, - config: &Config, cwd_filter: Option<&Path>, include_non_interactive: bool, lookup_mode: LatestSessionLookupMode, @@ -905,13 +903,8 @@ fn latest_session_lookup_params( project_id: None, parent_thread_id: None, ancestor_thread_id: None, - cwd: cwd_filter.map(|cwd| { - resume_picker::repository_cwd_filter( - cwd, - uses_remote_filesystem, - config.features.enabled(codex_features::Feature::Worktrees), - ) - }), + cwd: cwd_filter + .map(|cwd| resume_picker::repository_cwd_filter(cwd, uses_remote_filesystem)), use_state_db_only: match lookup_mode { LatestSessionLookupMode::StateDbOnly => true, LatestSessionLookupMode::ScanAndRepair => false, @@ -3334,13 +3327,11 @@ requires_openai_auth = {requires_openai_auth} async fn latest_session_lookup_params_keep_local_filters_for_embedded_sessions() -> std::io::Result<()> { let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; let cwd = temp_dir.path().join("project"); let params = latest_session_lookup_params( /*uses_remote_filesystem*/ false, /*model_provider*/ None, - &config, Some(cwd.as_path()), /*include_non_interactive*/ false, LatestSessionLookupMode::StateDbOnly, @@ -3356,7 +3347,6 @@ requires_openai_auth = {requires_openai_auth} let scan_params = latest_session_lookup_params( /*uses_remote_filesystem*/ false, /*model_provider*/ None, - &config, Some(cwd.as_path()), /*include_non_interactive*/ false, LatestSessionLookupMode::ScanAndRepair, @@ -3368,12 +3358,10 @@ requires_openai_auth = {requires_openai_auth} #[tokio::test] async fn latest_session_lookup_params_honor_explicit_provider() -> color_eyre::Result<()> { let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; let cwd = temp_dir.path().join("project"); let params = latest_session_lookup_params( /*uses_remote_filesystem*/ false, Some("selected-provider".to_string()), - &config, Some(cwd.as_path()), /*include_non_interactive*/ false, LatestSessionLookupMode::StateDbOnly, @@ -3393,13 +3381,9 @@ requires_openai_auth = {requires_openai_auth} #[tokio::test] async fn latest_session_lookup_params_omit_local_filters_for_remote_sessions() -> std::io::Result<()> { - let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; - let params = latest_session_lookup_params( /*uses_remote_filesystem*/ true, /*model_provider*/ None, - &config, /*cwd_filter*/ None, /*include_non_interactive*/ false, LatestSessionLookupMode::StateDbOnly, @@ -3413,13 +3397,9 @@ requires_openai_auth = {requires_openai_auth} #[tokio::test] async fn latest_session_lookup_params_can_include_non_interactive_sources() -> std::io::Result<()> { - let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; - let params = latest_session_lookup_params( /*uses_remote_filesystem*/ true, /*model_provider*/ None, - &config, /*cwd_filter*/ None, /*include_non_interactive*/ true, LatestSessionLookupMode::StateDbOnly, @@ -3440,14 +3420,11 @@ requires_openai_auth = {requires_openai_auth} #[tokio::test] async fn latest_session_lookup_params_keep_explicit_cwd_filter_for_remote_sessions() -> std::io::Result<()> { - let temp_dir = TempDir::new()?; - let config = build_config(&temp_dir).await?; let cwd = Path::new("repo/on/server"); let params = latest_session_lookup_params( /*uses_remote_filesystem*/ true, /*model_provider*/ None, - &config, Some(cwd), /*include_non_interactive*/ false, LatestSessionLookupMode::StateDbOnly, @@ -3517,9 +3494,6 @@ requires_openai_auth = {requires_openai_auth} }) .build() .await?; - config - .features - .set_enabled(codex_features::Feature::Worktrees, /*enabled*/ false)?; config .features .enable(codex_features::Feature::LocalThreadStoreCompression)?; @@ -3577,22 +3551,6 @@ requires_openai_auth = {requires_openai_auth} /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, /*show_all*/ false, ); - let disabled_target = lookup_latest_session_target_with_app_server( - /*uses_remote_filesystem*/ false, - &mut app_server, - &config, - filter_cwd, - /*include_non_interactive*/ false, - ) - .await? - .expect("expected current-checkout target with worktrees disabled"); - config - .features - .set_enabled(codex_features::Feature::Worktrees, /*enabled*/ true)?; - let filter_cwd = latest_session_cwd_filter( - /*uses_remote_workspace*/ false, /*remote_cwd_override*/ None, &config, - /*show_all*/ false, - ); let scoped_target = lookup_latest_session_target_with_app_server( /*uses_remote_filesystem*/ false, &mut app_server, @@ -3617,7 +3575,6 @@ requires_openai_auth = {requires_openai_auth} .expect("expected global fork --last target"); app_server.shutdown().await?; - assert_eq!(disabled_target.thread_id, project_thread_id); assert_eq!(scoped_target.thread_id, linked_thread_id); assert_eq!(show_all_target.thread_id, other_thread_id); Ok(()) diff --git a/codex-rs/tui/src/managed_worktree_tools_tests.rs b/codex-rs/tui/src/managed_worktree_tools_tests.rs index 27904dc9f6..ab59305251 100644 --- a/codex-rs/tui/src/managed_worktree_tools_tests.rs +++ b/codex-rs/tui/src/managed_worktree_tools_tests.rs @@ -106,7 +106,7 @@ async fn creation_attaches_to_original_task_and_survives_service_restart_scenari git(&repository, &["add", "."]); git(&repository, &["commit", "-m", "base"]); fs::write(repository.join("tracked.txt"), "source edits\n")?; - let mut config = ConfigBuilder::default() + let config = ConfigBuilder::default() .codex_home(home.path().to_path_buf()) .harness_overrides(ConfigOverrides { cwd: Some(home.path().to_path_buf()), @@ -115,7 +115,6 @@ async fn creation_attaches_to_original_task_and_survives_service_restart_scenari .loader_overrides(LoaderOverrides::without_managed_config_for_tests()) .build() .await?; - config.features.enable(codex_features::Feature::Worktrees)?; let mut server = Box::pin(start_server(&config)).await?; let mut conflicting = config.clone(); let raw: codex_config::RawMcpServerConfig = diff --git a/codex-rs/tui/src/resume_picker.rs b/codex-rs/tui/src/resume_picker.rs index 7424757fe3..6d1381d20f 100644 --- a/codex-rs/tui/src/resume_picker.rs +++ b/codex-rs/tui/src/resume_picker.rs @@ -345,7 +345,6 @@ struct SessionPickerRunOptions { show_all: bool, filter_cwd: Option, local_filter_cwd: Option, - worktrees_enabled: bool, action: SessionPickerAction, launch_context: SessionPickerLaunchContext, provider_filter: ProviderFilter, @@ -458,7 +457,6 @@ async fn run_resume_picker_with_launch_context( show_all, filter_cwd: cwd_filter, local_filter_cwd, - worktrees_enabled: config.features.enabled(codex_features::Feature::Worktrees), action: SessionPickerAction::Resume, launch_context, provider_filter, @@ -520,7 +518,6 @@ pub async fn run_fork_picker_with_app_server( show_all, filter_cwd: cwd_filter, local_filter_cwd, - worktrees_enabled: config.features.enabled(codex_features::Feature::Worktrees), action: SessionPickerAction::Fork, launch_context: SessionPickerLaunchContext::Startup, provider_filter, @@ -574,7 +571,6 @@ async fn run_session_picker_with_loader( state.copy_on_select = options.copy_on_select; state.copy_on_select_clear_selection = options.copy_on_select_clear_selection; state.mouse_scroll_speed = options.mouse_scroll_speed; - state.worktrees_enabled = options.worktrees_enabled; state.density = options.initial_density; state.view_persistence = options.view_persistence; state.keymap = options.keymap; @@ -726,9 +722,6 @@ fn spawn_app_server_page_loader( request.cursor.as_ref(), request.cwd_filter.as_deref(), uses_remote_filesystem, - config.as_ref().is_some_and(|config| { - config.features.enabled(codex_features::Feature::Worktrees) - }), ); let cursor = request.cursor.map(|PageCursor::AppServer(cursor)| cursor); let params = thread_list_params( @@ -879,7 +872,6 @@ struct PickerState { status: SessionStatus, filter_cwd: Option, local_filter_cwd: Option, - worktrees_enabled: bool, toolbar_focus: ToolbarControl, density: SessionListDensity, launch_context: SessionPickerLaunchContext, @@ -1080,7 +1072,6 @@ impl PickerState { status: SessionStatus::Active, local_filter_cwd: filter_cwd.clone(), local_cwd_matches: HashMap::new(), - worktrees_enabled: false, filter_cwd, toolbar_focus: ToolbarControl::Filter, density: SessionListDensity::Comfortable, @@ -1706,13 +1697,11 @@ impl PickerState { return false; }; paths_match(row_cwd, filter_cwd) - || (self.worktrees_enabled - && codex_git_utils::repository_identity(row_cwd) - .zip(codex_git_utils::repository_identity(filter_cwd)) - .is_some_and(|(row, filter)| { - row.common_dir == filter.common_dir - && row.relative_cwd == filter.relative_cwd - })) + || codex_git_utils::repository_identity(row_cwd) + .zip(codex_git_utils::repository_identity(filter_cwd)) + .is_some_and(|(row, filter)| { + row.common_dir == filter.common_dir && row.relative_cwd == filter.relative_cwd + }) } fn set_query(&mut self, new_query: String) { @@ -2130,10 +2119,8 @@ fn thread_list_params( pub(crate) fn repository_cwd_filter( cwd: &Path, uses_remote_filesystem: bool, - worktrees_enabled: bool, ) -> ThreadListCwdFilter { - if worktrees_enabled - && !uses_remote_filesystem + if !uses_remote_filesystem && let Some(cwds) = codex_git_utils::linked_worktree_cwds(cwd) && cwds.len() > 1 { @@ -2895,12 +2882,11 @@ fn render_comfortable_session_lines( .as_ref() .map(|path| format_directory_display(path, /*max_width*/ None)); let show_cwd = state.filter_mode == SessionFilterMode::All - || (state.worktrees_enabled - && row - .cwd - .as_deref() - .zip(state.local_filter_cwd.as_deref()) - .is_some_and(|(row_cwd, filter_cwd)| !paths_match(row_cwd, filter_cwd))); + || row + .cwd + .as_deref() + .zip(state.local_filter_cwd.as_deref()) + .is_some_and(|(row_cwd, filter_cwd)| !paths_match(row_cwd, filter_cwd)); let footer_lines = render_footer_lines( state.sort_key, &created, @@ -3767,7 +3753,7 @@ mod tests { } #[tokio::test] - async fn worktrees_filtering_requires_feature() { + async fn linked_worktree_sessions_are_included_for_local_repositories() { let root = tempfile::tempdir().expect("temporary repository"); let primary = root.path().join("primary"); let linked = root.path().join("linked"); @@ -3795,60 +3781,44 @@ mod tests { let mut row = make_row("session.jsonl", "2025-01-01T00:00:00Z", "Linked session"); row.cwd = Some(linked.clone()); let single = ThreadListCwdFilter::One(primary.display().to_string()); - for enabled in [false, true] { - state.worktrees_enabled = enabled; - state.start_initial_load(); - state.ingest_page(page( - vec![row.clone()], - /*next_cursor*/ None, - /*num_scanned_files*/ 1, - /*reached_scan_cap*/ false, - )); - assert_eq!( - state.filtered_rows, - if enabled { vec![row.clone()] } else { vec![] } - ); - if enabled { - state - .pagination - .finish_load(state.next_request_token - 1) - .expect("initial page completed"); - state.relative_time_reference = - Some(parse_timestamp_str("2025-01-02T00:00:00Z").expect("fixed time")); - // The retained row was filtered using its real checkout path; use a stable - // display path so the snapshot is independent of the temporary directory. - state.filtered_rows[0].cwd = Some(PathBuf::from("/repo/linked")); - state.update_viewport(/*rows*/ 12, /*width*/ 100); - assert_snapshot!( - "resume_picker_linked_worktree", - render_picker_list(&state, /*width*/ 100, /*height*/ 12) - ); - } - assert_eq!( - repository_cwd_filter(&primary, /*uses_remote_filesystem*/ false, enabled), - if enabled { - ThreadListCwdFilter::Many(vec![ - primary.display().to_string(), - linked.display().to_string(), - ]) - } else { - single.clone() - } - ); - assert_eq!( - repository_cwd_filter(&primary, /*uses_remote_filesystem*/ true, enabled), - single - ); - } - let mut config = crate::legacy_core::config::ConfigBuilder::default() + state.start_initial_load(); + state.ingest_page(page( + vec![row.clone()], + /*next_cursor*/ None, + /*num_scanned_files*/ 1, + /*reached_scan_cap*/ false, + )); + assert_eq!(state.filtered_rows, vec![row.clone()]); + state + .pagination + .finish_load(state.next_request_token - 1) + .expect("initial page completed"); + state.relative_time_reference = + Some(parse_timestamp_str("2025-01-02T00:00:00Z").expect("fixed time")); + // The retained row was filtered using its real checkout path; use a stable + // display path so the snapshot is independent of the temporary directory. + state.filtered_rows[0].cwd = Some(PathBuf::from("/repo/linked")); + state.update_viewport(/*rows*/ 12, /*width*/ 100); + assert_snapshot!( + "resume_picker_linked_worktree", + render_picker_list(&state, /*width*/ 100, /*height*/ 12) + ); + assert_eq!( + repository_cwd_filter(&primary, /*uses_remote_filesystem*/ false), + ThreadListCwdFilter::Many(vec![ + primary.display().to_string(), + linked.display().to_string(), + ]) + ); + assert_eq!( + repository_cwd_filter(&primary, /*uses_remote_filesystem*/ true), + single + ); + let config = crate::legacy_core::config::ConfigBuilder::default() .codex_home(root.path().to_path_buf()) .build() .await .unwrap(); - config - .features - .set_enabled(codex_features::Feature::Worktrees, /*enabled*/ true) - .unwrap(); for (filename_ts, timestamp, cwd) in [ ("2025-01-02T10-00-00", "2025-01-02T10:00:00Z", &primary), ("2025-01-02T11-00-00", "2025-01-02T11:00:00Z", &linked), @@ -3886,7 +3856,6 @@ mod tests { SessionPickerAction::Resume, ); live_picker.local_filter_cwd = Some(primary.clone()); - live_picker.worktrees_enabled = true; live_picker.start_initial_load(); let event = tokio::time::timeout(std::time::Duration::from_secs(10), bg_rx.recv()) .await @@ -3918,7 +3887,6 @@ mod tests { let mut local = crate::latest_session_lookup_params( /*uses_remote_filesystem*/ false, /*model_provider*/ None, - &config, Some(&primary), /*include_non_interactive*/ false, crate::LatestSessionLookupMode::StateDbOnly, @@ -3929,7 +3897,6 @@ mod tests { crate::latest_session_lookup_params( /*uses_remote_filesystem*/ true, /*model_provider*/ None, - &config, Some(&primary), /*include_non_interactive*/ false, crate::LatestSessionLookupMode::StateDbOnly, diff --git a/codex-rs/tui/src/resume_picker/page_loading.rs b/codex-rs/tui/src/resume_picker/page_loading.rs index 1a6da3d5e8..da2e132db6 100644 --- a/codex-rs/tui/src/resume_picker/page_loading.rs +++ b/codex-rs/tui/src/resume_picker/page_loading.rs @@ -18,11 +18,9 @@ impl PageCwdFilter { cursor: Option<&PageCursor>, cwd: Option<&Path>, uses_remote_filesystem: bool, - worktrees_enabled: bool, ) -> Option { if cursor.is_none() { - self.current = cwd - .map(|cwd| repository_cwd_filter(cwd, uses_remote_filesystem, worktrees_enabled)); + self.current = cwd.map(|cwd| repository_cwd_filter(cwd, uses_remote_filesystem)); } self.current.clone() } diff --git a/codex-rs/tui/src/resume_picker/page_loading_tests.rs b/codex-rs/tui/src/resume_picker/page_loading_tests.rs index 92c07de0b4..b85fa18cb3 100644 --- a/codex-rs/tui/src/resume_picker/page_loading_tests.rs +++ b/codex-rs/tui/src/resume_picker/page_loading_tests.rs @@ -21,7 +21,6 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() { /*cursor*/ None, Some(&primary), /*uses_remote_filesystem*/ false, - /*worktrees_enabled*/ true, ), single ); @@ -43,7 +42,6 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() { Some(&cursor), Some(&primary), /*uses_remote_filesystem*/ false, - /*worktrees_enabled*/ true, ), single ); @@ -56,7 +54,6 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() { /*cursor*/ None, Some(&primary), /*uses_remote_filesystem*/ false, - /*worktrees_enabled*/ true, ), both ); @@ -66,7 +63,6 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() { Some(&cursor), Some(&primary), /*uses_remote_filesystem*/ false, - /*worktrees_enabled*/ true, ), both ); @@ -75,7 +71,6 @@ fn linked_checkout_changes_only_affect_the_next_listing_cycle() { /*cursor*/ None, Some(&primary), /*uses_remote_filesystem*/ false, - /*worktrees_enabled*/ true, ), single ); diff --git a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__experimental_voice_tooltip.snap b/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__experimental_voice_tooltip.snap deleted file mode 100644 index c9a8edd901..0000000000 --- a/codex-rs/tui/src/snapshots/codex_tui__tooltips__tests__experimental_voice_tooltip.snap +++ /dev/null @@ -1,6 +0,0 @@ ---- -source: tui/src/tooltips.rs -assertion_line: 382 -expression: "available.into_iter().filter(|tip| *tip ==\nvoice_tip).collect::>().join(\"\\n\")" ---- -NEW: Voice conversations can now be enabled from /experimental. Restart Codex after enabling, then use /voice. diff --git a/codex-rs/tui/src/tooltips.rs b/codex-rs/tui/src/tooltips.rs index 5f3b31887b..4319a0988c 100644 --- a/codex-rs/tui/src/tooltips.rs +++ b/codex-rs/tui/src/tooltips.rs @@ -3,8 +3,6 @@ use crate::keymap::keymap_action_id; use crate::terminal_hyperlinks::HyperlinkLine; use codex_config::types::TuiKeymap; use codex_features::FEATURES; -use codex_features::Feature; -use codex_features::FeatureSpec; use codex_protocol::account::PlanType; use lazy_static::lazy_static; use rand::Rng; @@ -38,25 +36,15 @@ lazy_static! { static ref ALL_TOOLTIPS: Vec<&'static str> = { let mut tips = Vec::new(); tips.extend(TOOLTIPS.iter().copied()); - tips.extend(experimental_tooltips( - FEATURES, - codex_realtime_webrtc::RealtimeWebrtcSession::is_supported, - )); + tips.extend( + FEATURES + .iter() + .filter_map(|spec| spec.stage.experimental_announcement()), + ); tips }; } -fn experimental_tooltips( - features: &[FeatureSpec], - voice_supported: impl Fn() -> bool, -) -> Vec<&'static str> { - features - .iter() - .filter(|spec| spec.id != Feature::RealtimeConversation || voice_supported()) - .filter_map(|spec| spec.stage.experimental_announcement()) - .collect() -} - /// Pick a random tooltip to show to the user when starting Codex. pub(crate) fn get_tooltip(plan: Option, keymap: &TuiKeymap) -> Option { let mut rng = rand::rng(); @@ -386,43 +374,6 @@ mod tests { use rand::SeedableRng; use rand::rngs::StdRng; - #[test] - fn experimental_voice_tooltip_requires_runtime_support() { - let mut features = FEATURES.to_vec(); - features - .iter_mut() - .find(|spec| spec.id == Feature::RealtimeConversation) - .unwrap() - .stage = codex_features::Stage::Experimental { - name: "Voice conversations", - menu_description: "Talk with Codex using /voice.", - announcement: "NEW: Voice conversations can now be enabled from /experimental. Restart Codex after enabling, then use /voice.", - }; - let unavailable = experimental_tooltips(&features, || false); - let available = experimental_tooltips(&features, || true); - let voice_tip = features - .iter() - .find(|spec| spec.id == Feature::RealtimeConversation) - .and_then(|spec| spec.stage.experimental_announcement()) - .expect("voice has an experimental announcement"); - assert_eq!( - unavailable, - available - .iter() - .copied() - .filter(|tip| *tip != voice_tip) - .collect::>() - ); - insta::assert_snapshot!( - "experimental_voice_tooltip", - available - .into_iter() - .filter(|tip| *tip == voice_tip) - .collect::>() - .join("\n") - ); - } - #[test] fn random_tooltip_returns_some_tip_when_available() { let mut rng = StdRng::seed_from_u64(42); diff --git a/codex-rs/tui/src/worktree_startup.rs b/codex-rs/tui/src/worktree_startup.rs index c1d1ca31ca..afe8047d78 100644 --- a/codex-rs/tui/src/worktree_startup.rs +++ b/codex-rs/tui/src/worktree_startup.rs @@ -214,11 +214,6 @@ pub(super) async fn prepare( .await; } } - if !source.features.enabled(codex_features::Feature::Worktrees) { - color_eyre::eyre::bail!( - "`--worktree` requires the worktrees feature; enable it with `--enable worktrees`" - ); - } if source.active_project.is_untrusted() { color_eyre::eyre::bail!( "`--worktree` cannot create a checkout from an explicitly untrusted source"