| 1 | //! Constitution-first setup wizard shell (#3404/#3794). |
| 2 | //! |
| 3 | //! This module owns the reusable setup shell: step ordering, navigation, |
| 4 | //! per-step status projection, and the v0.8.67 constitution checkpoint action. |
| 5 | //! Individual step contents can grow behind [`SetupWizardStep`] without |
| 6 | //! changing the navigation or commit contract. |
| 7 | |
| 8 | use std::borrow::Cow; |
| 9 | use std::path::Path; |
| 10 | |
| 11 | use crossterm::event::{KeyCode, KeyEvent}; |
| 12 | use ratatui::{ |
| 13 | buffer::Buffer, |
| 14 | layout::Rect, |
| 15 | style::{Modifier, Style}, |
| 16 | text::{Line, Span}, |
| 17 | widgets::{Paragraph, Widget, Wrap}, |
| 18 | }; |
| 19 | |
| 20 | use crate::config::{Config, has_api_key}; |
| 21 | use crate::localization::{Locale, MessageId, tr}; |
| 22 | use crate::palette; |
| 23 | use crate::prompts::{ |
| 24 | BASE_PROMPT_OVERRIDE_OPT_IN_ENV, CONSTITUTION_OVERRIDE_FILE, base_prompt_override_opt_in, |
| 25 | }; |
| 26 | use crate::tui::app::App; |
| 27 | use crate::tui::onboarding; |
| 28 | use crate::tui::views::{ |
| 29 | ActionHint, ModalKind, ModalView, ViewAction, ViewEvent, render_modal_footer, |
| 30 | render_panel_scroll_rail, render_underwater_surface, |
| 31 | }; |
| 32 | |
| 33 | use codewhale_config::{ |
| 34 | AutonomyPreference, ConstitutionAuthoring, ConstitutionChoice, ConstitutionSource, |
| 35 | ConstitutionValidity, InheritedConfigFacts, RuntimePostureSource, SetupState, SetupStep, |
| 36 | StepEntry, StepStatus, UserConstitution, UserConstitutionLoad, |
| 37 | user_constitution::MAX_NOTES_LEN, |
| 38 | }; |
| 39 | |
| 40 | mod fleet_draft; |
| 41 | mod model_draft; |
| 42 | mod operate; |
| 43 | mod persistence; |
| 44 | mod provider; |
| 45 | mod remote; |
| 46 | mod tools_mcp; |
| 47 | |
| 48 | pub(crate) use fleet_draft::{draft_fleet_profile_with_model, workspace_fingerprint}; |
| 49 | pub(crate) use model_draft::draft_constitution_with_model; |
| 50 | use persistence::SetupPersistenceFacts; |
| 51 | use remote::SetupRemoteFacts; |
| 52 | |
| 53 | /// Target lane for the once-per-version constitution checkpoint. Bumped per |
| 54 | /// release when the bundled constitution materially changes, so existing users |
| 55 | /// re-acknowledge it once. 0.9.4 re-ships the Fleet/operate constitution. |
| 56 | pub const CONSTITUTION_CHECKPOINT_VERSION: &str = "0.9.4"; |
| 57 | |
| 58 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 59 | pub enum SetupCommitKind { |
| 60 | BundledConstitution, |
| 61 | DeferredConstitution, |
| 62 | } |
| 63 | |
| 64 | pub trait SetupWizardStep { |
| 65 | fn id(&self) -> SetupStep; |
| 66 | fn title_id(&self) -> MessageId; |
| 67 | fn why_id(&self) -> MessageId; |
| 68 | fn required(&self) -> bool; |
| 69 | } |
| 70 | |
| 71 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 72 | struct StaticSetupStep { |
| 73 | id: SetupStep, |
| 74 | title_id: MessageId, |
| 75 | why_id: MessageId, |
| 76 | required: bool, |
| 77 | } |
| 78 | |
| 79 | impl SetupWizardStep for StaticSetupStep { |
| 80 | fn id(&self) -> SetupStep { |
| 81 | self.id |
| 82 | } |
| 83 | |
| 84 | fn title_id(&self) -> MessageId { |
| 85 | self.title_id |
| 86 | } |
| 87 | |
| 88 | fn why_id(&self) -> MessageId { |
| 89 | self.why_id |
| 90 | } |
| 91 | |
| 92 | fn required(&self) -> bool { |
| 93 | self.required |
| 94 | } |
| 95 | } |
| 96 | |
| 97 | const STEP_SPECS: [StaticSetupStep; 10] = [ |
| 98 | StaticSetupStep { |
| 99 | id: SetupStep::Language, |
| 100 | title_id: MessageId::SetupStepLanguageTitle, |
| 101 | why_id: MessageId::SetupStepLanguageWhy, |
| 102 | required: true, |
| 103 | }, |
| 104 | StaticSetupStep { |
| 105 | id: SetupStep::ProviderModel, |
| 106 | title_id: MessageId::SetupStepProviderModelTitle, |
| 107 | why_id: MessageId::SetupStepProviderModelWhy, |
| 108 | required: true, |
| 109 | }, |
| 110 | StaticSetupStep { |
| 111 | id: SetupStep::TrustSandbox, |
| 112 | title_id: MessageId::SetupStepTrustSandboxTitle, |
| 113 | why_id: MessageId::SetupStepTrustSandboxWhy, |
| 114 | required: true, |
| 115 | }, |
| 116 | StaticSetupStep { |
| 117 | id: SetupStep::Constitution, |
| 118 | title_id: MessageId::SetupStepConstitutionTitle, |
| 119 | why_id: MessageId::SetupStepConstitutionWhy, |
| 120 | required: true, |
| 121 | }, |
| 122 | StaticSetupStep { |
| 123 | id: SetupStep::OperateFleet, |
| 124 | title_id: MessageId::SetupStepOperateFleetTitle, |
| 125 | why_id: MessageId::SetupStepOperateFleetWhy, |
| 126 | required: false, |
| 127 | }, |
| 128 | StaticSetupStep { |
| 129 | id: SetupStep::Hotbar, |
| 130 | title_id: MessageId::SetupStepHotbarTitle, |
| 131 | why_id: MessageId::SetupStepHotbarWhy, |
| 132 | required: false, |
| 133 | }, |
| 134 | StaticSetupStep { |
| 135 | id: SetupStep::ToolsMcp, |
| 136 | title_id: MessageId::SetupStepToolsMcpTitle, |
| 137 | why_id: MessageId::SetupStepToolsMcpWhy, |
| 138 | required: false, |
| 139 | }, |
| 140 | StaticSetupStep { |
| 141 | id: SetupStep::RemoteRuntime, |
| 142 | title_id: MessageId::SetupStepRemoteRuntimeTitle, |
| 143 | why_id: MessageId::SetupStepRemoteRuntimeWhy, |
| 144 | required: false, |
| 145 | }, |
| 146 | StaticSetupStep { |
| 147 | id: SetupStep::Persistence, |
| 148 | title_id: MessageId::SetupStepPersistenceTitle, |
| 149 | why_id: MessageId::SetupStepPersistenceWhy, |
| 150 | required: false, |
| 151 | }, |
| 152 | StaticSetupStep { |
| 153 | id: SetupStep::Verification, |
| 154 | title_id: MessageId::SetupStepVerificationTitle, |
| 155 | why_id: MessageId::SetupStepVerificationWhy, |
| 156 | required: false, |
| 157 | }, |
| 158 | ]; |
| 159 | |
| 160 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 161 | pub struct SetupWizardView { |
| 162 | state: SetupState, |
| 163 | selected: usize, |
| 164 | locale: Locale, |
| 165 | facts: SetupRuntimeFacts, |
| 166 | guided_draft: GuidedConstitutionDraft, |
| 167 | freeform_note: String, |
| 168 | editing_freeform_note: bool, |
| 169 | guided_preview_seen: bool, |
| 170 | /// The keep-existing path mirrors the guided two-step: the first `K` |
| 171 | /// opens the rendered preview of the existing file, the second completes |
| 172 | /// the checkpoint without touching it. |
| 173 | existing_preview_seen: bool, |
| 174 | /// A model-drafted constitution awaiting ratification, installed by the |
| 175 | /// host after a successful one-shot draft (already sanitized + bounded). |
| 176 | /// Cleared whenever a guided answer changes so a stale draft can never be |
| 177 | /// ratified against fresh answers. |
| 178 | model_draft: Option<Box<UserConstitution>>, |
| 179 | /// Display label of the model that authored `model_draft` (safe metadata, |
| 180 | /// e.g. "GLM-5.2"), for provenance copy only. |
| 181 | model_draft_label: Option<String>, |
| 182 | runtime_preset: SetupRuntimePreset, |
| 183 | runtime_preset_preview_seen: bool, |
| 184 | body_scroll: usize, |
| 185 | } |
| 186 | |
| 187 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 188 | struct SetupRuntimeFacts { |
| 189 | provider: String, |
| 190 | model: String, |
| 191 | auth: String, |
| 192 | health: String, |
| 193 | provider_ready: bool, |
| 194 | provider_result: String, |
| 195 | work_intent: String, |
| 196 | approval: String, |
| 197 | shell: String, |
| 198 | allow_shell_enabled: bool, |
| 199 | trust: String, |
| 200 | sandbox: String, |
| 201 | sandbox_mode_value: String, |
| 202 | network: String, |
| 203 | network_default_value: String, |
| 204 | runtime_result: String, |
| 205 | operate_runtime_ready: bool, |
| 206 | operate_runtime_result: String, |
| 207 | fleet_roster_ready: bool, |
| 208 | fleet_roster_result: String, |
| 209 | operate_concurrency_result: String, |
| 210 | operate_result: String, |
| 211 | hotbar_bindings_result: String, |
| 212 | hotbar_actions_result: String, |
| 213 | hotbar_result: String, |
| 214 | tools_mcp_servers_result: String, |
| 215 | tools_mcp_skills_result: String, |
| 216 | tools_mcp_tools_result: String, |
| 217 | tools_mcp_plugins_result: String, |
| 218 | tools_mcp_hotbar_result: String, |
| 219 | tools_mcp_result: String, |
| 220 | tools_mcp_needs_action: bool, |
| 221 | tools_mcp_path_display: String, |
| 222 | tools_mcp_skills_path_display: String, |
| 223 | tools_mcp_plugins_path_display: String, |
| 224 | remote_clouds_result: String, |
| 225 | remote_bridges_result: String, |
| 226 | remote_providers_result: String, |
| 227 | remote_mode_result: String, |
| 228 | remote_command_provider: String, |
| 229 | remote_result: String, |
| 230 | /// The four observed remote modes (#3409). Empty only before facts load. |
| 231 | remote_modes: Vec<remote::RemoteModeFact>, |
| 232 | /// True when a mode is missing a token or config. Recorded as |
| 233 | /// `NeedsAction`, which by contract never blocks the ready screen. |
| 234 | remote_needs_action: bool, |
| 235 | persistence: SetupPersistenceFacts, |
| 236 | default_mode: String, |
| 237 | approval_policy_value: String, |
| 238 | project_override_warning: Option<String>, |
| 239 | constitution_autonomy: String, |
| 240 | constitution_file: SetupConstitutionFileState, |
| 241 | expert_override: SetupExpertOverrideState, |
| 242 | } |
| 243 | |
| 244 | impl Default for SetupRuntimeFacts { |
| 245 | fn default() -> Self { |
| 246 | Self { |
| 247 | provider: "not loaded".to_string(), |
| 248 | model: "not loaded".to_string(), |
| 249 | auth: "not checked".to_string(), |
| 250 | health: "not checked".to_string(), |
| 251 | provider_ready: false, |
| 252 | provider_result: "provider/model not loaded".to_string(), |
| 253 | work_intent: "not loaded".to_string(), |
| 254 | approval: "not loaded".to_string(), |
| 255 | shell: "not loaded".to_string(), |
| 256 | allow_shell_enabled: false, |
| 257 | trust: "not loaded".to_string(), |
| 258 | sandbox: "not configured".to_string(), |
| 259 | sandbox_mode_value: "default".to_string(), |
| 260 | network: "not configured".to_string(), |
| 261 | network_default_value: "prompt".to_string(), |
| 262 | runtime_result: "runtime posture not loaded".to_string(), |
| 263 | operate_runtime_ready: false, |
| 264 | operate_runtime_result: "worker runtime not loaded".to_string(), |
| 265 | fleet_roster_ready: false, |
| 266 | fleet_roster_result: "Fleet roster not loaded".to_string(), |
| 267 | operate_concurrency_result: "concurrency not loaded".to_string(), |
| 268 | operate_result: "operate readiness not loaded".to_string(), |
| 269 | hotbar_bindings_result: "Hotbar config not loaded".to_string(), |
| 270 | hotbar_actions_result: "Hotbar actions not loaded".to_string(), |
| 271 | hotbar_result: "hotbar not loaded".to_string(), |
| 272 | tools_mcp_servers_result: "MCP config not loaded".to_string(), |
| 273 | tools_mcp_skills_result: "skills dir not loaded".to_string(), |
| 274 | tools_mcp_tools_result: "tools dir not loaded".to_string(), |
| 275 | tools_mcp_plugins_result: "plugins dir not loaded".to_string(), |
| 276 | tools_mcp_hotbar_result: "hotbar source metadata not loaded".to_string(), |
| 277 | tools_mcp_result: "tools/MCP not loaded".to_string(), |
| 278 | tools_mcp_needs_action: false, |
| 279 | tools_mcp_path_display: String::new(), |
| 280 | tools_mcp_skills_path_display: String::new(), |
| 281 | tools_mcp_plugins_path_display: String::new(), |
| 282 | remote_clouds_result: "remote cloud registry not loaded".to_string(), |
| 283 | remote_bridges_result: "remote bridge registry not loaded".to_string(), |
| 284 | remote_providers_result: "provider registry not loaded".to_string(), |
| 285 | remote_mode_result: "remote setup mode not loaded".to_string(), |
| 286 | remote_command_provider: "deepseek".to_string(), |
| 287 | remote_result: "remote runtime not loaded".to_string(), |
| 288 | remote_modes: Vec::new(), |
| 289 | remote_needs_action: false, |
| 290 | persistence: SetupPersistenceFacts::default(), |
| 291 | default_mode: "agent".to_string(), |
| 292 | approval_policy_value: "on-request".to_string(), |
| 293 | project_override_warning: None, |
| 294 | constitution_autonomy: "not loaded".to_string(), |
| 295 | constitution_file: SetupConstitutionFileState::NotChecked, |
| 296 | expert_override: SetupExpertOverrideState::NotChecked, |
| 297 | } |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | impl SetupRuntimeFacts { |
| 302 | fn from_app_config(app: &App, config: &Config) -> Self { |
| 303 | let expert_override = SetupExpertOverrideState::load(); |
| 304 | let readiness = crate::provider_readiness::resolve_for_model( |
| 305 | config, |
| 306 | app.api_provider, |
| 307 | if app.auto_model { "auto" } else { &app.model }, |
| 308 | &app.provider_health, |
| 309 | ); |
| 310 | // A failed observed check remains retryable in route pickers, but the |
| 311 | // setup receipt must not certify it as healthy. Saved-unchecked and |
| 312 | // local-unchecked are honest reviewed configuration states; an actual |
| 313 | // session failure is NeedsAction until a later success replaces it. |
| 314 | let provider_ready = readiness.can_attempt() |
| 315 | && !matches!( |
| 316 | &readiness, |
| 317 | crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. } |
| 318 | ); |
| 319 | let model = app.model_display_label(); |
| 320 | let provider_name = if app.api_provider == crate::config::ApiProvider::Custom { |
| 321 | app.provider_identity_for_persistence().to_string() |
| 322 | } else { |
| 323 | app.api_provider.display_name().to_string() |
| 324 | }; |
| 325 | let context_window = crate::route_budget::route_context_window_tokens( |
| 326 | app.api_provider, |
| 327 | &app.model, |
| 328 | app.active_route_limits, |
| 329 | ); |
| 330 | let context_window_source = app.active_context_window_source.label(); |
| 331 | let provider = |
| 332 | format!("{provider_name} · context {context_window} ({context_window_source})"); |
| 333 | let auth = readiness.label().into_owned(); |
| 334 | let health = if provider_ready { |
| 335 | format!("{}; route can be attempted", readiness.label()) |
| 336 | } else if matches!( |
| 337 | &readiness, |
| 338 | crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. } |
| 339 | ) { |
| 340 | format!("{}; retry or open /provider", readiness.label()) |
| 341 | } else if app.api_provider == crate::config::ApiProvider::OpenaiCodex { |
| 342 | format!( |
| 343 | "{}; run codex login, then grant exact read-only access with `codewhale auth external-consent --provider openai-codex --mode read-only`, or open /provider", |
| 344 | readiness.label() |
| 345 | ) |
| 346 | } else if let Some(url) = crate::config::credential_help_for_provider_route( |
| 347 | app.api_provider, |
| 348 | &config.deepseek_base_url(), |
| 349 | ) |
| 350 | .credential_url |
| 351 | { |
| 352 | format!( |
| 353 | "{}; credentials: {url}; open /provider to repair the route", |
| 354 | readiness.label() |
| 355 | ) |
| 356 | } else { |
| 357 | format!( |
| 358 | "{}; {}; open /provider to repair the route", |
| 359 | readiness.label(), |
| 360 | crate::config::credential_help_for_provider_route( |
| 361 | app.api_provider, |
| 362 | &config.deepseek_base_url(), |
| 363 | ) |
| 364 | .guidance |
| 365 | ) |
| 366 | }; |
| 367 | let provider_result = format!( |
| 368 | "provider={}, model={}, context_window={} ({}) auth={}, health={}", |
| 369 | app.provider_identity_for_persistence(), |
| 370 | model, |
| 371 | context_window, |
| 372 | context_window_source, |
| 373 | readiness.label(), |
| 374 | if provider_ready { |
| 375 | "attemptable" |
| 376 | } else { |
| 377 | "needs action" |
| 378 | } |
| 379 | ); |
| 380 | let shell = if app.allow_shell { "enabled" } else { "hidden" }.to_string(); |
| 381 | let trust = if app.trust_mode { |
| 382 | "trusted workspace / writes allowed by posture" |
| 383 | } else { |
| 384 | "workspace trust not elevated" |
| 385 | } |
| 386 | .to_string(); |
| 387 | let sandbox = config |
| 388 | .sandbox_mode |
| 389 | .as_deref() |
| 390 | .filter(|mode| !mode.trim().is_empty()) |
| 391 | .unwrap_or("default") |
| 392 | .to_string(); |
| 393 | let sandbox_mode_value = sandbox.clone(); |
| 394 | let network_default_value = config |
| 395 | .network |
| 396 | .as_ref() |
| 397 | .map_or("prompt".to_string(), |policy| policy.default.clone()); |
| 398 | let network = config |
| 399 | .network |
| 400 | .as_ref() |
| 401 | .map_or("prompt by default".to_string(), |policy| { |
| 402 | format!("default {}", policy.default) |
| 403 | }); |
| 404 | let runtime_result = format!( |
| 405 | "intent={}, approval={}, shell={}, trust={}, sandbox={}, network={}", |
| 406 | app.mode.as_setting(), |
| 407 | app.approval_mode |
| 408 | .permission_chip_label() |
| 409 | .to_ascii_lowercase(), |
| 410 | if app.allow_shell { "enabled" } else { "hidden" }, |
| 411 | if app.trust_mode { |
| 412 | "trusted" |
| 413 | } else { |
| 414 | "workspace" |
| 415 | }, |
| 416 | sandbox, |
| 417 | network |
| 418 | ); |
| 419 | let operate = operate::SetupOperateFacts::from_app_config(app, config, provider_ready); |
| 420 | let known_hotbar_action_ids = app |
| 421 | .hotbar_actions |
| 422 | .iter() |
| 423 | .map(|action| action.id()) |
| 424 | .collect::<Vec<_>>(); |
| 425 | let hotbar_resolution = config.resolve_hotbar_bindings(&known_hotbar_action_ids); |
| 426 | let configured_hotbar_slots = config.hotbar.as_ref().map_or(0, Vec::len); |
| 427 | let hotbar_state = match config.hotbar.as_ref() { |
| 428 | None => "hidden", |
| 429 | Some(bindings) if bindings.is_empty() => "disabled", |
| 430 | Some(_) => "customized", |
| 431 | }; |
| 432 | let active_hotbar_slots = hotbar_resolution.bindings.len(); |
| 433 | let hotbar_warning_count = hotbar_resolution.warnings.len(); |
| 434 | let hotbar_bindings_result = format!( |
| 435 | "{hotbar_state}; configured_slots={configured_hotbar_slots}; active_slots={active_hotbar_slots}; warnings={hotbar_warning_count}" |
| 436 | ); |
| 437 | let hotbar_actions_result = |
| 438 | format!("{} bindable actions registered", app.hotbar_actions.len()); |
| 439 | let hotbar_result = format!( |
| 440 | "state={hotbar_state}, configured_slots={configured_hotbar_slots}, active_slots={active_hotbar_slots}, actions={}, warnings={hotbar_warning_count}", |
| 441 | app.hotbar_actions.len() |
| 442 | ); |
| 443 | let codewhale_home = setup_codewhale_home_dir(); |
| 444 | let persistence = SetupPersistenceFacts::from_app_config(app, config, &codewhale_home); |
| 445 | let tools_mcp = |
| 446 | tools_mcp::SetupToolsMcpFacts::from_app_config(app, config, &codewhale_home); |
| 447 | let tools_mcp_servers_result = tools_mcp.servers_result; |
| 448 | let tools_mcp_skills_result = tools_mcp.skills_result; |
| 449 | let tools_mcp_tools_result = tools_mcp.tools_result; |
| 450 | let tools_mcp_plugins_result = tools_mcp.plugins_result; |
| 451 | let tools_mcp_hotbar_result = tools_mcp.hotbar_result; |
| 452 | let tools_mcp_result = tools_mcp.result; |
| 453 | let tools_mcp_needs_action = tools_mcp.needs_action; |
| 454 | let tools_mcp_path_display = tools_mcp.mcp_path_display; |
| 455 | let tools_mcp_skills_path_display = tools_mcp.skills_path_display; |
| 456 | let tools_mcp_plugins_path_display = tools_mcp.plugins_path_display; |
| 457 | let remote = SetupRemoteFacts::from_app(app); |
| 458 | let remote_needs_action = remote.needs_action(); |
| 459 | let constitution_autonomy = UserConstitution::load() |
| 460 | .ok() |
| 461 | .and_then(|load| { |
| 462 | load.constitution().map(|constitution| { |
| 463 | autonomy_label(constitution.autonomy_preference, app.ui_locale).to_string() |
| 464 | }) |
| 465 | }) |
| 466 | .unwrap_or_else(|| tr(app.ui_locale, MessageId::SetupAutonomyUnspecified).to_string()); |
| 467 | Self { |
| 468 | provider, |
| 469 | model, |
| 470 | auth, |
| 471 | health, |
| 472 | provider_ready, |
| 473 | provider_result, |
| 474 | work_intent: app.mode.display_name().to_string(), |
| 475 | approval: app |
| 476 | .approval_mode |
| 477 | .permission_chip_label() |
| 478 | .to_ascii_lowercase(), |
| 479 | shell, |
| 480 | allow_shell_enabled: app.allow_shell, |
| 481 | trust, |
| 482 | sandbox, |
| 483 | sandbox_mode_value, |
| 484 | network, |
| 485 | network_default_value, |
| 486 | runtime_result, |
| 487 | operate_runtime_ready: operate.runtime_ready, |
| 488 | operate_runtime_result: operate.runtime_result, |
| 489 | fleet_roster_ready: operate.roster_ready, |
| 490 | fleet_roster_result: operate.roster_result, |
| 491 | operate_concurrency_result: operate.concurrency_result, |
| 492 | operate_result: operate.result, |
| 493 | hotbar_bindings_result, |
| 494 | hotbar_actions_result, |
| 495 | hotbar_result, |
| 496 | tools_mcp_servers_result, |
| 497 | tools_mcp_skills_result, |
| 498 | tools_mcp_tools_result, |
| 499 | tools_mcp_plugins_result, |
| 500 | tools_mcp_hotbar_result, |
| 501 | tools_mcp_result, |
| 502 | tools_mcp_needs_action, |
| 503 | tools_mcp_path_display, |
| 504 | tools_mcp_skills_path_display, |
| 505 | tools_mcp_plugins_path_display, |
| 506 | remote_clouds_result: remote.clouds_result, |
| 507 | remote_bridges_result: remote.bridges_result, |
| 508 | remote_providers_result: remote.providers_result, |
| 509 | remote_mode_result: remote.mode_result, |
| 510 | remote_command_provider: remote.command_provider, |
| 511 | remote_result: remote.result, |
| 512 | remote_needs_action, |
| 513 | remote_modes: remote.modes, |
| 514 | persistence, |
| 515 | default_mode: app.mode.as_setting().to_string(), |
| 516 | approval_policy_value: config |
| 517 | .approval_policy |
| 518 | .as_deref() |
| 519 | .filter(|policy| !policy.trim().is_empty()) |
| 520 | .unwrap_or("on-request") |
| 521 | .to_string(), |
| 522 | project_override_warning: project_runtime_override_warning( |
| 523 | &app.workspace, |
| 524 | app.ui_locale, |
| 525 | ), |
| 526 | constitution_autonomy, |
| 527 | constitution_file: SetupConstitutionFileState::load(), |
| 528 | expert_override, |
| 529 | } |
| 530 | } |
| 531 | } |
| 532 | |
| 533 | fn setup_codewhale_home_dir() -> std::path::PathBuf { |
| 534 | codewhale_config::codewhale_home().unwrap_or_else(|_| { |
| 535 | crate::config::effective_home_dir().map_or_else( |
| 536 | || std::path::PathBuf::from(".codewhale"), |
| 537 | |home| home.join(".codewhale"), |
| 538 | ) |
| 539 | }) |
| 540 | } |
| 541 | |
| 542 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 543 | pub enum SetupRuntimePreset { |
| 544 | AskFirst, |
| 545 | #[default] |
| 546 | NormalAgent, |
| 547 | HighTrustLocal, |
| 548 | } |
| 549 | |
| 550 | impl SetupRuntimePreset { |
| 551 | const ALL: [Self; 3] = [Self::AskFirst, Self::NormalAgent, Self::HighTrustLocal]; |
| 552 | |
| 553 | fn from_key(key: char) -> Option<Self> { |
| 554 | match key { |
| 555 | '1' => Some(Self::AskFirst), |
| 556 | '2' => Some(Self::NormalAgent), |
| 557 | '3' => Some(Self::HighTrustLocal), |
| 558 | _ => None, |
| 559 | } |
| 560 | } |
| 561 | |
| 562 | pub fn id(self) -> &'static str { |
| 563 | match self { |
| 564 | Self::AskFirst => "ask-first", |
| 565 | Self::NormalAgent => "normal-agent", |
| 566 | Self::HighTrustLocal => "high-trust-local", |
| 567 | } |
| 568 | } |
| 569 | |
| 570 | fn title_id(self) -> MessageId { |
| 571 | match self { |
| 572 | Self::AskFirst => MessageId::SetupRuntimePresetAskFirstTitle, |
| 573 | Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentTitle, |
| 574 | Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustTitle, |
| 575 | } |
| 576 | } |
| 577 | |
| 578 | fn description_id(self) -> MessageId { |
| 579 | match self { |
| 580 | Self::AskFirst => MessageId::SetupRuntimePresetAskFirstDescription, |
| 581 | Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentDescription, |
| 582 | Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustDescription, |
| 583 | } |
| 584 | } |
| 585 | |
| 586 | pub fn default_mode(self) -> &'static str { |
| 587 | match self { |
| 588 | Self::AskFirst => "plan", |
| 589 | Self::NormalAgent | Self::HighTrustLocal => "agent", |
| 590 | } |
| 591 | } |
| 592 | |
| 593 | pub fn permission_posture(self) -> &'static str { |
| 594 | match self { |
| 595 | Self::AskFirst | Self::NormalAgent => "ask", |
| 596 | Self::HighTrustLocal => "full-access", |
| 597 | } |
| 598 | } |
| 599 | |
| 600 | pub fn approval_policy(self) -> Option<&'static str> { |
| 601 | match self { |
| 602 | Self::AskFirst | Self::NormalAgent => Some("on-request"), |
| 603 | // Full Access lives in TUI settings; it is intentionally not a |
| 604 | // top-level approval_policy value. |
| 605 | Self::HighTrustLocal => None, |
| 606 | } |
| 607 | } |
| 608 | |
| 609 | pub fn allow_shell(self) -> bool { |
| 610 | match self { |
| 611 | Self::AskFirst => false, |
| 612 | Self::NormalAgent | Self::HighTrustLocal => true, |
| 613 | } |
| 614 | } |
| 615 | |
| 616 | pub fn sandbox_mode(self) -> &'static str { |
| 617 | match self { |
| 618 | Self::AskFirst => "read-only", |
| 619 | Self::NormalAgent => "workspace-write", |
| 620 | Self::HighTrustLocal => "danger-full-access", |
| 621 | } |
| 622 | } |
| 623 | |
| 624 | pub fn result_summary(self) -> String { |
| 625 | let approval = self |
| 626 | .approval_policy() |
| 627 | .unwrap_or("unset (Full Access saved in TUI settings)"); |
| 628 | format!( |
| 629 | "preset={}, default_mode={}, permission_posture={}, approval_policy={}, allow_shell={}, sandbox_mode={}, network=unchanged, trust=unchanged", |
| 630 | self.id(), |
| 631 | self.display_mode(), |
| 632 | self.permission_posture(), |
| 633 | approval, |
| 634 | self.allow_shell(), |
| 635 | self.sandbox_mode() |
| 636 | ) |
| 637 | } |
| 638 | |
| 639 | fn display_mode(self) -> &'static str { |
| 640 | match self { |
| 641 | Self::AskFirst => "plan", |
| 642 | Self::NormalAgent => "act", |
| 643 | Self::HighTrustLocal => "act + full-access", |
| 644 | } |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 649 | enum SetupConstitutionFileState { |
| 650 | NotChecked, |
| 651 | Missing, |
| 652 | Loaded, |
| 653 | Empty, |
| 654 | Invalid, |
| 655 | Unreadable, |
| 656 | PathError, |
| 657 | } |
| 658 | |
| 659 | impl SetupConstitutionFileState { |
| 660 | fn load() -> Self { |
| 661 | match UserConstitution::path() { |
| 662 | Ok(path) => Self::from_load(&UserConstitution::load_from(&path)), |
| 663 | Err(_) => Self::PathError, |
| 664 | } |
| 665 | } |
| 666 | |
| 667 | fn from_load(load: &UserConstitutionLoad) -> Self { |
| 668 | match load { |
| 669 | UserConstitutionLoad::Missing => Self::Missing, |
| 670 | UserConstitutionLoad::Empty => Self::Empty, |
| 671 | UserConstitutionLoad::Invalid(_) => Self::Invalid, |
| 672 | UserConstitutionLoad::Unreadable(_) => Self::Unreadable, |
| 673 | UserConstitutionLoad::Loaded(_) => Self::Loaded, |
| 674 | } |
| 675 | } |
| 676 | |
| 677 | fn label(self, choice: ConstitutionChoice, locale: Locale) -> Cow<'static, str> { |
| 678 | let id = match self { |
| 679 | Self::NotChecked => MessageId::SetupConstitutionFileNotChecked, |
| 680 | Self::Missing => MessageId::SetupConstitutionFileMissing, |
| 681 | Self::Loaded if choice == ConstitutionChoice::GuidedCustom => { |
| 682 | MessageId::SetupConstitutionFileLoadedSelected |
| 683 | } |
| 684 | Self::Loaded if choice.is_explicit() => MessageId::SetupConstitutionFileLoadedInactive, |
| 685 | Self::Loaded => MessageId::SetupConstitutionFileLoadedUnselected, |
| 686 | Self::Empty => MessageId::SetupConstitutionFileEmpty, |
| 687 | Self::Invalid => MessageId::SetupConstitutionFileInvalid, |
| 688 | Self::Unreadable => MessageId::SetupConstitutionFileUnreadable, |
| 689 | Self::PathError => MessageId::SetupConstitutionFilePathError, |
| 690 | }; |
| 691 | tr(locale, id) |
| 692 | } |
| 693 | } |
| 694 | |
| 695 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 696 | enum SetupExpertOverrideState { |
| 697 | NotChecked, |
| 698 | Missing, |
| 699 | Active, |
| 700 | Disabled, |
| 701 | Empty, |
| 702 | Unreadable, |
| 703 | PathError, |
| 704 | } |
| 705 | |
| 706 | impl SetupExpertOverrideState { |
| 707 | fn load() -> Self { |
| 708 | let Some(path) = expert_override_path() else { |
| 709 | return Self::PathError; |
| 710 | }; |
| 711 | match std::fs::read_to_string(&path) { |
| 712 | Ok(raw) if raw.trim().is_empty() => Self::Empty, |
| 713 | Ok(_) if base_prompt_override_opt_in() => Self::Active, |
| 714 | Ok(_) => Self::Disabled, |
| 715 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => Self::Missing, |
| 716 | Err(_) => Self::Unreadable, |
| 717 | } |
| 718 | } |
| 719 | |
| 720 | fn is_active(self) -> bool { |
| 721 | matches!(self, Self::Active) |
| 722 | } |
| 723 | |
| 724 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 725 | match self { |
| 726 | Self::NotChecked => tr(locale, MessageId::SetupExpertOverrideNotChecked), |
| 727 | Self::Missing => tr(locale, MessageId::SetupExpertOverrideMissing), |
| 728 | Self::Active => tr(locale, MessageId::SetupExpertOverrideActive), |
| 729 | Self::Disabled => tr(locale, MessageId::SetupExpertOverrideDisabled) |
| 730 | .replace("{env}", BASE_PROMPT_OVERRIDE_OPT_IN_ENV) |
| 731 | .into(), |
| 732 | Self::Empty => tr(locale, MessageId::SetupExpertOverrideEmpty), |
| 733 | Self::Unreadable => tr(locale, MessageId::SetupExpertOverrideUnreadable), |
| 734 | Self::PathError => tr(locale, MessageId::SetupExpertOverridePathError), |
| 735 | } |
| 736 | } |
| 737 | } |
| 738 | |
| 739 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 740 | pub(crate) struct GuidedConstitutionDraft { |
| 741 | purpose: GuidedPurpose, |
| 742 | autonomy: AutonomyPreference, |
| 743 | evidence: GuidedEvidence, |
| 744 | communication: GuidedCommunication, |
| 745 | privacy: GuidedPrivacy, |
| 746 | principles: GuidedPrinciples, |
| 747 | } |
| 748 | |
| 749 | impl Default for GuidedConstitutionDraft { |
| 750 | fn default() -> Self { |
| 751 | Self { |
| 752 | purpose: GuidedPurpose::Coding, |
| 753 | autonomy: AutonomyPreference::Balanced, |
| 754 | evidence: GuidedEvidence::TestsAndReceipts, |
| 755 | communication: GuidedCommunication::Concise, |
| 756 | privacy: GuidedPrivacy::StandardCare, |
| 757 | principles: GuidedPrinciples::ScopedChanges, |
| 758 | } |
| 759 | } |
| 760 | } |
| 761 | |
| 762 | impl GuidedConstitutionDraft { |
| 763 | fn cycle(&mut self, key: char) -> bool { |
| 764 | match key { |
| 765 | '1' => self.purpose = self.purpose.next(), |
| 766 | '2' => self.autonomy = next_guided_autonomy(self.autonomy), |
| 767 | '3' => self.evidence = self.evidence.next(), |
| 768 | '4' => self.communication = self.communication.next(), |
| 769 | '5' => self.privacy = self.privacy.next(), |
| 770 | '6' => self.principles = self.principles.next(), |
| 771 | _ => return false, |
| 772 | } |
| 773 | true |
| 774 | } |
| 775 | |
| 776 | #[cfg(test)] |
| 777 | fn to_constitution(self, locale: Locale) -> UserConstitution { |
| 778 | self.to_constitution_with_freeform(locale, None) |
| 779 | } |
| 780 | |
| 781 | fn to_constitution_with_freeform( |
| 782 | self, |
| 783 | locale: Locale, |
| 784 | freeform_note: Option<&str>, |
| 785 | ) -> UserConstitution { |
| 786 | let mut notes = self.notes(locale); |
| 787 | if let Some(note) = freeform_note.map(str::trim).filter(|note| !note.is_empty()) { |
| 788 | let own_words = match locale { |
| 789 | Locale::Ja => format!( |
| 790 | "\nユーザー自由原則:{}", |
| 791 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 792 | ), |
| 793 | Locale::ZhHans => format!( |
| 794 | "\n用户自定义准则:{}", |
| 795 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 796 | ), |
| 797 | Locale::ZhHant => format!( |
| 798 | "\n使用者自由原則:{}", |
| 799 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 800 | ), |
| 801 | Locale::PtBr => format!( |
| 802 | "\nPrincípio livre do usuário: {}", |
| 803 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 804 | ), |
| 805 | Locale::Es419 => format!( |
| 806 | "\nPrincipio libre del usuario: {}", |
| 807 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 808 | ), |
| 809 | Locale::Vi => format!( |
| 810 | "\nNguyên tắc tự do của người dùng: {}", |
| 811 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 812 | ), |
| 813 | Locale::Ko => format!( |
| 814 | "\n사용자 자유 원칙: {}", |
| 815 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 816 | ), |
| 817 | Locale::Ca => format!( |
| 818 | "\nPrincipi lliure de l'usuari: {}", |
| 819 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 820 | ), |
| 821 | Locale::De => format!( |
| 822 | "\nFreitext-Prinzip des Nutzers: {}", |
| 823 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 824 | ), |
| 825 | Locale::Fr => format!( |
| 826 | "\nPrincipe en texte libre de l'utilisateur : {}", |
| 827 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 828 | ), |
| 829 | Locale::Id => format!( |
| 830 | "\nPrinsip bebas pengguna: {}", |
| 831 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 832 | ), |
| 833 | Locale::Hi => format!( |
| 834 | "\nउपयोगकर्ता मुक्त-पाठ सिद्धांत: {}", |
| 835 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 836 | ), |
| 837 | Locale::Ru => format!( |
| 838 | "\nСвободный принцип пользователя: {}", |
| 839 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 840 | ), |
| 841 | Locale::Uk => format!( |
| 842 | "\nВільний принцип користувача: {}", |
| 843 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 844 | ), |
| 845 | _ => format!( |
| 846 | "\nUser freeform principle: {}", |
| 847 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 848 | ), |
| 849 | }; |
| 850 | notes.push_str(&own_words); |
| 851 | } |
| 852 | UserConstitution { |
| 853 | language: Some(locale.tag().to_string()), |
| 854 | about: Some(self.purpose.about(locale).to_string()), |
| 855 | working_style: vec![ |
| 856 | self.purpose.working_style(locale).to_string(), |
| 857 | self.communication.working_style(locale).to_string(), |
| 858 | self.evidence.working_style(locale).to_string(), |
| 859 | self.privacy.working_style(locale).to_string(), |
| 860 | ], |
| 861 | priorities: vec![ |
| 862 | authority_priority(locale).to_string(), |
| 863 | autonomy_priority(self.autonomy, locale).to_string(), |
| 864 | self.privacy.escalation_rule(locale).to_string(), |
| 865 | ], |
| 866 | autonomy_preference: self.autonomy, |
| 867 | notes: Some(notes), |
| 868 | ..UserConstitution::default() |
| 869 | } |
| 870 | } |
| 871 | |
| 872 | fn notes(self, locale: Locale) -> String { |
| 873 | let notes = tr(locale, MessageId::SetupGuidedNotes); |
| 874 | notes |
| 875 | .replace("{purpose}", &self.purpose.label(locale)) |
| 876 | .replace("{initiative}", autonomy_label(self.autonomy, locale)) |
| 877 | .replace("{evidence}", &self.evidence.label(locale)) |
| 878 | .replace("{communication}", self.communication.label(locale)) |
| 879 | .replace("{privacy}", self.privacy.label(locale)) |
| 880 | .replace("{principles}", self.principles.label(locale)) |
| 881 | .replace("{notes}", self.principles.note(locale)) |
| 882 | .to_string() |
| 883 | } |
| 884 | } |
| 885 | |
| 886 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 887 | enum GuidedPurpose { |
| 888 | Coding, |
| 889 | Research, |
| 890 | Operations, |
| 891 | Mixed, |
| 892 | } |
| 893 | |
| 894 | impl GuidedPurpose { |
| 895 | fn next(self) -> Self { |
| 896 | match self { |
| 897 | Self::Coding => Self::Research, |
| 898 | Self::Research => Self::Operations, |
| 899 | Self::Operations => Self::Mixed, |
| 900 | Self::Mixed => Self::Coding, |
| 901 | } |
| 902 | } |
| 903 | |
| 904 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 905 | match self { |
| 906 | Self::Coding => tr(locale, MessageId::SetupGuidedPurposeCoding), |
| 907 | Self::Research => tr(locale, MessageId::SetupGuidedPurposeResearch), |
| 908 | Self::Operations => tr(locale, MessageId::SetupGuidedPurposeOperations), |
| 909 | Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeMixed), |
| 910 | } |
| 911 | } |
| 912 | |
| 913 | fn about(self, locale: Locale) -> Cow<'static, str> { |
| 914 | match self { |
| 915 | Self::Coding => tr(locale, MessageId::SetupGuidedPurposeAboutCoding), |
| 916 | Self::Research => tr(locale, MessageId::SetupGuidedPurposeAboutResearch), |
| 917 | Self::Operations => tr(locale, MessageId::SetupGuidedPurposeAboutOperations), |
| 918 | Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeAboutMixed), |
| 919 | } |
| 920 | } |
| 921 | |
| 922 | fn working_style(self, locale: Locale) -> Cow<'static, str> { |
| 923 | match self { |
| 924 | Self::Coding => tr(locale, MessageId::SetupGuidedStyleCoding), |
| 925 | Self::Research => tr(locale, MessageId::SetupGuidedStyleResearch), |
| 926 | Self::Operations => tr(locale, MessageId::SetupGuidedStyleOperations), |
| 927 | Self::Mixed => tr(locale, MessageId::SetupGuidedStyleMixed), |
| 928 | } |
| 929 | } |
| 930 | } |
| 931 | |
| 932 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 933 | enum GuidedEvidence { |
| 934 | Assumptions, |
| 935 | TestsAndReceipts, |
| 936 | ReleaseReceipts, |
| 937 | } |
| 938 | |
| 939 | impl GuidedEvidence { |
| 940 | fn next(self) -> Self { |
| 941 | match self { |
| 942 | Self::Assumptions => Self::TestsAndReceipts, |
| 943 | Self::TestsAndReceipts => Self::ReleaseReceipts, |
| 944 | Self::ReleaseReceipts => Self::Assumptions, |
| 945 | } |
| 946 | } |
| 947 | |
| 948 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 949 | match self { |
| 950 | Self::Assumptions => tr(locale, MessageId::SetupGuidedEvidenceAssumptions), |
| 951 | Self::TestsAndReceipts => tr(locale, MessageId::SetupGuidedEvidenceTestsAndReceipts), |
| 952 | Self::ReleaseReceipts => tr(locale, MessageId::SetupGuidedEvidenceReleaseReceipts), |
| 953 | } |
| 954 | } |
| 955 | |
| 956 | fn working_style(self, locale: Locale) -> &'static str { |
| 957 | match (locale, self) { |
| 958 | (Locale::Ja, Self::Assumptions) => { |
| 959 | "完了を主張する前に、前提、不明点、残るリスクを要約する。" |
| 960 | } |
| 961 | (Locale::Ja, Self::TestsAndReceipts) => { |
| 962 | "不確実性を減らせるときは、コマンド、テスト、スクリーンショット、引用で具体的に検証する。" |
| 963 | } |
| 964 | (Locale::Ja, Self::ReleaseReceipts) => { |
| 965 | "重要な主張とリリース証拠には、ファイル、コマンド、スクリーンショット、CI、出典を示す。" |
| 966 | } |
| 967 | (Locale::ZhHans, Self::Assumptions) => "在宣称完成前总结假设、未知和剩余风险。", |
| 968 | (Locale::ZhHans, Self::TestsAndReceipts) => { |
| 969 | "在能降低不确定性时,用命令、测试、截图或引用给出具体验证。" |
| 970 | } |
| 971 | (Locale::ZhHans, Self::ReleaseReceipts) => { |
| 972 | "对重要结论和发布证据标注文件、命令、截图、CI 或来源。" |
| 973 | } |
| 974 | (Locale::ZhHant, Self::Assumptions) => "在宣稱完成前總結假設、未知和剩餘風險。", |
| 975 | (Locale::ZhHant, Self::TestsAndReceipts) => { |
| 976 | "在能降低不確定性時,用命令、測試、截圖或引用給出具體驗證。" |
| 977 | } |
| 978 | (Locale::ZhHant, Self::ReleaseReceipts) => { |
| 979 | "對重要結論和發布證據標註檔案、命令、截圖、CI 或來源。" |
| 980 | } |
| 981 | (Locale::PtBr, Self::Assumptions) => { |
| 982 | "Resuma premissas, desconhecidos e risco restante antes de dizer que concluiu." |
| 983 | } |
| 984 | (Locale::PtBr, Self::TestsAndReceipts) => { |
| 985 | "Use comandos, testes, screenshots ou citações quando reduzirem a incerteza." |
| 986 | } |
| 987 | (Locale::PtBr, Self::ReleaseReceipts) => { |
| 988 | "Cite arquivos, comandos, screenshots, CI ou fontes para afirmações materiais e evidência de release." |
| 989 | } |
| 990 | (Locale::Es419, Self::Assumptions) => { |
| 991 | "Resume supuestos, incógnitas y riesgo restante antes de afirmar que terminaste." |
| 992 | } |
| 993 | (Locale::Es419, Self::TestsAndReceipts) => { |
| 994 | "Usa comandos, pruebas, capturas o citas cuando reduzcan materialmente la incertidumbre." |
| 995 | } |
| 996 | (Locale::Es419, Self::ReleaseReceipts) => { |
| 997 | "Cita archivos, comandos, capturas, CI o fuentes para afirmaciones materiales y evidencia de release." |
| 998 | } |
| 999 | (Locale::Vi, Self::Assumptions) => { |
| 1000 | "Tóm tắt giả định, điều chưa biết và rủi ro còn lại trước khi tuyên bố hoàn tất." |
| 1001 | } |
| 1002 | (Locale::Vi, Self::TestsAndReceipts) => { |
| 1003 | "Dùng lệnh, kiểm thử, ảnh chụp hoặc trích dẫn khi chúng giảm đáng kể bất định." |
| 1004 | } |
| 1005 | (Locale::Vi, Self::ReleaseReceipts) => { |
| 1006 | "Trích dẫn tệp, lệnh, ảnh chụp, CI hoặc nguồn cho tuyên bố quan trọng và bằng chứng phát hành." |
| 1007 | } |
| 1008 | (Locale::Ko, Self::Assumptions) => { |
| 1009 | "완료를 주장하기 전에 가정, 불확실한 점, 남은 위험을 요약한다." |
| 1010 | } |
| 1011 | (Locale::Ko, Self::TestsAndReceipts) => { |
| 1012 | "불확실성을 실질적으로 줄일 수 있을 때는 명령어, 테스트, 스크린샷, 인용으로 구체적으로 검증한다." |
| 1013 | } |
| 1014 | (Locale::Ko, Self::ReleaseReceipts) => { |
| 1015 | "중요한 주장과 릴리스 근거에는 파일 경로, 명령어, 스크린샷, CI, 출처를 제시한다." |
| 1016 | } |
| 1017 | (Locale::Ca, Self::Assumptions) => { |
| 1018 | "Resumeix supòsits, incògnites i risc pendent abans de dir que has acabat." |
| 1019 | } |
| 1020 | (Locale::Ca, Self::TestsAndReceipts) => { |
| 1021 | "Fes servir ordres, tests, captures de pantalla o citacions quan redueixin materialment la incertesa." |
| 1022 | } |
| 1023 | (Locale::Ca, Self::ReleaseReceipts) => { |
| 1024 | "Cita rutes de fitxers, ordres, captures de pantalla, CI o fonts per a afirmacions materials i evidència de release." |
| 1025 | } |
| 1026 | (Locale::De, Self::Assumptions) => { |
| 1027 | "Fasse Annahmen, Unbekannte und Restrisiken zusammen, bevor du Fertigstellung behauptest." |
| 1028 | } |
| 1029 | (Locale::De, Self::TestsAndReceipts) => { |
| 1030 | "Nutze Befehle, Tests, Screenshots oder Zitate, wenn sie die Unsicherheit wesentlich verringern." |
| 1031 | } |
| 1032 | (Locale::De, Self::ReleaseReceipts) => { |
| 1033 | "Nenne Dateipfade, Befehle, Screenshots, CI oder Quellen für wesentliche Aussagen und Release-Nachweise." |
| 1034 | } |
| 1035 | (Locale::Fr, Self::Assumptions) => { |
| 1036 | "Résumez les hypothèses, les inconnues et le risque restant avant d'annoncer la fin du travail." |
| 1037 | } |
| 1038 | (Locale::Fr, Self::TestsAndReceipts) => { |
| 1039 | "Utilisez commandes, tests, captures d'écran ou citations quand ils réduisent sensiblement l'incertitude." |
| 1040 | } |
| 1041 | (Locale::Fr, Self::ReleaseReceipts) => { |
| 1042 | "Citez chemins de fichiers, commandes, captures d'écran, CI ou sources pour les affirmations importantes et les preuves de release." |
| 1043 | } |
| 1044 | (Locale::Id, Self::Assumptions) => { |
| 1045 | "Ringkas asumsi, hal yang belum diketahui, dan risiko tersisa sebelum mengklaim selesai." |
| 1046 | } |
| 1047 | (Locale::Id, Self::TestsAndReceipts) => { |
| 1048 | "Gunakan perintah, tes, tangkapan layar, atau kutipan bila secara nyata mengurangi ketidakpastian." |
| 1049 | } |
| 1050 | (Locale::Id, Self::ReleaseReceipts) => { |
| 1051 | "Kutip path file, perintah, tangkapan layar, CI, atau sumber untuk klaim material dan bukti rilis." |
| 1052 | } |
| 1053 | (Locale::Hi, Self::Assumptions) => { |
| 1054 | "पूर्णता का दावा करने से पहले धारणाएँ, अज्ञात बातें और शेष जोखिम सारांशित करें।" |
| 1055 | } |
| 1056 | (Locale::Hi, Self::TestsAndReceipts) => { |
| 1057 | "जब वे अनिश्चितता सार्थक रूप से घटाएँ तो कमांड, टेस्ट, स्क्रीनशॉट या उद्धरण उपयोग करें।" |
| 1058 | } |
| 1059 | (Locale::Hi, Self::ReleaseReceipts) => { |
| 1060 | "महत्वपूर्ण दावों और रिलीज़ साक्ष्य के लिए फ़ाइल पथ, कमांड, स्क्रीनशॉट, CI या स्रोत उद्धृत करें।" |
| 1061 | } |
| 1062 | (Locale::Ru, Self::Assumptions) => { |
| 1063 | "Прежде чем заявить о завершении, перечислите предположения, неизвестные и оставшиеся риски." |
| 1064 | } |
| 1065 | (Locale::Ru, Self::TestsAndReceipts) => { |
| 1066 | "Используйте команды, тесты, скриншоты или цитаты, когда они существенно снижают неопределённость." |
| 1067 | } |
| 1068 | (Locale::Ru, Self::ReleaseReceipts) => { |
| 1069 | "Указывайте пути файлов, команды, скриншоты, CI или источники для существенных утверждений и доказательств релиза." |
| 1070 | } |
| 1071 | (Locale::Uk, Self::Assumptions) => { |
| 1072 | "Перш ніж заявити про завершення, підсумуйте припущення, невідомі та залишкові ризики." |
| 1073 | } |
| 1074 | (Locale::Uk, Self::TestsAndReceipts) => { |
| 1075 | "Використовуйте команди, тести, скриншоти або цитати, коли вони суттєво зменшують невизначеність." |
| 1076 | } |
| 1077 | (Locale::Uk, Self::ReleaseReceipts) => { |
| 1078 | "Посилайтеся на шляхи файлів, команди, скриншоти, CI або джерела для суттєвих тверджень і доказів релізу." |
| 1079 | } |
| 1080 | (_, Self::Assumptions) => { |
| 1081 | "Summarize assumptions, unknowns, and remaining risk before claiming completion." |
| 1082 | } |
| 1083 | (_, Self::TestsAndReceipts) => { |
| 1084 | "Use commands, tests, screenshots, or citations when they materially reduce uncertainty." |
| 1085 | } |
| 1086 | (_, Self::ReleaseReceipts) => { |
| 1087 | "Cite file paths, commands, screenshots, CI, or sources for material claims and release evidence." |
| 1088 | } |
| 1089 | } |
| 1090 | } |
| 1091 | } |
| 1092 | |
| 1093 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1094 | enum GuidedCommunication { |
| 1095 | Concise, |
| 1096 | Teaching, |
| 1097 | Direct, |
| 1098 | } |
| 1099 | |
| 1100 | impl GuidedCommunication { |
| 1101 | fn next(self) -> Self { |
| 1102 | match self { |
| 1103 | Self::Concise => Self::Teaching, |
| 1104 | Self::Teaching => Self::Direct, |
| 1105 | Self::Direct => Self::Concise, |
| 1106 | } |
| 1107 | } |
| 1108 | |
| 1109 | fn label(self, locale: Locale) -> &'static str { |
| 1110 | match (locale, self) { |
| 1111 | (Locale::Ja, Self::Concise) => "簡潔", |
| 1112 | (Locale::Ja, Self::Teaching) => "説明重視", |
| 1113 | (Locale::Ja, Self::Direct) => "直接的", |
| 1114 | (Locale::ZhHans, Self::Concise) => "简洁", |
| 1115 | (Locale::ZhHans, Self::Teaching) => "教学式", |
| 1116 | (Locale::ZhHans, Self::Direct) => "直接", |
| 1117 | (Locale::ZhHant, Self::Concise) => "簡潔", |
| 1118 | (Locale::ZhHant, Self::Teaching) => "教學式", |
| 1119 | (Locale::ZhHant, Self::Direct) => "直接", |
| 1120 | (Locale::PtBr, Self::Concise) => "conciso", |
| 1121 | (Locale::PtBr, Self::Teaching) => "didático", |
| 1122 | (Locale::PtBr, Self::Direct) => "direto", |
| 1123 | (Locale::Es419, Self::Concise) => "conciso", |
| 1124 | (Locale::Es419, Self::Teaching) => "didáctico", |
| 1125 | (Locale::Es419, Self::Direct) => "directo", |
| 1126 | (Locale::Vi, Self::Concise) => "ngắn gọn", |
| 1127 | (Locale::Vi, Self::Teaching) => "giảng giải", |
| 1128 | (Locale::Vi, Self::Direct) => "trực tiếp", |
| 1129 | (Locale::Ko, Self::Concise) => "간결함", |
| 1130 | (Locale::Ko, Self::Teaching) => "설명 중심", |
| 1131 | (Locale::Ko, Self::Direct) => "직설적", |
| 1132 | (Locale::Ca, Self::Concise) => "concís", |
| 1133 | (Locale::Ca, Self::Teaching) => "didàctic", |
| 1134 | (Locale::Ca, Self::Direct) => "directe", |
| 1135 | (Locale::De, Self::Concise) => "prägnant", |
| 1136 | (Locale::De, Self::Teaching) => "lehrend", |
| 1137 | (Locale::De, Self::Direct) => "direkt", |
| 1138 | (Locale::Fr, Self::Concise) => "concis", |
| 1139 | (Locale::Fr, Self::Teaching) => "pédagogique", |
| 1140 | (Locale::Fr, Self::Direct) => "direct", |
| 1141 | (Locale::Id, Self::Concise) => "ringkas", |
| 1142 | (Locale::Id, Self::Teaching) => "mengajar", |
| 1143 | (Locale::Id, Self::Direct) => "langsung", |
| 1144 | (Locale::Hi, Self::Concise) => "संक्षिप्त", |
| 1145 | (Locale::Hi, Self::Teaching) => "शिक्षणपरक", |
| 1146 | (Locale::Hi, Self::Direct) => "सीधा", |
| 1147 | (Locale::Ru, Self::Concise) => "краткий", |
| 1148 | (Locale::Ru, Self::Teaching) => "обучающий", |
| 1149 | (Locale::Ru, Self::Direct) => "прямой", |
| 1150 | (Locale::Uk, Self::Concise) => "стислий", |
| 1151 | (Locale::Uk, Self::Teaching) => "навчальний", |
| 1152 | (Locale::Uk, Self::Direct) => "прямий", |
| 1153 | (_, Self::Concise) => "concise", |
| 1154 | (_, Self::Teaching) => "teaching", |
| 1155 | (_, Self::Direct) => "direct", |
| 1156 | } |
| 1157 | } |
| 1158 | |
| 1159 | fn working_style(self, locale: Locale) -> &'static str { |
| 1160 | match (locale, self) { |
| 1161 | (Locale::Ja, Self::Concise) => "更新は簡潔にし、重要なトレードオフだけ短く説明する。", |
| 1162 | (Locale::Ja, Self::Teaching) => { |
| 1163 | "重要な推論とトレードオフを、ユーザーが仕組みを理解できる程度に説明する。" |
| 1164 | } |
| 1165 | (Locale::Ja, Self::Direct) => { |
| 1166 | "阻塞、リスク、不確実性を直接述べ、装飾的な文案を避ける。" |
| 1167 | } |
| 1168 | (Locale::ZhHans, Self::Concise) => "保持更新简洁,并只解释重要取舍。", |
| 1169 | (Locale::ZhHans, Self::Teaching) => "解释关键推理和取舍,让用户能理解系统。", |
| 1170 | (Locale::ZhHans, Self::Direct) => "直接说明阻塞、风险和不确定性,避免装饰性文案。", |
| 1171 | (Locale::ZhHant, Self::Concise) => "保持更新簡潔,並只解釋重要取捨。", |
| 1172 | (Locale::ZhHant, Self::Teaching) => "解釋關鍵推理和取捨,讓使用者能理解系統。", |
| 1173 | (Locale::ZhHant, Self::Direct) => "直接說明阻塞、風險和不確定性,避免裝飾性文案。", |
| 1174 | (Locale::PtBr, Self::Concise) => { |
| 1175 | "Mantenha atualizações concisas e explique brevemente só os tradeoffs importantes." |
| 1176 | } |
| 1177 | (Locale::PtBr, Self::Teaching) => { |
| 1178 | "Explique raciocínio e tradeoffs principais o bastante para o usuário entender o sistema." |
| 1179 | } |
| 1180 | (Locale::PtBr, Self::Direct) => { |
| 1181 | "Seja direto sobre bloqueios, risco e incerteza; evite texto ornamental." |
| 1182 | } |
| 1183 | (Locale::Es419, Self::Concise) => { |
| 1184 | "Mantén las actualizaciones concisas y explica brevemente solo los tradeoffs importantes." |
| 1185 | } |
| 1186 | (Locale::Es419, Self::Teaching) => { |
| 1187 | "Explica el razonamiento y los tradeoffs clave lo suficiente para que el usuario entienda el sistema." |
| 1188 | } |
| 1189 | (Locale::Es419, Self::Direct) => { |
| 1190 | "Sé directo sobre bloqueos, riesgo e incertidumbre; evita texto ornamental." |
| 1191 | } |
| 1192 | (Locale::Vi, Self::Concise) => { |
| 1193 | "Giữ cập nhật ngắn gọn và chỉ giải thích ngắn các đánh đổi quan trọng." |
| 1194 | } |
| 1195 | (Locale::Vi, Self::Teaching) => { |
| 1196 | "Giải thích suy luận và đánh đổi chính đủ để người dùng hiểu hệ thống." |
| 1197 | } |
| 1198 | (Locale::Vi, Self::Direct) => { |
| 1199 | "Nói thẳng về điểm chặn, rủi ro và bất định; tránh câu chữ trang trí." |
| 1200 | } |
| 1201 | (Locale::Ko, Self::Concise) => { |
| 1202 | "업데이트는 간결하게 유지하고, 중요한 트레이드오프만 짧게 설명한다." |
| 1203 | } |
| 1204 | (Locale::Ko, Self::Teaching) => { |
| 1205 | "사용자가 시스템을 이해할 수 있을 만큼 핵심 추론과 트레이드오프를 설명한다." |
| 1206 | } |
| 1207 | (Locale::Ko, Self::Direct) => { |
| 1208 | "차단 요인, 위험, 불확실성을 직설적으로 말하고 장식적인 표현은 피한다." |
| 1209 | } |
| 1210 | (Locale::Ca, Self::Concise) => { |
| 1211 | "Mantén les actualitzacions concises i explica breument només els compromisos importants." |
| 1212 | } |
| 1213 | (Locale::Ca, Self::Teaching) => { |
| 1214 | "Explica el raonament i els compromisos clau prou perquè l'usuari pugui entendre el sistema." |
| 1215 | } |
| 1216 | (Locale::Ca, Self::Direct) => { |
| 1217 | "Sigues directe sobre bloquejos, risc i incertesa; evita el text ornamental." |
| 1218 | } |
| 1219 | (Locale::De, Self::Concise) => { |
| 1220 | "Halte Aktualisierungen knapp und erkläre wichtige Trade-offs nur kurz." |
| 1221 | } |
| 1222 | (Locale::De, Self::Teaching) => { |
| 1223 | "Erkläre zentrale Begründungen und Trade-offs so weit, dass der Nutzer das System verstehen kann." |
| 1224 | } |
| 1225 | (Locale::De, Self::Direct) => { |
| 1226 | "Sei direkt bei Blockern, Risiken und Unsicherheit; vermeide dekorative Formulierungen." |
| 1227 | } |
| 1228 | (Locale::Fr, Self::Concise) => { |
| 1229 | "Gardez les mises à jour concises et n'expliquez que brièvement les arbitrages importants." |
| 1230 | } |
| 1231 | (Locale::Fr, Self::Teaching) => { |
| 1232 | "Expliquez le raisonnement et les arbitrages clés assez pour que l'utilisateur comprenne le système." |
| 1233 | } |
| 1234 | (Locale::Fr, Self::Direct) => { |
| 1235 | "Soyez direct sur les blocages, les risques et l'incertitude ; évitez le texte ornemental." |
| 1236 | } |
| 1237 | (Locale::Id, Self::Concise) => { |
| 1238 | "Jaga pembaruan tetap ringkas dan jelaskan tradeoff penting secara singkat." |
| 1239 | } |
| 1240 | (Locale::Id, Self::Teaching) => { |
| 1241 | "Jelaskan penalaran dan tradeoff kunci secukupnya agar pengguna dapat memahami sistem." |
| 1242 | } |
| 1243 | (Locale::Id, Self::Direct) => { |
| 1244 | "Bicara langsung soal penghambat, risiko, dan ketidakpastian; hindari teks hiasan." |
| 1245 | } |
| 1246 | (Locale::Hi, Self::Concise) => "अपडेट संक्षिप्त रखें और महत्वपूर्ण ट्रेडऑफ़ संक्षेप में समझाएँ।", |
| 1247 | (Locale::Hi, Self::Teaching) => { |
| 1248 | "मुख्य तर्क और ट्रेडऑफ़ इतना समझाएँ कि उपयोगकर्ता सिस्टम समझ सके।" |
| 1249 | } |
| 1250 | (Locale::Hi, Self::Direct) => { |
| 1251 | "रुकावटों, जोखिम और अनिश्चितता के बारे में सीधे बोलें; सजावटी भाषा से बचें।" |
| 1252 | } |
| 1253 | (Locale::Ru, Self::Concise) => { |
| 1254 | "Держите обновления краткими и лишь коротко поясняйте важные компромиссы." |
| 1255 | } |
| 1256 | (Locale::Ru, Self::Teaching) => { |
| 1257 | "Объясняйте ключевые рассуждения и компромиссы настолько, чтобы пользователь мог понять систему." |
| 1258 | } |
| 1259 | (Locale::Ru, Self::Direct) => { |
| 1260 | "Говорите прямо о блокерах, рисках и неопределённости; избегайте декоративных формулировок." |
| 1261 | } |
| 1262 | (Locale::Uk, Self::Concise) => { |
| 1263 | "Тримайте оновлення стислими й лише коротко пояснюйте важливі компроміси." |
| 1264 | } |
| 1265 | (Locale::Uk, Self::Teaching) => { |
| 1266 | "Пояснюйте ключові міркування та компроміси настільки, щоб користувач міг зрозуміти систему." |
| 1267 | } |
| 1268 | (Locale::Uk, Self::Direct) => { |
| 1269 | "Говоріть прямо про блокери, ризики та невизначеність; уникайте декоративних формулювань." |
| 1270 | } |
| 1271 | (_, Self::Concise) => "Keep updates concise and explain important tradeoffs briefly.", |
| 1272 | (_, Self::Teaching) => { |
| 1273 | "Explain key reasoning and tradeoffs enough that the user can learn the system." |
| 1274 | } |
| 1275 | (_, Self::Direct) => { |
| 1276 | "Be direct about blockers, risk, and uncertainty; avoid ornamental copy." |
| 1277 | } |
| 1278 | } |
| 1279 | } |
| 1280 | } |
| 1281 | |
| 1282 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1283 | enum GuidedPrivacy { |
| 1284 | StandardCare, |
| 1285 | StrictBoundaries, |
| 1286 | ProjectLocal, |
| 1287 | } |
| 1288 | |
| 1289 | impl GuidedPrivacy { |
| 1290 | fn next(self) -> Self { |
| 1291 | match self { |
| 1292 | Self::StandardCare => Self::StrictBoundaries, |
| 1293 | Self::StrictBoundaries => Self::ProjectLocal, |
| 1294 | Self::ProjectLocal => Self::StandardCare, |
| 1295 | } |
| 1296 | } |
| 1297 | |
| 1298 | fn label(self, locale: Locale) -> &'static str { |
| 1299 | match (locale, self) { |
| 1300 | (Locale::Ja, Self::StandardCare) => "標準保護", |
| 1301 | (Locale::Ja, Self::StrictBoundaries) => "厳格な境界", |
| 1302 | (Locale::Ja, Self::ProjectLocal) => "プロジェクト内メモリ", |
| 1303 | (Locale::ZhHans, Self::StandardCare) => "标准保护", |
| 1304 | (Locale::ZhHans, Self::StrictBoundaries) => "严格边界", |
| 1305 | (Locale::ZhHans, Self::ProjectLocal) => "项目内记忆", |
| 1306 | (Locale::ZhHant, Self::StandardCare) => "標準保護", |
| 1307 | (Locale::ZhHant, Self::StrictBoundaries) => "嚴格邊界", |
| 1308 | (Locale::ZhHant, Self::ProjectLocal) => "專案內記憶", |
| 1309 | (Locale::PtBr, Self::StandardCare) => "cuidado padrão", |
| 1310 | (Locale::PtBr, Self::StrictBoundaries) => "limites estritos", |
| 1311 | (Locale::PtBr, Self::ProjectLocal) => "memória local do projeto", |
| 1312 | (Locale::Es419, Self::StandardCare) => "cuidado estándar", |
| 1313 | (Locale::Es419, Self::StrictBoundaries) => "límites estrictos", |
| 1314 | (Locale::Es419, Self::ProjectLocal) => "memoria local del proyecto", |
| 1315 | (Locale::Vi, Self::StandardCare) => "bảo vệ tiêu chuẩn", |
| 1316 | (Locale::Vi, Self::StrictBoundaries) => "ranh giới nghiêm ngặt", |
| 1317 | (Locale::Vi, Self::ProjectLocal) => "bộ nhớ trong dự án", |
| 1318 | (Locale::Ko, Self::StandardCare) => "표준 보호", |
| 1319 | (Locale::Ko, Self::StrictBoundaries) => "엄격한 경계", |
| 1320 | (Locale::Ko, Self::ProjectLocal) => "프로젝트 내 메모리", |
| 1321 | (Locale::Ca, Self::StandardCare) => "cura estàndard", |
| 1322 | (Locale::Ca, Self::StrictBoundaries) => "límits estrictes", |
| 1323 | (Locale::Ca, Self::ProjectLocal) => "memòria local del projecte", |
| 1324 | (Locale::De, Self::StandardCare) => "Standardvorsorge", |
| 1325 | (Locale::De, Self::StrictBoundaries) => "strenge Grenzen", |
| 1326 | (Locale::De, Self::ProjectLocal) => "projektlokaler Speicher", |
| 1327 | (Locale::Fr, Self::StandardCare) => "soin standard", |
| 1328 | (Locale::Fr, Self::StrictBoundaries) => "limites strictes", |
| 1329 | (Locale::Fr, Self::ProjectLocal) => "mémoire locale au projet", |
| 1330 | (Locale::Id, Self::StandardCare) => "perlindungan standar", |
| 1331 | (Locale::Id, Self::StrictBoundaries) => "batasan ketat", |
| 1332 | (Locale::Id, Self::ProjectLocal) => "memori lokal proyek", |
| 1333 | (Locale::Hi, Self::StandardCare) => "मानक सावधानी", |
| 1334 | (Locale::Hi, Self::StrictBoundaries) => "सख्त सीमाएँ", |
| 1335 | (Locale::Hi, Self::ProjectLocal) => "प्रोजेक्ट-स्थानीय मेमोरी", |
| 1336 | (Locale::Ru, Self::StandardCare) => "стандартная осторожность", |
| 1337 | (Locale::Ru, Self::StrictBoundaries) => "строгие границы", |
| 1338 | (Locale::Ru, Self::ProjectLocal) => "память внутри проекта", |
| 1339 | (Locale::Uk, Self::StandardCare) => "стандартна обережність", |
| 1340 | (Locale::Uk, Self::StrictBoundaries) => "суворі межі", |
| 1341 | (Locale::Uk, Self::ProjectLocal) => "пам'ять у межах проєкту", |
| 1342 | (_, Self::StandardCare) => "standard care", |
| 1343 | (_, Self::StrictBoundaries) => "strict boundaries", |
| 1344 | (_, Self::ProjectLocal) => "project-local memory", |
| 1345 | } |
| 1346 | } |
| 1347 | |
| 1348 | fn working_style(self, locale: Locale) -> &'static str { |
| 1349 | match (locale, self) { |
| 1350 | (Locale::Ja, Self::StandardCare) => { |
| 1351 | "秘密情報、ユーザーファイル、Git 履歴、本番システム、コスト、プライバシー、時間を保護する。" |
| 1352 | } |
| 1353 | (Locale::Ja, Self::StrictBoundaries) => { |
| 1354 | "秘密、個人データ、認証情報、本番状態、資金、公開操作は、先に確認する境界として扱う。" |
| 1355 | } |
| 1356 | (Locale::Ja, Self::ProjectLocal) => { |
| 1357 | "プロジェクト固有の文脈はプロジェクト内に留め、明示要求がない限りメモリへ書かない。" |
| 1358 | } |
| 1359 | (Locale::ZhHans, Self::StandardCare) => { |
| 1360 | "保护密钥、用户文件、Git 历史、生产系统、成本、隐私和时间。" |
| 1361 | } |
| 1362 | (Locale::ZhHans, Self::StrictBoundaries) => { |
| 1363 | "把密钥、个人数据、凭据、生产状态、资金和发布动作视为先确认边界。" |
| 1364 | } |
| 1365 | (Locale::ZhHans, Self::ProjectLocal) => { |
| 1366 | "项目特定上下文留在项目内,除非明确要求,否则不要写入记忆。" |
| 1367 | } |
| 1368 | (Locale::ZhHant, Self::StandardCare) => { |
| 1369 | "保護密鑰、使用者檔案、Git 歷史、生產系統、成本、隱私和時間。" |
| 1370 | } |
| 1371 | (Locale::ZhHant, Self::StrictBoundaries) => { |
| 1372 | "把密鑰、個人資料、憑據、生產狀態、資金和發布動作視為先確認邊界。" |
| 1373 | } |
| 1374 | (Locale::ZhHant, Self::ProjectLocal) => { |
| 1375 | "專案特定上下文留在專案內,除非明確要求,否則不要寫入記憶。" |
| 1376 | } |
| 1377 | (Locale::PtBr, Self::StandardCare) => { |
| 1378 | "Proteja segredos, arquivos do usuário, histórico git, produção, custo, privacidade e tempo." |
| 1379 | } |
| 1380 | (Locale::PtBr, Self::StrictBoundaries) => { |
| 1381 | "Trate segredos, dados pessoais, credenciais, estado de produção, dinheiro e publicações como limites de confirmação." |
| 1382 | } |
| 1383 | (Locale::PtBr, Self::ProjectLocal) => { |
| 1384 | "Mantenha contexto específico do projeto no projeto; evite gravar na memória sem pedido explícito." |
| 1385 | } |
| 1386 | (Locale::Es419, Self::StandardCare) => { |
| 1387 | "Protege secretos, archivos del usuario, historial git, producción, costo, privacidad y tiempo." |
| 1388 | } |
| 1389 | (Locale::Es419, Self::StrictBoundaries) => { |
| 1390 | "Trata secretos, datos personales, credenciales, estado de producción, dinero y publicaciones como límites de confirmación." |
| 1391 | } |
| 1392 | (Locale::Es419, Self::ProjectLocal) => { |
| 1393 | "Mantén el contexto específico del proyecto en el proyecto; evita llevarlo a memoria sin pedido explícito." |
| 1394 | } |
| 1395 | (Locale::Vi, Self::StandardCare) => { |
| 1396 | "Bảo vệ bí mật, tệp người dùng, lịch sử git, hệ thống sản xuất, chi phí, riêng tư và thời gian." |
| 1397 | } |
| 1398 | (Locale::Vi, Self::StrictBoundaries) => { |
| 1399 | "Xem bí mật, dữ liệu cá nhân, thông tin xác thực, trạng thái sản xuất, tiền và xuất bản là ranh giới cần xác nhận." |
| 1400 | } |
| 1401 | (Locale::Vi, Self::ProjectLocal) => { |
| 1402 | "Giữ ngữ cảnh riêng của dự án trong dự án; tránh ghi vào bộ nhớ nếu không được yêu cầu rõ." |
| 1403 | } |
| 1404 | (Locale::Ko, Self::StandardCare) => { |
| 1405 | "비밀 정보, 사용자 파일, Git 이력, 프로덕션 시스템, 비용, 프라이버시, 시간을 보호한다." |
| 1406 | } |
| 1407 | (Locale::Ko, Self::StrictBoundaries) => { |
| 1408 | "비밀 정보, 개인 데이터, 자격 증명, 프로덕션 상태, 자금, 게시 작업은 먼저 확인하는 경계로 취급한다." |
| 1409 | } |
| 1410 | (Locale::Ko, Self::ProjectLocal) => { |
| 1411 | "프로젝트 고유 맥락은 프로젝트 안에 두고, 명시적으로 요청받지 않는 한 메모리에 쓰지 않는다." |
| 1412 | } |
| 1413 | (Locale::Ca, Self::StandardCare) => { |
| 1414 | "Protegeix secrets, fitxers de l'usuari, historial de git, sistemes de producció, cost, privacitat i temps." |
| 1415 | } |
| 1416 | (Locale::Ca, Self::StrictBoundaries) => { |
| 1417 | "Tracta secrets, dades personals, credencials, estat de producció, diners i accions de publicació com a límits que cal confirmar primer." |
| 1418 | } |
| 1419 | (Locale::Ca, Self::ProjectLocal) => { |
| 1420 | "Mantén el context específic del projecte dins del projecte; evita portar-lo a la memòria si no se't demana explícitament." |
| 1421 | } |
| 1422 | (Locale::De, Self::StandardCare) => { |
| 1423 | "Schütze Geheimnisse, Nutzerdateien, Git-Verlauf, Produktionssysteme, Kosten, Privatsphäre und Zeit." |
| 1424 | } |
| 1425 | (Locale::De, Self::StrictBoundaries) => { |
| 1426 | "Behandle Geheimnisse, persönliche Daten, Zugangsdaten, Produktionszustand, Geld und Veröffentlichungen als Grenzen, die erst bestätigt werden." |
| 1427 | } |
| 1428 | (Locale::De, Self::ProjectLocal) => { |
| 1429 | "Halte projektspezifischen Kontext im Projekt; vermeide es, sensible Details ohne ausdrückliche Bitte in den Speicher zu übernehmen." |
| 1430 | } |
| 1431 | (Locale::Fr, Self::StandardCare) => { |
| 1432 | "Protégez secrets, fichiers utilisateur, historique git, systèmes de production, coût, vie privée et temps." |
| 1433 | } |
| 1434 | (Locale::Fr, Self::StrictBoundaries) => { |
| 1435 | "Traitez secrets, données personnelles, identifiants, état de production, argent et publications comme des limites exigeant confirmation." |
| 1436 | } |
| 1437 | (Locale::Fr, Self::ProjectLocal) => { |
| 1438 | "Gardez le contexte propre au projet dans le projet ; évitez de l'écrire en mémoire sans demande explicite." |
| 1439 | } |
| 1440 | (Locale::Id, Self::StandardCare) => { |
| 1441 | "Lindungi rahasia, file pengguna, riwayat git, sistem produksi, biaya, privasi, dan waktu." |
| 1442 | } |
| 1443 | (Locale::Id, Self::StrictBoundaries) => { |
| 1444 | "Perlakukan rahasia, data pribadi, kredensial, status produksi, uang, dan tindakan publikasi sebagai batas yang harus dikonfirmasi dulu." |
| 1445 | } |
| 1446 | (Locale::Id, Self::ProjectLocal) => { |
| 1447 | "Simpan konteks khusus proyek di dalam proyek; hindari membawanya ke memori kecuali diminta secara eksplisit." |
| 1448 | } |
| 1449 | (Locale::Hi, Self::StandardCare) => { |
| 1450 | "रहस्यों, उपयोगकर्ता फ़ाइलों, git इतिहास, प्रोडक्शन सिस्टम, लागत, गोपनीयता और समय की रक्षा करें।" |
| 1451 | } |
| 1452 | (Locale::Hi, Self::StrictBoundaries) => { |
| 1453 | "रहस्यों, व्यक्तिगत डेटा, क्रेडेंशियल, प्रोडक्शन स्थिति, धन और प्रकाशन क्रियाओं को पहले-पुष्टि सीमाओं की तरह मानें।" |
| 1454 | } |
| 1455 | (Locale::Hi, Self::ProjectLocal) => { |
| 1456 | "प्रोजेक्ट-विशिष्ट संदर्भ प्रोजेक्ट के भीतर रखें; स्पष्ट अनुरोध के बिना संवेदनशील विवरण मेमोरी में न ले जाएँ।" |
| 1457 | } |
| 1458 | (Locale::Ru, Self::StandardCare) => { |
| 1459 | "Защищайте секреты, файлы пользователя, историю git, production-системы, затраты, приватность и время." |
| 1460 | } |
| 1461 | (Locale::Ru, Self::StrictBoundaries) => { |
| 1462 | "Считайте секреты, персональные данные, учётные данные, production-состояние, деньги и публикации границами, требующими подтверждения." |
| 1463 | } |
| 1464 | (Locale::Ru, Self::ProjectLocal) => { |
| 1465 | "Держите контекст, специфичный для проекта, внутри проекта; не переносите чувствительные детали в память без явного запроса." |
| 1466 | } |
| 1467 | (Locale::Uk, Self::StandardCare) => { |
| 1468 | "Захищайте секрети, файли користувача, історію git, production-системи, витрати, приватність і час." |
| 1469 | } |
| 1470 | (Locale::Uk, Self::StrictBoundaries) => { |
| 1471 | "Вважайте секрети, персональні дані, облікові дані, production-стан, гроші та публікації межами, що потребують підтвердження." |
| 1472 | } |
| 1473 | (Locale::Uk, Self::ProjectLocal) => { |
| 1474 | "Тримайте контекст, специфічний для проєкту, всередині проєкту; не переносьте чутливі деталі в пам'ять без явного запиту." |
| 1475 | } |
| 1476 | (_, Self::StandardCare) => { |
| 1477 | "Protect secrets, user files, git history, production systems, cost, privacy, and time." |
| 1478 | } |
| 1479 | (_, Self::StrictBoundaries) => { |
| 1480 | "Treat secrets, personal data, credentials, production state, money, and publish actions as stop-and-confirm boundaries." |
| 1481 | } |
| 1482 | (_, Self::ProjectLocal) => { |
| 1483 | "Keep project-specific context local; avoid carrying sensitive details into memory unless explicitly asked." |
| 1484 | } |
| 1485 | } |
| 1486 | } |
| 1487 | |
| 1488 | fn escalation_rule(self, locale: Locale) -> &'static str { |
| 1489 | match (locale, self) { |
| 1490 | (Locale::Ja, Self::StandardCare) => { |
| 1491 | "破壊的、高コスト、認証情報、公開、法務、セキュリティリスクのある操作の前に尋ねる。" |
| 1492 | } |
| 1493 | (Locale::Ja, Self::StrictBoundaries) => { |
| 1494 | "機微情報の読み取りや拡散、本番システム操作、支出、公開の前に停止して尋ねる。" |
| 1495 | } |
| 1496 | (Locale::Ja, Self::ProjectLocal) => { |
| 1497 | "プロジェクト詳細をメモリ、ワークスペース、古い引き継ぎへ持ち出す前に確認する。" |
| 1498 | } |
| 1499 | (Locale::ZhHans, Self::StandardCare) => { |
| 1500 | "遇到破坏性、高成本、凭据、发布、法律或安全风险操作时先询问。" |
| 1501 | } |
| 1502 | (Locale::ZhHans, Self::StrictBoundaries) => { |
| 1503 | "在读取或传播敏感信息、触碰生产系统、花费资金或发布内容前停止并询问。" |
| 1504 | } |
| 1505 | (Locale::ZhHans, Self::ProjectLocal) => { |
| 1506 | "需要跨项目记忆、复制项目细节或引用旧交接时,先确认这些上下文仍适用。" |
| 1507 | } |
| 1508 | (Locale::ZhHant, Self::StandardCare) => { |
| 1509 | "遇到破壞性、高成本、憑據、發布、法律或安全風險操作時先詢問。" |
| 1510 | } |
| 1511 | (Locale::ZhHant, Self::StrictBoundaries) => { |
| 1512 | "在讀取或傳播敏感資訊、觸碰生產系統、花費資金或發布內容前停止並詢問。" |
| 1513 | } |
| 1514 | (Locale::ZhHant, Self::ProjectLocal) => { |
| 1515 | "需要跨專案記憶、複製專案細節或引用舊交接時,先確認這些上下文仍適用。" |
| 1516 | } |
| 1517 | (Locale::PtBr, Self::StandardCare) => { |
| 1518 | "Pergunte antes de ações destrutivas, caras, com credenciais, publicação, risco legal ou de segurança." |
| 1519 | } |
| 1520 | (Locale::PtBr, Self::StrictBoundaries) => { |
| 1521 | "Pare e pergunte antes de ler ou espalhar dados sensíveis, tocar produção, gastar dinheiro ou publicar." |
| 1522 | } |
| 1523 | (Locale::PtBr, Self::ProjectLocal) => { |
| 1524 | "Confirme antes de levar detalhes do projeto para memória, workspaces ou handoffs antigos." |
| 1525 | } |
| 1526 | (Locale::Es419, Self::StandardCare) => { |
| 1527 | "Pregunta antes de acciones destructivas, costosas, con credenciales, publicación o riesgo legal/de seguridad." |
| 1528 | } |
| 1529 | (Locale::Es419, Self::StrictBoundaries) => { |
| 1530 | "Detente y pregunta antes de leer o difundir datos sensibles, tocar producción, gastar dinero o publicar." |
| 1531 | } |
| 1532 | (Locale::Es419, Self::ProjectLocal) => { |
| 1533 | "Confirma antes de llevar detalles del proyecto a memoria, workspaces o handoffs viejos." |
| 1534 | } |
| 1535 | (Locale::Vi, Self::StandardCare) => { |
| 1536 | "Hỏi trước các thao tác phá hủy, tốn kém, liên quan thông tin xác thực, xuất bản, pháp lý hoặc bảo mật." |
| 1537 | } |
| 1538 | (Locale::Vi, Self::StrictBoundaries) => { |
| 1539 | "Dừng và hỏi trước khi đọc/phát tán dữ liệu nhạy cảm, chạm sản xuất, chi tiền hoặc xuất bản." |
| 1540 | } |
| 1541 | (Locale::Vi, Self::ProjectLocal) => { |
| 1542 | "Xác nhận trước khi mang chi tiết dự án sang bộ nhớ, workspace khác hoặc handoff cũ." |
| 1543 | } |
| 1544 | (Locale::Ko, Self::StandardCare) => { |
| 1545 | "파괴적이거나, 비용이 크거나, 자격 증명, 게시, 법적, 보안 위험이 있는 작업 전에 먼저 물어본다." |
| 1546 | } |
| 1547 | (Locale::Ko, Self::StrictBoundaries) => { |
| 1548 | "민감 정보를 읽거나 퍼뜨리기 전, 프로덕션 시스템을 건드리기 전, 자금을 쓰거나 게시하기 전에 멈추고 물어본다." |
| 1549 | } |
| 1550 | (Locale::Ko, Self::ProjectLocal) => { |
| 1551 | "프로젝트 세부 정보를 메모리, 다른 워크스페이스, 오래된 인계 자료로 옮기기 전에 확인한다." |
| 1552 | } |
| 1553 | (Locale::Ca, Self::StandardCare) => { |
| 1554 | "Pregunta abans d'accions destructives, costoses, amb credencials, de publicació o amb risc legal o de seguretat." |
| 1555 | } |
| 1556 | (Locale::Ca, Self::StrictBoundaries) => { |
| 1557 | "Atura't i pregunta abans de llegir o difondre dades sensibles, tocar sistemes de producció, gastar diners o publicar." |
| 1558 | } |
| 1559 | (Locale::Ca, Self::ProjectLocal) => { |
| 1560 | "Confirma abans de portar detalls del projecte a la memòria, a altres espais de treball o a traspasos antics." |
| 1561 | } |
| 1562 | (Locale::De, Self::StandardCare) => { |
| 1563 | "Frage vor destruktiven, kostspieligen, zugangsdatenbezogenen, veröffentlichenden, rechtlichen oder sicherheitskritischen Aktionen." |
| 1564 | } |
| 1565 | (Locale::De, Self::StrictBoundaries) => { |
| 1566 | "Halte an und frage, bevor du sensible Daten liest oder verbreitest, Produktionssysteme anfasst, Geld ausgibst oder veröffentlichst." |
| 1567 | } |
| 1568 | (Locale::De, Self::ProjectLocal) => { |
| 1569 | "Bestätige, bevor du Projektdetails in Speicher, Workspaces oder veraltete Übergaben überträgst." |
| 1570 | } |
| 1571 | (Locale::Fr, Self::StandardCare) => { |
| 1572 | "Demandez avant toute action destructive, coûteuse, impliquant des identifiants, une publication, ou un risque juridique ou de sécurité." |
| 1573 | } |
| 1574 | (Locale::Fr, Self::StrictBoundaries) => { |
| 1575 | "Arrêtez et demandez avant de lire ou diffuser des données sensibles, de toucher aux systèmes de production, de dépenser de l'argent ou de publier." |
| 1576 | } |
| 1577 | (Locale::Fr, Self::ProjectLocal) => { |
| 1578 | "Confirmez avant de transporter des détails du projet vers la mémoire, d'autres espaces de travail ou d'anciens transferts." |
| 1579 | } |
| 1580 | (Locale::Id, Self::StandardCare) => { |
| 1581 | "Tanya sebelum tindakan destruktif, mahal, terkait kredensial, publikasi, hukum, atau berisiko keamanan." |
| 1582 | } |
| 1583 | (Locale::Id, Self::StrictBoundaries) => { |
| 1584 | "Berhenti dan tanya sebelum membaca atau menyebarkan data sensitif, menyentuh sistem produksi, membelanjakan uang, atau mempublikasikan." |
| 1585 | } |
| 1586 | (Locale::Id, Self::ProjectLocal) => { |
| 1587 | "Konfirmasi sebelum membawa detail proyek ke memori, workspace lain, atau handoff lama." |
| 1588 | } |
| 1589 | (Locale::Hi, Self::StandardCare) => { |
| 1590 | "विनाशकारी, उच्च-लागत, क्रेडेंशियल, प्रकाशन, कानूनी या सुरक्षा-जोखिम कार्यों से पहले पूछें।" |
| 1591 | } |
| 1592 | (Locale::Hi, Self::StrictBoundaries) => { |
| 1593 | "संवेदनशील डेटा पढ़ने या फैलाने, प्रोडक्शन सिस्टम छूने, धन खर्च करने या प्रकाशित करने से पहले रुककर पूछें।" |
| 1594 | } |
| 1595 | (Locale::Hi, Self::ProjectLocal) => { |
| 1596 | "प्रोजेक्ट विवरण मेमोरी, अन्य कार्यक्षेत्रों या पुराने हैंडऑफ़ में ले जाने से पहले पुष्टि करें।" |
| 1597 | } |
| 1598 | (Locale::Ru, Self::StandardCare) => { |
| 1599 | "Спрашивайте перед деструктивными, дорогими, связанными с учётными данными, публикацией, юридическими или угрожающими безопасности действиями." |
| 1600 | } |
| 1601 | (Locale::Ru, Self::StrictBoundaries) => { |
| 1602 | "Остановитесь и спросите, прежде чем читать или распространять чувствительные данные, трогать production-системы, тратить деньги или публиковать." |
| 1603 | } |
| 1604 | (Locale::Ru, Self::ProjectLocal) => { |
| 1605 | "Подтвердите, прежде чем переносить детали проекта в память, другие рабочие области или устаревшие передаточные заметки." |
| 1606 | } |
| 1607 | (Locale::Uk, Self::StandardCare) => { |
| 1608 | "Питайте перед руйнівними, дорогими, пов'язаними з обліковими даними, публікацією, юридичними чи небезпечними для безпеки діями." |
| 1609 | } |
| 1610 | (Locale::Uk, Self::StrictBoundaries) => { |
| 1611 | "Зупиніться й запитайте, перш ніж читати чи поширювати чутливі дані, чіпати production-системи, витрачати гроші або публікувати." |
| 1612 | } |
| 1613 | (Locale::Uk, Self::ProjectLocal) => { |
| 1614 | "Підтвердьте, перш ніж переносити деталі проєкту в пам'ять, інші робочі простори чи застарілі передаточні нотатки." |
| 1615 | } |
| 1616 | (_, Self::StandardCare) => { |
| 1617 | "Ask before destructive, high-cost, credential, publishing, legal, or security-risk actions." |
| 1618 | } |
| 1619 | (_, Self::StrictBoundaries) => { |
| 1620 | "Stop and ask before reading or spreading sensitive data, touching production systems, spending money, or publishing." |
| 1621 | } |
| 1622 | (_, Self::ProjectLocal) => { |
| 1623 | "Confirm before carrying project details across memory, workspaces, or stale handoffs." |
| 1624 | } |
| 1625 | } |
| 1626 | } |
| 1627 | } |
| 1628 | |
| 1629 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1630 | enum GuidedPrinciples { |
| 1631 | ScopedChanges, |
| 1632 | UserVoice, |
| 1633 | ReversibleOps, |
| 1634 | } |
| 1635 | |
| 1636 | impl GuidedPrinciples { |
| 1637 | fn next(self) -> Self { |
| 1638 | match self { |
| 1639 | Self::ScopedChanges => Self::UserVoice, |
| 1640 | Self::UserVoice => Self::ReversibleOps, |
| 1641 | Self::ReversibleOps => Self::ScopedChanges, |
| 1642 | } |
| 1643 | } |
| 1644 | |
| 1645 | fn label(self, locale: Locale) -> &'static str { |
| 1646 | match (locale, self) { |
| 1647 | (Locale::Ja, Self::ScopedChanges) => "小さく絞った変更", |
| 1648 | (Locale::Ja, Self::UserVoice) => "ユーザーの声を保つ", |
| 1649 | (Locale::Ja, Self::ReversibleOps) => "可逆手順", |
| 1650 | (Locale::ZhHans, Self::ScopedChanges) => "小范围改动", |
| 1651 | (Locale::ZhHans, Self::UserVoice) => "保留用户语气", |
| 1652 | (Locale::ZhHans, Self::ReversibleOps) => "可逆步骤", |
| 1653 | (Locale::ZhHant, Self::ScopedChanges) => "小範圍改動", |
| 1654 | (Locale::ZhHant, Self::UserVoice) => "保留使用者語氣", |
| 1655 | (Locale::ZhHant, Self::ReversibleOps) => "可逆步驟", |
| 1656 | (Locale::PtBr, Self::ScopedChanges) => "mudanças focadas", |
| 1657 | (Locale::PtBr, Self::UserVoice) => "preservar voz do usuário", |
| 1658 | (Locale::PtBr, Self::ReversibleOps) => "passos reversíveis", |
| 1659 | (Locale::Es419, Self::ScopedChanges) => "cambios acotados", |
| 1660 | (Locale::Es419, Self::UserVoice) => "preservar voz del usuario", |
| 1661 | (Locale::Es419, Self::ReversibleOps) => "pasos reversibles", |
| 1662 | (Locale::Vi, Self::ScopedChanges) => "thay đổi có phạm vi", |
| 1663 | (Locale::Vi, Self::UserVoice) => "giữ giọng người dùng", |
| 1664 | (Locale::Vi, Self::ReversibleOps) => "bước có thể đảo ngược", |
| 1665 | (Locale::Ko, Self::ScopedChanges) => "범위가 명확한 변경", |
| 1666 | (Locale::Ko, Self::UserVoice) => "사용자의 어조 유지", |
| 1667 | (Locale::Ko, Self::ReversibleOps) => "되돌릴 수 있는 단계", |
| 1668 | (Locale::Ca, Self::ScopedChanges) => "canvis acotats", |
| 1669 | (Locale::Ca, Self::UserVoice) => "veu de l'usuari", |
| 1670 | (Locale::Ca, Self::ReversibleOps) => "passos reversibles", |
| 1671 | (Locale::De, Self::ScopedChanges) => "begrenzte Änderungen", |
| 1672 | (Locale::De, Self::UserVoice) => "Stimme des Nutzers", |
| 1673 | (Locale::De, Self::ReversibleOps) => "reversible Schritte", |
| 1674 | (Locale::Fr, Self::ScopedChanges) => "changements ciblés", |
| 1675 | (Locale::Fr, Self::UserVoice) => "voix de l'utilisateur", |
| 1676 | (Locale::Fr, Self::ReversibleOps) => "étapes réversibles", |
| 1677 | (Locale::Id, Self::ScopedChanges) => "perubahan terbatas", |
| 1678 | (Locale::Id, Self::UserVoice) => "suara pengguna", |
| 1679 | (Locale::Id, Self::ReversibleOps) => "langkah reversibel", |
| 1680 | (Locale::Hi, Self::ScopedChanges) => "सीमित बदलाव", |
| 1681 | (Locale::Hi, Self::UserVoice) => "उपयोगकर्ता की आवाज़", |
| 1682 | (Locale::Hi, Self::ReversibleOps) => "उत्क्रमणीय चरण", |
| 1683 | (Locale::Ru, Self::ScopedChanges) => "ограниченные изменения", |
| 1684 | (Locale::Ru, Self::UserVoice) => "голос пользователя", |
| 1685 | (Locale::Ru, Self::ReversibleOps) => "обратимые шаги", |
| 1686 | (Locale::Uk, Self::ScopedChanges) => "обмежені зміни", |
| 1687 | (Locale::Uk, Self::UserVoice) => "голос користувача", |
| 1688 | (Locale::Uk, Self::ReversibleOps) => "оборотні кроки", |
| 1689 | (_, Self::ScopedChanges) => "scoped changes", |
| 1690 | (_, Self::UserVoice) => "user voice", |
| 1691 | (_, Self::ReversibleOps) => "reversible steps", |
| 1692 | } |
| 1693 | } |
| 1694 | |
| 1695 | fn note(self, locale: Locale) -> &'static str { |
| 1696 | match (locale, self) { |
| 1697 | (Locale::Ja, Self::ScopedChanges) => { |
| 1698 | "自由原則:小さくレビューしやすい変更を優先し、明示要求がない限り無関係なリファクタを避ける。" |
| 1699 | } |
| 1700 | (Locale::Ja, Self::UserVoice) => { |
| 1701 | "自由原則:ユーザーの語調、ブランド、制約を保ち、好みを権限拡大として扱わない。" |
| 1702 | } |
| 1703 | (Locale::Ja, Self::ReversibleOps) => { |
| 1704 | "自由原則:影響の大きい操作の前に、可逆手順、チェックポイント、ロールバック説明を選ぶ。" |
| 1705 | } |
| 1706 | (Locale::ZhHans, Self::ScopedChanges) => { |
| 1707 | "自定义准则:优先采用小范围、可审查的改动;除非明确要求,不做无关重构。" |
| 1708 | } |
| 1709 | (Locale::ZhHans, Self::UserVoice) => { |
| 1710 | "自定义准则:保留用户的语气、品牌和约束;不把偏好推断成权限扩大。" |
| 1711 | } |
| 1712 | (Locale::ZhHans, Self::ReversibleOps) => { |
| 1713 | "自定义准则:先选择可逆步骤、检查点和回滚说明,再进行高影响操作。" |
| 1714 | } |
| 1715 | (Locale::ZhHant, Self::ScopedChanges) => { |
| 1716 | "自由原則:優先採用小範圍、可審查的改動;除非明確要求,不做無關重構。" |
| 1717 | } |
| 1718 | (Locale::ZhHant, Self::UserVoice) => { |
| 1719 | "自由原則:保留使用者的語氣、品牌和約束;不把偏好推斷成權限擴大。" |
| 1720 | } |
| 1721 | (Locale::ZhHant, Self::ReversibleOps) => { |
| 1722 | "自由原則:先選擇可逆步驟、檢查點和回復說明,再進行高影響操作。" |
| 1723 | } |
| 1724 | (Locale::PtBr, Self::ScopedChanges) => { |
| 1725 | "Princípio livre: prefira mudanças pequenas e revisáveis; evite refactors não relacionados sem pedido explícito." |
| 1726 | } |
| 1727 | (Locale::PtBr, Self::UserVoice) => { |
| 1728 | "Princípio livre: preserve a voz, marca e restrições do usuário sem tratar preferências como expansão de permissão." |
| 1729 | } |
| 1730 | (Locale::PtBr, Self::ReversibleOps) => { |
| 1731 | "Princípio livre: favoreça passos reversíveis, checkpoints e notas de rollback antes de ações de alto impacto." |
| 1732 | } |
| 1733 | (Locale::Es419, Self::ScopedChanges) => { |
| 1734 | "Principio libre: prefiere cambios pequeños y revisables; evita refactors no relacionados sin pedido explícito." |
| 1735 | } |
| 1736 | (Locale::Es419, Self::UserVoice) => { |
| 1737 | "Principio libre: preserva la voz, marca y restricciones del usuario sin tratar preferencias como expansión de permisos." |
| 1738 | } |
| 1739 | (Locale::Es419, Self::ReversibleOps) => { |
| 1740 | "Principio libre: favorece pasos reversibles, checkpoints y notas de rollback antes de acciones de alto impacto." |
| 1741 | } |
| 1742 | (Locale::Vi, Self::ScopedChanges) => { |
| 1743 | "Nguyên tắc tự do: ưu tiên thay đổi nhỏ, dễ review; tránh refactor không liên quan nếu không được yêu cầu rõ." |
| 1744 | } |
| 1745 | (Locale::Vi, Self::UserVoice) => { |
| 1746 | "Nguyên tắc tự do: giữ giọng, thương hiệu và ràng buộc của người dùng, không xem sở thích là mở rộng quyền." |
| 1747 | } |
| 1748 | (Locale::Vi, Self::ReversibleOps) => { |
| 1749 | "Nguyên tắc tự do: ưu tiên bước có thể đảo ngược, checkpoint và ghi chú rollback trước thao tác tác động cao." |
| 1750 | } |
| 1751 | (Locale::Ko, Self::ScopedChanges) => { |
| 1752 | "자유 원칙: 작고 리뷰하기 쉬운 변경을 우선하고, 명시적으로 요청받지 않는 한 관련 없는 리팩터링은 하지 않는다." |
| 1753 | } |
| 1754 | (Locale::Ko, Self::UserVoice) => { |
| 1755 | "자유 원칙: 사용자의 어조, 브랜드, 제약을 유지하고 선호를 권한 확대로 취급하지 않는다." |
| 1756 | } |
| 1757 | (Locale::Ko, Self::ReversibleOps) => { |
| 1758 | "자유 원칙: 영향이 큰 작업 전에 되돌릴 수 있는 단계, 체크포인트, 롤백 메모를 우선한다." |
| 1759 | } |
| 1760 | (Locale::Ca, Self::ScopedChanges) => { |
| 1761 | "Principi lliure: prefereix canvis petits i revisables i evita refactors no relacionats si no es demanen explícitament." |
| 1762 | } |
| 1763 | (Locale::Ca, Self::UserVoice) => { |
| 1764 | "Principi lliure: preserva la veu, la marca i les restriccions de l'usuari sense tractar les preferències com una ampliació de permisos." |
| 1765 | } |
| 1766 | (Locale::Ca, Self::ReversibleOps) => { |
| 1767 | "Principi lliure: priorita passos reversibles, punts de control i notes de marxa enrere abans d'operacions d'alt impacte." |
| 1768 | } |
| 1769 | (Locale::De, Self::ScopedChanges) => { |
| 1770 | "Freitext-Prinzip: Bevorzuge kleine, überprüfbare Änderungen und vermeide unzusammenhängende Refactorings, sofern nicht ausdrücklich gewünscht." |
| 1771 | } |
| 1772 | (Locale::De, Self::UserVoice) => { |
| 1773 | "Freitext-Prinzip: Bewahre Stimme, Marke und Vorgaben des Nutzers, ohne Präferenzen als Rechteausweitung zu behandeln." |
| 1774 | } |
| 1775 | (Locale::De, Self::ReversibleOps) => { |
| 1776 | "Freitext-Prinzip: Bevorzuge reversible Schritte, Checkpoints und Rollback-Notizen vor einschneidenden Operationen." |
| 1777 | } |
| 1778 | (Locale::Fr, Self::ScopedChanges) => { |
| 1779 | "Principe libre : préférez des changements petits et révisables et évitez les refactors sans rapport, sauf demande explicite." |
| 1780 | } |
| 1781 | (Locale::Fr, Self::UserVoice) => { |
| 1782 | "Principe libre : préservez la voix, la marque et les contraintes de l'utilisateur sans traiter ses préférences comme une extension de permissions." |
| 1783 | } |
| 1784 | (Locale::Fr, Self::ReversibleOps) => { |
| 1785 | "Principe libre : privilégiez étapes réversibles, points de contrôle et notes de rollback avant les opérations à fort impact." |
| 1786 | } |
| 1787 | (Locale::Id, Self::ScopedChanges) => { |
| 1788 | "Prinsip bebas: utamakan perubahan kecil yang mudah ditinjau dan hindari refactor tak terkait kecuali diminta secara eksplisit." |
| 1789 | } |
| 1790 | (Locale::Id, Self::UserVoice) => { |
| 1791 | "Prinsip bebas: jaga suara, merek, dan batasan pengguna tanpa memperlakukan preferensi sebagai perluasan izin." |
| 1792 | } |
| 1793 | (Locale::Id, Self::ReversibleOps) => { |
| 1794 | "Prinsip bebas: utamakan langkah reversibel, checkpoint, dan catatan rollback sebelum operasi berdampak besar." |
| 1795 | } |
| 1796 | (Locale::Hi, Self::ScopedChanges) => { |
| 1797 | "मुक्त-पाठ सिद्धांत: छोटे, समीक्षायोग्य बदलावों को प्राथमिकता दें और स्पष्ट अनुरोध के बिना असंबंधित रिफैक्टर से बचें।" |
| 1798 | } |
| 1799 | (Locale::Hi, Self::UserVoice) => { |
| 1800 | "मुक्त-पाठ सिद्धांत: उपयोगकर्ता की आवाज़, ब्रांड और बाधाएँ सुरक्षित रखें; प्राथमिकताओं को अनुमति-विस्तार न मानें।" |
| 1801 | } |
| 1802 | (Locale::Hi, Self::ReversibleOps) => { |
| 1803 | "मुक्त-पाठ सिद्धांत: उच्च-प्रभाव कार्यों से पहले उत्क्रमणीय चरणों, चेकपॉइंट और रोलबैक नोट्स को प्राथमिकता दें।" |
| 1804 | } |
| 1805 | (Locale::Ru, Self::ScopedChanges) => { |
| 1806 | "Свободный принцип: предпочитайте небольшие, проверяемые изменения и избегайте несвязанных рефакторингов без явного запроса." |
| 1807 | } |
| 1808 | (Locale::Ru, Self::UserVoice) => { |
| 1809 | "Свободный принцип: сохраняйте голос, бренд и ограничения пользователя, не трактуя предпочтения как расширение полномочий." |
| 1810 | } |
| 1811 | (Locale::Ru, Self::ReversibleOps) => { |
| 1812 | "Свободный принцип: отдавайте предпочтение обратимым шагам, контрольным точкам и заметкам об откате перед высокорисковыми операциями." |
| 1813 | } |
| 1814 | (Locale::Uk, Self::ScopedChanges) => { |
| 1815 | "Вільний принцип: надавайте перевагу невеликим, перевірюваним змінам і уникайте непов'язаних рефакторингів без явного запиту." |
| 1816 | } |
| 1817 | (Locale::Uk, Self::UserVoice) => { |
| 1818 | "Вільний принцип: зберігайте голос, бренд і обмеження користувача, не трактуючи вподобання як розширення повноважень." |
| 1819 | } |
| 1820 | (Locale::Uk, Self::ReversibleOps) => { |
| 1821 | "Вільний принцип: надавайте перевагу оборотним крокам, контрольним точкам і нотаткам про відкат перед високоризиковими операціями." |
| 1822 | } |
| 1823 | (_, Self::ScopedChanges) => { |
| 1824 | "Freeform principle: prefer small, reviewable changes and avoid unrelated refactors unless explicitly requested." |
| 1825 | } |
| 1826 | (_, Self::UserVoice) => { |
| 1827 | "Freeform principle: preserve the user's voice, brand, and constraints without treating preferences as permission expansion." |
| 1828 | } |
| 1829 | (_, Self::ReversibleOps) => { |
| 1830 | "Freeform principle: favor reversible steps, checkpoints, and rollback notes before high-impact operations." |
| 1831 | } |
| 1832 | } |
| 1833 | } |
| 1834 | } |
| 1835 | |
| 1836 | fn next_guided_autonomy(preference: AutonomyPreference) -> AutonomyPreference { |
| 1837 | match preference { |
| 1838 | AutonomyPreference::Unspecified | AutonomyPreference::Cautious => { |
| 1839 | AutonomyPreference::Balanced |
| 1840 | } |
| 1841 | AutonomyPreference::Balanced => AutonomyPreference::Autonomous, |
| 1842 | AutonomyPreference::Autonomous => AutonomyPreference::Cautious, |
| 1843 | } |
| 1844 | } |
| 1845 | |
| 1846 | fn autonomy_label(preference: AutonomyPreference, locale: Locale) -> &'static str { |
| 1847 | match (locale, preference) { |
| 1848 | (Locale::Ja, AutonomyPreference::Cautious) => "慎重", |
| 1849 | (Locale::Ja, AutonomyPreference::Balanced) => "バランス", |
| 1850 | (Locale::Ja, AutonomyPreference::Autonomous) => "積極的", |
| 1851 | (Locale::ZhHans, AutonomyPreference::Cautious) => "谨慎", |
| 1852 | (Locale::ZhHans, AutonomyPreference::Balanced) => "平衡", |
| 1853 | (Locale::ZhHans, AutonomyPreference::Autonomous) => "积极主动", |
| 1854 | (Locale::ZhHant, AutonomyPreference::Cautious) => "謹慎", |
| 1855 | (Locale::ZhHant, AutonomyPreference::Balanced) => "平衡", |
| 1856 | (Locale::ZhHant, AutonomyPreference::Autonomous) => "積極主動", |
| 1857 | (Locale::PtBr, AutonomyPreference::Cautious) => "cauteloso", |
| 1858 | (Locale::PtBr, AutonomyPreference::Balanced) => "equilibrado", |
| 1859 | (Locale::PtBr, AutonomyPreference::Autonomous) => "ambicioso", |
| 1860 | (Locale::Es419, AutonomyPreference::Cautious) => "cauteloso", |
| 1861 | (Locale::Es419, AutonomyPreference::Balanced) => "equilibrado", |
| 1862 | (Locale::Es419, AutonomyPreference::Autonomous) => "ambicioso", |
| 1863 | (Locale::Vi, AutonomyPreference::Cautious) => "thận trọng", |
| 1864 | (Locale::Vi, AutonomyPreference::Balanced) => "cân bằng", |
| 1865 | (Locale::Vi, AutonomyPreference::Autonomous) => "chủ động", |
| 1866 | (Locale::Ko, AutonomyPreference::Cautious) => "신중함", |
| 1867 | (Locale::Ko, AutonomyPreference::Balanced) => "균형", |
| 1868 | (Locale::Ko, AutonomyPreference::Autonomous) => "적극적", |
| 1869 | (Locale::Ca, AutonomyPreference::Cautious) => "cautelós", |
| 1870 | (Locale::Ca, AutonomyPreference::Balanced) => "equilibrat", |
| 1871 | (Locale::Ca, AutonomyPreference::Autonomous) => "ambiciós", |
| 1872 | (Locale::De, AutonomyPreference::Cautious) => "vorsichtig", |
| 1873 | (Locale::De, AutonomyPreference::Balanced) => "ausgewogen", |
| 1874 | (Locale::De, AutonomyPreference::Autonomous) => "ambitioniert", |
| 1875 | (Locale::Fr, AutonomyPreference::Cautious) => "prudent", |
| 1876 | (Locale::Fr, AutonomyPreference::Balanced) => "équilibré", |
| 1877 | (Locale::Fr, AutonomyPreference::Autonomous) => "ambitieux", |
| 1878 | (Locale::Id, AutonomyPreference::Cautious) => "hati-hati", |
| 1879 | (Locale::Id, AutonomyPreference::Balanced) => "seimbang", |
| 1880 | (Locale::Id, AutonomyPreference::Autonomous) => "ambisius", |
| 1881 | (Locale::Hi, AutonomyPreference::Cautious) => "सावधान", |
| 1882 | (Locale::Hi, AutonomyPreference::Balanced) => "संतुलित", |
| 1883 | (Locale::Hi, AutonomyPreference::Autonomous) => "महत्वाकांक्षी", |
| 1884 | (Locale::Ru, AutonomyPreference::Cautious) => "осторожный", |
| 1885 | (Locale::Ru, AutonomyPreference::Balanced) => "сбалансированный", |
| 1886 | (Locale::Ru, AutonomyPreference::Autonomous) => "самостоятельный", |
| 1887 | (Locale::Uk, AutonomyPreference::Cautious) => "обережний", |
| 1888 | (Locale::Uk, AutonomyPreference::Balanced) => "збалансований", |
| 1889 | (Locale::Uk, AutonomyPreference::Autonomous) => "самостійний", |
| 1890 | (_, AutonomyPreference::Cautious) => "cautious", |
| 1891 | (_, AutonomyPreference::Balanced) => "balanced", |
| 1892 | (_, AutonomyPreference::Autonomous) => "ambitious", |
| 1893 | (_, AutonomyPreference::Unspecified) => "unspecified", |
| 1894 | } |
| 1895 | } |
| 1896 | |
| 1897 | fn autonomy_priority(preference: AutonomyPreference, locale: Locale) -> &'static str { |
| 1898 | match (locale, preference) { |
| 1899 | (Locale::Ja, AutonomyPreference::Cautious) => { |
| 1900 | "ファイル編集、コマンド実行、あいまいな製品判断の前に停止して尋ねる。" |
| 1901 | } |
| 1902 | (Locale::Ja, AutonomyPreference::Balanced) => { |
| 1903 | "明確で低リスクな作業は直接進め、危険、破壊的、あいまいな操作では先に確認する。" |
| 1904 | } |
| 1905 | (Locale::Ja, AutonomyPreference::Autonomous) => { |
| 1906 | "安全な定型作業はまとめて進めるが、破壊的、認証情報、公開、高コスト、法務、セキュリティリスクでは停止して尋ねる。" |
| 1907 | } |
| 1908 | (Locale::ZhHans, AutonomyPreference::Cautious) => { |
| 1909 | "在编辑文件、运行命令或产品选择不明确前,倾向先停下询问。" |
| 1910 | } |
| 1911 | (Locale::ZhHans, AutonomyPreference::Balanced) => { |
| 1912 | "清晰低风险任务可直接行动;遇到风险、破坏性或歧义时先确认。" |
| 1913 | } |
| 1914 | (Locale::ZhHans, AutonomyPreference::Autonomous) => { |
| 1915 | "可批量处理安全的常规工作,但遇到破坏性、凭据、发布、高成本、法律或安全风险时停止询问。" |
| 1916 | } |
| 1917 | (Locale::ZhHant, AutonomyPreference::Cautious) => { |
| 1918 | "在編輯檔案、執行命令或產品選擇不明確前,傾向先停下詢問。" |
| 1919 | } |
| 1920 | (Locale::ZhHant, AutonomyPreference::Balanced) => { |
| 1921 | "清晰低風險任務可直接行動;遇到風險、破壞性或歧義時先確認。" |
| 1922 | } |
| 1923 | (Locale::ZhHant, AutonomyPreference::Autonomous) => { |
| 1924 | "可批量處理安全的常規工作,但遇到破壞性、憑據、發布、高成本、法律或安全風險時停止詢問。" |
| 1925 | } |
| 1926 | (Locale::PtBr, AutonomyPreference::Cautious) => { |
| 1927 | "Pare e pergunte antes de editar arquivos, rodar comandos ou escolher entre caminhos ambíguos de produto." |
| 1928 | } |
| 1929 | (Locale::PtBr, AutonomyPreference::Balanced) => { |
| 1930 | "Aja diretamente em tarefas claras e de baixo risco; confirme antes de ações arriscadas, destrutivas ou ambíguas." |
| 1931 | } |
| 1932 | (Locale::PtBr, AutonomyPreference::Autonomous) => { |
| 1933 | "Agrupe trabalho seguro de rotina, mas pare para ações destrutivas, credenciais, publicação, alto custo, legais ou de segurança." |
| 1934 | } |
| 1935 | (Locale::Es419, AutonomyPreference::Cautious) => { |
| 1936 | "Detente y pregunta antes de editar archivos, ejecutar comandos o elegir entre caminos ambiguos de producto." |
| 1937 | } |
| 1938 | (Locale::Es419, AutonomyPreference::Balanced) => { |
| 1939 | "Actúa directamente en tareas claras y de bajo riesgo; confirma antes de acciones riesgosas, destructivas o ambiguas." |
| 1940 | } |
| 1941 | (Locale::Es419, AutonomyPreference::Autonomous) => { |
| 1942 | "Agrupa trabajo seguro de rutina, pero detente ante acciones destructivas, credenciales, publicación, alto costo, legales o de seguridad." |
| 1943 | } |
| 1944 | (Locale::Vi, AutonomyPreference::Cautious) => { |
| 1945 | "Dừng và hỏi trước khi sửa tệp, chạy lệnh hoặc chọn giữa đường sản phẩm mơ hồ." |
| 1946 | } |
| 1947 | (Locale::Vi, AutonomyPreference::Balanced) => { |
| 1948 | "Hành động trực tiếp với việc rõ, rủi ro thấp; xác nhận trước việc rủi ro, phá hủy hoặc mơ hồ." |
| 1949 | } |
| 1950 | (Locale::Vi, AutonomyPreference::Autonomous) => { |
| 1951 | "Gộp việc thường lệ an toàn, nhưng dừng với thao tác phá hủy, thông tin xác thực, xuất bản, chi phí cao, pháp lý hoặc bảo mật." |
| 1952 | } |
| 1953 | (Locale::Ko, AutonomyPreference::Cautious) => { |
| 1954 | "파일 수정, 명령어 실행, 애매한 제품 선택 전에 멈추고 물어본다." |
| 1955 | } |
| 1956 | (Locale::Ko, AutonomyPreference::Balanced) => { |
| 1957 | "명확하고 위험이 낮은 작업은 바로 진행하고, 위험하거나 파괴적이거나 애매한 작업은 먼저 확인한다." |
| 1958 | } |
| 1959 | (Locale::Ko, AutonomyPreference::Autonomous) => { |
| 1960 | "안전한 정형 작업은 모아서 진행하되, 파괴적이거나 자격 증명, 게시, 고비용, 법적, 보안 위험이 있는 작업에서는 멈추고 물어본다." |
| 1961 | } |
| 1962 | (Locale::Ca, AutonomyPreference::Cautious) => { |
| 1963 | "Atura't i pregunta abans d'editar fitxers, executar ordres o triar entre camins de producte ambigus." |
| 1964 | } |
| 1965 | (Locale::Ca, AutonomyPreference::Balanced) => { |
| 1966 | "Actua directament en tasques clares i de baix risc; confirma abans d'accions arriscades, destructives o ambigües." |
| 1967 | } |
| 1968 | (Locale::Ca, AutonomyPreference::Autonomous) => { |
| 1969 | "Agrupa la feina rutinària segura, però atura't davant accions destructives, amb credencials, de publicació, d'alt cost o amb risc legal o de seguretat." |
| 1970 | } |
| 1971 | (Locale::De, AutonomyPreference::Cautious) => { |
| 1972 | "Halte an und frage, bevor du Dateien bearbeitest, Befehle ausführst oder zwischen mehrdeutigen Produktwegen wählst." |
| 1973 | } |
| 1974 | (Locale::De, AutonomyPreference::Balanced) => { |
| 1975 | "Handle direkt bei klaren, risikoarmen Aufgaben; bestätige vor riskanten, destruktiven oder mehrdeutigen Aktionen." |
| 1976 | } |
| 1977 | (Locale::De, AutonomyPreference::Autonomous) => { |
| 1978 | "Bündle sichere Routinearbeit, aber halte an bei destruktiven, zugangsdatenbezogenen, veröffentlichenden, kostspieligen, rechtlichen oder sicherheitskritischen Aktionen." |
| 1979 | } |
| 1980 | (Locale::Fr, AutonomyPreference::Cautious) => { |
| 1981 | "Arrêtez et demandez avant de modifier des fichiers, d'exécuter des commandes ou de choisir entre des voies produit ambiguës." |
| 1982 | } |
| 1983 | (Locale::Fr, AutonomyPreference::Balanced) => { |
| 1984 | "Agissez directement sur les tâches claires et à faible risque ; confirmez avant les actions risquées, destructives ou ambiguës." |
| 1985 | } |
| 1986 | (Locale::Fr, AutonomyPreference::Autonomous) => { |
| 1987 | "Regroupez le travail de routine sûr, mais arrêtez devant les actions destructives, impliquant des identifiants, des publications, coûteuses, juridiques ou à risque de sécurité." |
| 1988 | } |
| 1989 | (Locale::Id, AutonomyPreference::Cautious) => { |
| 1990 | "Berhenti dan tanya sebelum mengedit file, menjalankan perintah, atau memilih di antara jalur produk yang ambigu." |
| 1991 | } |
| 1992 | (Locale::Id, AutonomyPreference::Balanced) => { |
| 1993 | "Bertindak langsung pada tugas yang jelas dan berisiko rendah; konfirmasi sebelum tindakan berisiko, destruktif, atau ambigu." |
| 1994 | } |
| 1995 | (Locale::Id, AutonomyPreference::Autonomous) => { |
| 1996 | "Kelompokkan pekerjaan rutin yang aman, tetapi berhenti untuk tindakan destruktif, terkait kredensial, publikasi, mahal, hukum, atau berisiko keamanan." |
| 1997 | } |
| 1998 | (Locale::Hi, AutonomyPreference::Cautious) => { |
| 1999 | "फ़ाइलें संपादित करने, कमांड चलाने या अस्पष्ट उत्पाद मार्गों में चुनने से पहले रुककर पूछें।" |
| 2000 | } |
| 2001 | (Locale::Hi, AutonomyPreference::Balanced) => { |
| 2002 | "स्पष्ट, कम-जोखिम वाले कार्यों पर सीधे कार्य करें; जोखिमपूर्ण, विनाशकारी या अस्पष्ट कार्यों से पहले पुष्टि करें।" |
| 2003 | } |
| 2004 | (Locale::Hi, AutonomyPreference::Autonomous) => { |
| 2005 | "सुरक्षित नियमित काम एक साथ करें, लेकिन विनाशकारी, क्रेडेंशियल, प्रकाशन, उच्च-लागत, कानूनी या सुरक्षा-जोखिम कार्यों पर रुककर पूछें।" |
| 2006 | } |
| 2007 | (Locale::Ru, AutonomyPreference::Cautious) => { |
| 2008 | "Остановитесь и спросите перед редактированием файлов, запуском команд или выбором между неоднозначными продуктовыми путями." |
| 2009 | } |
| 2010 | (Locale::Ru, AutonomyPreference::Balanced) => { |
| 2011 | "Действуйте напрямую в ясных низкорисковых задачах; подтверждайте перед рискованными, деструктивными или неоднозначными действиями." |
| 2012 | } |
| 2013 | (Locale::Ru, AutonomyPreference::Autonomous) => { |
| 2014 | "Группируйте безопасную рутинную работу, но останавливайтесь перед деструктивными действиями, действиями с учётными данными, публикациями, дорогими, юридическими или угрожающими безопасности операциями." |
| 2015 | } |
| 2016 | (Locale::Uk, AutonomyPreference::Cautious) => { |
| 2017 | "Зупиніться й запитайте перед редагуванням файлів, запуском команд або вибором між неоднозначними продуктовими шляхами." |
| 2018 | } |
| 2019 | (Locale::Uk, AutonomyPreference::Balanced) => { |
| 2020 | "Дійте безпосередньо в чітких низькоризикових завданнях; підтверджуйте перед ризикованими, руйнівними чи неоднозначними діями." |
| 2021 | } |
| 2022 | (Locale::Uk, AutonomyPreference::Autonomous) => { |
| 2023 | "Групуйте безпечну рутинну роботу, але зупиняйтеся перед руйнівними діями, діями з обліковими даними, публікаціями, дорогими, юридичними чи небезпечними для безпеки операціями." |
| 2024 | } |
| 2025 | (_, AutonomyPreference::Cautious) => { |
| 2026 | "Stop and ask before editing files, running commands, or choosing between ambiguous product paths." |
| 2027 | } |
| 2028 | (_, AutonomyPreference::Balanced) => { |
| 2029 | "Act directly on clear low-risk tasks; confirm before risky, destructive, or ambiguous actions." |
| 2030 | } |
| 2031 | (_, AutonomyPreference::Autonomous) => { |
| 2032 | "Batch routine safe work, then stop for destructive, credential, publishing, high-cost, legal, or security-risk actions." |
| 2033 | } |
| 2034 | (_, AutonomyPreference::Unspecified) => "No standing initiative preference was selected.", |
| 2035 | } |
| 2036 | } |
| 2037 | |
| 2038 | fn authority_priority(locale: Locale) -> &'static str { |
| 2039 | match locale { |
| 2040 | Locale::Ja => { |
| 2041 | "現在のユーザー要求とライブツール証拠は、メモリ、古い引き継ぎ、推測より優先される。" |
| 2042 | } |
| 2043 | Locale::ZhHans => "当前用户请求和实时工具证据优先于记忆、陈旧交接和猜测。", |
| 2044 | Locale::ZhHant => "目前使用者請求和即時工具證據優先於記憶、陳舊交接和猜測。", |
| 2045 | Locale::PtBr => { |
| 2046 | "Pedidos atuais do usuário e evidência viva das ferramentas superam memória, handoffs antigos e palpites." |
| 2047 | } |
| 2048 | Locale::Es419 => { |
| 2049 | "Las solicitudes actuales del usuario y la evidencia viva de herramientas superan memoria, handoffs viejos y suposiciones." |
| 2050 | } |
| 2051 | Locale::Vi => { |
| 2052 | "Yêu cầu hiện tại của người dùng và bằng chứng trực tiếp từ công cụ ưu tiên hơn bộ nhớ, handoff cũ và phỏng đoán." |
| 2053 | } |
| 2054 | Locale::Ko => { |
| 2055 | "현재 사용자 요청과 실시간 도구 근거는 메모리, 오래된 인계 자료, 추측보다 우선한다." |
| 2056 | } |
| 2057 | Locale::Ca => { |
| 2058 | "Les peticions actuals de l'usuari i l'evidència en directe de les eines prevalen sobre la memòria, els traspasos antics i les conjectures." |
| 2059 | } |
| 2060 | Locale::De => { |
| 2061 | "Aktuelle Nutzeranfragen und Live-Werkzeugnachweise haben Vorrang vor Speicher, veralteten Übergaben und Vermutungen." |
| 2062 | } |
| 2063 | Locale::Fr => { |
| 2064 | "Les demandes actuelles de l'utilisateur et les preuves directes des outils priment sur la mémoire, les anciens transferts et les suppositions." |
| 2065 | } |
| 2066 | Locale::Id => { |
| 2067 | "Permintaan pengguna saat ini dan bukti langsung dari alat mengalahkan memori, handoff lama, dan tebakan." |
| 2068 | } |
| 2069 | Locale::Hi => "वर्तमान उपयोगकर्ता अनुरोध और लाइव टूल साक्ष्य मेमोरी, पुराने हैंडऑफ़ और अनुमानों से ऊपर हैं।", |
| 2070 | Locale::Ru => { |
| 2071 | "Текущие запросы пользователя и живые свидетельства инструментов важнее памяти, устаревших передаточных заметок и догадок." |
| 2072 | } |
| 2073 | Locale::Uk => { |
| 2074 | "Поточні запити користувача та живі свідчення інструментів важливіші за пам'ять, застарілі передаточні нотатки й здогадки." |
| 2075 | } |
| 2076 | _ => { |
| 2077 | "Current user requests and live tool evidence outrank memory, stale handoffs, and guesses." |
| 2078 | } |
| 2079 | } |
| 2080 | } |
| 2081 | |
| 2082 | fn bounded_freeform_note(input: &str, max_chars: usize) -> String { |
| 2083 | input |
| 2084 | .chars() |
| 2085 | .filter_map(|ch| { |
| 2086 | if ch == '\t' { |
| 2087 | Some(' ') |
| 2088 | } else if ch == '\n' || !ch.is_control() { |
| 2089 | Some(ch) |
| 2090 | } else { |
| 2091 | None |
| 2092 | } |
| 2093 | }) |
| 2094 | .take(max_chars) |
| 2095 | .collect::<String>() |
| 2096 | .trim() |
| 2097 | .to_string() |
| 2098 | } |
| 2099 | |
| 2100 | fn compact_freeform_preview(note: &str) -> String { |
| 2101 | let compact = note.split_whitespace().collect::<Vec<_>>().join(" "); |
| 2102 | let mut preview = compact.chars().take(96).collect::<String>(); |
| 2103 | if compact.chars().count() > 96 { |
| 2104 | preview.push_str("..."); |
| 2105 | } |
| 2106 | preview |
| 2107 | } |
| 2108 | |
| 2109 | fn freeform_note_line(locale: Locale, note: &str, editing: bool) -> Line<'static> { |
| 2110 | let preview = compact_freeform_preview(note); |
| 2111 | let text = match (locale, editing, preview.is_empty()) { |
| 2112 | (Locale::Ja, true, true) => { |
| 2113 | "F 自由原則:編集中 - 有界の原則を入力または貼り付け、Enter で完了".to_string() |
| 2114 | } |
| 2115 | (Locale::Ja, true, false) => format!("F 自由原則:編集中 - {preview}"), |
| 2116 | (Locale::Ja, false, true) => "F 自由原則:F で有界の原則を入力または貼り付け".to_string(), |
| 2117 | (Locale::Ja, false, false) => format!("F 自由原則:{preview}"), |
| 2118 | (Locale::ZhHans, true, true) => { |
| 2119 | "F 自定义准则:正在编辑 - 输入或粘贴明确的准则,Enter 完成".to_string() |
| 2120 | } |
| 2121 | (Locale::ZhHans, true, false) => format!("F 自定义准则:正在编辑 - {preview}"), |
| 2122 | (Locale::ZhHans, false, true) => { |
| 2123 | "F 自定义准则:按 F 输入或粘贴自己的明确准则".to_string() |
| 2124 | } |
| 2125 | (Locale::ZhHans, false, false) => format!("F 自定义准则:{preview}"), |
| 2126 | (Locale::ZhHant, true, true) => { |
| 2127 | "F 自由原則:正在編輯 - 輸入或貼上有界原則,Enter 完成".to_string() |
| 2128 | } |
| 2129 | (Locale::ZhHant, true, false) => format!("F 自由原則:正在編輯 - {preview}"), |
| 2130 | (Locale::ZhHant, false, true) => "F 自由原則:按 F 輸入或貼上自己的有界原則".to_string(), |
| 2131 | (Locale::ZhHant, false, false) => format!("F 自由原則:{preview}"), |
| 2132 | (Locale::PtBr, true, true) => { |
| 2133 | "F Princípio livre: editando - digite ou cole um princípio limitado, Enter para concluir".to_string() |
| 2134 | } |
| 2135 | (Locale::PtBr, true, false) => format!("F Princípio livre: editando - {preview}"), |
| 2136 | (Locale::PtBr, false, true) => { |
| 2137 | "F Princípio livre: pressione F para digitar ou colar um princípio limitado".to_string() |
| 2138 | } |
| 2139 | (Locale::PtBr, false, false) => format!("F Princípio livre: {preview}"), |
| 2140 | (Locale::Es419, true, true) => { |
| 2141 | "F Principio libre: editando - escribe o pega un principio acotado, Enter para terminar".to_string() |
| 2142 | } |
| 2143 | (Locale::Es419, true, false) => format!("F Principio libre: editando - {preview}"), |
| 2144 | (Locale::Es419, false, true) => { |
| 2145 | "F Principio libre: presiona F para escribir o pegar un principio acotado".to_string() |
| 2146 | } |
| 2147 | (Locale::Es419, false, false) => format!("F Principio libre: {preview}"), |
| 2148 | (Locale::Vi, true, true) => { |
| 2149 | "F Nguyên tắc tự do: đang sửa - nhập hoặc dán nguyên tắc có giới hạn, Enter để xong".to_string() |
| 2150 | } |
| 2151 | (Locale::Vi, true, false) => format!("F Nguyên tắc tự do: đang sửa - {preview}"), |
| 2152 | (Locale::Vi, false, true) => { |
| 2153 | "F Nguyên tắc tự do: nhấn F để nhập hoặc dán nguyên tắc có giới hạn".to_string() |
| 2154 | } |
| 2155 | (Locale::Vi, false, false) => format!("F Nguyên tắc tự do: {preview}"), |
| 2156 | (Locale::Ko, true, true) => { |
| 2157 | "F 자유 원칙: 편집 중 - 제한된 원칙을 입력하거나 붙여넣고 Enter로 완료".to_string() |
| 2158 | } |
| 2159 | (Locale::Ko, true, false) => format!("F 자유 원칙: 편집 중 - {preview}"), |
| 2160 | (Locale::Ko, false, true) => "F 자유 원칙: F를 눌러 제한된 원칙을 입력하거나 붙여넣기".to_string(), |
| 2161 | (Locale::Ko, false, false) => format!("F 자유 원칙: {preview}"), |
| 2162 | (Locale::Ca, true, true) => { |
| 2163 | "F Paraules pròpies: editant - escriu o enganxa un principi acotat, Enter per acabar".to_string() |
| 2164 | } |
| 2165 | (Locale::Ca, true, false) => format!("F Paraules pròpies: editant - {preview}"), |
| 2166 | (Locale::Ca, false, true) => { |
| 2167 | "F Paraules pròpies: prem F per escriure o enganxar un principi acotat".to_string() |
| 2168 | } |
| 2169 | (Locale::Ca, false, false) => format!("F Paraules pròpies: {preview}"), |
| 2170 | (Locale::De, true, true) => { |
| 2171 | "F Eigene Worte: Bearbeitung - tippe oder füge ein begrenztes Prinzip ein, Enter zum Abschluss".to_string() |
| 2172 | } |
| 2173 | (Locale::De, true, false) => format!("F Eigene Worte: Bearbeitung - {preview}"), |
| 2174 | (Locale::De, false, true) => { |
| 2175 | "F Eigene Worte: F drücken, um ein begrenztes Prinzip zu tippen oder einzufügen".to_string() |
| 2176 | } |
| 2177 | (Locale::De, false, false) => format!("F Eigene Worte: {preview}"), |
| 2178 | (Locale::Fr, true, true) => { |
| 2179 | "F Vos mots : édition - tapez ou collez un principe borné, Entrée pour terminer".to_string() |
| 2180 | } |
| 2181 | (Locale::Fr, true, false) => format!("F Vos mots : édition - {preview}"), |
| 2182 | (Locale::Fr, false, true) => { |
| 2183 | "F Vos mots : appuyez sur F pour taper ou coller un principe borné".to_string() |
| 2184 | } |
| 2185 | (Locale::Fr, false, false) => format!("F Vos mots : {preview}"), |
| 2186 | (Locale::Id, true, true) => { |
| 2187 | "F Kata sendiri: mengedit - ketik atau tempel prinsip terbatas, Enter untuk selesai".to_string() |
| 2188 | } |
| 2189 | (Locale::Id, true, false) => format!("F Kata sendiri: mengedit - {preview}"), |
| 2190 | (Locale::Id, false, true) => { |
| 2191 | "F Kata sendiri: tekan F untuk mengetik atau menempel prinsip terbatas".to_string() |
| 2192 | } |
| 2193 | (Locale::Id, false, false) => format!("F Kata sendiri: {preview}"), |
| 2194 | (Locale::Hi, true, true) => { |
| 2195 | "F अपने शब्द: संपादन जारी - सीमित सिद्धांत टाइप या पेस्ट करें, Enter से समाप्त करें".to_string() |
| 2196 | } |
| 2197 | (Locale::Hi, true, false) => format!("F अपने शब्द: संपादन जारी - {preview}"), |
| 2198 | (Locale::Hi, false, true) => { |
| 2199 | "F अपने शब्द: सीमित सिद्धांत टाइप या पेस्ट करने के लिए F दबाएँ".to_string() |
| 2200 | } |
| 2201 | (Locale::Hi, false, false) => format!("F अपने शब्द: {preview}"), |
| 2202 | (Locale::Ru, true, true) => { |
| 2203 | "F Свои слова: редактирование - введите или вставьте ограниченный принцип, Enter для завершения".to_string() |
| 2204 | } |
| 2205 | (Locale::Ru, true, false) => format!("F Свои слова: редактирование - {preview}"), |
| 2206 | (Locale::Ru, false, true) => { |
| 2207 | "F Свои слова: нажмите F, чтобы ввести или вставить ограниченный принцип".to_string() |
| 2208 | } |
| 2209 | (Locale::Ru, false, false) => format!("F Свои слова: {preview}"), |
| 2210 | (Locale::Uk, true, true) => { |
| 2211 | "F Свої слова: редагування - введіть або вставте обмежений принцип, Enter для завершення".to_string() |
| 2212 | } |
| 2213 | (Locale::Uk, true, false) => format!("F Свої слова: редагування - {preview}"), |
| 2214 | (Locale::Uk, false, true) => { |
| 2215 | "F Свої слова: натисніть F, щоб ввести або вставити обмежений принцип".to_string() |
| 2216 | } |
| 2217 | (Locale::Uk, false, false) => format!("F Свої слова: {preview}"), |
| 2218 | (_, true, true) => { |
| 2219 | "F Own words: editing - type or paste a bounded principle, Enter to finish".to_string() |
| 2220 | } |
| 2221 | (_, true, false) => format!("F Own words: editing - {preview}"), |
| 2222 | (_, false, true) => "F Own words: press F to type or paste a bounded principle".to_string(), |
| 2223 | (_, false, false) => format!("F Own words: {preview}"), |
| 2224 | }; |
| 2225 | let style = if editing || !preview.is_empty() { |
| 2226 | Style::default().fg(palette::WHALE_HUMAN) |
| 2227 | } else { |
| 2228 | Style::default().fg(palette::TEXT_MUTED) |
| 2229 | }; |
| 2230 | Line::from(Span::styled(text, style)) |
| 2231 | } |
| 2232 | |
| 2233 | impl SetupWizardView { |
| 2234 | #[cfg(test)] |
| 2235 | #[must_use] |
| 2236 | pub fn new(state: SetupState, locale: Locale) -> Self { |
| 2237 | let selected = initial_step_index(&state); |
| 2238 | Self { |
| 2239 | state, |
| 2240 | selected, |
| 2241 | locale, |
| 2242 | facts: SetupRuntimeFacts::default(), |
| 2243 | guided_draft: GuidedConstitutionDraft::default(), |
| 2244 | freeform_note: String::new(), |
| 2245 | editing_freeform_note: false, |
| 2246 | guided_preview_seen: false, |
| 2247 | existing_preview_seen: false, |
| 2248 | model_draft: None, |
| 2249 | model_draft_label: None, |
| 2250 | runtime_preset: SetupRuntimePreset::default(), |
| 2251 | runtime_preset_preview_seen: false, |
| 2252 | body_scroll: 0, |
| 2253 | } |
| 2254 | } |
| 2255 | |
| 2256 | #[must_use] |
| 2257 | pub fn new_for_app(app: &App, config: &Config) -> Self { |
| 2258 | Self::new_with_facts( |
| 2259 | load_setup_state_for_app(app, config), |
| 2260 | app.ui_locale, |
| 2261 | SetupRuntimeFacts::from_app_config(app, config), |
| 2262 | ) |
| 2263 | } |
| 2264 | |
| 2265 | #[must_use] |
| 2266 | pub fn new_for_app_at(app: &App, config: &Config, step: SetupStep) -> Self { |
| 2267 | Self::new_at_with_facts( |
| 2268 | load_setup_state_for_app(app, config), |
| 2269 | app.ui_locale, |
| 2270 | step, |
| 2271 | SetupRuntimeFacts::from_app_config(app, config), |
| 2272 | ) |
| 2273 | } |
| 2274 | |
| 2275 | #[cfg(test)] |
| 2276 | #[must_use] |
| 2277 | pub fn state(&self) -> &SetupState { |
| 2278 | &self.state |
| 2279 | } |
| 2280 | |
| 2281 | #[must_use] |
| 2282 | pub fn selected_step(&self) -> SetupStep { |
| 2283 | STEP_SPECS[self.selected].id() |
| 2284 | } |
| 2285 | |
| 2286 | fn selected_spec(&self) -> &'static dyn SetupWizardStep { |
| 2287 | &STEP_SPECS[self.selected] |
| 2288 | } |
| 2289 | |
| 2290 | fn new_with_facts(state: SetupState, locale: Locale, facts: SetupRuntimeFacts) -> Self { |
| 2291 | let selected = initial_step_index(&state); |
| 2292 | Self { |
| 2293 | state, |
| 2294 | selected, |
| 2295 | locale, |
| 2296 | facts, |
| 2297 | guided_draft: GuidedConstitutionDraft::default(), |
| 2298 | freeform_note: String::new(), |
| 2299 | editing_freeform_note: false, |
| 2300 | guided_preview_seen: false, |
| 2301 | existing_preview_seen: false, |
| 2302 | model_draft: None, |
| 2303 | model_draft_label: None, |
| 2304 | runtime_preset: SetupRuntimePreset::default(), |
| 2305 | runtime_preset_preview_seen: false, |
| 2306 | body_scroll: 0, |
| 2307 | } |
| 2308 | } |
| 2309 | |
| 2310 | fn new_at_with_facts( |
| 2311 | state: SetupState, |
| 2312 | locale: Locale, |
| 2313 | step: SetupStep, |
| 2314 | facts: SetupRuntimeFacts, |
| 2315 | ) -> Self { |
| 2316 | Self { |
| 2317 | state, |
| 2318 | selected: visible_step_index(step), |
| 2319 | locale, |
| 2320 | facts, |
| 2321 | guided_draft: GuidedConstitutionDraft::default(), |
| 2322 | freeform_note: String::new(), |
| 2323 | editing_freeform_note: false, |
| 2324 | guided_preview_seen: false, |
| 2325 | existing_preview_seen: false, |
| 2326 | model_draft: None, |
| 2327 | model_draft_label: None, |
| 2328 | runtime_preset: SetupRuntimePreset::default(), |
| 2329 | runtime_preset_preview_seen: false, |
| 2330 | body_scroll: 0, |
| 2331 | } |
| 2332 | } |
| 2333 | |
| 2334 | fn move_next(&mut self) { |
| 2335 | self.selected = (self.selected + 1).min(STEP_SPECS.len().saturating_sub(1)); |
| 2336 | self.body_scroll = 0; |
| 2337 | } |
| 2338 | |
| 2339 | fn move_back(&mut self) { |
| 2340 | self.selected = self.selected.saturating_sub(1); |
| 2341 | self.body_scroll = 0; |
| 2342 | } |
| 2343 | |
| 2344 | fn commit_selected_status( |
| 2345 | &mut self, |
| 2346 | status: StepStatus, |
| 2347 | message_id: MessageId, |
| 2348 | advance: bool, |
| 2349 | ) -> ViewAction { |
| 2350 | let spec = self.selected_spec(); |
| 2351 | let result = match status { |
| 2352 | StepStatus::Skipped => Some("skipped by user"), |
| 2353 | StepStatus::NeedsAction => Some("retry requested; needs action"), |
| 2354 | _ => None, |
| 2355 | }; |
| 2356 | let mut entry = StepEntry::new(status, spec.required(), CONSTITUTION_CHECKPOINT_VERSION); |
| 2357 | if let Some(result) = result { |
| 2358 | entry = entry.with_result(result); |
| 2359 | } |
| 2360 | let mut state = self.state.clone(); |
| 2361 | state.set_step(spec.id(), entry); |
| 2362 | if spec.id() == SetupStep::Constitution && status == StepStatus::Skipped { |
| 2363 | // `S` is a durable response to the versioned checkpoint, just like |
| 2364 | // choosing the explicit defer action. It only skips this setup |
| 2365 | // checkpoint, though; it must not replace an already active |
| 2366 | // bundled or custom Constitution choice. A fresh state has no |
| 2367 | // active choice, so keep the bundled floor by recording Deferred. |
| 2368 | let choice = if state.constitution_choice.is_explicit() { |
| 2369 | state.constitution_choice |
| 2370 | } else { |
| 2371 | ConstitutionChoice::Deferred |
| 2372 | }; |
| 2373 | state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice); |
| 2374 | } |
| 2375 | self.state = state.clone(); |
| 2376 | if advance { |
| 2377 | self.move_next(); |
| 2378 | } |
| 2379 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2380 | state, |
| 2381 | message: tr(self.locale, message_id).to_string(), |
| 2382 | }) |
| 2383 | } |
| 2384 | |
| 2385 | fn commit_language_review(&mut self) -> ViewAction { |
| 2386 | let mut state = self.state.clone(); |
| 2387 | state.constitution_language = Some(self.locale.tag().to_string()); |
| 2388 | state.set_step( |
| 2389 | SetupStep::Language, |
| 2390 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2391 | .with_result(format!("setup locale {}", self.locale.tag())), |
| 2392 | ); |
| 2393 | self.state = state.clone(); |
| 2394 | self.move_next(); |
| 2395 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2396 | state, |
| 2397 | message: tr(self.locale, MessageId::SetupLanguageReviewed).to_string(), |
| 2398 | }) |
| 2399 | } |
| 2400 | |
| 2401 | fn commit_provider_model_review(&mut self) -> ViewAction { |
| 2402 | let status = provider::step_status(self.facts.provider_ready); |
| 2403 | let mut state = self.state.clone(); |
| 2404 | state.set_step( |
| 2405 | SetupStep::ProviderModel, |
| 2406 | provider::step_entry( |
| 2407 | self.facts.provider_ready, |
| 2408 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2409 | self.facts.provider_result.clone(), |
| 2410 | ), |
| 2411 | ); |
| 2412 | self.state = state.clone(); |
| 2413 | self.move_next(); |
| 2414 | let message_id = if status == StepStatus::Verified { |
| 2415 | MessageId::SetupProviderModelReviewed |
| 2416 | } else { |
| 2417 | MessageId::SetupProviderModelNeedsActionSaved |
| 2418 | }; |
| 2419 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2420 | state, |
| 2421 | message: tr(self.locale, message_id).to_string(), |
| 2422 | }) |
| 2423 | } |
| 2424 | |
| 2425 | fn commit_runtime_posture_review(&mut self) -> ViewAction { |
| 2426 | let mut state = self.state.clone(); |
| 2427 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 2428 | state.set_step( |
| 2429 | SetupStep::TrustSandbox, |
| 2430 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2431 | .with_result(self.facts.runtime_result.clone()), |
| 2432 | ); |
| 2433 | self.state = state.clone(); |
| 2434 | self.move_next(); |
| 2435 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2436 | state, |
| 2437 | message: tr(self.locale, MessageId::SetupRuntimePostureReviewed).to_string(), |
| 2438 | }) |
| 2439 | } |
| 2440 | |
| 2441 | fn operate_fleet_facts_ready(&self) -> bool { |
| 2442 | // Provider, capacity, and roster facts are configuration snapshots, |
| 2443 | // not proof of dispatch and terminal receipts. This release must never |
| 2444 | // persist an Operate-ready claim from those facts alone. |
| 2445 | false |
| 2446 | } |
| 2447 | |
| 2448 | fn commit_operate_fleet_review(&mut self) -> ViewAction { |
| 2449 | let status = if self.operate_fleet_facts_ready() { |
| 2450 | StepStatus::Verified |
| 2451 | } else { |
| 2452 | StepStatus::NeedsAction |
| 2453 | }; |
| 2454 | let mut state = self.state.clone(); |
| 2455 | state.set_step( |
| 2456 | SetupStep::OperateFleet, |
| 2457 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2458 | .with_result(self.facts.operate_result.clone()), |
| 2459 | ); |
| 2460 | self.state = state.clone(); |
| 2461 | self.move_next(); |
| 2462 | let message_id = if status == StepStatus::Verified { |
| 2463 | MessageId::SetupOperateReviewed |
| 2464 | } else { |
| 2465 | MessageId::SetupOperateNeedsActionSaved |
| 2466 | }; |
| 2467 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2468 | state, |
| 2469 | message: tr(self.locale, message_id).to_string(), |
| 2470 | }) |
| 2471 | } |
| 2472 | |
| 2473 | fn commit_hotbar_review(&mut self) -> ViewAction { |
| 2474 | let mut state = self.state.clone(); |
| 2475 | state.set_step( |
| 2476 | SetupStep::Hotbar, |
| 2477 | StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2478 | .with_result(self.facts.hotbar_result.clone()), |
| 2479 | ); |
| 2480 | self.state = state.clone(); |
| 2481 | self.move_next(); |
| 2482 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2483 | state, |
| 2484 | message: tr(self.locale, MessageId::SetupHotbarReviewed).to_string(), |
| 2485 | }) |
| 2486 | } |
| 2487 | |
| 2488 | fn commit_tools_mcp_review(&mut self) -> ViewAction { |
| 2489 | // Optional step: empty/off inventories settle as Optional; broken |
| 2490 | // configured tools record NeedsAction without blocking first-run. |
| 2491 | let status = if self.facts.tools_mcp_needs_action { |
| 2492 | StepStatus::NeedsAction |
| 2493 | } else if self.facts.tools_mcp_result.contains("overall=off") { |
| 2494 | StepStatus::Optional |
| 2495 | } else { |
| 2496 | StepStatus::Verified |
| 2497 | }; |
| 2498 | let mut state = self.state.clone(); |
| 2499 | state.set_step( |
| 2500 | SetupStep::ToolsMcp, |
| 2501 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2502 | .with_result(self.facts.tools_mcp_result.clone()), |
| 2503 | ); |
| 2504 | self.state = state.clone(); |
| 2505 | self.move_next(); |
| 2506 | let message_id = if status == StepStatus::NeedsAction { |
| 2507 | MessageId::SetupToolsMcpNeedsActionSaved |
| 2508 | } else { |
| 2509 | MessageId::SetupToolsMcpReviewed |
| 2510 | }; |
| 2511 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2512 | state, |
| 2513 | message: tr(self.locale, message_id).to_string(), |
| 2514 | }) |
| 2515 | } |
| 2516 | |
| 2517 | fn preview_tools_mcp_on_ramp(&self) -> ViewAction { |
| 2518 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2519 | title: tr(self.locale, MessageId::SetupToolsMcpPreviewTitle).to_string(), |
| 2520 | content: tools_mcp_on_ramp_text(self.locale, &self.facts), |
| 2521 | }) |
| 2522 | } |
| 2523 | |
| 2524 | fn preview_remote_runtime_on_ramp(&self) -> ViewAction { |
| 2525 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2526 | title: tr(self.locale, MessageId::SetupRemotePreviewTitle).to_string(), |
| 2527 | content: remote_runtime_on_ramp_text(self.locale, &self.facts), |
| 2528 | }) |
| 2529 | } |
| 2530 | |
| 2531 | /// Record the remote step honestly (#3409). |
| 2532 | /// |
| 2533 | /// Local-only always works, so Enter alone settles the step — a user who |
| 2534 | /// never wants remote access is finished in one key. When a *reachable* |
| 2535 | /// mode is missing a token or config the entry is `NeedsAction`, which the |
| 2536 | /// setup report and doctor inherit verbatim and which never blocks ready. |
| 2537 | fn commit_remote_runtime_review(&mut self) -> ViewAction { |
| 2538 | let mut state = self.state.clone(); |
| 2539 | let status = if self.facts.remote_needs_action { |
| 2540 | StepStatus::NeedsAction |
| 2541 | } else { |
| 2542 | StepStatus::Verified |
| 2543 | }; |
| 2544 | state.set_step( |
| 2545 | SetupStep::RemoteRuntime, |
| 2546 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2547 | .with_result(self.facts.remote_result.clone()), |
| 2548 | ); |
| 2549 | self.state = state.clone(); |
| 2550 | self.move_next(); |
| 2551 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2552 | state, |
| 2553 | message: tr(self.locale, MessageId::SetupRemoteReviewed).to_string(), |
| 2554 | }) |
| 2555 | } |
| 2556 | |
| 2557 | fn commit_persistence_review(&mut self) -> ViewAction { |
| 2558 | let mut state = self.state.clone(); |
| 2559 | state.set_step( |
| 2560 | SetupStep::Persistence, |
| 2561 | StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2562 | .with_result(self.facts.persistence.result.clone()), |
| 2563 | ); |
| 2564 | self.state = state.clone(); |
| 2565 | self.move_next(); |
| 2566 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2567 | state, |
| 2568 | message: tr(self.locale, MessageId::SetupPersistenceReviewed).to_string(), |
| 2569 | }) |
| 2570 | } |
| 2571 | |
| 2572 | fn select_runtime_preset(&mut self, key: char) -> ViewAction { |
| 2573 | if let Some(preset) = SetupRuntimePreset::from_key(key) |
| 2574 | && preset != self.runtime_preset |
| 2575 | { |
| 2576 | self.runtime_preset = preset; |
| 2577 | self.runtime_preset_preview_seen = false; |
| 2578 | } |
| 2579 | ViewAction::None |
| 2580 | } |
| 2581 | |
| 2582 | fn preview_runtime_preset(&mut self) -> ViewAction { |
| 2583 | self.runtime_preset_preview_seen = true; |
| 2584 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2585 | title: tr(self.locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(), |
| 2586 | content: runtime_preset_preview_text(self.locale, self.runtime_preset, &self.facts), |
| 2587 | }) |
| 2588 | } |
| 2589 | |
| 2590 | fn commit_runtime_preset(&mut self) -> ViewAction { |
| 2591 | if !self.runtime_preset_preview_seen { |
| 2592 | return self.preview_runtime_preset(); |
| 2593 | } |
| 2594 | |
| 2595 | let mut state = self.state.clone(); |
| 2596 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 2597 | state.set_step( |
| 2598 | SetupStep::TrustSandbox, |
| 2599 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2600 | .with_result(self.runtime_preset.result_summary()), |
| 2601 | ); |
| 2602 | self.state = state.clone(); |
| 2603 | self.move_next(); |
| 2604 | ViewAction::Emit(ViewEvent::SetupRuntimePresetApplyRequested { |
| 2605 | preset: self.runtime_preset, |
| 2606 | state, |
| 2607 | message: tr(self.locale, MessageId::SetupRuntimePresetApplied).to_string(), |
| 2608 | }) |
| 2609 | } |
| 2610 | |
| 2611 | fn commit_setup_report(&mut self) -> ViewAction { |
| 2612 | let mut state = self.state.clone(); |
| 2613 | let status = if setup_report_ready(&state) { |
| 2614 | StepStatus::Verified |
| 2615 | } else { |
| 2616 | StepStatus::NeedsAction |
| 2617 | }; |
| 2618 | state.set_step( |
| 2619 | SetupStep::Verification, |
| 2620 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2621 | .with_result(setup_report_result(&state, &self.facts)), |
| 2622 | ); |
| 2623 | self.state = state.clone(); |
| 2624 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2625 | state, |
| 2626 | message: tr(self.locale, MessageId::SetupReportRecorded).to_string(), |
| 2627 | }) |
| 2628 | } |
| 2629 | |
| 2630 | fn commit_guided_constitution(&mut self) -> ViewAction { |
| 2631 | if !self.guided_preview_seen { |
| 2632 | return self.preview_guided_constitution(); |
| 2633 | } |
| 2634 | |
| 2635 | let (constitution, authoring) = match self.model_draft.as_deref() { |
| 2636 | // Model drafts arrive sanitized + bounded from the untrusted-JSON |
| 2637 | // gate; ratify exactly what was previewed. |
| 2638 | Some(draft) => (draft.clone(), ConstitutionAuthoring::ModelDrafted), |
| 2639 | None => ( |
| 2640 | self.guided_draft |
| 2641 | .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()), |
| 2642 | ConstitutionAuthoring::Guided, |
| 2643 | ), |
| 2644 | }; |
| 2645 | let mut state = self.state.clone(); |
| 2646 | state.complete_constitution_checkpoint( |
| 2647 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2648 | ConstitutionChoice::GuidedCustom, |
| 2649 | ); |
| 2650 | state.constitution_language = constitution.language.clone(); |
| 2651 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 2652 | state.constitution_validity = ConstitutionValidity::Valid; |
| 2653 | state.constitution_authoring = Some(authoring); |
| 2654 | state.constitution_preview_hash = Some(constitution.preview_hash()); |
| 2655 | state.constitution_preview_version = |
| 2656 | state.constitution_preview_version.saturating_add(1).max(1); |
| 2657 | let hash = state |
| 2658 | .constitution_preview_hash |
| 2659 | .as_deref() |
| 2660 | .unwrap_or("unknown"); |
| 2661 | let result = match authoring { |
| 2662 | ConstitutionAuthoring::ModelDrafted => format!( |
| 2663 | "model-drafted constitution ratified ({}) preview_hash={hash}", |
| 2664 | self.model_draft_label.as_deref().unwrap_or("model") |
| 2665 | ), |
| 2666 | ConstitutionAuthoring::Guided => { |
| 2667 | format!("guided custom constitution preview_hash={hash}") |
| 2668 | } |
| 2669 | }; |
| 2670 | state.set_step( |
| 2671 | SetupStep::Constitution, |
| 2672 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2673 | .with_result(result), |
| 2674 | ); |
| 2675 | self.state = state.clone(); |
| 2676 | ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 2677 | constitution, |
| 2678 | state, |
| 2679 | message: tr(self.locale, MessageId::SetupCheckpointDoneGuided).to_string(), |
| 2680 | }) |
| 2681 | } |
| 2682 | |
| 2683 | fn preview_guided_constitution(&mut self) -> ViewAction { |
| 2684 | self.guided_preview_seen = true; |
| 2685 | let (constitution, provenance) = match self.model_draft.as_deref() { |
| 2686 | Some(draft) => ( |
| 2687 | draft.clone(), |
| 2688 | DraftProvenance::Model( |
| 2689 | self.model_draft_label |
| 2690 | .clone() |
| 2691 | .unwrap_or_else(|| "model".to_string()), |
| 2692 | ), |
| 2693 | ), |
| 2694 | None => ( |
| 2695 | self.guided_draft |
| 2696 | .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()), |
| 2697 | DraftProvenance::Guided, |
| 2698 | ), |
| 2699 | }; |
| 2700 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2701 | title: ratification_preview_title(self.locale).to_string(), |
| 2702 | content: constitution_ratification_text(self.locale, &constitution, &provenance), |
| 2703 | }) |
| 2704 | } |
| 2705 | |
| 2706 | fn cycle_guided_answer(&mut self, key: char) -> ViewAction { |
| 2707 | if self.guided_draft.cycle(key) { |
| 2708 | self.guided_preview_seen = false; |
| 2709 | // Answers changed under the draft: the model draft is stale law |
| 2710 | // and must be re-drafted or replaced by the guided rendering. |
| 2711 | self.model_draft = None; |
| 2712 | self.model_draft_label = None; |
| 2713 | } |
| 2714 | ViewAction::None |
| 2715 | } |
| 2716 | |
| 2717 | /// `A` on the constitution step: ask the first configured model to draft. |
| 2718 | /// Requires a ready provider route; otherwise the key is inert and the |
| 2719 | /// deterministic guided flow stands untouched. |
| 2720 | fn request_model_draft(&self) -> ViewAction { |
| 2721 | if !self.facts.provider_ready { |
| 2722 | return ViewAction::None; |
| 2723 | } |
| 2724 | ViewAction::Emit(ViewEvent::SetupConstitutionModelDraftRequested { |
| 2725 | draft: self.guided_draft, |
| 2726 | freeform_note: self.freeform_note_for_draft().map(str::to_string), |
| 2727 | locale: self.locale, |
| 2728 | }) |
| 2729 | } |
| 2730 | |
| 2731 | fn toggle_freeform_edit(&mut self) -> ViewAction { |
| 2732 | if self.selected_step() == SetupStep::Constitution { |
| 2733 | self.editing_freeform_note = !self.editing_freeform_note; |
| 2734 | } |
| 2735 | ViewAction::None |
| 2736 | } |
| 2737 | |
| 2738 | fn freeform_note_for_draft(&self) -> Option<&str> { |
| 2739 | let note = self.freeform_note.trim(); |
| 2740 | (!note.is_empty()).then_some(note) |
| 2741 | } |
| 2742 | |
| 2743 | fn append_freeform_note_text(&mut self, text: &str) { |
| 2744 | let mut next = self.freeform_note.clone(); |
| 2745 | next.push_str(text); |
| 2746 | self.freeform_note = bounded_freeform_note(&next, MAX_NOTES_LEN); |
| 2747 | self.guided_preview_seen = false; |
| 2748 | self.model_draft = None; |
| 2749 | self.model_draft_label = None; |
| 2750 | } |
| 2751 | |
| 2752 | fn handle_freeform_note_key(&mut self, key: KeyEvent) -> Option<ViewAction> { |
| 2753 | if self.selected_step() != SetupStep::Constitution || !self.editing_freeform_note { |
| 2754 | return None; |
| 2755 | } |
| 2756 | match key.code { |
| 2757 | KeyCode::Esc | KeyCode::Enter => { |
| 2758 | self.editing_freeform_note = false; |
| 2759 | Some(ViewAction::None) |
| 2760 | } |
| 2761 | KeyCode::Backspace => { |
| 2762 | self.freeform_note.pop(); |
| 2763 | self.guided_preview_seen = false; |
| 2764 | self.model_draft = None; |
| 2765 | self.model_draft_label = None; |
| 2766 | Some(ViewAction::None) |
| 2767 | } |
| 2768 | KeyCode::Char(c) if key.modifiers.is_empty() => { |
| 2769 | let mut buf = [0; 4]; |
| 2770 | self.append_freeform_note_text(c.encode_utf8(&mut buf)); |
| 2771 | Some(ViewAction::None) |
| 2772 | } |
| 2773 | _ => Some(ViewAction::None), |
| 2774 | } |
| 2775 | } |
| 2776 | |
| 2777 | /// Install a model-drafted constitution (already sanitized + bounded by |
| 2778 | /// the untrusted-JSON gate) and return the `(title, content)` of the |
| 2779 | /// ratification preview the host must open in the same breath — that is |
| 2780 | /// what satisfies the preview gate. Ratifying still takes the explicit |
| 2781 | /// `G` keypress afterwards. |
| 2782 | #[must_use] |
| 2783 | pub(crate) fn install_model_draft( |
| 2784 | &mut self, |
| 2785 | constitution: Box<UserConstitution>, |
| 2786 | model_label: String, |
| 2787 | ) -> (String, String) { |
| 2788 | let content = constitution_ratification_text( |
| 2789 | self.locale, |
| 2790 | &constitution, |
| 2791 | &DraftProvenance::Model(model_label.clone()), |
| 2792 | ); |
| 2793 | self.model_draft = Some(constitution); |
| 2794 | self.model_draft_label = Some(model_label); |
| 2795 | self.guided_preview_seen = true; |
| 2796 | (ratification_preview_title(self.locale).to_string(), content) |
| 2797 | } |
| 2798 | |
| 2799 | fn commit_constitution(&self, kind: SetupCommitKind) -> ViewAction { |
| 2800 | let choice = match kind { |
| 2801 | SetupCommitKind::BundledConstitution => ConstitutionChoice::Bundled, |
| 2802 | SetupCommitKind::DeferredConstitution => ConstitutionChoice::Deferred, |
| 2803 | }; |
| 2804 | let mut state = self.state.clone(); |
| 2805 | state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice); |
| 2806 | state.constitution_source = ConstitutionSource::Bundled; |
| 2807 | state.constitution_validity = ConstitutionValidity::Unknown; |
| 2808 | state.constitution_authoring = None; |
| 2809 | state.constitution_preview_hash = None; |
| 2810 | state.set_step( |
| 2811 | SetupStep::Constitution, |
| 2812 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2813 | .with_result(match kind { |
| 2814 | SetupCommitKind::BundledConstitution => "bundled/default constitution", |
| 2815 | SetupCommitKind::DeferredConstitution => "checkpoint deferred; bundled applies", |
| 2816 | }), |
| 2817 | ); |
| 2818 | let message_id = match kind { |
| 2819 | SetupCommitKind::BundledConstitution => MessageId::SetupCheckpointDoneBundled, |
| 2820 | SetupCommitKind::DeferredConstitution => MessageId::SetupCheckpointDeferred, |
| 2821 | }; |
| 2822 | ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { |
| 2823 | state, |
| 2824 | message: tr(self.locale, message_id).to_string(), |
| 2825 | }) |
| 2826 | } |
| 2827 | |
| 2828 | /// Complete the checkpoint by keeping the existing valid |
| 2829 | /// `constitution.json` exactly as it stands (#3794). First `K` previews |
| 2830 | /// the rendered law; second `K` records the choice. The file is never |
| 2831 | /// rewritten — only `setup_state.json` changes, through the same commit |
| 2832 | /// event as every other completion. |
| 2833 | fn commit_keep_existing_constitution(&mut self) -> ViewAction { |
| 2834 | if self.facts.constitution_file != SetupConstitutionFileState::Loaded { |
| 2835 | return ViewAction::None; |
| 2836 | } |
| 2837 | // Re-read the live file so a stale card cannot ratify a file that |
| 2838 | // has since become invalid; any non-loaded state leaves the key inert. |
| 2839 | let Ok(load) = UserConstitution::load() else { |
| 2840 | return ViewAction::None; |
| 2841 | }; |
| 2842 | let Some(constitution) = load.constitution() else { |
| 2843 | return ViewAction::None; |
| 2844 | }; |
| 2845 | if !self.existing_preview_seen { |
| 2846 | self.existing_preview_seen = true; |
| 2847 | let content = constitution_ratification_text( |
| 2848 | self.locale, |
| 2849 | constitution, |
| 2850 | &DraftProvenance::Existing, |
| 2851 | ); |
| 2852 | return ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2853 | title: ratification_preview_title(self.locale).to_string(), |
| 2854 | content, |
| 2855 | }); |
| 2856 | } |
| 2857 | let mut state = self.state.clone(); |
| 2858 | state.complete_constitution_checkpoint( |
| 2859 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2860 | ConstitutionChoice::GuidedCustom, |
| 2861 | ); |
| 2862 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 2863 | state.constitution_validity = ConstitutionValidity::Valid; |
| 2864 | state.constitution_preview_hash = Some(constitution.preview_hash()); |
| 2865 | state.set_step( |
| 2866 | SetupStep::Constitution, |
| 2867 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2868 | .with_result("existing constitution kept unchanged"), |
| 2869 | ); |
| 2870 | ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { |
| 2871 | state, |
| 2872 | message: tr(self.locale, MessageId::SetupCheckpointDoneKept).to_string(), |
| 2873 | }) |
| 2874 | } |
| 2875 | |
| 2876 | fn status_label(&self, status: StepStatus) -> Cow<'static, str> { |
| 2877 | tr( |
| 2878 | self.locale, |
| 2879 | match status { |
| 2880 | StepStatus::NotStarted => MessageId::SetupStatusNotStarted, |
| 2881 | StepStatus::Recommended => MessageId::SetupStatusRecommended, |
| 2882 | StepStatus::Optional => MessageId::SetupStatusOptional, |
| 2883 | StepStatus::Deferred => MessageId::SetupStatusDeferred, |
| 2884 | StepStatus::InProgress => MessageId::SetupStatusInProgress, |
| 2885 | StepStatus::NeedsAction => MessageId::SetupStatusNeedsAction, |
| 2886 | StepStatus::Verified => MessageId::SetupStatusVerified, |
| 2887 | StepStatus::Skipped => MessageId::SetupStatusSkipped, |
| 2888 | StepStatus::Failed => MessageId::SetupStatusFailed, |
| 2889 | }, |
| 2890 | ) |
| 2891 | } |
| 2892 | } |
| 2893 | |
| 2894 | impl ModalView for SetupWizardView { |
| 2895 | fn kind(&self) -> ModalKind { |
| 2896 | ModalKind::SetupWizard |
| 2897 | } |
| 2898 | |
| 2899 | fn handle_key(&mut self, key: KeyEvent) -> ViewAction { |
| 2900 | if let Some(action) = self.handle_freeform_note_key(key) { |
| 2901 | return action; |
| 2902 | } |
| 2903 | match key.code { |
| 2904 | KeyCode::Esc | KeyCode::Char('q') => ViewAction::Close, |
| 2905 | KeyCode::Left | KeyCode::Char('b') => { |
| 2906 | self.move_back(); |
| 2907 | ViewAction::None |
| 2908 | } |
| 2909 | KeyCode::Right | KeyCode::Char('n') => { |
| 2910 | self.move_next(); |
| 2911 | ViewAction::None |
| 2912 | } |
| 2913 | KeyCode::PageUp => { |
| 2914 | self.body_scroll = self.body_scroll.saturating_sub(8); |
| 2915 | ViewAction::None |
| 2916 | } |
| 2917 | KeyCode::PageDown => { |
| 2918 | self.body_scroll = self.body_scroll.saturating_add(8); |
| 2919 | ViewAction::None |
| 2920 | } |
| 2921 | KeyCode::Up => { |
| 2922 | self.move_back(); |
| 2923 | ViewAction::None |
| 2924 | } |
| 2925 | KeyCode::Down => { |
| 2926 | self.move_next(); |
| 2927 | ViewAction::None |
| 2928 | } |
| 2929 | KeyCode::Char('s') => { |
| 2930 | self.commit_selected_status(StepStatus::Skipped, MessageId::SetupStepSkipped, true) |
| 2931 | } |
| 2932 | KeyCode::Char('r') if self.selected_step() == SetupStep::ToolsMcp => { |
| 2933 | self.preview_tools_mcp_on_ramp() |
| 2934 | } |
| 2935 | KeyCode::Char('r') if self.selected_step() == SetupStep::RemoteRuntime => { |
| 2936 | self.preview_remote_runtime_on_ramp() |
| 2937 | } |
| 2938 | KeyCode::Char('r') => self.commit_selected_status( |
| 2939 | StepStatus::NeedsAction, |
| 2940 | MessageId::SetupStepRetryRecorded, |
| 2941 | false, |
| 2942 | ), |
| 2943 | KeyCode::Char('g') if self.selected_step() == SetupStep::Constitution => { |
| 2944 | self.commit_guided_constitution() |
| 2945 | } |
| 2946 | KeyCode::Char('p') if self.selected_step() == SetupStep::ProviderModel => { |
| 2947 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 2948 | } |
| 2949 | KeyCode::Char('m') if self.selected_step() == SetupStep::ProviderModel => { |
| 2950 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModelRequested) |
| 2951 | } |
| 2952 | KeyCode::Char('p') if self.selected_step() == SetupStep::OperateFleet => { |
| 2953 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 2954 | } |
| 2955 | KeyCode::Char('f') if self.selected_step() == SetupStep::OperateFleet => { |
| 2956 | ViewAction::EmitAndClose(ViewEvent::SetupOpenFleetRequested) |
| 2957 | } |
| 2958 | KeyCode::Char('h') if self.selected_step() == SetupStep::Hotbar => { |
| 2959 | ViewAction::EmitAndClose(ViewEvent::SetupOpenHotbarRequested) |
| 2960 | } |
| 2961 | KeyCode::Char('m') if self.selected_step() == SetupStep::TrustSandbox => { |
| 2962 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModeRequested) |
| 2963 | } |
| 2964 | KeyCode::Char('c') if self.selected_step() == SetupStep::TrustSandbox => { |
| 2965 | ViewAction::EmitAndClose(ViewEvent::SetupOpenConfigRequested) |
| 2966 | } |
| 2967 | KeyCode::Char(key @ ('1' | '2' | '3')) |
| 2968 | if self.selected_step() == SetupStep::TrustSandbox => |
| 2969 | { |
| 2970 | self.select_runtime_preset(key) |
| 2971 | } |
| 2972 | KeyCode::Char('a') if self.selected_step() == SetupStep::TrustSandbox => { |
| 2973 | self.commit_runtime_preset() |
| 2974 | } |
| 2975 | KeyCode::Char(key @ ('1' | '2' | '3' | '4' | '5' | '6')) |
| 2976 | if self.selected_step() == SetupStep::Constitution => |
| 2977 | { |
| 2978 | self.cycle_guided_answer(key) |
| 2979 | } |
| 2980 | KeyCode::Char('a') if self.selected_step() == SetupStep::Constitution => { |
| 2981 | self.request_model_draft() |
| 2982 | } |
| 2983 | KeyCode::Char('f') if self.selected_step() == SetupStep::Constitution => { |
| 2984 | self.toggle_freeform_edit() |
| 2985 | } |
| 2986 | KeyCode::Char('k') if self.selected_step() == SetupStep::Constitution => { |
| 2987 | self.commit_keep_existing_constitution() |
| 2988 | } |
| 2989 | KeyCode::Char('u') => self.commit_constitution(SetupCommitKind::BundledConstitution), |
| 2990 | KeyCode::Char('d') => self.commit_constitution(SetupCommitKind::DeferredConstitution), |
| 2991 | KeyCode::Enter if self.selected_step() == SetupStep::Constitution => { |
| 2992 | self.commit_constitution(SetupCommitKind::BundledConstitution) |
| 2993 | } |
| 2994 | KeyCode::Enter if self.selected_step() == SetupStep::Language => { |
| 2995 | self.commit_language_review() |
| 2996 | } |
| 2997 | KeyCode::Enter if self.selected_step() == SetupStep::ProviderModel => { |
| 2998 | self.commit_provider_model_review() |
| 2999 | } |
| 3000 | KeyCode::Enter if self.selected_step() == SetupStep::TrustSandbox => { |
| 3001 | self.commit_runtime_posture_review() |
| 3002 | } |
| 3003 | KeyCode::Enter if self.selected_step() == SetupStep::OperateFleet => { |
| 3004 | self.commit_operate_fleet_review() |
| 3005 | } |
| 3006 | KeyCode::Enter if self.selected_step() == SetupStep::Hotbar => { |
| 3007 | self.commit_hotbar_review() |
| 3008 | } |
| 3009 | KeyCode::Enter if self.selected_step() == SetupStep::ToolsMcp => { |
| 3010 | self.commit_tools_mcp_review() |
| 3011 | } |
| 3012 | KeyCode::Enter if self.selected_step() == SetupStep::RemoteRuntime => { |
| 3013 | self.commit_remote_runtime_review() |
| 3014 | } |
| 3015 | KeyCode::Enter if self.selected_step() == SetupStep::Persistence => { |
| 3016 | self.commit_persistence_review() |
| 3017 | } |
| 3018 | KeyCode::Enter if self.selected_step() == SetupStep::Verification => { |
| 3019 | self.commit_setup_report() |
| 3020 | } |
| 3021 | KeyCode::Enter => { |
| 3022 | self.move_next(); |
| 3023 | ViewAction::None |
| 3024 | } |
| 3025 | _ => ViewAction::None, |
| 3026 | } |
| 3027 | } |
| 3028 | |
| 3029 | fn handle_paste(&mut self, text: &str) -> bool { |
| 3030 | if self.selected_step() != SetupStep::Constitution { |
| 3031 | return false; |
| 3032 | } |
| 3033 | self.append_freeform_note_text(text); |
| 3034 | true |
| 3035 | } |
| 3036 | |
| 3037 | fn render(&self, area: Rect, buf: &mut Buffer) { |
| 3038 | let progress = format!( |
| 3039 | "{} {}/{}", |
| 3040 | tr(self.locale, MessageId::SetupWizardProgress), |
| 3041 | self.selected + 1, |
| 3042 | STEP_SPECS.len() |
| 3043 | ); |
| 3044 | let inner = render_underwater_surface( |
| 3045 | area, |
| 3046 | buf, |
| 3047 | format!( |
| 3048 | "{} · {progress}", |
| 3049 | tr(self.locale, MessageId::SetupWizardTitle) |
| 3050 | ), |
| 3051 | ); |
| 3052 | let mut hints = vec![ |
| 3053 | ActionHint::new("B", tr(self.locale, MessageId::SetupActionBack).to_string()), |
| 3054 | ActionHint::new( |
| 3055 | "N", |
| 3056 | tr(self.locale, MessageId::SetupActionContinue).to_string(), |
| 3057 | ), |
| 3058 | ActionHint::new("S", tr(self.locale, MessageId::SetupActionSkip).to_string()), |
| 3059 | ActionHint::new( |
| 3060 | "R", |
| 3061 | tr(self.locale, MessageId::SetupActionRetry).to_string(), |
| 3062 | ), |
| 3063 | ActionHint::new( |
| 3064 | "PgUp/Dn", |
| 3065 | tr(self.locale, MessageId::SetupActionScrollBody).to_string(), |
| 3066 | ), |
| 3067 | ]; |
| 3068 | if self.selected_step() == SetupStep::Constitution { |
| 3069 | hints.push(ActionHint::new( |
| 3070 | "1-6", |
| 3071 | tr(self.locale, MessageId::SetupActionTuneGuided).to_string(), |
| 3072 | )); |
| 3073 | if self.facts.provider_ready { |
| 3074 | hints.push(ActionHint::new( |
| 3075 | "A", |
| 3076 | tr(self.locale, MessageId::SetupActionModelDraft).to_string(), |
| 3077 | )); |
| 3078 | } |
| 3079 | hints.push(ActionHint::new( |
| 3080 | "G", |
| 3081 | tr(self.locale, MessageId::SetupActionGuided).to_string(), |
| 3082 | )); |
| 3083 | hints.push(ActionHint::new( |
| 3084 | "F", |
| 3085 | tr(self.locale, MessageId::SetupActionFreeform).to_string(), |
| 3086 | )); |
| 3087 | if self.facts.constitution_file == SetupConstitutionFileState::Loaded { |
| 3088 | hints.push(ActionHint::new( |
| 3089 | "K", |
| 3090 | tr(self.locale, MessageId::SetupActionKeepExisting).to_string(), |
| 3091 | )); |
| 3092 | } |
| 3093 | } else if self.selected_step() == SetupStep::ProviderModel { |
| 3094 | hints.push(ActionHint::new( |
| 3095 | "P", |
| 3096 | tr(self.locale, MessageId::SetupActionProvider).to_string(), |
| 3097 | )); |
| 3098 | hints.push(ActionHint::new( |
| 3099 | "M", |
| 3100 | tr(self.locale, MessageId::SetupActionModel).to_string(), |
| 3101 | )); |
| 3102 | } else if self.selected_step() == SetupStep::OperateFleet { |
| 3103 | hints.push(ActionHint::new( |
| 3104 | "P", |
| 3105 | tr(self.locale, MessageId::SetupActionProvider).to_string(), |
| 3106 | )); |
| 3107 | hints.push(ActionHint::new( |
| 3108 | "F", |
| 3109 | tr(self.locale, MessageId::SetupActionFleet).to_string(), |
| 3110 | )); |
| 3111 | } else if self.selected_step() == SetupStep::Hotbar { |
| 3112 | hints.push(ActionHint::new( |
| 3113 | "H", |
| 3114 | tr(self.locale, MessageId::SetupActionHotbar).to_string(), |
| 3115 | )); |
| 3116 | } else if self.selected_step() == SetupStep::RemoteRuntime { |
| 3117 | hints.push(ActionHint::new( |
| 3118 | "R", |
| 3119 | tr(self.locale, MessageId::SetupActionRemote).to_string(), |
| 3120 | )); |
| 3121 | } else if self.selected_step() == SetupStep::TrustSandbox { |
| 3122 | hints.push(ActionHint::new( |
| 3123 | "1-3", |
| 3124 | tr(self.locale, MessageId::SetupActionRuntimePreset).to_string(), |
| 3125 | )); |
| 3126 | hints.push(ActionHint::new( |
| 3127 | "A", |
| 3128 | tr(self.locale, MessageId::SetupActionApplyRuntimePreset).to_string(), |
| 3129 | )); |
| 3130 | hints.push(ActionHint::new( |
| 3131 | "M", |
| 3132 | tr(self.locale, MessageId::SetupActionMode).to_string(), |
| 3133 | )); |
| 3134 | hints.push(ActionHint::new( |
| 3135 | "C", |
| 3136 | tr(self.locale, MessageId::SetupActionConfig).to_string(), |
| 3137 | )); |
| 3138 | } |
| 3139 | hints.extend([ |
| 3140 | ActionHint::new( |
| 3141 | "U", |
| 3142 | tr(self.locale, MessageId::SetupActionUseBundled).to_string(), |
| 3143 | ), |
| 3144 | ActionHint::new( |
| 3145 | "D", |
| 3146 | tr(self.locale, MessageId::SetupActionDefer).to_string(), |
| 3147 | ), |
| 3148 | ActionHint::new( |
| 3149 | "Esc", |
| 3150 | tr(self.locale, MessageId::SetupActionCancel).to_string(), |
| 3151 | ), |
| 3152 | ]); |
| 3153 | let content_area = render_modal_footer(inner, buf, &hints); |
| 3154 | let spec = self.selected_spec(); |
| 3155 | let mut lines = vec![ |
| 3156 | Line::from(Span::styled( |
| 3157 | tr(self.locale, spec.title_id()).to_string(), |
| 3158 | Style::default() |
| 3159 | .fg(palette::WHALE_INFO) |
| 3160 | .add_modifier(Modifier::BOLD), |
| 3161 | )), |
| 3162 | Line::from(""), |
| 3163 | Line::from(Span::raw(tr(self.locale, spec.why_id()).to_string())), |
| 3164 | Line::from(""), |
| 3165 | ]; |
| 3166 | lines.extend(self.selected_step_detail_lines()); |
| 3167 | lines.push(Line::from("")); |
| 3168 | lines.push(Line::from(Span::styled( |
| 3169 | tr(self.locale, MessageId::SetupWizardWhy).to_string(), |
| 3170 | Style::default().fg(palette::TEXT_MUTED), |
| 3171 | ))); |
| 3172 | lines.push(Line::from("")); |
| 3173 | for (idx, step) in STEP_SPECS.iter().enumerate() { |
| 3174 | let selected = idx == self.selected; |
| 3175 | let status = self.state.status(step.id()); |
| 3176 | let status_color = match status { |
| 3177 | StepStatus::InProgress => palette::WHALE_LIVE, |
| 3178 | StepStatus::NeedsAction => palette::WHALE_HUMAN, |
| 3179 | StepStatus::Verified => palette::STATUS_SUCCESS, |
| 3180 | StepStatus::Failed => palette::STATUS_ERROR, |
| 3181 | StepStatus::NotStarted |
| 3182 | | StepStatus::Recommended |
| 3183 | | StepStatus::Optional |
| 3184 | | StepStatus::Deferred |
| 3185 | | StepStatus::Skipped => palette::TEXT_MUTED, |
| 3186 | }; |
| 3187 | let marker = crate::tui::glyphs::selection_marker(selected); |
| 3188 | let style = if selected { |
| 3189 | Style::default() |
| 3190 | .fg(palette::TEXT_PRIMARY) |
| 3191 | .add_modifier(Modifier::BOLD) |
| 3192 | } else { |
| 3193 | Style::default().fg(palette::TEXT_MUTED) |
| 3194 | }; |
| 3195 | lines.push(Line::from(vec![ |
| 3196 | Span::styled(format!("{marker} "), style), |
| 3197 | Span::styled(tr(self.locale, step.title_id()).to_string(), style), |
| 3198 | Span::raw(" "), |
| 3199 | Span::styled( |
| 3200 | self.status_label(status).to_string(), |
| 3201 | Style::default().fg(status_color), |
| 3202 | ), |
| 3203 | ])); |
| 3204 | } |
| 3205 | lines.push(Line::from("")); |
| 3206 | lines.push(Line::from(Span::raw( |
| 3207 | tr(self.locale, MessageId::SetupCheckpointLayerOrder).to_string(), |
| 3208 | ))); |
| 3209 | let wrap_width = usize::from(content_area.width).max(1); |
| 3210 | let visual_rows: usize = lines |
| 3211 | .iter() |
| 3212 | .map(|line| line.width().div_ceil(wrap_width).max(1)) |
| 3213 | .sum(); |
| 3214 | let visible_rows = usize::from(content_area.height).max(1); |
| 3215 | let max_scroll = visual_rows.saturating_sub(visible_rows); |
| 3216 | let scroll = self.body_scroll.min(max_scroll); |
| 3217 | let content_area = |
| 3218 | render_panel_scroll_rail(content_area, buf, visual_rows, scroll, visible_rows, true); |
| 3219 | Paragraph::new(lines) |
| 3220 | .wrap(Wrap { trim: false }) |
| 3221 | .scroll((scroll as u16, 0)) |
| 3222 | .render(content_area, buf); |
| 3223 | } |
| 3224 | |
| 3225 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { |
| 3226 | self |
| 3227 | } |
| 3228 | } |
| 3229 | |
| 3230 | impl SetupWizardView { |
| 3231 | fn selected_step_detail_lines(&self) -> Vec<Line<'static>> { |
| 3232 | match self.selected_step() { |
| 3233 | SetupStep::ProviderModel => self.provider_model_detail_lines(), |
| 3234 | SetupStep::TrustSandbox => self.runtime_posture_detail_lines(), |
| 3235 | SetupStep::Constitution => self.constitution_detail_lines(), |
| 3236 | SetupStep::OperateFleet => self.operate_fleet_detail_lines(), |
| 3237 | SetupStep::Hotbar => self.hotbar_detail_lines(), |
| 3238 | SetupStep::ToolsMcp => self.tools_mcp_detail_lines(), |
| 3239 | SetupStep::RemoteRuntime => self.remote_runtime_detail_lines(), |
| 3240 | SetupStep::Persistence => self.persistence_detail_lines(), |
| 3241 | SetupStep::Verification => self.verification_detail_lines(), |
| 3242 | _ => Vec::new(), |
| 3243 | } |
| 3244 | } |
| 3245 | |
| 3246 | fn provider_model_detail_lines(&self) -> Vec<Line<'static>> { |
| 3247 | vec![ |
| 3248 | self.detail_row(MessageId::SetupCardRouteLabel, &self.facts.provider), |
| 3249 | self.detail_row(MessageId::SetupCardModelLabel, &self.facts.model), |
| 3250 | self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth), |
| 3251 | self.detail_row(MessageId::SetupCardHealthLabel, &self.facts.health), |
| 3252 | Line::from(Span::styled( |
| 3253 | tr( |
| 3254 | self.locale, |
| 3255 | if self.facts.provider_ready { |
| 3256 | MessageId::SetupProviderModelReadyHint |
| 3257 | } else { |
| 3258 | MessageId::SetupProviderModelNeedsActionHint |
| 3259 | }, |
| 3260 | ) |
| 3261 | .to_string(), |
| 3262 | Style::default().fg(palette::TEXT_MUTED), |
| 3263 | )), |
| 3264 | ] |
| 3265 | } |
| 3266 | |
| 3267 | fn constitution_detail_lines(&self) -> Vec<Line<'static>> { |
| 3268 | let choice = constitution_choice_label(self.state.constitution_choice); |
| 3269 | let source = constitution_source_label(self.state.constitution_source); |
| 3270 | let validity = constitution_validity_label(self.state.constitution_validity); |
| 3271 | let source_state = format!("{source}; validity {validity}"); |
| 3272 | let existing_file = self |
| 3273 | .facts |
| 3274 | .constitution_file |
| 3275 | .label(self.state.constitution_choice, self.locale); |
| 3276 | let expert_override = self.facts.expert_override.label(self.locale); |
| 3277 | let preview = self |
| 3278 | .state |
| 3279 | .constitution_preview_hash |
| 3280 | .as_deref() |
| 3281 | .unwrap_or("not accepted yet") |
| 3282 | .to_string(); |
| 3283 | let mut lines = vec![ |
| 3284 | self.detail_row(MessageId::SetupConstitutionChoiceLabel, choice), |
| 3285 | self.detail_row(MessageId::SetupConstitutionSourceLabel, &source_state), |
| 3286 | self.detail_row(MessageId::SetupConstitutionPreviewLabel, &preview), |
| 3287 | self.detail_row(MessageId::SetupConstitutionExistingLabel, &existing_file), |
| 3288 | self.detail_row( |
| 3289 | MessageId::SetupConstitutionExpertOverrideLabel, |
| 3290 | &expert_override, |
| 3291 | ), |
| 3292 | Line::from(Span::styled( |
| 3293 | tr(self.locale, MessageId::SetupConstitutionGuidedAnswersHint).to_string(), |
| 3294 | Style::default().fg(palette::TEXT_MUTED), |
| 3295 | )), |
| 3296 | self.guided_answer_pair( |
| 3297 | ( |
| 3298 | "1", |
| 3299 | MessageId::SetupConstitutionPurposeLabel, |
| 3300 | &self.guided_draft.purpose.label(self.locale), |
| 3301 | ), |
| 3302 | ( |
| 3303 | "2", |
| 3304 | MessageId::SetupConstitutionAutonomyLabel, |
| 3305 | autonomy_label(self.guided_draft.autonomy, self.locale), |
| 3306 | ), |
| 3307 | ), |
| 3308 | self.guided_answer_pair( |
| 3309 | ( |
| 3310 | "3", |
| 3311 | MessageId::SetupConstitutionEvidenceLabel, |
| 3312 | &self.guided_draft.evidence.label(self.locale), |
| 3313 | ), |
| 3314 | ( |
| 3315 | "4", |
| 3316 | MessageId::SetupConstitutionCommunicationLabel, |
| 3317 | self.guided_draft.communication.label(self.locale), |
| 3318 | ), |
| 3319 | ), |
| 3320 | self.guided_answer_single( |
| 3321 | "5", |
| 3322 | MessageId::SetupConstitutionPrivacyLabel, |
| 3323 | self.guided_draft.privacy.label(self.locale), |
| 3324 | ), |
| 3325 | self.guided_answer_single( |
| 3326 | "6", |
| 3327 | MessageId::SetupConstitutionPrinciplesLabel, |
| 3328 | self.guided_draft.principles.label(self.locale), |
| 3329 | ), |
| 3330 | freeform_note_line(self.locale, &self.freeform_note, self.editing_freeform_note), |
| 3331 | ]; |
| 3332 | if self.facts.constitution_file == SetupConstitutionFileState::Loaded { |
| 3333 | lines.push(Line::from(Span::styled( |
| 3334 | keep_existing_invitation_line(self.locale), |
| 3335 | Style::default().fg(palette::WHALE_HUMAN), |
| 3336 | ))); |
| 3337 | } |
| 3338 | if let Some(label) = self |
| 3339 | .model_draft_label |
| 3340 | .as_deref() |
| 3341 | .filter(|_| self.model_draft.is_some()) |
| 3342 | { |
| 3343 | lines.push(Line::from(Span::styled( |
| 3344 | model_draft_ready_line(self.locale, label), |
| 3345 | Style::default().fg(palette::STATUS_SUCCESS), |
| 3346 | ))); |
| 3347 | } else if self.facts.provider_ready { |
| 3348 | lines.push(Line::from(Span::styled( |
| 3349 | model_draft_invitation_line(self.locale, &self.facts.model), |
| 3350 | Style::default().fg(palette::WHALE_HUMAN), |
| 3351 | ))); |
| 3352 | } |
| 3353 | lines.push(Line::from(Span::styled( |
| 3354 | tr(self.locale, MessageId::SetupConstitutionGuidedHint).to_string(), |
| 3355 | Style::default().fg(palette::TEXT_MUTED), |
| 3356 | ))); |
| 3357 | lines |
| 3358 | } |
| 3359 | |
| 3360 | fn runtime_posture_detail_lines(&self) -> Vec<Line<'static>> { |
| 3361 | let project_override = self |
| 3362 | .facts |
| 3363 | .project_override_warning |
| 3364 | .clone() |
| 3365 | .unwrap_or_else(|| { |
| 3366 | tr(self.locale, MessageId::SetupRuntimeProjectOverrideNone).to_string() |
| 3367 | }); |
| 3368 | let mut lines = vec![ |
| 3369 | self.detail_row(MessageId::SetupCardIntentLabel, &self.facts.work_intent), |
| 3370 | self.detail_row(MessageId::SetupCardApprovalLabel, &self.facts.approval), |
| 3371 | self.detail_row(MessageId::SetupCardShellLabel, &self.facts.shell), |
| 3372 | self.detail_row(MessageId::SetupCardTrustLabel, &self.facts.trust), |
| 3373 | self.detail_row(MessageId::SetupCardSandboxLabel, &self.facts.sandbox), |
| 3374 | self.detail_row(MessageId::SetupCardNetworkLabel, &self.facts.network), |
| 3375 | self.detail_row( |
| 3376 | MessageId::SetupRuntimePresetSelectedLabel, |
| 3377 | &runtime_preset_summary(self.locale, self.runtime_preset), |
| 3378 | ), |
| 3379 | self.detail_row( |
| 3380 | MessageId::SetupRuntimePresetDiffLabel, |
| 3381 | &runtime_preset_inline_diff(self.runtime_preset, &self.facts), |
| 3382 | ), |
| 3383 | self.detail_row( |
| 3384 | MessageId::SetupRuntimeProjectOverrideLabel, |
| 3385 | &project_override, |
| 3386 | ), |
| 3387 | Line::from(Span::styled( |
| 3388 | tr(self.locale, MessageId::SetupRuntimePostureBoundary).to_string(), |
| 3389 | Style::default().fg(palette::TEXT_MUTED), |
| 3390 | )), |
| 3391 | Line::from(Span::styled( |
| 3392 | tr(self.locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(), |
| 3393 | Style::default().fg(palette::TEXT_MUTED), |
| 3394 | )), |
| 3395 | self.setup_review_hint_line( |
| 3396 | MessageId::SetupRuntimePostureReviewHint, |
| 3397 | Some("Press M for work mode or C for config."), |
| 3398 | ), |
| 3399 | Line::from(Span::styled( |
| 3400 | tr(self.locale, MessageId::SetupRuntimePresetApplyHint).to_string(), |
| 3401 | Style::default().fg(palette::TEXT_MUTED), |
| 3402 | )), |
| 3403 | ]; |
| 3404 | for (idx, preset) in SetupRuntimePreset::ALL.iter().enumerate() { |
| 3405 | let marker = if *preset == self.runtime_preset { |
| 3406 | ">" |
| 3407 | } else { |
| 3408 | " " |
| 3409 | }; |
| 3410 | lines.push(Line::from(Span::styled( |
| 3411 | format!( |
| 3412 | "{marker} {}. {}", |
| 3413 | idx + 1, |
| 3414 | runtime_preset_summary(self.locale, *preset) |
| 3415 | ), |
| 3416 | Style::default().fg(if *preset == self.runtime_preset { |
| 3417 | palette::TEXT_PRIMARY |
| 3418 | } else { |
| 3419 | palette::TEXT_MUTED |
| 3420 | }), |
| 3421 | ))); |
| 3422 | } |
| 3423 | lines |
| 3424 | } |
| 3425 | |
| 3426 | fn operate_fleet_detail_lines(&self) -> Vec<Line<'static>> { |
| 3427 | let route = format!("{} / {}", self.facts.provider, self.facts.model); |
| 3428 | let readiness = self.ready_label(self.operate_fleet_facts_ready()); |
| 3429 | vec![ |
| 3430 | self.detail_row(MessageId::SetupCardRouteLabel, &route), |
| 3431 | self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth), |
| 3432 | self.detail_row( |
| 3433 | MessageId::SetupOperateRuntimeLabel, |
| 3434 | &self.facts.operate_runtime_result, |
| 3435 | ), |
| 3436 | self.detail_row( |
| 3437 | MessageId::SetupOperateRosterLabel, |
| 3438 | &self.facts.fleet_roster_result, |
| 3439 | ), |
| 3440 | self.detail_row( |
| 3441 | MessageId::SetupOperateConcurrencyLabel, |
| 3442 | &self.facts.operate_concurrency_result, |
| 3443 | ), |
| 3444 | self.detail_row(MessageId::SetupOperateReadinessLabel, &readiness), |
| 3445 | self.setup_review_hint_line(MessageId::SetupOperateReviewHint, None), |
| 3446 | ] |
| 3447 | } |
| 3448 | |
| 3449 | fn hotbar_detail_lines(&self) -> Vec<Line<'static>> { |
| 3450 | vec![ |
| 3451 | self.detail_row( |
| 3452 | MessageId::SetupHotbarBindingsLabel, |
| 3453 | &self.facts.hotbar_bindings_result, |
| 3454 | ), |
| 3455 | self.detail_row( |
| 3456 | MessageId::SetupHotbarActionsLabel, |
| 3457 | &self.facts.hotbar_actions_result, |
| 3458 | ), |
| 3459 | self.setup_review_hint_line( |
| 3460 | MessageId::SetupHotbarReviewHint, |
| 3461 | Some("Press H to customize slots."), |
| 3462 | ), |
| 3463 | ] |
| 3464 | } |
| 3465 | |
| 3466 | fn tools_mcp_detail_lines(&self) -> Vec<Line<'static>> { |
| 3467 | vec![ |
| 3468 | self.detail_row( |
| 3469 | MessageId::SetupToolsMcpServersLabel, |
| 3470 | &self.facts.tools_mcp_servers_result, |
| 3471 | ), |
| 3472 | self.detail_row( |
| 3473 | MessageId::SetupToolsMcpSkillsLabel, |
| 3474 | &self.facts.tools_mcp_skills_result, |
| 3475 | ), |
| 3476 | self.detail_row( |
| 3477 | MessageId::SetupToolsMcpToolsLabel, |
| 3478 | &self.facts.tools_mcp_tools_result, |
| 3479 | ), |
| 3480 | self.detail_row( |
| 3481 | MessageId::SetupToolsMcpPluginsLabel, |
| 3482 | &self.facts.tools_mcp_plugins_result, |
| 3483 | ), |
| 3484 | self.detail_row( |
| 3485 | MessageId::SetupToolsMcpHotbarLabel, |
| 3486 | &self.facts.tools_mcp_hotbar_result, |
| 3487 | ), |
| 3488 | self.setup_review_hint_line( |
| 3489 | MessageId::SetupToolsMcpReviewHint, |
| 3490 | Some("Press R for safe on-ramps (no auto-run)."), |
| 3491 | ), |
| 3492 | ] |
| 3493 | } |
| 3494 | |
| 3495 | /// #3409: one row per mode, each carrying its own observed status. The |
| 3496 | /// registry counts stay available in the preview; the card itself answers |
| 3497 | /// "where can this be reached from?" in four plain lines. |
| 3498 | fn remote_runtime_detail_lines(&self) -> Vec<Line<'static>> { |
| 3499 | let mut lines = Vec::new(); |
| 3500 | for fact in &self.facts.remote_modes { |
| 3501 | lines.push(self.detail_row( |
| 3502 | fact.mode.label_id(), |
| 3503 | &format!( |
| 3504 | "{} · {}", |
| 3505 | tr(self.locale, fact.status.label_id()), |
| 3506 | fact.detail |
| 3507 | ), |
| 3508 | )); |
| 3509 | } |
| 3510 | if lines.is_empty() { |
| 3511 | lines.push(self.detail_row( |
| 3512 | MessageId::SetupRemoteModeLabel, |
| 3513 | &self.facts.remote_mode_result, |
| 3514 | )); |
| 3515 | } |
| 3516 | lines.push(self.detail_row( |
| 3517 | MessageId::SetupRemoteProvidersLabel, |
| 3518 | &self.facts.remote_providers_result, |
| 3519 | )); |
| 3520 | lines.push(self.setup_review_hint_line( |
| 3521 | MessageId::SetupRemoteReviewHint, |
| 3522 | Some("Press R to preview (nothing is written). Enter keeps local-only."), |
| 3523 | )); |
| 3524 | lines |
| 3525 | } |
| 3526 | |
| 3527 | fn persistence_detail_lines(&self) -> Vec<Line<'static>> { |
| 3528 | vec![ |
| 3529 | self.detail_row( |
| 3530 | MessageId::SetupPersistenceHomeLabel, |
| 3531 | &self.facts.persistence.home_result, |
| 3532 | ), |
| 3533 | self.detail_row( |
| 3534 | MessageId::SetupPersistenceConfigLabel, |
| 3535 | &self.facts.persistence.config_result, |
| 3536 | ), |
| 3537 | self.detail_row( |
| 3538 | MessageId::SetupPersistenceStateLabel, |
| 3539 | &self.facts.persistence.state_result, |
| 3540 | ), |
| 3541 | self.detail_row( |
| 3542 | MessageId::SetupPersistenceConstitutionLabel, |
| 3543 | &self.facts.persistence.constitution_result, |
| 3544 | ), |
| 3545 | self.detail_row( |
| 3546 | MessageId::SetupPersistenceMemoryLabel, |
| 3547 | &self.facts.persistence.memory_result, |
| 3548 | ), |
| 3549 | self.detail_row( |
| 3550 | MessageId::SetupPersistenceNotesLabel, |
| 3551 | &self.facts.persistence.notes_result, |
| 3552 | ), |
| 3553 | self.setup_review_hint_line(MessageId::SetupPersistenceReviewHint, None), |
| 3554 | ] |
| 3555 | } |
| 3556 | |
| 3557 | fn verification_detail_lines(&self) -> Vec<Line<'static>> { |
| 3558 | let mut lines = vec![ |
| 3559 | self.detail_row( |
| 3560 | MessageId::SetupReportFirstRunLabel, |
| 3561 | &self.ready_label(self.state.first_run_ready()), |
| 3562 | ), |
| 3563 | self.detail_row( |
| 3564 | MessageId::SetupReportUpdateLabel, |
| 3565 | &self.ready_label(self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION)), |
| 3566 | ), |
| 3567 | self.detail_row( |
| 3568 | MessageId::SetupReportOperateLabel, |
| 3569 | &self.ready_label(self.state.operate_ready()), |
| 3570 | ), |
| 3571 | self.detail_row( |
| 3572 | MessageId::SetupReportSourceLabel, |
| 3573 | &self.state_source_label(), |
| 3574 | ), |
| 3575 | self.detail_row( |
| 3576 | MessageId::SetupReportAutonomyLabel, |
| 3577 | &self.facts.constitution_autonomy, |
| 3578 | ), |
| 3579 | self.detail_row( |
| 3580 | MessageId::SetupReportRuntimePostureLabel, |
| 3581 | &self.facts.runtime_result, |
| 3582 | ), |
| 3583 | Line::from(""), |
| 3584 | Line::from(Span::styled( |
| 3585 | tr(self.locale, MessageId::SetupReportRowsLabel).to_string(), |
| 3586 | Style::default() |
| 3587 | .fg(palette::TEXT_MUTED) |
| 3588 | .add_modifier(Modifier::BOLD), |
| 3589 | )), |
| 3590 | ]; |
| 3591 | |
| 3592 | for spec in STEP_SPECS { |
| 3593 | let step = spec.id(); |
| 3594 | let entry = self.state.steps.get(&step); |
| 3595 | let required = entry.map_or(spec.required(), |entry| entry.required); |
| 3596 | let required_label = if required { |
| 3597 | tr(self.locale, MessageId::SetupReportRequired) |
| 3598 | } else { |
| 3599 | tr(self.locale, MessageId::SetupReportOptional) |
| 3600 | }; |
| 3601 | let mut value = format!( |
| 3602 | "{} ({})", |
| 3603 | self.status_label(self.state.status(step)), |
| 3604 | required_label |
| 3605 | ); |
| 3606 | if let Some(version) = entry.and_then(|entry| entry.version.as_deref()) { |
| 3607 | value.push_str(&format!(" · {version}")); |
| 3608 | } |
| 3609 | if let Some(result) = entry.and_then(|entry| entry.result.as_deref()) { |
| 3610 | value.push_str(&format!(" · {result}")); |
| 3611 | } |
| 3612 | lines.push(self.detail_row(spec.title_id(), &value)); |
| 3613 | } |
| 3614 | |
| 3615 | lines.push(Line::from("")); |
| 3616 | let next_action = tr(self.locale, self.next_action_id()).to_string(); |
| 3617 | lines.push(self.detail_row(MessageId::SetupReportNextActionLabel, &next_action)); |
| 3618 | lines |
| 3619 | } |
| 3620 | |
| 3621 | fn setup_review_hint_line( |
| 3622 | &self, |
| 3623 | hint_id: MessageId, |
| 3624 | english_action: Option<&'static str>, |
| 3625 | ) -> Line<'static> { |
| 3626 | let hint = if self.locale == Locale::En { |
| 3627 | let mut hint = "Enter records this setup snapshot.".to_string(); |
| 3628 | if let Some(action) = english_action { |
| 3629 | hint.push(' '); |
| 3630 | hint.push_str(action); |
| 3631 | } |
| 3632 | hint |
| 3633 | } else { |
| 3634 | tr(self.locale, hint_id).to_string() |
| 3635 | }; |
| 3636 | Line::from(Span::styled(hint, Style::default().fg(palette::TEXT_MUTED))) |
| 3637 | } |
| 3638 | |
| 3639 | fn ready_label(&self, ready: bool) -> String { |
| 3640 | if ready { |
| 3641 | tr(self.locale, MessageId::SetupReportReady).to_string() |
| 3642 | } else { |
| 3643 | tr(self.locale, MessageId::SetupStatusNeedsAction).to_string() |
| 3644 | } |
| 3645 | } |
| 3646 | |
| 3647 | fn state_source_label(&self) -> String { |
| 3648 | if self.state.inherited { |
| 3649 | tr(self.locale, MessageId::SetupReportInherited).to_string() |
| 3650 | } else { |
| 3651 | tr(self.locale, MessageId::SetupReportPersisted).to_string() |
| 3652 | } |
| 3653 | } |
| 3654 | |
| 3655 | fn next_action_id(&self) -> MessageId { |
| 3656 | if !self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) { |
| 3657 | return MessageId::SetupReportNextActionConstitution; |
| 3658 | } |
| 3659 | if !matches!( |
| 3660 | self.state.status(SetupStep::ProviderModel), |
| 3661 | StepStatus::Verified | StepStatus::NeedsAction |
| 3662 | ) { |
| 3663 | return MessageId::SetupReportNextActionProvider; |
| 3664 | } |
| 3665 | if !self.state.runtime_posture_source.is_reviewed() { |
| 3666 | return MessageId::SetupReportNextActionRuntime; |
| 3667 | } |
| 3668 | if !self.state.first_run_ready() { |
| 3669 | return MessageId::SetupReportNextActionRequired; |
| 3670 | } |
| 3671 | if !self.state.operate_ready() { |
| 3672 | return MessageId::SetupReportNextActionOperate; |
| 3673 | } |
| 3674 | MessageId::SetupReportNextActionNone |
| 3675 | } |
| 3676 | |
| 3677 | fn detail_row(&self, label: MessageId, value: &str) -> Line<'static> { |
| 3678 | Line::from(vec![ |
| 3679 | Span::styled( |
| 3680 | format!("{} ", tr(self.locale, label)), |
| 3681 | Style::default() |
| 3682 | .fg(palette::TEXT_MUTED) |
| 3683 | .add_modifier(Modifier::BOLD), |
| 3684 | ), |
| 3685 | Span::raw(value.to_string()), |
| 3686 | ]) |
| 3687 | } |
| 3688 | |
| 3689 | fn guided_answer_pair( |
| 3690 | &self, |
| 3691 | left: (&str, MessageId, &str), |
| 3692 | right: (&str, MessageId, &str), |
| 3693 | ) -> Line<'static> { |
| 3694 | let label_style = Style::default() |
| 3695 | .fg(palette::TEXT_MUTED) |
| 3696 | .add_modifier(Modifier::BOLD); |
| 3697 | Line::from(vec![ |
| 3698 | Span::styled( |
| 3699 | format!("{} {} ", left.0, tr(self.locale, left.1)), |
| 3700 | label_style, |
| 3701 | ), |
| 3702 | Span::raw(left.2.to_string()), |
| 3703 | Span::styled(" · ", Style::default().fg(palette::TEXT_MUTED)), |
| 3704 | Span::styled( |
| 3705 | format!("{} {} ", right.0, tr(self.locale, right.1)), |
| 3706 | label_style, |
| 3707 | ), |
| 3708 | Span::raw(right.2.to_string()), |
| 3709 | ]) |
| 3710 | } |
| 3711 | |
| 3712 | fn guided_answer_single(&self, key: &str, label: MessageId, value: &str) -> Line<'static> { |
| 3713 | Line::from(vec![ |
| 3714 | Span::styled( |
| 3715 | format!("{key} {} ", tr(self.locale, label)), |
| 3716 | Style::default() |
| 3717 | .fg(palette::TEXT_MUTED) |
| 3718 | .add_modifier(Modifier::BOLD), |
| 3719 | ), |
| 3720 | Span::raw(value.to_string()), |
| 3721 | ]) |
| 3722 | } |
| 3723 | } |
| 3724 | |
| 3725 | fn setup_report_ready(state: &SetupState) -> bool { |
| 3726 | state.first_run_ready() || state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) |
| 3727 | } |
| 3728 | |
| 3729 | fn runtime_preset_summary(locale: Locale, preset: SetupRuntimePreset) -> String { |
| 3730 | format!( |
| 3731 | "{} - {}", |
| 3732 | tr(locale, preset.title_id()), |
| 3733 | tr(locale, preset.description_id()) |
| 3734 | ) |
| 3735 | } |
| 3736 | |
| 3737 | fn runtime_preset_inline_diff(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> String { |
| 3738 | runtime_preset_diff_rows(preset, facts).join("; ") |
| 3739 | } |
| 3740 | |
| 3741 | fn runtime_preset_preview_text( |
| 3742 | locale: Locale, |
| 3743 | preset: SetupRuntimePreset, |
| 3744 | facts: &SetupRuntimeFacts, |
| 3745 | ) -> String { |
| 3746 | let mut lines = vec![ |
| 3747 | tr(locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(), |
| 3748 | runtime_preset_summary(locale, preset), |
| 3749 | String::new(), |
| 3750 | tr(locale, MessageId::SetupRuntimePresetDiffLabel).to_string(), |
| 3751 | ]; |
| 3752 | lines.extend( |
| 3753 | runtime_preset_diff_rows(preset, facts) |
| 3754 | .into_iter() |
| 3755 | .map(|row| format!("- {row}")), |
| 3756 | ); |
| 3757 | lines.extend([ |
| 3758 | String::new(), |
| 3759 | tr(locale, MessageId::SetupRuntimePostureBoundary).to_string(), |
| 3760 | tr(locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(), |
| 3761 | tr(locale, MessageId::SetupRuntimePresetApplyHint).to_string(), |
| 3762 | ]); |
| 3763 | lines.join("\n") |
| 3764 | } |
| 3765 | |
| 3766 | fn runtime_preset_diff_rows(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> Vec<String> { |
| 3767 | let approval_target = preset.approval_policy().map_or_else( |
| 3768 | || "removed; Full Access comes from settings.permission_posture".to_string(), |
| 3769 | ToString::to_string, |
| 3770 | ); |
| 3771 | let mut rows = vec![ |
| 3772 | format!( |
| 3773 | "settings.default_mode: {} -> {}", |
| 3774 | facts.default_mode, |
| 3775 | preset.display_mode() |
| 3776 | ), |
| 3777 | format!( |
| 3778 | "settings.permission_posture: -> {}", |
| 3779 | preset.permission_posture() |
| 3780 | ), |
| 3781 | format!( |
| 3782 | "config.approval_policy: {} -> {}", |
| 3783 | facts.approval_policy_value, approval_target |
| 3784 | ), |
| 3785 | format!( |
| 3786 | "config.allow_shell: {} -> {}", |
| 3787 | facts.allow_shell_enabled, |
| 3788 | preset.allow_shell() |
| 3789 | ), |
| 3790 | format!( |
| 3791 | "config.sandbox_mode: {} -> {}", |
| 3792 | facts.sandbox_mode_value, |
| 3793 | preset.sandbox_mode() |
| 3794 | ), |
| 3795 | format!( |
| 3796 | "config.network.default: {} -> unchanged", |
| 3797 | facts.network_default_value |
| 3798 | ), |
| 3799 | format!("workspace trust: {} -> unchanged", facts.trust), |
| 3800 | ]; |
| 3801 | if let Some(warning) = facts.project_override_warning.as_deref() { |
| 3802 | rows.push(format!("project override warning: {warning}")); |
| 3803 | } |
| 3804 | rows |
| 3805 | } |
| 3806 | |
| 3807 | fn project_runtime_override_warning(workspace: &Path, locale: Locale) -> Option<String> { |
| 3808 | let outcome = codewhale_config::load_project_config_outcome(workspace); |
| 3809 | // A project config that exists but can't be parsed is not the same as no |
| 3810 | // project config: its restrictions are silently not in effect, and the |
| 3811 | // workspace falls back to the user's baseline. Say so here rather than |
| 3812 | // only in a log line the TUI never shows. |
| 3813 | if let Some((path, reason)) = outcome.invalid() { |
| 3814 | let path = path.display(); |
| 3815 | return Some(match locale { |
| 3816 | Locale::ZhHans => format!( |
| 3817 | "无法解析项目配置 {path}({reason})。此工作区的项目级运行姿态限制未生效,将回退到用户默认值。", |
| 3818 | ), |
| 3819 | _ => format!( |
| 3820 | "Project config {path} could not be parsed ({reason}). Its runtime posture restrictions are NOT in effect; this workspace falls back to your user defaults.", |
| 3821 | ), |
| 3822 | }); |
| 3823 | } |
| 3824 | let project = outcome.into_config()?; |
| 3825 | let mut fields = Vec::new(); |
| 3826 | if let Some(policy) = project.approval_policy.as_deref() { |
| 3827 | fields.push(format!("approval_policy={policy}")); |
| 3828 | } |
| 3829 | if let Some(mode) = project.sandbox_mode.as_deref() { |
| 3830 | fields.push(format!("sandbox_mode={mode}")); |
| 3831 | } |
| 3832 | if fields.is_empty() { |
| 3833 | return None; |
| 3834 | } |
| 3835 | Some(match locale { |
| 3836 | Locale::ZhHans => format!( |
| 3837 | "此工作区的项目配置包含 {}。预设会保存用户默认值;项目配置仍可在此工作区收紧运行姿态。", |
| 3838 | fields.join(", ") |
| 3839 | ), |
| 3840 | _ => format!( |
| 3841 | "Project config contains {}. Presets save user defaults; project config can still tighten runtime posture in this workspace.", |
| 3842 | fields.join(", ") |
| 3843 | ), |
| 3844 | }) |
| 3845 | } |
| 3846 | |
| 3847 | fn setup_report_result(state: &SetupState, facts: &SetupRuntimeFacts) -> String { |
| 3848 | format!( |
| 3849 | "first_run={}, update={}, operate={}, constitution={:?}, autonomy={}, posture={:?}, runtime={}, operate_fleet={}", |
| 3850 | if state.first_run_ready() { |
| 3851 | "ready" |
| 3852 | } else { |
| 3853 | "needs_action" |
| 3854 | }, |
| 3855 | if state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) { |
| 3856 | "ready" |
| 3857 | } else { |
| 3858 | "needs_action" |
| 3859 | }, |
| 3860 | if state.operate_ready() { |
| 3861 | "ready" |
| 3862 | } else { |
| 3863 | "needs_action" |
| 3864 | }, |
| 3865 | state.constitution_choice, |
| 3866 | facts.constitution_autonomy, |
| 3867 | state.runtime_posture_source, |
| 3868 | facts.runtime_result, |
| 3869 | facts.operate_result |
| 3870 | ) |
| 3871 | } |
| 3872 | |
| 3873 | fn remote_runtime_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String { |
| 3874 | remote::on_ramp_text( |
| 3875 | locale, |
| 3876 | &facts.remote_clouds_result, |
| 3877 | &facts.remote_bridges_result, |
| 3878 | &facts.remote_providers_result, |
| 3879 | &facts.remote_mode_result, |
| 3880 | &facts.remote_command_provider, |
| 3881 | ) |
| 3882 | } |
| 3883 | |
| 3884 | fn tools_mcp_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String { |
| 3885 | let tools_facts = tools_mcp::SetupToolsMcpFacts { |
| 3886 | servers_result: facts.tools_mcp_servers_result.clone(), |
| 3887 | skills_result: facts.tools_mcp_skills_result.clone(), |
| 3888 | tools_result: facts.tools_mcp_tools_result.clone(), |
| 3889 | plugins_result: facts.tools_mcp_plugins_result.clone(), |
| 3890 | hotbar_result: facts.tools_mcp_hotbar_result.clone(), |
| 3891 | result: facts.tools_mcp_result.clone(), |
| 3892 | overall_status: if facts.tools_mcp_needs_action { |
| 3893 | tools_mcp::InventoryStatus::NeedsConfig |
| 3894 | } else if facts.tools_mcp_result.contains("overall=off") { |
| 3895 | tools_mcp::InventoryStatus::Off |
| 3896 | } else { |
| 3897 | tools_mcp::InventoryStatus::Healthy |
| 3898 | }, |
| 3899 | needs_action: facts.tools_mcp_needs_action, |
| 3900 | mcp_path_display: facts.tools_mcp_path_display.clone(), |
| 3901 | skills_path_display: facts.tools_mcp_skills_path_display.clone(), |
| 3902 | plugins_path_display: facts.tools_mcp_plugins_path_display.clone(), |
| 3903 | }; |
| 3904 | tools_mcp::on_ramp_text(locale, &tools_facts) |
| 3905 | } |
| 3906 | |
| 3907 | #[cfg(test)] |
| 3908 | #[must_use] |
| 3909 | fn guided_constitution_template(locale: Locale) -> UserConstitution { |
| 3910 | GuidedConstitutionDraft::default().to_constitution(locale) |
| 3911 | } |
| 3912 | |
| 3913 | /// Who authored the draft being previewed for ratification. |
| 3914 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 3915 | enum DraftProvenance { |
| 3916 | /// Rendered deterministically from the guided answers. |
| 3917 | Guided, |
| 3918 | /// Drafted by the named model, then sanitized and bounded by Codewhale. |
| 3919 | Model(String), |
| 3920 | /// The user's existing `constitution.json`, shown unchanged for the |
| 3921 | /// keep-existing checkpoint completion (#3794). |
| 3922 | Existing, |
| 3923 | } |
| 3924 | |
| 3925 | fn ratification_preview_title(locale: Locale) -> &'static str { |
| 3926 | match locale { |
| 3927 | Locale::Ja => "ユーザー憲法 - 批准前の草案", |
| 3928 | Locale::ZhHans => "用户宪章 — 确认前草案", |
| 3929 | Locale::ZhHant => "使用者憲法 - 批准前草案", |
| 3930 | Locale::PtBr => "Constituição do Usuário - Rascunho para Ratificação", |
| 3931 | Locale::Es419 => "Constitución del Usuario - Borrador para Ratificación", |
| 3932 | Locale::Vi => "Hiến pháp Người dùng - Bản nháp để phê chuẩn", |
| 3933 | Locale::Ko => "사용자 헌법 - 승인 전 초안", |
| 3934 | Locale::Ca => "Constitució de l'Usuari - Esborrany per a Ratificació", |
| 3935 | Locale::De => "Nutzerverfassung - Entwurf zur Ratifizierung", |
| 3936 | Locale::Fr => "Constitution de l'Utilisateur - Brouillon pour Ratification", |
| 3937 | Locale::Id => "Konstitusi Pengguna - Draf untuk Ratifikasi", |
| 3938 | Locale::Hi => "उपयोगकर्ता संविधान - अंगीकार हेतु मसौदा", |
| 3939 | Locale::Ru => "Конституция пользователя - Проект для ратификации", |
| 3940 | Locale::Uk => "Конституція користувача - Проєкт для ратифікації", |
| 3941 | _ => "User Constitution — Draft for Ratification", |
| 3942 | } |
| 3943 | } |
| 3944 | |
| 3945 | /// The ratification artifact shown in the pager: provenance, what a |
| 3946 | /// constitution is, the exact block that will be injected (byte-identical to |
| 3947 | /// prompt assembly's rendering), its authority boundaries, and how to ratify |
| 3948 | /// or amend. Only the scaffold differs between guided and model drafts — the |
| 3949 | /// law itself always comes from the same renderer. |
| 3950 | fn constitution_ratification_text( |
| 3951 | locale: Locale, |
| 3952 | constitution: &UserConstitution, |
| 3953 | provenance: &DraftProvenance, |
| 3954 | ) -> String { |
| 3955 | const RULE: &str = "──────────────────────────────────────────────────────"; |
| 3956 | let rendered = constitution |
| 3957 | .render_block(None) |
| 3958 | .unwrap_or_else(|| match locale { |
| 3959 | Locale::Ja => "構造化された憲法は空です。".to_string(), |
| 3960 | Locale::ZhHans => "结构化宪章为空。".to_string(), |
| 3961 | Locale::ZhHant => "結構化憲法為空。".to_string(), |
| 3962 | Locale::PtBr => "A constituição estruturada está vazia.".to_string(), |
| 3963 | Locale::Es419 => "La constitución estructurada está vacía.".to_string(), |
| 3964 | Locale::Vi => "Hiến pháp có cấu trúc đang trống.".to_string(), |
| 3965 | Locale::Ko => "구조화된 헌법이 비어 있습니다.".to_string(), |
| 3966 | Locale::Ca => "La constitució estructurada és buida.".to_string(), |
| 3967 | Locale::De => "Die strukturierte Verfassung ist leer.".to_string(), |
| 3968 | Locale::Fr => "La constitution structurée est vide.".to_string(), |
| 3969 | Locale::Id => "Konstitusi terstruktur kosong.".to_string(), |
| 3970 | Locale::Hi => "संरचित संविधान खाली है।".to_string(), |
| 3971 | Locale::Ru => "Структурированная конституция пуста.".to_string(), |
| 3972 | Locale::Uk => "Структурована конституція порожня.".to_string(), |
| 3973 | _ => "The structured constitution is empty.".to_string(), |
| 3974 | }); |
| 3975 | let layer_order = tr(locale, MessageId::SetupCheckpointLayerOrder); |
| 3976 | |
| 3977 | match locale { |
| 3978 | Locale::Ja => { |
| 3979 | let drafted_by = match provenance { |
| 3980 | DraftProvenance::Model(label) => format!( |
| 3981 | "{label} があなたのガイド回答から起草し、Codewhale が構造検証と境界制限を適用しました。" |
| 3982 | ), |
| 3983 | DraftProvenance::Guided => { |
| 3984 | "あなたのガイド回答から決定的に生成されました。".to_string() |
| 3985 | } |
| 3986 | DraftProvenance::Existing => { |
| 3987 | "既存の憲法を constitution.json から読み込み、変更せずに表示しています。" |
| 3988 | .to_string() |
| 3989 | } |
| 3990 | }; |
| 3991 | let ratify_how = match provenance { |
| 3992 | DraftProvenance::Existing => { |
| 3993 | "これはすでに有効な基準です。プレビューを閉じて K を押すと、このまま保持してチェックポイントを完了します。\ |
| 3994 | ファイルは変更されません。/constitution または /setup でいつでも修正できます。" |
| 3995 | } |
| 3996 | _ => { |
| 3997 | "確認するまで、どの内容も基準にはなりません。プレビューを閉じて G を押すと批准して保存します。\ |
| 3998 | /constitution または /setup でいつでも修正できます。" |
| 3999 | } |
| 4000 | }; |
| 4001 | format!( |
| 4002 | "CODEWHALE · ユーザー憲法\n{RULE}\n\n{drafted_by}\n\n\ |
| 4003 | これは Codewhale があなたと協働するための常設の基準です。優れた憲法のように、使えるほど短く、\ |
| 4004 | 網羅的な規則ではなく持続する原則で構成され、あなたの変化に合わせて修正できます。\ |
| 4005 | すべての個別判断を裁くのではなく権限と境界を定め、セッションを越えて協働を継続させます。\ |
| 4006 | ただしこれは記憶ではありません。履歴ではなく原則を保持します。\n\n\ |
| 4007 | {rendered}\n\n\ |
| 4008 | 権限の階層\n{layer_order}\nあなたの直接の指示は常にこの文書より優先されます。\n\n\ |
| 4009 | これができないこと\n\ |
| 4010 | これは行動を導くものです。承認ポリシー、サンドボックス、Shell、ネットワーク、信頼、MCP 権限、\ |
| 4011 | 既定モード、公開、支出の権限を付与または変更することはできません。これらは実行時にあなたが管理します。\n\n\ |
| 4012 | 縮小コアと任意モジュール\n\ |
| 4013 | 組み込みのコアは引き続き有効です。この草案はユーザーグローバルの長期設定だけを保存します。\ |
| 4014 | 重い実行/オーケストレーション教義はモードプロンプトまたは将来の任意モジュールに属します。このプレビューはモジュールを有効化せず、設定も変更しません。\n\n\ |
| 4015 | 批准\n{ratify_how}" |
| 4016 | ) |
| 4017 | } |
| 4018 | Locale::ZhHans => { |
| 4019 | let drafted_by = match provenance { |
| 4020 | DraftProvenance::Model(label) => format!( |
| 4021 | "由 {label} 根据你的引导式答案起草,并已由 Codewhale 完成结构校验与边界限制。" |
| 4022 | ), |
| 4023 | DraftProvenance::Guided => "由你的引导式答案确定性生成。".to_string(), |
| 4024 | DraftProvenance::Existing => { |
| 4025 | "你现有的宪章,读取自 constitution.json——原样展示,未做任何修改。".to_string() |
| 4026 | } |
| 4027 | }; |
| 4028 | let ratify_how = match provenance { |
| 4029 | DraftProvenance::Existing => { |
| 4030 | "这已是你当前使用的宪章。关闭此预览后按 K 保留并完成检查点——文件不会被修改。\ |
| 4031 | 之后可随时用 /constitution 或 /setup 修改。" |
| 4032 | } |
| 4033 | _ => { |
| 4034 | "未经你确认,任何内容都不会成为宪章。关闭此预览后按 G 确认并保存;\ |
| 4035 | 之后可随时用 /constitution 或 /setup 修改。" |
| 4036 | } |
| 4037 | }; |
| 4038 | format!( |
| 4039 | "CODEWHALE · 用户宪章\n{RULE}\n\n{drafted_by}\n\n\ |
| 4040 | 这是 Codewhale 与你协作时长期遵循的偏好和规则。内容应保持简短、便于执行,以持久原则为主,并可随时调整。\ |
| 4041 | 它界定协作方式与行为边界,而不是替你决定每一种情况;它让协作跨会话延续——但它不是记忆,只保留原则,不保留历史。\n\n\ |
| 4042 | {rendered}\n\n\ |
| 4043 | 权限层级\n{layer_order}\n你的直接指令始终高于本文件。\n\n\ |
| 4044 | 它不能做什么\n\ |
| 4045 | 它只提供行为指导,不能授予或更改审批策略、沙箱、Shell、网络、信任、MCP 权限、默认模式、发布或支出权限——这些始终由你在运行时掌控。\n\n\ |
| 4046 | 精简核心与可选策略\n\ |
| 4047 | 内置核心始终生效。本草案只保存你的用户全局长期偏好。执行与编排等高级策略仍由模式提示词或未来的可选规则包管理;此预览不会启用任何策略或更改配置。\n\n\ |
| 4048 | 确认\n{ratify_how}" |
| 4049 | ) |
| 4050 | } |
| 4051 | Locale::ZhHant => { |
| 4052 | let drafted_by = match provenance { |
| 4053 | DraftProvenance::Model(label) => format!( |
| 4054 | "由 {label} 根據你的引導式答案起草,並已由 Codewhale 完成結構驗證與邊界限制。" |
| 4055 | ), |
| 4056 | DraftProvenance::Guided => "由你的引導式答案確定性生成。".to_string(), |
| 4057 | DraftProvenance::Existing => { |
| 4058 | "你現有的憲法,讀取自 constitution.json;原樣展示,未做任何修改。".to_string() |
| 4059 | } |
| 4060 | }; |
| 4061 | let ratify_how = match provenance { |
| 4062 | DraftProvenance::Existing => { |
| 4063 | "這已是你現行的準則。關閉此預覽後按 K 保留並完成檢查點;\ |
| 4064 | 檔案不會被修改。之後可隨時用 /constitution 或 /setup 修訂。" |
| 4065 | } |
| 4066 | _ => { |
| 4067 | "未經你確認,任何內容都不會成為準則。關閉此預覽後按 G 批准並保存;\ |
| 4068 | 之後可隨時用 /constitution 或 /setup 修訂。" |
| 4069 | } |
| 4070 | }; |
| 4071 | format!( |
| 4072 | "CODEWHALE · 使用者憲法\n{RULE}\n\n{drafted_by}\n\n\ |
| 4073 | 這是 Codewhale 與你協作的長期準則。像優秀的憲法一樣:足夠簡短因而可用,由持久原則而非詳盡規則構成,並且可以隨你修訂。\ |
| 4074 | 它界定權力與邊界,而非裁決每個具體決定;它讓協作跨會話延續,但它不是記憶,它承載的是原則,而非歷史。\n\n\ |
| 4075 | {rendered}\n\n\ |
| 4076 | 權限層級\n{layer_order}\n你的直接指令始終高於本文件。\n\n\ |
| 4077 | 它不能做什麼\n\ |
| 4078 | 它只提供行為指導,不能授予或更改審批策略、沙箱、Shell、網路、信任、MCP 權限、預設模式、發布或支出權限;這些始終由你在執行時掌控。\n\n\ |
| 4079 | 精簡核心與可選模組\n\ |
| 4080 | 內建核心始終生效。本草案只保存你的使用者全域長期偏好。執行/編排等重型教義位於模式提示詞或未來的可選模組中;此預覽不會啟用模組或更改其配置。\n\n\ |
| 4081 | 批准\n{ratify_how}" |
| 4082 | ) |
| 4083 | } |
| 4084 | Locale::PtBr => { |
| 4085 | let drafted_by = match provenance { |
| 4086 | DraftProvenance::Model(label) => format!( |
| 4087 | "Rascunhado por {label} a partir das suas respostas guiadas, depois validado por schema e limitado pelo Codewhale." |
| 4088 | ), |
| 4089 | DraftProvenance::Guided => { |
| 4090 | "Renderizado deterministicamente a partir das suas respostas guiadas.".to_string() |
| 4091 | } |
| 4092 | DraftProvenance::Existing => { |
| 4093 | "Sua constituição existente, carregada de constitution.json, é exibida sem alterações." |
| 4094 | .to_string() |
| 4095 | } |
| 4096 | }; |
| 4097 | let ratify_how = match provenance { |
| 4098 | DraftProvenance::Existing => { |
| 4099 | "Esta já é sua regra vigente. Feche a prévia e pressione K para mantê-la e concluir o checkpoint; \ |
| 4100 | o arquivo não será modificado. Edite quando quiser com /constitution ou /setup." |
| 4101 | } |
| 4102 | _ => { |
| 4103 | "Nada vira regra até você confirmar. Feche a prévia e pressione G para ratificar e salvar. \ |
| 4104 | Edite quando quiser com /constitution ou /setup." |
| 4105 | } |
| 4106 | }; |
| 4107 | format!( |
| 4108 | "CODEWHALE · CONSTITUIÇÃO DO USUÁRIO\n{RULE}\n\n{drafted_by}\n\n\ |
| 4109 | Esta é a regra permanente de como o Codewhale trabalha com você. Como boas constituições, \ |
| 4110 | ela é curta o bastante para ser usada, formada por princípios duráveis em vez de regras exaustivas, \ |
| 4111 | e pode ser emendada conforme você muda. Ela define poderes e limites em vez de decidir cada caso, \ |
| 4112 | e dá continuidade à colaboração entre sessões. Mas ela não é memória: carrega princípios, não histórico.\n\n\ |
| 4113 | {rendered}\n\n\ |
| 4114 | HIERARQUIA DE AUTORIDADE\n{layer_order}\nSeus pedidos diretos sempre superam este documento.\n\n\ |
| 4115 | O QUE ISTO NÃO PODE FAZER\n\ |
| 4116 | Isto orienta comportamento. Não pode conceder nem alterar política de aprovação, sandbox, shell, rede, \ |
| 4117 | confiança, permissões MCP, modo padrão, publicação ou autoridade para gastos; isso continua sob seu controle em tempo de execução.\n\n\ |
| 4118 | NÚCLEO REDUZIDO E MÓDULOS OPT-IN\n\ |
| 4119 | O núcleo embutido continua ativo. Este rascunho só salva suas preferências permanentes globais de usuário. \ |
| 4120 | Doutrina pesada de execução ou orquestração pertence a prompts de modo ou módulos opt-in futuros; esta prévia não ativa módulos nem muda sua configuração.\n\n\ |
| 4121 | RATIFICAÇÃO\n{ratify_how}" |
| 4122 | ) |
| 4123 | } |
| 4124 | Locale::Es419 => { |
| 4125 | let drafted_by = match provenance { |
| 4126 | DraftProvenance::Model(label) => format!( |
| 4127 | "Redactado por {label} desde tus respuestas guiadas, luego validado por schema y acotado por Codewhale." |
| 4128 | ), |
| 4129 | DraftProvenance::Guided => { |
| 4130 | "Renderizado de forma determinística desde tus respuestas guiadas.".to_string() |
| 4131 | } |
| 4132 | DraftProvenance::Existing => { |
| 4133 | "Tu constitución existente, cargada desde constitution.json, se muestra sin cambios." |
| 4134 | .to_string() |
| 4135 | } |
| 4136 | }; |
| 4137 | let ratify_how = match provenance { |
| 4138 | DraftProvenance::Existing => { |
| 4139 | "Esta ya es tu regla vigente. Cierra la vista previa y presiona K para conservarla y completar el checkpoint; \ |
| 4140 | el archivo no se modifica. Puedes enmendarla cuando quieras con /constitution o /setup." |
| 4141 | } |
| 4142 | _ => { |
| 4143 | "Nada se vuelve regla hasta que confirmes. Cierra la vista previa y presiona G para ratificar y guardar. \ |
| 4144 | Puedes enmendarla cuando quieras con /constitution o /setup." |
| 4145 | } |
| 4146 | }; |
| 4147 | format!( |
| 4148 | "CODEWHALE · CONSTITUCIÓN DEL USUARIO\n{RULE}\n\n{drafted_by}\n\n\ |
| 4149 | Esta es la regla permanente de cómo Codewhale trabaja contigo. Como las buenas constituciones, \ |
| 4150 | es lo bastante breve para usarse, hecha de principios duraderos en vez de reglas exhaustivas, \ |
| 4151 | y enmendable a medida que cambias. Define poderes y límites en vez de decidir cada caso, \ |
| 4152 | y da continuidad a la colaboración entre sesiones. Pero no es memoria: lleva principios, no historial.\n\n\ |
| 4153 | {rendered}\n\n\ |
| 4154 | JERARQUÍA DE AUTORIDAD\n{layer_order}\nTus pedidos directos siempre superan este documento.\n\n\ |
| 4155 | LO QUE ESTO NO PUEDE HACER\n\ |
| 4156 | Orienta comportamiento. No puede conceder ni cambiar política de aprobación, sandbox, shell, red, \ |
| 4157 | confianza, permisos MCP, modo predeterminado, publicación o autoridad de gasto; eso sigue bajo tu control en tiempo de ejecución.\n\n\ |
| 4158 | NÚCLEO REDUCIDO Y MÓDULOS OPT-IN\n\ |
| 4159 | El núcleo integrado sigue activo. Este borrador solo guarda tus preferencias permanentes globales de usuario. \ |
| 4160 | La doctrina pesada de ejecución u orquestación pertenece a prompts de modo o módulos opt-in futuros; esta vista previa no activa módulos ni cambia su configuración.\n\n\ |
| 4161 | RATIFICACIÓN\n{ratify_how}" |
| 4162 | ) |
| 4163 | } |
| 4164 | Locale::Vi => { |
| 4165 | let drafted_by = match provenance { |
| 4166 | DraftProvenance::Model(label) => format!( |
| 4167 | "Được {label} soạn từ câu trả lời hướng dẫn của bạn, rồi được Codewhale kiểm tra schema và giới hạn biên." |
| 4168 | ), |
| 4169 | DraftProvenance::Guided => { |
| 4170 | "Được kết xuất xác định từ câu trả lời hướng dẫn của bạn.".to_string() |
| 4171 | } |
| 4172 | DraftProvenance::Existing => { |
| 4173 | "Hiến pháp hiện có của bạn, tải từ constitution.json, được hiển thị nguyên trạng." |
| 4174 | .to_string() |
| 4175 | } |
| 4176 | }; |
| 4177 | let ratify_how = match provenance { |
| 4178 | DraftProvenance::Existing => { |
| 4179 | "Đây đã là luật hiện hành của bạn. Đóng bản xem trước rồi nhấn K để giữ nguyên và hoàn tất checkpoint; \ |
| 4180 | tệp không bị sửa. Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup." |
| 4181 | } |
| 4182 | _ => { |
| 4183 | "Không có gì trở thành luật cho đến khi bạn xác nhận. Đóng bản xem trước rồi nhấn G để phê chuẩn và lưu. \ |
| 4184 | Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup." |
| 4185 | } |
| 4186 | }; |
| 4187 | format!( |
| 4188 | "CODEWHALE · HIẾN PHÁP NGƯỜI DÙNG\n{RULE}\n\n{drafted_by}\n\n\ |
| 4189 | Đây là luật thường trực cho cách Codewhale làm việc với bạn. Giống các hiến pháp tốt, \ |
| 4190 | nó đủ ngắn để dùng, gồm các nguyên tắc bền vững thay vì luật lệ cạn kiệt, \ |
| 4191 | và có thể sửa khi bạn thay đổi. Nó định khung quyền hạn và giới hạn thay vì quyết định từng trường hợp, \ |
| 4192 | đồng thời giữ sự liên tục giữa các phiên. Nhưng nó không phải bộ nhớ: nó mang nguyên tắc, không mang lịch sử.\n\n\ |
| 4193 | {rendered}\n\n\ |
| 4194 | THỨ BẬC THẨM QUYỀN\n{layer_order}\nYêu cầu trực tiếp của bạn luôn cao hơn tài liệu này.\n\n\ |
| 4195 | ĐIỀU NÀY KHÔNG THỂ LÀM\n\ |
| 4196 | Nó hướng dẫn hành vi. Nó không thể cấp hoặc đổi chính sách phê duyệt, sandbox, shell, mạng, \ |
| 4197 | độ tin cậy, quyền MCP, chế độ mặc định, xuất bản hoặc quyền chi tiêu; những thứ đó vẫn do bạn kiểm soát lúc chạy.\n\n\ |
| 4198 | LÕI RÚT GỌN VÀ MÔ-ĐUN OPT-IN\n\ |
| 4199 | Lõi tích hợp vẫn hoạt động. Bản nháp này chỉ lưu tùy chọn thường trực toàn cục của người dùng. \ |
| 4200 | Giáo điều thực thi hoặc điều phối nặng thuộc về prompt chế độ hoặc mô-đun opt-in trong tương lai; bản xem trước này không bật mô-đun hoặc đổi cấu hình của chúng.\n\n\ |
| 4201 | PHÊ CHUẨN\n{ratify_how}" |
| 4202 | ) |
| 4203 | } |
| 4204 | Locale::Ko => { |
| 4205 | let drafted_by = match provenance { |
| 4206 | DraftProvenance::Model(label) => format!( |
| 4207 | "{label}이(가) 당신의 가이드 답변을 바탕으로 초안을 작성했고, Codewhale이 구조를 검증하고 범위를 제한했습니다." |
| 4208 | ), |
| 4209 | DraftProvenance::Guided => { |
| 4210 | "당신의 가이드 답변으로부터 결정적으로 생성되었습니다.".to_string() |
| 4211 | } |
| 4212 | DraftProvenance::Existing => { |
| 4213 | "constitution.json에서 불러온 기존 헌법이며, 변경 없이 그대로 표시됩니다." |
| 4214 | .to_string() |
| 4215 | } |
| 4216 | }; |
| 4217 | let ratify_how = match provenance { |
| 4218 | DraftProvenance::Existing => { |
| 4219 | "이것은 이미 당신의 상시 규칙입니다. 미리보기를 닫고 K를 눌러 그대로 유지하며 체크포인트를 완료하세요; \ |
| 4220 | 파일은 수정되지 않습니다. /constitution 또는 /setup으로 언제든지 수정할 수 있습니다." |
| 4221 | } |
| 4222 | _ => { |
| 4223 | "확인하기 전까지는 아무것도 규칙이 되지 않습니다. 미리보기를 닫고 G를 눌러 승인하고 저장하세요. \ |
| 4224 | /constitution 또는 /setup으로 언제든지 수정할 수 있습니다." |
| 4225 | } |
| 4226 | }; |
| 4227 | format!( |
| 4228 | "CODEWHALE · 사용자 헌법\n{RULE}\n\n{drafted_by}\n\n\ |
| 4229 | 이것은 Codewhale이 당신과 함께 일하는 방식에 대한 상시 규칙입니다. 훌륭한 헌법이 그렇듯, \ |
| 4230 | 사용할 수 있을 만큼 짧고, 소모적인 규칙이 아닌 지속적인 원칙으로 이루어져 있으며, 당신이 변화함에 따라 수정할 수 있습니다. \ |
| 4231 | 이는 모든 개별 사례를 판단하는 대신 권한과 한계를 규정하며, 세션을 넘어 협업의 연속성을 부여합니다. \ |
| 4232 | 다만 이것은 기억이 아닙니다: 이력이 아니라 원칙을 담습니다.\n\n\ |
| 4233 | {rendered}\n\n\ |
| 4234 | 권한 계층\n{layer_order}\n당신의 직접적인 요청은 언제나 이 문서보다 우선합니다.\n\n\ |
| 4235 | 이것이 할 수 없는 일\n\ |
| 4236 | 이것은 행동을 안내할 뿐입니다. 승인 정책, 샌드박스, 셸, 네트워크, 신뢰, MCP 권한, 기본 모드, 게시, 지출 권한을 \ |
| 4237 | 부여하거나 바꿀 수 없습니다; 이는 여전히 런타임에서 당신이 직접 관리합니다.\n\n\ |
| 4238 | 축소된 코어와 옵트인 모듈\n\ |
| 4239 | 내장된 코어는 계속 활성 상태입니다. 이 초안은 사용자 전역의 상시 선호만 저장합니다. \ |
| 4240 | 무거운 실행/오케스트레이션 지침은 모드 프롬프트나 향후 옵트인 모듈에 속합니다. 이 미리보기는 모듈을 활성화하지 않으며 그 설정도 바꾸지 않습니다.\n\n\ |
| 4241 | 승인\n{ratify_how}" |
| 4242 | ) |
| 4243 | } |
| 4244 | Locale::Ca => { |
| 4245 | let drafted_by = match provenance { |
| 4246 | DraftProvenance::Model(label) => format!( |
| 4247 | "Redactat per {label} a partir de les teves respostes guiades, després validat per esquema i acotat per Codewhale." |
| 4248 | ), |
| 4249 | DraftProvenance::Guided => { |
| 4250 | "Generat determinísticament a partir de les teves respostes guiades.".to_string() |
| 4251 | } |
| 4252 | DraftProvenance::Existing => { |
| 4253 | "La teva constitució existent, carregada de constitution.json, es mostra sense canvis." |
| 4254 | .to_string() |
| 4255 | } |
| 4256 | }; |
| 4257 | let ratify_how = match provenance { |
| 4258 | DraftProvenance::Existing => { |
| 4259 | "Aquesta ja és la teva llei vigent. Tanca la previsualització i prem K per conservar-la i completar el punt de control; \ |
| 4260 | el fitxer no es modifica. Esmena-la en qualsevol moment amb /constitution o /setup." |
| 4261 | } |
| 4262 | _ => { |
| 4263 | "Res no esdevé llei fins que ho confirmis. Tanca la previsualització i prem G per ratificar i desar. \ |
| 4264 | Esmena-la en qualsevol moment amb /constitution o /setup." |
| 4265 | } |
| 4266 | }; |
| 4267 | format!( |
| 4268 | "CODEWHALE · CONSTITUCIÓ DE L'USUARI\n{RULE}\n\n{drafted_by}\n\n\ |
| 4269 | Aquesta és la llei permanent de com Codewhale treballa amb tu. Com les bones constitucions, \ |
| 4270 | és prou curta per usar-se, feta de principis duradors en lloc de regles exhaustives, \ |
| 4271 | i esmenable a mesura que canvies. Defineix poders i límits en lloc de decidir cada cas, \ |
| 4272 | i dona continuïtat a la col·laboració entre sessions — però no és memòria: porta principis, no història.\n\n\ |
| 4273 | {rendered}\n\n\ |
| 4274 | JERARQUIA D'AUTORITAT\n{layer_order}\nLes teves peticions directes sempre prevalen sobre aquest document.\n\n\ |
| 4275 | EL QUE AIXÒ NO POT FER\n\ |
| 4276 | Orienta el comportament. No pot concedir ni canviar la política d'aprovació, sandbox, shell, xarxa, \ |
| 4277 | confiança, permisos MCP, mode per defecte, publicació o autoritat de despesa; això queda sota el teu control en temps d'execució.\n\n\ |
| 4278 | NUCLI REDUÏT I MÒDULS OPT-IN\n\ |
| 4279 | El nucli inclòs continua actiu. Aquest esborrany només desa les teves preferències permanents globals d'usuari. \ |
| 4280 | La doctrina pesada d'execució o orquestració pertany als prompts de mode o a futurs mòduls opt-in; aquesta previsualització no activa mòduls ni canvia la seva configuració.\n\n\ |
| 4281 | RATIFICACIÓ\n{ratify_how}" |
| 4282 | ) |
| 4283 | } |
| 4284 | Locale::De => { |
| 4285 | let drafted_by = match provenance { |
| 4286 | DraftProvenance::Model(label) => format!( |
| 4287 | "Entworfen von {label} aus deinen geführten Antworten, dann schema-geprüft und begrenzt durch Codewhale." |
| 4288 | ), |
| 4289 | DraftProvenance::Guided => { |
| 4290 | "Deterministisch aus deinen geführten Antworten erzeugt.".to_string() |
| 4291 | } |
| 4292 | DraftProvenance::Existing => { |
| 4293 | "Deine bestehende Verfassung, geladen aus constitution.json — unverändert gezeigt." |
| 4294 | .to_string() |
| 4295 | } |
| 4296 | }; |
| 4297 | let ratify_how = match provenance { |
| 4298 | DraftProvenance::Existing => { |
| 4299 | "Dies ist bereits dein geltendes Recht. Schließe die Vorschau und drücke K, um sie zu behalten und den Checkpoint abzuschließen — \ |
| 4300 | die Datei wird nicht verändert. Jederzeit mit /constitution oder /setup änderbar." |
| 4301 | } |
| 4302 | _ => { |
| 4303 | "Nichts wird Recht, bevor du bestätigst. Schließe die Vorschau und drücke G, um zu ratifizieren und zu speichern. \ |
| 4304 | Jederzeit mit /constitution oder /setup änderbar." |
| 4305 | } |
| 4306 | }; |
| 4307 | format!( |
| 4308 | "CODEWHALE · NUTZERVERFASSUNG\n{RULE}\n\n{drafted_by}\n\n\ |
| 4309 | Dies ist das geltende Gesetz dafür, wie Codewhale mit dir arbeitet. Wie die besten Verfassungen \ |
| 4310 | ist sie kurz genug, um genutzt zu werden, besteht aus dauerhaften Prinzipien statt erschöpfender Regeln \ |
| 4311 | und lässt sich ändern, wenn du dich änderst. Sie rahmt Befugnisse und Grenzen, statt jeden Einzelfall zu entscheiden, \ |
| 4312 | und gibt deiner Zusammenarbeit Kontinuität über Sitzungen hinweg — aber sie ist kein Gedächtnis: Sie trägt Prinzipien, nicht Geschichte.\n\n\ |
| 4313 | {rendered}\n\n\ |
| 4314 | HIERARCHIE DER AUTORITÄT\n{layer_order}\nDeine direkten Anweisungen stehen immer über diesem Dokument.\n\n\ |
| 4315 | WAS DIES NICHT KANN\n\ |
| 4316 | Sie leitet Verhalten. Sie kann keine Freigaberichtlinie, Sandbox, Shell, Netzwerk, \ |
| 4317 | Vertrauen, MCP-Berechtigungen, Standardmodus, Veröffentlichung oder Ausgabenbefugnis gewähren oder ändern — die bleiben zur Laufzeit in deiner Hand.\n\n\ |
| 4318 | REDUZIERTER KERN UND OPT-IN-MODULE\n\ |
| 4319 | Der mitgelieferte Kern bleibt aktiv. Dieser Entwurf speichert nur deine benutzer-globalen Dauerpräferenzen. \ |
| 4320 | Schwere Ausführungs- oder Orchestrierungsdoktrin gehört in Modus-Prompts oder künftige Opt-in-Module; diese Vorschau aktiviert keine Module und ändert nicht ihre Konfiguration.\n\n\ |
| 4321 | RATIFIZIERUNG\n{ratify_how}" |
| 4322 | ) |
| 4323 | } |
| 4324 | Locale::Fr => { |
| 4325 | let drafted_by = match provenance { |
| 4326 | DraftProvenance::Model(label) => format!( |
| 4327 | "Rédigé par {label} à partir de vos réponses guidées, puis validé par schéma et borné par Codewhale." |
| 4328 | ), |
| 4329 | DraftProvenance::Guided => { |
| 4330 | "Généré de façon déterministe à partir de vos réponses guidées.".to_string() |
| 4331 | } |
| 4332 | DraftProvenance::Existing => { |
| 4333 | "Votre constitution existante, chargée depuis constitution.json — affichée sans modification." |
| 4334 | .to_string() |
| 4335 | } |
| 4336 | }; |
| 4337 | let ratify_how = match provenance { |
| 4338 | DraftProvenance::Existing => { |
| 4339 | "C'est déjà votre loi permanente. Fermez cet aperçu, puis appuyez sur K pour la conserver et terminer le point de contrôle — \ |
| 4340 | le fichier n'est pas modifié. Amendez-la à tout moment avec /constitution ou /setup." |
| 4341 | } |
| 4342 | _ => { |
| 4343 | "Rien ne devient loi avant votre confirmation. Fermez cet aperçu, puis appuyez sur G pour ratifier et enregistrer. \ |
| 4344 | Amendez-la à tout moment avec /constitution ou /setup." |
| 4345 | } |
| 4346 | }; |
| 4347 | format!( |
| 4348 | "CODEWHALE · CONSTITUTION DE L'UTILISATEUR\n{RULE}\n\n{drafted_by}\n\n\ |
| 4349 | Voici la loi permanente qui régit la façon dont Codewhale travaille avec vous. Comme les meilleures constitutions, \ |
| 4350 | elle est assez courte pour être utilisée, faite de principes durables plutôt que de règles exhaustives, \ |
| 4351 | et amendable à mesure que vous changez. Elle encadre les pouvoirs et les limites plutôt que de trancher chaque cas, \ |
| 4352 | et donne à votre collaboration une continuité entre les sessions — mais elle n'est pas une mémoire : elle porte des principes, pas un historique.\n\n\ |
| 4353 | {rendered}\n\n\ |
| 4354 | HIÉRARCHIE D'AUTORITÉ\n{layer_order}\nVos demandes directes priment toujours sur ce document.\n\n\ |
| 4355 | CE QU'ELLE NE PEUT PAS FAIRE\n\ |
| 4356 | Elle guide le comportement. Elle ne peut ni accorder ni modifier la politique d'approbation, le sandbox, le shell, le réseau, \ |
| 4357 | la confiance, les permissions MCP, le mode par défaut, la publication ou le pouvoir de dépense — ceux-ci restent entre vos mains à l'exécution.\n\n\ |
| 4358 | NOYAU RÉDUIT ET MODULES OPT-IN\n\ |
| 4359 | Le noyau intégré reste actif. Ce brouillon n'enregistre que vos préférences permanentes globales. \ |
| 4360 | La doctrine lourde d'exécution ou d'orchestration appartient aux prompts de mode ou à de futurs modules opt-in ; cet aperçu n'active pas de modules et ne change pas leur configuration.\n\n\ |
| 4361 | RATIFICATION\n{ratify_how}" |
| 4362 | ) |
| 4363 | } |
| 4364 | Locale::Id => { |
| 4365 | let drafted_by = match provenance { |
| 4366 | DraftProvenance::Model(label) => format!( |
| 4367 | "Disusun oleh {label} dari jawaban terpandu Anda, lalu diperiksa skemanya dan dibatasi oleh Codewhale." |
| 4368 | ), |
| 4369 | DraftProvenance::Guided => { |
| 4370 | "Dihasilkan secara deterministik dari jawaban terpandu Anda.".to_string() |
| 4371 | } |
| 4372 | DraftProvenance::Existing => { |
| 4373 | "Konstitusi Anda yang ada, dimuat dari constitution.json — ditampilkan tanpa perubahan." |
| 4374 | .to_string() |
| 4375 | } |
| 4376 | }; |
| 4377 | let ratify_how = match provenance { |
| 4378 | DraftProvenance::Existing => { |
| 4379 | "Ini sudah menjadi hukum tetap Anda. Tutup pratinjau ini, lalu tekan K untuk mempertahankannya dan menyelesaikan checkpoint — \ |
| 4380 | file tidak diubah. Amendemen kapan saja dengan /constitution atau /setup." |
| 4381 | } |
| 4382 | _ => { |
| 4383 | "Tidak ada yang menjadi hukum sampai Anda mengonfirmasi. Tutup pratinjau ini, lalu tekan G untuk meratifikasi dan menyimpan. \ |
| 4384 | Amendemen kapan saja dengan /constitution atau /setup." |
| 4385 | } |
| 4386 | }; |
| 4387 | format!( |
| 4388 | "CODEWHALE · KONSTITUSI PENGGUNA\n{RULE}\n\n{drafted_by}\n\n\ |
| 4389 | Ini adalah hukum tetap tentang cara Codewhale bekerja dengan Anda. Seperti konstitusi terbaik, \ |
| 4390 | ia cukup singkat untuk dipakai, tersusun dari prinsip yang awet alih-alih aturan yang menyeluruh, \ |
| 4391 | dan dapat diamendemen seiring Anda berubah. Ia membingkai wewenang dan batasan alih-alih memutuskan setiap kasus, \ |
| 4392 | dan memberi kolaborasi Anda kesinambungan lintas sesi — tetapi ia bukan memori: ia membawa prinsip, bukan riwayat.\n\n\ |
| 4393 | {rendered}\n\n\ |
| 4394 | HIERARKI OTORITAS\n{layer_order}\nPermintaan langsung Anda selalu mengungguli dokumen ini.\n\n\ |
| 4395 | APA YANG TIDAK BISA DILAKUKANNYA\n\ |
| 4396 | Ia memandu perilaku. Ia tidak dapat memberi atau mengubah kebijakan persetujuan, sandbox, shell, jaringan, \ |
| 4397 | kepercayaan, izin MCP, mode default, publikasi, atau wewenang belanja — semua itu tetap di tangan Anda saat runtime.\n\n\ |
| 4398 | INTI RINGKAS DAN MODUL OPT-IN\n\ |
| 4399 | Inti bawaan tetap aktif. Draf ini hanya menyimpan preferensi tetap global pengguna Anda. \ |
| 4400 | Doktrin eksekusi atau orkestrasi yang berat termasuk dalam prompt mode atau modul opt-in mendatang; pratinjau ini tidak mengaktifkan modul atau mengubah konfigurasinya.\n\n\ |
| 4401 | RATIFIKASI\n{ratify_how}" |
| 4402 | ) |
| 4403 | } |
| 4404 | Locale::Hi => { |
| 4405 | let drafted_by = match provenance { |
| 4406 | DraftProvenance::Model(label) => format!( |
| 4407 | "{label} द्वारा आपके गाइडेड उत्तरों से तैयार, फिर Codewhale द्वारा स्कीमा-जाँचा और सीमित किया गया।" |
| 4408 | ), |
| 4409 | DraftProvenance::Guided => "आपके गाइडेड उत्तरों से नियत रूप से तैयार किया गया।".to_string(), |
| 4410 | DraftProvenance::Existing => { |
| 4411 | "आपका मौजूदा संविधान, constitution.json से लोड किया गया — अपरिवर्तित दिखाया गया।" |
| 4412 | .to_string() |
| 4413 | } |
| 4414 | }; |
| 4415 | let ratify_how = match provenance { |
| 4416 | DraftProvenance::Existing => { |
| 4417 | "यह पहले से ही आपका स्थायी कानून है। यह पूर्वावलोकन बंद करें, फिर इसे बनाए रखने और चेकपॉइंट पूरा करने के लिए K दबाएँ — \ |
| 4418 | फ़ाइल संशोधित नहीं होती। /constitution या /setup से कभी भी संशोधित करें।" |
| 4419 | } |
| 4420 | _ => { |
| 4421 | "जब तक आप पुष्टि नहीं करते, कुछ भी कानून नहीं बनता। यह पूर्वावलोकन बंद करें, फिर अंगीकार और सहेजने के लिए G दबाएँ। \ |
| 4422 | /constitution या /setup से कभी भी संशोधित करें।" |
| 4423 | } |
| 4424 | }; |
| 4425 | format!( |
| 4426 | "CODEWHALE · उपयोगकर्ता संविधान\n{RULE}\n\n{drafted_by}\n\n\ |
| 4427 | यह Codewhale आपके साथ कैसे काम करे, इसका स्थायी कानून है। सर्वोत्तम संविधानों की तरह, \ |
| 4428 | यह उपयोग के लिए पर्याप्त छोटा है, संपूर्ण नियमों के बजाय टिकाऊ सिद्धांतों से बना है, \ |
| 4429 | और आपके बदलने के साथ संशोधनीय है। यह हर मामले का फ़ैसला करने के बजाय शक्तियों और सीमाओं का ढाँचा देता है, \ |
| 4430 | और आपके सहयोग को सत्रों के पार निरंतरता देता है — लेकिन यह मेमोरी नहीं है: यह इतिहास नहीं, सिद्धांत रखता है।\n\n\ |
| 4431 | {rendered}\n\n\ |
| 4432 | अधिकार पदानुक्रम\n{layer_order}\nआपके प्रत्यक्ष अनुरोध हमेशा इस दस्तावेज़ से ऊपर हैं।\n\n\ |
| 4433 | यह क्या नहीं कर सकता\n\ |
| 4434 | यह व्यवहार का मार्गदर्शन करता है। यह अनुमति नीति, सैंडबॉक्स, शेल, नेटवर्क, \ |
| 4435 | ट्रस्ट, MCP अनुमतियाँ, डिफ़ॉल्ट मोड, प्रकाशन या खर्च का अधिकार प्रदान या परिवर्तित नहीं कर सकता — वे रनटाइम पर आपके हाथ में रहते हैं।\n\n\ |
| 4436 | संक्षिप्त कोर और ऑप्ट-इन मॉड्यूल\n\ |
| 4437 | Bundled कोर सक्रिय रहता है। यह मसौदा केवल आपकी उपयोगकर्ता-वैश्विक स्थायी प्राथमिकताएँ सहेजता है। \ |
| 4438 | भारी निष्पादन या ऑर्केस्ट्रेशन सिद्धांत मोड प्रॉम्प्ट या भविष्य के ऑप्ट-इन मॉड्यूल में रहते हैं; यह पूर्वावलोकन मॉड्यूल सक्षम नहीं करता और न ही उनकी कॉन्फ़िगरेशन बदलता है।\n\n\ |
| 4439 | अंगीकार\n{ratify_how}" |
| 4440 | ) |
| 4441 | } |
| 4442 | Locale::Ru => { |
| 4443 | let drafted_by = match provenance { |
| 4444 | DraftProvenance::Model(label) => format!( |
| 4445 | "Подготовлено {label} на основе ваших ответов на наводящие вопросы, затем проверено по схеме и ограничено Codewhale." |
| 4446 | ), |
| 4447 | DraftProvenance::Guided => { |
| 4448 | "Детерминированно построено из ваших ответов на наводящие вопросы.".to_string() |
| 4449 | } |
| 4450 | DraftProvenance::Existing => { |
| 4451 | "Ваша существующая конституция, загруженная из constitution.json, — показана без изменений." |
| 4452 | .to_string() |
| 4453 | } |
| 4454 | }; |
| 4455 | let ratify_how = match provenance { |
| 4456 | DraftProvenance::Existing => { |
| 4457 | "Это уже ваш действующий закон. Закройте это превью, затем нажмите K, чтобы сохранить её и завершить контрольную точку — \ |
| 4458 | файл не изменяется. Изменить можно в любое время через /constitution или /setup." |
| 4459 | } |
| 4460 | _ => { |
| 4461 | "Ничто не становится законом, пока вы не подтвердите. Закройте это превью, затем нажмите G, чтобы ратифицировать и сохранить. \ |
| 4462 | Изменить можно в любое время через /constitution или /setup." |
| 4463 | } |
| 4464 | }; |
| 4465 | format!( |
| 4466 | "CODEWHALE · КОНСТИТУЦИЯ ПОЛЬЗОВАТЕЛЯ\n{RULE}\n\n{drafted_by}\n\n\ |
| 4467 | Это постоянный закон о том, как Codewhale работает с вами. Как лучшие конституции, \ |
| 4468 | она достаточно коротка, чтобы ей пользоваться, состоит из долговечных принципов, а не исчерпывающих правил, \ |
| 4469 | и может изменяться вместе с вами. Она очерчивает полномочия и границы, а не решает каждый случай, \ |
| 4470 | и придаёт вашему сотрудничеству непрерывность между сессиями — но она не память: она хранит принципы, а не историю.\n\n\ |
| 4471 | {rendered}\n\n\ |
| 4472 | ИЕРАРХИЯ ПОЛНОМОЧИЙ\n{layer_order}\nВаши прямые указания всегда важнее этого документа.\n\n\ |
| 4473 | ЧЕГО ОНА НЕ МОЖЕТ\n\ |
| 4474 | Она направляет поведение. Она не может предоставить или изменить политику одобрения, sandbox, shell, сеть, \ |
| 4475 | доверие, разрешения MCP, режим по умолчанию, публикацию или право тратить — они остаются в ваших руках во время выполнения.\n\n\ |
| 4476 | СОКРАЩЁННОЕ ЯДРО И ОПЦИОНАЛЬНЫЕ МОДУЛИ\n\ |
| 4477 | Встроенное ядро остаётся активным. Этот проект сохраняет только ваши глобальные постоянные предпочтения. \ |
| 4478 | Тяжёлая доктрина исполнения или оркестрации принадлежит промптам режимов или будущим опциональным модулям; это превью не включает модули и не меняет их конфигурацию.\n\n\ |
| 4479 | РАТИФИКАЦИЯ\n{ratify_how}" |
| 4480 | ) |
| 4481 | } |
| 4482 | Locale::Uk => { |
| 4483 | let drafted_by = match provenance { |
| 4484 | DraftProvenance::Model(label) => format!( |
| 4485 | "Підготовлено {label} на основі ваших відповідей на навідні запитання, потім перевірено за схемою та обмежено Codewhale." |
| 4486 | ), |
| 4487 | DraftProvenance::Guided => { |
| 4488 | "Детерміновано побудовано з ваших відповідей на навідні запитання.".to_string() |
| 4489 | } |
| 4490 | DraftProvenance::Existing => { |
| 4491 | "Ваша чинна конституція, завантажена з constitution.json, — показана без змін." |
| 4492 | .to_string() |
| 4493 | } |
| 4494 | }; |
| 4495 | let ratify_how = match provenance { |
| 4496 | DraftProvenance::Existing => { |
| 4497 | "Це вже ваш чинний закон. Закрийте це прев'ю, потім натисніть K, щоб зберегти її та завершити контрольну точку — \ |
| 4498 | файл не змінюється. Змінити можна будь-коли через /constitution або /setup." |
| 4499 | } |
| 4500 | _ => { |
| 4501 | "Ніщо не стає законом, доки ви не підтвердите. Закрийте це прев'ю, потім натисніть G, щоб ратифікувати та зберегти. \ |
| 4502 | Змінити можна будь-коли через /constitution або /setup." |
| 4503 | } |
| 4504 | }; |
| 4505 | format!( |
| 4506 | "CODEWHALE · КОНСТИТУЦІЯ КОРИСТУВАЧА\n{RULE}\n\n{drafted_by}\n\n\ |
| 4507 | Це постійний закон про те, як Codewhale працює з вами. Як найкращі конституції, \ |
| 4508 | вона достатньо коротка, щоб нею користуватися, складається з довговічних принципів, а не вичерпних правил, \ |
| 4509 | і може змінюватися разом із вами. Вона окреслює повноваження та межі, а не вирішує кожен випадок, \ |
| 4510 | і надає вашій співпраці неперервність між сесіями — але вона не пам'ять: вона зберігає принципи, а не історію.\n\n\ |
| 4511 | {rendered}\n\n\ |
| 4512 | ІЄРАРХІЯ ПОВНОВАЖЕНЬ\n{layer_order}\nВаші прямі вказівки завжди важливіші за цей документ.\n\n\ |
| 4513 | ЧОГО ВОНА НЕ МОЖЕ\n\ |
| 4514 | Вона спрямовує поведінку. Вона не може надати або змінити політику схвалення, sandbox, shell, мережу, \ |
| 4515 | довіру, дозволи MCP, режим за замовчуванням, публікацію чи право витрачати — вони залишаються у ваших руках під час виконання.\n\n\ |
| 4516 | СКОРОЧЕНЕ ЯДРО Й ОПЦІЙНІ МОДУЛІ\n\ |
| 4517 | Вбудоване ядро залишається активним. Цей проєкт зберігає лише ваші глобальні постійні вподобання. \ |
| 4518 | Важка доктрина виконання чи оркестрації належить промптам режимів або майбутнім опційним модулям; це прев'ю не вмикає модулі й не змінює їхню конфігурацію.\n\n\ |
| 4519 | РАТИФІКАЦІЯ\n{ratify_how}" |
| 4520 | ) |
| 4521 | } |
| 4522 | _ => { |
| 4523 | let drafted_by = match provenance { |
| 4524 | DraftProvenance::Model(label) => format!( |
| 4525 | "Drafted by {label} from your guided answers, then schema-checked and bounded by Codewhale." |
| 4526 | ), |
| 4527 | DraftProvenance::Guided => { |
| 4528 | "Rendered deterministically from your guided answers.".to_string() |
| 4529 | } |
| 4530 | DraftProvenance::Existing => { |
| 4531 | "Your existing constitution, loaded from constitution.json — shown unchanged." |
| 4532 | .to_string() |
| 4533 | } |
| 4534 | }; |
| 4535 | let ratify_how = match provenance { |
| 4536 | DraftProvenance::Existing => { |
| 4537 | "This is already your standing law. Close this preview, then press K to \ |
| 4538 | keep it and complete the checkpoint — the file is not modified. Amend \ |
| 4539 | anytime with /constitution or /setup." |
| 4540 | } |
| 4541 | _ => { |
| 4542 | "Nothing becomes law until you confirm. Close this preview, then press G to \ |
| 4543 | ratify and save. Amend anytime with /constitution or /setup." |
| 4544 | } |
| 4545 | }; |
| 4546 | format!( |
| 4547 | "CODEWHALE · USER CONSTITUTION\n{RULE}\n\n{drafted_by}\n\n\ |
| 4548 | This is the standing law for how Codewhale works with you. Like the best \ |
| 4549 | constitutions, it is short enough to use, made of durable principles rather \ |
| 4550 | than exhaustive rules, and amendable as you change. It frames powers and \ |
| 4551 | limits rather than deciding every case, and it gives your collaboration \ |
| 4552 | continuity across sessions — but it is not memory: it carries principles, \ |
| 4553 | not history.\n\n\ |
| 4554 | {rendered}\n\n\ |
| 4555 | HIERARCHY OF AUTHORITY\n{layer_order}\nYour direct requests always outrank this document.\n\n\ |
| 4556 | WHAT THIS CANNOT DO\n\ |
| 4557 | It guides behavior. It cannot grant or change approval policy, sandbox, shell, \ |
| 4558 | network, trust, MCP permissions, default mode, publishing, or spending \ |
| 4559 | authority — those stay under your hand at runtime.\n\n\ |
| 4560 | REDUCED CORE AND OPT-IN MODULES\n\ |
| 4561 | The bundled core stays active. This draft only saves your user-global \ |
| 4562 | standing preferences. Heavy execution or orchestration doctrine belongs in mode \ |
| 4563 | prompts or future opt-in modules; this preview does not enable modules or change \ |
| 4564 | their configuration.\n\n\ |
| 4565 | RATIFICATION\n{ratify_how}" |
| 4566 | ) |
| 4567 | } |
| 4568 | } |
| 4569 | } |
| 4570 | |
| 4571 | /// Card line inviting the user to let their configured model draft the law. |
| 4572 | fn model_draft_invitation_line(locale: Locale, model_label: &str) -> String { |
| 4573 | match locale { |
| 4574 | Locale::Ja => { |
| 4575 | format!("A {model_label} が起草し、あなたが批准します。確認するまで保存しません。") |
| 4576 | } |
| 4577 | Locale::ZhHans => { |
| 4578 | format!("A {model_label} 生成草案,由你确认。未经确认不会保存。") |
| 4579 | } |
| 4580 | Locale::ZhHant => { |
| 4581 | format!("A {model_label} 起草,你批准。未經確認不會保存。") |
| 4582 | } |
| 4583 | Locale::PtBr => { |
| 4584 | format!("A {model_label} pode rascunhar. Você ratifica. Nada salva sem você.") |
| 4585 | } |
| 4586 | Locale::Es419 => { |
| 4587 | format!("A {model_label} puede redactarla. Tú ratificas. Nada se guarda sin ti.") |
| 4588 | } |
| 4589 | Locale::Vi => { |
| 4590 | format!("A {model_label} có thể soạn. Bạn phê chuẩn. Không lưu gì nếu chưa có bạn.") |
| 4591 | } |
| 4592 | Locale::Ko => { |
| 4593 | format!( |
| 4594 | "A {model_label}이(가) 초안을 작성할 수 있습니다. 승인은 당신이 합니다. 당신 없이는 아무것도 저장되지 않습니다." |
| 4595 | ) |
| 4596 | } |
| 4597 | Locale::Ca => { |
| 4598 | format!("A {model_label} la pot redactar. Tu la ratifiques. Res no es desa sense tu.") |
| 4599 | } |
| 4600 | Locale::De => { |
| 4601 | format!( |
| 4602 | "A {model_label} kann sie entwerfen. Du ratifizierst sie. Ohne dich wird nichts gespeichert." |
| 4603 | ) |
| 4604 | } |
| 4605 | Locale::Fr => { |
| 4606 | format!( |
| 4607 | "A {model_label} peut la rédiger. Vous la ratifiez. Rien ne s'enregistre sans vous." |
| 4608 | ) |
| 4609 | } |
| 4610 | Locale::Id => { |
| 4611 | format!( |
| 4612 | "A {model_label} dapat menyusunnya. Anda yang meratifikasi. Tidak ada yang tersimpan tanpa Anda." |
| 4613 | ) |
| 4614 | } |
| 4615 | Locale::Hi => { |
| 4616 | format!( |
| 4617 | "A {model_label} इसका मसौदा बना सकता है। अंगीकार आप करते हैं। आपके बिना कुछ भी सहेजा नहीं जाता।" |
| 4618 | ) |
| 4619 | } |
| 4620 | Locale::Ru => { |
| 4621 | format!( |
| 4622 | "A {model_label} может подготовить проект. Ратифицируете вы. Без вас ничего не сохраняется." |
| 4623 | ) |
| 4624 | } |
| 4625 | Locale::Uk => { |
| 4626 | format!( |
| 4627 | "A {model_label} може підготувати проєкт. Ратифікуєте ви. Без вас нічого не зберігається." |
| 4628 | ) |
| 4629 | } |
| 4630 | _ => format!("A {model_label} can draft it. You ratify it. Nothing saves without you."), |
| 4631 | } |
| 4632 | } |
| 4633 | |
| 4634 | /// Card line offering to keep an existing valid constitution unchanged. |
| 4635 | fn keep_existing_invitation_line(locale: Locale) -> &'static str { |
| 4636 | match locale { |
| 4637 | Locale::Ja => "K 既存の憲法を保持 - 確認して保持、ファイルは変更しません。", |
| 4638 | Locale::ZhHans => "K 保留现有宪章——先查看,再保留,文件不变。", |
| 4639 | Locale::ZhHant => "K 保留現有憲法 - 先查看,再保留,檔案不變。", |
| 4640 | Locale::PtBr => "K Manter constituição existente - revise, mantenha, arquivo inalterado.", |
| 4641 | Locale::Es419 => { |
| 4642 | "K Conservar constitución existente - revisa, conserva, archivo sin cambios." |
| 4643 | } |
| 4644 | Locale::Vi => "K Giữ hiến pháp hiện có - xem lại, giữ nguyên, tệp không đổi.", |
| 4645 | Locale::Ko => "K 기존 헌법 유지 - 검토 후 유지, 파일은 변경되지 않음.", |
| 4646 | Locale::Ca => { |
| 4647 | "K Mantén la constitució existent - revisa-la, conserva-la, fitxer sense canvis." |
| 4648 | } |
| 4649 | Locale::De => "K Bestehende Verfassung behalten - prüfen, behalten, Datei unverändert.", |
| 4650 | Locale::Fr => { |
| 4651 | "K Garder votre constitution existante - révisez-la, gardez-la, fichier inchangé." |
| 4652 | } |
| 4653 | Locale::Id => { |
| 4654 | "K Pertahankan konstitusi Anda yang ada - tinjau, pertahankan, file tidak berubah." |
| 4655 | } |
| 4656 | Locale::Hi => "K अपना मौजूदा संविधान रखें - समीक्षा करें, बनाए रखें, फ़ाइल अपरिवर्तित।", |
| 4657 | Locale::Ru => { |
| 4658 | "K Сохранить существующую конституцию - просмотрите, сохраните, файл не изменяется." |
| 4659 | } |
| 4660 | Locale::Uk => "K Зберегти чинну конституцію - перегляньте, збережіть, файл без змін.", |
| 4661 | _ => "K Keep your existing constitution — review it, keep it, file unchanged.", |
| 4662 | } |
| 4663 | } |
| 4664 | |
| 4665 | /// Card line shown while a model draft awaits ratification. |
| 4666 | fn model_draft_ready_line(locale: Locale, model_label: &str) -> String { |
| 4667 | match locale { |
| 4668 | Locale::Ja => { |
| 4669 | format!( |
| 4670 | "{model_label} の草案が批准待ちです - G で確認して批准、1-6 で草案を破棄します。" |
| 4671 | ) |
| 4672 | } |
| 4673 | Locale::ZhHans => { |
| 4674 | format!("{model_label} 的草案待确认——按 G 查看并确认;按 1-6 会丢弃草案。") |
| 4675 | } |
| 4676 | Locale::ZhHant => { |
| 4677 | format!("{model_label} 的草案待批准 - 按 G 查看並批准;按 1-6 會丟棄草案。") |
| 4678 | } |
| 4679 | Locale::PtBr => { |
| 4680 | format!( |
| 4681 | "Rascunho de {model_label} aguarda ratificação - G para revisar e ratificar; 1-6 descarta." |
| 4682 | ) |
| 4683 | } |
| 4684 | Locale::Es419 => { |
| 4685 | format!( |
| 4686 | "El borrador de {model_label} espera ratificación - G para revisar y ratificar; 1-6 lo descarta." |
| 4687 | ) |
| 4688 | } |
| 4689 | Locale::Vi => { |
| 4690 | format!( |
| 4691 | "Bản nháp của {model_label} chờ phê chuẩn - G để xem và phê chuẩn; 1-6 sẽ bỏ bản nháp." |
| 4692 | ) |
| 4693 | } |
| 4694 | Locale::Ko => { |
| 4695 | format!( |
| 4696 | "{model_label}의 초안이 승인을 기다리고 있습니다 - G로 확인하고 승인, 1-6은 초안을 버립니다." |
| 4697 | ) |
| 4698 | } |
| 4699 | Locale::Ca => { |
| 4700 | format!( |
| 4701 | "L'esborrany de {model_label} espera ratificació - G per revisar i ratificar; 1-6 el descarta." |
| 4702 | ) |
| 4703 | } |
| 4704 | Locale::De => { |
| 4705 | format!( |
| 4706 | "Entwurf von {model_label} wartet auf Ratifizierung - G zum Prüfen und Ratifizieren; 1-6 verwirft ihn." |
| 4707 | ) |
| 4708 | } |
| 4709 | Locale::Fr => { |
| 4710 | format!( |
| 4711 | "Le brouillon de {model_label} attend ratification - G pour réviser et ratifier ; 1-6 l'écarte." |
| 4712 | ) |
| 4713 | } |
| 4714 | Locale::Id => { |
| 4715 | format!( |
| 4716 | "Draf oleh {model_label} menunggu ratifikasi - G untuk meninjau dan meratifikasi; 1-6 membuangnya." |
| 4717 | ) |
| 4718 | } |
| 4719 | Locale::Hi => { |
| 4720 | format!( |
| 4721 | "{model_label} का मसौदा अंगीकार की प्रतीक्षा में है - समीक्षा और अंगीकार के लिए G; 1-6 उसे खारिज करता है।" |
| 4722 | ) |
| 4723 | } |
| 4724 | Locale::Ru => { |
| 4725 | format!( |
| 4726 | "Проект от {model_label} ожидает ратификации - G для просмотра и ратификации; 1-6 отклоняет его." |
| 4727 | ) |
| 4728 | } |
| 4729 | Locale::Uk => { |
| 4730 | format!( |
| 4731 | "Проєкт від {model_label} очікує ратифікації - G для перегляду та ратифікації; 1-6 відхиляє його." |
| 4732 | ) |
| 4733 | } |
| 4734 | _ => format!( |
| 4735 | "Draft by {model_label} awaits ratification — G to review and ratify; 1-6 discards it." |
| 4736 | ), |
| 4737 | } |
| 4738 | } |
| 4739 | |
| 4740 | /// Host-facing status line after a successful model draft. |
| 4741 | pub(crate) fn model_draft_ready_message(locale: Locale, model_label: &str) -> String { |
| 4742 | match locale { |
| 4743 | Locale::Ja => format!( |
| 4744 | "{model_label} があなたの憲法を起草しました。プレビューを確認してから G で批准してください。" |
| 4745 | ), |
| 4746 | Locale::ZhHans => { |
| 4747 | format!("{model_label} 已生成你的宪章草案。请查看预览,然后按 G 确认。") |
| 4748 | } |
| 4749 | Locale::ZhHant => format!("{model_label} 已起草你的憲法。請查看預覽,然後按 G 批准。"), |
| 4750 | Locale::PtBr => format!( |
| 4751 | "{model_label} rascunhou sua constituição. Revise a prévia e pressione G para ratificar." |
| 4752 | ), |
| 4753 | Locale::Es419 => format!( |
| 4754 | "{model_label} redactó tu constitución. Revisa la vista previa y presiona G para ratificar." |
| 4755 | ), |
| 4756 | Locale::Vi => format!( |
| 4757 | "{model_label} đã soạn hiến pháp của bạn. Xem bản xem trước rồi nhấn G để phê chuẩn." |
| 4758 | ), |
| 4759 | Locale::Ko => format!( |
| 4760 | "{model_label}이(가) 당신의 헌법 초안을 작성했습니다. 미리보기를 확인한 뒤 G를 눌러 승인하세요." |
| 4761 | ), |
| 4762 | Locale::Ca => format!( |
| 4763 | "{model_label} ha redactat la teva constitució. Revisa la previsualització i prem G per ratificar." |
| 4764 | ), |
| 4765 | Locale::De => format!( |
| 4766 | "{model_label} hat deine Verfassung entworfen. Prüfe die Vorschau und drücke G zum Ratifizieren." |
| 4767 | ), |
| 4768 | Locale::Fr => format!( |
| 4769 | "{model_label} a rédigé votre constitution. Révisez l'aperçu, puis appuyez sur G pour ratifier." |
| 4770 | ), |
| 4771 | Locale::Id => format!( |
| 4772 | "{model_label} menyusun konstitusi Anda. Tinjau pratinjaunya, lalu tekan G untuk meratifikasi." |
| 4773 | ), |
| 4774 | Locale::Hi => format!( |
| 4775 | "{model_label} ने आपके संविधान का मसौदा तैयार किया। पूर्वावलोकन देखें, फिर अंगीकार के लिए G दबाएँ।" |
| 4776 | ), |
| 4777 | Locale::Ru => format!( |
| 4778 | "{model_label} подготовил проект вашей конституции. Просмотрите превью, затем нажмите G для ратификации." |
| 4779 | ), |
| 4780 | Locale::Uk => format!( |
| 4781 | "{model_label} підготував проєкт вашої конституції. Перегляньте прев'ю, потім натисніть G для ратифікації." |
| 4782 | ), |
| 4783 | _ => format!( |
| 4784 | "{model_label} drafted your constitution. Review the preview, then press G to ratify." |
| 4785 | ), |
| 4786 | } |
| 4787 | } |
| 4788 | |
| 4789 | /// Host-facing status line when model drafting fails or is unavailable. The |
| 4790 | /// guided deterministic draft always remains the standing fallback. |
| 4791 | pub(crate) fn model_draft_failed_message( |
| 4792 | locale: Locale, |
| 4793 | model_label: &str, |
| 4794 | reason: &str, |
| 4795 | ) -> String { |
| 4796 | match locale { |
| 4797 | Locale::Ja => { |
| 4798 | format!( |
| 4799 | "{model_label} は起草を完了できませんでした({reason})。ガイド草案は有効です。G でプレビューして批准できます。" |
| 4800 | ) |
| 4801 | } |
| 4802 | Locale::ZhHans => { |
| 4803 | format!("{model_label} 未能生成草案({reason})。引导式草案仍可使用——按 G 预览并确认。") |
| 4804 | } |
| 4805 | Locale::ZhHant => { |
| 4806 | format!("{model_label} 未能完成起草({reason})。引導式草案仍然有效;按 G 預覽並批准。") |
| 4807 | } |
| 4808 | Locale::PtBr => { |
| 4809 | format!( |
| 4810 | "{model_label} não conseguiu rascunhar sua constituição ({reason}). O rascunho guiado continua válido; pressione G para pré-visualizar e ratificar." |
| 4811 | ) |
| 4812 | } |
| 4813 | Locale::Es419 => { |
| 4814 | format!( |
| 4815 | "{model_label} no pudo redactar tu constitución ({reason}). El borrador guiado sigue válido; presiona G para previsualizar y ratificar." |
| 4816 | ) |
| 4817 | } |
| 4818 | Locale::Vi => { |
| 4819 | format!( |
| 4820 | "{model_label} không thể soạn hiến pháp của bạn ({reason}). Bản nháp hướng dẫn vẫn hợp lệ; nhấn G để xem trước và phê chuẩn." |
| 4821 | ) |
| 4822 | } |
| 4823 | Locale::Ko => { |
| 4824 | format!( |
| 4825 | "{model_label}이(가) 당신의 헌법 초안을 작성하지 못했습니다 ({reason}). 가이드 초안은 여전히 유효합니다. G를 눌러 미리보고 승인하세요." |
| 4826 | ) |
| 4827 | } |
| 4828 | Locale::Ca => { |
| 4829 | format!( |
| 4830 | "{model_label} no ha pogut redactar la teva constitució ({reason}). L'esborrany guiat continua vigent; prem G per previsualitzar i ratificar." |
| 4831 | ) |
| 4832 | } |
| 4833 | Locale::De => { |
| 4834 | format!( |
| 4835 | "{model_label} konnte deine Verfassung nicht entwerfen ({reason}). Dein geführter Entwurf bleibt gültig; drücke G für Vorschau und Ratifizierung." |
| 4836 | ) |
| 4837 | } |
| 4838 | Locale::Fr => { |
| 4839 | format!( |
| 4840 | "{model_label} n'a pas pu rédiger votre constitution ({reason}). Votre brouillon guidé reste valide ; appuyez sur G pour l'aperçu et la ratification." |
| 4841 | ) |
| 4842 | } |
| 4843 | Locale::Id => { |
| 4844 | format!( |
| 4845 | "{model_label} tidak dapat menyusun konstitusi Anda ({reason}). Draf terpandu Anda tetap berlaku; tekan G untuk pratinjau dan ratifikasi." |
| 4846 | ) |
| 4847 | } |
| 4848 | Locale::Hi => { |
| 4849 | format!( |
| 4850 | "{model_label} आपके संविधान का मसौदा नहीं बना सका ({reason})। आपका गाइडेड मसौदा अब भी मान्य है; पूर्वावलोकन और अंगीकार के लिए G दबाएँ।" |
| 4851 | ) |
| 4852 | } |
| 4853 | Locale::Ru => { |
| 4854 | format!( |
| 4855 | "{model_label} не смог подготовить вашу конституцию ({reason}). Ваш управляемый проект остаётся в силе — нажмите G для просмотра и ратификации." |
| 4856 | ) |
| 4857 | } |
| 4858 | Locale::Uk => { |
| 4859 | format!( |
| 4860 | "{model_label} не зміг підготувати вашу конституцію ({reason}). Ваш керований проєкт залишається чинним — натисніть G для перегляду та ратифікації." |
| 4861 | ) |
| 4862 | } |
| 4863 | _ => format!( |
| 4864 | "{model_label} could not draft your constitution ({reason}). Your guided draft still \ |
| 4865 | stands — press G to preview and ratify." |
| 4866 | ), |
| 4867 | } |
| 4868 | } |
| 4869 | |
| 4870 | fn constitution_choice_label(choice: ConstitutionChoice) -> &'static str { |
| 4871 | match choice { |
| 4872 | ConstitutionChoice::Unset => "unset", |
| 4873 | ConstitutionChoice::Bundled => "bundled/default", |
| 4874 | ConstitutionChoice::GuidedCustom => "guided custom", |
| 4875 | ConstitutionChoice::ExpertOverride => "expert override", |
| 4876 | ConstitutionChoice::Deferred => "deferred", |
| 4877 | } |
| 4878 | } |
| 4879 | |
| 4880 | fn constitution_source_label(source: ConstitutionSource) -> &'static str { |
| 4881 | match source { |
| 4882 | ConstitutionSource::Bundled => "bundled", |
| 4883 | ConstitutionSource::UserGlobal => "user-global constitution.json", |
| 4884 | ConstitutionSource::ExpertOverride => "expert full Markdown override", |
| 4885 | } |
| 4886 | } |
| 4887 | |
| 4888 | fn constitution_validity_label(validity: ConstitutionValidity) -> &'static str { |
| 4889 | match validity { |
| 4890 | ConstitutionValidity::Unknown => "unknown", |
| 4891 | ConstitutionValidity::Valid => "valid", |
| 4892 | ConstitutionValidity::Invalid => "invalid", |
| 4893 | ConstitutionValidity::Empty => "empty", |
| 4894 | ConstitutionValidity::Unreadable => "unreadable", |
| 4895 | } |
| 4896 | } |
| 4897 | |
| 4898 | pub fn persist_user_constitution_choice( |
| 4899 | constitution: &UserConstitution, |
| 4900 | state: &SetupState, |
| 4901 | ) -> anyhow::Result<()> { |
| 4902 | let constitution_path = UserConstitution::path()?; |
| 4903 | let setup_state_path = SetupState::path()?; |
| 4904 | let mut transaction = codewhale_config::persistence::SetupTransaction::new(); |
| 4905 | transaction.stage_json(constitution_path, &constitution.bounded())?; |
| 4906 | transaction.stage_json(setup_state_path, state)?; |
| 4907 | transaction.commit() |
| 4908 | } |
| 4909 | |
| 4910 | #[must_use] |
| 4911 | pub fn should_open_update_checkpoint(app: &App, config: &Config) -> bool { |
| 4912 | let state = load_setup_state_for_app(app, config); |
| 4913 | state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) |
| 4914 | } |
| 4915 | |
| 4916 | pub fn defer_update_checkpoint_for_app(app: &App, config: &Config) -> anyhow::Result<SetupState> { |
| 4917 | let mut state = load_setup_state_for_app(app, config); |
| 4918 | if !state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) { |
| 4919 | return Ok(state); |
| 4920 | } |
| 4921 | state.complete_constitution_checkpoint( |
| 4922 | CONSTITUTION_CHECKPOINT_VERSION, |
| 4923 | ConstitutionChoice::Deferred, |
| 4924 | ); |
| 4925 | state.constitution_source = ConstitutionSource::Bundled; |
| 4926 | state.constitution_validity = ConstitutionValidity::Unknown; |
| 4927 | state.constitution_authoring = None; |
| 4928 | state.constitution_preview_hash = None; |
| 4929 | state.set_step( |
| 4930 | SetupStep::Constitution, |
| 4931 | StepEntry::new(StepStatus::Deferred, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 4932 | .with_result("checkpoint deferred; bundled applies"), |
| 4933 | ); |
| 4934 | state.save()?; |
| 4935 | Ok(state) |
| 4936 | } |
| 4937 | |
| 4938 | #[must_use] |
| 4939 | pub fn load_setup_state_for_app(app: &App, config: &Config) -> SetupState { |
| 4940 | if let Ok(Some(state)) = SetupState::load() { |
| 4941 | return state; |
| 4942 | } |
| 4943 | SetupState::derive_inherited(&inherited_facts_for_app(app, config)) |
| 4944 | } |
| 4945 | |
| 4946 | pub(crate) fn record_provider_model_setup_state_for_app( |
| 4947 | app: &App, |
| 4948 | config: &Config, |
| 4949 | ) -> anyhow::Result<SetupState> { |
| 4950 | let facts = SetupRuntimeFacts::from_app_config(app, config); |
| 4951 | let mut state = load_setup_state_for_app(app, config); |
| 4952 | state.set_step( |
| 4953 | SetupStep::ProviderModel, |
| 4954 | provider::step_entry( |
| 4955 | facts.provider_ready, |
| 4956 | CONSTITUTION_CHECKPOINT_VERSION, |
| 4957 | facts.provider_result, |
| 4958 | ), |
| 4959 | ); |
| 4960 | state.save()?; |
| 4961 | Ok(state) |
| 4962 | } |
| 4963 | |
| 4964 | #[must_use] |
| 4965 | fn inherited_facts_for_app(app: &App, config: &Config) -> InheritedConfigFacts { |
| 4966 | let user_constitution = UserConstitution::load().ok(); |
| 4967 | let user_constitution_validity = user_constitution.as_ref().map_or( |
| 4968 | ConstitutionValidity::Unknown, |
| 4969 | UserConstitutionLoad::validity, |
| 4970 | ); |
| 4971 | let has_user_constitution = user_constitution |
| 4972 | .as_ref() |
| 4973 | .is_some_and(|loaded| !matches!(loaded, UserConstitutionLoad::Missing)); |
| 4974 | let expert_override = SetupExpertOverrideState::load(); |
| 4975 | InheritedConfigFacts { |
| 4976 | language: Some(app.ui_locale.tag().to_string()), |
| 4977 | has_provider_route: !config.default_model().trim().is_empty(), |
| 4978 | has_credentials_or_local_runtime: has_api_key(config), |
| 4979 | trust_chosen: app.trust_mode || !onboarding::needs_trust(&app.workspace), |
| 4980 | has_expert_override: expert_override.is_active(), |
| 4981 | has_user_constitution, |
| 4982 | user_constitution_validity, |
| 4983 | } |
| 4984 | } |
| 4985 | |
| 4986 | fn expert_override_path() -> Option<std::path::PathBuf> { |
| 4987 | codewhale_config::codewhale_home() |
| 4988 | .ok() |
| 4989 | .map(|home| home.join(Path::new(CONSTITUTION_OVERRIDE_FILE))) |
| 4990 | } |
| 4991 | |
| 4992 | #[must_use] |
| 4993 | fn initial_step_index(state: &SetupState) -> usize { |
| 4994 | if state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) { |
| 4995 | return step_index(SetupStep::Constitution); |
| 4996 | } |
| 4997 | STEP_SPECS |
| 4998 | .iter() |
| 4999 | .position(|step| { |
| 5000 | step.required() |
| 5001 | && !matches!( |
| 5002 | state.status(step.id()), |
| 5003 | StepStatus::Verified |
| 5004 | | StepStatus::NeedsAction |
| 5005 | | StepStatus::Deferred |
| 5006 | | StepStatus::Optional |
| 5007 | | StepStatus::Skipped |
| 5008 | ) |
| 5009 | }) |
| 5010 | .unwrap_or_else(|| step_index(SetupStep::Verification)) |
| 5011 | } |
| 5012 | |
| 5013 | #[must_use] |
| 5014 | fn step_index(step: SetupStep) -> usize { |
| 5015 | STEP_SPECS |
| 5016 | .iter() |
| 5017 | .position(|spec| spec.id() == step) |
| 5018 | .expect("all setup-state steps should have wizard specs") |
| 5019 | } |
| 5020 | |
| 5021 | fn visible_step_index(step: SetupStep) -> usize { |
| 5022 | STEP_SPECS |
| 5023 | .iter() |
| 5024 | .position(|spec| spec.id() == step) |
| 5025 | .unwrap_or_else(|| step_index(SetupStep::Constitution)) |
| 5026 | } |
| 5027 | |
| 5028 | #[cfg(test)] |
| 5029 | mod tests { |
| 5030 | use super::*; |
| 5031 | use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; |
| 5032 | |
| 5033 | fn key(code: KeyCode) -> KeyEvent { |
| 5034 | KeyEvent::new(code, KeyModifiers::NONE) |
| 5035 | } |
| 5036 | |
| 5037 | fn setup_test_options(workspace: std::path::PathBuf) -> crate::tui::app::TuiOptions { |
| 5038 | crate::tui::app::TuiOptions { |
| 5039 | allow_shell: true, |
| 5040 | start_in_agent_mode: true, |
| 5041 | skip_onboarding: false, |
| 5042 | ..crate::test_support::test_tui_options(workspace) |
| 5043 | } |
| 5044 | } |
| 5045 | |
| 5046 | #[test] |
| 5047 | fn visible_release_rail_includes_supported_optional_steps() { |
| 5048 | let steps = STEP_SPECS.iter().map(|step| step.id()).collect::<Vec<_>>(); |
| 5049 | |
| 5050 | assert_eq!( |
| 5051 | steps, |
| 5052 | vec![ |
| 5053 | SetupStep::Language, |
| 5054 | SetupStep::ProviderModel, |
| 5055 | SetupStep::TrustSandbox, |
| 5056 | SetupStep::Constitution, |
| 5057 | SetupStep::OperateFleet, |
| 5058 | SetupStep::Hotbar, |
| 5059 | SetupStep::ToolsMcp, |
| 5060 | SetupStep::RemoteRuntime, |
| 5061 | SetupStep::Persistence, |
| 5062 | SetupStep::Verification, |
| 5063 | ] |
| 5064 | ); |
| 5065 | assert_eq!( |
| 5066 | SetupWizardView::new_at_with_facts( |
| 5067 | SetupState::default(), |
| 5068 | Locale::En, |
| 5069 | SetupStep::ToolsMcp, |
| 5070 | SetupRuntimeFacts::default(), |
| 5071 | ) |
| 5072 | .selected_step(), |
| 5073 | SetupStep::ToolsMcp |
| 5074 | ); |
| 5075 | } |
| 5076 | |
| 5077 | #[test] |
| 5078 | fn wizard_resumes_at_constitution_checkpoint_when_update_incomplete() { |
| 5079 | let state = SetupState::default(); |
| 5080 | |
| 5081 | let view = SetupWizardView::new(state, Locale::En); |
| 5082 | |
| 5083 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 5084 | } |
| 5085 | |
| 5086 | #[test] |
| 5087 | fn bundled_constitution_commit_marks_checkpoint_complete() { |
| 5088 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5089 | |
| 5090 | let action = view.handle_key(key(KeyCode::Enter)); |
| 5091 | |
| 5092 | let ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { state, message }) = |
| 5093 | action |
| 5094 | else { |
| 5095 | panic!("expected setup-state commit event"); |
| 5096 | }; |
| 5097 | assert_eq!( |
| 5098 | state.constitution_checkpoint_completed_for.as_deref(), |
| 5099 | Some(CONSTITUTION_CHECKPOINT_VERSION) |
| 5100 | ); |
| 5101 | assert_eq!(state.constitution_choice, ConstitutionChoice::Bundled); |
| 5102 | assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified); |
| 5103 | assert!(message.contains("Constitution checkpoint complete")); |
| 5104 | } |
| 5105 | |
| 5106 | #[test] |
| 5107 | fn back_keys_return_to_previous_step_and_clamp_at_first() { |
| 5108 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5109 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 5110 | |
| 5111 | let action = view.handle_key(key(KeyCode::Right)); |
| 5112 | assert!(matches!(action, ViewAction::None)); |
| 5113 | assert_eq!(view.selected_step(), SetupStep::OperateFleet); |
| 5114 | |
| 5115 | let action = view.handle_key(key(KeyCode::Char('b'))); |
| 5116 | assert!(matches!(action, ViewAction::None)); |
| 5117 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 5118 | |
| 5119 | for _ in 0..STEP_SPECS.len() { |
| 5120 | view.handle_key(key(KeyCode::Left)); |
| 5121 | } |
| 5122 | assert_eq!(view.selected_step(), SetupStep::Language); |
| 5123 | } |
| 5124 | |
| 5125 | #[test] |
| 5126 | fn cancel_closes_without_commit_event() { |
| 5127 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5128 | |
| 5129 | let action = view.handle_key(key(KeyCode::Esc)); |
| 5130 | |
| 5131 | assert!(matches!(action, ViewAction::Close)); |
| 5132 | } |
| 5133 | |
| 5134 | #[test] |
| 5135 | fn skip_and_retry_emit_setup_state_commits() { |
| 5136 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5137 | |
| 5138 | let action = view.handle_key(key(KeyCode::Char('s'))); |
| 5139 | |
| 5140 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 5141 | else { |
| 5142 | panic!("expected skipped setup-state commit event"); |
| 5143 | }; |
| 5144 | assert_eq!(state.status(SetupStep::Constitution), StepStatus::Skipped); |
| 5145 | assert_eq!( |
| 5146 | state.constitution_checkpoint_completed_for.as_deref(), |
| 5147 | Some(CONSTITUTION_CHECKPOINT_VERSION) |
| 5148 | ); |
| 5149 | assert_eq!(state.constitution_choice, ConstitutionChoice::Deferred); |
| 5150 | assert!(message.contains("skipped")); |
| 5151 | assert_eq!(view.selected_step(), SetupStep::OperateFleet); |
| 5152 | let restarted = SetupWizardView::new(state, Locale::En); |
| 5153 | assert_eq!(restarted.selected_step(), SetupStep::Language); |
| 5154 | |
| 5155 | let action = view.handle_key(key(KeyCode::Char('r'))); |
| 5156 | |
| 5157 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 5158 | else { |
| 5159 | panic!("expected retry setup-state commit event"); |
| 5160 | }; |
| 5161 | assert_eq!( |
| 5162 | state.status(SetupStep::OperateFleet), |
| 5163 | StepStatus::NeedsAction |
| 5164 | ); |
| 5165 | assert!(message.contains("retry")); |
| 5166 | } |
| 5167 | |
| 5168 | #[test] |
| 5169 | fn skipping_checkpoint_preserves_active_custom_constitution() { |
| 5170 | let mut state = SetupState { |
| 5171 | constitution_choice: ConstitutionChoice::GuidedCustom, |
| 5172 | constitution_source: ConstitutionSource::UserGlobal, |
| 5173 | constitution_validity: ConstitutionValidity::Valid, |
| 5174 | constitution_authoring: Some(ConstitutionAuthoring::Guided), |
| 5175 | constitution_preview_hash: Some("sha256:existing-custom-preview".to_string()), |
| 5176 | constitution_preview_version: 7, |
| 5177 | ..SetupState::default() |
| 5178 | }; |
| 5179 | state.constitution_checkpoint_completed_for = Some("0.8.66".to_string()); |
| 5180 | let mut view = SetupWizardView::new_at_with_facts( |
| 5181 | state, |
| 5182 | Locale::En, |
| 5183 | SetupStep::Constitution, |
| 5184 | SetupRuntimeFacts::default(), |
| 5185 | ); |
| 5186 | |
| 5187 | let action = view.handle_key(key(KeyCode::Char('s'))); |
| 5188 | |
| 5189 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 5190 | else { |
| 5191 | panic!("expected skipped setup-state commit event"); |
| 5192 | }; |
| 5193 | assert_eq!(state.status(SetupStep::Constitution), StepStatus::Skipped); |
| 5194 | assert_eq!( |
| 5195 | state.constitution_checkpoint_completed_for.as_deref(), |
| 5196 | Some(CONSTITUTION_CHECKPOINT_VERSION) |
| 5197 | ); |
| 5198 | assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom); |
| 5199 | assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal); |
| 5200 | assert_eq!(state.constitution_validity, ConstitutionValidity::Valid); |
| 5201 | assert_eq!( |
| 5202 | state.constitution_authoring, |
| 5203 | Some(ConstitutionAuthoring::Guided) |
| 5204 | ); |
| 5205 | assert_eq!( |
| 5206 | state.constitution_preview_hash.as_deref(), |
| 5207 | Some("sha256:existing-custom-preview") |
| 5208 | ); |
| 5209 | assert_eq!(state.constitution_preview_version, 7); |
| 5210 | assert!(message.contains("skipped")); |
| 5211 | |
| 5212 | let restarted = SetupWizardView::new(state, Locale::En); |
| 5213 | assert_eq!(restarted.selected_step(), SetupStep::Language); |
| 5214 | } |
| 5215 | |
| 5216 | #[test] |
| 5217 | fn completed_checkpoint_resumes_to_first_required_gap() { |
| 5218 | let mut state = SetupState::default(); |
| 5219 | state.complete_constitution_checkpoint( |
| 5220 | CONSTITUTION_CHECKPOINT_VERSION, |
| 5221 | ConstitutionChoice::Bundled, |
| 5222 | ); |
| 5223 | |
| 5224 | let view = SetupWizardView::new(state, Locale::En); |
| 5225 | |
| 5226 | assert_eq!(view.selected_step(), SetupStep::Language); |
| 5227 | } |
| 5228 | |
| 5229 | #[test] |
| 5230 | fn language_step_records_locale_and_unblocks_first_run_ready() { |
| 5231 | let mut state = SetupState::default(); |
| 5232 | state.set_step( |
| 5233 | SetupStep::ProviderModel, |
| 5234 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 5235 | ); |
| 5236 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 5237 | state.complete_constitution_checkpoint( |
| 5238 | CONSTITUTION_CHECKPOINT_VERSION, |
| 5239 | ConstitutionChoice::Bundled, |
| 5240 | ); |
| 5241 | state.set_step( |
| 5242 | SetupStep::Constitution, |
| 5243 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 5244 | ); |
| 5245 | let mut view = SetupWizardView::new(state, Locale::En); |
| 5246 | assert_eq!(view.selected_step(), SetupStep::Language); |
| 5247 | |
| 5248 | let action = view.handle_key(key(KeyCode::Enter)); |
| 5249 | |
| 5250 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 5251 | else { |
| 5252 | panic!("expected language setup-state commit event"); |
| 5253 | }; |
| 5254 | assert_eq!(state.status(SetupStep::Language), StepStatus::Verified); |
| 5255 | assert_eq!(state.constitution_language.as_deref(), Some("en")); |
| 5256 | assert!(state.first_run_ready()); |
| 5257 | assert!(message.contains("Setup language recorded")); |
| 5258 | assert_eq!(view.selected_step(), SetupStep::ProviderModel); |
| 5259 | } |
| 5260 | |
| 5261 | #[test] |
| 5262 | fn zh_hans_checkpoint_copy_is_localized() { |
| 5263 | assert_ne!( |
| 5264 | tr(Locale::ZhHans, MessageId::SetupWizardTitle), |
| 5265 | tr(Locale::En, MessageId::SetupWizardTitle) |
| 5266 | ); |
| 5267 | assert_ne!( |
| 5268 | tr(Locale::ZhHans, MessageId::SetupCheckpointDoneBundled), |
| 5269 | tr(Locale::En, MessageId::SetupCheckpointDoneBundled) |
| 5270 | ); |
| 5271 | } |
| 5272 | |
| 5273 | #[test] |
| 5274 | fn zh_hans_constitution_surfaces_use_functional_terminology() { |
| 5275 | let constitution = GuidedConstitutionDraft::default().to_constitution(Locale::ZhHans); |
| 5276 | let samples = [ |
| 5277 | ratification_preview_title(Locale::ZhHans).to_string(), |
| 5278 | constitution_ratification_text( |
| 5279 | Locale::ZhHans, |
| 5280 | &constitution, |
| 5281 | &DraftProvenance::Existing, |
| 5282 | ), |
| 5283 | lines_to_text(vec![freeform_note_line(Locale::ZhHans, "", false)]), |
| 5284 | keep_existing_invitation_line(Locale::ZhHans).to_string(), |
| 5285 | model_draft_invitation_line(Locale::ZhHans, "GLM-5.2"), |
| 5286 | model_draft_ready_line(Locale::ZhHans, "GLM-5.2"), |
| 5287 | model_draft_ready_message(Locale::ZhHans, "GLM-5.2"), |
| 5288 | model_draft_failed_message(Locale::ZhHans, "GLM-5.2", "超时"), |
| 5289 | ]; |
| 5290 | let visible_copy = samples.join("\n"); |
| 5291 | |
| 5292 | for literal_metaphor in ["宪法", "教义", "自由原则", "起草"] { |
| 5293 | assert!( |
| 5294 | !visible_copy.contains(literal_metaphor), |
| 5295 | "Simplified Chinese setup copy should avoid {literal_metaphor}: {visible_copy}" |
| 5296 | ); |
| 5297 | } |
| 5298 | assert!(visible_copy.contains("宪章")); |
| 5299 | assert!(visible_copy.contains("自定义准则")); |
| 5300 | assert!(visible_copy.contains("可选策略")); |
| 5301 | assert!(visible_copy.contains("Codewhale")); |
| 5302 | assert!(visible_copy.contains("/constitution")); |
| 5303 | assert!(visible_copy.contains("constitution.json")); |
| 5304 | } |
| 5305 | |
| 5306 | #[test] |
| 5307 | fn guided_constitution_requires_preview_before_save() { |
| 5308 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5309 | |
| 5310 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5311 | |
| 5312 | let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else { |
| 5313 | panic!("expected guided constitution preview event"); |
| 5314 | }; |
| 5315 | assert!(title.contains("Draft for Ratification")); |
| 5316 | assert!(content.contains("<codewhale_user_constitution")); |
| 5317 | assert!(content.contains("press G to ratify and save")); |
| 5318 | assert!(content.contains("REDUCED CORE AND OPT-IN MODULES")); |
| 5319 | assert!(content.contains("The bundled core stays active")); |
| 5320 | assert!(content.contains("does not enable modules")); |
| 5321 | assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset); |
| 5322 | |
| 5323 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5324 | |
| 5325 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 5326 | constitution, |
| 5327 | state, |
| 5328 | message, |
| 5329 | }) = action |
| 5330 | else { |
| 5331 | panic!("expected guided constitution commit event"); |
| 5332 | }; |
| 5333 | assert_eq!(constitution.language.as_deref(), Some("en")); |
| 5334 | assert_eq!( |
| 5335 | constitution.autonomy_preference, |
| 5336 | AutonomyPreference::Balanced |
| 5337 | ); |
| 5338 | assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom); |
| 5339 | assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal); |
| 5340 | assert_eq!(state.constitution_validity, ConstitutionValidity::Valid); |
| 5341 | assert_eq!( |
| 5342 | state.constitution_preview_hash.as_deref(), |
| 5343 | Some(constitution.preview_hash().as_str()) |
| 5344 | ); |
| 5345 | assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified); |
| 5346 | assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset); |
| 5347 | assert!(message.contains("Constitution ratified")); |
| 5348 | } |
| 5349 | |
| 5350 | #[test] |
| 5351 | fn ratification_preview_explains_reduced_core_modules_for_shipped_locales() { |
| 5352 | for locale in Locale::shipped() { |
| 5353 | let constitution = GuidedConstitutionDraft::default().to_constitution(*locale); |
| 5354 | let content = |
| 5355 | constitution_ratification_text(*locale, &constitution, &DraftProvenance::Guided); |
| 5356 | let (heading, module_marker, no_enable_marker, permission_marker, mcp_marker) = |
| 5357 | match locale { |
| 5358 | Locale::Ja => ( |
| 5359 | "縮小コア", |
| 5360 | "モジュール", |
| 5361 | "有効化せず", |
| 5362 | "承認ポリシー、サンドボックス、Shell、ネットワーク、信頼、MCP 権限", |
| 5363 | "付与または変更することはできません", |
| 5364 | ), |
| 5365 | Locale::ZhHans => ( |
| 5366 | "精简核心", |
| 5367 | "可选策略", |
| 5368 | "不会启用任何策略", |
| 5369 | "不能授予或更改审批策略、沙箱、Shell、网络、信任、MCP 权限", |
| 5370 | "发布或支出权限", |
| 5371 | ), |
| 5372 | Locale::ZhHant => ( |
| 5373 | "精簡核心", |
| 5374 | "模組", |
| 5375 | "不會啟用", |
| 5376 | "不能授予或更改審批策略、沙箱、Shell、網路、信任、MCP 權限", |
| 5377 | "發布或支出權限", |
| 5378 | ), |
| 5379 | Locale::PtBr => ( |
| 5380 | "NÚCLEO REDUZIDO", |
| 5381 | "módulos", |
| 5382 | "não ativa", |
| 5383 | "Não pode conceder nem alterar política de aprovação, sandbox, shell, rede", |
| 5384 | "permissões MCP", |
| 5385 | ), |
| 5386 | Locale::Es419 => ( |
| 5387 | "NÚCLEO REDUCIDO", |
| 5388 | "módulos", |
| 5389 | "no activa", |
| 5390 | "No puede conceder ni cambiar política de aprobación, sandbox, shell, red", |
| 5391 | "permisos MCP", |
| 5392 | ), |
| 5393 | Locale::Vi => ( |
| 5394 | "LÕI RÚT GỌN", |
| 5395 | "mô-đun", |
| 5396 | "không bật", |
| 5397 | "không thể cấp hoặc đổi chính sách phê duyệt, sandbox, shell, mạng", |
| 5398 | "quyền MCP", |
| 5399 | ), |
| 5400 | Locale::Ko => ( |
| 5401 | "축소된 코어", |
| 5402 | "모듈", |
| 5403 | "활성화하지 않으며", |
| 5404 | "승인 정책, 샌드박스, 셸, 네트워크, 신뢰, MCP 권한, 기본 모드, 게시, 지출 권한을 부여하거나 바꿀 수 없습니다", |
| 5405 | "MCP 권한", |
| 5406 | ), |
| 5407 | Locale::Ca => ( |
| 5408 | "NUCLI REDUÏT", |
| 5409 | "mòduls", |
| 5410 | "no activa", |
| 5411 | "No pot concedir ni canviar la política d'aprovació, sandbox, shell, xarxa", |
| 5412 | "permisos MCP", |
| 5413 | ), |
| 5414 | Locale::De => ( |
| 5415 | "REDUZIERTER KERN", |
| 5416 | "Module", |
| 5417 | "aktiviert keine", |
| 5418 | "Sie kann keine Freigaberichtlinie, Sandbox, Shell, Netzwerk", |
| 5419 | "MCP-Berechtigungen", |
| 5420 | ), |
| 5421 | Locale::Fr => ( |
| 5422 | "NOYAU RÉDUIT", |
| 5423 | "modules", |
| 5424 | "n'active pas", |
| 5425 | "Elle ne peut ni accorder ni modifier la politique d'approbation, le sandbox, le shell, le réseau", |
| 5426 | "permissions MCP", |
| 5427 | ), |
| 5428 | Locale::Id => ( |
| 5429 | "INTI RINGKAS", |
| 5430 | "modul", |
| 5431 | "tidak mengaktifkan", |
| 5432 | "tidak dapat memberi atau mengubah kebijakan persetujuan, sandbox, shell, jaringan", |
| 5433 | "izin MCP", |
| 5434 | ), |
| 5435 | Locale::Hi => ( |
| 5436 | "संक्षिप्त कोर", |
| 5437 | "मॉड्यूल", |
| 5438 | "सक्षम नहीं करता", |
| 5439 | "यह अनुमति नीति, सैंडबॉक्स, शेल, नेटवर्क", |
| 5440 | "MCP अनुमतियाँ", |
| 5441 | ), |
| 5442 | Locale::Ru => ( |
| 5443 | "СОКРАЩЁННОЕ ЯДРО", |
| 5444 | "модули", |
| 5445 | "не включает модули", |
| 5446 | "Она не может предоставить или изменить политику одобрения, sandbox, shell, сеть", |
| 5447 | "разрешения MCP", |
| 5448 | ), |
| 5449 | Locale::Uk => ( |
| 5450 | "СКОРОЧЕНЕ ЯДРО", |
| 5451 | "модулі", |
| 5452 | "не вмикає модулі", |
| 5453 | "Вона не може надати або змінити політику схвалення, sandbox, shell, мережу", |
| 5454 | "дозволи MCP", |
| 5455 | ), |
| 5456 | Locale::En => ( |
| 5457 | "REDUCED CORE", |
| 5458 | "modules", |
| 5459 | "does not enable", |
| 5460 | "cannot grant or change approval policy, sandbox, shell", |
| 5461 | "MCP permissions", |
| 5462 | ), |
| 5463 | }; |
| 5464 | |
| 5465 | assert!(content.contains(heading), "{}", locale.tag()); |
| 5466 | assert!(content.contains(module_marker), "{}", locale.tag()); |
| 5467 | assert!(content.contains(no_enable_marker), "{}", locale.tag()); |
| 5468 | assert!(content.contains(permission_marker), "{}", locale.tag()); |
| 5469 | assert!(content.contains(mcp_marker), "{}", locale.tag()); |
| 5470 | } |
| 5471 | } |
| 5472 | |
| 5473 | #[test] |
| 5474 | fn guided_constitution_key_is_contextual_to_constitution_step() { |
| 5475 | let mut view = SetupWizardView::new_at_with_facts( |
| 5476 | SetupState::default(), |
| 5477 | Locale::En, |
| 5478 | SetupStep::ProviderModel, |
| 5479 | SetupRuntimeFacts::default(), |
| 5480 | ); |
| 5481 | |
| 5482 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5483 | |
| 5484 | assert!(matches!(action, ViewAction::None)); |
| 5485 | assert_eq!(view.selected_step(), SetupStep::ProviderModel); |
| 5486 | assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset); |
| 5487 | } |
| 5488 | |
| 5489 | #[test] |
| 5490 | fn provider_model_step_hands_off_to_existing_route_surfaces() { |
| 5491 | let mut view = SetupWizardView::new_at_with_facts( |
| 5492 | SetupState::default(), |
| 5493 | Locale::En, |
| 5494 | SetupStep::ProviderModel, |
| 5495 | SetupRuntimeFacts::default(), |
| 5496 | ); |
| 5497 | |
| 5498 | let provider_action = view.handle_key(key(KeyCode::Char('p'))); |
| 5499 | assert!(matches!( |
| 5500 | provider_action, |
| 5501 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 5502 | )); |
| 5503 | |
| 5504 | let model_action = view.handle_key(key(KeyCode::Char('m'))); |
| 5505 | assert!(matches!( |
| 5506 | model_action, |
| 5507 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModelRequested) |
| 5508 | )); |
| 5509 | } |
| 5510 | |
| 5511 | #[test] |
| 5512 | fn provider_model_detail_lines_show_credential_url_for_missing_hosted_provider() { |
| 5513 | let _guard = crate::test_support::lock_test_env(); |
| 5514 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5515 | let workspace = tmp.path().join("workspace"); |
| 5516 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5517 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5518 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5519 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5520 | let _codewhale_home = |
| 5521 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5522 | let _deepseek_key = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY"); |
| 5523 | let _nim_key = crate::test_support::EnvVarGuard::remove("NVIDIA_API_KEY"); |
| 5524 | let _nim_alt_key = crate::test_support::EnvVarGuard::remove("NVIDIA_NIM_API_KEY"); |
| 5525 | let config = Config { |
| 5526 | provider: Some("nvidia-nim".to_string()), |
| 5527 | ..Config::default() |
| 5528 | }; |
| 5529 | let app = App::new(setup_test_options(workspace), &config); |
| 5530 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 5531 | let view = SetupWizardView::new_at_with_facts( |
| 5532 | SetupState::default(), |
| 5533 | Locale::En, |
| 5534 | SetupStep::ProviderModel, |
| 5535 | facts, |
| 5536 | ); |
| 5537 | |
| 5538 | let text = lines_to_text(view.provider_model_detail_lines()); |
| 5539 | |
| 5540 | assert!(text.contains("NVIDIA NIM"), "{text}"); |
| 5541 | assert!(text.contains("credentials: https://build.nvidia.com/settings/api-keys")); |
| 5542 | } |
| 5543 | |
| 5544 | #[test] |
| 5545 | fn provider_model_detail_lines_use_kimi_code_membership_console_for_exact_route() { |
| 5546 | let _guard = crate::test_support::lock_test_env(); |
| 5547 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5548 | let workspace = tmp.path().join("workspace"); |
| 5549 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5550 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5551 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5552 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5553 | let _codewhale_home = |
| 5554 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5555 | let _moonshot_key = crate::test_support::EnvVarGuard::remove("MOONSHOT_API_KEY"); |
| 5556 | let _kimi_key = crate::test_support::EnvVarGuard::remove("KIMI_API_KEY"); |
| 5557 | let config = Config { |
| 5558 | provider: Some("moonshot".to_string()), |
| 5559 | providers: Some(crate::config::ProvidersConfig { |
| 5560 | moonshot: crate::config::ProviderConfig { |
| 5561 | base_url: Some(crate::config::DEFAULT_KIMI_CODE_BASE_URL.to_string()), |
| 5562 | model: Some(crate::config::KIMI_CODE_K3_MODEL.to_string()), |
| 5563 | ..Default::default() |
| 5564 | }, |
| 5565 | ..Default::default() |
| 5566 | }), |
| 5567 | ..Config::default() |
| 5568 | }; |
| 5569 | let mut app = App::new(setup_test_options(workspace), &config); |
| 5570 | app.model = crate::config::KIMI_CODE_K3_MODEL.to_string(); |
| 5571 | let resolution = crate::route_runtime::resolve_route_candidate_with_context_metadata( |
| 5572 | app.api_provider, |
| 5573 | Some(crate::config::KIMI_CODE_K3_MODEL), |
| 5574 | Some(crate::config::KIMI_CODE_K3_MODEL), |
| 5575 | Some(config.deepseek_base_url()), |
| 5576 | None, |
| 5577 | None, |
| 5578 | ) |
| 5579 | .expect("bare k3 on the Kimi Code route must resolve"); |
| 5580 | app.set_active_route_resolution( |
| 5581 | resolution.candidate.endpoint().base_url.clone(), |
| 5582 | resolution.candidate.limits(), |
| 5583 | resolution.context_window.source, |
| 5584 | ); |
| 5585 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 5586 | let view = SetupWizardView::new_at_with_facts( |
| 5587 | SetupState::default(), |
| 5588 | Locale::En, |
| 5589 | SetupStep::ProviderModel, |
| 5590 | facts, |
| 5591 | ); |
| 5592 | |
| 5593 | let text = lines_to_text(view.provider_model_detail_lines()); |
| 5594 | |
| 5595 | assert!(text.contains("Moonshot/Kimi"), "{text}"); |
| 5596 | assert!(text.contains("static Kimi Code safe floor"), "{text}"); |
| 5597 | assert!( |
| 5598 | text.contains(crate::config::KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL), |
| 5599 | "{text}" |
| 5600 | ); |
| 5601 | assert!(!text.contains("https://platform.kimi.ai/console/api-keys")); |
| 5602 | } |
| 5603 | |
| 5604 | #[test] |
| 5605 | fn provider_model_detail_lines_keep_codex_oauth_url_free() { |
| 5606 | let _guard = crate::test_support::lock_test_env(); |
| 5607 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5608 | let workspace = tmp.path().join("workspace"); |
| 5609 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5610 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5611 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5612 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5613 | let _codewhale_home = |
| 5614 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5615 | let _openai_codex_key = |
| 5616 | crate::test_support::EnvVarGuard::remove("OPENAI_CODEX_ACCESS_TOKEN"); |
| 5617 | let _codex_key = crate::test_support::EnvVarGuard::remove("CODEX_ACCESS_TOKEN"); |
| 5618 | let config = Config { |
| 5619 | provider: Some("openai-codex".to_string()), |
| 5620 | ..Config::default() |
| 5621 | }; |
| 5622 | let app = App::new(setup_test_options(workspace), &config); |
| 5623 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 5624 | let view = SetupWizardView::new_at_with_facts( |
| 5625 | SetupState::default(), |
| 5626 | Locale::En, |
| 5627 | SetupStep::ProviderModel, |
| 5628 | facts, |
| 5629 | ); |
| 5630 | |
| 5631 | let text = lines_to_text(view.provider_model_detail_lines()); |
| 5632 | |
| 5633 | assert!(text.contains("codex login"), "{text}"); |
| 5634 | assert!(text.contains("external-consent"), "{text}"); |
| 5635 | assert!(!text.contains("credentials:"), "{text}"); |
| 5636 | } |
| 5637 | |
| 5638 | #[test] |
| 5639 | fn provider_model_detail_lines_cover_deepseek_cn_and_local_boundaries() { |
| 5640 | let _guard = crate::test_support::lock_test_env(); |
| 5641 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5642 | let workspace = tmp.path().join("workspace"); |
| 5643 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5644 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5645 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5646 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5647 | let _codewhale_home = |
| 5648 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5649 | let _deepseek_key = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY"); |
| 5650 | let _deepseek_source = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY_SOURCE"); |
| 5651 | |
| 5652 | let cn_config = Config { |
| 5653 | provider: Some("deepseek-cn".to_string()), |
| 5654 | ..Config::default() |
| 5655 | }; |
| 5656 | let cn_app = App::new(setup_test_options(workspace.clone()), &cn_config); |
| 5657 | let cn_view = SetupWizardView::new_at_with_facts( |
| 5658 | SetupState::default(), |
| 5659 | Locale::En, |
| 5660 | SetupStep::ProviderModel, |
| 5661 | SetupRuntimeFacts::from_app_config(&cn_app, &cn_config), |
| 5662 | ); |
| 5663 | let cn_text = lines_to_text(cn_view.provider_model_detail_lines()); |
| 5664 | assert!(cn_text.contains("DeepSeek (legacy alias)"), "{cn_text}"); |
| 5665 | assert!( |
| 5666 | cn_text.contains("credentials: https://platform.deepseek.com/api_keys"), |
| 5667 | "{cn_text}" |
| 5668 | ); |
| 5669 | assert!(cn_text.contains("missing key"), "{cn_text}"); |
| 5670 | |
| 5671 | let local_config = Config { |
| 5672 | provider: Some("ollama".to_string()), |
| 5673 | ..Config::default() |
| 5674 | }; |
| 5675 | let local_app = App::new(setup_test_options(workspace), &local_config); |
| 5676 | let local_view = SetupWizardView::new_at_with_facts( |
| 5677 | SetupState::default(), |
| 5678 | Locale::En, |
| 5679 | SetupStep::ProviderModel, |
| 5680 | SetupRuntimeFacts::from_app_config(&local_app, &local_config), |
| 5681 | ); |
| 5682 | let local_text = lines_to_text(local_view.provider_model_detail_lines()); |
| 5683 | assert!(local_text.contains("Ollama"), "{local_text}"); |
| 5684 | assert!(local_text.contains("local · not checked"), "{local_text}"); |
| 5685 | assert!(!local_text.contains("credentials:"), "{local_text}"); |
| 5686 | } |
| 5687 | |
| 5688 | #[test] |
| 5689 | fn runtime_posture_step_hands_off_to_mode_and_config_surfaces() { |
| 5690 | let mut view = SetupWizardView::new_at_with_facts( |
| 5691 | SetupState::default(), |
| 5692 | Locale::En, |
| 5693 | SetupStep::TrustSandbox, |
| 5694 | SetupRuntimeFacts::default(), |
| 5695 | ); |
| 5696 | |
| 5697 | let mode_action = view.handle_key(key(KeyCode::Char('m'))); |
| 5698 | assert!(matches!( |
| 5699 | mode_action, |
| 5700 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModeRequested) |
| 5701 | )); |
| 5702 | |
| 5703 | let config_action = view.handle_key(key(KeyCode::Char('c'))); |
| 5704 | assert!(matches!( |
| 5705 | config_action, |
| 5706 | ViewAction::EmitAndClose(ViewEvent::SetupOpenConfigRequested) |
| 5707 | )); |
| 5708 | } |
| 5709 | |
| 5710 | #[test] |
| 5711 | fn operate_fleet_step_hands_off_to_provider_and_fleet_surfaces() { |
| 5712 | let mut view = SetupWizardView::new_at_with_facts( |
| 5713 | SetupState::default(), |
| 5714 | Locale::En, |
| 5715 | SetupStep::OperateFleet, |
| 5716 | SetupRuntimeFacts::default(), |
| 5717 | ); |
| 5718 | |
| 5719 | let provider_action = view.handle_key(key(KeyCode::Char('p'))); |
| 5720 | assert!(matches!( |
| 5721 | provider_action, |
| 5722 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 5723 | )); |
| 5724 | |
| 5725 | let fleet_action = view.handle_key(key(KeyCode::Char('f'))); |
| 5726 | assert!(matches!( |
| 5727 | fleet_action, |
| 5728 | ViewAction::EmitAndClose(ViewEvent::SetupOpenFleetRequested) |
| 5729 | )); |
| 5730 | } |
| 5731 | |
| 5732 | #[test] |
| 5733 | fn hotbar_step_hands_off_to_existing_hotbar_setup() { |
| 5734 | let mut view = SetupWizardView::new_at_with_facts( |
| 5735 | SetupState::default(), |
| 5736 | Locale::En, |
| 5737 | SetupStep::Hotbar, |
| 5738 | SetupRuntimeFacts::default(), |
| 5739 | ); |
| 5740 | |
| 5741 | let action = view.handle_key(key(KeyCode::Char('h'))); |
| 5742 | |
| 5743 | assert!(matches!( |
| 5744 | action, |
| 5745 | ViewAction::EmitAndClose(ViewEvent::SetupOpenHotbarRequested) |
| 5746 | )); |
| 5747 | } |
| 5748 | |
| 5749 | #[test] |
| 5750 | fn remote_runtime_step_previews_generate_only_on_ramp() { |
| 5751 | let facts = SetupRuntimeFacts { |
| 5752 | remote_clouds_result: "3 cloud targets: lighthouse, azure, digitalocean".to_string(), |
| 5753 | remote_bridges_result: "2 chat bridges: feishu, telegram".to_string(), |
| 5754 | remote_providers_result: |
| 5755 | "12 providers from the provider registry; active route deepseek / deepseek-chat" |
| 5756 | .to_string(), |
| 5757 | remote_mode_result: |
| 5758 | "generate-only bundle; --apply not implemented; default port 7878, workers 2" |
| 5759 | .to_string(), |
| 5760 | ..SetupRuntimeFacts::default() |
| 5761 | }; |
| 5762 | let mut view = SetupWizardView::new_at_with_facts( |
| 5763 | SetupState::default(), |
| 5764 | Locale::En, |
| 5765 | SetupStep::RemoteRuntime, |
| 5766 | facts, |
| 5767 | ); |
| 5768 | |
| 5769 | let action = view.handle_key(key(KeyCode::Char('r'))); |
| 5770 | |
| 5771 | let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else { |
| 5772 | panic!("expected remote on-ramp pager"); |
| 5773 | }; |
| 5774 | assert_eq!(title, "Remote runtime on-ramp"); |
| 5775 | assert!(content.contains("does not generate deploy bundles")); |
| 5776 | assert!(content.contains("codewhale remote-setup --generate-only")); |
| 5777 | assert!(content.contains("`--apply` remains unimplemented")); |
| 5778 | } |
| 5779 | |
| 5780 | #[test] |
| 5781 | fn remote_runtime_on_ramp_command_uses_active_provider() { |
| 5782 | let _guard = crate::test_support::lock_test_env(); |
| 5783 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5784 | let workspace = tmp.path().join("workspace"); |
| 5785 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5786 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5787 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5788 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5789 | let _codewhale_home = |
| 5790 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5791 | let config = Config { |
| 5792 | provider: Some("openrouter".to_string()), |
| 5793 | ..Config::default() |
| 5794 | }; |
| 5795 | let app = App::new(setup_test_options(workspace), &config); |
| 5796 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 5797 | |
| 5798 | let content = remote_runtime_on_ramp_text(Locale::En, &facts); |
| 5799 | |
| 5800 | assert!(content.contains("--provider openrouter"), "{content}"); |
| 5801 | assert!(!content.contains("--provider deepseek"), "{content}"); |
| 5802 | assert!( |
| 5803 | content.contains("does not generate deploy bundles"), |
| 5804 | "{content}" |
| 5805 | ); |
| 5806 | assert!( |
| 5807 | content.contains("`--apply` remains unimplemented"), |
| 5808 | "{content}" |
| 5809 | ); |
| 5810 | } |
| 5811 | |
| 5812 | #[test] |
| 5813 | fn remote_runtime_on_ramp_never_substitutes_deepseek_for_named_custom_route() { |
| 5814 | let _guard = crate::test_support::lock_test_env(); |
| 5815 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 5816 | let workspace = tmp.path().join("workspace"); |
| 5817 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 5818 | let codewhale_home = tmp.path().join(".codewhale"); |
| 5819 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 5820 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 5821 | let _codewhale_home = |
| 5822 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 5823 | let mut custom = std::collections::HashMap::new(); |
| 5824 | custom.insert( |
| 5825 | "lm-studio".to_string(), |
| 5826 | crate::config::ProviderConfig { |
| 5827 | kind: Some("openai-compatible".to_string()), |
| 5828 | base_url: Some("http://127.0.0.1:1234/v1".to_string()), |
| 5829 | model: Some("local-code-model".to_string()), |
| 5830 | ..Default::default() |
| 5831 | }, |
| 5832 | ); |
| 5833 | let config = Config { |
| 5834 | provider: Some("lm-studio".to_string()), |
| 5835 | providers: Some(crate::config::ProvidersConfig { |
| 5836 | custom, |
| 5837 | ..Default::default() |
| 5838 | }), |
| 5839 | ..Config::default() |
| 5840 | }; |
| 5841 | let app = App::new(setup_test_options(workspace), &config); |
| 5842 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 5843 | |
| 5844 | let content = remote_runtime_on_ramp_text(Locale::En, &facts); |
| 5845 | |
| 5846 | assert!(content.contains("active route lm-studio"), "{content}"); |
| 5847 | assert!(content.contains("--provider lm-studio"), "{content}"); |
| 5848 | assert!(!content.contains("--provider deepseek"), "{content}"); |
| 5849 | } |
| 5850 | |
| 5851 | #[test] |
| 5852 | fn remote_runtime_on_ramp_is_localized_for_shipped_locales() { |
| 5853 | let facts = SetupRuntimeFacts { |
| 5854 | remote_clouds_result: "3 cloud targets: lighthouse, azure, digitalocean".to_string(), |
| 5855 | remote_bridges_result: "2 chat bridges: feishu, telegram".to_string(), |
| 5856 | remote_providers_result: |
| 5857 | "12 providers from the provider registry; active route deepseek / deepseek-chat" |
| 5858 | .to_string(), |
| 5859 | remote_mode_result: |
| 5860 | "generate-only bundle; --apply not implemented; default port 7878, workers 2" |
| 5861 | .to_string(), |
| 5862 | ..SetupRuntimeFacts::default() |
| 5863 | }; |
| 5864 | let english = remote_runtime_on_ramp_text(Locale::En, &facts); |
| 5865 | |
| 5866 | for locale in Locale::shipped() { |
| 5867 | let content = remote_runtime_on_ramp_text(*locale, &facts); |
| 5868 | assert!( |
| 5869 | content.contains("codewhale remote-setup --generate-only"), |
| 5870 | "{}", |
| 5871 | locale.tag() |
| 5872 | ); |
| 5873 | assert!(content.contains("`--apply`"), "{}", locale.tag()); |
| 5874 | if *locale != Locale::En { |
| 5875 | assert_ne!(content, english, "{}", locale.tag()); |
| 5876 | } |
| 5877 | } |
| 5878 | } |
| 5879 | |
| 5880 | #[test] |
| 5881 | fn guided_constitution_answers_shape_preview_and_saved_payload() { |
| 5882 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5883 | for key_char in ['1', '2', '3', '4', '5', '6'] { |
| 5884 | assert!(matches!( |
| 5885 | view.handle_key(key(KeyCode::Char(key_char))), |
| 5886 | ViewAction::None |
| 5887 | )); |
| 5888 | } |
| 5889 | |
| 5890 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5891 | |
| 5892 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else { |
| 5893 | panic!("expected tuned guided constitution preview event"); |
| 5894 | }; |
| 5895 | assert!(content.contains("current, cited research")); |
| 5896 | assert!(content.contains("ambitious initiative")); |
| 5897 | assert!(content.contains("release evidence")); |
| 5898 | assert!(content.contains("learn the system")); |
| 5899 | assert!(content.contains("sensitive data")); |
| 5900 | assert!(content.contains("user voice")); |
| 5901 | assert!(content.contains("preserve the user's voice")); |
| 5902 | |
| 5903 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5904 | |
| 5905 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 5906 | constitution, |
| 5907 | state, |
| 5908 | .. |
| 5909 | }) = action |
| 5910 | else { |
| 5911 | panic!("expected tuned guided constitution commit event"); |
| 5912 | }; |
| 5913 | assert_eq!( |
| 5914 | constitution.autonomy_preference, |
| 5915 | AutonomyPreference::Autonomous |
| 5916 | ); |
| 5917 | let body = constitution.render_body(); |
| 5918 | assert!(body.contains("current, cited research")); |
| 5919 | assert!(body.contains("release evidence")); |
| 5920 | assert!(body.contains("learn the system")); |
| 5921 | assert!(body.contains("sensitive data")); |
| 5922 | assert!(body.contains("preserve the user's voice")); |
| 5923 | assert_eq!( |
| 5924 | state.constitution_preview_hash.as_deref(), |
| 5925 | Some(constitution.preview_hash().as_str()) |
| 5926 | ); |
| 5927 | } |
| 5928 | |
| 5929 | #[test] |
| 5930 | fn constitution_detail_lines_explain_reduced_core_and_modules_boundary() { |
| 5931 | let view = SetupWizardView::new_at_with_facts( |
| 5932 | SetupState::default(), |
| 5933 | Locale::En, |
| 5934 | SetupStep::Constitution, |
| 5935 | SetupRuntimeFacts::default(), |
| 5936 | ); |
| 5937 | |
| 5938 | let text = lines_to_text(view.constitution_detail_lines()); |
| 5939 | |
| 5940 | assert!(text.contains("user-global preferences only")); |
| 5941 | // The en copy no longer claims a line count for the core (#4057 wave 2 |
| 5942 | // reword: the shipped core outgrew "55-line"). |
| 5943 | assert!(text.contains("bundled core")); |
| 5944 | assert!(text.contains("mode prompts")); |
| 5945 | assert!(text.contains("future opt-ins")); |
| 5946 | } |
| 5947 | |
| 5948 | #[test] |
| 5949 | fn freeform_note_previews_saves_and_stays_advisory() { |
| 5950 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5951 | |
| 5952 | let first_preview = view.handle_key(key(KeyCode::Char('g'))); |
| 5953 | assert!(matches!( |
| 5954 | first_preview, |
| 5955 | ViewAction::Emit(ViewEvent::OpenTextPager { .. }) |
| 5956 | )); |
| 5957 | assert!(view.handle_paste( |
| 5958 | "Prefer reversible demos; do not treat shell unrestricted as permission." |
| 5959 | )); |
| 5960 | |
| 5961 | let second_preview = view.handle_key(key(KeyCode::Char('g'))); |
| 5962 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = second_preview else { |
| 5963 | panic!("freeform note should force a fresh preview"); |
| 5964 | }; |
| 5965 | assert!(content.contains("User freeform principle")); |
| 5966 | assert!(content.contains("Prefer reversible demos")); |
| 5967 | assert!(content.contains("do not change approval, sandbox, shell")); |
| 5968 | |
| 5969 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 5970 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 5971 | constitution, |
| 5972 | state, |
| 5973 | .. |
| 5974 | }) = action |
| 5975 | else { |
| 5976 | panic!("expected guided constitution commit event"); |
| 5977 | }; |
| 5978 | let body = constitution.render_body(); |
| 5979 | assert!(body.contains("User freeform principle")); |
| 5980 | assert!(body.contains("Prefer reversible demos")); |
| 5981 | assert_eq!( |
| 5982 | state.constitution_authoring, |
| 5983 | Some(ConstitutionAuthoring::Guided) |
| 5984 | ); |
| 5985 | assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset); |
| 5986 | } |
| 5987 | |
| 5988 | #[test] |
| 5989 | fn changing_guided_answer_requires_fresh_preview() { |
| 5990 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 5991 | |
| 5992 | let first_preview = view.handle_key(key(KeyCode::Char('g'))); |
| 5993 | assert!(matches!( |
| 5994 | first_preview, |
| 5995 | ViewAction::Emit(ViewEvent::OpenTextPager { .. }) |
| 5996 | )); |
| 5997 | |
| 5998 | assert!(matches!( |
| 5999 | view.handle_key(key(KeyCode::Char('6'))), |
| 6000 | ViewAction::None |
| 6001 | )); |
| 6002 | let second_preview = view.handle_key(key(KeyCode::Char('g'))); |
| 6003 | |
| 6004 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = second_preview else { |
| 6005 | panic!("changed guided answer should preview again before saving"); |
| 6006 | }; |
| 6007 | assert!(content.contains("preserve the user's voice")); |
| 6008 | |
| 6009 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6010 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 6011 | constitution, |
| 6012 | .. |
| 6013 | }) = action |
| 6014 | else { |
| 6015 | panic!("expected save after fresh preview"); |
| 6016 | }; |
| 6017 | assert_eq!( |
| 6018 | constitution.autonomy_preference, |
| 6019 | AutonomyPreference::Balanced |
| 6020 | ); |
| 6021 | assert!( |
| 6022 | constitution |
| 6023 | .render_body() |
| 6024 | .contains("preserve the user's voice") |
| 6025 | ); |
| 6026 | } |
| 6027 | |
| 6028 | fn ready_facts(model: &str) -> SetupRuntimeFacts { |
| 6029 | SetupRuntimeFacts { |
| 6030 | provider_ready: true, |
| 6031 | model: model.to_string(), |
| 6032 | ..SetupRuntimeFacts::default() |
| 6033 | } |
| 6034 | } |
| 6035 | |
| 6036 | fn first_run_ready_state() -> SetupState { |
| 6037 | let mut state = SetupState::default(); |
| 6038 | state.set_step( |
| 6039 | SetupStep::Language, |
| 6040 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 6041 | ); |
| 6042 | state.set_step( |
| 6043 | SetupStep::ProviderModel, |
| 6044 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 6045 | ); |
| 6046 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 6047 | state.complete_constitution_checkpoint( |
| 6048 | CONSTITUTION_CHECKPOINT_VERSION, |
| 6049 | ConstitutionChoice::Bundled, |
| 6050 | ); |
| 6051 | state.set_step( |
| 6052 | SetupStep::Constitution, |
| 6053 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 6054 | ); |
| 6055 | state |
| 6056 | } |
| 6057 | |
| 6058 | fn sample_model_draft() -> Box<UserConstitution> { |
| 6059 | Box::new(UserConstitution { |
| 6060 | language: Some("en".to_string()), |
| 6061 | about: Some("A GLM-5.2 user shipping Rust.".to_string()), |
| 6062 | working_style: vec!["Keep diffs scoped.".to_string()], |
| 6063 | priorities: vec!["Evidence over vibes.".to_string()], |
| 6064 | autonomy_preference: AutonomyPreference::Balanced, |
| 6065 | notes: Some("Advisory only.".to_string()), |
| 6066 | ..UserConstitution::default() |
| 6067 | }) |
| 6068 | } |
| 6069 | |
| 6070 | #[test] |
| 6071 | fn model_draft_key_is_inert_without_a_ready_provider() { |
| 6072 | // Fallback contract: no route, no drafting offer — the deterministic |
| 6073 | // guided flow stands untouched. |
| 6074 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 6075 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 6076 | |
| 6077 | let action = view.handle_key(key(KeyCode::Char('a'))); |
| 6078 | |
| 6079 | assert!(matches!(action, ViewAction::None)); |
| 6080 | assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset); |
| 6081 | } |
| 6082 | |
| 6083 | #[test] |
| 6084 | fn model_draft_key_requests_drafting_with_current_answers() { |
| 6085 | let mut view = SetupWizardView::new_at_with_facts( |
| 6086 | SetupState::default(), |
| 6087 | Locale::En, |
| 6088 | SetupStep::Constitution, |
| 6089 | ready_facts("GLM-5.2"), |
| 6090 | ); |
| 6091 | // Tune one answer first: the request must carry the tuned draft. |
| 6092 | assert!(matches!( |
| 6093 | view.handle_key(key(KeyCode::Char('2'))), |
| 6094 | ViewAction::None |
| 6095 | )); |
| 6096 | assert!(view.handle_paste("Prefer demos before durable rewrites.")); |
| 6097 | |
| 6098 | let action = view.handle_key(key(KeyCode::Char('a'))); |
| 6099 | |
| 6100 | let ViewAction::Emit(ViewEvent::SetupConstitutionModelDraftRequested { |
| 6101 | draft, |
| 6102 | freeform_note, |
| 6103 | locale, |
| 6104 | }) = action |
| 6105 | else { |
| 6106 | panic!("expected model draft request event"); |
| 6107 | }; |
| 6108 | assert_eq!(locale, Locale::En); |
| 6109 | assert_eq!(draft.autonomy, AutonomyPreference::Autonomous); |
| 6110 | assert_eq!( |
| 6111 | freeform_note.as_deref(), |
| 6112 | Some("Prefer demos before durable rewrites.") |
| 6113 | ); |
| 6114 | // The wizard stays open (Emit, not EmitAndClose) and nothing commits. |
| 6115 | assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset); |
| 6116 | } |
| 6117 | |
| 6118 | #[test] |
| 6119 | fn installed_model_draft_previews_then_ratifies_with_provenance() { |
| 6120 | let mut view = SetupWizardView::new_at_with_facts( |
| 6121 | SetupState::default(), |
| 6122 | Locale::En, |
| 6123 | SetupStep::Constitution, |
| 6124 | ready_facts("GLM-5.2"), |
| 6125 | ); |
| 6126 | |
| 6127 | let (title, content) = |
| 6128 | view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string()); |
| 6129 | assert!(title.contains("Draft for Ratification")); |
| 6130 | assert!(content.contains("Drafted by GLM-5.2")); |
| 6131 | assert!(content.contains("A GLM-5.2 user shipping Rust.")); |
| 6132 | assert!(content.contains("<codewhale_user_constitution")); |
| 6133 | |
| 6134 | // The install satisfied the preview gate; G ratifies the model draft. |
| 6135 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6136 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 6137 | constitution, |
| 6138 | state, |
| 6139 | message, |
| 6140 | }) = action |
| 6141 | else { |
| 6142 | panic!("expected ratification commit event"); |
| 6143 | }; |
| 6144 | assert_eq!(constitution, *sample_model_draft()); |
| 6145 | assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom); |
| 6146 | assert_eq!( |
| 6147 | state.constitution_authoring, |
| 6148 | Some(ConstitutionAuthoring::ModelDrafted) |
| 6149 | ); |
| 6150 | assert_eq!( |
| 6151 | state.constitution_preview_hash.as_deref(), |
| 6152 | Some(constitution.preview_hash().as_str()) |
| 6153 | ); |
| 6154 | let step = state.steps.get(&SetupStep::Constitution).expect("step"); |
| 6155 | let result = step.result.as_deref().expect("result"); |
| 6156 | assert!(result.contains("model-drafted constitution ratified (GLM-5.2)")); |
| 6157 | assert!(message.contains("Constitution ratified")); |
| 6158 | } |
| 6159 | |
| 6160 | #[test] |
| 6161 | fn deterministic_ratification_records_guided_authoring() { |
| 6162 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 6163 | |
| 6164 | view.handle_key(key(KeyCode::Char('g'))); |
| 6165 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6166 | |
| 6167 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { state, .. }) = |
| 6168 | action |
| 6169 | else { |
| 6170 | panic!("expected guided commit event"); |
| 6171 | }; |
| 6172 | assert_eq!( |
| 6173 | state.constitution_authoring, |
| 6174 | Some(ConstitutionAuthoring::Guided) |
| 6175 | ); |
| 6176 | } |
| 6177 | |
| 6178 | #[test] |
| 6179 | fn cycling_answers_discards_the_model_draft() { |
| 6180 | let mut view = SetupWizardView::new_at_with_facts( |
| 6181 | SetupState::default(), |
| 6182 | Locale::En, |
| 6183 | SetupStep::Constitution, |
| 6184 | ready_facts("GLM-5.2"), |
| 6185 | ); |
| 6186 | let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string()); |
| 6187 | |
| 6188 | // Changing any answer makes the model draft stale law. |
| 6189 | assert!(matches!( |
| 6190 | view.handle_key(key(KeyCode::Char('1'))), |
| 6191 | ViewAction::None |
| 6192 | )); |
| 6193 | |
| 6194 | // The next G must preview afresh — and preview the guided rendering, |
| 6195 | // not the discarded model draft. |
| 6196 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6197 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else { |
| 6198 | panic!("stale draft should force a fresh preview"); |
| 6199 | }; |
| 6200 | assert!(content.contains("Rendered deterministically")); |
| 6201 | assert!(!content.contains("Drafted by GLM-5.2")); |
| 6202 | |
| 6203 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6204 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { state, .. }) = |
| 6205 | action |
| 6206 | else { |
| 6207 | panic!("expected guided commit after discard"); |
| 6208 | }; |
| 6209 | assert_eq!( |
| 6210 | state.constitution_authoring, |
| 6211 | Some(ConstitutionAuthoring::Guided) |
| 6212 | ); |
| 6213 | } |
| 6214 | |
| 6215 | #[test] |
| 6216 | fn freeform_note_discards_the_model_draft() { |
| 6217 | let mut view = SetupWizardView::new_at_with_facts( |
| 6218 | SetupState::default(), |
| 6219 | Locale::En, |
| 6220 | SetupStep::Constitution, |
| 6221 | ready_facts("GLM-5.2"), |
| 6222 | ); |
| 6223 | let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string()); |
| 6224 | |
| 6225 | assert!(view.handle_paste("Prefer local examples before broad rewrites.")); |
| 6226 | |
| 6227 | let action = view.handle_key(key(KeyCode::Char('g'))); |
| 6228 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else { |
| 6229 | panic!("changed freeform note should force a fresh guided preview"); |
| 6230 | }; |
| 6231 | assert!(content.contains("Rendered deterministically")); |
| 6232 | assert!(content.contains("Prefer local examples")); |
| 6233 | assert!(!content.contains("Drafted by GLM-5.2")); |
| 6234 | } |
| 6235 | |
| 6236 | #[test] |
| 6237 | fn constitution_card_gates_the_model_draft_invitation() { |
| 6238 | // No ready provider: no invitation (and the blocker-size layout holds). |
| 6239 | let not_ready = SetupWizardView::new(SetupState::default(), Locale::En); |
| 6240 | let text = lines_to_text(not_ready.constitution_detail_lines()); |
| 6241 | assert!(!text.contains("can draft it")); |
| 6242 | assert!(!text.contains("awaits ratification")); |
| 6243 | |
| 6244 | // Ready provider: the invitation names the first configured model. |
| 6245 | let ready = SetupWizardView::new_at_with_facts( |
| 6246 | SetupState::default(), |
| 6247 | Locale::En, |
| 6248 | SetupStep::Constitution, |
| 6249 | ready_facts("GLM-5.2"), |
| 6250 | ); |
| 6251 | let text = lines_to_text(ready.constitution_detail_lines()); |
| 6252 | assert!(text.contains("GLM-5.2 can draft it. You ratify it.")); |
| 6253 | |
| 6254 | // Installed draft: the card flips to the awaiting-ratification line. |
| 6255 | let mut with_draft = ready.clone(); |
| 6256 | let _ = with_draft.install_model_draft(sample_model_draft(), "GLM-5.2".to_string()); |
| 6257 | let text = lines_to_text(with_draft.constitution_detail_lines()); |
| 6258 | assert!(text.contains("Draft by GLM-5.2 awaits ratification")); |
| 6259 | assert!(!text.contains("GLM-5.2 can draft it")); |
| 6260 | } |
| 6261 | |
| 6262 | #[test] |
| 6263 | fn model_drafted_commit_round_trips_through_the_setup_transaction() { |
| 6264 | let _guard = crate::test_support::lock_test_env(); |
| 6265 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6266 | let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path()); |
| 6267 | |
| 6268 | let mut view = SetupWizardView::new_at_with_facts( |
| 6269 | SetupState::default(), |
| 6270 | Locale::En, |
| 6271 | SetupStep::Constitution, |
| 6272 | ready_facts("GLM-5.2"), |
| 6273 | ); |
| 6274 | let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string()); |
| 6275 | let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 6276 | constitution, |
| 6277 | state, |
| 6278 | .. |
| 6279 | }) = view.handle_key(key(KeyCode::Char('g'))) |
| 6280 | else { |
| 6281 | panic!("expected ratification commit event"); |
| 6282 | }; |
| 6283 | |
| 6284 | persist_user_constitution_choice(&constitution, &state).expect("persist"); |
| 6285 | |
| 6286 | let loaded = UserConstitution::load().expect("load constitution"); |
| 6287 | let loaded = loaded.constitution().expect("valid constitution"); |
| 6288 | assert_eq!(loaded.render_body(), constitution.render_body()); |
| 6289 | let loaded_state = SetupState::load().expect("load state").expect("state"); |
| 6290 | assert_eq!( |
| 6291 | loaded_state.constitution_authoring, |
| 6292 | Some(ConstitutionAuthoring::ModelDrafted) |
| 6293 | ); |
| 6294 | assert_eq!( |
| 6295 | loaded_state.constitution_preview_hash.as_deref(), |
| 6296 | Some(constitution.preview_hash().as_str()) |
| 6297 | ); |
| 6298 | } |
| 6299 | |
| 6300 | #[test] |
| 6301 | fn guided_constitution_template_localizes_content() { |
| 6302 | let english = guided_constitution_template(Locale::En).render_body(); |
| 6303 | let zh_hans = guided_constitution_template(Locale::ZhHans).render_body(); |
| 6304 | |
| 6305 | assert!(english.contains("evidence-first coding workbench")); |
| 6306 | assert!(zh_hans.contains("重证据")); |
| 6307 | assert_ne!(english, zh_hans); |
| 6308 | |
| 6309 | let markers = [ |
| 6310 | (Locale::Ja, "証拠重視"), |
| 6311 | (Locale::ZhHans, "重证据"), |
| 6312 | (Locale::ZhHant, "重證據"), |
| 6313 | (Locale::PtBr, "guiada por evidências"), |
| 6314 | (Locale::Es419, "basada en evidencia"), |
| 6315 | (Locale::Vi, "ưu tiên bằng chứng"), |
| 6316 | (Locale::Ko, "근거 중심"), |
| 6317 | ]; |
| 6318 | for (locale, marker) in markers { |
| 6319 | let body = guided_constitution_template(locale).render_body(); |
| 6320 | assert!( |
| 6321 | body.contains(marker), |
| 6322 | "missing localized guided marker for {}", |
| 6323 | locale.tag() |
| 6324 | ); |
| 6325 | assert_ne!( |
| 6326 | english, |
| 6327 | body, |
| 6328 | "locale {} fell back to English", |
| 6329 | locale.tag() |
| 6330 | ); |
| 6331 | assert!( |
| 6332 | !body.contains("A Codewhale user who wants"), |
| 6333 | "locale {} reused English purpose copy", |
| 6334 | locale.tag() |
| 6335 | ); |
| 6336 | assert!( |
| 6337 | !body.contains("Guided answers:"), |
| 6338 | "locale {} reused English guided-answer notes", |
| 6339 | locale.tag() |
| 6340 | ); |
| 6341 | assert!( |
| 6342 | !body.contains("Current user requests and live tool evidence"), |
| 6343 | "locale {} reused English authority priority", |
| 6344 | locale.tag() |
| 6345 | ); |
| 6346 | } |
| 6347 | } |
| 6348 | |
| 6349 | #[test] |
| 6350 | fn ratification_preview_uses_rendered_block_and_layer_order() { |
| 6351 | let draft = GuidedConstitutionDraft::default(); |
| 6352 | let english = constitution_ratification_text( |
| 6353 | Locale::En, |
| 6354 | &draft.to_constitution(Locale::En), |
| 6355 | &DraftProvenance::Guided, |
| 6356 | ); |
| 6357 | let zh_hans = constitution_ratification_text( |
| 6358 | Locale::ZhHans, |
| 6359 | &draft.to_constitution(Locale::ZhHans), |
| 6360 | &DraftProvenance::Guided, |
| 6361 | ); |
| 6362 | |
| 6363 | assert!(english.contains("<codewhale_user_constitution")); |
| 6364 | assert!(english.contains("Layer order")); |
| 6365 | assert!(english.contains("press G to ratify and save")); |
| 6366 | // Framing: powers and limits, not case-by-case; continuity, not memory. |
| 6367 | assert!(english.contains("powers and limits rather than deciding every case")); |
| 6368 | assert!(english.contains("but it is not memory")); |
| 6369 | assert!(zh_hans.contains("<codewhale_user_constitution")); |
| 6370 | assert!(zh_hans.contains("按 G 确认并保存")); |
| 6371 | assert!(zh_hans.contains("它界定协作方式与行为边界")); |
| 6372 | assert!(zh_hans.contains("但它不是记忆")); |
| 6373 | assert_ne!(english, zh_hans); |
| 6374 | |
| 6375 | let localized_markers = [ |
| 6376 | (Locale::Ja, "権限の階層"), |
| 6377 | (Locale::ZhHans, "精简核心与可选策略"), |
| 6378 | (Locale::ZhHant, "精簡核心與可選模組"), |
| 6379 | (Locale::PtBr, "NÚCLEO REDUZIDO E MÓDULOS OPT-IN"), |
| 6380 | (Locale::Es419, "NÚCLEO REDUCIDO Y MÓDULOS OPT-IN"), |
| 6381 | (Locale::Vi, "LÕI RÚT GỌN VÀ MÔ-ĐUN OPT-IN"), |
| 6382 | (Locale::Ko, "축소된 코어와 옵트인 모듈"), |
| 6383 | ]; |
| 6384 | for (locale, marker) in localized_markers { |
| 6385 | let content = constitution_ratification_text( |
| 6386 | locale, |
| 6387 | &draft.to_constitution(locale), |
| 6388 | &DraftProvenance::Guided, |
| 6389 | ); |
| 6390 | assert!( |
| 6391 | content.contains(marker), |
| 6392 | "missing localized ratification marker for {}", |
| 6393 | locale.tag() |
| 6394 | ); |
| 6395 | assert_ne!( |
| 6396 | english, |
| 6397 | content, |
| 6398 | "locale {} ratification preview fell back to English", |
| 6399 | locale.tag() |
| 6400 | ); |
| 6401 | for fallback in [ |
| 6402 | "CODEWHALE · USER CONSTITUTION", |
| 6403 | "HIERARCHY OF AUTHORITY", |
| 6404 | "WHAT THIS CANNOT DO", |
| 6405 | "REDUCED CORE AND OPT-IN MODULES", |
| 6406 | "Rendered deterministically from your guided answers", |
| 6407 | "Nothing becomes law until you confirm", |
| 6408 | ] { |
| 6409 | assert!( |
| 6410 | !content.contains(fallback), |
| 6411 | "locale {} reused English ratification scaffold: {fallback}", |
| 6412 | locale.tag() |
| 6413 | ); |
| 6414 | } |
| 6415 | } |
| 6416 | } |
| 6417 | |
| 6418 | #[test] |
| 6419 | fn ratification_preview_states_authority_boundaries_and_provenance() { |
| 6420 | let draft = GuidedConstitutionDraft::default(); |
| 6421 | let constitution = draft.to_constitution(Locale::En); |
| 6422 | |
| 6423 | let guided = |
| 6424 | constitution_ratification_text(Locale::En, &constitution, &DraftProvenance::Guided); |
| 6425 | assert!(guided.contains("HIERARCHY OF AUTHORITY")); |
| 6426 | assert!(guided.contains("WHAT THIS CANNOT DO")); |
| 6427 | assert!(guided.contains("cannot grant or change approval policy")); |
| 6428 | assert!(guided.contains("Nothing becomes law until you confirm")); |
| 6429 | assert!(guided.contains("Rendered deterministically")); |
| 6430 | |
| 6431 | let drafted = constitution_ratification_text( |
| 6432 | Locale::En, |
| 6433 | &constitution, |
| 6434 | &DraftProvenance::Model("GLM-5.2".to_string()), |
| 6435 | ); |
| 6436 | assert!(drafted.contains("Drafted by GLM-5.2")); |
| 6437 | assert!(drafted.contains("schema-checked and bounded by Codewhale")); |
| 6438 | |
| 6439 | let zh = constitution_ratification_text( |
| 6440 | Locale::ZhHans, |
| 6441 | &draft.to_constitution(Locale::ZhHans), |
| 6442 | &DraftProvenance::Model("GLM-5.2".to_string()), |
| 6443 | ); |
| 6444 | assert!(zh.contains("权限层级")); |
| 6445 | assert!(zh.contains("它不能做什么")); |
| 6446 | assert!(zh.contains("由 GLM-5.2 根据你的引导式答案起草")); |
| 6447 | } |
| 6448 | |
| 6449 | #[test] |
| 6450 | fn guided_constitution_detail_lines_show_localized_answers() { |
| 6451 | let english = SetupWizardView::new(SetupState::default(), Locale::En); |
| 6452 | let english_text = lines_to_text(english.constitution_detail_lines()); |
| 6453 | assert!(english_text.contains("Purpose:")); |
| 6454 | assert!(english_text.contains("coding workbench")); |
| 6455 | assert!(english_text.contains("Initiative:")); |
| 6456 | assert!(english_text.contains("balanced")); |
| 6457 | assert!(english_text.contains("Principles:")); |
| 6458 | assert!(english_text.contains("scoped changes")); |
| 6459 | |
| 6460 | let zh_hans = SetupWizardView::new(SetupState::default(), Locale::ZhHans); |
| 6461 | let zh_hans_text = lines_to_text(zh_hans.constitution_detail_lines()); |
| 6462 | assert!(zh_hans_text.contains("用途:")); |
| 6463 | assert!(zh_hans_text.contains("编码工作台")); |
| 6464 | assert!(zh_hans_text.contains("主动性:")); |
| 6465 | assert!(zh_hans_text.contains("平衡")); |
| 6466 | assert!(zh_hans_text.contains("原则:")); |
| 6467 | assert!(zh_hans_text.contains("小范围改动")); |
| 6468 | |
| 6469 | for locale in Locale::shipped() |
| 6470 | .iter() |
| 6471 | .copied() |
| 6472 | .filter(|locale| *locale != Locale::En) |
| 6473 | { |
| 6474 | let view = SetupWizardView::new(SetupState::default(), locale); |
| 6475 | let text = lines_to_text(view.constitution_detail_lines()); |
| 6476 | assert!( |
| 6477 | text.contains(&*GuidedPurpose::Coding.label(locale)), |
| 6478 | "missing localized purpose answer for {}", |
| 6479 | locale.tag() |
| 6480 | ); |
| 6481 | assert!( |
| 6482 | text.contains(autonomy_label(AutonomyPreference::Balanced, locale)), |
| 6483 | "missing localized autonomy answer for {}", |
| 6484 | locale.tag() |
| 6485 | ); |
| 6486 | assert!( |
| 6487 | text.contains(GuidedPrinciples::ScopedChanges.label(locale)), |
| 6488 | "missing localized principle answer for {}", |
| 6489 | locale.tag() |
| 6490 | ); |
| 6491 | assert!( |
| 6492 | !text.contains("Purpose:"), |
| 6493 | "locale {} reused English detail label", |
| 6494 | locale.tag() |
| 6495 | ); |
| 6496 | assert!( |
| 6497 | !text.contains("not checked yet"), |
| 6498 | "locale {} reused English file-state detail", |
| 6499 | locale.tag() |
| 6500 | ); |
| 6501 | } |
| 6502 | } |
| 6503 | |
| 6504 | #[test] |
| 6505 | fn constitution_file_state_labels_existing_override_states() { |
| 6506 | assert!( |
| 6507 | SetupConstitutionFileState::Missing |
| 6508 | .label(ConstitutionChoice::Bundled, Locale::En) |
| 6509 | .contains("no constitution.json") |
| 6510 | ); |
| 6511 | assert!( |
| 6512 | SetupConstitutionFileState::Loaded |
| 6513 | .label(ConstitutionChoice::GuidedCustom, Locale::En) |
| 6514 | .contains("selected") |
| 6515 | ); |
| 6516 | assert!( |
| 6517 | SetupConstitutionFileState::Loaded |
| 6518 | .label(ConstitutionChoice::Bundled, Locale::En) |
| 6519 | .contains("inactive") |
| 6520 | ); |
| 6521 | assert!( |
| 6522 | SetupConstitutionFileState::Invalid |
| 6523 | .label(ConstitutionChoice::Unset, Locale::En) |
| 6524 | .contains("invalid") |
| 6525 | ); |
| 6526 | assert!( |
| 6527 | SetupConstitutionFileState::Unreadable |
| 6528 | .label(ConstitutionChoice::Unset, Locale::En) |
| 6529 | .contains("unreadable") |
| 6530 | ); |
| 6531 | assert!( |
| 6532 | SetupConstitutionFileState::PathError |
| 6533 | .label(ConstitutionChoice::Unset, Locale::ZhHans) |
| 6534 | .contains("CODEWHALE_HOME") |
| 6535 | ); |
| 6536 | } |
| 6537 | |
| 6538 | #[test] |
| 6539 | fn expert_override_state_requires_content_and_opt_in() { |
| 6540 | let _guard = crate::test_support::lock_test_env(); |
| 6541 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6542 | let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path()); |
| 6543 | let _opt_in = crate::test_support::EnvVarGuard::remove(BASE_PROMPT_OVERRIDE_OPT_IN_ENV); |
| 6544 | |
| 6545 | assert_eq!( |
| 6546 | SetupExpertOverrideState::load(), |
| 6547 | SetupExpertOverrideState::Missing |
| 6548 | ); |
| 6549 | |
| 6550 | let path = tmp.path().join(CONSTITUTION_OVERRIDE_FILE); |
| 6551 | std::fs::create_dir_all(path.parent().expect("override parent")).expect("override parent"); |
| 6552 | std::fs::write(&path, "\n \n").expect("write empty override"); |
| 6553 | assert_eq!( |
| 6554 | SetupExpertOverrideState::load(), |
| 6555 | SetupExpertOverrideState::Empty |
| 6556 | ); |
| 6557 | |
| 6558 | std::fs::write(&path, "# Expert override\n").expect("write override"); |
| 6559 | assert_eq!( |
| 6560 | SetupExpertOverrideState::load(), |
| 6561 | SetupExpertOverrideState::Disabled |
| 6562 | ); |
| 6563 | assert!(!SetupExpertOverrideState::Disabled.is_active()); |
| 6564 | assert!( |
| 6565 | SetupExpertOverrideState::Disabled |
| 6566 | .label(Locale::En) |
| 6567 | .contains(BASE_PROMPT_OVERRIDE_OPT_IN_ENV) |
| 6568 | ); |
| 6569 | |
| 6570 | // SAFETY: the process-wide test env mutex is held by `_guard`. |
| 6571 | unsafe { std::env::set_var(BASE_PROMPT_OVERRIDE_OPT_IN_ENV, "1") }; |
| 6572 | assert_eq!( |
| 6573 | SetupExpertOverrideState::load(), |
| 6574 | SetupExpertOverrideState::Active |
| 6575 | ); |
| 6576 | assert!(SetupExpertOverrideState::Active.is_active()); |
| 6577 | } |
| 6578 | |
| 6579 | #[test] |
| 6580 | fn constitution_detail_lines_show_existing_file_state() { |
| 6581 | let mut state = SetupState { |
| 6582 | constitution_choice: ConstitutionChoice::Bundled, |
| 6583 | constitution_source: ConstitutionSource::Bundled, |
| 6584 | constitution_validity: ConstitutionValidity::Valid, |
| 6585 | ..SetupState::default() |
| 6586 | }; |
| 6587 | let facts = SetupRuntimeFacts { |
| 6588 | constitution_file: SetupConstitutionFileState::Loaded, |
| 6589 | ..SetupRuntimeFacts::default() |
| 6590 | }; |
| 6591 | let view = SetupWizardView::new_at_with_facts( |
| 6592 | state.clone(), |
| 6593 | Locale::En, |
| 6594 | SetupStep::Constitution, |
| 6595 | facts, |
| 6596 | ); |
| 6597 | |
| 6598 | let text = lines_to_text(view.constitution_detail_lines()); |
| 6599 | assert!(text.contains("Source: bundled; validity valid")); |
| 6600 | assert!(text.contains("Existing file:")); |
| 6601 | assert!(text.contains("inactive under the recorded choice")); |
| 6602 | assert!(text.contains("Expert override:")); |
| 6603 | assert!(text.contains("not checked yet")); |
| 6604 | |
| 6605 | state.constitution_choice = ConstitutionChoice::GuidedCustom; |
| 6606 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 6607 | let view = SetupWizardView::new_at_with_facts( |
| 6608 | state, |
| 6609 | Locale::ZhHans, |
| 6610 | SetupStep::Constitution, |
| 6611 | SetupRuntimeFacts { |
| 6612 | constitution_file: SetupConstitutionFileState::Loaded, |
| 6613 | ..SetupRuntimeFacts::default() |
| 6614 | }, |
| 6615 | ); |
| 6616 | let text = lines_to_text(view.constitution_detail_lines()); |
| 6617 | assert!(text.contains("现有文件:")); |
| 6618 | assert!(text.contains("已存在并已选择")); |
| 6619 | assert!(text.contains("专家覆盖:")); |
| 6620 | } |
| 6621 | |
| 6622 | #[test] |
| 6623 | fn setup_wizard_is_usable_and_opaque_at_blocker_sizes() { |
| 6624 | use crate::tui::views::ViewStack; |
| 6625 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 6626 | use unicode_width::UnicodeWidthStr; |
| 6627 | |
| 6628 | const BLOCKER_SIZES: [(u16, u16); 4] = [(80, 24), (100, 30), (120, 32), (160, 40)]; |
| 6629 | for (w, h) in BLOCKER_SIZES { |
| 6630 | let area = Rect::new(0, 0, w, h); |
| 6631 | let mut buf = Buffer::empty(area); |
| 6632 | for y in 0..h { |
| 6633 | for x in 0..w { |
| 6634 | buf[(x, y)].set_symbol("X"); |
| 6635 | } |
| 6636 | } |
| 6637 | let mut stack = ViewStack::new(); |
| 6638 | stack.push(SetupWizardView::new_at_with_facts( |
| 6639 | SetupState::default(), |
| 6640 | Locale::En, |
| 6641 | SetupStep::Constitution, |
| 6642 | SetupRuntimeFacts { |
| 6643 | constitution_file: SetupConstitutionFileState::Loaded, |
| 6644 | ..SetupRuntimeFacts::default() |
| 6645 | }, |
| 6646 | )); |
| 6647 | stack.render(area, &mut buf); |
| 6648 | |
| 6649 | let rows: Vec<String> = (0..h) |
| 6650 | .map(|y| (0..w).map(|x| buf[(x, y)].symbol().to_string()).collect()) |
| 6651 | .collect(); |
| 6652 | let text = rows.join("\n"); |
| 6653 | |
| 6654 | for label in [ |
| 6655 | "Setup", |
| 6656 | "Choice:", |
| 6657 | "Existing file:", |
| 6658 | "Purpose:", |
| 6659 | "preview/ratify", |
| 6660 | "use bundled", |
| 6661 | "cancel", |
| 6662 | ] { |
| 6663 | assert!(text.contains(label), "{w}x{h}: missing '{label}'"); |
| 6664 | } |
| 6665 | assert!( |
| 6666 | !text.contains('X'), |
| 6667 | "{w}x{h}: background bleed-through into setup modal" |
| 6668 | ); |
| 6669 | assert!( |
| 6670 | [palette::WHALE_BG, palette::WHALE_PANEL].contains(&buf[(w / 2, h / 2)].bg), |
| 6671 | "{w}x{h}: modal interior must be opaque" |
| 6672 | ); |
| 6673 | for (y, row) in rows.iter().enumerate() { |
| 6674 | assert!( |
| 6675 | UnicodeWidthStr::width(row.trim_end()) <= usize::from(w), |
| 6676 | "{w}x{h}: row {y} overflows width: {row:?}" |
| 6677 | ); |
| 6678 | } |
| 6679 | } |
| 6680 | } |
| 6681 | |
| 6682 | #[test] |
| 6683 | fn persist_user_constitution_choice_writes_constitution_and_state() { |
| 6684 | let _guard = crate::test_support::lock_test_env(); |
| 6685 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6686 | let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path()); |
| 6687 | let constitution = guided_constitution_template(Locale::En); |
| 6688 | let mut state = SetupState::default(); |
| 6689 | state.complete_constitution_checkpoint( |
| 6690 | CONSTITUTION_CHECKPOINT_VERSION, |
| 6691 | ConstitutionChoice::GuidedCustom, |
| 6692 | ); |
| 6693 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 6694 | state.constitution_validity = ConstitutionValidity::Valid; |
| 6695 | state.constitution_preview_hash = Some(constitution.preview_hash()); |
| 6696 | state.set_step( |
| 6697 | SetupStep::Constitution, |
| 6698 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION), |
| 6699 | ); |
| 6700 | |
| 6701 | persist_user_constitution_choice(&constitution, &state).expect("persist constitution"); |
| 6702 | |
| 6703 | let loaded_constitution = UserConstitution::load().expect("load constitution"); |
| 6704 | assert!(matches!( |
| 6705 | loaded_constitution, |
| 6706 | UserConstitutionLoad::Loaded(_) |
| 6707 | )); |
| 6708 | let loaded_state = SetupState::load() |
| 6709 | .expect("load setup state") |
| 6710 | .expect("setup state"); |
| 6711 | assert_eq!( |
| 6712 | loaded_state.constitution_choice, |
| 6713 | ConstitutionChoice::GuidedCustom |
| 6714 | ); |
| 6715 | assert_eq!( |
| 6716 | loaded_state |
| 6717 | .constitution_checkpoint_completed_for |
| 6718 | .as_deref(), |
| 6719 | Some(CONSTITUTION_CHECKPOINT_VERSION) |
| 6720 | ); |
| 6721 | } |
| 6722 | |
| 6723 | #[test] |
| 6724 | fn keep_existing_constitution_previews_then_completes_without_rewriting() { |
| 6725 | let _guard = crate::test_support::lock_test_env(); |
| 6726 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6727 | let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path()); |
| 6728 | |
| 6729 | // An existing valid custom constitution from a prior version. |
| 6730 | let existing = guided_constitution_template(Locale::En); |
| 6731 | persist_user_constitution_choice(&existing, &SetupState::default()) |
| 6732 | .expect("write existing constitution"); |
| 6733 | let path = UserConstitution::path().expect("constitution path"); |
| 6734 | let bytes_before = std::fs::read(&path).expect("existing file bytes"); |
| 6735 | |
| 6736 | let facts = SetupRuntimeFacts { |
| 6737 | constitution_file: SetupConstitutionFileState::Loaded, |
| 6738 | ..SetupRuntimeFacts::default() |
| 6739 | }; |
| 6740 | let mut view = SetupWizardView::new_at_with_facts( |
| 6741 | SetupState::default(), |
| 6742 | Locale::En, |
| 6743 | SetupStep::Constitution, |
| 6744 | facts, |
| 6745 | ); |
| 6746 | |
| 6747 | // The card offers the keep path. |
| 6748 | let text = lines_to_text(view.constitution_detail_lines()); |
| 6749 | assert!(text.contains("K Keep your existing constitution"), "{text}"); |
| 6750 | |
| 6751 | // First K previews the existing law, unchanged, with keep wording. |
| 6752 | let action = view.handle_key(key(KeyCode::Char('k'))); |
| 6753 | let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else { |
| 6754 | panic!("expected keep-existing preview event"); |
| 6755 | }; |
| 6756 | assert!(title.contains("Draft for Ratification")); |
| 6757 | assert!(content.contains("shown unchanged"), "{content}"); |
| 6758 | assert!(content.contains("press K to keep it"), "{content}"); |
| 6759 | assert!( |
| 6760 | content.contains("<codewhale_user_constitution"), |
| 6761 | "{content}" |
| 6762 | ); |
| 6763 | |
| 6764 | // Second K completes the checkpoint without touching the file. |
| 6765 | let action = view.handle_key(key(KeyCode::Char('k'))); |
| 6766 | let ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { state, message }) = |
| 6767 | action |
| 6768 | else { |
| 6769 | panic!("expected keep-existing commit event"); |
| 6770 | }; |
| 6771 | assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom); |
| 6772 | assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal); |
| 6773 | assert_eq!(state.constitution_validity, ConstitutionValidity::Valid); |
| 6774 | assert_eq!( |
| 6775 | state.constitution_checkpoint_completed_for.as_deref(), |
| 6776 | Some(CONSTITUTION_CHECKPOINT_VERSION) |
| 6777 | ); |
| 6778 | assert_eq!( |
| 6779 | state.constitution_preview_hash.as_deref(), |
| 6780 | Some(existing.preview_hash().as_str()) |
| 6781 | ); |
| 6782 | assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified); |
| 6783 | assert!(message.contains("Constitution kept"), "{message}"); |
| 6784 | |
| 6785 | let bytes_after = std::fs::read(&path).expect("file bytes after keep"); |
| 6786 | assert_eq!(bytes_before, bytes_after, "keep must not rewrite the file"); |
| 6787 | } |
| 6788 | |
| 6789 | #[test] |
| 6790 | fn keep_key_is_inert_without_a_valid_existing_constitution() { |
| 6791 | let _guard = crate::test_support::lock_test_env(); |
| 6792 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6793 | let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path()); |
| 6794 | |
| 6795 | for file_state in [ |
| 6796 | SetupConstitutionFileState::Missing, |
| 6797 | SetupConstitutionFileState::Invalid, |
| 6798 | SetupConstitutionFileState::Empty, |
| 6799 | ] { |
| 6800 | let facts = SetupRuntimeFacts { |
| 6801 | constitution_file: file_state, |
| 6802 | ..SetupRuntimeFacts::default() |
| 6803 | }; |
| 6804 | let mut view = SetupWizardView::new_at_with_facts( |
| 6805 | SetupState::default(), |
| 6806 | Locale::En, |
| 6807 | SetupStep::Constitution, |
| 6808 | facts, |
| 6809 | ); |
| 6810 | let text = lines_to_text(view.constitution_detail_lines()); |
| 6811 | assert!( |
| 6812 | !text.contains("K Keep your existing constitution"), |
| 6813 | "{file_state:?} must not offer keep: {text}" |
| 6814 | ); |
| 6815 | assert!( |
| 6816 | matches!(view.handle_key(key(KeyCode::Char('k'))), ViewAction::None), |
| 6817 | "{file_state:?} must leave K inert" |
| 6818 | ); |
| 6819 | } |
| 6820 | } |
| 6821 | |
| 6822 | #[test] |
| 6823 | fn provider_model_review_records_ready_route_and_continues() { |
| 6824 | let facts = SetupRuntimeFacts { |
| 6825 | provider: "DeepSeek".to_string(), |
| 6826 | model: "deepseek-v4-pro".to_string(), |
| 6827 | auth: "present".to_string(), |
| 6828 | health: "ready".to_string(), |
| 6829 | provider_ready: true, |
| 6830 | provider_result: |
| 6831 | "provider=deepseek, model=deepseek-v4-pro, auth=present/local, health=not checked" |
| 6832 | .to_string(), |
| 6833 | ..SetupRuntimeFacts::default() |
| 6834 | }; |
| 6835 | let mut view = SetupWizardView::new_at_with_facts( |
| 6836 | SetupState::default(), |
| 6837 | Locale::En, |
| 6838 | SetupStep::ProviderModel, |
| 6839 | facts, |
| 6840 | ); |
| 6841 | |
| 6842 | let action = view.handle_key(key(KeyCode::Enter)); |
| 6843 | |
| 6844 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 6845 | else { |
| 6846 | panic!("expected setup-state commit event"); |
| 6847 | }; |
| 6848 | assert_eq!(state.status(SetupStep::ProviderModel), StepStatus::Verified); |
| 6849 | assert_eq!(view.selected_step(), SetupStep::TrustSandbox); |
| 6850 | assert!(message.contains("Provider/model readiness recorded")); |
| 6851 | } |
| 6852 | |
| 6853 | #[test] |
| 6854 | fn provider_model_review_records_missing_auth_as_needs_action() { |
| 6855 | let facts = SetupRuntimeFacts { |
| 6856 | provider_ready: false, |
| 6857 | provider_result: |
| 6858 | "provider=deepseek, model=deepseek-v4-pro, auth=missing, health=needs action" |
| 6859 | .to_string(), |
| 6860 | ..SetupRuntimeFacts::default() |
| 6861 | }; |
| 6862 | let mut view = SetupWizardView::new_at_with_facts( |
| 6863 | SetupState::default(), |
| 6864 | Locale::En, |
| 6865 | SetupStep::ProviderModel, |
| 6866 | facts, |
| 6867 | ); |
| 6868 | |
| 6869 | let action = view.handle_key(key(KeyCode::Enter)); |
| 6870 | |
| 6871 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 6872 | else { |
| 6873 | panic!("expected setup-state commit event"); |
| 6874 | }; |
| 6875 | assert_eq!( |
| 6876 | state.status(SetupStep::ProviderModel), |
| 6877 | StepStatus::NeedsAction |
| 6878 | ); |
| 6879 | assert!(message.contains("needs action")); |
| 6880 | } |
| 6881 | |
| 6882 | #[test] |
| 6883 | fn observed_provider_failure_records_needs_action_not_verified() { |
| 6884 | let _guard = crate::test_support::lock_test_env(); |
| 6885 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6886 | let workspace = tmp.path().join("workspace"); |
| 6887 | std::fs::create_dir_all(&workspace).expect("workspace"); |
| 6888 | let codewhale_home = tmp.path().join(".codewhale"); |
| 6889 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 6890 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 6891 | let _codewhale_home = |
| 6892 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 6893 | let _deepseek_env = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY"); |
| 6894 | let config = Config { |
| 6895 | api_key: Some("saved-deepseek-key".to_string()), |
| 6896 | ..Default::default() |
| 6897 | }; |
| 6898 | let mut app = App::new(setup_test_options(workspace), &config); |
| 6899 | app.api_provider = crate::config::ApiProvider::Deepseek; |
| 6900 | app.model = "deepseek-v4-pro".to_string(); |
| 6901 | app.provider_health.record_failure_message( |
| 6902 | &config, |
| 6903 | crate::config::ApiProvider::Deepseek, |
| 6904 | "deepseek-v4-pro", |
| 6905 | crate::error_taxonomy::ErrorCategory::Authentication, |
| 6906 | "credential rejected", |
| 6907 | ); |
| 6908 | |
| 6909 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 6910 | assert!(!facts.provider_ready); |
| 6911 | assert!(facts.auth.contains("last check failed"), "{}", facts.auth); |
| 6912 | assert!(facts.provider_result.contains("health=needs action")); |
| 6913 | |
| 6914 | let mut view = SetupWizardView::new_at_with_facts( |
| 6915 | SetupState::default(), |
| 6916 | Locale::En, |
| 6917 | SetupStep::ProviderModel, |
| 6918 | facts, |
| 6919 | ); |
| 6920 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = |
| 6921 | view.handle_key(key(KeyCode::Enter)) |
| 6922 | else { |
| 6923 | panic!("expected setup-state commit event"); |
| 6924 | }; |
| 6925 | assert_eq!( |
| 6926 | state.status(SetupStep::ProviderModel), |
| 6927 | StepStatus::NeedsAction |
| 6928 | ); |
| 6929 | } |
| 6930 | |
| 6931 | #[test] |
| 6932 | fn runtime_posture_review_confirms_without_config_mutation() { |
| 6933 | let facts = SetupRuntimeFacts { |
| 6934 | runtime_result: "intent=agent, approval=suggest, shell=enabled, trust=workspace, sandbox=default, network=prompt by default".to_string(), |
| 6935 | ..SetupRuntimeFacts::default() |
| 6936 | }; |
| 6937 | let mut view = SetupWizardView::new_at_with_facts( |
| 6938 | SetupState::default(), |
| 6939 | Locale::En, |
| 6940 | SetupStep::TrustSandbox, |
| 6941 | facts, |
| 6942 | ); |
| 6943 | |
| 6944 | let action = view.handle_key(key(KeyCode::Enter)); |
| 6945 | |
| 6946 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 6947 | else { |
| 6948 | panic!("expected setup-state commit event"); |
| 6949 | }; |
| 6950 | assert_eq!(state.status(SetupStep::TrustSandbox), StepStatus::Verified); |
| 6951 | assert_eq!( |
| 6952 | state.runtime_posture_source, |
| 6953 | RuntimePostureSource::Confirmed |
| 6954 | ); |
| 6955 | assert!(message.contains("Runtime posture reviewed")); |
| 6956 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 6957 | } |
| 6958 | |
| 6959 | #[test] |
| 6960 | fn runtime_posture_review_result_redacts_secret_config() { |
| 6961 | let _guard = crate::test_support::lock_test_env(); |
| 6962 | let tmp = tempfile::TempDir::new().expect("tempdir"); |
| 6963 | let workspace = tmp.path().join("workspace"); |
| 6964 | std::fs::create_dir_all(&workspace).expect("workspace dir"); |
| 6965 | let codewhale_home = tmp.path().join(".codewhale"); |
| 6966 | let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path()); |
| 6967 | let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path()); |
| 6968 | let _codewhale_home = |
| 6969 | crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home); |
| 6970 | |
| 6971 | let mut config = Config { |
| 6972 | api_key: Some("sk-runtime-posture-secret".to_string()), |
| 6973 | sandbox_api_key: Some("sandbox-runtime-secret".to_string()), |
| 6974 | approval_policy: Some("on-request".to_string()), |
| 6975 | sandbox_mode: Some("workspace-write".to_string()), |
| 6976 | ..Config::default() |
| 6977 | }; |
| 6978 | config.default_text_model = Some("deepseek-v4-pro".to_string()); |
| 6979 | let app = App::new(setup_test_options(workspace), &config); |
| 6980 | let facts = SetupRuntimeFacts::from_app_config(&app, &config); |
| 6981 | let mut view = SetupWizardView::new_at_with_facts( |
| 6982 | SetupState::default(), |
| 6983 | Locale::En, |
| 6984 | SetupStep::TrustSandbox, |
| 6985 | facts, |
| 6986 | ); |
| 6987 | |
| 6988 | let action = view.handle_key(key(KeyCode::Enter)); |
| 6989 | |
| 6990 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = action else { |
| 6991 | panic!("expected runtime posture commit event"); |
| 6992 | }; |
| 6993 | let result = state |
| 6994 | .steps |
| 6995 | .get(&SetupStep::TrustSandbox) |
| 6996 | .and_then(|entry| entry.result.as_deref()) |
| 6997 | .expect("runtime posture result"); |
| 6998 | assert!(result.contains("intent=agent"), "{result}"); |
| 6999 | assert!(result.contains("sandbox=workspace-write"), "{result}"); |
| 7000 | for forbidden in [ |
| 7001 | "sk-runtime-posture-secret", |
| 7002 | "sandbox-runtime-secret", |
| 7003 | "api_key", |
| 7004 | "sandbox_api_key", |
| 7005 | "secret", |
| 7006 | ] { |
| 7007 | assert!( |
| 7008 | !result.contains(forbidden), |
| 7009 | "runtime posture result leaked {forbidden}: {result}" |
| 7010 | ); |
| 7011 | } |
| 7012 | } |
| 7013 | |
| 7014 | #[test] |
| 7015 | fn runtime_posture_skip_records_posture_specific_state() { |
| 7016 | let mut view = SetupWizardView::new_at_with_facts( |
| 7017 | SetupState::default(), |
| 7018 | Locale::En, |
| 7019 | SetupStep::TrustSandbox, |
| 7020 | SetupRuntimeFacts::default(), |
| 7021 | ); |
| 7022 | |
| 7023 | let action = view.handle_key(key(KeyCode::Char('s'))); |
| 7024 | |
| 7025 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7026 | else { |
| 7027 | panic!("expected runtime posture skip commit event"); |
| 7028 | }; |
| 7029 | let entry = state |
| 7030 | .steps |
| 7031 | .get(&SetupStep::TrustSandbox) |
| 7032 | .expect("trust/sandbox step entry"); |
| 7033 | assert_eq!(entry.status, StepStatus::Skipped); |
| 7034 | assert!(entry.required); |
| 7035 | assert_eq!(entry.result.as_deref(), Some("skipped by user")); |
| 7036 | assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset); |
| 7037 | assert!(message.contains("skipped")); |
| 7038 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 7039 | } |
| 7040 | |
| 7041 | #[test] |
| 7042 | fn runtime_posture_detail_lines_show_preset_diff() { |
| 7043 | let facts = SetupRuntimeFacts { |
| 7044 | default_mode: "agent".to_string(), |
| 7045 | approval_policy_value: "on-request".to_string(), |
| 7046 | allow_shell_enabled: true, |
| 7047 | sandbox_mode_value: "workspace-write".to_string(), |
| 7048 | network_default_value: "prompt".to_string(), |
| 7049 | trust: "workspace trust not elevated".to_string(), |
| 7050 | ..SetupRuntimeFacts::default() |
| 7051 | }; |
| 7052 | let view = SetupWizardView::new_at_with_facts( |
| 7053 | SetupState::default(), |
| 7054 | Locale::En, |
| 7055 | SetupStep::TrustSandbox, |
| 7056 | facts, |
| 7057 | ); |
| 7058 | |
| 7059 | let text = lines_to_text(view.runtime_posture_detail_lines()); |
| 7060 | |
| 7061 | assert!(text.contains("Selected preset:")); |
| 7062 | assert!(text.contains("Normal agent")); |
| 7063 | assert!(text.contains("settings.default_mode: agent -> act")); |
| 7064 | assert!(text.contains("config.allow_shell: true -> true")); |
| 7065 | assert!(text.contains("Safety floor:")); |
| 7066 | assert!(text.contains("Press A to preview")); |
| 7067 | } |
| 7068 | |
| 7069 | #[test] |
| 7070 | fn runtime_posture_detail_lines_warn_about_project_overrides() { |
| 7071 | let tmp = tempfile::TempDir::new().expect("workspace"); |
| 7072 | let project_dir = tmp.path().join(codewhale_config::CODEWHALE_APP_DIR); |
| 7073 | std::fs::create_dir_all(&project_dir).expect("project config dir"); |
| 7074 | std::fs::write( |
| 7075 | project_dir.join("config.toml"), |
| 7076 | "approval_policy = \"never\"\nsandbox_mode = \"read-only\"\n", |
| 7077 | ) |
| 7078 | .expect("project config"); |
| 7079 | let warning = |
| 7080 | project_runtime_override_warning(tmp.path(), Locale::En).expect("project warning"); |
| 7081 | let facts = SetupRuntimeFacts { |
| 7082 | project_override_warning: Some(warning), |
| 7083 | ..SetupRuntimeFacts::default() |
| 7084 | }; |
| 7085 | let view = SetupWizardView::new_at_with_facts( |
| 7086 | SetupState::default(), |
| 7087 | Locale::En, |
| 7088 | SetupStep::TrustSandbox, |
| 7089 | facts, |
| 7090 | ); |
| 7091 | |
| 7092 | let text = lines_to_text(view.runtime_posture_detail_lines()); |
| 7093 | |
| 7094 | assert!(text.contains("Project override:")); |
| 7095 | assert!(text.contains("approval_policy=never")); |
| 7096 | assert!(text.contains("sandbox_mode=read-only")); |
| 7097 | assert!(text.contains("project override warning")); |
| 7098 | assert!(text.contains("project config can still tighten")); |
| 7099 | } |
| 7100 | |
| 7101 | #[test] |
| 7102 | fn operate_fleet_detail_lines_show_read_only_facts() { |
| 7103 | let facts = SetupRuntimeFacts { |
| 7104 | provider: "DeepSeek".to_string(), |
| 7105 | model: "deepseek-v4-pro".to_string(), |
| 7106 | auth: "present".to_string(), |
| 7107 | provider_ready: true, |
| 7108 | operate_runtime_ready: true, |
| 7109 | operate_runtime_result: "worker runtime enabled for deepseek; max_subagents=4, launch_concurrency=2, admission=6".to_string(), |
| 7110 | fleet_roster_ready: true, |
| 7111 | fleet_roster_result: "3 Fleet members (1 config/workspace)".to_string(), |
| 7112 | operate_concurrency_result: |
| 7113 | "configured launch_concurrency=2; max_subagents=4; admission=6; plan limit not probed" |
| 7114 | .to_string(), |
| 7115 | ..SetupRuntimeFacts::default() |
| 7116 | }; |
| 7117 | let view = SetupWizardView::new_at_with_facts( |
| 7118 | first_run_ready_state(), |
| 7119 | Locale::En, |
| 7120 | SetupStep::OperateFleet, |
| 7121 | facts, |
| 7122 | ); |
| 7123 | |
| 7124 | let text = lines_to_text(view.operate_fleet_detail_lines()); |
| 7125 | |
| 7126 | assert!(text.contains("Worker runtime:")); |
| 7127 | assert!(text.contains("worker runtime enabled for deepseek")); |
| 7128 | assert!(text.contains("Fleet roster:")); |
| 7129 | assert!(text.contains("3 Fleet members")); |
| 7130 | assert!(text.contains("plan limit not probed")); |
| 7131 | assert!(text.contains("Enter records this setup snapshot.")); |
| 7132 | } |
| 7133 | |
| 7134 | #[test] |
| 7135 | fn operate_fleet_review_records_needs_action_without_receipt_capability() { |
| 7136 | let facts = SetupRuntimeFacts { |
| 7137 | provider_ready: true, |
| 7138 | operate_runtime_ready: true, |
| 7139 | fleet_roster_ready: true, |
| 7140 | operate_result: |
| 7141 | "provider=ready, runtime=ready, roster=ready, concurrency=configured launch_concurrency=2; max_subagents=4; admission=6; plan limit not probed" |
| 7142 | .to_string(), |
| 7143 | ..SetupRuntimeFacts::default() |
| 7144 | }; |
| 7145 | let mut view = SetupWizardView::new_at_with_facts( |
| 7146 | first_run_ready_state(), |
| 7147 | Locale::En, |
| 7148 | SetupStep::OperateFleet, |
| 7149 | facts, |
| 7150 | ); |
| 7151 | |
| 7152 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7153 | |
| 7154 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7155 | else { |
| 7156 | panic!("expected setup-state commit event"); |
| 7157 | }; |
| 7158 | assert_eq!( |
| 7159 | state.status(SetupStep::OperateFleet), |
| 7160 | StepStatus::NeedsAction |
| 7161 | ); |
| 7162 | assert!(!state.operate_ready()); |
| 7163 | let result = state |
| 7164 | .steps |
| 7165 | .get(&SetupStep::OperateFleet) |
| 7166 | .and_then(|entry| entry.result.as_deref()) |
| 7167 | .expect("operate result"); |
| 7168 | assert!(result.contains("plan limit not probed"), "{result}"); |
| 7169 | assert!(message.contains("needs action")); |
| 7170 | assert_eq!(view.selected_step(), SetupStep::Hotbar); |
| 7171 | } |
| 7172 | |
| 7173 | #[test] |
| 7174 | fn hotbar_detail_lines_show_read_only_config_facts() { |
| 7175 | let facts = SetupRuntimeFacts { |
| 7176 | hotbar_bindings_result: "customized; configured_slots=2; active_slots=2; warnings=0" |
| 7177 | .to_string(), |
| 7178 | hotbar_actions_result: "13 bindable actions registered".to_string(), |
| 7179 | ..SetupRuntimeFacts::default() |
| 7180 | }; |
| 7181 | let view = SetupWizardView::new_at_with_facts( |
| 7182 | SetupState::default(), |
| 7183 | Locale::En, |
| 7184 | SetupStep::Hotbar, |
| 7185 | facts, |
| 7186 | ); |
| 7187 | |
| 7188 | let text = lines_to_text(view.hotbar_detail_lines()); |
| 7189 | |
| 7190 | assert!(text.contains("Hotbar bindings:")); |
| 7191 | assert!(text.contains("configured_slots=2")); |
| 7192 | assert!(text.contains("Bindable actions:")); |
| 7193 | assert!(text.contains("13 bindable actions")); |
| 7194 | assert!(text.contains("Enter records this setup snapshot. Press H to customize slots.")); |
| 7195 | } |
| 7196 | |
| 7197 | #[test] |
| 7198 | fn hotbar_review_records_optional_snapshot() { |
| 7199 | let facts = SetupRuntimeFacts { |
| 7200 | hotbar_result: |
| 7201 | "state=customized, configured_slots=2, active_slots=2, actions=13, warnings=0" |
| 7202 | .to_string(), |
| 7203 | ..SetupRuntimeFacts::default() |
| 7204 | }; |
| 7205 | let mut view = SetupWizardView::new_at_with_facts( |
| 7206 | SetupState::default(), |
| 7207 | Locale::En, |
| 7208 | SetupStep::Hotbar, |
| 7209 | facts, |
| 7210 | ); |
| 7211 | |
| 7212 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7213 | |
| 7214 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7215 | else { |
| 7216 | panic!("expected setup-state commit event"); |
| 7217 | }; |
| 7218 | assert_eq!(state.status(SetupStep::Hotbar), StepStatus::Verified); |
| 7219 | let entry = state |
| 7220 | .steps |
| 7221 | .get(&SetupStep::Hotbar) |
| 7222 | .expect("hotbar setup entry"); |
| 7223 | assert!(!entry.required); |
| 7224 | assert!( |
| 7225 | entry |
| 7226 | .result |
| 7227 | .as_deref() |
| 7228 | .is_some_and(|result| result.contains("state=customized")) |
| 7229 | ); |
| 7230 | assert!(message.contains("Hotbar setup state recorded")); |
| 7231 | assert_eq!(view.selected_step(), SetupStep::ToolsMcp); |
| 7232 | } |
| 7233 | |
| 7234 | #[test] |
| 7235 | fn tools_mcp_detail_lines_show_read_only_inventory_facts() { |
| 7236 | let facts = SetupRuntimeFacts { |
| 7237 | tools_mcp_servers_result: "configured — 2 configured (2 configuration valid, 0 needs_config, 0 off; global present at /tmp/mcp.json; project missing at /tmp/project/.codewhale/mcp.json); live health not checked — servers not started; configuration valid: docs, search".to_string(), |
| 7238 | tools_mcp_skills_result: "healthy — 3 discovered (hotbar skill sources), 3 on disk at /tmp/skills".to_string(), |
| 7239 | tools_mcp_tools_result: "healthy — 1 entries, 0 script-plugin tools at /tmp/tools".to_string(), |
| 7240 | tools_mcp_plugins_result: "off — nothing configured yet (missing at /tmp/plugins); optional".to_string(), |
| 7241 | tools_mcp_hotbar_result: "healthy — shared adapters: mcp_actions=0, skill_actions=3, plugin_actions=0 (deferred), slash_actions=12".to_string(), |
| 7242 | ..SetupRuntimeFacts::default() |
| 7243 | }; |
| 7244 | let view = SetupWizardView::new_at_with_facts( |
| 7245 | SetupState::default(), |
| 7246 | Locale::En, |
| 7247 | SetupStep::ToolsMcp, |
| 7248 | facts, |
| 7249 | ); |
| 7250 | |
| 7251 | let text = lines_to_text(view.tools_mcp_detail_lines()); |
| 7252 | |
| 7253 | assert!(text.contains("MCP servers:")); |
| 7254 | assert!(text.contains("configured")); |
| 7255 | assert!(text.contains("live health not checked")); |
| 7256 | assert!(text.contains("/tmp/mcp.json")); |
| 7257 | assert!(text.contains("/tmp/project/.codewhale/mcp.json")); |
| 7258 | assert!(text.contains("Skills:")); |
| 7259 | assert!(text.contains("/tmp/skills")); |
| 7260 | assert!(text.contains("Tools dir:")); |
| 7261 | assert!(text.contains("Plugins:")); |
| 7262 | assert!(text.contains("Hotbar sources:")); |
| 7263 | assert!(text.contains("shared adapters")); |
| 7264 | assert!(text.contains("Enter records this setup snapshot.")); |
| 7265 | assert!(text.contains("Press R for safe on-ramps")); |
| 7266 | } |
| 7267 | |
| 7268 | #[test] |
| 7269 | fn tools_mcp_review_records_optional_snapshot_when_empty() { |
| 7270 | let facts = SetupRuntimeFacts { |
| 7271 | tools_mcp_result: |
| 7272 | "mcp=off, skills=off, tools=off, plugins=off, hotbar_sources=shared adapters: mcp_actions=0, overall=off, mode=read_only_safe_probe" |
| 7273 | .to_string(), |
| 7274 | tools_mcp_needs_action: false, |
| 7275 | ..SetupRuntimeFacts::default() |
| 7276 | }; |
| 7277 | let mut view = SetupWizardView::new_at_with_facts( |
| 7278 | SetupState::default(), |
| 7279 | Locale::En, |
| 7280 | SetupStep::ToolsMcp, |
| 7281 | facts, |
| 7282 | ); |
| 7283 | |
| 7284 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7285 | |
| 7286 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7287 | else { |
| 7288 | panic!("expected setup-state commit event"); |
| 7289 | }; |
| 7290 | assert_eq!(state.status(SetupStep::ToolsMcp), StepStatus::Optional); |
| 7291 | let entry = state |
| 7292 | .steps |
| 7293 | .get(&SetupStep::ToolsMcp) |
| 7294 | .expect("tools/mcp setup entry"); |
| 7295 | assert!(!entry.required); |
| 7296 | assert!( |
| 7297 | entry |
| 7298 | .result |
| 7299 | .as_deref() |
| 7300 | .is_some_and(|result| result.contains("mode=read_only_safe_probe")) |
| 7301 | ); |
| 7302 | assert!(message.contains("Tools/MCP readiness recorded")); |
| 7303 | assert_eq!(view.selected_step(), SetupStep::RemoteRuntime); |
| 7304 | } |
| 7305 | |
| 7306 | #[test] |
| 7307 | fn tools_mcp_review_records_needs_action_for_broken_config() { |
| 7308 | let facts = SetupRuntimeFacts { |
| 7309 | tools_mcp_result: |
| 7310 | "mcp=needs_config, skills=off, tools=off, plugins=off, overall=needs_config, mode=read_only_safe_probe" |
| 7311 | .to_string(), |
| 7312 | tools_mcp_needs_action: true, |
| 7313 | ..SetupRuntimeFacts::default() |
| 7314 | }; |
| 7315 | let mut view = SetupWizardView::new_at_with_facts( |
| 7316 | SetupState::default(), |
| 7317 | Locale::En, |
| 7318 | SetupStep::ToolsMcp, |
| 7319 | facts, |
| 7320 | ); |
| 7321 | |
| 7322 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7323 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7324 | else { |
| 7325 | panic!("expected setup-state commit event"); |
| 7326 | }; |
| 7327 | assert_eq!(state.status(SetupStep::ToolsMcp), StepStatus::NeedsAction); |
| 7328 | assert!( |
| 7329 | !state |
| 7330 | .steps |
| 7331 | .get(&SetupStep::ToolsMcp) |
| 7332 | .expect("entry") |
| 7333 | .required |
| 7334 | ); |
| 7335 | assert!(message.contains("needs action") || message.contains("Tools/MCP")); |
| 7336 | // Optional step still advances; first-run is not blocked. |
| 7337 | assert_eq!(view.selected_step(), SetupStep::RemoteRuntime); |
| 7338 | } |
| 7339 | |
| 7340 | #[test] |
| 7341 | fn tools_mcp_on_ramp_preview_is_safe() { |
| 7342 | let facts = SetupRuntimeFacts { |
| 7343 | tools_mcp_servers_result: "off — nothing configured".into(), |
| 7344 | tools_mcp_skills_result: "off — missing".into(), |
| 7345 | tools_mcp_tools_result: "off — missing".into(), |
| 7346 | tools_mcp_plugins_result: "off — missing".into(), |
| 7347 | tools_mcp_hotbar_result: "off — shared adapters".into(), |
| 7348 | tools_mcp_path_display: "~/.codewhale/mcp.json".into(), |
| 7349 | tools_mcp_skills_path_display: "~/.codewhale/skills".into(), |
| 7350 | tools_mcp_plugins_path_display: "~/.codewhale/plugins".into(), |
| 7351 | ..SetupRuntimeFacts::default() |
| 7352 | }; |
| 7353 | let mut view = SetupWizardView::new_at_with_facts( |
| 7354 | SetupState::default(), |
| 7355 | Locale::En, |
| 7356 | SetupStep::ToolsMcp, |
| 7357 | facts, |
| 7358 | ); |
| 7359 | |
| 7360 | let action = view.handle_key(key(KeyCode::Char('r'))); |
| 7361 | let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else { |
| 7362 | panic!("expected on-ramp pager, got {action:?}"); |
| 7363 | }; |
| 7364 | assert!(title.to_ascii_lowercase().contains("tool") || title.contains("MCP")); |
| 7365 | assert!(content.contains("/mcp") || content.contains("mcp init")); |
| 7366 | assert!(!content.contains("sk-")); |
| 7367 | } |
| 7368 | |
| 7369 | /// #3409: the card answers "where can this be reached from?" with one row |
| 7370 | /// per observed mode. The registry counts it used to list are still |
| 7371 | /// reachable — they moved into the `R` preview — so the card itself stays |
| 7372 | /// four plain lines plus the active route. |
| 7373 | #[test] |
| 7374 | fn remote_runtime_detail_lines_show_one_row_per_observed_mode() { |
| 7375 | let facts = SetupRuntimeFacts { |
| 7376 | remote_modes: vec![ |
| 7377 | remote::RemoteModeFact { |
| 7378 | mode: remote::RemoteMode::LocalOnly, |
| 7379 | status: remote::RemoteModeStatus::Ready, |
| 7380 | detail: "this machine only; nothing is exposed".to_string(), |
| 7381 | }, |
| 7382 | remote::RemoteModeFact { |
| 7383 | mode: remote::RemoteMode::MobileLan, |
| 7384 | status: remote::RemoteModeStatus::Disabled, |
| 7385 | detail: "runtime binds 127.0.0.1 only".to_string(), |
| 7386 | }, |
| 7387 | ], |
| 7388 | remote_providers_result: |
| 7389 | "12 providers from the provider registry; active route deepseek / deepseek-chat" |
| 7390 | .to_string(), |
| 7391 | ..SetupRuntimeFacts::default() |
| 7392 | }; |
| 7393 | let view = SetupWizardView::new_at_with_facts( |
| 7394 | SetupState::default(), |
| 7395 | Locale::En, |
| 7396 | SetupStep::RemoteRuntime, |
| 7397 | facts, |
| 7398 | ); |
| 7399 | |
| 7400 | let text = lines_to_text(view.remote_runtime_detail_lines()); |
| 7401 | |
| 7402 | assert!(text.contains("This machine only:")); |
| 7403 | assert!(text.contains("ready · this machine only; nothing is exposed")); |
| 7404 | assert!(text.contains("Phone on your network:")); |
| 7405 | assert!(text.contains("not available · runtime binds 127.0.0.1 only")); |
| 7406 | assert!(text.contains("Providers:")); |
| 7407 | assert!(text.contains("deepseek-chat")); |
| 7408 | // Local-only is always usable, so the hint says Enter alone is enough. |
| 7409 | assert!(text.contains("Enter keeps local-only.")); |
| 7410 | } |
| 7411 | |
| 7412 | /// Before facts load there are no modes to show; the card falls back to the |
| 7413 | /// single mode line rather than rendering an empty panel. |
| 7414 | #[test] |
| 7415 | fn remote_runtime_detail_lines_fall_back_when_no_mode_is_observed_yet() { |
| 7416 | let facts = SetupRuntimeFacts { |
| 7417 | remote_mode_result: "remote setup mode not loaded".to_string(), |
| 7418 | ..SetupRuntimeFacts::default() |
| 7419 | }; |
| 7420 | let view = SetupWizardView::new_at_with_facts( |
| 7421 | SetupState::default(), |
| 7422 | Locale::En, |
| 7423 | SetupStep::RemoteRuntime, |
| 7424 | facts, |
| 7425 | ); |
| 7426 | |
| 7427 | let text = lines_to_text(view.remote_runtime_detail_lines()); |
| 7428 | assert!(text.contains("Remote mode:")); |
| 7429 | assert!(text.contains("remote setup mode not loaded")); |
| 7430 | } |
| 7431 | |
| 7432 | /// A missing token records `NeedsAction` — surfaced, never blocking. |
| 7433 | #[test] |
| 7434 | fn remote_runtime_review_records_needs_action_without_blocking_ready() { |
| 7435 | let facts = SetupRuntimeFacts { |
| 7436 | remote_needs_action: true, |
| 7437 | remote_result: "runtime_api=needs_action".to_string(), |
| 7438 | ..SetupRuntimeFacts::default() |
| 7439 | }; |
| 7440 | let mut view = SetupWizardView::new_at_with_facts( |
| 7441 | SetupState::default(), |
| 7442 | Locale::En, |
| 7443 | SetupStep::RemoteRuntime, |
| 7444 | facts, |
| 7445 | ); |
| 7446 | let _ = view.commit_remote_runtime_review(); |
| 7447 | |
| 7448 | let entry = view.state().status(SetupStep::RemoteRuntime); |
| 7449 | assert_eq!(entry, StepStatus::NeedsAction); |
| 7450 | assert!(entry.is_settled(), "needs-action must not block ready"); |
| 7451 | } |
| 7452 | |
| 7453 | #[test] |
| 7454 | fn remote_runtime_review_records_optional_snapshot() { |
| 7455 | let facts = SetupRuntimeFacts { |
| 7456 | remote_result: |
| 7457 | "clouds=3, bridges=2, providers=12, mode=generate_only, apply=not_implemented" |
| 7458 | .to_string(), |
| 7459 | ..SetupRuntimeFacts::default() |
| 7460 | }; |
| 7461 | let mut view = SetupWizardView::new_at_with_facts( |
| 7462 | SetupState::default(), |
| 7463 | Locale::En, |
| 7464 | SetupStep::RemoteRuntime, |
| 7465 | facts, |
| 7466 | ); |
| 7467 | |
| 7468 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7469 | |
| 7470 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7471 | else { |
| 7472 | panic!("expected setup-state commit event"); |
| 7473 | }; |
| 7474 | assert_eq!(state.status(SetupStep::RemoteRuntime), StepStatus::Verified); |
| 7475 | let entry = state |
| 7476 | .steps |
| 7477 | .get(&SetupStep::RemoteRuntime) |
| 7478 | .expect("remote setup entry"); |
| 7479 | assert!(!entry.required); |
| 7480 | assert!( |
| 7481 | entry |
| 7482 | .result |
| 7483 | .as_deref() |
| 7484 | .is_some_and(|result| result.contains("mode=generate_only")) |
| 7485 | ); |
| 7486 | assert!(message.contains("Remote runtime on-ramp recorded")); |
| 7487 | assert_eq!(view.selected_step(), SetupStep::Persistence); |
| 7488 | } |
| 7489 | |
| 7490 | #[test] |
| 7491 | fn persistence_detail_lines_show_read_only_path_facts() { |
| 7492 | let facts = SetupRuntimeFacts { |
| 7493 | persistence: SetupPersistenceFacts { |
| 7494 | home_result: "explicit CODEWHALE_HOME at /tmp/cw-home (present)".to_string(), |
| 7495 | config_result: "/tmp/cw-home/config.toml (present)".to_string(), |
| 7496 | state_result: "/tmp/cw-home/setup_state.json (missing)".to_string(), |
| 7497 | constitution_result: "/tmp/cw-home/constitution.json (present)".to_string(), |
| 7498 | memory_result: "/tmp/cw-home/memory.md (missing)".to_string(), |
| 7499 | notes_result: "/tmp/cw-home/notes.md (exists-not-file)".to_string(), |
| 7500 | result: "home_source=explicit, home=present, config=present, setup_state=missing, constitution=present, memory=missing, notes=exists-not-file, mode=read_only_review".to_string(), |
| 7501 | }, |
| 7502 | ..SetupRuntimeFacts::default() |
| 7503 | }; |
| 7504 | let view = SetupWizardView::new_at_with_facts( |
| 7505 | SetupState::default(), |
| 7506 | Locale::En, |
| 7507 | SetupStep::Persistence, |
| 7508 | facts, |
| 7509 | ); |
| 7510 | |
| 7511 | let text = lines_to_text(view.persistence_detail_lines()); |
| 7512 | |
| 7513 | assert!(text.contains("Home:")); |
| 7514 | assert!(text.contains("explicit CODEWHALE_HOME")); |
| 7515 | assert!(text.contains("/tmp/cw-home/config.toml")); |
| 7516 | assert!(text.contains("/tmp/cw-home/setup_state.json (missing)")); |
| 7517 | assert!(text.contains("Constitution:")); |
| 7518 | assert!(text.contains("Memory:")); |
| 7519 | assert!(text.contains("Notes:")); |
| 7520 | assert!(text.contains("Enter records this setup snapshot.")); |
| 7521 | } |
| 7522 | |
| 7523 | #[test] |
| 7524 | fn persistence_review_records_optional_snapshot() { |
| 7525 | let facts = SetupRuntimeFacts { |
| 7526 | persistence: SetupPersistenceFacts { |
| 7527 | result: "home_source=explicit, home=present, config=present, setup_state=missing, constitution=present, memory=missing, notes=missing, mode=read_only_review".to_string(), |
| 7528 | ..SetupPersistenceFacts::default() |
| 7529 | }, |
| 7530 | ..SetupRuntimeFacts::default() |
| 7531 | }; |
| 7532 | let mut view = SetupWizardView::new_at_with_facts( |
| 7533 | SetupState::default(), |
| 7534 | Locale::En, |
| 7535 | SetupStep::Persistence, |
| 7536 | facts, |
| 7537 | ); |
| 7538 | |
| 7539 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7540 | |
| 7541 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7542 | else { |
| 7543 | panic!("expected setup-state commit event"); |
| 7544 | }; |
| 7545 | assert_eq!(state.status(SetupStep::Persistence), StepStatus::Verified); |
| 7546 | let entry = state |
| 7547 | .steps |
| 7548 | .get(&SetupStep::Persistence) |
| 7549 | .expect("persistence setup entry"); |
| 7550 | assert!(!entry.required); |
| 7551 | assert!( |
| 7552 | entry |
| 7553 | .result |
| 7554 | .as_deref() |
| 7555 | .is_some_and(|result| result.contains("mode=read_only_review")) |
| 7556 | ); |
| 7557 | assert!(message.contains("Persistence paths recorded")); |
| 7558 | assert_eq!(view.selected_step(), SetupStep::Verification); |
| 7559 | } |
| 7560 | |
| 7561 | #[test] |
| 7562 | fn operate_fleet_review_records_needs_action_until_first_run_ready() { |
| 7563 | let facts = SetupRuntimeFacts { |
| 7564 | provider_ready: true, |
| 7565 | operate_runtime_ready: true, |
| 7566 | fleet_roster_ready: true, |
| 7567 | operate_result: |
| 7568 | "provider=ready, runtime=ready, roster=ready, concurrency=plan limit not probed" |
| 7569 | .to_string(), |
| 7570 | ..SetupRuntimeFacts::default() |
| 7571 | }; |
| 7572 | let mut view = SetupWizardView::new_at_with_facts( |
| 7573 | SetupState::default(), |
| 7574 | Locale::En, |
| 7575 | SetupStep::OperateFleet, |
| 7576 | facts, |
| 7577 | ); |
| 7578 | |
| 7579 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7580 | |
| 7581 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7582 | else { |
| 7583 | panic!("expected setup-state commit event"); |
| 7584 | }; |
| 7585 | assert_eq!( |
| 7586 | state.status(SetupStep::OperateFleet), |
| 7587 | StepStatus::NeedsAction |
| 7588 | ); |
| 7589 | assert!(!state.operate_ready()); |
| 7590 | assert!(message.contains("needs action")); |
| 7591 | } |
| 7592 | |
| 7593 | #[test] |
| 7594 | fn runtime_posture_preset_requires_preview_before_apply() { |
| 7595 | let facts = SetupRuntimeFacts { |
| 7596 | default_mode: "agent".to_string(), |
| 7597 | approval_policy_value: "never".to_string(), |
| 7598 | allow_shell_enabled: false, |
| 7599 | sandbox_mode_value: "read-only".to_string(), |
| 7600 | network_default_value: "deny".to_string(), |
| 7601 | trust: "workspace trust not elevated".to_string(), |
| 7602 | ..SetupRuntimeFacts::default() |
| 7603 | }; |
| 7604 | let mut view = SetupWizardView::new_at_with_facts( |
| 7605 | SetupState::default(), |
| 7606 | Locale::En, |
| 7607 | SetupStep::TrustSandbox, |
| 7608 | facts, |
| 7609 | ); |
| 7610 | |
| 7611 | assert!(matches!( |
| 7612 | view.handle_key(key(KeyCode::Char('3'))), |
| 7613 | ViewAction::None |
| 7614 | )); |
| 7615 | let preview = view.handle_key(key(KeyCode::Char('a'))); |
| 7616 | let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = preview else { |
| 7617 | panic!("first apply should preview the exact diff"); |
| 7618 | }; |
| 7619 | assert!(content.contains("Runtime Posture Preset Preview")); |
| 7620 | assert!(content.contains("settings.default_mode: agent -> act + full-access")); |
| 7621 | assert!(content.contains( |
| 7622 | "config.approval_policy: never -> removed; Full Access comes from settings.permission_posture" |
| 7623 | )); |
| 7624 | assert!(content.contains("settings.permission_posture: -> full-access")); |
| 7625 | assert!(content.contains("config.network.default: deny -> unchanged")); |
| 7626 | |
| 7627 | let action = view.handle_key(key(KeyCode::Char('a'))); |
| 7628 | let ViewAction::Emit(ViewEvent::SetupRuntimePresetApplyRequested { |
| 7629 | preset, |
| 7630 | state, |
| 7631 | message, |
| 7632 | }) = action |
| 7633 | else { |
| 7634 | panic!("second apply should request preset persistence"); |
| 7635 | }; |
| 7636 | assert_eq!(preset, SetupRuntimePreset::HighTrustLocal); |
| 7637 | assert_eq!(state.status(SetupStep::TrustSandbox), StepStatus::Verified); |
| 7638 | assert_eq!( |
| 7639 | state.runtime_posture_source, |
| 7640 | RuntimePostureSource::Confirmed |
| 7641 | ); |
| 7642 | assert!( |
| 7643 | state |
| 7644 | .steps |
| 7645 | .get(&SetupStep::TrustSandbox) |
| 7646 | .and_then(|entry| entry.result.as_deref()) |
| 7647 | .is_some_and(|result| { |
| 7648 | result.contains("preset=high-trust-local") |
| 7649 | && result.contains("default_mode=act + full-access") |
| 7650 | && result.contains("network=unchanged") |
| 7651 | }) |
| 7652 | ); |
| 7653 | assert!(message.contains("Runtime preset applied")); |
| 7654 | assert_eq!(view.selected_step(), SetupStep::Constitution); |
| 7655 | } |
| 7656 | |
| 7657 | #[test] |
| 7658 | fn verification_report_records_needs_action_until_checkpoint_complete() { |
| 7659 | let facts = SetupRuntimeFacts { |
| 7660 | constitution_autonomy: "balanced".to_string(), |
| 7661 | runtime_result: "intent=agent, approval=suggest".to_string(), |
| 7662 | ..SetupRuntimeFacts::default() |
| 7663 | }; |
| 7664 | let mut view = SetupWizardView::new_at_with_facts( |
| 7665 | SetupState::default(), |
| 7666 | Locale::En, |
| 7667 | SetupStep::Verification, |
| 7668 | facts, |
| 7669 | ); |
| 7670 | |
| 7671 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7672 | |
| 7673 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action |
| 7674 | else { |
| 7675 | panic!("expected setup-state commit event"); |
| 7676 | }; |
| 7677 | assert_eq!( |
| 7678 | state.status(SetupStep::Verification), |
| 7679 | StepStatus::NeedsAction |
| 7680 | ); |
| 7681 | assert!( |
| 7682 | state |
| 7683 | .steps |
| 7684 | .get(&SetupStep::Verification) |
| 7685 | .and_then(|entry| entry.result.as_deref()) |
| 7686 | .is_some_and(|result| { |
| 7687 | result.contains("update=needs_action") |
| 7688 | && result.contains("operate=needs_action") |
| 7689 | && result.contains("autonomy=balanced") |
| 7690 | && result.contains("runtime=intent=agent, approval=suggest") |
| 7691 | }) |
| 7692 | ); |
| 7693 | assert!(message.contains("Setup report recorded")); |
| 7694 | } |
| 7695 | |
| 7696 | #[test] |
| 7697 | fn verification_report_records_ready_after_bundled_checkpoint() { |
| 7698 | let mut state = SetupState::default(); |
| 7699 | state.complete_constitution_checkpoint( |
| 7700 | CONSTITUTION_CHECKPOINT_VERSION, |
| 7701 | ConstitutionChoice::Bundled, |
| 7702 | ); |
| 7703 | let mut view = SetupWizardView::new_at_with_facts( |
| 7704 | state, |
| 7705 | Locale::En, |
| 7706 | SetupStep::Verification, |
| 7707 | SetupRuntimeFacts::default(), |
| 7708 | ); |
| 7709 | |
| 7710 | let action = view.handle_key(key(KeyCode::Enter)); |
| 7711 | |
| 7712 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = action else { |
| 7713 | panic!("expected setup-state commit event"); |
| 7714 | }; |
| 7715 | assert_eq!(state.status(SetupStep::Verification), StepStatus::Verified); |
| 7716 | assert!( |
| 7717 | state |
| 7718 | .steps |
| 7719 | .get(&SetupStep::Verification) |
| 7720 | .and_then(|entry| entry.result.as_deref()) |
| 7721 | .is_some_and(|result| { |
| 7722 | result.contains("update=ready") && result.contains("operate=needs_action") |
| 7723 | }) |
| 7724 | ); |
| 7725 | } |
| 7726 | |
| 7727 | #[test] |
| 7728 | fn verification_detail_lines_show_next_action() { |
| 7729 | let facts = SetupRuntimeFacts { |
| 7730 | constitution_autonomy: "balanced".to_string(), |
| 7731 | runtime_result: "intent=agent, approval=suggest".to_string(), |
| 7732 | ..SetupRuntimeFacts::default() |
| 7733 | }; |
| 7734 | let view = SetupWizardView::new_at_with_facts( |
| 7735 | SetupState::default(), |
| 7736 | Locale::En, |
| 7737 | SetupStep::Verification, |
| 7738 | facts, |
| 7739 | ); |
| 7740 | |
| 7741 | let text = lines_to_text(view.verification_detail_lines()); |
| 7742 | |
| 7743 | assert!(text.contains("First-run:")); |
| 7744 | assert!(text.contains("Update checkpoint:")); |
| 7745 | assert!(text.contains("Operate/Fleet:")); |
| 7746 | assert!(text.contains("Constitution autonomy:")); |
| 7747 | assert!(text.contains("balanced")); |
| 7748 | assert!(text.contains("Runtime posture:")); |
| 7749 | assert!(text.contains("intent=agent, approval=suggest")); |
| 7750 | assert!(text.contains("Complete the constitution checkpoint")); |
| 7751 | } |
| 7752 | |
| 7753 | #[test] |
| 7754 | fn setup_wizard_body_scroll_resets_on_step_change() { |
| 7755 | let mut view = SetupWizardView::new(SetupState::default(), Locale::En); |
| 7756 | view.body_scroll = 12; |
| 7757 | view.move_next(); |
| 7758 | assert_eq!(view.body_scroll, 0, "step change should reset body scroll"); |
| 7759 | view.body_scroll = 5; |
| 7760 | view.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE)); |
| 7761 | assert!(view.body_scroll >= 5); |
| 7762 | view.move_back(); |
| 7763 | assert_eq!(view.body_scroll, 0); |
| 7764 | } |
| 7765 | |
| 7766 | #[test] |
| 7767 | fn setup_wizard_page_down_clamps_scroll_at_80x24() { |
| 7768 | use ratatui::text::{Line, Span}; |
| 7769 | |
| 7770 | let mut view = SetupWizardView::new_at_with_facts( |
| 7771 | SetupState::default(), |
| 7772 | Locale::En, |
| 7773 | SetupStep::Constitution, |
| 7774 | SetupRuntimeFacts { |
| 7775 | constitution_file: SetupConstitutionFileState::Loaded, |
| 7776 | ..SetupRuntimeFacts::default() |
| 7777 | }, |
| 7778 | ); |
| 7779 | let wrap_width = 76usize; |
| 7780 | let visible_rows = 10usize; |
| 7781 | let mut lines = view.constitution_detail_lines(); |
| 7782 | lines.extend(std::iter::repeat_n( |
| 7783 | Line::from(Span::raw("x".repeat(wrap_width))), |
| 7784 | 40, |
| 7785 | )); |
| 7786 | let visual_rows: usize = lines |
| 7787 | .iter() |
| 7788 | .map(|line| line.width().div_ceil(wrap_width).max(1)) |
| 7789 | .sum(); |
| 7790 | let max_scroll = visual_rows.saturating_sub(visible_rows); |
| 7791 | assert!(max_scroll > 0, "fixture should overflow a small viewport"); |
| 7792 | |
| 7793 | for _ in 0..32 { |
| 7794 | view.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE)); |
| 7795 | } |
| 7796 | assert!( |
| 7797 | view.body_scroll >= max_scroll.saturating_sub(8), |
| 7798 | "page down should reach the scroll ceiling" |
| 7799 | ); |
| 7800 | |
| 7801 | let clamped = view.body_scroll.min(max_scroll); |
| 7802 | assert_eq!( |
| 7803 | clamped, max_scroll, |
| 7804 | "render path should clamp overshoot to max scroll" |
| 7805 | ); |
| 7806 | } |
| 7807 | |
| 7808 | fn lines_to_text(lines: Vec<Line<'static>>) -> String { |
| 7809 | lines |
| 7810 | .into_iter() |
| 7811 | .map(|line| { |
| 7812 | line.spans |
| 7813 | .into_iter() |
| 7814 | .map(|span| span.content.into_owned()) |
| 7815 | .collect::<String>() |
| 7816 | }) |
| 7817 | .collect::<Vec<_>>() |
| 7818 | .join("\n") |
| 7819 | } |
| 7820 | } |
| 7821 |