1use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
2
3use acp_thread::{AcpThread, AgentSessionInfo};
4use agent::{ContextServerRegistry, SharedThread, ThreadStore};
5use agent_client_protocol as acp;
6use agent_servers::AgentServer;
7use db::kvp::{Dismissable, KEY_VALUE_STORE};
8use project::{
9 ExternalAgentServerName,
10 agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
11};
12use serde::{Deserialize, Serialize};
13use settings::{LanguageModelProviderSetting, LanguageModelSelection};
14
15use zed_actions::agent::{OpenClaudeAgentOnboardingModal, ReauthenticateAgent};
16
17use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
18use crate::{
19 AddContextServer, AgentDiffPane, CopyThreadToClipboard, Follow, InlineAssistant,
20 LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
21 OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
22 ToggleOptionsMenu,
23 acp::AcpServerView,
24 agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
25 slash_command::SlashCommandCompletionProvider,
26 text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
27 ui::{AgentOnboardingModal, EndTrialUpsell},
28};
29use crate::{
30 ExpandMessageEditor,
31 acp::{AcpThreadHistory, ThreadHistoryEvent},
32 text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
33};
34use crate::{
35 ExternalAgent, ExternalAgentInitialContent, NewExternalAgentThread,
36 NewNativeAgentThreadFromSummary,
37};
38use crate::{ManageProfiles, acp::thread_view::AcpThreadView};
39use agent_settings::AgentSettings;
40use ai_onboarding::AgentPanelOnboarding;
41use anyhow::{Result, anyhow};
42use assistant_slash_command::SlashCommandWorkingSet;
43use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
44use client::UserStore;
45use cloud_api_types::Plan;
46use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
47use extension::ExtensionEvents;
48use extension_host::ExtensionStore;
49use fs::Fs;
50use gpui::{
51 Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
52 DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
53 Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
54};
55use language::LanguageRegistry;
56use language_model::{ConfigurationError, LanguageModelRegistry};
57use project::{Project, ProjectPath, Worktree};
58use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
59use rules_library::{RulesLibrary, open_rules_library};
60use search::{BufferSearchBar, buffer_search};
61use settings::{Settings, update_settings_file};
62use theme::ThemeSettings;
63use ui::{
64 Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab,
65 Tooltip, prelude::*, utils::WithRemSize,
66};
67use util::ResultExt as _;
68use workspace::{
69 CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
70 WorkspaceId,
71 dock::{DockPosition, Panel, PanelEvent},
72};
73use zed_actions::{
74 DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
75 agent::{
76 OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetAgentZoom, ResetOnboarding,
77 },
78 assistant::{OpenRulesLibrary, Toggle, ToggleFocus},
79};
80
81const AGENT_PANEL_KEY: &str = "agent_panel";
82const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
83const DEFAULT_THREAD_TITLE: &str = "New Thread";
84
85fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
86 let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
87 let key = i64::from(workspace_id).to_string();
88 scope
89 .read(&key)
90 .log_err()
91 .flatten()
92 .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
93}
94
95async fn save_serialized_panel(
96 workspace_id: workspace::WorkspaceId,
97 panel: SerializedAgentPanel,
98) -> Result<()> {
99 let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
100 let key = i64::from(workspace_id).to_string();
101 scope.write(key, serde_json::to_string(&panel)?).await?;
102 Ok(())
103}
104
105/// Migration: reads the original single-panel format stored under the
106/// `"agent_panel"` KVP key before per-workspace keying was introduced.
107fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
108 KEY_VALUE_STORE
109 .read_kvp(AGENT_PANEL_KEY)
110 .log_err()
111 .flatten()
112 .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
113}
114
115#[derive(Serialize, Deserialize, Debug, Clone)]
116struct SerializedAgentPanel {
117 width: Option<Pixels>,
118 selected_agent: Option<AgentType>,
119 #[serde(default)]
120 last_active_thread: Option<SerializedActiveThread>,
121}
122
123#[derive(Serialize, Deserialize, Debug, Clone)]
124struct SerializedActiveThread {
125 session_id: String,
126 agent_type: AgentType,
127 title: Option<String>,
128 cwd: Option<std::path::PathBuf>,
129}
130
131pub fn init(cx: &mut App) {
132 cx.observe_new(
133 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
134 workspace
135 .register_action(|workspace, action: &NewThread, window, cx| {
136 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
137 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
138 workspace.focus_panel::<AgentPanel>(window, cx);
139 }
140 })
141 .register_action(
142 |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
143 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
144 panel.update(cx, |panel, cx| {
145 panel.new_native_agent_thread_from_summary(action, window, cx)
146 });
147 workspace.focus_panel::<AgentPanel>(window, cx);
148 }
149 },
150 )
151 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
152 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
153 workspace.focus_panel::<AgentPanel>(window, cx);
154 panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
155 }
156 })
157 .register_action(|workspace, _: &OpenHistory, window, cx| {
158 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
159 workspace.focus_panel::<AgentPanel>(window, cx);
160 panel.update(cx, |panel, cx| panel.open_history(window, cx));
161 }
162 })
163 .register_action(|workspace, _: &OpenSettings, window, cx| {
164 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
165 workspace.focus_panel::<AgentPanel>(window, cx);
166 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
167 }
168 })
169 .register_action(|workspace, _: &NewTextThread, window, cx| {
170 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
171 workspace.focus_panel::<AgentPanel>(window, cx);
172 panel.update(cx, |panel, cx| {
173 panel.new_text_thread(window, cx);
174 });
175 }
176 })
177 .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
178 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
179 workspace.focus_panel::<AgentPanel>(window, cx);
180 panel.update(cx, |panel, cx| {
181 panel.external_thread(action.agent.clone(), None, None, window, cx)
182 });
183 }
184 })
185 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
186 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
187 workspace.focus_panel::<AgentPanel>(window, cx);
188 panel.update(cx, |panel, cx| {
189 panel.deploy_rules_library(action, window, cx)
190 });
191 }
192 })
193 .register_action(|workspace, _: &Follow, window, cx| {
194 workspace.follow(CollaboratorId::Agent, window, cx);
195 })
196 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
197 let thread = workspace
198 .panel::<AgentPanel>(cx)
199 .and_then(|panel| panel.read(cx).active_thread_view().cloned())
200 .and_then(|thread_view| {
201 thread_view
202 .read(cx)
203 .active_thread()
204 .map(|r| r.read(cx).thread.clone())
205 });
206
207 if let Some(thread) = thread {
208 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
209 }
210 })
211 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
212 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
213 workspace.focus_panel::<AgentPanel>(window, cx);
214 panel.update(cx, |panel, cx| {
215 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
216 });
217 }
218 })
219 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
220 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
221 workspace.focus_panel::<AgentPanel>(window, cx);
222 panel.update(cx, |panel, cx| {
223 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
224 });
225 }
226 })
227 .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
228 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
229 workspace.focus_panel::<AgentPanel>(window, cx);
230 panel.update(cx, |panel, cx| {
231 panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
232 });
233 }
234 })
235 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
236 AgentOnboardingModal::toggle(workspace, window, cx)
237 })
238 .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
239 AcpOnboardingModal::toggle(workspace, window, cx)
240 })
241 .register_action(
242 |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| {
243 ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
244 },
245 )
246 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
247 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
248 window.refresh();
249 })
250 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
251 OnboardingUpsell::set_dismissed(false, cx);
252 })
253 .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
254 TrialEndUpsell::set_dismissed(false, cx);
255 })
256 .register_action(|workspace, _: &ResetAgentZoom, window, cx| {
257 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
258 panel.update(cx, |panel, cx| {
259 panel.reset_agent_zoom(window, cx);
260 });
261 }
262 })
263 .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| {
264 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
265 panel.update(cx, |panel, cx| {
266 panel.copy_thread_to_clipboard(window, cx);
267 });
268 }
269 })
270 .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| {
271 if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
272 workspace.focus_panel::<AgentPanel>(window, cx);
273 panel.update(cx, |panel, cx| {
274 panel.load_thread_from_clipboard(window, cx);
275 });
276 }
277 });
278 },
279 )
280 .detach();
281}
282
283#[derive(Clone, Copy, Debug, PartialEq, Eq)]
284enum HistoryKind {
285 AgentThreads,
286 TextThreads,
287}
288
289enum ActiveView {
290 Uninitialized,
291 AgentThread {
292 server_view: Entity<AcpServerView>,
293 },
294 TextThread {
295 text_thread_editor: Entity<TextThreadEditor>,
296 title_editor: Entity<Editor>,
297 buffer_search_bar: Entity<BufferSearchBar>,
298 _subscriptions: Vec<gpui::Subscription>,
299 },
300 History {
301 kind: HistoryKind,
302 },
303 Configuration,
304}
305
306enum WhichFontSize {
307 AgentFont,
308 BufferFont,
309 None,
310}
311
312// TODO unify this with ExternalAgent
313#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
314pub enum AgentType {
315 #[default]
316 NativeAgent,
317 TextThread,
318 Gemini,
319 ClaudeAgent,
320 Codex,
321 Custom {
322 name: SharedString,
323 },
324}
325
326impl AgentType {
327 fn label(&self) -> SharedString {
328 match self {
329 Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
330 Self::Gemini => "Gemini CLI".into(),
331 Self::ClaudeAgent => "Claude Agent".into(),
332 Self::Codex => "Codex".into(),
333 Self::Custom { name, .. } => name.into(),
334 }
335 }
336
337 fn icon(&self) -> Option<IconName> {
338 match self {
339 Self::NativeAgent | Self::TextThread => None,
340 Self::Gemini => Some(IconName::AiGemini),
341 Self::ClaudeAgent => Some(IconName::AiClaude),
342 Self::Codex => Some(IconName::AiOpenAi),
343 Self::Custom { .. } => Some(IconName::Sparkle),
344 }
345 }
346}
347
348impl From<ExternalAgent> for AgentType {
349 fn from(value: ExternalAgent) -> Self {
350 match value {
351 ExternalAgent::Gemini => Self::Gemini,
352 ExternalAgent::ClaudeCode => Self::ClaudeAgent,
353 ExternalAgent::Codex => Self::Codex,
354 ExternalAgent::Custom { name } => Self::Custom { name },
355 ExternalAgent::NativeAgent => Self::NativeAgent,
356 }
357 }
358}
359
360impl ActiveView {
361 pub fn which_font_size_used(&self) -> WhichFontSize {
362 match self {
363 ActiveView::Uninitialized
364 | ActiveView::AgentThread { .. }
365 | ActiveView::History { .. } => WhichFontSize::AgentFont,
366 ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
367 ActiveView::Configuration => WhichFontSize::None,
368 }
369 }
370
371 pub fn text_thread(
372 text_thread_editor: Entity<TextThreadEditor>,
373 language_registry: Arc<LanguageRegistry>,
374 window: &mut Window,
375 cx: &mut App,
376 ) -> Self {
377 let title = text_thread_editor.read(cx).title(cx).to_string();
378
379 let editor = cx.new(|cx| {
380 let mut editor = Editor::single_line(window, cx);
381 editor.set_text(title, window, cx);
382 editor
383 });
384
385 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
386 // cause a custom summary to be set. The presence of this custom summary would cause
387 // summarization to not happen.
388 let mut suppress_first_edit = true;
389
390 let subscriptions = vec![
391 window.subscribe(&editor, cx, {
392 {
393 let text_thread_editor = text_thread_editor.clone();
394 move |editor, event, window, cx| match event {
395 EditorEvent::BufferEdited => {
396 if suppress_first_edit {
397 suppress_first_edit = false;
398 return;
399 }
400 let new_summary = editor.read(cx).text(cx);
401
402 text_thread_editor.update(cx, |text_thread_editor, cx| {
403 text_thread_editor
404 .text_thread()
405 .update(cx, |text_thread, cx| {
406 text_thread.set_custom_summary(new_summary, cx);
407 })
408 })
409 }
410 EditorEvent::Blurred => {
411 if editor.read(cx).text(cx).is_empty() {
412 let summary = text_thread_editor
413 .read(cx)
414 .text_thread()
415 .read(cx)
416 .summary()
417 .or_default();
418
419 editor.update(cx, |editor, cx| {
420 editor.set_text(summary, window, cx);
421 });
422 }
423 }
424 _ => {}
425 }
426 }
427 }),
428 window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
429 let editor = editor.clone();
430 move |text_thread, event, window, cx| match event {
431 TextThreadEvent::SummaryGenerated => {
432 let summary = text_thread.read(cx).summary().or_default();
433
434 editor.update(cx, |editor, cx| {
435 editor.set_text(summary, window, cx);
436 })
437 }
438 TextThreadEvent::PathChanged { .. } => {}
439 _ => {}
440 }
441 }),
442 ];
443
444 let buffer_search_bar =
445 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
446 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
447 buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx)
448 });
449
450 Self::TextThread {
451 text_thread_editor,
452 title_editor: editor,
453 buffer_search_bar,
454 _subscriptions: subscriptions,
455 }
456 }
457}
458
459pub struct AgentPanel {
460 workspace: WeakEntity<Workspace>,
461 /// Workspace id is used as a database key
462 workspace_id: Option<WorkspaceId>,
463 user_store: Entity<UserStore>,
464 project: Entity<Project>,
465 fs: Arc<dyn Fs>,
466 language_registry: Arc<LanguageRegistry>,
467 acp_history: Entity<AcpThreadHistory>,
468 text_thread_history: Entity<TextThreadHistory>,
469 thread_store: Entity<ThreadStore>,
470 text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
471 prompt_store: Option<Entity<PromptStore>>,
472 context_server_registry: Entity<ContextServerRegistry>,
473 configuration: Option<Entity<AgentConfiguration>>,
474 configuration_subscription: Option<Subscription>,
475 focus_handle: FocusHandle,
476 active_view: ActiveView,
477 previous_view: Option<ActiveView>,
478 _active_view_observation: Option<Subscription>,
479 new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
480 agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
481 agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
482 agent_navigation_menu: Option<Entity<ContextMenu>>,
483 _extension_subscription: Option<Subscription>,
484 width: Option<Pixels>,
485 height: Option<Pixels>,
486 zoomed: bool,
487 pending_serialization: Option<Task<Result<()>>>,
488 onboarding: Entity<AgentPanelOnboarding>,
489 selected_agent: AgentType,
490 show_trust_workspace_message: bool,
491 last_configuration_error_telemetry: Option<String>,
492}
493
494impl AgentPanel {
495 fn serialize(&mut self, cx: &mut App) {
496 let Some(workspace_id) = self.workspace_id else {
497 return;
498 };
499
500 let width = self.width;
501 let selected_agent = self.selected_agent.clone();
502
503 let last_active_thread = self.active_agent_thread(cx).map(|thread| {
504 let thread = thread.read(cx);
505 let title = thread.title();
506 SerializedActiveThread {
507 session_id: thread.session_id().0.to_string(),
508 agent_type: self.selected_agent.clone(),
509 title: if title.as_ref() != DEFAULT_THREAD_TITLE {
510 Some(title.to_string())
511 } else {
512 None
513 },
514 cwd: None,
515 }
516 });
517
518 self.pending_serialization = Some(cx.background_spawn(async move {
519 save_serialized_panel(
520 workspace_id,
521 SerializedAgentPanel {
522 width,
523 selected_agent: Some(selected_agent),
524 last_active_thread,
525 },
526 )
527 .await?;
528 anyhow::Ok(())
529 }));
530 }
531
532 pub fn load(
533 workspace: WeakEntity<Workspace>,
534 prompt_builder: Arc<PromptBuilder>,
535 mut cx: AsyncWindowContext,
536 ) -> Task<Result<Entity<Self>>> {
537 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
538 cx.spawn(async move |cx| {
539 let prompt_store = match prompt_store {
540 Ok(prompt_store) => prompt_store.await.ok(),
541 Err(_) => None,
542 };
543 let workspace_id = workspace
544 .read_with(cx, |workspace, _| workspace.database_id())
545 .ok()
546 .flatten();
547
548 let serialized_panel = cx
549 .background_spawn(async move {
550 workspace_id
551 .and_then(read_serialized_panel)
552 .or_else(read_legacy_serialized_panel)
553 })
554 .await;
555
556 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
557 let text_thread_store = workspace
558 .update(cx, |workspace, cx| {
559 let project = workspace.project().clone();
560 assistant_text_thread::TextThreadStore::new(
561 project,
562 prompt_builder,
563 slash_commands,
564 cx,
565 )
566 })?
567 .await?;
568
569 let panel = workspace.update_in(cx, |workspace, window, cx| {
570 let panel =
571 cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
572
573 if let Some(serialized_panel) = &serialized_panel {
574 panel.update(cx, |panel, cx| {
575 panel.width = serialized_panel.width.map(|w| w.round());
576 if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
577 panel.selected_agent = selected_agent;
578 }
579 cx.notify();
580 });
581 }
582
583 panel
584 })?;
585
586 if let Some(thread_info) = serialized_panel.and_then(|p| p.last_active_thread) {
587 let session_id = acp::SessionId::new(thread_info.session_id.clone());
588 let load_task = panel.update(cx, |panel, cx| {
589 let thread_store = panel.thread_store.clone();
590 thread_store.update(cx, |store, cx| store.load_thread(session_id, cx))
591 });
592 let thread_exists = load_task
593 .await
594 .map(|thread: Option<agent::DbThread>| thread.is_some())
595 .unwrap_or(false);
596
597 if thread_exists {
598 panel.update_in(cx, |panel, window, cx| {
599 panel.selected_agent = thread_info.agent_type.clone();
600 let session_info = AgentSessionInfo {
601 session_id: acp::SessionId::new(thread_info.session_id),
602 cwd: thread_info.cwd,
603 title: thread_info.title.map(SharedString::from),
604 updated_at: None,
605 meta: None,
606 };
607 panel.load_agent_thread(session_info, window, cx);
608 })?;
609 } else {
610 log::error!(
611 "could not restore last active thread: \
612 no thread found in database with ID {:?}",
613 thread_info.session_id
614 );
615 }
616 }
617
618 Ok(panel)
619 })
620 }
621
622 pub(crate) fn new(
623 workspace: &Workspace,
624 text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
625 prompt_store: Option<Entity<PromptStore>>,
626 window: &mut Window,
627 cx: &mut Context<Self>,
628 ) -> Self {
629 let fs = workspace.app_state().fs.clone();
630 let user_store = workspace.app_state().user_store.clone();
631 let project = workspace.project();
632 let language_registry = project.read(cx).languages().clone();
633 let client = workspace.client().clone();
634 let workspace_id = workspace.database_id();
635 let workspace = workspace.weak_handle();
636
637 let context_server_registry =
638 cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
639
640 let thread_store = ThreadStore::global(cx);
641 let acp_history = cx.new(|cx| AcpThreadHistory::new(None, window, cx));
642 let text_thread_history =
643 cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
644 cx.subscribe_in(
645 &acp_history,
646 window,
647 |this, _, event, window, cx| match event {
648 ThreadHistoryEvent::Open(thread) => {
649 this.load_agent_thread(thread.clone(), window, cx);
650 }
651 },
652 )
653 .detach();
654 cx.subscribe_in(
655 &text_thread_history,
656 window,
657 |this, _, event, window, cx| match event {
658 TextThreadHistoryEvent::Open(thread) => {
659 this.open_saved_text_thread(thread.path.clone(), window, cx)
660 .detach_and_log_err(cx);
661 }
662 },
663 )
664 .detach();
665
666 let active_view = ActiveView::Uninitialized;
667
668 let weak_panel = cx.entity().downgrade();
669
670 window.defer(cx, move |window, cx| {
671 let panel = weak_panel.clone();
672 let agent_navigation_menu =
673 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
674 if let Some(panel) = panel.upgrade() {
675 if let Some(kind) = panel.read(cx).history_kind_for_selected_agent(cx) {
676 menu =
677 Self::populate_recently_updated_menu_section(menu, panel, kind, cx);
678 let view_all_label = match kind {
679 HistoryKind::AgentThreads => "View All",
680 HistoryKind::TextThreads => "View All Text Threads",
681 };
682 menu = menu.action(view_all_label, Box::new(OpenHistory));
683 }
684 }
685
686 menu = menu
687 .fixed_width(px(320.).into())
688 .keep_open_on_confirm(false)
689 .key_context("NavigationMenu");
690
691 menu
692 });
693 weak_panel
694 .update(cx, |panel, cx| {
695 cx.subscribe_in(
696 &agent_navigation_menu,
697 window,
698 |_, menu, _: &DismissEvent, window, cx| {
699 menu.update(cx, |menu, _| {
700 menu.clear_selected();
701 });
702 cx.focus_self(window);
703 },
704 )
705 .detach();
706 panel.agent_navigation_menu = Some(agent_navigation_menu);
707 })
708 .ok();
709 });
710
711 let onboarding = cx.new(|cx| {
712 AgentPanelOnboarding::new(
713 user_store.clone(),
714 client,
715 |_window, cx| {
716 OnboardingUpsell::set_dismissed(true, cx);
717 },
718 cx,
719 )
720 });
721
722 // Subscribe to extension events to sync agent servers when extensions change
723 let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
724 {
725 Some(
726 cx.subscribe(&extension_events, |this, _source, event, cx| match event {
727 extension::Event::ExtensionInstalled(_)
728 | extension::Event::ExtensionUninstalled(_)
729 | extension::Event::ExtensionsInstalledChanged => {
730 this.sync_agent_servers_from_extensions(cx);
731 }
732 _ => {}
733 }),
734 )
735 } else {
736 None
737 };
738
739 let mut panel = Self {
740 workspace_id,
741 active_view,
742 workspace,
743 user_store,
744 project: project.clone(),
745 fs: fs.clone(),
746 language_registry,
747 text_thread_store,
748 prompt_store,
749 configuration: None,
750 configuration_subscription: None,
751 focus_handle: cx.focus_handle(),
752 context_server_registry,
753 previous_view: None,
754 _active_view_observation: None,
755 new_thread_menu_handle: PopoverMenuHandle::default(),
756 agent_panel_menu_handle: PopoverMenuHandle::default(),
757 agent_navigation_menu_handle: PopoverMenuHandle::default(),
758 agent_navigation_menu: None,
759 _extension_subscription: extension_subscription,
760 width: None,
761 height: None,
762 zoomed: false,
763 pending_serialization: None,
764 onboarding,
765 acp_history,
766 text_thread_history,
767 thread_store,
768 selected_agent: AgentType::default(),
769 show_trust_workspace_message: false,
770 last_configuration_error_telemetry: None,
771 };
772
773 // Initial sync of agent servers from extensions
774 panel.sync_agent_servers_from_extensions(cx);
775 panel
776 }
777
778 pub fn toggle_focus(
779 workspace: &mut Workspace,
780 _: &ToggleFocus,
781 window: &mut Window,
782 cx: &mut Context<Workspace>,
783 ) {
784 if workspace
785 .panel::<Self>(cx)
786 .is_some_and(|panel| panel.read(cx).enabled(cx))
787 {
788 workspace.toggle_panel_focus::<Self>(window, cx);
789 }
790 }
791
792 pub fn toggle(
793 workspace: &mut Workspace,
794 _: &Toggle,
795 window: &mut Window,
796 cx: &mut Context<Workspace>,
797 ) {
798 if workspace
799 .panel::<Self>(cx)
800 .is_some_and(|panel| panel.read(cx).enabled(cx))
801 {
802 if !workspace.toggle_panel_focus::<Self>(window, cx) {
803 workspace.close_panel::<Self>(window, cx);
804 }
805 }
806 }
807
808 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
809 &self.prompt_store
810 }
811
812 pub fn thread_store(&self) -> &Entity<ThreadStore> {
813 &self.thread_store
814 }
815
816 pub fn history(&self) -> &Entity<AcpThreadHistory> {
817 &self.acp_history
818 }
819
820 pub fn open_thread(
821 &mut self,
822 thread: AgentSessionInfo,
823 window: &mut Window,
824 cx: &mut Context<Self>,
825 ) {
826 self.external_thread(
827 Some(crate::ExternalAgent::NativeAgent),
828 Some(thread),
829 None,
830 window,
831 cx,
832 );
833 }
834
835 pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
836 &self.context_server_registry
837 }
838
839 pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
840 let workspace_read = workspace.read(cx);
841
842 workspace_read
843 .panel::<AgentPanel>(cx)
844 .map(|panel| {
845 let panel_id = Entity::entity_id(&panel);
846
847 workspace_read.all_docks().iter().any(|dock| {
848 dock.read(cx)
849 .visible_panel()
850 .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
851 })
852 })
853 .unwrap_or(false)
854 }
855
856 pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
857 match &self.active_view {
858 ActiveView::AgentThread { server_view, .. } => Some(server_view),
859 ActiveView::Uninitialized
860 | ActiveView::TextThread { .. }
861 | ActiveView::History { .. }
862 | ActiveView::Configuration => None,
863 }
864 }
865
866 fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
867 self.new_agent_thread(AgentType::NativeAgent, window, cx);
868 }
869
870 fn new_native_agent_thread_from_summary(
871 &mut self,
872 action: &NewNativeAgentThreadFromSummary,
873 window: &mut Window,
874 cx: &mut Context<Self>,
875 ) {
876 let Some(thread) = self
877 .acp_history
878 .read(cx)
879 .session_for_id(&action.from_session_id)
880 else {
881 return;
882 };
883
884 self.external_thread(
885 Some(ExternalAgent::NativeAgent),
886 None,
887 Some(ExternalAgentInitialContent::ThreadSummary(thread)),
888 window,
889 cx,
890 );
891 }
892
893 fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
894 telemetry::event!("Agent Thread Started", agent = "zed-text");
895
896 let context = self
897 .text_thread_store
898 .update(cx, |context_store, cx| context_store.create(cx));
899 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
900 .log_err()
901 .flatten();
902
903 let text_thread_editor = cx.new(|cx| {
904 let mut editor = TextThreadEditor::for_text_thread(
905 context,
906 self.fs.clone(),
907 self.workspace.clone(),
908 self.project.clone(),
909 lsp_adapter_delegate,
910 window,
911 cx,
912 );
913 editor.insert_default_prompt(window, cx);
914 editor
915 });
916
917 if self.selected_agent != AgentType::TextThread {
918 self.selected_agent = AgentType::TextThread;
919 self.serialize(cx);
920 }
921
922 self.set_active_view(
923 ActiveView::text_thread(
924 text_thread_editor.clone(),
925 self.language_registry.clone(),
926 window,
927 cx,
928 ),
929 true,
930 window,
931 cx,
932 );
933 text_thread_editor.focus_handle(cx).focus(window, cx);
934 }
935
936 fn external_thread(
937 &mut self,
938 agent_choice: Option<crate::ExternalAgent>,
939 resume_thread: Option<AgentSessionInfo>,
940 initial_content: Option<ExternalAgentInitialContent>,
941 window: &mut Window,
942 cx: &mut Context<Self>,
943 ) {
944 let workspace = self.workspace.clone();
945 let project = self.project.clone();
946 let fs = self.fs.clone();
947 let is_via_collab = self.project.read(cx).is_via_collab();
948
949 const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
950
951 #[derive(Serialize, Deserialize)]
952 struct LastUsedExternalAgent {
953 agent: crate::ExternalAgent,
954 }
955
956 let thread_store = self.thread_store.clone();
957
958 cx.spawn_in(window, async move |this, cx| {
959 let ext_agent = match agent_choice {
960 Some(agent) => {
961 cx.background_spawn({
962 let agent = agent.clone();
963 async move {
964 if let Some(serialized) =
965 serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
966 {
967 KEY_VALUE_STORE
968 .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
969 .await
970 .log_err();
971 }
972 }
973 })
974 .detach();
975
976 agent
977 }
978 None => {
979 if is_via_collab {
980 ExternalAgent::NativeAgent
981 } else {
982 cx.background_spawn(async move {
983 KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
984 })
985 .await
986 .log_err()
987 .flatten()
988 .and_then(|value| {
989 serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
990 })
991 .map(|agent| agent.agent)
992 .unwrap_or(ExternalAgent::NativeAgent)
993 }
994 }
995 };
996
997 let server = ext_agent.server(fs, thread_store);
998 this.update_in(cx, |agent_panel, window, cx| {
999 agent_panel._external_thread(
1000 server,
1001 resume_thread,
1002 initial_content,
1003 workspace,
1004 project,
1005 ext_agent,
1006 window,
1007 cx,
1008 );
1009 })?;
1010
1011 anyhow::Ok(())
1012 })
1013 .detach_and_log_err(cx);
1014 }
1015
1016 fn deploy_rules_library(
1017 &mut self,
1018 action: &OpenRulesLibrary,
1019 _window: &mut Window,
1020 cx: &mut Context<Self>,
1021 ) {
1022 open_rules_library(
1023 self.language_registry.clone(),
1024 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1025 Rc::new(|| {
1026 Rc::new(SlashCommandCompletionProvider::new(
1027 Arc::new(SlashCommandWorkingSet::default()),
1028 None,
1029 None,
1030 ))
1031 }),
1032 action
1033 .prompt_to_select
1034 .map(|uuid| UserPromptId(uuid).into()),
1035 cx,
1036 )
1037 .detach_and_log_err(cx);
1038 }
1039
1040 fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1041 let Some(thread_view) = self.active_thread_view() else {
1042 return;
1043 };
1044
1045 let Some(active_thread) = thread_view.read(cx).active_thread().cloned() else {
1046 return;
1047 };
1048
1049 active_thread.update(cx, |active_thread, cx| {
1050 active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
1051 active_thread.focus_handle(cx).focus(window, cx);
1052 })
1053 }
1054
1055 fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
1056 match self.selected_agent {
1057 AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
1058 AgentType::TextThread => Some(HistoryKind::TextThreads),
1059 AgentType::Gemini
1060 | AgentType::ClaudeAgent
1061 | AgentType::Codex
1062 | AgentType::Custom { .. } => {
1063 if self.acp_history.read(cx).has_session_list() {
1064 Some(HistoryKind::AgentThreads)
1065 } else {
1066 None
1067 }
1068 }
1069 }
1070 }
1071
1072 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1073 let Some(kind) = self.history_kind_for_selected_agent(cx) else {
1074 return;
1075 };
1076
1077 if let ActiveView::History { kind: active_kind } = self.active_view {
1078 if active_kind == kind {
1079 if let Some(previous_view) = self.previous_view.take() {
1080 self.set_active_view(previous_view, true, window, cx);
1081 }
1082 return;
1083 }
1084 }
1085
1086 self.set_active_view(ActiveView::History { kind }, true, window, cx);
1087 cx.notify();
1088 }
1089
1090 pub(crate) fn open_saved_text_thread(
1091 &mut self,
1092 path: Arc<Path>,
1093 window: &mut Window,
1094 cx: &mut Context<Self>,
1095 ) -> Task<Result<()>> {
1096 let text_thread_task = self
1097 .text_thread_store
1098 .update(cx, |store, cx| store.open_local(path, cx));
1099 cx.spawn_in(window, async move |this, cx| {
1100 let text_thread = text_thread_task.await?;
1101 this.update_in(cx, |this, window, cx| {
1102 this.open_text_thread(text_thread, window, cx);
1103 })
1104 })
1105 }
1106
1107 pub(crate) fn open_text_thread(
1108 &mut self,
1109 text_thread: Entity<TextThread>,
1110 window: &mut Window,
1111 cx: &mut Context<Self>,
1112 ) {
1113 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1114 .log_err()
1115 .flatten();
1116 let editor = cx.new(|cx| {
1117 TextThreadEditor::for_text_thread(
1118 text_thread,
1119 self.fs.clone(),
1120 self.workspace.clone(),
1121 self.project.clone(),
1122 lsp_adapter_delegate,
1123 window,
1124 cx,
1125 )
1126 });
1127
1128 if self.selected_agent != AgentType::TextThread {
1129 self.selected_agent = AgentType::TextThread;
1130 self.serialize(cx);
1131 }
1132
1133 self.set_active_view(
1134 ActiveView::text_thread(editor, self.language_registry.clone(), window, cx),
1135 true,
1136 window,
1137 cx,
1138 );
1139 }
1140
1141 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1142 match self.active_view {
1143 ActiveView::Configuration | ActiveView::History { .. } => {
1144 if let Some(previous_view) = self.previous_view.take() {
1145 self.set_active_view(previous_view, true, window, cx);
1146 }
1147 cx.notify();
1148 }
1149 _ => {}
1150 }
1151 }
1152
1153 pub fn toggle_navigation_menu(
1154 &mut self,
1155 _: &ToggleNavigationMenu,
1156 window: &mut Window,
1157 cx: &mut Context<Self>,
1158 ) {
1159 if self.history_kind_for_selected_agent(cx).is_none() {
1160 return;
1161 }
1162 self.agent_navigation_menu_handle.toggle(window, cx);
1163 }
1164
1165 pub fn toggle_options_menu(
1166 &mut self,
1167 _: &ToggleOptionsMenu,
1168 window: &mut Window,
1169 cx: &mut Context<Self>,
1170 ) {
1171 self.agent_panel_menu_handle.toggle(window, cx);
1172 }
1173
1174 pub fn toggle_new_thread_menu(
1175 &mut self,
1176 _: &ToggleNewThreadMenu,
1177 window: &mut Window,
1178 cx: &mut Context<Self>,
1179 ) {
1180 self.new_thread_menu_handle.toggle(window, cx);
1181 }
1182
1183 pub fn increase_font_size(
1184 &mut self,
1185 action: &IncreaseBufferFontSize,
1186 _: &mut Window,
1187 cx: &mut Context<Self>,
1188 ) {
1189 self.handle_font_size_action(action.persist, px(1.0), cx);
1190 }
1191
1192 pub fn decrease_font_size(
1193 &mut self,
1194 action: &DecreaseBufferFontSize,
1195 _: &mut Window,
1196 cx: &mut Context<Self>,
1197 ) {
1198 self.handle_font_size_action(action.persist, px(-1.0), cx);
1199 }
1200
1201 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1202 match self.active_view.which_font_size_used() {
1203 WhichFontSize::AgentFont => {
1204 if persist {
1205 update_settings_file(self.fs.clone(), cx, move |settings, cx| {
1206 let agent_ui_font_size =
1207 ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
1208 let agent_buffer_font_size =
1209 ThemeSettings::get_global(cx).agent_buffer_font_size(cx) + delta;
1210
1211 let _ = settings
1212 .theme
1213 .agent_ui_font_size
1214 .insert(f32::from(theme::clamp_font_size(agent_ui_font_size)).into());
1215 let _ = settings.theme.agent_buffer_font_size.insert(
1216 f32::from(theme::clamp_font_size(agent_buffer_font_size)).into(),
1217 );
1218 });
1219 } else {
1220 theme::adjust_agent_ui_font_size(cx, |size| size + delta);
1221 theme::adjust_agent_buffer_font_size(cx, |size| size + delta);
1222 }
1223 }
1224 WhichFontSize::BufferFont => {
1225 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1226 // default handler that changes that font size.
1227 cx.propagate();
1228 }
1229 WhichFontSize::None => {}
1230 }
1231 }
1232
1233 pub fn reset_font_size(
1234 &mut self,
1235 action: &ResetBufferFontSize,
1236 _: &mut Window,
1237 cx: &mut Context<Self>,
1238 ) {
1239 if action.persist {
1240 update_settings_file(self.fs.clone(), cx, move |settings, _| {
1241 settings.theme.agent_ui_font_size = None;
1242 settings.theme.agent_buffer_font_size = None;
1243 });
1244 } else {
1245 theme::reset_agent_ui_font_size(cx);
1246 theme::reset_agent_buffer_font_size(cx);
1247 }
1248 }
1249
1250 pub fn reset_agent_zoom(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1251 theme::reset_agent_ui_font_size(cx);
1252 theme::reset_agent_buffer_font_size(cx);
1253 }
1254
1255 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1256 if self.zoomed {
1257 cx.emit(PanelEvent::ZoomOut);
1258 } else {
1259 if !self.focus_handle(cx).contains_focused(window, cx) {
1260 cx.focus_self(window);
1261 }
1262 cx.emit(PanelEvent::ZoomIn);
1263 }
1264 }
1265
1266 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1267 let agent_server_store = self.project.read(cx).agent_server_store().clone();
1268 let context_server_store = self.project.read(cx).context_server_store();
1269 let fs = self.fs.clone();
1270
1271 self.set_active_view(ActiveView::Configuration, true, window, cx);
1272 self.configuration = Some(cx.new(|cx| {
1273 AgentConfiguration::new(
1274 fs,
1275 agent_server_store,
1276 context_server_store,
1277 self.context_server_registry.clone(),
1278 self.language_registry.clone(),
1279 self.workspace.clone(),
1280 window,
1281 cx,
1282 )
1283 }));
1284
1285 if let Some(configuration) = self.configuration.as_ref() {
1286 self.configuration_subscription = Some(cx.subscribe_in(
1287 configuration,
1288 window,
1289 Self::handle_agent_configuration_event,
1290 ));
1291
1292 configuration.focus_handle(cx).focus(window, cx);
1293 }
1294 }
1295
1296 pub(crate) fn open_active_thread_as_markdown(
1297 &mut self,
1298 _: &OpenActiveThreadAsMarkdown,
1299 window: &mut Window,
1300 cx: &mut Context<Self>,
1301 ) {
1302 if let Some(workspace) = self.workspace.upgrade()
1303 && let Some(thread_view) = self.active_thread_view()
1304 && let Some(active_thread) = thread_view.read(cx).active_thread().cloned()
1305 {
1306 active_thread.update(cx, |thread, cx| {
1307 thread
1308 .open_thread_as_markdown(workspace, window, cx)
1309 .detach_and_log_err(cx);
1310 });
1311 }
1312 }
1313
1314 fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1315 let Some(thread) = self.active_native_agent_thread(cx) else {
1316 if let Some(workspace) = self.workspace.upgrade() {
1317 workspace.update(cx, |workspace, cx| {
1318 struct NoThreadToast;
1319 workspace.show_toast(
1320 workspace::Toast::new(
1321 workspace::notifications::NotificationId::unique::<NoThreadToast>(),
1322 "No active native thread to copy",
1323 )
1324 .autohide(),
1325 cx,
1326 );
1327 });
1328 }
1329 return;
1330 };
1331
1332 let workspace = self.workspace.clone();
1333 let load_task = thread.read(cx).to_db(cx);
1334
1335 cx.spawn_in(window, async move |_this, cx| {
1336 let db_thread = load_task.await;
1337 let shared_thread = SharedThread::from_db_thread(&db_thread);
1338 let thread_data = shared_thread.to_bytes()?;
1339 let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1340
1341 cx.update(|_window, cx| {
1342 cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1343 if let Some(workspace) = workspace.upgrade() {
1344 workspace.update(cx, |workspace, cx| {
1345 struct ThreadCopiedToast;
1346 workspace.show_toast(
1347 workspace::Toast::new(
1348 workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1349 "Thread copied to clipboard (base64 encoded)",
1350 )
1351 .autohide(),
1352 cx,
1353 );
1354 });
1355 }
1356 })?;
1357
1358 anyhow::Ok(())
1359 })
1360 .detach_and_log_err(cx);
1361 }
1362
1363 fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1364 let Some(clipboard) = cx.read_from_clipboard() else {
1365 if let Some(workspace) = self.workspace.upgrade() {
1366 workspace.update(cx, |workspace, cx| {
1367 struct NoClipboardToast;
1368 workspace.show_toast(
1369 workspace::Toast::new(
1370 workspace::notifications::NotificationId::unique::<NoClipboardToast>(),
1371 "No clipboard content available",
1372 )
1373 .autohide(),
1374 cx,
1375 );
1376 });
1377 }
1378 return;
1379 };
1380
1381 let Some(encoded) = clipboard.text() else {
1382 if let Some(workspace) = self.workspace.upgrade() {
1383 workspace.update(cx, |workspace, cx| {
1384 struct InvalidClipboardToast;
1385 workspace.show_toast(
1386 workspace::Toast::new(
1387 workspace::notifications::NotificationId::unique::<InvalidClipboardToast>(),
1388 "Clipboard does not contain text",
1389 )
1390 .autohide(),
1391 cx,
1392 );
1393 });
1394 }
1395 return;
1396 };
1397
1398 let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1399 {
1400 Ok(data) => data,
1401 Err(_) => {
1402 if let Some(workspace) = self.workspace.upgrade() {
1403 workspace.update(cx, |workspace, cx| {
1404 struct DecodeErrorToast;
1405 workspace.show_toast(
1406 workspace::Toast::new(
1407 workspace::notifications::NotificationId::unique::<DecodeErrorToast>(),
1408 "Failed to decode clipboard content (expected base64)",
1409 )
1410 .autohide(),
1411 cx,
1412 );
1413 });
1414 }
1415 return;
1416 }
1417 };
1418
1419 let shared_thread = match SharedThread::from_bytes(&thread_data) {
1420 Ok(thread) => thread,
1421 Err(_) => {
1422 if let Some(workspace) = self.workspace.upgrade() {
1423 workspace.update(cx, |workspace, cx| {
1424 struct ParseErrorToast;
1425 workspace.show_toast(
1426 workspace::Toast::new(
1427 workspace::notifications::NotificationId::unique::<ParseErrorToast>(
1428 ),
1429 "Failed to parse thread data from clipboard",
1430 )
1431 .autohide(),
1432 cx,
1433 );
1434 });
1435 }
1436 return;
1437 }
1438 };
1439
1440 let db_thread = shared_thread.to_db_thread();
1441 let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1442 let thread_store = self.thread_store.clone();
1443 let title = db_thread.title.clone();
1444 let workspace = self.workspace.clone();
1445
1446 cx.spawn_in(window, async move |this, cx| {
1447 thread_store
1448 .update(&mut cx.clone(), |store, cx| {
1449 store.save_thread(session_id.clone(), db_thread, cx)
1450 })
1451 .await?;
1452
1453 let thread_metadata = acp_thread::AgentSessionInfo {
1454 session_id,
1455 cwd: None,
1456 title: Some(title),
1457 updated_at: Some(chrono::Utc::now()),
1458 meta: None,
1459 };
1460
1461 this.update_in(cx, |this, window, cx| {
1462 this.open_thread(thread_metadata, window, cx);
1463 })?;
1464
1465 this.update_in(cx, |_, _window, cx| {
1466 if let Some(workspace) = workspace.upgrade() {
1467 workspace.update(cx, |workspace, cx| {
1468 struct ThreadLoadedToast;
1469 workspace.show_toast(
1470 workspace::Toast::new(
1471 workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1472 "Thread loaded from clipboard",
1473 )
1474 .autohide(),
1475 cx,
1476 );
1477 });
1478 }
1479 })?;
1480
1481 anyhow::Ok(())
1482 })
1483 .detach_and_log_err(cx);
1484 }
1485
1486 fn handle_agent_configuration_event(
1487 &mut self,
1488 _entity: &Entity<AgentConfiguration>,
1489 event: &AssistantConfigurationEvent,
1490 window: &mut Window,
1491 cx: &mut Context<Self>,
1492 ) {
1493 match event {
1494 AssistantConfigurationEvent::NewThread(provider) => {
1495 if LanguageModelRegistry::read_global(cx)
1496 .default_model()
1497 .is_none_or(|model| model.provider.id() != provider.id())
1498 && let Some(model) = provider.default_model(cx)
1499 {
1500 update_settings_file(self.fs.clone(), cx, move |settings, _| {
1501 let provider = model.provider_id().0.to_string();
1502 let enable_thinking = model.supports_thinking();
1503 let effort = model
1504 .default_effort_level()
1505 .map(|effort| effort.value.to_string());
1506 let model = model.id().0.to_string();
1507 settings
1508 .agent
1509 .get_or_insert_default()
1510 .set_model(LanguageModelSelection {
1511 provider: LanguageModelProviderSetting(provider),
1512 model,
1513 enable_thinking,
1514 effort,
1515 })
1516 });
1517 }
1518
1519 self.new_thread(&NewThread, window, cx);
1520 if let Some((thread, model)) = self
1521 .active_native_agent_thread(cx)
1522 .zip(provider.default_model(cx))
1523 {
1524 thread.update(cx, |thread, cx| {
1525 thread.set_model(model, cx);
1526 });
1527 }
1528 }
1529 }
1530 }
1531
1532 pub fn as_active_server_view(&self) -> Option<&Entity<AcpServerView>> {
1533 match &self.active_view {
1534 ActiveView::AgentThread { server_view } => Some(server_view),
1535 _ => None,
1536 }
1537 }
1538
1539 pub fn as_active_thread_view(&self, cx: &App) -> Option<Entity<AcpThreadView>> {
1540 let server_view = self.as_active_server_view()?;
1541 server_view.read(cx).active_thread().cloned()
1542 }
1543
1544 pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1545 match &self.active_view {
1546 ActiveView::AgentThread { server_view, .. } => server_view
1547 .read(cx)
1548 .active_thread()
1549 .map(|r| r.read(cx).thread.clone()),
1550 _ => None,
1551 }
1552 }
1553
1554 pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1555 match &self.active_view {
1556 ActiveView::AgentThread { server_view, .. } => {
1557 server_view.read(cx).as_native_thread(cx)
1558 }
1559 _ => None,
1560 }
1561 }
1562
1563 pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1564 match &self.active_view {
1565 ActiveView::TextThread {
1566 text_thread_editor, ..
1567 } => Some(text_thread_editor.clone()),
1568 _ => None,
1569 }
1570 }
1571
1572 fn set_active_view(
1573 &mut self,
1574 new_view: ActiveView,
1575 focus: bool,
1576 window: &mut Window,
1577 cx: &mut Context<Self>,
1578 ) {
1579 let was_in_agent_history = matches!(
1580 self.active_view,
1581 ActiveView::History {
1582 kind: HistoryKind::AgentThreads
1583 }
1584 );
1585 let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1586 let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1587 let new_is_history = matches!(new_view, ActiveView::History { .. });
1588
1589 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1590 let new_is_config = matches!(new_view, ActiveView::Configuration);
1591
1592 let current_is_special = current_is_history || current_is_config;
1593 let new_is_special = new_is_history || new_is_config;
1594
1595 if current_is_uninitialized || (current_is_special && !new_is_special) {
1596 self.active_view = new_view;
1597 } else if !current_is_special && new_is_special {
1598 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1599 } else {
1600 if !new_is_special {
1601 self.previous_view = None;
1602 }
1603 self.active_view = new_view;
1604 }
1605
1606 self._active_view_observation = match &self.active_view {
1607 ActiveView::AgentThread { server_view } => {
1608 Some(cx.observe(server_view, |this, _, cx| {
1609 cx.emit(AgentPanelEvent::ActiveViewChanged);
1610 this.serialize(cx);
1611 cx.notify();
1612 }))
1613 }
1614 _ => None,
1615 };
1616
1617 let is_in_agent_history = matches!(
1618 self.active_view,
1619 ActiveView::History {
1620 kind: HistoryKind::AgentThreads
1621 }
1622 );
1623
1624 if !was_in_agent_history && is_in_agent_history {
1625 self.acp_history
1626 .update(cx, |history, cx| history.refresh_full_history(cx));
1627 }
1628
1629 if focus {
1630 self.focus_handle(cx).focus(window, cx);
1631 }
1632 cx.emit(AgentPanelEvent::ActiveViewChanged);
1633 }
1634
1635 fn populate_recently_updated_menu_section(
1636 mut menu: ContextMenu,
1637 panel: Entity<Self>,
1638 kind: HistoryKind,
1639 cx: &mut Context<ContextMenu>,
1640 ) -> ContextMenu {
1641 match kind {
1642 HistoryKind::AgentThreads => {
1643 let entries = panel
1644 .read(cx)
1645 .acp_history
1646 .read(cx)
1647 .sessions()
1648 .iter()
1649 .take(RECENTLY_UPDATED_MENU_LIMIT)
1650 .cloned()
1651 .collect::<Vec<_>>();
1652
1653 if entries.is_empty() {
1654 return menu;
1655 }
1656
1657 menu = menu.header("Recently Updated");
1658
1659 for entry in entries {
1660 let title = entry
1661 .title
1662 .as_ref()
1663 .filter(|title| !title.is_empty())
1664 .cloned()
1665 .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1666
1667 menu = menu.entry(title, None, {
1668 let panel = panel.downgrade();
1669 let entry = entry.clone();
1670 move |window, cx| {
1671 let entry = entry.clone();
1672 panel
1673 .update(cx, move |this, cx| {
1674 this.load_agent_thread(entry.clone(), window, cx);
1675 })
1676 .ok();
1677 }
1678 });
1679 }
1680 }
1681 HistoryKind::TextThreads => {
1682 let entries = panel
1683 .read(cx)
1684 .text_thread_store
1685 .read(cx)
1686 .ordered_text_threads()
1687 .take(RECENTLY_UPDATED_MENU_LIMIT)
1688 .cloned()
1689 .collect::<Vec<_>>();
1690
1691 if entries.is_empty() {
1692 return menu;
1693 }
1694
1695 menu = menu.header("Recent Text Threads");
1696
1697 for entry in entries {
1698 let title = if entry.title.is_empty() {
1699 SharedString::new_static(DEFAULT_THREAD_TITLE)
1700 } else {
1701 entry.title.clone()
1702 };
1703
1704 menu = menu.entry(title, None, {
1705 let panel = panel.downgrade();
1706 let entry = entry.clone();
1707 move |window, cx| {
1708 let path = entry.path.clone();
1709 panel
1710 .update(cx, move |this, cx| {
1711 this.open_saved_text_thread(path.clone(), window, cx)
1712 .detach_and_log_err(cx);
1713 })
1714 .ok();
1715 }
1716 });
1717 }
1718 }
1719 }
1720
1721 menu.separator()
1722 }
1723
1724 pub fn selected_agent(&self) -> AgentType {
1725 self.selected_agent.clone()
1726 }
1727
1728 fn selected_external_agent(&self) -> Option<ExternalAgent> {
1729 match &self.selected_agent {
1730 AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
1731 AgentType::Gemini => Some(ExternalAgent::Gemini),
1732 AgentType::ClaudeAgent => Some(ExternalAgent::ClaudeCode),
1733 AgentType::Codex => Some(ExternalAgent::Codex),
1734 AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
1735 AgentType::TextThread => None,
1736 }
1737 }
1738
1739 fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1740 if let Some(extension_store) = ExtensionStore::try_global(cx) {
1741 let (manifests, extensions_dir) = {
1742 let store = extension_store.read(cx);
1743 let installed = store.installed_extensions();
1744 let manifests: Vec<_> = installed
1745 .iter()
1746 .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1747 .collect();
1748 let extensions_dir = paths::extensions_dir().join("installed");
1749 (manifests, extensions_dir)
1750 };
1751
1752 self.project.update(cx, |project, cx| {
1753 project.agent_server_store().update(cx, |store, cx| {
1754 let manifest_refs: Vec<_> = manifests
1755 .iter()
1756 .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1757 .collect();
1758 store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1759 });
1760 });
1761 }
1762 }
1763
1764 pub fn new_external_thread_with_text(
1765 &mut self,
1766 initial_text: Option<String>,
1767 window: &mut Window,
1768 cx: &mut Context<Self>,
1769 ) {
1770 self.external_thread(
1771 None,
1772 None,
1773 initial_text.map(ExternalAgentInitialContent::Text),
1774 window,
1775 cx,
1776 );
1777 }
1778
1779 pub fn new_agent_thread(
1780 &mut self,
1781 agent: AgentType,
1782 window: &mut Window,
1783 cx: &mut Context<Self>,
1784 ) {
1785 match agent {
1786 AgentType::TextThread => {
1787 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1788 }
1789 AgentType::NativeAgent => self.external_thread(
1790 Some(crate::ExternalAgent::NativeAgent),
1791 None,
1792 None,
1793 window,
1794 cx,
1795 ),
1796 AgentType::Gemini => {
1797 self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1798 }
1799 AgentType::ClaudeAgent => {
1800 self.selected_agent = AgentType::ClaudeAgent;
1801 self.serialize(cx);
1802 self.external_thread(
1803 Some(crate::ExternalAgent::ClaudeCode),
1804 None,
1805 None,
1806 window,
1807 cx,
1808 )
1809 }
1810 AgentType::Codex => {
1811 self.selected_agent = AgentType::Codex;
1812 self.serialize(cx);
1813 self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
1814 }
1815 AgentType::Custom { name } => self.external_thread(
1816 Some(crate::ExternalAgent::Custom { name }),
1817 None,
1818 None,
1819 window,
1820 cx,
1821 ),
1822 }
1823 }
1824
1825 pub fn load_agent_thread(
1826 &mut self,
1827 thread: AgentSessionInfo,
1828 window: &mut Window,
1829 cx: &mut Context<Self>,
1830 ) {
1831 let Some(agent) = self.selected_external_agent() else {
1832 return;
1833 };
1834 self.external_thread(Some(agent), Some(thread), None, window, cx);
1835 }
1836
1837 fn _external_thread(
1838 &mut self,
1839 server: Rc<dyn AgentServer>,
1840 resume_thread: Option<AgentSessionInfo>,
1841 initial_content: Option<ExternalAgentInitialContent>,
1842 workspace: WeakEntity<Workspace>,
1843 project: Entity<Project>,
1844 ext_agent: ExternalAgent,
1845 window: &mut Window,
1846 cx: &mut Context<Self>,
1847 ) {
1848 let selected_agent = AgentType::from(ext_agent);
1849 if self.selected_agent != selected_agent {
1850 self.selected_agent = selected_agent;
1851 self.serialize(cx);
1852 }
1853 let thread_store = server
1854 .clone()
1855 .downcast::<agent::NativeAgentServer>()
1856 .is_some()
1857 .then(|| self.thread_store.clone());
1858
1859 let server_view = cx.new(|cx| {
1860 crate::acp::AcpServerView::new(
1861 server,
1862 resume_thread,
1863 initial_content,
1864 workspace.clone(),
1865 project,
1866 thread_store,
1867 self.prompt_store.clone(),
1868 self.acp_history.clone(),
1869 window,
1870 cx,
1871 )
1872 });
1873
1874 self.set_active_view(ActiveView::AgentThread { server_view }, true, window, cx);
1875 }
1876}
1877
1878impl Focusable for AgentPanel {
1879 fn focus_handle(&self, cx: &App) -> FocusHandle {
1880 match &self.active_view {
1881 ActiveView::Uninitialized => self.focus_handle.clone(),
1882 ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
1883 ActiveView::History { kind } => match kind {
1884 HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1885 HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1886 },
1887 ActiveView::TextThread {
1888 text_thread_editor, ..
1889 } => text_thread_editor.focus_handle(cx),
1890 ActiveView::Configuration => {
1891 if let Some(configuration) = self.configuration.as_ref() {
1892 configuration.focus_handle(cx)
1893 } else {
1894 self.focus_handle.clone()
1895 }
1896 }
1897 }
1898 }
1899}
1900
1901fn agent_panel_dock_position(cx: &App) -> DockPosition {
1902 AgentSettings::get_global(cx).dock.into()
1903}
1904
1905pub enum AgentPanelEvent {
1906 ActiveViewChanged,
1907}
1908
1909impl EventEmitter<PanelEvent> for AgentPanel {}
1910impl EventEmitter<AgentPanelEvent> for AgentPanel {}
1911
1912impl Panel for AgentPanel {
1913 fn persistent_name() -> &'static str {
1914 "AgentPanel"
1915 }
1916
1917 fn panel_key() -> &'static str {
1918 AGENT_PANEL_KEY
1919 }
1920
1921 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1922 agent_panel_dock_position(cx)
1923 }
1924
1925 fn position_is_valid(&self, position: DockPosition) -> bool {
1926 position != DockPosition::Bottom
1927 }
1928
1929 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1930 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1931 settings
1932 .agent
1933 .get_or_insert_default()
1934 .set_dock(position.into());
1935 });
1936 }
1937
1938 fn size(&self, window: &Window, cx: &App) -> Pixels {
1939 let settings = AgentSettings::get_global(cx);
1940 match self.position(window, cx) {
1941 DockPosition::Left | DockPosition::Right => {
1942 self.width.unwrap_or(settings.default_width)
1943 }
1944 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1945 }
1946 }
1947
1948 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1949 match self.position(window, cx) {
1950 DockPosition::Left | DockPosition::Right => self.width = size,
1951 DockPosition::Bottom => self.height = size,
1952 }
1953 self.serialize(cx);
1954 cx.notify();
1955 }
1956
1957 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1958 if active && matches!(self.active_view, ActiveView::Uninitialized) {
1959 let selected_agent = self.selected_agent.clone();
1960 self.new_agent_thread(selected_agent, window, cx);
1961 }
1962 }
1963
1964 fn remote_id() -> Option<proto::PanelId> {
1965 Some(proto::PanelId::AssistantPanel)
1966 }
1967
1968 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1969 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1970 }
1971
1972 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1973 Some("Agent Panel")
1974 }
1975
1976 fn toggle_action(&self) -> Box<dyn Action> {
1977 Box::new(ToggleFocus)
1978 }
1979
1980 fn activation_priority(&self) -> u32 {
1981 3
1982 }
1983
1984 fn enabled(&self, cx: &App) -> bool {
1985 AgentSettings::get_global(cx).enabled(cx)
1986 }
1987
1988 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1989 self.zoomed
1990 }
1991
1992 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1993 self.zoomed = zoomed;
1994 cx.notify();
1995 }
1996}
1997
1998impl AgentPanel {
1999 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2000 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2001
2002 let content = match &self.active_view {
2003 ActiveView::AgentThread { server_view } => {
2004 let is_generating_title = server_view
2005 .read(cx)
2006 .as_native_thread(cx)
2007 .map_or(false, |t| t.read(cx).is_generating_title());
2008
2009 if let Some(title_editor) = server_view
2010 .read(cx)
2011 .parent_thread(cx)
2012 .map(|r| r.read(cx).title_editor.clone())
2013 {
2014 let container = div()
2015 .w_full()
2016 .on_action({
2017 let thread_view = server_view.downgrade();
2018 move |_: &menu::Confirm, window, cx| {
2019 if let Some(thread_view) = thread_view.upgrade() {
2020 thread_view.focus_handle(cx).focus(window, cx);
2021 }
2022 }
2023 })
2024 .on_action({
2025 let thread_view = server_view.downgrade();
2026 move |_: &editor::actions::Cancel, window, cx| {
2027 if let Some(thread_view) = thread_view.upgrade() {
2028 thread_view.focus_handle(cx).focus(window, cx);
2029 }
2030 }
2031 })
2032 .child(title_editor);
2033
2034 if is_generating_title {
2035 container
2036 .with_animation(
2037 "generating_title",
2038 Animation::new(Duration::from_secs(2))
2039 .repeat()
2040 .with_easing(pulsating_between(0.4, 0.8)),
2041 |div, delta| div.opacity(delta),
2042 )
2043 .into_any_element()
2044 } else {
2045 container.into_any_element()
2046 }
2047 } else {
2048 Label::new(server_view.read(cx).title(cx))
2049 .color(Color::Muted)
2050 .truncate()
2051 .into_any_element()
2052 }
2053 }
2054 ActiveView::TextThread {
2055 title_editor,
2056 text_thread_editor,
2057 ..
2058 } => {
2059 let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
2060
2061 match summary {
2062 TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
2063 .color(Color::Muted)
2064 .truncate()
2065 .into_any_element(),
2066 TextThreadSummary::Content(summary) => {
2067 if summary.done {
2068 div()
2069 .w_full()
2070 .child(title_editor.clone())
2071 .into_any_element()
2072 } else {
2073 Label::new(LOADING_SUMMARY_PLACEHOLDER)
2074 .truncate()
2075 .color(Color::Muted)
2076 .with_animation(
2077 "generating_title",
2078 Animation::new(Duration::from_secs(2))
2079 .repeat()
2080 .with_easing(pulsating_between(0.4, 0.8)),
2081 |label, delta| label.alpha(delta),
2082 )
2083 .into_any_element()
2084 }
2085 }
2086 TextThreadSummary::Error => h_flex()
2087 .w_full()
2088 .child(title_editor.clone())
2089 .child(
2090 IconButton::new("retry-summary-generation", IconName::RotateCcw)
2091 .icon_size(IconSize::Small)
2092 .on_click({
2093 let text_thread_editor = text_thread_editor.clone();
2094 move |_, _window, cx| {
2095 text_thread_editor.update(cx, |text_thread_editor, cx| {
2096 text_thread_editor.regenerate_summary(cx);
2097 });
2098 }
2099 })
2100 .tooltip(move |_window, cx| {
2101 cx.new(|_| {
2102 Tooltip::new("Failed to generate title")
2103 .meta("Click to try again")
2104 })
2105 .into()
2106 }),
2107 )
2108 .into_any_element(),
2109 }
2110 }
2111 ActiveView::History { kind } => {
2112 let title = match kind {
2113 HistoryKind::AgentThreads => "History",
2114 HistoryKind::TextThreads => "Text Thread History",
2115 };
2116 Label::new(title).truncate().into_any_element()
2117 }
2118 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2119 ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
2120 };
2121
2122 h_flex()
2123 .key_context("TitleEditor")
2124 .id("TitleEditor")
2125 .flex_grow()
2126 .w_full()
2127 .max_w_full()
2128 .overflow_x_scroll()
2129 .child(content)
2130 .into_any()
2131 }
2132
2133 fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
2134 thread_view.update(cx, |thread_view, cx| {
2135 if let Some(thread) = thread_view.as_native_thread(cx) {
2136 thread.update(cx, |thread, cx| {
2137 thread.generate_title(cx);
2138 });
2139 }
2140 });
2141 }
2142
2143 fn handle_regenerate_text_thread_title(
2144 text_thread_editor: Entity<TextThreadEditor>,
2145 cx: &mut App,
2146 ) {
2147 text_thread_editor.update(cx, |text_thread_editor, cx| {
2148 text_thread_editor.regenerate_summary(cx);
2149 });
2150 }
2151
2152 fn render_panel_options_menu(
2153 &self,
2154 window: &mut Window,
2155 cx: &mut Context<Self>,
2156 ) -> impl IntoElement {
2157 let focus_handle = self.focus_handle(cx);
2158
2159 let full_screen_label = if self.is_zoomed(window, cx) {
2160 "Disable Full Screen"
2161 } else {
2162 "Enable Full Screen"
2163 };
2164
2165 let selected_agent = self.selected_agent.clone();
2166
2167 let text_thread_view = match &self.active_view {
2168 ActiveView::TextThread {
2169 text_thread_editor, ..
2170 } => Some(text_thread_editor.clone()),
2171 _ => None,
2172 };
2173 let text_thread_with_messages = match &self.active_view {
2174 ActiveView::TextThread {
2175 text_thread_editor, ..
2176 } => text_thread_editor
2177 .read(cx)
2178 .text_thread()
2179 .read(cx)
2180 .messages(cx)
2181 .any(|message| message.role == language_model::Role::Assistant),
2182 _ => false,
2183 };
2184
2185 let thread_view = match &self.active_view {
2186 ActiveView::AgentThread { server_view } => Some(server_view.clone()),
2187 _ => None,
2188 };
2189 let thread_with_messages = match &self.active_view {
2190 ActiveView::AgentThread { server_view } => {
2191 server_view.read(cx).has_user_submitted_prompt(cx)
2192 }
2193 _ => false,
2194 };
2195
2196 PopoverMenu::new("agent-options-menu")
2197 .trigger_with_tooltip(
2198 IconButton::new("agent-options-menu", IconName::Ellipsis)
2199 .icon_size(IconSize::Small),
2200 {
2201 let focus_handle = focus_handle.clone();
2202 move |_window, cx| {
2203 Tooltip::for_action_in(
2204 "Toggle Agent Menu",
2205 &ToggleOptionsMenu,
2206 &focus_handle,
2207 cx,
2208 )
2209 }
2210 },
2211 )
2212 .anchor(Corner::TopRight)
2213 .with_handle(self.agent_panel_menu_handle.clone())
2214 .menu({
2215 move |window, cx| {
2216 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2217 menu = menu.context(focus_handle.clone());
2218
2219 if thread_with_messages | text_thread_with_messages {
2220 menu = menu.header("Current Thread");
2221
2222 if let Some(text_thread_view) = text_thread_view.as_ref() {
2223 menu = menu
2224 .entry("Regenerate Thread Title", None, {
2225 let text_thread_view = text_thread_view.clone();
2226 move |_, cx| {
2227 Self::handle_regenerate_text_thread_title(
2228 text_thread_view.clone(),
2229 cx,
2230 );
2231 }
2232 })
2233 .separator();
2234 }
2235
2236 if let Some(thread_view) = thread_view.as_ref() {
2237 menu = menu
2238 .entry("Regenerate Thread Title", None, {
2239 let thread_view = thread_view.clone();
2240 move |_, cx| {
2241 Self::handle_regenerate_thread_title(
2242 thread_view.clone(),
2243 cx,
2244 );
2245 }
2246 })
2247 .separator();
2248 }
2249 }
2250
2251 menu = menu
2252 .header("MCP Servers")
2253 .action(
2254 "View Server Extensions",
2255 Box::new(zed_actions::Extensions {
2256 category_filter: Some(
2257 zed_actions::ExtensionCategoryFilter::ContextServers,
2258 ),
2259 id: None,
2260 }),
2261 )
2262 .action("Add Custom Server…", Box::new(AddContextServer))
2263 .separator()
2264 .action("Rules", Box::new(OpenRulesLibrary::default()))
2265 .action("Profiles", Box::new(ManageProfiles::default()))
2266 .action("Settings", Box::new(OpenSettings))
2267 .separator()
2268 .action(full_screen_label, Box::new(ToggleZoom));
2269
2270 if selected_agent == AgentType::Gemini {
2271 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2272 }
2273
2274 menu
2275 }))
2276 }
2277 })
2278 }
2279
2280 fn render_recent_entries_menu(
2281 &self,
2282 icon: IconName,
2283 corner: Corner,
2284 cx: &mut Context<Self>,
2285 ) -> impl IntoElement {
2286 let focus_handle = self.focus_handle(cx);
2287
2288 PopoverMenu::new("agent-nav-menu")
2289 .trigger_with_tooltip(
2290 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2291 {
2292 move |_window, cx| {
2293 Tooltip::for_action_in(
2294 "Toggle Recently Updated Threads",
2295 &ToggleNavigationMenu,
2296 &focus_handle,
2297 cx,
2298 )
2299 }
2300 },
2301 )
2302 .anchor(corner)
2303 .with_handle(self.agent_navigation_menu_handle.clone())
2304 .menu({
2305 let menu = self.agent_navigation_menu.clone();
2306 move |window, cx| {
2307 telemetry::event!("View Thread History Clicked");
2308
2309 if let Some(menu) = menu.as_ref() {
2310 menu.update(cx, |_, cx| {
2311 cx.defer_in(window, |menu, window, cx| {
2312 menu.rebuild(window, cx);
2313 });
2314 })
2315 }
2316 menu.clone()
2317 }
2318 })
2319 }
2320
2321 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2322 let focus_handle = self.focus_handle(cx);
2323
2324 IconButton::new("go-back", IconName::ArrowLeft)
2325 .icon_size(IconSize::Small)
2326 .on_click(cx.listener(|this, _, window, cx| {
2327 this.go_back(&workspace::GoBack, window, cx);
2328 }))
2329 .tooltip({
2330 move |_window, cx| {
2331 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2332 }
2333 })
2334 }
2335
2336 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2337 let agent_server_store = self.project.read(cx).agent_server_store().clone();
2338 let focus_handle = self.focus_handle(cx);
2339
2340 let (selected_agent_custom_icon, selected_agent_label) =
2341 if let AgentType::Custom { name, .. } = &self.selected_agent {
2342 let store = agent_server_store.read(cx);
2343 let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2344
2345 let label = store
2346 .agent_display_name(&ExternalAgentServerName(name.clone()))
2347 .unwrap_or_else(|| self.selected_agent.label());
2348 (icon, label)
2349 } else {
2350 (None, self.selected_agent.label())
2351 };
2352
2353 let active_thread = match &self.active_view {
2354 ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
2355 ActiveView::Uninitialized
2356 | ActiveView::TextThread { .. }
2357 | ActiveView::History { .. }
2358 | ActiveView::Configuration => None,
2359 };
2360
2361 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2362 .trigger_with_tooltip(
2363 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2364 {
2365 let focus_handle = focus_handle.clone();
2366 move |_window, cx| {
2367 Tooltip::for_action_in(
2368 "New Thread…",
2369 &ToggleNewThreadMenu,
2370 &focus_handle,
2371 cx,
2372 )
2373 }
2374 },
2375 )
2376 .anchor(Corner::TopRight)
2377 .with_handle(self.new_thread_menu_handle.clone())
2378 .menu({
2379 let selected_agent = self.selected_agent.clone();
2380 let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2381
2382 let workspace = self.workspace.clone();
2383 let is_via_collab = workspace
2384 .update(cx, |workspace, cx| {
2385 workspace.project().read(cx).is_via_collab()
2386 })
2387 .unwrap_or_default();
2388
2389 move |window, cx| {
2390 telemetry::event!("New Thread Clicked");
2391
2392 let active_thread = active_thread.clone();
2393 Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2394 menu.context(focus_handle.clone())
2395 .when_some(active_thread, |this, active_thread| {
2396 let thread = active_thread.read(cx);
2397
2398 if !thread.is_empty() {
2399 let session_id = thread.id().clone();
2400 this.item(
2401 ContextMenuEntry::new("New From Summary")
2402 .icon(IconName::ThreadFromSummary)
2403 .icon_color(Color::Muted)
2404 .handler(move |window, cx| {
2405 window.dispatch_action(
2406 Box::new(NewNativeAgentThreadFromSummary {
2407 from_session_id: session_id.clone(),
2408 }),
2409 cx,
2410 );
2411 }),
2412 )
2413 } else {
2414 this
2415 }
2416 })
2417 .item(
2418 ContextMenuEntry::new("Zed Agent")
2419 .when(
2420 is_agent_selected(AgentType::NativeAgent)
2421 | is_agent_selected(AgentType::TextThread),
2422 |this| {
2423 this.action(Box::new(NewExternalAgentThread {
2424 agent: None,
2425 }))
2426 },
2427 )
2428 .icon(IconName::ZedAgent)
2429 .icon_color(Color::Muted)
2430 .handler({
2431 let workspace = workspace.clone();
2432 move |window, cx| {
2433 if let Some(workspace) = workspace.upgrade() {
2434 workspace.update(cx, |workspace, cx| {
2435 if let Some(panel) =
2436 workspace.panel::<AgentPanel>(cx)
2437 {
2438 panel.update(cx, |panel, cx| {
2439 panel.new_agent_thread(
2440 AgentType::NativeAgent,
2441 window,
2442 cx,
2443 );
2444 });
2445 }
2446 });
2447 }
2448 }
2449 }),
2450 )
2451 .item(
2452 ContextMenuEntry::new("Text Thread")
2453 .action(NewTextThread.boxed_clone())
2454 .icon(IconName::TextThread)
2455 .icon_color(Color::Muted)
2456 .handler({
2457 let workspace = workspace.clone();
2458 move |window, cx| {
2459 if let Some(workspace) = workspace.upgrade() {
2460 workspace.update(cx, |workspace, cx| {
2461 if let Some(panel) =
2462 workspace.panel::<AgentPanel>(cx)
2463 {
2464 panel.update(cx, |panel, cx| {
2465 panel.new_agent_thread(
2466 AgentType::TextThread,
2467 window,
2468 cx,
2469 );
2470 });
2471 }
2472 });
2473 }
2474 }
2475 }),
2476 )
2477 .separator()
2478 .header("External Agents")
2479 .item(
2480 ContextMenuEntry::new("Claude Agent")
2481 .when(is_agent_selected(AgentType::ClaudeAgent), |this| {
2482 this.action(Box::new(NewExternalAgentThread {
2483 agent: None,
2484 }))
2485 })
2486 .icon(IconName::AiClaude)
2487 .disabled(is_via_collab)
2488 .icon_color(Color::Muted)
2489 .handler({
2490 let workspace = workspace.clone();
2491 move |window, cx| {
2492 if let Some(workspace) = workspace.upgrade() {
2493 workspace.update(cx, |workspace, cx| {
2494 if let Some(panel) =
2495 workspace.panel::<AgentPanel>(cx)
2496 {
2497 panel.update(cx, |panel, cx| {
2498 panel.new_agent_thread(
2499 AgentType::ClaudeAgent,
2500 window,
2501 cx,
2502 );
2503 });
2504 }
2505 });
2506 }
2507 }
2508 }),
2509 )
2510 .item(
2511 ContextMenuEntry::new("Codex CLI")
2512 .when(is_agent_selected(AgentType::Codex), |this| {
2513 this.action(Box::new(NewExternalAgentThread {
2514 agent: None,
2515 }))
2516 })
2517 .icon(IconName::AiOpenAi)
2518 .disabled(is_via_collab)
2519 .icon_color(Color::Muted)
2520 .handler({
2521 let workspace = workspace.clone();
2522 move |window, cx| {
2523 if let Some(workspace) = workspace.upgrade() {
2524 workspace.update(cx, |workspace, cx| {
2525 if let Some(panel) =
2526 workspace.panel::<AgentPanel>(cx)
2527 {
2528 panel.update(cx, |panel, cx| {
2529 panel.new_agent_thread(
2530 AgentType::Codex,
2531 window,
2532 cx,
2533 );
2534 });
2535 }
2536 });
2537 }
2538 }
2539 }),
2540 )
2541 .item(
2542 ContextMenuEntry::new("Gemini CLI")
2543 .when(is_agent_selected(AgentType::Gemini), |this| {
2544 this.action(Box::new(NewExternalAgentThread {
2545 agent: None,
2546 }))
2547 })
2548 .icon(IconName::AiGemini)
2549 .icon_color(Color::Muted)
2550 .disabled(is_via_collab)
2551 .handler({
2552 let workspace = workspace.clone();
2553 move |window, cx| {
2554 if let Some(workspace) = workspace.upgrade() {
2555 workspace.update(cx, |workspace, cx| {
2556 if let Some(panel) =
2557 workspace.panel::<AgentPanel>(cx)
2558 {
2559 panel.update(cx, |panel, cx| {
2560 panel.new_agent_thread(
2561 AgentType::Gemini,
2562 window,
2563 cx,
2564 );
2565 });
2566 }
2567 });
2568 }
2569 }
2570 }),
2571 )
2572 .map(|mut menu| {
2573 let agent_server_store = agent_server_store.read(cx);
2574 let agent_names = agent_server_store
2575 .external_agents()
2576 .filter(|name| {
2577 name.0 != GEMINI_NAME
2578 && name.0 != CLAUDE_AGENT_NAME
2579 && name.0 != CODEX_NAME
2580 })
2581 .cloned()
2582 .collect::<Vec<_>>();
2583
2584 for agent_name in agent_names {
2585 let icon_path = agent_server_store.agent_icon(&agent_name);
2586 let display_name = agent_server_store
2587 .agent_display_name(&agent_name)
2588 .unwrap_or_else(|| agent_name.0.clone());
2589
2590 let mut entry = ContextMenuEntry::new(display_name);
2591
2592 if let Some(icon_path) = icon_path {
2593 entry = entry.custom_icon_svg(icon_path);
2594 } else {
2595 entry = entry.icon(IconName::Sparkle);
2596 }
2597 entry = entry
2598 .when(
2599 is_agent_selected(AgentType::Custom {
2600 name: agent_name.0.clone(),
2601 }),
2602 |this| {
2603 this.action(Box::new(NewExternalAgentThread {
2604 agent: None,
2605 }))
2606 },
2607 )
2608 .icon_color(Color::Muted)
2609 .disabled(is_via_collab)
2610 .handler({
2611 let workspace = workspace.clone();
2612 let agent_name = agent_name.clone();
2613 move |window, cx| {
2614 if let Some(workspace) = workspace.upgrade() {
2615 workspace.update(cx, |workspace, cx| {
2616 if let Some(panel) =
2617 workspace.panel::<AgentPanel>(cx)
2618 {
2619 panel.update(cx, |panel, cx| {
2620 panel.new_agent_thread(
2621 AgentType::Custom {
2622 name: agent_name
2623 .clone()
2624 .into(),
2625 },
2626 window,
2627 cx,
2628 );
2629 });
2630 }
2631 });
2632 }
2633 }
2634 });
2635
2636 menu = menu.item(entry);
2637 }
2638
2639 menu
2640 })
2641 .separator()
2642 .item(
2643 ContextMenuEntry::new("Add More Agents")
2644 .icon(IconName::Plus)
2645 .icon_color(Color::Muted)
2646 .handler({
2647 move |window, cx| {
2648 window.dispatch_action(
2649 Box::new(zed_actions::AcpRegistry),
2650 cx,
2651 )
2652 }
2653 }),
2654 )
2655 }))
2656 }
2657 });
2658
2659 let is_thread_loading = self
2660 .active_thread_view()
2661 .map(|thread| thread.read(cx).is_loading())
2662 .unwrap_or(false);
2663
2664 let has_custom_icon = selected_agent_custom_icon.is_some();
2665
2666 let selected_agent = div()
2667 .id("selected_agent_icon")
2668 .when_some(selected_agent_custom_icon, |this, icon_path| {
2669 this.px_1()
2670 .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2671 })
2672 .when(!has_custom_icon, |this| {
2673 this.when_some(self.selected_agent.icon(), |this, icon| {
2674 this.px_1().child(Icon::new(icon).color(Color::Muted))
2675 })
2676 })
2677 .tooltip(move |_, cx| {
2678 Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2679 });
2680
2681 let selected_agent = if is_thread_loading {
2682 selected_agent
2683 .with_animation(
2684 "pulsating-icon",
2685 Animation::new(Duration::from_secs(1))
2686 .repeat()
2687 .with_easing(pulsating_between(0.2, 0.6)),
2688 |icon, delta| icon.opacity(delta),
2689 )
2690 .into_any_element()
2691 } else {
2692 selected_agent.into_any_element()
2693 };
2694
2695 let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2696
2697 h_flex()
2698 .id("agent-panel-toolbar")
2699 .h(Tab::container_height(cx))
2700 .max_w_full()
2701 .flex_none()
2702 .justify_between()
2703 .gap_2()
2704 .bg(cx.theme().colors().tab_bar_background)
2705 .border_b_1()
2706 .border_color(cx.theme().colors().border)
2707 .child(
2708 h_flex()
2709 .size_full()
2710 .gap(DynamicSpacing::Base04.rems(cx))
2711 .pl(DynamicSpacing::Base04.rems(cx))
2712 .child(match &self.active_view {
2713 ActiveView::History { .. } | ActiveView::Configuration => {
2714 self.render_toolbar_back_button(cx).into_any_element()
2715 }
2716 _ => selected_agent.into_any_element(),
2717 })
2718 .child(self.render_title_view(window, cx)),
2719 )
2720 .child(
2721 h_flex()
2722 .flex_none()
2723 .gap(DynamicSpacing::Base02.rems(cx))
2724 .pl(DynamicSpacing::Base04.rems(cx))
2725 .pr(DynamicSpacing::Base06.rems(cx))
2726 .child(new_thread_menu)
2727 .when(show_history_menu, |this| {
2728 this.child(self.render_recent_entries_menu(
2729 IconName::MenuAltTemp,
2730 Corner::TopRight,
2731 cx,
2732 ))
2733 })
2734 .child(self.render_panel_options_menu(window, cx)),
2735 )
2736 }
2737
2738 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2739 if TrialEndUpsell::dismissed() {
2740 return false;
2741 }
2742
2743 match &self.active_view {
2744 ActiveView::TextThread { .. } => {
2745 if LanguageModelRegistry::global(cx)
2746 .read(cx)
2747 .default_model()
2748 .is_some_and(|model| {
2749 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2750 })
2751 {
2752 return false;
2753 }
2754 }
2755 ActiveView::Uninitialized
2756 | ActiveView::AgentThread { .. }
2757 | ActiveView::History { .. }
2758 | ActiveView::Configuration => return false,
2759 }
2760
2761 let plan = self.user_store.read(cx).plan();
2762 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2763
2764 plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2765 }
2766
2767 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2768 if OnboardingUpsell::dismissed() {
2769 return false;
2770 }
2771
2772 let user_store = self.user_store.read(cx);
2773
2774 if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2775 && user_store
2776 .subscription_period()
2777 .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2778 .is_some_and(|date| date < chrono::Utc::now())
2779 {
2780 OnboardingUpsell::set_dismissed(true, cx);
2781 return false;
2782 }
2783
2784 match &self.active_view {
2785 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2786 false
2787 }
2788 ActiveView::AgentThread { server_view, .. }
2789 if server_view.read(cx).as_native_thread(cx).is_none() =>
2790 {
2791 false
2792 }
2793 _ => {
2794 let history_is_empty = self.acp_history.read(cx).is_empty();
2795
2796 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2797 .visible_providers()
2798 .iter()
2799 .any(|provider| {
2800 provider.is_authenticated(cx)
2801 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2802 });
2803
2804 history_is_empty || !has_configured_non_zed_providers
2805 }
2806 }
2807 }
2808
2809 fn render_onboarding(
2810 &self,
2811 _window: &mut Window,
2812 cx: &mut Context<Self>,
2813 ) -> Option<impl IntoElement> {
2814 if !self.should_render_onboarding(cx) {
2815 return None;
2816 }
2817
2818 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2819
2820 Some(
2821 div()
2822 .when(text_thread_view, |this| {
2823 this.bg(cx.theme().colors().editor_background)
2824 })
2825 .child(self.onboarding.clone()),
2826 )
2827 }
2828
2829 fn render_trial_end_upsell(
2830 &self,
2831 _window: &mut Window,
2832 cx: &mut Context<Self>,
2833 ) -> Option<impl IntoElement> {
2834 if !self.should_render_trial_end_upsell(cx) {
2835 return None;
2836 }
2837
2838 Some(
2839 v_flex()
2840 .absolute()
2841 .inset_0()
2842 .size_full()
2843 .bg(cx.theme().colors().panel_background)
2844 .opacity(0.85)
2845 .block_mouse_except_scroll()
2846 .child(EndTrialUpsell::new(Arc::new({
2847 let this = cx.entity();
2848 move |_, cx| {
2849 this.update(cx, |_this, cx| {
2850 TrialEndUpsell::set_dismissed(true, cx);
2851 cx.notify();
2852 });
2853 }
2854 }))),
2855 )
2856 }
2857
2858 fn emit_configuration_error_telemetry_if_needed(
2859 &mut self,
2860 configuration_error: Option<&ConfigurationError>,
2861 ) {
2862 let error_kind = configuration_error.map(|err| match err {
2863 ConfigurationError::NoProvider => "no_provider",
2864 ConfigurationError::ModelNotFound => "model_not_found",
2865 ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
2866 });
2867
2868 let error_kind_string = error_kind.map(String::from);
2869
2870 if self.last_configuration_error_telemetry == error_kind_string {
2871 return;
2872 }
2873
2874 self.last_configuration_error_telemetry = error_kind_string;
2875
2876 if let Some(kind) = error_kind {
2877 let message = configuration_error
2878 .map(|err| err.to_string())
2879 .unwrap_or_default();
2880
2881 telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
2882 }
2883 }
2884
2885 fn render_configuration_error(
2886 &self,
2887 border_bottom: bool,
2888 configuration_error: &ConfigurationError,
2889 focus_handle: &FocusHandle,
2890 cx: &mut App,
2891 ) -> impl IntoElement {
2892 let zed_provider_configured = AgentSettings::get_global(cx)
2893 .default_model
2894 .as_ref()
2895 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2896
2897 let callout = if zed_provider_configured {
2898 Callout::new()
2899 .icon(IconName::Warning)
2900 .severity(Severity::Warning)
2901 .when(border_bottom, |this| {
2902 this.border_position(ui::BorderPosition::Bottom)
2903 })
2904 .title("Sign in to continue using Zed as your LLM provider.")
2905 .actions_slot(
2906 Button::new("sign_in", "Sign In")
2907 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2908 .label_size(LabelSize::Small)
2909 .on_click({
2910 let workspace = self.workspace.clone();
2911 move |_, _, cx| {
2912 let Ok(client) =
2913 workspace.update(cx, |workspace, _| workspace.client().clone())
2914 else {
2915 return;
2916 };
2917
2918 cx.spawn(async move |cx| {
2919 client.sign_in_with_optional_connect(true, cx).await
2920 })
2921 .detach_and_log_err(cx);
2922 }
2923 }),
2924 )
2925 } else {
2926 Callout::new()
2927 .icon(IconName::Warning)
2928 .severity(Severity::Warning)
2929 .when(border_bottom, |this| {
2930 this.border_position(ui::BorderPosition::Bottom)
2931 })
2932 .title(configuration_error.to_string())
2933 .actions_slot(
2934 Button::new("settings", "Configure")
2935 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2936 .label_size(LabelSize::Small)
2937 .key_binding(
2938 KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2939 .map(|kb| kb.size(rems_from_px(12.))),
2940 )
2941 .on_click(|_event, window, cx| {
2942 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2943 }),
2944 )
2945 };
2946
2947 match configuration_error {
2948 ConfigurationError::ModelNotFound
2949 | ConfigurationError::ProviderNotAuthenticated(_)
2950 | ConfigurationError::NoProvider => callout.into_any_element(),
2951 }
2952 }
2953
2954 fn render_text_thread(
2955 &self,
2956 text_thread_editor: &Entity<TextThreadEditor>,
2957 buffer_search_bar: &Entity<BufferSearchBar>,
2958 window: &mut Window,
2959 cx: &mut Context<Self>,
2960 ) -> Div {
2961 let mut registrar = buffer_search::DivRegistrar::new(
2962 |this, _, _cx| match &this.active_view {
2963 ActiveView::TextThread {
2964 buffer_search_bar, ..
2965 } => Some(buffer_search_bar.clone()),
2966 _ => None,
2967 },
2968 cx,
2969 );
2970 BufferSearchBar::register(&mut registrar);
2971 registrar
2972 .into_div()
2973 .size_full()
2974 .relative()
2975 .map(|parent| {
2976 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2977 if buffer_search_bar.is_dismissed() {
2978 return parent;
2979 }
2980 parent.child(
2981 div()
2982 .p(DynamicSpacing::Base08.rems(cx))
2983 .border_b_1()
2984 .border_color(cx.theme().colors().border_variant)
2985 .bg(cx.theme().colors().editor_background)
2986 .child(buffer_search_bar.render(window, cx)),
2987 )
2988 })
2989 })
2990 .child(text_thread_editor.clone())
2991 .child(self.render_drag_target(cx))
2992 }
2993
2994 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2995 let is_local = self.project.read(cx).is_local();
2996 div()
2997 .invisible()
2998 .absolute()
2999 .top_0()
3000 .right_0()
3001 .bottom_0()
3002 .left_0()
3003 .bg(cx.theme().colors().drop_target_background)
3004 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3005 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3006 .when(is_local, |this| {
3007 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3008 })
3009 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3010 let item = tab.pane.read(cx).item_for_index(tab.ix);
3011 let project_paths = item
3012 .and_then(|item| item.project_path(cx))
3013 .into_iter()
3014 .collect::<Vec<_>>();
3015 this.handle_drop(project_paths, vec![], window, cx);
3016 }))
3017 .on_drop(
3018 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3019 let project_paths = selection
3020 .items()
3021 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3022 .collect::<Vec<_>>();
3023 this.handle_drop(project_paths, vec![], window, cx);
3024 }),
3025 )
3026 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3027 let tasks = paths
3028 .paths()
3029 .iter()
3030 .map(|path| {
3031 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3032 })
3033 .collect::<Vec<_>>();
3034 cx.spawn_in(window, async move |this, cx| {
3035 let mut paths = vec![];
3036 let mut added_worktrees = vec![];
3037 let opened_paths = futures::future::join_all(tasks).await;
3038 for entry in opened_paths {
3039 if let Some((worktree, project_path)) = entry.log_err() {
3040 added_worktrees.push(worktree);
3041 paths.push(project_path);
3042 }
3043 }
3044 this.update_in(cx, |this, window, cx| {
3045 this.handle_drop(paths, added_worktrees, window, cx);
3046 })
3047 .ok();
3048 })
3049 .detach();
3050 }))
3051 }
3052
3053 fn handle_drop(
3054 &mut self,
3055 paths: Vec<ProjectPath>,
3056 added_worktrees: Vec<Entity<Worktree>>,
3057 window: &mut Window,
3058 cx: &mut Context<Self>,
3059 ) {
3060 match &self.active_view {
3061 ActiveView::AgentThread { server_view } => {
3062 server_view.update(cx, |thread_view, cx| {
3063 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3064 });
3065 }
3066 ActiveView::TextThread {
3067 text_thread_editor, ..
3068 } => {
3069 text_thread_editor.update(cx, |text_thread_editor, cx| {
3070 TextThreadEditor::insert_dragged_files(
3071 text_thread_editor,
3072 paths,
3073 added_worktrees,
3074 window,
3075 cx,
3076 );
3077 });
3078 }
3079 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3080 }
3081 }
3082
3083 fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
3084 if !self.show_trust_workspace_message {
3085 return None;
3086 }
3087
3088 let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
3089
3090 Some(
3091 Callout::new()
3092 .icon(IconName::Warning)
3093 .severity(Severity::Warning)
3094 .border_position(ui::BorderPosition::Bottom)
3095 .title("You're in Restricted Mode")
3096 .description(description)
3097 .actions_slot(
3098 Button::new("open-trust-modal", "Configure Project Trust")
3099 .label_size(LabelSize::Small)
3100 .style(ButtonStyle::Outlined)
3101 .on_click({
3102 cx.listener(move |this, _, window, cx| {
3103 this.workspace
3104 .update(cx, |workspace, cx| {
3105 workspace
3106 .show_worktree_trust_security_modal(true, window, cx)
3107 })
3108 .log_err();
3109 })
3110 }),
3111 ),
3112 )
3113 }
3114
3115 fn key_context(&self) -> KeyContext {
3116 let mut key_context = KeyContext::new_with_defaults();
3117 key_context.add("AgentPanel");
3118 match &self.active_view {
3119 ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
3120 ActiveView::TextThread { .. } => key_context.add("text_thread"),
3121 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3122 }
3123 key_context
3124 }
3125}
3126
3127impl Render for AgentPanel {
3128 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3129 // WARNING: Changes to this element hierarchy can have
3130 // non-obvious implications to the layout of children.
3131 //
3132 // If you need to change it, please confirm:
3133 // - The message editor expands (cmd-option-esc) correctly
3134 // - When expanded, the buttons at the bottom of the panel are displayed correctly
3135 // - Font size works as expected and can be changed with cmd-+/cmd-
3136 // - Scrolling in all views works as expected
3137 // - Files can be dropped into the panel
3138 let content = v_flex()
3139 .relative()
3140 .size_full()
3141 .justify_between()
3142 .key_context(self.key_context())
3143 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3144 this.new_thread(action, window, cx);
3145 }))
3146 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3147 this.open_history(window, cx);
3148 }))
3149 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3150 this.open_configuration(window, cx);
3151 }))
3152 .on_action(cx.listener(Self::open_active_thread_as_markdown))
3153 .on_action(cx.listener(Self::deploy_rules_library))
3154 .on_action(cx.listener(Self::go_back))
3155 .on_action(cx.listener(Self::toggle_navigation_menu))
3156 .on_action(cx.listener(Self::toggle_options_menu))
3157 .on_action(cx.listener(Self::increase_font_size))
3158 .on_action(cx.listener(Self::decrease_font_size))
3159 .on_action(cx.listener(Self::reset_font_size))
3160 .on_action(cx.listener(Self::toggle_zoom))
3161 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3162 if let Some(thread_view) = this.active_thread_view() {
3163 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3164 }
3165 }))
3166 .child(self.render_toolbar(window, cx))
3167 .children(self.render_workspace_trust_message(cx))
3168 .children(self.render_onboarding(window, cx))
3169 .map(|parent| {
3170 // Emit configuration error telemetry before entering the match to avoid borrow conflicts
3171 if matches!(&self.active_view, ActiveView::TextThread { .. }) {
3172 let model_registry = LanguageModelRegistry::read_global(cx);
3173 let configuration_error =
3174 model_registry.configuration_error(model_registry.default_model(), cx);
3175 self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
3176 }
3177
3178 match &self.active_view {
3179 ActiveView::Uninitialized => parent,
3180 ActiveView::AgentThread { server_view, .. } => parent
3181 .child(server_view.clone())
3182 .child(self.render_drag_target(cx)),
3183 ActiveView::History { kind } => match kind {
3184 HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
3185 HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
3186 },
3187 ActiveView::TextThread {
3188 text_thread_editor,
3189 buffer_search_bar,
3190 ..
3191 } => {
3192 let model_registry = LanguageModelRegistry::read_global(cx);
3193 let configuration_error =
3194 model_registry.configuration_error(model_registry.default_model(), cx);
3195
3196 parent
3197 .map(|this| {
3198 if !self.should_render_onboarding(cx)
3199 && let Some(err) = configuration_error.as_ref()
3200 {
3201 this.child(self.render_configuration_error(
3202 true,
3203 err,
3204 &self.focus_handle(cx),
3205 cx,
3206 ))
3207 } else {
3208 this
3209 }
3210 })
3211 .child(self.render_text_thread(
3212 text_thread_editor,
3213 buffer_search_bar,
3214 window,
3215 cx,
3216 ))
3217 }
3218 ActiveView::Configuration => parent.children(self.configuration.clone()),
3219 }
3220 })
3221 .children(self.render_trial_end_upsell(window, cx));
3222
3223 match self.active_view.which_font_size_used() {
3224 WhichFontSize::AgentFont => {
3225 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3226 .size_full()
3227 .child(content)
3228 .into_any()
3229 }
3230 _ => content.into_any(),
3231 }
3232 }
3233}
3234
3235struct PromptLibraryInlineAssist {
3236 workspace: WeakEntity<Workspace>,
3237}
3238
3239impl PromptLibraryInlineAssist {
3240 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3241 Self { workspace }
3242 }
3243}
3244
3245impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3246 fn assist(
3247 &self,
3248 prompt_editor: &Entity<Editor>,
3249 initial_prompt: Option<String>,
3250 window: &mut Window,
3251 cx: &mut Context<RulesLibrary>,
3252 ) {
3253 InlineAssistant::update_global(cx, |assistant, cx| {
3254 let Some(workspace) = self.workspace.upgrade() else {
3255 return;
3256 };
3257 let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3258 return;
3259 };
3260 let project = workspace.read(cx).project().downgrade();
3261 let panel = panel.read(cx);
3262 let thread_store = panel.thread_store().clone();
3263 let history = panel.history().downgrade();
3264 assistant.assist(
3265 prompt_editor,
3266 self.workspace.clone(),
3267 project,
3268 thread_store,
3269 None,
3270 history,
3271 initial_prompt,
3272 window,
3273 cx,
3274 );
3275 })
3276 }
3277
3278 fn focus_agent_panel(
3279 &self,
3280 workspace: &mut Workspace,
3281 window: &mut Window,
3282 cx: &mut Context<Workspace>,
3283 ) -> bool {
3284 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3285 }
3286}
3287
3288pub struct ConcreteAssistantPanelDelegate;
3289
3290impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3291 fn active_text_thread_editor(
3292 &self,
3293 workspace: &mut Workspace,
3294 _window: &mut Window,
3295 cx: &mut Context<Workspace>,
3296 ) -> Option<Entity<TextThreadEditor>> {
3297 let panel = workspace.panel::<AgentPanel>(cx)?;
3298 panel.read(cx).active_text_thread_editor()
3299 }
3300
3301 fn open_local_text_thread(
3302 &self,
3303 workspace: &mut Workspace,
3304 path: Arc<Path>,
3305 window: &mut Window,
3306 cx: &mut Context<Workspace>,
3307 ) -> Task<Result<()>> {
3308 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3309 return Task::ready(Err(anyhow!("Agent panel not found")));
3310 };
3311
3312 panel.update(cx, |panel, cx| {
3313 panel.open_saved_text_thread(path, window, cx)
3314 })
3315 }
3316
3317 fn open_remote_text_thread(
3318 &self,
3319 _workspace: &mut Workspace,
3320 _text_thread_id: assistant_text_thread::TextThreadId,
3321 _window: &mut Window,
3322 _cx: &mut Context<Workspace>,
3323 ) -> Task<Result<Entity<TextThreadEditor>>> {
3324 Task::ready(Err(anyhow!("opening remote context not implemented")))
3325 }
3326
3327 fn quote_selection(
3328 &self,
3329 workspace: &mut Workspace,
3330 selection_ranges: Vec<Range<Anchor>>,
3331 buffer: Entity<MultiBuffer>,
3332 window: &mut Window,
3333 cx: &mut Context<Workspace>,
3334 ) {
3335 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3336 return;
3337 };
3338
3339 if !panel.focus_handle(cx).contains_focused(window, cx) {
3340 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3341 }
3342
3343 panel.update(cx, |_, cx| {
3344 // Wait to create a new context until the workspace is no longer
3345 // being updated.
3346 cx.defer_in(window, move |panel, window, cx| {
3347 if let Some(thread_view) = panel.active_thread_view() {
3348 thread_view.update(cx, |thread_view, cx| {
3349 thread_view.insert_selections(window, cx);
3350 });
3351 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3352 let snapshot = buffer.read(cx).snapshot(cx);
3353 let selection_ranges = selection_ranges
3354 .into_iter()
3355 .map(|range| range.to_point(&snapshot))
3356 .collect::<Vec<_>>();
3357
3358 text_thread_editor.update(cx, |text_thread_editor, cx| {
3359 text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3360 });
3361 }
3362 });
3363 });
3364 }
3365
3366 fn quote_terminal_text(
3367 &self,
3368 workspace: &mut Workspace,
3369 text: String,
3370 window: &mut Window,
3371 cx: &mut Context<Workspace>,
3372 ) {
3373 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3374 return;
3375 };
3376
3377 if !panel.focus_handle(cx).contains_focused(window, cx) {
3378 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3379 }
3380
3381 panel.update(cx, |_, cx| {
3382 // Wait to create a new context until the workspace is no longer
3383 // being updated.
3384 cx.defer_in(window, move |panel, window, cx| {
3385 if let Some(thread_view) = panel.active_thread_view() {
3386 thread_view.update(cx, |thread_view, cx| {
3387 thread_view.insert_terminal_text(text, window, cx);
3388 });
3389 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3390 text_thread_editor.update(cx, |text_thread_editor, cx| {
3391 text_thread_editor.quote_terminal_text(text, window, cx)
3392 });
3393 }
3394 });
3395 });
3396 }
3397}
3398
3399struct OnboardingUpsell;
3400
3401impl Dismissable for OnboardingUpsell {
3402 const KEY: &'static str = "dismissed-trial-upsell";
3403}
3404
3405struct TrialEndUpsell;
3406
3407impl Dismissable for TrialEndUpsell {
3408 const KEY: &'static str = "dismissed-trial-end-upsell";
3409}
3410
3411/// Test-only helper methods
3412#[cfg(any(test, feature = "test-support"))]
3413impl AgentPanel {
3414 /// Opens an external thread using an arbitrary AgentServer.
3415 ///
3416 /// This is a test-only helper that allows visual tests and integration tests
3417 /// to inject a stub server without modifying production code paths.
3418 /// Not compiled into production builds.
3419 pub fn open_external_thread_with_server(
3420 &mut self,
3421 server: Rc<dyn AgentServer>,
3422 window: &mut Window,
3423 cx: &mut Context<Self>,
3424 ) {
3425 let workspace = self.workspace.clone();
3426 let project = self.project.clone();
3427
3428 let ext_agent = ExternalAgent::Custom {
3429 name: server.name(),
3430 };
3431
3432 self._external_thread(
3433 server, None, None, workspace, project, ext_agent, window, cx,
3434 );
3435 }
3436
3437 /// Returns the currently active thread view, if any.
3438 ///
3439 /// This is a test-only accessor that exposes the private `active_thread_view()`
3440 /// method for test assertions. Not compiled into production builds.
3441 pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3442 self.active_thread_view()
3443 }
3444}
3445
3446#[cfg(test)]
3447mod tests {
3448 use super::*;
3449 use crate::acp::thread_view::tests::{StubAgentServer, init_test};
3450 use assistant_text_thread::TextThreadStore;
3451 use feature_flags::FeatureFlagAppExt;
3452 use fs::FakeFs;
3453 use gpui::{TestAppContext, VisualTestContext};
3454 use project::Project;
3455 use workspace::MultiWorkspace;
3456
3457 #[gpui::test]
3458 async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
3459 init_test(cx);
3460 cx.update(|cx| {
3461 cx.update_flags(true, vec!["agent-v2".to_string()]);
3462 agent::ThreadStore::init_global(cx);
3463 language_model::LanguageModelRegistry::test(cx);
3464 });
3465
3466 // --- Create a MultiWorkspace window with two workspaces ---
3467 let fs = FakeFs::new(cx.executor());
3468 let project_a = Project::test(fs.clone(), [], cx).await;
3469 let project_b = Project::test(fs, [], cx).await;
3470
3471 let multi_workspace =
3472 cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
3473
3474 let workspace_a = multi_workspace
3475 .read_with(cx, |multi_workspace, _cx| {
3476 multi_workspace.workspace().clone()
3477 })
3478 .unwrap();
3479
3480 let workspace_b = multi_workspace
3481 .update(cx, |multi_workspace, window, cx| {
3482 multi_workspace.test_add_workspace(project_b.clone(), window, cx)
3483 })
3484 .unwrap();
3485
3486 workspace_a.update(cx, |workspace, _cx| {
3487 workspace.set_random_database_id();
3488 });
3489 workspace_b.update(cx, |workspace, _cx| {
3490 workspace.set_random_database_id();
3491 });
3492
3493 let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
3494
3495 // --- Set up workspace A: width=300, with an active thread ---
3496 let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
3497 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
3498 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
3499 });
3500
3501 panel_a.update(cx, |panel, _cx| {
3502 panel.width = Some(px(300.0));
3503 });
3504
3505 panel_a.update_in(cx, |panel, window, cx| {
3506 panel.open_external_thread_with_server(
3507 Rc::new(StubAgentServer::default_response()),
3508 window,
3509 cx,
3510 );
3511 });
3512
3513 cx.run_until_parked();
3514
3515 panel_a.read_with(cx, |panel, cx| {
3516 assert!(
3517 panel.active_agent_thread(cx).is_some(),
3518 "workspace A should have an active thread after connection"
3519 );
3520 });
3521
3522 let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
3523
3524 // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
3525 let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
3526 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
3527 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
3528 });
3529
3530 panel_b.update(cx, |panel, _cx| {
3531 panel.width = Some(px(400.0));
3532 panel.selected_agent = AgentType::ClaudeAgent;
3533 });
3534
3535 // --- Serialize both panels ---
3536 panel_a.update(cx, |panel, cx| panel.serialize(cx));
3537 panel_b.update(cx, |panel, cx| panel.serialize(cx));
3538 cx.run_until_parked();
3539
3540 // --- Load fresh panels for each workspace and verify independent state ---
3541 let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
3542
3543 let async_cx = cx.update(|window, cx| window.to_async(cx));
3544 let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
3545 .await
3546 .expect("panel A load should succeed");
3547 cx.run_until_parked();
3548
3549 let async_cx = cx.update(|window, cx| window.to_async(cx));
3550 let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
3551 .await
3552 .expect("panel B load should succeed");
3553 cx.run_until_parked();
3554
3555 // Workspace A should restore width and agent type, but the thread
3556 // should NOT be restored because the stub agent never persisted it
3557 // to the database (the load-side validation skips missing threads).
3558 loaded_a.read_with(cx, |panel, _cx| {
3559 assert_eq!(
3560 panel.width,
3561 Some(px(300.0)),
3562 "workspace A width should be restored"
3563 );
3564 assert_eq!(
3565 panel.selected_agent, agent_type_a,
3566 "workspace A agent type should be restored"
3567 );
3568 });
3569
3570 // Workspace B should restore its own width and agent type, with no thread
3571 loaded_b.read_with(cx, |panel, _cx| {
3572 assert_eq!(
3573 panel.width,
3574 Some(px(400.0)),
3575 "workspace B width should be restored"
3576 );
3577 assert_eq!(
3578 panel.selected_agent,
3579 AgentType::ClaudeAgent,
3580 "workspace B agent type should be restored"
3581 );
3582 assert!(
3583 panel.active_thread_view().is_none(),
3584 "workspace B should have no active thread"
3585 );
3586 });
3587 }
3588
3589 // Simple regression test
3590 #[gpui::test]
3591 async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
3592 init_test(cx);
3593
3594 let fs = FakeFs::new(cx.executor());
3595
3596 cx.update(|cx| {
3597 cx.update_flags(true, vec!["agent-v2".to_string()]);
3598 agent::ThreadStore::init_global(cx);
3599 language_model::LanguageModelRegistry::test(cx);
3600 let slash_command_registry =
3601 assistant_slash_command::SlashCommandRegistry::default_global(cx);
3602 slash_command_registry
3603 .register_command(assistant_slash_commands::DefaultSlashCommand, false);
3604 <dyn fs::Fs>::set_global(fs.clone(), cx);
3605 });
3606
3607 let project = Project::test(fs.clone(), [], cx).await;
3608
3609 let multi_workspace =
3610 cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3611
3612 let workspace_a = multi_workspace
3613 .read_with(cx, |multi_workspace, _cx| {
3614 multi_workspace.workspace().clone()
3615 })
3616 .unwrap();
3617
3618 let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
3619
3620 workspace_a.update_in(cx, |workspace, window, cx| {
3621 let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
3622 let panel =
3623 cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
3624 workspace.add_panel(panel, window, cx);
3625 });
3626
3627 cx.run_until_parked();
3628
3629 workspace_a.update_in(cx, |_, window, cx| {
3630 window.dispatch_action(NewTextThread.boxed_clone(), cx);
3631 });
3632
3633 cx.run_until_parked();
3634 }
3635}