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