Skip to main content

vtcode_commons/
modal_hints.rs

1//! Shared copy for inline list modals: footer key hints and recurring sentences.
2//!
3//! Modal headers stay descriptive (full sentences, no keybindings); all
4//! keybinding guidance lives in `footer_hint` / summary rows using these
5//! constants so approval, limit, and picker modals read identically.
6
7/// Navigate, select, or deny: tool approval, hook approval, session-limit, and
8/// policy-denied prompts.
9pub const APPROVAL_NAVIGATE_DENY: &str = "Use ↑↓ or Tab to navigate • Enter to select • Esc to deny";
10/// Navigate, select, or cancel: MCP approval variant.
11pub const APPROVAL_NAVIGATE_CANCEL: &str = "Use ↑↓ or Tab to navigate • Enter to select • Esc to cancel";
12/// Navigate, select, or stop: tool-loop-limit variant.
13pub const APPROVAL_NAVIGATE_STOP: &str = "Use ↑↓ or Tab to navigate • Enter to select • Esc to stop";
14
15/// Trailing sentence before approval options, e.g. `choose_handling_line("this tool")`.
16pub fn choose_handling_line(object: &str) -> String {
17    format!("Choose how to handle {object}:")
18}
19
20/// Truncate modal body text to `max_chars` characters, appending `…` only when
21/// truncation actually happened so shortened text never reads as complete.
22pub fn truncate_modal_text(text: &str, max_chars: usize) -> String {
23    if text.chars().count() <= max_chars {
24        return text.to_string();
25    }
26    let mut truncated = text.chars().take(max_chars.saturating_sub(1)).collect::<String>();
27    truncated.push('…');
28    truncated
29}
30
31#[cfg(test)]
32mod tests {
33    use super::*;
34
35    #[test]
36    fn approval_hints_share_navigation_prefix_but_differ_by_exit_verb() {
37        for hint in [APPROVAL_NAVIGATE_DENY, APPROVAL_NAVIGATE_CANCEL, APPROVAL_NAVIGATE_STOP] {
38            assert!(hint.starts_with("Use ↑↓ or Tab to navigate • Enter to select • Esc to "));
39        }
40        assert!(APPROVAL_NAVIGATE_DENY.ends_with("deny"));
41        assert!(APPROVAL_NAVIGATE_CANCEL.ends_with("cancel"));
42        assert!(APPROVAL_NAVIGATE_STOP.ends_with("stop"));
43    }
44
45    #[test]
46    fn choose_handling_line_interpolates_object_as_sentence() {
47        assert_eq!(choose_handling_line("this tool"), "Choose how to handle this tool:");
48        assert_eq!(
49            choose_handling_line("workspace lifecycle hooks"),
50            "Choose how to handle workspace lifecycle hooks:"
51        );
52    }
53
54    #[test]
55    fn truncate_modal_text_marks_truncation_with_ellipsis_only_when_shortened() {
56        assert_eq!(truncate_modal_text("short", 10), "short");
57        assert_eq!(truncate_modal_text("exactly-ten", 11), "exactly-ten");
58        assert_eq!(truncate_modal_text("over the limit text", 10), "over the …");
59    }
60}