diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index cb5cef24c50f9f9ac637f3ac70adb24d37e56d61..5780eedb4445f613cbbd4e9a09976f2d475b28c7 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -258,7 +258,7 @@ "ctrl-shift-j": "agent::ToggleNavigationMenu", "ctrl-alt-i": "agent::ToggleOptionsMenu", "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu", - "ctrl-alt-shift-t": "agent::ToggleStartThreadInSelector", + "ctrl-shift-t": "agent::CycleStartThreadIn", "shift-alt-escape": "agent::ExpandMessageEditor", "ctrl->": "agent::AddSelectionToThread", "ctrl-shift-e": "project_panel::ToggleFocus", diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 08fb63868be875f41f6c461354b46f1081a2026f..6fc6905dd5f4502ff7ee90e7f6f9499b2e03fa6a 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -297,7 +297,7 @@ "cmd-shift-j": "agent::ToggleNavigationMenu", "cmd-alt-m": "agent::ToggleOptionsMenu", "cmd-alt-shift-n": "agent::ToggleNewThreadMenu", - "cmd-alt-shift-t": "agent::ToggleStartThreadInSelector", + "cmd-shift-t": "agent::CycleStartThreadIn", "shift-alt-escape": "agent::ExpandMessageEditor", "cmd->": "agent::AddSelectionToThread", "cmd-shift-e": "project_panel::ToggleFocus", diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 600025e2069978f3020afb5cb978d05a53317682..ac23d45695e11ec46172c566282ea65bf7774ac8 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -259,7 +259,7 @@ "shift-alt-j": "agent::ToggleNavigationMenu", "shift-alt-i": "agent::ToggleOptionsMenu", "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu", - "ctrl-shift-alt-t": "agent::ToggleStartThreadInSelector", + "ctrl-shift-t": "agent::CycleStartThreadIn", "shift-alt-escape": "agent::ExpandMessageEditor", "ctrl-shift-.": "agent::AddSelectionToThread", "ctrl-shift-e": "project_panel::ToggleFocus", diff --git a/crates/agent/src/tool_permissions.rs b/crates/agent/src/tool_permissions.rs index 79564bbddea7063d00e18d97c8eab89533b20da5..4cb4d265b3170429430b815d7490099a50678714 100644 --- a/crates/agent/src/tool_permissions.rs +++ b/crates/agent/src/tool_permissions.rs @@ -560,6 +560,7 @@ mod tests { message_editor_min_lines: 1, tool_permissions, show_turn_stats: false, + new_thread_location: Default::default(), } } diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 02341af42b9247ba07cb3f8c771a51626cd721ed..d5d4f16eb742a92f6abf8081c43709f161ef4038 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -12,7 +12,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::{ DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, - NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode, + NewThreadLocation, NotifyWhenAgentWaiting, RegisterSetting, Settings, ToolPermissionMode, }; pub use crate::agent_profile::*; @@ -51,6 +51,7 @@ pub struct AgentSettings { pub message_editor_min_lines: usize, pub show_turn_stats: bool, pub tool_permissions: ToolPermissions, + pub new_thread_location: NewThreadLocation, } impl AgentSettings { @@ -438,6 +439,7 @@ impl Settings for AgentSettings { message_editor_min_lines: agent.message_editor_min_lines.unwrap(), show_turn_stats: agent.show_turn_stats.unwrap(), tool_permissions: compile_tool_permissions(agent.tool_permissions), + new_thread_location: agent.new_thread_location.unwrap_or_default(), } } } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index f7c07abe5541187c7daf0dc037c00286c606f5bb..4fc6e3dd1f257377e3f5213b1ae216115fd01fff 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -29,12 +29,12 @@ use zed_actions::agent::{ ResolveConflictedFilesWithAgent, ResolveConflictsWithAgent, ReviewBranchDiff, }; -use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; +use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal, HoldForDefault}; use crate::{ - AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, Follow, - InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, - OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn, - ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, ToggleStartThreadInSelector, + AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, CycleStartThreadIn, + Follow, InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, + OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, + StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, connection_view::{AcpThreadViewEvent, ThreadView}, slash_command::SlashCommandCompletionProvider, @@ -312,18 +312,6 @@ pub fn init(cx: &mut App) { }); } }) - .register_action(|workspace, _: &ToggleStartThreadInSelector, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - workspace.focus_panel::(window, cx); - panel.update(cx, |panel, cx| { - panel.toggle_start_thread_in_selector( - &ToggleStartThreadInSelector, - window, - cx, - ); - }); - } - }) .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| { AcpOnboardingModal::toggle(workspace, window, cx) }) @@ -477,6 +465,13 @@ pub fn init(cx: &mut App) { }); } }) + .register_action(|workspace, _: &CycleStartThreadIn, _window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.cycle_start_thread_in(cx); + }); + } + }) .register_action(|workspace, _: &ToggleWorkspaceSidebar, window, cx| { if !multi_workspace_enabled(cx) { return; @@ -1751,15 +1746,6 @@ impl AgentPanel { self.new_thread_menu_handle.toggle(window, cx); } - pub fn toggle_start_thread_in_selector( - &mut self, - _: &ToggleStartThreadInSelector, - window: &mut Window, - cx: &mut Context, - ) { - self.start_thread_in_menu_handle.toggle(window, cx); - } - pub fn increase_font_size( &mut self, action: &IncreaseBufferFontSize, @@ -2388,6 +2374,28 @@ impl AgentPanel { cx.notify(); } + fn cycle_start_thread_in(&mut self, cx: &mut Context) { + let next = match self.start_thread_in { + StartThreadIn::LocalProject => StartThreadIn::NewWorktree, + StartThreadIn::NewWorktree => StartThreadIn::LocalProject, + }; + self.set_start_thread_in(&next, cx); + } + + fn reset_start_thread_in_to_default(&mut self, cx: &mut Context) { + use settings::{NewThreadLocation, Settings}; + let default = AgentSettings::get_global(cx).new_thread_location; + let start_thread_in = match default { + NewThreadLocation::LocalProject => StartThreadIn::LocalProject, + NewThreadLocation::NewWorktree => StartThreadIn::NewWorktree, + }; + if self.start_thread_in != start_thread_in { + self.start_thread_in = start_thread_in; + self.serialize(cx); + cx.notify(); + } + } + fn selected_external_agent(&self) -> Option { match &self.selected_agent { AgentType::NativeAgent => Some(Agent::NativeAgent), @@ -2445,6 +2453,7 @@ impl AgentPanel { window: &mut Window, cx: &mut Context, ) { + self.reset_start_thread_in_to_default(cx); self.new_agent_thread_inner(agent, true, window, cx); } @@ -3592,9 +3601,12 @@ impl AgentPanel { } fn render_start_thread_in_selector(&self, cx: &mut Context) -> impl IntoElement { + use settings::{NewThreadLocation, Settings}; + let focus_handle = self.focus_handle(cx); let has_git_repo = self.project_has_git_repository(cx); let is_via_collab = self.project.read(cx).is_via_collab(); + let fs = self.fs.clone(); let is_creating = matches!( self.worktree_creation_status, @@ -3604,6 +3616,10 @@ impl AgentPanel { let current_target = self.start_thread_in; let trigger_label = self.start_thread_in.label(); + let new_thread_location = AgentSettings::get_global(cx).new_thread_location; + let is_local_default = new_thread_location == NewThreadLocation::LocalProject; + let is_new_worktree_default = new_thread_location == NewThreadLocation::NewWorktree; + let icon = if self.start_thread_in_menu_handle.is_deployed() { IconName::ChevronUp } else { @@ -3631,7 +3647,7 @@ impl AgentPanel { move |_window, cx| { Tooltip::for_action_in( "Start Thread In…", - &ToggleStartThreadInSelector, + &CycleStartThreadIn, &focus_handle, cx, ) @@ -3640,6 +3656,7 @@ impl AgentPanel { .menu(move |window, cx| { let is_local_selected = current_target == StartThreadIn::LocalProject; let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree; + let fs = fs.clone(); Some(ContextMenu::build(window, cx, move |menu, _window, _cx| { let new_worktree_disabled = !has_git_repo || is_via_collab; @@ -3648,18 +3665,53 @@ impl AgentPanel { .item( ContextMenuEntry::new("Current Project") .toggleable(IconPosition::End, is_local_selected) - .handler(|window, cx| { - window - .dispatch_action(Box::new(StartThreadIn::LocalProject), cx); + .documentation_aside(documentation_side, move |_| { + HoldForDefault::new(is_local_default) + .more_content(false) + .into_any_element() + }) + .handler({ + let fs = fs.clone(); + move |window, cx| { + if window.modifiers().secondary() { + update_settings_file(fs.clone(), cx, |settings, _| { + settings + .agent + .get_or_insert_default() + .set_new_thread_location( + NewThreadLocation::LocalProject, + ); + }); + } + window.dispatch_action( + Box::new(StartThreadIn::LocalProject), + cx, + ); + } }), ) .item({ let entry = ContextMenuEntry::new("New Worktree") .toggleable(IconPosition::End, is_new_worktree_selected) .disabled(new_worktree_disabled) - .handler(|window, cx| { - window - .dispatch_action(Box::new(StartThreadIn::NewWorktree), cx); + .handler({ + let fs = fs.clone(); + move |window, cx| { + if window.modifiers().secondary() { + update_settings_file(fs.clone(), cx, |settings, _| { + settings + .agent + .get_or_insert_default() + .set_new_thread_location( + NewThreadLocation::NewWorktree, + ); + }); + } + window.dispatch_action( + Box::new(StartThreadIn::NewWorktree), + cx, + ); + } }); if new_worktree_disabled { @@ -3675,7 +3727,11 @@ impl AgentPanel { .into_any_element() }) } else { - entry + entry.documentation_aside(documentation_side, move |_| { + HoldForDefault::new(is_new_worktree_default) + .more_content(false) + .into_any_element() + }) } }) })) @@ -4849,7 +4905,6 @@ impl Render for AgentPanel { .on_action(cx.listener(Self::go_back)) .on_action(cx.listener(Self::toggle_navigation_menu)) .on_action(cx.listener(Self::toggle_options_menu)) - .on_action(cx.listener(Self::toggle_start_thread_in_selector)) .on_action(cx.listener(Self::increase_font_size)) .on_action(cx.listener(Self::decrease_font_size)) .on_action(cx.listener(Self::reset_font_size)) diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index fbf47615cb23b75eaeff1f785ada8bf8605556d3..ea70d155b79e190dcfe9138b620aff4415b6d935 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -86,8 +86,8 @@ actions!( NewTextThread, /// Toggles the menu to create new agent threads. ToggleNewThreadMenu, - /// Toggles the selector for choosing where new threads start (current project or new worktree). - ToggleStartThreadInSelector, + /// Cycles through the options for where new threads start (current project or new worktree). + CycleStartThreadIn, /// Toggles the navigation menu for switching between threads and views. ToggleNavigationMenu, /// Toggles the options menu for agent settings and preferences. @@ -655,6 +655,7 @@ mod tests { message_editor_min_lines: 1, tool_permissions: Default::default(), show_turn_stats: false, + new_thread_location: Default::default(), }; cx.update(|cx| { diff --git a/crates/agent_ui/src/ui/hold_for_default.rs b/crates/agent_ui/src/ui/hold_for_default.rs index 1972f5de4d38fd5ba47ff91709be6ded302b61ae..436ca65ddd93b977a09c8de8eaeb25dc6c0eb1a0 100644 --- a/crates/agent_ui/src/ui/hold_for_default.rs +++ b/crates/agent_ui/src/ui/hold_for_default.rs @@ -4,20 +4,31 @@ use ui::{prelude::*, render_modifiers}; #[derive(IntoElement)] pub struct HoldForDefault { is_default: bool, + more_content: bool, } impl HoldForDefault { pub fn new(is_default: bool) -> Self { - Self { is_default } + Self { + is_default, + more_content: true, + } + } + + pub fn more_content(mut self, more_content: bool) -> Self { + self.more_content = more_content; + self } } impl RenderOnce for HoldForDefault { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { h_flex() - .pt_1() - .border_t_1() - .border_color(cx.theme().colors().border_variant) + .when(self.more_content, |this| { + this.pt_1() + .border_t_1() + .border_color(cx.theme().colors().border_variant) + }) .gap_0p5() .text_sm() .text_color(Color::Muted.color(cx)) diff --git a/crates/settings_content/src/agent.rs b/crates/settings_content/src/agent.rs index 87e117b8b0bbdd9a789bae18c3f9dce98a6f1bc0..8061e591b0a3f81e8b8081a0b363c112fb388ce4 100644 --- a/crates/settings_content/src/agent.rs +++ b/crates/settings_content/src/agent.rs @@ -9,6 +9,19 @@ use crate::ExtendingVec; use crate::DockPosition; +/// Where new threads should start by default. +#[derive( + Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, +)] +#[serde(rename_all = "snake_case")] +pub enum NewThreadLocation { + /// Start threads in the current project. + #[default] + LocalProject, + /// Start threads in a new worktree. + NewWorktree, +} + #[with_fallible_options] #[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] pub struct AgentSettingsContent { @@ -59,6 +72,10 @@ pub struct AgentSettingsContent { /// /// Default: "thread" pub default_view: Option, + /// Where new threads should start by default. + /// + /// Default: "local_project" + pub new_thread_location: Option, /// The available agent profiles. pub profiles: Option, AgentProfileContent>>, /// Where to show a popup notification when the agent is waiting for user input. @@ -146,6 +163,10 @@ impl AgentSettingsContent { self.default_profile = Some(profile_id); } + pub fn set_new_thread_location(&mut self, value: NewThreadLocation) { + self.new_thread_location = Some(value); + } + pub fn add_favorite_model(&mut self, model: LanguageModelSelection) { if !self.favorite_models.contains(&model) { self.favorite_models.push(model); diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs index dbac4d7ba350fcff07016a2ccfa483f3d84472c7..708840668d7502ae0c34e9f1751fd7b76da2ca07 100644 --- a/crates/settings_ui/src/page_data.rs +++ b/crates/settings_ui/src/page_data.rs @@ -6972,7 +6972,7 @@ fn ai_page() -> SettingsPage { ] } - fn agent_configuration_section() -> [SettingsPageItem; 12] { + fn agent_configuration_section() -> [SettingsPageItem; 13] { [ SettingsPageItem::SectionHeader("Agent Configuration"), SettingsPageItem::SubPageLink(SubPageLink { @@ -6984,6 +6984,28 @@ fn ai_page() -> SettingsPage { files: USER, render: render_tool_permissions_setup_page, }), + SettingsPageItem::SettingItem(SettingItem { + title: "New Thread Location", + description: "Whether to start a new thread in the current local project or in a new Git worktree.", + field: Box::new(SettingField { + json_path: Some("agent.default_start_thread_in"), + pick: |settings_content| { + settings_content + .agent + .as_ref()? + .new_thread_location + .as_ref() + }, + write: |settings_content, value| { + settings_content + .agent + .get_or_insert_default() + .new_thread_location = value; + }, + }), + metadata: None, + files: USER, + }), SettingsPageItem::SettingItem(SettingItem { title: "Single File Review", description: "When enabled, agent edits will also be displayed in single-file buffers for review.",