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