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(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 let argument = argument.map(ToString::to_string);
1600 let output = command.run(
1601 argument.as_deref(),
1602 workspace,
1603 self.lsp_adapter_delegate.clone(),
1604 cx,
1605 );
1606 self.context.update(cx, |context, cx| {
1607 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
1608 });
1609 }
1610 }
1611
1612 fn handle_context_event(
1613 &mut self,
1614 _: Model<Context>,
1615 event: &ContextEvent,
1616 cx: &mut ViewContext<Self>,
1617 ) {
1618 let context_editor = cx.view().downgrade();
1619
1620 match event {
1621 ContextEvent::MessagesEdited => {
1622 self.update_message_headers(cx);
1623 self.context.update(cx, |context, cx| {
1624 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1625 });
1626 }
1627 ContextEvent::EditStepsChanged => {
1628 cx.notify();
1629 }
1630 ContextEvent::SummaryChanged => {
1631 cx.emit(EditorEvent::TitleChanged);
1632 self.context.update(cx, |context, cx| {
1633 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1634 });
1635 }
1636 ContextEvent::StreamedCompletion => {
1637 self.editor.update(cx, |editor, cx| {
1638 if let Some(scroll_position) = self.scroll_position {
1639 let snapshot = editor.snapshot(cx);
1640 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1641 let scroll_top =
1642 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1643 editor.set_scroll_position(
1644 point(scroll_position.offset_before_cursor.x, scroll_top),
1645 cx,
1646 );
1647 }
1648 });
1649 }
1650 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
1651 self.editor.update(cx, |editor, cx| {
1652 let buffer = editor.buffer().read(cx).snapshot(cx);
1653 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
1654 let excerpt_id = *excerpt_id;
1655
1656 editor.remove_creases(
1657 removed
1658 .iter()
1659 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
1660 cx,
1661 );
1662
1663 editor.remove_blocks(
1664 HashSet::from_iter(
1665 removed.iter().filter_map(|range| {
1666 self.pending_slash_command_blocks.remove(range)
1667 }),
1668 ),
1669 None,
1670 cx,
1671 );
1672
1673 let crease_ids = editor.insert_creases(
1674 updated.iter().map(|command| {
1675 let workspace = self.workspace.clone();
1676 let confirm_command = Arc::new({
1677 let context_editor = context_editor.clone();
1678 let command = command.clone();
1679 move |cx: &mut WindowContext| {
1680 context_editor
1681 .update(cx, |context_editor, cx| {
1682 context_editor.run_command(
1683 command.source_range.clone(),
1684 &command.name,
1685 command.argument.as_deref(),
1686 false,
1687 workspace.clone(),
1688 cx,
1689 );
1690 })
1691 .ok();
1692 }
1693 });
1694 let placeholder = FoldPlaceholder {
1695 render: Arc::new(move |_, _, _| Empty.into_any()),
1696 constrain_width: false,
1697 merge_adjacent: false,
1698 };
1699 let render_toggle = {
1700 let confirm_command = confirm_command.clone();
1701 let command = command.clone();
1702 move |row, _, _, _cx: &mut WindowContext| {
1703 render_pending_slash_command_gutter_decoration(
1704 row,
1705 &command.status,
1706 confirm_command.clone(),
1707 )
1708 }
1709 };
1710 let render_trailer = {
1711 let command = command.clone();
1712 move |row, _unfold, cx: &mut WindowContext| {
1713 // TODO: In the future we should investigate how we can expose
1714 // this as a hook on the `SlashCommand` trait so that we don't
1715 // need to special-case it here.
1716 if command.name == DocsSlashCommand::NAME {
1717 return render_docs_slash_command_trailer(
1718 row,
1719 command.clone(),
1720 cx,
1721 );
1722 }
1723
1724 Empty.into_any()
1725 }
1726 };
1727
1728 let start = buffer
1729 .anchor_in_excerpt(excerpt_id, command.source_range.start)
1730 .unwrap();
1731 let end = buffer
1732 .anchor_in_excerpt(excerpt_id, command.source_range.end)
1733 .unwrap();
1734 Crease::new(start..end, placeholder, render_toggle, render_trailer)
1735 }),
1736 cx,
1737 );
1738
1739 let block_ids = editor.insert_blocks(
1740 updated
1741 .iter()
1742 .filter_map(|command| match &command.status {
1743 PendingSlashCommandStatus::Error(error) => {
1744 Some((command, error.clone()))
1745 }
1746 _ => None,
1747 })
1748 .map(|(command, error_message)| BlockProperties {
1749 style: BlockStyle::Fixed,
1750 position: Anchor {
1751 buffer_id: Some(buffer_id),
1752 excerpt_id,
1753 text_anchor: command.source_range.start,
1754 },
1755 height: 1,
1756 disposition: BlockDisposition::Below,
1757 render: slash_command_error_block_renderer(error_message),
1758 }),
1759 None,
1760 cx,
1761 );
1762
1763 self.pending_slash_command_creases.extend(
1764 updated
1765 .iter()
1766 .map(|command| command.source_range.clone())
1767 .zip(crease_ids),
1768 );
1769
1770 self.pending_slash_command_blocks.extend(
1771 updated
1772 .iter()
1773 .map(|command| command.source_range.clone())
1774 .zip(block_ids),
1775 );
1776 })
1777 }
1778 ContextEvent::SlashCommandFinished {
1779 output_range,
1780 sections,
1781 run_commands_in_output,
1782 } => {
1783 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
1784
1785 if *run_commands_in_output {
1786 let commands = self.context.update(cx, |context, cx| {
1787 context.reparse_slash_commands(cx);
1788 context
1789 .pending_commands_for_range(output_range.clone(), cx)
1790 .to_vec()
1791 });
1792
1793 for command in commands {
1794 self.run_command(
1795 command.source_range,
1796 &command.name,
1797 command.argument.as_deref(),
1798 false,
1799 self.workspace.clone(),
1800 cx,
1801 );
1802 }
1803 }
1804 }
1805 ContextEvent::Operation(_) => {}
1806 }
1807 }
1808
1809 fn insert_slash_command_output_sections(
1810 &mut self,
1811 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
1812 cx: &mut ViewContext<Self>,
1813 ) {
1814 self.editor.update(cx, |editor, cx| {
1815 let buffer = editor.buffer().read(cx).snapshot(cx);
1816 let excerpt_id = *buffer.as_singleton().unwrap().0;
1817 let mut buffer_rows_to_fold = BTreeSet::new();
1818 let mut creases = Vec::new();
1819 for section in sections {
1820 let start = buffer
1821 .anchor_in_excerpt(excerpt_id, section.range.start)
1822 .unwrap();
1823 let end = buffer
1824 .anchor_in_excerpt(excerpt_id, section.range.end)
1825 .unwrap();
1826 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
1827 buffer_rows_to_fold.insert(buffer_row);
1828 creases.push(Crease::new(
1829 start..end,
1830 FoldPlaceholder {
1831 render: Arc::new({
1832 let editor = cx.view().downgrade();
1833 let icon = section.icon;
1834 let label = section.label.clone();
1835 move |fold_id, fold_range, _cx| {
1836 let editor = editor.clone();
1837 ButtonLike::new(fold_id)
1838 .style(ButtonStyle::Filled)
1839 .layer(ElevationIndex::ElevatedSurface)
1840 .child(Icon::new(icon))
1841 .child(Label::new(label.clone()).single_line())
1842 .on_click(move |_, cx| {
1843 editor
1844 .update(cx, |editor, cx| {
1845 let buffer_start = fold_range
1846 .start
1847 .to_point(&editor.buffer().read(cx).read(cx));
1848 let buffer_row = MultiBufferRow(buffer_start.row);
1849 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
1850 })
1851 .ok();
1852 })
1853 .into_any_element()
1854 }
1855 }),
1856 constrain_width: false,
1857 merge_adjacent: false,
1858 },
1859 render_slash_command_output_toggle,
1860 |_, _, _| Empty.into_any_element(),
1861 ));
1862 }
1863
1864 editor.insert_creases(creases, cx);
1865
1866 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
1867 editor.fold_at(&FoldAt { buffer_row }, cx);
1868 }
1869 });
1870 }
1871
1872 fn handle_editor_event(
1873 &mut self,
1874 _: View<Editor>,
1875 event: &EditorEvent,
1876 cx: &mut ViewContext<Self>,
1877 ) {
1878 match event {
1879 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
1880 let cursor_scroll_position = self.cursor_scroll_position(cx);
1881 if *autoscroll {
1882 self.scroll_position = cursor_scroll_position;
1883 } else if self.scroll_position != cursor_scroll_position {
1884 self.scroll_position = None;
1885 }
1886 }
1887 EditorEvent::SelectionsChanged { .. } => {
1888 self.scroll_position = self.cursor_scroll_position(cx);
1889 if self
1890 .edit_step_for_cursor(cx)
1891 .map(|step| step.source_range.start)
1892 != self.active_edit_step.as_ref().map(|step| step.start)
1893 {
1894 if let Some(old_active_edit_step) = self.active_edit_step.take() {
1895 if let Some(editor) = old_active_edit_step
1896 .editor
1897 .and_then(|editor| editor.upgrade())
1898 {
1899 self.workspace
1900 .update(cx, |workspace, cx| {
1901 if let Some(pane) = workspace.pane_for(&editor) {
1902 pane.update(cx, |pane, cx| {
1903 let item_id = editor.entity_id();
1904 if pane.is_active_preview_item(item_id) {
1905 pane.close_item_by_id(
1906 item_id,
1907 SaveIntent::Skip,
1908 cx,
1909 )
1910 .detach_and_log_err(cx);
1911 }
1912 });
1913 }
1914 })
1915 .ok();
1916 }
1917 }
1918
1919 if let Some(new_active_step) = self.edit_step_for_cursor(cx) {
1920 let start = new_active_step.source_range.start;
1921 let open_editor = new_active_step
1922 .edit_suggestions(&self.project, cx)
1923 .map(|suggestions| {
1924 self.open_editor_for_edit_suggestions(suggestions, cx)
1925 })
1926 .unwrap_or_else(|| Task::ready(Ok(())));
1927 self.active_edit_step = Some(ActiveEditStep {
1928 start,
1929 assist_ids: Vec::new(),
1930 editor: None,
1931 _open_editor: open_editor,
1932 });
1933 }
1934 }
1935 }
1936 _ => {}
1937 }
1938 cx.emit(event.clone());
1939 }
1940
1941 fn open_editor_for_edit_suggestions(
1942 &mut self,
1943 edit_step_suggestions: Task<EditStepSuggestions>,
1944 cx: &mut ViewContext<Self>,
1945 ) -> Task<Result<()>> {
1946 let workspace = self.workspace.clone();
1947 let project = self.project.clone();
1948 let assistant_panel = self.assistant_panel.clone();
1949 cx.spawn(|this, mut cx| async move {
1950 let edit_step_suggestions = edit_step_suggestions.await;
1951
1952 let mut assist_ids = Vec::new();
1953 let editor = if edit_step_suggestions.suggestions.is_empty() {
1954 return Ok(());
1955 } else if edit_step_suggestions.suggestions.len() == 1
1956 && edit_step_suggestions
1957 .suggestions
1958 .values()
1959 .next()
1960 .unwrap()
1961 .len()
1962 == 1
1963 {
1964 // If there's only one buffer and one suggestion group, open it directly
1965 let (buffer, suggestion_groups) = edit_step_suggestions
1966 .suggestions
1967 .into_iter()
1968 .next()
1969 .unwrap();
1970 let suggestion_group = suggestion_groups.into_iter().next().unwrap();
1971 let editor = workspace.update(&mut cx, |workspace, cx| {
1972 let active_pane = workspace.active_pane().clone();
1973 workspace.open_project_item::<Editor>(active_pane, buffer, false, false, cx)
1974 })?;
1975
1976 cx.update(|cx| {
1977 for suggestion in suggestion_group.suggestions {
1978 let description = suggestion.description.unwrap_or_else(|| "Delete".into());
1979
1980 let range = {
1981 let multibuffer = editor.read(cx).buffer().read(cx).read(cx);
1982 let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap();
1983 multibuffer
1984 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
1985 .unwrap()
1986 ..multibuffer
1987 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
1988 .unwrap()
1989 };
1990
1991 InlineAssistant::update_global(cx, |assistant, cx| {
1992 let suggestion_id = assistant.suggest_assist(
1993 &editor,
1994 range,
1995 description,
1996 suggestion.initial_insertion,
1997 Some(workspace.clone()),
1998 assistant_panel.upgrade().as_ref(),
1999 cx,
2000 );
2001 assist_ids.push(suggestion_id);
2002 });
2003 }
2004
2005 // Scroll the editor to the suggested assist
2006 editor.update(cx, |editor, cx| {
2007 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2008 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2009 let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0
2010 {
2011 Anchor::min()
2012 } else {
2013 multibuffer
2014 .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start)
2015 .unwrap()
2016 };
2017
2018 editor.set_scroll_anchor(
2019 ScrollAnchor {
2020 offset: gpui::Point::default(),
2021 anchor,
2022 },
2023 cx,
2024 );
2025 });
2026 })?;
2027
2028 editor
2029 } else {
2030 // If there are multiple buffers or suggestion groups, create a multibuffer
2031 let mut inline_assist_suggestions = Vec::new();
2032 let multibuffer = cx.new_model(|cx| {
2033 let replica_id = project.read(cx).replica_id();
2034 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2035 .with_title(edit_step_suggestions.title);
2036 for (buffer, suggestion_groups) in edit_step_suggestions.suggestions {
2037 let excerpt_ids = multibuffer.push_excerpts(
2038 buffer,
2039 suggestion_groups
2040 .iter()
2041 .map(|suggestion_group| ExcerptRange {
2042 context: suggestion_group.context_range.clone(),
2043 primary: None,
2044 }),
2045 cx,
2046 );
2047
2048 for (excerpt_id, suggestion_group) in
2049 excerpt_ids.into_iter().zip(suggestion_groups)
2050 {
2051 for suggestion in suggestion_group.suggestions {
2052 let description =
2053 suggestion.description.unwrap_or_else(|| "Delete".into());
2054 let range = {
2055 let multibuffer = multibuffer.read(cx);
2056 multibuffer
2057 .anchor_in_excerpt(excerpt_id, suggestion.range.start)
2058 .unwrap()
2059 ..multibuffer
2060 .anchor_in_excerpt(excerpt_id, suggestion.range.end)
2061 .unwrap()
2062 };
2063 inline_assist_suggestions.push((
2064 range,
2065 description,
2066 suggestion.initial_insertion,
2067 ));
2068 }
2069 }
2070 }
2071 multibuffer
2072 })?;
2073
2074 let editor = cx
2075 .new_view(|cx| Editor::for_multibuffer(multibuffer, Some(project), true, cx))?;
2076 cx.update(|cx| {
2077 InlineAssistant::update_global(cx, |assistant, cx| {
2078 for (range, description, initial_insertion) in inline_assist_suggestions {
2079 assist_ids.push(assistant.suggest_assist(
2080 &editor,
2081 range,
2082 description,
2083 initial_insertion,
2084 Some(workspace.clone()),
2085 assistant_panel.upgrade().as_ref(),
2086 cx,
2087 ));
2088 }
2089 })
2090 })?;
2091 workspace.update(&mut cx, |workspace, cx| {
2092 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2093 })?;
2094
2095 editor
2096 };
2097
2098 this.update(&mut cx, |this, _cx| {
2099 if let Some(step) = this.active_edit_step.as_mut() {
2100 step.assist_ids = assist_ids;
2101 step.editor = Some(editor.downgrade());
2102 }
2103 })
2104 })
2105 }
2106
2107 fn handle_editor_search_event(
2108 &mut self,
2109 _: View<Editor>,
2110 event: &SearchEvent,
2111 cx: &mut ViewContext<Self>,
2112 ) {
2113 cx.emit(event.clone());
2114 }
2115
2116 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2117 self.editor.update(cx, |editor, cx| {
2118 let snapshot = editor.snapshot(cx);
2119 let cursor = editor.selections.newest_anchor().head();
2120 let cursor_row = cursor
2121 .to_display_point(&snapshot.display_snapshot)
2122 .row()
2123 .as_f32();
2124 let scroll_position = editor
2125 .scroll_manager
2126 .anchor()
2127 .scroll_position(&snapshot.display_snapshot);
2128
2129 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2130 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2131 Some(ScrollPosition {
2132 cursor,
2133 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2134 })
2135 } else {
2136 None
2137 }
2138 })
2139 }
2140
2141 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2142 self.editor.update(cx, |editor, cx| {
2143 let buffer = editor.buffer().read(cx).snapshot(cx);
2144 let excerpt_id = *buffer.as_singleton().unwrap().0;
2145 let old_blocks = std::mem::take(&mut self.blocks);
2146 let new_blocks = self
2147 .context
2148 .read(cx)
2149 .messages(cx)
2150 .map(|message| BlockProperties {
2151 position: buffer
2152 .anchor_in_excerpt(excerpt_id, message.anchor)
2153 .unwrap(),
2154 height: 2,
2155 style: BlockStyle::Sticky,
2156 render: Box::new({
2157 let context = self.context.clone();
2158 move |cx| {
2159 let message_id = message.id;
2160 let sender = ButtonLike::new("role")
2161 .style(ButtonStyle::Filled)
2162 .child(match message.role {
2163 Role::User => Label::new("You").color(Color::Default),
2164 Role::Assistant => Label::new("Assistant").color(Color::Info),
2165 Role::System => Label::new("System").color(Color::Warning),
2166 })
2167 .tooltip(|cx| {
2168 Tooltip::with_meta(
2169 "Toggle message role",
2170 None,
2171 "Available roles: You (User), Assistant, System",
2172 cx,
2173 )
2174 })
2175 .on_click({
2176 let context = context.clone();
2177 move |_, cx| {
2178 context.update(cx, |context, cx| {
2179 context.cycle_message_roles(
2180 HashSet::from_iter(Some(message_id)),
2181 cx,
2182 )
2183 })
2184 }
2185 });
2186
2187 h_flex()
2188 .id(("message_header", message_id.as_u64()))
2189 .pl(cx.gutter_dimensions.full_width())
2190 .h_11()
2191 .w_full()
2192 .relative()
2193 .gap_1()
2194 .child(sender)
2195 .children(
2196 if let MessageStatus::Error(error) = message.status.clone() {
2197 Some(
2198 div()
2199 .id("error")
2200 .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2201 .child(Icon::new(IconName::XCircle)),
2202 )
2203 } else {
2204 None
2205 },
2206 )
2207 .into_any_element()
2208 }
2209 }),
2210 disposition: BlockDisposition::Above,
2211 })
2212 .collect::<Vec<_>>();
2213
2214 editor.remove_blocks(old_blocks, None, cx);
2215 let ids = editor.insert_blocks(new_blocks, None, cx);
2216 self.blocks = HashSet::from_iter(ids);
2217 });
2218 }
2219
2220 fn insert_selection(
2221 workspace: &mut Workspace,
2222 _: &InsertIntoEditor,
2223 cx: &mut ViewContext<Workspace>,
2224 ) {
2225 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2226 return;
2227 };
2228 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2229 return;
2230 };
2231 let Some(active_editor_view) = workspace
2232 .active_item(cx)
2233 .and_then(|item| item.act_as::<Editor>(cx))
2234 else {
2235 return;
2236 };
2237
2238 let context_editor = context_editor_view.read(cx).editor.read(cx);
2239 let anchor = context_editor.selections.newest_anchor();
2240 let text = context_editor
2241 .buffer()
2242 .read(cx)
2243 .read(cx)
2244 .text_for_range(anchor.range())
2245 .collect::<String>();
2246
2247 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2248 if !text.is_empty() {
2249 active_editor_view.update(cx, |editor, cx| {
2250 editor.insert(&text, cx);
2251 editor.focus(cx);
2252 })
2253 }
2254 }
2255
2256 fn quote_selection(
2257 workspace: &mut Workspace,
2258 _: &QuoteSelection,
2259 cx: &mut ViewContext<Workspace>,
2260 ) {
2261 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2262 return;
2263 };
2264 let Some(editor) = workspace
2265 .active_item(cx)
2266 .and_then(|item| item.act_as::<Editor>(cx))
2267 else {
2268 return;
2269 };
2270
2271 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2272 let editor = editor.read(cx);
2273 let buffer = editor.buffer().read(cx).snapshot(cx);
2274 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2275 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2276 let start_language = buffer.language_at(range.start);
2277 let end_language = buffer.language_at(range.end);
2278 let language_name = if start_language == end_language {
2279 start_language.map(|language| language.code_fence_block_name())
2280 } else {
2281 None
2282 };
2283 let language_name = language_name.as_deref().unwrap_or("");
2284
2285 let selected_text = buffer.text_for_range(range).collect::<String>();
2286 let text = if selected_text.is_empty() {
2287 None
2288 } else {
2289 Some(if language_name == "markdown" {
2290 selected_text
2291 .lines()
2292 .map(|line| format!("> {}", line))
2293 .collect::<Vec<_>>()
2294 .join("\n")
2295 } else {
2296 format!("```{language_name}\n{selected_text}\n```")
2297 })
2298 };
2299
2300 // Activate the panel
2301 if !panel.focus_handle(cx).contains_focused(cx) {
2302 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2303 }
2304
2305 if let Some(text) = text {
2306 panel.update(cx, |_, cx| {
2307 // Wait to create a new context until the workspace is no longer
2308 // being updated.
2309 cx.defer(move |panel, cx| {
2310 if let Some(context) = panel
2311 .active_context_editor(cx)
2312 .or_else(|| panel.new_context(cx))
2313 {
2314 context.update(cx, |context, cx| {
2315 context
2316 .editor
2317 .update(cx, |editor, cx| editor.insert(&text, cx))
2318 });
2319 };
2320 });
2321 });
2322 }
2323 }
2324
2325 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2326 let editor = self.editor.read(cx);
2327 let context = self.context.read(cx);
2328 if editor.selections.count() == 1 {
2329 let selection = editor.selections.newest::<usize>(cx);
2330 let mut copied_text = String::new();
2331 let mut spanned_messages = 0;
2332 for message in context.messages(cx) {
2333 if message.offset_range.start >= selection.range().end {
2334 break;
2335 } else if message.offset_range.end >= selection.range().start {
2336 let range = cmp::max(message.offset_range.start, selection.range().start)
2337 ..cmp::min(message.offset_range.end, selection.range().end);
2338 if !range.is_empty() {
2339 spanned_messages += 1;
2340 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2341 for chunk in context.buffer().read(cx).text_for_range(range) {
2342 copied_text.push_str(chunk);
2343 }
2344 copied_text.push('\n');
2345 }
2346 }
2347 }
2348
2349 if spanned_messages > 1 {
2350 cx.write_to_clipboard(ClipboardItem::new(copied_text));
2351 return;
2352 }
2353 }
2354
2355 cx.propagate();
2356 }
2357
2358 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2359 self.context.update(cx, |context, cx| {
2360 let selections = self.editor.read(cx).selections.disjoint_anchors();
2361 for selection in selections.as_ref() {
2362 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2363 let range = selection
2364 .map(|endpoint| endpoint.to_offset(&buffer))
2365 .range();
2366 context.split_message(range, cx);
2367 }
2368 });
2369 }
2370
2371 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2372 self.context.update(cx, |context, cx| {
2373 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
2374 });
2375 }
2376
2377 fn title(&self, cx: &AppContext) -> Cow<str> {
2378 self.context
2379 .read(cx)
2380 .summary()
2381 .map(|summary| summary.text.clone())
2382 .map(Cow::Owned)
2383 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
2384 }
2385
2386 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2387 let focus_handle = self.focus_handle(cx).clone();
2388 let button_text = match self.edit_step_for_cursor(cx) {
2389 Some(edit_step) => match &edit_step.state {
2390 Some(EditStepState::Pending(_)) => "Computing Changes...",
2391 Some(EditStepState::Resolved(_)) => "Apply Changes",
2392 None => "Send",
2393 },
2394 None => "Send",
2395 };
2396
2397 let (style, tooltip) = match token_state(&self.context, cx) {
2398 Some(TokenState::NoTokensLeft { .. }) => (
2399 ButtonStyle::Tinted(TintColor::Negative),
2400 Some(Tooltip::text("Token limit reached", cx)),
2401 ),
2402 Some(TokenState::HasMoreTokens {
2403 over_warn_threshold,
2404 ..
2405 }) => {
2406 let (style, tooltip) = if over_warn_threshold {
2407 (
2408 ButtonStyle::Tinted(TintColor::Warning),
2409 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
2410 )
2411 } else {
2412 (ButtonStyle::Filled, None)
2413 };
2414 (style, tooltip)
2415 }
2416 None => (ButtonStyle::Filled, None),
2417 };
2418
2419 ButtonLike::new("send_button")
2420 .style(style)
2421 .when_some(tooltip, |button, tooltip| {
2422 button.tooltip(move |_| tooltip.clone())
2423 })
2424 .layer(ElevationIndex::ModalSurface)
2425 .children(
2426 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
2427 .map(|binding| binding.into_any_element()),
2428 )
2429 .child(Label::new(button_text))
2430 .on_click(move |_event, cx| {
2431 focus_handle.dispatch_action(&Assist, cx);
2432 })
2433 }
2434
2435 fn edit_step_for_cursor<'a>(&'a self, cx: &'a AppContext) -> Option<&'a EditStep> {
2436 let newest_cursor = self
2437 .editor
2438 .read(cx)
2439 .selections
2440 .newest_anchor()
2441 .head()
2442 .text_anchor;
2443 let context = self.context.read(cx);
2444 let buffer = context.buffer().read(cx);
2445
2446 let edit_steps = context.edit_steps();
2447 edit_steps
2448 .binary_search_by(|step| {
2449 let step_range = step.source_range.clone();
2450 if newest_cursor.cmp(&step_range.start, buffer).is_lt() {
2451 Ordering::Greater
2452 } else if newest_cursor.cmp(&step_range.end, buffer).is_gt() {
2453 Ordering::Less
2454 } else {
2455 Ordering::Equal
2456 }
2457 })
2458 .ok()
2459 .map(|index| &edit_steps[index])
2460 }
2461}
2462
2463impl EventEmitter<EditorEvent> for ContextEditor {}
2464impl EventEmitter<SearchEvent> for ContextEditor {}
2465
2466impl Render for ContextEditor {
2467 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2468 div()
2469 .key_context("ContextEditor")
2470 .capture_action(cx.listener(ContextEditor::cancel_last_assist))
2471 .capture_action(cx.listener(ContextEditor::save))
2472 .capture_action(cx.listener(ContextEditor::copy))
2473 .capture_action(cx.listener(ContextEditor::cycle_message_role))
2474 .capture_action(cx.listener(ContextEditor::confirm_command))
2475 .on_action(cx.listener(ContextEditor::assist))
2476 .on_action(cx.listener(ContextEditor::split))
2477 .on_action(cx.listener(ContextEditor::debug_edit_steps))
2478 .size_full()
2479 .v_flex()
2480 .child(
2481 div()
2482 .flex_grow()
2483 .bg(cx.theme().colors().editor_background)
2484 .child(self.editor.clone())
2485 .child(
2486 h_flex()
2487 .w_full()
2488 .absolute()
2489 .bottom_0()
2490 .p_4()
2491 .justify_end()
2492 .child(self.render_send_button(cx)),
2493 ),
2494 )
2495 }
2496}
2497
2498impl FocusableView for ContextEditor {
2499 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2500 self.editor.focus_handle(cx)
2501 }
2502}
2503
2504impl Item for ContextEditor {
2505 type Event = editor::EditorEvent;
2506
2507 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
2508 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
2509 }
2510
2511 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
2512 match event {
2513 EditorEvent::Edited { .. } => {
2514 f(item::ItemEvent::Edit);
2515 }
2516 EditorEvent::TitleChanged => {
2517 f(item::ItemEvent::UpdateTab);
2518 }
2519 _ => {}
2520 }
2521 }
2522
2523 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
2524 Some(self.title(cx).to_string().into())
2525 }
2526
2527 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
2528 Some(Box::new(handle.clone()))
2529 }
2530
2531 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
2532 self.editor.update(cx, |editor, cx| {
2533 Item::set_nav_history(editor, nav_history, cx)
2534 })
2535 }
2536
2537 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
2538 self.editor
2539 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
2540 }
2541
2542 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
2543 self.editor
2544 .update(cx, |editor, cx| Item::deactivated(editor, cx))
2545 }
2546}
2547
2548impl SearchableItem for ContextEditor {
2549 type Match = <Editor as SearchableItem>::Match;
2550
2551 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
2552 self.editor.update(cx, |editor, cx| {
2553 editor.clear_matches(cx);
2554 });
2555 }
2556
2557 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2558 self.editor
2559 .update(cx, |editor, cx| editor.update_matches(matches, cx));
2560 }
2561
2562 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
2563 self.editor
2564 .update(cx, |editor, cx| editor.query_suggestion(cx))
2565 }
2566
2567 fn activate_match(
2568 &mut self,
2569 index: usize,
2570 matches: &[Self::Match],
2571 cx: &mut ViewContext<Self>,
2572 ) {
2573 self.editor.update(cx, |editor, cx| {
2574 editor.activate_match(index, matches, cx);
2575 });
2576 }
2577
2578 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
2579 self.editor
2580 .update(cx, |editor, cx| editor.select_matches(matches, cx));
2581 }
2582
2583 fn replace(
2584 &mut self,
2585 identifier: &Self::Match,
2586 query: &project::search::SearchQuery,
2587 cx: &mut ViewContext<Self>,
2588 ) {
2589 self.editor
2590 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
2591 }
2592
2593 fn find_matches(
2594 &mut self,
2595 query: Arc<project::search::SearchQuery>,
2596 cx: &mut ViewContext<Self>,
2597 ) -> Task<Vec<Self::Match>> {
2598 self.editor
2599 .update(cx, |editor, cx| editor.find_matches(query, cx))
2600 }
2601
2602 fn active_match_index(
2603 &mut self,
2604 matches: &[Self::Match],
2605 cx: &mut ViewContext<Self>,
2606 ) -> Option<usize> {
2607 self.editor
2608 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
2609 }
2610}
2611
2612impl FollowableItem for ContextEditor {
2613 fn remote_id(&self) -> Option<workspace::ViewId> {
2614 self.remote_id
2615 }
2616
2617 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
2618 let context = self.context.read(cx);
2619 Some(proto::view::Variant::ContextEditor(
2620 proto::view::ContextEditor {
2621 context_id: context.id().to_proto(),
2622 editor: if let Some(proto::view::Variant::Editor(proto)) =
2623 self.editor.read(cx).to_state_proto(cx)
2624 {
2625 Some(proto)
2626 } else {
2627 None
2628 },
2629 },
2630 ))
2631 }
2632
2633 fn from_state_proto(
2634 workspace: View<Workspace>,
2635 id: workspace::ViewId,
2636 state: &mut Option<proto::view::Variant>,
2637 cx: &mut WindowContext,
2638 ) -> Option<Task<Result<View<Self>>>> {
2639 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
2640 return None;
2641 };
2642 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
2643 unreachable!()
2644 };
2645
2646 let context_id = ContextId::from_proto(state.context_id);
2647 let editor_state = state.editor?;
2648
2649 let (project, panel) = workspace.update(cx, |workspace, cx| {
2650 Some((
2651 workspace.project().clone(),
2652 workspace.panel::<AssistantPanel>(cx)?,
2653 ))
2654 })?;
2655
2656 let context_editor =
2657 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
2658
2659 Some(cx.spawn(|mut cx| async move {
2660 let context_editor = context_editor.await?;
2661 context_editor
2662 .update(&mut cx, |context_editor, cx| {
2663 context_editor.remote_id = Some(id);
2664 context_editor.editor.update(cx, |editor, cx| {
2665 editor.apply_update_proto(
2666 &project,
2667 proto::update_view::Variant::Editor(proto::update_view::Editor {
2668 selections: editor_state.selections,
2669 pending_selection: editor_state.pending_selection,
2670 scroll_top_anchor: editor_state.scroll_top_anchor,
2671 scroll_x: editor_state.scroll_y,
2672 scroll_y: editor_state.scroll_y,
2673 ..Default::default()
2674 }),
2675 cx,
2676 )
2677 })
2678 })?
2679 .await?;
2680 Ok(context_editor)
2681 }))
2682 }
2683
2684 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
2685 Editor::to_follow_event(event)
2686 }
2687
2688 fn add_event_to_update_proto(
2689 &self,
2690 event: &Self::Event,
2691 update: &mut Option<proto::update_view::Variant>,
2692 cx: &WindowContext,
2693 ) -> bool {
2694 self.editor
2695 .read(cx)
2696 .add_event_to_update_proto(event, update, cx)
2697 }
2698
2699 fn apply_update_proto(
2700 &mut self,
2701 project: &Model<Project>,
2702 message: proto::update_view::Variant,
2703 cx: &mut ViewContext<Self>,
2704 ) -> Task<Result<()>> {
2705 self.editor.update(cx, |editor, cx| {
2706 editor.apply_update_proto(project, message, cx)
2707 })
2708 }
2709
2710 fn is_project_item(&self, _cx: &WindowContext) -> bool {
2711 true
2712 }
2713
2714 fn set_leader_peer_id(
2715 &mut self,
2716 leader_peer_id: Option<proto::PeerId>,
2717 cx: &mut ViewContext<Self>,
2718 ) {
2719 self.editor.update(cx, |editor, cx| {
2720 editor.set_leader_peer_id(leader_peer_id, cx)
2721 })
2722 }
2723
2724 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
2725 if existing.context.read(cx).id() == self.context.read(cx).id() {
2726 Some(item::Dedup::KeepExisting)
2727 } else {
2728 None
2729 }
2730 }
2731}
2732
2733pub struct ContextEditorToolbarItem {
2734 fs: Arc<dyn Fs>,
2735 workspace: WeakView<Workspace>,
2736 active_context_editor: Option<WeakView<ContextEditor>>,
2737 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2738 model_summary_editor: View<Editor>,
2739}
2740
2741impl ContextEditorToolbarItem {
2742 pub fn new(
2743 workspace: &Workspace,
2744 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
2745 model_summary_editor: View<Editor>,
2746 ) -> Self {
2747 Self {
2748 fs: workspace.app_state().fs.clone(),
2749 workspace: workspace.weak_handle(),
2750 active_context_editor: None,
2751 model_selector_menu_handle,
2752 model_summary_editor,
2753 }
2754 }
2755
2756 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
2757 let commands = SlashCommandRegistry::global(cx);
2758 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
2759 Some(
2760 workspace
2761 .read(cx)
2762 .active_item_as::<Editor>(cx)?
2763 .focus_handle(cx),
2764 )
2765 });
2766 let active_context_editor = self.active_context_editor.clone();
2767
2768 PopoverMenu::new("inject-context-menu")
2769 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
2770 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
2771 }))
2772 .menu(move |cx| {
2773 let active_context_editor = active_context_editor.clone()?;
2774 ContextMenu::build(cx, |mut menu, _cx| {
2775 for command_name in commands.featured_command_names() {
2776 if let Some(command) = commands.command(&command_name) {
2777 let menu_text = SharedString::from(Arc::from(command.menu_text()));
2778 menu = menu.custom_entry(
2779 {
2780 let command_name = command_name.clone();
2781 move |_cx| {
2782 h_flex()
2783 .gap_4()
2784 .w_full()
2785 .justify_between()
2786 .child(Label::new(menu_text.clone()))
2787 .child(
2788 Label::new(format!("/{command_name}"))
2789 .color(Color::Muted),
2790 )
2791 .into_any()
2792 }
2793 },
2794 {
2795 let active_context_editor = active_context_editor.clone();
2796 move |cx| {
2797 active_context_editor
2798 .update(cx, |context_editor, cx| {
2799 context_editor.insert_command(&command_name, cx)
2800 })
2801 .ok();
2802 }
2803 },
2804 )
2805 }
2806 }
2807
2808 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
2809 menu = menu
2810 .context(active_editor_focus_handle)
2811 .action("Quote Selection", Box::new(QuoteSelection));
2812 }
2813
2814 menu
2815 })
2816 .into()
2817 })
2818 }
2819
2820 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2821 let context = &self
2822 .active_context_editor
2823 .as_ref()?
2824 .upgrade()?
2825 .read(cx)
2826 .context;
2827 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
2828 TokenState::NoTokensLeft {
2829 max_token_count,
2830 token_count,
2831 } => (Color::Error, token_count, max_token_count),
2832 TokenState::HasMoreTokens {
2833 max_token_count,
2834 token_count,
2835 over_warn_threshold,
2836 } => {
2837 let color = if over_warn_threshold {
2838 Color::Warning
2839 } else {
2840 Color::Muted
2841 };
2842 (color, token_count, max_token_count)
2843 }
2844 };
2845 Some(
2846 h_flex()
2847 .gap_0p5()
2848 .child(
2849 Label::new(humanize_token_count(token_count))
2850 .size(LabelSize::Small)
2851 .color(token_count_color),
2852 )
2853 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2854 .child(
2855 Label::new(humanize_token_count(max_token_count))
2856 .size(LabelSize::Small)
2857 .color(Color::Muted),
2858 ),
2859 )
2860 }
2861}
2862
2863impl Render for ContextEditorToolbarItem {
2864 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2865 let left_side = h_flex()
2866 .gap_2()
2867 .flex_1()
2868 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
2869 .when(self.active_context_editor.is_some(), |left_side| {
2870 left_side
2871 .child(
2872 IconButton::new("regenerate-context", IconName::ArrowCircle)
2873 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
2874 .on_click(cx.listener(move |_, _, cx| {
2875 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
2876 })),
2877 )
2878 .child(self.model_summary_editor.clone())
2879 });
2880 let right_side = h_flex()
2881 .gap_2()
2882 .child(
2883 ModelSelector::new(
2884 self.fs.clone(),
2885 ButtonLike::new("active-model")
2886 .style(ButtonStyle::Subtle)
2887 .child(
2888 h_flex()
2889 .w_full()
2890 .gap_0p5()
2891 .child(
2892 div()
2893 .overflow_x_hidden()
2894 .flex_grow()
2895 .whitespace_nowrap()
2896 .child(
2897 Label::new(
2898 LanguageModelRegistry::read_global(cx)
2899 .active_model()
2900 .map(|model| {
2901 format!(
2902 "{}: {}",
2903 model.provider_name().0,
2904 model.name().0
2905 )
2906 })
2907 .unwrap_or_else(|| "No model selected".into()),
2908 )
2909 .size(LabelSize::Small)
2910 .color(Color::Muted),
2911 ),
2912 )
2913 .child(
2914 Icon::new(IconName::ChevronDown)
2915 .color(Color::Muted)
2916 .size(IconSize::XSmall),
2917 ),
2918 )
2919 .tooltip(move |cx| {
2920 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
2921 }),
2922 )
2923 .with_handle(self.model_selector_menu_handle.clone()),
2924 )
2925 .children(self.render_remaining_tokens(cx))
2926 .child(self.render_inject_context_menu(cx));
2927
2928 h_flex()
2929 .size_full()
2930 .justify_between()
2931 .child(left_side)
2932 .child(right_side)
2933 }
2934}
2935
2936impl ToolbarItemView for ContextEditorToolbarItem {
2937 fn set_active_pane_item(
2938 &mut self,
2939 active_pane_item: Option<&dyn ItemHandle>,
2940 cx: &mut ViewContext<Self>,
2941 ) -> ToolbarItemLocation {
2942 self.active_context_editor = active_pane_item
2943 .and_then(|item| item.act_as::<ContextEditor>(cx))
2944 .map(|editor| editor.downgrade());
2945 cx.notify();
2946 if self.active_context_editor.is_none() {
2947 ToolbarItemLocation::Hidden
2948 } else {
2949 ToolbarItemLocation::PrimaryRight
2950 }
2951 }
2952
2953 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
2954 cx.notify();
2955 }
2956}
2957
2958impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
2959
2960enum ContextEditorToolbarItemEvent {
2961 RegenerateSummary,
2962}
2963impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
2964
2965pub struct ContextHistory {
2966 picker: View<Picker<SavedContextPickerDelegate>>,
2967 _subscriptions: Vec<Subscription>,
2968 assistant_panel: WeakView<AssistantPanel>,
2969}
2970
2971impl ContextHistory {
2972 fn new(
2973 project: Model<Project>,
2974 context_store: Model<ContextStore>,
2975 assistant_panel: WeakView<AssistantPanel>,
2976 cx: &mut ViewContext<Self>,
2977 ) -> Self {
2978 let picker = cx.new_view(|cx| {
2979 Picker::uniform_list(
2980 SavedContextPickerDelegate::new(project, context_store.clone()),
2981 cx,
2982 )
2983 .modal(false)
2984 .max_height(None)
2985 });
2986
2987 let _subscriptions = vec![
2988 cx.observe(&context_store, |this, _, cx| {
2989 this.picker.update(cx, |picker, cx| picker.refresh(cx));
2990 }),
2991 cx.subscribe(&picker, Self::handle_picker_event),
2992 ];
2993
2994 Self {
2995 picker,
2996 _subscriptions,
2997 assistant_panel,
2998 }
2999 }
3000
3001 fn handle_picker_event(
3002 &mut self,
3003 _: View<Picker<SavedContextPickerDelegate>>,
3004 event: &SavedContextPickerEvent,
3005 cx: &mut ViewContext<Self>,
3006 ) {
3007 let SavedContextPickerEvent::Confirmed(context) = event;
3008 self.assistant_panel
3009 .update(cx, |assistant_panel, cx| match context {
3010 ContextMetadata::Remote(metadata) => {
3011 assistant_panel
3012 .open_remote_context(metadata.id.clone(), cx)
3013 .detach_and_log_err(cx);
3014 }
3015 ContextMetadata::Saved(metadata) => {
3016 assistant_panel
3017 .open_saved_context(metadata.path.clone(), cx)
3018 .detach_and_log_err(cx);
3019 }
3020 })
3021 .ok();
3022 }
3023}
3024
3025impl Render for ContextHistory {
3026 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3027 div().size_full().child(self.picker.clone())
3028 }
3029}
3030
3031impl FocusableView for ContextHistory {
3032 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3033 self.picker.focus_handle(cx)
3034 }
3035}
3036
3037impl EventEmitter<()> for ContextHistory {}
3038
3039impl Item for ContextHistory {
3040 type Event = ();
3041
3042 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3043 Some("History".into())
3044 }
3045}
3046
3047struct ActiveTab {
3048 provider: Arc<dyn LanguageModelProvider>,
3049 configuration_prompt: AnyView,
3050 focus_handle: Option<FocusHandle>,
3051 load_credentials_task: Option<Task<()>>,
3052}
3053
3054impl ActiveTab {
3055 fn is_loading_credentials(&self) -> bool {
3056 if let Some(task) = &self.load_credentials_task {
3057 if let Task::Spawned(_) = task {
3058 return true;
3059 }
3060 }
3061 false
3062 }
3063}
3064
3065pub struct ConfigurationView {
3066 focus_handle: FocusHandle,
3067 active_tab: Option<ActiveTab>,
3068}
3069
3070impl ConfigurationView {
3071 fn new(cx: &mut ViewContext<Self>) -> Self {
3072 let focus_handle = cx.focus_handle();
3073
3074 cx.on_focus(&focus_handle, |this, cx| {
3075 if let Some(focus_handle) = this
3076 .active_tab
3077 .as_ref()
3078 .and_then(|tab| tab.focus_handle.as_ref())
3079 {
3080 focus_handle.focus(cx);
3081 }
3082 })
3083 .detach();
3084
3085 let mut this = Self {
3086 focus_handle,
3087 active_tab: None,
3088 };
3089
3090 let providers = LanguageModelRegistry::read_global(cx).providers();
3091 if !providers.is_empty() {
3092 this.set_active_tab(providers[0].clone(), cx);
3093 }
3094
3095 this
3096 }
3097
3098 fn set_active_tab(
3099 &mut self,
3100 provider: Arc<dyn LanguageModelProvider>,
3101 cx: &mut ViewContext<Self>,
3102 ) {
3103 let (view, focus_handle) = provider.configuration_view(cx);
3104
3105 if let Some(focus_handle) = &focus_handle {
3106 focus_handle.focus(cx);
3107 } else {
3108 self.focus_handle.focus(cx);
3109 }
3110
3111 let load_credentials = provider.authenticate(cx);
3112 let load_credentials_task = cx.spawn(|this, mut cx| async move {
3113 let _ = load_credentials.await;
3114 this.update(&mut cx, |this, cx| {
3115 if let Some(active_tab) = &mut this.active_tab {
3116 active_tab.load_credentials_task = None;
3117 cx.notify();
3118 }
3119 })
3120 .log_err();
3121 });
3122
3123 self.active_tab = Some(ActiveTab {
3124 provider,
3125 configuration_prompt: view,
3126 focus_handle,
3127 load_credentials_task: Some(load_credentials_task),
3128 });
3129 cx.notify();
3130 }
3131
3132 fn render_active_tab_view(&mut self, cx: &mut ViewContext<Self>) -> Option<Div> {
3133 let Some(active_tab) = &self.active_tab else {
3134 return None;
3135 };
3136
3137 let provider = active_tab.provider.clone();
3138 let provider_name = provider.name().0.clone();
3139
3140 let show_spinner = active_tab.is_loading_credentials();
3141
3142 let content = if show_spinner {
3143 let loading_icon = svg()
3144 .size_4()
3145 .path(IconName::ArrowCircle.path())
3146 .text_color(cx.text_style().color)
3147 .with_animation(
3148 "icon_circle_arrow",
3149 Animation::new(Duration::from_secs(2)).repeat(),
3150 |svg, delta| svg.with_transformation(Transformation::rotate(percentage(delta))),
3151 );
3152
3153 h_flex()
3154 .gap_2()
3155 .child(loading_icon)
3156 .child(Label::new("Loading provider configuration...").size(LabelSize::Small))
3157 .into_any_element()
3158 } else {
3159 active_tab.configuration_prompt.clone().into_any_element()
3160 };
3161
3162 Some(
3163 v_flex()
3164 .gap_4()
3165 .child(
3166 div()
3167 .p(Spacing::Large.rems(cx))
3168 .bg(cx.theme().colors().title_bar_background)
3169 .border_1()
3170 .border_color(cx.theme().colors().border_variant)
3171 .rounded_md()
3172 .child(content),
3173 )
3174 .when(
3175 !show_spinner && provider.is_authenticated(cx),
3176 move |this| {
3177 this.child(
3178 h_flex().justify_end().child(
3179 Button::new(
3180 "new-context",
3181 format!("Open new context using {}", provider_name),
3182 )
3183 .icon_position(IconPosition::Start)
3184 .icon(IconName::Plus)
3185 .style(ButtonStyle::Filled)
3186 .layer(ElevationIndex::ModalSurface)
3187 .on_click(cx.listener(
3188 move |_, _, cx| {
3189 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3190 provider.clone(),
3191 ))
3192 },
3193 )),
3194 ),
3195 )
3196 },
3197 ),
3198 )
3199 }
3200
3201 fn render_tab(
3202 &self,
3203 provider: &Arc<dyn LanguageModelProvider>,
3204 cx: &mut ViewContext<Self>,
3205 ) -> impl IntoElement {
3206 let button_id = SharedString::from(format!("tab-{}", provider.id().0));
3207 let is_active = self.active_tab.as_ref().map(|t| t.provider.id()) == Some(provider.id());
3208 ButtonLike::new(button_id)
3209 .size(ButtonSize::Compact)
3210 .style(ButtonStyle::Transparent)
3211 .selected(is_active)
3212 .on_click(cx.listener({
3213 let provider = provider.clone();
3214 move |this, _, cx| {
3215 this.set_active_tab(provider.clone(), cx);
3216 }
3217 }))
3218 .child(
3219 div()
3220 .my_3()
3221 .pb_px()
3222 .border_b_1()
3223 .border_color(if is_active {
3224 cx.theme().colors().text_accent
3225 } else {
3226 cx.theme().colors().border_transparent
3227 })
3228 .when(!is_active, |this| {
3229 this.group_hover("", |this| {
3230 this.border_color(cx.theme().colors().border_variant)
3231 })
3232 })
3233 .child(Label::new(provider.name().0).size(LabelSize::Small).color(
3234 if is_active {
3235 Color::Accent
3236 } else {
3237 Color::Default
3238 },
3239 )),
3240 )
3241 }
3242}
3243
3244impl Render for ConfigurationView {
3245 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3246 let providers = LanguageModelRegistry::read_global(cx).providers();
3247 let tabs = h_flex().mx_neg_1().gap_3().children(
3248 providers
3249 .iter()
3250 .map(|provider| self.render_tab(provider, cx)),
3251 );
3252
3253 v_flex()
3254 .id("assistant-configuration-view")
3255 .track_focus(&self.focus_handle)
3256 .w_full()
3257 .min_h_full()
3258 .p(Spacing::XXLarge.rems(cx))
3259 .overflow_y_scroll()
3260 .gap_6()
3261 .child(
3262 v_flex()
3263 .gap_2()
3264 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium)),
3265 )
3266 .child(
3267 v_flex()
3268 .gap_2()
3269 .child(Headline::new("Configure providers").size(HeadlineSize::Small))
3270 .child(
3271 Label::new(
3272 "At least one provider must be configured to use the assistant.",
3273 )
3274 .color(Color::Muted),
3275 )
3276 .child(tabs)
3277 .when(self.active_tab.is_some(), |this| {
3278 this.children(self.render_active_tab_view(cx))
3279 })
3280 .when(self.active_tab.is_none(), |this| {
3281 this.child(Label::new("No providers configured").color(Color::Warning))
3282 }),
3283 )
3284 }
3285}
3286
3287pub enum ConfigurationViewEvent {
3288 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
3289}
3290
3291impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
3292
3293impl FocusableView for ConfigurationView {
3294 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3295 self.focus_handle.clone()
3296 }
3297}
3298
3299impl Item for ConfigurationView {
3300 type Event = ConfigurationViewEvent;
3301
3302 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3303 Some("Configuration".into())
3304 }
3305}
3306
3307type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
3308
3309fn render_slash_command_output_toggle(
3310 row: MultiBufferRow,
3311 is_folded: bool,
3312 fold: ToggleFold,
3313 _cx: &mut WindowContext,
3314) -> AnyElement {
3315 Disclosure::new(
3316 ("slash-command-output-fold-indicator", row.0 as u64),
3317 !is_folded,
3318 )
3319 .selected(is_folded)
3320 .on_click(move |_e, cx| fold(!is_folded, cx))
3321 .into_any_element()
3322}
3323
3324fn render_pending_slash_command_gutter_decoration(
3325 row: MultiBufferRow,
3326 status: &PendingSlashCommandStatus,
3327 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
3328) -> AnyElement {
3329 let mut icon = IconButton::new(
3330 ("slash-command-gutter-decoration", row.0),
3331 ui::IconName::TriangleRight,
3332 )
3333 .on_click(move |_e, cx| confirm_command(cx))
3334 .icon_size(ui::IconSize::Small)
3335 .size(ui::ButtonSize::None);
3336
3337 match status {
3338 PendingSlashCommandStatus::Idle => {
3339 icon = icon.icon_color(Color::Muted);
3340 }
3341 PendingSlashCommandStatus::Running { .. } => {
3342 icon = icon.selected(true);
3343 }
3344 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
3345 }
3346
3347 icon.into_any_element()
3348}
3349
3350fn render_docs_slash_command_trailer(
3351 row: MultiBufferRow,
3352 command: PendingSlashCommand,
3353 cx: &mut WindowContext,
3354) -> AnyElement {
3355 let Some(argument) = command.argument else {
3356 return Empty.into_any();
3357 };
3358
3359 let args = DocsSlashCommandArgs::parse(&argument);
3360
3361 let Some(store) = args
3362 .provider()
3363 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
3364 else {
3365 return Empty.into_any();
3366 };
3367
3368 let Some(package) = args.package() else {
3369 return Empty.into_any();
3370 };
3371
3372 let mut children = Vec::new();
3373
3374 if store.is_indexing(&package) {
3375 children.push(
3376 div()
3377 .id(("crates-being-indexed", row.0))
3378 .child(Icon::new(IconName::ArrowCircle).with_animation(
3379 "arrow-circle",
3380 Animation::new(Duration::from_secs(4)).repeat(),
3381 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
3382 ))
3383 .tooltip({
3384 let package = package.clone();
3385 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
3386 })
3387 .into_any_element(),
3388 );
3389 }
3390
3391 if let Some(latest_error) = store.latest_error_for_package(&package) {
3392 children.push(
3393 div()
3394 .id(("latest-error", row.0))
3395 .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
3396 .tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
3397 .into_any_element(),
3398 )
3399 }
3400
3401 let is_indexing = store.is_indexing(&package);
3402 let latest_error = store.latest_error_for_package(&package);
3403
3404 if !is_indexing && latest_error.is_none() {
3405 return Empty.into_any();
3406 }
3407
3408 h_flex().gap_2().children(children).into_any_element()
3409}
3410
3411fn make_lsp_adapter_delegate(
3412 project: &Model<Project>,
3413 cx: &mut AppContext,
3414) -> Result<Arc<dyn LspAdapterDelegate>> {
3415 project.update(cx, |project, cx| {
3416 // TODO: Find the right worktree.
3417 let worktree = project
3418 .worktrees(cx)
3419 .next()
3420 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
3421 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
3422 })
3423}
3424
3425fn slash_command_error_block_renderer(message: String) -> RenderBlock {
3426 Box::new(move |_| {
3427 div()
3428 .pl_6()
3429 .child(
3430 Label::new(format!("error: {}", message))
3431 .single_line()
3432 .color(Color::Error),
3433 )
3434 .into_any()
3435 })
3436}
3437
3438enum TokenState {
3439 NoTokensLeft {
3440 max_token_count: usize,
3441 token_count: usize,
3442 },
3443 HasMoreTokens {
3444 max_token_count: usize,
3445 token_count: usize,
3446 over_warn_threshold: bool,
3447 },
3448}
3449
3450fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
3451 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
3452
3453 let model = LanguageModelRegistry::read_global(cx).active_model()?;
3454 let token_count = context.read(cx).token_count()?;
3455 let max_token_count = model.max_token_count();
3456
3457 let remaining_tokens = max_token_count as isize - token_count as isize;
3458 let token_state = if remaining_tokens <= 0 {
3459 TokenState::NoTokensLeft {
3460 max_token_count,
3461 token_count,
3462 }
3463 } else {
3464 let over_warn_threshold =
3465 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
3466 TokenState::HasMoreTokens {
3467 max_token_count,
3468 token_count,
3469 over_warn_threshold,
3470 }
3471 };
3472 Some(token_state)
3473}