1use crate::thread::{MessageId, RequestKind, Thread, ThreadError, ThreadEvent};
2use crate::thread_store::ThreadStore;
3use crate::tool_use::{ToolUse, ToolUseStatus};
4use crate::ui::ContextPill;
5use collections::HashMap;
6use editor::{Editor, MultiBuffer};
7use gpui::{
8 list, percentage, AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent,
9 DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Length, ListAlignment, ListOffset,
10 ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Transformation,
11 UnderlineStyle,
12};
13use language::{Buffer, LanguageRegistry};
14use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
15use markdown::{Markdown, MarkdownStyle};
16use scripting_tool::{ScriptingTool, ScriptingToolInput};
17use settings::Settings as _;
18use std::sync::Arc;
19use std::time::Duration;
20use theme::ThemeSettings;
21use ui::Color;
22use ui::{prelude::*, Disclosure, KeyBinding};
23use util::ResultExt as _;
24
25pub struct ActiveThread {
26 language_registry: Arc<LanguageRegistry>,
27 thread_store: Entity<ThreadStore>,
28 thread: Entity<Thread>,
29 save_thread_task: Option<Task<()>>,
30 messages: Vec<MessageId>,
31 list_state: ListState,
32 rendered_messages_by_id: HashMap<MessageId, Entity<Markdown>>,
33 rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
34 editing_message: Option<(MessageId, EditMessageState)>,
35 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
36 last_error: Option<ThreadError>,
37 _subscriptions: Vec<Subscription>,
38}
39
40struct EditMessageState {
41 editor: Entity<Editor>,
42}
43
44impl ActiveThread {
45 pub fn new(
46 thread: Entity<Thread>,
47 thread_store: Entity<ThreadStore>,
48 language_registry: Arc<LanguageRegistry>,
49 window: &mut Window,
50 cx: &mut Context<Self>,
51 ) -> Self {
52 let subscriptions = vec![
53 cx.observe(&thread, |_, _, cx| cx.notify()),
54 cx.subscribe_in(&thread, window, Self::handle_thread_event),
55 ];
56
57 let mut this = Self {
58 language_registry,
59 thread_store,
60 thread: thread.clone(),
61 save_thread_task: None,
62 messages: Vec::new(),
63 rendered_messages_by_id: HashMap::default(),
64 rendered_scripting_tool_uses: HashMap::default(),
65 expanded_tool_uses: HashMap::default(),
66 list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
67 let this = cx.entity().downgrade();
68 move |ix, window: &mut Window, cx: &mut App| {
69 this.update(cx, |this, cx| this.render_message(ix, window, cx))
70 .unwrap()
71 }
72 }),
73 editing_message: None,
74 last_error: None,
75 _subscriptions: subscriptions,
76 };
77
78 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
79 this.push_message(&message.id, message.text.clone(), window, cx);
80
81 for tool_use in thread.read(cx).scripting_tool_uses_for_message(message.id) {
82 this.render_scripting_tool_use_markdown(
83 tool_use.id.clone(),
84 tool_use.name.as_ref(),
85 tool_use.input.clone(),
86 window,
87 cx,
88 );
89 }
90 }
91
92 this
93 }
94
95 pub fn thread(&self) -> &Entity<Thread> {
96 &self.thread
97 }
98
99 pub fn is_empty(&self) -> bool {
100 self.messages.is_empty()
101 }
102
103 pub fn summary(&self, cx: &App) -> Option<SharedString> {
104 self.thread.read(cx).summary()
105 }
106
107 pub fn summary_or_default(&self, cx: &App) -> SharedString {
108 self.thread.read(cx).summary_or_default()
109 }
110
111 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
112 self.last_error.take();
113 self.thread
114 .update(cx, |thread, _cx| thread.cancel_last_completion())
115 }
116
117 pub fn last_error(&self) -> Option<ThreadError> {
118 self.last_error.clone()
119 }
120
121 pub fn clear_last_error(&mut self) {
122 self.last_error.take();
123 }
124
125 fn push_message(
126 &mut self,
127 id: &MessageId,
128 text: String,
129 window: &mut Window,
130 cx: &mut Context<Self>,
131 ) {
132 let old_len = self.messages.len();
133 self.messages.push(*id);
134 self.list_state.splice(old_len..old_len, 1);
135
136 let markdown = self.render_markdown(text.into(), window, cx);
137 self.rendered_messages_by_id.insert(*id, markdown);
138 self.list_state.scroll_to(ListOffset {
139 item_ix: old_len,
140 offset_in_item: Pixels(0.0),
141 });
142 }
143
144 fn edited_message(
145 &mut self,
146 id: &MessageId,
147 text: String,
148 window: &mut Window,
149 cx: &mut Context<Self>,
150 ) {
151 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
152 return;
153 };
154 self.list_state.splice(index..index + 1, 1);
155 let markdown = self.render_markdown(text.into(), window, cx);
156 self.rendered_messages_by_id.insert(*id, markdown);
157 }
158
159 fn deleted_message(&mut self, id: &MessageId) {
160 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
161 return;
162 };
163 self.messages.remove(index);
164 self.list_state.splice(index..index + 1, 0);
165 self.rendered_messages_by_id.remove(id);
166 }
167
168 fn render_markdown(
169 &self,
170 text: SharedString,
171 window: &Window,
172 cx: &mut Context<Self>,
173 ) -> Entity<Markdown> {
174 let theme_settings = ThemeSettings::get_global(cx);
175 let colors = cx.theme().colors();
176 let ui_font_size = TextSize::Default.rems(cx);
177 let buffer_font_size = TextSize::Small.rems(cx);
178 let mut text_style = window.text_style();
179
180 text_style.refine(&TextStyleRefinement {
181 font_family: Some(theme_settings.ui_font.family.clone()),
182 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
183 font_features: Some(theme_settings.ui_font.features.clone()),
184 font_size: Some(ui_font_size.into()),
185 color: Some(cx.theme().colors().text),
186 ..Default::default()
187 });
188
189 let markdown_style = MarkdownStyle {
190 base_text_style: text_style,
191 syntax: cx.theme().syntax().clone(),
192 selection_background_color: cx.theme().players().local().selection,
193 code_block_overflow_x_scroll: true,
194 table_overflow_x_scroll: true,
195 code_block: StyleRefinement {
196 margin: EdgesRefinement {
197 top: Some(Length::Definite(rems(0.).into())),
198 left: Some(Length::Definite(rems(0.).into())),
199 right: Some(Length::Definite(rems(0.).into())),
200 bottom: Some(Length::Definite(rems(0.5).into())),
201 },
202 padding: EdgesRefinement {
203 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
204 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
205 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
206 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
207 },
208 background: Some(colors.editor_background.into()),
209 border_color: Some(colors.border_variant),
210 border_widths: EdgesRefinement {
211 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
212 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
213 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
214 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
215 },
216 text: Some(TextStyleRefinement {
217 font_family: Some(theme_settings.buffer_font.family.clone()),
218 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
219 font_features: Some(theme_settings.buffer_font.features.clone()),
220 font_size: Some(buffer_font_size.into()),
221 ..Default::default()
222 }),
223 ..Default::default()
224 },
225 inline_code: TextStyleRefinement {
226 font_family: Some(theme_settings.buffer_font.family.clone()),
227 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
228 font_features: Some(theme_settings.buffer_font.features.clone()),
229 font_size: Some(buffer_font_size.into()),
230 background_color: Some(colors.editor_foreground.opacity(0.1)),
231 ..Default::default()
232 },
233 link: TextStyleRefinement {
234 background_color: Some(colors.editor_foreground.opacity(0.025)),
235 underline: Some(UnderlineStyle {
236 color: Some(colors.text_accent.opacity(0.5)),
237 thickness: px(1.),
238 ..Default::default()
239 }),
240 ..Default::default()
241 },
242 ..Default::default()
243 };
244
245 cx.new(|cx| {
246 Markdown::new(
247 text,
248 markdown_style,
249 Some(self.language_registry.clone()),
250 None,
251 cx,
252 )
253 })
254 }
255
256 /// Renders the input of a scripting tool use to Markdown.
257 ///
258 /// Does nothing if the tool use does not correspond to the scripting tool.
259 fn render_scripting_tool_use_markdown(
260 &mut self,
261 tool_use_id: LanguageModelToolUseId,
262 tool_name: &str,
263 tool_input: serde_json::Value,
264 window: &mut Window,
265 cx: &mut Context<Self>,
266 ) {
267 if tool_name != ScriptingTool::NAME {
268 return;
269 }
270
271 let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
272 .map(|input| input.lua_script)
273 .unwrap_or_default();
274
275 let lua_script =
276 self.render_markdown(format!("```lua\n{lua_script}\n```").into(), window, cx);
277
278 self.rendered_scripting_tool_uses
279 .insert(tool_use_id, lua_script);
280 }
281
282 fn handle_thread_event(
283 &mut self,
284 _thread: &Entity<Thread>,
285 event: &ThreadEvent,
286 window: &mut Window,
287 cx: &mut Context<Self>,
288 ) {
289 match event {
290 ThreadEvent::ShowError(error) => {
291 self.last_error = Some(error.clone());
292 }
293 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
294 self.save_thread(cx);
295 }
296 ThreadEvent::StreamedAssistantText(message_id, text) => {
297 if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
298 markdown.update(cx, |markdown, cx| {
299 markdown.append(text, cx);
300 });
301 }
302 }
303 ThreadEvent::MessageAdded(message_id) => {
304 if let Some(message_text) = self
305 .thread
306 .read(cx)
307 .message(*message_id)
308 .map(|message| message.text.clone())
309 {
310 self.push_message(message_id, message_text, window, cx);
311 }
312
313 self.save_thread(cx);
314 cx.notify();
315 }
316 ThreadEvent::MessageEdited(message_id) => {
317 if let Some(message_text) = self
318 .thread
319 .read(cx)
320 .message(*message_id)
321 .map(|message| message.text.clone())
322 {
323 self.edited_message(message_id, message_text, window, cx);
324 }
325
326 self.save_thread(cx);
327 cx.notify();
328 }
329 ThreadEvent::MessageDeleted(message_id) => {
330 self.deleted_message(message_id);
331 self.save_thread(cx);
332 cx.notify();
333 }
334 ThreadEvent::UsePendingTools => {
335 self.thread.update(cx, |thread, cx| {
336 thread.use_pending_tools(cx);
337 });
338 }
339 ThreadEvent::ToolFinished {
340 pending_tool_use, ..
341 } => {
342 if let Some(tool_use) = pending_tool_use {
343 self.render_scripting_tool_use_markdown(
344 tool_use.id.clone(),
345 tool_use.name.as_ref(),
346 tool_use.input.clone(),
347 window,
348 cx,
349 );
350 }
351
352 if self.thread.read(cx).all_tools_finished() {
353 let model_registry = LanguageModelRegistry::read_global(cx);
354 if let Some(model) = model_registry.active_model() {
355 self.thread.update(cx, |thread, cx| {
356 thread.send_tool_results_to_model(model, cx);
357 });
358 }
359 }
360 }
361 }
362 }
363
364 /// Spawns a task to save the active thread.
365 ///
366 /// Only one task to save the thread will be in flight at a time.
367 fn save_thread(&mut self, cx: &mut Context<Self>) {
368 let thread = self.thread.clone();
369 self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
370 let task = this
371 .update(&mut cx, |this, cx| {
372 this.thread_store
373 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
374 })
375 .ok();
376
377 if let Some(task) = task {
378 task.await.log_err();
379 }
380 }));
381 }
382
383 fn start_editing_message(
384 &mut self,
385 message_id: MessageId,
386 message_text: String,
387 window: &mut Window,
388 cx: &mut Context<Self>,
389 ) {
390 let buffer = cx.new(|cx| {
391 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
392 });
393 let editor = cx.new(|cx| {
394 let mut editor = Editor::new(
395 editor::EditorMode::AutoHeight { max_lines: 8 },
396 buffer,
397 None,
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 // Get all the data we need from thread before we start using it in closures
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 => {
656 v_flex()
657 .id(("message-container", ix))
658 .child(message_content)
659 .when(
660 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
661 |parent| {
662 parent.child(
663 v_flex()
664 .children(
665 tool_uses
666 .into_iter()
667 .map(|tool_use| self.render_tool_use(tool_use, cx)),
668 )
669 .children(scripting_tool_uses.into_iter().map(|tool_use| {
670 self.render_scripting_tool_use(tool_use, cx)
671 })),
672 )
673 },
674 )
675 }
676 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
677 v_flex()
678 .bg(colors.editor_background)
679 .rounded_sm()
680 .child(message_content),
681 ),
682 };
683
684 styled_message.into_any()
685 }
686
687 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
688 let is_open = self
689 .expanded_tool_uses
690 .get(&tool_use.id)
691 .copied()
692 .unwrap_or_default();
693
694 let lighter_border = cx.theme().colors().border.opacity(0.5);
695
696 div().px_2p5().child(
697 v_flex()
698 .rounded_lg()
699 .border_1()
700 .border_color(lighter_border)
701 .child(
702 h_flex()
703 .justify_between()
704 .py_1()
705 .pl_1()
706 .pr_2()
707 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
708 .map(|element| {
709 if is_open {
710 element.border_b_1().rounded_t_md()
711 } else {
712 element.rounded_md()
713 }
714 })
715 .border_color(lighter_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(
733 Label::new(tool_use.name)
734 .size(LabelSize::Small)
735 .buffer_font(cx),
736 ),
737 )
738 .child({
739 let (icon_name, color, animated) = match &tool_use.status {
740 ToolUseStatus::Pending => {
741 (IconName::Warning, Color::Warning, false)
742 }
743 ToolUseStatus::Running => {
744 (IconName::ArrowCircle, Color::Accent, true)
745 }
746 ToolUseStatus::Finished(_) => {
747 (IconName::Check, Color::Success, false)
748 }
749 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
750 };
751
752 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
753
754 if animated {
755 icon.with_animation(
756 "arrow-circle",
757 Animation::new(Duration::from_secs(2)).repeat(),
758 |icon, delta| {
759 icon.transform(Transformation::rotate(percentage(delta)))
760 },
761 )
762 .into_any_element()
763 } else {
764 icon.into_any_element()
765 }
766 }),
767 )
768 .map(|parent| {
769 if !is_open {
770 return parent;
771 }
772
773 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
774
775 parent.child(
776 v_flex()
777 .gap_1()
778 .bg(cx.theme().colors().editor_background)
779 .rounded_b_lg()
780 .child(
781 content_container()
782 .border_b_1()
783 .border_color(lighter_border)
784 .child(
785 Label::new("Input")
786 .size(LabelSize::XSmall)
787 .color(Color::Muted)
788 .buffer_font(cx),
789 )
790 .child(
791 Label::new(
792 serde_json::to_string_pretty(&tool_use.input)
793 .unwrap_or_default(),
794 )
795 .size(LabelSize::Small)
796 .buffer_font(cx),
797 ),
798 )
799 .map(|container| match tool_use.status {
800 ToolUseStatus::Finished(output) => container.child(
801 content_container()
802 .child(
803 Label::new("Result")
804 .size(LabelSize::XSmall)
805 .color(Color::Muted)
806 .buffer_font(cx),
807 )
808 .child(
809 Label::new(output)
810 .size(LabelSize::Small)
811 .buffer_font(cx),
812 ),
813 ),
814 ToolUseStatus::Running => container.child(
815 content_container().child(
816 h_flex()
817 .gap_1()
818 .pb_1()
819 .child(
820 Icon::new(IconName::ArrowCircle)
821 .size(IconSize::Small)
822 .color(Color::Accent)
823 .with_animation(
824 "arrow-circle",
825 Animation::new(Duration::from_secs(2))
826 .repeat(),
827 |icon, delta| {
828 icon.transform(Transformation::rotate(
829 percentage(delta),
830 ))
831 },
832 ),
833 )
834 .child(
835 Label::new("Running…")
836 .size(LabelSize::XSmall)
837 .color(Color::Muted)
838 .buffer_font(cx),
839 ),
840 ),
841 ),
842 ToolUseStatus::Error(err) => container.child(
843 content_container()
844 .child(
845 Label::new("Error")
846 .size(LabelSize::XSmall)
847 .color(Color::Muted)
848 .buffer_font(cx),
849 )
850 .child(
851 Label::new(err).size(LabelSize::Small).buffer_font(cx),
852 ),
853 ),
854 ToolUseStatus::Pending => container,
855 }),
856 )
857 }),
858 )
859 }
860
861 fn render_scripting_tool_use(
862 &self,
863 tool_use: ToolUse,
864 cx: &mut Context<Self>,
865 ) -> impl IntoElement {
866 let is_open = self
867 .expanded_tool_uses
868 .get(&tool_use.id)
869 .copied()
870 .unwrap_or_default();
871
872 div().px_2p5().child(
873 v_flex()
874 .gap_1()
875 .rounded_lg()
876 .border_1()
877 .border_color(cx.theme().colors().border)
878 .child(
879 h_flex()
880 .justify_between()
881 .py_0p5()
882 .pl_1()
883 .pr_2()
884 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
885 .map(|element| {
886 if is_open {
887 element.border_b_1().rounded_t_md()
888 } else {
889 element.rounded_md()
890 }
891 })
892 .border_color(cx.theme().colors().border)
893 .child(
894 h_flex()
895 .gap_1()
896 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
897 cx.listener({
898 let tool_use_id = tool_use.id.clone();
899 move |this, _event, _window, _cx| {
900 let is_open = this
901 .expanded_tool_uses
902 .entry(tool_use_id.clone())
903 .or_insert(false);
904
905 *is_open = !*is_open;
906 }
907 }),
908 ))
909 .child(Label::new(tool_use.name)),
910 )
911 .child(
912 Label::new(match tool_use.status {
913 ToolUseStatus::Pending => "Pending",
914 ToolUseStatus::Running => "Running",
915 ToolUseStatus::Finished(_) => "Finished",
916 ToolUseStatus::Error(_) => "Error",
917 })
918 .size(LabelSize::XSmall)
919 .buffer_font(cx),
920 ),
921 )
922 .map(|parent| {
923 if !is_open {
924 return parent;
925 }
926
927 let lua_script_markdown =
928 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
929
930 parent.child(
931 v_flex()
932 .child(
933 v_flex()
934 .gap_0p5()
935 .py_1()
936 .px_2p5()
937 .border_b_1()
938 .border_color(cx.theme().colors().border)
939 .child(Label::new("Input:"))
940 .map(|parent| {
941 if let Some(markdown) = lua_script_markdown {
942 parent.child(markdown)
943 } else {
944 parent.child(Label::new(
945 "Failed to render script input to Markdown",
946 ))
947 }
948 }),
949 )
950 .map(|parent| match tool_use.status {
951 ToolUseStatus::Finished(output) => parent.child(
952 v_flex()
953 .gap_0p5()
954 .py_1()
955 .px_2p5()
956 .child(Label::new("Result:"))
957 .child(Label::new(output)),
958 ),
959 ToolUseStatus::Error(err) => parent.child(
960 v_flex()
961 .gap_0p5()
962 .py_1()
963 .px_2p5()
964 .child(Label::new("Error:"))
965 .child(Label::new(err)),
966 ),
967 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
968 }),
969 )
970 }),
971 )
972 }
973}
974
975impl Render for ActiveThread {
976 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
977 v_flex()
978 .size_full()
979 .child(list(self.list_state.clone()).flex_grow())
980 }
981}