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