| 1 | //! Config command area: settings, modes, themes, trust, and status surfaces. |
| 2 | |
| 3 | // This group dir intentionally has a `config.rs` child module with the same |
| 4 | // name. The module_inception allow is a permanent structure rationale, not |
| 5 | // migration scaffolding; see docs/architecture/command-dispatch.md. |
| 6 | #[allow(clippy::module_inception)] |
| 7 | pub mod config; |
| 8 | mod import_claude; |
| 9 | mod permissions; |
| 10 | mod status; |
| 11 | |
| 12 | use crate::commands::CommandResult; |
| 13 | use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand}; |
| 14 | use crate::tui::app::App; |
| 15 | use codewhale_localization::MessageId; |
| 16 | |
| 17 | pub struct ConfigCommands; |
| 18 | |
| 19 | impl CommandGroup for ConfigCommands { |
| 20 | fn commands(&self) -> &'static [Box<dyn Command>] { |
| 21 | cached_command_list!(vec![ |
| 22 | Box::new(FunctionCommand::new(&CONFIG_INFO, run_config)), |
| 23 | Box::new(FunctionCommand::new(&IMPORT_CLAUDE_INFO, run_import_claude)), |
| 24 | Box::new(FunctionCommand::new(&PERMISSIONS_INFO, run_permissions)), |
| 25 | Box::new(FunctionCommand::new(&LOGIN_INFO, run_login)), |
| 26 | Box::new(FunctionCommand::new(&AUTH_INFO, run_auth)), |
| 27 | Box::new(FunctionCommand::new(&RAIL_INFO, run_rail)), |
| 28 | Box::new(FunctionCommand::new(&PET_INFO, run_pet)), |
| 29 | Box::new(FunctionCommand::new(&SETTINGS_INFO, run_settings)), |
| 30 | Box::new(FunctionCommand::new(&STATUS_INFO, run_status)), |
| 31 | Box::new(FunctionCommand::new(&STATUSLINE_INFO, run_statusline)), |
| 32 | Box::new(FunctionCommand::new(&MODE_INFO, run_mode)), |
| 33 | Box::new(FunctionCommand::new(&FULLSCREEN_INFO, run_fullscreen)), |
| 34 | Box::new(FunctionCommand::new(&INLINE_INFO, run_inline)), |
| 35 | Box::new(FunctionCommand::new(&THEME_INFO, run_theme)), |
| 36 | Box::new(FunctionCommand::new(&VERBOSE_INFO, run_verbose)), |
| 37 | Box::new(FunctionCommand::new(&TRUST_INFO, run_trust)), |
| 38 | Box::new(FunctionCommand::new(&LOGOUT_INFO, run_logout)), |
| 39 | ]) |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | static CONFIG_INFO: CommandInfo = CommandInfo { |
| 44 | name: "config", |
| 45 | aliases: &[], |
| 46 | usage: "/config [ask-rules|status|<key> [value]]", |
| 47 | description_id: MessageId::CmdConfigDescription, |
| 48 | }; |
| 49 | static IMPORT_CLAUDE_INFO: CommandInfo = CommandInfo { |
| 50 | name: "import-claude", |
| 51 | aliases: &["import_claude"], |
| 52 | usage: "/import-claude [--apply]", |
| 53 | description_id: MessageId::CmdImportClaudeDescription, |
| 54 | }; |
| 55 | static PERMISSIONS_INFO: CommandInfo = CommandInfo { |
| 56 | name: "permissions", |
| 57 | aliases: &["permission-rules", "permission_rules"], |
| 58 | usage: "/permissions [list|remove <rule-number> [--confirm <token>]]", |
| 59 | description_id: MessageId::CmdPermissionsDescription, |
| 60 | }; |
| 61 | static LOGIN_INFO: CommandInfo = CommandInfo { |
| 62 | name: "login", |
| 63 | aliases: &[], |
| 64 | usage: "/login [status|account|key]", |
| 65 | description_id: MessageId::CmdLoginDescription, |
| 66 | }; |
| 67 | static AUTH_INFO: CommandInfo = CommandInfo { |
| 68 | name: "auth", |
| 69 | aliases: &[], |
| 70 | usage: "/auth xai-device|chatgpt|chatgpt-revoke", |
| 71 | description_id: MessageId::CmdAuthDescription, |
| 72 | }; |
| 73 | static RAIL_INFO: CommandInfo = CommandInfo { |
| 74 | name: "workbar", |
| 75 | // /rail and /sidebar are the names users already know; both now drive |
| 76 | // the one workbar. |
| 77 | aliases: &["rail", "sidebar"], |
| 78 | usage: "/workbar [bottom|top|left|right|off|tasks|agents|context|pinned] [--save]", |
| 79 | description_id: MessageId::CmdSidebarDescription, |
| 80 | }; |
| 81 | static PET_INFO: CommandInfo = CommandInfo { |
| 82 | name: "pet", |
| 83 | aliases: &[], |
| 84 | usage: "/pet [on|off|status|appearance|window|source|export|sound on|off]", |
| 85 | description_id: MessageId::CmdPetDescription, |
| 86 | }; |
| 87 | static SETTINGS_INFO: CommandInfo = CommandInfo { |
| 88 | name: "settings", |
| 89 | aliases: &[], |
| 90 | usage: "/settings [text]", |
| 91 | description_id: MessageId::CmdSettingsDescription, |
| 92 | }; |
| 93 | static STATUS_INFO: CommandInfo = CommandInfo { |
| 94 | name: "status", |
| 95 | aliases: &[], |
| 96 | usage: "/status", |
| 97 | description_id: MessageId::CmdStatusDescription, |
| 98 | }; |
| 99 | static STATUSLINE_INFO: CommandInfo = CommandInfo { |
| 100 | name: "statusline", |
| 101 | aliases: &[], |
| 102 | usage: "/statusline", |
| 103 | description_id: MessageId::CmdStatuslineDescription, |
| 104 | }; |
| 105 | static MODE_INFO: CommandInfo = CommandInfo { |
| 106 | name: "mode", |
| 107 | aliases: &["jihua", "zidong"], |
| 108 | usage: "/mode [act|plan|operate|1|2|3]", |
| 109 | description_id: MessageId::CmdModeDescription, |
| 110 | }; |
| 111 | static FULLSCREEN_INFO: CommandInfo = CommandInfo { |
| 112 | name: "fullscreen", |
| 113 | aliases: &[], |
| 114 | usage: "/fullscreen", |
| 115 | description_id: MessageId::CmdFullscreenDescription, |
| 116 | }; |
| 117 | static INLINE_INFO: CommandInfo = CommandInfo { |
| 118 | name: "inline", |
| 119 | aliases: &[], |
| 120 | usage: "/inline", |
| 121 | description_id: MessageId::CmdInlineDescription, |
| 122 | }; |
| 123 | static THEME_INFO: CommandInfo = CommandInfo { |
| 124 | name: "theme", |
| 125 | aliases: &[], |
| 126 | usage: "/theme [name|underwater|custom:<name>|schema|path]", |
| 127 | description_id: MessageId::CmdThemeDescription, |
| 128 | }; |
| 129 | static VERBOSE_INFO: CommandInfo = CommandInfo { |
| 130 | name: "verbose", |
| 131 | aliases: &[], |
| 132 | usage: "/verbose [on|off]", |
| 133 | description_id: MessageId::CmdVerboseDescription, |
| 134 | }; |
| 135 | static TRUST_INFO: CommandInfo = CommandInfo { |
| 136 | name: "trust", |
| 137 | aliases: &["xinren"], |
| 138 | usage: "/trust [on|off|add <path>|remove <path>|list]", |
| 139 | description_id: MessageId::CmdTrustDescription, |
| 140 | }; |
| 141 | static LOGOUT_INFO: CommandInfo = CommandInfo { |
| 142 | name: "logout", |
| 143 | aliases: &[], |
| 144 | usage: "/logout", |
| 145 | description_id: MessageId::CmdLogoutDescription, |
| 146 | }; |
| 147 | fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult { |
| 148 | dispatch(app, name, arg).expect("registered config command should dispatch") |
| 149 | } |
| 150 | |
| 151 | fn run_config(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 152 | run_registered(app, "config", arg) |
| 153 | } |
| 154 | fn run_import_claude(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 155 | import_claude::import_claude_command(app, arg) |
| 156 | } |
| 157 | fn run_permissions(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 158 | run_registered(app, "permissions", arg) |
| 159 | } |
| 160 | fn run_login(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 161 | run_registered(app, "login", arg) |
| 162 | } |
| 163 | fn run_auth(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 164 | run_registered(app, "auth", arg) |
| 165 | } |
| 166 | fn run_rail(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 167 | run_registered(app, "workbar", arg) |
| 168 | } |
| 169 | fn run_pet(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 170 | run_registered(app, "pet", arg) |
| 171 | } |
| 172 | fn run_settings(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 173 | run_registered(app, "settings", arg) |
| 174 | } |
| 175 | fn run_status(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 176 | run_registered(app, "status", arg) |
| 177 | } |
| 178 | fn run_statusline(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 179 | run_registered(app, "statusline", arg) |
| 180 | } |
| 181 | fn run_mode(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 182 | run_registered(app, "mode", arg) |
| 183 | } |
| 184 | fn run_fullscreen(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 185 | run_registered(app, "fullscreen", arg) |
| 186 | } |
| 187 | fn run_inline(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 188 | run_registered(app, "inline", arg) |
| 189 | } |
| 190 | fn run_theme(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 191 | run_registered(app, "theme", arg) |
| 192 | } |
| 193 | fn run_verbose(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 194 | run_registered(app, "verbose", arg) |
| 195 | } |
| 196 | fn run_trust(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 197 | run_registered(app, "trust", arg) |
| 198 | } |
| 199 | fn run_logout(app: &mut App, arg: Option<&str>) -> CommandResult { |
| 200 | run_registered(app, "logout", arg) |
| 201 | } |
| 202 | pub(in crate::commands) fn dispatch( |
| 203 | app: &mut App, |
| 204 | command: &str, |
| 205 | arg: Option<&str>, |
| 206 | ) -> Option<CommandResult> { |
| 207 | let result = match command { |
| 208 | "config" => config::config_command(app, arg), |
| 209 | "permissions" | "permission-rules" | "permission_rules" => { |
| 210 | permissions::permissions_command(app, arg) |
| 211 | } |
| 212 | "login" => config::login(app, arg), |
| 213 | "auth" => match arg.map(str::trim) { |
| 214 | Some("xai-device") | Some("xai_device") => { |
| 215 | CommandResult::action(crate::tui::app::AppAction::StartXaiDeviceLogin) |
| 216 | } |
| 217 | Some("chatgpt") | Some("openai-codex") | Some("openai_codex") => { |
| 218 | CommandResult::action(crate::tui::app::AppAction::StartChatgptPkceLogin) |
| 219 | } |
| 220 | Some("chatgpt-revoke") | Some("chatgpt_revoke") => { |
| 221 | CommandResult::action(crate::tui::app::AppAction::StartChatgptRevoke) |
| 222 | } |
| 223 | _ => CommandResult::error("Usage: /auth xai-device|chatgpt|chatgpt-revoke"), |
| 224 | }, |
| 225 | "workbar" | "rail" | "sidebar" => config::sidebar(app, arg), |
| 226 | "pet" => config::pet(app, arg), |
| 227 | "settings" => config::settings_command(app, arg), |
| 228 | "status" => status::status(app), |
| 229 | "statusline" => config::status_line(app), |
| 230 | "mode" => config::mode(app, arg), |
| 231 | "fullscreen" => config::screen(app, crate::tui::app::ScreenMode::Fullscreen, arg), |
| 232 | "inline" => config::screen(app, crate::tui::app::ScreenMode::Inline, arg), |
| 233 | "jihua" => config::mode(app, Some("plan")), |
| 234 | "zidong" => config::mode(app, Some("yolo")), |
| 235 | "theme" => config::theme(app, arg), |
| 236 | "verbose" => config::verbose(app, arg), |
| 237 | "trust" | "xinren" => config::trust(app, arg), |
| 238 | "logout" => config::logout(app), |
| 239 | _ => return None, |
| 240 | }; |
| 241 | Some(result) |
| 242 | } |
| 243 | |
| 244 | /// `/workflow settings` and `/config workflow`: the effective `[workflow]` |
| 245 | /// and `[goal]` tables with what each value does, read from the refreshed |
| 246 | /// session table after a config.toml reload (no model turn). The workflow |
| 247 | /// tool reads the same table, so the two surfaces cannot disagree. This |
| 248 | /// surface explains, it does not edit. |
| 249 | pub(in crate::commands) fn workflow_settings(app: &App) -> CommandResult { |
| 250 | let refreshed = crate::tools::workflow::session_workflow_config(&app.workspace); |
| 251 | let cfg = refreshed.as_ref().unwrap_or(&app.workflow_config); |
| 252 | let on = |value: bool| if value { "on" } else { "off" }; |
| 253 | let lines = [ |
| 254 | "[workflow] — config.toml".to_string(), |
| 255 | format!( |
| 256 | "automatic = {} · the agent may start a workflow itself for broad or staged work; off means only /workflow starts one", |
| 257 | on(cfg.automatic) |
| 258 | ), |
| 259 | format!( |
| 260 | "auto_start_read_only = {} · read-only plans start without an approval card", |
| 261 | on(cfg.auto_start_read_only) |
| 262 | ), |
| 263 | format!( |
| 264 | "require_approval_for_writes = {} · plans that write, use shell/network, or elevate show an approval card first", |
| 265 | on(cfg.require_approval_for_writes) |
| 266 | ), |
| 267 | format!( |
| 268 | "max_children = {} · max_concurrent = {} · max_depth = {} · hard ceilings for one run", |
| 269 | cfg.max_children, cfg.max_concurrent, cfg.max_depth |
| 270 | ), |
| 271 | format!( |
| 272 | "default_token_budget = {} · shared admission cap for a run and its children (0 = none)", |
| 273 | cfg.default_token_budget |
| 274 | ), |
| 275 | String::new(), |
| 276 | "[goal] — config.toml".to_string(), |
| 277 | format!( |
| 278 | "max_continuations = {} · automatic continuation passes before a goal pauses; 0 = unlimited (completion, blocked, or you stop it)", |
| 279 | app.goal_max_continuations |
| 280 | ), |
| 281 | format!( |
| 282 | "enforce_token_budget = {} · true = a goal's token budget is a hard stop; false = advisory telemetry", |
| 283 | on(app.goal_enforce_token_budget) |
| 284 | ), |
| 285 | ]; |
| 286 | CommandResult::message(lines.join("\n")) |
| 287 | } |
| 288 |