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,
|
ConfigSectionSidebar,
|
||||||
ConfigSectionHistory,
|
ConfigSectionHistory,
|
||||||
ConfigSectionMcp,
|
ConfigSectionMcp,
|
||||||
|
ConfigSectionExperimental,
|
||||||
ConfigScopeSession,
|
ConfigScopeSession,
|
||||||
ConfigScopeSaved,
|
ConfigScopeSaved,
|
||||||
ConfigEditCancelled,
|
ConfigEditCancelled,
|
||||||
@@ -672,6 +673,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
|||||||
MessageId::ConfigSectionSidebar,
|
MessageId::ConfigSectionSidebar,
|
||||||
MessageId::ConfigSectionHistory,
|
MessageId::ConfigSectionHistory,
|
||||||
MessageId::ConfigSectionMcp,
|
MessageId::ConfigSectionMcp,
|
||||||
|
MessageId::ConfigSectionExperimental,
|
||||||
MessageId::ConfigScopeSession,
|
MessageId::ConfigScopeSession,
|
||||||
MessageId::ConfigScopeSaved,
|
MessageId::ConfigScopeSaved,
|
||||||
MessageId::ConfigEditCancelled,
|
MessageId::ConfigEditCancelled,
|
||||||
@@ -1262,6 +1264,7 @@ fn english(id: MessageId) -> &'static str {
|
|||||||
MessageId::ConfigSectionSidebar => "Sidebar",
|
MessageId::ConfigSectionSidebar => "Sidebar",
|
||||||
MessageId::ConfigSectionHistory => "History",
|
MessageId::ConfigSectionHistory => "History",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "Experimental",
|
||||||
MessageId::ConfigScopeSession => "SESSION",
|
MessageId::ConfigScopeSession => "SESSION",
|
||||||
MessageId::ConfigScopeSaved => "SAVED",
|
MessageId::ConfigScopeSaved => "SAVED",
|
||||||
MessageId::ConfigEditCancelled => "Edit cancelled",
|
MessageId::ConfigEditCancelled => "Edit cancelled",
|
||||||
@@ -1827,6 +1830,7 @@ fn vietnamese(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "Thanh bên",
|
MessageId::ConfigSectionSidebar => "Thanh bên",
|
||||||
MessageId::ConfigSectionHistory => "Lịch sử",
|
MessageId::ConfigSectionHistory => "Lịch sử",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "Thử nghiệm",
|
||||||
MessageId::ConfigScopeSession => "PHIÊN",
|
MessageId::ConfigScopeSession => "PHIÊN",
|
||||||
MessageId::ConfigScopeSaved => "ĐÃ LƯU",
|
MessageId::ConfigScopeSaved => "ĐÃ LƯU",
|
||||||
MessageId::ConfigEditCancelled => "Đã hủy chỉnh sửa",
|
MessageId::ConfigEditCancelled => "Đã hủy chỉnh sửa",
|
||||||
@@ -2493,6 +2497,7 @@ fn traditional_chinese(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "側邊欄",
|
MessageId::ConfigSectionSidebar => "側邊欄",
|
||||||
MessageId::ConfigSectionHistory => "歷史",
|
MessageId::ConfigSectionHistory => "歷史",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "實驗",
|
||||||
MessageId::ConfigScopeSession => "會話",
|
MessageId::ConfigScopeSession => "會話",
|
||||||
MessageId::ConfigScopeSaved => "已儲存",
|
MessageId::ConfigScopeSaved => "已儲存",
|
||||||
MessageId::ConfigEditCancelled => "編輯已取消",
|
MessageId::ConfigEditCancelled => "編輯已取消",
|
||||||
@@ -2570,6 +2575,7 @@ fn japanese(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "サイドバー",
|
MessageId::ConfigSectionSidebar => "サイドバー",
|
||||||
MessageId::ConfigSectionHistory => "履歴",
|
MessageId::ConfigSectionHistory => "履歴",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "実験",
|
||||||
MessageId::ConfigScopeSession => "セッション",
|
MessageId::ConfigScopeSession => "セッション",
|
||||||
MessageId::ConfigScopeSaved => "保存済み",
|
MessageId::ConfigScopeSaved => "保存済み",
|
||||||
MessageId::ConfigEditCancelled => "編集をキャンセルしました",
|
MessageId::ConfigEditCancelled => "編集をキャンセルしました",
|
||||||
@@ -3126,6 +3132,7 @@ fn chinese_simplified(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "侧边栏",
|
MessageId::ConfigSectionSidebar => "侧边栏",
|
||||||
MessageId::ConfigSectionHistory => "历史",
|
MessageId::ConfigSectionHistory => "历史",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "实验",
|
||||||
MessageId::ConfigScopeSession => "会话",
|
MessageId::ConfigScopeSession => "会话",
|
||||||
MessageId::ConfigScopeSaved => "已保存",
|
MessageId::ConfigScopeSaved => "已保存",
|
||||||
MessageId::ConfigEditCancelled => "编辑已取消",
|
MessageId::ConfigEditCancelled => "编辑已取消",
|
||||||
@@ -3622,6 +3629,7 @@ fn portuguese_brazil(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "Barra lateral",
|
MessageId::ConfigSectionSidebar => "Barra lateral",
|
||||||
MessageId::ConfigSectionHistory => "Histórico",
|
MessageId::ConfigSectionHistory => "Histórico",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "Experimental",
|
||||||
MessageId::ConfigScopeSession => "SESSÃO",
|
MessageId::ConfigScopeSession => "SESSÃO",
|
||||||
MessageId::ConfigScopeSaved => "SALVO",
|
MessageId::ConfigScopeSaved => "SALVO",
|
||||||
MessageId::ConfigEditCancelled => "Edição cancelada",
|
MessageId::ConfigEditCancelled => "Edição cancelada",
|
||||||
@@ -4206,6 +4214,7 @@ fn spanish_latin_america(id: MessageId) -> Option<&'static str> {
|
|||||||
MessageId::ConfigSectionSidebar => "Barra lateral",
|
MessageId::ConfigSectionSidebar => "Barra lateral",
|
||||||
MessageId::ConfigSectionHistory => "Historial",
|
MessageId::ConfigSectionHistory => "Historial",
|
||||||
MessageId::ConfigSectionMcp => "MCP",
|
MessageId::ConfigSectionMcp => "MCP",
|
||||||
|
MessageId::ConfigSectionExperimental => "Experimental",
|
||||||
MessageId::ConfigScopeSession => "SESIÓN",
|
MessageId::ConfigScopeSession => "SESIÓN",
|
||||||
MessageId::ConfigScopeSaved => "GUARDADO",
|
MessageId::ConfigScopeSaved => "GUARDADO",
|
||||||
MessageId::ConfigEditCancelled => "Edición cancelada",
|
MessageId::ConfigEditCancelled => "Edición cancelada",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use std::cell::{Cell, RefCell};
|
|||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::config::{ApiProvider, Config};
|
use crate::config::{ApiProvider, Config};
|
||||||
|
use crate::features::{FEATURES, Stage};
|
||||||
use crate::localization::{Locale, MessageId, tr};
|
use crate::localization::{Locale, MessageId, tr};
|
||||||
use crate::palette;
|
use crate::palette;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
@@ -406,6 +407,7 @@ enum ConfigSection {
|
|||||||
Sidebar,
|
Sidebar,
|
||||||
History,
|
History,
|
||||||
Mcp,
|
Mcp,
|
||||||
|
Experimental,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConfigSection {
|
impl ConfigSection {
|
||||||
@@ -422,6 +424,7 @@ impl ConfigSection {
|
|||||||
ConfigSection::Sidebar => MessageId::ConfigSectionSidebar,
|
ConfigSection::Sidebar => MessageId::ConfigSectionSidebar,
|
||||||
ConfigSection::History => MessageId::ConfigSectionHistory,
|
ConfigSection::History => MessageId::ConfigSectionHistory,
|
||||||
ConfigSection::Mcp => MessageId::ConfigSectionMcp,
|
ConfigSection::Mcp => MessageId::ConfigSectionMcp,
|
||||||
|
ConfigSection::Experimental => MessageId::ConfigSectionExperimental,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -466,7 +469,9 @@ const CONFIG_COLUMN_GAPS_WIDTH: usize = 2;
|
|||||||
impl ConfigView {
|
impl ConfigView {
|
||||||
pub fn new_for_app(app: &App) -> Self {
|
pub fn new_for_app(app: &App) -> Self {
|
||||||
let settings = Settings::load().unwrap_or_else(|_| Settings::default());
|
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 {
|
ConfigRow {
|
||||||
section: ConfigSection::Provider,
|
section: ConfigSection::Provider,
|
||||||
key: "provider".to_string(),
|
key: "provider".to_string(),
|
||||||
@@ -750,6 +755,7 @@ impl ConfigView {
|
|||||||
scope: ConfigScope::Saved,
|
scope: ConfigScope::Saved,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
rows.extend(experimental_config_rows(&config));
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
rows,
|
rows,
|
||||||
@@ -1133,6 +1139,64 @@ fn cost_currency_config_value(app: &App) -> String {
|
|||||||
.to_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 {
|
fn config_hint_for_key(key: &str) -> &'static str {
|
||||||
match key {
|
match key {
|
||||||
"model" => "deepseek-v4-pro | deepseek-v4-flash | deepseek-*",
|
"model" => "deepseek-v4-pro | deepseek-v4-flash | deepseek-*",
|
||||||
@@ -2324,6 +2388,7 @@ mod tests {
|
|||||||
"Sidebar",
|
"Sidebar",
|
||||||
"History",
|
"History",
|
||||||
"MCP",
|
"MCP",
|
||||||
|
"Experimental",
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2359,7 +2424,96 @@ mod tests {
|
|||||||
assert!(keys.contains(&"cost_currency"));
|
assert!(keys.contains(&"cost_currency"));
|
||||||
assert!(keys.contains(&"prefer_external_pdftotext"));
|
assert!(keys.contains(&"prefer_external_pdftotext"));
|
||||||
assert!(keys.contains(&"mcp_config_path"));
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -1203,6 +1203,13 @@ You can also override features for a single run:
|
|||||||
- `codewhale-tui --disable subagents`
|
- `codewhale-tui --disable subagents`
|
||||||
|
|
||||||
Use `codewhale-tui features list` to inspect known flags and their effective state.
|
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
|
## Web Search Provider
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user