thread_element.rs

  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}