assistant_panel.rs

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