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