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