| 1 | //! Welcome screen content for onboarding. |
| 2 | |
| 3 | use ratatui::style::{Modifier, Style}; |
| 4 | use ratatui::text::{Line, Span}; |
| 5 | |
| 6 | use crate::localization::MessageId; |
| 7 | use crate::palette; |
| 8 | use crate::tui::app::App; |
| 9 | |
| 10 | pub fn lines(app: &App) -> Vec<Line<'static>> { |
| 11 | let steps = welcome_step_labels(app).join(" -> "); |
| 12 | let version = app |
| 13 | .tr(MessageId::OnboardWelcomeVersion) |
| 14 | .replace("{version}", env!("CARGO_PKG_VERSION")); |
| 15 | let next_steps = app |
| 16 | .tr(MessageId::OnboardWelcomeSteps) |
| 17 | .replace("{steps}", &steps); |
| 18 | |
| 19 | vec![ |
| 20 | Line::from(Span::styled( |
| 21 | "codewhale", |
| 22 | Style::default() |
| 23 | .fg(palette::WHALE_HUMAN) |
| 24 | .add_modifier(Modifier::BOLD), |
| 25 | )), |
| 26 | Line::from(Span::styled( |
| 27 | version, |
| 28 | Style::default().fg(palette::TEXT_MUTED), |
| 29 | )), |
| 30 | Line::from(""), |
| 31 | Line::from(Span::styled( |
| 32 | app.tr(MessageId::OnboardWelcomeLead).to_string(), |
| 33 | Style::default().fg(palette::TEXT_PRIMARY), |
| 34 | )), |
| 35 | Line::from(Span::styled( |
| 36 | app.tr(MessageId::OnboardWelcomeSetupBlurb).to_string(), |
| 37 | Style::default().fg(palette::TEXT_MUTED), |
| 38 | )), |
| 39 | Line::from(Span::styled( |
| 40 | next_steps, |
| 41 | Style::default().fg(palette::TEXT_MUTED), |
| 42 | )), |
| 43 | Line::from(Span::styled( |
| 44 | app.tr(MessageId::OnboardWelcomeDefaults).to_string(), |
| 45 | Style::default().fg(palette::TEXT_MUTED), |
| 46 | )), |
| 47 | Line::from(""), |
| 48 | Line::from(Span::styled( |
| 49 | app.tr(MessageId::OnboardWelcomeEnter).to_string(), |
| 50 | Style::default().fg(palette::TEXT_PRIMARY), |
| 51 | )), |
| 52 | Line::from(Span::styled( |
| 53 | app.tr(MessageId::OnboardWelcomeExit).to_string(), |
| 54 | Style::default().fg(palette::TEXT_MUTED), |
| 55 | )), |
| 56 | ] |
| 57 | } |
| 58 | |
| 59 | fn welcome_step_labels(app: &App) -> Vec<String> { |
| 60 | let mut steps = vec![ |
| 61 | app.tr(MessageId::OnboardWelcomeStepLanguage).to_string(), |
| 62 | // #3937: the appearance step is unconditional, so the preview names it. |
| 63 | app.tr(MessageId::OnboardWelcomeStepAppearance).to_string(), |
| 64 | ]; |
| 65 | if app.onboarding_needs_api_key { |
| 66 | steps.push(app.tr(MessageId::OnboardWelcomeStepApiKey).to_string()); |
| 67 | } |
| 68 | if !app.trust_mode && super::needs_trust(&app.workspace) { |
| 69 | steps.push(app.tr(MessageId::OnboardWelcomeStepTrust).to_string()); |
| 70 | } |
| 71 | steps.push( |
| 72 | app.tr(MessageId::OnboardWelcomeStepMentalModels) |
| 73 | .to_string(), |
| 74 | ); |
| 75 | steps.push(app.tr(MessageId::OnboardWelcomeStepTips).to_string()); |
| 76 | steps |
| 77 | } |
| 78 | |
| 79 | #[cfg(test)] |
| 80 | mod tests { |
| 81 | use super::*; |
| 82 | use crate::config::Config; |
| 83 | use crate::localization::Locale; |
| 84 | use crate::tui::app::TuiOptions; |
| 85 | use std::path::PathBuf; |
| 86 | |
| 87 | fn test_app_with_locale(locale: Locale) -> App { |
| 88 | let options = TuiOptions { |
| 89 | ..crate::test_support::test_tui_options(PathBuf::from(".")) |
| 90 | }; |
| 91 | let mut app = App::new(options, &Config::default()); |
| 92 | app.ui_locale = locale; |
| 93 | app |
| 94 | } |
| 95 | |
| 96 | fn body(app: &App) -> String { |
| 97 | lines(app) |
| 98 | .into_iter() |
| 99 | .flat_map(|line| { |
| 100 | line.spans |
| 101 | .into_iter() |
| 102 | .map(|span| span.content.to_string()) |
| 103 | .collect::<Vec<_>>() |
| 104 | }) |
| 105 | .collect::<Vec<_>>() |
| 106 | .join("\n") |
| 107 | } |
| 108 | |
| 109 | #[test] |
| 110 | fn welcome_copy_centers_constitution_first_setup() { |
| 111 | let mut app = test_app_with_locale(Locale::En); |
| 112 | app.onboarding_needs_api_key = false; |
| 113 | app.trust_mode = true; |
| 114 | let body = body(&app); |
| 115 | |
| 116 | // The dual meaning of "code" opens the arc: software and law. |
| 117 | assert!(body.contains("Code means two things")); |
| 118 | assert!(body.contains("the law this agent works under")); |
| 119 | assert!(body.contains("only these screens will appear")); |
| 120 | assert!(body.contains( |
| 121 | "Next: choose language -> pick a look -> learn modes and permissions -> setup tips." |
| 122 | )); |
| 123 | assert!(body.contains("/constitution")); |
| 124 | assert!(!body.contains("add an API key")); |
| 125 | assert!(!body.contains("land in the chat")); |
| 126 | } |
| 127 | |
| 128 | #[test] |
| 129 | fn welcome_wordmark_uses_the_human_brand_lane() { |
| 130 | let app = test_app_with_locale(Locale::En); |
| 131 | let welcome = lines(&app); |
| 132 | assert_eq!(welcome[0].spans[0].style.fg, Some(palette::WHALE_HUMAN)); |
| 133 | assert_ne!(palette::WHALE_HUMAN, palette::WHALE_ACTION); |
| 134 | } |
| 135 | |
| 136 | #[test] |
| 137 | fn welcome_steps_include_optional_api_key_and_trust_screens() { |
| 138 | let tmp = tempfile::tempdir().expect("tempdir"); |
| 139 | let mut app = test_app_with_locale(Locale::En); |
| 140 | app.workspace = tmp.path().to_path_buf(); |
| 141 | app.onboarding_needs_api_key = true; |
| 142 | app.trust_mode = false; |
| 143 | |
| 144 | let body = body(&app); |
| 145 | |
| 146 | assert!(body.contains( |
| 147 | "Next: choose language -> pick a look -> connect API key -> trust workspace -> learn modes and permissions -> setup tips." |
| 148 | )); |
| 149 | } |
| 150 | |
| 151 | #[test] |
| 152 | fn welcome_copy_uses_locale_registry() { |
| 153 | let mut app = test_app_with_locale(Locale::ZhHans); |
| 154 | app.onboarding_needs_api_key = false; |
| 155 | app.trust_mode = true; |
| 156 | |
| 157 | let body = body(&app); |
| 158 | |
| 159 | assert!(body.contains("Codewhale 会与你协作完成工作")); |
| 160 | assert!(!body.contains("代码在这里有两层含义")); |
| 161 | assert!(body.contains("接下来:选择语言 -> 选择外观 -> 了解模式与权限 -> 设置提示。")); |
| 162 | assert!(!body.contains("Press Enter")); |
| 163 | } |
| 164 | } |
| 165 |