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_FILLED_TEXT_LUMINANCE_CROSSOVER: f32 = 0.179;
123
124pub const THEME_FOREGROUND_LIGHTEN_RATIO: f32 = 0.25;
125pub const THEME_SECONDARY_LIGHTEN_RATIO: f32 = 0.2;
126pub const THEME_MIX_RATIO: f32 = 0.35;
127pub const THEME_TOOL_BODY_MIX_RATIO: f32 = 0.35;
128pub const THEME_TOOL_BODY_LIGHTEN_RATIO: f32 = 0.2;
129pub const THEME_RESPONSE_COLOR_LIGHTEN_RATIO: f32 = 0.15;
130pub const THEME_INPUT_BACKGROUND_MIX_RATIO: f32 = 0.08;
131pub const THEME_USER_COLOR_LIGHTEN_RATIO: f32 = 0.2;
132pub const THEME_SECONDARY_USER_COLOR_LIGHTEN_RATIO: f32 = 0.4;
133pub const THEME_PRIMARY_STATUS_LIGHTEN_RATIO: f32 = 0.35;
134pub const THEME_PRIMARY_STATUS_SECONDARY_LIGHTEN_RATIO: f32 = 0.5;
135pub const THEME_LOGO_ACCENT_BANNER_LIGHTEN_RATIO: f32 = 0.35;
136pub const THEME_LOGO_ACCENT_BANNER_SECONDARY_LIGHTEN_RATIO: f32 = 0.25;
137
138pub const THEME_COLOR_WHITE_RED: u8 = 0xFF;
140pub const THEME_COLOR_WHITE_GREEN: u8 = 0xFF;
141pub const THEME_COLOR_WHITE_BLUE: u8 = 0xFF;
142pub const THEME_MIX_RATIO_MIN: f32 = 0.0;
143pub const THEME_MIX_RATIO_MAX: f32 = 1.0;
144pub const THEME_BLEND_CLAMP_MIN: f32 = 0.0;
145pub const THEME_BLEND_CLAMP_MAX: f32 = 255.0;
146
147pub const THEME_RELATIVE_LUMINANCE_CUTOFF: f32 = 0.03928;
149pub const THEME_RELATIVE_LUMINANCE_LOW_FACTOR: f32 = 12.92;
150pub const THEME_RELATIVE_LUMINANCE_OFFSET: f32 = 0.055;
151pub const THEME_RELATIVE_LUMINANCE_EXPONENT: f32 = 2.4;
152pub const THEME_CONTRAST_RATIO_OFFSET: f32 = 0.05;
153pub const THEME_RED_LUMINANCE_COEFFICIENT: f32 = 0.2126;
154pub const THEME_GREEN_LUMINANCE_COEFFICIENT: f32 = 0.7152;
155pub const THEME_BLUE_LUMINANCE_COEFFICIENT: f32 = 0.0722;
156pub const THEME_LUMINANCE_LIGHTEN_RATIO: f32 = 0.2;
157pub const THEME_PTY_OUTPUT_MIX_RATIO: f32 = 0.50;
159
160pub const PLACEHOLDER_R: u8 = 0x88;
162pub const PLACEHOLDER_G: u8 = 0x88;
163pub const PLACEHOLDER_B: u8 = 0x88;
164
165pub const SAFE_ANSI_BRIGHT_CYAN: u8 = 14;
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176
177 const DARK_BG: (u8, u8, u8) = (0x18, 0x18, 0x18);
181 const LIGHT_BG: (u8, u8, u8) = (0xFD, 0xF6, 0xE3);
182
183 fn hex_to_rgb(hex: &str) -> Option<(u8, u8, u8)> {
186 let hex = hex.strip_prefix('#')?;
187 if hex.len() != 6 || !hex.bytes().all(|b| b.is_ascii_hexdigit()) {
188 return None;
189 }
190 let channel = |i: usize| u8::from_str_radix(&hex[i..i + 2], 16).ok();
191 Some((channel(0)?, channel(2)?, channel(4)?))
192 }
193
194 fn hue_rgb_on(hue: &str, light: bool) -> Option<(u8, u8, u8)> {
199 if let Some(rgb) = hex_to_rgb(hue) {
200 return Some(rgb);
201 }
202 let (d, l) = match hue {
203 "red" => ((0xFF, 0x5F, 0x5F), (0xCD, 0x00, 0x00)),
204 "green" => ((0x5F, 0xFF, 0x5F), (0x00, 0xCD, 0x00)),
205 "blue" => ((0x5F, 0x5F, 0xFF), (0x00, 0x00, 0xCD)),
206 "magenta" => ((0xFF, 0x5F, 0xFF), (0xCD, 0x00, 0xCD)),
207 "yellow" => ((0xFF, 0xFF, 0x5F), (0xCD, 0xCD, 0x00)),
208 "cyan" => ((0x5F, 0xFF, 0xFF), (0x00, 0xCD, 0xCD)),
209 _ => return None,
210 };
211 Some(if light { l } else { d })
212 }
213
214 fn channel_lin(c: u8) -> f32 {
215 let c = c as f32 / 255.0;
216 if c <= 0.03928 {
217 c / 12.92
218 } else {
219 ((c + 0.055) / 1.055).powf(2.4)
220 }
221 }
222
223 fn relative_luminance((r, g, b): (u8, u8, u8)) -> f32 {
224 0.2126 * channel_lin(r) + 0.7152 * channel_lin(g) + 0.0722 * channel_lin(b)
225 }
226
227 fn contrast_ratio(fg: (u8, u8, u8), bg: (u8, u8, u8)) -> f32 {
228 let l_fg = relative_luminance(fg);
229 let l_bg = relative_luminance(bg);
230 let (hi, lo) = if l_fg > l_bg { (l_fg, l_bg) } else { (l_bg, l_fg) };
231 (hi + 0.05) / (lo + 0.05)
232 }
233
234 const MIN_CONTRAST_DARK: f32 = 3.0;
240 const MIN_CONTRAST_LIGHT: f32 = 2.0;
241
242 fn assert_agent_hue_readable(name: &str, hue: &str, light: bool, bg: (u8, u8, u8)) {
243 let fg =
244 hue_rgb_on(hue, light).unwrap_or_else(|| panic!("{name} ({hue}) is not a supported standard ANSI hue"));
245 let cr = contrast_ratio(fg, bg);
246 let min = if light { MIN_CONTRAST_LIGHT } else { MIN_CONTRAST_DARK };
247 assert!(
248 cr >= min,
249 "{name} ({hue}) on {} bg contrast {cr:.2} below {min}",
250 if light { "light" } else { "dark" }
251 );
252 }
253
254 #[test]
255 fn agent_mode_color_tokens_are_supported() {
256 for (_, hue) in AGENT_MODE_HUE {
257 let supported = AGENT_HUE_NAMES.contains(hue) || hex_to_rgb(hue).is_some();
258 assert!(supported, "mode token {hue} is neither a standard ANSI hue nor #rrggbb");
259 }
260 }
261
262 #[test]
263 fn agent_mode_colors_are_readable_in_dark_and_light_terminals() {
264 for (mode, hue) in AGENT_MODE_HUE {
265 assert_agent_hue_readable(mode, hue, false, DARK_BG);
267 assert_agent_hue_readable(mode, hue, true, LIGHT_BG);
268 }
269 }
270
271 #[test]
274 fn agent_mode_colors_are_distinct() {
275 let hues: Vec<&str> = AGENT_MODE_HUE.iter().map(|(_, h)| *h).collect();
276 for i in 0..hues.len() {
277 for j in (i + 1)..hues.len() {
278 assert_ne!(hues[i], hues[j], "mode hues {} and {} are identical", hues[i], hues[j]);
279 }
280 }
281 }
282
283 #[test]
284 fn agent_mode_hue_resolves_mode_names() {
285 assert_eq!(agent_mode_hue("build"), Some(AGENT_COLOR_BUILD));
286 assert_eq!(agent_mode_hue("auto"), Some(AGENT_COLOR_AUTO));
287 assert_eq!(agent_mode_hue("plan"), Some(AGENT_COLOR_PLAN));
288 assert_eq!(agent_mode_hue("duck"), Some(AGENT_COLOR_DUCK));
289 assert_eq!(agent_mode_hue("unknown"), None);
290 }
291
292 #[test]
293 fn agent_mode_colors_are_wired_into_builtin_primary_agents() {
294 assert_eq!(AGENT_COLOR_BUILD, "#73a18e");
296 assert_eq!(AGENT_COLOR_AUTO, "green");
297 assert_eq!(AGENT_COLOR_PLAN, "blue");
298 assert_eq!(AGENT_COLOR_DUCK, "magenta");
299 }
300
301 #[test]
303 fn chat_input_placeholders_share_one_shortcut_hint() {
304 let hint = chat_input_shortcut_hint!();
305 assert!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP.ends_with(hint));
306 assert!(CHAT_INPUT_PLACEHOLDER_FOLLOW_UP.ends_with(hint));
307 assert!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP.starts_with("Type a request"));
308 assert!(CHAT_INPUT_PLACEHOLDER_FOLLOW_UP.starts_with("Continue"));
309 assert_ne!(CHAT_INPUT_PLACEHOLDER_BOOTSTRAP, CHAT_INPUT_PLACEHOLDER_FOLLOW_UP);
310 }
311
312 #[test]
317 fn meta_bullet_separator_is_the_single_source() {
318 assert_eq!(META_BULLET_SEPARATOR, " • ");
319 }
320
321 #[test]
328 fn removed_shadowed_tui_constants_stay_removed() {
329 const SHADOWED: &[&str] = &[
330 "TUI_ACTIVE_TICK_RATE_HZ",
331 "TUI_IDLE_TICK_RATE_HZ",
332 "TUI_ACTIVE_TIMEOUT_MS",
333 "TUI_SHIMMER_FRAME_INTERVAL_MS",
334 "TUI_SHIMMER_SWEEP_DURATION_MS",
335 "TUI_SCROLL_CURSOR_STEADY_MS",
336 "TUI_MAX_VIEWPORT_WIDTH",
337 "TUI_MAX_VIEWPORT_HEIGHT",
338 "MODAL_WIDTH_RATIO",
339 "MODAL_HEIGHT_RATIO",
340 "MODAL_MAX_WIDTH_RATIO",
341 "MODAL_MAX_HEIGHT_RATIO",
342 "SLASH_PALETTE_MIN_WIDTH",
343 "SLASH_PALETTE_MIN_HEIGHT",
344 "HEADER_SHORTCUT_HINT",
345 "SCROLL_INDICATOR_FORMAT",
346 "HEADER_META_SEPARATOR",
347 "SLASH_PALETTE_TOP_OFFSET",
348 ];
349 let src = include_str!("ui.rs");
351 for name in SHADOWED {
352 let declared = src.lines().any(|line| {
353 let trimmed = line.trim_start();
354 trimmed
355 .strip_prefix("pub const ")
356 .or_else(|| trimmed.strip_prefix("pub(crate) const "))
357 .is_some_and(|rest| rest.starts_with(&format!("{name}:")))
358 });
359 assert!(!declared, "`{name}` is shadowed by vtcode-ui; declare the live value there, not here");
360 }
361 }
362}