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
This commit is contained in:
Benjamin Carlsson
2026-10-15 03:48:33 +00:00
committed by copyberry
parent 160b2b1ef6
commit bb64b894fd
38 changed files with 166 additions and 461 deletions
+9
View File
@@ -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(())
@@ -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,
-5
View File
@@ -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,
+12 -30
View File
@@ -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(())
}
+5 -5
View File
@@ -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 {
+19
View File
@@ -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());
}
}
+1 -3
View File
@@ -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]
+4 -5
View File
@@ -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(),
+4 -6
View File
@@ -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(),
);
+19 -32
View File
@@ -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::<Vec<_>>()
.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::<Vec<_>>()
.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());
}
@@ -64,11 +64,7 @@ impl App {
mode: crate::app_event::ManagedWorktreeMode,
name: Option<String>,
) {
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(),
@@ -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
@@ -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,
@@ -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());
@@ -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);
+1 -4
View File
@@ -457,10 +457,7 @@ impl AppServerSession {
app_event_tx: AppEventSender,
status_updates: tokio::sync::broadcast::Sender<ThreadStatusChangedNotification>,
) -> 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
@@ -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<ExperimentalFeatureItem>,
voice_supported: bool,
initial_enabled: Vec<bool>,
unconfirmed: Vec<String>,
catalog_rx: Option<oneshot::Receiver<Result<Vec<ExperimentalFeature>, 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;
}
@@ -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);
}
}
}
@@ -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
-6
View File
@@ -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."
@@ -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!(
@@ -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::<Vec<_>>()
.join("\n"),
),
_ => None,
})
.collect::<Vec<_>>()
.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;
+1 -2
View File
@@ -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
+2 -21
View File
@@ -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) {
@@ -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,
}
@@ -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(),
@@ -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);
@@ -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))];
+2 -10
View File
@@ -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;
+2 -1
View File
@@ -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"
+2 -45
View File
@@ -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<String>,
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(())
@@ -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 =
+46 -79
View File
@@ -345,7 +345,6 @@ struct SessionPickerRunOptions {
show_all: bool,
filter_cwd: Option<PathBuf>,
local_filter_cwd: Option<PathBuf>,
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<PathBuf>,
local_filter_cwd: Option<PathBuf>,
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,
@@ -18,11 +18,9 @@ impl PageCwdFilter {
cursor: Option<&PageCursor>,
cwd: Option<&Path>,
uses_remote_filesystem: bool,
worktrees_enabled: bool,
) -> Option<ThreadListCwdFilter> {
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()
}
@@ -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
);
@@ -1,6 +0,0 @@
---
source: tui/src/tooltips.rs
assertion_line: 382
expression: "available.into_iter().filter(|tip| *tip ==\nvoice_tip).collect::<Vec<_>>().join(\"\\n\")"
---
NEW: Voice conversations can now be enabled from /experimental. Restart Codex after enabling, then use /voice.
+5 -54
View File
@@ -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<PlanType>, keymap: &TuiKeymap) -> Option<String> {
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::<Vec<_>>()
);
insta::assert_snapshot!(
"experimental_voice_tooltip",
available
.into_iter()
.filter(|tip| *tip == voice_tip)
.collect::<Vec<_>>()
.join("\n")
);
}
#[test]
fn random_tooltip_returns_some_tip_when_available() {
let mut rng = StdRng::seed_from_u64(42);
-5
View File
@@ -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"