Skip to main content

vtcode_commons/
history_scope.rs

1//! Cross-platform prompt-history search scope.
2//!
3//! Shared by the CLI history picker, the desktop history palette, and the
4//! desktop service so scope labels, cycling order, and wire values cannot
5//! drift between surfaces.
6
7use serde::{Deserialize, Serialize};
8
9/// Which sessions contribute prompt-history matches.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum HistoryScope {
13    /// Every session in every workspace.
14    #[default]
15    Everywhere,
16    /// Sessions in the current workspace/project only.
17    Project,
18    /// The current session/thread only.
19    Session,
20}
21
22impl HistoryScope {
23    /// Short display label used in picker titles; also the wire value.
24    #[must_use]
25    pub fn label(self) -> &'static str {
26        match self {
27            Self::Everywhere => "everywhere",
28            Self::Project => "project",
29            Self::Session => "session",
30        }
31    }
32
33    /// Title-case label for switcher buttons. English-only until
34    /// native-quality translations ship (thread-menu precedent).
35    #[must_use]
36    pub fn title(self) -> &'static str {
37        match self {
38            Self::Everywhere => "Everywhere",
39            Self::Project => "Project",
40            Self::Session => "Session",
41        }
42    }
43
44    /// Next scope in the Ctrl+S cycle order (broad to narrow).
45    #[must_use]
46    pub fn cycle_next(self) -> Self {
47        match self {
48            Self::Everywhere => Self::Project,
49            Self::Project => Self::Session,
50            Self::Session => Self::Everywhere,
51        }
52    }
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn cycle_runs_broad_to_narrow_and_wraps() {
61        assert_eq!(HistoryScope::Everywhere.cycle_next(), HistoryScope::Project);
62        assert_eq!(HistoryScope::Project.cycle_next(), HistoryScope::Session);
63        assert_eq!(HistoryScope::Session.cycle_next(), HistoryScope::Everywhere);
64    }
65
66    #[test]
67    fn labels_match_wire_values() {
68        for scope in [HistoryScope::Everywhere, HistoryScope::Project, HistoryScope::Session] {
69            let encoded = serde_json::to_value(scope).expect("scope serializes");
70            assert_eq!(encoded, serde_json::Value::String(scope.label().to_string()));
71            let decoded: HistoryScope = serde_json::from_value(encoded).expect("scope deserializes");
72            assert_eq!(decoded, scope);
73        }
74        assert_eq!(HistoryScope::Everywhere.title(), "Everywhere");
75        assert_eq!(HistoryScope::Project.title(), "Project");
76        assert_eq!(HistoryScope::Session.title(), "Session");
77    }
78
79    #[test]
80    fn unknown_wire_values_reject() {
81        for raw in ["", "EventEverywhere", "workspace", "galaxy"] {
82            assert!(
83                serde_json::from_value::<HistoryScope>(serde_json::Value::String(raw.to_string())).is_err(),
84                "{raw:?} must not parse as a scope"
85            );
86        }
87    }
88}