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::LocalLspAdapterDelegate;
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 context = self.context.read(cx);
1910 let sections = context
1911 .slash_command_output_sections()
1912 .into_iter()
1913 .filter(|section| section.is_valid(context.buffer().read(cx)))
1914 .cloned()
1915 .collect::<Vec<_>>();
1916 let snapshot = context.buffer().read(cx).snapshot();
1917 let output = command.run(
1918 arguments,
1919 §ions,
1920 snapshot,
1921 workspace,
1922 self.lsp_adapter_delegate.clone(),
1923 cx,
1924 );
1925 self.context.update(cx, |context, cx| {
1926 context.insert_command_output(
1927 command_range,
1928 output,
1929 ensure_trailing_newline,
1930 expand_result,
1931 cx,
1932 )
1933 });
1934 }
1935 }
1936
1937 fn handle_context_event(
1938 &mut self,
1939 _: Model<Context>,
1940 event: &ContextEvent,
1941 cx: &mut ViewContext<Self>,
1942 ) {
1943 let context_editor = cx.view().downgrade();
1944
1945 match event {
1946 ContextEvent::MessagesEdited => {
1947 self.update_message_headers(cx);
1948 self.update_image_blocks(cx);
1949 self.context.update(cx, |context, cx| {
1950 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1951 });
1952 }
1953 ContextEvent::SummaryChanged => {
1954 cx.emit(EditorEvent::TitleChanged);
1955 self.context.update(cx, |context, cx| {
1956 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1957 });
1958 }
1959 ContextEvent::StreamedCompletion => {
1960 self.editor.update(cx, |editor, cx| {
1961 if let Some(scroll_position) = self.scroll_position {
1962 let snapshot = editor.snapshot(cx);
1963 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1964 let scroll_top =
1965 cursor_point.row().as_f32() - scroll_position.offset_before_cursor.y;
1966 editor.set_scroll_position(
1967 point(scroll_position.offset_before_cursor.x, scroll_top),
1968 cx,
1969 );
1970 }
1971
1972 let new_tool_uses = self
1973 .context
1974 .read(cx)
1975 .pending_tool_uses()
1976 .into_iter()
1977 .filter(|tool_use| {
1978 !self
1979 .pending_tool_use_creases
1980 .contains_key(&tool_use.source_range)
1981 })
1982 .cloned()
1983 .collect::<Vec<_>>();
1984
1985 let buffer = editor.buffer().read(cx).snapshot(cx);
1986 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
1987 let excerpt_id = *excerpt_id;
1988
1989 let mut buffer_rows_to_fold = BTreeSet::new();
1990
1991 let creases = new_tool_uses
1992 .iter()
1993 .map(|tool_use| {
1994 let placeholder = FoldPlaceholder {
1995 render: render_fold_icon_button(
1996 cx.view().downgrade(),
1997 IconName::PocketKnife,
1998 tool_use.name.clone().into(),
1999 ),
2000 constrain_width: false,
2001 merge_adjacent: false,
2002 };
2003 let render_trailer =
2004 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2005
2006 let start = buffer
2007 .anchor_in_excerpt(excerpt_id, tool_use.source_range.start)
2008 .unwrap();
2009 let end = buffer
2010 .anchor_in_excerpt(excerpt_id, tool_use.source_range.end)
2011 .unwrap();
2012
2013 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2014 buffer_rows_to_fold.insert(buffer_row);
2015
2016 self.context.update(cx, |context, cx| {
2017 context.insert_content(
2018 Content::ToolUse {
2019 range: tool_use.source_range.clone(),
2020 tool_use: LanguageModelToolUse {
2021 id: tool_use.id.to_string(),
2022 name: tool_use.name.clone(),
2023 input: tool_use.input.clone(),
2024 },
2025 },
2026 cx,
2027 );
2028 });
2029
2030 Crease::new(
2031 start..end,
2032 placeholder,
2033 fold_toggle("tool-use"),
2034 render_trailer,
2035 )
2036 })
2037 .collect::<Vec<_>>();
2038
2039 let crease_ids = editor.insert_creases(creases, cx);
2040
2041 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2042 editor.fold_at(&FoldAt { buffer_row }, cx);
2043 }
2044
2045 self.pending_tool_use_creases.extend(
2046 new_tool_uses
2047 .iter()
2048 .map(|tool_use| tool_use.source_range.clone())
2049 .zip(crease_ids),
2050 );
2051 });
2052 }
2053 ContextEvent::WorkflowStepsUpdated { removed, updated } => {
2054 self.workflow_steps_updated(removed, updated, cx);
2055 }
2056 ContextEvent::PendingSlashCommandsUpdated { removed, updated } => {
2057 self.editor.update(cx, |editor, cx| {
2058 let buffer = editor.buffer().read(cx).snapshot(cx);
2059 let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap();
2060 let excerpt_id = *excerpt_id;
2061
2062 editor.remove_creases(
2063 removed
2064 .iter()
2065 .filter_map(|range| self.pending_slash_command_creases.remove(range)),
2066 cx,
2067 );
2068
2069 editor.remove_blocks(
2070 HashSet::from_iter(
2071 removed.iter().filter_map(|range| {
2072 self.pending_slash_command_blocks.remove(range)
2073 }),
2074 ),
2075 None,
2076 cx,
2077 );
2078
2079 let crease_ids = editor.insert_creases(
2080 updated.iter().map(|command| {
2081 let workspace = self.workspace.clone();
2082 let confirm_command = Arc::new({
2083 let context_editor = context_editor.clone();
2084 let command = command.clone();
2085 move |cx: &mut WindowContext| {
2086 context_editor
2087 .update(cx, |context_editor, cx| {
2088 context_editor.run_command(
2089 command.source_range.clone(),
2090 &command.name,
2091 &command.arguments,
2092 false,
2093 false,
2094 workspace.clone(),
2095 cx,
2096 );
2097 })
2098 .ok();
2099 }
2100 });
2101 let placeholder = FoldPlaceholder {
2102 render: Arc::new(move |_, _, _| Empty.into_any()),
2103 constrain_width: false,
2104 merge_adjacent: false,
2105 };
2106 let render_toggle = {
2107 let confirm_command = confirm_command.clone();
2108 let command = command.clone();
2109 move |row, _, _, _cx: &mut WindowContext| {
2110 render_pending_slash_command_gutter_decoration(
2111 row,
2112 &command.status,
2113 confirm_command.clone(),
2114 )
2115 }
2116 };
2117 let render_trailer = {
2118 let command = command.clone();
2119 move |row, _unfold, cx: &mut WindowContext| {
2120 // TODO: In the future we should investigate how we can expose
2121 // this as a hook on the `SlashCommand` trait so that we don't
2122 // need to special-case it here.
2123 if command.name == DocsSlashCommand::NAME {
2124 return render_docs_slash_command_trailer(
2125 row,
2126 command.clone(),
2127 cx,
2128 );
2129 }
2130
2131 Empty.into_any()
2132 }
2133 };
2134
2135 let start = buffer
2136 .anchor_in_excerpt(excerpt_id, command.source_range.start)
2137 .unwrap();
2138 let end = buffer
2139 .anchor_in_excerpt(excerpt_id, command.source_range.end)
2140 .unwrap();
2141 Crease::new(start..end, placeholder, render_toggle, render_trailer)
2142 }),
2143 cx,
2144 );
2145
2146 let block_ids = editor.insert_blocks(
2147 updated
2148 .iter()
2149 .filter_map(|command| match &command.status {
2150 PendingSlashCommandStatus::Error(error) => {
2151 Some((command, error.clone()))
2152 }
2153 _ => None,
2154 })
2155 .map(|(command, error_message)| BlockProperties {
2156 style: BlockStyle::Fixed,
2157 position: Anchor {
2158 buffer_id: Some(buffer_id),
2159 excerpt_id,
2160 text_anchor: command.source_range.start,
2161 },
2162 height: 1,
2163 disposition: BlockDisposition::Below,
2164 render: slash_command_error_block_renderer(error_message),
2165 priority: 0,
2166 }),
2167 None,
2168 cx,
2169 );
2170
2171 self.pending_slash_command_creases.extend(
2172 updated
2173 .iter()
2174 .map(|command| command.source_range.clone())
2175 .zip(crease_ids),
2176 );
2177
2178 self.pending_slash_command_blocks.extend(
2179 updated
2180 .iter()
2181 .map(|command| command.source_range.clone())
2182 .zip(block_ids),
2183 );
2184 })
2185 }
2186 ContextEvent::SlashCommandFinished {
2187 output_range,
2188 sections,
2189 run_commands_in_output,
2190 expand_result,
2191 } => {
2192 self.insert_slash_command_output_sections(
2193 sections.iter().cloned(),
2194 *expand_result,
2195 cx,
2196 );
2197
2198 if *run_commands_in_output {
2199 let commands = self.context.update(cx, |context, cx| {
2200 context.reparse(cx);
2201 context
2202 .pending_commands_for_range(output_range.clone(), cx)
2203 .to_vec()
2204 });
2205
2206 for command in commands {
2207 self.run_command(
2208 command.source_range,
2209 &command.name,
2210 &command.arguments,
2211 false,
2212 false,
2213 self.workspace.clone(),
2214 cx,
2215 );
2216 }
2217 }
2218 }
2219 ContextEvent::UsePendingTools => {
2220 let pending_tool_uses = self
2221 .context
2222 .read(cx)
2223 .pending_tool_uses()
2224 .into_iter()
2225 .filter(|tool_use| tool_use.status.is_idle())
2226 .cloned()
2227 .collect::<Vec<_>>();
2228
2229 for tool_use in pending_tool_uses {
2230 let tool_registry = ToolRegistry::global(cx);
2231 if let Some(tool) = tool_registry.tool(&tool_use.name) {
2232 let task = tool.run(tool_use.input, self.workspace.clone(), cx);
2233
2234 self.context.update(cx, |context, cx| {
2235 context.insert_tool_output(tool_use.id.clone(), task, cx);
2236 });
2237 }
2238 }
2239 }
2240 ContextEvent::ToolFinished {
2241 tool_use_id,
2242 output_range,
2243 } => {
2244 self.editor.update(cx, |editor, cx| {
2245 let buffer = editor.buffer().read(cx).snapshot(cx);
2246 let (excerpt_id, _buffer_id, _) = buffer.as_singleton().unwrap();
2247 let excerpt_id = *excerpt_id;
2248
2249 let placeholder = FoldPlaceholder {
2250 render: render_fold_icon_button(
2251 cx.view().downgrade(),
2252 IconName::PocketKnife,
2253 format!("Tool Result: {tool_use_id}").into(),
2254 ),
2255 constrain_width: false,
2256 merge_adjacent: false,
2257 };
2258 let render_trailer =
2259 move |_row, _unfold, _cx: &mut WindowContext| Empty.into_any();
2260
2261 let start = buffer
2262 .anchor_in_excerpt(excerpt_id, output_range.start)
2263 .unwrap();
2264 let end = buffer
2265 .anchor_in_excerpt(excerpt_id, output_range.end)
2266 .unwrap();
2267
2268 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2269
2270 let crease = Crease::new(
2271 start..end,
2272 placeholder,
2273 fold_toggle("tool-use"),
2274 render_trailer,
2275 );
2276
2277 editor.insert_creases([crease], cx);
2278 editor.fold_at(&FoldAt { buffer_row }, cx);
2279 });
2280 }
2281 ContextEvent::Operation(_) => {}
2282 ContextEvent::ShowAssistError(error_message) => {
2283 self.error_message = Some(error_message.clone());
2284 }
2285 }
2286 }
2287
2288 fn workflow_steps_updated(
2289 &mut self,
2290 removed: &Vec<Range<text::Anchor>>,
2291 updated: &Vec<Range<text::Anchor>>,
2292 cx: &mut ViewContext<ContextEditor>,
2293 ) {
2294 let this = cx.view().downgrade();
2295 let mut removed_crease_ids = Vec::new();
2296 let mut removed_block_ids = HashSet::default();
2297 let mut editors_to_close = Vec::new();
2298 for range in removed {
2299 if let Some(state) = self.workflow_steps.remove(range) {
2300 editors_to_close.extend(self.hide_workflow_step(range.clone(), cx));
2301 removed_block_ids.insert(state.header_block_id);
2302 removed_crease_ids.push(state.header_crease_id);
2303 removed_block_ids.extend(state.footer_block_id);
2304 removed_crease_ids.extend(state.footer_crease_id);
2305 }
2306 }
2307
2308 for range in updated {
2309 editors_to_close.extend(self.hide_workflow_step(range.clone(), cx));
2310 }
2311
2312 self.editor.update(cx, |editor, cx| {
2313 let snapshot = editor.snapshot(cx);
2314 let multibuffer = &snapshot.buffer_snapshot;
2315 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2316
2317 for range in updated {
2318 let Some(step) = self.context.read(cx).workflow_step_for_range(&range, cx) else {
2319 continue;
2320 };
2321
2322 let resolution = step.resolution.clone();
2323 let header_start = step.range.start;
2324 let header_end = if buffer.contains_str_at(step.leading_tags_end, "\n") {
2325 buffer.anchor_before(step.leading_tags_end.to_offset(&buffer) + 1)
2326 } else {
2327 step.leading_tags_end
2328 };
2329 let header_range = multibuffer
2330 .anchor_in_excerpt(excerpt_id, header_start)
2331 .unwrap()
2332 ..multibuffer
2333 .anchor_in_excerpt(excerpt_id, header_end)
2334 .unwrap();
2335 let footer_range = step.trailing_tag_start.map(|start| {
2336 let mut step_range_end = step.range.end.to_offset(&buffer);
2337 if buffer.contains_str_at(step_range_end, "\n") {
2338 // Only include the newline if it belongs to the same message.
2339 let messages = self
2340 .context
2341 .read(cx)
2342 .messages_for_offsets([step_range_end, step_range_end + 1], cx);
2343 if messages.len() == 1 {
2344 step_range_end += 1;
2345 }
2346 }
2347
2348 let end = buffer.anchor_before(step_range_end);
2349 multibuffer.anchor_in_excerpt(excerpt_id, start).unwrap()
2350 ..multibuffer.anchor_in_excerpt(excerpt_id, end).unwrap()
2351 });
2352
2353 let block_ids = editor.insert_blocks(
2354 [BlockProperties {
2355 position: header_range.start,
2356 height: 1,
2357 style: BlockStyle::Flex,
2358 render: Box::new({
2359 let this = this.clone();
2360 let range = step.range.clone();
2361 move |cx| {
2362 let block_id = cx.block_id;
2363 let max_width = cx.max_width;
2364 let gutter_width = cx.gutter_dimensions.full_width();
2365 this.update(&mut **cx, |this, cx| {
2366 this.render_workflow_step_header(
2367 range.clone(),
2368 max_width,
2369 gutter_width,
2370 block_id,
2371 cx,
2372 )
2373 })
2374 .ok()
2375 .flatten()
2376 .unwrap_or_else(|| Empty.into_any())
2377 }
2378 }),
2379 disposition: BlockDisposition::Above,
2380 priority: 0,
2381 }]
2382 .into_iter()
2383 .chain(footer_range.as_ref().map(|footer_range| {
2384 return BlockProperties {
2385 position: footer_range.end,
2386 height: 1,
2387 style: BlockStyle::Flex,
2388 render: Box::new({
2389 let this = this.clone();
2390 let range = step.range.clone();
2391 move |cx| {
2392 let max_width = cx.max_width;
2393 let gutter_width = cx.gutter_dimensions.full_width();
2394 this.update(&mut **cx, |this, cx| {
2395 this.render_workflow_step_footer(
2396 range.clone(),
2397 max_width,
2398 gutter_width,
2399 cx,
2400 )
2401 })
2402 .ok()
2403 .flatten()
2404 .unwrap_or_else(|| Empty.into_any())
2405 }
2406 }),
2407 disposition: BlockDisposition::Below,
2408 priority: 0,
2409 };
2410 })),
2411 None,
2412 cx,
2413 );
2414
2415 let header_placeholder = FoldPlaceholder {
2416 render: Arc::new(move |_, _crease_range, _cx| Empty.into_any()),
2417 constrain_width: false,
2418 merge_adjacent: false,
2419 };
2420 let footer_placeholder = FoldPlaceholder {
2421 render: render_fold_icon_button(
2422 cx.view().downgrade(),
2423 IconName::Code,
2424 "Edits".into(),
2425 ),
2426 constrain_width: false,
2427 merge_adjacent: false,
2428 };
2429
2430 let new_crease_ids = editor.insert_creases(
2431 [Crease::new(
2432 header_range.clone(),
2433 header_placeholder.clone(),
2434 fold_toggle("step-header"),
2435 |_, _, _| Empty.into_any_element(),
2436 )]
2437 .into_iter()
2438 .chain(footer_range.clone().map(|footer_range| {
2439 Crease::new(
2440 footer_range,
2441 footer_placeholder.clone(),
2442 |row, is_folded, fold, cx| {
2443 if is_folded {
2444 Empty.into_any_element()
2445 } else {
2446 fold_toggle("step-footer")(row, is_folded, fold, cx)
2447 }
2448 },
2449 |_, _, _| Empty.into_any_element(),
2450 )
2451 })),
2452 cx,
2453 );
2454
2455 let state = WorkflowStepViewState {
2456 header_block_id: block_ids[0],
2457 header_crease_id: new_crease_ids[0],
2458 footer_block_id: block_ids.get(1).copied(),
2459 footer_crease_id: new_crease_ids.get(1).copied(),
2460 resolution,
2461 assist: None,
2462 };
2463
2464 let mut folds_to_insert = [(header_range.clone(), header_placeholder)]
2465 .into_iter()
2466 .chain(
2467 footer_range
2468 .clone()
2469 .map(|range| (range, footer_placeholder)),
2470 )
2471 .collect::<Vec<_>>();
2472
2473 match self.workflow_steps.entry(range.clone()) {
2474 hash_map::Entry::Vacant(entry) => {
2475 entry.insert(state);
2476 }
2477 hash_map::Entry::Occupied(mut entry) => {
2478 let entry = entry.get_mut();
2479 removed_block_ids.insert(entry.header_block_id);
2480 removed_crease_ids.push(entry.header_crease_id);
2481 removed_block_ids.extend(entry.footer_block_id);
2482 removed_crease_ids.extend(entry.footer_crease_id);
2483 folds_to_insert.retain(|(range, _)| snapshot.intersects_fold(range.start));
2484 *entry = state;
2485 }
2486 }
2487
2488 editor.unfold_ranges(
2489 [header_range.clone()]
2490 .into_iter()
2491 .chain(footer_range.clone()),
2492 true,
2493 false,
2494 cx,
2495 );
2496
2497 if !folds_to_insert.is_empty() {
2498 editor.fold_ranges(folds_to_insert, false, cx);
2499 }
2500 }
2501
2502 editor.remove_creases(removed_crease_ids, cx);
2503 editor.remove_blocks(removed_block_ids, None, cx);
2504 });
2505
2506 for (editor, editor_was_open) in editors_to_close {
2507 self.close_workflow_editor(cx, editor, editor_was_open);
2508 }
2509
2510 self.update_active_workflow_step(cx);
2511 }
2512
2513 fn insert_slash_command_output_sections(
2514 &mut self,
2515 sections: impl IntoIterator<Item = SlashCommandOutputSection<language::Anchor>>,
2516 expand_result: bool,
2517 cx: &mut ViewContext<Self>,
2518 ) {
2519 self.editor.update(cx, |editor, cx| {
2520 let buffer = editor.buffer().read(cx).snapshot(cx);
2521 let excerpt_id = *buffer.as_singleton().unwrap().0;
2522 let mut buffer_rows_to_fold = BTreeSet::new();
2523 let mut creases = Vec::new();
2524 for section in sections {
2525 let start = buffer
2526 .anchor_in_excerpt(excerpt_id, section.range.start)
2527 .unwrap();
2528 let end = buffer
2529 .anchor_in_excerpt(excerpt_id, section.range.end)
2530 .unwrap();
2531 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
2532 buffer_rows_to_fold.insert(buffer_row);
2533 creases.push(
2534 Crease::new(
2535 start..end,
2536 FoldPlaceholder {
2537 render: render_fold_icon_button(
2538 cx.view().downgrade(),
2539 section.icon,
2540 section.label.clone(),
2541 ),
2542 constrain_width: false,
2543 merge_adjacent: false,
2544 },
2545 render_slash_command_output_toggle,
2546 |_, _, _| Empty.into_any_element(),
2547 )
2548 .with_metadata(CreaseMetadata {
2549 icon: section.icon,
2550 label: section.label,
2551 }),
2552 );
2553 }
2554
2555 editor.insert_creases(creases, cx);
2556
2557 if expand_result {
2558 buffer_rows_to_fold.clear();
2559 }
2560 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
2561 editor.fold_at(&FoldAt { buffer_row }, cx);
2562 }
2563 });
2564 }
2565
2566 fn handle_editor_event(
2567 &mut self,
2568 _: View<Editor>,
2569 event: &EditorEvent,
2570 cx: &mut ViewContext<Self>,
2571 ) {
2572 match event {
2573 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2574 let cursor_scroll_position = self.cursor_scroll_position(cx);
2575 if *autoscroll {
2576 self.scroll_position = cursor_scroll_position;
2577 } else if self.scroll_position != cursor_scroll_position {
2578 self.scroll_position = None;
2579 }
2580 }
2581 EditorEvent::SelectionsChanged { .. } => {
2582 self.scroll_position = self.cursor_scroll_position(cx);
2583 self.update_active_workflow_step(cx);
2584 }
2585 _ => {}
2586 }
2587 cx.emit(event.clone());
2588 }
2589
2590 fn active_workflow_step(&self) -> Option<(Range<text::Anchor>, &WorkflowStepViewState)> {
2591 let step = self.active_workflow_step.as_ref()?;
2592 Some((step.range.clone(), self.workflow_steps.get(&step.range)?))
2593 }
2594
2595 fn update_active_workflow_step(&mut self, cx: &mut ViewContext<Self>) {
2596 let newest_cursor = self.editor.read(cx).selections.newest::<usize>(cx).head();
2597 let context = self.context.read(cx);
2598
2599 let new_step = context
2600 .workflow_step_containing(newest_cursor, cx)
2601 .map(|step| ActiveWorkflowStep {
2602 resolved: step.resolution.is_some(),
2603 range: step.range.clone(),
2604 });
2605
2606 if new_step.as_ref() != self.active_workflow_step.as_ref() {
2607 let mut old_editor = None;
2608 let mut old_editor_was_open = None;
2609 if let Some(old_step) = self.active_workflow_step.take() {
2610 (old_editor, old_editor_was_open) =
2611 self.hide_workflow_step(old_step.range, cx).unzip();
2612 }
2613
2614 let mut new_editor = None;
2615 if let Some(new_step) = new_step {
2616 new_editor = self.show_workflow_step(new_step.range.clone(), cx);
2617 self.active_workflow_step = Some(new_step);
2618 }
2619
2620 if new_editor != old_editor {
2621 if let Some((old_editor, old_editor_was_open)) = old_editor.zip(old_editor_was_open)
2622 {
2623 self.close_workflow_editor(cx, old_editor, old_editor_was_open)
2624 }
2625 }
2626 }
2627 }
2628
2629 fn hide_workflow_step(
2630 &mut self,
2631 step_range: Range<language::Anchor>,
2632 cx: &mut ViewContext<Self>,
2633 ) -> Option<(View<Editor>, bool)> {
2634 if let Some(step) = self.workflow_steps.get_mut(&step_range) {
2635 let assist = step.assist.as_ref()?;
2636 let editor = assist.editor.upgrade()?;
2637
2638 if matches!(step.status(cx), WorkflowStepStatus::Idle) {
2639 let assist = step.assist.take().unwrap();
2640 InlineAssistant::update_global(cx, |assistant, cx| {
2641 for assist_id in assist.assist_ids {
2642 assistant.finish_assist(assist_id, true, cx)
2643 }
2644 });
2645 return Some((editor, assist.editor_was_open));
2646 }
2647 }
2648
2649 None
2650 }
2651
2652 fn close_workflow_editor(
2653 &mut self,
2654 cx: &mut ViewContext<ContextEditor>,
2655 editor: View<Editor>,
2656 editor_was_open: bool,
2657 ) {
2658 self.workspace
2659 .update(cx, |workspace, cx| {
2660 if let Some(pane) = workspace.pane_for(&editor) {
2661 pane.update(cx, |pane, cx| {
2662 let item_id = editor.entity_id();
2663 if !editor_was_open && !editor.read(cx).is_focused(cx) {
2664 pane.close_item_by_id(item_id, SaveIntent::Skip, cx)
2665 .detach_and_log_err(cx);
2666 }
2667 });
2668 }
2669 })
2670 .ok();
2671 }
2672
2673 fn show_workflow_step(
2674 &mut self,
2675 step_range: Range<language::Anchor>,
2676 cx: &mut ViewContext<Self>,
2677 ) -> Option<View<Editor>> {
2678 let step = self.workflow_steps.get_mut(&step_range)?;
2679
2680 let mut editor_to_return = None;
2681 let mut scroll_to_assist_id = None;
2682 match step.status(cx) {
2683 WorkflowStepStatus::Idle => {
2684 if let Some(assist) = step.assist.as_ref() {
2685 scroll_to_assist_id = assist.assist_ids.first().copied();
2686 } else if let Some(Ok(resolved)) = step.resolution.clone().as_deref() {
2687 step.assist = Self::open_assists_for_step(
2688 &resolved,
2689 &self.project,
2690 &self.assistant_panel,
2691 &self.workspace,
2692 cx,
2693 );
2694 editor_to_return = step
2695 .assist
2696 .as_ref()
2697 .and_then(|assist| assist.editor.upgrade());
2698 }
2699 }
2700 WorkflowStepStatus::Pending => {
2701 if let Some(assist) = step.assist.as_ref() {
2702 let assistant = InlineAssistant::global(cx);
2703 scroll_to_assist_id = assist
2704 .assist_ids
2705 .iter()
2706 .copied()
2707 .find(|assist_id| assistant.assist_status(*assist_id, cx).is_pending());
2708 }
2709 }
2710 WorkflowStepStatus::Done => {
2711 if let Some(assist) = step.assist.as_ref() {
2712 scroll_to_assist_id = assist.assist_ids.first().copied();
2713 }
2714 }
2715 _ => {}
2716 }
2717
2718 if let Some(assist_id) = scroll_to_assist_id {
2719 if let Some(assist_editor) = step
2720 .assist
2721 .as_ref()
2722 .and_then(|assists| assists.editor.upgrade())
2723 {
2724 editor_to_return = Some(assist_editor.clone());
2725 self.workspace
2726 .update(cx, |workspace, cx| {
2727 workspace.activate_item(&assist_editor, false, false, cx);
2728 })
2729 .ok();
2730 InlineAssistant::update_global(cx, |assistant, cx| {
2731 assistant.scroll_to_assist(assist_id, cx)
2732 });
2733 }
2734 }
2735
2736 editor_to_return
2737 }
2738
2739 fn open_assists_for_step(
2740 resolved_step: &WorkflowStepResolution,
2741 project: &Model<Project>,
2742 assistant_panel: &WeakView<AssistantPanel>,
2743 workspace: &WeakView<Workspace>,
2744 cx: &mut ViewContext<Self>,
2745 ) -> Option<WorkflowAssist> {
2746 let assistant_panel = assistant_panel.upgrade()?;
2747 if resolved_step.suggestion_groups.is_empty() {
2748 return None;
2749 }
2750
2751 let editor;
2752 let mut editor_was_open = false;
2753 let mut suggestion_groups = Vec::new();
2754 if resolved_step.suggestion_groups.len() == 1
2755 && resolved_step
2756 .suggestion_groups
2757 .values()
2758 .next()
2759 .unwrap()
2760 .len()
2761 == 1
2762 {
2763 // If there's only one buffer and one suggestion group, open it directly
2764 let (buffer, groups) = resolved_step.suggestion_groups.iter().next().unwrap();
2765 let group = groups.into_iter().next().unwrap();
2766 editor = workspace
2767 .update(cx, |workspace, cx| {
2768 let active_pane = workspace.active_pane().clone();
2769 editor_was_open =
2770 workspace.is_project_item_open::<Editor>(&active_pane, buffer, cx);
2771 workspace.open_project_item::<Editor>(
2772 active_pane,
2773 buffer.clone(),
2774 false,
2775 false,
2776 cx,
2777 )
2778 })
2779 .log_err()?;
2780 let (&excerpt_id, _, _) = editor
2781 .read(cx)
2782 .buffer()
2783 .read(cx)
2784 .read(cx)
2785 .as_singleton()
2786 .unwrap();
2787
2788 // Scroll the editor to the suggested assist
2789 editor.update(cx, |editor, cx| {
2790 let multibuffer = editor.buffer().read(cx).snapshot(cx);
2791 let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap();
2792 let anchor = if group.context_range.start.to_offset(buffer) == 0 {
2793 Anchor::min()
2794 } else {
2795 multibuffer
2796 .anchor_in_excerpt(excerpt_id, group.context_range.start)
2797 .unwrap()
2798 };
2799
2800 editor.set_scroll_anchor(
2801 ScrollAnchor {
2802 offset: gpui::Point::default(),
2803 anchor,
2804 },
2805 cx,
2806 );
2807 });
2808
2809 suggestion_groups.push((excerpt_id, group));
2810 } else {
2811 // If there are multiple buffers or suggestion groups, create a multibuffer
2812 let multibuffer = cx.new_model(|cx| {
2813 let replica_id = project.read(cx).replica_id();
2814 let mut multibuffer = MultiBuffer::new(replica_id, Capability::ReadWrite)
2815 .with_title(resolved_step.title.clone());
2816 for (buffer, groups) in &resolved_step.suggestion_groups {
2817 let excerpt_ids = multibuffer.push_excerpts(
2818 buffer.clone(),
2819 groups.iter().map(|suggestion_group| ExcerptRange {
2820 context: suggestion_group.context_range.clone(),
2821 primary: None,
2822 }),
2823 cx,
2824 );
2825 suggestion_groups.extend(excerpt_ids.into_iter().zip(groups));
2826 }
2827 multibuffer
2828 });
2829
2830 editor = cx.new_view(|cx| {
2831 Editor::for_multibuffer(multibuffer, Some(project.clone()), true, cx)
2832 });
2833 workspace
2834 .update(cx, |workspace, cx| {
2835 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, false, cx)
2836 })
2837 .log_err()?;
2838 }
2839
2840 let mut assist_ids = Vec::new();
2841 for (excerpt_id, suggestion_group) in suggestion_groups {
2842 for suggestion in &suggestion_group.suggestions {
2843 assist_ids.extend(suggestion.show(
2844 &editor,
2845 excerpt_id,
2846 workspace,
2847 &assistant_panel,
2848 cx,
2849 ));
2850 }
2851 }
2852
2853 Some(WorkflowAssist {
2854 assist_ids,
2855 editor: editor.downgrade(),
2856 editor_was_open,
2857 })
2858 }
2859
2860 fn handle_editor_search_event(
2861 &mut self,
2862 _: View<Editor>,
2863 event: &SearchEvent,
2864 cx: &mut ViewContext<Self>,
2865 ) {
2866 cx.emit(event.clone());
2867 }
2868
2869 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2870 self.editor.update(cx, |editor, cx| {
2871 let snapshot = editor.snapshot(cx);
2872 let cursor = editor.selections.newest_anchor().head();
2873 let cursor_row = cursor
2874 .to_display_point(&snapshot.display_snapshot)
2875 .row()
2876 .as_f32();
2877 let scroll_position = editor
2878 .scroll_manager
2879 .anchor()
2880 .scroll_position(&snapshot.display_snapshot);
2881
2882 let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2883 if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2884 Some(ScrollPosition {
2885 cursor,
2886 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2887 })
2888 } else {
2889 None
2890 }
2891 })
2892 }
2893
2894 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2895 self.editor.update(cx, |editor, cx| {
2896 let buffer = editor.buffer().read(cx).snapshot(cx);
2897
2898 let excerpt_id = *buffer.as_singleton().unwrap().0;
2899 let mut old_blocks = std::mem::take(&mut self.blocks);
2900 let mut blocks_to_remove: HashMap<_, _> = old_blocks
2901 .iter()
2902 .map(|(message_id, (_, block_id))| (*message_id, *block_id))
2903 .collect();
2904 let mut blocks_to_replace: HashMap<_, RenderBlock> = Default::default();
2905
2906 let render_block = |message: MessageMetadata| -> RenderBlock {
2907 Box::new({
2908 let context = self.context.clone();
2909 move |cx| {
2910 let message_id = MessageId(message.timestamp);
2911 let show_spinner = message.role == Role::Assistant
2912 && message.status == MessageStatus::Pending;
2913
2914 let label = match message.role {
2915 Role::User => {
2916 Label::new("You").color(Color::Default).into_any_element()
2917 }
2918 Role::Assistant => {
2919 let label = Label::new("Assistant").color(Color::Info);
2920 if show_spinner {
2921 label
2922 .with_animation(
2923 "pulsating-label",
2924 Animation::new(Duration::from_secs(2))
2925 .repeat()
2926 .with_easing(pulsating_between(0.4, 0.8)),
2927 |label, delta| label.alpha(delta),
2928 )
2929 .into_any_element()
2930 } else {
2931 label.into_any_element()
2932 }
2933 }
2934
2935 Role::System => Label::new("System")
2936 .color(Color::Warning)
2937 .into_any_element(),
2938 };
2939
2940 let sender = ButtonLike::new("role")
2941 .style(ButtonStyle::Filled)
2942 .child(label)
2943 .tooltip(|cx| {
2944 Tooltip::with_meta(
2945 "Toggle message role",
2946 None,
2947 "Available roles: You (User), Assistant, System",
2948 cx,
2949 )
2950 })
2951 .on_click({
2952 let context = context.clone();
2953 move |_, cx| {
2954 context.update(cx, |context, cx| {
2955 context.cycle_message_roles(
2956 HashSet::from_iter(Some(message_id)),
2957 cx,
2958 )
2959 })
2960 }
2961 });
2962
2963 h_flex()
2964 .id(("message_header", message_id.as_u64()))
2965 .pl(cx.gutter_dimensions.full_width())
2966 .h_11()
2967 .w_full()
2968 .relative()
2969 .gap_1()
2970 .child(sender)
2971 .children(match &message.cache {
2972 Some(cache) if cache.is_final_anchor => match cache.status {
2973 CacheStatus::Cached => Some(
2974 div()
2975 .id("cached")
2976 .child(
2977 Icon::new(IconName::DatabaseZap)
2978 .size(IconSize::XSmall)
2979 .color(Color::Hint),
2980 )
2981 .tooltip(|cx| {
2982 Tooltip::with_meta(
2983 "Context cached",
2984 None,
2985 "Large messages cached to optimize performance",
2986 cx,
2987 )
2988 })
2989 .into_any_element(),
2990 ),
2991 CacheStatus::Pending => Some(
2992 div()
2993 .child(
2994 Icon::new(IconName::Ellipsis)
2995 .size(IconSize::XSmall)
2996 .color(Color::Hint),
2997 )
2998 .into_any_element(),
2999 ),
3000 },
3001 _ => None,
3002 })
3003 .children(match &message.status {
3004 MessageStatus::Error(error) => Some(
3005 Button::new("show-error", "Error")
3006 .color(Color::Error)
3007 .selected_label_color(Color::Error)
3008 .selected_icon_color(Color::Error)
3009 .icon(IconName::XCircle)
3010 .icon_color(Color::Error)
3011 .icon_size(IconSize::Small)
3012 .icon_position(IconPosition::Start)
3013 .tooltip(move |cx| {
3014 Tooltip::with_meta(
3015 "Error interacting with language model",
3016 None,
3017 "Click for more details",
3018 cx,
3019 )
3020 })
3021 .on_click({
3022 let context = context.clone();
3023 let error = error.clone();
3024 move |_, cx| {
3025 context.update(cx, |_, cx| {
3026 cx.emit(ContextEvent::ShowAssistError(
3027 error.clone(),
3028 ));
3029 });
3030 }
3031 })
3032 .into_any_element(),
3033 ),
3034 MessageStatus::Canceled => Some(
3035 ButtonLike::new("canceled")
3036 .child(Icon::new(IconName::XCircle).color(Color::Disabled))
3037 .child(
3038 Label::new("Canceled")
3039 .size(LabelSize::Small)
3040 .color(Color::Disabled),
3041 )
3042 .tooltip(move |cx| {
3043 Tooltip::with_meta(
3044 "Canceled",
3045 None,
3046 "Interaction with the assistant was canceled",
3047 cx,
3048 )
3049 })
3050 .into_any_element(),
3051 ),
3052 _ => None,
3053 })
3054 .into_any_element()
3055 }
3056 })
3057 };
3058 let create_block_properties = |message: &Message| BlockProperties {
3059 position: buffer
3060 .anchor_in_excerpt(excerpt_id, message.anchor_range.start)
3061 .unwrap(),
3062 height: 2,
3063 style: BlockStyle::Sticky,
3064 disposition: BlockDisposition::Above,
3065 priority: usize::MAX,
3066 render: render_block(MessageMetadata::from(message)),
3067 };
3068 let mut new_blocks = vec![];
3069 let mut block_index_to_message = vec![];
3070 for message in self.context.read(cx).messages(cx) {
3071 if let Some(_) = blocks_to_remove.remove(&message.id) {
3072 // This is an old message that we might modify.
3073 let Some((meta, block_id)) = old_blocks.get_mut(&message.id) else {
3074 debug_assert!(
3075 false,
3076 "old_blocks should contain a message_id we've just removed."
3077 );
3078 continue;
3079 };
3080 // Should we modify it?
3081 let message_meta = MessageMetadata::from(&message);
3082 if meta != &message_meta {
3083 blocks_to_replace.insert(*block_id, render_block(message_meta.clone()));
3084 *meta = message_meta;
3085 }
3086 } else {
3087 // This is a new message.
3088 new_blocks.push(create_block_properties(&message));
3089 block_index_to_message.push((message.id, MessageMetadata::from(&message)));
3090 }
3091 }
3092 editor.replace_blocks(blocks_to_replace, None, cx);
3093 editor.remove_blocks(blocks_to_remove.into_values().collect(), None, cx);
3094
3095 let ids = editor.insert_blocks(new_blocks, None, cx);
3096 old_blocks.extend(ids.into_iter().zip(block_index_to_message).map(
3097 |(block_id, (message_id, message_meta))| (message_id, (message_meta, block_id)),
3098 ));
3099 self.blocks = old_blocks;
3100 });
3101 }
3102
3103 fn insert_selection(
3104 workspace: &mut Workspace,
3105 _: &InsertIntoEditor,
3106 cx: &mut ViewContext<Workspace>,
3107 ) {
3108 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3109 return;
3110 };
3111 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
3112 return;
3113 };
3114 let Some(active_editor_view) = workspace
3115 .active_item(cx)
3116 .and_then(|item| item.act_as::<Editor>(cx))
3117 else {
3118 return;
3119 };
3120
3121 let context_editor = context_editor_view.read(cx).editor.read(cx);
3122 let anchor = context_editor.selections.newest_anchor();
3123 let text = context_editor
3124 .buffer()
3125 .read(cx)
3126 .read(cx)
3127 .text_for_range(anchor.range())
3128 .collect::<String>();
3129
3130 // If nothing is selected, don't delete the current selection; instead, be a no-op.
3131 if !text.is_empty() {
3132 active_editor_view.update(cx, |editor, cx| {
3133 editor.insert(&text, cx);
3134 editor.focus(cx);
3135 })
3136 }
3137 }
3138
3139 fn insert_dragged_files(
3140 workspace: &mut Workspace,
3141 action: &InsertDraggedFiles,
3142 cx: &mut ViewContext<Workspace>,
3143 ) {
3144 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3145 return;
3146 };
3147 let Some(context_editor_view) = panel.read(cx).active_context_editor(cx) else {
3148 return;
3149 };
3150
3151 let project = workspace.project().clone();
3152
3153 let paths = match action {
3154 InsertDraggedFiles::ProjectPaths(paths) => Task::ready((paths.clone(), vec![])),
3155 InsertDraggedFiles::ExternalFiles(paths) => {
3156 let tasks = paths
3157 .clone()
3158 .into_iter()
3159 .map(|path| Workspace::project_path_for_path(project.clone(), &path, false, cx))
3160 .collect::<Vec<_>>();
3161
3162 cx.spawn(move |_, cx| async move {
3163 let mut paths = vec![];
3164 let mut worktrees = vec![];
3165
3166 let opened_paths = futures::future::join_all(tasks).await;
3167 for (worktree, project_path) in opened_paths.into_iter().flatten() {
3168 let Ok(worktree_root_name) =
3169 worktree.read_with(&cx, |worktree, _| worktree.root_name().to_string())
3170 else {
3171 continue;
3172 };
3173
3174 let mut full_path = PathBuf::from(worktree_root_name.clone());
3175 full_path.push(&project_path.path);
3176 paths.push(full_path);
3177 worktrees.push(worktree);
3178 }
3179
3180 (paths, worktrees)
3181 })
3182 }
3183 };
3184
3185 cx.spawn(|_, mut cx| async move {
3186 let (paths, dragged_file_worktrees) = paths.await;
3187 let cmd_name = file_command::FileSlashCommand.name();
3188
3189 context_editor_view
3190 .update(&mut cx, |context_editor, cx| {
3191 let file_argument = paths
3192 .into_iter()
3193 .map(|path| path.to_string_lossy().to_string())
3194 .collect::<Vec<_>>()
3195 .join(" ");
3196
3197 context_editor.editor.update(cx, |editor, cx| {
3198 editor.insert("\n", cx);
3199 editor.insert(&format!("/{} {}", cmd_name, file_argument), cx);
3200 });
3201
3202 context_editor.confirm_command(&ConfirmCommand, cx);
3203
3204 context_editor
3205 .dragged_file_worktrees
3206 .extend(dragged_file_worktrees);
3207 })
3208 .log_err();
3209 })
3210 .detach();
3211 }
3212
3213 fn quote_selection(
3214 workspace: &mut Workspace,
3215 _: &QuoteSelection,
3216 cx: &mut ViewContext<Workspace>,
3217 ) {
3218 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
3219 return;
3220 };
3221 let Some(editor) = workspace
3222 .active_item(cx)
3223 .and_then(|item| item.act_as::<Editor>(cx))
3224 else {
3225 return;
3226 };
3227
3228 let mut creases = vec![];
3229 editor.update(cx, |editor, cx| {
3230 let selections = editor.selections.all_adjusted(cx);
3231 let buffer = editor.buffer().read(cx).snapshot(cx);
3232 for selection in selections {
3233 let range = editor::ToOffset::to_offset(&selection.start, &buffer)
3234 ..editor::ToOffset::to_offset(&selection.end, &buffer);
3235 let selected_text = buffer.text_for_range(range.clone()).collect::<String>();
3236 if selected_text.is_empty() {
3237 continue;
3238 }
3239 let start_language = buffer.language_at(range.start);
3240 let end_language = buffer.language_at(range.end);
3241 let language_name = if start_language == end_language {
3242 start_language.map(|language| language.code_fence_block_name())
3243 } else {
3244 None
3245 };
3246 let language_name = language_name.as_deref().unwrap_or("");
3247 let filename = buffer
3248 .file_at(selection.start)
3249 .map(|file| file.full_path(cx));
3250 let text = if language_name == "markdown" {
3251 selected_text
3252 .lines()
3253 .map(|line| format!("> {}", line))
3254 .collect::<Vec<_>>()
3255 .join("\n")
3256 } else {
3257 let start_symbols = buffer
3258 .symbols_containing(selection.start, None)
3259 .map(|(_, symbols)| symbols);
3260 let end_symbols = buffer
3261 .symbols_containing(selection.end, None)
3262 .map(|(_, symbols)| symbols);
3263
3264 let outline_text = if let Some((start_symbols, end_symbols)) =
3265 start_symbols.zip(end_symbols)
3266 {
3267 Some(
3268 start_symbols
3269 .into_iter()
3270 .zip(end_symbols)
3271 .take_while(|(a, b)| a == b)
3272 .map(|(a, _)| a.text)
3273 .collect::<Vec<_>>()
3274 .join(" > "),
3275 )
3276 } else {
3277 None
3278 };
3279
3280 let line_comment_prefix = start_language
3281 .and_then(|l| l.default_scope().line_comment_prefixes().first().cloned());
3282
3283 let fence = codeblock_fence_for_path(
3284 filename.as_deref(),
3285 Some(selection.start.row..=selection.end.row),
3286 );
3287
3288 if let Some((line_comment_prefix, outline_text)) =
3289 line_comment_prefix.zip(outline_text)
3290 {
3291 let breadcrumb =
3292 format!("{line_comment_prefix}Excerpt from: {outline_text}\n");
3293 format!("{fence}{breadcrumb}{selected_text}\n```")
3294 } else {
3295 format!("{fence}{selected_text}\n```")
3296 }
3297 };
3298 let crease_title = if let Some(path) = filename {
3299 let start_line = selection.start.row + 1;
3300 let end_line = selection.end.row + 1;
3301 if start_line == end_line {
3302 format!("{}, Line {}", path.display(), start_line)
3303 } else {
3304 format!("{}, Lines {} to {}", path.display(), start_line, end_line)
3305 }
3306 } else {
3307 "Quoted selection".to_string()
3308 };
3309 creases.push((text, crease_title));
3310 }
3311 });
3312 if creases.is_empty() {
3313 return;
3314 }
3315 // Activate the panel
3316 if !panel.focus_handle(cx).contains_focused(cx) {
3317 workspace.toggle_panel_focus::<AssistantPanel>(cx);
3318 }
3319
3320 panel.update(cx, |_, cx| {
3321 // Wait to create a new context until the workspace is no longer
3322 // being updated.
3323 cx.defer(move |panel, cx| {
3324 if let Some(context) = panel
3325 .active_context_editor(cx)
3326 .or_else(|| panel.new_context(cx))
3327 {
3328 context.update(cx, |context, cx| {
3329 context.editor.update(cx, |editor, cx| {
3330 editor.insert("\n", cx);
3331 for (text, crease_title) in creases {
3332 let point = editor.selections.newest::<Point>(cx).head();
3333 let start_row = MultiBufferRow(point.row);
3334
3335 editor.insert(&text, cx);
3336
3337 let snapshot = editor.buffer().read(cx).snapshot(cx);
3338 let anchor_before = snapshot.anchor_after(point);
3339 let anchor_after = editor
3340 .selections
3341 .newest_anchor()
3342 .head()
3343 .bias_left(&snapshot);
3344
3345 editor.insert("\n", cx);
3346
3347 let fold_placeholder = quote_selection_fold_placeholder(
3348 crease_title,
3349 cx.view().downgrade(),
3350 );
3351 let crease = Crease::new(
3352 anchor_before..anchor_after,
3353 fold_placeholder,
3354 render_quote_selection_output_toggle,
3355 |_, _, _| Empty.into_any(),
3356 );
3357 editor.insert_creases(vec![crease], cx);
3358 editor.fold_at(
3359 &FoldAt {
3360 buffer_row: start_row,
3361 },
3362 cx,
3363 );
3364 }
3365 })
3366 });
3367 };
3368 });
3369 });
3370 }
3371
3372 fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
3373 if self.editor.read(cx).selections.count() == 1 {
3374 let (copied_text, metadata) = self.get_clipboard_contents(cx);
3375 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3376 copied_text,
3377 metadata,
3378 ));
3379 cx.stop_propagation();
3380 return;
3381 }
3382
3383 cx.propagate();
3384 }
3385
3386 fn cut(&mut self, _: &editor::actions::Cut, cx: &mut ViewContext<Self>) {
3387 if self.editor.read(cx).selections.count() == 1 {
3388 let (copied_text, metadata) = self.get_clipboard_contents(cx);
3389
3390 self.editor.update(cx, |editor, cx| {
3391 let selections = editor.selections.all::<Point>(cx);
3392
3393 editor.transact(cx, |this, cx| {
3394 this.change_selections(Some(Autoscroll::fit()), cx, |s| {
3395 s.select(selections);
3396 });
3397 this.insert("", cx);
3398 cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata(
3399 copied_text,
3400 metadata,
3401 ));
3402 });
3403 });
3404
3405 cx.stop_propagation();
3406 return;
3407 }
3408
3409 cx.propagate();
3410 }
3411
3412 fn get_clipboard_contents(&mut self, cx: &mut ViewContext<Self>) -> (String, CopyMetadata) {
3413 let creases = self.editor.update(cx, |editor, cx| {
3414 let selection = editor.selections.newest::<Point>(cx);
3415 let selection_start = editor.selections.newest::<usize>(cx).start;
3416 let snapshot = editor.buffer().read(cx).snapshot(cx);
3417 editor.display_map.update(cx, |display_map, cx| {
3418 display_map
3419 .snapshot(cx)
3420 .crease_snapshot
3421 .creases_in_range(
3422 MultiBufferRow(selection.start.row)..MultiBufferRow(selection.end.row + 1),
3423 &snapshot,
3424 )
3425 .filter_map(|crease| {
3426 if let Some(metadata) = &crease.metadata {
3427 let start = crease
3428 .range
3429 .start
3430 .to_offset(&snapshot)
3431 .saturating_sub(selection_start);
3432 let end = crease
3433 .range
3434 .end
3435 .to_offset(&snapshot)
3436 .saturating_sub(selection_start);
3437
3438 let range_relative_to_selection = start..end;
3439
3440 if range_relative_to_selection.is_empty() {
3441 None
3442 } else {
3443 Some(SelectedCreaseMetadata {
3444 range_relative_to_selection,
3445 crease: metadata.clone(),
3446 })
3447 }
3448 } else {
3449 None
3450 }
3451 })
3452 .collect::<Vec<_>>()
3453 })
3454 });
3455
3456 let context = self.context.read(cx);
3457 let selection = self.editor.read(cx).selections.newest::<usize>(cx);
3458 let mut text = String::new();
3459 for message in context.messages(cx) {
3460 if message.offset_range.start >= selection.range().end {
3461 break;
3462 } else if message.offset_range.end >= selection.range().start {
3463 let range = cmp::max(message.offset_range.start, selection.range().start)
3464 ..cmp::min(message.offset_range.end, selection.range().end);
3465 if !range.is_empty() {
3466 for chunk in context.buffer().read(cx).text_for_range(range) {
3467 text.push_str(chunk);
3468 }
3469 text.push('\n');
3470 }
3471 }
3472 }
3473
3474 (text, CopyMetadata { creases })
3475 }
3476
3477 fn paste(&mut self, action: &editor::actions::Paste, cx: &mut ViewContext<Self>) {
3478 cx.stop_propagation();
3479
3480 let images = if let Some(item) = cx.read_from_clipboard() {
3481 item.into_entries()
3482 .filter_map(|entry| {
3483 if let ClipboardEntry::Image(image) = entry {
3484 Some(image)
3485 } else {
3486 None
3487 }
3488 })
3489 .collect()
3490 } else {
3491 Vec::new()
3492 };
3493
3494 let metadata = if let Some(item) = cx.read_from_clipboard() {
3495 item.entries().first().and_then(|entry| {
3496 if let ClipboardEntry::String(text) = entry {
3497 text.metadata_json::<CopyMetadata>()
3498 } else {
3499 None
3500 }
3501 })
3502 } else {
3503 None
3504 };
3505
3506 if images.is_empty() {
3507 self.editor.update(cx, |editor, cx| {
3508 let paste_position = editor.selections.newest::<usize>(cx).head();
3509 editor.paste(action, cx);
3510
3511 if let Some(metadata) = metadata {
3512 let buffer = editor.buffer().read(cx).snapshot(cx);
3513
3514 let mut buffer_rows_to_fold = BTreeSet::new();
3515 let weak_editor = cx.view().downgrade();
3516 editor.insert_creases(
3517 metadata.creases.into_iter().map(|metadata| {
3518 let start = buffer.anchor_after(
3519 paste_position + metadata.range_relative_to_selection.start,
3520 );
3521 let end = buffer.anchor_before(
3522 paste_position + metadata.range_relative_to_selection.end,
3523 );
3524
3525 let buffer_row = MultiBufferRow(start.to_point(&buffer).row);
3526 buffer_rows_to_fold.insert(buffer_row);
3527 Crease::new(
3528 start..end,
3529 FoldPlaceholder {
3530 constrain_width: false,
3531 render: render_fold_icon_button(
3532 weak_editor.clone(),
3533 metadata.crease.icon,
3534 metadata.crease.label.clone(),
3535 ),
3536 merge_adjacent: false,
3537 },
3538 render_slash_command_output_toggle,
3539 |_, _, _| Empty.into_any(),
3540 )
3541 .with_metadata(metadata.crease.clone())
3542 }),
3543 cx,
3544 );
3545 for buffer_row in buffer_rows_to_fold.into_iter().rev() {
3546 editor.fold_at(&FoldAt { buffer_row }, cx);
3547 }
3548 }
3549 });
3550 } else {
3551 let mut image_positions = Vec::new();
3552 self.editor.update(cx, |editor, cx| {
3553 editor.transact(cx, |editor, cx| {
3554 let edits = editor
3555 .selections
3556 .all::<usize>(cx)
3557 .into_iter()
3558 .map(|selection| (selection.start..selection.end, "\n"));
3559 editor.edit(edits, cx);
3560
3561 let snapshot = editor.buffer().read(cx).snapshot(cx);
3562 for selection in editor.selections.all::<usize>(cx) {
3563 image_positions.push(snapshot.anchor_before(selection.end));
3564 }
3565 });
3566 });
3567
3568 self.context.update(cx, |context, cx| {
3569 for image in images {
3570 let Some(render_image) = image.to_image_data(cx).log_err() else {
3571 continue;
3572 };
3573 let image_id = image.id();
3574 let image_task = LanguageModelImage::from_image(image, cx).shared();
3575
3576 for image_position in image_positions.iter() {
3577 context.insert_content(
3578 Content::Image {
3579 anchor: image_position.text_anchor,
3580 image_id,
3581 image: image_task.clone(),
3582 render_image: render_image.clone(),
3583 },
3584 cx,
3585 );
3586 }
3587 }
3588 });
3589 }
3590 }
3591
3592 fn update_image_blocks(&mut self, cx: &mut ViewContext<Self>) {
3593 self.editor.update(cx, |editor, cx| {
3594 let buffer = editor.buffer().read(cx).snapshot(cx);
3595 let excerpt_id = *buffer.as_singleton().unwrap().0;
3596 let old_blocks = std::mem::take(&mut self.image_blocks);
3597 let new_blocks = self
3598 .context
3599 .read(cx)
3600 .contents(cx)
3601 .filter_map(|content| {
3602 if let Content::Image {
3603 anchor,
3604 render_image,
3605 ..
3606 } = content
3607 {
3608 Some((anchor, render_image))
3609 } else {
3610 None
3611 }
3612 })
3613 .filter_map(|(anchor, render_image)| {
3614 const MAX_HEIGHT_IN_LINES: u32 = 8;
3615 let anchor = buffer.anchor_in_excerpt(excerpt_id, anchor).unwrap();
3616 let image = render_image.clone();
3617 anchor.is_valid(&buffer).then(|| BlockProperties {
3618 position: anchor,
3619 height: MAX_HEIGHT_IN_LINES,
3620 style: BlockStyle::Sticky,
3621 render: Box::new(move |cx| {
3622 let image_size = size_for_image(
3623 &image,
3624 size(
3625 cx.max_width - cx.gutter_dimensions.full_width(),
3626 MAX_HEIGHT_IN_LINES as f32 * cx.line_height,
3627 ),
3628 );
3629 h_flex()
3630 .pl(cx.gutter_dimensions.full_width())
3631 .child(
3632 img(image.clone())
3633 .object_fit(gpui::ObjectFit::ScaleDown)
3634 .w(image_size.width)
3635 .h(image_size.height),
3636 )
3637 .into_any_element()
3638 }),
3639
3640 disposition: BlockDisposition::Above,
3641 priority: 0,
3642 })
3643 })
3644 .collect::<Vec<_>>();
3645
3646 editor.remove_blocks(old_blocks, None, cx);
3647 let ids = editor.insert_blocks(new_blocks, None, cx);
3648 self.image_blocks = HashSet::from_iter(ids);
3649 });
3650 }
3651
3652 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
3653 self.context.update(cx, |context, cx| {
3654 let selections = self.editor.read(cx).selections.disjoint_anchors();
3655 for selection in selections.as_ref() {
3656 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
3657 let range = selection
3658 .map(|endpoint| endpoint.to_offset(&buffer))
3659 .range();
3660 context.split_message(range, cx);
3661 }
3662 });
3663 }
3664
3665 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
3666 self.context.update(cx, |context, cx| {
3667 context.save(Some(Duration::from_millis(500)), self.fs.clone(), cx)
3668 });
3669 }
3670
3671 fn title(&self, cx: &AppContext) -> Cow<str> {
3672 self.context
3673 .read(cx)
3674 .summary()
3675 .map(|summary| summary.text.clone())
3676 .map(Cow::Owned)
3677 .unwrap_or_else(|| Cow::Borrowed(DEFAULT_TAB_TITLE))
3678 }
3679
3680 fn render_workflow_step_header(
3681 &self,
3682 range: Range<text::Anchor>,
3683 max_width: Pixels,
3684 gutter_width: Pixels,
3685 id: BlockId,
3686 cx: &mut ViewContext<Self>,
3687 ) -> Option<AnyElement> {
3688 let step_state = self.workflow_steps.get(&range)?;
3689 let status = step_state.status(cx);
3690 let this = cx.view().downgrade();
3691
3692 let theme = cx.theme().status();
3693 let is_confirmed = status.is_confirmed();
3694 let border_color = if is_confirmed {
3695 theme.ignored_border
3696 } else {
3697 theme.info_border
3698 };
3699
3700 let editor = self.editor.read(cx);
3701 let focus_handle = editor.focus_handle(cx);
3702 let snapshot = editor
3703 .buffer()
3704 .read(cx)
3705 .as_singleton()?
3706 .read(cx)
3707 .text_snapshot();
3708 let start_offset = range.start.to_offset(&snapshot);
3709 let parent_message = self
3710 .context
3711 .read(cx)
3712 .messages_for_offsets([start_offset], cx);
3713 debug_assert_eq!(parent_message.len(), 1);
3714 let parent_message = parent_message.first()?;
3715
3716 let step_index = self
3717 .workflow_steps
3718 .keys()
3719 .filter(|workflow_step_range| {
3720 workflow_step_range
3721 .start
3722 .cmp(&parent_message.anchor_range.start, &snapshot)
3723 .is_ge()
3724 && workflow_step_range.end.cmp(&range.end, &snapshot).is_le()
3725 })
3726 .count();
3727
3728 let step_label = Label::new(format!("Step {step_index}")).size(LabelSize::Small);
3729
3730 let step_label = if is_confirmed {
3731 h_flex()
3732 .items_center()
3733 .gap_2()
3734 .child(step_label.strikethrough(true).color(Color::Muted))
3735 .child(
3736 Icon::new(IconName::Check)
3737 .size(IconSize::Small)
3738 .color(Color::Created),
3739 )
3740 } else {
3741 div().child(step_label)
3742 };
3743
3744 Some(
3745 v_flex()
3746 .w(max_width)
3747 .pl(gutter_width)
3748 .child(
3749 h_flex()
3750 .w_full()
3751 .h_8()
3752 .border_b_1()
3753 .border_color(border_color)
3754 .items_center()
3755 .justify_between()
3756 .gap_2()
3757 .child(h_flex().justify_start().gap_2().child(step_label))
3758 .child(h_flex().w_full().justify_end().child(
3759 Self::render_workflow_step_status(
3760 status,
3761 range.clone(),
3762 focus_handle.clone(),
3763 this.clone(),
3764 id,
3765 ),
3766 )),
3767 )
3768 // todo!("do we wanna keep this?")
3769 // .children(edit_paths.iter().map(|path| {
3770 // h_flex()
3771 // .gap_1()
3772 // .child(Icon::new(IconName::File))
3773 // .child(Label::new(path.clone()))
3774 // }))
3775 .into_any(),
3776 )
3777 }
3778
3779 fn render_workflow_step_footer(
3780 &self,
3781 step_range: Range<text::Anchor>,
3782 max_width: Pixels,
3783 gutter_width: Pixels,
3784 cx: &mut ViewContext<Self>,
3785 ) -> Option<AnyElement> {
3786 let step = self.workflow_steps.get(&step_range)?;
3787 let current_status = step.status(cx);
3788 let theme = cx.theme().status();
3789 let border_color = if current_status.is_confirmed() {
3790 theme.ignored_border
3791 } else {
3792 theme.info_border
3793 };
3794 Some(
3795 v_flex()
3796 .w(max_width)
3797 .pt_1()
3798 .pl(gutter_width)
3799 .child(h_flex().h(px(1.)).bg(border_color))
3800 .into_any(),
3801 )
3802 }
3803
3804 fn render_workflow_step_status(
3805 status: WorkflowStepStatus,
3806 step_range: Range<language::Anchor>,
3807 focus_handle: FocusHandle,
3808 editor: WeakView<ContextEditor>,
3809 id: BlockId,
3810 ) -> AnyElement {
3811 let id = EntityId::from(id).as_u64();
3812 fn display_keybind_in_tooltip(
3813 step_range: &Range<language::Anchor>,
3814 editor: &WeakView<ContextEditor>,
3815 cx: &mut WindowContext<'_>,
3816 ) -> bool {
3817 editor
3818 .update(cx, |this, _| {
3819 this.active_workflow_step
3820 .as_ref()
3821 .map(|step| &step.range == step_range)
3822 })
3823 .ok()
3824 .flatten()
3825 .unwrap_or_default()
3826 }
3827
3828 match status {
3829 WorkflowStepStatus::Error(error) => {
3830 let error = error.to_string();
3831 h_flex()
3832 .gap_2()
3833 .child(
3834 div()
3835 .id("step-resolution-failure")
3836 .child(
3837 Label::new("Step Resolution Failed")
3838 .size(LabelSize::Small)
3839 .color(Color::Error),
3840 )
3841 .tooltip(move |cx| Tooltip::text(error.clone(), cx)),
3842 )
3843 .child(
3844 Button::new(("transform", id), "Retry")
3845 .icon(IconName::Update)
3846 .icon_position(IconPosition::Start)
3847 .icon_size(IconSize::Small)
3848 .label_size(LabelSize::Small)
3849 .on_click({
3850 let editor = editor.clone();
3851 let step_range = step_range.clone();
3852 move |_, cx| {
3853 editor
3854 .update(cx, |this, cx| {
3855 this.resolve_workflow_step(step_range.clone(), cx)
3856 })
3857 .ok();
3858 }
3859 }),
3860 )
3861 .into_any()
3862 }
3863 WorkflowStepStatus::Idle | WorkflowStepStatus::Resolving { .. } => {
3864 Button::new(("transform", id), "Transform")
3865 .icon(IconName::SparkleAlt)
3866 .icon_position(IconPosition::Start)
3867 .icon_size(IconSize::Small)
3868 .label_size(LabelSize::Small)
3869 .style(ButtonStyle::Tinted(TintColor::Accent))
3870 .tooltip({
3871 let step_range = step_range.clone();
3872 let editor = editor.clone();
3873 move |cx| {
3874 cx.new_view(|cx| {
3875 let tooltip = Tooltip::new("Transform");
3876 if display_keybind_in_tooltip(&step_range, &editor, cx) {
3877 tooltip.key_binding(KeyBinding::for_action_in(
3878 &Assist,
3879 &focus_handle,
3880 cx,
3881 ))
3882 } else {
3883 tooltip
3884 }
3885 })
3886 .into()
3887 }
3888 })
3889 .on_click({
3890 let editor = editor.clone();
3891 let step_range = step_range.clone();
3892 let is_idle = matches!(status, WorkflowStepStatus::Idle);
3893 move |_, cx| {
3894 if is_idle {
3895 editor
3896 .update(cx, |this, cx| {
3897 this.apply_workflow_step(step_range.clone(), cx)
3898 })
3899 .ok();
3900 }
3901 }
3902 })
3903 .map(|this| {
3904 if let WorkflowStepStatus::Resolving = &status {
3905 this.with_animation(
3906 ("resolving-suggestion-animation", id),
3907 Animation::new(Duration::from_secs(2))
3908 .repeat()
3909 .with_easing(pulsating_between(0.4, 0.8)),
3910 |label, delta| label.alpha(delta),
3911 )
3912 .into_any_element()
3913 } else {
3914 this.into_any_element()
3915 }
3916 })
3917 }
3918 WorkflowStepStatus::Pending => h_flex()
3919 .items_center()
3920 .gap_2()
3921 .child(
3922 Label::new("Applying...")
3923 .size(LabelSize::Small)
3924 .with_animation(
3925 ("applying-step-transformation-label", id),
3926 Animation::new(Duration::from_secs(2))
3927 .repeat()
3928 .with_easing(pulsating_between(0.4, 0.8)),
3929 |label, delta| label.alpha(delta),
3930 ),
3931 )
3932 .child(
3933 IconButton::new(("stop-transformation", id), IconName::Stop)
3934 .icon_size(IconSize::Small)
3935 .icon_color(Color::Error)
3936 .style(ButtonStyle::Subtle)
3937 .tooltip({
3938 let step_range = step_range.clone();
3939 let editor = editor.clone();
3940 move |cx| {
3941 cx.new_view(|cx| {
3942 let tooltip = Tooltip::new("Stop Transformation");
3943 if display_keybind_in_tooltip(&step_range, &editor, cx) {
3944 tooltip.key_binding(KeyBinding::for_action_in(
3945 &editor::actions::Cancel,
3946 &focus_handle,
3947 cx,
3948 ))
3949 } else {
3950 tooltip
3951 }
3952 })
3953 .into()
3954 }
3955 })
3956 .on_click({
3957 let editor = editor.clone();
3958 let step_range = step_range.clone();
3959 move |_, cx| {
3960 editor
3961 .update(cx, |this, cx| {
3962 this.stop_workflow_step(step_range.clone(), cx)
3963 })
3964 .ok();
3965 }
3966 }),
3967 )
3968 .into_any_element(),
3969 WorkflowStepStatus::Done => h_flex()
3970 .gap_1()
3971 .child(
3972 IconButton::new(("stop-transformation", id), IconName::Close)
3973 .icon_size(IconSize::Small)
3974 .style(ButtonStyle::Tinted(TintColor::Negative))
3975 .tooltip({
3976 let focus_handle = focus_handle.clone();
3977 let editor = editor.clone();
3978 let step_range = step_range.clone();
3979 move |cx| {
3980 cx.new_view(|cx| {
3981 let tooltip = Tooltip::new("Reject Transformation");
3982 if display_keybind_in_tooltip(&step_range, &editor, cx) {
3983 tooltip.key_binding(KeyBinding::for_action_in(
3984 &editor::actions::Cancel,
3985 &focus_handle,
3986 cx,
3987 ))
3988 } else {
3989 tooltip
3990 }
3991 })
3992 .into()
3993 }
3994 })
3995 .on_click({
3996 let editor = editor.clone();
3997 let step_range = step_range.clone();
3998 move |_, cx| {
3999 editor
4000 .update(cx, |this, cx| {
4001 this.reject_workflow_step(step_range.clone(), cx);
4002 })
4003 .ok();
4004 }
4005 }),
4006 )
4007 .child(
4008 Button::new(("confirm-workflow-step", id), "Accept")
4009 .icon(IconName::Check)
4010 .icon_position(IconPosition::Start)
4011 .icon_size(IconSize::Small)
4012 .label_size(LabelSize::Small)
4013 .style(ButtonStyle::Tinted(TintColor::Positive))
4014 .tooltip({
4015 let editor = editor.clone();
4016 let step_range = step_range.clone();
4017 move |cx| {
4018 cx.new_view(|cx| {
4019 let tooltip = Tooltip::new("Accept Transformation");
4020 if display_keybind_in_tooltip(&step_range, &editor, cx) {
4021 tooltip.key_binding(KeyBinding::for_action_in(
4022 &Assist,
4023 &focus_handle,
4024 cx,
4025 ))
4026 } else {
4027 tooltip
4028 }
4029 })
4030 .into()
4031 }
4032 })
4033 .on_click({
4034 let editor = editor.clone();
4035 let step_range = step_range.clone();
4036 move |_, cx| {
4037 editor
4038 .update(cx, |this, cx| {
4039 this.confirm_workflow_step(step_range.clone(), cx);
4040 })
4041 .ok();
4042 }
4043 }),
4044 )
4045 .into_any_element(),
4046 WorkflowStepStatus::Confirmed => h_flex()
4047 .child(
4048 Button::new(("revert-workflow-step", id), "Undo")
4049 .style(ButtonStyle::Filled)
4050 .icon(Some(IconName::Undo))
4051 .icon_position(IconPosition::Start)
4052 .icon_size(IconSize::Small)
4053 .label_size(LabelSize::Small)
4054 .on_click({
4055 let editor = editor.clone();
4056 let step_range = step_range.clone();
4057 move |_, cx| {
4058 editor
4059 .update(cx, |this, cx| {
4060 this.undo_workflow_step(step_range.clone(), cx);
4061 })
4062 .ok();
4063 }
4064 }),
4065 )
4066 .into_any_element(),
4067 }
4068 }
4069
4070 fn render_notice(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
4071 use feature_flags::FeatureFlagAppExt;
4072 let nudge = self.assistant_panel.upgrade().map(|assistant_panel| {
4073 assistant_panel.read(cx).show_zed_ai_notice && cx.has_flag::<feature_flags::ZedPro>()
4074 });
4075
4076 if nudge.map_or(false, |value| value) {
4077 Some(
4078 h_flex()
4079 .p_3()
4080 .border_b_1()
4081 .border_color(cx.theme().colors().border_variant)
4082 .bg(cx.theme().colors().editor_background)
4083 .justify_between()
4084 .child(
4085 h_flex()
4086 .gap_3()
4087 .child(Icon::new(IconName::ZedAssistant).color(Color::Accent))
4088 .child(Label::new("Zed AI is here! Get started by signing in →")),
4089 )
4090 .child(
4091 Button::new("sign-in", "Sign in")
4092 .size(ButtonSize::Compact)
4093 .style(ButtonStyle::Filled)
4094 .on_click(cx.listener(|this, _event, cx| {
4095 let client = this
4096 .workspace
4097 .update(cx, |workspace, _| workspace.client().clone())
4098 .log_err();
4099
4100 if let Some(client) = client {
4101 cx.spawn(|this, mut cx| async move {
4102 client.authenticate_and_connect(true, &mut cx).await?;
4103 this.update(&mut cx, |_, cx| cx.notify())
4104 })
4105 .detach_and_log_err(cx)
4106 }
4107 })),
4108 )
4109 .into_any_element(),
4110 )
4111 } else if let Some(configuration_error) = configuration_error(cx) {
4112 let label = match configuration_error {
4113 ConfigurationError::NoProvider => "No LLM provider selected.",
4114 ConfigurationError::ProviderNotAuthenticated => "LLM provider is not configured.",
4115 };
4116 Some(
4117 h_flex()
4118 .px_3()
4119 .py_2()
4120 .border_b_1()
4121 .border_color(cx.theme().colors().border_variant)
4122 .bg(cx.theme().colors().editor_background)
4123 .justify_between()
4124 .child(
4125 h_flex()
4126 .gap_3()
4127 .child(
4128 Icon::new(IconName::Warning)
4129 .size(IconSize::Small)
4130 .color(Color::Warning),
4131 )
4132 .child(Label::new(label)),
4133 )
4134 .child(
4135 Button::new("open-configuration", "Configure Providers")
4136 .size(ButtonSize::Compact)
4137 .icon(Some(IconName::SlidersVertical))
4138 .icon_size(IconSize::Small)
4139 .icon_position(IconPosition::Start)
4140 .style(ButtonStyle::Filled)
4141 .on_click({
4142 let focus_handle = self.focus_handle(cx).clone();
4143 move |_event, cx| {
4144 focus_handle.dispatch_action(&ShowConfiguration, cx);
4145 }
4146 }),
4147 )
4148 .into_any_element(),
4149 )
4150 } else {
4151 None
4152 }
4153 }
4154
4155 fn render_send_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4156 let focus_handle = self.focus_handle(cx).clone();
4157 let button_text = match self.active_workflow_step() {
4158 Some((_, step)) => match step.status(cx) {
4159 WorkflowStepStatus::Error(_) => "Retry Step Resolution",
4160 WorkflowStepStatus::Resolving => "Transform",
4161 WorkflowStepStatus::Idle => "Transform",
4162 WorkflowStepStatus::Pending => "Applying...",
4163 WorkflowStepStatus::Done => "Accept",
4164 WorkflowStepStatus::Confirmed => "Send",
4165 },
4166 None => "Send",
4167 };
4168
4169 let (style, tooltip) = match token_state(&self.context, cx) {
4170 Some(TokenState::NoTokensLeft { .. }) => (
4171 ButtonStyle::Tinted(TintColor::Negative),
4172 Some(Tooltip::text("Token limit reached", cx)),
4173 ),
4174 Some(TokenState::HasMoreTokens {
4175 over_warn_threshold,
4176 ..
4177 }) => {
4178 let (style, tooltip) = if over_warn_threshold {
4179 (
4180 ButtonStyle::Tinted(TintColor::Warning),
4181 Some(Tooltip::text("Token limit is close to exhaustion", cx)),
4182 )
4183 } else {
4184 (ButtonStyle::Filled, None)
4185 };
4186 (style, tooltip)
4187 }
4188 None => (ButtonStyle::Filled, None),
4189 };
4190
4191 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4192
4193 let has_configuration_error = configuration_error(cx).is_some();
4194 let needs_to_accept_terms = self.show_accept_terms
4195 && provider
4196 .as_ref()
4197 .map_or(false, |provider| provider.must_accept_terms(cx));
4198 let disabled = has_configuration_error || needs_to_accept_terms;
4199
4200 ButtonLike::new("send_button")
4201 .disabled(disabled)
4202 .style(style)
4203 .when_some(tooltip, |button, tooltip| {
4204 button.tooltip(move |_| tooltip.clone())
4205 })
4206 .layer(ElevationIndex::ModalSurface)
4207 .child(Label::new(button_text))
4208 .children(
4209 KeyBinding::for_action_in(&Assist, &focus_handle, cx)
4210 .map(|binding| binding.into_any_element()),
4211 )
4212 .on_click(move |_event, cx| {
4213 focus_handle.dispatch_action(&Assist, cx);
4214 })
4215 }
4216}
4217
4218fn render_fold_icon_button(
4219 editor: WeakView<Editor>,
4220 icon: IconName,
4221 label: SharedString,
4222) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut WindowContext) -> AnyElement> {
4223 Arc::new(move |fold_id, fold_range, _cx| {
4224 let editor = editor.clone();
4225 ButtonLike::new(fold_id)
4226 .style(ButtonStyle::Filled)
4227 .layer(ElevationIndex::ElevatedSurface)
4228 .child(Icon::new(icon))
4229 .child(Label::new(label.clone()).single_line())
4230 .on_click(move |_, cx| {
4231 editor
4232 .update(cx, |editor, cx| {
4233 let buffer_start = fold_range
4234 .start
4235 .to_point(&editor.buffer().read(cx).read(cx));
4236 let buffer_row = MultiBufferRow(buffer_start.row);
4237 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
4238 })
4239 .ok();
4240 })
4241 .into_any_element()
4242 })
4243}
4244
4245#[derive(Debug, Clone, Serialize, Deserialize)]
4246struct CopyMetadata {
4247 creases: Vec<SelectedCreaseMetadata>,
4248}
4249
4250#[derive(Debug, Clone, Serialize, Deserialize)]
4251struct SelectedCreaseMetadata {
4252 range_relative_to_selection: Range<usize>,
4253 crease: CreaseMetadata,
4254}
4255
4256impl EventEmitter<EditorEvent> for ContextEditor {}
4257impl EventEmitter<SearchEvent> for ContextEditor {}
4258
4259impl Render for ContextEditor {
4260 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4261 let provider = LanguageModelRegistry::read_global(cx).active_provider();
4262 let accept_terms = if self.show_accept_terms {
4263 provider
4264 .as_ref()
4265 .and_then(|provider| provider.render_accept_terms(cx))
4266 } else {
4267 None
4268 };
4269 let focus_handle = self
4270 .workspace
4271 .update(cx, |workspace, cx| {
4272 Some(workspace.active_item_as::<Editor>(cx)?.focus_handle(cx))
4273 })
4274 .ok()
4275 .flatten();
4276 v_flex()
4277 .key_context("ContextEditor")
4278 .capture_action(cx.listener(ContextEditor::cancel))
4279 .capture_action(cx.listener(ContextEditor::save))
4280 .capture_action(cx.listener(ContextEditor::copy))
4281 .capture_action(cx.listener(ContextEditor::cut))
4282 .capture_action(cx.listener(ContextEditor::paste))
4283 .capture_action(cx.listener(ContextEditor::cycle_message_role))
4284 .capture_action(cx.listener(ContextEditor::confirm_command))
4285 .on_action(cx.listener(ContextEditor::assist))
4286 .on_action(cx.listener(ContextEditor::split))
4287 .size_full()
4288 .children(self.render_notice(cx))
4289 .child(
4290 div()
4291 .flex_grow()
4292 .bg(cx.theme().colors().editor_background)
4293 .child(self.editor.clone()),
4294 )
4295 .when_some(accept_terms, |this, element| {
4296 this.child(
4297 div()
4298 .absolute()
4299 .right_3()
4300 .bottom_12()
4301 .max_w_96()
4302 .py_2()
4303 .px_3()
4304 .elevation_2(cx)
4305 .bg(cx.theme().colors().surface_background)
4306 .occlude()
4307 .child(element),
4308 )
4309 })
4310 .when_some(self.error_message.clone(), |this, error_message| {
4311 this.child(
4312 div()
4313 .absolute()
4314 .right_3()
4315 .bottom_12()
4316 .max_w_96()
4317 .py_2()
4318 .px_3()
4319 .elevation_2(cx)
4320 .occlude()
4321 .child(
4322 v_flex()
4323 .gap_0p5()
4324 .child(
4325 h_flex()
4326 .gap_1p5()
4327 .items_center()
4328 .child(Icon::new(IconName::XCircle).color(Color::Error))
4329 .child(
4330 Label::new("Error interacting with language model")
4331 .weight(FontWeight::MEDIUM),
4332 ),
4333 )
4334 .child(
4335 div()
4336 .id("error-message")
4337 .max_h_24()
4338 .overflow_y_scroll()
4339 .child(Label::new(error_message)),
4340 )
4341 .child(h_flex().justify_end().mt_1().child(
4342 Button::new("dismiss", "Dismiss").on_click(cx.listener(
4343 |this, _, cx| {
4344 this.error_message = None;
4345 cx.notify();
4346 },
4347 )),
4348 )),
4349 ),
4350 )
4351 })
4352 .child(
4353 h_flex().w_full().relative().child(
4354 h_flex()
4355 .p_2()
4356 .w_full()
4357 .border_t_1()
4358 .border_color(cx.theme().colors().border_variant)
4359 .bg(cx.theme().colors().editor_background)
4360 .child(
4361 h_flex()
4362 .gap_2()
4363 .child(render_inject_context_menu(cx.view().downgrade(), cx))
4364 .child(
4365 IconButton::new("quote-button", IconName::Quote)
4366 .icon_size(IconSize::Small)
4367 .on_click(|_, cx| {
4368 cx.dispatch_action(QuoteSelection.boxed_clone());
4369 })
4370 .tooltip(move |cx| {
4371 cx.new_view(|cx| {
4372 Tooltip::new("Insert Selection").key_binding(
4373 focus_handle.as_ref().and_then(|handle| {
4374 KeyBinding::for_action_in(
4375 &QuoteSelection,
4376 &handle,
4377 cx,
4378 )
4379 }),
4380 )
4381 })
4382 .into()
4383 }),
4384 ),
4385 )
4386 .child(
4387 h_flex()
4388 .w_full()
4389 .justify_end()
4390 .child(div().child(self.render_send_button(cx))),
4391 ),
4392 ),
4393 )
4394 }
4395}
4396
4397impl FocusableView for ContextEditor {
4398 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
4399 self.editor.focus_handle(cx)
4400 }
4401}
4402
4403impl Item for ContextEditor {
4404 type Event = editor::EditorEvent;
4405
4406 fn tab_content_text(&self, cx: &WindowContext) -> Option<SharedString> {
4407 Some(util::truncate_and_trailoff(&self.title(cx), MAX_TAB_TITLE_LEN).into())
4408 }
4409
4410 fn to_item_events(event: &Self::Event, mut f: impl FnMut(item::ItemEvent)) {
4411 match event {
4412 EditorEvent::Edited { .. } => {
4413 f(item::ItemEvent::Edit);
4414 }
4415 EditorEvent::TitleChanged => {
4416 f(item::ItemEvent::UpdateTab);
4417 }
4418 _ => {}
4419 }
4420 }
4421
4422 fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
4423 Some(self.title(cx).to_string().into())
4424 }
4425
4426 fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
4427 Some(Box::new(handle.clone()))
4428 }
4429
4430 fn set_nav_history(&mut self, nav_history: pane::ItemNavHistory, cx: &mut ViewContext<Self>) {
4431 self.editor.update(cx, |editor, cx| {
4432 Item::set_nav_history(editor, nav_history, cx)
4433 })
4434 }
4435
4436 fn navigate(&mut self, data: Box<dyn std::any::Any>, cx: &mut ViewContext<Self>) -> bool {
4437 self.editor
4438 .update(cx, |editor, cx| Item::navigate(editor, data, cx))
4439 }
4440
4441 fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
4442 self.editor.update(cx, Item::deactivated)
4443 }
4444}
4445
4446impl SearchableItem for ContextEditor {
4447 type Match = <Editor as SearchableItem>::Match;
4448
4449 fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
4450 self.editor.update(cx, |editor, cx| {
4451 editor.clear_matches(cx);
4452 });
4453 }
4454
4455 fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4456 self.editor
4457 .update(cx, |editor, cx| editor.update_matches(matches, cx));
4458 }
4459
4460 fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
4461 self.editor
4462 .update(cx, |editor, cx| editor.query_suggestion(cx))
4463 }
4464
4465 fn activate_match(
4466 &mut self,
4467 index: usize,
4468 matches: &[Self::Match],
4469 cx: &mut ViewContext<Self>,
4470 ) {
4471 self.editor.update(cx, |editor, cx| {
4472 editor.activate_match(index, matches, cx);
4473 });
4474 }
4475
4476 fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
4477 self.editor
4478 .update(cx, |editor, cx| editor.select_matches(matches, cx));
4479 }
4480
4481 fn replace(
4482 &mut self,
4483 identifier: &Self::Match,
4484 query: &project::search::SearchQuery,
4485 cx: &mut ViewContext<Self>,
4486 ) {
4487 self.editor
4488 .update(cx, |editor, cx| editor.replace(identifier, query, cx));
4489 }
4490
4491 fn find_matches(
4492 &mut self,
4493 query: Arc<project::search::SearchQuery>,
4494 cx: &mut ViewContext<Self>,
4495 ) -> Task<Vec<Self::Match>> {
4496 self.editor
4497 .update(cx, |editor, cx| editor.find_matches(query, cx))
4498 }
4499
4500 fn active_match_index(
4501 &mut self,
4502 matches: &[Self::Match],
4503 cx: &mut ViewContext<Self>,
4504 ) -> Option<usize> {
4505 self.editor
4506 .update(cx, |editor, cx| editor.active_match_index(matches, cx))
4507 }
4508}
4509
4510impl FollowableItem for ContextEditor {
4511 fn remote_id(&self) -> Option<workspace::ViewId> {
4512 self.remote_id
4513 }
4514
4515 fn to_state_proto(&self, cx: &WindowContext) -> Option<proto::view::Variant> {
4516 let context = self.context.read(cx);
4517 Some(proto::view::Variant::ContextEditor(
4518 proto::view::ContextEditor {
4519 context_id: context.id().to_proto(),
4520 editor: if let Some(proto::view::Variant::Editor(proto)) =
4521 self.editor.read(cx).to_state_proto(cx)
4522 {
4523 Some(proto)
4524 } else {
4525 None
4526 },
4527 },
4528 ))
4529 }
4530
4531 fn from_state_proto(
4532 workspace: View<Workspace>,
4533 id: workspace::ViewId,
4534 state: &mut Option<proto::view::Variant>,
4535 cx: &mut WindowContext,
4536 ) -> Option<Task<Result<View<Self>>>> {
4537 let proto::view::Variant::ContextEditor(_) = state.as_ref()? else {
4538 return None;
4539 };
4540 let Some(proto::view::Variant::ContextEditor(state)) = state.take() else {
4541 unreachable!()
4542 };
4543
4544 let context_id = ContextId::from_proto(state.context_id);
4545 let editor_state = state.editor?;
4546
4547 let (project, panel) = workspace.update(cx, |workspace, cx| {
4548 Some((
4549 workspace.project().clone(),
4550 workspace.panel::<AssistantPanel>(cx)?,
4551 ))
4552 })?;
4553
4554 let context_editor =
4555 panel.update(cx, |panel, cx| panel.open_remote_context(context_id, cx));
4556
4557 Some(cx.spawn(|mut cx| async move {
4558 let context_editor = context_editor.await?;
4559 context_editor
4560 .update(&mut cx, |context_editor, cx| {
4561 context_editor.remote_id = Some(id);
4562 context_editor.editor.update(cx, |editor, cx| {
4563 editor.apply_update_proto(
4564 &project,
4565 proto::update_view::Variant::Editor(proto::update_view::Editor {
4566 selections: editor_state.selections,
4567 pending_selection: editor_state.pending_selection,
4568 scroll_top_anchor: editor_state.scroll_top_anchor,
4569 scroll_x: editor_state.scroll_y,
4570 scroll_y: editor_state.scroll_y,
4571 ..Default::default()
4572 }),
4573 cx,
4574 )
4575 })
4576 })?
4577 .await?;
4578 Ok(context_editor)
4579 }))
4580 }
4581
4582 fn to_follow_event(event: &Self::Event) -> Option<item::FollowEvent> {
4583 Editor::to_follow_event(event)
4584 }
4585
4586 fn add_event_to_update_proto(
4587 &self,
4588 event: &Self::Event,
4589 update: &mut Option<proto::update_view::Variant>,
4590 cx: &WindowContext,
4591 ) -> bool {
4592 self.editor
4593 .read(cx)
4594 .add_event_to_update_proto(event, update, cx)
4595 }
4596
4597 fn apply_update_proto(
4598 &mut self,
4599 project: &Model<Project>,
4600 message: proto::update_view::Variant,
4601 cx: &mut ViewContext<Self>,
4602 ) -> Task<Result<()>> {
4603 self.editor.update(cx, |editor, cx| {
4604 editor.apply_update_proto(project, message, cx)
4605 })
4606 }
4607
4608 fn is_project_item(&self, _cx: &WindowContext) -> bool {
4609 true
4610 }
4611
4612 fn set_leader_peer_id(
4613 &mut self,
4614 leader_peer_id: Option<proto::PeerId>,
4615 cx: &mut ViewContext<Self>,
4616 ) {
4617 self.editor.update(cx, |editor, cx| {
4618 editor.set_leader_peer_id(leader_peer_id, cx)
4619 })
4620 }
4621
4622 fn dedup(&self, existing: &Self, cx: &WindowContext) -> Option<item::Dedup> {
4623 if existing.context.read(cx).id() == self.context.read(cx).id() {
4624 Some(item::Dedup::KeepExisting)
4625 } else {
4626 None
4627 }
4628 }
4629}
4630
4631pub struct ContextEditorToolbarItem {
4632 fs: Arc<dyn Fs>,
4633 workspace: WeakView<Workspace>,
4634 active_context_editor: Option<WeakView<ContextEditor>>,
4635 model_summary_editor: View<Editor>,
4636 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4637}
4638
4639fn active_editor_focus_handle(
4640 workspace: &WeakView<Workspace>,
4641 cx: &WindowContext<'_>,
4642) -> Option<FocusHandle> {
4643 workspace.upgrade().and_then(|workspace| {
4644 Some(
4645 workspace
4646 .read(cx)
4647 .active_item_as::<Editor>(cx)?
4648 .focus_handle(cx),
4649 )
4650 })
4651}
4652
4653fn render_inject_context_menu(
4654 active_context_editor: WeakView<ContextEditor>,
4655 cx: &mut WindowContext<'_>,
4656) -> impl IntoElement {
4657 let commands = SlashCommandRegistry::global(cx);
4658
4659 slash_command_picker::SlashCommandSelector::new(
4660 commands.clone(),
4661 active_context_editor,
4662 IconButton::new("trigger", IconName::SlashSquare)
4663 .icon_size(IconSize::Small)
4664 .tooltip(|cx| {
4665 Tooltip::with_meta("Insert Context", None, "Type / to insert via keyboard", cx)
4666 }),
4667 )
4668}
4669
4670impl ContextEditorToolbarItem {
4671 pub fn new(
4672 workspace: &Workspace,
4673 model_selector_menu_handle: PopoverMenuHandle<Picker<ModelPickerDelegate>>,
4674 model_summary_editor: View<Editor>,
4675 ) -> Self {
4676 Self {
4677 fs: workspace.app_state().fs.clone(),
4678 workspace: workspace.weak_handle(),
4679 active_context_editor: None,
4680 model_summary_editor,
4681 model_selector_menu_handle,
4682 }
4683 }
4684
4685 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
4686 let context = &self
4687 .active_context_editor
4688 .as_ref()?
4689 .upgrade()?
4690 .read(cx)
4691 .context;
4692 let (token_count_color, token_count, max_token_count) = match token_state(context, cx)? {
4693 TokenState::NoTokensLeft {
4694 max_token_count,
4695 token_count,
4696 } => (Color::Error, token_count, max_token_count),
4697 TokenState::HasMoreTokens {
4698 max_token_count,
4699 token_count,
4700 over_warn_threshold,
4701 } => {
4702 let color = if over_warn_threshold {
4703 Color::Warning
4704 } else {
4705 Color::Muted
4706 };
4707 (color, token_count, max_token_count)
4708 }
4709 };
4710 Some(
4711 h_flex()
4712 .gap_0p5()
4713 .child(
4714 Label::new(humanize_token_count(token_count))
4715 .size(LabelSize::Small)
4716 .color(token_count_color),
4717 )
4718 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
4719 .child(
4720 Label::new(humanize_token_count(max_token_count))
4721 .size(LabelSize::Small)
4722 .color(Color::Muted),
4723 ),
4724 )
4725 }
4726}
4727
4728impl Render for ContextEditorToolbarItem {
4729 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
4730 let left_side = h_flex()
4731 .pl_1()
4732 .gap_2()
4733 .flex_1()
4734 .min_w(rems(DEFAULT_TAB_TITLE.len() as f32))
4735 .when(self.active_context_editor.is_some(), |left_side| {
4736 left_side.child(self.model_summary_editor.clone())
4737 });
4738 let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
4739 let active_model = LanguageModelRegistry::read_global(cx).active_model();
4740 let weak_self = cx.view().downgrade();
4741 let right_side = h_flex()
4742 .gap_2()
4743 // TODO display this in a nicer way, once we have a design for it.
4744 // .children({
4745 // let project = self
4746 // .workspace
4747 // .upgrade()
4748 // .map(|workspace| workspace.read(cx).project().downgrade());
4749 //
4750 // let scan_items_remaining = cx.update_global(|db: &mut SemanticDb, cx| {
4751 // project.and_then(|project| db.remaining_summaries(&project, cx))
4752 // });
4753
4754 // scan_items_remaining
4755 // .map(|remaining_items| format!("Files to scan: {}", remaining_items))
4756 // })
4757 .child(
4758 ModelSelector::new(
4759 self.fs.clone(),
4760 ButtonLike::new("active-model")
4761 .style(ButtonStyle::Subtle)
4762 .child(
4763 h_flex()
4764 .w_full()
4765 .gap_0p5()
4766 .child(
4767 div()
4768 .overflow_x_hidden()
4769 .flex_grow()
4770 .whitespace_nowrap()
4771 .child(match (active_provider, active_model) {
4772 (Some(provider), Some(model)) => h_flex()
4773 .gap_1()
4774 .child(
4775 Icon::new(model.icon().unwrap_or_else(|| provider.icon()))
4776 .color(Color::Muted)
4777 .size(IconSize::XSmall),
4778 )
4779 .child(
4780 Label::new(model.name().0)
4781 .size(LabelSize::Small)
4782 .color(Color::Muted),
4783 )
4784 .into_any_element(),
4785 _ => Label::new("No model selected")
4786 .size(LabelSize::Small)
4787 .color(Color::Muted)
4788 .into_any_element(),
4789 }),
4790 )
4791 .child(
4792 Icon::new(IconName::ChevronDown)
4793 .color(Color::Muted)
4794 .size(IconSize::XSmall),
4795 ),
4796 )
4797 .tooltip(move |cx| {
4798 Tooltip::for_action("Change Model", &ToggleModelSelector, cx)
4799 }),
4800 )
4801 .with_handle(self.model_selector_menu_handle.clone()),
4802 )
4803 .children(self.render_remaining_tokens(cx))
4804 .child(
4805 PopoverMenu::new("context-editor-popover")
4806 .trigger(
4807 IconButton::new("context-editor-trigger", IconName::EllipsisVertical)
4808 .icon_size(IconSize::Small)
4809 .tooltip(|cx| Tooltip::text("Open Context Options", cx)),
4810 )
4811 .menu({
4812 let weak_self = weak_self.clone();
4813 move |cx| {
4814 let weak_self = weak_self.clone();
4815 Some(ContextMenu::build(cx, move |menu, cx| {
4816 let context = weak_self
4817 .update(cx, |this, cx| {
4818 active_editor_focus_handle(&this.workspace, cx)
4819 })
4820 .ok()
4821 .flatten();
4822 menu.when_some(context, |menu, context| menu.context(context))
4823 .entry("Regenerate Context Title", None, {
4824 let weak_self = weak_self.clone();
4825 move |cx| {
4826 weak_self
4827 .update(cx, |_, cx| {
4828 cx.emit(ContextEditorToolbarItemEvent::RegenerateSummary)
4829 })
4830 .ok();
4831 }
4832 })
4833 .custom_entry(
4834 |_| {
4835 h_flex()
4836 .w_full()
4837 .justify_between()
4838 .gap_2()
4839 .child(Label::new("Insert Context"))
4840 .child(Label::new("/ command").color(Color::Muted))
4841 .into_any()
4842 },
4843 {
4844 let weak_self = weak_self.clone();
4845 move |cx| {
4846 weak_self
4847 .update(cx, |this, cx| {
4848 if let Some(editor) =
4849 &this.active_context_editor
4850 {
4851 editor
4852 .update(cx, |this, cx| {
4853 this.slash_menu_handle
4854 .toggle(cx);
4855 })
4856 .ok();
4857 }
4858 })
4859 .ok();
4860 }
4861 },
4862 )
4863 .action("Insert Selection", QuoteSelection.boxed_clone())
4864 }))
4865 }
4866 }),
4867 );
4868
4869 h_flex()
4870 .size_full()
4871 .gap_2()
4872 .justify_between()
4873 .child(left_side)
4874 .child(right_side)
4875 }
4876}
4877
4878impl ToolbarItemView for ContextEditorToolbarItem {
4879 fn set_active_pane_item(
4880 &mut self,
4881 active_pane_item: Option<&dyn ItemHandle>,
4882 cx: &mut ViewContext<Self>,
4883 ) -> ToolbarItemLocation {
4884 self.active_context_editor = active_pane_item
4885 .and_then(|item| item.act_as::<ContextEditor>(cx))
4886 .map(|editor| editor.downgrade());
4887 cx.notify();
4888 if self.active_context_editor.is_none() {
4889 ToolbarItemLocation::Hidden
4890 } else {
4891 ToolbarItemLocation::PrimaryRight
4892 }
4893 }
4894
4895 fn pane_focus_update(&mut self, _pane_focused: bool, cx: &mut ViewContext<Self>) {
4896 cx.notify();
4897 }
4898}
4899
4900impl EventEmitter<ToolbarItemEvent> for ContextEditorToolbarItem {}
4901
4902enum ContextEditorToolbarItemEvent {
4903 RegenerateSummary,
4904}
4905impl EventEmitter<ContextEditorToolbarItemEvent> for ContextEditorToolbarItem {}
4906
4907pub struct ContextHistory {
4908 picker: View<Picker<SavedContextPickerDelegate>>,
4909 _subscriptions: Vec<Subscription>,
4910 assistant_panel: WeakView<AssistantPanel>,
4911}
4912
4913impl ContextHistory {
4914 fn new(
4915 project: Model<Project>,
4916 context_store: Model<ContextStore>,
4917 assistant_panel: WeakView<AssistantPanel>,
4918 cx: &mut ViewContext<Self>,
4919 ) -> Self {
4920 let picker = cx.new_view(|cx| {
4921 Picker::uniform_list(
4922 SavedContextPickerDelegate::new(project, context_store.clone()),
4923 cx,
4924 )
4925 .modal(false)
4926 .max_height(None)
4927 });
4928
4929 let _subscriptions = vec![
4930 cx.observe(&context_store, |this, _, cx| {
4931 this.picker.update(cx, |picker, cx| picker.refresh(cx));
4932 }),
4933 cx.subscribe(&picker, Self::handle_picker_event),
4934 ];
4935
4936 Self {
4937 picker,
4938 _subscriptions,
4939 assistant_panel,
4940 }
4941 }
4942
4943 fn handle_picker_event(
4944 &mut self,
4945 _: View<Picker<SavedContextPickerDelegate>>,
4946 event: &SavedContextPickerEvent,
4947 cx: &mut ViewContext<Self>,
4948 ) {
4949 let SavedContextPickerEvent::Confirmed(context) = event;
4950 self.assistant_panel
4951 .update(cx, |assistant_panel, cx| match context {
4952 ContextMetadata::Remote(metadata) => {
4953 assistant_panel
4954 .open_remote_context(metadata.id.clone(), cx)
4955 .detach_and_log_err(cx);
4956 }
4957 ContextMetadata::Saved(metadata) => {
4958 assistant_panel
4959 .open_saved_context(metadata.path.clone(), cx)
4960 .detach_and_log_err(cx);
4961 }
4962 })
4963 .ok();
4964 }
4965}
4966
4967#[derive(Debug, PartialEq, Eq, Clone, Copy)]
4968pub enum WorkflowAssistStatus {
4969 Pending,
4970 Confirmed,
4971 Done,
4972 Idle,
4973}
4974
4975impl WorkflowAssist {
4976 pub fn status(&self, cx: &AppContext) -> WorkflowAssistStatus {
4977 let assistant = InlineAssistant::global(cx);
4978 if self
4979 .assist_ids
4980 .iter()
4981 .any(|assist_id| assistant.assist_status(*assist_id, cx).is_pending())
4982 {
4983 WorkflowAssistStatus::Pending
4984 } else if self
4985 .assist_ids
4986 .iter()
4987 .all(|assist_id| assistant.assist_status(*assist_id, cx).is_confirmed())
4988 {
4989 WorkflowAssistStatus::Confirmed
4990 } else if self
4991 .assist_ids
4992 .iter()
4993 .all(|assist_id| assistant.assist_status(*assist_id, cx).is_done())
4994 {
4995 WorkflowAssistStatus::Done
4996 } else {
4997 WorkflowAssistStatus::Idle
4998 }
4999 }
5000}
5001
5002impl Render for ContextHistory {
5003 fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
5004 div().size_full().child(self.picker.clone())
5005 }
5006}
5007
5008impl FocusableView for ContextHistory {
5009 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
5010 self.picker.focus_handle(cx)
5011 }
5012}
5013
5014impl EventEmitter<()> for ContextHistory {}
5015
5016impl Item for ContextHistory {
5017 type Event = ();
5018
5019 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
5020 Some("History".into())
5021 }
5022}
5023
5024pub struct ConfigurationView {
5025 focus_handle: FocusHandle,
5026 configuration_views: HashMap<LanguageModelProviderId, AnyView>,
5027 _registry_subscription: Subscription,
5028}
5029
5030impl ConfigurationView {
5031 fn new(cx: &mut ViewContext<Self>) -> Self {
5032 let focus_handle = cx.focus_handle();
5033
5034 let registry_subscription = cx.subscribe(
5035 &LanguageModelRegistry::global(cx),
5036 |this, _, event: &language_model::Event, cx| match event {
5037 language_model::Event::AddedProvider(provider_id) => {
5038 let provider = LanguageModelRegistry::read_global(cx).provider(provider_id);
5039 if let Some(provider) = provider {
5040 this.add_configuration_view(&provider, cx);
5041 }
5042 }
5043 language_model::Event::RemovedProvider(provider_id) => {
5044 this.remove_configuration_view(provider_id);
5045 }
5046 _ => {}
5047 },
5048 );
5049
5050 let mut this = Self {
5051 focus_handle,
5052 configuration_views: HashMap::default(),
5053 _registry_subscription: registry_subscription,
5054 };
5055 this.build_configuration_views(cx);
5056 this
5057 }
5058
5059 fn build_configuration_views(&mut self, cx: &mut ViewContext<Self>) {
5060 let providers = LanguageModelRegistry::read_global(cx).providers();
5061 for provider in providers {
5062 self.add_configuration_view(&provider, cx);
5063 }
5064 }
5065
5066 fn remove_configuration_view(&mut self, provider_id: &LanguageModelProviderId) {
5067 self.configuration_views.remove(provider_id);
5068 }
5069
5070 fn add_configuration_view(
5071 &mut self,
5072 provider: &Arc<dyn LanguageModelProvider>,
5073 cx: &mut ViewContext<Self>,
5074 ) {
5075 let configuration_view = provider.configuration_view(cx);
5076 self.configuration_views
5077 .insert(provider.id(), configuration_view);
5078 }
5079
5080 fn render_provider_view(
5081 &mut self,
5082 provider: &Arc<dyn LanguageModelProvider>,
5083 cx: &mut ViewContext<Self>,
5084 ) -> Div {
5085 let provider_id = provider.id().0.clone();
5086 let provider_name = provider.name().0.clone();
5087 let configuration_view = self.configuration_views.get(&provider.id()).cloned();
5088
5089 let open_new_context = cx.listener({
5090 let provider = provider.clone();
5091 move |_, _, cx| {
5092 cx.emit(ConfigurationViewEvent::NewProviderContextEditor(
5093 provider.clone(),
5094 ))
5095 }
5096 });
5097
5098 v_flex()
5099 .gap_2()
5100 .child(
5101 h_flex()
5102 .justify_between()
5103 .child(Headline::new(provider_name.clone()).size(HeadlineSize::Small))
5104 .when(provider.is_authenticated(cx), move |this| {
5105 this.child(
5106 h_flex().justify_end().child(
5107 Button::new(
5108 SharedString::from(format!("new-context-{provider_id}")),
5109 "Open new context",
5110 )
5111 .icon_position(IconPosition::Start)
5112 .icon(IconName::Plus)
5113 .style(ButtonStyle::Filled)
5114 .layer(ElevationIndex::ModalSurface)
5115 .on_click(open_new_context),
5116 ),
5117 )
5118 }),
5119 )
5120 .child(
5121 div()
5122 .p(Spacing::Large.rems(cx))
5123 .bg(cx.theme().colors().surface_background)
5124 .border_1()
5125 .border_color(cx.theme().colors().border_variant)
5126 .rounded_md()
5127 .when(configuration_view.is_none(), |this| {
5128 this.child(div().child(Label::new(format!(
5129 "No configuration view for {}",
5130 provider_name
5131 ))))
5132 })
5133 .when_some(configuration_view, |this, configuration_view| {
5134 this.child(configuration_view)
5135 }),
5136 )
5137 }
5138}
5139
5140impl Render for ConfigurationView {
5141 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
5142 let providers = LanguageModelRegistry::read_global(cx).providers();
5143 let provider_views = providers
5144 .into_iter()
5145 .map(|provider| self.render_provider_view(&provider, cx))
5146 .collect::<Vec<_>>();
5147
5148 let mut element = v_flex()
5149 .id("assistant-configuration-view")
5150 .track_focus(&self.focus_handle)
5151 .bg(cx.theme().colors().editor_background)
5152 .size_full()
5153 .overflow_y_scroll()
5154 .child(
5155 v_flex()
5156 .p(Spacing::XXLarge.rems(cx))
5157 .border_b_1()
5158 .border_color(cx.theme().colors().border)
5159 .gap_1()
5160 .child(Headline::new("Configure your Assistant").size(HeadlineSize::Medium))
5161 .child(
5162 Label::new(
5163 "At least one LLM provider must be configured to use the Assistant.",
5164 )
5165 .color(Color::Muted),
5166 ),
5167 )
5168 .child(
5169 v_flex()
5170 .p(Spacing::XXLarge.rems(cx))
5171 .mt_1()
5172 .gap_6()
5173 .flex_1()
5174 .children(provider_views),
5175 )
5176 .into_any();
5177
5178 // We use a canvas here to get scrolling to work in the ConfigurationView. It's a workaround
5179 // because we couldn't the element to take up the size of the parent.
5180 canvas(
5181 move |bounds, cx| {
5182 element.prepaint_as_root(bounds.origin, bounds.size.into(), cx);
5183 element
5184 },
5185 |_, mut element, cx| {
5186 element.paint(cx);
5187 },
5188 )
5189 .flex_1()
5190 .w_full()
5191 }
5192}
5193
5194pub enum ConfigurationViewEvent {
5195 NewProviderContextEditor(Arc<dyn LanguageModelProvider>),
5196}
5197
5198impl EventEmitter<ConfigurationViewEvent> for ConfigurationView {}
5199
5200impl FocusableView for ConfigurationView {
5201 fn focus_handle(&self, _: &AppContext) -> FocusHandle {
5202 self.focus_handle.clone()
5203 }
5204}
5205
5206impl Item for ConfigurationView {
5207 type Event = ConfigurationViewEvent;
5208
5209 fn tab_content_text(&self, _cx: &WindowContext) -> Option<SharedString> {
5210 Some("Configuration".into())
5211 }
5212}
5213
5214type ToggleFold = Arc<dyn Fn(bool, &mut WindowContext) + Send + Sync>;
5215
5216fn render_slash_command_output_toggle(
5217 row: MultiBufferRow,
5218 is_folded: bool,
5219 fold: ToggleFold,
5220 _cx: &mut WindowContext,
5221) -> AnyElement {
5222 Disclosure::new(
5223 ("slash-command-output-fold-indicator", row.0 as u64),
5224 !is_folded,
5225 )
5226 .selected(is_folded)
5227 .on_click(move |_e, cx| fold(!is_folded, cx))
5228 .into_any_element()
5229}
5230
5231fn fold_toggle(
5232 name: &'static str,
5233) -> impl Fn(
5234 MultiBufferRow,
5235 bool,
5236 Arc<dyn Fn(bool, &mut WindowContext<'_>) + Send + Sync>,
5237 &mut WindowContext<'_>,
5238) -> AnyElement {
5239 move |row, is_folded, fold, _cx| {
5240 Disclosure::new((name, row.0 as u64), !is_folded)
5241 .selected(is_folded)
5242 .on_click(move |_e, cx| fold(!is_folded, cx))
5243 .into_any_element()
5244 }
5245}
5246
5247fn quote_selection_fold_placeholder(title: String, editor: WeakView<Editor>) -> FoldPlaceholder {
5248 FoldPlaceholder {
5249 render: Arc::new({
5250 move |fold_id, fold_range, _cx| {
5251 let editor = editor.clone();
5252 ButtonLike::new(fold_id)
5253 .style(ButtonStyle::Filled)
5254 .layer(ElevationIndex::ElevatedSurface)
5255 .child(Icon::new(IconName::TextSnippet))
5256 .child(Label::new(title.clone()).single_line())
5257 .on_click(move |_, cx| {
5258 editor
5259 .update(cx, |editor, cx| {
5260 let buffer_start = fold_range
5261 .start
5262 .to_point(&editor.buffer().read(cx).read(cx));
5263 let buffer_row = MultiBufferRow(buffer_start.row);
5264 editor.unfold_at(&UnfoldAt { buffer_row }, cx);
5265 })
5266 .ok();
5267 })
5268 .into_any_element()
5269 }
5270 }),
5271 constrain_width: false,
5272 merge_adjacent: false,
5273 }
5274}
5275
5276fn render_quote_selection_output_toggle(
5277 row: MultiBufferRow,
5278 is_folded: bool,
5279 fold: ToggleFold,
5280 _cx: &mut WindowContext,
5281) -> AnyElement {
5282 Disclosure::new(("quote-selection-indicator", row.0 as u64), !is_folded)
5283 .selected(is_folded)
5284 .on_click(move |_e, cx| fold(!is_folded, cx))
5285 .into_any_element()
5286}
5287
5288fn render_pending_slash_command_gutter_decoration(
5289 row: MultiBufferRow,
5290 status: &PendingSlashCommandStatus,
5291 confirm_command: Arc<dyn Fn(&mut WindowContext)>,
5292) -> AnyElement {
5293 let mut icon = IconButton::new(
5294 ("slash-command-gutter-decoration", row.0),
5295 ui::IconName::TriangleRight,
5296 )
5297 .on_click(move |_e, cx| confirm_command(cx))
5298 .icon_size(ui::IconSize::Small)
5299 .size(ui::ButtonSize::None);
5300
5301 match status {
5302 PendingSlashCommandStatus::Idle => {
5303 icon = icon.icon_color(Color::Muted);
5304 }
5305 PendingSlashCommandStatus::Running { .. } => {
5306 icon = icon.selected(true);
5307 }
5308 PendingSlashCommandStatus::Error(_) => icon = icon.icon_color(Color::Error),
5309 }
5310
5311 icon.into_any_element()
5312}
5313
5314fn render_docs_slash_command_trailer(
5315 row: MultiBufferRow,
5316 command: PendingSlashCommand,
5317 cx: &mut WindowContext,
5318) -> AnyElement {
5319 if command.arguments.is_empty() {
5320 return Empty.into_any();
5321 }
5322 let args = DocsSlashCommandArgs::parse(&command.arguments);
5323
5324 let Some(store) = args
5325 .provider()
5326 .and_then(|provider| IndexedDocsStore::try_global(provider, cx).ok())
5327 else {
5328 return Empty.into_any();
5329 };
5330
5331 let Some(package) = args.package() else {
5332 return Empty.into_any();
5333 };
5334
5335 let mut children = Vec::new();
5336
5337 if store.is_indexing(&package) {
5338 children.push(
5339 div()
5340 .id(("crates-being-indexed", row.0))
5341 .child(Icon::new(IconName::ArrowCircle).with_animation(
5342 "arrow-circle",
5343 Animation::new(Duration::from_secs(4)).repeat(),
5344 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
5345 ))
5346 .tooltip({
5347 let package = package.clone();
5348 move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
5349 })
5350 .into_any_element(),
5351 );
5352 }
5353
5354 if let Some(latest_error) = store.latest_error_for_package(&package) {
5355 children.push(
5356 div()
5357 .id(("latest-error", row.0))
5358 .child(
5359 Icon::new(IconName::Warning)
5360 .size(IconSize::Small)
5361 .color(Color::Warning),
5362 )
5363 .tooltip(move |cx| Tooltip::text(format!("Failed to index: {latest_error}"), cx))
5364 .into_any_element(),
5365 )
5366 }
5367
5368 let is_indexing = store.is_indexing(&package);
5369 let latest_error = store.latest_error_for_package(&package);
5370
5371 if !is_indexing && latest_error.is_none() {
5372 return Empty.into_any();
5373 }
5374
5375 h_flex().gap_2().children(children).into_any_element()
5376}
5377
5378fn make_lsp_adapter_delegate(
5379 project: &Model<Project>,
5380 cx: &mut AppContext,
5381) -> Result<Arc<dyn LspAdapterDelegate>> {
5382 project.update(cx, |project, cx| {
5383 // TODO: Find the right worktree.
5384 let worktree = project
5385 .worktrees(cx)
5386 .next()
5387 .ok_or_else(|| anyhow!("no worktrees when constructing LocalLspAdapterDelegate"))?;
5388 let http_client = project.client().http_client().clone();
5389 project.lsp_store().update(cx, |lsp_store, cx| {
5390 Ok(LocalLspAdapterDelegate::new(
5391 lsp_store,
5392 &worktree,
5393 http_client,
5394 project.fs().clone(),
5395 cx,
5396 ) as Arc<dyn LspAdapterDelegate>)
5397 })
5398 })
5399}
5400
5401fn slash_command_error_block_renderer(message: String) -> RenderBlock {
5402 Box::new(move |_| {
5403 div()
5404 .pl_6()
5405 .child(
5406 Label::new(format!("error: {}", message))
5407 .single_line()
5408 .color(Color::Error),
5409 )
5410 .into_any()
5411 })
5412}
5413
5414enum TokenState {
5415 NoTokensLeft {
5416 max_token_count: usize,
5417 token_count: usize,
5418 },
5419 HasMoreTokens {
5420 max_token_count: usize,
5421 token_count: usize,
5422 over_warn_threshold: bool,
5423 },
5424}
5425
5426fn token_state(context: &Model<Context>, cx: &AppContext) -> Option<TokenState> {
5427 const WARNING_TOKEN_THRESHOLD: f32 = 0.8;
5428
5429 let model = LanguageModelRegistry::read_global(cx).active_model()?;
5430 let token_count = context.read(cx).token_count()?;
5431 let max_token_count = model.max_token_count();
5432
5433 let remaining_tokens = max_token_count as isize - token_count as isize;
5434 let token_state = if remaining_tokens <= 0 {
5435 TokenState::NoTokensLeft {
5436 max_token_count,
5437 token_count,
5438 }
5439 } else {
5440 let over_warn_threshold =
5441 token_count as f32 / max_token_count as f32 >= WARNING_TOKEN_THRESHOLD;
5442 TokenState::HasMoreTokens {
5443 max_token_count,
5444 token_count,
5445 over_warn_threshold,
5446 }
5447 };
5448 Some(token_state)
5449}
5450
5451fn size_for_image(data: &RenderImage, max_size: Size<Pixels>) -> Size<Pixels> {
5452 let image_size = data
5453 .size(0)
5454 .map(|dimension| Pixels::from(u32::from(dimension)));
5455 let image_ratio = image_size.width / image_size.height;
5456 let bounds_ratio = max_size.width / max_size.height;
5457
5458 if image_size.width > max_size.width || image_size.height > max_size.height {
5459 if bounds_ratio > image_ratio {
5460 size(
5461 image_size.width * (max_size.height / image_size.height),
5462 max_size.height,
5463 )
5464 } else {
5465 size(
5466 max_size.width,
5467 image_size.height * (max_size.width / image_size.width),
5468 )
5469 }
5470 } else {
5471 size(image_size.width, image_size.height)
5472 }
5473}
5474
5475enum ConfigurationError {
5476 NoProvider,
5477 ProviderNotAuthenticated,
5478}
5479
5480fn configuration_error(cx: &AppContext) -> Option<ConfigurationError> {
5481 let provider = LanguageModelRegistry::read_global(cx).active_provider();
5482 let is_authenticated = provider
5483 .as_ref()
5484 .map_or(false, |provider| provider.is_authenticated(cx));
5485
5486 if provider.is_some() && is_authenticated {
5487 return None;
5488 }
5489
5490 if provider.is_none() {
5491 return Some(ConfigurationError::NoProvider);
5492 }
5493
5494 if !is_authenticated {
5495 return Some(ConfigurationError::ProviderNotAuthenticated);
5496 }
5497
5498 None
5499}