mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 19:05:00 +07:00
sidebar: Show pending/unread state in project header when collapsed (#57322)
Closes AI-285 Similar to how we display whether there are running threads or a thread waiting for permission in the collapsed version of the project's header in the sidebar, I was missing the "unread" state from being shown. I had to change the approach here as to how we extract this information because the previous method was relying on observing the state of the list entries within the sidebar at every `rebuild_contents` run, and given there aren't any list entires when the project is collapsed, it wouldn't work. Release Notes: - Agent: Added the notification indicator on collapsed project headers in the sidebar when a thread completes.
This commit is contained in:
parent
7afcc87927
commit
7ec36d3661
3 changed files with 97 additions and 17 deletions
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8 11C9.65685 11 11 9.65685 11 8C11 6.34315 9.65685 5 8 5C6.34315 5 5 6.34315 5 8C5 9.65685 6.34315 11 8 11Z" fill="black"/>
|
||||
<path d="M8 12C10.2091 12 12 10.2091 12 8C12 5.79087 10.2091 4 8 4C5.79087 4 4 5.79087 4 8C4 10.2091 5.79087 12 8 12Z" fill="#C6CAD0"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 237 B After Width: | Height: | Size: 239 B |
|
|
@ -303,6 +303,7 @@ enum ListEntry {
|
|||
highlight_positions: Vec<usize>,
|
||||
has_running_threads: bool,
|
||||
waiting_thread_count: usize,
|
||||
has_notifications: bool,
|
||||
is_active: bool,
|
||||
has_threads: bool,
|
||||
},
|
||||
|
|
@ -654,6 +655,10 @@ pub struct Sidebar {
|
|||
thread_switcher: Option<Entity<ThreadSwitcher>>,
|
||||
_thread_switcher_subscriptions: Vec<gpui::Subscription>,
|
||||
pending_thread_activation: Option<agent_ui::ThreadId>,
|
||||
/// Persists live thread statuses across rebuilds so that Running→Completed
|
||||
/// transitions can be detected even when the group is collapsed (and
|
||||
/// thread entries are not present in the list).
|
||||
live_thread_statuses: HashMap<acp::SessionId, (AgentThreadStatus, ThreadId)>,
|
||||
view: SidebarView,
|
||||
restoring_tasks: HashMap<agent_ui::ThreadId, Task<()>>,
|
||||
recent_projects_popover_handle: PopoverMenuHandle<SidebarRecentProjects>,
|
||||
|
|
@ -762,6 +767,7 @@ impl Sidebar {
|
|||
thread_switcher: None,
|
||||
_thread_switcher_subscriptions: Vec::new(),
|
||||
pending_thread_activation: None,
|
||||
live_thread_statuses: HashMap::new(),
|
||||
view: SidebarView::default(),
|
||||
restoring_tasks: HashMap::new(),
|
||||
recent_projects_popover_handle: PopoverMenuHandle::default(),
|
||||
|
|
@ -1205,21 +1211,13 @@ impl Sidebar {
|
|||
|
||||
let previous = mem::take(&mut self.contents);
|
||||
|
||||
let old_statuses: HashMap<acp::SessionId, AgentThreadStatus> = previous
|
||||
.entries
|
||||
.iter()
|
||||
.filter_map(|entry| match entry {
|
||||
ListEntry::Thread(thread) if thread.is_live => {
|
||||
let sid = thread.metadata.session_id.clone()?;
|
||||
Some((sid, thread.status))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
let old_statuses = &self.live_thread_statuses;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut notified_threads = previous.notified_threads;
|
||||
let mut notified_terminals: HashSet<TerminalId> = HashSet::new();
|
||||
let mut new_live_statuses: HashMap<acp::SessionId, (AgentThreadStatus, ThreadId)> =
|
||||
HashMap::new();
|
||||
let mut current_session_ids: HashSet<acp::SessionId> = HashSet::new();
|
||||
let mut current_thread_ids: HashSet<agent_ui::ThreadId> = HashSet::new();
|
||||
let mut current_terminal_ids: HashSet<TerminalId> = HashSet::new();
|
||||
|
|
@ -1566,9 +1564,12 @@ impl Sidebar {
|
|||
// Merge live info into threads and update notification state
|
||||
// in a single pass.
|
||||
for thread in &mut threads {
|
||||
if let Some(session_id) = &thread.metadata.session_id {
|
||||
if let Some(info) = live_info_by_session.get(session_id) {
|
||||
if let Some(session_id) = thread.metadata.session_id.clone() {
|
||||
if let Some(&info) = live_info_by_session.get(&session_id) {
|
||||
let status = info.status;
|
||||
let thread_id = thread.metadata.thread_id;
|
||||
thread.apply_active_info(info);
|
||||
new_live_statuses.insert(session_id, (status, thread_id));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1582,8 +1583,10 @@ impl Sidebar {
|
|||
|
||||
if thread.status == AgentThreadStatus::Completed
|
||||
&& !is_active_thread
|
||||
&& session_id.as_ref().and_then(|sid| old_statuses.get(sid))
|
||||
== Some(&AgentThreadStatus::Running)
|
||||
&& session_id
|
||||
.as_ref()
|
||||
.and_then(|sid| old_statuses.get(sid))
|
||||
.is_some_and(|(s, _)| *s == AgentThreadStatus::Running)
|
||||
{
|
||||
notified_threads.insert(thread.metadata.thread_id);
|
||||
}
|
||||
|
|
@ -1599,13 +1602,35 @@ impl Sidebar {
|
|||
b_time.cmp(&a_time)
|
||||
});
|
||||
} else {
|
||||
for info in live_infos {
|
||||
for info in &live_infos {
|
||||
if info.status == AgentThreadStatus::Running {
|
||||
has_running_threads = true;
|
||||
}
|
||||
if info.status == AgentThreadStatus::WaitingForConfirmation {
|
||||
waiting_thread_count += 1;
|
||||
}
|
||||
// Resolve the thread_id for this session so we can
|
||||
// track its status and detect transitions even while
|
||||
// the group is collapsed.
|
||||
let thread_id = old_statuses
|
||||
.get(&info.session_id)
|
||||
.map(|(_, tid)| *tid)
|
||||
.or_else(|| {
|
||||
ThreadMetadataStore::global(cx)
|
||||
.read(cx)
|
||||
.entry_by_session(&info.session_id)
|
||||
.map(|m| m.thread_id)
|
||||
});
|
||||
|
||||
if let Some(thread_id) = thread_id {
|
||||
let old_status = old_statuses.get(&info.session_id).map(|(s, _)| *s);
|
||||
new_live_statuses.insert(info.session_id.clone(), (info.status, thread_id));
|
||||
if info.status == AgentThreadStatus::Completed
|
||||
&& old_status == Some(AgentThreadStatus::Running)
|
||||
{
|
||||
notified_threads.insert(thread_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1698,6 +1723,14 @@ impl Sidebar {
|
|||
continue;
|
||||
}
|
||||
|
||||
// Check for notifications: threads that completed while not active.
|
||||
let has_thread_notifications = matched_threads
|
||||
.iter()
|
||||
.any(|t| notified_threads.contains(&t.metadata.thread_id));
|
||||
let has_terminal_notifications = matched_terminals
|
||||
.iter()
|
||||
.any(|t| notified_terminals.contains(&t.metadata.terminal_id));
|
||||
|
||||
project_header_indices.push(entries.len());
|
||||
entries.push(ListEntry::ProjectHeader {
|
||||
key: group_key.clone(),
|
||||
|
|
@ -1705,6 +1738,7 @@ impl Sidebar {
|
|||
highlight_positions: workspace_highlight_positions,
|
||||
has_running_threads,
|
||||
waiting_thread_count,
|
||||
has_notifications: has_thread_notifications || has_terminal_notifications,
|
||||
is_active,
|
||||
has_threads,
|
||||
});
|
||||
|
|
@ -1717,6 +1751,32 @@ impl Sidebar {
|
|||
&mut current_thread_ids,
|
||||
);
|
||||
} else {
|
||||
let has_terminal_notifications = terminals
|
||||
.iter()
|
||||
.any(|t| notified_terminals.contains(&t.metadata.terminal_id));
|
||||
|
||||
// When collapsed, threads aren't loaded into `threads`, so we
|
||||
// query the store for thread IDs to check notifications and
|
||||
// to prevent the retain below from purging them.
|
||||
let has_thread_notifications = if threads.is_empty() && !notified_threads.is_empty()
|
||||
{
|
||||
let thread_store = ThreadMetadataStore::global(cx);
|
||||
let store = thread_store.read(cx);
|
||||
let group_thread_ids = store
|
||||
.entries_for_main_worktree_path(group_key.path_list(), group_host.as_ref())
|
||||
.chain(store.entries_for_path(group_key.path_list(), group_host.as_ref()))
|
||||
.map(|m| m.thread_id)
|
||||
.collect::<HashSet<_>>();
|
||||
current_thread_ids.extend(group_thread_ids.iter());
|
||||
group_thread_ids
|
||||
.iter()
|
||||
.any(|id| notified_threads.contains(id))
|
||||
} else {
|
||||
threads
|
||||
.iter()
|
||||
.any(|t| notified_threads.contains(&t.metadata.thread_id))
|
||||
};
|
||||
|
||||
project_header_indices.push(entries.len());
|
||||
entries.push(ListEntry::ProjectHeader {
|
||||
key: group_key.clone(),
|
||||
|
|
@ -1724,6 +1784,7 @@ impl Sidebar {
|
|||
highlight_positions: Vec::new(),
|
||||
has_running_threads,
|
||||
waiting_thread_count,
|
||||
has_notifications: has_thread_notifications || has_terminal_notifications,
|
||||
is_active,
|
||||
has_threads,
|
||||
});
|
||||
|
|
@ -1749,6 +1810,8 @@ impl Sidebar {
|
|||
self.terminal_last_accessed
|
||||
.retain(|id, _| current_terminal_ids.contains(id));
|
||||
|
||||
self.live_thread_statuses = new_live_statuses;
|
||||
|
||||
self.contents = SidebarContents {
|
||||
entries,
|
||||
notified_threads,
|
||||
|
|
@ -1862,6 +1925,7 @@ impl Sidebar {
|
|||
highlight_positions,
|
||||
has_running_threads,
|
||||
waiting_thread_count,
|
||||
has_notifications,
|
||||
is_active: is_active_group,
|
||||
has_threads,
|
||||
} => {
|
||||
|
|
@ -1885,6 +1949,7 @@ impl Sidebar {
|
|||
highlight_positions,
|
||||
*has_running_threads,
|
||||
*waiting_thread_count,
|
||||
*has_notifications,
|
||||
*is_active_group,
|
||||
is_selected,
|
||||
*has_threads,
|
||||
|
|
@ -1943,6 +2008,7 @@ impl Sidebar {
|
|||
highlight_positions: &[usize],
|
||||
has_running_threads: bool,
|
||||
waiting_thread_count: usize,
|
||||
has_notifications: bool,
|
||||
is_active: bool,
|
||||
is_focused: bool,
|
||||
has_threads: bool,
|
||||
|
|
@ -2054,6 +2120,16 @@ impl Sidebar {
|
|||
.tooltip(Tooltip::text(tooltip_text)),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
has_notifications && !has_running_threads && waiting_thread_count == 0,
|
||||
|this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Circle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Accent),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
|
|
@ -2509,6 +2585,7 @@ impl Sidebar {
|
|||
highlight_positions,
|
||||
has_running_threads,
|
||||
waiting_thread_count,
|
||||
has_notifications,
|
||||
is_active,
|
||||
has_threads,
|
||||
} = self.contents.entries.get(header_idx)?
|
||||
|
|
@ -2538,6 +2615,7 @@ impl Sidebar {
|
|||
&highlight_positions,
|
||||
*has_running_threads,
|
||||
*waiting_thread_count,
|
||||
*has_notifications,
|
||||
*is_active,
|
||||
is_selected,
|
||||
*has_threads,
|
||||
|
|
|
|||
|
|
@ -938,6 +938,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
|
|||
highlight_positions: Vec::new(),
|
||||
has_running_threads: false,
|
||||
waiting_thread_count: 0,
|
||||
has_notifications: false,
|
||||
is_active: true,
|
||||
has_threads: true,
|
||||
},
|
||||
|
|
@ -1084,6 +1085,7 @@ async fn test_visible_entries_as_strings(cx: &mut TestAppContext) {
|
|||
highlight_positions: Vec::new(),
|
||||
has_running_threads: false,
|
||||
waiting_thread_count: 0,
|
||||
has_notifications: false,
|
||||
is_active: false,
|
||||
has_threads: false,
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue