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    /// Long-lived display stages that keep input enabled between turns.
30    pub const fn is_stage(self) -> bool {
31        matches!(self, Self::Planning | Self::Building | Self::Recovery)
32    }
33
34    pub const fn status(self) -> Option<&'static str> {
35        match self {
36            Self::Idle => None,
37            Self::Planning => Some("Planning..."),
38            Self::PreparingFreshExecutionThread => Some("Preparing fresh execution thread..."),
39            Self::RestoringApprovedPlan => Some("Restoring approved plan..."),
40            Self::Building => Some("Building..."),
41            Self::StartingBuild => Some("Starting build..."),
42            Self::Recovery => Some("Recovery: tools disabled..."),
43            Self::Blocked => Some("Blocked — Type 'continue' to retry..."),
44        }
45    }
46}
47
48#[cfg(test)]
49mod tests {
50    use super::ActivityState;
51
52    #[test]
53    fn idle_has_no_status_and_is_not_busy_or_stage() {
54        assert_eq!(ActivityState::Idle.status(), None);
55        assert!(!ActivityState::Idle.is_busy());
56        assert!(!ActivityState::Idle.is_stage());
57    }
58
59    #[test]
60    fn stages_are_not_busy_but_keep_a_status() {
61        for state in [ActivityState::Planning, ActivityState::Building] {
62            assert!(!state.is_busy());
63            assert!(state.is_stage());
64            assert!(state.status().is_some());
65        }
66        assert_eq!(ActivityState::Planning.status(), Some("Planning..."));
67        assert_eq!(ActivityState::Building.status(), Some("Building..."));
68    }
69
70    #[test]
71    fn transient_handoffs_are_busy_but_not_stages() {
72        for state in [
73            ActivityState::PreparingFreshExecutionThread,
74            ActivityState::RestoringApprovedPlan,
75            ActivityState::StartingBuild,
76        ] {
77            assert!(state.is_busy());
78            assert!(!state.is_stage());
79            assert!(state.status().is_some());
80        }
81    }
82}