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