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