1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 humanize_token_count,
4 prompt_library::open_prompt_library,
5 slash_command::{
6 default_command::DefaultSlashCommand,
7 docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
8 SlashCommandCompletionProvider, SlashCommandRegistry,
9 },
10 terminal_inline_assistant::TerminalInlineAssistant,
11 Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
12 DebugEditSteps, DeployHistory, DeployPromptLibrary, EditStep, EditStepState,
13 EditStepSuggestions, InlineAssist, InlineAssistId, InlineAssistant, InsertIntoEditor,
14 MessageStatus, ModelSelector, PendingSlashCommand, PendingSlashCommandStatus, QuoteSelection,
15 RemoteContextMetadata, SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
16};
17use crate::{ContextStoreEvent, ShowConfiguration};
18use anyhow::{anyhow, Result};
19use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
20use client::proto;
21use collections::{BTreeSet, HashMap, HashSet};
22use editor::{
23 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
24 display_map::{
25 BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock,
26 ToDisplayPoint,
27 },
28 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
29 Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
30};
31use editor::{display_map::CreaseId, FoldPlaceholder};
32use fs::Fs;
33use gpui::{
34 div, percentage, point, svg, Action, Animation, AnimationExt, AnyElement, AnyView, AppContext,
35 AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity, EventEmitter,
36 FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Pixels,
37 Render, SharedString, StatefulInteractiveElement, Styled, Subscription, Task, Transformation,
38 UpdateGlobal, View, ViewContext, VisualContext, WeakView, WindowContext,
39};
40use indexed_docs::IndexedDocsStore;
41use language::{
42 language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
43};
44use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, Role};
45use multi_buffer::MultiBufferRow;
46use picker::{Picker, PickerDelegate};
47use project::{Project, ProjectLspAdapterDelegate};
48use search::{buffer_search::DivRegistrar, BufferSearchBar};
49use settings::{update_settings_file, Settings};
50use std::{
51 borrow::Cow,
52 cmp::{self, Ordering},
53 fmt::Write,
54 ops::Range,
55 path::PathBuf,
56 sync::Arc,
57 time::Duration,
58};
59use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
60use ui::TintColor;
61use ui::{
62 prelude::*,
63 utils::{format_distance_from_now, DateTimeType},
64 Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
65 ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
66};
67use util::ResultExt;
68use workspace::{
69 dock::{DockPosition, Panel, PanelEvent},
70 item::{self, FollowableItem, Item, ItemHandle},
71 notifications::NotifyTaskExt,
72 pane::{self, SaveIntent},
73 searchable::{SearchEvent, SearchableItem},
74 Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
75};
76use workspace::{searchable::SearchableItemHandle, NewFile};
77
78pub fn init(cx: &mut AppContext) {
79 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
80 cx.observe_new_views(
81 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
82 workspace
83 .register_action(|workspace, _: &ToggleFocus, cx| {
84 let settings = AssistantSettings::get_global(cx);
85 if !settings.enabled {
86 return;
87 }
88
89 workspace.toggle_panel_focus::<AssistantPanel>(cx);
90 })
91 .register_action(AssistantPanel::inline_assist)
92 .register_action(ContextEditor::quote_selection)
93 .register_action(ContextEditor::insert_selection)
94 .register_action(AssistantPanel::show_configuration);
95 },
96 )
97 .detach();
98
99 cx.observe_new_views(
100 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
101 let settings = AssistantSettings::get_global(cx);
102 if !settings.enabled {
103 return;
104 }
105
106 terminal_panel.register_tab_bar_button(cx.new_view(|_| InlineAssistTabBarButton), cx);
107 },
108 )
109 .detach();
110}
111
112struct InlineAssistTabBarButton;
113
114impl Render for InlineAssistTabBarButton {
115 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
116 IconButton::new("terminal_inline_assistant", IconName::MagicWand)
117 .icon_size(IconSize::Small)
118 .on_click(cx.listener(|_, _, cx| {
119 cx.dispatch_action(InlineAssist::default().boxed_clone());
120 }))
121 .tooltip(move |cx| Tooltip::for_action("Inline Assist", &InlineAssist::default(), cx))
122 }
123}
124
125pub enum AssistantPanelEvent {
126 ContextEdited,
127}
128
129pub struct AssistantPanel {
130 pane: View<Pane>,
131 workspace: WeakView<Workspace>,
132 width: Option<Pixels>,
133 height: Option<Pixels>,
134 project: Model<Project>,
135 context_store: Model<ContextStore>,
136 languages: Arc<LanguageRegistry>,
137 fs: Arc<dyn Fs>,
138 subscriptions: Vec<Subscription>,
139 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
140 model_summary_editor: View<Editor>,
141 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
142 configuration_subscription: Option<Subscription>,
143}
144
145#[derive(Clone)]
146enum ContextMetadata {
147 Remote(RemoteContextMetadata),
148 Saved(SavedContextMetadata),
149}
150
151struct SavedContextPickerDelegate {
152 store: Model<ContextStore>,
153 project: Model<Project>,
154 matches: Vec<ContextMetadata>,
155 selected_index: usize,
156}
157
158enum SavedContextPickerEvent {
159 Confirmed(ContextMetadata),
160}
161
162enum InlineAssistTarget {
163 Editor(View<Editor>, bool),
164 Terminal(View<TerminalView>),
165}
166
167impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
168
169impl SavedContextPickerDelegate {
170 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
171 Self {
172 project,
173 store,
174 matches: Vec::new(),
175 selected_index: 0,
176 }
177 }
178}
179
180impl PickerDelegate for SavedContextPickerDelegate {
181 type ListItem = ListItem;
182
183 fn match_count(&self) -> usize {
184 self.matches.len()
185 }
186
187 fn selected_index(&self) -> usize {
188 self.selected_index
189 }
190
191 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
192 self.selected_index = ix;
193 }
194
195 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
196 "Search...".into()
197 }
198
199 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
200 let search = self.store.read(cx).search(query, cx);
201 cx.spawn(|this, mut cx| async move {
202 let matches = search.await;
203 this.update(&mut cx, |this, cx| {
204 let host_contexts = this.delegate.store.read(cx).host_contexts();
205 this.delegate.matches = host_contexts
206 .iter()
207 .cloned()
208 .map(ContextMetadata::Remote)
209 .chain(matches.into_iter().map(ContextMetadata::Saved))
210 .collect();
211 this.delegate.selected_index = 0;
212 cx.notify();
213 })
214 .ok();
215 })
216 }
217
218 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
219 if let Some(metadata) = self.matches.get(self.selected_index) {
220 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
221 }
222 }
223
224 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
225
226 fn render_match(
227 &self,
228 ix: usize,
229 selected: bool,
230 cx: &mut ViewContext<Picker<Self>>,
231 ) -> Option<Self::ListItem> {
232 let context = self.matches.get(ix)?;
233 let item = match context {
234 ContextMetadata::Remote(context) => {
235 let host_user = self.project.read(cx).host().and_then(|collaborator| {
236 self.project
237 .read(cx)
238 .user_store()
239 .read(cx)
240 .get_cached_user(collaborator.user_id)
241 });
242 div()
243 .flex()
244 .w_full()
245 .justify_between()
246 .gap_2()
247 .child(
248 h_flex().flex_1().overflow_x_hidden().child(
249 Label::new(context.summary.clone().unwrap_or("New Context".into()))
250 .size(LabelSize::Small),
251 ),
252 )
253 .child(
254 h_flex()
255 .gap_2()
256 .children(if let Some(host_user) = host_user {
257 vec![
258 Avatar::new(host_user.avatar_uri.clone())
259 .shape(AvatarShape::Circle)
260 .into_any_element(),
261 Label::new(format!("Shared by @{}", host_user.github_login))
262 .color(Color::Muted)
263 .size(LabelSize::Small)
264 .into_any_element(),
265 ]
266 } else {
267 vec![Label::new("Shared by host")
268 .color(Color::Muted)
269 .size(LabelSize::Small)
270 .into_any_element()]
271 }),
272 )
273 }
274 ContextMetadata::Saved(context) => div()
275 .flex()
276 .w_full()
277 .justify_between()
278 .gap_2()
279 .child(
280 h_flex()
281 .flex_1()
282 .child(Label::new(context.title.clone()).size(LabelSize::Small))
283 .overflow_x_hidden(),
284 )
285 .child(
286 Label::new(format_distance_from_now(
287 DateTimeType::Local(context.mtime),
288 false,
289 true,
290 true,
291 ))
292 .color(Color::Muted)
293 .size(LabelSize::Small),
294 ),
295 };
296 Some(
297 ListItem::new(ix)
298 .inset(true)
299 .spacing(ListItemSpacing::Sparse)
300 .selected(selected)
301 .child(item),
302 )
303 }
304}
305
306impl AssistantPanel {
307 pub fn load(
308 workspace: WeakView<Workspace>,
309 cx: AsyncWindowContext,
310 ) -> Task<Result<View<Self>>> {
311 cx.spawn(|mut cx| async move {
312 let context_store = workspace
313 .update(&mut cx, |workspace, cx| {
314 ContextStore::new(workspace.project().clone(), cx)
315 })?
316 .await?;
317 workspace.update(&mut cx, |workspace, cx| {
318 // TODO: deserialize state.
319 cx.new_view(|cx| Self::new(workspace, context_store, cx))
320 })
321 })
322 }
323
324 fn new(
325 workspace: &Workspace,
326 context_store: Model<ContextStore>,
327 cx: &mut ViewContext<Self>,
328 ) -> Self {
329 let model_selector_menu_handle = PopoverMenuHandle::default();
330 let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx));
331 let context_editor_toolbar = cx.new_view(|_| {
332 ContextEditorToolbarItem::new(
333 workspace,
334 model_selector_menu_handle.clone(),
335 model_summary_editor.clone(),
336 )
337 });
338 let pane = cx.new_view(|cx| {
339 let mut pane = Pane::new(
340 workspace.weak_handle(),
341 workspace.project().clone(),
342 Default::default(),
343 None,
344 NewFile.boxed_clone(),
345 cx,
346 );
347 pane.set_can_split(false, cx);
348 pane.set_can_navigate(true, cx);
349 pane.display_nav_history_buttons(None);
350 pane.set_should_display_tab_bar(|_| true);
351 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
352 h_flex()
353 .gap(Spacing::Small.rems(cx))
354 .child(
355 IconButton::new("menu", IconName::Menu)
356 .icon_size(IconSize::Small)
357 .on_click(cx.listener(|pane, _, cx| {
358 let zoom_label = if pane.is_zoomed() {
359 "Zoom Out"
360 } else {
361 "Zoom In"
362 };
363 let menu = ContextMenu::build(cx, |menu, cx| {
364 menu.context(pane.focus_handle(cx))
365 .action("New Context", Box::new(NewFile))
366 .action("History", Box::new(DeployHistory))
367 .action("Prompt Library", Box::new(DeployPromptLibrary))
368 .action("Configure", Box::new(ShowConfiguration))
369 .action(zoom_label, Box::new(ToggleZoom))
370 });
371 cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
372 pane.new_item_menu = None;
373 })
374 .detach();
375 pane.new_item_menu = Some(menu);
376 })),
377 )
378 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
379 el.child(Pane::render_menu_overlay(new_item_menu))
380 })
381 .into_any_element()
382 .into()
383 });
384 pane.toolbar().update(cx, |toolbar, cx| {
385 toolbar.add_item(context_editor_toolbar.clone(), cx);
386 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
387 });
388 pane
389 });
390
391 let subscriptions = vec![
392 cx.observe(&pane, |_, _, cx| cx.notify()),
393 cx.subscribe(&pane, Self::handle_pane_event),
394 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
395 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
396 cx.subscribe(&context_store, Self::handle_context_store_event),
397 cx.subscribe(
398 &LanguageModelRegistry::global(cx),
399 |this, _, event: &language_model::Event, cx| match event {
400 language_model::Event::ActiveModelChanged => {
401 this.completion_provider_changed(cx);
402 }
403 language_model::Event::ProviderStateChanged => {
404 this.ensure_authenticated(cx);
405 }
406 language_model::Event::AddedProvider(_)
407 | language_model::Event::RemovedProvider(_) => {
408 this.ensure_authenticated(cx);
409 }
410 },
411 ),
412 ];
413
414 let mut this = Self {
415 pane,
416 workspace: workspace.weak_handle(),
417 width: None,
418 height: None,
419 project: workspace.project().clone(),
420 context_store,
421 languages: workspace.app_state().languages.clone(),
422 fs: workspace.app_state().fs.clone(),
423 subscriptions,
424 model_selector_menu_handle,
425 model_summary_editor,
426 authenticate_provider_task: None,
427 configuration_subscription: None,
428 };
429
430 if LanguageModelRegistry::read_global(cx)
431 .active_provider()
432 .is_none()
433 {
434 this.show_configuration_for_provider(None, cx);
435 } else {
436 this.new_context(cx);
437 };
438
439 this
440 }
441
442 fn handle_pane_event(
443 &mut self,
444 pane: View<Pane>,
445 event: &pane::Event,
446 cx: &mut ViewContext<Self>,
447 ) {
448 let update_model_summary = match event {
449 pane::Event::Remove => {
450 cx.emit(PanelEvent::Close);
451 false
452 }
453 pane::Event::ZoomIn => {
454 cx.emit(PanelEvent::ZoomIn);
455 false
456 }
457 pane::Event::ZoomOut => {
458 cx.emit(PanelEvent::ZoomOut);
459 false
460 }
461
462 pane::Event::AddItem { item } => {
463 self.workspace
464 .update(cx, |workspace, cx| {
465 item.added_to_pane(workspace, self.pane.clone(), cx)
466 })
467 .ok();
468 true
469 }
470
471 pane::Event::ActivateItem { local } => {
472 if *local {
473 self.workspace
474 .update(cx, |workspace, cx| {
475 workspace.unfollow_in_pane(&pane, cx);
476 })
477 .ok();
478 }
479 cx.emit(AssistantPanelEvent::ContextEdited);
480 true
481 }
482
483 pane::Event::RemoveItem { idx } => {
484 if self
485 .pane
486 .read(cx)
487 .item_for_index(*idx)
488 .map_or(false, |item| item.downcast::<ConfigurationView>().is_some())
489 {
490 self.configuration_subscription = None;
491 }
492 false
493 }
494 pane::Event::RemovedItem { .. } => {
495 cx.emit(AssistantPanelEvent::ContextEdited);
496 true
497 }
498
499 _ => false,
500 };
501
502 if update_model_summary {
503 if let Some(editor) = self.active_context_editor(cx) {
504 self.show_updated_summary(&editor, cx)
505 }
506 }
507 }
508
509 fn handle_summary_editor_event(
510 &mut self,
511 model_summary_editor: View<Editor>,
512 event: &EditorEvent,
513 cx: &mut ViewContext<Self>,
514 ) {
515 if matches!(event, EditorEvent::Edited { .. }) {
516 if let Some(context_editor) = self.active_context_editor(cx) {
517 let new_summary = model_summary_editor.read(cx).text(cx);
518 context_editor.update(cx, |context_editor, cx| {
519 context_editor.context.update(cx, |context, cx| {
520 if context.summary().is_none()
521 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
522 {
523 return;
524 }
525 context.custom_summary(new_summary, cx)
526 });
527 });
528 }
529 }
530 }
531
532 fn handle_toolbar_event(
533 &mut self,
534 _: View<ContextEditorToolbarItem>,
535 _: &ContextEditorToolbarItemEvent,
536 cx: &mut ViewContext<Self>,
537 ) {
538 if let Some(context_editor) = self.active_context_editor(cx) {
539 context_editor.update(cx, |context_editor, cx| {
540 context_editor.context.update(cx, |context, cx| {
541 context.summarize(true, cx);
542 })
543 })
544 }
545 }
546
547 fn handle_context_store_event(
548 &mut self,
549 _context_store: Model<ContextStore>,
550 event: &ContextStoreEvent,
551 cx: &mut ViewContext<Self>,
552 ) {
553 let ContextStoreEvent::ContextCreated(context_id) = event;
554 let Some(context) = self
555 .context_store
556 .read(cx)
557 .loaded_context_for_id(&context_id, cx)
558 else {
559 log::error!("no context found with ID: {}", context_id.to_proto());
560 return;
561 };
562 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
563
564 let assistant_panel = cx.view().downgrade();
565 let editor = cx.new_view(|cx| {
566 let mut editor = ContextEditor::for_context(
567 context,
568 self.fs.clone(),
569 self.workspace.clone(),
570 self.project.clone(),
571 lsp_adapter_delegate,
572 assistant_panel,
573 cx,
574 );
575 editor.insert_default_prompt(cx);
576 editor
577 });
578
579 self.show_context(editor.clone(), cx);
580 }
581
582 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
583 if let Some(editor) = self.active_context_editor(cx) {
584 editor.update(cx, |active_context, cx| {
585 active_context
586 .context
587 .update(cx, |context, cx| context.completion_provider_changed(cx))
588 })
589 }
590
591 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
592 .active_provider()
593 .map(|p| p.id())
594 else {
595 return;
596 };
597
598 if self
599 .authenticate_provider_task
600 .as_ref()
601 .map_or(true, |(old_provider_id, _)| {
602 *old_provider_id != new_provider_id
603 })
604 {
605 self.authenticate_provider_task = None;
606 self.ensure_authenticated(cx);
607 }
608 }
609
610 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
611 if self.is_authenticated(cx) {
612 return;
613 }
614
615 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
616 return;
617 };
618
619 let load_credentials = self.authenticate(cx);
620
621 if self.authenticate_provider_task.is_none() {
622 self.authenticate_provider_task = Some((
623 provider.id(),
624 cx.spawn(|this, mut cx| async move {
625 let _ = load_credentials.await;
626 this.update(&mut cx, |this, cx| {
627 if !provider.is_authenticated(cx) {
628 this.show_configuration_for_provider(Some(provider), cx)
629 } else if !this.has_any_context_editors(cx) {
630 this.new_context(cx);
631 }
632 this.authenticate_provider_task = None;
633 })
634 .log_err();
635 }),
636 ));
637 }
638 }
639
640 pub fn inline_assist(
641 workspace: &mut Workspace,
642 action: &InlineAssist,
643 cx: &mut ViewContext<Workspace>,
644 ) {
645 let settings = AssistantSettings::get_global(cx);
646 if !settings.enabled {
647 return;
648 }
649
650 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
651 return;
652 };
653
654 let Some(inline_assist_target) =
655 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
656 else {
657 return;
658 };
659
660 let initial_prompt = action.prompt.clone();
661 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
662 match inline_assist_target {
663 InlineAssistTarget::Editor(active_editor, include_context) => {
664 InlineAssistant::update_global(cx, |assistant, cx| {
665 assistant.assist(
666 &active_editor,
667 Some(cx.view().downgrade()),
668 include_context.then_some(&assistant_panel),
669 initial_prompt,
670 cx,
671 )
672 })
673 }
674 InlineAssistTarget::Terminal(active_terminal) => {
675 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
676 assistant.assist(
677 &active_terminal,
678 Some(cx.view().downgrade()),
679 Some(&assistant_panel),
680 initial_prompt,
681 cx,
682 )
683 })
684 }
685 }
686 } else {
687 let assistant_panel = assistant_panel.downgrade();
688 cx.spawn(|workspace, mut cx| async move {
689 assistant_panel
690 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
691 .await?;
692 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
693 cx.update(|cx| match inline_assist_target {
694 InlineAssistTarget::Editor(active_editor, include_context) => {
695 let assistant_panel = if include_context {
696 assistant_panel.upgrade()
697 } else {
698 None
699 };
700 InlineAssistant::update_global(cx, |assistant, cx| {
701 assistant.assist(
702 &active_editor,
703 Some(workspace),
704 assistant_panel.as_ref(),
705 initial_prompt,
706 cx,
707 )
708 })
709 }
710 InlineAssistTarget::Terminal(active_terminal) => {
711 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
712 assistant.assist(
713 &active_terminal,
714 Some(workspace),
715 assistant_panel.upgrade().as_ref(),
716 initial_prompt,
717 cx,
718 )
719 })
720 }
721 })?
722 } else {
723 workspace.update(&mut cx, |workspace, cx| {
724 workspace.focus_panel::<AssistantPanel>(cx)
725 })?;
726 }
727
728 anyhow::Ok(())
729 })
730 .detach_and_log_err(cx)
731 }
732 }
733
734 fn resolve_inline_assist_target(
735 workspace: &mut Workspace,
736 assistant_panel: &View<AssistantPanel>,
737 cx: &mut WindowContext,
738 ) -> Option<InlineAssistTarget> {
739 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
740 if terminal_panel
741 .read(cx)
742 .focus_handle(cx)
743 .contains_focused(cx)
744 {
745 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
746 pane.read(cx)
747 .active_item()
748 .and_then(|t| t.downcast::<TerminalView>())
749 }) {
750 return Some(InlineAssistTarget::Terminal(terminal_view));
751 }
752 }
753 }
754 let context_editor =
755 assistant_panel
756 .read(cx)
757 .active_context_editor(cx)
758 .and_then(|editor| {
759 let editor = &editor.read(cx).editor;
760 if editor.read(cx).is_focused(cx) {
761 Some(editor.clone())
762 } else {
763 None
764 }
765 });
766
767 if let Some(context_editor) = context_editor {
768 Some(InlineAssistTarget::Editor(context_editor, false))
769 } else if let Some(workspace_editor) = workspace
770 .active_item(cx)
771 .and_then(|item| item.act_as::<Editor>(cx))
772 {
773 Some(InlineAssistTarget::Editor(workspace_editor, true))
774 } else {
775 None
776 }
777 }
778
779 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
780 if self.project.read(cx).is_remote() {
781 let task = self
782 .context_store
783 .update(cx, |store, cx| store.create_remote_context(cx));
784
785 cx.spawn(|this, mut cx| async move {
786 let context = task.await?;
787
788 this.update(&mut cx, |this, cx| {
789 let workspace = this.workspace.clone();
790 let project = this.project.clone();
791 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
792
793 let fs = this.fs.clone();
794 let project = this.project.clone();
795 let weak_assistant_panel = cx.view().downgrade();
796
797 let editor = cx.new_view(|cx| {
798 ContextEditor::for_context(
799 context,
800 fs,
801 workspace,
802 project,
803 lsp_adapter_delegate,
804 weak_assistant_panel,
805 cx,
806 )
807 });
808
809 this.show_context(editor, cx);
810
811 anyhow::Ok(())
812 })??;
813
814 anyhow::Ok(())
815 })
816 .detach_and_log_err(cx);
817
818 None
819 } else {
820 let context = self.context_store.update(cx, |store, cx| store.create(cx));
821 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
822
823 let assistant_panel = cx.view().downgrade();
824 let editor = cx.new_view(|cx| {
825 let mut editor = ContextEditor::for_context(
826 context,
827 self.fs.clone(),
828 self.workspace.clone(),
829 self.project.clone(),
830 lsp_adapter_delegate,
831 assistant_panel,
832 cx,
833 );
834 editor.insert_default_prompt(cx);
835 editor
836 });
837
838 self.show_context(editor.clone(), cx);
839 Some(editor)
840 }
841 }
842
843 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
844 let focus = self.focus_handle(cx).contains_focused(cx);
845 let prev_len = self.pane.read(cx).items_len();
846 self.pane.update(cx, |pane, cx| {
847 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
848 });
849
850 if prev_len != self.pane.read(cx).items_len() {
851 self.subscriptions
852 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
853 }
854
855 self.show_updated_summary(&context_editor, cx);
856
857 cx.emit(AssistantPanelEvent::ContextEdited);
858 cx.notify();
859 }
860
861 fn show_updated_summary(
862 &self,
863 context_editor: &View<ContextEditor>,
864 cx: &mut ViewContext<Self>,
865 ) {
866 context_editor.update(cx, |context_editor, cx| {
867 let new_summary = context_editor
868 .context
869 .read(cx)
870 .summary()
871 .map(|s| s.text.clone())
872 .unwrap_or_else(|| context_editor.title(cx).to_string());
873 self.model_summary_editor.update(cx, |summary_editor, cx| {
874 if summary_editor.text(cx) != new_summary {
875 summary_editor.set_text(new_summary, cx);
876 }
877 });
878 });
879 }
880
881 fn handle_context_editor_event(
882 &mut self,
883 context_editor: View<ContextEditor>,
884 event: &EditorEvent,
885 cx: &mut ViewContext<Self>,
886 ) {
887 match event {
888 EditorEvent::TitleChanged => {
889 self.show_updated_summary(&context_editor, cx);
890 cx.notify()
891 }
892 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
893 _ => {}
894 }
895 }
896
897 fn show_configuration(
898 workspace: &mut Workspace,
899 _: &ShowConfiguration,
900 cx: &mut ViewContext<Workspace>,
901 ) {
902 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
903 return;
904 };
905
906 if !panel.focus_handle(cx).contains_focused(cx) {
907 workspace.toggle_panel_focus::<AssistantPanel>(cx);
908 }
909
910 panel.update(cx, |this, cx| {
911 this.show_configuration_for_active_provider(cx);
912 })
913 }
914
915 fn show_configuration_for_active_provider(&mut self, cx: &mut ViewContext<Self>) {
916 let provider = LanguageModelRegistry::read_global(cx).active_provider();
917 self.show_configuration_for_provider(provider, cx);
918 }
919
920 fn show_configuration_for_provider(
921 &mut self,
922 provider: Option<Arc<dyn LanguageModelProvider>>,
923 cx: &mut ViewContext<Self>,
924 ) {
925 let configuration_item_ix = self
926 .pane
927 .read(cx)
928 .items()
929 .position(|item| item.downcast::<ConfigurationView>().is_some());
930
931 if let Some(configuration_item_ix) = configuration_item_ix {
932 self.pane.update(cx, |pane, cx| {
933 pane.activate_item(configuration_item_ix, true, true, cx);
934 if let Some((item, provider)) =
935 pane.item_for_index(configuration_item_ix).zip(provider)
936 {
937 if let Some(view) = item.downcast::<ConfigurationView>() {
938 view.update(cx, |view, cx| {
939 view.set_active_tab(provider, cx);
940 });
941 }
942 }
943 });
944 } else {
945 let configuration = cx.new_view(|cx| {
946 let mut view = ConfigurationView::new(self.focus_handle(cx), cx);
947 if let Some(provider) = provider {
948 view.set_active_tab(provider, cx);
949 }
950 view
951 });
952 self.configuration_subscription = Some(cx.subscribe(
953 &configuration,
954 |this, _, event: &ConfigurationViewEvent, cx| match event {
955 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
956 if LanguageModelRegistry::read_global(cx)
957 .active_provider()
958 .map_or(true, |p| p.id() != provider.id())
959 {
960 if let Some(model) = provider.provided_models(cx).first().cloned() {
961 update_settings_file::<AssistantSettings>(
962 this.fs.clone(),
963 cx,
964 move |settings, _| settings.set_model(model),
965 );
966 }
967 }
968
969 this.new_context(cx);
970 }
971 },
972 ));
973 self.pane.update(cx, |pane, cx| {
974 pane.add_item(Box::new(configuration), true, true, None, cx);
975 });
976 }
977 }
978
979 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
980 let history_item_ix = self
981 .pane
982 .read(cx)
983 .items()
984 .position(|item| item.downcast::<ContextHistory>().is_some());
985
986 if let Some(history_item_ix) = history_item_ix {
987 self.pane.update(cx, |pane, cx| {
988 pane.activate_item(history_item_ix, true, true, cx);
989 });
990 } else {
991 let assistant_panel = cx.view().downgrade();
992 let history = cx.new_view(|cx| {
993 ContextHistory::new(
994 self.project.clone(),
995 self.context_store.clone(),
996 assistant_panel,
997 cx,
998 )
999 });
1000 self.pane.update(cx, |pane, cx| {
1001 pane.add_item(Box::new(history), true, true, None, cx);
1002 });
1003 }
1004 }
1005
1006 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1007 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1008 }
1009
1010 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1011 self.model_selector_menu_handle.toggle(cx);
1012 }
1013
1014 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1015 self.pane
1016 .read(cx)
1017 .active_item()?
1018 .downcast::<ContextEditor>()
1019 }
1020
1021 fn has_any_context_editors(&self, cx: &AppContext) -> bool {
1022 self.pane
1023 .read(cx)
1024 .items()
1025 .any(|item| item.downcast::<ContextEditor>().is_some())
1026 }
1027
1028 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1029 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1030 }
1031
1032 fn open_saved_context(
1033 &mut self,
1034 path: PathBuf,
1035 cx: &mut ViewContext<Self>,
1036 ) -> Task<Result<()>> {
1037 let existing_context = self.pane.read(cx).items().find_map(|item| {
1038 item.downcast::<ContextEditor>()
1039 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1040 });
1041 if let Some(existing_context) = existing_context {
1042 return cx.spawn(|this, mut cx| async move {
1043 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1044 });
1045 }
1046
1047 let context = self
1048 .context_store
1049 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1050 let fs = self.fs.clone();
1051 let project = self.project.clone();
1052 let workspace = self.workspace.clone();
1053
1054 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
1055
1056 cx.spawn(|this, mut cx| async move {
1057 let context = context.await?;
1058 let assistant_panel = this.clone();
1059 this.update(&mut cx, |this, cx| {
1060 let editor = cx.new_view(|cx| {
1061 ContextEditor::for_context(
1062 context,
1063 fs,
1064 workspace,
1065 project,
1066 lsp_adapter_delegate,
1067 assistant_panel,
1068 cx,
1069 )
1070 });
1071 this.show_context(editor, cx);
1072 anyhow::Ok(())
1073 })??;
1074 Ok(())
1075 })
1076 }
1077
1078 fn open_remote_context(
1079 &mut self,
1080 id: ContextId,
1081 cx: &mut ViewContext<Self>,
1082 ) -> Task<Result<View<ContextEditor>>> {
1083 let existing_context = self.pane.read(cx).items().find_map(|item| {
1084 item.downcast::<ContextEditor>()
1085 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1086 });
1087 if let Some(existing_context) = existing_context {
1088 return cx.spawn(|this, mut cx| async move {
1089 this.update(&mut cx, |this, cx| {
1090 this.show_context(existing_context.clone(), cx)
1091 })?;
1092 Ok(existing_context)
1093 });
1094 }
1095
1096 let context = self
1097 .context_store
1098 .update(cx, |store, cx| store.open_remote_context(id, cx));
1099 let fs = self.fs.clone();
1100 let workspace = self.workspace.clone();
1101 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
1102
1103 cx.spawn(|this, mut cx| async move {
1104 let context = context.await?;
1105 let assistant_panel = this.clone();
1106 this.update(&mut cx, |this, cx| {
1107 let editor = cx.new_view(|cx| {
1108 ContextEditor::for_context(
1109 context,
1110 fs,
1111 workspace,
1112 this.project.clone(),
1113 lsp_adapter_delegate,
1114 assistant_panel,
1115 cx,
1116 )
1117 });
1118 this.show_context(editor.clone(), cx);
1119 anyhow::Ok(editor)
1120 })?
1121 })
1122 }
1123
1124 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1125 LanguageModelRegistry::read_global(cx)
1126 .active_provider()
1127 .map_or(false, |provider| provider.is_authenticated(cx))
1128 }
1129
1130 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1131 LanguageModelRegistry::read_global(cx)
1132 .active_provider()
1133 .map_or(
1134 Task::ready(Err(anyhow!("no active language model provider"))),
1135 |provider| provider.authenticate(cx),
1136 )
1137 }
1138}
1139
1140impl Render for AssistantPanel {
1141 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1142 let mut registrar = DivRegistrar::new(
1143 |panel, cx| {
1144 panel
1145 .pane
1146 .read(cx)
1147 .toolbar()
1148 .read(cx)
1149 .item_of_type::<BufferSearchBar>()
1150 },
1151 cx,
1152 );
1153 BufferSearchBar::register(&mut registrar);
1154 let registrar = registrar.into_div();
1155
1156 v_flex()
1157 .key_context("AssistantPanel")
1158 .size_full()
1159 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1160 this.new_context(cx);
1161 }))
1162 .on_action(cx.listener(|this, _: &ShowConfiguration, cx| {
1163 this.show_configuration_for_active_provider(cx)
1164 }))
1165 .on_action(cx.listener(AssistantPanel::deploy_history))
1166 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1167 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1168 .child(registrar.size_full().child(self.pane.clone()))
1169 .into_any_element()
1170 }
1171}
1172
1173impl Panel for AssistantPanel {
1174 fn persistent_name() -> &'static str {
1175 "AssistantPanel"
1176 }
1177
1178 fn position(&self, cx: &WindowContext) -> DockPosition {
1179 match AssistantSettings::get_global(cx).dock {
1180 AssistantDockPosition::Left => DockPosition::Left,
1181 AssistantDockPosition::Bottom => DockPosition::Bottom,
1182 AssistantDockPosition::Right => DockPosition::Right,
1183 }
1184 }
1185
1186 fn position_is_valid(&self, _: DockPosition) -> bool {
1187 true
1188 }
1189
1190 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1191 settings::update_settings_file::<AssistantSettings>(
1192 self.fs.clone(),
1193 cx,
1194 move |settings, _| {
1195 let dock = match position {
1196 DockPosition::Left => AssistantDockPosition::Left,
1197 DockPosition::Bottom => AssistantDockPosition::Bottom,
1198 DockPosition::Right => AssistantDockPosition::Right,
1199 };
1200 settings.set_dock(dock);
1201 },
1202 );
1203 }
1204
1205 fn size(&self, cx: &WindowContext) -> Pixels {
1206 let settings = AssistantSettings::get_global(cx);
1207 match self.position(cx) {
1208 DockPosition::Left | DockPosition::Right => {
1209 self.width.unwrap_or(settings.default_width)
1210 }
1211 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1212 }
1213 }
1214
1215 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1216 match self.position(cx) {
1217 DockPosition::Left | DockPosition::Right => self.width = size,
1218 DockPosition::Bottom => self.height = size,
1219 }
1220 cx.notify();
1221 }
1222
1223 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1224 self.pane.read(cx).is_zoomed()
1225 }
1226
1227 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1228 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1229 }
1230
1231 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1232 if active {
1233 if self.pane.read(cx).items_len() == 0 {
1234 if LanguageModelRegistry::read_global(cx)
1235 .active_provider()
1236 .is_none()
1237 {
1238 self.show_configuration_for_provider(None, cx);
1239 } else {
1240 self.new_context(cx);
1241 };
1242 }
1243
1244 self.ensure_authenticated(cx);
1245 }
1246 }
1247
1248 fn pane(&self) -> Option<View<Pane>> {
1249 Some(self.pane.clone())
1250 }
1251
1252 fn remote_id() -> Option<proto::PanelId> {
1253 Some(proto::PanelId::AssistantPanel)
1254 }
1255
1256 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1257 let settings = AssistantSettings::get_global(cx);
1258 if !settings.enabled || !settings.button {
1259 return None;
1260 }
1261
1262 Some(IconName::ZedAssistant)
1263 }
1264
1265 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1266 Some("Assistant Panel")
1267 }
1268
1269 fn toggle_action(&self) -> Box<dyn Action> {
1270 Box::new(ToggleFocus)
1271 }
1272}
1273
1274impl EventEmitter<PanelEvent> for AssistantPanel {}
1275impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1276
1277impl FocusableView for AssistantPanel {
1278 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1279 self.pane.focus_handle(cx)
1280 }
1281}
1282
1283pub enum ContextEditorEvent {
1284 Edited,
1285 TabContentChanged,
1286}
1287
1288#[derive(Copy, Clone, Debug, PartialEq)]
1289struct ScrollPosition {
1290 offset_before_cursor: gpui::Point<f32>,
1291 cursor: Anchor,
1292}
1293
1294struct ActiveEditStep {
1295 start: language::Anchor,
1296 assist_ids: Vec<InlineAssistId>,
1297 editor: Option<WeakView<Editor>>,
1298 _open_editor: Task<Result<()>>,
1299}
1300
1301pub struct ContextEditor {
1302 context: Model<Context>,
1303 fs: Arc<dyn Fs>,
1304 workspace: WeakView<Workspace>,
1305 project: Model<Project>,
1306 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1307 editor: View<Editor>,
1308 blocks: HashSet<CustomBlockId>,
1309 scroll_position: Option<ScrollPosition>,
1310 remote_id: Option<workspace::ViewId>,
1311 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1312 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1313 _subscriptions: Vec<Subscription>,
1314 active_edit_step: Option<ActiveEditStep>,
1315 assistant_panel: WeakView<AssistantPanel>,
1316}
1317
1318const DEFAULT_TAB_TITLE: &str = "New Context";
1319const MAX_TAB_TITLE_LEN: usize = 16;
1320
1321impl ContextEditor {
1322 fn for_context(
1323 context: Model<Context>,
1324 fs: Arc<dyn Fs>,
1325 workspace: WeakView<Workspace>,
1326 project: Model<Project>,
1327 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1328 assistant_panel: WeakView<AssistantPanel>,
1329 cx: &mut ViewContext<Self>,
1330 ) -> Self {
1331 let completion_provider = SlashCommandCompletionProvider::new(
1332 Some(cx.view().downgrade()),
1333 Some(workspace.clone()),
1334 );
1335
1336 let editor = cx.new_view(|cx| {
1337 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1338 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1339 editor.set_show_line_numbers(false, cx);
1340 editor.set_show_git_diff_gutter(false, cx);
1341 editor.set_show_code_actions(false, cx);
1342 editor.set_show_runnables(false, cx);
1343 editor.set_show_wrap_guides(false, cx);
1344 editor.set_show_indent_guides(false, cx);
1345 editor.set_completion_provider(Box::new(completion_provider));
1346 editor.set_collaboration_hub(Box::new(project.clone()));
1347 editor
1348 });
1349
1350 let _subscriptions = vec![
1351 cx.observe(&context, |_, _, cx| cx.notify()),
1352 cx.subscribe(&context, Self::handle_context_event),
1353 cx.subscribe(&editor, Self::handle_editor_event),
1354 cx.subscribe(&editor, Self::handle_editor_search_event),
1355 ];
1356
1357 let sections = context.read(cx).slash_command_output_sections().to_vec();
1358 let mut this = Self {
1359 context,
1360 editor,
1361 lsp_adapter_delegate,
1362 blocks: Default::default(),
1363 scroll_position: None,
1364 remote_id: None,
1365 fs,
1366 workspace,
1367 project,
1368 pending_slash_command_creases: HashMap::default(),
1369 pending_slash_command_blocks: HashMap::default(),
1370 _subscriptions,
1371 active_edit_step: None,
1372 assistant_panel,
1373 };
1374 this.update_message_headers(cx);
1375 this.insert_slash_command_output_sections(sections, cx);
1376 this
1377 }
1378
1379 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1380 let command_name = DefaultSlashCommand.name();
1381 self.editor.update(cx, |editor, cx| {
1382 editor.insert(&format!("/{command_name}"), cx)
1383 });
1384 self.split(&Split, cx);
1385 let command = self.context.update(cx, |context, cx| {
1386 let first_message_id = context.messages(cx).next().unwrap().id;
1387 context.update_metadata(first_message_id, cx, |metadata| {
1388 metadata.role = Role::System;
1389 });
1390 context.reparse_slash_commands(cx);
1391 context.pending_slash_commands()[0].clone()
1392 });
1393
1394 self.run_command(
1395 command.source_range,
1396 &command.name,
1397 command.argument.as_deref(),
1398 false,
1399 self.workspace.clone(),
1400 cx,
1401 );
1402 }
1403
1404 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1405 if !self.apply_edit_step(cx) {
1406 self.send_to_model(cx);
1407 }
1408 }
1409
1410 fn apply_edit_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1411 if let Some(step) = self.active_edit_step.as_ref() {
1412 let assist_ids = step.assist_ids.clone();
1413 cx.window_context().defer(|cx| {
1414 InlineAssistant::update_global(cx, |assistant, cx| {
1415 for assist_id in assist_ids {
1416 assistant.start_assist(assist_id, cx);
1417 }
1418 })
1419 });
1420
1421 !step.assist_ids.is_empty()
1422 } else {
1423 false
1424 }
1425 }
1426
1427 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1428 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1429 let new_selection = {
1430 let cursor = user_message
1431 .start
1432 .to_offset(self.context.read(cx).buffer().read(cx));
1433 cursor..cursor
1434 };
1435 self.editor.update(cx, |editor, cx| {
1436 editor.change_selections(
1437 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1438 cx,
1439 |selections| selections.select_ranges([new_selection]),
1440 );
1441 });
1442 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1443 cx.defer(|this, _| this.scroll_position = None);
1444 }
1445 }
1446
1447 fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1448 if !self
1449 .context
1450 .update(cx, |context, _| context.cancel_last_assist())
1451 {
1452 cx.propagate();
1453 }
1454 }
1455
1456 fn debug_edit_steps(&mut self, _: &DebugEditSteps, cx: &mut ViewContext<Self>) {
1457 let mut output = String::new();
1458 for (i, step) in self.context.read(cx).edit_steps().iter().enumerate() {
1459 output.push_str(&format!("Step {}:\n", i + 1));
1460 output.push_str(&format!(
1461 "Content: {}\n",
1462 self.context
1463 .read(cx)
1464 .buffer()
1465 .read(cx)
1466 .text_for_range(step.source_range.clone())
1467 .collect::<String>()
1468 ));
1469 match &step.state {
1470 Some(EditStepState::Resolved(resolution)) => {
1471 output.push_str("Resolution:\n");
1472 output.push_str(&format!(" {:?}\n", resolution.step_title));
1473 for op in &resolution.operations {
1474 output.push_str(&format!(" {:?}\n", op));
1475 }
1476 }
1477 Some(EditStepState::Pending(_)) => {
1478 output.push_str("Resolution: Pending\n");
1479 }
1480 None => {
1481 output.push_str("Resolution: None\n");
1482 }
1483 }
1484 output.push('\n');
1485 }
1486
1487 let editor = self
1488 .workspace
1489 .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
1490
1491 if let Ok(editor) = editor {
1492 cx.spawn(|_, mut cx| async move {
1493 let editor = editor.await?;
1494 editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
1495 })
1496 .detach_and_notify_err(cx);
1497 }
1498 }
1499
1500 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1501 let cursors = self.cursors(cx);
1502 self.context.update(cx, |context, cx| {
1503 let messages = context
1504 .messages_for_offsets(cursors, cx)
1505 .into_iter()
1506 .map(|message| message.id)
1507 .collect();
1508 context.cycle_message_roles(messages, cx)
1509 });
1510 }
1511
1512 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1513 let selections = self.editor.read(cx).selections.all::<usize>(cx);
1514 selections
1515 .into_iter()
1516 .map(|selection| selection.head())
1517 .collect()
1518 }
1519
1520 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1521 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1522 self.editor.update(cx, |editor, cx| {
1523 editor.transact(cx, |editor, cx| {
1524 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1525 let snapshot = editor.buffer().read(cx).snapshot(cx);
1526 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1527 if newest_cursor.column > 0
1528 || snapshot
1529 .chars_at(newest_cursor)
1530 .next()
1531 .map_or(false, |ch| ch != '\n')
1532 {
1533 editor.move_to_end_of_line(
1534 &MoveToEndOfLine {
1535 stop_at_soft_wraps: false,
1536 },
1537 cx,
1538 );
1539 editor.newline(&Newline, cx);
1540 }
1541
1542 editor.insert(&format!("/{name}"), cx);
1543 if command.requires_argument() {
1544 editor.insert(" ", cx);
1545 editor.show_completions(&ShowCompletions::default(), cx);
1546 }
1547 });
1548 });
1549 if !command.requires_argument() {
1550 self.confirm_command(&ConfirmCommand, cx);
1551 }
1552 }
1553 }
1554
1555 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1556 let selections = self.editor.read(cx).selections.disjoint_anchors();
1557 let mut commands_by_range = HashMap::default();
1558 let workspace = self.workspace.clone();
1559 self.context.update(cx, |context, cx| {
1560 context.reparse_slash_commands(cx);
1561 for selection in selections.iter() {
1562 if let Some(command) =
1563 context.pending_command_for_position(selection.head().text_anchor, cx)
1564 {
1565 commands_by_range
1566 .entry(command.source_range.clone())
1567 .or_insert_with(|| command.clone());
1568 }
1569 }
1570 });
1571
1572 if commands_by_range.is_empty() {
1573 cx.propagate();
1574 } else {
1575 for command in commands_by_range.into_values() {
1576 self.run_command(
1577 command.source_range,
1578 &command.name,
1579 command.argument.as_deref(),
1580 true,
1581 workspace.clone(),
1582 cx,
1583 );
1584 }
1585 cx.stop_propagation();
1586 }
1587 }
1588
1589 pub fn run_command(
1590 &mut self,
1591 command_range: Range<language::Anchor>,
1592 name: &str,
1593 argument: Option<&str>,
1594 insert_trailing_newline: bool,
1595 workspace: WeakView<Workspace>,
1596 cx: &mut ViewContext<Self>,
1597 ) {
1598 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1599 if let Some(lsp_adapter_delegate) = self.lsp_adapter_delegate.clone() {
1600 let argument = argument.map(ToString::to_string);
1601 let output = command.run(argument.as_deref(), workspace, lsp_adapter_delegate, cx);
1602 self.context.update(cx, |context, cx| {
1603 context.insert_command_output(
1604 command_range,
1605 output,
1606 insert_trailing_newline,
1607 cx,
1608 )
1609 });
1610 }
1611 }
1612 }
1613
1614 fn handle_context_event(
1615 &mut self,
1616 _: Model<Context>,
1617 event: &ContextEvent,
1618 cx: &mut ViewContext<Self>,
1619 ) {
1620 let context_editor = cx.view().downgrade();
1621
1622 match event {
1623 ContextEvent::MessagesEdited => {
1624 self.update_message_headers(cx);
1625 self.context.update(cx, |context, cx| {
1626 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1627 });
1628 }
1629 ContextEvent::EditStepsChanged => {
1630 cx.notify();
1631 }
1632 ContextEvent::SummaryChanged => {
1633 cx.emit(EditorEvent::TitleChanged);
1634 self.context.update(cx, |context, cx| {
1635 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1636 });
1637 }
1638 ContextEvent::StreamedCompletion => {
1639 self.editor.update(cx, |editor, cx| {
1640 if let Some(scroll_position) = self.scroll_position {
1641 let snapshot = editor.snapshot(cx);
1642 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1643 let scroll_top =
1644 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1645 editor.set_scroll_position(
1646 point(scroll_position.offset_before_cursor.x, scroll_top),
1647 cx,
1648 );
1649 }
1650 });
1651 }
1652 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1653 self.editor.update(cx, |editor, cx| {
1654 let buffer = editor.buffer().read(cx).snapshot(cx);
1655 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1656 let excerpt_id = *excerpt_id;
1657
1658 editor.remove_creases(
1659 removed
1660 .iter()
1661 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1662 cx,
1663 );
1664
1665 editor.remove_blocks(
1666 HashSet::from_iter(
1667 removed.iter().filter_map(|range| {
1668 self.pending_slash_command_blocks.remove(range)
1669 }),
1670 ),
1671 None,
1672 cx,
1673 );
1674
1675 let crease_ids = editor.insert_creases(
1676 updated.iter().map(|command| {
1677 let workspace = self.workspace.clone();
1678 let confirm_command = Arc::new({
1679 let context_editor = context_editor.clone();
1680 let command = command.clone();
1681 move |cx: &mut WindowContext| {
1682 context_editor
1683 .update(cx, |context_editor, cx| {
1684 context_editor.run_command(
1685 command.source_range.clone(),
1686 &command.name,
1687 command.argument.as_deref(),
1688 false,
1689 workspace.clone(),
1690 cx,
1691 );
1692 })
1693 .ok();
1694 }
1695 });
1696 let placeholder = FoldPlaceholder {
1697 render: Arc::new(move |_, _, _| Empty.into_any()),
1698 constrain_width: false,
1699 merge_adjacent: false,
1700 };
1701 let render_toggle = {
1702 let confirm_command = confirm_command.clone();
1703 let command = command.clone();
1704 move |row, _, _, _cx: &mut WindowContext| {
1705 render_pending_slash_command_gutter_decoration(
1706 row,
1707 &command.status,
1708 confirm_command.clone(),
1709 )
1710 }
1711 };
1712 let render_trailer = {
1713 let command = command.clone();
1714 move |row, _unfold, cx: &mut WindowContext| {
1715 // TODO: In the future we should investigate how we can expose
1716 // this as a hook on the `SlashCommand` trait so that we don't
1717 // need to special-case it here.
1718 if command.name == DocsSlashCommand::NAME {
1719 return render_docs_slash_command_trailer(
1720 row,
1721 command.clone(),
1722 cx,
1723 );
1724 }
1725
1726 Empty.into_any()
1727 }
1728 };
1729
1730 let start = buffer
1731 .anchor_in_excerpt(excerpt_id, command.source_range.start)
1732 .unwrap();
1733 let end = buffer
1734 .anchor_in_excerpt(excerpt_id, command.source_range.end)
1735 .unwrap();
1736 Crease::new(start..end, placeholder, render_toggle, render_trailer)
1737 }),
1738 cx,
1739 );
1740
1741 let block_ids = editor.insert_blocks(
1742 updated
1743 .iter()
1744 .filter_map(|command| match &command.status {
1745 PendingSlashCommandStatus::Error(error) => {
1746 Some((command, error.clone()))
1747 }
1748 _ => None,
1749 })
1750 .map(|(command, error_message)| BlockProperties {
1751 style: BlockStyle::Fixed,
1752 position: Anchor {
1753 buffer_id: Some(buffer_id),
1754 excerpt_id,
1755 text_anchor: command.source_range.start,
1756 },
1757 height: 1,
1758 disposition: BlockDisposition::Below,
1759 render: slash_command_error_block_renderer(error_message),
1760 }),
1761 None,
1762 cx,
1763 );
1764
1765 self.pending_slash_command_creases.extend(
1766 updated
1767 .iter()
1768 .map(|command| command.source_range.clone())
1769 .zip(crease_ids),
1770 );
1771
1772 self.pending_slash_command_blocks.extend(
1773 updated
1774 .iter()
1775 .map(|command| command.source_range.clone())
1776 .zip(block_ids),
1777 );
1778 })
1779 }
1780 ContextEvent::SlashCommandFinished {
1781 output_range,
1782 sections,
1783 run_commands_in_output,
1784 } => {
1785 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
1786
1787 if *run_commands_in_output {
1788 let commands = self.context.update(cx, |context, cx| {
1789 context.reparse_slash_commands(cx);
1790 context
1791 .pending_commands_for_range(output_range.clone(), cx)
1792 .to_vec()
1793 });
1794
1795 for command in commands {
1796 self.run_command(
1797 command.source_range,
1798 &command.name,
1799 command.argument.as_deref(),
1800 false,
1801 self.workspace.clone(),
1802 cx,
1803 );
1804 }
1805 }
1806 }
1807 ContextEvent::Operation(_) => {}
1808 }
1809 }
1810
1811 fn insert_slash_command_output_sections(
1812 &mut self,
1813 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
1814 cx: &mut ViewContext<Self>,
1815 ) {
1816 self.editor.update(cx, |editor, cx| {
1817 let buffer = editor.buffer().read(cx).snapshot(cx);
1818 let excerpt_id = *buffer.as_singleton().unwrap().0;
1819 let mut buffer_rows_to_fold = BTreeSet::new();
1820 let mut creases = Vec::new();
1821 for section in sections {
1822 let start = buffer
1823 .anchor_in_excerpt(excerpt_id, section.range.start)
1824 .unwrap();
1825 let end = buffer
1826 .anchor_in_excerpt(excerpt_id, section.range.end)
1827 .unwrap();
1828 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1829 buffer_rows_to_fold.insert(buffer_row);
1830 creases.push(Crease::new(
1831 start..end,
1832 FoldPlaceholder {
1833 render: Arc::new({
1834 let editor = cx.view().downgrade();
1835 let icon = section.icon;
1836 let label = section.label.clone();
1837 move |fold_id, fold_range, _cx| {
1838 let editor = editor.clone();
1839 ButtonLike::new(fold_id)
1840 .style(ButtonStyle::Filled)
1841 .layer(ElevationIndex::ElevatedSurface)
1842 .child(Icon::new(icon))
1843 .child(Label::new(label.clone()).single_line())
1844 .on_click(move |_, cx| {
1845 editor
1846 .update(cx, |editor, cx| {
1847 let buffer_start = fold_range
1848 .start
1849 .to_point(&editor.buffer().read(cx).read(cx));
1850 let buffer_row = MultiBufferRow(buffer_start.row);
1851 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
1852 })
1853 .ok();
1854 })
1855 .into_any_element()
1856 }
1857 }),
1858 constrain_width: false,
1859 merge_adjacent: false,
1860 },
1861 render_slash_command_output_toggle,
1862 |_, _, _| Empty.into_any_element(),
1863 ));
1864 }
1865
1866 editor.insert_creases(creases, cx);
1867
1868 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1869 editor.fold_at(&FoldAt { buffer_row }, cx);
1870 }
1871 });
1872 }
1873
1874 fn handle_editor_event(
1875 &mut self,
1876 _: View<Editor>,
1877 event: &EditorEvent,
1878 cx: &mut ViewContext<Self>,
1879 ) {
1880 match event {
1881 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
1882 let cursor_scroll_position = self.cursor_scroll_position(cx);
1883 if *autoscroll {
1884 self.scroll_position = cursor_scroll_position;
1885 } else if self.scroll_position != cursor_scroll_position {
1886 self.scroll_position = None;
1887 }
1888 }
1889 EditorEvent::SelectionsChanged { .. } => {
1890 self.scroll_position = self.cursor_scroll_position(cx);
1891 if self
1892 .edit_step_for_cursor(cx)
1893 .map(|step| step.source_range.start)
1894 != self.active_edit_step.as_ref().map(|step| step.start)
1895 {
1896 if let Some(old_active_edit_step) = self.active_edit_step.take() {
1897 if let Some(editor) = old_active_edit_step
1898 .editor
1899 .and_then(|editor| editor.upgrade())
1900 {
1901 self.workspace
1902 .update(cx, |workspace, cx| {
1903 if let Some(pane) = workspace.pane_for(&editor) {
1904 pane.update(cx, |pane, cx| {
1905 let item_id = editor.entity_id();
1906 if pane.is_active_preview_item(item_id) {
1907 pane.close_item_by_id(
1908 item_id,
1909 SaveIntent::Skip,
1910 cx,
1911 )
1912 .detach_and_log_err(cx);
1913 }
1914 });
1915 }
1916 })
1917 .ok();
1918 }
1919 }
1920
1921 if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
1922 let start = new_active_step.source_range.start;
1923 let open_editor = new_active_step
1924 .edit_suggestions(&self.project, cx)
1925 .map(|suggestions| {
1926 self.open_editor_for_edit_suggestions(suggestions, cx)
1927 })
1928 .unwrap_or_else(|| Task::ready(Ok(())));
1929 self.active_edit_step = Some(ActiveEditStep {
1930 start,
1931 assist_ids: Vec::new(),
1932 editor: None,
1933 _open_editor: open_editor,
1934 });
1935 }
1936 }
1937 }
1938 _ => {}
1939 }
1940 cx.emit(event.clone());
1941 }
1942
1943 fn open_editor_for_edit_suggestions(
1944 &mut self,
1945 edit_step_suggestions: Task<EditStepSuggestions>,
1946 cx: &mut ViewContext<Self>,
1947 ) -> Task<Result<()>> {
1948 let workspace = self.workspace.clone();
1949 let project = self.project.clone();
1950 let assistant_panel = self.assistant_panel.clone();
1951 cx.spawn(|this, mut cx| async move {
1952 let edit_step_suggestions = edit_step_suggestions.await;
1953
1954 let mut assist_ids = Vec::new();
1955 let editor = if edit_step_suggestions.suggestions.is_empty() {
1956 return Ok(());
1957 } else if edit_step_suggestions.suggestions.len() == 1
1958 && edit_step_suggestions
1959 .suggestions
1960 .values()
1961 .next()
1962 .unwrap()
1963 .len()
1964 == 1
1965 {
1966 // If there's only one buffer and one suggestion group, open it directly
1967 let (buffer, suggestion_groups) = edit_step_suggestions
1968 .suggestions
1969 .into_iter()
1970 .next()
1971 .unwrap();
1972 let suggestion_group = suggestion_groups.into_iter().next().unwrap();
1973 let editor = workspace.update(&mut cx, |workspace, cx| {
1974 let active_pane = workspace.active_pane().clone();
1975 workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
1976 })?;
1977
1978 cx.update(|cx| {
1979 for suggestion in suggestion_group.suggestions {
1980 let description = suggestion.description.unwrap_or_else(|| "Delete".into());
1981
1982 let range = {
1983 let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
1984 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
1985 multibuffer
1986 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
1987 .unwrap()
1988 ..multibuffer
1989 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
1990 .unwrap()
1991 };
1992
1993 InlineAssistant::update_global(cx, |assistant, cx| {
1994 let suggestion_id = assistant.suggest_assist(
1995 &editor,
1996 range,
1997 description,
1998 suggestion.initial_insertion,
1999 Some(workspace.clone()),
2000 assistant_panel.upgrade().as_ref(),
2001 cx,
2002 );
2003 assist_ids.push(suggestion_id);
2004 });
2005 }
2006
2007 // Scroll the editor to the suggested assist
2008 editor.update(cx, |editor, cx| {
2009 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2010 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2011 let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
2012 {
2013 Anchor::min()
2014 } else {
2015 multibuffer
2016 .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
2017 .unwrap()
2018 };
2019
2020 editor.set_scroll_anchor(
2021 ScrollAnchor {
2022 offset: gpui::Point::default(),
2023 anchor,
2024 },
2025 cx,
2026 );
2027 });
2028 })?;
2029
2030 editor
2031 } else {
2032 // If there are multiple buffers or suggestion groups, create a multibuffer
2033 let mut inline_assist_suggestions = Vec::new();
2034 let multibuffer = cx.new_model(|cx| {
2035 let replica_id = project.read(cx).replica_id();
2036 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2037 .with_title(edit_step_suggestions.title);
2038 for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
2039 let excerpt_ids = multibuffer.push_excerpts(
2040 buffer,
2041 suggestion_groups
2042 .iter()
2043 .map(|suggestion_group| ExcerptRange {
2044 context: suggestion_group.context_range.clone(),
2045 primary: None,
2046 }),
2047 cx,
2048 );
2049
2050 for (excerpt_id, suggestion_group) in
2051 excerpt_ids.into_iter().zip(suggestion_groups)
2052 {
2053 for suggestion in suggestion_group.suggestions {
2054 let description =
2055 suggestion.description.unwrap_or_else(|| "Delete".into());
2056 let range = {
2057 let multibuffer = multibuffer.read(cx);
2058 multibuffer
2059 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
2060 .unwrap()
2061 ..multibuffer
2062 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
2063 .unwrap()
2064 };
2065 inline_assist_suggestions.push((
2066 range,
2067 description,
2068 suggestion.initial_insertion,
2069 ));
2070 }
2071 }
2072 }
2073 multibuffer
2074 })?;
2075
2076 let editor = cx
2077 .new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
2078 cx.update(|cx| {
2079 InlineAssistant::update_global(cx, |assistant, cx| {
2080 for (range, description, initial_insertion) in inline_assist_suggestions {
2081 assist_ids.push(assistant.suggest_assist(
2082 &editor,
2083 range,
2084 description,
2085 initial_insertion,
2086 Some(workspace.clone()),
2087 assistant_panel.upgrade().as_ref(),
2088 cx,
2089 ));
2090 }
2091 })
2092 })?;
2093 workspace.update(&mut cx, |workspace, cx| {
2094 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2095 })?;
2096
2097 editor
2098 };
2099
2100 this.update(&mut cx, |this, _cx| {
2101 if let Some(step) = this.active_edit_step.as_mut() {
2102 step.assist_ids = assist_ids;
2103 step.editor = Some(editor.downgrade());
2104 }
2105 })
2106 })
2107 }
2108
2109 fn handle_editor_search_event(
2110 &mut self,
2111 _: View<Editor>,
2112 event: &SearchEvent,
2113 cx: &mut ViewContext<Self>,
2114 ) {
2115 cx.emit(event.clone());
2116 }
2117
2118 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2119 self.editor.update(cx, |editor, cx| {
2120 let snapshot = editor.snapshot(cx);
2121 let cursor = editor.selections.newest_anchor().head();
2122 let cursor_row = cursor
2123 .to_display_point(&snapshot.display_snapshot)
2124 .row()
2125 .as_f32();
2126 let scroll_position = editor
2127 .scroll_manager
2128 .anchor()
2129 .scroll_position(&snapshot.display_snapshot);
2130
2131 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2132 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2133 Some(ScrollPosition {
2134 cursor,
2135 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2136 })
2137 } else {
2138 None
2139 }
2140 })
2141 }
2142
2143 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2144 self.editor.update(cx, |editor, cx| {
2145 let buffer = editor.buffer().read(cx).snapshot(cx);
2146 let excerpt_id = *buffer.as_singleton().unwrap().0;
2147 let old_blocks = std::mem::take(&mut self.blocks);
2148 let new_blocks = self
2149 .context
2150 .read(cx)
2151 .messages(cx)
2152 .map(|message| BlockProperties {
2153 position: buffer
2154 .anchor_in_excerpt(excerpt_id, message.anchor)
2155 .unwrap(),
2156 height: 2,
2157 style: BlockStyle::Sticky,
2158 render: Box::new({
2159 let context = self.context.clone();
2160 move |cx| {
2161 let message_id = message.id;
2162 let sender = ButtonLike::new("role")
2163 .style(ButtonStyle::Filled)
2164 .child(match message.role {
2165 Role::User => Label::new("You").color(Color::Default),
2166 Role::Assistant => Label::new("Assistant").color(Color::Info),
2167 Role::System => Label::new("System").color(Color::Warning),
2168 })
2169 .tooltip(|cx| {
2170 Tooltip::with_meta(
2171 "Toggle message role",
2172 None,
2173 "Available roles: You (User), Assistant, System",
2174 cx,
2175 )
2176 })
2177 .on_click({
2178 let context = context.clone();
2179 move |_, cx| {
2180 context.update(cx, |context, cx| {
2181 context.cycle_message_roles(
2182 HashSet::from_iter(Some(message_id)),
2183 cx,
2184 )
2185 })
2186 }
2187 });
2188
2189 h_flex()
2190 .id(("message_header", message_id.as_u64()))
2191 .pl(cx.gutter_dimensions.full_width())
2192 .h_11()
2193 .w_full()
2194 .relative()
2195 .gap_1()
2196 .child(sender)
2197 .children(
2198 if let MessageStatus::Error(error) = message.status.clone() {
2199 Some(
2200 div()
2201 .id("error")
2202 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2203 .child(Icon::new(IconName::XCircle)),
2204 )
2205 } else {
2206 None
2207 },
2208 )
2209 .into_any_element()
2210 }
2211 }),
2212 disposition: BlockDisposition::Above,
2213 })
2214 .collect::<Vec<_>>();
2215
2216 editor.remove_blocks(old_blocks, None, cx);
2217 let ids = editor.insert_blocks(new_blocks, None, cx);
2218 self.blocks = HashSet::from_iter(ids);
2219 });
2220 }
2221
2222 fn insert_selection(
2223 workspace: &mut Workspace,
2224 _: &InsertIntoEditor,
2225 cx: &mut ViewContext<Workspace>,
2226 ) {
2227 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2228 return;
2229 };
2230 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2231 return;
2232 };
2233 let Some(active_editor_view) = workspace
2234 .active_item(cx)
2235 .and_then(|item| item.act_as::<Editor>(cx))
2236 else {
2237 return;
2238 };
2239
2240 let context_editor = context_editor_view.read(cx).editor.read(cx);
2241 let anchor = context_editor.selections.newest_anchor();
2242 let text = context_editor
2243 .buffer()
2244 .read(cx)
2245 .read(cx)
2246 .text_for_range(anchor.range())
2247 .collect::<String>();
2248
2249 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2250 if !text.is_empty() {
2251 active_editor_view.update(cx, |editor, cx| {
2252 editor.insert(&text, cx);
2253 editor.focus(cx);
2254 })
2255 }
2256 }
2257
2258 fn quote_selection(
2259 workspace: &mut Workspace,
2260 _: &QuoteSelection,
2261 cx: &mut ViewContext<Workspace>,
2262 ) {
2263 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2264 return;
2265 };
2266 let Some(editor) = workspace
2267 .active_item(cx)
2268 .and_then(|item| item.act_as::<Editor>(cx))
2269 else {
2270 return;
2271 };
2272
2273 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2274 let editor = editor.read(cx);
2275 let buffer = editor.buffer().read(cx).snapshot(cx);
2276 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2277 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2278 let start_language = buffer.language_at(range.start);
2279 let end_language = buffer.language_at(range.end);
2280 let language_name = if start_language == end_language {
2281 start_language.map(|language| language.code_fence_block_name())
2282 } else {
2283 None
2284 };
2285 let language_name = language_name.as_deref().unwrap_or("");
2286
2287 let selected_text = buffer.text_for_range(range).collect::<String>();
2288 let text = if selected_text.is_empty() {
2289 None
2290 } else {
2291 Some(if language_name == "markdown" {
2292 selected_text
2293 .lines()
2294 .map(|line| format!("> {}", line))
2295 .collect::<Vec<_>>()
2296 .join("\n")
2297 } else {
2298 format!("```{language_name}\n{selected_text}\n```")
2299 })
2300 };
2301
2302 // Activate the panel
2303 if !panel.focus_handle(cx).contains_focused(cx) {
2304 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2305 }
2306
2307 if let Some(text) = text {
2308 panel.update(cx, |_, cx| {
2309 // Wait to create a new context until the workspace is no longer
2310 // being updated.
2311 cx.defer(move |panel, cx| {
2312 if let Some(context) = panel
2313 .active_context_editor(cx)
2314 .or_else(|| panel.new_context(cx))
2315 {
2316 context.update(cx, |context, cx| {
2317 context
2318 .editor
2319 .update(cx, |editor, cx| editor.insert(&text, cx))
2320 });
2321 };
2322 });
2323 });
2324 }
2325 }
2326
2327 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2328 let editor = self.editor.read(cx);
2329 let context = self.context.read(cx);
2330 if editor.selections.count() == 1 {
2331 let selection = editor.selections.newest::<usize>(cx);
2332 let mut copied_text = String::new();
2333 let mut spanned_messages = 0;
2334 for message in context.messages(cx) {
2335 if message.offset_range.start >= selection.range().end {
2336 break;
2337 } else if message.offset_range.end >= selection.range().start {
2338 let range = cmp::max(message.offset_range.start, selection.range().start)
2339 ..cmp::min(message.offset_range.end, selection.range().end);
2340 if !range.is_empty() {
2341 spanned_messages += 1;
2342 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2343 for chunk in context.buffer().read(cx).text_for_range(range) {
2344 copied_text.push_str(chunk);
2345 }
2346 copied_text.push('\n');
2347 }
2348 }
2349 }
2350
2351 if spanned_messages > 1 {
2352 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2353 return;
2354 }
2355 }
2356
2357 cx.propagate();
2358 }
2359
2360 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2361 self.context.update(cx, |context, cx| {
2362 let selections = self.editor.read(cx).selections.disjoint_anchors();
2363 for selection in selections.as_ref() {
2364 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2365 let range = selection
2366 .map(|endpoint| endpoint.to_offset(&buffer))
2367 .range();
2368 context.split_message(range, cx);
2369 }
2370 });
2371 }
2372
2373 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2374 self.context.update(cx, |context, cx| {
2375 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2376 });
2377 }
2378
2379 fn title(&self, cx: &AppContext) -> Cow<str> {
2380 self.context
2381 .read(cx)
2382 .summary()
2383 .map(|summary| summary.text.clone())
2384 .map(Cow::Owned)
2385 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2386 }
2387
2388 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2389 let focus_handle = self.focus_handle(cx).clone();
2390 let button_text = match self.edit_step_for_cursor(cx) {
2391 Some(edit_step) => match &edit_step.state {
2392 Some(EditStepState::Pending(_)) => "Computing Changes...",
2393 Some(EditStepState::Resolved(_)) => "Apply Changes",
2394 None => "Send",
2395 },
2396 None => "Send",
2397 };
2398
2399 let (style, tooltip) = match token_state(&self.context, cx) {
2400 Some(TokenState::NoTokensLeft { .. }) => (
2401 ButtonStyle::Tinted(TintColor::Negative),
2402 Some(Tooltip::text("Token limit reached", cx)),
2403 ),
2404 Some(TokenState::HasMoreTokens {
2405 over_warn_threshold,
2406 ..
2407 }) => {
2408 let (style, tooltip) = if over_warn_threshold {
2409 (
2410 ButtonStyle::Tinted(TintColor::Warning),
2411 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
2412 )
2413 } else {
2414 (ButtonStyle::Filled, None)
2415 };
2416 (style, tooltip)
2417 }
2418 None => (ButtonStyle::Filled, None),
2419 };
2420
2421 ButtonLike::new("send_button")
2422 .style(style)
2423 .when_some(tooltip, |button, tooltip| {
2424 button.tooltip(move |_| tooltip.clone())
2425 })
2426 .layer(ElevationIndex::ModalSurface)
2427 .children(
2428 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2429 .map(|binding| binding.into_any_element()),
2430 )
2431 .child(Label::new(button_text))
2432 .on_click(move |_event, cx| {
2433 focus_handle.dispatch_action(&Assist, cx);
2434 })
2435 }
2436
2437 fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
2438 let newest_cursor = self
2439 .editor
2440 .read(cx)
2441 .selections
2442 .newest_anchor()
2443 .head()
2444 .text_anchor;
2445 let context = self.context.read(cx);
2446 let buffer = context.buffer().read(cx);
2447
2448 let edit_steps = context.edit_steps();
2449 edit_steps
2450 .binary_search_by(|step| {
2451 let step_range = step.source_range.clone();
2452 if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
2453 Ordering::Greater
2454 } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
2455 Ordering::Less
2456 } else {
2457 Ordering::Equal
2458 }
2459 })
2460 .ok()
2461 .map(|index| &edit_steps[index])
2462 }
2463}
2464
2465impl EventEmitter<EditorEvent> for ContextEditor {}
2466impl EventEmitter<SearchEvent> for ContextEditor {}
2467
2468impl Render for ContextEditor {
2469 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2470 div()
2471 .key_context("ContextEditor")
2472 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
2473 .capture_action(cx.listener(ContextEditor::save))
2474 .capture_action(cx.listener(ContextEditor::copy))
2475 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2476 .capture_action(cx.listener(ContextEditor::confirm_command))
2477 .on_action(cx.listener(ContextEditor::assist))
2478 .on_action(cx.listener(ContextEditor::split))
2479 .on_action(cx.listener(ContextEditor::debug_edit_steps))
2480 .size_full()
2481 .v_flex()
2482 .child(
2483 div()
2484 .flex_grow()
2485 .bg(cx.theme().colors().editor_background)
2486 .child(self.editor.clone())
2487 .child(
2488 h_flex()
2489 .w_full()
2490 .absolute()
2491 .bottom_0()
2492 .p_4()
2493 .justify_end()
2494 .child(self.render_send_button(cx)),
2495 ),
2496 )
2497 }
2498}
2499
2500impl FocusableView for ContextEditor {
2501 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2502 self.editor.focus_handle(cx)
2503 }
2504}
2505
2506impl Item for ContextEditor {
2507 type Event = editor::EditorEvent;
2508
2509 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
2510 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2511 }
2512
2513 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2514 match event {
2515 EditorEvent::Edited { .. } => {
2516 f(item::ItemEvent::Edit);
2517 }
2518 EditorEvent::TitleChanged => {
2519 f(item::ItemEvent::UpdateTab);
2520 }
2521 _ => {}
2522 }
2523 }
2524
2525 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2526 Some(self.title(cx).to_string().into())
2527 }
2528
2529 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2530 Some(Box::new(handle.clone()))
2531 }
2532
2533 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2534 self.editor.update(cx, |editor, cx| {
2535 Item::set_nav_history(editor, nav_history, cx)
2536 })
2537 }
2538
2539 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2540 self.editor
2541 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2542 }
2543
2544 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2545 self.editor
2546 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2547 }
2548}
2549
2550impl SearchableItem for ContextEditor {
2551 type Match = <Editor as SearchableItem>::Match;
2552
2553 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2554 self.editor.update(cx, |editor, cx| {
2555 editor.clear_matches(cx);
2556 });
2557 }
2558
2559 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2560 self.editor
2561 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2562 }
2563
2564 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2565 self.editor
2566 .update(cx, |editor, cx| editor.query_suggestion(cx))
2567 }
2568
2569 fn activate_match(
2570 &mut self,
2571 index: usize,
2572 matches: &[Self::Match],
2573 cx: &mut ViewContext<Self>,
2574 ) {
2575 self.editor.update(cx, |editor, cx| {
2576 editor.activate_match(index, matches, cx);
2577 });
2578 }
2579
2580 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2581 self.editor
2582 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2583 }
2584
2585 fn replace(
2586 &mut self,
2587 identifier: &Self::Match,
2588 query: &project::search::SearchQuery,
2589 cx: &mut ViewContext<Self>,
2590 ) {
2591 self.editor
2592 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2593 }
2594
2595 fn find_matches(
2596 &mut self,
2597 query: Arc<project::search::SearchQuery>,
2598 cx: &mut ViewContext<Self>,
2599 ) -> Task<Vec<Self::Match>> {
2600 self.editor
2601 .update(cx, |editor, cx| editor.find_matches(query, cx))
2602 }
2603
2604 fn active_match_index(
2605 &mut self,
2606 matches: &[Self::Match],
2607 cx: &mut ViewContext<Self>,
2608 ) -> Option<usize> {
2609 self.editor
2610 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2611 }
2612}
2613
2614impl FollowableItem for ContextEditor {
2615 fn remote_id(&self) -> Option<workspace::ViewId> {
2616 self.remote_id
2617 }
2618
2619 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2620 let context = self.context.read(cx);
2621 Some(proto::view::Variant::ContextEditor(
2622 proto::view::ContextEditor {
2623 context_id: context.id().to_proto(),
2624 editor: if let Some(proto::view::Variant::Editor(proto)) =
2625 self.editor.read(cx).to_state_proto(cx)
2626 {
2627 Some(proto)
2628 } else {
2629 None
2630 },
2631 },
2632 ))
2633 }
2634
2635 fn from_state_proto(
2636 workspace: View<Workspace>,
2637 id: workspace::ViewId,
2638 state: &mut Option<proto::view::Variant>,
2639 cx: &mut WindowContext,
2640 ) -> Option<Task<Result<View<Self>>>> {
2641 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2642 return None;
2643 };
2644 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2645 unreachable!()
2646 };
2647
2648 let context_id = ContextId::from_proto(state.context_id);
2649 let editor_state = state.editor?;
2650
2651 let (project, panel) = workspace.update(cx, |workspace, cx| {
2652 Some((
2653 workspace.project().clone(),
2654 workspace.panel::<AssistantPanel>(cx)?,
2655 ))
2656 })?;
2657
2658 let context_editor =
2659 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2660
2661 Some(cx.spawn(|mut cx| async move {
2662 let context_editor = context_editor.await?;
2663 context_editor
2664 .update(&mut cx, |context_editor, cx| {
2665 context_editor.remote_id = Some(id);
2666 context_editor.editor.update(cx, |editor, cx| {
2667 editor.apply_update_proto(
2668 &project,
2669 proto::update_view::Variant::Editor(proto::update_view::Editor {
2670 selections: editor_state.selections,
2671 pending_selection: editor_state.pending_selection,
2672 scroll_top_anchor: editor_state.scroll_top_anchor,
2673 scroll_x: editor_state.scroll_y,
2674 scroll_y: editor_state.scroll_y,
2675 ..Default::default()
2676 }),
2677 cx,
2678 )
2679 })
2680 })?
2681 .await?;
2682 Ok(context_editor)
2683 }))
2684 }
2685
2686 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2687 Editor::to_follow_event(event)
2688 }
2689
2690 fn add_event_to_update_proto(
2691 &self,
2692 event: &Self::Event,
2693 update: &mut Option<proto::update_view::Variant>,
2694 cx: &WindowContext,
2695 ) -> bool {
2696 self.editor
2697 .read(cx)
2698 .add_event_to_update_proto(event, update, cx)
2699 }
2700
2701 fn apply_update_proto(
2702 &mut self,
2703 project: &Model<Project>,
2704 message: proto::update_view::Variant,
2705 cx: &mut ViewContext<Self>,
2706 ) -> Task<Result<()>> {
2707 self.editor.update(cx, |editor, cx| {
2708 editor.apply_update_proto(project, message, cx)
2709 })
2710 }
2711
2712 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2713 true
2714 }
2715
2716 fn set_leader_peer_id(
2717 &mut self,
2718 leader_peer_id: Option<proto::PeerId>,
2719 cx: &mut ViewContext<Self>,
2720 ) {
2721 self.editor.update(cx, |editor, cx| {
2722 editor.set_leader_peer_id(leader_peer_id, cx)
2723 })
2724 }
2725
2726 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2727 if existing.context.read(cx).id() == self.context.read(cx).id() {
2728 Some(item::Dedup::KeepExisting)
2729 } else {
2730 None
2731 }
2732 }
2733}
2734
2735pub struct ContextEditorToolbarItem {
2736 fs: Arc<dyn Fs>,
2737 workspace: WeakView<Workspace>,
2738 active_context_editor: Option<WeakView<ContextEditor>>,
2739 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2740 model_summary_editor: View<Editor>,
2741}
2742
2743impl ContextEditorToolbarItem {
2744 pub fn new(
2745 workspace: &Workspace,
2746 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2747 model_summary_editor: View<Editor>,
2748 ) -> Self {
2749 Self {
2750 fs: workspace.app_state().fs.clone(),
2751 workspace: workspace.weak_handle(),
2752 active_context_editor: None,
2753 model_selector_menu_handle,
2754 model_summary_editor,
2755 }
2756 }
2757
2758 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2759 let commands = SlashCommandRegistry::global(cx);
2760 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2761 Some(
2762 workspace
2763 .read(cx)
2764 .active_item_as::<Editor>(cx)?
2765 .focus_handle(cx),
2766 )
2767 });
2768 let active_context_editor = self.active_context_editor.clone();
2769
2770 PopoverMenu::new("inject-context-menu")
2771 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
2772 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
2773 }))
2774 .menu(move |cx| {
2775 let active_context_editor = active_context_editor.clone()?;
2776 ContextMenu::build(cx, |mut menu, _cx| {
2777 for command_name in commands.featured_command_names() {
2778 if let Some(command) = commands.command(&command_name) {
2779 let menu_text = SharedString::from(Arc::from(command.menu_text()));
2780 menu = menu.custom_entry(
2781 {
2782 let command_name = command_name.clone();
2783 move |_cx| {
2784 h_flex()
2785 .gap_4()
2786 .w_full()
2787 .justify_between()
2788 .child(Label::new(menu_text.clone()))
2789 .child(
2790 Label::new(format!("/{command_name}"))
2791 .color(Color::Muted),
2792 )
2793 .into_any()
2794 }
2795 },
2796 {
2797 let active_context_editor = active_context_editor.clone();
2798 move |cx| {
2799 active_context_editor
2800 .update(cx, |context_editor, cx| {
2801 context_editor.insert_command(&command_name, cx)
2802 })
2803 .ok();
2804 }
2805 },
2806 )
2807 }
2808 }
2809
2810 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
2811 menu = menu
2812 .context(active_editor_focus_handle)
2813 .action("Quote Selection", Box::new(QuoteSelection));
2814 }
2815
2816 menu
2817 })
2818 .into()
2819 })
2820 }
2821
2822 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2823 let context = &self
2824 .active_context_editor
2825 .as_ref()?
2826 .upgrade()?
2827 .read(cx)
2828 .context;
2829 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
2830 TokenState::NoTokensLeft {
2831 max_token_count,
2832 token_count,
2833 } => (Color::Error, token_count, max_token_count),
2834 TokenState::HasMoreTokens {
2835 max_token_count,
2836 token_count,
2837 over_warn_threshold,
2838 } => {
2839 let color = if over_warn_threshold {
2840 Color::Warning
2841 } else {
2842 Color::Muted
2843 };
2844 (color, token_count, max_token_count)
2845 }
2846 };
2847 Some(
2848 h_flex()
2849 .gap_0p5()
2850 .child(
2851 Label::new(humanize_token_count(token_count))
2852 .size(LabelSize::Small)
2853 .color(token_count_color),
2854 )
2855 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2856 .child(
2857 Label::new(humanize_token_count(max_token_count))
2858 .size(LabelSize::Small)
2859 .color(Color::Muted),
2860 ),
2861 )
2862 }
2863}
2864
2865impl Render for ContextEditorToolbarItem {
2866 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2867 let left_side = h_flex()
2868 .gap_2()
2869 .flex_1()
2870 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
2871 .when(self.active_context_editor.is_some(), |left_side| {
2872 left_side
2873 .child(
2874 IconButton::new("regenerate-context", IconName::ArrowCircle)
2875 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
2876 .on_click(cx.listener(move |_, _, cx| {
2877 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
2878 })),
2879 )
2880 .child(self.model_summary_editor.clone())
2881 });
2882 let right_side = h_flex()
2883 .gap_2()
2884 .child(
2885 ModelSelector::new(
2886 self.fs.clone(),
2887 ButtonLike::new("active-model")
2888 .style(ButtonStyle::Subtle)
2889 .child(
2890 h_flex()
2891 .w_full()
2892 .gap_0p5()
2893 .child(
2894 div()
2895 .overflow_x_hidden()
2896 .flex_grow()
2897 .whitespace_nowrap()
2898 .child(
2899 Label::new(
2900 LanguageModelRegistry::read_global(cx)
2901 .active_model()
2902 .map(|model| {
2903 format!(
2904 "{}: {}",
2905 model.provider_name().0,
2906 model.name().0
2907 )
2908 })
2909 .unwrap_or_else(|| "No model selected".into()),
2910 )
2911 .size(LabelSize::Small)
2912 .color(Color::Muted),
2913 ),
2914 )
2915 .child(
2916 Icon::new(IconName::ChevronDown)
2917 .color(Color::Muted)
2918 .size(IconSize::XSmall),
2919 ),
2920 )
2921 .tooltip(move |cx| {
2922 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
2923 }),
2924 )
2925 .with_handle(self.model_selector_menu_handle.clone()),
2926 )
2927 .children(self.render_remaining_tokens(cx))
2928 .child(self.render_inject_context_menu(cx));
2929
2930 h_flex()
2931 .size_full()
2932 .justify_between()
2933 .child(left_side)
2934 .child(right_side)
2935 }
2936}
2937
2938impl ToolbarItemView for ContextEditorToolbarItem {
2939 fn set_active_pane_item(
2940 &mut self,
2941 active_pane_item: Option<&dyn ItemHandle>,
2942 cx: &mut ViewContext<Self>,
2943 ) -> ToolbarItemLocation {
2944 self.active_context_editor = active_pane_item
2945 .and_then(|item| item.act_as::<ContextEditor>(cx))
2946 .map(|editor| editor.downgrade());
2947 cx.notify();
2948 if self.active_context_editor.is_none() {
2949 ToolbarItemLocation::Hidden
2950 } else {
2951 ToolbarItemLocation::PrimaryRight
2952 }
2953 }
2954
2955 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
2956 cx.notify();
2957 }
2958}
2959
2960impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
2961
2962enum ContextEditorToolbarItemEvent {
2963 RegenerateSummary,
2964}
2965impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
2966
2967pub struct ContextHistory {
2968 picker: View<Picker<SavedContextPickerDelegate>>,
2969 _subscriptions: Vec<Subscription>,
2970 assistant_panel: WeakView<AssistantPanel>,
2971}
2972
2973impl ContextHistory {
2974 fn new(
2975 project: Model<Project>,
2976 context_store: Model<ContextStore>,
2977 assistant_panel: WeakView<AssistantPanel>,
2978 cx: &mut ViewContext<Self>,
2979 ) -> Self {
2980 let picker = cx.new_view(|cx| {
2981 Picker::uniform_list(
2982 SavedContextPickerDelegate::new(project, context_store.clone()),
2983 cx,
2984 )
2985 .modal(false)
2986 .max_height(None)
2987 });
2988
2989 let _subscriptions = vec![
2990 cx.observe(&context_store, |this, _, cx| {
2991 this.picker.update(cx, |picker, cx| picker.refresh(cx));
2992 }),
2993 cx.subscribe(&picker, Self::handle_picker_event),
2994 ];
2995
2996 Self {
2997 picker,
2998 _subscriptions,
2999 assistant_panel,
3000 }
3001 }
3002
3003 fn handle_picker_event(
3004 &mut self,
3005 _: View<Picker<SavedContextPickerDelegate>>,
3006 event: &SavedContextPickerEvent,
3007 cx: &mut ViewContext<Self>,
3008 ) {
3009 let SavedContextPickerEvent::Confirmed(context) = event;
3010 self.assistant_panel
3011 .update(cx, |assistant_panel, cx| match context {
3012 ContextMetadata::Remote(metadata) => {
3013 assistant_panel
3014 .open_remote_context(metadata.id.clone(), cx)
3015 .detach_and_log_err(cx);
3016 }
3017 ContextMetadata::Saved(metadata) => {
3018 assistant_panel
3019 .open_saved_context(metadata.path.clone(), cx)
3020 .detach_and_log_err(cx);
3021 }
3022 })
3023 .ok();
3024 }
3025}
3026
3027impl Render for ContextHistory {
3028 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3029 div().size_full().child(self.picker.clone())
3030 }
3031}
3032
3033impl FocusableView for ContextHistory {
3034 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3035 self.picker.focus_handle(cx)
3036 }
3037}
3038
3039impl EventEmitter<()> for ContextHistory {}
3040
3041impl Item for ContextHistory {
3042 type Event = ();
3043
3044 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3045 Some("History".into())
3046 }
3047}
3048
3049pub struct ConfigurationView {
3050 fallback_handle: FocusHandle,
3051 active_tab: Option<ActiveTab>,
3052}
3053
3054struct ActiveTab {
3055 provider: Arc<dyn LanguageModelProvider>,
3056 configuration_prompt: AnyView,
3057 focus_handle: Option<FocusHandle>,
3058 load_credentials_task: Option<Task<()>>,
3059}
3060
3061impl ActiveTab {
3062 fn is_loading_credentials(&self) -> bool {
3063 if let Some(task) = &self.load_credentials_task {
3064 if let Task::Spawned(_) = task {
3065 return true;
3066 }
3067 }
3068 false
3069 }
3070}
3071
3072impl ConfigurationView {
3073 fn new(fallback_handle: FocusHandle, _cx: &mut ViewContext<Self>) -> Self {
3074 Self {
3075 fallback_handle,
3076 active_tab: None,
3077 }
3078 }
3079
3080 fn set_active_tab(
3081 &mut self,
3082 provider: Arc<dyn LanguageModelProvider>,
3083 cx: &mut ViewContext<Self>,
3084 ) {
3085 let (view, focus_handle) = provider.configuration_view(cx);
3086
3087 if let Some(focus_handle) = &focus_handle {
3088 focus_handle.focus(cx);
3089 } else {
3090 self.fallback_handle.focus(cx);
3091 }
3092
3093 let load_credentials = provider.authenticate(cx);
3094 let load_credentials_task = cx.spawn(|this, mut cx| async move {
3095 let _ = load_credentials.await;
3096 this.update(&mut cx, |this, cx| {
3097 if let Some(active_tab) = &mut this.active_tab {
3098 active_tab.load_credentials_task = None;
3099 cx.notify();
3100 }
3101 })
3102 .log_err();
3103 });
3104
3105 self.active_tab = Some(ActiveTab {
3106 provider,
3107 configuration_prompt: view,
3108 focus_handle,
3109 load_credentials_task: Some(load_credentials_task),
3110 });
3111 cx.notify();
3112 }
3113
3114 fn render_active_tab_view(&mut self, cx: &mut ViewContext<Self>) -> Option<Div> {
3115 let Some(active_tab) = &self.active_tab else {
3116 return None;
3117 };
3118
3119 let provider = active_tab.provider.clone();
3120 let provider_name = provider.name().0.clone();
3121
3122 let show_spinner = active_tab.is_loading_credentials();
3123
3124 let content = if show_spinner {
3125 let loading_icon = svg()
3126 .size_4()
3127 .path(IconName::ArrowCircle.path())
3128 .text_color(cx.text_style().color)
3129 .with_animation(
3130 "icon_circle_arrow",
3131 Animation::new(Duration::from_secs(2)).repeat(),
3132 |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
3133 );
3134
3135 h_flex()
3136 .gap_2()
3137 .child(loading_icon)
3138 .child(Label::new("Loading provider configuration...").size(LabelSize::Small))
3139 .into_any_element()
3140 } else {
3141 active_tab.configuration_prompt.clone().into_any_element()
3142 };
3143
3144 Some(
3145 v_flex()
3146 .gap_4()
3147 .child(
3148 div()
3149 .p(Spacing::Large.rems(cx))
3150 .bg(cx.theme().colors().title_bar_background)
3151 .border_1()
3152 .border_color(cx.theme().colors().border_variant)
3153 .rounded_md()
3154 .child(content),
3155 )
3156 .when(
3157 !show_spinner && provider.is_authenticated(cx),
3158 move |this| {
3159 this.child(
3160 h_flex().justify_end().child(
3161 Button::new(
3162 "new-context",
3163 format!("Open new context using {}", provider_name),
3164 )
3165 .icon_position(IconPosition::Start)
3166 .icon(IconName::Plus)
3167 .style(ButtonStyle::Filled)
3168 .layer(ElevationIndex::ModalSurface)
3169 .on_click(cx.listener(
3170 move |_, _, cx| {
3171 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3172 provider.clone(),
3173 ))
3174 },
3175 )),
3176 ),
3177 )
3178 },
3179 ),
3180 )
3181 }
3182
3183 fn render_tab(
3184 &self,
3185 provider: &Arc<dyn LanguageModelProvider>,
3186 cx: &mut ViewContext<Self>,
3187 ) -> impl IntoElement {
3188 let button_id = SharedString::from(format!("tab-{}", provider.id().0));
3189 let is_active = self.active_tab.as_ref().map(|t| t.provider.id()) == Some(provider.id());
3190 ButtonLike::new(button_id)
3191 .size(ButtonSize::Compact)
3192 .style(ButtonStyle::Transparent)
3193 .selected(is_active)
3194 .on_click(cx.listener({
3195 let provider = provider.clone();
3196 move |this, _, cx| {
3197 this.set_active_tab(provider.clone(), cx);
3198 }
3199 }))
3200 .child(
3201 div()
3202 .my_3()
3203 .pb_px()
3204 .border_b_1()
3205 .border_color(if is_active {
3206 cx.theme().colors().text_accent
3207 } else {
3208 cx.theme().colors().border_transparent
3209 })
3210 .when(!is_active, |this| {
3211 this.group_hover("", |this| {
3212 this.border_color(cx.theme().colors().border_variant)
3213 })
3214 })
3215 .child(Label::new(provider.name().0).size(LabelSize::Small).color(
3216 if is_active {
3217 Color::Accent
3218 } else {
3219 Color::Default
3220 },
3221 )),
3222 )
3223 }
3224}
3225
3226impl Render for ConfigurationView {
3227 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3228 let providers = LanguageModelRegistry::read_global(cx).providers();
3229
3230 if self.active_tab.is_none() && !providers.is_empty() {
3231 self.set_active_tab(providers[0].clone(), cx);
3232 }
3233
3234 let tabs = h_flex().mx_neg_1().gap_3().children(
3235 providers
3236 .iter()
3237 .map(|provider| self.render_tab(provider, cx)),
3238 );
3239
3240 v_flex()
3241 .id("assistant-configuration-view")
3242 .w_full()
3243 .min_h_full()
3244 .p(Spacing::XXLarge.rems(cx))
3245 .overflow_y_scroll()
3246 .gap_6()
3247 .child(
3248 v_flex()
3249 .gap_2()
3250 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)),
3251 )
3252 .child(
3253 v_flex()
3254 .gap_2()
3255 .child(Headline::new("Configure providers").size(HeadlineSize::Small))
3256 .child(
3257 Label::new(
3258 "At least one provider must be configured to use the assistant.",
3259 )
3260 .color(Color::Muted),
3261 )
3262 .child(tabs)
3263 .children(self.render_active_tab_view(cx)),
3264 )
3265 }
3266}
3267
3268pub enum ConfigurationViewEvent {
3269 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
3270}
3271
3272impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
3273
3274impl FocusableView for ConfigurationView {
3275 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3276 self.active_tab
3277 .as_ref()
3278 .and_then(|tab| tab.focus_handle.clone())
3279 .unwrap_or(self.fallback_handle.clone())
3280 }
3281}
3282
3283impl Item for ConfigurationView {
3284 type Event = ConfigurationViewEvent;
3285
3286 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3287 Some("Configuration".into())
3288 }
3289}
3290
3291type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3292
3293fn render_slash_command_output_toggle(
3294 row: MultiBufferRow,
3295 is_folded: bool,
3296 fold: ToggleFold,
3297 _cx: &mut WindowContext,
3298) -> AnyElement {
3299 Disclosure::new(
3300 ("slash-command-output-fold-indicator", row.0 as u64),
3301 !is_folded,
3302 )
3303 .selected(is_folded)
3304 .on_click(move |_e, cx| fold(!is_folded, cx))
3305 .into_any_element()
3306}
3307
3308fn render_pending_slash_command_gutter_decoration(
3309 row: MultiBufferRow,
3310 status: &PendingSlashCommandStatus,
3311 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3312) -> AnyElement {
3313 let mut icon = IconButton::new(
3314 ("slash-command-gutter-decoration", row.0),
3315 ui::IconName::TriangleRight,
3316 )
3317 .on_click(move |_e, cx| confirm_command(cx))
3318 .icon_size(ui::IconSize::Small)
3319 .size(ui::ButtonSize::None);
3320
3321 match status {
3322 PendingSlashCommandStatus::Idle => {
3323 icon = icon.icon_color(Color::Muted);
3324 }
3325 PendingSlashCommandStatus::Running { .. } => {
3326 icon = icon.selected(true);
3327 }
3328 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3329 }
3330
3331 icon.into_any_element()
3332}
3333
3334fn render_docs_slash_command_trailer(
3335 row: MultiBufferRow,
3336 command: PendingSlashCommand,
3337 cx: &mut WindowContext,
3338) -> AnyElement {
3339 let Some(argument) = command.argument else {
3340 return Empty.into_any();
3341 };
3342
3343 let args = DocsSlashCommandArgs::parse(&argument);
3344
3345 let Some(store) = args
3346 .provider()
3347 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3348 else {
3349 return Empty.into_any();
3350 };
3351
3352 let Some(package) = args.package() else {
3353 return Empty.into_any();
3354 };
3355
3356 let mut children = Vec::new();
3357
3358 if store.is_indexing(&package) {
3359 children.push(
3360 div()
3361 .id(("crates-being-indexed", row.0))
3362 .child(Icon::new(IconName::ArrowCircle).with_animation(
3363 "arrow-circle",
3364 Animation::new(Duration::from_secs(4)).repeat(),
3365 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3366 ))
3367 .tooltip({
3368 let package = package.clone();
3369 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
3370 })
3371 .into_any_element(),
3372 );
3373 }
3374
3375 if let Some(latest_error) = store.latest_error_for_package(&package) {
3376 children.push(
3377 div()
3378 .id(("latest-error", row.0))
3379 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
3380 .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
3381 .into_any_element(),
3382 )
3383 }
3384
3385 let is_indexing = store.is_indexing(&package);
3386 let latest_error = store.latest_error_for_package(&package);
3387
3388 if !is_indexing && latest_error.is_none() {
3389 return Empty.into_any();
3390 }
3391
3392 h_flex().gap_2().children(children).into_any_element()
3393}
3394
3395fn make_lsp_adapter_delegate(
3396 project: &Model<Project>,
3397 cx: &mut AppContext,
3398) -> Result<Arc<dyn LspAdapterDelegate>> {
3399 project.update(cx, |project, cx| {
3400 // TODO: Find the right worktree.
3401 let worktree = project
3402 .worktrees(cx)
3403 .next()
3404 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3405 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3406 })
3407}
3408
3409fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3410 Box::new(move |_| {
3411 div()
3412 .pl_6()
3413 .child(
3414 Label::new(format!("error: {}", message))
3415 .single_line()
3416 .color(Color::Error),
3417 )
3418 .into_any()
3419 })
3420}
3421
3422enum TokenState {
3423 NoTokensLeft {
3424 max_token_count: usize,
3425 token_count: usize,
3426 },
3427 HasMoreTokens {
3428 max_token_count: usize,
3429 token_count: usize,
3430 over_warn_threshold: bool,
3431 },
3432}
3433
3434fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
3435 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3436
3437 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3438 let token_count = context.read(cx).token_count()?;
3439 let max_token_count = model.max_token_count();
3440
3441 let remaining_tokens = max_token_count as isize - token_count as isize;
3442 let token_state = if remaining_tokens <= 0 {
3443 TokenState::NoTokensLeft {
3444 max_token_count,
3445 token_count,
3446 }
3447 } else {
3448 let over_warn_threshold =
3449 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3450 TokenState::HasMoreTokens {
3451 max_token_count,
3452 token_count,
3453 over_warn_threshold,
3454 }
3455 };
3456 Some(token_state)
3457}