thread_view.rs

  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}