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