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}