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}