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