Skip to main content

ragrig_gpui/model/
component.rs

1//! Agent pipeline components and their status.
2
3use gpui_kit::assets::IconName;
4
5use crate::icon::ChatIcon;
6
7/// The role a component plays in the RAG pipeline.
8#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
9pub enum ComponentKind {
10    /// The chat model.
11    Chat,
12    /// The embedding model.
13    Embedder,
14    /// The vector or document store.
15    Store,
16    /// The document parser.
17    Parser,
18    /// The chunker.
19    Chunker,
20    /// The retrieval strategy.
21    Retriever,
22    /// Conversation memory and query rewriting.
23    Memory,
24    /// A vision or OCR component.
25    Vision,
26    /// Bibliographic reference extraction.
27    References,
28    /// Anything else.
29    Other,
30}
31
32impl ComponentKind {
33    /// Short human-readable name.
34    pub fn label(self) -> &'static str {
35        match self {
36            Self::Chat => "Chat model",
37            Self::Embedder => "Embedder",
38            Self::Store => "Store",
39            Self::Parser => "Parser",
40            Self::Chunker => "Chunker",
41            Self::Retriever => "Retriever",
42            Self::Memory => "Chat memory",
43            Self::Vision => "Vision",
44            Self::References => "References",
45            Self::Other => "Component",
46        }
47    }
48
49    /// Icon used in the agent bar.
50    pub fn icon(self) -> IconName {
51        match self {
52            Self::Chat => IconName::Bot,
53            Self::Embedder => IconName::Cpu,
54            Self::Store => IconName::Database,
55            Self::Parser => IconName::FileText,
56            Self::Chunker => IconName::Pilcrow,
57            Self::Retriever => IconName::Search,
58            Self::Memory => IconName::Brain,
59            Self::Vision => IconName::Eye,
60            Self::References => IconName::BookOpen,
61            Self::Other => IconName::Settings,
62        }
63    }
64}
65
66/// Status of one agent component.
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68pub enum ComponentStatus {
69    /// Not configured or switched off.
70    #[default]
71    Inactive,
72    /// Configured and available.
73    Ready,
74    /// Currently working.
75    Busy,
76    /// Failed; the detail line explains why.
77    Error,
78}
79
80impl ComponentStatus {
81    /// Short human-readable name.
82    pub fn label(self) -> &'static str {
83        match self {
84            Self::Inactive => "inactive",
85            Self::Ready => "ready",
86            Self::Busy => "busy",
87            Self::Error => "error",
88        }
89    }
90
91    /// Returns whether the component deserves a pulsing dot.
92    pub fn is_busy(self) -> bool {
93        matches!(self, Self::Busy)
94    }
95}
96
97/// A switch shown in a component's status popover.
98///
99/// Components that can be turned on and off expose one or more of these.
100/// The bar reports a change through `AgentBarEvent::Toggle`; the host owns
101/// the actual setting and reflects the new value in the component on the next
102/// render.
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub struct ComponentToggle {
105    /// Stable identifier reported when the switch changes.
106    pub id: String,
107    /// Switch label, e.g. `Enabled`.
108    pub label: String,
109    /// Whether the aspect is currently on.
110    pub enabled: bool,
111    /// Optional explanation shown under the label.
112    pub detail: Option<String>,
113}
114
115impl ComponentToggle {
116    /// A switch with an ID, label, and current state.
117    pub fn new(id: impl Into<String>, label: impl Into<String>, enabled: bool) -> Self {
118        Self {
119            id: id.into(),
120            label: label.into(),
121            enabled,
122            detail: None,
123        }
124    }
125
126    /// Describe what switching the aspect on or off means.
127    pub fn detail(mut self, detail: impl Into<String>) -> Self {
128        self.detail = Some(detail.into());
129        self
130    }
131}
132
133/// One component shown in the agent bar.
134#[derive(Clone, Debug, PartialEq)]
135pub struct AgentComponent {
136    /// Stable identifier reported when the component is configured.
137    pub id: String,
138    /// Role in the pipeline.
139    pub kind: ComponentKind,
140    /// Display label, e.g. `DeepSeek Chat`.
141    pub label: String,
142    /// Current status.
143    pub status: ComponentStatus,
144    /// Optional detail line: backend, model, last error.
145    pub detail: Option<String>,
146    /// Optional mark overriding the kind's catalog icon.
147    pub icon: Option<ChatIcon>,
148    /// Switches offered in the component popover.
149    pub toggles: Vec<ComponentToggle>,
150}
151
152impl AgentComponent {
153    /// A component with an ID, kind, and label.
154    pub fn new(id: impl Into<String>, kind: ComponentKind, label: impl Into<String>) -> Self {
155        Self {
156            id: id.into(),
157            kind,
158            label: label.into(),
159            status: ComponentStatus::default(),
160            detail: None,
161            icon: None,
162            toggles: Vec::new(),
163        }
164    }
165
166    /// Replace the catalog icon with a host-supplied mark.
167    ///
168    /// Buttons need a monochrome form; a raster mark falls back to the kind's
169    /// catalog icon.
170    pub fn icon(mut self, icon: impl Into<ChatIcon>) -> Self {
171        self.icon = Some(icon.into());
172        self
173    }
174
175    /// Set the status.
176    pub fn status(mut self, status: ComponentStatus) -> Self {
177        self.status = status;
178        self
179    }
180
181    /// Set the detail line.
182    pub fn detail(mut self, detail: impl Into<String>) -> Self {
183        self.detail = Some(detail.into());
184        self
185    }
186
187    /// Add a switch to the component popover.
188    pub fn toggle(mut self, toggle: ComponentToggle) -> Self {
189        self.toggles.push(toggle);
190        self
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn kinds_have_labels_and_icons() {
200        assert_eq!(ComponentKind::Chat.label(), "Chat model");
201        assert_eq!(ComponentKind::Store.icon(), IconName::Database);
202        assert_eq!(ComponentKind::Chunker.icon(), IconName::Pilcrow);
203        assert_eq!(ComponentKind::References.label(), "References");
204        assert_eq!(ComponentKind::References.icon(), IconName::BookOpen);
205    }
206
207    #[test]
208    fn every_component_icon_draws_visible_pixels() {
209        use gpui_kit::{AssetSource as _, SvgRenderer};
210        use std::sync::Arc;
211
212        let assets = Arc::new(gpui_kit::assets::AllAssets);
213        let renderer = SvgRenderer::new(assets.clone());
214        let kinds = [
215            ComponentKind::Chat,
216            ComponentKind::Embedder,
217            ComponentKind::Store,
218            ComponentKind::Parser,
219            ComponentKind::Chunker,
220            ComponentKind::Retriever,
221            ComponentKind::Memory,
222            ComponentKind::Vision,
223            ComponentKind::References,
224            ComponentKind::Other,
225        ];
226        for kind in kinds {
227            let path = kind.icon().path();
228            let bytes = assets.load(&path).expect("asset load").expect("asset");
229            let parsed = renderer.parse_svg(&bytes).expect("parse svg");
230            let image = renderer.render_parsed(&parsed, 2.0f32).expect("render svg");
231            let pixels = image.as_bytes(0).expect("frame");
232            assert!(
233                pixels.as_chunks::<4>().0.iter().any(|pixel| pixel[3] != 0),
234                "{:?} ({path}) rasterised to a blank image",
235                kind
236            );
237        }
238    }
239
240    #[test]
241    fn components_default_to_inactive() {
242        let component = AgentComponent::new("chat", ComponentKind::Chat, "DeepSeek");
243        assert_eq!(component.status, ComponentStatus::Inactive);
244        assert!(!component.status.is_busy());
245        assert!(component.toggles.is_empty());
246    }
247
248    #[test]
249    fn a_component_can_carry_switches() {
250        let component = AgentComponent::new("memory", ComponentKind::Memory, "Chat memory")
251            .toggle(ComponentToggle::new("memory", "Enabled", true).detail("rewrite"));
252        assert_eq!(component.toggles.len(), 1);
253        assert_eq!(component.toggles[0].id, "memory");
254        assert!(component.toggles[0].enabled);
255    }
256}