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