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