From 81265ec71b8a670c47e494f5f4f3b579b9ba580b Mon Sep 17 00:00:00 2001 From: CodeWhale Agent Date: Fri, 12 Jun 2026 13:36:13 -0700 Subject: [PATCH] feat(config): surface experimental feature flags --- crates/tui/src/localization.rs | 9 ++ crates/tui/src/tui/views/mod.rs | 158 +++++++++++++++++++++++++++++++- docs/CONFIGURATION.md | 7 ++ 3 files changed, 172 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index 43251048..da179577 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -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", diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 892d7107..e1aaa1f5 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -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 { + 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] diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 0a527f0d..9073d85c 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -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