返回 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 }
52
53 impl fmt::Display for Stage {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 f.write_str(self.as_str())
56 }
57 }
58
59 impl Feature {
60 pub fn key(self) -> &'static str {
61 self.info().key
62 }
63
64 pub fn stage(self) -> Stage {
65 self.info().stage
66 }
67
68 pub fn default_enabled(self) -> bool {
69 self.info().default_enabled
70 }
71
72 fn info(self) -> &'static FeatureSpec {
73 FEATURES
74 .iter()
75 .find(|spec| spec.id == self)
76 .unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
77 }
78 }
79
80 /// Holds the effective set of enabled features.
81 #[derive(Debug, Clone, Default, PartialEq)]
82 pub struct Features {
83 enabled: BTreeSet<Feature>,
84 }
85
86 impl Features {
87 /// Starts with built-in defaults.
88 pub fn with_defaults() -> Self {
89 let mut set = BTreeSet::new();
90 for spec in FEATURES {
91 if spec.default_enabled {
92 set.insert(spec.id);
93 }
94 }
95 Self { enabled: set }
96 }
97
98 pub fn enabled(&self, feature: Feature) -> bool {
99 self.enabled.contains(&feature)
100 }
101
102 pub fn enable(&mut self, feature: Feature) -> &mut Self {
103 self.enabled.insert(feature);
104 self
105 }
106
107 pub fn disable(&mut self, feature: Feature) -> &mut Self {
108 self.enabled.remove(&feature);
109 self
110 }
111
112 pub fn apply_map(&mut self, entries: &BTreeMap<String, bool>) {
113 for (key, enabled) in entries {
114 if let Some(feature) = feature_from_key(key) {
115 if *enabled {
116 self.enable(feature);
117 } else {
118 self.disable(feature);
119 }
120 }
121 }
122 }
123
124 pub fn enabled_features(&self) -> Vec<Feature> {
125 let mut list: Vec<_> = self.enabled.iter().copied().collect();
126 list.sort();
127 list
128 }
129 }
130
131 /// Keys accepted in `[features]` tables.
132 pub fn is_known_feature_key(key: &str) -> bool {
133 FEATURES.iter().any(|spec| spec.key == key)
134 }
135
136 pub fn feature_from_key(key: &str) -> Option<Feature> {
137 FEATURES
138 .iter()
139 .find(|spec| spec.key == key)
140 .map(|spec| spec.id)
141 }
142
143 pub fn feature_spec_by_key(key: &str) -> Option<&'static FeatureSpec> {
144 FEATURES.iter().find(|spec| spec.key == key)
145 }
146
147 pub fn render_feature_table(features: &Features) -> String {
148 let mut output = String::from("feature\tstage\tenabled\n");
149 for spec in FEATURES {
150 let _ = writeln!(
151 output,
152 "{}\t{}\t{}",
153 spec.key,
154 spec.stage,
155 features.enabled(spec.id)
156 );
157 }
158 output
159 }
160
161 /// Deserializable features table for TOML.
162 #[derive(Serialize, Debug, Clone, Default, PartialEq)]
163 pub struct FeaturesToml {
164 #[serde(flatten)]
165 pub entries: BTreeMap<String, bool>,
166 }
167
168 impl<'de> Deserialize<'de> for FeaturesToml {
169 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
170 where
171 D: Deserializer<'de>,
172 {
173 let raw = BTreeMap::<String, toml::Value>::deserialize(deserializer)?;
174 let mut entries = BTreeMap::new();
175
176 for (key, value) in raw {
177 match value {
178 toml::Value::Boolean(enabled) => {
179 entries.insert(key, enabled);
180 }
181 toml::Value::Table(table) if key == "enabled" => {
182 for (feature_key, feature_value) in table {
183 match feature_value {
184 toml::Value::Boolean(enabled) => {
185 entries.insert(feature_key, enabled);
186 }
187 other => {
188 return Err(de::Error::custom(format!(
189 "features.enabled.{feature_key} must be a boolean, got {other:?}"
190 )));
191 }
192 }
193 }
194 }
195 other if is_known_feature_key(&key) => {
196 return Err(de::Error::custom(format!(
197 "features.{key} must be a boolean, got {other:?}"
198 )));
199 }
200 _ => {}
201 }
202 }
203
204 Ok(Self { entries })
205 }
206 }
207
208 /// Single registry of all feature definitions.
209 #[derive(Debug, Clone, Copy)]
210 pub struct FeatureSpec {
211 pub id: Feature,
212 pub key: &'static str,
213 pub stage: Stage,
214 pub default_enabled: bool,
215 }
216
217 pub const FEATURES: &[FeatureSpec] = &[
218 FeatureSpec {
219 id: Feature::ShellTool,
220 key: "shell_tool",
221 stage: Stage::Stable,
222 default_enabled: true,
223 },
224 FeatureSpec {
225 id: Feature::Subagents,
226 key: "subagents",
227 stage: Stage::Stable,
228 default_enabled: true,
229 },
230 FeatureSpec {
231 id: Feature::WebSearch,
232 key: "web_search",
233 stage: Stage::Stable,
234 default_enabled: true,
235 },
236 FeatureSpec {
237 id: Feature::ApplyPatch,
238 key: "apply_patch",
239 stage: Stage::Stable,
240 default_enabled: true,
241 },
242 FeatureSpec {
243 id: Feature::Mcp,
244 key: "mcp",
245 stage: Stage::Stable,
246 default_enabled: true,
247 },
248 FeatureSpec {
249 id: Feature::ExecPolicy,
250 key: "exec_policy",
251 stage: Stage::Stable,
252 default_enabled: true,
253 },
254 FeatureSpec {
255 id: Feature::VisionModel,
256 key: "vision_model",
257 stage: Stage::Beta,
258 default_enabled: false,
259 },
260 FeatureSpec {
261 id: Feature::Verify,
262 key: "verify_tool",
263 stage: Stage::Stable,
264 default_enabled: true,
265 },
266 ];
267
268 #[cfg(test)]
269 mod tests {
270 use super::*;
271
272 #[test]
273 fn apply_map_toggles_known_features_and_ignores_unknown_keys() {
274 let mut features = Features::with_defaults();
275 let entries = BTreeMap::from([
276 ("mcp".to_string(), false),
277 ("shell_tool".to_string(), false),
278 ("not_real".to_string(), false),
279 ]);
280
281 features.apply_map(&entries);
282
283 assert!(!features.enabled(Feature::Mcp));
284 assert!(!features.enabled(Feature::ShellTool));
285 assert_eq!(feature_from_key("not_real"), None);
286 }
287
288 #[test]
289 fn render_feature_table_uses_registry_order_and_effective_state() {
290 let mut features = Features::with_defaults();
291 features.disable(Feature::Mcp);
292
293 let table = render_feature_table(&features);
294 let lines = table.lines().collect::<Vec<_>>();
295
296 assert_eq!(lines.first(), Some(&"feature\tstage\tenabled"));
297 assert!(lines.contains(&"shell_tool\tstable\ttrue"));
298 assert!(lines.contains(&"mcp\tstable\tfalse"));
299 }
300 }
301
301 lines RUST