From 192a0585b649b8d59c5505fdae8f8bcbc82ff316 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 25 Sep 2026 11:06:37 +0700 Subject: [PATCH] add avatar to tab --- CHANGELOG.md | 4 +- crates/workspace/src/views/commit_diff/mod.rs | 15 ++++--- crates/workspace/src/views/inbox.rs | 4 +- crates/workspace/src/views/issues/detail.rs | 5 ++- crates/workspace/src/views/issues/mod.rs | 8 +++- crates/workspace/src/views/mod.rs | 45 +++++++++++++++++++ .../src/views/pull_requests/detail.rs | 10 +++-- .../workspace/src/views/pull_requests/mod.rs | 11 ++--- .../workspace/src/views/pull_requests/new.rs | 12 ++--- crates/workspace/src/views/repo/history.rs | 6 ++- crates/workspace/src/views/repo/mod.rs | 7 ++- crates/workspace/src/views/repo_list.rs | 4 +- crates/workspace/src/views/send_patch.rs | 9 +++- 13 files changed, 109 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9bc62d9..8eec138 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,12 @@ ### Added +- Show an avatar in each panel's tab, using the repository owner's profile picture when set and a pixel avatar otherwise + ### Changed - Migrate the GPUI foundation to the published `gpui-pre` crates and GPUI Kit 0.6, off the zed and gpui-component git pins -- Redesign the dock tab bar: the active tab is a muted grey, each tab holds a close button that shows on hover or when active, double-clicking a tab zooms its panel, and the trailing panel toolbar is gone +- Redesign the dock tab bar, using muted grey active tab, added close buttons, double-click to zoom, and removed panel toolbar ### Fixed diff --git a/crates/workspace/src/views/commit_diff/mod.rs b/crates/workspace/src/views/commit_diff/mod.rs index 98ad225..b907adb 100644 --- a/crates/workspace/src/views/commit_diff/mod.rs +++ b/crates/workspace/src/views/commit_diff/mod.rs @@ -18,10 +18,12 @@ use gpui_component::{ ActiveTheme, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, v_virtual_list, }; use signed_git::{CommitDiff, DiffHunk, DiffLine, DiffLineKind, DiffStatus, FileCommit, FileDiff}; +use signed_state::RepoStore; use signed_ui::{placeholder, tree_row}; use utils::relative_time_secs; use crate::views::tree::{build_tree_items, tree_items}; +use crate::views::{repo_tab_avatar, tab_title}; const TREE_WIDTH: f32 = 260.; @@ -293,6 +295,7 @@ impl Render for DiffPane { pub struct CommitDiffView { focus_handle: FocusHandle, + store: Entity, worktree: PathBuf, repo_name: SharedString, commit: FileCommit, @@ -304,6 +307,7 @@ pub struct CommitDiffView { impl CommitDiffView { pub fn new( + store: Entity, worktree: PathBuf, repo_name: SharedString, commit_id: String, @@ -319,6 +323,7 @@ impl CommitDiffView { Self { focus_handle: cx.focus_handle(), + store, worktree, repo_name, commit: FileCommit { @@ -465,11 +470,11 @@ impl BasePanel for CommitDiffView { } impl Panel for CommitDiffView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().text_sm().child(SharedString::from(format!( - "{}/{}", - self.repo_name, self.commit.id - ))) + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); + let label = SharedString::from(format!("{}/{}", self.repo_name, self.commit.id)); + + tab_title(avatar, label) } } diff --git a/crates/workspace/src/views/inbox.rs b/crates/workspace/src/views/inbox.rs index ad96522..df319ec 100644 --- a/crates/workspace/src/views/inbox.rs +++ b/crates/workspace/src/views/inbox.rs @@ -18,7 +18,7 @@ use signed_state::{ use signed_ui::{CountBadge, UserAvatar}; use utils::relative_time; -use super::{RepoItem, open_repo_item}; +use super::{RepoItem, open_repo_item, panel_avatar, tab_title}; const LIST_OVERDRAW: Pixels = px(400.); const MAX_SUB_ACTIVITIES: usize = 5; @@ -619,7 +619,7 @@ impl BasePanel for InboxView { impl Panel for InboxView { fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().text_sm().child(SharedString::from("Inbox")) + tab_title(panel_avatar("inbox"), SharedString::from("Inbox")) } } diff --git a/crates/workspace/src/views/issues/detail.rs b/crates/workspace/src/views/issues/detail.rs index ecb78cb..f8ef447 100644 --- a/crates/workspace/src/views/issues/detail.rs +++ b/crates/workspace/src/views/issues/detail.rs @@ -14,6 +14,7 @@ use signed_ui::{UserAvatar, placeholder, status_badge}; use utils::relative_time; use crate::views::discussion::{comment_form, comments_section, issue_roots, sidebar_section}; +use crate::views::{repo_tab_avatar, tab_title}; pub struct IssueDetailView { focus_handle: FocusHandle, @@ -65,7 +66,9 @@ impl Panel for IssueDetailView { }) .unwrap_or_else(|| SharedString::from("Issue")); - div().text_sm().child(short_id) + let avatar = repo_tab_avatar(self.store.read(cx), cx); + + tab_title(avatar, short_id) } } diff --git a/crates/workspace/src/views/issues/mod.rs b/crates/workspace/src/views/issues/mod.rs index 5100175..bf65e99 100644 --- a/crates/workspace/src/views/issues/mod.rs +++ b/crates/workspace/src/views/issues/mod.rs @@ -25,6 +25,7 @@ pub(super) mod detail; use self::detail::IssueDetailView; use super::status_list::{StatusCounts, filter_by_status}; +use crate::views::{repo_tab_avatar, tab_title}; const ISSUE_ROW_HEIGHT: f32 = 73.; @@ -333,8 +334,11 @@ impl BasePanel for IssuesView { } impl Panel for IssuesView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(SharedString::from(format!("{}/issues", self.repo_name))) + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); + let label = SharedString::from(format!("{}/issues", self.repo_name)); + + tab_title(avatar, label) } } diff --git a/crates/workspace/src/views/mod.rs b/crates/workspace/src/views/mod.rs index 6c8caa1..598c273 100644 --- a/crates/workspace/src/views/mod.rs +++ b/crates/workspace/src/views/mod.rs @@ -11,8 +11,53 @@ pub(crate) mod sidebar; mod status_list; pub(crate) mod tree; +use gpui::prelude::*; +use gpui::{AnyElement, App, div}; +use gpui_component::{Sizable as _, h_flex}; pub use inbox::InboxView; pub use repo::RepoDetailView; pub(crate) use repo::{RepoItem, open_repo_item, open_repo_panel}; pub use repo_list::RepoListView; pub use sidebar::SidebarPanel; +use signed_state::{ProfileStore, RepoStore}; +use signed_ui::{PixelAvatar, UserAvatar}; + +pub(crate) fn tab_title(avatar: AnyElement, label: impl IntoElement) -> impl IntoElement { + h_flex() + .gap_1() + .items_center() + .child(div().flex_shrink_0().child(avatar)) + .child(label) +} + +pub(crate) fn panel_avatar(seed: impl AsRef) -> AnyElement { + PixelAvatar::new(seed).xsmall().into_any_element() +} + +pub(crate) fn repo_tab_avatar(store: &RepoStore, cx: &App) -> AnyElement { + let seed = store + .announcement + .as_ref() + .map(|announcement| format!("{}:{}", announcement.owner, announcement.id)) + .or_else(|| store.addr().map(|addr| addr.to_string())) + .or_else(|| { + store + .path + .as_ref() + .map(|path| path.to_string_lossy().into_owned()) + }) + .unwrap_or_default(); + + let owner = store + .announcement + .as_ref() + .map(|announcement| ProfileStore::global(cx).read(cx).get(&announcement.owner)); + + match owner.and_then(|profile| profile.picture().map(|picture| (profile.name(), picture))) { + Some((name, picture)) => UserAvatar::new(name) + .picture(Some(picture)) + .xsmall() + .into_any_element(), + None => PixelAvatar::new(seed).xsmall().into_any_element(), + } +} diff --git a/crates/workspace/src/views/pull_requests/detail.rs b/crates/workspace/src/views/pull_requests/detail.rs index 0a45185..9748efd 100644 --- a/crates/workspace/src/views/pull_requests/detail.rs +++ b/crates/workspace/src/views/pull_requests/detail.rs @@ -32,6 +32,7 @@ use utils::{relative_time, relative_time_secs}; use crate::views::commit_diff::{CommitDiffView, DiffPane}; use crate::views::discussion::{comment_form, comments_section, pr_roots, sidebar_section}; +use crate::views::{repo_tab_avatar, tab_title}; const ROW_HEIGHT: f32 = 37.; @@ -343,6 +344,7 @@ impl PullRequestDetailView { let panel = cx.new(|cx| { CommitDiffView::new( + self.store.clone(), worktree, self.repo_name.clone(), commit_id.into(), @@ -788,15 +790,17 @@ impl BasePanel for PullRequestDetailView { } impl Panel for PullRequestDetailView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); let hex = self.pr_id.to_hex(); let id = SharedString::from(&hex[..8]); - let title = if self.repo_name.is_empty() { + let label = if self.repo_name.is_empty() { id } else { SharedString::from(format!("{}/{}", self.repo_name, id)) }; - div().text_sm().child(title) + + tab_title(avatar, label) } } diff --git a/crates/workspace/src/views/pull_requests/mod.rs b/crates/workspace/src/views/pull_requests/mod.rs index f27af31..36e9cfc 100644 --- a/crates/workspace/src/views/pull_requests/mod.rs +++ b/crates/workspace/src/views/pull_requests/mod.rs @@ -27,6 +27,7 @@ use self::new::open_new_pull_panel; use super::send_patch::open_send_patch_panel; use super::status_list::{StatusCounts, filter_by_status}; use crate::views::repo::RepoAction; +use crate::views::{repo_tab_avatar, tab_title}; const ROW_HEIGHT: f32 = 73.; @@ -333,11 +334,11 @@ impl BasePanel for PullRequestsView { } impl Panel for PullRequestsView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(SharedString::from(format!( - "{}/pull-requests", - self.repo_name - ))) + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); + let label = SharedString::from(format!("{}/pulls", self.repo_name)); + + tab_title(avatar, label) } } diff --git a/crates/workspace/src/views/pull_requests/new.rs b/crates/workspace/src/views/pull_requests/new.rs index 5a5f238..a3e3ef8 100644 --- a/crates/workspace/src/views/pull_requests/new.rs +++ b/crates/workspace/src/views/pull_requests/new.rs @@ -32,6 +32,7 @@ use signed_state::{ use signed_ui::{CountBadge, placeholder, ref_selector_trigger}; use crate::views::commit_diff::{COMMIT_ROW_HEIGHT, CommitDiffView, DiffPane, commit_row}; +use crate::views::{repo_tab_avatar, tab_title}; pub struct NewPullRequestView { focus_handle: FocusHandle, @@ -924,6 +925,7 @@ impl NewPullRequestView { let panel = cx.new(|cx| { CommitDiffView::new( + self.store.clone(), repo_path, self.repo_name.clone(), commit_id.into(), @@ -1323,11 +1325,11 @@ impl BasePanel for NewPullRequestView { } impl Panel for NewPullRequestView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(SharedString::from(format!( - "{}/new-pull-request", - self.repo_name - ))) + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); + let label = SharedString::from(format!("{}/new-pull-request", self.repo_name)); + + tab_title(avatar, label) } } diff --git a/crates/workspace/src/views/repo/history.rs b/crates/workspace/src/views/repo/history.rs index 4ec8db5..21360ed 100644 --- a/crates/workspace/src/views/repo/history.rs +++ b/crates/workspace/src/views/repo/history.rs @@ -106,8 +106,10 @@ impl RepoHistoryView { // Same display name as the repo detail panel's title. let repo_name = repo_display_name(self.store.read(cx)); - let panel = - cx.new(|cx| CommitDiffView::new(worktree, repo_name, commit_id.into(), window, cx)); + let store = self.store.clone(); + let panel = cx.new(|cx| { + CommitDiffView::new(store, worktree, repo_name, commit_id.into(), window, cx) + }); dock_area.update(cx, |dock_area, cx| { add_center_panel(dock_area, panel_handle(panel), window, cx); diff --git a/crates/workspace/src/views/repo/mod.rs b/crates/workspace/src/views/repo/mod.rs index 9007ea3..eb467b9 100644 --- a/crates/workspace/src/views/repo/mod.rs +++ b/crates/workspace/src/views/repo/mod.rs @@ -55,6 +55,7 @@ use crate::views::pull_requests::new::open_new_pull_panel; use crate::views::repo::about::open_about_dialog; use crate::views::send_patch::open_send_patch_panel; use crate::views::tree::{TreeItemSeed, build_tree_items, sorted_worktree_paths}; +use crate::views::{repo_tab_avatar, tab_title}; #[derive(Clone, Copy, PartialEq, Eq)] enum RefKind { @@ -2054,7 +2055,11 @@ impl BasePanel for RepoDetailView { impl Panel for RepoDetailView { fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - repo_display_name(self.store.read(cx)) + let store = self.store.read(cx); + let name = repo_display_name(store); + let avatar = repo_tab_avatar(store, cx); + + tab_title(avatar, name) } } diff --git a/crates/workspace/src/views/repo_list.rs b/crates/workspace/src/views/repo_list.rs index ed61ab1..fae8991 100644 --- a/crates/workspace/src/views/repo_list.rs +++ b/crates/workspace/src/views/repo_list.rs @@ -19,7 +19,7 @@ use signed_state::{ProfileStore, RepoListStore, Timestamp}; use signed_ui::{SegmentButton, UserAvatar}; use utils::relative_time; -use super::open_repo_panel; +use super::{open_repo_panel, panel_avatar, tab_title}; const COLUMNS: usize = 2; const CARD_HEIGHT: f32 = 40. + 64. + 48. + 2. + 6.; @@ -389,7 +389,7 @@ impl BasePanel for RepoListView { impl Panel for RepoListView { fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().text_sm().child(SharedString::from("Explore")) + tab_title(panel_avatar("explore"), SharedString::from("Explore")) } } diff --git a/crates/workspace/src/views/send_patch.rs b/crates/workspace/src/views/send_patch.rs index ff5958e..e174458 100644 --- a/crates/workspace/src/views/send_patch.rs +++ b/crates/workspace/src/views/send_patch.rs @@ -11,6 +11,8 @@ use gpui_component::spinner::Spinner; use gpui_component::{ActiveTheme, Icon, IconName, Sizable, h_flex, v_flex}; use signed_state::RepoStore; +use crate::views::{repo_tab_avatar, tab_title}; + pub struct SendPatchView { focus_handle: FocusHandle, dock_area: WeakEntity, @@ -205,8 +207,11 @@ impl BasePanel for SendPatchView { } impl Panel for SendPatchView { - fn title(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().child(SharedString::from(format!("{}/send-patch", self.repo_name))) + fn title(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let avatar = repo_tab_avatar(self.store.read(cx), cx); + let label = SharedString::from(format!("{}/send-patch", self.repo_name)); + + tab_title(avatar, label) } }