返回 CodeWhale
features.rs
根目录 / crates / tui / src / features.rs
1 #![allow(dead_code)]
2
3 //! Feature flags and metadata for codewhale.
4
5 use std::collections::{BTreeMap, BTreeSet};
6 use std::fmt::{self, Write as _};
7
8 use serde::{Deserialize, Deserializer, Serialize, de};
9
10 /// Lifecycle stage for a feature flag.
11 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
12 pub enum Stage {
13 Experimental,
14 Beta,
15 Stable,
16 Deprecated,
17 Removed,
18 }
19
20 impl Stage {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Experimental => "experimental",
24 Self::Beta => "beta",
25 Self::Stable => "stable",
26 Self::Deprecated => "deprecated",
27 Self::Removed => "removed",
28 }
29 }
30 }
31
32 /// Unique features toggled via configuration.
33 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
34 pub enum Feature {
35 /// Enable the default shell tool.
36 ShellTool,
37 /// Enable background sub-agent tooling.
38 Subagents,
39 /// Enable web search tool.
40 WebSearch,
41 /// Enable apply_patch tool.
42 ApplyPatch,
43 /// Enable MCP tools.
44 Mcp,
45 /// Enable execpolicy integration/tooling.
46 ExecPolicy,
47 /// Enable vision model for image analysis.
48 VisionModel,
49 /// Enable the agent-callable `verify` adversarial self-critique tool (#4196).
50 Verify,
51 /// Expose `execute_tools` eagerly so the model composes by default (CodeMode).
52 CodeMode,
53 }
54
55 impl fmt::Display for Stage {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(self.as_str())
58 }
59 }
60
61 /// Holds the effective set of enabled features.
62 #[derive(Debug, Clone, Default, PartialEq)]
63 pub struct Features {
64 enabled: BTreeSet<Feature>,
65 }
66
67 impl Features {
68 /// Starts with built-in defaults.
69 pub fn with_defaults() -> Self {
70 let mut set = BTreeSet::new();
71 for spec in FEATURES {
72 if spec.default_enabled {
73 set.insert(spec.id);
74 }
75 }
76 Self { enabled: set }
77 }
78
79 pub fn enabled(&self, feature: Feature) -> bool {
80 self.enabled.contains(&feature)
81 }
82
83 pub fn enable(&mut self, feature: Feature) -> &mut Self {
84 self.enabled.insert(feature);
85 self
86 }
87
88 pub fn disable(&mut self, feature: Feature) -> &mut Self {
89 self.enabled.remove(&feature);
90 self
91 }
92
93 pub fn apply_map(&mut self, entries: &BTreeMap<String, bool>) {
94 for (key, enabled) in entries {
95 if let Some(feature) = feature_from_key(key) {
96 if *enabled {
97 self.enable(feature);
98 } else {
99 self.disable(feature);
100 }
101 }
102 }
103 }
104 }
105
106 /// Keys accepted in `[features]` tables.
107 pub fn is_known_feature_key(key: &str) -> bool {
108 FEATURES.iter().any(|spec| spec.key == key)
109 }
110
111 pub fn feature_from_key(key: &str) -> Option<Feature> {
112 FEATURES
113 .iter()
114 .find(|spec| spec.key == key)
115 .map(|spec| spec.id)
116 }
117
118 pub fn render_feature_table(features: &Features) -> String {
119 let mut output = String::from("feature\tstage\tenabled\n");
120 for spec in FEATURES {
121 let _ = writeln!(
122 output,
123 "{}\t{}\t{}",
124 spec.key,
125 spec.stage,
126 features.enabled(spec.id)
127 );
128 }
129 output
130 }
131
132 /// Deserializable features table for TOML.
133 #[derive(Serialize, Debug, Clone, Default, PartialEq)]
134 pub struct FeaturesToml {
135 #[serde(flatten)]
136 pub entries: BTreeMap<String, bool>,
137 }
138
139 impl<'de> Deserialize<'de> for FeaturesToml {
140 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
141 where
142 D: Deserializer<'de>,
143 {
144 let raw = BTreeMap::<String, toml::Value>::deserialize(deserializer)?;
145 let mut entries = BTreeMap::new();
146
147 for (key, value) in raw {
148 match value {
149 toml::Value::Boolean(enabled) => {
150 entries.insert(key, enabled);
151 }
152 toml::Value::Table(table) if key == "enabled" => {
153 for (feature_key, feature_value) in table {
154 match feature_value {
155 toml::Value::Boolean(enabled) => {
156 entries.insert(feature_key, enabled);
157 }
158 other => {
159 return Err(de::Error::custom(format!(
160 "features.enabled.{feature_key} must be a boolean, got {other:?}"
161 )));
162 }
163 }
164 }
165 }
166 other if is_known_feature_key(&key) => {
167 return Err(de::Error::custom(format!(
168 "features.{key} must be a boolean, got {other:?}"
169 )));
170 }
171 _ => {}
172 }
173 }
174
175 Ok(Self { entries })
176 }
177 }
178
179 /// Single registry of all feature definitions.
180 #[derive(Debug, Clone, Copy)]
181 pub struct FeatureSpec {
182 pub id: Feature,
183 pub key: &'static str,
184 pub stage: Stage,
185 pub default_enabled: bool,
186 }
187
188 pub const FEATURES: &[FeatureSpec] = &[
189 FeatureSpec {
190 id: Feature::ShellTool,
191 key: "shell_tool",
192 stage: Stage::Stable,
193 default_enabled: true,
194 },
195 FeatureSpec {
196 id: Feature::Subagents,
197 key: "subagents",
198 stage: Stage::Stable,
199 default_enabled: true,
200 },
201 FeatureSpec {
202 id: Feature::WebSearch,
203 key: "web_search",
204 stage: Stage::Stable,
205 default_enabled: true,
206 },
207 FeatureSpec {
208 id: Feature::ApplyPatch,
209 key: "apply_patch",
210 stage: Stage::Stable,
211 default_enabled: true,
212 },
213 FeatureSpec {
214 id: Feature::Mcp,
215 key: "mcp",
216 stage: Stage::Stable,
217 default_enabled: true,
218 },
219 FeatureSpec {
220 id: Feature::ExecPolicy,
221 key: "exec_policy",
222 stage: Stage::Stable,
223 default_enabled: true,
224 },
225 FeatureSpec {
226 id: Feature::VisionModel,
227 key: "vision_model",
228 stage: Stage::Beta,
229 default_enabled: false,
230 },
231 FeatureSpec {
232 id: Feature::Verify,
233 key: "verify_tool",
234 stage: Stage::Stable,
235 default_enabled: true,
236 },
237 FeatureSpec {
238 id: Feature::CodeMode,
239 key: "code_mode",
240 stage: Stage::Experimental,
241 default_enabled: false,
242 },
243 ];
244
245 #[cfg(test)]
246 mod tests {
247 use super::*;
248
249 #[test]
250 fn apply_map_toggles_known_features_and_ignores_unknown_keys() {
251 let mut features = Features::with_defaults();
252 let entries = BTreeMap::from([
253 ("mcp".to_string(), false),
254 ("shell_tool".to_string(), false),
255 ("not_real".to_string(), false),
256 ]);
257
258 features.apply_map(&entries);
259
260 assert!(!features.enabled(Feature::Mcp));
261 assert!(!features.enabled(Feature::ShellTool));
262 assert_eq!(feature_from_key("not_real"), None);
263 }
264
265 #[test]
266 fn code_mode_flag_parses_and_defaults_off() {
267 assert_eq!(feature_from_key("code_mode"), Some(Feature::CodeMode));
268 assert!(!Features::with_defaults().enabled(Feature::CodeMode));
269 }
270
271 #[test]
272 fn render_feature_table_uses_registry_order_and_effective_state() {
273 let mut features = Features::with_defaults();
274 features.disable(Feature::Mcp);
275
276 let table = render_feature_table(&features);
277 let lines = table.lines().collect::<Vec<_>>();
278
279 assert_eq!(lines.first(), Some(&"feature\tstage\tenabled"));
280 assert!(lines.contains(&"shell_tool\tstable\ttrue"));
281 assert!(lines.contains(&"mcp\tstable\tfalse"));
282 }
283 }
284
284 lines RUST