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:
authored and GitHub committed 2025-12-15 10:14:15 +00:00
1 parent 213c1b210b
commit 75c71a9fc5
28 files changed
+2452 -98

No files matched your search

Generated
+33
View File
@@ -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",
+2
View File
@@ -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" }
+2
View File
@@ -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.
+3 -1
View File
@@ -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()),
+16 -3
View File
@@ -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,
+40
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
LICENSE-GPL
+290
View File
@@ -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)
}
}
+4
View File
@@ -0,0 +1,4 @@
mod agent_thread_pane;
mod thread_history;
pub mod agents_panel;
+438
View File
@@ -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())
}
}
+735
View File
@@ -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);
}
}
+1 -1
View File
@@ -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))
})
});
}
+4 -4
View File
@@ -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);
}
}
+2 -2
View File
@@ -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);
+6
View File
@@ -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
+10 -5
View File
@@ -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);
+36 -12
View File
@@ -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),
)
}),
)
},
)
}
}
+1
View File
@@ -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
+101 -1
View File
@@ -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();
+101 -4
View File
@@ -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!"
+78 -7
View File
@@ -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>,
+282
View File
@@ -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()
}
}
+166 -15
View File
@@ -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,
+1
View File
@@ -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
+1
View File
@@ -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
View File
@@ -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);
+2
View File
@@ -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,
]
);
}