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 _observe_assist_status: Task<()>,
1723}
1724
1725pub struct ContextEditor {
1726 context: Model<Context>,
1727 fs: Arc<dyn Fs>,
1728 workspace: WeakView<Workspace>,
1729 project: Model<Project>,
1730 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1731 editor: View<Editor>,
1732 blocks: HashSet<CustomBlockId>,
1733 image_blocks: HashSet<CustomBlockId>,
1734 scroll_position: Option<ScrollPosition>,
1735 remote_id: Option<workspace::ViewId>,
1736 pending_slash_command_creases: HashMap<Range<language::Anchor>, CreaseId>,
1737 pending_slash_command_blocks: HashMap<Range<language::Anchor>, CustomBlockId>,
1738 _subscriptions: Vec<Subscription>,
1739 workflow_steps: HashMap<Range<language::Anchor>, WorkflowStep>,
1740 active_workflow_step: Option<ActiveWorkflowStep>,
1741 assistant_panel: WeakView<AssistantPanel>,
1742 error_message: Option<SharedString>,
1743 show_accept_terms: bool,
1744 pub(crate) slash_menu_handle:
1745 PopoverMenuHandle<Picker<slash_command_picker::SlashCommandDelegate>>,
1746}
1747
1748const DEFAULT_TAB_TITLE: &str = "New Context";
1749const MAX_TAB_TITLE_LEN: usize = 16;
1750
1751impl ContextEditor {
1752 fn for_context(
1753 context: Model<Context>,
1754 fs: Arc<dyn Fs>,
1755 workspace: WeakView<Workspace>,
1756 project: Model<Project>,
1757 lsp_adapter_delegate: Option<Arc<dyn LspAdapterDelegate>>,
1758 assistant_panel: WeakView<AssistantPanel>,
1759 cx: &mut ViewContext<Self>,
1760 ) -> Self {
1761 let completion_provider = SlashCommandCompletionProvider::new(
1762 Some(cx.view().downgrade()),
1763 Some(workspace.clone()),
1764 );
1765
1766 let editor = cx.new_view(|cx| {
1767 let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, cx);
1768 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1769 editor.set_show_line_numbers(false, cx);
1770 editor.set_show_git_diff_gutter(false, cx);
1771 editor.set_show_code_actions(false, cx);
1772 editor.set_show_runnables(false, cx);
1773 editor.set_show_wrap_guides(false, cx);
1774 editor.set_show_indent_guides(false, cx);
1775 editor.set_completion_provider(Box::new(completion_provider));
1776 editor.set_collaboration_hub(Box::new(project.clone()));
1777 editor
1778 });
1779
1780 let _subscriptions = vec![
1781 cx.observe(&context, |_, _, cx| cx.notify()),
1782 cx.subscribe(&context, Self::handle_context_event),
1783 cx.subscribe(&editor, Self::handle_editor_event),
1784 cx.subscribe(&editor, Self::handle_editor_search_event),
1785 ];
1786
1787 let sections = context.read(cx).slash_command_output_sections().to_vec();
1788 let mut this = Self {
1789 context,
1790 editor,
1791 lsp_adapter_delegate,
1792 blocks: Default::default(),
1793 image_blocks: Default::default(),
1794 scroll_position: None,
1795 remote_id: None,
1796 fs,
1797 workspace,
1798 project,
1799 pending_slash_command_creases: HashMap::default(),
1800 pending_slash_command_blocks: HashMap::default(),
1801 _subscriptions,
1802 workflow_steps: HashMap::default(),
1803 active_workflow_step: None,
1804 assistant_panel,
1805 error_message: None,
1806 show_accept_terms: false,
1807 slash_menu_handle: Default::default(),
1808 };
1809 this.update_message_headers(cx);
1810 this.update_image_blocks(cx);
1811 this.insert_slash_command_output_sections(sections, false, cx);
1812 this
1813 }
1814
1815 fn insert_default_prompt(&mut self, cx: &mut ViewContext<Self>) {
1816 let command_name = DefaultSlashCommand.name();
1817 self.editor.update(cx, |editor, cx| {
1818 editor.insert(&format!("/{command_name}"), cx)
1819 });
1820 self.split(&Split, cx);
1821 let command = self.context.update(cx, |context, cx| {
1822 let first_message_id = context.messages(cx).next().unwrap().id;
1823 context.update_metadata(first_message_id, cx, |metadata| {
1824 metadata.role = Role::User;
1825 });
1826 context.reparse_slash_commands(cx);
1827 context.pending_slash_commands()[0].clone()
1828 });
1829
1830 self.run_command(
1831 command.source_range,
1832 &command.name,
1833 &command.arguments,
1834 false,
1835 true,
1836 self.workspace.clone(),
1837 cx,
1838 );
1839 }
1840
1841 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1842 let provider = LanguageModelRegistry::read_global(cx).active_provider();
1843 if provider
1844 .as_ref()
1845 .map_or(false, |provider| provider.must_accept_terms(cx))
1846 {
1847 self.show_accept_terms = true;
1848 cx.notify();
1849 return;
1850 }
1851
1852 if !self.apply_active_workflow_step(cx) {
1853 self.error_message = None;
1854 self.send_to_model(cx);
1855 cx.notify();
1856 }
1857 }
1858
1859 fn apply_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1860 self.show_workflow_step(range.clone(), cx);
1861
1862 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1863 if let Some(assist) = workflow_step.assist.as_ref() {
1864 let assist_ids = assist.assist_ids.clone();
1865 cx.window_context().defer(|cx| {
1866 InlineAssistant::update_global(cx, |assistant, cx| {
1867 for assist_id in assist_ids {
1868 assistant.start_assist(assist_id, cx);
1869 }
1870 })
1871 });
1872 }
1873 }
1874 }
1875
1876 fn apply_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) -> bool {
1877 let Some(step) = self.active_workflow_step() else {
1878 return false;
1879 };
1880
1881 let range = step.range.clone();
1882 match step.status(cx) {
1883 WorkflowStepStatus::Resolving { .. } | WorkflowStepStatus::Pending => true,
1884 WorkflowStepStatus::Idle => {
1885 self.apply_workflow_step(range, cx);
1886 true
1887 }
1888 WorkflowStepStatus::Done => {
1889 self.confirm_workflow_step(range, cx);
1890 true
1891 }
1892 WorkflowStepStatus::Error(_) | WorkflowStepStatus::Empty => {
1893 self.resolve_workflow_step(range, cx);
1894 true
1895 }
1896 WorkflowStepStatus::Confirmed => false,
1897 }
1898 }
1899
1900 fn resolve_workflow_step(
1901 &mut self,
1902 range: Range<language::Anchor>,
1903 cx: &mut ViewContext<Self>,
1904 ) {
1905 self.context
1906 .update(cx, |context, cx| context.resolve_workflow_step(range, cx));
1907 }
1908
1909 fn stop_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1910 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1911 if let Some(assist) = workflow_step.assist.as_ref() {
1912 let assist_ids = assist.assist_ids.clone();
1913 cx.window_context().defer(|cx| {
1914 InlineAssistant::update_global(cx, |assistant, cx| {
1915 for assist_id in assist_ids {
1916 assistant.stop_assist(assist_id, cx);
1917 }
1918 })
1919 });
1920 }
1921 }
1922 }
1923
1924 fn undo_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1925 if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
1926 if let Some(assist) = workflow_step.assist.take() {
1927 cx.window_context().defer(|cx| {
1928 InlineAssistant::update_global(cx, |assistant, cx| {
1929 for assist_id in assist.assist_ids {
1930 assistant.undo_assist(assist_id, cx);
1931 }
1932 })
1933 });
1934 }
1935 }
1936 }
1937
1938 fn confirm_workflow_step(
1939 &mut self,
1940 range: Range<language::Anchor>,
1941 cx: &mut ViewContext<Self>,
1942 ) {
1943 if let Some(workflow_step) = self.workflow_steps.get(&range) {
1944 if let Some(assist) = workflow_step.assist.as_ref() {
1945 let assist_ids = assist.assist_ids.clone();
1946 cx.window_context().defer(move |cx| {
1947 InlineAssistant::update_global(cx, |assistant, cx| {
1948 for assist_id in assist_ids {
1949 assistant.finish_assist(assist_id, false, cx);
1950 }
1951 })
1952 });
1953 }
1954 }
1955 }
1956
1957 fn reject_workflow_step(&mut self, range: Range<language::Anchor>, cx: &mut ViewContext<Self>) {
1958 if let Some(workflow_step) = self.workflow_steps.get_mut(&range) {
1959 if let Some(assist) = workflow_step.assist.take() {
1960 cx.window_context().defer(move |cx| {
1961 InlineAssistant::update_global(cx, |assistant, cx| {
1962 for assist_id in assist.assist_ids {
1963 assistant.finish_assist(assist_id, true, cx);
1964 }
1965 })
1966 });
1967 }
1968 }
1969 }
1970
1971 fn send_to_model(&mut self, cx: &mut ViewContext<Self>) {
1972 if let Some(user_message) = self.context.update(cx, |context, cx| context.assist(cx)) {
1973 let new_selection = {
1974 let cursor = user_message
1975 .start
1976 .to_offset(self.context.read(cx).buffer().read(cx));
1977 cursor..cursor
1978 };
1979 self.editor.update(cx, |editor, cx| {
1980 editor.change_selections(
1981 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1982 cx,
1983 |selections| selections.select_ranges([new_selection]),
1984 );
1985 });
1986 // Avoid scrolling to the new cursor position so the assistant's output is stable.
1987 cx.defer(|this, _| this.scroll_position = None);
1988 }
1989 }
1990
1991 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
1992 self.error_message = None;
1993
1994 if self
1995 .context
1996 .update(cx, |context, cx| context.cancel_last_assist(cx))
1997 {
1998 return;
1999 }
2000
2001 if let Some(active_step) = self.active_workflow_step() {
2002 match active_step.status(cx) {
2003 WorkflowStepStatus::Pending => {
2004 self.stop_workflow_step(active_step.range.clone(), cx);
2005 return;
2006 }
2007 WorkflowStepStatus::Done => {
2008 self.reject_workflow_step(active_step.range.clone(), cx);
2009 return;
2010 }
2011 _ => {}
2012 }
2013 }
2014 cx.propagate();
2015 }
2016
2017 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2018 let cursors = self.cursors(cx);
2019 self.context.update(cx, |context, cx| {
2020 let messages = context
2021 .messages_for_offsets(cursors, cx)
2022 .into_iter()
2023 .map(|message| message.id)
2024 .collect();
2025 context.cycle_message_roles(messages, cx)
2026 });
2027 }
2028
2029 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2030 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2031 selections
2032 .into_iter()
2033 .map(|selection| selection.head())
2034 .collect()
2035 }
2036
2037 pub fn insert_command(&mut self, name: &str, cx: &mut ViewContext<Self>) {
2038 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
2039 self.editor.update(cx, |editor, cx| {
2040 editor.transact(cx, |editor, cx| {
2041 editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.try_cancel());
2042 let snapshot = editor.buffer().read(cx).snapshot(cx);
2043 let newest_cursor = editor.selections.newest::<Point>(cx).head();
2044 if newest_cursor.column > 0
2045 || snapshot
2046 .chars_at(newest_cursor)
2047 .next()
2048 .map_or(false, |ch| ch != '\n')
2049 {
2050 editor.move_to_end_of_line(
2051 &MoveToEndOfLine {
2052 stop_at_soft_wraps: false,
2053 },
2054 cx,
2055 );
2056 editor.newline(&Newline, cx);
2057 }
2058
2059 editor.insert(&format!("/{name}"), cx);
2060 if command.accepts_arguments() {
2061 editor.insert(" ", cx);
2062 editor.show_completions(&ShowCompletions::default(), cx);
2063 }
2064 });
2065 });
2066 if !command.requires_argument() {
2067 self.confirm_command(&ConfirmCommand, cx);
2068 }
2069 }
2070 }
2071
2072 pub fn confirm_command(&mut self, _: &ConfirmCommand, cx: &mut ViewContext<Self>) {
2073 if self.editor.read(cx).has_active_completions_menu() {
2074 return;
2075 }
2076
2077 let selections = self.editor.read(cx).selections.disjoint_anchors();
2078 let mut commands_by_range = HashMap::default();
2079 let workspace = self.workspace.clone();
2080 self.context.update(cx, |context, cx| {
2081 context.reparse_slash_commands(cx);
2082 for selection in selections.iter() {
2083 if let Some(command) =
2084 context.pending_command_for_position(selection.head().text_anchor, cx)
2085 {
2086 commands_by_range
2087 .entry(command.source_range.clone())
2088 .or_insert_with(|| command.clone());
2089 }
2090 }
2091 });
2092
2093 if commands_by_range.is_empty() {
2094 cx.propagate();
2095 } else {
2096 for command in commands_by_range.into_values() {
2097 self.run_command(
2098 command.source_range,
2099 &command.name,
2100 &command.arguments,
2101 true,
2102 false,
2103 workspace.clone(),
2104 cx,
2105 );
2106 }
2107 cx.stop_propagation();
2108 }
2109 }
2110
2111 #[allow(clippy::too_many_arguments)]
2112 pub fn run_command(
2113 &mut self,
2114 command_range: Range<language::Anchor>,
2115 name: &str,
2116 arguments: &[String],
2117 ensure_trailing_newline: bool,
2118 expand_result: bool,
2119 workspace: WeakView<Workspace>,
2120 cx: &mut ViewContext<Self>,
2121 ) {
2122 if let Some(command) = SlashCommandRegistry::global(cx).command(name) {
2123 let output = command.run(arguments, workspace, self.lsp_adapter_delegate.clone(), cx);
2124 self.context.update(cx, |context, cx| {
2125 context.insert_command_output(
2126 command_range,
2127 output,
2128 ensure_trailing_newline,
2129 expand_result,
2130 cx,
2131 )
2132 });
2133 }
2134 }
2135
2136 fn handle_context_event(
2137 &mut self,
2138 _: Model<Context>,
2139 event: &ContextEvent,
2140 cx: &mut ViewContext<Self>,
2141 ) {
2142 let context_editor = cx.view().downgrade();
2143
2144 match event {
2145 ContextEvent::MessagesEdited => {
2146 self.update_message_headers(cx);
2147 self.update_image_blocks(cx);
2148 self.context.update(cx, |context, cx| {
2149 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2150 });
2151 }
2152 ContextEvent::WorkflowStepsRemoved(removed) => {
2153 self.remove_workflow_steps(removed, cx);
2154 cx.notify();
2155 }
2156 ContextEvent::WorkflowStepUpdated(updated) => {
2157 self.update_workflow_step(updated.clone(), cx);
2158 cx.notify();
2159 }
2160 ContextEvent::SummaryChanged => {
2161 cx.emit(EditorEvent::TitleChanged);
2162 self.context.update(cx, |context, cx| {
2163 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2164 });
2165 }
2166 ContextEvent::StreamedCompletion => {
2167 self.editor.update(cx, |editor, cx| {
2168 if let Some(scroll_position) = self.scroll_position {
2169 let snapshot = editor.snapshot(cx);
2170 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2171 let scroll_top =
2172 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
2173 editor.set_scroll_position(
2174 point(scroll_position.offset_before_cursor.x, scroll_top),
2175 cx,
2176 );
2177 }
2178 });
2179 }
2180 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2181 self.editor.update(cx, |editor, cx| {
2182 let buffer = editor.buffer().read(cx).snapshot(cx);
2183 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2184 let excerpt_id = *excerpt_id;
2185
2186 editor.remove_creases(
2187 removed
2188 .iter()
2189 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2190 cx,
2191 );
2192
2193 editor.remove_blocks(
2194 HashSet::from_iter(
2195 removed.iter().filter_map(|range| {
2196 self.pending_slash_command_blocks.remove(range)
2197 }),
2198 ),
2199 None,
2200 cx,
2201 );
2202
2203 let crease_ids = editor.insert_creases(
2204 updated.iter().map(|command| {
2205 let workspace = self.workspace.clone();
2206 let confirm_command = Arc::new({
2207 let context_editor = context_editor.clone();
2208 let command = command.clone();
2209 move |cx: &mut WindowContext| {
2210 context_editor
2211 .update(cx, |context_editor, cx| {
2212 context_editor.run_command(
2213 command.source_range.clone(),
2214 &command.name,
2215 &command.arguments,
2216 false,
2217 false,
2218 workspace.clone(),
2219 cx,
2220 );
2221 })
2222 .ok();
2223 }
2224 });
2225 let placeholder = FoldPlaceholder {
2226 render: Arc::new(move |_, _, _| Empty.into_any()),
2227 constrain_width: false,
2228 merge_adjacent: false,
2229 };
2230 let render_toggle = {
2231 let confirm_command = confirm_command.clone();
2232 let command = command.clone();
2233 move |row, _, _, _cx: &mut WindowContext| {
2234 render_pending_slash_command_gutter_decoration(
2235 row,
2236 &command.status,
2237 confirm_command.clone(),
2238 )
2239 }
2240 };
2241 let render_trailer = {
2242 let command = command.clone();
2243 move |row, _unfold, cx: &mut WindowContext| {
2244 // TODO: In the future we should investigate how we can expose
2245 // this as a hook on the `SlashCommand` trait so that we don't
2246 // need to special-case it here.
2247 if command.name == DocsSlashCommand::NAME {
2248 return render_docs_slash_command_trailer(
2249 row,
2250 command.clone(),
2251 cx,
2252 );
2253 }
2254
2255 Empty.into_any()
2256 }
2257 };
2258
2259 let start = buffer
2260 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2261 .unwrap();
2262 let end = buffer
2263 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2264 .unwrap();
2265 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2266 }),
2267 cx,
2268 );
2269
2270 let block_ids = editor.insert_blocks(
2271 updated
2272 .iter()
2273 .filter_map(|command| match &command.status {
2274 PendingSlashCommandStatus::Error(error) => {
2275 Some((command, error.clone()))
2276 }
2277 _ => None,
2278 })
2279 .map(|(command, error_message)| BlockProperties {
2280 style: BlockStyle::Fixed,
2281 position: Anchor {
2282 buffer_id: Some(buffer_id),
2283 excerpt_id,
2284 text_anchor: command.source_range.start,
2285 },
2286 height: 1,
2287 disposition: BlockDisposition::Below,
2288 render: slash_command_error_block_renderer(error_message),
2289 priority: 0,
2290 }),
2291 None,
2292 cx,
2293 );
2294
2295 self.pending_slash_command_creases.extend(
2296 updated
2297 .iter()
2298 .map(|command| command.source_range.clone())
2299 .zip(crease_ids),
2300 );
2301
2302 self.pending_slash_command_blocks.extend(
2303 updated
2304 .iter()
2305 .map(|command| command.source_range.clone())
2306 .zip(block_ids),
2307 );
2308 })
2309 }
2310 ContextEvent::SlashCommandFinished {
2311 output_range,
2312 sections,
2313 run_commands_in_output,
2314 expand_result,
2315 } => {
2316 self.insert_slash_command_output_sections(
2317 sections.iter().cloned(),
2318 *expand_result,
2319 cx,
2320 );
2321
2322 if *run_commands_in_output {
2323 let commands = self.context.update(cx, |context, cx| {
2324 context.reparse_slash_commands(cx);
2325 context
2326 .pending_commands_for_range(output_range.clone(), cx)
2327 .to_vec()
2328 });
2329
2330 for command in commands {
2331 self.run_command(
2332 command.source_range,
2333 &command.name,
2334 &command.arguments,
2335 false,
2336 false,
2337 self.workspace.clone(),
2338 cx,
2339 );
2340 }
2341 }
2342 }
2343 ContextEvent::Operation(_) => {}
2344 ContextEvent::ShowAssistError(error_message) => {
2345 self.error_message = Some(error_message.clone());
2346 }
2347 }
2348 }
2349
2350 fn insert_slash_command_output_sections(
2351 &mut self,
2352 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2353 expand_result: bool,
2354 cx: &mut ViewContext<Self>,
2355 ) {
2356 self.editor.update(cx, |editor, cx| {
2357 let buffer = editor.buffer().read(cx).snapshot(cx);
2358 let excerpt_id = *buffer.as_singleton().unwrap().0;
2359 let mut buffer_rows_to_fold = BTreeSet::new();
2360 let mut creases = Vec::new();
2361 for section in sections {
2362 let start = buffer
2363 .anchor_in_excerpt(excerpt_id, section.range.start)
2364 .unwrap();
2365 let end = buffer
2366 .anchor_in_excerpt(excerpt_id, section.range.end)
2367 .unwrap();
2368 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2369 buffer_rows_to_fold.insert(buffer_row);
2370 creases.push(Crease::new(
2371 start..end,
2372 FoldPlaceholder {
2373 render: Arc::new({
2374 let editor = cx.view().downgrade();
2375 let icon = section.icon;
2376 let label = section.label.clone();
2377 move |fold_id, fold_range, _cx| {
2378 let editor = editor.clone();
2379 ButtonLike::new(fold_id)
2380 .style(ButtonStyle::Filled)
2381 .layer(ElevationIndex::ElevatedSurface)
2382 .child(Icon::new(icon))
2383 .child(Label::new(label.clone()).single_line())
2384 .on_click(move |_, cx| {
2385 editor
2386 .update(cx, |editor, cx| {
2387 let buffer_start = fold_range
2388 .start
2389 .to_point(&editor.buffer().read(cx).read(cx));
2390 let buffer_row = MultiBufferRow(buffer_start.row);
2391 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
2392 })
2393 .ok();
2394 })
2395 .into_any_element()
2396 }
2397 }),
2398 constrain_width: false,
2399 merge_adjacent: false,
2400 },
2401 render_slash_command_output_toggle,
2402 |_, _, _| Empty.into_any_element(),
2403 ));
2404 }
2405
2406 editor.insert_creases(creases, cx);
2407
2408 if expand_result {
2409 buffer_rows_to_fold.clear();
2410 }
2411 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2412 editor.fold_at(&FoldAt { buffer_row }, cx);
2413 }
2414 });
2415 }
2416
2417 fn handle_editor_event(
2418 &mut self,
2419 _: View<Editor>,
2420 event: &EditorEvent,
2421 cx: &mut ViewContext<Self>,
2422 ) {
2423 match event {
2424 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2425 let cursor_scroll_position = self.cursor_scroll_position(cx);
2426 if *autoscroll {
2427 self.scroll_position = cursor_scroll_position;
2428 } else if self.scroll_position != cursor_scroll_position {
2429 self.scroll_position = None;
2430 }
2431 }
2432 EditorEvent::SelectionsChanged { .. } => {
2433 self.scroll_position = self.cursor_scroll_position(cx);
2434 self.update_active_workflow_step(cx);
2435 }
2436 _ => {}
2437 }
2438 cx.emit(event.clone());
2439 }
2440
2441 fn active_workflow_step(&self) -> Option<&WorkflowStep> {
2442 let step = self.active_workflow_step.as_ref()?;
2443 self.workflow_steps.get(&step.range)
2444 }
2445
2446 fn remove_workflow_steps(
2447 &mut self,
2448 removed_steps: &[Range<language::Anchor>],
2449 cx: &mut ViewContext<Self>,
2450 ) {
2451 let mut blocks_to_remove = HashSet::default();
2452 for step_range in removed_steps {
2453 self.hide_workflow_step(step_range.clone(), cx);
2454 if let Some(step) = self.workflow_steps.remove(step_range) {
2455 blocks_to_remove.insert(step.header_block_id);
2456 blocks_to_remove.insert(step.footer_block_id);
2457 }
2458 }
2459 self.editor.update(cx, |editor, cx| {
2460 editor.remove_blocks(blocks_to_remove, None, cx)
2461 });
2462 self.update_active_workflow_step(cx);
2463 }
2464
2465 fn update_workflow_step(
2466 &mut self,
2467 step_range: Range<language::Anchor>,
2468 cx: &mut ViewContext<Self>,
2469 ) {
2470 let buffer_snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2471 let (&excerpt_id, _, _) = buffer_snapshot.as_singleton().unwrap();
2472
2473 let Some(step) = self
2474 .context
2475 .read(cx)
2476 .workflow_step_for_range(step_range.clone(), cx)
2477 else {
2478 return;
2479 };
2480
2481 let resolved_step = step.read(cx).resolution.clone();
2482 if let Some(existing_step) = self.workflow_steps.get_mut(&step_range) {
2483 existing_step.resolved_step = resolved_step;
2484 } else {
2485 let start = buffer_snapshot
2486 .anchor_in_excerpt(excerpt_id, step_range.start)
2487 .unwrap();
2488 let end = buffer_snapshot
2489 .anchor_in_excerpt(excerpt_id, step_range.end)
2490 .unwrap();
2491 let weak_self = cx.view().downgrade();
2492 let block_ids = self.editor.update(cx, |editor, cx| {
2493 let step_range = step_range.clone();
2494 let editor_focus_handle = editor.focus_handle(cx);
2495 editor.insert_blocks(
2496 vec![
2497 BlockProperties {
2498 position: start,
2499 height: 1,
2500 style: BlockStyle::Sticky,
2501 render: Box::new({
2502 let weak_self = weak_self.clone();
2503 let step_range = step_range.clone();
2504 move |cx| {
2505 let current_status = weak_self
2506 .update(&mut **cx, |context_editor, cx| {
2507 let step =
2508 context_editor.workflow_steps.get(&step_range)?;
2509 Some(step.status(cx))
2510 })
2511 .ok()
2512 .flatten();
2513
2514 let theme = cx.theme().status();
2515 let border_color = if current_status
2516 .as_ref()
2517 .map_or(false, |status| status.is_confirmed())
2518 {
2519 theme.ignored_border
2520 } else {
2521 theme.info_border
2522 };
2523 let step_index = weak_self
2524 .update(&mut **cx, |this, cx| {
2525 let snapshot = this
2526 .editor
2527 .read(cx)
2528 .buffer()
2529 .read(cx)
2530 .as_singleton()?
2531 .read(cx)
2532 .text_snapshot();
2533 let start_offset =
2534 step_range.start.to_offset(&snapshot);
2535 let parent_message = this
2536 .context
2537 .read(cx)
2538 .messages_for_offsets([start_offset], cx);
2539 debug_assert_eq!(parent_message.len(), 1);
2540 let parent_message = parent_message.first()?;
2541
2542 let index_of_current_step = this
2543 .workflow_steps
2544 .keys()
2545 .filter(|workflow_step_range| {
2546 workflow_step_range
2547 .start
2548 .cmp(&parent_message.anchor, &snapshot)
2549 .is_ge()
2550 && workflow_step_range
2551 .end
2552 .cmp(&step_range.end, &snapshot)
2553 .is_le()
2554 })
2555 .count();
2556 Some(index_of_current_step)
2557 })
2558 .ok()
2559 .flatten();
2560
2561 let step_label = if let Some(index) = step_index {
2562 Label::new(format!("Step {index}")).size(LabelSize::Small)
2563 } else {
2564 Label::new("Step").size(LabelSize::Small)
2565 };
2566
2567 let step_label = if current_status
2568 .as_ref()
2569 .is_some_and(|status| status.is_confirmed())
2570 {
2571 h_flex()
2572 .items_center()
2573 .gap_2()
2574 .child(
2575 step_label.strikethrough(true).color(Color::Muted),
2576 )
2577 .child(
2578 Icon::new(IconName::Check)
2579 .size(IconSize::Small)
2580 .color(Color::Created),
2581 )
2582 } else {
2583 div().child(step_label)
2584 };
2585
2586 let step_label_element = step_label.into_any_element();
2587
2588 let step_label = h_flex()
2589 .id("step")
2590 .group("step-label")
2591 .items_center()
2592 .gap_1()
2593 .child(step_label_element)
2594 .child(
2595 IconButton::new("edit-step", IconName::SearchCode)
2596 .size(ButtonSize::Compact)
2597 .icon_size(IconSize::Small)
2598 .shape(IconButtonShape::Square)
2599 .visible_on_hover("step-label")
2600 .tooltip(|cx| Tooltip::text("Open Step View", cx))
2601 .on_click({
2602 let this = weak_self.clone();
2603 let step_range = step_range.clone();
2604 move |_, cx| {
2605 this.update(cx, |this, cx| {
2606 this.open_workflow_step(
2607 step_range.clone(),
2608 cx,
2609 );
2610 })
2611 .ok();
2612 }
2613 }),
2614 );
2615
2616 div()
2617 .w_full()
2618 .px(cx.gutter_dimensions.full_width())
2619 .child(
2620 h_flex()
2621 .w_full()
2622 .h_8()
2623 .border_b_1()
2624 .border_color(border_color)
2625 .pb_2()
2626 .items_center()
2627 .justify_between()
2628 .gap_2()
2629 .child(
2630 h_flex()
2631 .justify_start()
2632 .gap_2()
2633 .child(step_label),
2634 )
2635 .children(current_status.as_ref().map(|status| {
2636 h_flex().w_full().justify_end().child(
2637 status.into_element(
2638 step_range.clone(),
2639 editor_focus_handle.clone(),
2640 weak_self.clone(),
2641 cx,
2642 ),
2643 )
2644 })),
2645 )
2646 .into_any()
2647 }
2648 }),
2649 disposition: BlockDisposition::Above,
2650 priority: 0,
2651 },
2652 BlockProperties {
2653 position: end,
2654 height: 0,
2655 style: BlockStyle::Sticky,
2656 render: Box::new(move |cx| {
2657 let current_status = weak_self
2658 .update(&mut **cx, |context_editor, cx| {
2659 let step =
2660 context_editor.workflow_steps.get(&step_range)?;
2661 Some(step.status(cx))
2662 })
2663 .ok()
2664 .flatten();
2665 let theme = cx.theme().status();
2666 let border_color = if current_status
2667 .as_ref()
2668 .map_or(false, |status| status.is_confirmed())
2669 {
2670 theme.ignored_border
2671 } else {
2672 theme.info_border
2673 };
2674
2675 div()
2676 .w_full()
2677 .px(cx.gutter_dimensions.full_width())
2678 .child(h_flex().h(px(1.)).bg(border_color))
2679 .into_any()
2680 }),
2681 disposition: BlockDisposition::Below,
2682 priority: 0,
2683 },
2684 ],
2685 None,
2686 cx,
2687 )
2688 });
2689 self.workflow_steps.insert(
2690 step_range.clone(),
2691 WorkflowStep {
2692 range: step_range.clone(),
2693 header_block_id: block_ids[0],
2694 footer_block_id: block_ids[1],
2695 resolved_step,
2696 assist: None,
2697 auto_apply: false,
2698 },
2699 );
2700 }
2701
2702 self.update_active_workflow_step(cx);
2703 if let Some(step) = self.workflow_steps.get_mut(&step_range) {
2704 if step.auto_apply && matches!(step.status(cx), WorkflowStepStatus::Idle) {
2705 self.apply_workflow_step(step_range, cx);
2706 }
2707 }
2708 }
2709
2710 fn open_workflow_step(
2711 &mut self,
2712 step_range: Range<language::Anchor>,
2713 cx: &mut ViewContext<Self>,
2714 ) -> Option<()> {
2715 let pane = self
2716 .assistant_panel
2717 .update(cx, |panel, _| panel.pane())
2718 .ok()??;
2719 let context = self.context.read(cx);
2720 let language_registry = context.language_registry();
2721 let step = context.workflow_step_for_range(step_range, cx)?;
2722 let context = self.context.clone();
2723 cx.deref_mut().defer(move |cx| {
2724 pane.update(cx, |pane, cx| {
2725 let existing_item = pane
2726 .items_of_type::<WorkflowStepView>()
2727 .find(|item| *item.read(cx).step() == step.downgrade());
2728 if let Some(item) = existing_item {
2729 if let Some(index) = pane.index_for_item(&item) {
2730 pane.activate_item(index, true, true, cx);
2731 }
2732 } else {
2733 let view = cx
2734 .new_view(|cx| WorkflowStepView::new(context, step, language_registry, cx));
2735 pane.add_item(Box::new(view), true, true, None, cx);
2736 }
2737 });
2738 });
2739 None
2740 }
2741
2742 fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
2743 let new_step = self.active_workflow_step_for_cursor(cx);
2744 if new_step.as_ref() != self.active_workflow_step.as_ref() {
2745 let mut old_editor = None;
2746 let mut old_editor_was_open = None;
2747 if let Some(old_step) = self.active_workflow_step.take() {
2748 (old_editor, old_editor_was_open) =
2749 self.hide_workflow_step(old_step.range, cx).unzip();
2750 }
2751
2752 let mut new_editor = None;
2753 if let Some(new_step) = new_step {
2754 new_editor = self.show_workflow_step(new_step.range.clone(), cx);
2755 self.active_workflow_step = Some(new_step);
2756 }
2757
2758 if new_editor != old_editor {
2759 if let Some((old_editor, old_editor_was_open)) = old_editor.zip(old_editor_was_open)
2760 {
2761 self.close_workflow_editor(cx, old_editor, old_editor_was_open)
2762 }
2763 }
2764 }
2765 }
2766
2767 fn hide_workflow_step(
2768 &mut self,
2769 step_range: Range<language::Anchor>,
2770 cx: &mut ViewContext<Self>,
2771 ) -> Option<(View<Editor>, bool)> {
2772 let Some(step) = self.workflow_steps.get_mut(&step_range) else {
2773 return None;
2774 };
2775 let Some(assist) = step.assist.as_ref() else {
2776 return None;
2777 };
2778 let Some(editor) = assist.editor.upgrade() else {
2779 return None;
2780 };
2781
2782 if matches!(step.status(cx), WorkflowStepStatus::Idle) {
2783 let assist = step.assist.take().unwrap();
2784 InlineAssistant::update_global(cx, |assistant, cx| {
2785 for assist_id in assist.assist_ids {
2786 assistant.finish_assist(assist_id, true, cx)
2787 }
2788 });
2789 return Some((editor, assist.editor_was_open));
2790 }
2791
2792 return None;
2793 }
2794
2795 fn close_workflow_editor(
2796 &mut self,
2797 cx: &mut ViewContext<ContextEditor>,
2798 editor: View<Editor>,
2799 editor_was_open: bool,
2800 ) {
2801 self.workspace
2802 .update(cx, |workspace, cx| {
2803 if let Some(pane) = workspace.pane_for(&editor) {
2804 pane.update(cx, |pane, cx| {
2805 let item_id = editor.entity_id();
2806 if !editor_was_open && pane.is_active_preview_item(item_id) {
2807 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
2808 .detach_and_log_err(cx);
2809 }
2810 });
2811 }
2812 })
2813 .ok();
2814 }
2815
2816 fn show_workflow_step(
2817 &mut self,
2818 step_range: Range<language::Anchor>,
2819 cx: &mut ViewContext<Self>,
2820 ) -> Option<View<Editor>> {
2821 let Some(step) = self.workflow_steps.get_mut(&step_range) else {
2822 return None;
2823 };
2824 let mut editor_to_return = None;
2825 let mut scroll_to_assist_id = None;
2826 match step.status(cx) {
2827 WorkflowStepStatus::Idle => {
2828 if let Some(assist) = step.assist.as_ref() {
2829 scroll_to_assist_id = assist.assist_ids.first().copied();
2830 } else if let Some(Ok(resolved)) = step.resolved_step.as_ref() {
2831 step.assist = Self::open_assists_for_step(
2832 resolved,
2833 &self.project,
2834 &self.assistant_panel,
2835 &self.workspace,
2836 cx,
2837 );
2838 editor_to_return = step
2839 .assist
2840 .as_ref()
2841 .and_then(|assist| assist.editor.upgrade());
2842 }
2843 }
2844 WorkflowStepStatus::Pending => {
2845 if let Some(assist) = step.assist.as_ref() {
2846 let assistant = InlineAssistant::global(cx);
2847 scroll_to_assist_id = assist
2848 .assist_ids
2849 .iter()
2850 .copied()
2851 .find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
2852 }
2853 }
2854 WorkflowStepStatus::Done => {
2855 if let Some(assist) = step.assist.as_ref() {
2856 scroll_to_assist_id = assist.assist_ids.first().copied();
2857 }
2858 }
2859 _ => {}
2860 }
2861
2862 if let Some(assist_id) = scroll_to_assist_id {
2863 if let Some(assist_editor) = step
2864 .assist
2865 .as_ref()
2866 .and_then(|assists| assists.editor.upgrade())
2867 {
2868 editor_to_return = Some(assist_editor.clone());
2869 self.workspace
2870 .update(cx, |workspace, cx| {
2871 workspace.activate_item(&assist_editor, false, false, cx);
2872 })
2873 .ok();
2874 InlineAssistant::update_global(cx, |assistant, cx| {
2875 assistant.scroll_to_assist(assist_id, cx)
2876 });
2877 }
2878 }
2879
2880 return editor_to_return;
2881 }
2882
2883 fn open_assists_for_step(
2884 resolved_step: &WorkflowStepResolution,
2885 project: &Model<Project>,
2886 assistant_panel: &WeakView<AssistantPanel>,
2887 workspace: &WeakView<Workspace>,
2888 cx: &mut ViewContext<Self>,
2889 ) -> Option<WorkflowAssist> {
2890 let assistant_panel = assistant_panel.upgrade()?;
2891 if resolved_step.suggestion_groups.is_empty() {
2892 return None;
2893 }
2894
2895 let editor;
2896 let mut editor_was_open = false;
2897 let mut suggestion_groups = Vec::new();
2898 if resolved_step.suggestion_groups.len() == 1
2899 && resolved_step
2900 .suggestion_groups
2901 .values()
2902 .next()
2903 .unwrap()
2904 .len()
2905 == 1
2906 {
2907 // If there's only one buffer and one suggestion group, open it directly
2908 let (buffer, groups) = resolved_step.suggestion_groups.iter().next().unwrap();
2909 let group = groups.into_iter().next().unwrap();
2910 editor = workspace
2911 .update(cx, |workspace, cx| {
2912 let active_pane = workspace.active_pane().clone();
2913 editor_was_open =
2914 workspace.is_project_item_open::<Editor>(&active_pane, buffer, cx);
2915 workspace.open_project_item::<Editor>(
2916 active_pane,
2917 buffer.clone(),
2918 false,
2919 false,
2920 cx,
2921 )
2922 })
2923 .log_err()?;
2924 let (&excerpt_id, _, _) = editor
2925 .read(cx)
2926 .buffer()
2927 .read(cx)
2928 .read(cx)
2929 .as_singleton()
2930 .unwrap();
2931
2932 // Scroll the editor to the suggested assist
2933 editor.update(cx, |editor, cx| {
2934 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2935 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2936 let anchor = if group.context_range.start.to_offset(buffer) == 0 {
2937 Anchor::min()
2938 } else {
2939 multibuffer
2940 .anchor_in_excerpt(excerpt_id, group.context_range.start)
2941 .unwrap()
2942 };
2943
2944 editor.set_scroll_anchor(
2945 ScrollAnchor {
2946 offset: gpui::Point::default(),
2947 anchor,
2948 },
2949 cx,
2950 );
2951 });
2952
2953 suggestion_groups.push((excerpt_id, group));
2954 } else {
2955 // If there are multiple buffers or suggestion groups, create a multibuffer
2956 let multibuffer = cx.new_model(|cx| {
2957 let replica_id = project.read(cx).replica_id();
2958 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2959 .with_title(resolved_step.title.clone());
2960 for (buffer, groups) in &resolved_step.suggestion_groups {
2961 let excerpt_ids = multibuffer.push_excerpts(
2962 buffer.clone(),
2963 groups.iter().map(|suggestion_group| ExcerptRange {
2964 context: suggestion_group.context_range.clone(),
2965 primary: None,
2966 }),
2967 cx,
2968 );
2969 suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
2970 }
2971 multibuffer
2972 });
2973
2974 editor = cx.new_view(|cx| {
2975 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
2976 });
2977 workspace
2978 .update(cx, |workspace, cx| {
2979 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2980 })
2981 .log_err()?;
2982 }
2983
2984 let mut assist_ids = Vec::new();
2985 for (excerpt_id, suggestion_group) in suggestion_groups {
2986 for suggestion in &suggestion_group.suggestions {
2987 assist_ids.extend(suggestion.show(
2988 &editor,
2989 excerpt_id,
2990 workspace,
2991 &assistant_panel,
2992 cx,
2993 ));
2994 }
2995 }
2996
2997 let mut observations = Vec::new();
2998 InlineAssistant::update_global(cx, |assistant, _cx| {
2999 for assist_id in &assist_ids {
3000 observations.push(assistant.observe_assist(*assist_id));
3001 }
3002 });
3003
3004 Some(WorkflowAssist {
3005 assist_ids,
3006 editor: editor.downgrade(),
3007 editor_was_open,
3008 _observe_assist_status: cx.spawn(|this, mut cx| async move {
3009 while !observations.is_empty() {
3010 let (result, ix, _) = futures::future::select_all(
3011 observations
3012 .iter_mut()
3013 .map(|observation| Box::pin(observation.changed())),
3014 )
3015 .await;
3016
3017 if result.is_err() {
3018 observations.remove(ix);
3019 }
3020
3021 if this.update(&mut cx, |_, cx| cx.notify()).is_err() {
3022 break;
3023 }
3024 }
3025 }),
3026 })
3027 }
3028
3029 fn handle_editor_search_event(
3030 &mut self,
3031 _: View<Editor>,
3032 event: &SearchEvent,
3033 cx: &mut ViewContext<Self>,
3034 ) {
3035 cx.emit(event.clone());
3036 }
3037
3038 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
3039 self.editor.update(cx, |editor, cx| {
3040 let snapshot = editor.snapshot(cx);
3041 let cursor = editor.selections.newest_anchor().head();
3042 let cursor_row = cursor
3043 .to_display_point(&snapshot.display_snapshot)
3044 .row()
3045 .as_f32();
3046 let scroll_position = editor
3047 .scroll_manager
3048 .anchor()
3049 .scroll_position(&snapshot.display_snapshot);
3050
3051 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
3052 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
3053 Some(ScrollPosition {
3054 cursor,
3055 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
3056 })
3057 } else {
3058 None
3059 }
3060 })
3061 }
3062
3063 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
3064 self.editor.update(cx, |editor, cx| {
3065 let buffer = editor.buffer().read(cx).snapshot(cx);
3066 let excerpt_id = *buffer.as_singleton().unwrap().0;
3067 let old_blocks = std::mem::take(&mut self.blocks);
3068 let new_blocks = self
3069 .context
3070 .read(cx)
3071 .messages(cx)
3072 .map(|message| BlockProperties {
3073 position: buffer
3074 .anchor_in_excerpt(excerpt_id, message.anchor)
3075 .unwrap(),
3076 height: 2,
3077 style: BlockStyle::Sticky,
3078 render: Box::new({
3079 let context = self.context.clone();
3080 move |cx| {
3081 let message_id = message.id;
3082 let show_spinner = message.role == Role::Assistant
3083 && message.status == MessageStatus::Pending;
3084
3085 let label = match message.role {
3086 Role::User => {
3087 Label::new("You").color(Color::Default).into_any_element()
3088 }
3089 Role::Assistant => {
3090 let label = Label::new("Assistant").color(Color::Info);
3091 if show_spinner {
3092 label
3093 .with_animation(
3094 "pulsating-label",
3095 Animation::new(Duration::from_secs(2))
3096 .repeat()
3097 .with_easing(pulsating_between(0.4, 0.8)),
3098 |label, delta| label.alpha(delta),
3099 )
3100 .into_any_element()
3101 } else {
3102 label.into_any_element()
3103 }
3104 }
3105
3106 Role::System => Label::new("System")
3107 .color(Color::Warning)
3108 .into_any_element(),
3109 };
3110
3111 let sender = ButtonLike::new("role")
3112 .style(ButtonStyle::Filled)
3113 .child(label)
3114 .tooltip(|cx| {
3115 Tooltip::with_meta(
3116 "Toggle message role",
3117 None,
3118 "Available roles: You (User), Assistant, System",
3119 cx,
3120 )
3121 })
3122 .on_click({
3123 let context = context.clone();
3124 move |_, cx| {
3125 context.update(cx, |context, cx| {
3126 context.cycle_message_roles(
3127 HashSet::from_iter(Some(message_id)),
3128 cx,
3129 )
3130 })
3131 }
3132 });
3133
3134 h_flex()
3135 .id(("message_header", message_id.as_u64()))
3136 .pl(cx.gutter_dimensions.full_width())
3137 .h_11()
3138 .w_full()
3139 .relative()
3140 .gap_1()
3141 .child(sender)
3142 .children(match &message.status {
3143 MessageStatus::Error(error) => Some(
3144 Button::new("show-error", "Error")
3145 .color(Color::Error)
3146 .selected_label_color(Color::Error)
3147 .selected_icon_color(Color::Error)
3148 .icon(IconName::XCircle)
3149 .icon_color(Color::Error)
3150 .icon_size(IconSize::Small)
3151 .icon_position(IconPosition::Start)
3152 .tooltip(move |cx| {
3153 Tooltip::with_meta(
3154 "Error interacting with language model",
3155 None,
3156 "Click for more details",
3157 cx,
3158 )
3159 })
3160 .on_click({
3161 let context = context.clone();
3162 let error = error.clone();
3163 move |_, cx| {
3164 context.update(cx, |_, cx| {
3165 cx.emit(ContextEvent::ShowAssistError(
3166 error.clone(),
3167 ));
3168 });
3169 }
3170 })
3171 .into_any_element(),
3172 ),
3173 MessageStatus::Canceled => Some(
3174 ButtonLike::new("canceled")
3175 .child(
3176 Icon::new(IconName::XCircle).color(Color::Disabled),
3177 )
3178 .child(
3179 Label::new("Canceled")
3180 .size(LabelSize::Small)
3181 .color(Color::Disabled),
3182 )
3183 .tooltip(move |cx| {
3184 Tooltip::with_meta(
3185 "Canceled",
3186 None,
3187 "Interaction with the assistant was canceled",
3188 cx,
3189 )
3190 })
3191 .into_any_element(),
3192 ),
3193 _ => None,
3194 })
3195 .into_any_element()
3196 }
3197 }),
3198 disposition: BlockDisposition::Above,
3199 priority: usize::MAX,
3200 })
3201 .collect::<Vec<_>>();
3202
3203 editor.remove_blocks(old_blocks, None, cx);
3204 let ids = editor.insert_blocks(new_blocks, None, cx);
3205 self.blocks = HashSet::from_iter(ids);
3206 });
3207 }
3208
3209 fn insert_selection(
3210 workspace: &mut Workspace,
3211 _: &InsertIntoEditor,
3212 cx: &mut ViewContext<Workspace>,
3213 ) {
3214 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3215 return;
3216 };
3217 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
3218 return;
3219 };
3220 let Some(active_editor_view) = workspace
3221 .active_item(cx)
3222 .and_then(|item| item.act_as::<Editor>(cx))
3223 else {
3224 return;
3225 };
3226
3227 let context_editor = context_editor_view.read(cx).editor.read(cx);
3228 let anchor = context_editor.selections.newest_anchor();
3229 let text = context_editor
3230 .buffer()
3231 .read(cx)
3232 .read(cx)
3233 .text_for_range(anchor.range())
3234 .collect::<String>();
3235
3236 // If nothing is selected, don't delete the current selection; instead, be a no-op.
3237 if !text.is_empty() {
3238 active_editor_view.update(cx, |editor, cx| {
3239 editor.insert(&text, cx);
3240 editor.focus(cx);
3241 })
3242 }
3243 }
3244
3245 fn quote_selection(
3246 workspace: &mut Workspace,
3247 _: &QuoteSelection,
3248 cx: &mut ViewContext<Workspace>,
3249 ) {
3250 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3251 return;
3252 };
3253 let Some(editor) = workspace
3254 .active_item(cx)
3255 .and_then(|item| item.act_as::<Editor>(cx))
3256 else {
3257 return;
3258 };
3259
3260 let selection = editor.update(cx, |editor, cx| editor.selections.newest_adjusted(cx));
3261 let editor = editor.read(cx);
3262 let buffer = editor.buffer().read(cx).snapshot(cx);
3263 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
3264 ..editor::ToOffset::to_offset(&selection.end, &buffer);
3265 let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
3266 if selected_text.is_empty() {
3267 return;
3268 }
3269
3270 let start_language = buffer.language_at(range.start);
3271 let end_language = buffer.language_at(range.end);
3272 let language_name = if start_language == end_language {
3273 start_language.map(|language| language.code_fence_block_name())
3274 } else {
3275 None
3276 };
3277 let language_name = language_name.as_deref().unwrap_or("");
3278
3279 let filename = buffer
3280 .file_at(selection.start)
3281 .map(|file| file.full_path(cx));
3282
3283 let text = if language_name == "markdown" {
3284 selected_text
3285 .lines()
3286 .map(|line| format!("> {}", line))
3287 .collect::<Vec<_>>()
3288 .join("\n")
3289 } else {
3290 let start_symbols = buffer
3291 .symbols_containing(selection.start, None)
3292 .map(|(_, symbols)| symbols);
3293 let end_symbols = buffer
3294 .symbols_containing(selection.end, None)
3295 .map(|(_, symbols)| symbols);
3296
3297 let outline_text =
3298 if let Some((start_symbols, end_symbols)) = start_symbols.zip(end_symbols) {
3299 Some(
3300 start_symbols
3301 .into_iter()
3302 .zip(end_symbols)
3303 .take_while(|(a, b)| a == b)
3304 .map(|(a, _)| a.text)
3305 .collect::<Vec<_>>()
3306 .join(" > "),
3307 )
3308 } else {
3309 None
3310 };
3311
3312 let line_comment_prefix = start_language
3313 .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
3314
3315 let fence = codeblock_fence_for_path(
3316 filename.as_deref(),
3317 Some(selection.start.row..selection.end.row),
3318 );
3319
3320 if let Some((line_comment_prefix, outline_text)) = line_comment_prefix.zip(outline_text)
3321 {
3322 let breadcrumb = format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
3323 format!("{fence}{breadcrumb}{selected_text}\n```")
3324 } else {
3325 format!("{fence}{selected_text}\n```")
3326 }
3327 };
3328
3329 let crease_title = if let Some(path) = filename {
3330 let start_line = selection.start.row + 1;
3331 let end_line = selection.end.row + 1;
3332 if start_line == end_line {
3333 format!("{}, Line {}", path.display(), start_line)
3334 } else {
3335 format!("{}, Lines {} to {}", path.display(), start_line, end_line)
3336 }
3337 } else {
3338 "Quoted selection".to_string()
3339 };
3340
3341 // Activate the panel
3342 if !panel.focus_handle(cx).contains_focused(cx) {
3343 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3344 }
3345
3346 panel.update(cx, |_, cx| {
3347 // Wait to create a new context until the workspace is no longer
3348 // being updated.
3349 cx.defer(move |panel, cx| {
3350 if let Some(context) = panel
3351 .active_context_editor(cx)
3352 .or_else(|| panel.new_context(cx))
3353 {
3354 context.update(cx, |context, cx| {
3355 context.editor.update(cx, |editor, cx| {
3356 editor.insert("\n", cx);
3357
3358 let point = editor.selections.newest::<Point>(cx).head();
3359 let start_row = MultiBufferRow(point.row);
3360
3361 editor.insert(&text, cx);
3362
3363 let snapshot = editor.buffer().read(cx).snapshot(cx);
3364 let anchor_before = snapshot.anchor_after(point);
3365 let anchor_after = editor
3366 .selections
3367 .newest_anchor()
3368 .head()
3369 .bias_left(&snapshot);
3370
3371 editor.insert("\n", cx);
3372
3373 let fold_placeholder = quote_selection_fold_placeholder(
3374 crease_title,
3375 cx.view().downgrade(),
3376 );
3377 let crease = Crease::new(
3378 anchor_before..anchor_after,
3379 fold_placeholder,
3380 render_quote_selection_output_toggle,
3381 |_, _, _| Empty.into_any(),
3382 );
3383 editor.insert_creases(vec![crease], cx);
3384 editor.fold_at(
3385 &FoldAt {
3386 buffer_row: start_row,
3387 },
3388 cx,
3389 );
3390 })
3391 });
3392 };
3393 });
3394 });
3395 }
3396
3397 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3398 let editor = self.editor.read(cx);
3399 let context = self.context.read(cx);
3400 if editor.selections.count() == 1 {
3401 let selection = editor.selections.newest::<usize>(cx);
3402 let mut copied_text = String::new();
3403 let mut spanned_messages = 0;
3404 for message in context.messages(cx) {
3405 if message.offset_range.start >= selection.range().end {
3406 break;
3407 } else if message.offset_range.end >= selection.range().start {
3408 let range = cmp::max(message.offset_range.start, selection.range().start)
3409 ..cmp::min(message.offset_range.end, selection.range().end);
3410 if !range.is_empty() {
3411 spanned_messages += 1;
3412 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
3413 for chunk in context.buffer().read(cx).text_for_range(range) {
3414 copied_text.push_str(chunk);
3415 }
3416 copied_text.push('\n');
3417 }
3418 }
3419 }
3420
3421 if spanned_messages > 1 {
3422 cx.write_to_clipboard(ClipboardItem::new_string(copied_text));
3423 return;
3424 }
3425 }
3426
3427 cx.propagate();
3428 }
3429
3430 fn paste(&mut self, _: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
3431 let images = if let Some(item) = cx.read_from_clipboard() {
3432 item.into_entries()
3433 .filter_map(|entry| {
3434 if let ClipboardEntry::Image(image) = entry {
3435 Some(image)
3436 } else {
3437 None
3438 }
3439 })
3440 .collect()
3441 } else {
3442 Vec::new()
3443 };
3444
3445 if images.is_empty() {
3446 // If we didn't find any valid image data to paste, propagate to let normal pasting happen.
3447 cx.propagate();
3448 } else {
3449 let mut image_positions = Vec::new();
3450 self.editor.update(cx, |editor, cx| {
3451 editor.transact(cx, |editor, cx| {
3452 let edits = editor
3453 .selections
3454 .all::<usize>(cx)
3455 .into_iter()
3456 .map(|selection| (selection.start..selection.end, "\n"));
3457 editor.edit(edits, cx);
3458
3459 let snapshot = editor.buffer().read(cx).snapshot(cx);
3460 for selection in editor.selections.all::<usize>(cx) {
3461 image_positions.push(snapshot.anchor_before(selection.end));
3462 }
3463 });
3464 });
3465
3466 self.context.update(cx, |context, cx| {
3467 for image in images {
3468 let image_id = image.id();
3469 context.insert_image(image, cx);
3470 for image_position in image_positions.iter() {
3471 context.insert_image_anchor(image_id, image_position.text_anchor, cx);
3472 }
3473 }
3474 });
3475 }
3476 }
3477
3478 fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
3479 self.editor.update(cx, |editor, cx| {
3480 let buffer = editor.buffer().read(cx).snapshot(cx);
3481 let excerpt_id = *buffer.as_singleton().unwrap().0;
3482 let old_blocks = std::mem::take(&mut self.image_blocks);
3483 let new_blocks = self
3484 .context
3485 .read(cx)
3486 .images(cx)
3487 .filter_map(|image| {
3488 const MAX_HEIGHT_IN_LINES: u32 = 8;
3489 let anchor = buffer.anchor_in_excerpt(excerpt_id, image.anchor).unwrap();
3490 let image = image.render_image.clone();
3491 anchor.is_valid(&buffer).then(|| BlockProperties {
3492 position: anchor,
3493 height: MAX_HEIGHT_IN_LINES,
3494 style: BlockStyle::Sticky,
3495 render: Box::new(move |cx| {
3496 let image_size = size_for_image(
3497 &image,
3498 size(
3499 cx.max_width - cx.gutter_dimensions.full_width(),
3500 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
3501 ),
3502 );
3503 h_flex()
3504 .pl(cx.gutter_dimensions.full_width())
3505 .child(
3506 img(image.clone())
3507 .object_fit(gpui::ObjectFit::ScaleDown)
3508 .w(image_size.width)
3509 .h(image_size.height),
3510 )
3511 .into_any_element()
3512 }),
3513
3514 disposition: BlockDisposition::Above,
3515 priority: 0,
3516 })
3517 })
3518 .collect::<Vec<_>>();
3519
3520 editor.remove_blocks(old_blocks, None, cx);
3521 let ids = editor.insert_blocks(new_blocks, None, cx);
3522 self.image_blocks = HashSet::from_iter(ids);
3523 });
3524 }
3525
3526 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3527 self.context.update(cx, |context, cx| {
3528 let selections = self.editor.read(cx).selections.disjoint_anchors();
3529 for selection in selections.as_ref() {
3530 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3531 let range = selection
3532 .map(|endpoint| endpoint.to_offset(&buffer))
3533 .range();
3534 context.split_message(range, cx);
3535 }
3536 });
3537 }
3538
3539 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3540 self.context.update(cx, |context, cx| {
3541 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
3542 });
3543 }
3544
3545 fn title(&self, cx: &AppContext) -> Cow<str> {
3546 self.context
3547 .read(cx)
3548 .summary()
3549 .map(|summary| summary.text.clone())
3550 .map(Cow::Owned)
3551 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
3552 }
3553
3554 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3555 use feature_flags::FeatureFlagAppExt;
3556 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
3557 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
3558 });
3559
3560 if nudge.map_or(false, |value| value) {
3561 Some(
3562 h_flex()
3563 .p_3()
3564 .border_b_1()
3565 .border_color(cx.theme().colors().border_variant)
3566 .bg(cx.theme().colors().editor_background)
3567 .justify_between()
3568 .child(
3569 h_flex()
3570 .gap_3()
3571 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
3572 .child(Label::new("Zed AI is here! Get started by signing in →")),
3573 )
3574 .child(
3575 Button::new("sign-in", "Sign in")
3576 .size(ButtonSize::Compact)
3577 .style(ButtonStyle::Filled)
3578 .on_click(cx.listener(|this, _event, cx| {
3579 let client = this
3580 .workspace
3581 .update(cx, |workspace, _| workspace.client().clone())
3582 .log_err();
3583
3584 if let Some(client) = client {
3585 cx.spawn(|this, mut cx| async move {
3586 client.authenticate_and_connect(true, &mut cx).await?;
3587 this.update(&mut cx, |_, cx| cx.notify())
3588 })
3589 .detach_and_log_err(cx)
3590 }
3591 })),
3592 )
3593 .into_any_element(),
3594 )
3595 } else if let Some(configuration_error) = configuration_error(cx) {
3596 let label = match configuration_error {
3597 ConfigurationError::NoProvider => "No LLM provider selected.",
3598 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
3599 };
3600 Some(
3601 h_flex()
3602 .px_3()
3603 .py_2()
3604 .border_b_1()
3605 .border_color(cx.theme().colors().border_variant)
3606 .bg(cx.theme().colors().editor_background)
3607 .justify_between()
3608 .child(
3609 h_flex()
3610 .gap_3()
3611 .child(
3612 Icon::new(IconName::ExclamationTriangle)
3613 .size(IconSize::Small)
3614 .color(Color::Warning),
3615 )
3616 .child(Label::new(label)),
3617 )
3618 .child(
3619 Button::new("open-configuration", "Open configuration")
3620 .size(ButtonSize::Compact)
3621 .icon_size(IconSize::Small)
3622 .style(ButtonStyle::Filled)
3623 .on_click({
3624 let focus_handle = self.focus_handle(cx).clone();
3625 move |_event, cx| {
3626 focus_handle.dispatch_action(&ShowConfiguration, cx);
3627 }
3628 }),
3629 )
3630 .into_any_element(),
3631 )
3632 } else {
3633 None
3634 }
3635 }
3636
3637 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3638 let focus_handle = self.focus_handle(cx).clone();
3639 let mut should_pulsate = false;
3640 let button_text = match self.active_workflow_step() {
3641 Some(step) => match step.status(cx) {
3642 WorkflowStepStatus::Empty | WorkflowStepStatus::Error(_) => "Retry Step Resolution",
3643 WorkflowStepStatus::Resolving { auto_apply } => {
3644 should_pulsate = auto_apply;
3645 "Transform"
3646 }
3647 WorkflowStepStatus::Idle => "Transform",
3648 WorkflowStepStatus::Pending => "Applying...",
3649 WorkflowStepStatus::Done => "Accept",
3650 WorkflowStepStatus::Confirmed => "Send",
3651 },
3652 None => "Send",
3653 };
3654
3655 let (style, tooltip) = match token_state(&self.context, cx) {
3656 Some(TokenState::NoTokensLeft { .. }) => (
3657 ButtonStyle::Tinted(TintColor::Negative),
3658 Some(Tooltip::text("Token limit reached", cx)),
3659 ),
3660 Some(TokenState::HasMoreTokens {
3661 over_warn_threshold,
3662 ..
3663 }) => {
3664 let (style, tooltip) = if over_warn_threshold {
3665 (
3666 ButtonStyle::Tinted(TintColor::Warning),
3667 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
3668 )
3669 } else {
3670 (ButtonStyle::Filled, None)
3671 };
3672 (style, tooltip)
3673 }
3674 None => (ButtonStyle::Filled, None),
3675 };
3676
3677 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3678
3679 let has_configuration_error = configuration_error(cx).is_some();
3680 let needs_to_accept_terms = self.show_accept_terms
3681 && provider
3682 .as_ref()
3683 .map_or(false, |provider| provider.must_accept_terms(cx));
3684 let disabled = has_configuration_error || needs_to_accept_terms;
3685
3686 ButtonLike::new("send_button")
3687 .disabled(disabled)
3688 .style(style)
3689 .when_some(tooltip, |button, tooltip| {
3690 button.tooltip(move |_| tooltip.clone())
3691 })
3692 .layer(ElevationIndex::ModalSurface)
3693 .child(Label::new(button_text).map(|this| {
3694 if should_pulsate {
3695 this.with_animation(
3696 "resolving-suggestion-send-button-animation",
3697 Animation::new(Duration::from_secs(2))
3698 .repeat()
3699 .with_easing(pulsating_between(0.4, 0.8)),
3700 |label, delta| label.alpha(delta),
3701 )
3702 .into_any_element()
3703 } else {
3704 this.into_any_element()
3705 }
3706 }))
3707 .children(
3708 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
3709 .map(|binding| binding.into_any_element()),
3710 )
3711 .on_click(move |_event, cx| {
3712 focus_handle.dispatch_action(&Assist, cx);
3713 })
3714 }
3715
3716 fn active_workflow_step_for_cursor(&self, cx: &AppContext) -> Option<ActiveWorkflowStep> {
3717 let newest_cursor = self.editor.read(cx).selections.newest::<usize>(cx).head();
3718 let context = self.context.read(cx);
3719 let (range, step) = context.workflow_step_containing(newest_cursor, cx)?;
3720 Some(ActiveWorkflowStep {
3721 resolved: step.read(cx).resolution.is_some(),
3722 range,
3723 })
3724 }
3725}
3726
3727impl EventEmitter<EditorEvent> for ContextEditor {}
3728impl EventEmitter<SearchEvent> for ContextEditor {}
3729
3730impl Render for ContextEditor {
3731 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3732 let provider = LanguageModelRegistry::read_global(cx).active_provider();
3733 let accept_terms = if self.show_accept_terms {
3734 provider
3735 .as_ref()
3736 .and_then(|provider| provider.render_accept_terms(cx))
3737 } else {
3738 None
3739 };
3740 let focus_handle = self
3741 .workspace
3742 .update(cx, |workspace, cx| {
3743 Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
3744 })
3745 .ok()
3746 .flatten();
3747 v_flex()
3748 .key_context("ContextEditor")
3749 .capture_action(cx.listener(ContextEditor::cancel))
3750 .capture_action(cx.listener(ContextEditor::save))
3751 .capture_action(cx.listener(ContextEditor::copy))
3752 .capture_action(cx.listener(ContextEditor::paste))
3753 .capture_action(cx.listener(ContextEditor::cycle_message_role))
3754 .capture_action(cx.listener(ContextEditor::confirm_command))
3755 .on_action(cx.listener(ContextEditor::assist))
3756 .on_action(cx.listener(ContextEditor::split))
3757 .size_full()
3758 .children(self.render_notice(cx))
3759 .child(
3760 div()
3761 .flex_grow()
3762 .bg(cx.theme().colors().editor_background)
3763 .child(self.editor.clone()),
3764 )
3765 .when_some(accept_terms, |this, element| {
3766 this.child(
3767 div()
3768 .absolute()
3769 .right_3()
3770 .bottom_12()
3771 .max_w_96()
3772 .py_2()
3773 .px_3()
3774 .elevation_2(cx)
3775 .bg(cx.theme().colors().surface_background)
3776 .occlude()
3777 .child(element),
3778 )
3779 })
3780 .when_some(self.error_message.clone(), |this, error_message| {
3781 this.child(
3782 div()
3783 .absolute()
3784 .right_3()
3785 .bottom_12()
3786 .max_w_96()
3787 .py_2()
3788 .px_3()
3789 .elevation_2(cx)
3790 .occlude()
3791 .child(
3792 v_flex()
3793 .gap_0p5()
3794 .child(
3795 h_flex()
3796 .gap_1p5()
3797 .items_center()
3798 .child(Icon::new(IconName::XCircle).color(Color::Error))
3799 .child(
3800 Label::new("Error interacting with language model")
3801 .weight(FontWeight::MEDIUM),
3802 ),
3803 )
3804 .child(
3805 div()
3806 .id("error-message")
3807 .max_h_24()
3808 .overflow_y_scroll()
3809 .child(Label::new(error_message)),
3810 )
3811 .child(h_flex().justify_end().mt_1().child(
3812 Button::new("dismiss", "Dismiss").on_click(cx.listener(
3813 |this, _, cx| {
3814 this.error_message = None;
3815 cx.notify();
3816 },
3817 )),
3818 )),
3819 ),
3820 )
3821 })
3822 .child(
3823 h_flex().w_full().relative().child(
3824 h_flex()
3825 .p_2()
3826 .w_full()
3827 .border_t_1()
3828 .border_color(cx.theme().colors().border_variant)
3829 .bg(cx.theme().colors().editor_background)
3830 .child(
3831 h_flex()
3832 .gap_2()
3833 .child(render_inject_context_menu(cx.view().downgrade(), cx))
3834 .child(
3835 IconButton::new("quote-button", IconName::Quote)
3836 .icon_size(IconSize::Small)
3837 .on_click(|_, cx| {
3838 cx.dispatch_action(QuoteSelection.boxed_clone());
3839 })
3840 .tooltip(move |cx| {
3841 cx.new_view(|cx| {
3842 Tooltip::new("Insert Selection").key_binding(
3843 focus_handle.as_ref().and_then(|handle| {
3844 KeyBinding::for_action_in(
3845 &QuoteSelection,
3846 &handle,
3847 cx,
3848 )
3849 }),
3850 )
3851 })
3852 .into()
3853 }),
3854 ),
3855 )
3856 .child(
3857 h_flex()
3858 .w_full()
3859 .justify_end()
3860 .child(div().child(self.render_send_button(cx))),
3861 ),
3862 ),
3863 )
3864 }
3865}
3866
3867impl FocusableView for ContextEditor {
3868 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
3869 self.editor.focus_handle(cx)
3870 }
3871}
3872
3873impl Item for ContextEditor {
3874 type Event = editor::EditorEvent;
3875
3876 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
3877 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
3878 }
3879
3880 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
3881 match event {
3882 EditorEvent::Edited { .. } => {
3883 f(item::ItemEvent::Edit);
3884 }
3885 EditorEvent::TitleChanged => {
3886 f(item::ItemEvent::UpdateTab);
3887 }
3888 _ => {}
3889 }
3890 }
3891
3892 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
3893 Some(self.title(cx).to_string().into())
3894 }
3895
3896 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
3897 Some(Box::new(handle.clone()))
3898 }
3899
3900 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
3901 self.editor.update(cx, |editor, cx| {
3902 Item::set_nav_history(editor, nav_history, cx)
3903 })
3904 }
3905
3906 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
3907 self.editor
3908 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
3909 }
3910
3911 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
3912 self.editor
3913 .update(cx, |editor, cx| Item::deactivated(editor, cx))
3914 }
3915}
3916
3917impl SearchableItem for ContextEditor {
3918 type Match = <Editor as SearchableItem>::Match;
3919
3920 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
3921 self.editor.update(cx, |editor, cx| {
3922 editor.clear_matches(cx);
3923 });
3924 }
3925
3926 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3927 self.editor
3928 .update(cx, |editor, cx| editor.update_matches(matches, cx));
3929 }
3930
3931 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
3932 self.editor
3933 .update(cx, |editor, cx| editor.query_suggestion(cx))
3934 }
3935
3936 fn activate_match(
3937 &mut self,
3938 index: usize,
3939 matches: &[Self::Match],
3940 cx: &mut ViewContext<Self>,
3941 ) {
3942 self.editor.update(cx, |editor, cx| {
3943 editor.activate_match(index, matches, cx);
3944 });
3945 }
3946
3947 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
3948 self.editor
3949 .update(cx, |editor, cx| editor.select_matches(matches, cx));
3950 }
3951
3952 fn replace(
3953 &mut self,
3954 identifier: &Self::Match,
3955 query: &project::search::SearchQuery,
3956 cx: &mut ViewContext<Self>,
3957 ) {
3958 self.editor
3959 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
3960 }
3961
3962 fn find_matches(
3963 &mut self,
3964 query: Arc<project::search::SearchQuery>,
3965 cx: &mut ViewContext<Self>,
3966 ) -> Task<Vec<Self::Match>> {
3967 self.editor
3968 .update(cx, |editor, cx| editor.find_matches(query, cx))
3969 }
3970
3971 fn active_match_index(
3972 &mut self,
3973 matches: &[Self::Match],
3974 cx: &mut ViewContext<Self>,
3975 ) -> Option<usize> {
3976 self.editor
3977 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
3978 }
3979}
3980
3981impl FollowableItem for ContextEditor {
3982 fn remote_id(&self) -> Option<workspace::ViewId> {
3983 self.remote_id
3984 }
3985
3986 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
3987 let context = self.context.read(cx);
3988 Some(proto::view::Variant::ContextEditor(
3989 proto::view::ContextEditor {
3990 context_id: context.id().to_proto(),
3991 editor: if let Some(proto::view::Variant::Editor(proto)) =
3992 self.editor.read(cx).to_state_proto(cx)
3993 {
3994 Some(proto)
3995 } else {
3996 None
3997 },
3998 },
3999 ))
4000 }
4001
4002 fn from_state_proto(
4003 workspace: View<Workspace>,
4004 id: workspace::ViewId,
4005 state: &mut Option<proto::view::Variant>,
4006 cx: &mut WindowContext,
4007 ) -> Option<Task<Result<View<Self>>>> {
4008 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
4009 return None;
4010 };
4011 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
4012 unreachable!()
4013 };
4014
4015 let context_id = ContextId::from_proto(state.context_id);
4016 let editor_state = state.editor?;
4017
4018 let (project, panel) = workspace.update(cx, |workspace, cx| {
4019 Some((
4020 workspace.project().clone(),
4021 workspace.panel::<AssistantPanel>(cx)?,
4022 ))
4023 })?;
4024
4025 let context_editor =
4026 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
4027
4028 Some(cx.spawn(|mut cx| async move {
4029 let context_editor = context_editor.await?;
4030 context_editor
4031 .update(&mut cx, |context_editor, cx| {
4032 context_editor.remote_id = Some(id);
4033 context_editor.editor.update(cx, |editor, cx| {
4034 editor.apply_update_proto(
4035 &project,
4036 proto::update_view::Variant::Editor(proto::update_view::Editor {
4037 selections: editor_state.selections,
4038 pending_selection: editor_state.pending_selection,
4039 scroll_top_anchor: editor_state.scroll_top_anchor,
4040 scroll_x: editor_state.scroll_y,
4041 scroll_y: editor_state.scroll_y,
4042 ..Default::default()
4043 }),
4044 cx,
4045 )
4046 })
4047 })?
4048 .await?;
4049 Ok(context_editor)
4050 }))
4051 }
4052
4053 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
4054 Editor::to_follow_event(event)
4055 }
4056
4057 fn add_event_to_update_proto(
4058 &self,
4059 event: &Self::Event,
4060 update: &mut Option<proto::update_view::Variant>,
4061 cx: &WindowContext,
4062 ) -> bool {
4063 self.editor
4064 .read(cx)
4065 .add_event_to_update_proto(event, update, cx)
4066 }
4067
4068 fn apply_update_proto(
4069 &mut self,
4070 project: &Model<Project>,
4071 message: proto::update_view::Variant,
4072 cx: &mut ViewContext<Self>,
4073 ) -> Task<Result<()>> {
4074 self.editor.update(cx, |editor, cx| {
4075 editor.apply_update_proto(project, message, cx)
4076 })
4077 }
4078
4079 fn is_project_item(&self, _cx: &WindowContext) -> bool {
4080 true
4081 }
4082
4083 fn set_leader_peer_id(
4084 &mut self,
4085 leader_peer_id: Option<proto::PeerId>,
4086 cx: &mut ViewContext<Self>,
4087 ) {
4088 self.editor.update(cx, |editor, cx| {
4089 editor.set_leader_peer_id(leader_peer_id, cx)
4090 })
4091 }
4092
4093 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
4094 if existing.context.read(cx).id() == self.context.read(cx).id() {
4095 Some(item::Dedup::KeepExisting)
4096 } else {
4097 None
4098 }
4099 }
4100}
4101
4102pub struct ContextEditorToolbarItem {
4103 fs: Arc<dyn Fs>,
4104 workspace: WeakView<Workspace>,
4105 active_context_editor: Option<WeakView<ContextEditor>>,
4106 model_summary_editor: View<Editor>,
4107 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4108}
4109
4110fn active_editor_focus_handle(
4111 workspace: &WeakView<Workspace>,
4112 cx: &WindowContext<'_>,
4113) -> Option<FocusHandle> {
4114 workspace.upgrade().and_then(|workspace| {
4115 Some(
4116 workspace
4117 .read(cx)
4118 .active_item_as::<Editor>(cx)?
4119 .focus_handle(cx),
4120 )
4121 })
4122}
4123
4124fn render_inject_context_menu(
4125 active_context_editor: WeakView<ContextEditor>,
4126 cx: &mut WindowContext<'_>,
4127) -> impl IntoElement {
4128 let commands = SlashCommandRegistry::global(cx);
4129
4130 slash_command_picker::SlashCommandSelector::new(
4131 commands.clone(),
4132 active_context_editor,
4133 IconButton::new("trigger", IconName::SlashSquare)
4134 .icon_size(IconSize::Small)
4135 .tooltip(|cx| {
4136 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
4137 }),
4138 )
4139}
4140
4141impl ContextEditorToolbarItem {
4142 pub fn new(
4143 workspace: &Workspace,
4144 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4145 model_summary_editor: View<Editor>,
4146 ) -> Self {
4147 Self {
4148 fs: workspace.app_state().fs.clone(),
4149 workspace: workspace.weak_handle(),
4150 active_context_editor: None,
4151 model_summary_editor,
4152 model_selector_menu_handle,
4153 }
4154 }
4155
4156 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
4157 let context = &self
4158 .active_context_editor
4159 .as_ref()?
4160 .upgrade()?
4161 .read(cx)
4162 .context;
4163 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
4164 TokenState::NoTokensLeft {
4165 max_token_count,
4166 token_count,
4167 } => (Color::Error, token_count, max_token_count),
4168 TokenState::HasMoreTokens {
4169 max_token_count,
4170 token_count,
4171 over_warn_threshold,
4172 } => {
4173 let color = if over_warn_threshold {
4174 Color::Warning
4175 } else {
4176 Color::Muted
4177 };
4178 (color, token_count, max_token_count)
4179 }
4180 };
4181 Some(
4182 h_flex()
4183 .gap_0p5()
4184 .child(
4185 Label::new(humanize_token_count(token_count))
4186 .size(LabelSize::Small)
4187 .color(token_count_color),
4188 )
4189 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
4190 .child(
4191 Label::new(humanize_token_count(max_token_count))
4192 .size(LabelSize::Small)
4193 .color(Color::Muted),
4194 ),
4195 )
4196 }
4197}
4198
4199impl Render for ContextEditorToolbarItem {
4200 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4201 let left_side = h_flex()
4202 .pl_1()
4203 .gap_2()
4204 .flex_1()
4205 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
4206 .when(self.active_context_editor.is_some(), |left_side| {
4207 left_side.child(self.model_summary_editor.clone())
4208 });
4209 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
4210 let active_model = LanguageModelRegistry::read_global(cx).active_model();
4211 let weak_self = cx.view().downgrade();
4212 let right_side = h_flex()
4213 .gap_2()
4214 .child(
4215 ModelSelector::new(
4216 self.fs.clone(),
4217 ButtonLike::new("active-model")
4218 .style(ButtonStyle::Subtle)
4219 .child(
4220 h_flex()
4221 .w_full()
4222 .gap_0p5()
4223 .child(
4224 div()
4225 .overflow_x_hidden()
4226 .flex_grow()
4227 .whitespace_nowrap()
4228 .child(match (active_provider, active_model) {
4229 (Some(provider), Some(model)) => h_flex()
4230 .gap_1()
4231 .child(
4232 Icon::new(model.icon().unwrap_or_else(|| provider.icon()))
4233 .color(Color::Muted)
4234 .size(IconSize::XSmall),
4235 )
4236 .child(
4237 Label::new(model.name().0)
4238 .size(LabelSize::Small)
4239 .color(Color::Muted),
4240 )
4241 .into_any_element(),
4242 _ => Label::new("No model selected")
4243 .size(LabelSize::Small)
4244 .color(Color::Muted)
4245 .into_any_element(),
4246 }),
4247 )
4248 .child(
4249 Icon::new(IconName::ChevronDown)
4250 .color(Color::Muted)
4251 .size(IconSize::XSmall),
4252 ),
4253 )
4254 .tooltip(move |cx| {
4255 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
4256 }),
4257 )
4258 .with_handle(self.model_selector_menu_handle.clone()),
4259 )
4260 .children(self.render_remaining_tokens(cx))
4261 .child(
4262 PopoverMenu::new("context-editor-popover")
4263 .trigger(
4264 IconButton::new("context-editor-trigger", IconName::EllipsisVertical)
4265 .icon_size(IconSize::Small)
4266 .tooltip(|cx| Tooltip::text("Open Context Options", cx)),
4267 )
4268 .menu({
4269 let weak_self = weak_self.clone();
4270 move |cx| {
4271 let weak_self = weak_self.clone();
4272 Some(ContextMenu::build(cx, move |menu, cx| {
4273 let context = weak_self
4274 .update(cx, |this, cx| {
4275 active_editor_focus_handle(&this.workspace, cx)
4276 })
4277 .ok()
4278 .flatten();
4279 menu.when_some(context, |menu, context| menu.context(context))
4280 .entry("Regenerate Context Title", None, {
4281 let weak_self = weak_self.clone();
4282 move |cx| {
4283 weak_self
4284 .update(cx, |_, cx| {
4285 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
4286 })
4287 .ok();
4288 }
4289 })
4290 .custom_entry(
4291 |_| {
4292 h_flex()
4293 .w_full()
4294 .justify_between()
4295 .gap_2()
4296 .child(Label::new("Insert Context"))
4297 .child(Label::new("/ command").color(Color::Muted))
4298 .into_any()
4299 },
4300 {
4301 let weak_self = weak_self.clone();
4302 move |cx| {
4303 weak_self
4304 .update(cx, |this, cx| {
4305 if let Some(editor) =
4306 &this.active_context_editor
4307 {
4308 editor
4309 .update(cx, |this, cx| {
4310 this.slash_menu_handle
4311 .toggle(cx);
4312 })
4313 .ok();
4314 }
4315 })
4316 .ok();
4317 }
4318 },
4319 )
4320 .action("Insert Selection", QuoteSelection.boxed_clone())
4321 }))
4322 }
4323 }),
4324 );
4325
4326 h_flex()
4327 .size_full()
4328 .gap_2()
4329 .justify_between()
4330 .child(left_side)
4331 .child(right_side)
4332 }
4333}
4334
4335impl ToolbarItemView for ContextEditorToolbarItem {
4336 fn set_active_pane_item(
4337 &mut self,
4338 active_pane_item: Option<&dyn ItemHandle>,
4339 cx: &mut ViewContext<Self>,
4340 ) -> ToolbarItemLocation {
4341 self.active_context_editor = active_pane_item
4342 .and_then(|item| item.act_as::<ContextEditor>(cx))
4343 .map(|editor| editor.downgrade());
4344 cx.notify();
4345 if self.active_context_editor.is_none() {
4346 ToolbarItemLocation::Hidden
4347 } else {
4348 ToolbarItemLocation::PrimaryRight
4349 }
4350 }
4351
4352 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
4353 cx.notify();
4354 }
4355}
4356
4357impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
4358
4359enum ContextEditorToolbarItemEvent {
4360 RegenerateSummary,
4361}
4362impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
4363
4364pub struct ContextHistory {
4365 picker: View<Picker<SavedContextPickerDelegate>>,
4366 _subscriptions: Vec<Subscription>,
4367 assistant_panel: WeakView<AssistantPanel>,
4368}
4369
4370impl ContextHistory {
4371 fn new(
4372 project: Model<Project>,
4373 context_store: Model<ContextStore>,
4374 assistant_panel: WeakView<AssistantPanel>,
4375 cx: &mut ViewContext<Self>,
4376 ) -> Self {
4377 let picker = cx.new_view(|cx| {
4378 Picker::uniform_list(
4379 SavedContextPickerDelegate::new(project, context_store.clone()),
4380 cx,
4381 )
4382 .modal(false)
4383 .max_height(None)
4384 });
4385
4386 let _subscriptions = vec![
4387 cx.observe(&context_store, |this, _, cx| {
4388 this.picker.update(cx, |picker, cx| picker.refresh(cx));
4389 }),
4390 cx.subscribe(&picker, Self::handle_picker_event),
4391 ];
4392
4393 Self {
4394 picker,
4395 _subscriptions,
4396 assistant_panel,
4397 }
4398 }
4399
4400 fn handle_picker_event(
4401 &mut self,
4402 _: View<Picker<SavedContextPickerDelegate>>,
4403 event: &SavedContextPickerEvent,
4404 cx: &mut ViewContext<Self>,
4405 ) {
4406 let SavedContextPickerEvent::Confirmed(context) = event;
4407 self.assistant_panel
4408 .update(cx, |assistant_panel, cx| match context {
4409 ContextMetadata::Remote(metadata) => {
4410 assistant_panel
4411 .open_remote_context(metadata.id.clone(), cx)
4412 .detach_and_log_err(cx);
4413 }
4414 ContextMetadata::Saved(metadata) => {
4415 assistant_panel
4416 .open_saved_context(metadata.path.clone(), cx)
4417 .detach_and_log_err(cx);
4418 }
4419 })
4420 .ok();
4421 }
4422}
4423
4424impl Render for ContextHistory {
4425 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
4426 div().size_full().child(self.picker.clone())
4427 }
4428}
4429
4430impl FocusableView for ContextHistory {
4431 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4432 self.picker.focus_handle(cx)
4433 }
4434}
4435
4436impl EventEmitter<()> for ContextHistory {}
4437
4438impl Item for ContextHistory {
4439 type Event = ();
4440
4441 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4442 Some("History".into())
4443 }
4444}
4445
4446pub struct ConfigurationView {
4447 focus_handle: FocusHandle,
4448 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
4449 _registry_subscription: Subscription,
4450}
4451
4452impl ConfigurationView {
4453 fn new(cx: &mut ViewContext<Self>) -> Self {
4454 let focus_handle = cx.focus_handle();
4455
4456 let registry_subscription = cx.subscribe(
4457 &LanguageModelRegistry::global(cx),
4458 |this, _, event: &language_model::Event, cx| match event {
4459 language_model::Event::AddedProvider(provider_id) => {
4460 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
4461 if let Some(provider) = provider {
4462 this.add_configuration_view(&provider, cx);
4463 }
4464 }
4465 language_model::Event::RemovedProvider(provider_id) => {
4466 this.remove_configuration_view(provider_id);
4467 }
4468 _ => {}
4469 },
4470 );
4471
4472 let mut this = Self {
4473 focus_handle,
4474 configuration_views: HashMap::default(),
4475 _registry_subscription: registry_subscription,
4476 };
4477 this.build_configuration_views(cx);
4478 this
4479 }
4480
4481 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
4482 let providers = LanguageModelRegistry::read_global(cx).providers();
4483 for provider in providers {
4484 self.add_configuration_view(&provider, cx);
4485 }
4486 }
4487
4488 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
4489 self.configuration_views.remove(provider_id);
4490 }
4491
4492 fn add_configuration_view(
4493 &mut self,
4494 provider: &Arc<dyn LanguageModelProvider>,
4495 cx: &mut ViewContext<Self>,
4496 ) {
4497 let configuration_view = provider.configuration_view(cx);
4498 self.configuration_views
4499 .insert(provider.id(), configuration_view);
4500 }
4501
4502 fn render_provider_view(
4503 &mut self,
4504 provider: &Arc<dyn LanguageModelProvider>,
4505 cx: &mut ViewContext<Self>,
4506 ) -> Div {
4507 let provider_id = provider.id().0.clone();
4508 let provider_name = provider.name().0.clone();
4509 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
4510
4511 let open_new_context = cx.listener({
4512 let provider = provider.clone();
4513 move |_, _, cx| {
4514 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
4515 provider.clone(),
4516 ))
4517 }
4518 });
4519
4520 v_flex()
4521 .gap_2()
4522 .child(
4523 h_flex()
4524 .justify_between()
4525 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
4526 .when(provider.is_authenticated(cx), move |this| {
4527 this.child(
4528 h_flex().justify_end().child(
4529 Button::new(
4530 SharedString::from(format!("new-context-{provider_id}")),
4531 "Open new context",
4532 )
4533 .icon_position(IconPosition::Start)
4534 .icon(IconName::Plus)
4535 .style(ButtonStyle::Filled)
4536 .layer(ElevationIndex::ModalSurface)
4537 .on_click(open_new_context),
4538 ),
4539 )
4540 }),
4541 )
4542 .child(
4543 div()
4544 .p(Spacing::Large.rems(cx))
4545 .bg(cx.theme().colors().surface_background)
4546 .border_1()
4547 .border_color(cx.theme().colors().border_variant)
4548 .rounded_md()
4549 .when(configuration_view.is_none(), |this| {
4550 this.child(div().child(Label::new(format!(
4551 "No configuration view for {}",
4552 provider_name
4553 ))))
4554 })
4555 .when_some(configuration_view, |this, configuration_view| {
4556 this.child(configuration_view)
4557 }),
4558 )
4559 }
4560}
4561
4562impl Render for ConfigurationView {
4563 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4564 let providers = LanguageModelRegistry::read_global(cx).providers();
4565 let provider_views = providers
4566 .into_iter()
4567 .map(|provider| self.render_provider_view(&provider, cx))
4568 .collect::<Vec<_>>();
4569
4570 let mut element = v_flex()
4571 .id("assistant-configuration-view")
4572 .track_focus(&self.focus_handle)
4573 .bg(cx.theme().colors().editor_background)
4574 .size_full()
4575 .overflow_y_scroll()
4576 .child(
4577 v_flex()
4578 .p(Spacing::XXLarge.rems(cx))
4579 .border_b_1()
4580 .border_color(cx.theme().colors().border)
4581 .gap_1()
4582 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
4583 .child(
4584 Label::new(
4585 "At least one LLM provider must be configured to use the Assistant.",
4586 )
4587 .color(Color::Muted),
4588 ),
4589 )
4590 .child(
4591 v_flex()
4592 .p(Spacing::XXLarge.rems(cx))
4593 .mt_1()
4594 .gap_6()
4595 .flex_1()
4596 .children(provider_views),
4597 )
4598 .into_any();
4599
4600 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
4601 // because we couldn't the element to take up the size of the parent.
4602 canvas(
4603 move |bounds, cx| {
4604 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
4605 element
4606 },
4607 |_, mut element, cx| {
4608 element.paint(cx);
4609 },
4610 )
4611 .flex_1()
4612 .w_full()
4613 }
4614}
4615
4616pub enum ConfigurationViewEvent {
4617 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
4618}
4619
4620impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
4621
4622impl FocusableView for ConfigurationView {
4623 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
4624 self.focus_handle.clone()
4625 }
4626}
4627
4628impl Item for ConfigurationView {
4629 type Event = ConfigurationViewEvent;
4630
4631 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
4632 Some("Configuration".into())
4633 }
4634}
4635
4636type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
4637
4638fn render_slash_command_output_toggle(
4639 row: MultiBufferRow,
4640 is_folded: bool,
4641 fold: ToggleFold,
4642 _cx: &mut WindowContext,
4643) -> AnyElement {
4644 Disclosure::new(
4645 ("slash-command-output-fold-indicator", row.0 as u64),
4646 !is_folded,
4647 )
4648 .selected(is_folded)
4649 .on_click(move |_e, cx| fold(!is_folded, cx))
4650 .into_any_element()
4651}
4652
4653fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
4654 FoldPlaceholder {
4655 render: Arc::new({
4656 move |fold_id, fold_range, _cx| {
4657 let editor = editor.clone();
4658 ButtonLike::new(fold_id)
4659 .style(ButtonStyle::Filled)
4660 .layer(ElevationIndex::ElevatedSurface)
4661 .child(Icon::new(IconName::TextSelect))
4662 .child(Label::new(title.clone()).single_line())
4663 .on_click(move |_, cx| {
4664 editor
4665 .update(cx, |editor, cx| {
4666 let buffer_start = fold_range
4667 .start
4668 .to_point(&editor.buffer().read(cx).read(cx));
4669 let buffer_row = MultiBufferRow(buffer_start.row);
4670 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4671 })
4672 .ok();
4673 })
4674 .into_any_element()
4675 }
4676 }),
4677 constrain_width: false,
4678 merge_adjacent: false,
4679 }
4680}
4681
4682fn render_quote_selection_output_toggle(
4683 row: MultiBufferRow,
4684 is_folded: bool,
4685 fold: ToggleFold,
4686 _cx: &mut WindowContext,
4687) -> AnyElement {
4688 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
4689 .selected(is_folded)
4690 .on_click(move |_e, cx| fold(!is_folded, cx))
4691 .into_any_element()
4692}
4693
4694fn render_pending_slash_command_gutter_decoration(
4695 row: MultiBufferRow,
4696 status: &PendingSlashCommandStatus,
4697 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
4698) -> AnyElement {
4699 let mut icon = IconButton::new(
4700 ("slash-command-gutter-decoration", row.0),
4701 ui::IconName::TriangleRight,
4702 )
4703 .on_click(move |_e, cx| confirm_command(cx))
4704 .icon_size(ui::IconSize::Small)
4705 .size(ui::ButtonSize::None);
4706
4707 match status {
4708 PendingSlashCommandStatus::Idle => {
4709 icon = icon.icon_color(Color::Muted);
4710 }
4711 PendingSlashCommandStatus::Running { .. } => {
4712 icon = icon.selected(true);
4713 }
4714 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
4715 }
4716
4717 icon.into_any_element()
4718}
4719
4720fn render_docs_slash_command_trailer(
4721 row: MultiBufferRow,
4722 command: PendingSlashCommand,
4723 cx: &mut WindowContext,
4724) -> AnyElement {
4725 if command.arguments.is_empty() {
4726 return Empty.into_any();
4727 }
4728 let args = DocsSlashCommandArgs::parse(&command.arguments);
4729
4730 let Some(store) = args
4731 .provider()
4732 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
4733 else {
4734 return Empty.into_any();
4735 };
4736
4737 let Some(package) = args.package() else {
4738 return Empty.into_any();
4739 };
4740
4741 let mut children = Vec::new();
4742
4743 if store.is_indexing(&package) {
4744 children.push(
4745 div()
4746 .id(("crates-being-indexed", row.0))
4747 .child(Icon::new(IconName::ArrowCircle).with_animation(
4748 "arrow-circle",
4749 Animation::new(Duration::from_secs(4)).repeat(),
4750 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
4751 ))
4752 .tooltip({
4753 let package = package.clone();
4754 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
4755 })
4756 .into_any_element(),
4757 );
4758 }
4759
4760 if let Some(latest_error) = store.latest_error_for_package(&package) {
4761 children.push(
4762 div()
4763 .id(("latest-error", row.0))
4764 .child(
4765 Icon::new(IconName::ExclamationTriangle)
4766 .size(IconSize::Small)
4767 .color(Color::Warning),
4768 )
4769 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
4770 .into_any_element(),
4771 )
4772 }
4773
4774 let is_indexing = store.is_indexing(&package);
4775 let latest_error = store.latest_error_for_package(&package);
4776
4777 if !is_indexing && latest_error.is_none() {
4778 return Empty.into_any();
4779 }
4780
4781 h_flex().gap_2().children(children).into_any_element()
4782}
4783
4784fn make_lsp_adapter_delegate(
4785 project: &Model<Project>,
4786 cx: &mut AppContext,
4787) -> Result<Arc<dyn LspAdapterDelegate>> {
4788 project.update(cx, |project, cx| {
4789 // TODO: Find the right worktree.
4790 let worktree = project
4791 .worktrees(cx)
4792 .next()
4793 .ok_or_else(|| anyhow!("no worktrees when constructing ProjectLspAdapterDelegate"))?;
4794 Ok(ProjectLspAdapterDelegate::new(project, &worktree, cx) as Arc<dyn LspAdapterDelegate>)
4795 })
4796}
4797
4798fn slash_command_error_block_renderer(message: String) -> RenderBlock {
4799 Box::new(move |_| {
4800 div()
4801 .pl_6()
4802 .child(
4803 Label::new(format!("error: {}", message))
4804 .single_line()
4805 .color(Color::Error),
4806 )
4807 .into_any()
4808 })
4809}
4810
4811enum TokenState {
4812 NoTokensLeft {
4813 max_token_count: usize,
4814 token_count: usize,
4815 },
4816 HasMoreTokens {
4817 max_token_count: usize,
4818 token_count: usize,
4819 over_warn_threshold: bool,
4820 },
4821}
4822
4823fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
4824 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
4825
4826 let model = LanguageModelRegistry::read_global(cx).active_model()?;
4827 let token_count = context.read(cx).token_count()?;
4828 let max_token_count = model.max_token_count();
4829
4830 let remaining_tokens = max_token_count as isize - token_count as isize;
4831 let token_state = if remaining_tokens <= 0 {
4832 TokenState::NoTokensLeft {
4833 max_token_count,
4834 token_count,
4835 }
4836 } else {
4837 let over_warn_threshold =
4838 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
4839 TokenState::HasMoreTokens {
4840 max_token_count,
4841 token_count,
4842 over_warn_threshold,
4843 }
4844 };
4845 Some(token_state)
4846}
4847
4848fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
4849 let image_size = data
4850 .size(0)
4851 .map(|dimension| Pixels::from(u32::from(dimension)));
4852 let image_ratio = image_size.width / image_size.height;
4853 let bounds_ratio = max_size.width / max_size.height;
4854
4855 if image_size.width > max_size.width || image_size.height > max_size.height {
4856 if bounds_ratio > image_ratio {
4857 size(
4858 image_size.width * (max_size.height / image_size.height),
4859 max_size.height,
4860 )
4861 } else {
4862 size(
4863 max_size.width,
4864 image_size.height * (max_size.width / image_size.width),
4865 )
4866 }
4867 } else {
4868 size(image_size.width, image_size.height)
4869 }
4870}
4871
4872enum ConfigurationError {
4873 NoProvider,
4874 ProviderNotAuthenticated,
4875}
4876
4877fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
4878 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4879 let is_authenticated = provider
4880 .as_ref()
4881 .map_or(false, |provider| provider.is_authenticated(cx));
4882
4883 if provider.is_some() && is_authenticated {
4884 return None;
4885 }
4886
4887 if provider.is_none() {
4888 return Some(ConfigurationError::NoProvider);
4889 }
4890
4891 if !is_authenticated {
4892 return Some(ConfigurationError::ProviderNotAuthenticated);
4893 }
4894
4895 None
4896}