assistant_panel.rs

   1use std::ops::Range;
   2use std::path::Path;
   3use std::sync::Arc;
   4use std::time::Duration;
   5
   6use anyhow::{Result, anyhow};
   7use assistant_context_editor::{
   8    AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
   9    SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
  10    render_remaining_tokens,
  11};
  12use assistant_settings::{AssistantDockPosition, AssistantSettings};
  13use assistant_slash_command::SlashCommandWorkingSet;
  14use assistant_tool::ToolWorkingSet;
  15
  16use client::{UserStore, zed_urls};
  17use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  18use fs::Fs;
  19use gpui::{
  20    Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
  21    Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
  22    Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  23};
  24use language::LanguageRegistry;
  25use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
  26use language_model_selector::ToggleModelSelector;
  27use project::Project;
  28use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
  29use proto::Plan;
  30use rules_library::{RulesLibrary, open_rules_library};
  31use settings::{Settings, update_settings_file};
  32use time::UtcOffset;
  33use ui::{
  34    Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
  35};
  36use util::ResultExt as _;
  37use workspace::Workspace;
  38use workspace::dock::{DockPosition, Panel, PanelEvent};
  39use zed_actions::agent::OpenConfiguration;
  40use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
  41
  42use crate::active_thread::{ActiveThread, ActiveThreadEvent};
  43use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
  44use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
  45use crate::message_editor::{MessageEditor, MessageEditorEvent};
  46use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
  47use crate::thread_history::{PastContext, PastThread, ThreadHistory};
  48use crate::thread_store::ThreadStore;
  49use crate::ui::UsageBanner;
  50use crate::{
  51    AddContextServer, AgentDiff, DeleteRecentlyOpenThread, ExpandMessageEditor, InlineAssistant,
  52    NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent,
  53    ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
  54};
  55
  56pub fn init(cx: &mut App) {
  57    cx.observe_new(
  58        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
  59            workspace
  60                .register_action(|workspace, action: &NewThread, window, cx| {
  61                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  62                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
  63                        workspace.focus_panel::<AssistantPanel>(window, cx);
  64                    }
  65                })
  66                .register_action(|workspace, _: &OpenHistory, window, cx| {
  67                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  68                        workspace.focus_panel::<AssistantPanel>(window, cx);
  69                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
  70                    }
  71                })
  72                .register_action(|workspace, _: &OpenConfiguration, window, cx| {
  73                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  74                        workspace.focus_panel::<AssistantPanel>(window, cx);
  75                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
  76                    }
  77                })
  78                .register_action(|workspace, _: &NewTextThread, window, cx| {
  79                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  80                        workspace.focus_panel::<AssistantPanel>(window, cx);
  81                        panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
  82                    }
  83                })
  84                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
  85                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  86                        workspace.focus_panel::<AssistantPanel>(window, cx);
  87                        panel.update(cx, |panel, cx| {
  88                            panel.deploy_rules_library(action, window, cx)
  89                        });
  90                    }
  91                })
  92                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
  93                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
  94                        workspace.focus_panel::<AssistantPanel>(window, cx);
  95                        let thread = panel.read(cx).thread.read(cx).thread().clone();
  96                        AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
  97                    }
  98                })
  99                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
 100                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 101                        workspace.focus_panel::<AssistantPanel>(window, cx);
 102                        panel.update(cx, |panel, cx| {
 103                            panel.message_editor.update(cx, |editor, cx| {
 104                                editor.expand_message_editor(&ExpandMessageEditor, window, cx);
 105                            });
 106                        });
 107                    }
 108                })
 109                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 110                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 111                        workspace.focus_panel::<AssistantPanel>(window, cx);
 112                        panel.update(cx, |panel, cx| {
 113                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 114                        });
 115                    }
 116                })
 117                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 118                    if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 119                        workspace.focus_panel::<AssistantPanel>(window, cx);
 120                        panel.update(cx, |panel, cx| {
 121                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 122                        });
 123                    }
 124                });
 125        },
 126    )
 127    .detach();
 128}
 129
 130enum ActiveView {
 131    Thread {
 132        change_title_editor: Entity<Editor>,
 133        thread: WeakEntity<Thread>,
 134        _subscriptions: Vec<gpui::Subscription>,
 135    },
 136    PromptEditor {
 137        context_editor: Entity<ContextEditor>,
 138        title_editor: Entity<Editor>,
 139        _subscriptions: Vec<gpui::Subscription>,
 140    },
 141    History,
 142    Configuration,
 143}
 144
 145impl ActiveView {
 146    pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
 147        let summary = thread.read(cx).summary_or_default();
 148
 149        let editor = cx.new(|cx| {
 150            let mut editor = Editor::single_line(window, cx);
 151            editor.set_text(summary.clone(), window, cx);
 152            editor
 153        });
 154
 155        let subscriptions = vec![
 156            window.subscribe(&editor, cx, {
 157                {
 158                    let thread = thread.clone();
 159                    move |editor, event, window, cx| match event {
 160                        EditorEvent::BufferEdited => {
 161                            let new_summary = editor.read(cx).text(cx);
 162
 163                            thread.update(cx, |thread, cx| {
 164                                thread.set_summary(new_summary, cx);
 165                            })
 166                        }
 167                        EditorEvent::Blurred => {
 168                            if editor.read(cx).text(cx).is_empty() {
 169                                let summary = thread.read(cx).summary_or_default();
 170
 171                                editor.update(cx, |editor, cx| {
 172                                    editor.set_text(summary, window, cx);
 173                                });
 174                            }
 175                        }
 176                        _ => {}
 177                    }
 178                }
 179            }),
 180            window.subscribe(&thread, cx, {
 181                let editor = editor.clone();
 182                move |thread, event, window, cx| match event {
 183                    ThreadEvent::SummaryGenerated => {
 184                        let summary = thread.read(cx).summary_or_default();
 185
 186                        editor.update(cx, |editor, cx| {
 187                            editor.set_text(summary, window, cx);
 188                        })
 189                    }
 190                    _ => {}
 191                }
 192            }),
 193        ];
 194
 195        Self::Thread {
 196            change_title_editor: editor,
 197            thread: thread.downgrade(),
 198            _subscriptions: subscriptions,
 199        }
 200    }
 201
 202    pub fn prompt_editor(
 203        context_editor: Entity<ContextEditor>,
 204        window: &mut Window,
 205        cx: &mut App,
 206    ) -> Self {
 207        let title = context_editor.read(cx).title(cx).to_string();
 208
 209        let editor = cx.new(|cx| {
 210            let mut editor = Editor::single_line(window, cx);
 211            editor.set_text(title, window, cx);
 212            editor
 213        });
 214
 215        // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
 216        // cause a custom summary to be set. The presence of this custom summary would cause
 217        // summarization to not happen.
 218        let mut suppress_first_edit = true;
 219
 220        let subscriptions = vec![
 221            window.subscribe(&editor, cx, {
 222                {
 223                    let context_editor = context_editor.clone();
 224                    move |editor, event, window, cx| match event {
 225                        EditorEvent::BufferEdited => {
 226                            if suppress_first_edit {
 227                                suppress_first_edit = false;
 228                                return;
 229                            }
 230                            let new_summary = editor.read(cx).text(cx);
 231
 232                            context_editor.update(cx, |context_editor, cx| {
 233                                context_editor
 234                                    .context()
 235                                    .update(cx, |assistant_context, cx| {
 236                                        assistant_context.set_custom_summary(new_summary, cx);
 237                                    })
 238                            })
 239                        }
 240                        EditorEvent::Blurred => {
 241                            if editor.read(cx).text(cx).is_empty() {
 242                                let summary = context_editor
 243                                    .read(cx)
 244                                    .context()
 245                                    .read(cx)
 246                                    .summary_or_default();
 247
 248                                editor.update(cx, |editor, cx| {
 249                                    editor.set_text(summary, window, cx);
 250                                });
 251                            }
 252                        }
 253                        _ => {}
 254                    }
 255                }
 256            }),
 257            window.subscribe(&context_editor.read(cx).context().clone(), cx, {
 258                let editor = editor.clone();
 259                move |assistant_context, event, window, cx| match event {
 260                    ContextEvent::SummaryGenerated => {
 261                        let summary = assistant_context.read(cx).summary_or_default();
 262
 263                        editor.update(cx, |editor, cx| {
 264                            editor.set_text(summary, window, cx);
 265                        })
 266                    }
 267                    _ => {}
 268                }
 269            }),
 270        ];
 271
 272        Self::PromptEditor {
 273            context_editor,
 274            title_editor: editor,
 275            _subscriptions: subscriptions,
 276        }
 277    }
 278}
 279
 280pub struct AssistantPanel {
 281    workspace: WeakEntity<Workspace>,
 282    user_store: Entity<UserStore>,
 283    project: Entity<Project>,
 284    fs: Arc<dyn Fs>,
 285    language_registry: Arc<LanguageRegistry>,
 286    thread_store: Entity<ThreadStore>,
 287    thread: Entity<ActiveThread>,
 288    message_editor: Entity<MessageEditor>,
 289    _active_thread_subscriptions: Vec<Subscription>,
 290    _default_model_subscription: Subscription,
 291    context_store: Entity<assistant_context_editor::ContextStore>,
 292    prompt_store: Option<Entity<PromptStore>>,
 293    configuration: Option<Entity<AssistantConfiguration>>,
 294    configuration_subscription: Option<Subscription>,
 295    local_timezone: UtcOffset,
 296    active_view: ActiveView,
 297    previous_view: Option<ActiveView>,
 298    history_store: Entity<HistoryStore>,
 299    history: Entity<ThreadHistory>,
 300    assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
 301    assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 302    assistant_navigation_menu: Option<Entity<ContextMenu>>,
 303    width: Option<Pixels>,
 304    height: Option<Pixels>,
 305}
 306
 307impl AssistantPanel {
 308    pub fn load(
 309        workspace: WeakEntity<Workspace>,
 310        prompt_builder: Arc<PromptBuilder>,
 311        mut cx: AsyncWindowContext,
 312    ) -> Task<Result<Entity<Self>>> {
 313        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 314        cx.spawn(async move |cx| {
 315            let prompt_store = match prompt_store {
 316                Ok(prompt_store) => prompt_store.await.ok(),
 317                Err(_) => None,
 318            };
 319            let tools = cx.new(|_| ToolWorkingSet::default())?;
 320            let thread_store = workspace
 321                .update(cx, |workspace, cx| {
 322                    let project = workspace.project().clone();
 323                    ThreadStore::load(
 324                        project,
 325                        tools.clone(),
 326                        prompt_store.clone(),
 327                        prompt_builder.clone(),
 328                        cx,
 329                    )
 330                })?
 331                .await?;
 332
 333            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 334            let context_store = workspace
 335                .update(cx, |workspace, cx| {
 336                    let project = workspace.project().clone();
 337                    assistant_context_editor::ContextStore::new(
 338                        project,
 339                        prompt_builder.clone(),
 340                        slash_commands,
 341                        cx,
 342                    )
 343                })?
 344                .await?;
 345
 346            workspace.update_in(cx, |workspace, window, cx| {
 347                cx.new(|cx| {
 348                    Self::new(
 349                        workspace,
 350                        thread_store,
 351                        context_store,
 352                        prompt_store,
 353                        window,
 354                        cx,
 355                    )
 356                })
 357            })
 358        })
 359    }
 360
 361    fn new(
 362        workspace: &Workspace,
 363        thread_store: Entity<ThreadStore>,
 364        context_store: Entity<assistant_context_editor::ContextStore>,
 365        prompt_store: Option<Entity<PromptStore>>,
 366        window: &mut Window,
 367        cx: &mut Context<Self>,
 368    ) -> Self {
 369        let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
 370        let fs = workspace.app_state().fs.clone();
 371        let user_store = workspace.app_state().user_store.clone();
 372        let project = workspace.project();
 373        let language_registry = project.read(cx).languages().clone();
 374        let workspace = workspace.weak_handle();
 375        let weak_self = cx.entity().downgrade();
 376
 377        let message_editor_context_store = cx.new(|_cx| {
 378            crate::context_store::ContextStore::new(
 379                project.downgrade(),
 380                Some(thread_store.downgrade()),
 381            )
 382        });
 383
 384        let message_editor = cx.new(|cx| {
 385            MessageEditor::new(
 386                fs.clone(),
 387                workspace.clone(),
 388                message_editor_context_store.clone(),
 389                prompt_store.clone(),
 390                thread_store.downgrade(),
 391                thread.clone(),
 392                window,
 393                cx,
 394            )
 395        });
 396
 397        let message_editor_subscription =
 398            cx.subscribe(&message_editor, |_, _, event, cx| match event {
 399                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 400                    cx.notify();
 401                }
 402            });
 403
 404        let history_store = cx.new(|cx| {
 405            HistoryStore::new(
 406                thread_store.clone(),
 407                context_store.clone(),
 408                [RecentEntry::Thread(thread.clone())],
 409                cx,
 410            )
 411        });
 412
 413        cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
 414
 415        let active_view = ActiveView::thread(thread.clone(), window, cx);
 416        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 417            if let ThreadEvent::MessageAdded(_) = &event {
 418                // needed to leave empty state
 419                cx.notify();
 420            }
 421        });
 422        let active_thread = cx.new(|cx| {
 423            ActiveThread::new(
 424                thread.clone(),
 425                thread_store.clone(),
 426                language_registry.clone(),
 427                workspace.clone(),
 428                window,
 429                cx,
 430            )
 431        });
 432
 433        let active_thread_subscription =
 434            cx.subscribe(&active_thread, |_, _, event, cx| match &event {
 435                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 436                    cx.notify();
 437                }
 438            });
 439
 440        let weak_panel = weak_self.clone();
 441
 442        window.defer(cx, move |window, cx| {
 443            let panel = weak_panel.clone();
 444            let assistant_navigation_menu =
 445                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
 446                    let recently_opened = panel
 447                        .update(cx, |this, cx| {
 448                            this.history_store.update(cx, |history_store, cx| {
 449                                history_store.recently_opened_entries(cx)
 450                            })
 451                        })
 452                        .unwrap_or_default();
 453
 454                    if !recently_opened.is_empty() {
 455                        menu = menu.header("Recently Opened");
 456
 457                        for entry in recently_opened.iter() {
 458                            let summary = entry.summary(cx);
 459                            menu = menu.entry_with_end_slot(
 460                                summary,
 461                                None,
 462                                {
 463                                    let panel = panel.clone();
 464                                    let entry = entry.clone();
 465                                    move |window, cx| {
 466                                        panel
 467                                            .update(cx, {
 468                                                let entry = entry.clone();
 469                                                move |this, cx| match entry {
 470                                                    RecentEntry::Thread(thread) => {
 471                                                        this.open_thread(thread, window, cx)
 472                                                    }
 473                                                    RecentEntry::Context(context) => {
 474                                                        let Some(path) = context.read(cx).path()
 475                                                        else {
 476                                                            return;
 477                                                        };
 478                                                        this.open_saved_prompt_editor(
 479                                                            path.clone(),
 480                                                            window,
 481                                                            cx,
 482                                                        )
 483                                                        .detach_and_log_err(cx)
 484                                                    }
 485                                                }
 486                                            })
 487                                            .ok();
 488                                    }
 489                                },
 490                                IconName::Close,
 491                                "Close Entry".into(),
 492                                {
 493                                    let panel = panel.clone();
 494                                    let entry = entry.clone();
 495                                    move |_window, cx| {
 496                                        panel
 497                                            .update(cx, |this, cx| {
 498                                                this.history_store.update(
 499                                                    cx,
 500                                                    |history_store, cx| {
 501                                                        history_store.remove_recently_opened_entry(
 502                                                            &entry, cx,
 503                                                        );
 504                                                    },
 505                                                );
 506                                            })
 507                                            .ok();
 508                                    }
 509                                },
 510                            );
 511                        }
 512
 513                        menu = menu.separator();
 514                    }
 515
 516                    menu.action("View All", Box::new(OpenHistory))
 517                        .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
 518                        .fixed_width(px(320.).into())
 519                        .keep_open_on_confirm(false)
 520                        .key_context("NavigationMenu")
 521                });
 522            weak_panel
 523                .update(cx, |panel, cx| {
 524                    cx.subscribe_in(
 525                        &assistant_navigation_menu,
 526                        window,
 527                        |_, menu, _: &DismissEvent, window, cx| {
 528                            menu.update(cx, |menu, _| {
 529                                menu.clear_selected();
 530                            });
 531                            cx.focus_self(window);
 532                        },
 533                    )
 534                    .detach();
 535                    panel.assistant_navigation_menu = Some(assistant_navigation_menu);
 536                })
 537                .ok();
 538        });
 539
 540        let _default_model_subscription = cx.subscribe(
 541            &LanguageModelRegistry::global(cx),
 542            |this, _, event: &language_model::Event, cx| match event {
 543                language_model::Event::DefaultModelChanged => {
 544                    this.thread
 545                        .read(cx)
 546                        .thread()
 547                        .clone()
 548                        .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
 549                }
 550                _ => {}
 551            },
 552        );
 553
 554        Self {
 555            active_view,
 556            workspace,
 557            user_store,
 558            project: project.clone(),
 559            fs: fs.clone(),
 560            language_registry,
 561            thread_store: thread_store.clone(),
 562            thread: active_thread,
 563            message_editor,
 564            _active_thread_subscriptions: vec![
 565                thread_subscription,
 566                active_thread_subscription,
 567                message_editor_subscription,
 568            ],
 569            _default_model_subscription,
 570            context_store,
 571            prompt_store,
 572            configuration: None,
 573            configuration_subscription: None,
 574            local_timezone: UtcOffset::from_whole_seconds(
 575                chrono::Local::now().offset().local_minus_utc(),
 576            )
 577            .unwrap(),
 578            previous_view: None,
 579            history_store: history_store.clone(),
 580            history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
 581            assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
 582            assistant_navigation_menu_handle: PopoverMenuHandle::default(),
 583            assistant_navigation_menu: None,
 584            width: None,
 585            height: None,
 586        }
 587    }
 588
 589    pub fn toggle_focus(
 590        workspace: &mut Workspace,
 591        _: &ToggleFocus,
 592        window: &mut Window,
 593        cx: &mut Context<Workspace>,
 594    ) {
 595        if workspace
 596            .panel::<Self>(cx)
 597            .is_some_and(|panel| panel.read(cx).enabled(cx))
 598        {
 599            workspace.toggle_panel_focus::<Self>(window, cx);
 600        }
 601    }
 602
 603    pub(crate) fn local_timezone(&self) -> UtcOffset {
 604        self.local_timezone
 605    }
 606
 607    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
 608        &self.prompt_store
 609    }
 610
 611    pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
 612        &self.thread_store
 613    }
 614
 615    fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
 616        self.thread
 617            .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
 618    }
 619
 620    fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
 621        let thread = self
 622            .thread_store
 623            .update(cx, |this, cx| this.create_thread(cx));
 624
 625        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 626        self.set_active_view(thread_view, window, cx);
 627
 628        let message_editor_context_store = cx.new(|_cx| {
 629            crate::context_store::ContextStore::new(
 630                self.project.downgrade(),
 631                Some(self.thread_store.downgrade()),
 632            )
 633        });
 634
 635        if let Some(other_thread_id) = action.from_thread_id.clone() {
 636            let other_thread_task = self
 637                .thread_store
 638                .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
 639
 640            cx.spawn({
 641                let context_store = message_editor_context_store.clone();
 642
 643                async move |_panel, cx| {
 644                    let other_thread = other_thread_task.await?;
 645
 646                    context_store.update(cx, |this, cx| {
 647                        this.add_thread(other_thread, false, cx);
 648                    })?;
 649                    anyhow::Ok(())
 650                }
 651            })
 652            .detach_and_log_err(cx);
 653        }
 654
 655        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 656            if let ThreadEvent::MessageAdded(_) = &event {
 657                // needed to leave empty state
 658                cx.notify();
 659            }
 660        });
 661
 662        self.thread = cx.new(|cx| {
 663            ActiveThread::new(
 664                thread.clone(),
 665                self.thread_store.clone(),
 666                self.language_registry.clone(),
 667                self.workspace.clone(),
 668                window,
 669                cx,
 670            )
 671        });
 672
 673        let active_thread_subscription =
 674            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
 675                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 676                    cx.notify();
 677                }
 678            });
 679
 680        self.message_editor = cx.new(|cx| {
 681            MessageEditor::new(
 682                self.fs.clone(),
 683                self.workspace.clone(),
 684                message_editor_context_store,
 685                self.prompt_store.clone(),
 686                self.thread_store.downgrade(),
 687                thread,
 688                window,
 689                cx,
 690            )
 691        });
 692        self.message_editor.focus_handle(cx).focus(window);
 693
 694        let message_editor_subscription =
 695            cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
 696                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 697                    cx.notify();
 698                }
 699            });
 700
 701        self._active_thread_subscriptions = vec![
 702            thread_subscription,
 703            active_thread_subscription,
 704            message_editor_subscription,
 705        ];
 706    }
 707
 708    fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 709        let context = self
 710            .context_store
 711            .update(cx, |context_store, cx| context_store.create(cx));
 712        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 713            .log_err()
 714            .flatten();
 715
 716        let context_editor = cx.new(|cx| {
 717            let mut editor = ContextEditor::for_context(
 718                context,
 719                self.fs.clone(),
 720                self.workspace.clone(),
 721                self.project.clone(),
 722                lsp_adapter_delegate,
 723                window,
 724                cx,
 725            );
 726            editor.insert_default_prompt(window, cx);
 727            editor
 728        });
 729
 730        self.set_active_view(
 731            ActiveView::prompt_editor(context_editor.clone(), window, cx),
 732            window,
 733            cx,
 734        );
 735        context_editor.focus_handle(cx).focus(window);
 736    }
 737
 738    fn deploy_rules_library(
 739        &mut self,
 740        action: &OpenRulesLibrary,
 741        _window: &mut Window,
 742        cx: &mut Context<Self>,
 743    ) {
 744        open_rules_library(
 745            self.language_registry.clone(),
 746            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
 747            Arc::new(|| {
 748                Box::new(SlashCommandCompletionProvider::new(
 749                    Arc::new(SlashCommandWorkingSet::default()),
 750                    None,
 751                    None,
 752                ))
 753            }),
 754            action
 755                .prompt_to_select
 756                .map(|uuid| UserPromptId(uuid).into()),
 757            cx,
 758        )
 759        .detach_and_log_err(cx);
 760    }
 761
 762    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 763        if matches!(self.active_view, ActiveView::History) {
 764            if let Some(previous_view) = self.previous_view.take() {
 765                self.set_active_view(previous_view, window, cx);
 766            }
 767        } else {
 768            self.thread_store
 769                .update(cx, |thread_store, cx| thread_store.reload(cx))
 770                .detach_and_log_err(cx);
 771            self.set_active_view(ActiveView::History, window, cx);
 772        }
 773        cx.notify();
 774    }
 775
 776    pub(crate) fn open_saved_prompt_editor(
 777        &mut self,
 778        path: Arc<Path>,
 779        window: &mut Window,
 780        cx: &mut Context<Self>,
 781    ) -> Task<Result<()>> {
 782        let context = self
 783            .context_store
 784            .update(cx, |store, cx| store.open_local_context(path, cx));
 785        let fs = self.fs.clone();
 786        let project = self.project.clone();
 787        let workspace = self.workspace.clone();
 788
 789        let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
 790
 791        cx.spawn_in(window, async move |this, cx| {
 792            let context = context.await?;
 793            this.update_in(cx, |this, window, cx| {
 794                let editor = cx.new(|cx| {
 795                    ContextEditor::for_context(
 796                        context,
 797                        fs,
 798                        workspace,
 799                        project,
 800                        lsp_adapter_delegate,
 801                        window,
 802                        cx,
 803                    )
 804                });
 805
 806                this.set_active_view(
 807                    ActiveView::prompt_editor(editor.clone(), window, cx),
 808                    window,
 809                    cx,
 810                );
 811
 812                anyhow::Ok(())
 813            })??;
 814            Ok(())
 815        })
 816    }
 817
 818    pub(crate) fn open_thread_by_id(
 819        &mut self,
 820        thread_id: &ThreadId,
 821        window: &mut Window,
 822        cx: &mut Context<Self>,
 823    ) -> Task<Result<()>> {
 824        let open_thread_task = self
 825            .thread_store
 826            .update(cx, |this, cx| this.open_thread(thread_id, cx));
 827        cx.spawn_in(window, async move |this, cx| {
 828            let thread = open_thread_task.await?;
 829            this.update_in(cx, |this, window, cx| {
 830                this.open_thread(thread, window, cx);
 831                anyhow::Ok(())
 832            })??;
 833            Ok(())
 834        })
 835    }
 836
 837    pub(crate) fn open_thread(
 838        &mut self,
 839        thread: Entity<Thread>,
 840        window: &mut Window,
 841        cx: &mut Context<Self>,
 842    ) {
 843        let thread_view = ActiveView::thread(thread.clone(), window, cx);
 844        self.set_active_view(thread_view, window, cx);
 845        let message_editor_context_store = cx.new(|_cx| {
 846            crate::context_store::ContextStore::new(
 847                self.project.downgrade(),
 848                Some(self.thread_store.downgrade()),
 849            )
 850        });
 851        let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
 852            if let ThreadEvent::MessageAdded(_) = &event {
 853                // needed to leave empty state
 854                cx.notify();
 855            }
 856        });
 857
 858        self.thread = cx.new(|cx| {
 859            ActiveThread::new(
 860                thread.clone(),
 861                self.thread_store.clone(),
 862                self.language_registry.clone(),
 863                self.workspace.clone(),
 864                window,
 865                cx,
 866            )
 867        });
 868
 869        let active_thread_subscription =
 870            cx.subscribe(&self.thread, |_, _, event, cx| match &event {
 871                ActiveThreadEvent::EditingMessageTokenCountChanged => {
 872                    cx.notify();
 873                }
 874            });
 875
 876        self.message_editor = cx.new(|cx| {
 877            MessageEditor::new(
 878                self.fs.clone(),
 879                self.workspace.clone(),
 880                message_editor_context_store,
 881                self.prompt_store.clone(),
 882                self.thread_store.downgrade(),
 883                thread,
 884                window,
 885                cx,
 886            )
 887        });
 888        self.message_editor.focus_handle(cx).focus(window);
 889
 890        let message_editor_subscription =
 891            cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
 892                MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
 893                    cx.notify();
 894                }
 895            });
 896
 897        self._active_thread_subscriptions = vec![
 898            thread_subscription,
 899            active_thread_subscription,
 900            message_editor_subscription,
 901        ];
 902    }
 903
 904    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
 905        match self.active_view {
 906            ActiveView::Configuration | ActiveView::History => {
 907                self.active_view =
 908                    ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
 909                self.message_editor.focus_handle(cx).focus(window);
 910                cx.notify();
 911            }
 912            _ => {}
 913        }
 914    }
 915
 916    pub fn toggle_navigation_menu(
 917        &mut self,
 918        _: &ToggleNavigationMenu,
 919        window: &mut Window,
 920        cx: &mut Context<Self>,
 921    ) {
 922        self.assistant_navigation_menu_handle.toggle(window, cx);
 923    }
 924
 925    pub fn toggle_options_menu(
 926        &mut self,
 927        _: &ToggleOptionsMenu,
 928        window: &mut Window,
 929        cx: &mut Context<Self>,
 930    ) {
 931        self.assistant_dropdown_menu_handle.toggle(window, cx);
 932    }
 933
 934    pub fn open_agent_diff(
 935        &mut self,
 936        _: &OpenAgentDiff,
 937        window: &mut Window,
 938        cx: &mut Context<Self>,
 939    ) {
 940        let thread = self.thread.read(cx).thread().clone();
 941        self.workspace
 942            .update(cx, |workspace, cx| {
 943                AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
 944            })
 945            .log_err();
 946    }
 947
 948    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 949        let context_server_manager = self.thread_store.read(cx).context_server_manager();
 950        let tools = self.thread_store.read(cx).tools();
 951        let fs = self.fs.clone();
 952
 953        self.set_active_view(ActiveView::Configuration, window, cx);
 954        self.configuration =
 955            Some(cx.new(|cx| {
 956                AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
 957            }));
 958
 959        if let Some(configuration) = self.configuration.as_ref() {
 960            self.configuration_subscription = Some(cx.subscribe_in(
 961                configuration,
 962                window,
 963                Self::handle_assistant_configuration_event,
 964            ));
 965
 966            configuration.focus_handle(cx).focus(window);
 967        }
 968    }
 969
 970    pub(crate) fn open_active_thread_as_markdown(
 971        &mut self,
 972        _: &OpenActiveThreadAsMarkdown,
 973        window: &mut Window,
 974        cx: &mut Context<Self>,
 975    ) {
 976        let Some(workspace) = self
 977            .workspace
 978            .upgrade()
 979            .ok_or_else(|| anyhow!("workspace dropped"))
 980            .log_err()
 981        else {
 982            return;
 983        };
 984
 985        let markdown_language_task = workspace
 986            .read(cx)
 987            .app_state()
 988            .languages
 989            .language_for_name("Markdown");
 990        let thread = self.active_thread(cx);
 991        cx.spawn_in(window, async move |_this, cx| {
 992            let markdown_language = markdown_language_task.await?;
 993
 994            workspace.update_in(cx, |workspace, window, cx| {
 995                let thread = thread.read(cx);
 996                let markdown = thread.to_markdown(cx)?;
 997                let thread_summary = thread
 998                    .summary()
 999                    .map(|summary| summary.to_string())
1000                    .unwrap_or_else(|| "Thread".to_string());
1001
1002                let project = workspace.project().clone();
1003                let buffer = project.update(cx, |project, cx| {
1004                    project.create_local_buffer(&markdown, Some(markdown_language), cx)
1005                });
1006                let buffer = cx.new(|cx| {
1007                    MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
1008                });
1009
1010                workspace.add_item_to_active_pane(
1011                    Box::new(cx.new(|cx| {
1012                        let mut editor =
1013                            Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
1014                        editor.set_breadcrumb_header(thread_summary);
1015                        editor
1016                    })),
1017                    None,
1018                    true,
1019                    window,
1020                    cx,
1021                );
1022
1023                anyhow::Ok(())
1024            })
1025        })
1026        .detach_and_log_err(cx);
1027    }
1028
1029    fn handle_assistant_configuration_event(
1030        &mut self,
1031        _entity: &Entity<AssistantConfiguration>,
1032        event: &AssistantConfigurationEvent,
1033        window: &mut Window,
1034        cx: &mut Context<Self>,
1035    ) {
1036        match event {
1037            AssistantConfigurationEvent::NewThread(provider) => {
1038                if LanguageModelRegistry::read_global(cx)
1039                    .default_model()
1040                    .map_or(true, |model| model.provider.id() != provider.id())
1041                {
1042                    if let Some(model) = provider.default_model(cx) {
1043                        update_settings_file::<AssistantSettings>(
1044                            self.fs.clone(),
1045                            cx,
1046                            move |settings, _| settings.set_model(model),
1047                        );
1048                    }
1049                }
1050
1051                self.new_thread(&NewThread::default(), window, cx);
1052            }
1053        }
1054    }
1055
1056    pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
1057        self.thread.read(cx).thread().clone()
1058    }
1059
1060    pub(crate) fn delete_thread(
1061        &mut self,
1062        thread_id: &ThreadId,
1063        cx: &mut Context<Self>,
1064    ) -> Task<Result<()>> {
1065        self.thread_store
1066            .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1067    }
1068
1069    pub(crate) fn has_active_thread(&self) -> bool {
1070        matches!(self.active_view, ActiveView::Thread { .. })
1071    }
1072
1073    pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1074        match &self.active_view {
1075            ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1076            _ => None,
1077        }
1078    }
1079
1080    pub(crate) fn delete_context(
1081        &mut self,
1082        path: Arc<Path>,
1083        cx: &mut Context<Self>,
1084    ) -> Task<Result<()>> {
1085        self.context_store
1086            .update(cx, |this, cx| this.delete_local_context(path, cx))
1087    }
1088
1089    fn set_active_view(
1090        &mut self,
1091        new_view: ActiveView,
1092        window: &mut Window,
1093        cx: &mut Context<Self>,
1094    ) {
1095        let current_is_history = matches!(self.active_view, ActiveView::History);
1096        let new_is_history = matches!(new_view, ActiveView::History);
1097
1098        match &self.active_view {
1099            ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1100                if let Some(thread) = thread.upgrade() {
1101                    if thread.read(cx).is_empty() {
1102                        store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
1103                    }
1104                }
1105            }),
1106            _ => {}
1107        }
1108
1109        match &new_view {
1110            ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1111                if let Some(thread) = thread.upgrade() {
1112                    store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
1113                }
1114            }),
1115            ActiveView::PromptEditor { context_editor, .. } => {
1116                self.history_store.update(cx, |store, cx| {
1117                    let context = context_editor.read(cx).context().clone();
1118                    store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1119                })
1120            }
1121            _ => {}
1122        }
1123
1124        if current_is_history && !new_is_history {
1125            self.active_view = new_view;
1126        } else if !current_is_history && new_is_history {
1127            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1128        } else {
1129            if !new_is_history {
1130                self.previous_view = None;
1131            }
1132            self.active_view = new_view;
1133        }
1134
1135        self.focus_handle(cx).focus(window);
1136    }
1137}
1138
1139impl Focusable for AssistantPanel {
1140    fn focus_handle(&self, cx: &App) -> FocusHandle {
1141        match &self.active_view {
1142            ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1143            ActiveView::History => self.history.focus_handle(cx),
1144            ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1145            ActiveView::Configuration => {
1146                if let Some(configuration) = self.configuration.as_ref() {
1147                    configuration.focus_handle(cx)
1148                } else {
1149                    cx.focus_handle()
1150                }
1151            }
1152        }
1153    }
1154}
1155
1156impl EventEmitter<PanelEvent> for AssistantPanel {}
1157
1158impl Panel for AssistantPanel {
1159    fn persistent_name() -> &'static str {
1160        "AgentPanel"
1161    }
1162
1163    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1164        match AssistantSettings::get_global(cx).dock {
1165            AssistantDockPosition::Left => DockPosition::Left,
1166            AssistantDockPosition::Bottom => DockPosition::Bottom,
1167            AssistantDockPosition::Right => DockPosition::Right,
1168        }
1169    }
1170
1171    fn position_is_valid(&self, _: DockPosition) -> bool {
1172        true
1173    }
1174
1175    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1176        settings::update_settings_file::<AssistantSettings>(
1177            self.fs.clone(),
1178            cx,
1179            move |settings, _| {
1180                let dock = match position {
1181                    DockPosition::Left => AssistantDockPosition::Left,
1182                    DockPosition::Bottom => AssistantDockPosition::Bottom,
1183                    DockPosition::Right => AssistantDockPosition::Right,
1184                };
1185                settings.set_dock(dock);
1186            },
1187        );
1188    }
1189
1190    fn size(&self, window: &Window, cx: &App) -> Pixels {
1191        let settings = AssistantSettings::get_global(cx);
1192        match self.position(window, cx) {
1193            DockPosition::Left | DockPosition::Right => {
1194                self.width.unwrap_or(settings.default_width)
1195            }
1196            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1197        }
1198    }
1199
1200    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1201        match self.position(window, cx) {
1202            DockPosition::Left | DockPosition::Right => self.width = size,
1203            DockPosition::Bottom => self.height = size,
1204        }
1205        cx.notify();
1206    }
1207
1208    fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1209
1210    fn remote_id() -> Option<proto::PanelId> {
1211        Some(proto::PanelId::AssistantPanel)
1212    }
1213
1214    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1215        (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1216            .then_some(IconName::ZedAssistant)
1217    }
1218
1219    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1220        Some("Agent Panel")
1221    }
1222
1223    fn toggle_action(&self) -> Box<dyn Action> {
1224        Box::new(ToggleFocus)
1225    }
1226
1227    fn activation_priority(&self) -> u32 {
1228        3
1229    }
1230
1231    fn enabled(&self, cx: &App) -> bool {
1232        AssistantSettings::get_global(cx).enabled
1233    }
1234}
1235
1236impl AssistantPanel {
1237    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1238        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1239
1240        let content = match &self.active_view {
1241            ActiveView::Thread {
1242                change_title_editor,
1243                ..
1244            } => {
1245                let active_thread = self.thread.read(cx);
1246                let is_empty = active_thread.is_empty();
1247
1248                let summary = active_thread.summary(cx);
1249
1250                if is_empty {
1251                    Label::new(Thread::DEFAULT_SUMMARY.clone())
1252                        .truncate()
1253                        .into_any_element()
1254                } else if summary.is_none() {
1255                    Label::new(LOADING_SUMMARY_PLACEHOLDER)
1256                        .truncate()
1257                        .into_any_element()
1258                } else {
1259                    div()
1260                        .w_full()
1261                        .child(change_title_editor.clone())
1262                        .into_any_element()
1263                }
1264            }
1265            ActiveView::PromptEditor {
1266                title_editor,
1267                context_editor,
1268                ..
1269            } => {
1270                let context_editor = context_editor.read(cx);
1271                let summary = context_editor.context().read(cx).summary();
1272
1273                match summary {
1274                    None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1275                        .truncate()
1276                        .into_any_element(),
1277                    Some(summary) => {
1278                        if summary.done {
1279                            div()
1280                                .w_full()
1281                                .child(title_editor.clone())
1282                                .into_any_element()
1283                        } else {
1284                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
1285                                .truncate()
1286                                .into_any_element()
1287                        }
1288                    }
1289                }
1290            }
1291            ActiveView::History => Label::new("History").truncate().into_any_element(),
1292            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1293        };
1294
1295        h_flex()
1296            .key_context("TitleEditor")
1297            .id("TitleEditor")
1298            .flex_grow()
1299            .w_full()
1300            .max_w_full()
1301            .overflow_x_scroll()
1302            .child(content)
1303            .into_any()
1304    }
1305
1306    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1307        let active_thread = self.thread.read(cx);
1308        let thread = active_thread.thread().read(cx);
1309        let thread_id = thread.id().clone();
1310        let is_empty = active_thread.is_empty();
1311
1312        let show_token_count = match &self.active_view {
1313            ActiveView::Thread { .. } => !is_empty,
1314            ActiveView::PromptEditor { .. } => true,
1315            _ => false,
1316        };
1317
1318        let focus_handle = self.focus_handle(cx);
1319
1320        let go_back_button = div().child(
1321            IconButton::new("go-back", IconName::ArrowLeft)
1322                .icon_size(IconSize::Small)
1323                .on_click(cx.listener(|this, _, window, cx| {
1324                    this.go_back(&workspace::GoBack, window, cx);
1325                }))
1326                .tooltip({
1327                    let focus_handle = focus_handle.clone();
1328                    move |window, cx| {
1329                        Tooltip::for_action_in(
1330                            "Go Back",
1331                            &workspace::GoBack,
1332                            &focus_handle,
1333                            window,
1334                            cx,
1335                        )
1336                    }
1337                }),
1338        );
1339
1340        let recent_entries_menu = div().child(
1341            PopoverMenu::new("agent-nav-menu")
1342                .trigger_with_tooltip(
1343                    IconButton::new("agent-nav-menu", IconName::MenuAlt)
1344                        .icon_size(IconSize::Small)
1345                        .style(ui::ButtonStyle::Subtle),
1346                    {
1347                        let focus_handle = focus_handle.clone();
1348                        move |window, cx| {
1349                            Tooltip::for_action_in(
1350                                "Toggle Panel Menu",
1351                                &ToggleNavigationMenu,
1352                                &focus_handle,
1353                                window,
1354                                cx,
1355                            )
1356                        }
1357                    },
1358                )
1359                .anchor(Corner::TopLeft)
1360                .with_handle(self.assistant_navigation_menu_handle.clone())
1361                .menu({
1362                    let menu = self.assistant_navigation_menu.clone();
1363                    move |window, cx| {
1364                        if let Some(menu) = menu.as_ref() {
1365                            menu.update(cx, |_, cx| {
1366                                cx.defer_in(window, |menu, window, cx| {
1367                                    menu.rebuild(window, cx);
1368                                });
1369                            })
1370                        }
1371                        menu.clone()
1372                    }
1373                }),
1374        );
1375
1376        let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1377            .trigger_with_tooltip(
1378                IconButton::new("agent-options-menu", IconName::Ellipsis)
1379                    .icon_size(IconSize::Small),
1380                {
1381                    let focus_handle = focus_handle.clone();
1382                    move |window, cx| {
1383                        Tooltip::for_action_in(
1384                            "Toggle Agent Menu",
1385                            &ToggleOptionsMenu,
1386                            &focus_handle,
1387                            window,
1388                            cx,
1389                        )
1390                    }
1391                },
1392            )
1393            .anchor(Corner::TopRight)
1394            .with_handle(self.assistant_dropdown_menu_handle.clone())
1395            .menu(move |window, cx| {
1396                Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
1397                    menu.when(!is_empty, |menu| {
1398                        menu.action(
1399                            "Start New From Summary",
1400                            Box::new(NewThread {
1401                                from_thread_id: Some(thread_id.clone()),
1402                            }),
1403                        )
1404                        .separator()
1405                    })
1406                    .action("New Text Thread", NewTextThread.boxed_clone())
1407                    .action("Rules Library", Box::new(OpenRulesLibrary::default()))
1408                    .action("Settings", Box::new(OpenConfiguration))
1409                    .separator()
1410                    .header("MCPs")
1411                    .action(
1412                        "View Server Extensions",
1413                        Box::new(zed_actions::Extensions {
1414                            category_filter: Some(
1415                                zed_actions::ExtensionCategoryFilter::ContextServers,
1416                            ),
1417                        }),
1418                    )
1419                    .action("Add Custom Server", Box::new(AddContextServer))
1420                }))
1421            });
1422
1423        h_flex()
1424            .id("assistant-toolbar")
1425            .h(Tab::container_height(cx))
1426            .max_w_full()
1427            .flex_none()
1428            .justify_between()
1429            .gap_2()
1430            .bg(cx.theme().colors().tab_bar_background)
1431            .border_b_1()
1432            .border_color(cx.theme().colors().border)
1433            .child(
1434                h_flex()
1435                    .size_full()
1436                    .pl_1()
1437                    .gap_1()
1438                    .child(match &self.active_view {
1439                        ActiveView::History | ActiveView::Configuration => go_back_button,
1440                        _ => recent_entries_menu,
1441                    })
1442                    .child(self.render_title_view(window, cx)),
1443            )
1444            .child(
1445                h_flex()
1446                    .h_full()
1447                    .gap_2()
1448                    .when(show_token_count, |parent| {
1449                        parent.children(self.render_token_count(&thread, cx))
1450                    })
1451                    .child(
1452                        h_flex()
1453                            .h_full()
1454                            .gap(DynamicSpacing::Base02.rems(cx))
1455                            .px(DynamicSpacing::Base08.rems(cx))
1456                            .border_l_1()
1457                            .border_color(cx.theme().colors().border)
1458                            .child(
1459                                IconButton::new("new", IconName::Plus)
1460                                    .icon_size(IconSize::Small)
1461                                    .style(ButtonStyle::Subtle)
1462                                    .tooltip(move |window, cx| {
1463                                        Tooltip::for_action_in(
1464                                            "New Thread",
1465                                            &NewThread::default(),
1466                                            &focus_handle,
1467                                            window,
1468                                            cx,
1469                                        )
1470                                    })
1471                                    .on_click(move |_event, window, cx| {
1472                                        window.dispatch_action(
1473                                            NewThread::default().boxed_clone(),
1474                                            cx,
1475                                        );
1476                                    }),
1477                            )
1478                            .child(agent_extra_menu),
1479                    ),
1480            )
1481    }
1482
1483    fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1484        let is_generating = thread.is_generating();
1485        let message_editor = self.message_editor.read(cx);
1486
1487        let conversation_token_usage = thread.total_token_usage()?;
1488
1489        let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1490            self.thread.read(cx).editing_message_id()
1491        {
1492            let combined = thread
1493                .token_usage_up_to_message(editing_message_id)
1494                .add(unsent_tokens);
1495
1496            (combined, unsent_tokens > 0)
1497        } else {
1498            let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1499            let combined = conversation_token_usage.add(unsent_tokens);
1500
1501            (combined, unsent_tokens > 0)
1502        };
1503
1504        let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1505
1506        match &self.active_view {
1507            ActiveView::Thread { .. } => {
1508                if total_token_usage.total == 0 {
1509                    return None;
1510                }
1511
1512                let token_color = match total_token_usage.ratio() {
1513                    TokenUsageRatio::Normal if is_estimating => Color::Default,
1514                    TokenUsageRatio::Normal => Color::Muted,
1515                    TokenUsageRatio::Warning => Color::Warning,
1516                    TokenUsageRatio::Exceeded => Color::Error,
1517                };
1518
1519                let token_count = h_flex()
1520                    .id("token-count")
1521                    .flex_shrink_0()
1522                    .gap_0p5()
1523                    .when(!is_generating && is_estimating, |parent| {
1524                        parent
1525                            .child(
1526                                h_flex()
1527                                    .mr_1()
1528                                    .size_2p5()
1529                                    .justify_center()
1530                                    .rounded_full()
1531                                    .bg(cx.theme().colors().text.opacity(0.1))
1532                                    .child(
1533                                        div().size_1().rounded_full().bg(cx.theme().colors().text),
1534                                    ),
1535                            )
1536                            .tooltip(move |window, cx| {
1537                                Tooltip::with_meta(
1538                                    "Estimated New Token Count",
1539                                    None,
1540                                    format!(
1541                                        "Current Conversation Tokens: {}",
1542                                        humanize_token_count(conversation_token_usage.total)
1543                                    ),
1544                                    window,
1545                                    cx,
1546                                )
1547                            })
1548                    })
1549                    .child(
1550                        Label::new(humanize_token_count(total_token_usage.total))
1551                            .size(LabelSize::Small)
1552                            .color(token_color)
1553                            .map(|label| {
1554                                if is_generating || is_waiting_to_update_token_count {
1555                                    label
1556                                        .with_animation(
1557                                            "used-tokens-label",
1558                                            Animation::new(Duration::from_secs(2))
1559                                                .repeat()
1560                                                .with_easing(pulsating_between(0.6, 1.)),
1561                                            |label, delta| label.alpha(delta),
1562                                        )
1563                                        .into_any()
1564                                } else {
1565                                    label.into_any_element()
1566                                }
1567                            }),
1568                    )
1569                    .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1570                    .child(
1571                        Label::new(humanize_token_count(total_token_usage.max))
1572                            .size(LabelSize::Small)
1573                            .color(Color::Muted),
1574                    )
1575                    .into_any();
1576
1577                Some(token_count)
1578            }
1579            ActiveView::PromptEditor { context_editor, .. } => {
1580                let element = render_remaining_tokens(context_editor, cx)?;
1581
1582                Some(element.into_any_element())
1583            }
1584            _ => None,
1585        }
1586    }
1587
1588    fn render_active_thread_or_empty_state(
1589        &self,
1590        window: &mut Window,
1591        cx: &mut Context<Self>,
1592    ) -> AnyElement {
1593        if self.thread.read(cx).is_empty() {
1594            return self
1595                .render_thread_empty_state(window, cx)
1596                .into_any_element();
1597        }
1598
1599        self.thread.clone().into_any_element()
1600    }
1601
1602    fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1603        let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1604            return Some(ConfigurationError::NoProvider);
1605        };
1606
1607        if !model.provider.is_authenticated(cx) {
1608            return Some(ConfigurationError::ProviderNotAuthenticated);
1609        }
1610
1611        if model.provider.must_accept_terms(cx) {
1612            return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1613                model.provider,
1614            ));
1615        }
1616
1617        None
1618    }
1619
1620    fn render_thread_empty_state(
1621        &self,
1622        window: &mut Window,
1623        cx: &mut Context<Self>,
1624    ) -> impl IntoElement {
1625        let recent_history = self
1626            .history_store
1627            .update(cx, |this, cx| this.recent_entries(6, cx));
1628
1629        let configuration_error = self.configuration_error(cx);
1630        let no_error = configuration_error.is_none();
1631        let focus_handle = self.focus_handle(cx);
1632
1633        v_flex()
1634            .size_full()
1635            .when(recent_history.is_empty(), |this| {
1636                let configuration_error_ref = &configuration_error;
1637                this.child(
1638                    v_flex()
1639                        .size_full()
1640                        .max_w_80()
1641                        .mx_auto()
1642                        .justify_center()
1643                        .items_center()
1644                        .gap_1()
1645                        .child(
1646                            h_flex().child(
1647                                Headline::new("Welcome to the Agent Panel")
1648                            ),
1649                        )
1650                        .when(no_error, |parent| {
1651                            parent
1652                                .child(
1653                                    h_flex().child(
1654                                        Label::new("Ask and build anything.")
1655                                            .color(Color::Muted)
1656                                            .mb_2p5(),
1657                                    ),
1658                                )
1659                                .child(
1660                                    Button::new("new-thread", "Start New Thread")
1661                                        .icon(IconName::Plus)
1662                                        .icon_position(IconPosition::Start)
1663                                        .icon_size(IconSize::Small)
1664                                        .icon_color(Color::Muted)
1665                                        .full_width()
1666                                        .key_binding(KeyBinding::for_action_in(
1667                                            &NewThread::default(),
1668                                            &focus_handle,
1669                                            window,
1670                                            cx,
1671                                        ))
1672                                        .on_click(|_event, window, cx| {
1673                                            window.dispatch_action(NewThread::default().boxed_clone(), cx)
1674                                        }),
1675                                )
1676                                .child(
1677                                    Button::new("context", "Add Context")
1678                                        .icon(IconName::FileCode)
1679                                        .icon_position(IconPosition::Start)
1680                                        .icon_size(IconSize::Small)
1681                                        .icon_color(Color::Muted)
1682                                        .full_width()
1683                                        .key_binding(KeyBinding::for_action_in(
1684                                            &ToggleContextPicker,
1685                                            &focus_handle,
1686                                            window,
1687                                            cx,
1688                                        ))
1689                                        .on_click(|_event, window, cx| {
1690                                            window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1691                                        }),
1692                                )
1693                                .child(
1694                                    Button::new("mode", "Switch Model")
1695                                        .icon(IconName::DatabaseZap)
1696                                        .icon_position(IconPosition::Start)
1697                                        .icon_size(IconSize::Small)
1698                                        .icon_color(Color::Muted)
1699                                        .full_width()
1700                                        .key_binding(KeyBinding::for_action_in(
1701                                            &ToggleModelSelector,
1702                                            &focus_handle,
1703                                            window,
1704                                            cx,
1705                                        ))
1706                                        .on_click(|_event, window, cx| {
1707                                            window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1708                                        }),
1709                                )
1710                                .child(
1711                                    Button::new("settings", "View Settings")
1712                                        .icon(IconName::Settings)
1713                                        .icon_position(IconPosition::Start)
1714                                        .icon_size(IconSize::Small)
1715                                        .icon_color(Color::Muted)
1716                                        .full_width()
1717                                        .key_binding(KeyBinding::for_action_in(
1718                                            &OpenConfiguration,
1719                                            &focus_handle,
1720                                            window,
1721                                            cx,
1722                                        ))
1723                                        .on_click(|_event, window, cx| {
1724                                            window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1725                                        }),
1726                                )
1727                        })
1728                        .map(|parent| {
1729                            match configuration_error_ref {
1730                                Some(ConfigurationError::ProviderNotAuthenticated)
1731                                | Some(ConfigurationError::NoProvider) => {
1732                                    parent
1733                                        .child(
1734                                            h_flex().child(
1735                                                Label::new("To start using the agent, configure at least one LLM provider.")
1736                                                    .color(Color::Muted)
1737                                                    .mb_2p5()
1738                                            )
1739                                        )
1740                                        .child(
1741                                            Button::new("settings", "Configure a Provider")
1742                                                .icon(IconName::Settings)
1743                                                .icon_position(IconPosition::Start)
1744                                                .icon_size(IconSize::Small)
1745                                                .icon_color(Color::Muted)
1746                                                .full_width()
1747                                                .key_binding(KeyBinding::for_action_in(
1748                                                    &OpenConfiguration,
1749                                                    &focus_handle,
1750                                                    window,
1751                                                    cx,
1752                                                ))
1753                                                .on_click(|_event, window, cx| {
1754                                                    window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1755                                                }),
1756                                        )
1757                                }
1758                                Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1759                                    parent.children(
1760                                        provider.render_accept_terms(
1761                                            LanguageModelProviderTosView::ThreadFreshStart,
1762                                            cx,
1763                                        ),
1764                                    )
1765                                }
1766                                None => parent,
1767                            }
1768                        })
1769                )
1770            })
1771            .when(!recent_history.is_empty(), |parent| {
1772                let focus_handle = focus_handle.clone();
1773                let configuration_error_ref = &configuration_error;
1774
1775                parent
1776                    .overflow_hidden()
1777                    .p_1p5()
1778                    .justify_end()
1779                    .gap_1()
1780                    .child(
1781                        h_flex()
1782                            .pl_1p5()
1783                            .pb_1()
1784                            .w_full()
1785                            .justify_between()
1786                            .border_b_1()
1787                            .border_color(cx.theme().colors().border_variant)
1788                            .child(
1789                                Label::new("Past Interactions")
1790                                    .size(LabelSize::Small)
1791                                    .color(Color::Muted),
1792                            )
1793                            .child(
1794                                Button::new("view-history", "View All")
1795                                    .style(ButtonStyle::Subtle)
1796                                    .label_size(LabelSize::Small)
1797                                    .key_binding(
1798                                        KeyBinding::for_action_in(
1799                                            &OpenHistory,
1800                                            &self.focus_handle(cx),
1801                                            window,
1802                                            cx,
1803                                        ).map(|kb| kb.size(rems_from_px(12.))),
1804                                    )
1805                                    .on_click(move |_event, window, cx| {
1806                                        window.dispatch_action(OpenHistory.boxed_clone(), cx);
1807                                    }),
1808                            ),
1809                    )
1810                    .child(
1811                        v_flex()
1812                            .gap_1()
1813                            .children(
1814                                recent_history.into_iter().map(|entry| {
1815                                    // TODO: Add keyboard navigation.
1816                                    match entry {
1817                                        HistoryEntry::Thread(thread) => {
1818                                            PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1819                                                .into_any_element()
1820                                        }
1821                                        HistoryEntry::Context(context) => {
1822                                            PastContext::new(context, cx.entity().downgrade(), false, vec![])
1823                                                .into_any_element()
1824                                        }
1825                                    }
1826                                }),
1827                            )
1828                    )
1829                    .map(|parent| {
1830                        match configuration_error_ref {
1831                            Some(ConfigurationError::ProviderNotAuthenticated)
1832                            | Some(ConfigurationError::NoProvider) => {
1833                                parent
1834                                    .child(
1835                                        Banner::new()
1836                                            .severity(ui::Severity::Warning)
1837                                            .children(
1838                                                Label::new(
1839                                                    "Configure at least one LLM provider to start using the panel.",
1840                                                )
1841                                                .size(LabelSize::Small),
1842                                            )
1843                                            .action_slot(
1844                                                Button::new("settings", "Configure Provider")
1845                                                    .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1846                                                    .label_size(LabelSize::Small)
1847                                                    .key_binding(
1848                                                        KeyBinding::for_action_in(
1849                                                            &OpenConfiguration,
1850                                                            &focus_handle,
1851                                                            window,
1852                                                            cx,
1853                                                        )
1854                                                        .map(|kb| kb.size(rems_from_px(12.))),
1855                                                    )
1856                                                    .on_click(|_event, window, cx| {
1857                                                        window.dispatch_action(
1858                                                            OpenConfiguration.boxed_clone(),
1859                                                            cx,
1860                                                        )
1861                                                    }),
1862                                            ),
1863                                    )
1864                            }
1865                            Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1866                                parent
1867                                    .child(
1868                                        Banner::new()
1869                                            .severity(ui::Severity::Warning)
1870                                            .children(
1871                                                h_flex()
1872                                                    .w_full()
1873                                                    .children(
1874                                                        provider.render_accept_terms(
1875                                                            LanguageModelProviderTosView::ThreadtEmptyState,
1876                                                            cx,
1877                                                        ),
1878                                                    ),
1879                                            ),
1880                                    )
1881                            }
1882                            None => parent,
1883                        }
1884                    })
1885            })
1886    }
1887
1888    fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1889        let plan = self
1890            .user_store
1891            .read(cx)
1892            .current_plan()
1893            .map(|plan| match plan {
1894                Plan::Free => zed_llm_client::Plan::Free,
1895                Plan::ZedPro => zed_llm_client::Plan::ZedPro,
1896                Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
1897            })
1898            .unwrap_or(zed_llm_client::Plan::Free);
1899        let usage = self.thread.read(cx).last_usage()?;
1900
1901        Some(UsageBanner::new(plan, usage).into_any_element())
1902    }
1903
1904    fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1905        let last_error = self.thread.read(cx).last_error()?;
1906
1907        Some(
1908            div()
1909                .absolute()
1910                .right_3()
1911                .bottom_12()
1912                .max_w_96()
1913                .py_2()
1914                .px_3()
1915                .elevation_2(cx)
1916                .occlude()
1917                .child(match last_error {
1918                    ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1919                    ThreadError::MaxMonthlySpendReached => {
1920                        self.render_max_monthly_spend_reached_error(cx)
1921                    }
1922                    ThreadError::ModelRequestLimitReached { plan } => {
1923                        self.render_model_request_limit_reached_error(plan, cx)
1924                    }
1925                    ThreadError::Message { header, message } => {
1926                        self.render_error_message(header, message, cx)
1927                    }
1928                })
1929                .into_any(),
1930        )
1931    }
1932
1933    fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1934        const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
1935
1936        v_flex()
1937            .gap_0p5()
1938            .child(
1939                h_flex()
1940                    .gap_1p5()
1941                    .items_center()
1942                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1943                    .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1944            )
1945            .child(
1946                div()
1947                    .id("error-message")
1948                    .max_h_24()
1949                    .overflow_y_scroll()
1950                    .child(Label::new(ERROR_MESSAGE)),
1951            )
1952            .child(
1953                h_flex()
1954                    .justify_end()
1955                    .mt_1()
1956                    .gap_1()
1957                    .child(self.create_copy_button(ERROR_MESSAGE))
1958                    .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1959                        |this, _, _, cx| {
1960                            this.thread.update(cx, |this, _cx| {
1961                                this.clear_last_error();
1962                            });
1963
1964                            cx.open_url(&zed_urls::account_url(cx));
1965                            cx.notify();
1966                        },
1967                    )))
1968                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1969                        |this, _, _, cx| {
1970                            this.thread.update(cx, |this, _cx| {
1971                                this.clear_last_error();
1972                            });
1973
1974                            cx.notify();
1975                        },
1976                    ))),
1977            )
1978            .into_any()
1979    }
1980
1981    fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1982        const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1983
1984        v_flex()
1985            .gap_0p5()
1986            .child(
1987                h_flex()
1988                    .gap_1p5()
1989                    .items_center()
1990                    .child(Icon::new(IconName::XCircle).color(Color::Error))
1991                    .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1992            )
1993            .child(
1994                div()
1995                    .id("error-message")
1996                    .max_h_24()
1997                    .overflow_y_scroll()
1998                    .child(Label::new(ERROR_MESSAGE)),
1999            )
2000            .child(
2001                h_flex()
2002                    .justify_end()
2003                    .mt_1()
2004                    .gap_1()
2005                    .child(self.create_copy_button(ERROR_MESSAGE))
2006                    .child(
2007                        Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2008                            cx.listener(|this, _, _, cx| {
2009                                this.thread.update(cx, |this, _cx| {
2010                                    this.clear_last_error();
2011                                });
2012
2013                                cx.open_url(&zed_urls::account_url(cx));
2014                                cx.notify();
2015                            }),
2016                        ),
2017                    )
2018                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2019                        |this, _, _, cx| {
2020                            this.thread.update(cx, |this, _cx| {
2021                                this.clear_last_error();
2022                            });
2023
2024                            cx.notify();
2025                        },
2026                    ))),
2027            )
2028            .into_any()
2029    }
2030
2031    fn render_model_request_limit_reached_error(
2032        &self,
2033        plan: Plan,
2034        cx: &mut Context<Self>,
2035    ) -> AnyElement {
2036        let error_message = match plan {
2037            Plan::ZedPro => {
2038                "Model request limit reached. Upgrade to usage-based billing for more requests."
2039            }
2040            Plan::ZedProTrial => {
2041                "Model request limit reached. Upgrade to Zed Pro for more requests."
2042            }
2043            Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2044        };
2045        let call_to_action = match plan {
2046            Plan::ZedPro => "Upgrade to usage-based billing",
2047            Plan::ZedProTrial => "Upgrade to Zed Pro",
2048            Plan::Free => "Upgrade to Zed Pro",
2049        };
2050
2051        v_flex()
2052            .gap_0p5()
2053            .child(
2054                h_flex()
2055                    .gap_1p5()
2056                    .items_center()
2057                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2058                    .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2059            )
2060            .child(
2061                div()
2062                    .id("error-message")
2063                    .max_h_24()
2064                    .overflow_y_scroll()
2065                    .child(Label::new(error_message)),
2066            )
2067            .child(
2068                h_flex()
2069                    .justify_end()
2070                    .mt_1()
2071                    .gap_1()
2072                    .child(self.create_copy_button(error_message))
2073                    .child(
2074                        Button::new("subscribe", call_to_action).on_click(cx.listener(
2075                            |this, _, _, cx| {
2076                                this.thread.update(cx, |this, _cx| {
2077                                    this.clear_last_error();
2078                                });
2079
2080                                cx.open_url(&zed_urls::account_url(cx));
2081                                cx.notify();
2082                            },
2083                        )),
2084                    )
2085                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2086                        |this, _, _, cx| {
2087                            this.thread.update(cx, |this, _cx| {
2088                                this.clear_last_error();
2089                            });
2090
2091                            cx.notify();
2092                        },
2093                    ))),
2094            )
2095            .into_any()
2096    }
2097
2098    fn render_error_message(
2099        &self,
2100        header: SharedString,
2101        message: SharedString,
2102        cx: &mut Context<Self>,
2103    ) -> AnyElement {
2104        let message_with_header = format!("{}\n{}", header, message);
2105        v_flex()
2106            .gap_0p5()
2107            .child(
2108                h_flex()
2109                    .gap_1p5()
2110                    .items_center()
2111                    .child(Icon::new(IconName::XCircle).color(Color::Error))
2112                    .child(Label::new(header).weight(FontWeight::MEDIUM)),
2113            )
2114            .child(
2115                div()
2116                    .id("error-message")
2117                    .max_h_32()
2118                    .overflow_y_scroll()
2119                    .child(Label::new(message.clone())),
2120            )
2121            .child(
2122                h_flex()
2123                    .justify_end()
2124                    .mt_1()
2125                    .gap_1()
2126                    .child(self.create_copy_button(message_with_header))
2127                    .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2128                        |this, _, _, cx| {
2129                            this.thread.update(cx, |this, _cx| {
2130                                this.clear_last_error();
2131                            });
2132
2133                            cx.notify();
2134                        },
2135                    ))),
2136            )
2137            .into_any()
2138    }
2139
2140    fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2141        let message = message.into();
2142        IconButton::new("copy", IconName::Copy)
2143            .on_click(move |_, _, cx| {
2144                cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2145            })
2146            .tooltip(Tooltip::text("Copy Error Message"))
2147    }
2148
2149    fn key_context(&self) -> KeyContext {
2150        let mut key_context = KeyContext::new_with_defaults();
2151        key_context.add("AgentPanel");
2152        if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2153            key_context.add("prompt_editor");
2154        }
2155        key_context
2156    }
2157}
2158
2159impl Render for AssistantPanel {
2160    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2161        v_flex()
2162            .key_context(self.key_context())
2163            .justify_between()
2164            .size_full()
2165            .on_action(cx.listener(Self::cancel))
2166            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2167                this.new_thread(action, window, cx);
2168            }))
2169            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2170                this.open_history(window, cx);
2171            }))
2172            .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2173                this.open_configuration(window, cx);
2174            }))
2175            .on_action(cx.listener(Self::open_active_thread_as_markdown))
2176            .on_action(cx.listener(Self::deploy_rules_library))
2177            .on_action(cx.listener(Self::open_agent_diff))
2178            .on_action(cx.listener(Self::go_back))
2179            .on_action(cx.listener(Self::toggle_navigation_menu))
2180            .on_action(cx.listener(Self::toggle_options_menu))
2181            .child(self.render_toolbar(window, cx))
2182            .map(|parent| match &self.active_view {
2183                ActiveView::Thread { .. } => parent
2184                    .child(self.render_active_thread_or_empty_state(window, cx))
2185                    .children(self.render_usage_banner(cx))
2186                    .child(h_flex().child(self.message_editor.clone()))
2187                    .children(self.render_last_error(cx)),
2188                ActiveView::History => parent.child(self.history.clone()),
2189                ActiveView::PromptEditor { context_editor, .. } => {
2190                    parent.child(context_editor.clone())
2191                }
2192                ActiveView::Configuration => parent.children(self.configuration.clone()),
2193            })
2194    }
2195}
2196
2197struct PromptLibraryInlineAssist {
2198    workspace: WeakEntity<Workspace>,
2199}
2200
2201impl PromptLibraryInlineAssist {
2202    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2203        Self { workspace }
2204    }
2205}
2206
2207impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2208    fn assist(
2209        &self,
2210        prompt_editor: &Entity<Editor>,
2211        _initial_prompt: Option<String>,
2212        window: &mut Window,
2213        cx: &mut Context<RulesLibrary>,
2214    ) {
2215        InlineAssistant::update_global(cx, |assistant, cx| {
2216            let Some(project) = self
2217                .workspace
2218                .upgrade()
2219                .map(|workspace| workspace.read(cx).project().downgrade())
2220            else {
2221                return;
2222            };
2223            let prompt_store = None;
2224            let thread_store = None;
2225            assistant.assist(
2226                &prompt_editor,
2227                self.workspace.clone(),
2228                project,
2229                prompt_store,
2230                thread_store,
2231                window,
2232                cx,
2233            )
2234        })
2235    }
2236
2237    fn focus_assistant_panel(
2238        &self,
2239        workspace: &mut Workspace,
2240        window: &mut Window,
2241        cx: &mut Context<Workspace>,
2242    ) -> bool {
2243        workspace
2244            .focus_panel::<AssistantPanel>(window, cx)
2245            .is_some()
2246    }
2247}
2248
2249pub struct ConcreteAssistantPanelDelegate;
2250
2251impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2252    fn active_context_editor(
2253        &self,
2254        workspace: &mut Workspace,
2255        _window: &mut Window,
2256        cx: &mut Context<Workspace>,
2257    ) -> Option<Entity<ContextEditor>> {
2258        let panel = workspace.panel::<AssistantPanel>(cx)?;
2259        panel.read(cx).active_context_editor()
2260    }
2261
2262    fn open_saved_context(
2263        &self,
2264        workspace: &mut Workspace,
2265        path: Arc<Path>,
2266        window: &mut Window,
2267        cx: &mut Context<Workspace>,
2268    ) -> Task<Result<()>> {
2269        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2270            return Task::ready(Err(anyhow!("Agent panel not found")));
2271        };
2272
2273        panel.update(cx, |panel, cx| {
2274            panel.open_saved_prompt_editor(path, window, cx)
2275        })
2276    }
2277
2278    fn open_remote_context(
2279        &self,
2280        _workspace: &mut Workspace,
2281        _context_id: assistant_context_editor::ContextId,
2282        _window: &mut Window,
2283        _cx: &mut Context<Workspace>,
2284    ) -> Task<Result<Entity<ContextEditor>>> {
2285        Task::ready(Err(anyhow!("opening remote context not implemented")))
2286    }
2287
2288    fn quote_selection(
2289        &self,
2290        workspace: &mut Workspace,
2291        selection_ranges: Vec<Range<Anchor>>,
2292        buffer: Entity<MultiBuffer>,
2293        window: &mut Window,
2294        cx: &mut Context<Workspace>,
2295    ) {
2296        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2297            return;
2298        };
2299
2300        if !panel.focus_handle(cx).contains_focused(window, cx) {
2301            workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2302        }
2303
2304        panel.update(cx, |_, cx| {
2305            // Wait to create a new context until the workspace is no longer
2306            // being updated.
2307            cx.defer_in(window, move |panel, window, cx| {
2308                if panel.has_active_thread() {
2309                    panel.message_editor.update(cx, |message_editor, cx| {
2310                        message_editor.context_store().update(cx, |store, cx| {
2311                            let buffer = buffer.read(cx);
2312                            let selection_ranges = selection_ranges
2313                                .into_iter()
2314                                .flat_map(|range| {
2315                                    let (start_buffer, start) =
2316                                        buffer.text_anchor_for_position(range.start, cx)?;
2317                                    let (end_buffer, end) =
2318                                        buffer.text_anchor_for_position(range.end, cx)?;
2319                                    if start_buffer != end_buffer {
2320                                        return None;
2321                                    }
2322                                    Some((start_buffer, start..end))
2323                                })
2324                                .collect::<Vec<_>>();
2325
2326                            for (buffer, range) in selection_ranges {
2327                                store.add_selection(buffer, range, cx);
2328                            }
2329                        })
2330                    })
2331                } else if let Some(context_editor) = panel.active_context_editor() {
2332                    let snapshot = buffer.read(cx).snapshot(cx);
2333                    let selection_ranges = selection_ranges
2334                        .into_iter()
2335                        .map(|range| range.to_point(&snapshot))
2336                        .collect::<Vec<_>>();
2337
2338                    context_editor.update(cx, |context_editor, cx| {
2339                        context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2340                    });
2341                }
2342            });
2343        });
2344    }
2345}