1use anyhow::Result;
2use editor::{Editor, MultiBuffer};
3use gpui::{App, Entity, Focusable, SharedString, Subscription, Window, div, prelude::*};
4use gpui::{FocusHandle, Task};
5use language::Buffer;
6use ui::Tooltip;
7use ui::prelude::*;
8use zed_actions::agent::Chat;
9
10use crate::{AgentThreadEntryContent, Message, MessageChunk, Role, Thread, ThreadEntry};
11
12pub struct ThreadElement {
13 thread: Entity<Thread>,
14 // todo! use full message editor from agent2
15 message_editor: Entity<Editor>,
16 send_task: Option<Task<Result<()>>>,
17 _subscription: Subscription,
18}
19
20impl ThreadElement {
21 pub fn new(thread: Entity<Thread>, window: &mut Window, cx: &mut Context<Self>) -> Self {
22 let message_editor = cx.new(|cx| {
23 let buffer = cx.new(|cx| Buffer::local("", cx));
24 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25
26 let mut editor = Editor::new(
27 editor::EditorMode::AutoHeight {
28 min_lines: 5,
29 max_lines: None,
30 },
31 buffer,
32 None,
33 window,
34 cx,
35 );
36 editor.set_placeholder_text("Send a message", cx);
37 editor.set_soft_wrap();
38 editor
39 });
40
41 let subscription = cx.observe(&thread, |_, _, cx| {
42 cx.notify();
43 });
44
45 Self {
46 thread,
47 message_editor,
48 send_task: None,
49 _subscription: subscription,
50 }
51 }
52
53 pub fn title(&self, cx: &App) -> SharedString {
54 self.thread.read(cx).title()
55 }
56
57 pub fn cancel(&mut self) {
58 self.send_task.take();
59 }
60
61 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
62 let text = self.message_editor.read(cx).text(cx);
63 if text.is_empty() {
64 return;
65 }
66
67 let task = self.thread.update(cx, |thread, cx| {
68 let message = Message {
69 role: Role::User,
70 chunks: vec![MessageChunk::Text { chunk: text.into() }],
71 };
72 thread.send(message, cx)
73 });
74
75 self.send_task = Some(cx.spawn(async move |this, cx| {
76 task.await?;
77
78 this.update(cx, |this, _cx| {
79 this.send_task.take();
80 })
81 }));
82
83 self.message_editor.update(cx, |editor, cx| {
84 editor.clear(window, cx);
85 });
86 }
87
88 fn render_entry(
89 &self,
90 entry: &ThreadEntry,
91 _window: &mut Window,
92 _cx: &Context<Self>,
93 ) -> AnyElement {
94 match &entry.content {
95 AgentThreadEntryContent::Message(message) => div()
96 .children(message.chunks.iter().map(|chunk| match chunk {
97 MessageChunk::Text { chunk } => div().child(chunk.clone()),
98 _ => todo!(),
99 }))
100 .into_any(),
101 AgentThreadEntryContent::ReadFile { path, content: _ } => {
102 // todo!
103 div()
104 .child(format!("<Reading file {}>", path.display()))
105 .into_any()
106 }
107 }
108 }
109}
110
111impl Focusable for ThreadElement {
112 fn focus_handle(&self, cx: &App) -> FocusHandle {
113 self.message_editor.focus_handle(cx)
114 }
115}
116
117impl Render for ThreadElement {
118 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
119 let text = self.message_editor.read(cx).text(cx);
120 let is_editor_empty = text.is_empty();
121 let focus_handle = self.message_editor.focus_handle(cx);
122
123 v_flex()
124 .key_context("MessageEditor")
125 .on_action(cx.listener(Self::chat))
126 .child(
127 v_flex().h_full().gap_1().children(
128 self.thread
129 .read(cx)
130 .entries()
131 .iter()
132 .map(|entry| self.render_entry(entry, window, cx)),
133 ),
134 )
135 .when(self.send_task.is_some(), |this| {
136 this.child(
137 Label::new("Generating...")
138 .color(Color::Muted)
139 .size(LabelSize::Small),
140 )
141 })
142 .child(
143 div()
144 .bg(cx.theme().colors().editor_background)
145 .border_t_1()
146 .border_color(cx.theme().colors().border)
147 .p_2()
148 .child(self.message_editor.clone()),
149 )
150 .child(
151 h_flex().p_2().justify_end().child(
152 IconButton::new("send-message", IconName::Send)
153 .icon_color(Color::Accent)
154 .style(ButtonStyle::Filled)
155 .disabled(is_editor_empty)
156 .on_click({
157 let focus_handle = focus_handle.clone();
158 move |_event, window, cx| {
159 focus_handle.dispatch_action(&Chat, window, cx);
160 }
161 })
162 .when(!is_editor_empty, |button| {
163 button.tooltip(move |window, cx| {
164 Tooltip::for_action("Send", &Chat, window, cx)
165 })
166 })
167 .when(is_editor_empty, |button| {
168 button.tooltip(Tooltip::text("Type a message to submit"))
169 }),
170 ),
171 )
172 }
173}