返回 CodeWhale
format_helpers.rs
根目录 / crates / tui / src / tui / format_helpers.rs
1 //! Small string builders that compose status-bar / footer chips and
2 //! one-off informational messages.
3 //!
4 //! Each helper is a pure function over a small slice of `App` or
5 //! response data. Grouped here so the composer/footer renderer doesn't
6 //! need to scroll past their bodies, and so the labels can be unit
7 //! tested in isolation.
8
9 use codewhale_models::Usage;
10
11 /// Build the multi-line "Cache warmup complete: …" status message
12 /// shown after a prefix-cache warmup turn finishes. Handles all four
13 /// combinations of `prompt_cache_hit_tokens` / `prompt_cache_miss_tokens`
14 /// being present or absent so we never report "0% cache hit" for an
15 /// API call that didn't surface telemetry at all.
16 pub(super) fn cache_warmup_result(usage: &Usage) -> String {
17 let cache = match (
18 usage.prompt_cache_hit_tokens,
19 usage.prompt_cache_miss_tokens,
20 ) {
21 (Some(hit), Some(miss)) => format!("Cache warmup complete: hit {hit} | miss {miss}"),
22 (Some(hit), None) => format!("Cache warmup complete: hit {hit} | miss unavailable"),
23 (None, Some(miss)) => format!("Cache warmup complete: hit unavailable | miss {miss}"),
24 (None, None) => "Cache warmup complete: cache telemetry unavailable".to_string(),
25 };
26 format!(
27 "{cache}\nNote: the first warmup is usually a miss. Later requests that reuse the same stable prefix may hit the provider cache; a hit is not guaranteed."
28 )
29 }
30
31 /// Render the response body for `/models` / `models list` — the current
32 /// model is starred and other available models follow underneath.
33 pub(super) fn available_models_message(
34 locale: codewhale_localization::Locale,
35 current_provider: &str,
36 current_model: &str,
37 models: &[String],
38 fleet: &Result<Vec<crate::fleet::members::FleetModel>, crate::fleet::store::FleetStoreError>,
39 ) -> String {
40 use codewhale_localization::{MessageId, tr};
41 let mut lines = Vec::new();
42 // The fleet leads (design §10 F1): what the person added, with the roles
43 // each model fills, before the provider's full list. A selected fleet
44 // that cannot be read is named as such, never shown as "no fleet".
45 match fleet.as_deref() {
46 Err(error) => lines
47 .push(tr(locale, MessageId::FleetModelsBroken).replace("{error}", &error.to_string())),
48 Ok([]) => lines.push(tr(locale, MessageId::FleetModelsEmpty).into_owned()),
49 Ok(fleet) => {
50 lines.push(
51 tr(locale, MessageId::FleetModelsHeader)
52 .replace("{fleet}", &fleet[0].fleet)
53 .replace("{count}", &fleet.len().to_string()),
54 );
55 for member in fleet {
56 // The exact route, not the bare id: two providers may serve
57 // the same model id and only one of them is the current route.
58 let marker = if member.matches(current_provider, current_model) {
59 "*"
60 } else {
61 " "
62 };
63 lines.push(format!(
64 "{marker} {}/{} · {}",
65 member.provider,
66 member.model,
67 member.roles_label()
68 ));
69 }
70 lines.push(String::new());
71 }
72 }
73 lines.push(format!("Available models ({})", models.len()));
74 for model in models {
75 if model == current_model {
76 lines.push(format!("* {model} (current)"));
77 } else {
78 lines.push(format!(" {model}"));
79 }
80 }
81 lines.join("\n")
82 }
83
84 #[cfg(test)]
85 mod tests {
86 use super::*;
87
88 #[test]
89 fn available_models_message_marks_current_model() {
90 let models = vec![
91 "deepseek-v4-pro".to_string(),
92 "deepseek-v4-flash".to_string(),
93 ];
94 let msg = available_models_message(
95 codewhale_localization::Locale::En,
96 "deepseek",
97 "deepseek-v4-pro",
98 &models,
99 &Ok(Vec::new()),
100 );
101 assert!(msg.contains("* deepseek-v4-pro (current)"), "got: {msg}");
102 assert!(msg.contains(" deepseek-v4-flash"), "got: {msg}");
103 assert!(
104 msg.starts_with("Your team is the session model only"),
105 "got: {msg}"
106 );
107 assert!(msg.contains("Available models (2)"), "got: {msg}");
108 }
109
110 /// A selected fleet that cannot be read is reported as an error, not as
111 /// "the session model only".
112 #[test]
113 fn available_models_message_names_a_broken_fleet_selection() {
114 let broken = Err(crate::fleet::store::FleetStoreError::NotFound(
115 "selected fleet `Ops`".to_string(),
116 ));
117 let msg = available_models_message(
118 codewhale_localization::Locale::En,
119 "deepseek",
120 "deepseek-v4-pro",
121 &[],
122 &broken,
123 );
124 assert!(
125 msg.starts_with(
126 "Your selected team could not be loaded: fleet file not found: selected fleet `Ops`"
127 ),
128 "got: {msg}"
129 );
130 assert!(!msg.contains("session model only"), "got: {msg}");
131 }
132
133 #[test]
134 fn fleet_current_marker_matches_the_exact_route_not_the_bare_id() {
135 let fleet = vec![
136 crate::fleet::members::FleetModel {
137 provider: "openrouter".to_string(),
138 model: "deepseek/deepseek-v4-flash".to_string(),
139 roles: vec!["scout".to_string()],
140 fleet: "Ops".to_string(),
141 },
142 crate::fleet::members::FleetModel {
143 provider: "novita".to_string(),
144 model: "deepseek/deepseek-v4-flash".to_string(),
145 roles: Vec::new(),
146 fleet: "Ops".to_string(),
147 },
148 ];
149 let msg = available_models_message(
150 codewhale_localization::Locale::En,
151 "novita",
152 "deepseek/deepseek-v4-flash",
153 &[],
154 &Ok(fleet),
155 );
156 assert!(
157 msg.contains(" openrouter/deepseek/deepseek-v4-flash · explore"),
158 "got: {msg}"
159 );
160 assert!(
161 msg.contains("* novita/deepseek/deepseek-v4-flash · member"),
162 "got: {msg}"
163 );
164 }
165
166 #[test]
167 fn cache_warmup_result_handles_missing_telemetry() {
168 let usage = Usage {
169 prompt_cache_hit_tokens: None,
170 prompt_cache_miss_tokens: None,
171 ..Default::default()
172 };
173 let msg = cache_warmup_result(&usage);
174 assert!(msg.contains("cache telemetry unavailable"), "got: {msg}");
175 }
176 }
177
177 lines RUST