1use crate::thread::{
2 LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,
3 ThreadEvent, ThreadFeedback,
4};
5use crate::thread_store::ThreadStore;
6use crate::tool_use::{PendingToolUseStatus, ToolType, ToolUse, ToolUseStatus};
7use crate::ui::ContextPill;
8use collections::HashMap;
9use editor::{Editor, MultiBuffer};
10use gpui::{
11 linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
12 Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
13 Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
14 Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
15};
16use language::{Buffer, LanguageRegistry};
17use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
18use markdown::{Markdown, MarkdownStyle};
19use scripting_tool::{ScriptingTool, ScriptingToolInput};
20use settings::Settings as _;
21use std::sync::Arc;
22use std::time::Duration;
23use theme::ThemeSettings;
24use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
25use util::ResultExt as _;
26use workspace::{OpenOptions, Workspace};
27
28use crate::context_store::{refresh_context_store_text, ContextStore};
29
30pub struct ActiveThread {
31 language_registry: Arc<LanguageRegistry>,
32 thread_store: Entity<ThreadStore>,
33 thread: Entity<Thread>,
34 context_store: Entity<ContextStore>,
35 workspace: WeakEntity<Workspace>,
36 save_thread_task: Option<Task<()>>,
37 messages: Vec<MessageId>,
38 list_state: ListState,
39 rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
40 rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
41 rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
42 editing_message: Option<(MessageId, EditMessageState)>,
43 expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
44 expanded_thinking_segments: HashMap<(MessageId, usize), bool>,
45 last_error: Option<ThreadError>,
46 _subscriptions: Vec<Subscription>,
47}
48
49struct RenderedMessage {
50 language_registry: Arc<LanguageRegistry>,
51 segments: Vec<RenderedMessageSegment>,
52}
53
54impl RenderedMessage {
55 fn from_segments(
56 segments: &[MessageSegment],
57 language_registry: Arc<LanguageRegistry>,
58 window: &Window,
59 cx: &mut App,
60 ) -> Self {
61 let mut this = Self {
62 language_registry,
63 segments: Vec::with_capacity(segments.len()),
64 };
65 for segment in segments {
66 this.push_segment(segment, window, cx);
67 }
68 this
69 }
70
71 fn append_thinking(&mut self, text: &String, window: &Window, cx: &mut App) {
72 if let Some(RenderedMessageSegment::Thinking {
73 content,
74 scroll_handle,
75 }) = self.segments.last_mut()
76 {
77 content.update(cx, |markdown, cx| {
78 markdown.append(text, cx);
79 });
80 scroll_handle.scroll_to_bottom();
81 } else {
82 self.segments.push(RenderedMessageSegment::Thinking {
83 content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
84 scroll_handle: ScrollHandle::default(),
85 });
86 }
87 }
88
89 fn append_text(&mut self, text: &String, window: &Window, cx: &mut App) {
90 if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {
91 markdown.update(cx, |markdown, cx| markdown.append(text, cx));
92 } else {
93 self.segments
94 .push(RenderedMessageSegment::Text(render_markdown(
95 SharedString::from(text),
96 self.language_registry.clone(),
97 window,
98 cx,
99 )));
100 }
101 }
102
103 fn push_segment(&mut self, segment: &MessageSegment, window: &Window, cx: &mut App) {
104 let rendered_segment = match segment {
105 MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {
106 content: render_markdown(text.into(), self.language_registry.clone(), window, cx),
107 scroll_handle: ScrollHandle::default(),
108 },
109 MessageSegment::Text(text) => RenderedMessageSegment::Text(render_markdown(
110 text.into(),
111 self.language_registry.clone(),
112 window,
113 cx,
114 )),
115 };
116 self.segments.push(rendered_segment);
117 }
118}
119
120enum RenderedMessageSegment {
121 Thinking {
122 content: Entity<Markdown>,
123 scroll_handle: ScrollHandle,
124 },
125 Text(Entity<Markdown>),
126}
127
128fn render_markdown(
129 text: SharedString,
130 language_registry: Arc<LanguageRegistry>,
131 window: &Window,
132 cx: &mut App,
133) -> Entity<Markdown> {
134 let theme_settings = ThemeSettings::get_global(cx);
135 let colors = cx.theme().colors();
136 let ui_font_size = TextSize::Default.rems(cx);
137 let buffer_font_size = TextSize::Small.rems(cx);
138 let mut text_style = window.text_style();
139
140 text_style.refine(&TextStyleRefinement {
141 font_family: Some(theme_settings.ui_font.family.clone()),
142 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
143 font_features: Some(theme_settings.ui_font.features.clone()),
144 font_size: Some(ui_font_size.into()),
145 color: Some(cx.theme().colors().text),
146 ..Default::default()
147 });
148
149 let markdown_style = MarkdownStyle {
150 base_text_style: text_style,
151 syntax: cx.theme().syntax().clone(),
152 selection_background_color: cx.theme().players().local().selection,
153 code_block_overflow_x_scroll: true,
154 table_overflow_x_scroll: true,
155 code_block: StyleRefinement {
156 margin: EdgesRefinement {
157 top: Some(Length::Definite(rems(0.).into())),
158 left: Some(Length::Definite(rems(0.).into())),
159 right: Some(Length::Definite(rems(0.).into())),
160 bottom: Some(Length::Definite(rems(0.5).into())),
161 },
162 padding: EdgesRefinement {
163 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
164 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
165 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
166 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
167 },
168 background: Some(colors.editor_background.into()),
169 border_color: Some(colors.border_variant),
170 border_widths: EdgesRefinement {
171 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
172 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
173 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
174 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
175 },
176 text: Some(TextStyleRefinement {
177 font_family: Some(theme_settings.buffer_font.family.clone()),
178 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
179 font_features: Some(theme_settings.buffer_font.features.clone()),
180 font_size: Some(buffer_font_size.into()),
181 ..Default::default()
182 }),
183 ..Default::default()
184 },
185 inline_code: TextStyleRefinement {
186 font_family: Some(theme_settings.buffer_font.family.clone()),
187 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
188 font_features: Some(theme_settings.buffer_font.features.clone()),
189 font_size: Some(buffer_font_size.into()),
190 background_color: Some(colors.editor_foreground.opacity(0.1)),
191 ..Default::default()
192 },
193 link: TextStyleRefinement {
194 background_color: Some(colors.editor_foreground.opacity(0.025)),
195 underline: Some(UnderlineStyle {
196 color: Some(colors.text_accent.opacity(0.5)),
197 thickness: px(1.),
198 ..Default::default()
199 }),
200 ..Default::default()
201 },
202 ..Default::default()
203 };
204
205 cx.new(|cx| Markdown::new(text, markdown_style, Some(language_registry), None, cx))
206}
207
208struct EditMessageState {
209 editor: Entity<Editor>,
210}
211
212impl ActiveThread {
213 pub fn new(
214 thread: Entity<Thread>,
215 thread_store: Entity<ThreadStore>,
216 language_registry: Arc<LanguageRegistry>,
217 context_store: Entity<ContextStore>,
218 workspace: WeakEntity<Workspace>,
219 window: &mut Window,
220 cx: &mut Context<Self>,
221 ) -> Self {
222 let subscriptions = vec![
223 cx.observe(&thread, |_, _, cx| cx.notify()),
224 cx.subscribe_in(&thread, window, Self::handle_thread_event),
225 ];
226
227 let mut this = Self {
228 language_registry,
229 thread_store,
230 thread: thread.clone(),
231 context_store,
232 workspace,
233 save_thread_task: None,
234 messages: Vec::new(),
235 rendered_messages_by_id: HashMap::default(),
236 rendered_scripting_tool_uses: HashMap::default(),
237 rendered_tool_use_labels: HashMap::default(),
238 expanded_tool_uses: HashMap::default(),
239 expanded_thinking_segments: HashMap::default(),
240 list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
241 let this = cx.entity().downgrade();
242 move |ix, window: &mut Window, cx: &mut App| {
243 this.update(cx, |this, cx| this.render_message(ix, window, cx))
244 .unwrap()
245 }
246 }),
247 editing_message: None,
248 last_error: None,
249 _subscriptions: subscriptions,
250 };
251
252 for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
253 this.push_message(&message.id, &message.segments, window, cx);
254
255 for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
256 this.render_tool_use_label_markdown(
257 tool_use.id.clone(),
258 tool_use.ui_text.clone(),
259 window,
260 cx,
261 );
262 }
263
264 for tool_use in thread
265 .read(cx)
266 .scripting_tool_uses_for_message(message.id, cx)
267 {
268 this.render_tool_use_label_markdown(
269 tool_use.id.clone(),
270 tool_use.ui_text.clone(),
271 window,
272 cx,
273 );
274
275 this.render_scripting_tool_use_markdown(
276 tool_use.id.clone(),
277 tool_use.ui_text.as_ref(),
278 tool_use.input.clone(),
279 window,
280 cx,
281 );
282 }
283 }
284
285 this
286 }
287
288 pub fn thread(&self) -> &Entity<Thread> {
289 &self.thread
290 }
291
292 pub fn is_empty(&self) -> bool {
293 self.messages.is_empty()
294 }
295
296 pub fn summary(&self, cx: &App) -> Option<SharedString> {
297 self.thread.read(cx).summary()
298 }
299
300 pub fn summary_or_default(&self, cx: &App) -> SharedString {
301 self.thread.read(cx).summary_or_default()
302 }
303
304 pub fn cancel_last_completion(&mut self, cx: &mut App) -> bool {
305 self.last_error.take();
306 self.thread
307 .update(cx, |thread, cx| thread.cancel_last_completion(cx))
308 }
309
310 pub fn last_error(&self) -> Option<ThreadError> {
311 self.last_error.clone()
312 }
313
314 pub fn clear_last_error(&mut self) {
315 self.last_error.take();
316 }
317
318 fn push_message(
319 &mut self,
320 id: &MessageId,
321 segments: &[MessageSegment],
322 window: &mut Window,
323 cx: &mut Context<Self>,
324 ) {
325 let old_len = self.messages.len();
326 self.messages.push(*id);
327 self.list_state.splice(old_len..old_len, 1);
328
329 let rendered_message =
330 RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
331 self.rendered_messages_by_id.insert(*id, rendered_message);
332 self.list_state.scroll_to(ListOffset {
333 item_ix: old_len,
334 offset_in_item: Pixels(0.0),
335 });
336 }
337
338 fn edited_message(
339 &mut self,
340 id: &MessageId,
341 segments: &[MessageSegment],
342 window: &mut Window,
343 cx: &mut Context<Self>,
344 ) {
345 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
346 return;
347 };
348 self.list_state.splice(index..index + 1, 1);
349 let rendered_message =
350 RenderedMessage::from_segments(segments, self.language_registry.clone(), window, cx);
351 self.rendered_messages_by_id.insert(*id, rendered_message);
352 }
353
354 fn deleted_message(&mut self, id: &MessageId) {
355 let Some(index) = self.messages.iter().position(|message_id| message_id == id) else {
356 return;
357 };
358 self.messages.remove(index);
359 self.list_state.splice(index..index + 1, 0);
360 self.rendered_messages_by_id.remove(id);
361 }
362
363 /// Renders the input of a scripting tool use to Markdown.
364 ///
365 /// Does nothing if the tool use does not correspond to the scripting tool.
366 fn render_scripting_tool_use_markdown(
367 &mut self,
368 tool_use_id: LanguageModelToolUseId,
369 tool_name: &str,
370 tool_input: serde_json::Value,
371 window: &mut Window,
372 cx: &mut Context<Self>,
373 ) {
374 if tool_name != ScriptingTool::NAME {
375 return;
376 }
377
378 let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
379 .map(|input| input.lua_script)
380 .unwrap_or_default();
381
382 let lua_script = render_markdown(
383 format!("```lua\n{lua_script}\n```").into(),
384 self.language_registry.clone(),
385 window,
386 cx,
387 );
388
389 self.rendered_scripting_tool_uses
390 .insert(tool_use_id, lua_script);
391 }
392
393 fn render_tool_use_label_markdown(
394 &mut self,
395 tool_use_id: LanguageModelToolUseId,
396 tool_label: impl Into<SharedString>,
397 window: &mut Window,
398 cx: &mut Context<Self>,
399 ) {
400 self.rendered_tool_use_labels.insert(
401 tool_use_id,
402 render_markdown(
403 tool_label.into(),
404 self.language_registry.clone(),
405 window,
406 cx,
407 ),
408 );
409 }
410
411 fn handle_thread_event(
412 &mut self,
413 _thread: &Entity<Thread>,
414 event: &ThreadEvent,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) {
418 match event {
419 ThreadEvent::ShowError(error) => {
420 self.last_error = Some(error.clone());
421 }
422 ThreadEvent::StreamedCompletion | ThreadEvent::SummaryChanged => {
423 self.save_thread(cx);
424 }
425 ThreadEvent::DoneStreaming => {}
426 ThreadEvent::StreamedAssistantText(message_id, text) => {
427 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
428 rendered_message.append_text(text, window, cx);
429 }
430 }
431 ThreadEvent::StreamedAssistantThinking(message_id, text) => {
432 if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
433 rendered_message.append_thinking(text, window, cx);
434 }
435 }
436 ThreadEvent::MessageAdded(message_id) => {
437 if let Some(message_segments) = self
438 .thread
439 .read(cx)
440 .message(*message_id)
441 .map(|message| message.segments.clone())
442 {
443 self.push_message(message_id, &message_segments, window, cx);
444 }
445
446 self.save_thread(cx);
447 cx.notify();
448 }
449 ThreadEvent::MessageEdited(message_id) => {
450 if let Some(message_segments) = self
451 .thread
452 .read(cx)
453 .message(*message_id)
454 .map(|message| message.segments.clone())
455 {
456 self.edited_message(message_id, &message_segments, window, cx);
457 }
458
459 self.save_thread(cx);
460 cx.notify();
461 }
462 ThreadEvent::MessageDeleted(message_id) => {
463 self.deleted_message(message_id);
464 self.save_thread(cx);
465 cx.notify();
466 }
467 ThreadEvent::UsePendingTools => {
468 let tool_uses = self
469 .thread
470 .update(cx, |thread, cx| thread.use_pending_tools(cx));
471
472 for tool_use in tool_uses {
473 self.render_tool_use_label_markdown(
474 tool_use.id.clone(),
475 tool_use.ui_text.clone(),
476 window,
477 cx,
478 );
479 self.render_scripting_tool_use_markdown(
480 tool_use.id,
481 tool_use.name.as_ref(),
482 tool_use.input.clone(),
483 window,
484 cx,
485 );
486 }
487 }
488 ThreadEvent::ToolFinished {
489 pending_tool_use,
490 canceled,
491 ..
492 } => {
493 let canceled = *canceled;
494 if let Some(tool_use) = pending_tool_use {
495 self.render_tool_use_label_markdown(
496 tool_use.id.clone(),
497 SharedString::from(tool_use.ui_text.clone()),
498 window,
499 cx,
500 );
501 }
502
503 if self.thread.read(cx).all_tools_finished() {
504 let pending_refresh_buffers = self.thread.update(cx, |thread, cx| {
505 thread.action_log().update(cx, |action_log, _cx| {
506 action_log.take_stale_buffers_in_context()
507 })
508 });
509
510 let context_update_task = if !pending_refresh_buffers.is_empty() {
511 let refresh_task = refresh_context_store_text(
512 self.context_store.clone(),
513 &pending_refresh_buffers,
514 cx,
515 );
516
517 cx.spawn(async move |this, cx| {
518 let updated_context_ids = refresh_task.await;
519
520 this.update(cx, |this, cx| {
521 this.context_store.read_with(cx, |context_store, cx| {
522 context_store
523 .context()
524 .iter()
525 .filter(|context| {
526 updated_context_ids.contains(&context.id())
527 })
528 .flat_map(|context| context.snapshot(cx))
529 .collect()
530 })
531 })
532 })
533 } else {
534 Task::ready(anyhow::Ok(Vec::new()))
535 };
536
537 let model_registry = LanguageModelRegistry::read_global(cx);
538 if let Some(model) = model_registry.active_model() {
539 cx.spawn(async move |this, cx| {
540 let updated_context = context_update_task.await?;
541
542 this.update(cx, |this, cx| {
543 this.thread.update(cx, |thread, cx| {
544 thread.attach_tool_results(updated_context, cx);
545 if !canceled {
546 thread.send_to_model(model, RequestKind::Chat, cx);
547 }
548 });
549 })
550 })
551 .detach();
552 }
553 }
554 }
555 ThreadEvent::CheckpointChanged => cx.notify(),
556 }
557 }
558
559 /// Spawns a task to save the active thread.
560 ///
561 /// Only one task to save the thread will be in flight at a time.
562 fn save_thread(&mut self, cx: &mut Context<Self>) {
563 let thread = self.thread.clone();
564 self.save_thread_task = Some(cx.spawn(async move |this, cx| {
565 let task = this
566 .update(cx, |this, cx| {
567 this.thread_store
568 .update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
569 })
570 .ok();
571
572 if let Some(task) = task {
573 task.await.log_err();
574 }
575 }));
576 }
577
578 fn start_editing_message(
579 &mut self,
580 message_id: MessageId,
581 message_segments: &[MessageSegment],
582 window: &mut Window,
583 cx: &mut Context<Self>,
584 ) {
585 // User message should always consist of a single text segment,
586 // therefore we can skip returning early if it's not a text segment.
587 let Some(MessageSegment::Text(message_text)) = message_segments.first() else {
588 return;
589 };
590
591 let buffer = cx.new(|cx| {
592 MultiBuffer::singleton(cx.new(|cx| Buffer::local(message_text.clone(), cx)), cx)
593 });
594 let editor = cx.new(|cx| {
595 let mut editor = Editor::new(
596 editor::EditorMode::AutoHeight { max_lines: 8 },
597 buffer,
598 None,
599 window,
600 cx,
601 );
602 editor.focus_handle(cx).focus(window);
603 editor.move_to_end(&editor::actions::MoveToEnd, window, cx);
604 editor
605 });
606 self.editing_message = Some((
607 message_id,
608 EditMessageState {
609 editor: editor.clone(),
610 },
611 ));
612 cx.notify();
613 }
614
615 fn cancel_editing_message(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
616 self.editing_message.take();
617 cx.notify();
618 }
619
620 fn confirm_editing_message(
621 &mut self,
622 _: &menu::Confirm,
623 _: &mut Window,
624 cx: &mut Context<Self>,
625 ) {
626 let Some((message_id, state)) = self.editing_message.take() else {
627 return;
628 };
629 let edited_text = state.editor.read(cx).text(cx);
630 self.thread.update(cx, |thread, cx| {
631 thread.edit_message(
632 message_id,
633 Role::User,
634 vec![MessageSegment::Text(edited_text)],
635 cx,
636 );
637 for message_id in self.messages_after(message_id) {
638 thread.delete_message(*message_id, cx);
639 }
640 });
641
642 let provider = LanguageModelRegistry::read_global(cx).active_provider();
643 if provider
644 .as_ref()
645 .map_or(false, |provider| provider.must_accept_terms(cx))
646 {
647 cx.notify();
648 return;
649 }
650 let model_registry = LanguageModelRegistry::read_global(cx);
651 let Some(model) = model_registry.active_model() else {
652 return;
653 };
654
655 self.thread.update(cx, |thread, cx| {
656 thread.send_to_model(model, RequestKind::Chat, cx)
657 });
658 cx.notify();
659 }
660
661 fn last_user_message(&self, cx: &Context<Self>) -> Option<MessageId> {
662 self.messages
663 .iter()
664 .rev()
665 .find(|message_id| {
666 self.thread
667 .read(cx)
668 .message(**message_id)
669 .map_or(false, |message| message.role == Role::User)
670 })
671 .cloned()
672 }
673
674 fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
675 self.messages
676 .iter()
677 .position(|id| *id == message_id)
678 .map(|index| &self.messages[index + 1..])
679 .unwrap_or(&[])
680 }
681
682 fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
683 self.cancel_editing_message(&menu::Cancel, window, cx);
684 }
685
686 fn handle_regenerate_click(
687 &mut self,
688 _: &ClickEvent,
689 window: &mut Window,
690 cx: &mut Context<Self>,
691 ) {
692 self.confirm_editing_message(&menu::Confirm, window, cx);
693 }
694
695 fn handle_feedback_click(
696 &mut self,
697 feedback: ThreadFeedback,
698 _window: &mut Window,
699 cx: &mut Context<Self>,
700 ) {
701 let report = self
702 .thread
703 .update(cx, |thread, cx| thread.report_feedback(feedback, cx));
704
705 let this = cx.entity().downgrade();
706 cx.spawn(async move |_, cx| {
707 report.await?;
708 this.update(cx, |_this, cx| cx.notify())
709 })
710 .detach_and_log_err(cx);
711 }
712
713 fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
714 let message_id = self.messages[ix];
715 let Some(message) = self.thread.read(cx).message(message_id) else {
716 return Empty.into_any();
717 };
718
719 let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
720 return Empty.into_any();
721 };
722
723 let thread = self.thread.read(cx);
724 // Get all the data we need from thread before we start using it in closures
725 let checkpoint = thread.checkpoint_for_message(message_id);
726 let context = thread.context_for_message(message_id);
727 let tool_uses = thread.tool_uses_for_message(message_id, cx);
728 let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
729
730 // Don't render user messages that are just there for returning tool results.
731 if message.role == Role::User
732 && (thread.message_has_tool_results(message_id)
733 || thread.message_has_scripting_tool_results(message_id))
734 {
735 return Empty.into_any();
736 }
737
738 let allow_editing_message =
739 message.role == Role::User && self.last_user_message(cx) == Some(message_id);
740
741 let edit_message_editor = self
742 .editing_message
743 .as_ref()
744 .filter(|(id, _)| *id == message_id)
745 .map(|(_, state)| state.editor.clone());
746
747 let first_message = ix == 0;
748 let is_last_message = ix == self.messages.len() - 1;
749
750 let colors = cx.theme().colors();
751 let active_color = colors.element_active;
752 let editor_bg_color = colors.editor_background;
753 let bg_user_message_header = editor_bg_color.blend(active_color.opacity(0.25));
754
755 let feedback_container = h_flex().pt_2().pb_4().px_4().gap_1().justify_between();
756 let feedback_items = match self.thread.read(cx).feedback() {
757 Some(feedback) => feedback_container
758 .child(
759 Label::new(match feedback {
760 ThreadFeedback::Positive => "Thanks for your feedback!",
761 ThreadFeedback::Negative => {
762 "We appreciate your feedback and will use it to improve."
763 }
764 })
765 .color(Color::Muted)
766 .size(LabelSize::XSmall),
767 )
768 .child(
769 h_flex()
770 .gap_1()
771 .child(
772 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
773 .icon_size(IconSize::XSmall)
774 .icon_color(match feedback {
775 ThreadFeedback::Positive => Color::Accent,
776 ThreadFeedback::Negative => Color::Ignored,
777 })
778 .shape(ui::IconButtonShape::Square)
779 .tooltip(Tooltip::text("Helpful Response"))
780 .on_click(cx.listener(move |this, _, window, cx| {
781 this.handle_feedback_click(
782 ThreadFeedback::Positive,
783 window,
784 cx,
785 );
786 })),
787 )
788 .child(
789 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
790 .icon_size(IconSize::XSmall)
791 .icon_color(match feedback {
792 ThreadFeedback::Positive => Color::Ignored,
793 ThreadFeedback::Negative => Color::Accent,
794 })
795 .shape(ui::IconButtonShape::Square)
796 .tooltip(Tooltip::text("Not Helpful"))
797 .on_click(cx.listener(move |this, _, window, cx| {
798 this.handle_feedback_click(
799 ThreadFeedback::Negative,
800 window,
801 cx,
802 );
803 })),
804 ),
805 )
806 .into_any_element(),
807 None => feedback_container
808 .child(
809 Label::new(
810 "Rating the thread sends all of your current conversation to the Zed team.",
811 )
812 .color(Color::Muted)
813 .size(LabelSize::XSmall),
814 )
815 .child(
816 h_flex()
817 .gap_1()
818 .child(
819 IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
820 .icon_size(IconSize::XSmall)
821 .icon_color(Color::Ignored)
822 .shape(ui::IconButtonShape::Square)
823 .tooltip(Tooltip::text("Helpful Response"))
824 .on_click(cx.listener(move |this, _, window, cx| {
825 this.handle_feedback_click(
826 ThreadFeedback::Positive,
827 window,
828 cx,
829 );
830 })),
831 )
832 .child(
833 IconButton::new("feedback-thumbs-down", IconName::ThumbsDown)
834 .icon_size(IconSize::XSmall)
835 .icon_color(Color::Ignored)
836 .shape(ui::IconButtonShape::Square)
837 .tooltip(Tooltip::text("Not Helpful"))
838 .on_click(cx.listener(move |this, _, window, cx| {
839 this.handle_feedback_click(
840 ThreadFeedback::Negative,
841 window,
842 cx,
843 );
844 })),
845 ),
846 )
847 .into_any_element(),
848 };
849
850 let message_content = v_flex()
851 .gap_1p5()
852 .child(
853 if let Some(edit_message_editor) = edit_message_editor.clone() {
854 div()
855 .key_context("EditMessageEditor")
856 .on_action(cx.listener(Self::cancel_editing_message))
857 .on_action(cx.listener(Self::confirm_editing_message))
858 .min_h_6()
859 .child(edit_message_editor)
860 } else {
861 div()
862 .min_h_6()
863 .text_ui(cx)
864 .child(self.render_message_content(message_id, rendered_message, cx))
865 },
866 )
867 .when_some(context, |parent, context| {
868 if !context.is_empty() {
869 parent.child(
870 h_flex().flex_wrap().gap_1().children(
871 context
872 .into_iter()
873 .map(|context| ContextPill::added(context, false, false, None)),
874 ),
875 )
876 } else {
877 parent
878 }
879 });
880
881 let styled_message = match message.role {
882 Role::User => v_flex()
883 .id(("message-container", ix))
884 .map(|this| {
885 if first_message {
886 this.pt_2()
887 } else {
888 this.pt_4()
889 }
890 })
891 .pb_4()
892 .pl_2()
893 .pr_2p5()
894 .child(
895 v_flex()
896 .bg(colors.editor_background)
897 .rounded_lg()
898 .border_1()
899 .border_color(colors.border)
900 .shadow_md()
901 .child(
902 h_flex()
903 .py_1()
904 .pl_2()
905 .pr_1()
906 .bg(bg_user_message_header)
907 .border_b_1()
908 .border_color(colors.border)
909 .justify_between()
910 .rounded_t_md()
911 .child(
912 h_flex()
913 .gap_1p5()
914 .child(
915 Icon::new(IconName::PersonCircle)
916 .size(IconSize::XSmall)
917 .color(Color::Muted),
918 )
919 .child(
920 Label::new("You")
921 .size(LabelSize::Small)
922 .color(Color::Muted),
923 ),
924 )
925 .child(
926 h_flex()
927 // DL: To double-check whether we want to fully remove
928 // the editing feature from meassages. Checkpoint sort of
929 // solve the same problem.
930 .invisible()
931 .gap_1()
932 .when_some(
933 edit_message_editor.clone(),
934 |this, edit_message_editor| {
935 let focus_handle =
936 edit_message_editor.focus_handle(cx);
937 this.child(
938 Button::new("cancel-edit-message", "Cancel")
939 .label_size(LabelSize::Small)
940 .key_binding(
941 KeyBinding::for_action_in(
942 &menu::Cancel,
943 &focus_handle,
944 window,
945 cx,
946 )
947 .map(|kb| kb.size(rems_from_px(12.))),
948 )
949 .on_click(
950 cx.listener(Self::handle_cancel_click),
951 ),
952 )
953 .child(
954 Button::new(
955 "confirm-edit-message",
956 "Regenerate",
957 )
958 .label_size(LabelSize::Small)
959 .key_binding(
960 KeyBinding::for_action_in(
961 &menu::Confirm,
962 &focus_handle,
963 window,
964 cx,
965 )
966 .map(|kb| kb.size(rems_from_px(12.))),
967 )
968 .on_click(
969 cx.listener(Self::handle_regenerate_click),
970 ),
971 )
972 },
973 )
974 .when(
975 edit_message_editor.is_none() && allow_editing_message,
976 |this| {
977 this.child(
978 Button::new("edit-message", "Edit")
979 .label_size(LabelSize::Small)
980 .on_click(cx.listener({
981 let message_segments =
982 message.segments.clone();
983 move |this, _, window, cx| {
984 this.start_editing_message(
985 message_id,
986 &message_segments,
987 window,
988 cx,
989 );
990 }
991 })),
992 )
993 },
994 ),
995 ),
996 )
997 .child(div().p_2().child(message_content)),
998 ),
999 Role::Assistant => {
1000 v_flex()
1001 .id(("message-container", ix))
1002 .ml_2()
1003 .pl_2()
1004 .pr_4()
1005 .border_l_1()
1006 .border_color(cx.theme().colors().border_variant)
1007 .child(message_content)
1008 .when(
1009 !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
1010 |parent| {
1011 parent.child(
1012 v_flex()
1013 .children(
1014 tool_uses
1015 .into_iter()
1016 .map(|tool_use| self.render_tool_use(tool_use, cx)),
1017 )
1018 .children(scripting_tool_uses.into_iter().map(|tool_use| {
1019 self.render_scripting_tool_use(tool_use, cx)
1020 })),
1021 )
1022 },
1023 )
1024 }
1025 Role::System => div().id(("message-container", ix)).py_1().px_2().child(
1026 v_flex()
1027 .bg(colors.editor_background)
1028 .rounded_sm()
1029 .child(div().p_4().child(message_content)),
1030 ),
1031 };
1032
1033 v_flex()
1034 .w_full()
1035 .when(first_message, |parent| {
1036 parent.child(self.render_rules_item(cx))
1037 })
1038 .when_some(checkpoint, |parent, checkpoint| {
1039 let mut is_pending = false;
1040 let mut error = None;
1041 if let Some(last_restore_checkpoint) =
1042 self.thread.read(cx).last_restore_checkpoint()
1043 {
1044 if last_restore_checkpoint.message_id() == message_id {
1045 match last_restore_checkpoint {
1046 LastRestoreCheckpoint::Pending { .. } => is_pending = true,
1047 LastRestoreCheckpoint::Error { error: err, .. } => {
1048 error = Some(err.clone());
1049 }
1050 }
1051 }
1052 }
1053
1054 let restore_checkpoint_button =
1055 Button::new(("restore-checkpoint", ix), "Restore Checkpoint")
1056 .icon(if error.is_some() {
1057 IconName::XCircle
1058 } else {
1059 IconName::Undo
1060 })
1061 .icon_size(IconSize::XSmall)
1062 .icon_position(IconPosition::Start)
1063 .icon_color(if error.is_some() {
1064 Some(Color::Error)
1065 } else {
1066 None
1067 })
1068 .label_size(LabelSize::XSmall)
1069 .disabled(is_pending)
1070 .on_click(cx.listener(move |this, _, _window, cx| {
1071 this.thread.update(cx, |thread, cx| {
1072 thread
1073 .restore_checkpoint(checkpoint.clone(), cx)
1074 .detach_and_log_err(cx);
1075 });
1076 }));
1077
1078 let restore_checkpoint_button = if is_pending {
1079 restore_checkpoint_button
1080 .with_animation(
1081 ("pulsating-restore-checkpoint-button", ix),
1082 Animation::new(Duration::from_secs(2))
1083 .repeat()
1084 .with_easing(pulsating_between(0.6, 1.)),
1085 |label, delta| label.alpha(delta),
1086 )
1087 .into_any_element()
1088 } else if let Some(error) = error {
1089 restore_checkpoint_button
1090 .tooltip(Tooltip::text(error.to_string()))
1091 .into_any_element()
1092 } else {
1093 restore_checkpoint_button.into_any_element()
1094 };
1095
1096 parent.child(
1097 h_flex()
1098 .px_2p5()
1099 .w_full()
1100 .gap_1()
1101 .child(ui::Divider::horizontal())
1102 .child(restore_checkpoint_button)
1103 .child(ui::Divider::horizontal()),
1104 )
1105 })
1106 .child(styled_message)
1107 .when(
1108 is_last_message && !self.thread.read(cx).is_generating(),
1109 |parent| parent.child(feedback_items),
1110 )
1111 .into_any()
1112 }
1113
1114 fn render_message_content(
1115 &self,
1116 message_id: MessageId,
1117 rendered_message: &RenderedMessage,
1118 cx: &Context<Self>,
1119 ) -> impl IntoElement {
1120 let pending_thinking_segment_index = rendered_message
1121 .segments
1122 .iter()
1123 .enumerate()
1124 .last()
1125 .filter(|(_, segment)| matches!(segment, RenderedMessageSegment::Thinking { .. }))
1126 .map(|(index, _)| index);
1127
1128 div()
1129 .text_ui(cx)
1130 .gap_2()
1131 .children(
1132 rendered_message.segments.iter().enumerate().map(
1133 |(index, segment)| match segment {
1134 RenderedMessageSegment::Thinking {
1135 content,
1136 scroll_handle,
1137 } => self
1138 .render_message_thinking_segment(
1139 message_id,
1140 index,
1141 content.clone(),
1142 &scroll_handle,
1143 Some(index) == pending_thinking_segment_index,
1144 cx,
1145 )
1146 .into_any_element(),
1147 RenderedMessageSegment::Text(markdown) => {
1148 div().child(markdown.clone()).into_any_element()
1149 }
1150 },
1151 ),
1152 )
1153 }
1154
1155 fn render_message_thinking_segment(
1156 &self,
1157 message_id: MessageId,
1158 ix: usize,
1159 markdown: Entity<Markdown>,
1160 scroll_handle: &ScrollHandle,
1161 pending: bool,
1162 cx: &Context<Self>,
1163 ) -> impl IntoElement {
1164 let is_open = self
1165 .expanded_thinking_segments
1166 .get(&(message_id, ix))
1167 .copied()
1168 .unwrap_or_default();
1169
1170 let lighter_border = cx.theme().colors().border.opacity(0.5);
1171 let editor_bg = cx.theme().colors().editor_background;
1172
1173 div().py_2().child(
1174 v_flex()
1175 .rounded_lg()
1176 .border_1()
1177 .border_color(lighter_border)
1178 .child(
1179 h_flex()
1180 .group("disclosure-header")
1181 .justify_between()
1182 .py_1()
1183 .px_2()
1184 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1185 .map(|this| {
1186 if pending || is_open {
1187 this.rounded_t_md()
1188 .border_b_1()
1189 .border_color(lighter_border)
1190 } else {
1191 this.rounded_md()
1192 }
1193 })
1194 .child(
1195 h_flex()
1196 .gap_1p5()
1197 .child(
1198 Icon::new(IconName::Brain)
1199 .size(IconSize::XSmall)
1200 .color(Color::Muted),
1201 )
1202 .child({
1203 if pending {
1204 Label::new("Thinking…")
1205 .size(LabelSize::Small)
1206 .buffer_font(cx)
1207 .with_animation(
1208 "pulsating-label",
1209 Animation::new(Duration::from_secs(2))
1210 .repeat()
1211 .with_easing(pulsating_between(0.4, 0.8)),
1212 |label, delta| label.alpha(delta),
1213 )
1214 .into_any_element()
1215 } else {
1216 Label::new("Thought Process")
1217 .size(LabelSize::Small)
1218 .buffer_font(cx)
1219 .into_any_element()
1220 }
1221 }),
1222 )
1223 .child(
1224 h_flex()
1225 .gap_1()
1226 .child(
1227 div().visible_on_hover("disclosure-header").child(
1228 Disclosure::new("thinking-disclosure", is_open)
1229 .opened_icon(IconName::ChevronUp)
1230 .closed_icon(IconName::ChevronDown)
1231 .on_click(cx.listener({
1232 move |this, _event, _window, _cx| {
1233 let is_open = this
1234 .expanded_thinking_segments
1235 .entry((message_id, ix))
1236 .or_insert(false);
1237
1238 *is_open = !*is_open;
1239 }
1240 })),
1241 ),
1242 )
1243 .child({
1244 let (icon_name, color, animated) = if pending {
1245 (IconName::ArrowCircle, Color::Accent, true)
1246 } else {
1247 (IconName::Check, Color::Success, false)
1248 };
1249
1250 let icon =
1251 Icon::new(icon_name).color(color).size(IconSize::Small);
1252
1253 if animated {
1254 icon.with_animation(
1255 "arrow-circle",
1256 Animation::new(Duration::from_secs(2)).repeat(),
1257 |icon, delta| {
1258 icon.transform(Transformation::rotate(percentage(
1259 delta,
1260 )))
1261 },
1262 )
1263 .into_any_element()
1264 } else {
1265 icon.into_any_element()
1266 }
1267 }),
1268 ),
1269 )
1270 .when(pending && !is_open, |this| {
1271 let gradient_overlay = div()
1272 .rounded_b_lg()
1273 .h_20()
1274 .absolute()
1275 .w_full()
1276 .bottom_0()
1277 .left_0()
1278 .bg(linear_gradient(
1279 180.,
1280 linear_color_stop(editor_bg, 1.),
1281 linear_color_stop(editor_bg.opacity(0.2), 0.),
1282 ));
1283
1284 this.child(
1285 div()
1286 .relative()
1287 .bg(editor_bg)
1288 .rounded_b_lg()
1289 .child(
1290 div()
1291 .id(("thinking-content", ix))
1292 .p_2()
1293 .h_20()
1294 .track_scroll(scroll_handle)
1295 .text_ui_sm(cx)
1296 .child(markdown.clone())
1297 .overflow_hidden(),
1298 )
1299 .child(gradient_overlay),
1300 )
1301 })
1302 .when(is_open, |this| {
1303 this.child(
1304 div()
1305 .id(("thinking-content", ix))
1306 .h_full()
1307 .p_2()
1308 .rounded_b_lg()
1309 .bg(editor_bg)
1310 .text_ui_sm(cx)
1311 .child(markdown.clone()),
1312 )
1313 }),
1314 )
1315 }
1316
1317 fn render_tool_use(&self, tool_use: ToolUse, cx: &mut Context<Self>) -> impl IntoElement {
1318 let is_open = self
1319 .expanded_tool_uses
1320 .get(&tool_use.id)
1321 .copied()
1322 .unwrap_or_default();
1323
1324 let lighter_border = cx.theme().colors().border.opacity(0.5);
1325
1326 let tool_icon = match tool_use.name.as_ref() {
1327 "bash" => IconName::Terminal,
1328 "delete-path" => IconName::Trash,
1329 "diagnostics" => IconName::Warning,
1330 "edit-files" => IconName::Pencil,
1331 "fetch" => IconName::Globe,
1332 "list-directory" => IconName::Folder,
1333 "now" => IconName::Info,
1334 "path-search" => IconName::SearchCode,
1335 "read-file" => IconName::Eye,
1336 "regex-search" => IconName::Regex,
1337 "thinking" => IconName::Brain,
1338 _ => IconName::Terminal,
1339 };
1340
1341 div().py_2().child(
1342 v_flex()
1343 .rounded_lg()
1344 .border_1()
1345 .border_color(lighter_border)
1346 .overflow_hidden()
1347 .child(
1348 h_flex()
1349 .group("disclosure-header")
1350 .justify_between()
1351 .py_1()
1352 .px_2()
1353 .bg(cx.theme().colors().editor_foreground.opacity(0.025))
1354 .map(|element| {
1355 if is_open {
1356 element.border_b_1().rounded_t_md()
1357 } else {
1358 element.rounded_md()
1359 }
1360 })
1361 .border_color(lighter_border)
1362 .child(
1363 h_flex()
1364 .gap_1p5()
1365 .child(
1366 Icon::new(tool_icon)
1367 .size(IconSize::XSmall)
1368 .color(Color::Muted),
1369 )
1370 .child(
1371 div()
1372 .text_ui_sm(cx)
1373 .children(
1374 self.rendered_tool_use_labels
1375 .get(&tool_use.id)
1376 .cloned(),
1377 )
1378 .truncate(),
1379 ),
1380 )
1381 .child(
1382 h_flex()
1383 .gap_1()
1384 .child(
1385 div().visible_on_hover("disclosure-header").child(
1386 Disclosure::new("tool-use-disclosure", is_open)
1387 .opened_icon(IconName::ChevronUp)
1388 .closed_icon(IconName::ChevronDown)
1389 .on_click(cx.listener({
1390 let tool_use_id = tool_use.id.clone();
1391 move |this, _event, _window, _cx| {
1392 let is_open = this
1393 .expanded_tool_uses
1394 .entry(tool_use_id.clone())
1395 .or_insert(false);
1396
1397 *is_open = !*is_open;
1398 }
1399 })),
1400 ),
1401 )
1402 .child({
1403 let (icon_name, color, animated) = match &tool_use.status {
1404 ToolUseStatus::Pending
1405 | ToolUseStatus::NeedsConfirmation => {
1406 (IconName::Warning, Color::Warning, false)
1407 }
1408 ToolUseStatus::Running => {
1409 (IconName::ArrowCircle, Color::Accent, true)
1410 }
1411 ToolUseStatus::Finished(_) => {
1412 (IconName::Check, Color::Success, false)
1413 }
1414 ToolUseStatus::Error(_) => {
1415 (IconName::Close, Color::Error, false)
1416 }
1417 };
1418
1419 let icon =
1420 Icon::new(icon_name).color(color).size(IconSize::Small);
1421
1422 if animated {
1423 icon.with_animation(
1424 "arrow-circle",
1425 Animation::new(Duration::from_secs(2)).repeat(),
1426 |icon, delta| {
1427 icon.transform(Transformation::rotate(percentage(
1428 delta,
1429 )))
1430 },
1431 )
1432 .into_any_element()
1433 } else {
1434 icon.into_any_element()
1435 }
1436 }),
1437 ),
1438 )
1439 .map(|parent| {
1440 if !is_open {
1441 return parent;
1442 }
1443
1444 let content_container = || v_flex().py_1().gap_0p5().px_2p5();
1445
1446 parent.child(
1447 v_flex()
1448 .gap_1()
1449 .bg(cx.theme().colors().editor_background)
1450 .rounded_b_lg()
1451 .child(
1452 content_container()
1453 .border_b_1()
1454 .border_color(lighter_border)
1455 .child(
1456 Label::new("Input")
1457 .size(LabelSize::XSmall)
1458 .color(Color::Muted)
1459 .buffer_font(cx),
1460 )
1461 .child(
1462 Label::new(
1463 serde_json::to_string_pretty(&tool_use.input)
1464 .unwrap_or_default(),
1465 )
1466 .size(LabelSize::Small)
1467 .buffer_font(cx),
1468 ),
1469 )
1470 .map(|container| match tool_use.status {
1471 ToolUseStatus::Finished(output) => container.child(
1472 content_container()
1473 .child(
1474 Label::new("Result")
1475 .size(LabelSize::XSmall)
1476 .color(Color::Muted)
1477 .buffer_font(cx),
1478 )
1479 .child(
1480 Label::new(output)
1481 .size(LabelSize::Small)
1482 .buffer_font(cx),
1483 ),
1484 ),
1485 ToolUseStatus::Running => container.child(
1486 content_container().child(
1487 h_flex()
1488 .gap_1()
1489 .pb_1()
1490 .child(
1491 Icon::new(IconName::ArrowCircle)
1492 .size(IconSize::Small)
1493 .color(Color::Accent)
1494 .with_animation(
1495 "arrow-circle",
1496 Animation::new(Duration::from_secs(2))
1497 .repeat(),
1498 |icon, delta| {
1499 icon.transform(Transformation::rotate(
1500 percentage(delta),
1501 ))
1502 },
1503 ),
1504 )
1505 .child(
1506 Label::new("Running…")
1507 .size(LabelSize::XSmall)
1508 .color(Color::Muted)
1509 .buffer_font(cx),
1510 ),
1511 ),
1512 ),
1513 ToolUseStatus::Error(err) => container.child(
1514 content_container()
1515 .child(
1516 Label::new("Error")
1517 .size(LabelSize::XSmall)
1518 .color(Color::Muted)
1519 .buffer_font(cx),
1520 )
1521 .child(
1522 Label::new(err).size(LabelSize::Small).buffer_font(cx),
1523 ),
1524 ),
1525 ToolUseStatus::Pending => container,
1526 ToolUseStatus::NeedsConfirmation => container.child(
1527 content_container().child(
1528 Label::new("Asking Permission")
1529 .size(LabelSize::Small)
1530 .color(Color::Muted)
1531 .buffer_font(cx),
1532 ),
1533 ),
1534 }),
1535 )
1536 }),
1537 )
1538 }
1539
1540 fn render_scripting_tool_use(
1541 &self,
1542 tool_use: ToolUse,
1543 cx: &mut Context<Self>,
1544 ) -> impl IntoElement {
1545 let is_open = self
1546 .expanded_tool_uses
1547 .get(&tool_use.id)
1548 .copied()
1549 .unwrap_or_default();
1550
1551 div().px_2p5().child(
1552 v_flex()
1553 .gap_1()
1554 .rounded_lg()
1555 .border_1()
1556 .border_color(cx.theme().colors().border)
1557 .child(
1558 h_flex()
1559 .justify_between()
1560 .py_0p5()
1561 .pl_1()
1562 .pr_2()
1563 .bg(cx.theme().colors().editor_foreground.opacity(0.02))
1564 .map(|element| {
1565 if is_open {
1566 element.border_b_1().rounded_t_md()
1567 } else {
1568 element.rounded_md()
1569 }
1570 })
1571 .border_color(cx.theme().colors().border)
1572 .child(
1573 h_flex()
1574 .gap_1()
1575 .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
1576 cx.listener({
1577 let tool_use_id = tool_use.id.clone();
1578 move |this, _event, _window, _cx| {
1579 let is_open = this
1580 .expanded_tool_uses
1581 .entry(tool_use_id.clone())
1582 .or_insert(false);
1583
1584 *is_open = !*is_open;
1585 }
1586 }),
1587 ))
1588 .child(
1589 h_flex()
1590 .gap_1p5()
1591 .child(
1592 Icon::new(IconName::Terminal)
1593 .size(IconSize::XSmall)
1594 .color(Color::Muted),
1595 )
1596 .child(
1597 div()
1598 .text_ui_sm(cx)
1599 .children(
1600 self.rendered_tool_use_labels
1601 .get(&tool_use.id)
1602 .cloned(),
1603 )
1604 .truncate(),
1605 ),
1606 ),
1607 )
1608 .child(
1609 Label::new(match tool_use.status {
1610 ToolUseStatus::Pending => "Pending",
1611 ToolUseStatus::Running => "Running",
1612 ToolUseStatus::Finished(_) => "Finished",
1613 ToolUseStatus::Error(_) => "Error",
1614 ToolUseStatus::NeedsConfirmation => "Asking Permission",
1615 })
1616 .size(LabelSize::XSmall)
1617 .buffer_font(cx),
1618 ),
1619 )
1620 .map(|parent| {
1621 if !is_open {
1622 return parent;
1623 }
1624
1625 let lua_script_markdown =
1626 self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
1627
1628 parent.child(
1629 v_flex()
1630 .child(
1631 v_flex()
1632 .gap_0p5()
1633 .py_1()
1634 .px_2p5()
1635 .border_b_1()
1636 .border_color(cx.theme().colors().border)
1637 .child(Label::new("Input:"))
1638 .map(|parent| {
1639 if let Some(markdown) = lua_script_markdown {
1640 parent.child(markdown)
1641 } else {
1642 parent.child(Label::new(
1643 "Failed to render script input to Markdown",
1644 ))
1645 }
1646 }),
1647 )
1648 .map(|parent| match tool_use.status {
1649 ToolUseStatus::Finished(output) => parent.child(
1650 v_flex()
1651 .gap_0p5()
1652 .py_1()
1653 .px_2p5()
1654 .child(Label::new("Result:"))
1655 .child(Label::new(output)),
1656 ),
1657 ToolUseStatus::Error(err) => parent.child(
1658 v_flex()
1659 .gap_0p5()
1660 .py_1()
1661 .px_2p5()
1662 .child(Label::new("Error:"))
1663 .child(Label::new(err)),
1664 ),
1665 ToolUseStatus::Pending | ToolUseStatus::Running => parent,
1666 ToolUseStatus::NeedsConfirmation => parent.child(
1667 v_flex()
1668 .gap_0p5()
1669 .py_1()
1670 .px_2p5()
1671 .child(Label::new("Asking Permission")),
1672 ),
1673 }),
1674 )
1675 }),
1676 )
1677 }
1678
1679 fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
1680 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1681 else {
1682 return div().into_any();
1683 };
1684
1685 let rules_files = system_prompt_context
1686 .worktrees
1687 .iter()
1688 .filter_map(|worktree| worktree.rules_file.as_ref())
1689 .collect::<Vec<_>>();
1690
1691 let label_text = match rules_files.as_slice() {
1692 &[] => return div().into_any(),
1693 &[rules_file] => {
1694 format!("Using {:?} file", rules_file.rel_path)
1695 }
1696 rules_files => {
1697 format!("Using {} rules files", rules_files.len())
1698 }
1699 };
1700
1701 div()
1702 .pt_1()
1703 .px_2p5()
1704 .child(
1705 h_flex()
1706 .w_full()
1707 .gap_0p5()
1708 .child(
1709 h_flex()
1710 .gap_1p5()
1711 .child(
1712 Icon::new(IconName::File)
1713 .size(IconSize::XSmall)
1714 .color(Color::Disabled),
1715 )
1716 .child(
1717 Label::new(label_text)
1718 .size(LabelSize::XSmall)
1719 .color(Color::Muted)
1720 .buffer_font(cx),
1721 ),
1722 )
1723 .child(
1724 IconButton::new("open-rule", IconName::ArrowUpRightAlt)
1725 .shape(ui::IconButtonShape::Square)
1726 .icon_size(IconSize::XSmall)
1727 .icon_color(Color::Ignored)
1728 .on_click(cx.listener(Self::handle_open_rules))
1729 .tooltip(Tooltip::text("View Rules")),
1730 ),
1731 )
1732 .into_any()
1733 }
1734
1735 fn handle_allow_tool(
1736 &mut self,
1737 tool_use_id: LanguageModelToolUseId,
1738 _: &ClickEvent,
1739 _window: &mut Window,
1740 cx: &mut Context<Self>,
1741 ) {
1742 if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
1743 .thread
1744 .read(cx)
1745 .pending_tool(&tool_use_id)
1746 .map(|tool_use| tool_use.status.clone())
1747 {
1748 self.thread.update(cx, |thread, cx| {
1749 thread.run_tool(
1750 c.tool_use_id.clone(),
1751 c.ui_text.clone(),
1752 c.input.clone(),
1753 &c.messages,
1754 c.tool_type.clone(),
1755 cx,
1756 );
1757 });
1758 }
1759 }
1760
1761 fn handle_deny_tool(
1762 &mut self,
1763 tool_use_id: LanguageModelToolUseId,
1764 tool_type: ToolType,
1765 _: &ClickEvent,
1766 _window: &mut Window,
1767 cx: &mut Context<Self>,
1768 ) {
1769 self.thread.update(cx, |thread, cx| {
1770 thread.deny_tool_use(tool_use_id, tool_type, cx);
1771 });
1772 }
1773
1774 fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
1775 let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
1776 else {
1777 return;
1778 };
1779
1780 let abs_paths = system_prompt_context
1781 .worktrees
1782 .iter()
1783 .flat_map(|worktree| worktree.rules_file.as_ref())
1784 .map(|rules_file| rules_file.abs_path.to_path_buf())
1785 .collect::<Vec<_>>();
1786
1787 if let Ok(task) = self.workspace.update(cx, move |workspace, cx| {
1788 // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
1789 // files clear. For example, if rules file 1 is already open but rules file 2 is not,
1790 // this would open and focus rules file 2 in a tab that is not next to rules file 1.
1791 workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx)
1792 }) {
1793 task.detach();
1794 }
1795 }
1796
1797 fn render_confirmations<'a>(
1798 &'a mut self,
1799 cx: &'a mut Context<Self>,
1800 ) -> impl Iterator<Item = AnyElement> + 'a {
1801 let thread = self.thread.read(cx);
1802
1803 thread
1804 .tools_needing_confirmation()
1805 .map(|(tool_type, tool)| {
1806 div()
1807 .m_3()
1808 .p_2()
1809 .bg(cx.theme().colors().editor_background)
1810 .border_1()
1811 .border_color(cx.theme().colors().border)
1812 .rounded_lg()
1813 .child(
1814 v_flex()
1815 .gap_1()
1816 .child(
1817 v_flex()
1818 .gap_0p5()
1819 .child(
1820 Label::new("The agent wants to run this action:")
1821 .color(Color::Muted),
1822 )
1823 .child(div().p_3().child(Label::new(&tool.ui_text))),
1824 )
1825 .child(
1826 h_flex()
1827 .gap_1()
1828 .child({
1829 let tool_id = tool.id.clone();
1830 Button::new("allow-tool-action", "Allow").on_click(
1831 cx.listener(move |this, event, window, cx| {
1832 this.handle_allow_tool(
1833 tool_id.clone(),
1834 event,
1835 window,
1836 cx,
1837 )
1838 }),
1839 )
1840 })
1841 .child({
1842 let tool_id = tool.id.clone();
1843 Button::new("deny-tool", "Deny").on_click(cx.listener(
1844 move |this, event, window, cx| {
1845 this.handle_deny_tool(
1846 tool_id.clone(),
1847 tool_type.clone(),
1848 event,
1849 window,
1850 cx,
1851 )
1852 },
1853 ))
1854 }),
1855 )
1856 .child(
1857 Label::new("Note: A future release will introduce a way to remember your answers to these. In the meantime, you can avoid these prompts by adding \"assistant\": { \"always_allow_tool_actions\": true } to your settings.json.")
1858 .color(Color::Muted)
1859 .size(LabelSize::Small),
1860 ),
1861 )
1862 .into_any()
1863 })
1864 }
1865}
1866
1867impl Render for ActiveThread {
1868 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1869 v_flex()
1870 .size_full()
1871 .child(list(self.list_state.clone()).flex_grow())
1872 .children(self.render_confirmations(cx))
1873 }
1874}