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