Merge pull request #3177 from Hmbown/scratch/v0.8.59-experimental-config
feat(config): surface experimental feature flags
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user