1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
//! Interaction cues: the moments an application may want to mark with a
//! sound, haptic, or notification.
//!
//! gpui-ai does not play audio. Components emit a [`Cue`] at the moments
//! that matter (a reply arriving, a response settling, text copied, a
//! prompt submitted, …) and an application can observe them in one place:
//!
//! ```no_run
//! # fn example(cx: &mut gpui::App, play: fn(&str)) {
//! use gpui_ai::cues::{self, Cue};
//!
//! let _cues = cues::observe(cx, move |cue, _cx| match cue {
//! Cue::ResponseSettled { .. } => play("chime"),
//! Cue::Copied => play("tick"),
//! _ => {}
//! });
//! // Keep `_cues` alive for as long as the sounds should play.
//! # }
//! ```
//!
//! Cues are hints, never state: every cue corresponds to a typed component
//! event or snapshot transition that the application already receives.
use gpui::{App, Global, SharedString};
use std::{cell::Cell, rc::Rc};
/// A moment worth a cue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cue {
/// A new message entered a conversation.
MessageArrived {
/// Stable message identifier.
message_id: SharedString,
},
/// A streaming response finished (successfully or not).
ResponseSettled {
/// Stable message identifier.
message_id: SharedString,
/// Whether the response completed rather than failed.
succeeded: bool,
},
/// Text was copied to the clipboard.
Copied,
/// The user submitted a prompt.
Submitted,
/// The user cancelled running work.
Cancelled,
/// The user chose a starter suggestion.
SuggestionSelected,
/// The user switched conversations.
ThreadSelected,
/// A tool call or approval gate was decided.
Decided {
/// Whether the decision was an approval.
approved: bool,
},
}
type Observer = Rc<dyn Fn(&Cue, &mut App)>;
struct ObserverRegistration {
active: Rc<Cell<bool>>,
observer: Observer,
}
#[derive(Default)]
struct CueHub {
observers: Vec<ObserverRegistration>,
}
impl Global for CueHub {}
/// Keeps a cue observer registered; dropping it unregisters the observer.
#[must_use = "dropping the subscription stops cue delivery"]
pub struct CueSubscription {
active: Option<Rc<Cell<bool>>>,
}
impl CueSubscription {
/// Detaches the observer for the rest of the application's lifetime.
pub fn detach(mut self) {
self.active.take();
}
}
impl Drop for CueSubscription {
fn drop(&mut self) {
// The registration owns another reference, so this signal remains
// valid until that app next emits and prunes the observer.
if let Some(active) = self.active.as_ref() {
active.set(false);
}
}
}
/// Registers an observer for every cue emitted by gpui-ai components.
pub fn observe(cx: &mut App, observer: impl Fn(&Cue, &mut App) + 'static) -> CueSubscription {
let active = Rc::new(Cell::new(true));
cx.default_global::<CueHub>()
.observers
.push(ObserverRegistration {
active: active.clone(),
observer: Rc::new(observer),
});
CueSubscription {
active: Some(active),
}
}
/// Emits a cue to every live observer. Components call this at the moments
/// described on [`Cue`]; applications may also emit their own.
pub fn emit(cx: &mut App, cue: Cue) {
if !cx.has_global::<CueHub>() {
return;
}
let observers: Vec<Observer> = {
let hub = cx.global_mut::<CueHub>();
hub.observers
.retain(|registration| registration.active.get());
hub.observers
.iter()
.map(|registration| registration.observer.clone())
.collect()
};
// Observers run outside the hub borrow: one of them may register another
// observer or drop a subscription.
for observer in observers {
observer(&cue, cx);
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::TestAppContext;
use std::{cell::RefCell, rc::Rc};
#[gpui::test]
fn observers_receive_cues_until_dropped(cx: &mut TestAppContext) {
let received = Rc::new(RefCell::new(Vec::new()));
let subscription = cx.update(|cx| {
let received = received.clone();
observe(cx, move |cue, _| received.borrow_mut().push(cue.clone()))
});
cx.update(|cx| emit(cx, Cue::Copied));
assert_eq!(received.borrow().as_slice(), &[Cue::Copied]);
drop(subscription);
cx.update(|cx| emit(cx, Cue::Submitted));
assert_eq!(received.borrow().as_slice(), &[Cue::Copied]);
}
#[gpui::test]
fn emitting_without_observers_is_a_no_op(cx: &mut TestAppContext) {
cx.update(|cx| emit(cx, Cue::Cancelled));
}
}