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