Skip to main content

codewhale_config/
app_mode.rs

1//! The TUI's user-facing operating mode. Lives in codewhale-config so
2//! settings, receipts, and other crates can name it without depending on
3//! the TUI; the TUI adds the localized picker strings through an extension
4//! trait.
5
6/// Supported application modes for the TUI.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum AppMode {
9    Agent,
10    #[allow(dead_code)]
11    Auto,
12    /// Legacy compatibility alias; resolves to [`Self::Agent`] + bypass approvals.
13    Yolo,
14    Plan,
15    Operate,
16}
17
18impl AppMode {
19    /// Productive keyboard cycle: Plan -> Act -> Operate -> Plan.
20    ///
21    /// `Auto` remains an internal variant while the real implementation is
22    /// redesigned; do not expose it through user-facing mode selection (#3733).
23    /// `Yolo` is kept for parse/back-compat only and is not in the Tab cycle.
24    /// Operate joins the visible cycle because ordinary messages can now
25    /// coordinate background workers without requiring a Workflow definition.
26    pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate];
27
28    #[must_use]
29    pub fn parse(value: &str) -> Option<Self> {
30        match value.trim().to_ascii_lowercase().as_str() {
31            "agent" | "act" | "work" | "auto" | "1" => Some(Self::Agent),
32            "plan" | "2" => Some(Self::Plan),
33            "operate" | "operation" | "ops" | "3" => Some(Self::Operate),
34            // Invisible one-way permission shorthand only — never a visible mode.
35            "yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" => {
36                Some(Self::Yolo)
37            }
38            _ => None,
39        }
40    }
41
42    #[must_use]
43    pub fn from_setting(value: &str) -> Self {
44        // Unreleased Multitask never shipped; normalize leftover settings to Operate.
45        match value.trim().to_ascii_lowercase().as_str() {
46            "multitask" | "multi" | "5" => Self::Operate,
47            other => Self::parse(other).unwrap_or(Self::Agent),
48        }
49    }
50
51    #[must_use]
52    pub fn as_setting(self) -> &'static str {
53        match self {
54            Self::Agent => "agent",
55            Self::Auto => "agent",
56            // Write current permission vocabulary, not the legacy YOLO label.
57            Self::Yolo => "agent",
58            Self::Plan => "plan",
59            Self::Operate => "operate",
60        }
61    }
62
63    /// Short label used in the UI footer.
64    pub fn label(self) -> &'static str {
65        match self {
66            AppMode::Agent => "ACT",
67            AppMode::Auto => "ACT",
68            AppMode::Yolo => "ACT",
69            AppMode::Plan => "PLAN",
70            AppMode::Operate => "OPERATE",
71        }
72    }
73
74    #[must_use]
75    pub fn display_name(self) -> &'static str {
76        match self {
77            AppMode::Agent => "Act",
78            AppMode::Auto => "Act",
79            AppMode::Yolo => "Act",
80            AppMode::Plan => "Plan",
81            AppMode::Operate => "Operate",
82        }
83    }
84
85    #[must_use]
86    pub fn number(self) -> char {
87        match self {
88            AppMode::Agent | AppMode::Auto | AppMode::Yolo => '1',
89            AppMode::Plan => '2',
90            AppMode::Operate => '3',
91        }
92    }
93
94    #[must_use]
95    pub fn uses_agent_baseline(self) -> bool {
96        matches!(self, Self::Agent | Self::Auto | Self::Operate)
97    }
98
99    /// Operate gets a higher parallel launch floor so background fan-out is
100    /// not throttled to a single slot when config is low.
101    #[must_use]
102    pub fn mode_delegation_launch_floor(self) -> usize {
103        match self {
104            Self::Operate => 4,
105            _ => 1,
106        }
107    }
108
109    #[allow(dead_code)]
110    /// Description shown in help or onboarding text.
111    pub fn description(self) -> &'static str {
112        match self {
113            AppMode::Agent | AppMode::Auto => {
114                "Act mode - direct work in the current session with tools"
115            }
116            AppMode::Yolo => "Act mode with Full Access (legacy compatibility setting)",
117            AppMode::Plan => "Plan mode - research and design before implementing",
118            AppMode::Operate => "Operate mode - send tasks while Fleet workers run in parallel",
119        }
120    }
121
122    #[must_use]
123    pub fn next(self) -> Self {
124        let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
125            return Self::Agent;
126        };
127        Self::CYCLE[(index + 1) % Self::CYCLE.len()]
128    }
129
130    #[must_use]
131    pub fn previous(self) -> Self {
132        let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
133            return Self::Agent;
134        };
135        Self::CYCLE[(index + Self::CYCLE.len() - 1) % Self::CYCLE.len()]
136    }
137}