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}