Skip to main content

vtcode_commons/
message_timestamp.rs

1//! Wall-clock timestamps for the final agent response of a turn.
2//!
3//! Each completed turn carries one short local-time footer (e.g. `· 3:12 PM`)
4//! after its final agent message, so the clock marks the turn's summary point
5//! instead of repeating on every intermediate response. Formatting lives here
6//! so the TUI transcript, plain CLI output, and future desktop/export surfaces
7//! share one shape.
8
9use chrono::{DateTime, Local};
10
11/// Format a local timestamp the way agent footers display it (`3:12 PM`).
12///
13/// Twelve-hour clock without seconds or leading zeros, matching the chat-style
14/// reference. Takes the instant as a parameter so tests stay deterministic.
15pub fn format_message_time(now: DateTime<Local>) -> String {
16    now.format("%-I:%M %p").to_string()
17}
18
19/// Build the full footer line for a turn's final agent response (`· 3:12 PM`).
20///
21/// The leading middot marks the line as chrome in no-color output where
22/// dimming is unavailable.
23pub fn message_timestamp_footer(now: DateTime<Local>) -> String {
24    format!("· {}", format_message_time(now))
25}
26
27/// Build the footer for the current local time.
28pub fn current_message_timestamp_footer() -> String {
29    message_timestamp_footer(Local::now())
30}
31
32#[cfg(test)]
33mod tests {
34    use super::*;
35    use chrono::TimeZone;
36
37    fn local_at(hour: u32, minute: u32) -> DateTime<Local> {
38        Local
39            .with_ymd_and_hms(2026, 11, 2, hour, minute, 0)
40            .single()
41            .expect("fixture timestamp must be unambiguous")
42    }
43
44    #[test]
45    fn formats_afternoon_without_leading_zero() {
46        assert_eq!(format_message_time(local_at(15, 12)), "3:12 PM");
47    }
48
49    #[test]
50    fn formats_single_digit_morning_hour() {
51        assert_eq!(format_message_time(local_at(9, 5)), "9:05 AM");
52    }
53
54    #[test]
55    fn formats_double_digit_morning_hour() {
56        assert_eq!(format_message_time(local_at(11, 55)), "11:55 AM");
57    }
58
59    #[test]
60    fn midnight_is_twelve_am_not_zero() {
61        assert_eq!(format_message_time(local_at(0, 5)), "12:05 AM");
62    }
63
64    #[test]
65    fn noon_is_twelve_pm_not_zero() {
66        assert_eq!(format_message_time(local_at(12, 0)), "12:00 PM");
67    }
68
69    #[test]
70    fn footer_prefixes_time_with_middot() {
71        assert_eq!(message_timestamp_footer(local_at(15, 12)), "· 3:12 PM");
72    }
73
74    #[test]
75    fn current_footer_has_footer_shape() {
76        let footer = current_message_timestamp_footer();
77        let time = footer.strip_prefix("· ").expect("current footer must carry the middot");
78        assert!(time.contains(':'), "current footer must show clock time: {footer:?}");
79        assert!(time.ends_with("AM") || time.ends_with("PM"), "current footer must show meridiem: {footer:?}");
80    }
81}