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