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