agent: Improve sidebar design and behavior (#51763)

- Selection/focus improvements (reduces flickers, move selection more
correctly throughout the list)
- Adds open folder button in the sidebar header
- Fixes sidebar header design, including the thread view one, too
- Fixes behavior of cmd-n when focused in the sidebar
- Changes the design for the "new thread" button in the sidebar and adds
a preview of the prompt on it
- Rename items in the "start thread in" dropdown

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
Danilo Leal 2026-03-17 20:44:20 -03:00 committed by GitHub
parent 65b80ff689
commit 9f3e3be65f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 915 additions and 549 deletions

1
Cargo.lock generated
View file

@ -15870,6 +15870,7 @@ dependencies = [
"pretty_assertions", "pretty_assertions",
"project", "project",
"prompt_store", "prompt_store",
"recent_projects",
"serde_json", "serde_json",
"settings", "settings",
"theme", "theme",

View file

@ -674,7 +674,7 @@
"context": "ThreadsSidebar", "context": "ThreadsSidebar",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow", "ctrl-n": "agents_sidebar::NewThreadInGroup",
"left": "agents_sidebar::CollapseSelectedEntry", "left": "agents_sidebar::CollapseSelectedEntry",
"right": "agents_sidebar::ExpandSelectedEntry", "right": "agents_sidebar::ExpandSelectedEntry",
"enter": "menu::Confirm", "enter": "menu::Confirm",

View file

@ -742,7 +742,7 @@
"context": "ThreadsSidebar", "context": "ThreadsSidebar",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"cmd-n": "multi_workspace::NewWorkspaceInWindow", "cmd-n": "agents_sidebar::NewThreadInGroup",
"left": "agents_sidebar::CollapseSelectedEntry", "left": "agents_sidebar::CollapseSelectedEntry",
"right": "agents_sidebar::ExpandSelectedEntry", "right": "agents_sidebar::ExpandSelectedEntry",
"enter": "menu::Confirm", "enter": "menu::Confirm",

View file

@ -678,7 +678,7 @@
"context": "ThreadsSidebar", "context": "ThreadsSidebar",
"use_key_equivalents": true, "use_key_equivalents": true,
"bindings": { "bindings": {
"ctrl-n": "multi_workspace::NewWorkspaceInWindow", "ctrl-n": "agents_sidebar::NewThreadInGroup",
"left": "agents_sidebar::CollapseSelectedEntry", "left": "agents_sidebar::CollapseSelectedEntry",
"right": "agents_sidebar::ExpandSelectedEntry", "right": "agents_sidebar::ExpandSelectedEntry",
"enter": "menu::Confirm", "enter": "menu::Confirm",

View file

@ -601,8 +601,8 @@ impl From<Agent> for AgentType {
impl StartThreadIn { impl StartThreadIn {
fn label(&self) -> SharedString { fn label(&self) -> SharedString {
match self { match self {
Self::LocalProject => "Current Project".into(), Self::LocalProject => "Current Worktree".into(),
Self::NewWorktree => "New Worktree".into(), Self::NewWorktree => "New Git Worktree".into(),
} }
} }
} }
@ -1951,6 +1951,21 @@ impl AgentPanel {
self.background_threads.contains_key(session_id) self.background_threads.contains_key(session_id)
} }
pub fn cancel_thread(&self, session_id: &acp::SessionId, cx: &mut Context<Self>) -> bool {
let conversation_views = self
.active_conversation_view()
.into_iter()
.chain(self.background_threads.values());
for conversation_view in conversation_views {
if let Some(thread_view) = conversation_view.read(cx).thread_view(session_id) {
thread_view.update(cx, |view, cx| view.cancel_generation(cx));
return true;
}
}
false
}
/// active thread plus any background threads that are still running or /// active thread plus any background threads that are still running or
/// completed but unseen. /// completed but unseen.
pub fn parent_threads(&self, cx: &App) -> Vec<Entity<ThreadView>> { pub fn parent_threads(&self, cx: &App) -> Vec<Entity<ThreadView>> {
@ -3551,7 +3566,7 @@ impl AgentPanel {
menu.header("Start Thread In…") menu.header("Start Thread In…")
.item( .item(
ContextMenuEntry::new("Current Project") ContextMenuEntry::new("Current Worktree")
.toggleable(IconPosition::End, is_local_selected) .toggleable(IconPosition::End, is_local_selected)
.documentation_aside(documentation_side, move |_| { .documentation_aside(documentation_side, move |_| {
HoldForDefault::new(is_local_default) HoldForDefault::new(is_local_default)
@ -3579,7 +3594,7 @@ impl AgentPanel {
}), }),
) )
.item({ .item({
let entry = ContextMenuEntry::new("New Worktree") let entry = ContextMenuEntry::new("New Git Worktree")
.toggleable(IconPosition::End, is_new_worktree_selected) .toggleable(IconPosition::End, is_new_worktree_selected)
.disabled(new_worktree_disabled) .disabled(new_worktree_disabled)
.handler({ .handler({

View file

@ -21,6 +21,7 @@ use theme::ActiveTheme;
use ui::{ use ui::{
ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, HighlightedLabel, ListItem, ButtonLike, CommonAnimationExt, ContextMenu, ContextMenuEntry, HighlightedLabel, ListItem,
PopoverMenu, PopoverMenuHandle, Tab, TintColor, Tooltip, WithScrollbar, prelude::*, PopoverMenu, PopoverMenuHandle, Tab, TintColor, Tooltip, WithScrollbar, prelude::*,
utils::platform_title_bar_height,
}; };
use util::ResultExt as _; use util::ResultExt as _;
use zed_actions::editor::{MoveDown, MoveUp}; use zed_actions::editor::{MoveDown, MoveUp};
@ -676,32 +677,56 @@ impl ThreadsArchiveView {
}) })
} }
fn render_header(&self, cx: &mut Context<Self>) -> impl IntoElement { fn render_header(&self, window: &Window, cx: &mut Context<Self>) -> impl IntoElement {
let has_query = !self.filter_editor.read(cx).text(cx).is_empty(); let has_query = !self.filter_editor.read(cx).text(cx).is_empty();
let traffic_lights = cfg!(target_os = "macos") && !window.is_fullscreen();
let header_height = platform_title_bar_height(window);
h_flex() v_flex()
.h(Tab::container_height(cx))
.px_1()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border)
.child( .child(
h_flex() h_flex()
.flex_1() .h(header_height)
.w_full() .mt_px()
.gap_1p5() .pb_px()
.when(traffic_lights, |this| {
this.pl(px(ui::utils::TRAFFIC_LIGHT_PADDING))
})
.pr_1p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.justify_between()
.child( .child(
IconButton::new("back", IconName::ArrowLeft) h_flex()
.icon_size(IconSize::Small) .gap_1p5()
.tooltip(Tooltip::text("Back to Sidebar")) .child(
.on_click(cx.listener(|this, _, window, cx| { IconButton::new("back", IconName::ArrowLeft)
this.go_back(window, cx); .icon_size(IconSize::Small)
})), .tooltip(Tooltip::text("Back to Sidebar"))
.on_click(cx.listener(|this, _, window, cx| {
this.go_back(window, cx);
})),
)
.child(Label::new("Threads Archive").size(LabelSize::Small).mb_px()),
)
.child(self.render_agent_picker(cx)),
)
.child(
h_flex()
.h(Tab::container_height(cx))
.p_2()
.pr_1p5()
.gap_1p5()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
Icon::new(IconName::MagnifyingGlass)
.size(IconSize::Small)
.color(Color::Muted),
) )
.child(self.filter_editor.clone()) .child(self.filter_editor.clone())
.when(has_query, |this| { .when(has_query, |this| {
this.border_r_1().child( this.child(
IconButton::new("clear_archive_filter", IconName::Close) IconButton::new("clear_filter", IconName::Close)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.tooltip(Tooltip::text("Clear Search")) .tooltip(Tooltip::text("Clear Search"))
.on_click(cx.listener(|this, _, window, cx| { .on_click(cx.listener(|this, _, window, cx| {
@ -711,7 +736,6 @@ impl ThreadsArchiveView {
) )
}), }),
) )
.child(self.render_agent_picker(cx))
} }
} }
@ -783,8 +807,7 @@ impl Render for ThreadsArchiveView {
.on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::confirm))
.on_action(cx.listener(Self::remove_selected_thread)) .on_action(cx.listener(Self::remove_selected_thread))
.size_full() .size_full()
.bg(cx.theme().colors().surface_background) .child(self.render_header(window, cx))
.child(self.render_header(cx))
.child(content) .child(content)
} }
} }

View file

@ -28,6 +28,7 @@ fs.workspace = true
gpui.workspace = true gpui.workspace = true
menu.workspace = true menu.workspace = true
project.workspace = true project.workspace = true
recent_projects.workspace = true
settings.workspace = true settings.workspace = true
theme.workspace = true theme.workspace = true
ui.workspace = true ui.workspace = true

File diff suppressed because it is too large Load diff

View file

@ -41,8 +41,8 @@ use std::sync::Arc;
use theme::ActiveTheme; use theme::ActiveTheme;
use title_bar_settings::TitleBarSettings; use title_bar_settings::TitleBarSettings;
use ui::{ use ui::{
Avatar, ButtonLike, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Avatar, ButtonLike, ContextMenu, Divider, IconWithIndicator, Indicator, PopoverMenu,
TintColor, Tooltip, prelude::*, utils::platform_title_bar_height, PopoverMenuHandle, TintColor, Tooltip, prelude::*, utils::platform_title_bar_height,
}; };
use update_version::UpdateVersion; use update_version::UpdateVersion;
use util::ResultExt; use util::ResultExt;
@ -169,6 +169,7 @@ impl Render for TitleBar {
children.push( children.push(
h_flex() h_flex()
.h_full()
.gap_0p5() .gap_0p5()
.map(|title_bar| { .map(|title_bar| {
let mut render_project_items = title_bar_settings.show_branch_name let mut render_project_items = title_bar_settings.show_branch_name
@ -705,23 +706,29 @@ impl TitleBar {
let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications(); let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
Some( Some(
IconButton::new( h_flex()
"toggle-workspace-sidebar", .h_full()
IconName::ThreadsSidebarLeftClosed, .gap_0p5()
) .child(
.icon_size(IconSize::Small) IconButton::new(
.when(has_notifications, |button| { "toggle-workspace-sidebar",
button IconName::ThreadsSidebarLeftClosed,
.indicator(Indicator::dot().color(Color::Accent)) )
.indicator_border_color(Some(cx.theme().colors().title_bar_background)) .icon_size(IconSize::Small)
}) .when(has_notifications, |button| {
.tooltip(move |_, cx| { button
Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx) .indicator(Indicator::dot().color(Color::Accent))
}) .indicator_border_color(Some(cx.theme().colors().title_bar_background))
.on_click(|_, window, cx| { })
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx); .tooltip(move |_, cx| {
}) Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
.into_any_element(), })
.on_click(|_, window, cx| {
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
}),
)
.child(Divider::vertical().color(ui::DividerColor::Border))
.into_any_element(),
) )
} }
@ -741,6 +748,14 @@ impl TitleBar {
"Open Recent Project".to_string() "Open Recent Project".to_string()
}; };
let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
if is_sidebar_open {
return self
.render_project_name_with_sidebar_popover(display_name, is_project_selected, cx)
.into_any_element();
}
let focus_handle = workspace let focus_handle = workspace
.upgrade() .upgrade()
.map(|w| w.read(cx).focus_handle(cx)) .map(|w| w.read(cx).focus_handle(cx))
@ -782,6 +797,53 @@ impl TitleBar {
.into_any_element() .into_any_element()
} }
/// When the sidebar is open, the title bar's project name button becomes a
/// plain button that toggles the sidebar's popover (so the popover is always
/// anchored to the sidebar). Both buttons show their selected state together.
fn render_project_name_with_sidebar_popover(
&self,
display_name: String,
is_project_selected: bool,
cx: &mut Context<Self>,
) -> impl IntoElement {
let multi_workspace = self.multi_workspace.clone();
let is_popover_deployed = multi_workspace
.as_ref()
.and_then(|mw| mw.upgrade())
.map(|mw| mw.read(cx).is_recent_projects_popover_deployed(cx))
.unwrap_or(false);
Button::new("project_name_trigger", display_name)
.label_size(LabelSize::Small)
.when(self.worktree_count(cx) > 1, |this| {
this.end_icon(
Icon::new(IconName::ChevronDown)
.size(IconSize::XSmall)
.color(Color::Muted),
)
})
.toggle_state(is_popover_deployed)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.when(!is_project_selected, |s| s.color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::for_action(
"Recent Projects",
&zed_actions::OpenRecent {
create_new_window: false,
},
cx,
)
})
.on_click(move |_, window, cx| {
if let Some(mw) = multi_workspace.as_ref().and_then(|mw| mw.upgrade()) {
mw.update(cx, |mw, cx| {
mw.toggle_recent_projects_popover(window, cx);
});
}
})
}
pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> { pub fn render_project_branch(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
let effective_worktree = self.effective_active_worktree(cx)?; let effective_worktree = self.effective_active_worktree(cx)?;
let repository = self.get_repository_for_worktree(&effective_worktree, cx)?; let repository = self.get_repository_for_worktree(&effective_worktree, cx)?;

View file

@ -1,7 +1,5 @@
mod configured_api_card; mod configured_api_card;
mod thread_item; mod thread_item;
mod thread_sidebar_toggle;
pub use configured_api_card::*; pub use configured_api_card::*;
pub use thread_item::*; pub use thread_item::*;
pub use thread_sidebar_toggle::*;

View file

@ -3,7 +3,10 @@ use crate::{
IconDecorationKind, prelude::*, IconDecorationKind, prelude::*,
}; };
use gpui::{Animation, AnimationExt, AnyView, ClickEvent, Hsla, SharedString, pulsating_between}; use gpui::{
Animation, AnimationExt, AnyView, ClickEvent, Hsla, MouseButton, SharedString,
pulsating_between,
};
use std::time::Duration; use std::time::Duration;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
@ -36,6 +39,7 @@ pub struct ThreadItem {
worktree_highlight_positions: Vec<usize>, worktree_highlight_positions: Vec<usize>,
on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>, on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>, on_hover: Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>,
title_label_color: Option<Color>,
action_slot: Option<AnyElement>, action_slot: Option<AnyElement>,
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>, tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
} }
@ -62,6 +66,7 @@ impl ThreadItem {
worktree_highlight_positions: Vec::new(), worktree_highlight_positions: Vec::new(),
on_click: None, on_click: None,
on_hover: Box::new(|_, _, _| {}), on_hover: Box::new(|_, _, _| {}),
title_label_color: None,
action_slot: None, action_slot: None,
tooltip: None, tooltip: None,
} }
@ -155,6 +160,11 @@ impl ThreadItem {
self self
} }
pub fn title_label_color(mut self, color: Color) -> Self {
self.title_label_color = Some(color);
self
}
pub fn action_slot(mut self, element: impl IntoElement) -> Self { pub fn action_slot(mut self, element: impl IntoElement) -> Self {
self.action_slot = Some(element.into_any_element()); self.action_slot = Some(element.into_any_element());
self self
@ -230,7 +240,7 @@ impl RenderOnce for ThreadItem {
let title = self.title; let title = self.title;
let highlight_positions = self.highlight_positions; let highlight_positions = self.highlight_positions;
let title_label = if self.generating_title { let title_label = if self.generating_title {
Label::new("New Thread…") Label::new(title)
.color(Color::Muted) .color(Color::Muted)
.with_animation( .with_animation(
"generating-title", "generating-title",
@ -241,15 +251,31 @@ impl RenderOnce for ThreadItem {
) )
.into_any_element() .into_any_element()
} else if highlight_positions.is_empty() { } else if highlight_positions.is_empty() {
Label::new(title).into_any_element() let label = Label::new(title);
let label = if let Some(color) = self.title_label_color {
label.color(color)
} else {
label
};
label.into_any_element()
} else { } else {
HighlightedLabel::new(title, highlight_positions).into_any_element() let label = HighlightedLabel::new(title, highlight_positions);
let label = if let Some(color) = self.title_label_color {
label.color(color)
} else {
label
};
label.into_any_element()
}; };
let b_bg = color
.title_bar_background
.blend(color.panel_background.opacity(0.8));
let base_bg = if self.selected { let base_bg = if self.selected {
color.element_active color.element_active
} else { } else {
color.panel_background b_bg
}; };
let gradient_overlay = let gradient_overlay =
@ -314,7 +340,15 @@ impl RenderOnce for ThreadItem {
.gradient_stop(0.75) .gradient_stop(0.75)
.group_name("thread-item"); .group_name("thread-item");
this.child(h_flex().relative().child(overlay).child(slot)) this.child(
h_flex()
.relative()
.on_mouse_down(MouseButton::Left, |_, _, cx| {
cx.stop_propagation()
})
.child(overlay)
.child(slot),
)
}) })
}), }),
) )

View file

@ -1,177 +0,0 @@
use gpui::{AnyView, ClickEvent};
use ui_macros::RegisterComponent;
use crate::prelude::*;
use crate::{IconButton, IconName, Tooltip};
#[derive(IntoElement, RegisterComponent)]
pub struct ThreadSidebarToggle {
sidebar_selected: bool,
thread_selected: bool,
flipped: bool,
sidebar_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
thread_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
on_sidebar_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
on_thread_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
}
impl ThreadSidebarToggle {
pub fn new() -> Self {
Self {
sidebar_selected: false,
thread_selected: false,
flipped: false,
sidebar_tooltip: None,
thread_tooltip: None,
on_sidebar_click: None,
on_thread_click: None,
}
}
pub fn sidebar_selected(mut self, selected: bool) -> Self {
self.sidebar_selected = selected;
self
}
pub fn thread_selected(mut self, selected: bool) -> Self {
self.thread_selected = selected;
self
}
pub fn flipped(mut self, flipped: bool) -> Self {
self.flipped = flipped;
self
}
pub fn sidebar_tooltip(
mut self,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
self.sidebar_tooltip = Some(Box::new(tooltip));
self
}
pub fn thread_tooltip(
mut self,
tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
) -> Self {
self.thread_tooltip = Some(Box::new(tooltip));
self
}
pub fn on_sidebar_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_sidebar_click = Some(Box::new(handler));
self
}
pub fn on_thread_click(
mut self,
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
) -> Self {
self.on_thread_click = Some(Box::new(handler));
self
}
}
impl RenderOnce for ThreadSidebarToggle {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let sidebar_icon = match (self.sidebar_selected, self.flipped) {
(true, false) => IconName::ThreadsSidebarLeftOpen,
(false, false) => IconName::ThreadsSidebarLeftClosed,
(true, true) => IconName::ThreadsSidebarRightOpen,
(false, true) => IconName::ThreadsSidebarRightClosed,
};
h_flex()
.min_w_0()
.rounded_sm()
.gap_px()
.border_1()
.border_color(cx.theme().colors().border)
.when(self.flipped, |this| this.flex_row_reverse())
.child(
IconButton::new("sidebar-toggle", sidebar_icon)
.icon_size(IconSize::Small)
.toggle_state(self.sidebar_selected)
.when_some(self.sidebar_tooltip, |this, tooltip| this.tooltip(tooltip))
.when_some(self.on_sidebar_click, |this, handler| {
this.on_click(handler)
}),
)
.child(div().h_4().w_px().bg(cx.theme().colors().border))
.child(
IconButton::new("thread-toggle", IconName::Thread)
.icon_size(IconSize::Small)
.toggle_state(self.thread_selected)
.when_some(self.thread_tooltip, |this, tooltip| this.tooltip(tooltip))
.when_some(self.on_thread_click, |this, handler| this.on_click(handler)),
)
}
}
impl Component for ThreadSidebarToggle {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let container = || div().p_2().bg(cx.theme().colors().status_bar_background);
let examples = vec![
single_example(
"Both Unselected",
container()
.child(ThreadSidebarToggle::new())
.into_any_element(),
),
single_example(
"Sidebar Selected",
container()
.child(ThreadSidebarToggle::new().sidebar_selected(true))
.into_any_element(),
),
single_example(
"Thread Selected",
container()
.child(ThreadSidebarToggle::new().thread_selected(true))
.into_any_element(),
),
single_example(
"Both Selected",
container()
.child(
ThreadSidebarToggle::new()
.sidebar_selected(true)
.thread_selected(true),
)
.into_any_element(),
),
single_example(
"Flipped",
container()
.child(
ThreadSidebarToggle::new()
.sidebar_selected(true)
.thread_selected(true)
.flipped(true),
)
.into_any_element(),
),
single_example(
"With Tooltips",
container()
.child(
ThreadSidebarToggle::new()
.sidebar_tooltip(Tooltip::text("Toggle Sidebar"))
.thread_tooltip(Tooltip::text("Toggle Thread")),
)
.into_any_element(),
),
];
Some(example_group(examples).into_any_element())
}
}

