Merge pull request #3177 from Hmbown/scratch/v0.8.59-experimental-config

feat(config): surface experimental feature flags
This commit is contained in:
Hunter Bown
2026-06-12 13:36:57 -07:00
committed by GitHub
3 changed files with 172 additions and 2 deletions
+9
View File
@@ -255,6 +255,7 @@ pub enum MessageId {
ConfigSectionSidebar,
ConfigSectionHistory,
ConfigSectionMcp,
ConfigSectionExperimental,
ConfigScopeSession,
ConfigScopeSaved,
ConfigEditCancelled,
@@ -672,6 +673,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ConfigSectionSidebar,
MessageId::ConfigSectionHistory,
MessageId::ConfigSectionMcp,
MessageId::ConfigSectionExperimental,
MessageId::ConfigScopeSession,
MessageId::ConfigScopeSaved,
MessageId::ConfigEditCancelled,
@@ -1262,6 +1264,7 @@ fn english(id: MessageId) -> &'static str {
MessageId::ConfigSectionSidebar => "Sidebar",
MessageId::ConfigSectionHistory => "History",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "Experimental",
MessageId::ConfigScopeSession => "SESSION",
MessageId::ConfigScopeSaved => "SAVED",
MessageId::ConfigEditCancelled => "Edit cancelled",
@@ -1827,6 +1830,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "Thanh bên",
MessageId::ConfigSectionHistory => "Lịch sử",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "Thử nghiệm",
MessageId::ConfigScopeSession => "PHIÊN",
MessageId::ConfigScopeSaved => "ĐÃ LƯU",
MessageId::ConfigEditCancelled => "Đã hủy chỉnh sửa",
@@ -2493,6 +2497,7 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "側邊欄",
MessageId::ConfigSectionHistory => "歷史",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "實驗",
MessageId::ConfigScopeSession => "會話",
MessageId::ConfigScopeSaved => "已儲存",
MessageId::ConfigEditCancelled => "編輯已取消",
@@ -2570,6 +2575,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "サイドバー",
MessageId::ConfigSectionHistory => "履歴",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "実験",
MessageId::ConfigScopeSession => "セッション",
MessageId::ConfigScopeSaved => "保存済み",
MessageId::ConfigEditCancelled => "編集をキャンセルしました",
@@ -3126,6 +3132,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "侧边栏",
MessageId::ConfigSectionHistory => "历史",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "实验",
MessageId::ConfigScopeSession => "会话",
MessageId::ConfigScopeSaved => "已保存",
MessageId::ConfigEditCancelled => "编辑已取消",
@@ -3622,6 +3629,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "Barra lateral",
MessageId::ConfigSectionHistory => "Histórico",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "Experimental",
MessageId::ConfigScopeSession => "SESSÃO",
MessageId::ConfigScopeSaved => "SALVO",
MessageId::ConfigEditCancelled => "Edição cancelada",
@@ -4206,6 +4214,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
MessageId::ConfigSectionSidebar => "Barra lateral",
MessageId::ConfigSectionHistory => "Historial",
MessageId::ConfigSectionMcp => "MCP",
MessageId::ConfigSectionExperimental => "Experimental",
MessageId::ConfigScopeSession => "SESIÓN",
MessageId::ConfigScopeSaved => "GUARDADO",
MessageId::ConfigEditCancelled => "Edición cancelada",
+156 -2
View File
@@ -4,6 +4,7 @@ use std::cell::{Cell, RefCell};
use std::fmt;
use crate::config::{ApiProvider, Config};
use crate::features::{FEATURES, Stage};
use crate::localization::{Locale, MessageId, tr};
use crate::palette;
use crate::settings::Settings;
@@ -406,6 +407,7 @@ enum ConfigSection {
Sidebar,
History,
Mcp,
Experimental,
}
impl ConfigSection {
@@ -422,6 +424,7 @@ impl ConfigSection {
ConfigSection::Sidebar => MessageId::ConfigSectionSidebar,
ConfigSection::History => MessageId::ConfigSectionHistory,
ConfigSection::Mcp => MessageId::ConfigSectionMcp,
ConfigSection::Experimental => MessageId::ConfigSectionExperimental,
},
)
}
@@ -466,7 +469,9 @@ const CONFIG_COLUMN_GAPS_WIDTH: usize = 2;
impl ConfigView {
pub fn new_for_app(app: &App) -> Self {
let settings = Settings::load().unwrap_or_else(|_| Settings::default());
let rows = vec![
let config = Config::load(app.config_path.clone(), app.config_profile.as_deref())
.unwrap_or_default();
let mut rows = vec![
ConfigRow {
section: ConfigSection::Provider,
key: "provider".to_string(),
@@ -750,6 +755,7 @@ impl ConfigView {
scope: ConfigScope::Saved,
},
];
rows.extend(experimental_config_rows(&config));
Self {
rows,
@@ -1133,6 +1139,64 @@ fn cost_currency_config_value(app: &App) -> String {
.to_string()
}
fn experimental_config_rows(config: &Config) -> Vec<ConfigRow> {
let features = config.features();
let configured = config.features.as_ref().map(|table| &table.entries);
let mut rows = Vec::new();
for spec in FEATURES
.iter()
.filter(|spec| spec.stage == Stage::Experimental)
{
let effective = features.enabled(spec.id);
let configured_value = configured
.and_then(|entries| entries.get(spec.key))
.copied();
rows.push(ConfigRow {
section: ConfigSection::Experimental,
key: format!("features.{}", spec.key),
value: experimental_feature_value(
effective,
spec.default_enabled,
configured_value.is_some(),
),
editable: false,
scope: ConfigScope::Saved,
});
}
rows.push(ConfigRow {
section: ConfigSection::Experimental,
key: "goal_command".to_string(),
value: "preview placeholder (not stable; see #1976/#891)".to_string(),
editable: false,
scope: ConfigScope::Saved,
});
rows.push(ConfigRow {
section: ConfigSection::Experimental,
key: "whaleflow".to_string(),
value: "preview placeholder (not stable; see #2981/#2974)".to_string(),
editable: false,
scope: ConfigScope::Saved,
});
rows
}
fn experimental_feature_value(effective: bool, default_enabled: bool, configured: bool) -> String {
let state = if effective { "enabled" } else { "disabled" };
let default_state = if default_enabled {
"enabled"
} else {
"disabled"
};
if configured {
format!("{state} (configured; default {default_state})")
} else {
format!("{state} (default {default_state})")
}
}
fn config_hint_for_key(key: &str) -> &'static str {
match key {
"model" => "deepseek-v4-pro | deepseek-v4-flash | deepseek-*",
@@ -2324,6 +2388,7 @@ mod tests {
"Sidebar",
"History",
"MCP",
"Experimental",
]
);
}
@@ -2359,7 +2424,96 @@ mod tests {
assert!(keys.contains(&"cost_currency"));
assert!(keys.contains(&"prefer_external_pdftotext"));
assert!(keys.contains(&"mcp_config_path"));
assert!(view.rows.iter().all(|row| row.editable));
assert!(keys.contains(&"features.subagents"));
assert!(keys.contains(&"features.web_search"));
assert!(keys.contains(&"features.apply_patch"));
assert!(keys.contains(&"features.mcp"));
assert!(keys.contains(&"features.exec_policy"));
assert!(keys.contains(&"features.vision_model"));
assert!(keys.contains(&"goal_command"));
assert!(keys.contains(&"whaleflow"));
assert!(
view.rows
.iter()
.filter(|row| row.section != super::ConfigSection::Experimental)
.all(|row| row.editable)
);
assert!(
view.rows
.iter()
.filter(|row| row.section == super::ConfigSection::Experimental)
.all(|row| !row.editable)
);
}
#[test]
fn config_view_experimental_features_show_effective_state_and_overrides() {
let temp_root = std::env::temp_dir().join(format!(
"codewhale-experimental-config-view-test-{}",
std::process::id()
));
fs::create_dir_all(&temp_root).unwrap();
let config_path = temp_root.join("config.toml");
fs::write(
&config_path,
r#"
[features]
web_search = false
vision_model = true
"#,
)
.unwrap();
let mut app = create_test_app();
app.config_path = Some(config_path);
let view = ConfigView::new_for_app(&app);
let web_search = view
.rows
.iter()
.find(|row| row.key == "features.web_search")
.expect("web_search feature row");
assert_eq!(web_search.value, "disabled (configured; default enabled)");
assert!(!web_search.editable);
let vision = view
.rows
.iter()
.find(|row| row.key == "features.vision_model")
.expect("vision feature row");
assert_eq!(vision.value, "enabled (configured; default disabled)");
assert!(!vision.editable);
let subagents = view
.rows
.iter()
.find(|row| row.key == "features.subagents")
.expect("subagents feature row");
assert_eq!(subagents.value, "enabled (default enabled)");
}
#[test]
fn config_view_experimental_section_is_searchable() {
let mut view = create_config_view(Locale::En);
view.update_filter(|filter| filter.push_str("experimental"));
assert_eq!(visible_section_labels(&view), vec!["Experimental"]);
assert!(visible_row_keys(&view).contains(&"features.subagents"));
view.clear_filter();
type_filter(&mut view, "feature vision");
assert_eq!(visible_section_labels(&view), vec!["Experimental"]);
assert_eq!(visible_row_keys(&view), vec!["features.vision_model"]);
view.clear_filter();
type_filter(&mut view, "goal");
assert_eq!(visible_section_labels(&view), vec!["Experimental"]);
assert_eq!(visible_row_keys(&view), vec!["goal_command"]);
view.clear_filter();
type_filter(&mut view, "whaleflow");
assert_eq!(visible_section_labels(&view), vec!["Experimental"]);
assert_eq!(visible_row_keys(&view), vec!["whaleflow"]);
}
#[test]
+7
View File
@@ -1203,6 +1203,13 @@ You can also override features for a single run:
- `codewhale-tui --disable subagents`
Use `codewhale-tui features list` to inspect known flags and their effective state.
The native `/config` view also includes a read-only **Experimental** section
for experimental feature flags. It shows each flag's effective enabled/disabled
state and whether that state comes from the default or a configured override.
Change feature flags in `[features]` or with `--enable` / `--disable`; the
`/config` section is an audit surface, not a stability promise. Goal and
WhaleFlow preview rows may appear there as placeholders until those workflows
graduate behind real gated flags.
## Web Search Provider