Skip to main content

ag_agent/model/
session.rs

1//! Session usage statistics produced by agent transports.
2
3/// Known availability of a session worktree diff.
4#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
5pub enum SessionDiffState {
6    /// Diff availability could not be determined, so callers should preserve
7    /// access to diagnostic diff output.
8    #[default]
9    Unknown,
10    /// The latest successful diff refresh returned no content.
11    Empty,
12    /// The latest successful diff refresh returned content.
13    Present,
14}
15
16/// Token and diff usage statistics associated with one agent session or
17/// isolated prompt.
18#[derive(Clone, PartialEq, Eq, Debug, Default)]
19pub struct SessionStats {
20    /// Added diff lines currently attributed to the session worktree.
21    pub added_lines: u64,
22    /// Deleted diff lines currently attributed to the session worktree.
23    pub deleted_lines: u64,
24    /// Availability derived from the latest worktree diff refresh.
25    pub diff_state: SessionDiffState,
26    /// Input/prompt tokens consumed by this session.
27    pub input_tokens: u64,
28    /// Output/response tokens produced by this session.
29    pub output_tokens: u64,
30}
31
32impl SessionStats {
33    /// Returns whether the UI should advertise access to the session diff.
34    ///
35    /// Unknown state retains the shortcut so a subsequent diff attempt can
36    /// surface the underlying Git diagnostic instead of hiding it.
37    pub fn should_show_diff(&self) -> bool {
38        self.diff_state != SessionDiffState::Empty
39    }
40
41    /// Counts added and deleted lines in one git patch while ignoring file
42    /// header markers such as `+++` and `---`.
43    pub fn line_change_counts(diff: &str) -> (u64, u64) {
44        diff.lines()
45            .fold((0_u64, 0_u64), |(added_lines, deleted_lines), line| {
46                if line.starts_with('+') && !line.starts_with("+++") {
47                    return (added_lines.saturating_add(1), deleted_lines);
48                }
49
50                if line.starts_with('-') && !line.starts_with("---") {
51                    return (added_lines, deleted_lines.saturating_add(1));
52                }
53
54                (added_lines, deleted_lines)
55            })
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    #[test]
64    fn should_show_diff_hides_only_known_empty_diffs() {
65        // Arrange
66        let unknown = SessionStats::default();
67        let empty = SessionStats {
68            diff_state: SessionDiffState::Empty,
69            ..SessionStats::default()
70        };
71        let present = SessionStats {
72            diff_state: SessionDiffState::Present,
73            ..SessionStats::default()
74        };
75
76        // Act, Assert
77        assert!(unknown.should_show_diff());
78        assert!(!empty.should_show_diff());
79        assert!(present.should_show_diff());
80    }
81}