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 checkpoint = thread.checkpoint_for_message(message_id);
554 let context = thread.context_for_message(message_id);
555 let tool_uses = thread.tool_uses_for_message(message_id);
556 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id);
557
558 // Don't render user messages that are just there for returning tool results.
559 if message.role == Role::User
560 && (thread.message_has_tool_results(message_id)
561 || thread.message_has_scripting_tool_results(message_id))
562 {
563 return Empty.into_any();
564 }
565
566 let allow_editing_message =
567 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
568
569 let edit_message_editor = self
570 .editing_message
571 .as_ref()
572 .filter(|(id, _)| *id == message_id)
573 .map(|(_, state)| state.editor.clone());
574
575 let colors = cx.theme().colors();
576
577 let message_content = v_flex()
578 .child(
579 if let Some(edit_message_editor) = edit_message_editor.clone() {
580 div()
581 .key_context("EditMessageEditor")
582 .on_action(cx.listener(Self::cancel_editing_message))
583 .on_action(cx.listener(Self::confirm_editing_message))
584 .p_2p5()
585 .child(edit_message_editor)
586 } else {
587 div().text_ui(cx).child(markdown.clone())
588 },
589 )
590 .when_some(context, |parent, context| {
591 if !context.is_empty() {
592 parent.child(
593 h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
594 context
595 .into_iter()
596 .map(|context| ContextPill::added(context, false, false, None)),
597 ),
598 )
599 } else {
600 parent
601 }
602 });
603
604 let styled_message = match message.role {
605 Role::User => v_flex()
606 .id(("message-container", ix))
607 .pt_2()
608 .pl_2()
609 .pr_2p5()
610 .child(
611 v_flex()
612 .bg(colors.editor_background)
613 .rounded_lg()
614 .border_1()
615 .border_color(colors.border)
616 .shadow_md()
617 .child(
618 h_flex()
619 .py_1()
620 .pl_2()
621 .pr_1()
622 .bg(colors.editor_foreground.opacity(0.05))
623 .border_b_1()
624 .border_color(colors.border)
625 .justify_between()
626 .rounded_t(px(6.))
627 .child(
628 h_flex()
629 .gap_1p5()
630 .child(
631 Icon::new(IconName::PersonCircle)
632 .size(IconSize::XSmall)
633 .color(Color::Muted),
634 )
635 .child(
636 Label::new("You")
637 .size(LabelSize::Small)
638 .color(Color::Muted),
639 ),
640 )
641 .when_some(
642 edit_message_editor.clone(),
643 |this, edit_message_editor| {
644 let focus_handle = edit_message_editor.focus_handle(cx);
645 this.child(
646 h_flex()
647 .gap_1()
648 .child(
649 Button::new("cancel-edit-message", "Cancel")
650 .label_size(LabelSize::Small)
651 .key_binding(
652 KeyBinding::for_action_in(
653 &menu::Cancel,
654 &focus_handle,
655 window,
656 cx,
657 )
658 .map(|kb| kb.size(rems_from_px(12.))),
659 )
660 .on_click(
661 cx.listener(Self::handle_cancel_click),
662 ),
663 )
664 .child(
665 Button::new(
666 "confirm-edit-message",
667 "Regenerate",
668 )
669 .label_size(LabelSize::Small)
670 .key_binding(
671 KeyBinding::for_action_in(
672 &menu::Confirm,
673 &focus_handle,
674 window,
675 cx,
676 )
677 .map(|kb| kb.size(rems_from_px(12.))),
678 )
679 .on_click(
680 cx.listener(Self::handle_regenerate_click),
681 ),
682 ),
683 )
684 },
685 )
686 .when(
687 edit_message_editor.is_none() && allow_editing_message,
688 |this| {
689 this.child(
690 Button::new("edit-message", "Edit")
691 .label_size(LabelSize::Small)
692 .on_click(cx.listener({
693 let message_text = message.text.clone();
694 move |this, _, window, cx| {
695 this.start_editing_message(
696 message_id,
697 message_text.clone(),
698 window,
699 cx,
700 );
701 }
702 })),
703 )
704 },
705 ),
706 )
707 .child(div().p_2().child(message_content)),
708 ),
709 Role::Assistant => {
710 v_flex()
711 .id(("message-container", ix))
712 .child(div().py_3().px_4().child(message_content))
713 .when(
714 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
715 |parent| {
716 parent.child(
717 v_flex()
718 .children(
719 tool_uses
720 .into_iter()
721 .map(|tool_use| self.render_tool_use(tool_use, cx)),
722 )
723 .children(scripting_tool_uses.into_iter().map(|tool_use| {
724 self.render_scripting_tool_use(tool_use, cx)
725 })),
726 )
727 },
728 )
729 }
730 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
731 v_flex()
732 .bg(colors.editor_background)
733 .rounded_sm()
734 .child(div().p_4().child(message_content)),
735 ),
736 };
737
738 v_flex()
739 .when_some(checkpoint, |parent, checkpoint| {
740 parent.child(
741 h_flex().pl_2().child(
742 Button::new("restore-checkpoint", "Restore Checkpoint")
743 .icon(IconName::Undo)
744 .size(ButtonSize::Compact)
745 .on_click(cx.listener(move |this, _, _window, cx| {
746 this.thread.update(cx, |thread, cx| {
747 thread
748 .restore_checkpoint(checkpoint.clone(), cx)
749 .detach_and_log_err(cx);
750 });
751 })),
752 ),
753 )
754 })
755 .child(styled_message)
756 .into_any()
757 }
758
759 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
760 let is_open = self
761 .expanded_tool_uses
762 .get(&tool_use.id)
763 .copied()
764 .unwrap_or_default();
765
766 let lighter_border = cx.theme().colors().border.opacity(0.5);
767
768 div().px_4().child(
769 v_flex()
770 .rounded_lg()
771 .border_1()
772 .border_color(lighter_border)
773 .child(
774 h_flex()
775 .justify_between()
776 .py_1()
777 .pl_1()
778 .pr_2()
779 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
780 .map(|element| {
781 if is_open {
782 element.border_b_1().rounded_t_md()
783 } else {
784 element.rounded_md()
785 }
786 })
787 .border_color(lighter_border)
788 .child(
789 h_flex()
790 .gap_1()
791 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
792 cx.listener({
793 let tool_use_id = tool_use.id.clone();
794 move |this, _event, _window, _cx| {
795 let is_open = this
796 .expanded_tool_uses
797 .entry(tool_use_id.clone())
798 .or_insert(false);
799
800 *is_open = !*is_open;
801 }
802 }),
803 ))
804 .child(
805 Label::new(tool_use.name)
806 .size(LabelSize::Small)
807 .buffer_font(cx),
808 ),
809 )
810 .child({
811 let (icon_name, color, animated) = match &tool_use.status {
812 ToolUseStatus::Pending => {
813 (IconName::Warning, Color::Warning, false)
814 }
815 ToolUseStatus::Running => {
816 (IconName::ArrowCircle, Color::Accent, true)
817 }
818 ToolUseStatus::Finished(_) => {
819 (IconName::Check, Color::Success, false)
820 }
821 ToolUseStatus::Error(_) => (IconName::Close, Color::Error, false),
822 };
823
824 let icon = Icon::new(icon_name).color(color).size(IconSize::Small);
825
826 if animated {
827 icon.with_animation(
828 "arrow-circle",
829 Animation::new(Duration::from_secs(2)).repeat(),
830 |icon, delta| {
831 icon.transform(Transformation::rotate(percentage(delta)))
832 },
833 )
834 .into_any_element()
835 } else {
836 icon.into_any_element()
837 }
838 }),
839 )
840 .map(|parent| {
841 if !is_open {
842 return parent;
843 }
844
845 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
846
847 parent.child(
848 v_flex()
849 .gap_1()
850 .bg(cx.theme().colors().editor_background)
851 .rounded_b_lg()
852 .child(
853 content_container()
854 .border_b_1()
855 .border_color(lighter_border)
856 .child(
857 Label::new("Input")
858 .size(LabelSize::XSmall)
859 .color(Color::Muted)
860 .buffer_font(cx),
861 )
862 .child(
863 Label::new(
864 serde_json::to_string_pretty(&tool_use.input)
865 .unwrap_or_default(),
866 )
867 .size(LabelSize::Small)
868 .buffer_font(cx),
869 ),
870 )
871 .map(|container| match tool_use.status {
872 ToolUseStatus::Finished(output) => container.child(
873 content_container()
874 .child(
875 Label::new("Result")
876 .size(LabelSize::XSmall)
877 .color(Color::Muted)
878 .buffer_font(cx),
879 )
880 .child(
881 Label::new(output)
882 .size(LabelSize::Small)
883 .buffer_font(cx),
884 ),
885 ),
886 ToolUseStatus::Running => container.child(
887 content_container().child(
888 h_flex()
889 .gap_1()
890 .pb_1()
891 .child(
892 Icon::new(IconName::ArrowCircle)
893 .size(IconSize::Small)
894 .color(Color::Accent)
895 .with_animation(
896 "arrow-circle",
897 Animation::new(Duration::from_secs(2))
898 .repeat(),
899 |icon, delta| {
900 icon.transform(Transformation::rotate(
901 percentage(delta),
902 ))
903 },
904 ),
905 )
906 .child(
907 Label::new("Running…")
908 .size(LabelSize::XSmall)
909 .color(Color::Muted)
910 .buffer_font(cx),
911 ),
912 ),
913 ),
914 ToolUseStatus::Error(err) => container.child(
915 content_container()
916 .child(
917 Label::new("Error")
918 .size(LabelSize::XSmall)
919 .color(Color::Muted)
920 .buffer_font(cx),
921 )
922 .child(
923 Label::new(err).size(LabelSize::Small).buffer_font(cx),
924 ),
925 ),
926 ToolUseStatus::Pending => container,
927 }),
928 )
929 }),
930 )
931 }
932
933 fn render_scripting_tool_use(
934 &self,
935 tool_use: ToolUse,
936 cx: &mut Context<Self>,
937 ) -> impl IntoElement {
938 let is_open = self
939 .expanded_tool_uses
940 .get(&tool_use.id)
941 .copied()
942 .unwrap_or_default();
943
944 div().px_2p5().child(
945 v_flex()
946 .gap_1()
947 .rounded_lg()
948 .border_1()
949 .border_color(cx.theme().colors().border)
950 .child(
951 h_flex()
952 .justify_between()
953 .py_0p5()
954 .pl_1()
955 .pr_2()
956 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
957 .map(|element| {
958 if is_open {
959 element.border_b_1().rounded_t_md()
960 } else {
961 element.rounded_md()
962 }
963 })
964 .border_color(cx.theme().colors().border)
965 .child(
966 h_flex()
967 .gap_1()
968 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
969 cx.listener({
970 let tool_use_id = tool_use.id.clone();
971 move |this, _event, _window, _cx| {
972 let is_open = this
973 .expanded_tool_uses
974 .entry(tool_use_id.clone())
975 .or_insert(false);
976
977 *is_open = !*is_open;
978 }
979 }),
980 ))
981 .child(Label::new(tool_use.name)),
982 )
983 .child(
984 Label::new(match tool_use.status {
985 ToolUseStatus::Pending => "Pending",
986 ToolUseStatus::Running => "Running",
987 ToolUseStatus::Finished(_) => "Finished",
988 ToolUseStatus::Error(_) => "Error",
989 })
990 .size(LabelSize::XSmall)
991 .buffer_font(cx),
992 ),
993 )
994 .map(|parent| {
995 if !is_open {
996 return parent;
997 }
998
999 let lua_script_markdown =
1000 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1001
1002 parent.child(
1003 v_flex()
1004 .child(
1005 v_flex()
1006 .gap_0p5()
1007 .py_1()
1008 .px_2p5()
1009 .border_b_1()
1010 .border_color(cx.theme().colors().border)
1011 .child(Label::new("Input:"))
1012 .map(|parent| {
1013 if let Some(markdown) = lua_script_markdown {
1014 parent.child(markdown)
1015 } else {
1016 parent.child(Label::new(
1017 "Failed to render script input to Markdown",
1018 ))
1019 }
1020 }),
1021 )
1022 .map(|parent| match tool_use.status {
1023 ToolUseStatus::Finished(output) => parent.child(
1024 v_flex()
1025 .gap_0p5()
1026 .py_1()
1027 .px_2p5()
1028 .child(Label::new("Result:"))
1029 .child(Label::new(output)),
1030 ),
1031 ToolUseStatus::Error(err) => parent.child(
1032 v_flex()
1033 .gap_0p5()
1034 .py_1()
1035 .px_2p5()
1036 .child(Label::new("Error:"))
1037 .child(Label::new(err)),
1038 ),
1039 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1040 }),
1041 )
1042 }),
1043 )
1044 }
1045}
1046
1047impl Render for ActiveThread {
1048 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
1049 v_flex()
1050 .size_full()
1051 .child(list(self.list_state.clone()).flex_grow())
1052 }
1053}