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