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