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 connection.supports_load_session(cx)
155 && let Some(session_list) = connection.session_list(cx)
156 {
157 history_handle.update(cx, |history, cx| {
158 history.set_session_list(Some(session_list), cx);
159 });
160 }
161 });
162 })
163 .detach();
164
165 let this = cx.weak_entity();
166 let subscriptions = vec![
167 cx.subscribe_in(&history, window, Self::handle_history_event),
168 cx.observe_in(&history, window, Self::handle_history_updated),
169 cx.on_flags_ready(move |_, cx| {
170 this.update(cx, |_, cx| {
171 cx.notify();
172 })
173 .ok();
174 }),
175 ];
176
177 Self {
178 focus_handle,
179 workspace,
180 project,
181 agent_thread_pane: None,
182 history,
183 thread_store,
184 prompt_store,
185 fs,
186 width: None,
187 pending_restore: None,
188 pending_serialization: Task::ready(None),
189 _subscriptions: subscriptions,
190 }
191 }
192
193 fn restore_utility_pane(
194 &mut self,
195 serialized_pane: SerializedAgentThreadPane,
196 window: &mut Window,
197 cx: &mut Context<Self>,
198 ) {
199 let Some(thread_id) = &serialized_pane.thread_id else {
200 return;
201 };
202
203 let SerializedHistoryEntryId::AcpThread(id) = thread_id;
204 let session_id = acp::SessionId::new(id.clone());
205 if let Some(entry) = self.history.read(cx).session_for_id(&session_id) {
206 self.open_thread(
207 entry,
208 serialized_pane.expanded,
209 serialized_pane.width,
210 window,
211 cx,
212 );
213 } else {
214 self.pending_restore = Some(serialized_pane);
215 }
216 }
217
218 fn handle_utility_pane_event(
219 &mut self,
220 _utility_pane: Entity<AgentThreadPane>,
221 event: &AgentsUtilityPaneEvent,
222 cx: &mut Context<Self>,
223 ) {
224 match event {
225 AgentsUtilityPaneEvent::StateChanged => {
226 self.serialize(cx);
227 cx.notify();
228 }
229 }
230 }
231
232 fn handle_close_pane_event(
233 &mut self,
234 _utility_pane: Entity<AgentThreadPane>,
235 _event: &ClosePane,
236 cx: &mut Context<Self>,
237 ) {
238 self.agent_thread_pane = None;
239 self.serialize(cx);
240 cx.notify();
241 }
242
243 fn handle_history_updated(
244 &mut self,
245 _history: Entity<AcpThreadHistory>,
246 window: &mut Window,
247 cx: &mut Context<Self>,
248 ) {
249 self.maybe_restore_pending(window, cx);
250 }
251
252 fn handle_history_event(
253 &mut self,
254 _history: &Entity<AcpThreadHistory>,
255 event: &ThreadHistoryEvent,
256 window: &mut Window,
257 cx: &mut Context<Self>,
258 ) {
259 match event {
260 ThreadHistoryEvent::Open(entry) => {
261 self.open_thread(entry.clone(), true, None, window, cx);
262 }
263 }
264 }
265
266 fn maybe_restore_pending(&mut self, window: &mut Window, cx: &mut Context<Self>) {
267 if self.agent_thread_pane.is_some() {
268 self.pending_restore = None;
269 return;
270 }
271
272 let Some(pending) = self.pending_restore.as_ref() else {
273 return;
274 };
275 let Some(thread_id) = &pending.thread_id else {
276 self.pending_restore = None;
277 return;
278 };
279
280 let SerializedHistoryEntryId::AcpThread(id) = thread_id;
281 let session_id = acp::SessionId::new(id.clone());
282 let Some(entry) = self.history.read(cx).session_for_id(&session_id) else {
283 return;
284 };
285
286 let pending = self.pending_restore.take().expect("pending restore");
287 self.open_thread(entry, pending.expanded, pending.width, window, cx);
288 }
289
290 fn open_thread(
291 &mut self,
292 entry: AgentSessionInfo,
293 expanded: bool,
294 width: Option<Pixels>,
295 window: &mut Window,
296 cx: &mut Context<Self>,
297 ) {
298 let entry_id = entry.session_id.clone();
299 self.pending_restore = None;
300
301 if let Some(existing_pane) = &self.agent_thread_pane {
302 if existing_pane.read(cx).thread_id() == Some(entry_id) {
303 existing_pane.update(cx, |pane, cx| {
304 pane.set_expanded(true, cx);
305 });
306 return;
307 }
308 }
309
310 let fs = self.fs.clone();
311 let workspace = self.workspace.clone();
312 let project = self.project.clone();
313 let thread_store = self.thread_store.clone();
314 let prompt_store = self.prompt_store.clone();
315 let history = self.history.clone();
316
317 let agent_thread_pane = cx.new(|cx| {
318 let mut pane = AgentThreadPane::new(workspace.clone(), history, cx);
319 pane.open_thread(
320 entry,
321 fs,
322 workspace.clone(),
323 project,
324 thread_store,
325 prompt_store,
326 window,
327 cx,
328 );
329 if let Some(width) = width {
330 pane.set_width(Some(width), cx);
331 }
332 pane.set_expanded(expanded, cx);
333 pane
334 });
335
336 let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
337 let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
338
339 self._subscriptions.push(state_subscription);
340 self._subscriptions.push(close_subscription);
341
342 let slot = self.utility_slot(window, cx);
343 let panel_id = cx.entity_id();
344
345 if let Some(workspace) = self.workspace.upgrade() {
346 workspace.update(cx, |workspace, cx| {
347 workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
348 });
349 }
350
351 self.agent_thread_pane = Some(agent_thread_pane);
352 self.serialize(cx);
353 cx.notify();
354 }
355
356 fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
357 let position = self.position(window, cx);
358 utility_slot_for_dock_position(position)
359 }
360
361 fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
362 if let Some(pane) = &self.agent_thread_pane {
363 let slot = self.utility_slot(window, cx);
364 let panel_id = cx.entity_id();
365 let pane = pane.clone();
366
367 if let Some(workspace) = self.workspace.upgrade() {
368 workspace.update(cx, |workspace, cx| {
369 workspace.register_utility_pane(slot, panel_id, pane, cx);
370 });
371 }
372 }
373 }
374
375 fn serialize(&mut self, cx: &mut Context<Self>) {
376 let width = self.width;
377 let pane = self
378 .agent_thread_pane
379 .as_ref()
380 .map(|pane| pane.read(cx).serialize());
381
382 self.pending_serialization = cx.background_spawn(async move {
383 KEY_VALUE_STORE
384 .write_kvp(
385 AGENTS_PANEL_KEY.into(),
386 serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
387 )
388 .await
389 .log_err()
390 });
391 }
392}
393
394impl EventEmitter<PanelEvent> for AgentsPanel {}
395
396impl Focusable for AgentsPanel {
397 fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
398 self.focus_handle.clone()
399 }
400}
401
402impl Panel for AgentsPanel {
403 fn persistent_name() -> &'static str {
404 "AgentsPanel"
405 }
406
407 fn panel_key() -> &'static str {
408 AGENTS_PANEL_KEY
409 }
410
411 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
412 match AgentSettings::get_global(cx).agents_panel_dock {
413 settings::DockSide::Left => DockPosition::Left,
414 settings::DockSide::Right => DockPosition::Right,
415 }
416 }
417
418 fn position_is_valid(&self, position: DockPosition) -> bool {
419 position != DockPosition::Bottom
420 }
421
422 fn set_position(
423 &mut self,
424 position: DockPosition,
425 window: &mut Window,
426 cx: &mut Context<Self>,
427 ) {
428 update_settings_file(self.fs.clone(), cx, move |settings, _| {
429 settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
430 DockPosition::Left => settings::DockSide::Left,
431 DockPosition::Right | DockPosition::Bottom => settings::DockSide::Right,
432 });
433 });
434 self.re_register_utility_pane(window, cx);
435 }
436
437 fn size(&self, window: &Window, cx: &App) -> Pixels {
438 let settings = AgentSettings::get_global(cx);
439 match self.position(window, cx) {
440 DockPosition::Left | DockPosition::Right => {
441 self.width.unwrap_or(settings.default_width)
442 }
443 DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
444 }
445 }
446
447 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
448 match self.position(window, cx) {
449 DockPosition::Left | DockPosition::Right => self.width = size,
450 DockPosition::Bottom => {}
451 }
452 self.serialize(cx);
453 cx.notify();
454 }
455
456 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
457 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
458 }
459
460 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
461 Some("Agents Panel")
462 }
463
464 fn toggle_action(&self) -> Box<dyn Action> {
465 Box::new(ToggleAgentsPanel)
466 }
467
468 fn activation_priority(&self) -> u32 {
469 4
470 }
471
472 fn enabled(&self, cx: &App) -> bool {
473 AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
474 }
475}
476
477impl Render for AgentsPanel {
478 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
479 gpui::div().size_full().child(self.history.clone())
480 }
481}