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