mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
sidebar: Fix stale sidebar thread header state (#57017)
There was a case where if you archived or closed all threads, you wouldn't see the empty state again. 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
This commit is contained in:
parent
c5f6fca756
commit
ad437c93c2
3 changed files with 155 additions and 26 deletions
|
|
@ -12,6 +12,7 @@ use agent_client_protocol::schema as acp;
|
|||
use anyhow::Context as _;
|
||||
use db::kvp::KeyValueStore;
|
||||
use gpui::{App, AppContext as _, Entity, Task};
|
||||
use itertools::Itertools;
|
||||
use ui::SharedString;
|
||||
use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
|
@ -128,7 +129,6 @@ pub fn display_label_for_draft(
|
|||
acp::ContentBlock::ResourceLink(link) => Some(link.uri.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
truncate_draft_label(&raw)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -220,6 +220,32 @@ impl ThreadEntryWorkspace {
|
|||
}
|
||||
}
|
||||
|
||||
fn draft_display_label_for_thread_metadata(
|
||||
metadata: &ThreadMetadata,
|
||||
workspace: &ThreadEntryWorkspace,
|
||||
cx: &App,
|
||||
) -> Option<SharedString> {
|
||||
let workspace = match workspace {
|
||||
ThreadEntryWorkspace::Open(workspace) => Some(workspace),
|
||||
ThreadEntryWorkspace::Closed { .. } => None,
|
||||
};
|
||||
agent_ui::draft_prompt_store::display_label_for_draft(workspace, metadata.thread_id, cx)
|
||||
}
|
||||
|
||||
fn thread_metadata_would_render_sidebar_row(
|
||||
metadata: &ThreadMetadata,
|
||||
workspace: &ThreadEntryWorkspace,
|
||||
hidden_draft_thread_ids: &HashSet<ThreadId>,
|
||||
cx: &App,
|
||||
) -> bool {
|
||||
if !metadata.is_draft() {
|
||||
return true;
|
||||
}
|
||||
|
||||
!hidden_draft_thread_ids.contains(&metadata.thread_id)
|
||||
&& draft_display_label_for_thread_metadata(metadata, workspace, cx).is_some()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ThreadEntry {
|
||||
metadata: ThreadMetadata,
|
||||
|
|
@ -1385,19 +1411,18 @@ impl Sidebar {
|
|||
let mut has_running_threads = false;
|
||||
let mut waiting_thread_count: usize = 0;
|
||||
let group_host = group_key.host();
|
||||
let hidden_draft_thread_ids: HashSet<ThreadId> = group_workspaces
|
||||
.iter()
|
||||
.filter_map(|ws| {
|
||||
ws.read(cx)
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| panel.read(cx).ephemeral_draft_thread_id(cx))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if should_load_threads {
|
||||
let thread_store = ThreadMetadataStore::global(cx);
|
||||
|
||||
let ephemeral_drafts: HashSet<ThreadId> = group_workspaces
|
||||
.iter()
|
||||
.filter_map(|ws| {
|
||||
ws.read(cx)
|
||||
.panel::<AgentPanel>(cx)
|
||||
.and_then(|panel| panel.read(cx).ephemeral_draft_thread_id(cx))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let make_thread_entry =
|
||||
|row: ThreadMetadata, workspace: ThreadEntryWorkspace| -> ThreadEntry {
|
||||
let (icon, icon_from_external_svg) = resolve_agent_icon(&row.agent_id);
|
||||
|
|
@ -1503,20 +1528,18 @@ impl Sidebar {
|
|||
}
|
||||
}
|
||||
|
||||
if !ephemeral_drafts.is_empty() {
|
||||
threads.retain(|thread| !ephemeral_drafts.contains(&thread.metadata.thread_id));
|
||||
if !hidden_draft_thread_ids.is_empty() {
|
||||
threads.retain(|thread| {
|
||||
!hidden_draft_thread_ids.contains(&thread.metadata.thread_id)
|
||||
});
|
||||
}
|
||||
for thread in &mut threads {
|
||||
if !thread.is_draft {
|
||||
continue;
|
||||
}
|
||||
let workspace = match &thread.workspace {
|
||||
ThreadEntryWorkspace::Open(workspace) => Some(workspace),
|
||||
ThreadEntryWorkspace::Closed { .. } => None,
|
||||
};
|
||||
thread.metadata.title = agent_ui::draft_prompt_store::display_label_for_draft(
|
||||
workspace,
|
||||
thread.metadata.thread_id,
|
||||
thread.metadata.title = draft_display_label_for_thread_metadata(
|
||||
&thread.metadata,
|
||||
&thread.workspace,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
|
@ -1582,19 +1605,33 @@ impl Sidebar {
|
|||
}
|
||||
}
|
||||
|
||||
let has_threads = if !threads.is_empty() || !terminals.is_empty() {
|
||||
true
|
||||
} else {
|
||||
let has_visible_rows = !threads.is_empty() || !terminals.is_empty();
|
||||
let has_stored_thread_rows = !should_load_threads && !has_visible_rows && {
|
||||
let store = ThreadMetadataStore::global(cx).read(cx);
|
||||
store
|
||||
.entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
|
||||
.next()
|
||||
.is_some()
|
||||
.any(|metadata| {
|
||||
let workspace = resolve_workspace(metadata.folder_paths());
|
||||
thread_metadata_would_render_sidebar_row(
|
||||
metadata,
|
||||
&workspace,
|
||||
&hidden_draft_thread_ids,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
|| store
|
||||
.entries_for_path(group_key.path_list(), group_host.as_ref())
|
||||
.next()
|
||||
.is_some()
|
||||
.any(|metadata| {
|
||||
let workspace = resolve_workspace(metadata.folder_paths());
|
||||
thread_metadata_would_render_sidebar_row(
|
||||
metadata,
|
||||
&workspace,
|
||||
&hidden_draft_thread_ids,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
};
|
||||
let has_threads = has_visible_rows || has_stored_thread_rows;
|
||||
|
||||
if !query.is_empty() {
|
||||
let workspace_highlight_positions =
|
||||
|
|
|
|||
|
|
@ -95,6 +95,34 @@ fn has_thread_entry(sidebar: &Sidebar, session_id: &acp::SessionId) -> bool {
|
|||
.any(|entry| matches!(entry, ListEntry::Thread(t) if t.metadata.session_id.as_ref() == Some(session_id)))
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_project_header_has_threads(
|
||||
sidebar: &Entity<Sidebar>,
|
||||
project_name: &str,
|
||||
expected_has_threads: bool,
|
||||
cx: &mut gpui::VisualTestContext,
|
||||
) {
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
let has_threads = sidebar.contents.entries.iter().find_map(|entry| {
|
||||
if let ListEntry::ProjectHeader {
|
||||
label, has_threads, ..
|
||||
} = entry
|
||||
&& label.as_ref() == project_name
|
||||
{
|
||||
Some(*has_threads)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
has_threads,
|
||||
Some(expected_has_threads),
|
||||
"expected project header `{project_name}` to have has_threads={expected_has_threads}, got {has_threads:?}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn assert_remote_project_integration_sidebar_state(
|
||||
sidebar: &mut Sidebar,
|
||||
|
|
@ -1540,6 +1568,70 @@ async fn test_agent_panel_terminals_appear_in_sidebar_and_search(cx: &mut TestAp
|
|||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_closing_last_agent_panel_terminal_restores_empty_header(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/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, cx);
|
||||
|
||||
assert_project_header_has_threads(&sidebar, "my-project", false, cx);
|
||||
|
||||
let terminal_id = panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Dev Server", true, window, cx)
|
||||
})
|
||||
.expect("test terminal should be inserted");
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_project_header_has_threads(&sidebar, "my-project", true, cx);
|
||||
|
||||
let (terminal_metadata, terminal_workspace) = sidebar.read_with(cx, |sidebar, _cx| {
|
||||
sidebar
|
||||
.contents
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
ListEntry::Terminal(terminal) if terminal.metadata.terminal_id == terminal_id => {
|
||||
Some((terminal.metadata.clone(), terminal.workspace.clone()))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.expect("terminal should be visible in sidebar")
|
||||
});
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.close_terminal(&terminal_metadata, &terminal_workspace, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
panel.read_with(cx, |panel, cx| {
|
||||
assert!(!panel.has_terminal(terminal_id));
|
||||
assert!(
|
||||
panel.active_view_is_new_draft(cx),
|
||||
"closing the active terminal should leave the panel on a hidden empty draft"
|
||||
);
|
||||
});
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
vec!["v [my-project]"]
|
||||
);
|
||||
assert_project_header_has_threads(&sidebar, "my-project", false, cx);
|
||||
|
||||
let project_group_key = multi_workspace.read_with(cx, |multi_workspace, cx| {
|
||||
multi_workspace.workspace().read(cx).project_group_key(cx)
|
||||
});
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.toggle_collapse(&project_group_key, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&sidebar, cx),
|
||||
vec!["> [my-project]"]
|
||||
);
|
||||
assert_project_header_has_threads(&sidebar, "my-project", false, cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_panel_terminal_metadata_remains_visible_after_panel_is_removed(
|
||||
cx: &mut TestAppContext,
|
||||
|
|
|
|||
Loading…
Reference in a new issue