agents_panel.rs

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