Kick off agent v2 (#44190)
🔜 TODO: - [x] Add a utility pane to the left and right edges of the workspace - [x] Add a maximize button to the left and right side of the pane - [x] Add a new agents pane - [x] Add a feature flag turning these off POV: You're working agentically <img width="354" height="606" alt="Screenshot 2025-12-13 at 11 50 14 PM" src="https://github.com/user-attachments/assets/ce5469f9-adc2-47f5-a978-a48bf992f5f7" /> Release Notes: - N/A --------- Co-authored-by: Nathan Sobo <nathan@zed.dev> Co-authored-by: Zed <zed@zed.dev>
This commit is contained in:
28 files changed
+2452
-98
No files matched your search
Generated
+33
@@ -406,6 +406,37 @@ dependencies = [
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent_ui_v2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agent",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
"assistant_text_thread",
|
||||
"chrono",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"menu",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
"time",
|
||||
"time_format",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -20059,6 +20090,7 @@ dependencies = [
|
||||
"component",
|
||||
"dap",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
@@ -20475,6 +20507,7 @@ dependencies = [
|
||||
"activity_indicator",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"agent_ui_v2",
|
||||
"anyhow",
|
||||
"ashpd 0.11.0",
|
||||
"askpass",
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"crates/agent_servers",
|
||||
"crates/agent_settings",
|
||||
"crates/agent_ui",
|
||||
"crates/agent_ui_v2",
|
||||
"crates/ai_onboarding",
|
||||
"crates/anthropic",
|
||||
"crates/askpass",
|
||||
@@ -242,6 +243,7 @@ action_log = { path = "crates/action_log" }
|
||||
agent = { path = "crates/agent" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_ui_v2 = { path = "crates/agent_ui_v2" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
agent_servers = { path = "crates/agent_servers" }
|
||||
ai_onboarding = { path = "crates/ai_onboarding" }
|
||||
|
||||
@@ -906,6 +906,8 @@
|
||||
"button": true,
|
||||
// Where to dock the agent panel. Can be 'left', 'right' or 'bottom'.
|
||||
"dock": "right",
|
||||
// Where to dock the agents panel. Can be 'left' or 'right'.
|
||||
"agents_panel_dock": "left",
|
||||
// Default width when the agent panel is docked to the left or right.
|
||||
"default_width": 640,
|
||||
// Default height when the agent panel is docked to the bottom.
|
||||
|
||||
@@ -9,7 +9,7 @@ use project::DisableAiSettings;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection,
|
||||
DefaultAgentView, DockPosition, DockSide, LanguageModelParameters, LanguageModelSelection,
|
||||
NotifyWhenAgentWaiting, RegisterSetting, Settings,
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ pub struct AgentSettings {
|
||||
pub enabled: bool,
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub agents_panel_dock: DockSide,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
@@ -152,6 +153,7 @@ impl Settings for AgentSettings {
|
||||
enabled: agent.enabled.unwrap(),
|
||||
button: agent.button.unwrap(),
|
||||
dock: agent.dock.unwrap(),
|
||||
agents_panel_dock: agent.agents_panel_dock.unwrap(),
|
||||
default_width: px(agent.default_width.unwrap()),
|
||||
default_height: px(agent.default_height.unwrap()),
|
||||
default_model: Some(agent.default_model.unwrap()),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod acp;
|
||||
pub mod acp;
|
||||
mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
@@ -26,7 +26,7 @@ use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use language::{
|
||||
@@ -244,11 +244,17 @@ pub fn init(
|
||||
update_command_palette_filter(app_cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.on_flags_ready(|_, cx| {
|
||||
update_command_palette_filter(cx);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn update_command_palette_filter(cx: &mut App) {
|
||||
let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
|
||||
let agent_enabled = AgentSettings::get_global(cx).enabled;
|
||||
let agent_v2_enabled = cx.has_flag::<AgentV2FeatureFlag>();
|
||||
let edit_prediction_provider = AllLanguageSettings::get_global(cx)
|
||||
.edit_predictions
|
||||
.provider;
|
||||
@@ -269,6 +275,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
|
||||
if disable_ai {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("agents");
|
||||
filter.hide_namespace("assistant");
|
||||
filter.hide_namespace("copilot");
|
||||
filter.hide_namespace("supermaven");
|
||||
@@ -280,8 +287,10 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
} else {
|
||||
if agent_enabled {
|
||||
filter.show_namespace("agent");
|
||||
filter.show_namespace("agents");
|
||||
} else {
|
||||
filter.hide_namespace("agent");
|
||||
filter.hide_namespace("agents");
|
||||
}
|
||||
|
||||
filter.show_namespace("assistant");
|
||||
@@ -317,6 +326,9 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
|
||||
filter.show_namespace("zed_predict_onboarding");
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
if !agent_v2_enabled {
|
||||
filter.hide_action_types(&[TypeId::of::<zed_actions::agent::ToggleAgentPane>()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -415,7 +427,7 @@ mod tests {
|
||||
use gpui::{BorrowAppContext, TestAppContext, px};
|
||||
use project::DisableAiSettings;
|
||||
use settings::{
|
||||
DefaultAgentView, DockPosition, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
DefaultAgentView, DockPosition, DockSide, NotifyWhenAgentWaiting, Settings, SettingsStore,
|
||||
};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -434,6 +446,7 @@ mod tests {
|
||||
enabled: true,
|
||||
button: true,
|
||||
dock: DockPosition::Right,
|
||||
agents_panel_dock: DockSide::Left,
|
||||
default_width: px(300.),
|
||||
default_height: px(600.),
|
||||
default_model: None,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
[package]
|
||||
name = "agent_ui_v2"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/agent_ui_v2.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
agent.workspace = true
|
||||
agent_servers.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_ui.workspace = true
|
||||
anyhow.workspace = true
|
||||
assistant_text_thread.workspace = true
|
||||
chrono.workspace = true
|
||||
db.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
text.workspace = true
|
||||
time.workspace = true
|
||||
time_format.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
@@ -0,0 +1,290 @@
|
||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use agent_servers::AgentServer;
|
||||
use agent_settings::AgentSettings;
|
||||
use agent_ui::acp::AcpThreadView;
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Entity, EventEmitter, Focusable, Pixels, SharedString, Subscription, WeakEntity, prelude::*,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::PromptStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::DockSide;
|
||||
use settings::Settings as _;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use ui::{
|
||||
App, Clickable as _, Context, DynamicSpacing, IconButton, IconName, IconSize, IntoElement,
|
||||
Label, LabelCommon as _, LabelSize, Render, Tab, Window, div,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
use workspace::dock::{ClosePane, MinimizePane, UtilityPane, UtilityPanePosition};
|
||||
use workspace::utility_pane::UtilityPaneSlot;
|
||||
|
||||
pub const DEFAULT_UTILITY_PANE_WIDTH: Pixels = gpui::px(400.0);
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub enum SerializedHistoryEntryId {
|
||||
AcpThread(String),
|
||||
TextThread(String),
|
||||
}
|
||||
|
||||
impl From<HistoryEntryId> for SerializedHistoryEntryId {
|
||||
fn from(id: HistoryEntryId) -> Self {
|
||||
match id {
|
||||
HistoryEntryId::AcpThread(session_id) => {
|
||||
SerializedHistoryEntryId::AcpThread(session_id.0.to_string())
|
||||
}
|
||||
HistoryEntryId::TextThread(path) => {
|
||||
SerializedHistoryEntryId::TextThread(path.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SerializedAgentThreadPane {
|
||||
pub expanded: bool,
|
||||
pub width: Option<Pixels>,
|
||||
pub thread_id: Option<SerializedHistoryEntryId>,
|
||||
}
|
||||
|
||||
pub enum AgentsUtilityPaneEvent {
|
||||
StateChanged,
|
||||
}
|
||||
|
||||
impl EventEmitter<AgentsUtilityPaneEvent> for AgentThreadPane {}
|
||||
impl EventEmitter<MinimizePane> for AgentThreadPane {}
|
||||
impl EventEmitter<ClosePane> for AgentThreadPane {}
|
||||
|
||||
struct ActiveThreadView {
|
||||
view: Entity<AcpThreadView>,
|
||||
thread_id: HistoryEntryId,
|
||||
_notify: Subscription,
|
||||
}
|
||||
|
||||
pub struct AgentThreadPane {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
expanded: bool,
|
||||
width: Option<Pixels>,
|
||||
thread_view: Option<ActiveThreadView>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
}
|
||||
|
||||
impl AgentThreadPane {
|
||||
pub fn new(workspace: WeakEntity<Workspace>, cx: &mut ui::Context<Self>) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
Self {
|
||||
focus_handle,
|
||||
expanded: false,
|
||||
width: None,
|
||||
thread_view: None,
|
||||
workspace,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn thread_id(&self) -> Option<HistoryEntryId> {
|
||||
self.thread_view.as_ref().map(|tv| tv.thread_id.clone())
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> SerializedAgentThreadPane {
|
||||
SerializedAgentThreadPane {
|
||||
expanded: self.expanded,
|
||||
width: self.width,
|
||||
thread_id: self.thread_id().map(SerializedHistoryEntryId::from),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_thread(
|
||||
&mut self,
|
||||
entry: HistoryEntry,
|
||||
fs: Arc<dyn Fs>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let thread_id = entry.id();
|
||||
|
||||
let resume_thread = match &entry {
|
||||
HistoryEntry::AcpThread(thread) => Some(thread.clone()),
|
||||
HistoryEntry::TextThread(_) => None,
|
||||
};
|
||||
|
||||
let agent: Rc<dyn AgentServer> = Rc::new(NativeAgentServer::new(fs, history_store.clone()));
|
||||
|
||||
let thread_view = cx.new(|cx| {
|
||||
AcpThreadView::new(
|
||||
agent,
|
||||
resume_thread,
|
||||
None,
|
||||
workspace,
|
||||
project,
|
||||
history_store,
|
||||
prompt_store,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let notify = cx.observe(&thread_view, |_, _, cx| {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
self.thread_view = Some(ActiveThreadView {
|
||||
view: thread_view,
|
||||
thread_id,
|
||||
_notify: notify,
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn title(&self, cx: &App) -> SharedString {
|
||||
if let Some(active_thread_view) = &self.thread_view {
|
||||
let thread_view = active_thread_view.view.read(cx);
|
||||
if let Some(thread) = thread_view.thread() {
|
||||
let title = thread.read(cx).title();
|
||||
if !title.is_empty() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
thread_view.title(cx)
|
||||
} else {
|
||||
"Thread".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let position = self.position(window, cx);
|
||||
let slot = match position {
|
||||
UtilityPanePosition::Left => UtilityPaneSlot::Left,
|
||||
UtilityPanePosition::Right => UtilityPaneSlot::Right,
|
||||
};
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let toggle_icon = self.toggle_icon(cx);
|
||||
let title = self.title(cx);
|
||||
|
||||
let make_toggle_button = |workspace: WeakEntity<Workspace>, cx: &App| {
|
||||
div().px(DynamicSpacing::Base06.rems(cx)).child(
|
||||
IconButton::new("toggle_utility_pane", toggle_icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_utility_pane(slot, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let make_close_button = |id: &'static str, cx: &mut Context<Self>| {
|
||||
let on_click = cx.listener(|this, _: &gpui::ClickEvent, _window, cx| {
|
||||
cx.emit(ClosePane);
|
||||
this.thread_view = None;
|
||||
cx.notify();
|
||||
});
|
||||
div().px(DynamicSpacing::Base06.rems(cx)).child(
|
||||
IconButton::new(id, IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(on_click),
|
||||
)
|
||||
};
|
||||
|
||||
let make_title_label = |title: SharedString, cx: &App| {
|
||||
div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.child(Label::new(title).size(LabelSize::Small))
|
||||
};
|
||||
|
||||
div()
|
||||
.id("utility-pane-header")
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.w_full()
|
||||
.h(Tab::container_height(cx))
|
||||
.when(slot == UtilityPaneSlot::Left, |this| {
|
||||
this.child(make_toggle_button(workspace.clone(), cx))
|
||||
.child(make_title_label(title.clone(), cx))
|
||||
.child(div().flex_grow())
|
||||
.child(make_close_button("close_utility_pane_left", cx))
|
||||
})
|
||||
.when(slot == UtilityPaneSlot::Right, |this| {
|
||||
this.child(make_close_button("close_utility_pane_right", cx))
|
||||
.child(make_title_label(title.clone(), cx))
|
||||
.child(div().flex_grow())
|
||||
.child(make_toggle_button(workspace.clone(), cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AgentThreadPane {
|
||||
fn focus_handle(&self, cx: &ui::App) -> gpui::FocusHandle {
|
||||
if let Some(thread_view) = &self.thread_view {
|
||||
thread_view.view.focus_handle(cx)
|
||||
} else {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl UtilityPane for AgentThreadPane {
|
||||
fn position(&self, _window: &Window, cx: &App) -> UtilityPanePosition {
|
||||
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||
DockSide::Left => UtilityPanePosition::Left,
|
||||
DockSide::Right => UtilityPanePosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_icon(&self, _cx: &App) -> IconName {
|
||||
IconName::Thread
|
||||
}
|
||||
|
||||
fn expanded(&self, _cx: &App) -> bool {
|
||||
self.expanded
|
||||
}
|
||||
|
||||
fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>) {
|
||||
self.expanded = expanded;
|
||||
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn width(&self, _cx: &App) -> Pixels {
|
||||
self.width.unwrap_or(DEFAULT_UTILITY_PANE_WIDTH)
|
||||
}
|
||||
|
||||
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
|
||||
self.width = width;
|
||||
cx.emit(AgentsUtilityPaneEvent::StateChanged);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentThreadPane {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let content = if let Some(thread_view) = &self.thread_view {
|
||||
div().size_full().child(thread_view.view.clone())
|
||||
} else {
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.child(Label::new("Select a thread to view details").size(LabelSize::Default))
|
||||
};
|
||||
|
||||
div()
|
||||
.size_full()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(self.render_header(window, cx))
|
||||
.child(content)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
mod agent_thread_pane;
|
||||
mod thread_history;
|
||||
|
||||
pub mod agents_panel;
|
||||
@@ -0,0 +1,438 @@
|
||||
use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AsyncWindowContext, Entity, EventEmitter, Focusable, Pixels, Subscription, Task,
|
||||
WeakEntity, actions, prelude::*,
|
||||
};
|
||||
use project::Project;
|
||||
use prompt_store::{PromptBuilder, PromptStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings as _, update_settings_file};
|
||||
use std::sync::Arc;
|
||||
use ui::{App, Context, IconName, IntoElement, ParentElement, Render, Styled, Window};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Panel, Workspace,
|
||||
dock::{ClosePane, DockPosition, PanelEvent, UtilityPane},
|
||||
utility_pane::{UtilityPaneSlot, utility_slot_for_dock_position},
|
||||
};
|
||||
|
||||
use crate::agent_thread_pane::{
|
||||
AgentThreadPane, AgentsUtilityPaneEvent, SerializedAgentThreadPane, SerializedHistoryEntryId,
|
||||
};
|
||||
use crate::thread_history::{AcpThreadHistory, ThreadHistoryEvent};
|
||||
|
||||
const AGENTS_PANEL_KEY: &str = "agents_panel";
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
struct SerializedAgentsPanel {
|
||||
width: Option<Pixels>,
|
||||
pane: Option<SerializedAgentThreadPane>,
|
||||
}
|
||||
|
||||
actions!(
|
||||
agents,
|
||||
[
|
||||
/// Toggle the visibility of the agents panel.
|
||||
ToggleAgentsPanel
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &ToggleAgentsPanel, window, cx| {
|
||||
workspace.toggle_panel_focus::<AgentsPanel>(window, cx);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct AgentsPanel {
|
||||
focus_handle: gpui::FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
agent_thread_pane: Option<Entity<AgentThreadPane>>,
|
||||
history: Entity<AcpThreadHistory>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
fs: Arc<dyn Fs>,
|
||||
width: Option<Pixels>,
|
||||
pending_serialization: Task<Option<()>>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl AgentsPanel {
|
||||
pub fn load(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: AsyncWindowContext,
|
||||
) -> Task<Result<Entity<Self>, anyhow::Error>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let serialized_panel = cx
|
||||
.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.read_kvp(AGENTS_PANEL_KEY)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|panel| {
|
||||
serde_json::from_str::<SerializedAgentsPanel>(&panel).ok()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
let (fs, project, prompt_builder) = workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let project = workspace.project().clone();
|
||||
let prompt_builder = PromptBuilder::load(fs.clone(), false, cx);
|
||||
(fs, project, prompt_builder)
|
||||
})?;
|
||||
|
||||
let text_thread_store = workspace
|
||||
.update(cx, |_, cx| {
|
||||
TextThreadStore::new(
|
||||
project.clone(),
|
||||
prompt_builder.clone(),
|
||||
Default::default(),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let prompt_store = workspace
|
||||
.update(cx, |_, cx| PromptStore::global(cx))?
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
workspace.update_in(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
let mut panel = Self::new(
|
||||
workspace.clone(),
|
||||
fs,
|
||||
project,
|
||||
prompt_store,
|
||||
text_thread_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(serialized_panel) = serialized_panel {
|
||||
panel.width = serialized_panel.width;
|
||||
if let Some(serialized_pane) = serialized_panel.pane {
|
||||
panel.restore_utility_pane(serialized_pane, window, cx);
|
||||
}
|
||||
}
|
||||
panel
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn new(
|
||||
workspace: WeakEntity<Workspace>,
|
||||
fs: Arc<dyn Fs>,
|
||||
project: Entity<Project>,
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
|
||||
let history = cx.new(|cx| AcpThreadHistory::new(history_store.clone(), window, cx));
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&history, window, Self::handle_history_event),
|
||||
cx.on_flags_ready(move |_, cx| {
|
||||
this.update(cx, |_, cx| {
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
];
|
||||
|
||||
Self {
|
||||
focus_handle,
|
||||
workspace,
|
||||
project,
|
||||
agent_thread_pane: None,
|
||||
history,
|
||||
history_store,
|
||||
prompt_store,
|
||||
fs,
|
||||
width: None,
|
||||
pending_serialization: Task::ready(None),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_utility_pane(
|
||||
&mut self,
|
||||
serialized_pane: SerializedAgentThreadPane,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(thread_id) = &serialized_pane.thread_id else {
|
||||
return;
|
||||
};
|
||||
|
||||
let entry = self
|
||||
.history_store
|
||||
.read(cx)
|
||||
.entries()
|
||||
.find(|e| match (&e.id(), thread_id) {
|
||||
(
|
||||
HistoryEntryId::AcpThread(session_id),
|
||||
SerializedHistoryEntryId::AcpThread(id),
|
||||
) => session_id.to_string() == *id,
|
||||
(HistoryEntryId::TextThread(path), SerializedHistoryEntryId::TextThread(id)) => {
|
||||
path.to_string_lossy() == *id
|
||||
}
|
||||
_ => false,
|
||||
});
|
||||
|
||||
if let Some(entry) = entry {
|
||||
self.open_thread(
|
||||
entry,
|
||||
serialized_pane.expanded,
|
||||
serialized_pane.width,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_utility_pane_event(
|
||||
&mut self,
|
||||
_utility_pane: Entity<AgentThreadPane>,
|
||||
event: &AgentsUtilityPaneEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
AgentsUtilityPaneEvent::StateChanged => {
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_close_pane_event(
|
||||
&mut self,
|
||||
_utility_pane: Entity<AgentThreadPane>,
|
||||
_event: &ClosePane,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.agent_thread_pane = None;
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_history_event(
|
||||
&mut self,
|
||||
_history: &Entity<AcpThreadHistory>,
|
||||
event: &ThreadHistoryEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThreadHistoryEvent::Open(entry) => {
|
||||
self.open_thread(entry.clone(), true, None, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_thread(
|
||||
&mut self,
|
||||
entry: HistoryEntry,
|
||||
expanded: bool,
|
||||
width: Option<Pixels>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let entry_id = entry.id();
|
||||
|
||||
if let Some(existing_pane) = &self.agent_thread_pane {
|
||||
if existing_pane.read(cx).thread_id() == Some(entry_id) {
|
||||
existing_pane.update(cx, |pane, cx| {
|
||||
pane.set_expanded(true, cx);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let fs = self.fs.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let history_store = self.history_store.clone();
|
||||
let prompt_store = self.prompt_store.clone();
|
||||
|
||||
let agent_thread_pane = cx.new(|cx| {
|
||||
let mut pane = AgentThreadPane::new(workspace.clone(), cx);
|
||||
pane.open_thread(
|
||||
entry,
|
||||
fs,
|
||||
workspace.clone(),
|
||||
project,
|
||||
history_store,
|
||||
prompt_store,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
if let Some(width) = width {
|
||||
pane.set_width(Some(width), cx);
|
||||
}
|
||||
pane.set_expanded(expanded, cx);
|
||||
pane
|
||||
});
|
||||
|
||||
let state_subscription = cx.subscribe(&agent_thread_pane, Self::handle_utility_pane_event);
|
||||
let close_subscription = cx.subscribe(&agent_thread_pane, Self::handle_close_pane_event);
|
||||
|
||||
self._subscriptions.push(state_subscription);
|
||||
self._subscriptions.push(close_subscription);
|
||||
|
||||
let slot = self.utility_slot(window, cx);
|
||||
let panel_id = cx.entity_id();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.register_utility_pane(slot, panel_id, agent_thread_pane.clone(), cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.agent_thread_pane = Some(agent_thread_pane);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn utility_slot(&self, window: &Window, cx: &App) -> UtilityPaneSlot {
|
||||
let position = self.position(window, cx);
|
||||
utility_slot_for_dock_position(position)
|
||||
}
|
||||
|
||||
fn re_register_utility_pane(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(pane) = &self.agent_thread_pane {
|
||||
let slot = self.utility_slot(window, cx);
|
||||
let panel_id = cx.entity_id();
|
||||
let pane = pane.clone();
|
||||
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.register_utility_pane(slot, panel_id, pane, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize(&mut self, cx: &mut Context<Self>) {
|
||||
let width = self.width;
|
||||
let pane = self
|
||||
.agent_thread_pane
|
||||
.as_ref()
|
||||
.map(|pane| pane.read(cx).serialize());
|
||||
|
||||
self.pending_serialization = cx.background_spawn(async move {
|
||||
KEY_VALUE_STORE
|
||||
.write_kvp(
|
||||
AGENTS_PANEL_KEY.into(),
|
||||
serde_json::to_string(&SerializedAgentsPanel { width, pane }).unwrap(),
|
||||
)
|
||||
.await
|
||||
.log_err()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for AgentsPanel {}
|
||||
|
||||
impl Focusable for AgentsPanel {
|
||||
fn focus_handle(&self, _cx: &ui::App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Panel for AgentsPanel {
|
||||
fn persistent_name() -> &'static str {
|
||||
"AgentsPanel"
|
||||
}
|
||||
|
||||
fn panel_key() -> &'static str {
|
||||
AGENTS_PANEL_KEY
|
||||
}
|
||||
|
||||
fn position(&self, _window: &Window, cx: &App) -> DockPosition {
|
||||
match AgentSettings::get_global(cx).agents_panel_dock {
|
||||
settings::DockSide::Left => DockPosition::Left,
|
||||
settings::DockSide::Right => DockPosition::Right,
|
||||
}
|
||||
}
|
||||
|
||||
fn position_is_valid(&self, position: DockPosition) -> bool {
|
||||
position != DockPosition::Bottom
|
||||
}
|
||||
|
||||
fn set_position(
|
||||
&mut self,
|
||||
position: DockPosition,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
update_settings_file(self.fs.clone(), cx, move |settings, _| {
|
||||
settings.agent.get_or_insert_default().agents_panel_dock = Some(match position {
|
||||
DockPosition::Left => settings::DockSide::Left,
|
||||
DockPosition::Bottom => settings::DockSide::Right,
|
||||
DockPosition::Right => settings::DockSide::Left,
|
||||
});
|
||||
});
|
||||
self.re_register_utility_pane(window, cx);
|
||||
}
|
||||
|
||||
fn size(&self, window: &Window, cx: &App) -> Pixels {
|
||||
let settings = AgentSettings::get_global(cx);
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => {
|
||||
self.width.unwrap_or(settings.default_width)
|
||||
}
|
||||
DockPosition::Bottom => self.width.unwrap_or(settings.default_height),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.position(window, cx) {
|
||||
DockPosition::Left | DockPosition::Right => self.width = size,
|
||||
DockPosition::Bottom => {}
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
|
||||
(self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAgent)
|
||||
}
|
||||
|
||||
fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
|
||||
Some("Agents Panel")
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleAgentsPanel)
|
||||
}
|
||||
|
||||
fn activation_priority(&self) -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
fn enabled(&self, cx: &App) -> bool {
|
||||
AgentSettings::get_global(cx).enabled(cx) && cx.has_flag::<AgentV2FeatureFlag>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentsPanel {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
gpui::div().size_full().child(self.history.clone())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,735 @@
|
||||
use agent::{HistoryEntry, HistoryStore};
|
||||
use chrono::{Datelike as _, Local, NaiveDate, TimeDelta};
|
||||
use editor::{Editor, EditorEvent};
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task,
|
||||
UniformListScrollHandle, Window, actions, uniform_list,
|
||||
};
|
||||
use std::{fmt::Display, ops::Range};
|
||||
use text::Bias;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
actions!(
|
||||
agents,
|
||||
[
|
||||
/// Removes all thread history.
|
||||
RemoveHistory,
|
||||
/// Removes the currently selected thread.
|
||||
RemoveSelectedThread,
|
||||
]
|
||||
);
|
||||
|
||||
pub struct AcpThreadHistory {
|
||||
pub(crate) history_store: Entity<HistoryStore>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
selected_index: usize,
|
||||
hovered_index: Option<usize>,
|
||||
search_editor: Entity<Editor>,
|
||||
search_query: SharedString,
|
||||
visible_items: Vec<ListItemType>,
|
||||
local_timezone: UtcOffset,
|
||||
confirming_delete_history: bool,
|
||||
_update_task: Task<()>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
enum ListItemType {
|
||||
BucketSeparator(TimeBucket),
|
||||
Entry {
|
||||
entry: HistoryEntry,
|
||||
format: EntryTimeFormat,
|
||||
},
|
||||
SearchResult {
|
||||
entry: HistoryEntry,
|
||||
positions: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ListItemType {
|
||||
fn history_entry(&self) -> Option<&HistoryEntry> {
|
||||
match self {
|
||||
ListItemType::Entry { entry, .. } => Some(entry),
|
||||
ListItemType::SearchResult { entry, .. } => Some(entry),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum ThreadHistoryEvent {
|
||||
Open(HistoryEntry),
|
||||
}
|
||||
|
||||
impl EventEmitter<ThreadHistoryEvent> for AcpThreadHistory {}
|
||||
|
||||
impl AcpThreadHistory {
|
||||
pub fn new(
|
||||
history_store: Entity<agent::HistoryStore>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let search_editor = cx.new(|cx| {
|
||||
let mut editor = Editor::single_line(window, cx);
|
||||
editor.set_placeholder_text("Search threads...", window, cx);
|
||||
editor
|
||||
});
|
||||
|
||||
let search_editor_subscription =
|
||||
cx.subscribe(&search_editor, |this, search_editor, event, cx| {
|
||||
if let EditorEvent::BufferEdited = event {
|
||||
let query = search_editor.read(cx).text(cx);
|
||||
if this.search_query != query {
|
||||
this.search_query = query.into();
|
||||
this.update_visible_items(false, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let history_store_subscription = cx.observe(&history_store, |this, _, cx| {
|
||||
this.update_visible_items(true, cx);
|
||||
});
|
||||
|
||||
let scroll_handle = UniformListScrollHandle::default();
|
||||
|
||||
let mut this = Self {
|
||||
history_store,
|
||||
scroll_handle,
|
||||
selected_index: 0,
|
||||
hovered_index: None,
|
||||
visible_items: Default::default(),
|
||||
search_editor,
|
||||
local_timezone: UtcOffset::from_whole_seconds(
|
||||
chrono::Local::now().offset().local_minus_utc(),
|
||||
)
|
||||
.unwrap(),
|
||||
search_query: SharedString::default(),
|
||||
confirming_delete_history: false,
|
||||
_subscriptions: vec![search_editor_subscription, history_store_subscription],
|
||||
_update_task: Task::ready(()),
|
||||
};
|
||||
this.update_visible_items(false, cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context<Self>) {
|
||||
let entries = self
|
||||
.history_store
|
||||
.update(cx, |store, _| store.entries().collect());
|
||||
let new_list_items = if self.search_query.is_empty() {
|
||||
self.add_list_separators(entries, cx)
|
||||
} else {
|
||||
self.filter_search_results(entries, cx)
|
||||
};
|
||||
let selected_history_entry = if preserve_selected_item {
|
||||
self.selected_history_entry().cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self._update_task = cx.spawn(async move |this, cx| {
|
||||
let new_visible_items = new_list_items.await;
|
||||
this.update(cx, |this, cx| {
|
||||
let new_selected_index = if let Some(history_entry) = selected_history_entry {
|
||||
let history_entry_id = history_entry.id();
|
||||
new_visible_items
|
||||
.iter()
|
||||
.position(|visible_entry| {
|
||||
visible_entry
|
||||
.history_entry()
|
||||
.is_some_and(|entry| entry.id() == history_entry_id)
|
||||
})
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
this.visible_items = new_visible_items;
|
||||
this.set_selected_index(new_selected_index, Bias::Right, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
}
|
||||
|
||||
fn add_list_separators(&self, entries: Vec<HistoryEntry>, cx: &App) -> Task<Vec<ListItemType>> {
|
||||
cx.background_spawn(async move {
|
||||
let mut items = Vec::with_capacity(entries.len() + 1);
|
||||
let mut bucket = None;
|
||||
let today = Local::now().naive_local().date();
|
||||
|
||||
for entry in entries.into_iter() {
|
||||
let entry_date = entry
|
||||
.updated_at()
|
||||
.with_timezone(&Local)
|
||||
.naive_local()
|
||||
.date();
|
||||
let entry_bucket = TimeBucket::from_dates(today, entry_date);
|
||||
|
||||
if Some(entry_bucket) != bucket {
|
||||
bucket = Some(entry_bucket);
|
||||
items.push(ListItemType::BucketSeparator(entry_bucket));
|
||||
}
|
||||
|
||||
items.push(ListItemType::Entry {
|
||||
entry,
|
||||
format: entry_bucket.into(),
|
||||
});
|
||||
}
|
||||
items
|
||||
})
|
||||
}
|
||||
|
||||
fn filter_search_results(
|
||||
&self,
|
||||
entries: Vec<HistoryEntry>,
|
||||
cx: &App,
|
||||
) -> Task<Vec<ListItemType>> {
|
||||
let query = self.search_query.clone();
|
||||
cx.background_spawn({
|
||||
let executor = cx.background_executor().clone();
|
||||
async move {
|
||||
let mut candidates = Vec::with_capacity(entries.len());
|
||||
|
||||
for (idx, entry) in entries.iter().enumerate() {
|
||||
candidates.push(StringMatchCandidate::new(idx, entry.title()));
|
||||
}
|
||||
|
||||
const MAX_MATCHES: usize = 100;
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
true,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|search_match| ListItemType::SearchResult {
|
||||
entry: entries[search_match.candidate_id].clone(),
|
||||
positions: search_match.positions,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn search_produced_no_matches(&self) -> bool {
|
||||
self.visible_items.is_empty() && !self.search_query.is_empty()
|
||||
}
|
||||
|
||||
fn selected_history_entry(&self) -> Option<&HistoryEntry> {
|
||||
self.get_history_entry(self.selected_index)
|
||||
}
|
||||
|
||||
fn get_history_entry(&self, visible_items_ix: usize) -> Option<&HistoryEntry> {
|
||||
self.visible_items.get(visible_items_ix)?.history_entry()
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, mut index: usize, bias: Bias, cx: &mut Context<Self>) {
|
||||
if self.visible_items.is_empty() {
|
||||
self.selected_index = 0;
|
||||
return;
|
||||
}
|
||||
while matches!(
|
||||
self.visible_items.get(index),
|
||||
None | Some(ListItemType::BucketSeparator(..))
|
||||
) {
|
||||
index = match bias {
|
||||
Bias::Left => {
|
||||
if index == 0 {
|
||||
self.visible_items.len() - 1
|
||||
} else {
|
||||
index - 1
|
||||
}
|
||||
}
|
||||
Bias::Right => {
|
||||
if index >= self.visible_items.len() - 1 {
|
||||
0
|
||||
} else {
|
||||
index + 1
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
self.selected_index = index;
|
||||
self.scroll_handle
|
||||
.scroll_to_item(index, ScrollStrategy::Top);
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
pub fn select_previous(
|
||||
&mut self,
|
||||
_: &menu::SelectPrevious,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_index == 0 {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index - 1, Bias::Left, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn select_next(
|
||||
&mut self,
|
||||
_: &menu::SelectNext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.selected_index == self.visible_items.len() - 1 {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
} else {
|
||||
self.set_selected_index(self.selected_index + 1, Bias::Right, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_first(
|
||||
&mut self,
|
||||
_: &menu::SelectFirst,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.set_selected_index(0, Bias::Right, cx);
|
||||
}
|
||||
|
||||
fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.set_selected_index(self.visible_items.len() - 1, Bias::Left, cx);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirm_entry(self.selected_index, cx);
|
||||
}
|
||||
|
||||
fn confirm_entry(&mut self, ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(ix) else {
|
||||
return;
|
||||
};
|
||||
cx.emit(ThreadHistoryEvent::Open(entry.clone()));
|
||||
}
|
||||
|
||||
fn remove_selected_thread(
|
||||
&mut self,
|
||||
_: &RemoveSelectedThread,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.remove_thread(self.selected_index, cx)
|
||||
}
|
||||
|
||||
fn remove_thread(&mut self, visible_item_ix: usize, cx: &mut Context<Self>) {
|
||||
let Some(entry) = self.get_history_entry(visible_item_ix) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let task = match entry {
|
||||
HistoryEntry::AcpThread(thread) => self
|
||||
.history_store
|
||||
.update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)),
|
||||
HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| {
|
||||
this.delete_text_thread(text_thread.path.clone(), cx)
|
||||
}),
|
||||
};
|
||||
task.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn remove_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
store.delete_threads(cx).detach_and_log_err(cx)
|
||||
});
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = true;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.confirming_delete_history = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_list_items(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AnyElement> {
|
||||
self.visible_items
|
||||
.get(range.clone())
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.enumerate()
|
||||
.map(|(ix, item)| self.render_list_item(item, range.start + ix, cx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_list_item(&self, item: &ListItemType, ix: usize, cx: &Context<Self>) -> AnyElement {
|
||||
match item {
|
||||
ListItemType::Entry { entry, format } => self
|
||||
.render_history_entry(entry, *format, ix, Vec::default(), cx)
|
||||
.into_any(),
|
||||
ListItemType::SearchResult { entry, positions } => self.render_history_entry(
|
||||
entry,
|
||||
EntryTimeFormat::DateAndTime,
|
||||
ix,
|
||||
positions.clone(),
|
||||
cx,
|
||||
),
|
||||
ListItemType::BucketSeparator(bucket) => div()
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.pt_2()
|
||||
.pb_1()
|
||||
.child(
|
||||
Label::new(bucket.to_string())
|
||||
.size(LabelSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_history_entry(
|
||||
&self,
|
||||
entry: &HistoryEntry,
|
||||
format: EntryTimeFormat,
|
||||
ix: usize,
|
||||
highlight_positions: Vec<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let selected = ix == self.selected_index;
|
||||
let hovered = Some(ix) == self.hovered_index;
|
||||
let timestamp = entry.updated_at().timestamp();
|
||||
let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone);
|
||||
|
||||
h_flex()
|
||||
.w_full()
|
||||
.pb_1()
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.rounded()
|
||||
.toggle_state(selected)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
HighlightedLabel::new(entry.title(), highlight_positions)
|
||||
.size(LabelSize::Small)
|
||||
.truncate(),
|
||||
)
|
||||
.child(
|
||||
Label::new(thread_timestamp)
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::XSmall),
|
||||
),
|
||||
)
|
||||
.on_hover(cx.listener(move |this, is_hovered, _window, cx| {
|
||||
if *is_hovered {
|
||||
this.hovered_index = Some(ix);
|
||||
} else if this.hovered_index == Some(ix) {
|
||||
this.hovered_index = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}))
|
||||
.end_slot::<IconButton>(if hovered {
|
||||
Some(
|
||||
IconButton::new("delete", IconName::Trash)
|
||||
.shape(IconButtonShape::Square)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| {
|
||||
this.remove_thread(ix, cx);
|
||||
cx.stop_propagation()
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for AcpThreadHistory {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.search_editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AcpThreadHistory {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_no_history = self.history_store.read(cx).is_empty(cx);
|
||||
|
||||
v_flex()
|
||||
.key_context("ThreadHistory")
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().panel_background)
|
||||
.on_action(cx.listener(Self::select_previous))
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
.on_action(cx.listener(Self::select_first))
|
||||
.on_action(cx.listener(Self::select_last))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.on_action(cx.listener(Self::remove_selected_thread))
|
||||
.on_action(cx.listener(|this, _: &RemoveHistory, window, cx| {
|
||||
this.remove_history(window, cx);
|
||||
}))
|
||||
.child(
|
||||
h_flex()
|
||||
.h(Tab::container_height(cx))
|
||||
.w_full()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.child(self.search_editor.clone()),
|
||||
)
|
||||
.child({
|
||||
let view = v_flex()
|
||||
.id("list-container")
|
||||
.relative()
|
||||
.overflow_hidden()
|
||||
.flex_grow();
|
||||
|
||||
if has_no_history {
|
||||
view.justify_center().items_center().child(
|
||||
Label::new("You don't have any past threads yet.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
} else if self.search_produced_no_matches() {
|
||||
view.justify_center()
|
||||
.items_center()
|
||||
.child(Label::new("No threads match your search.").size(LabelSize::Small))
|
||||
} else {
|
||||
view.child(
|
||||
uniform_list(
|
||||
"thread-history",
|
||||
self.visible_items.len(),
|
||||
cx.processor(|this, range: Range<usize>, window, cx| {
|
||||
this.render_list_items(range, window, cx)
|
||||
}),
|
||||
)
|
||||
.p_1()
|
||||
.pr_4()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.flex_grow(),
|
||||
)
|
||||
.vertical_scrollbar_for(&self.scroll_handle, window, cx)
|
||||
}
|
||||
})
|
||||
.when(!has_no_history, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.when(!self.confirming_delete_history, |this| {
|
||||
this.child(
|
||||
Button::new("delete_history", "Delete All History")
|
||||
.full_width()
|
||||
.style(ButtonStyle::Outlined)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.prompt_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
})
|
||||
.when(self.confirming_delete_history, |this| {
|
||||
this.w_full()
|
||||
.gap_2()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_wrap()
|
||||
.gap_1()
|
||||
.child(
|
||||
Label::new("Delete all threads?")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(
|
||||
Label::new("You won't be able to recover them later.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
Button::new("cancel_delete", "Cancel")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.cancel_delete_history(window, cx);
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
Button::new("confirm_delete", "Delete")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Error))
|
||||
.color(Color::Error)
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|_, _, window, cx| {
|
||||
window.dispatch_action(
|
||||
Box::new(RemoveHistory),
|
||||
cx,
|
||||
);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum EntryTimeFormat {
|
||||
DateAndTime,
|
||||
TimeOnly,
|
||||
}
|
||||
|
||||
impl EntryTimeFormat {
|
||||
fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String {
|
||||
let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap();
|
||||
|
||||
match self {
|
||||
EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp(
|
||||
timestamp,
|
||||
OffsetDateTime::now_utc(),
|
||||
timezone,
|
||||
time_format::TimestampFormat::EnhancedAbsolute,
|
||||
),
|
||||
EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TimeBucket> for EntryTimeFormat {
|
||||
fn from(bucket: TimeBucket) -> Self {
|
||||
match bucket {
|
||||
TimeBucket::Today => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::Yesterday => EntryTimeFormat::TimeOnly,
|
||||
TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::PastWeek => EntryTimeFormat::DateAndTime,
|
||||
TimeBucket::All => EntryTimeFormat::DateAndTime,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
|
||||
enum TimeBucket {
|
||||
Today,
|
||||
Yesterday,
|
||||
ThisWeek,
|
||||
PastWeek,
|
||||
All,
|
||||
}
|
||||
|
||||
impl TimeBucket {
|
||||
fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self {
|
||||
if date == reference {
|
||||
return TimeBucket::Today;
|
||||
}
|
||||
|
||||
if date == reference - TimeDelta::days(1) {
|
||||
return TimeBucket::Yesterday;
|
||||
}
|
||||
|
||||
let week = date.iso_week();
|
||||
|
||||
if reference.iso_week() == week {
|
||||
return TimeBucket::ThisWeek;
|
||||
}
|
||||
|
||||
let last_week = (reference - TimeDelta::days(7)).iso_week();
|
||||
|
||||
if week == last_week {
|
||||
return TimeBucket::PastWeek;
|
||||
}
|
||||
|
||||
TimeBucket::All
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for TimeBucket {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
TimeBucket::Today => write!(f, "Today"),
|
||||
TimeBucket::Yesterday => write!(f, "Yesterday"),
|
||||
TimeBucket::ThisWeek => write!(f, "This Week"),
|
||||
TimeBucket::PastWeek => write!(f, "Past Week"),
|
||||
TimeBucket::All => write!(f, "All"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use chrono::NaiveDate;
|
||||
|
||||
#[test]
|
||||
fn test_time_bucket_from_dates() {
|
||||
let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap();
|
||||
|
||||
let date = today;
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek);
|
||||
|
||||
// All: not in this week or last week
|
||||
let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All);
|
||||
|
||||
// Test year boundary cases
|
||||
let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap();
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap();
|
||||
assert_eq!(
|
||||
TimeBucket::from_dates(new_year, date),
|
||||
TimeBucket::Yesterday
|
||||
);
|
||||
|
||||
let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap();
|
||||
assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek);
|
||||
}
|
||||
}
|
||||
@@ -1557,7 +1557,7 @@ impl Panel for DebugPanel {
|
||||
self.sessions_with_children.keys().for_each(|session_item| {
|
||||
session_item.update(cx, |item, cx| {
|
||||
item.running_state()
|
||||
.update(cx, |state, _| state.invert_axies())
|
||||
.update(cx, |state, cx| state.invert_axies(cx))
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -348,7 +348,7 @@ pub(crate) fn new_debugger_pane(
|
||||
debug_assert!(_previous_subscription.is_none());
|
||||
running
|
||||
.panes
|
||||
.split(&this_pane, &new_pane, split_direction)?;
|
||||
.split(&this_pane, &new_pane, split_direction, cx)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
})
|
||||
@@ -1462,7 +1462,7 @@ impl RunningState {
|
||||
this.serialize_layout(window, cx);
|
||||
match event {
|
||||
Event::Remove { .. } => {
|
||||
let _did_find_pane = this.panes.remove(source_pane).is_ok();
|
||||
let _did_find_pane = this.panes.remove(source_pane, cx).is_ok();
|
||||
debug_assert!(_did_find_pane);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -1889,9 +1889,9 @@ impl RunningState {
|
||||
Member::Axis(group_root)
|
||||
}
|
||||
|
||||
pub(crate) fn invert_axies(&mut self) {
|
||||
pub(crate) fn invert_axies(&mut self, cx: &mut App) {
|
||||
self.dock_axis = self.dock_axis.invert();
|
||||
self.panes.invert_axies();
|
||||
self.panes.invert_axies(cx);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ impl SplittableEditor {
|
||||
});
|
||||
let primary_pane = self.panes.first_pane();
|
||||
self.panes
|
||||
.split(&primary_pane, &secondary_pane, SplitDirection::Left)
|
||||
.split(&primary_pane, &secondary_pane, SplitDirection::Left, cx)
|
||||
.unwrap();
|
||||
cx.notify();
|
||||
}
|
||||
@@ -203,7 +203,7 @@ impl SplittableEditor {
|
||||
let Some(secondary) = self.secondary.take() else {
|
||||
return;
|
||||
};
|
||||
self.panes.remove(&secondary.pane).unwrap();
|
||||
self.panes.remove(&secondary.pane, cx).unwrap();
|
||||
self.primary_editor.update(cx, |primary, cx| {
|
||||
primary.buffer().update(cx, |buffer, _| {
|
||||
buffer.set_filter_mode(None);
|
||||
|
||||
@@ -17,3 +17,9 @@ pub struct InlineAssistantUseToolFeatureFlag;
|
||||
impl FeatureFlag for InlineAssistantUseToolFeatureFlag {
|
||||
const NAME: &'static str = "inline-assistant-use-tool";
|
||||
}
|
||||
|
||||
pub struct AgentV2FeatureFlag;
|
||||
|
||||
impl FeatureFlag for AgentV2FeatureFlag {
|
||||
const NAME: &'static str = "agent-v2";
|
||||
}
|
||||
@@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||
use settings_macros::{MergeFrom, with_fallible_options};
|
||||
use std::{borrow::Cow, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::DockPosition;
|
||||
use crate::{DockPosition, DockSide};
|
||||
|
||||
#[with_fallible_options]
|
||||
#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)]
|
||||
@@ -22,6 +22,10 @@ pub struct AgentSettingsContent {
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<DockPosition>,
|
||||
/// Where to dock the utility pane (the thread view pane).
|
||||
///
|
||||
/// Default: left
|
||||
pub agents_panel_dock: Option<DockSide>,
|
||||
/// Default width in pixels when the agent panel is docked to the left or right.
|
||||
///
|
||||
/// Default: 640
|
||||
|
||||
@@ -342,7 +342,7 @@ impl TerminalPanel {
|
||||
pane::Event::RemovedItem { .. } => self.serialize(cx),
|
||||
pane::Event::Remove { focus_on_pane } => {
|
||||
let pane_count_before_removal = self.center.panes().len();
|
||||
let _removal_result = self.center.remove(pane);
|
||||
let _removal_result = self.center.remove(pane, cx);
|
||||
if pane_count_before_removal == 1 {
|
||||
self.center.first_pane().update(cx, |pane, cx| {
|
||||
pane.set_zoomed(false, cx);
|
||||
@@ -393,7 +393,10 @@ impl TerminalPanel {
|
||||
};
|
||||
panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.center.split(&pane, &new_pane, direction).log_err();
|
||||
panel
|
||||
.center
|
||||
.split(&pane, &new_pane, direction, cx)
|
||||
.log_err();
|
||||
window.focus(&new_pane.focus_handle(cx));
|
||||
})
|
||||
.ok();
|
||||
@@ -415,7 +418,7 @@ impl TerminalPanel {
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction).log_err();
|
||||
self.center.split(&pane, &new_pane, direction, cx).log_err();
|
||||
window.focus(&new_pane.focus_handle(cx));
|
||||
}
|
||||
}
|
||||
@@ -1066,7 +1069,7 @@ impl TerminalPanel {
|
||||
.find_pane_in_direction(&self.active_pane, direction, cx)
|
||||
.cloned()
|
||||
{
|
||||
self.center.swap(&self.active_pane, &to);
|
||||
self.center.swap(&self.active_pane, &to, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -1074,7 +1077,7 @@ impl TerminalPanel {
|
||||
fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
if self
|
||||
.center
|
||||
.move_to_border(&self.active_pane, direction)
|
||||
.move_to_border(&self.active_pane, direction, cx)
|
||||
.unwrap()
|
||||
{
|
||||
cx.notify();
|
||||
@@ -1189,6 +1192,7 @@ pub fn new_terminal_pane(
|
||||
&this_pane,
|
||||
&new_pane,
|
||||
split_direction,
|
||||
cx,
|
||||
)?;
|
||||
anyhow::Ok(new_pane)
|
||||
})
|
||||
@@ -1482,6 +1486,7 @@ impl Render for TerminalPanel {
|
||||
&terminal_panel.active_pane,
|
||||
&new_pane,
|
||||
SplitDirection::Right,
|
||||
cx,
|
||||
)
|
||||
.log_err();
|
||||
let new_pane = new_pane.read(cx);
|
||||
|
||||
@@ -10,6 +10,7 @@ pub struct TabBar {
|
||||
start_children: SmallVec<[AnyElement; 2]>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
end_children: SmallVec<[AnyElement; 2]>,
|
||||
pre_end_children: SmallVec<[AnyElement; 2]>,
|
||||
scroll_handle: Option<ScrollHandle>,
|
||||
}
|
||||
|
||||
@@ -20,6 +21,7 @@ impl TabBar {
|
||||
start_children: SmallVec::new(),
|
||||
children: SmallVec::new(),
|
||||
end_children: SmallVec::new(),
|
||||
pre_end_children: SmallVec::new(),
|
||||
scroll_handle: None,
|
||||
}
|
||||
}
|
||||
@@ -70,6 +72,15 @@ impl TabBar {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn pre_end_child(mut self, end_child: impl IntoElement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.pre_end_children
|
||||
.push(end_child.into_element().into_any());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_children(mut self, end_children: impl IntoIterator<Item = impl IntoElement>) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
@@ -137,18 +148,31 @@ impl RenderOnce for TabBar {
|
||||
.children(self.children),
|
||||
),
|
||||
)
|
||||
.when(!self.end_children.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.border_b_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.end_children),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
!self.end_children.is_empty() || !self.pre_end_children.is_empty(),
|
||||
|this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.px(DynamicSpacing::Base06.rems(cx))
|
||||
.children(self.pre_end_children)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_b_1()
|
||||
.when(!self.end_children.is_empty(), |div| {
|
||||
div.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.pl(DynamicSpacing::Base04.rems(cx))
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.children(self.end_children),
|
||||
)
|
||||
}),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ clock.workspace = true
|
||||
collections.workspace = true
|
||||
component.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use crate::persistence::model::DockData;
|
||||
use crate::utility_pane::utility_slot_for_dock_position;
|
||||
use crate::{DraggedDock, Event, ModalLayer, Pane};
|
||||
use crate::{Workspace, status_bar::StatusItemView};
|
||||
use anyhow::Context as _;
|
||||
use client::proto;
|
||||
|
||||
use gpui::{
|
||||
Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement,
|
||||
@@ -13,6 +15,7 @@ use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex};
|
||||
use ui::{prelude::*, right_click_menu};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
|
||||
|
||||
@@ -25,6 +28,72 @@ pub enum PanelEvent {
|
||||
|
||||
pub use proto::PanelId;
|
||||
|
||||
pub struct MinimizePane;
|
||||
pub struct ClosePane;
|
||||
|
||||
pub trait UtilityPane: EventEmitter<MinimizePane> + EventEmitter<ClosePane> + Render {
|
||||
fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition;
|
||||
/// The icon to render in the adjacent pane's tab bar for toggling this utility pane
|
||||
fn toggle_icon(&self, cx: &App) -> IconName;
|
||||
fn expanded(&self, cx: &App) -> bool;
|
||||
fn set_expanded(&mut self, expanded: bool, cx: &mut Context<Self>);
|
||||
fn width(&self, cx: &App) -> Pixels;
|
||||
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
|
||||
}
|
||||
|
||||
pub trait UtilityPaneHandle: 'static + Send + Sync {
|
||||
fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition;
|
||||
fn toggle_icon(&self, cx: &App) -> IconName;
|
||||
fn expanded(&self, cx: &App) -> bool;
|
||||
fn set_expanded(&self, expanded: bool, cx: &mut App);
|
||||
fn width(&self, cx: &App) -> Pixels;
|
||||
fn set_width(&self, width: Option<Pixels>, cx: &mut App);
|
||||
fn to_any(&self) -> AnyView;
|
||||
fn box_clone(&self) -> Box<dyn UtilityPaneHandle>;
|
||||
}
|
||||
|
||||
impl<T> UtilityPaneHandle for Entity<T>
|
||||
where
|
||||
T: UtilityPane,
|
||||
{
|
||||
fn position(&self, window: &Window, cx: &App) -> UtilityPanePosition {
|
||||
self.read(cx).position(window, cx)
|
||||
}
|
||||
|
||||
fn toggle_icon(&self, cx: &App) -> IconName {
|
||||
self.read(cx).toggle_icon(cx)
|
||||
}
|
||||
|
||||
fn expanded(&self, cx: &App) -> bool {
|
||||
self.read(cx).expanded(cx)
|
||||
}
|
||||
|
||||
fn set_expanded(&self, expanded: bool, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.set_expanded(expanded, cx))
|
||||
}
|
||||
|
||||
fn width(&self, cx: &App) -> Pixels {
|
||||
self.read(cx).width(cx)
|
||||
}
|
||||
|
||||
fn set_width(&self, width: Option<Pixels>, cx: &mut App) {
|
||||
self.update(cx, |this, cx| this.set_width(width, cx))
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn box_clone(&self) -> Box<dyn UtilityPaneHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub enum UtilityPanePosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
|
||||
fn persistent_name() -> &'static str;
|
||||
fn panel_key() -> &'static str;
|
||||
@@ -384,6 +453,13 @@ impl Dock {
|
||||
.position(|entry| entry.panel.remote_id() == Some(panel_id))
|
||||
}
|
||||
|
||||
pub fn panel_for_id(&self, panel_id: EntityId) -> Option<&Arc<dyn PanelHandle>> {
|
||||
self.panel_entries
|
||||
.iter()
|
||||
.find(|entry| entry.panel.panel_id() == panel_id)
|
||||
.map(|entry| &entry.panel)
|
||||
}
|
||||
|
||||
pub fn first_enabled_panel_idx(&mut self, cx: &mut Context<Self>) -> anyhow::Result<usize> {
|
||||
self.panel_entries
|
||||
.iter()
|
||||
@@ -491,6 +567,9 @@ impl Dock {
|
||||
|
||||
new_dock.update(cx, |new_dock, cx| {
|
||||
new_dock.remove_panel(&panel, window, cx);
|
||||
});
|
||||
|
||||
new_dock.update(cx, |new_dock, cx| {
|
||||
let index =
|
||||
new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
|
||||
if was_visible {
|
||||
@@ -498,6 +577,12 @@ impl Dock {
|
||||
new_dock.activate_panel(index, window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.serialize_workspace(window, cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}),
|
||||
cx.subscribe_in(
|
||||
@@ -586,6 +671,7 @@ impl Dock {
|
||||
);
|
||||
|
||||
self.restore_state(window, cx);
|
||||
|
||||
if panel.read(cx).starts_open(window, cx) {
|
||||
self.activate_panel(index, window, cx);
|
||||
self.set_open(true, window, cx);
|
||||
@@ -637,6 +723,14 @@ impl Dock {
|
||||
std::cmp::Ordering::Greater => {}
|
||||
}
|
||||
}
|
||||
|
||||
let slot = utility_slot_for_dock_position(self.position);
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
|
||||
});
|
||||
}
|
||||
|
||||
self.panel_entries.remove(panel_ix);
|
||||
cx.notify();
|
||||
}
|
||||
@@ -891,7 +985,13 @@ impl Render for PanelButtons {
|
||||
.enumerate()
|
||||
.filter_map(|(i, entry)| {
|
||||
let icon = entry.panel.icon(window, cx)?;
|
||||
let icon_tooltip = entry.panel.icon_tooltip(window, cx)?;
|
||||
let icon_tooltip = entry
|
||||
.panel
|
||||
.icon_tooltip(window, cx)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("can't render a panel button without an icon tooltip")
|
||||
})
|
||||
.log_err()?;
|
||||
let name = entry.panel.persistent_name();
|
||||
let panel = entry.panel.clone();
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ use crate::{
|
||||
move_item,
|
||||
notifications::NotifyResultExt,
|
||||
toolbar::Toolbar,
|
||||
utility_pane::UtilityPaneSlot,
|
||||
workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div,
|
||||
@@ -396,6 +398,10 @@ pub struct Pane {
|
||||
diagnostic_summary_update: Task<()>,
|
||||
/// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here.
|
||||
pub project_item_restoration_data: HashMap<ProjectItemKind, Box<dyn Any + Send>>,
|
||||
|
||||
pub in_center_group: bool,
|
||||
pub is_upper_left: bool,
|
||||
pub is_upper_right: bool,
|
||||
}
|
||||
|
||||
pub struct ActivationHistoryEntry {
|
||||
@@ -540,6 +546,9 @@ impl Pane {
|
||||
zoom_out_on_close: true,
|
||||
diagnostic_summary_update: Task::ready(()),
|
||||
project_item_restoration_data: HashMap::default(),
|
||||
in_center_group: false,
|
||||
is_upper_left: false,
|
||||
is_upper_right: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3033,6 +3042,10 @@ impl Pane {
|
||||
}
|
||||
|
||||
fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context<Pane>) -> AnyElement {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return gpui::Empty.into_any();
|
||||
};
|
||||
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft)
|
||||
.icon_size(IconSize::Small)
|
||||
@@ -3057,6 +3070,44 @@ impl Pane {
|
||||
}
|
||||
});
|
||||
|
||||
let open_aside_left = {
|
||||
let workspace = workspace.read(cx);
|
||||
workspace.utility_pane(UtilityPaneSlot::Left).map(|pane| {
|
||||
let toggle_icon = pane.toggle_icon(cx);
|
||||
let workspace_handle = self.workspace.clone();
|
||||
|
||||
IconButton::new("open_aside_left", toggle_icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
workspace_handle
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_utility_pane(UtilityPaneSlot::Left, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
};
|
||||
|
||||
let open_aside_right = {
|
||||
let workspace = workspace.read(cx);
|
||||
workspace.utility_pane(UtilityPaneSlot::Right).map(|pane| {
|
||||
let toggle_icon = pane.toggle_icon(cx);
|
||||
let workspace_handle = self.workspace.clone();
|
||||
|
||||
IconButton::new("open_aside_right", toggle_icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(move |_, window, cx| {
|
||||
workspace_handle
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.toggle_utility_pane(UtilityPaneSlot::Right, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
.into_any_element()
|
||||
})
|
||||
};
|
||||
|
||||
let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click({
|
||||
@@ -3103,13 +3154,50 @@ impl Pane {
|
||||
let unpinned_tabs = tab_items.split_off(self.pinned_tab_count);
|
||||
let pinned_tabs = tab_items;
|
||||
|
||||
let render_aside_toggle_left = cx.has_flag::<AgentV2FeatureFlag>()
|
||||
&& self
|
||||
.is_upper_left
|
||||
.then(|| {
|
||||
self.workspace.upgrade().and_then(|entity| {
|
||||
let workspace = entity.read(cx);
|
||||
workspace
|
||||
.utility_pane(UtilityPaneSlot::Left)
|
||||
.map(|pane| !pane.expanded(cx))
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
|
||||
let render_aside_toggle_right = cx.has_flag::<AgentV2FeatureFlag>()
|
||||
&& self
|
||||
.is_upper_right
|
||||
.then(|| {
|
||||
self.workspace.upgrade().and_then(|entity| {
|
||||
let workspace = entity.read(cx);
|
||||
workspace
|
||||
.utility_pane(UtilityPaneSlot::Right)
|
||||
.map(|pane| !pane.expanded(cx))
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
|
||||
TabBar::new("tab_bar")
|
||||
.map(|tab_bar| {
|
||||
if let Some(open_aside_left) = open_aside_left
|
||||
&& render_aside_toggle_left
|
||||
{
|
||||
tab_bar.start_child(open_aside_left)
|
||||
} else {
|
||||
tab_bar
|
||||
}
|
||||
})
|
||||
.when(
|
||||
self.display_nav_history_buttons.unwrap_or_default(),
|
||||
|tab_bar| {
|
||||
tab_bar
|
||||
.start_child(navigate_backward)
|
||||
.start_child(navigate_forward)
|
||||
.pre_end_child(navigate_backward)
|
||||
.pre_end_child(navigate_forward)
|
||||
},
|
||||
)
|
||||
.map(|tab_bar| {
|
||||
@@ -3196,6 +3284,15 @@ impl Pane {
|
||||
})),
|
||||
),
|
||||
)
|
||||
.map(|tab_bar| {
|
||||
if let Some(open_aside_right) = open_aside_right
|
||||
&& render_aside_toggle_right
|
||||
{
|
||||
tab_bar.end_child(open_aside_right)
|
||||
} else {
|
||||
tab_bar
|
||||
}
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
@@ -6664,8 +6761,8 @@ mod tests {
|
||||
let scroll_bounds = tab_bar_scroll_handle.bounds();
|
||||
let scroll_offset = tab_bar_scroll_handle.offset();
|
||||
assert!(tab_bounds.right() <= scroll_bounds.right() + scroll_offset.x);
|
||||
// -39.5 is the magic number for this setup
|
||||
assert_eq!(scroll_offset.x, px(-39.5));
|
||||
// -35.0 is the magic number for this setup
|
||||
assert_eq!(scroll_offset.x, px(-35.0));
|
||||
assert!(
|
||||
!tab_bounds.intersects(&new_tab_button_bounds),
|
||||
"Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!"
|
||||
|
||||
@@ -28,6 +28,7 @@ const VERTICAL_MIN_SIZE: f32 = 100.;
|
||||
#[derive(Clone)]
|
||||
pub struct PaneGroup {
|
||||
pub root: Member,
|
||||
pub is_center: bool,
|
||||
}
|
||||
|
||||
pub struct PaneRenderResult {
|
||||
@@ -37,22 +38,31 @@ pub struct PaneRenderResult {
|
||||
|
||||
impl PaneGroup {
|
||||
pub fn with_root(root: Member) -> Self {
|
||||
Self { root }
|
||||
Self {
|
||||
root,
|
||||
is_center: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(pane: Entity<Pane>) -> Self {
|
||||
Self {
|
||||
root: Member::Pane(pane),
|
||||
is_center: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_is_center(&mut self, is_center: bool) {
|
||||
self.is_center = is_center;
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
&mut self,
|
||||
old_pane: &Entity<Pane>,
|
||||
new_pane: &Entity<Pane>,
|
||||
direction: SplitDirection,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
match &mut self.root {
|
||||
let result = match &mut self.root {
|
||||
Member::Pane(pane) => {
|
||||
if pane == old_pane {
|
||||
self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction);
|
||||
@@ -62,7 +72,11 @@ impl PaneGroup {
|
||||
}
|
||||
}
|
||||
Member::Axis(axis) => axis.split(old_pane, new_pane, direction),
|
||||
};
|
||||
if result.is_ok() {
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
|
||||
@@ -90,6 +104,7 @@ impl PaneGroup {
|
||||
&mut self,
|
||||
active_pane: &Entity<Pane>,
|
||||
direction: SplitDirection,
|
||||
cx: &mut App,
|
||||
) -> Result<bool> {
|
||||
if let Some(pane) = self.find_pane_at_border(direction)
|
||||
&& pane == active_pane
|
||||
@@ -97,7 +112,7 @@ impl PaneGroup {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if !self.remove(active_pane)? {
|
||||
if !self.remove_internal(active_pane)? {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
@@ -110,6 +125,7 @@ impl PaneGroup {
|
||||
0
|
||||
};
|
||||
root.insert_pane(idx, active_pane);
|
||||
self.mark_positions(cx);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -119,6 +135,7 @@ impl PaneGroup {
|
||||
vec![Member::Pane(active_pane.clone()), self.root.clone()]
|
||||
};
|
||||
self.root = Member::Axis(PaneAxis::new(direction.axis(), members));
|
||||
self.mark_positions(cx);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
@@ -133,7 +150,15 @@ impl PaneGroup {
|
||||
/// - Ok(true) if it found and removed a pane
|
||||
/// - Ok(false) if it found but did not remove the pane
|
||||
/// - Err(_) if it did not find the pane
|
||||
pub fn remove(&mut self, pane: &Entity<Pane>) -> Result<bool> {
|
||||
pub fn remove(&mut self, pane: &Entity<Pane>, cx: &mut App) -> Result<bool> {
|
||||
let result = self.remove_internal(pane);
|
||||
if let Ok(true) = result {
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn remove_internal(&mut self, pane: &Entity<Pane>) -> Result<bool> {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => Ok(false),
|
||||
Member::Axis(axis) => {
|
||||
@@ -151,6 +176,7 @@ impl PaneGroup {
|
||||
direction: Axis,
|
||||
amount: Pixels,
|
||||
bounds: &Bounds<Pixels>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => {}
|
||||
@@ -158,22 +184,29 @@ impl PaneGroup {
|
||||
let _ = axis.resize(pane, direction, amount, bounds);
|
||||
}
|
||||
};
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
|
||||
pub fn reset_pane_sizes(&mut self) {
|
||||
pub fn reset_pane_sizes(&mut self, cx: &mut App) {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => {}
|
||||
Member::Axis(axis) => {
|
||||
let _ = axis.reset_pane_sizes();
|
||||
}
|
||||
};
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
|
||||
pub fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>) {
|
||||
pub fn swap(&mut self, from: &Entity<Pane>, to: &Entity<Pane>, cx: &mut App) {
|
||||
match &mut self.root {
|
||||
Member::Pane(_) => {}
|
||||
Member::Axis(axis) => axis.swap(from, to),
|
||||
};
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
|
||||
pub fn mark_positions(&mut self, cx: &mut App) {
|
||||
self.root.mark_positions(self.is_center, true, true, cx);
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
@@ -232,8 +265,9 @@ impl PaneGroup {
|
||||
self.pane_at_pixel_position(target)
|
||||
}
|
||||
|
||||
pub fn invert_axies(&mut self) {
|
||||
pub fn invert_axies(&mut self, cx: &mut App) {
|
||||
self.root.invert_pane_axies();
|
||||
self.mark_positions(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +277,43 @@ pub enum Member {
|
||||
Pane(Entity<Pane>),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
pub fn mark_positions(
|
||||
&mut self,
|
||||
in_center_group: bool,
|
||||
is_upper_left: bool,
|
||||
is_upper_right: bool,
|
||||
cx: &mut App,
|
||||
) {
|
||||
match self {
|
||||
Member::Axis(pane_axis) => {
|
||||
let len = pane_axis.members.len();
|
||||
for (idx, member) in pane_axis.members.iter_mut().enumerate() {
|
||||
let member_upper_left = match pane_axis.axis {
|
||||
Axis::Vertical => is_upper_left && idx == 0,
|
||||
Axis::Horizontal => is_upper_left && idx == 0,
|
||||
};
|
||||
let member_upper_right = match pane_axis.axis {
|
||||
Axis::Vertical => is_upper_right && idx == 0,
|
||||
Axis::Horizontal => is_upper_right && idx == len - 1,
|
||||
};
|
||||
member.mark_positions(
|
||||
in_center_group,
|
||||
member_upper_left,
|
||||
member_upper_right,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
Member::Pane(entity) => entity.update(cx, |pane, _| {
|
||||
pane.in_center_group = in_center_group;
|
||||
pane.is_upper_left = is_upper_left;
|
||||
pane.is_upper_right = is_upper_right;
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct PaneRenderContext<'a> {
|
||||
pub project: &'a Entity<Project>,
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
use gpui::{
|
||||
AppContext as _, EntityId, MouseButton, Pixels, Render, StatefulInteractiveElement,
|
||||
Subscription, WeakEntity, deferred, px,
|
||||
};
|
||||
use ui::{
|
||||
ActiveTheme as _, Context, FluentBuilder as _, InteractiveElement as _, IntoElement,
|
||||
ParentElement as _, RenderOnce, Styled as _, Window, div,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
DockPosition, Workspace,
|
||||
dock::{ClosePane, MinimizePane, UtilityPane, UtilityPaneHandle},
|
||||
};
|
||||
|
||||
pub(crate) const UTILITY_PANE_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
|
||||
pub(crate) const UTILITY_PANE_MIN_WIDTH: Pixels = px(20.0);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum UtilityPaneSlot {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
struct UtilityPaneSlotState {
|
||||
panel_id: EntityId,
|
||||
utility_pane: Box<dyn UtilityPaneHandle>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UtilityPaneState {
|
||||
left_slot: Option<UtilityPaneSlotState>,
|
||||
right_slot: Option<UtilityPaneSlotState>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DraggedUtilityPane(pub UtilityPaneSlot);
|
||||
|
||||
impl Render for DraggedUtilityPane {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
gpui::Empty
|
||||
}
|
||||
}
|
||||
|
||||
pub fn utility_slot_for_dock_position(position: DockPosition) -> UtilityPaneSlot {
|
||||
match position {
|
||||
DockPosition::Left => UtilityPaneSlot::Left,
|
||||
DockPosition::Right => UtilityPaneSlot::Right,
|
||||
DockPosition::Bottom => UtilityPaneSlot::Left,
|
||||
}
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub fn utility_pane(&self, slot: UtilityPaneSlot) -> Option<&dyn UtilityPaneHandle> {
|
||||
match slot {
|
||||
UtilityPaneSlot::Left => self
|
||||
.utility_panes
|
||||
.left_slot
|
||||
.as_ref()
|
||||
.map(|s| s.utility_pane.as_ref()),
|
||||
UtilityPaneSlot::Right => self
|
||||
.utility_panes
|
||||
.right_slot
|
||||
.as_ref()
|
||||
.map(|s| s.utility_pane.as_ref()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_utility_pane(
|
||||
&mut self,
|
||||
slot: UtilityPaneSlot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(handle) = self.utility_pane(slot) {
|
||||
let current = handle.expanded(cx);
|
||||
handle.set_expanded(!current, cx);
|
||||
}
|
||||
cx.notify();
|
||||
self.serialize_workspace(window, cx);
|
||||
}
|
||||
|
||||
pub fn register_utility_pane<T: UtilityPane>(
|
||||
&mut self,
|
||||
slot: UtilityPaneSlot,
|
||||
panel_id: EntityId,
|
||||
handle: gpui::Entity<T>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let minimize_subscription =
|
||||
cx.subscribe(&handle, move |this, _, _event: &MinimizePane, cx| {
|
||||
if let Some(handle) = this.utility_pane(slot) {
|
||||
handle.set_expanded(false, cx);
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let close_subscription = cx.subscribe(&handle, move |this, _, _event: &ClosePane, cx| {
|
||||
this.clear_utility_pane(slot, cx);
|
||||
});
|
||||
|
||||
let subscriptions = vec![minimize_subscription, close_subscription];
|
||||
let boxed_handle: Box<dyn UtilityPaneHandle> = Box::new(handle);
|
||||
|
||||
match slot {
|
||||
UtilityPaneSlot::Left => {
|
||||
self.utility_panes.left_slot = Some(UtilityPaneSlotState {
|
||||
panel_id,
|
||||
utility_pane: boxed_handle,
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
}
|
||||
UtilityPaneSlot::Right => {
|
||||
self.utility_panes.right_slot = Some(UtilityPaneSlotState {
|
||||
panel_id,
|
||||
utility_pane: boxed_handle,
|
||||
_subscriptions: subscriptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_utility_pane(&mut self, slot: UtilityPaneSlot, cx: &mut Context<Self>) {
|
||||
match slot {
|
||||
UtilityPaneSlot::Left => {
|
||||
self.utility_panes.left_slot = None;
|
||||
}
|
||||
UtilityPaneSlot::Right => {
|
||||
self.utility_panes.right_slot = None;
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn clear_utility_pane_if_provider(
|
||||
&mut self,
|
||||
slot: UtilityPaneSlot,
|
||||
provider_panel_id: EntityId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let should_clear = match slot {
|
||||
UtilityPaneSlot::Left => self
|
||||
.utility_panes
|
||||
.left_slot
|
||||
.as_ref()
|
||||
.is_some_and(|slot| slot.panel_id == provider_panel_id),
|
||||
UtilityPaneSlot::Right => self
|
||||
.utility_panes
|
||||
.right_slot
|
||||
.as_ref()
|
||||
.is_some_and(|slot| slot.panel_id == provider_panel_id),
|
||||
};
|
||||
|
||||
if should_clear {
|
||||
self.clear_utility_pane(slot, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize_utility_pane(
|
||||
&mut self,
|
||||
slot: UtilityPaneSlot,
|
||||
new_width: Pixels,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(handle) = self.utility_pane(slot) {
|
||||
let max_width = self.max_utility_pane_width(window, cx);
|
||||
let width = new_width.max(UTILITY_PANE_MIN_WIDTH).min(max_width);
|
||||
handle.set_width(Some(width), cx);
|
||||
cx.notify();
|
||||
self.serialize_workspace(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset_utility_pane_width(
|
||||
&mut self,
|
||||
slot: UtilityPaneSlot,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(handle) = self.utility_pane(slot) {
|
||||
handle.set_width(None, cx);
|
||||
cx.notify();
|
||||
self.serialize_workspace(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct UtilityPaneFrame {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
slot: UtilityPaneSlot,
|
||||
handle: Box<dyn UtilityPaneHandle>,
|
||||
}
|
||||
|
||||
impl UtilityPaneFrame {
|
||||
pub fn new(
|
||||
slot: UtilityPaneSlot,
|
||||
handle: Box<dyn UtilityPaneHandle>,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Self {
|
||||
let workspace = cx.weak_entity();
|
||||
Self {
|
||||
workspace,
|
||||
slot,
|
||||
handle,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for UtilityPaneFrame {
|
||||
fn render(self, _window: &mut Window, cx: &mut ui::App) -> impl IntoElement {
|
||||
let workspace = self.workspace.clone();
|
||||
let slot = self.slot;
|
||||
let width = self.handle.width(cx);
|
||||
|
||||
let create_resize_handle = || {
|
||||
let workspace_handle = workspace.clone();
|
||||
let handle = div()
|
||||
.id(match slot {
|
||||
UtilityPaneSlot::Left => "utility-pane-resize-handle-left",
|
||||
UtilityPaneSlot::Right => "utility-pane-resize-handle-right",
|
||||
})
|
||||
.on_drag(DraggedUtilityPane(slot), move |pane, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new(|_| pane.clone())
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, move |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
move |e: &gpui::MouseUpEvent, window, cx| {
|
||||
if e.click_count == 2 {
|
||||
workspace_handle
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.reset_utility_pane_width(slot, window, cx);
|
||||
})
|
||||
.ok();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
},
|
||||
)
|
||||
.occlude();
|
||||
|
||||
match slot {
|
||||
UtilityPaneSlot::Left => deferred(
|
||||
handle
|
||||
.absolute()
|
||||
.right(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.)
|
||||
.top(px(0.))
|
||||
.h_full()
|
||||
.w(UTILITY_PANE_RESIZE_HANDLE_SIZE)
|
||||
.cursor_col_resize(),
|
||||
),
|
||||
UtilityPaneSlot::Right => deferred(
|
||||
handle
|
||||
.absolute()
|
||||
.left(-UTILITY_PANE_RESIZE_HANDLE_SIZE / 2.)
|
||||
.top(px(0.))
|
||||
.h_full()
|
||||
.w(UTILITY_PANE_RESIZE_HANDLE_SIZE)
|
||||
.cursor_col_resize(),
|
||||
),
|
||||
}
|
||||
};
|
||||
|
||||
div()
|
||||
.h_full()
|
||||
.bg(cx.theme().colors().tab_bar_background)
|
||||
.w(width)
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(self.slot == UtilityPaneSlot::Left, |this| this.border_r_1())
|
||||
.when(self.slot == UtilityPaneSlot::Right, |this| {
|
||||
this.border_l_1()
|
||||
})
|
||||
.child(create_resize_handle())
|
||||
.child(self.handle.to_any())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ pub mod tasks;
|
||||
mod theme_preview;
|
||||
mod toast_layer;
|
||||
mod toolbar;
|
||||
pub mod utility_pane;
|
||||
mod workspace_settings;
|
||||
|
||||
pub use crate::notifications::NotificationFrame;
|
||||
@@ -30,6 +31,7 @@ use client::{
|
||||
};
|
||||
use collections::{HashMap, HashSet, hash_map};
|
||||
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use futures::{
|
||||
Future, FutureExt, StreamExt,
|
||||
channel::{
|
||||
@@ -126,11 +128,16 @@ pub use workspace_settings::{
|
||||
};
|
||||
use zed_actions::{Spawn, feedback::FileBugReport};
|
||||
|
||||
use crate::persistence::{
|
||||
SerializedAxis,
|
||||
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
use crate::{
|
||||
item::ItemBufferKind, notifications::NotificationId, utility_pane::UTILITY_PANE_MIN_WIDTH,
|
||||
};
|
||||
use crate::{
|
||||
persistence::{
|
||||
SerializedAxis,
|
||||
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
|
||||
},
|
||||
utility_pane::{DraggedUtilityPane, UtilityPaneFrame, UtilityPaneSlot, UtilityPaneState},
|
||||
};
|
||||
use crate::{item::ItemBufferKind, notifications::NotificationId};
|
||||
|
||||
pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
|
||||
|
||||
@@ -1175,6 +1182,7 @@ pub struct Workspace {
|
||||
scheduled_tasks: Vec<Task<()>>,
|
||||
last_open_dock_positions: Vec<DockPosition>,
|
||||
removing: bool,
|
||||
utility_panes: UtilityPaneState,
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for Workspace {}
|
||||
@@ -1466,12 +1474,17 @@ impl Workspace {
|
||||
this.update_window_title(window, cx);
|
||||
this.show_initial_notifications(cx);
|
||||
});
|
||||
|
||||
let mut center = PaneGroup::new(center_pane.clone());
|
||||
center.set_is_center(true);
|
||||
center.mark_positions(cx);
|
||||
|
||||
Workspace {
|
||||
weak_self: weak_handle.clone(),
|
||||
zoomed: None,
|
||||
zoomed_position: None,
|
||||
previous_dock_drag_coordinates: None,
|
||||
center: PaneGroup::new(center_pane.clone()),
|
||||
center,
|
||||
panes: vec![center_pane.clone()],
|
||||
panes_by_item: Default::default(),
|
||||
active_pane: center_pane.clone(),
|
||||
@@ -1519,6 +1532,7 @@ impl Workspace {
|
||||
scheduled_tasks: Vec::new(),
|
||||
last_open_dock_positions: Vec::new(),
|
||||
removing: false,
|
||||
utility_panes: UtilityPaneState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3771,7 +3785,7 @@ impl Workspace {
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
if self
|
||||
.center
|
||||
.split(&split_off_pane, &new_pane, direction)
|
||||
.split(&split_off_pane, &new_pane, direction, cx)
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
@@ -3956,7 +3970,7 @@ impl Workspace {
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
if self
|
||||
.center
|
||||
.split(&self.active_pane, &new_pane, action.direction)
|
||||
.split(&self.active_pane, &new_pane, action.direction, cx)
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
@@ -4010,7 +4024,7 @@ impl Workspace {
|
||||
|
||||
pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
if let Some(to) = self.find_pane_in_direction(direction, cx) {
|
||||
self.center.swap(&self.active_pane, &to);
|
||||
self.center.swap(&self.active_pane, &to, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -4018,7 +4032,7 @@ impl Workspace {
|
||||
pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context<Self>) {
|
||||
if self
|
||||
.center
|
||||
.move_to_border(&self.active_pane, direction)
|
||||
.move_to_border(&self.active_pane, direction, cx)
|
||||
.unwrap()
|
||||
{
|
||||
cx.notify();
|
||||
@@ -4048,13 +4062,13 @@ impl Workspace {
|
||||
}
|
||||
} else {
|
||||
self.center
|
||||
.resize(&self.active_pane, axis, amount, &self.bounds);
|
||||
.resize(&self.active_pane, axis, amount, &self.bounds, cx);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reset_pane_sizes(&mut self, cx: &mut Context<Self>) {
|
||||
self.center.reset_pane_sizes();
|
||||
self.center.reset_pane_sizes(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -4240,7 +4254,7 @@ impl Workspace {
|
||||
) -> Entity<Pane> {
|
||||
let new_pane = self.add_pane(window, cx);
|
||||
self.center
|
||||
.split(&pane_to_split, &new_pane, split_direction)
|
||||
.split(&pane_to_split, &new_pane, split_direction, cx)
|
||||
.unwrap();
|
||||
cx.notify();
|
||||
new_pane
|
||||
@@ -4260,7 +4274,7 @@ impl Workspace {
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx)
|
||||
});
|
||||
self.center.split(&pane, &new_pane, direction).unwrap();
|
||||
self.center.split(&pane, &new_pane, direction, cx).unwrap();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -4285,7 +4299,7 @@ impl Workspace {
|
||||
new_pane.update(cx, |pane, cx| {
|
||||
pane.add_item(clone, true, true, None, window, cx)
|
||||
});
|
||||
this.center.split(&pane, &new_pane, direction).unwrap();
|
||||
this.center.split(&pane, &new_pane, direction, cx).unwrap();
|
||||
cx.notify();
|
||||
new_pane
|
||||
})
|
||||
@@ -4332,7 +4346,7 @@ impl Workspace {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.center.remove(&pane).unwrap() {
|
||||
if self.center.remove(&pane, cx).unwrap() {
|
||||
self.force_remove_pane(&pane, &focus_on, window, cx);
|
||||
self.unfollow_in_pane(&pane, window, cx);
|
||||
self.last_leaders_by_pane.remove(&pane.downgrade());
|
||||
@@ -5684,6 +5698,9 @@ impl Workspace {
|
||||
|
||||
// Swap workspace center group
|
||||
workspace.center = PaneGroup::with_root(center_group);
|
||||
workspace.center.set_is_center(true);
|
||||
workspace.center.mark_positions(cx);
|
||||
|
||||
if let Some(active_pane) = active_pane {
|
||||
workspace.set_active_pane(&active_pane, window, cx);
|
||||
cx.focus_self(window);
|
||||
@@ -6309,6 +6326,7 @@ impl Workspace {
|
||||
left_dock.resize_active_panel(Some(size), window, cx);
|
||||
}
|
||||
});
|
||||
self.clamp_utility_pane_widths(window, cx);
|
||||
}
|
||||
|
||||
fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
|
||||
@@ -6331,6 +6349,7 @@ impl Workspace {
|
||||
right_dock.resize_active_panel(Some(size), window, cx);
|
||||
}
|
||||
});
|
||||
self.clamp_utility_pane_widths(window, cx);
|
||||
}
|
||||
|
||||
fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) {
|
||||
@@ -6345,6 +6364,42 @@ impl Workspace {
|
||||
bottom_dock.resize_active_panel(Some(size), window, cx);
|
||||
}
|
||||
});
|
||||
self.clamp_utility_pane_widths(window, cx);
|
||||
}
|
||||
|
||||
fn max_utility_pane_width(&self, window: &Window, cx: &App) -> Pixels {
|
||||
let left_dock_width = self
|
||||
.left_dock
|
||||
.read(cx)
|
||||
.active_panel_size(window, cx)
|
||||
.unwrap_or(px(0.0));
|
||||
let right_dock_width = self
|
||||
.right_dock
|
||||
.read(cx)
|
||||
.active_panel_size(window, cx)
|
||||
.unwrap_or(px(0.0));
|
||||
let center_pane_width = self.bounds.size.width - left_dock_width - right_dock_width;
|
||||
center_pane_width - px(10.0)
|
||||
}
|
||||
|
||||
fn clamp_utility_pane_widths(&mut self, window: &mut Window, cx: &mut App) {
|
||||
let max_width = self.max_utility_pane_width(window, cx);
|
||||
|
||||
// Clamp left slot utility pane if it exists
|
||||
if let Some(handle) = self.utility_pane(UtilityPaneSlot::Left) {
|
||||
let current_width = handle.width(cx);
|
||||
if current_width > max_width {
|
||||
handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx);
|
||||
}
|
||||
}
|
||||
|
||||
// Clamp right slot utility pane if it exists
|
||||
if let Some(handle) = self.utility_pane(UtilityPaneSlot::Right) {
|
||||
let current_width = handle.width(cx);
|
||||
if current_width > max_width {
|
||||
handle.set_width(Some(max_width.max(UTILITY_PANE_MIN_WIDTH)), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_edit_predictions_all_files(
|
||||
@@ -6812,6 +6867,34 @@ impl Render for Workspace {
|
||||
}
|
||||
},
|
||||
))
|
||||
.on_drag_move(cx.listener(
|
||||
move |workspace,
|
||||
e: &DragMoveEvent<DraggedUtilityPane>,
|
||||
window,
|
||||
cx| {
|
||||
let slot = e.drag(cx).0;
|
||||
match slot {
|
||||
UtilityPaneSlot::Left => {
|
||||
let left_dock_width = workspace.left_dock.read(cx)
|
||||
.active_panel_size(window, cx)
|
||||
.unwrap_or(gpui::px(0.0));
|
||||
let new_width = e.event.position.x
|
||||
- workspace.bounds.left()
|
||||
- left_dock_width;
|
||||
workspace.resize_utility_pane(slot, new_width, window, cx);
|
||||
}
|
||||
UtilityPaneSlot::Right => {
|
||||
let right_dock_width = workspace.right_dock.read(cx)
|
||||
.active_panel_size(window, cx)
|
||||
.unwrap_or(gpui::px(0.0));
|
||||
let new_width = workspace.bounds.right()
|
||||
- e.event.position.x
|
||||
- right_dock_width;
|
||||
workspace.resize_utility_pane(slot, new_width, window, cx);
|
||||
}
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.child({
|
||||
match bottom_dock_layout {
|
||||
@@ -6831,6 +6914,15 @@ impl Render for Workspace {
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -6872,6 +6964,15 @@ impl Render for Workspace {
|
||||
),
|
||||
),
|
||||
)
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.children(self.render_dock(
|
||||
DockPosition::Right,
|
||||
&self.right_dock,
|
||||
@@ -6902,6 +7003,15 @@ impl Render for Workspace {
|
||||
.flex_row()
|
||||
.flex_1()
|
||||
.children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -6929,6 +7039,13 @@ impl Render for Workspace {
|
||||
.when_some(paddings.1, |this, p| this.child(p.border_l_1())),
|
||||
)
|
||||
)
|
||||
.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
@@ -6953,6 +7070,15 @@ impl Render for Workspace {
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -6991,6 +7117,15 @@ impl Render for Workspace {
|
||||
.when_some(paddings.1, |this, p| this.child(p.border_l_1())),
|
||||
)
|
||||
)
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
|
||||
)
|
||||
.child(
|
||||
@@ -7010,6 +7145,13 @@ impl Render for Workspace {
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.when_some(self.utility_pane(UtilityPaneSlot::Left), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Left, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
@@ -7047,6 +7189,15 @@ impl Render for Workspace {
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.when(cx.has_flag::<AgentV2FeatureFlag>(), |this| {
|
||||
this.when_some(self.utility_pane(UtilityPaneSlot::Right), |this, pane| {
|
||||
this.when(pane.expanded(cx), |this| {
|
||||
this.child(
|
||||
UtilityPaneFrame::new(UtilityPaneSlot::Right, pane.box_clone(), cx)
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
.children(self.render_dock(
|
||||
DockPosition::Right,
|
||||
&self.right_dock,
|
||||
|
||||
@@ -26,6 +26,7 @@ acp_tools.workspace = true
|
||||
activity_indicator.workspace = true
|
||||
agent_settings.workspace = true
|
||||
agent_ui.workspace = true
|
||||
agent_ui_v2.workspace = true
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
assets.workspace = true
|
||||
|
||||
@@ -597,6 +597,7 @@ pub fn main() {
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
agent_ui_v2::agents_panel::init(cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
recent_projects::init(cx);
|
||||
|
||||
|
||||
+91
-42
@@ -10,6 +10,7 @@ mod quick_action_bar;
|
||||
pub(crate) mod windows_only_instance;
|
||||
|
||||
use agent_ui::{AgentDiffToolbar, AgentPanelDelegate};
|
||||
use agent_ui_v2::agents_panel::AgentsPanel;
|
||||
use anyhow::Context as _;
|
||||
pub use app_menus::*;
|
||||
use assets::Assets;
|
||||
@@ -81,8 +82,9 @@ use vim_mode_setting::VimModeSetting;
|
||||
use workspace::notifications::{
|
||||
NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification,
|
||||
};
|
||||
use workspace::utility_pane::utility_slot_for_dock_position;
|
||||
use workspace::{
|
||||
AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,
|
||||
AppState, NewFile, NewWindow, OpenLog, Panel, Toast, Workspace, WorkspaceSettings,
|
||||
create_and_open_local_file, notifications::simple_message_notification::MessageNotification,
|
||||
open_new,
|
||||
};
|
||||
@@ -679,7 +681,8 @@ fn initialize_panels(
|
||||
add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()),
|
||||
add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()),
|
||||
add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()),
|
||||
initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err())
|
||||
initialize_agent_panel(workspace_handle.clone(), prompt_builder, cx.clone()).map(|r| r.log_err()),
|
||||
initialize_agents_panel(workspace_handle, cx.clone()).map(|r| r.log_err())
|
||||
);
|
||||
|
||||
anyhow::Ok(())
|
||||
@@ -687,58 +690,65 @@ fn initialize_panels(
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn setup_or_teardown_ai_panel<P: Panel>(
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
load_panel: impl FnOnce(
|
||||
WeakEntity<Workspace>,
|
||||
AsyncWindowContext,
|
||||
) -> Task<anyhow::Result<Entity<P>>>
|
||||
+ 'static,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let disable_ai = SettingsStore::global(cx)
|
||||
.get::<DisableAiSettings>(None)
|
||||
.disable_ai
|
||||
|| cfg!(test);
|
||||
let existing_panel = workspace.panel::<P>(cx);
|
||||
|
||||
match (disable_ai, existing_panel) {
|
||||
(false, None) => cx.spawn_in(window, async move |workspace, cx| {
|
||||
let panel = load_panel(workspace.clone(), cx.clone()).await?;
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let disable_ai = SettingsStore::global(cx)
|
||||
.get::<DisableAiSettings>(None)
|
||||
.disable_ai;
|
||||
let have_panel = workspace.panel::<P>(cx).is_some();
|
||||
if !disable_ai && !have_panel {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
(true, Some(existing_panel)) => {
|
||||
workspace.remove_panel::<P>(&existing_panel, window, cx);
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
_ => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
async fn initialize_agent_panel(
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
fn setup_or_teardown_agent_panel(
|
||||
workspace: &mut Workspace,
|
||||
prompt_builder: Arc<PromptBuilder>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
let disable_ai = SettingsStore::global(cx)
|
||||
.get::<DisableAiSettings>(None)
|
||||
.disable_ai
|
||||
|| cfg!(test);
|
||||
let existing_panel = workspace.panel::<agent_ui::AgentPanel>(cx);
|
||||
match (disable_ai, existing_panel) {
|
||||
(false, None) => cx.spawn_in(window, async move |workspace, cx| {
|
||||
let panel =
|
||||
agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone())
|
||||
.await?;
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let disable_ai = SettingsStore::global(cx)
|
||||
.get::<DisableAiSettings>(None)
|
||||
.disable_ai;
|
||||
let have_panel = workspace.panel::<agent_ui::AgentPanel>(cx).is_some();
|
||||
if !disable_ai && !have_panel {
|
||||
workspace.add_panel(panel, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
(true, Some(existing_panel)) => {
|
||||
workspace.remove_panel::<agent_ui::AgentPanel>(&existing_panel, window, cx);
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
_ => Task::ready(Ok(())),
|
||||
}
|
||||
}
|
||||
|
||||
workspace_handle
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
|
||||
agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
workspace_handle.update_in(&mut cx, |workspace, window, cx| {
|
||||
cx.observe_global_in::<SettingsStore>(window, {
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
|
||||
let prompt_builder = prompt_builder.clone();
|
||||
move |workspace, window, cx| {
|
||||
setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, move |workspace, cx| {
|
||||
agent_ui::AgentPanel::load(workspace, prompt_builder, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach();
|
||||
|
||||
@@ -763,6 +773,31 @@ async fn initialize_agent_panel(
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
async fn initialize_agents_panel(
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
mut cx: AsyncWindowContext,
|
||||
) -> anyhow::Result<()> {
|
||||
workspace_handle
|
||||
.update_in(&mut cx, |workspace, window, cx| {
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
|
||||
AgentsPanel::load(workspace, cx)
|
||||
})
|
||||
})?
|
||||
.await?;
|
||||
|
||||
workspace_handle.update_in(&mut cx, |_workspace, window, cx| {
|
||||
cx.observe_global_in::<SettingsStore>(window, move |workspace, window, cx| {
|
||||
setup_or_teardown_ai_panel(workspace, window, cx, |workspace, cx| {
|
||||
AgentsPanel::load(workspace, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.detach();
|
||||
})?;
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn register_actions(
|
||||
app_state: Arc<AppState>,
|
||||
workspace: &mut Workspace,
|
||||
@@ -1052,6 +1087,18 @@ fn register_actions(
|
||||
workspace.toggle_panel_focus::<TerminalPanel>(window, cx);
|
||||
},
|
||||
)
|
||||
.register_action(
|
||||
|workspace: &mut Workspace,
|
||||
_: &zed_actions::agent::ToggleAgentPane,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>| {
|
||||
if let Some(panel) = workspace.panel::<AgentsPanel>(cx) {
|
||||
let position = panel.read(cx).position(window, cx);
|
||||
let slot = utility_slot_for_dock_position(position);
|
||||
workspace.toggle_utility_pane(slot, window, cx);
|
||||
}
|
||||
},
|
||||
)
|
||||
.register_action({
|
||||
let app_state = Arc::downgrade(&app_state);
|
||||
move |_, _: &NewWindow, _, cx| {
|
||||
@@ -4714,6 +4761,7 @@ mod tests {
|
||||
"action",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agents",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
"app_menu",
|
||||
"assistant",
|
||||
@@ -4941,6 +4989,7 @@ mod tests {
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
agent_ui_v2::agents_panel::init(cx);
|
||||
repl::init(app_state.fs.clone(), cx);
|
||||
repl::notebook::init(cx);
|
||||
tasks_ui::init(cx);
|
||||
|
||||
@@ -350,6 +350,8 @@ pub mod agent {
|
||||
AddSelectionToThread,
|
||||
/// Resets the agent panel zoom levels (agent UI and buffer font sizes).
|
||||
ResetAgentZoom,
|
||||
/// Toggles the utility/agent pane open/closed state.
|
||||
ToggleAgentPane,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user