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 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 let history = self.history.clone();
314
315 let agent_thread_pane = cx.new(|cx| {
316 let mut pane = AgentThreadPane::new(workspace.clone(), history, cx);
317 pane.open_thread(
318 entry,
319 fs,
320 workspace.clone(),
321 project,
322 thread_store,
323 prompt_store,
324 window,
325 cx,
326 );
327 if let Some(width) = width {
328 pane.set_width(Some(width), cx);
329 }
330 pane.set_expanded(expanded, cx);
331 pane
332 });
333
334 let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
335 let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
336
337 self._subscriptions.push(state_subscription);
338 self._subscriptions.push(close_subscription);
339
340 let slot = self.utility_slot(window, cx);
341 let panel_id = cx.entity_id();
342
343 if let Some(workspace) = self.workspace.upgrade() {
344 workspace.update(cx, |workspace, cx| {
345 workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
346 });
347 }
348
349 self.agent_thread_pane = Some(agent_thread_pane);
350 self.serialize(cx);
351 cx.notify();
352 }
353
354 fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
355 let position = self.position(window, cx);
356 utility_slot_for_dock_position(position)
357 }
358
359 fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
360 if let Some(pane) = &self.agent_thread_pane {
361 let slot = self.utility_slot(window, cx);
362 let panel_id = cx.entity_id();
363 let pane = pane.clone();
364
365 if let Some(workspace) = self.workspace.upgrade() {
366 workspace.update(cx, |workspace, cx| {
367 workspace.register_utility_pane(slot, panel_id, pane, cx);
368 });
369 }
370 }
371 }
372
373 fn serialize(&mut self, cx: &mut Context<Self>) {
374 let width = self.width;
375 let pane = self
376 .agent_thread_pane
377 .as_ref()
378 .map(|pane| pane.read(cx).serialize());
379
380 self.pending_serialization = cx.background_spawn(async move {
381 KEY_VALUE_STORE
382 .write_kvp(
383 AGENTS_PANEL_KEY.into(),
384 serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
385 )
386 .await
387 .log_err()
388 });
389 }
390}
391
392impl EventEmitter<PanelEvent> for AgentsPanel {}
393
394impl Focusable for AgentsPanel {
395 fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
396 self.focus_handle.clone()
397 }
398}
399
400impl Panel for AgentsPanel {
401 fn persistent_name() -> &'static str {
402 "AgentsPanel"
403 }
404
405 fn panel_key() -> &'static str {
406 AGENTS_PANEL_KEY
407 }
408
409 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
410 match AgentSettings::get_global(cx).agents_panel_dock {
411 settings::DockSide::Left => DockPosition::Left,
412 settings::DockSide::Right => DockPosition::Right,
413 }
414 }
415
416 fn position_is_valid(&self, position: DockPosition) -> bool {
417 position != DockPosition::Bottom
418 }
419
420 fn set_position(
421 &mut self,
422 position: DockPosition,
423 window: &mut Window,
424 cx: &mut Context<Self>,
425 ) {
426 update_settings_file(self.fs.clone(), cx, move |settings, _| {
427 settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
428 DockPosition::Left => settings::DockSide::Left,
429 DockPosition::Right | DockPosition::Bottom => settings::DockSide::Right,
430 });
431 });
432 self.re_register_utility_pane(window, cx);
433 }
434
435 fn size(&self, window: &Window, cx: &App) -> Pixels {
436 let settings = AgentSettings::get_global(cx);
437 match self.position(window, cx) {
438 DockPosition::Left | DockPosition::Right => {
439 self.width.unwrap_or(settings.default_width)
440 }
441 DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
442 }
443 }
444
445 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
446 match self.position(window, cx) {
447 DockPosition::Left | DockPosition::Right => self.width = size,
448 DockPosition::Bottom => {}
449 }
450 self.serialize(cx);
451 cx.notify();
452 }
453
454 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
455 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgentTwo)
456 }
457
458 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
459 Some("Agents Panel")
460 }
461
462 fn toggle_action(&self) -> Box<dyn Action> {
463 Box::new(ToggleAgentsPanel)
464 }
465
466 fn activation_priority(&self) -> u32 {
467 4
468 }
469
470 fn enabled(&self, cx: &App) -> bool {
471 AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
472 }
473}
474
475impl Render for AgentsPanel {
476 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
477 gpui::div().size_full().child(self.history.clone())
478 }
479}