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