use std::collections::HashMap; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeStartOptions { #[serde(default)] pub working_dir: String, #[serde(default)] pub model: Option, #[serde(default)] pub api_key: Option, #[serde(default)] pub custom_instructions: Option, #[serde(default)] pub mcp_servers_json: Option, #[serde(default)] pub allowed_tools: Vec, #[serde(default)] pub skip_greeting: bool, #[serde(default)] pub resume_session_id: Option, #[serde(default)] pub use_worktree: bool, #[serde(default)] pub disable_1m_context: bool, #[serde(default)] pub max_output_tokens: Option, #[serde(default)] pub disable_cron: bool, #[serde(default = "default_include_git_instructions")] pub include_git_instructions: bool, #[serde(default = "default_enable_claudeai_mcp_servers")] pub enable_claudeai_mcp_servers: bool, #[serde(default)] pub auto_memory_directory: Option, #[serde(default)] pub model_overrides: Option>, #[serde(default)] pub session_name: Option, #[serde(default)] pub disable_skill_shell_execution: bool, /// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+). #[serde(default)] pub bare_mode: bool, /// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+). /// Defaults to true (matching CLI default). Set to false to suppress the dialog. #[serde(default = "default_show_clear_context")] pub show_clear_context_on_plan_accept: bool, /// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+). #[serde(default)] pub custom_model_option: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(default)] pub struct HikariConfig { #[serde(default)] pub model: Option, #[serde(default)] pub api_key: Option, #[serde(default)] pub custom_instructions: Option, #[serde(default)] pub mcp_servers_json: Option, #[serde(default)] pub auto_granted_tools: Vec, #[serde(default)] pub theme: Theme, #[serde(default = "default_greeting_enabled")] pub greeting_enabled: bool, #[serde(default)] pub greeting_custom_prompt: Option, #[serde(default = "default_notifications_enabled")] pub notifications_enabled: bool, #[serde(default = "default_notification_volume")] pub notification_volume: f32, #[serde(default)] pub always_on_top: bool, #[serde(default = "default_update_checks_enabled")] pub update_checks_enabled: bool, #[serde(default)] pub character_panel_width: Option, #[serde(default = "default_font_size")] pub font_size: u32, #[serde(default)] pub streamer_mode: bool, #[serde(default)] pub streamer_hide_paths: bool, #[serde(default)] pub compact_mode: bool, // Profile fields #[serde(default)] pub profile_name: Option, #[serde(default)] pub profile_avatar_path: Option, #[serde(default)] pub profile_bio: Option, // Custom theme colors #[serde(default)] pub custom_theme_colors: CustomThemeColors, // Token budget settings #[serde(default)] pub budget_enabled: bool, #[serde(default)] pub session_token_budget: Option, #[serde(default)] pub session_cost_budget: Option, #[serde(default = "default_budget_action")] pub budget_action: BudgetAction, #[serde(default = "default_budget_warning_threshold")] pub budget_warning_threshold: f32, #[serde(default = "default_discord_rpc_enabled")] pub discord_rpc_enabled: bool, #[serde(default)] pub use_worktree: bool, #[serde(default)] pub disable_1m_context: bool, #[serde(default)] pub max_output_tokens: Option, #[serde(default)] pub trusted_workspaces: Vec, // Background image settings #[serde(default)] pub background_image_path: Option, #[serde(default = "default_background_image_opacity")] pub background_image_opacity: f32, #[serde(default)] pub show_thinking_blocks: bool, // Custom terminal font settings #[serde(default)] pub custom_font_path: Option, #[serde(default)] pub custom_font_family: Option, // Custom UI font settings #[serde(default)] pub custom_ui_font_path: Option, #[serde(default)] pub custom_ui_font_family: Option, // Task Loop auto-commit settings #[serde(default)] pub task_loop_auto_commit: bool, #[serde(default = "default_task_loop_commit_prefix")] pub task_loop_commit_prefix: String, #[serde(default)] pub task_loop_include_summary: bool, #[serde(default)] pub disable_cron: bool, #[serde(default = "default_include_git_instructions")] pub include_git_instructions: bool, #[serde(default = "default_enable_claudeai_mcp_servers")] pub enable_claudeai_mcp_servers: bool, #[serde(default)] pub auto_memory_directory: Option, #[serde(default)] pub model_overrides: Option>, /// Prevents skill scripts from executing shell commands (Claude Code v2.1.91+). /// Passes `"disableSkillShellExecution": true` via the `--settings` flag. #[serde(default)] pub disable_skill_shell_execution: bool, /// Pass `--bare` flag to suppress UI chrome, useful for scripted headless `-p` calls (v2.1.81+). #[serde(default)] pub bare_mode: bool, /// Controls `showClearContextOnPlanAccept` in `--settings` (v2.1.81+). #[serde(default = "default_show_clear_context")] pub show_clear_context_on_plan_accept: bool, /// Sets `ANTHROPIC_CUSTOM_MODEL_OPTION` env var for custom model providers (v2.1.81+). #[serde(default)] pub custom_model_option: Option, } impl Default for HikariConfig { fn default() -> Self { Self { model: None, api_key: None, custom_instructions: None, mcp_servers_json: None, auto_granted_tools: Vec::new(), theme: Theme::default(), greeting_enabled: true, greeting_custom_prompt: None, notifications_enabled: true, notification_volume: 0.7, always_on_top: false, update_checks_enabled: true, character_panel_width: None, font_size: 14, streamer_mode: false, streamer_hide_paths: false, compact_mode: false, profile_name: None, profile_avatar_path: None, profile_bio: None, custom_theme_colors: CustomThemeColors::default(), budget_enabled: false, session_token_budget: None, session_cost_budget: None, budget_action: BudgetAction::Warn, budget_warning_threshold: 0.8, discord_rpc_enabled: true, use_worktree: false, disable_1m_context: false, max_output_tokens: None, trusted_workspaces: Vec::new(), background_image_path: None, background_image_opacity: 0.3, show_thinking_blocks: false, custom_font_path: None, custom_font_family: None, custom_ui_font_path: None, custom_ui_font_family: None, task_loop_auto_commit: false, task_loop_commit_prefix: "feat".to_string(), task_loop_include_summary: false, disable_cron: false, include_git_instructions: true, enable_claudeai_mcp_servers: true, auto_memory_directory: None, model_overrides: None, disable_skill_shell_execution: false, bare_mode: false, show_clear_context_on_plan_accept: true, custom_model_option: None, } } } fn default_update_checks_enabled() -> bool { true } fn default_greeting_enabled() -> bool { true } fn default_notifications_enabled() -> bool { true } fn default_notification_volume() -> f32 { 0.7 } fn default_font_size() -> u32 { 14 } fn default_budget_action() -> BudgetAction { BudgetAction::Warn } fn default_budget_warning_threshold() -> f32 { 0.8 } fn default_discord_rpc_enabled() -> bool { true } fn default_background_image_opacity() -> f32 { 0.3 } fn default_task_loop_commit_prefix() -> String { "feat".to_string() } fn default_include_git_instructions() -> bool { true } fn default_enable_claudeai_mcp_servers() -> bool { true } fn default_show_clear_context() -> bool { true } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum BudgetAction { #[default] Warn, Block, } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] #[serde(rename_all = "lowercase")] pub enum Theme { #[default] Dark, Light, #[serde(rename = "high-contrast")] HighContrast, Custom, Dracula, Catppuccin, Nord, Solarized, #[serde(rename = "solarized-light")] SolarizedLight, #[serde(rename = "catppuccin-latte")] CatppuccinLatte, #[serde(rename = "gruvbox-light")] GruvboxLight, #[serde(rename = "rose-pine-dawn")] RosePineDawn, } #[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] pub struct CustomThemeColors { #[serde(default)] pub bg_primary: Option, #[serde(default)] pub bg_secondary: Option, #[serde(default)] pub bg_terminal: Option, #[serde(default)] pub accent_primary: Option, #[serde(default)] pub accent_secondary: Option, #[serde(default)] pub text_primary: Option, #[serde(default)] pub text_secondary: Option, #[serde(default)] pub border_color: Option, } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = HikariConfig::default(); assert!(config.model.is_none()); assert!(config.api_key.is_none()); assert!(config.custom_instructions.is_none()); assert!(config.mcp_servers_json.is_none()); assert!(config.auto_granted_tools.is_empty()); assert_eq!(config.theme, Theme::Dark); assert!(config.greeting_enabled); assert!(config.greeting_custom_prompt.is_none()); assert!(!config.always_on_top); assert!(config.update_checks_enabled); assert!(config.character_panel_width.is_none()); assert_eq!(config.font_size, 14); assert!(!config.streamer_mode); assert!(!config.streamer_hide_paths); assert!(!config.compact_mode); assert!(config.profile_name.is_none()); assert!(config.profile_avatar_path.is_none()); assert!(config.profile_bio.is_none()); assert_eq!(config.custom_theme_colors, CustomThemeColors::default()); assert!(!config.budget_enabled); assert!(config.session_token_budget.is_none()); assert!(config.session_cost_budget.is_none()); assert_eq!(config.budget_action, BudgetAction::Warn); assert!((config.budget_warning_threshold - 0.8).abs() < f32::EPSILON); assert!(config.discord_rpc_enabled); assert!(!config.use_worktree); assert!(!config.disable_1m_context); assert!(config.trusted_workspaces.is_empty()); assert!(!config.show_thinking_blocks); assert!(config.custom_font_path.is_none()); assert!(config.custom_font_family.is_none()); assert!(config.custom_ui_font_path.is_none()); assert!(config.custom_ui_font_family.is_none()); assert!(!config.task_loop_auto_commit); assert_eq!(config.task_loop_commit_prefix, "feat"); assert!(!config.task_loop_include_summary); assert!(!config.disable_cron); assert!(config.include_git_instructions); assert!(config.enable_claudeai_mcp_servers); assert!(config.auto_memory_directory.is_none()); assert!(config.model_overrides.is_none()); assert!(!config.disable_skill_shell_execution); assert!(!config.bare_mode); assert!(config.show_clear_context_on_plan_accept); assert!(config.custom_model_option.is_none()); } #[test] fn test_config_serialization() { let config = HikariConfig { model: Some("claude-sonnet-4-20250514".to_string()), api_key: None, custom_instructions: Some("Be helpful".to_string()), mcp_servers_json: None, auto_granted_tools: vec!["Read".to_string(), "Glob".to_string()], theme: Theme::Light, greeting_enabled: true, greeting_custom_prompt: Some("Hello!".to_string()), notifications_enabled: true, notification_volume: 0.7, always_on_top: true, update_checks_enabled: true, character_panel_width: Some(400), font_size: 16, streamer_mode: false, streamer_hide_paths: false, compact_mode: false, profile_name: Some("Test User".to_string()), profile_avatar_path: None, profile_bio: Some("A test bio".to_string()), custom_theme_colors: CustomThemeColors::default(), budget_enabled: true, session_token_budget: Some(100000), session_cost_budget: Some(1.50), budget_action: BudgetAction::Block, budget_warning_threshold: 0.75, discord_rpc_enabled: true, use_worktree: true, disable_1m_context: false, max_output_tokens: Some(32000), trusted_workspaces: vec!["/home/naomi/projects/trusted".to_string()], background_image_path: Some("/home/naomi/bg.png".to_string()), background_image_opacity: 0.25, show_thinking_blocks: true, custom_font_path: Some("/home/naomi/.fonts/MyFont.ttf".to_string()), custom_font_family: Some("MyFont".to_string()), custom_ui_font_path: None, custom_ui_font_family: None, task_loop_auto_commit: true, task_loop_commit_prefix: "fix".to_string(), task_loop_include_summary: true, disable_cron: true, include_git_instructions: false, enable_claudeai_mcp_servers: false, auto_memory_directory: Some("/custom/memory".to_string()), model_overrides: Some(HashMap::from([( "claude-opus-4-6".to_string(), "arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1".to_string(), )])), disable_skill_shell_execution: true, bare_mode: false, show_clear_context_on_plan_accept: true, custom_model_option: None, }; let json = serde_json::to_string(&config).unwrap(); let deserialized: HikariConfig = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.model, config.model); assert_eq!(deserialized.custom_instructions, config.custom_instructions); assert_eq!(deserialized.auto_granted_tools, config.auto_granted_tools); assert_eq!(deserialized.theme, Theme::Light); assert!(deserialized.greeting_enabled); assert_eq!( deserialized.greeting_custom_prompt, Some("Hello!".to_string()) ); assert!(deserialized.task_loop_auto_commit); assert_eq!(deserialized.task_loop_commit_prefix, "fix"); assert!(deserialized.task_loop_include_summary); assert!(deserialized.disable_cron); assert!(!deserialized.include_git_instructions); assert!(!deserialized.enable_claudeai_mcp_servers); assert_eq!( deserialized.auto_memory_directory, Some("/custom/memory".to_string()) ); assert!(deserialized.model_overrides.is_some()); let overrides = deserialized.model_overrides.unwrap(); assert_eq!( overrides.get("claude-opus-4-6").map(String::as_str), Some("arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-opus-4-6-v1") ); } #[test] fn test_theme_serialization() { assert_eq!(serde_json::to_string(&Theme::Dark).unwrap(), "\"dark\""); assert_eq!(serde_json::to_string(&Theme::Light).unwrap(), "\"light\""); assert_eq!( serde_json::to_string(&Theme::HighContrast).unwrap(), "\"high-contrast\"" ); assert_eq!(serde_json::to_string(&Theme::Custom).unwrap(), "\"custom\""); assert_eq!( serde_json::to_string(&Theme::Dracula).unwrap(), "\"dracula\"" ); assert_eq!( serde_json::to_string(&Theme::Catppuccin).unwrap(), "\"catppuccin\"" ); assert_eq!(serde_json::to_string(&Theme::Nord).unwrap(), "\"nord\""); assert_eq!( serde_json::to_string(&Theme::Solarized).unwrap(), "\"solarized\"" ); assert_eq!( serde_json::to_string(&Theme::SolarizedLight).unwrap(), "\"solarized-light\"" ); assert_eq!( serde_json::to_string(&Theme::CatppuccinLatte).unwrap(), "\"catppuccin-latte\"" ); assert_eq!( serde_json::to_string(&Theme::GruvboxLight).unwrap(), "\"gruvbox-light\"" ); assert_eq!( serde_json::to_string(&Theme::RosePineDawn).unwrap(), "\"rose-pine-dawn\"" ); } #[test] fn test_theme_deserialization() { assert_eq!( serde_json::from_str::("\"dracula\"").unwrap(), Theme::Dracula ); assert_eq!( serde_json::from_str::("\"catppuccin\"").unwrap(), Theme::Catppuccin ); assert_eq!( serde_json::from_str::("\"nord\"").unwrap(), Theme::Nord ); assert_eq!( serde_json::from_str::("\"solarized\"").unwrap(), Theme::Solarized ); assert_eq!( serde_json::from_str::("\"solarized-light\"").unwrap(), Theme::SolarizedLight ); assert_eq!( serde_json::from_str::("\"catppuccin-latte\"").unwrap(), Theme::CatppuccinLatte ); assert_eq!( serde_json::from_str::("\"gruvbox-light\"").unwrap(), Theme::GruvboxLight ); assert_eq!( serde_json::from_str::("\"rose-pine-dawn\"").unwrap(), Theme::RosePineDawn ); } }