Skip to main content

lean_ctx/core/config/
value_display.rs

1// SPDX-License-Identifier: Apache-2.0
2//! `[value_display]` — how lean-ctx shows its measured value outside the
3//! dashboard (status line, turn/session recaps, prompt segment, milestones).
4//!
5//! None of these channels reach the model's context; they are user-only.
6
7use serde::{Deserialize, Serialize};
8
9/// How much of lean-ctx's value is surfaced.
10///
11/// - `off`: nothing (no snapshot is written either)
12/// - `minimal` (default): status line / prompt segment, plus a recap when a
13///   turn window crossed its threshold
14/// - `milestones`: `minimal` plus OS notifications for milestones
15/// - `verbose`: every recap window, regardless of the token threshold
16#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(rename_all = "lowercase")]
18pub enum ValueDisplayMode {
19    Off,
20    #[default]
21    Minimal,
22    Milestones,
23    Verbose,
24}
25
26impl ValueDisplayMode {
27    pub fn parse(value: &str) -> Option<Self> {
28        match value.trim().to_ascii_lowercase().as_str() {
29            "off" | "0" | "false" | "none" => Some(Self::Off),
30            "minimal" | "on" | "1" | "true" => Some(Self::Minimal),
31            "milestones" => Some(Self::Milestones),
32            "verbose" => Some(Self::Verbose),
33            _ => None,
34        }
35    }
36}
37
38/// How the Claude Code status line draws lean-ctx's segment.
39///
40/// - `brand` (default): LeanCTX colours, the work grouped as SELECT and CONTROL
41/// - `subtle`: the single dim line other channels use
42#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(rename_all = "lowercase")]
44pub enum StatuslineStyle {
45    #[default]
46    Brand,
47    Subtle,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(default)]
52pub struct ValueDisplayConfig {
53    /// Override via `LEAN_CTX_VALUE_DISPLAY`.
54    pub mode: ValueDisplayMode,
55    /// Claude Code status line style: `brand` or `subtle`.
56    pub statusline: StatuslineStyle,
57    /// A turn recap is considered every N agent turns.
58    pub recap_every_turns: u32,
59    /// …and only shown when the window saved at least this many tokens (or a
60    /// security event happened).
61    pub recap_min_tokens: u64,
62    /// OS notifications for milestones (at most one per day) in mode
63    /// `milestones` or `verbose`; `false` silences them there.
64    pub notifications: bool,
65    /// Opt-in `lean-ctx:` trailer in commit messages.
66    pub git_trailer: bool,
67}
68
69impl Default for ValueDisplayConfig {
70    fn default() -> Self {
71        Self {
72            mode: ValueDisplayMode::default(),
73            statusline: StatuslineStyle::default(),
74            // 50K per 10 turns was rarely reached in real sessions, so most
75            // users never saw a recap. 10K per 5 turns is still quiet (a recap
76            // needs a real saving) but shows up in a normal working session.
77            recap_every_turns: 5,
78            recap_min_tokens: 10_000,
79            notifications: true,
80            git_trailer: false,
81        }
82    }
83}
84
85impl ValueDisplayConfig {
86    /// The configured mode, with `LEAN_CTX_VALUE_DISPLAY` taking precedence.
87    pub fn effective_mode(&self) -> ValueDisplayMode {
88        std::env::var("LEAN_CTX_VALUE_DISPLAY")
89            .ok()
90            .and_then(|v| ValueDisplayMode::parse(&v))
91            .unwrap_or(self.mode)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn defaults_are_subtle_and_on() {
101        let cfg: ValueDisplayConfig = toml::from_str("").unwrap();
102        assert_eq!(cfg, ValueDisplayConfig::default());
103        assert_eq!(cfg.mode, ValueDisplayMode::Minimal);
104        assert!(!cfg.git_trailer);
105    }
106
107    #[test]
108    fn parses_toml_section() {
109        let cfg: ValueDisplayConfig =
110            toml::from_str("mode = \"off\"\nrecap_every_turns = 20").unwrap();
111        assert_eq!(cfg.mode, ValueDisplayMode::Off);
112        assert_eq!(cfg.recap_every_turns, 20);
113        assert_eq!(cfg.recap_min_tokens, 10_000);
114        assert_eq!(cfg.statusline, StatuslineStyle::Brand);
115    }
116
117    #[test]
118    fn statusline_style_parses() {
119        let cfg: ValueDisplayConfig = toml::from_str("statusline = \"subtle\"").unwrap();
120        assert_eq!(cfg.statusline, StatuslineStyle::Subtle);
121        assert!(toml::from_str::<ValueDisplayConfig>("statusline = \"loud\"").is_err());
122    }
123
124    #[test]
125    fn env_values_parse() {
126        assert_eq!(ValueDisplayMode::parse("OFF"), Some(ValueDisplayMode::Off));
127        assert_eq!(ValueDisplayMode::parse("0"), Some(ValueDisplayMode::Off));
128        assert_eq!(
129            ValueDisplayMode::parse("verbose"),
130            Some(ValueDisplayMode::Verbose)
131        );
132        assert_eq!(ValueDisplayMode::parse("loud"), None);
133    }
134}