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