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