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