1use anyhow::Result;
2use gpui::{
3 prelude::*, px, Action, AppContext, AsyncWindowContext, EventEmitter, FocusHandle,
4 FocusableView, Model, Pixels, Subscription, Task, View, ViewContext, WeakView, WindowContext,
5};
6use language_model::LanguageModelRegistry;
7use language_model_selector::LanguageModelSelector;
8use ui::{prelude::*, ButtonLike, Divider, IconButtonShape, Tab, Tooltip};
9use workspace::dock::{DockPosition, Panel, PanelEvent};
10use workspace::{Pane, Workspace};
11
12use crate::message_editor::MessageEditor;
13use crate::thread::Thread;
14use crate::{NewThread, ToggleFocus, ToggleModelSelector};
15
16pub fn init(cx: &mut AppContext) {
17 cx.observe_new_views(
18 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
19 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
20 workspace.toggle_panel_focus::<AssistantPanel>(cx);
21 });
22 },
23 )
24 .detach();
25}
26
27pub struct AssistantPanel {
28 pane: View<Pane>,
29 thread: Model<Thread>,
30 message_editor: View<MessageEditor>,
31 _subscriptions: Vec<Subscription>,
32}
33
34impl AssistantPanel {
35 pub fn load(
36 workspace: WeakView<Workspace>,
37 cx: AsyncWindowContext,
38 ) -> Task<Result<View<Self>>> {
39 cx.spawn(|mut cx| async move {
40 workspace.update(&mut cx, |workspace, cx| {
41 cx.new_view(|cx| Self::new(workspace, cx))
42 })
43 })
44 }
45
46 fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
47 let pane = cx.new_view(|cx| {
48 let mut pane = Pane::new(
49 workspace.weak_handle(),
50 workspace.project().clone(),
51 Default::default(),
52 None,
53 NewThread.boxed_clone(),
54 cx,
55 );
56 pane.set_can_split(false, cx);
57 pane.set_can_navigate(true, cx);
58
59 pane
60 });
61
62 let thread = cx.new_model(Thread::new);
63 let subscriptions = vec![cx.observe(&thread, |_, _, cx| cx.notify())];
64
65 Self {
66 pane,
67 thread: thread.clone(),
68 message_editor: cx.new_view(|cx| MessageEditor::new(thread, cx)),
69 _subscriptions: subscriptions,
70 }
71 }
72}
73
74impl FocusableView for AssistantPanel {
75 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
76 self.pane.focus_handle(cx)
77 }
78}
79
80impl EventEmitter<PanelEvent> for AssistantPanel {}
81
82impl Panel for AssistantPanel {
83 fn persistent_name() -> &'static str {
84 "AssistantPanel2"
85 }
86
87 fn position(&self, _cx: &WindowContext) -> DockPosition {
88 DockPosition::Right
89 }
90
91 fn position_is_valid(&self, _: DockPosition) -> bool {
92 true
93 }
94
95 fn set_position(&mut self, _position: DockPosition, _cx: &mut ViewContext<Self>) {}
96
97 fn size(&self, _cx: &WindowContext) -> Pixels {
98 px(640.)
99 }
100
101 fn set_size(&mut self, _size: Option<Pixels>, _cx: &mut ViewContext<Self>) {}
102
103 fn is_zoomed(&self, cx: &WindowContext) -> bool {
104 self.pane.read(cx).is_zoomed()
105 }
106
107 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
108 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
109 }
110
111 fn set_active(&mut self, _active: bool, _cx: &mut ViewContext<Self>) {}
112
113 fn pane(&self) -> Option<View<Pane>> {
114 Some(self.pane.clone())
115 }
116
117 fn remote_id() -> Option<proto::PanelId> {
118 Some(proto::PanelId::AssistantPanel)
119 }
120
121 fn icon(&self, _cx: &WindowContext) -> Option<IconName> {
122 Some(IconName::ZedAssistant)
123 }
124
125 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
126 Some("Assistant Panel")
127 }
128
129 fn toggle_action(&self) -> Box<dyn Action> {
130 Box::new(ToggleFocus)
131 }
132}
133
134impl AssistantPanel {
135 fn render_toolbar(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
136 let focus_handle = self.focus_handle(cx);
137
138 h_flex()
139 .id("assistant-toolbar")
140 .justify_between()
141 .gap(DynamicSpacing::Base08.rems(cx))
142 .h(Tab::container_height(cx))
143 .px(DynamicSpacing::Base08.rems(cx))
144 .bg(cx.theme().colors().tab_bar_background)
145 .border_b_1()
146 .border_color(cx.theme().colors().border_variant)
147 .child(h_flex().child(Label::new("Thread Title Goes Here")))
148 .child(
149 h_flex()
150 .gap(DynamicSpacing::Base08.rems(cx))
151 .child(self.render_language_model_selector(cx))
152 .child(Divider::vertical())
153 .child(
154 IconButton::new("new-thread", IconName::Plus)
155 .shape(IconButtonShape::Square)
156 .icon_size(IconSize::Small)
157 .style(ButtonStyle::Subtle)
158 .tooltip({
159 let focus_handle = focus_handle.clone();
160 move |cx| {
161 Tooltip::for_action_in(
162 "New Thread",
163 &NewThread,
164 &focus_handle,
165 cx,
166 )
167 }
168 })
169 .on_click(move |_event, _cx| {
170 println!("New Thread");
171 }),
172 )
173 .child(
174 IconButton::new("open-history", IconName::HistoryRerun)
175 .shape(IconButtonShape::Square)
176 .icon_size(IconSize::Small)
177 .style(ButtonStyle::Subtle)
178 .tooltip(move |cx| Tooltip::text("Open History", cx))
179 .on_click(move |_event, _cx| {
180 println!("Open History");
181 }),
182 )
183 .child(
184 IconButton::new("configure-assistant", IconName::Settings)
185 .shape(IconButtonShape::Square)
186 .icon_size(IconSize::Small)
187 .style(ButtonStyle::Subtle)
188 .tooltip(move |cx| Tooltip::text("Configure Assistant", cx))
189 .on_click(move |_event, _cx| {
190 println!("Configure Assistant");
191 }),
192 ),
193 )
194 }
195
196 fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
197 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
198 let active_model = LanguageModelRegistry::read_global(cx).active_model();
199
200 LanguageModelSelector::new(
201 |model, _cx| {
202 println!("Selected {:?}", model.name());
203 },
204 ButtonLike::new("active-model")
205 .style(ButtonStyle::Subtle)
206 .child(
207 h_flex()
208 .w_full()
209 .gap_0p5()
210 .child(
211 div()
212 .overflow_x_hidden()
213 .flex_grow()
214 .whitespace_nowrap()
215 .child(match (active_provider, active_model) {
216 (Some(provider), Some(model)) => h_flex()
217 .gap_1()
218 .child(
219 Icon::new(
220 model.icon().unwrap_or_else(|| provider.icon()),
221 )
222 .color(Color::Muted)
223 .size(IconSize::XSmall),
224 )
225 .child(
226 Label::new(model.name().0)
227 .size(LabelSize::Small)
228 .color(Color::Muted),
229 )
230 .into_any_element(),
231 _ => Label::new("No model selected")
232 .size(LabelSize::Small)
233 .color(Color::Muted)
234 .into_any_element(),
235 }),
236 )
237 .child(
238 Icon::new(IconName::ChevronDown)
239 .color(Color::Muted)
240 .size(IconSize::XSmall),
241 ),
242 )
243 .tooltip(move |cx| Tooltip::for_action("Change Model", &ToggleModelSelector, cx)),
244 )
245 }
246}
247
248impl Render for AssistantPanel {
249 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
250 v_flex()
251 .key_context("AssistantPanel2")
252 .justify_between()
253 .size_full()
254 .on_action(cx.listener(|_this, _: &NewThread, _cx| {
255 println!("Action: New Thread");
256 }))
257 .child(self.render_toolbar(cx))
258 .child(
259 v_flex()
260 .id("message-list")
261 .gap_2()
262 .size_full()
263 .p_2()
264 .overflow_y_scroll()
265 .bg(cx.theme().colors().panel_background)
266 .children(self.thread.read(cx).messages.iter().map(|message| {
267 v_flex()
268 .p_2()
269 .border_1()
270 .border_color(cx.theme().colors().border_variant)
271 .rounded_md()
272 .child(Label::new(message.role.to_string()))
273 .child(Label::new(message.text.clone()))
274 })),
275 )
276 .child(
277 h_flex()
278 .border_t_1()
279 .border_color(cx.theme().colors().border_variant)
280 .child(self.message_editor.clone()),
281 )
282 }
283}