mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
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:
committed by
copyberry
parent
160b2b1ef6
commit
bb64b894fd
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
|
||||
@@ -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
-7
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-14
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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 =
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user