agents_panel.rs

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