Skip to main content

kimun_notes/rag/
mod.rs

1//! All wiring for the optional RAG server lives in this module — config
2//! reading, client construction (`client`) and the background sync loop
3//! (`sync`). Everything talks to the server through [`crate::server_client`];
4//! the rest of the TUI only consumes these helpers and renders status.
5
6mod client;
7mod sync;
8
9pub use client::{rag_client, rag_configured};
10pub use sync::spawn_rag_sync;
11
12/// RAG connection status surfaced in the footer. `Disabled` (no server
13/// configured) is never sent — the loop simply doesn't start — so the footer
14/// shows nothing.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum RagStatus {
17    Disabled,
18    Offline,
19    /// Reachable but the server rejects our credentials: it requires a bearer
20    /// token and none is configured, or API calls come back 401/403 (wrong
21    /// token). Distinct from `Offline` so the user learns it's a token
22    /// problem, not an unreachable server.
23    Unauthorized,
24    /// Reachable but the server has no embedder configured: nothing
25    /// works server-side, so the loop skips pushing and reconciling entirely —
26    /// every call would 503 — and just reports the state.
27    NotConfigured,
28    /// Reachable, a sync pass in flight. `llm_available` carries whether the
29    /// server has an LLM configured (question-answering possible), so Ask stays
30    /// gated consistently while syncing.
31    Syncing {
32        llm_available: bool,
33    },
34    /// Reachable and idle. `llm_available` = the server has an LLM (Q&A on);
35    /// `false` = semantic-only (search only).
36    Online {
37        llm_available: bool,
38    },
39}
40
41impl RagStatus {
42    /// Short footer label, or `None` when nothing should show.
43    pub fn label(self) -> Option<&'static str> {
44        match self {
45            RagStatus::Disabled => None,
46            RagStatus::Offline => Some("rag: offline"),
47            RagStatus::Unauthorized => Some("rag: unauthorized"),
48            RagStatus::NotConfigured => Some("rag: not configured"),
49            RagStatus::Syncing { .. } => Some("rag: syncing"),
50            RagStatus::Online { .. } => Some("rag: online"),
51        }
52    }
53
54    /// Whether question-answering (Ask) is available right now: the server is
55    /// reachable AND has an LLM configured. `false` when offline, disabled, or
56    /// connected to a semantic-only server — the ASK rail entry is hidden in
57    /// those cases.
58    pub fn llm_available(self) -> bool {
59        matches!(
60            self,
61            RagStatus::Online {
62                llm_available: true
63            } | RagStatus::Syncing {
64                llm_available: true
65            }
66        )
67    }
68
69    /// Whether semantic search is usable right now: the server is reachable AND
70    /// has an embedder — i.e. `Online`/`Syncing`, regardless of `llm_available`
71    /// (a semantic-only server still searches). `false` for `Offline`,
72    /// `Unauthorized`, `NotConfigured` and `Disabled`. The SEM rail entry is
73    /// driven by this, mirroring how ASK is driven by `llm_available` — a
74    /// configured-but-unreachable server hides SEM just as it hides ASK.
75    pub fn search_available(self) -> bool {
76        matches!(self, RagStatus::Online { .. } | RagStatus::Syncing { .. })
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn not_configured_status_labels_and_gates() {
86        assert_eq!(
87            RagStatus::NotConfigured.label(),
88            Some("rag: not configured")
89        );
90        assert!(!RagStatus::NotConfigured.llm_available());
91    }
92
93    #[test]
94    fn unauthorized_status_labels_and_gates() {
95        assert_eq!(RagStatus::Unauthorized.label(), Some("rag: unauthorized"));
96        assert!(!RagStatus::Unauthorized.llm_available());
97    }
98
99    #[test]
100    fn search_available_tracks_reachable_with_embedder() {
101        // Online/Syncing → searchable, whether or not an LLM is configured
102        // (a semantic-only server still searches).
103        assert!(
104            RagStatus::Online {
105                llm_available: false
106            }
107            .search_available()
108        );
109        assert!(
110            RagStatus::Online {
111                llm_available: true
112            }
113            .search_available()
114        );
115        assert!(
116            RagStatus::Syncing {
117                llm_available: false
118            }
119            .search_available()
120        );
121        assert!(
122            RagStatus::Syncing {
123                llm_available: true
124            }
125            .search_available()
126        );
127        // Not reachable / no embedder → not searchable.
128        assert!(!RagStatus::Offline.search_available());
129        assert!(!RagStatus::Unauthorized.search_available());
130        assert!(!RagStatus::NotConfigured.search_available());
131        assert!(!RagStatus::Disabled.search_available());
132    }
133
134    #[test]
135    fn semantic_only_server_searches_but_does_not_answer() {
136        let semantic_only = RagStatus::Online {
137            llm_available: false,
138        };
139        assert!(semantic_only.search_available(), "SEM must show");
140        assert!(!semantic_only.llm_available(), "ASK must stay hidden");
141    }
142}