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