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 false,
400 window,
401 cx,
402 );
403 editor.focus_handle(cx).focus(window);
404 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
405 editor
406 });
407 self.editing_message = Some((
408 message_id,
409 EditMessageState {
410 editor: editor.clone(),
411 },
412 ));
413 cx.notify();
414 }
415
416 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
417 self.editing_message.take();
418 cx.notify();
419 }
420
421 fn confirm_editing_message(
422 &mut self,
423 _: &menu::Confirm,
424 _: &mut Window,
425 cx: &mut Context<Self>,
426 ) {
427 let Some((message_id, state)) = self.editing_message.take() else {
428 return;
429 };
430 let edited_text = state.editor.read(cx).text(cx);
431 self.thread.update(cx, |thread, cx| {
432 thread.edit_message(message_id, Role::User, edited_text, cx);
433 for message_id in self.messages_after(message_id) {
434 thread.delete_message(*message_id, cx);
435 }
436 });
437
438 let provider = LanguageModelRegistry::read_global(cx).active_provider();
439 if provider
440 .as_ref()
441 .map_or(false, |provider| provider.must_accept_terms(cx))
442 {
443 cx.notify();
444 return;
445 }
446 let model_registry = LanguageModelRegistry::read_global(cx);
447 let Some(model) = model_registry.active_model() else {
448 return;
449 };
450
451 self.thread.update(cx, |thread, cx| {
452 thread.send_to_model(model, RequestKind::Chat, cx)
453 });
454 cx.notify();
455 }
456
457 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
458 self.messages
459 .iter()
460 .rev()
461 .find(|message_id| {
462 self.thread
463 .read(cx)
464 .message(**message_id)
465 .map_or(false, |message| message.role == Role::User)
466 })
467 .cloned()
468 }
469
470 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
471 self.messages
472 .iter()
473 .position(|id| *id == message_id)
474 .map(|index| &self.messages[index + 1..])
475 .unwrap_or(&[])
476 }
477
478 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
479 self.cancel_editing_message(&menu::Cancel, window, cx);
480 }
481
482 fn handle_regenerate_click(
483 &mut self,
484 _: &ClickEvent,
485 window: &mut Window,
486 cx: &mut Context<Self>,
487 ) {
488 self.confirm_editing_message(&menu::Confirm, window, cx);
489 }
490
491 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
492 let message_id = self.messages[ix];
493 let Some(message) = self.thread.read(cx).message(message_id) else {
494 return Empty.into_any();
495 };
496
497 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
498 return Empty.into_any();
499 };
500
501 let thread = self.thread.read(cx);
502
503 let context = thread.context_for_message(message_id);
504 let tool_uses = thread.tool_uses_for_message(message_id);
505 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
506
507 // Don't render user messages that are just there for returning tool results.
508 if message.role == Role::User
509 && (thread.message_has_tool_results(message_id)
510 || thread.message_has_scripting_tool_results(message_id))
511 {
512 return Empty.into_any();
513 }
514
515 let allow_editing_message =
516 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
517
518 let edit_message_editor = self
519 .editing_message
520 .as_ref()
521 .filter(|(id, _)| *id == message_id)
522 .map(|(_, state)| state.editor.clone());
523
524 let colors = cx.theme().colors();
525
526 let message_content = v_flex()
527 .child(
528 if let Some(edit_message_editor) = edit_message_editor.clone() {
529 div()
530 .key_context("EditMessageEditor")
531 .on_action(cx.listener(Self::cancel_editing_message))
532 .on_action(cx.listener(Self::confirm_editing_message))
533 .p_2p5()
534 .child(edit_message_editor)
535 } else {
536 div().p_2p5().text_ui(cx).child(markdown.clone())
537 },
538 )
539 .when_some(context, |parent, context| {
540 if !context.is_empty() {
541 parent.child(
542 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
543 context
544 .into_iter()
545 .map(|context| ContextPill::added(context, false, false, None)),
546 ),
547 )
548 } else {
549 parent
550 }
551 });
552
553 let styled_message = match message.role {
554 Role::User => v_flex()
555 .id(("message-container", ix))
556 .pt_2p5()
557 .px_2p5()
558 .child(
559 v_flex()
560 .bg(colors.editor_background)
561 .rounded_lg()
562 .border_1()
563 .border_color(colors.border)
564 .shadow_sm()
565 .child(
566 h_flex()
567 .py_1()
568 .pl_2()
569 .pr_1()
570 .bg(colors.editor_foreground.opacity(0.05))
571 .border_b_1()
572 .border_color(colors.border)
573 .justify_between()
574 .rounded_t(px(6.))
575 .child(
576 h_flex()
577 .gap_1p5()
578 .child(
579 Icon::new(IconName::PersonCircle)
580 .size(IconSize::XSmall)
581 .color(Color::Muted),
582 )
583 .child(
584 Label::new("You")
585 .size(LabelSize::Small)
586 .color(Color::Muted),
587 ),
588 )
589 .when_some(
590 edit_message_editor.clone(),
591 |this, edit_message_editor| {
592 let focus_handle = edit_message_editor.focus_handle(cx);
593 this.child(
594 h_flex()
595 .gap_1()
596 .child(
597 Button::new("cancel-edit-message", "Cancel")
598 .label_size(LabelSize::Small)
599 .key_binding(
600 KeyBinding::for_action_in(
601 &menu::Cancel,
602 &focus_handle,
603 window,
604 cx,
605 )
606 .map(|kb| kb.size(rems_from_px(12.))),
607 )
608 .on_click(
609 cx.listener(Self::handle_cancel_click),
610 ),
611 )
612 .child(
613 Button::new(
614 "confirm-edit-message",
615 "Regenerate",
616 )
617 .label_size(LabelSize::Small)
618 .key_binding(
619 KeyBinding::for_action_in(
620 &menu::Confirm,
621 &focus_handle,
622 window,
623 cx,
624 )
625 .map(|kb| kb.size(rems_from_px(12.))),
626 )
627 .on_click(
628 cx.listener(Self::handle_regenerate_click),
629 ),
630 ),
631 )
632 },
633 )
634 .when(
635 edit_message_editor.is_none() && allow_editing_message,
636 |this| {
637 this.child(
638 Button::new("edit-message", "Edit")
639 .label_size(LabelSize::Small)
640 .on_click(cx.listener({
641 let message_text = message.text.clone();
642 move |this, _, window, cx| {
643 this.start_editing_message(
644 message_id,
645 message_text.clone(),
646 window,
647 cx,
648 );
649 }
650 })),
651 )
652 },
653 ),
654 )
655 .child(message_content),
656 ),
657 Role::Assistant => v_flex()
658 .id(("message-container", ix))
659 .child(message_content)
660 .map(|parent| {
661 if tool_uses.is_empty() && scripting_tool_uses.is_empty() {
662 return parent;
663 }
664
665 parent.child(
666 v_flex()
667 .children(
668 tool_uses
669 .into_iter()
670 .map(|tool_use| self.render_tool_use(tool_use, cx)),
671 )
672 .children(
673 scripting_tool_uses
674 .into_iter()
675 .map(|tool_use| self.render_scripting_tool_use(tool_use, cx)),
676 ),
677 )
678 }),
679 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
680 v_flex()
681 .bg(colors.editor_background)
682 .rounded_sm()
683 .child(message_content),
684 ),
685 };
686
687 styled_message.into_any()
688 }
689
690 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
691 let is_open = self
692 .expanded_tool_uses
693 .get(&tool_use.id)
694 .copied()
695 .unwrap_or_default();
696
697 div().px_2p5().child(
698 v_flex()
699 .gap_1()
700 .rounded_lg()
701 .border_1()
702 .border_color(cx.theme().colors().border)
703 .child(
704 h_flex()
705 .justify_between()
706 .py_1()
707 .pl_1()
708 .pr_2()
709 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
710 .map(|element| {
711 if is_open {
712 element.border_b_1().rounded_t_md()
713 } else {
714 element.rounded_md()
715 }
716 })
717 .border_color(cx.theme().colors().border)
718 .child(
719 h_flex()
720 .gap_1()
721 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
722 cx.listener({
723 let tool_use_id = tool_use.id.clone();
724 move |this, _event, _window, _cx| {
725 let is_open = this
726 .expanded_tool_uses
727 .entry(tool_use_id.clone())
728 .or_insert(false);
729
730 *is_open = !*is_open;
731 }
732 }),
733 ))
734 .child(Label::new(tool_use.name)),
735 )
736 .child({
737 let (icon_name, color, animated) = match &tool_use.status {
738 ToolUseStatus::Pending => {
739 (IconName::Warning, Color::Warning, false)
740 }
741 ToolUseStatus::Running => {
742 (IconName::ArrowCircle, Color::Accent, true)
743 }
744 ToolUseStatus::Finished(_) => {
745 (IconName::Check, Color::Success, false)
746 }
747 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
748 };
749
750 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
751
752 if animated {
753 icon.with_animation(
754 "arrow-circle",
755 Animation::new(Duration::from_secs(2)).repeat(),
756 |icon, delta| {
757 icon.transform(Transformation::rotate(percentage(delta)))
758 },
759 )
760 .into_any_element()
761 } else {
762 icon.into_any_element()
763 }
764 }),
765 )
766 .map(|parent| {
767 if !is_open {
768 return parent;
769 }
770
771 parent.child(
772 v_flex()
773 .child(
774 v_flex()
775 .gap_0p5()
776 .py_1()
777 .px_2p5()
778 .border_b_1()
779 .border_color(cx.theme().colors().border)
780 .child(Label::new("Input:"))
781 .child(Label::new(
782 serde_json::to_string_pretty(&tool_use.input)
783 .unwrap_or_default(),
784 )),
785 )
786 .map(|parent| match tool_use.status {
787 ToolUseStatus::Finished(output) => parent.child(
788 v_flex()
789 .gap_0p5()
790 .py_1()
791 .px_2p5()
792 .child(Label::new("Result:"))
793 .child(Label::new(output)),
794 ),
795 ToolUseStatus::Error(err) => parent.child(
796 v_flex()
797 .gap_0p5()
798 .py_1()
799 .px_2p5()
800 .child(Label::new("Error:"))
801 .child(Label::new(err)),
802 ),
803 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
804 }),
805 )
806 }),
807 )
808 }
809
810 fn render_scripting_tool_use(
811 &self,
812 tool_use: ToolUse,
813 cx: &mut Context<Self>,
814 ) -> impl IntoElement {
815 let is_open = self
816 .expanded_tool_uses
817 .get(&tool_use.id)
818 .copied()
819 .unwrap_or_default();
820
821 div().px_2p5().child(
822 v_flex()
823 .gap_1()
824 .rounded_lg()
825 .border_1()
826 .border_color(cx.theme().colors().border)
827 .child(
828 h_flex()
829 .justify_between()
830 .py_0p5()
831 .pl_1()
832 .pr_2()
833 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
834 .map(|element| {
835 if is_open {
836 element.border_b_1().rounded_t_md()
837 } else {
838 element.rounded_md()
839 }
840 })
841 .border_color(cx.theme().colors().border)
842 .child(
843 h_flex()
844 .gap_1()
845 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
846 cx.listener({
847 let tool_use_id = tool_use.id.clone();
848 move |this, _event, _window, _cx| {
849 let is_open = this
850 .expanded_tool_uses
851 .entry(tool_use_id.clone())
852 .or_insert(false);
853
854 *is_open = !*is_open;
855 }
856 }),
857 ))
858 .child(Label::new(tool_use.name)),
859 )
860 .child(
861 Label::new(match tool_use.status {
862 ToolUseStatus::Pending => "Pending",
863 ToolUseStatus::Running => "Running",
864 ToolUseStatus::Finished(_) => "Finished",
865 ToolUseStatus::Error(_) => "Error",
866 })
867 .size(LabelSize::XSmall)
868 .buffer_font(cx),
869 ),
870 )
871 .map(|parent| {
872 if !is_open {
873 return parent;
874 }
875
876 let lua_script_markdown =
877 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
878
879 parent.child(
880 v_flex()
881 .child(
882 v_flex()
883 .gap_0p5()
884 .py_1()
885 .px_2p5()
886 .border_b_1()
887 .border_color(cx.theme().colors().border)
888 .child(Label::new("Input:"))
889 .map(|parent| {
890 if let Some(markdown) = lua_script_markdown {
891 parent.child(markdown)
892 } else {
893 parent.child(Label::new(
894 "Failed to render script input to Markdown",
895 ))
896 }
897 }),
898 )
899 .map(|parent| match tool_use.status {
900 ToolUseStatus::Finished(output) => parent.child(
901 v_flex()
902 .gap_0p5()
903 .py_1()
904 .px_2p5()
905 .child(Label::new("Result:"))
906 .child(Label::new(output)),
907 ),
908 ToolUseStatus::Error(err) => parent.child(
909 v_flex()
910 .gap_0p5()
911 .py_1()
912 .px_2p5()
913 .child(Label::new("Error:"))
914 .child(Label::new(err)),
915 ),
916 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
917 }),
918 )
919 }),
920 )
921 }
922}
923
924impl Render for ActiveThread {
925 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
926 v_flex()
927 .size_full()
928 .child(list(self.list_state.clone()).flex_grow())
929 }
930}