Skip to main content

hanzo_protocol/
config_types.rs

1use schemars::JsonSchema;
2use serde::Deserialize;
3use serde::Serialize;
4use strum_macros::Display;
5use strum_macros::EnumIter;
6use ts_rs::TS;
7
8pub use crate::openai_models::ReasoningEffort;
9
10/// A summary of the reasoning performed by the model. This can be useful for
11/// debugging and understanding the model's reasoning process.
12/// See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#reasoning-summaries
13#[derive(
14    Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
15)]
16#[serde(rename_all = "lowercase")]
17#[strum(serialize_all = "lowercase")]
18pub enum ReasoningSummary {
19    #[default]
20    Auto,
21    Concise,
22    Detailed,
23    /// Option to disable reasoning summaries.
24    None,
25}
26
27/// Controls output length/detail on GPT-5 models via the Responses API.
28/// Serialized with lowercase values to match the OpenAI API.
29#[derive(
30    Hash,
31    Debug,
32    Serialize,
33    Deserialize,
34    Default,
35    Clone,
36    Copy,
37    PartialEq,
38    Eq,
39    Display,
40    JsonSchema,
41    TS,
42)]
43#[serde(rename_all = "lowercase")]
44#[strum(serialize_all = "lowercase")]
45pub enum Verbosity {
46    Low,
47    #[default]
48    Medium,
49    High,
50}
51
52#[derive(
53    Deserialize, Debug, Clone, Copy, PartialEq, Default, Serialize, Display, JsonSchema, TS,
54)]
55#[serde(rename_all = "kebab-case")]
56#[strum(serialize_all = "kebab-case")]
57pub enum SandboxMode {
58    #[serde(rename = "read-only")]
59    #[default]
60    ReadOnly,
61
62    #[serde(rename = "workspace-write")]
63    WorkspaceWrite,
64
65    #[serde(rename = "danger-full-access")]
66    DangerFullAccess,
67}
68
69#[derive(
70    Deserialize, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Display, JsonSchema, TS,
71)]
72#[serde(rename_all = "kebab-case")]
73#[strum(serialize_all = "kebab-case")]
74pub enum WindowsSandboxLevel {
75    #[default]
76    Disabled,
77    RestrictedToken,
78    Elevated,
79}
80
81#[derive(
82    Debug,
83    Serialize,
84    Deserialize,
85    Clone,
86    Copy,
87    PartialEq,
88    Eq,
89    Display,
90    JsonSchema,
91    TS,
92    PartialOrd,
93    Ord,
94    EnumIter,
95)]
96#[serde(rename_all = "lowercase")]
97#[strum(serialize_all = "lowercase")]
98pub enum Personality {
99    None,
100    Friendly,
101    Pragmatic,
102}
103
104#[derive(
105    Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS, Default,
106)]
107#[serde(rename_all = "lowercase")]
108#[strum(serialize_all = "lowercase")]
109pub enum WebSearchMode {
110    Disabled,
111    #[default]
112    Cached,
113    Live,
114}
115
116#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
117#[serde(rename_all = "lowercase")]
118#[strum(serialize_all = "lowercase")]
119pub enum ServiceTier {
120    /// Legacy compatibility value for older local config files.
121    Standard,
122    Fast,
123}
124
125#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
126#[serde(rename_all = "lowercase")]
127#[strum(serialize_all = "lowercase")]
128pub enum ForcedLoginMethod {
129    Chatgpt,
130    Api,
131}
132
133/// Represents the trust level for a project directory.
134/// This determines the approval policy and sandbox mode applied.
135#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS)]
136#[serde(rename_all = "lowercase")]
137#[strum(serialize_all = "lowercase")]
138pub enum TrustLevel {
139    Trusted,
140    Untrusted,
141}
142
143/// Controls whether the TUI uses the terminal's alternate screen buffer.
144///
145/// **Background:** The alternate screen buffer provides a cleaner fullscreen experience
146/// without polluting the terminal's scrollback history. However, it conflicts with terminal
147/// multiplexers like Zellij that strictly follow the xterm specification, which defines
148/// that alternate screen buffers should not have scrollback.
149///
150/// **Zellij's behavior:** Zellij intentionally disables scrollback in alternate screen mode
151/// (see https://github.com/zellij-org/zellij/pull/1032) to comply with the xterm spec. This
152/// is by design and not configurable in Zellij—there is no option to enable scrollback in
153/// alternate screen mode.
154///
155/// **Solution:** This setting provides a pragmatic workaround:
156/// - `auto` (default): Automatically detect the terminal multiplexer. If running in Zellij,
157///   disable alternate screen to preserve scrollback. Enable it everywhere else.
158/// - `always`: Always use alternate screen mode (original behavior before this fix).
159/// - `never`: Never use alternate screen mode. Runs in inline mode, preserving scrollback
160///   in all multiplexers.
161///
162/// The CLI flag `--no-alt-screen` can override this setting at runtime.
163#[derive(
164    Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, Display, JsonSchema, TS,
165)]
166#[serde(rename_all = "lowercase")]
167#[strum(serialize_all = "lowercase")]
168pub enum AltScreenMode {
169    /// Auto-detect: disable alternate screen in Zellij, enable elsewhere.
170    #[default]
171    Auto,
172    /// Always use alternate screen (original behavior).
173    Always,
174    /// Never use alternate screen (inline mode only).
175    Never,
176}
177
178/// Initial collaboration mode to use when the TUI starts.
179#[derive(
180    Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema, TS, Default,
181)]
182#[serde(rename_all = "snake_case")]
183pub enum ModeKind {
184    Plan,
185    #[default]
186    #[serde(
187        alias = "code",
188        alias = "pair_programming",
189        alias = "execute",
190        alias = "custom"
191    )]
192    Default,
193    #[doc(hidden)]
194    #[serde(skip_serializing, skip_deserializing)]
195    #[schemars(skip)]
196    #[ts(skip)]
197    PairProgramming,
198    #[doc(hidden)]
199    #[serde(skip_serializing, skip_deserializing)]
200    #[schemars(skip)]
201    #[ts(skip)]
202    Execute,
203}
204
205pub const TUI_VISIBLE_COLLABORATION_MODES: [ModeKind; 2] = [ModeKind::Default, ModeKind::Plan];
206
207impl ModeKind {
208    pub const fn display_name(self) -> &'static str {
209        match self {
210            Self::Plan => "Plan",
211            Self::Default => "Default",
212            Self::PairProgramming => "Pair Programming",
213            Self::Execute => "Execute",
214        }
215    }
216
217    pub const fn is_tui_visible(self) -> bool {
218        matches!(self, Self::Plan | Self::Default)
219    }
220
221    pub const fn allows_request_user_input(self) -> bool {
222        matches!(self, Self::Plan)
223    }
224}
225
226/// Collaboration mode for a Codex session.
227#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
228#[serde(rename_all = "lowercase")]
229pub struct CollaborationMode {
230    pub mode: ModeKind,
231    pub settings: Settings,
232}
233
234impl CollaborationMode {
235    /// Returns a reference to the settings.
236    fn settings_ref(&self) -> &Settings {
237        &self.settings
238    }
239
240    pub fn model(&self) -> &str {
241        self.settings_ref().model.as_str()
242    }
243
244    pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
245        self.settings_ref().reasoning_effort
246    }
247
248    /// Updates the collaboration mode with new model and/or effort values.
249    ///
250    /// - `model`: `Some(s)` to update the model, `None` to keep the current model
251    /// - `effort`: `Some(Some(e))` to set effort to `e`, `Some(None)` to clear effort, `None` to keep current effort
252    /// - `developer_instructions`: `Some(Some(s))` to set instructions, `Some(None)` to clear them, `None` to keep current
253    ///
254    /// Returns a new `CollaborationMode` with updated values, preserving the mode.
255    pub fn with_updates(
256        &self,
257        model: Option<String>,
258        effort: Option<Option<ReasoningEffort>>,
259        developer_instructions: Option<Option<String>>,
260    ) -> Self {
261        let settings = self.settings_ref();
262        let updated_settings = Settings {
263            model: model.unwrap_or_else(|| settings.model.clone()),
264            reasoning_effort: effort.unwrap_or(settings.reasoning_effort),
265            developer_instructions: developer_instructions
266                .unwrap_or_else(|| settings.developer_instructions.clone()),
267        };
268
269        CollaborationMode {
270            mode: self.mode,
271            settings: updated_settings,
272        }
273    }
274
275    /// Applies a mask to this collaboration mode, returning a new collaboration mode
276    /// with the mask values applied. Fields in the mask that are `Some` will override
277    /// the corresponding fields, while `None` values will preserve the original values.
278    ///
279    /// The `name` field in the mask is ignored as it's metadata for the mask itself.
280    pub fn apply_mask(&self, mask: &CollaborationModeMask) -> Self {
281        let settings = self.settings_ref();
282        CollaborationMode {
283            mode: mask.mode.unwrap_or(self.mode),
284            settings: Settings {
285                model: mask.model.clone().unwrap_or_else(|| settings.model.clone()),
286                reasoning_effort: mask.reasoning_effort.unwrap_or(settings.reasoning_effort),
287                developer_instructions: mask
288                    .developer_instructions
289                    .clone()
290                    .unwrap_or_else(|| settings.developer_instructions.clone()),
291            },
292        }
293    }
294}
295
296/// Settings for a collaboration mode.
297#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
298pub struct Settings {
299    pub model: String,
300    pub reasoning_effort: Option<ReasoningEffort>,
301    pub developer_instructions: Option<String>,
302}
303
304/// A mask for collaboration mode settings, allowing partial updates.
305/// All fields except `name` are optional, enabling selective updates.
306#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize, Deserialize, JsonSchema, TS)]
307pub struct CollaborationModeMask {
308    pub name: String,
309    pub mode: Option<ModeKind>,
310    pub model: Option<String>,
311    pub reasoning_effort: Option<Option<ReasoningEffort>>,
312    pub developer_instructions: Option<Option<String>>,
313}
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318    use pretty_assertions::assert_eq;
319
320    #[test]
321    fn apply_mask_can_clear_optional_fields() {
322        let mode = CollaborationMode {
323            mode: ModeKind::Default,
324            settings: Settings {
325                model: "gpt-5.2-codex".to_string(),
326                reasoning_effort: Some(ReasoningEffort::High),
327                developer_instructions: Some("stay focused".to_string()),
328            },
329        };
330        let mask = CollaborationModeMask {
331            name: "Clear".to_string(),
332            mode: None,
333            model: None,
334            reasoning_effort: Some(None),
335            developer_instructions: Some(None),
336        };
337
338        let expected = CollaborationMode {
339            mode: ModeKind::Default,
340            settings: Settings {
341                model: "gpt-5.2-codex".to_string(),
342                reasoning_effort: None,
343                developer_instructions: None,
344            },
345        };
346        assert_eq!(expected, mode.apply_mask(&mask));
347    }
348
349    #[test]
350    fn mode_kind_deserializes_alias_values_to_default() {
351        for alias in ["code", "pair_programming", "execute", "custom"] {
352            let json = format!("\"{alias}\"");
353            let mode: ModeKind = serde_json::from_str(&json).expect("deserialize mode");
354            assert_eq!(ModeKind::Default, mode);
355        }
356    }
357
358    #[test]
359    fn tui_visible_collaboration_modes_match_mode_kind_visibility() {
360        let expected = [ModeKind::Default, ModeKind::Plan];
361        assert_eq!(expected, TUI_VISIBLE_COLLABORATION_MODES);
362
363        for mode in TUI_VISIBLE_COLLABORATION_MODES {
364            assert!(mode.is_tui_visible());
365        }
366
367        assert!(!ModeKind::PairProgramming.is_tui_visible());
368        assert!(!ModeKind::Execute.is_tui_visible());
369    }
370}