Skip to main content

gpui_ai/
stream.rs

1//! Shared lifecycle and revision tracking for progressively arriving content.
2//!
3//! Applications own a [`Progressive`] value and mutate it as work advances.
4//! Components receive snapshots, so no component needs its own timer or
5//! competing lifecycle state machine.
6
7use gpui::SharedString;
8
9/// Lifecycle shared by every progressive component.
10#[derive(Debug, Clone, PartialEq, Eq, Default)]
11pub enum ProgressState {
12    /// Work has been declared but has not started.
13    #[default]
14    Pending,
15    /// Work is active and its content may still change.
16    Running,
17    /// Work completed successfully.
18    Complete,
19    /// Work stopped with a user-visible reason.
20    Failed(SharedString),
21}
22
23/// Typed content paired with a shared lifecycle and monotonic revision.
24///
25/// The revision changes only when lifecycle or content actually changes. It
26/// can therefore be used as a cheap cache key by expensive renderers.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct Progressive<T> {
29    content: T,
30    state: ProgressState,
31    revision: u64,
32}
33
34impl<T> Progressive<T> {
35    /// Creates pending progressive content.
36    pub fn pending(content: T) -> Self {
37        Self::with_state(content, ProgressState::Pending)
38    }
39
40    /// Creates running progressive content.
41    pub fn running(content: T) -> Self {
42        Self::with_state(content, ProgressState::Running)
43    }
44
45    /// Creates completed progressive content.
46    pub fn complete(content: T) -> Self {
47        Self::with_state(content, ProgressState::Complete)
48    }
49
50    /// Creates failed progressive content.
51    pub fn failed(content: T, reason: impl Into<SharedString>) -> Self {
52        Self::with_state(content, ProgressState::Failed(reason.into()))
53    }
54
55    fn with_state(content: T, state: ProgressState) -> Self {
56        Self {
57            content,
58            state,
59            revision: 0,
60        }
61    }
62
63    /// Returns the current typed content.
64    pub fn content(&self) -> &T {
65        &self.content
66    }
67
68    /// Returns the current lifecycle.
69    pub fn state(&self) -> &ProgressState {
70        &self.state
71    }
72
73    /// Returns the monotonic content/lifecycle revision.
74    pub fn revision(&self) -> u64 {
75        self.revision
76    }
77
78    /// Moves pending or terminal work into the running state.
79    pub fn start(&mut self) {
80        self.set_state(ProgressState::Running);
81    }
82
83    /// Marks work complete.
84    pub fn finish(&mut self) {
85        self.set_state(ProgressState::Complete);
86    }
87
88    /// Marks work failed with a user-visible reason.
89    pub fn fail(&mut self, reason: impl Into<SharedString>) {
90        self.set_state(ProgressState::Failed(reason.into()));
91    }
92
93    fn set_state(&mut self, state: ProgressState) {
94        if self.state != state {
95            self.state = state;
96            self.bump_revision();
97        }
98    }
99
100    fn bump_revision(&mut self) {
101        self.revision = self.revision.saturating_add(1);
102    }
103}
104
105/// Progressively arriving text.
106///
107/// # Example
108///
109/// ```
110/// use gpui_ai::stream::{ProgressState, StreamedContent};
111///
112/// let mut answer = StreamedContent::new();
113/// answer.append("Hello, ");
114/// answer.append("world.");
115/// answer.finish();
116/// assert_eq!(answer.text(), "Hello, world.");
117/// assert_eq!(answer.state(), &ProgressState::Complete);
118/// ```
119pub type StreamedContent = Progressive<String>;
120
121impl Progressive<String> {
122    /// Creates empty running text.
123    pub fn new() -> Self {
124        Self::running(String::new())
125    }
126
127    /// Creates text that is already complete.
128    pub fn done(text: impl Into<String>) -> Self {
129        Self::complete(text.into())
130    }
131
132    /// Returns the accumulated text.
133    pub fn text(&self) -> &str {
134        &self.content
135    }
136
137    /// Appends a non-empty chunk and advances the revision.
138    pub fn append(&mut self, chunk: &str) {
139        if !chunk.is_empty() {
140            self.content.push_str(chunk);
141            self.bump_revision();
142        }
143    }
144
145    /// Replaces text when the replacement differs and advances the revision.
146    pub fn replace(&mut self, text: impl Into<String>) {
147        let text = text.into();
148        if self.content != text {
149            self.content = text;
150            self.bump_revision();
151        }
152    }
153
154    /// Returns whether the text is still arriving.
155    pub fn is_streaming(&self) -> bool {
156        self.state == ProgressState::Running
157    }
158}
159
160impl Default for Progressive<String> {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::{ProgressState, Progressive, StreamedContent};
169
170    #[test]
171    fn constructors_preserve_content_and_initial_state() {
172        let pending = Progressive::pending(vec![1, 2]);
173        let running = Progressive::running(vec![3]);
174        let complete = Progressive::complete(vec![4]);
175        let failed = Progressive::failed(vec![5], "offline");
176
177        assert_eq!(pending.content(), &[1, 2]);
178        assert_eq!(pending.state(), &ProgressState::Pending);
179        assert_eq!(running.state(), &ProgressState::Running);
180        assert_eq!(complete.state(), &ProgressState::Complete);
181        assert_eq!(failed.state(), &ProgressState::Failed("offline".into()));
182        assert_eq!(pending.revision(), 0);
183    }
184
185    #[test]
186    fn lifecycle_revision_changes_only_for_real_transitions() {
187        let mut value = Progressive::pending(());
188        value.start();
189        assert_eq!(value.revision(), 1);
190        value.start();
191        assert_eq!(value.revision(), 1);
192        value.finish();
193        assert_eq!(value.revision(), 2);
194        value.finish();
195        assert_eq!(value.revision(), 2);
196        value.fail("late failure");
197        assert_eq!(value.revision(), 3);
198        value.fail("late failure");
199        assert_eq!(value.revision(), 3);
200    }
201
202    #[test]
203    fn text_mutations_track_meaningful_revisions() {
204        let mut text = StreamedContent::new();
205        assert_eq!(text.state(), &ProgressState::Running);
206        text.append("");
207        assert_eq!(text.revision(), 0);
208        text.append("hello");
209        assert_eq!(text.text(), "hello");
210        assert_eq!(text.revision(), 1);
211        text.replace("hello");
212        assert_eq!(text.revision(), 1);
213        text.replace("hello, world");
214        assert_eq!(text.text(), "hello, world");
215        assert_eq!(text.revision(), 2);
216        text.finish();
217        assert!(!text.is_streaming());
218        assert_eq!(text.revision(), 3);
219    }
220}