feat: experimental menu (#8071)

This will automatically render any `Stage::Beta` features.

The change only gets applied to the *next session*. This started as a
bug but actually this is a good thing to prevent out of distribution
push

<img width="986" height="288" alt="Screenshot 2025-12-15 at 15 38 35"
src="https://github.com/user-attachments/assets/78b7a71d-0e43-4828-a118-91c5237909c7"
/>


<img width="509" height="109" alt="Screenshot 2025-12-15 at 17 35 44"
src="https://github.com/user-attachments/assets/6933de52-9b66-4abf-b58b-a5f26d5747e2"
/>
This commit is contained in:
jif-oai
2025-12-17 17:08:03 +00:00
committed by GitHub
parent 9352c6b235
commit ac6ba286aa
15 changed files with 577 additions and 38 deletions

View File

@@ -9,6 +9,8 @@ use codex_app_server_protocol::AuthMode;
use codex_backend_client::Client as BackendClient;
use codex_core::config::Config;
use codex_core::config::types::Notifications;
use codex_core::features::FEATURES;
use codex_core::features::Feature;
use codex_core::git_info::current_branch_name;
use codex_core::git_info::local_git_branches;
use codex_core::openai_models::model_family::ModelFamily;
@@ -85,9 +87,11 @@ use tracing::debug;
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::ApprovalRequest;
use crate::bottom_pane::BetaFeatureItem;
use crate::bottom_pane::BottomPane;
use crate::bottom_pane::BottomPaneParams;
use crate::bottom_pane::CancellationEvent;
use crate::bottom_pane::ExperimentalFeaturesView;
use crate::bottom_pane::InputResult;
use crate::bottom_pane::SelectionAction;
use crate::bottom_pane::SelectionItem;
@@ -1571,6 +1575,9 @@ impl ChatWidget {
SlashCommand::Approvals => {
self.open_approvals_popup();
}
SlashCommand::Experimental => {
self.open_experimental_popup();
}
SlashCommand::Quit | SlashCommand::Exit => {
self.request_exit();
}
@@ -2635,6 +2642,24 @@ impl ChatWidget {
});
}
pub(crate) fn open_experimental_popup(&mut self) {
let features: Vec<BetaFeatureItem> = FEATURES
.iter()
.filter_map(|spec| {
let description = spec.stage.beta_menu_description()?;
Some(BetaFeatureItem {
feature: spec.id,
name: feature_label_from_key(spec.key),
description: description.to_string(),
enabled: self.config.features.enabled(spec.id),
})
})
.collect();
let view = ExperimentalFeaturesView::new(features, self.app_event_tx.clone());
self.bottom_pane.show_view(Box::new(view));
}
fn approval_preset_actions(
approval: AskForApproval,
sandbox: SandboxPolicy,
@@ -2977,6 +3002,14 @@ impl ChatWidget {
}
}
pub(crate) fn set_feature_enabled(&mut self, feature: Feature, enabled: bool) {
if enabled {
self.config.features.enable(feature);
} else {
self.config.features.disable(feature);
}
}
pub(crate) fn set_full_access_warning_acknowledged(&mut self, acknowledged: bool) {
self.config.notices.hide_full_access_warning = Some(acknowledged);
}
@@ -3310,6 +3343,23 @@ impl ChatWidget {
}
}
fn feature_label_from_key(key: &str) -> String {
let mut out = String::with_capacity(key.len());
let mut capitalize = true;
for ch in key.chars() {
if ch == '_' || ch == '-' {
out.push(' ');
capitalize = true;
} else if capitalize {
out.push(ch.to_ascii_uppercase());
capitalize = false;
} else {
out.push(ch);
}
}
out
}
impl Drop for ChatWidget {
fn drop(&mut self) {
self.stop_rate_limit_poller();