mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Move threads sidebar into agent panel (#51241)
* [x] Put back persistence of sidebar open state * [x] when agent panel is docked right, put sidebar on the right side * [x] remove stale entries from `SidebarsByWindow` Release Notes: - N/A --------- Co-authored-by: Eric Holk <eric@zed.dev> Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com> Co-authored-by: Anthony Eid <hello@anthonyeid.me>
This commit is contained in:
parent
f0e301cea0
commit
b5666319b4
21 changed files with 524 additions and 1019 deletions
29
Cargo.lock
generated
29
Cargo.lock
generated
|
|
@ -15807,33 +15807,6 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "sidebar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp_thread",
|
||||
"agent",
|
||||
"agent-client-protocol",
|
||||
"agent_ui",
|
||||
"assistant_text_thread",
|
||||
"chrono",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language_model",
|
||||
"menu",
|
||||
"project",
|
||||
"recent_projects",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
|
|
@ -17660,7 +17633,6 @@ dependencies = [
|
|||
"client",
|
||||
"cloud_api_types",
|
||||
"db",
|
||||
"feature_flags",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"notifications",
|
||||
|
|
@ -21887,7 +21859,6 @@ dependencies = [
|
|||
"settings_profile_selector",
|
||||
"settings_ui",
|
||||
"shellexpand 2.1.2",
|
||||
"sidebar",
|
||||
"smol",
|
||||
"snippet_provider",
|
||||
"snippets_ui",
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ members = [
|
|||
"crates/settings_profile_selector",
|
||||
"crates/settings_ui",
|
||||
"crates/shell_command_parser",
|
||||
"crates/sidebar",
|
||||
"crates/snippet",
|
||||
"crates/snippet_provider",
|
||||
"crates/snippets_ui",
|
||||
|
|
@ -412,7 +411,6 @@ rules_library = { path = "crates/rules_library" }
|
|||
scheduler = { path = "crates/scheduler" }
|
||||
search = { path = "crates/search" }
|
||||
session = { path = "crates/session" }
|
||||
sidebar = { path = "crates/sidebar" }
|
||||
settings = { path = "crates/settings" }
|
||||
settings_content = { path = "crates/settings_content" }
|
||||
settings_json = { path = "crates/settings_json" }
|
||||
|
|
@ -907,7 +905,6 @@ refineable = { codegen-units = 1 }
|
|||
release_channel = { codegen-units = 1 }
|
||||
reqwest_client = { codegen-units = 1 }
|
||||
session = { codegen-units = 1 }
|
||||
sidebar = { codegen-units = 1 }
|
||||
snippet = { codegen-units = 1 }
|
||||
snippets_ui = { codegen-units = 1 }
|
||||
story = { codegen-units = 1 }
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ languages = { workspace = true, features = ["test-support"] }
|
|||
language_model = { workspace = true, "features" = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
|
||||
semver.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
|
|
|
|||
|
|
@ -65,9 +65,10 @@ use extension_host::ExtensionStore;
|
|||
use fs::Fs;
|
||||
use git::repository::validate_worktree_directory;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
|
||||
DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
|
||||
Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
|
||||
Action, Animation, AnimationExt, AnyElement, AnyView, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, DragMoveEvent, Entity, EventEmitter, ExternalPaths, FocusHandle,
|
||||
Focusable, KeyContext, MouseButton, Pixels, Subscription, Task, UpdateGlobal, WeakEntity,
|
||||
deferred, prelude::*, pulsating_between,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{ConfigurationError, LanguageModelRegistry};
|
||||
|
|
@ -79,15 +80,17 @@ use search::{BufferSearchBar, buffer_search};
|
|||
use settings::{Settings, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding,
|
||||
PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
|
||||
Button, ButtonLike, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, Indicator,
|
||||
KeyBinding, PopoverMenu, PopoverMenuHandle, SpinnerLabel, Tab, TintColor, Tooltip, prelude::*,
|
||||
utils::WithRemSize,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{
|
||||
CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
|
||||
WorkspaceId,
|
||||
CollaboratorId, DraggedSelection, DraggedSidebar, DraggedTab, FocusWorkspaceSidebar,
|
||||
MultiWorkspace, SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, ToggleZoom,
|
||||
ToolbarItemView, Workspace, WorkspaceId,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
multi_workspace_enabled,
|
||||
};
|
||||
use zed_actions::{
|
||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||
|
|
@ -99,6 +102,55 @@ const AGENT_PANEL_KEY: &str = "agent_panel";
|
|||
const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
|
||||
const DEFAULT_THREAD_TITLE: &str = "New Thread";
|
||||
|
||||
#[derive(Default)]
|
||||
struct SidebarsByWindow(
|
||||
collections::HashMap<gpui::WindowId, gpui::WeakEntity<crate::sidebar::Sidebar>>,
|
||||
);
|
||||
|
||||
impl gpui::Global for SidebarsByWindow {}
|
||||
|
||||
pub(crate) fn sidebar_is_open(window: &Window, cx: &App) -> bool {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return false;
|
||||
}
|
||||
let window_id = window.window_handle().window_id();
|
||||
cx.try_global::<SidebarsByWindow>()
|
||||
.and_then(|sidebars| sidebars.0.get(&window_id)?.upgrade())
|
||||
.is_some_and(|sidebar| sidebar.read(cx).is_open())
|
||||
}
|
||||
|
||||
fn find_or_create_sidebar_for_window(
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Option<Entity<crate::sidebar::Sidebar>> {
|
||||
let window_id = window.window_handle().window_id();
|
||||
let multi_workspace = window.root::<MultiWorkspace>().flatten()?;
|
||||
|
||||
if !cx.has_global::<SidebarsByWindow>() {
|
||||
cx.set_global(SidebarsByWindow::default());
|
||||
}
|
||||
|
||||
cx.global_mut::<SidebarsByWindow>()
|
||||
.0
|
||||
.retain(|_, weak| weak.upgrade().is_some());
|
||||
|
||||
let existing = cx
|
||||
.global::<SidebarsByWindow>()
|
||||
.0
|
||||
.get(&window_id)
|
||||
.and_then(|weak| weak.upgrade());
|
||||
|
||||
if let Some(sidebar) = existing {
|
||||
return Some(sidebar);
|
||||
}
|
||||
|
||||
let sidebar = cx.new(|cx| crate::sidebar::Sidebar::new(multi_workspace, window, cx));
|
||||
cx.global_mut::<SidebarsByWindow>()
|
||||
.0
|
||||
.insert(window_id, sidebar.downgrade());
|
||||
Some(sidebar)
|
||||
}
|
||||
|
||||
fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
|
||||
let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
|
||||
let key = i64::from(workspace_id).to_string();
|
||||
|
|
@ -424,6 +476,30 @@ pub fn init(cx: &mut App) {
|
|||
panel.set_start_thread_in(action, cx);
|
||||
});
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &ToggleWorkspaceSidebar, window, cx| {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(sidebar) = panel.read(cx).sidebar.clone() {
|
||||
sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.toggle(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &FocusWorkspaceSidebar, window, cx| {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
if let Some(sidebar) = panel.read(cx).sidebar.clone() {
|
||||
sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.focus_or_unfocus(workspace, window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
)
|
||||
|
|
@ -820,6 +896,7 @@ pub struct AgentPanel {
|
|||
last_configuration_error_telemetry: Option<String>,
|
||||
on_boarding_upsell_dismissed: AtomicBool,
|
||||
_active_view_observation: Option<Subscription>,
|
||||
pub(crate) sidebar: Option<Entity<crate::sidebar::Sidebar>>,
|
||||
}
|
||||
|
||||
impl AgentPanel {
|
||||
|
|
@ -991,7 +1068,6 @@ impl AgentPanel {
|
|||
let client = workspace.client().clone();
|
||||
let workspace_id = workspace.database_id();
|
||||
let workspace = workspace.weak_handle();
|
||||
|
||||
let context_server_registry =
|
||||
cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
|
||||
|
||||
|
|
@ -1149,10 +1225,17 @@ impl AgentPanel {
|
|||
last_configuration_error_telemetry: None,
|
||||
on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()),
|
||||
_active_view_observation: None,
|
||||
sidebar: None,
|
||||
};
|
||||
|
||||
// Initial sync of agent servers from extensions
|
||||
panel.sync_agent_servers_from_extensions(cx);
|
||||
|
||||
cx.defer_in(window, move |this, window, cx| {
|
||||
this.sidebar = find_or_create_sidebar_for_window(window, cx);
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
panel
|
||||
}
|
||||
|
||||
|
|
@ -3526,9 +3609,109 @@ impl AgentPanel {
|
|||
})
|
||||
}
|
||||
|
||||
fn sidebar_info(&self, cx: &App) -> Option<(AnyView, Pixels, bool)> {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return None;
|
||||
}
|
||||
let sidebar = self.sidebar.as_ref()?;
|
||||
let is_open = sidebar.read(cx).is_open();
|
||||
let width = sidebar.read(cx).width(cx);
|
||||
let view: AnyView = sidebar.clone().into();
|
||||
Some((view, width, is_open))
|
||||
}
|
||||
|
||||
fn render_sidebar_toggle(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return None;
|
||||
}
|
||||
let sidebar = self.sidebar.as_ref()?;
|
||||
let sidebar_read = sidebar.read(cx);
|
||||
if sidebar_read.is_open() {
|
||||
return None;
|
||||
}
|
||||
let has_notifications = sidebar_read.has_notifications(cx);
|
||||
|
||||
Some(
|
||||
IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
|
||||
.icon_size(IconSize::Small)
|
||||
.when(has_notifications, |button| {
|
||||
button
|
||||
.indicator(Indicator::dot().color(Color::Accent))
|
||||
.indicator_border_color(Some(cx.theme().colors().tab_bar_background))
|
||||
})
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_sidebar(&self, cx: &Context<Self>) -> Option<AnyElement> {
|
||||
let (sidebar_view, sidebar_width, is_open) = self.sidebar_info(cx)?;
|
||||
if !is_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
|
||||
let sidebar = self.sidebar.as_ref()?.downgrade();
|
||||
|
||||
let resize_handle = deferred(
|
||||
div()
|
||||
.id("sidebar-resize-handle")
|
||||
.absolute()
|
||||
.when(docked_right, |this| {
|
||||
this.left(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
|
||||
})
|
||||
.when(!docked_right, |this| {
|
||||
this.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
|
||||
})
|
||||
.top(px(0.))
|
||||
.h_full()
|
||||
.w(SIDEBAR_RESIZE_HANDLE_SIZE)
|
||||
.cursor_col_resize()
|
||||
.on_drag(DraggedSidebar, |dragged, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new(|_| dragged.clone())
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, move |event, _, cx| {
|
||||
if event.click_count == 2 {
|
||||
sidebar
|
||||
.update(cx, |sidebar, cx| {
|
||||
sidebar.set_width(None, cx);
|
||||
})
|
||||
.ok();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
})
|
||||
.occlude(),
|
||||
);
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id("sidebar-container")
|
||||
.relative()
|
||||
.h_full()
|
||||
.w(sidebar_width)
|
||||
.flex_shrink_0()
|
||||
.when(docked_right, |this| this.border_l_1())
|
||||
.when(!docked_right, |this| this.border_r_1())
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(sidebar_view)
|
||||
.child(resize_handle)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
|
||||
|
||||
let (selected_agent_custom_icon, selected_agent_label) =
|
||||
if let AgentType::Custom { name, .. } = &self.selected_agent {
|
||||
|
|
@ -3991,6 +4174,9 @@ impl AgentPanel {
|
|||
.size_full()
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.pl(DynamicSpacing::Base04.rems(cx))
|
||||
.when(!docked_right, |this| {
|
||||
this.children(self.render_sidebar_toggle(cx))
|
||||
})
|
||||
.child(agent_selector_menu)
|
||||
.child(self.render_start_thread_in_selector(cx)),
|
||||
)
|
||||
|
|
@ -4007,7 +4193,10 @@ impl AgentPanel {
|
|||
cx,
|
||||
))
|
||||
})
|
||||
.child(self.render_panel_options_menu(window, cx)),
|
||||
.child(self.render_panel_options_menu(window, cx))
|
||||
.when(docked_right, |this| {
|
||||
this.children(self.render_sidebar_toggle(cx))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
|
|
@ -4045,6 +4234,9 @@ impl AgentPanel {
|
|||
.size_full()
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.pl(DynamicSpacing::Base04.rems(cx))
|
||||
.when(!docked_right, |this| {
|
||||
this.children(self.render_sidebar_toggle(cx))
|
||||
})
|
||||
.child(match &self.active_view {
|
||||
ActiveView::History { .. } | ActiveView::Configuration => {
|
||||
self.render_toolbar_back_button(cx).into_any_element()
|
||||
|
|
@ -4067,7 +4259,10 @@ impl AgentPanel {
|
|||
cx,
|
||||
))
|
||||
})
|
||||
.child(self.render_panel_options_menu(window, cx)),
|
||||
.child(self.render_panel_options_menu(window, cx))
|
||||
.when(docked_right, |this| {
|
||||
this.children(self.render_sidebar_toggle(cx))
|
||||
}),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
|
@ -4607,14 +4802,44 @@ impl Render for AgentPanel {
|
|||
})
|
||||
.children(self.render_trial_end_upsell(window, cx));
|
||||
|
||||
let sidebar = self.render_sidebar(cx);
|
||||
let has_sidebar = sidebar.is_some();
|
||||
let docked_right = agent_panel_dock_position(cx) == DockPosition::Right;
|
||||
|
||||
let panel = h_flex()
|
||||
.size_full()
|
||||
.when(has_sidebar, |this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
move |this, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
|
||||
if let Some(sidebar) = &this.sidebar {
|
||||
let width = if docked_right {
|
||||
e.bounds.right() - e.event.position.x
|
||||
} else {
|
||||
e.event.position.x
|
||||
};
|
||||
sidebar.update(cx, |sidebar, cx| {
|
||||
sidebar.set_width(Some(width), cx);
|
||||
});
|
||||
}
|
||||
},
|
||||
))
|
||||
})
|
||||
.map(|this| {
|
||||
if docked_right {
|
||||
this.child(content).children(sidebar)
|
||||
} else {
|
||||
this.children(sidebar).child(content)
|
||||
}
|
||||
});
|
||||
|
||||
match self.active_view.which_font_size_used() {
|
||||
WhichFontSize::AgentFont => {
|
||||
WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
|
||||
.size_full()
|
||||
.child(content)
|
||||
.child(panel)
|
||||
.into_any()
|
||||
}
|
||||
_ => content.into_any(),
|
||||
_ => panel.into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ mod mode_selector;
|
|||
mod model_selector;
|
||||
mod model_selector_popover;
|
||||
mod profile_selector;
|
||||
pub mod sidebar;
|
||||
mod slash_command;
|
||||
mod slash_command_picker;
|
||||
mod terminal_codegen;
|
||||
|
|
|
|||
|
|
@ -2340,7 +2340,7 @@ impl ConnectionView {
|
|||
}
|
||||
|
||||
if let Some(multi_workspace) = window.root::<MultiWorkspace>().flatten() {
|
||||
multi_workspace.read(cx).is_sidebar_open()
|
||||
crate::agent_panel::sidebar_is_open(window, cx)
|
||||
|| self.agent_panel_visible(&multi_workspace, cx)
|
||||
} else {
|
||||
self.workspace
|
||||
|
|
|
|||
|
|
@ -1,33 +1,32 @@
|
|||
use crate::{AgentPanel, AgentPanelEvent, NewThread};
|
||||
use acp_thread::ThreadStatus;
|
||||
use agent::ThreadStore;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_ui::{AgentPanel, AgentPanelEvent, NewThread};
|
||||
use agent_settings::AgentSettings;
|
||||
use chrono::Utc;
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagViewExt as _};
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, FontStyle, ListState,
|
||||
Action as _, AnyElement, App, Context, Entity, FocusHandle, Focusable, FontStyle, ListState,
|
||||
Pixels, Render, SharedString, TextStyle, WeakEntity, Window, actions, list, prelude::*, px,
|
||||
relative, rems,
|
||||
};
|
||||
use menu::{Cancel, Confirm, SelectFirst, SelectLast, SelectNext, SelectPrevious};
|
||||
use project::Event as ProjectEvent;
|
||||
use recent_projects::RecentProjects;
|
||||
use settings::Settings;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::mem;
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
use ui::utils::TRAFFIC_LIGHT_PADDING;
|
||||
use ui::{
|
||||
AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, KeyBinding, ListItem,
|
||||
PopoverMenu, PopoverMenuHandle, Tab, ThreadItem, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||
AgentThreadStatus, ButtonStyle, HighlightedLabel, IconButtonShape, ListItem, Tab, ThreadItem,
|
||||
Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::path_list::PathList;
|
||||
use workspace::{
|
||||
FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent, Sidebar as WorkspaceSidebar,
|
||||
SidebarEvent, ToggleWorkspaceSidebar, Workspace,
|
||||
MultiWorkspace, MultiWorkspaceEvent, ToggleWorkspaceSidebar, Workspace, multi_workspace_enabled,
|
||||
};
|
||||
use zed_actions::OpenRecent;
|
||||
use zed_actions::editor::{MoveDown, MoveUp};
|
||||
|
||||
actions!(
|
||||
|
|
@ -44,6 +43,27 @@ const DEFAULT_WIDTH: Pixels = px(320.0);
|
|||
const MIN_WIDTH: Pixels = px(200.0);
|
||||
const MAX_WIDTH: Pixels = px(800.0);
|
||||
const DEFAULT_THREADS_SHOWN: usize = 5;
|
||||
const SIDEBAR_STATE_KEY: &str = "sidebar_state";
|
||||
|
||||
fn read_sidebar_open_state(multi_workspace_id: u64) -> bool {
|
||||
KEY_VALUE_STORE
|
||||
.scoped(SIDEBAR_STATE_KEY)
|
||||
.read(&multi_workspace_id.to_string())
|
||||
.log_err()
|
||||
.flatten()
|
||||
.and_then(|json| serde_json::from_str::<bool>(&json).ok())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
async fn save_sidebar_open_state(multi_workspace_id: u64, is_open: bool) {
|
||||
if let Ok(json) = serde_json::to_string(&is_open) {
|
||||
KEY_VALUE_STORE
|
||||
.scoped(SIDEBAR_STATE_KEY)
|
||||
.write(multi_workspace_id.to_string(), json)
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ActiveThreadInfo {
|
||||
|
|
@ -173,6 +193,8 @@ fn workspace_path_list_and_label(
|
|||
|
||||
pub struct Sidebar {
|
||||
multi_workspace: WeakEntity<MultiWorkspace>,
|
||||
persistence_key: Option<u64>,
|
||||
is_open: bool,
|
||||
width: Pixels,
|
||||
focus_handle: FocusHandle,
|
||||
filter_editor: Entity<Editor>,
|
||||
|
|
@ -186,11 +208,8 @@ pub struct Sidebar {
|
|||
active_entry_index: Option<usize>,
|
||||
collapsed_groups: HashSet<PathList>,
|
||||
expanded_groups: HashMap<PathList, usize>,
|
||||
recent_projects_popover_handle: PopoverMenuHandle<RecentProjects>,
|
||||
}
|
||||
|
||||
impl EventEmitter<SidebarEvent> for Sidebar {}
|
||||
|
||||
impl Sidebar {
|
||||
pub fn new(
|
||||
multi_workspace: Entity<MultiWorkspace>,
|
||||
|
|
@ -212,7 +231,6 @@ impl Sidebar {
|
|||
window,
|
||||
|this, _multi_workspace, event: &MultiWorkspaceEvent, window, cx| match event {
|
||||
MultiWorkspaceEvent::ActiveWorkspaceChanged => {
|
||||
this.focused_thread = None;
|
||||
this.update_entries(cx);
|
||||
}
|
||||
MultiWorkspaceEvent::WorkspaceAdded(workspace) => {
|
||||
|
|
@ -270,8 +288,15 @@ impl Sidebar {
|
|||
this.update_entries(cx);
|
||||
});
|
||||
|
||||
let persistence_key = multi_workspace.read(cx).database_id().map(|id| id.0);
|
||||
let is_open = persistence_key
|
||||
.map(read_sidebar_open_state)
|
||||
.unwrap_or(false);
|
||||
|
||||
Self {
|
||||
multi_workspace: multi_workspace.downgrade(),
|
||||
persistence_key,
|
||||
is_open,
|
||||
width: DEFAULT_WIDTH,
|
||||
focus_handle,
|
||||
filter_editor,
|
||||
|
|
@ -282,7 +307,6 @@ impl Sidebar {
|
|||
active_entry_index: None,
|
||||
collapsed_groups: HashSet::new(),
|
||||
expanded_groups: HashMap::new(),
|
||||
recent_projects_popover_handle: PopoverMenuHandle::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -334,31 +358,10 @@ impl Sidebar {
|
|||
cx.subscribe_in(
|
||||
agent_panel,
|
||||
window,
|
||||
|this, agent_panel, event: &AgentPanelEvent, _window, cx| match event {
|
||||
AgentPanelEvent::ActiveViewChanged => {
|
||||
match agent_panel.read(cx).active_connection_view() {
|
||||
Some(thread) => {
|
||||
if let Some(session_id) = thread.read(cx).parent_id(cx) {
|
||||
this.focused_thread = Some(session_id);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
this.focused_thread = None;
|
||||
}
|
||||
}
|
||||
this.update_entries(cx);
|
||||
}
|
||||
AgentPanelEvent::ThreadFocused => {
|
||||
let new_focused = agent_panel
|
||||
.read(cx)
|
||||
.active_connection_view()
|
||||
.and_then(|thread| thread.read(cx).parent_id(cx));
|
||||
if new_focused.is_some() && new_focused != this.focused_thread {
|
||||
this.focused_thread = new_focused;
|
||||
this.update_entries(cx);
|
||||
}
|
||||
}
|
||||
AgentPanelEvent::BackgroundThreadChanged => {
|
||||
|this, _agent_panel, event: &AgentPanelEvent, _window, cx| match event {
|
||||
AgentPanelEvent::ActiveViewChanged
|
||||
| AgentPanelEvent::ThreadFocused
|
||||
| AgentPanelEvent::BackgroundThreadChanged => {
|
||||
this.update_entries(cx);
|
||||
}
|
||||
},
|
||||
|
|
@ -419,6 +422,12 @@ impl Sidebar {
|
|||
let workspaces = mw.workspaces().to_vec();
|
||||
let active_workspace = mw.workspaces().get(mw.active_workspace_index()).cloned();
|
||||
|
||||
self.focused_thread = active_workspace
|
||||
.as_ref()
|
||||
.and_then(|ws| ws.read(cx).panel::<AgentPanel>(cx))
|
||||
.and_then(|panel| panel.read(cx).active_connection_view().cloned())
|
||||
.and_then(|cv| cv.read(cx).parent_id(cx));
|
||||
|
||||
let thread_store = ThreadStore::try_global(cx);
|
||||
let query = self.filter_editor.read(cx).text(cx);
|
||||
|
||||
|
|
@ -657,7 +666,7 @@ impl Sidebar {
|
|||
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
if !multi_workspace.read(cx).multi_workspace_enabled(cx) {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -885,8 +894,6 @@ impl Sidebar {
|
|||
return;
|
||||
};
|
||||
|
||||
self.focused_thread = None;
|
||||
|
||||
multi_workspace.update(cx, |multi_workspace, cx| {
|
||||
multi_workspace.activate(workspace.clone(), cx);
|
||||
});
|
||||
|
|
@ -1173,48 +1180,6 @@ impl Sidebar {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_recent_projects_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let workspace = self
|
||||
.multi_workspace
|
||||
.upgrade()
|
||||
.map(|mw| mw.read(cx).workspace().downgrade());
|
||||
|
||||
let focus_handle = workspace
|
||||
.as_ref()
|
||||
.and_then(|ws| ws.upgrade())
|
||||
.map(|w| w.read(cx).focus_handle(cx))
|
||||
.unwrap_or_else(|| cx.focus_handle());
|
||||
|
||||
let popover_handle = self.recent_projects_popover_handle.clone();
|
||||
|
||||
PopoverMenu::new("sidebar-recent-projects-menu")
|
||||
.with_handle(popover_handle)
|
||||
.menu(move |window, cx| {
|
||||
workspace.as_ref().map(|ws| {
|
||||
RecentProjects::popover(ws.clone(), false, focus_handle.clone(), window, cx)
|
||||
})
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("open-project", IconName::OpenFolder)
|
||||
.icon_size(IconSize::Small)
|
||||
.selected_style(ButtonStyle::Tinted(TintColor::Accent)),
|
||||
|_window, cx| {
|
||||
Tooltip::for_action(
|
||||
"Recent Projects",
|
||||
&OpenRecent {
|
||||
create_new_window: false,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)
|
||||
.anchor(gpui::Corner::TopLeft)
|
||||
.offset(gpui::Point {
|
||||
x: px(0.0),
|
||||
y: px(2.0),
|
||||
})
|
||||
}
|
||||
|
||||
fn render_filter_input(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
|
|
@ -1343,27 +1308,67 @@ impl Sidebar {
|
|||
}
|
||||
}
|
||||
|
||||
impl WorkspaceSidebar for Sidebar {
|
||||
fn width(&self, _cx: &App) -> Pixels {
|
||||
impl Sidebar {
|
||||
pub fn is_open(&self) -> bool {
|
||||
self.is_open
|
||||
}
|
||||
|
||||
pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
|
||||
if self.is_open == open {
|
||||
return;
|
||||
}
|
||||
self.is_open = open;
|
||||
cx.notify();
|
||||
if let Some(key) = self.persistence_key {
|
||||
let is_open = self.is_open;
|
||||
cx.background_spawn(async move {
|
||||
save_sidebar_open_state(key, is_open).await;
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let new_state = !self.is_open;
|
||||
self.set_open(new_state, cx);
|
||||
if new_state {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_or_unfocus(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.is_open {
|
||||
let sidebar_is_focused = self.focus_handle(cx).contains_focused(window, cx);
|
||||
if sidebar_is_focused {
|
||||
let active_pane = workspace.active_pane().clone();
|
||||
let pane_focus = active_pane.read(cx).focus_handle(cx);
|
||||
window.focus(&pane_focus, cx);
|
||||
} else {
|
||||
cx.focus_self(window);
|
||||
}
|
||||
} else {
|
||||
self.set_open(true, cx);
|
||||
cx.focus_self(window);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn width(&self, _cx: &App) -> Pixels {
|
||||
self.width
|
||||
}
|
||||
|
||||
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
|
||||
pub fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>) {
|
||||
self.width = width.unwrap_or(DEFAULT_WIDTH).clamp(MIN_WIDTH, MAX_WIDTH);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn has_notifications(&self, _cx: &App) -> bool {
|
||||
pub fn has_notifications(&self, _cx: &App) -> bool {
|
||||
!self.contents.notified_threads.is_empty()
|
||||
}
|
||||
|
||||
fn toggle_recent_projects_popover(&self, window: &mut Window, cx: &mut App) {
|
||||
self.recent_projects_popover_handle.toggle(window, cx);
|
||||
}
|
||||
|
||||
fn is_recent_projects_popover_deployed(&self) -> bool {
|
||||
self.recent_projects_popover_handle.is_deployed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for Sidebar {
|
||||
|
|
@ -1374,18 +1379,9 @@ impl Focusable for Sidebar {
|
|||
|
||||
impl Render for Sidebar {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let titlebar_height = ui::utils::platform_title_bar_height(window);
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let is_focused = self.focus_handle.is_focused(window)
|
||||
|| self.filter_editor.focus_handle(cx).is_focused(window);
|
||||
let has_query = self.has_filter_query(cx);
|
||||
|
||||
let focus_tooltip_label = if is_focused {
|
||||
"Focus Workspace"
|
||||
} else {
|
||||
"Focus Sidebar"
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.id("workspace-sidebar")
|
||||
.key_context("WorkspaceSidebar")
|
||||
|
|
@ -1401,69 +1397,26 @@ impl Render for Sidebar {
|
|||
.on_action(cx.listener(Self::collapse_selected_entry))
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.font(ui_font)
|
||||
.h_full()
|
||||
.w(self.width)
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().surface_background)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.h(titlebar_height)
|
||||
.w_full()
|
||||
.mt_px()
|
||||
.pb_px()
|
||||
.pr_1()
|
||||
.when_else(
|
||||
cfg!(target_os = "macos") && !window.is_fullscreen(),
|
||||
|this| this.pl(px(TRAFFIC_LIGHT_PADDING)),
|
||||
|this| this.pl_2(),
|
||||
)
|
||||
.justify_between()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child({
|
||||
let focus_handle_toggle = self.focus_handle.clone();
|
||||
let focus_handle_focus = self.focus_handle.clone();
|
||||
IconButton::new("close-sidebar", IconName::WorkspaceNavOpen)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::element(move |_, cx| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(Label::new("Close Sidebar"))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&ToggleWorkspaceSidebar,
|
||||
&focus_handle_toggle,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.pt_1()
|
||||
.gap_2()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.justify_between()
|
||||
.child(Label::new(focus_tooltip_label))
|
||||
.child(KeyBinding::for_action_in(
|
||||
&FocusWorkspaceSidebar,
|
||||
&focus_handle_focus,
|
||||
cx,
|
||||
)),
|
||||
)
|
||||
.into_any_element()
|
||||
}))
|
||||
.on_click(cx.listener(|_this, _, _window, cx| {
|
||||
cx.emit(SidebarEvent::Close);
|
||||
}))
|
||||
})
|
||||
.child(self.render_recent_projects_button(cx)),
|
||||
)
|
||||
.child(
|
||||
.child({
|
||||
let docked_right =
|
||||
AgentSettings::get_global(cx).dock == settings::DockPosition::Right;
|
||||
let render_close_button = || {
|
||||
IconButton::new("sidebar-close-toggle", IconName::WorkspaceNavOpen)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::for_action(
|
||||
"Close Threads Sidebar",
|
||||
&ToggleWorkspaceSidebar,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
|
||||
})
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.px_2p5()
|
||||
|
|
@ -1471,6 +1424,7 @@ impl Render for Sidebar {
|
|||
.gap_2()
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.when(!docked_right, |this| this.child(render_close_button()))
|
||||
.child(
|
||||
Icon::new(IconName::MagnifyingGlass)
|
||||
.size(IconSize::Small)
|
||||
|
|
@ -1487,8 +1441,9 @@ impl Render for Sidebar {
|
|||
this.update_entries(cx);
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(docked_right, |this| this.child(render_close_button()))
|
||||
})
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
|
|
@ -1509,26 +1464,24 @@ impl Render for Sidebar {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::test_support::{active_session_id, open_thread_with_connection, send_message};
|
||||
use acp_thread::StubAgentConnection;
|
||||
use agent::ThreadStore;
|
||||
use agent_ui::test_support::{active_session_id, open_thread_with_connection, send_message};
|
||||
use assistant_text_thread::TextThreadStore;
|
||||
use chrono::DateTime;
|
||||
use feature_flags::FeatureFlagAppExt as _;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use util::path_list::PathList;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
crate::test_support::init_test(cx);
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
editor::init(cx);
|
||||
cx.update_flags(false, vec!["agent-v2".into()]);
|
||||
ThreadStore::init_global(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
prompt_store::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1569,14 +1522,33 @@ mod tests {
|
|||
multi_workspace: &Entity<MultiWorkspace>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> Entity<Sidebar> {
|
||||
let multi_workspace = multi_workspace.clone();
|
||||
let sidebar =
|
||||
cx.update(|window, cx| cx.new(|cx| Sidebar::new(multi_workspace.clone(), window, cx)));
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.register_sidebar(sidebar.clone(), window, cx);
|
||||
let (sidebar, _panel) = setup_sidebar_with_agent_panel(multi_workspace, cx);
|
||||
sidebar
|
||||
}
|
||||
|
||||
fn setup_sidebar_with_agent_panel(
|
||||
multi_workspace: &Entity<MultiWorkspace>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> (Entity<Sidebar>, Entity<AgentPanel>) {
|
||||
let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
|
||||
let project = workspace.read_with(cx, |ws, _cx| ws.project().clone());
|
||||
let panel = add_agent_panel(&workspace, &project, cx);
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
workspace.right_dock().update(cx, |dock, cx| {
|
||||
if let Some(panel_ix) = dock.panel_index_for_type::<AgentPanel>() {
|
||||
dock.activate_panel(panel_ix, window, cx);
|
||||
}
|
||||
dock.set_open(true, window, cx);
|
||||
});
|
||||
});
|
||||
cx.run_until_parked();
|
||||
sidebar
|
||||
let sidebar = panel.read_with(cx, |panel, _cx| {
|
||||
panel
|
||||
.sidebar
|
||||
.clone()
|
||||
.expect("AgentPanel should have created a sidebar")
|
||||
});
|
||||
(sidebar, panel)
|
||||
}
|
||||
|
||||
async fn save_n_test_threads(
|
||||
|
|
@ -1623,16 +1595,10 @@ mod tests {
|
|||
cx.run_until_parked();
|
||||
}
|
||||
|
||||
fn open_and_focus_sidebar(
|
||||
sidebar: &Entity<Sidebar>,
|
||||
multi_workspace: &Entity<MultiWorkspace>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) {
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.toggle_sidebar(window, cx);
|
||||
});
|
||||
fn open_and_focus_sidebar(sidebar: &Entity<Sidebar>, cx: &mut gpui::VisualTestContext) {
|
||||
cx.run_until_parked();
|
||||
sidebar.update_in(cx, |_, window, cx| {
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.set_open(true, cx);
|
||||
cx.focus_self(window);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
|
@ -1886,7 +1852,7 @@ mod tests {
|
|||
assert!(entries.iter().any(|e| e.contains("View More (12)")));
|
||||
|
||||
// Focus and navigate to View More, then confirm to expand by one batch
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
for _ in 0..7 {
|
||||
cx.dispatch_action(SelectNext);
|
||||
}
|
||||
|
|
@ -2169,7 +2135,7 @@ mod tests {
|
|||
// Entries: [header, thread3, thread2, thread1]
|
||||
// Focusing the sidebar does not set a selection; select_next/select_previous
|
||||
// handle None gracefully by starting from the first or last entry.
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
|
||||
|
||||
// First SelectNext from None starts at index 0
|
||||
|
|
@ -2218,7 +2184,7 @@ mod tests {
|
|||
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
|
||||
cx.run_until_parked();
|
||||
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
|
||||
// SelectLast jumps to the end
|
||||
cx.dispatch_action(SelectLast);
|
||||
|
|
@ -2241,7 +2207,7 @@ mod tests {
|
|||
|
||||
// Open the sidebar so it's rendered, then focus it to trigger focus_in.
|
||||
// focus_in no longer sets a default selection.
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
|
||||
|
||||
// Manually set a selection, blur, then refocus — selection should be preserved
|
||||
|
|
@ -2273,6 +2239,9 @@ mod tests {
|
|||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Add an agent panel to workspace 1 so the sidebar renders when it's active.
|
||||
setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
save_n_test_threads(1, &path_list, cx).await;
|
||||
multi_workspace.update_in(cx, |_, _window, cx| cx.notify());
|
||||
|
|
@ -2299,7 +2268,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Focus the sidebar and manually select the header (index 0)
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
sidebar.update_in(cx, |sidebar, _window, _cx| {
|
||||
sidebar.selection = Some(0);
|
||||
});
|
||||
|
|
@ -2342,7 +2311,7 @@ mod tests {
|
|||
assert!(entries.iter().any(|e| e.contains("View More (3)")));
|
||||
|
||||
// Focus sidebar (selection starts at None), then navigate down to the "View More" entry (index 6)
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
for _ in 0..7 {
|
||||
cx.dispatch_action(SelectNext);
|
||||
}
|
||||
|
|
@ -2377,7 +2346,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Focus sidebar and manually select the header (index 0). Press left to collapse.
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
sidebar.update_in(cx, |sidebar, _window, _cx| {
|
||||
sidebar.selection = Some(0);
|
||||
});
|
||||
|
|
@ -2417,7 +2386,7 @@ mod tests {
|
|||
cx.run_until_parked();
|
||||
|
||||
// Focus sidebar (selection starts at None), then navigate down to the thread (child)
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(SelectNext);
|
||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
|
||||
|
|
@ -2452,7 +2421,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// Focus sidebar — focus_in does not set a selection
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), None);
|
||||
|
||||
// First SelectNext from None starts at index 0 (header)
|
||||
|
|
@ -2485,7 +2454,7 @@ mod tests {
|
|||
cx.run_until_parked();
|
||||
|
||||
// Focus sidebar (selection starts at None), navigate down to the thread (index 1)
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
cx.dispatch_action(SelectNext);
|
||||
cx.dispatch_action(SelectNext);
|
||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(1));
|
||||
|
|
@ -2505,24 +2474,6 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
async fn init_test_project_with_agent_panel(
|
||||
worktree_path: &str,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<project::Project> {
|
||||
agent_ui::test_support::init_test(cx);
|
||||
cx.update(|cx| {
|
||||
cx.update_flags(false, vec!["agent-v2".into()]);
|
||||
ThreadStore::init_global(cx);
|
||||
language_model::LanguageModelRegistry::test(cx);
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(worktree_path, serde_json::json!({ "src": {} }))
|
||||
.await;
|
||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||
project::Project::test(fs, [worktree_path.as_ref()], cx).await
|
||||
}
|
||||
|
||||
fn add_agent_panel(
|
||||
workspace: &Entity<Workspace>,
|
||||
project: &Entity<project::Project>,
|
||||
|
|
@ -2536,23 +2487,12 @@ mod tests {
|
|||
})
|
||||
}
|
||||
|
||||
fn setup_sidebar_with_agent_panel(
|
||||
multi_workspace: &Entity<MultiWorkspace>,
|
||||
project: &Entity<project::Project>,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) -> (Entity<Sidebar>, Entity<AgentPanel>) {
|
||||
let sidebar = setup_sidebar(multi_workspace, cx);
|
||||
let workspace = multi_workspace.read_with(cx, |mw, _cx| mw.workspace().clone());
|
||||
let panel = add_agent_panel(&workspace, project, cx);
|
||||
(sidebar, panel)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_parallel_threads_shown_with_live_status(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let project = init_test_project("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
|
|
@ -2595,10 +2535,10 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_background_thread_completion_triggers_notification(cx: &mut TestAppContext) {
|
||||
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
|
||||
let project_a = init_test_project("/project-a", cx).await;
|
||||
let (multi_workspace, cx) = cx
|
||||
.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
|
||||
|
||||
|
|
@ -2802,7 +2742,7 @@ mod tests {
|
|||
);
|
||||
|
||||
// User types a search query to filter down.
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
type_in_search(&sidebar, "alpha", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
|
|
@ -3125,7 +3065,7 @@ mod tests {
|
|||
|
||||
// User focuses the sidebar and collapses the group using keyboard:
|
||||
// manually select the header, then press CollapseSelectedEntry to collapse.
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
sidebar.update_in(cx, |sidebar, _window, _cx| {
|
||||
sidebar.selection = Some(0);
|
||||
});
|
||||
|
|
@ -3175,7 +3115,7 @@ mod tests {
|
|||
}
|
||||
cx.run_until_parked();
|
||||
|
||||
open_and_focus_sidebar(&sidebar, &multi_workspace, cx);
|
||||
open_and_focus_sidebar(&sidebar, cx);
|
||||
|
||||
// User types "fix" — two threads match.
|
||||
type_in_search(&sidebar, "fix", cx);
|
||||
|
|
@ -3352,10 +3292,10 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_thread_title_update_propagates_to_sidebar(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
let project = init_test_project("/my-project", cx).await;
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, &project, cx);
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list = PathList::new(&[std::path::PathBuf::from("/my-project")]);
|
||||
|
||||
|
|
@ -3400,10 +3340,10 @@ mod tests {
|
|||
|
||||
#[gpui::test]
|
||||
async fn test_focused_thread_tracks_user_intent(cx: &mut TestAppContext) {
|
||||
let project_a = init_test_project_with_agent_panel("/project-a", cx).await;
|
||||
let project_a = init_test_project("/project-a", cx).await;
|
||||
let (multi_workspace, cx) = cx
|
||||
.add_window_view(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, &project_a, cx);
|
||||
let (sidebar, panel_a) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let path_list_a = PathList::new(&[std::path::PathBuf::from("/project-a")]);
|
||||
|
||||
|
|
@ -3432,7 +3372,8 @@ mod tests {
|
|||
let workspace_a = multi_workspace.read_with(cx, |mw, _cx| mw.workspaces()[0].clone());
|
||||
|
||||
// ── 1. Initial state: no focused thread ──────────────────────────────
|
||||
// Workspace B is active (just added), so its header is the active entry.
|
||||
// Workspace B is active (just added) and has no thread, so its header
|
||||
// is the active entry.
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread, None,
|
||||
|
|
@ -3447,6 +3388,7 @@ mod tests {
|
|||
);
|
||||
});
|
||||
|
||||
// ── 2. Click thread in workspace A via sidebar ───────────────────────
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.activate_thread(
|
||||
acp_thread::AgentSessionInfo {
|
||||
|
|
@ -3490,6 +3432,7 @@ mod tests {
|
|||
);
|
||||
});
|
||||
|
||||
// ── 3. Open thread in workspace B, then click it via sidebar ─────────
|
||||
let connection_b = StubAgentConnection::new();
|
||||
connection_b.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk::new("Thread B".into()),
|
||||
|
|
@ -3501,6 +3444,16 @@ mod tests {
|
|||
save_thread_to_store(&session_id_b, &path_list_b, cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
// Opening a thread in a non-active workspace should NOT change
|
||||
// focused_thread — it's derived from the active workspace.
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_a),
|
||||
"Opening a thread in a non-active workspace should not affect focused_thread"
|
||||
);
|
||||
});
|
||||
|
||||
// Workspace A is currently active. Click a thread in workspace B,
|
||||
// which also triggers a workspace switch.
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
|
|
@ -3535,25 +3488,30 @@ mod tests {
|
|||
);
|
||||
});
|
||||
|
||||
// ── 4. Switch workspace → focused_thread reflects new workspace ──────
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.activate_next_workspace(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Workspace A is now active. Its agent panel still has session_id_a
|
||||
// loaded, so focused_thread should reflect that.
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread, None,
|
||||
"External workspace switch should clear focused_thread"
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_a),
|
||||
"Switching workspaces should derive focused_thread from the new active workspace"
|
||||
);
|
||||
let active_entry = sidebar
|
||||
.active_entry_index
|
||||
.and_then(|ix| sidebar.contents.entries.get(ix));
|
||||
assert!(
|
||||
matches!(active_entry, Some(ListEntry::ProjectHeader { .. })),
|
||||
"Active entry should be the workspace header after external switch"
|
||||
matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_a),
|
||||
"Active entry should be workspace_a's active thread"
|
||||
);
|
||||
});
|
||||
|
||||
// ── 5. Opening a thread in a non-active workspace is ignored ──────────
|
||||
let connection_b2 = StubAgentConnection::new();
|
||||
connection_b2.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk::new("New thread".into()),
|
||||
|
|
@ -3564,69 +3522,48 @@ mod tests {
|
|||
save_thread_to_store(&session_id_b2, &path_list_b, cx).await;
|
||||
cx.run_until_parked();
|
||||
|
||||
// Workspace A is still active, so focused_thread stays on session_id_a.
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_b2),
|
||||
"Opening a thread externally should set focused_thread"
|
||||
);
|
||||
});
|
||||
|
||||
workspace_b.update_in(cx, |workspace, window, cx| {
|
||||
workspace.focus_handle(cx).focus(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_b2),
|
||||
"Defocusing the sidebar should not clear focused_thread"
|
||||
Some(&session_id_a),
|
||||
"Opening a thread in a non-active workspace should not affect focused_thread"
|
||||
);
|
||||
});
|
||||
|
||||
// ── 6. Activating workspace B shows its active thread ────────────────
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.activate_workspace(&workspace_b, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread, None,
|
||||
"Clicking a workspace header should clear focused_thread"
|
||||
);
|
||||
let active_entry = sidebar
|
||||
.active_entry_index
|
||||
.and_then(|ix| sidebar.contents.entries.get(ix));
|
||||
assert!(
|
||||
matches!(active_entry, Some(ListEntry::ProjectHeader { .. })),
|
||||
"Active entry should be the workspace header"
|
||||
);
|
||||
});
|
||||
|
||||
// ── 8. Focusing the agent panel thread restores focused_thread ────
|
||||
// Workspace B still has session_id_b2 loaded in the agent panel.
|
||||
// Clicking into the thread (simulated by focusing its view) should
|
||||
// set focused_thread via the ThreadFocused event.
|
||||
panel_b.update_in(cx, |panel, window, cx| {
|
||||
if let Some(thread_view) = panel.active_connection_view() {
|
||||
thread_view.read(cx).focus_handle(cx).focus(window, cx);
|
||||
}
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
// Workspace B is now active with session_id_b2 loaded.
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_b2),
|
||||
"Focusing the agent panel thread should set focused_thread"
|
||||
"Activating workspace_b should show workspace_b's active thread"
|
||||
);
|
||||
let active_entry = sidebar
|
||||
.active_entry_index
|
||||
.and_then(|ix| sidebar.contents.entries.get(ix));
|
||||
assert!(
|
||||
matches!(active_entry, Some(ListEntry::Thread(thread)) if thread.session_info.session_id == session_id_b2),
|
||||
"Active entry should be the focused thread"
|
||||
"Active entry should be workspace_b's active thread"
|
||||
);
|
||||
});
|
||||
|
||||
// ── 7. Switching back to workspace A reflects its thread ─────────────
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.activate_next_workspace(window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert_eq!(
|
||||
sidebar.focused_thread.as_ref(),
|
||||
Some(&session_id_a),
|
||||
"Switching back to workspace_a should show its active thread"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
@ -1211,7 +1211,9 @@ async fn test_stack_frame_filter_persistence(
|
|||
cx.run_until_parked();
|
||||
|
||||
let workspace_id = workspace
|
||||
.update(cx, |workspace, _window, cx| workspace.database_id(cx))
|
||||
.update(cx, |workspace, _window, cx| {
|
||||
workspace.active_workspace_database_id(cx)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.expect("workspace id has to be some for this test to work properly");
|
||||
|
|
|
|||
|
|
@ -31,8 +31,6 @@ pub struct PlatformTitleBar {
|
|||
children: SmallVec<[AnyElement; 2]>,
|
||||
should_move: bool,
|
||||
system_window_tabs: Entity<SystemWindowTabs>,
|
||||
workspace_sidebar_open: bool,
|
||||
sidebar_has_notifications: bool,
|
||||
}
|
||||
|
||||
impl PlatformTitleBar {
|
||||
|
|
@ -46,8 +44,6 @@ impl PlatformTitleBar {
|
|||
children: SmallVec::new(),
|
||||
should_move: false,
|
||||
system_window_tabs,
|
||||
workspace_sidebar_open: false,
|
||||
sidebar_has_notifications: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -74,28 +70,6 @@ impl PlatformTitleBar {
|
|||
SystemWindowTabs::init(cx);
|
||||
}
|
||||
|
||||
pub fn is_workspace_sidebar_open(&self) -> bool {
|
||||
self.workspace_sidebar_open
|
||||
}
|
||||
|
||||
pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
|
||||
self.workspace_sidebar_open = open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn sidebar_has_notifications(&self) -> bool {
|
||||
self.sidebar_has_notifications
|
||||
}
|
||||
|
||||
pub fn set_sidebar_has_notifications(
|
||||
&mut self,
|
||||
has_notifications: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.sidebar_has_notifications = has_notifications;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn is_multi_workspace_enabled(cx: &App) -> bool {
|
||||
cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
|
||||
}
|
||||
|
|
@ -110,9 +84,6 @@ impl Render for PlatformTitleBar {
|
|||
let close_action = Box::new(workspace::CloseWindow);
|
||||
let children = mem::take(&mut self.children);
|
||||
|
||||
let is_multiworkspace_sidebar_open =
|
||||
PlatformTitleBar::is_multi_workspace_enabled(cx) && self.is_workspace_sidebar_open();
|
||||
|
||||
let title_bar = h_flex()
|
||||
.window_control_area(WindowControlArea::Drag)
|
||||
.w_full()
|
||||
|
|
@ -161,9 +132,7 @@ impl Render for PlatformTitleBar {
|
|||
.map(|this| {
|
||||
if window.is_fullscreen() {
|
||||
this.pl_2()
|
||||
} else if self.platform_style == PlatformStyle::Mac
|
||||
&& !is_multiworkspace_sidebar_open
|
||||
{
|
||||
} else if self.platform_style == PlatformStyle::Mac {
|
||||
this.pl(px(TRAFFIC_LIGHT_PADDING))
|
||||
} else {
|
||||
this.pl_2()
|
||||
|
|
@ -175,10 +144,9 @@ impl Render for PlatformTitleBar {
|
|||
.when(!(tiling.top || tiling.right), |el| {
|
||||
el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(
|
||||
!(tiling.top || tiling.left) && !is_multiworkspace_sidebar_open,
|
||||
|el| el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING),
|
||||
)
|
||||
.when(!(tiling.top || tiling.left), |el| {
|
||||
el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
// this border is to avoid a transparent gap in the rounded corners
|
||||
.mt(px(-1.))
|
||||
.mb(px(-1.))
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
[package]
|
||||
name = "sidebar"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/sidebar.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
agent.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
agent_ui.workspace = true
|
||||
chrono.workspace = true
|
||||
editor.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
recent_projects.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
acp_thread = { workspace = true, features = ["test-support"] }
|
||||
agent = { workspace = true, features = ["test-support"] }
|
||||
agent_ui = { workspace = true, features = ["test-support"] }
|
||||
assistant_text_thread = { workspace = true, features = ["test-support"] }
|
||||
editor.workspace = true
|
||||
language_model = { workspace = true, features = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
feature_flags.workspace = true
|
||||
fs = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
settings = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
recent_projects = { workspace = true, features = ["test-support"] }
|
||||
|
|
@ -1 +0,0 @@
|
|||
../../LICENSE-GPL
|
||||
|
|
@ -38,7 +38,6 @@ chrono.workspace = true
|
|||
client.workspace = true
|
||||
cloud_api_types.workspace = true
|
||||
db.workspace = true
|
||||
feature_flags.workspace = true
|
||||
git_ui.workspace = true
|
||||
gpui = { workspace = true, features = ["screen-capture"] }
|
||||
notifications.workspace = true
|
||||
|
|
|
|||
|
|
@ -24,16 +24,13 @@ use auto_update::AutoUpdateStatus;
|
|||
use call::ActiveCall;
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use cloud_api_types::Plan;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
Action, AnyElement, App, Context, Corner, Element, Empty, Entity, Focusable,
|
||||
InteractiveElement, IntoElement, MouseButton, ParentElement, Render,
|
||||
StatefulInteractiveElement, Styled, Subscription, WeakEntity, Window, actions, div,
|
||||
};
|
||||
use onboarding_banner::OnboardingBanner;
|
||||
use project::{
|
||||
DisableAiSettings, Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees,
|
||||
};
|
||||
use project::{Project, git_store::GitStoreEvent, trusted_worktrees::TrustedWorktrees};
|
||||
use remote::RemoteConnectionOptions;
|
||||
use settings::Settings;
|
||||
use settings::WorktreeId;
|
||||
|
|
@ -47,8 +44,7 @@ use ui::{
|
|||
use update_version::UpdateVersion;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
MultiWorkspace, ToggleWorkspaceSidebar, ToggleWorktreeSecurity, Workspace,
|
||||
notifications::NotifyResultExt,
|
||||
MultiWorkspace, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt,
|
||||
};
|
||||
use zed_actions::OpenRemote;
|
||||
|
||||
|
|
@ -174,7 +170,6 @@ impl Render for TitleBar {
|
|||
let mut render_project_items = title_bar_settings.show_branch_name
|
||||
|| title_bar_settings.show_project_items;
|
||||
title_bar
|
||||
.children(self.render_workspace_sidebar_toggle(window, cx))
|
||||
.when_some(
|
||||
self.application_menu.clone().filter(|_| !show_menus),
|
||||
|title_bar, menu| {
|
||||
|
|
@ -357,7 +352,6 @@ impl TitleBar {
|
|||
|
||||
// Set up observer to sync sidebar state from MultiWorkspace to PlatformTitleBar.
|
||||
{
|
||||
let platform_titlebar = platform_titlebar.clone();
|
||||
let window_handle = window.window_handle();
|
||||
cx.spawn(async move |this: WeakEntity<TitleBar>, cx| {
|
||||
let Some(multi_workspace_handle) = window_handle.downcast::<MultiWorkspace>()
|
||||
|
|
@ -370,26 +364,8 @@ impl TitleBar {
|
|||
return;
|
||||
};
|
||||
|
||||
let is_open = multi_workspace.read(cx).is_sidebar_open();
|
||||
let has_notifications = multi_workspace.read(cx).sidebar_has_notifications(cx);
|
||||
platform_titlebar.update(cx, |titlebar, cx| {
|
||||
titlebar.set_workspace_sidebar_open(is_open, cx);
|
||||
titlebar.set_sidebar_has_notifications(has_notifications, cx);
|
||||
});
|
||||
|
||||
let platform_titlebar = platform_titlebar.clone();
|
||||
let subscription = cx.observe(&multi_workspace, move |mw, cx| {
|
||||
let is_open = mw.read(cx).is_sidebar_open();
|
||||
let has_notifications = mw.read(cx).sidebar_has_notifications(cx);
|
||||
platform_titlebar.update(cx, |titlebar, cx| {
|
||||
titlebar.set_workspace_sidebar_open(is_open, cx);
|
||||
titlebar.set_sidebar_has_notifications(has_notifications, cx);
|
||||
});
|
||||
});
|
||||
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, _| {
|
||||
this._subscriptions.push(subscription);
|
||||
this.multi_workspace = Some(multi_workspace.downgrade());
|
||||
});
|
||||
}
|
||||
|
|
@ -686,46 +662,7 @@ impl TitleBar {
|
|||
)
|
||||
}
|
||||
|
||||
fn render_workspace_sidebar_toggle(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<AnyElement> {
|
||||
if !cx.has_flag::<AgentV2FeatureFlag>() || DisableAiSettings::get_global(cx).disable_ai {
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_sidebar_open = self.platform_titlebar.read(cx).is_workspace_sidebar_open();
|
||||
|
||||
if is_sidebar_open {
|
||||
return None;
|
||||
}
|
||||
|
||||
let has_notifications = self.platform_titlebar.read(cx).sidebar_has_notifications();
|
||||
|
||||
Some(
|
||||
IconButton::new("toggle-workspace-sidebar", IconName::WorkspaceNavClosed)
|
||||
.icon_size(IconSize::Small)
|
||||
.when(has_notifications, |button| {
|
||||
button
|
||||
.indicator(Indicator::dot().color(Color::Accent))
|
||||
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
|
||||
})
|
||||
.tooltip(move |_, cx| {
|
||||
Tooltip::for_action("Open Threads Sidebar", &ToggleWorkspaceSidebar, cx)
|
||||
})
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(ToggleWorkspaceSidebar.boxed_clone(), cx);
|
||||
})
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn render_project_name(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
pub fn render_project_name(&self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
let name = self.effective_active_worktree(cx).map(|worktree| {
|
||||
|
|
@ -741,19 +678,6 @@ impl TitleBar {
|
|||
"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(
|
||||
window,
|
||||
display_name,
|
||||
is_project_selected,
|
||||
cx,
|
||||
)
|
||||
.into_any_element();
|
||||
}
|
||||
|
||||
let focus_handle = workspace
|
||||
.upgrade()
|
||||
.map(|w| w.read(cx).focus_handle(cx))
|
||||
|
|
@ -793,49 +717,6 @@ impl TitleBar {
|
|||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_project_name_with_sidebar_popover(
|
||||
&self,
|
||||
_window: &Window,
|
||||
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.icon(IconName::ChevronDown)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::XSmall)
|
||||
})
|
||||
.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> {
|
||||
let effective_worktree = self.effective_active_worktree(cx)?;
|
||||
let repository = self.get_repository_for_worktree(&effective_worktree, cx)?;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use anyhow::Result;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt};
|
||||
use gpui::{
|
||||
AnyView, App, Context, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
ManagedView, MouseButton, Pixels, Render, Subscription, Task, Tiling, Window, WindowId,
|
||||
actions, deferred, px,
|
||||
App, Context, Entity, EntityId, EventEmitter, Focusable, ManagedView, Pixels, Render,
|
||||
Subscription, Task, Tiling, Window, WindowId, actions, px,
|
||||
};
|
||||
use project::{DisableAiSettings, Project};
|
||||
use settings::Settings;
|
||||
|
|
@ -12,11 +11,12 @@ use std::path::PathBuf;
|
|||
use ui::prelude::*;
|
||||
use util::ResultExt;
|
||||
|
||||
const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
|
||||
pub const SIDEBAR_RESIZE_HANDLE_SIZE: Pixels = px(6.0);
|
||||
|
||||
use crate::{
|
||||
CloseIntent, CloseWindow, DockPosition, Event as WorkspaceEvent, Item, ModalView, Panel, Toast,
|
||||
Workspace, WorkspaceId, client_side_decorations, notifications::NotificationId,
|
||||
persistence::model::MultiWorkspaceId,
|
||||
};
|
||||
|
||||
actions!(
|
||||
|
|
@ -41,31 +41,6 @@ pub enum MultiWorkspaceEvent {
|
|||
WorkspaceRemoved(EntityId),
|
||||
}
|
||||
|
||||
pub enum SidebarEvent {
|
||||
Open,
|
||||
Close,
|
||||
}
|
||||
|
||||
pub trait Sidebar: EventEmitter<SidebarEvent> + Focusable + Render + Sized {
|
||||
fn width(&self, cx: &App) -> Pixels;
|
||||
fn set_width(&mut self, width: Option<Pixels>, cx: &mut Context<Self>);
|
||||
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 {
|
||||
fn width(&self, cx: &App) -> Pixels;
|
||||
fn set_width(&self, width: Option<Pixels>, cx: &mut App);
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle;
|
||||
fn focus(&self, window: &mut Window, cx: &mut App);
|
||||
fn has_notifications(&self, cx: &App) -> bool;
|
||||
fn to_any(&self) -> AnyView;
|
||||
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)]
|
||||
pub struct DraggedSidebar;
|
||||
|
||||
|
|
@ -75,54 +50,11 @@ impl Render for DraggedSidebar {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Sidebar> SidebarHandle for Entity<T> {
|
||||
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 focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
self.read(cx).focus_handle(cx)
|
||||
}
|
||||
|
||||
fn focus(&self, window: &mut Window, cx: &mut App) {
|
||||
let handle = self.read(cx).focus_handle(cx);
|
||||
window.focus(&handle, cx);
|
||||
}
|
||||
|
||||
fn has_notifications(&self, cx: &App) -> bool {
|
||||
self.read(cx).has_notifications(cx)
|
||||
}
|
||||
|
||||
fn to_any(&self) -> AnyView {
|
||||
self.clone().into()
|
||||
}
|
||||
|
||||
fn entity_id(&self) -> EntityId {
|
||||
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 {
|
||||
window_id: WindowId,
|
||||
workspaces: Vec<Entity<Workspace>>,
|
||||
database_id: Option<MultiWorkspaceId>,
|
||||
active_workspace_index: usize,
|
||||
sidebar: Option<Box<dyn SidebarHandle>>,
|
||||
sidebar_open: bool,
|
||||
_sidebar_subscription: Option<Subscription>,
|
||||
pending_removal_tasks: Vec<Task<()>>,
|
||||
_serialize_task: Option<Task<()>>,
|
||||
_create_task: Option<Task<()>>,
|
||||
|
|
@ -131,6 +63,10 @@ pub struct MultiWorkspace {
|
|||
|
||||
impl EventEmitter<MultiWorkspaceEvent> for MultiWorkspace {}
|
||||
|
||||
pub fn multi_workspace_enabled(cx: &App) -> bool {
|
||||
cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
|
||||
}
|
||||
|
||||
impl MultiWorkspace {
|
||||
pub fn new(workspace: Entity<Workspace>, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let release_subscription = cx.on_release(|this: &mut MultiWorkspace, _cx| {
|
||||
|
|
@ -145,144 +81,19 @@ impl MultiWorkspace {
|
|||
}
|
||||
});
|
||||
let quit_subscription = cx.on_app_quit(Self::app_will_quit);
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<settings::SettingsStore>(window, |this, window, cx| {
|
||||
if DisableAiSettings::get_global(cx).disable_ai && this.sidebar_open {
|
||||
this.close_sidebar(window, cx);
|
||||
}
|
||||
});
|
||||
Self::subscribe_to_workspace(&workspace, cx);
|
||||
Self {
|
||||
window_id: window.window_handle().window_id(),
|
||||
database_id: None,
|
||||
workspaces: vec![workspace],
|
||||
active_workspace_index: 0,
|
||||
sidebar: None,
|
||||
sidebar_open: false,
|
||||
_sidebar_subscription: None,
|
||||
pending_removal_tasks: Vec::new(),
|
||||
_serialize_task: None,
|
||||
_create_task: None,
|
||||
_subscriptions: vec![
|
||||
release_subscription,
|
||||
quit_subscription,
|
||||
settings_subscription,
|
||||
],
|
||||
_subscriptions: vec![release_subscription, quit_subscription],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register_sidebar<T: Sidebar>(
|
||||
&mut self,
|
||||
sidebar: Entity<T>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let subscription =
|
||||
cx.subscribe_in(&sidebar, window, |this, _, event, window, cx| match event {
|
||||
SidebarEvent::Open => this.toggle_sidebar(window, cx),
|
||||
SidebarEvent::Close => {
|
||||
this.close_sidebar(window, cx);
|
||||
}
|
||||
});
|
||||
self.sidebar = Some(Box::new(sidebar));
|
||||
self._sidebar_subscription = Some(subscription);
|
||||
}
|
||||
|
||||
pub fn sidebar(&self) -> Option<&dyn SidebarHandle> {
|
||||
self.sidebar.as_deref()
|
||||
}
|
||||
|
||||
pub fn sidebar_open(&self) -> bool {
|
||||
self.sidebar_open && self.sidebar.is_some()
|
||||
}
|
||||
|
||||
pub fn sidebar_has_notifications(&self, cx: &App) -> bool {
|
||||
self.sidebar
|
||||
.as_ref()
|
||||
.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 {
|
||||
cx.has_flag::<AgentV2FeatureFlag>() && !DisableAiSettings::get_global(cx).disable_ai
|
||||
}
|
||||
|
||||
pub fn toggle_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.sidebar_open {
|
||||
self.close_sidebar(window, cx);
|
||||
} else {
|
||||
self.open_sidebar(cx);
|
||||
if let Some(sidebar) = &self.sidebar {
|
||||
sidebar.focus(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.sidebar_open {
|
||||
let sidebar_is_focused = self
|
||||
.sidebar
|
||||
.as_ref()
|
||||
.is_some_and(|s| s.focus_handle(cx).contains_focused(window, cx));
|
||||
|
||||
if sidebar_is_focused {
|
||||
let pane = self.workspace().read(cx).active_pane().clone();
|
||||
let pane_focus = pane.read(cx).focus_handle(cx);
|
||||
window.focus(&pane_focus, cx);
|
||||
} else if let Some(sidebar) = &self.sidebar {
|
||||
sidebar.focus(window, cx);
|
||||
}
|
||||
} else {
|
||||
self.open_sidebar(cx);
|
||||
if let Some(sidebar) = &self.sidebar {
|
||||
sidebar.focus(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_sidebar(&mut self, cx: &mut Context<Self>) {
|
||||
self.sidebar_open = true;
|
||||
for workspace in &self.workspaces {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_workspace_sidebar_open(true, cx);
|
||||
});
|
||||
}
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn close_sidebar(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.sidebar_open = false;
|
||||
for workspace in &self.workspaces {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_workspace_sidebar_open(false, cx);
|
||||
});
|
||||
}
|
||||
let pane = self.workspace().read(cx).active_pane().clone();
|
||||
let pane_focus = pane.read(cx).focus_handle(cx);
|
||||
window.focus(&pane_focus, cx);
|
||||
self.serialize(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let workspaces = this.update(cx, |multi_workspace, _cx| {
|
||||
|
|
@ -318,10 +129,6 @@ impl MultiWorkspace {
|
|||
.detach();
|
||||
}
|
||||
|
||||
pub fn is_sidebar_open(&self) -> bool {
|
||||
self.sidebar_open
|
||||
}
|
||||
|
||||
pub fn workspace(&self) -> &Entity<Workspace> {
|
||||
&self.workspaces[self.active_workspace_index]
|
||||
}
|
||||
|
|
@ -335,7 +142,7 @@ impl MultiWorkspace {
|
|||
}
|
||||
|
||||
pub fn activate(&mut self, workspace: Entity<Workspace>, cx: &mut Context<Self>) {
|
||||
if !self.multi_workspace_enabled(cx) {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
self.workspaces[0] = workspace;
|
||||
self.active_workspace_index = 0;
|
||||
cx.emit(MultiWorkspaceEvent::ActiveWorkspaceChanged);
|
||||
|
|
@ -371,11 +178,6 @@ impl MultiWorkspace {
|
|||
if let Some(index) = self.workspaces.iter().position(|w| *w == workspace) {
|
||||
index
|
||||
} else {
|
||||
if self.sidebar_open {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.set_workspace_sidebar_open(true, cx);
|
||||
});
|
||||
}
|
||||
Self::subscribe_to_workspace(&workspace, cx);
|
||||
self.workspaces.push(workspace.clone());
|
||||
cx.emit(MultiWorkspaceEvent::WorkspaceAdded(workspace));
|
||||
|
|
@ -384,6 +186,14 @@ impl MultiWorkspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn database_id(&self) -> Option<MultiWorkspaceId> {
|
||||
self.database_id
|
||||
}
|
||||
|
||||
pub fn set_database_id(&mut self, id: Option<MultiWorkspaceId>) {
|
||||
self.database_id = id;
|
||||
}
|
||||
|
||||
pub fn activate_index(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
|
||||
debug_assert!(
|
||||
index < self.workspaces.len(),
|
||||
|
|
@ -421,7 +231,6 @@ impl MultiWorkspace {
|
|||
let window_id = self.window_id;
|
||||
let state = crate::persistence::model::MultiWorkspaceState {
|
||||
active_workspace_id: self.workspace().read(cx).database_id(),
|
||||
sidebar_open: self.sidebar_open,
|
||||
};
|
||||
self._serialize_task = Some(cx.background_spawn(async move {
|
||||
crate::persistence::write_multi_workspace_state(window_id, state).await;
|
||||
|
|
@ -540,7 +349,7 @@ impl MultiWorkspace {
|
|||
self.workspace().read(cx).items_of_type::<T>(cx)
|
||||
}
|
||||
|
||||
pub fn database_id(&self, cx: &App) -> Option<WorkspaceId> {
|
||||
pub fn active_workspace_database_id(&self, cx: &App) -> Option<WorkspaceId> {
|
||||
self.workspace().read(cx).database_id()
|
||||
}
|
||||
|
||||
|
|
@ -583,7 +392,7 @@ impl MultiWorkspace {
|
|||
}
|
||||
|
||||
pub fn create_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.multi_workspace_enabled(cx) {
|
||||
if !multi_workspace_enabled(cx) {
|
||||
return;
|
||||
}
|
||||
let app_state = self.workspace().read(cx).app_state().clone();
|
||||
|
|
@ -692,7 +501,7 @@ impl MultiWorkspace {
|
|||
) -> Task<Result<()>> {
|
||||
let workspace = self.workspace().clone();
|
||||
|
||||
if self.multi_workspace_enabled(cx) {
|
||||
if multi_workspace_enabled(cx) {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.open_workspace_for_paths(true, paths, window, cx)
|
||||
})
|
||||
|
|
@ -719,57 +528,6 @@ impl MultiWorkspace {
|
|||
|
||||
impl Render for MultiWorkspace {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let multi_workspace_enabled = self.multi_workspace_enabled(cx);
|
||||
|
||||
let sidebar: Option<AnyElement> = if multi_workspace_enabled && self.sidebar_open {
|
||||
self.sidebar.as_ref().map(|sidebar_handle| {
|
||||
let weak = cx.weak_entity();
|
||||
|
||||
let sidebar_width = sidebar_handle.width(cx);
|
||||
let resize_handle = deferred(
|
||||
div()
|
||||
.id("sidebar-resize-handle")
|
||||
.absolute()
|
||||
.right(-SIDEBAR_RESIZE_HANDLE_SIZE / 2.)
|
||||
.top(px(0.))
|
||||
.h_full()
|
||||
.w(SIDEBAR_RESIZE_HANDLE_SIZE)
|
||||
.cursor_col_resize()
|
||||
.on_drag(DraggedSidebar, |dragged, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
cx.new(|_| dragged.clone())
|
||||
})
|
||||
.on_mouse_down(MouseButton::Left, |_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, move |event, _, cx| {
|
||||
if event.click_count == 2 {
|
||||
weak.update(cx, |this, cx| {
|
||||
if let Some(sidebar) = this.sidebar.as_mut() {
|
||||
sidebar.set_width(None, cx);
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
cx.stop_propagation();
|
||||
}
|
||||
})
|
||||
.occlude(),
|
||||
);
|
||||
|
||||
div()
|
||||
.id("sidebar-container")
|
||||
.relative()
|
||||
.h_full()
|
||||
.w(sidebar_width)
|
||||
.flex_shrink_0()
|
||||
.child(sidebar_handle.to_any())
|
||||
.child(resize_handle)
|
||||
.into_any_element()
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let ui_font = theme::setup_ui_font(window, cx);
|
||||
let text_color = cx.theme().colors().text;
|
||||
|
||||
|
|
@ -799,32 +557,6 @@ impl Render for MultiWorkspace {
|
|||
this.activate_previous_workspace(window, cx);
|
||||
},
|
||||
))
|
||||
.when(self.multi_workspace_enabled(cx), |this| {
|
||||
this.on_action(cx.listener(
|
||||
|this: &mut Self, _: &ToggleWorkspaceSidebar, window, cx| {
|
||||
this.toggle_sidebar(window, cx);
|
||||
},
|
||||
))
|
||||
.on_action(cx.listener(
|
||||
|this: &mut Self, _: &FocusWorkspaceSidebar, window, cx| {
|
||||
this.focus_sidebar(window, cx);
|
||||
},
|
||||
))
|
||||
})
|
||||
.when(
|
||||
self.sidebar_open() && self.multi_workspace_enabled(cx),
|
||||
|this| {
|
||||
this.on_drag_move(cx.listener(
|
||||
|this: &mut Self, e: &DragMoveEvent<DraggedSidebar>, _window, cx| {
|
||||
if let Some(sidebar) = &this.sidebar {
|
||||
let new_width = e.event.position.x;
|
||||
sidebar.set_width(Some(new_width), cx);
|
||||
}
|
||||
},
|
||||
))
|
||||
.children(sidebar)
|
||||
},
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
|
|
@ -837,98 +569,9 @@ impl Render for MultiWorkspace {
|
|||
window,
|
||||
cx,
|
||||
Tiling {
|
||||
left: multi_workspace_enabled && self.sidebar_open,
|
||||
left: false,
|
||||
..Tiling::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use settings::SettingsStore;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
DisableAiSettings::register(cx);
|
||||
cx.update_flags(false, vec!["agent-v2".into()]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_sidebar_disabled_when_disable_ai_is_enabled(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
|
||||
|
||||
multi_workspace.read_with(cx, |mw, cx| {
|
||||
assert!(mw.multi_workspace_enabled(cx));
|
||||
});
|
||||
|
||||
multi_workspace.update_in(cx, |mw, _window, cx| {
|
||||
mw.open_sidebar(cx);
|
||||
assert!(mw.is_sidebar_open());
|
||||
});
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
DisableAiSettings::override_global(DisableAiSettings { disable_ai: true }, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
multi_workspace.read_with(cx, |mw, cx| {
|
||||
assert!(
|
||||
!mw.is_sidebar_open(),
|
||||
"Sidebar should be closed when disable_ai is true"
|
||||
);
|
||||
assert!(
|
||||
!mw.multi_workspace_enabled(cx),
|
||||
"Multi-workspace should be disabled when disable_ai is true"
|
||||
);
|
||||
});
|
||||
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.toggle_sidebar(window, cx);
|
||||
});
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
assert!(
|
||||
!mw.is_sidebar_open(),
|
||||
"Sidebar should remain closed when toggled with disable_ai true"
|
||||
);
|
||||
});
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
DisableAiSettings::override_global(DisableAiSettings { disable_ai: false }, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
multi_workspace.read_with(cx, |mw, cx| {
|
||||
assert!(
|
||||
mw.multi_workspace_enabled(cx),
|
||||
"Multi-workspace should be enabled after re-enabling AI"
|
||||
);
|
||||
assert!(
|
||||
!mw.is_sidebar_open(),
|
||||
"Sidebar should still be closed after re-enabling AI (not auto-opened)"
|
||||
);
|
||||
});
|
||||
|
||||
multi_workspace.update_in(cx, |mw, window, cx| {
|
||||
mw.toggle_sidebar(window, cx);
|
||||
});
|
||||
multi_workspace.read_with(cx, |mw, _cx| {
|
||||
assert!(
|
||||
mw.is_sidebar_open(),
|
||||
"Sidebar should open when toggled after re-enabling AI"
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@ pub fn read_serialized_multi_workspaces(
|
|||
.map(read_multi_workspace_state)
|
||||
.unwrap_or_default();
|
||||
model::SerializedMultiWorkspace {
|
||||
id: window_id.map(|id| model::MultiWorkspaceId(id.as_u64())),
|
||||
workspaces: group,
|
||||
state,
|
||||
}
|
||||
|
|
@ -3877,7 +3878,6 @@ mod tests {
|
|||
window_10,
|
||||
MultiWorkspaceState {
|
||||
active_workspace_id: Some(WorkspaceId(2)),
|
||||
sidebar_open: true,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -3886,7 +3886,6 @@ mod tests {
|
|||
window_20,
|
||||
MultiWorkspaceState {
|
||||
active_workspace_id: Some(WorkspaceId(3)),
|
||||
sidebar_open: false,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
|
@ -3924,23 +3923,20 @@ mod tests {
|
|||
// Should produce 3 groups: window 10, window 20, and the orphan.
|
||||
assert_eq!(results.len(), 3);
|
||||
|
||||
// Window 10 group: 2 workspaces, active_workspace_id = 2, sidebar open.
|
||||
// Window 10 group: 2 workspaces, active_workspace_id = 2.
|
||||
let group_10 = &results[0];
|
||||
assert_eq!(group_10.workspaces.len(), 2);
|
||||
assert_eq!(group_10.state.active_workspace_id, Some(WorkspaceId(2)));
|
||||
assert_eq!(group_10.state.sidebar_open, true);
|
||||
|
||||
// Window 20 group: 1 workspace, active_workspace_id = 3, sidebar closed.
|
||||
// Window 20 group: 1 workspace, active_workspace_id = 3.
|
||||
let group_20 = &results[1];
|
||||
assert_eq!(group_20.workspaces.len(), 1);
|
||||
assert_eq!(group_20.state.active_workspace_id, Some(WorkspaceId(3)));
|
||||
assert_eq!(group_20.state.sidebar_open, false);
|
||||
|
||||
// Orphan group: no window_id, so state is default.
|
||||
let group_none = &results[2];
|
||||
assert_eq!(group_none.workspaces.len(), 1);
|
||||
assert_eq!(group_none.state.active_workspace_id, None);
|
||||
assert_eq!(group_none.state.sidebar_open, false);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
|
|
|||
|
|
@ -63,18 +63,19 @@ pub struct SessionWorkspace {
|
|||
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
|
||||
pub struct MultiWorkspaceState {
|
||||
pub active_workspace_id: Option<WorkspaceId>,
|
||||
pub sidebar_open: bool,
|
||||
}
|
||||
|
||||
/// The serialized state of a single MultiWorkspace window from a previous session:
|
||||
/// all workspaces that shared the window, which one was active, and whether the
|
||||
/// sidebar was open.
|
||||
/// The serialized state of a single MultiWorkspace window from a previous session.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SerializedMultiWorkspace {
|
||||
pub id: Option<MultiWorkspaceId>,
|
||||
pub workspaces: Vec<SessionWorkspace>,
|
||||
pub state: MultiWorkspaceState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct MultiWorkspaceId(pub u64);
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub(crate) struct SerializedWorkspace {
|
||||
pub(crate) id: WorkspaceId,
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ pub struct StatusBar {
|
|||
right_items: Vec<Box<dyn StatusItemViewHandle>>,
|
||||
active_pane: Entity<Pane>,
|
||||
_observe_active_pane: Subscription,
|
||||
workspace_sidebar_open: bool,
|
||||
}
|
||||
|
||||
impl Render for StatusBar {
|
||||
|
|
@ -52,10 +51,9 @@ impl Render for StatusBar {
|
|||
.when(!(tiling.bottom || tiling.right), |el| {
|
||||
el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
.when(
|
||||
!(tiling.bottom || tiling.left) && !self.workspace_sidebar_open,
|
||||
|el| el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING),
|
||||
)
|
||||
.when(!(tiling.bottom || tiling.left), |el| {
|
||||
el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING)
|
||||
})
|
||||
// This border is to avoid a transparent gap in the rounded corners
|
||||
.mb(px(-1.))
|
||||
.border_b(px(1.0))
|
||||
|
|
@ -91,17 +89,11 @@ impl StatusBar {
|
|||
_observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| {
|
||||
this.update_active_pane_item(window, cx)
|
||||
}),
|
||||
workspace_sidebar_open: false,
|
||||
};
|
||||
this.update_active_pane_item(window, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn set_workspace_sidebar_open(&mut self, open: bool, cx: &mut Context<Self>) {
|
||||
self.workspace_sidebar_open = open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn add_left_item<T>(&mut self, item: Entity<T>, window: &mut Window, cx: &mut Context<Self>)
|
||||
where
|
||||
T: 'static + StatusItemView,
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ pub use crate::notifications::NotificationFrame;
|
|||
pub use dock::Panel;
|
||||
pub use multi_workspace::{
|
||||
DraggedSidebar, FocusWorkspaceSidebar, MultiWorkspace, MultiWorkspaceEvent,
|
||||
NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow, Sidebar, SidebarEvent,
|
||||
SidebarHandle, ToggleWorkspaceSidebar,
|
||||
NewWorkspaceInWindow, NextWorkspaceInWindow, PreviousWorkspaceInWindow,
|
||||
SIDEBAR_RESIZE_HANDLE_SIZE, ToggleWorkspaceSidebar, multi_workspace_enabled,
|
||||
};
|
||||
pub use path_list::{PathList, SerializedPathList};
|
||||
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
|
||||
|
|
@ -80,8 +80,8 @@ use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
|
|||
pub use persistence::{
|
||||
DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
|
||||
model::{
|
||||
DockStructure, ItemId, SerializedMultiWorkspace, SerializedWorkspaceLocation,
|
||||
SessionWorkspace,
|
||||
DockStructure, ItemId, MultiWorkspaceId, SerializedMultiWorkspace,
|
||||
SerializedWorkspaceLocation, SessionWorkspace,
|
||||
},
|
||||
read_serialized_multi_workspaces,
|
||||
};
|
||||
|
|
@ -2154,12 +2154,6 @@ impl Workspace {
|
|||
&self.status_bar
|
||||
}
|
||||
|
||||
pub fn set_workspace_sidebar_open(&self, open: bool, cx: &mut App) {
|
||||
self.status_bar.update(cx, |status_bar, cx| {
|
||||
status_bar.set_workspace_sidebar_open(open, cx);
|
||||
});
|
||||
}
|
||||
|
||||
pub fn status_bar_visible(&self, cx: &App) -> bool {
|
||||
StatusBarSettings::get_global(cx).show
|
||||
}
|
||||
|
|
@ -8184,7 +8178,11 @@ pub async fn restore_multiworkspace(
|
|||
app_state: Arc<AppState>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> anyhow::Result<MultiWorkspaceRestoreResult> {
|
||||
let SerializedMultiWorkspace { workspaces, state } = multi_workspace;
|
||||
let SerializedMultiWorkspace {
|
||||
workspaces,
|
||||
state,
|
||||
id: window_id,
|
||||
} = multi_workspace;
|
||||
let mut group_iter = workspaces.into_iter();
|
||||
let first = group_iter
|
||||
.next()
|
||||
|
|
@ -8248,6 +8246,7 @@ pub async fn restore_multiworkspace(
|
|||
if let Some(target_id) = state.active_workspace_id {
|
||||
window_handle
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.set_database_id(window_id);
|
||||
let target_index = multi_workspace
|
||||
.workspaces()
|
||||
.iter()
|
||||
|
|
@ -8269,14 +8268,6 @@ pub async fn restore_multiworkspace(
|
|||
.ok();
|
||||
}
|
||||
|
||||
if state.sidebar_open {
|
||||
window_handle
|
||||
.update(cx, |multi_workspace, _, cx| {
|
||||
multi_workspace.open_sidebar(cx);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
window_handle
|
||||
.update(cx, |_, window, _cx| {
|
||||
window.activate_window();
|
||||
|
|
|
|||
|
|
@ -182,7 +182,6 @@ settings.workspace = true
|
|||
settings_profile_selector.workspace = true
|
||||
settings_ui.workspace = true
|
||||
shellexpand.workspace = true
|
||||
sidebar.workspace = true
|
||||
smol.workspace = true
|
||||
snippet_provider.workspace = true
|
||||
snippets_ui.workspace = true
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ use {
|
|||
feature_flags::FeatureFlagAppExt as _,
|
||||
git_ui::project_diff::ProjectDiff,
|
||||
gpui::{
|
||||
App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString, VisualTestAppContext,
|
||||
WindowBounds, WindowHandle, WindowOptions, point, px, size,
|
||||
Action as _, App, AppContext as _, Bounds, KeyBinding, Modifiers, SharedString,
|
||||
VisualTestAppContext, WindowBounds, WindowHandle, WindowOptions, point, px, size,
|
||||
},
|
||||
image::RgbaImage,
|
||||
project_panel::ProjectPanel,
|
||||
|
|
@ -2649,22 +2649,6 @@ fn run_multi_workspace_sidebar_visual_tests(
|
|||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Create the sidebar and register it on the MultiWorkspace
|
||||
let sidebar = multi_workspace_window
|
||||
.update(cx, |_multi_workspace, window, cx| {
|
||||
let multi_workspace_handle = cx.entity();
|
||||
cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
|
||||
})
|
||||
.context("Failed to create sidebar")?;
|
||||
|
||||
multi_workspace_window
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.register_sidebar(sidebar.clone(), window, cx);
|
||||
})
|
||||
.context("Failed to register sidebar")?;
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Save test threads to the ThreadStore for each workspace
|
||||
let save_tasks = multi_workspace_window
|
||||
.update(cx, |multi_workspace, _window, cx| {
|
||||
|
|
@ -2742,8 +2726,8 @@ fn run_multi_workspace_sidebar_visual_tests(
|
|||
|
||||
// Open the sidebar
|
||||
multi_workspace_window
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.toggle_sidebar(window, cx);
|
||||
.update(cx, |_multi_workspace, window, cx| {
|
||||
window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx);
|
||||
})
|
||||
.context("Failed to toggle sidebar")?;
|
||||
|
||||
|
|
@ -3181,24 +3165,10 @@ edition = "2021"
|
|||
|
||||
cx.run_until_parked();
|
||||
|
||||
// Create and register the workspace sidebar
|
||||
let sidebar = workspace_window
|
||||
.update(cx, |_multi_workspace, window, cx| {
|
||||
let multi_workspace_handle = cx.entity();
|
||||
cx.new(|cx| sidebar::Sidebar::new(multi_workspace_handle, window, cx))
|
||||
})
|
||||
.context("Failed to create sidebar")?;
|
||||
|
||||
workspace_window
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.register_sidebar(sidebar.clone(), window, cx);
|
||||
})
|
||||
.context("Failed to register sidebar")?;
|
||||
|
||||
// Open the sidebar
|
||||
workspace_window
|
||||
.update(cx, |multi_workspace, window, cx| {
|
||||
multi_workspace.toggle_sidebar(window, cx);
|
||||
.update(cx, |_multi_workspace, window, cx| {
|
||||
window.dispatch_action(workspace::ToggleWorkspaceSidebar.boxed_clone(), cx);
|
||||
})
|
||||
.context("Failed to toggle sidebar")?;
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@ use settings::{
|
|||
initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content,
|
||||
update_settings_file,
|
||||
};
|
||||
use sidebar::Sidebar;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
|
|
@ -389,20 +388,6 @@ pub fn initialize_workspace(
|
|||
})
|
||||
.unwrap_or(true)
|
||||
});
|
||||
|
||||
let window_handle = window.window_handle();
|
||||
let multi_workspace_handle = cx.entity();
|
||||
cx.defer(move |cx| {
|
||||
window_handle
|
||||
.update(cx, |_, window, cx| {
|
||||
let sidebar =
|
||||
cx.new(|cx| Sidebar::new(multi_workspace_handle.clone(), window, cx));
|
||||
multi_workspace_handle.update(cx, |multi_workspace, cx| {
|
||||
multi_workspace.register_sidebar(sidebar, window, cx);
|
||||
});
|
||||
})
|
||||
.ok();
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue