Skip to main content

forge_agent/
commands.rs

1//! Mode-state helpers - supported-mode list filtering + `ModeState`
2//! builder. Used by the worker to assemble the Connected event payload
3//! at spawn time.
4
5use std::collections::HashSet;
6
7use forge_primitives::permission::PermissionMode;
8use forge_primitives::{ModeInfo, ModeState};
9
10/// Canonical MODE_OPTIONS order mirrored from upstream.
11const CANONICAL_ORDER: [PermissionMode; 6] = [
12    PermissionMode::Ask,
13    PermissionMode::AcceptEdits,
14    PermissionMode::Plan,
15    PermissionMode::DontAsk,
16    PermissionMode::Auto,
17    PermissionMode::BypassPermissions,
18];
19
20/// Returns the supported-mode list filtered by the runtime-unavailable
21/// list (but keeping the current mode if it's still set). Mirrors the
22/// upstream rules: BASE + Auto (if model supports it) + the current
23/// mode itself (so the active mode never disappears mid-session).
24/// BypassPermissions is offered only when the session launched into
25/// it - the CLI refuses a mid-session entry.
26pub fn supported_mode_ids_filtered(
27    current_model_supports_auto_mode: bool,
28    launched_into_bypass: bool,
29    current_mode: Option<PermissionMode>,
30    runtime_unavailable_mode_ids: &[PermissionMode],
31) -> Vec<PermissionMode> {
32    let mut seen: HashSet<PermissionMode> = CANONICAL_ORDER[..4].iter().copied().collect();
33    if current_model_supports_auto_mode {
34        seen.insert(PermissionMode::Auto);
35    }
36    if launched_into_bypass {
37        seen.insert(PermissionMode::BypassPermissions);
38    }
39    if let Some(mode) = current_mode {
40        seen.insert(mode);
41    }
42    CANONICAL_ORDER
43        .into_iter()
44        .filter(|m| seen.contains(m))
45        .filter(|m| current_mode == Some(*m) || !runtime_unavailable_mode_ids.contains(m))
46        .collect()
47}
48
49/// Composes a `ModeState` from the active mode + the resolved
50/// supported-mode list.
51pub fn build_mode_state_from_supported(
52    mode: PermissionMode,
53    supported_mode_ids: &[PermissionMode],
54) -> ModeState {
55    ModeState {
56        current_mode_id: mode.as_wire().to_owned(),
57        current_mode_name: mode.display_name().to_owned(),
58        available_modes: supported_mode_ids
59            .iter()
60            .map(|m| ModeInfo {
61                id: m.as_wire().to_owned(),
62                name: m.display_name().to_owned(),
63                description: None,
64            })
65            .collect(),
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn base_supported_modes_exclude_bypass() {
75        let supported = supported_mode_ids_filtered(false, false, None, &[]);
76        assert_eq!(
77            supported,
78            vec![
79                PermissionMode::Ask,
80                PermissionMode::AcceptEdits,
81                PermissionMode::Plan,
82                PermissionMode::DontAsk
83            ]
84        );
85    }
86
87    #[test]
88    fn bypass_offered_when_launched_into_it() {
89        let supported = supported_mode_ids_filtered(false, true, None, &[]);
90        assert!(supported.contains(&PermissionMode::BypassPermissions));
91    }
92
93    #[test]
94    fn auto_mode_appears_when_current_model_supports_it() {
95        let supported = supported_mode_ids_filtered(true, false, None, &[]);
96        assert!(supported.contains(&PermissionMode::Auto));
97    }
98
99    #[test]
100    fn current_mode_survives_runtime_unavailable_filter() {
101        let supported = supported_mode_ids_filtered(
102            false,
103            false,
104            Some(PermissionMode::Plan),
105            &[PermissionMode::Plan],
106        );
107        assert!(supported.contains(&PermissionMode::Plan));
108    }
109
110    #[test]
111    fn build_mode_state_from_supported_uses_supported_list() {
112        let supported = vec![
113            PermissionMode::Ask,
114            PermissionMode::AcceptEdits,
115            PermissionMode::Plan,
116            PermissionMode::DontAsk,
117        ];
118        let state = build_mode_state_from_supported(PermissionMode::AcceptEdits, &supported);
119        assert_eq!(state.current_mode_id, "acceptEdits");
120        assert_eq!(state.current_mode_name, "Accept Edits");
121        assert_eq!(state.available_modes.len(), 4);
122        assert_eq!(state.available_modes[0].id, "default");
123    }
124}