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