返回 CodeWhale
welcome.rs
根目录 / crates / tui / src / tui / onboarding / welcome.rs
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
165 lines RUST