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