Skip to main content

zagens_core/
features.rs

1//! Feature flags shared between the runtime core and the TUI shell.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{self, Write as _};
5
6use serde::{Deserialize, Serialize};
7
8/// Lifecycle stage for a feature flag.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Stage {
11    Experimental,
12    Beta,
13    Stable,
14    Deprecated,
15    Removed,
16}
17
18impl Stage {
19    pub fn as_str(self) -> &'static str {
20        match self {
21            Self::Experimental => "experimental",
22            Self::Beta => "beta",
23            Self::Stable => "stable",
24            Self::Deprecated => "deprecated",
25            Self::Removed => "removed",
26        }
27    }
28}
29
30/// Unique features toggled via configuration.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
32pub enum Feature {
33    /// Enable the default shell tool.
34    ShellTool,
35    /// Enable background sub-agent tooling.
36    Subagents,
37    /// Enable web search tool.
38    WebSearch,
39    /// Enable apply_patch tool.
40    ApplyPatch,
41    /// Enable MCP tools.
42    Mcp,
43    /// Enable execpolicy integration/tooling.
44    ExecPolicy,
45}
46
47impl fmt::Display for Stage {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.write_str(self.as_str())
50    }
51}
52
53impl Feature {
54    pub fn key(self) -> &'static str {
55        self.info().key
56    }
57
58    pub fn stage(self) -> Stage {
59        self.info().stage
60    }
61
62    pub fn default_enabled(self) -> bool {
63        self.info().default_enabled
64    }
65
66    fn info(self) -> &'static FeatureSpec {
67        FEATURES
68            .iter()
69            .find(|spec| spec.id == self)
70            .unwrap_or_else(|| unreachable!("missing FeatureSpec for {:?}", self))
71    }
72}
73
74/// Holds the effective set of enabled features.
75#[derive(Debug, Clone, Default, PartialEq)]
76pub struct Features {
77    enabled: BTreeSet<Feature>,
78}
79
80impl Features {
81    /// Starts with built-in defaults.
82    pub fn with_defaults() -> Self {
83        let mut set = BTreeSet::new();
84        for spec in FEATURES {
85            if spec.default_enabled {
86                set.insert(spec.id);
87            }
88        }
89        Self { enabled: set }
90    }
91
92    pub fn enabled(&self, feature: Feature) -> bool {
93        self.enabled.contains(&feature)
94    }
95
96    pub fn enable(&mut self, feature: Feature) -> &mut Self {
97        self.enabled.insert(feature);
98        self
99    }
100
101    pub fn disable(&mut self, feature: Feature) -> &mut Self {
102        self.enabled.remove(&feature);
103        self
104    }
105
106    pub fn apply_map(&mut self, entries: &BTreeMap<String, bool>) {
107        for (key, enabled) in entries {
108            if let Some(feature) = feature_from_key(key) {
109                if *enabled {
110                    self.enable(feature);
111                } else {
112                    self.disable(feature);
113                }
114            }
115        }
116    }
117
118    pub fn enabled_features(&self) -> Vec<Feature> {
119        let mut list: Vec<_> = self.enabled.iter().copied().collect();
120        list.sort();
121        list
122    }
123}
124
125/// Keys accepted in `[features]` tables.
126pub fn is_known_feature_key(key: &str) -> bool {
127    FEATURES.iter().any(|spec| spec.key == key)
128}
129
130pub fn feature_from_key(key: &str) -> Option<Feature> {
131    FEATURES
132        .iter()
133        .find(|spec| spec.key == key)
134        .map(|spec| spec.id)
135}
136
137pub fn feature_spec_by_key(key: &str) -> Option<&'static FeatureSpec> {
138    FEATURES.iter().find(|spec| spec.key == key)
139}
140
141pub fn render_feature_table(features: &Features) -> String {
142    let mut output = String::from("feature\tstage\tenabled\n");
143    for spec in FEATURES {
144        let _ = writeln!(
145            output,
146            "{}\t{}\t{}",
147            spec.key,
148            spec.stage,
149            features.enabled(spec.id)
150        );
151    }
152    output
153}
154
155/// Deserializable features table for TOML.
156#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq)]
157pub struct FeaturesToml {
158    #[serde(flatten)]
159    pub entries: BTreeMap<String, bool>,
160}
161
162/// Single registry of all feature definitions.
163#[derive(Debug, Clone, Copy)]
164pub struct FeatureSpec {
165    pub id: Feature,
166    pub key: &'static str,
167    pub stage: Stage,
168    pub default_enabled: bool,
169}
170
171pub const FEATURES: &[FeatureSpec] = &[
172    FeatureSpec {
173        id: Feature::ShellTool,
174        key: "shell_tool",
175        stage: Stage::Stable,
176        default_enabled: true,
177    },
178    FeatureSpec {
179        id: Feature::Subagents,
180        key: "subagents",
181        stage: Stage::Experimental,
182        default_enabled: true,
183    },
184    FeatureSpec {
185        id: Feature::WebSearch,
186        key: "web_search",
187        stage: Stage::Experimental,
188        default_enabled: true,
189    },
190    FeatureSpec {
191        id: Feature::ApplyPatch,
192        key: "apply_patch",
193        stage: Stage::Experimental,
194        default_enabled: true,
195    },
196    FeatureSpec {
197        id: Feature::Mcp,
198        key: "mcp",
199        stage: Stage::Experimental,
200        default_enabled: true,
201    },
202    FeatureSpec {
203        id: Feature::ExecPolicy,
204        key: "exec_policy",
205        stage: Stage::Experimental,
206        default_enabled: true,
207    },
208];