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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user