Skip to main content

vtcode_commons/ui_protocol/
activity.rs

1//! Explicit global activity states shared by the runloop and terminal UI.
2
3#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
4pub enum ActivityState {
5    #[default]
6    Idle,
7    /// The agent is working inside the plan workflow (research/synthesis).
8    Planning,
9    PreparingFreshExecutionThread,
10    RestoringApprovedPlan,
11    /// The agent is executing an approved plan.
12    Building,
13    StartingBuild,
14    /// A bounded tool-free recovery pass is running after blocked tool calls.
15    /// Tools are disabled for this pass; input stays enabled.
16    Recovery,
17    /// The turn is blocked and waiting for user guidance (`continue`, new
18    /// instructions, or `vtcode --resume`). Distinct from `Idle` so the TUI
19    /// can keep a persistent blocked hint visible.
20    Blocked,
21}
22
23impl ActivityState {
24    /// Transient handoff states that must block input and mode switches.
25    pub const fn is_busy(self) -> bool {
26        matches!(self, Self::PreparingFreshExecutionThread | Self::RestoringApprovedPlan | Self::StartingBuild)
27    }
28
29    /// Whether primary-agent/mode switching must be rejected while this state
30    /// is visible. Long-lived Build, Recovery, and Blocked states keep input
31    /// enabled, but their active turn state still owns the mode boundary.
32    pub const fn locks_mode_switch(self) -> bool {
33        matches!(
34            self,
35            Self::PreparingFreshExecutionThread
36                | Self::RestoringApprovedPlan
37                | Self::Building
38                | Self::StartingBuild
39                | Self::Recovery
40                | Self::Blocked
41        )
42    }
43
44    /// Long-lived display stages that keep input enabled between turns.
45    pub const fn is_stage(self) -> bool {
46        matches!(self, Self::Planning | Self::Building | Self::Recovery)
47    }
48
49    pub const fn status(self) -> Option<&'static str> {
50        match self {
51            Self::Idle => None,
52            Self::Planning => Some("Planning..."),
53            Self::PreparingFreshExecutionThread => Some("Preparing fresh execution thread..."),
54            Self::RestoringApprovedPlan => Some("Restoring approved plan..."),
55            Self::Building => Some("Building..."),
56            Self::StartingBuild => Some("Starting build..."),
57            Self::Recovery => Some("Recovery: tools disabled..."),
58            Self::Blocked => Some("Blocked — Type 'continue' to retry..."),
59        }
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::ActivityState;
66
67    #[test]
68    fn idle_has_no_status_and_is_not_busy_or_stage() {
69        assert_eq!(ActivityState::Idle.status(), None);
70        assert!(!ActivityState::Idle.is_busy());
71        assert!(!ActivityState::Idle.is_stage());
72    }
73
74    #[test]
75    fn stages_are_not_busy_but_keep_a_status() {
76        for state in [ActivityState::Planning, ActivityState::Building] {
77            assert!(!state.is_busy());
78            assert!(state.is_stage());
79            assert!(state.status().is_some());
80        }
81        assert_eq!(ActivityState::Planning.status(), Some("Planning..."));
82        assert_eq!(ActivityState::Building.status(), Some("Building..."));
83    }
84
85    #[test]
86    fn transient_handoffs_are_busy_but_not_stages() {
87        for state in [
88            ActivityState::PreparingFreshExecutionThread,
89            ActivityState::RestoringApprovedPlan,
90            ActivityState::StartingBuild,
91        ] {
92            assert!(state.is_busy());
93            assert!(!state.is_stage());
94            assert!(state.status().is_some());
95        }
96    }
97
98    #[test]
99    fn mode_switch_is_locked_for_active_build_recovery_and_blocked_states() {
100        for state in [
101            ActivityState::PreparingFreshExecutionThread,
102            ActivityState::RestoringApprovedPlan,
103            ActivityState::Building,
104            ActivityState::StartingBuild,
105            ActivityState::Recovery,
106            ActivityState::Blocked,
107        ] {
108            assert!(state.locks_mode_switch(), "{state:?} must lock mode switching");
109        }
110        assert!(!ActivityState::Idle.locks_mode_switch());
111        assert!(!ActivityState::Planning.locks_mode_switch());
112    }
113}