agents_panel.rs

  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}