View file

@ -45,6 +45,8 @@ pub trait Sidebar: Focusable + Render + Sized {
fn width(&self, cx: &App) -> Pixels; fn width(&self, cx: &App) -> Pixels;
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>); fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
fn has_notifications(&self, cx: &App) -> bool; fn has_notifications(&self, cx: &App) -> bool;
fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App);
fn is_recent_projects_popover_deployed(&self) -> bool;
} }
pub trait SidebarHandle: 'static + Send + Sync { pub trait SidebarHandle: 'static + Send + Sync {
@ -55,6 +57,8 @@ pub trait SidebarHandle: 'static + Send + Sync {
fn has_notifications(&self, cx: &App) -> bool; fn has_notifications(&self, cx: &App) -> bool;
fn to_any(&self) -> AnyView; fn to_any(&self) -> AnyView;
fn entity_id(&self) -> EntityId; fn entity_id(&self) -> EntityId;
fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App);
fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool;
} }
#[derive(Clone)] #[derive(Clone)]
@ -95,6 +99,16 @@ impl<T: Sidebar> SidebarHandle for Entity<T> {
fn entity_id(&self) -> EntityId { fn entity_id(&self) -> EntityId {
Entity::entity_id(self) Entity::entity_id(self)
} }
fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
self.update(cx, |this, cx| {
this.toggle_recent_projects_popover(window, cx);
});
}
fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool {
self.read(cx).is_recent_projects_popover_deployed()
}
} }
pub struct MultiWorkspace { pub struct MultiWorkspace {
@ -167,6 +181,18 @@ impl MultiWorkspace {
.map_or(false, |s| s.has_notifications(cx)) .map_or(false, |s| s.has_notifications(cx))
} }
pub fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
if let Some(sidebar) = &self.sidebar {
sidebar.toggle_recent_projects_popover(window, cx);
}
}
pub fn is_recent_projects_popover_deployed(&self, cx: &App) -> bool {
self.sidebar
.as_ref()
.map_or(false, |s| s.is_recent_projects_popover_deployed(cx))
}
pub fn multi_workspace_enabled(&self, cx: &App) -> bool { pub fn multi_workspace_enabled(&self, cx: &App) -> bool {
cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
} }