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