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