| 1 | //! Progressive setup and repair guide. |
| 2 | //! |
| 3 | //! Bare `/setup` starts at the first missing decision and otherwise shows a |
| 4 | //! compact readiness summary. Optional power tools only join that journey when |
| 5 | //! they are already configured or need repair. Named `/setup <target>` routes |
| 6 | //! and the versioned Constitution checkpoint remain compatibility entrypoints, |
| 7 | //! but ordinary preferences belong to `/settings` and advanced keys belong to |
| 8 | //! `/config <key>`. |
| 9 | |
| 10 | use std::borrow::Cow; |
| 11 | use std::path::Path; |
| 12 | |
| 13 | use crossterm::event::{KeyCode, KeyEvent}; |
| 14 | use ratatui::{ |
| 15 | buffer::Buffer, |
| 16 | layout::Rect, |
| 17 | style::{Modifier, Style}, |
| 18 | text::{Line, Span}, |
| 19 | widgets::{Paragraph, Widget, Wrap}, |
| 20 | }; |
| 21 | |
| 22 | use crate::config::{Config, has_api_key}; |
| 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 | use codewhale_localization::{Locale, MessageId, tr}; |
| 33 | use codewhale_palette as palette; |
| 34 | |
| 35 | use codewhale_config::{ |
| 36 | AutonomyPreference, ConstitutionAuthoring, ConstitutionChoice, ConstitutionSource, |
| 37 | ConstitutionValidity, InheritedConfigFacts, RuntimePostureSource, SetupState, SetupStep, |
| 38 | StepEntry, StepStatus, UserConstitution, UserConstitutionLoad, |
| 39 | user_constitution::MAX_NOTES_LEN, |
| 40 | }; |
| 41 | |
| 42 | mod fleet_draft; |
| 43 | mod model_draft; |
| 44 | mod operate; |
| 45 | mod persistence; |
| 46 | mod provider; |
| 47 | mod remote; |
| 48 | mod tools_mcp; |
| 49 | |
| 50 | pub(crate) use fleet_draft::{draft_fleet_profile_with_model, workspace_fingerprint}; |
| 51 | pub(crate) use model_draft::draft_constitution_with_model; |
| 52 | use persistence::SetupPersistenceFacts; |
| 53 | use remote::SetupRemoteFacts; |
| 54 | |
| 55 | /// Target lane for the once-per-version constitution checkpoint. Bumped per |
| 56 | /// release when the bundled constitution materially changes, so existing users |
| 57 | /// re-acknowledge it once. 0.9.4 re-ships the Fleet/operate constitution. |
| 58 | pub const CONSTITUTION_CHECKPOINT_VERSION: &str = "0.9.4"; |
| 59 | |
| 60 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 61 | pub enum SetupCommitKind { |
| 62 | BundledConstitution, |
| 63 | DeferredConstitution, |
| 64 | } |
| 65 | |
| 66 | pub trait SetupWizardStep { |
| 67 | fn id(&self) -> SetupStep; |
| 68 | fn title_id(&self) -> MessageId; |
| 69 | fn why_id(&self) -> MessageId; |
| 70 | fn required(&self) -> bool; |
| 71 | } |
| 72 | |
| 73 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 74 | struct StaticSetupStep { |
| 75 | id: SetupStep, |
| 76 | title_id: MessageId, |
| 77 | why_id: MessageId, |
| 78 | required: bool, |
| 79 | } |
| 80 | |
| 81 | impl SetupWizardStep for StaticSetupStep { |
| 82 | fn id(&self) -> SetupStep { |
| 83 | self.id |
| 84 | } |
| 85 | |
| 86 | fn title_id(&self) -> MessageId { |
| 87 | self.title_id |
| 88 | } |
| 89 | |
| 90 | fn why_id(&self) -> MessageId { |
| 91 | self.why_id |
| 92 | } |
| 93 | |
| 94 | fn required(&self) -> bool { |
| 95 | self.required |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | const STEP_SPECS: [StaticSetupStep; 10] = [ |
| 100 | StaticSetupStep { |
| 101 | id: SetupStep::Language, |
| 102 | title_id: MessageId::SetupStepLanguageTitle, |
| 103 | why_id: MessageId::SetupStepLanguageWhy, |
| 104 | required: true, |
| 105 | }, |
| 106 | StaticSetupStep { |
| 107 | id: SetupStep::ProviderModel, |
| 108 | title_id: MessageId::SetupStepProviderModelTitle, |
| 109 | why_id: MessageId::SetupStepProviderModelWhy, |
| 110 | required: true, |
| 111 | }, |
| 112 | StaticSetupStep { |
| 113 | id: SetupStep::TrustSandbox, |
| 114 | title_id: MessageId::SetupStepTrustSandboxTitle, |
| 115 | why_id: MessageId::SetupStepTrustSandboxWhy, |
| 116 | required: true, |
| 117 | }, |
| 118 | StaticSetupStep { |
| 119 | id: SetupStep::Constitution, |
| 120 | title_id: MessageId::SetupStepConstitutionTitle, |
| 121 | why_id: MessageId::SetupStepConstitutionWhy, |
| 122 | required: true, |
| 123 | }, |
| 124 | StaticSetupStep { |
| 125 | id: SetupStep::OperateFleet, |
| 126 | title_id: MessageId::SetupStepOperateFleetTitle, |
| 127 | why_id: MessageId::SetupStepOperateFleetWhy, |
| 128 | required: false, |
| 129 | }, |
| 130 | StaticSetupStep { |
| 131 | id: SetupStep::Hotbar, |
| 132 | title_id: MessageId::SetupStepHotbarTitle, |
| 133 | why_id: MessageId::SetupStepHotbarWhy, |
| 134 | required: false, |
| 135 | }, |
| 136 | StaticSetupStep { |
| 137 | id: SetupStep::ToolsMcp, |
| 138 | title_id: MessageId::SetupStepToolsMcpTitle, |
| 139 | why_id: MessageId::SetupStepToolsMcpWhy, |
| 140 | required: false, |
| 141 | }, |
| 142 | StaticSetupStep { |
| 143 | id: SetupStep::RemoteRuntime, |
| 144 | title_id: MessageId::SetupStepRemoteRuntimeTitle, |
| 145 | why_id: MessageId::SetupStepRemoteRuntimeWhy, |
| 146 | required: false, |
| 147 | }, |
| 148 | StaticSetupStep { |
| 149 | id: SetupStep::Persistence, |
| 150 | title_id: MessageId::SetupStepPersistenceTitle, |
| 151 | why_id: MessageId::SetupStepPersistenceWhy, |
| 152 | required: false, |
| 153 | }, |
| 154 | StaticSetupStep { |
| 155 | id: SetupStep::Verification, |
| 156 | title_id: MessageId::SetupStepVerificationTitle, |
| 157 | why_id: MessageId::SetupStepVerificationWhy, |
| 158 | required: false, |
| 159 | }, |
| 160 | ]; |
| 161 | |
| 162 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 163 | pub struct SetupWizardView { |
| 164 | state: SetupState, |
| 165 | selected: usize, |
| 166 | locale: Locale, |
| 167 | /// Bare `/setup` is a short runtime-derived repair journey. Explicit |
| 168 | /// targets remain focused compatibility cards outside that journey. |
| 169 | progressive_guide: bool, |
| 170 | /// Technical inventory and preset details stay off the first paint. |
| 171 | details_expanded: bool, |
| 172 | facts: SetupRuntimeFacts, |
| 173 | guided_draft: GuidedConstitutionDraft, |
| 174 | /// First-run shows one plain-language initiative choice. The six-axis |
| 175 | /// editor remains available explicitly, but never competes with the |
| 176 | /// recommended path on first paint. |
| 177 | constitution_advanced: bool, |
| 178 | freeform_note: String, |
| 179 | editing_freeform_note: bool, |
| 180 | guided_preview_seen: bool, |
| 181 | /// The keep-existing path mirrors the guided two-step: the first `K` |
| 182 | /// opens the rendered preview of the existing file, the second completes |
| 183 | /// the checkpoint without touching it. |
| 184 | existing_preview_seen: bool, |
| 185 | /// A model-drafted constitution awaiting ratification, installed by the |
| 186 | /// host after a successful one-shot draft (already sanitized + bounded). |
| 187 | /// Cleared whenever a guided answer changes so a stale draft can never be |
| 188 | /// ratified against fresh answers. |
| 189 | model_draft: Option<Box<UserConstitution>>, |
| 190 | /// Display label of the model that authored `model_draft` (safe metadata, |
| 191 | /// e.g. "GLM-5.2"), for provenance copy only. |
| 192 | model_draft_label: Option<String>, |
| 193 | runtime_preset: SetupRuntimePreset, |
| 194 | runtime_preset_preview_seen: bool, |
| 195 | body_scroll: usize, |
| 196 | } |
| 197 | |
| 198 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 199 | struct SetupRuntimeFacts { |
| 200 | provider: String, |
| 201 | model: String, |
| 202 | auth: String, |
| 203 | health: String, |
| 204 | provider_ready: bool, |
| 205 | provider_result: String, |
| 206 | work_intent: String, |
| 207 | approval: String, |
| 208 | shell: String, |
| 209 | allow_shell_enabled: bool, |
| 210 | trust: String, |
| 211 | sandbox: String, |
| 212 | sandbox_mode_value: String, |
| 213 | network: String, |
| 214 | network_default_value: String, |
| 215 | runtime_result: String, |
| 216 | operate_runtime_ready: bool, |
| 217 | operate_runtime_result: String, |
| 218 | fleet_roster_ready: bool, |
| 219 | fleet_roster_result: String, |
| 220 | operate_concurrency_result: String, |
| 221 | operate_result: String, |
| 222 | hotbar_bindings_result: String, |
| 223 | hotbar_actions_result: String, |
| 224 | hotbar_result: String, |
| 225 | tools_mcp_servers_result: String, |
| 226 | tools_mcp_skills_result: String, |
| 227 | tools_mcp_tools_result: String, |
| 228 | tools_mcp_plugins_result: String, |
| 229 | tools_mcp_dsh_result: String, |
| 230 | tools_mcp_hotbar_result: String, |
| 231 | tools_mcp_result: String, |
| 232 | tools_mcp_needs_action: bool, |
| 233 | tools_mcp_path_display: String, |
| 234 | tools_mcp_skills_path_display: String, |
| 235 | tools_mcp_plugins_path_display: String, |
| 236 | remote_clouds_result: String, |
| 237 | remote_bridges_result: String, |
| 238 | remote_providers_result: String, |
| 239 | remote_mode_result: String, |
| 240 | remote_command_provider: String, |
| 241 | remote_result: String, |
| 242 | remote_control_result: String, |
| 243 | /// The four observed remote modes (#3409). Empty only before facts load. |
| 244 | remote_modes: Vec<remote::RemoteModeFact>, |
| 245 | /// True when a mode is missing a token or config. Recorded as |
| 246 | /// `NeedsAction`, which by contract never blocks the ready screen. |
| 247 | remote_needs_action: bool, |
| 248 | persistence: SetupPersistenceFacts, |
| 249 | default_mode: String, |
| 250 | approval_policy_value: String, |
| 251 | project_override_warning: Option<String>, |
| 252 | constitution_autonomy: String, |
| 253 | constitution_file: SetupConstitutionFileState, |
| 254 | expert_override: SetupExpertOverrideState, |
| 255 | } |
| 256 | |
| 257 | impl Default for SetupRuntimeFacts { |
| 258 | fn default() -> Self { |
| 259 | Self { |
| 260 | provider: "not loaded".to_string(), |
| 261 | model: "not loaded".to_string(), |
| 262 | auth: "not checked".to_string(), |
| 263 | health: "not checked".to_string(), |
| 264 | provider_ready: false, |
| 265 | provider_result: "provider/model not loaded".to_string(), |
| 266 | work_intent: "not loaded".to_string(), |
| 267 | approval: "not loaded".to_string(), |
| 268 | shell: "not loaded".to_string(), |
| 269 | allow_shell_enabled: false, |
| 270 | trust: "not loaded".to_string(), |
| 271 | sandbox: "not configured".to_string(), |
| 272 | sandbox_mode_value: "default".to_string(), |
| 273 | network: "not configured".to_string(), |
| 274 | network_default_value: "prompt".to_string(), |
| 275 | runtime_result: "runtime posture not loaded".to_string(), |
| 276 | operate_runtime_ready: false, |
| 277 | operate_runtime_result: "worker runtime not loaded".to_string(), |
| 278 | fleet_roster_ready: false, |
| 279 | fleet_roster_result: "Team roster not loaded".to_string(), |
| 280 | operate_concurrency_result: "concurrency not loaded".to_string(), |
| 281 | operate_result: "operate readiness not loaded".to_string(), |
| 282 | hotbar_bindings_result: "Hotbar config not loaded".to_string(), |
| 283 | hotbar_actions_result: "Hotbar actions not loaded".to_string(), |
| 284 | hotbar_result: "hotbar not loaded".to_string(), |
| 285 | tools_mcp_servers_result: "MCP config not loaded".to_string(), |
| 286 | tools_mcp_skills_result: "skills dir not loaded".to_string(), |
| 287 | tools_mcp_tools_result: "tools dir not loaded".to_string(), |
| 288 | tools_mcp_plugins_result: "plugins dir not loaded".to_string(), |
| 289 | tools_mcp_dsh_result: "DeepSeek Harness not probed".to_string(), |
| 290 | tools_mcp_hotbar_result: "hotbar source metadata not loaded".to_string(), |
| 291 | tools_mcp_result: "tools/MCP not loaded".to_string(), |
| 292 | tools_mcp_needs_action: false, |
| 293 | tools_mcp_path_display: String::new(), |
| 294 | tools_mcp_skills_path_display: String::new(), |
| 295 | tools_mcp_plugins_path_display: String::new(), |
| 296 | remote_clouds_result: "remote cloud registry not loaded".to_string(), |
| 297 | remote_bridges_result: "remote bridge registry not loaded".to_string(), |
| 298 | remote_providers_result: "provider registry not loaded".to_string(), |
| 299 | remote_mode_result: "remote setup mode not loaded".to_string(), |
| 300 | remote_command_provider: "deepseek".to_string(), |
| 301 | remote_result: "remote runtime not loaded".to_string(), |
| 302 | remote_control_result: "off".to_string(), |
| 303 | remote_modes: Vec::new(), |
| 304 | remote_needs_action: false, |
| 305 | persistence: SetupPersistenceFacts::default(), |
| 306 | default_mode: "agent".to_string(), |
| 307 | approval_policy_value: "on-request".to_string(), |
| 308 | project_override_warning: None, |
| 309 | constitution_autonomy: "not loaded".to_string(), |
| 310 | constitution_file: SetupConstitutionFileState::NotChecked, |
| 311 | expert_override: SetupExpertOverrideState::NotChecked, |
| 312 | } |
| 313 | } |
| 314 | } |
| 315 | |
| 316 | impl SetupRuntimeFacts { |
| 317 | fn from_app_config(app: &App, config: &Config) -> Self { |
| 318 | let expert_override = SetupExpertOverrideState::load(); |
| 319 | let readiness = crate::provider_readiness::resolve_for_model( |
| 320 | config, |
| 321 | app.api_provider, |
| 322 | if app.auto_model { "auto" } else { &app.model }, |
| 323 | &app.provider_health, |
| 324 | ); |
| 325 | // A failed observed check remains retryable in route pickers, but the |
| 326 | // setup receipt must not certify it as healthy. Saved-unchecked and |
| 327 | // local-unchecked are honest reviewed configuration states; an actual |
| 328 | // session failure is NeedsAction until a later success replaces it. |
| 329 | let provider_ready = readiness.can_attempt() |
| 330 | && !matches!( |
| 331 | &readiness, |
| 332 | crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. } |
| 333 | ); |
| 334 | let model = app.model_display_label(); |
| 335 | let provider_name = if app.api_provider == crate::config::ApiProvider::Custom { |
| 336 | app.provider_identity_for_persistence().to_string() |
| 337 | } else { |
| 338 | app.api_provider.display_name().to_string() |
| 339 | }; |
| 340 | let context_window = crate::route_budget::route_context_window_tokens( |
| 341 | app.api_provider, |
| 342 | &app.model, |
| 343 | app.active_route_limits, |
| 344 | ); |
| 345 | let context_window_source = app.active_context_window_source.display_label(); |
| 346 | let provider = |
| 347 | format!("{provider_name} · context {context_window} ({context_window_source})"); |
| 348 | let auth = readiness.label().into_owned(); |
| 349 | let health = if provider_ready { |
| 350 | format!("{}; route can be attempted", readiness.label()) |
| 351 | } else if matches!( |
| 352 | &readiness, |
| 353 | crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. } |
| 354 | ) { |
| 355 | format!("{}; retry or open /provider", readiness.label()) |
| 356 | } else if app.api_provider == crate::config::ApiProvider::OpenaiCodex { |
| 357 | format!( |
| 358 | "{}; Sign in with ChatGPT via `codewhale auth chatgpt` or /provider setup openai-codex (subscription billing). Codex CLI import remains an explicit alternative.", |
| 359 | readiness.label() |
| 360 | ) |
| 361 | } else if let Some(url) = crate::config::credential_help_for_provider_route( |
| 362 | app.api_provider, |
| 363 | &config.active_route_base_url(), |
| 364 | ) |
| 365 | .credential_url |
| 366 | { |
| 367 | format!( |
| 368 | "{}; credentials: {url}; open /provider to repair the route", |
| 369 | readiness.label() |
| 370 | ) |
| 371 | } else { |
| 372 | format!( |
| 373 | "{}; {}; open /provider to repair the route", |
| 374 | readiness.label(), |
| 375 | crate::config::credential_help_for_provider_route( |
| 376 | app.api_provider, |
| 377 | &config.active_route_base_url(), |
| 378 | ) |
| 379 | .guidance |
| 380 | ) |
| 381 | }; |
| 382 | let provider_result = format!( |
| 383 | "provider={}, model={}, context_window={} ({}) auth={}, health={}", |
| 384 | app.provider_identity_for_persistence(), |
| 385 | model, |
| 386 | context_window, |
| 387 | context_window_source, |
| 388 | readiness.label(), |
| 389 | if provider_ready { |
| 390 | "attemptable" |
| 391 | } else { |
| 392 | "needs action" |
| 393 | } |
| 394 | ); |
| 395 | let shell = if app.allow_shell { "enabled" } else { "hidden" }.to_string(); |
| 396 | let trust = if app.trust_mode { |
| 397 | "trusted workspace / writes allowed by posture" |
| 398 | } else { |
| 399 | "workspace trust not elevated" |
| 400 | } |
| 401 | .to_string(); |
| 402 | let sandbox = config |
| 403 | .sandbox_mode |
| 404 | .as_deref() |
| 405 | .filter(|mode| !mode.trim().is_empty()) |
| 406 | .unwrap_or("default") |
| 407 | .to_string(); |
| 408 | let sandbox_mode_value = sandbox.clone(); |
| 409 | let network_default_value = config |
| 410 | .network |
| 411 | .as_ref() |
| 412 | .map_or("prompt".to_string(), |policy| policy.default.clone()); |
| 413 | let network = config |
| 414 | .network |
| 415 | .as_ref() |
| 416 | .map_or("prompt by default".to_string(), |policy| { |
| 417 | format!("default {}", policy.default) |
| 418 | }); |
| 419 | let runtime_result = format!( |
| 420 | "intent={}, approval={}, shell={}, trust={}, sandbox={}, network={}", |
| 421 | app.mode.as_setting(), |
| 422 | app.approval_mode |
| 423 | .permission_chip_label() |
| 424 | .to_ascii_lowercase(), |
| 425 | if app.allow_shell { "enabled" } else { "hidden" }, |
| 426 | if app.trust_mode { |
| 427 | "trusted" |
| 428 | } else { |
| 429 | "workspace" |
| 430 | }, |
| 431 | sandbox, |
| 432 | network |
| 433 | ); |
| 434 | let operate = operate::SetupOperateFacts::from_app_config(app, config, provider_ready); |
| 435 | let known_hotbar_action_ids = app |
| 436 | .hotbar_actions |
| 437 | .iter() |
| 438 | .map(|action| action.id()) |
| 439 | .collect::<Vec<_>>(); |
| 440 | let hotbar_resolution = config.resolve_hotbar_bindings(&known_hotbar_action_ids); |
| 441 | let configured_hotbar_slots = config.hotbar.as_ref().map_or(0, Vec::len); |
| 442 | let hotbar_state = match config.hotbar.as_ref() { |
| 443 | None => "hidden", |
| 444 | Some(bindings) if bindings.is_empty() => "disabled", |
| 445 | Some(_) => "customized", |
| 446 | }; |
| 447 | let active_hotbar_slots = hotbar_resolution.bindings.len(); |
| 448 | let hotbar_warning_count = hotbar_resolution.warnings.len(); |
| 449 | let hotbar_bindings_result = format!( |
| 450 | "{hotbar_state}; configured_slots={configured_hotbar_slots}; active_slots={active_hotbar_slots}; warnings={hotbar_warning_count}" |
| 451 | ); |
| 452 | let hotbar_actions_result = |
| 453 | format!("{} bindable actions registered", app.hotbar_actions.len()); |
| 454 | let hotbar_result = format!( |
| 455 | "state={hotbar_state}, configured_slots={configured_hotbar_slots}, active_slots={active_hotbar_slots}, actions={}, warnings={hotbar_warning_count}", |
| 456 | app.hotbar_actions.len() |
| 457 | ); |
| 458 | let codewhale_home = setup_codewhale_home_dir(); |
| 459 | let persistence = SetupPersistenceFacts::from_app_config(app, config, &codewhale_home); |
| 460 | let tools_mcp = |
| 461 | tools_mcp::SetupToolsMcpFacts::from_app_config(app, config, &codewhale_home); |
| 462 | let tools_mcp_servers_result = tools_mcp.servers_result; |
| 463 | let tools_mcp_skills_result = tools_mcp.skills_result; |
| 464 | let tools_mcp_tools_result = tools_mcp.tools_result; |
| 465 | let tools_mcp_plugins_result = tools_mcp.plugins_result; |
| 466 | let tools_mcp_hotbar_result = tools_mcp.hotbar_result; |
| 467 | let tools_mcp_dsh_result = tools_mcp.dsh_result; |
| 468 | let tools_mcp_result = tools_mcp.result; |
| 469 | let tools_mcp_needs_action = tools_mcp.needs_action; |
| 470 | let tools_mcp_path_display = tools_mcp.mcp_path_display; |
| 471 | let tools_mcp_skills_path_display = tools_mcp.skills_path_display; |
| 472 | let tools_mcp_plugins_path_display = tools_mcp.plugins_path_display; |
| 473 | let remote = SetupRemoteFacts::from_app(app); |
| 474 | let remote_needs_action = remote.needs_action(); |
| 475 | let constitution_autonomy = UserConstitution::load() |
| 476 | .ok() |
| 477 | .and_then(|load| { |
| 478 | load.constitution().map(|constitution| { |
| 479 | autonomy_label(constitution.autonomy_preference, app.ui_locale).to_string() |
| 480 | }) |
| 481 | }) |
| 482 | .unwrap_or_else(|| tr(app.ui_locale, MessageId::SetupAutonomyUnspecified).to_string()); |
| 483 | Self { |
| 484 | provider, |
| 485 | model, |
| 486 | auth, |
| 487 | health, |
| 488 | provider_ready, |
| 489 | provider_result, |
| 490 | work_intent: app.mode.display_name().to_string(), |
| 491 | approval: app |
| 492 | .approval_mode |
| 493 | .permission_chip_label() |
| 494 | .to_ascii_lowercase(), |
| 495 | shell, |
| 496 | allow_shell_enabled: app.allow_shell, |
| 497 | trust, |
| 498 | sandbox, |
| 499 | sandbox_mode_value, |
| 500 | network, |
| 501 | network_default_value, |
| 502 | runtime_result, |
| 503 | operate_runtime_ready: operate.runtime_ready, |
| 504 | operate_runtime_result: operate.runtime_result, |
| 505 | fleet_roster_ready: operate.roster_ready, |
| 506 | fleet_roster_result: operate.roster_result, |
| 507 | operate_concurrency_result: operate.concurrency_result, |
| 508 | operate_result: operate.result, |
| 509 | hotbar_bindings_result, |
| 510 | hotbar_actions_result, |
| 511 | hotbar_result, |
| 512 | tools_mcp_servers_result, |
| 513 | tools_mcp_skills_result, |
| 514 | tools_mcp_tools_result, |
| 515 | tools_mcp_plugins_result, |
| 516 | tools_mcp_hotbar_result, |
| 517 | tools_mcp_dsh_result, |
| 518 | tools_mcp_result, |
| 519 | tools_mcp_needs_action, |
| 520 | tools_mcp_path_display, |
| 521 | tools_mcp_skills_path_display, |
| 522 | tools_mcp_plugins_path_display, |
| 523 | remote_clouds_result: remote.clouds_result, |
| 524 | remote_bridges_result: remote.bridges_result, |
| 525 | remote_providers_result: remote.providers_result, |
| 526 | remote_mode_result: remote.mode_result, |
| 527 | remote_command_provider: remote.command_provider, |
| 528 | remote_result: remote.result, |
| 529 | remote_control_result: { |
| 530 | let status = app.remote_control.status_line(); |
| 531 | let message = if status.starts_with("Remote control: connected") { |
| 532 | MessageId::SetupRemoteStatusReady |
| 533 | } else if status.starts_with("Remote control: connecting") |
| 534 | || status.starts_with("Remote control: stopping") |
| 535 | { |
| 536 | MessageId::SetupStatusInProgress |
| 537 | } else if status.starts_with("Remote control: disconnected") { |
| 538 | MessageId::SetupRemoteStatusNeedsAction |
| 539 | } else { |
| 540 | MessageId::SetupRemoteStatusDisabled |
| 541 | }; |
| 542 | tr(app.ui_locale, message).into_owned() |
| 543 | }, |
| 544 | remote_needs_action, |
| 545 | remote_modes: remote.modes, |
| 546 | persistence, |
| 547 | default_mode: app.mode.as_setting().to_string(), |
| 548 | approval_policy_value: config |
| 549 | .approval_policy |
| 550 | .as_deref() |
| 551 | .filter(|policy| !policy.trim().is_empty()) |
| 552 | .unwrap_or("on-request") |
| 553 | .to_string(), |
| 554 | project_override_warning: project_runtime_override_warning( |
| 555 | &app.workspace, |
| 556 | app.ui_locale, |
| 557 | ), |
| 558 | constitution_autonomy, |
| 559 | constitution_file: SetupConstitutionFileState::load(), |
| 560 | expert_override, |
| 561 | } |
| 562 | } |
| 563 | } |
| 564 | |
| 565 | fn setup_codewhale_home_dir() -> std::path::PathBuf { |
| 566 | codewhale_config::codewhale_home().unwrap_or_else(|_| { |
| 567 | crate::config::effective_home_dir().map_or_else( |
| 568 | || std::path::PathBuf::from(".codewhale"), |
| 569 | |home| home.join(".codewhale"), |
| 570 | ) |
| 571 | }) |
| 572 | } |
| 573 | |
| 574 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] |
| 575 | pub enum SetupRuntimePreset { |
| 576 | AskFirst, |
| 577 | #[default] |
| 578 | NormalAgent, |
| 579 | HighTrustLocal, |
| 580 | } |
| 581 | |
| 582 | impl SetupRuntimePreset { |
| 583 | const ALL: [Self; 3] = [Self::AskFirst, Self::NormalAgent, Self::HighTrustLocal]; |
| 584 | |
| 585 | fn from_key(key: char) -> Option<Self> { |
| 586 | match key { |
| 587 | '1' => Some(Self::AskFirst), |
| 588 | '2' => Some(Self::NormalAgent), |
| 589 | '3' => Some(Self::HighTrustLocal), |
| 590 | _ => None, |
| 591 | } |
| 592 | } |
| 593 | |
| 594 | pub fn id(self) -> &'static str { |
| 595 | match self { |
| 596 | Self::AskFirst => "ask-first", |
| 597 | Self::NormalAgent => "normal-agent", |
| 598 | Self::HighTrustLocal => "high-trust-local", |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | fn title_id(self) -> MessageId { |
| 603 | match self { |
| 604 | Self::AskFirst => MessageId::SetupRuntimePresetAskFirstTitle, |
| 605 | Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentTitle, |
| 606 | Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustTitle, |
| 607 | } |
| 608 | } |
| 609 | |
| 610 | fn description_id(self) -> MessageId { |
| 611 | match self { |
| 612 | Self::AskFirst => MessageId::SetupRuntimePresetAskFirstDescription, |
| 613 | Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentDescription, |
| 614 | Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustDescription, |
| 615 | } |
| 616 | } |
| 617 | |
| 618 | pub fn default_mode(self) -> &'static str { |
| 619 | match self { |
| 620 | Self::AskFirst => "plan", |
| 621 | Self::NormalAgent | Self::HighTrustLocal => "agent", |
| 622 | } |
| 623 | } |
| 624 | |
| 625 | pub fn permission_posture(self) -> &'static str { |
| 626 | match self { |
| 627 | Self::AskFirst | Self::NormalAgent => "ask", |
| 628 | Self::HighTrustLocal => "full-access", |
| 629 | } |
| 630 | } |
| 631 | |
| 632 | pub fn approval_policy(self) -> Option<&'static str> { |
| 633 | match self { |
| 634 | Self::AskFirst | Self::NormalAgent => Some("on-request"), |
| 635 | // Full Access lives in TUI settings; it is intentionally not a |
| 636 | // top-level approval_policy value. |
| 637 | Self::HighTrustLocal => None, |
| 638 | } |
| 639 | } |
| 640 | |
| 641 | pub fn allow_shell(self) -> bool { |
| 642 | match self { |
| 643 | Self::AskFirst => false, |
| 644 | Self::NormalAgent | Self::HighTrustLocal => true, |
| 645 | } |
| 646 | } |
| 647 | |
| 648 | pub fn sandbox_mode(self) -> &'static str { |
| 649 | match self { |
| 650 | Self::AskFirst => "read-only", |
| 651 | Self::NormalAgent => "workspace-write", |
| 652 | Self::HighTrustLocal => "danger-full-access", |
| 653 | } |
| 654 | } |
| 655 | |
| 656 | pub fn result_summary(self) -> String { |
| 657 | let approval = self |
| 658 | .approval_policy() |
| 659 | .unwrap_or("unset (Full Access saved in TUI settings)"); |
| 660 | format!( |
| 661 | "preset={}, default_mode={}, permission_posture={}, approval_policy={}, allow_shell={}, sandbox_mode={}, network=unchanged, trust=unchanged", |
| 662 | self.id(), |
| 663 | self.display_mode(), |
| 664 | self.permission_posture(), |
| 665 | approval, |
| 666 | self.allow_shell(), |
| 667 | self.sandbox_mode() |
| 668 | ) |
| 669 | } |
| 670 | |
| 671 | fn display_mode(self) -> &'static str { |
| 672 | match self { |
| 673 | Self::AskFirst => "plan", |
| 674 | Self::NormalAgent => "act", |
| 675 | Self::HighTrustLocal => "act + full-access", |
| 676 | } |
| 677 | } |
| 678 | } |
| 679 | |
| 680 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 681 | enum SetupConstitutionFileState { |
| 682 | NotChecked, |
| 683 | Missing, |
| 684 | Loaded, |
| 685 | Empty, |
| 686 | Invalid, |
| 687 | Unreadable, |
| 688 | PathError, |
| 689 | } |
| 690 | |
| 691 | impl SetupConstitutionFileState { |
| 692 | fn load() -> Self { |
| 693 | match UserConstitution::path() { |
| 694 | Ok(path) => Self::from_load(&UserConstitution::load_from(&path)), |
| 695 | Err(_) => Self::PathError, |
| 696 | } |
| 697 | } |
| 698 | |
| 699 | fn from_load(load: &UserConstitutionLoad) -> Self { |
| 700 | match load { |
| 701 | UserConstitutionLoad::Missing => Self::Missing, |
| 702 | UserConstitutionLoad::Empty => Self::Empty, |
| 703 | UserConstitutionLoad::Invalid(_) => Self::Invalid, |
| 704 | UserConstitutionLoad::Unreadable(_) => Self::Unreadable, |
| 705 | UserConstitutionLoad::Loaded(_) => Self::Loaded, |
| 706 | } |
| 707 | } |
| 708 | |
| 709 | fn label(self, choice: ConstitutionChoice, locale: Locale) -> Cow<'static, str> { |
| 710 | let id = match self { |
| 711 | Self::NotChecked => MessageId::SetupConstitutionFileNotChecked, |
| 712 | Self::Missing => MessageId::SetupConstitutionFileMissing, |
| 713 | Self::Loaded if choice == ConstitutionChoice::GuidedCustom => { |
| 714 | MessageId::SetupConstitutionFileLoadedSelected |
| 715 | } |
| 716 | Self::Loaded if choice.is_explicit() => MessageId::SetupConstitutionFileLoadedInactive, |
| 717 | Self::Loaded => MessageId::SetupConstitutionFileLoadedUnselected, |
| 718 | Self::Empty => MessageId::SetupConstitutionFileEmpty, |
| 719 | Self::Invalid => MessageId::SetupConstitutionFileInvalid, |
| 720 | Self::Unreadable => MessageId::SetupConstitutionFileUnreadable, |
| 721 | Self::PathError => MessageId::SetupConstitutionFilePathError, |
| 722 | }; |
| 723 | tr(locale, id) |
| 724 | } |
| 725 | } |
| 726 | |
| 727 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 728 | enum SetupExpertOverrideState { |
| 729 | NotChecked, |
| 730 | Missing, |
| 731 | Active, |
| 732 | Disabled, |
| 733 | Empty, |
| 734 | Unreadable, |
| 735 | PathError, |
| 736 | } |
| 737 | |
| 738 | impl SetupExpertOverrideState { |
| 739 | fn load() -> Self { |
| 740 | let Some(path) = expert_override_path() else { |
| 741 | return Self::PathError; |
| 742 | }; |
| 743 | match std::fs::read_to_string(&path) { |
| 744 | Ok(raw) if raw.trim().is_empty() => Self::Empty, |
| 745 | Ok(_) if base_prompt_override_opt_in() => Self::Active, |
| 746 | Ok(_) => Self::Disabled, |
| 747 | Err(err) if err.kind() == std::io::ErrorKind::NotFound => Self::Missing, |
| 748 | Err(_) => Self::Unreadable, |
| 749 | } |
| 750 | } |
| 751 | |
| 752 | fn is_active(self) -> bool { |
| 753 | matches!(self, Self::Active) |
| 754 | } |
| 755 | |
| 756 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 757 | match self { |
| 758 | Self::NotChecked => tr(locale, MessageId::SetupExpertOverrideNotChecked), |
| 759 | Self::Missing => tr(locale, MessageId::SetupExpertOverrideMissing), |
| 760 | Self::Active => tr(locale, MessageId::SetupExpertOverrideActive), |
| 761 | Self::Disabled => tr(locale, MessageId::SetupExpertOverrideDisabled) |
| 762 | .replace("{env}", BASE_PROMPT_OVERRIDE_OPT_IN_ENV) |
| 763 | .into(), |
| 764 | Self::Empty => tr(locale, MessageId::SetupExpertOverrideEmpty), |
| 765 | Self::Unreadable => tr(locale, MessageId::SetupExpertOverrideUnreadable), |
| 766 | Self::PathError => tr(locale, MessageId::SetupExpertOverridePathError), |
| 767 | } |
| 768 | } |
| 769 | } |
| 770 | |
| 771 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 772 | pub(crate) struct GuidedConstitutionDraft { |
| 773 | purpose: GuidedPurpose, |
| 774 | autonomy: AutonomyPreference, |
| 775 | evidence: GuidedEvidence, |
| 776 | communication: GuidedCommunication, |
| 777 | privacy: GuidedPrivacy, |
| 778 | principles: GuidedPrinciples, |
| 779 | } |
| 780 | |
| 781 | impl Default for GuidedConstitutionDraft { |
| 782 | fn default() -> Self { |
| 783 | Self { |
| 784 | purpose: GuidedPurpose::Coding, |
| 785 | autonomy: AutonomyPreference::Balanced, |
| 786 | evidence: GuidedEvidence::TestsAndReceipts, |
| 787 | communication: GuidedCommunication::Concise, |
| 788 | privacy: GuidedPrivacy::StandardCare, |
| 789 | principles: GuidedPrinciples::ScopedChanges, |
| 790 | } |
| 791 | } |
| 792 | } |
| 793 | |
| 794 | impl GuidedConstitutionDraft { |
| 795 | fn cycle(&mut self, key: char) -> bool { |
| 796 | match key { |
| 797 | '1' => self.purpose = self.purpose.next(), |
| 798 | '2' => self.autonomy = next_guided_autonomy(self.autonomy), |
| 799 | '3' => self.evidence = self.evidence.next(), |
| 800 | '4' => self.communication = self.communication.next(), |
| 801 | '5' => self.privacy = self.privacy.next(), |
| 802 | '6' => self.principles = self.principles.next(), |
| 803 | _ => return false, |
| 804 | } |
| 805 | true |
| 806 | } |
| 807 | |
| 808 | fn to_constitution_with_freeform( |
| 809 | self, |
| 810 | locale: Locale, |
| 811 | freeform_note: Option<&str>, |
| 812 | ) -> UserConstitution { |
| 813 | let mut notes = self.notes(locale); |
| 814 | if let Some(note) = freeform_note.map(str::trim).filter(|note| !note.is_empty()) { |
| 815 | let own_words = match locale { |
| 816 | Locale::Ja => format!( |
| 817 | "\nユーザー自由原則:{}", |
| 818 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 819 | ), |
| 820 | Locale::ZhHans => format!( |
| 821 | "\n用户自定义准则:{}", |
| 822 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 823 | ), |
| 824 | Locale::ZhHant => format!( |
| 825 | "\n使用者自由原則:{}", |
| 826 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 827 | ), |
| 828 | Locale::PtBr => format!( |
| 829 | "\nPrincípio livre do usuário: {}", |
| 830 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 831 | ), |
| 832 | Locale::Es419 => format!( |
| 833 | "\nPrincipio libre del usuario: {}", |
| 834 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 835 | ), |
| 836 | Locale::Vi => format!( |
| 837 | "\nNguyên tắc tự do của người dùng: {}", |
| 838 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 839 | ), |
| 840 | Locale::Ko => format!( |
| 841 | "\n사용자 자유 원칙: {}", |
| 842 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 843 | ), |
| 844 | Locale::Ca => format!( |
| 845 | "\nPrincipi lliure de l'usuari: {}", |
| 846 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 847 | ), |
| 848 | Locale::De => format!( |
| 849 | "\nFreitext-Prinzip des Nutzers: {}", |
| 850 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 851 | ), |
| 852 | Locale::Fr => format!( |
| 853 | "\nPrincipe en texte libre de l'utilisateur : {}", |
| 854 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 855 | ), |
| 856 | Locale::Id => format!( |
| 857 | "\nPrinsip bebas pengguna: {}", |
| 858 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 859 | ), |
| 860 | Locale::Hi => format!( |
| 861 | "\nउपयोगकर्ता मुक्त-पाठ सिद्धांत: {}", |
| 862 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 863 | ), |
| 864 | Locale::Ru => format!( |
| 865 | "\nСвободный принцип пользователя: {}", |
| 866 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 867 | ), |
| 868 | Locale::Uk => format!( |
| 869 | "\nВільний принцип користувача: {}", |
| 870 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 871 | ), |
| 872 | _ => format!( |
| 873 | "\nUser freeform principle: {}", |
| 874 | bounded_freeform_note(note, MAX_NOTES_LEN) |
| 875 | ), |
| 876 | }; |
| 877 | notes.push_str(&own_words); |
| 878 | } |
| 879 | UserConstitution { |
| 880 | language: Some(locale.tag().to_string()), |
| 881 | about: Some(self.purpose.about(locale).to_string()), |
| 882 | working_style: vec![ |
| 883 | self.purpose.working_style(locale).to_string(), |
| 884 | self.communication.working_style(locale).to_string(), |
| 885 | self.evidence.working_style(locale).to_string(), |
| 886 | self.privacy.working_style(locale).to_string(), |
| 887 | ], |
| 888 | priorities: vec![ |
| 889 | authority_priority(locale).to_string(), |
| 890 | autonomy_priority(self.autonomy, locale).to_string(), |
| 891 | self.privacy.escalation_rule(locale).to_string(), |
| 892 | ], |
| 893 | autonomy_preference: self.autonomy, |
| 894 | notes: Some(notes), |
| 895 | ..UserConstitution::default() |
| 896 | } |
| 897 | } |
| 898 | |
| 899 | fn notes(self, locale: Locale) -> String { |
| 900 | let notes = tr(locale, MessageId::SetupGuidedNotes); |
| 901 | notes |
| 902 | .replace("{purpose}", &self.purpose.label(locale)) |
| 903 | .replace("{initiative}", autonomy_label(self.autonomy, locale)) |
| 904 | .replace("{evidence}", &self.evidence.label(locale)) |
| 905 | .replace("{communication}", self.communication.label(locale)) |
| 906 | .replace("{privacy}", self.privacy.label(locale)) |
| 907 | .replace("{principles}", self.principles.label(locale)) |
| 908 | .replace("{notes}", self.principles.note(locale)) |
| 909 | .to_string() |
| 910 | } |
| 911 | } |
| 912 | |
| 913 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 914 | enum GuidedPurpose { |
| 915 | Coding, |
| 916 | Research, |
| 917 | Operations, |
| 918 | Mixed, |
| 919 | } |
| 920 | |
| 921 | impl GuidedPurpose { |
| 922 | fn next(self) -> Self { |
| 923 | match self { |
| 924 | Self::Coding => Self::Research, |
| 925 | Self::Research => Self::Operations, |
| 926 | Self::Operations => Self::Mixed, |
| 927 | Self::Mixed => Self::Coding, |
| 928 | } |
| 929 | } |
| 930 | |
| 931 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 932 | match self { |
| 933 | Self::Coding => tr(locale, MessageId::SetupGuidedPurposeCoding), |
| 934 | Self::Research => tr(locale, MessageId::SetupGuidedPurposeResearch), |
| 935 | Self::Operations => tr(locale, MessageId::SetupGuidedPurposeOperations), |
| 936 | Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeMixed), |
| 937 | } |
| 938 | } |
| 939 | |
| 940 | fn about(self, locale: Locale) -> Cow<'static, str> { |
| 941 | match self { |
| 942 | Self::Coding => tr(locale, MessageId::SetupGuidedPurposeAboutCoding), |
| 943 | Self::Research => tr(locale, MessageId::SetupGuidedPurposeAboutResearch), |
| 944 | Self::Operations => tr(locale, MessageId::SetupGuidedPurposeAboutOperations), |
| 945 | Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeAboutMixed), |
| 946 | } |
| 947 | } |
| 948 | |
| 949 | fn working_style(self, locale: Locale) -> Cow<'static, str> { |
| 950 | match self { |
| 951 | Self::Coding => tr(locale, MessageId::SetupGuidedStyleCoding), |
| 952 | Self::Research => tr(locale, MessageId::SetupGuidedStyleResearch), |
| 953 | Self::Operations => tr(locale, MessageId::SetupGuidedStyleOperations), |
| 954 | Self::Mixed => tr(locale, MessageId::SetupGuidedStyleMixed), |
| 955 | } |
| 956 | } |
| 957 | } |
| 958 | |
| 959 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 960 | enum GuidedEvidence { |
| 961 | Assumptions, |
| 962 | TestsAndReceipts, |
| 963 | ReleaseReceipts, |
| 964 | } |
| 965 | |
| 966 | impl GuidedEvidence { |
| 967 | fn next(self) -> Self { |
| 968 | match self { |
| 969 | Self::Assumptions => Self::TestsAndReceipts, |
| 970 | Self::TestsAndReceipts => Self::ReleaseReceipts, |
| 971 | Self::ReleaseReceipts => Self::Assumptions, |
| 972 | } |
| 973 | } |
| 974 | |
| 975 | fn label(self, locale: Locale) -> Cow<'static, str> { |
| 976 | match self { |
| 977 | Self::Assumptions => tr(locale, MessageId::SetupGuidedEvidenceAssumptions), |
| 978 | Self::TestsAndReceipts => tr(locale, MessageId::SetupGuidedEvidenceTestsAndReceipts), |
| 979 | Self::ReleaseReceipts => tr(locale, MessageId::SetupGuidedEvidenceReleaseReceipts), |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | fn working_style(self, locale: Locale) -> &'static str { |
| 984 | match (locale, self) { |
| 985 | (Locale::Ja, Self::Assumptions) => { |
| 986 | "完了を主張する前に、前提、不明点、残るリスクを要約する。" |
| 987 | } |
| 988 | (Locale::Ja, Self::TestsAndReceipts) => { |
| 989 | "不確実性を減らせるときは、コマンド、テスト、スクリーンショット、引用で具体的に検証する。" |
| 990 | } |
| 991 | (Locale::Ja, Self::ReleaseReceipts) => { |
| 992 | "重要な主張とリリース証拠には、ファイル、コマンド、スクリーンショット、CI、出典を示す。" |
| 993 | } |
| 994 | (Locale::ZhHans, Self::Assumptions) => "在宣称完成前总结假设、未知和剩余风险。", |
| 995 | (Locale::ZhHans, Self::TestsAndReceipts) => { |
| 996 | "在能降低不确定性时,用命令、测试、截图或引用给出具体验证。" |
| 997 | } |
| 998 | (Locale::ZhHans, Self::ReleaseReceipts) => { |
| 999 | "对重要结论和发布证据标注文件、命令、截图、CI 或来源。" |
| 1000 | } |
| 1001 | (Locale::ZhHant, Self::Assumptions) => "在宣稱完成前總結假設、未知和剩餘風險。", |
| 1002 | (Locale::ZhHant, Self::TestsAndReceipts) => { |
| 1003 | "在能降低不確定性時,用命令、測試、截圖或引用給出具體驗證。" |
| 1004 | } |
| 1005 | (Locale::ZhHant, Self::ReleaseReceipts) => { |
| 1006 | "對重要結論和發布證據標註檔案、命令、截圖、CI 或來源。" |
| 1007 | } |
| 1008 | (Locale::PtBr, Self::Assumptions) => { |
| 1009 | "Resuma premissas, desconhecidos e risco restante antes de dizer que concluiu." |
| 1010 | } |
| 1011 | (Locale::PtBr, Self::TestsAndReceipts) => { |
| 1012 | "Use comandos, testes, screenshots ou citações quando reduzirem a incerteza." |
| 1013 | } |
| 1014 | (Locale::PtBr, Self::ReleaseReceipts) => { |
| 1015 | "Cite arquivos, comandos, screenshots, CI ou fontes para afirmações materiais e evidência de release." |
| 1016 | } |
| 1017 | (Locale::Es419, Self::Assumptions) => { |
| 1018 | "Resume supuestos, incógnitas y riesgo restante antes de afirmar que terminaste." |
| 1019 | } |
| 1020 | (Locale::Es419, Self::TestsAndReceipts) => { |
| 1021 | "Usa comandos, pruebas, capturas o citas cuando reduzcan materialmente la incertidumbre." |
| 1022 | } |
| 1023 | (Locale::Es419, Self::ReleaseReceipts) => { |
| 1024 | "Cita archivos, comandos, capturas, CI o fuentes para afirmaciones materiales y evidencia de release." |
| 1025 | } |
| 1026 | (Locale::Vi, Self::Assumptions) => { |
| 1027 | "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." |
| 1028 | } |
| 1029 | (Locale::Vi, Self::TestsAndReceipts) => { |
| 1030 | "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." |
| 1031 | } |
| 1032 | (Locale::Vi, Self::ReleaseReceipts) => { |
| 1033 | "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." |
| 1034 | } |
| 1035 | (Locale::Ko, Self::Assumptions) => { |
| 1036 | "완료를 주장하기 전에 가정, 불확실한 점, 남은 위험을 요약한다." |
| 1037 | } |
| 1038 | (Locale::Ko, Self::TestsAndReceipts) => { |
| 1039 | "불확실성을 실질적으로 줄일 수 있을 때는 명령어, 테스트, 스크린샷, 인용으로 구체적으로 검증한다." |
| 1040 | } |
| 1041 | (Locale::Ko, Self::ReleaseReceipts) => { |
| 1042 | "중요한 주장과 릴리스 근거에는 파일 경로, 명령어, 스크린샷, CI, 출처를 제시한다." |
| 1043 | } |
| 1044 | (Locale::Ca, Self::Assumptions) => { |
| 1045 | "Resumeix supòsits, incògnites i risc pendent abans de dir que has acabat." |
| 1046 | } |
| 1047 | (Locale::Ca, Self::TestsAndReceipts) => { |
| 1048 | "Fes servir ordres, tests, captures de pantalla o citacions quan redueixin materialment la incertesa." |
| 1049 | } |
| 1050 | (Locale::Ca, Self::ReleaseReceipts) => { |
| 1051 | "Cita rutes de fitxers, ordres, captures de pantalla, CI o fonts per a afirmacions materials i evidència de release." |
| 1052 | } |
| 1053 | (Locale::De, Self::Assumptions) => { |
| 1054 | "Fasse Annahmen, Unbekannte und Restrisiken zusammen, bevor du Fertigstellung behauptest." |
| 1055 | } |
| 1056 | (Locale::De, Self::TestsAndReceipts) => { |
| 1057 | "Nutze Befehle, Tests, Screenshots oder Zitate, wenn sie die Unsicherheit wesentlich verringern." |
| 1058 | } |
| 1059 | (Locale::De, Self::ReleaseReceipts) => { |
| 1060 | "Nenne Dateipfade, Befehle, Screenshots, CI oder Quellen für wesentliche Aussagen und Release-Nachweise." |
| 1061 | } |
| 1062 | (Locale::Fr, Self::Assumptions) => { |
| 1063 | "Résumez les hypothèses, les inconnues et le risque restant avant d'annoncer la fin du travail." |
| 1064 | } |
| 1065 | (Locale::Fr, Self::TestsAndReceipts) => { |
| 1066 | "Utilisez commandes, tests, captures d'écran ou citations quand ils réduisent sensiblement l'incertitude." |
| 1067 | } |
| 1068 | (Locale::Fr, Self::ReleaseReceipts) => { |
| 1069 | "Citez chemins de fichiers, commandes, captures d'écran, CI ou sources pour les affirmations importantes et les preuves de release." |
| 1070 | } |
| 1071 | (Locale::Id, Self::Assumptions) => { |
| 1072 | "Ringkas asumsi, hal yang belum diketahui, dan risiko tersisa sebelum mengklaim selesai." |
| 1073 | } |
| 1074 | (Locale::Id, Self::TestsAndReceipts) => { |
| 1075 | "Gunakan perintah, tes, tangkapan layar, atau kutipan bila secara nyata mengurangi ketidakpastian." |
| 1076 | } |
| 1077 | (Locale::Id, Self::ReleaseReceipts) => { |
| 1078 | "Kutip path file, perintah, tangkapan layar, CI, atau sumber untuk klaim material dan bukti rilis." |
| 1079 | } |
| 1080 | (Locale::Hi, Self::Assumptions) => { |
| 1081 | "पूर्णता का दावा करने से पहले धारणाएँ, अज्ञात बातें और शेष जोखिम सारांशित करें।" |
| 1082 | } |
| 1083 | (Locale::Hi, Self::TestsAndReceipts) => { |
| 1084 | "जब वे अनिश्चितता सार्थक रूप से घटाएँ तो कमांड, टेस्ट, स्क्रीनशॉट या उद्धरण उपयोग करें।" |
| 1085 | } |
| 1086 | (Locale::Hi, Self::ReleaseReceipts) => { |
| 1087 | "महत्वपूर्ण दावों और रिलीज़ साक्ष्य के लिए फ़ाइल पथ, कमांड, स्क्रीनशॉट, CI या स्रोत उद्धृत करें।" |
| 1088 | } |
| 1089 | (Locale::Ru, Self::Assumptions) => { |
| 1090 | "Прежде чем заявить о завершении, перечислите предположения, неизвестные и оставшиеся риски." |
| 1091 | } |
| 1092 | (Locale::Ru, Self::TestsAndReceipts) => { |
| 1093 | "Используйте команды, тесты, скриншоты или цитаты, когда они существенно снижают неопределённость." |
| 1094 | } |
| 1095 | (Locale::Ru, Self::ReleaseReceipts) => { |
| 1096 | "Указывайте пути файлов, команды, скриншоты, CI или источники для существенных утверждений и доказательств релиза." |
| 1097 | } |
| 1098 | (Locale::Uk, Self::Assumptions) => { |
| 1099 | "Перш ніж заявити про завершення, підсумуйте припущення, невідомі та залишкові ризики." |
| 1100 | } |
| 1101 | (Locale::Uk, Self::TestsAndReceipts) => { |
| 1102 | "Використовуйте команди, тести, скриншоти або цитати, коли вони суттєво зменшують невизначеність." |
| 1103 | } |
| 1104 | (Locale::Uk, Self::ReleaseReceipts) => { |
| 1105 | "Посилайтеся на шляхи файлів, команди, скриншоти, CI або джерела для суттєвих тверджень і доказів релізу." |
| 1106 | } |
| 1107 | (_, Self::Assumptions) => { |
| 1108 | "Summarize assumptions, unknowns, and remaining risk before claiming completion." |
| 1109 | } |
| 1110 | (_, Self::TestsAndReceipts) => { |
| 1111 | "Use commands, tests, screenshots, or citations when they materially reduce uncertainty." |
| 1112 | } |
| 1113 | (_, Self::ReleaseReceipts) => { |
| 1114 | "Cite file paths, commands, screenshots, CI, or sources for material claims and release evidence." |
| 1115 | } |
| 1116 | } |
| 1117 | } |
| 1118 | } |
| 1119 | |
| 1120 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1121 | enum GuidedCommunication { |
| 1122 | Concise, |
| 1123 | Teaching, |
| 1124 | Direct, |
| 1125 | } |
| 1126 | |
| 1127 | impl GuidedCommunication { |
| 1128 | fn next(self) -> Self { |
| 1129 | match self { |
| 1130 | Self::Concise => Self::Teaching, |
| 1131 | Self::Teaching => Self::Direct, |
| 1132 | Self::Direct => Self::Concise, |
| 1133 | } |
| 1134 | } |
| 1135 | |
| 1136 | fn label(self, locale: Locale) -> &'static str { |
| 1137 | match (locale, self) { |
| 1138 | (Locale::Ja, Self::Concise) => "簡潔", |
| 1139 | (Locale::Ja, Self::Teaching) => "説明重視", |
| 1140 | (Locale::Ja, Self::Direct) => "直接的", |
| 1141 | (Locale::ZhHans, Self::Concise) => "简洁", |
| 1142 | (Locale::ZhHans, Self::Teaching) => "教学式", |
| 1143 | (Locale::ZhHans, Self::Direct) => "直接", |
| 1144 | (Locale::ZhHant, Self::Concise) => "簡潔", |
| 1145 | (Locale::ZhHant, Self::Teaching) => "教學式", |
| 1146 | (Locale::ZhHant, Self::Direct) => "直接", |
| 1147 | (Locale::PtBr, Self::Concise) => "conciso", |
| 1148 | (Locale::PtBr, Self::Teaching) => "didático", |
| 1149 | (Locale::PtBr, Self::Direct) => "direto", |
| 1150 | (Locale::Es419, Self::Concise) => "conciso", |
| 1151 | (Locale::Es419, Self::Teaching) => "didáctico", |
| 1152 | (Locale::Es419, Self::Direct) => "directo", |
| 1153 | (Locale::Vi, Self::Concise) => "ngắn gọn", |
| 1154 | (Locale::Vi, Self::Teaching) => "giảng giải", |
| 1155 | (Locale::Vi, Self::Direct) => "trực tiếp", |
| 1156 | (Locale::Ko, Self::Concise) => "간결함", |
| 1157 | (Locale::Ko, Self::Teaching) => "설명 중심", |
| 1158 | (Locale::Ko, Self::Direct) => "직설적", |
| 1159 | (Locale::Ca, Self::Concise) => "concís", |
| 1160 | (Locale::Ca, Self::Teaching) => "didàctic", |
| 1161 | (Locale::Ca, Self::Direct) => "directe", |
| 1162 | (Locale::De, Self::Concise) => "prägnant", |
| 1163 | (Locale::De, Self::Teaching) => "lehrend", |
| 1164 | (Locale::De, Self::Direct) => "direkt", |
| 1165 | (Locale::Fr, Self::Concise) => "concis", |
| 1166 | (Locale::Fr, Self::Teaching) => "pédagogique", |
| 1167 | (Locale::Fr, Self::Direct) => "direct", |
| 1168 | (Locale::Id, Self::Concise) => "ringkas", |
| 1169 | (Locale::Id, Self::Teaching) => "mengajar", |
| 1170 | (Locale::Id, Self::Direct) => "langsung", |
| 1171 | (Locale::Hi, Self::Concise) => "संक्षिप्त", |
| 1172 | (Locale::Hi, Self::Teaching) => "शिक्षणपरक", |
| 1173 | (Locale::Hi, Self::Direct) => "सीधा", |
| 1174 | (Locale::Ru, Self::Concise) => "краткий", |
| 1175 | (Locale::Ru, Self::Teaching) => "обучающий", |
| 1176 | (Locale::Ru, Self::Direct) => "прямой", |
| 1177 | (Locale::Uk, Self::Concise) => "стислий", |
| 1178 | (Locale::Uk, Self::Teaching) => "навчальний", |
| 1179 | (Locale::Uk, Self::Direct) => "прямий", |
| 1180 | (_, Self::Concise) => "concise", |
| 1181 | (_, Self::Teaching) => "teaching", |
| 1182 | (_, Self::Direct) => "direct", |
| 1183 | } |
| 1184 | } |
| 1185 | |
| 1186 | fn working_style(self, locale: Locale) -> &'static str { |
| 1187 | match (locale, self) { |
| 1188 | (Locale::Ja, Self::Concise) => "更新は簡潔にし、重要なトレードオフだけ短く説明する。", |
| 1189 | (Locale::Ja, Self::Teaching) => { |
| 1190 | "重要な推論とトレードオフを、ユーザーが仕組みを理解できる程度に説明する。" |
| 1191 | } |
| 1192 | (Locale::Ja, Self::Direct) => { |
| 1193 | "阻塞、リスク、不確実性を直接述べ、装飾的な文案を避ける。" |
| 1194 | } |
| 1195 | (Locale::ZhHans, Self::Concise) => "保持更新简洁,并只解释重要取舍。", |
| 1196 | (Locale::ZhHans, Self::Teaching) => "解释关键推理和取舍,让用户能理解系统。", |
| 1197 | (Locale::ZhHans, Self::Direct) => "直接说明阻塞、风险和不确定性,避免装饰性文案。", |
| 1198 | (Locale::ZhHant, Self::Concise) => "保持更新簡潔,並只解釋重要取捨。", |
| 1199 | (Locale::ZhHant, Self::Teaching) => "解釋關鍵推理和取捨,讓使用者能理解系統。", |
| 1200 | (Locale::ZhHant, Self::Direct) => "直接說明阻塞、風險和不確定性,避免裝飾性文案。", |
| 1201 | (Locale::PtBr, Self::Concise) => { |
| 1202 | "Mantenha atualizações concisas e explique brevemente só os tradeoffs importantes." |
| 1203 | } |
| 1204 | (Locale::PtBr, Self::Teaching) => { |
| 1205 | "Explique raciocínio e tradeoffs principais o bastante para o usuário entender o sistema." |
| 1206 | } |
| 1207 | (Locale::PtBr, Self::Direct) => { |
| 1208 | "Seja direto sobre bloqueios, risco e incerteza; evite texto ornamental." |
| 1209 | } |
| 1210 | (Locale::Es419, Self::Concise) => { |
| 1211 | "Mantén las actualizaciones concisas y explica brevemente solo los tradeoffs importantes." |
| 1212 | } |
| 1213 | (Locale::Es419, Self::Teaching) => { |
| 1214 | "Explica el razonamiento y los tradeoffs clave lo suficiente para que el usuario entienda el sistema." |
| 1215 | } |
| 1216 | (Locale::Es419, Self::Direct) => { |
| 1217 | "Sé directo sobre bloqueos, riesgo e incertidumbre; evita texto ornamental." |
| 1218 | } |
| 1219 | (Locale::Vi, Self::Concise) => { |
| 1220 | "Giữ cập nhật ngắn gọn và chỉ giải thích ngắn các đánh đổi quan trọng." |
| 1221 | } |
| 1222 | (Locale::Vi, Self::Teaching) => { |
| 1223 | "Giải thích suy luận và đánh đổi chính đủ để người dùng hiểu hệ thống." |
| 1224 | } |
| 1225 | (Locale::Vi, Self::Direct) => { |
| 1226 | "Nói thẳng về điểm chặn, rủi ro và bất định; tránh câu chữ trang trí." |
| 1227 | } |
| 1228 | (Locale::Ko, Self::Concise) => { |
| 1229 | "업데이트는 간결하게 유지하고, 중요한 트레이드오프만 짧게 설명한다." |
| 1230 | } |
| 1231 | (Locale::Ko, Self::Teaching) => { |
| 1232 | "사용자가 시스템을 이해할 수 있을 만큼 핵심 추론과 트레이드오프를 설명한다." |
| 1233 | } |
| 1234 | (Locale::Ko, Self::Direct) => { |
| 1235 | "차단 요인, 위험, 불확실성을 직설적으로 말하고 장식적인 표현은 피한다." |
| 1236 | } |
| 1237 | (Locale::Ca, Self::Concise) => { |
| 1238 | "Mantén les actualitzacions concises i explica breument només els compromisos importants." |
| 1239 | } |
| 1240 | (Locale::Ca, Self::Teaching) => { |
| 1241 | "Explica el raonament i els compromisos clau prou perquè l'usuari pugui entendre el sistema." |
| 1242 | } |
| 1243 | (Locale::Ca, Self::Direct) => { |
| 1244 | "Sigues directe sobre bloquejos, risc i incertesa; evita el text ornamental." |
| 1245 | } |
| 1246 | (Locale::De, Self::Concise) => { |
| 1247 | "Halte Aktualisierungen knapp und erkläre wichtige Trade-offs nur kurz." |
| 1248 | } |
| 1249 | (Locale::De, Self::Teaching) => { |
| 1250 | "Erkläre zentrale Begründungen und Trade-offs so weit, dass der Nutzer das System verstehen kann." |
| 1251 | } |
| 1252 | (Locale::De, Self::Direct) => { |
| 1253 | "Sei direkt bei Blockern, Risiken und Unsicherheit; vermeide dekorative Formulierungen." |
| 1254 | } |
| 1255 | (Locale::Fr, Self::Concise) => { |
| 1256 | "Gardez les mises à jour concises et n'expliquez que brièvement les arbitrages importants." |
| 1257 | } |
| 1258 | (Locale::Fr, Self::Teaching) => { |
| 1259 | "Expliquez le raisonnement et les arbitrages clés assez pour que l'utilisateur comprenne le système." |
| 1260 | } |
| 1261 | (Locale::Fr, Self::Direct) => { |
| 1262 | "Soyez direct sur les blocages, les risques et l'incertitude ; évitez le texte ornemental." |
| 1263 | } |
| 1264 | (Locale::Id, Self::Concise) => { |
| 1265 | "Jaga pembaruan tetap ringkas dan jelaskan tradeoff penting secara singkat." |
| 1266 | } |
| 1267 | (Locale::Id, Self::Teaching) => { |
| 1268 | "Jelaskan penalaran dan tradeoff kunci secukupnya agar pengguna dapat memahami sistem." |
| 1269 | } |
| 1270 | (Locale::Id, Self::Direct) => { |
| 1271 | "Bicara langsung soal penghambat, risiko, dan ketidakpastian; hindari teks hiasan." |
| 1272 | } |
| 1273 | (Locale::Hi, Self::Concise) => "अपडेट संक्षिप्त रखें और महत्वपूर्ण ट्रेडऑफ़ संक्षेप में समझाएँ।", |
| 1274 | (Locale::Hi, Self::Teaching) => { |
| 1275 | "मुख्य तर्क और ट्रेडऑफ़ इतना समझाएँ कि उपयोगकर्ता सिस्टम समझ सके।" |
| 1276 | } |
| 1277 | (Locale::Hi, Self::Direct) => { |
| 1278 | "रुकावटों, जोखिम और अनिश्चितता के बारे में सीधे बोलें; सजावटी भाषा से बचें।" |
| 1279 | } |
| 1280 | (Locale::Ru, Self::Concise) => { |
| 1281 | "Держите обновления краткими и лишь коротко поясняйте важные компромиссы." |
| 1282 | } |
| 1283 | (Locale::Ru, Self::Teaching) => { |
| 1284 | "Объясняйте ключевые рассуждения и компромиссы настолько, чтобы пользователь мог понять систему." |
| 1285 | } |
| 1286 | (Locale::Ru, Self::Direct) => { |
| 1287 | "Говорите прямо о блокерах, рисках и неопределённости; избегайте декоративных формулировок." |
| 1288 | } |
| 1289 | (Locale::Uk, Self::Concise) => { |
| 1290 | "Тримайте оновлення стислими й лише коротко пояснюйте важливі компроміси." |
| 1291 | } |
| 1292 | (Locale::Uk, Self::Teaching) => { |
| 1293 | "Пояснюйте ключові міркування та компроміси настільки, щоб користувач міг зрозуміти систему." |
| 1294 | } |
| 1295 | (Locale::Uk, Self::Direct) => { |
| 1296 | "Говоріть прямо про блокери, ризики та невизначеність; уникайте декоративних формулювань." |
| 1297 | } |
| 1298 | (_, Self::Concise) => "Keep updates concise and explain important tradeoffs briefly.", |
| 1299 | (_, Self::Teaching) => { |
| 1300 | "Explain key reasoning and tradeoffs enough that the user can learn the system." |
| 1301 | } |
| 1302 | (_, Self::Direct) => { |
| 1303 | "Be direct about blockers, risk, and uncertainty; avoid ornamental copy." |
| 1304 | } |
| 1305 | } |
| 1306 | } |
| 1307 | } |
| 1308 | |
| 1309 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1310 | enum GuidedPrivacy { |
| 1311 | StandardCare, |
| 1312 | StrictBoundaries, |
| 1313 | ProjectLocal, |
| 1314 | } |
| 1315 | |
| 1316 | impl GuidedPrivacy { |
| 1317 | fn next(self) -> Self { |
| 1318 | match self { |
| 1319 | Self::StandardCare => Self::StrictBoundaries, |
| 1320 | Self::StrictBoundaries => Self::ProjectLocal, |
| 1321 | Self::ProjectLocal => Self::StandardCare, |
| 1322 | } |
| 1323 | } |
| 1324 | |
| 1325 | fn label(self, locale: Locale) -> &'static str { |
| 1326 | match (locale, self) { |
| 1327 | (Locale::Ja, Self::StandardCare) => "標準保護", |
| 1328 | (Locale::Ja, Self::StrictBoundaries) => "厳格な境界", |
| 1329 | (Locale::Ja, Self::ProjectLocal) => "プロジェクト内メモリ", |
| 1330 | (Locale::ZhHans, Self::StandardCare) => "标准保护", |
| 1331 | (Locale::ZhHans, Self::StrictBoundaries) => "严格边界", |
| 1332 | (Locale::ZhHans, Self::ProjectLocal) => "项目内记忆", |
| 1333 | (Locale::ZhHant, Self::StandardCare) => "標準保護", |
| 1334 | (Locale::ZhHant, Self::StrictBoundaries) => "嚴格邊界", |
| 1335 | (Locale::ZhHant, Self::ProjectLocal) => "專案內記憶", |
| 1336 | (Locale::PtBr, Self::StandardCare) => "cuidado padrão", |
| 1337 | (Locale::PtBr, Self::StrictBoundaries) => "limites estritos", |
| 1338 | (Locale::PtBr, Self::ProjectLocal) => "memória local do projeto", |
| 1339 | (Locale::Es419, Self::StandardCare) => "cuidado estándar", |
| 1340 | (Locale::Es419, Self::StrictBoundaries) => "límites estrictos", |
| 1341 | (Locale::Es419, Self::ProjectLocal) => "memoria local del proyecto", |
| 1342 | (Locale::Vi, Self::StandardCare) => "bảo vệ tiêu chuẩn", |
| 1343 | (Locale::Vi, Self::StrictBoundaries) => "ranh giới nghiêm ngặt", |
| 1344 | (Locale::Vi, Self::ProjectLocal) => "bộ nhớ trong dự án", |
| 1345 | (Locale::Ko, Self::StandardCare) => "표준 보호", |
| 1346 | (Locale::Ko, Self::StrictBoundaries) => "엄격한 경계", |
| 1347 | (Locale::Ko, Self::ProjectLocal) => "프로젝트 내 메모리", |
| 1348 | (Locale::Ca, Self::StandardCare) => "cura estàndard", |
| 1349 | (Locale::Ca, Self::StrictBoundaries) => "límits estrictes", |
| 1350 | (Locale::Ca, Self::ProjectLocal) => "memòria local del projecte", |
| 1351 | (Locale::De, Self::StandardCare) => "Standardvorsorge", |
| 1352 | (Locale::De, Self::StrictBoundaries) => "strenge Grenzen", |
| 1353 | (Locale::De, Self::ProjectLocal) => "projektlokaler Speicher", |
| 1354 | (Locale::Fr, Self::StandardCare) => "soin standard", |
| 1355 | (Locale::Fr, Self::StrictBoundaries) => "limites strictes", |
| 1356 | (Locale::Fr, Self::ProjectLocal) => "mémoire locale au projet", |
| 1357 | (Locale::Id, Self::StandardCare) => "perlindungan standar", |
| 1358 | (Locale::Id, Self::StrictBoundaries) => "batasan ketat", |
| 1359 | (Locale::Id, Self::ProjectLocal) => "memori lokal proyek", |
| 1360 | (Locale::Hi, Self::StandardCare) => "मानक सावधानी", |
| 1361 | (Locale::Hi, Self::StrictBoundaries) => "सख्त सीमाएँ", |
| 1362 | (Locale::Hi, Self::ProjectLocal) => "प्रोजेक्ट-स्थानीय मेमोरी", |
| 1363 | (Locale::Ru, Self::StandardCare) => "стандартная осторожность", |
| 1364 | (Locale::Ru, Self::StrictBoundaries) => "строгие границы", |
| 1365 | (Locale::Ru, Self::ProjectLocal) => "память внутри проекта", |
| 1366 | (Locale::Uk, Self::StandardCare) => "стандартна обережність", |
| 1367 | (Locale::Uk, Self::StrictBoundaries) => "суворі межі", |
| 1368 | (Locale::Uk, Self::ProjectLocal) => "пам'ять у межах проєкту", |
| 1369 | (_, Self::StandardCare) => "standard care", |
| 1370 | (_, Self::StrictBoundaries) => "strict boundaries", |
| 1371 | (_, Self::ProjectLocal) => "project-local memory", |
| 1372 | } |
| 1373 | } |
| 1374 | |
| 1375 | fn working_style(self, locale: Locale) -> &'static str { |
| 1376 | match (locale, self) { |
| 1377 | (Locale::Ja, Self::StandardCare) => { |
| 1378 | "秘密情報、ユーザーファイル、Git 履歴、本番システム、コスト、プライバシー、時間を保護する。" |
| 1379 | } |
| 1380 | (Locale::Ja, Self::StrictBoundaries) => { |
| 1381 | "秘密、個人データ、認証情報、本番状態、資金、公開操作は、先に確認する境界として扱う。" |
| 1382 | } |
| 1383 | (Locale::Ja, Self::ProjectLocal) => { |
| 1384 | "プロジェクト固有の文脈はプロジェクト内に留め、明示要求がない限りメモリへ書かない。" |
| 1385 | } |
| 1386 | (Locale::ZhHans, Self::StandardCare) => { |
| 1387 | "保护密钥、用户文件、Git 历史、生产系统、成本、隐私和时间。" |
| 1388 | } |
| 1389 | (Locale::ZhHans, Self::StrictBoundaries) => { |
| 1390 | "把密钥、个人数据、凭据、生产状态、资金和发布动作视为先确认边界。" |
| 1391 | } |
| 1392 | (Locale::ZhHans, Self::ProjectLocal) => { |
| 1393 | "项目特定上下文留在项目内,除非明确要求,否则不要写入记忆。" |
| 1394 | } |
| 1395 | (Locale::ZhHant, Self::StandardCare) => { |
| 1396 | "保護密鑰、使用者檔案、Git 歷史、生產系統、成本、隱私和時間。" |
| 1397 | } |
| 1398 | (Locale::ZhHant, Self::StrictBoundaries) => { |
| 1399 | "把密鑰、個人資料、憑據、生產狀態、資金和發布動作視為先確認邊界。" |
| 1400 | } |
| 1401 | (Locale::ZhHant, Self::ProjectLocal) => { |
| 1402 | "專案特定上下文留在專案內,除非明確要求,否則不要寫入記憶。" |
| 1403 | } |
| 1404 | (Locale::PtBr, Self::StandardCare) => { |
| 1405 | "Proteja segredos, arquivos do usuário, histórico git, produção, custo, privacidade e tempo." |
| 1406 | } |
| 1407 | (Locale::PtBr, Self::StrictBoundaries) => { |
| 1408 | "Trate segredos, dados pessoais, credenciais, estado de produção, dinheiro e publicações como limites de confirmação." |
| 1409 | } |
| 1410 | (Locale::PtBr, Self::ProjectLocal) => { |
| 1411 | "Mantenha contexto específico do projeto no projeto; evite gravar na memória sem pedido explícito." |
| 1412 | } |
| 1413 | (Locale::Es419, Self::StandardCare) => { |
| 1414 | "Protege secretos, archivos del usuario, historial git, producción, costo, privacidad y tiempo." |
| 1415 | } |
| 1416 | (Locale::Es419, Self::StrictBoundaries) => { |
| 1417 | "Trata secretos, datos personales, credenciales, estado de producción, dinero y publicaciones como límites de confirmación." |
| 1418 | } |
| 1419 | (Locale::Es419, Self::ProjectLocal) => { |
| 1420 | "Mantén el contexto específico del proyecto en el proyecto; evita llevarlo a memoria sin pedido explícito." |
| 1421 | } |
| 1422 | (Locale::Vi, Self::StandardCare) => { |
| 1423 | "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." |
| 1424 | } |
| 1425 | (Locale::Vi, Self::StrictBoundaries) => { |
| 1426 | "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." |
| 1427 | } |
| 1428 | (Locale::Vi, Self::ProjectLocal) => { |
| 1429 | "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õ." |
| 1430 | } |
| 1431 | (Locale::Ko, Self::StandardCare) => { |
| 1432 | "비밀 정보, 사용자 파일, Git 이력, 프로덕션 시스템, 비용, 프라이버시, 시간을 보호한다." |
| 1433 | } |
| 1434 | (Locale::Ko, Self::StrictBoundaries) => { |
| 1435 | "비밀 정보, 개인 데이터, 자격 증명, 프로덕션 상태, 자금, 게시 작업은 먼저 확인하는 경계로 취급한다." |
| 1436 | } |
| 1437 | (Locale::Ko, Self::ProjectLocal) => { |
| 1438 | "프로젝트 고유 맥락은 프로젝트 안에 두고, 명시적으로 요청받지 않는 한 메모리에 쓰지 않는다." |
| 1439 | } |
| 1440 | (Locale::Ca, Self::StandardCare) => { |
| 1441 | "Protegeix secrets, fitxers de l'usuari, historial de git, sistemes de producció, cost, privacitat i temps." |
| 1442 | } |
| 1443 | (Locale::Ca, Self::StrictBoundaries) => { |
| 1444 | "Tracta secrets, dades personals, credencials, estat de producció, diners i accions de publicació com a límits que cal confirmar primer." |
| 1445 | } |
| 1446 | (Locale::Ca, Self::ProjectLocal) => { |
| 1447 | "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." |
| 1448 | } |
| 1449 | (Locale::De, Self::StandardCare) => { |
| 1450 | "Schütze Geheimnisse, Nutzerdateien, Git-Verlauf, Produktionssysteme, Kosten, Privatsphäre und Zeit." |
| 1451 | } |
| 1452 | (Locale::De, Self::StrictBoundaries) => { |
| 1453 | "Behandle Geheimnisse, persönliche Daten, Zugangsdaten, Produktionszustand, Geld und Veröffentlichungen als Grenzen, die erst bestätigt werden." |
| 1454 | } |
| 1455 | (Locale::De, Self::ProjectLocal) => { |
| 1456 | "Halte projektspezifischen Kontext im Projekt; vermeide es, sensible Details ohne ausdrückliche Bitte in den Speicher zu übernehmen." |
| 1457 | } |
| 1458 | (Locale::Fr, Self::StandardCare) => { |
| 1459 | "Protégez secrets, fichiers utilisateur, historique git, systèmes de production, coût, vie privée et temps." |
| 1460 | } |
| 1461 | (Locale::Fr, Self::StrictBoundaries) => { |
| 1462 | "Traitez secrets, données personnelles, identifiants, état de production, argent et publications comme des limites exigeant confirmation." |
| 1463 | } |
| 1464 | (Locale::Fr, Self::ProjectLocal) => { |
| 1465 | "Gardez le contexte propre au projet dans le projet ; évitez de l'écrire en mémoire sans demande explicite." |
| 1466 | } |
| 1467 | (Locale::Id, Self::StandardCare) => { |
| 1468 | "Lindungi rahasia, file pengguna, riwayat git, sistem produksi, biaya, privasi, dan waktu." |
| 1469 | } |
| 1470 | (Locale::Id, Self::StrictBoundaries) => { |
| 1471 | "Perlakukan rahasia, data pribadi, kredensial, status produksi, uang, dan tindakan publikasi sebagai batas yang harus dikonfirmasi dulu." |
| 1472 | } |
| 1473 | (Locale::Id, Self::ProjectLocal) => { |
| 1474 | "Simpan konteks khusus proyek di dalam proyek; hindari membawanya ke memori kecuali diminta secara eksplisit." |
| 1475 | } |
| 1476 | (Locale::Hi, Self::StandardCare) => { |
| 1477 | "रहस्यों, उपयोगकर्ता फ़ाइलों, git इतिहास, प्रोडक्शन सिस्टम, लागत, गोपनीयता और समय की रक्षा करें।" |
| 1478 | } |
| 1479 | (Locale::Hi, Self::StrictBoundaries) => { |
| 1480 | "रहस्यों, व्यक्तिगत डेटा, क्रेडेंशियल, प्रोडक्शन स्थिति, धन और प्रकाशन क्रियाओं को पहले-पुष्टि सीमाओं की तरह मानें।" |
| 1481 | } |
| 1482 | (Locale::Hi, Self::ProjectLocal) => { |
| 1483 | "प्रोजेक्ट-विशिष्ट संदर्भ प्रोजेक्ट के भीतर रखें; स्पष्ट अनुरोध के बिना संवेदनशील विवरण मेमोरी में न ले जाएँ।" |
| 1484 | } |
| 1485 | (Locale::Ru, Self::StandardCare) => { |
| 1486 | "Защищайте секреты, файлы пользователя, историю git, production-системы, затраты, приватность и время." |
| 1487 | } |
| 1488 | (Locale::Ru, Self::StrictBoundaries) => { |
| 1489 | "Считайте секреты, персональные данные, учётные данные, production-состояние, деньги и публикации границами, требующими подтверждения." |
| 1490 | } |
| 1491 | (Locale::Ru, Self::ProjectLocal) => { |
| 1492 | "Держите контекст, специфичный для проекта, внутри проекта; не переносите чувствительные детали в память без явного запроса." |
| 1493 | } |
| 1494 | (Locale::Uk, Self::StandardCare) => { |
| 1495 | "Захищайте секрети, файли користувача, історію git, production-системи, витрати, приватність і час." |
| 1496 | } |
| 1497 | (Locale::Uk, Self::StrictBoundaries) => { |
| 1498 | "Вважайте секрети, персональні дані, облікові дані, production-стан, гроші та публікації межами, що потребують підтвердження." |
| 1499 | } |
| 1500 | (Locale::Uk, Self::ProjectLocal) => { |
| 1501 | "Тримайте контекст, специфічний для проєкту, всередині проєкту; не переносьте чутливі деталі в пам'ять без явного запиту." |
| 1502 | } |
| 1503 | (_, Self::StandardCare) => { |
| 1504 | "Protect secrets, user files, git history, production systems, cost, privacy, and time." |
| 1505 | } |
| 1506 | (_, Self::StrictBoundaries) => { |
| 1507 | "Treat secrets, personal data, credentials, production state, money, and publish actions as stop-and-confirm boundaries." |
| 1508 | } |
| 1509 | (_, Self::ProjectLocal) => { |
| 1510 | "Keep project-specific context local; avoid carrying sensitive details into memory unless explicitly asked." |
| 1511 | } |
| 1512 | } |
| 1513 | } |
| 1514 | |
| 1515 | fn escalation_rule(self, locale: Locale) -> &'static str { |
| 1516 | match (locale, self) { |
| 1517 | (Locale::Ja, Self::StandardCare) => { |
| 1518 | "破壊的、高コスト、認証情報、公開、法務、セキュリティリスクのある操作の前に尋ねる。" |
| 1519 | } |
| 1520 | (Locale::Ja, Self::StrictBoundaries) => { |
| 1521 | "機微情報の読み取りや拡散、本番システム操作、支出、公開の前に停止して尋ねる。" |
| 1522 | } |
| 1523 | (Locale::Ja, Self::ProjectLocal) => { |
| 1524 | "プロジェクト詳細をメモリ、ワークスペース、古い引き継ぎへ持ち出す前に確認する。" |
| 1525 | } |
| 1526 | (Locale::ZhHans, Self::StandardCare) => { |
| 1527 | "遇到破坏性、高成本、凭据、发布、法律或安全风险操作时先询问。" |
| 1528 | } |
| 1529 | (Locale::ZhHans, Self::StrictBoundaries) => { |
| 1530 | "在读取或传播敏感信息、触碰生产系统、花费资金或发布内容前停止并询问。" |
| 1531 | } |
| 1532 | (Locale::ZhHans, Self::ProjectLocal) => { |
| 1533 | "需要跨项目记忆、复制项目细节或引用旧交接时,先确认这些上下文仍适用。" |
| 1534 | } |
| 1535 | (Locale::ZhHant, Self::StandardCare) => { |
| 1536 | "遇到破壞性、高成本、憑據、發布、法律或安全風險操作時先詢問。" |
| 1537 | } |
| 1538 | (Locale::ZhHant, Self::StrictBoundaries) => { |
| 1539 | "在讀取或傳播敏感資訊、觸碰生產系統、花費資金或發布內容前停止並詢問。" |
| 1540 | } |
| 1541 | (Locale::ZhHant, Self::ProjectLocal) => { |
| 1542 | "需要跨專案記憶、複製專案細節或引用舊交接時,先確認這些上下文仍適用。" |
| 1543 | } |
| 1544 | (Locale::PtBr, Self::StandardCare) => { |
| 1545 | "Pergunte antes de ações destrutivas, caras, com credenciais, publicação, risco legal ou de segurança." |
| 1546 | } |
| 1547 | (Locale::PtBr, Self::StrictBoundaries) => { |
| 1548 | "Pare e pergunte antes de ler ou espalhar dados sensíveis, tocar produção, gastar dinheiro ou publicar." |
| 1549 | } |
| 1550 | (Locale::PtBr, Self::ProjectLocal) => { |
| 1551 | "Confirme antes de levar detalhes do projeto para memória, workspaces ou handoffs antigos." |
| 1552 | } |
| 1553 | (Locale::Es419, Self::StandardCare) => { |
| 1554 | "Pregunta antes de acciones destructivas, costosas, con credenciales, publicación o riesgo legal/de seguridad." |
| 1555 | } |
| 1556 | (Locale::Es419, Self::StrictBoundaries) => { |
| 1557 | "Detente y pregunta antes de leer o difundir datos sensibles, tocar producción, gastar dinero o publicar." |
| 1558 | } |
| 1559 | (Locale::Es419, Self::ProjectLocal) => { |
| 1560 | "Confirma antes de llevar detalles del proyecto a memoria, workspaces o handoffs viejos." |
| 1561 | } |
| 1562 | (Locale::Vi, Self::StandardCare) => { |
| 1563 | "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." |
| 1564 | } |
| 1565 | (Locale::Vi, Self::StrictBoundaries) => { |
| 1566 | "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." |
| 1567 | } |
| 1568 | (Locale::Vi, Self::ProjectLocal) => { |
| 1569 | "Xác nhận trước khi mang chi tiết dự án sang bộ nhớ, workspace khác hoặc handoff cũ." |
| 1570 | } |
| 1571 | (Locale::Ko, Self::StandardCare) => { |
| 1572 | "파괴적이거나, 비용이 크거나, 자격 증명, 게시, 법적, 보안 위험이 있는 작업 전에 먼저 물어본다." |
| 1573 | } |
| 1574 | (Locale::Ko, Self::StrictBoundaries) => { |
| 1575 | "민감 정보를 읽거나 퍼뜨리기 전, 프로덕션 시스템을 건드리기 전, 자금을 쓰거나 게시하기 전에 멈추고 물어본다." |
| 1576 | } |
| 1577 | (Locale::Ko, Self::ProjectLocal) => { |
| 1578 | "프로젝트 세부 정보를 메모리, 다른 워크스페이스, 오래된 인계 자료로 옮기기 전에 확인한다." |
| 1579 | } |
| 1580 | (Locale::Ca, Self::StandardCare) => { |
| 1581 | "Pregunta abans d'accions destructives, costoses, amb credencials, de publicació o amb risc legal o de seguretat." |
| 1582 | } |
| 1583 | (Locale::Ca, Self::StrictBoundaries) => { |
| 1584 | "Atura't i pregunta abans de llegir o difondre dades sensibles, tocar sistemes de producció, gastar diners o publicar." |
| 1585 | } |
| 1586 | (Locale::Ca, Self::ProjectLocal) => { |
| 1587 | "Confirma abans de portar detalls del projecte a la memòria, a altres espais de treball o a traspasos antics." |
| 1588 | } |
| 1589 | (Locale::De, Self::StandardCare) => { |
| 1590 | "Frage vor destruktiven, kostspieligen, zugangsdatenbezogenen, veröffentlichenden, rechtlichen oder sicherheitskritischen Aktionen." |
| 1591 | } |
| 1592 | (Locale::De, Self::StrictBoundaries) => { |
| 1593 | "Halte an und frage, bevor du sensible Daten liest oder verbreitest, Produktionssysteme anfasst, Geld ausgibst oder veröffentlichst." |
| 1594 | } |
| 1595 | (Locale::De, Self::ProjectLocal) => { |
| 1596 | "Bestätige, bevor du Projektdetails in Speicher, Workspaces oder veraltete Übergaben überträgst." |
| 1597 | } |
| 1598 | (Locale::Fr, Self::StandardCare) => { |
| 1599 | "Demandez avant toute action destructive, coûteuse, impliquant des identifiants, une publication, ou un risque juridique ou de sécurité." |
| 1600 | } |
| 1601 | (Locale::Fr, Self::StrictBoundaries) => { |
| 1602 | "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." |
| 1603 | } |
| 1604 | (Locale::Fr, Self::ProjectLocal) => { |
| 1605 | "Confirmez avant de transporter des détails du projet vers la mémoire, d'autres espaces de travail ou d'anciens transferts." |
| 1606 | } |
| 1607 | (Locale::Id, Self::StandardCare) => { |
| 1608 | "Tanya sebelum tindakan destruktif, mahal, terkait kredensial, publikasi, hukum, atau berisiko keamanan." |
| 1609 | } |
| 1610 | (Locale::Id, Self::StrictBoundaries) => { |
| 1611 | "Berhenti dan tanya sebelum membaca atau menyebarkan data sensitif, menyentuh sistem produksi, membelanjakan uang, atau mempublikasikan." |
| 1612 | } |
| 1613 | (Locale::Id, Self::ProjectLocal) => { |
| 1614 | "Konfirmasi sebelum membawa detail proyek ke memori, workspace lain, atau handoff lama." |
| 1615 | } |
| 1616 | (Locale::Hi, Self::StandardCare) => { |
| 1617 | "विनाशकारी, उच्च-लागत, क्रेडेंशियल, प्रकाशन, कानूनी या सुरक्षा-जोखिम कार्यों से पहले पूछें।" |
| 1618 | } |
| 1619 | (Locale::Hi, Self::StrictBoundaries) => { |
| 1620 | "संवेदनशील डेटा पढ़ने या फैलाने, प्रोडक्शन सिस्टम छूने, धन खर्च करने या प्रकाशित करने से पहले रुककर पूछें।" |
| 1621 | } |
| 1622 | (Locale::Hi, Self::ProjectLocal) => { |
| 1623 | "प्रोजेक्ट विवरण मेमोरी, अन्य कार्यक्षेत्रों या पुराने हैंडऑफ़ में ले जाने से पहले पुष्टि करें।" |
| 1624 | } |
| 1625 | (Locale::Ru, Self::StandardCare) => { |
| 1626 | "Спрашивайте перед деструктивными, дорогими, связанными с учётными данными, публикацией, юридическими или угрожающими безопасности действиями." |
| 1627 | } |
| 1628 | (Locale::Ru, Self::StrictBoundaries) => { |
| 1629 | "Остановитесь и спросите, прежде чем читать или распространять чувствительные данные, трогать production-системы, тратить деньги или публиковать." |
| 1630 | } |
| 1631 | (Locale::Ru, Self::ProjectLocal) => { |
| 1632 | "Подтвердите, прежде чем переносить детали проекта в память, другие рабочие области или устаревшие передаточные заметки." |
| 1633 | } |
| 1634 | (Locale::Uk, Self::StandardCare) => { |
| 1635 | "Питайте перед руйнівними, дорогими, пов'язаними з обліковими даними, публікацією, юридичними чи небезпечними для безпеки діями." |
| 1636 | } |
| 1637 | (Locale::Uk, Self::StrictBoundaries) => { |
| 1638 | "Зупиніться й запитайте, перш ніж читати чи поширювати чутливі дані, чіпати production-системи, витрачати гроші або публікувати." |
| 1639 | } |
| 1640 | (Locale::Uk, Self::ProjectLocal) => { |
| 1641 | "Підтвердьте, перш ніж переносити деталі проєкту в пам'ять, інші робочі простори чи застарілі передаточні нотатки." |
| 1642 | } |
| 1643 | (_, Self::StandardCare) => { |
| 1644 | "Ask before destructive, high-cost, credential, publishing, legal, or security-risk actions." |
| 1645 | } |
| 1646 | (_, Self::StrictBoundaries) => { |
| 1647 | "Stop and ask before reading or spreading sensitive data, touching production systems, spending money, or publishing." |
| 1648 | } |
| 1649 | (_, Self::ProjectLocal) => { |
| 1650 | "Confirm before carrying project details across memory, workspaces, or stale handoffs." |
| 1651 | } |
| 1652 | } |
| 1653 | } |
| 1654 | } |
| 1655 | |
| 1656 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 1657 | enum GuidedPrinciples { |
| 1658 | ScopedChanges, |
| 1659 | UserVoice, |
| 1660 | ReversibleOps, |
| 1661 | } |
| 1662 | |
| 1663 | impl GuidedPrinciples { |
| 1664 | fn next(self) -> Self { |
| 1665 | match self { |
| 1666 | Self::ScopedChanges => Self::UserVoice, |
| 1667 | Self::UserVoice => Self::ReversibleOps, |
| 1668 | Self::ReversibleOps => Self::ScopedChanges, |
| 1669 | } |
| 1670 | } |
| 1671 | |
| 1672 | fn label(self, locale: Locale) -> &'static str { |
| 1673 | match (locale, self) { |
| 1674 | (Locale::Ja, Self::ScopedChanges) => "小さく絞った変更", |
| 1675 | (Locale::Ja, Self::UserVoice) => "ユーザーの声を保つ", |
| 1676 | (Locale::Ja, Self::ReversibleOps) => "可逆手順", |
| 1677 | (Locale::ZhHans, Self::ScopedChanges) => "小范围改动", |
| 1678 | (Locale::ZhHans, Self::UserVoice) => "保留用户语气", |
| 1679 | (Locale::ZhHans, Self::ReversibleOps) => "可逆步骤", |
| 1680 | (Locale::ZhHant, Self::ScopedChanges) => "小範圍改動", |
| 1681 | (Locale::ZhHant, Self::UserVoice) => "保留使用者語氣", |
| 1682 | (Locale::ZhHant, Self::ReversibleOps) => "可逆步驟", |
| 1683 | (Locale::PtBr, Self::ScopedChanges) => "mudanças focadas", |
| 1684 | (Locale::PtBr, Self::UserVoice) => "preservar voz do usuário", |
| 1685 | (Locale::PtBr, Self::ReversibleOps) => "passos reversíveis", |
| 1686 | (Locale::Es419, Self::ScopedChanges) => "cambios acotados", |
| 1687 | (Locale::Es419, Self::UserVoice) => "preservar voz del usuario", |
| 1688 | (Locale::Es419, Self::ReversibleOps) => "pasos reversibles", |
| 1689 | (Locale::Vi, Self::ScopedChanges) => "thay đổi có phạm vi", |
| 1690 | (Locale::Vi, Self::UserVoice) => "giữ giọng người dùng", |
| 1691 | (Locale::Vi, Self::ReversibleOps) => "bước có thể đảo ngược", |
| 1692 | (Locale::Ko, Self::ScopedChanges) => "범위가 명확한 변경", |
| 1693 | (Locale::Ko, Self::UserVoice) => "사용자의 어조 유지", |
| 1694 | (Locale::Ko, Self::ReversibleOps) => "되돌릴 수 있는 단계", |
| 1695 | (Locale::Ca, Self::ScopedChanges) => "canvis acotats", |
| 1696 | (Locale::Ca, Self::UserVoice) => "veu de l'usuari", |
| 1697 | (Locale::Ca, Self::ReversibleOps) => "passos reversibles", |
| 1698 | (Locale::De, Self::ScopedChanges) => "begrenzte Änderungen", |
| 1699 | (Locale::De, Self::UserVoice) => "Stimme des Nutzers", |
| 1700 | (Locale::De, Self::ReversibleOps) => "reversible Schritte", |
| 1701 | (Locale::Fr, Self::ScopedChanges) => "changements ciblés", |
| 1702 | (Locale::Fr, Self::UserVoice) => "voix de l'utilisateur", |
| 1703 | (Locale::Fr, Self::ReversibleOps) => "étapes réversibles", |
| 1704 | (Locale::Id, Self::ScopedChanges) => "perubahan terbatas", |
| 1705 | (Locale::Id, Self::UserVoice) => "suara pengguna", |
| 1706 | (Locale::Id, Self::ReversibleOps) => "langkah reversibel", |
| 1707 | (Locale::Hi, Self::ScopedChanges) => "सीमित बदलाव", |
| 1708 | (Locale::Hi, Self::UserVoice) => "उपयोगकर्ता की आवाज़", |
| 1709 | (Locale::Hi, Self::ReversibleOps) => "उत्क्रमणीय चरण", |
| 1710 | (Locale::Ru, Self::ScopedChanges) => "ограниченные изменения", |
| 1711 | (Locale::Ru, Self::UserVoice) => "голос пользователя", |
| 1712 | (Locale::Ru, Self::ReversibleOps) => "обратимые шаги", |
| 1713 | (Locale::Uk, Self::ScopedChanges) => "обмежені зміни", |
| 1714 | (Locale::Uk, Self::UserVoice) => "голос користувача", |
| 1715 | (Locale::Uk, Self::ReversibleOps) => "оборотні кроки", |
| 1716 | (_, Self::ScopedChanges) => "scoped changes", |
| 1717 | (_, Self::UserVoice) => "user voice", |
| 1718 | (_, Self::ReversibleOps) => "reversible steps", |
| 1719 | } |
| 1720 | } |
| 1721 | |
| 1722 | fn note(self, locale: Locale) -> &'static str { |
| 1723 | match (locale, self) { |
| 1724 | (Locale::Ja, Self::ScopedChanges) => { |
| 1725 | "自由原則:小さくレビューしやすい変更を優先し、明示要求がない限り無関係なリファクタを避ける。" |
| 1726 | } |
| 1727 | (Locale::Ja, Self::UserVoice) => { |
| 1728 | "自由原則:ユーザーの語調、ブランド、制約を保ち、好みを権限拡大として扱わない。" |
| 1729 | } |
| 1730 | (Locale::Ja, Self::ReversibleOps) => { |
| 1731 | "自由原則:影響の大きい操作の前に、可逆手順、チェックポイント、ロールバック説明を選ぶ。" |
| 1732 | } |
| 1733 | (Locale::ZhHans, Self::ScopedChanges) => { |
| 1734 | "自定义准则:优先采用小范围、可审查的改动;除非明确要求,不做无关重构。" |
| 1735 | } |
| 1736 | (Locale::ZhHans, Self::UserVoice) => { |
| 1737 | "自定义准则:保留用户的语气、品牌和约束;不把偏好推断成权限扩大。" |
| 1738 | } |
| 1739 | (Locale::ZhHans, Self::ReversibleOps) => { |
| 1740 | "自定义准则:先选择可逆步骤、检查点和回滚说明,再进行高影响操作。" |
| 1741 | } |
| 1742 | (Locale::ZhHant, Self::ScopedChanges) => { |
| 1743 | "自由原則:優先採用小範圍、可審查的改動;除非明確要求,不做無關重構。" |
| 1744 | } |
| 1745 | (Locale::ZhHant, Self::UserVoice) => { |
| 1746 | "自由原則:保留使用者的語氣、品牌和約束;不把偏好推斷成權限擴大。" |
| 1747 | } |
| 1748 | (Locale::ZhHant, Self::ReversibleOps) => { |
| 1749 | "自由原則:先選擇可逆步驟、檢查點和回復說明,再進行高影響操作。" |
| 1750 | } |
| 1751 | (Locale::PtBr, Self::ScopedChanges) => { |
| 1752 | "Princípio livre: prefira mudanças pequenas e revisáveis; evite refactors não relacionados sem pedido explícito." |
| 1753 | } |
| 1754 | (Locale::PtBr, Self::UserVoice) => { |
| 1755 | "Princípio livre: preserve a voz, marca e restrições do usuário sem tratar preferências como expansão de permissão." |
| 1756 | } |
| 1757 | (Locale::PtBr, Self::ReversibleOps) => { |
| 1758 | "Princípio livre: favoreça passos reversíveis, checkpoints e notas de rollback antes de ações de alto impacto." |
| 1759 | } |
| 1760 | (Locale::Es419, Self::ScopedChanges) => { |
| 1761 | "Principio libre: prefiere cambios pequeños y revisables; evita refactors no relacionados sin pedido explícito." |
| 1762 | } |
| 1763 | (Locale::Es419, Self::UserVoice) => { |
| 1764 | "Principio libre: preserva la voz, marca y restricciones del usuario sin tratar preferencias como expansión de permisos." |
| 1765 | } |
| 1766 | (Locale::Es419, Self::ReversibleOps) => { |
| 1767 | "Principio libre: favorece pasos reversibles, checkpoints y notas de rollback antes de acciones de alto impacto." |
| 1768 | } |
| 1769 | (Locale::Vi, Self::ScopedChanges) => { |
| 1770 | "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õ." |
| 1771 | } |
| 1772 | (Locale::Vi, Self::UserVoice) => { |
| 1773 | "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." |
| 1774 | } |
| 1775 | (Locale::Vi, Self::ReversibleOps) => { |
| 1776 | "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." |
| 1777 | } |
| 1778 | (Locale::Ko, Self::ScopedChanges) => { |
| 1779 | "자유 원칙: 작고 리뷰하기 쉬운 변경을 우선하고, 명시적으로 요청받지 않는 한 관련 없는 리팩터링은 하지 않는다." |
| 1780 | } |
| 1781 | (Locale::Ko, Self::UserVoice) => { |
| 1782 | "자유 원칙: 사용자의 어조, 브랜드, 제약을 유지하고 선호를 권한 확대로 취급하지 않는다." |
| 1783 | } |
| 1784 | (Locale::Ko, Self::ReversibleOps) => { |
| 1785 | "자유 원칙: 영향이 큰 작업 전에 되돌릴 수 있는 단계, 체크포인트, 롤백 메모를 우선한다." |
| 1786 | } |
| 1787 | (Locale::Ca, Self::ScopedChanges) => { |
| 1788 | "Principi lliure: prefereix canvis petits i revisables i evita refactors no relacionats si no es demanen explícitament." |
| 1789 | } |
| 1790 | (Locale::Ca, Self::UserVoice) => { |
| 1791 | "Principi lliure: preserva la veu, la marca i les restriccions de l'usuari sense tractar les preferències com una ampliació de permisos." |
| 1792 | } |
| 1793 | (Locale::Ca, Self::ReversibleOps) => { |
| 1794 | "Principi lliure: priorita passos reversibles, punts de control i notes de marxa enrere abans d'operacions d'alt impacte." |
| 1795 | } |
| 1796 | (Locale::De, Self::ScopedChanges) => { |
| 1797 | "Freitext-Prinzip: Bevorzuge kleine, überprüfbare Änderungen und vermeide unzusammenhängende Refactorings, sofern nicht ausdrücklich gewünscht." |
| 1798 | } |
| 1799 | (Locale::De, Self::UserVoice) => { |
| 1800 | "Freitext-Prinzip: Bewahre Stimme, Marke und Vorgaben des Nutzers, ohne Präferenzen als Rechteausweitung zu behandeln." |
| 1801 | } |
| 1802 | (Locale::De, Self::ReversibleOps) => { |
| 1803 | "Freitext-Prinzip: Bevorzuge reversible Schritte, Checkpoints und Rollback-Notizen vor einschneidenden Operationen." |
| 1804 | } |
| 1805 | (Locale::Fr, Self::ScopedChanges) => { |
| 1806 | "Principe libre : préférez des changements petits et révisables et évitez les refactors sans rapport, sauf demande explicite." |
| 1807 | } |
| 1808 | (Locale::Fr, Self::UserVoice) => { |
| 1809 | "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." |
| 1810 | } |
| 1811 | (Locale::Fr, Self::ReversibleOps) => { |
| 1812 | "Principe libre : privilégiez étapes réversibles, points de contrôle et notes de rollback avant les opérations à fort impact." |
| 1813 | } |
| 1814 | (Locale::Id, Self::ScopedChanges) => { |
| 1815 | "Prinsip bebas: utamakan perubahan kecil yang mudah ditinjau dan hindari refactor tak terkait kecuali diminta secara eksplisit." |
| 1816 | } |
| 1817 | (Locale::Id, Self::UserVoice) => { |
| 1818 | "Prinsip bebas: jaga suara, merek, dan batasan pengguna tanpa memperlakukan preferensi sebagai perluasan izin." |
| 1819 | } |
| 1820 | (Locale::Id, Self::ReversibleOps) => { |
| 1821 | "Prinsip bebas: utamakan langkah reversibel, checkpoint, dan catatan rollback sebelum operasi berdampak besar." |
| 1822 | } |
| 1823 | (Locale::Hi, Self::ScopedChanges) => { |
| 1824 | "मुक्त-पाठ सिद्धांत: छोटे, समीक्षायोग्य बदलावों को प्राथमिकता दें और स्पष्ट अनुरोध के बिना असंबंधित रिफैक्टर से बचें।" |
| 1825 | } |
| 1826 | (Locale::Hi, Self::UserVoice) => { |
| 1827 | "मुक्त-पाठ सिद्धांत: उपयोगकर्ता की आवाज़, ब्रांड और बाधाएँ सुरक्षित रखें; प्राथमिकताओं को अनुमति-विस्तार न मानें।" |
| 1828 | } |
| 1829 | (Locale::Hi, Self::ReversibleOps) => { |
| 1830 | "मुक्त-पाठ सिद्धांत: उच्च-प्रभाव कार्यों से पहले उत्क्रमणीय चरणों, चेकपॉइंट और रोलबैक नोट्स को प्राथमिकता दें।" |
| 1831 | } |
| 1832 | (Locale::Ru, Self::ScopedChanges) => { |
| 1833 | "Свободный принцип: предпочитайте небольшие, проверяемые изменения и избегайте несвязанных рефакторингов без явного запроса." |
| 1834 | } |
| 1835 | (Locale::Ru, Self::UserVoice) => { |
| 1836 | "Свободный принцип: сохраняйте голос, бренд и ограничения пользователя, не трактуя предпочтения как расширение полномочий." |
| 1837 | } |
| 1838 | (Locale::Ru, Self::ReversibleOps) => { |
| 1839 | "Свободный принцип: отдавайте предпочтение обратимым шагам, контрольным точкам и заметкам об откате перед высокорисковыми операциями." |
| 1840 | } |
| 1841 | (Locale::Uk, Self::ScopedChanges) => { |
| 1842 | "Вільний принцип: надавайте перевагу невеликим, перевірюваним змінам і уникайте непов'язаних рефакторингів без явного запиту." |
| 1843 | } |
| 1844 | (Locale::Uk, Self::UserVoice) => { |
| 1845 | "Вільний принцип: зберігайте голос, бренд і обмеження користувача, не трактуючи вподобання як розширення повноважень." |
| 1846 | } |
| 1847 | (Locale::Uk, Self::ReversibleOps) => { |
| 1848 | "Вільний принцип: надавайте перевагу оборотним крокам, контрольним точкам і нотаткам про відкат перед високоризиковими операціями." |
| 1849 | } |
| 1850 | (_, Self::ScopedChanges) => { |
| 1851 | "Freeform principle: prefer small, reviewable changes and avoid unrelated refactors unless explicitly requested." |
| 1852 | } |
| 1853 | (_, Self::UserVoice) => { |
| 1854 | "Freeform principle: preserve the user's voice, brand, and constraints without treating preferences as permission expansion." |
| 1855 | } |
| 1856 | (_, Self::ReversibleOps) => { |
| 1857 | "Freeform principle: favor reversible steps, checkpoints, and rollback notes before high-impact operations." |
| 1858 | } |
| 1859 | } |
| 1860 | } |
| 1861 | } |
| 1862 | |
| 1863 | fn next_guided_autonomy(preference: AutonomyPreference) -> AutonomyPreference { |
| 1864 | match preference { |
| 1865 | AutonomyPreference::Unspecified | AutonomyPreference::Cautious => { |
| 1866 | AutonomyPreference::Balanced |
| 1867 | } |
| 1868 | AutonomyPreference::Balanced => AutonomyPreference::Autonomous, |
| 1869 | AutonomyPreference::Autonomous => AutonomyPreference::Cautious, |
| 1870 | } |
| 1871 | } |
| 1872 | |
| 1873 | fn autonomy_label(preference: AutonomyPreference, locale: Locale) -> &'static str { |
| 1874 | match (locale, preference) { |
| 1875 | (Locale::Ja, AutonomyPreference::Cautious) => "慎重", |
| 1876 | (Locale::Ja, AutonomyPreference::Balanced) => "バランス", |
| 1877 | (Locale::Ja, AutonomyPreference::Autonomous) => "積極的", |
| 1878 | (Locale::ZhHans, AutonomyPreference::Cautious) => "谨慎", |
| 1879 | (Locale::ZhHans, AutonomyPreference::Balanced) => "平衡", |
| 1880 | (Locale::ZhHans, AutonomyPreference::Autonomous) => "积极主动", |
| 1881 | (Locale::ZhHant, AutonomyPreference::Cautious) => "謹慎", |
| 1882 | (Locale::ZhHant, AutonomyPreference::Balanced) => "平衡", |
| 1883 | (Locale::ZhHant, AutonomyPreference::Autonomous) => "積極主動", |
| 1884 | (Locale::PtBr, AutonomyPreference::Cautious) => "cauteloso", |
| 1885 | (Locale::PtBr, AutonomyPreference::Balanced) => "equilibrado", |
| 1886 | (Locale::PtBr, AutonomyPreference::Autonomous) => "ambicioso", |
| 1887 | (Locale::Es419, AutonomyPreference::Cautious) => "cauteloso", |
| 1888 | (Locale::Es419, AutonomyPreference::Balanced) => "equilibrado", |
| 1889 | (Locale::Es419, AutonomyPreference::Autonomous) => "ambicioso", |
| 1890 | (Locale::Vi, AutonomyPreference::Cautious) => "thận trọng", |
| 1891 | (Locale::Vi, AutonomyPreference::Balanced) => "cân bằng", |
| 1892 | (Locale::Vi, AutonomyPreference::Autonomous) => "chủ động", |
| 1893 | (Locale::Ko, AutonomyPreference::Cautious) => "신중함", |
| 1894 | (Locale::Ko, AutonomyPreference::Balanced) => "균형", |
| 1895 | (Locale::Ko, AutonomyPreference::Autonomous) => "적극적", |
| 1896 | (Locale::Ca, AutonomyPreference::Cautious) => "cautelós", |
| 1897 | (Locale::Ca, AutonomyPreference::Balanced) => "equilibrat", |
| 1898 | (Locale::Ca, AutonomyPreference::Autonomous) => "ambiciós", |
| 1899 | (Locale::De, AutonomyPreference::Cautious) => "vorsichtig", |
| 1900 | (Locale::De, AutonomyPreference::Balanced) => "ausgewogen", |
| 1901 | (Locale::De, AutonomyPreference::Autonomous) => "ambitioniert", |
| 1902 | (Locale::Fr, AutonomyPreference::Cautious) => "prudent", |
| 1903 | (Locale::Fr, AutonomyPreference::Balanced) => "équilibré", |
| 1904 | (Locale::Fr, AutonomyPreference::Autonomous) => "ambitieux", |
| 1905 | (Locale::Id, AutonomyPreference::Cautious) => "hati-hati", |
| 1906 | (Locale::Id, AutonomyPreference::Balanced) => "seimbang", |
| 1907 | (Locale::Id, AutonomyPreference::Autonomous) => "ambisius", |
| 1908 | (Locale::Hi, AutonomyPreference::Cautious) => "सावधान", |
| 1909 | (Locale::Hi, AutonomyPreference::Balanced) => "संतुलित", |
| 1910 | (Locale::Hi, AutonomyPreference::Autonomous) => "महत्वाकांक्षी", |
| 1911 | (Locale::Ru, AutonomyPreference::Cautious) => "осторожный", |
| 1912 | (Locale::Ru, AutonomyPreference::Balanced) => "сбалансированный", |
| 1913 | (Locale::Ru, AutonomyPreference::Autonomous) => "самостоятельный", |
| 1914 | (Locale::Uk, AutonomyPreference::Cautious) => "обережний", |
| 1915 | (Locale::Uk, AutonomyPreference::Balanced) => "збалансований", |
| 1916 | (Locale::Uk, AutonomyPreference::Autonomous) => "самостійний", |
| 1917 | (_, AutonomyPreference::Cautious) => "cautious", |
| 1918 | (_, AutonomyPreference::Balanced) => "balanced", |
| 1919 | (_, AutonomyPreference::Autonomous) => "ambitious", |
| 1920 | (_, AutonomyPreference::Unspecified) => "unspecified", |
| 1921 | } |
| 1922 | } |
| 1923 | |
| 1924 | fn autonomy_priority(preference: AutonomyPreference, locale: Locale) -> &'static str { |
| 1925 | match (locale, preference) { |
| 1926 | (Locale::Ja, AutonomyPreference::Cautious) => { |
| 1927 | "ファイル編集、コマンド実行、あいまいな製品判断の前に停止して尋ねる。" |
| 1928 | } |
| 1929 | (Locale::Ja, AutonomyPreference::Balanced) => { |
| 1930 | "明確で低リスクな作業は直接進め、危険、破壊的、あいまいな操作では先に確認する。" |
| 1931 | } |
| 1932 | (Locale::Ja, AutonomyPreference::Autonomous) => { |
| 1933 | "安全な定型作業はまとめて進めるが、破壊的、認証情報、公開、高コスト、法務、セキュリティリスクでは停止して尋ねる。" |
| 1934 | } |
| 1935 | (Locale::ZhHans, AutonomyPreference::Cautious) => { |
| 1936 | "在编辑文件、运行命令或产品选择不明确前,倾向先停下询问。" |
| 1937 | } |
| 1938 | (Locale::ZhHans, AutonomyPreference::Balanced) => { |
| 1939 | "清晰低风险任务可直接行动;遇到风险、破坏性或歧义时先确认。" |
| 1940 | } |
| 1941 | (Locale::ZhHans, AutonomyPreference::Autonomous) => { |
| 1942 | "可批量处理安全的常规工作,但遇到破坏性、凭据、发布、高成本、法律或安全风险时停止询问。" |
| 1943 | } |
| 1944 | (Locale::ZhHant, AutonomyPreference::Cautious) => { |
| 1945 | "在編輯檔案、執行命令或產品選擇不明確前,傾向先停下詢問。" |
| 1946 | } |
| 1947 | (Locale::ZhHant, AutonomyPreference::Balanced) => { |
| 1948 | "清晰低風險任務可直接行動;遇到風險、破壞性或歧義時先確認。" |
| 1949 | } |
| 1950 | (Locale::ZhHant, AutonomyPreference::Autonomous) => { |
| 1951 | "可批量處理安全的常規工作,但遇到破壞性、憑據、發布、高成本、法律或安全風險時停止詢問。" |
| 1952 | } |
| 1953 | (Locale::PtBr, AutonomyPreference::Cautious) => { |
| 1954 | "Pare e pergunte antes de editar arquivos, rodar comandos ou escolher entre caminhos ambíguos de produto." |
| 1955 | } |
| 1956 | (Locale::PtBr, AutonomyPreference::Balanced) => { |
| 1957 | "Aja diretamente em tarefas claras e de baixo risco; confirme antes de ações arriscadas, destrutivas ou ambíguas." |
| 1958 | } |
| 1959 | (Locale::PtBr, AutonomyPreference::Autonomous) => { |
| 1960 | "Agrupe trabalho seguro de rotina, mas pare para ações destrutivas, credenciais, publicação, alto custo, legais ou de segurança." |
| 1961 | } |
| 1962 | (Locale::Es419, AutonomyPreference::Cautious) => { |
| 1963 | "Detente y pregunta antes de editar archivos, ejecutar comandos o elegir entre caminos ambiguos de producto." |
| 1964 | } |
| 1965 | (Locale::Es419, AutonomyPreference::Balanced) => { |
| 1966 | "Actúa directamente en tareas claras y de bajo riesgo; confirma antes de acciones riesgosas, destructivas o ambiguas." |
| 1967 | } |
| 1968 | (Locale::Es419, AutonomyPreference::Autonomous) => { |
| 1969 | "Agrupa trabajo seguro de rutina, pero detente ante acciones destructivas, credenciales, publicación, alto costo, legales o de seguridad." |
| 1970 | } |
| 1971 | (Locale::Vi, AutonomyPreference::Cautious) => { |
| 1972 | "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ồ." |
| 1973 | } |
| 1974 | (Locale::Vi, AutonomyPreference::Balanced) => { |
| 1975 | "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ồ." |
| 1976 | } |
| 1977 | (Locale::Vi, AutonomyPreference::Autonomous) => { |
| 1978 | "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." |
| 1979 | } |
| 1980 | (Locale::Ko, AutonomyPreference::Cautious) => { |
| 1981 | "파일 수정, 명령어 실행, 애매한 제품 선택 전에 멈추고 물어본다." |
| 1982 | } |
| 1983 | (Locale::Ko, AutonomyPreference::Balanced) => { |
| 1984 | "명확하고 위험이 낮은 작업은 바로 진행하고, 위험하거나 파괴적이거나 애매한 작업은 먼저 확인한다." |
| 1985 | } |
| 1986 | (Locale::Ko, AutonomyPreference::Autonomous) => { |
| 1987 | "안전한 정형 작업은 모아서 진행하되, 파괴적이거나 자격 증명, 게시, 고비용, 법적, 보안 위험이 있는 작업에서는 멈추고 물어본다." |
| 1988 | } |
| 1989 | (Locale::Ca, AutonomyPreference::Cautious) => { |
| 1990 | "Atura't i pregunta abans d'editar fitxers, executar ordres o triar entre camins de producte ambigus." |
| 1991 | } |
| 1992 | (Locale::Ca, AutonomyPreference::Balanced) => { |
| 1993 | "Actua directament en tasques clares i de baix risc; confirma abans d'accions arriscades, destructives o ambigües." |
| 1994 | } |
| 1995 | (Locale::Ca, AutonomyPreference::Autonomous) => { |
| 1996 | "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." |
| 1997 | } |
| 1998 | (Locale::De, AutonomyPreference::Cautious) => { |
| 1999 | "Halte an und frage, bevor du Dateien bearbeitest, Befehle ausführst oder zwischen mehrdeutigen Produktwegen wählst." |
| 2000 | } |
| 2001 | (Locale::De, AutonomyPreference::Balanced) => { |
| 2002 | "Handle direkt bei klaren, risikoarmen Aufgaben; bestätige vor riskanten, destruktiven oder mehrdeutigen Aktionen." |
| 2003 | } |
| 2004 | (Locale::De, AutonomyPreference::Autonomous) => { |
| 2005 | "Bündle sichere Routinearbeit, aber halte an bei destruktiven, zugangsdatenbezogenen, veröffentlichenden, kostspieligen, rechtlichen oder sicherheitskritischen Aktionen." |
| 2006 | } |
| 2007 | (Locale::Fr, AutonomyPreference::Cautious) => { |
| 2008 | "Arrêtez et demandez avant de modifier des fichiers, d'exécuter des commandes ou de choisir entre des voies produit ambiguës." |
| 2009 | } |
| 2010 | (Locale::Fr, AutonomyPreference::Balanced) => { |
| 2011 | "Agissez directement sur les tâches claires et à faible risque ; confirmez avant les actions risquées, destructives ou ambiguës." |
| 2012 | } |
| 2013 | (Locale::Fr, AutonomyPreference::Autonomous) => { |
| 2014 | "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é." |
| 2015 | } |
| 2016 | (Locale::Id, AutonomyPreference::Cautious) => { |
| 2017 | "Berhenti dan tanya sebelum mengedit file, menjalankan perintah, atau memilih di antara jalur produk yang ambigu." |
| 2018 | } |
| 2019 | (Locale::Id, AutonomyPreference::Balanced) => { |
| 2020 | "Bertindak langsung pada tugas yang jelas dan berisiko rendah; konfirmasi sebelum tindakan berisiko, destruktif, atau ambigu." |
| 2021 | } |
| 2022 | (Locale::Id, AutonomyPreference::Autonomous) => { |
| 2023 | "Kelompokkan pekerjaan rutin yang aman, tetapi berhenti untuk tindakan destruktif, terkait kredensial, publikasi, mahal, hukum, atau berisiko keamanan." |
| 2024 | } |
| 2025 | (Locale::Hi, AutonomyPreference::Cautious) => { |
| 2026 | "फ़ाइलें संपादित करने, कमांड चलाने या अस्पष्ट उत्पाद मार्गों में चुनने से पहले रुककर पूछें।" |
| 2027 | } |
| 2028 | (Locale::Hi, AutonomyPreference::Balanced) => { |
| 2029 | "स्पष्ट, कम-जोखिम वाले कार्यों पर सीधे कार्य करें; जोखिमपूर्ण, विनाशकारी या अस्पष्ट कार्यों से पहले पुष्टि करें।" |
| 2030 | } |
| 2031 | (Locale::Hi, AutonomyPreference::Autonomous) => { |
| 2032 | "सुरक्षित नियमित काम एक साथ करें, लेकिन विनाशकारी, क्रेडेंशियल, प्रकाशन, उच्च-लागत, कानूनी या सुरक्षा-जोखिम कार्यों पर रुककर पूछें।" |
| 2033 | } |
| 2034 | (Locale::Ru, AutonomyPreference::Cautious) => { |
| 2035 | "Остановитесь и спросите перед редактированием файлов, запуском команд или выбором между неоднозначными продуктовыми путями." |
| 2036 | } |
| 2037 | (Locale::Ru, AutonomyPreference::Balanced) => { |
| 2038 | "Действуйте напрямую в ясных низкорисковых задачах; подтверждайте перед рискованными, деструктивными или неоднозначными действиями." |
| 2039 | } |
| 2040 | (Locale::Ru, AutonomyPreference::Autonomous) => { |
| 2041 | "Группируйте безопасную рутинную работу, но останавливайтесь перед деструктивными действиями, действиями с учётными данными, публикациями, дорогими, юридическими или угрожающими безопасности операциями." |
| 2042 | } |
| 2043 | (Locale::Uk, AutonomyPreference::Cautious) => { |
| 2044 | "Зупиніться й запитайте перед редагуванням файлів, запуском команд або вибором між неоднозначними продуктовими шляхами." |
| 2045 | } |
| 2046 | (Locale::Uk, AutonomyPreference::Balanced) => { |
| 2047 | "Дійте безпосередньо в чітких низькоризикових завданнях; підтверджуйте перед ризикованими, руйнівними чи неоднозначними діями." |
| 2048 | } |
| 2049 | (Locale::Uk, AutonomyPreference::Autonomous) => { |
| 2050 | "Групуйте безпечну рутинну роботу, але зупиняйтеся перед руйнівними діями, діями з обліковими даними, публікаціями, дорогими, юридичними чи небезпечними для безпеки операціями." |
| 2051 | } |
| 2052 | (_, AutonomyPreference::Cautious) => { |
| 2053 | "Stop and ask before editing files, running commands, or choosing between ambiguous product paths." |
| 2054 | } |
| 2055 | (_, AutonomyPreference::Balanced) => { |
| 2056 | "Do clear, low-risk work; ask before risky, destructive, or unclear work." |
| 2057 | } |
| 2058 | (_, AutonomyPreference::Autonomous) => { |
| 2059 | "Batch routine safe work, then stop for destructive, credential, publishing, high-cost, legal, or security-risk actions." |
| 2060 | } |
| 2061 | (_, AutonomyPreference::Unspecified) => "No standing initiative preference was selected.", |
| 2062 | } |
| 2063 | } |
| 2064 | |
| 2065 | fn authority_priority(locale: Locale) -> &'static str { |
| 2066 | match locale { |
| 2067 | Locale::Ja => { |
| 2068 | "現在のユーザー要求とライブツール証拠は、メモリ、古い引き継ぎ、推測より優先される。" |
| 2069 | } |
| 2070 | Locale::ZhHans => "当前用户请求和实时工具证据优先于记忆、陈旧交接和猜测。", |
| 2071 | Locale::ZhHant => "目前使用者請求和即時工具證據優先於記憶、陳舊交接和猜測。", |
| 2072 | Locale::PtBr => { |
| 2073 | "Pedidos atuais do usuário e evidência viva das ferramentas superam memória, handoffs antigos e palpites." |
| 2074 | } |
| 2075 | Locale::Es419 => { |
| 2076 | "Las solicitudes actuales del usuario y la evidencia viva de herramientas superan memoria, handoffs viejos y suposiciones." |
| 2077 | } |
| 2078 | Locale::Vi => { |
| 2079 | "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." |
| 2080 | } |
| 2081 | Locale::Ko => { |
| 2082 | "현재 사용자 요청과 실시간 도구 근거는 메모리, 오래된 인계 자료, 추측보다 우선한다." |
| 2083 | } |
| 2084 | Locale::Ca => { |
| 2085 | "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." |
| 2086 | } |
| 2087 | Locale::De => { |
| 2088 | "Aktuelle Nutzeranfragen und Live-Werkzeugnachweise haben Vorrang vor Speicher, veralteten Übergaben und Vermutungen." |
| 2089 | } |
| 2090 | Locale::Fr => { |
| 2091 | "Les demandes actuelles de l'utilisateur et les preuves directes des outils priment sur la mémoire, les anciens transferts et les suppositions." |
| 2092 | } |
| 2093 | Locale::Id => { |
| 2094 | "Permintaan pengguna saat ini dan bukti langsung dari alat mengalahkan memori, handoff lama, dan tebakan." |
| 2095 | } |
| 2096 | Locale::Hi => "वर्तमान उपयोगकर्ता अनुरोध और लाइव टूल साक्ष्य मेमोरी, पुराने हैंडऑफ़ और अनुमानों से ऊपर हैं।", |
| 2097 | Locale::Ru => { |
| 2098 | "Текущие запросы пользователя и живые свидетельства инструментов важнее памяти, устаревших передаточных заметок и догадок." |
| 2099 | } |
| 2100 | Locale::Uk => { |
| 2101 | "Поточні запити користувача та живі свідчення інструментів важливіші за пам'ять, застарілі передаточні нотатки й здогадки." |
| 2102 | } |
| 2103 | _ => { |
| 2104 | "Current user requests and live tool evidence outrank memory, stale handoffs, and guesses." |
| 2105 | } |
| 2106 | } |
| 2107 | } |
| 2108 | |
| 2109 | fn bounded_freeform_note(input: &str, max_chars: usize) -> String { |
| 2110 | input |
| 2111 | .chars() |
| 2112 | .filter_map(|ch| { |
| 2113 | if ch == '\t' { |
| 2114 | Some(' ') |
| 2115 | } else if ch == '\n' || !ch.is_control() { |
| 2116 | Some(ch) |
| 2117 | } else { |
| 2118 | None |
| 2119 | } |
| 2120 | }) |
| 2121 | .take(max_chars) |
| 2122 | .collect::<String>() |
| 2123 | .trim() |
| 2124 | .to_string() |
| 2125 | } |
| 2126 | |
| 2127 | fn compact_freeform_preview(note: &str) -> String { |
| 2128 | let compact = note.split_whitespace().collect::<Vec<_>>().join(" "); |
| 2129 | let mut preview = compact.chars().take(96).collect::<String>(); |
| 2130 | if compact.chars().count() > 96 { |
| 2131 | preview.push_str("..."); |
| 2132 | } |
| 2133 | preview |
| 2134 | } |
| 2135 | |
| 2136 | fn freeform_note_line(locale: Locale, note: &str, editing: bool) -> Line<'static> { |
| 2137 | let preview = compact_freeform_preview(note); |
| 2138 | let text = match (locale, editing, preview.is_empty()) { |
| 2139 | (Locale::Ja, true, true) => { |
| 2140 | "F 自由原則:編集中 - 有界の原則を入力または貼り付け、Enter で完了".to_string() |
| 2141 | } |
| 2142 | (Locale::Ja, true, false) => format!("F 自由原則:編集中 - {preview}"), |
| 2143 | (Locale::Ja, false, true) => "F 自由原則:F で有界の原則を入力または貼り付け".to_string(), |
| 2144 | (Locale::Ja, false, false) => format!("F 自由原則:{preview}"), |
| 2145 | (Locale::ZhHans, true, true) => { |
| 2146 | "F 自定义准则:正在编辑 - 输入或粘贴明确的准则,Enter 完成".to_string() |
| 2147 | } |
| 2148 | (Locale::ZhHans, true, false) => format!("F 自定义准则:正在编辑 - {preview}"), |
| 2149 | (Locale::ZhHans, false, true) => { |
| 2150 | "F 自定义准则:按 F 输入或粘贴自己的明确准则".to_string() |
| 2151 | } |
| 2152 | (Locale::ZhHans, false, false) => format!("F 自定义准则:{preview}"), |
| 2153 | (Locale::ZhHant, true, true) => { |
| 2154 | "F 自由原則:正在編輯 - 輸入或貼上有界原則,Enter 完成".to_string() |
| 2155 | } |
| 2156 | (Locale::ZhHant, true, false) => format!("F 自由原則:正在編輯 - {preview}"), |
| 2157 | (Locale::ZhHant, false, true) => "F 自由原則:按 F 輸入或貼上自己的有界原則".to_string(), |
| 2158 | (Locale::ZhHant, false, false) => format!("F 自由原則:{preview}"), |
| 2159 | (Locale::PtBr, true, true) => { |
| 2160 | "F Princípio livre: editando - digite ou cole um princípio limitado, Enter para concluir".to_string() |
| 2161 | } |
| 2162 | (Locale::PtBr, true, false) => format!("F Princípio livre: editando - {preview}"), |
| 2163 | (Locale::PtBr, false, true) => { |
| 2164 | "F Princípio livre: pressione F para digitar ou colar um princípio limitado".to_string() |
| 2165 | } |
| 2166 | (Locale::PtBr, false, false) => format!("F Princípio livre: {preview}"), |
| 2167 | (Locale::Es419, true, true) => { |
| 2168 | "F Principio libre: editando - escribe o pega un principio acotado, Enter para terminar".to_string() |
| 2169 | } |
| 2170 | (Locale::Es419, true, false) => format!("F Principio libre: editando - {preview}"), |
| 2171 | (Locale::Es419, false, true) => { |
| 2172 | "F Principio libre: presiona F para escribir o pegar un principio acotado".to_string() |
| 2173 | } |
| 2174 | (Locale::Es419, false, false) => format!("F Principio libre: {preview}"), |
| 2175 | (Locale::Vi, true, true) => { |
| 2176 | "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() |
| 2177 | } |
| 2178 | (Locale::Vi, true, false) => format!("F Nguyên tắc tự do: đang sửa - {preview}"), |
| 2179 | (Locale::Vi, false, true) => { |
| 2180 | "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() |
| 2181 | } |
| 2182 | (Locale::Vi, false, false) => format!("F Nguyên tắc tự do: {preview}"), |
| 2183 | (Locale::Ko, true, true) => { |
| 2184 | "F 자유 원칙: 편집 중 - 제한된 원칙을 입력하거나 붙여넣고 Enter로 완료".to_string() |
| 2185 | } |
| 2186 | (Locale::Ko, true, false) => format!("F 자유 원칙: 편집 중 - {preview}"), |
| 2187 | (Locale::Ko, false, true) => "F 자유 원칙: F를 눌러 제한된 원칙을 입력하거나 붙여넣기".to_string(), |
| 2188 | (Locale::Ko, false, false) => format!("F 자유 원칙: {preview}"), |
| 2189 | (Locale::Ca, true, true) => { |
| 2190 | "F Paraules pròpies: editant - escriu o enganxa un principi acotat, Enter per acabar".to_string() |
| 2191 | } |
| 2192 | (Locale::Ca, true, false) => format!("F Paraules pròpies: editant - {preview}"), |
| 2193 | (Locale::Ca, false, true) => { |
| 2194 | "F Paraules pròpies: prem F per escriure o enganxar un principi acotat".to_string() |
| 2195 | } |
| 2196 | (Locale::Ca, false, false) => format!("F Paraules pròpies: {preview}"), |
| 2197 | (Locale::De, true, true) => { |
| 2198 | "F Eigene Worte: Bearbeitung - tippe oder füge ein begrenztes Prinzip ein, Enter zum Abschluss".to_string() |
| 2199 | } |
| 2200 | (Locale::De, true, false) => format!("F Eigene Worte: Bearbeitung - {preview}"), |
| 2201 | (Locale::De, false, true) => { |
| 2202 | "F Eigene Worte: F drücken, um ein begrenztes Prinzip zu tippen oder einzufügen".to_string() |
| 2203 | } |
| 2204 | (Locale::De, false, false) => format!("F Eigene Worte: {preview}"), |
| 2205 | (Locale::Fr, true, true) => { |
| 2206 | "F Vos mots : édition - tapez ou collez un principe borné, Entrée pour terminer".to_string() |
| 2207 | } |
| 2208 | (Locale::Fr, true, false) => format!("F Vos mots : édition - {preview}"), |
| 2209 | (Locale::Fr, false, true) => { |
| 2210 | "F Vos mots : appuyez sur F pour taper ou coller un principe borné".to_string() |
| 2211 | } |
| 2212 | (Locale::Fr, false, false) => format!("F Vos mots : {preview}"), |
| 2213 | (Locale::Id, true, true) => { |
| 2214 | "F Kata sendiri: mengedit - ketik atau tempel prinsip terbatas, Enter untuk selesai".to_string() |
| 2215 | } |
| 2216 | (Locale::Id, true, false) => format!("F Kata sendiri: mengedit - {preview}"), |
| 2217 | (Locale::Id, false, true) => { |
| 2218 | "F Kata sendiri: tekan F untuk mengetik atau menempel prinsip terbatas".to_string() |
| 2219 | } |
| 2220 | (Locale::Id, false, false) => format!("F Kata sendiri: {preview}"), |
| 2221 | (Locale::Hi, true, true) => { |
| 2222 | "F अपने शब्द: संपादन जारी - सीमित सिद्धांत टाइप या पेस्ट करें, Enter से समाप्त करें".to_string() |
| 2223 | } |
| 2224 | (Locale::Hi, true, false) => format!("F अपने शब्द: संपादन जारी - {preview}"), |
| 2225 | (Locale::Hi, false, true) => { |
| 2226 | "F अपने शब्द: सीमित सिद्धांत टाइप या पेस्ट करने के लिए F दबाएँ".to_string() |
| 2227 | } |
| 2228 | (Locale::Hi, false, false) => format!("F अपने शब्द: {preview}"), |
| 2229 | (Locale::Ru, true, true) => { |
| 2230 | "F Свои слова: редактирование - введите или вставьте ограниченный принцип, Enter для завершения".to_string() |
| 2231 | } |
| 2232 | (Locale::Ru, true, false) => format!("F Свои слова: редактирование - {preview}"), |
| 2233 | (Locale::Ru, false, true) => { |
| 2234 | "F Свои слова: нажмите F, чтобы ввести или вставить ограниченный принцип".to_string() |
| 2235 | } |
| 2236 | (Locale::Ru, false, false) => format!("F Свои слова: {preview}"), |
| 2237 | (Locale::Uk, true, true) => { |
| 2238 | "F Свої слова: редагування - введіть або вставте обмежений принцип, Enter для завершення".to_string() |
| 2239 | } |
| 2240 | (Locale::Uk, true, false) => format!("F Свої слова: редагування - {preview}"), |
| 2241 | (Locale::Uk, false, true) => { |
| 2242 | "F Свої слова: натисніть F, щоб ввести або вставити обмежений принцип".to_string() |
| 2243 | } |
| 2244 | (Locale::Uk, false, false) => format!("F Свої слова: {preview}"), |
| 2245 | (_, true, true) => { |
| 2246 | "F Own words: editing - type or paste a bounded principle, Enter to finish".to_string() |
| 2247 | } |
| 2248 | (_, true, false) => format!("F Own words: editing - {preview}"), |
| 2249 | (_, false, true) => "F Own words: press F to type or paste a bounded principle".to_string(), |
| 2250 | (_, false, false) => format!("F Own words: {preview}"), |
| 2251 | }; |
| 2252 | let style = if editing || !preview.is_empty() { |
| 2253 | Style::default().fg(palette::WHALE_HUMAN) |
| 2254 | } else { |
| 2255 | Style::default().fg(palette::TEXT_MUTED) |
| 2256 | }; |
| 2257 | Line::from(Span::styled(text, style)) |
| 2258 | } |
| 2259 | |
| 2260 | impl SetupWizardView { |
| 2261 | #[must_use] |
| 2262 | pub fn new_for_app(app: &App, config: &Config) -> Self { |
| 2263 | Self::new_with_facts( |
| 2264 | load_setup_state_for_app(app, config), |
| 2265 | app.ui_locale, |
| 2266 | SetupRuntimeFacts::from_app_config(app, config), |
| 2267 | ) |
| 2268 | } |
| 2269 | |
| 2270 | #[must_use] |
| 2271 | pub fn new_checkpoint_for_app(app: &App, config: &Config) -> Self { |
| 2272 | Self::new_checkpoint_with_facts( |
| 2273 | load_setup_state_for_app(app, config), |
| 2274 | app.ui_locale, |
| 2275 | SetupRuntimeFacts::from_app_config(app, config), |
| 2276 | ) |
| 2277 | } |
| 2278 | |
| 2279 | #[must_use] |
| 2280 | pub fn new_for_app_at(app: &App, config: &Config, step: SetupStep) -> Self { |
| 2281 | Self::new_at_with_facts( |
| 2282 | load_setup_state_for_app(app, config), |
| 2283 | app.ui_locale, |
| 2284 | step, |
| 2285 | SetupRuntimeFacts::from_app_config(app, config), |
| 2286 | ) |
| 2287 | } |
| 2288 | |
| 2289 | #[must_use] |
| 2290 | pub fn selected_step(&self) -> SetupStep { |
| 2291 | STEP_SPECS[self.selected].id() |
| 2292 | } |
| 2293 | |
| 2294 | fn selected_spec(&self) -> &'static dyn SetupWizardStep { |
| 2295 | &STEP_SPECS[self.selected] |
| 2296 | } |
| 2297 | |
| 2298 | fn new_with_facts(state: SetupState, locale: Locale, facts: SetupRuntimeFacts) -> Self { |
| 2299 | let selected = progressive_initial_step_index(&state, &facts); |
| 2300 | Self { |
| 2301 | state, |
| 2302 | selected, |
| 2303 | locale, |
| 2304 | progressive_guide: true, |
| 2305 | details_expanded: false, |
| 2306 | facts, |
| 2307 | guided_draft: GuidedConstitutionDraft::default(), |
| 2308 | constitution_advanced: false, |
| 2309 | freeform_note: String::new(), |
| 2310 | editing_freeform_note: false, |
| 2311 | guided_preview_seen: false, |
| 2312 | existing_preview_seen: false, |
| 2313 | model_draft: None, |
| 2314 | model_draft_label: None, |
| 2315 | runtime_preset: SetupRuntimePreset::default(), |
| 2316 | runtime_preset_preview_seen: false, |
| 2317 | body_scroll: 0, |
| 2318 | } |
| 2319 | } |
| 2320 | |
| 2321 | fn new_at_with_facts( |
| 2322 | state: SetupState, |
| 2323 | locale: Locale, |
| 2324 | step: SetupStep, |
| 2325 | facts: SetupRuntimeFacts, |
| 2326 | ) -> Self { |
| 2327 | Self { |
| 2328 | state, |
| 2329 | selected: visible_step_index(step), |
| 2330 | locale, |
| 2331 | progressive_guide: false, |
| 2332 | details_expanded: false, |
| 2333 | facts, |
| 2334 | guided_draft: GuidedConstitutionDraft::default(), |
| 2335 | constitution_advanced: false, |
| 2336 | freeform_note: String::new(), |
| 2337 | editing_freeform_note: false, |
| 2338 | guided_preview_seen: false, |
| 2339 | existing_preview_seen: false, |
| 2340 | model_draft: None, |
| 2341 | model_draft_label: None, |
| 2342 | runtime_preset: SetupRuntimePreset::default(), |
| 2343 | runtime_preset_preview_seen: false, |
| 2344 | body_scroll: 0, |
| 2345 | } |
| 2346 | } |
| 2347 | |
| 2348 | fn new_checkpoint_with_facts( |
| 2349 | state: SetupState, |
| 2350 | locale: Locale, |
| 2351 | facts: SetupRuntimeFacts, |
| 2352 | ) -> Self { |
| 2353 | Self::new_at_with_facts(state, locale, SetupStep::Constitution, facts) |
| 2354 | } |
| 2355 | |
| 2356 | fn surface_title(&self) -> String { |
| 2357 | tr(self.locale, MessageId::SetupWizardTitle).into_owned() |
| 2358 | } |
| 2359 | |
| 2360 | fn tools_relevant(&self) -> bool { |
| 2361 | self.facts.tools_mcp_needs_action || !self.facts.tools_mcp_result.contains("overall=off") |
| 2362 | } |
| 2363 | |
| 2364 | fn progressive_steps(&self) -> Vec<SetupStep> { |
| 2365 | let mut steps = vec![ |
| 2366 | SetupStep::ProviderModel, |
| 2367 | SetupStep::TrustSandbox, |
| 2368 | SetupStep::RemoteRuntime, |
| 2369 | ]; |
| 2370 | if self.tools_relevant() { |
| 2371 | steps.push(SetupStep::ToolsMcp); |
| 2372 | } |
| 2373 | steps.push(SetupStep::Verification); |
| 2374 | steps |
| 2375 | } |
| 2376 | |
| 2377 | fn move_next(&mut self) { |
| 2378 | if self.progressive_guide { |
| 2379 | let steps = self.progressive_steps(); |
| 2380 | let position = steps |
| 2381 | .iter() |
| 2382 | .position(|step| *step == self.selected_step()) |
| 2383 | .unwrap_or(0); |
| 2384 | let next = steps[(position + 1).min(steps.len().saturating_sub(1))]; |
| 2385 | self.selected = visible_step_index(next); |
| 2386 | } else { |
| 2387 | self.selected = (self.selected + 1).min(STEP_SPECS.len().saturating_sub(1)); |
| 2388 | } |
| 2389 | self.constitution_advanced = false; |
| 2390 | self.details_expanded = false; |
| 2391 | self.body_scroll = 0; |
| 2392 | } |
| 2393 | |
| 2394 | fn move_back(&mut self) { |
| 2395 | if self.progressive_guide { |
| 2396 | let steps = self.progressive_steps(); |
| 2397 | let position = steps |
| 2398 | .iter() |
| 2399 | .position(|step| *step == self.selected_step()) |
| 2400 | .unwrap_or(0); |
| 2401 | let previous = steps[position.saturating_sub(1)]; |
| 2402 | self.selected = visible_step_index(previous); |
| 2403 | } else { |
| 2404 | self.selected = self.selected.saturating_sub(1); |
| 2405 | } |
| 2406 | self.constitution_advanced = false; |
| 2407 | self.details_expanded = false; |
| 2408 | self.body_scroll = 0; |
| 2409 | } |
| 2410 | |
| 2411 | fn commit_selected_status( |
| 2412 | &mut self, |
| 2413 | status: StepStatus, |
| 2414 | message_id: MessageId, |
| 2415 | advance: bool, |
| 2416 | ) -> ViewAction { |
| 2417 | let spec = self.selected_spec(); |
| 2418 | let result = match status { |
| 2419 | StepStatus::Skipped => Some("skipped by user"), |
| 2420 | StepStatus::NeedsAction => Some("retry requested; needs action"), |
| 2421 | _ => None, |
| 2422 | }; |
| 2423 | let mut entry = StepEntry::new(status, spec.required(), CONSTITUTION_CHECKPOINT_VERSION); |
| 2424 | if let Some(result) = result { |
| 2425 | entry = entry.with_result(result); |
| 2426 | } |
| 2427 | let mut state = self.state.clone(); |
| 2428 | state.set_step(spec.id(), entry); |
| 2429 | if spec.id() == SetupStep::Constitution && status == StepStatus::Skipped { |
| 2430 | // `S` is a durable response to the versioned checkpoint, just like |
| 2431 | // choosing the explicit defer action. It only skips this setup |
| 2432 | // checkpoint, though; it must not replace an already active |
| 2433 | // bundled or custom Constitution choice. A fresh state has no |
| 2434 | // active choice, so keep the bundled floor by recording Deferred. |
| 2435 | let choice = if state.constitution_choice.is_explicit() { |
| 2436 | state.constitution_choice |
| 2437 | } else { |
| 2438 | ConstitutionChoice::Deferred |
| 2439 | }; |
| 2440 | state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice); |
| 2441 | } |
| 2442 | self.state = state.clone(); |
| 2443 | if advance { |
| 2444 | self.move_next(); |
| 2445 | } |
| 2446 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2447 | state, |
| 2448 | message: tr(self.locale, message_id).to_string(), |
| 2449 | }) |
| 2450 | } |
| 2451 | |
| 2452 | fn commit_language_review(&mut self) -> ViewAction { |
| 2453 | let mut state = self.state.clone(); |
| 2454 | state.constitution_language = Some(self.locale.tag().to_string()); |
| 2455 | state.set_step( |
| 2456 | SetupStep::Language, |
| 2457 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2458 | .with_result(format!("setup locale {}", self.locale.tag())), |
| 2459 | ); |
| 2460 | self.state = state.clone(); |
| 2461 | self.move_next(); |
| 2462 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2463 | state, |
| 2464 | message: tr(self.locale, MessageId::SetupLanguageReviewed).to_string(), |
| 2465 | }) |
| 2466 | } |
| 2467 | |
| 2468 | fn commit_provider_model_review(&mut self) -> ViewAction { |
| 2469 | let status = provider::step_status(self.facts.provider_ready); |
| 2470 | let mut state = self.state.clone(); |
| 2471 | state.set_step( |
| 2472 | SetupStep::ProviderModel, |
| 2473 | provider::step_entry( |
| 2474 | self.facts.provider_ready, |
| 2475 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2476 | self.facts.provider_result.clone(), |
| 2477 | ), |
| 2478 | ); |
| 2479 | self.state = state.clone(); |
| 2480 | self.move_next(); |
| 2481 | let message_id = if status == StepStatus::Verified { |
| 2482 | MessageId::SetupProviderModelReviewed |
| 2483 | } else { |
| 2484 | MessageId::SetupProviderModelNeedsActionSaved |
| 2485 | }; |
| 2486 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2487 | state, |
| 2488 | message: tr(self.locale, message_id).to_string(), |
| 2489 | }) |
| 2490 | } |
| 2491 | |
| 2492 | fn commit_runtime_posture_review(&mut self) -> ViewAction { |
| 2493 | let mut state = self.state.clone(); |
| 2494 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 2495 | state.set_step( |
| 2496 | SetupStep::TrustSandbox, |
| 2497 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2498 | .with_result(self.facts.runtime_result.clone()), |
| 2499 | ); |
| 2500 | self.state = state.clone(); |
| 2501 | self.move_next(); |
| 2502 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2503 | state, |
| 2504 | message: tr(self.locale, MessageId::SetupRuntimePostureReviewed).to_string(), |
| 2505 | }) |
| 2506 | } |
| 2507 | |
| 2508 | fn operate_fleet_facts_ready(&self) -> bool { |
| 2509 | // Provider, capacity, and roster facts are configuration snapshots, |
| 2510 | // not proof of dispatch and terminal receipts. This release must never |
| 2511 | // persist an Operate-ready claim from those facts alone. |
| 2512 | false |
| 2513 | } |
| 2514 | |
| 2515 | fn commit_operate_fleet_review(&mut self) -> ViewAction { |
| 2516 | let status = if self.operate_fleet_facts_ready() { |
| 2517 | StepStatus::Verified |
| 2518 | } else { |
| 2519 | StepStatus::NeedsAction |
| 2520 | }; |
| 2521 | let mut state = self.state.clone(); |
| 2522 | state.set_step( |
| 2523 | SetupStep::OperateFleet, |
| 2524 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2525 | .with_result(self.facts.operate_result.clone()), |
| 2526 | ); |
| 2527 | self.state = state.clone(); |
| 2528 | self.move_next(); |
| 2529 | let message_id = if status == StepStatus::Verified { |
| 2530 | MessageId::SetupOperateReviewed |
| 2531 | } else { |
| 2532 | MessageId::SetupOperateNeedsActionSaved |
| 2533 | }; |
| 2534 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2535 | state, |
| 2536 | message: tr(self.locale, message_id).to_string(), |
| 2537 | }) |
| 2538 | } |
| 2539 | |
| 2540 | fn commit_hotbar_review(&mut self) -> ViewAction { |
| 2541 | let mut state = self.state.clone(); |
| 2542 | state.set_step( |
| 2543 | SetupStep::Hotbar, |
| 2544 | StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2545 | .with_result(self.facts.hotbar_result.clone()), |
| 2546 | ); |
| 2547 | self.state = state.clone(); |
| 2548 | self.move_next(); |
| 2549 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2550 | state, |
| 2551 | message: tr(self.locale, MessageId::SetupHotbarReviewed).to_string(), |
| 2552 | }) |
| 2553 | } |
| 2554 | |
| 2555 | fn commit_tools_mcp_review(&mut self) -> ViewAction { |
| 2556 | // Optional step: empty/off inventories settle as Optional; broken |
| 2557 | // configured tools record NeedsAction without blocking first-run. |
| 2558 | let status = if self.facts.tools_mcp_needs_action { |
| 2559 | StepStatus::NeedsAction |
| 2560 | } else if self.facts.tools_mcp_result.contains("overall=off") { |
| 2561 | StepStatus::Optional |
| 2562 | } else { |
| 2563 | StepStatus::Verified |
| 2564 | }; |
| 2565 | let mut state = self.state.clone(); |
| 2566 | state.set_step( |
| 2567 | SetupStep::ToolsMcp, |
| 2568 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2569 | .with_result(self.facts.tools_mcp_result.clone()), |
| 2570 | ); |
| 2571 | self.state = state.clone(); |
| 2572 | self.move_next(); |
| 2573 | let message_id = if status == StepStatus::NeedsAction { |
| 2574 | MessageId::SetupToolsMcpNeedsActionSaved |
| 2575 | } else { |
| 2576 | MessageId::SetupToolsMcpReviewed |
| 2577 | }; |
| 2578 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2579 | state, |
| 2580 | message: tr(self.locale, message_id).to_string(), |
| 2581 | }) |
| 2582 | } |
| 2583 | |
| 2584 | fn preview_tools_mcp_on_ramp(&self) -> ViewAction { |
| 2585 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2586 | title: tr(self.locale, MessageId::SetupToolsMcpPreviewTitle).to_string(), |
| 2587 | content: tools_mcp_on_ramp_text(self.locale, &self.facts), |
| 2588 | }) |
| 2589 | } |
| 2590 | |
| 2591 | fn preview_remote_runtime_on_ramp(&self) -> ViewAction { |
| 2592 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2593 | title: tr(self.locale, MessageId::SetupRemotePreviewTitle).to_string(), |
| 2594 | content: remote_runtime_on_ramp_text(self.locale, &self.facts), |
| 2595 | }) |
| 2596 | } |
| 2597 | |
| 2598 | /// Record the remote step honestly (#3409). |
| 2599 | /// |
| 2600 | /// Local-only always works, so Enter alone settles the step — a user who |
| 2601 | /// never wants remote access is finished in one key. When a *reachable* |
| 2602 | /// mode is missing a token or config the entry is `NeedsAction`, which the |
| 2603 | /// setup report and doctor inherit verbatim and which never blocks ready. |
| 2604 | fn commit_remote_runtime_review(&mut self) -> ViewAction { |
| 2605 | let mut state = self.state.clone(); |
| 2606 | let status = if self.facts.remote_needs_action { |
| 2607 | StepStatus::NeedsAction |
| 2608 | } else { |
| 2609 | StepStatus::Verified |
| 2610 | }; |
| 2611 | state.set_step( |
| 2612 | SetupStep::RemoteRuntime, |
| 2613 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2614 | .with_result(self.facts.remote_result.clone()), |
| 2615 | ); |
| 2616 | self.state = state.clone(); |
| 2617 | self.move_next(); |
| 2618 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2619 | state, |
| 2620 | message: tr(self.locale, MessageId::SetupRemoteReviewed).to_string(), |
| 2621 | }) |
| 2622 | } |
| 2623 | |
| 2624 | fn commit_persistence_review(&mut self) -> ViewAction { |
| 2625 | let mut state = self.state.clone(); |
| 2626 | state.set_step( |
| 2627 | SetupStep::Persistence, |
| 2628 | StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2629 | .with_result(self.facts.persistence.result.clone()), |
| 2630 | ); |
| 2631 | self.state = state.clone(); |
| 2632 | self.move_next(); |
| 2633 | ViewAction::Emit(ViewEvent::SetupStateCommitRequested { |
| 2634 | state, |
| 2635 | message: tr(self.locale, MessageId::SetupPersistenceReviewed).to_string(), |
| 2636 | }) |
| 2637 | } |
| 2638 | |
| 2639 | fn select_runtime_preset(&mut self, key: char) -> ViewAction { |
| 2640 | if let Some(preset) = SetupRuntimePreset::from_key(key) |
| 2641 | && preset != self.runtime_preset |
| 2642 | { |
| 2643 | self.runtime_preset = preset; |
| 2644 | self.runtime_preset_preview_seen = false; |
| 2645 | } |
| 2646 | ViewAction::None |
| 2647 | } |
| 2648 | |
| 2649 | fn preview_runtime_preset(&mut self) -> ViewAction { |
| 2650 | self.runtime_preset_preview_seen = true; |
| 2651 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2652 | title: tr(self.locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(), |
| 2653 | content: runtime_preset_preview_text(self.locale, self.runtime_preset, &self.facts), |
| 2654 | }) |
| 2655 | } |
| 2656 | |
| 2657 | fn commit_runtime_preset(&mut self) -> ViewAction { |
| 2658 | if !self.runtime_preset_preview_seen { |
| 2659 | return self.preview_runtime_preset(); |
| 2660 | } |
| 2661 | |
| 2662 | let mut state = self.state.clone(); |
| 2663 | state.runtime_posture_source = RuntimePostureSource::Confirmed; |
| 2664 | state.set_step( |
| 2665 | SetupStep::TrustSandbox, |
| 2666 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2667 | .with_result(self.runtime_preset.result_summary()), |
| 2668 | ); |
| 2669 | self.state = state.clone(); |
| 2670 | self.move_next(); |
| 2671 | ViewAction::Emit(ViewEvent::SetupRuntimePresetApplyRequested { |
| 2672 | preset: self.runtime_preset, |
| 2673 | state, |
| 2674 | message: tr(self.locale, MessageId::SetupRuntimePresetApplied).to_string(), |
| 2675 | }) |
| 2676 | } |
| 2677 | |
| 2678 | fn commit_setup_report(&mut self) -> ViewAction { |
| 2679 | let mut state = self.state.clone(); |
| 2680 | let status = if setup_report_ready(&state) { |
| 2681 | StepStatus::Verified |
| 2682 | } else { |
| 2683 | StepStatus::NeedsAction |
| 2684 | }; |
| 2685 | state.set_step( |
| 2686 | SetupStep::Verification, |
| 2687 | StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION) |
| 2688 | .with_result(setup_report_result(&state, &self.facts)), |
| 2689 | ); |
| 2690 | self.state = state.clone(); |
| 2691 | let event = ViewEvent::SetupStateCommitRequested { |
| 2692 | state, |
| 2693 | message: tr(self.locale, MessageId::SetupReportRecorded).to_string(), |
| 2694 | }; |
| 2695 | if self.progressive_guide { |
| 2696 | ViewAction::EmitAndClose(event) |
| 2697 | } else { |
| 2698 | ViewAction::Emit(event) |
| 2699 | } |
| 2700 | } |
| 2701 | |
| 2702 | fn open_constitution_advanced(&mut self) -> ViewAction { |
| 2703 | self.constitution_advanced = true; |
| 2704 | self.body_scroll = 0; |
| 2705 | ViewAction::None |
| 2706 | } |
| 2707 | |
| 2708 | fn close_constitution_advanced(&mut self) -> ViewAction { |
| 2709 | self.constitution_advanced = false; |
| 2710 | self.editing_freeform_note = false; |
| 2711 | self.body_scroll = 0; |
| 2712 | ViewAction::None |
| 2713 | } |
| 2714 | |
| 2715 | /// The first-run path is intentionally one decision: how much initiative |
| 2716 | /// Codewhale should take. This saves guidance only. Runtime approval, |
| 2717 | /// sandbox, shell, network, trust, and MCP policy remain untouched. |
| 2718 | fn commit_simple_constitution(&mut self) -> ViewAction { |
| 2719 | match self.facts.constitution_file { |
| 2720 | // Existing law is the safest default on an update checkpoint. |
| 2721 | // Keep it byte-for-byte and only advance setup state. |
| 2722 | SetupConstitutionFileState::Loaded => { |
| 2723 | return self.commit_existing_constitution_unchanged(); |
| 2724 | } |
| 2725 | // Do not overwrite a file the user attempted to provide when it |
| 2726 | // cannot be parsed or read. The bundled floor remains active and |
| 2727 | // Advanced exposes the explicit repair/regenerate choices. |
| 2728 | SetupConstitutionFileState::Empty |
| 2729 | | SetupConstitutionFileState::Invalid |
| 2730 | | SetupConstitutionFileState::Unreadable |
| 2731 | | SetupConstitutionFileState::PathError => { |
| 2732 | return self.commit_constitution(SetupCommitKind::BundledConstitution); |
| 2733 | } |
| 2734 | SetupConstitutionFileState::NotChecked | SetupConstitutionFileState::Missing => {} |
| 2735 | } |
| 2736 | |
| 2737 | // The compiled constitution already embodies the balanced posture: |
| 2738 | // act on clear reversible work, ask when ambiguity is costly, and |
| 2739 | // require express authorization for irreversible or external effects. |
| 2740 | // Accepting the recommendation therefore records Bundled rather than |
| 2741 | // pinning a generated user-global fork that would miss future bundled |
| 2742 | // law improvements. Only Customize writes a GuidedCustom file. |
| 2743 | self.commit_constitution(SetupCommitKind::BundledConstitution) |
| 2744 | } |
| 2745 | |
| 2746 | fn commit_custom_constitution( |
| 2747 | &mut self, |
| 2748 | constitution: UserConstitution, |
| 2749 | authoring: ConstitutionAuthoring, |
| 2750 | result_prefix: &str, |
| 2751 | ) -> ViewAction { |
| 2752 | let mut state = self.state.clone(); |
| 2753 | state.complete_constitution_checkpoint( |
| 2754 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2755 | ConstitutionChoice::GuidedCustom, |
| 2756 | ); |
| 2757 | state.constitution_language = constitution.language.clone(); |
| 2758 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 2759 | state.constitution_validity = ConstitutionValidity::Valid; |
| 2760 | state.constitution_authoring = Some(authoring); |
| 2761 | state.constitution_preview_hash = Some(constitution.preview_hash()); |
| 2762 | state.constitution_preview_version = |
| 2763 | state.constitution_preview_version.saturating_add(1).max(1); |
| 2764 | let hash = state |
| 2765 | .constitution_preview_hash |
| 2766 | .as_deref() |
| 2767 | .unwrap_or("unknown"); |
| 2768 | state.set_step( |
| 2769 | SetupStep::Constitution, |
| 2770 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2771 | .with_result(format!("{result_prefix} preview_hash={hash}")), |
| 2772 | ); |
| 2773 | self.state = state.clone(); |
| 2774 | ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { |
| 2775 | constitution, |
| 2776 | state, |
| 2777 | message: tr(self.locale, MessageId::SetupCheckpointDoneGuided).to_string(), |
| 2778 | }) |
| 2779 | } |
| 2780 | |
| 2781 | fn commit_guided_constitution(&mut self) -> ViewAction { |
| 2782 | if !self.guided_preview_seen { |
| 2783 | return self.preview_guided_constitution(); |
| 2784 | } |
| 2785 | |
| 2786 | let (constitution, authoring) = match self.model_draft.as_deref() { |
| 2787 | // Model drafts arrive sanitized + bounded from the untrusted-JSON |
| 2788 | // gate; ratify exactly what was previewed. |
| 2789 | Some(draft) => (draft.clone(), ConstitutionAuthoring::ModelDrafted), |
| 2790 | None => ( |
| 2791 | self.guided_draft |
| 2792 | .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()), |
| 2793 | ConstitutionAuthoring::Guided, |
| 2794 | ), |
| 2795 | }; |
| 2796 | let result_prefix = match authoring { |
| 2797 | ConstitutionAuthoring::ModelDrafted => format!( |
| 2798 | "model-drafted constitution ratified ({})", |
| 2799 | self.model_draft_label.as_deref().unwrap_or("model") |
| 2800 | ), |
| 2801 | ConstitutionAuthoring::Guided => "guided custom constitution".to_string(), |
| 2802 | }; |
| 2803 | self.commit_custom_constitution(constitution, authoring, &result_prefix) |
| 2804 | } |
| 2805 | |
| 2806 | fn preview_guided_constitution(&mut self) -> ViewAction { |
| 2807 | self.guided_preview_seen = true; |
| 2808 | let (constitution, provenance) = match self.model_draft.as_deref() { |
| 2809 | Some(draft) => ( |
| 2810 | draft.clone(), |
| 2811 | DraftProvenance::Model( |
| 2812 | self.model_draft_label |
| 2813 | .clone() |
| 2814 | .unwrap_or_else(|| "model".to_string()), |
| 2815 | ), |
| 2816 | ), |
| 2817 | None => ( |
| 2818 | self.guided_draft |
| 2819 | .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()), |
| 2820 | DraftProvenance::Guided, |
| 2821 | ), |
| 2822 | }; |
| 2823 | ViewAction::Emit(ViewEvent::OpenTextPager { |
| 2824 | title: ratification_preview_title(self.locale).to_string(), |
| 2825 | content: constitution_ratification_text(self.locale, &constitution, &provenance), |
| 2826 | }) |
| 2827 | } |
| 2828 | |
| 2829 | fn cycle_guided_answer(&mut self, key: char) -> ViewAction { |
| 2830 | if self.guided_draft.cycle(key) { |
| 2831 | self.guided_preview_seen = false; |
| 2832 | // Answers changed under the draft: the model draft is stale law |
| 2833 | // and must be re-drafted or replaced by the guided rendering. |
| 2834 | self.model_draft = None; |
| 2835 | self.model_draft_label = None; |
| 2836 | } |
| 2837 | ViewAction::None |
| 2838 | } |
| 2839 | |
| 2840 | /// `A` on the constitution step: ask the first configured model to draft. |
| 2841 | /// Requires a ready provider route; otherwise the key is inert and the |
| 2842 | /// deterministic guided flow stands untouched. |
| 2843 | fn request_model_draft(&self) -> ViewAction { |
| 2844 | if !self.facts.provider_ready { |
| 2845 | return ViewAction::None; |
| 2846 | } |
| 2847 | ViewAction::Emit(ViewEvent::SetupConstitutionModelDraftRequested { |
| 2848 | draft: self.guided_draft, |
| 2849 | freeform_note: self.freeform_note_for_draft().map(str::to_string), |
| 2850 | locale: self.locale, |
| 2851 | }) |
| 2852 | } |
| 2853 | |
| 2854 | fn toggle_freeform_edit(&mut self) -> ViewAction { |
| 2855 | if self.selected_step() == SetupStep::Constitution { |
| 2856 | self.editing_freeform_note = !self.editing_freeform_note; |
| 2857 | } |
| 2858 | ViewAction::None |
| 2859 | } |
| 2860 | |
| 2861 | fn freeform_note_for_draft(&self) -> Option<&str> { |
| 2862 | let note = self.freeform_note.trim(); |
| 2863 | (!note.is_empty()).then_some(note) |
| 2864 | } |
| 2865 | |
| 2866 | fn append_freeform_note_text(&mut self, text: &str) { |
| 2867 | let mut next = self.freeform_note.clone(); |
| 2868 | next.push_str(text); |
| 2869 | self.freeform_note = bounded_freeform_note(&next, MAX_NOTES_LEN); |
| 2870 | self.guided_preview_seen = false; |
| 2871 | self.model_draft = None; |
| 2872 | self.model_draft_label = None; |
| 2873 | } |
| 2874 | |
| 2875 | fn handle_freeform_note_key(&mut self, key: KeyEvent) -> Option<ViewAction> { |
| 2876 | if self.selected_step() != SetupStep::Constitution || !self.editing_freeform_note { |
| 2877 | return None; |
| 2878 | } |
| 2879 | match key.code { |
| 2880 | KeyCode::Esc | KeyCode::Enter => { |
| 2881 | self.editing_freeform_note = false; |
| 2882 | Some(ViewAction::None) |
| 2883 | } |
| 2884 | KeyCode::Backspace => { |
| 2885 | self.freeform_note.pop(); |
| 2886 | self.guided_preview_seen = false; |
| 2887 | self.model_draft = None; |
| 2888 | self.model_draft_label = None; |
| 2889 | Some(ViewAction::None) |
| 2890 | } |
| 2891 | KeyCode::Char(c) if key.modifiers.is_empty() => { |
| 2892 | let mut buf = [0; 4]; |
| 2893 | self.append_freeform_note_text(c.encode_utf8(&mut buf)); |
| 2894 | Some(ViewAction::None) |
| 2895 | } |
| 2896 | _ => Some(ViewAction::None), |
| 2897 | } |
| 2898 | } |
| 2899 | |
| 2900 | /// Install a model-drafted constitution (already sanitized + bounded by |
| 2901 | /// the untrusted-JSON gate) and return the `(title, content)` of the |
| 2902 | /// ratification preview the host must open in the same breath — that is |
| 2903 | /// what satisfies the preview gate. Ratifying still takes the explicit |
| 2904 | /// `G` keypress afterwards. |
| 2905 | #[must_use] |
| 2906 | pub(crate) fn install_model_draft( |
| 2907 | &mut self, |
| 2908 | constitution: Box<UserConstitution>, |
| 2909 | model_label: String, |
| 2910 | ) -> (String, String) { |
| 2911 | let content = constitution_ratification_text( |
| 2912 | self.locale, |
| 2913 | &constitution, |
| 2914 | &DraftProvenance::Model(model_label.clone()), |
| 2915 | ); |
| 2916 | self.model_draft = Some(constitution); |
| 2917 | self.model_draft_label = Some(model_label); |
| 2918 | self.guided_preview_seen = true; |
| 2919 | (ratification_preview_title(self.locale).to_string(), content) |
| 2920 | } |
| 2921 | |
| 2922 | fn commit_constitution(&self, kind: SetupCommitKind) -> ViewAction { |
| 2923 | let choice = match kind { |
| 2924 | SetupCommitKind::BundledConstitution => ConstitutionChoice::Bundled, |
| 2925 | SetupCommitKind::DeferredConstitution => ConstitutionChoice::Deferred, |
| 2926 | }; |
| 2927 | let mut state = self.state.clone(); |
| 2928 | state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice); |
| 2929 | state.constitution_source = ConstitutionSource::Bundled; |
| 2930 | state.constitution_validity = ConstitutionValidity::Unknown; |
| 2931 | state.constitution_authoring = None; |
| 2932 | state.constitution_preview_hash = None; |
| 2933 | state.set_step( |
| 2934 | SetupStep::Constitution, |
| 2935 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2936 | .with_result(match kind { |
| 2937 | SetupCommitKind::BundledConstitution => "bundled/default constitution", |
| 2938 | SetupCommitKind::DeferredConstitution => "checkpoint deferred; bundled applies", |
| 2939 | }), |
| 2940 | ); |
| 2941 | let message_id = match kind { |
| 2942 | SetupCommitKind::BundledConstitution => MessageId::SetupCheckpointDoneBundled, |
| 2943 | SetupCommitKind::DeferredConstitution => MessageId::SetupCheckpointDeferred, |
| 2944 | }; |
| 2945 | ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { |
| 2946 | state, |
| 2947 | message: tr(self.locale, message_id).to_string(), |
| 2948 | }) |
| 2949 | } |
| 2950 | |
| 2951 | fn load_existing_constitution(&self) -> Option<UserConstitution> { |
| 2952 | if self.facts.constitution_file != SetupConstitutionFileState::Loaded { |
| 2953 | return None; |
| 2954 | } |
| 2955 | // Re-read the live file so a stale card cannot ratify a file that |
| 2956 | // has since become invalid; any non-loaded state leaves the key inert. |
| 2957 | UserConstitution::load().ok()?.constitution().cloned() |
| 2958 | } |
| 2959 | |
| 2960 | fn commit_existing_constitution_unchanged(&mut self) -> ViewAction { |
| 2961 | let Some(constitution) = self.load_existing_constitution() else { |
| 2962 | return ViewAction::None; |
| 2963 | }; |
| 2964 | let mut state = self.state.clone(); |
| 2965 | state.complete_constitution_checkpoint( |
| 2966 | CONSTITUTION_CHECKPOINT_VERSION, |
| 2967 | ConstitutionChoice::GuidedCustom, |
| 2968 | ); |
| 2969 | state.constitution_source = ConstitutionSource::UserGlobal; |
| 2970 | state.constitution_validity = ConstitutionValidity::Valid; |
| 2971 | state.constitution_preview_hash = Some(constitution.preview_hash()); |
| 2972 | state.set_step( |
| 2973 | SetupStep::Constitution, |
| 2974 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 2975 | .with_result("existing constitution kept unchanged"), |
| 2976 | ); |
| 2977 | self.state = state.clone(); |
| 2978 | ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { |
| 2979 | state, |
| 2980 | message: tr(self.locale, MessageId::SetupCheckpointDoneKept).to_string(), |
| 2981 | }) |
| 2982 | } |
| 2983 | |
| 2984 | /// Complete the checkpoint by keeping the existing valid |
| 2985 | /// `constitution.json` exactly as it stands (#3794). First `K` previews |
| 2986 | /// the rendered law; second `K` records the choice. The file is never |
| 2987 | /// rewritten — only `setup_state.json` changes, through the same commit |
| 2988 | /// event as every other completion. |
| 2989 | fn commit_keep_existing_constitution(&mut self) -> ViewAction { |
| 2990 | let Some(constitution) = self.load_existing_constitution() else { |
| 2991 | return ViewAction::None; |
| 2992 | }; |
| 2993 | if !self.existing_preview_seen { |
| 2994 | self.existing_preview_seen = true; |
| 2995 | let content = constitution_ratification_text( |
| 2996 | self.locale, |
| 2997 | &constitution, |
| 2998 | &DraftProvenance::Existing, |
| 2999 | ); |
| 3000 | return ViewAction::Emit(ViewEvent::OpenTextPager { |
| 3001 | title: ratification_preview_title(self.locale).to_string(), |
| 3002 | content, |
| 3003 | }); |
| 3004 | } |
| 3005 | self.commit_existing_constitution_unchanged() |
| 3006 | } |
| 3007 | |
| 3008 | fn status_label(&self, status: StepStatus) -> Cow<'static, str> { |
| 3009 | tr( |
| 3010 | self.locale, |
| 3011 | match status { |
| 3012 | StepStatus::NotStarted => MessageId::SetupStatusNotStarted, |
| 3013 | StepStatus::Recommended => MessageId::SetupStatusRecommended, |
| 3014 | StepStatus::Optional => MessageId::SetupStatusOptional, |
| 3015 | StepStatus::Deferred => MessageId::SetupStatusDeferred, |
| 3016 | StepStatus::InProgress => MessageId::SetupStatusInProgress, |
| 3017 | StepStatus::NeedsAction => MessageId::SetupStatusNeedsAction, |
| 3018 | StepStatus::Verified => MessageId::SetupStatusVerified, |
| 3019 | StepStatus::Skipped => MessageId::SetupStatusSkipped, |
| 3020 | StepStatus::Failed => MessageId::SetupStatusFailed, |
| 3021 | }, |
| 3022 | ) |
| 3023 | } |
| 3024 | } |
| 3025 | |
| 3026 | impl ModalView for SetupWizardView { |
| 3027 | fn kind(&self) -> ModalKind { |
| 3028 | ModalKind::SetupWizard |
| 3029 | } |
| 3030 | |
| 3031 | fn handle_key(&mut self, key: KeyEvent) -> ViewAction { |
| 3032 | if let Some(action) = self.handle_freeform_note_key(key) { |
| 3033 | return action; |
| 3034 | } |
| 3035 | if self.selected_step() == SetupStep::Constitution && !self.constitution_advanced { |
| 3036 | return match key.code { |
| 3037 | KeyCode::Esc | KeyCode::Char('q') => ViewAction::Close, |
| 3038 | KeyCode::Left | KeyCode::Char('b') => { |
| 3039 | self.move_back(); |
| 3040 | ViewAction::None |
| 3041 | } |
| 3042 | KeyCode::Char('c') => self.open_constitution_advanced(), |
| 3043 | KeyCode::Enter => self.commit_simple_constitution(), |
| 3044 | _ => ViewAction::None, |
| 3045 | }; |
| 3046 | } |
| 3047 | if self.selected_step() == SetupStep::Constitution |
| 3048 | && self.constitution_advanced |
| 3049 | && key.code == KeyCode::Esc |
| 3050 | { |
| 3051 | return self.close_constitution_advanced(); |
| 3052 | } |
| 3053 | match key.code { |
| 3054 | KeyCode::Esc | KeyCode::Char('q') => ViewAction::Close, |
| 3055 | KeyCode::Char('i') | KeyCode::Char('?') if self.progressive_guide => { |
| 3056 | self.details_expanded = !self.details_expanded; |
| 3057 | self.body_scroll = 0; |
| 3058 | ViewAction::None |
| 3059 | } |
| 3060 | KeyCode::Left | KeyCode::Char('b') => { |
| 3061 | self.move_back(); |
| 3062 | ViewAction::None |
| 3063 | } |
| 3064 | KeyCode::Right | KeyCode::Char('n') => { |
| 3065 | self.move_next(); |
| 3066 | ViewAction::None |
| 3067 | } |
| 3068 | KeyCode::PageUp => { |
| 3069 | self.body_scroll = self.body_scroll.saturating_sub(8); |
| 3070 | ViewAction::None |
| 3071 | } |
| 3072 | KeyCode::PageDown => { |
| 3073 | self.body_scroll = self.body_scroll.saturating_add(8); |
| 3074 | ViewAction::None |
| 3075 | } |
| 3076 | KeyCode::Up if self.progressive_guide => { |
| 3077 | self.body_scroll = self.body_scroll.saturating_sub(1); |
| 3078 | ViewAction::None |
| 3079 | } |
| 3080 | KeyCode::Down if self.progressive_guide => { |
| 3081 | self.body_scroll = self.body_scroll.saturating_add(1); |
| 3082 | ViewAction::None |
| 3083 | } |
| 3084 | KeyCode::Up => { |
| 3085 | self.move_back(); |
| 3086 | ViewAction::None |
| 3087 | } |
| 3088 | KeyCode::Down => { |
| 3089 | self.move_next(); |
| 3090 | ViewAction::None |
| 3091 | } |
| 3092 | KeyCode::Char('s') if self.progressive_guide => { |
| 3093 | if self.selected_step() == SetupStep::Verification { |
| 3094 | ViewAction::Close |
| 3095 | } else { |
| 3096 | self.move_next(); |
| 3097 | ViewAction::None |
| 3098 | } |
| 3099 | } |
| 3100 | KeyCode::Char('s') => { |
| 3101 | self.commit_selected_status(StepStatus::Skipped, MessageId::SetupStepSkipped, true) |
| 3102 | } |
| 3103 | KeyCode::Char('r') |
| 3104 | if self.progressive_guide |
| 3105 | && matches!( |
| 3106 | self.selected_step(), |
| 3107 | SetupStep::RemoteRuntime | SetupStep::Verification |
| 3108 | ) => |
| 3109 | { |
| 3110 | ViewAction::EmitAndClose(ViewEvent::SetupOpenRemoteControlRequested) |
| 3111 | } |
| 3112 | KeyCode::Char('p') |
| 3113 | if self.progressive_guide && self.selected_step() == SetupStep::Verification => |
| 3114 | { |
| 3115 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 3116 | } |
| 3117 | KeyCode::Char('c') |
| 3118 | if self.progressive_guide && self.selected_step() == SetupStep::Verification => |
| 3119 | { |
| 3120 | self.selected = visible_step_index(SetupStep::TrustSandbox); |
| 3121 | self.details_expanded = false; |
| 3122 | self.body_scroll = 0; |
| 3123 | ViewAction::None |
| 3124 | } |
| 3125 | KeyCode::Char('r') |
| 3126 | if !self.progressive_guide && self.selected_step() == SetupStep::ToolsMcp => |
| 3127 | { |
| 3128 | self.preview_tools_mcp_on_ramp() |
| 3129 | } |
| 3130 | KeyCode::Char('r') if self.selected_step() == SetupStep::RemoteRuntime => { |
| 3131 | self.preview_remote_runtime_on_ramp() |
| 3132 | } |
| 3133 | KeyCode::Char('r') => self.commit_selected_status( |
| 3134 | StepStatus::NeedsAction, |
| 3135 | MessageId::SetupStepRetryRecorded, |
| 3136 | false, |
| 3137 | ), |
| 3138 | KeyCode::Char('g') if self.selected_step() == SetupStep::Constitution => { |
| 3139 | self.commit_guided_constitution() |
| 3140 | } |
| 3141 | KeyCode::Char('p') if self.selected_step() == SetupStep::ProviderModel => { |
| 3142 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 3143 | } |
| 3144 | KeyCode::Char('m') if self.selected_step() == SetupStep::ProviderModel => { |
| 3145 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModelRequested) |
| 3146 | } |
| 3147 | KeyCode::Char('p') if self.selected_step() == SetupStep::OperateFleet => { |
| 3148 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 3149 | } |
| 3150 | KeyCode::Char('f') if self.selected_step() == SetupStep::OperateFleet => { |
| 3151 | ViewAction::EmitAndClose(ViewEvent::SetupOpenFleetRequested) |
| 3152 | } |
| 3153 | KeyCode::Char('h') if self.selected_step() == SetupStep::Hotbar => { |
| 3154 | ViewAction::EmitAndClose(ViewEvent::SetupOpenHotbarRequested) |
| 3155 | } |
| 3156 | KeyCode::Char('m') if self.selected_step() == SetupStep::TrustSandbox => { |
| 3157 | ViewAction::EmitAndClose(ViewEvent::SetupOpenModeRequested) |
| 3158 | } |
| 3159 | KeyCode::Char('c') if self.selected_step() == SetupStep::TrustSandbox => { |
| 3160 | ViewAction::EmitAndClose(ViewEvent::SetupOpenConfigRequested) |
| 3161 | } |
| 3162 | KeyCode::Char(key @ ('1' | '2' | '3')) |
| 3163 | if self.selected_step() == SetupStep::TrustSandbox => |
| 3164 | { |
| 3165 | self.select_runtime_preset(key) |
| 3166 | } |
| 3167 | KeyCode::Char('a') if self.selected_step() == SetupStep::TrustSandbox => { |
| 3168 | self.commit_runtime_preset() |
| 3169 | } |
| 3170 | KeyCode::Char(key @ ('1' | '2' | '3' | '4' | '5' | '6')) |
| 3171 | if self.selected_step() == SetupStep::Constitution => |
| 3172 | { |
| 3173 | self.cycle_guided_answer(key) |
| 3174 | } |
| 3175 | KeyCode::Char('a') if self.selected_step() == SetupStep::Constitution => { |
| 3176 | self.request_model_draft() |
| 3177 | } |
| 3178 | KeyCode::Char('f') if self.selected_step() == SetupStep::Constitution => { |
| 3179 | self.toggle_freeform_edit() |
| 3180 | } |
| 3181 | KeyCode::Char('k') if self.selected_step() == SetupStep::Constitution => { |
| 3182 | self.commit_keep_existing_constitution() |
| 3183 | } |
| 3184 | KeyCode::Char('u') => self.commit_constitution(SetupCommitKind::BundledConstitution), |
| 3185 | KeyCode::Char('d') => self.commit_constitution(SetupCommitKind::DeferredConstitution), |
| 3186 | KeyCode::Enter if self.selected_step() == SetupStep::Constitution => { |
| 3187 | self.commit_constitution(SetupCommitKind::BundledConstitution) |
| 3188 | } |
| 3189 | KeyCode::Enter if self.selected_step() == SetupStep::Language => { |
| 3190 | self.commit_language_review() |
| 3191 | } |
| 3192 | KeyCode::Enter if self.selected_step() == SetupStep::ProviderModel => { |
| 3193 | if self.progressive_guide && !self.facts.provider_ready { |
| 3194 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 3195 | } else { |
| 3196 | self.commit_provider_model_review() |
| 3197 | } |
| 3198 | } |
| 3199 | KeyCode::Enter if self.selected_step() == SetupStep::TrustSandbox => { |
| 3200 | self.commit_runtime_posture_review() |
| 3201 | } |
| 3202 | KeyCode::Enter if self.selected_step() == SetupStep::OperateFleet => { |
| 3203 | self.commit_operate_fleet_review() |
| 3204 | } |
| 3205 | KeyCode::Enter if self.selected_step() == SetupStep::Hotbar => { |
| 3206 | self.commit_hotbar_review() |
| 3207 | } |
| 3208 | KeyCode::Enter if self.selected_step() == SetupStep::ToolsMcp => { |
| 3209 | self.commit_tools_mcp_review() |
| 3210 | } |
| 3211 | KeyCode::Enter if self.selected_step() == SetupStep::RemoteRuntime => { |
| 3212 | self.commit_remote_runtime_review() |
| 3213 | } |
| 3214 | KeyCode::Enter if self.selected_step() == SetupStep::Persistence => { |
| 3215 | self.commit_persistence_review() |
| 3216 | } |
| 3217 | KeyCode::Enter if self.selected_step() == SetupStep::Verification => { |
| 3218 | self.commit_setup_report() |
| 3219 | } |
| 3220 | KeyCode::Enter => { |
| 3221 | self.move_next(); |
| 3222 | ViewAction::None |
| 3223 | } |
| 3224 | _ => ViewAction::None, |
| 3225 | } |
| 3226 | } |
| 3227 | |
| 3228 | fn handle_paste(&mut self, text: &str) -> bool { |
| 3229 | if self.selected_step() != SetupStep::Constitution || !self.constitution_advanced { |
| 3230 | return false; |
| 3231 | } |
| 3232 | self.append_freeform_note_text(text); |
| 3233 | true |
| 3234 | } |
| 3235 | |
| 3236 | fn render(&self, area: Rect, buf: &mut Buffer) { |
| 3237 | let inner = render_underwater_surface(area, buf, self.surface_title()); |
| 3238 | let simple_constitution = |
| 3239 | self.selected_step() == SetupStep::Constitution && !self.constitution_advanced; |
| 3240 | let hints = if self.progressive_guide { |
| 3241 | self.progressive_action_hints() |
| 3242 | } else if simple_constitution { |
| 3243 | vec![ |
| 3244 | ActionHint::new( |
| 3245 | "Enter", |
| 3246 | tr( |
| 3247 | self.locale, |
| 3248 | if self.facts.constitution_file == SetupConstitutionFileState::Loaded { |
| 3249 | MessageId::SetupActionKeepExisting |
| 3250 | } else { |
| 3251 | MessageId::SetupActionUseRecommended |
| 3252 | }, |
| 3253 | ) |
| 3254 | .to_string(), |
| 3255 | ), |
| 3256 | ActionHint::new( |
| 3257 | "C", |
| 3258 | tr(self.locale, MessageId::SetupActionCustomize).to_string(), |
| 3259 | ), |
| 3260 | ActionHint::new("B", tr(self.locale, MessageId::SetupActionBack).to_string()), |
| 3261 | ActionHint::new( |
| 3262 | "Esc", |
| 3263 | tr(self.locale, MessageId::SetupActionCancel).to_string(), |
| 3264 | ), |
| 3265 | ] |
| 3266 | } else { |
| 3267 | let mut hints = vec![ |
| 3268 | ActionHint::new( |
| 3269 | "Enter", |
| 3270 | tr(self.locale, MessageId::SetupActionContinue).to_string(), |
| 3271 | ), |
| 3272 | ActionHint::new("B", tr(self.locale, MessageId::SetupActionBack).to_string()), |
| 3273 | ActionHint::new("S", tr(self.locale, MessageId::SetupActionSkip).to_string()), |
| 3274 | ]; |
| 3275 | self.extend_focused_action_hints(&mut hints); |
| 3276 | hints.push(ActionHint::new( |
| 3277 | "Esc", |
| 3278 | tr(self.locale, MessageId::SetupActionCancel).to_string(), |
| 3279 | )); |
| 3280 | hints |
| 3281 | }; |
| 3282 | let content_area = render_modal_footer(inner, buf, &hints); |
| 3283 | let spec = self.selected_spec(); |
| 3284 | let (title_text, question_text) = if self.progressive_guide { |
| 3285 | match self.selected_step() { |
| 3286 | SetupStep::ProviderModel => ( |
| 3287 | tr(self.locale, MessageId::OnboardProviderTitle).into_owned(), |
| 3288 | tr(self.locale, MessageId::OnboardProviderBlurb).into_owned(), |
| 3289 | ), |
| 3290 | SetupStep::TrustSandbox => ( |
| 3291 | tr(self.locale, MessageId::SetupStepTrustSandboxTitle).into_owned(), |
| 3292 | tr(self.locale, MessageId::SetupRuntimePostureReviewHint).into_owned(), |
| 3293 | ), |
| 3294 | SetupStep::RemoteRuntime => ( |
| 3295 | "/rc".to_string(), |
| 3296 | tr(self.locale, MessageId::CmdRemoteControlDescription).into_owned(), |
| 3297 | ), |
| 3298 | SetupStep::Verification => ( |
| 3299 | tr(self.locale, MessageId::OnboardReadyTitle).into_owned(), |
| 3300 | tr(self.locale, MessageId::OnboardReadyLead).into_owned(), |
| 3301 | ), |
| 3302 | _ => ( |
| 3303 | tr(self.locale, spec.title_id()).into_owned(), |
| 3304 | tr(self.locale, spec.why_id()).into_owned(), |
| 3305 | ), |
| 3306 | } |
| 3307 | } else { |
| 3308 | ( |
| 3309 | tr(self.locale, spec.title_id()).into_owned(), |
| 3310 | tr(self.locale, spec.why_id()).into_owned(), |
| 3311 | ) |
| 3312 | }; |
| 3313 | let title = Line::from(Span::styled( |
| 3314 | title_text, |
| 3315 | Style::default() |
| 3316 | .fg(palette::WHALE_ACTION) |
| 3317 | .add_modifier(Modifier::BOLD), |
| 3318 | )); |
| 3319 | let why = Line::from(Span::raw(question_text)); |
| 3320 | let mut lines = vec![title, why, Line::from("")]; |
| 3321 | lines.extend(self.selected_step_detail_lines()); |
| 3322 | let wrap_width = usize::from(content_area.width).max(1); |
| 3323 | let visual_rows: usize = lines |
| 3324 | .iter() |
| 3325 | .map(|line| line.width().div_ceil(wrap_width).max(1)) |
| 3326 | .sum(); |
| 3327 | let visible_rows = usize::from(content_area.height).max(1); |
| 3328 | let max_scroll = visual_rows.saturating_sub(visible_rows); |
| 3329 | let scroll = self.body_scroll.min(max_scroll); |
| 3330 | let content_area = |
| 3331 | render_panel_scroll_rail(content_area, buf, visual_rows, scroll, visible_rows, true); |
| 3332 | // Explicit base ink: same black-on-black hazard as the pager body; |
| 3333 | // value spans below carry no fg of their own. |
| 3334 | Paragraph::new(lines) |
| 3335 | .wrap(Wrap { trim: false }) |
| 3336 | .scroll((scroll as u16, 0)) |
| 3337 | .style(Style::default().fg(palette::TEXT_PRIMARY)) |
| 3338 | .render(content_area, buf); |
| 3339 | } |
| 3340 | |
| 3341 | fn as_any_mut(&mut self) -> &mut dyn std::any::Any { |
| 3342 | self |
| 3343 | } |
| 3344 | } |
| 3345 | |
| 3346 | impl SetupWizardView { |
| 3347 | fn progressive_action_hints(&self) -> Vec<ActionHint> { |
| 3348 | let back = || ActionHint::new("B", tr(self.locale, MessageId::SetupActionBack).to_string()); |
| 3349 | let exit = || { |
| 3350 | ActionHint::new( |
| 3351 | "Esc", |
| 3352 | tr(self.locale, MessageId::SetupActionCancel).to_string(), |
| 3353 | ) |
| 3354 | }; |
| 3355 | let details = || { |
| 3356 | ActionHint::new( |
| 3357 | "I", |
| 3358 | tr(self.locale, MessageId::CtxMenuOpenDetails).to_string(), |
| 3359 | ) |
| 3360 | }; |
| 3361 | let skip = || ActionHint::new("S", tr(self.locale, MessageId::SetupActionSkip).to_string()); |
| 3362 | let mut hints = match self.selected_step() { |
| 3363 | SetupStep::ProviderModel => vec![ |
| 3364 | ActionHint::new( |
| 3365 | "Enter", |
| 3366 | tr( |
| 3367 | self.locale, |
| 3368 | if self.facts.provider_ready { |
| 3369 | MessageId::SetupActionContinue |
| 3370 | } else { |
| 3371 | MessageId::SetupActionProvider |
| 3372 | }, |
| 3373 | ) |
| 3374 | .to_string(), |
| 3375 | ), |
| 3376 | skip(), |
| 3377 | details(), |
| 3378 | exit(), |
| 3379 | ], |
| 3380 | SetupStep::TrustSandbox => vec![ |
| 3381 | ActionHint::new( |
| 3382 | "Enter", |
| 3383 | tr(self.locale, MessageId::SetupActionKeepExisting).to_string(), |
| 3384 | ), |
| 3385 | skip(), |
| 3386 | details(), |
| 3387 | exit(), |
| 3388 | ], |
| 3389 | SetupStep::RemoteRuntime => vec![ |
| 3390 | ActionHint::new( |
| 3391 | "R", |
| 3392 | tr(self.locale, MessageId::CmdRemoteControlDescription).to_string(), |
| 3393 | ), |
| 3394 | skip(), |
| 3395 | details(), |
| 3396 | exit(), |
| 3397 | ], |
| 3398 | SetupStep::ToolsMcp => vec![ |
| 3399 | ActionHint::new( |
| 3400 | "Enter", |
| 3401 | tr(self.locale, MessageId::SetupActionContinue).to_string(), |
| 3402 | ), |
| 3403 | skip(), |
| 3404 | details(), |
| 3405 | exit(), |
| 3406 | ], |
| 3407 | SetupStep::Verification => vec![ |
| 3408 | ActionHint::new( |
| 3409 | "Enter", |
| 3410 | tr(self.locale, MessageId::OnboardReadyStart).to_string(), |
| 3411 | ), |
| 3412 | details(), |
| 3413 | exit(), |
| 3414 | ], |
| 3415 | _ => vec![exit()], |
| 3416 | }; |
| 3417 | let position = self |
| 3418 | .progressive_steps() |
| 3419 | .iter() |
| 3420 | .position(|step| *step == self.selected_step()) |
| 3421 | .unwrap_or(0); |
| 3422 | if position > 0 { |
| 3423 | hints.insert(hints.len().saturating_sub(1), back()); |
| 3424 | } |
| 3425 | hints |
| 3426 | } |
| 3427 | |
| 3428 | fn extend_focused_action_hints(&self, hints: &mut Vec<ActionHint>) { |
| 3429 | match self.selected_step() { |
| 3430 | SetupStep::Constitution if self.constitution_advanced => { |
| 3431 | hints.push(ActionHint::new( |
| 3432 | "1-6", |
| 3433 | tr(self.locale, MessageId::SetupActionTuneGuided).to_string(), |
| 3434 | )); |
| 3435 | hints.push(ActionHint::new( |
| 3436 | "G", |
| 3437 | tr(self.locale, MessageId::SetupActionGuided).to_string(), |
| 3438 | )); |
| 3439 | hints.push(ActionHint::new( |
| 3440 | "F", |
| 3441 | tr(self.locale, MessageId::SetupActionFreeform).to_string(), |
| 3442 | )); |
| 3443 | } |
| 3444 | SetupStep::ProviderModel => { |
| 3445 | hints.push(ActionHint::new( |
| 3446 | "P", |
| 3447 | tr(self.locale, MessageId::SetupActionProvider).to_string(), |
| 3448 | )); |
| 3449 | hints.push(ActionHint::new( |
| 3450 | "M", |
| 3451 | tr(self.locale, MessageId::SetupActionModel).to_string(), |
| 3452 | )); |
| 3453 | } |
| 3454 | SetupStep::OperateFleet => hints.push(ActionHint::new( |
| 3455 | "F", |
| 3456 | tr(self.locale, MessageId::SetupActionFleet).to_string(), |
| 3457 | )), |
| 3458 | SetupStep::Hotbar => hints.push(ActionHint::new( |
| 3459 | "H", |
| 3460 | tr(self.locale, MessageId::SetupActionHotbar).to_string(), |
| 3461 | )), |
| 3462 | SetupStep::ToolsMcp | SetupStep::RemoteRuntime => hints.push(ActionHint::new( |
| 3463 | "R", |
| 3464 | tr(self.locale, MessageId::SetupActionRetry).to_string(), |
| 3465 | )), |
| 3466 | SetupStep::TrustSandbox => hints.push(ActionHint::new( |
| 3467 | "C", |
| 3468 | tr(self.locale, MessageId::SetupActionConfig).to_string(), |
| 3469 | )), |
| 3470 | _ => {} |
| 3471 | } |
| 3472 | } |
| 3473 | |
| 3474 | fn selected_step_detail_lines(&self) -> Vec<Line<'static>> { |
| 3475 | if self.progressive_guide { |
| 3476 | return if self.details_expanded { |
| 3477 | self.progressive_expanded_lines() |
| 3478 | } else { |
| 3479 | self.progressive_detail_lines() |
| 3480 | }; |
| 3481 | } |
| 3482 | match self.selected_step() { |
| 3483 | SetupStep::ProviderModel => self.provider_model_detail_lines(), |
| 3484 | SetupStep::TrustSandbox => self.runtime_posture_detail_lines(), |
| 3485 | SetupStep::Constitution if self.constitution_advanced => { |
| 3486 | self.constitution_detail_lines() |
| 3487 | } |
| 3488 | SetupStep::Constitution => self.constitution_simple_lines(), |
| 3489 | SetupStep::OperateFleet => self.operate_fleet_detail_lines(), |
| 3490 | SetupStep::Hotbar => self.hotbar_detail_lines(), |
| 3491 | SetupStep::ToolsMcp => self.tools_mcp_detail_lines(), |
| 3492 | SetupStep::RemoteRuntime => self.remote_runtime_detail_lines(), |
| 3493 | SetupStep::Persistence => self.persistence_detail_lines(), |
| 3494 | SetupStep::Verification => self.verification_detail_lines(), |
| 3495 | _ => Vec::new(), |
| 3496 | } |
| 3497 | } |
| 3498 | |
| 3499 | fn progressive_detail_lines(&self) -> Vec<Line<'static>> { |
| 3500 | match self.selected_step() { |
| 3501 | SetupStep::ProviderModel => { |
| 3502 | let answer = format!( |
| 3503 | "{} · {} · {}", |
| 3504 | self.facts.provider, self.facts.model, self.facts.auth |
| 3505 | ); |
| 3506 | vec![self.detail_row(MessageId::SetupCardRouteLabel, &answer)] |
| 3507 | } |
| 3508 | SetupStep::TrustSandbox => { |
| 3509 | let answer = format!( |
| 3510 | "{} · {} · {}", |
| 3511 | self.facts.approval, self.facts.trust, self.facts.sandbox |
| 3512 | ); |
| 3513 | let mut lines = vec![self.detail_row(MessageId::SetupCardApprovalLabel, &answer)]; |
| 3514 | if let Some(warning) = &self.facts.project_override_warning { |
| 3515 | lines.push( |
| 3516 | self.detail_row(MessageId::SetupRuntimeProjectOverrideLabel, warning), |
| 3517 | ); |
| 3518 | } |
| 3519 | lines |
| 3520 | } |
| 3521 | SetupStep::RemoteRuntime => vec![self.detail_row( |
| 3522 | MessageId::SetupRemoteModeLabel, |
| 3523 | &self.facts.remote_control_result, |
| 3524 | )], |
| 3525 | SetupStep::ToolsMcp => { |
| 3526 | let status = tr( |
| 3527 | self.locale, |
| 3528 | if self.facts.tools_mcp_needs_action { |
| 3529 | MessageId::SetupStatusNeedsAction |
| 3530 | } else { |
| 3531 | MessageId::SetupStatusVerified |
| 3532 | }, |
| 3533 | ) |
| 3534 | .into_owned(); |
| 3535 | vec![self.detail_row(MessageId::SetupStepToolsMcpTitle, &status)] |
| 3536 | } |
| 3537 | SetupStep::Verification => self.progressive_summary_lines(), |
| 3538 | _ => self.selected_step_detail_lines_expanded(), |
| 3539 | } |
| 3540 | } |
| 3541 | |
| 3542 | fn selected_step_detail_lines_expanded(&self) -> Vec<Line<'static>> { |
| 3543 | match self.selected_step() { |
| 3544 | SetupStep::ProviderModel => self.provider_model_detail_lines(), |
| 3545 | SetupStep::TrustSandbox => self.runtime_posture_detail_lines(), |
| 3546 | SetupStep::ToolsMcp => self.tools_mcp_detail_lines(), |
| 3547 | SetupStep::RemoteRuntime => self.remote_runtime_detail_lines(), |
| 3548 | SetupStep::Verification => self.verification_detail_lines(), |
| 3549 | _ => Vec::new(), |
| 3550 | } |
| 3551 | } |
| 3552 | |
| 3553 | fn progressive_expanded_lines(&self) -> Vec<Line<'static>> { |
| 3554 | if self.selected_step() != SetupStep::Verification { |
| 3555 | return self.selected_step_detail_lines_expanded(); |
| 3556 | } |
| 3557 | let mut lines = self.progressive_summary_lines(); |
| 3558 | lines.push(Line::from("")); |
| 3559 | lines.push(Line::from(vec![ |
| 3560 | Span::styled( |
| 3561 | "/settings ", |
| 3562 | Style::default() |
| 3563 | .fg(palette::WHALE_ACTION) |
| 3564 | .add_modifier(Modifier::BOLD), |
| 3565 | ), |
| 3566 | Span::raw(tr(self.locale, MessageId::CmdSettingsDescription).to_string()), |
| 3567 | ])); |
| 3568 | lines.push(Line::from(vec![ |
| 3569 | Span::styled( |
| 3570 | "/config <key> ", |
| 3571 | Style::default() |
| 3572 | .fg(palette::TEXT_MUTED) |
| 3573 | .add_modifier(Modifier::BOLD), |
| 3574 | ), |
| 3575 | Span::raw(tr(self.locale, MessageId::SetupActionConfig).to_string()), |
| 3576 | ])); |
| 3577 | lines |
| 3578 | } |
| 3579 | |
| 3580 | fn progressive_summary_lines(&self) -> Vec<Line<'static>> { |
| 3581 | let route = format!("{} · {}", self.facts.provider, self.facts.model); |
| 3582 | let permissions = format!( |
| 3583 | "{} · {} · {}", |
| 3584 | self.facts.approval, self.facts.trust, self.facts.sandbox |
| 3585 | ); |
| 3586 | let mut lines = vec![ |
| 3587 | self.detail_row(MessageId::SetupStepProviderModelTitle, &route), |
| 3588 | self.detail_row(MessageId::SetupStepTrustSandboxTitle, &permissions), |
| 3589 | self.detail_row( |
| 3590 | MessageId::SetupStepRemoteRuntimeTitle, |
| 3591 | &self.facts.remote_control_result, |
| 3592 | ), |
| 3593 | ]; |
| 3594 | if self.tools_relevant() { |
| 3595 | let tools_status = tr( |
| 3596 | self.locale, |
| 3597 | if self.facts.tools_mcp_needs_action { |
| 3598 | MessageId::SetupStatusNeedsAction |
| 3599 | } else { |
| 3600 | MessageId::SetupStatusVerified |
| 3601 | }, |
| 3602 | ) |
| 3603 | .into_owned(); |
| 3604 | lines.push(self.detail_row(MessageId::SetupStepToolsMcpTitle, &tools_status)); |
| 3605 | } |
| 3606 | lines |
| 3607 | } |
| 3608 | |
| 3609 | fn constitution_simple_lines(&self) -> Vec<Line<'static>> { |
| 3610 | if self.facts.constitution_file == SetupConstitutionFileState::Loaded { |
| 3611 | return vec![ |
| 3612 | self.detail_row( |
| 3613 | MessageId::SetupConstitutionExistingLabel, |
| 3614 | &self |
| 3615 | .facts |
| 3616 | .constitution_file |
| 3617 | .label(self.state.constitution_choice, self.locale), |
| 3618 | ), |
| 3619 | Line::from(Span::styled( |
| 3620 | tr( |
| 3621 | self.locale, |
| 3622 | MessageId::SetupConstitutionExistingDefaultDetail, |
| 3623 | ) |
| 3624 | .to_string(), |
| 3625 | Style::default().fg(palette::TEXT_MUTED), |
| 3626 | )), |
| 3627 | ]; |
| 3628 | } |
| 3629 | if !matches!( |
| 3630 | self.facts.constitution_file, |
| 3631 | SetupConstitutionFileState::NotChecked | SetupConstitutionFileState::Missing |
| 3632 | ) { |
| 3633 | return vec![ |
| 3634 | self.detail_row( |
| 3635 | MessageId::SetupConstitutionExistingLabel, |
| 3636 | &self |
| 3637 | .facts |
| 3638 | .constitution_file |
| 3639 | .label(self.state.constitution_choice, self.locale), |
| 3640 | ), |
| 3641 | Line::from(Span::styled( |
| 3642 | tr(self.locale, MessageId::SetupConstitutionRepairDefaultDetail).to_string(), |
| 3643 | Style::default().fg(palette::TEXT_MUTED), |
| 3644 | )), |
| 3645 | ]; |
| 3646 | } |
| 3647 | |
| 3648 | let recommendation = format!( |
| 3649 | "{} · {}", |
| 3650 | tr(self.locale, MessageId::SetupStatusRecommended), |
| 3651 | autonomy_label(AutonomyPreference::Balanced, self.locale) |
| 3652 | ); |
| 3653 | vec![ |
| 3654 | Line::from(Span::styled( |
| 3655 | recommendation, |
| 3656 | Style::default() |
| 3657 | .fg(palette::TEXT_PRIMARY) |
| 3658 | .add_modifier(Modifier::BOLD), |
| 3659 | )), |
| 3660 | Line::from(Span::styled( |
| 3661 | autonomy_priority(AutonomyPreference::Balanced, self.locale).to_string(), |
| 3662 | Style::default().fg(palette::TEXT_MUTED), |
| 3663 | )), |
| 3664 | ] |
| 3665 | } |
| 3666 | |
| 3667 | fn provider_model_detail_lines(&self) -> Vec<Line<'static>> { |
| 3668 | vec![ |
| 3669 | self.detail_row(MessageId::SetupCardRouteLabel, &self.facts.provider), |
| 3670 | self.detail_row(MessageId::SetupCardModelLabel, &self.facts.model), |
| 3671 | self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth), |
| 3672 | self.detail_row(MessageId::SetupCardHealthLabel, &self.facts.health), |
| 3673 | Line::from(Span::styled( |
| 3674 | tr( |
| 3675 | self.locale, |
| 3676 | if self.facts.provider_ready { |
| 3677 | MessageId::SetupProviderModelReadyHint |
| 3678 | } else { |
| 3679 | MessageId::SetupProviderModelNeedsActionHint |
| 3680 | }, |
| 3681 | ) |
| 3682 | .to_string(), |
| 3683 | Style::default().fg(palette::TEXT_MUTED), |
| 3684 | )), |
| 3685 | ] |
| 3686 | } |
| 3687 | |
| 3688 | fn constitution_detail_lines(&self) -> Vec<Line<'static>> { |
| 3689 | let choice = constitution_choice_label(self.state.constitution_choice); |
| 3690 | let source = constitution_source_label(self.state.constitution_source); |
| 3691 | let validity = constitution_validity_label(self.state.constitution_validity); |
| 3692 | let source_state = format!("{source}; validity {validity}"); |
| 3693 | let existing_file = self |
| 3694 | .facts |
| 3695 | .constitution_file |
| 3696 | .label(self.state.constitution_choice, self.locale); |
| 3697 | let expert_override = self.facts.expert_override.label(self.locale); |
| 3698 | let preview = self |
| 3699 | .state |
| 3700 | .constitution_preview_hash |
| 3701 | .as_deref() |
| 3702 | .unwrap_or("not accepted yet") |
| 3703 | .to_string(); |
| 3704 | let mut lines = vec![ |
| 3705 | self.detail_row(MessageId::SetupConstitutionChoiceLabel, choice), |
| 3706 | self.detail_row(MessageId::SetupConstitutionSourceLabel, &source_state), |
| 3707 | self.detail_row(MessageId::SetupConstitutionPreviewLabel, &preview), |
| 3708 | self.detail_row(MessageId::SetupConstitutionExistingLabel, &existing_file), |
| 3709 | self.detail_row( |
| 3710 | MessageId::SetupConstitutionExpertOverrideLabel, |
| 3711 | &expert_override, |
| 3712 | ), |
| 3713 | Line::from(Span::styled( |
| 3714 | tr(self.locale, MessageId::SetupConstitutionGuidedAnswersHint).to_string(), |
| 3715 | Style::default().fg(palette::TEXT_MUTED), |
| 3716 | )), |
| 3717 | self.guided_answer_pair( |
| 3718 | ( |
| 3719 | "1", |
| 3720 | MessageId::SetupConstitutionPurposeLabel, |
| 3721 | &self.guided_draft.purpose.label(self.locale), |
| 3722 | ), |
| 3723 | ( |
| 3724 | "2", |
| 3725 | MessageId::SetupConstitutionAutonomyLabel, |
| 3726 | autonomy_label(self.guided_draft.autonomy, self.locale), |
| 3727 | ), |
| 3728 | ), |
| 3729 | self.guided_answer_pair( |
| 3730 | ( |
| 3731 | "3", |
| 3732 | MessageId::SetupConstitutionEvidenceLabel, |
| 3733 | &self.guided_draft.evidence.label(self.locale), |
| 3734 | ), |
| 3735 | ( |
| 3736 | "4", |
| 3737 | MessageId::SetupConstitutionCommunicationLabel, |
| 3738 | self.guided_draft.communication.label(self.locale), |
| 3739 | ), |
| 3740 | ), |
| 3741 | self.guided_answer_single( |
| 3742 | "5", |
| 3743 | MessageId::SetupConstitutionPrivacyLabel, |
| 3744 | self.guided_draft.privacy.label(self.locale), |
| 3745 | ), |
| 3746 | self.guided_answer_single( |
| 3747 | "6", |
| 3748 | MessageId::SetupConstitutionPrinciplesLabel, |
| 3749 | self.guided_draft.principles.label(self.locale), |
| 3750 | ), |
| 3751 | freeform_note_line(self.locale, &self.freeform_note, self.editing_freeform_note), |
| 3752 | ]; |
| 3753 | if self.facts.constitution_file == SetupConstitutionFileState::Loaded { |
| 3754 | lines.push(Line::from(Span::styled( |
| 3755 | keep_existing_invitation_line(self.locale), |
| 3756 | Style::default().fg(palette::WHALE_HUMAN), |
| 3757 | ))); |
| 3758 | } |
| 3759 | if let Some(label) = self |
| 3760 | .model_draft_label |
| 3761 | .as_deref() |
| 3762 | .filter(|_| self.model_draft.is_some()) |
| 3763 | { |
| 3764 | lines.push(Line::from(Span::styled( |
| 3765 | model_draft_ready_line(self.locale, label), |
| 3766 | Style::default().fg(palette::STATUS_SUCCESS), |
| 3767 | ))); |
| 3768 | } else if self.facts.provider_ready { |
| 3769 | lines.push(Line::from(Span::styled( |
| 3770 | model_draft_invitation_line(self.locale, &self.facts.model), |
| 3771 | Style::default().fg(palette::WHALE_HUMAN), |
| 3772 | ))); |
| 3773 | } |
| 3774 | lines.push(Line::from(Span::styled( |
| 3775 | tr(self.locale, MessageId::SetupConstitutionGuidedHint).to_string(), |
| 3776 | Style::default().fg(palette::TEXT_MUTED), |
| 3777 | ))); |
| 3778 | lines |
| 3779 | } |
| 3780 | |
| 3781 | fn runtime_posture_detail_lines(&self) -> Vec<Line<'static>> { |
| 3782 | let project_override = self |
| 3783 | .facts |
| 3784 | .project_override_warning |
| 3785 | .clone() |
| 3786 | .unwrap_or_else(|| { |
| 3787 | tr(self.locale, MessageId::SetupRuntimeProjectOverrideNone).to_string() |
| 3788 | }); |
| 3789 | let mut lines = vec![ |
| 3790 | self.detail_row(MessageId::SetupCardIntentLabel, &self.facts.work_intent), |
| 3791 | self.detail_row(MessageId::SetupCardApprovalLabel, &self.facts.approval), |
| 3792 | self.detail_row(MessageId::SetupCardShellLabel, &self.facts.shell), |
| 3793 | self.detail_row(MessageId::SetupCardTrustLabel, &self.facts.trust), |
| 3794 | self.detail_row(MessageId::SetupCardSandboxLabel, &self.facts.sandbox), |
| 3795 | self.detail_row(MessageId::SetupCardNetworkLabel, &self.facts.network), |
| 3796 | self.detail_row( |
| 3797 | MessageId::SetupRuntimePresetSelectedLabel, |
| 3798 | &runtime_preset_summary(self.locale, self.runtime_preset), |
| 3799 | ), |
| 3800 | self.detail_row( |
| 3801 | MessageId::SetupRuntimePresetDiffLabel, |
| 3802 | &runtime_preset_inline_diff(self.runtime_preset, &self.facts), |
| 3803 | ), |
| 3804 | self.detail_row( |
| 3805 | MessageId::SetupRuntimeProjectOverrideLabel, |
| 3806 | &project_override, |
| 3807 | ), |
| 3808 | Line::from(Span::styled( |
| 3809 | tr(self.locale, MessageId::SetupRuntimePostureBoundary).to_string(), |
| 3810 | Style::default().fg(palette::TEXT_MUTED), |
| 3811 | )), |
| 3812 | Line::from(Span::styled( |
| 3813 | tr(self.locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(), |
| 3814 | Style::default().fg(palette::TEXT_MUTED), |
| 3815 | )), |
| 3816 | self.setup_review_hint_line( |
| 3817 | MessageId::SetupRuntimePostureReviewHint, |
| 3818 | Some("Press M for work mode or C for config."), |
| 3819 | ), |
| 3820 | Line::from(Span::styled( |
| 3821 | tr(self.locale, MessageId::SetupRuntimePresetApplyHint).to_string(), |
| 3822 | Style::default().fg(palette::TEXT_MUTED), |
| 3823 | )), |
| 3824 | ]; |
| 3825 | for (idx, preset) in SetupRuntimePreset::ALL.iter().enumerate() { |
| 3826 | let marker = if *preset == self.runtime_preset { |
| 3827 | ">" |
| 3828 | } else { |
| 3829 | " " |
| 3830 | }; |
| 3831 | lines.push(Line::from(Span::styled( |
| 3832 | format!( |
| 3833 | "{marker} {}. {}", |
| 3834 | idx + 1, |
| 3835 | runtime_preset_summary(self.locale, *preset) |
| 3836 | ), |
| 3837 | Style::default().fg(if *preset == self.runtime_preset { |
| 3838 | palette::TEXT_PRIMARY |
| 3839 | } else { |
| 3840 | palette::TEXT_MUTED |
| 3841 | }), |
| 3842 | ))); |
| 3843 | } |
| 3844 | lines |
| 3845 | } |
| 3846 | |
| 3847 | fn operate_fleet_detail_lines(&self) -> Vec<Line<'static>> { |
| 3848 | let route = format!("{} / {}", self.facts.provider, self.facts.model); |
| 3849 | let readiness = self.ready_label(self.operate_fleet_facts_ready()); |
| 3850 | vec![ |
| 3851 | self.detail_row(MessageId::SetupCardRouteLabel, &route), |
| 3852 | self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth), |
| 3853 | self.detail_row( |
| 3854 | MessageId::SetupOperateRuntimeLabel, |
| 3855 | &self.facts.operate_runtime_result, |
| 3856 | ), |
| 3857 | self.detail_row( |
| 3858 | MessageId::SetupOperateRosterLabel, |
| 3859 | &self.facts.fleet_roster_result, |
| 3860 | ), |
| 3861 | self.detail_row( |
| 3862 | MessageId::SetupOperateConcurrencyLabel, |
| 3863 | &self.facts.operate_concurrency_result, |
| 3864 | ), |
| 3865 | self.detail_row(MessageId::SetupOperateReadinessLabel, &readiness), |
| 3866 | self.setup_review_hint_line(MessageId::SetupOperateReviewHint, None), |
| 3867 | ] |
| 3868 | } |
| 3869 | |
| 3870 | fn hotbar_detail_lines(&self) -> Vec<Line<'static>> { |
| 3871 | vec![ |
| 3872 | self.detail_row( |
| 3873 | MessageId::SetupHotbarBindingsLabel, |
| 3874 | &self.facts.hotbar_bindings_result, |
| 3875 | ), |
| 3876 | self.detail_row( |
| 3877 | MessageId::SetupHotbarActionsLabel, |
| 3878 | &self.facts.hotbar_actions_result, |
| 3879 | ), |
| 3880 | self.setup_review_hint_line( |
| 3881 | MessageId::SetupHotbarReviewHint, |
| 3882 | Some("Press H to customize slots."), |
| 3883 | ), |
| 3884 | ] |
| 3885 | } |
| 3886 | |
| 3887 | fn tools_mcp_detail_lines(&self) -> Vec<Line<'static>> { |
| 3888 | vec![ |
| 3889 | self.detail_row( |
| 3890 | MessageId::SetupToolsMcpServersLabel, |
| 3891 | &self.facts.tools_mcp_servers_result, |
| 3892 | ), |
| 3893 | self.detail_row( |
| 3894 | MessageId::SetupToolsMcpSkillsLabel, |
| 3895 | &self.facts.tools_mcp_skills_result, |
| 3896 | ), |
| 3897 | self.detail_row( |
| 3898 | MessageId::SetupToolsMcpToolsLabel, |
| 3899 | &self.facts.tools_mcp_tools_result, |
| 3900 | ), |
| 3901 | self.detail_row( |
| 3902 | MessageId::SetupToolsMcpPluginsLabel, |
| 3903 | &self.facts.tools_mcp_plugins_result, |
| 3904 | ), |
| 3905 | self.detail_row( |
| 3906 | MessageId::SetupToolsMcpHotbarLabel, |
| 3907 | &self.facts.tools_mcp_hotbar_result, |
| 3908 | ), |
| 3909 | self.detail_row( |
| 3910 | MessageId::SetupToolsMcpDshLabel, |
| 3911 | &self.facts.tools_mcp_dsh_result, |
| 3912 | ), |
| 3913 | self.setup_review_hint_line( |
| 3914 | MessageId::SetupToolsMcpReviewHint, |
| 3915 | Some("Press R for safe on-ramps (no auto-run)."), |
| 3916 | ), |
| 3917 | ] |
| 3918 | } |
| 3919 | |
| 3920 | /// #3409: one row per mode, each carrying its own observed status. The |
| 3921 | /// registry counts stay available in the preview; the card itself answers |
| 3922 | /// "where can this be reached from?" in four plain lines. |
| 3923 | fn remote_runtime_detail_lines(&self) -> Vec<Line<'static>> { |
| 3924 | let mut lines = Vec::new(); |
| 3925 | for fact in &self.facts.remote_modes { |
| 3926 | lines.push(self.detail_row( |
| 3927 | fact.mode.label_id(), |
| 3928 | &format!( |
| 3929 | "{} · {}", |
| 3930 | tr(self.locale, fact.status.label_id()), |
| 3931 | fact.detail |
| 3932 | ), |
| 3933 | )); |
| 3934 | } |
| 3935 | if lines.is_empty() { |
| 3936 | lines.push(self.detail_row( |
| 3937 | MessageId::SetupRemoteModeLabel, |
| 3938 | &self.facts.remote_mode_result, |
| 3939 | )); |
| 3940 | } |
| 3941 | lines.push(self.detail_row( |
| 3942 | MessageId::SetupRemoteProvidersLabel, |
| 3943 | &self.facts.remote_providers_result, |
| 3944 | )); |
| 3945 | lines.push(self.setup_review_hint_line( |
| 3946 | MessageId::SetupRemoteReviewHint, |
| 3947 | Some("Press R to preview (nothing is written). Enter keeps local-only."), |
| 3948 | )); |
| 3949 | lines |
| 3950 | } |
| 3951 | |
| 3952 | fn persistence_detail_lines(&self) -> Vec<Line<'static>> { |
| 3953 | vec![ |
| 3954 | self.detail_row( |
| 3955 | MessageId::SetupPersistenceHomeLabel, |
| 3956 | &self.facts.persistence.home_result, |
| 3957 | ), |
| 3958 | self.detail_row( |
| 3959 | MessageId::SetupPersistenceConfigLabel, |
| 3960 | &self.facts.persistence.config_result, |
| 3961 | ), |
| 3962 | self.detail_row( |
| 3963 | MessageId::SetupPersistenceStateLabel, |
| 3964 | &self.facts.persistence.state_result, |
| 3965 | ), |
| 3966 | self.detail_row( |
| 3967 | MessageId::SetupPersistenceConstitutionLabel, |
| 3968 | &self.facts.persistence.constitution_result, |
| 3969 | ), |
| 3970 | self.detail_row( |
| 3971 | MessageId::SetupPersistenceMemoryLabel, |
| 3972 | &self.facts.persistence.memory_result, |
| 3973 | ), |
| 3974 | self.detail_row( |
| 3975 | MessageId::SetupPersistenceNotesLabel, |
| 3976 | &self.facts.persistence.notes_result, |
| 3977 | ), |
| 3978 | self.setup_review_hint_line(MessageId::SetupPersistenceReviewHint, None), |
| 3979 | ] |
| 3980 | } |
| 3981 | |
| 3982 | fn verification_detail_lines(&self) -> Vec<Line<'static>> { |
| 3983 | let mut lines = vec![ |
| 3984 | self.detail_row( |
| 3985 | MessageId::SetupReportFirstRunLabel, |
| 3986 | &self.ready_label(self.state.first_run_ready()), |
| 3987 | ), |
| 3988 | self.detail_row( |
| 3989 | MessageId::SetupReportUpdateLabel, |
| 3990 | &self.ready_label(self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION)), |
| 3991 | ), |
| 3992 | self.detail_row( |
| 3993 | MessageId::SetupReportOperateLabel, |
| 3994 | &self.ready_label(self.state.operate_ready()), |
| 3995 | ), |
| 3996 | self.detail_row( |
| 3997 | MessageId::SetupReportSourceLabel, |
| 3998 | &self.state_source_label(), |
| 3999 | ), |
| 4000 | self.detail_row( |
| 4001 | MessageId::SetupReportAutonomyLabel, |
| 4002 | &self.facts.constitution_autonomy, |
| 4003 | ), |
| 4004 | self.detail_row( |
| 4005 | MessageId::SetupReportRuntimePostureLabel, |
| 4006 | &self.facts.runtime_result, |
| 4007 | ), |
| 4008 | Line::from(""), |
| 4009 | Line::from(Span::styled( |
| 4010 | tr(self.locale, MessageId::SetupReportRowsLabel).to_string(), |
| 4011 | Style::default() |
| 4012 | .fg(palette::TEXT_MUTED) |
| 4013 | .add_modifier(Modifier::BOLD), |
| 4014 | )), |
| 4015 | ]; |
| 4016 | |
| 4017 | for spec in STEP_SPECS { |
| 4018 | let step = spec.id(); |
| 4019 | let entry = self.state.steps.get(&step); |
| 4020 | let required = entry.map_or(spec.required(), |entry| entry.required); |
| 4021 | let required_label = if required { |
| 4022 | tr(self.locale, MessageId::SetupReportRequired) |
| 4023 | } else { |
| 4024 | tr(self.locale, MessageId::SetupReportOptional) |
| 4025 | }; |
| 4026 | let mut value = format!( |
| 4027 | "{} ({})", |
| 4028 | self.status_label(self.state.status(step)), |
| 4029 | required_label |
| 4030 | ); |
| 4031 | if let Some(version) = entry.and_then(|entry| entry.version.as_deref()) { |
| 4032 | value.push_str(&format!(" · {version}")); |
| 4033 | } |
| 4034 | if let Some(result) = entry.and_then(|entry| entry.result.as_deref()) { |
| 4035 | value.push_str(&format!(" · {result}")); |
| 4036 | } |
| 4037 | lines.push(self.detail_row(spec.title_id(), &value)); |
| 4038 | } |
| 4039 | |
| 4040 | lines.push(Line::from("")); |
| 4041 | let next_action = tr(self.locale, self.next_action_id()).to_string(); |
| 4042 | lines.push(self.detail_row(MessageId::SetupReportNextActionLabel, &next_action)); |
| 4043 | lines |
| 4044 | } |
| 4045 | |
| 4046 | fn setup_review_hint_line( |
| 4047 | &self, |
| 4048 | hint_id: MessageId, |
| 4049 | english_action: Option<&'static str>, |
| 4050 | ) -> Line<'static> { |
| 4051 | let hint = if self.locale == Locale::En { |
| 4052 | let mut hint = "Enter records this setup snapshot.".to_string(); |
| 4053 | if let Some(action) = english_action { |
| 4054 | hint.push(' '); |
| 4055 | hint.push_str(action); |
| 4056 | } |
| 4057 | hint |
| 4058 | } else { |
| 4059 | tr(self.locale, hint_id).to_string() |
| 4060 | }; |
| 4061 | Line::from(Span::styled(hint, Style::default().fg(palette::TEXT_MUTED))) |
| 4062 | } |
| 4063 | |
| 4064 | fn ready_label(&self, ready: bool) -> String { |
| 4065 | if ready { |
| 4066 | tr(self.locale, MessageId::SetupReportReady).to_string() |
| 4067 | } else { |
| 4068 | tr(self.locale, MessageId::SetupStatusNeedsAction).to_string() |
| 4069 | } |
| 4070 | } |
| 4071 | |
| 4072 | fn state_source_label(&self) -> String { |
| 4073 | if self.state.inherited { |
| 4074 | tr(self.locale, MessageId::SetupReportInherited).to_string() |
| 4075 | } else { |
| 4076 | tr(self.locale, MessageId::SetupReportPersisted).to_string() |
| 4077 | } |
| 4078 | } |
| 4079 | |
| 4080 | fn next_action_id(&self) -> MessageId { |
| 4081 | if !self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) { |
| 4082 | return MessageId::SetupReportNextActionConstitution; |
| 4083 | } |
| 4084 | if !matches!( |
| 4085 | self.state.status(SetupStep::ProviderModel), |
| 4086 | StepStatus::Verified | StepStatus::NeedsAction |
| 4087 | ) { |
| 4088 | return MessageId::SetupReportNextActionProvider; |
| 4089 | } |
| 4090 | if !self.state.runtime_posture_source.is_reviewed() { |
| 4091 | return MessageId::SetupReportNextActionRuntime; |
| 4092 | } |
| 4093 | if !self.state.first_run_ready() { |
| 4094 | return MessageId::SetupReportNextActionRequired; |
| 4095 | } |
| 4096 | if !self.state.operate_ready() { |
| 4097 | return MessageId::SetupReportNextActionOperate; |
| 4098 | } |
| 4099 | MessageId::SetupReportNextActionNone |
| 4100 | } |
| 4101 | |
| 4102 | fn detail_row(&self, label: MessageId, value: &str) -> Line<'static> { |
| 4103 | Line::from(vec![ |
| 4104 | Span::styled( |
| 4105 | format!("{} ", tr(self.locale, label)), |
| 4106 | Style::default() |
| 4107 | .fg(palette::TEXT_MUTED) |
| 4108 | .add_modifier(Modifier::BOLD), |
| 4109 | ), |
| 4110 | Span::raw(value.to_string()), |
| 4111 | ]) |
| 4112 | } |
| 4113 | |
| 4114 | fn guided_answer_pair( |
| 4115 | &self, |
| 4116 | left: (&str, MessageId, &str), |
| 4117 | right: (&str, MessageId, &str), |
| 4118 | ) -> Line<'static> { |
| 4119 | let label_style = Style::default() |
| 4120 | .fg(palette::TEXT_MUTED) |
| 4121 | .add_modifier(Modifier::BOLD); |
| 4122 | Line::from(vec![ |
| 4123 | Span::styled( |
| 4124 | format!("{} {} ", left.0, tr(self.locale, left.1)), |
| 4125 | label_style, |
| 4126 | ), |
| 4127 | Span::raw(left.2.to_string()), |
| 4128 | Span::styled(" · ", Style::default().fg(palette::TEXT_MUTED)), |
| 4129 | Span::styled( |
| 4130 | format!("{} {} ", right.0, tr(self.locale, right.1)), |
| 4131 | label_style, |
| 4132 | ), |
| 4133 | Span::raw(right.2.to_string()), |
| 4134 | ]) |
| 4135 | } |
| 4136 | |
| 4137 | fn guided_answer_single(&self, key: &str, label: MessageId, value: &str) -> Line<'static> { |
| 4138 | Line::from(vec![ |
| 4139 | Span::styled( |
| 4140 | format!("{key} {} ", tr(self.locale, label)), |
| 4141 | Style::default() |
| 4142 | .fg(palette::TEXT_MUTED) |
| 4143 | .add_modifier(Modifier::BOLD), |
| 4144 | ), |
| 4145 | Span::raw(value.to_string()), |
| 4146 | ]) |
| 4147 | } |
| 4148 | } |
| 4149 | |
| 4150 | fn setup_report_ready(state: &SetupState) -> bool { |
| 4151 | state.first_run_ready() || state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) |
| 4152 | } |
| 4153 | |
| 4154 | fn runtime_preset_summary(locale: Locale, preset: SetupRuntimePreset) -> String { |
| 4155 | format!( |
| 4156 | "{} - {}", |
| 4157 | tr(locale, preset.title_id()), |
| 4158 | tr(locale, preset.description_id()) |
| 4159 | ) |
| 4160 | } |
| 4161 | |
| 4162 | fn runtime_preset_inline_diff(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> String { |
| 4163 | runtime_preset_diff_rows(preset, facts).join("; ") |
| 4164 | } |
| 4165 | |
| 4166 | fn runtime_preset_preview_text( |
| 4167 | locale: Locale, |
| 4168 | preset: SetupRuntimePreset, |
| 4169 | facts: &SetupRuntimeFacts, |
| 4170 | ) -> String { |
| 4171 | let mut lines = vec![ |
| 4172 | tr(locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(), |
| 4173 | runtime_preset_summary(locale, preset), |
| 4174 | String::new(), |
| 4175 | tr(locale, MessageId::SetupRuntimePresetDiffLabel).to_string(), |
| 4176 | ]; |
| 4177 | lines.extend( |
| 4178 | runtime_preset_diff_rows(preset, facts) |
| 4179 | .into_iter() |
| 4180 | .map(|row| format!("- {row}")), |
| 4181 | ); |
| 4182 | lines.extend([ |
| 4183 | String::new(), |
| 4184 | tr(locale, MessageId::SetupRuntimePostureBoundary).to_string(), |
| 4185 | tr(locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(), |
| 4186 | tr(locale, MessageId::SetupRuntimePresetApplyHint).to_string(), |
| 4187 | ]); |
| 4188 | lines.join("\n") |
| 4189 | } |
| 4190 | |
| 4191 | fn runtime_preset_diff_rows(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> Vec<String> { |
| 4192 | let approval_target = preset.approval_policy().map_or_else( |
| 4193 | || "removed; Full Access comes from settings.permission_posture".to_string(), |
| 4194 | ToString::to_string, |
| 4195 | ); |
| 4196 | let mut rows = vec![ |
| 4197 | format!( |
| 4198 | "settings.default_mode: {} -> {}", |
| 4199 | facts.default_mode, |
| 4200 | preset.display_mode() |
| 4201 | ), |
| 4202 | format!( |
| 4203 | "settings.permission_posture: -> {}", |
| 4204 | preset.permission_posture() |
| 4205 | ), |
| 4206 | format!( |
| 4207 | "config.approval_policy: {} -> {}", |
| 4208 | facts.approval_policy_value, approval_target |
| 4209 | ), |
| 4210 | format!( |
| 4211 | "config.allow_shell: {} -> {}", |
| 4212 | facts.allow_shell_enabled, |
| 4213 | preset.allow_shell() |
| 4214 | ), |
| 4215 | format!( |
| 4216 | "config.sandbox_mode: {} -> {}", |
| 4217 | facts.sandbox_mode_value, |
| 4218 | preset.sandbox_mode() |
| 4219 | ), |
| 4220 | format!( |
| 4221 | "config.network.default: {} -> unchanged", |
| 4222 | facts.network_default_value |
| 4223 | ), |
| 4224 | format!("workspace trust: {} -> unchanged", facts.trust), |
| 4225 | ]; |
| 4226 | if let Some(warning) = facts.project_override_warning.as_deref() { |
| 4227 | rows.push(format!("project override warning: {warning}")); |
| 4228 | } |
| 4229 | rows |
| 4230 | } |
| 4231 | |
| 4232 | fn project_runtime_override_warning(workspace: &Path, locale: Locale) -> Option<String> { |
| 4233 | let outcome = codewhale_config::load_project_config_outcome(workspace); |
| 4234 | // A project config that exists but can't be parsed is not the same as no |
| 4235 | // project config: its restrictions are silently not in effect, and the |
| 4236 | // workspace falls back to the user's baseline. Say so here rather than |
| 4237 | // only in a log line the TUI never shows. |
| 4238 | if let Some((path, reason)) = outcome.invalid() { |
| 4239 | let path = path.display(); |
| 4240 | return Some(match locale { |
| 4241 | Locale::ZhHans => format!( |
| 4242 | "无法解析项目配置 {path}({reason})。此工作区的项目级运行姿态限制未生效,将回退到用户默认值。", |
| 4243 | ), |
| 4244 | _ => format!( |
| 4245 | "Project config {path} could not be parsed ({reason}). Its runtime posture restrictions are NOT in effect; this workspace falls back to your user defaults.", |
| 4246 | ), |
| 4247 | }); |
| 4248 | } |
| 4249 | let project = outcome.into_config()?; |
| 4250 | let mut fields = Vec::new(); |
| 4251 | if let Some(policy) = project.approval_policy.as_deref() { |
| 4252 | fields.push(format!("approval_policy={policy}")); |
| 4253 | } |
| 4254 | if let Some(mode) = project.sandbox_mode.as_deref() { |
| 4255 | fields.push(format!("sandbox_mode={mode}")); |
| 4256 | } |
| 4257 | if fields.is_empty() { |
| 4258 | return None; |
| 4259 | } |
| 4260 | Some(match locale { |
| 4261 | Locale::ZhHans => format!( |
| 4262 | "此工作区的项目配置包含 {}。预设会保存用户默认值;项目配置仍可在此工作区收紧运行姿态。", |
| 4263 | fields.join(", ") |
| 4264 | ), |
| 4265 | _ => format!( |
| 4266 | "Project config contains {}. Presets save user defaults; project config can still tighten runtime posture in this workspace.", |
| 4267 | fields.join(", ") |
| 4268 | ), |
| 4269 | }) |
| 4270 | } |
| 4271 | |
| 4272 | fn setup_report_result(state: &SetupState, facts: &SetupRuntimeFacts) -> String { |
| 4273 | format!( |
| 4274 | "first_run={}, update={}, operate={}, constitution={:?}, autonomy={}, posture={:?}, runtime={}, operate_fleet={}", |
| 4275 | if state.first_run_ready() { |
| 4276 | "ready" |
| 4277 | } else { |
| 4278 | "needs_action" |
| 4279 | }, |
| 4280 | if state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) { |
| 4281 | "ready" |
| 4282 | } else { |
| 4283 | "needs_action" |
| 4284 | }, |
| 4285 | if state.operate_ready() { |
| 4286 | "ready" |
| 4287 | } else { |
| 4288 | "needs_action" |
| 4289 | }, |
| 4290 | state.constitution_choice, |
| 4291 | facts.constitution_autonomy, |
| 4292 | state.runtime_posture_source, |
| 4293 | facts.runtime_result, |
| 4294 | facts.operate_result |
| 4295 | ) |
| 4296 | } |
| 4297 | |
| 4298 | fn remote_runtime_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String { |
| 4299 | remote::on_ramp_text( |
| 4300 | locale, |
| 4301 | &facts.remote_clouds_result, |
| 4302 | &facts.remote_bridges_result, |
| 4303 | &facts.remote_providers_result, |
| 4304 | &facts.remote_mode_result, |
| 4305 | &facts.remote_command_provider, |
| 4306 | ) |
| 4307 | } |
| 4308 | |
| 4309 | fn tools_mcp_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String { |
| 4310 | let tools_facts = tools_mcp::SetupToolsMcpFacts { |
| 4311 | servers_result: facts.tools_mcp_servers_result.clone(), |
| 4312 | skills_result: facts.tools_mcp_skills_result.clone(), |
| 4313 | tools_result: facts.tools_mcp_tools_result.clone(), |
| 4314 | plugins_result: facts.tools_mcp_plugins_result.clone(), |
| 4315 | hotbar_result: facts.tools_mcp_hotbar_result.clone(), |
| 4316 | dsh_result: facts.tools_mcp_dsh_result.clone(), |
| 4317 | result: facts.tools_mcp_result.clone(), |
| 4318 | overall_status: if facts.tools_mcp_needs_action { |
| 4319 | tools_mcp::InventoryStatus::NeedsConfig |
| 4320 | } else if facts.tools_mcp_result.contains("overall=off") { |
| 4321 | tools_mcp::InventoryStatus::Off |
| 4322 | } else { |
| 4323 | tools_mcp::InventoryStatus::Healthy |
| 4324 | }, |
| 4325 | needs_action: facts.tools_mcp_needs_action, |
| 4326 | mcp_path_display: facts.tools_mcp_path_display.clone(), |
| 4327 | skills_path_display: facts.tools_mcp_skills_path_display.clone(), |
| 4328 | plugins_path_display: facts.tools_mcp_plugins_path_display.clone(), |
| 4329 | }; |
| 4330 | tools_mcp::on_ramp_text(locale, &tools_facts) |
| 4331 | } |
| 4332 | |
| 4333 | /// Who authored the draft being previewed for ratification. |
| 4334 | #[derive(Debug, Clone, PartialEq, Eq)] |
| 4335 | enum DraftProvenance { |
| 4336 | /// Rendered deterministically from the guided answers. |
| 4337 | Guided, |
| 4338 | /// Drafted by the named model, then sanitized and bounded by Codewhale. |
| 4339 | Model(String), |
| 4340 | /// The user's existing `constitution.json`, shown unchanged for the |
| 4341 | /// keep-existing checkpoint completion (#3794). |
| 4342 | Existing, |
| 4343 | } |
| 4344 | |
| 4345 | fn ratification_preview_title(locale: Locale) -> &'static str { |
| 4346 | match locale { |
| 4347 | Locale::Ja => "ユーザー憲法 - 批准前の草案", |
| 4348 | Locale::ZhHans => "用户宪章 — 确认前草案", |
| 4349 | Locale::ZhHant => "使用者憲法 - 批准前草案", |
| 4350 | Locale::PtBr => "Constituição do Usuário - Rascunho para Ratificação", |
| 4351 | Locale::Es419 => "Constitución del Usuario - Borrador para Ratificación", |
| 4352 | Locale::Vi => "Hiến pháp Người dùng - Bản nháp để phê chuẩn", |
| 4353 | Locale::Ko => "사용자 헌법 - 승인 전 초안", |
| 4354 | Locale::Ca => "Constitució de l'Usuari - Esborrany per a Ratificació", |
| 4355 | Locale::De => "Nutzerverfassung - Entwurf zur Ratifizierung", |
| 4356 | Locale::Fr => "Constitution de l'Utilisateur - Brouillon pour Ratification", |
| 4357 | Locale::Id => "Konstitusi Pengguna - Draf untuk Ratifikasi", |
| 4358 | Locale::Hi => "उपयोगकर्ता संविधान - अंगीकार हेतु मसौदा", |
| 4359 | Locale::Ru => "Конституция пользователя - Проект для ратификации", |
| 4360 | Locale::Uk => "Конституція користувача - Проєкт для ратифікації", |
| 4361 | _ => "User Constitution — Draft for Ratification", |
| 4362 | } |
| 4363 | } |
| 4364 | |
| 4365 | /// The ratification artifact shown in the pager: provenance, what a |
| 4366 | /// constitution is, the exact block that will be injected (byte-identical to |
| 4367 | /// prompt assembly's rendering), its authority boundaries, and how to ratify |
| 4368 | /// or amend. Only the scaffold differs between guided and model drafts — the |
| 4369 | /// law itself always comes from the same renderer. |
| 4370 | fn constitution_ratification_text( |
| 4371 | locale: Locale, |
| 4372 | constitution: &UserConstitution, |
| 4373 | provenance: &DraftProvenance, |
| 4374 | ) -> String { |
| 4375 | const RULE: &str = "──────────────────────────────────────────────────────"; |
| 4376 | let rendered = constitution |
| 4377 | .render_block(None) |
| 4378 | .unwrap_or_else(|| match locale { |
| 4379 | Locale::Ja => "構造化された憲法は空です。".to_string(), |
| 4380 | Locale::ZhHans => "结构化宪章为空。".to_string(), |
| 4381 | Locale::ZhHant => "結構化憲法為空。".to_string(), |
| 4382 | Locale::PtBr => "A constituição estruturada está vazia.".to_string(), |
| 4383 | Locale::Es419 => "La constitución estructurada está vacía.".to_string(), |
| 4384 | Locale::Vi => "Hiến pháp có cấu trúc đang trống.".to_string(), |
| 4385 | Locale::Ko => "구조화된 헌법이 비어 있습니다.".to_string(), |
| 4386 | Locale::Ca => "La constitució estructurada és buida.".to_string(), |
| 4387 | Locale::De => "Die strukturierte Verfassung ist leer.".to_string(), |
| 4388 | Locale::Fr => "La constitution structurée est vide.".to_string(), |
| 4389 | Locale::Id => "Konstitusi terstruktur kosong.".to_string(), |
| 4390 | Locale::Hi => "संरचित संविधान खाली है।".to_string(), |
| 4391 | Locale::Ru => "Структурированная конституция пуста.".to_string(), |
| 4392 | Locale::Uk => "Структурована конституція порожня.".to_string(), |
| 4393 | _ => "The structured constitution is empty.".to_string(), |
| 4394 | }); |
| 4395 | let layer_order = tr(locale, MessageId::SetupCheckpointLayerOrder); |
| 4396 | |
| 4397 | match locale { |
| 4398 | Locale::Ja => { |
| 4399 | let drafted_by = match provenance { |
| 4400 | DraftProvenance::Model(label) => format!( |
| 4401 | "{label} があなたのガイド回答から起草し、Codewhale が構造検証と境界制限を適用しました。" |
| 4402 | ), |
| 4403 | DraftProvenance::Guided => { |
| 4404 | "あなたのガイド回答から決定的に生成されました。".to_string() |
| 4405 | } |
| 4406 | DraftProvenance::Existing => { |
| 4407 | "既存の憲法を constitution.json から読み込み、変更せずに表示しています。" |
| 4408 | .to_string() |
| 4409 | } |
| 4410 | }; |
| 4411 | let ratify_how = match provenance { |
| 4412 | DraftProvenance::Existing => { |
| 4413 | "これはすでに有効な基準です。プレビューを閉じて K を押すと、このまま保持してチェックポイントを完了します。\ |
| 4414 | ファイルは変更されません。/constitution または /setup でいつでも修正できます。" |
| 4415 | } |
| 4416 | _ => { |
| 4417 | "確認するまで、どの内容も基準にはなりません。プレビューを閉じて G を押すと批准して保存します。\ |
| 4418 | /constitution または /setup でいつでも修正できます。" |
| 4419 | } |
| 4420 | }; |
| 4421 | format!( |
| 4422 | "CODEWHALE · ユーザー憲法\n{RULE}\n\n{drafted_by}\n\n\ |
| 4423 | これは Codewhale があなたと協働するための常設の基準です。優れた憲法のように、使えるほど短く、\ |
| 4424 | 網羅的な規則ではなく持続する原則で構成され、あなたの変化に合わせて修正できます。\ |
| 4425 | すべての個別判断を裁くのではなく権限と境界を定め、セッションを越えて協働を継続させます。\ |
| 4426 | ただしこれは記憶ではありません。履歴ではなく原則を保持します。\n\n\ |
| 4427 | {rendered}\n\n\ |
| 4428 | 権限の階層\n{layer_order}\nあなたの直接の指示は常にこの文書より優先されます。\n\n\ |
| 4429 | これができないこと\n\ |
| 4430 | これは行動を導くものです。承認ポリシー、サンドボックス、Shell、ネットワーク、信頼、MCP 権限、\ |
| 4431 | 既定モード、公開、支出の権限を付与または変更することはできません。これらは実行時にあなたが管理します。\n\n\ |
| 4432 | 縮小コアと任意モジュール\n\ |
| 4433 | 組み込みのコアは引き続き有効です。この草案はユーザーグローバルの長期設定だけを保存します。\ |
| 4434 | 実行とオーケストレーションの機能は、ランタイムポリシー、現在のツールカタログ、または将来の任意モジュールから提供されます。このプレビューはモジュールを有効化せず、設定も変更しません。\n\n\ |
| 4435 | 批准\n{ratify_how}" |
| 4436 | ) |
| 4437 | } |
| 4438 | Locale::ZhHans => { |
| 4439 | let drafted_by = match provenance { |
| 4440 | DraftProvenance::Model(label) => format!( |
| 4441 | "由 {label} 根据你的引导式答案起草,并已由 Codewhale 完成结构校验与边界限制。" |
| 4442 | ), |
| 4443 | DraftProvenance::Guided => "由你的引导式答案确定性生成。".to_string(), |
| 4444 | DraftProvenance::Existing => { |
| 4445 | "你现有的宪章,读取自 constitution.json——原样展示,未做任何修改。".to_string() |
| 4446 | } |
| 4447 | }; |
| 4448 | let ratify_how = match provenance { |
| 4449 | DraftProvenance::Existing => { |
| 4450 | "这已是你当前使用的宪章。关闭此预览后按 K 保留并完成检查点——文件不会被修改。\ |
| 4451 | 之后可随时用 /constitution 或 /setup 修改。" |
| 4452 | } |
| 4453 | _ => { |
| 4454 | "未经你确认,任何内容都不会成为宪章。关闭此预览后按 G 确认并保存;\ |
| 4455 | 之后可随时用 /constitution 或 /setup 修改。" |
| 4456 | } |
| 4457 | }; |
| 4458 | format!( |
| 4459 | "CODEWHALE · 用户宪章\n{RULE}\n\n{drafted_by}\n\n\ |
| 4460 | 这是 Codewhale 与你协作时长期遵循的偏好和规则。内容应保持简短、便于执行,以持久原则为主,并可随时调整。\ |
| 4461 | 它界定协作方式与行为边界,而不是替你决定每一种情况;它让协作跨会话延续——但它不是记忆,只保留原则,不保留历史。\n\n\ |
| 4462 | {rendered}\n\n\ |
| 4463 | 权限层级\n{layer_order}\n你的直接指令始终高于本文件。\n\n\ |
| 4464 | 它不能做什么\n\ |
| 4465 | 它只提供行为指导,不能授予或更改审批策略、沙箱、Shell、网络、信任、MCP 权限、默认模式、发布或支出权限——这些始终由你在运行时掌控。\n\n\ |
| 4466 | 精简核心与可选策略\n\ |
| 4467 | 内置核心始终生效。本草案只保存你的用户全局长期偏好。执行与编排能力来自运行时策略、当前工具目录或未来的可选规则包;此预览不会启用任何策略或更改配置。\n\n\ |
| 4468 | 确认\n{ratify_how}" |
| 4469 | ) |
| 4470 | } |
| 4471 | Locale::ZhHant => { |
| 4472 | let drafted_by = match provenance { |
| 4473 | DraftProvenance::Model(label) => format!( |
| 4474 | "由 {label} 根據你的引導式答案起草,並已由 Codewhale 完成結構驗證與邊界限制。" |
| 4475 | ), |
| 4476 | DraftProvenance::Guided => "由你的引導式答案確定性生成。".to_string(), |
| 4477 | DraftProvenance::Existing => { |
| 4478 | "你現有的憲法,讀取自 constitution.json;原樣展示,未做任何修改。".to_string() |
| 4479 | } |
| 4480 | }; |
| 4481 | let ratify_how = match provenance { |
| 4482 | DraftProvenance::Existing => { |
| 4483 | "這已是你現行的準則。關閉此預覽後按 K 保留並完成檢查點;\ |
| 4484 | 檔案不會被修改。之後可隨時用 /constitution 或 /setup 修訂。" |
| 4485 | } |
| 4486 | _ => { |
| 4487 | "未經你確認,任何內容都不會成為準則。關閉此預覽後按 G 批准並保存;\ |
| 4488 | 之後可隨時用 /constitution 或 /setup 修訂。" |
| 4489 | } |
| 4490 | }; |
| 4491 | format!( |
| 4492 | "CODEWHALE · 使用者憲法\n{RULE}\n\n{drafted_by}\n\n\ |
| 4493 | 這是 Codewhale 與你協作的長期準則。像優秀的憲法一樣:足夠簡短因而可用,由持久原則而非詳盡規則構成,並且可以隨你修訂。\ |
| 4494 | 它界定權力與邊界,而非裁決每個具體決定;它讓協作跨會話延續,但它不是記憶,它承載的是原則,而非歷史。\n\n\ |
| 4495 | {rendered}\n\n\ |
| 4496 | 權限層級\n{layer_order}\n你的直接指令始終高於本文件。\n\n\ |
| 4497 | 它不能做什麼\n\ |
| 4498 | 它只提供行為指導,不能授予或更改審批策略、沙箱、Shell、網路、信任、MCP 權限、預設模式、發布或支出權限;這些始終由你在執行時掌控。\n\n\ |
| 4499 | 精簡核心與可選模組\n\ |
| 4500 | 內建核心始終生效。本草案只保存你的使用者全域長期偏好。執行與編排能力由執行時政策、即時工具目錄或未來的可選模組提供;此預覽不會啟用模組或更改其配置。\n\n\ |
| 4501 | 批准\n{ratify_how}" |
| 4502 | ) |
| 4503 | } |
| 4504 | Locale::PtBr => { |
| 4505 | let drafted_by = match provenance { |
| 4506 | DraftProvenance::Model(label) => format!( |
| 4507 | "Rascunhado por {label} a partir das suas respostas guiadas, depois validado por schema e limitado pelo Codewhale." |
| 4508 | ), |
| 4509 | DraftProvenance::Guided => { |
| 4510 | "Renderizado deterministicamente a partir das suas respostas guiadas.".to_string() |
| 4511 | } |
| 4512 | DraftProvenance::Existing => { |
| 4513 | "Sua constituição existente, carregada de constitution.json, é exibida sem alterações." |
| 4514 | .to_string() |
| 4515 | } |
| 4516 | }; |
| 4517 | let ratify_how = match provenance { |
| 4518 | DraftProvenance::Existing => { |
| 4519 | "Esta já é sua regra vigente. Feche a prévia e pressione K para mantê-la e concluir o checkpoint; \ |
| 4520 | o arquivo não será modificado. Edite quando quiser com /constitution ou /setup." |
| 4521 | } |
| 4522 | _ => { |
| 4523 | "Nada vira regra até você confirmar. Feche a prévia e pressione G para ratificar e salvar. \ |
| 4524 | Edite quando quiser com /constitution ou /setup." |
| 4525 | } |
| 4526 | }; |
| 4527 | format!( |
| 4528 | "CODEWHALE · CONSTITUIÇÃO DO USUÁRIO\n{RULE}\n\n{drafted_by}\n\n\ |
| 4529 | Esta é a regra permanente de como o Codewhale trabalha com você. Como boas constituições, \ |
| 4530 | ela é curta o bastante para ser usada, formada por princípios duráveis em vez de regras exaustivas, \ |
| 4531 | e pode ser emendada conforme você muda. Ela define poderes e limites em vez de decidir cada caso, \ |
| 4532 | e dá continuidade à colaboração entre sessões. Mas ela não é memória: carrega princípios, não histórico.\n\n\ |
| 4533 | {rendered}\n\n\ |
| 4534 | HIERARQUIA DE AUTORIDADE\n{layer_order}\nSeus pedidos diretos sempre superam este documento.\n\n\ |
| 4535 | O QUE ISTO NÃO PODE FAZER\n\ |
| 4536 | Isto orienta comportamento. Não pode conceder nem alterar política de aprovação, sandbox, shell, rede, \ |
| 4537 | 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\ |
| 4538 | NÚCLEO REDUZIDO E MÓDULOS OPT-IN\n\ |
| 4539 | O núcleo embutido continua ativo. Este rascunho só salva suas preferências permanentes globais de usuário. \ |
| 4540 | Capacidades de execução e orquestração vêm da política de execução, do catálogo de ferramentas ativo ou de módulos opt-in futuros; esta prévia não ativa módulos nem muda sua configuração.\n\n\ |
| 4541 | RATIFICAÇÃO\n{ratify_how}" |
| 4542 | ) |
| 4543 | } |
| 4544 | Locale::Es419 => { |
| 4545 | let drafted_by = match provenance { |
| 4546 | DraftProvenance::Model(label) => format!( |
| 4547 | "Redactado por {label} desde tus respuestas guiadas, luego validado por schema y acotado por Codewhale." |
| 4548 | ), |
| 4549 | DraftProvenance::Guided => { |
| 4550 | "Renderizado de forma determinística desde tus respuestas guiadas.".to_string() |
| 4551 | } |
| 4552 | DraftProvenance::Existing => { |
| 4553 | "Tu constitución existente, cargada desde constitution.json, se muestra sin cambios." |
| 4554 | .to_string() |
| 4555 | } |
| 4556 | }; |
| 4557 | let ratify_how = match provenance { |
| 4558 | DraftProvenance::Existing => { |
| 4559 | "Esta ya es tu regla vigente. Cierra la vista previa y presiona K para conservarla y completar el checkpoint; \ |
| 4560 | el archivo no se modifica. Puedes enmendarla cuando quieras con /constitution o /setup." |
| 4561 | } |
| 4562 | _ => { |
| 4563 | "Nada se vuelve regla hasta que confirmes. Cierra la vista previa y presiona G para ratificar y guardar. \ |
| 4564 | Puedes enmendarla cuando quieras con /constitution o /setup." |
| 4565 | } |
| 4566 | }; |
| 4567 | format!( |
| 4568 | "CODEWHALE · CONSTITUCIÓN DEL USUARIO\n{RULE}\n\n{drafted_by}\n\n\ |
| 4569 | Esta es la regla permanente de cómo Codewhale trabaja contigo. Como las buenas constituciones, \ |
| 4570 | es lo bastante breve para usarse, hecha de principios duraderos en vez de reglas exhaustivas, \ |
| 4571 | y enmendable a medida que cambias. Define poderes y límites en vez de decidir cada caso, \ |
| 4572 | y da continuidad a la colaboración entre sesiones. Pero no es memoria: lleva principios, no historial.\n\n\ |
| 4573 | {rendered}\n\n\ |
| 4574 | JERARQUÍA DE AUTORIDAD\n{layer_order}\nTus pedidos directos siempre superan este documento.\n\n\ |
| 4575 | LO QUE ESTO NO PUEDE HACER\n\ |
| 4576 | Orienta comportamiento. No puede conceder ni cambiar política de aprobación, sandbox, shell, red, \ |
| 4577 | confianza, permisos MCP, modo predeterminado, publicación o autoridad de gasto; eso sigue bajo tu control en tiempo de ejecución.\n\n\ |
| 4578 | NÚCLEO REDUCIDO Y MÓDULOS OPT-IN\n\ |
| 4579 | El núcleo integrado sigue activo. Este borrador solo guarda tus preferencias permanentes globales de usuario. \ |
| 4580 | Las capacidades de ejecución y orquestación provienen de la política de ejecución, el catálogo activo de herramientas o módulos opt-in futuros; esta vista previa no activa módulos ni cambia su configuración.\n\n\ |
| 4581 | RATIFICACIÓN\n{ratify_how}" |
| 4582 | ) |
| 4583 | } |
| 4584 | Locale::Vi => { |
| 4585 | let drafted_by = match provenance { |
| 4586 | DraftProvenance::Model(label) => format!( |
| 4587 | "Đượ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." |
| 4588 | ), |
| 4589 | DraftProvenance::Guided => { |
| 4590 | "Được kết xuất xác định từ câu trả lời hướng dẫn của bạn.".to_string() |
| 4591 | } |
| 4592 | DraftProvenance::Existing => { |
| 4593 | "Hiến pháp hiện có của bạn, tải từ constitution.json, được hiển thị nguyên trạng." |
| 4594 | .to_string() |
| 4595 | } |
| 4596 | }; |
| 4597 | let ratify_how = match provenance { |
| 4598 | DraftProvenance::Existing => { |
| 4599 | "Đâ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; \ |
| 4600 | tệp không bị sửa. Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup." |
| 4601 | } |
| 4602 | _ => { |
| 4603 | "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. \ |
| 4604 | Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup." |
| 4605 | } |
| 4606 | }; |
| 4607 | format!( |
| 4608 | "CODEWHALE · HIẾN PHÁP NGƯỜI DÙNG\n{RULE}\n\n{drafted_by}\n\n\ |
| 4609 | Đâ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, \ |
| 4610 | 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, \ |
| 4611 | 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, \ |
| 4612 | đồ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\ |
| 4613 | {rendered}\n\n\ |
| 4614 | 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\ |
| 4615 | ĐIỀU NÀY KHÔNG THỂ LÀM\n\ |
| 4616 | 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, \ |
| 4617 | độ 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\ |
| 4618 | LÕI RÚT GỌN VÀ MÔ-ĐUN OPT-IN\n\ |
| 4619 | 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. \ |
| 4620 | Khả năng thực thi và điều phối đến từ chính sách thời gian chạy, danh mục công cụ đang hoạt động 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\ |
| 4621 | PHÊ CHUẨN\n{ratify_how}" |
| 4622 | ) |
| 4623 | } |
| 4624 | Locale::Ko => { |
| 4625 | let drafted_by = match provenance { |
| 4626 | DraftProvenance::Model(label) => format!( |
| 4627 | "{label}이(가) 당신의 가이드 답변을 바탕으로 초안을 작성했고, Codewhale이 구조를 검증하고 범위를 제한했습니다." |
| 4628 | ), |
| 4629 | DraftProvenance::Guided => { |
| 4630 | "당신의 가이드 답변으로부터 결정적으로 생성되었습니다.".to_string() |
| 4631 | } |
| 4632 | DraftProvenance::Existing => { |
| 4633 | "constitution.json에서 불러온 기존 헌법이며, 변경 없이 그대로 표시됩니다." |
| 4634 | .to_string() |
| 4635 | } |
| 4636 | }; |
| 4637 | let ratify_how = match provenance { |
| 4638 | DraftProvenance::Existing => { |
| 4639 | "이것은 이미 당신의 상시 규칙입니다. 미리보기를 닫고 K를 눌러 그대로 유지하며 체크포인트를 완료하세요; \ |
| 4640 | 파일은 수정되지 않습니다. /constitution 또는 /setup으로 언제든지 수정할 수 있습니다." |
| 4641 | } |
| 4642 | _ => { |
| 4643 | "확인하기 전까지는 아무것도 규칙이 되지 않습니다. 미리보기를 닫고 G를 눌러 승인하고 저장하세요. \ |
| 4644 | /constitution 또는 /setup으로 언제든지 수정할 수 있습니다." |
| 4645 | } |
| 4646 | }; |
| 4647 | format!( |
| 4648 | "CODEWHALE · 사용자 헌법\n{RULE}\n\n{drafted_by}\n\n\ |
| 4649 | 이것은 Codewhale이 당신과 함께 일하는 방식에 대한 상시 규칙입니다. 훌륭한 헌법이 그렇듯, \ |
| 4650 | 사용할 수 있을 만큼 짧고, 소모적인 규칙이 아닌 지속적인 원칙으로 이루어져 있으며, 당신이 변화함에 따라 수정할 수 있습니다. \ |
| 4651 | 이는 모든 개별 사례를 판단하는 대신 권한과 한계를 규정하며, 세션을 넘어 협업의 연속성을 부여합니다. \ |
| 4652 | 다만 이것은 기억이 아닙니다: 이력이 아니라 원칙을 담습니다.\n\n\ |
| 4653 | {rendered}\n\n\ |
| 4654 | 권한 계층\n{layer_order}\n당신의 직접적인 요청은 언제나 이 문서보다 우선합니다.\n\n\ |
| 4655 | 이것이 할 수 없는 일\n\ |
| 4656 | 이것은 행동을 안내할 뿐입니다. 승인 정책, 샌드박스, 셸, 네트워크, 신뢰, MCP 권한, 기본 모드, 게시, 지출 권한을 \ |
| 4657 | 부여하거나 바꿀 수 없습니다; 이는 여전히 런타임에서 당신이 직접 관리합니다.\n\n\ |
| 4658 | 축소된 코어와 옵트인 모듈\n\ |
| 4659 | 내장된 코어는 계속 활성 상태입니다. 이 초안은 사용자 전역의 상시 선호만 저장합니다. \ |
| 4660 | 실행 및 오케스트레이션 기능은 런타임 정책, 현재 도구 카탈로그 또는 향후 옵트인 모듈에서 제공됩니다. 이 미리보기는 모듈을 활성화하지 않으며 그 설정도 바꾸지 않습니다.\n\n\ |
| 4661 | 승인\n{ratify_how}" |
| 4662 | ) |
| 4663 | } |
| 4664 | Locale::Ca => { |
| 4665 | let drafted_by = match provenance { |
| 4666 | DraftProvenance::Model(label) => format!( |
| 4667 | "Redactat per {label} a partir de les teves respostes guiades, després validat per esquema i acotat per Codewhale." |
| 4668 | ), |
| 4669 | DraftProvenance::Guided => { |
| 4670 | "Generat determinísticament a partir de les teves respostes guiades.".to_string() |
| 4671 | } |
| 4672 | DraftProvenance::Existing => { |
| 4673 | "La teva constitució existent, carregada de constitution.json, es mostra sense canvis." |
| 4674 | .to_string() |
| 4675 | } |
| 4676 | }; |
| 4677 | let ratify_how = match provenance { |
| 4678 | DraftProvenance::Existing => { |
| 4679 | "Aquesta ja és la teva llei vigent. Tanca la previsualització i prem K per conservar-la i completar el punt de control; \ |
| 4680 | el fitxer no es modifica. Esmena-la en qualsevol moment amb /constitution o /setup." |
| 4681 | } |
| 4682 | _ => { |
| 4683 | "Res no esdevé llei fins que ho confirmis. Tanca la previsualització i prem G per ratificar i desar. \ |
| 4684 | Esmena-la en qualsevol moment amb /constitution o /setup." |
| 4685 | } |
| 4686 | }; |
| 4687 | format!( |
| 4688 | "CODEWHALE · CONSTITUCIÓ DE L'USUARI\n{RULE}\n\n{drafted_by}\n\n\ |
| 4689 | Aquesta és la llei permanent de com Codewhale treballa amb tu. Com les bones constitucions, \ |
| 4690 | és prou curta per usar-se, feta de principis duradors en lloc de regles exhaustives, \ |
| 4691 | i esmenable a mesura que canvies. Defineix poders i límits en lloc de decidir cada cas, \ |
| 4692 | i dona continuïtat a la col·laboració entre sessions — però no és memòria: porta principis, no història.\n\n\ |
| 4693 | {rendered}\n\n\ |
| 4694 | JERARQUIA D'AUTORITAT\n{layer_order}\nLes teves peticions directes sempre prevalen sobre aquest document.\n\n\ |
| 4695 | EL QUE AIXÒ NO POT FER\n\ |
| 4696 | Orienta el comportament. No pot concedir ni canviar la política d'aprovació, sandbox, shell, xarxa, \ |
| 4697 | confiança, permisos MCP, mode per defecte, publicació o autoritat de despesa; això queda sota el teu control en temps d'execució.\n\n\ |
| 4698 | NUCLI REDUÏT I MÒDULS OPT-IN\n\ |
| 4699 | El nucli inclòs continua actiu. Aquest esborrany només desa les teves preferències permanents globals d'usuari. \ |
| 4700 | Les capacitats d'execució i orquestració provenen de la política d'execució, el catàleg d'eines actiu o futurs mòduls opt-in; aquesta previsualització no activa mòduls ni canvia la seva configuració.\n\n\ |
| 4701 | RATIFICACIÓ\n{ratify_how}" |
| 4702 | ) |
| 4703 | } |
| 4704 | Locale::De => { |
| 4705 | let drafted_by = match provenance { |
| 4706 | DraftProvenance::Model(label) => format!( |
| 4707 | "Entworfen von {label} aus deinen geführten Antworten, dann schema-geprüft und begrenzt durch Codewhale." |
| 4708 | ), |
| 4709 | DraftProvenance::Guided => { |
| 4710 | "Deterministisch aus deinen geführten Antworten erzeugt.".to_string() |
| 4711 | } |
| 4712 | DraftProvenance::Existing => { |
| 4713 | "Deine bestehende Verfassung, geladen aus constitution.json — unverändert gezeigt." |
| 4714 | .to_string() |
| 4715 | } |
| 4716 | }; |
| 4717 | let ratify_how = match provenance { |
| 4718 | DraftProvenance::Existing => { |
| 4719 | "Dies ist bereits dein geltendes Recht. Schließe die Vorschau und drücke K, um sie zu behalten und den Checkpoint abzuschließen — \ |
| 4720 | die Datei wird nicht verändert. Jederzeit mit /constitution oder /setup änderbar." |
| 4721 | } |
| 4722 | _ => { |
| 4723 | "Nichts wird Recht, bevor du bestätigst. Schließe die Vorschau und drücke G, um zu ratifizieren und zu speichern. \ |
| 4724 | Jederzeit mit /constitution oder /setup änderbar." |
| 4725 | } |
| 4726 | }; |
| 4727 | format!( |
| 4728 | "CODEWHALE · NUTZERVERFASSUNG\n{RULE}\n\n{drafted_by}\n\n\ |
| 4729 | Dies ist das geltende Gesetz dafür, wie Codewhale mit dir arbeitet. Wie die besten Verfassungen \ |
| 4730 | ist sie kurz genug, um genutzt zu werden, besteht aus dauerhaften Prinzipien statt erschöpfender Regeln \ |
| 4731 | und lässt sich ändern, wenn du dich änderst. Sie rahmt Befugnisse und Grenzen, statt jeden Einzelfall zu entscheiden, \ |
| 4732 | und gibt deiner Zusammenarbeit Kontinuität über Sitzungen hinweg — aber sie ist kein Gedächtnis: Sie trägt Prinzipien, nicht Geschichte.\n\n\ |
| 4733 | {rendered}\n\n\ |
| 4734 | HIERARCHIE DER AUTORITÄT\n{layer_order}\nDeine direkten Anweisungen stehen immer über diesem Dokument.\n\n\ |
| 4735 | WAS DIES NICHT KANN\n\ |
| 4736 | Sie leitet Verhalten. Sie kann keine Freigaberichtlinie, Sandbox, Shell, Netzwerk, \ |
| 4737 | Vertrauen, MCP-Berechtigungen, Standardmodus, Veröffentlichung oder Ausgabenbefugnis gewähren oder ändern — die bleiben zur Laufzeit in deiner Hand.\n\n\ |
| 4738 | REDUZIERTER KERN UND OPT-IN-MODULE\n\ |
| 4739 | Der mitgelieferte Kern bleibt aktiv. Dieser Entwurf speichert nur deine benutzer-globalen Dauerpräferenzen. \ |
| 4740 | Ausführungs- und Orchestrierungsfähigkeiten kommen aus der Laufzeitrichtlinie, dem aktuellen Werkzeugkatalog oder künftigen Opt-in-Modulen; diese Vorschau aktiviert keine Module und ändert nicht ihre Konfiguration.\n\n\ |
| 4741 | RATIFIZIERUNG\n{ratify_how}" |
| 4742 | ) |
| 4743 | } |
| 4744 | Locale::Fr => { |
| 4745 | let drafted_by = match provenance { |
| 4746 | DraftProvenance::Model(label) => format!( |
| 4747 | "Rédigé par {label} à partir de vos réponses guidées, puis validé par schéma et borné par Codewhale." |
| 4748 | ), |
| 4749 | DraftProvenance::Guided => { |
| 4750 | "Généré de façon déterministe à partir de vos réponses guidées.".to_string() |
| 4751 | } |
| 4752 | DraftProvenance::Existing => { |
| 4753 | "Votre constitution existante, chargée depuis constitution.json — affichée sans modification." |
| 4754 | .to_string() |
| 4755 | } |
| 4756 | }; |
| 4757 | let ratify_how = match provenance { |
| 4758 | DraftProvenance::Existing => { |
| 4759 | "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 — \ |
| 4760 | le fichier n'est pas modifié. Amendez-la à tout moment avec /constitution ou /setup." |
| 4761 | } |
| 4762 | _ => { |
| 4763 | "Rien ne devient loi avant votre confirmation. Fermez cet aperçu, puis appuyez sur G pour ratifier et enregistrer. \ |
| 4764 | Amendez-la à tout moment avec /constitution ou /setup." |
| 4765 | } |
| 4766 | }; |
| 4767 | format!( |
| 4768 | "CODEWHALE · CONSTITUTION DE L'UTILISATEUR\n{RULE}\n\n{drafted_by}\n\n\ |
| 4769 | Voici la loi permanente qui régit la façon dont Codewhale travaille avec vous. Comme les meilleures constitutions, \ |
| 4770 | elle est assez courte pour être utilisée, faite de principes durables plutôt que de règles exhaustives, \ |
| 4771 | et amendable à mesure que vous changez. Elle encadre les pouvoirs et les limites plutôt que de trancher chaque cas, \ |
| 4772 | 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\ |
| 4773 | {rendered}\n\n\ |
| 4774 | HIÉRARCHIE D'AUTORITÉ\n{layer_order}\nVos demandes directes priment toujours sur ce document.\n\n\ |
| 4775 | CE QU'ELLE NE PEUT PAS FAIRE\n\ |
| 4776 | Elle guide le comportement. Elle ne peut ni accorder ni modifier la politique d'approbation, le sandbox, le shell, le réseau, \ |
| 4777 | 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\ |
| 4778 | NOYAU RÉDUIT ET MODULES OPT-IN\n\ |
| 4779 | Le noyau intégré reste actif. Ce brouillon n'enregistre que vos préférences permanentes globales. \ |
| 4780 | Les capacités d'exécution et d'orchestration proviennent de la politique d'exécution, du catalogue d'outils actif ou de futurs modules opt-in ; cet aperçu n'active pas de modules et ne change pas leur configuration.\n\n\ |
| 4781 | RATIFICATION\n{ratify_how}" |
| 4782 | ) |
| 4783 | } |
| 4784 | Locale::Id => { |
| 4785 | let drafted_by = match provenance { |
| 4786 | DraftProvenance::Model(label) => format!( |
| 4787 | "Disusun oleh {label} dari jawaban terpandu Anda, lalu diperiksa skemanya dan dibatasi oleh Codewhale." |
| 4788 | ), |
| 4789 | DraftProvenance::Guided => { |
| 4790 | "Dihasilkan secara deterministik dari jawaban terpandu Anda.".to_string() |
| 4791 | } |
| 4792 | DraftProvenance::Existing => { |
| 4793 | "Konstitusi Anda yang ada, dimuat dari constitution.json — ditampilkan tanpa perubahan." |
| 4794 | .to_string() |
| 4795 | } |
| 4796 | }; |
| 4797 | let ratify_how = match provenance { |
| 4798 | DraftProvenance::Existing => { |
| 4799 | "Ini sudah menjadi hukum tetap Anda. Tutup pratinjau ini, lalu tekan K untuk mempertahankannya dan menyelesaikan checkpoint — \ |
| 4800 | file tidak diubah. Amendemen kapan saja dengan /constitution atau /setup." |
| 4801 | } |
| 4802 | _ => { |
| 4803 | "Tidak ada yang menjadi hukum sampai Anda mengonfirmasi. Tutup pratinjau ini, lalu tekan G untuk meratifikasi dan menyimpan. \ |
| 4804 | Amendemen kapan saja dengan /constitution atau /setup." |
| 4805 | } |
| 4806 | }; |
| 4807 | format!( |
| 4808 | "CODEWHALE · KONSTITUSI PENGGUNA\n{RULE}\n\n{drafted_by}\n\n\ |
| 4809 | Ini adalah hukum tetap tentang cara Codewhale bekerja dengan Anda. Seperti konstitusi terbaik, \ |
| 4810 | ia cukup singkat untuk dipakai, tersusun dari prinsip yang awet alih-alih aturan yang menyeluruh, \ |
| 4811 | dan dapat diamendemen seiring Anda berubah. Ia membingkai wewenang dan batasan alih-alih memutuskan setiap kasus, \ |
| 4812 | dan memberi kolaborasi Anda kesinambungan lintas sesi — tetapi ia bukan memori: ia membawa prinsip, bukan riwayat.\n\n\ |
| 4813 | {rendered}\n\n\ |
| 4814 | HIERARKI OTORITAS\n{layer_order}\nPermintaan langsung Anda selalu mengungguli dokumen ini.\n\n\ |
| 4815 | APA YANG TIDAK BISA DILAKUKANNYA\n\ |
| 4816 | Ia memandu perilaku. Ia tidak dapat memberi atau mengubah kebijakan persetujuan, sandbox, shell, jaringan, \ |
| 4817 | kepercayaan, izin MCP, mode default, publikasi, atau wewenang belanja — semua itu tetap di tangan Anda saat runtime.\n\n\ |
| 4818 | INTI RINGKAS DAN MODUL OPT-IN\n\ |
| 4819 | Inti bawaan tetap aktif. Draf ini hanya menyimpan preferensi tetap global pengguna Anda. \ |
| 4820 | Kemampuan eksekusi dan orkestrasi berasal dari kebijakan runtime, katalog alat aktif, atau modul opt-in mendatang; pratinjau ini tidak mengaktifkan modul atau mengubah konfigurasinya.\n\n\ |
| 4821 | RATIFIKASI\n{ratify_how}" |
| 4822 | ) |
| 4823 | } |
| 4824 | Locale::Hi => { |
| 4825 | let drafted_by = match provenance { |
| 4826 | DraftProvenance::Model(label) => format!( |
| 4827 | "{label} द्वारा आपके गाइडेड उत्तरों से तैयार, फिर Codewhale द्वारा स्कीमा-जाँचा और सीमित किया गया।" |
| 4828 | ), |
| 4829 | DraftProvenance::Guided => "आपके गाइडेड उत्तरों से नियत रूप से तैयार किया गया।".to_string(), |
| 4830 | DraftProvenance::Existing => { |
| 4831 | "आपका मौजूदा संविधान, constitution.json से लोड किया गया — अपरिवर्तित दिखाया गया।" |
| 4832 | .to_string() |
| 4833 | } |
| 4834 | }; |
| 4835 | let ratify_how = match provenance { |
| 4836 | DraftProvenance::Existing => { |
| 4837 | "यह पहले से ही आपका स्थायी कानून है। यह पूर्वावलोकन बंद करें, फिर इसे बनाए रखने और चेकपॉइंट पूरा करने के लिए K दबाएँ — \ |
| 4838 | फ़ाइल संशोधित नहीं होती। /constitution या /setup से कभी भी संशोधित करें।" |
| 4839 | } |
| 4840 | _ => { |
| 4841 | "जब तक आप पुष्टि नहीं करते, कुछ भी कानून नहीं बनता। यह पूर्वावलोकन बंद करें, फिर अंगीकार और सहेजने के लिए G दबाएँ। \ |
| 4842 | /constitution या /setup से कभी भी संशोधित करें।" |
| 4843 | } |
| 4844 | }; |
| 4845 | format!( |
| 4846 | "CODEWHALE · उपयोगकर्ता संविधान\n{RULE}\n\n{drafted_by}\n\n\ |
| 4847 | यह Codewhale आपके साथ कैसे काम करे, इसका स्थायी कानून है। सर्वोत्तम संविधानों की तरह, \ |
| 4848 | यह उपयोग के लिए पर्याप्त छोटा है, संपूर्ण नियमों के बजाय टिकाऊ सिद्धांतों से बना है, \ |
| 4849 | और आपके बदलने के साथ संशोधनीय है। यह हर मामले का फ़ैसला करने के बजाय शक्तियों और सीमाओं का ढाँचा देता है, \ |
| 4850 | और आपके सहयोग को सत्रों के पार निरंतरता देता है — लेकिन यह मेमोरी नहीं है: यह इतिहास नहीं, सिद्धांत रखता है।\n\n\ |
| 4851 | {rendered}\n\n\ |
| 4852 | अधिकार पदानुक्रम\n{layer_order}\nआपके प्रत्यक्ष अनुरोध हमेशा इस दस्तावेज़ से ऊपर हैं।\n\n\ |
| 4853 | यह क्या नहीं कर सकता\n\ |
| 4854 | यह व्यवहार का मार्गदर्शन करता है। यह अनुमति नीति, सैंडबॉक्स, शेल, नेटवर्क, \ |
| 4855 | ट्रस्ट, MCP अनुमतियाँ, डिफ़ॉल्ट मोड, प्रकाशन या खर्च का अधिकार प्रदान या परिवर्तित नहीं कर सकता — वे रनटाइम पर आपके हाथ में रहते हैं।\n\n\ |
| 4856 | संक्षिप्त कोर और ऑप्ट-इन मॉड्यूल\n\ |
| 4857 | Bundled कोर सक्रिय रहता है। यह मसौदा केवल आपकी उपयोगकर्ता-वैश्विक स्थायी प्राथमिकताएँ सहेजता है। \ |
| 4858 | भारी निष्पादन या ऑर्केस्ट्रेशन सिद्धांत मोड प्रॉम्प्ट या भविष्य के ऑप्ट-इन मॉड्यूल में रहते हैं; यह पूर्वावलोकन मॉड्यूल सक्षम नहीं करता और न ही उनकी कॉन्फ़िगरेशन बदलता है।\n\n\ |
| 4859 | अंगीकार\n{ratify_how}" |
| 4860 | ) |
| 4861 | } |
| 4862 | Locale::Ru => { |
| 4863 | let drafted_by = match provenance { |
| 4864 | DraftProvenance::Model(label) => format!( |
| 4865 | "Подготовлено {label} на основе ваших ответов на наводящие вопросы, затем проверено по схеме и ограничено Codewhale." |
| 4866 | ), |
| 4867 | DraftProvenance::Guided => { |
| 4868 | "Детерминированно построено из ваших ответов на наводящие вопросы.".to_string() |
| 4869 | } |
| 4870 | DraftProvenance::Existing => { |
| 4871 | "Ваша существующая конституция, загруженная из constitution.json, — показана без изменений." |
| 4872 | .to_string() |
| 4873 | } |
| 4874 | }; |
| 4875 | let ratify_how = match provenance { |
| 4876 | DraftProvenance::Existing => { |
| 4877 | "Это уже ваш действующий закон. Закройте это превью, затем нажмите K, чтобы сохранить её и завершить контрольную точку — \ |
| 4878 | файл не изменяется. Изменить можно в любое время через /constitution или /setup." |
| 4879 | } |
| 4880 | _ => { |
| 4881 | "Ничто не становится законом, пока вы не подтвердите. Закройте это превью, затем нажмите G, чтобы ратифицировать и сохранить. \ |
| 4882 | Изменить можно в любое время через /constitution или /setup." |
| 4883 | } |
| 4884 | }; |
| 4885 | format!( |
| 4886 | "CODEWHALE · КОНСТИТУЦИЯ ПОЛЬЗОВАТЕЛЯ\n{RULE}\n\n{drafted_by}\n\n\ |
| 4887 | Это постоянный закон о том, как Codewhale работает с вами. Как лучшие конституции, \ |
| 4888 | она достаточно коротка, чтобы ей пользоваться, состоит из долговечных принципов, а не исчерпывающих правил, \ |
| 4889 | и может изменяться вместе с вами. Она очерчивает полномочия и границы, а не решает каждый случай, \ |
| 4890 | и придаёт вашему сотрудничеству непрерывность между сессиями — но она не память: она хранит принципы, а не историю.\n\n\ |
| 4891 | {rendered}\n\n\ |
| 4892 | ИЕРАРХИЯ ПОЛНОМОЧИЙ\n{layer_order}\nВаши прямые указания всегда важнее этого документа.\n\n\ |
| 4893 | ЧЕГО ОНА НЕ МОЖЕТ\n\ |
| 4894 | Она направляет поведение. Она не может предоставить или изменить политику одобрения, sandbox, shell, сеть, \ |
| 4895 | доверие, разрешения MCP, режим по умолчанию, публикацию или право тратить — они остаются в ваших руках во время выполнения.\n\n\ |
| 4896 | СОКРАЩЁННОЕ ЯДРО И ОПЦИОНАЛЬНЫЕ МОДУЛИ\n\ |
| 4897 | Встроенное ядро остаётся активным. Этот проект сохраняет только ваши глобальные постоянные предпочтения. \ |
| 4898 | Тяжёлая доктрина исполнения или оркестрации принадлежит промптам режимов или будущим опциональным модулям; это превью не включает модули и не меняет их конфигурацию.\n\n\ |
| 4899 | РАТИФИКАЦИЯ\n{ratify_how}" |
| 4900 | ) |
| 4901 | } |
| 4902 | Locale::Uk => { |
| 4903 | let drafted_by = match provenance { |
| 4904 | DraftProvenance::Model(label) => format!( |
| 4905 | "Підготовлено {label} на основі ваших відповідей на навідні запитання, потім перевірено за схемою та обмежено Codewhale." |
| 4906 | ), |
| 4907 | DraftProvenance::Guided => { |
| 4908 | "Детерміновано побудовано з ваших відповідей на навідні запитання.".to_string() |
| 4909 | } |
| 4910 | DraftProvenance::Existing => { |
| 4911 | "Ваша чинна конституція, завантажена з constitution.json, — показана без змін." |
| 4912 | .to_string() |
| 4913 | } |
| 4914 | }; |
| 4915 | let ratify_how = match provenance { |
| 4916 | DraftProvenance::Existing => { |
| 4917 | "Це вже ваш чинний закон. Закрийте це прев'ю, потім натисніть K, щоб зберегти її та завершити контрольну точку — \ |
| 4918 | файл не змінюється. Змінити можна будь-коли через /constitution або /setup." |
| 4919 | } |
| 4920 | _ => { |
| 4921 | "Ніщо не стає законом, доки ви не підтвердите. Закрийте це прев'ю, потім натисніть G, щоб ратифікувати та зберегти. \ |
| 4922 | Змінити можна будь-коли через /constitution або /setup." |
| 4923 | } |
| 4924 | }; |
| 4925 | format!( |
| 4926 | "CODEWHALE · КОНСТИТУЦІЯ КОРИСТУВАЧА\n{RULE}\n\n{drafted_by}\n\n\ |
| 4927 | Це постійний закон про те, як Codewhale працює з вами. Як найкращі конституції, \ |
| 4928 | вона достатньо коротка, щоб нею користуватися, складається з довговічних принципів, а не вичерпних правил, \ |
| 4929 | і може змінюватися разом із вами. Вона окреслює повноваження та межі, а не вирішує кожен випадок, \ |
| 4930 | і надає вашій співпраці неперервність між сесіями — але вона не пам'ять: вона зберігає принципи, а не історію.\n\n\ |
| 4931 | {rendered}\n\n\ |
| 4932 | ІЄРАРХІЯ ПОВНОВАЖЕНЬ\n{layer_order}\nВаші прямі вказівки завжди важливіші за цей документ.\n\n\ |
| 4933 | ЧОГО ВОНА НЕ МОЖЕ\n\ |
| 4934 | Вона спрямовує поведінку. Вона не може надати або змінити політику схвалення, sandbox, shell, мережу, \ |
| 4935 | довіру, дозволи MCP, режим за замовчуванням, публікацію чи право витрачати — вони залишаються у ваших руках під час виконання.\n\n\ |
| 4936 | СКОРОЧЕНЕ ЯДРО Й ОПЦІЙНІ МОДУЛІ\n\ |
| 4937 | Вбудоване ядро залишається активним. Цей проєкт зберігає лише ваші глобальні постійні вподобання. \ |
| 4938 | Важка доктрина виконання чи оркестрації належить промптам режимів або майбутнім опційним модулям; це прев'ю не вмикає модулі й не змінює їхню конфігурацію.\n\n\ |
| 4939 | РАТИФІКАЦІЯ\n{ratify_how}" |
| 4940 | ) |
| 4941 | } |
| 4942 | _ => { |
| 4943 | let drafted_by = match provenance { |
| 4944 | DraftProvenance::Model(label) => format!( |
| 4945 | "Drafted by {label} from your guided answers, then schema-checked and bounded by Codewhale." |
| 4946 | ), |
| 4947 | DraftProvenance::Guided => { |
| 4948 | "Rendered deterministically from your guided answers.".to_string() |
| 4949 | } |
| 4950 | DraftProvenance::Existing => { |
| 4951 | "Your existing constitution, loaded from constitution.json — shown unchanged." |
| 4952 | .to_string() |
| 4953 | } |
| 4954 | }; |
| 4955 | let ratify_how = match provenance { |
| 4956 | DraftProvenance::Existing => { |
| 4957 | "This is already your standing law. Close this preview, then press K to \ |
| 4958 | keep it and complete the checkpoint — the file is not modified. Amend \ |
| 4959 | anytime with /constitution or /setup." |
| 4960 | } |
| 4961 | _ => { |
| 4962 | "Nothing becomes law until you confirm. Close this preview, then press G to \ |
| 4963 | ratify and save. Amend anytime with /constitution or /setup." |
| 4964 | } |
| 4965 | }; |
| 4966 | format!( |
| 4967 | "CODEWHALE · USER CONSTITUTION\n{RULE}\n\n{drafted_by}\n\n\ |
| 4968 | This is the standing law for how Codewhale works with you. Like the best \ |
| 4969 | constitutions, it is short enough to use, made of durable principles rather \ |
| 4970 | than exhaustive rules, and amendable as you change. It frames powers and \ |
| 4971 | limits rather than deciding every case, and it gives your collaboration \ |
| 4972 | continuity across sessions — but it is not memory: it carries principles, \ |
| 4973 | not history.\n\n\ |
| 4974 | {rendered}\n\n\ |
| 4975 | HIERARCHY OF AUTHORITY\n{layer_order}\nYour direct requests always outrank this document.\n\n\ |
| 4976 | WHAT THIS CANNOT DO\n\ |
| 4977 | It guides behavior. It cannot grant or change approval policy, sandbox, shell, \ |
| 4978 | network, trust, MCP permissions, default mode, publishing, or spending \ |
| 4979 | authority — those stay under your hand at runtime.\n\n\ |
| 4980 | REDUCED CORE AND OPT-IN MODULES\n\ |
| 4981 | The bundled core stays active. This draft only saves your user-global \ |
| 4982 | standing preferences. Execution and orchestration capabilities come from runtime \ |
| 4983 | policy, the live tool catalog, or future opt-in modules; this preview does not enable modules or change \ |
| 4984 | their configuration.\n\n\ |
| 4985 | RATIFICATION\n{ratify_how}" |
| 4986 | ) |
| 4987 | } |
| 4988 | } |
| 4989 | } |
| 4990 | |
| 4991 | /// Card line inviting the user to let their configured model draft the law. |
| 4992 | fn model_draft_invitation_line(locale: Locale, model_label: &str) -> String { |
| 4993 | match locale { |
| 4994 | Locale::Ja => { |
| 4995 | format!("A {model_label} が起草し、あなたが批准します。確認するまで保存しません。") |
| 4996 | } |
| 4997 | Locale::ZhHans => { |
| 4998 | format!("A {model_label} 生成草案,由你确认。未经确认不会保存。") |
| 4999 | } |
| 5000 | Locale::ZhHant => { |
| 5001 | format!("A {model_label} 起草,你批准。未經確認不會保存。") |
| 5002 | } |
| 5003 | Locale::PtBr => { |
| 5004 | format!("A {model_label} pode rascunhar. Você ratifica. Nada salva sem você.") |
| 5005 | } |
| 5006 | Locale::Es419 => { |
| 5007 | format!("A {model_label} puede redactarla. Tú ratificas. Nada se guarda sin ti.") |
| 5008 | } |
| 5009 | Locale::Vi => { |
| 5010 | 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.") |
| 5011 | } |
| 5012 | Locale::Ko => { |
| 5013 | format!( |
| 5014 | "A {model_label}이(가) 초안을 작성할 수 있습니다. 승인은 당신이 합니다. 당신 없이는 아무것도 저장되지 않습니다." |
| 5015 | ) |
| 5016 | } |
| 5017 | Locale::Ca => { |
| 5018 | format!("A {model_label} la pot redactar. Tu la ratifiques. Res no es desa sense tu.") |
| 5019 | } |
| 5020 | Locale::De => { |
| 5021 | format!( |
| 5022 | "A {model_label} kann sie entwerfen. Du ratifizierst sie. Ohne dich wird nichts gespeichert." |
| 5023 | ) |
| 5024 | } |
| 5025 | Locale::Fr => { |
| 5026 | format!( |
| 5027 | "A {model_label} peut la rédiger. Vous la ratifiez. Rien ne s'enregistre sans vous." |
| 5028 | ) |
| 5029 | } |
| 5030 | Locale::Id => { |
| 5031 | format!( |
| 5032 | "A {model_label} dapat menyusunnya. Anda yang meratifikasi. Tidak ada yang tersimpan tanpa Anda." |
| 5033 | ) |
| 5034 | } |
| 5035 | Locale::Hi => { |
| 5036 | format!( |
| 5037 | "A {model_label} इसका मसौदा बना सकता है। अंगीकार आप करते हैं। आपके बिना कुछ भी सहेजा नहीं जाता।" |
| 5038 | ) |
| 5039 | } |
| 5040 | Locale::Ru => { |
| 5041 | format!( |
| 5042 | "A {model_label} может подготовить проект. Ратифицируете вы. Без вас ничего не сохраняется." |
| 5043 | ) |
| 5044 | } |
| 5045 | Locale::Uk => { |
| 5046 | format!( |
| 5047 | "A {model_label} може підготувати проєкт. Ратифікуєте ви. Без вас нічого не зберігається." |
| 5048 | ) |
| 5049 | } |
| 5050 | _ => format!("A {model_label} can draft it. You ratify it. Nothing saves without you."), |
| 5051 | } |
| 5052 | } |
| 5053 | |
| 5054 | /// Card line offering to keep an existing valid constitution unchanged. |
| 5055 | fn keep_existing_invitation_line(locale: Locale) -> &'static str { |
| 5056 | match locale { |
| 5057 | Locale::Ja => "K 既存の憲法を保持 - 確認して保持、ファイルは変更しません。", |
| 5058 | Locale::ZhHans => "K 保留现有宪章——先查看,再保留,文件不变。", |
| 5059 | Locale::ZhHant => "K 保留現有憲法 - 先查看,再保留,檔案不變。", |
| 5060 | Locale::PtBr => "K Manter constituição existente - revise, mantenha, arquivo inalterado.", |
| 5061 | Locale::Es419 => { |
| 5062 | "K Conservar constitución existente - revisa, conserva, archivo sin cambios." |
| 5063 | } |
| 5064 | Locale::Vi => "K Giữ hiến pháp hiện có - xem lại, giữ nguyên, tệp không đổi.", |
| 5065 | Locale::Ko => "K 기존 헌법 유지 - 검토 후 유지, 파일은 변경되지 않음.", |
| 5066 | Locale::Ca => { |
| 5067 | "K Mantén la constitució existent - revisa-la, conserva-la, fitxer sense canvis." |
| 5068 | } |
| 5069 | Locale::De => "K Bestehende Verfassung behalten - prüfen, behalten, Datei unverändert.", |
| 5070 | Locale::Fr => { |
| 5071 | "K Garder votre constitution existante - révisez-la, gardez-la, fichier inchangé." |
| 5072 | } |
| 5073 | Locale::Id => { |
| 5074 | "K Pertahankan konstitusi Anda yang ada - tinjau, pertahankan, file tidak berubah." |
| 5075 | } |
| 5076 | Locale::Hi => "K अपना मौजूदा संविधान रखें - समीक्षा करें, बनाए रखें, फ़ाइल अपरिवर्तित।", |
| 5077 | Locale::Ru => { |
| 5078 | "K Сохранить существующую конституцию - просмотрите, сохраните, файл не изменяется." |
| 5079 | } |
| 5080 | Locale::Uk => "K Зберегти чинну конституцію - перегляньте, збережіть, файл без змін.", |
| 5081 | _ => "K Keep your existing constitution — review it, keep it, file unchanged.", |
| 5082 | } |
| 5083 | } |
| 5084 | |
| 5085 | /// Card line shown while a model draft awaits ratification. |
| 5086 | fn model_draft_ready_line(locale: Locale, model_label: &str) -> String { |
| 5087 | match locale { |
| 5088 | Locale::Ja => { |
| 5089 | format!( |
| 5090 | "{model_label} の草案が批准待ちです - G で確認して批准、1-6 で草案を破棄します。" |
| 5091 | ) |
| 5092 | } |
| 5093 | Locale::ZhHans => { |
| 5094 | format!("{model_label} 的草案待确认——按 G 查看并确认;按 1-6 会丢弃草案。") |
| 5095 | } |
| 5096 | Locale::ZhHant => { |
| 5097 | format!("{model_label} 的草案待批准 - 按 G 查看並批准;按 1-6 會丟棄草案。") |
| 5098 | } |
| 5099 | Locale::PtBr => { |
| 5100 | format!( |
| 5101 | "Rascunho de {model_label} aguarda ratificação - G para revisar e ratificar; 1-6 descarta." |
| 5102 | ) |
| 5103 | } |
| 5104 | Locale::Es419 => { |
| 5105 | format!( |
| 5106 | "El borrador de {model_label} espera ratificación - G para revisar y ratificar; 1-6 lo descarta." |
| 5107 | ) |
| 5108 | } |
| 5109 | Locale::Vi => { |
| 5110 | format!( |
| 5111 | "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." |
| 5112 | ) |
| 5113 | } |
| 5114 | Locale::Ko => { |
| 5115 | format!( |
| 5116 | "{model_label}의 초안이 승인을 기다리고 있습니다 - G로 확인하고 승인, 1-6은 초안을 버립니다." |
| 5117 | ) |
| 5118 | } |
| 5119 | Locale::Ca => { |
| 5120 | format!( |
| 5121 | "L'esborrany de {model_label} espera ratificació - G per revisar i ratificar; 1-6 el descarta." |
| 5122 | ) |
| 5123 | } |
| 5124 | Locale::De => { |
| 5125 | format!( |
| 5126 | "Entwurf von {model_label} wartet auf Ratifizierung - G zum Prüfen und Ratifizieren; 1-6 verwirft ihn." |
| 5127 | ) |
| 5128 | } |
| 5129 | Locale::Fr => { |
| 5130 | format!( |
| 5131 | "Le brouillon de {model_label} attend ratification - G pour réviser et ratifier ; 1-6 l'écarte." |
| 5132 | ) |
| 5133 | } |
| 5134 | Locale::Id => { |
| 5135 | format!( |
| 5136 | "Draf oleh {model_label} menunggu ratifikasi - G untuk meninjau dan meratifikasi; 1-6 membuangnya." |
| 5137 | ) |
| 5138 | } |
| 5139 | Locale::Hi => { |
| 5140 | format!( |
| 5141 | "{model_label} का मसौदा अंगीकार की प्रतीक्षा में है - समीक्षा और अंगीकार के लिए G; 1-6 उसे खारिज करता है।" |
| 5142 | ) |
| 5143 | } |
| 5144 | Locale::Ru => { |
| 5145 | format!( |
| 5146 | "Проект от {model_label} ожидает ратификации - G для просмотра и ратификации; 1-6 отклоняет его." |
| 5147 | ) |
| 5148 | } |
| 5149 | Locale::Uk => { |
| 5150 | format!( |
| 5151 | "Проєкт від {model_label} очікує ратифікації - G для перегляду та ратифікації; 1-6 відхиляє його." |
| 5152 | ) |
| 5153 | } |
| 5154 | _ => format!( |
| 5155 | "Draft by {model_label} awaits ratification — G to review and ratify; 1-6 discards it." |
| 5156 | ), |
| 5157 | } |
| 5158 | } |
| 5159 | |
| 5160 | /// Host-facing status line after a successful model draft. |
| 5161 | pub(crate) fn model_draft_ready_message(locale: Locale, model_label: &str) -> String { |
| 5162 | match locale { |
| 5163 | Locale::Ja => format!( |
| 5164 | "{model_label} があなたの憲法を起草しました。プレビューを確認してから G で批准してください。" |
| 5165 | ), |
| 5166 | Locale::ZhHans => { |
| 5167 | format!("{model_label} 已生成你的宪章草案。请查看预览,然后按 G 确认。") |
| 5168 | } |
| 5169 | Locale::ZhHant => format!("{model_label} 已起草你的憲法。請查看預覽,然後按 G 批准。"), |
| 5170 | Locale::PtBr => format!( |
| 5171 | "{model_label} rascunhou sua constituição. Revise a prévia e pressione G para ratificar." |
| 5172 | ), |
| 5173 | Locale::Es419 => format!( |
| 5174 | "{model_label} redactó tu constitución. Revisa la vista previa y presiona G para ratificar." |
| 5175 | ), |
| 5176 | Locale::Vi => format!( |
| 5177 | "{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." |
| 5178 | ), |
| 5179 | Locale::Ko => format!( |
| 5180 | "{model_label}이(가) 당신의 헌법 초안을 작성했습니다. 미리보기를 확인한 뒤 G를 눌러 승인하세요." |
| 5181 | ), |
| 5182 | Locale::Ca => format!( |
| 5183 | "{model_label} ha redactat la teva constitució. Revisa la previsualització i prem G per ratificar." |
| 5184 | ), |
| 5185 | Locale::De => format!( |
| 5186 | "{model_label} hat deine Verfassung entworfen. Prüfe die Vorschau und drücke G zum Ratifizieren." |
| 5187 | ), |
| 5188 | Locale::Fr => format!( |
| 5189 | "{model_label} a rédigé votre constitution. Révisez l'aperçu, puis appuyez sur G pour ratifier." |
| 5190 | ), |
| 5191 | Locale::Id => format!( |
| 5192 | "{model_label} menyusun konstitusi Anda. Tinjau pratinjaunya, lalu tekan G untuk meratifikasi." |
| 5193 | ), |
| 5194 | Locale::Hi => format!( |
| 5195 | "{model_label} ने आपके संविधान का मसौदा तैयार किया। पूर्वावलोकन देखें, फिर अंगीकार के लिए G दबाएँ।" |
| 5196 | ), |
| 5197 | Locale::Ru => format!( |
| 5198 | "{model_label} подготовил проект вашей конституции. Просмотрите превью, затем нажмите G для ратификации." |
| 5199 | ), |
| 5200 | Locale::Uk => format!( |
| 5201 | "{model_label} підготував проєкт вашої конституції. Перегляньте прев'ю, потім натисніть G для ратифікації." |
| 5202 | ), |
| 5203 | _ => format!( |
| 5204 | "{model_label} drafted your constitution. Review the preview, then press G to ratify." |
| 5205 | ), |
| 5206 | } |
| 5207 | } |
| 5208 | |
| 5209 | /// Host-facing status line when model drafting fails or is unavailable. The |
| 5210 | /// guided deterministic draft always remains the standing fallback. |
| 5211 | pub(crate) fn model_draft_failed_message( |
| 5212 | locale: Locale, |
| 5213 | model_label: &str, |
| 5214 | reason: &str, |
| 5215 | ) -> String { |
| 5216 | match locale { |
| 5217 | Locale::Ja => { |
| 5218 | format!( |
| 5219 | "{model_label} は起草を完了できませんでした({reason})。ガイド草案は有効です。G でプレビューして批准できます。" |
| 5220 | ) |
| 5221 | } |
| 5222 | Locale::ZhHans => { |
| 5223 | format!("{model_label} 未能生成草案({reason})。引导式草案仍可使用——按 G 预览并确认。") |
| 5224 | } |
| 5225 | Locale::ZhHant => { |
| 5226 | format!("{model_label} 未能完成起草({reason})。引導式草案仍然有效;按 G 預覽並批准。") |
| 5227 | } |
| 5228 | Locale::PtBr => { |
| 5229 | format!( |
| 5230 | "{model_label} não conseguiu rascunhar sua constituição ({reason}). O rascunho guiado continua válido; pressione G para pré-visualizar e ratificar." |
| 5231 | ) |
| 5232 | } |
| 5233 | Locale::Es419 => { |
| 5234 | format!( |
| 5235 | "{model_label} no pudo redactar tu constitución ({reason}). El borrador guiado sigue válido; presiona G para previsualizar y ratificar." |
| 5236 | ) |
| 5237 | } |
| 5238 | Locale::Vi => { |
| 5239 | format!( |
| 5240 | "{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." |
| 5241 | ) |
| 5242 | } |
| 5243 | Locale::Ko => { |
| 5244 | format!( |
| 5245 | "{model_label}이(가) 당신의 헌법 초안을 작성하지 못했습니다 ({reason}). 가이드 초안은 여전히 유효합니다. G를 눌러 미리보고 승인하세요." |
| 5246 | ) |
| 5247 | } |
| 5248 | Locale::Ca => { |
| 5249 | format!( |
| 5250 | "{model_label} no ha pogut redactar la teva constitució ({reason}). L'esborrany guiat continua vigent; prem G per previsualitzar i ratificar." |
| 5251 | ) |
| 5252 | } |
| 5253 | Locale::De => { |
| 5254 | format!( |
| 5255 | "{model_label} konnte deine Verfassung nicht entwerfen ({reason}). Dein geführter Entwurf bleibt gültig; drücke G für Vorschau und Ratifizierung." |
| 5256 | ) |
| 5257 | } |
| 5258 | Locale::Fr => { |
| 5259 | format!( |
| 5260 | "{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." |
| 5261 | ) |
| 5262 | } |
| 5263 | Locale::Id => { |
| 5264 | format!( |
| 5265 | "{model_label} tidak dapat menyusun konstitusi Anda ({reason}). Draf terpandu Anda tetap berlaku; tekan G untuk pratinjau dan ratifikasi." |
| 5266 | ) |
| 5267 | } |
| 5268 | Locale::Hi => { |
| 5269 | format!( |
| 5270 | "{model_label} आपके संविधान का मसौदा नहीं बना सका ({reason})। आपका गाइडेड मसौदा अब भी मान्य है; पूर्वावलोकन और अंगीकार के लिए G दबाएँ।" |
| 5271 | ) |
| 5272 | } |
| 5273 | Locale::Ru => { |
| 5274 | format!( |
| 5275 | "{model_label} не смог подготовить вашу конституцию ({reason}). Ваш управляемый проект остаётся в силе — нажмите G для просмотра и ратификации." |
| 5276 | ) |
| 5277 | } |
| 5278 | Locale::Uk => { |
| 5279 | format!( |
| 5280 | "{model_label} не зміг підготувати вашу конституцію ({reason}). Ваш керований проєкт залишається чинним — натисніть G для перегляду та ратифікації." |
| 5281 | ) |
| 5282 | } |
| 5283 | _ => format!( |
| 5284 | "{model_label} could not draft your constitution ({reason}). Your guided draft still \ |
| 5285 | stands — press G to preview and ratify." |
| 5286 | ), |
| 5287 | } |
| 5288 | } |
| 5289 | |
| 5290 | fn constitution_choice_label(choice: ConstitutionChoice) -> &'static str { |
| 5291 | match choice { |
| 5292 | ConstitutionChoice::Unset => "unset", |
| 5293 | ConstitutionChoice::Bundled => "bundled/default", |
| 5294 | ConstitutionChoice::GuidedCustom => "guided custom", |
| 5295 | ConstitutionChoice::ExpertOverride => "expert override", |
| 5296 | ConstitutionChoice::Deferred => "deferred", |
| 5297 | } |
| 5298 | } |
| 5299 | |
| 5300 | fn constitution_source_label(source: ConstitutionSource) -> &'static str { |
| 5301 | match source { |
| 5302 | ConstitutionSource::Bundled => "bundled", |
| 5303 | ConstitutionSource::UserGlobal => "user-global constitution.json", |
| 5304 | ConstitutionSource::ExpertOverride => "expert full Markdown override", |
| 5305 | } |
| 5306 | } |
| 5307 | |
| 5308 | fn constitution_validity_label(validity: ConstitutionValidity) -> &'static str { |
| 5309 | match validity { |
| 5310 | ConstitutionValidity::Unknown => "unknown", |
| 5311 | ConstitutionValidity::Valid => "valid", |
| 5312 | ConstitutionValidity::Invalid => "invalid", |
| 5313 | ConstitutionValidity::Empty => "empty", |
| 5314 | ConstitutionValidity::Unreadable => "unreadable", |
| 5315 | } |
| 5316 | } |
| 5317 | |
| 5318 | pub fn persist_user_constitution_choice( |
| 5319 | constitution: &UserConstitution, |
| 5320 | state: &SetupState, |
| 5321 | ) -> anyhow::Result<()> { |
| 5322 | let constitution_path = UserConstitution::path()?; |
| 5323 | let setup_state_path = SetupState::path()?; |
| 5324 | let mut transaction = codewhale_config::persistence::SetupTransaction::new(); |
| 5325 | transaction.stage_json(constitution_path, &constitution.bounded())?; |
| 5326 | transaction.stage_json(setup_state_path, state)?; |
| 5327 | transaction.commit() |
| 5328 | } |
| 5329 | |
| 5330 | #[must_use] |
| 5331 | pub fn should_open_update_checkpoint(app: &App, config: &Config) -> bool { |
| 5332 | let state = load_setup_state_for_app(app, config); |
| 5333 | state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) |
| 5334 | } |
| 5335 | |
| 5336 | pub fn defer_update_checkpoint_for_app(app: &App, config: &Config) -> anyhow::Result<SetupState> { |
| 5337 | let mut state = load_setup_state_for_app(app, config); |
| 5338 | if !state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) { |
| 5339 | return Ok(state); |
| 5340 | } |
| 5341 | state.complete_constitution_checkpoint( |
| 5342 | CONSTITUTION_CHECKPOINT_VERSION, |
| 5343 | ConstitutionChoice::Deferred, |
| 5344 | ); |
| 5345 | state.constitution_source = ConstitutionSource::Bundled; |
| 5346 | state.constitution_validity = ConstitutionValidity::Unknown; |
| 5347 | state.constitution_authoring = None; |
| 5348 | state.constitution_preview_hash = None; |
| 5349 | state.set_step( |
| 5350 | SetupStep::Constitution, |
| 5351 | StepEntry::new(StepStatus::Deferred, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 5352 | .with_result("checkpoint deferred; bundled applies"), |
| 5353 | ); |
| 5354 | state.save()?; |
| 5355 | Ok(state) |
| 5356 | } |
| 5357 | |
| 5358 | #[must_use] |
| 5359 | pub fn load_setup_state_for_app(app: &App, config: &Config) -> SetupState { |
| 5360 | if let Ok(Some(state)) = SetupState::load() { |
| 5361 | return state; |
| 5362 | } |
| 5363 | SetupState::derive_inherited(&inherited_facts_for_app(app, config)) |
| 5364 | } |
| 5365 | |
| 5366 | pub(crate) fn record_provider_model_setup_state_for_app( |
| 5367 | app: &App, |
| 5368 | config: &Config, |
| 5369 | ) -> anyhow::Result<SetupState> { |
| 5370 | let facts = SetupRuntimeFacts::from_app_config(app, config); |
| 5371 | let mut state = load_setup_state_for_app(app, config); |
| 5372 | state.set_step( |
| 5373 | SetupStep::ProviderModel, |
| 5374 | provider::step_entry( |
| 5375 | facts.provider_ready, |
| 5376 | CONSTITUTION_CHECKPOINT_VERSION, |
| 5377 | facts.provider_result, |
| 5378 | ), |
| 5379 | ); |
| 5380 | state.save()?; |
| 5381 | Ok(state) |
| 5382 | } |
| 5383 | |
| 5384 | #[must_use] |
| 5385 | fn inherited_facts_for_app(app: &App, config: &Config) -> InheritedConfigFacts { |
| 5386 | let user_constitution = UserConstitution::load().ok(); |
| 5387 | let user_constitution_validity = user_constitution.as_ref().map_or( |
| 5388 | ConstitutionValidity::Unknown, |
| 5389 | UserConstitutionLoad::validity, |
| 5390 | ); |
| 5391 | let has_user_constitution = user_constitution |
| 5392 | .as_ref() |
| 5393 | .is_some_and(|loaded| !matches!(loaded, UserConstitutionLoad::Missing)); |
| 5394 | let expert_override = SetupExpertOverrideState::load(); |
| 5395 | InheritedConfigFacts { |
| 5396 | language: Some(app.ui_locale.tag().to_string()), |
| 5397 | has_provider_route: !config.default_model().trim().is_empty(), |
| 5398 | has_credentials_or_local_runtime: has_api_key(config), |
| 5399 | trust_chosen: app.trust_mode || !onboarding::needs_trust(&app.workspace), |
| 5400 | has_expert_override: expert_override.is_active(), |
| 5401 | has_user_constitution, |
| 5402 | user_constitution_validity, |
| 5403 | } |
| 5404 | } |
| 5405 | |
| 5406 | fn expert_override_path() -> Option<std::path::PathBuf> { |
| 5407 | codewhale_config::codewhale_home() |
| 5408 | .ok() |
| 5409 | .map(|home| home.join(Path::new(CONSTITUTION_OVERRIDE_FILE))) |
| 5410 | } |
| 5411 | |
| 5412 | #[must_use] |
| 5413 | fn progressive_initial_step_index(state: &SetupState, facts: &SetupRuntimeFacts) -> usize { |
| 5414 | if !facts.provider_ready { |
| 5415 | return step_index(SetupStep::ProviderModel); |
| 5416 | } |
| 5417 | let runtime_current = if state.inherited { |
| 5418 | matches!(state.status(SetupStep::TrustSandbox), StepStatus::Verified) |
| 5419 | && state.runtime_posture_source.is_reviewed() |
| 5420 | } else { |
| 5421 | state |
| 5422 | .steps |
| 5423 | .get(&SetupStep::TrustSandbox) |
| 5424 | .is_some_and(|entry| { |
| 5425 | entry.status == StepStatus::Verified |
| 5426 | && entry.result.as_deref() == Some(facts.runtime_result.as_str()) |
| 5427 | }) |
| 5428 | && state.runtime_posture_source.is_reviewed() |
| 5429 | }; |
| 5430 | if !runtime_current { |
| 5431 | return step_index(SetupStep::TrustSandbox); |
| 5432 | } |
| 5433 | if facts.tools_mcp_needs_action { |
| 5434 | return step_index(SetupStep::ToolsMcp); |
| 5435 | } |
| 5436 | step_index(SetupStep::Verification) |
| 5437 | } |
| 5438 | |
| 5439 | #[must_use] |
| 5440 | fn step_index(step: SetupStep) -> usize { |
| 5441 | STEP_SPECS |
| 5442 | .iter() |
| 5443 | .position(|spec| spec.id() == step) |
| 5444 | .expect("all setup-state steps should have wizard specs") |
| 5445 | } |
| 5446 | |
| 5447 | fn visible_step_index(step: SetupStep) -> usize { |
| 5448 | STEP_SPECS |
| 5449 | .iter() |
| 5450 | .position(|spec| spec.id() == step) |
| 5451 | .unwrap_or_else(|| step_index(SetupStep::Constitution)) |
| 5452 | } |
| 5453 | |
| 5454 | #[cfg(test)] |
| 5455 | mod progressive_tests { |
| 5456 | use super::*; |
| 5457 | use crossterm::event::KeyModifiers; |
| 5458 | |
| 5459 | fn facts(provider_ready: bool) -> SetupRuntimeFacts { |
| 5460 | SetupRuntimeFacts { |
| 5461 | provider: "local".to_string(), |
| 5462 | model: "stub-model".to_string(), |
| 5463 | auth: if provider_ready { "ready" } else { "missing" }.to_string(), |
| 5464 | provider_ready, |
| 5465 | runtime_result: "approval=ask; sandbox=workspace; network=prompt".to_string(), |
| 5466 | tools_mcp_result: "mcp=off, skills=off, tools=off, plugins=off, overall=off" |
| 5467 | .to_string(), |
| 5468 | remote_control_result: tr(Locale::En, MessageId::SetupRemoteStatusDisabled) |
| 5469 | .into_owned(), |
| 5470 | ..SetupRuntimeFacts::default() |
| 5471 | } |
| 5472 | } |
| 5473 | |
| 5474 | fn complete_state(runtime_result: &str) -> SetupState { |
| 5475 | let mut state = SetupState { |
| 5476 | runtime_posture_source: RuntimePostureSource::Confirmed, |
| 5477 | ..SetupState::default() |
| 5478 | }; |
| 5479 | state.set_step( |
| 5480 | SetupStep::TrustSandbox, |
| 5481 | StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION) |
| 5482 | .with_result(runtime_result), |
| 5483 | ); |
| 5484 | state |
| 5485 | } |
| 5486 | |
| 5487 | fn render_text(view: &SetupWizardView, width: u16, height: u16) -> String { |
| 5488 | let area = Rect::new(0, 0, width, height); |
| 5489 | let mut buffer = Buffer::empty(area); |
| 5490 | ModalView::render(view, area, &mut buffer); |
| 5491 | (0..height) |
| 5492 | .map(|y| { |
| 5493 | (0..width) |
| 5494 | .map(|x| buffer[(x, y)].symbol()) |
| 5495 | .collect::<String>() |
| 5496 | }) |
| 5497 | .collect::<Vec<_>>() |
| 5498 | .join("\n") |
| 5499 | } |
| 5500 | |
| 5501 | #[test] |
| 5502 | fn wizard_body_cells_carry_explicit_ink_on_the_dark_surface() { |
| 5503 | // The setup surface paints WHALE_BG while the blurb span carries no |
| 5504 | // fg of its own, so without the base paragraph style it inherits the |
| 5505 | // terminal default: black-on-black on light-profile terminals. Every |
| 5506 | // blurb cell must pin to the body ink; the explicitly styled title |
| 5507 | // must patch over the base unchanged. |
| 5508 | let view = SetupWizardView::new_with_facts(SetupState::default(), Locale::En, facts(false)); |
| 5509 | let blurb_head: String = tr(Locale::En, MessageId::OnboardProviderBlurb) |
| 5510 | .chars() |
| 5511 | .take(16) |
| 5512 | .collect(); |
| 5513 | let title_head: String = tr(Locale::En, MessageId::OnboardProviderTitle) |
| 5514 | .chars() |
| 5515 | .take(16) |
| 5516 | .collect(); |
| 5517 | assert!( |
| 5518 | !blurb_head.is_empty() && !title_head.is_empty(), |
| 5519 | "test needs non-empty title and blurb heads to locate rows" |
| 5520 | ); |
| 5521 | let area = Rect::new(0, 0, 100, 24); |
| 5522 | let mut buffer = Buffer::empty(area); |
| 5523 | ModalView::render(&view, area, &mut buffer); |
| 5524 | let mut blurb_hit = false; |
| 5525 | let mut title_hit = false; |
| 5526 | let mut checked = 0; |
| 5527 | for y in 0..area.height { |
| 5528 | let row: String = (0..area.width).map(|x| buffer[(x, y)].symbol()).collect(); |
| 5529 | let expected = if row.contains(blurb_head.as_str()) { |
| 5530 | blurb_hit = true; |
| 5531 | Some(palette::TEXT_PRIMARY) |
| 5532 | } else if row.contains(title_head.as_str()) { |
| 5533 | title_hit = true; |
| 5534 | Some(palette::WHALE_ACTION) |
| 5535 | } else { |
| 5536 | None |
| 5537 | }; |
| 5538 | let Some(fg) = expected else { |
| 5539 | continue; |
| 5540 | }; |
| 5541 | for x in 0..area.width { |
| 5542 | let cell = &buffer[(x, y)]; |
| 5543 | if cell.symbol().trim().is_empty() { |
| 5544 | continue; |
| 5545 | } |
| 5546 | assert_eq!( |
| 5547 | cell.style().fg, |
| 5548 | Some(fg), |
| 5549 | "setup body cell ({x}, {y}) must carry explicit ink" |
| 5550 | ); |
| 5551 | checked += 1; |
| 5552 | } |
| 5553 | } |
| 5554 | assert!( |
| 5555 | blurb_hit && title_hit && checked > 0, |
| 5556 | "expected title and blurb rows in the rendered wizard" |
| 5557 | ); |
| 5558 | } |
| 5559 | |
| 5560 | #[test] |
| 5561 | fn fresh_setup_starts_at_the_missing_provider_decision() { |
| 5562 | let view = SetupWizardView::new_with_facts(SetupState::default(), Locale::En, facts(false)); |
| 5563 | |
| 5564 | assert_eq!(view.selected_step(), SetupStep::ProviderModel); |
| 5565 | assert!(matches!( |
| 5566 | ModalView::handle_key( |
| 5567 | &mut view.clone(), |
| 5568 | KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE) |
| 5569 | ), |
| 5570 | ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested) |
| 5571 | )); |
| 5572 | } |
| 5573 | |
| 5574 | #[test] |
| 5575 | fn partial_setup_skips_the_ready_provider_and_asks_for_permissions() { |
| 5576 | let mut view = |
| 5577 | SetupWizardView::new_with_facts(SetupState::default(), Locale::En, facts(true)); |
| 5578 | |
| 5579 | assert_eq!(view.selected_step(), SetupStep::TrustSandbox); |
| 5580 | let action = |
| 5581 | ModalView::handle_key(&mut view, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); |
| 5582 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message, .. }) = action |
| 5583 | else { |
| 5584 | panic!("reviewing the current permissions posture should persist a receipt"); |
| 5585 | }; |
| 5586 | assert_eq!( |
| 5587 | state.runtime_posture_source, |
| 5588 | RuntimePostureSource::Confirmed |
| 5589 | ); |
| 5590 | assert_eq!(state.status(SetupStep::TrustSandbox), StepStatus::Verified); |
| 5591 | assert!(!message.is_empty()); |
| 5592 | assert_eq!(view.selected_step(), SetupStep::RemoteRuntime); |
| 5593 | } |
| 5594 | |
| 5595 | #[test] |
| 5596 | fn fully_configured_setup_opens_the_compact_summary() { |
| 5597 | let facts = facts(true); |
| 5598 | let state = complete_state(&facts.runtime_result); |
| 5599 | let mut view = SetupWizardView::new_with_facts(state, Locale::En, facts); |
| 5600 | |
| 5601 | assert_eq!(view.selected_step(), SetupStep::Verification); |
| 5602 | let action = |
| 5603 | ModalView::handle_key(&mut view, KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); |
| 5604 | let ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { state, .. }) = action |
| 5605 | else { |
| 5606 | panic!("starting from Ready should persist the report before closing"); |
| 5607 | }; |
| 5608 | assert!(state.steps.contains_key(&SetupStep::Verification)); |
| 5609 | } |
| 5610 | |
| 5611 | #[test] |
| 5612 | fn stale_complete_receipts_reopen_the_real_broken_surface() { |
| 5613 | let mut provider_broken = facts(false); |
| 5614 | provider_broken.runtime_result = "current".to_string(); |
| 5615 | let state = complete_state("old"); |
| 5616 | assert_eq!( |
| 5617 | SetupWizardView::new_with_facts(state, Locale::En, provider_broken).selected_step(), |
| 5618 | SetupStep::ProviderModel |
| 5619 | ); |
| 5620 | |
| 5621 | let runtime_current = facts(true); |
| 5622 | let stale_state = complete_state("old runtime snapshot"); |
| 5623 | assert_eq!( |
| 5624 | SetupWizardView::new_with_facts(stale_state, Locale::En, runtime_current) |
| 5625 | .selected_step(), |
| 5626 | SetupStep::TrustSandbox |
| 5627 | ); |
| 5628 | } |
| 5629 | |
| 5630 | #[test] |
| 5631 | fn configured_broken_tools_join_the_journey_but_empty_tools_do_not() { |
| 5632 | let mut broken = facts(true); |
| 5633 | let state = complete_state(&broken.runtime_result); |
| 5634 | broken.tools_mcp_needs_action = true; |
| 5635 | broken.tools_mcp_result = "overall=needs_config".to_string(); |
| 5636 | let mut broken_view = SetupWizardView::new_with_facts(state.clone(), Locale::En, broken); |
| 5637 | assert_eq!(broken_view.selected_step(), SetupStep::ToolsMcp); |
| 5638 | let action = ModalView::handle_key( |
| 5639 | &mut broken_view, |
| 5640 | KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE), |
| 5641 | ); |
| 5642 | let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = action else { |
| 5643 | panic!("continuing past a broken optional tool should save its visible issue"); |
| 5644 | }; |
| 5645 | assert_eq!(state.status(SetupStep::ToolsMcp), StepStatus::NeedsAction); |
| 5646 | assert_eq!(broken_view.selected_step(), SetupStep::Verification); |
| 5647 | |
| 5648 | let empty = facts(true); |
| 5649 | assert_eq!( |
| 5650 | SetupWizardView::new_with_facts(state, Locale::En, empty).selected_step(), |
| 5651 | SetupStep::Verification |
| 5652 | ); |
| 5653 | } |
| 5654 | |
| 5655 | #[test] |
| 5656 | fn account_action_uses_the_real_remote_control_handoff() { |
| 5657 | let facts = facts(true); |
| 5658 | let state = complete_state(&facts.runtime_result); |
| 5659 | let mut view = SetupWizardView::new_with_facts(state, Locale::En, facts); |
| 5660 | view.selected = visible_step_index(SetupStep::RemoteRuntime); |
| 5661 | |
| 5662 | assert!(matches!( |
| 5663 | ModalView::handle_key( |
| 5664 | &mut view, |
| 5665 | KeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE) |
| 5666 | ), |
| 5667 | ViewAction::EmitAndClose(ViewEvent::SetupOpenRemoteControlRequested) |
| 5668 | )); |
| 5669 | } |
| 5670 | |
| 5671 | #[test] |
| 5672 | fn progressive_arrow_keys_scroll_details_without_changing_the_decision() { |
| 5673 | let mut view = |
| 5674 | SetupWizardView::new_with_facts(SetupState::default(), Locale::En, facts(false)); |
| 5675 | view.details_expanded = true; |
| 5676 | view.body_scroll = 4; |
| 5677 | let step = view.selected_step(); |
| 5678 | |
| 5679 | assert!(matches!( |
| 5680 | ModalView::handle_key(&mut view, KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)), |
| 5681 | ViewAction::None |
| 5682 | )); |
| 5683 | assert_eq!(view.selected_step(), step); |
| 5684 | assert_eq!(view.body_scroll, 3); |
| 5685 | |
| 5686 | assert!(matches!( |
| 5687 | ModalView::handle_key(&mut view, KeyEvent::new(KeyCode::Down, KeyModifiers::NONE)), |
| 5688 | ViewAction::None |
| 5689 | )); |
| 5690 | assert_eq!(view.selected_step(), step); |
| 5691 | assert_eq!(view.body_scroll, 4); |
| 5692 | } |
| 5693 | |
| 5694 | #[test] |
| 5695 | fn fresh_and_complete_guides_remain_reachable_at_compact_and_normal_sizes() { |
| 5696 | let fresh = |
| 5697 | SetupWizardView::new_with_facts(SetupState::default(), Locale::En, facts(false)); |
| 5698 | let fresh_text = render_text(&fresh, 40, 12); |
| 5699 | assert!(fresh_text.contains(tr(Locale::En, MessageId::OnboardProviderTitle).as_ref())); |
| 5700 | let normal_text = render_text(&fresh, 100, 28); |
| 5701 | assert!(normal_text.contains(tr(Locale::En, MessageId::OnboardProviderTitle).as_ref())); |
| 5702 | assert!(normal_text.contains("local · stub-model")); |
| 5703 | |
| 5704 | let facts = facts(true); |
| 5705 | let complete = SetupWizardView::new_with_facts( |
| 5706 | complete_state(&facts.runtime_result), |
| 5707 | Locale::En, |
| 5708 | facts, |
| 5709 | ); |
| 5710 | let complete_text = render_text(&complete, 40, 12); |
| 5711 | assert!(complete_text.contains(tr(Locale::En, MessageId::OnboardReadyTitle).as_ref())); |
| 5712 | } |
| 5713 | } |
| 5714 |