mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Cherry-pick of #56577 to preview ---- A bit brute force, but it works. <img width="1106" height="988" alt="image" src="https://github.com/user-attachments/assets/d23f9a80-01c5-4ad3-a280-faf8b8bc9dbe" /> Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com> Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
fac7b9245f
commit
3da868a4bf
8 changed files with 513 additions and 175 deletions
|
|
@ -78,8 +78,8 @@ use terminal::{Event as TerminalEvent, terminal_settings::TerminalSettings};
|
||||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Button, ContextMenu, ContextMenuEntry, GradientFade, IconButton, PopoverMenu,
|
Button, ContextMenu, ContextMenuEntry, GradientFade, IconButton, KeyBinding, PopoverMenu,
|
||||||
PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
|
PopoverMenuHandle, ProjectEmptyState, Tab, Tooltip, prelude::*, utils::WithRemSize,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
|
|
@ -936,36 +936,43 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let thread_to_restore = serialized_panel
|
let has_open_project = workspace
|
||||||
.as_ref()
|
.read_with(cx, |workspace, cx| !workspace.root_paths(cx).is_empty())
|
||||||
.and_then(|panel| panel.last_active_thread.as_ref())
|
.unwrap_or(false);
|
||||||
.and_then(|info| {
|
let thread_to_restore = if has_open_project {
|
||||||
let lookup = cx.update(|_window, cx| {
|
serialized_panel
|
||||||
let store = ThreadMetadataStore::global(cx);
|
.as_ref()
|
||||||
let store = store.read(cx);
|
.and_then(|panel| panel.last_active_thread.as_ref())
|
||||||
let primary = info.thread_id.and_then(|tid| store.entry(tid));
|
.and_then(|info| {
|
||||||
let fallback = info.session_id.as_ref().and_then(|sid| {
|
let lookup = cx.update(|_window, cx| {
|
||||||
store.entry_by_session(&acp::SessionId::new(sid.clone()))
|
let store = ThreadMetadataStore::global(cx);
|
||||||
|
let store = store.read(cx);
|
||||||
|
let primary = info.thread_id.and_then(|tid| store.entry(tid));
|
||||||
|
let fallback = info.session_id.as_ref().and_then(|sid| {
|
||||||
|
store.entry_by_session(&acp::SessionId::new(sid.clone()))
|
||||||
|
});
|
||||||
|
primary
|
||||||
|
.or(fallback)
|
||||||
|
.filter(|entry| !entry.archived)
|
||||||
|
.map(|entry| entry.thread_id)
|
||||||
});
|
});
|
||||||
primary
|
match lookup {
|
||||||
.or(fallback)
|
Ok(Some(thread_id)) => Some((info, thread_id)),
|
||||||
.filter(|entry| !entry.archived)
|
Ok(None) => {
|
||||||
.map(|entry| entry.thread_id)
|
log::info!(
|
||||||
});
|
"last active thread is archived or missing, skipping restoration"
|
||||||
match lookup {
|
);
|
||||||
Ok(Some(thread_id)) => Some((info, thread_id)),
|
None
|
||||||
Ok(None) => {
|
}
|
||||||
log::info!(
|
Err(err) => {
|
||||||
"last active thread is archived or missing, skipping restoration"
|
log::warn!("failed to look up last active thread metadata: {err}");
|
||||||
);
|
None
|
||||||
None
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
})
|
||||||
log::warn!("failed to look up last active thread metadata: {err}");
|
} else {
|
||||||
None
|
None
|
||||||
}
|
};
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
|
let panel = cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
|
||||||
|
|
@ -1091,23 +1098,15 @@ impl AgentPanel {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let connection_store = cx.new(|cx| {
|
let connection_store = cx.new(|cx| AgentConnectionStore::new(project.clone(), cx));
|
||||||
let mut store = AgentConnectionStore::new(project.clone(), cx);
|
|
||||||
// Register the native agent right away, so that it is available for
|
|
||||||
// the inline assistant etc.
|
|
||||||
store.request_connection(
|
|
||||||
Agent::NativeAgent,
|
|
||||||
Agent::NativeAgent.server(fs.clone(), thread_store.clone()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
store
|
|
||||||
});
|
|
||||||
let _project_subscription =
|
let _project_subscription =
|
||||||
cx.subscribe(&project, |this, _project, event, cx| match event {
|
cx.subscribe(&project, |this, _project, event, cx| match event {
|
||||||
project::Event::WorktreeAdded(_)
|
project::Event::WorktreeAdded(_)
|
||||||
| project::Event::WorktreeRemoved(_)
|
| project::Event::WorktreeRemoved(_)
|
||||||
| project::Event::WorktreeOrderChanged => {
|
| project::Event::WorktreeOrderChanged => {
|
||||||
|
this.ensure_native_agent_connection(cx);
|
||||||
this.update_thread_work_dirs(cx);
|
this.update_thread_work_dirs(cx);
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
|
@ -1164,6 +1163,7 @@ impl AgentPanel {
|
||||||
|
|
||||||
// Initial sync of agent servers from extensions
|
// Initial sync of agent servers from extensions
|
||||||
panel.sync_agent_servers_from_extensions(cx);
|
panel.sync_agent_servers_from_extensions(cx);
|
||||||
|
panel.ensure_native_agent_connection(cx);
|
||||||
panel
|
panel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1323,6 +1323,10 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.new_thread_with_workspace(None, window, cx);
|
self.new_thread_with_workspace(None, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1346,6 +1350,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.set_last_created_entry_kind(AgentPanelEntryKind::Thread, cx);
|
self.set_last_created_entry_kind(AgentPanelEntryKind::Thread, cx);
|
||||||
|
|
||||||
// If the user is viewing a *parked* draft and the ephemeral
|
// If the user is viewing a *parked* draft and the ephemeral
|
||||||
|
|
@ -1424,6 +1432,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let active_matching = match &self.base_view {
|
let active_matching = match &self.base_view {
|
||||||
BaseView::AgentThread { conversation_view }
|
BaseView::AgentThread { conversation_view }
|
||||||
if conversation_view.read(cx).thread_id == thread_id =>
|
if conversation_view.read(cx).thread_id == thread_id =>
|
||||||
|
|
@ -1477,6 +1489,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.selected_agent = action.agent.clone().into();
|
self.selected_agent = action.agent.clone().into();
|
||||||
self.activate_new_thread(true, "agent_panel", window, cx);
|
self.activate_new_thread(true, "agent_panel", window, cx);
|
||||||
}
|
}
|
||||||
|
|
@ -1487,6 +1503,9 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.supports_terminal(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let working_directory = self.terminal_working_directory(workspace, cx);
|
let working_directory = self.terminal_working_directory(workspace, cx);
|
||||||
self.spawn_terminal(TerminalId::new(), working_directory, true, window, cx);
|
self.spawn_terminal(TerminalId::new(), working_directory, true, window, cx);
|
||||||
}
|
}
|
||||||
|
|
@ -1501,6 +1520,10 @@ impl AgentPanel {
|
||||||
.unwrap_or_else(|| self.default_terminal_working_directory(cx))
|
.unwrap_or_else(|| self.default_terminal_working_directory(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn supports_terminal(&self, cx: &App) -> bool {
|
||||||
|
self.has_open_project(cx) && self.project.read(cx).supports_terminal(cx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn should_create_terminal_for_new_entry(&self, cx: &App) -> bool {
|
pub fn should_create_terminal_for_new_entry(&self, cx: &App) -> bool {
|
||||||
self.last_created_entry_kind == AgentPanelEntryKind::Terminal
|
self.last_created_entry_kind == AgentPanelEntryKind::Terminal
|
||||||
&& self.project.read(cx).supports_terminal(cx)
|
&& self.project.read(cx).supports_terminal(cx)
|
||||||
|
|
@ -2015,6 +2038,26 @@ impl AgentPanel {
|
||||||
.and_then(|workspace| terminal_view::default_working_directory(workspace.read(cx), cx))
|
.and_then(|workspace| terminal_view::default_working_directory(workspace.read(cx), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn has_open_project(&self, cx: &App) -> bool {
|
||||||
|
self.project.read(cx).visible_worktrees(cx).next().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_native_agent_connection(&self, cx: &mut Context<Self>) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fs = self.fs.clone();
|
||||||
|
let thread_store = self.thread_store.clone();
|
||||||
|
self.connection_store.update(cx, |store, cx| {
|
||||||
|
store.request_connection(
|
||||||
|
Agent::NativeAgent,
|
||||||
|
Agent::NativeAgent.server(fs, thread_store),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
pub fn activate_draft(
|
pub fn activate_draft(
|
||||||
&mut self,
|
&mut self,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
|
|
@ -2022,6 +2065,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let draft = self.ensure_draft(source, window, cx);
|
let draft = self.ensure_draft(source, window, cx);
|
||||||
if let BaseView::AgentThread { conversation_view } = &self.base_view {
|
if let BaseView::AgentThread { conversation_view } = &self.base_view {
|
||||||
if conversation_view.entity_id() == draft.entity_id() {
|
if conversation_view.entity_id() == draft.entity_id() {
|
||||||
|
|
@ -2445,6 +2492,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if resume_thread_id.is_none() && !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let agent = agent_choice.unwrap_or_else(|| self.selected_agent(cx));
|
let agent = agent_choice.unwrap_or_else(|| self.selected_agent(cx));
|
||||||
let thread = self.create_agent_thread_with_server(
|
let thread = self.create_agent_thread_with_server(
|
||||||
agent,
|
agent,
|
||||||
|
|
@ -2515,6 +2566,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
self.new_thread_menu_handle.toggle(window, cx);
|
self.new_thread_menu_handle.toggle(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2729,6 +2784,11 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
Self::show_deferred_toast(&self.workspace, "Open a project to load a thread", cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(clipboard) = cx.read_from_clipboard() else {
|
let Some(clipboard) = cx.read_from_clipboard() else {
|
||||||
Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
|
Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
|
||||||
return;
|
return;
|
||||||
|
|
@ -3815,6 +3875,10 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
|
if !self.has_open_project(cx) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if self.destination_has_meaningful_state(cx) {
|
if self.destination_has_meaningful_state(cx) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
@ -4000,21 +4064,24 @@ impl AgentPanel {
|
||||||
.max_w_full()
|
.max_w_full()
|
||||||
.overflow_x_hidden()
|
.overflow_x_hidden()
|
||||||
.child(content)
|
.child(content)
|
||||||
.when(!self.is_title_editor_focused(window, cx), |this| {
|
.when(
|
||||||
this.child(gradient_overlay).child(
|
self.has_open_project(cx) && !self.is_title_editor_focused(window, cx),
|
||||||
h_flex()
|
|this| {
|
||||||
.visible_on_hover("title_editor")
|
this.child(gradient_overlay).child(
|
||||||
.absolute()
|
h_flex()
|
||||||
.right_0()
|
.visible_on_hover("title_editor")
|
||||||
.h_full()
|
.absolute()
|
||||||
.bg(cx.theme().colors().tab_bar_background)
|
.right_0()
|
||||||
.child(
|
.h_full()
|
||||||
IconButton::new("edit_tile", IconName::Pencil)
|
.bg(cx.theme().colors().tab_bar_background)
|
||||||
.icon_size(IconSize::Small)
|
.child(
|
||||||
.tooltip(Tooltip::text("Edit Thread Title")),
|
IconButton::new("edit_tile", IconName::Pencil)
|
||||||
),
|
.icon_size(IconSize::Small)
|
||||||
)
|
.tooltip(Tooltip::text("Edit Thread Title")),
|
||||||
})
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4147,12 +4214,31 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_no_project_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
|
ProjectEmptyState::new(
|
||||||
|
"Agent Panel",
|
||||||
|
focus_handle.clone(),
|
||||||
|
KeyBinding::for_action_in(&workspace::Open::default(), &focus_handle, cx),
|
||||||
|
)
|
||||||
|
.on_open_project(|_, window, cx| {
|
||||||
|
telemetry::event!("Agent Panel Add Project Clicked");
|
||||||
|
window.dispatch_action(workspace::Open::default().boxed_clone(), cx);
|
||||||
|
})
|
||||||
|
.on_clone_repo(|_, window, cx| {
|
||||||
|
telemetry::event!("Agent Panel Clone Repo Clicked");
|
||||||
|
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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 agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||||
|
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
let supports_terminal = self.project.read(cx).supports_terminal(cx);
|
let can_create_entries = self.has_open_project(cx);
|
||||||
|
let supports_terminal = self.supports_terminal(cx);
|
||||||
let showing_terminal = matches!(self.visible_surface(), VisibleSurface::Terminal(_));
|
let showing_terminal = matches!(self.visible_surface(), VisibleSurface::Terminal(_));
|
||||||
|
|
||||||
let (selected_agent_custom_icon, selected_agent_label) = if showing_terminal {
|
let (selected_agent_custom_icon, selected_agent_label) = if showing_terminal {
|
||||||
|
|
@ -4483,7 +4569,7 @@ impl AgentPanel {
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.justify_between();
|
.justify_between();
|
||||||
|
|
||||||
let toolbar_content = if matches!(mode, ToolbarMode::EmptyThread) {
|
let toolbar_content = if can_create_entries && matches!(mode, ToolbarMode::EmptyThread) {
|
||||||
let (chevron_icon, icon_color, label_color) =
|
let (chevron_icon, icon_color, label_color) =
|
||||||
if self.new_thread_menu_handle.is_deployed() {
|
if self.new_thread_menu_handle.is_deployed() {
|
||||||
(IconName::ChevronUp, Color::Accent, Color::Accent)
|
(IconName::ChevronUp, Color::Accent, Color::Accent)
|
||||||
|
|
@ -4594,7 +4680,7 @@ impl AgentPanel {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.pr_1()
|
.pr_1()
|
||||||
.child(new_thread_menu)
|
.when(can_create_entries, |this| this.child(new_thread_menu))
|
||||||
.child(full_screen_button)
|
.child(full_screen_button)
|
||||||
.child(self.render_panel_options_menu(window, cx)),
|
.child(self.render_panel_options_menu(window, cx)),
|
||||||
)
|
)
|
||||||
|
|
@ -4833,10 +4919,11 @@ impl Render for AgentPanel {
|
||||||
// - Scrolling in all views works as expected
|
// - Scrolling in all views works as expected
|
||||||
// - Files can be dropped into the panel
|
// - Files can be dropped into the panel
|
||||||
let content = v_flex()
|
let content = v_flex()
|
||||||
|
.key_context(self.key_context())
|
||||||
.relative()
|
.relative()
|
||||||
.size_full()
|
.size_full()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.key_context(self.key_context())
|
.bg(cx.theme().colors().panel_background)
|
||||||
.on_action(cx.listener(|this, action: &NewThread, window, cx| {
|
.on_action(cx.listener(|this, action: &NewThread, window, cx| {
|
||||||
this.new_thread(action, window, cx);
|
this.new_thread(action, window, cx);
|
||||||
}))
|
}))
|
||||||
|
|
@ -4861,6 +4948,9 @@ impl Render for AgentPanel {
|
||||||
.child(self.render_toolbar(window, cx))
|
.child(self.render_toolbar(window, cx))
|
||||||
.children(self.render_new_user_onboarding(window, cx))
|
.children(self.render_new_user_onboarding(window, cx))
|
||||||
.map(|parent| match self.visible_surface() {
|
.map(|parent| match self.visible_surface() {
|
||||||
|
VisibleSurface::Uninitialized if !self.has_open_project(cx) => {
|
||||||
|
parent.child(self.render_no_project_state(cx))
|
||||||
|
}
|
||||||
VisibleSurface::Uninitialized => parent,
|
VisibleSurface::Uninitialized => parent,
|
||||||
VisibleSurface::AgentThread(conversation_view) => parent
|
VisibleSurface::AgentThread(conversation_view) => parent
|
||||||
.child(conversation_view.clone())
|
.child(conversation_view.clone())
|
||||||
|
|
@ -6408,6 +6498,94 @@ mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_empty_workspace_does_not_create_agent_entries(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
agent::ThreadStore::init_global(cx);
|
||||||
|
language_model::LanguageModelRegistry::test(cx);
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
let project = Project::test(fs.clone(), [], cx).await;
|
||||||
|
let multi_workspace =
|
||||||
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
let workspace = multi_workspace
|
||||||
|
.read_with(cx, |multi_workspace, _cx| {
|
||||||
|
multi_workspace.workspace().clone()
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
|
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
||||||
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
|
panel
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.read_with(cx, |panel, cx| {
|
||||||
|
assert_eq!(
|
||||||
|
panel
|
||||||
|
.connection_store()
|
||||||
|
.read(cx)
|
||||||
|
.connection_status(&Agent::NativeAgent, cx),
|
||||||
|
crate::agent_connection_store::AgentConnectionStatus::Disconnected,
|
||||||
|
"empty workspaces should not start the native agent connection"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.new_thread(&NewThread, window, cx);
|
||||||
|
panel.activate_draft(true, "agent_panel", window, cx);
|
||||||
|
panel.new_external_agent_thread(
|
||||||
|
&NewExternalAgentThread {
|
||||||
|
agent: AgentId::new("external-agent"),
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(cx, |panel, cx| {
|
||||||
|
assert!(
|
||||||
|
panel.active_conversation_view().is_none(),
|
||||||
|
"empty workspaces should not create agent threads"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
panel.draft_thread.is_none(),
|
||||||
|
"empty workspaces should not create draft threads"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
panel.terminals(cx).is_empty(),
|
||||||
|
"empty workspaces should not create agent panel terminals"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
cx.update_flags(true, vec!["agent-panel-terminal".to_string()]);
|
||||||
|
});
|
||||||
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
panel.new_terminal(None, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(cx, |panel, cx| {
|
||||||
|
assert!(
|
||||||
|
panel.terminals(cx).is_empty(),
|
||||||
|
"empty workspaces should not create terminals after the terminal feature is enabled"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
panel
|
||||||
|
.connection_store()
|
||||||
|
.read(cx)
|
||||||
|
.connection_status(&Agent::NativeAgent, cx),
|
||||||
|
crate::agent_connection_store::AgentConnectionStatus::Disconnected,
|
||||||
|
"empty workspace actions should not start the native agent connection"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
|
async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
|
|
@ -6417,7 +6595,8 @@ mod tests {
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -7612,7 +7791,8 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -7672,7 +7852,8 @@ mod tests {
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -7761,7 +7942,8 @@ mod tests {
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -7855,7 +8037,8 @@ mod tests {
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
let workspace = multi_workspace
|
let workspace = multi_workspace
|
||||||
|
|
@ -7964,7 +8147,8 @@ mod tests {
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
let workspace = multi_workspace
|
let workspace = multi_workspace
|
||||||
|
|
@ -8062,7 +8246,8 @@ mod tests {
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
||||||
});
|
});
|
||||||
|
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -8876,8 +9061,9 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree("/project", json!({ "file.txt": "" })).await;
|
||||||
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
cx.update(|cx| <dyn fs::Fs>::set_global(fs.clone(), cx));
|
||||||
let project = Project::test(fs.clone(), [], cx).await;
|
let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||||
|
|
@ -9102,8 +9288,12 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let project_a = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project_a", json!({ "file.txt": "" }))
|
||||||
let project_b = Project::test(fs.clone(), [], cx).await;
|
.await;
|
||||||
|
fs.insert_tree("/project_b", json!({ "file.txt": "" }))
|
||||||
|
.await;
|
||||||
|
let project_a = Project::test(fs.clone(), [Path::new("/project_a")], cx).await;
|
||||||
|
let project_b = Project::test(fs.clone(), [Path::new("/project_b")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||||
|
|
@ -9192,8 +9382,12 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
let fs = FakeFs::new(cx.executor());
|
||||||
let project_a = Project::test(fs.clone(), [], cx).await;
|
fs.insert_tree("/project_a", json!({ "file.txt": "" }))
|
||||||
let project_b = Project::test(fs.clone(), [], cx).await;
|
.await;
|
||||||
|
fs.insert_tree("/project_b", json!({ "file.txt": "" }))
|
||||||
|
.await;
|
||||||
|
let project_a = Project::test(fs.clone(), [Path::new("/project_a")], cx).await;
|
||||||
|
let project_b = Project::test(fs.clone(), [Path::new("/project_b")], cx).await;
|
||||||
|
|
||||||
let multi_workspace =
|
let multi_workspace =
|
||||||
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
|
||||||
|
|
|
||||||
|
|
@ -75,9 +75,9 @@ use strum::{IntoEnumIterator, VariantNames};
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonLike, Checkbox, ContextMenu, Divider, ElevationIndex, IndentGuideColors, PopoverMenu,
|
ButtonLike, Checkbox, ContextMenu, Divider, ElevationIndex, IndentGuideColors, KeyBinding,
|
||||||
RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab, TintColor, Tooltip,
|
PopoverMenu, ProjectEmptyState, RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab,
|
||||||
WithScrollbar, prelude::*,
|
TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||||
};
|
};
|
||||||
use util::paths::PathStyle;
|
use util::paths::PathStyle;
|
||||||
use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath};
|
use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath};
|
||||||
|
|
@ -5453,6 +5453,22 @@ impl GitPanel {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
} else if worktree_count == 0 {
|
||||||
|
let focus_handle = self.focus_handle.clone();
|
||||||
|
ProjectEmptyState::new(
|
||||||
|
"Git Panel",
|
||||||
|
focus_handle.clone(),
|
||||||
|
KeyBinding::for_action_in(&workspace::Open::default(), &focus_handle, cx),
|
||||||
|
)
|
||||||
|
.on_open_project(|_, window, cx| {
|
||||||
|
telemetry::event!("Git Panel Add Project Clicked");
|
||||||
|
window.dispatch_action(workspace::Open::default().boxed_clone(), cx);
|
||||||
|
})
|
||||||
|
.on_clone_repo(|_, window, cx| {
|
||||||
|
telemetry::event!("Git Panel Clone Repo Clicked");
|
||||||
|
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
Empty.into_any_element()
|
Empty.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,10 +60,10 @@ use std::{
|
||||||
};
|
};
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Divider, Icon, IconDecoration,
|
Color, ContextMenu, ContextMenuEntry, DecoratedIcon, Icon, IconDecoration, IconDecorationKind,
|
||||||
IconDecorationKind, IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label,
|
IndentGuideColors, IndentGuideLayout, Indicator, KeyBinding, Label, LabelSize, ListItem,
|
||||||
LabelSize, ListItem, ListItemSpacing, ScrollAxes, ScrollableHandle, Scrollbars,
|
ListItemSpacing, ProjectEmptyState, ScrollAxes, ScrollableHandle, Scrollbars, StickyCandidate,
|
||||||
StickyCandidate, Tooltip, WithScrollbar, prelude::*, v_flex,
|
Tooltip, WithScrollbar, prelude::*, v_flex,
|
||||||
};
|
};
|
||||||
use util::{
|
use util::{
|
||||||
ResultExt, TakeUntilExt, TryFutureExt,
|
ResultExt, TakeUntilExt, TryFutureExt,
|
||||||
|
|
@ -7112,52 +7112,35 @@ impl Render for ProjectPanel {
|
||||||
}))
|
}))
|
||||||
} else {
|
} else {
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let workspace_clone = self.workspace.clone();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("empty-project_panel")
|
.id("empty-project_panel-wrapper")
|
||||||
.p_4()
|
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
|
||||||
.justify_center()
|
|
||||||
.gap_1()
|
|
||||||
.track_focus(&self.focus_handle(cx))
|
|
||||||
.child(
|
.child(
|
||||||
Button::new("open_project", "Open Project")
|
ProjectEmptyState::new(
|
||||||
.full_width()
|
"Project Panel",
|
||||||
.key_binding(KeyBinding::for_action_in(
|
focus_handle.clone(),
|
||||||
&workspace::Open::default(),
|
KeyBinding::for_action_in(&workspace::Open::default(), &focus_handle, cx),
|
||||||
&focus_handle,
|
)
|
||||||
cx,
|
.on_open_project(move |_, window, cx| {
|
||||||
))
|
telemetry::event!("Project Panel Add Project Clicked");
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
workspace
|
||||||
this.workspace
|
.update(cx, |_, cx| {
|
||||||
.update(cx, |_, cx| {
|
window
|
||||||
window.dispatch_action(
|
.dispatch_action(workspace::Open::default().boxed_clone(), cx);
|
||||||
workspace::Open::default().boxed_clone(),
|
})
|
||||||
cx,
|
.log_err();
|
||||||
);
|
})
|
||||||
})
|
.on_clone_repo(move |_, window, cx| {
|
||||||
.log_err();
|
telemetry::event!("Project Panel Clone Repo Clicked");
|
||||||
})),
|
workspace_clone
|
||||||
)
|
.update(cx, |_, cx| {
|
||||||
.child(
|
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
||||||
h_flex()
|
})
|
||||||
.w_1_2()
|
.log_err();
|
||||||
.gap_2()
|
}),
|
||||||
.child(Divider::horizontal())
|
|
||||||
.child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
|
|
||||||
.child(Divider::horizontal()),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("clone_repo", "Clone Repository")
|
|
||||||
.full_width()
|
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
|
||||||
this.workspace
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
})),
|
|
||||||
)
|
)
|
||||||
.when(is_local, |div| {
|
.when(is_local, |div| {
|
||||||
div.when(panel_settings.drag_and_drop, |div| {
|
div.when(panel_settings.drag_and_drop, |div| {
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,9 @@ use std::sync::Arc;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{
|
use ui::{
|
||||||
AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, GradientFade, HighlightedLabel,
|
AgentThreadStatus, CommonAnimationExt, ContextMenu, Divider, GradientFade, HighlightedLabel,
|
||||||
KeyBinding, PopoverMenu, PopoverMenuHandle, ScrollAxes, Scrollbars, Tab, ThreadItem,
|
KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, ScrollAxes, Scrollbars, Tab,
|
||||||
ThreadItemWorktreeInfo, TintColor, Tooltip, WithScrollbar, prelude::*, render_modifiers,
|
ThreadItem, ThreadItemWorktreeInfo, TintColor, Tooltip, WithScrollbar, prelude::*,
|
||||||
|
render_modifiers,
|
||||||
};
|
};
|
||||||
use util::ResultExt as _;
|
use util::ResultExt as _;
|
||||||
use util::path_list::PathList;
|
use util::path_list::PathList;
|
||||||
|
|
@ -4719,6 +4720,10 @@ impl Sidebar {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if workspace_path_list(workspace, cx).paths().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if self.should_create_terminal_for_workspace(workspace, cx) {
|
if self.should_create_terminal_for_workspace(workspace, cx) {
|
||||||
self.create_new_terminal(workspace, window, cx);
|
self.create_new_terminal(workspace, window, cx);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -4743,6 +4748,10 @@ impl Sidebar {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if workspace_path_list(workspace, cx).paths().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
|
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -4776,6 +4785,10 @@ impl Sidebar {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if workspace_path_list(workspace, cx).paths().is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
|
let Some(multi_workspace) = self.multi_workspace.upgrade() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
@ -5025,48 +5038,28 @@ impl Sidebar {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
v_flex()
|
ProjectEmptyState::new(
|
||||||
.id("sidebar-empty-state")
|
"Threads Sidebar",
|
||||||
.p_4()
|
self.focus_handle(cx),
|
||||||
.size_full()
|
KeyBinding::for_action(&workspace::Open::default(), cx),
|
||||||
.items_center()
|
)
|
||||||
.justify_center()
|
.on_open_project(|_, window, cx| {
|
||||||
.gap_1()
|
let side = match AgentSettings::get_global(cx).sidebar_side() {
|
||||||
.track_focus(&self.focus_handle(cx))
|
SidebarSide::Left => "left",
|
||||||
.child(
|
SidebarSide::Right => "right",
|
||||||
Button::new("open_project", "Open Project")
|
};
|
||||||
.full_width()
|
telemetry::event!("Sidebar Add Project Clicked", side = side);
|
||||||
.key_binding(KeyBinding::for_action(&workspace::Open::default(), cx))
|
window.dispatch_action(
|
||||||
.on_click(|_, window, cx| {
|
Open {
|
||||||
let side = match AgentSettings::get_global(cx).sidebar_side() {
|
create_new_window: false,
|
||||||
SidebarSide::Left => "left",
|
}
|
||||||
SidebarSide::Right => "right",
|
.boxed_clone(),
|
||||||
};
|
cx,
|
||||||
telemetry::event!("Sidebar Add Project Clicked", side = side);
|
);
|
||||||
window.dispatch_action(
|
})
|
||||||
Open {
|
.on_clone_repo(|_, window, cx| {
|
||||||
create_new_window: false,
|
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
||||||
}
|
})
|
||||||
.boxed_clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_1_2()
|
|
||||||
.gap_2()
|
|
||||||
.child(Divider::horizontal().color(ui::DividerColor::Border))
|
|
||||||
.child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
|
|
||||||
.child(Divider::horizontal().color(ui::DividerColor::Border)),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
Button::new("clone_repo", "Clone Repository")
|
|
||||||
.full_width()
|
|
||||||
.on_click(|_, window, cx| {
|
|
||||||
window.dispatch_action(git::Clone.boxed_clone(), cx);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_sidebar_header(
|
fn render_sidebar_header(
|
||||||
|
|
|
||||||
|
|
@ -1366,6 +1366,43 @@ async fn test_keyboard_navigation_on_empty_list(cx: &mut TestAppContext) {
|
||||||
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
|
assert_eq!(sidebar.read_with(cx, |s, _| s.selection), Some(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_new_entry_noops_without_open_project(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
cx.update(|cx| <dyn Fs>::set_global(fs.clone(), cx));
|
||||||
|
let project = project::Project::test(fs, [], cx).await;
|
||||||
|
|
||||||
|
let (multi_workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
|
||||||
|
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||||
|
let workspace = multi_workspace.read_with(cx, |multi_workspace, _cx| {
|
||||||
|
multi_workspace.workspace().clone()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!sidebar.read_with(cx, |sidebar, _cx| sidebar.contents.has_open_projects),
|
||||||
|
"empty workspaces should be treated as having no open projects"
|
||||||
|
);
|
||||||
|
|
||||||
|
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||||
|
sidebar.create_new_entry(&workspace, window, cx);
|
||||||
|
});
|
||||||
|
cx.run_until_parked();
|
||||||
|
|
||||||
|
panel.read_with(cx, |panel, _cx| {
|
||||||
|
assert!(
|
||||||
|
panel.active_conversation_view().is_none(),
|
||||||
|
"sidebar should not create an agent thread without an open project"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&sidebar, cx),
|
||||||
|
Vec::<String>::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_selection_clamps_after_entry_removal(cx: &mut TestAppContext) {
|
async fn test_selection_clamps_after_entry_removal(cx: &mut TestAppContext) {
|
||||||
let project = init_test_project("/my-project", cx).await;
|
let project = init_test_project("/my-project", cx).await;
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ mod notification;
|
||||||
mod popover;
|
mod popover;
|
||||||
mod popover_menu;
|
mod popover_menu;
|
||||||
mod progress;
|
mod progress;
|
||||||
|
mod project_empty_state;
|
||||||
mod redistributable_columns;
|
mod redistributable_columns;
|
||||||
mod right_click_menu;
|
mod right_click_menu;
|
||||||
mod scrollbar;
|
mod scrollbar;
|
||||||
|
|
@ -71,6 +72,7 @@ pub use notification::*;
|
||||||
pub use popover::*;
|
pub use popover::*;
|
||||||
pub use popover_menu::*;
|
pub use popover_menu::*;
|
||||||
pub use progress::*;
|
pub use progress::*;
|
||||||
|
pub use project_empty_state::*;
|
||||||
pub use redistributable_columns::*;
|
pub use redistributable_columns::*;
|
||||||
pub use right_click_menu::*;
|
pub use right_click_menu::*;
|
||||||
pub use scrollbar::*;
|
pub use scrollbar::*;
|
||||||
|
|
|
||||||
94
crates/ui/src/components/project_empty_state.rs
Normal file
94
crates/ui/src/components/project_empty_state.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
use crate::{Divider, DividerColor, KeyBinding, prelude::*};
|
||||||
|
use gpui::{ClickEvent, FocusHandle};
|
||||||
|
|
||||||
|
type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ProjectEmptyState {
|
||||||
|
label: SharedString,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
open_project_key_binding: KeyBinding,
|
||||||
|
on_open_project: Option<ClickHandler>,
|
||||||
|
on_clone_repo: Option<ClickHandler>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProjectEmptyState {
|
||||||
|
pub fn new(
|
||||||
|
label: impl Into<SharedString>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
open_project_key_binding: KeyBinding,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
label: label.into(),
|
||||||
|
focus_handle,
|
||||||
|
open_project_key_binding,
|
||||||
|
on_open_project: None,
|
||||||
|
on_clone_repo: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_open_project(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_open_project = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_clone_repo(
|
||||||
|
mut self,
|
||||||
|
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_clone_repo = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ProjectEmptyState {
|
||||||
|
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
|
||||||
|
let id = format!("empty-state-{}", self.label);
|
||||||
|
let label = format!("Choose one of the options below to use the {}", self.label);
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id(id)
|
||||||
|
.p_4()
|
||||||
|
.size_full()
|
||||||
|
.items_center()
|
||||||
|
.justify_center()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_48()
|
||||||
|
.max_w_full()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_center()
|
||||||
|
.mb_2()
|
||||||
|
.child(Label::new(label).size(LabelSize::Small).color(Color::Muted)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("open_project", "Open Project")
|
||||||
|
.full_width()
|
||||||
|
.key_binding(self.open_project_key_binding)
|
||||||
|
.when_some(self.on_open_project, |button, handler| {
|
||||||
|
button.on_click(handler)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(Divider::horizontal().color(DividerColor::Border))
|
||||||
|
.child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted))
|
||||||
|
.child(Divider::horizontal().color(DividerColor::Border)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("clone_repo", "Clone Repository")
|
||||||
|
.full_width()
|
||||||
|
.when_some(self.on_clone_repo, |button, handler| {
|
||||||
|
button.on_click(handler)
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1025,16 +1025,35 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||||
let workspace =
|
let workspace =
|
||||||
multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?;
|
multi_workspace.read_with(cx, |mw, _| mw.workspace().clone())?;
|
||||||
|
|
||||||
let (client, thread_store) =
|
let import_state = multi_workspace.update(cx, |_, window, cx| {
|
||||||
multi_workspace.update(cx, |_, _window, cx| {
|
workspace.update(cx, |workspace, cx| {
|
||||||
workspace.update(cx, |workspace, cx| {
|
if workspace.root_paths(cx).is_empty() {
|
||||||
let client = workspace.project().read(cx).client();
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
|
|
||||||
.panel::<AgentPanel>(cx)
|
struct OpenProjectForSharedThreadToast;
|
||||||
.map(|panel| panel.read(cx).thread_store().clone());
|
workspace.show_toast(
|
||||||
anyhow::Ok((client, thread_store))
|
Toast::new(
|
||||||
})
|
NotificationId::unique::<OpenProjectForSharedThreadToast>(),
|
||||||
})??;
|
"Open a project to import shared threads",
|
||||||
|
)
|
||||||
|
.autohide(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
|
||||||
|
return anyhow::Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = workspace.project().read(cx).client();
|
||||||
|
let thread_store: Option<gpui::Entity<ThreadStore>> = workspace
|
||||||
|
.panel::<AgentPanel>(cx)
|
||||||
|
.map(|panel| panel.read(cx).thread_store().clone());
|
||||||
|
anyhow::Ok(Some((client, thread_store)))
|
||||||
|
})
|
||||||
|
})??;
|
||||||
|
|
||||||
|
let Some((client, thread_store)) = import_state else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
let Some(thread_store): Option<gpui::Entity<ThreadStore>> = thread_store else {
|
let Some(thread_store): Option<gpui::Entity<ThreadStore>> = thread_store else {
|
||||||
anyhow::bail!("Agent panel not available");
|
anyhow::bail!("Agent panel not available");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue