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