1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings},
3 humanize_token_count,
4 prompt_library::open_prompt_library,
5 prompts::PromptBuilder,
6 slash_command::{
7 default_command::DefaultSlashCommand,
8 docs_command::{DocsSlashCommand, DocsSlashCommandArgs},
9 file_command::codeblock_fence_for_path,
10 SlashCommandCompletionProvider, SlashCommandRegistry,
11 },
12 terminal_inline_assistant::TerminalInlineAssistant,
13 Assist, ConfirmCommand, Context, ContextEvent, ContextId, ContextStore, CycleMessageRole,
14 DebugWorkflowSteps, DeployHistory, DeployPromptLibrary, InlineAssist, InlineAssistId,
15 InlineAssistant, InsertIntoEditor, MessageStatus, ModelSelector, PendingSlashCommand,
16 PendingSlashCommandStatus, QuoteSelection, RemoteContextMetadata, ResolvedWorkflowStep,
17 SavedContextMetadata, Split, ToggleFocus, ToggleModelSelector,
18};
19use crate::{ContextStoreEvent, ShowConfiguration};
20use anyhow::{anyhow, Result};
21use assistant_slash_command::{SlashCommand, SlashCommandOutputSection};
22use client::{proto, Client, Status};
23use collections::{BTreeSet, HashMap, HashSet};
24use editor::{
25 actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt},
26 display_map::{
27 BlockContext, BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId,
28 RenderBlock, ToDisplayPoint,
29 },
30 scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor},
31 Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint,
32};
33use editor::{display_map::CreaseId, FoldPlaceholder};
34use fs::Fs;
35use gpui::{
36 canvas, div, percentage, point, Action, Animation, AnimationExt, AnyElement, AnyView,
37 AppContext, AsyncWindowContext, ClipboardItem, Context as _, DismissEvent, Empty, Entity,
38 EntityId, EventEmitter, FocusHandle, FocusableView, FontWeight, InteractiveElement,
39 IntoElement, Model, ParentElement, Pixels, ReadGlobal, Render, SharedString,
40 StatefulInteractiveElement, Styled, Subscription, Task, Transformation, UpdateGlobal, View,
41 ViewContext, VisualContext, WeakView, WindowContext,
42};
43use indexed_docs::IndexedDocsStore;
44use language::{
45 language_settings::SoftWrap, Capability, LanguageRegistry, LspAdapterDelegate, Point, ToOffset,
46};
47use language_model::{
48 provider::cloud::PROVIDER_ID, LanguageModelProvider, LanguageModelProviderId,
49 LanguageModelRegistry, Role,
50};
51use multi_buffer::MultiBufferRow;
52use picker::{Picker, PickerDelegate};
53use project::{Project, ProjectLspAdapterDelegate};
54use search::{buffer_search::DivRegistrar, BufferSearchBar};
55use settings::{update_settings_file, Settings};
56use smol::stream::StreamExt;
57use std::{
58 borrow::Cow,
59 cmp::{self, Ordering},
60 fmt::Write,
61 ops::Range,
62 path::PathBuf,
63 sync::Arc,
64 time::Duration,
65};
66use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
67use text::OffsetRangeExt;
68use ui::TintColor;
69use ui::{
70 prelude::*,
71 utils::{format_distance_from_now, DateTimeType},
72 Avatar, AvatarShape, ButtonLike, ContextMenu, Disclosure, ElevationIndex, KeyBinding, ListItem,
73 ListItemSpacing, PopoverMenu, PopoverMenuHandle, Tooltip,
74};
75use util::ResultExt;
76use workspace::{
77 dock::{DockPosition, Panel, PanelEvent},
78 item::{self, FollowableItem, Item, ItemHandle},
79 notifications::NotifyTaskExt,
80 pane::{self, SaveIntent},
81 searchable::{SearchEvent, SearchableItem},
82 Pane, Save, ToggleZoom, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace,
83};
84use workspace::{searchable::SearchableItemHandle, NewFile};
85
86pub fn init(cx: &mut AppContext) {
87 workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
88 cx.observe_new_views(
89 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
90 workspace
91 .register_action(|workspace, _: &ToggleFocus, cx| {
92 let settings = AssistantSettings::get_global(cx);
93 if !settings.enabled {
94 return;
95 }
96
97 workspace.toggle_panel_focus::<AssistantPanel>(cx);
98 })
99 .register_action(AssistantPanel::inline_assist)
100 .register_action(ContextEditor::quote_selection)
101 .register_action(ContextEditor::insert_selection)
102 .register_action(AssistantPanel::show_configuration);
103 },
104 )
105 .detach();
106
107 cx.observe_new_views(
108 |terminal_panel: &mut TerminalPanel, cx: &mut ViewContext<TerminalPanel>| {
109 let settings = AssistantSettings::get_global(cx);
110 if !settings.enabled {
111 return;
112 }
113
114 terminal_panel.register_tab_bar_button(cx.new_view(|_| InlineAssistTabBarButton), cx);
115 },
116 )
117 .detach();
118}
119
120struct InlineAssistTabBarButton;
121
122impl Render for InlineAssistTabBarButton {
123 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
124 IconButton::new("terminal_inline_assistant", IconName::MagicWand)
125 .icon_size(IconSize::Small)
126 .on_click(cx.listener(|_, _, cx| {
127 cx.dispatch_action(InlineAssist::default().boxed_clone());
128 }))
129 .tooltip(move |cx| Tooltip::for_action("Inline Assist", &InlineAssist::default(), cx))
130 }
131}
132
133pub enum AssistantPanelEvent {
134 ContextEdited,
135}
136
137pub struct AssistantPanel {
138 pane: View<Pane>,
139 workspace: WeakView<Workspace>,
140 width: Option<Pixels>,
141 height: Option<Pixels>,
142 project: Model<Project>,
143 context_store: Model<ContextStore>,
144 languages: Arc<LanguageRegistry>,
145 fs: Arc<dyn Fs>,
146 subscriptions: Vec<Subscription>,
147 model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
148 model_summary_editor: View<Editor>,
149 authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
150 configuration_subscription: Option<Subscription>,
151 client_status: Option<client::Status>,
152 watch_client_status: Option<Task<()>>,
153 show_zed_ai_notice: bool,
154}
155
156#[derive(Clone)]
157enum ContextMetadata {
158 Remote(RemoteContextMetadata),
159 Saved(SavedContextMetadata),
160}
161
162struct SavedContextPickerDelegate {
163 store: Model<ContextStore>,
164 project: Model<Project>,
165 matches: Vec<ContextMetadata>,
166 selected_index: usize,
167}
168
169enum SavedContextPickerEvent {
170 Confirmed(ContextMetadata),
171}
172
173enum InlineAssistTarget {
174 Editor(View<Editor>, bool),
175 Terminal(View<TerminalView>),
176}
177
178impl EventEmitter<SavedContextPickerEvent> for Picker<SavedContextPickerDelegate> {}
179
180impl SavedContextPickerDelegate {
181 fn new(project: Model<Project>, store: Model<ContextStore>) -> Self {
182 Self {
183 project,
184 store,
185 matches: Vec::new(),
186 selected_index: 0,
187 }
188 }
189}
190
191impl PickerDelegate for SavedContextPickerDelegate {
192 type ListItem = ListItem;
193
194 fn match_count(&self) -> usize {
195 self.matches.len()
196 }
197
198 fn selected_index(&self) -> usize {
199 self.selected_index
200 }
201
202 fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
203 self.selected_index = ix;
204 }
205
206 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
207 "Search...".into()
208 }
209
210 fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
211 let search = self.store.read(cx).search(query, cx);
212 cx.spawn(|this, mut cx| async move {
213 let matches = search.await;
214 this.update(&mut cx, |this, cx| {
215 let host_contexts = this.delegate.store.read(cx).host_contexts();
216 this.delegate.matches = host_contexts
217 .iter()
218 .cloned()
219 .map(ContextMetadata::Remote)
220 .chain(matches.into_iter().map(ContextMetadata::Saved))
221 .collect();
222 this.delegate.selected_index = 0;
223 cx.notify();
224 })
225 .ok();
226 })
227 }
228
229 fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
230 if let Some(metadata) = self.matches.get(self.selected_index) {
231 cx.emit(SavedContextPickerEvent::Confirmed(metadata.clone()));
232 }
233 }
234
235 fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
236
237 fn render_match(
238 &self,
239 ix: usize,
240 selected: bool,
241 cx: &mut ViewContext<Picker<Self>>,
242 ) -> Option<Self::ListItem> {
243 let context = self.matches.get(ix)?;
244 let item = match context {
245 ContextMetadata::Remote(context) => {
246 let host_user = self.project.read(cx).host().and_then(|collaborator| {
247 self.project
248 .read(cx)
249 .user_store()
250 .read(cx)
251 .get_cached_user(collaborator.user_id)
252 });
253 div()
254 .flex()
255 .w_full()
256 .justify_between()
257 .gap_2()
258 .child(
259 h_flex().flex_1().overflow_x_hidden().child(
260 Label::new(context.summary.clone().unwrap_or(DEFAULT_TAB_TITLE.into()))
261 .size(LabelSize::Small),
262 ),
263 )
264 .child(
265 h_flex()
266 .gap_2()
267 .children(if let Some(host_user) = host_user {
268 vec![
269 Avatar::new(host_user.avatar_uri.clone())
270 .shape(AvatarShape::Circle)
271 .into_any_element(),
272 Label::new(format!("Shared by @{}", host_user.github_login))
273 .color(Color::Muted)
274 .size(LabelSize::Small)
275 .into_any_element(),
276 ]
277 } else {
278 vec![Label::new("Shared by host")
279 .color(Color::Muted)
280 .size(LabelSize::Small)
281 .into_any_element()]
282 }),
283 )
284 }
285 ContextMetadata::Saved(context) => div()
286 .flex()
287 .w_full()
288 .justify_between()
289 .gap_2()
290 .child(
291 h_flex()
292 .flex_1()
293 .child(Label::new(context.title.clone()).size(LabelSize::Small))
294 .overflow_x_hidden(),
295 )
296 .child(
297 Label::new(format_distance_from_now(
298 DateTimeType::Local(context.mtime),
299 false,
300 true,
301 true,
302 ))
303 .color(Color::Muted)
304 .size(LabelSize::Small),
305 ),
306 };
307 Some(
308 ListItem::new(ix)
309 .inset(true)
310 .spacing(ListItemSpacing::Sparse)
311 .selected(selected)
312 .child(item),
313 )
314 }
315}
316
317impl AssistantPanel {
318 pub fn load(
319 workspace: WeakView<Workspace>,
320 prompt_builder: Arc<PromptBuilder>,
321 cx: AsyncWindowContext,
322 ) -> Task<Result<View<Self>>> {
323 cx.spawn(|mut cx| async move {
324 let context_store = workspace
325 .update(&mut cx, |workspace, cx| {
326 let project = workspace.project().clone();
327 ContextStore::new(project, prompt_builder.clone(), cx)
328 })?
329 .await?;
330
331 workspace.update(&mut cx, |workspace, cx| {
332 // TODO: deserialize state.
333 cx.new_view(|cx| Self::new(workspace, context_store, cx))
334 })
335 })
336 }
337
338 fn new(
339 workspace: &Workspace,
340 context_store: Model<ContextStore>,
341 cx: &mut ViewContext<Self>,
342 ) -> Self {
343 let model_selector_menu_handle = PopoverMenuHandle::default();
344 let model_summary_editor = cx.new_view(|cx| Editor::single_line(cx));
345 let context_editor_toolbar = cx.new_view(|_| {
346 ContextEditorToolbarItem::new(
347 workspace,
348 model_selector_menu_handle.clone(),
349 model_summary_editor.clone(),
350 )
351 });
352 let pane = cx.new_view(|cx| {
353 let mut pane = Pane::new(
354 workspace.weak_handle(),
355 workspace.project().clone(),
356 Default::default(),
357 None,
358 NewFile.boxed_clone(),
359 cx,
360 );
361 pane.set_can_split(false, cx);
362 pane.set_can_navigate(true, cx);
363 pane.display_nav_history_buttons(None);
364 pane.set_should_display_tab_bar(|_| true);
365 pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
366 let focus_handle = pane.focus_handle(cx);
367 let left_children = IconButton::new("history", IconName::TextSearch)
368 .on_click(cx.listener({
369 let focus_handle = focus_handle.clone();
370 move |_, _, cx| {
371 focus_handle.focus(cx);
372 cx.dispatch_action(DeployHistory.boxed_clone())
373 }
374 }))
375 .tooltip(move |cx| {
376 cx.new_view(|cx| {
377 let keybind =
378 KeyBinding::for_action_in(&DeployHistory, &focus_handle, cx);
379 Tooltip::new("History").key_binding(keybind)
380 })
381 .into()
382 })
383 .selected(
384 pane.active_item()
385 .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
386 );
387 let right_children = h_flex()
388 .gap(Spacing::Small.rems(cx))
389 .child(
390 IconButton::new("new-context", IconName::Plus)
391 .on_click(
392 cx.listener(|_, _, cx| cx.dispatch_action(NewFile.boxed_clone())),
393 )
394 .tooltip(|cx| Tooltip::for_action("New Context", &NewFile, cx)),
395 )
396 .child(
397 IconButton::new("menu", IconName::Menu)
398 .icon_size(IconSize::Small)
399 .on_click(cx.listener(|pane, _, cx| {
400 let zoom_label = if pane.is_zoomed() {
401 "Zoom Out"
402 } else {
403 "Zoom In"
404 };
405 let menu = ContextMenu::build(cx, |menu, cx| {
406 menu.context(pane.focus_handle(cx))
407 .action("New Context", Box::new(NewFile))
408 .action("History", Box::new(DeployHistory))
409 .action("Prompt Library", Box::new(DeployPromptLibrary))
410 .action("Configure", Box::new(ShowConfiguration))
411 .action(zoom_label, Box::new(ToggleZoom))
412 });
413 cx.subscribe(&menu, |pane, _, _: &DismissEvent, _| {
414 pane.new_item_menu = None;
415 })
416 .detach();
417 pane.new_item_menu = Some(menu);
418 })),
419 )
420 .when_some(pane.new_item_menu.as_ref(), |el, new_item_menu| {
421 el.child(Pane::render_menu_overlay(new_item_menu))
422 })
423 .into_any_element()
424 .into();
425
426 (Some(left_children.into_any_element()), right_children)
427 });
428 pane.toolbar().update(cx, |toolbar, cx| {
429 toolbar.add_item(context_editor_toolbar.clone(), cx);
430 toolbar.add_item(cx.new_view(BufferSearchBar::new), cx)
431 });
432 pane
433 });
434
435 let subscriptions = vec![
436 cx.observe(&pane, |_, _, cx| cx.notify()),
437 cx.subscribe(&pane, Self::handle_pane_event),
438 cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
439 cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
440 cx.subscribe(&context_store, Self::handle_context_store_event),
441 cx.subscribe(
442 &LanguageModelRegistry::global(cx),
443 |this, _, event: &language_model::Event, cx| match event {
444 language_model::Event::ActiveModelChanged => {
445 this.completion_provider_changed(cx);
446 }
447 language_model::Event::ProviderStateChanged => {
448 this.ensure_authenticated(cx);
449 }
450 language_model::Event::AddedProvider(_)
451 | language_model::Event::RemovedProvider(_) => {
452 this.ensure_authenticated(cx);
453 }
454 },
455 ),
456 ];
457
458 let watch_client_status = Self::watch_client_status(workspace.client().clone(), cx);
459
460 let mut this = Self {
461 pane,
462 workspace: workspace.weak_handle(),
463 width: None,
464 height: None,
465 project: workspace.project().clone(),
466 context_store,
467 languages: workspace.app_state().languages.clone(),
468 fs: workspace.app_state().fs.clone(),
469 subscriptions,
470 model_selector_menu_handle,
471 model_summary_editor,
472 authenticate_provider_task: None,
473 configuration_subscription: None,
474 client_status: None,
475 watch_client_status: Some(watch_client_status),
476 show_zed_ai_notice: false,
477 };
478 this.new_context(cx);
479 this
480 }
481
482 fn watch_client_status(client: Arc<Client>, cx: &mut ViewContext<Self>) -> Task<()> {
483 let mut status_rx = client.status();
484
485 cx.spawn(|this, mut cx| async move {
486 while let Some(status) = status_rx.next().await {
487 this.update(&mut cx, |this, cx| {
488 if this.client_status.is_none()
489 || this
490 .client_status
491 .map_or(false, |old_status| old_status != status)
492 {
493 this.update_zed_ai_notice_visibility(status, cx);
494 }
495 this.client_status = Some(status);
496 })
497 .log_err();
498 }
499 this.update(&mut cx, |this, _cx| this.watch_client_status = None)
500 .log_err();
501 })
502 }
503
504 fn handle_pane_event(
505 &mut self,
506 pane: View<Pane>,
507 event: &pane::Event,
508 cx: &mut ViewContext<Self>,
509 ) {
510 let update_model_summary = match event {
511 pane::Event::Remove => {
512 cx.emit(PanelEvent::Close);
513 false
514 }
515 pane::Event::ZoomIn => {
516 cx.emit(PanelEvent::ZoomIn);
517 false
518 }
519 pane::Event::ZoomOut => {
520 cx.emit(PanelEvent::ZoomOut);
521 false
522 }
523
524 pane::Event::AddItem { item } => {
525 self.workspace
526 .update(cx, |workspace, cx| {
527 item.added_to_pane(workspace, self.pane.clone(), cx)
528 })
529 .ok();
530 true
531 }
532
533 pane::Event::ActivateItem { local } => {
534 if *local {
535 self.workspace
536 .update(cx, |workspace, cx| {
537 workspace.unfollow_in_pane(&pane, cx);
538 })
539 .ok();
540 }
541 cx.emit(AssistantPanelEvent::ContextEdited);
542 true
543 }
544
545 pane::Event::RemoveItem { idx } => {
546 if self
547 .pane
548 .read(cx)
549 .item_for_index(*idx)
550 .map_or(false, |item| item.downcast::<ConfigurationView>().is_some())
551 {
552 self.configuration_subscription = None;
553 }
554 false
555 }
556 pane::Event::RemovedItem { .. } => {
557 cx.emit(AssistantPanelEvent::ContextEdited);
558 true
559 }
560
561 _ => false,
562 };
563
564 if update_model_summary {
565 if let Some(editor) = self.active_context_editor(cx) {
566 self.show_updated_summary(&editor, cx)
567 }
568 }
569 }
570
571 fn handle_summary_editor_event(
572 &mut self,
573 model_summary_editor: View<Editor>,
574 event: &EditorEvent,
575 cx: &mut ViewContext<Self>,
576 ) {
577 if matches!(event, EditorEvent::Edited { .. }) {
578 if let Some(context_editor) = self.active_context_editor(cx) {
579 let new_summary = model_summary_editor.read(cx).text(cx);
580 context_editor.update(cx, |context_editor, cx| {
581 context_editor.context.update(cx, |context, cx| {
582 if context.summary().is_none()
583 && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
584 {
585 return;
586 }
587 context.custom_summary(new_summary, cx)
588 });
589 });
590 }
591 }
592 }
593
594 fn update_zed_ai_notice_visibility(
595 &mut self,
596 client_status: Status,
597 cx: &mut ViewContext<Self>,
598 ) {
599 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
600
601 // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
602 // the provider, we want to show a nudge to sign in.
603 let show_zed_ai_notice = client_status.is_signed_out()
604 && active_provider.map_or(true, |provider| provider.id().0 == PROVIDER_ID);
605
606 self.show_zed_ai_notice = show_zed_ai_notice;
607 cx.notify();
608 }
609
610 fn handle_toolbar_event(
611 &mut self,
612 _: View<ContextEditorToolbarItem>,
613 _: &ContextEditorToolbarItemEvent,
614 cx: &mut ViewContext<Self>,
615 ) {
616 if let Some(context_editor) = self.active_context_editor(cx) {
617 context_editor.update(cx, |context_editor, cx| {
618 context_editor.context.update(cx, |context, cx| {
619 context.summarize(true, cx);
620 })
621 })
622 }
623 }
624
625 fn handle_context_store_event(
626 &mut self,
627 _context_store: Model<ContextStore>,
628 event: &ContextStoreEvent,
629 cx: &mut ViewContext<Self>,
630 ) {
631 let ContextStoreEvent::ContextCreated(context_id) = event;
632 let Some(context) = self
633 .context_store
634 .read(cx)
635 .loaded_context_for_id(&context_id, cx)
636 else {
637 log::error!("no context found with ID: {}", context_id.to_proto());
638 return;
639 };
640 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
641
642 let assistant_panel = cx.view().downgrade();
643 let editor = cx.new_view(|cx| {
644 let mut editor = ContextEditor::for_context(
645 context,
646 self.fs.clone(),
647 self.workspace.clone(),
648 self.project.clone(),
649 lsp_adapter_delegate,
650 assistant_panel,
651 cx,
652 );
653 editor.insert_default_prompt(cx);
654 editor
655 });
656
657 self.show_context(editor.clone(), cx);
658 }
659
660 fn completion_provider_changed(&mut self, cx: &mut ViewContext<Self>) {
661 if let Some(editor) = self.active_context_editor(cx) {
662 editor.update(cx, |active_context, cx| {
663 active_context
664 .context
665 .update(cx, |context, cx| context.completion_provider_changed(cx))
666 })
667 }
668
669 let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
670 .active_provider()
671 .map(|p| p.id())
672 else {
673 return;
674 };
675
676 if self
677 .authenticate_provider_task
678 .as_ref()
679 .map_or(true, |(old_provider_id, _)| {
680 *old_provider_id != new_provider_id
681 })
682 {
683 self.authenticate_provider_task = None;
684 self.ensure_authenticated(cx);
685 }
686
687 if let Some(status) = self.client_status {
688 self.update_zed_ai_notice_visibility(status, cx);
689 }
690 }
691
692 fn ensure_authenticated(&mut self, cx: &mut ViewContext<Self>) {
693 if self.is_authenticated(cx) {
694 return;
695 }
696
697 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
698 return;
699 };
700
701 let load_credentials = self.authenticate(cx);
702
703 if self.authenticate_provider_task.is_none() {
704 self.authenticate_provider_task = Some((
705 provider.id(),
706 cx.spawn(|this, mut cx| async move {
707 let _ = load_credentials.await;
708 this.update(&mut cx, |this, _cx| {
709 this.authenticate_provider_task = None;
710 })
711 .log_err();
712 }),
713 ));
714 }
715 }
716
717 pub fn inline_assist(
718 workspace: &mut Workspace,
719 action: &InlineAssist,
720 cx: &mut ViewContext<Workspace>,
721 ) {
722 let settings = AssistantSettings::get_global(cx);
723 if !settings.enabled {
724 return;
725 }
726
727 let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
728 return;
729 };
730
731 let Some(inline_assist_target) =
732 Self::resolve_inline_assist_target(workspace, &assistant_panel, cx)
733 else {
734 return;
735 };
736
737 let initial_prompt = action.prompt.clone();
738 if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
739 match inline_assist_target {
740 InlineAssistTarget::Editor(active_editor, include_context) => {
741 InlineAssistant::update_global(cx, |assistant, cx| {
742 assistant.assist(
743 &active_editor,
744 Some(cx.view().downgrade()),
745 include_context.then_some(&assistant_panel),
746 initial_prompt,
747 cx,
748 )
749 })
750 }
751 InlineAssistTarget::Terminal(active_terminal) => {
752 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
753 assistant.assist(
754 &active_terminal,
755 Some(cx.view().downgrade()),
756 Some(&assistant_panel),
757 initial_prompt,
758 cx,
759 )
760 })
761 }
762 }
763 } else {
764 let assistant_panel = assistant_panel.downgrade();
765 cx.spawn(|workspace, mut cx| async move {
766 assistant_panel
767 .update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
768 .await?;
769 if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
770 cx.update(|cx| match inline_assist_target {
771 InlineAssistTarget::Editor(active_editor, include_context) => {
772 let assistant_panel = if include_context {
773 assistant_panel.upgrade()
774 } else {
775 None
776 };
777 InlineAssistant::update_global(cx, |assistant, cx| {
778 assistant.assist(
779 &active_editor,
780 Some(workspace),
781 assistant_panel.as_ref(),
782 initial_prompt,
783 cx,
784 )
785 })
786 }
787 InlineAssistTarget::Terminal(active_terminal) => {
788 TerminalInlineAssistant::update_global(cx, |assistant, cx| {
789 assistant.assist(
790 &active_terminal,
791 Some(workspace),
792 assistant_panel.upgrade().as_ref(),
793 initial_prompt,
794 cx,
795 )
796 })
797 }
798 })?
799 } else {
800 workspace.update(&mut cx, |workspace, cx| {
801 workspace.focus_panel::<AssistantPanel>(cx)
802 })?;
803 }
804
805 anyhow::Ok(())
806 })
807 .detach_and_log_err(cx)
808 }
809 }
810
811 fn resolve_inline_assist_target(
812 workspace: &mut Workspace,
813 assistant_panel: &View<AssistantPanel>,
814 cx: &mut WindowContext,
815 ) -> Option<InlineAssistTarget> {
816 if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
817 if terminal_panel
818 .read(cx)
819 .focus_handle(cx)
820 .contains_focused(cx)
821 {
822 if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
823 pane.read(cx)
824 .active_item()
825 .and_then(|t| t.downcast::<TerminalView>())
826 }) {
827 return Some(InlineAssistTarget::Terminal(terminal_view));
828 }
829 }
830 }
831 let context_editor =
832 assistant_panel
833 .read(cx)
834 .active_context_editor(cx)
835 .and_then(|editor| {
836 let editor = &editor.read(cx).editor;
837 if editor.read(cx).is_focused(cx) {
838 Some(editor.clone())
839 } else {
840 None
841 }
842 });
843
844 if let Some(context_editor) = context_editor {
845 Some(InlineAssistTarget::Editor(context_editor, false))
846 } else if let Some(workspace_editor) = workspace
847 .active_item(cx)
848 .and_then(|item| item.act_as::<Editor>(cx))
849 {
850 Some(InlineAssistTarget::Editor(workspace_editor, true))
851 } else if let Some(terminal_view) = workspace
852 .active_item(cx)
853 .and_then(|item| item.act_as::<TerminalView>(cx))
854 {
855 Some(InlineAssistTarget::Terminal(terminal_view))
856 } else {
857 None
858 }
859 }
860
861 fn new_context(&mut self, cx: &mut ViewContext<Self>) -> Option<View<ContextEditor>> {
862 if self.project.read(cx).is_remote() {
863 let task = self
864 .context_store
865 .update(cx, |store, cx| store.create_remote_context(cx));
866
867 cx.spawn(|this, mut cx| async move {
868 let context = task.await?;
869
870 this.update(&mut cx, |this, cx| {
871 let workspace = this.workspace.clone();
872 let project = this.project.clone();
873 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
874
875 let fs = this.fs.clone();
876 let project = this.project.clone();
877 let weak_assistant_panel = cx.view().downgrade();
878
879 let editor = cx.new_view(|cx| {
880 ContextEditor::for_context(
881 context,
882 fs,
883 workspace,
884 project,
885 lsp_adapter_delegate,
886 weak_assistant_panel,
887 cx,
888 )
889 });
890
891 this.show_context(editor, cx);
892
893 anyhow::Ok(())
894 })??;
895
896 anyhow::Ok(())
897 })
898 .detach_and_log_err(cx);
899
900 None
901 } else {
902 let context = self.context_store.update(cx, |store, cx| store.create(cx));
903 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
904
905 let assistant_panel = cx.view().downgrade();
906 let editor = cx.new_view(|cx| {
907 let mut editor = ContextEditor::for_context(
908 context,
909 self.fs.clone(),
910 self.workspace.clone(),
911 self.project.clone(),
912 lsp_adapter_delegate,
913 assistant_panel,
914 cx,
915 );
916 editor.insert_default_prompt(cx);
917 editor
918 });
919
920 self.show_context(editor.clone(), cx);
921 Some(editor)
922 }
923 }
924
925 fn show_context(&mut self, context_editor: View<ContextEditor>, cx: &mut ViewContext<Self>) {
926 let focus = self.focus_handle(cx).contains_focused(cx);
927 let prev_len = self.pane.read(cx).items_len();
928 self.pane.update(cx, |pane, cx| {
929 pane.add_item(Box::new(context_editor.clone()), focus, focus, None, cx)
930 });
931
932 if prev_len != self.pane.read(cx).items_len() {
933 self.subscriptions
934 .push(cx.subscribe(&context_editor, Self::handle_context_editor_event));
935 }
936
937 self.show_updated_summary(&context_editor, cx);
938
939 cx.emit(AssistantPanelEvent::ContextEdited);
940 cx.notify();
941 }
942
943 fn show_updated_summary(
944 &self,
945 context_editor: &View<ContextEditor>,
946 cx: &mut ViewContext<Self>,
947 ) {
948 context_editor.update(cx, |context_editor, cx| {
949 let new_summary = context_editor.title(cx).to_string();
950 self.model_summary_editor.update(cx, |summary_editor, cx| {
951 if summary_editor.text(cx) != new_summary {
952 summary_editor.set_text(new_summary, cx);
953 }
954 });
955 });
956 }
957
958 fn handle_context_editor_event(
959 &mut self,
960 context_editor: View<ContextEditor>,
961 event: &EditorEvent,
962 cx: &mut ViewContext<Self>,
963 ) {
964 match event {
965 EditorEvent::TitleChanged => {
966 self.show_updated_summary(&context_editor, cx);
967 cx.notify()
968 }
969 EditorEvent::Edited { .. } => cx.emit(AssistantPanelEvent::ContextEdited),
970 _ => {}
971 }
972 }
973
974 fn show_configuration(
975 workspace: &mut Workspace,
976 _: &ShowConfiguration,
977 cx: &mut ViewContext<Workspace>,
978 ) {
979 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
980 return;
981 };
982
983 if !panel.focus_handle(cx).contains_focused(cx) {
984 workspace.toggle_panel_focus::<AssistantPanel>(cx);
985 }
986
987 panel.update(cx, |this, cx| {
988 this.show_configuration_tab(cx);
989 })
990 }
991
992 fn show_configuration_tab(&mut self, cx: &mut ViewContext<Self>) {
993 let configuration_item_ix = self
994 .pane
995 .read(cx)
996 .items()
997 .position(|item| item.downcast::<ConfigurationView>().is_some());
998
999 if let Some(configuration_item_ix) = configuration_item_ix {
1000 self.pane.update(cx, |pane, cx| {
1001 pane.activate_item(configuration_item_ix, true, true, cx);
1002 });
1003 } else {
1004 let configuration = cx.new_view(|cx| ConfigurationView::new(cx));
1005 self.configuration_subscription = Some(cx.subscribe(
1006 &configuration,
1007 |this, _, event: &ConfigurationViewEvent, cx| match event {
1008 ConfigurationViewEvent::NewProviderContextEditor(provider) => {
1009 if LanguageModelRegistry::read_global(cx)
1010 .active_provider()
1011 .map_or(true, |p| p.id() != provider.id())
1012 {
1013 if let Some(model) = provider.provided_models(cx).first().cloned() {
1014 update_settings_file::<AssistantSettings>(
1015 this.fs.clone(),
1016 cx,
1017 move |settings, _| settings.set_model(model),
1018 );
1019 }
1020 }
1021
1022 this.new_context(cx);
1023 }
1024 },
1025 ));
1026 self.pane.update(cx, |pane, cx| {
1027 pane.add_item(Box::new(configuration), true, true, None, cx);
1028 });
1029 }
1030 }
1031
1032 fn deploy_history(&mut self, _: &DeployHistory, cx: &mut ViewContext<Self>) {
1033 let history_item_ix = self
1034 .pane
1035 .read(cx)
1036 .items()
1037 .position(|item| item.downcast::<ContextHistory>().is_some());
1038
1039 if let Some(history_item_ix) = history_item_ix {
1040 self.pane.update(cx, |pane, cx| {
1041 pane.activate_item(history_item_ix, true, true, cx);
1042 });
1043 } else {
1044 let assistant_panel = cx.view().downgrade();
1045 let history = cx.new_view(|cx| {
1046 ContextHistory::new(
1047 self.project.clone(),
1048 self.context_store.clone(),
1049 assistant_panel,
1050 cx,
1051 )
1052 });
1053 self.pane.update(cx, |pane, cx| {
1054 pane.add_item(Box::new(history), true, true, None, cx);
1055 });
1056 }
1057 }
1058
1059 fn deploy_prompt_library(&mut self, _: &DeployPromptLibrary, cx: &mut ViewContext<Self>) {
1060 open_prompt_library(self.languages.clone(), cx).detach_and_log_err(cx);
1061 }
1062
1063 fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
1064 self.model_selector_menu_handle.toggle(cx);
1065 }
1066
1067 fn active_context_editor(&self, cx: &AppContext) -> Option<View<ContextEditor>> {
1068 self.pane
1069 .read(cx)
1070 .active_item()?
1071 .downcast::<ContextEditor>()
1072 }
1073
1074 pub fn active_context(&self, cx: &AppContext) -> Option<Model<Context>> {
1075 Some(self.active_context_editor(cx)?.read(cx).context.clone())
1076 }
1077
1078 fn open_saved_context(
1079 &mut self,
1080 path: PathBuf,
1081 cx: &mut ViewContext<Self>,
1082 ) -> Task<Result<()>> {
1083 let existing_context = self.pane.read(cx).items().find_map(|item| {
1084 item.downcast::<ContextEditor>()
1085 .filter(|editor| editor.read(cx).context.read(cx).path() == Some(&path))
1086 });
1087 if let Some(existing_context) = existing_context {
1088 return cx.spawn(|this, mut cx| async move {
1089 this.update(&mut cx, |this, cx| this.show_context(existing_context, cx))
1090 });
1091 }
1092
1093 let context = self
1094 .context_store
1095 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1096 let fs = self.fs.clone();
1097 let project = self.project.clone();
1098 let workspace = self.workspace.clone();
1099
1100 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err();
1101
1102 cx.spawn(|this, mut cx| async move {
1103 let context = context.await?;
1104 let assistant_panel = this.clone();
1105 this.update(&mut cx, |this, cx| {
1106 let editor = cx.new_view(|cx| {
1107 ContextEditor::for_context(
1108 context,
1109 fs,
1110 workspace,
1111 project,
1112 lsp_adapter_delegate,
1113 assistant_panel,
1114 cx,
1115 )
1116 });
1117 this.show_context(editor, cx);
1118 anyhow::Ok(())
1119 })??;
1120 Ok(())
1121 })
1122 }
1123
1124 fn open_remote_context(
1125 &mut self,
1126 id: ContextId,
1127 cx: &mut ViewContext<Self>,
1128 ) -> Task<Result<View<ContextEditor>>> {
1129 let existing_context = self.pane.read(cx).items().find_map(|item| {
1130 item.downcast::<ContextEditor>()
1131 .filter(|editor| *editor.read(cx).context.read(cx).id() == id)
1132 });
1133 if let Some(existing_context) = existing_context {
1134 return cx.spawn(|this, mut cx| async move {
1135 this.update(&mut cx, |this, cx| {
1136 this.show_context(existing_context.clone(), cx)
1137 })?;
1138 Ok(existing_context)
1139 });
1140 }
1141
1142 let context = self
1143 .context_store
1144 .update(cx, |store, cx| store.open_remote_context(id, cx));
1145 let fs = self.fs.clone();
1146 let workspace = self.workspace.clone();
1147 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx).log_err();
1148
1149 cx.spawn(|this, mut cx| async move {
1150 let context = context.await?;
1151 let assistant_panel = this.clone();
1152 this.update(&mut cx, |this, cx| {
1153 let editor = cx.new_view(|cx| {
1154 ContextEditor::for_context(
1155 context,
1156 fs,
1157 workspace,
1158 this.project.clone(),
1159 lsp_adapter_delegate,
1160 assistant_panel,
1161 cx,
1162 )
1163 });
1164 this.show_context(editor.clone(), cx);
1165 anyhow::Ok(editor)
1166 })?
1167 })
1168 }
1169
1170 fn is_authenticated(&mut self, cx: &mut ViewContext<Self>) -> bool {
1171 LanguageModelRegistry::read_global(cx)
1172 .active_provider()
1173 .map_or(false, |provider| provider.is_authenticated(cx))
1174 }
1175
1176 fn authenticate(&mut self, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1177 LanguageModelRegistry::read_global(cx)
1178 .active_provider()
1179 .map_or(
1180 Task::ready(Err(anyhow!("no active language model provider"))),
1181 |provider| provider.authenticate(cx),
1182 )
1183 }
1184}
1185
1186impl Render for AssistantPanel {
1187 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1188 let mut registrar = DivRegistrar::new(
1189 |panel, cx| {
1190 panel
1191 .pane
1192 .read(cx)
1193 .toolbar()
1194 .read(cx)
1195 .item_of_type::<BufferSearchBar>()
1196 },
1197 cx,
1198 );
1199 BufferSearchBar::register(&mut registrar);
1200 let registrar = registrar.into_div();
1201
1202 v_flex()
1203 .key_context("AssistantPanel")
1204 .size_full()
1205 .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1206 this.new_context(cx);
1207 }))
1208 .on_action(
1209 cx.listener(|this, _: &ShowConfiguration, cx| this.show_configuration_tab(cx)),
1210 )
1211 .on_action(cx.listener(AssistantPanel::deploy_history))
1212 .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1213 .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1214 .child(registrar.size_full().child(self.pane.clone()))
1215 .into_any_element()
1216 }
1217}
1218
1219impl Panel for AssistantPanel {
1220 fn persistent_name() -> &'static str {
1221 "AssistantPanel"
1222 }
1223
1224 fn position(&self, cx: &WindowContext) -> DockPosition {
1225 match AssistantSettings::get_global(cx).dock {
1226 AssistantDockPosition::Left => DockPosition::Left,
1227 AssistantDockPosition::Bottom => DockPosition::Bottom,
1228 AssistantDockPosition::Right => DockPosition::Right,
1229 }
1230 }
1231
1232 fn position_is_valid(&self, _: DockPosition) -> bool {
1233 true
1234 }
1235
1236 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1237 settings::update_settings_file::<AssistantSettings>(
1238 self.fs.clone(),
1239 cx,
1240 move |settings, _| {
1241 let dock = match position {
1242 DockPosition::Left => AssistantDockPosition::Left,
1243 DockPosition::Bottom => AssistantDockPosition::Bottom,
1244 DockPosition::Right => AssistantDockPosition::Right,
1245 };
1246 settings.set_dock(dock);
1247 },
1248 );
1249 }
1250
1251 fn size(&self, cx: &WindowContext) -> Pixels {
1252 let settings = AssistantSettings::get_global(cx);
1253 match self.position(cx) {
1254 DockPosition::Left | DockPosition::Right => {
1255 self.width.unwrap_or(settings.default_width)
1256 }
1257 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1258 }
1259 }
1260
1261 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1262 match self.position(cx) {
1263 DockPosition::Left | DockPosition::Right => self.width = size,
1264 DockPosition::Bottom => self.height = size,
1265 }
1266 cx.notify();
1267 }
1268
1269 fn is_zoomed(&self, cx: &WindowContext) -> bool {
1270 self.pane.read(cx).is_zoomed()
1271 }
1272
1273 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1274 self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1275 }
1276
1277 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1278 if active {
1279 if self.pane.read(cx).items_len() == 0 {
1280 self.new_context(cx);
1281 }
1282
1283 self.ensure_authenticated(cx);
1284 }
1285 }
1286
1287 fn pane(&self) -> Option<View<Pane>> {
1288 Some(self.pane.clone())
1289 }
1290
1291 fn remote_id() -> Option<proto::PanelId> {
1292 Some(proto::PanelId::AssistantPanel)
1293 }
1294
1295 fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1296 let settings = AssistantSettings::get_global(cx);
1297 if !settings.enabled || !settings.button {
1298 return None;
1299 }
1300
1301 Some(IconName::ZedAssistant)
1302 }
1303
1304 fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1305 Some("Assistant Panel")
1306 }
1307
1308 fn toggle_action(&self) -> Box<dyn Action> {
1309 Box::new(ToggleFocus)
1310 }
1311}
1312
1313impl EventEmitter<PanelEvent> for AssistantPanel {}
1314impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1315
1316impl FocusableView for AssistantPanel {
1317 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
1318 self.pane.focus_handle(cx)
1319 }
1320}
1321
1322pub enum ContextEditorEvent {
1323 Edited,
1324 TabContentChanged,
1325}
1326
1327#[derive(Copy, Clone, Debug, PartialEq)]
1328struct ScrollPosition {
1329 offset_before_cursor: gpui::Point<f32>,
1330 cursor: Anchor,
1331}
1332
1333struct WorkflowStep {
1334 range: Range<language::Anchor>,
1335 header_block_id: CustomBlockId,
1336 footer_block_id: CustomBlockId,
1337 resolved_step: Option<Result<ResolvedWorkflowStep, Arc<anyhow::Error>>>,
1338 assist: Option<WorkflowAssist>,
1339}
1340
1341impl WorkflowStep {
1342 fn status(&self, cx: &AppContext) -> WorkflowStepStatus {
1343 match self.resolved_step.as_ref() {
1344 Some(Ok(_)) => {
1345 if let Some(assist) = self.assist.as_ref() {
1346 let assistant = InlineAssistant::global(cx);
1347 if assist
1348 .assist_ids
1349 .iter()
1350 .any(|assist_id| assistant.assist_status(*assist_id, cx).is_pending())
1351 {
1352 WorkflowStepStatus::Pending
1353 } else if assist
1354 .assist_ids
1355 .iter()
1356 .all(|assist_id| assistant.assist_status(*assist_id, cx).is_confirmed())
1357 {
1358 WorkflowStepStatus::Confirmed
1359 } else if assist
1360 .assist_ids
1361 .iter()
1362 .all(|assist_id| assistant.assist_status(*assist_id, cx).is_done())
1363 {
1364 WorkflowStepStatus::Done
1365 } else {
1366 WorkflowStepStatus::Idle
1367 }
1368 } else {
1369 WorkflowStepStatus::Idle
1370 }
1371 }
1372 Some(Err(error)) => WorkflowStepStatus::Error(error.clone()),
1373 None => WorkflowStepStatus::Resolving,
1374 }
1375 }
1376}
1377
1378enum WorkflowStepStatus {
1379 Resolving,
1380 Error(Arc<anyhow::Error>),
1381 Idle,
1382 Pending,
1383 Done,
1384 Confirmed,
1385}
1386
1387impl WorkflowStepStatus {
1388 pub(crate) fn is_confirmed(&self) -> bool {
1389 matches!(self, Self::Confirmed)
1390 }
1391
1392 pub(crate) fn into_element(
1393 &self,
1394 step_range: Range<language::Anchor>,
1395 focus_handle: FocusHandle,
1396 editor: WeakView<ContextEditor>,
1397 cx: &mut BlockContext<'_, '_>,
1398 ) -> AnyElement {
1399 let id = EntityId::from(cx.block_id);
1400 match self {
1401 WorkflowStepStatus::Resolving => Icon::new(IconName::ArrowCircle)
1402 .size(IconSize::Small)
1403 .with_animation(
1404 ("resolving-suggestion-label", id),
1405 Animation::new(Duration::from_secs(2)).repeat(),
1406 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1407 )
1408 .into_any_element(),
1409
1410 WorkflowStepStatus::Error(error) => {
1411 let error = error.clone();
1412 h_flex()
1413 .gap_2()
1414 .child(
1415 div()
1416 .id("step-resolution-failure")
1417 .child(
1418 Label::new("Step Resolution Failed")
1419 .size(LabelSize::Small)
1420 .color(Color::Error),
1421 )
1422 .tooltip(move |cx| Tooltip::text(error.to_string(), cx)),
1423 )
1424 .child(
1425 Button::new(("transform", id), "Retry")
1426 .icon(IconName::Update)
1427 .icon_position(IconPosition::Start)
1428 .icon_size(IconSize::Small)
1429 .label_size(LabelSize::Small)
1430 .on_click({
1431 let editor = editor.clone();
1432 let step_range = step_range.clone();
1433 move |_, cx| {
1434 editor
1435 .update(cx, |this, cx| {
1436 this.resolve_workflow_step(step_range.clone(), cx)
1437 })
1438 .ok();
1439 }
1440 }),
1441 )
1442 .into_any()
1443 }
1444
1445 WorkflowStepStatus::Idle => Button::new(("transform", id), "Transform")
1446 .icon(IconName::Sparkle)
1447 .icon_position(IconPosition::Start)
1448 .icon_size(IconSize::Small)
1449 .label_size(LabelSize::Small)
1450 .style(ButtonStyle::Tinted(TintColor::Accent))
1451 .tooltip(move |cx| {
1452 cx.new_view(|cx| {
1453 Tooltip::new("Transform").key_binding(KeyBinding::for_action_in(
1454 &Assist,
1455 &focus_handle,
1456 cx,
1457 ))
1458 })
1459 .into()
1460 })
1461 .on_click({
1462 let editor = editor.clone();
1463 let step_range = step_range.clone();
1464 move |_, cx| {
1465 editor
1466 .update(cx, |this, cx| {
1467 this.apply_workflow_step(step_range.clone(), cx)
1468 })
1469 .ok();
1470 }
1471 })
1472 .into_any_element(),
1473 WorkflowStepStatus::Pending => Button::new(("stop-transformation", id), "Stop")
1474 .icon(IconName::Stop)
1475 .icon_position(IconPosition::Start)
1476 .icon_size(IconSize::Small)
1477 .label_size(LabelSize::Small)
1478 .style(ButtonStyle::Tinted(TintColor::Negative))
1479 .tooltip(move |cx| {
1480 cx.new_view(|cx| {
1481 Tooltip::new("Stop Transformation").key_binding(KeyBinding::for_action_in(
1482 &editor::actions::Cancel,
1483 &focus_handle,
1484 cx,
1485 ))
1486 })
1487 .into()
1488 })
1489 .on_click({
1490 let editor = editor.clone();
1491 let step_range = step_range.clone();
1492 move |_, cx| {
1493 editor
1494 .update(cx, |this, cx| {
1495 this.stop_workflow_step(step_range.clone(), cx)
1496 })
1497 .ok();
1498 }
1499 })
1500 .into_any_element(),
1501 WorkflowStepStatus::Done => h_flex()
1502 .gap_1()
1503 .child(
1504 Button::new(("stop-transformation", id), "Reject")
1505 .icon(IconName::Close)
1506 .icon_position(IconPosition::Start)
1507 .icon_size(IconSize::Small)
1508 .label_size(LabelSize::Small)
1509 .style(ButtonStyle::Tinted(TintColor::Negative))
1510 .tooltip({
1511 let focus_handle = focus_handle.clone();
1512 move |cx| {
1513 cx.new_view(|cx| {
1514 Tooltip::new("Reject Transformation").key_binding(
1515 KeyBinding::for_action_in(
1516 &editor::actions::Cancel,
1517 &focus_handle,
1518 cx,
1519 ),
1520 )
1521 })
1522 .into()
1523 }
1524 })
1525 .on_click({
1526 let editor = editor.clone();
1527 let step_range = step_range.clone();
1528 move |_, cx| {
1529 editor
1530 .update(cx, |this, cx| {
1531 this.reject_workflow_step(step_range.clone(), cx);
1532 })
1533 .ok();
1534 }
1535 }),
1536 )
1537 .child(
1538 Button::new(("confirm-workflow-step", id), "Accept")
1539 .icon(IconName::Check)
1540 .icon_position(IconPosition::Start)
1541 .icon_size(IconSize::Small)
1542 .label_size(LabelSize::Small)
1543 .style(ButtonStyle::Tinted(TintColor::Positive))
1544 .tooltip(move |cx| {
1545 cx.new_view(|cx| {
1546 Tooltip::new("Accept Transformation").key_binding(
1547 KeyBinding::for_action_in(&Assist, &focus_handle, cx),
1548 )
1549 })
1550 .into()
1551 })
1552 .on_click({
1553 let editor = editor.clone();
1554 let step_range = step_range.clone();
1555 move |_, cx| {
1556 editor
1557 .update(cx, |this, cx| {
1558 this.confirm_workflow_step(step_range.clone(), cx);
1559 })
1560 .ok();
1561 }
1562 }),
1563 )
1564 .into_any_element(),
1565 WorkflowStepStatus::Confirmed => h_flex()
1566 .child(
1567 Button::new(("revert-workflow-step", id), "Undo")
1568 .style(ButtonStyle::Filled)
1569 .icon(Some(IconName::Undo))
1570 .icon_position(IconPosition::Start)
1571 .icon_size(IconSize::Small)
1572 .label_size(LabelSize::Small)
1573 .tooltip(|cx| Tooltip::text("Undo Transformation", cx))
1574 .on_click({
1575 let editor = editor.clone();
1576 let step_range = step_range.clone();
1577 move |_, cx| {
1578 editor
1579 .update(cx, |this, cx| {
1580 this.undo_workflow_step(step_range.clone(), cx);
1581 })
1582 .ok();
1583 }
1584 }),
1585 )
1586 .into_any_element(),
1587 }
1588 }
1589}
1590
1591#[derive(Debug, Eq, PartialEq)]
1592struct ActiveWorkflowStep {
1593 range: Range<language::Anchor>,
1594 resolved: bool,
1595}
1596
1597struct WorkflowAssist {
1598 editor: WeakView<Editor>,
1599 editor_was_open: bool,
1600 assist_ids: Vec<InlineAssistId>,
1601 _observe_assist_status: Task<()>,
1602}
1603
1604pub struct ContextEditor {
1605 context: Model<Context>,
1606 fs: Arc<dyn Fs>,
1607 workspace: WeakView<Workspace>,
1608 project: Model<Project>,
1609 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1610 editor: View<Editor>,
1611 blocks: HashSet<CustomBlockId>,
1612 scroll_position: Option<ScrollPosition>,
1613 remote_id: Option<workspace::ViewId>,
1614 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1615 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1616 _subscriptions: Vec<Subscription>,
1617 workflow_steps: HashMap<Range<language::Anchor>, WorkflowStep>,
1618 active_workflow_step: Option<ActiveWorkflowStep>,
1619 assistant_panel: WeakView<AssistantPanel>,
1620 error_message: Option<SharedString>,
1621}
1622
1623const DEFAULT_TAB_TITLE: &str = "New Context";
1624const MAX_TAB_TITLE_LEN: usize = 16;
1625
1626impl ContextEditor {
1627 fn for_context(
1628 context: Model<Context>,
1629 fs: Arc<dyn Fs>,
1630 workspace: WeakView<Workspace>,
1631 project: Model<Project>,
1632 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1633 assistant_panel: WeakView<AssistantPanel>,
1634 cx: &mut ViewContext<Self>,
1635 ) -> Self {
1636 let completion_provider = SlashCommandCompletionProvider::new(
1637 Some(cx.view().downgrade()),
1638 Some(workspace.clone()),
1639 );
1640
1641 let editor = cx.new_view(|cx| {
1642 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1643 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1644 editor.set_show_line_numbers(false, cx);
1645 editor.set_show_git_diff_gutter(false, cx);
1646 editor.set_show_code_actions(false, cx);
1647 editor.set_show_runnables(false, cx);
1648 editor.set_show_wrap_guides(false, cx);
1649 editor.set_show_indent_guides(false, cx);
1650 editor.set_completion_provider(Box::new(completion_provider));
1651 editor.set_collaboration_hub(Box::new(project.clone()));
1652 editor
1653 });
1654
1655 let _subscriptions = vec![
1656 cx.observe(&context, |_, _, cx| cx.notify()),
1657 cx.subscribe(&context, Self::handle_context_event),
1658 cx.subscribe(&editor, Self::handle_editor_event),
1659 cx.subscribe(&editor, Self::handle_editor_search_event),
1660 ];
1661
1662 let sections = context.read(cx).slash_command_output_sections().to_vec();
1663 let mut this = Self {
1664 context,
1665 editor,
1666 lsp_adapter_delegate,
1667 blocks: Default::default(),
1668 scroll_position: None,
1669 remote_id: None,
1670 fs,
1671 workspace,
1672 project,
1673 pending_slash_command_creases: HashMap::default(),
1674 pending_slash_command_blocks: HashMap::default(),
1675 _subscriptions,
1676 workflow_steps: HashMap::default(),
1677 active_workflow_step: None,
1678 assistant_panel,
1679 error_message: None,
1680 };
1681 this.update_message_headers(cx);
1682 this.insert_slash_command_output_sections(sections, cx);
1683 this
1684 }
1685
1686 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1687 let command_name = DefaultSlashCommand.name();
1688 self.editor.update(cx, |editor, cx| {
1689 editor.insert(&format!("/{command_name}"), cx)
1690 });
1691 self.split(&Split, cx);
1692 let command = self.context.update(cx, |context, cx| {
1693 let first_message_id = context.messages(cx).next().unwrap().id;
1694 context.update_metadata(first_message_id, cx, |metadata| {
1695 metadata.role = Role::System;
1696 });
1697 context.reparse_slash_commands(cx);
1698 context.pending_slash_commands()[0].clone()
1699 });
1700
1701 self.run_command(
1702 command.source_range,
1703 &command.name,
1704 command.argument.as_deref(),
1705 false,
1706 self.workspace.clone(),
1707 cx,
1708 );
1709 }
1710
1711 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1712 if !self.apply_active_workflow_step(cx) {
1713 self.error_message = None;
1714 self.send_to_model(cx);
1715 cx.notify();
1716 }
1717 }
1718
1719 fn apply_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1720 self.show_workflow_step(range.clone(), cx);
1721
1722 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1723 if let Some(assist) = workflow_step.assist.as_ref() {
1724 let assist_ids = assist.assist_ids.clone();
1725 cx.window_context().defer(|cx| {
1726 InlineAssistant::update_global(cx, |assistant, cx| {
1727 for assist_id in assist_ids {
1728 assistant.start_assist(assist_id, cx);
1729 }
1730 })
1731 });
1732 }
1733 }
1734 }
1735
1736 fn apply_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1737 let Some(step) = self.active_workflow_step() else {
1738 return false;
1739 };
1740
1741 let range = step.range.clone();
1742 match step.status(cx) {
1743 WorkflowStepStatus::Resolving | WorkflowStepStatus::Pending => true,
1744 WorkflowStepStatus::Idle => {
1745 self.apply_workflow_step(range, cx);
1746 true
1747 }
1748 WorkflowStepStatus::Done => {
1749 self.confirm_workflow_step(range, cx);
1750 true
1751 }
1752 WorkflowStepStatus::Error(_) => {
1753 self.resolve_workflow_step(range, cx);
1754 true
1755 }
1756 WorkflowStepStatus::Confirmed => false,
1757 }
1758 }
1759
1760 fn resolve_workflow_step(
1761 &mut self,
1762 range: Range<language::Anchor>,
1763 cx: &mut ViewContext<Self>,
1764 ) {
1765 self.context.update(cx, |context, cx| {
1766 context.resolve_workflow_step(range, self.project.clone(), cx)
1767 });
1768 }
1769
1770 fn stop_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1771 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1772 if let Some(assist) = workflow_step.assist.as_ref() {
1773 let assist_ids = assist.assist_ids.clone();
1774 cx.window_context().defer(|cx| {
1775 InlineAssistant::update_global(cx, |assistant, cx| {
1776 for assist_id in assist_ids {
1777 assistant.stop_assist(assist_id, cx);
1778 }
1779 })
1780 });
1781 }
1782 }
1783 }
1784
1785 fn undo_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1786 if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
1787 if let Some(assist) = workflow_step.assist.take() {
1788 cx.window_context().defer(|cx| {
1789 InlineAssistant::update_global(cx, |assistant, cx| {
1790 for assist_id in assist.assist_ids {
1791 assistant.undo_assist(assist_id, cx);
1792 }
1793 })
1794 });
1795 }
1796 }
1797 }
1798
1799 fn confirm_workflow_step(
1800 &mut self,
1801 range: Range<language::Anchor>,
1802 cx: &mut ViewContext<Self>,
1803 ) {
1804 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1805 if let Some(assist) = workflow_step.assist.as_ref() {
1806 let assist_ids = assist.assist_ids.clone();
1807 cx.window_context().defer(move |cx| {
1808 InlineAssistant::update_global(cx, |assistant, cx| {
1809 for assist_id in assist_ids {
1810 assistant.finish_assist(assist_id, false, cx);
1811 }
1812 })
1813 });
1814 }
1815 }
1816 }
1817
1818 fn reject_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1819 if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
1820 if let Some(assist) = workflow_step.assist.take() {
1821 cx.window_context().defer(move |cx| {
1822 InlineAssistant::update_global(cx, |assistant, cx| {
1823 for assist_id in assist.assist_ids {
1824 assistant.finish_assist(assist_id, true, cx);
1825 }
1826 })
1827 });
1828 }
1829 }
1830 }
1831
1832 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1833 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1834 let new_selection = {
1835 let cursor = user_message
1836 .start
1837 .to_offset(self.context.read(cx).buffer().read(cx));
1838 cursor..cursor
1839 };
1840 self.editor.update(cx, |editor, cx| {
1841 editor.change_selections(
1842 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1843 cx,
1844 |selections| selections.select_ranges([new_selection]),
1845 );
1846 });
1847 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1848 cx.defer(|this, _| this.scroll_position = None);
1849 }
1850 }
1851
1852 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1853 if self
1854 .context
1855 .update(cx, |context, _| context.cancel_last_assist())
1856 {
1857 return;
1858 }
1859
1860 if let Some(active_step) = self.active_workflow_step() {
1861 match active_step.status(cx) {
1862 WorkflowStepStatus::Pending => {
1863 self.stop_workflow_step(active_step.range.clone(), cx);
1864 return;
1865 }
1866 WorkflowStepStatus::Done => {
1867 self.reject_workflow_step(active_step.range.clone(), cx);
1868 return;
1869 }
1870 _ => {}
1871 }
1872 }
1873 cx.propagate();
1874 }
1875
1876 fn debug_workflow_steps(&mut self, _: &DebugWorkflowSteps, cx: &mut ViewContext<Self>) {
1877 let mut output = String::new();
1878 for (i, step) in self.context.read(cx).workflow_steps().iter().enumerate() {
1879 output.push_str(&format!("Step {}:\n", i + 1));
1880 output.push_str(&format!(
1881 "Content: {}\n",
1882 self.context
1883 .read(cx)
1884 .buffer()
1885 .read(cx)
1886 .text_for_range(step.tagged_range.clone())
1887 .collect::<String>()
1888 ));
1889 match &step.status {
1890 crate::WorkflowStepStatus::Resolved(ResolvedWorkflowStep {
1891 title,
1892 suggestions,
1893 }) => {
1894 output.push_str("Resolution:\n");
1895 output.push_str(&format!(" {:?}\n", title));
1896 output.push_str(&format!(" {:?}\n", suggestions));
1897 }
1898 crate::WorkflowStepStatus::Pending(_) => {
1899 output.push_str("Resolution: Pending\n");
1900 }
1901 crate::WorkflowStepStatus::Error(error) => {
1902 writeln!(output, "Resolution: Error\n{:?}", error).unwrap();
1903 }
1904 }
1905 output.push('\n');
1906 }
1907
1908 let editor = self
1909 .workspace
1910 .update(cx, |workspace, cx| Editor::new_in_workspace(workspace, cx));
1911
1912 if let Ok(editor) = editor {
1913 cx.spawn(|_, mut cx| async move {
1914 let editor = editor.await?;
1915 editor.update(&mut cx, |editor, cx| editor.set_text(output, cx))
1916 })
1917 .detach_and_notify_err(cx);
1918 }
1919 }
1920
1921 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1922 let cursors = self.cursors(cx);
1923 self.context.update(cx, |context, cx| {
1924 let messages = context
1925 .messages_for_offsets(cursors, cx)
1926 .into_iter()
1927 .map(|message| message.id)
1928 .collect();
1929 context.cycle_message_roles(messages, cx)
1930 });
1931 }
1932
1933 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1934 let selections = self.editor.read(cx).selections.all::<usize>(cx);
1935 selections
1936 .into_iter()
1937 .map(|selection| selection.head())
1938 .collect()
1939 }
1940
1941 fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
1942 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
1943 self.editor.update(cx, |editor, cx| {
1944 editor.transact(cx, |editor, cx| {
1945 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
1946 let snapshot = editor.buffer().read(cx).snapshot(cx);
1947 let newest_cursor = editor.selections.newest::<Point>(cx).head();
1948 if newest_cursor.column > 0
1949 || snapshot
1950 .chars_at(newest_cursor)
1951 .next()
1952 .map_or(false, |ch| ch != '\n')
1953 {
1954 editor.move_to_end_of_line(
1955 &MoveToEndOfLine {
1956 stop_at_soft_wraps: false,
1957 },
1958 cx,
1959 );
1960 editor.newline(&Newline, cx);
1961 }
1962
1963 editor.insert(&format!("/{name}"), cx);
1964 if command.requires_argument() {
1965 editor.insert(" ", cx);
1966 editor.show_completions(&ShowCompletions::default(), cx);
1967 }
1968 });
1969 });
1970 if !command.requires_argument() {
1971 self.confirm_command(&ConfirmCommand, cx);
1972 }
1973 }
1974 }
1975
1976 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
1977 let selections = self.editor.read(cx).selections.disjoint_anchors();
1978 let mut commands_by_range = HashMap::default();
1979 let workspace = self.workspace.clone();
1980 self.context.update(cx, |context, cx| {
1981 context.reparse_slash_commands(cx);
1982 for selection in selections.iter() {
1983 if let Some(command) =
1984 context.pending_command_for_position(selection.head().text_anchor, cx)
1985 {
1986 commands_by_range
1987 .entry(command.source_range.clone())
1988 .or_insert_with(|| command.clone());
1989 }
1990 }
1991 });
1992
1993 if commands_by_range.is_empty() {
1994 cx.propagate();
1995 } else {
1996 for command in commands_by_range.into_values() {
1997 self.run_command(
1998 command.source_range,
1999 &command.name,
2000 command.argument.as_deref(),
2001 true,
2002 workspace.clone(),
2003 cx,
2004 );
2005 }
2006 cx.stop_propagation();
2007 }
2008 }
2009
2010 pub fn run_command(
2011 &mut self,
2012 command_range: Range<language::Anchor>,
2013 name: &str,
2014 argument: Option<&str>,
2015 insert_trailing_newline: bool,
2016 workspace: WeakView<Workspace>,
2017 cx: &mut ViewContext<Self>,
2018 ) {
2019 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
2020 let argument = argument.map(ToString::to_string);
2021 let output = command.run(
2022 argument.as_deref(),
2023 workspace,
2024 self.lsp_adapter_delegate.clone(),
2025 cx,
2026 );
2027 self.context.update(cx, |context, cx| {
2028 context.insert_command_output(command_range, output, insert_trailing_newline, cx)
2029 });
2030 }
2031 }
2032
2033 fn handle_context_event(
2034 &mut self,
2035 _: Model<Context>,
2036 event: &ContextEvent,
2037 cx: &mut ViewContext<Self>,
2038 ) {
2039 let context_editor = cx.view().downgrade();
2040
2041 match event {
2042 ContextEvent::MessagesEdited => {
2043 self.update_message_headers(cx);
2044 self.context.update(cx, |context, cx| {
2045 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2046 });
2047 }
2048 ContextEvent::WorkflowStepsRemoved(removed) => {
2049 self.remove_workflow_steps(removed, cx);
2050 cx.notify();
2051 }
2052 ContextEvent::WorkflowStepUpdated(updated) => {
2053 self.update_workflow_step(updated.clone(), cx);
2054 cx.notify();
2055 }
2056 ContextEvent::SummaryChanged => {
2057 cx.emit(EditorEvent::TitleChanged);
2058 self.context.update(cx, |context, cx| {
2059 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2060 });
2061 }
2062 ContextEvent::StreamedCompletion => {
2063 self.editor.update(cx, |editor, cx| {
2064 if let Some(scroll_position) = self.scroll_position {
2065 let snapshot = editor.snapshot(cx);
2066 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2067 let scroll_top =
2068 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2069 editor.set_scroll_position(
2070 point(scroll_position.offset_before_cursor.x, scroll_top),
2071 cx,
2072 );
2073 }
2074 });
2075 }
2076 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2077 self.editor.update(cx, |editor, cx| {
2078 let buffer = editor.buffer().read(cx).snapshot(cx);
2079 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2080 let excerpt_id = *excerpt_id;
2081
2082 editor.remove_creases(
2083 removed
2084 .iter()
2085 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2086 cx,
2087 );
2088
2089 editor.remove_blocks(
2090 HashSet::from_iter(
2091 removed.iter().filter_map(|range| {
2092 self.pending_slash_command_blocks.remove(range)
2093 }),
2094 ),
2095 None,
2096 cx,
2097 );
2098
2099 let crease_ids = editor.insert_creases(
2100 updated.iter().map(|command| {
2101 let workspace = self.workspace.clone();
2102 let confirm_command = Arc::new({
2103 let context_editor = context_editor.clone();
2104 let command = command.clone();
2105 move |cx: &mut WindowContext| {
2106 context_editor
2107 .update(cx, |context_editor, cx| {
2108 context_editor.run_command(
2109 command.source_range.clone(),
2110 &command.name,
2111 command.argument.as_deref(),
2112 false,
2113 workspace.clone(),
2114 cx,
2115 );
2116 })
2117 .ok();
2118 }
2119 });
2120 let placeholder = FoldPlaceholder {
2121 render: Arc::new(move |_, _, _| Empty.into_any()),
2122 constrain_width: false,
2123 merge_adjacent: false,
2124 };
2125 let render_toggle = {
2126 let confirm_command = confirm_command.clone();
2127 let command = command.clone();
2128 move |row, _, _, _cx: &mut WindowContext| {
2129 render_pending_slash_command_gutter_decoration(
2130 row,
2131 &command.status,
2132 confirm_command.clone(),
2133 )
2134 }
2135 };
2136 let render_trailer = {
2137 let command = command.clone();
2138 move |row, _unfold, cx: &mut WindowContext| {
2139 // TODO: In the future we should investigate how we can expose
2140 // this as a hook on the `SlashCommand` trait so that we don't
2141 // need to special-case it here.
2142 if command.name == DocsSlashCommand::NAME {
2143 return render_docs_slash_command_trailer(
2144 row,
2145 command.clone(),
2146 cx,
2147 );
2148 }
2149
2150 Empty.into_any()
2151 }
2152 };
2153
2154 let start = buffer
2155 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2156 .unwrap();
2157 let end = buffer
2158 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2159 .unwrap();
2160 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2161 }),
2162 cx,
2163 );
2164
2165 let block_ids = editor.insert_blocks(
2166 updated
2167 .iter()
2168 .filter_map(|command| match &command.status {
2169 PendingSlashCommandStatus::Error(error) => {
2170 Some((command, error.clone()))
2171 }
2172 _ => None,
2173 })
2174 .map(|(command, error_message)| BlockProperties {
2175 style: BlockStyle::Fixed,
2176 position: Anchor {
2177 buffer_id: Some(buffer_id),
2178 excerpt_id,
2179 text_anchor: command.source_range.start,
2180 },
2181 height: 1,
2182 disposition: BlockDisposition::Below,
2183 render: slash_command_error_block_renderer(error_message),
2184 priority: 0,
2185 }),
2186 None,
2187 cx,
2188 );
2189
2190 self.pending_slash_command_creases.extend(
2191 updated
2192 .iter()
2193 .map(|command| command.source_range.clone())
2194 .zip(crease_ids),
2195 );
2196
2197 self.pending_slash_command_blocks.extend(
2198 updated
2199 .iter()
2200 .map(|command| command.source_range.clone())
2201 .zip(block_ids),
2202 );
2203 })
2204 }
2205 ContextEvent::SlashCommandFinished {
2206 output_range,
2207 sections,
2208 run_commands_in_output,
2209 } => {
2210 self.insert_slash_command_output_sections(sections.iter().cloned(), cx);
2211
2212 if *run_commands_in_output {
2213 let commands = self.context.update(cx, |context, cx| {
2214 context.reparse_slash_commands(cx);
2215 context
2216 .pending_commands_for_range(output_range.clone(), cx)
2217 .to_vec()
2218 });
2219
2220 for command in commands {
2221 self.run_command(
2222 command.source_range,
2223 &command.name,
2224 command.argument.as_deref(),
2225 false,
2226 self.workspace.clone(),
2227 cx,
2228 );
2229 }
2230 }
2231 }
2232 ContextEvent::Operation(_) => {}
2233 ContextEvent::AssistError(error_message) => {
2234 self.error_message = Some(SharedString::from(error_message.clone()));
2235 }
2236 }
2237 }
2238
2239 fn insert_slash_command_output_sections(
2240 &mut self,
2241 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2242 cx: &mut ViewContext<Self>,
2243 ) {
2244 self.editor.update(cx, |editor, cx| {
2245 let buffer = editor.buffer().read(cx).snapshot(cx);
2246 let excerpt_id = *buffer.as_singleton().unwrap().0;
2247 let mut buffer_rows_to_fold = BTreeSet::new();
2248 let mut creases = Vec::new();
2249 for section in sections {
2250 let start = buffer
2251 .anchor_in_excerpt(excerpt_id, section.range.start)
2252 .unwrap();
2253 let end = buffer
2254 .anchor_in_excerpt(excerpt_id, section.range.end)
2255 .unwrap();
2256 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2257 buffer_rows_to_fold.insert(buffer_row);
2258 creases.push(Crease::new(
2259 start..end,
2260 FoldPlaceholder {
2261 render: Arc::new({
2262 let editor = cx.view().downgrade();
2263 let icon = section.icon;
2264 let label = section.label.clone();
2265 move |fold_id, fold_range, _cx| {
2266 let editor = editor.clone();
2267 ButtonLike::new(fold_id)
2268 .style(ButtonStyle::Filled)
2269 .layer(ElevationIndex::ElevatedSurface)
2270 .child(Icon::new(icon))
2271 .child(Label::new(label.clone()).single_line())
2272 .on_click(move |_, cx| {
2273 editor
2274 .update(cx, |editor, cx| {
2275 let buffer_start = fold_range
2276 .start
2277 .to_point(&editor.buffer().read(cx).read(cx));
2278 let buffer_row = MultiBufferRow(buffer_start.row);
2279 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2280 })
2281 .ok();
2282 })
2283 .into_any_element()
2284 }
2285 }),
2286 constrain_width: false,
2287 merge_adjacent: false,
2288 },
2289 render_slash_command_output_toggle,
2290 |_, _, _| Empty.into_any_element(),
2291 ));
2292 }
2293
2294 editor.insert_creases(creases, cx);
2295
2296 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2297 editor.fold_at(&FoldAt { buffer_row }, cx);
2298 }
2299 });
2300 }
2301
2302 fn handle_editor_event(
2303 &mut self,
2304 _: View<Editor>,
2305 event: &EditorEvent,
2306 cx: &mut ViewContext<Self>,
2307 ) {
2308 match event {
2309 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2310 let cursor_scroll_position = self.cursor_scroll_position(cx);
2311 if *autoscroll {
2312 self.scroll_position = cursor_scroll_position;
2313 } else if self.scroll_position != cursor_scroll_position {
2314 self.scroll_position = None;
2315 }
2316 }
2317 EditorEvent::SelectionsChanged { .. } => {
2318 self.scroll_position = self.cursor_scroll_position(cx);
2319 self.update_active_workflow_step(cx);
2320 }
2321 _ => {}
2322 }
2323 cx.emit(event.clone());
2324 }
2325
2326 fn active_workflow_step(&self) -> Option<&WorkflowStep> {
2327 let step = self.active_workflow_step.as_ref()?;
2328 self.workflow_steps.get(&step.range)
2329 }
2330
2331 fn remove_workflow_steps(
2332 &mut self,
2333 removed_steps: &[Range<language::Anchor>],
2334 cx: &mut ViewContext<Self>,
2335 ) {
2336 let mut blocks_to_remove = HashSet::default();
2337 for step_range in removed_steps {
2338 self.hide_workflow_step(step_range.clone(), cx);
2339 if let Some(step) = self.workflow_steps.remove(step_range) {
2340 blocks_to_remove.insert(step.header_block_id);
2341 blocks_to_remove.insert(step.footer_block_id);
2342 }
2343 }
2344 self.editor.update(cx, |editor, cx| {
2345 editor.remove_blocks(blocks_to_remove, None, cx)
2346 });
2347 self.update_active_workflow_step(cx);
2348 }
2349
2350 fn update_workflow_step(
2351 &mut self,
2352 step_range: Range<language::Anchor>,
2353 cx: &mut ViewContext<Self>,
2354 ) {
2355 let buffer_snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2356 let (&excerpt_id, _, _) = buffer_snapshot.as_singleton().unwrap();
2357
2358 let Some(step) = self
2359 .context
2360 .read(cx)
2361 .workflow_step_for_range(step_range.clone())
2362 else {
2363 return;
2364 };
2365
2366 let resolved_step = step.status.into_resolved();
2367 if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) {
2368 existing_step.resolved_step = resolved_step;
2369 } else {
2370 let start = buffer_snapshot
2371 .anchor_in_excerpt(excerpt_id, step_range.start)
2372 .unwrap();
2373 let end = buffer_snapshot
2374 .anchor_in_excerpt(excerpt_id, step_range.end)
2375 .unwrap();
2376 let weak_self = cx.view().downgrade();
2377 let block_ids = self.editor.update(cx, |editor, cx| {
2378 let step_range = step_range.clone();
2379 let editor_focus_handle = editor.focus_handle(cx);
2380 editor.insert_blocks(
2381 vec![
2382 BlockProperties {
2383 position: start,
2384 height: 1,
2385 style: BlockStyle::Sticky,
2386 render: Box::new({
2387 let weak_self = weak_self.clone();
2388 let step_range = step_range.clone();
2389 move |cx| {
2390 let current_status = weak_self
2391 .update(&mut **cx, |context_editor, cx| {
2392 let step =
2393 context_editor.workflow_steps.get(&step_range)?;
2394 Some(step.status(cx))
2395 })
2396 .ok()
2397 .flatten();
2398
2399 let theme = cx.theme().status();
2400 let border_color = if current_status
2401 .as_ref()
2402 .map_or(false, |status| status.is_confirmed())
2403 {
2404 theme.ignored_border
2405 } else {
2406 theme.info_border
2407 };
2408
2409 div()
2410 .w_full()
2411 .px(cx.gutter_dimensions.full_width())
2412 .child(
2413 h_flex()
2414 .w_full()
2415 .border_b_1()
2416 .border_color(border_color)
2417 .pb_1()
2418 .justify_end()
2419 .gap_2()
2420 .children(current_status.as_ref().map(|status| {
2421 status.into_element(
2422 step_range.clone(),
2423 editor_focus_handle.clone(),
2424 weak_self.clone(),
2425 cx,
2426 )
2427 })),
2428 )
2429 .into_any()
2430 }
2431 }),
2432 disposition: BlockDisposition::Above,
2433 priority: 0,
2434 },
2435 BlockProperties {
2436 position: end,
2437 height: 0,
2438 style: BlockStyle::Sticky,
2439 render: Box::new(move |cx| {
2440 let current_status = weak_self
2441 .update(&mut **cx, |context_editor, cx| {
2442 let step =
2443 context_editor.workflow_steps.get(&step_range)?;
2444 Some(step.status(cx))
2445 })
2446 .ok()
2447 .flatten();
2448 let theme = cx.theme().status();
2449 let border_color = if current_status
2450 .as_ref()
2451 .map_or(false, |status| status.is_confirmed())
2452 {
2453 theme.ignored_border
2454 } else {
2455 theme.info_border
2456 };
2457
2458 div()
2459 .w_full()
2460 .px(cx.gutter_dimensions.full_width())
2461 .child(
2462 h_flex().w_full().border_t_1().border_color(border_color),
2463 )
2464 .into_any()
2465 }),
2466 disposition: BlockDisposition::Below,
2467 priority: 0,
2468 },
2469 ],
2470 None,
2471 cx,
2472 )
2473 });
2474 self.workflow_steps.insert(
2475 step_range.clone(),
2476 WorkflowStep {
2477 range: step_range.clone(),
2478 header_block_id: block_ids[0],
2479 footer_block_id: block_ids[1],
2480 resolved_step,
2481 assist: None,
2482 },
2483 );
2484 }
2485
2486 self.update_active_workflow_step(cx);
2487 }
2488
2489 fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
2490 let new_step = self.active_workflow_step_for_cursor(cx);
2491 if new_step.as_ref() != self.active_workflow_step.as_ref() {
2492 if let Some(old_step) = self.active_workflow_step.take() {
2493 self.hide_workflow_step(old_step.range, cx);
2494 }
2495
2496 if let Some(new_step) = new_step {
2497 self.show_workflow_step(new_step.range.clone(), cx);
2498 self.active_workflow_step = Some(new_step);
2499 }
2500 }
2501 }
2502
2503 fn hide_workflow_step(
2504 &mut self,
2505 step_range: Range<language::Anchor>,
2506 cx: &mut ViewContext<Self>,
2507 ) {
2508 let Some(step) = self.workflow_steps.get_mut(&step_range) else {
2509 return;
2510 };
2511 let Some(assist) = step.assist.as_ref() else {
2512 return;
2513 };
2514 let Some(editor) = assist.editor.upgrade() else {
2515 return;
2516 };
2517
2518 if matches!(step.status(cx), WorkflowStepStatus::Idle) {
2519 let assist = step.assist.take().unwrap();
2520 InlineAssistant::update_global(cx, |assistant, cx| {
2521 for assist_id in assist.assist_ids {
2522 assistant.finish_assist(assist_id, true, cx)
2523 }
2524 });
2525
2526 self.workspace
2527 .update(cx, |workspace, cx| {
2528 if let Some(pane) = workspace.pane_for(&editor) {
2529 pane.update(cx, |pane, cx| {
2530 let item_id = editor.entity_id();
2531 if !assist.editor_was_open && pane.is_active_preview_item(item_id) {
2532 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
2533 .detach_and_log_err(cx);
2534 }
2535 });
2536 }
2537 })
2538 .ok();
2539 }
2540 }
2541
2542 fn show_workflow_step(
2543 &mut self,
2544 step_range: Range<language::Anchor>,
2545 cx: &mut ViewContext<Self>,
2546 ) {
2547 let Some(step) = self.workflow_steps.get_mut(&step_range) else {
2548 return;
2549 };
2550
2551 let mut scroll_to_assist_id = None;
2552 match step.status(cx) {
2553 WorkflowStepStatus::Idle => {
2554 if let Some(assist) = step.assist.as_ref() {
2555 scroll_to_assist_id = assist.assist_ids.first().copied();
2556 } else if let Some(Ok(resolved)) = step.resolved_step.as_ref() {
2557 step.assist = Self::open_assists_for_step(
2558 resolved,
2559 &self.project,
2560 &self.assistant_panel,
2561 &self.workspace,
2562 cx,
2563 );
2564 }
2565 }
2566 WorkflowStepStatus::Pending => {
2567 if let Some(assist) = step.assist.as_ref() {
2568 let assistant = InlineAssistant::global(cx);
2569 scroll_to_assist_id = assist
2570 .assist_ids
2571 .iter()
2572 .copied()
2573 .find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
2574 }
2575 }
2576 WorkflowStepStatus::Done => {
2577 if let Some(assist) = step.assist.as_ref() {
2578 scroll_to_assist_id = assist.assist_ids.first().copied();
2579 }
2580 }
2581 _ => {}
2582 }
2583
2584 if let Some(assist_id) = scroll_to_assist_id {
2585 if let Some(editor) = step
2586 .assist
2587 .as_ref()
2588 .and_then(|assists| assists.editor.upgrade())
2589 {
2590 self.workspace
2591 .update(cx, |workspace, cx| {
2592 workspace.activate_item(&editor, false, false, cx);
2593 })
2594 .ok();
2595 InlineAssistant::update_global(cx, |assistant, cx| {
2596 assistant.scroll_to_assist(assist_id, cx)
2597 });
2598 }
2599 }
2600 }
2601
2602 fn open_assists_for_step(
2603 resolved_step: &ResolvedWorkflowStep,
2604 project: &Model<Project>,
2605 assistant_panel: &WeakView<AssistantPanel>,
2606 workspace: &WeakView<Workspace>,
2607 cx: &mut ViewContext<Self>,
2608 ) -> Option<WorkflowAssist> {
2609 let assistant_panel = assistant_panel.upgrade()?;
2610 if resolved_step.suggestions.is_empty() {
2611 return None;
2612 }
2613
2614 let editor;
2615 let mut editor_was_open = false;
2616 let mut suggestion_groups = Vec::new();
2617 if resolved_step.suggestions.len() == 1
2618 && resolved_step.suggestions.values().next().unwrap().len() == 1
2619 {
2620 // If there's only one buffer and one suggestion group, open it directly
2621 let (buffer, groups) = resolved_step.suggestions.iter().next().unwrap();
2622 let group = groups.into_iter().next().unwrap();
2623 editor = workspace
2624 .update(cx, |workspace, cx| {
2625 let active_pane = workspace.active_pane().clone();
2626 editor_was_open =
2627 workspace.is_project_item_open::<Editor>(&active_pane, buffer, cx);
2628 workspace.open_project_item::<Editor>(
2629 active_pane,
2630 buffer.clone(),
2631 false,
2632 false,
2633 cx,
2634 )
2635 })
2636 .log_err()?;
2637
2638 let (&excerpt_id, _, _) = editor
2639 .read(cx)
2640 .buffer()
2641 .read(cx)
2642 .read(cx)
2643 .as_singleton()
2644 .unwrap();
2645
2646 // Scroll the editor to the suggested assist
2647 editor.update(cx, |editor, cx| {
2648 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2649 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2650 let anchor = if group.context_range.start.to_offset(buffer) == 0 {
2651 Anchor::min()
2652 } else {
2653 multibuffer
2654 .anchor_in_excerpt(excerpt_id, group.context_range.start)
2655 .unwrap()
2656 };
2657
2658 editor.set_scroll_anchor(
2659 ScrollAnchor {
2660 offset: gpui::Point::default(),
2661 anchor,
2662 },
2663 cx,
2664 );
2665 });
2666
2667 suggestion_groups.push((excerpt_id, group));
2668 } else {
2669 // If there are multiple buffers or suggestion groups, create a multibuffer
2670 let multibuffer = cx.new_model(|cx| {
2671 let replica_id = project.read(cx).replica_id();
2672 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2673 .with_title(resolved_step.title.clone());
2674 for (buffer, groups) in &resolved_step.suggestions {
2675 let excerpt_ids = multibuffer.push_excerpts(
2676 buffer.clone(),
2677 groups.iter().map(|suggestion_group| ExcerptRange {
2678 context: suggestion_group.context_range.clone(),
2679 primary: None,
2680 }),
2681 cx,
2682 );
2683 suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
2684 }
2685 multibuffer
2686 });
2687
2688 editor = cx.new_view(|cx| {
2689 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
2690 });
2691 workspace
2692 .update(cx, |workspace, cx| {
2693 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2694 })
2695 .log_err()?;
2696 }
2697
2698 let mut assist_ids = Vec::new();
2699 for (excerpt_id, suggestion_group) in suggestion_groups {
2700 for suggestion in &suggestion_group.suggestions {
2701 assist_ids.extend(suggestion.show(
2702 &editor,
2703 excerpt_id,
2704 workspace,
2705 &assistant_panel,
2706 cx,
2707 ));
2708 }
2709 }
2710
2711 let mut observations = Vec::new();
2712 InlineAssistant::update_global(cx, |assistant, _cx| {
2713 for assist_id in &assist_ids {
2714 observations.push(assistant.observe_assist(*assist_id));
2715 }
2716 });
2717
2718 Some(WorkflowAssist {
2719 assist_ids,
2720 editor: editor.downgrade(),
2721 editor_was_open,
2722 _observe_assist_status: cx.spawn(|this, mut cx| async move {
2723 while !observations.is_empty() {
2724 let (result, ix, _) = futures::future::select_all(
2725 observations
2726 .iter_mut()
2727 .map(|observation| Box::pin(observation.changed())),
2728 )
2729 .await;
2730
2731 if result.is_err() {
2732 observations.remove(ix);
2733 }
2734
2735 if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
2736 break;
2737 }
2738 }
2739 }),
2740 })
2741 }
2742
2743 fn handle_editor_search_event(
2744 &mut self,
2745 _: View<Editor>,
2746 event: &SearchEvent,
2747 cx: &mut ViewContext<Self>,
2748 ) {
2749 cx.emit(event.clone());
2750 }
2751
2752 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2753 self.editor.update(cx, |editor, cx| {
2754 let snapshot = editor.snapshot(cx);
2755 let cursor = editor.selections.newest_anchor().head();
2756 let cursor_row = cursor
2757 .to_display_point(&snapshot.display_snapshot)
2758 .row()
2759 .as_f32();
2760 let scroll_position = editor
2761 .scroll_manager
2762 .anchor()
2763 .scroll_position(&snapshot.display_snapshot);
2764
2765 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2766 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2767 Some(ScrollPosition {
2768 cursor,
2769 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2770 })
2771 } else {
2772 None
2773 }
2774 })
2775 }
2776
2777 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2778 self.editor.update(cx, |editor, cx| {
2779 let buffer = editor.buffer().read(cx).snapshot(cx);
2780 let excerpt_id = *buffer.as_singleton().unwrap().0;
2781 let old_blocks = std::mem::take(&mut self.blocks);
2782 let new_blocks = self
2783 .context
2784 .read(cx)
2785 .messages(cx)
2786 .map(|message| BlockProperties {
2787 position: buffer
2788 .anchor_in_excerpt(excerpt_id, message.anchor)
2789 .unwrap(),
2790 height: 2,
2791 style: BlockStyle::Sticky,
2792 render: Box::new({
2793 let context = self.context.clone();
2794 move |cx| {
2795 let message_id = message.id;
2796 let sender = ButtonLike::new("role")
2797 .style(ButtonStyle::Filled)
2798 .child(match message.role {
2799 Role::User => Label::new("You").color(Color::Default),
2800 Role::Assistant => Label::new("Assistant").color(Color::Info),
2801 Role::System => Label::new("System").color(Color::Warning),
2802 })
2803 .tooltip(|cx| {
2804 Tooltip::with_meta(
2805 "Toggle message role",
2806 None,
2807 "Available roles: You (User), Assistant, System",
2808 cx,
2809 )
2810 })
2811 .on_click({
2812 let context = context.clone();
2813 move |_, cx| {
2814 context.update(cx, |context, cx| {
2815 context.cycle_message_roles(
2816 HashSet::from_iter(Some(message_id)),
2817 cx,
2818 )
2819 })
2820 }
2821 });
2822
2823 let trigger = Button::new("show-error", "Error")
2824 .color(Color::Error)
2825 .selected_label_color(Color::Error)
2826 .selected_icon_color(Color::Error)
2827 .icon(IconName::XCircle)
2828 .icon_color(Color::Error)
2829 .icon_size(IconSize::Small)
2830 .icon_position(IconPosition::Start)
2831 .tooltip(move |cx| {
2832 Tooltip::with_meta(
2833 "Error interacting with language model",
2834 None,
2835 "Click for more details",
2836 cx,
2837 )
2838 });
2839 h_flex()
2840 .id(("message_header", message_id.as_u64()))
2841 .pl(cx.gutter_dimensions.full_width())
2842 .h_11()
2843 .w_full()
2844 .relative()
2845 .gap_1()
2846 .child(sender)
2847 .children(
2848 if let MessageStatus::Error(error) = message.status.clone() {
2849 Some(
2850 PopoverMenu::new("show-error-popover")
2851 .menu(move |cx| {
2852 Some(cx.new_view(|cx| ErrorPopover {
2853 error: error.clone(),
2854 focus_handle: cx.focus_handle(),
2855 }))
2856 })
2857 .trigger(trigger),
2858 )
2859 } else {
2860 None
2861 },
2862 )
2863 .into_any_element()
2864 }
2865 }),
2866 disposition: BlockDisposition::Above,
2867 priority: usize::MAX,
2868 })
2869 .collect::<Vec<_>>();
2870
2871 editor.remove_blocks(old_blocks, None, cx);
2872 let ids = editor.insert_blocks(new_blocks, None, cx);
2873 self.blocks = HashSet::from_iter(ids);
2874 });
2875 }
2876
2877 fn insert_selection(
2878 workspace: &mut Workspace,
2879 _: &InsertIntoEditor,
2880 cx: &mut ViewContext<Workspace>,
2881 ) {
2882 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2883 return;
2884 };
2885 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
2886 return;
2887 };
2888 let Some(active_editor_view) = workspace
2889 .active_item(cx)
2890 .and_then(|item| item.act_as::<Editor>(cx))
2891 else {
2892 return;
2893 };
2894
2895 let context_editor = context_editor_view.read(cx).editor.read(cx);
2896 let anchor = context_editor.selections.newest_anchor();
2897 let text = context_editor
2898 .buffer()
2899 .read(cx)
2900 .read(cx)
2901 .text_for_range(anchor.range())
2902 .collect::<String>();
2903
2904 // If nothing is selected, don't delete the current selection; instead, be a no-op.
2905 if !text.is_empty() {
2906 active_editor_view.update(cx, |editor, cx| {
2907 editor.insert(&text, cx);
2908 editor.focus(cx);
2909 })
2910 }
2911 }
2912
2913 fn quote_selection(
2914 workspace: &mut Workspace,
2915 _: &QuoteSelection,
2916 cx: &mut ViewContext<Workspace>,
2917 ) {
2918 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2919 return;
2920 };
2921 let Some(editor) = workspace
2922 .active_item(cx)
2923 .and_then(|item| item.act_as::<Editor>(cx))
2924 else {
2925 return;
2926 };
2927
2928 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
2929 let editor = editor.read(cx);
2930 let buffer = editor.buffer().read(cx).snapshot(cx);
2931 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
2932 ..editor::ToOffset::to_offset(&selection.end, &buffer);
2933 let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
2934 if selected_text.is_empty() {
2935 return;
2936 }
2937
2938 let start_language = buffer.language_at(range.start);
2939 let end_language = buffer.language_at(range.end);
2940 let language_name = if start_language == end_language {
2941 start_language.map(|language| language.code_fence_block_name())
2942 } else {
2943 None
2944 };
2945 let language_name = language_name.as_deref().unwrap_or("");
2946
2947 let filename = buffer
2948 .file_at(selection.start)
2949 .map(|file| file.full_path(cx));
2950
2951 let text = if language_name == "markdown" {
2952 selected_text
2953 .lines()
2954 .map(|line| format!("> {}", line))
2955 .collect::<Vec<_>>()
2956 .join("\n")
2957 } else {
2958 let start_symbols = buffer
2959 .symbols_containing(selection.start, None)
2960 .map(|(_, symbols)| symbols);
2961 let end_symbols = buffer
2962 .symbols_containing(selection.end, None)
2963 .map(|(_, symbols)| symbols);
2964
2965 let outline_text =
2966 if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
2967 Some(
2968 start_symbols
2969 .into_iter()
2970 .zip(end_symbols)
2971 .take_while(|(a, b)| a == b)
2972 .map(|(a, _)| a.text)
2973 .collect::<Vec<_>>()
2974 .join(" > "),
2975 )
2976 } else {
2977 None
2978 };
2979
2980 let line_comment_prefix = start_language
2981 .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
2982
2983 let fence = codeblock_fence_for_path(
2984 filename.as_deref(),
2985 Some(selection.start.row..selection.end.row),
2986 );
2987
2988 if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
2989 {
2990 let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
2991 format!("{fence}{breadcrumb}{selected_text}\n```")
2992 } else {
2993 format!("{fence}{selected_text}\n```")
2994 }
2995 };
2996
2997 let crease_title = if let Some(path) = filename {
2998 let start_line = selection.start.row + 1;
2999 let end_line = selection.end.row + 1;
3000 if start_line == end_line {
3001 format!("{}, Line {}", path.display(), start_line)
3002 } else {
3003 format!("{}, Lines {} to {}", path.display(), start_line, end_line)
3004 }
3005 } else {
3006 "Quoted selection".to_string()
3007 };
3008
3009 // Activate the panel
3010 if !panel.focus_handle(cx).contains_focused(cx) {
3011 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3012 }
3013
3014 panel.update(cx, |_, cx| {
3015 // Wait to create a new context until the workspace is no longer
3016 // being updated.
3017 cx.defer(move |panel, cx| {
3018 if let Some(context) = panel
3019 .active_context_editor(cx)
3020 .or_else(|| panel.new_context(cx))
3021 {
3022 context.update(cx, |context, cx| {
3023 context.editor.update(cx, |editor, cx| {
3024 editor.insert("\n", cx);
3025
3026 let point = editor.selections.newest::<Point>(cx).head();
3027 let start_row = MultiBufferRow(point.row);
3028
3029 editor.insert(&text, cx);
3030
3031 let snapshot = editor.buffer().read(cx).snapshot(cx);
3032 let anchor_before = snapshot.anchor_after(point);
3033 let anchor_after = editor
3034 .selections
3035 .newest_anchor()
3036 .head()
3037 .bias_left(&snapshot);
3038
3039 editor.insert("\n", cx);
3040
3041 let fold_placeholder = quote_selection_fold_placeholder(
3042 crease_title,
3043 cx.view().downgrade(),
3044 );
3045 let crease = Crease::new(
3046 anchor_before..anchor_after,
3047 fold_placeholder,
3048 render_quote_selection_output_toggle,
3049 |_, _, _| Empty.into_any(),
3050 );
3051 editor.insert_creases(vec![crease], cx);
3052 editor.fold_at(
3053 &FoldAt {
3054 buffer_row: start_row,
3055 },
3056 cx,
3057 );
3058 })
3059 });
3060 };
3061 });
3062 });
3063 }
3064
3065 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3066 let editor = self.editor.read(cx);
3067 let context = self.context.read(cx);
3068 if editor.selections.count() == 1 {
3069 let selection = editor.selections.newest::<usize>(cx);
3070 let mut copied_text = String::new();
3071 let mut spanned_messages = 0;
3072 for message in context.messages(cx) {
3073 if message.offset_range.start >= selection.range().end {
3074 break;
3075 } else if message.offset_range.end >= selection.range().start {
3076 let range = cmp::max(message.offset_range.start, selection.range().start)
3077 ..cmp::min(message.offset_range.end, selection.range().end);
3078 if !range.is_empty() {
3079 spanned_messages += 1;
3080 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
3081 for chunk in context.buffer().read(cx).text_for_range(range) {
3082 copied_text.push_str(chunk);
3083 }
3084 copied_text.push('\n');
3085 }
3086 }
3087 }
3088
3089 if spanned_messages > 1 {
3090 cx.write_to_clipboard(ClipboardItem::new(copied_text));
3091 return;
3092 }
3093 }
3094
3095 cx.propagate();
3096 }
3097
3098 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3099 self.context.update(cx, |context, cx| {
3100 let selections = self.editor.read(cx).selections.disjoint_anchors();
3101 for selection in selections.as_ref() {
3102 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3103 let range = selection
3104 .map(|endpoint| endpoint.to_offset(&buffer))
3105 .range();
3106 context.split_message(range, cx);
3107 }
3108 });
3109 }
3110
3111 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3112 self.context.update(cx, |context, cx| {
3113 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
3114 });
3115 }
3116
3117 fn title(&self, cx: &AppContext) -> Cow<str> {
3118 self.context
3119 .read(cx)
3120 .summary()
3121 .map(|summary| summary.text.clone())
3122 .map(Cow::Owned)
3123 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
3124 }
3125
3126 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3127 use feature_flags::FeatureFlagAppExt;
3128 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
3129 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
3130 });
3131
3132 if nudge.map_or(false, |value| value) {
3133 Some(
3134 h_flex()
3135 .p_3()
3136 .border_b_1()
3137 .border_color(cx.theme().colors().border_variant)
3138 .bg(cx.theme().colors().editor_background)
3139 .justify_between()
3140 .child(
3141 h_flex()
3142 .gap_3()
3143 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
3144 .child(Label::new("Zed AI is here! Get started by signing in →")),
3145 )
3146 .child(
3147 Button::new("sign-in", "Sign in")
3148 .size(ButtonSize::Compact)
3149 .style(ButtonStyle::Filled)
3150 .on_click(cx.listener(|this, _event, cx| {
3151 let client = this
3152 .workspace
3153 .update(cx, |workspace, _| workspace.client().clone())
3154 .log_err();
3155
3156 if let Some(client) = client {
3157 cx.spawn(|this, mut cx| async move {
3158 client.authenticate_and_connect(true, &mut cx).await?;
3159 this.update(&mut cx, |_, cx| cx.notify())
3160 })
3161 .detach_and_log_err(cx)
3162 }
3163 })),
3164 )
3165 .into_any_element(),
3166 )
3167 } else if let Some(configuration_error) = configuration_error(cx) {
3168 let label = match configuration_error {
3169 ConfigurationError::NoProvider => "No LLM provider selected.",
3170 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
3171 };
3172 Some(
3173 h_flex()
3174 .p_3()
3175 .border_b_1()
3176 .border_color(cx.theme().colors().border_variant)
3177 .bg(cx.theme().colors().editor_background)
3178 .justify_between()
3179 .child(
3180 h_flex()
3181 .gap_3()
3182 .child(
3183 Icon::new(IconName::ExclamationTriangle)
3184 .size(IconSize::Small)
3185 .color(Color::Warning),
3186 )
3187 .child(Label::new(label)),
3188 )
3189 .child(
3190 Button::new("open-configuration", "Open configuration")
3191 .size(ButtonSize::Compact)
3192 .icon_size(IconSize::Small)
3193 .style(ButtonStyle::Filled)
3194 .on_click({
3195 let focus_handle = self.focus_handle(cx).clone();
3196 move |_event, cx| {
3197 focus_handle.dispatch_action(&ShowConfiguration, cx);
3198 }
3199 }),
3200 )
3201 .into_any_element(),
3202 )
3203 } else {
3204 None
3205 }
3206 }
3207
3208 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3209 let focus_handle = self.focus_handle(cx).clone();
3210 let button_text = match self.active_workflow_step() {
3211 Some(step) => match step.status(cx) {
3212 WorkflowStepStatus::Resolving => "Resolving Step...",
3213 WorkflowStepStatus::Error(_) => "Retry Step Resolution",
3214 WorkflowStepStatus::Idle => "Transform",
3215 WorkflowStepStatus::Pending => "Transforming...",
3216 WorkflowStepStatus::Done => "Accept Transformation",
3217 WorkflowStepStatus::Confirmed => "Send",
3218 },
3219 None => "Send",
3220 };
3221
3222 let (style, tooltip) = match token_state(&self.context, cx) {
3223 Some(TokenState::NoTokensLeft { .. }) => (
3224 ButtonStyle::Tinted(TintColor::Negative),
3225 Some(Tooltip::text("Token limit reached", cx)),
3226 ),
3227 Some(TokenState::HasMoreTokens {
3228 over_warn_threshold,
3229 ..
3230 }) => {
3231 let (style, tooltip) = if over_warn_threshold {
3232 (
3233 ButtonStyle::Tinted(TintColor::Warning),
3234 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3235 )
3236 } else {
3237 (ButtonStyle::Filled, None)
3238 };
3239 (style, tooltip)
3240 }
3241 None => (ButtonStyle::Filled, None),
3242 };
3243
3244 ButtonLike::new("send_button")
3245 .style(style)
3246 .when_some(tooltip, |button, tooltip| {
3247 button.tooltip(move |_| tooltip.clone())
3248 })
3249 .layer(ElevationIndex::ModalSurface)
3250 .children(
3251 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3252 .map(|binding| binding.into_any_element()),
3253 )
3254 .child(Label::new(button_text))
3255 .on_click(move |_event, cx| {
3256 focus_handle.dispatch_action(&Assist, cx);
3257 })
3258 }
3259
3260 fn active_workflow_step_for_cursor(&self, cx: &AppContext) -> Option<ActiveWorkflowStep> {
3261 let newest_cursor = self.editor.read(cx).selections.newest::<usize>(cx).head();
3262 let context = self.context.read(cx);
3263 let buffer = context.buffer().read(cx);
3264
3265 let workflow_steps = context.workflow_steps();
3266 workflow_steps
3267 .binary_search_by(|step| {
3268 let step_range = step.tagged_range.to_offset(&buffer);
3269 if newest_cursor < step_range.start {
3270 Ordering::Greater
3271 } else if newest_cursor > step_range.end {
3272 Ordering::Less
3273 } else {
3274 Ordering::Equal
3275 }
3276 })
3277 .ok()
3278 .and_then(|index| {
3279 let range = workflow_steps[index].tagged_range.clone();
3280 Some(ActiveWorkflowStep {
3281 resolved: self.workflow_steps.get(&range)?.resolved_step.is_some(),
3282 range,
3283 })
3284 })
3285 }
3286}
3287
3288impl EventEmitter<EditorEvent> for ContextEditor {}
3289impl EventEmitter<SearchEvent> for ContextEditor {}
3290
3291impl Render for ContextEditor {
3292 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3293 v_flex()
3294 .key_context("ContextEditor")
3295 .capture_action(cx.listener(ContextEditor::cancel))
3296 .capture_action(cx.listener(ContextEditor::save))
3297 .capture_action(cx.listener(ContextEditor::copy))
3298 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3299 .capture_action(cx.listener(ContextEditor::confirm_command))
3300 .on_action(cx.listener(ContextEditor::assist))
3301 .on_action(cx.listener(ContextEditor::split))
3302 .on_action(cx.listener(ContextEditor::debug_workflow_steps))
3303 .size_full()
3304 .children(self.render_notice(cx))
3305 .child(
3306 div()
3307 .flex_grow()
3308 .bg(cx.theme().colors().editor_background)
3309 .child(self.editor.clone()),
3310 )
3311 .child(
3312 h_flex().flex_none().relative().child(
3313 h_flex()
3314 .w_full()
3315 .absolute()
3316 .right_4()
3317 .bottom_2()
3318 .justify_end()
3319 .child(self.render_send_button(cx)),
3320 ),
3321 )
3322 }
3323}
3324
3325struct ErrorPopover {
3326 error: SharedString,
3327 focus_handle: FocusHandle,
3328}
3329
3330impl EventEmitter<DismissEvent> for ErrorPopover {}
3331
3332impl FocusableView for ErrorPopover {
3333 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
3334 self.focus_handle.clone()
3335 }
3336}
3337
3338impl Render for ErrorPopover {
3339 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3340 v_flex()
3341 .mt_2()
3342 .max_w_96()
3343 .py_2()
3344 .px_3()
3345 .gap_0p5()
3346 .elevation_2(cx)
3347 .bg(cx.theme().colors().surface_background)
3348 .occlude()
3349 .child(Label::new("Error interacting with language model").weight(FontWeight::SEMIBOLD))
3350 .child(Label::new(self.error.clone()))
3351 .child(
3352 h_flex().justify_end().mt_1().child(
3353 Button::new("dismiss", "Dismiss")
3354 .on_click(cx.listener(|_, _, cx| cx.emit(DismissEvent))),
3355 ),
3356 )
3357 }
3358}
3359
3360impl FocusableView for ContextEditor {
3361 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3362 self.editor.focus_handle(cx)
3363 }
3364}
3365
3366impl Item for ContextEditor {
3367 type Event = editor::EditorEvent;
3368
3369 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
3370 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
3371 }
3372
3373 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
3374 match event {
3375 EditorEvent::Edited { .. } => {
3376 f(item::ItemEvent::Edit);
3377 }
3378 EditorEvent::TitleChanged => {
3379 f(item::ItemEvent::UpdateTab);
3380 }
3381 _ => {}
3382 }
3383 }
3384
3385 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
3386 Some(self.title(cx).to_string().into())
3387 }
3388
3389 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
3390 Some(Box::new(handle.clone()))
3391 }
3392
3393 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
3394 self.editor.update(cx, |editor, cx| {
3395 Item::set_nav_history(editor, nav_history, cx)
3396 })
3397 }
3398
3399 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
3400 self.editor
3401 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
3402 }
3403
3404 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3405 self.editor
3406 .update(cx, |editor, cx| Item::deactivated(editor, cx))
3407 }
3408}
3409
3410impl SearchableItem for ContextEditor {
3411 type Match = <Editor as SearchableItem>::Match;
3412
3413 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
3414 self.editor.update(cx, |editor, cx| {
3415 editor.clear_matches(cx);
3416 });
3417 }
3418
3419 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3420 self.editor
3421 .update(cx, |editor, cx| editor.update_matches(matches, cx));
3422 }
3423
3424 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
3425 self.editor
3426 .update(cx, |editor, cx| editor.query_suggestion(cx))
3427 }
3428
3429 fn activate_match(
3430 &mut self,
3431 index: usize,
3432 matches: &[Self::Match],
3433 cx: &mut ViewContext<Self>,
3434 ) {
3435 self.editor.update(cx, |editor, cx| {
3436 editor.activate_match(index, matches, cx);
3437 });
3438 }
3439
3440 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3441 self.editor
3442 .update(cx, |editor, cx| editor.select_matches(matches, cx));
3443 }
3444
3445 fn replace(
3446 &mut self,
3447 identifier: &Self::Match,
3448 query: &project::search::SearchQuery,
3449 cx: &mut ViewContext<Self>,
3450 ) {
3451 self.editor
3452 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
3453 }
3454
3455 fn find_matches(
3456 &mut self,
3457 query: Arc<project::search::SearchQuery>,
3458 cx: &mut ViewContext<Self>,
3459 ) -> Task<Vec<Self::Match>> {
3460 self.editor
3461 .update(cx, |editor, cx| editor.find_matches(query, cx))
3462 }
3463
3464 fn active_match_index(
3465 &mut self,
3466 matches: &[Self::Match],
3467 cx: &mut ViewContext<Self>,
3468 ) -> Option<usize> {
3469 self.editor
3470 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
3471 }
3472}
3473
3474impl FollowableItem for ContextEditor {
3475 fn remote_id(&self) -> Option<workspace::ViewId> {
3476 self.remote_id
3477 }
3478
3479 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
3480 let context = self.context.read(cx);
3481 Some(proto::view::Variant::ContextEditor(
3482 proto::view::ContextEditor {
3483 context_id: context.id().to_proto(),
3484 editor: if let Some(proto::view::Variant::Editor(proto)) =
3485 self.editor.read(cx).to_state_proto(cx)
3486 {
3487 Some(proto)
3488 } else {
3489 None
3490 },
3491 },
3492 ))
3493 }
3494
3495 fn from_state_proto(
3496 workspace: View<Workspace>,
3497 id: workspace::ViewId,
3498 state: &mut Option<proto::view::Variant>,
3499 cx: &mut WindowContext,
3500 ) -> Option<Task<Result<View<Self>>>> {
3501 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
3502 return None;
3503 };
3504 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
3505 unreachable!()
3506 };
3507
3508 let context_id = ContextId::from_proto(state.context_id);
3509 let editor_state = state.editor?;
3510
3511 let (project, panel) = workspace.update(cx, |workspace, cx| {
3512 Some((
3513 workspace.project().clone(),
3514 workspace.panel::<AssistantPanel>(cx)?,
3515 ))
3516 })?;
3517
3518 let context_editor =
3519 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
3520
3521 Some(cx.spawn(|mut cx| async move {
3522 let context_editor = context_editor.await?;
3523 context_editor
3524 .update(&mut cx, |context_editor, cx| {
3525 context_editor.remote_id = Some(id);
3526 context_editor.editor.update(cx, |editor, cx| {
3527 editor.apply_update_proto(
3528 &project,
3529 proto::update_view::Variant::Editor(proto::update_view::Editor {
3530 selections: editor_state.selections,
3531 pending_selection: editor_state.pending_selection,
3532 scroll_top_anchor: editor_state.scroll_top_anchor,
3533 scroll_x: editor_state.scroll_y,
3534 scroll_y: editor_state.scroll_y,
3535 ..Default::default()
3536 }),
3537 cx,
3538 )
3539 })
3540 })?
3541 .await?;
3542 Ok(context_editor)
3543 }))
3544 }
3545
3546 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
3547 Editor::to_follow_event(event)
3548 }
3549
3550 fn add_event_to_update_proto(
3551 &self,
3552 event: &Self::Event,
3553 update: &mut Option<proto::update_view::Variant>,
3554 cx: &WindowContext,
3555 ) -> bool {
3556 self.editor
3557 .read(cx)
3558 .add_event_to_update_proto(event, update, cx)
3559 }
3560
3561 fn apply_update_proto(
3562 &mut self,
3563 project: &Model<Project>,
3564 message: proto::update_view::Variant,
3565 cx: &mut ViewContext<Self>,
3566 ) -> Task<Result<()>> {
3567 self.editor.update(cx, |editor, cx| {
3568 editor.apply_update_proto(project, message, cx)
3569 })
3570 }
3571
3572 fn is_project_item(&self, _cx: &WindowContext) -> bool {
3573 true
3574 }
3575
3576 fn set_leader_peer_id(
3577 &mut self,
3578 leader_peer_id: Option<proto::PeerId>,
3579 cx: &mut ViewContext<Self>,
3580 ) {
3581 self.editor.update(cx, |editor, cx| {
3582 editor.set_leader_peer_id(leader_peer_id, cx)
3583 })
3584 }
3585
3586 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
3587 if existing.context.read(cx).id() == self.context.read(cx).id() {
3588 Some(item::Dedup::KeepExisting)
3589 } else {
3590 None
3591 }
3592 }
3593}
3594
3595pub struct ContextEditorToolbarItem {
3596 fs: Arc<dyn Fs>,
3597 workspace: WeakView<Workspace>,
3598 active_context_editor: Option<WeakView<ContextEditor>>,
3599 model_summary_editor: View<Editor>,
3600}
3601
3602impl ContextEditorToolbarItem {
3603 pub fn new(
3604 workspace: &Workspace,
3605 _model_selector_menu_handle: PopoverMenuHandle<ContextMenu>,
3606 model_summary_editor: View<Editor>,
3607 ) -> Self {
3608 Self {
3609 fs: workspace.app_state().fs.clone(),
3610 workspace: workspace.weak_handle(),
3611 active_context_editor: None,
3612 model_summary_editor,
3613 }
3614 }
3615
3616 fn render_inject_context_menu(&self, cx: &mut ViewContext<Self>) -> impl Element {
3617 let commands = SlashCommandRegistry::global(cx);
3618 let active_editor_focus_handle = self.workspace.upgrade().and_then(|workspace| {
3619 Some(
3620 workspace
3621 .read(cx)
3622 .active_item_as::<Editor>(cx)?
3623 .focus_handle(cx),
3624 )
3625 });
3626 let active_context_editor = self.active_context_editor.clone();
3627
3628 PopoverMenu::new("inject-context-menu")
3629 .trigger(IconButton::new("trigger", IconName::Quote).tooltip(|cx| {
3630 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
3631 }))
3632 .menu(move |cx| {
3633 let active_context_editor = active_context_editor.clone()?;
3634 ContextMenu::build(cx, |mut menu, _cx| {
3635 for command_name in commands.featured_command_names() {
3636 if let Some(command) = commands.command(&command_name) {
3637 let menu_text = SharedString::from(Arc::from(command.menu_text()));
3638 menu = menu.custom_entry(
3639 {
3640 let command_name = command_name.clone();
3641 move |_cx| {
3642 h_flex()
3643 .gap_4()
3644 .w_full()
3645 .justify_between()
3646 .child(Label::new(menu_text.clone()))
3647 .child(
3648 Label::new(format!("/{command_name}"))
3649 .color(Color::Muted),
3650 )
3651 .into_any()
3652 }
3653 },
3654 {
3655 let active_context_editor = active_context_editor.clone();
3656 move |cx| {
3657 active_context_editor
3658 .update(cx, |context_editor, cx| {
3659 context_editor.insert_command(&command_name, cx)
3660 })
3661 .ok();
3662 }
3663 },
3664 )
3665 }
3666 }
3667
3668 if let Some(active_editor_focus_handle) = active_editor_focus_handle.clone() {
3669 menu = menu
3670 .context(active_editor_focus_handle)
3671 .action("Quote Selection", Box::new(QuoteSelection));
3672 }
3673
3674 menu
3675 })
3676 .into()
3677 })
3678 }
3679
3680 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
3681 let context = &self
3682 .active_context_editor
3683 .as_ref()?
3684 .upgrade()?
3685 .read(cx)
3686 .context;
3687 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
3688 TokenState::NoTokensLeft {
3689 max_token_count,
3690 token_count,
3691 } => (Color::Error, token_count, max_token_count),
3692 TokenState::HasMoreTokens {
3693 max_token_count,
3694 token_count,
3695 over_warn_threshold,
3696 } => {
3697 let color = if over_warn_threshold {
3698 Color::Warning
3699 } else {
3700 Color::Muted
3701 };
3702 (color, token_count, max_token_count)
3703 }
3704 };
3705 Some(
3706 h_flex()
3707 .gap_0p5()
3708 .child(
3709 Label::new(humanize_token_count(token_count))
3710 .size(LabelSize::Small)
3711 .color(token_count_color),
3712 )
3713 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
3714 .child(
3715 Label::new(humanize_token_count(max_token_count))
3716 .size(LabelSize::Small)
3717 .color(Color::Muted),
3718 ),
3719 )
3720 }
3721}
3722
3723impl Render for ContextEditorToolbarItem {
3724 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3725 let left_side = h_flex()
3726 .gap_2()
3727 .flex_1()
3728 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
3729 .when(self.active_context_editor.is_some(), |left_side| {
3730 left_side
3731 .child(
3732 IconButton::new("regenerate-context", IconName::ArrowCircle)
3733 .visible_on_hover("toolbar")
3734 .tooltip(|cx| Tooltip::text("Regenerate Summary", cx))
3735 .on_click(cx.listener(move |_, _, cx| {
3736 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
3737 })),
3738 )
3739 .child(self.model_summary_editor.clone())
3740 });
3741
3742 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
3743 let active_model = LanguageModelRegistry::read_global(cx).active_model();
3744
3745 let right_side = h_flex()
3746 .gap_2()
3747 .child(ModelSelector::new(
3748 self.fs.clone(),
3749 ButtonLike::new("active-model")
3750 .style(ButtonStyle::Subtle)
3751 .child(
3752 h_flex()
3753 .w_full()
3754 .gap_0p5()
3755 .child(
3756 div()
3757 .overflow_x_hidden()
3758 .flex_grow()
3759 .whitespace_nowrap()
3760 .child(match (active_provider, active_model) {
3761 (Some(provider), Some(model)) => h_flex()
3762 .gap_1()
3763 .child(
3764 Icon::new(provider.icon())
3765 .color(Color::Muted)
3766 .size(IconSize::XSmall),
3767 )
3768 .child(
3769 Label::new(model.name().0)
3770 .size(LabelSize::Small)
3771 .color(Color::Muted),
3772 )
3773 .into_any_element(),
3774 _ => Label::new("No model selected")
3775 .size(LabelSize::Small)
3776 .color(Color::Muted)
3777 .into_any_element(),
3778 }),
3779 )
3780 .child(
3781 Icon::new(IconName::ChevronDown)
3782 .color(Color::Muted)
3783 .size(IconSize::XSmall),
3784 ),
3785 )
3786 .tooltip(move |cx| {
3787 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
3788 }),
3789 ))
3790 .children(self.render_remaining_tokens(cx))
3791 .child(self.render_inject_context_menu(cx));
3792
3793 h_flex()
3794 .size_full()
3795 .justify_between()
3796 .child(left_side)
3797 .child(right_side)
3798 }
3799}
3800
3801impl ToolbarItemView for ContextEditorToolbarItem {
3802 fn set_active_pane_item(
3803 &mut self,
3804 active_pane_item: Option<&dyn ItemHandle>,
3805 cx: &mut ViewContext<Self>,
3806 ) -> ToolbarItemLocation {
3807 self.active_context_editor = active_pane_item
3808 .and_then(|item| item.act_as::<ContextEditor>(cx))
3809 .map(|editor| editor.downgrade());
3810 cx.notify();
3811 if self.active_context_editor.is_none() {
3812 ToolbarItemLocation::Hidden
3813 } else {
3814 ToolbarItemLocation::PrimaryRight
3815 }
3816 }
3817
3818 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
3819 cx.notify();
3820 }
3821}
3822
3823impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
3824
3825enum ContextEditorToolbarItemEvent {
3826 RegenerateSummary,
3827}
3828impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
3829
3830pub struct ContextHistory {
3831 picker: View<Picker<SavedContextPickerDelegate>>,
3832 _subscriptions: Vec<Subscription>,
3833 assistant_panel: WeakView<AssistantPanel>,
3834}
3835
3836impl ContextHistory {
3837 fn new(
3838 project: Model<Project>,
3839 context_store: Model<ContextStore>,
3840 assistant_panel: WeakView<AssistantPanel>,
3841 cx: &mut ViewContext<Self>,
3842 ) -> Self {
3843 let picker = cx.new_view(|cx| {
3844 Picker::uniform_list(
3845 SavedContextPickerDelegate::new(project, context_store.clone()),
3846 cx,
3847 )
3848 .modal(false)
3849 .max_height(None)
3850 });
3851
3852 let _subscriptions = vec![
3853 cx.observe(&context_store, |this, _, cx| {
3854 this.picker.update(cx, |picker, cx| picker.refresh(cx));
3855 }),
3856 cx.subscribe(&picker, Self::handle_picker_event),
3857 ];
3858
3859 Self {
3860 picker,
3861 _subscriptions,
3862 assistant_panel,
3863 }
3864 }
3865
3866 fn handle_picker_event(
3867 &mut self,
3868 _: View<Picker<SavedContextPickerDelegate>>,
3869 event: &SavedContextPickerEvent,
3870 cx: &mut ViewContext<Self>,
3871 ) {
3872 let SavedContextPickerEvent::Confirmed(context) = event;
3873 self.assistant_panel
3874 .update(cx, |assistant_panel, cx| match context {
3875 ContextMetadata::Remote(metadata) => {
3876 assistant_panel
3877 .open_remote_context(metadata.id.clone(), cx)
3878 .detach_and_log_err(cx);
3879 }
3880 ContextMetadata::Saved(metadata) => {
3881 assistant_panel
3882 .open_saved_context(metadata.path.clone(), cx)
3883 .detach_and_log_err(cx);
3884 }
3885 })
3886 .ok();
3887 }
3888}
3889
3890impl Render for ContextHistory {
3891 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
3892 div().size_full().child(self.picker.clone())
3893 }
3894}
3895
3896impl FocusableView for ContextHistory {
3897 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3898 self.picker.focus_handle(cx)
3899 }
3900}
3901
3902impl EventEmitter<()> for ContextHistory {}
3903
3904impl Item for ContextHistory {
3905 type Event = ();
3906
3907 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
3908 Some("History".into())
3909 }
3910}
3911
3912pub struct ConfigurationView {
3913 focus_handle: FocusHandle,
3914 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
3915 _registry_subscription: Subscription,
3916}
3917
3918impl ConfigurationView {
3919 fn new(cx: &mut ViewContext<Self>) -> Self {
3920 let focus_handle = cx.focus_handle();
3921
3922 let registry_subscription = cx.subscribe(
3923 &LanguageModelRegistry::global(cx),
3924 |this, _, event: &language_model::Event, cx| match event {
3925 language_model::Event::AddedProvider(provider_id) => {
3926 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
3927 if let Some(provider) = provider {
3928 this.add_configuration_view(&provider, cx);
3929 }
3930 }
3931 language_model::Event::RemovedProvider(provider_id) => {
3932 this.remove_configuration_view(provider_id);
3933 }
3934 _ => {}
3935 },
3936 );
3937
3938 let mut this = Self {
3939 focus_handle,
3940 configuration_views: HashMap::default(),
3941 _registry_subscription: registry_subscription,
3942 };
3943 this.build_configuration_views(cx);
3944 this
3945 }
3946
3947 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
3948 let providers = LanguageModelRegistry::read_global(cx).providers();
3949 for provider in providers {
3950 self.add_configuration_view(&provider, cx);
3951 }
3952 }
3953
3954 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
3955 self.configuration_views.remove(provider_id);
3956 }
3957
3958 fn add_configuration_view(
3959 &mut self,
3960 provider: &Arc<dyn LanguageModelProvider>,
3961 cx: &mut ViewContext<Self>,
3962 ) {
3963 let configuration_view = provider.configuration_view(cx);
3964 self.configuration_views
3965 .insert(provider.id(), configuration_view);
3966 }
3967
3968 fn render_provider_view(
3969 &mut self,
3970 provider: &Arc<dyn LanguageModelProvider>,
3971 cx: &mut ViewContext<Self>,
3972 ) -> Div {
3973 let provider_name = provider.name().0.clone();
3974 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
3975
3976 let open_new_context = cx.listener({
3977 let provider = provider.clone();
3978 move |_, _, cx| {
3979 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
3980 provider.clone(),
3981 ))
3982 }
3983 });
3984
3985 v_flex()
3986 .gap_2()
3987 .child(
3988 h_flex()
3989 .justify_between()
3990 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
3991 .when(provider.is_authenticated(cx), move |this| {
3992 this.child(
3993 h_flex().justify_end().child(
3994 Button::new("new-context", "Open new context")
3995 .icon_position(IconPosition::Start)
3996 .icon(IconName::Plus)
3997 .style(ButtonStyle::Filled)
3998 .layer(ElevationIndex::ModalSurface)
3999 .on_click(open_new_context),
4000 ),
4001 )
4002 }),
4003 )
4004 .child(
4005 div()
4006 .p(Spacing::Large.rems(cx))
4007 .bg(cx.theme().colors().surface_background)
4008 .border_1()
4009 .border_color(cx.theme().colors().border_variant)
4010 .rounded_md()
4011 .when(configuration_view.is_none(), |this| {
4012 this.child(div().child(Label::new(format!(
4013 "No configuration view for {}",
4014 provider_name
4015 ))))
4016 })
4017 .when_some(configuration_view, |this, configuration_view| {
4018 this.child(configuration_view)
4019 }),
4020 )
4021 }
4022}
4023
4024impl Render for ConfigurationView {
4025 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4026 let providers = LanguageModelRegistry::read_global(cx).providers();
4027 let provider_views = providers
4028 .into_iter()
4029 .map(|provider| self.render_provider_view(&provider, cx))
4030 .collect::<Vec<_>>();
4031
4032 let mut element = v_flex()
4033 .id("assistant-configuration-view")
4034 .track_focus(&self.focus_handle)
4035 .bg(cx.theme().colors().editor_background)
4036 .size_full()
4037 .overflow_y_scroll()
4038 .child(
4039 v_flex()
4040 .p(Spacing::XXLarge.rems(cx))
4041 .border_b_1()
4042 .border_color(cx.theme().colors().border)
4043 .gap_1()
4044 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
4045 .child(
4046 Label::new(
4047 "At least one LLM provider must be configured to use the Assistant.",
4048 )
4049 .color(Color::Muted),
4050 ),
4051 )
4052 .child(
4053 v_flex()
4054 .p(Spacing::XXLarge.rems(cx))
4055 .mt_1()
4056 .gap_6()
4057 .flex_1()
4058 .children(provider_views),
4059 )
4060 .into_any();
4061
4062 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
4063 // because we couldn't the element to take up the size of the parent.
4064 canvas(
4065 move |bounds, cx| {
4066 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
4067 element
4068 },
4069 |_, mut element, cx| {
4070 element.paint(cx);
4071 },
4072 )
4073 .flex_1()
4074 .w_full()
4075 }
4076}
4077
4078pub enum ConfigurationViewEvent {
4079 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
4080}
4081
4082impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
4083
4084impl FocusableView for ConfigurationView {
4085 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
4086 self.focus_handle.clone()
4087 }
4088}
4089
4090impl Item for ConfigurationView {
4091 type Event = ConfigurationViewEvent;
4092
4093 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4094 Some("Configuration".into())
4095 }
4096}
4097
4098type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
4099
4100fn render_slash_command_output_toggle(
4101 row: MultiBufferRow,
4102 is_folded: bool,
4103 fold: ToggleFold,
4104 _cx: &mut WindowContext,
4105) -> AnyElement {
4106 Disclosure::new(
4107 ("slash-command-output-fold-indicator", row.0 as u64),
4108 !is_folded,
4109 )
4110 .selected(is_folded)
4111 .on_click(move |_e, cx| fold(!is_folded, cx))
4112 .into_any_element()
4113}
4114
4115fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
4116 FoldPlaceholder {
4117 render: Arc::new({
4118 move |fold_id, fold_range, _cx| {
4119 let editor = editor.clone();
4120 ButtonLike::new(fold_id)
4121 .style(ButtonStyle::Filled)
4122 .layer(ElevationIndex::ElevatedSurface)
4123 .child(Icon::new(IconName::FileText))
4124 .child(Label::new(title.clone()).single_line())
4125 .on_click(move |_, cx| {
4126 editor
4127 .update(cx, |editor, cx| {
4128 let buffer_start = fold_range
4129 .start
4130 .to_point(&editor.buffer().read(cx).read(cx));
4131 let buffer_row = MultiBufferRow(buffer_start.row);
4132 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4133 })
4134 .ok();
4135 })
4136 .into_any_element()
4137 }
4138 }),
4139 constrain_width: false,
4140 merge_adjacent: false,
4141 }
4142}
4143
4144fn render_quote_selection_output_toggle(
4145 row: MultiBufferRow,
4146 is_folded: bool,
4147 fold: ToggleFold,
4148 _cx: &mut WindowContext,
4149) -> AnyElement {
4150 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
4151 .selected(is_folded)
4152 .on_click(move |_e, cx| fold(!is_folded, cx))
4153 .into_any_element()
4154}
4155
4156fn render_pending_slash_command_gutter_decoration(
4157 row: MultiBufferRow,
4158 status: &PendingSlashCommandStatus,
4159 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
4160) -> AnyElement {
4161 let mut icon = IconButton::new(
4162 ("slash-command-gutter-decoration", row.0),
4163 ui::IconName::TriangleRight,
4164 )
4165 .on_click(move |_e, cx| confirm_command(cx))
4166 .icon_size(ui::IconSize::Small)
4167 .size(ui::ButtonSize::None);
4168
4169 match status {
4170 PendingSlashCommandStatus::Idle => {
4171 icon = icon.icon_color(Color::Muted);
4172 }
4173 PendingSlashCommandStatus::Running { .. } => {
4174 icon = icon.selected(true);
4175 }
4176 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
4177 }
4178
4179 icon.into_any_element()
4180}
4181
4182fn render_docs_slash_command_trailer(
4183 row: MultiBufferRow,
4184 command: PendingSlashCommand,
4185 cx: &mut WindowContext,
4186) -> AnyElement {
4187 let Some(argument) = command.argument else {
4188 return Empty.into_any();
4189 };
4190
4191 let args = DocsSlashCommandArgs::parse(&argument);
4192
4193 let Some(store) = args
4194 .provider()
4195 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
4196 else {
4197 return Empty.into_any();
4198 };
4199
4200 let Some(package) = args.package() else {
4201 return Empty.into_any();
4202 };
4203
4204 let mut children = Vec::new();
4205
4206 if store.is_indexing(&package) {
4207 children.push(
4208 div()
4209 .id(("crates-being-indexed", row.0))
4210 .child(Icon::new(IconName::ArrowCircle).with_animation(
4211 "arrow-circle",
4212 Animation::new(Duration::from_secs(4)).repeat(),
4213 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
4214 ))
4215 .tooltip({
4216 let package = package.clone();
4217 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
4218 })
4219 .into_any_element(),
4220 );
4221 }
4222
4223 if let Some(latest_error) = store.latest_error_for_package(&package) {
4224 children.push(
4225 div()
4226 .id(("latest-error", row.0))
4227 .child(
4228 Icon::new(IconName::ExclamationTriangle)
4229 .size(IconSize::Small)
4230 .color(Color::Warning),
4231 )
4232 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
4233 .into_any_element(),
4234 )
4235 }
4236
4237 let is_indexing = store.is_indexing(&package);
4238 let latest_error = store.latest_error_for_package(&package);
4239
4240 if !is_indexing && latest_error.is_none() {
4241 return Empty.into_any();
4242 }
4243
4244 h_flex().gap_2().children(children).into_any_element()
4245}
4246
4247fn make_lsp_adapter_delegate(
4248 project: &Model<Project>,
4249 cx: &mut AppContext,
4250) -> Result<Arc<dyn LspAdapterDelegate>> {
4251 project.update(cx, |project, cx| {
4252 // TODO: Find the right worktree.
4253 let worktree = project
4254 .worktrees(cx)
4255 .next()
4256 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
4257 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
4258 })
4259}
4260
4261fn slash_command_error_block_renderer(message: String) -> RenderBlock {
4262 Box::new(move |_| {
4263 div()
4264 .pl_6()
4265 .child(
4266 Label::new(format!("error: {}", message))
4267 .single_line()
4268 .color(Color::Error),
4269 )
4270 .into_any()
4271 })
4272}
4273
4274enum TokenState {
4275 NoTokensLeft {
4276 max_token_count: usize,
4277 token_count: usize,
4278 },
4279 HasMoreTokens {
4280 max_token_count: usize,
4281 token_count: usize,
4282 over_warn_threshold: bool,
4283 },
4284}
4285
4286fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
4287 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
4288
4289 let model = LanguageModelRegistry::read_global(cx).active_model()?;
4290 let token_count = context.read(cx).token_count()?;
4291 let max_token_count = model.max_token_count();
4292
4293 let remaining_tokens = max_token_count as isize - token_count as isize;
4294 let token_state = if remaining_tokens <= 0 {
4295 TokenState::NoTokensLeft {
4296 max_token_count,
4297 token_count,
4298 }
4299 } else {
4300 let over_warn_threshold =
4301 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
4302 TokenState::HasMoreTokens {
4303 max_token_count,
4304 token_count,
4305 over_warn_threshold,
4306 }
4307 };
4308 Some(token_state)
4309}
4310
4311enum ConfigurationError {
4312 NoProvider,
4313 ProviderNotAuthenticated,
4314}
4315
4316fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
4317 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4318 let is_authenticated = provider
4319 .as_ref()
4320 .map_or(false, |provider| provider.is_authenticated(cx));
4321
4322 if provider.is_some() && is_authenticated {
4323 return None;
4324 }
4325
4326 if provider.is_none() {
4327 return Some(ConfigurationError::NoProvider);
4328 }
4329
4330 if !is_authenticated {
4331 return Some(ConfigurationError::ProviderNotAuthenticated);
4332 }
4333
4334 None
4335}