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