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