返回 CodeWhale
trust_directory.rs
根目录 / crates / tui / src / tui / onboarding / trust_directory.rs
1 //! Workspace trust prompt 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 /// Wrap a path-bearing line at `/` boundaries so a deep workspace never
11 /// hard-splits mid-component under ratatui's whitespace-only `Wrap`.
12 /// Continuation lines are indented to read as one location.
13 fn wrap_on_path_separators(text: &str, width: usize) -> Vec<String> {
14 let width = width.max(8);
15 let mut out: Vec<String> = Vec::new();
16 let mut current = String::new();
17 let mut chunk = String::new();
18 let flush = |current: &mut String, chunk: &mut String, out: &mut Vec<String>| {
19 if chunk.is_empty() {
20 return;
21 }
22 let candidate_len = current.chars().count() + chunk.chars().count();
23 if candidate_len > width && !current.is_empty() {
24 out.push(std::mem::take(current));
25 current.push_str(" ");
26 }
27 current.push_str(chunk);
28 chunk.clear();
29 };
30 for ch in text.chars() {
31 chunk.push(ch);
32 if ch == '/' {
33 flush(&mut current, &mut chunk, &mut out);
34 }
35 }
36 flush(&mut current, &mut chunk, &mut out);
37 if !current.is_empty() {
38 out.push(current);
39 }
40 if out.is_empty() {
41 vec![String::new()]
42 } else {
43 out
44 }
45 }
46
47 pub fn lines(app: &App, content_width: usize) -> Vec<Line<'static>> {
48 let mut lines = Vec::new();
49 lines.push(Line::from(Span::styled(
50 app.tr(MessageId::OnboardTrustTitle).to_string(),
51 Style::default()
52 .fg(palette::WHALE_INFO)
53 .add_modifier(Modifier::BOLD),
54 )));
55 lines.push(Line::from(""));
56 lines.push(Line::from(Span::styled(
57 app.tr(MessageId::OnboardTrustQuestion).to_string(),
58 Style::default().fg(palette::TEXT_PRIMARY),
59 )));
60 let location = format!(
61 "{}{}",
62 app.tr(MessageId::OnboardTrustLocationPrefix),
63 crate::utils::display_path(&app.workspace)
64 );
65 for segment in wrap_on_path_separators(&location, content_width) {
66 lines.push(Line::from(Span::styled(
67 segment,
68 Style::default().fg(palette::TEXT_MUTED),
69 )));
70 }
71 lines.push(Line::from(""));
72 lines.push(Line::from(Span::styled(
73 app.tr(MessageId::OnboardTrustRiskHint).to_string(),
74 Style::default().fg(palette::TEXT_MUTED),
75 )));
76 lines.push(Line::from(Span::styled(
77 app.tr(MessageId::OnboardTrustEffectHint).to_string(),
78 Style::default().fg(palette::TEXT_MUTED),
79 )));
80 if let Some(message) = app.status_message.as_deref() {
81 lines.push(Line::from(""));
82 lines.push(Line::from(Span::styled(
83 message.to_string(),
84 Style::default().fg(palette::STATUS_WARNING),
85 )));
86 }
87 lines.push(Line::from(""));
88 lines.push(Line::from(vec![
89 Span::styled(
90 app.tr(MessageId::OnboardTrustFooterPrefix).to_string(),
91 Style::default().fg(palette::TEXT_MUTED),
92 ),
93 Span::styled(
94 "1/Y",
95 Style::default()
96 .fg(palette::TEXT_PRIMARY)
97 .add_modifier(Modifier::BOLD),
98 ),
99 Span::styled(
100 app.tr(MessageId::OnboardTrustFooterMiddle).to_string(),
101 Style::default().fg(palette::TEXT_MUTED),
102 ),
103 Span::styled(
104 "2/U",
105 Style::default()
106 .fg(palette::TEXT_PRIMARY)
107 .add_modifier(Modifier::BOLD),
108 ),
109 Span::styled(
110 app.tr(MessageId::OnboardTrustFooterUntrustedMiddle)
111 .to_string(),
112 Style::default().fg(palette::TEXT_MUTED),
113 ),
114 Span::styled(
115 "3/N/Esc",
116 Style::default()
117 .fg(palette::TEXT_PRIMARY)
118 .add_modifier(Modifier::BOLD),
119 ),
120 Span::styled(
121 app.tr(MessageId::OnboardTrustFooterSuffix).to_string(),
122 Style::default().fg(palette::TEXT_MUTED),
123 ),
124 ]));
125 lines
126 }
127
128 #[cfg(test)]
129 mod tests {
130 use super::*;
131 use crate::config::Config;
132 use crate::tui::app::TuiOptions;
133 use std::path::PathBuf;
134
135 #[test]
136 fn prompt_names_the_workspace_boundary_and_effects() {
137 let options = TuiOptions {
138 model: "test-model".to_string(),
139 ..crate::test_support::test_tui_options(PathBuf::from("workspace-fixture"))
140 };
141 let mut app = App::new(options, &Config::default());
142 app.ui_locale = crate::localization::Locale::En;
143 let body = lines(&app, 70)
144 .into_iter()
145 .flat_map(|line| line.spans.into_iter().map(|span| span.content.to_string()))
146 .collect::<Vec<_>>()
147 .join("\n");
148
149 assert!(body.contains("Know this workspace"));
150 assert!(body.contains("instructions and files"));
151 assert!(body.contains("prompt injection"));
152 assert!(body.contains("tools and hooks"));
153 assert!(body.contains("1/Y"));
154 assert!(body.contains("2/U"));
155 assert!(body.contains("continue without trusting"));
156 assert!(body.contains("3/N/Esc"));
157 assert!(body.contains("quit Codewhale"));
158 }
159 }
160
160 lines RUST