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