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