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())
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::StreamedAssistantText(message_id, text) => {
302 if let Some(markdown) = self.rendered_messages_by_id.get_mut(&message_id) {
303 markdown.update(cx, |markdown, cx| {
304 markdown.append(text, cx);
305 });
306 }
307 }
308 ThreadEvent::MessageAdded(message_id) => {
309 if let Some(message_text) = self
310 .thread
311 .read(cx)
312 .message(*message_id)
313 .map(|message| message.text.clone())
314 {
315 self.push_message(message_id, message_text, window, cx);
316 }
317
318 self.save_thread(cx);
319 cx.notify();
320 }
321 ThreadEvent::MessageEdited(message_id) => {
322 if let Some(message_text) = self
323 .thread
324 .read(cx)
325 .message(*message_id)
326 .map(|message| message.text.clone())
327 {
328 self.edited_message(message_id, message_text, window, cx);
329 }
330
331 self.save_thread(cx);
332 cx.notify();
333 }
334 ThreadEvent::MessageDeleted(message_id) => {
335 self.deleted_message(message_id);
336 self.save_thread(cx);
337 cx.notify();
338 }
339 ThreadEvent::UsePendingTools => {
340 self.thread.update(cx, |thread, cx| {
341 thread.use_pending_tools(cx);
342 });
343 }
344 ThreadEvent::ToolFinished {
345 pending_tool_use, ..
346 } => {
347 if let Some(tool_use) = pending_tool_use {
348 self.render_scripting_tool_use_markdown(
349 tool_use.id.clone(),
350 tool_use.name.as_ref(),
351 tool_use.input.clone(),
352 window,
353 cx,
354 );
355 }
356
357 if self.thread.read(cx).all_tools_finished() {
358 let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
359 thread.action_log().update(cx, |action_log, _cx| {
360 action_log.take_pending_refresh_buffers()
361 })
362 });
363
364 let context_update_task = if !pending_refresh_buffers.is_empty() {
365 let refresh_task = refresh_context_store_text(
366 self.context_store.clone(),
367 &pending_refresh_buffers,
368 cx,
369 );
370
371 cx.spawn(|this, mut cx| async move {
372 let updated_context_ids = refresh_task.await;
373
374 this.update(&mut cx, |this, cx| {
375 this.context_store.read_with(cx, |context_store, cx| {
376 context_store
377 .context()
378 .iter()
379 .filter(|context| {
380 updated_context_ids.contains(&context.id())
381 })
382 .flat_map(|context| context.snapshot(cx))
383 .collect()
384 })
385 })
386 })
387 } else {
388 Task::ready(anyhow::Ok(Vec::new()))
389 };
390
391 let model_registry = LanguageModelRegistry::read_global(cx);
392 if let Some(model) = model_registry.active_model() {
393 cx.spawn(|this, mut cx| async move {
394 let updated_context = context_update_task.await?;
395
396 this.update(&mut cx, |this, cx| {
397 this.thread.update(cx, |thread, cx| {
398 thread.send_tool_results_to_model(model, updated_context, cx);
399 });
400 })
401 })
402 .detach();
403 }
404 }
405 }
406 }
407 }
408
409 /// Spawns a task to save the active thread.
410 ///
411 /// Only one task to save the thread will be in flight at a time.
412 fn save_thread(&mut self, cx: &mut Context<Self>) {
413 let thread = self.thread.clone();
414 self.save_thread_task = Some(cx.spawn(|this, mut cx| async move {
415 let task = this
416 .update(&mut cx, |this, cx| {
417 this.thread_store
418 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
419 })
420 .ok();
421
422 if let Some(task) = task {
423 task.await.log_err();
424 }
425 }));
426 }
427
428 fn start_editing_message(
429 &mut self,
430 message_id: MessageId,
431 message_text: String,
432 window: &mut Window,
433 cx: &mut Context<Self>,
434 ) {
435 let buffer = cx.new(|cx| {
436 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
437 });
438 let editor = cx.new(|cx| {
439 let mut editor = Editor::new(
440 editor::EditorMode::AutoHeight { max_lines: 8 },
441 buffer,
442 None,
443 window,
444 cx,
445 );
446 editor.focus_handle(cx).focus(window);
447 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
448 editor
449 });
450 self.editing_message = Some((
451 message_id,
452 EditMessageState {
453 editor: editor.clone(),
454 },
455 ));
456 cx.notify();
457 }
458
459 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
460 self.editing_message.take();
461 cx.notify();
462 }
463
464 fn confirm_editing_message(
465 &mut self,
466 _: &menu::Confirm,
467 _: &mut Window,
468 cx: &mut Context<Self>,
469 ) {
470 let Some((message_id, state)) = self.editing_message.take() else {
471 return;
472 };
473 let edited_text = state.editor.read(cx).text(cx);
474 self.thread.update(cx, |thread, cx| {
475 thread.edit_message(message_id, Role::User, edited_text, cx);
476 for message_id in self.messages_after(message_id) {
477 thread.delete_message(*message_id, cx);
478 }
479 });
480
481 let provider = LanguageModelRegistry::read_global(cx).active_provider();
482 if provider
483 .as_ref()
484 .map_or(false, |provider| provider.must_accept_terms(cx))
485 {
486 cx.notify();
487 return;
488 }
489 let model_registry = LanguageModelRegistry::read_global(cx);
490 let Some(model) = model_registry.active_model() else {
491 return;
492 };
493
494 self.thread.update(cx, |thread, cx| {
495 thread.send_to_model(model, RequestKind::Chat, cx)
496 });
497 cx.notify();
498 }
499
500 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
501 self.messages
502 .iter()
503 .rev()
504 .find(|message_id| {
505 self.thread
506 .read(cx)
507 .message(**message_id)
508 .map_or(false, |message| message.role == Role::User)
509 })
510 .cloned()
511 }
512
513 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
514 self.messages
515 .iter()
516 .position(|id| *id == message_id)
517 .map(|index| &self.messages[index + 1..])
518 .unwrap_or(&[])
519 }
520
521 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
522 self.cancel_editing_message(&menu::Cancel, window, cx);
523 }
524
525 fn handle_regenerate_click(
526 &mut self,
527 _: &ClickEvent,
528 window: &mut Window,
529 cx: &mut Context<Self>,
530 ) {
531 self.confirm_editing_message(&menu::Confirm, window, cx);
532 }
533
534 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
535 let message_id = self.messages[ix];
536 let Some(message) = self.thread.read(cx).message(message_id) else {
537 return Empty.into_any();
538 };
539
540 let Some(markdown) = self.rendered_messages_by_id.get(&message_id) else {
541 return Empty.into_any();
542 };
543
544 let thread = self.thread.read(cx);
545 // Get all the data we need from thread before we start using it in closures
546 let context = thread.context_for_message(message_id);
547 let tool_uses = thread.tool_uses_for_message(message_id);
548 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
549
550 // Don't render user messages that are just there for returning tool results.
551 if message.role == Role::User
552 && (thread.message_has_tool_results(message_id)
553 || thread.message_has_scripting_tool_results(message_id))
554 {
555 return Empty.into_any();
556 }
557
558 let allow_editing_message =
559 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
560
561 let edit_message_editor = self
562 .editing_message
563 .as_ref()
564 .filter(|(id, _)| *id == message_id)
565 .map(|(_, state)| state.editor.clone());
566
567 let colors = cx.theme().colors();
568
569 let message_content = v_flex()
570 .child(
571 if let Some(edit_message_editor) = edit_message_editor.clone() {
572 div()
573 .key_context("EditMessageEditor")
574 .on_action(cx.listener(Self::cancel_editing_message))
575 .on_action(cx.listener(Self::confirm_editing_message))
576 .p_2p5()
577 .child(edit_message_editor)
578 } else {
579 div().p_2p5().text_ui(cx).child(markdown.clone())
580 },
581 )
582 .when_some(context, |parent, context| {
583 if !context.is_empty() {
584 parent.child(
585 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
586 context
587 .into_iter()
588 .map(|context| ContextPill::added(context, false, false, None)),
589 ),
590 )
591 } else {
592 parent
593 }
594 });
595
596 let styled_message = match message.role {
597 Role::User => v_flex()
598 .id(("message-container", ix))
599 .pt_2p5()
600 .px_2p5()
601 .child(
602 v_flex()
603 .bg(colors.editor_background)
604 .rounded_lg()
605 .border_1()
606 .border_color(colors.border)
607 .shadow_sm()
608 .child(
609 h_flex()
610 .py_1()
611 .pl_2()
612 .pr_1()
613 .bg(colors.editor_foreground.opacity(0.05))
614 .border_b_1()
615 .border_color(colors.border)
616 .justify_between()
617 .rounded_t(px(6.))
618 .child(
619 h_flex()
620 .gap_1p5()
621 .child(
622 Icon::new(IconName::PersonCircle)
623 .size(IconSize::XSmall)
624 .color(Color::Muted),
625 )
626 .child(
627 Label::new("You")
628 .size(LabelSize::Small)
629 .color(Color::Muted),
630 ),
631 )
632 .when_some(
633 edit_message_editor.clone(),
634 |this, edit_message_editor| {
635 let focus_handle = edit_message_editor.focus_handle(cx);
636 this.child(
637 h_flex()
638 .gap_1()
639 .child(
640 Button::new("cancel-edit-message", "Cancel")
641 .label_size(LabelSize::Small)
642 .key_binding(
643 KeyBinding::for_action_in(
644 &menu::Cancel,
645 &focus_handle,
646 window,
647 cx,
648 )
649 .map(|kb| kb.size(rems_from_px(12.))),
650 )
651 .on_click(
652 cx.listener(Self::handle_cancel_click),
653 ),
654 )
655 .child(
656 Button::new(
657 "confirm-edit-message",
658 "Regenerate",
659 )
660 .label_size(LabelSize::Small)
661 .key_binding(
662 KeyBinding::for_action_in(
663 &menu::Confirm,
664 &focus_handle,
665 window,
666 cx,
667 )
668 .map(|kb| kb.size(rems_from_px(12.))),
669 )
670 .on_click(
671 cx.listener(Self::handle_regenerate_click),
672 ),
673 ),
674 )
675 },
676 )
677 .when(
678 edit_message_editor.is_none() && allow_editing_message,
679 |this| {
680 this.child(
681 Button::new("edit-message", "Edit")
682 .label_size(LabelSize::Small)
683 .on_click(cx.listener({
684 let message_text = message.text.clone();
685 move |this, _, window, cx| {
686 this.start_editing_message(
687 message_id,
688 message_text.clone(),
689 window,
690 cx,
691 );
692 }
693 })),
694 )
695 },
696 ),
697 )
698 .child(message_content),
699 ),
700 Role::Assistant => {
701 v_flex()
702 .id(("message-container", ix))
703 .child(message_content)
704 .when(
705 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
706 |parent| {
707 parent.child(
708 v_flex()
709 .children(
710 tool_uses
711 .into_iter()
712 .map(|tool_use| self.render_tool_use(tool_use, cx)),
713 )
714 .children(scripting_tool_uses.into_iter().map(|tool_use| {
715 self.render_scripting_tool_use(tool_use, cx)
716 })),
717 )
718 },
719 )
720 }
721 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
722 v_flex()
723 .bg(colors.editor_background)
724 .rounded_sm()
725 .child(message_content),
726 ),
727 };
728
729 styled_message.into_any()
730 }
731
732 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
733 let is_open = self
734 .expanded_tool_uses
735 .get(&tool_use.id)
736 .copied()
737 .unwrap_or_default();
738
739 let lighter_border = cx.theme().colors().border.opacity(0.5);
740
741 div().px_2p5().child(
742 v_flex()
743 .rounded_lg()
744 .border_1()
745 .border_color(lighter_border)
746 .child(
747 h_flex()
748 .justify_between()
749 .py_1()
750 .pl_1()
751 .pr_2()
752 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
753 .map(|element| {
754 if is_open {
755 element.border_b_1().rounded_t_md()
756 } else {
757 element.rounded_md()
758 }
759 })
760 .border_color(lighter_border)
761 .child(
762 h_flex()
763 .gap_1()
764 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
765 cx.listener({
766 let tool_use_id = tool_use.id.clone();
767 move |this, _event, _window, _cx| {
768 let is_open = this
769 .expanded_tool_uses
770 .entry(tool_use_id.clone())
771 .or_insert(false);
772
773 *is_open = !*is_open;
774 }
775 }),
776 ))
777 .child(
778 Label::new(tool_use.name)
779 .size(LabelSize::Small)
780 .buffer_font(cx),
781 ),
782 )
783 .child({
784 let (icon_name, color, animated) = match &tool_use.status {
785 ToolUseStatus::Pending => {
786 (IconName::Warning, Color::Warning, false)
787 }
788 ToolUseStatus::Running => {
789 (IconName::ArrowCircle, Color::Accent, true)
790 }
791 ToolUseStatus::Finished(_) => {
792 (IconName::Check, Color::Success, false)
793 }
794 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
795 };
796
797 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
798
799 if animated {
800 icon.with_animation(
801 "arrow-circle",
802 Animation::new(Duration::from_secs(2)).repeat(),
803 |icon, delta| {
804 icon.transform(Transformation::rotate(percentage(delta)))
805 },
806 )
807 .into_any_element()
808 } else {
809 icon.into_any_element()
810 }
811 }),
812 )
813 .map(|parent| {
814 if !is_open {
815 return parent;
816 }
817
818 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
819
820 parent.child(
821 v_flex()
822 .gap_1()
823 .bg(cx.theme().colors().editor_background)
824 .rounded_b_lg()
825 .child(
826 content_container()
827 .border_b_1()
828 .border_color(lighter_border)
829 .child(
830 Label::new("Input")
831 .size(LabelSize::XSmall)
832 .color(Color::Muted)
833 .buffer_font(cx),
834 )
835 .child(
836 Label::new(
837 serde_json::to_string_pretty(&tool_use.input)
838 .unwrap_or_default(),
839 )
840 .size(LabelSize::Small)
841 .buffer_font(cx),
842 ),
843 )
844 .map(|container| match tool_use.status {
845 ToolUseStatus::Finished(output) => container.child(
846 content_container()
847 .child(
848 Label::new("Result")
849 .size(LabelSize::XSmall)
850 .color(Color::Muted)
851 .buffer_font(cx),
852 )
853 .child(
854 Label::new(output)
855 .size(LabelSize::Small)
856 .buffer_font(cx),
857 ),
858 ),
859 ToolUseStatus::Running => container.child(
860 content_container().child(
861 h_flex()
862 .gap_1()
863 .pb_1()
864 .child(
865 Icon::new(IconName::ArrowCircle)
866 .size(IconSize::Small)
867 .color(Color::Accent)
868 .with_animation(
869 "arrow-circle",
870 Animation::new(Duration::from_secs(2))
871 .repeat(),
872 |icon, delta| {
873 icon.transform(Transformation::rotate(
874 percentage(delta),
875 ))
876 },
877 ),
878 )
879 .child(
880 Label::new("Running…")
881 .size(LabelSize::XSmall)
882 .color(Color::Muted)
883 .buffer_font(cx),
884 ),
885 ),
886 ),
887 ToolUseStatus::Error(err) => container.child(
888 content_container()
889 .child(
890 Label::new("Error")
891 .size(LabelSize::XSmall)
892 .color(Color::Muted)
893 .buffer_font(cx),
894 )
895 .child(
896 Label::new(err).size(LabelSize::Small).buffer_font(cx),
897 ),
898 ),
899 ToolUseStatus::Pending => container,
900 }),
901 )
902 }),
903 )
904 }
905
906 fn render_scripting_tool_use(
907 &self,
908 tool_use: ToolUse,
909 cx: &mut Context<Self>,
910 ) -> impl IntoElement {
911 let is_open = self
912 .expanded_tool_uses
913 .get(&tool_use.id)
914 .copied()
915 .unwrap_or_default();
916
917 div().px_2p5().child(
918 v_flex()
919 .gap_1()
920 .rounded_lg()
921 .border_1()
922 .border_color(cx.theme().colors().border)
923 .child(
924 h_flex()
925 .justify_between()
926 .py_0p5()
927 .pl_1()
928 .pr_2()
929 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
930 .map(|element| {
931 if is_open {
932 element.border_b_1().rounded_t_md()
933 } else {
934 element.rounded_md()
935 }
936 })
937 .border_color(cx.theme().colors().border)
938 .child(
939 h_flex()
940 .gap_1()
941 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
942 cx.listener({
943 let tool_use_id = tool_use.id.clone();
944 move |this, _event, _window, _cx| {
945 let is_open = this
946 .expanded_tool_uses
947 .entry(tool_use_id.clone())
948 .or_insert(false);
949
950 *is_open = !*is_open;
951 }
952 }),
953 ))
954 .child(Label::new(tool_use.name)),
955 )
956 .child(
957 Label::new(match tool_use.status {
958 ToolUseStatus::Pending => "Pending",
959 ToolUseStatus::Running => "Running",
960 ToolUseStatus::Finished(_) => "Finished",
961 ToolUseStatus::Error(_) => "Error",
962 })
963 .size(LabelSize::XSmall)
964 .buffer_font(cx),
965 ),
966 )
967 .map(|parent| {
968 if !is_open {
969 return parent;
970 }
971
972 let lua_script_markdown =
973 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
974
975 parent.child(
976 v_flex()
977 .child(
978 v_flex()
979 .gap_0p5()
980 .py_1()
981 .px_2p5()
982 .border_b_1()
983 .border_color(cx.theme().colors().border)
984 .child(Label::new("Input:"))
985 .map(|parent| {
986 if let Some(markdown) = lua_script_markdown {
987 parent.child(markdown)
988 } else {
989 parent.child(Label::new(
990 "Failed to render script input to Markdown",
991 ))
992 }
993 }),
994 )
995 .map(|parent| match tool_use.status {
996 ToolUseStatus::Finished(output) => parent.child(
997 v_flex()
998 .gap_0p5()
999 .py_1()
1000 .px_2p5()
1001 .child(Label::new("Result:"))
1002 .child(Label::new(output)),
1003 ),
1004 ToolUseStatus::Error(err) => parent.child(
1005 v_flex()
1006 .gap_0p5()
1007 .py_1()
1008 .px_2p5()
1009 .child(Label::new("Error:"))
1010 .child(Label::new(err)),
1011 ),
1012 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1013 }),
1014 )
1015 }),
1016 )
1017 }
1018}
1019
1020impl Render for ActiveThread {
1021 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1022 v_flex()
1023 .size_full()
1024 .child(list(self.list_state.clone()).flex_grow())
1025 }
1026}