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