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