1use anyhow::Result;
2use editor::{Editor, MultiBuffer};
3use gpui::{
4 App, Empty, Entity, Focusable, ListState, SharedString, Subscription, Window, div, list,
5 prelude::*,
6};
7use gpui::{FocusHandle, Task};
8use language::Buffer;
9use ui::Tooltip;
10use ui::prelude::*;
11use zed_actions::agent::Chat;
12
13use crate::{AcpThread, AgentThreadEntryContent, Message, MessageChunk, Role, ThreadEntry};
14
15pub struct AcpThreadView {
16 thread: Entity<AcpThread>,
17 // todo! use full message editor from agent2
18 message_editor: Entity<Editor>,
19 list_state: ListState,
20 send_task: Option<Task<Result<()>>>,
21 _subscription: Subscription,
22}
23
24impl AcpThreadView {
25 pub fn new(thread: Entity<AcpThread>, window: &mut Window, cx: &mut Context<Self>) -> Self {
26 let message_editor = cx.new(|cx| {
27 let buffer = cx.new(|cx| Buffer::local("", cx));
28 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
29
30 let mut editor = Editor::new(
31 editor::EditorMode::AutoHeight {
32 min_lines: 5,
33 max_lines: None,
34 },
35 buffer,
36 None,
37 window,
38 cx,
39 );
40 editor.set_placeholder_text("Send a message", cx);
41 editor.set_soft_wrap();
42 editor
43 });
44
45 let subscription = cx.observe(&thread, |this, thread, cx| {
46 let count = this.list_state.item_count();
47 // TODO: Incremental updates
48 this.list_state
49 .splice(0..count, thread.read(cx).entries.len());
50 cx.notify();
51 });
52
53 let list_state = ListState::new(
54 thread.read(cx).entries.len(),
55 gpui::ListAlignment::Top,
56 px(1000.0),
57 cx.processor({
58 move |this: &mut Self, item: usize, window, cx| {
59 let Some(entry) = this.thread.read(cx).entries.get(item) else {
60 return Empty.into_any();
61 };
62 this.render_entry(entry, window, cx)
63 }
64 }),
65 );
66 Self {
67 thread,
68 message_editor,
69 send_task: None,
70 list_state: list_state,
71 _subscription: subscription,
72 }
73 }
74
75 pub fn title(&self, cx: &App) -> SharedString {
76 self.thread.read(cx).title()
77 }
78
79 pub fn cancel(&mut self) {
80 self.send_task.take();
81 }
82
83 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
84 let text = self.message_editor.read(cx).text(cx);
85 if text.is_empty() {
86 return;
87 }
88
89 let task = self.thread.update(cx, |thread, cx| {
90 let message = Message {
91 role: Role::User,
92 chunks: vec![MessageChunk::Text { chunk: text.into() }],
93 };
94 thread.send(message, cx)
95 });
96
97 self.send_task = Some(cx.spawn(async move |this, cx| {
98 task.await?;
99
100 this.update(cx, |this, _cx| {
101 this.send_task.take();
102 })
103 }));
104
105 self.message_editor.update(cx, |editor, cx| {
106 editor.clear(window, cx);
107 });
108 }
109
110 fn render_entry(
111 &self,
112 entry: &ThreadEntry,
113 _window: &mut Window,
114 cx: &Context<Self>,
115 ) -> AnyElement {
116 match &entry.content {
117 AgentThreadEntryContent::Message(message) => {
118 let message_body = div()
119 .children(message.chunks.iter().map(|chunk| match chunk {
120 MessageChunk::Text { chunk } => {
121 // todo! markdown
122 Label::new(chunk.clone())
123 }
124 _ => todo!(),
125 }))
126 .into_any();
127
128 match message.role {
129 Role::User => div()
130 .my_1()
131 .p_2()
132 .bg(cx.theme().colors().editor_background)
133 .rounded_lg()
134 .shadow_md()
135 .border_1()
136 .border_color(cx.theme().colors().border)
137 .child(message_body)
138 .into_any(),
139 Role::Assistant => message_body,
140 }
141 }
142 AgentThreadEntryContent::ReadFile { path, content: _ } => {
143 // todo!
144 div()
145 .child(format!("<Reading file {}>", path.display()))
146 .into_any()
147 }
148 }
149 }
150}
151
152impl Focusable for AcpThreadView {
153 fn focus_handle(&self, cx: &App) -> FocusHandle {
154 self.message_editor.focus_handle(cx)
155 }
156}
157
158impl Render for AcpThreadView {
159 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
160 let text = self.message_editor.read(cx).text(cx);
161 let is_editor_empty = text.is_empty();
162 let focus_handle = self.message_editor.focus_handle(cx);
163
164 v_flex()
165 .key_context("MessageEditor")
166 .on_action(cx.listener(Self::chat))
167 .child(
168 div()
169 .child(
170 list(self.list_state.clone())
171 .with_sizing_behavior(gpui::ListSizingBehavior::Infer),
172 )
173 .p_2(),
174 )
175 .when(self.send_task.is_some(), |this| {
176 this.child(
177 div().p_2().child(
178 Label::new("Generating...")
179 .color(Color::Muted)
180 .size(LabelSize::Small),
181 ),
182 )
183 })
184 .child(
185 div()
186 .bg(cx.theme().colors().editor_background)
187 .border_t_1()
188 .border_color(cx.theme().colors().border)
189 .p_2()
190 .child(self.message_editor.clone()),
191 )
192 .child(
193 h_flex()
194 .p_2()
195 .justify_end()
196 .child(if self.send_task.is_some() {
197 IconButton::new("stop-generation", IconName::StopFilled)
198 .icon_color(Color::Error)
199 .style(ButtonStyle::Tinted(ui::TintColor::Error))
200 .tooltip(move |window, cx| {
201 Tooltip::for_action(
202 "Stop Generation",
203 &editor::actions::Cancel,
204 window,
205 cx,
206 )
207 })
208 .disabled(is_editor_empty)
209 .on_click(cx.listener(|this, _event, _, _| this.cancel()))
210 } else {
211 IconButton::new("send-message", IconName::Send)
212 .icon_color(Color::Accent)
213 .style(ButtonStyle::Filled)
214 .disabled(is_editor_empty)
215 .on_click({
216 let focus_handle = focus_handle.clone();
217 move |_event, window, cx| {
218 focus_handle.dispatch_action(&Chat, window, cx);
219 }
220 })
221 .when(!is_editor_empty, |button| {
222 button.tooltip(move |window, cx| {
223 Tooltip::for_action("Send", &Chat, window, cx)
224 })
225 })
226 .when(is_editor_empty, |button| {
227 button.tooltip(Tooltip::text("Type a message to submit"))
228 })
229 }),
230 )
231 }
232}