1use std::sync::Arc;
2
3use collections::HashMap;
4use editor::{Editor, MultiBuffer};
5use gpui::{
6 list, AbsoluteLength, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
7 Entity, Focusable, Length, ListAlignment, ListOffset, ListState, StyleRefinement, Subscription,
8 Task, TextStyleRefinement, UnderlineStyle,
9};
10use language::{Buffer, LanguageRegistry};
11use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
12use markdown::{Markdown, MarkdownStyle};
13use scripting_tool::{ScriptingTool, ScriptingToolInput};
14use settings::Settings as _;
15use theme::ThemeSettings;
16use ui::{prelude::*, Disclosure, KeyBinding};
17use util::ResultExt as _;
18
19use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
20use crate::thread_store::ThreadStore;
21use crate::tool_use::{ToolUse, ToolUseStatus};
22use crate::ui::ContextPill;
23
24pub struct ActiveThread {
25 language_registry: Arc<LanguageRegistry>,
26 thread_store: Entity<ThreadStore>,
27 thread: Entity<Thread>,
28 save_thread_task: Option<Task<()>>,
29 messages: Vec<MessageId>,
30 list_state: ListState,
31 rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
32 rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
33 editing_message: Option<(MessageId, EditMessageState)>,
34 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
35 last_error: Option<ThreadError>,
36 _subscriptions: Vec<Subscription>,
37}
38
39struct EditMessageState {
40 editor: Entity<Editor>,
41}
42
43impl ActiveThread {
44 pub fn new(
45 thread: Entity<Thread>,
46 thread_store: Entity<ThreadStore>,
47 language_registry: Arc<LanguageRegistry>,
48 window: &mut Window,
49 cx: &mut Context<Self>,
50 ) -> Self {
51 let subscriptions = vec![
52 cx.observe(&thread, |_, _, cx| cx.notify()),
53 cx.subscribe_in(&thread, window, Self::handle_thread_event),
54 ];
55
56 let mut this = Self {
57 language_registry,
58 thread_store,
59 thread: thread.clone(),
60 save_thread_task: None,
61 messages: Vec::new(),
62 rendered_messages_by_id: HashMap::default(),
63 rendered_scripting_tool_uses: HashMap::default(),
64 expanded_tool_uses: HashMap::default(),
65 list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
66 let this = cx.entity().downgrade();
67 move |ix, window: &mut Window, cx: &mut App| {
68 this.update(cx, |this, cx| this.render_message(ix, window, cx))
69 .unwrap()
70 }
71 }),
72 editing_message: None,
73 last_error: None,
74 _subscriptions: subscriptions,
75 };
76
77 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
78 this.push_message(&message.id, message.text.clone(), window, cx);
79
80 for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
81 this.render_scripting_tool_use_markdown(
82 tool_use.id.clone(),
83 tool_use.name.as_ref(),
84 tool_use.input.clone(),
85 window,
86 cx,
87 );
88 }
89 }
90
91 this
92 }
93
94 pub fn thread(&self) -> &Entity<Thread> {
95 &self.thread
96 }
97
98 pub fn is_empty(&self) -> bool {
99 self.messages.is_empty()
100 }
101
102 pub fn summary(&self, cx: &App) -> Option<SharedString> {
103 self.thread.read(cx).summary()
104 }
105
106 pub fn summary_or_default(&self, cx: &App) -> SharedString {
107 self.thread.read(cx).summary_or_default()
108 }
109
110 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
111 self.last_error.take();
112 self.thread
113 .update(cx, |thread, _cx| thread.cancel_last_completion())
114 }
115
116 pub fn last_error(&self) -> Option<ThreadError> {
117 self.last_error.clone()
118 }
119
120 pub fn clear_last_error(&mut self) {
121 self.last_error.take();
122 }
123
124 fn push_message(
125 &mut self,
126 id: &MessageId,
127 text: String,
128 window: &mut Window,
129 cx: &mut Context<Self>,
130 ) {
131 let old_len = self.messages.len();
132 self.messages.push(*id);
133 self.list_state.splice(old_len..old_len, 1);
134
135 let markdown = self.render_markdown(text.into(), window, cx);
136 self.rendered_messages_by_id.insert(*id, markdown);
137 self.list_state.scroll_to(ListOffset {
138 item_ix: old_len,
139 offset_in_item: Pixels(0.0),
140 });
141 }
142
143 fn edited_message(
144 &mut self,
145 id: &MessageId,
146 text: String,
147 window: &mut Window,
148 cx: &mut Context<Self>,
149 ) {
150 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
151 return;
152 };
153 self.list_state.splice(index..index + 1, 1);
154 let markdown = self.render_markdown(text.into(), window, cx);
155 self.rendered_messages_by_id.insert(*id, markdown);
156 }
157
158 fn deleted_message(&mut self, id: &MessageId) {
159 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
160 return;
161 };
162 self.messages.remove(index);
163 self.list_state.splice(index..index + 1, 0);
164 self.rendered_messages_by_id.remove(id);
165 }
166
167 fn render_markdown(
168 &self,
169 text: SharedString,
170 window: &Window,
171 cx: &mut Context<Self>,
172 ) -> Entity<Markdown> {
173 let theme_settings = ThemeSettings::get_global(cx);
174 let colors = cx.theme().colors();
175 let ui_font_size = TextSize::Default.rems(cx);
176 let buffer_font_size = TextSize::Small.rems(cx);
177 let mut text_style = window.text_style();
178
179 text_style.refine(&TextStyleRefinement {
180 font_family: Some(theme_settings.ui_font.family.clone()),
181 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
182 font_features: Some(theme_settings.ui_font.features.clone()),
183 font_size: Some(ui_font_size.into()),
184 color: Some(cx.theme().colors().text),
185 ..Default::default()
186 });
187
188 let markdown_style = MarkdownStyle {
189 base_text_style: text_style,
190 syntax: cx.theme().syntax().clone(),
191 selection_background_color: cx.theme().players().local().selection,
192 code_block_overflow_x_scroll: true,
193 table_overflow_x_scroll: true,
194 code_block: StyleRefinement {
195 margin: EdgesRefinement {
196 top: Some(Length::Definite(rems(0.).into())),
197 left: Some(Length::Definite(rems(0.).into())),
198 right: Some(Length::Definite(rems(0.).into())),
199 bottom: Some(Length::Definite(rems(0.5).into())),
200 },
201 padding: EdgesRefinement {
202 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
203 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
204 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
205 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
206 },
207 background: Some(colors.editor_background.into()),
208 border_color: Some(colors.border_variant),
209 border_widths: EdgesRefinement {
210 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
211 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
212 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
213 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
214 },
215 text: Some(TextStyleRefinement {
216 font_family: Some(theme_settings.buffer_font.family.clone()),
217 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
218 font_features: Some(theme_settings.buffer_font.features.clone()),
219 font_size: Some(buffer_font_size.into()),
220 ..Default::default()
221 }),
222 ..Default::default()
223 },
224 inline_code: TextStyleRefinement {
225 font_family: Some(theme_settings.buffer_font.family.clone()),
226 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
227 font_features: Some(theme_settings.buffer_font.features.clone()),
228 font_size: Some(buffer_font_size.into()),
229 background_color: Some(colors.editor_foreground.opacity(0.1)),
230 ..Default::default()
231 },
232 link: TextStyleRefinement {
233 background_color: Some(colors.editor_foreground.opacity(0.025)),
234 underline: Some(UnderlineStyle {
235 color: Some(colors.text_accent.opacity(0.5)),
236 thickness: px(1.),
237 ..Default::default()
238 }),
239 ..Default::default()
240 },
241 ..Default::default()
242 };
243
244 cx.new(|cx| {
245 Markdown::new(
246 text,
247 markdown_style,
248 Some(self.language_registry.clone()),
249 None,
250 cx,
251 )
252 })
253 }
254
255 /// Renders the input of a scripting tool use to Markdown.
256 ///
257 /// Does nothing if the tool use does not correspond to the scripting tool.
258 fn render_scripting_tool_use_markdown(
259 &mut self,
260 tool_use_id: LanguageModelToolUseId,
261 tool_name: &str,
262 tool_input: serde_json::Value,
263 window: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 if tool_name != ScriptingTool::NAME {
267 return;
268 }
269
270 let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
271 .map(|input| input.lua_script)
272 .unwrap_or_default();
273
274 let lua_script =
275 self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
276
277 self.rendered_scripting_tool_uses
278 .insert(tool_use_id, lua_script);
279 }
280
281 fn handle_thread_event(
282 &mut self,
283 _thread: &Entity<Thread>,
284 event: &ThreadEvent,
285 window: &mut Window,
286 cx: &mut Context<Self>,
287 ) {
288 match event {
289 ThreadEvent::ShowError(error) => {
290 self.last_error = Some(error.clone());
291 }
292 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
293 self.save_thread(cx);
294 }
295 ThreadEvent::StreamedAssistantText(message_id, text) => {
296 if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
297 markdown.update(cx, |markdown, cx| {
298 markdown.append(text, cx);
299 });
300 }
301 }
302 ThreadEvent::MessageAdded(message_id) => {
303 if let Some(message_text) = self
304 .thread
305 .read(cx)
306 .message(*message_id)
307 .map(|message| message.text.clone())
308 {
309 self.push_message(message_id, message_text, window, cx);
310 }
311
312 self.save_thread(cx);
313 cx.notify();
314 }
315 ThreadEvent::MessageEdited(message_id) => {
316 if let Some(message_text) = self
317 .thread
318 .read(cx)
319 .message(*message_id)
320 .map(|message| message.text.clone())
321 {
322 self.edited_message(message_id, message_text, window, cx);
323 }
324
325 self.save_thread(cx);
326 cx.notify();
327 }
328 ThreadEvent::MessageDeleted(message_id) => {
329 self.deleted_message(message_id);
330 self.save_thread(cx);
331 cx.notify();
332 }
333 ThreadEvent::UsePendingTools => {
334 self.thread.update(cx, |thread, cx| {
335 thread.use_pending_tools(cx);
336 });
337 }
338 ThreadEvent::ToolFinished {
339 pending_tool_use, ..
340 } => {
341 if let Some(tool_use) = pending_tool_use {
342 self.render_scripting_tool_use_markdown(
343 tool_use.id.clone(),
344 tool_use.name.as_ref(),
345 tool_use.input.clone(),
346 window,
347 cx,
348 );
349 }
350
351 if self.thread.read(cx).all_tools_finished() {
352 let model_registry = LanguageModelRegistry::read_global(cx);
353 if let Some(model) = model_registry.active_model() {
354 self.thread.update(cx, |thread, cx| {
355 thread.send_tool_results_to_model(model, cx);
356 });
357 }
358 }
359 }
360 }
361 }
362
363 /// Spawns a task to save the active thread.
364 ///
365 /// Only one task to save the thread will be in flight at a time.
366 fn save_thread(&mut self, cx: &mut Context<Self>) {
367 let thread = self.thread.clone();
368 self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
369 let task = this
370 .update(&mut cx, |this, cx| {
371 this.thread_store
372 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
373 })
374 .ok();
375
376 if let Some(task) = task {
377 task.await.log_err();
378 }
379 }));
380 }
381
382 fn start_editing_message(
383 &mut self,
384 message_id: MessageId,
385 message_text: String,
386 window: &mut Window,
387 cx: &mut Context<Self>,
388 ) {
389 let buffer = cx.new(|cx| {
390 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
391 });
392 let editor = cx.new(|cx| {
393 let mut editor = Editor::new(
394 editor::EditorMode::AutoHeight { max_lines: 8 },
395 buffer,
396 None,
397 false,
398 window,
399 cx,
400 );
401 editor.focus_handle(cx).focus(window);
402 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
403 editor
404 });
405 self.editing_message = Some((
406 message_id,
407 EditMessageState {
408 editor: editor.clone(),
409 },
410 ));
411 cx.notify();
412 }
413
414 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
415 self.editing_message.take();
416 cx.notify();
417 }
418
419 fn confirm_editing_message(
420 &mut self,
421 _: &menu::Confirm,
422 _: &mut Window,
423 cx: &mut Context<Self>,
424 ) {
425 let Some((message_id, state)) = self.editing_message.take() else {
426 return;
427 };
428 let edited_text = state.editor.read(cx).text(cx);
429 self.thread.update(cx, |thread, cx| {
430 thread.edit_message(message_id, Role::User, edited_text, cx);
431 for message_id in self.messages_after(message_id) {
432 thread.delete_message(*message_id, cx);
433 }
434 });
435
436 let provider = LanguageModelRegistry::read_global(cx).active_provider();
437 if provider
438 .as_ref()
439 .map_or(false, |provider| provider.must_accept_terms(cx))
440 {
441 cx.notify();
442 return;
443 }
444 let model_registry = LanguageModelRegistry::read_global(cx);
445 let Some(model) = model_registry.active_model() else {
446 return;
447 };
448
449 self.thread.update(cx, |thread, cx| {
450 thread.send_to_model(model, RequestKind::Chat, cx)
451 });
452 cx.notify();
453 }
454
455 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
456 self.messages
457 .iter()
458 .rev()
459 .find(|message_id| {
460 self.thread
461 .read(cx)
462 .message(**message_id)
463 .map_or(false, |message| message.role == Role::User)
464 })
465 .cloned()
466 }
467
468 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
469 self.messages
470 .iter()
471 .position(|id| *id == message_id)
472 .map(|index| &self.messages[index + 1..])
473 .unwrap_or(&[])
474 }
475
476 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
477 self.cancel_editing_message(&menu::Cancel, window, cx);
478 }
479
480 fn handle_regenerate_click(
481 &mut self,
482 _: &ClickEvent,
483 window: &mut Window,
484 cx: &mut Context<Self>,
485 ) {
486 self.confirm_editing_message(&menu::Confirm, window, cx);
487 }
488
489 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
490 let message_id = self.messages[ix];
491 let Some(message) = self.thread.read(cx).message(message_id) else {
492 return Empty.into_any();
493 };
494
495 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
496 return Empty.into_any();
497 };
498
499 let thread = self.thread.read(cx);
500
501 let context = thread.context_for_message(message_id);
502 let tool_uses = thread.tool_uses_for_message(message_id);
503 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
504
505 // Don't render user messages that are just there for returning tool results.
506 if message.role == Role::User
507 && (thread.message_has_tool_results(message_id)
508 || thread.message_has_scripting_tool_results(message_id))
509 {
510 return Empty.into_any();
511 }
512
513 let allow_editing_message =
514 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
515
516 let edit_message_editor = self
517 .editing_message
518 .as_ref()
519 .filter(|(id, _)| *id == message_id)
520 .map(|(_, state)| state.editor.clone());
521
522 let colors = cx.theme().colors();
523
524 let message_content = v_flex()
525 .child(
526 if let Some(edit_message_editor) = edit_message_editor.clone() {
527 div()
528 .key_context("EditMessageEditor")
529 .on_action(cx.listener(Self::cancel_editing_message))
530 .on_action(cx.listener(Self::confirm_editing_message))
531 .p_2p5()
532 .child(edit_message_editor)
533 } else {
534 div().p_2p5().text_ui(cx).child(markdown.clone())
535 },
536 )
537 .when_some(context, |parent, context| {
538 if !context.is_empty() {
539 parent.child(
540 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
541 context
542 .into_iter()
543 .map(|context| ContextPill::added(context, false, false, None)),
544 ),
545 )
546 } else {
547 parent
548 }
549 });
550
551 let styled_message = match message.role {
552 Role::User => v_flex()
553 .id(("message-container", ix))
554 .pt_2p5()
555 .px_2p5()
556 .child(
557 v_flex()
558 .bg(colors.editor_background)
559 .rounded_lg()
560 .border_1()
561 .border_color(colors.border)
562 .shadow_sm()
563 .child(
564 h_flex()
565 .py_1()
566 .pl_2()
567 .pr_1()
568 .bg(colors.editor_foreground.opacity(0.05))
569 .border_b_1()
570 .border_color(colors.border)
571 .justify_between()
572 .rounded_t(px(6.))
573 .child(
574 h_flex()
575 .gap_1p5()
576 .child(
577 Icon::new(IconName::PersonCircle)
578 .size(IconSize::XSmall)
579 .color(Color::Muted),
580 )
581 .child(
582 Label::new("You")
583 .size(LabelSize::Small)
584 .color(Color::Muted),
585 ),
586 )
587 .when_some(
588 edit_message_editor.clone(),
589 |this, edit_message_editor| {
590 let focus_handle = edit_message_editor.focus_handle(cx);
591 this.child(
592 h_flex()
593 .gap_1()
594 .child(
595 Button::new("cancel-edit-message", "Cancel")
596 .label_size(LabelSize::Small)
597 .key_binding(
598 KeyBinding::for_action_in(
599 &menu::Cancel,
600 &focus_handle,
601 window,
602 cx,
603 )
604 .map(|kb| kb.size(rems_from_px(12.))),
605 )
606 .on_click(
607 cx.listener(Self::handle_cancel_click),
608 ),
609 )
610 .child(
611 Button::new(
612 "confirm-edit-message",
613 "Regenerate",
614 )
615 .label_size(LabelSize::Small)
616 .key_binding(
617 KeyBinding::for_action_in(
618 &menu::Confirm,
619 &focus_handle,
620 window,
621 cx,
622 )
623 .map(|kb| kb.size(rems_from_px(12.))),
624 )
625 .on_click(
626 cx.listener(Self::handle_regenerate_click),
627 ),
628 ),
629 )
630 },
631 )
632 .when(
633 edit_message_editor.is_none() && allow_editing_message,
634 |this| {
635 this.child(
636 Button::new("edit-message", "Edit")
637 .label_size(LabelSize::Small)
638 .on_click(cx.listener({
639 let message_text = message.text.clone();
640 move |this, _, window, cx| {
641 this.start_editing_message(
642 message_id,
643 message_text.clone(),
644 window,
645 cx,
646 );
647 }
648 })),
649 )
650 },
651 ),
652 )
653 .child(message_content),
654 ),
655 Role::Assistant => div()
656 .id(("message-container", ix))
657 .child(message_content)
658 .map(|parent| {
659 if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
660 return parent;
661 }
662
663 parent.child(
664 v_flex()
665 .children(
666 tool_uses
667 .into_iter()
668 .map(|tool_use| self.render_tool_use(tool_use, cx)),
669 )
670 .children(
671 scripting_tool_uses
672 .into_iter()
673 .map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
674 ),
675 )
676 }),
677 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
678 v_flex()
679 .bg(colors.editor_background)
680 .rounded_sm()
681 .child(message_content),
682 ),
683 };
684
685 styled_message.into_any()
686 }
687
688 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
689 let is_open = self
690 .expanded_tool_uses
691 .get(&tool_use.id)
692 .copied()
693 .unwrap_or_default();
694
695 div().px_2p5().child(
696 v_flex()
697 .gap_1()
698 .rounded_lg()
699 .border_1()
700 .border_color(cx.theme().colors().border)
701 .child(
702 h_flex()
703 .justify_between()
704 .py_0p5()
705 .pl_1()
706 .pr_2()
707 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
708 .map(|element| {
709 if is_open {
710 element.border_b_1().rounded_t(px(6.))
711 } else {
712 element.rounded_md()
713 }
714 })
715 .border_color(cx.theme().colors().border)
716 .child(
717 h_flex()
718 .gap_1()
719 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
720 cx.listener({
721 let tool_use_id = tool_use.id.clone();
722 move |this, _event, _window, _cx| {
723 let is_open = this
724 .expanded_tool_uses
725 .entry(tool_use_id.clone())
726 .or_insert(false);
727
728 *is_open = !*is_open;
729 }
730 }),
731 ))
732 .child(Label::new(tool_use.name)),
733 )
734 .child(
735 Label::new(match tool_use.status {
736 ToolUseStatus::Pending => "Pending",
737 ToolUseStatus::Running => "Running",
738 ToolUseStatus::Finished(_) => "Finished",
739 ToolUseStatus::Error(_) => "Error",
740 })
741 .size(LabelSize::XSmall)
742 .buffer_font(cx),
743 ),
744 )
745 .map(|parent| {
746 if !is_open {
747 return parent;
748 }
749
750 parent.child(
751 v_flex()
752 .child(
753 v_flex()
754 .gap_0p5()
755 .py_1()
756 .px_2p5()
757 .border_b_1()
758 .border_color(cx.theme().colors().border)
759 .child(Label::new("Input:"))
760 .child(Label::new(
761 serde_json::to_string_pretty(&tool_use.input)
762 .unwrap_or_default(),
763 )),
764 )
765 .map(|parent| match tool_use.status {
766 ToolUseStatus::Finished(output) => parent.child(
767 v_flex()
768 .gap_0p5()
769 .py_1()
770 .px_2p5()
771 .child(Label::new("Result:"))
772 .child(Label::new(output)),
773 ),
774 ToolUseStatus::Error(err) => parent.child(
775 v_flex()
776 .gap_0p5()
777 .py_1()
778 .px_2p5()
779 .child(Label::new("Error:"))
780 .child(Label::new(err)),
781 ),
782 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
783 }),
784 )
785 }),
786 )
787 }
788
789 fn render_scripting_tool_use(
790 &self,
791 tool_use: ToolUse,
792 cx: &mut Context<Self>,
793 ) -> impl IntoElement {
794 let is_open = self
795 .expanded_tool_uses
796 .get(&tool_use.id)
797 .copied()
798 .unwrap_or_default();
799
800 div().px_2p5().child(
801 v_flex()
802 .gap_1()
803 .rounded_lg()
804 .border_1()
805 .border_color(cx.theme().colors().border)
806 .child(
807 h_flex()
808 .justify_between()
809 .py_0p5()
810 .pl_1()
811 .pr_2()
812 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
813 .map(|element| {
814 if is_open {
815 element.border_b_1().rounded_t(px(6.))
816 } else {
817 element.rounded_md()
818 }
819 })
820 .border_color(cx.theme().colors().border)
821 .child(
822 h_flex()
823 .gap_1()
824 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
825 cx.listener({
826 let tool_use_id = tool_use.id.clone();
827 move |this, _event, _window, _cx| {
828 let is_open = this
829 .expanded_tool_uses
830 .entry(tool_use_id.clone())
831 .or_insert(false);
832
833 *is_open = !*is_open;
834 }
835 }),
836 ))
837 .child(Label::new(tool_use.name)),
838 )
839 .child(
840 Label::new(match tool_use.status {
841 ToolUseStatus::Pending => "Pending",
842 ToolUseStatus::Running => "Running",
843 ToolUseStatus::Finished(_) => "Finished",
844 ToolUseStatus::Error(_) => "Error",
845 })
846 .size(LabelSize::XSmall)
847 .buffer_font(cx),
848 ),
849 )
850 .map(|parent| {
851 if !is_open {
852 return parent;
853 }
854
855 let lua_script_markdown =
856 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
857
858 parent.child(
859 v_flex()
860 .child(
861 v_flex()
862 .gap_0p5()
863 .py_1()
864 .px_2p5()
865 .border_b_1()
866 .border_color(cx.theme().colors().border)
867 .child(Label::new("Input:"))
868 .map(|parent| {
869 if let Some(markdown) = lua_script_markdown {
870 parent.child(markdown)
871 } else {
872 parent.child(Label::new(
873 "Failed to render script input to Markdown",
874 ))
875 }
876 }),
877 )
878 .map(|parent| match tool_use.status {
879 ToolUseStatus::Finished(output) => parent.child(
880 v_flex()
881 .gap_0p5()
882 .py_1()
883 .px_2p5()
884 .child(Label::new("Result:"))
885 .child(Label::new(output)),
886 ),
887 ToolUseStatus::Error(err) => parent.child(
888 v_flex()
889 .gap_0p5()
890 .py_1()
891 .px_2p5()
892 .child(Label::new("Error:"))
893 .child(Label::new(err)),
894 ),
895 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
896 }),
897 )
898 }),
899 )
900 }
901}
902
903impl Render for ActiveThread {
904 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
905 v_flex()
906 .size_full()
907 .child(list(self.list_state.clone()).flex_grow())
908 }
909}