1use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
2use agent_settings::AgentSettings;
3use anyhow::Result;
4use assistant_text_thread::TextThreadStore;
5use db::kvp::KEY_VALUE_STORE;
6use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
7use fs::Fs;
8use gpui::{
9 Action, AsyncWindowContext, Entity, EventEmitter, Focusable, Pixels, Subscription, Task,
10 WeakEntity, actions, prelude::*,
11};
12use project::Project;
13use prompt_store::{PromptBuilder, PromptStore};
14use serde::{Deserialize, Serialize};
15use settings::{Settings as _, update_settings_file};
16use std::sync::Arc;
17use ui::{App, Context, IconName, IntoElement, ParentElement, Render, Styled, Window};
18use util::ResultExt;
19use workspace::{
20 Panel, Workspace,
21 dock::{ClosePane, DockPosition, PanelEvent, UtilityPane},
22 utility_pane::{UtilityPaneSlot, utility_slot_for_dock_position},
23};
24
25use crate::agent_thread_pane::{
26 AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId,
27};
28use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent};
29
30const AGENTS_PANEL_KEY: &str = "agents_panel";
31
32#[derive(Serialize, Deserialize, Debug)]
33struct SerializedAgentsPanel {
34 width: Option<Pixels>,
35 pane: Option<SerializedAgentThreadPane>,
36}
37
38actions!(
39 agents,
40 [
41 /// Toggle the visibility of the agents panel.
42 ToggleAgentsPanel
43 ]
44);
45
46pub fn init(cx: &mut App) {
47 cx.observe_new(|workspace: &mut Workspace, _, _| {
48 workspace.register_action(|workspace, _: &ToggleAgentsPanel, window, cx| {
49 workspace.toggle_panel_focus::<AgentsPanel>(window, cx);
50 });
51 })
52 .detach();
53}
54
55pub struct AgentsPanel {
56 focus_handle: gpui::FocusHandle,
57 workspace: WeakEntity<Workspace>,
58 project: Entity<Project>,
59 agent_thread_pane: Option<Entity<AgentThreadPane>>,
60 history: Entity<AcpThreadHistory>,
61 history_store: Entity<HistoryStore>,
62 prompt_store: Option<Entity<PromptStore>>,
63 fs: Arc<dyn Fs>,
64 width: Option<Pixels>,
65 pending_serialization: Task<Option<()>>,
66 _subscriptions: Vec<Subscription>,
67}
68
69impl AgentsPanel {
70 pub fn load(
71 workspace: WeakEntity<Workspace>,
72 cx: AsyncWindowContext,
73 ) -> Task<Result<Entity<Self>, anyhow::Error>> {
74 cx.spawn(async move |cx| {
75 let serialized_panel = cx
76 .background_spawn(async move {
77 KEY_VALUE_STORE
78 .read_kvp(AGENTS_PANEL_KEY)
79 .ok()
80 .flatten()
81 .and_then(|panel| {
82 serde_json::from_str::<SerializedAgentsPanel>(&panel).ok()
83 })
84 })
85 .await;
86
87 let (fs, project, prompt_builder) = workspace.update(cx, |workspace, cx| {
88 let fs = workspace.app_state().fs.clone();
89 let project = workspace.project().clone();
90 let prompt_builder = PromptBuilder::load(fs.clone(), false, cx);
91 (fs, project, prompt_builder)
92 })?;
93
94 let text_thread_store = workspace
95 .update(cx, |_, cx| {
96 TextThreadStore::new(
97 project.clone(),
98 prompt_builder.clone(),
99 Default::default(),
100 cx,
101 )
102 })?
103 .await?;
104
105 let prompt_store = workspace
106 .update(cx, |_, cx| PromptStore::global(cx))?
107 .await
108 .log_err();
109
110 workspace.update_in(cx, |_, window, cx| {
111 cx.new(|cx| {
112 let mut panel = Self::new(
113 workspace.clone(),
114 fs,
115 project,
116 prompt_store,
117 text_thread_store,
118 window,
119 cx,
120 );
121 if let Some(serialized_panel) = serialized_panel {
122 panel.width = serialized_panel.width;
123 if let Some(serialized_pane) = serialized_panel.pane {
124 panel.restore_utility_pane(serialized_pane, window, cx);
125 }
126 }
127 panel
128 })
129 })
130 })
131 }
132
133 fn new(
134 workspace: WeakEntity<Workspace>,
135 fs: Arc<dyn Fs>,
136 project: Entity<Project>,
137 prompt_store: Option<Entity<PromptStore>>,
138 text_thread_store: Entity<TextThreadStore>,
139 window: &mut Window,
140 cx: &mut ui::Context<Self>,
141 ) -> Self {
142 let focus_handle = cx.focus_handle();
143
144 let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
145 let history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
146
147 let this = cx.weak_entity();
148 let subscriptions = vec![
149 cx.subscribe_in(&history, window, Self::handle_history_event),
150 cx.on_flags_ready(move |_, cx| {
151 this.update(cx, |_, cx| {
152 cx.notify();
153 })
154 .ok();
155 }),
156 ];
157
158 Self {
159 focus_handle,
160 workspace,
161 project,
162 agent_thread_pane: None,
163 history,
164 history_store,
165 prompt_store,
166 fs,
167 width: None,
168 pending_serialization: Task::ready(None),
169 _subscriptions: subscriptions,
170 }
171 }
172
173 fn restore_utility_pane(
174 &mut self,
175 serialized_pane: SerializedAgentThreadPane,
176 window: &mut Window,
177 cx: &mut Context<Self>,
178 ) {
179 let Some(thread_id) = &serialized_pane.thread_id else {
180 return;
181 };
182
183 let entry = self
184 .history_store
185 .read(cx)
186 .entries()
187 .find(|e| match (&e.id(), thread_id) {
188 (
189 HistoryEntryId::AcpThread(session_id),
190 SerializedHistoryEntryId::AcpThread(id),
191 ) => session_id.to_string() == *id,
192 (HistoryEntryId::TextThread(path), SerializedHistoryEntryId::TextThread(id)) => {
193 path.to_string_lossy() == *id
194 }
195 _ => false,
196 });
197
198 if let Some(entry) = entry {
199 self.open_thread(
200 entry,
201 serialized_pane.expanded,
202 serialized_pane.width,
203 window,
204 cx,
205 );
206 }
207 }
208
209 fn handle_utility_pane_event(
210 &mut self,
211 _utility_pane: Entity<AgentThreadPane>,
212 event: &AgentsUtilityPaneEvent,
213 cx: &mut Context<Self>,
214 ) {
215 match event {
216 AgentsUtilityPaneEvent::StateChanged => {
217 self.serialize(cx);
218 cx.notify();
219 }
220 }
221 }
222
223 fn handle_close_pane_event(
224 &mut self,
225 _utility_pane: Entity<AgentThreadPane>,
226 _event: &ClosePane,
227 cx: &mut Context<Self>,
228 ) {
229 self.agent_thread_pane = None;
230 self.serialize(cx);
231 cx.notify();
232 }
233
234 fn handle_history_event(
235 &mut self,
236 _history: &Entity<AcpThreadHistory>,
237 event: &ThreadHistoryEvent,
238 window: &mut Window,
239 cx: &mut Context<Self>,
240 ) {
241 match event {
242 ThreadHistoryEvent::Open(entry) => {
243 self.open_thread(entry.clone(), true, None, window, cx);
244 }
245 }
246 }
247
248 fn open_thread(
249 &mut self,
250 entry: HistoryEntry,
251 expanded: bool,
252 width: Option<Pixels>,
253 window: &mut Window,
254 cx: &mut Context<Self>,
255 ) {
256 let entry_id = entry.id();
257
258 if let Some(existing_pane) = &self.agent_thread_pane {
259 if existing_pane.read(cx).thread_id() == Some(entry_id) {
260 existing_pane.update(cx, |pane, cx| {
261 pane.set_expanded(true, cx);
262 });
263 return;
264 }
265 }
266
267 let fs = self.fs.clone();
268 let workspace = self.workspace.clone();
269 let project = self.project.clone();
270 let history_store = self.history_store.clone();
271 let prompt_store = self.prompt_store.clone();
272
273 let agent_thread_pane = cx.new(|cx| {
274 let mut pane = AgentThreadPane::new(workspace.clone(), cx);
275 pane.open_thread(
276 entry,
277 fs,
278 workspace.clone(),
279 project,
280 history_store,
281 prompt_store,
282 window,
283 cx,
284 );
285 if let Some(width) = width {
286 pane.set_width(Some(width), cx);
287 }
288 pane.set_expanded(expanded, cx);
289 pane
290 });
291
292 let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
293 let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
294
295 self._subscriptions.push(state_subscription);
296 self._subscriptions.push(close_subscription);
297
298 let slot = self.utility_slot(window, cx);
299 let panel_id = cx.entity_id();
300
301 if let Some(workspace) = self.workspace.upgrade() {
302 workspace.update(cx, |workspace, cx| {
303 workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
304 });
305 }
306
307 self.agent_thread_pane = Some(agent_thread_pane);
308 self.serialize(cx);
309 cx.notify();
310 }
311
312 fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
313 let position = self.position(window, cx);
314 utility_slot_for_dock_position(position)
315 }
316
317 fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
318 if let Some(pane) = &self.agent_thread_pane {
319 let slot = self.utility_slot(window, cx);
320 let panel_id = cx.entity_id();
321 let pane = pane.clone();
322
323 if let Some(workspace) = self.workspace.upgrade() {
324 workspace.update(cx, |workspace, cx| {
325 workspace.register_utility_pane(slot, panel_id, pane, cx);
326 });
327 }
328 }
329 }
330
331 fn serialize(&mut self, cx: &mut Context<Self>) {
332 let width = self.width;
333 let pane = self
334 .agent_thread_pane
335 .as_ref()
336 .map(|pane| pane.read(cx).serialize());
337
338 self.pending_serialization = cx.background_spawn(async move {
339 KEY_VALUE_STORE
340 .write_kvp(
341 AGENTS_PANEL_KEY.into(),
342 serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
343 )
344 .await
345 .log_err()
346 });
347 }
348}
349
350impl EventEmitter<PanelEvent> for AgentsPanel {}
351
352impl Focusable for AgentsPanel {
353 fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
354 self.focus_handle.clone()
355 }
356}
357
358impl Panel for AgentsPanel {
359 fn persistent_name() -> &'static str {
360 "AgentsPanel"
361 }
362
363 fn panel_key() -> &'static str {
364 AGENTS_PANEL_KEY
365 }
366
367 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
368 match AgentSettings::get_global(cx).agents_panel_dock {
369 settings::DockSide::Left => DockPosition::Left,
370 settings::DockSide::Right => DockPosition::Right,
371 }
372 }
373
374 fn position_is_valid(&self, position: DockPosition) -> bool {
375 position != DockPosition::Bottom
376 }
377
378 fn set_position(
379 &mut self,
380 position: DockPosition,
381 window: &mut Window,
382 cx: &mut Context<Self>,
383 ) {
384 update_settings_file(self.fs.clone(), cx, move |settings, _| {
385 settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
386 DockPosition::Left => settings::DockSide::Left,
387 DockPosition::Bottom => settings::DockSide::Right,
388 DockPosition::Right => settings::DockSide::Left,
389 });
390 });
391 self.re_register_utility_pane(window, cx);
392 }
393
394 fn size(&self, window: &Window, cx: &App) -> Pixels {
395 let settings = AgentSettings::get_global(cx);
396 match self.position(window, cx) {
397 DockPosition::Left | DockPosition::Right => {
398 self.width.unwrap_or(settings.default_width)
399 }
400 DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
401 }
402 }
403
404 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
405 match self.position(window, cx) {
406 DockPosition::Left | DockPosition::Right => self.width = size,
407 DockPosition::Bottom => {}
408 }
409 self.serialize(cx);
410 cx.notify();
411 }
412
413 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
414 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgent)
415 }
416
417 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
418 Some("Agents Panel")
419 }
420
421 fn toggle_action(&self) -> Box<dyn Action> {
422 Box::new(ToggleAgentsPanel)
423 }
424
425 fn activation_priority(&self) -> u32 {
426 4
427 }
428
429 fn enabled(&self, cx: &App) -> bool {
430 AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
431 }
432}
433
434impl Render for AgentsPanel {
435 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
436 gpui::div().size_full().child(self.history.clone())
437 }
438}