use std::{ cell::Cell, fmt, path::PathBuf, rc::Rc, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration,}; use acp_thread::{AcpThread, AcpThreadEvent, MentionUri, ThreadStatus, line_range_suffix};use agent::{ContextServerRegistry, SharedThread, ThreadStore};use agent_client_protocol::schema::v1 as acp;use agent_servers::AgentServer;use agent_settings::UserAgentsMd;use collections::HashSet;use db::kvp::{Dismissable, KeyValueStore};use itertools::Itertools;use project::{AgentId, ProjectItem};use serde::{Deserialize, Serialize}; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, agent::{ AddSelectionToThread, ConflictContent, LogoutAgent, OpenSettings, ReauthenticateAgent, ResetAgentZoom, ResetOnboarding, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, SelectAgent, }, assistant::{ FocusAgent, ManageSkills, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules, Toggle, ToggleFocus, },}; use crate::ExpandMessageEditor;use crate::ManageProfiles;use crate::agent_connection_store::AgentConnectionStore;use crate::completion_provider::{AgentContextSelection, AgentContextSource};use crate::terminal_thread_metadata_store::{ TerminalThreadMetadata, TerminalThreadMetadataStore, compose_terminal_thread_title, terminal_title_without_prefix,};use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent};use crate::{ Agent, AgentInitialContent, AgentThreadSource, ExternalSourcePrompt, NewExternalAgentThread, NewNativeAgentThreadFromSummary,};use crate::{ AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, LoadThreadFromClipboard, NewTerminalThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, ResetFastModeWarnings, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata, ToggleNewThreadMenu, ToggleOptionsMenu, conversation_view::{ AcpThreadViewEvent, RootThreadUpdated, ThreadView, reset_fast_mode_warnings, }, ui::{AgentNotification, AgentNotificationEvent, EndTrialUpsell},};use agent_settings::AgentSettings;use ai_onboarding::AgentPanelOnboarding;use anyhow::{Context as _, Result, anyhow};#[cfg(feature = "audio")]use audio::{Audio, Sound};use chrono::{DateTime, Utc};use client::UserStore;use cloud_api_types::Plan;use collections::HashMap;use editor::{Editor, MultiBuffer};use extension_host::ExtensionStore;use feature_flags::{CreateThreadToolFeatureFlag, FeatureFlagAppExt as _}; use fs::Fs;use futures::FutureExt as _;use gpui::{ Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, PlatformDisplay, Subscription, Task, TaskExt, WeakEntity, WindowHandle, prelude::*, pulsating_between,};use language::LanguageRegistry;use language_model::LanguageModelRegistry;use notifications::status_toast::StatusToast;use project::{Project, ProjectPath, Worktree};use settings::TerminalDockPosition;use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file}; use search::{BufferSearchBar, buffer_search::Deploy as DeployBufferSearch};use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings};use terminal_view::{TerminalView, terminal_panel::TerminalPanel};use text::OffsetRangeExt;use theme_settings::ThemeSettings;use ui::{ ContextMenu, ContextMenuEntry, GradientFade, IconButton, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, Tab, Tooltip, prelude::*, utils::WithRemSize,};use util::ResultExt as _;use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, MultiWorkspace, PathList, SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, WorkspaceId, dock::{DockPosition, Panel, PanelEvent}, item::{ItemEvent, ItemHandle},}; const AGENT_PANEL_KEY: &str = "agent_panel";const MIN_PANEL_WIDTH: Pixels = px(300.);const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";const LAST_CREATED_ENTRY_KIND_KEY: &str = "agent_panel__last_created_entry_kind";const TERMINAL_AGENT_TELEMETRY_ID: &str = "terminal";const TERMINAL_INIT_COMMAND_STARTUP_TIMEOUT: Duration = Duration::from_secs(5);const KNOWN_TERMINAL_AGENT_COMMANDS: &[&str] = &[ "agent", // Unfortunately, both Cursor cli + grok "agy", "aider", "amp", "claude", "codex", "copilot", "crush", "devin", "droid", "gemini", "goose", "grok", "openhands", "opencode", "pi", "qwen",]; fn is_known_terminal_agent_command(command: &str) -> bool { KNOWN_TERMINAL_AGENT_COMMANDS.contains(&command)} fn terminal_program_to_report( last_observed_program: &mut Option<String>, current_program: Option<String>,) -> Option<String> { let current_program = current_program.filter(|program| is_known_terminal_agent_command(program)); let program_to_report = if current_program.is_some() && current_program != *last_observed_program { current_program.clone() } else { None }; *last_observed_program = current_program; program_to_report} /// Maximum number of idle threads kept in the agent panel's retained list./// Set as a GPUI global to override; otherwise defaults to 5.pub struct MaxIdleRetainedThreads(pub usize);impl gpui::Global for MaxIdleRetainedThreads {} impl MaxIdleRetainedThreads { pub fn global(cx: &App) -> usize { cx.try_global::<Self>().map_or(5, |g| g.0) }} #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]pub struct TerminalId(uuid::Uuid); impl TerminalId { pub(crate) fn new() -> Self { Self(uuid::Uuid::new_v4()) } pub(crate) fn to_key_string(self) -> String { self.0.hyphenated().to_string() } pub(crate) fn from_key_string(key: &str) -> anyhow::Result<Self> { Ok(Self(uuid::Uuid::parse_str(key)?)) }} impl fmt::Display for TerminalId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(formatter) }} #[derive(Clone, Debug)]pub struct AgentPanelTerminalInfo { pub id: TerminalId, pub title: SharedString, pub created_at: DateTime<Utc>, pub has_notification: bool, pub custom_title: Option<SharedString>, pub working_directory: Option<PathBuf>,} #[derive(Serialize, Deserialize)]struct LastUsedAgent { agent: Agent,} #[derive(Serialize, Deserialize)]struct LastCreatedEntryKind { entry_kind: AgentPanelEntryKind,} struct SourcePanelInitialization { agent: Agent, initial_content: Option<AgentInitialContent>,} /// Reads the most recently used agent across all workspaces. Used as a fallback/// when opening a workspace that has no per-workspace agent preference yet.fn read_global_last_used_agent(kvp: &KeyValueStore) -> Option<Agent> { kvp.read_kvp(LAST_USED_AGENT_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<LastUsedAgent>(&json).log_err()) .map(|entry| entry.agent)} async fn write_global_last_used_agent(kvp: KeyValueStore, agent: Agent) { if let Some(json) = serde_json::to_string(&LastUsedAgent { agent }).log_err() { kvp.write_kvp(LAST_USED_AGENT_KEY.to_string(), json) .await .log_err(); }} fn read_global_last_created_entry_kind(kvp: &KeyValueStore) -> Option<AgentPanelEntryKind> { kvp.read_kvp(LAST_CREATED_ENTRY_KIND_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<LastCreatedEntryKind>(&json).log_err()) .map(|entry| entry.entry_kind)} fn project_agents_md_path( project: &Entity<Project>, require_existing_file: bool, cx: &App,) -> Option<PathBuf> { let rel_path = util::rel_path::RelPath::from_unix_str("AGENTS.md").ok()?; project .read(cx) .visible_worktrees(cx) .next() .and_then(|worktree| { let worktree = worktree.read(cx); if require_existing_file { let entry = worktree.entry_for_path(rel_path)?; if !entry.is_file() { return None; } } Some(worktree.absolutize(rel_path)) })} fn open_global_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) { workspace .open_abs_path( paths::agents_file().clone(), workspace::OpenOptions { focus: Some(true), ..Default::default() }, window, cx, ) .detach_and_log_err(cx);} fn open_project_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) { if let Some(path) = project_agents_md_path(workspace.project(), false, cx) { workspace .open_abs_path( path, workspace::OpenOptions { focus: Some(true), ..Default::default() }, window, cx, ) .detach_and_log_err(cx); }} async fn write_global_last_created_entry_kind(kvp: KeyValueStore, entry_kind: AgentPanelEntryKind) { if let Some(json) = serde_json::to_string(&LastCreatedEntryKind { entry_kind }).log_err() { kvp.write_kvp(LAST_CREATED_ENTRY_KIND_KEY.to_string(), json) .await .log_err(); }} fn read_serialized_panel( workspace_id: workspace::WorkspaceId, kvp: &KeyValueStore,) -> Option<SerializedAgentPanel> { let scope = kvp.scoped(AGENT_PANEL_KEY); let key = i64::from(workspace_id).to_string(); scope .read(&key) .log_err() .flatten() .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())} async fn save_serialized_panel( workspace_id: workspace::WorkspaceId, panel: SerializedAgentPanel, kvp: KeyValueStore,) -> Result<()> { let scope = kvp.scoped(AGENT_PANEL_KEY); let key = i64::from(workspace_id).to_string(); scope.write(key, serde_json::to_string(&panel)?).await?; Ok(())} /// Migration: reads the original single-panel format stored under the/// `"agent_panel"` KVP key before per-workspace keying was introduced.fn read_legacy_serialized_panel(kvp: &KeyValueStore) -> Option<SerializedAgentPanel> { kvp.read_kvp(AGENT_PANEL_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())} #[derive(Clone, Copy, Debug, Eq, PartialEq)]pub enum ThreadTitleRegenerationResult { NotOpen, Started, NoModel, AlreadyGenerating,} #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]enum AgentPanelEntryKind { #[default] Thread, Terminal,} #[derive(Serialize, Deserialize, Debug)]struct SerializedAgentPanel { selected_agent: Option<Agent>, #[serde(default)] last_created_entry_kind: AgentPanelEntryKind, #[serde(default)] last_active_thread: Option<SerializedActiveThread>, #[serde(default)] last_active_terminal_id: Option<String>, #[serde(default)] new_draft_thread_id: Option<ThreadId>,} #[derive(Serialize, Deserialize, Debug)]struct SerializedActiveThread { /// For drafts this is `None`; use `thread_id` to address them instead. session_id: Option<String>, /// Optional for back-compat with older serialized payloads that only carried `session_id`. #[serde(default)] thread_id: Option<ThreadId>, agent_type: Agent, title: Option<String>, work_dirs: Option<SerializedPathList>,} pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| { workspace .register_action(|workspace, _: &NewThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_thread_with_workspace(Some(workspace), window, cx) }); workspace.focus_panel::<AgentPanel>(window, cx); } }) .register_action(|workspace, _: &NewTerminalThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_terminal( Some(workspace), AgentThreadSource::AgentPanel, window, cx, ) }); workspace.focus_panel::<AgentPanel>(window, cx); } }) .register_action( |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_native_agent_thread_from_summary(action, window, cx) }); workspace.focus_panel::<AgentPanel>(window, cx); } }, ) .register_action(|workspace, _: &ExpandMessageEditor, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx)); } }) .register_action(|workspace, _: &OpenSettings, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); } }) .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.new_external_agent_thread(action, window, cx); }); } }) .register_action(|workspace, action: &SelectAgent, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { let agent = AgentId::new(action.agent.clone()).into(); panel.select_agent(agent, window, cx); }); } }) .register_action(|workspace, action: &ManageSkills, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.manage_skills(action, window, cx)); } }) .register_action(|workspace, _: &OpenGlobalAgentsMdRules, window, cx| { open_global_rules(workspace, window, cx); }) .register_action(|workspace, _: &OpenProjectAgentsMdRules, window, cx| { open_project_rules(workspace, window, cx); }) .register_action(|workspace, _: &Follow, window, cx| { workspace.follow(CollaboratorId::Agent, window, cx); }) .register_action(|workspace, _: &OpenAgentDiff, window, cx| { let thread = workspace .panel::<AgentPanel>(cx) .and_then(|panel| panel.read(cx).active_conversation_view().cloned()) .and_then(|conversation| { conversation .read(cx) .root_thread_view() .map(|r| r.read(cx).thread.clone()) }); if let Some(thread) = thread { AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } }) .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.toggle_options_menu(&ToggleOptionsMenu, window, cx); }); } }) .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); }); } }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); }) .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, _| { panel .new_user_onboarding_upsell_dismissed .store(false, Ordering::Release); }); } OnboardingUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetFastModeWarnings, _window, cx| { reset_fast_mode_warnings(cx); }) .register_action(|workspace, _: &ResetAgentZoom, window, cx| {Lines 1–500 of 13492. Load more
use std::{ cell::Cell, fmt, path::PathBuf, rc::Rc, sync::{ Arc, atomic::{AtomicBool, Ordering}, }, time::Duration, }; use acp_thread::{AcpThread, AcpThreadEvent, MentionUri, ThreadStatus, line_range_suffix}; use agent::{ContextServerRegistry, SharedThread, ThreadStore}; use agent_client_protocol::schema::v1 as acp; use agent_servers::AgentServer; use agent_settings::UserAgentsMd; use collections::HashSet; use db::kvp::{Dismissable, KeyValueStore}; use itertools::Itertools; use project::{AgentId, ProjectItem}; use serde::{Deserialize, Serialize}; use zed_actions::{ DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize, agent::{ AddSelectionToThread, ConflictContent, LogoutAgent, OpenSettings, ReauthenticateAgent, ResetAgentZoom, ResetOnboarding, ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, SelectAgent, }, assistant::{ FocusAgent, ManageSkills, OpenGlobalAgentsMdRules, OpenProjectAgentsMdRules, Toggle, ToggleFocus, }, }; use crate::ExpandMessageEditor; use crate::ManageProfiles; use crate::agent_connection_store::AgentConnectionStore; use crate::completion_provider::{AgentContextSelection, AgentContextSource}; use crate::terminal_thread_metadata_store::{ TerminalThreadMetadata, TerminalThreadMetadataStore, compose_terminal_thread_title, terminal_title_without_prefix, }; use crate::thread_metadata_store::{ThreadId, ThreadMetadataStore, ThreadMetadataStoreEvent}; use crate::{ Agent, AgentInitialContent, AgentThreadSource, ExternalSourcePrompt, NewExternalAgentThread, NewNativeAgentThreadFromSummary, }; use crate::{ AgentDiffPane, ConversationView, CopyThreadToClipboard, Follow, LoadThreadFromClipboard, NewTerminalThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, ResetFastModeWarnings, ResetTrialEndUpsell, ResetTrialUpsell, ShowAllSidebarThreadMetadata, ShowThreadMetadata, ToggleNewThreadMenu, ToggleOptionsMenu, conversation_view::{ AcpThreadViewEvent, RootThreadUpdated, ThreadView, reset_fast_mode_warnings, }, ui::{AgentNotification, AgentNotificationEvent, EndTrialUpsell}, }; use agent_settings::AgentSettings; use ai_onboarding::AgentPanelOnboarding; use anyhow::{Context as _, Result, anyhow}; #[cfg(feature = "audio")] use audio::{Audio, Sound}; use chrono::{DateTime, Utc}; use client::UserStore; use cloud_api_types::Plan; use collections::HashMap; use editor::{Editor, MultiBuffer}; use extension_host::ExtensionStore; use feature_flags::{CreateThreadToolFeatureFlag, FeatureFlagAppExt as _}; use fs::Fs; use futures::FutureExt as _; use gpui::{ Action, Anchor, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, PlatformDisplay, Subscription, Task, TaskExt, WeakEntity, WindowHandle, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::LanguageModelRegistry; use notifications::status_toast::StatusToast; use project::{Project, ProjectPath, Worktree}; use settings::TerminalDockPosition; use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file}; use search::{BufferSearchBar, buffer_search::Deploy as DeployBufferSearch}; use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings}; use terminal_view::{TerminalView, terminal_panel::TerminalPanel}; use text::OffsetRangeExt; use theme_settings::ThemeSettings; use ui::{ ContextMenu, ContextMenuEntry, GradientFade, IconButton, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, Tab, Tooltip, prelude::*, utils::WithRemSize, }; use util::ResultExt as _; use workspace::{ CollaboratorId, DraggedSelection, DraggedTab, MultiWorkspace, PathList, SerializedPathList, ToggleWorkspaceSidebar, ToggleZoom, ToolbarItemView, Workspace, WorkspaceId, dock::{DockPosition, Panel, PanelEvent}, item::{ItemEvent, ItemHandle}, }; const AGENT_PANEL_KEY: &str = "agent_panel"; const MIN_PANEL_WIDTH: Pixels = px(300.); const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent"; const LAST_CREATED_ENTRY_KIND_KEY: &str = "agent_panel__last_created_entry_kind"; const TERMINAL_AGENT_TELEMETRY_ID: &str = "terminal"; const TERMINAL_INIT_COMMAND_STARTUP_TIMEOUT: Duration = Duration::from_secs(5); const KNOWN_TERMINAL_AGENT_COMMANDS: &[&str] = &[ "agent", // Unfortunately, both Cursor cli + grok "agy", "aider", "amp", "claude", "codex", "copilot", "crush", "devin", "droid", "gemini", "goose", "grok", "openhands", "opencode", "pi", "qwen", ]; fn is_known_terminal_agent_command(command: &str) -> bool { KNOWN_TERMINAL_AGENT_COMMANDS.contains(&command) } fn terminal_program_to_report( last_observed_program: &mut Option<String>, current_program: Option<String>, ) -> Option<String> { let current_program = current_program.filter(|program| is_known_terminal_agent_command(program)); let program_to_report = if current_program.is_some() && current_program != *last_observed_program { current_program.clone() } else { None }; *last_observed_program = current_program; program_to_report } /// Maximum number of idle threads kept in the agent panel's retained list. /// Set as a GPUI global to override; otherwise defaults to 5. pub struct MaxIdleRetainedThreads(pub usize); impl gpui::Global for MaxIdleRetainedThreads {} impl MaxIdleRetainedThreads { pub fn global(cx: &App) -> usize { cx.try_global::<Self>().map_or(5, |g| g.0) } } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] pub struct TerminalId(uuid::Uuid); impl TerminalId { pub(crate) fn new() -> Self { Self(uuid::Uuid::new_v4()) } pub(crate) fn to_key_string(self) -> String { self.0.hyphenated().to_string() } pub(crate) fn from_key_string(key: &str) -> anyhow::Result<Self> { Ok(Self(uuid::Uuid::parse_str(key)?)) } } impl fmt::Display for TerminalId { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(formatter) } } #[derive(Clone, Debug)] pub struct AgentPanelTerminalInfo { pub id: TerminalId, pub title: SharedString, pub created_at: DateTime<Utc>, pub has_notification: bool, pub custom_title: Option<SharedString>, pub working_directory: Option<PathBuf>, } #[derive(Serialize, Deserialize)] struct LastUsedAgent { agent: Agent, } #[derive(Serialize, Deserialize)] struct LastCreatedEntryKind { entry_kind: AgentPanelEntryKind, } struct SourcePanelInitialization { agent: Agent, initial_content: Option<AgentInitialContent>, } /// Reads the most recently used agent across all workspaces. Used as a fallback /// when opening a workspace that has no per-workspace agent preference yet. fn read_global_last_used_agent(kvp: &KeyValueStore) -> Option<Agent> { kvp.read_kvp(LAST_USED_AGENT_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<LastUsedAgent>(&json).log_err()) .map(|entry| entry.agent) } async fn write_global_last_used_agent(kvp: KeyValueStore, agent: Agent) { if let Some(json) = serde_json::to_string(&LastUsedAgent { agent }).log_err() { kvp.write_kvp(LAST_USED_AGENT_KEY.to_string(), json) .await .log_err(); } } fn read_global_last_created_entry_kind(kvp: &KeyValueStore) -> Option<AgentPanelEntryKind> { kvp.read_kvp(LAST_CREATED_ENTRY_KIND_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<LastCreatedEntryKind>(&json).log_err()) .map(|entry| entry.entry_kind) } fn project_agents_md_path( project: &Entity<Project>, require_existing_file: bool, cx: &App, ) -> Option<PathBuf> { let rel_path = util::rel_path::RelPath::from_unix_str("AGENTS.md").ok()?; project .read(cx) .visible_worktrees(cx) .next() .and_then(|worktree| { let worktree = worktree.read(cx); if require_existing_file { let entry = worktree.entry_for_path(rel_path)?; if !entry.is_file() { return None; } } Some(worktree.absolutize(rel_path)) }) } fn open_global_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) { workspace .open_abs_path( paths::agents_file().clone(), workspace::OpenOptions { focus: Some(true), ..Default::default() }, window, cx, ) .detach_and_log_err(cx); } fn open_project_rules(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) { if let Some(path) = project_agents_md_path(workspace.project(), false, cx) { workspace .open_abs_path( path, workspace::OpenOptions { focus: Some(true), ..Default::default() }, window, cx, ) .detach_and_log_err(cx); } } async fn write_global_last_created_entry_kind(kvp: KeyValueStore, entry_kind: AgentPanelEntryKind) { if let Some(json) = serde_json::to_string(&LastCreatedEntryKind { entry_kind }).log_err() { kvp.write_kvp(LAST_CREATED_ENTRY_KIND_KEY.to_string(), json) .await .log_err(); } } fn read_serialized_panel( workspace_id: workspace::WorkspaceId, kvp: &KeyValueStore, ) -> Option<SerializedAgentPanel> { let scope = kvp.scoped(AGENT_PANEL_KEY); let key = i64::from(workspace_id).to_string(); scope .read(&key) .log_err() .flatten() .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err()) } async fn save_serialized_panel( workspace_id: workspace::WorkspaceId, panel: SerializedAgentPanel, kvp: KeyValueStore, ) -> Result<()> { let scope = kvp.scoped(AGENT_PANEL_KEY); let key = i64::from(workspace_id).to_string(); scope.write(key, serde_json::to_string(&panel)?).await?; Ok(()) } /// Migration: reads the original single-panel format stored under the /// `"agent_panel"` KVP key before per-workspace keying was introduced. fn read_legacy_serialized_panel(kvp: &KeyValueStore) -> Option<SerializedAgentPanel> { kvp.read_kvp(AGENT_PANEL_KEY) .log_err() .flatten() .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err()) } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ThreadTitleRegenerationResult { NotOpen, Started, NoModel, AlreadyGenerating, } #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] enum AgentPanelEntryKind { #[default] Thread, Terminal, } #[derive(Serialize, Deserialize, Debug)] struct SerializedAgentPanel { selected_agent: Option<Agent>, #[serde(default)] last_created_entry_kind: AgentPanelEntryKind, #[serde(default)] last_active_thread: Option<SerializedActiveThread>, #[serde(default)] last_active_terminal_id: Option<String>, #[serde(default)] new_draft_thread_id: Option<ThreadId>, } #[derive(Serialize, Deserialize, Debug)] struct SerializedActiveThread { /// For drafts this is `None`; use `thread_id` to address them instead. session_id: Option<String>, /// Optional for back-compat with older serialized payloads that only carried `session_id`. #[serde(default)] thread_id: Option<ThreadId>, agent_type: Agent, title: Option<String>, work_dirs: Option<SerializedPathList>, } pub fn init(cx: &mut App) { cx.observe_new( |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| { workspace .register_action(|workspace, _: &NewThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_thread_with_workspace(Some(workspace), window, cx) }); workspace.focus_panel::<AgentPanel>(window, cx); } }) .register_action(|workspace, _: &NewTerminalThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_terminal( Some(workspace), AgentThreadSource::AgentPanel, window, cx, ) }); workspace.focus_panel::<AgentPanel>(window, cx); } }) .register_action( |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { panel.new_native_agent_thread_from_summary(action, window, cx) }); workspace.focus_panel::<AgentPanel>(window, cx); } }, ) .register_action(|workspace, _: &ExpandMessageEditor, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx)); } }) .register_action(|workspace, _: &OpenSettings, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.open_configuration(window, cx)); } }) .register_action(|workspace, action: &NewExternalAgentThread, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.new_external_agent_thread(action, window, cx); }); } }) .register_action(|workspace, action: &SelectAgent, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, cx| { let agent = AgentId::new(action.agent.clone()).into(); panel.select_agent(agent, window, cx); }); } }) .register_action(|workspace, action: &ManageSkills, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| panel.manage_skills(action, window, cx)); } }) .register_action(|workspace, _: &OpenGlobalAgentsMdRules, window, cx| { open_global_rules(workspace, window, cx); }) .register_action(|workspace, _: &OpenProjectAgentsMdRules, window, cx| { open_project_rules(workspace, window, cx); }) .register_action(|workspace, _: &Follow, window, cx| { workspace.follow(CollaboratorId::Agent, window, cx); }) .register_action(|workspace, _: &OpenAgentDiff, window, cx| { let thread = workspace .panel::<AgentPanel>(cx) .and_then(|panel| panel.read(cx).active_conversation_view().cloned()) .and_then(|conversation| { conversation .read(cx) .root_thread_view() .map(|r| r.read(cx).thread.clone()) }); if let Some(thread) = thread { AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx); } }) .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.toggle_options_menu(&ToggleOptionsMenu, window, cx); }); } }) .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { workspace.focus_panel::<AgentPanel>(window, cx); panel.update(cx, |panel, cx| { panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx); }); } }) .register_action(|_workspace, _: &ResetOnboarding, window, cx| { window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx); window.refresh(); }) .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| { if let Some(panel) = workspace.panel::<AgentPanel>(cx) { panel.update(cx, |panel, _| { panel .new_user_onboarding_upsell_dismissed .store(false, Ordering::Release); }); } OnboardingUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| { TrialEndUpsell::set_dismissed(false, cx); }) .register_action(|_workspace, _: &ResetFastModeWarnings, _window, cx| { reset_fast_mode_warnings(cx); }) .register_action(|workspace, _: &ResetAgentZoom, window, cx| {