assistant_panel.rs

   1use crate::assistant_configuration::{ConfigurationView, ConfigurationViewEvent};
   2use crate::{
   3    terminal_inline_assistant::TerminalInlineAssistant, DeployHistory, InlineAssistant, NewContext,
   4};
   5use anyhow::{anyhow, Result};
   6use assistant_context_editor::{
   7    make_lsp_adapter_delegate, AssistantContext, AssistantPanelDelegate, ContextEditor,
   8    ContextEditorToolbarItem, ContextEditorToolbarItemEvent, ContextHistory, ContextId,
   9    ContextStore, ContextStoreEvent, InsertDraggedFiles, SlashCommandCompletionProvider,
  10    ToggleModelSelector, DEFAULT_TAB_TITLE,
  11};
  12use assistant_settings::{AssistantDockPosition, AssistantSettings};
  13use assistant_slash_command::SlashCommandWorkingSet;
  14use client::{proto, Client, Status};
  15use editor::{Editor, EditorEvent};
  16use fs::Fs;
  17use gpui::{
  18    prelude::*, Action, App, AsyncWindowContext, Entity, EventEmitter, ExternalPaths, FocusHandle,
  19    Focusable, InteractiveElement, IntoElement, ParentElement, Pixels, Render, Styled,
  20    Subscription, Task, UpdateGlobal, WeakEntity,
  21};
  22use language::LanguageRegistry;
  23use language_model::{LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID};
  24use language_model_selector::LanguageModelSelector;
  25use project::Project;
  26use prompt_library::{open_prompt_library, PromptBuilder, PromptLibrary};
  27use search::{buffer_search::DivRegistrar, BufferSearchBar};
  28use settings::{update_settings_file, Settings};
  29use smol::stream::StreamExt;
  30use std::{ops::ControlFlow, path::PathBuf, sync::Arc};
  31use terminal_view::{terminal_panel::TerminalPanel, TerminalView};
  32use ui::{prelude::*, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip};
  33use util::{maybe, ResultExt};
  34use workspace::DraggedTab;
  35use workspace::{
  36    dock::{DockPosition, Panel, PanelEvent},
  37    pane, DraggedSelection, Pane, ShowConfiguration, ToggleZoom, Workspace,
  38};
  39use zed_actions::assistant::{DeployPromptLibrary, InlineAssist, ToggleFocus};
  40
  41pub fn init(cx: &mut App) {
  42    workspace::FollowableViewRegistry::register::<ContextEditor>(cx);
  43    cx.observe_new(
  44        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
  45            workspace
  46                .register_action(ContextEditor::quote_selection)
  47                .register_action(ContextEditor::insert_selection)
  48                .register_action(ContextEditor::copy_code)
  49                .register_action(ContextEditor::insert_dragged_files)
  50                .register_action(AssistantPanel::show_configuration)
  51                .register_action(AssistantPanel::create_new_context)
  52                .register_action(AssistantPanel::restart_context_servers);
  53        },
  54    )
  55    .detach();
  56
  57    cx.observe_new(
  58        |terminal_panel: &mut TerminalPanel, _, cx: &mut Context<TerminalPanel>| {
  59            let settings = AssistantSettings::get_global(cx);
  60            terminal_panel.set_assistant_enabled(settings.enabled, cx);
  61        },
  62    )
  63    .detach();
  64}
  65
  66pub enum AssistantPanelEvent {
  67    ContextEdited,
  68}
  69
  70pub struct AssistantPanel {
  71    pane: Entity<Pane>,
  72    workspace: WeakEntity<Workspace>,
  73    width: Option<Pixels>,
  74    height: Option<Pixels>,
  75    project: Entity<Project>,
  76    context_store: Entity<ContextStore>,
  77    languages: Arc<LanguageRegistry>,
  78    fs: Arc<dyn Fs>,
  79    subscriptions: Vec<Subscription>,
  80    model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
  81    model_summary_editor: Entity<Editor>,
  82    authenticate_provider_task: Option<(LanguageModelProviderId, Task<()>)>,
  83    configuration_subscription: Option<Subscription>,
  84    client_status: Option<client::Status>,
  85    watch_client_status: Option<Task<()>>,
  86    pub(crate) show_zed_ai_notice: bool,
  87}
  88
  89enum InlineAssistTarget {
  90    Editor(Entity<Editor>, bool),
  91    Terminal(Entity<TerminalView>),
  92}
  93
  94impl AssistantPanel {
  95    pub fn load(
  96        workspace: WeakEntity<Workspace>,
  97        prompt_builder: Arc<PromptBuilder>,
  98        cx: AsyncWindowContext,
  99    ) -> Task<Result<Entity<Self>>> {
 100        cx.spawn(|mut cx| async move {
 101            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 102            let context_store = workspace
 103                .update(&mut cx, |workspace, cx| {
 104                    let project = workspace.project().clone();
 105                    ContextStore::new(project, prompt_builder.clone(), slash_commands, cx)
 106                })?
 107                .await?;
 108
 109            workspace.update_in(&mut cx, |workspace, window, cx| {
 110                // TODO: deserialize state.
 111                cx.new(|cx| Self::new(workspace, context_store, window, cx))
 112            })
 113        })
 114    }
 115
 116    fn new(
 117        workspace: &Workspace,
 118        context_store: Entity<ContextStore>,
 119        window: &mut Window,
 120        cx: &mut Context<Self>,
 121    ) -> Self {
 122        let model_selector_menu_handle = PopoverMenuHandle::default();
 123        let model_summary_editor = cx.new(|cx| Editor::single_line(window, cx));
 124        let context_editor_toolbar = cx.new(|cx| {
 125            ContextEditorToolbarItem::new(
 126                workspace,
 127                model_selector_menu_handle.clone(),
 128                model_summary_editor.clone(),
 129                window,
 130                cx,
 131            )
 132        });
 133
 134        let pane = cx.new(|cx| {
 135            let mut pane = Pane::new(
 136                workspace.weak_handle(),
 137                workspace.project().clone(),
 138                Default::default(),
 139                None,
 140                NewContext.boxed_clone(),
 141                window,
 142                cx,
 143            );
 144
 145            let project = workspace.project().clone();
 146            pane.set_custom_drop_handle(cx, move |_, dropped_item, window, cx| {
 147                let action = maybe!({
 148                    if project.read(cx).is_local() {
 149                        if let Some(paths) = dropped_item.downcast_ref::<ExternalPaths>() {
 150                            return Some(InsertDraggedFiles::ExternalFiles(paths.paths().to_vec()));
 151                        }
 152                    }
 153
 154                    let project_paths = if let Some(tab) = dropped_item.downcast_ref::<DraggedTab>()
 155                    {
 156                        if tab.pane == cx.entity() {
 157                            return None;
 158                        }
 159                        let item = tab.pane.read(cx).item_for_index(tab.ix);
 160                        Some(
 161                            item.and_then(|item| item.project_path(cx))
 162                                .into_iter()
 163                                .collect::<Vec<_>>(),
 164                        )
 165                    } else if let Some(selection) = dropped_item.downcast_ref::<DraggedSelection>()
 166                    {
 167                        Some(
 168                            selection
 169                                .items()
 170                                .filter_map(|item| {
 171                                    project.read(cx).path_for_entry(item.entry_id, cx)
 172                                })
 173                                .collect::<Vec<_>>(),
 174                        )
 175                    } else {
 176                        None
 177                    }?;
 178
 179                    let paths = project_paths
 180                        .into_iter()
 181                        .filter_map(|project_path| {
 182                            let worktree = project
 183                                .read(cx)
 184                                .worktree_for_id(project_path.worktree_id, cx)?;
 185
 186                            let mut full_path = PathBuf::from(worktree.read(cx).root_name());
 187                            full_path.push(&project_path.path);
 188                            Some(full_path)
 189                        })
 190                        .collect::<Vec<_>>();
 191
 192                    Some(InsertDraggedFiles::ProjectPaths(paths))
 193                });
 194
 195                if let Some(action) = action {
 196                    window.dispatch_action(action.boxed_clone(), cx);
 197                }
 198
 199                ControlFlow::Break(())
 200            });
 201
 202            pane.set_can_navigate(true, cx);
 203            pane.display_nav_history_buttons(None);
 204            pane.set_should_display_tab_bar(|_, _| true);
 205            pane.set_render_tab_bar_buttons(cx, move |pane, _window, cx| {
 206                let focus_handle = pane.focus_handle(cx);
 207                let left_children = IconButton::new("history", IconName::HistoryRerun)
 208                    .icon_size(IconSize::Small)
 209                    .on_click(cx.listener({
 210                        let focus_handle = focus_handle.clone();
 211                        move |_, _, window, cx| {
 212                            focus_handle.focus(window);
 213                            window.dispatch_action(DeployHistory.boxed_clone(), cx)
 214                        }
 215                    }))
 216                    .tooltip({
 217                        let focus_handle = focus_handle.clone();
 218                        move |window, cx| {
 219                            Tooltip::for_action_in(
 220                                "Open History",
 221                                &DeployHistory,
 222                                &focus_handle,
 223                                window,
 224                                cx,
 225                            )
 226                        }
 227                    })
 228                    .toggle_state(
 229                        pane.active_item()
 230                            .map_or(false, |item| item.downcast::<ContextHistory>().is_some()),
 231                    );
 232                let _pane = cx.entity().clone();
 233                let right_children = h_flex()
 234                    .gap(DynamicSpacing::Base02.rems(cx))
 235                    .child(
 236                        IconButton::new("new-chat", IconName::Plus)
 237                            .icon_size(IconSize::Small)
 238                            .on_click(cx.listener(|_, _, window, cx| {
 239                                window.dispatch_action(NewContext.boxed_clone(), cx)
 240                            }))
 241                            .tooltip(move |window, cx| {
 242                                Tooltip::for_action_in(
 243                                    "New Chat",
 244                                    &NewContext,
 245                                    &focus_handle,
 246                                    window,
 247                                    cx,
 248                                )
 249                            }),
 250                    )
 251                    .child(
 252                        PopoverMenu::new("assistant-panel-popover-menu")
 253                            .trigger_with_tooltip(
 254                                IconButton::new("menu", IconName::EllipsisVertical)
 255                                    .icon_size(IconSize::Small),
 256                                Tooltip::text("Toggle Assistant Menu"),
 257                            )
 258                            .menu(move |window, cx| {
 259                                let zoom_label = if _pane.read(cx).is_zoomed() {
 260                                    "Zoom Out"
 261                                } else {
 262                                    "Zoom In"
 263                                };
 264                                let focus_handle = _pane.focus_handle(cx);
 265                                Some(ContextMenu::build(window, cx, move |menu, _, _| {
 266                                    menu.context(focus_handle.clone())
 267                                        .action("New Chat", Box::new(NewContext))
 268                                        .action("History", Box::new(DeployHistory))
 269                                        .action("Prompt Library", Box::new(DeployPromptLibrary))
 270                                        .action("Configure", Box::new(ShowConfiguration))
 271                                        .action(zoom_label, Box::new(ToggleZoom))
 272                                }))
 273                            }),
 274                    )
 275                    .into_any_element()
 276                    .into();
 277
 278                (Some(left_children.into_any_element()), right_children)
 279            });
 280            pane.toolbar().update(cx, |toolbar, cx| {
 281                toolbar.add_item(context_editor_toolbar.clone(), window, cx);
 282                toolbar.add_item(
 283                    cx.new(|cx| {
 284                        BufferSearchBar::new(
 285                            Some(workspace.project().read(cx).languages().clone()),
 286                            window,
 287                            cx,
 288                        )
 289                    }),
 290                    window,
 291                    cx,
 292                )
 293            });
 294            pane
 295        });
 296
 297        let subscriptions = vec![
 298            cx.observe(&pane, |_, _, cx| cx.notify()),
 299            cx.subscribe_in(&pane, window, Self::handle_pane_event),
 300            cx.subscribe(&context_editor_toolbar, Self::handle_toolbar_event),
 301            cx.subscribe(&model_summary_editor, Self::handle_summary_editor_event),
 302            cx.subscribe_in(&context_store, window, Self::handle_context_store_event),
 303            cx.subscribe_in(
 304                &LanguageModelRegistry::global(cx),
 305                window,
 306                |this, _, event: &language_model::Event, window, cx| match event {
 307                    language_model::Event::ActiveModelChanged => {
 308                        this.completion_provider_changed(window, cx);
 309                    }
 310                    language_model::Event::ProviderStateChanged => {
 311                        this.ensure_authenticated(window, cx);
 312                        cx.notify()
 313                    }
 314                    language_model::Event::AddedProvider(_)
 315                    | language_model::Event::RemovedProvider(_) => {
 316                        this.ensure_authenticated(window, cx);
 317                    }
 318                },
 319            ),
 320        ];
 321
 322        let watch_client_status = Self::watch_client_status(workspace.client().clone(), window, cx);
 323
 324        let mut this = Self {
 325            pane,
 326            workspace: workspace.weak_handle(),
 327            width: None,
 328            height: None,
 329            project: workspace.project().clone(),
 330            context_store,
 331            languages: workspace.app_state().languages.clone(),
 332            fs: workspace.app_state().fs.clone(),
 333            subscriptions,
 334            model_selector_menu_handle,
 335            model_summary_editor,
 336            authenticate_provider_task: None,
 337            configuration_subscription: None,
 338            client_status: None,
 339            watch_client_status: Some(watch_client_status),
 340            show_zed_ai_notice: false,
 341        };
 342        this.new_context(window, cx);
 343        this
 344    }
 345
 346    pub fn toggle_focus(
 347        workspace: &mut Workspace,
 348        _: &ToggleFocus,
 349        window: &mut Window,
 350        cx: &mut Context<Workspace>,
 351    ) {
 352        let settings = AssistantSettings::get_global(cx);
 353        if !settings.enabled {
 354            return;
 355        }
 356
 357        workspace.toggle_panel_focus::<Self>(window, cx);
 358    }
 359
 360    fn watch_client_status(
 361        client: Arc<Client>,
 362        window: &mut Window,
 363        cx: &mut Context<Self>,
 364    ) -> Task<()> {
 365        let mut status_rx = client.status();
 366
 367        cx.spawn_in(window, |this, mut cx| async move {
 368            while let Some(status) = status_rx.next().await {
 369                this.update(&mut cx, |this, cx| {
 370                    if this.client_status.is_none()
 371                        || this
 372                            .client_status
 373                            .map_or(false, |old_status| old_status != status)
 374                    {
 375                        this.update_zed_ai_notice_visibility(status, cx);
 376                    }
 377                    this.client_status = Some(status);
 378                })
 379                .log_err();
 380            }
 381            this.update(&mut cx, |this, _cx| this.watch_client_status = None)
 382                .log_err();
 383        })
 384    }
 385
 386    fn handle_pane_event(
 387        &mut self,
 388        pane: &Entity<Pane>,
 389        event: &pane::Event,
 390        window: &mut Window,
 391        cx: &mut Context<Self>,
 392    ) {
 393        let update_model_summary = match event {
 394            pane::Event::Remove { .. } => {
 395                cx.emit(PanelEvent::Close);
 396                false
 397            }
 398            pane::Event::ZoomIn => {
 399                cx.emit(PanelEvent::ZoomIn);
 400                false
 401            }
 402            pane::Event::ZoomOut => {
 403                cx.emit(PanelEvent::ZoomOut);
 404                false
 405            }
 406
 407            pane::Event::AddItem { item } => {
 408                self.workspace
 409                    .update(cx, |workspace, cx| {
 410                        item.added_to_pane(workspace, self.pane.clone(), window, cx)
 411                    })
 412                    .ok();
 413                true
 414            }
 415
 416            pane::Event::ActivateItem { local, .. } => {
 417                if *local {
 418                    self.workspace
 419                        .update(cx, |workspace, cx| {
 420                            workspace.unfollow_in_pane(&pane, window, cx);
 421                        })
 422                        .ok();
 423                }
 424                cx.emit(AssistantPanelEvent::ContextEdited);
 425                true
 426            }
 427            pane::Event::RemovedItem { .. } => {
 428                let has_configuration_view = self
 429                    .pane
 430                    .read(cx)
 431                    .items_of_type::<ConfigurationView>()
 432                    .next()
 433                    .is_some();
 434
 435                if !has_configuration_view {
 436                    self.configuration_subscription = None;
 437                }
 438
 439                cx.emit(AssistantPanelEvent::ContextEdited);
 440                true
 441            }
 442
 443            _ => false,
 444        };
 445
 446        if update_model_summary {
 447            if let Some(editor) = self.active_context_editor(cx) {
 448                self.show_updated_summary(&editor, window, cx)
 449            }
 450        }
 451    }
 452
 453    fn handle_summary_editor_event(
 454        &mut self,
 455        model_summary_editor: Entity<Editor>,
 456        event: &EditorEvent,
 457        cx: &mut Context<Self>,
 458    ) {
 459        if matches!(event, EditorEvent::Edited { .. }) {
 460            if let Some(context_editor) = self.active_context_editor(cx) {
 461                let new_summary = model_summary_editor.read(cx).text(cx);
 462                context_editor.update(cx, |context_editor, cx| {
 463                    context_editor.context().update(cx, |context, cx| {
 464                        if context.summary().is_none()
 465                            && (new_summary == DEFAULT_TAB_TITLE || new_summary.trim().is_empty())
 466                        {
 467                            return;
 468                        }
 469                        context.custom_summary(new_summary, cx)
 470                    });
 471                });
 472            }
 473        }
 474    }
 475
 476    fn update_zed_ai_notice_visibility(&mut self, client_status: Status, cx: &mut Context<Self>) {
 477        let active_provider = LanguageModelRegistry::read_global(cx).active_provider();
 478
 479        // If we're signed out and don't have a provider configured, or we're signed-out AND Zed.dev is
 480        // the provider, we want to show a nudge to sign in.
 481        let show_zed_ai_notice = client_status.is_signed_out()
 482            && active_provider.map_or(true, |provider| provider.id().0 == ZED_CLOUD_PROVIDER_ID);
 483
 484        self.show_zed_ai_notice = show_zed_ai_notice;
 485        cx.notify();
 486    }
 487
 488    fn handle_toolbar_event(
 489        &mut self,
 490        _: Entity<ContextEditorToolbarItem>,
 491        _: &ContextEditorToolbarItemEvent,
 492        cx: &mut Context<Self>,
 493    ) {
 494        if let Some(context_editor) = self.active_context_editor(cx) {
 495            context_editor.update(cx, |context_editor, cx| {
 496                context_editor.context().update(cx, |context, cx| {
 497                    context.summarize(true, cx);
 498                })
 499            })
 500        }
 501    }
 502
 503    fn handle_context_store_event(
 504        &mut self,
 505        _context_store: &Entity<ContextStore>,
 506        event: &ContextStoreEvent,
 507        window: &mut Window,
 508        cx: &mut Context<Self>,
 509    ) {
 510        let ContextStoreEvent::ContextCreated(context_id) = event;
 511        let Some(context) = self
 512            .context_store
 513            .read(cx)
 514            .loaded_context_for_id(&context_id, cx)
 515        else {
 516            log::error!("no context found with ID: {}", context_id.to_proto());
 517            return;
 518        };
 519        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 520            .log_err()
 521            .flatten();
 522
 523        let editor = cx.new(|cx| {
 524            let mut editor = ContextEditor::for_context(
 525                context,
 526                self.fs.clone(),
 527                self.workspace.clone(),
 528                self.project.clone(),
 529                lsp_adapter_delegate,
 530                window,
 531                cx,
 532            );
 533            editor.insert_default_prompt(window, cx);
 534            editor
 535        });
 536
 537        self.show_context(editor.clone(), window, cx);
 538    }
 539
 540    fn completion_provider_changed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 541        if let Some(editor) = self.active_context_editor(cx) {
 542            editor.update(cx, |active_context, cx| {
 543                active_context
 544                    .context()
 545                    .update(cx, |context, cx| context.completion_provider_changed(cx))
 546            })
 547        }
 548
 549        let Some(new_provider_id) = LanguageModelRegistry::read_global(cx)
 550            .active_provider()
 551            .map(|p| p.id())
 552        else {
 553            return;
 554        };
 555
 556        if self
 557            .authenticate_provider_task
 558            .as_ref()
 559            .map_or(true, |(old_provider_id, _)| {
 560                *old_provider_id != new_provider_id
 561            })
 562        {
 563            self.authenticate_provider_task = None;
 564            self.ensure_authenticated(window, cx);
 565        }
 566
 567        if let Some(status) = self.client_status {
 568            self.update_zed_ai_notice_visibility(status, cx);
 569        }
 570    }
 571
 572    fn ensure_authenticated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 573        if self.is_authenticated(cx) {
 574            return;
 575        }
 576
 577        let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
 578            return;
 579        };
 580
 581        let load_credentials = self.authenticate(cx);
 582
 583        if self.authenticate_provider_task.is_none() {
 584            self.authenticate_provider_task = Some((
 585                provider.id(),
 586                cx.spawn_in(window, |this, mut cx| async move {
 587                    if let Some(future) = load_credentials {
 588                        let _ = future.await;
 589                    }
 590                    this.update(&mut cx, |this, _cx| {
 591                        this.authenticate_provider_task = None;
 592                    })
 593                    .log_err();
 594                }),
 595            ));
 596        }
 597    }
 598
 599    pub fn inline_assist(
 600        workspace: &mut Workspace,
 601        action: &InlineAssist,
 602        window: &mut Window,
 603        cx: &mut Context<Workspace>,
 604    ) {
 605        let settings = AssistantSettings::get_global(cx);
 606        if !settings.enabled {
 607            return;
 608        }
 609
 610        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
 611            return;
 612        };
 613
 614        let Some(inline_assist_target) =
 615            Self::resolve_inline_assist_target(workspace, &assistant_panel, window, cx)
 616        else {
 617            return;
 618        };
 619
 620        let initial_prompt = action.prompt.clone();
 621
 622        if assistant_panel.update(cx, |assistant, cx| assistant.is_authenticated(cx)) {
 623            match inline_assist_target {
 624                InlineAssistTarget::Editor(active_editor, include_context) => {
 625                    InlineAssistant::update_global(cx, |assistant, cx| {
 626                        assistant.assist(
 627                            &active_editor,
 628                            Some(cx.entity().downgrade()),
 629                            include_context.then_some(&assistant_panel),
 630                            initial_prompt,
 631                            window,
 632                            cx,
 633                        )
 634                    })
 635                }
 636                InlineAssistTarget::Terminal(active_terminal) => {
 637                    TerminalInlineAssistant::update_global(cx, |assistant, cx| {
 638                        assistant.assist(
 639                            &active_terminal,
 640                            Some(cx.entity().downgrade()),
 641                            Some(&assistant_panel),
 642                            initial_prompt,
 643                            window,
 644                            cx,
 645                        )
 646                    })
 647                }
 648            }
 649        } else {
 650            let assistant_panel = assistant_panel.downgrade();
 651            cx.spawn_in(window, |workspace, mut cx| async move {
 652                let Some(task) =
 653                    assistant_panel.update(&mut cx, |assistant, cx| assistant.authenticate(cx))?
 654                else {
 655                    let answer = cx
 656                        .prompt(
 657                            gpui::PromptLevel::Warning,
 658                            "No language model provider configured",
 659                            None,
 660                            &["Configure", "Cancel"],
 661                        )
 662                        .await
 663                        .ok();
 664                    if let Some(answer) = answer {
 665                        if answer == 0 {
 666                            cx.update(|window, cx| {
 667                                window.dispatch_action(Box::new(ShowConfiguration), cx)
 668                            })
 669                            .ok();
 670                        }
 671                    }
 672                    return Ok(());
 673                };
 674                task.await?;
 675                if assistant_panel.update(&mut cx, |panel, cx| panel.is_authenticated(cx))? {
 676                    cx.update(|window, cx| match inline_assist_target {
 677                        InlineAssistTarget::Editor(active_editor, include_context) => {
 678                            let assistant_panel = if include_context {
 679                                assistant_panel.upgrade()
 680                            } else {
 681                                None
 682                            };
 683                            InlineAssistant::update_global(cx, |assistant, cx| {
 684                                assistant.assist(
 685                                    &active_editor,
 686                                    Some(workspace),
 687                                    assistant_panel.as_ref(),
 688                                    initial_prompt,
 689                                    window,
 690                                    cx,
 691                                )
 692                            })
 693                        }
 694                        InlineAssistTarget::Terminal(active_terminal) => {
 695                            TerminalInlineAssistant::update_global(cx, |assistant, cx| {
 696                                assistant.assist(
 697                                    &active_terminal,
 698                                    Some(workspace),
 699                                    assistant_panel.upgrade().as_ref(),
 700                                    initial_prompt,
 701                                    window,
 702                                    cx,
 703                                )
 704                            })
 705                        }
 706                    })?
 707                } else {
 708                    workspace.update_in(&mut cx, |workspace, window, cx| {
 709                        workspace.focus_panel::<AssistantPanel>(window, cx)
 710                    })?;
 711                }
 712
 713                anyhow::Ok(())
 714            })
 715            .detach_and_log_err(cx)
 716        }
 717    }
 718
 719    fn resolve_inline_assist_target(
 720        workspace: &mut Workspace,
 721        assistant_panel: &Entity<AssistantPanel>,
 722        window: &mut Window,
 723        cx: &mut App,
 724    ) -> Option<InlineAssistTarget> {
 725        if let Some(terminal_panel) = workspace.panel::<TerminalPanel>(cx) {
 726            if terminal_panel
 727                .read(cx)
 728                .focus_handle(cx)
 729                .contains_focused(window, cx)
 730            {
 731                if let Some(terminal_view) = terminal_panel.read(cx).pane().and_then(|pane| {
 732                    pane.read(cx)
 733                        .active_item()
 734                        .and_then(|t| t.downcast::<TerminalView>())
 735                }) {
 736                    return Some(InlineAssistTarget::Terminal(terminal_view));
 737                }
 738            }
 739        }
 740        let context_editor =
 741            assistant_panel
 742                .read(cx)
 743                .active_context_editor(cx)
 744                .and_then(|editor| {
 745                    let editor = &editor.read(cx).editor().clone();
 746                    if editor.read(cx).is_focused(window) {
 747                        Some(editor.clone())
 748                    } else {
 749                        None
 750                    }
 751                });
 752
 753        if let Some(context_editor) = context_editor {
 754            Some(InlineAssistTarget::Editor(context_editor, false))
 755        } else if let Some(workspace_editor) = workspace
 756            .active_item(cx)
 757            .and_then(|item| item.act_as::<Editor>(cx))
 758        {
 759            Some(InlineAssistTarget::Editor(workspace_editor, true))
 760        } else if let Some(terminal_view) = workspace
 761            .active_item(cx)
 762            .and_then(|item| item.act_as::<TerminalView>(cx))
 763        {
 764            Some(InlineAssistTarget::Terminal(terminal_view))
 765        } else {
 766            None
 767        }
 768    }
 769
 770    pub fn create_new_context(
 771        workspace: &mut Workspace,
 772        _: &NewContext,
 773        window: &mut Window,
 774        cx: &mut Context<Workspace>,
 775    ) {
 776        if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 777            let did_create_context = panel
 778                .update(cx, |panel, cx| {
 779                    panel.new_context(window, cx)?;
 780
 781                    Some(())
 782                })
 783                .is_some();
 784            if did_create_context {
 785                ContextEditor::quote_selection(workspace, &Default::default(), window, cx);
 786            }
 787        }
 788    }
 789
 790    pub fn new_context(
 791        &mut self,
 792        window: &mut Window,
 793        cx: &mut Context<Self>,
 794    ) -> Option<Entity<ContextEditor>> {
 795        let project = self.project.read(cx);
 796        if project.is_via_collab() {
 797            let task = self
 798                .context_store
 799                .update(cx, |store, cx| store.create_remote_context(cx));
 800
 801            cx.spawn_in(window, |this, mut cx| async move {
 802                let context = task.await?;
 803
 804                this.update_in(&mut cx, |this, window, cx| {
 805                    let workspace = this.workspace.clone();
 806                    let project = this.project.clone();
 807                    let lsp_adapter_delegate =
 808                        make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 809
 810                    let fs = this.fs.clone();
 811                    let project = this.project.clone();
 812
 813                    let editor = cx.new(|cx| {
 814                        ContextEditor::for_context(
 815                            context,
 816                            fs,
 817                            workspace,
 818                            project,
 819                            lsp_adapter_delegate,
 820                            window,
 821                            cx,
 822                        )
 823                    });
 824
 825                    this.show_context(editor, window, cx);
 826
 827                    anyhow::Ok(())
 828                })??;
 829
 830                anyhow::Ok(())
 831            })
 832            .detach_and_log_err(cx);
 833
 834            None
 835        } else {
 836            let context = self.context_store.update(cx, |store, cx| store.create(cx));
 837            let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 838                .log_err()
 839                .flatten();
 840
 841            let editor = cx.new(|cx| {
 842                let mut editor = ContextEditor::for_context(
 843                    context,
 844                    self.fs.clone(),
 845                    self.workspace.clone(),
 846                    self.project.clone(),
 847                    lsp_adapter_delegate,
 848                    window,
 849                    cx,
 850                );
 851                editor.insert_default_prompt(window, cx);
 852                editor
 853            });
 854
 855            self.show_context(editor.clone(), window, cx);
 856            let workspace = self.workspace.clone();
 857            cx.spawn_in(window, move |_, mut cx| async move {
 858                workspace
 859                    .update_in(&mut cx, |workspace, window, cx| {
 860                        workspace.focus_panel::<AssistantPanel>(window, cx);
 861                    })
 862                    .ok();
 863            })
 864            .detach();
 865            Some(editor)
 866        }
 867    }
 868
 869    fn show_context(
 870        &mut self,
 871        context_editor: Entity<ContextEditor>,
 872        window: &mut Window,
 873        cx: &mut Context<Self>,
 874    ) {
 875        let focus = self.focus_handle(cx).contains_focused(window, cx);
 876        let prev_len = self.pane.read(cx).items_len();
 877        self.pane.update(cx, |pane, cx| {
 878            pane.add_item(
 879                Box::new(context_editor.clone()),
 880                focus,
 881                focus,
 882                None,
 883                window,
 884                cx,
 885            )
 886        });
 887
 888        if prev_len != self.pane.read(cx).items_len() {
 889            self.subscriptions.push(cx.subscribe_in(
 890                &context_editor,
 891                window,
 892                Self::handle_context_editor_event,
 893            ));
 894        }
 895
 896        self.show_updated_summary(&context_editor, window, cx);
 897
 898        cx.emit(AssistantPanelEvent::ContextEdited);
 899        cx.notify();
 900    }
 901
 902    fn show_updated_summary(
 903        &self,
 904        context_editor: &Entity<ContextEditor>,
 905        window: &mut Window,
 906        cx: &mut Context<Self>,
 907    ) {
 908        context_editor.update(cx, |context_editor, cx| {
 909            let new_summary = context_editor.title(cx).to_string();
 910            self.model_summary_editor.update(cx, |summary_editor, cx| {
 911                if summary_editor.text(cx) != new_summary {
 912                    summary_editor.set_text(new_summary, window, cx);
 913                }
 914            });
 915        });
 916    }
 917
 918    fn handle_context_editor_event(
 919        &mut self,
 920        context_editor: &Entity<ContextEditor>,
 921        event: &EditorEvent,
 922        window: &mut Window,
 923        cx: &mut Context<Self>,
 924    ) {
 925        match event {
 926            EditorEvent::TitleChanged => {
 927                self.show_updated_summary(&context_editor, window, cx);
 928                cx.notify()
 929            }
 930            EditorEvent::Edited { .. } => {
 931                self.workspace
 932                    .update(cx, |workspace, cx| {
 933                        let is_via_ssh = workspace
 934                            .project()
 935                            .update(cx, |project, _| project.is_via_ssh());
 936
 937                        workspace
 938                            .client()
 939                            .telemetry()
 940                            .log_edit_event("assistant panel", is_via_ssh);
 941                    })
 942                    .log_err();
 943                cx.emit(AssistantPanelEvent::ContextEdited)
 944            }
 945            _ => {}
 946        }
 947    }
 948
 949    fn show_configuration(
 950        workspace: &mut Workspace,
 951        _: &ShowConfiguration,
 952        window: &mut Window,
 953        cx: &mut Context<Workspace>,
 954    ) {
 955        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
 956            return;
 957        };
 958
 959        if !panel.focus_handle(cx).contains_focused(window, cx) {
 960            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
 961        }
 962
 963        panel.update(cx, |this, cx| {
 964            this.show_configuration_tab(window, cx);
 965        })
 966    }
 967
 968    fn show_configuration_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 969        let configuration_item_ix = self
 970            .pane
 971            .read(cx)
 972            .items()
 973            .position(|item| item.downcast::<ConfigurationView>().is_some());
 974
 975        if let Some(configuration_item_ix) = configuration_item_ix {
 976            self.pane.update(cx, |pane, cx| {
 977                pane.activate_item(configuration_item_ix, true, true, window, cx);
 978            });
 979        } else {
 980            let configuration = cx.new(|cx| ConfigurationView::new(window, cx));
 981            self.configuration_subscription = Some(cx.subscribe_in(
 982                &configuration,
 983                window,
 984                |this, _, event: &ConfigurationViewEvent, window, cx| match event {
 985                    ConfigurationViewEvent::NewProviderContextEditor(provider) => {
 986                        if LanguageModelRegistry::read_global(cx)
 987                            .active_provider()
 988                            .map_or(true, |p| p.id() != provider.id())
 989                        {
 990                            if let Some(model) = provider.provided_models(cx).first().cloned() {
 991                                update_settings_file::<AssistantSettings>(
 992                                    this.fs.clone(),
 993                                    cx,
 994                                    move |settings, _| settings.set_model(model),
 995                                );
 996                            }
 997                        }
 998
 999                        this.new_context(window, cx);
1000                    }
1001                },
1002            ));
1003            self.pane.update(cx, |pane, cx| {
1004                pane.add_item(Box::new(configuration), true, true, None, window, cx);
1005            });
1006        }
1007    }
1008
1009    fn deploy_history(&mut self, _: &DeployHistory, window: &mut Window, cx: &mut Context<Self>) {
1010        let history_item_ix = self
1011            .pane
1012            .read(cx)
1013            .items()
1014            .position(|item| item.downcast::<ContextHistory>().is_some());
1015
1016        if let Some(history_item_ix) = history_item_ix {
1017            self.pane.update(cx, |pane, cx| {
1018                pane.activate_item(history_item_ix, true, true, window, cx);
1019            });
1020        } else {
1021            let history = cx.new(|cx| {
1022                ContextHistory::new(
1023                    self.project.clone(),
1024                    self.context_store.clone(),
1025                    self.workspace.clone(),
1026                    window,
1027                    cx,
1028                )
1029            });
1030            self.pane.update(cx, |pane, cx| {
1031                pane.add_item(Box::new(history), true, true, None, window, cx);
1032            });
1033        }
1034    }
1035
1036    fn deploy_prompt_library(
1037        &mut self,
1038        _: &DeployPromptLibrary,
1039        _window: &mut Window,
1040        cx: &mut Context<Self>,
1041    ) {
1042        open_prompt_library(
1043            self.languages.clone(),
1044            Box::new(PromptLibraryInlineAssist),
1045            Arc::new(|| {
1046                Box::new(SlashCommandCompletionProvider::new(
1047                    Arc::new(SlashCommandWorkingSet::default()),
1048                    None,
1049                    None,
1050                ))
1051            }),
1052            cx,
1053        )
1054        .detach_and_log_err(cx);
1055    }
1056
1057    fn toggle_model_selector(
1058        &mut self,
1059        _: &ToggleModelSelector,
1060        window: &mut Window,
1061        cx: &mut Context<Self>,
1062    ) {
1063        self.model_selector_menu_handle.toggle(window, cx);
1064    }
1065
1066    pub(crate) fn active_context_editor(&self, cx: &App) -> Option<Entity<ContextEditor>> {
1067        self.pane
1068            .read(cx)
1069            .active_item()?
1070            .downcast::<ContextEditor>()
1071    }
1072
1073    pub fn active_context(&self, cx: &App) -> Option<Entity<AssistantContext>> {
1074        Some(self.active_context_editor(cx)?.read(cx).context().clone())
1075    }
1076
1077    pub fn open_saved_context(
1078        &mut self,
1079        path: PathBuf,
1080        window: &mut Window,
1081        cx: &mut Context<Self>,
1082    ) -> Task<Result<()>> {
1083        let existing_context = self.pane.read(cx).items().find_map(|item| {
1084            item.downcast::<ContextEditor>()
1085                .filter(|editor| editor.read(cx).context().read(cx).path() == Some(&path))
1086        });
1087        if let Some(existing_context) = existing_context {
1088            return cx.spawn_in(window, |this, mut cx| async move {
1089                this.update_in(&mut cx, |this, window, cx| {
1090                    this.show_context(existing_context, window, cx)
1091                })
1092            });
1093        }
1094
1095        let context = self
1096            .context_store
1097            .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
1098        let fs = self.fs.clone();
1099        let project = self.project.clone();
1100        let workspace = self.workspace.clone();
1101
1102        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
1103
1104        cx.spawn_in(window, |this, mut cx| async move {
1105            let context = context.await?;
1106            this.update_in(&mut cx, |this, window, cx| {
1107                let editor = cx.new(|cx| {
1108                    ContextEditor::for_context(
1109                        context,
1110                        fs,
1111                        workspace,
1112                        project,
1113                        lsp_adapter_delegate,
1114                        window,
1115                        cx,
1116                    )
1117                });
1118                this.show_context(editor, window, cx);
1119                anyhow::Ok(())
1120            })??;
1121            Ok(())
1122        })
1123    }
1124
1125    pub fn open_remote_context(
1126        &mut self,
1127        id: ContextId,
1128        window: &mut Window,
1129        cx: &mut Context<Self>,
1130    ) -> Task<Result<Entity<ContextEditor>>> {
1131        let existing_context = self.pane.read(cx).items().find_map(|item| {
1132            item.downcast::<ContextEditor>()
1133                .filter(|editor| *editor.read(cx).context().read(cx).id() == id)
1134        });
1135        if let Some(existing_context) = existing_context {
1136            return cx.spawn_in(window, |this, mut cx| async move {
1137                this.update_in(&mut cx, |this, window, cx| {
1138                    this.show_context(existing_context.clone(), window, cx)
1139                })?;
1140                Ok(existing_context)
1141            });
1142        }
1143
1144        let context = self
1145            .context_store
1146            .update(cx, |store, cx| store.open_remote_context(id, cx));
1147        let fs = self.fs.clone();
1148        let workspace = self.workspace.clone();
1149        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1150            .log_err()
1151            .flatten();
1152
1153        cx.spawn_in(window, |this, mut cx| async move {
1154            let context = context.await?;
1155            this.update_in(&mut cx, |this, window, cx| {
1156                let editor = cx.new(|cx| {
1157                    ContextEditor::for_context(
1158                        context,
1159                        fs,
1160                        workspace,
1161                        this.project.clone(),
1162                        lsp_adapter_delegate,
1163                        window,
1164                        cx,
1165                    )
1166                });
1167                this.show_context(editor.clone(), window, cx);
1168                anyhow::Ok(editor)
1169            })?
1170        })
1171    }
1172
1173    fn is_authenticated(&mut self, cx: &mut Context<Self>) -> bool {
1174        LanguageModelRegistry::read_global(cx)
1175            .active_provider()
1176            .map_or(false, |provider| provider.is_authenticated(cx))
1177    }
1178
1179    fn authenticate(&mut self, cx: &mut Context<Self>) -> Option<Task<Result<()>>> {
1180        LanguageModelRegistry::read_global(cx)
1181            .active_provider()
1182            .map_or(None, |provider| Some(provider.authenticate(cx)))
1183    }
1184
1185    fn restart_context_servers(
1186        workspace: &mut Workspace,
1187        _action: &context_server::Restart,
1188        _: &mut Window,
1189        cx: &mut Context<Workspace>,
1190    ) {
1191        let Some(assistant_panel) = workspace.panel::<AssistantPanel>(cx) else {
1192            return;
1193        };
1194
1195        assistant_panel.update(cx, |assistant_panel, cx| {
1196            assistant_panel
1197                .context_store
1198                .update(cx, |context_store, cx| {
1199                    context_store.restart_context_servers(cx);
1200                });
1201        });
1202    }
1203}
1204
1205impl Render for AssistantPanel {
1206    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1207        let mut registrar = DivRegistrar::new(
1208            |panel, _, cx| {
1209                panel
1210                    .pane
1211                    .read(cx)
1212                    .toolbar()
1213                    .read(cx)
1214                    .item_of_type::<BufferSearchBar>()
1215            },
1216            cx,
1217        );
1218        BufferSearchBar::register(&mut registrar);
1219        let registrar = registrar.into_div();
1220
1221        v_flex()
1222            .key_context("AssistantPanel")
1223            .size_full()
1224            .on_action(cx.listener(|this, _: &NewContext, window, cx| {
1225                this.new_context(window, cx);
1226            }))
1227            .on_action(cx.listener(|this, _: &ShowConfiguration, window, cx| {
1228                this.show_configuration_tab(window, cx)
1229            }))
1230            .on_action(cx.listener(AssistantPanel::deploy_history))
1231            .on_action(cx.listener(AssistantPanel::deploy_prompt_library))
1232            .on_action(cx.listener(AssistantPanel::toggle_model_selector))
1233            .child(registrar.size_full().child(self.pane.clone()))
1234            .into_any_element()
1235    }
1236}
1237
1238impl Panel for AssistantPanel {
1239    fn persistent_name() -> &'static str {
1240        "AssistantPanel"
1241    }
1242
1243    fn position(&self, _: &Window, cx: &App) -> DockPosition {
1244        match AssistantSettings::get_global(cx).dock {
1245            AssistantDockPosition::Left => DockPosition::Left,
1246            AssistantDockPosition::Bottom => DockPosition::Bottom,
1247            AssistantDockPosition::Right => DockPosition::Right,
1248        }
1249    }
1250
1251    fn position_is_valid(&self, _: DockPosition) -> bool {
1252        true
1253    }
1254
1255    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1256        settings::update_settings_file::<AssistantSettings>(
1257            self.fs.clone(),
1258            cx,
1259            move |settings, _| {
1260                let dock = match position {
1261                    DockPosition::Left => AssistantDockPosition::Left,
1262                    DockPosition::Bottom => AssistantDockPosition::Bottom,
1263                    DockPosition::Right => AssistantDockPosition::Right,
1264                };
1265                settings.set_dock(dock);
1266            },
1267        );
1268    }
1269
1270    fn size(&self, window: &Window, cx: &App) -> Pixels {
1271        let settings = AssistantSettings::get_global(cx);
1272        match self.position(window, cx) {
1273            DockPosition::Left | DockPosition::Right => {
1274                self.width.unwrap_or(settings.default_width)
1275            }
1276            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1277        }
1278    }
1279
1280    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1281        match self.position(window, cx) {
1282            DockPosition::Left | DockPosition::Right => self.width = size,
1283            DockPosition::Bottom => self.height = size,
1284        }
1285        cx.notify();
1286    }
1287
1288    fn is_zoomed(&self, _: &Window, cx: &App) -> bool {
1289        self.pane.read(cx).is_zoomed()
1290    }
1291
1292    fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context<Self>) {
1293        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
1294    }
1295
1296    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1297        if active {
1298            if self.pane.read(cx).items_len() == 0 {
1299                self.new_context(window, cx);
1300            }
1301
1302            self.ensure_authenticated(window, cx);
1303        }
1304    }
1305
1306    fn pane(&self) -> Option<Entity<Pane>> {
1307        Some(self.pane.clone())
1308    }
1309
1310    fn remote_id() -> Option<proto::PanelId> {
1311        Some(proto::PanelId::AssistantPanel)
1312    }
1313
1314    fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
1315        let settings = AssistantSettings::get_global(cx);
1316        if !settings.enabled || !settings.button {
1317            return None;
1318        }
1319
1320        Some(IconName::ZedAssistant)
1321    }
1322
1323    fn icon_tooltip(&self, _: &Window, _: &App) -> Option<&'static str> {
1324        Some("Assistant Panel")
1325    }
1326
1327    fn toggle_action(&self) -> Box<dyn Action> {
1328        Box::new(ToggleFocus)
1329    }
1330
1331    fn activation_priority(&self) -> u32 {
1332        4
1333    }
1334}
1335
1336impl EventEmitter<PanelEvent> for AssistantPanel {}
1337impl EventEmitter<AssistantPanelEvent> for AssistantPanel {}
1338
1339impl Focusable for AssistantPanel {
1340    fn focus_handle(&self, cx: &App) -> FocusHandle {
1341        self.pane.focus_handle(cx)
1342    }
1343}
1344
1345struct PromptLibraryInlineAssist;
1346
1347impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1348    fn assist(
1349        &self,
1350        prompt_editor: &Entity<Editor>,
1351        initial_prompt: Option<String>,
1352        window: &mut Window,
1353        cx: &mut Context<PromptLibrary>,
1354    ) {
1355        InlineAssistant::update_global(cx, |assistant, cx| {
1356            assistant.assist(&prompt_editor, None, None, initial_prompt, window, cx)
1357        })
1358    }
1359
1360    fn focus_assistant_panel(
1361        &self,
1362        workspace: &mut Workspace,
1363        window: &mut Window,
1364        cx: &mut Context<Workspace>,
1365    ) -> bool {
1366        workspace
1367            .focus_panel::<AssistantPanel>(window, cx)
1368            .is_some()
1369    }
1370}
1371
1372pub struct ConcreteAssistantPanelDelegate;
1373
1374impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1375    fn active_context_editor(
1376        &self,
1377        workspace: &mut Workspace,
1378        _window: &mut Window,
1379        cx: &mut Context<Workspace>,
1380    ) -> Option<Entity<ContextEditor>> {
1381        let panel = workspace.panel::<AssistantPanel>(cx)?;
1382        panel.read(cx).active_context_editor(cx)
1383    }
1384
1385    fn open_saved_context(
1386        &self,
1387        workspace: &mut Workspace,
1388        path: PathBuf,
1389        window: &mut Window,
1390        cx: &mut Context<Workspace>,
1391    ) -> Task<Result<()>> {
1392        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1393            return Task::ready(Err(anyhow!("no Assistant panel found")));
1394        };
1395
1396        panel.update(cx, |panel, cx| panel.open_saved_context(path, window, cx))
1397    }
1398
1399    fn open_remote_context(
1400        &self,
1401        workspace: &mut Workspace,
1402        context_id: ContextId,
1403        window: &mut Window,
1404        cx: &mut Context<Workspace>,
1405    ) -> Task<Result<Entity<ContextEditor>>> {
1406        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1407            return Task::ready(Err(anyhow!("no Assistant panel found")));
1408        };
1409
1410        panel.update(cx, |panel, cx| {
1411            panel.open_remote_context(context_id, window, cx)
1412        })
1413    }
1414
1415    fn quote_selection(
1416        &self,
1417        workspace: &mut Workspace,
1418        creases: Vec<(String, String)>,
1419        window: &mut Window,
1420        cx: &mut Context<Workspace>,
1421    ) {
1422        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1423            return;
1424        };
1425
1426        if !panel.focus_handle(cx).contains_focused(window, cx) {
1427            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
1428        }
1429
1430        panel.update(cx, |_, cx| {
1431            // Wait to create a new context until the workspace is no longer
1432            // being updated.
1433            cx.defer_in(window, move |panel, window, cx| {
1434                if let Some(context) = panel
1435                    .active_context_editor(cx)
1436                    .or_else(|| panel.new_context(window, cx))
1437                {
1438                    context.update(cx, |context, cx| context.quote_creases(creases, window, cx));
1439                };
1440            });
1441        });
1442    }
1443}
1444
1445#[derive(Debug, PartialEq, Eq, Clone, Copy)]
1446pub enum WorkflowAssistStatus {
1447    Pending,
1448    Confirmed,
1449    Done,
1450    Idle,
1451}