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(window, cx));
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 = cx.new(|_cx| agent2::ThreadElement::new(thread))?;
923 this.update_in(cx, |this, window, cx| {
924 this.set_active_view(ActiveView::Agent2Thread { thread_element }, window, cx);
925 })
926 })
927 .detach();
928 }
929
930 fn deploy_rules_library(
931 &mut self,
932 action: &OpenRulesLibrary,
933 _window: &mut Window,
934 cx: &mut Context<Self>,
935 ) {
936 open_rules_library(
937 self.language_registry.clone(),
938 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
939 Rc::new(|| {
940 Rc::new(SlashCommandCompletionProvider::new(
941 Arc::new(SlashCommandWorkingSet::default()),
942 None,
943 None,
944 ))
945 }),
946 action
947 .prompt_to_select
948 .map(|uuid| UserPromptId(uuid).into()),
949 cx,
950 )
951 .detach_and_log_err(cx);
952 }
953
954 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
955 if matches!(self.active_view, ActiveView::History) {
956 if let Some(previous_view) = self.previous_view.take() {
957 self.set_active_view(previous_view, window, cx);
958 }
959 } else {
960 self.thread_store
961 .update(cx, |thread_store, cx| thread_store.reload(cx))
962 .detach_and_log_err(cx);
963 self.set_active_view(ActiveView::History, window, cx);
964 }
965 cx.notify();
966 }
967
968 pub(crate) fn open_saved_prompt_editor(
969 &mut self,
970 path: Arc<Path>,
971 window: &mut Window,
972 cx: &mut Context<Self>,
973 ) -> Task<Result<()>> {
974 let context = self
975 .context_store
976 .update(cx, |store, cx| store.open_local_context(path, cx));
977 cx.spawn_in(window, async move |this, cx| {
978 let context = context.await?;
979 this.update_in(cx, |this, window, cx| {
980 this.open_prompt_editor(context, window, cx);
981 })
982 })
983 }
984
985 pub(crate) fn open_prompt_editor(
986 &mut self,
987 context: Entity<AssistantContext>,
988 window: &mut Window,
989 cx: &mut Context<Self>,
990 ) {
991 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
992 .log_err()
993 .flatten();
994 let editor = cx.new(|cx| {
995 TextThreadEditor::for_context(
996 context,
997 self.fs.clone(),
998 self.workspace.clone(),
999 self.project.clone(),
1000 lsp_adapter_delegate,
1001 window,
1002 cx,
1003 )
1004 });
1005 self.set_active_view(
1006 ActiveView::prompt_editor(
1007 editor.clone(),
1008 self.history_store.clone(),
1009 self.language_registry.clone(),
1010 window,
1011 cx,
1012 ),
1013 window,
1014 cx,
1015 );
1016 }
1017
1018 pub(crate) fn open_thread_by_id(
1019 &mut self,
1020 thread_id: &ThreadId,
1021 window: &mut Window,
1022 cx: &mut Context<Self>,
1023 ) -> Task<Result<()>> {
1024 let open_thread_task = self
1025 .thread_store
1026 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
1027 cx.spawn_in(window, async move |this, cx| {
1028 let thread = open_thread_task.await?;
1029 this.update_in(cx, |this, window, cx| {
1030 this.open_thread(thread, window, cx);
1031 anyhow::Ok(())
1032 })??;
1033 Ok(())
1034 })
1035 }
1036
1037 pub(crate) fn open_thread(
1038 &mut self,
1039 thread: Entity<Thread>,
1040 window: &mut Window,
1041 cx: &mut Context<Self>,
1042 ) {
1043 let context_store = cx.new(|_cx| {
1044 ContextStore::new(
1045 self.project.downgrade(),
1046 Some(self.thread_store.downgrade()),
1047 )
1048 });
1049
1050 let active_thread = cx.new(|cx| {
1051 ActiveThread::new(
1052 thread.clone(),
1053 self.thread_store.clone(),
1054 self.context_store.clone(),
1055 context_store.clone(),
1056 self.language_registry.clone(),
1057 self.workspace.clone(),
1058 window,
1059 cx,
1060 )
1061 });
1062
1063 let message_editor = cx.new(|cx| {
1064 MessageEditor::new(
1065 self.fs.clone(),
1066 self.workspace.clone(),
1067 self.user_store.clone(),
1068 context_store,
1069 self.prompt_store.clone(),
1070 self.thread_store.downgrade(),
1071 self.context_store.downgrade(),
1072 thread.clone(),
1073 window,
1074 cx,
1075 )
1076 });
1077 message_editor.focus_handle(cx).focus(window);
1078
1079 let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
1080 self.set_active_view(thread_view, window, cx);
1081 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
1082 }
1083
1084 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1085 match self.active_view {
1086 ActiveView::Configuration | ActiveView::History => {
1087 // todo! check go back works correctly
1088 if let Some(previous_view) = self.previous_view.take() {
1089 self.active_view = previous_view;
1090
1091 match &self.active_view {
1092 ActiveView::Thread { message_editor, .. } => {
1093 message_editor.focus_handle(cx).focus(window);
1094 }
1095 ActiveView::Agent2Thread { .. } => {
1096 todo!()
1097 }
1098 ActiveView::TextThread { context_editor, .. } => {
1099 context_editor.focus_handle(cx).focus(window);
1100 }
1101 ActiveView::History | ActiveView::Configuration => {}
1102 }
1103 }
1104 cx.notify();
1105 }
1106 _ => {}
1107 }
1108 }
1109
1110 pub fn toggle_navigation_menu(
1111 &mut self,
1112 _: &ToggleNavigationMenu,
1113 window: &mut Window,
1114 cx: &mut Context<Self>,
1115 ) {
1116 self.assistant_navigation_menu_handle.toggle(window, cx);
1117 }
1118
1119 pub fn toggle_options_menu(
1120 &mut self,
1121 _: &ToggleOptionsMenu,
1122 window: &mut Window,
1123 cx: &mut Context<Self>,
1124 ) {
1125 self.assistant_dropdown_menu_handle.toggle(window, cx);
1126 }
1127
1128 pub fn increase_font_size(
1129 &mut self,
1130 action: &IncreaseBufferFontSize,
1131 _: &mut Window,
1132 cx: &mut Context<Self>,
1133 ) {
1134 self.handle_font_size_action(action.persist, px(1.0), cx);
1135 }
1136
1137 pub fn decrease_font_size(
1138 &mut self,
1139 action: &DecreaseBufferFontSize,
1140 _: &mut Window,
1141 cx: &mut Context<Self>,
1142 ) {
1143 self.handle_font_size_action(action.persist, px(-1.0), cx);
1144 }
1145
1146 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1147 match self.active_view.which_font_size_used() {
1148 WhichFontSize::AgentFont => {
1149 if persist {
1150 update_settings_file::<ThemeSettings>(
1151 self.fs.clone(),
1152 cx,
1153 move |settings, cx| {
1154 let agent_font_size =
1155 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1156 let _ = settings
1157 .agent_font_size
1158 .insert(theme::clamp_font_size(agent_font_size).0);
1159 },
1160 );
1161 } else {
1162 theme::adjust_agent_font_size(cx, |size| {
1163 *size += delta;
1164 });
1165 }
1166 }
1167 WhichFontSize::BufferFont => {
1168 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1169 // default handler that changes that font size.
1170 cx.propagate();
1171 }
1172 WhichFontSize::None => {}
1173 }
1174 }
1175
1176 pub fn reset_font_size(
1177 &mut self,
1178 action: &ResetBufferFontSize,
1179 _: &mut Window,
1180 cx: &mut Context<Self>,
1181 ) {
1182 if action.persist {
1183 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1184 settings.agent_font_size = None;
1185 });
1186 } else {
1187 theme::reset_agent_font_size(cx);
1188 }
1189 }
1190
1191 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1192 if self.zoomed {
1193 cx.emit(PanelEvent::ZoomOut);
1194 } else {
1195 if !self.focus_handle(cx).contains_focused(window, cx) {
1196 cx.focus_self(window);
1197 }
1198 cx.emit(PanelEvent::ZoomIn);
1199 }
1200 }
1201
1202 pub fn open_agent_diff(
1203 &mut self,
1204 _: &OpenAgentDiff,
1205 window: &mut Window,
1206 cx: &mut Context<Self>,
1207 ) {
1208 match &self.active_view {
1209 ActiveView::Thread { thread, .. } => {
1210 let thread = thread.read(cx).thread().clone();
1211 self.workspace
1212 .update(cx, |workspace, cx| {
1213 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1214 })
1215 .log_err();
1216 }
1217 ActiveView::Agent2Thread { .. } => todo!(),
1218 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1219 }
1220 }
1221
1222 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1223 let context_server_store = self.project.read(cx).context_server_store();
1224 let tools = self.thread_store.read(cx).tools();
1225 let fs = self.fs.clone();
1226
1227 self.set_active_view(ActiveView::Configuration, window, cx);
1228 self.configuration = Some(cx.new(|cx| {
1229 AgentConfiguration::new(
1230 fs,
1231 context_server_store,
1232 tools,
1233 self.language_registry.clone(),
1234 self.workspace.clone(),
1235 window,
1236 cx,
1237 )
1238 }));
1239
1240 if let Some(configuration) = self.configuration.as_ref() {
1241 self.configuration_subscription = Some(cx.subscribe_in(
1242 configuration,
1243 window,
1244 Self::handle_agent_configuration_event,
1245 ));
1246
1247 configuration.focus_handle(cx).focus(window);
1248 }
1249 }
1250
1251 pub(crate) fn open_active_thread_as_markdown(
1252 &mut self,
1253 _: &OpenActiveThreadAsMarkdown,
1254 window: &mut Window,
1255 cx: &mut Context<Self>,
1256 ) {
1257 let Some(workspace) = self.workspace.upgrade() else {
1258 return;
1259 };
1260
1261 match &self.active_view {
1262 ActiveView::Thread { thread, .. } => {
1263 active_thread::open_active_thread_as_markdown(
1264 thread.read(cx).thread().clone(),
1265 workspace,
1266 window,
1267 cx,
1268 )
1269 .detach_and_log_err(cx);
1270 }
1271 ActiveView::Agent2Thread { .. } => {
1272 todo!()
1273 }
1274 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1275 }
1276 }
1277
1278 fn handle_agent_configuration_event(
1279 &mut self,
1280 _entity: &Entity<AgentConfiguration>,
1281 event: &AssistantConfigurationEvent,
1282 window: &mut Window,
1283 cx: &mut Context<Self>,
1284 ) {
1285 match event {
1286 AssistantConfigurationEvent::NewThread(provider) => {
1287 if LanguageModelRegistry::read_global(cx)
1288 .default_model()
1289 .map_or(true, |model| model.provider.id() != provider.id())
1290 {
1291 if let Some(model) = provider.default_model(cx) {
1292 update_settings_file::<AgentSettings>(
1293 self.fs.clone(),
1294 cx,
1295 move |settings, _| settings.set_model(model),
1296 );
1297 }
1298 }
1299
1300 self.new_thread(&NewThread::default(), window, cx);
1301 }
1302 }
1303 }
1304
1305 pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
1306 match &self.active_view {
1307 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1308 ActiveView::Agent2Thread { .. } => {
1309 // todo!
1310 None
1311 }
1312 _ => None,
1313 }
1314 }
1315
1316 pub(crate) fn delete_thread(
1317 &mut self,
1318 thread_id: &ThreadId,
1319 cx: &mut Context<Self>,
1320 ) -> Task<Result<()>> {
1321 self.thread_store
1322 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1323 }
1324
1325 fn continue_conversation(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1326 let ActiveView::Thread { thread, .. } = &self.active_view else {
1327 return;
1328 };
1329
1330 let thread_state = thread.read(cx).thread().read(cx);
1331 if !thread_state.tool_use_limit_reached() {
1332 return;
1333 }
1334
1335 let model = thread_state.configured_model().map(|cm| cm.model.clone());
1336 if let Some(model) = model {
1337 thread.update(cx, |active_thread, cx| {
1338 active_thread.thread().update(cx, |thread, cx| {
1339 thread.insert_invisible_continue_message(cx);
1340 thread.advance_prompt_id();
1341 thread.send_to_model(
1342 model,
1343 CompletionIntent::UserPrompt,
1344 Some(window.window_handle()),
1345 cx,
1346 );
1347 });
1348 });
1349 } else {
1350 log::warn!("No configured model available for continuation");
1351 }
1352 }
1353
1354 fn toggle_burn_mode(
1355 &mut self,
1356 _: &ToggleBurnMode,
1357 _window: &mut Window,
1358 cx: &mut Context<Self>,
1359 ) {
1360 let ActiveView::Thread { thread, .. } = &self.active_view else {
1361 return;
1362 };
1363
1364 thread.update(cx, |active_thread, cx| {
1365 active_thread.thread().update(cx, |thread, _cx| {
1366 let current_mode = thread.completion_mode();
1367
1368 thread.set_completion_mode(match current_mode {
1369 CompletionMode::Burn => CompletionMode::Normal,
1370 CompletionMode::Normal => CompletionMode::Burn,
1371 });
1372 });
1373 });
1374 }
1375
1376 pub(crate) fn active_context_editor(&self) -> Option<Entity<TextThreadEditor>> {
1377 match &self.active_view {
1378 ActiveView::TextThread { context_editor, .. } => Some(context_editor.clone()),
1379 _ => None,
1380 }
1381 }
1382
1383 pub(crate) fn delete_context(
1384 &mut self,
1385 path: Arc<Path>,
1386 cx: &mut Context<Self>,
1387 ) -> Task<Result<()>> {
1388 self.context_store
1389 .update(cx, |this, cx| this.delete_local_context(path, cx))
1390 }
1391
1392 fn set_active_view(
1393 &mut self,
1394 new_view: ActiveView,
1395 window: &mut Window,
1396 cx: &mut Context<Self>,
1397 ) {
1398 let current_is_history = matches!(self.active_view, ActiveView::History);
1399 let new_is_history = matches!(new_view, ActiveView::History);
1400
1401 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1402 let new_is_config = matches!(new_view, ActiveView::Configuration);
1403
1404 let current_is_special = current_is_history || current_is_config;
1405 let new_is_special = new_is_history || new_is_config;
1406
1407 match &self.active_view {
1408 ActiveView::Thread { thread, .. } => {
1409 let thread = thread.read(cx);
1410 if thread.is_empty() {
1411 let id = thread.thread().read(cx).id().clone();
1412 self.history_store.update(cx, |store, cx| {
1413 store.remove_recently_opened_thread(id, cx);
1414 });
1415 }
1416 }
1417 ActiveView::Agent2Thread { .. } => {
1418 // todo!
1419 }
1420 _ => {}
1421 }
1422
1423 match &new_view {
1424 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1425 let id = thread.read(cx).thread().read(cx).id().clone();
1426 store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
1427 }),
1428 ActiveView::TextThread { context_editor, .. } => {
1429 self.history_store.update(cx, |store, cx| {
1430 if let Some(path) = context_editor.read(cx).context().read(cx).path() {
1431 store.push_recently_opened_entry(HistoryEntryId::Context(path.clone()), cx)
1432 }
1433 })
1434 }
1435 ActiveView::Agent2Thread { .. } => {
1436 // todo! push history entry
1437 }
1438 _ => {}
1439 }
1440
1441 if current_is_special && !new_is_special {
1442 self.active_view = new_view;
1443 } else if !current_is_special && new_is_special {
1444 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1445 } else {
1446 if !new_is_special {
1447 self.previous_view = None;
1448 }
1449 self.active_view = new_view;
1450 }
1451
1452 self.focus_handle(cx).focus(window);
1453 }
1454
1455 fn populate_recently_opened_menu_section(
1456 mut menu: ContextMenu,
1457 panel: Entity<Self>,
1458 cx: &mut Context<ContextMenu>,
1459 ) -> ContextMenu {
1460 let entries = panel
1461 .read(cx)
1462 .history_store
1463 .read(cx)
1464 .recently_opened_entries(cx);
1465
1466 if entries.is_empty() {
1467 return menu;
1468 }
1469
1470 menu = menu.header("Recently Opened");
1471
1472 for entry in entries {
1473 let title = entry.title().clone();
1474 let id = entry.id();
1475
1476 menu = menu.entry_with_end_slot_on_hover(
1477 title,
1478 None,
1479 {
1480 let panel = panel.downgrade();
1481 let id = id.clone();
1482 move |window, cx| {
1483 let id = id.clone();
1484 panel
1485 .update(cx, move |this, cx| match id {
1486 HistoryEntryId::Thread(id) => this
1487 .open_thread_by_id(&id, window, cx)
1488 .detach_and_log_err(cx),
1489 HistoryEntryId::Context(path) => this
1490 .open_saved_prompt_editor(path.clone(), window, cx)
1491 .detach_and_log_err(cx),
1492 })
1493 .ok();
1494 }
1495 },
1496 IconName::Close,
1497 "Close Entry".into(),
1498 {
1499 let panel = panel.downgrade();
1500 let id = id.clone();
1501 move |_window, cx| {
1502 panel
1503 .update(cx, |this, cx| {
1504 this.history_store.update(cx, |history_store, cx| {
1505 history_store.remove_recently_opened_entry(&id, cx);
1506 });
1507 })
1508 .ok();
1509 }
1510 },
1511 );
1512 }
1513
1514 menu = menu.separator();
1515
1516 menu
1517 }
1518}
1519
1520impl Focusable for AgentPanel {
1521 fn focus_handle(&self, cx: &App) -> FocusHandle {
1522 match &self.active_view {
1523 ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
1524 ActiveView::Agent2Thread { .. } => {
1525 // todo! add own message editor to agent2
1526 cx.focus_handle()
1527 }
1528 ActiveView::History => self.history.focus_handle(cx),
1529 ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
1530 ActiveView::Configuration => {
1531 if let Some(configuration) = self.configuration.as_ref() {
1532 configuration.focus_handle(cx)
1533 } else {
1534 cx.focus_handle()
1535 }
1536 }
1537 }
1538 }
1539}
1540
1541fn agent_panel_dock_position(cx: &App) -> DockPosition {
1542 match AgentSettings::get_global(cx).dock {
1543 AgentDockPosition::Left => DockPosition::Left,
1544 AgentDockPosition::Bottom => DockPosition::Bottom,
1545 AgentDockPosition::Right => DockPosition::Right,
1546 }
1547}
1548
1549impl EventEmitter<PanelEvent> for AgentPanel {}
1550
1551impl Panel for AgentPanel {
1552 fn persistent_name() -> &'static str {
1553 "AgentPanel"
1554 }
1555
1556 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1557 agent_panel_dock_position(cx)
1558 }
1559
1560 fn position_is_valid(&self, position: DockPosition) -> bool {
1561 position != DockPosition::Bottom
1562 }
1563
1564 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1565 settings::update_settings_file::<AgentSettings>(self.fs.clone(), cx, move |settings, _| {
1566 let dock = match position {
1567 DockPosition::Left => AgentDockPosition::Left,
1568 DockPosition::Bottom => AgentDockPosition::Bottom,
1569 DockPosition::Right => AgentDockPosition::Right,
1570 };
1571 settings.set_dock(dock);
1572 });
1573 }
1574
1575 fn size(&self, window: &Window, cx: &App) -> Pixels {
1576 let settings = AgentSettings::get_global(cx);
1577 match self.position(window, cx) {
1578 DockPosition::Left | DockPosition::Right => {
1579 self.width.unwrap_or(settings.default_width)
1580 }
1581 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1582 }
1583 }
1584
1585 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1586 match self.position(window, cx) {
1587 DockPosition::Left | DockPosition::Right => self.width = size,
1588 DockPosition::Bottom => self.height = size,
1589 }
1590 self.serialize(cx);
1591 cx.notify();
1592 }
1593
1594 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1595
1596 fn remote_id() -> Option<proto::PanelId> {
1597 Some(proto::PanelId::AssistantPanel)
1598 }
1599
1600 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1601 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1602 }
1603
1604 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1605 Some("Agent Panel")
1606 }
1607
1608 fn toggle_action(&self) -> Box<dyn Action> {
1609 Box::new(ToggleFocus)
1610 }
1611
1612 fn activation_priority(&self) -> u32 {
1613 3
1614 }
1615
1616 fn enabled(&self, cx: &App) -> bool {
1617 AgentSettings::get_global(cx).enabled
1618 }
1619
1620 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1621 self.zoomed
1622 }
1623
1624 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1625 self.zoomed = zoomed;
1626 cx.notify();
1627 }
1628}
1629
1630impl AgentPanel {
1631 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1632 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1633
1634 let content = match &self.active_view {
1635 ActiveView::Thread {
1636 thread: active_thread,
1637 change_title_editor,
1638 ..
1639 } => {
1640 let state = {
1641 let active_thread = active_thread.read(cx);
1642 if active_thread.is_empty() {
1643 &ThreadSummary::Pending
1644 } else {
1645 active_thread.summary(cx)
1646 }
1647 };
1648
1649 match state {
1650 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1651 .truncate()
1652 .into_any_element(),
1653 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1654 .truncate()
1655 .into_any_element(),
1656 ThreadSummary::Ready(_) => div()
1657 .w_full()
1658 .child(change_title_editor.clone())
1659 .into_any_element(),
1660 ThreadSummary::Error => h_flex()
1661 .w_full()
1662 .child(change_title_editor.clone())
1663 .child(
1664 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1665 .on_click({
1666 let active_thread = active_thread.clone();
1667 move |_, _window, cx| {
1668 active_thread.update(cx, |thread, cx| {
1669 thread.regenerate_summary(cx);
1670 });
1671 }
1672 })
1673 .tooltip(move |_window, cx| {
1674 cx.new(|_| {
1675 Tooltip::new("Failed to generate title")
1676 .meta("Click to try again")
1677 })
1678 .into()
1679 }),
1680 )
1681 .into_any_element(),
1682 }
1683 }
1684 ActiveView::Agent2Thread { thread_element } => {
1685 Label::new(thread_element.read(cx).title(cx))
1686 .truncate()
1687 .into_any_element()
1688 }
1689 ActiveView::TextThread {
1690 title_editor,
1691 context_editor,
1692 ..
1693 } => {
1694 let summary = context_editor.read(cx).context().read(cx).summary();
1695
1696 match summary {
1697 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1698 .truncate()
1699 .into_any_element(),
1700 ContextSummary::Content(summary) => {
1701 if summary.done {
1702 div()
1703 .w_full()
1704 .child(title_editor.clone())
1705 .into_any_element()
1706 } else {
1707 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1708 .truncate()
1709 .into_any_element()
1710 }
1711 }
1712 ContextSummary::Error => h_flex()
1713 .w_full()
1714 .child(title_editor.clone())
1715 .child(
1716 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1717 .on_click({
1718 let context_editor = context_editor.clone();
1719 move |_, _window, cx| {
1720 context_editor.update(cx, |context_editor, cx| {
1721 context_editor.regenerate_summary(cx);
1722 });
1723 }
1724 })
1725 .tooltip(move |_window, cx| {
1726 cx.new(|_| {
1727 Tooltip::new("Failed to generate title")
1728 .meta("Click to try again")
1729 })
1730 .into()
1731 }),
1732 )
1733 .into_any_element(),
1734 }
1735 }
1736 ActiveView::History => Label::new("History").truncate().into_any_element(),
1737 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1738 };
1739
1740 h_flex()
1741 .key_context("TitleEditor")
1742 .id("TitleEditor")
1743 .flex_grow()
1744 .w_full()
1745 .max_w_full()
1746 .overflow_x_scroll()
1747 .child(content)
1748 .into_any()
1749 }
1750
1751 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1752 let user_store = self.user_store.read(cx);
1753 let usage = user_store.model_request_usage();
1754
1755 let account_url = zed_urls::account_url(cx);
1756
1757 let focus_handle = self.focus_handle(cx);
1758
1759 let go_back_button = div().child(
1760 IconButton::new("go-back", IconName::ArrowLeft)
1761 .icon_size(IconSize::Small)
1762 .on_click(cx.listener(|this, _, window, cx| {
1763 this.go_back(&workspace::GoBack, window, cx);
1764 }))
1765 .tooltip({
1766 let focus_handle = focus_handle.clone();
1767 move |window, cx| {
1768 Tooltip::for_action_in(
1769 "Go Back",
1770 &workspace::GoBack,
1771 &focus_handle,
1772 window,
1773 cx,
1774 )
1775 }
1776 }),
1777 );
1778
1779 let recent_entries_menu = div().child(
1780 PopoverMenu::new("agent-nav-menu")
1781 .trigger_with_tooltip(
1782 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1783 .icon_size(IconSize::Small)
1784 .style(ui::ButtonStyle::Subtle),
1785 {
1786 let focus_handle = focus_handle.clone();
1787 move |window, cx| {
1788 Tooltip::for_action_in(
1789 "Toggle Panel Menu",
1790 &ToggleNavigationMenu,
1791 &focus_handle,
1792 window,
1793 cx,
1794 )
1795 }
1796 },
1797 )
1798 .anchor(Corner::TopLeft)
1799 .with_handle(self.assistant_navigation_menu_handle.clone())
1800 .menu({
1801 let menu = self.assistant_navigation_menu.clone();
1802 move |window, cx| {
1803 if let Some(menu) = menu.as_ref() {
1804 menu.update(cx, |_, cx| {
1805 cx.defer_in(window, |menu, window, cx| {
1806 menu.rebuild(window, cx);
1807 });
1808 })
1809 }
1810 menu.clone()
1811 }
1812 }),
1813 );
1814
1815 let zoom_in_label = if self.is_zoomed(window, cx) {
1816 "Zoom Out"
1817 } else {
1818 "Zoom In"
1819 };
1820
1821 let active_thread = match &self.active_view {
1822 ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
1823 ActiveView::Agent2Thread { .. } => {
1824 // todo!
1825 None
1826 }
1827 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
1828 };
1829
1830 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1831 .trigger_with_tooltip(
1832 IconButton::new("agent-options-menu", IconName::Ellipsis)
1833 .icon_size(IconSize::Small),
1834 {
1835 let focus_handle = focus_handle.clone();
1836 move |window, cx| {
1837 Tooltip::for_action_in(
1838 "Toggle Agent Menu",
1839 &ToggleOptionsMenu,
1840 &focus_handle,
1841 window,
1842 cx,
1843 )
1844 }
1845 },
1846 )
1847 .anchor(Corner::TopRight)
1848 .with_handle(self.assistant_dropdown_menu_handle.clone())
1849 .menu(move |window, cx| {
1850 let active_thread = active_thread.clone();
1851 Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
1852 menu = menu
1853 .action("New Thread", NewThread::default().boxed_clone())
1854 .action("New Text Thread", NewTextThread.boxed_clone())
1855 .action("New Gemini Thread", NewGeminiThread.boxed_clone())
1856 .when_some(active_thread, |this, active_thread| {
1857 let thread = active_thread.read(cx);
1858 if !thread.is_empty() {
1859 this.action(
1860 "New From Summary",
1861 Box::new(NewThread {
1862 from_thread_id: Some(thread.id().clone()),
1863 }),
1864 )
1865 } else {
1866 this
1867 }
1868 })
1869 .separator();
1870
1871 menu = menu
1872 .header("MCP Servers")
1873 .action(
1874 "View Server Extensions",
1875 Box::new(zed_actions::Extensions {
1876 category_filter: Some(
1877 zed_actions::ExtensionCategoryFilter::ContextServers,
1878 ),
1879 }),
1880 )
1881 .action("Add Custom Server…", Box::new(AddContextServer))
1882 .separator();
1883
1884 if let Some(usage) = usage {
1885 menu = menu
1886 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1887 .custom_entry(
1888 move |_window, cx| {
1889 let used_percentage = match usage.limit {
1890 UsageLimit::Limited(limit) => {
1891 Some((usage.amount as f32 / limit as f32) * 100.)
1892 }
1893 UsageLimit::Unlimited => None,
1894 };
1895
1896 h_flex()
1897 .flex_1()
1898 .gap_1p5()
1899 .children(used_percentage.map(|percent| {
1900 ProgressBar::new("usage", percent, 100., cx)
1901 }))
1902 .child(
1903 Label::new(match usage.limit {
1904 UsageLimit::Limited(limit) => {
1905 format!("{} / {limit}", usage.amount)
1906 }
1907 UsageLimit::Unlimited => {
1908 format!("{} / ∞", usage.amount)
1909 }
1910 })
1911 .size(LabelSize::Small)
1912 .color(Color::Muted),
1913 )
1914 .into_any_element()
1915 },
1916 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1917 )
1918 .separator()
1919 }
1920
1921 menu = menu
1922 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1923 .action("Settings", Box::new(OpenConfiguration))
1924 .action(zoom_in_label, Box::new(ToggleZoom));
1925 menu
1926 }))
1927 });
1928
1929 h_flex()
1930 .id("assistant-toolbar")
1931 .h(Tab::container_height(cx))
1932 .max_w_full()
1933 .flex_none()
1934 .justify_between()
1935 .gap_2()
1936 .bg(cx.theme().colors().tab_bar_background)
1937 .border_b_1()
1938 .border_color(cx.theme().colors().border)
1939 .child(
1940 h_flex()
1941 .size_full()
1942 .pl_1()
1943 .gap_1()
1944 .child(match &self.active_view {
1945 ActiveView::History | ActiveView::Configuration => go_back_button,
1946 _ => recent_entries_menu,
1947 })
1948 .child(self.render_title_view(window, cx)),
1949 )
1950 .child(
1951 h_flex()
1952 .h_full()
1953 .gap_2()
1954 .children(self.render_token_count(cx))
1955 .child(
1956 h_flex()
1957 .h_full()
1958 .gap(DynamicSpacing::Base02.rems(cx))
1959 .px(DynamicSpacing::Base08.rems(cx))
1960 .border_l_1()
1961 .border_color(cx.theme().colors().border)
1962 .child(
1963 IconButton::new("new", IconName::Plus)
1964 .icon_size(IconSize::Small)
1965 .style(ButtonStyle::Subtle)
1966 .tooltip(move |window, cx| {
1967 Tooltip::for_action_in(
1968 "New Thread",
1969 &NewThread::default(),
1970 &focus_handle,
1971 window,
1972 cx,
1973 )
1974 })
1975 .on_click(move |_event, window, cx| {
1976 window.dispatch_action(
1977 NewThread::default().boxed_clone(),
1978 cx,
1979 );
1980 }),
1981 )
1982 .child(agent_extra_menu),
1983 ),
1984 )
1985 }
1986
1987 fn render_token_count(&self, cx: &App) -> Option<AnyElement> {
1988 let (active_thread, message_editor) = match &self.active_view {
1989 ActiveView::Thread {
1990 thread,
1991 message_editor,
1992 ..
1993 } => (thread.read(cx), message_editor.read(cx)),
1994 ActiveView::Agent2Thread { .. } => {
1995 // todo!
1996 return None;
1997 }
1998 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
1999 return None;
2000 }
2001 };
2002
2003 let editor_empty = message_editor.is_editor_fully_empty(cx);
2004
2005 if active_thread.is_empty() && editor_empty {
2006 return None;
2007 }
2008
2009 let thread = active_thread.thread().read(cx);
2010 let is_generating = thread.is_generating();
2011 let conversation_token_usage = thread.total_token_usage()?;
2012
2013 let (total_token_usage, is_estimating) =
2014 if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
2015 let combined = thread
2016 .token_usage_up_to_message(editing_message_id)
2017 .add(unsent_tokens);
2018
2019 (combined, unsent_tokens > 0)
2020 } else {
2021 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
2022 let combined = conversation_token_usage.add(unsent_tokens);
2023
2024 (combined, unsent_tokens > 0)
2025 };
2026
2027 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
2028
2029 match &self.active_view {
2030 ActiveView::Thread { .. } => {
2031 if total_token_usage.total == 0 {
2032 return None;
2033 }
2034
2035 let token_color = match total_token_usage.ratio() {
2036 TokenUsageRatio::Normal if is_estimating => Color::Default,
2037 TokenUsageRatio::Normal => Color::Muted,
2038 TokenUsageRatio::Warning => Color::Warning,
2039 TokenUsageRatio::Exceeded => Color::Error,
2040 };
2041
2042 let token_count = h_flex()
2043 .id("token-count")
2044 .flex_shrink_0()
2045 .gap_0p5()
2046 .when(!is_generating && is_estimating, |parent| {
2047 parent
2048 .child(
2049 h_flex()
2050 .mr_1()
2051 .size_2p5()
2052 .justify_center()
2053 .rounded_full()
2054 .bg(cx.theme().colors().text.opacity(0.1))
2055 .child(
2056 div().size_1().rounded_full().bg(cx.theme().colors().text),
2057 ),
2058 )
2059 .tooltip(move |window, cx| {
2060 Tooltip::with_meta(
2061 "Estimated New Token Count",
2062 None,
2063 format!(
2064 "Current Conversation Tokens: {}",
2065 humanize_token_count(conversation_token_usage.total)
2066 ),
2067 window,
2068 cx,
2069 )
2070 })
2071 })
2072 .child(
2073 Label::new(humanize_token_count(total_token_usage.total))
2074 .size(LabelSize::Small)
2075 .color(token_color)
2076 .map(|label| {
2077 if is_generating || is_waiting_to_update_token_count {
2078 label
2079 .with_animation(
2080 "used-tokens-label",
2081 Animation::new(Duration::from_secs(2))
2082 .repeat()
2083 .with_easing(pulsating_between(0.6, 1.)),
2084 |label, delta| label.alpha(delta),
2085 )
2086 .into_any()
2087 } else {
2088 label.into_any_element()
2089 }
2090 }),
2091 )
2092 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
2093 .child(
2094 Label::new(humanize_token_count(total_token_usage.max))
2095 .size(LabelSize::Small)
2096 .color(Color::Muted),
2097 )
2098 .into_any();
2099
2100 Some(token_count)
2101 }
2102 ActiveView::TextThread { context_editor, .. } => {
2103 let element = render_remaining_tokens(context_editor, cx)?;
2104
2105 Some(element.into_any_element())
2106 }
2107 _ => None,
2108 }
2109 }
2110
2111 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2112 if TrialEndUpsell::dismissed() {
2113 return false;
2114 }
2115
2116 let plan = self.user_store.read(cx).current_plan();
2117 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2118
2119 matches!(plan, Some(Plan::Free)) && has_previous_trial
2120 }
2121
2122 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
2123 match &self.active_view {
2124 ActiveView::Thread { thread, .. } => {
2125 let is_using_zed_provider = thread
2126 .read(cx)
2127 .thread()
2128 .read(cx)
2129 .configured_model()
2130 .map_or(false, |model| {
2131 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
2132 });
2133
2134 if !is_using_zed_provider {
2135 return false;
2136 }
2137 }
2138 ActiveView::Agent2Thread { .. } => {
2139 // todo!
2140 return false;
2141 }
2142 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2143 return false;
2144 }
2145 };
2146
2147 if self.hide_upsell || Upsell::dismissed() {
2148 return false;
2149 }
2150
2151 let plan = self.user_store.read(cx).current_plan();
2152 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
2153 return false;
2154 }
2155
2156 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2157 if has_previous_trial {
2158 return false;
2159 }
2160
2161 true
2162 }
2163
2164 fn render_upsell(
2165 &self,
2166 _window: &mut Window,
2167 cx: &mut Context<Self>,
2168 ) -> Option<impl IntoElement> {
2169 if !self.should_render_upsell(cx) {
2170 return None;
2171 }
2172
2173 if self.user_store.read(cx).account_too_young() {
2174 Some(self.render_young_account_upsell(cx).into_any_element())
2175 } else {
2176 Some(self.render_trial_upsell(cx).into_any_element())
2177 }
2178 }
2179
2180 fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2181 let checkbox = CheckboxWithLabel::new(
2182 "dont-show-again",
2183 Label::new("Don't show again").color(Color::Muted),
2184 ToggleState::Unselected,
2185 move |toggle_state, _window, cx| {
2186 let toggle_state_bool = toggle_state.selected();
2187
2188 Upsell::set_dismissed(toggle_state_bool, cx);
2189 },
2190 );
2191
2192 let contents = div()
2193 .size_full()
2194 .gap_2()
2195 .flex()
2196 .flex_col()
2197 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2198 .child(
2199 Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
2200 .size(LabelSize::Small),
2201 )
2202 .child(
2203 Label::new(
2204 "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
2205 )
2206 .color(Color::Muted),
2207 )
2208 .child(
2209 h_flex()
2210 .w_full()
2211 .px_neg_1()
2212 .justify_between()
2213 .items_center()
2214 .child(h_flex().items_center().gap_1().child(checkbox))
2215 .child(
2216 h_flex()
2217 .gap_2()
2218 .child(
2219 Button::new("dismiss-button", "Not Now")
2220 .style(ButtonStyle::Transparent)
2221 .color(Color::Muted)
2222 .on_click({
2223 let agent_panel = cx.entity();
2224 move |_, _, cx| {
2225 agent_panel.update(cx, |this, cx| {
2226 this.hide_upsell = true;
2227 cx.notify();
2228 });
2229 }
2230 }),
2231 )
2232 .child(
2233 Button::new("cta-button", "Upgrade to Zed Pro")
2234 .style(ButtonStyle::Transparent)
2235 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2236 ),
2237 ),
2238 );
2239
2240 self.render_upsell_container(cx, contents)
2241 }
2242
2243 fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2244 let checkbox = CheckboxWithLabel::new(
2245 "dont-show-again",
2246 Label::new("Don't show again").color(Color::Muted),
2247 ToggleState::Unselected,
2248 move |toggle_state, _window, cx| {
2249 let toggle_state_bool = toggle_state.selected();
2250
2251 Upsell::set_dismissed(toggle_state_bool, cx);
2252 },
2253 );
2254
2255 let contents = div()
2256 .size_full()
2257 .gap_2()
2258 .flex()
2259 .flex_col()
2260 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2261 .child(
2262 Label::new("Try Zed Pro for free for 14 days - no credit card required.")
2263 .size(LabelSize::Small),
2264 )
2265 .child(
2266 Label::new(
2267 "Use your own API keys or enable usage-based billing once you hit the cap.",
2268 )
2269 .color(Color::Muted),
2270 )
2271 .child(
2272 h_flex()
2273 .w_full()
2274 .px_neg_1()
2275 .justify_between()
2276 .items_center()
2277 .child(h_flex().items_center().gap_1().child(checkbox))
2278 .child(
2279 h_flex()
2280 .gap_2()
2281 .child(
2282 Button::new("dismiss-button", "Not Now")
2283 .style(ButtonStyle::Transparent)
2284 .color(Color::Muted)
2285 .on_click({
2286 let agent_panel = cx.entity();
2287 move |_, _, cx| {
2288 agent_panel.update(cx, |this, cx| {
2289 this.hide_upsell = true;
2290 cx.notify();
2291 });
2292 }
2293 }),
2294 )
2295 .child(
2296 Button::new("cta-button", "Start Trial")
2297 .style(ButtonStyle::Transparent)
2298 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2299 ),
2300 ),
2301 );
2302
2303 self.render_upsell_container(cx, contents)
2304 }
2305
2306 fn render_trial_end_upsell(
2307 &self,
2308 _window: &mut Window,
2309 cx: &mut Context<Self>,
2310 ) -> Option<impl IntoElement> {
2311 if !self.should_render_trial_end_upsell(cx) {
2312 return None;
2313 }
2314
2315 Some(
2316 self.render_upsell_container(
2317 cx,
2318 div()
2319 .size_full()
2320 .gap_2()
2321 .flex()
2322 .flex_col()
2323 .child(
2324 Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
2325 )
2326 .child(
2327 Label::new("You've been automatically reset to the free plan.")
2328 .size(LabelSize::Small),
2329 )
2330 .child(
2331 h_flex()
2332 .w_full()
2333 .px_neg_1()
2334 .justify_between()
2335 .items_center()
2336 .child(div())
2337 .child(
2338 h_flex()
2339 .gap_2()
2340 .child(
2341 Button::new("dismiss-button", "Stay on Free")
2342 .style(ButtonStyle::Transparent)
2343 .color(Color::Muted)
2344 .on_click({
2345 let agent_panel = cx.entity();
2346 move |_, _, cx| {
2347 agent_panel.update(cx, |_this, cx| {
2348 TrialEndUpsell::set_dismissed(true, cx);
2349 cx.notify();
2350 });
2351 }
2352 }),
2353 )
2354 .child(
2355 Button::new("cta-button", "Upgrade to Zed Pro")
2356 .style(ButtonStyle::Transparent)
2357 .on_click(|_, _, cx| {
2358 cx.open_url(&zed_urls::account_url(cx))
2359 }),
2360 ),
2361 ),
2362 ),
2363 ),
2364 )
2365 }
2366
2367 fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
2368 div().p_2().child(
2369 v_flex()
2370 .w_full()
2371 .elevation_2(cx)
2372 .rounded(px(8.))
2373 .bg(cx.theme().colors().background.alpha(0.5))
2374 .p(px(3.))
2375 .child(
2376 div()
2377 .gap_2()
2378 .flex()
2379 .flex_col()
2380 .size_full()
2381 .border_1()
2382 .rounded(px(5.))
2383 .border_color(cx.theme().colors().text.alpha(0.1))
2384 .overflow_hidden()
2385 .relative()
2386 .bg(cx.theme().colors().panel_background)
2387 .px_4()
2388 .py_3()
2389 .child(
2390 div()
2391 .absolute()
2392 .top_0()
2393 .right(px(-1.0))
2394 .w(px(441.))
2395 .h(px(167.))
2396 .child(
2397 Vector::new(
2398 VectorName::Grid,
2399 rems_from_px(441.),
2400 rems_from_px(167.),
2401 )
2402 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
2403 ),
2404 )
2405 .child(
2406 div()
2407 .absolute()
2408 .top(px(-8.0))
2409 .right_0()
2410 .w(px(400.))
2411 .h(px(92.))
2412 .child(
2413 Vector::new(
2414 VectorName::AiGrid,
2415 rems_from_px(400.),
2416 rems_from_px(92.),
2417 )
2418 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
2419 ),
2420 )
2421 // .child(
2422 // div()
2423 // .absolute()
2424 // .top_0()
2425 // .right(px(360.))
2426 // .size(px(401.))
2427 // .overflow_hidden()
2428 // .bg(cx.theme().colors().panel_background)
2429 // )
2430 .child(
2431 div()
2432 .absolute()
2433 .top_0()
2434 .right_0()
2435 .w(px(660.))
2436 .h(px(401.))
2437 .overflow_hidden()
2438 .bg(linear_gradient(
2439 75.,
2440 linear_color_stop(
2441 cx.theme().colors().panel_background.alpha(0.01),
2442 1.0,
2443 ),
2444 linear_color_stop(cx.theme().colors().panel_background, 0.45),
2445 )),
2446 )
2447 .child(content),
2448 ),
2449 )
2450 }
2451
2452 fn render_thread_empty_state(
2453 &self,
2454 window: &mut Window,
2455 cx: &mut Context<Self>,
2456 ) -> impl IntoElement {
2457 let recent_history = self
2458 .history_store
2459 .update(cx, |this, cx| this.recent_entries(6, cx));
2460
2461 let model_registry = LanguageModelRegistry::read_global(cx);
2462 let configuration_error =
2463 model_registry.configuration_error(model_registry.default_model(), cx);
2464 let no_error = configuration_error.is_none();
2465 let focus_handle = self.focus_handle(cx);
2466
2467 v_flex()
2468 .size_full()
2469 .bg(cx.theme().colors().panel_background)
2470 .when(recent_history.is_empty(), |this| {
2471 let configuration_error_ref = &configuration_error;
2472 this.child(
2473 v_flex()
2474 .size_full()
2475 .max_w_80()
2476 .mx_auto()
2477 .justify_center()
2478 .items_center()
2479 .gap_1()
2480 .child(h_flex().child(Headline::new("Welcome to the Agent Panel")))
2481 .when(no_error, |parent| {
2482 parent
2483 .child(
2484 h_flex().child(
2485 Label::new("Ask and build anything.")
2486 .color(Color::Muted)
2487 .mb_2p5(),
2488 ),
2489 )
2490 .child(
2491 Button::new("new-thread", "Start New Thread")
2492 .icon(IconName::Plus)
2493 .icon_position(IconPosition::Start)
2494 .icon_size(IconSize::Small)
2495 .icon_color(Color::Muted)
2496 .full_width()
2497 .key_binding(KeyBinding::for_action_in(
2498 &NewThread::default(),
2499 &focus_handle,
2500 window,
2501 cx,
2502 ))
2503 .on_click(|_event, window, cx| {
2504 window.dispatch_action(
2505 NewThread::default().boxed_clone(),
2506 cx,
2507 )
2508 }),
2509 )
2510 .child(
2511 Button::new("context", "Add Context")
2512 .icon(IconName::FileCode)
2513 .icon_position(IconPosition::Start)
2514 .icon_size(IconSize::Small)
2515 .icon_color(Color::Muted)
2516 .full_width()
2517 .key_binding(KeyBinding::for_action_in(
2518 &ToggleContextPicker,
2519 &focus_handle,
2520 window,
2521 cx,
2522 ))
2523 .on_click(|_event, window, cx| {
2524 window.dispatch_action(
2525 ToggleContextPicker.boxed_clone(),
2526 cx,
2527 )
2528 }),
2529 )
2530 .child(
2531 Button::new("mode", "Switch Model")
2532 .icon(IconName::DatabaseZap)
2533 .icon_position(IconPosition::Start)
2534 .icon_size(IconSize::Small)
2535 .icon_color(Color::Muted)
2536 .full_width()
2537 .key_binding(KeyBinding::for_action_in(
2538 &ToggleModelSelector,
2539 &focus_handle,
2540 window,
2541 cx,
2542 ))
2543 .on_click(|_event, window, cx| {
2544 window.dispatch_action(
2545 ToggleModelSelector.boxed_clone(),
2546 cx,
2547 )
2548 }),
2549 )
2550 .child(
2551 Button::new("settings", "View Settings")
2552 .icon(IconName::Settings)
2553 .icon_position(IconPosition::Start)
2554 .icon_size(IconSize::Small)
2555 .icon_color(Color::Muted)
2556 .full_width()
2557 .key_binding(KeyBinding::for_action_in(
2558 &OpenConfiguration,
2559 &focus_handle,
2560 window,
2561 cx,
2562 ))
2563 .on_click(|_event, window, cx| {
2564 window.dispatch_action(
2565 OpenConfiguration.boxed_clone(),
2566 cx,
2567 )
2568 }),
2569 )
2570 })
2571 .map(|parent| match configuration_error_ref {
2572 Some(
2573 err @ (ConfigurationError::ModelNotFound
2574 | ConfigurationError::ProviderNotAuthenticated(_)
2575 | ConfigurationError::NoProvider),
2576 ) => parent
2577 .child(h_flex().child(
2578 Label::new(err.to_string()).color(Color::Muted).mb_2p5(),
2579 ))
2580 .child(
2581 Button::new("settings", "Configure a Provider")
2582 .icon(IconName::Settings)
2583 .icon_position(IconPosition::Start)
2584 .icon_size(IconSize::Small)
2585 .icon_color(Color::Muted)
2586 .full_width()
2587 .key_binding(KeyBinding::for_action_in(
2588 &OpenConfiguration,
2589 &focus_handle,
2590 window,
2591 cx,
2592 ))
2593 .on_click(|_event, window, cx| {
2594 window.dispatch_action(
2595 OpenConfiguration.boxed_clone(),
2596 cx,
2597 )
2598 }),
2599 ),
2600 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2601 parent.children(provider.render_accept_terms(
2602 LanguageModelProviderTosView::ThreadFreshStart,
2603 cx,
2604 ))
2605 }
2606 None => parent,
2607 }),
2608 )
2609 })
2610 .when(!recent_history.is_empty(), |parent| {
2611 let focus_handle = focus_handle.clone();
2612 let configuration_error_ref = &configuration_error;
2613
2614 parent
2615 .overflow_hidden()
2616 .p_1p5()
2617 .justify_end()
2618 .gap_1()
2619 .child(
2620 h_flex()
2621 .pl_1p5()
2622 .pb_1()
2623 .w_full()
2624 .justify_between()
2625 .border_b_1()
2626 .border_color(cx.theme().colors().border_variant)
2627 .child(
2628 Label::new("Recent")
2629 .size(LabelSize::Small)
2630 .color(Color::Muted),
2631 )
2632 .child(
2633 Button::new("view-history", "View All")
2634 .style(ButtonStyle::Subtle)
2635 .label_size(LabelSize::Small)
2636 .key_binding(
2637 KeyBinding::for_action_in(
2638 &OpenHistory,
2639 &self.focus_handle(cx),
2640 window,
2641 cx,
2642 )
2643 .map(|kb| kb.size(rems_from_px(12.))),
2644 )
2645 .on_click(move |_event, window, cx| {
2646 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2647 }),
2648 ),
2649 )
2650 .child(
2651 v_flex()
2652 .gap_1()
2653 .children(recent_history.into_iter().enumerate().map(
2654 |(index, entry)| {
2655 // TODO: Add keyboard navigation.
2656 let is_hovered =
2657 self.hovered_recent_history_item == Some(index);
2658 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2659 .hovered(is_hovered)
2660 .on_hover(cx.listener(
2661 move |this, is_hovered, _window, cx| {
2662 if *is_hovered {
2663 this.hovered_recent_history_item = Some(index);
2664 } else if this.hovered_recent_history_item
2665 == Some(index)
2666 {
2667 this.hovered_recent_history_item = None;
2668 }
2669 cx.notify();
2670 },
2671 ))
2672 .into_any_element()
2673 },
2674 )),
2675 )
2676 .map(|parent| match configuration_error_ref {
2677 Some(
2678 err @ (ConfigurationError::ModelNotFound
2679 | ConfigurationError::ProviderNotAuthenticated(_)
2680 | ConfigurationError::NoProvider),
2681 ) => parent.child(
2682 Banner::new()
2683 .severity(ui::Severity::Warning)
2684 .child(Label::new(err.to_string()).size(LabelSize::Small))
2685 .action_slot(
2686 Button::new("settings", "Configure Provider")
2687 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2688 .label_size(LabelSize::Small)
2689 .key_binding(
2690 KeyBinding::for_action_in(
2691 &OpenConfiguration,
2692 &focus_handle,
2693 window,
2694 cx,
2695 )
2696 .map(|kb| kb.size(rems_from_px(12.))),
2697 )
2698 .on_click(|_event, window, cx| {
2699 window.dispatch_action(
2700 OpenConfiguration.boxed_clone(),
2701 cx,
2702 )
2703 }),
2704 ),
2705 ),
2706 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2707 parent.child(Banner::new().severity(ui::Severity::Warning).child(
2708 h_flex().w_full().children(provider.render_accept_terms(
2709 LanguageModelProviderTosView::ThreadtEmptyState,
2710 cx,
2711 )),
2712 ))
2713 }
2714 None => parent,
2715 })
2716 })
2717 }
2718
2719 fn render_tool_use_limit_reached(
2720 &self,
2721 window: &mut Window,
2722 cx: &mut Context<Self>,
2723 ) -> Option<AnyElement> {
2724 let active_thread = match &self.active_view {
2725 ActiveView::Thread { thread, .. } => thread,
2726 ActiveView::Agent2Thread { .. } => {
2727 // todo!
2728 return None;
2729 }
2730 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
2731 return None;
2732 }
2733 };
2734
2735 let thread = active_thread.read(cx).thread().read(cx);
2736
2737 let tool_use_limit_reached = thread.tool_use_limit_reached();
2738 if !tool_use_limit_reached {
2739 return None;
2740 }
2741
2742 let model = thread.configured_model()?.model;
2743
2744 let focus_handle = self.focus_handle(cx);
2745
2746 let banner = Banner::new()
2747 .severity(ui::Severity::Info)
2748 .child(Label::new("Consecutive tool use limit reached.").size(LabelSize::Small))
2749 .action_slot(
2750 h_flex()
2751 .gap_1()
2752 .child(
2753 Button::new("continue-conversation", "Continue")
2754 .layer(ElevationIndex::ModalSurface)
2755 .label_size(LabelSize::Small)
2756 .key_binding(
2757 KeyBinding::for_action_in(
2758 &ContinueThread,
2759 &focus_handle,
2760 window,
2761 cx,
2762 )
2763 .map(|kb| kb.size(rems_from_px(10.))),
2764 )
2765 .on_click(cx.listener(|this, _, window, cx| {
2766 this.continue_conversation(window, cx);
2767 })),
2768 )
2769 .when(model.supports_max_mode(), |this| {
2770 this.child(
2771 Button::new("continue-burn-mode", "Continue with Burn Mode")
2772 .style(ButtonStyle::Filled)
2773 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
2774 .layer(ElevationIndex::ModalSurface)
2775 .label_size(LabelSize::Small)
2776 .key_binding(
2777 KeyBinding::for_action_in(
2778 &ContinueWithBurnMode,
2779 &focus_handle,
2780 window,
2781 cx,
2782 )
2783 .map(|kb| kb.size(rems_from_px(10.))),
2784 )
2785 .tooltip(Tooltip::text("Enable Burn Mode for unlimited tool use."))
2786 .on_click({
2787 let active_thread = active_thread.clone();
2788 cx.listener(move |this, _, window, cx| {
2789 active_thread.update(cx, |active_thread, cx| {
2790 active_thread.thread().update(cx, |thread, _cx| {
2791 thread.set_completion_mode(CompletionMode::Burn);
2792 });
2793 });
2794 this.continue_conversation(window, cx);
2795 })
2796 }),
2797 )
2798 }),
2799 );
2800
2801 Some(div().px_2().pb_2().child(banner).into_any_element())
2802 }
2803
2804 fn render_payment_required_error(
2805 &self,
2806 thread: &Entity<ActiveThread>,
2807 cx: &mut Context<Self>,
2808 ) -> AnyElement {
2809 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.";
2810
2811 v_flex()
2812 .gap_0p5()
2813 .child(
2814 h_flex()
2815 .gap_1p5()
2816 .items_center()
2817 .child(Icon::new(IconName::XCircle).color(Color::Error))
2818 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2819 )
2820 .child(
2821 div()
2822 .id("error-message")
2823 .max_h_24()
2824 .overflow_y_scroll()
2825 .child(Label::new(ERROR_MESSAGE)),
2826 )
2827 .child(
2828 h_flex()
2829 .justify_end()
2830 .mt_1()
2831 .gap_1()
2832 .child(self.create_copy_button(ERROR_MESSAGE))
2833 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
2834 let thread = thread.clone();
2835 move |_, _, _, cx| {
2836 thread.update(cx, |this, _cx| {
2837 this.clear_last_error();
2838 });
2839
2840 cx.open_url(&zed_urls::account_url(cx));
2841 cx.notify();
2842 }
2843 })))
2844 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
2845 let thread = thread.clone();
2846 move |_, _, _, cx| {
2847 thread.update(cx, |this, _cx| {
2848 this.clear_last_error();
2849 });
2850
2851 cx.notify();
2852 }
2853 }))),
2854 )
2855 .into_any()
2856 }
2857
2858 fn render_model_request_limit_reached_error(
2859 &self,
2860 plan: Plan,
2861 thread: &Entity<ActiveThread>,
2862 cx: &mut Context<Self>,
2863 ) -> AnyElement {
2864 let error_message = match plan {
2865 Plan::ZedPro => {
2866 "Model request limit reached. Upgrade to usage-based billing for more requests."
2867 }
2868 Plan::ZedProTrial => {
2869 "Model request limit reached. Upgrade to Zed Pro for more requests."
2870 }
2871 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2872 };
2873 let call_to_action = match plan {
2874 Plan::ZedPro => "Upgrade to usage-based billing",
2875 Plan::ZedProTrial => "Upgrade to Zed Pro",
2876 Plan::Free => "Upgrade to Zed Pro",
2877 };
2878
2879 v_flex()
2880 .gap_0p5()
2881 .child(
2882 h_flex()
2883 .gap_1p5()
2884 .items_center()
2885 .child(Icon::new(IconName::XCircle).color(Color::Error))
2886 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2887 )
2888 .child(
2889 div()
2890 .id("error-message")
2891 .max_h_24()
2892 .overflow_y_scroll()
2893 .child(Label::new(error_message)),
2894 )
2895 .child(
2896 h_flex()
2897 .justify_end()
2898 .mt_1()
2899 .gap_1()
2900 .child(self.create_copy_button(error_message))
2901 .child(
2902 Button::new("subscribe", call_to_action).on_click(cx.listener({
2903 let thread = thread.clone();
2904 move |_, _, _, cx| {
2905 thread.update(cx, |this, _cx| {
2906 this.clear_last_error();
2907 });
2908
2909 cx.open_url(&zed_urls::account_url(cx));
2910 cx.notify();
2911 }
2912 })),
2913 )
2914 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
2915 let thread = thread.clone();
2916 move |_, _, _, cx| {
2917 thread.update(cx, |this, _cx| {
2918 this.clear_last_error();
2919 });
2920
2921 cx.notify();
2922 }
2923 }))),
2924 )
2925 .into_any()
2926 }
2927
2928 fn render_error_message(
2929 &self,
2930 header: SharedString,
2931 message: SharedString,
2932 thread: &Entity<ActiveThread>,
2933 cx: &mut Context<Self>,
2934 ) -> AnyElement {
2935 let message_with_header = format!("{}\n{}", header, message);
2936 v_flex()
2937 .gap_0p5()
2938 .child(
2939 h_flex()
2940 .gap_1p5()
2941 .items_center()
2942 .child(Icon::new(IconName::XCircle).color(Color::Error))
2943 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2944 )
2945 .child(
2946 div()
2947 .id("error-message")
2948 .max_h_32()
2949 .overflow_y_scroll()
2950 .child(Label::new(message.clone())),
2951 )
2952 .child(
2953 h_flex()
2954 .justify_end()
2955 .mt_1()
2956 .gap_1()
2957 .child(self.create_copy_button(message_with_header))
2958 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
2959 let thread = thread.clone();
2960 move |_, _, _, cx| {
2961 thread.update(cx, |this, _cx| {
2962 this.clear_last_error();
2963 });
2964
2965 cx.notify();
2966 }
2967 }))),
2968 )
2969 .into_any()
2970 }
2971
2972 fn render_prompt_editor(
2973 &self,
2974 context_editor: &Entity<TextThreadEditor>,
2975 buffer_search_bar: &Entity<BufferSearchBar>,
2976 window: &mut Window,
2977 cx: &mut Context<Self>,
2978 ) -> Div {
2979 let mut registrar = buffer_search::DivRegistrar::new(
2980 |this, _, _cx| match &this.active_view {
2981 ActiveView::TextThread {
2982 buffer_search_bar, ..
2983 } => Some(buffer_search_bar.clone()),
2984 _ => None,
2985 },
2986 cx,
2987 );
2988 BufferSearchBar::register(&mut registrar);
2989 registrar
2990 .into_div()
2991 .size_full()
2992 .relative()
2993 .map(|parent| {
2994 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2995 if buffer_search_bar.is_dismissed() {
2996 return parent;
2997 }
2998 parent.child(
2999 div()
3000 .p(DynamicSpacing::Base08.rems(cx))
3001 .border_b_1()
3002 .border_color(cx.theme().colors().border_variant)
3003 .bg(cx.theme().colors().editor_background)
3004 .child(buffer_search_bar.render(window, cx)),
3005 )
3006 })
3007 })
3008 .child(context_editor.clone())
3009 .child(self.render_drag_target(cx))
3010 }
3011
3012 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3013 let is_local = self.project.read(cx).is_local();
3014 div()
3015 .invisible()
3016 .absolute()
3017 .top_0()
3018 .right_0()
3019 .bottom_0()
3020 .left_0()
3021 .bg(cx.theme().colors().drop_target_background)
3022 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3023 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3024 .when(is_local, |this| {
3025 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3026 })
3027 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3028 let item = tab.pane.read(cx).item_for_index(tab.ix);
3029 let project_paths = item
3030 .and_then(|item| item.project_path(cx))
3031 .into_iter()
3032 .collect::<Vec<_>>();
3033 this.handle_drop(project_paths, vec![], window, cx);
3034 }))
3035 .on_drop(
3036 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3037 let project_paths = selection
3038 .items()
3039 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3040 .collect::<Vec<_>>();
3041 this.handle_drop(project_paths, vec![], window, cx);
3042 }),
3043 )
3044 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3045 let tasks = paths
3046 .paths()
3047 .into_iter()
3048 .map(|path| {
3049 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
3050 })
3051 .collect::<Vec<_>>();
3052 cx.spawn_in(window, async move |this, cx| {
3053 let mut paths = vec![];
3054 let mut added_worktrees = vec![];
3055 let opened_paths = futures::future::join_all(tasks).await;
3056 for entry in opened_paths {
3057 if let Some((worktree, project_path)) = entry.log_err() {
3058 added_worktrees.push(worktree);
3059 paths.push(project_path);
3060 }
3061 }
3062 this.update_in(cx, |this, window, cx| {
3063 this.handle_drop(paths, added_worktrees, window, cx);
3064 })
3065 .ok();
3066 })
3067 .detach();
3068 }))
3069 }
3070
3071 fn handle_drop(
3072 &mut self,
3073 paths: Vec<ProjectPath>,
3074 added_worktrees: Vec<Entity<Worktree>>,
3075 window: &mut Window,
3076 cx: &mut Context<Self>,
3077 ) {
3078 match &self.active_view {
3079 ActiveView::Thread { thread, .. } => {
3080 let context_store = thread.read(cx).context_store().clone();
3081 context_store.update(cx, move |context_store, cx| {
3082 let mut tasks = Vec::new();
3083 for project_path in &paths {
3084 tasks.push(context_store.add_file_from_path(
3085 project_path.clone(),
3086 false,
3087 cx,
3088 ));
3089 }
3090 cx.background_spawn(async move {
3091 futures::future::join_all(tasks).await;
3092 // Need to hold onto the worktrees until they have already been used when
3093 // opening the buffers.
3094 drop(added_worktrees);
3095 })
3096 .detach();
3097 });
3098 }
3099 ActiveView::Agent2Thread { .. } => {
3100 unimplemented!()
3101 }
3102 ActiveView::TextThread { context_editor, .. } => {
3103 context_editor.update(cx, |context_editor, cx| {
3104 TextThreadEditor::insert_dragged_files(
3105 context_editor,
3106 paths,
3107 added_worktrees,
3108 window,
3109 cx,
3110 );
3111 });
3112 }
3113 ActiveView::History | ActiveView::Configuration => {}
3114 }
3115 }
3116
3117 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
3118 let message = message.into();
3119 IconButton::new("copy", IconName::Copy)
3120 .on_click(move |_, _, cx| {
3121 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
3122 })
3123 .tooltip(Tooltip::text("Copy Error Message"))
3124 }
3125
3126 fn key_context(&self) -> KeyContext {
3127 let mut key_context = KeyContext::new_with_defaults();
3128 key_context.add("AgentPanel");
3129 if matches!(self.active_view, ActiveView::TextThread { .. }) {
3130 key_context.add("prompt_editor");
3131 }
3132 key_context
3133 }
3134}
3135
3136impl Render for AgentPanel {
3137 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3138 // WARNING: Changes to this element hierarchy can have
3139 // non-obvious implications to the layout of children.
3140 //
3141 // If you need to change it, please confirm:
3142 // - The message editor expands (cmd-option-esc) correctly
3143 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3144 // - Font size works as expected and can be changed with cmd-+/cmd-
3145 // - Scrolling in all views works as expected
3146 // - Files can be dropped into the panel
3147 let content = v_flex()
3148 .key_context(self.key_context())
3149 .justify_between()
3150 .size_full()
3151 .on_action(cx.listener(Self::cancel))
3152 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3153 this.new_thread(action, window, cx);
3154 }))
3155 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3156 this.open_history(window, cx);
3157 }))
3158 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
3159 this.open_configuration(window, cx);
3160 }))
3161 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3162 .on_action(cx.listener(Self::deploy_rules_library))
3163 .on_action(cx.listener(Self::open_agent_diff))
3164 .on_action(cx.listener(Self::go_back))
3165 .on_action(cx.listener(Self::toggle_navigation_menu))
3166 .on_action(cx.listener(Self::toggle_options_menu))
3167 .on_action(cx.listener(Self::increase_font_size))
3168 .on_action(cx.listener(Self::decrease_font_size))
3169 .on_action(cx.listener(Self::reset_font_size))
3170 .on_action(cx.listener(Self::toggle_zoom))
3171 .on_action(cx.listener(|this, _: &ContinueThread, window, cx| {
3172 this.continue_conversation(window, cx);
3173 }))
3174 .on_action(cx.listener(|this, _: &ContinueWithBurnMode, window, cx| {
3175 match &this.active_view {
3176 ActiveView::Thread { thread, .. } => {
3177 thread.update(cx, |active_thread, cx| {
3178 active_thread.thread().update(cx, |thread, _cx| {
3179 thread.set_completion_mode(CompletionMode::Burn);
3180 });
3181 });
3182 this.continue_conversation(window, cx);
3183 }
3184 ActiveView::Agent2Thread { .. } => {
3185 todo!()
3186 }
3187 ActiveView::TextThread { .. }
3188 | ActiveView::History
3189 | ActiveView::Configuration => {}
3190 }
3191 }))
3192 .on_action(cx.listener(Self::toggle_burn_mode))
3193 .child(self.render_toolbar(window, cx))
3194 .children(self.render_upsell(window, cx))
3195 .children(self.render_trial_end_upsell(window, cx))
3196 .map(|parent| match &self.active_view {
3197 ActiveView::Thread {
3198 thread,
3199 message_editor,
3200 ..
3201 } => parent
3202 .relative()
3203 .child(if thread.read(cx).is_empty() {
3204 self.render_thread_empty_state(window, cx)
3205 .into_any_element()
3206 } else {
3207 thread.clone().into_any_element()
3208 })
3209 .children(self.render_tool_use_limit_reached(window, cx))
3210 .child(h_flex().child(message_editor.clone()))
3211 .when_some(thread.read(cx).last_error(), |this, last_error| {
3212 this.child(
3213 div()
3214 .absolute()
3215 .right_3()
3216 .bottom_12()
3217 .max_w_96()
3218 .py_2()
3219 .px_3()
3220 .elevation_2(cx)
3221 .occlude()
3222 .child(match last_error {
3223 ThreadError::PaymentRequired => {
3224 self.render_payment_required_error(thread, cx)
3225 }
3226 ThreadError::ModelRequestLimitReached { plan } => self
3227 .render_model_request_limit_reached_error(plan, thread, cx),
3228 ThreadError::Message { header, message } => {
3229 self.render_error_message(header, message, thread, cx)
3230 }
3231 })
3232 .into_any(),
3233 )
3234 })
3235 .child(self.render_drag_target(cx)),
3236 ActiveView::Agent2Thread { thread_element, .. } => parent
3237 .relative()
3238 .child(thread_element.clone())
3239 // todo!
3240 // .child(h_flex().child(self.message_editor.clone()))
3241 .child(self.render_drag_target(cx)),
3242 ActiveView::History => parent.child(self.history.clone()),
3243 ActiveView::TextThread {
3244 context_editor,
3245 buffer_search_bar,
3246 ..
3247 } => parent.child(self.render_prompt_editor(
3248 context_editor,
3249 buffer_search_bar,
3250 window,
3251 cx,
3252 )),
3253 ActiveView::Configuration => parent.children(self.configuration.clone()),
3254 });
3255
3256 match self.active_view.which_font_size_used() {
3257 WhichFontSize::AgentFont => {
3258 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3259 .size_full()
3260 .child(content)
3261 .into_any()
3262 }
3263 _ => content.into_any(),
3264 }
3265 }
3266}
3267
3268struct PromptLibraryInlineAssist {
3269 workspace: WeakEntity<Workspace>,
3270}
3271
3272impl PromptLibraryInlineAssist {
3273 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3274 Self { workspace }
3275 }
3276}
3277
3278impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3279 fn assist(
3280 &self,
3281 prompt_editor: &Entity<Editor>,
3282 initial_prompt: Option<String>,
3283 window: &mut Window,
3284 cx: &mut Context<RulesLibrary>,
3285 ) {
3286 InlineAssistant::update_global(cx, |assistant, cx| {
3287 let Some(project) = self
3288 .workspace
3289 .upgrade()
3290 .map(|workspace| workspace.read(cx).project().downgrade())
3291 else {
3292 return;
3293 };
3294 let prompt_store = None;
3295 let thread_store = None;
3296 let text_thread_store = None;
3297 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3298 assistant.assist(
3299 &prompt_editor,
3300 self.workspace.clone(),
3301 context_store,
3302 project,
3303 prompt_store,
3304 thread_store,
3305 text_thread_store,
3306 initial_prompt,
3307 window,
3308 cx,
3309 )
3310 })
3311 }
3312
3313 fn focus_agent_panel(
3314 &self,
3315 workspace: &mut Workspace,
3316 window: &mut Window,
3317 cx: &mut Context<Workspace>,
3318 ) -> bool {
3319 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3320 }
3321}
3322
3323pub struct ConcreteAssistantPanelDelegate;
3324
3325impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3326 fn active_context_editor(
3327 &self,
3328 workspace: &mut Workspace,
3329 _window: &mut Window,
3330 cx: &mut Context<Workspace>,
3331 ) -> Option<Entity<TextThreadEditor>> {
3332 let panel = workspace.panel::<AgentPanel>(cx)?;
3333 panel.read(cx).active_context_editor()
3334 }
3335
3336 fn open_saved_context(
3337 &self,
3338 workspace: &mut Workspace,
3339 path: Arc<Path>,
3340 window: &mut Window,
3341 cx: &mut Context<Workspace>,
3342 ) -> Task<Result<()>> {
3343 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3344 return Task::ready(Err(anyhow!("Agent panel not found")));
3345 };
3346
3347 panel.update(cx, |panel, cx| {
3348 panel.open_saved_prompt_editor(path, window, cx)
3349 })
3350 }
3351
3352 fn open_remote_context(
3353 &self,
3354 _workspace: &mut Workspace,
3355 _context_id: assistant_context::ContextId,
3356 _window: &mut Window,
3357 _cx: &mut Context<Workspace>,
3358 ) -> Task<Result<Entity<TextThreadEditor>>> {
3359 Task::ready(Err(anyhow!("opening remote context not implemented")))
3360 }
3361
3362 fn quote_selection(
3363 &self,
3364 workspace: &mut Workspace,
3365 selection_ranges: Vec<Range<Anchor>>,
3366 buffer: Entity<MultiBuffer>,
3367 window: &mut Window,
3368 cx: &mut Context<Workspace>,
3369 ) {
3370 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3371 return;
3372 };
3373
3374 if !panel.focus_handle(cx).contains_focused(window, cx) {
3375 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3376 }
3377
3378 panel.update(cx, |_, cx| {
3379 // Wait to create a new context until the workspace is no longer
3380 // being updated.
3381 cx.defer_in(window, move |panel, window, cx| {
3382 if let Some(message_editor) = panel.active_message_editor() {
3383 message_editor.update(cx, |message_editor, cx| {
3384 message_editor.context_store().update(cx, |store, cx| {
3385 let buffer = buffer.read(cx);
3386 let selection_ranges = selection_ranges
3387 .into_iter()
3388 .flat_map(|range| {
3389 let (start_buffer, start) =
3390 buffer.text_anchor_for_position(range.start, cx)?;
3391 let (end_buffer, end) =
3392 buffer.text_anchor_for_position(range.end, cx)?;
3393 if start_buffer != end_buffer {
3394 return None;
3395 }
3396 Some((start_buffer, start..end))
3397 })
3398 .collect::<Vec<_>>();
3399
3400 for (buffer, range) in selection_ranges {
3401 store.add_selection(buffer, range, cx);
3402 }
3403 })
3404 })
3405 } else if let Some(context_editor) = panel.active_context_editor() {
3406 let snapshot = buffer.read(cx).snapshot(cx);
3407 let selection_ranges = selection_ranges
3408 .into_iter()
3409 .map(|range| range.to_point(&snapshot))
3410 .collect::<Vec<_>>();
3411
3412 context_editor.update(cx, |context_editor, cx| {
3413 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3414 });
3415 }
3416 });
3417 });
3418 }
3419}
3420
3421struct Upsell;
3422
3423impl Dismissable for Upsell {
3424 const KEY: &'static str = "dismissed-trial-upsell";
3425}
3426
3427struct TrialEndUpsell;
3428
3429impl Dismissable for TrialEndUpsell {
3430 const KEY: &'static str = "dismissed-trial-end-upsell";
3431}