1use std::rc::Rc;
2
3use editor::Editor;
4use gpui::{AppContext, FocusHandle, Model, View, WeakModel, WeakView};
5use language::Buffer;
6use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
7use workspace::Workspace;
8
9use crate::context_picker::{ConfirmBehavior, ContextPicker};
10use crate::context_store::ContextStore;
11use crate::thread::Thread;
12use crate::thread_store::ThreadStore;
13use crate::ui::ContextPill;
14use crate::{AssistantPanel, ToggleContextPicker};
15
16pub struct ContextStrip {
17 context_store: Model<ContextStore>,
18 context_picker: View<ContextPicker>,
19 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
20 focus_handle: FocusHandle,
21 suggest_context_kind: SuggestContextKind,
22 workspace: WeakView<Workspace>,
23}
24
25impl ContextStrip {
26 pub fn new(
27 context_store: Model<ContextStore>,
28 workspace: WeakView<Workspace>,
29 thread_store: Option<WeakModel<ThreadStore>>,
30 focus_handle: FocusHandle,
31 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
32 suggest_context_kind: SuggestContextKind,
33 cx: &mut ViewContext<Self>,
34 ) -> Self {
35 Self {
36 context_store: context_store.clone(),
37 context_picker: cx.new_view(|cx| {
38 ContextPicker::new(
39 workspace.clone(),
40 thread_store.clone(),
41 context_store.downgrade(),
42 ConfirmBehavior::KeepOpen,
43 cx,
44 )
45 }),
46 context_picker_menu_handle,
47 focus_handle,
48 suggest_context_kind,
49 workspace,
50 }
51 }
52
53 fn suggested_context(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
54 match self.suggest_context_kind {
55 SuggestContextKind::File => self.suggested_file(cx),
56 SuggestContextKind::Thread => self.suggested_thread(cx),
57 }
58 }
59
60 fn suggested_file(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
61 let workspace = self.workspace.upgrade()?;
62 let active_item = workspace.read(cx).active_item(cx)?;
63
64 let editor = active_item.to_any().downcast::<Editor>().ok()?.read(cx);
65 let active_buffer = editor.buffer().read(cx).as_singleton()?;
66
67 let path = active_buffer.read(cx).file()?.path();
68
69 if self.context_store.read(cx).included_file(path).is_some() {
70 return None;
71 }
72
73 let title = path.to_string_lossy().into_owned().into();
74
75 Some(SuggestedContext::File {
76 title,
77 buffer: active_buffer.downgrade(),
78 })
79 }
80
81 fn suggested_thread(&self, cx: &ViewContext<Self>) -> Option<SuggestedContext> {
82 let workspace = self.workspace.upgrade()?;
83 let active_thread = workspace
84 .read(cx)
85 .panel::<AssistantPanel>(cx)?
86 .read(cx)
87 .active_thread(cx);
88 let weak_active_thread = active_thread.downgrade();
89
90 let active_thread = active_thread.read(cx);
91
92 if self
93 .context_store
94 .read(cx)
95 .included_thread(active_thread.id())
96 .is_some()
97 {
98 return None;
99 }
100
101 Some(SuggestedContext::Thread {
102 title: active_thread.summary().unwrap_or("Active Thread".into()),
103 thread: weak_active_thread,
104 })
105 }
106}
107
108impl Render for ContextStrip {
109 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
110 let context_store = self.context_store.read(cx);
111 let context = context_store.context().clone();
112 let context_picker = self.context_picker.clone();
113 let focus_handle = self.focus_handle.clone();
114
115 let suggested_context = self.suggested_context(cx);
116
117 h_flex()
118 .flex_wrap()
119 .gap_1()
120 .child(
121 PopoverMenu::new("context-picker")
122 .menu(move |_cx| Some(context_picker.clone()))
123 .trigger(
124 IconButton::new("add-context", IconName::Plus)
125 .icon_size(IconSize::Small)
126 .style(ui::ButtonStyle::Filled)
127 .tooltip({
128 let focus_handle = focus_handle.clone();
129
130 move |cx| {
131 Tooltip::for_action_in(
132 "Add Context",
133 &ToggleContextPicker,
134 &focus_handle,
135 cx,
136 )
137 }
138 }),
139 )
140 .attach(gpui::Corner::TopLeft)
141 .anchor(gpui::Corner::BottomLeft)
142 .offset(gpui::Point {
143 x: px(0.0),
144 y: px(-16.0),
145 })
146 .with_handle(self.context_picker_menu_handle.clone()),
147 )
148 .when(context.is_empty() && suggested_context.is_none(), {
149 |parent| {
150 parent.child(
151 h_flex()
152 .ml_1p5()
153 .gap_2()
154 .child(
155 Label::new("Add Context")
156 .size(LabelSize::Small)
157 .color(Color::Muted),
158 )
159 .opacity(0.5)
160 .children(
161 KeyBinding::for_action_in(&ToggleContextPicker, &focus_handle, cx)
162 .map(|binding| binding.into_any_element()),
163 ),
164 )
165 }
166 })
167 .children(context.iter().map(|context| {
168 ContextPill::new(context.clone()).on_remove({
169 let context = context.clone();
170 let context_store = self.context_store.clone();
171 Rc::new(cx.listener(move |_this, _event, cx| {
172 context_store.update(cx, |this, _cx| {
173 this.remove_context(&context.id);
174 });
175 cx.notify();
176 }))
177 })
178 }))
179 .when_some(suggested_context, |el, suggested| {
180 el.child(
181 Button::new("add-suggested-context", suggested.title().clone())
182 .on_click({
183 let context_store = self.context_store.clone();
184
185 cx.listener(move |_this, _event, cx| {
186 context_store.update(cx, |context_store, cx| {
187 suggested.accept(context_store, cx);
188 });
189 cx.notify();
190 })
191 })
192 .icon(IconName::Plus)
193 .icon_position(IconPosition::Start)
194 .icon_size(IconSize::XSmall)
195 .icon_color(Color::Muted)
196 .label_size(LabelSize::Small)
197 .style(ButtonStyle::Filled)
198 .tooltip(|cx| {
199 Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
200 }),
201 )
202 })
203 .when(!context.is_empty(), {
204 move |parent| {
205 parent.child(
206 IconButton::new("remove-all-context", IconName::Eraser)
207 .icon_size(IconSize::Small)
208 .tooltip(move |cx| Tooltip::text("Remove All Context", cx))
209 .on_click({
210 let context_store = self.context_store.clone();
211 cx.listener(move |_this, _event, cx| {
212 context_store.update(cx, |this, _cx| this.clear());
213 cx.notify();
214 })
215 }),
216 )
217 }
218 })
219 }
220}
221
222pub enum SuggestContextKind {
223 File,
224 Thread,
225}
226
227#[derive(Clone)]
228pub enum SuggestedContext {
229 File {
230 title: SharedString,
231 buffer: WeakModel<Buffer>,
232 },
233 Thread {
234 title: SharedString,
235 thread: WeakModel<Thread>,
236 },
237}
238
239impl SuggestedContext {
240 pub fn title(&self) -> &SharedString {
241 match self {
242 Self::File { title, .. } => title,
243 Self::Thread { title, .. } => title,
244 }
245 }
246
247 pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
248 match self {
249 Self::File { buffer, title: _ } => {
250 if let Some(buffer) = buffer.upgrade() {
251 context_store.insert_file(buffer.read(cx));
252 };
253 }
254 Self::Thread { thread, title: _ } => {
255 if let Some(thread) = thread.upgrade() {
256 context_store.insert_thread(thread.read(cx));
257 };
258 }
259 }
260 }
261}