1use std::cell::RefCell;
2use std::ops::{Not, Range};
3use std::path::Path;
4use std::rc::Rc;
5use std::sync::Arc;
6use std::time::Duration;
7
8use agent_servers::AgentServer;
9use db::kvp::{Dismissable, KEY_VALUE_STORE};
10use serde::{Deserialize, Serialize};
11
12use crate::NewExternalAgentThread;
13use crate::agent_diff::AgentDiffThread;
14use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
15use crate::ui::NewThreadButton;
16use crate::{
17 AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
18 DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
19 NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell,
20 ResetTrialUpsell, ToggleBurnMode, ToggleContextPicker, ToggleNavigationMenu, ToggleOptionsMenu,
21 acp::AcpThreadView,
22 active_thread::{self, ActiveThread, ActiveThreadEvent},
23 agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
24 agent_diff::AgentDiff,
25 message_editor::{MessageEditor, MessageEditorEvent},
26 slash_command::SlashCommandCompletionProvider,
27 text_thread_editor::{
28 AgentPanelDelegate, TextThreadEditor, humanize_token_count, make_lsp_adapter_delegate,
29 render_remaining_tokens,
30 },
31 thread_history::{HistoryEntryElement, ThreadHistory},
32 ui::{AgentOnboardingModal, EndTrialUpsell},
33};
34use agent::{
35 Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
36 context_store::ContextStore,
37 history_store::{HistoryEntryId, HistoryStore},
38 thread_store::{TextThreadStore, ThreadStore},
39};
40use agent_settings::{AgentDockPosition, AgentSettings, CompletionMode, DefaultView};
41use ai_onboarding::AgentPanelOnboarding;
42use anyhow::{Result, anyhow};
43use assistant_context::{AssistantContext, ContextEvent, ContextSummary};
44use assistant_slash_command::SlashCommandWorkingSet;
45use assistant_tool::ToolWorkingSet;
46use client::{UserStore, zed_urls};
47use cloud_llm_client::{CompletionIntent, Plan, UsageLimit};
48use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
49use feature_flags::{self, FeatureFlagAppExt};
50use fs::Fs;
51use gpui::{
52 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
53 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
54 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*,
55 pulsating_between,
56};
57use language::LanguageRegistry;
58use language_model::{
59 ConfigurationError, ConfiguredModel, LanguageModelProviderTosView, LanguageModelRegistry,
60};
61use project::{DisableAiSettings, Project, ProjectPath, Worktree};
62use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
63use rules_library::{RulesLibrary, open_rules_library};
64use search::{BufferSearchBar, buffer_search};
65use settings::{Settings, update_settings_file};
66use theme::ThemeSettings;
67use time::UtcOffset;
68use ui::utils::WithRemSize;
69use ui::{
70 Banner, Callout, ContextMenu, ContextMenuEntry, ElevationIndex, KeyBinding, PopoverMenu,
71 PopoverMenuHandle, ProgressBar, Tab, Tooltip, prelude::*,
72};
73use util::ResultExt as _;
74use workspace::{
75 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
76 dock::{DockPosition, Panel, PanelEvent},
77};
78use zed_actions::{
79 DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
80 agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
81 assistant::{OpenRulesLibrary, ToggleFocus},
82};
83
84const AGENT_PANEL_KEY: &str = "agent_panel";
85
86#[derive(Serialize, Deserialize)]
87struct SerializedAgentPanel {
88 width: Option<Pixels>,
89}
90
91pub fn init(cx: &mut App) {
92 cx.observe_new(
93 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
94 workspace
95 .register_action(|workspace, action: &NewThread, window, cx| {
96 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
97 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
98 workspace.focus_panel::<AgentPanel>(window, cx);
99 }
100 })
101 .register_action(|workspace, _: &OpenHistory, window, cx| {
102 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
103 workspace.focus_panel::<AgentPanel>(window, cx);
104 panel.update(cx, |panel, cx| panel.open_history(window, cx));
105 }
106 })
107 .register_action(|workspace, _: &OpenSettings, window, cx| {
108 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
109 workspace.focus_panel::<AgentPanel>(window, cx);
110 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
111 }
112 })
113 .register_action(|workspace, _: &NewTextThread, window, cx| {
114 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
115 workspace.focus_panel::<AgentPanel>(window, cx);
116 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
117 }
118 })
119 .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
120 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
121 workspace.focus_panel::<AgentPanel>(window, cx);
122 panel.update(cx, |panel, cx| {
123 panel.new_external_thread(action.agent, window, cx)
124 });
125 }
126 })
127 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
128 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
129 workspace.focus_panel::<AgentPanel>(window, cx);
130 panel.update(cx, |panel, cx| {
131 panel.deploy_rules_library(action, window, cx)
132 });
133 }
134 })
135 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
136 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
137 workspace.focus_panel::<AgentPanel>(window, cx);
138 match &panel.read(cx).active_view {
139 ActiveView::Thread { thread, .. } => {
140 let thread = thread.read(cx).thread().clone();
141 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
142 }
143 ActiveView::ExternalAgentThread { .. }
144 | ActiveView::TextThread { .. }
145 | ActiveView::History
146 | ActiveView::Configuration => {}
147 }
148 }
149 })
150 .register_action(|workspace, _: &Follow, window, cx| {
151 workspace.follow(CollaboratorId::Agent, window, cx);
152 })
153 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
154 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
155 return;
156 };
157 workspace.focus_panel::<AgentPanel>(window, cx);
158 panel.update(cx, |panel, cx| {
159 if let Some(message_editor) = panel.active_message_editor() {
160 message_editor.update(cx, |editor, cx| {
161 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
162 });
163 }
164 });
165 })
166 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
167 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
168 workspace.focus_panel::<AgentPanel>(window, cx);
169 panel.update(cx, |panel, cx| {
170 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
171 });
172 }
173 })
174 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
175 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
176 workspace.focus_panel::<AgentPanel>(window, cx);
177 panel.update(cx, |panel, cx| {
178 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
179 });
180 }
181 })
182 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
183 AgentOnboardingModal::toggle(workspace, window, cx)
184 })
185 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
186 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
187 window.refresh();
188 })
189 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
190 OnboardingUpsell::set_dismissed(false, cx);
191 })
192 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
193 TrialEndUpsell::set_dismissed(false, cx);
194 });
195 },
196 )
197 .detach();
198}
199
200enum ActiveView {
201 Thread {
202 thread: Entity<ActiveThread>,
203 change_title_editor: Entity<Editor>,
204 message_editor: Entity<MessageEditor>,
205 _subscriptions: Vec<gpui::Subscription>,
206 },
207 ExternalAgentThread {
208 thread_view: Entity<AcpThreadView>,
209 },
210 TextThread {
211 context_editor: Entity<TextThreadEditor>,
212 title_editor: Entity<Editor>,
213 buffer_search_bar: Entity<BufferSearchBar>,
214 _subscriptions: Vec<gpui::Subscription>,
215 },
216 History,
217 Configuration,
218}
219
220enum WhichFontSize {
221 AgentFont,
222 BufferFont,
223 None,
224}
225
226impl ActiveView {
227 pub fn which_font_size_used(&self) -> WhichFontSize {
228 match self {
229 ActiveView::Thread { .. }
230 | ActiveView::ExternalAgentThread { .. }
231 | ActiveView::History => WhichFontSize::AgentFont,
232 ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
233 ActiveView::Configuration => WhichFontSize::None,
234 }
235 }
236
237 pub fn thread(
238 active_thread: Entity<ActiveThread>,
239 message_editor: Entity<MessageEditor>,
240 window: &mut Window,
241 cx: &mut Context<AgentPanel>,
242 ) -> Self {
243 let summary = active_thread.read(cx).summary(cx).or_default();
244
245 let editor = cx.new(|cx| {
246 let mut editor = Editor::single_line(window, cx);
247 editor.set_text(summary.clone(), window, cx);
248 editor
249 });
250
251 let subscriptions = vec![
252 cx.subscribe(&message_editor, |this, _, event, cx| match event {
253 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
254 cx.notify();
255 }
256 MessageEditorEvent::ScrollThreadToBottom => match &this.active_view {
257 ActiveView::Thread { thread, .. } => {
258 thread.update(cx, |thread, cx| {
259 thread.scroll_to_bottom(cx);
260 });
261 }
262 ActiveView::ExternalAgentThread { .. } => {}
263 ActiveView::TextThread { .. }
264 | ActiveView::History
265 | ActiveView::Configuration => {}
266 },
267 }),
268 window.subscribe(&editor, cx, {
269 {
270 let thread = active_thread.clone();
271 move |editor, event, window, cx| match event {
272 EditorEvent::BufferEdited => {
273 let new_summary = editor.read(cx).text(cx);
274
275 thread.update(cx, |thread, cx| {
276 thread.thread().update(cx, |thread, cx| {
277 thread.set_summary(new_summary, cx);
278 });
279 })
280 }
281 EditorEvent::Blurred => {
282 if editor.read(cx).text(cx).is_empty() {
283 let summary = thread.read(cx).summary(cx).or_default();
284
285 editor.update(cx, |editor, cx| {
286 editor.set_text(summary, window, cx);
287 });
288 }
289 }
290 _ => {}
291 }
292 }
293 }),
294 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
295 ActiveThreadEvent::EditingMessageTokenCountChanged => {
296 cx.notify();
297 }
298 }),
299 cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
300 let editor = editor.clone();
301 move |_, thread, event, window, cx| match event {
302 ThreadEvent::SummaryGenerated => {
303 let summary = thread.read(cx).summary().or_default();
304
305 editor.update(cx, |editor, cx| {
306 editor.set_text(summary, window, cx);
307 })
308 }
309 ThreadEvent::MessageAdded(_) => {
310 cx.notify();
311 }
312 _ => {}
313 }
314 }),
315 ];
316
317 Self::Thread {
318 change_title_editor: editor,
319 thread: active_thread,
320 message_editor: message_editor,
321 _subscriptions: subscriptions,
322 }
323 }
324
325 pub fn prompt_editor(
326 context_editor: Entity<TextThreadEditor>,
327 history_store: Entity<HistoryStore>,
328 language_registry: Arc<LanguageRegistry>,
329 window: &mut Window,
330 cx: &mut App,
331 ) -> Self {
332 let title = context_editor.read(cx).title(cx).to_string();
333
334 let editor = cx.new(|cx| {
335 let mut editor = Editor::single_line(window, cx);
336 editor.set_text(title, window, cx);
337 editor
338 });
339
340 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
341 // cause a custom summary to be set. The presence of this custom summary would cause
342 // summarization to not happen.
343 let mut suppress_first_edit = true;
344
345 let subscriptions = vec![
346 window.subscribe(&editor, cx, {
347 {
348 let context_editor = context_editor.clone();
349 move |editor, event, window, cx| match event {
350 EditorEvent::BufferEdited => {
351 if suppress_first_edit {
352 suppress_first_edit = false;
353 return;
354 }
355 let new_summary = editor.read(cx).text(cx);
356
357 context_editor.update(cx, |context_editor, cx| {
358 context_editor
359 .context()
360 .update(cx, |assistant_context, cx| {
361 assistant_context.set_custom_summary(new_summary, cx);
362 })
363 })
364 }
365 EditorEvent::Blurred => {
366 if editor.read(cx).text(cx).is_empty() {
367 let summary = context_editor
368 .read(cx)
369 .context()
370 .read(cx)
371 .summary()
372 .or_default();
373
374 editor.update(cx, |editor, cx| {
375 editor.set_text(summary, window, cx);
376 });
377 }
378 }
379 _ => {}
380 }
381 }
382 }),
383 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
384 let editor = editor.clone();
385 move |assistant_context, event, window, cx| match event {
386 ContextEvent::SummaryGenerated => {
387 let summary = assistant_context.read(cx).summary().or_default();
388
389 editor.update(cx, |editor, cx| {
390 editor.set_text(summary, window, cx);
391 })
392 }
393 ContextEvent::PathChanged { old_path, new_path } => {
394 history_store.update(cx, |history_store, cx| {
395 if let Some(old_path) = old_path {
396 history_store
397 .replace_recently_opened_text_thread(old_path, new_path, cx);
398 } else {
399 history_store.push_recently_opened_entry(
400 HistoryEntryId::Context(new_path.clone()),
401 cx,
402 );
403 }
404 });
405 }
406 _ => {}
407 }
408 }),
409 ];
410
411 let buffer_search_bar =
412 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
413 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
414 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
415 });
416
417 Self::TextThread {
418 context_editor,
419 title_editor: editor,
420 buffer_search_bar,
421 _subscriptions: subscriptions,
422 }
423 }
424}
425
426pub struct AgentPanel {
427 workspace: WeakEntity<Workspace>,
428 user_store: Entity<UserStore>,
429 project: Entity<Project>,
430 fs: Arc<dyn Fs>,
431 language_registry: Arc<LanguageRegistry>,
432 thread_store: Entity<ThreadStore>,
433 _default_model_subscription: Subscription,
434 context_store: Entity<TextThreadStore>,
435 prompt_store: Option<Entity<PromptStore>>,
436 inline_assist_context_store: Entity<ContextStore>,
437 configuration: Option<Entity<AgentConfiguration>>,
438 configuration_subscription: Option<Subscription>,
439 local_timezone: UtcOffset,
440 active_view: ActiveView,
441 acp_message_history:
442 Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
443 previous_view: Option<ActiveView>,
444 history_store: Entity<HistoryStore>,
445 history: Entity<ThreadHistory>,
446 hovered_recent_history_item: Option<usize>,
447 new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
448 agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
449 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
450 assistant_navigation_menu: Option<Entity<ContextMenu>>,
451 width: Option<Pixels>,
452 height: Option<Pixels>,
453 zoomed: bool,
454 pending_serialization: Option<Task<Result<()>>>,
455 onboarding: Entity<AgentPanelOnboarding>,
456}
457
458impl AgentPanel {
459 fn serialize(&mut self, cx: &mut Context<Self>) {
460 let width = self.width;
461 self.pending_serialization = Some(cx.background_spawn(async move {
462 KEY_VALUE_STORE
463 .write_kvp(
464 AGENT_PANEL_KEY.into(),
465 serde_json::to_string(&SerializedAgentPanel { width })?,
466 )
467 .await?;
468 anyhow::Ok(())
469 }));
470 }
471 pub fn load(
472 workspace: WeakEntity<Workspace>,
473 prompt_builder: Arc<PromptBuilder>,
474 mut cx: AsyncWindowContext,
475 ) -> Task<Result<Entity<Self>>> {
476 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
477 cx.spawn(async move |cx| {
478 let prompt_store = match prompt_store {
479 Ok(prompt_store) => prompt_store.await.ok(),
480 Err(_) => None,
481 };
482 let tools = cx.new(|_| ToolWorkingSet::default())?;
483 let thread_store = workspace
484 .update(cx, |workspace, cx| {
485 let project = workspace.project().clone();
486 ThreadStore::load(
487 project,
488 tools.clone(),
489 prompt_store.clone(),
490 prompt_builder.clone(),
491 cx,
492 )
493 })?
494 .await?;
495
496 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
497 let context_store = workspace
498 .update(cx, |workspace, cx| {
499 let project = workspace.project().clone();
500 assistant_context::ContextStore::new(
501 project,
502 prompt_builder.clone(),
503 slash_commands,
504 cx,
505 )
506 })?
507 .await?;
508
509 let serialized_panel = if let Some(panel) = cx
510 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
511 .await
512 .log_err()
513 .flatten()
514 {
515 Some(serde_json::from_str::<SerializedAgentPanel>(&panel)?)
516 } else {
517 None
518 };
519
520 let panel = workspace.update_in(cx, |workspace, window, cx| {
521 let panel = cx.new(|cx| {
522 Self::new(
523 workspace,
524 thread_store,
525 context_store,
526 prompt_store,
527 window,
528 cx,
529 )
530 });
531 if let Some(serialized_panel) = serialized_panel {
532 panel.update(cx, |panel, cx| {
533 panel.width = serialized_panel.width.map(|w| w.round());
534 cx.notify();
535 });
536 }
537 panel
538 })?;
539
540 Ok(panel)
541 })
542 }
543
544 fn new(
545 workspace: &Workspace,
546 thread_store: Entity<ThreadStore>,
547 context_store: Entity<TextThreadStore>,
548 prompt_store: Option<Entity<PromptStore>>,
549 window: &mut Window,
550 cx: &mut Context<Self>,
551 ) -> Self {
552 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
553 let fs = workspace.app_state().fs.clone();
554 let user_store = workspace.app_state().user_store.clone();
555 let project = workspace.project();
556 let language_registry = project.read(cx).languages().clone();
557 let client = workspace.client().clone();
558 let workspace = workspace.weak_handle();
559 let weak_self = cx.entity().downgrade();
560
561 let message_editor_context_store =
562 cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
563 let inline_assist_context_store =
564 cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
565
566 let thread_id = thread.read(cx).id().clone();
567
568 let history_store = cx.new(|cx| {
569 HistoryStore::new(
570 thread_store.clone(),
571 context_store.clone(),
572 [HistoryEntryId::Thread(thread_id)],
573 cx,
574 )
575 });
576
577 let message_editor = cx.new(|cx| {
578 MessageEditor::new(
579 fs.clone(),
580 workspace.clone(),
581 message_editor_context_store.clone(),
582 prompt_store.clone(),
583 thread_store.downgrade(),
584 context_store.downgrade(),
585 Some(history_store.downgrade()),
586 thread.clone(),
587 window,
588 cx,
589 )
590 });
591
592 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
593
594 let active_thread = cx.new(|cx| {
595 ActiveThread::new(
596 thread.clone(),
597 thread_store.clone(),
598 context_store.clone(),
599 message_editor_context_store.clone(),
600 language_registry.clone(),
601 workspace.clone(),
602 window,
603 cx,
604 )
605 });
606
607 let panel_type = AgentSettings::get_global(cx).default_view;
608 let active_view = match panel_type {
609 DefaultView::Thread => ActiveView::thread(active_thread, message_editor, window, cx),
610 DefaultView::TextThread => {
611 let context =
612 context_store.update(cx, |context_store, cx| context_store.create(cx));
613 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project.clone(), cx).unwrap();
614 let context_editor = cx.new(|cx| {
615 let mut editor = TextThreadEditor::for_context(
616 context,
617 fs.clone(),
618 workspace.clone(),
619 project.clone(),
620 lsp_adapter_delegate,
621 window,
622 cx,
623 );
624 editor.insert_default_prompt(window, cx);
625 editor
626 });
627 ActiveView::prompt_editor(
628 context_editor,
629 history_store.clone(),
630 language_registry.clone(),
631 window,
632 cx,
633 )
634 }
635 };
636
637 AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
638
639 let weak_panel = weak_self.clone();
640
641 window.defer(cx, move |window, cx| {
642 let panel = weak_panel.clone();
643 let assistant_navigation_menu =
644 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
645 if let Some(panel) = panel.upgrade() {
646 menu = Self::populate_recently_opened_menu_section(menu, panel, cx);
647 }
648 menu.action("View All", Box::new(OpenHistory))
649 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
650 .fixed_width(px(320.).into())
651 .keep_open_on_confirm(false)
652 .key_context("NavigationMenu")
653 });
654 weak_panel
655 .update(cx, |panel, cx| {
656 cx.subscribe_in(
657 &assistant_navigation_menu,
658 window,
659 |_, menu, _: &DismissEvent, window, cx| {
660 menu.update(cx, |menu, _| {
661 menu.clear_selected();
662 });
663 cx.focus_self(window);
664 },
665 )
666 .detach();
667 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
668 })
669 .ok();
670 });
671
672 let _default_model_subscription = cx.subscribe(
673 &LanguageModelRegistry::global(cx),
674 |this, _, event: &language_model::Event, cx| match event {
675 language_model::Event::DefaultModelChanged => match &this.active_view {
676 ActiveView::Thread { thread, .. } => {
677 thread
678 .read(cx)
679 .thread()
680 .clone()
681 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
682 }
683 ActiveView::ExternalAgentThread { .. }
684 | ActiveView::TextThread { .. }
685 | ActiveView::History
686 | ActiveView::Configuration => {}
687 },
688 _ => {}
689 },
690 );
691
692 let onboarding = cx.new(|cx| {
693 AgentPanelOnboarding::new(
694 user_store.clone(),
695 client,
696 |_window, cx| {
697 OnboardingUpsell::set_dismissed(true, cx);
698 },
699 cx,
700 )
701 });
702
703 Self {
704 active_view,
705 workspace,
706 user_store,
707 project: project.clone(),
708 fs: fs.clone(),
709 language_registry,
710 thread_store: thread_store.clone(),
711 _default_model_subscription,
712 context_store,
713 prompt_store,
714 configuration: None,
715 configuration_subscription: None,
716 local_timezone: UtcOffset::from_whole_seconds(
717 chrono::Local::now().offset().local_minus_utc(),
718 )
719 .unwrap(),
720 inline_assist_context_store,
721 previous_view: None,
722 acp_message_history: Default::default(),
723 history_store: history_store.clone(),
724 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
725 hovered_recent_history_item: None,
726 new_thread_menu_handle: PopoverMenuHandle::default(),
727 agent_panel_menu_handle: PopoverMenuHandle::default(),
728 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
729 assistant_navigation_menu: None,
730 width: None,
731 height: None,
732 zoomed: false,
733 pending_serialization: None,
734 onboarding,
735 }
736 }
737
738 pub fn toggle_focus(
739 workspace: &mut Workspace,
740 _: &ToggleFocus,
741 window: &mut Window,
742 cx: &mut Context<Workspace>,
743 ) {
744 if workspace
745 .panel::<Self>(cx)
746 .is_some_and(|panel| panel.read(cx).enabled(cx))
747 && !DisableAiSettings::get_global(cx).disable_ai
748 {
749 workspace.toggle_panel_focus::<Self>(window, cx);
750 }
751 }
752
753 pub(crate) fn local_timezone(&self) -> UtcOffset {
754 self.local_timezone
755 }
756
757 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
758 &self.prompt_store
759 }
760
761 pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
762 &self.inline_assist_context_store
763 }
764
765 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
766 &self.thread_store
767 }
768
769 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
770 &self.context_store
771 }
772
773 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
774 match &self.active_view {
775 ActiveView::Thread { thread, .. } => {
776 thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
777 }
778 ActiveView::ExternalAgentThread { thread_view, .. } => {
779 thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
780 }
781 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
782 }
783 }
784
785 fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
786 match &self.active_view {
787 ActiveView::Thread { message_editor, .. } => Some(message_editor),
788 ActiveView::ExternalAgentThread { .. }
789 | ActiveView::TextThread { .. }
790 | ActiveView::History
791 | ActiveView::Configuration => None,
792 }
793 }
794
795 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
796 // Preserve chat box text when using creating new thread
797 let preserved_text = self
798 .active_message_editor()
799 .map(|editor| editor.read(cx).get_text(cx).trim().to_string());
800
801 let thread = self
802 .thread_store
803 .update(cx, |this, cx| this.create_thread(cx));
804
805 let context_store = cx.new(|_cx| {
806 ContextStore::new(
807 self.project.downgrade(),
808 Some(self.thread_store.downgrade()),
809 )
810 });
811
812 if let Some(other_thread_id) = action.from_thread_id.clone() {
813 let other_thread_task = self.thread_store.update(cx, |this, cx| {
814 this.open_thread(&other_thread_id, window, cx)
815 });
816
817 cx.spawn({
818 let context_store = context_store.clone();
819
820 async move |_panel, cx| {
821 let other_thread = other_thread_task.await?;
822
823 context_store.update(cx, |this, cx| {
824 this.add_thread(other_thread, false, cx);
825 })?;
826 anyhow::Ok(())
827 }
828 })
829 .detach_and_log_err(cx);
830 }
831
832 let active_thread = cx.new(|cx| {
833 ActiveThread::new(
834 thread.clone(),
835 self.thread_store.clone(),
836 self.context_store.clone(),
837 context_store.clone(),
838 self.language_registry.clone(),
839 self.workspace.clone(),
840 window,
841 cx,
842 )
843 });
844
845 let message_editor = cx.new(|cx| {
846 MessageEditor::new(
847 self.fs.clone(),
848 self.workspace.clone(),
849 context_store.clone(),
850 self.prompt_store.clone(),
851 self.thread_store.downgrade(),
852 self.context_store.downgrade(),
853 Some(self.history_store.downgrade()),
854 thread.clone(),
855 window,
856 cx,
857 )
858 });
859
860 if let Some(text) = preserved_text {
861 message_editor.update(cx, |editor, cx| {
862 editor.set_text(text, window, cx);
863 });
864 }
865
866 message_editor.focus_handle(cx).focus(window);
867
868 let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
869 self.set_active_view(thread_view, window, cx);
870
871 AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
872 }
873
874 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
875 let context = self
876 .context_store
877 .update(cx, |context_store, cx| context_store.create(cx));
878 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
879 .log_err()
880 .flatten();
881
882 let context_editor = cx.new(|cx| {
883 let mut editor = TextThreadEditor::for_context(
884 context,
885 self.fs.clone(),
886 self.workspace.clone(),
887 self.project.clone(),
888 lsp_adapter_delegate,
889 window,
890 cx,
891 );
892 editor.insert_default_prompt(window, cx);
893 editor
894 });
895
896 self.set_active_view(
897 ActiveView::prompt_editor(
898 context_editor.clone(),
899 self.history_store.clone(),
900 self.language_registry.clone(),
901 window,
902 cx,
903 ),
904 window,
905 cx,
906 );
907 context_editor.focus_handle(cx).focus(window);
908 }
909
910 fn new_external_thread(
911 &mut self,
912 agent_choice: Option<crate::ExternalAgent>,
913 window: &mut Window,
914 cx: &mut Context<Self>,
915 ) {
916 let workspace = self.workspace.clone();
917 let project = self.project.clone();
918 let message_history = self.acp_message_history.clone();
919 let fs = self.fs.clone();
920
921 const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
922
923 #[derive(Default, Serialize, Deserialize)]
924 struct LastUsedExternalAgent {
925 agent: crate::ExternalAgent,
926 }
927
928 cx.spawn_in(window, async move |this, cx| {
929 let server: Rc<dyn AgentServer> = match agent_choice {
930 Some(agent) => {
931 cx.background_spawn(async move {
932 if let Some(serialized) =
933 serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
934 {
935 KEY_VALUE_STORE
936 .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
937 .await
938 .log_err();
939 }
940 })
941 .detach();
942
943 agent.server(fs)
944 }
945 None => cx
946 .background_spawn(async move {
947 KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
948 })
949 .await
950 .log_err()
951 .flatten()
952 .and_then(|value| {
953 serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
954 })
955 .unwrap_or_default()
956 .agent
957 .server(fs),
958 };
959
960 this.update_in(cx, |this, window, cx| {
961 let thread_view = cx.new(|cx| {
962 crate::acp::AcpThreadView::new(
963 server,
964 workspace.clone(),
965 project,
966 message_history,
967 MIN_EDITOR_LINES,
968 Some(MAX_EDITOR_LINES),
969 window,
970 cx,
971 )
972 });
973
974 this.set_active_view(ActiveView::ExternalAgentThread { thread_view }, window, cx);
975 })
976 })
977 .detach_and_log_err(cx);
978 }
979
980 fn deploy_rules_library(
981 &mut self,
982 action: &OpenRulesLibrary,
983 _window: &mut Window,
984 cx: &mut Context<Self>,
985 ) {
986 open_rules_library(
987 self.language_registry.clone(),
988 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
989 Rc::new(|| {
990 Rc::new(SlashCommandCompletionProvider::new(
991 Arc::new(SlashCommandWorkingSet::default()),
992 None,
993 None,
994 ))
995 }),
996 action
997 .prompt_to_select
998 .map(|uuid| UserPromptId(uuid).into()),
999 cx,
1000 )
1001 .detach_and_log_err(cx);
1002 }
1003
1004 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1005 if matches!(self.active_view, ActiveView::History) {
1006 if let Some(previous_view) = self.previous_view.take() {
1007 self.set_active_view(previous_view, window, cx);
1008 }
1009 } else {
1010 self.thread_store
1011 .update(cx, |thread_store, cx| thread_store.reload(cx))
1012 .detach_and_log_err(cx);
1013 self.set_active_view(ActiveView::History, window, cx);
1014 }
1015 cx.notify();
1016 }
1017
1018 pub(crate) fn open_saved_prompt_editor(
1019 &mut self,
1020 path: Arc<Path>,
1021 window: &mut Window,
1022 cx: &mut Context<Self>,
1023 ) -> Task<Result<()>> {
1024 let context = self
1025 .context_store
1026 .update(cx, |store, cx| store.open_local_context(path, cx));
1027 cx.spawn_in(window, async move |this, cx| {
1028 let context = context.await?;
1029 this.update_in(cx, |this, window, cx| {
1030 this.open_prompt_editor(context, window, cx);
1031 })
1032 })
1033 }
1034
1035 pub(crate) fn open_prompt_editor(
1036 &mut self,
1037 context: Entity<AssistantContext>,
1038 window: &mut Window,
1039 cx: &mut Context<Self>,
1040 ) {
1041 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1042 .log_err()
1043 .flatten();
1044 let editor = cx.new(|cx| {
1045 TextThreadEditor::for_context(
1046 context,
1047 self.fs.clone(),
1048 self.workspace.clone(),
1049 self.project.clone(),
1050 lsp_adapter_delegate,
1051 window,
1052 cx,
1053 )
1054 });
1055 self.set_active_view(
1056 ActiveView::prompt_editor(
1057 editor.clone(),
1058 self.history_store.clone(),
1059 self.language_registry.clone(),
1060 window,
1061 cx,
1062 ),
1063 window,
1064 cx,
1065 );
1066 }
1067
1068 pub(crate) fn open_thread_by_id(
1069 &mut self,
1070 thread_id: &ThreadId,
1071 window: &mut Window,
1072 cx: &mut Context<Self>,
1073 ) -> Task<Result<()>> {
1074 let open_thread_task = self
1075 .thread_store
1076 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
1077 cx.spawn_in(window, async move |this, cx| {
1078 let thread = open_thread_task.await?;
1079 this.update_in(cx, |this, window, cx| {
1080 this.open_thread(thread, window, cx);
1081 anyhow::Ok(())
1082 })??;
1083 Ok(())
1084 })
1085 }
1086
1087 pub(crate) fn open_thread(
1088 &mut self,
1089 thread: Entity<Thread>,
1090 window: &mut Window,
1091 cx: &mut Context<Self>,
1092 ) {
1093 let context_store = cx.new(|_cx| {
1094 ContextStore::new(
1095 self.project.downgrade(),
1096 Some(self.thread_store.downgrade()),
1097 )
1098 });
1099
1100 let active_thread = cx.new(|cx| {
1101 ActiveThread::new(
1102 thread.clone(),
1103 self.thread_store.clone(),
1104 self.context_store.clone(),
1105 context_store.clone(),
1106 self.language_registry.clone(),
1107 self.workspace.clone(),
1108 window,
1109 cx,
1110 )
1111 });
1112
1113 let message_editor = cx.new(|cx| {
1114 MessageEditor::new(
1115 self.fs.clone(),
1116 self.workspace.clone(),
1117 context_store,
1118 self.prompt_store.clone(),
1119 self.thread_store.downgrade(),
1120 self.context_store.downgrade(),
1121 Some(self.history_store.downgrade()),
1122 thread.clone(),
1123 window,
1124 cx,
1125 )
1126 });
1127 message_editor.focus_handle(cx).focus(window);
1128
1129 let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
1130 self.set_active_view(thread_view, window, cx);
1131 AgentDiff::set_active_thread(&self.workspace, thread.clone(), window, cx);
1132 }
1133
1134 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1135 match self.active_view {
1136 ActiveView::Configuration | ActiveView::History => {
1137 if let Some(previous_view) = self.previous_view.take() {
1138 self.active_view = previous_view;
1139
1140 match &self.active_view {
1141 ActiveView::Thread { message_editor, .. } => {
1142 message_editor.focus_handle(cx).focus(window);
1143 }
1144 ActiveView::ExternalAgentThread { thread_view } => {
1145 thread_view.focus_handle(cx).focus(window);
1146 }
1147 ActiveView::TextThread { context_editor, .. } => {
1148 context_editor.focus_handle(cx).focus(window);
1149 }
1150 ActiveView::History | ActiveView::Configuration => {}
1151 }
1152 }
1153 cx.notify();
1154 }
1155 _ => {}
1156 }
1157 }
1158
1159 pub fn toggle_navigation_menu(
1160 &mut self,
1161 _: &ToggleNavigationMenu,
1162 window: &mut Window,
1163 cx: &mut Context<Self>,
1164 ) {
1165 self.assistant_navigation_menu_handle.toggle(window, cx);
1166 }
1167
1168 pub fn toggle_options_menu(
1169 &mut self,
1170 _: &ToggleOptionsMenu,
1171 window: &mut Window,
1172 cx: &mut Context<Self>,
1173 ) {
1174 self.agent_panel_menu_handle.toggle(window, cx);
1175 }
1176
1177 pub fn increase_font_size(
1178 &mut self,
1179 action: &IncreaseBufferFontSize,
1180 _: &mut Window,
1181 cx: &mut Context<Self>,
1182 ) {
1183 self.handle_font_size_action(action.persist, px(1.0), cx);
1184 }
1185
1186 pub fn decrease_font_size(
1187 &mut self,
1188 action: &DecreaseBufferFontSize,
1189 _: &mut Window,
1190 cx: &mut Context<Self>,
1191 ) {
1192 self.handle_font_size_action(action.persist, px(-1.0), cx);
1193 }
1194
1195 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1196 match self.active_view.which_font_size_used() {
1197 WhichFontSize::AgentFont => {
1198 if persist {
1199 update_settings_file::<ThemeSettings>(
1200 self.fs.clone(),
1201 cx,
1202 move |settings, cx| {
1203 let agent_font_size =
1204 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1205 let _ = settings
1206 .agent_font_size
1207 .insert(theme::clamp_font_size(agent_font_size).0);
1208 },
1209 );
1210 } else {
1211 theme::adjust_agent_font_size(cx, |size| {
1212 *size += delta;
1213 });
1214 }
1215 }
1216 WhichFontSize::BufferFont => {
1217 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1218 // default handler that changes that font size.
1219 cx.propagate();
1220 }
1221 WhichFontSize::None => {}
1222 }
1223 }
1224
1225 pub fn reset_font_size(
1226 &mut self,
1227 action: &ResetBufferFontSize,
1228 _: &mut Window,
1229 cx: &mut Context<Self>,
1230 ) {
1231 if action.persist {
1232 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1233 settings.agent_font_size = None;
1234 });
1235 } else {
1236 theme::reset_agent_font_size(cx);
1237 }
1238 }
1239
1240 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1241 if self.zoomed {
1242 cx.emit(PanelEvent::ZoomOut);
1243 } else {
1244 if !self.focus_handle(cx).contains_focused(window, cx) {
1245 cx.focus_self(window);
1246 }
1247 cx.emit(PanelEvent::ZoomIn);
1248 }
1249 }
1250
1251 pub fn open_agent_diff(
1252 &mut self,
1253 _: &OpenAgentDiff,
1254 window: &mut Window,
1255 cx: &mut Context<Self>,
1256 ) {
1257 match &self.active_view {
1258 ActiveView::Thread { thread, .. } => {
1259 let thread = thread.read(cx).thread().clone();
1260 self.workspace
1261 .update(cx, |workspace, cx| {
1262 AgentDiffPane::deploy_in_workspace(
1263 AgentDiffThread::Native(thread),
1264 workspace,
1265 window,
1266 cx,
1267 )
1268 })
1269 .log_err();
1270 }
1271 ActiveView::ExternalAgentThread { .. }
1272 | ActiveView::TextThread { .. }
1273 | ActiveView::History
1274 | ActiveView::Configuration => {}
1275 }
1276 }
1277
1278 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1279 let context_server_store = self.project.read(cx).context_server_store();
1280 let tools = self.thread_store.read(cx).tools();
1281 let fs = self.fs.clone();
1282
1283 self.set_active_view(ActiveView::Configuration, window, cx);
1284 self.configuration = Some(cx.new(|cx| {
1285 AgentConfiguration::new(
1286 fs,
1287 context_server_store,
1288 tools,
1289 self.language_registry.clone(),
1290 self.workspace.clone(),
1291 window,
1292 cx,
1293 )
1294 }));
1295
1296 if let Some(configuration) = self.configuration.as_ref() {
1297 self.configuration_subscription = Some(cx.subscribe_in(
1298 configuration,
1299 window,
1300 Self::handle_agent_configuration_event,
1301 ));
1302
1303 configuration.focus_handle(cx).focus(window);
1304 }
1305 }
1306
1307 pub(crate) fn open_active_thread_as_markdown(
1308 &mut self,
1309 _: &OpenActiveThreadAsMarkdown,
1310 window: &mut Window,
1311 cx: &mut Context<Self>,
1312 ) {
1313 let Some(workspace) = self.workspace.upgrade() else {
1314 return;
1315 };
1316
1317 match &self.active_view {
1318 ActiveView::Thread { thread, .. } => {
1319 active_thread::open_active_thread_as_markdown(
1320 thread.read(cx).thread().clone(),
1321 workspace,
1322 window,
1323 cx,
1324 )
1325 .detach_and_log_err(cx);
1326 }
1327 ActiveView::ExternalAgentThread { thread_view } => {
1328 thread_view
1329 .update(cx, |thread_view, cx| {
1330 thread_view.open_thread_as_markdown(workspace, window, cx)
1331 })
1332 .detach_and_log_err(cx);
1333 }
1334 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1335 }
1336 }
1337
1338 fn handle_agent_configuration_event(
1339 &mut self,
1340 _entity: &Entity<AgentConfiguration>,
1341 event: &AssistantConfigurationEvent,
1342 window: &mut Window,
1343 cx: &mut Context<Self>,
1344 ) {
1345 match event {
1346 AssistantConfigurationEvent::NewThread(provider) => {
1347 if LanguageModelRegistry::read_global(cx)
1348 .default_model()
1349 .map_or(true, |model| model.provider.id() != provider.id())
1350 {
1351 if let Some(model) = provider.default_model(cx) {
1352 update_settings_file::<AgentSettings>(
1353 self.fs.clone(),
1354 cx,
1355 move |settings, _| settings.set_model(model),
1356 );
1357 }
1358 }
1359
1360 self.new_thread(&NewThread::default(), window, cx);
1361 if let Some((thread, model)) =
1362 self.active_thread(cx).zip(provider.default_model(cx))
1363 {
1364 thread.update(cx, |thread, cx| {
1365 thread.set_configured_model(
1366 Some(ConfiguredModel {
1367 provider: provider.clone(),
1368 model,
1369 }),
1370 cx,
1371 );
1372 });
1373 }
1374 }
1375 }
1376 }
1377
1378 pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
1379 match &self.active_view {
1380 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1381 _ => None,
1382 }
1383 }
1384
1385 pub(crate) fn delete_thread(
1386 &mut self,
1387 thread_id: &ThreadId,
1388 cx: &mut Context<Self>,
1389 ) -> Task<Result<()>> {
1390 self.thread_store
1391 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1392 }
1393
1394 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1395 let ActiveView::Thread { thread, .. } = &self.active_view else {
1396 return;
1397 };
1398
1399 let thread_state = thread.read(cx).thread().read(cx);
1400 if !thread_state.tool_use_limit_reached() {
1401 return;
1402 }
1403
1404 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1405 if let Some(model) = model {
1406 thread.update(cx, |active_thread, cx| {
1407 active_thread.thread().update(cx, |thread, cx| {
1408 thread.insert_invisible_continue_message(cx);
1409 thread.advance_prompt_id();
1410 thread.send_to_model(
1411 model,
1412 CompletionIntent::UserPrompt,
1413 Some(window.window_handle()),
1414 cx,
1415 );
1416 });
1417 });
1418 } else {
1419 log::warn!("No configured model available for continuation");
1420 }
1421 }
1422
1423 fn toggle_burn_mode(
1424 &mut self,
1425 _: &ToggleBurnMode,
1426 _window: &mut Window,
1427 cx: &mut Context<Self>,
1428 ) {
1429 let ActiveView::Thread { thread, .. } = &self.active_view else {
1430 return;
1431 };
1432
1433 thread.update(cx, |active_thread, cx| {
1434 active_thread.thread().update(cx, |thread, _cx| {
1435 let current_mode = thread.completion_mode();
1436
1437 thread.set_completion_mode(match current_mode {
1438 CompletionMode::Burn => CompletionMode::Normal,
1439 CompletionMode::Normal => CompletionMode::Burn,
1440 });
1441 });
1442 });
1443 }
1444
1445 pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
1446 match &self.active_view {
1447 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1448 _ => None,
1449 }
1450 }
1451
1452 pub(crate) fn delete_context(
1453 &mut self,
1454 path: Arc<Path>,
1455 cx: &mut Context<Self>,
1456 ) -> Task<Result<()>> {
1457 self.context_store
1458 .update(cx, |this, cx| this.delete_local_context(path, cx))
1459 }
1460
1461 fn set_active_view(
1462 &mut self,
1463 new_view: ActiveView,
1464 window: &mut Window,
1465 cx: &mut Context<Self>,
1466 ) {
1467 let current_is_history = matches!(self.active_view, ActiveView::History);
1468 let new_is_history = matches!(new_view, ActiveView::History);
1469
1470 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1471 let new_is_config = matches!(new_view, ActiveView::Configuration);
1472
1473 let current_is_special = current_is_history || current_is_config;
1474 let new_is_special = new_is_history || new_is_config;
1475
1476 match &self.active_view {
1477 ActiveView::Thread { thread, .. } => {
1478 let thread = thread.read(cx);
1479 if thread.is_empty() {
1480 let id = thread.thread().read(cx).id().clone();
1481 self.history_store.update(cx, |store, cx| {
1482 store.remove_recently_opened_thread(id, cx);
1483 });
1484 }
1485 }
1486 _ => {}
1487 }
1488
1489 match &new_view {
1490 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1491 let id = thread.read(cx).thread().read(cx).id().clone();
1492 store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
1493 }),
1494 ActiveView::TextThread { context_editor, .. } => {
1495 self.history_store.update(cx, |store, cx| {
1496 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1497 store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
1498 }
1499 })
1500 }
1501 ActiveView::ExternalAgentThread { .. } => {}
1502 ActiveView::History | ActiveView::Configuration => {}
1503 }
1504
1505 if current_is_special && !new_is_special {
1506 self.active_view = new_view;
1507 } else if !current_is_special && new_is_special {
1508 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1509 } else {
1510 if !new_is_special {
1511 self.previous_view = None;
1512 }
1513 self.active_view = new_view;
1514 }
1515
1516 self.acp_message_history.borrow_mut().reset_position();
1517
1518 self.focus_handle(cx).focus(window);
1519 }
1520
1521 fn populate_recently_opened_menu_section(
1522 mut menu: ContextMenu,
1523 panel: Entity<Self>,
1524 cx: &mut Context<ContextMenu>,
1525 ) -> ContextMenu {
1526 let entries = panel
1527 .read(cx)
1528 .history_store
1529 .read(cx)
1530 .recently_opened_entries(cx);
1531
1532 if entries.is_empty() {
1533 return menu;
1534 }
1535
1536 menu = menu.header("Recently Opened");
1537
1538 for entry in entries {
1539 let title = entry.title().clone();
1540 let id = entry.id();
1541
1542 menu = menu.entry_with_end_slot_on_hover(
1543 title,
1544 None,
1545 {
1546 let panel = panel.downgrade();
1547 let id = id.clone();
1548 move |window, cx| {
1549 let id = id.clone();
1550 panel
1551 .update(cx, move |this, cx| match id {
1552 HistoryEntryId::Thread(id) => this
1553 .open_thread_by_id(&id, window, cx)
1554 .detach_and_log_err(cx),
1555 HistoryEntryId::Context(path) => this
1556 .open_saved_prompt_editor(path.clone(), window, cx)
1557 .detach_and_log_err(cx),
1558 })
1559 .ok();
1560 }
1561 },
1562 IconName::Close,
1563 "Close Entry".into(),
1564 {
1565 let panel = panel.downgrade();
1566 let id = id.clone();
1567 move |_window, cx| {
1568 panel
1569 .update(cx, |this, cx| {
1570 this.history_store.update(cx, |history_store, cx| {
1571 history_store.remove_recently_opened_entry(&id, cx);
1572 });
1573 })
1574 .ok();
1575 }
1576 },
1577 );
1578 }
1579
1580 menu = menu.separator();
1581
1582 menu
1583 }
1584}
1585
1586impl Focusable for AgentPanel {
1587 fn focus_handle(&self, cx: &App) -> FocusHandle {
1588 match &self.active_view {
1589 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1590 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1591 ActiveView::History => self.history.focus_handle(cx),
1592 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1593 ActiveView::Configuration => {
1594 if let Some(configuration) = self.configuration.as_ref() {
1595 configuration.focus_handle(cx)
1596 } else {
1597 cx.focus_handle()
1598 }
1599 }
1600 }
1601 }
1602}
1603
1604fn agent_panel_dock_position(cx: &App) -> DockPosition {
1605 match AgentSettings::get_global(cx).dock {
1606 AgentDockPosition::Left => DockPosition::Left,
1607 AgentDockPosition::Bottom => DockPosition::Bottom,
1608 AgentDockPosition::Right => DockPosition::Right,
1609 }
1610}
1611
1612impl EventEmitter<PanelEvent> for AgentPanel {}
1613
1614impl Panel for AgentPanel {
1615 fn persistent_name() -> &'static str {
1616 "AgentPanel"
1617 }
1618
1619 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1620 agent_panel_dock_position(cx)
1621 }
1622
1623 fn position_is_valid(&self, position: DockPosition) -> bool {
1624 position != DockPosition::Bottom
1625 }
1626
1627 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1628 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1629 let dock = match position {
1630 DockPosition::Left => AgentDockPosition::Left,
1631 DockPosition::Bottom => AgentDockPosition::Bottom,
1632 DockPosition::Right => AgentDockPosition::Right,
1633 };
1634 settings.set_dock(dock);
1635 });
1636 }
1637
1638 fn size(&self, window: &Window, cx: &App) -> Pixels {
1639 let settings = AgentSettings::get_global(cx);
1640 match self.position(window, cx) {
1641 DockPosition::Left | DockPosition::Right => {
1642 self.width.unwrap_or(settings.default_width)
1643 }
1644 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1645 }
1646 }
1647
1648 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1649 match self.position(window, cx) {
1650 DockPosition::Left | DockPosition::Right => self.width = size,
1651 DockPosition::Bottom => self.height = size,
1652 }
1653 self.serialize(cx);
1654 cx.notify();
1655 }
1656
1657 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1658
1659 fn remote_id() -> Option<proto::PanelId> {
1660 Some(proto::PanelId::AssistantPanel)
1661 }
1662
1663 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1664 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1665 }
1666
1667 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1668 Some("Agent Panel")
1669 }
1670
1671 fn toggle_action(&self) -> Box<dyn Action> {
1672 Box::new(ToggleFocus)
1673 }
1674
1675 fn activation_priority(&self) -> u32 {
1676 3
1677 }
1678
1679 fn enabled(&self, cx: &App) -> bool {
1680 DisableAiSettings::get_global(cx).disable_ai.not() && AgentSettings::get_global(cx).enabled
1681 }
1682
1683 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1684 self.zoomed
1685 }
1686
1687 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1688 self.zoomed = zoomed;
1689 cx.notify();
1690 }
1691}
1692
1693impl AgentPanel {
1694 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1695 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1696
1697 let content = match &self.active_view {
1698 ActiveView::Thread {
1699 thread: active_thread,
1700 change_title_editor,
1701 ..
1702 } => {
1703 let state = {
1704 let active_thread = active_thread.read(cx);
1705 if active_thread.is_empty() {
1706 &ThreadSummary::Pending
1707 } else {
1708 active_thread.summary(cx)
1709 }
1710 };
1711
1712 match state {
1713 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1714 .truncate()
1715 .into_any_element(),
1716 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1717 .truncate()
1718 .into_any_element(),
1719 ThreadSummary::Ready(_) => div()
1720 .w_full()
1721 .child(change_title_editor.clone())
1722 .into_any_element(),
1723 ThreadSummary::Error => h_flex()
1724 .w_full()
1725 .child(change_title_editor.clone())
1726 .child(
1727 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1728 .on_click({
1729 let active_thread = active_thread.clone();
1730 move |_, _window, cx| {
1731 active_thread.update(cx, |thread, cx| {
1732 thread.regenerate_summary(cx);
1733 });
1734 }
1735 })
1736 .tooltip(move |_window, cx| {
1737 cx.new(|_| {
1738 Tooltip::new("Failed to generate title")
1739 .meta("Click to try again")
1740 })
1741 .into()
1742 }),
1743 )
1744 .into_any_element(),
1745 }
1746 }
1747 ActiveView::ExternalAgentThread { thread_view } => {
1748 Label::new(thread_view.read(cx).title(cx))
1749 .truncate()
1750 .into_any_element()
1751 }
1752 ActiveView::TextThread {
1753 title_editor,
1754 context_editor,
1755 ..
1756 } => {
1757 let summary = context_editor.read(cx).context().read(cx).summary();
1758
1759 match summary {
1760 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1761 .truncate()
1762 .into_any_element(),
1763 ContextSummary::Content(summary) => {
1764 if summary.done {
1765 div()
1766 .w_full()
1767 .child(title_editor.clone())
1768 .into_any_element()
1769 } else {
1770 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1771 .truncate()
1772 .into_any_element()
1773 }
1774 }
1775 ContextSummary::Error => h_flex()
1776 .w_full()
1777 .child(title_editor.clone())
1778 .child(
1779 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1780 .on_click({
1781 let context_editor = context_editor.clone();
1782 move |_, _window, cx| {
1783 context_editor.update(cx, |context_editor, cx| {
1784 context_editor.regenerate_summary(cx);
1785 });
1786 }
1787 })
1788 .tooltip(move |_window, cx| {
1789 cx.new(|_| {
1790 Tooltip::new("Failed to generate title")
1791 .meta("Click to try again")
1792 })
1793 .into()
1794 }),
1795 )
1796 .into_any_element(),
1797 }
1798 }
1799 ActiveView::History => Label::new("History").truncate().into_any_element(),
1800 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1801 };
1802
1803 h_flex()
1804 .key_context("TitleEditor")
1805 .id("TitleEditor")
1806 .flex_grow()
1807 .w_full()
1808 .max_w_full()
1809 .overflow_x_scroll()
1810 .child(content)
1811 .into_any()
1812 }
1813
1814 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1815 let user_store = self.user_store.read(cx);
1816 let usage = user_store.model_request_usage();
1817
1818 let account_url = zed_urls::account_url(cx);
1819
1820 let focus_handle = self.focus_handle(cx);
1821
1822 let go_back_button = div().child(
1823 IconButton::new("go-back", IconName::ArrowLeft)
1824 .icon_size(IconSize::Small)
1825 .on_click(cx.listener(|this, _, window, cx| {
1826 this.go_back(&workspace::GoBack, window, cx);
1827 }))
1828 .tooltip({
1829 let focus_handle = focus_handle.clone();
1830 move |window, cx| {
1831 Tooltip::for_action_in(
1832 "Go Back",
1833 &workspace::GoBack,
1834 &focus_handle,
1835 window,
1836 cx,
1837 )
1838 }
1839 }),
1840 );
1841
1842 let recent_entries_menu = div().child(
1843 PopoverMenu::new("agent-nav-menu")
1844 .trigger_with_tooltip(
1845 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1846 .icon_size(IconSize::Small)
1847 .style(ui::ButtonStyle::Subtle),
1848 {
1849 let focus_handle = focus_handle.clone();
1850 move |window, cx| {
1851 Tooltip::for_action_in(
1852 "Toggle Panel Menu",
1853 &ToggleNavigationMenu,
1854 &focus_handle,
1855 window,
1856 cx,
1857 )
1858 }
1859 },
1860 )
1861 .anchor(Corner::TopLeft)
1862 .with_handle(self.assistant_navigation_menu_handle.clone())
1863 .menu({
1864 let menu = self.assistant_navigation_menu.clone();
1865 move |window, cx| {
1866 if let Some(menu) = menu.as_ref() {
1867 menu.update(cx, |_, cx| {
1868 cx.defer_in(window, |menu, window, cx| {
1869 menu.rebuild(window, cx);
1870 });
1871 })
1872 }
1873 menu.clone()
1874 }
1875 }),
1876 );
1877
1878 let full_screen_label = if self.is_zoomed(window, cx) {
1879 "Disable Full Screen"
1880 } else {
1881 "Enable Full Screen"
1882 };
1883
1884 let active_thread = match &self.active_view {
1885 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1886 ActiveView::ExternalAgentThread { .. }
1887 | ActiveView::TextThread { .. }
1888 | ActiveView::History
1889 | ActiveView::Configuration => None,
1890 };
1891
1892 let new_thread_menu = PopoverMenu::new("new_thread_menu")
1893 .trigger_with_tooltip(
1894 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
1895 Tooltip::text("New Thread…"),
1896 )
1897 .anchor(Corner::TopRight)
1898 .with_handle(self.new_thread_menu_handle.clone())
1899 .menu({
1900 let focus_handle = focus_handle.clone();
1901 move |window, cx| {
1902 let active_thread = active_thread.clone();
1903 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
1904 menu = menu
1905 .context(focus_handle.clone())
1906 .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
1907 this.header("Zed Agent")
1908 })
1909 .when_some(active_thread, |this, active_thread| {
1910 let thread = active_thread.read(cx);
1911
1912 if !thread.is_empty() {
1913 let thread_id = thread.id().clone();
1914 this.item(
1915 ContextMenuEntry::new("New From Summary")
1916 .icon(IconName::ThreadFromSummary)
1917 .icon_color(Color::Muted)
1918 .handler(move |window, cx| {
1919 window.dispatch_action(
1920 Box::new(NewThread {
1921 from_thread_id: Some(thread_id.clone()),
1922 }),
1923 cx,
1924 );
1925 }),
1926 )
1927 } else {
1928 this
1929 }
1930 })
1931 .item(
1932 ContextMenuEntry::new("New Thread")
1933 .icon(IconName::Thread)
1934 .icon_color(Color::Muted)
1935 .action(NewThread::default().boxed_clone())
1936 .handler(move |window, cx| {
1937 window.dispatch_action(
1938 NewThread::default().boxed_clone(),
1939 cx,
1940 );
1941 }),
1942 )
1943 .item(
1944 ContextMenuEntry::new("New Text Thread")
1945 .icon(IconName::TextThread)
1946 .icon_color(Color::Muted)
1947 .action(NewTextThread.boxed_clone())
1948 .handler(move |window, cx| {
1949 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1950 }),
1951 )
1952 .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
1953 this.separator()
1954 .header("External Agents")
1955 .item(
1956 ContextMenuEntry::new("New Gemini Thread")
1957 .icon(IconName::AiGemini)
1958 .icon_color(Color::Muted)
1959 .handler(move |window, cx| {
1960 window.dispatch_action(
1961 NewExternalAgentThread {
1962 agent: Some(crate::ExternalAgent::Gemini),
1963 }
1964 .boxed_clone(),
1965 cx,
1966 );
1967 }),
1968 )
1969 .item(
1970 ContextMenuEntry::new("New Claude Code Thread")
1971 .icon(IconName::AiClaude)
1972 .icon_color(Color::Muted)
1973 .handler(move |window, cx| {
1974 window.dispatch_action(
1975 NewExternalAgentThread {
1976 agent: Some(
1977 crate::ExternalAgent::ClaudeCode,
1978 ),
1979 }
1980 .boxed_clone(),
1981 cx,
1982 );
1983 }),
1984 )
1985 .item(
1986 ContextMenuEntry::new("New Native Agent Thread")
1987 .icon(IconName::ZedAssistant)
1988 .icon_color(Color::Muted)
1989 .handler(move |window, cx| {
1990 window.dispatch_action(
1991 NewExternalAgentThread {
1992 agent: Some(
1993 crate::ExternalAgent::NativeAgent,
1994 ),
1995 }
1996 .boxed_clone(),
1997 cx,
1998 );
1999 }),
2000 )
2001 });
2002 menu
2003 }))
2004 }
2005 });
2006
2007 let agent_panel_menu = PopoverMenu::new("agent-options-menu")
2008 .trigger_with_tooltip(
2009 IconButton::new("agent-options-menu", IconName::Ellipsis)
2010 .icon_size(IconSize::Small),
2011 {
2012 let focus_handle = focus_handle.clone();
2013 move |window, cx| {
2014 Tooltip::for_action_in(
2015 "Toggle Agent Menu",
2016 &ToggleOptionsMenu,
2017 &focus_handle,
2018 window,
2019 cx,
2020 )
2021 }
2022 },
2023 )
2024 .anchor(Corner::TopRight)
2025 .with_handle(self.agent_panel_menu_handle.clone())
2026 .menu({
2027 let focus_handle = focus_handle.clone();
2028 move |window, cx| {
2029 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2030 menu = menu.context(focus_handle.clone());
2031 if let Some(usage) = usage {
2032 menu = menu
2033 .header_with_link("Prompt Usage", "Manage", account_url.clone())
2034 .custom_entry(
2035 move |_window, cx| {
2036 let used_percentage = match usage.limit {
2037 UsageLimit::Limited(limit) => {
2038 Some((usage.amount as f32 / limit as f32) * 100.)
2039 }
2040 UsageLimit::Unlimited => None,
2041 };
2042
2043 h_flex()
2044 .flex_1()
2045 .gap_1p5()
2046 .children(used_percentage.map(|percent| {
2047 ProgressBar::new("usage", percent, 100., cx)
2048 }))
2049 .child(
2050 Label::new(match usage.limit {
2051 UsageLimit::Limited(limit) => {
2052 format!("{} / {limit}", usage.amount)
2053 }
2054 UsageLimit::Unlimited => {
2055 format!("{} / ∞", usage.amount)
2056 }
2057 })
2058 .size(LabelSize::Small)
2059 .color(Color::Muted),
2060 )
2061 .into_any_element()
2062 },
2063 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
2064 )
2065 .separator()
2066 }
2067
2068 menu = menu
2069 .header("MCP Servers")
2070 .action(
2071 "View Server Extensions",
2072 Box::new(zed_actions::Extensions {
2073 category_filter: Some(
2074 zed_actions::ExtensionCategoryFilter::ContextServers,
2075 ),
2076 id: None,
2077 }),
2078 )
2079 .action("Add Custom Server…", Box::new(AddContextServer))
2080 .separator();
2081
2082 menu = menu
2083 .action("Rules…", Box::new(OpenRulesLibrary::default()))
2084 .action("Settings", Box::new(OpenSettings))
2085 .separator()
2086 .action(full_screen_label, Box::new(ToggleZoom));
2087 menu
2088 }))
2089 }
2090 });
2091
2092 h_flex()
2093 .id("assistant-toolbar")
2094 .h(Tab::container_height(cx))
2095 .max_w_full()
2096 .flex_none()
2097 .justify_between()
2098 .gap_2()
2099 .bg(cx.theme().colors().tab_bar_background)
2100 .border_b_1()
2101 .border_color(cx.theme().colors().border)
2102 .child(
2103 h_flex()
2104 .size_full()
2105 .pl_1()
2106 .gap_1()
2107 .child(match &self.active_view {
2108 ActiveView::History | ActiveView::Configuration => go_back_button,
2109 _ => recent_entries_menu,
2110 })
2111 .child(self.render_title_view(window, cx)),
2112 )
2113 .child(
2114 h_flex()
2115 .h_full()
2116 .gap_2()
2117 .children(self.render_token_count(cx))
2118 .child(
2119 h_flex()
2120 .h_full()
2121 .gap(DynamicSpacing::Base02.rems(cx))
2122 .px(DynamicSpacing::Base08.rems(cx))
2123 .border_l_1()
2124 .border_color(cx.theme().colors().border)
2125 .child(new_thread_menu)
2126 .child(agent_panel_menu),
2127 ),
2128 )
2129 }
2130
2131 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
2132 match &self.active_view {
2133 ActiveView::Thread {
2134 thread,
2135 message_editor,
2136 ..
2137 } => {
2138 let active_thread = thread.read(cx);
2139 let message_editor = message_editor.read(cx);
2140
2141 let editor_empty = message_editor.is_editor_fully_empty(cx);
2142
2143 if active_thread.is_empty() && editor_empty {
2144 return None;
2145 }
2146
2147 let thread = active_thread.thread().read(cx);
2148 let is_generating = thread.is_generating();
2149 let conversation_token_usage = thread.total_token_usage()?;
2150
2151 let (total_token_usage, is_estimating) =
2152 if let Some((editing_message_id, unsent_tokens)) =
2153 active_thread.editing_message_id()
2154 {
2155 let combined = thread
2156 .token_usage_up_to_message(editing_message_id)
2157 .add(unsent_tokens);
2158
2159 (combined, unsent_tokens > 0)
2160 } else {
2161 let unsent_tokens =
2162 message_editor.last_estimated_token_count().unwrap_or(0);
2163 let combined = conversation_token_usage.add(unsent_tokens);
2164
2165 (combined, unsent_tokens > 0)
2166 };
2167
2168 let is_waiting_to_update_token_count =
2169 message_editor.is_waiting_to_update_token_count();
2170
2171 if total_token_usage.total == 0 {
2172 return None;
2173 }
2174
2175 let token_color = match total_token_usage.ratio() {
2176 TokenUsageRatio::Normal if is_estimating => Color::Default,
2177 TokenUsageRatio::Normal => Color::Muted,
2178 TokenUsageRatio::Warning => Color::Warning,
2179 TokenUsageRatio::Exceeded => Color::Error,
2180 };
2181
2182 let token_count = h_flex()
2183 .id("token-count")
2184 .flex_shrink_0()
2185 .gap_0p5()
2186 .when(!is_generating && is_estimating, |parent| {
2187 parent
2188 .child(
2189 h_flex()
2190 .mr_1()
2191 .size_2p5()
2192 .justify_center()
2193 .rounded_full()
2194 .bg(cx.theme().colors().text.opacity(0.1))
2195 .child(
2196 div().size_1().rounded_full().bg(cx.theme().colors().text),
2197 ),
2198 )
2199 .tooltip(move |window, cx| {
2200 Tooltip::with_meta(
2201 "Estimated New Token Count",
2202 None,
2203 format!(
2204 "Current Conversation Tokens: {}",
2205 humanize_token_count(conversation_token_usage.total)
2206 ),
2207 window,
2208 cx,
2209 )
2210 })
2211 })
2212 .child(
2213 Label::new(humanize_token_count(total_token_usage.total))
2214 .size(LabelSize::Small)
2215 .color(token_color)
2216 .map(|label| {
2217 if is_generating || is_waiting_to_update_token_count {
2218 label
2219 .with_animation(
2220 "used-tokens-label",
2221 Animation::new(Duration::from_secs(2))
2222 .repeat()
2223 .with_easing(pulsating_between(0.6, 1.)),
2224 |label, delta| label.alpha(delta),
2225 )
2226 .into_any()
2227 } else {
2228 label.into_any_element()
2229 }
2230 }),
2231 )
2232 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2233 .child(
2234 Label::new(humanize_token_count(total_token_usage.max))
2235 .size(LabelSize::Small)
2236 .color(Color::Muted),
2237 )
2238 .into_any();
2239
2240 Some(token_count)
2241 }
2242 ActiveView::TextThread { context_editor, .. } => {
2243 let element = render_remaining_tokens(context_editor, cx)?;
2244
2245 Some(element.into_any_element())
2246 }
2247 ActiveView::ExternalAgentThread { .. }
2248 | ActiveView::History
2249 | ActiveView::Configuration => {
2250 return None;
2251 }
2252 }
2253 }
2254
2255 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2256 if TrialEndUpsell::dismissed() {
2257 return false;
2258 }
2259
2260 match &self.active_view {
2261 ActiveView::Thread { thread, .. } => {
2262 if thread
2263 .read(cx)
2264 .thread()
2265 .read(cx)
2266 .configured_model()
2267 .map_or(false, |model| {
2268 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2269 })
2270 {
2271 return false;
2272 }
2273 }
2274 ActiveView::TextThread { .. } => {
2275 if LanguageModelRegistry::global(cx)
2276 .read(cx)
2277 .default_model()
2278 .map_or(false, |model| {
2279 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2280 })
2281 {
2282 return false;
2283 }
2284 }
2285 ActiveView::ExternalAgentThread { .. }
2286 | ActiveView::History
2287 | ActiveView::Configuration => return false,
2288 }
2289
2290 let plan = self.user_store.read(cx).plan();
2291 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2292
2293 matches!(plan, Some(Plan::ZedFree)) && has_previous_trial
2294 }
2295
2296 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2297 if OnboardingUpsell::dismissed() {
2298 return false;
2299 }
2300
2301 match &self.active_view {
2302 ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
2303 let history_is_empty = self
2304 .history_store
2305 .update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
2306
2307 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2308 .providers()
2309 .iter()
2310 .any(|provider| {
2311 provider.is_authenticated(cx)
2312 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2313 });
2314
2315 history_is_empty || !has_configured_non_zed_providers
2316 }
2317 ActiveView::ExternalAgentThread { .. }
2318 | ActiveView::History
2319 | ActiveView::Configuration => false,
2320 }
2321 }
2322
2323 fn render_onboarding(
2324 &self,
2325 _window: &mut Window,
2326 cx: &mut Context<Self>,
2327 ) -> Option<impl IntoElement> {
2328 if !self.should_render_onboarding(cx) {
2329 return None;
2330 }
2331
2332 let thread_view = matches!(&self.active_view, ActiveView::Thread { .. });
2333 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2334
2335 Some(
2336 div()
2337 .when(thread_view, |this| {
2338 this.size_full().bg(cx.theme().colors().panel_background)
2339 })
2340 .when(text_thread_view, |this| {
2341 this.bg(cx.theme().colors().editor_background)
2342 })
2343 .child(self.onboarding.clone()),
2344 )
2345 }
2346
2347 fn render_backdrop(&self, cx: &mut Context<Self>) -> impl IntoElement {
2348 div()
2349 .size_full()
2350 .absolute()
2351 .inset_0()
2352 .bg(cx.theme().colors().panel_background)
2353 .opacity(0.8)
2354 .block_mouse_except_scroll()
2355 }
2356
2357 fn render_trial_end_upsell(
2358 &self,
2359 _window: &mut Window,
2360 cx: &mut Context<Self>,
2361 ) -> Option<impl IntoElement> {
2362 if !self.should_render_trial_end_upsell(cx) {
2363 return None;
2364 }
2365
2366 Some(
2367 v_flex()
2368 .absolute()
2369 .inset_0()
2370 .size_full()
2371 .bg(cx.theme().colors().panel_background)
2372 .opacity(0.85)
2373 .block_mouse_except_scroll()
2374 .child(EndTrialUpsell::new(Arc::new({
2375 let this = cx.entity();
2376 move |_, cx| {
2377 this.update(cx, |_this, cx| {
2378 TrialEndUpsell::set_dismissed(true, cx);
2379 cx.notify();
2380 });
2381 }
2382 }))),
2383 )
2384 }
2385
2386 fn render_empty_state_section_header(
2387 &self,
2388 label: impl Into<SharedString>,
2389 action_slot: Option<AnyElement>,
2390 cx: &mut Context<Self>,
2391 ) -> impl IntoElement {
2392 h_flex()
2393 .mt_2()
2394 .pl_1p5()
2395 .pb_1()
2396 .w_full()
2397 .justify_between()
2398 .border_b_1()
2399 .border_color(cx.theme().colors().border_variant)
2400 .child(
2401 Label::new(label.into())
2402 .size(LabelSize::Small)
2403 .color(Color::Muted),
2404 )
2405 .children(action_slot)
2406 }
2407
2408 fn render_thread_empty_state(
2409 &self,
2410 window: &mut Window,
2411 cx: &mut Context<Self>,
2412 ) -> impl IntoElement {
2413 let recent_history = self
2414 .history_store
2415 .update(cx, |this, cx| this.recent_entries(6, cx));
2416
2417 let model_registry = LanguageModelRegistry::read_global(cx);
2418
2419 let configuration_error =
2420 model_registry.configuration_error(model_registry.default_model(), cx);
2421
2422 let no_error = configuration_error.is_none();
2423 let focus_handle = self.focus_handle(cx);
2424
2425 v_flex()
2426 .size_full()
2427 .bg(cx.theme().colors().panel_background)
2428 .when(recent_history.is_empty(), |this| {
2429 this.child(
2430 v_flex()
2431 .size_full()
2432 .mx_auto()
2433 .justify_center()
2434 .items_center()
2435 .gap_1()
2436 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
2437 .when(no_error, |parent| {
2438 parent
2439 .child(h_flex().child(
2440 Label::new("Ask and build anything.").color(Color::Muted),
2441 ))
2442 .child(
2443 v_flex()
2444 .mt_2()
2445 .gap_1()
2446 .max_w_48()
2447 .child(
2448 Button::new("context", "Add Context")
2449 .label_size(LabelSize::Small)
2450 .icon(IconName::FileCode)
2451 .icon_position(IconPosition::Start)
2452 .icon_size(IconSize::Small)
2453 .icon_color(Color::Muted)
2454 .full_width()
2455 .key_binding(KeyBinding::for_action_in(
2456 &ToggleContextPicker,
2457 &focus_handle,
2458 window,
2459 cx,
2460 ))
2461 .on_click(|_event, window, cx| {
2462 window.dispatch_action(
2463 ToggleContextPicker.boxed_clone(),
2464 cx,
2465 )
2466 }),
2467 )
2468 .child(
2469 Button::new("mode", "Switch Model")
2470 .label_size(LabelSize::Small)
2471 .icon(IconName::DatabaseZap)
2472 .icon_position(IconPosition::Start)
2473 .icon_size(IconSize::Small)
2474 .icon_color(Color::Muted)
2475 .full_width()
2476 .key_binding(KeyBinding::for_action_in(
2477 &ToggleModelSelector,
2478 &focus_handle,
2479 window,
2480 cx,
2481 ))
2482 .on_click(|_event, window, cx| {
2483 window.dispatch_action(
2484 ToggleModelSelector.boxed_clone(),
2485 cx,
2486 )
2487 }),
2488 )
2489 .child(
2490 Button::new("settings", "View Settings")
2491 .label_size(LabelSize::Small)
2492 .icon(IconName::Settings)
2493 .icon_position(IconPosition::Start)
2494 .icon_size(IconSize::Small)
2495 .icon_color(Color::Muted)
2496 .full_width()
2497 .key_binding(KeyBinding::for_action_in(
2498 &OpenSettings,
2499 &focus_handle,
2500 window,
2501 cx,
2502 ))
2503 .on_click(|_event, window, cx| {
2504 window.dispatch_action(
2505 OpenSettings.boxed_clone(),
2506 cx,
2507 )
2508 }),
2509 ),
2510 )
2511 })
2512 .when_some(configuration_error.as_ref(), |this, err| {
2513 this.child(self.render_configuration_error(
2514 err,
2515 &focus_handle,
2516 window,
2517 cx,
2518 ))
2519 }),
2520 )
2521 })
2522 .when(!recent_history.is_empty(), |parent| {
2523 let focus_handle = focus_handle.clone();
2524 parent
2525 .overflow_hidden()
2526 .p_1p5()
2527 .justify_end()
2528 .gap_1()
2529 .child(
2530 self.render_empty_state_section_header(
2531 "Recent",
2532 Some(
2533 Button::new("view-history", "View All")
2534 .style(ButtonStyle::Subtle)
2535 .label_size(LabelSize::Small)
2536 .key_binding(
2537 KeyBinding::for_action_in(
2538 &OpenHistory,
2539 &self.focus_handle(cx),
2540 window,
2541 cx,
2542 )
2543 .map(|kb| kb.size(rems_from_px(12.))),
2544 )
2545 .on_click(move |_event, window, cx| {
2546 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2547 })
2548 .into_any_element(),
2549 ),
2550 cx,
2551 ),
2552 )
2553 .child(
2554 v_flex()
2555 .gap_1()
2556 .children(recent_history.into_iter().enumerate().map(
2557 |(index, entry)| {
2558 // TODO: Add keyboard navigation.
2559 let is_hovered =
2560 self.hovered_recent_history_item == Some(index);
2561 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2562 .hovered(is_hovered)
2563 .on_hover(cx.listener(
2564 move |this, is_hovered, _window, cx| {
2565 if *is_hovered {
2566 this.hovered_recent_history_item = Some(index);
2567 } else if this.hovered_recent_history_item
2568 == Some(index)
2569 {
2570 this.hovered_recent_history_item = None;
2571 }
2572 cx.notify();
2573 },
2574 ))
2575 .into_any_element()
2576 },
2577 )),
2578 )
2579 .child(self.render_empty_state_section_header("Start", None, cx))
2580 .child(
2581 v_flex()
2582 .p_1()
2583 .gap_2()
2584 .child(
2585 h_flex()
2586 .w_full()
2587 .gap_2()
2588 .child(
2589 NewThreadButton::new(
2590 "new-thread-btn",
2591 "New Thread",
2592 IconName::Thread,
2593 )
2594 .keybinding(KeyBinding::for_action_in(
2595 &NewThread::default(),
2596 &self.focus_handle(cx),
2597 window,
2598 cx,
2599 ))
2600 .on_click(
2601 |window, cx| {
2602 window.dispatch_action(
2603 NewThread::default().boxed_clone(),
2604 cx,
2605 )
2606 },
2607 ),
2608 )
2609 .child(
2610 NewThreadButton::new(
2611 "new-text-thread-btn",
2612 "New Text Thread",
2613 IconName::TextThread,
2614 )
2615 .keybinding(KeyBinding::for_action_in(
2616 &NewTextThread,
2617 &self.focus_handle(cx),
2618 window,
2619 cx,
2620 ))
2621 .on_click(
2622 |window, cx| {
2623 window.dispatch_action(Box::new(NewTextThread), cx)
2624 },
2625 ),
2626 ),
2627 )
2628 .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
2629 this.child(
2630 h_flex()
2631 .w_full()
2632 .gap_2()
2633 .child(
2634 NewThreadButton::new(
2635 "new-gemini-thread-btn",
2636 "New Gemini Thread",
2637 IconName::AiGemini,
2638 )
2639 // .keybinding(KeyBinding::for_action_in(
2640 // &OpenHistory,
2641 // &self.focus_handle(cx),
2642 // window,
2643 // cx,
2644 // ))
2645 .on_click(
2646 |window, cx| {
2647 window.dispatch_action(
2648 Box::new(NewExternalAgentThread {
2649 agent: Some(
2650 crate::ExternalAgent::Gemini,
2651 ),
2652 }),
2653 cx,
2654 )
2655 },
2656 ),
2657 )
2658 .child(
2659 NewThreadButton::new(
2660 "new-claude-thread-btn",
2661 "New Claude Code Thread",
2662 IconName::AiClaude,
2663 )
2664 // .keybinding(KeyBinding::for_action_in(
2665 // &OpenHistory,
2666 // &self.focus_handle(cx),
2667 // window,
2668 // cx,
2669 // ))
2670 .on_click(
2671 |window, cx| {
2672 window.dispatch_action(
2673 Box::new(NewExternalAgentThread {
2674 agent: Some(
2675 crate::ExternalAgent::ClaudeCode,
2676 ),
2677 }),
2678 cx,
2679 )
2680 },
2681 ),
2682 )
2683 .child(
2684 NewThreadButton::new(
2685 "new-native-agent-thread-btn",
2686 "New Native Agent Thread",
2687 IconName::ZedAssistant,
2688 )
2689 // .keybinding(KeyBinding::for_action_in(
2690 // &OpenHistory,
2691 // &self.focus_handle(cx),
2692 // window,
2693 // cx,
2694 // ))
2695 .on_click(
2696 |window, cx| {
2697 window.dispatch_action(
2698 Box::new(NewExternalAgentThread {
2699 agent: Some(
2700 crate::ExternalAgent::NativeAgent,
2701 ),
2702 }),
2703 cx,
2704 )
2705 },
2706 ),
2707 ),
2708 )
2709 }),
2710 )
2711 .when_some(configuration_error.as_ref(), |this, err| {
2712 this.child(self.render_configuration_error(err, &focus_handle, window, cx))
2713 })
2714 })
2715 }
2716
2717 fn render_configuration_error(
2718 &self,
2719 configuration_error: &ConfigurationError,
2720 focus_handle: &FocusHandle,
2721 window: &mut Window,
2722 cx: &mut App,
2723 ) -> impl IntoElement {
2724 match configuration_error {
2725 ConfigurationError::ModelNotFound
2726 | ConfigurationError::ProviderNotAuthenticated(_)
2727 | ConfigurationError::NoProvider => Banner::new()
2728 .severity(ui::Severity::Warning)
2729 .child(Label::new(configuration_error.to_string()))
2730 .action_slot(
2731 Button::new("settings", "Configure Provider")
2732 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2733 .label_size(LabelSize::Small)
2734 .key_binding(
2735 KeyBinding::for_action_in(&OpenSettings, &focus_handle, window, cx)
2736 .map(|kb| kb.size(rems_from_px(12.))),
2737 )
2738 .on_click(|_event, window, cx| {
2739 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2740 }),
2741 ),
2742 ConfigurationError::ProviderPendingTermsAcceptance(provider) => {
2743 Banner::new().severity(ui::Severity::Warning).child(
2744 h_flex().w_full().children(
2745 provider.render_accept_terms(
2746 LanguageModelProviderTosView::ThreadEmptyState,
2747 cx,
2748 ),
2749 ),
2750 )
2751 }
2752 }
2753 }
2754
2755 fn render_tool_use_limit_reached(
2756 &self,
2757 window: &mut Window,
2758 cx: &mut Context<Self>,
2759 ) -> Option<AnyElement> {
2760 let active_thread = match &self.active_view {
2761 ActiveView::Thread { thread, .. } => thread,
2762 ActiveView::ExternalAgentThread { .. } => {
2763 return None;
2764 }
2765 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2766 return None;
2767 }
2768 };
2769
2770 let thread = active_thread.read(cx).thread().read(cx);
2771
2772 let tool_use_limit_reached = thread.tool_use_limit_reached();
2773 if !tool_use_limit_reached {
2774 return None;
2775 }
2776
2777 let model = thread.configured_model()?.model;
2778
2779 let focus_handle = self.focus_handle(cx);
2780
2781 let banner = Banner::new()
2782 .severity(ui::Severity::Info)
2783 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
2784 .action_slot(
2785 h_flex()
2786 .gap_1()
2787 .child(
2788 Button::new("continue-conversation", "Continue")
2789 .layer(ElevationIndex::ModalSurface)
2790 .label_size(LabelSize::Small)
2791 .key_binding(
2792 KeyBinding::for_action_in(
2793 &ContinueThread,
2794 &focus_handle,
2795 window,
2796 cx,
2797 )
2798 .map(|kb| kb.size(rems_from_px(10.))),
2799 )
2800 .on_click(cx.listener(|this, _, window, cx| {
2801 this.continue_conversation(window, cx);
2802 })),
2803 )
2804 .when(model.supports_burn_mode(), |this| {
2805 this.child(
2806 Button::new("continue-burn-mode", "Continue with Burn Mode")
2807 .style(ButtonStyle::Filled)
2808 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2809 .layer(ElevationIndex::ModalSurface)
2810 .label_size(LabelSize::Small)
2811 .key_binding(
2812 KeyBinding::for_action_in(
2813 &ContinueWithBurnMode,
2814 &focus_handle,
2815 window,
2816 cx,
2817 )
2818 .map(|kb| kb.size(rems_from_px(10.))),
2819 )
2820 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
2821 .on_click({
2822 let active_thread = active_thread.clone();
2823 cx.listener(move |this, _, window, cx| {
2824 active_thread.update(cx, |active_thread, cx| {
2825 active_thread.thread().update(cx, |thread, _cx| {
2826 thread.set_completion_mode(CompletionMode::Burn);
2827 });
2828 });
2829 this.continue_conversation(window, cx);
2830 })
2831 }),
2832 )
2833 }),
2834 );
2835
2836 Some(div().px_2().pb_2().child(banner).into_any_element())
2837 }
2838
2839 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2840 let message = message.into();
2841
2842 IconButton::new("copy", IconName::Copy)
2843 .icon_size(IconSize::Small)
2844 .icon_color(Color::Muted)
2845 .tooltip(Tooltip::text("Copy Error Message"))
2846 .on_click(move |_, _, cx| {
2847 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2848 })
2849 }
2850
2851 fn dismiss_error_button(
2852 &self,
2853 thread: &Entity<ActiveThread>,
2854 cx: &mut Context<Self>,
2855 ) -> impl IntoElement {
2856 IconButton::new("dismiss", IconName::Close)
2857 .icon_size(IconSize::Small)
2858 .icon_color(Color::Muted)
2859 .tooltip(Tooltip::text("Dismiss Error"))
2860 .on_click(cx.listener({
2861 let thread = thread.clone();
2862 move |_, _, _, cx| {
2863 thread.update(cx, |this, _cx| {
2864 this.clear_last_error();
2865 });
2866
2867 cx.notify();
2868 }
2869 }))
2870 }
2871
2872 fn upgrade_button(
2873 &self,
2874 thread: &Entity<ActiveThread>,
2875 cx: &mut Context<Self>,
2876 ) -> impl IntoElement {
2877 Button::new("upgrade", "Upgrade")
2878 .label_size(LabelSize::Small)
2879 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2880 .on_click(cx.listener({
2881 let thread = thread.clone();
2882 move |_, _, _, cx| {
2883 thread.update(cx, |this, _cx| {
2884 this.clear_last_error();
2885 });
2886
2887 cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx));
2888 cx.notify();
2889 }
2890 }))
2891 }
2892
2893 fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
2894 cx.theme().status().error.opacity(0.08)
2895 }
2896
2897 fn render_payment_required_error(
2898 &self,
2899 thread: &Entity<ActiveThread>,
2900 cx: &mut Context<Self>,
2901 ) -> AnyElement {
2902 const ERROR_MESSAGE: &str =
2903 "You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
2904
2905 let icon = Icon::new(IconName::XCircle)
2906 .size(IconSize::Small)
2907 .color(Color::Error);
2908
2909 div()
2910 .border_t_1()
2911 .border_color(cx.theme().colors().border)
2912 .child(
2913 Callout::new()
2914 .icon(icon)
2915 .title("Free Usage Exceeded")
2916 .description(ERROR_MESSAGE)
2917 .tertiary_action(self.upgrade_button(thread, cx))
2918 .secondary_action(self.create_copy_button(ERROR_MESSAGE))
2919 .primary_action(self.dismiss_error_button(thread, cx))
2920 .bg_color(self.error_callout_bg(cx)),
2921 )
2922 .into_any_element()
2923 }
2924
2925 fn render_model_request_limit_reached_error(
2926 &self,
2927 plan: Plan,
2928 thread: &Entity<ActiveThread>,
2929 cx: &mut Context<Self>,
2930 ) -> AnyElement {
2931 let error_message = match plan {
2932 Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
2933 Plan::ZedProTrial | Plan::ZedFree => "Upgrade to Zed Pro for more prompts.",
2934 };
2935
2936 let icon = Icon::new(IconName::XCircle)
2937 .size(IconSize::Small)
2938 .color(Color::Error);
2939
2940 div()
2941 .border_t_1()
2942 .border_color(cx.theme().colors().border)
2943 .child(
2944 Callout::new()
2945 .icon(icon)
2946 .title("Model Prompt Limit Reached")
2947 .description(error_message)
2948 .tertiary_action(self.upgrade_button(thread, cx))
2949 .secondary_action(self.create_copy_button(error_message))
2950 .primary_action(self.dismiss_error_button(thread, cx))
2951 .bg_color(self.error_callout_bg(cx)),
2952 )
2953 .into_any_element()
2954 }
2955
2956 fn render_error_message(
2957 &self,
2958 header: SharedString,
2959 message: SharedString,
2960 thread: &Entity<ActiveThread>,
2961 cx: &mut Context<Self>,
2962 ) -> AnyElement {
2963 let message_with_header = format!("{}\n{}", header, message);
2964
2965 let icon = Icon::new(IconName::XCircle)
2966 .size(IconSize::Small)
2967 .color(Color::Error);
2968
2969 let retry_button = Button::new("retry", "Retry")
2970 .icon(IconName::RotateCw)
2971 .icon_position(IconPosition::Start)
2972 .icon_size(IconSize::Small)
2973 .label_size(LabelSize::Small)
2974 .on_click({
2975 let thread = thread.clone();
2976 move |_, window, cx| {
2977 thread.update(cx, |thread, cx| {
2978 thread.clear_last_error();
2979 thread.thread().update(cx, |thread, cx| {
2980 thread.retry_last_completion(Some(window.window_handle()), cx);
2981 });
2982 });
2983 }
2984 });
2985
2986 div()
2987 .border_t_1()
2988 .border_color(cx.theme().colors().border)
2989 .child(
2990 Callout::new()
2991 .icon(icon)
2992 .title(header)
2993 .description(message.clone())
2994 .primary_action(retry_button)
2995 .secondary_action(self.dismiss_error_button(thread, cx))
2996 .tertiary_action(self.create_copy_button(message_with_header))
2997 .bg_color(self.error_callout_bg(cx)),
2998 )
2999 .into_any_element()
3000 }
3001
3002 fn render_retryable_error(
3003 &self,
3004 message: SharedString,
3005 can_enable_burn_mode: bool,
3006 thread: &Entity<ActiveThread>,
3007 cx: &mut Context<Self>,
3008 ) -> AnyElement {
3009 let icon = Icon::new(IconName::XCircle)
3010 .size(IconSize::Small)
3011 .color(Color::Error);
3012
3013 let retry_button = Button::new("retry", "Retry")
3014 .icon(IconName::RotateCw)
3015 .icon_position(IconPosition::Start)
3016 .icon_size(IconSize::Small)
3017 .label_size(LabelSize::Small)
3018 .on_click({
3019 let thread = thread.clone();
3020 move |_, window, cx| {
3021 thread.update(cx, |thread, cx| {
3022 thread.clear_last_error();
3023 thread.thread().update(cx, |thread, cx| {
3024 thread.retry_last_completion(Some(window.window_handle()), cx);
3025 });
3026 });
3027 }
3028 });
3029
3030 let mut callout = Callout::new()
3031 .icon(icon)
3032 .title("Error")
3033 .description(message.clone())
3034 .bg_color(self.error_callout_bg(cx))
3035 .primary_action(retry_button);
3036
3037 if can_enable_burn_mode {
3038 let burn_mode_button = Button::new("enable_burn_retry", "Enable Burn Mode and Retry")
3039 .icon(IconName::ZedBurnMode)
3040 .icon_position(IconPosition::Start)
3041 .icon_size(IconSize::Small)
3042 .label_size(LabelSize::Small)
3043 .on_click({
3044 let thread = thread.clone();
3045 move |_, window, cx| {
3046 thread.update(cx, |thread, cx| {
3047 thread.clear_last_error();
3048 thread.thread().update(cx, |thread, cx| {
3049 thread.enable_burn_mode_and_retry(Some(window.window_handle()), cx);
3050 });
3051 });
3052 }
3053 });
3054 callout = callout.secondary_action(burn_mode_button);
3055 }
3056
3057 div()
3058 .border_t_1()
3059 .border_color(cx.theme().colors().border)
3060 .child(callout)
3061 .into_any_element()
3062 }
3063
3064 fn render_prompt_editor(
3065 &self,
3066 context_editor: &Entity<TextThreadEditor>,
3067 buffer_search_bar: &Entity<BufferSearchBar>,
3068 window: &mut Window,
3069 cx: &mut Context<Self>,
3070 ) -> Div {
3071 let mut registrar = buffer_search::DivRegistrar::new(
3072 |this, _, _cx| match &this.active_view {
3073 ActiveView::TextThread {
3074 buffer_search_bar, ..
3075 } => Some(buffer_search_bar.clone()),
3076 _ => None,
3077 },
3078 cx,
3079 );
3080 BufferSearchBar::register(&mut registrar);
3081 registrar
3082 .into_div()
3083 .size_full()
3084 .relative()
3085 .map(|parent| {
3086 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3087 if buffer_search_bar.is_dismissed() {
3088 return parent;
3089 }
3090 parent.child(
3091 div()
3092 .p(DynamicSpacing::Base08.rems(cx))
3093 .border_b_1()
3094 .border_color(cx.theme().colors().border_variant)
3095 .bg(cx.theme().colors().editor_background)
3096 .child(buffer_search_bar.render(window, cx)),
3097 )
3098 })
3099 })
3100 .child(context_editor.clone())
3101 .child(self.render_drag_target(cx))
3102 }
3103
3104 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3105 let is_local = self.project.read(cx).is_local();
3106 div()
3107 .invisible()
3108 .absolute()
3109 .top_0()
3110 .right_0()
3111 .bottom_0()
3112 .left_0()
3113 .bg(cx.theme().colors().drop_target_background)
3114 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3115 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3116 .when(is_local, |this| {
3117 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3118 })
3119 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3120 let item = tab.pane.read(cx).item_for_index(tab.ix);
3121 let project_paths = item
3122 .and_then(|item| item.project_path(cx))
3123 .into_iter()
3124 .collect::<Vec<_>>();
3125 this.handle_drop(project_paths, vec![], window, cx);
3126 }))
3127 .on_drop(
3128 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3129 let project_paths = selection
3130 .items()
3131 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3132 .collect::<Vec<_>>();
3133 this.handle_drop(project_paths, vec![], window, cx);
3134 }),
3135 )
3136 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3137 let tasks = paths
3138 .paths()
3139 .into_iter()
3140 .map(|path| {
3141 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
3142 })
3143 .collect::<Vec<_>>();
3144 cx.spawn_in(window, async move |this, cx| {
3145 let mut paths = vec![];
3146 let mut added_worktrees = vec![];
3147 let opened_paths = futures::future::join_all(tasks).await;
3148 for entry in opened_paths {
3149 if let Some((worktree, project_path)) = entry.log_err() {
3150 added_worktrees.push(worktree);
3151 paths.push(project_path);
3152 }
3153 }
3154 this.update_in(cx, |this, window, cx| {
3155 this.handle_drop(paths, added_worktrees, window, cx);
3156 })
3157 .ok();
3158 })
3159 .detach();
3160 }))
3161 }
3162
3163 fn handle_drop(
3164 &mut self,
3165 paths: Vec<ProjectPath>,
3166 added_worktrees: Vec<Entity<Worktree>>,
3167 window: &mut Window,
3168 cx: &mut Context<Self>,
3169 ) {
3170 match &self.active_view {
3171 ActiveView::Thread { thread, .. } => {
3172 let context_store = thread.read(cx).context_store().clone();
3173 context_store.update(cx, move |context_store, cx| {
3174 let mut tasks = Vec::new();
3175 for project_path in &paths {
3176 tasks.push(context_store.add_file_from_path(
3177 project_path.clone(),
3178 false,
3179 cx,
3180 ));
3181 }
3182 cx.background_spawn(async move {
3183 futures::future::join_all(tasks).await;
3184 // Need to hold onto the worktrees until they have already been used when
3185 // opening the buffers.
3186 drop(added_worktrees);
3187 })
3188 .detach();
3189 });
3190 }
3191 ActiveView::ExternalAgentThread { thread_view } => {
3192 thread_view.update(cx, |thread_view, cx| {
3193 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3194 });
3195 }
3196 ActiveView::TextThread { context_editor, .. } => {
3197 context_editor.update(cx, |context_editor, cx| {
3198 TextThreadEditor::insert_dragged_files(
3199 context_editor,
3200 paths,
3201 added_worktrees,
3202 window,
3203 cx,
3204 );
3205 });
3206 }
3207 ActiveView::History | ActiveView::Configuration => {}
3208 }
3209 }
3210
3211 fn key_context(&self) -> KeyContext {
3212 let mut key_context = KeyContext::new_with_defaults();
3213 key_context.add("AgentPanel");
3214 match &self.active_view {
3215 ActiveView::ExternalAgentThread { .. } => key_context.add("external_agent_thread"),
3216 ActiveView::TextThread { .. } => key_context.add("prompt_editor"),
3217 ActiveView::Thread { .. } | ActiveView::History | ActiveView::Configuration => {}
3218 }
3219 key_context
3220 }
3221}
3222
3223impl Render for AgentPanel {
3224 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3225 // WARNING: Changes to this element hierarchy can have
3226 // non-obvious implications to the layout of children.
3227 //
3228 // If you need to change it, please confirm:
3229 // - The message editor expands (cmd-option-esc) correctly
3230 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3231 // - Font size works as expected and can be changed with cmd-+/cmd-
3232 // - Scrolling in all views works as expected
3233 // - Files can be dropped into the panel
3234 let content = v_flex()
3235 .relative()
3236 .size_full()
3237 .justify_between()
3238 .key_context(self.key_context())
3239 .on_action(cx.listener(Self::cancel))
3240 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3241 this.new_thread(action, window, cx);
3242 }))
3243 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3244 this.open_history(window, cx);
3245 }))
3246 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3247 this.open_configuration(window, cx);
3248 }))
3249 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3250 .on_action(cx.listener(Self::deploy_rules_library))
3251 .on_action(cx.listener(Self::open_agent_diff))
3252 .on_action(cx.listener(Self::go_back))
3253 .on_action(cx.listener(Self::toggle_navigation_menu))
3254 .on_action(cx.listener(Self::toggle_options_menu))
3255 .on_action(cx.listener(Self::increase_font_size))
3256 .on_action(cx.listener(Self::decrease_font_size))
3257 .on_action(cx.listener(Self::reset_font_size))
3258 .on_action(cx.listener(Self::toggle_zoom))
3259 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3260 this.continue_conversation(window, cx);
3261 }))
3262 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3263 match &this.active_view {
3264 ActiveView::Thread { thread, .. } => {
3265 thread.update(cx, |active_thread, cx| {
3266 active_thread.thread().update(cx, |thread, _cx| {
3267 thread.set_completion_mode(CompletionMode::Burn);
3268 });
3269 });
3270 this.continue_conversation(window, cx);
3271 }
3272 ActiveView::ExternalAgentThread { .. } => {}
3273 ActiveView::TextThread { .. }
3274 | ActiveView::History
3275 | ActiveView::Configuration => {}
3276 }
3277 }))
3278 .on_action(cx.listener(Self::toggle_burn_mode))
3279 .child(self.render_toolbar(window, cx))
3280 .children(self.render_onboarding(window, cx))
3281 .map(|parent| match &self.active_view {
3282 ActiveView::Thread {
3283 thread,
3284 message_editor,
3285 ..
3286 } => parent
3287 .child(
3288 if thread.read(cx).is_empty() && !self.should_render_onboarding(cx) {
3289 self.render_thread_empty_state(window, cx)
3290 .into_any_element()
3291 } else {
3292 thread.clone().into_any_element()
3293 },
3294 )
3295 .children(self.render_tool_use_limit_reached(window, cx))
3296 .when_some(thread.read(cx).last_error(), |this, last_error| {
3297 this.child(
3298 div()
3299 .child(match last_error {
3300 ThreadError::PaymentRequired => {
3301 self.render_payment_required_error(thread, cx)
3302 }
3303 ThreadError::ModelRequestLimitReached { plan } => self
3304 .render_model_request_limit_reached_error(plan, thread, cx),
3305 ThreadError::Message { header, message } => {
3306 self.render_error_message(header, message, thread, cx)
3307 }
3308 ThreadError::RetryableError {
3309 message,
3310 can_enable_burn_mode,
3311 } => self.render_retryable_error(
3312 message,
3313 can_enable_burn_mode,
3314 thread,
3315 cx,
3316 ),
3317 })
3318 .into_any(),
3319 )
3320 })
3321 .child(h_flex().relative().child(message_editor.clone()).when(
3322 !LanguageModelRegistry::read_global(cx).has_authenticated_provider(cx),
3323 |this| this.child(self.render_backdrop(cx)),
3324 ))
3325 .child(self.render_drag_target(cx)),
3326 ActiveView::ExternalAgentThread { thread_view, .. } => parent
3327 .child(thread_view.clone())
3328 .child(self.render_drag_target(cx)),
3329 ActiveView::History => parent.child(self.history.clone()),
3330 ActiveView::TextThread {
3331 context_editor,
3332 buffer_search_bar,
3333 ..
3334 } => {
3335 let model_registry = LanguageModelRegistry::read_global(cx);
3336 let configuration_error =
3337 model_registry.configuration_error(model_registry.default_model(), cx);
3338 parent
3339 .map(|this| {
3340 if !self.should_render_onboarding(cx)
3341 && let Some(err) = configuration_error.as_ref()
3342 {
3343 this.child(
3344 div().bg(cx.theme().colors().editor_background).p_2().child(
3345 self.render_configuration_error(
3346 err,
3347 &self.focus_handle(cx),
3348 window,
3349 cx,
3350 ),
3351 ),
3352 )
3353 } else {
3354 this
3355 }
3356 })
3357 .child(self.render_prompt_editor(
3358 context_editor,
3359 buffer_search_bar,
3360 window,
3361 cx,
3362 ))
3363 }
3364 ActiveView::Configuration => parent.children(self.configuration.clone()),
3365 })
3366 .children(self.render_trial_end_upsell(window, cx));
3367
3368 match self.active_view.which_font_size_used() {
3369 WhichFontSize::AgentFont => {
3370 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3371 .size_full()
3372 .child(content)
3373 .into_any()
3374 }
3375 _ => content.into_any(),
3376 }
3377 }
3378}
3379
3380struct PromptLibraryInlineAssist {
3381 workspace: WeakEntity<Workspace>,
3382}
3383
3384impl PromptLibraryInlineAssist {
3385 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3386 Self { workspace }
3387 }
3388}
3389
3390impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3391 fn assist(
3392 &self,
3393 prompt_editor: &Entity<Editor>,
3394 initial_prompt: Option<String>,
3395 window: &mut Window,
3396 cx: &mut Context<RulesLibrary>,
3397 ) {
3398 InlineAssistant::update_global(cx, |assistant, cx| {
3399 let Some(project) = self
3400 .workspace
3401 .upgrade()
3402 .map(|workspace| workspace.read(cx).project().downgrade())
3403 else {
3404 return;
3405 };
3406 let prompt_store = None;
3407 let thread_store = None;
3408 let text_thread_store = None;
3409 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3410 assistant.assist(
3411 &prompt_editor,
3412 self.workspace.clone(),
3413 context_store,
3414 project,
3415 prompt_store,
3416 thread_store,
3417 text_thread_store,
3418 initial_prompt,
3419 window,
3420 cx,
3421 )
3422 })
3423 }
3424
3425 fn focus_agent_panel(
3426 &self,
3427 workspace: &mut Workspace,
3428 window: &mut Window,
3429 cx: &mut Context<Workspace>,
3430 ) -> bool {
3431 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3432 }
3433}
3434
3435pub struct ConcreteAssistantPanelDelegate;
3436
3437impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3438 fn active_context_editor(
3439 &self,
3440 workspace: &mut Workspace,
3441 _window: &mut Window,
3442 cx: &mut Context<Workspace>,
3443 ) -> Option<Entity<TextThreadEditor>> {
3444 let panel = workspace.panel::<AgentPanel>(cx)?;
3445 panel.read(cx).active_context_editor()
3446 }
3447
3448 fn open_saved_context(
3449 &self,
3450 workspace: &mut Workspace,
3451 path: Arc<Path>,
3452 window: &mut Window,
3453 cx: &mut Context<Workspace>,
3454 ) -> Task<Result<()>> {
3455 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3456 return Task::ready(Err(anyhow!("Agent panel not found")));
3457 };
3458
3459 panel.update(cx, |panel, cx| {
3460 panel.open_saved_prompt_editor(path, window, cx)
3461 })
3462 }
3463
3464 fn open_remote_context(
3465 &self,
3466 _workspace: &mut Workspace,
3467 _context_id: assistant_context::ContextId,
3468 _window: &mut Window,
3469 _cx: &mut Context<Workspace>,
3470 ) -> Task<Result<Entity<TextThreadEditor>>> {
3471 Task::ready(Err(anyhow!("opening remote context not implemented")))
3472 }
3473
3474 fn quote_selection(
3475 &self,
3476 workspace: &mut Workspace,
3477 selection_ranges: Vec<Range<Anchor>>,
3478 buffer: Entity<MultiBuffer>,
3479 window: &mut Window,
3480 cx: &mut Context<Workspace>,
3481 ) {
3482 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3483 return;
3484 };
3485
3486 if !panel.focus_handle(cx).contains_focused(window, cx) {
3487 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3488 }
3489
3490 panel.update(cx, |_, cx| {
3491 // Wait to create a new context until the workspace is no longer
3492 // being updated.
3493 cx.defer_in(window, move |panel, window, cx| {
3494 if let Some(message_editor) = panel.active_message_editor() {
3495 message_editor.update(cx, |message_editor, cx| {
3496 message_editor.context_store().update(cx, |store, cx| {
3497 let buffer = buffer.read(cx);
3498 let selection_ranges = selection_ranges
3499 .into_iter()
3500 .flat_map(|range| {
3501 let (start_buffer, start) =
3502 buffer.text_anchor_for_position(range.start, cx)?;
3503 let (end_buffer, end) =
3504 buffer.text_anchor_for_position(range.end, cx)?;
3505 if start_buffer != end_buffer {
3506 return None;
3507 }
3508 Some((start_buffer, start..end))
3509 })
3510 .collect::<Vec<_>>();
3511
3512 for (buffer, range) in selection_ranges {
3513 store.add_selection(buffer, range, cx);
3514 }
3515 })
3516 })
3517 } else if let Some(context_editor) = panel.active_context_editor() {
3518 let snapshot = buffer.read(cx).snapshot(cx);
3519 let selection_ranges = selection_ranges
3520 .into_iter()
3521 .map(|range| range.to_point(&snapshot))
3522 .collect::<Vec<_>>();
3523
3524 context_editor.update(cx, |context_editor, cx| {
3525 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3526 });
3527 }
3528 });
3529 });
3530 }
3531}
3532
3533struct OnboardingUpsell;
3534
3535impl Dismissable for OnboardingUpsell {
3536 const KEY: &'static str = "dismissed-trial-upsell";
3537}
3538
3539struct TrialEndUpsell;
3540
3541impl Dismissable for TrialEndUpsell {
3542 const KEY: &'static str = "dismissed-trial-end-upsell";
3543}