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