assistant_panel.rs

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