返回 CodeWhale
mental_models.rs
根目录 / crates / tui / src / tui / onboarding / mental_models.rs
1 //! First-run primer for Codewhale's two independent control axes.
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, AppMode};
9
10 pub fn lines(app: &App) -> Vec<Line<'static>> {
11 let plan_mode = AppMode::Plan
12 .display_name_localized(app.ui_locale)
13 .to_string();
14 let act_mode = AppMode::Agent
15 .display_name_localized(app.ui_locale)
16 .to_string();
17 let operate_mode = AppMode::Operate
18 .display_name_localized(app.ui_locale)
19 .to_string();
20 let current_mode = app.mode.display_name_localized(app.ui_locale).to_string();
21 let current_permission = app.approval_mode.permission_chip_label().to_string();
22
23 vec![
24 Line::from(Span::styled(
25 app.tr(MessageId::OnboardMentalTitle).to_string(),
26 Style::default()
27 .fg(palette::WHALE_INFO)
28 .add_modifier(Modifier::BOLD),
29 )),
30 Line::from(""),
31 Line::from(vec![
32 Span::styled(
33 app.tr(MessageId::OnboardMentalModesLabel).to_string(),
34 Style::default().add_modifier(Modifier::BOLD),
35 ),
36 Span::raw(" — "),
37 Span::styled(plan_mode, Style::default().fg(palette::WHALE_ACTION)),
38 Span::raw(format!(
39 " ({}) · ",
40 app.tr(MessageId::OnboardMentalPlanHint)
41 )),
42 Span::styled(act_mode, Style::default().fg(palette::WHALE_ACTION)),
43 Span::raw(format!(" ({}) · ", app.tr(MessageId::OnboardMentalActHint))),
44 Span::styled(operate_mode, Style::default().fg(palette::WHALE_ACTION)),
45 Span::raw(format!(
46 " ({})",
47 app.tr(MessageId::OnboardMentalOperateHint)
48 )),
49 ]),
50 Line::from(vec![
51 Span::styled(
52 app.tr(MessageId::OnboardMentalPermissionLabel).to_string(),
53 Style::default().add_modifier(Modifier::BOLD),
54 ),
55 Span::raw(" — "),
56 Span::styled("Ask", Style::default().fg(palette::WHALE_ACTION)),
57 Span::raw(" · "),
58 Span::styled("Auto-Review", Style::default().fg(palette::WHALE_ACTION)),
59 Span::raw(" · "),
60 Span::styled("Full Access", Style::default().fg(palette::WHALE_ACTION)),
61 ]),
62 Line::from(""),
63 Line::from(vec![
64 Span::raw(format!(
65 "{} ",
66 app.tr(MessageId::OnboardMentalCurrentLabel)
67 )),
68 Span::styled(
69 current_mode,
70 Style::default()
71 .fg(palette::WHALE_ACTION)
72 .add_modifier(Modifier::BOLD),
73 ),
74 Span::raw(" · "),
75 Span::styled(
76 current_permission,
77 Style::default()
78 .fg(palette::WHALE_ACTION)
79 .add_modifier(Modifier::BOLD),
80 ),
81 ]),
82 Line::from(Span::styled(
83 app.tr(MessageId::OnboardMentalConstitution).to_string(),
84 Style::default().fg(palette::WHALE_HUMAN),
85 )),
86 Line::from(""),
87 Line::from(vec![
88 Span::styled(
89 "Tab",
90 Style::default()
91 .fg(palette::TEXT_PRIMARY)
92 .add_modifier(Modifier::BOLD),
93 ),
94 Span::raw(format!(" {} ", app.tr(MessageId::OnboardMentalCycleMode))),
95 Span::styled(
96 "Shift+Tab",
97 Style::default()
98 .fg(palette::TEXT_PRIMARY)
99 .add_modifier(Modifier::BOLD),
100 ),
101 Span::raw(format!(
102 " {}",
103 app.tr(MessageId::OnboardMentalCyclePermission)
104 )),
105 ]),
106 Line::from(vec![
107 Span::styled(
108 "Enter",
109 Style::default()
110 .fg(palette::TEXT_PRIMARY)
111 .add_modifier(Modifier::BOLD),
112 ),
113 Span::raw(format!(" {} ", app.tr(MessageId::OnboardMentalContinue))),
114 Span::styled(
115 "Esc",
116 Style::default()
117 .fg(palette::TEXT_PRIMARY)
118 .add_modifier(Modifier::BOLD),
119 ),
120 Span::raw(format!(" {}", app.tr(MessageId::OnboardMentalBack))),
121 ]),
122 ]
123 }
124
125 #[cfg(test)]
126 mod tests {
127 use super::*;
128 use crate::config::Config;
129 use crate::tui::app::TuiOptions;
130 use std::path::PathBuf;
131
132 #[test]
133 fn primer_names_both_control_axes_and_current_values() {
134 let options = TuiOptions {
135 model: "test-model".to_string(),
136 ..crate::test_support::test_tui_options(PathBuf::from("."))
137 };
138 let mut app = App::new(options, &Config::default());
139 app.ui_locale = crate::localization::Locale::En;
140 let body = lines(&app)
141 .into_iter()
142 .flat_map(|line| line.spans.into_iter().map(|span| span.content.to_string()))
143 .collect::<Vec<_>>()
144 .join("\n");
145
146 assert!(body.contains("How Codewhale works"));
147 assert!(body.contains("Plan"));
148 assert!(body.contains("Act"));
149 assert!(body.contains("Operate"));
150 assert!(body.contains("Ask"));
151 assert!(body.contains("Auto-Review"));
152 assert!(body.contains("Full Access"));
153 assert!(body.contains("Tab"));
154 assert!(body.contains("Shift+Tab"));
155 assert!(body.contains("constitution"));
156 }
157
158 #[test]
159 fn primer_localizes_mode_names_consistently() {
160 let options = TuiOptions {
161 model: "test-model".to_string(),
162 ..crate::test_support::test_tui_options(PathBuf::from("."))
163 };
164 let mut app = App::new(options, &Config::default());
165 app.ui_locale = crate::localization::Locale::Ko;
166 let body = lines(&app)
167 .into_iter()
168 .flat_map(|line| line.spans.into_iter().map(|span| span.content.to_string()))
169 .collect::<Vec<_>>()
170 .join("\n");
171
172 for mode in [AppMode::Plan, AppMode::Agent, AppMode::Operate] {
173 let localized = mode.display_name_localized(app.ui_locale);
174 assert!(body.contains(localized.as_ref()));
175 }
176 }
177 }
178
178 lines RUST