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 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        let history = self.history.clone();
314
315        let agent_thread_pane = cx.new(|cx| {
316            let mut pane = AgentThreadPane::new(workspace.clone(), history, cx);
317            pane.open_thread(
318                entry,
319                fs,
320                workspace.clone(),
321                project,
322                thread_store,
323                prompt_store,
324                window,
325                cx,
326            );
327            if let Some(width) = width {
328                pane.set_width(Some(width), cx);
329            }
330            pane.set_expanded(expanded, cx);
331            pane
332        });
333
334        let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
335        let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
336
337        self._subscriptions.push(state_subscription);
338        self._subscriptions.push(close_subscription);
339
340        let slot = self.utility_slot(window, cx);
341        let panel_id = cx.entity_id();
342
343        if let Some(workspace) = self.workspace.upgrade() {
344            workspace.update(cx, |workspace, cx| {
345                workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
346            });
347        }
348
349        self.agent_thread_pane = Some(agent_thread_pane);
350        self.serialize(cx);
351        cx.notify();
352    }
353
354    fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
355        let position = self.position(window, cx);
356        utility_slot_for_dock_position(position)
357    }
358
359    fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
360        if let Some(pane) = &self.agent_thread_pane {
361            let slot = self.utility_slot(window, cx);
362            let panel_id = cx.entity_id();
363            let pane = pane.clone();
364
365            if let Some(workspace) = self.workspace.upgrade() {
366                workspace.update(cx, |workspace, cx| {
367                    workspace.register_utility_pane(slot, panel_id, pane, cx);
368                });
369            }
370        }
371    }
372
373    fn serialize(&mut self, cx: &mut Context<Self>) {
374        let width = self.width;
375        let pane = self
376            .agent_thread_pane
377            .as_ref()
378            .map(|pane| pane.read(cx).serialize());
379
380        self.pending_serialization = cx.background_spawn(async move {
381            KEY_VALUE_STORE
382                .write_kvp(
383                    AGENTS_PANEL_KEY.into(),
384                    serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
385                )
386                .await
387                .log_err()
388        });
389    }
390}
391
392impl EventEmitter<PanelEvent> for AgentsPanel {}
393
394impl Focusable for AgentsPanel {
395    fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
396        self.focus_handle.clone()
397    }
398}
399
400impl Panel for AgentsPanel {
401    fn persistent_name() -> &'static str {
402        "AgentsPanel"
403    }
404
405    fn panel_key() -> &'static str {
406        AGENTS_PANEL_KEY
407    }
408
409    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
410        match AgentSettings::get_global(cx).agents_panel_dock {
411            settings::DockSide::Left => DockPosition::Left,
412            settings::DockSide::Right => DockPosition::Right,
413        }
414    }
415
416    fn position_is_valid(&self, position: DockPosition) -> bool {
417        position != DockPosition::Bottom
418    }
419
420    fn set_position(
421        &mut self,
422        position: DockPosition,
423        window: &mut Window,
424        cx: &mut Context<Self>,
425    ) {
426        update_settings_file(self.fs.clone(), cx, move |settings, _| {
427            settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
428                DockPosition::Left => settings::DockSide::Left,
429                DockPosition::Right | DockPosition::Bottom => settings::DockSide::Right,
430            });
431        });
432        self.re_register_utility_pane(window, cx);
433    }
434
435    fn size(&self, window: &Window, cx: &App) -> Pixels {
436        let settings = AgentSettings::get_global(cx);
437        match self.position(window, cx) {
438            DockPosition::Left | DockPosition::Right => {
439                self.width.unwrap_or(settings.default_width)
440            }
441            DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
442        }
443    }
444
445    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
446        match self.position(window, cx) {
447            DockPosition::Left | DockPosition::Right => self.width = size,
448            DockPosition::Bottom => {}
449        }
450        self.serialize(cx);
451        cx.notify();
452    }
453
454    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
455        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
456    }
457
458    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
459        Some("Agents Panel")
460    }
461
462    fn toggle_action(&self) -> Box<dyn Action> {
463        Box::new(ToggleAgentsPanel)
464    }
465
466    fn activation_priority(&self) -> u32 {
467        4
468    }
469
470    fn enabled(&self, cx: &App) -> bool {
471        AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
472    }
473}
474
475impl Render for AgentsPanel {
476    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
477        gpui::div().size_full().child(self.history.clone())
478    }
479}