feat(client): logged-in user card in sidebar (always visible)

The earlier UserPill landed in the TitleBar tab strip, but the main
desktop layout (HeicodeTasksHome shell) renders its own chrome without
that strip — so the user info ended up invisible despite mcpAuth
having the right data after a2deeb6. Pull the same identity (avatar
gradient + name + email + role badge) straight into the bottom of
Sidebar.tsx, right above the logout/settings rows, so it's visible
on every screen regardless of which top bar is mounted.

Two layouts:
- expanded: 36px gradient avatar + name + email + admin/root badge
- collapsed: 36px avatar circle only, title tooltip carries the email

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-11 22:10:17 +08:00
co-authored by Claude Opus 4.7
parent da218ebbe5
commit 54d6a67ecf
@@ -36,6 +36,12 @@ export function Sidebar() {
const closeTab = useTabStore((s) => s.closeTab)
const disconnectSession = useChatStore((s) => s.disconnectSession)
const logout = useHeicodeAuthStore((s) => s.logout)
// Logged-in user identity, surfaced as a card right above logout/settings so
// the user can always SEE who they're signed in as without clicking anywhere.
// Populated by /api/heicode-auth/status from the active provider's mcpAuth
// record (credentials login → set in handleLoginWithCredentials; OAuth login
// → set from query params by handleOAuthCallback after backend a2deeb6).
const authUser = useHeicodeAuthStore((s) => s.status?.user ?? null)
const [searchQuery, setSearchQuery] = useState('')
const [contextMenu, setContextMenu] = useState<{ id: string; x: number; y: number } | null>(null)
const [pendingDeleteSessionId, setPendingDeleteSessionId] = useState<string | null>(null)
@@ -372,7 +378,8 @@ export function Sidebar() {
<div className="flex-1" aria-hidden="true" />
)}
<div className={`border-t border-[var(--color-border)] p-3 ${sidebarOpen ? '' : 'flex justify-center'}`}>
<div className={`border-t border-[var(--color-border)] p-3 ${sidebarOpen ? '' : 'flex flex-col items-center'}`}>
{authUser ? <SidebarUserCard user={authUser} collapsed={!sidebarOpen} /> : null}
<div className={`mb-1 ${sidebarOpen ? '' : 'mb-2'}`}>
<NavItem
active={false}
@@ -508,6 +515,78 @@ function NavItem({
)
}
// Logged-in user card in the bottom sidebar — gradient-filled avatar +
// name + email + role badge. Always visible so the user can see who they
// are signed in as without clicking anywhere. Two layouts: expanded (full
// pill) and collapsed (small avatar circle with title tooltip).
function SidebarUserCard({
user,
collapsed,
}: {
user: {
email?: string
displayName?: string
role?: string
userId?: string
}
collapsed: boolean
}) {
const name = user.displayName?.trim() || user.email?.trim() || 'You'
const initial = (name.slice(0, 1) || '?').toUpperCase()
const role = user.role?.trim().toLowerCase()
const roleBadge = role && role !== 'user' && role !== '1' ? role : null
if (collapsed) {
return (
<div
className="mb-2 flex h-9 w-9 items-center justify-center rounded-full text-[12px] font-bold text-white"
style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
title={user.email || name}
>
{initial}
</div>
)
}
return (
<div
className="mb-2 flex items-center gap-2.5 rounded-[12px] border px-2.5 py-2"
style={{
borderColor: 'rgba(123,107,227,0.22)',
backgroundColor: 'rgba(123,107,227,0.06)',
}}
>
<span
className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full text-xs font-bold text-white"
style={{ backgroundImage: 'var(--gradient-brand-wordmark)' }}
>
{initial}
</span>
<div className="min-w-0 flex-1 leading-tight">
<div className="truncate text-[12px] font-semibold text-[var(--color-text-primary)]">
{name}
</div>
{user.email && (
<div className="truncate text-[10px] text-[var(--color-text-tertiary)]">
{user.email}
</div>
)}
</div>
{roleBadge && (
<span
className="flex-shrink-0 rounded-full px-1.5 py-0.5 text-[9px] font-semibold uppercase tracking-wider"
style={{
backgroundColor: 'rgba(123,107,227,0.14)',
color: 'var(--color-primary)',
}}
>
{roleBadge}
</span>
)}
</div>
)
}
function formatRelativeTime(dateStr: string): string {
const diff = Date.now() - new Date(dateStr).getTime()
const min = Math.floor(diff / 60000)