1use acp_thread::AgentSessionInfo;
2use agent::{NativeAgentServer, ThreadStore};
3use agent_client_protocol as acp;
4use agent_servers::AgentServer;
5use agent_settings::AgentSettings;
6use agent_ui::acp::AcpThreadView;
7use fs::Fs;
8use gpui::{
9 Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
10};
11use project::Project;
12use prompt_store::PromptStore;
13use serde::{Deserialize, Serialize};
14use settings::DockSide;
15use settings::Settings as _;
16use std::rc::Rc;
17use std::sync::Arc;
18use ui::{Tab, Tooltip, prelude::*};
19use workspace::{
20 Workspace,
21 dock::{ClosePane, MinimizePane, UtilityPane, UtilityPanePosition},
22 utility_pane::UtilityPaneSlot,
23};
24
25pub const DEFAULT_UTILITY_PANE_WIDTH: Pixels = gpui::px(400.0);
26
27#[derive(Serialize, Deserialize, Debug, Clone)]
28pub enum SerializedHistoryEntryId {
29 AcpThread(String),
30}
31
32impl From<acp::SessionId> for SerializedHistoryEntryId {
33 fn from(id: acp::SessionId) -> Self {
34 SerializedHistoryEntryId::AcpThread(id.0.to_string())
35 }
36}
37
38#[derive(Serialize, Deserialize, Debug)]
39pub struct SerializedAgentThreadPane {
40 pub expanded: bool,
41 pub width: Option<Pixels>,
42 pub thread_id: Option<SerializedHistoryEntryId>,
43}
44
45pub enum AgentsUtilityPaneEvent {
46 StateChanged,
47}
48
49impl EventEmitter<AgentsUtilityPaneEvent> for AgentThreadPane {}
50impl EventEmitter<MinimizePane> for AgentThreadPane {}
51impl EventEmitter<ClosePane> for AgentThreadPane {}
52
53struct ActiveThreadView {
54 view: Entity<AcpThreadView>,
55 thread_id: acp::SessionId,
56 _notify: Subscription,
57}
58
59pub struct AgentThreadPane {
60 focus_handle: gpui::FocusHandle,
61 expanded: bool,
62 width: Option<Pixels>,
63 thread_view: Option<ActiveThreadView>,
64 workspace: WeakEntity<Workspace>,
65}
66
67impl AgentThreadPane {
68 pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
69 let focus_handle = cx.focus_handle();
70 Self {
71 focus_handle,
72 expanded: false,
73 width: None,
74 thread_view: None,
75 workspace,
76 }
77 }
78
79 pub fn thread_id(&self) -> Option<acp::SessionId> {
80 self.thread_view.as_ref().map(|tv| tv.thread_id.clone())
81 }
82
83 pub fn serialize(&self) -> SerializedAgentThreadPane {
84 SerializedAgentThreadPane {
85 expanded: self.expanded,
86 width: self.width,
87 thread_id: self.thread_id().map(SerializedHistoryEntryId::from),
88 }
89 }
90
91 pub fn open_thread(
92 &mut self,
93 entry: AgentSessionInfo,
94 fs: Arc<dyn Fs>,
95 workspace: WeakEntity<Workspace>,
96 project: Entity<Project>,
97 thread_store: Entity<ThreadStore>,
98 prompt_store: Option<Entity<PromptStore>>,
99 window: &mut Window,
100 cx: &mut Context<Self>,
101 ) {
102 let thread_id = entry.session_id.clone();
103 let resume_thread = Some(entry);
104
105 let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, thread_store.clone()));
106
107 let thread_view = cx.new(|cx| {
108 AcpThreadView::new(
109 agent,
110 resume_thread,
111 None,
112 workspace,
113 project,
114 Some(thread_store),
115 prompt_store,
116 true,
117 window,
118 cx,
119 )
120 });
121
122 let notify = cx.observe(&thread_view, |_, _, cx| {
123 cx.notify();
124 });
125
126 self.thread_view = Some(ActiveThreadView {
127 view: thread_view,
128 thread_id,
129 _notify: notify,
130 });
131
132 cx.notify();
133 }
134
135 fn title(&self, cx: &App) -> SharedString {
136 if let Some(active_thread_view) = &self.thread_view {
137 let thread_view = active_thread_view.view.read(cx);
138 if let Some(thread) = thread_view.thread() {
139 let title = thread.read(cx).title();
140 if !title.is_empty() {
141 return title;
142 }
143 }
144 thread_view.title(cx)
145 } else {
146 "Thread".into()
147 }
148 }
149
150 fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
151 let position = self.position(window, cx);
152 let slot = match position {
153 UtilityPanePosition::Left => UtilityPaneSlot::Left,
154 UtilityPanePosition::Right => UtilityPaneSlot::Right,
155 };
156
157 let workspace = self.workspace.clone();
158 let toggle_icon = self.toggle_icon(cx);
159 let title = self.title(cx);
160
161 let pane_toggle_button = |workspace: WeakEntity<Workspace>| {
162 IconButton::new("toggle_utility_pane", toggle_icon)
163 .icon_size(IconSize::Small)
164 .tooltip(Tooltip::text("Toggle Agent Pane"))
165 .on_click(move |_, window, cx| {
166 workspace
167 .update(cx, |workspace, cx| {
168 workspace.toggle_utility_pane(slot, window, cx)
169 })
170 .ok();
171 })
172 };
173
174 h_flex()
175 .id("utility-pane-header")
176 .w_full()
177 .h(Tab::container_height(cx))
178 .px_1p5()
179 .gap(DynamicSpacing::Base06.rems(cx))
180 .when(slot == UtilityPaneSlot::Right, |this| {
181 this.flex_row_reverse()
182 })
183 .flex_none()
184 .border_b_1()
185 .border_color(cx.theme().colors().border)
186 .child(pane_toggle_button(workspace))
187 .child(
188 h_flex()
189 .size_full()
190 .min_w_0()
191 .gap_1()
192 .map(|this| {
193 if slot == UtilityPaneSlot::Right {
194 this.flex_row_reverse().justify_start()
195 } else {
196 this.justify_between()
197 }
198 })
199 .child(Label::new(title).truncate())
200 .child(
201 IconButton::new("close_btn", IconName::Close)
202 .icon_size(IconSize::Small)
203 .tooltip(Tooltip::text("Close Agent Pane"))
204 .on_click(cx.listener(|this, _: &gpui::ClickEvent, _window, cx| {
205 cx.emit(ClosePane);
206 this.thread_view = None;
207 cx.notify()
208 })),
209 ),
210 )
211 }
212}
213
214impl Focusable for AgentThreadPane {
215 fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
216 if let Some(thread_view) = &self.thread_view {
217 thread_view.view.focus_handle(cx)
218 } else {
219 self.focus_handle.clone()
220 }
221 }
222}
223
224impl UtilityPane for AgentThreadPane {
225 fn position(&self, _window: &Window, cx: &App) -> UtilityPanePosition {
226 match AgentSettings::get_global(cx).agents_panel_dock {
227 DockSide::Left => UtilityPanePosition::Left,
228 DockSide::Right => UtilityPanePosition::Right,
229 }
230 }
231
232 fn toggle_icon(&self, _cx: &App) -> IconName {
233 IconName::Thread
234 }
235
236 fn expanded(&self, _cx: &App) -> bool {
237 self.expanded
238 }
239
240 fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
241 self.expanded = expanded;
242 cx.emit(AgentsUtilityPaneEvent::StateChanged);
243 cx.notify();
244 }
245
246 fn width(&self, _cx: &App) -> Pixels {
247 self.width.unwrap_or(DEFAULT_UTILITY_PANE_WIDTH)
248 }
249
250 fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
251 self.width = width;
252 cx.emit(AgentsUtilityPaneEvent::StateChanged);
253 cx.notify();
254 }
255}
256
257impl Render for AgentThreadPane {
258 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
259 let content = if let Some(thread_view) = &self.thread_view {
260 div().size_full().child(thread_view.view.clone())
261 } else {
262 div()
263 .size_full()
264 .flex()
265 .items_center()
266 .justify_center()
267 .child(Label::new("Select a thread to view details").size(LabelSize::Default))
268 };
269
270 div()
271 .size_full()
272 .flex()
273 .flex_col()
274 .child(self.render_header(window, cx))
275 .child(content)
276 }
277}