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