返回 CodeWhale
mod.rs
根目录 / crates / tui / src / tui / setup / mod.rs
1 //! Constitution-first setup wizard shell (#3404/#3794).
2 //!
3 //! This module owns the reusable setup shell: step ordering, navigation,
4 //! per-step status projection, and the v0.8.67 constitution checkpoint action.
5 //! Individual step contents can grow behind [`SetupWizardStep`] without
6 //! changing the navigation or commit contract.
7
8 use std::borrow::Cow;
9 use std::path::Path;
10
11 use crossterm::event::{KeyCode, KeyEvent};
12 use ratatui::{
13 buffer::Buffer,
14 layout::Rect,
15 style::{Modifier, Style},
16 text::{Line, Span},
17 widgets::{Paragraph, Widget, Wrap},
18 };
19
20 use crate::config::{Config, has_api_key};
21 use crate::localization::{Locale, MessageId, tr};
22 use crate::palette;
23 use crate::prompts::{
24 BASE_PROMPT_OVERRIDE_OPT_IN_ENV, CONSTITUTION_OVERRIDE_FILE, base_prompt_override_opt_in,
25 };
26 use crate::tui::app::App;
27 use crate::tui::onboarding;
28 use crate::tui::views::{
29 ActionHint, ModalKind, ModalView, ViewAction, ViewEvent, render_modal_footer,
30 render_panel_scroll_rail, render_underwater_surface,
31 };
32
33 use codewhale_config::{
34 AutonomyPreference, ConstitutionAuthoring, ConstitutionChoice, ConstitutionSource,
35 ConstitutionValidity, InheritedConfigFacts, RuntimePostureSource, SetupState, SetupStep,
36 StepEntry, StepStatus, UserConstitution, UserConstitutionLoad,
37 user_constitution::MAX_NOTES_LEN,
38 };
39
40 mod fleet_draft;
41 mod model_draft;
42 mod operate;
43 mod persistence;
44 mod provider;
45 mod remote;
46 mod tools_mcp;
47
48 pub(crate) use fleet_draft::{draft_fleet_profile_with_model, workspace_fingerprint};
49 pub(crate) use model_draft::draft_constitution_with_model;
50 use persistence::SetupPersistenceFacts;
51 use remote::SetupRemoteFacts;
52
53 /// Target lane for the once-per-version constitution checkpoint. Bumped per
54 /// release when the bundled constitution materially changes, so existing users
55 /// re-acknowledge it once. 0.9.4 re-ships the Fleet/operate constitution.
56 pub const CONSTITUTION_CHECKPOINT_VERSION: &str = "0.9.4";
57
58 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
59 pub enum SetupCommitKind {
60 BundledConstitution,
61 DeferredConstitution,
62 }
63
64 pub trait SetupWizardStep {
65 fn id(&self) -> SetupStep;
66 fn title_id(&self) -> MessageId;
67 fn why_id(&self) -> MessageId;
68 fn required(&self) -> bool;
69 }
70
71 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
72 struct StaticSetupStep {
73 id: SetupStep,
74 title_id: MessageId,
75 why_id: MessageId,
76 required: bool,
77 }
78
79 impl SetupWizardStep for StaticSetupStep {
80 fn id(&self) -> SetupStep {
81 self.id
82 }
83
84 fn title_id(&self) -> MessageId {
85 self.title_id
86 }
87
88 fn why_id(&self) -> MessageId {
89 self.why_id
90 }
91
92 fn required(&self) -> bool {
93 self.required
94 }
95 }
96
97 const STEP_SPECS: [StaticSetupStep; 10] = [
98 StaticSetupStep {
99 id: SetupStep::Language,
100 title_id: MessageId::SetupStepLanguageTitle,
101 why_id: MessageId::SetupStepLanguageWhy,
102 required: true,
103 },
104 StaticSetupStep {
105 id: SetupStep::ProviderModel,
106 title_id: MessageId::SetupStepProviderModelTitle,
107 why_id: MessageId::SetupStepProviderModelWhy,
108 required: true,
109 },
110 StaticSetupStep {
111 id: SetupStep::TrustSandbox,
112 title_id: MessageId::SetupStepTrustSandboxTitle,
113 why_id: MessageId::SetupStepTrustSandboxWhy,
114 required: true,
115 },
116 StaticSetupStep {
117 id: SetupStep::Constitution,
118 title_id: MessageId::SetupStepConstitutionTitle,
119 why_id: MessageId::SetupStepConstitutionWhy,
120 required: true,
121 },
122 StaticSetupStep {
123 id: SetupStep::OperateFleet,
124 title_id: MessageId::SetupStepOperateFleetTitle,
125 why_id: MessageId::SetupStepOperateFleetWhy,
126 required: false,
127 },
128 StaticSetupStep {
129 id: SetupStep::Hotbar,
130 title_id: MessageId::SetupStepHotbarTitle,
131 why_id: MessageId::SetupStepHotbarWhy,
132 required: false,
133 },
134 StaticSetupStep {
135 id: SetupStep::ToolsMcp,
136 title_id: MessageId::SetupStepToolsMcpTitle,
137 why_id: MessageId::SetupStepToolsMcpWhy,
138 required: false,
139 },
140 StaticSetupStep {
141 id: SetupStep::RemoteRuntime,
142 title_id: MessageId::SetupStepRemoteRuntimeTitle,
143 why_id: MessageId::SetupStepRemoteRuntimeWhy,
144 required: false,
145 },
146 StaticSetupStep {
147 id: SetupStep::Persistence,
148 title_id: MessageId::SetupStepPersistenceTitle,
149 why_id: MessageId::SetupStepPersistenceWhy,
150 required: false,
151 },
152 StaticSetupStep {
153 id: SetupStep::Verification,
154 title_id: MessageId::SetupStepVerificationTitle,
155 why_id: MessageId::SetupStepVerificationWhy,
156 required: false,
157 },
158 ];
159
160 #[derive(Debug, Clone, PartialEq, Eq)]
161 pub struct SetupWizardView {
162 state: SetupState,
163 selected: usize,
164 locale: Locale,
165 facts: SetupRuntimeFacts,
166 guided_draft: GuidedConstitutionDraft,
167 freeform_note: String,
168 editing_freeform_note: bool,
169 guided_preview_seen: bool,
170 /// The keep-existing path mirrors the guided two-step: the first `K`
171 /// opens the rendered preview of the existing file, the second completes
172 /// the checkpoint without touching it.
173 existing_preview_seen: bool,
174 /// A model-drafted constitution awaiting ratification, installed by the
175 /// host after a successful one-shot draft (already sanitized + bounded).
176 /// Cleared whenever a guided answer changes so a stale draft can never be
177 /// ratified against fresh answers.
178 model_draft: Option<Box<UserConstitution>>,
179 /// Display label of the model that authored `model_draft` (safe metadata,
180 /// e.g. "GLM-5.2"), for provenance copy only.
181 model_draft_label: Option<String>,
182 runtime_preset: SetupRuntimePreset,
183 runtime_preset_preview_seen: bool,
184 body_scroll: usize,
185 }
186
187 #[derive(Debug, Clone, PartialEq, Eq)]
188 struct SetupRuntimeFacts {
189 provider: String,
190 model: String,
191 auth: String,
192 health: String,
193 provider_ready: bool,
194 provider_result: String,
195 work_intent: String,
196 approval: String,
197 shell: String,
198 allow_shell_enabled: bool,
199 trust: String,
200 sandbox: String,
201 sandbox_mode_value: String,
202 network: String,
203 network_default_value: String,
204 runtime_result: String,
205 operate_runtime_ready: bool,
206 operate_runtime_result: String,
207 fleet_roster_ready: bool,
208 fleet_roster_result: String,
209 operate_concurrency_result: String,
210 operate_result: String,
211 hotbar_bindings_result: String,
212 hotbar_actions_result: String,
213 hotbar_result: String,
214 tools_mcp_servers_result: String,
215 tools_mcp_skills_result: String,
216 tools_mcp_tools_result: String,
217 tools_mcp_plugins_result: String,
218 tools_mcp_hotbar_result: String,
219 tools_mcp_result: String,
220 tools_mcp_needs_action: bool,
221 tools_mcp_path_display: String,
222 tools_mcp_skills_path_display: String,
223 tools_mcp_plugins_path_display: String,
224 remote_clouds_result: String,
225 remote_bridges_result: String,
226 remote_providers_result: String,
227 remote_mode_result: String,
228 remote_command_provider: String,
229 remote_result: String,
230 /// The four observed remote modes (#3409). Empty only before facts load.
231 remote_modes: Vec<remote::RemoteModeFact>,
232 /// True when a mode is missing a token or config. Recorded as
233 /// `NeedsAction`, which by contract never blocks the ready screen.
234 remote_needs_action: bool,
235 persistence: SetupPersistenceFacts,
236 default_mode: String,
237 approval_policy_value: String,
238 project_override_warning: Option<String>,
239 constitution_autonomy: String,
240 constitution_file: SetupConstitutionFileState,
241 expert_override: SetupExpertOverrideState,
242 }
243
244 impl Default for SetupRuntimeFacts {
245 fn default() -> Self {
246 Self {
247 provider: "not loaded".to_string(),
248 model: "not loaded".to_string(),
249 auth: "not checked".to_string(),
250 health: "not checked".to_string(),
251 provider_ready: false,
252 provider_result: "provider/model not loaded".to_string(),
253 work_intent: "not loaded".to_string(),
254 approval: "not loaded".to_string(),
255 shell: "not loaded".to_string(),
256 allow_shell_enabled: false,
257 trust: "not loaded".to_string(),
258 sandbox: "not configured".to_string(),
259 sandbox_mode_value: "default".to_string(),
260 network: "not configured".to_string(),
261 network_default_value: "prompt".to_string(),
262 runtime_result: "runtime posture not loaded".to_string(),
263 operate_runtime_ready: false,
264 operate_runtime_result: "worker runtime not loaded".to_string(),
265 fleet_roster_ready: false,
266 fleet_roster_result: "Fleet roster not loaded".to_string(),
267 operate_concurrency_result: "concurrency not loaded".to_string(),
268 operate_result: "operate readiness not loaded".to_string(),
269 hotbar_bindings_result: "Hotbar config not loaded".to_string(),
270 hotbar_actions_result: "Hotbar actions not loaded".to_string(),
271 hotbar_result: "hotbar not loaded".to_string(),
272 tools_mcp_servers_result: "MCP config not loaded".to_string(),
273 tools_mcp_skills_result: "skills dir not loaded".to_string(),
274 tools_mcp_tools_result: "tools dir not loaded".to_string(),
275 tools_mcp_plugins_result: "plugins dir not loaded".to_string(),
276 tools_mcp_hotbar_result: "hotbar source metadata not loaded".to_string(),
277 tools_mcp_result: "tools/MCP not loaded".to_string(),
278 tools_mcp_needs_action: false,
279 tools_mcp_path_display: String::new(),
280 tools_mcp_skills_path_display: String::new(),
281 tools_mcp_plugins_path_display: String::new(),
282 remote_clouds_result: "remote cloud registry not loaded".to_string(),
283 remote_bridges_result: "remote bridge registry not loaded".to_string(),
284 remote_providers_result: "provider registry not loaded".to_string(),
285 remote_mode_result: "remote setup mode not loaded".to_string(),
286 remote_command_provider: "deepseek".to_string(),
287 remote_result: "remote runtime not loaded".to_string(),
288 remote_modes: Vec::new(),
289 remote_needs_action: false,
290 persistence: SetupPersistenceFacts::default(),
291 default_mode: "agent".to_string(),
292 approval_policy_value: "on-request".to_string(),
293 project_override_warning: None,
294 constitution_autonomy: "not loaded".to_string(),
295 constitution_file: SetupConstitutionFileState::NotChecked,
296 expert_override: SetupExpertOverrideState::NotChecked,
297 }
298 }
299 }
300
301 impl SetupRuntimeFacts {
302 fn from_app_config(app: &App, config: &Config) -> Self {
303 let expert_override = SetupExpertOverrideState::load();
304 let readiness = crate::provider_readiness::resolve_for_model(
305 config,
306 app.api_provider,
307 if app.auto_model { "auto" } else { &app.model },
308 &app.provider_health,
309 );
310 // A failed observed check remains retryable in route pickers, but the
311 // setup receipt must not certify it as healthy. Saved-unchecked and
312 // local-unchecked are honest reviewed configuration states; an actual
313 // session failure is NeedsAction until a later success replaces it.
314 let provider_ready = readiness.can_attempt()
315 && !matches!(
316 &readiness,
317 crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. }
318 );
319 let model = app.model_display_label();
320 let provider_name = if app.api_provider == crate::config::ApiProvider::Custom {
321 app.provider_identity_for_persistence().to_string()
322 } else {
323 app.api_provider.display_name().to_string()
324 };
325 let context_window = crate::route_budget::route_context_window_tokens(
326 app.api_provider,
327 &app.model,
328 app.active_route_limits,
329 );
330 let context_window_source = app.active_context_window_source.label();
331 let provider =
332 format!("{provider_name} · context {context_window} ({context_window_source})");
333 let auth = readiness.label().into_owned();
334 let health = if provider_ready {
335 format!("{}; route can be attempted", readiness.label())
336 } else if matches!(
337 &readiness,
338 crate::provider_readiness::ResolvedProviderReadiness::SavedLastCheckFailed { .. }
339 ) {
340 format!("{}; retry or open /provider", readiness.label())
341 } else if app.api_provider == crate::config::ApiProvider::OpenaiCodex {
342 format!(
343 "{}; run codex login, then grant exact read-only access with `codewhale auth external-consent --provider openai-codex --mode read-only`, or open /provider",
344 readiness.label()
345 )
346 } else if let Some(url) = crate::config::credential_help_for_provider_route(
347 app.api_provider,
348 &config.deepseek_base_url(),
349 )
350 .credential_url
351 {
352 format!(
353 "{}; credentials: {url}; open /provider to repair the route",
354 readiness.label()
355 )
356 } else {
357 format!(
358 "{}; {}; open /provider to repair the route",
359 readiness.label(),
360 crate::config::credential_help_for_provider_route(
361 app.api_provider,
362 &config.deepseek_base_url(),
363 )
364 .guidance
365 )
366 };
367 let provider_result = format!(
368 "provider={}, model={}, context_window={} ({}) auth={}, health={}",
369 app.provider_identity_for_persistence(),
370 model,
371 context_window,
372 context_window_source,
373 readiness.label(),
374 if provider_ready {
375 "attemptable"
376 } else {
377 "needs action"
378 }
379 );
380 let shell = if app.allow_shell { "enabled" } else { "hidden" }.to_string();
381 let trust = if app.trust_mode {
382 "trusted workspace / writes allowed by posture"
383 } else {
384 "workspace trust not elevated"
385 }
386 .to_string();
387 let sandbox = config
388 .sandbox_mode
389 .as_deref()
390 .filter(|mode| !mode.trim().is_empty())
391 .unwrap_or("default")
392 .to_string();
393 let sandbox_mode_value = sandbox.clone();
394 let network_default_value = config
395 .network
396 .as_ref()
397 .map_or("prompt".to_string(), |policy| policy.default.clone());
398 let network = config
399 .network
400 .as_ref()
401 .map_or("prompt by default".to_string(), |policy| {
402 format!("default {}", policy.default)
403 });
404 let runtime_result = format!(
405 "intent={}, approval={}, shell={}, trust={}, sandbox={}, network={}",
406 app.mode.as_setting(),
407 app.approval_mode
408 .permission_chip_label()
409 .to_ascii_lowercase(),
410 if app.allow_shell { "enabled" } else { "hidden" },
411 if app.trust_mode {
412 "trusted"
413 } else {
414 "workspace"
415 },
416 sandbox,
417 network
418 );
419 let operate = operate::SetupOperateFacts::from_app_config(app, config, provider_ready);
420 let known_hotbar_action_ids = app
421 .hotbar_actions
422 .iter()
423 .map(|action| action.id())
424 .collect::<Vec<_>>();
425 let hotbar_resolution = config.resolve_hotbar_bindings(&known_hotbar_action_ids);
426 let configured_hotbar_slots = config.hotbar.as_ref().map_or(0, Vec::len);
427 let hotbar_state = match config.hotbar.as_ref() {
428 None => "hidden",
429 Some(bindings) if bindings.is_empty() => "disabled",
430 Some(_) => "customized",
431 };
432 let active_hotbar_slots = hotbar_resolution.bindings.len();
433 let hotbar_warning_count = hotbar_resolution.warnings.len();
434 let hotbar_bindings_result = format!(
435 "{hotbar_state}; configured_slots={configured_hotbar_slots}; active_slots={active_hotbar_slots}; warnings={hotbar_warning_count}"
436 );
437 let hotbar_actions_result =
438 format!("{} bindable actions registered", app.hotbar_actions.len());
439 let hotbar_result = format!(
440 "state={hotbar_state}, configured_slots={configured_hotbar_slots}, active_slots={active_hotbar_slots}, actions={}, warnings={hotbar_warning_count}",
441 app.hotbar_actions.len()
442 );
443 let codewhale_home = setup_codewhale_home_dir();
444 let persistence = SetupPersistenceFacts::from_app_config(app, config, &codewhale_home);
445 let tools_mcp =
446 tools_mcp::SetupToolsMcpFacts::from_app_config(app, config, &codewhale_home);
447 let tools_mcp_servers_result = tools_mcp.servers_result;
448 let tools_mcp_skills_result = tools_mcp.skills_result;
449 let tools_mcp_tools_result = tools_mcp.tools_result;
450 let tools_mcp_plugins_result = tools_mcp.plugins_result;
451 let tools_mcp_hotbar_result = tools_mcp.hotbar_result;
452 let tools_mcp_result = tools_mcp.result;
453 let tools_mcp_needs_action = tools_mcp.needs_action;
454 let tools_mcp_path_display = tools_mcp.mcp_path_display;
455 let tools_mcp_skills_path_display = tools_mcp.skills_path_display;
456 let tools_mcp_plugins_path_display = tools_mcp.plugins_path_display;
457 let remote = SetupRemoteFacts::from_app(app);
458 let remote_needs_action = remote.needs_action();
459 let constitution_autonomy = UserConstitution::load()
460 .ok()
461 .and_then(|load| {
462 load.constitution().map(|constitution| {
463 autonomy_label(constitution.autonomy_preference, app.ui_locale).to_string()
464 })
465 })
466 .unwrap_or_else(|| tr(app.ui_locale, MessageId::SetupAutonomyUnspecified).to_string());
467 Self {
468 provider,
469 model,
470 auth,
471 health,
472 provider_ready,
473 provider_result,
474 work_intent: app.mode.display_name().to_string(),
475 approval: app
476 .approval_mode
477 .permission_chip_label()
478 .to_ascii_lowercase(),
479 shell,
480 allow_shell_enabled: app.allow_shell,
481 trust,
482 sandbox,
483 sandbox_mode_value,
484 network,
485 network_default_value,
486 runtime_result,
487 operate_runtime_ready: operate.runtime_ready,
488 operate_runtime_result: operate.runtime_result,
489 fleet_roster_ready: operate.roster_ready,
490 fleet_roster_result: operate.roster_result,
491 operate_concurrency_result: operate.concurrency_result,
492 operate_result: operate.result,
493 hotbar_bindings_result,
494 hotbar_actions_result,
495 hotbar_result,
496 tools_mcp_servers_result,
497 tools_mcp_skills_result,
498 tools_mcp_tools_result,
499 tools_mcp_plugins_result,
500 tools_mcp_hotbar_result,
501 tools_mcp_result,
502 tools_mcp_needs_action,
503 tools_mcp_path_display,
504 tools_mcp_skills_path_display,
505 tools_mcp_plugins_path_display,
506 remote_clouds_result: remote.clouds_result,
507 remote_bridges_result: remote.bridges_result,
508 remote_providers_result: remote.providers_result,
509 remote_mode_result: remote.mode_result,
510 remote_command_provider: remote.command_provider,
511 remote_result: remote.result,
512 remote_needs_action,
513 remote_modes: remote.modes,
514 persistence,
515 default_mode: app.mode.as_setting().to_string(),
516 approval_policy_value: config
517 .approval_policy
518 .as_deref()
519 .filter(|policy| !policy.trim().is_empty())
520 .unwrap_or("on-request")
521 .to_string(),
522 project_override_warning: project_runtime_override_warning(
523 &app.workspace,
524 app.ui_locale,
525 ),
526 constitution_autonomy,
527 constitution_file: SetupConstitutionFileState::load(),
528 expert_override,
529 }
530 }
531 }
532
533 fn setup_codewhale_home_dir() -> std::path::PathBuf {
534 codewhale_config::codewhale_home().unwrap_or_else(|_| {
535 crate::config::effective_home_dir().map_or_else(
536 || std::path::PathBuf::from(".codewhale"),
537 |home| home.join(".codewhale"),
538 )
539 })
540 }
541
542 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
543 pub enum SetupRuntimePreset {
544 AskFirst,
545 #[default]
546 NormalAgent,
547 HighTrustLocal,
548 }
549
550 impl SetupRuntimePreset {
551 const ALL: [Self; 3] = [Self::AskFirst, Self::NormalAgent, Self::HighTrustLocal];
552
553 fn from_key(key: char) -> Option<Self> {
554 match key {
555 '1' => Some(Self::AskFirst),
556 '2' => Some(Self::NormalAgent),
557 '3' => Some(Self::HighTrustLocal),
558 _ => None,
559 }
560 }
561
562 pub fn id(self) -> &'static str {
563 match self {
564 Self::AskFirst => "ask-first",
565 Self::NormalAgent => "normal-agent",
566 Self::HighTrustLocal => "high-trust-local",
567 }
568 }
569
570 fn title_id(self) -> MessageId {
571 match self {
572 Self::AskFirst => MessageId::SetupRuntimePresetAskFirstTitle,
573 Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentTitle,
574 Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustTitle,
575 }
576 }
577
578 fn description_id(self) -> MessageId {
579 match self {
580 Self::AskFirst => MessageId::SetupRuntimePresetAskFirstDescription,
581 Self::NormalAgent => MessageId::SetupRuntimePresetNormalAgentDescription,
582 Self::HighTrustLocal => MessageId::SetupRuntimePresetHighTrustDescription,
583 }
584 }
585
586 pub fn default_mode(self) -> &'static str {
587 match self {
588 Self::AskFirst => "plan",
589 Self::NormalAgent | Self::HighTrustLocal => "agent",
590 }
591 }
592
593 pub fn permission_posture(self) -> &'static str {
594 match self {
595 Self::AskFirst | Self::NormalAgent => "ask",
596 Self::HighTrustLocal => "full-access",
597 }
598 }
599
600 pub fn approval_policy(self) -> Option<&'static str> {
601 match self {
602 Self::AskFirst | Self::NormalAgent => Some("on-request"),
603 // Full Access lives in TUI settings; it is intentionally not a
604 // top-level approval_policy value.
605 Self::HighTrustLocal => None,
606 }
607 }
608
609 pub fn allow_shell(self) -> bool {
610 match self {
611 Self::AskFirst => false,
612 Self::NormalAgent | Self::HighTrustLocal => true,
613 }
614 }
615
616 pub fn sandbox_mode(self) -> &'static str {
617 match self {
618 Self::AskFirst => "read-only",
619 Self::NormalAgent => "workspace-write",
620 Self::HighTrustLocal => "danger-full-access",
621 }
622 }
623
624 pub fn result_summary(self) -> String {
625 let approval = self
626 .approval_policy()
627 .unwrap_or("unset (Full Access saved in TUI settings)");
628 format!(
629 "preset={}, default_mode={}, permission_posture={}, approval_policy={}, allow_shell={}, sandbox_mode={}, network=unchanged, trust=unchanged",
630 self.id(),
631 self.display_mode(),
632 self.permission_posture(),
633 approval,
634 self.allow_shell(),
635 self.sandbox_mode()
636 )
637 }
638
639 fn display_mode(self) -> &'static str {
640 match self {
641 Self::AskFirst => "plan",
642 Self::NormalAgent => "act",
643 Self::HighTrustLocal => "act + full-access",
644 }
645 }
646 }
647
648 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
649 enum SetupConstitutionFileState {
650 NotChecked,
651 Missing,
652 Loaded,
653 Empty,
654 Invalid,
655 Unreadable,
656 PathError,
657 }
658
659 impl SetupConstitutionFileState {
660 fn load() -> Self {
661 match UserConstitution::path() {
662 Ok(path) => Self::from_load(&UserConstitution::load_from(&path)),
663 Err(_) => Self::PathError,
664 }
665 }
666
667 fn from_load(load: &UserConstitutionLoad) -> Self {
668 match load {
669 UserConstitutionLoad::Missing => Self::Missing,
670 UserConstitutionLoad::Empty => Self::Empty,
671 UserConstitutionLoad::Invalid(_) => Self::Invalid,
672 UserConstitutionLoad::Unreadable(_) => Self::Unreadable,
673 UserConstitutionLoad::Loaded(_) => Self::Loaded,
674 }
675 }
676
677 fn label(self, choice: ConstitutionChoice, locale: Locale) -> Cow<'static, str> {
678 let id = match self {
679 Self::NotChecked => MessageId::SetupConstitutionFileNotChecked,
680 Self::Missing => MessageId::SetupConstitutionFileMissing,
681 Self::Loaded if choice == ConstitutionChoice::GuidedCustom => {
682 MessageId::SetupConstitutionFileLoadedSelected
683 }
684 Self::Loaded if choice.is_explicit() => MessageId::SetupConstitutionFileLoadedInactive,
685 Self::Loaded => MessageId::SetupConstitutionFileLoadedUnselected,
686 Self::Empty => MessageId::SetupConstitutionFileEmpty,
687 Self::Invalid => MessageId::SetupConstitutionFileInvalid,
688 Self::Unreadable => MessageId::SetupConstitutionFileUnreadable,
689 Self::PathError => MessageId::SetupConstitutionFilePathError,
690 };
691 tr(locale, id)
692 }
693 }
694
695 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
696 enum SetupExpertOverrideState {
697 NotChecked,
698 Missing,
699 Active,
700 Disabled,
701 Empty,
702 Unreadable,
703 PathError,
704 }
705
706 impl SetupExpertOverrideState {
707 fn load() -> Self {
708 let Some(path) = expert_override_path() else {
709 return Self::PathError;
710 };
711 match std::fs::read_to_string(&path) {
712 Ok(raw) if raw.trim().is_empty() => Self::Empty,
713 Ok(_) if base_prompt_override_opt_in() => Self::Active,
714 Ok(_) => Self::Disabled,
715 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Self::Missing,
716 Err(_) => Self::Unreadable,
717 }
718 }
719
720 fn is_active(self) -> bool {
721 matches!(self, Self::Active)
722 }
723
724 fn label(self, locale: Locale) -> Cow<'static, str> {
725 match self {
726 Self::NotChecked => tr(locale, MessageId::SetupExpertOverrideNotChecked),
727 Self::Missing => tr(locale, MessageId::SetupExpertOverrideMissing),
728 Self::Active => tr(locale, MessageId::SetupExpertOverrideActive),
729 Self::Disabled => tr(locale, MessageId::SetupExpertOverrideDisabled)
730 .replace("{env}", BASE_PROMPT_OVERRIDE_OPT_IN_ENV)
731 .into(),
732 Self::Empty => tr(locale, MessageId::SetupExpertOverrideEmpty),
733 Self::Unreadable => tr(locale, MessageId::SetupExpertOverrideUnreadable),
734 Self::PathError => tr(locale, MessageId::SetupExpertOverridePathError),
735 }
736 }
737 }
738
739 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
740 pub(crate) struct GuidedConstitutionDraft {
741 purpose: GuidedPurpose,
742 autonomy: AutonomyPreference,
743 evidence: GuidedEvidence,
744 communication: GuidedCommunication,
745 privacy: GuidedPrivacy,
746 principles: GuidedPrinciples,
747 }
748
749 impl Default for GuidedConstitutionDraft {
750 fn default() -> Self {
751 Self {
752 purpose: GuidedPurpose::Coding,
753 autonomy: AutonomyPreference::Balanced,
754 evidence: GuidedEvidence::TestsAndReceipts,
755 communication: GuidedCommunication::Concise,
756 privacy: GuidedPrivacy::StandardCare,
757 principles: GuidedPrinciples::ScopedChanges,
758 }
759 }
760 }
761
762 impl GuidedConstitutionDraft {
763 fn cycle(&mut self, key: char) -> bool {
764 match key {
765 '1' => self.purpose = self.purpose.next(),
766 '2' => self.autonomy = next_guided_autonomy(self.autonomy),
767 '3' => self.evidence = self.evidence.next(),
768 '4' => self.communication = self.communication.next(),
769 '5' => self.privacy = self.privacy.next(),
770 '6' => self.principles = self.principles.next(),
771 _ => return false,
772 }
773 true
774 }
775
776 #[cfg(test)]
777 fn to_constitution(self, locale: Locale) -> UserConstitution {
778 self.to_constitution_with_freeform(locale, None)
779 }
780
781 fn to_constitution_with_freeform(
782 self,
783 locale: Locale,
784 freeform_note: Option<&str>,
785 ) -> UserConstitution {
786 let mut notes = self.notes(locale);
787 if let Some(note) = freeform_note.map(str::trim).filter(|note| !note.is_empty()) {
788 let own_words = match locale {
789 Locale::Ja => format!(
790 "\nユーザー自由原則:{}",
791 bounded_freeform_note(note, MAX_NOTES_LEN)
792 ),
793 Locale::ZhHans => format!(
794 "\n用户自定义准则:{}",
795 bounded_freeform_note(note, MAX_NOTES_LEN)
796 ),
797 Locale::ZhHant => format!(
798 "\n使用者自由原則:{}",
799 bounded_freeform_note(note, MAX_NOTES_LEN)
800 ),
801 Locale::PtBr => format!(
802 "\nPrincípio livre do usuário: {}",
803 bounded_freeform_note(note, MAX_NOTES_LEN)
804 ),
805 Locale::Es419 => format!(
806 "\nPrincipio libre del usuario: {}",
807 bounded_freeform_note(note, MAX_NOTES_LEN)
808 ),
809 Locale::Vi => format!(
810 "\nNguyên tắc tự do của người dùng: {}",
811 bounded_freeform_note(note, MAX_NOTES_LEN)
812 ),
813 Locale::Ko => format!(
814 "\n사용자 자유 원칙: {}",
815 bounded_freeform_note(note, MAX_NOTES_LEN)
816 ),
817 Locale::Ca => format!(
818 "\nPrincipi lliure de l'usuari: {}",
819 bounded_freeform_note(note, MAX_NOTES_LEN)
820 ),
821 Locale::De => format!(
822 "\nFreitext-Prinzip des Nutzers: {}",
823 bounded_freeform_note(note, MAX_NOTES_LEN)
824 ),
825 Locale::Fr => format!(
826 "\nPrincipe en texte libre de l'utilisateur : {}",
827 bounded_freeform_note(note, MAX_NOTES_LEN)
828 ),
829 Locale::Id => format!(
830 "\nPrinsip bebas pengguna: {}",
831 bounded_freeform_note(note, MAX_NOTES_LEN)
832 ),
833 Locale::Hi => format!(
834 "\nउपयोगकर्ता मुक्त-पाठ सिद्धांत: {}",
835 bounded_freeform_note(note, MAX_NOTES_LEN)
836 ),
837 Locale::Ru => format!(
838 "\nСвободный принцип пользователя: {}",
839 bounded_freeform_note(note, MAX_NOTES_LEN)
840 ),
841 Locale::Uk => format!(
842 "\nВільний принцип користувача: {}",
843 bounded_freeform_note(note, MAX_NOTES_LEN)
844 ),
845 _ => format!(
846 "\nUser freeform principle: {}",
847 bounded_freeform_note(note, MAX_NOTES_LEN)
848 ),
849 };
850 notes.push_str(&own_words);
851 }
852 UserConstitution {
853 language: Some(locale.tag().to_string()),
854 about: Some(self.purpose.about(locale).to_string()),
855 working_style: vec![
856 self.purpose.working_style(locale).to_string(),
857 self.communication.working_style(locale).to_string(),
858 self.evidence.working_style(locale).to_string(),
859 self.privacy.working_style(locale).to_string(),
860 ],
861 priorities: vec![
862 authority_priority(locale).to_string(),
863 autonomy_priority(self.autonomy, locale).to_string(),
864 self.privacy.escalation_rule(locale).to_string(),
865 ],
866 autonomy_preference: self.autonomy,
867 notes: Some(notes),
868 ..UserConstitution::default()
869 }
870 }
871
872 fn notes(self, locale: Locale) -> String {
873 let notes = tr(locale, MessageId::SetupGuidedNotes);
874 notes
875 .replace("{purpose}", &self.purpose.label(locale))
876 .replace("{initiative}", autonomy_label(self.autonomy, locale))
877 .replace("{evidence}", &self.evidence.label(locale))
878 .replace("{communication}", self.communication.label(locale))
879 .replace("{privacy}", self.privacy.label(locale))
880 .replace("{principles}", self.principles.label(locale))
881 .replace("{notes}", self.principles.note(locale))
882 .to_string()
883 }
884 }
885
886 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
887 enum GuidedPurpose {
888 Coding,
889 Research,
890 Operations,
891 Mixed,
892 }
893
894 impl GuidedPurpose {
895 fn next(self) -> Self {
896 match self {
897 Self::Coding => Self::Research,
898 Self::Research => Self::Operations,
899 Self::Operations => Self::Mixed,
900 Self::Mixed => Self::Coding,
901 }
902 }
903
904 fn label(self, locale: Locale) -> Cow<'static, str> {
905 match self {
906 Self::Coding => tr(locale, MessageId::SetupGuidedPurposeCoding),
907 Self::Research => tr(locale, MessageId::SetupGuidedPurposeResearch),
908 Self::Operations => tr(locale, MessageId::SetupGuidedPurposeOperations),
909 Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeMixed),
910 }
911 }
912
913 fn about(self, locale: Locale) -> Cow<'static, str> {
914 match self {
915 Self::Coding => tr(locale, MessageId::SetupGuidedPurposeAboutCoding),
916 Self::Research => tr(locale, MessageId::SetupGuidedPurposeAboutResearch),
917 Self::Operations => tr(locale, MessageId::SetupGuidedPurposeAboutOperations),
918 Self::Mixed => tr(locale, MessageId::SetupGuidedPurposeAboutMixed),
919 }
920 }
921
922 fn working_style(self, locale: Locale) -> Cow<'static, str> {
923 match self {
924 Self::Coding => tr(locale, MessageId::SetupGuidedStyleCoding),
925 Self::Research => tr(locale, MessageId::SetupGuidedStyleResearch),
926 Self::Operations => tr(locale, MessageId::SetupGuidedStyleOperations),
927 Self::Mixed => tr(locale, MessageId::SetupGuidedStyleMixed),
928 }
929 }
930 }
931
932 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
933 enum GuidedEvidence {
934 Assumptions,
935 TestsAndReceipts,
936 ReleaseReceipts,
937 }
938
939 impl GuidedEvidence {
940 fn next(self) -> Self {
941 match self {
942 Self::Assumptions => Self::TestsAndReceipts,
943 Self::TestsAndReceipts => Self::ReleaseReceipts,
944 Self::ReleaseReceipts => Self::Assumptions,
945 }
946 }
947
948 fn label(self, locale: Locale) -> Cow<'static, str> {
949 match self {
950 Self::Assumptions => tr(locale, MessageId::SetupGuidedEvidenceAssumptions),
951 Self::TestsAndReceipts => tr(locale, MessageId::SetupGuidedEvidenceTestsAndReceipts),
952 Self::ReleaseReceipts => tr(locale, MessageId::SetupGuidedEvidenceReleaseReceipts),
953 }
954 }
955
956 fn working_style(self, locale: Locale) -> &'static str {
957 match (locale, self) {
958 (Locale::Ja, Self::Assumptions) => {
959 "完了を主張する前に、前提、不明点、残るリスクを要約する。"
960 }
961 (Locale::Ja, Self::TestsAndReceipts) => {
962 "不確実性を減らせるときは、コマンド、テスト、スクリーンショット、引用で具体的に検証する。"
963 }
964 (Locale::Ja, Self::ReleaseReceipts) => {
965 "重要な主張とリリース証拠には、ファイル、コマンド、スクリーンショット、CI、出典を示す。"
966 }
967 (Locale::ZhHans, Self::Assumptions) => "在宣称完成前总结假设、未知和剩余风险。",
968 (Locale::ZhHans, Self::TestsAndReceipts) => {
969 "在能降低不确定性时,用命令、测试、截图或引用给出具体验证。"
970 }
971 (Locale::ZhHans, Self::ReleaseReceipts) => {
972 "对重要结论和发布证据标注文件、命令、截图、CI 或来源。"
973 }
974 (Locale::ZhHant, Self::Assumptions) => "在宣稱完成前總結假設、未知和剩餘風險。",
975 (Locale::ZhHant, Self::TestsAndReceipts) => {
976 "在能降低不確定性時,用命令、測試、截圖或引用給出具體驗證。"
977 }
978 (Locale::ZhHant, Self::ReleaseReceipts) => {
979 "對重要結論和發布證據標註檔案、命令、截圖、CI 或來源。"
980 }
981 (Locale::PtBr, Self::Assumptions) => {
982 "Resuma premissas, desconhecidos e risco restante antes de dizer que concluiu."
983 }
984 (Locale::PtBr, Self::TestsAndReceipts) => {
985 "Use comandos, testes, screenshots ou citações quando reduzirem a incerteza."
986 }
987 (Locale::PtBr, Self::ReleaseReceipts) => {
988 "Cite arquivos, comandos, screenshots, CI ou fontes para afirmações materiais e evidência de release."
989 }
990 (Locale::Es419, Self::Assumptions) => {
991 "Resume supuestos, incógnitas y riesgo restante antes de afirmar que terminaste."
992 }
993 (Locale::Es419, Self::TestsAndReceipts) => {
994 "Usa comandos, pruebas, capturas o citas cuando reduzcan materialmente la incertidumbre."
995 }
996 (Locale::Es419, Self::ReleaseReceipts) => {
997 "Cita archivos, comandos, capturas, CI o fuentes para afirmaciones materiales y evidencia de release."
998 }
999 (Locale::Vi, Self::Assumptions) => {
1000 "Tóm tắt giả định, điều chưa biết và rủi ro còn lại trước khi tuyên bố hoàn tất."
1001 }
1002 (Locale::Vi, Self::TestsAndReceipts) => {
1003 "Dùng lệnh, kiểm thử, ảnh chụp hoặc trích dẫn khi chúng giảm đáng kể bất định."
1004 }
1005 (Locale::Vi, Self::ReleaseReceipts) => {
1006 "Trích dẫn tệp, lệnh, ảnh chụp, CI hoặc nguồn cho tuyên bố quan trọng và bằng chứng phát hành."
1007 }
1008 (Locale::Ko, Self::Assumptions) => {
1009 "완료를 주장하기 전에 가정, 불확실한 점, 남은 위험을 요약한다."
1010 }
1011 (Locale::Ko, Self::TestsAndReceipts) => {
1012 "불확실성을 실질적으로 줄일 수 있을 때는 명령어, 테스트, 스크린샷, 인용으로 구체적으로 검증한다."
1013 }
1014 (Locale::Ko, Self::ReleaseReceipts) => {
1015 "중요한 주장과 릴리스 근거에는 파일 경로, 명령어, 스크린샷, CI, 출처를 제시한다."
1016 }
1017 (Locale::Ca, Self::Assumptions) => {
1018 "Resumeix supòsits, incògnites i risc pendent abans de dir que has acabat."
1019 }
1020 (Locale::Ca, Self::TestsAndReceipts) => {
1021 "Fes servir ordres, tests, captures de pantalla o citacions quan redueixin materialment la incertesa."
1022 }
1023 (Locale::Ca, Self::ReleaseReceipts) => {
1024 "Cita rutes de fitxers, ordres, captures de pantalla, CI o fonts per a afirmacions materials i evidència de release."
1025 }
1026 (Locale::De, Self::Assumptions) => {
1027 "Fasse Annahmen, Unbekannte und Restrisiken zusammen, bevor du Fertigstellung behauptest."
1028 }
1029 (Locale::De, Self::TestsAndReceipts) => {
1030 "Nutze Befehle, Tests, Screenshots oder Zitate, wenn sie die Unsicherheit wesentlich verringern."
1031 }
1032 (Locale::De, Self::ReleaseReceipts) => {
1033 "Nenne Dateipfade, Befehle, Screenshots, CI oder Quellen für wesentliche Aussagen und Release-Nachweise."
1034 }
1035 (Locale::Fr, Self::Assumptions) => {
1036 "Résumez les hypothèses, les inconnues et le risque restant avant d'annoncer la fin du travail."
1037 }
1038 (Locale::Fr, Self::TestsAndReceipts) => {
1039 "Utilisez commandes, tests, captures d'écran ou citations quand ils réduisent sensiblement l'incertitude."
1040 }
1041 (Locale::Fr, Self::ReleaseReceipts) => {
1042 "Citez chemins de fichiers, commandes, captures d'écran, CI ou sources pour les affirmations importantes et les preuves de release."
1043 }
1044 (Locale::Id, Self::Assumptions) => {
1045 "Ringkas asumsi, hal yang belum diketahui, dan risiko tersisa sebelum mengklaim selesai."
1046 }
1047 (Locale::Id, Self::TestsAndReceipts) => {
1048 "Gunakan perintah, tes, tangkapan layar, atau kutipan bila secara nyata mengurangi ketidakpastian."
1049 }
1050 (Locale::Id, Self::ReleaseReceipts) => {
1051 "Kutip path file, perintah, tangkapan layar, CI, atau sumber untuk klaim material dan bukti rilis."
1052 }
1053 (Locale::Hi, Self::Assumptions) => {
1054 "पूर्णता का दावा करने से पहले धारणाएँ, अज्ञात बातें और शेष जोखिम सारांशित करें।"
1055 }
1056 (Locale::Hi, Self::TestsAndReceipts) => {
1057 "जब वे अनिश्चितता सार्थक रूप से घटाएँ तो कमांड, टेस्ट, स्क्रीनशॉट या उद्धरण उपयोग करें।"
1058 }
1059 (Locale::Hi, Self::ReleaseReceipts) => {
1060 "महत्वपूर्ण दावों और रिलीज़ साक्ष्य के लिए फ़ाइल पथ, कमांड, स्क्रीनशॉट, CI या स्रोत उद्धृत करें।"
1061 }
1062 (Locale::Ru, Self::Assumptions) => {
1063 "Прежде чем заявить о завершении, перечислите предположения, неизвестные и оставшиеся риски."
1064 }
1065 (Locale::Ru, Self::TestsAndReceipts) => {
1066 "Используйте команды, тесты, скриншоты или цитаты, когда они существенно снижают неопределённость."
1067 }
1068 (Locale::Ru, Self::ReleaseReceipts) => {
1069 "Указывайте пути файлов, команды, скриншоты, CI или источники для существенных утверждений и доказательств релиза."
1070 }
1071 (Locale::Uk, Self::Assumptions) => {
1072 "Перш ніж заявити про завершення, підсумуйте припущення, невідомі та залишкові ризики."
1073 }
1074 (Locale::Uk, Self::TestsAndReceipts) => {
1075 "Використовуйте команди, тести, скриншоти або цитати, коли вони суттєво зменшують невизначеність."
1076 }
1077 (Locale::Uk, Self::ReleaseReceipts) => {
1078 "Посилайтеся на шляхи файлів, команди, скриншоти, CI або джерела для суттєвих тверджень і доказів релізу."
1079 }
1080 (_, Self::Assumptions) => {
1081 "Summarize assumptions, unknowns, and remaining risk before claiming completion."
1082 }
1083 (_, Self::TestsAndReceipts) => {
1084 "Use commands, tests, screenshots, or citations when they materially reduce uncertainty."
1085 }
1086 (_, Self::ReleaseReceipts) => {
1087 "Cite file paths, commands, screenshots, CI, or sources for material claims and release evidence."
1088 }
1089 }
1090 }
1091 }
1092
1093 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1094 enum GuidedCommunication {
1095 Concise,
1096 Teaching,
1097 Direct,
1098 }
1099
1100 impl GuidedCommunication {
1101 fn next(self) -> Self {
1102 match self {
1103 Self::Concise => Self::Teaching,
1104 Self::Teaching => Self::Direct,
1105 Self::Direct => Self::Concise,
1106 }
1107 }
1108
1109 fn label(self, locale: Locale) -> &'static str {
1110 match (locale, self) {
1111 (Locale::Ja, Self::Concise) => "簡潔",
1112 (Locale::Ja, Self::Teaching) => "説明重視",
1113 (Locale::Ja, Self::Direct) => "直接的",
1114 (Locale::ZhHans, Self::Concise) => "简洁",
1115 (Locale::ZhHans, Self::Teaching) => "教学式",
1116 (Locale::ZhHans, Self::Direct) => "直接",
1117 (Locale::ZhHant, Self::Concise) => "簡潔",
1118 (Locale::ZhHant, Self::Teaching) => "教學式",
1119 (Locale::ZhHant, Self::Direct) => "直接",
1120 (Locale::PtBr, Self::Concise) => "conciso",
1121 (Locale::PtBr, Self::Teaching) => "didático",
1122 (Locale::PtBr, Self::Direct) => "direto",
1123 (Locale::Es419, Self::Concise) => "conciso",
1124 (Locale::Es419, Self::Teaching) => "didáctico",
1125 (Locale::Es419, Self::Direct) => "directo",
1126 (Locale::Vi, Self::Concise) => "ngắn gọn",
1127 (Locale::Vi, Self::Teaching) => "giảng giải",
1128 (Locale::Vi, Self::Direct) => "trực tiếp",
1129 (Locale::Ko, Self::Concise) => "간결함",
1130 (Locale::Ko, Self::Teaching) => "설명 중심",
1131 (Locale::Ko, Self::Direct) => "직설적",
1132 (Locale::Ca, Self::Concise) => "concís",
1133 (Locale::Ca, Self::Teaching) => "didàctic",
1134 (Locale::Ca, Self::Direct) => "directe",
1135 (Locale::De, Self::Concise) => "prägnant",
1136 (Locale::De, Self::Teaching) => "lehrend",
1137 (Locale::De, Self::Direct) => "direkt",
1138 (Locale::Fr, Self::Concise) => "concis",
1139 (Locale::Fr, Self::Teaching) => "pédagogique",
1140 (Locale::Fr, Self::Direct) => "direct",
1141 (Locale::Id, Self::Concise) => "ringkas",
1142 (Locale::Id, Self::Teaching) => "mengajar",
1143 (Locale::Id, Self::Direct) => "langsung",
1144 (Locale::Hi, Self::Concise) => "संक्षिप्त",
1145 (Locale::Hi, Self::Teaching) => "शिक्षणपरक",
1146 (Locale::Hi, Self::Direct) => "सीधा",
1147 (Locale::Ru, Self::Concise) => "краткий",
1148 (Locale::Ru, Self::Teaching) => "обучающий",
1149 (Locale::Ru, Self::Direct) => "прямой",
1150 (Locale::Uk, Self::Concise) => "стислий",
1151 (Locale::Uk, Self::Teaching) => "навчальний",
1152 (Locale::Uk, Self::Direct) => "прямий",
1153 (_, Self::Concise) => "concise",
1154 (_, Self::Teaching) => "teaching",
1155 (_, Self::Direct) => "direct",
1156 }
1157 }
1158
1159 fn working_style(self, locale: Locale) -> &'static str {
1160 match (locale, self) {
1161 (Locale::Ja, Self::Concise) => "更新は簡潔にし、重要なトレードオフだけ短く説明する。",
1162 (Locale::Ja, Self::Teaching) => {
1163 "重要な推論とトレードオフを、ユーザーが仕組みを理解できる程度に説明する。"
1164 }
1165 (Locale::Ja, Self::Direct) => {
1166 "阻塞、リスク、不確実性を直接述べ、装飾的な文案を避ける。"
1167 }
1168 (Locale::ZhHans, Self::Concise) => "保持更新简洁,并只解释重要取舍。",
1169 (Locale::ZhHans, Self::Teaching) => "解释关键推理和取舍,让用户能理解系统。",
1170 (Locale::ZhHans, Self::Direct) => "直接说明阻塞、风险和不确定性,避免装饰性文案。",
1171 (Locale::ZhHant, Self::Concise) => "保持更新簡潔,並只解釋重要取捨。",
1172 (Locale::ZhHant, Self::Teaching) => "解釋關鍵推理和取捨,讓使用者能理解系統。",
1173 (Locale::ZhHant, Self::Direct) => "直接說明阻塞、風險和不確定性,避免裝飾性文案。",
1174 (Locale::PtBr, Self::Concise) => {
1175 "Mantenha atualizações concisas e explique brevemente só os tradeoffs importantes."
1176 }
1177 (Locale::PtBr, Self::Teaching) => {
1178 "Explique raciocínio e tradeoffs principais o bastante para o usuário entender o sistema."
1179 }
1180 (Locale::PtBr, Self::Direct) => {
1181 "Seja direto sobre bloqueios, risco e incerteza; evite texto ornamental."
1182 }
1183 (Locale::Es419, Self::Concise) => {
1184 "Mantén las actualizaciones concisas y explica brevemente solo los tradeoffs importantes."
1185 }
1186 (Locale::Es419, Self::Teaching) => {
1187 "Explica el razonamiento y los tradeoffs clave lo suficiente para que el usuario entienda el sistema."
1188 }
1189 (Locale::Es419, Self::Direct) => {
1190 "Sé directo sobre bloqueos, riesgo e incertidumbre; evita texto ornamental."
1191 }
1192 (Locale::Vi, Self::Concise) => {
1193 "Giữ cập nhật ngắn gọn và chỉ giải thích ngắn các đánh đổi quan trọng."
1194 }
1195 (Locale::Vi, Self::Teaching) => {
1196 "Giải thích suy luận và đánh đổi chính đủ để người dùng hiểu hệ thống."
1197 }
1198 (Locale::Vi, Self::Direct) => {
1199 "Nói thẳng về điểm chặn, rủi ro và bất định; tránh câu chữ trang trí."
1200 }
1201 (Locale::Ko, Self::Concise) => {
1202 "업데이트는 간결하게 유지하고, 중요한 트레이드오프만 짧게 설명한다."
1203 }
1204 (Locale::Ko, Self::Teaching) => {
1205 "사용자가 시스템을 이해할 수 있을 만큼 핵심 추론과 트레이드오프를 설명한다."
1206 }
1207 (Locale::Ko, Self::Direct) => {
1208 "차단 요인, 위험, 불확실성을 직설적으로 말하고 장식적인 표현은 피한다."
1209 }
1210 (Locale::Ca, Self::Concise) => {
1211 "Mantén les actualitzacions concises i explica breument només els compromisos importants."
1212 }
1213 (Locale::Ca, Self::Teaching) => {
1214 "Explica el raonament i els compromisos clau prou perquè l'usuari pugui entendre el sistema."
1215 }
1216 (Locale::Ca, Self::Direct) => {
1217 "Sigues directe sobre bloquejos, risc i incertesa; evita el text ornamental."
1218 }
1219 (Locale::De, Self::Concise) => {
1220 "Halte Aktualisierungen knapp und erkläre wichtige Trade-offs nur kurz."
1221 }
1222 (Locale::De, Self::Teaching) => {
1223 "Erkläre zentrale Begründungen und Trade-offs so weit, dass der Nutzer das System verstehen kann."
1224 }
1225 (Locale::De, Self::Direct) => {
1226 "Sei direkt bei Blockern, Risiken und Unsicherheit; vermeide dekorative Formulierungen."
1227 }
1228 (Locale::Fr, Self::Concise) => {
1229 "Gardez les mises à jour concises et n'expliquez que brièvement les arbitrages importants."
1230 }
1231 (Locale::Fr, Self::Teaching) => {
1232 "Expliquez le raisonnement et les arbitrages clés assez pour que l'utilisateur comprenne le système."
1233 }
1234 (Locale::Fr, Self::Direct) => {
1235 "Soyez direct sur les blocages, les risques et l'incertitude ; évitez le texte ornemental."
1236 }
1237 (Locale::Id, Self::Concise) => {
1238 "Jaga pembaruan tetap ringkas dan jelaskan tradeoff penting secara singkat."
1239 }
1240 (Locale::Id, Self::Teaching) => {
1241 "Jelaskan penalaran dan tradeoff kunci secukupnya agar pengguna dapat memahami sistem."
1242 }
1243 (Locale::Id, Self::Direct) => {
1244 "Bicara langsung soal penghambat, risiko, dan ketidakpastian; hindari teks hiasan."
1245 }
1246 (Locale::Hi, Self::Concise) => "अपडेट संक्षिप्त रखें और महत्वपूर्ण ट्रेडऑफ़ संक्षेप में समझाएँ।",
1247 (Locale::Hi, Self::Teaching) => {
1248 "मुख्य तर्क और ट्रेडऑफ़ इतना समझाएँ कि उपयोगकर्ता सिस्टम समझ सके।"
1249 }
1250 (Locale::Hi, Self::Direct) => {
1251 "रुकावटों, जोखिम और अनिश्चितता के बारे में सीधे बोलें; सजावटी भाषा से बचें।"
1252 }
1253 (Locale::Ru, Self::Concise) => {
1254 "Держите обновления краткими и лишь коротко поясняйте важные компромиссы."
1255 }
1256 (Locale::Ru, Self::Teaching) => {
1257 "Объясняйте ключевые рассуждения и компромиссы настолько, чтобы пользователь мог понять систему."
1258 }
1259 (Locale::Ru, Self::Direct) => {
1260 "Говорите прямо о блокерах, рисках и неопределённости; избегайте декоративных формулировок."
1261 }
1262 (Locale::Uk, Self::Concise) => {
1263 "Тримайте оновлення стислими й лише коротко пояснюйте важливі компроміси."
1264 }
1265 (Locale::Uk, Self::Teaching) => {
1266 "Пояснюйте ключові міркування та компроміси настільки, щоб користувач міг зрозуміти систему."
1267 }
1268 (Locale::Uk, Self::Direct) => {
1269 "Говоріть прямо про блокери, ризики та невизначеність; уникайте декоративних формулювань."
1270 }
1271 (_, Self::Concise) => "Keep updates concise and explain important tradeoffs briefly.",
1272 (_, Self::Teaching) => {
1273 "Explain key reasoning and tradeoffs enough that the user can learn the system."
1274 }
1275 (_, Self::Direct) => {
1276 "Be direct about blockers, risk, and uncertainty; avoid ornamental copy."
1277 }
1278 }
1279 }
1280 }
1281
1282 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1283 enum GuidedPrivacy {
1284 StandardCare,
1285 StrictBoundaries,
1286 ProjectLocal,
1287 }
1288
1289 impl GuidedPrivacy {
1290 fn next(self) -> Self {
1291 match self {
1292 Self::StandardCare => Self::StrictBoundaries,
1293 Self::StrictBoundaries => Self::ProjectLocal,
1294 Self::ProjectLocal => Self::StandardCare,
1295 }
1296 }
1297
1298 fn label(self, locale: Locale) -> &'static str {
1299 match (locale, self) {
1300 (Locale::Ja, Self::StandardCare) => "標準保護",
1301 (Locale::Ja, Self::StrictBoundaries) => "厳格な境界",
1302 (Locale::Ja, Self::ProjectLocal) => "プロジェクト内メモリ",
1303 (Locale::ZhHans, Self::StandardCare) => "标准保护",
1304 (Locale::ZhHans, Self::StrictBoundaries) => "严格边界",
1305 (Locale::ZhHans, Self::ProjectLocal) => "项目内记忆",
1306 (Locale::ZhHant, Self::StandardCare) => "標準保護",
1307 (Locale::ZhHant, Self::StrictBoundaries) => "嚴格邊界",
1308 (Locale::ZhHant, Self::ProjectLocal) => "專案內記憶",
1309 (Locale::PtBr, Self::StandardCare) => "cuidado padrão",
1310 (Locale::PtBr, Self::StrictBoundaries) => "limites estritos",
1311 (Locale::PtBr, Self::ProjectLocal) => "memória local do projeto",
1312 (Locale::Es419, Self::StandardCare) => "cuidado estándar",
1313 (Locale::Es419, Self::StrictBoundaries) => "límites estrictos",
1314 (Locale::Es419, Self::ProjectLocal) => "memoria local del proyecto",
1315 (Locale::Vi, Self::StandardCare) => "bảo vệ tiêu chuẩn",
1316 (Locale::Vi, Self::StrictBoundaries) => "ranh giới nghiêm ngặt",
1317 (Locale::Vi, Self::ProjectLocal) => "bộ nhớ trong dự án",
1318 (Locale::Ko, Self::StandardCare) => "표준 보호",
1319 (Locale::Ko, Self::StrictBoundaries) => "엄격한 경계",
1320 (Locale::Ko, Self::ProjectLocal) => "프로젝트 내 메모리",
1321 (Locale::Ca, Self::StandardCare) => "cura estàndard",
1322 (Locale::Ca, Self::StrictBoundaries) => "límits estrictes",
1323 (Locale::Ca, Self::ProjectLocal) => "memòria local del projecte",
1324 (Locale::De, Self::StandardCare) => "Standardvorsorge",
1325 (Locale::De, Self::StrictBoundaries) => "strenge Grenzen",
1326 (Locale::De, Self::ProjectLocal) => "projektlokaler Speicher",
1327 (Locale::Fr, Self::StandardCare) => "soin standard",
1328 (Locale::Fr, Self::StrictBoundaries) => "limites strictes",
1329 (Locale::Fr, Self::ProjectLocal) => "mémoire locale au projet",
1330 (Locale::Id, Self::StandardCare) => "perlindungan standar",
1331 (Locale::Id, Self::StrictBoundaries) => "batasan ketat",
1332 (Locale::Id, Self::ProjectLocal) => "memori lokal proyek",
1333 (Locale::Hi, Self::StandardCare) => "मानक सावधानी",
1334 (Locale::Hi, Self::StrictBoundaries) => "सख्त सीमाएँ",
1335 (Locale::Hi, Self::ProjectLocal) => "प्रोजेक्ट-स्थानीय मेमोरी",
1336 (Locale::Ru, Self::StandardCare) => "стандартная осторожность",
1337 (Locale::Ru, Self::StrictBoundaries) => "строгие границы",
1338 (Locale::Ru, Self::ProjectLocal) => "память внутри проекта",
1339 (Locale::Uk, Self::StandardCare) => "стандартна обережність",
1340 (Locale::Uk, Self::StrictBoundaries) => "суворі межі",
1341 (Locale::Uk, Self::ProjectLocal) => "пам'ять у межах проєкту",
1342 (_, Self::StandardCare) => "standard care",
1343 (_, Self::StrictBoundaries) => "strict boundaries",
1344 (_, Self::ProjectLocal) => "project-local memory",
1345 }
1346 }
1347
1348 fn working_style(self, locale: Locale) -> &'static str {
1349 match (locale, self) {
1350 (Locale::Ja, Self::StandardCare) => {
1351 "秘密情報、ユーザーファイル、Git 履歴、本番システム、コスト、プライバシー、時間を保護する。"
1352 }
1353 (Locale::Ja, Self::StrictBoundaries) => {
1354 "秘密、個人データ、認証情報、本番状態、資金、公開操作は、先に確認する境界として扱う。"
1355 }
1356 (Locale::Ja, Self::ProjectLocal) => {
1357 "プロジェクト固有の文脈はプロジェクト内に留め、明示要求がない限りメモリへ書かない。"
1358 }
1359 (Locale::ZhHans, Self::StandardCare) => {
1360 "保护密钥、用户文件、Git 历史、生产系统、成本、隐私和时间。"
1361 }
1362 (Locale::ZhHans, Self::StrictBoundaries) => {
1363 "把密钥、个人数据、凭据、生产状态、资金和发布动作视为先确认边界。"
1364 }
1365 (Locale::ZhHans, Self::ProjectLocal) => {
1366 "项目特定上下文留在项目内,除非明确要求,否则不要写入记忆。"
1367 }
1368 (Locale::ZhHant, Self::StandardCare) => {
1369 "保護密鑰、使用者檔案、Git 歷史、生產系統、成本、隱私和時間。"
1370 }
1371 (Locale::ZhHant, Self::StrictBoundaries) => {
1372 "把密鑰、個人資料、憑據、生產狀態、資金和發布動作視為先確認邊界。"
1373 }
1374 (Locale::ZhHant, Self::ProjectLocal) => {
1375 "專案特定上下文留在專案內,除非明確要求,否則不要寫入記憶。"
1376 }
1377 (Locale::PtBr, Self::StandardCare) => {
1378 "Proteja segredos, arquivos do usuário, histórico git, produção, custo, privacidade e tempo."
1379 }
1380 (Locale::PtBr, Self::StrictBoundaries) => {
1381 "Trate segredos, dados pessoais, credenciais, estado de produção, dinheiro e publicações como limites de confirmação."
1382 }
1383 (Locale::PtBr, Self::ProjectLocal) => {
1384 "Mantenha contexto específico do projeto no projeto; evite gravar na memória sem pedido explícito."
1385 }
1386 (Locale::Es419, Self::StandardCare) => {
1387 "Protege secretos, archivos del usuario, historial git, producción, costo, privacidad y tiempo."
1388 }
1389 (Locale::Es419, Self::StrictBoundaries) => {
1390 "Trata secretos, datos personales, credenciales, estado de producción, dinero y publicaciones como límites de confirmación."
1391 }
1392 (Locale::Es419, Self::ProjectLocal) => {
1393 "Mantén el contexto específico del proyecto en el proyecto; evita llevarlo a memoria sin pedido explícito."
1394 }
1395 (Locale::Vi, Self::StandardCare) => {
1396 "Bảo vệ bí mật, tệp người dùng, lịch sử git, hệ thống sản xuất, chi phí, riêng tư và thời gian."
1397 }
1398 (Locale::Vi, Self::StrictBoundaries) => {
1399 "Xem bí mật, dữ liệu cá nhân, thông tin xác thực, trạng thái sản xuất, tiền và xuất bản là ranh giới cần xác nhận."
1400 }
1401 (Locale::Vi, Self::ProjectLocal) => {
1402 "Giữ ngữ cảnh riêng của dự án trong dự án; tránh ghi vào bộ nhớ nếu không được yêu cầu rõ."
1403 }
1404 (Locale::Ko, Self::StandardCare) => {
1405 "비밀 정보, 사용자 파일, Git 이력, 프로덕션 시스템, 비용, 프라이버시, 시간을 보호한다."
1406 }
1407 (Locale::Ko, Self::StrictBoundaries) => {
1408 "비밀 정보, 개인 데이터, 자격 증명, 프로덕션 상태, 자금, 게시 작업은 먼저 확인하는 경계로 취급한다."
1409 }
1410 (Locale::Ko, Self::ProjectLocal) => {
1411 "프로젝트 고유 맥락은 프로젝트 안에 두고, 명시적으로 요청받지 않는 한 메모리에 쓰지 않는다."
1412 }
1413 (Locale::Ca, Self::StandardCare) => {
1414 "Protegeix secrets, fitxers de l'usuari, historial de git, sistemes de producció, cost, privacitat i temps."
1415 }
1416 (Locale::Ca, Self::StrictBoundaries) => {
1417 "Tracta secrets, dades personals, credencials, estat de producció, diners i accions de publicació com a límits que cal confirmar primer."
1418 }
1419 (Locale::Ca, Self::ProjectLocal) => {
1420 "Mantén el context específic del projecte dins del projecte; evita portar-lo a la memòria si no se't demana explícitament."
1421 }
1422 (Locale::De, Self::StandardCare) => {
1423 "Schütze Geheimnisse, Nutzerdateien, Git-Verlauf, Produktionssysteme, Kosten, Privatsphäre und Zeit."
1424 }
1425 (Locale::De, Self::StrictBoundaries) => {
1426 "Behandle Geheimnisse, persönliche Daten, Zugangsdaten, Produktionszustand, Geld und Veröffentlichungen als Grenzen, die erst bestätigt werden."
1427 }
1428 (Locale::De, Self::ProjectLocal) => {
1429 "Halte projektspezifischen Kontext im Projekt; vermeide es, sensible Details ohne ausdrückliche Bitte in den Speicher zu übernehmen."
1430 }
1431 (Locale::Fr, Self::StandardCare) => {
1432 "Protégez secrets, fichiers utilisateur, historique git, systèmes de production, coût, vie privée et temps."
1433 }
1434 (Locale::Fr, Self::StrictBoundaries) => {
1435 "Traitez secrets, données personnelles, identifiants, état de production, argent et publications comme des limites exigeant confirmation."
1436 }
1437 (Locale::Fr, Self::ProjectLocal) => {
1438 "Gardez le contexte propre au projet dans le projet ; évitez de l'écrire en mémoire sans demande explicite."
1439 }
1440 (Locale::Id, Self::StandardCare) => {
1441 "Lindungi rahasia, file pengguna, riwayat git, sistem produksi, biaya, privasi, dan waktu."
1442 }
1443 (Locale::Id, Self::StrictBoundaries) => {
1444 "Perlakukan rahasia, data pribadi, kredensial, status produksi, uang, dan tindakan publikasi sebagai batas yang harus dikonfirmasi dulu."
1445 }
1446 (Locale::Id, Self::ProjectLocal) => {
1447 "Simpan konteks khusus proyek di dalam proyek; hindari membawanya ke memori kecuali diminta secara eksplisit."
1448 }
1449 (Locale::Hi, Self::StandardCare) => {
1450 "रहस्यों, उपयोगकर्ता फ़ाइलों, git इतिहास, प्रोडक्शन सिस्टम, लागत, गोपनीयता और समय की रक्षा करें।"
1451 }
1452 (Locale::Hi, Self::StrictBoundaries) => {
1453 "रहस्यों, व्यक्तिगत डेटा, क्रेडेंशियल, प्रोडक्शन स्थिति, धन और प्रकाशन क्रियाओं को पहले-पुष्टि सीमाओं की तरह मानें।"
1454 }
1455 (Locale::Hi, Self::ProjectLocal) => {
1456 "प्रोजेक्ट-विशिष्ट संदर्भ प्रोजेक्ट के भीतर रखें; स्पष्ट अनुरोध के बिना संवेदनशील विवरण मेमोरी में न ले जाएँ।"
1457 }
1458 (Locale::Ru, Self::StandardCare) => {
1459 "Защищайте секреты, файлы пользователя, историю git, production-системы, затраты, приватность и время."
1460 }
1461 (Locale::Ru, Self::StrictBoundaries) => {
1462 "Считайте секреты, персональные данные, учётные данные, production-состояние, деньги и публикации границами, требующими подтверждения."
1463 }
1464 (Locale::Ru, Self::ProjectLocal) => {
1465 "Держите контекст, специфичный для проекта, внутри проекта; не переносите чувствительные детали в память без явного запроса."
1466 }
1467 (Locale::Uk, Self::StandardCare) => {
1468 "Захищайте секрети, файли користувача, історію git, production-системи, витрати, приватність і час."
1469 }
1470 (Locale::Uk, Self::StrictBoundaries) => {
1471 "Вважайте секрети, персональні дані, облікові дані, production-стан, гроші та публікації межами, що потребують підтвердження."
1472 }
1473 (Locale::Uk, Self::ProjectLocal) => {
1474 "Тримайте контекст, специфічний для проєкту, всередині проєкту; не переносьте чутливі деталі в пам'ять без явного запиту."
1475 }
1476 (_, Self::StandardCare) => {
1477 "Protect secrets, user files, git history, production systems, cost, privacy, and time."
1478 }
1479 (_, Self::StrictBoundaries) => {
1480 "Treat secrets, personal data, credentials, production state, money, and publish actions as stop-and-confirm boundaries."
1481 }
1482 (_, Self::ProjectLocal) => {
1483 "Keep project-specific context local; avoid carrying sensitive details into memory unless explicitly asked."
1484 }
1485 }
1486 }
1487
1488 fn escalation_rule(self, locale: Locale) -> &'static str {
1489 match (locale, self) {
1490 (Locale::Ja, Self::StandardCare) => {
1491 "破壊的、高コスト、認証情報、公開、法務、セキュリティリスクのある操作の前に尋ねる。"
1492 }
1493 (Locale::Ja, Self::StrictBoundaries) => {
1494 "機微情報の読み取りや拡散、本番システム操作、支出、公開の前に停止して尋ねる。"
1495 }
1496 (Locale::Ja, Self::ProjectLocal) => {
1497 "プロジェクト詳細をメモリ、ワークスペース、古い引き継ぎへ持ち出す前に確認する。"
1498 }
1499 (Locale::ZhHans, Self::StandardCare) => {
1500 "遇到破坏性、高成本、凭据、发布、法律或安全风险操作时先询问。"
1501 }
1502 (Locale::ZhHans, Self::StrictBoundaries) => {
1503 "在读取或传播敏感信息、触碰生产系统、花费资金或发布内容前停止并询问。"
1504 }
1505 (Locale::ZhHans, Self::ProjectLocal) => {
1506 "需要跨项目记忆、复制项目细节或引用旧交接时,先确认这些上下文仍适用。"
1507 }
1508 (Locale::ZhHant, Self::StandardCare) => {
1509 "遇到破壞性、高成本、憑據、發布、法律或安全風險操作時先詢問。"
1510 }
1511 (Locale::ZhHant, Self::StrictBoundaries) => {
1512 "在讀取或傳播敏感資訊、觸碰生產系統、花費資金或發布內容前停止並詢問。"
1513 }
1514 (Locale::ZhHant, Self::ProjectLocal) => {
1515 "需要跨專案記憶、複製專案細節或引用舊交接時,先確認這些上下文仍適用。"
1516 }
1517 (Locale::PtBr, Self::StandardCare) => {
1518 "Pergunte antes de ações destrutivas, caras, com credenciais, publicação, risco legal ou de segurança."
1519 }
1520 (Locale::PtBr, Self::StrictBoundaries) => {
1521 "Pare e pergunte antes de ler ou espalhar dados sensíveis, tocar produção, gastar dinheiro ou publicar."
1522 }
1523 (Locale::PtBr, Self::ProjectLocal) => {
1524 "Confirme antes de levar detalhes do projeto para memória, workspaces ou handoffs antigos."
1525 }
1526 (Locale::Es419, Self::StandardCare) => {
1527 "Pregunta antes de acciones destructivas, costosas, con credenciales, publicación o riesgo legal/de seguridad."
1528 }
1529 (Locale::Es419, Self::StrictBoundaries) => {
1530 "Detente y pregunta antes de leer o difundir datos sensibles, tocar producción, gastar dinero o publicar."
1531 }
1532 (Locale::Es419, Self::ProjectLocal) => {
1533 "Confirma antes de llevar detalles del proyecto a memoria, workspaces o handoffs viejos."
1534 }
1535 (Locale::Vi, Self::StandardCare) => {
1536 "Hỏi trước các thao tác phá hủy, tốn kém, liên quan thông tin xác thực, xuất bản, pháp lý hoặc bảo mật."
1537 }
1538 (Locale::Vi, Self::StrictBoundaries) => {
1539 "Dừng và hỏi trước khi đọc/phát tán dữ liệu nhạy cảm, chạm sản xuất, chi tiền hoặc xuất bản."
1540 }
1541 (Locale::Vi, Self::ProjectLocal) => {
1542 "Xác nhận trước khi mang chi tiết dự án sang bộ nhớ, workspace khác hoặc handoff cũ."
1543 }
1544 (Locale::Ko, Self::StandardCare) => {
1545 "파괴적이거나, 비용이 크거나, 자격 증명, 게시, 법적, 보안 위험이 있는 작업 전에 먼저 물어본다."
1546 }
1547 (Locale::Ko, Self::StrictBoundaries) => {
1548 "민감 정보를 읽거나 퍼뜨리기 전, 프로덕션 시스템을 건드리기 전, 자금을 쓰거나 게시하기 전에 멈추고 물어본다."
1549 }
1550 (Locale::Ko, Self::ProjectLocal) => {
1551 "프로젝트 세부 정보를 메모리, 다른 워크스페이스, 오래된 인계 자료로 옮기기 전에 확인한다."
1552 }
1553 (Locale::Ca, Self::StandardCare) => {
1554 "Pregunta abans d'accions destructives, costoses, amb credencials, de publicació o amb risc legal o de seguretat."
1555 }
1556 (Locale::Ca, Self::StrictBoundaries) => {
1557 "Atura't i pregunta abans de llegir o difondre dades sensibles, tocar sistemes de producció, gastar diners o publicar."
1558 }
1559 (Locale::Ca, Self::ProjectLocal) => {
1560 "Confirma abans de portar detalls del projecte a la memòria, a altres espais de treball o a traspasos antics."
1561 }
1562 (Locale::De, Self::StandardCare) => {
1563 "Frage vor destruktiven, kostspieligen, zugangsdatenbezogenen, veröffentlichenden, rechtlichen oder sicherheitskritischen Aktionen."
1564 }
1565 (Locale::De, Self::StrictBoundaries) => {
1566 "Halte an und frage, bevor du sensible Daten liest oder verbreitest, Produktionssysteme anfasst, Geld ausgibst oder veröffentlichst."
1567 }
1568 (Locale::De, Self::ProjectLocal) => {
1569 "Bestätige, bevor du Projektdetails in Speicher, Workspaces oder veraltete Übergaben überträgst."
1570 }
1571 (Locale::Fr, Self::StandardCare) => {
1572 "Demandez avant toute action destructive, coûteuse, impliquant des identifiants, une publication, ou un risque juridique ou de sécurité."
1573 }
1574 (Locale::Fr, Self::StrictBoundaries) => {
1575 "Arrêtez et demandez avant de lire ou diffuser des données sensibles, de toucher aux systèmes de production, de dépenser de l'argent ou de publier."
1576 }
1577 (Locale::Fr, Self::ProjectLocal) => {
1578 "Confirmez avant de transporter des détails du projet vers la mémoire, d'autres espaces de travail ou d'anciens transferts."
1579 }
1580 (Locale::Id, Self::StandardCare) => {
1581 "Tanya sebelum tindakan destruktif, mahal, terkait kredensial, publikasi, hukum, atau berisiko keamanan."
1582 }
1583 (Locale::Id, Self::StrictBoundaries) => {
1584 "Berhenti dan tanya sebelum membaca atau menyebarkan data sensitif, menyentuh sistem produksi, membelanjakan uang, atau mempublikasikan."
1585 }
1586 (Locale::Id, Self::ProjectLocal) => {
1587 "Konfirmasi sebelum membawa detail proyek ke memori, workspace lain, atau handoff lama."
1588 }
1589 (Locale::Hi, Self::StandardCare) => {
1590 "विनाशकारी, उच्च-लागत, क्रेडेंशियल, प्रकाशन, कानूनी या सुरक्षा-जोखिम कार्यों से पहले पूछें।"
1591 }
1592 (Locale::Hi, Self::StrictBoundaries) => {
1593 "संवेदनशील डेटा पढ़ने या फैलाने, प्रोडक्शन सिस्टम छूने, धन खर्च करने या प्रकाशित करने से पहले रुककर पूछें।"
1594 }
1595 (Locale::Hi, Self::ProjectLocal) => {
1596 "प्रोजेक्ट विवरण मेमोरी, अन्य कार्यक्षेत्रों या पुराने हैंडऑफ़ में ले जाने से पहले पुष्टि करें।"
1597 }
1598 (Locale::Ru, Self::StandardCare) => {
1599 "Спрашивайте перед деструктивными, дорогими, связанными с учётными данными, публикацией, юридическими или угрожающими безопасности действиями."
1600 }
1601 (Locale::Ru, Self::StrictBoundaries) => {
1602 "Остановитесь и спросите, прежде чем читать или распространять чувствительные данные, трогать production-системы, тратить деньги или публиковать."
1603 }
1604 (Locale::Ru, Self::ProjectLocal) => {
1605 "Подтвердите, прежде чем переносить детали проекта в память, другие рабочие области или устаревшие передаточные заметки."
1606 }
1607 (Locale::Uk, Self::StandardCare) => {
1608 "Питайте перед руйнівними, дорогими, пов'язаними з обліковими даними, публікацією, юридичними чи небезпечними для безпеки діями."
1609 }
1610 (Locale::Uk, Self::StrictBoundaries) => {
1611 "Зупиніться й запитайте, перш ніж читати чи поширювати чутливі дані, чіпати production-системи, витрачати гроші або публікувати."
1612 }
1613 (Locale::Uk, Self::ProjectLocal) => {
1614 "Підтвердьте, перш ніж переносити деталі проєкту в пам'ять, інші робочі простори чи застарілі передаточні нотатки."
1615 }
1616 (_, Self::StandardCare) => {
1617 "Ask before destructive, high-cost, credential, publishing, legal, or security-risk actions."
1618 }
1619 (_, Self::StrictBoundaries) => {
1620 "Stop and ask before reading or spreading sensitive data, touching production systems, spending money, or publishing."
1621 }
1622 (_, Self::ProjectLocal) => {
1623 "Confirm before carrying project details across memory, workspaces, or stale handoffs."
1624 }
1625 }
1626 }
1627 }
1628
1629 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
1630 enum GuidedPrinciples {
1631 ScopedChanges,
1632 UserVoice,
1633 ReversibleOps,
1634 }
1635
1636 impl GuidedPrinciples {
1637 fn next(self) -> Self {
1638 match self {
1639 Self::ScopedChanges => Self::UserVoice,
1640 Self::UserVoice => Self::ReversibleOps,
1641 Self::ReversibleOps => Self::ScopedChanges,
1642 }
1643 }
1644
1645 fn label(self, locale: Locale) -> &'static str {
1646 match (locale, self) {
1647 (Locale::Ja, Self::ScopedChanges) => "小さく絞った変更",
1648 (Locale::Ja, Self::UserVoice) => "ユーザーの声を保つ",
1649 (Locale::Ja, Self::ReversibleOps) => "可逆手順",
1650 (Locale::ZhHans, Self::ScopedChanges) => "小范围改动",
1651 (Locale::ZhHans, Self::UserVoice) => "保留用户语气",
1652 (Locale::ZhHans, Self::ReversibleOps) => "可逆步骤",
1653 (Locale::ZhHant, Self::ScopedChanges) => "小範圍改動",
1654 (Locale::ZhHant, Self::UserVoice) => "保留使用者語氣",
1655 (Locale::ZhHant, Self::ReversibleOps) => "可逆步驟",
1656 (Locale::PtBr, Self::ScopedChanges) => "mudanças focadas",
1657 (Locale::PtBr, Self::UserVoice) => "preservar voz do usuário",
1658 (Locale::PtBr, Self::ReversibleOps) => "passos reversíveis",
1659 (Locale::Es419, Self::ScopedChanges) => "cambios acotados",
1660 (Locale::Es419, Self::UserVoice) => "preservar voz del usuario",
1661 (Locale::Es419, Self::ReversibleOps) => "pasos reversibles",
1662 (Locale::Vi, Self::ScopedChanges) => "thay đổi có phạm vi",
1663 (Locale::Vi, Self::UserVoice) => "giữ giọng người dùng",
1664 (Locale::Vi, Self::ReversibleOps) => "bước có thể đảo ngược",
1665 (Locale::Ko, Self::ScopedChanges) => "범위가 명확한 변경",
1666 (Locale::Ko, Self::UserVoice) => "사용자의 어조 유지",
1667 (Locale::Ko, Self::ReversibleOps) => "되돌릴 수 있는 단계",
1668 (Locale::Ca, Self::ScopedChanges) => "canvis acotats",
1669 (Locale::Ca, Self::UserVoice) => "veu de l'usuari",
1670 (Locale::Ca, Self::ReversibleOps) => "passos reversibles",
1671 (Locale::De, Self::ScopedChanges) => "begrenzte Änderungen",
1672 (Locale::De, Self::UserVoice) => "Stimme des Nutzers",
1673 (Locale::De, Self::ReversibleOps) => "reversible Schritte",
1674 (Locale::Fr, Self::ScopedChanges) => "changements ciblés",
1675 (Locale::Fr, Self::UserVoice) => "voix de l'utilisateur",
1676 (Locale::Fr, Self::ReversibleOps) => "étapes réversibles",
1677 (Locale::Id, Self::ScopedChanges) => "perubahan terbatas",
1678 (Locale::Id, Self::UserVoice) => "suara pengguna",
1679 (Locale::Id, Self::ReversibleOps) => "langkah reversibel",
1680 (Locale::Hi, Self::ScopedChanges) => "सीमित बदलाव",
1681 (Locale::Hi, Self::UserVoice) => "उपयोगकर्ता की आवाज़",
1682 (Locale::Hi, Self::ReversibleOps) => "उत्क्रमणीय चरण",
1683 (Locale::Ru, Self::ScopedChanges) => "ограниченные изменения",
1684 (Locale::Ru, Self::UserVoice) => "голос пользователя",
1685 (Locale::Ru, Self::ReversibleOps) => "обратимые шаги",
1686 (Locale::Uk, Self::ScopedChanges) => "обмежені зміни",
1687 (Locale::Uk, Self::UserVoice) => "голос користувача",
1688 (Locale::Uk, Self::ReversibleOps) => "оборотні кроки",
1689 (_, Self::ScopedChanges) => "scoped changes",
1690 (_, Self::UserVoice) => "user voice",
1691 (_, Self::ReversibleOps) => "reversible steps",
1692 }
1693 }
1694
1695 fn note(self, locale: Locale) -> &'static str {
1696 match (locale, self) {
1697 (Locale::Ja, Self::ScopedChanges) => {
1698 "自由原則:小さくレビューしやすい変更を優先し、明示要求がない限り無関係なリファクタを避ける。"
1699 }
1700 (Locale::Ja, Self::UserVoice) => {
1701 "自由原則:ユーザーの語調、ブランド、制約を保ち、好みを権限拡大として扱わない。"
1702 }
1703 (Locale::Ja, Self::ReversibleOps) => {
1704 "自由原則:影響の大きい操作の前に、可逆手順、チェックポイント、ロールバック説明を選ぶ。"
1705 }
1706 (Locale::ZhHans, Self::ScopedChanges) => {
1707 "自定义准则:优先采用小范围、可审查的改动;除非明确要求,不做无关重构。"
1708 }
1709 (Locale::ZhHans, Self::UserVoice) => {
1710 "自定义准则:保留用户的语气、品牌和约束;不把偏好推断成权限扩大。"
1711 }
1712 (Locale::ZhHans, Self::ReversibleOps) => {
1713 "自定义准则:先选择可逆步骤、检查点和回滚说明,再进行高影响操作。"
1714 }
1715 (Locale::ZhHant, Self::ScopedChanges) => {
1716 "自由原則:優先採用小範圍、可審查的改動;除非明確要求,不做無關重構。"
1717 }
1718 (Locale::ZhHant, Self::UserVoice) => {
1719 "自由原則:保留使用者的語氣、品牌和約束;不把偏好推斷成權限擴大。"
1720 }
1721 (Locale::ZhHant, Self::ReversibleOps) => {
1722 "自由原則:先選擇可逆步驟、檢查點和回復說明,再進行高影響操作。"
1723 }
1724 (Locale::PtBr, Self::ScopedChanges) => {
1725 "Princípio livre: prefira mudanças pequenas e revisáveis; evite refactors não relacionados sem pedido explícito."
1726 }
1727 (Locale::PtBr, Self::UserVoice) => {
1728 "Princípio livre: preserve a voz, marca e restrições do usuário sem tratar preferências como expansão de permissão."
1729 }
1730 (Locale::PtBr, Self::ReversibleOps) => {
1731 "Princípio livre: favoreça passos reversíveis, checkpoints e notas de rollback antes de ações de alto impacto."
1732 }
1733 (Locale::Es419, Self::ScopedChanges) => {
1734 "Principio libre: prefiere cambios pequeños y revisables; evita refactors no relacionados sin pedido explícito."
1735 }
1736 (Locale::Es419, Self::UserVoice) => {
1737 "Principio libre: preserva la voz, marca y restricciones del usuario sin tratar preferencias como expansión de permisos."
1738 }
1739 (Locale::Es419, Self::ReversibleOps) => {
1740 "Principio libre: favorece pasos reversibles, checkpoints y notas de rollback antes de acciones de alto impacto."
1741 }
1742 (Locale::Vi, Self::ScopedChanges) => {
1743 "Nguyên tắc tự do: ưu tiên thay đổi nhỏ, dễ review; tránh refactor không liên quan nếu không được yêu cầu rõ."
1744 }
1745 (Locale::Vi, Self::UserVoice) => {
1746 "Nguyên tắc tự do: giữ giọng, thương hiệu và ràng buộc của người dùng, không xem sở thích là mở rộng quyền."
1747 }
1748 (Locale::Vi, Self::ReversibleOps) => {
1749 "Nguyên tắc tự do: ưu tiên bước có thể đảo ngược, checkpoint và ghi chú rollback trước thao tác tác động cao."
1750 }
1751 (Locale::Ko, Self::ScopedChanges) => {
1752 "자유 원칙: 작고 리뷰하기 쉬운 변경을 우선하고, 명시적으로 요청받지 않는 한 관련 없는 리팩터링은 하지 않는다."
1753 }
1754 (Locale::Ko, Self::UserVoice) => {
1755 "자유 원칙: 사용자의 어조, 브랜드, 제약을 유지하고 선호를 권한 확대로 취급하지 않는다."
1756 }
1757 (Locale::Ko, Self::ReversibleOps) => {
1758 "자유 원칙: 영향이 큰 작업 전에 되돌릴 수 있는 단계, 체크포인트, 롤백 메모를 우선한다."
1759 }
1760 (Locale::Ca, Self::ScopedChanges) => {
1761 "Principi lliure: prefereix canvis petits i revisables i evita refactors no relacionats si no es demanen explícitament."
1762 }
1763 (Locale::Ca, Self::UserVoice) => {
1764 "Principi lliure: preserva la veu, la marca i les restriccions de l'usuari sense tractar les preferències com una ampliació de permisos."
1765 }
1766 (Locale::Ca, Self::ReversibleOps) => {
1767 "Principi lliure: priorita passos reversibles, punts de control i notes de marxa enrere abans d'operacions d'alt impacte."
1768 }
1769 (Locale::De, Self::ScopedChanges) => {
1770 "Freitext-Prinzip: Bevorzuge kleine, überprüfbare Änderungen und vermeide unzusammenhängende Refactorings, sofern nicht ausdrücklich gewünscht."
1771 }
1772 (Locale::De, Self::UserVoice) => {
1773 "Freitext-Prinzip: Bewahre Stimme, Marke und Vorgaben des Nutzers, ohne Präferenzen als Rechteausweitung zu behandeln."
1774 }
1775 (Locale::De, Self::ReversibleOps) => {
1776 "Freitext-Prinzip: Bevorzuge reversible Schritte, Checkpoints und Rollback-Notizen vor einschneidenden Operationen."
1777 }
1778 (Locale::Fr, Self::ScopedChanges) => {
1779 "Principe libre : préférez des changements petits et révisables et évitez les refactors sans rapport, sauf demande explicite."
1780 }
1781 (Locale::Fr, Self::UserVoice) => {
1782 "Principe libre : préservez la voix, la marque et les contraintes de l'utilisateur sans traiter ses préférences comme une extension de permissions."
1783 }
1784 (Locale::Fr, Self::ReversibleOps) => {
1785 "Principe libre : privilégiez étapes réversibles, points de contrôle et notes de rollback avant les opérations à fort impact."
1786 }
1787 (Locale::Id, Self::ScopedChanges) => {
1788 "Prinsip bebas: utamakan perubahan kecil yang mudah ditinjau dan hindari refactor tak terkait kecuali diminta secara eksplisit."
1789 }
1790 (Locale::Id, Self::UserVoice) => {
1791 "Prinsip bebas: jaga suara, merek, dan batasan pengguna tanpa memperlakukan preferensi sebagai perluasan izin."
1792 }
1793 (Locale::Id, Self::ReversibleOps) => {
1794 "Prinsip bebas: utamakan langkah reversibel, checkpoint, dan catatan rollback sebelum operasi berdampak besar."
1795 }
1796 (Locale::Hi, Self::ScopedChanges) => {
1797 "मुक्त-पाठ सिद्धांत: छोटे, समीक्षायोग्य बदलावों को प्राथमिकता दें और स्पष्ट अनुरोध के बिना असंबंधित रिफैक्टर से बचें।"
1798 }
1799 (Locale::Hi, Self::UserVoice) => {
1800 "मुक्त-पाठ सिद्धांत: उपयोगकर्ता की आवाज़, ब्रांड और बाधाएँ सुरक्षित रखें; प्राथमिकताओं को अनुमति-विस्तार न मानें।"
1801 }
1802 (Locale::Hi, Self::ReversibleOps) => {
1803 "मुक्त-पाठ सिद्धांत: उच्च-प्रभाव कार्यों से पहले उत्क्रमणीय चरणों, चेकपॉइंट और रोलबैक नोट्स को प्राथमिकता दें।"
1804 }
1805 (Locale::Ru, Self::ScopedChanges) => {
1806 "Свободный принцип: предпочитайте небольшие, проверяемые изменения и избегайте несвязанных рефакторингов без явного запроса."
1807 }
1808 (Locale::Ru, Self::UserVoice) => {
1809 "Свободный принцип: сохраняйте голос, бренд и ограничения пользователя, не трактуя предпочтения как расширение полномочий."
1810 }
1811 (Locale::Ru, Self::ReversibleOps) => {
1812 "Свободный принцип: отдавайте предпочтение обратимым шагам, контрольным точкам и заметкам об откате перед высокорисковыми операциями."
1813 }
1814 (Locale::Uk, Self::ScopedChanges) => {
1815 "Вільний принцип: надавайте перевагу невеликим, перевірюваним змінам і уникайте непов'язаних рефакторингів без явного запиту."
1816 }
1817 (Locale::Uk, Self::UserVoice) => {
1818 "Вільний принцип: зберігайте голос, бренд і обмеження користувача, не трактуючи вподобання як розширення повноважень."
1819 }
1820 (Locale::Uk, Self::ReversibleOps) => {
1821 "Вільний принцип: надавайте перевагу оборотним крокам, контрольним точкам і нотаткам про відкат перед високоризиковими операціями."
1822 }
1823 (_, Self::ScopedChanges) => {
1824 "Freeform principle: prefer small, reviewable changes and avoid unrelated refactors unless explicitly requested."
1825 }
1826 (_, Self::UserVoice) => {
1827 "Freeform principle: preserve the user's voice, brand, and constraints without treating preferences as permission expansion."
1828 }
1829 (_, Self::ReversibleOps) => {
1830 "Freeform principle: favor reversible steps, checkpoints, and rollback notes before high-impact operations."
1831 }
1832 }
1833 }
1834 }
1835
1836 fn next_guided_autonomy(preference: AutonomyPreference) -> AutonomyPreference {
1837 match preference {
1838 AutonomyPreference::Unspecified | AutonomyPreference::Cautious => {
1839 AutonomyPreference::Balanced
1840 }
1841 AutonomyPreference::Balanced => AutonomyPreference::Autonomous,
1842 AutonomyPreference::Autonomous => AutonomyPreference::Cautious,
1843 }
1844 }
1845
1846 fn autonomy_label(preference: AutonomyPreference, locale: Locale) -> &'static str {
1847 match (locale, preference) {
1848 (Locale::Ja, AutonomyPreference::Cautious) => "慎重",
1849 (Locale::Ja, AutonomyPreference::Balanced) => "バランス",
1850 (Locale::Ja, AutonomyPreference::Autonomous) => "積極的",
1851 (Locale::ZhHans, AutonomyPreference::Cautious) => "谨慎",
1852 (Locale::ZhHans, AutonomyPreference::Balanced) => "平衡",
1853 (Locale::ZhHans, AutonomyPreference::Autonomous) => "积极主动",
1854 (Locale::ZhHant, AutonomyPreference::Cautious) => "謹慎",
1855 (Locale::ZhHant, AutonomyPreference::Balanced) => "平衡",
1856 (Locale::ZhHant, AutonomyPreference::Autonomous) => "積極主動",
1857 (Locale::PtBr, AutonomyPreference::Cautious) => "cauteloso",
1858 (Locale::PtBr, AutonomyPreference::Balanced) => "equilibrado",
1859 (Locale::PtBr, AutonomyPreference::Autonomous) => "ambicioso",
1860 (Locale::Es419, AutonomyPreference::Cautious) => "cauteloso",
1861 (Locale::Es419, AutonomyPreference::Balanced) => "equilibrado",
1862 (Locale::Es419, AutonomyPreference::Autonomous) => "ambicioso",
1863 (Locale::Vi, AutonomyPreference::Cautious) => "thận trọng",
1864 (Locale::Vi, AutonomyPreference::Balanced) => "cân bằng",
1865 (Locale::Vi, AutonomyPreference::Autonomous) => "chủ động",
1866 (Locale::Ko, AutonomyPreference::Cautious) => "신중함",
1867 (Locale::Ko, AutonomyPreference::Balanced) => "균형",
1868 (Locale::Ko, AutonomyPreference::Autonomous) => "적극적",
1869 (Locale::Ca, AutonomyPreference::Cautious) => "cautelós",
1870 (Locale::Ca, AutonomyPreference::Balanced) => "equilibrat",
1871 (Locale::Ca, AutonomyPreference::Autonomous) => "ambiciós",
1872 (Locale::De, AutonomyPreference::Cautious) => "vorsichtig",
1873 (Locale::De, AutonomyPreference::Balanced) => "ausgewogen",
1874 (Locale::De, AutonomyPreference::Autonomous) => "ambitioniert",
1875 (Locale::Fr, AutonomyPreference::Cautious) => "prudent",
1876 (Locale::Fr, AutonomyPreference::Balanced) => "équilibré",
1877 (Locale::Fr, AutonomyPreference::Autonomous) => "ambitieux",
1878 (Locale::Id, AutonomyPreference::Cautious) => "hati-hati",
1879 (Locale::Id, AutonomyPreference::Balanced) => "seimbang",
1880 (Locale::Id, AutonomyPreference::Autonomous) => "ambisius",
1881 (Locale::Hi, AutonomyPreference::Cautious) => "सावधान",
1882 (Locale::Hi, AutonomyPreference::Balanced) => "संतुलित",
1883 (Locale::Hi, AutonomyPreference::Autonomous) => "महत्वाकांक्षी",
1884 (Locale::Ru, AutonomyPreference::Cautious) => "осторожный",
1885 (Locale::Ru, AutonomyPreference::Balanced) => "сбалансированный",
1886 (Locale::Ru, AutonomyPreference::Autonomous) => "самостоятельный",
1887 (Locale::Uk, AutonomyPreference::Cautious) => "обережний",
1888 (Locale::Uk, AutonomyPreference::Balanced) => "збалансований",
1889 (Locale::Uk, AutonomyPreference::Autonomous) => "самостійний",
1890 (_, AutonomyPreference::Cautious) => "cautious",
1891 (_, AutonomyPreference::Balanced) => "balanced",
1892 (_, AutonomyPreference::Autonomous) => "ambitious",
1893 (_, AutonomyPreference::Unspecified) => "unspecified",
1894 }
1895 }
1896
1897 fn autonomy_priority(preference: AutonomyPreference, locale: Locale) -> &'static str {
1898 match (locale, preference) {
1899 (Locale::Ja, AutonomyPreference::Cautious) => {
1900 "ファイル編集、コマンド実行、あいまいな製品判断の前に停止して尋ねる。"
1901 }
1902 (Locale::Ja, AutonomyPreference::Balanced) => {
1903 "明確で低リスクな作業は直接進め、危険、破壊的、あいまいな操作では先に確認する。"
1904 }
1905 (Locale::Ja, AutonomyPreference::Autonomous) => {
1906 "安全な定型作業はまとめて進めるが、破壊的、認証情報、公開、高コスト、法務、セキュリティリスクでは停止して尋ねる。"
1907 }
1908 (Locale::ZhHans, AutonomyPreference::Cautious) => {
1909 "在编辑文件、运行命令或产品选择不明确前,倾向先停下询问。"
1910 }
1911 (Locale::ZhHans, AutonomyPreference::Balanced) => {
1912 "清晰低风险任务可直接行动;遇到风险、破坏性或歧义时先确认。"
1913 }
1914 (Locale::ZhHans, AutonomyPreference::Autonomous) => {
1915 "可批量处理安全的常规工作,但遇到破坏性、凭据、发布、高成本、法律或安全风险时停止询问。"
1916 }
1917 (Locale::ZhHant, AutonomyPreference::Cautious) => {
1918 "在編輯檔案、執行命令或產品選擇不明確前,傾向先停下詢問。"
1919 }
1920 (Locale::ZhHant, AutonomyPreference::Balanced) => {
1921 "清晰低風險任務可直接行動;遇到風險、破壞性或歧義時先確認。"
1922 }
1923 (Locale::ZhHant, AutonomyPreference::Autonomous) => {
1924 "可批量處理安全的常規工作,但遇到破壞性、憑據、發布、高成本、法律或安全風險時停止詢問。"
1925 }
1926 (Locale::PtBr, AutonomyPreference::Cautious) => {
1927 "Pare e pergunte antes de editar arquivos, rodar comandos ou escolher entre caminhos ambíguos de produto."
1928 }
1929 (Locale::PtBr, AutonomyPreference::Balanced) => {
1930 "Aja diretamente em tarefas claras e de baixo risco; confirme antes de ações arriscadas, destrutivas ou ambíguas."
1931 }
1932 (Locale::PtBr, AutonomyPreference::Autonomous) => {
1933 "Agrupe trabalho seguro de rotina, mas pare para ações destrutivas, credenciais, publicação, alto custo, legais ou de segurança."
1934 }
1935 (Locale::Es419, AutonomyPreference::Cautious) => {
1936 "Detente y pregunta antes de editar archivos, ejecutar comandos o elegir entre caminos ambiguos de producto."
1937 }
1938 (Locale::Es419, AutonomyPreference::Balanced) => {
1939 "Actúa directamente en tareas claras y de bajo riesgo; confirma antes de acciones riesgosas, destructivas o ambiguas."
1940 }
1941 (Locale::Es419, AutonomyPreference::Autonomous) => {
1942 "Agrupa trabajo seguro de rutina, pero detente ante acciones destructivas, credenciales, publicación, alto costo, legales o de seguridad."
1943 }
1944 (Locale::Vi, AutonomyPreference::Cautious) => {
1945 "Dừng và hỏi trước khi sửa tệp, chạy lệnh hoặc chọn giữa đường sản phẩm mơ hồ."
1946 }
1947 (Locale::Vi, AutonomyPreference::Balanced) => {
1948 "Hành động trực tiếp với việc rõ, rủi ro thấp; xác nhận trước việc rủi ro, phá hủy hoặc mơ hồ."
1949 }
1950 (Locale::Vi, AutonomyPreference::Autonomous) => {
1951 "Gộp việc thường lệ an toàn, nhưng dừng với thao tác phá hủy, thông tin xác thực, xuất bản, chi phí cao, pháp lý hoặc bảo mật."
1952 }
1953 (Locale::Ko, AutonomyPreference::Cautious) => {
1954 "파일 수정, 명령어 실행, 애매한 제품 선택 전에 멈추고 물어본다."
1955 }
1956 (Locale::Ko, AutonomyPreference::Balanced) => {
1957 "명확하고 위험이 낮은 작업은 바로 진행하고, 위험하거나 파괴적이거나 애매한 작업은 먼저 확인한다."
1958 }
1959 (Locale::Ko, AutonomyPreference::Autonomous) => {
1960 "안전한 정형 작업은 모아서 진행하되, 파괴적이거나 자격 증명, 게시, 고비용, 법적, 보안 위험이 있는 작업에서는 멈추고 물어본다."
1961 }
1962 (Locale::Ca, AutonomyPreference::Cautious) => {
1963 "Atura't i pregunta abans d'editar fitxers, executar ordres o triar entre camins de producte ambigus."
1964 }
1965 (Locale::Ca, AutonomyPreference::Balanced) => {
1966 "Actua directament en tasques clares i de baix risc; confirma abans d'accions arriscades, destructives o ambigües."
1967 }
1968 (Locale::Ca, AutonomyPreference::Autonomous) => {
1969 "Agrupa la feina rutinària segura, però atura't davant accions destructives, amb credencials, de publicació, d'alt cost o amb risc legal o de seguretat."
1970 }
1971 (Locale::De, AutonomyPreference::Cautious) => {
1972 "Halte an und frage, bevor du Dateien bearbeitest, Befehle ausführst oder zwischen mehrdeutigen Produktwegen wählst."
1973 }
1974 (Locale::De, AutonomyPreference::Balanced) => {
1975 "Handle direkt bei klaren, risikoarmen Aufgaben; bestätige vor riskanten, destruktiven oder mehrdeutigen Aktionen."
1976 }
1977 (Locale::De, AutonomyPreference::Autonomous) => {
1978 "Bündle sichere Routinearbeit, aber halte an bei destruktiven, zugangsdatenbezogenen, veröffentlichenden, kostspieligen, rechtlichen oder sicherheitskritischen Aktionen."
1979 }
1980 (Locale::Fr, AutonomyPreference::Cautious) => {
1981 "Arrêtez et demandez avant de modifier des fichiers, d'exécuter des commandes ou de choisir entre des voies produit ambiguës."
1982 }
1983 (Locale::Fr, AutonomyPreference::Balanced) => {
1984 "Agissez directement sur les tâches claires et à faible risque ; confirmez avant les actions risquées, destructives ou ambiguës."
1985 }
1986 (Locale::Fr, AutonomyPreference::Autonomous) => {
1987 "Regroupez le travail de routine sûr, mais arrêtez devant les actions destructives, impliquant des identifiants, des publications, coûteuses, juridiques ou à risque de sécurité."
1988 }
1989 (Locale::Id, AutonomyPreference::Cautious) => {
1990 "Berhenti dan tanya sebelum mengedit file, menjalankan perintah, atau memilih di antara jalur produk yang ambigu."
1991 }
1992 (Locale::Id, AutonomyPreference::Balanced) => {
1993 "Bertindak langsung pada tugas yang jelas dan berisiko rendah; konfirmasi sebelum tindakan berisiko, destruktif, atau ambigu."
1994 }
1995 (Locale::Id, AutonomyPreference::Autonomous) => {
1996 "Kelompokkan pekerjaan rutin yang aman, tetapi berhenti untuk tindakan destruktif, terkait kredensial, publikasi, mahal, hukum, atau berisiko keamanan."
1997 }
1998 (Locale::Hi, AutonomyPreference::Cautious) => {
1999 "फ़ाइलें संपादित करने, कमांड चलाने या अस्पष्ट उत्पाद मार्गों में चुनने से पहले रुककर पूछें।"
2000 }
2001 (Locale::Hi, AutonomyPreference::Balanced) => {
2002 "स्पष्ट, कम-जोखिम वाले कार्यों पर सीधे कार्य करें; जोखिमपूर्ण, विनाशकारी या अस्पष्ट कार्यों से पहले पुष्टि करें।"
2003 }
2004 (Locale::Hi, AutonomyPreference::Autonomous) => {
2005 "सुरक्षित नियमित काम एक साथ करें, लेकिन विनाशकारी, क्रेडेंशियल, प्रकाशन, उच्च-लागत, कानूनी या सुरक्षा-जोखिम कार्यों पर रुककर पूछें।"
2006 }
2007 (Locale::Ru, AutonomyPreference::Cautious) => {
2008 "Остановитесь и спросите перед редактированием файлов, запуском команд или выбором между неоднозначными продуктовыми путями."
2009 }
2010 (Locale::Ru, AutonomyPreference::Balanced) => {
2011 "Действуйте напрямую в ясных низкорисковых задачах; подтверждайте перед рискованными, деструктивными или неоднозначными действиями."
2012 }
2013 (Locale::Ru, AutonomyPreference::Autonomous) => {
2014 "Группируйте безопасную рутинную работу, но останавливайтесь перед деструктивными действиями, действиями с учётными данными, публикациями, дорогими, юридическими или угрожающими безопасности операциями."
2015 }
2016 (Locale::Uk, AutonomyPreference::Cautious) => {
2017 "Зупиніться й запитайте перед редагуванням файлів, запуском команд або вибором між неоднозначними продуктовими шляхами."
2018 }
2019 (Locale::Uk, AutonomyPreference::Balanced) => {
2020 "Дійте безпосередньо в чітких низькоризикових завданнях; підтверджуйте перед ризикованими, руйнівними чи неоднозначними діями."
2021 }
2022 (Locale::Uk, AutonomyPreference::Autonomous) => {
2023 "Групуйте безпечну рутинну роботу, але зупиняйтеся перед руйнівними діями, діями з обліковими даними, публікаціями, дорогими, юридичними чи небезпечними для безпеки операціями."
2024 }
2025 (_, AutonomyPreference::Cautious) => {
2026 "Stop and ask before editing files, running commands, or choosing between ambiguous product paths."
2027 }
2028 (_, AutonomyPreference::Balanced) => {
2029 "Act directly on clear low-risk tasks; confirm before risky, destructive, or ambiguous actions."
2030 }
2031 (_, AutonomyPreference::Autonomous) => {
2032 "Batch routine safe work, then stop for destructive, credential, publishing, high-cost, legal, or security-risk actions."
2033 }
2034 (_, AutonomyPreference::Unspecified) => "No standing initiative preference was selected.",
2035 }
2036 }
2037
2038 fn authority_priority(locale: Locale) -> &'static str {
2039 match locale {
2040 Locale::Ja => {
2041 "現在のユーザー要求とライブツール証拠は、メモリ、古い引き継ぎ、推測より優先される。"
2042 }
2043 Locale::ZhHans => "当前用户请求和实时工具证据优先于记忆、陈旧交接和猜测。",
2044 Locale::ZhHant => "目前使用者請求和即時工具證據優先於記憶、陳舊交接和猜測。",
2045 Locale::PtBr => {
2046 "Pedidos atuais do usuário e evidência viva das ferramentas superam memória, handoffs antigos e palpites."
2047 }
2048 Locale::Es419 => {
2049 "Las solicitudes actuales del usuario y la evidencia viva de herramientas superan memoria, handoffs viejos y suposiciones."
2050 }
2051 Locale::Vi => {
2052 "Yêu cầu hiện tại của người dùng và bằng chứng trực tiếp từ công cụ ưu tiên hơn bộ nhớ, handoff cũ và phỏng đoán."
2053 }
2054 Locale::Ko => {
2055 "현재 사용자 요청과 실시간 도구 근거는 메모리, 오래된 인계 자료, 추측보다 우선한다."
2056 }
2057 Locale::Ca => {
2058 "Les peticions actuals de l'usuari i l'evidència en directe de les eines prevalen sobre la memòria, els traspasos antics i les conjectures."
2059 }
2060 Locale::De => {
2061 "Aktuelle Nutzeranfragen und Live-Werkzeugnachweise haben Vorrang vor Speicher, veralteten Übergaben und Vermutungen."
2062 }
2063 Locale::Fr => {
2064 "Les demandes actuelles de l'utilisateur et les preuves directes des outils priment sur la mémoire, les anciens transferts et les suppositions."
2065 }
2066 Locale::Id => {
2067 "Permintaan pengguna saat ini dan bukti langsung dari alat mengalahkan memori, handoff lama, dan tebakan."
2068 }
2069 Locale::Hi => "वर्तमान उपयोगकर्ता अनुरोध और लाइव टूल साक्ष्य मेमोरी, पुराने हैंडऑफ़ और अनुमानों से ऊपर हैं।",
2070 Locale::Ru => {
2071 "Текущие запросы пользователя и живые свидетельства инструментов важнее памяти, устаревших передаточных заметок и догадок."
2072 }
2073 Locale::Uk => {
2074 "Поточні запити користувача та живі свідчення інструментів важливіші за пам'ять, застарілі передаточні нотатки й здогадки."
2075 }
2076 _ => {
2077 "Current user requests and live tool evidence outrank memory, stale handoffs, and guesses."
2078 }
2079 }
2080 }
2081
2082 fn bounded_freeform_note(input: &str, max_chars: usize) -> String {
2083 input
2084 .chars()
2085 .filter_map(|ch| {
2086 if ch == '\t' {
2087 Some(' ')
2088 } else if ch == '\n' || !ch.is_control() {
2089 Some(ch)
2090 } else {
2091 None
2092 }
2093 })
2094 .take(max_chars)
2095 .collect::<String>()
2096 .trim()
2097 .to_string()
2098 }
2099
2100 fn compact_freeform_preview(note: &str) -> String {
2101 let compact = note.split_whitespace().collect::<Vec<_>>().join(" ");
2102 let mut preview = compact.chars().take(96).collect::<String>();
2103 if compact.chars().count() > 96 {
2104 preview.push_str("...");
2105 }
2106 preview
2107 }
2108
2109 fn freeform_note_line(locale: Locale, note: &str, editing: bool) -> Line<'static> {
2110 let preview = compact_freeform_preview(note);
2111 let text = match (locale, editing, preview.is_empty()) {
2112 (Locale::Ja, true, true) => {
2113 "F 自由原則:編集中 - 有界の原則を入力または貼り付け、Enter で完了".to_string()
2114 }
2115 (Locale::Ja, true, false) => format!("F 自由原則:編集中 - {preview}"),
2116 (Locale::Ja, false, true) => "F 自由原則:F で有界の原則を入力または貼り付け".to_string(),
2117 (Locale::Ja, false, false) => format!("F 自由原則:{preview}"),
2118 (Locale::ZhHans, true, true) => {
2119 "F 自定义准则:正在编辑 - 输入或粘贴明确的准则,Enter 完成".to_string()
2120 }
2121 (Locale::ZhHans, true, false) => format!("F 自定义准则:正在编辑 - {preview}"),
2122 (Locale::ZhHans, false, true) => {
2123 "F 自定义准则:按 F 输入或粘贴自己的明确准则".to_string()
2124 }
2125 (Locale::ZhHans, false, false) => format!("F 自定义准则:{preview}"),
2126 (Locale::ZhHant, true, true) => {
2127 "F 自由原則:正在編輯 - 輸入或貼上有界原則,Enter 完成".to_string()
2128 }
2129 (Locale::ZhHant, true, false) => format!("F 自由原則:正在編輯 - {preview}"),
2130 (Locale::ZhHant, false, true) => "F 自由原則:按 F 輸入或貼上自己的有界原則".to_string(),
2131 (Locale::ZhHant, false, false) => format!("F 自由原則:{preview}"),
2132 (Locale::PtBr, true, true) => {
2133 "F Princípio livre: editando - digite ou cole um princípio limitado, Enter para concluir".to_string()
2134 }
2135 (Locale::PtBr, true, false) => format!("F Princípio livre: editando - {preview}"),
2136 (Locale::PtBr, false, true) => {
2137 "F Princípio livre: pressione F para digitar ou colar um princípio limitado".to_string()
2138 }
2139 (Locale::PtBr, false, false) => format!("F Princípio livre: {preview}"),
2140 (Locale::Es419, true, true) => {
2141 "F Principio libre: editando - escribe o pega un principio acotado, Enter para terminar".to_string()
2142 }
2143 (Locale::Es419, true, false) => format!("F Principio libre: editando - {preview}"),
2144 (Locale::Es419, false, true) => {
2145 "F Principio libre: presiona F para escribir o pegar un principio acotado".to_string()
2146 }
2147 (Locale::Es419, false, false) => format!("F Principio libre: {preview}"),
2148 (Locale::Vi, true, true) => {
2149 "F Nguyên tắc tự do: đang sửa - nhập hoặc dán nguyên tắc có giới hạn, Enter để xong".to_string()
2150 }
2151 (Locale::Vi, true, false) => format!("F Nguyên tắc tự do: đang sửa - {preview}"),
2152 (Locale::Vi, false, true) => {
2153 "F Nguyên tắc tự do: nhấn F để nhập hoặc dán nguyên tắc có giới hạn".to_string()
2154 }
2155 (Locale::Vi, false, false) => format!("F Nguyên tắc tự do: {preview}"),
2156 (Locale::Ko, true, true) => {
2157 "F 자유 원칙: 편집 중 - 제한된 원칙을 입력하거나 붙여넣고 Enter로 완료".to_string()
2158 }
2159 (Locale::Ko, true, false) => format!("F 자유 원칙: 편집 중 - {preview}"),
2160 (Locale::Ko, false, true) => "F 자유 원칙: F를 눌러 제한된 원칙을 입력하거나 붙여넣기".to_string(),
2161 (Locale::Ko, false, false) => format!("F 자유 원칙: {preview}"),
2162 (Locale::Ca, true, true) => {
2163 "F Paraules pròpies: editant - escriu o enganxa un principi acotat, Enter per acabar".to_string()
2164 }
2165 (Locale::Ca, true, false) => format!("F Paraules pròpies: editant - {preview}"),
2166 (Locale::Ca, false, true) => {
2167 "F Paraules pròpies: prem F per escriure o enganxar un principi acotat".to_string()
2168 }
2169 (Locale::Ca, false, false) => format!("F Paraules pròpies: {preview}"),
2170 (Locale::De, true, true) => {
2171 "F Eigene Worte: Bearbeitung - tippe oder füge ein begrenztes Prinzip ein, Enter zum Abschluss".to_string()
2172 }
2173 (Locale::De, true, false) => format!("F Eigene Worte: Bearbeitung - {preview}"),
2174 (Locale::De, false, true) => {
2175 "F Eigene Worte: F drücken, um ein begrenztes Prinzip zu tippen oder einzufügen".to_string()
2176 }
2177 (Locale::De, false, false) => format!("F Eigene Worte: {preview}"),
2178 (Locale::Fr, true, true) => {
2179 "F Vos mots : édition - tapez ou collez un principe borné, Entrée pour terminer".to_string()
2180 }
2181 (Locale::Fr, true, false) => format!("F Vos mots : édition - {preview}"),
2182 (Locale::Fr, false, true) => {
2183 "F Vos mots : appuyez sur F pour taper ou coller un principe borné".to_string()
2184 }
2185 (Locale::Fr, false, false) => format!("F Vos mots : {preview}"),
2186 (Locale::Id, true, true) => {
2187 "F Kata sendiri: mengedit - ketik atau tempel prinsip terbatas, Enter untuk selesai".to_string()
2188 }
2189 (Locale::Id, true, false) => format!("F Kata sendiri: mengedit - {preview}"),
2190 (Locale::Id, false, true) => {
2191 "F Kata sendiri: tekan F untuk mengetik atau menempel prinsip terbatas".to_string()
2192 }
2193 (Locale::Id, false, false) => format!("F Kata sendiri: {preview}"),
2194 (Locale::Hi, true, true) => {
2195 "F अपने शब्द: संपादन जारी - सीमित सिद्धांत टाइप या पेस्ट करें, Enter से समाप्त करें".to_string()
2196 }
2197 (Locale::Hi, true, false) => format!("F अपने शब्द: संपादन जारी - {preview}"),
2198 (Locale::Hi, false, true) => {
2199 "F अपने शब्द: सीमित सिद्धांत टाइप या पेस्ट करने के लिए F दबाएँ".to_string()
2200 }
2201 (Locale::Hi, false, false) => format!("F अपने शब्द: {preview}"),
2202 (Locale::Ru, true, true) => {
2203 "F Свои слова: редактирование - введите или вставьте ограниченный принцип, Enter для завершения".to_string()
2204 }
2205 (Locale::Ru, true, false) => format!("F Свои слова: редактирование - {preview}"),
2206 (Locale::Ru, false, true) => {
2207 "F Свои слова: нажмите F, чтобы ввести или вставить ограниченный принцип".to_string()
2208 }
2209 (Locale::Ru, false, false) => format!("F Свои слова: {preview}"),
2210 (Locale::Uk, true, true) => {
2211 "F Свої слова: редагування - введіть або вставте обмежений принцип, Enter для завершення".to_string()
2212 }
2213 (Locale::Uk, true, false) => format!("F Свої слова: редагування - {preview}"),
2214 (Locale::Uk, false, true) => {
2215 "F Свої слова: натисніть F, щоб ввести або вставити обмежений принцип".to_string()
2216 }
2217 (Locale::Uk, false, false) => format!("F Свої слова: {preview}"),
2218 (_, true, true) => {
2219 "F Own words: editing - type or paste a bounded principle, Enter to finish".to_string()
2220 }
2221 (_, true, false) => format!("F Own words: editing - {preview}"),
2222 (_, false, true) => "F Own words: press F to type or paste a bounded principle".to_string(),
2223 (_, false, false) => format!("F Own words: {preview}"),
2224 };
2225 let style = if editing || !preview.is_empty() {
2226 Style::default().fg(palette::WHALE_HUMAN)
2227 } else {
2228 Style::default().fg(palette::TEXT_MUTED)
2229 };
2230 Line::from(Span::styled(text, style))
2231 }
2232
2233 impl SetupWizardView {
2234 #[cfg(test)]
2235 #[must_use]
2236 pub fn new(state: SetupState, locale: Locale) -> Self {
2237 let selected = initial_step_index(&state);
2238 Self {
2239 state,
2240 selected,
2241 locale,
2242 facts: SetupRuntimeFacts::default(),
2243 guided_draft: GuidedConstitutionDraft::default(),
2244 freeform_note: String::new(),
2245 editing_freeform_note: false,
2246 guided_preview_seen: false,
2247 existing_preview_seen: false,
2248 model_draft: None,
2249 model_draft_label: None,
2250 runtime_preset: SetupRuntimePreset::default(),
2251 runtime_preset_preview_seen: false,
2252 body_scroll: 0,
2253 }
2254 }
2255
2256 #[must_use]
2257 pub fn new_for_app(app: &App, config: &Config) -> Self {
2258 Self::new_with_facts(
2259 load_setup_state_for_app(app, config),
2260 app.ui_locale,
2261 SetupRuntimeFacts::from_app_config(app, config),
2262 )
2263 }
2264
2265 #[must_use]
2266 pub fn new_for_app_at(app: &App, config: &Config, step: SetupStep) -> Self {
2267 Self::new_at_with_facts(
2268 load_setup_state_for_app(app, config),
2269 app.ui_locale,
2270 step,
2271 SetupRuntimeFacts::from_app_config(app, config),
2272 )
2273 }
2274
2275 #[cfg(test)]
2276 #[must_use]
2277 pub fn state(&self) -> &SetupState {
2278 &self.state
2279 }
2280
2281 #[must_use]
2282 pub fn selected_step(&self) -> SetupStep {
2283 STEP_SPECS[self.selected].id()
2284 }
2285
2286 fn selected_spec(&self) -> &'static dyn SetupWizardStep {
2287 &STEP_SPECS[self.selected]
2288 }
2289
2290 fn new_with_facts(state: SetupState, locale: Locale, facts: SetupRuntimeFacts) -> Self {
2291 let selected = initial_step_index(&state);
2292 Self {
2293 state,
2294 selected,
2295 locale,
2296 facts,
2297 guided_draft: GuidedConstitutionDraft::default(),
2298 freeform_note: String::new(),
2299 editing_freeform_note: false,
2300 guided_preview_seen: false,
2301 existing_preview_seen: false,
2302 model_draft: None,
2303 model_draft_label: None,
2304 runtime_preset: SetupRuntimePreset::default(),
2305 runtime_preset_preview_seen: false,
2306 body_scroll: 0,
2307 }
2308 }
2309
2310 fn new_at_with_facts(
2311 state: SetupState,
2312 locale: Locale,
2313 step: SetupStep,
2314 facts: SetupRuntimeFacts,
2315 ) -> Self {
2316 Self {
2317 state,
2318 selected: visible_step_index(step),
2319 locale,
2320 facts,
2321 guided_draft: GuidedConstitutionDraft::default(),
2322 freeform_note: String::new(),
2323 editing_freeform_note: false,
2324 guided_preview_seen: false,
2325 existing_preview_seen: false,
2326 model_draft: None,
2327 model_draft_label: None,
2328 runtime_preset: SetupRuntimePreset::default(),
2329 runtime_preset_preview_seen: false,
2330 body_scroll: 0,
2331 }
2332 }
2333
2334 fn move_next(&mut self) {
2335 self.selected = (self.selected + 1).min(STEP_SPECS.len().saturating_sub(1));
2336 self.body_scroll = 0;
2337 }
2338
2339 fn move_back(&mut self) {
2340 self.selected = self.selected.saturating_sub(1);
2341 self.body_scroll = 0;
2342 }
2343
2344 fn commit_selected_status(
2345 &mut self,
2346 status: StepStatus,
2347 message_id: MessageId,
2348 advance: bool,
2349 ) -> ViewAction {
2350 let spec = self.selected_spec();
2351 let result = match status {
2352 StepStatus::Skipped => Some("skipped by user"),
2353 StepStatus::NeedsAction => Some("retry requested; needs action"),
2354 _ => None,
2355 };
2356 let mut entry = StepEntry::new(status, spec.required(), CONSTITUTION_CHECKPOINT_VERSION);
2357 if let Some(result) = result {
2358 entry = entry.with_result(result);
2359 }
2360 let mut state = self.state.clone();
2361 state.set_step(spec.id(), entry);
2362 if spec.id() == SetupStep::Constitution && status == StepStatus::Skipped {
2363 // `S` is a durable response to the versioned checkpoint, just like
2364 // choosing the explicit defer action. It only skips this setup
2365 // checkpoint, though; it must not replace an already active
2366 // bundled or custom Constitution choice. A fresh state has no
2367 // active choice, so keep the bundled floor by recording Deferred.
2368 let choice = if state.constitution_choice.is_explicit() {
2369 state.constitution_choice
2370 } else {
2371 ConstitutionChoice::Deferred
2372 };
2373 state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice);
2374 }
2375 self.state = state.clone();
2376 if advance {
2377 self.move_next();
2378 }
2379 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2380 state,
2381 message: tr(self.locale, message_id).to_string(),
2382 })
2383 }
2384
2385 fn commit_language_review(&mut self) -> ViewAction {
2386 let mut state = self.state.clone();
2387 state.constitution_language = Some(self.locale.tag().to_string());
2388 state.set_step(
2389 SetupStep::Language,
2390 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2391 .with_result(format!("setup locale {}", self.locale.tag())),
2392 );
2393 self.state = state.clone();
2394 self.move_next();
2395 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2396 state,
2397 message: tr(self.locale, MessageId::SetupLanguageReviewed).to_string(),
2398 })
2399 }
2400
2401 fn commit_provider_model_review(&mut self) -> ViewAction {
2402 let status = provider::step_status(self.facts.provider_ready);
2403 let mut state = self.state.clone();
2404 state.set_step(
2405 SetupStep::ProviderModel,
2406 provider::step_entry(
2407 self.facts.provider_ready,
2408 CONSTITUTION_CHECKPOINT_VERSION,
2409 self.facts.provider_result.clone(),
2410 ),
2411 );
2412 self.state = state.clone();
2413 self.move_next();
2414 let message_id = if status == StepStatus::Verified {
2415 MessageId::SetupProviderModelReviewed
2416 } else {
2417 MessageId::SetupProviderModelNeedsActionSaved
2418 };
2419 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2420 state,
2421 message: tr(self.locale, message_id).to_string(),
2422 })
2423 }
2424
2425 fn commit_runtime_posture_review(&mut self) -> ViewAction {
2426 let mut state = self.state.clone();
2427 state.runtime_posture_source = RuntimePostureSource::Confirmed;
2428 state.set_step(
2429 SetupStep::TrustSandbox,
2430 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2431 .with_result(self.facts.runtime_result.clone()),
2432 );
2433 self.state = state.clone();
2434 self.move_next();
2435 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2436 state,
2437 message: tr(self.locale, MessageId::SetupRuntimePostureReviewed).to_string(),
2438 })
2439 }
2440
2441 fn operate_fleet_facts_ready(&self) -> bool {
2442 // Provider, capacity, and roster facts are configuration snapshots,
2443 // not proof of dispatch and terminal receipts. This release must never
2444 // persist an Operate-ready claim from those facts alone.
2445 false
2446 }
2447
2448 fn commit_operate_fleet_review(&mut self) -> ViewAction {
2449 let status = if self.operate_fleet_facts_ready() {
2450 StepStatus::Verified
2451 } else {
2452 StepStatus::NeedsAction
2453 };
2454 let mut state = self.state.clone();
2455 state.set_step(
2456 SetupStep::OperateFleet,
2457 StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION)
2458 .with_result(self.facts.operate_result.clone()),
2459 );
2460 self.state = state.clone();
2461 self.move_next();
2462 let message_id = if status == StepStatus::Verified {
2463 MessageId::SetupOperateReviewed
2464 } else {
2465 MessageId::SetupOperateNeedsActionSaved
2466 };
2467 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2468 state,
2469 message: tr(self.locale, message_id).to_string(),
2470 })
2471 }
2472
2473 fn commit_hotbar_review(&mut self) -> ViewAction {
2474 let mut state = self.state.clone();
2475 state.set_step(
2476 SetupStep::Hotbar,
2477 StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION)
2478 .with_result(self.facts.hotbar_result.clone()),
2479 );
2480 self.state = state.clone();
2481 self.move_next();
2482 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2483 state,
2484 message: tr(self.locale, MessageId::SetupHotbarReviewed).to_string(),
2485 })
2486 }
2487
2488 fn commit_tools_mcp_review(&mut self) -> ViewAction {
2489 // Optional step: empty/off inventories settle as Optional; broken
2490 // configured tools record NeedsAction without blocking first-run.
2491 let status = if self.facts.tools_mcp_needs_action {
2492 StepStatus::NeedsAction
2493 } else if self.facts.tools_mcp_result.contains("overall=off") {
2494 StepStatus::Optional
2495 } else {
2496 StepStatus::Verified
2497 };
2498 let mut state = self.state.clone();
2499 state.set_step(
2500 SetupStep::ToolsMcp,
2501 StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION)
2502 .with_result(self.facts.tools_mcp_result.clone()),
2503 );
2504 self.state = state.clone();
2505 self.move_next();
2506 let message_id = if status == StepStatus::NeedsAction {
2507 MessageId::SetupToolsMcpNeedsActionSaved
2508 } else {
2509 MessageId::SetupToolsMcpReviewed
2510 };
2511 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2512 state,
2513 message: tr(self.locale, message_id).to_string(),
2514 })
2515 }
2516
2517 fn preview_tools_mcp_on_ramp(&self) -> ViewAction {
2518 ViewAction::Emit(ViewEvent::OpenTextPager {
2519 title: tr(self.locale, MessageId::SetupToolsMcpPreviewTitle).to_string(),
2520 content: tools_mcp_on_ramp_text(self.locale, &self.facts),
2521 })
2522 }
2523
2524 fn preview_remote_runtime_on_ramp(&self) -> ViewAction {
2525 ViewAction::Emit(ViewEvent::OpenTextPager {
2526 title: tr(self.locale, MessageId::SetupRemotePreviewTitle).to_string(),
2527 content: remote_runtime_on_ramp_text(self.locale, &self.facts),
2528 })
2529 }
2530
2531 /// Record the remote step honestly (#3409).
2532 ///
2533 /// Local-only always works, so Enter alone settles the step — a user who
2534 /// never wants remote access is finished in one key. When a *reachable*
2535 /// mode is missing a token or config the entry is `NeedsAction`, which the
2536 /// setup report and doctor inherit verbatim and which never blocks ready.
2537 fn commit_remote_runtime_review(&mut self) -> ViewAction {
2538 let mut state = self.state.clone();
2539 let status = if self.facts.remote_needs_action {
2540 StepStatus::NeedsAction
2541 } else {
2542 StepStatus::Verified
2543 };
2544 state.set_step(
2545 SetupStep::RemoteRuntime,
2546 StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION)
2547 .with_result(self.facts.remote_result.clone()),
2548 );
2549 self.state = state.clone();
2550 self.move_next();
2551 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2552 state,
2553 message: tr(self.locale, MessageId::SetupRemoteReviewed).to_string(),
2554 })
2555 }
2556
2557 fn commit_persistence_review(&mut self) -> ViewAction {
2558 let mut state = self.state.clone();
2559 state.set_step(
2560 SetupStep::Persistence,
2561 StepEntry::new(StepStatus::Verified, false, CONSTITUTION_CHECKPOINT_VERSION)
2562 .with_result(self.facts.persistence.result.clone()),
2563 );
2564 self.state = state.clone();
2565 self.move_next();
2566 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2567 state,
2568 message: tr(self.locale, MessageId::SetupPersistenceReviewed).to_string(),
2569 })
2570 }
2571
2572 fn select_runtime_preset(&mut self, key: char) -> ViewAction {
2573 if let Some(preset) = SetupRuntimePreset::from_key(key)
2574 && preset != self.runtime_preset
2575 {
2576 self.runtime_preset = preset;
2577 self.runtime_preset_preview_seen = false;
2578 }
2579 ViewAction::None
2580 }
2581
2582 fn preview_runtime_preset(&mut self) -> ViewAction {
2583 self.runtime_preset_preview_seen = true;
2584 ViewAction::Emit(ViewEvent::OpenTextPager {
2585 title: tr(self.locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(),
2586 content: runtime_preset_preview_text(self.locale, self.runtime_preset, &self.facts),
2587 })
2588 }
2589
2590 fn commit_runtime_preset(&mut self) -> ViewAction {
2591 if !self.runtime_preset_preview_seen {
2592 return self.preview_runtime_preset();
2593 }
2594
2595 let mut state = self.state.clone();
2596 state.runtime_posture_source = RuntimePostureSource::Confirmed;
2597 state.set_step(
2598 SetupStep::TrustSandbox,
2599 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2600 .with_result(self.runtime_preset.result_summary()),
2601 );
2602 self.state = state.clone();
2603 self.move_next();
2604 ViewAction::Emit(ViewEvent::SetupRuntimePresetApplyRequested {
2605 preset: self.runtime_preset,
2606 state,
2607 message: tr(self.locale, MessageId::SetupRuntimePresetApplied).to_string(),
2608 })
2609 }
2610
2611 fn commit_setup_report(&mut self) -> ViewAction {
2612 let mut state = self.state.clone();
2613 let status = if setup_report_ready(&state) {
2614 StepStatus::Verified
2615 } else {
2616 StepStatus::NeedsAction
2617 };
2618 state.set_step(
2619 SetupStep::Verification,
2620 StepEntry::new(status, false, CONSTITUTION_CHECKPOINT_VERSION)
2621 .with_result(setup_report_result(&state, &self.facts)),
2622 );
2623 self.state = state.clone();
2624 ViewAction::Emit(ViewEvent::SetupStateCommitRequested {
2625 state,
2626 message: tr(self.locale, MessageId::SetupReportRecorded).to_string(),
2627 })
2628 }
2629
2630 fn commit_guided_constitution(&mut self) -> ViewAction {
2631 if !self.guided_preview_seen {
2632 return self.preview_guided_constitution();
2633 }
2634
2635 let (constitution, authoring) = match self.model_draft.as_deref() {
2636 // Model drafts arrive sanitized + bounded from the untrusted-JSON
2637 // gate; ratify exactly what was previewed.
2638 Some(draft) => (draft.clone(), ConstitutionAuthoring::ModelDrafted),
2639 None => (
2640 self.guided_draft
2641 .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()),
2642 ConstitutionAuthoring::Guided,
2643 ),
2644 };
2645 let mut state = self.state.clone();
2646 state.complete_constitution_checkpoint(
2647 CONSTITUTION_CHECKPOINT_VERSION,
2648 ConstitutionChoice::GuidedCustom,
2649 );
2650 state.constitution_language = constitution.language.clone();
2651 state.constitution_source = ConstitutionSource::UserGlobal;
2652 state.constitution_validity = ConstitutionValidity::Valid;
2653 state.constitution_authoring = Some(authoring);
2654 state.constitution_preview_hash = Some(constitution.preview_hash());
2655 state.constitution_preview_version =
2656 state.constitution_preview_version.saturating_add(1).max(1);
2657 let hash = state
2658 .constitution_preview_hash
2659 .as_deref()
2660 .unwrap_or("unknown");
2661 let result = match authoring {
2662 ConstitutionAuthoring::ModelDrafted => format!(
2663 "model-drafted constitution ratified ({}) preview_hash={hash}",
2664 self.model_draft_label.as_deref().unwrap_or("model")
2665 ),
2666 ConstitutionAuthoring::Guided => {
2667 format!("guided custom constitution preview_hash={hash}")
2668 }
2669 };
2670 state.set_step(
2671 SetupStep::Constitution,
2672 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2673 .with_result(result),
2674 );
2675 self.state = state.clone();
2676 ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
2677 constitution,
2678 state,
2679 message: tr(self.locale, MessageId::SetupCheckpointDoneGuided).to_string(),
2680 })
2681 }
2682
2683 fn preview_guided_constitution(&mut self) -> ViewAction {
2684 self.guided_preview_seen = true;
2685 let (constitution, provenance) = match self.model_draft.as_deref() {
2686 Some(draft) => (
2687 draft.clone(),
2688 DraftProvenance::Model(
2689 self.model_draft_label
2690 .clone()
2691 .unwrap_or_else(|| "model".to_string()),
2692 ),
2693 ),
2694 None => (
2695 self.guided_draft
2696 .to_constitution_with_freeform(self.locale, self.freeform_note_for_draft()),
2697 DraftProvenance::Guided,
2698 ),
2699 };
2700 ViewAction::Emit(ViewEvent::OpenTextPager {
2701 title: ratification_preview_title(self.locale).to_string(),
2702 content: constitution_ratification_text(self.locale, &constitution, &provenance),
2703 })
2704 }
2705
2706 fn cycle_guided_answer(&mut self, key: char) -> ViewAction {
2707 if self.guided_draft.cycle(key) {
2708 self.guided_preview_seen = false;
2709 // Answers changed under the draft: the model draft is stale law
2710 // and must be re-drafted or replaced by the guided rendering.
2711 self.model_draft = None;
2712 self.model_draft_label = None;
2713 }
2714 ViewAction::None
2715 }
2716
2717 /// `A` on the constitution step: ask the first configured model to draft.
2718 /// Requires a ready provider route; otherwise the key is inert and the
2719 /// deterministic guided flow stands untouched.
2720 fn request_model_draft(&self) -> ViewAction {
2721 if !self.facts.provider_ready {
2722 return ViewAction::None;
2723 }
2724 ViewAction::Emit(ViewEvent::SetupConstitutionModelDraftRequested {
2725 draft: self.guided_draft,
2726 freeform_note: self.freeform_note_for_draft().map(str::to_string),
2727 locale: self.locale,
2728 })
2729 }
2730
2731 fn toggle_freeform_edit(&mut self) -> ViewAction {
2732 if self.selected_step() == SetupStep::Constitution {
2733 self.editing_freeform_note = !self.editing_freeform_note;
2734 }
2735 ViewAction::None
2736 }
2737
2738 fn freeform_note_for_draft(&self) -> Option<&str> {
2739 let note = self.freeform_note.trim();
2740 (!note.is_empty()).then_some(note)
2741 }
2742
2743 fn append_freeform_note_text(&mut self, text: &str) {
2744 let mut next = self.freeform_note.clone();
2745 next.push_str(text);
2746 self.freeform_note = bounded_freeform_note(&next, MAX_NOTES_LEN);
2747 self.guided_preview_seen = false;
2748 self.model_draft = None;
2749 self.model_draft_label = None;
2750 }
2751
2752 fn handle_freeform_note_key(&mut self, key: KeyEvent) -> Option<ViewAction> {
2753 if self.selected_step() != SetupStep::Constitution || !self.editing_freeform_note {
2754 return None;
2755 }
2756 match key.code {
2757 KeyCode::Esc | KeyCode::Enter => {
2758 self.editing_freeform_note = false;
2759 Some(ViewAction::None)
2760 }
2761 KeyCode::Backspace => {
2762 self.freeform_note.pop();
2763 self.guided_preview_seen = false;
2764 self.model_draft = None;
2765 self.model_draft_label = None;
2766 Some(ViewAction::None)
2767 }
2768 KeyCode::Char(c) if key.modifiers.is_empty() => {
2769 let mut buf = [0; 4];
2770 self.append_freeform_note_text(c.encode_utf8(&mut buf));
2771 Some(ViewAction::None)
2772 }
2773 _ => Some(ViewAction::None),
2774 }
2775 }
2776
2777 /// Install a model-drafted constitution (already sanitized + bounded by
2778 /// the untrusted-JSON gate) and return the `(title, content)` of the
2779 /// ratification preview the host must open in the same breath — that is
2780 /// what satisfies the preview gate. Ratifying still takes the explicit
2781 /// `G` keypress afterwards.
2782 #[must_use]
2783 pub(crate) fn install_model_draft(
2784 &mut self,
2785 constitution: Box<UserConstitution>,
2786 model_label: String,
2787 ) -> (String, String) {
2788 let content = constitution_ratification_text(
2789 self.locale,
2790 &constitution,
2791 &DraftProvenance::Model(model_label.clone()),
2792 );
2793 self.model_draft = Some(constitution);
2794 self.model_draft_label = Some(model_label);
2795 self.guided_preview_seen = true;
2796 (ratification_preview_title(self.locale).to_string(), content)
2797 }
2798
2799 fn commit_constitution(&self, kind: SetupCommitKind) -> ViewAction {
2800 let choice = match kind {
2801 SetupCommitKind::BundledConstitution => ConstitutionChoice::Bundled,
2802 SetupCommitKind::DeferredConstitution => ConstitutionChoice::Deferred,
2803 };
2804 let mut state = self.state.clone();
2805 state.complete_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION, choice);
2806 state.constitution_source = ConstitutionSource::Bundled;
2807 state.constitution_validity = ConstitutionValidity::Unknown;
2808 state.constitution_authoring = None;
2809 state.constitution_preview_hash = None;
2810 state.set_step(
2811 SetupStep::Constitution,
2812 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2813 .with_result(match kind {
2814 SetupCommitKind::BundledConstitution => "bundled/default constitution",
2815 SetupCommitKind::DeferredConstitution => "checkpoint deferred; bundled applies",
2816 }),
2817 );
2818 let message_id = match kind {
2819 SetupCommitKind::BundledConstitution => MessageId::SetupCheckpointDoneBundled,
2820 SetupCommitKind::DeferredConstitution => MessageId::SetupCheckpointDeferred,
2821 };
2822 ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested {
2823 state,
2824 message: tr(self.locale, message_id).to_string(),
2825 })
2826 }
2827
2828 /// Complete the checkpoint by keeping the existing valid
2829 /// `constitution.json` exactly as it stands (#3794). First `K` previews
2830 /// the rendered law; second `K` records the choice. The file is never
2831 /// rewritten — only `setup_state.json` changes, through the same commit
2832 /// event as every other completion.
2833 fn commit_keep_existing_constitution(&mut self) -> ViewAction {
2834 if self.facts.constitution_file != SetupConstitutionFileState::Loaded {
2835 return ViewAction::None;
2836 }
2837 // Re-read the live file so a stale card cannot ratify a file that
2838 // has since become invalid; any non-loaded state leaves the key inert.
2839 let Ok(load) = UserConstitution::load() else {
2840 return ViewAction::None;
2841 };
2842 let Some(constitution) = load.constitution() else {
2843 return ViewAction::None;
2844 };
2845 if !self.existing_preview_seen {
2846 self.existing_preview_seen = true;
2847 let content = constitution_ratification_text(
2848 self.locale,
2849 constitution,
2850 &DraftProvenance::Existing,
2851 );
2852 return ViewAction::Emit(ViewEvent::OpenTextPager {
2853 title: ratification_preview_title(self.locale).to_string(),
2854 content,
2855 });
2856 }
2857 let mut state = self.state.clone();
2858 state.complete_constitution_checkpoint(
2859 CONSTITUTION_CHECKPOINT_VERSION,
2860 ConstitutionChoice::GuidedCustom,
2861 );
2862 state.constitution_source = ConstitutionSource::UserGlobal;
2863 state.constitution_validity = ConstitutionValidity::Valid;
2864 state.constitution_preview_hash = Some(constitution.preview_hash());
2865 state.set_step(
2866 SetupStep::Constitution,
2867 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION)
2868 .with_result("existing constitution kept unchanged"),
2869 );
2870 ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested {
2871 state,
2872 message: tr(self.locale, MessageId::SetupCheckpointDoneKept).to_string(),
2873 })
2874 }
2875
2876 fn status_label(&self, status: StepStatus) -> Cow<'static, str> {
2877 tr(
2878 self.locale,
2879 match status {
2880 StepStatus::NotStarted => MessageId::SetupStatusNotStarted,
2881 StepStatus::Recommended => MessageId::SetupStatusRecommended,
2882 StepStatus::Optional => MessageId::SetupStatusOptional,
2883 StepStatus::Deferred => MessageId::SetupStatusDeferred,
2884 StepStatus::InProgress => MessageId::SetupStatusInProgress,
2885 StepStatus::NeedsAction => MessageId::SetupStatusNeedsAction,
2886 StepStatus::Verified => MessageId::SetupStatusVerified,
2887 StepStatus::Skipped => MessageId::SetupStatusSkipped,
2888 StepStatus::Failed => MessageId::SetupStatusFailed,
2889 },
2890 )
2891 }
2892 }
2893
2894 impl ModalView for SetupWizardView {
2895 fn kind(&self) -> ModalKind {
2896 ModalKind::SetupWizard
2897 }
2898
2899 fn handle_key(&mut self, key: KeyEvent) -> ViewAction {
2900 if let Some(action) = self.handle_freeform_note_key(key) {
2901 return action;
2902 }
2903 match key.code {
2904 KeyCode::Esc | KeyCode::Char('q') => ViewAction::Close,
2905 KeyCode::Left | KeyCode::Char('b') => {
2906 self.move_back();
2907 ViewAction::None
2908 }
2909 KeyCode::Right | KeyCode::Char('n') => {
2910 self.move_next();
2911 ViewAction::None
2912 }
2913 KeyCode::PageUp => {
2914 self.body_scroll = self.body_scroll.saturating_sub(8);
2915 ViewAction::None
2916 }
2917 KeyCode::PageDown => {
2918 self.body_scroll = self.body_scroll.saturating_add(8);
2919 ViewAction::None
2920 }
2921 KeyCode::Up => {
2922 self.move_back();
2923 ViewAction::None
2924 }
2925 KeyCode::Down => {
2926 self.move_next();
2927 ViewAction::None
2928 }
2929 KeyCode::Char('s') => {
2930 self.commit_selected_status(StepStatus::Skipped, MessageId::SetupStepSkipped, true)
2931 }
2932 KeyCode::Char('r') if self.selected_step() == SetupStep::ToolsMcp => {
2933 self.preview_tools_mcp_on_ramp()
2934 }
2935 KeyCode::Char('r') if self.selected_step() == SetupStep::RemoteRuntime => {
2936 self.preview_remote_runtime_on_ramp()
2937 }
2938 KeyCode::Char('r') => self.commit_selected_status(
2939 StepStatus::NeedsAction,
2940 MessageId::SetupStepRetryRecorded,
2941 false,
2942 ),
2943 KeyCode::Char('g') if self.selected_step() == SetupStep::Constitution => {
2944 self.commit_guided_constitution()
2945 }
2946 KeyCode::Char('p') if self.selected_step() == SetupStep::ProviderModel => {
2947 ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested)
2948 }
2949 KeyCode::Char('m') if self.selected_step() == SetupStep::ProviderModel => {
2950 ViewAction::EmitAndClose(ViewEvent::SetupOpenModelRequested)
2951 }
2952 KeyCode::Char('p') if self.selected_step() == SetupStep::OperateFleet => {
2953 ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested)
2954 }
2955 KeyCode::Char('f') if self.selected_step() == SetupStep::OperateFleet => {
2956 ViewAction::EmitAndClose(ViewEvent::SetupOpenFleetRequested)
2957 }
2958 KeyCode::Char('h') if self.selected_step() == SetupStep::Hotbar => {
2959 ViewAction::EmitAndClose(ViewEvent::SetupOpenHotbarRequested)
2960 }
2961 KeyCode::Char('m') if self.selected_step() == SetupStep::TrustSandbox => {
2962 ViewAction::EmitAndClose(ViewEvent::SetupOpenModeRequested)
2963 }
2964 KeyCode::Char('c') if self.selected_step() == SetupStep::TrustSandbox => {
2965 ViewAction::EmitAndClose(ViewEvent::SetupOpenConfigRequested)
2966 }
2967 KeyCode::Char(key @ ('1' | '2' | '3'))
2968 if self.selected_step() == SetupStep::TrustSandbox =>
2969 {
2970 self.select_runtime_preset(key)
2971 }
2972 KeyCode::Char('a') if self.selected_step() == SetupStep::TrustSandbox => {
2973 self.commit_runtime_preset()
2974 }
2975 KeyCode::Char(key @ ('1' | '2' | '3' | '4' | '5' | '6'))
2976 if self.selected_step() == SetupStep::Constitution =>
2977 {
2978 self.cycle_guided_answer(key)
2979 }
2980 KeyCode::Char('a') if self.selected_step() == SetupStep::Constitution => {
2981 self.request_model_draft()
2982 }
2983 KeyCode::Char('f') if self.selected_step() == SetupStep::Constitution => {
2984 self.toggle_freeform_edit()
2985 }
2986 KeyCode::Char('k') if self.selected_step() == SetupStep::Constitution => {
2987 self.commit_keep_existing_constitution()
2988 }
2989 KeyCode::Char('u') => self.commit_constitution(SetupCommitKind::BundledConstitution),
2990 KeyCode::Char('d') => self.commit_constitution(SetupCommitKind::DeferredConstitution),
2991 KeyCode::Enter if self.selected_step() == SetupStep::Constitution => {
2992 self.commit_constitution(SetupCommitKind::BundledConstitution)
2993 }
2994 KeyCode::Enter if self.selected_step() == SetupStep::Language => {
2995 self.commit_language_review()
2996 }
2997 KeyCode::Enter if self.selected_step() == SetupStep::ProviderModel => {
2998 self.commit_provider_model_review()
2999 }
3000 KeyCode::Enter if self.selected_step() == SetupStep::TrustSandbox => {
3001 self.commit_runtime_posture_review()
3002 }
3003 KeyCode::Enter if self.selected_step() == SetupStep::OperateFleet => {
3004 self.commit_operate_fleet_review()
3005 }
3006 KeyCode::Enter if self.selected_step() == SetupStep::Hotbar => {
3007 self.commit_hotbar_review()
3008 }
3009 KeyCode::Enter if self.selected_step() == SetupStep::ToolsMcp => {
3010 self.commit_tools_mcp_review()
3011 }
3012 KeyCode::Enter if self.selected_step() == SetupStep::RemoteRuntime => {
3013 self.commit_remote_runtime_review()
3014 }
3015 KeyCode::Enter if self.selected_step() == SetupStep::Persistence => {
3016 self.commit_persistence_review()
3017 }
3018 KeyCode::Enter if self.selected_step() == SetupStep::Verification => {
3019 self.commit_setup_report()
3020 }
3021 KeyCode::Enter => {
3022 self.move_next();
3023 ViewAction::None
3024 }
3025 _ => ViewAction::None,
3026 }
3027 }
3028
3029 fn handle_paste(&mut self, text: &str) -> bool {
3030 if self.selected_step() != SetupStep::Constitution {
3031 return false;
3032 }
3033 self.append_freeform_note_text(text);
3034 true
3035 }
3036
3037 fn render(&self, area: Rect, buf: &mut Buffer) {
3038 let progress = format!(
3039 "{} {}/{}",
3040 tr(self.locale, MessageId::SetupWizardProgress),
3041 self.selected + 1,
3042 STEP_SPECS.len()
3043 );
3044 let inner = render_underwater_surface(
3045 area,
3046 buf,
3047 format!(
3048 "{} · {progress}",
3049 tr(self.locale, MessageId::SetupWizardTitle)
3050 ),
3051 );
3052 let mut hints = vec![
3053 ActionHint::new("B", tr(self.locale, MessageId::SetupActionBack).to_string()),
3054 ActionHint::new(
3055 "N",
3056 tr(self.locale, MessageId::SetupActionContinue).to_string(),
3057 ),
3058 ActionHint::new("S", tr(self.locale, MessageId::SetupActionSkip).to_string()),
3059 ActionHint::new(
3060 "R",
3061 tr(self.locale, MessageId::SetupActionRetry).to_string(),
3062 ),
3063 ActionHint::new(
3064 "PgUp/Dn",
3065 tr(self.locale, MessageId::SetupActionScrollBody).to_string(),
3066 ),
3067 ];
3068 if self.selected_step() == SetupStep::Constitution {
3069 hints.push(ActionHint::new(
3070 "1-6",
3071 tr(self.locale, MessageId::SetupActionTuneGuided).to_string(),
3072 ));
3073 if self.facts.provider_ready {
3074 hints.push(ActionHint::new(
3075 "A",
3076 tr(self.locale, MessageId::SetupActionModelDraft).to_string(),
3077 ));
3078 }
3079 hints.push(ActionHint::new(
3080 "G",
3081 tr(self.locale, MessageId::SetupActionGuided).to_string(),
3082 ));
3083 hints.push(ActionHint::new(
3084 "F",
3085 tr(self.locale, MessageId::SetupActionFreeform).to_string(),
3086 ));
3087 if self.facts.constitution_file == SetupConstitutionFileState::Loaded {
3088 hints.push(ActionHint::new(
3089 "K",
3090 tr(self.locale, MessageId::SetupActionKeepExisting).to_string(),
3091 ));
3092 }
3093 } else if self.selected_step() == SetupStep::ProviderModel {
3094 hints.push(ActionHint::new(
3095 "P",
3096 tr(self.locale, MessageId::SetupActionProvider).to_string(),
3097 ));
3098 hints.push(ActionHint::new(
3099 "M",
3100 tr(self.locale, MessageId::SetupActionModel).to_string(),
3101 ));
3102 } else if self.selected_step() == SetupStep::OperateFleet {
3103 hints.push(ActionHint::new(
3104 "P",
3105 tr(self.locale, MessageId::SetupActionProvider).to_string(),
3106 ));
3107 hints.push(ActionHint::new(
3108 "F",
3109 tr(self.locale, MessageId::SetupActionFleet).to_string(),
3110 ));
3111 } else if self.selected_step() == SetupStep::Hotbar {
3112 hints.push(ActionHint::new(
3113 "H",
3114 tr(self.locale, MessageId::SetupActionHotbar).to_string(),
3115 ));
3116 } else if self.selected_step() == SetupStep::RemoteRuntime {
3117 hints.push(ActionHint::new(
3118 "R",
3119 tr(self.locale, MessageId::SetupActionRemote).to_string(),
3120 ));
3121 } else if self.selected_step() == SetupStep::TrustSandbox {
3122 hints.push(ActionHint::new(
3123 "1-3",
3124 tr(self.locale, MessageId::SetupActionRuntimePreset).to_string(),
3125 ));
3126 hints.push(ActionHint::new(
3127 "A",
3128 tr(self.locale, MessageId::SetupActionApplyRuntimePreset).to_string(),
3129 ));
3130 hints.push(ActionHint::new(
3131 "M",
3132 tr(self.locale, MessageId::SetupActionMode).to_string(),
3133 ));
3134 hints.push(ActionHint::new(
3135 "C",
3136 tr(self.locale, MessageId::SetupActionConfig).to_string(),
3137 ));
3138 }
3139 hints.extend([
3140 ActionHint::new(
3141 "U",
3142 tr(self.locale, MessageId::SetupActionUseBundled).to_string(),
3143 ),
3144 ActionHint::new(
3145 "D",
3146 tr(self.locale, MessageId::SetupActionDefer).to_string(),
3147 ),
3148 ActionHint::new(
3149 "Esc",
3150 tr(self.locale, MessageId::SetupActionCancel).to_string(),
3151 ),
3152 ]);
3153 let content_area = render_modal_footer(inner, buf, &hints);
3154 let spec = self.selected_spec();
3155 let mut lines = vec![
3156 Line::from(Span::styled(
3157 tr(self.locale, spec.title_id()).to_string(),
3158 Style::default()
3159 .fg(palette::WHALE_INFO)
3160 .add_modifier(Modifier::BOLD),
3161 )),
3162 Line::from(""),
3163 Line::from(Span::raw(tr(self.locale, spec.why_id()).to_string())),
3164 Line::from(""),
3165 ];
3166 lines.extend(self.selected_step_detail_lines());
3167 lines.push(Line::from(""));
3168 lines.push(Line::from(Span::styled(
3169 tr(self.locale, MessageId::SetupWizardWhy).to_string(),
3170 Style::default().fg(palette::TEXT_MUTED),
3171 )));
3172 lines.push(Line::from(""));
3173 for (idx, step) in STEP_SPECS.iter().enumerate() {
3174 let selected = idx == self.selected;
3175 let status = self.state.status(step.id());
3176 let status_color = match status {
3177 StepStatus::InProgress => palette::WHALE_LIVE,
3178 StepStatus::NeedsAction => palette::WHALE_HUMAN,
3179 StepStatus::Verified => palette::STATUS_SUCCESS,
3180 StepStatus::Failed => palette::STATUS_ERROR,
3181 StepStatus::NotStarted
3182 | StepStatus::Recommended
3183 | StepStatus::Optional
3184 | StepStatus::Deferred
3185 | StepStatus::Skipped => palette::TEXT_MUTED,
3186 };
3187 let marker = crate::tui::glyphs::selection_marker(selected);
3188 let style = if selected {
3189 Style::default()
3190 .fg(palette::TEXT_PRIMARY)
3191 .add_modifier(Modifier::BOLD)
3192 } else {
3193 Style::default().fg(palette::TEXT_MUTED)
3194 };
3195 lines.push(Line::from(vec![
3196 Span::styled(format!("{marker} "), style),
3197 Span::styled(tr(self.locale, step.title_id()).to_string(), style),
3198 Span::raw(" "),
3199 Span::styled(
3200 self.status_label(status).to_string(),
3201 Style::default().fg(status_color),
3202 ),
3203 ]));
3204 }
3205 lines.push(Line::from(""));
3206 lines.push(Line::from(Span::raw(
3207 tr(self.locale, MessageId::SetupCheckpointLayerOrder).to_string(),
3208 )));
3209 let wrap_width = usize::from(content_area.width).max(1);
3210 let visual_rows: usize = lines
3211 .iter()
3212 .map(|line| line.width().div_ceil(wrap_width).max(1))
3213 .sum();
3214 let visible_rows = usize::from(content_area.height).max(1);
3215 let max_scroll = visual_rows.saturating_sub(visible_rows);
3216 let scroll = self.body_scroll.min(max_scroll);
3217 let content_area =
3218 render_panel_scroll_rail(content_area, buf, visual_rows, scroll, visible_rows, true);
3219 Paragraph::new(lines)
3220 .wrap(Wrap { trim: false })
3221 .scroll((scroll as u16, 0))
3222 .render(content_area, buf);
3223 }
3224
3225 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
3226 self
3227 }
3228 }
3229
3230 impl SetupWizardView {
3231 fn selected_step_detail_lines(&self) -> Vec<Line<'static>> {
3232 match self.selected_step() {
3233 SetupStep::ProviderModel => self.provider_model_detail_lines(),
3234 SetupStep::TrustSandbox => self.runtime_posture_detail_lines(),
3235 SetupStep::Constitution => self.constitution_detail_lines(),
3236 SetupStep::OperateFleet => self.operate_fleet_detail_lines(),
3237 SetupStep::Hotbar => self.hotbar_detail_lines(),
3238 SetupStep::ToolsMcp => self.tools_mcp_detail_lines(),
3239 SetupStep::RemoteRuntime => self.remote_runtime_detail_lines(),
3240 SetupStep::Persistence => self.persistence_detail_lines(),
3241 SetupStep::Verification => self.verification_detail_lines(),
3242 _ => Vec::new(),
3243 }
3244 }
3245
3246 fn provider_model_detail_lines(&self) -> Vec<Line<'static>> {
3247 vec![
3248 self.detail_row(MessageId::SetupCardRouteLabel, &self.facts.provider),
3249 self.detail_row(MessageId::SetupCardModelLabel, &self.facts.model),
3250 self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth),
3251 self.detail_row(MessageId::SetupCardHealthLabel, &self.facts.health),
3252 Line::from(Span::styled(
3253 tr(
3254 self.locale,
3255 if self.facts.provider_ready {
3256 MessageId::SetupProviderModelReadyHint
3257 } else {
3258 MessageId::SetupProviderModelNeedsActionHint
3259 },
3260 )
3261 .to_string(),
3262 Style::default().fg(palette::TEXT_MUTED),
3263 )),
3264 ]
3265 }
3266
3267 fn constitution_detail_lines(&self) -> Vec<Line<'static>> {
3268 let choice = constitution_choice_label(self.state.constitution_choice);
3269 let source = constitution_source_label(self.state.constitution_source);
3270 let validity = constitution_validity_label(self.state.constitution_validity);
3271 let source_state = format!("{source}; validity {validity}");
3272 let existing_file = self
3273 .facts
3274 .constitution_file
3275 .label(self.state.constitution_choice, self.locale);
3276 let expert_override = self.facts.expert_override.label(self.locale);
3277 let preview = self
3278 .state
3279 .constitution_preview_hash
3280 .as_deref()
3281 .unwrap_or("not accepted yet")
3282 .to_string();
3283 let mut lines = vec![
3284 self.detail_row(MessageId::SetupConstitutionChoiceLabel, choice),
3285 self.detail_row(MessageId::SetupConstitutionSourceLabel, &source_state),
3286 self.detail_row(MessageId::SetupConstitutionPreviewLabel, &preview),
3287 self.detail_row(MessageId::SetupConstitutionExistingLabel, &existing_file),
3288 self.detail_row(
3289 MessageId::SetupConstitutionExpertOverrideLabel,
3290 &expert_override,
3291 ),
3292 Line::from(Span::styled(
3293 tr(self.locale, MessageId::SetupConstitutionGuidedAnswersHint).to_string(),
3294 Style::default().fg(palette::TEXT_MUTED),
3295 )),
3296 self.guided_answer_pair(
3297 (
3298 "1",
3299 MessageId::SetupConstitutionPurposeLabel,
3300 &self.guided_draft.purpose.label(self.locale),
3301 ),
3302 (
3303 "2",
3304 MessageId::SetupConstitutionAutonomyLabel,
3305 autonomy_label(self.guided_draft.autonomy, self.locale),
3306 ),
3307 ),
3308 self.guided_answer_pair(
3309 (
3310 "3",
3311 MessageId::SetupConstitutionEvidenceLabel,
3312 &self.guided_draft.evidence.label(self.locale),
3313 ),
3314 (
3315 "4",
3316 MessageId::SetupConstitutionCommunicationLabel,
3317 self.guided_draft.communication.label(self.locale),
3318 ),
3319 ),
3320 self.guided_answer_single(
3321 "5",
3322 MessageId::SetupConstitutionPrivacyLabel,
3323 self.guided_draft.privacy.label(self.locale),
3324 ),
3325 self.guided_answer_single(
3326 "6",
3327 MessageId::SetupConstitutionPrinciplesLabel,
3328 self.guided_draft.principles.label(self.locale),
3329 ),
3330 freeform_note_line(self.locale, &self.freeform_note, self.editing_freeform_note),
3331 ];
3332 if self.facts.constitution_file == SetupConstitutionFileState::Loaded {
3333 lines.push(Line::from(Span::styled(
3334 keep_existing_invitation_line(self.locale),
3335 Style::default().fg(palette::WHALE_HUMAN),
3336 )));
3337 }
3338 if let Some(label) = self
3339 .model_draft_label
3340 .as_deref()
3341 .filter(|_| self.model_draft.is_some())
3342 {
3343 lines.push(Line::from(Span::styled(
3344 model_draft_ready_line(self.locale, label),
3345 Style::default().fg(palette::STATUS_SUCCESS),
3346 )));
3347 } else if self.facts.provider_ready {
3348 lines.push(Line::from(Span::styled(
3349 model_draft_invitation_line(self.locale, &self.facts.model),
3350 Style::default().fg(palette::WHALE_HUMAN),
3351 )));
3352 }
3353 lines.push(Line::from(Span::styled(
3354 tr(self.locale, MessageId::SetupConstitutionGuidedHint).to_string(),
3355 Style::default().fg(palette::TEXT_MUTED),
3356 )));
3357 lines
3358 }
3359
3360 fn runtime_posture_detail_lines(&self) -> Vec<Line<'static>> {
3361 let project_override = self
3362 .facts
3363 .project_override_warning
3364 .clone()
3365 .unwrap_or_else(|| {
3366 tr(self.locale, MessageId::SetupRuntimeProjectOverrideNone).to_string()
3367 });
3368 let mut lines = vec![
3369 self.detail_row(MessageId::SetupCardIntentLabel, &self.facts.work_intent),
3370 self.detail_row(MessageId::SetupCardApprovalLabel, &self.facts.approval),
3371 self.detail_row(MessageId::SetupCardShellLabel, &self.facts.shell),
3372 self.detail_row(MessageId::SetupCardTrustLabel, &self.facts.trust),
3373 self.detail_row(MessageId::SetupCardSandboxLabel, &self.facts.sandbox),
3374 self.detail_row(MessageId::SetupCardNetworkLabel, &self.facts.network),
3375 self.detail_row(
3376 MessageId::SetupRuntimePresetSelectedLabel,
3377 &runtime_preset_summary(self.locale, self.runtime_preset),
3378 ),
3379 self.detail_row(
3380 MessageId::SetupRuntimePresetDiffLabel,
3381 &runtime_preset_inline_diff(self.runtime_preset, &self.facts),
3382 ),
3383 self.detail_row(
3384 MessageId::SetupRuntimeProjectOverrideLabel,
3385 &project_override,
3386 ),
3387 Line::from(Span::styled(
3388 tr(self.locale, MessageId::SetupRuntimePostureBoundary).to_string(),
3389 Style::default().fg(palette::TEXT_MUTED),
3390 )),
3391 Line::from(Span::styled(
3392 tr(self.locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(),
3393 Style::default().fg(palette::TEXT_MUTED),
3394 )),
3395 self.setup_review_hint_line(
3396 MessageId::SetupRuntimePostureReviewHint,
3397 Some("Press M for work mode or C for config."),
3398 ),
3399 Line::from(Span::styled(
3400 tr(self.locale, MessageId::SetupRuntimePresetApplyHint).to_string(),
3401 Style::default().fg(palette::TEXT_MUTED),
3402 )),
3403 ];
3404 for (idx, preset) in SetupRuntimePreset::ALL.iter().enumerate() {
3405 let marker = if *preset == self.runtime_preset {
3406 ">"
3407 } else {
3408 " "
3409 };
3410 lines.push(Line::from(Span::styled(
3411 format!(
3412 "{marker} {}. {}",
3413 idx + 1,
3414 runtime_preset_summary(self.locale, *preset)
3415 ),
3416 Style::default().fg(if *preset == self.runtime_preset {
3417 palette::TEXT_PRIMARY
3418 } else {
3419 palette::TEXT_MUTED
3420 }),
3421 )));
3422 }
3423 lines
3424 }
3425
3426 fn operate_fleet_detail_lines(&self) -> Vec<Line<'static>> {
3427 let route = format!("{} / {}", self.facts.provider, self.facts.model);
3428 let readiness = self.ready_label(self.operate_fleet_facts_ready());
3429 vec![
3430 self.detail_row(MessageId::SetupCardRouteLabel, &route),
3431 self.detail_row(MessageId::SetupCardAuthLabel, &self.facts.auth),
3432 self.detail_row(
3433 MessageId::SetupOperateRuntimeLabel,
3434 &self.facts.operate_runtime_result,
3435 ),
3436 self.detail_row(
3437 MessageId::SetupOperateRosterLabel,
3438 &self.facts.fleet_roster_result,
3439 ),
3440 self.detail_row(
3441 MessageId::SetupOperateConcurrencyLabel,
3442 &self.facts.operate_concurrency_result,
3443 ),
3444 self.detail_row(MessageId::SetupOperateReadinessLabel, &readiness),
3445 self.setup_review_hint_line(MessageId::SetupOperateReviewHint, None),
3446 ]
3447 }
3448
3449 fn hotbar_detail_lines(&self) -> Vec<Line<'static>> {
3450 vec![
3451 self.detail_row(
3452 MessageId::SetupHotbarBindingsLabel,
3453 &self.facts.hotbar_bindings_result,
3454 ),
3455 self.detail_row(
3456 MessageId::SetupHotbarActionsLabel,
3457 &self.facts.hotbar_actions_result,
3458 ),
3459 self.setup_review_hint_line(
3460 MessageId::SetupHotbarReviewHint,
3461 Some("Press H to customize slots."),
3462 ),
3463 ]
3464 }
3465
3466 fn tools_mcp_detail_lines(&self) -> Vec<Line<'static>> {
3467 vec![
3468 self.detail_row(
3469 MessageId::SetupToolsMcpServersLabel,
3470 &self.facts.tools_mcp_servers_result,
3471 ),
3472 self.detail_row(
3473 MessageId::SetupToolsMcpSkillsLabel,
3474 &self.facts.tools_mcp_skills_result,
3475 ),
3476 self.detail_row(
3477 MessageId::SetupToolsMcpToolsLabel,
3478 &self.facts.tools_mcp_tools_result,
3479 ),
3480 self.detail_row(
3481 MessageId::SetupToolsMcpPluginsLabel,
3482 &self.facts.tools_mcp_plugins_result,
3483 ),
3484 self.detail_row(
3485 MessageId::SetupToolsMcpHotbarLabel,
3486 &self.facts.tools_mcp_hotbar_result,
3487 ),
3488 self.setup_review_hint_line(
3489 MessageId::SetupToolsMcpReviewHint,
3490 Some("Press R for safe on-ramps (no auto-run)."),
3491 ),
3492 ]
3493 }
3494
3495 /// #3409: one row per mode, each carrying its own observed status. The
3496 /// registry counts stay available in the preview; the card itself answers
3497 /// "where can this be reached from?" in four plain lines.
3498 fn remote_runtime_detail_lines(&self) -> Vec<Line<'static>> {
3499 let mut lines = Vec::new();
3500 for fact in &self.facts.remote_modes {
3501 lines.push(self.detail_row(
3502 fact.mode.label_id(),
3503 &format!(
3504 "{} · {}",
3505 tr(self.locale, fact.status.label_id()),
3506 fact.detail
3507 ),
3508 ));
3509 }
3510 if lines.is_empty() {
3511 lines.push(self.detail_row(
3512 MessageId::SetupRemoteModeLabel,
3513 &self.facts.remote_mode_result,
3514 ));
3515 }
3516 lines.push(self.detail_row(
3517 MessageId::SetupRemoteProvidersLabel,
3518 &self.facts.remote_providers_result,
3519 ));
3520 lines.push(self.setup_review_hint_line(
3521 MessageId::SetupRemoteReviewHint,
3522 Some("Press R to preview (nothing is written). Enter keeps local-only."),
3523 ));
3524 lines
3525 }
3526
3527 fn persistence_detail_lines(&self) -> Vec<Line<'static>> {
3528 vec![
3529 self.detail_row(
3530 MessageId::SetupPersistenceHomeLabel,
3531 &self.facts.persistence.home_result,
3532 ),
3533 self.detail_row(
3534 MessageId::SetupPersistenceConfigLabel,
3535 &self.facts.persistence.config_result,
3536 ),
3537 self.detail_row(
3538 MessageId::SetupPersistenceStateLabel,
3539 &self.facts.persistence.state_result,
3540 ),
3541 self.detail_row(
3542 MessageId::SetupPersistenceConstitutionLabel,
3543 &self.facts.persistence.constitution_result,
3544 ),
3545 self.detail_row(
3546 MessageId::SetupPersistenceMemoryLabel,
3547 &self.facts.persistence.memory_result,
3548 ),
3549 self.detail_row(
3550 MessageId::SetupPersistenceNotesLabel,
3551 &self.facts.persistence.notes_result,
3552 ),
3553 self.setup_review_hint_line(MessageId::SetupPersistenceReviewHint, None),
3554 ]
3555 }
3556
3557 fn verification_detail_lines(&self) -> Vec<Line<'static>> {
3558 let mut lines = vec![
3559 self.detail_row(
3560 MessageId::SetupReportFirstRunLabel,
3561 &self.ready_label(self.state.first_run_ready()),
3562 ),
3563 self.detail_row(
3564 MessageId::SetupReportUpdateLabel,
3565 &self.ready_label(self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION)),
3566 ),
3567 self.detail_row(
3568 MessageId::SetupReportOperateLabel,
3569 &self.ready_label(self.state.operate_ready()),
3570 ),
3571 self.detail_row(
3572 MessageId::SetupReportSourceLabel,
3573 &self.state_source_label(),
3574 ),
3575 self.detail_row(
3576 MessageId::SetupReportAutonomyLabel,
3577 &self.facts.constitution_autonomy,
3578 ),
3579 self.detail_row(
3580 MessageId::SetupReportRuntimePostureLabel,
3581 &self.facts.runtime_result,
3582 ),
3583 Line::from(""),
3584 Line::from(Span::styled(
3585 tr(self.locale, MessageId::SetupReportRowsLabel).to_string(),
3586 Style::default()
3587 .fg(palette::TEXT_MUTED)
3588 .add_modifier(Modifier::BOLD),
3589 )),
3590 ];
3591
3592 for spec in STEP_SPECS {
3593 let step = spec.id();
3594 let entry = self.state.steps.get(&step);
3595 let required = entry.map_or(spec.required(), |entry| entry.required);
3596 let required_label = if required {
3597 tr(self.locale, MessageId::SetupReportRequired)
3598 } else {
3599 tr(self.locale, MessageId::SetupReportOptional)
3600 };
3601 let mut value = format!(
3602 "{} ({})",
3603 self.status_label(self.state.status(step)),
3604 required_label
3605 );
3606 if let Some(version) = entry.and_then(|entry| entry.version.as_deref()) {
3607 value.push_str(&format!(" · {version}"));
3608 }
3609 if let Some(result) = entry.and_then(|entry| entry.result.as_deref()) {
3610 value.push_str(&format!(" · {result}"));
3611 }
3612 lines.push(self.detail_row(spec.title_id(), &value));
3613 }
3614
3615 lines.push(Line::from(""));
3616 let next_action = tr(self.locale, self.next_action_id()).to_string();
3617 lines.push(self.detail_row(MessageId::SetupReportNextActionLabel, &next_action));
3618 lines
3619 }
3620
3621 fn setup_review_hint_line(
3622 &self,
3623 hint_id: MessageId,
3624 english_action: Option<&'static str>,
3625 ) -> Line<'static> {
3626 let hint = if self.locale == Locale::En {
3627 let mut hint = "Enter records this setup snapshot.".to_string();
3628 if let Some(action) = english_action {
3629 hint.push(' ');
3630 hint.push_str(action);
3631 }
3632 hint
3633 } else {
3634 tr(self.locale, hint_id).to_string()
3635 };
3636 Line::from(Span::styled(hint, Style::default().fg(palette::TEXT_MUTED)))
3637 }
3638
3639 fn ready_label(&self, ready: bool) -> String {
3640 if ready {
3641 tr(self.locale, MessageId::SetupReportReady).to_string()
3642 } else {
3643 tr(self.locale, MessageId::SetupStatusNeedsAction).to_string()
3644 }
3645 }
3646
3647 fn state_source_label(&self) -> String {
3648 if self.state.inherited {
3649 tr(self.locale, MessageId::SetupReportInherited).to_string()
3650 } else {
3651 tr(self.locale, MessageId::SetupReportPersisted).to_string()
3652 }
3653 }
3654
3655 fn next_action_id(&self) -> MessageId {
3656 if !self.state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) {
3657 return MessageId::SetupReportNextActionConstitution;
3658 }
3659 if !matches!(
3660 self.state.status(SetupStep::ProviderModel),
3661 StepStatus::Verified | StepStatus::NeedsAction
3662 ) {
3663 return MessageId::SetupReportNextActionProvider;
3664 }
3665 if !self.state.runtime_posture_source.is_reviewed() {
3666 return MessageId::SetupReportNextActionRuntime;
3667 }
3668 if !self.state.first_run_ready() {
3669 return MessageId::SetupReportNextActionRequired;
3670 }
3671 if !self.state.operate_ready() {
3672 return MessageId::SetupReportNextActionOperate;
3673 }
3674 MessageId::SetupReportNextActionNone
3675 }
3676
3677 fn detail_row(&self, label: MessageId, value: &str) -> Line<'static> {
3678 Line::from(vec![
3679 Span::styled(
3680 format!("{} ", tr(self.locale, label)),
3681 Style::default()
3682 .fg(palette::TEXT_MUTED)
3683 .add_modifier(Modifier::BOLD),
3684 ),
3685 Span::raw(value.to_string()),
3686 ])
3687 }
3688
3689 fn guided_answer_pair(
3690 &self,
3691 left: (&str, MessageId, &str),
3692 right: (&str, MessageId, &str),
3693 ) -> Line<'static> {
3694 let label_style = Style::default()
3695 .fg(palette::TEXT_MUTED)
3696 .add_modifier(Modifier::BOLD);
3697 Line::from(vec![
3698 Span::styled(
3699 format!("{} {} ", left.0, tr(self.locale, left.1)),
3700 label_style,
3701 ),
3702 Span::raw(left.2.to_string()),
3703 Span::styled(" · ", Style::default().fg(palette::TEXT_MUTED)),
3704 Span::styled(
3705 format!("{} {} ", right.0, tr(self.locale, right.1)),
3706 label_style,
3707 ),
3708 Span::raw(right.2.to_string()),
3709 ])
3710 }
3711
3712 fn guided_answer_single(&self, key: &str, label: MessageId, value: &str) -> Line<'static> {
3713 Line::from(vec![
3714 Span::styled(
3715 format!("{key} {} ", tr(self.locale, label)),
3716 Style::default()
3717 .fg(palette::TEXT_MUTED)
3718 .add_modifier(Modifier::BOLD),
3719 ),
3720 Span::raw(value.to_string()),
3721 ])
3722 }
3723 }
3724
3725 fn setup_report_ready(state: &SetupState) -> bool {
3726 state.first_run_ready() || state.update_ready(CONSTITUTION_CHECKPOINT_VERSION)
3727 }
3728
3729 fn runtime_preset_summary(locale: Locale, preset: SetupRuntimePreset) -> String {
3730 format!(
3731 "{} - {}",
3732 tr(locale, preset.title_id()),
3733 tr(locale, preset.description_id())
3734 )
3735 }
3736
3737 fn runtime_preset_inline_diff(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> String {
3738 runtime_preset_diff_rows(preset, facts).join("; ")
3739 }
3740
3741 fn runtime_preset_preview_text(
3742 locale: Locale,
3743 preset: SetupRuntimePreset,
3744 facts: &SetupRuntimeFacts,
3745 ) -> String {
3746 let mut lines = vec![
3747 tr(locale, MessageId::SetupRuntimePresetPreviewTitle).to_string(),
3748 runtime_preset_summary(locale, preset),
3749 String::new(),
3750 tr(locale, MessageId::SetupRuntimePresetDiffLabel).to_string(),
3751 ];
3752 lines.extend(
3753 runtime_preset_diff_rows(preset, facts)
3754 .into_iter()
3755 .map(|row| format!("- {row}")),
3756 );
3757 lines.extend([
3758 String::new(),
3759 tr(locale, MessageId::SetupRuntimePostureBoundary).to_string(),
3760 tr(locale, MessageId::SetupRuntimePresetSafetyFloor).to_string(),
3761 tr(locale, MessageId::SetupRuntimePresetApplyHint).to_string(),
3762 ]);
3763 lines.join("\n")
3764 }
3765
3766 fn runtime_preset_diff_rows(preset: SetupRuntimePreset, facts: &SetupRuntimeFacts) -> Vec<String> {
3767 let approval_target = preset.approval_policy().map_or_else(
3768 || "removed; Full Access comes from settings.permission_posture".to_string(),
3769 ToString::to_string,
3770 );
3771 let mut rows = vec![
3772 format!(
3773 "settings.default_mode: {} -> {}",
3774 facts.default_mode,
3775 preset.display_mode()
3776 ),
3777 format!(
3778 "settings.permission_posture: -> {}",
3779 preset.permission_posture()
3780 ),
3781 format!(
3782 "config.approval_policy: {} -> {}",
3783 facts.approval_policy_value, approval_target
3784 ),
3785 format!(
3786 "config.allow_shell: {} -> {}",
3787 facts.allow_shell_enabled,
3788 preset.allow_shell()
3789 ),
3790 format!(
3791 "config.sandbox_mode: {} -> {}",
3792 facts.sandbox_mode_value,
3793 preset.sandbox_mode()
3794 ),
3795 format!(
3796 "config.network.default: {} -> unchanged",
3797 facts.network_default_value
3798 ),
3799 format!("workspace trust: {} -> unchanged", facts.trust),
3800 ];
3801 if let Some(warning) = facts.project_override_warning.as_deref() {
3802 rows.push(format!("project override warning: {warning}"));
3803 }
3804 rows
3805 }
3806
3807 fn project_runtime_override_warning(workspace: &Path, locale: Locale) -> Option<String> {
3808 let outcome = codewhale_config::load_project_config_outcome(workspace);
3809 // A project config that exists but can't be parsed is not the same as no
3810 // project config: its restrictions are silently not in effect, and the
3811 // workspace falls back to the user's baseline. Say so here rather than
3812 // only in a log line the TUI never shows.
3813 if let Some((path, reason)) = outcome.invalid() {
3814 let path = path.display();
3815 return Some(match locale {
3816 Locale::ZhHans => format!(
3817 "无法解析项目配置 {path}({reason})。此工作区的项目级运行姿态限制未生效,将回退到用户默认值。",
3818 ),
3819 _ => format!(
3820 "Project config {path} could not be parsed ({reason}). Its runtime posture restrictions are NOT in effect; this workspace falls back to your user defaults.",
3821 ),
3822 });
3823 }
3824 let project = outcome.into_config()?;
3825 let mut fields = Vec::new();
3826 if let Some(policy) = project.approval_policy.as_deref() {
3827 fields.push(format!("approval_policy={policy}"));
3828 }
3829 if let Some(mode) = project.sandbox_mode.as_deref() {
3830 fields.push(format!("sandbox_mode={mode}"));
3831 }
3832 if fields.is_empty() {
3833 return None;
3834 }
3835 Some(match locale {
3836 Locale::ZhHans => format!(
3837 "此工作区的项目配置包含 {}。预设会保存用户默认值;项目配置仍可在此工作区收紧运行姿态。",
3838 fields.join(", ")
3839 ),
3840 _ => format!(
3841 "Project config contains {}. Presets save user defaults; project config can still tighten runtime posture in this workspace.",
3842 fields.join(", ")
3843 ),
3844 })
3845 }
3846
3847 fn setup_report_result(state: &SetupState, facts: &SetupRuntimeFacts) -> String {
3848 format!(
3849 "first_run={}, update={}, operate={}, constitution={:?}, autonomy={}, posture={:?}, runtime={}, operate_fleet={}",
3850 if state.first_run_ready() {
3851 "ready"
3852 } else {
3853 "needs_action"
3854 },
3855 if state.update_ready(CONSTITUTION_CHECKPOINT_VERSION) {
3856 "ready"
3857 } else {
3858 "needs_action"
3859 },
3860 if state.operate_ready() {
3861 "ready"
3862 } else {
3863 "needs_action"
3864 },
3865 state.constitution_choice,
3866 facts.constitution_autonomy,
3867 state.runtime_posture_source,
3868 facts.runtime_result,
3869 facts.operate_result
3870 )
3871 }
3872
3873 fn remote_runtime_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String {
3874 remote::on_ramp_text(
3875 locale,
3876 &facts.remote_clouds_result,
3877 &facts.remote_bridges_result,
3878 &facts.remote_providers_result,
3879 &facts.remote_mode_result,
3880 &facts.remote_command_provider,
3881 )
3882 }
3883
3884 fn tools_mcp_on_ramp_text(locale: Locale, facts: &SetupRuntimeFacts) -> String {
3885 let tools_facts = tools_mcp::SetupToolsMcpFacts {
3886 servers_result: facts.tools_mcp_servers_result.clone(),
3887 skills_result: facts.tools_mcp_skills_result.clone(),
3888 tools_result: facts.tools_mcp_tools_result.clone(),
3889 plugins_result: facts.tools_mcp_plugins_result.clone(),
3890 hotbar_result: facts.tools_mcp_hotbar_result.clone(),
3891 result: facts.tools_mcp_result.clone(),
3892 overall_status: if facts.tools_mcp_needs_action {
3893 tools_mcp::InventoryStatus::NeedsConfig
3894 } else if facts.tools_mcp_result.contains("overall=off") {
3895 tools_mcp::InventoryStatus::Off
3896 } else {
3897 tools_mcp::InventoryStatus::Healthy
3898 },
3899 needs_action: facts.tools_mcp_needs_action,
3900 mcp_path_display: facts.tools_mcp_path_display.clone(),
3901 skills_path_display: facts.tools_mcp_skills_path_display.clone(),
3902 plugins_path_display: facts.tools_mcp_plugins_path_display.clone(),
3903 };
3904 tools_mcp::on_ramp_text(locale, &tools_facts)
3905 }
3906
3907 #[cfg(test)]
3908 #[must_use]
3909 fn guided_constitution_template(locale: Locale) -> UserConstitution {
3910 GuidedConstitutionDraft::default().to_constitution(locale)
3911 }
3912
3913 /// Who authored the draft being previewed for ratification.
3914 #[derive(Debug, Clone, PartialEq, Eq)]
3915 enum DraftProvenance {
3916 /// Rendered deterministically from the guided answers.
3917 Guided,
3918 /// Drafted by the named model, then sanitized and bounded by Codewhale.
3919 Model(String),
3920 /// The user's existing `constitution.json`, shown unchanged for the
3921 /// keep-existing checkpoint completion (#3794).
3922 Existing,
3923 }
3924
3925 fn ratification_preview_title(locale: Locale) -> &'static str {
3926 match locale {
3927 Locale::Ja => "ユーザー憲法 - 批准前の草案",
3928 Locale::ZhHans => "用户宪章 — 确认前草案",
3929 Locale::ZhHant => "使用者憲法 - 批准前草案",
3930 Locale::PtBr => "Constituição do Usuário - Rascunho para Ratificação",
3931 Locale::Es419 => "Constitución del Usuario - Borrador para Ratificación",
3932 Locale::Vi => "Hiến pháp Người dùng - Bản nháp để phê chuẩn",
3933 Locale::Ko => "사용자 헌법 - 승인 전 초안",
3934 Locale::Ca => "Constitució de l'Usuari - Esborrany per a Ratificació",
3935 Locale::De => "Nutzerverfassung - Entwurf zur Ratifizierung",
3936 Locale::Fr => "Constitution de l'Utilisateur - Brouillon pour Ratification",
3937 Locale::Id => "Konstitusi Pengguna - Draf untuk Ratifikasi",
3938 Locale::Hi => "उपयोगकर्ता संविधान - अंगीकार हेतु मसौदा",
3939 Locale::Ru => "Конституция пользователя - Проект для ратификации",
3940 Locale::Uk => "Конституція користувача - Проєкт для ратифікації",
3941 _ => "User Constitution — Draft for Ratification",
3942 }
3943 }
3944
3945 /// The ratification artifact shown in the pager: provenance, what a
3946 /// constitution is, the exact block that will be injected (byte-identical to
3947 /// prompt assembly's rendering), its authority boundaries, and how to ratify
3948 /// or amend. Only the scaffold differs between guided and model drafts — the
3949 /// law itself always comes from the same renderer.
3950 fn constitution_ratification_text(
3951 locale: Locale,
3952 constitution: &UserConstitution,
3953 provenance: &DraftProvenance,
3954 ) -> String {
3955 const RULE: &str = "──────────────────────────────────────────────────────";
3956 let rendered = constitution
3957 .render_block(None)
3958 .unwrap_or_else(|| match locale {
3959 Locale::Ja => "構造化された憲法は空です。".to_string(),
3960 Locale::ZhHans => "结构化宪章为空。".to_string(),
3961 Locale::ZhHant => "結構化憲法為空。".to_string(),
3962 Locale::PtBr => "A constituição estruturada está vazia.".to_string(),
3963 Locale::Es419 => "La constitución estructurada está vacía.".to_string(),
3964 Locale::Vi => "Hiến pháp có cấu trúc đang trống.".to_string(),
3965 Locale::Ko => "구조화된 헌법이 비어 있습니다.".to_string(),
3966 Locale::Ca => "La constitució estructurada és buida.".to_string(),
3967 Locale::De => "Die strukturierte Verfassung ist leer.".to_string(),
3968 Locale::Fr => "La constitution structurée est vide.".to_string(),
3969 Locale::Id => "Konstitusi terstruktur kosong.".to_string(),
3970 Locale::Hi => "संरचित संविधान खाली है।".to_string(),
3971 Locale::Ru => "Структурированная конституция пуста.".to_string(),
3972 Locale::Uk => "Структурована конституція порожня.".to_string(),
3973 _ => "The structured constitution is empty.".to_string(),
3974 });
3975 let layer_order = tr(locale, MessageId::SetupCheckpointLayerOrder);
3976
3977 match locale {
3978 Locale::Ja => {
3979 let drafted_by = match provenance {
3980 DraftProvenance::Model(label) => format!(
3981 "{label} があなたのガイド回答から起草し、Codewhale が構造検証と境界制限を適用しました。"
3982 ),
3983 DraftProvenance::Guided => {
3984 "あなたのガイド回答から決定的に生成されました。".to_string()
3985 }
3986 DraftProvenance::Existing => {
3987 "既存の憲法を constitution.json から読み込み、変更せずに表示しています。"
3988 .to_string()
3989 }
3990 };
3991 let ratify_how = match provenance {
3992 DraftProvenance::Existing => {
3993 "これはすでに有効な基準です。プレビューを閉じて K を押すと、このまま保持してチェックポイントを完了します。\
3994 ファイルは変更されません。/constitution または /setup でいつでも修正できます。"
3995 }
3996 _ => {
3997 "確認するまで、どの内容も基準にはなりません。プレビューを閉じて G を押すと批准して保存します。\
3998 /constitution または /setup でいつでも修正できます。"
3999 }
4000 };
4001 format!(
4002 "CODEWHALE · ユーザー憲法\n{RULE}\n\n{drafted_by}\n\n\
4003 これは Codewhale があなたと協働するための常設の基準です。優れた憲法のように、使えるほど短く、\
4004 網羅的な規則ではなく持続する原則で構成され、あなたの変化に合わせて修正できます。\
4005 すべての個別判断を裁くのではなく権限と境界を定め、セッションを越えて協働を継続させます。\
4006 ただしこれは記憶ではありません。履歴ではなく原則を保持します。\n\n\
4007 {rendered}\n\n\
4008 権限の階層\n{layer_order}\nあなたの直接の指示は常にこの文書より優先されます。\n\n\
4009 これができないこと\n\
4010 これは行動を導くものです。承認ポリシー、サンドボックス、Shell、ネットワーク、信頼、MCP 権限、\
4011 既定モード、公開、支出の権限を付与または変更することはできません。これらは実行時にあなたが管理します。\n\n\
4012 縮小コアと任意モジュール\n\
4013 組み込みのコアは引き続き有効です。この草案はユーザーグローバルの長期設定だけを保存します。\
4014 重い実行/オーケストレーション教義はモードプロンプトまたは将来の任意モジュールに属します。このプレビューはモジュールを有効化せず、設定も変更しません。\n\n\
4015 批准\n{ratify_how}"
4016 )
4017 }
4018 Locale::ZhHans => {
4019 let drafted_by = match provenance {
4020 DraftProvenance::Model(label) => format!(
4021 "由 {label} 根据你的引导式答案起草,并已由 Codewhale 完成结构校验与边界限制。"
4022 ),
4023 DraftProvenance::Guided => "由你的引导式答案确定性生成。".to_string(),
4024 DraftProvenance::Existing => {
4025 "你现有的宪章,读取自 constitution.json——原样展示,未做任何修改。".to_string()
4026 }
4027 };
4028 let ratify_how = match provenance {
4029 DraftProvenance::Existing => {
4030 "这已是你当前使用的宪章。关闭此预览后按 K 保留并完成检查点——文件不会被修改。\
4031 之后可随时用 /constitution 或 /setup 修改。"
4032 }
4033 _ => {
4034 "未经你确认,任何内容都不会成为宪章。关闭此预览后按 G 确认并保存;\
4035 之后可随时用 /constitution 或 /setup 修改。"
4036 }
4037 };
4038 format!(
4039 "CODEWHALE · 用户宪章\n{RULE}\n\n{drafted_by}\n\n\
4040 这是 Codewhale 与你协作时长期遵循的偏好和规则。内容应保持简短、便于执行,以持久原则为主,并可随时调整。\
4041 它界定协作方式与行为边界,而不是替你决定每一种情况;它让协作跨会话延续——但它不是记忆,只保留原则,不保留历史。\n\n\
4042 {rendered}\n\n\
4043 权限层级\n{layer_order}\n你的直接指令始终高于本文件。\n\n\
4044 它不能做什么\n\
4045 它只提供行为指导,不能授予或更改审批策略、沙箱、Shell、网络、信任、MCP 权限、默认模式、发布或支出权限——这些始终由你在运行时掌控。\n\n\
4046 精简核心与可选策略\n\
4047 内置核心始终生效。本草案只保存你的用户全局长期偏好。执行与编排等高级策略仍由模式提示词或未来的可选规则包管理;此预览不会启用任何策略或更改配置。\n\n\
4048 确认\n{ratify_how}"
4049 )
4050 }
4051 Locale::ZhHant => {
4052 let drafted_by = match provenance {
4053 DraftProvenance::Model(label) => format!(
4054 "由 {label} 根據你的引導式答案起草,並已由 Codewhale 完成結構驗證與邊界限制。"
4055 ),
4056 DraftProvenance::Guided => "由你的引導式答案確定性生成。".to_string(),
4057 DraftProvenance::Existing => {
4058 "你現有的憲法,讀取自 constitution.json;原樣展示,未做任何修改。".to_string()
4059 }
4060 };
4061 let ratify_how = match provenance {
4062 DraftProvenance::Existing => {
4063 "這已是你現行的準則。關閉此預覽後按 K 保留並完成檢查點;\
4064 檔案不會被修改。之後可隨時用 /constitution 或 /setup 修訂。"
4065 }
4066 _ => {
4067 "未經你確認,任何內容都不會成為準則。關閉此預覽後按 G 批准並保存;\
4068 之後可隨時用 /constitution 或 /setup 修訂。"
4069 }
4070 };
4071 format!(
4072 "CODEWHALE · 使用者憲法\n{RULE}\n\n{drafted_by}\n\n\
4073 這是 Codewhale 與你協作的長期準則。像優秀的憲法一樣:足夠簡短因而可用,由持久原則而非詳盡規則構成,並且可以隨你修訂。\
4074 它界定權力與邊界,而非裁決每個具體決定;它讓協作跨會話延續,但它不是記憶,它承載的是原則,而非歷史。\n\n\
4075 {rendered}\n\n\
4076 權限層級\n{layer_order}\n你的直接指令始終高於本文件。\n\n\
4077 它不能做什麼\n\
4078 它只提供行為指導,不能授予或更改審批策略、沙箱、Shell、網路、信任、MCP 權限、預設模式、發布或支出權限;這些始終由你在執行時掌控。\n\n\
4079 精簡核心與可選模組\n\
4080 內建核心始終生效。本草案只保存你的使用者全域長期偏好。執行/編排等重型教義位於模式提示詞或未來的可選模組中;此預覽不會啟用模組或更改其配置。\n\n\
4081 批准\n{ratify_how}"
4082 )
4083 }
4084 Locale::PtBr => {
4085 let drafted_by = match provenance {
4086 DraftProvenance::Model(label) => format!(
4087 "Rascunhado por {label} a partir das suas respostas guiadas, depois validado por schema e limitado pelo Codewhale."
4088 ),
4089 DraftProvenance::Guided => {
4090 "Renderizado deterministicamente a partir das suas respostas guiadas.".to_string()
4091 }
4092 DraftProvenance::Existing => {
4093 "Sua constituição existente, carregada de constitution.json, é exibida sem alterações."
4094 .to_string()
4095 }
4096 };
4097 let ratify_how = match provenance {
4098 DraftProvenance::Existing => {
4099 "Esta já é sua regra vigente. Feche a prévia e pressione K para mantê-la e concluir o checkpoint; \
4100 o arquivo não será modificado. Edite quando quiser com /constitution ou /setup."
4101 }
4102 _ => {
4103 "Nada vira regra até você confirmar. Feche a prévia e pressione G para ratificar e salvar. \
4104 Edite quando quiser com /constitution ou /setup."
4105 }
4106 };
4107 format!(
4108 "CODEWHALE · CONSTITUIÇÃO DO USUÁRIO\n{RULE}\n\n{drafted_by}\n\n\
4109 Esta é a regra permanente de como o Codewhale trabalha com você. Como boas constituições, \
4110 ela é curta o bastante para ser usada, formada por princípios duráveis em vez de regras exaustivas, \
4111 e pode ser emendada conforme você muda. Ela define poderes e limites em vez de decidir cada caso, \
4112 e dá continuidade à colaboração entre sessões. Mas ela não é memória: carrega princípios, não histórico.\n\n\
4113 {rendered}\n\n\
4114 HIERARQUIA DE AUTORIDADE\n{layer_order}\nSeus pedidos diretos sempre superam este documento.\n\n\
4115 O QUE ISTO NÃO PODE FAZER\n\
4116 Isto orienta comportamento. Não pode conceder nem alterar política de aprovação, sandbox, shell, rede, \
4117 confiança, permissões MCP, modo padrão, publicação ou autoridade para gastos; isso continua sob seu controle em tempo de execução.\n\n\
4118 NÚCLEO REDUZIDO E MÓDULOS OPT-IN\n\
4119 O núcleo embutido continua ativo. Este rascunho só salva suas preferências permanentes globais de usuário. \
4120 Doutrina pesada de execução ou orquestração pertence a prompts de modo ou módulos opt-in futuros; esta prévia não ativa módulos nem muda sua configuração.\n\n\
4121 RATIFICAÇÃO\n{ratify_how}"
4122 )
4123 }
4124 Locale::Es419 => {
4125 let drafted_by = match provenance {
4126 DraftProvenance::Model(label) => format!(
4127 "Redactado por {label} desde tus respuestas guiadas, luego validado por schema y acotado por Codewhale."
4128 ),
4129 DraftProvenance::Guided => {
4130 "Renderizado de forma determinística desde tus respuestas guiadas.".to_string()
4131 }
4132 DraftProvenance::Existing => {
4133 "Tu constitución existente, cargada desde constitution.json, se muestra sin cambios."
4134 .to_string()
4135 }
4136 };
4137 let ratify_how = match provenance {
4138 DraftProvenance::Existing => {
4139 "Esta ya es tu regla vigente. Cierra la vista previa y presiona K para conservarla y completar el checkpoint; \
4140 el archivo no se modifica. Puedes enmendarla cuando quieras con /constitution o /setup."
4141 }
4142 _ => {
4143 "Nada se vuelve regla hasta que confirmes. Cierra la vista previa y presiona G para ratificar y guardar. \
4144 Puedes enmendarla cuando quieras con /constitution o /setup."
4145 }
4146 };
4147 format!(
4148 "CODEWHALE · CONSTITUCIÓN DEL USUARIO\n{RULE}\n\n{drafted_by}\n\n\
4149 Esta es la regla permanente de cómo Codewhale trabaja contigo. Como las buenas constituciones, \
4150 es lo bastante breve para usarse, hecha de principios duraderos en vez de reglas exhaustivas, \
4151 y enmendable a medida que cambias. Define poderes y límites en vez de decidir cada caso, \
4152 y da continuidad a la colaboración entre sesiones. Pero no es memoria: lleva principios, no historial.\n\n\
4153 {rendered}\n\n\
4154 JERARQUÍA DE AUTORIDAD\n{layer_order}\nTus pedidos directos siempre superan este documento.\n\n\
4155 LO QUE ESTO NO PUEDE HACER\n\
4156 Orienta comportamiento. No puede conceder ni cambiar política de aprobación, sandbox, shell, red, \
4157 confianza, permisos MCP, modo predeterminado, publicación o autoridad de gasto; eso sigue bajo tu control en tiempo de ejecución.\n\n\
4158 NÚCLEO REDUCIDO Y MÓDULOS OPT-IN\n\
4159 El núcleo integrado sigue activo. Este borrador solo guarda tus preferencias permanentes globales de usuario. \
4160 La doctrina pesada de ejecución u orquestación pertenece a prompts de modo o módulos opt-in futuros; esta vista previa no activa módulos ni cambia su configuración.\n\n\
4161 RATIFICACIÓN\n{ratify_how}"
4162 )
4163 }
4164 Locale::Vi => {
4165 let drafted_by = match provenance {
4166 DraftProvenance::Model(label) => format!(
4167 "Được {label} soạn từ câu trả lời hướng dẫn của bạn, rồi được Codewhale kiểm tra schema và giới hạn biên."
4168 ),
4169 DraftProvenance::Guided => {
4170 "Được kết xuất xác định từ câu trả lời hướng dẫn của bạn.".to_string()
4171 }
4172 DraftProvenance::Existing => {
4173 "Hiến pháp hiện có của bạn, tải từ constitution.json, được hiển thị nguyên trạng."
4174 .to_string()
4175 }
4176 };
4177 let ratify_how = match provenance {
4178 DraftProvenance::Existing => {
4179 "Đây đã là luật hiện hành của bạn. Đóng bản xem trước rồi nhấn K để giữ nguyên và hoàn tất checkpoint; \
4180 tệp không bị sửa. Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup."
4181 }
4182 _ => {
4183 "Không có gì trở thành luật cho đến khi bạn xác nhận. Đóng bản xem trước rồi nhấn G để phê chuẩn và lưu. \
4184 Có thể chỉnh bất cứ lúc nào bằng /constitution hoặc /setup."
4185 }
4186 };
4187 format!(
4188 "CODEWHALE · HIẾN PHÁP NGƯỜI DÙNG\n{RULE}\n\n{drafted_by}\n\n\
4189 Đây là luật thường trực cho cách Codewhale làm việc với bạn. Giống các hiến pháp tốt, \
4190 nó đủ ngắn để dùng, gồm các nguyên tắc bền vững thay vì luật lệ cạn kiệt, \
4191 và có thể sửa khi bạn thay đổi. Nó định khung quyền hạn và giới hạn thay vì quyết định từng trường hợp, \
4192 đồng thời giữ sự liên tục giữa các phiên. Nhưng nó không phải bộ nhớ: nó mang nguyên tắc, không mang lịch sử.\n\n\
4193 {rendered}\n\n\
4194 THỨ BẬC THẨM QUYỀN\n{layer_order}\nYêu cầu trực tiếp của bạn luôn cao hơn tài liệu này.\n\n\
4195 ĐIỀU NÀY KHÔNG THỂ LÀM\n\
4196 Nó hướng dẫn hành vi. Nó không thể cấp hoặc đổi chính sách phê duyệt, sandbox, shell, mạng, \
4197 độ tin cậy, quyền MCP, chế độ mặc định, xuất bản hoặc quyền chi tiêu; những thứ đó vẫn do bạn kiểm soát lúc chạy.\n\n\
4198 LÕI RÚT GỌN VÀ MÔ-ĐUN OPT-IN\n\
4199 Lõi tích hợp vẫn hoạt động. Bản nháp này chỉ lưu tùy chọn thường trực toàn cục của người dùng. \
4200 Giáo điều thực thi hoặc điều phối nặng thuộc về prompt chế độ hoặc mô-đun opt-in trong tương lai; bản xem trước này không bật mô-đun hoặc đổi cấu hình của chúng.\n\n\
4201 PHÊ CHUẨN\n{ratify_how}"
4202 )
4203 }
4204 Locale::Ko => {
4205 let drafted_by = match provenance {
4206 DraftProvenance::Model(label) => format!(
4207 "{label}이(가) 당신의 가이드 답변을 바탕으로 초안을 작성했고, Codewhale이 구조를 검증하고 범위를 제한했습니다."
4208 ),
4209 DraftProvenance::Guided => {
4210 "당신의 가이드 답변으로부터 결정적으로 생성되었습니다.".to_string()
4211 }
4212 DraftProvenance::Existing => {
4213 "constitution.json에서 불러온 기존 헌법이며, 변경 없이 그대로 표시됩니다."
4214 .to_string()
4215 }
4216 };
4217 let ratify_how = match provenance {
4218 DraftProvenance::Existing => {
4219 "이것은 이미 당신의 상시 규칙입니다. 미리보기를 닫고 K를 눌러 그대로 유지하며 체크포인트를 완료하세요; \
4220 파일은 수정되지 않습니다. /constitution 또는 /setup으로 언제든지 수정할 수 있습니다."
4221 }
4222 _ => {
4223 "확인하기 전까지는 아무것도 규칙이 되지 않습니다. 미리보기를 닫고 G를 눌러 승인하고 저장하세요. \
4224 /constitution 또는 /setup으로 언제든지 수정할 수 있습니다."
4225 }
4226 };
4227 format!(
4228 "CODEWHALE · 사용자 헌법\n{RULE}\n\n{drafted_by}\n\n\
4229 이것은 Codewhale이 당신과 함께 일하는 방식에 대한 상시 규칙입니다. 훌륭한 헌법이 그렇듯, \
4230 사용할 수 있을 만큼 짧고, 소모적인 규칙이 아닌 지속적인 원칙으로 이루어져 있으며, 당신이 변화함에 따라 수정할 수 있습니다. \
4231 이는 모든 개별 사례를 판단하는 대신 권한과 한계를 규정하며, 세션을 넘어 협업의 연속성을 부여합니다. \
4232 다만 이것은 기억이 아닙니다: 이력이 아니라 원칙을 담습니다.\n\n\
4233 {rendered}\n\n\
4234 권한 계층\n{layer_order}\n당신의 직접적인 요청은 언제나 이 문서보다 우선합니다.\n\n\
4235 이것이 할 수 없는 일\n\
4236 이것은 행동을 안내할 뿐입니다. 승인 정책, 샌드박스, 셸, 네트워크, 신뢰, MCP 권한, 기본 모드, 게시, 지출 권한을 \
4237 부여하거나 바꿀 수 없습니다; 이는 여전히 런타임에서 당신이 직접 관리합니다.\n\n\
4238 축소된 코어와 옵트인 모듈\n\
4239 내장된 코어는 계속 활성 상태입니다. 이 초안은 사용자 전역의 상시 선호만 저장합니다. \
4240 무거운 실행/오케스트레이션 지침은 모드 프롬프트나 향후 옵트인 모듈에 속합니다. 이 미리보기는 모듈을 활성화하지 않으며 그 설정도 바꾸지 않습니다.\n\n\
4241 승인\n{ratify_how}"
4242 )
4243 }
4244 Locale::Ca => {
4245 let drafted_by = match provenance {
4246 DraftProvenance::Model(label) => format!(
4247 "Redactat per {label} a partir de les teves respostes guiades, després validat per esquema i acotat per Codewhale."
4248 ),
4249 DraftProvenance::Guided => {
4250 "Generat determinísticament a partir de les teves respostes guiades.".to_string()
4251 }
4252 DraftProvenance::Existing => {
4253 "La teva constitució existent, carregada de constitution.json, es mostra sense canvis."
4254 .to_string()
4255 }
4256 };
4257 let ratify_how = match provenance {
4258 DraftProvenance::Existing => {
4259 "Aquesta ja és la teva llei vigent. Tanca la previsualització i prem K per conservar-la i completar el punt de control; \
4260 el fitxer no es modifica. Esmena-la en qualsevol moment amb /constitution o /setup."
4261 }
4262 _ => {
4263 "Res no esdevé llei fins que ho confirmis. Tanca la previsualització i prem G per ratificar i desar. \
4264 Esmena-la en qualsevol moment amb /constitution o /setup."
4265 }
4266 };
4267 format!(
4268 "CODEWHALE · CONSTITUCIÓ DE L'USUARI\n{RULE}\n\n{drafted_by}\n\n\
4269 Aquesta és la llei permanent de com Codewhale treballa amb tu. Com les bones constitucions, \
4270 és prou curta per usar-se, feta de principis duradors en lloc de regles exhaustives, \
4271 i esmenable a mesura que canvies. Defineix poders i límits en lloc de decidir cada cas, \
4272 i dona continuïtat a la col·laboració entre sessions — però no és memòria: porta principis, no història.\n\n\
4273 {rendered}\n\n\
4274 JERARQUIA D'AUTORITAT\n{layer_order}\nLes teves peticions directes sempre prevalen sobre aquest document.\n\n\
4275 EL QUE AIXÒ NO POT FER\n\
4276 Orienta el comportament. No pot concedir ni canviar la política d'aprovació, sandbox, shell, xarxa, \
4277 confiança, permisos MCP, mode per defecte, publicació o autoritat de despesa; això queda sota el teu control en temps d'execució.\n\n\
4278 NUCLI REDUÏT I MÒDULS OPT-IN\n\
4279 El nucli inclòs continua actiu. Aquest esborrany només desa les teves preferències permanents globals d'usuari. \
4280 La doctrina pesada d'execució o orquestració pertany als prompts de mode o a futurs mòduls opt-in; aquesta previsualització no activa mòduls ni canvia la seva configuració.\n\n\
4281 RATIFICACIÓ\n{ratify_how}"
4282 )
4283 }
4284 Locale::De => {
4285 let drafted_by = match provenance {
4286 DraftProvenance::Model(label) => format!(
4287 "Entworfen von {label} aus deinen geführten Antworten, dann schema-geprüft und begrenzt durch Codewhale."
4288 ),
4289 DraftProvenance::Guided => {
4290 "Deterministisch aus deinen geführten Antworten erzeugt.".to_string()
4291 }
4292 DraftProvenance::Existing => {
4293 "Deine bestehende Verfassung, geladen aus constitution.json — unverändert gezeigt."
4294 .to_string()
4295 }
4296 };
4297 let ratify_how = match provenance {
4298 DraftProvenance::Existing => {
4299 "Dies ist bereits dein geltendes Recht. Schließe die Vorschau und drücke K, um sie zu behalten und den Checkpoint abzuschließen — \
4300 die Datei wird nicht verändert. Jederzeit mit /constitution oder /setup änderbar."
4301 }
4302 _ => {
4303 "Nichts wird Recht, bevor du bestätigst. Schließe die Vorschau und drücke G, um zu ratifizieren und zu speichern. \
4304 Jederzeit mit /constitution oder /setup änderbar."
4305 }
4306 };
4307 format!(
4308 "CODEWHALE · NUTZERVERFASSUNG\n{RULE}\n\n{drafted_by}\n\n\
4309 Dies ist das geltende Gesetz dafür, wie Codewhale mit dir arbeitet. Wie die besten Verfassungen \
4310 ist sie kurz genug, um genutzt zu werden, besteht aus dauerhaften Prinzipien statt erschöpfender Regeln \
4311 und lässt sich ändern, wenn du dich änderst. Sie rahmt Befugnisse und Grenzen, statt jeden Einzelfall zu entscheiden, \
4312 und gibt deiner Zusammenarbeit Kontinuität über Sitzungen hinweg — aber sie ist kein Gedächtnis: Sie trägt Prinzipien, nicht Geschichte.\n\n\
4313 {rendered}\n\n\
4314 HIERARCHIE DER AUTORITÄT\n{layer_order}\nDeine direkten Anweisungen stehen immer über diesem Dokument.\n\n\
4315 WAS DIES NICHT KANN\n\
4316 Sie leitet Verhalten. Sie kann keine Freigaberichtlinie, Sandbox, Shell, Netzwerk, \
4317 Vertrauen, MCP-Berechtigungen, Standardmodus, Veröffentlichung oder Ausgabenbefugnis gewähren oder ändern — die bleiben zur Laufzeit in deiner Hand.\n\n\
4318 REDUZIERTER KERN UND OPT-IN-MODULE\n\
4319 Der mitgelieferte Kern bleibt aktiv. Dieser Entwurf speichert nur deine benutzer-globalen Dauerpräferenzen. \
4320 Schwere Ausführungs- oder Orchestrierungsdoktrin gehört in Modus-Prompts oder künftige Opt-in-Module; diese Vorschau aktiviert keine Module und ändert nicht ihre Konfiguration.\n\n\
4321 RATIFIZIERUNG\n{ratify_how}"
4322 )
4323 }
4324 Locale::Fr => {
4325 let drafted_by = match provenance {
4326 DraftProvenance::Model(label) => format!(
4327 "Rédigé par {label} à partir de vos réponses guidées, puis validé par schéma et borné par Codewhale."
4328 ),
4329 DraftProvenance::Guided => {
4330 "Généré de façon déterministe à partir de vos réponses guidées.".to_string()
4331 }
4332 DraftProvenance::Existing => {
4333 "Votre constitution existante, chargée depuis constitution.json — affichée sans modification."
4334 .to_string()
4335 }
4336 };
4337 let ratify_how = match provenance {
4338 DraftProvenance::Existing => {
4339 "C'est déjà votre loi permanente. Fermez cet aperçu, puis appuyez sur K pour la conserver et terminer le point de contrôle — \
4340 le fichier n'est pas modifié. Amendez-la à tout moment avec /constitution ou /setup."
4341 }
4342 _ => {
4343 "Rien ne devient loi avant votre confirmation. Fermez cet aperçu, puis appuyez sur G pour ratifier et enregistrer. \
4344 Amendez-la à tout moment avec /constitution ou /setup."
4345 }
4346 };
4347 format!(
4348 "CODEWHALE · CONSTITUTION DE L'UTILISATEUR\n{RULE}\n\n{drafted_by}\n\n\
4349 Voici la loi permanente qui régit la façon dont Codewhale travaille avec vous. Comme les meilleures constitutions, \
4350 elle est assez courte pour être utilisée, faite de principes durables plutôt que de règles exhaustives, \
4351 et amendable à mesure que vous changez. Elle encadre les pouvoirs et les limites plutôt que de trancher chaque cas, \
4352 et donne à votre collaboration une continuité entre les sessions — mais elle n'est pas une mémoire : elle porte des principes, pas un historique.\n\n\
4353 {rendered}\n\n\
4354 HIÉRARCHIE D'AUTORITÉ\n{layer_order}\nVos demandes directes priment toujours sur ce document.\n\n\
4355 CE QU'ELLE NE PEUT PAS FAIRE\n\
4356 Elle guide le comportement. Elle ne peut ni accorder ni modifier la politique d'approbation, le sandbox, le shell, le réseau, \
4357 la confiance, les permissions MCP, le mode par défaut, la publication ou le pouvoir de dépense — ceux-ci restent entre vos mains à l'exécution.\n\n\
4358 NOYAU RÉDUIT ET MODULES OPT-IN\n\
4359 Le noyau intégré reste actif. Ce brouillon n'enregistre que vos préférences permanentes globales. \
4360 La doctrine lourde d'exécution ou d'orchestration appartient aux prompts de mode ou à de futurs modules opt-in ; cet aperçu n'active pas de modules et ne change pas leur configuration.\n\n\
4361 RATIFICATION\n{ratify_how}"
4362 )
4363 }
4364 Locale::Id => {
4365 let drafted_by = match provenance {
4366 DraftProvenance::Model(label) => format!(
4367 "Disusun oleh {label} dari jawaban terpandu Anda, lalu diperiksa skemanya dan dibatasi oleh Codewhale."
4368 ),
4369 DraftProvenance::Guided => {
4370 "Dihasilkan secara deterministik dari jawaban terpandu Anda.".to_string()
4371 }
4372 DraftProvenance::Existing => {
4373 "Konstitusi Anda yang ada, dimuat dari constitution.json — ditampilkan tanpa perubahan."
4374 .to_string()
4375 }
4376 };
4377 let ratify_how = match provenance {
4378 DraftProvenance::Existing => {
4379 "Ini sudah menjadi hukum tetap Anda. Tutup pratinjau ini, lalu tekan K untuk mempertahankannya dan menyelesaikan checkpoint — \
4380 file tidak diubah. Amendemen kapan saja dengan /constitution atau /setup."
4381 }
4382 _ => {
4383 "Tidak ada yang menjadi hukum sampai Anda mengonfirmasi. Tutup pratinjau ini, lalu tekan G untuk meratifikasi dan menyimpan. \
4384 Amendemen kapan saja dengan /constitution atau /setup."
4385 }
4386 };
4387 format!(
4388 "CODEWHALE · KONSTITUSI PENGGUNA\n{RULE}\n\n{drafted_by}\n\n\
4389 Ini adalah hukum tetap tentang cara Codewhale bekerja dengan Anda. Seperti konstitusi terbaik, \
4390 ia cukup singkat untuk dipakai, tersusun dari prinsip yang awet alih-alih aturan yang menyeluruh, \
4391 dan dapat diamendemen seiring Anda berubah. Ia membingkai wewenang dan batasan alih-alih memutuskan setiap kasus, \
4392 dan memberi kolaborasi Anda kesinambungan lintas sesi — tetapi ia bukan memori: ia membawa prinsip, bukan riwayat.\n\n\
4393 {rendered}\n\n\
4394 HIERARKI OTORITAS\n{layer_order}\nPermintaan langsung Anda selalu mengungguli dokumen ini.\n\n\
4395 APA YANG TIDAK BISA DILAKUKANNYA\n\
4396 Ia memandu perilaku. Ia tidak dapat memberi atau mengubah kebijakan persetujuan, sandbox, shell, jaringan, \
4397 kepercayaan, izin MCP, mode default, publikasi, atau wewenang belanja — semua itu tetap di tangan Anda saat runtime.\n\n\
4398 INTI RINGKAS DAN MODUL OPT-IN\n\
4399 Inti bawaan tetap aktif. Draf ini hanya menyimpan preferensi tetap global pengguna Anda. \
4400 Doktrin eksekusi atau orkestrasi yang berat termasuk dalam prompt mode atau modul opt-in mendatang; pratinjau ini tidak mengaktifkan modul atau mengubah konfigurasinya.\n\n\
4401 RATIFIKASI\n{ratify_how}"
4402 )
4403 }
4404 Locale::Hi => {
4405 let drafted_by = match provenance {
4406 DraftProvenance::Model(label) => format!(
4407 "{label} द्वारा आपके गाइडेड उत्तरों से तैयार, फिर Codewhale द्वारा स्कीमा-जाँचा और सीमित किया गया।"
4408 ),
4409 DraftProvenance::Guided => "आपके गाइडेड उत्तरों से नियत रूप से तैयार किया गया।".to_string(),
4410 DraftProvenance::Existing => {
4411 "आपका मौजूदा संविधान, constitution.json से लोड किया गया — अपरिवर्तित दिखाया गया।"
4412 .to_string()
4413 }
4414 };
4415 let ratify_how = match provenance {
4416 DraftProvenance::Existing => {
4417 "यह पहले से ही आपका स्थायी कानून है। यह पूर्वावलोकन बंद करें, फिर इसे बनाए रखने और चेकपॉइंट पूरा करने के लिए K दबाएँ — \
4418 फ़ाइल संशोधित नहीं होती। /constitution या /setup से कभी भी संशोधित करें।"
4419 }
4420 _ => {
4421 "जब तक आप पुष्टि नहीं करते, कुछ भी कानून नहीं बनता। यह पूर्वावलोकन बंद करें, फिर अंगीकार और सहेजने के लिए G दबाएँ। \
4422 /constitution या /setup से कभी भी संशोधित करें।"
4423 }
4424 };
4425 format!(
4426 "CODEWHALE · उपयोगकर्ता संविधान\n{RULE}\n\n{drafted_by}\n\n\
4427 यह Codewhale आपके साथ कैसे काम करे, इसका स्थायी कानून है। सर्वोत्तम संविधानों की तरह, \
4428 यह उपयोग के लिए पर्याप्त छोटा है, संपूर्ण नियमों के बजाय टिकाऊ सिद्धांतों से बना है, \
4429 और आपके बदलने के साथ संशोधनीय है। यह हर मामले का फ़ैसला करने के बजाय शक्तियों और सीमाओं का ढाँचा देता है, \
4430 और आपके सहयोग को सत्रों के पार निरंतरता देता है — लेकिन यह मेमोरी नहीं है: यह इतिहास नहीं, सिद्धांत रखता है।\n\n\
4431 {rendered}\n\n\
4432 अधिकार पदानुक्रम\n{layer_order}\nआपके प्रत्यक्ष अनुरोध हमेशा इस दस्तावेज़ से ऊपर हैं।\n\n\
4433 यह क्या नहीं कर सकता\n\
4434 यह व्यवहार का मार्गदर्शन करता है। यह अनुमति नीति, सैंडबॉक्स, शेल, नेटवर्क, \
4435 ट्रस्ट, MCP अनुमतियाँ, डिफ़ॉल्ट मोड, प्रकाशन या खर्च का अधिकार प्रदान या परिवर्तित नहीं कर सकता — वे रनटाइम पर आपके हाथ में रहते हैं।\n\n\
4436 संक्षिप्त कोर और ऑप्ट-इन मॉड्यूल\n\
4437 Bundled कोर सक्रिय रहता है। यह मसौदा केवल आपकी उपयोगकर्ता-वैश्विक स्थायी प्राथमिकताएँ सहेजता है। \
4438 भारी निष्पादन या ऑर्केस्ट्रेशन सिद्धांत मोड प्रॉम्प्ट या भविष्य के ऑप्ट-इन मॉड्यूल में रहते हैं; यह पूर्वावलोकन मॉड्यूल सक्षम नहीं करता और न ही उनकी कॉन्फ़िगरेशन बदलता है।\n\n\
4439 अंगीकार\n{ratify_how}"
4440 )
4441 }
4442 Locale::Ru => {
4443 let drafted_by = match provenance {
4444 DraftProvenance::Model(label) => format!(
4445 "Подготовлено {label} на основе ваших ответов на наводящие вопросы, затем проверено по схеме и ограничено Codewhale."
4446 ),
4447 DraftProvenance::Guided => {
4448 "Детерминированно построено из ваших ответов на наводящие вопросы.".to_string()
4449 }
4450 DraftProvenance::Existing => {
4451 "Ваша существующая конституция, загруженная из constitution.json, — показана без изменений."
4452 .to_string()
4453 }
4454 };
4455 let ratify_how = match provenance {
4456 DraftProvenance::Existing => {
4457 "Это уже ваш действующий закон. Закройте это превью, затем нажмите K, чтобы сохранить её и завершить контрольную точку — \
4458 файл не изменяется. Изменить можно в любое время через /constitution или /setup."
4459 }
4460 _ => {
4461 "Ничто не становится законом, пока вы не подтвердите. Закройте это превью, затем нажмите G, чтобы ратифицировать и сохранить. \
4462 Изменить можно в любое время через /constitution или /setup."
4463 }
4464 };
4465 format!(
4466 "CODEWHALE · КОНСТИТУЦИЯ ПОЛЬЗОВАТЕЛЯ\n{RULE}\n\n{drafted_by}\n\n\
4467 Это постоянный закон о том, как Codewhale работает с вами. Как лучшие конституции, \
4468 она достаточно коротка, чтобы ей пользоваться, состоит из долговечных принципов, а не исчерпывающих правил, \
4469 и может изменяться вместе с вами. Она очерчивает полномочия и границы, а не решает каждый случай, \
4470 и придаёт вашему сотрудничеству непрерывность между сессиями — но она не память: она хранит принципы, а не историю.\n\n\
4471 {rendered}\n\n\
4472 ИЕРАРХИЯ ПОЛНОМОЧИЙ\n{layer_order}\nВаши прямые указания всегда важнее этого документа.\n\n\
4473 ЧЕГО ОНА НЕ МОЖЕТ\n\
4474 Она направляет поведение. Она не может предоставить или изменить политику одобрения, sandbox, shell, сеть, \
4475 доверие, разрешения MCP, режим по умолчанию, публикацию или право тратить — они остаются в ваших руках во время выполнения.\n\n\
4476 СОКРАЩЁННОЕ ЯДРО И ОПЦИОНАЛЬНЫЕ МОДУЛИ\n\
4477 Встроенное ядро остаётся активным. Этот проект сохраняет только ваши глобальные постоянные предпочтения. \
4478 Тяжёлая доктрина исполнения или оркестрации принадлежит промптам режимов или будущим опциональным модулям; это превью не включает модули и не меняет их конфигурацию.\n\n\
4479 РАТИФИКАЦИЯ\n{ratify_how}"
4480 )
4481 }
4482 Locale::Uk => {
4483 let drafted_by = match provenance {
4484 DraftProvenance::Model(label) => format!(
4485 "Підготовлено {label} на основі ваших відповідей на навідні запитання, потім перевірено за схемою та обмежено Codewhale."
4486 ),
4487 DraftProvenance::Guided => {
4488 "Детерміновано побудовано з ваших відповідей на навідні запитання.".to_string()
4489 }
4490 DraftProvenance::Existing => {
4491 "Ваша чинна конституція, завантажена з constitution.json, — показана без змін."
4492 .to_string()
4493 }
4494 };
4495 let ratify_how = match provenance {
4496 DraftProvenance::Existing => {
4497 "Це вже ваш чинний закон. Закрийте це прев'ю, потім натисніть K, щоб зберегти її та завершити контрольну точку — \
4498 файл не змінюється. Змінити можна будь-коли через /constitution або /setup."
4499 }
4500 _ => {
4501 "Ніщо не стає законом, доки ви не підтвердите. Закрийте це прев'ю, потім натисніть G, щоб ратифікувати та зберегти. \
4502 Змінити можна будь-коли через /constitution або /setup."
4503 }
4504 };
4505 format!(
4506 "CODEWHALE · КОНСТИТУЦІЯ КОРИСТУВАЧА\n{RULE}\n\n{drafted_by}\n\n\
4507 Це постійний закон про те, як Codewhale працює з вами. Як найкращі конституції, \
4508 вона достатньо коротка, щоб нею користуватися, складається з довговічних принципів, а не вичерпних правил, \
4509 і може змінюватися разом із вами. Вона окреслює повноваження та межі, а не вирішує кожен випадок, \
4510 і надає вашій співпраці неперервність між сесіями — але вона не пам'ять: вона зберігає принципи, а не історію.\n\n\
4511 {rendered}\n\n\
4512 ІЄРАРХІЯ ПОВНОВАЖЕНЬ\n{layer_order}\nВаші прямі вказівки завжди важливіші за цей документ.\n\n\
4513 ЧОГО ВОНА НЕ МОЖЕ\n\
4514 Вона спрямовує поведінку. Вона не може надати або змінити політику схвалення, sandbox, shell, мережу, \
4515 довіру, дозволи MCP, режим за замовчуванням, публікацію чи право витрачати — вони залишаються у ваших руках під час виконання.\n\n\
4516 СКОРОЧЕНЕ ЯДРО Й ОПЦІЙНІ МОДУЛІ\n\
4517 Вбудоване ядро залишається активним. Цей проєкт зберігає лише ваші глобальні постійні вподобання. \
4518 Важка доктрина виконання чи оркестрації належить промптам режимів або майбутнім опційним модулям; це прев'ю не вмикає модулі й не змінює їхню конфігурацію.\n\n\
4519 РАТИФІКАЦІЯ\n{ratify_how}"
4520 )
4521 }
4522 _ => {
4523 let drafted_by = match provenance {
4524 DraftProvenance::Model(label) => format!(
4525 "Drafted by {label} from your guided answers, then schema-checked and bounded by Codewhale."
4526 ),
4527 DraftProvenance::Guided => {
4528 "Rendered deterministically from your guided answers.".to_string()
4529 }
4530 DraftProvenance::Existing => {
4531 "Your existing constitution, loaded from constitution.json — shown unchanged."
4532 .to_string()
4533 }
4534 };
4535 let ratify_how = match provenance {
4536 DraftProvenance::Existing => {
4537 "This is already your standing law. Close this preview, then press K to \
4538 keep it and complete the checkpoint — the file is not modified. Amend \
4539 anytime with /constitution or /setup."
4540 }
4541 _ => {
4542 "Nothing becomes law until you confirm. Close this preview, then press G to \
4543 ratify and save. Amend anytime with /constitution or /setup."
4544 }
4545 };
4546 format!(
4547 "CODEWHALE · USER CONSTITUTION\n{RULE}\n\n{drafted_by}\n\n\
4548 This is the standing law for how Codewhale works with you. Like the best \
4549 constitutions, it is short enough to use, made of durable principles rather \
4550 than exhaustive rules, and amendable as you change. It frames powers and \
4551 limits rather than deciding every case, and it gives your collaboration \
4552 continuity across sessions — but it is not memory: it carries principles, \
4553 not history.\n\n\
4554 {rendered}\n\n\
4555 HIERARCHY OF AUTHORITY\n{layer_order}\nYour direct requests always outrank this document.\n\n\
4556 WHAT THIS CANNOT DO\n\
4557 It guides behavior. It cannot grant or change approval policy, sandbox, shell, \
4558 network, trust, MCP permissions, default mode, publishing, or spending \
4559 authority — those stay under your hand at runtime.\n\n\
4560 REDUCED CORE AND OPT-IN MODULES\n\
4561 The bundled core stays active. This draft only saves your user-global \
4562 standing preferences. Heavy execution or orchestration doctrine belongs in mode \
4563 prompts or future opt-in modules; this preview does not enable modules or change \
4564 their configuration.\n\n\
4565 RATIFICATION\n{ratify_how}"
4566 )
4567 }
4568 }
4569 }
4570
4571 /// Card line inviting the user to let their configured model draft the law.
4572 fn model_draft_invitation_line(locale: Locale, model_label: &str) -> String {
4573 match locale {
4574 Locale::Ja => {
4575 format!("A {model_label} が起草し、あなたが批准します。確認するまで保存しません。")
4576 }
4577 Locale::ZhHans => {
4578 format!("A {model_label} 生成草案,由你确认。未经确认不会保存。")
4579 }
4580 Locale::ZhHant => {
4581 format!("A {model_label} 起草,你批准。未經確認不會保存。")
4582 }
4583 Locale::PtBr => {
4584 format!("A {model_label} pode rascunhar. Você ratifica. Nada salva sem você.")
4585 }
4586 Locale::Es419 => {
4587 format!("A {model_label} puede redactarla. Tú ratificas. Nada se guarda sin ti.")
4588 }
4589 Locale::Vi => {
4590 format!("A {model_label} có thể soạn. Bạn phê chuẩn. Không lưu gì nếu chưa có bạn.")
4591 }
4592 Locale::Ko => {
4593 format!(
4594 "A {model_label}이(가) 초안을 작성할 수 있습니다. 승인은 당신이 합니다. 당신 없이는 아무것도 저장되지 않습니다."
4595 )
4596 }
4597 Locale::Ca => {
4598 format!("A {model_label} la pot redactar. Tu la ratifiques. Res no es desa sense tu.")
4599 }
4600 Locale::De => {
4601 format!(
4602 "A {model_label} kann sie entwerfen. Du ratifizierst sie. Ohne dich wird nichts gespeichert."
4603 )
4604 }
4605 Locale::Fr => {
4606 format!(
4607 "A {model_label} peut la rédiger. Vous la ratifiez. Rien ne s'enregistre sans vous."
4608 )
4609 }
4610 Locale::Id => {
4611 format!(
4612 "A {model_label} dapat menyusunnya. Anda yang meratifikasi. Tidak ada yang tersimpan tanpa Anda."
4613 )
4614 }
4615 Locale::Hi => {
4616 format!(
4617 "A {model_label} इसका मसौदा बना सकता है। अंगीकार आप करते हैं। आपके बिना कुछ भी सहेजा नहीं जाता।"
4618 )
4619 }
4620 Locale::Ru => {
4621 format!(
4622 "A {model_label} может подготовить проект. Ратифицируете вы. Без вас ничего не сохраняется."
4623 )
4624 }
4625 Locale::Uk => {
4626 format!(
4627 "A {model_label} може підготувати проєкт. Ратифікуєте ви. Без вас нічого не зберігається."
4628 )
4629 }
4630 _ => format!("A {model_label} can draft it. You ratify it. Nothing saves without you."),
4631 }
4632 }
4633
4634 /// Card line offering to keep an existing valid constitution unchanged.
4635 fn keep_existing_invitation_line(locale: Locale) -> &'static str {
4636 match locale {
4637 Locale::Ja => "K 既存の憲法を保持 - 確認して保持、ファイルは変更しません。",
4638 Locale::ZhHans => "K 保留现有宪章——先查看,再保留,文件不变。",
4639 Locale::ZhHant => "K 保留現有憲法 - 先查看,再保留,檔案不變。",
4640 Locale::PtBr => "K Manter constituição existente - revise, mantenha, arquivo inalterado.",
4641 Locale::Es419 => {
4642 "K Conservar constitución existente - revisa, conserva, archivo sin cambios."
4643 }
4644 Locale::Vi => "K Giữ hiến pháp hiện có - xem lại, giữ nguyên, tệp không đổi.",
4645 Locale::Ko => "K 기존 헌법 유지 - 검토 후 유지, 파일은 변경되지 않음.",
4646 Locale::Ca => {
4647 "K Mantén la constitució existent - revisa-la, conserva-la, fitxer sense canvis."
4648 }
4649 Locale::De => "K Bestehende Verfassung behalten - prüfen, behalten, Datei unverändert.",
4650 Locale::Fr => {
4651 "K Garder votre constitution existante - révisez-la, gardez-la, fichier inchangé."
4652 }
4653 Locale::Id => {
4654 "K Pertahankan konstitusi Anda yang ada - tinjau, pertahankan, file tidak berubah."
4655 }
4656 Locale::Hi => "K अपना मौजूदा संविधान रखें - समीक्षा करें, बनाए रखें, फ़ाइल अपरिवर्तित।",
4657 Locale::Ru => {
4658 "K Сохранить существующую конституцию - просмотрите, сохраните, файл не изменяется."
4659 }
4660 Locale::Uk => "K Зберегти чинну конституцію - перегляньте, збережіть, файл без змін.",
4661 _ => "K Keep your existing constitution — review it, keep it, file unchanged.",
4662 }
4663 }
4664
4665 /// Card line shown while a model draft awaits ratification.
4666 fn model_draft_ready_line(locale: Locale, model_label: &str) -> String {
4667 match locale {
4668 Locale::Ja => {
4669 format!(
4670 "{model_label} の草案が批准待ちです - G で確認して批准、1-6 で草案を破棄します。"
4671 )
4672 }
4673 Locale::ZhHans => {
4674 format!("{model_label} 的草案待确认——按 G 查看并确认;按 1-6 会丢弃草案。")
4675 }
4676 Locale::ZhHant => {
4677 format!("{model_label} 的草案待批准 - 按 G 查看並批准;按 1-6 會丟棄草案。")
4678 }
4679 Locale::PtBr => {
4680 format!(
4681 "Rascunho de {model_label} aguarda ratificação - G para revisar e ratificar; 1-6 descarta."
4682 )
4683 }
4684 Locale::Es419 => {
4685 format!(
4686 "El borrador de {model_label} espera ratificación - G para revisar y ratificar; 1-6 lo descarta."
4687 )
4688 }
4689 Locale::Vi => {
4690 format!(
4691 "Bản nháp của {model_label} chờ phê chuẩn - G để xem và phê chuẩn; 1-6 sẽ bỏ bản nháp."
4692 )
4693 }
4694 Locale::Ko => {
4695 format!(
4696 "{model_label}의 초안이 승인을 기다리고 있습니다 - G로 확인하고 승인, 1-6은 초안을 버립니다."
4697 )
4698 }
4699 Locale::Ca => {
4700 format!(
4701 "L'esborrany de {model_label} espera ratificació - G per revisar i ratificar; 1-6 el descarta."
4702 )
4703 }
4704 Locale::De => {
4705 format!(
4706 "Entwurf von {model_label} wartet auf Ratifizierung - G zum Prüfen und Ratifizieren; 1-6 verwirft ihn."
4707 )
4708 }
4709 Locale::Fr => {
4710 format!(
4711 "Le brouillon de {model_label} attend ratification - G pour réviser et ratifier ; 1-6 l'écarte."
4712 )
4713 }
4714 Locale::Id => {
4715 format!(
4716 "Draf oleh {model_label} menunggu ratifikasi - G untuk meninjau dan meratifikasi; 1-6 membuangnya."
4717 )
4718 }
4719 Locale::Hi => {
4720 format!(
4721 "{model_label} का मसौदा अंगीकार की प्रतीक्षा में है - समीक्षा और अंगीकार के लिए G; 1-6 उसे खारिज करता है।"
4722 )
4723 }
4724 Locale::Ru => {
4725 format!(
4726 "Проект от {model_label} ожидает ратификации - G для просмотра и ратификации; 1-6 отклоняет его."
4727 )
4728 }
4729 Locale::Uk => {
4730 format!(
4731 "Проєкт від {model_label} очікує ратифікації - G для перегляду та ратифікації; 1-6 відхиляє його."
4732 )
4733 }
4734 _ => format!(
4735 "Draft by {model_label} awaits ratification — G to review and ratify; 1-6 discards it."
4736 ),
4737 }
4738 }
4739
4740 /// Host-facing status line after a successful model draft.
4741 pub(crate) fn model_draft_ready_message(locale: Locale, model_label: &str) -> String {
4742 match locale {
4743 Locale::Ja => format!(
4744 "{model_label} があなたの憲法を起草しました。プレビューを確認してから G で批准してください。"
4745 ),
4746 Locale::ZhHans => {
4747 format!("{model_label} 已生成你的宪章草案。请查看预览,然后按 G 确认。")
4748 }
4749 Locale::ZhHant => format!("{model_label} 已起草你的憲法。請查看預覽,然後按 G 批准。"),
4750 Locale::PtBr => format!(
4751 "{model_label} rascunhou sua constituição. Revise a prévia e pressione G para ratificar."
4752 ),
4753 Locale::Es419 => format!(
4754 "{model_label} redactó tu constitución. Revisa la vista previa y presiona G para ratificar."
4755 ),
4756 Locale::Vi => format!(
4757 "{model_label} đã soạn hiến pháp của bạn. Xem bản xem trước rồi nhấn G để phê chuẩn."
4758 ),
4759 Locale::Ko => format!(
4760 "{model_label}이(가) 당신의 헌법 초안을 작성했습니다. 미리보기를 확인한 뒤 G를 눌러 승인하세요."
4761 ),
4762 Locale::Ca => format!(
4763 "{model_label} ha redactat la teva constitució. Revisa la previsualització i prem G per ratificar."
4764 ),
4765 Locale::De => format!(
4766 "{model_label} hat deine Verfassung entworfen. Prüfe die Vorschau und drücke G zum Ratifizieren."
4767 ),
4768 Locale::Fr => format!(
4769 "{model_label} a rédigé votre constitution. Révisez l'aperçu, puis appuyez sur G pour ratifier."
4770 ),
4771 Locale::Id => format!(
4772 "{model_label} menyusun konstitusi Anda. Tinjau pratinjaunya, lalu tekan G untuk meratifikasi."
4773 ),
4774 Locale::Hi => format!(
4775 "{model_label} ने आपके संविधान का मसौदा तैयार किया। पूर्वावलोकन देखें, फिर अंगीकार के लिए G दबाएँ।"
4776 ),
4777 Locale::Ru => format!(
4778 "{model_label} подготовил проект вашей конституции. Просмотрите превью, затем нажмите G для ратификации."
4779 ),
4780 Locale::Uk => format!(
4781 "{model_label} підготував проєкт вашої конституції. Перегляньте прев'ю, потім натисніть G для ратифікації."
4782 ),
4783 _ => format!(
4784 "{model_label} drafted your constitution. Review the preview, then press G to ratify."
4785 ),
4786 }
4787 }
4788
4789 /// Host-facing status line when model drafting fails or is unavailable. The
4790 /// guided deterministic draft always remains the standing fallback.
4791 pub(crate) fn model_draft_failed_message(
4792 locale: Locale,
4793 model_label: &str,
4794 reason: &str,
4795 ) -> String {
4796 match locale {
4797 Locale::Ja => {
4798 format!(
4799 "{model_label} は起草を完了できませんでした({reason})。ガイド草案は有効です。G でプレビューして批准できます。"
4800 )
4801 }
4802 Locale::ZhHans => {
4803 format!("{model_label} 未能生成草案({reason})。引导式草案仍可使用——按 G 预览并确认。")
4804 }
4805 Locale::ZhHant => {
4806 format!("{model_label} 未能完成起草({reason})。引導式草案仍然有效;按 G 預覽並批准。")
4807 }
4808 Locale::PtBr => {
4809 format!(
4810 "{model_label} não conseguiu rascunhar sua constituição ({reason}). O rascunho guiado continua válido; pressione G para pré-visualizar e ratificar."
4811 )
4812 }
4813 Locale::Es419 => {
4814 format!(
4815 "{model_label} no pudo redactar tu constitución ({reason}). El borrador guiado sigue válido; presiona G para previsualizar y ratificar."
4816 )
4817 }
4818 Locale::Vi => {
4819 format!(
4820 "{model_label} không thể soạn hiến pháp của bạn ({reason}). Bản nháp hướng dẫn vẫn hợp lệ; nhấn G để xem trước và phê chuẩn."
4821 )
4822 }
4823 Locale::Ko => {
4824 format!(
4825 "{model_label}이(가) 당신의 헌법 초안을 작성하지 못했습니다 ({reason}). 가이드 초안은 여전히 유효합니다. G를 눌러 미리보고 승인하세요."
4826 )
4827 }
4828 Locale::Ca => {
4829 format!(
4830 "{model_label} no ha pogut redactar la teva constitució ({reason}). L'esborrany guiat continua vigent; prem G per previsualitzar i ratificar."
4831 )
4832 }
4833 Locale::De => {
4834 format!(
4835 "{model_label} konnte deine Verfassung nicht entwerfen ({reason}). Dein geführter Entwurf bleibt gültig; drücke G für Vorschau und Ratifizierung."
4836 )
4837 }
4838 Locale::Fr => {
4839 format!(
4840 "{model_label} n'a pas pu rédiger votre constitution ({reason}). Votre brouillon guidé reste valide ; appuyez sur G pour l'aperçu et la ratification."
4841 )
4842 }
4843 Locale::Id => {
4844 format!(
4845 "{model_label} tidak dapat menyusun konstitusi Anda ({reason}). Draf terpandu Anda tetap berlaku; tekan G untuk pratinjau dan ratifikasi."
4846 )
4847 }
4848 Locale::Hi => {
4849 format!(
4850 "{model_label} आपके संविधान का मसौदा नहीं बना सका ({reason})। आपका गाइडेड मसौदा अब भी मान्य है; पूर्वावलोकन और अंगीकार के लिए G दबाएँ।"
4851 )
4852 }
4853 Locale::Ru => {
4854 format!(
4855 "{model_label} не смог подготовить вашу конституцию ({reason}). Ваш управляемый проект остаётся в силе — нажмите G для просмотра и ратификации."
4856 )
4857 }
4858 Locale::Uk => {
4859 format!(
4860 "{model_label} не зміг підготувати вашу конституцію ({reason}). Ваш керований проєкт залишається чинним — натисніть G для перегляду та ратифікації."
4861 )
4862 }
4863 _ => format!(
4864 "{model_label} could not draft your constitution ({reason}). Your guided draft still \
4865 stands — press G to preview and ratify."
4866 ),
4867 }
4868 }
4869
4870 fn constitution_choice_label(choice: ConstitutionChoice) -> &'static str {
4871 match choice {
4872 ConstitutionChoice::Unset => "unset",
4873 ConstitutionChoice::Bundled => "bundled/default",
4874 ConstitutionChoice::GuidedCustom => "guided custom",
4875 ConstitutionChoice::ExpertOverride => "expert override",
4876 ConstitutionChoice::Deferred => "deferred",
4877 }
4878 }
4879
4880 fn constitution_source_label(source: ConstitutionSource) -> &'static str {
4881 match source {
4882 ConstitutionSource::Bundled => "bundled",
4883 ConstitutionSource::UserGlobal => "user-global constitution.json",
4884 ConstitutionSource::ExpertOverride => "expert full Markdown override",
4885 }
4886 }
4887
4888 fn constitution_validity_label(validity: ConstitutionValidity) -> &'static str {
4889 match validity {
4890 ConstitutionValidity::Unknown => "unknown",
4891 ConstitutionValidity::Valid => "valid",
4892 ConstitutionValidity::Invalid => "invalid",
4893 ConstitutionValidity::Empty => "empty",
4894 ConstitutionValidity::Unreadable => "unreadable",
4895 }
4896 }
4897
4898 pub fn persist_user_constitution_choice(
4899 constitution: &UserConstitution,
4900 state: &SetupState,
4901 ) -> anyhow::Result<()> {
4902 let constitution_path = UserConstitution::path()?;
4903 let setup_state_path = SetupState::path()?;
4904 let mut transaction = codewhale_config::persistence::SetupTransaction::new();
4905 transaction.stage_json(constitution_path, &constitution.bounded())?;
4906 transaction.stage_json(setup_state_path, state)?;
4907 transaction.commit()
4908 }
4909
4910 #[must_use]
4911 pub fn should_open_update_checkpoint(app: &App, config: &Config) -> bool {
4912 let state = load_setup_state_for_app(app, config);
4913 state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION)
4914 }
4915
4916 pub fn defer_update_checkpoint_for_app(app: &App, config: &Config) -> anyhow::Result<SetupState> {
4917 let mut state = load_setup_state_for_app(app, config);
4918 if !state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) {
4919 return Ok(state);
4920 }
4921 state.complete_constitution_checkpoint(
4922 CONSTITUTION_CHECKPOINT_VERSION,
4923 ConstitutionChoice::Deferred,
4924 );
4925 state.constitution_source = ConstitutionSource::Bundled;
4926 state.constitution_validity = ConstitutionValidity::Unknown;
4927 state.constitution_authoring = None;
4928 state.constitution_preview_hash = None;
4929 state.set_step(
4930 SetupStep::Constitution,
4931 StepEntry::new(StepStatus::Deferred, true, CONSTITUTION_CHECKPOINT_VERSION)
4932 .with_result("checkpoint deferred; bundled applies"),
4933 );
4934 state.save()?;
4935 Ok(state)
4936 }
4937
4938 #[must_use]
4939 pub fn load_setup_state_for_app(app: &App, config: &Config) -> SetupState {
4940 if let Ok(Some(state)) = SetupState::load() {
4941 return state;
4942 }
4943 SetupState::derive_inherited(&inherited_facts_for_app(app, config))
4944 }
4945
4946 pub(crate) fn record_provider_model_setup_state_for_app(
4947 app: &App,
4948 config: &Config,
4949 ) -> anyhow::Result<SetupState> {
4950 let facts = SetupRuntimeFacts::from_app_config(app, config);
4951 let mut state = load_setup_state_for_app(app, config);
4952 state.set_step(
4953 SetupStep::ProviderModel,
4954 provider::step_entry(
4955 facts.provider_ready,
4956 CONSTITUTION_CHECKPOINT_VERSION,
4957 facts.provider_result,
4958 ),
4959 );
4960 state.save()?;
4961 Ok(state)
4962 }
4963
4964 #[must_use]
4965 fn inherited_facts_for_app(app: &App, config: &Config) -> InheritedConfigFacts {
4966 let user_constitution = UserConstitution::load().ok();
4967 let user_constitution_validity = user_constitution.as_ref().map_or(
4968 ConstitutionValidity::Unknown,
4969 UserConstitutionLoad::validity,
4970 );
4971 let has_user_constitution = user_constitution
4972 .as_ref()
4973 .is_some_and(|loaded| !matches!(loaded, UserConstitutionLoad::Missing));
4974 let expert_override = SetupExpertOverrideState::load();
4975 InheritedConfigFacts {
4976 language: Some(app.ui_locale.tag().to_string()),
4977 has_provider_route: !config.default_model().trim().is_empty(),
4978 has_credentials_or_local_runtime: has_api_key(config),
4979 trust_chosen: app.trust_mode || !onboarding::needs_trust(&app.workspace),
4980 has_expert_override: expert_override.is_active(),
4981 has_user_constitution,
4982 user_constitution_validity,
4983 }
4984 }
4985
4986 fn expert_override_path() -> Option<std::path::PathBuf> {
4987 codewhale_config::codewhale_home()
4988 .ok()
4989 .map(|home| home.join(Path::new(CONSTITUTION_OVERRIDE_FILE)))
4990 }
4991
4992 #[must_use]
4993 fn initial_step_index(state: &SetupState) -> usize {
4994 if state.needs_constitution_checkpoint(CONSTITUTION_CHECKPOINT_VERSION) {
4995 return step_index(SetupStep::Constitution);
4996 }
4997 STEP_SPECS
4998 .iter()
4999 .position(|step| {
5000 step.required()
5001 && !matches!(
5002 state.status(step.id()),
5003 StepStatus::Verified
5004 | StepStatus::NeedsAction
5005 | StepStatus::Deferred
5006 | StepStatus::Optional
5007 | StepStatus::Skipped
5008 )
5009 })
5010 .unwrap_or_else(|| step_index(SetupStep::Verification))
5011 }
5012
5013 #[must_use]
5014 fn step_index(step: SetupStep) -> usize {
5015 STEP_SPECS
5016 .iter()
5017 .position(|spec| spec.id() == step)
5018 .expect("all setup-state steps should have wizard specs")
5019 }
5020
5021 fn visible_step_index(step: SetupStep) -> usize {
5022 STEP_SPECS
5023 .iter()
5024 .position(|spec| spec.id() == step)
5025 .unwrap_or_else(|| step_index(SetupStep::Constitution))
5026 }
5027
5028 #[cfg(test)]
5029 mod tests {
5030 use super::*;
5031 use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
5032
5033 fn key(code: KeyCode) -> KeyEvent {
5034 KeyEvent::new(code, KeyModifiers::NONE)
5035 }
5036
5037 fn setup_test_options(workspace: std::path::PathBuf) -> crate::tui::app::TuiOptions {
5038 crate::tui::app::TuiOptions {
5039 allow_shell: true,
5040 start_in_agent_mode: true,
5041 skip_onboarding: false,
5042 ..crate::test_support::test_tui_options(workspace)
5043 }
5044 }
5045
5046 #[test]
5047 fn visible_release_rail_includes_supported_optional_steps() {
5048 let steps = STEP_SPECS.iter().map(|step| step.id()).collect::<Vec<_>>();
5049
5050 assert_eq!(
5051 steps,
5052 vec![
5053 SetupStep::Language,
5054 SetupStep::ProviderModel,
5055 SetupStep::TrustSandbox,
5056 SetupStep::Constitution,
5057 SetupStep::OperateFleet,
5058 SetupStep::Hotbar,
5059 SetupStep::ToolsMcp,
5060 SetupStep::RemoteRuntime,
5061 SetupStep::Persistence,
5062 SetupStep::Verification,
5063 ]
5064 );
5065 assert_eq!(
5066 SetupWizardView::new_at_with_facts(
5067 SetupState::default(),
5068 Locale::En,
5069 SetupStep::ToolsMcp,
5070 SetupRuntimeFacts::default(),
5071 )
5072 .selected_step(),
5073 SetupStep::ToolsMcp
5074 );
5075 }
5076
5077 #[test]
5078 fn wizard_resumes_at_constitution_checkpoint_when_update_incomplete() {
5079 let state = SetupState::default();
5080
5081 let view = SetupWizardView::new(state, Locale::En);
5082
5083 assert_eq!(view.selected_step(), SetupStep::Constitution);
5084 }
5085
5086 #[test]
5087 fn bundled_constitution_commit_marks_checkpoint_complete() {
5088 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5089
5090 let action = view.handle_key(key(KeyCode::Enter));
5091
5092 let ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { state, message }) =
5093 action
5094 else {
5095 panic!("expected setup-state commit event");
5096 };
5097 assert_eq!(
5098 state.constitution_checkpoint_completed_for.as_deref(),
5099 Some(CONSTITUTION_CHECKPOINT_VERSION)
5100 );
5101 assert_eq!(state.constitution_choice, ConstitutionChoice::Bundled);
5102 assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified);
5103 assert!(message.contains("Constitution checkpoint complete"));
5104 }
5105
5106 #[test]
5107 fn back_keys_return_to_previous_step_and_clamp_at_first() {
5108 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5109 assert_eq!(view.selected_step(), SetupStep::Constitution);
5110
5111 let action = view.handle_key(key(KeyCode::Right));
5112 assert!(matches!(action, ViewAction::None));
5113 assert_eq!(view.selected_step(), SetupStep::OperateFleet);
5114
5115 let action = view.handle_key(key(KeyCode::Char('b')));
5116 assert!(matches!(action, ViewAction::None));
5117 assert_eq!(view.selected_step(), SetupStep::Constitution);
5118
5119 for _ in 0..STEP_SPECS.len() {
5120 view.handle_key(key(KeyCode::Left));
5121 }
5122 assert_eq!(view.selected_step(), SetupStep::Language);
5123 }
5124
5125 #[test]
5126 fn cancel_closes_without_commit_event() {
5127 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5128
5129 let action = view.handle_key(key(KeyCode::Esc));
5130
5131 assert!(matches!(action, ViewAction::Close));
5132 }
5133
5134 #[test]
5135 fn skip_and_retry_emit_setup_state_commits() {
5136 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5137
5138 let action = view.handle_key(key(KeyCode::Char('s')));
5139
5140 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
5141 else {
5142 panic!("expected skipped setup-state commit event");
5143 };
5144 assert_eq!(state.status(SetupStep::Constitution), StepStatus::Skipped);
5145 assert_eq!(
5146 state.constitution_checkpoint_completed_for.as_deref(),
5147 Some(CONSTITUTION_CHECKPOINT_VERSION)
5148 );
5149 assert_eq!(state.constitution_choice, ConstitutionChoice::Deferred);
5150 assert!(message.contains("skipped"));
5151 assert_eq!(view.selected_step(), SetupStep::OperateFleet);
5152 let restarted = SetupWizardView::new(state, Locale::En);
5153 assert_eq!(restarted.selected_step(), SetupStep::Language);
5154
5155 let action = view.handle_key(key(KeyCode::Char('r')));
5156
5157 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
5158 else {
5159 panic!("expected retry setup-state commit event");
5160 };
5161 assert_eq!(
5162 state.status(SetupStep::OperateFleet),
5163 StepStatus::NeedsAction
5164 );
5165 assert!(message.contains("retry"));
5166 }
5167
5168 #[test]
5169 fn skipping_checkpoint_preserves_active_custom_constitution() {
5170 let mut state = SetupState {
5171 constitution_choice: ConstitutionChoice::GuidedCustom,
5172 constitution_source: ConstitutionSource::UserGlobal,
5173 constitution_validity: ConstitutionValidity::Valid,
5174 constitution_authoring: Some(ConstitutionAuthoring::Guided),
5175 constitution_preview_hash: Some("sha256:existing-custom-preview".to_string()),
5176 constitution_preview_version: 7,
5177 ..SetupState::default()
5178 };
5179 state.constitution_checkpoint_completed_for = Some("0.8.66".to_string());
5180 let mut view = SetupWizardView::new_at_with_facts(
5181 state,
5182 Locale::En,
5183 SetupStep::Constitution,
5184 SetupRuntimeFacts::default(),
5185 );
5186
5187 let action = view.handle_key(key(KeyCode::Char('s')));
5188
5189 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
5190 else {
5191 panic!("expected skipped setup-state commit event");
5192 };
5193 assert_eq!(state.status(SetupStep::Constitution), StepStatus::Skipped);
5194 assert_eq!(
5195 state.constitution_checkpoint_completed_for.as_deref(),
5196 Some(CONSTITUTION_CHECKPOINT_VERSION)
5197 );
5198 assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom);
5199 assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal);
5200 assert_eq!(state.constitution_validity, ConstitutionValidity::Valid);
5201 assert_eq!(
5202 state.constitution_authoring,
5203 Some(ConstitutionAuthoring::Guided)
5204 );
5205 assert_eq!(
5206 state.constitution_preview_hash.as_deref(),
5207 Some("sha256:existing-custom-preview")
5208 );
5209 assert_eq!(state.constitution_preview_version, 7);
5210 assert!(message.contains("skipped"));
5211
5212 let restarted = SetupWizardView::new(state, Locale::En);
5213 assert_eq!(restarted.selected_step(), SetupStep::Language);
5214 }
5215
5216 #[test]
5217 fn completed_checkpoint_resumes_to_first_required_gap() {
5218 let mut state = SetupState::default();
5219 state.complete_constitution_checkpoint(
5220 CONSTITUTION_CHECKPOINT_VERSION,
5221 ConstitutionChoice::Bundled,
5222 );
5223
5224 let view = SetupWizardView::new(state, Locale::En);
5225
5226 assert_eq!(view.selected_step(), SetupStep::Language);
5227 }
5228
5229 #[test]
5230 fn language_step_records_locale_and_unblocks_first_run_ready() {
5231 let mut state = SetupState::default();
5232 state.set_step(
5233 SetupStep::ProviderModel,
5234 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
5235 );
5236 state.runtime_posture_source = RuntimePostureSource::Confirmed;
5237 state.complete_constitution_checkpoint(
5238 CONSTITUTION_CHECKPOINT_VERSION,
5239 ConstitutionChoice::Bundled,
5240 );
5241 state.set_step(
5242 SetupStep::Constitution,
5243 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
5244 );
5245 let mut view = SetupWizardView::new(state, Locale::En);
5246 assert_eq!(view.selected_step(), SetupStep::Language);
5247
5248 let action = view.handle_key(key(KeyCode::Enter));
5249
5250 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
5251 else {
5252 panic!("expected language setup-state commit event");
5253 };
5254 assert_eq!(state.status(SetupStep::Language), StepStatus::Verified);
5255 assert_eq!(state.constitution_language.as_deref(), Some("en"));
5256 assert!(state.first_run_ready());
5257 assert!(message.contains("Setup language recorded"));
5258 assert_eq!(view.selected_step(), SetupStep::ProviderModel);
5259 }
5260
5261 #[test]
5262 fn zh_hans_checkpoint_copy_is_localized() {
5263 assert_ne!(
5264 tr(Locale::ZhHans, MessageId::SetupWizardTitle),
5265 tr(Locale::En, MessageId::SetupWizardTitle)
5266 );
5267 assert_ne!(
5268 tr(Locale::ZhHans, MessageId::SetupCheckpointDoneBundled),
5269 tr(Locale::En, MessageId::SetupCheckpointDoneBundled)
5270 );
5271 }
5272
5273 #[test]
5274 fn zh_hans_constitution_surfaces_use_functional_terminology() {
5275 let constitution = GuidedConstitutionDraft::default().to_constitution(Locale::ZhHans);
5276 let samples = [
5277 ratification_preview_title(Locale::ZhHans).to_string(),
5278 constitution_ratification_text(
5279 Locale::ZhHans,
5280 &constitution,
5281 &DraftProvenance::Existing,
5282 ),
5283 lines_to_text(vec![freeform_note_line(Locale::ZhHans, "", false)]),
5284 keep_existing_invitation_line(Locale::ZhHans).to_string(),
5285 model_draft_invitation_line(Locale::ZhHans, "GLM-5.2"),
5286 model_draft_ready_line(Locale::ZhHans, "GLM-5.2"),
5287 model_draft_ready_message(Locale::ZhHans, "GLM-5.2"),
5288 model_draft_failed_message(Locale::ZhHans, "GLM-5.2", "超时"),
5289 ];
5290 let visible_copy = samples.join("\n");
5291
5292 for literal_metaphor in ["宪法", "教义", "自由原则", "起草"] {
5293 assert!(
5294 !visible_copy.contains(literal_metaphor),
5295 "Simplified Chinese setup copy should avoid {literal_metaphor}: {visible_copy}"
5296 );
5297 }
5298 assert!(visible_copy.contains("宪章"));
5299 assert!(visible_copy.contains("自定义准则"));
5300 assert!(visible_copy.contains("可选策略"));
5301 assert!(visible_copy.contains("Codewhale"));
5302 assert!(visible_copy.contains("/constitution"));
5303 assert!(visible_copy.contains("constitution.json"));
5304 }
5305
5306 #[test]
5307 fn guided_constitution_requires_preview_before_save() {
5308 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5309
5310 let action = view.handle_key(key(KeyCode::Char('g')));
5311
5312 let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else {
5313 panic!("expected guided constitution preview event");
5314 };
5315 assert!(title.contains("Draft for Ratification"));
5316 assert!(content.contains("<codewhale_user_constitution"));
5317 assert!(content.contains("press G to ratify and save"));
5318 assert!(content.contains("REDUCED CORE AND OPT-IN MODULES"));
5319 assert!(content.contains("The bundled core stays active"));
5320 assert!(content.contains("does not enable modules"));
5321 assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset);
5322
5323 let action = view.handle_key(key(KeyCode::Char('g')));
5324
5325 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
5326 constitution,
5327 state,
5328 message,
5329 }) = action
5330 else {
5331 panic!("expected guided constitution commit event");
5332 };
5333 assert_eq!(constitution.language.as_deref(), Some("en"));
5334 assert_eq!(
5335 constitution.autonomy_preference,
5336 AutonomyPreference::Balanced
5337 );
5338 assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom);
5339 assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal);
5340 assert_eq!(state.constitution_validity, ConstitutionValidity::Valid);
5341 assert_eq!(
5342 state.constitution_preview_hash.as_deref(),
5343 Some(constitution.preview_hash().as_str())
5344 );
5345 assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified);
5346 assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset);
5347 assert!(message.contains("Constitution ratified"));
5348 }
5349
5350 #[test]
5351 fn ratification_preview_explains_reduced_core_modules_for_shipped_locales() {
5352 for locale in Locale::shipped() {
5353 let constitution = GuidedConstitutionDraft::default().to_constitution(*locale);
5354 let content =
5355 constitution_ratification_text(*locale, &constitution, &DraftProvenance::Guided);
5356 let (heading, module_marker, no_enable_marker, permission_marker, mcp_marker) =
5357 match locale {
5358 Locale::Ja => (
5359 "縮小コア",
5360 "モジュール",
5361 "有効化せず",
5362 "承認ポリシー、サンドボックス、Shell、ネットワーク、信頼、MCP 権限",
5363 "付与または変更することはできません",
5364 ),
5365 Locale::ZhHans => (
5366 "精简核心",
5367 "可选策略",
5368 "不会启用任何策略",
5369 "不能授予或更改审批策略、沙箱、Shell、网络、信任、MCP 权限",
5370 "发布或支出权限",
5371 ),
5372 Locale::ZhHant => (
5373 "精簡核心",
5374 "模組",
5375 "不會啟用",
5376 "不能授予或更改審批策略、沙箱、Shell、網路、信任、MCP 權限",
5377 "發布或支出權限",
5378 ),
5379 Locale::PtBr => (
5380 "NÚCLEO REDUZIDO",
5381 "módulos",
5382 "não ativa",
5383 "Não pode conceder nem alterar política de aprovação, sandbox, shell, rede",
5384 "permissões MCP",
5385 ),
5386 Locale::Es419 => (
5387 "NÚCLEO REDUCIDO",
5388 "módulos",
5389 "no activa",
5390 "No puede conceder ni cambiar política de aprobación, sandbox, shell, red",
5391 "permisos MCP",
5392 ),
5393 Locale::Vi => (
5394 "LÕI RÚT GỌN",
5395 "mô-đun",
5396 "không bật",
5397 "không thể cấp hoặc đổi chính sách phê duyệt, sandbox, shell, mạng",
5398 "quyền MCP",
5399 ),
5400 Locale::Ko => (
5401 "축소된 코어",
5402 "모듈",
5403 "활성화하지 않으며",
5404 "승인 정책, 샌드박스, 셸, 네트워크, 신뢰, MCP 권한, 기본 모드, 게시, 지출 권한을 부여하거나 바꿀 수 없습니다",
5405 "MCP 권한",
5406 ),
5407 Locale::Ca => (
5408 "NUCLI REDUÏT",
5409 "mòduls",
5410 "no activa",
5411 "No pot concedir ni canviar la política d'aprovació, sandbox, shell, xarxa",
5412 "permisos MCP",
5413 ),
5414 Locale::De => (
5415 "REDUZIERTER KERN",
5416 "Module",
5417 "aktiviert keine",
5418 "Sie kann keine Freigaberichtlinie, Sandbox, Shell, Netzwerk",
5419 "MCP-Berechtigungen",
5420 ),
5421 Locale::Fr => (
5422 "NOYAU RÉDUIT",
5423 "modules",
5424 "n'active pas",
5425 "Elle ne peut ni accorder ni modifier la politique d'approbation, le sandbox, le shell, le réseau",
5426 "permissions MCP",
5427 ),
5428 Locale::Id => (
5429 "INTI RINGKAS",
5430 "modul",
5431 "tidak mengaktifkan",
5432 "tidak dapat memberi atau mengubah kebijakan persetujuan, sandbox, shell, jaringan",
5433 "izin MCP",
5434 ),
5435 Locale::Hi => (
5436 "संक्षिप्त कोर",
5437 "मॉड्यूल",
5438 "सक्षम नहीं करता",
5439 "यह अनुमति नीति, सैंडबॉक्स, शेल, नेटवर्क",
5440 "MCP अनुमतियाँ",
5441 ),
5442 Locale::Ru => (
5443 "СОКРАЩЁННОЕ ЯДРО",
5444 "модули",
5445 "не включает модули",
5446 "Она не может предоставить или изменить политику одобрения, sandbox, shell, сеть",
5447 "разрешения MCP",
5448 ),
5449 Locale::Uk => (
5450 "СКОРОЧЕНЕ ЯДРО",
5451 "модулі",
5452 "не вмикає модулі",
5453 "Вона не може надати або змінити політику схвалення, sandbox, shell, мережу",
5454 "дозволи MCP",
5455 ),
5456 Locale::En => (
5457 "REDUCED CORE",
5458 "modules",
5459 "does not enable",
5460 "cannot grant or change approval policy, sandbox, shell",
5461 "MCP permissions",
5462 ),
5463 };
5464
5465 assert!(content.contains(heading), "{}", locale.tag());
5466 assert!(content.contains(module_marker), "{}", locale.tag());
5467 assert!(content.contains(no_enable_marker), "{}", locale.tag());
5468 assert!(content.contains(permission_marker), "{}", locale.tag());
5469 assert!(content.contains(mcp_marker), "{}", locale.tag());
5470 }
5471 }
5472
5473 #[test]
5474 fn guided_constitution_key_is_contextual_to_constitution_step() {
5475 let mut view = SetupWizardView::new_at_with_facts(
5476 SetupState::default(),
5477 Locale::En,
5478 SetupStep::ProviderModel,
5479 SetupRuntimeFacts::default(),
5480 );
5481
5482 let action = view.handle_key(key(KeyCode::Char('g')));
5483
5484 assert!(matches!(action, ViewAction::None));
5485 assert_eq!(view.selected_step(), SetupStep::ProviderModel);
5486 assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset);
5487 }
5488
5489 #[test]
5490 fn provider_model_step_hands_off_to_existing_route_surfaces() {
5491 let mut view = SetupWizardView::new_at_with_facts(
5492 SetupState::default(),
5493 Locale::En,
5494 SetupStep::ProviderModel,
5495 SetupRuntimeFacts::default(),
5496 );
5497
5498 let provider_action = view.handle_key(key(KeyCode::Char('p')));
5499 assert!(matches!(
5500 provider_action,
5501 ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested)
5502 ));
5503
5504 let model_action = view.handle_key(key(KeyCode::Char('m')));
5505 assert!(matches!(
5506 model_action,
5507 ViewAction::EmitAndClose(ViewEvent::SetupOpenModelRequested)
5508 ));
5509 }
5510
5511 #[test]
5512 fn provider_model_detail_lines_show_credential_url_for_missing_hosted_provider() {
5513 let _guard = crate::test_support::lock_test_env();
5514 let tmp = tempfile::TempDir::new().expect("tempdir");
5515 let workspace = tmp.path().join("workspace");
5516 std::fs::create_dir_all(&workspace).expect("workspace dir");
5517 let codewhale_home = tmp.path().join(".codewhale");
5518 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5519 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5520 let _codewhale_home =
5521 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5522 let _deepseek_key = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
5523 let _nim_key = crate::test_support::EnvVarGuard::remove("NVIDIA_API_KEY");
5524 let _nim_alt_key = crate::test_support::EnvVarGuard::remove("NVIDIA_NIM_API_KEY");
5525 let config = Config {
5526 provider: Some("nvidia-nim".to_string()),
5527 ..Config::default()
5528 };
5529 let app = App::new(setup_test_options(workspace), &config);
5530 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
5531 let view = SetupWizardView::new_at_with_facts(
5532 SetupState::default(),
5533 Locale::En,
5534 SetupStep::ProviderModel,
5535 facts,
5536 );
5537
5538 let text = lines_to_text(view.provider_model_detail_lines());
5539
5540 assert!(text.contains("NVIDIA NIM"), "{text}");
5541 assert!(text.contains("credentials: https://build.nvidia.com/settings/api-keys"));
5542 }
5543
5544 #[test]
5545 fn provider_model_detail_lines_use_kimi_code_membership_console_for_exact_route() {
5546 let _guard = crate::test_support::lock_test_env();
5547 let tmp = tempfile::TempDir::new().expect("tempdir");
5548 let workspace = tmp.path().join("workspace");
5549 std::fs::create_dir_all(&workspace).expect("workspace dir");
5550 let codewhale_home = tmp.path().join(".codewhale");
5551 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5552 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5553 let _codewhale_home =
5554 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5555 let _moonshot_key = crate::test_support::EnvVarGuard::remove("MOONSHOT_API_KEY");
5556 let _kimi_key = crate::test_support::EnvVarGuard::remove("KIMI_API_KEY");
5557 let config = Config {
5558 provider: Some("moonshot".to_string()),
5559 providers: Some(crate::config::ProvidersConfig {
5560 moonshot: crate::config::ProviderConfig {
5561 base_url: Some(crate::config::DEFAULT_KIMI_CODE_BASE_URL.to_string()),
5562 model: Some(crate::config::KIMI_CODE_K3_MODEL.to_string()),
5563 ..Default::default()
5564 },
5565 ..Default::default()
5566 }),
5567 ..Config::default()
5568 };
5569 let mut app = App::new(setup_test_options(workspace), &config);
5570 app.model = crate::config::KIMI_CODE_K3_MODEL.to_string();
5571 let resolution = crate::route_runtime::resolve_route_candidate_with_context_metadata(
5572 app.api_provider,
5573 Some(crate::config::KIMI_CODE_K3_MODEL),
5574 Some(crate::config::KIMI_CODE_K3_MODEL),
5575 Some(config.deepseek_base_url()),
5576 None,
5577 None,
5578 )
5579 .expect("bare k3 on the Kimi Code route must resolve");
5580 app.set_active_route_resolution(
5581 resolution.candidate.endpoint().base_url.clone(),
5582 resolution.candidate.limits(),
5583 resolution.context_window.source,
5584 );
5585 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
5586 let view = SetupWizardView::new_at_with_facts(
5587 SetupState::default(),
5588 Locale::En,
5589 SetupStep::ProviderModel,
5590 facts,
5591 );
5592
5593 let text = lines_to_text(view.provider_model_detail_lines());
5594
5595 assert!(text.contains("Moonshot/Kimi"), "{text}");
5596 assert!(text.contains("static Kimi Code safe floor"), "{text}");
5597 assert!(
5598 text.contains(crate::config::KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL),
5599 "{text}"
5600 );
5601 assert!(!text.contains("https://platform.kimi.ai/console/api-keys"));
5602 }
5603
5604 #[test]
5605 fn provider_model_detail_lines_keep_codex_oauth_url_free() {
5606 let _guard = crate::test_support::lock_test_env();
5607 let tmp = tempfile::TempDir::new().expect("tempdir");
5608 let workspace = tmp.path().join("workspace");
5609 std::fs::create_dir_all(&workspace).expect("workspace dir");
5610 let codewhale_home = tmp.path().join(".codewhale");
5611 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5612 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5613 let _codewhale_home =
5614 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5615 let _openai_codex_key =
5616 crate::test_support::EnvVarGuard::remove("OPENAI_CODEX_ACCESS_TOKEN");
5617 let _codex_key = crate::test_support::EnvVarGuard::remove("CODEX_ACCESS_TOKEN");
5618 let config = Config {
5619 provider: Some("openai-codex".to_string()),
5620 ..Config::default()
5621 };
5622 let app = App::new(setup_test_options(workspace), &config);
5623 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
5624 let view = SetupWizardView::new_at_with_facts(
5625 SetupState::default(),
5626 Locale::En,
5627 SetupStep::ProviderModel,
5628 facts,
5629 );
5630
5631 let text = lines_to_text(view.provider_model_detail_lines());
5632
5633 assert!(text.contains("codex login"), "{text}");
5634 assert!(text.contains("external-consent"), "{text}");
5635 assert!(!text.contains("credentials:"), "{text}");
5636 }
5637
5638 #[test]
5639 fn provider_model_detail_lines_cover_deepseek_cn_and_local_boundaries() {
5640 let _guard = crate::test_support::lock_test_env();
5641 let tmp = tempfile::TempDir::new().expect("tempdir");
5642 let workspace = tmp.path().join("workspace");
5643 std::fs::create_dir_all(&workspace).expect("workspace dir");
5644 let codewhale_home = tmp.path().join(".codewhale");
5645 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5646 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5647 let _codewhale_home =
5648 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5649 let _deepseek_key = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
5650 let _deepseek_source = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY_SOURCE");
5651
5652 let cn_config = Config {
5653 provider: Some("deepseek-cn".to_string()),
5654 ..Config::default()
5655 };
5656 let cn_app = App::new(setup_test_options(workspace.clone()), &cn_config);
5657 let cn_view = SetupWizardView::new_at_with_facts(
5658 SetupState::default(),
5659 Locale::En,
5660 SetupStep::ProviderModel,
5661 SetupRuntimeFacts::from_app_config(&cn_app, &cn_config),
5662 );
5663 let cn_text = lines_to_text(cn_view.provider_model_detail_lines());
5664 assert!(cn_text.contains("DeepSeek (legacy alias)"), "{cn_text}");
5665 assert!(
5666 cn_text.contains("credentials: https://platform.deepseek.com/api_keys"),
5667 "{cn_text}"
5668 );
5669 assert!(cn_text.contains("missing key"), "{cn_text}");
5670
5671 let local_config = Config {
5672 provider: Some("ollama".to_string()),
5673 ..Config::default()
5674 };
5675 let local_app = App::new(setup_test_options(workspace), &local_config);
5676 let local_view = SetupWizardView::new_at_with_facts(
5677 SetupState::default(),
5678 Locale::En,
5679 SetupStep::ProviderModel,
5680 SetupRuntimeFacts::from_app_config(&local_app, &local_config),
5681 );
5682 let local_text = lines_to_text(local_view.provider_model_detail_lines());
5683 assert!(local_text.contains("Ollama"), "{local_text}");
5684 assert!(local_text.contains("local · not checked"), "{local_text}");
5685 assert!(!local_text.contains("credentials:"), "{local_text}");
5686 }
5687
5688 #[test]
5689 fn runtime_posture_step_hands_off_to_mode_and_config_surfaces() {
5690 let mut view = SetupWizardView::new_at_with_facts(
5691 SetupState::default(),
5692 Locale::En,
5693 SetupStep::TrustSandbox,
5694 SetupRuntimeFacts::default(),
5695 );
5696
5697 let mode_action = view.handle_key(key(KeyCode::Char('m')));
5698 assert!(matches!(
5699 mode_action,
5700 ViewAction::EmitAndClose(ViewEvent::SetupOpenModeRequested)
5701 ));
5702
5703 let config_action = view.handle_key(key(KeyCode::Char('c')));
5704 assert!(matches!(
5705 config_action,
5706 ViewAction::EmitAndClose(ViewEvent::SetupOpenConfigRequested)
5707 ));
5708 }
5709
5710 #[test]
5711 fn operate_fleet_step_hands_off_to_provider_and_fleet_surfaces() {
5712 let mut view = SetupWizardView::new_at_with_facts(
5713 SetupState::default(),
5714 Locale::En,
5715 SetupStep::OperateFleet,
5716 SetupRuntimeFacts::default(),
5717 );
5718
5719 let provider_action = view.handle_key(key(KeyCode::Char('p')));
5720 assert!(matches!(
5721 provider_action,
5722 ViewAction::EmitAndClose(ViewEvent::SetupOpenProviderRequested)
5723 ));
5724
5725 let fleet_action = view.handle_key(key(KeyCode::Char('f')));
5726 assert!(matches!(
5727 fleet_action,
5728 ViewAction::EmitAndClose(ViewEvent::SetupOpenFleetRequested)
5729 ));
5730 }
5731
5732 #[test]
5733 fn hotbar_step_hands_off_to_existing_hotbar_setup() {
5734 let mut view = SetupWizardView::new_at_with_facts(
5735 SetupState::default(),
5736 Locale::En,
5737 SetupStep::Hotbar,
5738 SetupRuntimeFacts::default(),
5739 );
5740
5741 let action = view.handle_key(key(KeyCode::Char('h')));
5742
5743 assert!(matches!(
5744 action,
5745 ViewAction::EmitAndClose(ViewEvent::SetupOpenHotbarRequested)
5746 ));
5747 }
5748
5749 #[test]
5750 fn remote_runtime_step_previews_generate_only_on_ramp() {
5751 let facts = SetupRuntimeFacts {
5752 remote_clouds_result: "3 cloud targets: lighthouse, azure, digitalocean".to_string(),
5753 remote_bridges_result: "2 chat bridges: feishu, telegram".to_string(),
5754 remote_providers_result:
5755 "12 providers from the provider registry; active route deepseek / deepseek-chat"
5756 .to_string(),
5757 remote_mode_result:
5758 "generate-only bundle; --apply not implemented; default port 7878, workers 2"
5759 .to_string(),
5760 ..SetupRuntimeFacts::default()
5761 };
5762 let mut view = SetupWizardView::new_at_with_facts(
5763 SetupState::default(),
5764 Locale::En,
5765 SetupStep::RemoteRuntime,
5766 facts,
5767 );
5768
5769 let action = view.handle_key(key(KeyCode::Char('r')));
5770
5771 let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else {
5772 panic!("expected remote on-ramp pager");
5773 };
5774 assert_eq!(title, "Remote runtime on-ramp");
5775 assert!(content.contains("does not generate deploy bundles"));
5776 assert!(content.contains("codewhale remote-setup --generate-only"));
5777 assert!(content.contains("`--apply` remains unimplemented"));
5778 }
5779
5780 #[test]
5781 fn remote_runtime_on_ramp_command_uses_active_provider() {
5782 let _guard = crate::test_support::lock_test_env();
5783 let tmp = tempfile::TempDir::new().expect("tempdir");
5784 let workspace = tmp.path().join("workspace");
5785 std::fs::create_dir_all(&workspace).expect("workspace dir");
5786 let codewhale_home = tmp.path().join(".codewhale");
5787 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5788 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5789 let _codewhale_home =
5790 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5791 let config = Config {
5792 provider: Some("openrouter".to_string()),
5793 ..Config::default()
5794 };
5795 let app = App::new(setup_test_options(workspace), &config);
5796 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
5797
5798 let content = remote_runtime_on_ramp_text(Locale::En, &facts);
5799
5800 assert!(content.contains("--provider openrouter"), "{content}");
5801 assert!(!content.contains("--provider deepseek"), "{content}");
5802 assert!(
5803 content.contains("does not generate deploy bundles"),
5804 "{content}"
5805 );
5806 assert!(
5807 content.contains("`--apply` remains unimplemented"),
5808 "{content}"
5809 );
5810 }
5811
5812 #[test]
5813 fn remote_runtime_on_ramp_never_substitutes_deepseek_for_named_custom_route() {
5814 let _guard = crate::test_support::lock_test_env();
5815 let tmp = tempfile::TempDir::new().expect("tempdir");
5816 let workspace = tmp.path().join("workspace");
5817 std::fs::create_dir_all(&workspace).expect("workspace dir");
5818 let codewhale_home = tmp.path().join(".codewhale");
5819 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
5820 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
5821 let _codewhale_home =
5822 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
5823 let mut custom = std::collections::HashMap::new();
5824 custom.insert(
5825 "lm-studio".to_string(),
5826 crate::config::ProviderConfig {
5827 kind: Some("openai-compatible".to_string()),
5828 base_url: Some("http://127.0.0.1:1234/v1".to_string()),
5829 model: Some("local-code-model".to_string()),
5830 ..Default::default()
5831 },
5832 );
5833 let config = Config {
5834 provider: Some("lm-studio".to_string()),
5835 providers: Some(crate::config::ProvidersConfig {
5836 custom,
5837 ..Default::default()
5838 }),
5839 ..Config::default()
5840 };
5841 let app = App::new(setup_test_options(workspace), &config);
5842 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
5843
5844 let content = remote_runtime_on_ramp_text(Locale::En, &facts);
5845
5846 assert!(content.contains("active route lm-studio"), "{content}");
5847 assert!(content.contains("--provider lm-studio"), "{content}");
5848 assert!(!content.contains("--provider deepseek"), "{content}");
5849 }
5850
5851 #[test]
5852 fn remote_runtime_on_ramp_is_localized_for_shipped_locales() {
5853 let facts = SetupRuntimeFacts {
5854 remote_clouds_result: "3 cloud targets: lighthouse, azure, digitalocean".to_string(),
5855 remote_bridges_result: "2 chat bridges: feishu, telegram".to_string(),
5856 remote_providers_result:
5857 "12 providers from the provider registry; active route deepseek / deepseek-chat"
5858 .to_string(),
5859 remote_mode_result:
5860 "generate-only bundle; --apply not implemented; default port 7878, workers 2"
5861 .to_string(),
5862 ..SetupRuntimeFacts::default()
5863 };
5864 let english = remote_runtime_on_ramp_text(Locale::En, &facts);
5865
5866 for locale in Locale::shipped() {
5867 let content = remote_runtime_on_ramp_text(*locale, &facts);
5868 assert!(
5869 content.contains("codewhale remote-setup --generate-only"),
5870 "{}",
5871 locale.tag()
5872 );
5873 assert!(content.contains("`--apply`"), "{}", locale.tag());
5874 if *locale != Locale::En {
5875 assert_ne!(content, english, "{}", locale.tag());
5876 }
5877 }
5878 }
5879
5880 #[test]
5881 fn guided_constitution_answers_shape_preview_and_saved_payload() {
5882 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5883 for key_char in ['1', '2', '3', '4', '5', '6'] {
5884 assert!(matches!(
5885 view.handle_key(key(KeyCode::Char(key_char))),
5886 ViewAction::None
5887 ));
5888 }
5889
5890 let action = view.handle_key(key(KeyCode::Char('g')));
5891
5892 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else {
5893 panic!("expected tuned guided constitution preview event");
5894 };
5895 assert!(content.contains("current, cited research"));
5896 assert!(content.contains("ambitious initiative"));
5897 assert!(content.contains("release evidence"));
5898 assert!(content.contains("learn the system"));
5899 assert!(content.contains("sensitive data"));
5900 assert!(content.contains("user voice"));
5901 assert!(content.contains("preserve the user's voice"));
5902
5903 let action = view.handle_key(key(KeyCode::Char('g')));
5904
5905 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
5906 constitution,
5907 state,
5908 ..
5909 }) = action
5910 else {
5911 panic!("expected tuned guided constitution commit event");
5912 };
5913 assert_eq!(
5914 constitution.autonomy_preference,
5915 AutonomyPreference::Autonomous
5916 );
5917 let body = constitution.render_body();
5918 assert!(body.contains("current, cited research"));
5919 assert!(body.contains("release evidence"));
5920 assert!(body.contains("learn the system"));
5921 assert!(body.contains("sensitive data"));
5922 assert!(body.contains("preserve the user's voice"));
5923 assert_eq!(
5924 state.constitution_preview_hash.as_deref(),
5925 Some(constitution.preview_hash().as_str())
5926 );
5927 }
5928
5929 #[test]
5930 fn constitution_detail_lines_explain_reduced_core_and_modules_boundary() {
5931 let view = SetupWizardView::new_at_with_facts(
5932 SetupState::default(),
5933 Locale::En,
5934 SetupStep::Constitution,
5935 SetupRuntimeFacts::default(),
5936 );
5937
5938 let text = lines_to_text(view.constitution_detail_lines());
5939
5940 assert!(text.contains("user-global preferences only"));
5941 // The en copy no longer claims a line count for the core (#4057 wave 2
5942 // reword: the shipped core outgrew "55-line").
5943 assert!(text.contains("bundled core"));
5944 assert!(text.contains("mode prompts"));
5945 assert!(text.contains("future opt-ins"));
5946 }
5947
5948 #[test]
5949 fn freeform_note_previews_saves_and_stays_advisory() {
5950 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5951
5952 let first_preview = view.handle_key(key(KeyCode::Char('g')));
5953 assert!(matches!(
5954 first_preview,
5955 ViewAction::Emit(ViewEvent::OpenTextPager { .. })
5956 ));
5957 assert!(view.handle_paste(
5958 "Prefer reversible demos; do not treat shell unrestricted as permission."
5959 ));
5960
5961 let second_preview = view.handle_key(key(KeyCode::Char('g')));
5962 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = second_preview else {
5963 panic!("freeform note should force a fresh preview");
5964 };
5965 assert!(content.contains("User freeform principle"));
5966 assert!(content.contains("Prefer reversible demos"));
5967 assert!(content.contains("do not change approval, sandbox, shell"));
5968
5969 let action = view.handle_key(key(KeyCode::Char('g')));
5970 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
5971 constitution,
5972 state,
5973 ..
5974 }) = action
5975 else {
5976 panic!("expected guided constitution commit event");
5977 };
5978 let body = constitution.render_body();
5979 assert!(body.contains("User freeform principle"));
5980 assert!(body.contains("Prefer reversible demos"));
5981 assert_eq!(
5982 state.constitution_authoring,
5983 Some(ConstitutionAuthoring::Guided)
5984 );
5985 assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset);
5986 }
5987
5988 #[test]
5989 fn changing_guided_answer_requires_fresh_preview() {
5990 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
5991
5992 let first_preview = view.handle_key(key(KeyCode::Char('g')));
5993 assert!(matches!(
5994 first_preview,
5995 ViewAction::Emit(ViewEvent::OpenTextPager { .. })
5996 ));
5997
5998 assert!(matches!(
5999 view.handle_key(key(KeyCode::Char('6'))),
6000 ViewAction::None
6001 ));
6002 let second_preview = view.handle_key(key(KeyCode::Char('g')));
6003
6004 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = second_preview else {
6005 panic!("changed guided answer should preview again before saving");
6006 };
6007 assert!(content.contains("preserve the user's voice"));
6008
6009 let action = view.handle_key(key(KeyCode::Char('g')));
6010 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
6011 constitution,
6012 ..
6013 }) = action
6014 else {
6015 panic!("expected save after fresh preview");
6016 };
6017 assert_eq!(
6018 constitution.autonomy_preference,
6019 AutonomyPreference::Balanced
6020 );
6021 assert!(
6022 constitution
6023 .render_body()
6024 .contains("preserve the user's voice")
6025 );
6026 }
6027
6028 fn ready_facts(model: &str) -> SetupRuntimeFacts {
6029 SetupRuntimeFacts {
6030 provider_ready: true,
6031 model: model.to_string(),
6032 ..SetupRuntimeFacts::default()
6033 }
6034 }
6035
6036 fn first_run_ready_state() -> SetupState {
6037 let mut state = SetupState::default();
6038 state.set_step(
6039 SetupStep::Language,
6040 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
6041 );
6042 state.set_step(
6043 SetupStep::ProviderModel,
6044 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
6045 );
6046 state.runtime_posture_source = RuntimePostureSource::Confirmed;
6047 state.complete_constitution_checkpoint(
6048 CONSTITUTION_CHECKPOINT_VERSION,
6049 ConstitutionChoice::Bundled,
6050 );
6051 state.set_step(
6052 SetupStep::Constitution,
6053 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
6054 );
6055 state
6056 }
6057
6058 fn sample_model_draft() -> Box<UserConstitution> {
6059 Box::new(UserConstitution {
6060 language: Some("en".to_string()),
6061 about: Some("A GLM-5.2 user shipping Rust.".to_string()),
6062 working_style: vec!["Keep diffs scoped.".to_string()],
6063 priorities: vec!["Evidence over vibes.".to_string()],
6064 autonomy_preference: AutonomyPreference::Balanced,
6065 notes: Some("Advisory only.".to_string()),
6066 ..UserConstitution::default()
6067 })
6068 }
6069
6070 #[test]
6071 fn model_draft_key_is_inert_without_a_ready_provider() {
6072 // Fallback contract: no route, no drafting offer — the deterministic
6073 // guided flow stands untouched.
6074 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
6075 assert_eq!(view.selected_step(), SetupStep::Constitution);
6076
6077 let action = view.handle_key(key(KeyCode::Char('a')));
6078
6079 assert!(matches!(action, ViewAction::None));
6080 assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset);
6081 }
6082
6083 #[test]
6084 fn model_draft_key_requests_drafting_with_current_answers() {
6085 let mut view = SetupWizardView::new_at_with_facts(
6086 SetupState::default(),
6087 Locale::En,
6088 SetupStep::Constitution,
6089 ready_facts("GLM-5.2"),
6090 );
6091 // Tune one answer first: the request must carry the tuned draft.
6092 assert!(matches!(
6093 view.handle_key(key(KeyCode::Char('2'))),
6094 ViewAction::None
6095 ));
6096 assert!(view.handle_paste("Prefer demos before durable rewrites."));
6097
6098 let action = view.handle_key(key(KeyCode::Char('a')));
6099
6100 let ViewAction::Emit(ViewEvent::SetupConstitutionModelDraftRequested {
6101 draft,
6102 freeform_note,
6103 locale,
6104 }) = action
6105 else {
6106 panic!("expected model draft request event");
6107 };
6108 assert_eq!(locale, Locale::En);
6109 assert_eq!(draft.autonomy, AutonomyPreference::Autonomous);
6110 assert_eq!(
6111 freeform_note.as_deref(),
6112 Some("Prefer demos before durable rewrites.")
6113 );
6114 // The wizard stays open (Emit, not EmitAndClose) and nothing commits.
6115 assert_eq!(view.state().constitution_choice, ConstitutionChoice::Unset);
6116 }
6117
6118 #[test]
6119 fn installed_model_draft_previews_then_ratifies_with_provenance() {
6120 let mut view = SetupWizardView::new_at_with_facts(
6121 SetupState::default(),
6122 Locale::En,
6123 SetupStep::Constitution,
6124 ready_facts("GLM-5.2"),
6125 );
6126
6127 let (title, content) =
6128 view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string());
6129 assert!(title.contains("Draft for Ratification"));
6130 assert!(content.contains("Drafted by GLM-5.2"));
6131 assert!(content.contains("A GLM-5.2 user shipping Rust."));
6132 assert!(content.contains("<codewhale_user_constitution"));
6133
6134 // The install satisfied the preview gate; G ratifies the model draft.
6135 let action = view.handle_key(key(KeyCode::Char('g')));
6136 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
6137 constitution,
6138 state,
6139 message,
6140 }) = action
6141 else {
6142 panic!("expected ratification commit event");
6143 };
6144 assert_eq!(constitution, *sample_model_draft());
6145 assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom);
6146 assert_eq!(
6147 state.constitution_authoring,
6148 Some(ConstitutionAuthoring::ModelDrafted)
6149 );
6150 assert_eq!(
6151 state.constitution_preview_hash.as_deref(),
6152 Some(constitution.preview_hash().as_str())
6153 );
6154 let step = state.steps.get(&SetupStep::Constitution).expect("step");
6155 let result = step.result.as_deref().expect("result");
6156 assert!(result.contains("model-drafted constitution ratified (GLM-5.2)"));
6157 assert!(message.contains("Constitution ratified"));
6158 }
6159
6160 #[test]
6161 fn deterministic_ratification_records_guided_authoring() {
6162 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
6163
6164 view.handle_key(key(KeyCode::Char('g')));
6165 let action = view.handle_key(key(KeyCode::Char('g')));
6166
6167 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { state, .. }) =
6168 action
6169 else {
6170 panic!("expected guided commit event");
6171 };
6172 assert_eq!(
6173 state.constitution_authoring,
6174 Some(ConstitutionAuthoring::Guided)
6175 );
6176 }
6177
6178 #[test]
6179 fn cycling_answers_discards_the_model_draft() {
6180 let mut view = SetupWizardView::new_at_with_facts(
6181 SetupState::default(),
6182 Locale::En,
6183 SetupStep::Constitution,
6184 ready_facts("GLM-5.2"),
6185 );
6186 let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string());
6187
6188 // Changing any answer makes the model draft stale law.
6189 assert!(matches!(
6190 view.handle_key(key(KeyCode::Char('1'))),
6191 ViewAction::None
6192 ));
6193
6194 // The next G must preview afresh — and preview the guided rendering,
6195 // not the discarded model draft.
6196 let action = view.handle_key(key(KeyCode::Char('g')));
6197 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else {
6198 panic!("stale draft should force a fresh preview");
6199 };
6200 assert!(content.contains("Rendered deterministically"));
6201 assert!(!content.contains("Drafted by GLM-5.2"));
6202
6203 let action = view.handle_key(key(KeyCode::Char('g')));
6204 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested { state, .. }) =
6205 action
6206 else {
6207 panic!("expected guided commit after discard");
6208 };
6209 assert_eq!(
6210 state.constitution_authoring,
6211 Some(ConstitutionAuthoring::Guided)
6212 );
6213 }
6214
6215 #[test]
6216 fn freeform_note_discards_the_model_draft() {
6217 let mut view = SetupWizardView::new_at_with_facts(
6218 SetupState::default(),
6219 Locale::En,
6220 SetupStep::Constitution,
6221 ready_facts("GLM-5.2"),
6222 );
6223 let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string());
6224
6225 assert!(view.handle_paste("Prefer local examples before broad rewrites."));
6226
6227 let action = view.handle_key(key(KeyCode::Char('g')));
6228 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = action else {
6229 panic!("changed freeform note should force a fresh guided preview");
6230 };
6231 assert!(content.contains("Rendered deterministically"));
6232 assert!(content.contains("Prefer local examples"));
6233 assert!(!content.contains("Drafted by GLM-5.2"));
6234 }
6235
6236 #[test]
6237 fn constitution_card_gates_the_model_draft_invitation() {
6238 // No ready provider: no invitation (and the blocker-size layout holds).
6239 let not_ready = SetupWizardView::new(SetupState::default(), Locale::En);
6240 let text = lines_to_text(not_ready.constitution_detail_lines());
6241 assert!(!text.contains("can draft it"));
6242 assert!(!text.contains("awaits ratification"));
6243
6244 // Ready provider: the invitation names the first configured model.
6245 let ready = SetupWizardView::new_at_with_facts(
6246 SetupState::default(),
6247 Locale::En,
6248 SetupStep::Constitution,
6249 ready_facts("GLM-5.2"),
6250 );
6251 let text = lines_to_text(ready.constitution_detail_lines());
6252 assert!(text.contains("GLM-5.2 can draft it. You ratify it."));
6253
6254 // Installed draft: the card flips to the awaiting-ratification line.
6255 let mut with_draft = ready.clone();
6256 let _ = with_draft.install_model_draft(sample_model_draft(), "GLM-5.2".to_string());
6257 let text = lines_to_text(with_draft.constitution_detail_lines());
6258 assert!(text.contains("Draft by GLM-5.2 awaits ratification"));
6259 assert!(!text.contains("GLM-5.2 can draft it"));
6260 }
6261
6262 #[test]
6263 fn model_drafted_commit_round_trips_through_the_setup_transaction() {
6264 let _guard = crate::test_support::lock_test_env();
6265 let tmp = tempfile::TempDir::new().expect("tempdir");
6266 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
6267
6268 let mut view = SetupWizardView::new_at_with_facts(
6269 SetupState::default(),
6270 Locale::En,
6271 SetupStep::Constitution,
6272 ready_facts("GLM-5.2"),
6273 );
6274 let _ = view.install_model_draft(sample_model_draft(), "GLM-5.2".to_string());
6275 let ViewAction::EmitAndClose(ViewEvent::SetupConstitutionCommitRequested {
6276 constitution,
6277 state,
6278 ..
6279 }) = view.handle_key(key(KeyCode::Char('g')))
6280 else {
6281 panic!("expected ratification commit event");
6282 };
6283
6284 persist_user_constitution_choice(&constitution, &state).expect("persist");
6285
6286 let loaded = UserConstitution::load().expect("load constitution");
6287 let loaded = loaded.constitution().expect("valid constitution");
6288 assert_eq!(loaded.render_body(), constitution.render_body());
6289 let loaded_state = SetupState::load().expect("load state").expect("state");
6290 assert_eq!(
6291 loaded_state.constitution_authoring,
6292 Some(ConstitutionAuthoring::ModelDrafted)
6293 );
6294 assert_eq!(
6295 loaded_state.constitution_preview_hash.as_deref(),
6296 Some(constitution.preview_hash().as_str())
6297 );
6298 }
6299
6300 #[test]
6301 fn guided_constitution_template_localizes_content() {
6302 let english = guided_constitution_template(Locale::En).render_body();
6303 let zh_hans = guided_constitution_template(Locale::ZhHans).render_body();
6304
6305 assert!(english.contains("evidence-first coding workbench"));
6306 assert!(zh_hans.contains("重证据"));
6307 assert_ne!(english, zh_hans);
6308
6309 let markers = [
6310 (Locale::Ja, "証拠重視"),
6311 (Locale::ZhHans, "重证据"),
6312 (Locale::ZhHant, "重證據"),
6313 (Locale::PtBr, "guiada por evidências"),
6314 (Locale::Es419, "basada en evidencia"),
6315 (Locale::Vi, "ưu tiên bằng chứng"),
6316 (Locale::Ko, "근거 중심"),
6317 ];
6318 for (locale, marker) in markers {
6319 let body = guided_constitution_template(locale).render_body();
6320 assert!(
6321 body.contains(marker),
6322 "missing localized guided marker for {}",
6323 locale.tag()
6324 );
6325 assert_ne!(
6326 english,
6327 body,
6328 "locale {} fell back to English",
6329 locale.tag()
6330 );
6331 assert!(
6332 !body.contains("A Codewhale user who wants"),
6333 "locale {} reused English purpose copy",
6334 locale.tag()
6335 );
6336 assert!(
6337 !body.contains("Guided answers:"),
6338 "locale {} reused English guided-answer notes",
6339 locale.tag()
6340 );
6341 assert!(
6342 !body.contains("Current user requests and live tool evidence"),
6343 "locale {} reused English authority priority",
6344 locale.tag()
6345 );
6346 }
6347 }
6348
6349 #[test]
6350 fn ratification_preview_uses_rendered_block_and_layer_order() {
6351 let draft = GuidedConstitutionDraft::default();
6352 let english = constitution_ratification_text(
6353 Locale::En,
6354 &draft.to_constitution(Locale::En),
6355 &DraftProvenance::Guided,
6356 );
6357 let zh_hans = constitution_ratification_text(
6358 Locale::ZhHans,
6359 &draft.to_constitution(Locale::ZhHans),
6360 &DraftProvenance::Guided,
6361 );
6362
6363 assert!(english.contains("<codewhale_user_constitution"));
6364 assert!(english.contains("Layer order"));
6365 assert!(english.contains("press G to ratify and save"));
6366 // Framing: powers and limits, not case-by-case; continuity, not memory.
6367 assert!(english.contains("powers and limits rather than deciding every case"));
6368 assert!(english.contains("but it is not memory"));
6369 assert!(zh_hans.contains("<codewhale_user_constitution"));
6370 assert!(zh_hans.contains("按 G 确认并保存"));
6371 assert!(zh_hans.contains("它界定协作方式与行为边界"));
6372 assert!(zh_hans.contains("但它不是记忆"));
6373 assert_ne!(english, zh_hans);
6374
6375 let localized_markers = [
6376 (Locale::Ja, "権限の階層"),
6377 (Locale::ZhHans, "精简核心与可选策略"),
6378 (Locale::ZhHant, "精簡核心與可選模組"),
6379 (Locale::PtBr, "NÚCLEO REDUZIDO E MÓDULOS OPT-IN"),
6380 (Locale::Es419, "NÚCLEO REDUCIDO Y MÓDULOS OPT-IN"),
6381 (Locale::Vi, "LÕI RÚT GỌN VÀ MÔ-ĐUN OPT-IN"),
6382 (Locale::Ko, "축소된 코어와 옵트인 모듈"),
6383 ];
6384 for (locale, marker) in localized_markers {
6385 let content = constitution_ratification_text(
6386 locale,
6387 &draft.to_constitution(locale),
6388 &DraftProvenance::Guided,
6389 );
6390 assert!(
6391 content.contains(marker),
6392 "missing localized ratification marker for {}",
6393 locale.tag()
6394 );
6395 assert_ne!(
6396 english,
6397 content,
6398 "locale {} ratification preview fell back to English",
6399 locale.tag()
6400 );
6401 for fallback in [
6402 "CODEWHALE · USER CONSTITUTION",
6403 "HIERARCHY OF AUTHORITY",
6404 "WHAT THIS CANNOT DO",
6405 "REDUCED CORE AND OPT-IN MODULES",
6406 "Rendered deterministically from your guided answers",
6407 "Nothing becomes law until you confirm",
6408 ] {
6409 assert!(
6410 !content.contains(fallback),
6411 "locale {} reused English ratification scaffold: {fallback}",
6412 locale.tag()
6413 );
6414 }
6415 }
6416 }
6417
6418 #[test]
6419 fn ratification_preview_states_authority_boundaries_and_provenance() {
6420 let draft = GuidedConstitutionDraft::default();
6421 let constitution = draft.to_constitution(Locale::En);
6422
6423 let guided =
6424 constitution_ratification_text(Locale::En, &constitution, &DraftProvenance::Guided);
6425 assert!(guided.contains("HIERARCHY OF AUTHORITY"));
6426 assert!(guided.contains("WHAT THIS CANNOT DO"));
6427 assert!(guided.contains("cannot grant or change approval policy"));
6428 assert!(guided.contains("Nothing becomes law until you confirm"));
6429 assert!(guided.contains("Rendered deterministically"));
6430
6431 let drafted = constitution_ratification_text(
6432 Locale::En,
6433 &constitution,
6434 &DraftProvenance::Model("GLM-5.2".to_string()),
6435 );
6436 assert!(drafted.contains("Drafted by GLM-5.2"));
6437 assert!(drafted.contains("schema-checked and bounded by Codewhale"));
6438
6439 let zh = constitution_ratification_text(
6440 Locale::ZhHans,
6441 &draft.to_constitution(Locale::ZhHans),
6442 &DraftProvenance::Model("GLM-5.2".to_string()),
6443 );
6444 assert!(zh.contains("权限层级"));
6445 assert!(zh.contains("它不能做什么"));
6446 assert!(zh.contains("由 GLM-5.2 根据你的引导式答案起草"));
6447 }
6448
6449 #[test]
6450 fn guided_constitution_detail_lines_show_localized_answers() {
6451 let english = SetupWizardView::new(SetupState::default(), Locale::En);
6452 let english_text = lines_to_text(english.constitution_detail_lines());
6453 assert!(english_text.contains("Purpose:"));
6454 assert!(english_text.contains("coding workbench"));
6455 assert!(english_text.contains("Initiative:"));
6456 assert!(english_text.contains("balanced"));
6457 assert!(english_text.contains("Principles:"));
6458 assert!(english_text.contains("scoped changes"));
6459
6460 let zh_hans = SetupWizardView::new(SetupState::default(), Locale::ZhHans);
6461 let zh_hans_text = lines_to_text(zh_hans.constitution_detail_lines());
6462 assert!(zh_hans_text.contains("用途:"));
6463 assert!(zh_hans_text.contains("编码工作台"));
6464 assert!(zh_hans_text.contains("主动性:"));
6465 assert!(zh_hans_text.contains("平衡"));
6466 assert!(zh_hans_text.contains("原则:"));
6467 assert!(zh_hans_text.contains("小范围改动"));
6468
6469 for locale in Locale::shipped()
6470 .iter()
6471 .copied()
6472 .filter(|locale| *locale != Locale::En)
6473 {
6474 let view = SetupWizardView::new(SetupState::default(), locale);
6475 let text = lines_to_text(view.constitution_detail_lines());
6476 assert!(
6477 text.contains(&*GuidedPurpose::Coding.label(locale)),
6478 "missing localized purpose answer for {}",
6479 locale.tag()
6480 );
6481 assert!(
6482 text.contains(autonomy_label(AutonomyPreference::Balanced, locale)),
6483 "missing localized autonomy answer for {}",
6484 locale.tag()
6485 );
6486 assert!(
6487 text.contains(GuidedPrinciples::ScopedChanges.label(locale)),
6488 "missing localized principle answer for {}",
6489 locale.tag()
6490 );
6491 assert!(
6492 !text.contains("Purpose:"),
6493 "locale {} reused English detail label",
6494 locale.tag()
6495 );
6496 assert!(
6497 !text.contains("not checked yet"),
6498 "locale {} reused English file-state detail",
6499 locale.tag()
6500 );
6501 }
6502 }
6503
6504 #[test]
6505 fn constitution_file_state_labels_existing_override_states() {
6506 assert!(
6507 SetupConstitutionFileState::Missing
6508 .label(ConstitutionChoice::Bundled, Locale::En)
6509 .contains("no constitution.json")
6510 );
6511 assert!(
6512 SetupConstitutionFileState::Loaded
6513 .label(ConstitutionChoice::GuidedCustom, Locale::En)
6514 .contains("selected")
6515 );
6516 assert!(
6517 SetupConstitutionFileState::Loaded
6518 .label(ConstitutionChoice::Bundled, Locale::En)
6519 .contains("inactive")
6520 );
6521 assert!(
6522 SetupConstitutionFileState::Invalid
6523 .label(ConstitutionChoice::Unset, Locale::En)
6524 .contains("invalid")
6525 );
6526 assert!(
6527 SetupConstitutionFileState::Unreadable
6528 .label(ConstitutionChoice::Unset, Locale::En)
6529 .contains("unreadable")
6530 );
6531 assert!(
6532 SetupConstitutionFileState::PathError
6533 .label(ConstitutionChoice::Unset, Locale::ZhHans)
6534 .contains("CODEWHALE_HOME")
6535 );
6536 }
6537
6538 #[test]
6539 fn expert_override_state_requires_content_and_opt_in() {
6540 let _guard = crate::test_support::lock_test_env();
6541 let tmp = tempfile::TempDir::new().expect("tempdir");
6542 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
6543 let _opt_in = crate::test_support::EnvVarGuard::remove(BASE_PROMPT_OVERRIDE_OPT_IN_ENV);
6544
6545 assert_eq!(
6546 SetupExpertOverrideState::load(),
6547 SetupExpertOverrideState::Missing
6548 );
6549
6550 let path = tmp.path().join(CONSTITUTION_OVERRIDE_FILE);
6551 std::fs::create_dir_all(path.parent().expect("override parent")).expect("override parent");
6552 std::fs::write(&path, "\n \n").expect("write empty override");
6553 assert_eq!(
6554 SetupExpertOverrideState::load(),
6555 SetupExpertOverrideState::Empty
6556 );
6557
6558 std::fs::write(&path, "# Expert override\n").expect("write override");
6559 assert_eq!(
6560 SetupExpertOverrideState::load(),
6561 SetupExpertOverrideState::Disabled
6562 );
6563 assert!(!SetupExpertOverrideState::Disabled.is_active());
6564 assert!(
6565 SetupExpertOverrideState::Disabled
6566 .label(Locale::En)
6567 .contains(BASE_PROMPT_OVERRIDE_OPT_IN_ENV)
6568 );
6569
6570 // SAFETY: the process-wide test env mutex is held by `_guard`.
6571 unsafe { std::env::set_var(BASE_PROMPT_OVERRIDE_OPT_IN_ENV, "1") };
6572 assert_eq!(
6573 SetupExpertOverrideState::load(),
6574 SetupExpertOverrideState::Active
6575 );
6576 assert!(SetupExpertOverrideState::Active.is_active());
6577 }
6578
6579 #[test]
6580 fn constitution_detail_lines_show_existing_file_state() {
6581 let mut state = SetupState {
6582 constitution_choice: ConstitutionChoice::Bundled,
6583 constitution_source: ConstitutionSource::Bundled,
6584 constitution_validity: ConstitutionValidity::Valid,
6585 ..SetupState::default()
6586 };
6587 let facts = SetupRuntimeFacts {
6588 constitution_file: SetupConstitutionFileState::Loaded,
6589 ..SetupRuntimeFacts::default()
6590 };
6591 let view = SetupWizardView::new_at_with_facts(
6592 state.clone(),
6593 Locale::En,
6594 SetupStep::Constitution,
6595 facts,
6596 );
6597
6598 let text = lines_to_text(view.constitution_detail_lines());
6599 assert!(text.contains("Source: bundled; validity valid"));
6600 assert!(text.contains("Existing file:"));
6601 assert!(text.contains("inactive under the recorded choice"));
6602 assert!(text.contains("Expert override:"));
6603 assert!(text.contains("not checked yet"));
6604
6605 state.constitution_choice = ConstitutionChoice::GuidedCustom;
6606 state.constitution_source = ConstitutionSource::UserGlobal;
6607 let view = SetupWizardView::new_at_with_facts(
6608 state,
6609 Locale::ZhHans,
6610 SetupStep::Constitution,
6611 SetupRuntimeFacts {
6612 constitution_file: SetupConstitutionFileState::Loaded,
6613 ..SetupRuntimeFacts::default()
6614 },
6615 );
6616 let text = lines_to_text(view.constitution_detail_lines());
6617 assert!(text.contains("现有文件:"));
6618 assert!(text.contains("已存在并已选择"));
6619 assert!(text.contains("专家覆盖:"));
6620 }
6621
6622 #[test]
6623 fn setup_wizard_is_usable_and_opaque_at_blocker_sizes() {
6624 use crate::tui::views::ViewStack;
6625 use ratatui::{buffer::Buffer, layout::Rect};
6626 use unicode_width::UnicodeWidthStr;
6627
6628 const BLOCKER_SIZES: [(u16, u16); 4] = [(80, 24), (100, 30), (120, 32), (160, 40)];
6629 for (w, h) in BLOCKER_SIZES {
6630 let area = Rect::new(0, 0, w, h);
6631 let mut buf = Buffer::empty(area);
6632 for y in 0..h {
6633 for x in 0..w {
6634 buf[(x, y)].set_symbol("X");
6635 }
6636 }
6637 let mut stack = ViewStack::new();
6638 stack.push(SetupWizardView::new_at_with_facts(
6639 SetupState::default(),
6640 Locale::En,
6641 SetupStep::Constitution,
6642 SetupRuntimeFacts {
6643 constitution_file: SetupConstitutionFileState::Loaded,
6644 ..SetupRuntimeFacts::default()
6645 },
6646 ));
6647 stack.render(area, &mut buf);
6648
6649 let rows: Vec<String> = (0..h)
6650 .map(|y| (0..w).map(|x| buf[(x, y)].symbol().to_string()).collect())
6651 .collect();
6652 let text = rows.join("\n");
6653
6654 for label in [
6655 "Setup",
6656 "Choice:",
6657 "Existing file:",
6658 "Purpose:",
6659 "preview/ratify",
6660 "use bundled",
6661 "cancel",
6662 ] {
6663 assert!(text.contains(label), "{w}x{h}: missing '{label}'");
6664 }
6665 assert!(
6666 !text.contains('X'),
6667 "{w}x{h}: background bleed-through into setup modal"
6668 );
6669 assert!(
6670 [palette::WHALE_BG, palette::WHALE_PANEL].contains(&buf[(w / 2, h / 2)].bg),
6671 "{w}x{h}: modal interior must be opaque"
6672 );
6673 for (y, row) in rows.iter().enumerate() {
6674 assert!(
6675 UnicodeWidthStr::width(row.trim_end()) <= usize::from(w),
6676 "{w}x{h}: row {y} overflows width: {row:?}"
6677 );
6678 }
6679 }
6680 }
6681
6682 #[test]
6683 fn persist_user_constitution_choice_writes_constitution_and_state() {
6684 let _guard = crate::test_support::lock_test_env();
6685 let tmp = tempfile::TempDir::new().expect("tempdir");
6686 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
6687 let constitution = guided_constitution_template(Locale::En);
6688 let mut state = SetupState::default();
6689 state.complete_constitution_checkpoint(
6690 CONSTITUTION_CHECKPOINT_VERSION,
6691 ConstitutionChoice::GuidedCustom,
6692 );
6693 state.constitution_source = ConstitutionSource::UserGlobal;
6694 state.constitution_validity = ConstitutionValidity::Valid;
6695 state.constitution_preview_hash = Some(constitution.preview_hash());
6696 state.set_step(
6697 SetupStep::Constitution,
6698 StepEntry::new(StepStatus::Verified, true, CONSTITUTION_CHECKPOINT_VERSION),
6699 );
6700
6701 persist_user_constitution_choice(&constitution, &state).expect("persist constitution");
6702
6703 let loaded_constitution = UserConstitution::load().expect("load constitution");
6704 assert!(matches!(
6705 loaded_constitution,
6706 UserConstitutionLoad::Loaded(_)
6707 ));
6708 let loaded_state = SetupState::load()
6709 .expect("load setup state")
6710 .expect("setup state");
6711 assert_eq!(
6712 loaded_state.constitution_choice,
6713 ConstitutionChoice::GuidedCustom
6714 );
6715 assert_eq!(
6716 loaded_state
6717 .constitution_checkpoint_completed_for
6718 .as_deref(),
6719 Some(CONSTITUTION_CHECKPOINT_VERSION)
6720 );
6721 }
6722
6723 #[test]
6724 fn keep_existing_constitution_previews_then_completes_without_rewriting() {
6725 let _guard = crate::test_support::lock_test_env();
6726 let tmp = tempfile::TempDir::new().expect("tempdir");
6727 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
6728
6729 // An existing valid custom constitution from a prior version.
6730 let existing = guided_constitution_template(Locale::En);
6731 persist_user_constitution_choice(&existing, &SetupState::default())
6732 .expect("write existing constitution");
6733 let path = UserConstitution::path().expect("constitution path");
6734 let bytes_before = std::fs::read(&path).expect("existing file bytes");
6735
6736 let facts = SetupRuntimeFacts {
6737 constitution_file: SetupConstitutionFileState::Loaded,
6738 ..SetupRuntimeFacts::default()
6739 };
6740 let mut view = SetupWizardView::new_at_with_facts(
6741 SetupState::default(),
6742 Locale::En,
6743 SetupStep::Constitution,
6744 facts,
6745 );
6746
6747 // The card offers the keep path.
6748 let text = lines_to_text(view.constitution_detail_lines());
6749 assert!(text.contains("K Keep your existing constitution"), "{text}");
6750
6751 // First K previews the existing law, unchanged, with keep wording.
6752 let action = view.handle_key(key(KeyCode::Char('k')));
6753 let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else {
6754 panic!("expected keep-existing preview event");
6755 };
6756 assert!(title.contains("Draft for Ratification"));
6757 assert!(content.contains("shown unchanged"), "{content}");
6758 assert!(content.contains("press K to keep it"), "{content}");
6759 assert!(
6760 content.contains("<codewhale_user_constitution"),
6761 "{content}"
6762 );
6763
6764 // Second K completes the checkpoint without touching the file.
6765 let action = view.handle_key(key(KeyCode::Char('k')));
6766 let ViewAction::EmitAndClose(ViewEvent::SetupStateCommitRequested { state, message }) =
6767 action
6768 else {
6769 panic!("expected keep-existing commit event");
6770 };
6771 assert_eq!(state.constitution_choice, ConstitutionChoice::GuidedCustom);
6772 assert_eq!(state.constitution_source, ConstitutionSource::UserGlobal);
6773 assert_eq!(state.constitution_validity, ConstitutionValidity::Valid);
6774 assert_eq!(
6775 state.constitution_checkpoint_completed_for.as_deref(),
6776 Some(CONSTITUTION_CHECKPOINT_VERSION)
6777 );
6778 assert_eq!(
6779 state.constitution_preview_hash.as_deref(),
6780 Some(existing.preview_hash().as_str())
6781 );
6782 assert_eq!(state.status(SetupStep::Constitution), StepStatus::Verified);
6783 assert!(message.contains("Constitution kept"), "{message}");
6784
6785 let bytes_after = std::fs::read(&path).expect("file bytes after keep");
6786 assert_eq!(bytes_before, bytes_after, "keep must not rewrite the file");
6787 }
6788
6789 #[test]
6790 fn keep_key_is_inert_without_a_valid_existing_constitution() {
6791 let _guard = crate::test_support::lock_test_env();
6792 let tmp = tempfile::TempDir::new().expect("tempdir");
6793 let _home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", tmp.path());
6794
6795 for file_state in [
6796 SetupConstitutionFileState::Missing,
6797 SetupConstitutionFileState::Invalid,
6798 SetupConstitutionFileState::Empty,
6799 ] {
6800 let facts = SetupRuntimeFacts {
6801 constitution_file: file_state,
6802 ..SetupRuntimeFacts::default()
6803 };
6804 let mut view = SetupWizardView::new_at_with_facts(
6805 SetupState::default(),
6806 Locale::En,
6807 SetupStep::Constitution,
6808 facts,
6809 );
6810 let text = lines_to_text(view.constitution_detail_lines());
6811 assert!(
6812 !text.contains("K Keep your existing constitution"),
6813 "{file_state:?} must not offer keep: {text}"
6814 );
6815 assert!(
6816 matches!(view.handle_key(key(KeyCode::Char('k'))), ViewAction::None),
6817 "{file_state:?} must leave K inert"
6818 );
6819 }
6820 }
6821
6822 #[test]
6823 fn provider_model_review_records_ready_route_and_continues() {
6824 let facts = SetupRuntimeFacts {
6825 provider: "DeepSeek".to_string(),
6826 model: "deepseek-v4-pro".to_string(),
6827 auth: "present".to_string(),
6828 health: "ready".to_string(),
6829 provider_ready: true,
6830 provider_result:
6831 "provider=deepseek, model=deepseek-v4-pro, auth=present/local, health=not checked"
6832 .to_string(),
6833 ..SetupRuntimeFacts::default()
6834 };
6835 let mut view = SetupWizardView::new_at_with_facts(
6836 SetupState::default(),
6837 Locale::En,
6838 SetupStep::ProviderModel,
6839 facts,
6840 );
6841
6842 let action = view.handle_key(key(KeyCode::Enter));
6843
6844 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
6845 else {
6846 panic!("expected setup-state commit event");
6847 };
6848 assert_eq!(state.status(SetupStep::ProviderModel), StepStatus::Verified);
6849 assert_eq!(view.selected_step(), SetupStep::TrustSandbox);
6850 assert!(message.contains("Provider/model readiness recorded"));
6851 }
6852
6853 #[test]
6854 fn provider_model_review_records_missing_auth_as_needs_action() {
6855 let facts = SetupRuntimeFacts {
6856 provider_ready: false,
6857 provider_result:
6858 "provider=deepseek, model=deepseek-v4-pro, auth=missing, health=needs action"
6859 .to_string(),
6860 ..SetupRuntimeFacts::default()
6861 };
6862 let mut view = SetupWizardView::new_at_with_facts(
6863 SetupState::default(),
6864 Locale::En,
6865 SetupStep::ProviderModel,
6866 facts,
6867 );
6868
6869 let action = view.handle_key(key(KeyCode::Enter));
6870
6871 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
6872 else {
6873 panic!("expected setup-state commit event");
6874 };
6875 assert_eq!(
6876 state.status(SetupStep::ProviderModel),
6877 StepStatus::NeedsAction
6878 );
6879 assert!(message.contains("needs action"));
6880 }
6881
6882 #[test]
6883 fn observed_provider_failure_records_needs_action_not_verified() {
6884 let _guard = crate::test_support::lock_test_env();
6885 let tmp = tempfile::TempDir::new().expect("tempdir");
6886 let workspace = tmp.path().join("workspace");
6887 std::fs::create_dir_all(&workspace).expect("workspace");
6888 let codewhale_home = tmp.path().join(".codewhale");
6889 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
6890 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
6891 let _codewhale_home =
6892 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
6893 let _deepseek_env = crate::test_support::EnvVarGuard::remove("DEEPSEEK_API_KEY");
6894 let config = Config {
6895 api_key: Some("saved-deepseek-key".to_string()),
6896 ..Default::default()
6897 };
6898 let mut app = App::new(setup_test_options(workspace), &config);
6899 app.api_provider = crate::config::ApiProvider::Deepseek;
6900 app.model = "deepseek-v4-pro".to_string();
6901 app.provider_health.record_failure_message(
6902 &config,
6903 crate::config::ApiProvider::Deepseek,
6904 "deepseek-v4-pro",
6905 crate::error_taxonomy::ErrorCategory::Authentication,
6906 "credential rejected",
6907 );
6908
6909 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
6910 assert!(!facts.provider_ready);
6911 assert!(facts.auth.contains("last check failed"), "{}", facts.auth);
6912 assert!(facts.provider_result.contains("health=needs action"));
6913
6914 let mut view = SetupWizardView::new_at_with_facts(
6915 SetupState::default(),
6916 Locale::En,
6917 SetupStep::ProviderModel,
6918 facts,
6919 );
6920 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) =
6921 view.handle_key(key(KeyCode::Enter))
6922 else {
6923 panic!("expected setup-state commit event");
6924 };
6925 assert_eq!(
6926 state.status(SetupStep::ProviderModel),
6927 StepStatus::NeedsAction
6928 );
6929 }
6930
6931 #[test]
6932 fn runtime_posture_review_confirms_without_config_mutation() {
6933 let facts = SetupRuntimeFacts {
6934 runtime_result: "intent=agent, approval=suggest, shell=enabled, trust=workspace, sandbox=default, network=prompt by default".to_string(),
6935 ..SetupRuntimeFacts::default()
6936 };
6937 let mut view = SetupWizardView::new_at_with_facts(
6938 SetupState::default(),
6939 Locale::En,
6940 SetupStep::TrustSandbox,
6941 facts,
6942 );
6943
6944 let action = view.handle_key(key(KeyCode::Enter));
6945
6946 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
6947 else {
6948 panic!("expected setup-state commit event");
6949 };
6950 assert_eq!(state.status(SetupStep::TrustSandbox), StepStatus::Verified);
6951 assert_eq!(
6952 state.runtime_posture_source,
6953 RuntimePostureSource::Confirmed
6954 );
6955 assert!(message.contains("Runtime posture reviewed"));
6956 assert_eq!(view.selected_step(), SetupStep::Constitution);
6957 }
6958
6959 #[test]
6960 fn runtime_posture_review_result_redacts_secret_config() {
6961 let _guard = crate::test_support::lock_test_env();
6962 let tmp = tempfile::TempDir::new().expect("tempdir");
6963 let workspace = tmp.path().join("workspace");
6964 std::fs::create_dir_all(&workspace).expect("workspace dir");
6965 let codewhale_home = tmp.path().join(".codewhale");
6966 let _home = crate::test_support::EnvVarGuard::set("HOME", tmp.path());
6967 let _userprofile = crate::test_support::EnvVarGuard::set("USERPROFILE", tmp.path());
6968 let _codewhale_home =
6969 crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
6970
6971 let mut config = Config {
6972 api_key: Some("sk-runtime-posture-secret".to_string()),
6973 sandbox_api_key: Some("sandbox-runtime-secret".to_string()),
6974 approval_policy: Some("on-request".to_string()),
6975 sandbox_mode: Some("workspace-write".to_string()),
6976 ..Config::default()
6977 };
6978 config.default_text_model = Some("deepseek-v4-pro".to_string());
6979 let app = App::new(setup_test_options(workspace), &config);
6980 let facts = SetupRuntimeFacts::from_app_config(&app, &config);
6981 let mut view = SetupWizardView::new_at_with_facts(
6982 SetupState::default(),
6983 Locale::En,
6984 SetupStep::TrustSandbox,
6985 facts,
6986 );
6987
6988 let action = view.handle_key(key(KeyCode::Enter));
6989
6990 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = action else {
6991 panic!("expected runtime posture commit event");
6992 };
6993 let result = state
6994 .steps
6995 .get(&SetupStep::TrustSandbox)
6996 .and_then(|entry| entry.result.as_deref())
6997 .expect("runtime posture result");
6998 assert!(result.contains("intent=agent"), "{result}");
6999 assert!(result.contains("sandbox=workspace-write"), "{result}");
7000 for forbidden in [
7001 "sk-runtime-posture-secret",
7002 "sandbox-runtime-secret",
7003 "api_key",
7004 "sandbox_api_key",
7005 "secret",
7006 ] {
7007 assert!(
7008 !result.contains(forbidden),
7009 "runtime posture result leaked {forbidden}: {result}"
7010 );
7011 }
7012 }
7013
7014 #[test]
7015 fn runtime_posture_skip_records_posture_specific_state() {
7016 let mut view = SetupWizardView::new_at_with_facts(
7017 SetupState::default(),
7018 Locale::En,
7019 SetupStep::TrustSandbox,
7020 SetupRuntimeFacts::default(),
7021 );
7022
7023 let action = view.handle_key(key(KeyCode::Char('s')));
7024
7025 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7026 else {
7027 panic!("expected runtime posture skip commit event");
7028 };
7029 let entry = state
7030 .steps
7031 .get(&SetupStep::TrustSandbox)
7032 .expect("trust/sandbox step entry");
7033 assert_eq!(entry.status, StepStatus::Skipped);
7034 assert!(entry.required);
7035 assert_eq!(entry.result.as_deref(), Some("skipped by user"));
7036 assert_eq!(state.runtime_posture_source, RuntimePostureSource::Unset);
7037 assert!(message.contains("skipped"));
7038 assert_eq!(view.selected_step(), SetupStep::Constitution);
7039 }
7040
7041 #[test]
7042 fn runtime_posture_detail_lines_show_preset_diff() {
7043 let facts = SetupRuntimeFacts {
7044 default_mode: "agent".to_string(),
7045 approval_policy_value: "on-request".to_string(),
7046 allow_shell_enabled: true,
7047 sandbox_mode_value: "workspace-write".to_string(),
7048 network_default_value: "prompt".to_string(),
7049 trust: "workspace trust not elevated".to_string(),
7050 ..SetupRuntimeFacts::default()
7051 };
7052 let view = SetupWizardView::new_at_with_facts(
7053 SetupState::default(),
7054 Locale::En,
7055 SetupStep::TrustSandbox,
7056 facts,
7057 );
7058
7059 let text = lines_to_text(view.runtime_posture_detail_lines());
7060
7061 assert!(text.contains("Selected preset:"));
7062 assert!(text.contains("Normal agent"));
7063 assert!(text.contains("settings.default_mode: agent -> act"));
7064 assert!(text.contains("config.allow_shell: true -> true"));
7065 assert!(text.contains("Safety floor:"));
7066 assert!(text.contains("Press A to preview"));
7067 }
7068
7069 #[test]
7070 fn runtime_posture_detail_lines_warn_about_project_overrides() {
7071 let tmp = tempfile::TempDir::new().expect("workspace");
7072 let project_dir = tmp.path().join(codewhale_config::CODEWHALE_APP_DIR);
7073 std::fs::create_dir_all(&project_dir).expect("project config dir");
7074 std::fs::write(
7075 project_dir.join("config.toml"),
7076 "approval_policy = \"never\"\nsandbox_mode = \"read-only\"\n",
7077 )
7078 .expect("project config");
7079 let warning =
7080 project_runtime_override_warning(tmp.path(), Locale::En).expect("project warning");
7081 let facts = SetupRuntimeFacts {
7082 project_override_warning: Some(warning),
7083 ..SetupRuntimeFacts::default()
7084 };
7085 let view = SetupWizardView::new_at_with_facts(
7086 SetupState::default(),
7087 Locale::En,
7088 SetupStep::TrustSandbox,
7089 facts,
7090 );
7091
7092 let text = lines_to_text(view.runtime_posture_detail_lines());
7093
7094 assert!(text.contains("Project override:"));
7095 assert!(text.contains("approval_policy=never"));
7096 assert!(text.contains("sandbox_mode=read-only"));
7097 assert!(text.contains("project override warning"));
7098 assert!(text.contains("project config can still tighten"));
7099 }
7100
7101 #[test]
7102 fn operate_fleet_detail_lines_show_read_only_facts() {
7103 let facts = SetupRuntimeFacts {
7104 provider: "DeepSeek".to_string(),
7105 model: "deepseek-v4-pro".to_string(),
7106 auth: "present".to_string(),
7107 provider_ready: true,
7108 operate_runtime_ready: true,
7109 operate_runtime_result: "worker runtime enabled for deepseek; max_subagents=4, launch_concurrency=2, admission=6".to_string(),
7110 fleet_roster_ready: true,
7111 fleet_roster_result: "3 Fleet members (1 config/workspace)".to_string(),
7112 operate_concurrency_result:
7113 "configured launch_concurrency=2; max_subagents=4; admission=6; plan limit not probed"
7114 .to_string(),
7115 ..SetupRuntimeFacts::default()
7116 };
7117 let view = SetupWizardView::new_at_with_facts(
7118 first_run_ready_state(),
7119 Locale::En,
7120 SetupStep::OperateFleet,
7121 facts,
7122 );
7123
7124 let text = lines_to_text(view.operate_fleet_detail_lines());
7125
7126 assert!(text.contains("Worker runtime:"));
7127 assert!(text.contains("worker runtime enabled for deepseek"));
7128 assert!(text.contains("Fleet roster:"));
7129 assert!(text.contains("3 Fleet members"));
7130 assert!(text.contains("plan limit not probed"));
7131 assert!(text.contains("Enter records this setup snapshot."));
7132 }
7133
7134 #[test]
7135 fn operate_fleet_review_records_needs_action_without_receipt_capability() {
7136 let facts = SetupRuntimeFacts {
7137 provider_ready: true,
7138 operate_runtime_ready: true,
7139 fleet_roster_ready: true,
7140 operate_result:
7141 "provider=ready, runtime=ready, roster=ready, concurrency=configured launch_concurrency=2; max_subagents=4; admission=6; plan limit not probed"
7142 .to_string(),
7143 ..SetupRuntimeFacts::default()
7144 };
7145 let mut view = SetupWizardView::new_at_with_facts(
7146 first_run_ready_state(),
7147 Locale::En,
7148 SetupStep::OperateFleet,
7149 facts,
7150 );
7151
7152 let action = view.handle_key(key(KeyCode::Enter));
7153
7154 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7155 else {
7156 panic!("expected setup-state commit event");
7157 };
7158 assert_eq!(
7159 state.status(SetupStep::OperateFleet),
7160 StepStatus::NeedsAction
7161 );
7162 assert!(!state.operate_ready());
7163 let result = state
7164 .steps
7165 .get(&SetupStep::OperateFleet)
7166 .and_then(|entry| entry.result.as_deref())
7167 .expect("operate result");
7168 assert!(result.contains("plan limit not probed"), "{result}");
7169 assert!(message.contains("needs action"));
7170 assert_eq!(view.selected_step(), SetupStep::Hotbar);
7171 }
7172
7173 #[test]
7174 fn hotbar_detail_lines_show_read_only_config_facts() {
7175 let facts = SetupRuntimeFacts {
7176 hotbar_bindings_result: "customized; configured_slots=2; active_slots=2; warnings=0"
7177 .to_string(),
7178 hotbar_actions_result: "13 bindable actions registered".to_string(),
7179 ..SetupRuntimeFacts::default()
7180 };
7181 let view = SetupWizardView::new_at_with_facts(
7182 SetupState::default(),
7183 Locale::En,
7184 SetupStep::Hotbar,
7185 facts,
7186 );
7187
7188 let text = lines_to_text(view.hotbar_detail_lines());
7189
7190 assert!(text.contains("Hotbar bindings:"));
7191 assert!(text.contains("configured_slots=2"));
7192 assert!(text.contains("Bindable actions:"));
7193 assert!(text.contains("13 bindable actions"));
7194 assert!(text.contains("Enter records this setup snapshot. Press H to customize slots."));
7195 }
7196
7197 #[test]
7198 fn hotbar_review_records_optional_snapshot() {
7199 let facts = SetupRuntimeFacts {
7200 hotbar_result:
7201 "state=customized, configured_slots=2, active_slots=2, actions=13, warnings=0"
7202 .to_string(),
7203 ..SetupRuntimeFacts::default()
7204 };
7205 let mut view = SetupWizardView::new_at_with_facts(
7206 SetupState::default(),
7207 Locale::En,
7208 SetupStep::Hotbar,
7209 facts,
7210 );
7211
7212 let action = view.handle_key(key(KeyCode::Enter));
7213
7214 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7215 else {
7216 panic!("expected setup-state commit event");
7217 };
7218 assert_eq!(state.status(SetupStep::Hotbar), StepStatus::Verified);
7219 let entry = state
7220 .steps
7221 .get(&SetupStep::Hotbar)
7222 .expect("hotbar setup entry");
7223 assert!(!entry.required);
7224 assert!(
7225 entry
7226 .result
7227 .as_deref()
7228 .is_some_and(|result| result.contains("state=customized"))
7229 );
7230 assert!(message.contains("Hotbar setup state recorded"));
7231 assert_eq!(view.selected_step(), SetupStep::ToolsMcp);
7232 }
7233
7234 #[test]
7235 fn tools_mcp_detail_lines_show_read_only_inventory_facts() {
7236 let facts = SetupRuntimeFacts {
7237 tools_mcp_servers_result: "configured — 2 configured (2 configuration valid, 0 needs_config, 0 off; global present at /tmp/mcp.json; project missing at /tmp/project/.codewhale/mcp.json); live health not checked — servers not started; configuration valid: docs, search".to_string(),
7238 tools_mcp_skills_result: "healthy — 3 discovered (hotbar skill sources), 3 on disk at /tmp/skills".to_string(),
7239 tools_mcp_tools_result: "healthy — 1 entries, 0 script-plugin tools at /tmp/tools".to_string(),
7240 tools_mcp_plugins_result: "off — nothing configured yet (missing at /tmp/plugins); optional".to_string(),
7241 tools_mcp_hotbar_result: "healthy — shared adapters: mcp_actions=0, skill_actions=3, plugin_actions=0 (deferred), slash_actions=12".to_string(),
7242 ..SetupRuntimeFacts::default()
7243 };
7244 let view = SetupWizardView::new_at_with_facts(
7245 SetupState::default(),
7246 Locale::En,
7247 SetupStep::ToolsMcp,
7248 facts,
7249 );
7250
7251 let text = lines_to_text(view.tools_mcp_detail_lines());
7252
7253 assert!(text.contains("MCP servers:"));
7254 assert!(text.contains("configured"));
7255 assert!(text.contains("live health not checked"));
7256 assert!(text.contains("/tmp/mcp.json"));
7257 assert!(text.contains("/tmp/project/.codewhale/mcp.json"));
7258 assert!(text.contains("Skills:"));
7259 assert!(text.contains("/tmp/skills"));
7260 assert!(text.contains("Tools dir:"));
7261 assert!(text.contains("Plugins:"));
7262 assert!(text.contains("Hotbar sources:"));
7263 assert!(text.contains("shared adapters"));
7264 assert!(text.contains("Enter records this setup snapshot."));
7265 assert!(text.contains("Press R for safe on-ramps"));
7266 }
7267
7268 #[test]
7269 fn tools_mcp_review_records_optional_snapshot_when_empty() {
7270 let facts = SetupRuntimeFacts {
7271 tools_mcp_result:
7272 "mcp=off, skills=off, tools=off, plugins=off, hotbar_sources=shared adapters: mcp_actions=0, overall=off, mode=read_only_safe_probe"
7273 .to_string(),
7274 tools_mcp_needs_action: false,
7275 ..SetupRuntimeFacts::default()
7276 };
7277 let mut view = SetupWizardView::new_at_with_facts(
7278 SetupState::default(),
7279 Locale::En,
7280 SetupStep::ToolsMcp,
7281 facts,
7282 );
7283
7284 let action = view.handle_key(key(KeyCode::Enter));
7285
7286 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7287 else {
7288 panic!("expected setup-state commit event");
7289 };
7290 assert_eq!(state.status(SetupStep::ToolsMcp), StepStatus::Optional);
7291 let entry = state
7292 .steps
7293 .get(&SetupStep::ToolsMcp)
7294 .expect("tools/mcp setup entry");
7295 assert!(!entry.required);
7296 assert!(
7297 entry
7298 .result
7299 .as_deref()
7300 .is_some_and(|result| result.contains("mode=read_only_safe_probe"))
7301 );
7302 assert!(message.contains("Tools/MCP readiness recorded"));
7303 assert_eq!(view.selected_step(), SetupStep::RemoteRuntime);
7304 }
7305
7306 #[test]
7307 fn tools_mcp_review_records_needs_action_for_broken_config() {
7308 let facts = SetupRuntimeFacts {
7309 tools_mcp_result:
7310 "mcp=needs_config, skills=off, tools=off, plugins=off, overall=needs_config, mode=read_only_safe_probe"
7311 .to_string(),
7312 tools_mcp_needs_action: true,
7313 ..SetupRuntimeFacts::default()
7314 };
7315 let mut view = SetupWizardView::new_at_with_facts(
7316 SetupState::default(),
7317 Locale::En,
7318 SetupStep::ToolsMcp,
7319 facts,
7320 );
7321
7322 let action = view.handle_key(key(KeyCode::Enter));
7323 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7324 else {
7325 panic!("expected setup-state commit event");
7326 };
7327 assert_eq!(state.status(SetupStep::ToolsMcp), StepStatus::NeedsAction);
7328 assert!(
7329 !state
7330 .steps
7331 .get(&SetupStep::ToolsMcp)
7332 .expect("entry")
7333 .required
7334 );
7335 assert!(message.contains("needs action") || message.contains("Tools/MCP"));
7336 // Optional step still advances; first-run is not blocked.
7337 assert_eq!(view.selected_step(), SetupStep::RemoteRuntime);
7338 }
7339
7340 #[test]
7341 fn tools_mcp_on_ramp_preview_is_safe() {
7342 let facts = SetupRuntimeFacts {
7343 tools_mcp_servers_result: "off — nothing configured".into(),
7344 tools_mcp_skills_result: "off — missing".into(),
7345 tools_mcp_tools_result: "off — missing".into(),
7346 tools_mcp_plugins_result: "off — missing".into(),
7347 tools_mcp_hotbar_result: "off — shared adapters".into(),
7348 tools_mcp_path_display: "~/.codewhale/mcp.json".into(),
7349 tools_mcp_skills_path_display: "~/.codewhale/skills".into(),
7350 tools_mcp_plugins_path_display: "~/.codewhale/plugins".into(),
7351 ..SetupRuntimeFacts::default()
7352 };
7353 let mut view = SetupWizardView::new_at_with_facts(
7354 SetupState::default(),
7355 Locale::En,
7356 SetupStep::ToolsMcp,
7357 facts,
7358 );
7359
7360 let action = view.handle_key(key(KeyCode::Char('r')));
7361 let ViewAction::Emit(ViewEvent::OpenTextPager { title, content }) = action else {
7362 panic!("expected on-ramp pager, got {action:?}");
7363 };
7364 assert!(title.to_ascii_lowercase().contains("tool") || title.contains("MCP"));
7365 assert!(content.contains("/mcp") || content.contains("mcp init"));
7366 assert!(!content.contains("sk-"));
7367 }
7368
7369 /// #3409: the card answers "where can this be reached from?" with one row
7370 /// per observed mode. The registry counts it used to list are still
7371 /// reachable — they moved into the `R` preview — so the card itself stays
7372 /// four plain lines plus the active route.
7373 #[test]
7374 fn remote_runtime_detail_lines_show_one_row_per_observed_mode() {
7375 let facts = SetupRuntimeFacts {
7376 remote_modes: vec![
7377 remote::RemoteModeFact {
7378 mode: remote::RemoteMode::LocalOnly,
7379 status: remote::RemoteModeStatus::Ready,
7380 detail: "this machine only; nothing is exposed".to_string(),
7381 },
7382 remote::RemoteModeFact {
7383 mode: remote::RemoteMode::MobileLan,
7384 status: remote::RemoteModeStatus::Disabled,
7385 detail: "runtime binds 127.0.0.1 only".to_string(),
7386 },
7387 ],
7388 remote_providers_result:
7389 "12 providers from the provider registry; active route deepseek / deepseek-chat"
7390 .to_string(),
7391 ..SetupRuntimeFacts::default()
7392 };
7393 let view = SetupWizardView::new_at_with_facts(
7394 SetupState::default(),
7395 Locale::En,
7396 SetupStep::RemoteRuntime,
7397 facts,
7398 );
7399
7400 let text = lines_to_text(view.remote_runtime_detail_lines());
7401
7402 assert!(text.contains("This machine only:"));
7403 assert!(text.contains("ready · this machine only; nothing is exposed"));
7404 assert!(text.contains("Phone on your network:"));
7405 assert!(text.contains("not available · runtime binds 127.0.0.1 only"));
7406 assert!(text.contains("Providers:"));
7407 assert!(text.contains("deepseek-chat"));
7408 // Local-only is always usable, so the hint says Enter alone is enough.
7409 assert!(text.contains("Enter keeps local-only."));
7410 }
7411
7412 /// Before facts load there are no modes to show; the card falls back to the
7413 /// single mode line rather than rendering an empty panel.
7414 #[test]
7415 fn remote_runtime_detail_lines_fall_back_when_no_mode_is_observed_yet() {
7416 let facts = SetupRuntimeFacts {
7417 remote_mode_result: "remote setup mode not loaded".to_string(),
7418 ..SetupRuntimeFacts::default()
7419 };
7420 let view = SetupWizardView::new_at_with_facts(
7421 SetupState::default(),
7422 Locale::En,
7423 SetupStep::RemoteRuntime,
7424 facts,
7425 );
7426
7427 let text = lines_to_text(view.remote_runtime_detail_lines());
7428 assert!(text.contains("Remote mode:"));
7429 assert!(text.contains("remote setup mode not loaded"));
7430 }
7431
7432 /// A missing token records `NeedsAction` — surfaced, never blocking.
7433 #[test]
7434 fn remote_runtime_review_records_needs_action_without_blocking_ready() {
7435 let facts = SetupRuntimeFacts {
7436 remote_needs_action: true,
7437 remote_result: "runtime_api=needs_action".to_string(),
7438 ..SetupRuntimeFacts::default()
7439 };
7440 let mut view = SetupWizardView::new_at_with_facts(
7441 SetupState::default(),
7442 Locale::En,
7443 SetupStep::RemoteRuntime,
7444 facts,
7445 );
7446 let _ = view.commit_remote_runtime_review();
7447
7448 let entry = view.state().status(SetupStep::RemoteRuntime);
7449 assert_eq!(entry, StepStatus::NeedsAction);
7450 assert!(entry.is_settled(), "needs-action must not block ready");
7451 }
7452
7453 #[test]
7454 fn remote_runtime_review_records_optional_snapshot() {
7455 let facts = SetupRuntimeFacts {
7456 remote_result:
7457 "clouds=3, bridges=2, providers=12, mode=generate_only, apply=not_implemented"
7458 .to_string(),
7459 ..SetupRuntimeFacts::default()
7460 };
7461 let mut view = SetupWizardView::new_at_with_facts(
7462 SetupState::default(),
7463 Locale::En,
7464 SetupStep::RemoteRuntime,
7465 facts,
7466 );
7467
7468 let action = view.handle_key(key(KeyCode::Enter));
7469
7470 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7471 else {
7472 panic!("expected setup-state commit event");
7473 };
7474 assert_eq!(state.status(SetupStep::RemoteRuntime), StepStatus::Verified);
7475 let entry = state
7476 .steps
7477 .get(&SetupStep::RemoteRuntime)
7478 .expect("remote setup entry");
7479 assert!(!entry.required);
7480 assert!(
7481 entry
7482 .result
7483 .as_deref()
7484 .is_some_and(|result| result.contains("mode=generate_only"))
7485 );
7486 assert!(message.contains("Remote runtime on-ramp recorded"));
7487 assert_eq!(view.selected_step(), SetupStep::Persistence);
7488 }
7489
7490 #[test]
7491 fn persistence_detail_lines_show_read_only_path_facts() {
7492 let facts = SetupRuntimeFacts {
7493 persistence: SetupPersistenceFacts {
7494 home_result: "explicit CODEWHALE_HOME at /tmp/cw-home (present)".to_string(),
7495 config_result: "/tmp/cw-home/config.toml (present)".to_string(),
7496 state_result: "/tmp/cw-home/setup_state.json (missing)".to_string(),
7497 constitution_result: "/tmp/cw-home/constitution.json (present)".to_string(),
7498 memory_result: "/tmp/cw-home/memory.md (missing)".to_string(),
7499 notes_result: "/tmp/cw-home/notes.md (exists-not-file)".to_string(),
7500 result: "home_source=explicit, home=present, config=present, setup_state=missing, constitution=present, memory=missing, notes=exists-not-file, mode=read_only_review".to_string(),
7501 },
7502 ..SetupRuntimeFacts::default()
7503 };
7504 let view = SetupWizardView::new_at_with_facts(
7505 SetupState::default(),
7506 Locale::En,
7507 SetupStep::Persistence,
7508 facts,
7509 );
7510
7511 let text = lines_to_text(view.persistence_detail_lines());
7512
7513 assert!(text.contains("Home:"));
7514 assert!(text.contains("explicit CODEWHALE_HOME"));
7515 assert!(text.contains("/tmp/cw-home/config.toml"));
7516 assert!(text.contains("/tmp/cw-home/setup_state.json (missing)"));
7517 assert!(text.contains("Constitution:"));
7518 assert!(text.contains("Memory:"));
7519 assert!(text.contains("Notes:"));
7520 assert!(text.contains("Enter records this setup snapshot."));
7521 }
7522
7523 #[test]
7524 fn persistence_review_records_optional_snapshot() {
7525 let facts = SetupRuntimeFacts {
7526 persistence: SetupPersistenceFacts {
7527 result: "home_source=explicit, home=present, config=present, setup_state=missing, constitution=present, memory=missing, notes=missing, mode=read_only_review".to_string(),
7528 ..SetupPersistenceFacts::default()
7529 },
7530 ..SetupRuntimeFacts::default()
7531 };
7532 let mut view = SetupWizardView::new_at_with_facts(
7533 SetupState::default(),
7534 Locale::En,
7535 SetupStep::Persistence,
7536 facts,
7537 );
7538
7539 let action = view.handle_key(key(KeyCode::Enter));
7540
7541 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7542 else {
7543 panic!("expected setup-state commit event");
7544 };
7545 assert_eq!(state.status(SetupStep::Persistence), StepStatus::Verified);
7546 let entry = state
7547 .steps
7548 .get(&SetupStep::Persistence)
7549 .expect("persistence setup entry");
7550 assert!(!entry.required);
7551 assert!(
7552 entry
7553 .result
7554 .as_deref()
7555 .is_some_and(|result| result.contains("mode=read_only_review"))
7556 );
7557 assert!(message.contains("Persistence paths recorded"));
7558 assert_eq!(view.selected_step(), SetupStep::Verification);
7559 }
7560
7561 #[test]
7562 fn operate_fleet_review_records_needs_action_until_first_run_ready() {
7563 let facts = SetupRuntimeFacts {
7564 provider_ready: true,
7565 operate_runtime_ready: true,
7566 fleet_roster_ready: true,
7567 operate_result:
7568 "provider=ready, runtime=ready, roster=ready, concurrency=plan limit not probed"
7569 .to_string(),
7570 ..SetupRuntimeFacts::default()
7571 };
7572 let mut view = SetupWizardView::new_at_with_facts(
7573 SetupState::default(),
7574 Locale::En,
7575 SetupStep::OperateFleet,
7576 facts,
7577 );
7578
7579 let action = view.handle_key(key(KeyCode::Enter));
7580
7581 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7582 else {
7583 panic!("expected setup-state commit event");
7584 };
7585 assert_eq!(
7586 state.status(SetupStep::OperateFleet),
7587 StepStatus::NeedsAction
7588 );
7589 assert!(!state.operate_ready());
7590 assert!(message.contains("needs action"));
7591 }
7592
7593 #[test]
7594 fn runtime_posture_preset_requires_preview_before_apply() {
7595 let facts = SetupRuntimeFacts {
7596 default_mode: "agent".to_string(),
7597 approval_policy_value: "never".to_string(),
7598 allow_shell_enabled: false,
7599 sandbox_mode_value: "read-only".to_string(),
7600 network_default_value: "deny".to_string(),
7601 trust: "workspace trust not elevated".to_string(),
7602 ..SetupRuntimeFacts::default()
7603 };
7604 let mut view = SetupWizardView::new_at_with_facts(
7605 SetupState::default(),
7606 Locale::En,
7607 SetupStep::TrustSandbox,
7608 facts,
7609 );
7610
7611 assert!(matches!(
7612 view.handle_key(key(KeyCode::Char('3'))),
7613 ViewAction::None
7614 ));
7615 let preview = view.handle_key(key(KeyCode::Char('a')));
7616 let ViewAction::Emit(ViewEvent::OpenTextPager { content, .. }) = preview else {
7617 panic!("first apply should preview the exact diff");
7618 };
7619 assert!(content.contains("Runtime Posture Preset Preview"));
7620 assert!(content.contains("settings.default_mode: agent -> act + full-access"));
7621 assert!(content.contains(
7622 "config.approval_policy: never -> removed; Full Access comes from settings.permission_posture"
7623 ));
7624 assert!(content.contains("settings.permission_posture: -> full-access"));
7625 assert!(content.contains("config.network.default: deny -> unchanged"));
7626
7627 let action = view.handle_key(key(KeyCode::Char('a')));
7628 let ViewAction::Emit(ViewEvent::SetupRuntimePresetApplyRequested {
7629 preset,
7630 state,
7631 message,
7632 }) = action
7633 else {
7634 panic!("second apply should request preset persistence");
7635 };
7636 assert_eq!(preset, SetupRuntimePreset::HighTrustLocal);
7637 assert_eq!(state.status(SetupStep::TrustSandbox), StepStatus::Verified);
7638 assert_eq!(
7639 state.runtime_posture_source,
7640 RuntimePostureSource::Confirmed
7641 );
7642 assert!(
7643 state
7644 .steps
7645 .get(&SetupStep::TrustSandbox)
7646 .and_then(|entry| entry.result.as_deref())
7647 .is_some_and(|result| {
7648 result.contains("preset=high-trust-local")
7649 && result.contains("default_mode=act + full-access")
7650 && result.contains("network=unchanged")
7651 })
7652 );
7653 assert!(message.contains("Runtime preset applied"));
7654 assert_eq!(view.selected_step(), SetupStep::Constitution);
7655 }
7656
7657 #[test]
7658 fn verification_report_records_needs_action_until_checkpoint_complete() {
7659 let facts = SetupRuntimeFacts {
7660 constitution_autonomy: "balanced".to_string(),
7661 runtime_result: "intent=agent, approval=suggest".to_string(),
7662 ..SetupRuntimeFacts::default()
7663 };
7664 let mut view = SetupWizardView::new_at_with_facts(
7665 SetupState::default(),
7666 Locale::En,
7667 SetupStep::Verification,
7668 facts,
7669 );
7670
7671 let action = view.handle_key(key(KeyCode::Enter));
7672
7673 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, message }) = action
7674 else {
7675 panic!("expected setup-state commit event");
7676 };
7677 assert_eq!(
7678 state.status(SetupStep::Verification),
7679 StepStatus::NeedsAction
7680 );
7681 assert!(
7682 state
7683 .steps
7684 .get(&SetupStep::Verification)
7685 .and_then(|entry| entry.result.as_deref())
7686 .is_some_and(|result| {
7687 result.contains("update=needs_action")
7688 && result.contains("operate=needs_action")
7689 && result.contains("autonomy=balanced")
7690 && result.contains("runtime=intent=agent, approval=suggest")
7691 })
7692 );
7693 assert!(message.contains("Setup report recorded"));
7694 }
7695
7696 #[test]
7697 fn verification_report_records_ready_after_bundled_checkpoint() {
7698 let mut state = SetupState::default();
7699 state.complete_constitution_checkpoint(
7700 CONSTITUTION_CHECKPOINT_VERSION,
7701 ConstitutionChoice::Bundled,
7702 );
7703 let mut view = SetupWizardView::new_at_with_facts(
7704 state,
7705 Locale::En,
7706 SetupStep::Verification,
7707 SetupRuntimeFacts::default(),
7708 );
7709
7710 let action = view.handle_key(key(KeyCode::Enter));
7711
7712 let ViewAction::Emit(ViewEvent::SetupStateCommitRequested { state, .. }) = action else {
7713 panic!("expected setup-state commit event");
7714 };
7715 assert_eq!(state.status(SetupStep::Verification), StepStatus::Verified);
7716 assert!(
7717 state
7718 .steps
7719 .get(&SetupStep::Verification)
7720 .and_then(|entry| entry.result.as_deref())
7721 .is_some_and(|result| {
7722 result.contains("update=ready") && result.contains("operate=needs_action")
7723 })
7724 );
7725 }
7726
7727 #[test]
7728 fn verification_detail_lines_show_next_action() {
7729 let facts = SetupRuntimeFacts {
7730 constitution_autonomy: "balanced".to_string(),
7731 runtime_result: "intent=agent, approval=suggest".to_string(),
7732 ..SetupRuntimeFacts::default()
7733 };
7734 let view = SetupWizardView::new_at_with_facts(
7735 SetupState::default(),
7736 Locale::En,
7737 SetupStep::Verification,
7738 facts,
7739 );
7740
7741 let text = lines_to_text(view.verification_detail_lines());
7742
7743 assert!(text.contains("First-run:"));
7744 assert!(text.contains("Update checkpoint:"));
7745 assert!(text.contains("Operate/Fleet:"));
7746 assert!(text.contains("Constitution autonomy:"));
7747 assert!(text.contains("balanced"));
7748 assert!(text.contains("Runtime posture:"));
7749 assert!(text.contains("intent=agent, approval=suggest"));
7750 assert!(text.contains("Complete the constitution checkpoint"));
7751 }
7752
7753 #[test]
7754 fn setup_wizard_body_scroll_resets_on_step_change() {
7755 let mut view = SetupWizardView::new(SetupState::default(), Locale::En);
7756 view.body_scroll = 12;
7757 view.move_next();
7758 assert_eq!(view.body_scroll, 0, "step change should reset body scroll");
7759 view.body_scroll = 5;
7760 view.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE));
7761 assert!(view.body_scroll >= 5);
7762 view.move_back();
7763 assert_eq!(view.body_scroll, 0);
7764 }
7765
7766 #[test]
7767 fn setup_wizard_page_down_clamps_scroll_at_80x24() {
7768 use ratatui::text::{Line, Span};
7769
7770 let mut view = SetupWizardView::new_at_with_facts(
7771 SetupState::default(),
7772 Locale::En,
7773 SetupStep::Constitution,
7774 SetupRuntimeFacts {
7775 constitution_file: SetupConstitutionFileState::Loaded,
7776 ..SetupRuntimeFacts::default()
7777 },
7778 );
7779 let wrap_width = 76usize;
7780 let visible_rows = 10usize;
7781 let mut lines = view.constitution_detail_lines();
7782 lines.extend(std::iter::repeat_n(
7783 Line::from(Span::raw("x".repeat(wrap_width))),
7784 40,
7785 ));
7786 let visual_rows: usize = lines
7787 .iter()
7788 .map(|line| line.width().div_ceil(wrap_width).max(1))
7789 .sum();
7790 let max_scroll = visual_rows.saturating_sub(visible_rows);
7791 assert!(max_scroll > 0, "fixture should overflow a small viewport");
7792
7793 for _ in 0..32 {
7794 view.handle_key(KeyEvent::new(KeyCode::PageDown, KeyModifiers::NONE));
7795 }
7796 assert!(
7797 view.body_scroll >= max_scroll.saturating_sub(8),
7798 "page down should reach the scroll ceiling"
7799 );
7800
7801 let clamped = view.body_scroll.min(max_scroll);
7802 assert_eq!(
7803 clamped, max_scroll,
7804 "render path should clamp overshoot to max scroll"
7805 );
7806 }
7807
7808 fn lines_to_text(lines: Vec<Line<'static>>) -> String {
7809 lines
7810 .into_iter()
7811 .map(|line| {
7812 line.spans
7813 .into_iter()
7814 .map(|span| span.content.into_owned())
7815 .collect::<String>()
7816 })
7817 .collect::<Vec<_>>()
7818 .join("\n")
7819 }
7820 }
7821
7821 lines RUST