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 crate::thread_history::{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 let Some(session_list) = connection.session_list(cx) {
155                    history_handle.update(cx, |history, cx| {
156                        history.set_session_list(Some(session_list), cx);
157                    });
158                }
159            });
160        })
161        .detach();
162
163        let this = cx.weak_entity();
164        let subscriptions = vec![
165            cx.subscribe_in(&history, window, Self::handle_history_event),
166            cx.observe_in(&history, window, Self::handle_history_updated),
167            cx.on_flags_ready(move |_, cx| {
168                this.update(cx, |_, cx| {
169                    cx.notify();
170                })
171                .ok();
172            }),
173        ];
174
175        Self {
176            focus_handle,
177            workspace,
178            project,
179            agent_thread_pane: None,
180            history,
181            thread_store,
182            prompt_store,
183            fs,
184            width: None,
185            pending_restore: None,
186            pending_serialization: Task::ready(None),
187            _subscriptions: subscriptions,
188        }
189    }
190
191    fn restore_utility_pane(
192        &mut self,
193        serialized_pane: SerializedAgentThreadPane,
194        window: &mut Window,
195        cx: &mut Context<Self>,
196    ) {
197        let Some(thread_id) = &serialized_pane.thread_id else {
198            return;
199        };
200
201        let SerializedHistoryEntryId::AcpThread(id) = thread_id;
202        let session_id = acp::SessionId::new(id.clone());
203        if let Some(entry) = self.history.read(cx).session_for_id(&session_id) {
204            self.open_thread(
205                entry,
206                serialized_pane.expanded,
207                serialized_pane.width,
208                window,
209                cx,
210            );
211        } else {
212            self.pending_restore = Some(serialized_pane);
213        }
214    }
215
216    fn handle_utility_pane_event(
217        &mut self,
218        _utility_pane: Entity<AgentThreadPane>,
219        event: &AgentsUtilityPaneEvent,
220        cx: &mut Context<Self>,
221    ) {
222        match event {
223            AgentsUtilityPaneEvent::StateChanged => {
224                self.serialize(cx);
225                cx.notify();
226            }
227        }
228    }
229
230    fn handle_close_pane_event(
231        &mut self,
232        _utility_pane: Entity<AgentThreadPane>,
233        _event: &ClosePane,
234        cx: &mut Context<Self>,
235    ) {
236        self.agent_thread_pane = None;
237        self.serialize(cx);
238        cx.notify();
239    }
240
241    fn handle_history_updated(
242        &mut self,
243        _history: Entity<AcpThreadHistory>,
244        window: &mut Window,
245        cx: &mut Context<Self>,
246    ) {
247        self.maybe_restore_pending(window, cx);
248    }
249
250    fn handle_history_event(
251        &mut self,
252        _history: &Entity<AcpThreadHistory>,
253        event: &ThreadHistoryEvent,
254        window: &mut Window,
255        cx: &mut Context<Self>,
256    ) {
257        match event {
258            ThreadHistoryEvent::Open(entry) => {
259                self.open_thread(entry.clone(), true, None, window, cx);
260            }
261        }
262    }
263
264    fn maybe_restore_pending(&mut self, window: &mut Window, cx: &mut Context<Self>) {
265        if self.agent_thread_pane.is_some() {
266            self.pending_restore = None;
267            return;
268        }
269
270        let Some(pending) = self.pending_restore.as_ref() else {
271            return;
272        };
273        let Some(thread_id) = &pending.thread_id else {
274            self.pending_restore = None;
275            return;
276        };
277
278        let SerializedHistoryEntryId::AcpThread(id) = thread_id;
279        let session_id = acp::SessionId::new(id.clone());
280        let Some(entry) = self.history.read(cx).session_for_id(&session_id) else {
281            return;
282        };
283
284        let pending = self.pending_restore.take().expect("pending restore");
285        self.open_thread(entry, pending.expanded, pending.width, window, cx);
286    }
287
288    fn open_thread(
289        &mut self,
290        entry: AgentSessionInfo,
291        expanded: bool,
292        width: Option<Pixels>,
293        window: &mut Window,
294        cx: &mut Context<Self>,
295    ) {
296        let entry_id = entry.session_id.clone();
297        self.pending_restore = None;
298
299        if let Some(existing_pane) = &self.agent_thread_pane {
300            if existing_pane.read(cx).thread_id() == Some(entry_id) {
301                existing_pane.update(cx, |pane, cx| {
302                    pane.set_expanded(true, cx);
303                });
304                return;
305            }
306        }
307
308        let fs = self.fs.clone();
309        let workspace = self.workspace.clone();
310        let project = self.project.clone();
311        let thread_store = self.thread_store.clone();
312        let prompt_store = self.prompt_store.clone();
313
314        let agent_thread_pane = cx.new(|cx| {
315            let mut pane = AgentThreadPane::new(workspace.clone(), cx);
316            pane.open_thread(
317                entry,
318                fs,
319                workspace.clone(),
320                project,
321                thread_store,
322                prompt_store,
323                window,
324                cx,
325            );
326            if let Some(width) = width {
327                pane.set_width(Some(width), cx);
328            }
329            pane.set_expanded(expanded, cx);
330            pane
331        });
332
333        let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
334        let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
335
336        self._subscriptions.push(state_subscription);
337        self._subscriptions.push(close_subscription);
338
339        let slot = self.utility_slot(window, cx);
340        let panel_id = cx.entity_id();
341
342        if let Some(workspace) = self.workspace.upgrade() {
343            workspace.update(cx, |workspace, cx| {
344                workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
345            });
346        }
347
348        self.agent_thread_pane = Some(agent_thread_pane);
349        self.serialize(cx);
350        cx.notify();
351    }
352
353    fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
354        let position = self.position(window, cx);
355        utility_slot_for_dock_position(position)
356    }
357
358    fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
359        if let Some(pane) = &self.agent_thread_pane {
360            let slot = self.utility_slot(window, cx);
361            let panel_id = cx.entity_id();
362            let pane = pane.clone();
363
364            if let Some(workspace) = self.workspace.upgrade() {
365                workspace.update(cx, |workspace, cx| {
366                    workspace.register_utility_pane(slot, panel_id, pane, cx);
367                });
368            }
369        }
370    }
371
372    fn serialize(&mut self, cx: &mut Context<Self>) {
373        let width = self.width;
374        let pane = self
375            .agent_thread_pane
376            .as_ref()
377            .map(|pane| pane.read(cx).serialize());
378
379        self.pending_serialization = cx.background_spawn(async move {
380            KEY_VALUE_STORE
381                .write_kvp(
382                    AGENTS_PANEL_KEY.into(),
383                    serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
384                )
385                .await
386                .log_err()
387        });
388    }
389}
390
391impl EventEmitter<PanelEvent> for AgentsPanel {}
392
393impl Focusable for AgentsPanel {
394    fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
395        self.focus_handle.clone()
396    }
397}
398
399impl Panel for AgentsPanel {
400    fn persistent_name() -> &'static str {
401        "AgentsPanel"
402    }
403
404    fn panel_key() -> &'static str {
405        AGENTS_PANEL_KEY
406    }
407
408    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
409        match AgentSettings::get_global(cx).agents_panel_dock {
410            settings::DockSide::Left => DockPosition::Left,
411            settings::DockSide::Right => DockPosition::Right,
412        }
413    }
414
415    fn position_is_valid(&self, position: DockPosition) -> bool {
416        position != DockPosition::Bottom
417    }
418
419    fn set_position(
420        &mut self,
421        position: DockPosition,
422        window: &mut Window,
423        cx: &mut Context<Self>,
424    ) {
425        update_settings_file(self.fs.clone(), cx, move |settings, _| {
426            settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
427                DockPosition::Left => settings::DockSide::Left,
428                DockPosition::Right | DockPosition::Bottom => settings::DockSide::Right,
429            });
430        });
431        self.re_register_utility_pane(window, cx);
432    }
433
434    fn size(&self, window: &Window, cx: &App) -> Pixels {
435        let settings = AgentSettings::get_global(cx);
436        match self.position(window, cx) {
437            DockPosition::Left | DockPosition::Right => {
438                self.width.unwrap_or(settings.default_width)
439            }
440            DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
441        }
442    }
443
444    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
445        match self.position(window, cx) {
446            DockPosition::Left | DockPosition::Right => self.width = size,
447            DockPosition::Bottom => {}
448        }
449        self.serialize(cx);
450        cx.notify();
451    }
452
453    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
454        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
455    }
456
457    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
458        Some("Agents Panel")
459    }
460
461    fn toggle_action(&self) -> Box<dyn Action> {
462        Box::new(ToggleAgentsPanel)
463    }
464
465    fn activation_priority(&self) -> u32 {
466        4
467    }
468
469    fn enabled(&self, cx: &App) -> bool {
470        AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
471    }
472}
473
474impl Render for AgentsPanel {
475    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
476        gpui::div().size_full().child(self.history.clone())
477    }
478}