1pub const TOOL_OUTPUT_MODE_COMPACT: &str = "compact";
2pub const TOOL_OUTPUT_MODE_FULL: &str = "full";
3pub const DEFAULT_INLINE_VIEWPORT_ROWS: u16 = 16;
4pub const DEFAULT_REASONING_VISIBLE: bool = true;
5pub const MODAL_MIN_WIDTH: u16 = 36;
6pub const MODAL_MIN_HEIGHT: u16 = 16;
7pub const MODAL_INSTRUCTIONS_BULLET: &str = "•";
8pub const INLINE_HEADER_HEIGHT: u16 = 3;
9pub const INLINE_INPUT_PADDING_HORIZONTAL: u16 = 1;
10pub const INLINE_INPUT_PADDING_VERTICAL: u16 = 1;
11pub const INLINE_INPUT_MAX_LINES: usize = 10;
12pub const INLINE_TRANSCRIPT_BOTTOM_PADDING: u16 = 2;
16pub const INLINE_PREVIEW_ELLIPSIS: &str = "…";
17pub const INLINE_PASTE_COLLAPSE_LINE_THRESHOLD: usize = 10;
18pub const HEADER_HIGHLIGHT_PREVIEW_MAX_CHARS: usize = 48;
19pub const INLINE_AGENT_MESSAGE_LEFT_PADDING: &str = " ";
20pub const INLINE_AGENT_QUOTE_PREFIX: &str = " •";
21pub const INLINE_USER_MESSAGE_DIVIDER_SYMBOL: &str = "─";
22
23pub const INLINE_BLOCK_HORIZONTAL: &str = "─";
24pub const META_BULLET_SEPARATOR: &str = " • ";
29pub const MODAL_LIST_HIGHLIGHT_FULL: &str = "┃ ";
30pub const MODAL_LIST_SUMMARY_NO_MATCHES: &str = "No matches";
31pub const MODAL_LIST_SUMMARY_RESET_HINT: &str = "Press Esc to reset";
32pub const MODAL_LIST_NO_RESULTS_MESSAGE: &str = "No matching options";
33pub const HEADER_VERSION_PROMPT: &str = "> ";
34pub const HEADER_VERSION_PREFIX: &str = "VT Code";
35pub const HEADER_VERSION_LEFT_DELIMITER: &str = "(";
36pub const HEADER_VERSION_RIGHT_DELIMITER: &str = ")";
37pub const HEADER_PROVIDER_PREFIX: &str = "Provider: ";
38pub const HEADER_MODEL_PREFIX: &str = "Model: ";
39pub const HEADER_SERVICE_TIER_PREFIX: &str = "Tier: ";
40pub const HEADER_REASONING_PREFIX: &str = "";
41pub const HEADER_TRUST_PREFIX: &str = "Trust: ";
42pub const HEADER_TOOLS_PREFIX: &str = "Tools: ";
43pub const HEADER_MCP_PREFIX: &str = "MCP: ";
44pub const HEADER_GIT_PREFIX: &str = "git: ";
45pub const HEADER_GIT_CLEAN_SUFFIX: &str = "✓";
46pub const HEADER_GIT_DIRTY_SUFFIX: &str = "*";
47pub const HEADER_UNKNOWN_PLACEHOLDER: &str = "unavailable";
48macro_rules! chat_input_shortcut_hint {
58 () => {
59 "@files /commands • Enter run • Tab queue • Shift+Tab agent • Ctrl+R history"
60 };
61}
62pub const CHAT_INPUT_PLACEHOLDER_BOOTSTRAP: &str = concat!("Type a request… ", chat_input_shortcut_hint!());
64pub const CHAT_INPUT_PLACEHOLDER_FOLLOW_UP: &str = concat!("Continue… ", chat_input_shortcut_hint!());
66pub const CHAT_INPUT_PLACEHOLDER_INTERRUPTED: &str =
67 "Interrupted · What should VT Code do instead? • Esc cancel • Ctrl+C interrupt • Ctrl+D exit";
68pub const WELCOME_SLASH_COMMAND_LIMIT: usize = 6;
69pub const WELCOME_SLASH_COMMAND_PREFIX: &str = "/";
70pub const WELCOME_SLASH_COMMAND_INTRO: &str = "";
71pub const WELCOME_SLASH_COMMAND_INDENT: &str = " ";
72pub const PLAN_BLOCK_TITLE: &str = "TODOs";
73pub const PLAN_STATUS_EMPTY: &str = "No TODOs";
74pub(crate) const STATUS_LINE_REFRESH_INTERVAL_MS: u64 = 1000;
75pub(crate) const STATUS_LINE_COMMAND_TIMEOUT_MS: u64 = 200;
76
77const AGENT_HUE_NAMES: &[&str] = &["red", "green", "blue", "magenta", "yellow", "cyan"];
89
90pub const AGENT_COLOR_BUILD: &str = "#73a18e";
92pub const AGENT_COLOR_AUTO: &str = "green";
94pub const AGENT_COLOR_PLAN: &str = "blue";
96pub const AGENT_COLOR_DUCK: &str = "magenta";
98
99const AGENT_MODE_HUE: &[(&str, &str)] = &[
102 ("build", AGENT_COLOR_BUILD),
103 ("auto", AGENT_COLOR_AUTO),
104 ("plan", AGENT_COLOR_PLAN),
105 ("duck", AGENT_COLOR_DUCK),
106];
107
108pub fn agent_mode_hue(mode: &str) -> Option<&'static str> {
111 AGENT_MODE_HUE.iter().find(|(m, _)| *m == mode).map(|(_, h)| *h)
112}
113
114pub const THEME_MIN_CONTRAST_RATIO: f32 = 4.5;
120pub const THEME_FOREGROUND_LIGHTEN_RATIO: f32 = 0.25;
121pub const THEME_SECONDARY_LIGHTEN_RATIO: f32 = 0.2;
122pub const THEME_MIX_RATIO: f32 = 0.35;
123pub const THEME_TOOL_BODY_MIX_RATIO: f32 = 0.35;
124pub const THEME_TOOL_BODY_LIGHTEN_RATIO: f32 = 0.2;
125pub const THEME_RESPONSE_COLOR_LIGHTEN_RATIO: f32 = 0.15;
126pub const THEME_INPUT_BACKGROUND_MIX_RATIO: f32 = 0.08;
127pub const THEME_USER_COLOR_LIGHTEN_RATIO: f32 = 0.2;
128pub const THEME_SECONDARY_USER_COLOR_LIGHTEN_RATIO: f32 = 0.4;
129pub const THEME_PRIMARY_STATUS_LIGHTEN_RATIO: f32 = 0.35;
130pub const THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO: f32 = 0.5;
131pub const THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO: f32 = 0.35;
132pub const THEME_LOGO_ACCENT_BANNER_SECONDARY_LIGHTEN_RATIO: f32 = 0.25;
133
134pub const THEME_COLOR_WHITE_RED: u8 = 0xFF;
136pub const THEME_COLOR_WHITE_GREEN: u8 = 0xFF;
137pub const THEME_COLOR_WHITE_BLUE: u8 = 0xFF;
138pub const THEME_MIX_RATIO_MIN: f32 = 0.0;
139pub const THEME_MIX_RATIO_MAX: f32 = 1.0;
140pub const THEME_BLEND_CLAMP_MIN: f32 = 0.0;
141pub const THEME_BLEND_CLAMP_MAX: f32 = 255.0;
142
143pub const THEME_RELATIVE_LUMINANCE_CUTOFF: f32 = 0.03928;
145pub const THEME_RELATIVE_LUMINANCE_LOW_FACTOR: f32 = 12.92;
146pub const THEME_RELATIVE_LUMINANCE_OFFSET: f32 = 0.055;
147pub const THEME_RELATIVE_LUMINANCE_EXPONENT: f32 = 2.4;
148pub const THEME_CONTRAST_RATIO_OFFSET: f32 = 0.05;
149pub const THEME_RED_LUMINANCE_COEFFICIENT: f32 = 0.2126;
150pub const THEME_GREEN_LUMINANCE_COEFFICIENT: f32 = 0.7152;
151pub const THEME_BLUE_LUMINANCE_COEFFICIENT: f32 = 0.0722;
152pub const THEME_LUMINANCE_LIGHTEN_RATIO: f32 = 0.2;
153pub const THEME_PTY_OUTPUT_MIX_RATIO: f32 = 0.50;
155
156pub const PLACEHOLDER_R: u8 = 0x88;
158pub const PLACEHOLDER_G: u8 = 0x88;
159pub const PLACEHOLDER_B: u8 = 0x88;
160
161pub const SAFE_ANSI_BRIGHT_CYAN: u8 = 14;
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 const DARK_BG: (u8, u8, u8) = (0x18, 0x18, 0x18);
177 const LIGHT_BG: (u8, u8, u8) = (0xFD, 0xF6, 0xE3);
178
179 fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
182 let hex = hex.strip_prefix('#')?;
183 if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
184 return None;
185 }
186 let channel = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
187 Some((channel(0)?, channel(2)?, channel(4)?))
188 }
189
190 fn hue_rgb_on(hue: &str, light: bool) -> Option<(u8, u8, u8)> {
195 if let Some(rgb) = hex_to_rgb(hue) {
196 return Some(rgb);
197 }
198 let (d, l) = match hue {
199 "red" => ((0xFF, 0x5F, 0x5F), (0xCD, 0x00, 0x00)),
200 "green" => ((0x5F, 0xFF, 0x5F), (0x00, 0xCD, 0x00)),
201 "blue" => ((0x5F, 0x5F, 0xFF), (0x00, 0x00, 0xCD)),
202 "magenta" => ((0xFF, 0x5F, 0xFF), (0xCD, 0x00, 0xCD)),
203 "yellow" => ((0xFF, 0xFF, 0x5F), (0xCD, 0xCD, 0x00)),
204 "cyan" => ((0x5F, 0xFF, 0xFF), (0x00, 0xCD, 0xCD)),
205 _ => return None,
206 };
207 Some(if light { l } else { d })
208 }
209
210 fn channel_lin(c: u8) -> f32 {
211 let c = c as f32 / 255.0;
212 if c <= 0.03928 {
213 c / 12.92
214 } else {
215 ((c + 0.055) / 1.055).powf(2.4)
216 }
217 }
218
219 fn relative_luminance((r, g, b): (u8, u8, u8)) -> f32 {
220 0.2126 * channel_lin(r) + 0.7152 * channel_lin(g) + 0.0722 * channel_lin(b)
221 }
222
223 fn contrast_ratio(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> f32 {
224 let l_fg = relative_luminance(fg);
225 let l_bg = relative_luminance(bg);
226 let (hi, lo) = if l_fg > l_bg { (l_fg, l_bg) } else { (l_bg, l_fg) };
227 (hi + 0.05) / (lo + 0.05)
228 }
229
230 const MIN_CONTRAST_DARK: f32 = 3.0;
236 const MIN_CONTRAST_LIGHT: f32 = 2.0;
237
238 fn assert_agent_hue_readable(name: &str, hue: &str, light: bool, bg: (u8, u8, u8)) {
239 let fg =
240 hue_rgb_on(hue, light).unwrap_or_else(|| panic!("{name} ({hue}) is not a supported standard ANSI hue"));
241 let cr = contrast_ratio(fg, bg);
242 let min = if light { MIN_CONTRAST_LIGHT } else { MIN_CONTRAST_DARK };
243 assert!(
244 cr >= min,
245 "{name} ({hue}) on {} bg contrast {cr:.2} below {min}",
246 if light { "light" } else { "dark" }
247 );
248 }
249
250 #[test]
251 fn agent_mode_color_tokens_are_supported() {
252 for (_, hue) in AGENT_MODE_HUE {
253 let supported = AGENT_HUE_NAMES.contains(hue) || hex_to_rgb(hue).is_some();
254 assert!(supported, "mode token {hue} is neither a standard ANSI hue nor #rrggbb");
255 }
256 }
257
258 #[test]
259 fn agent_mode_colors_are_readable_in_dark_and_light_terminals() {
260 for (mode, hue) in AGENT_MODE_HUE {
261 assert_agent_hue_readable(mode, hue, false, DARK_BG);
263 assert_agent_hue_readable(mode, hue, true, LIGHT_BG);
264 }
265 }
266
267 #[test]
270 fn agent_mode_colors_are_distinct() {
271 let hues: Vec<&str> = AGENT_MODE_HUE.iter().map(|(_, h)| *h).collect();
272 for i in 0..hues.len() {
273 for j in (i + 1)..hues.len() {
274 assert_ne!(hues[i], hues[j], "mode hues {} and {} are identical", hues[i], hues[j]);
275 }
276 }
277 }
278
279 #[test]
280 fn agent_mode_hue_resolves_mode_names() {
281 assert_eq!(agent_mode_hue("build"), Some(AGENT_COLOR_BUILD));
282 assert_eq!(agent_mode_hue("auto"), Some(AGENT_COLOR_AUTO));
283 assert_eq!(agent_mode_hue("plan"), Some(AGENT_COLOR_PLAN));
284 assert_eq!(agent_mode_hue("duck"), Some(AGENT_COLOR_DUCK));
285 assert_eq!(agent_mode_hue("unknown"), None);
286 }
287
288 #[test]
289 fn agent_mode_colors_are_wired_into_builtin_primary_agents() {
290 assert_eq!(AGENT_COLOR_BUILD, "#73a18e");
292 assert_eq!(AGENT_COLOR_AUTO, "green");
293 assert_eq!(AGENT_COLOR_PLAN, "blue");
294 assert_eq!(AGENT_COLOR_DUCK, "magenta");
295 }
296
297 #[test]
299 fn chat_input_placeholders_share_one_shortcut_hint() {
300 let hint = chat_input_shortcut_hint!();
301 assert!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP.ends_with(hint));
302 assert!(CHAT_INPUT_PLACEHOLDER_FOLLOW_UP.ends_with(hint));
303 assert!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP.starts_with("Type a request"));
304 assert!(CHAT_INPUT_PLACEHOLDER_FOLLOW_UP.starts_with("Continue"));
305 assert_ne!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP, CHAT_INPUT_PLACEHOLDER_FOLLOW_UP);
306 }
307
308 #[test]
313 fn meta_bullet_separator_is_the_single_source() {
314 assert_eq!(META_BULLET_SEPARATOR, " • ");
315 }
316
317 #[test]
324 fn removed_shadowed_tui_constants_stay_removed() {
325 const SHADOWED: &[&str] = &[
326 "TUI_ACTIVE_TICK_RATE_HZ",
327 "TUI_IDLE_TICK_RATE_HZ",
328 "TUI_ACTIVE_TIMEOUT_MS",
329 "TUI_SHIMMER_FRAME_INTERVAL_MS",
330 "TUI_SHIMMER_SWEEP_DURATION_MS",
331 "TUI_SCROLL_CURSOR_STEADY_MS",
332 "TUI_MAX_VIEWPORT_WIDTH",
333 "TUI_MAX_VIEWPORT_HEIGHT",
334 "MODAL_WIDTH_RATIO",
335 "MODAL_HEIGHT_RATIO",
336 "MODAL_MAX_WIDTH_RATIO",
337 "MODAL_MAX_HEIGHT_RATIO",
338 "SLASH_PALETTE_MIN_WIDTH",
339 "SLASH_PALETTE_MIN_HEIGHT",
340 "HEADER_SHORTCUT_HINT",
341 "SCROLL_INDICATOR_FORMAT",
342 "HEADER_META_SEPARATOR",
343 "SLASH_PALETTE_TOP_OFFSET",
344 ];
345 let src = include_str!("ui.rs");
347 for name in SHADOWED {
348 let declared = src.lines().any(|line| {
349 let trimmed = line.trim_start();
350 trimmed
351 .strip_prefix("pub const ")
352 .or_else(|| trimmed.strip_prefix("pub(crate) const "))
353 .is_some_and(|rest| rest.starts_with(&format!("{name}:")))
354 });
355 assert!(!declared, "`{name}` is shadowed by vtcode-ui; declare the live value there, not here");
356 }
357 }
358}