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