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