feat(client): match login color to logo + show user pill in title bar

#1 Login wordmark color
   The big "HEICODE" wordmark on the login page was rendered in the
   gold brand color (#C5A572), which clashed visually with the
   purple→blue glassy H app icon shown in the window/taskbar/dock.
   Now the wordmark uses the same linear-gradient(135deg, #B888E5,
   #6B7CE0) as the logo so the login surface matches the icon.
   Backdrop radial glow tinted to match. Other surfaces keep gold
   primary so buttons/accents stay consistent app-wide.

#3 User pill in TitleBar
   Logged-in users had no in-app affordance to confirm "I'm signed
   in as X" or to know which account they're using. Add a pill on
   the right of TitleBar showing initial avatar (logo gradient) +
   display name + admin/root role badge.

   Wiring:
   - cc-haha/src/server/types/provider.ts: McpAuth gains optional
     email / displayName / role fields. Backwards-compatible (all
     optional, existing saved providers stay valid).
   - cc-haha/src/server/api/heicode-auth.ts: handleLoginWithCredentials
     now reads user.{name|display_name, email, role} from the
     /api/auth/login response and persists them on the provider's
     mcpAuth record. /api/heicode-auth/status returns a `user` field
     synthesized from the active provider.
   - cc-haha/desktop/src/api/heicodeAuth.ts: HeicodeAuthStatus type
     gains the `user` shape.
   - cc-haha/desktop/src/components/layout/TitleBar.tsx: new UserPill
     component reads from useHeicodeAuthStore; hidden until status
     loads so the bar doesn't flicker on boot.

Type-check: bunx tsc -b --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 16:52:36 +08:00
co-authored by Claude Opus 4.7
parent 2db20738d7
commit 6ca8bf42d6
5 changed files with 97 additions and 5 deletions
+8
View File
@@ -29,6 +29,14 @@ export type HeicodeAuthStatus = {
baseUrl: string
models: { main: string; haiku: string; sonnet: string; opus: string }
} | null
/** Logged-in user profile (Manager-side identity). Null until login. */
user: {
userId?: string
email?: string
displayName?: string
role?: string
channelId?: string
} | null
}
export type HeicodeLoginInput = {
@@ -1,5 +1,6 @@
import { useUIStore } from '../../stores/uiStore'
import { useTranslation } from '../../i18n'
import { useHeicodeAuthStore } from '../../stores/heicodeAuthStore'
import { WindowControls } from './WindowControls'
const isMacHost = typeof navigator !== 'undefined'
@@ -61,8 +62,9 @@ export function TitleBar() {
</TabButton>
</div>
{/* Right: Settings + Window controls */}
{/* Right: User pill + Settings + Window controls */}
<div className="flex items-center h-full">
<UserPill />
<button className="p-1.5 mr-2 rounded-[var(--radius-md)] text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] transition-colors">
<span className="material-symbols-outlined text-[18px]">settings</span>
</button>
@@ -72,6 +74,44 @@ export function TitleBar() {
)
}
// Tiny avatar + display name + role badge. Sourced from
// useHeicodeAuthStore.status.user, populated by /api/heicode-auth/status.
// Hidden until auth is loaded so the bar doesn't flicker on app boot.
function UserPill() {
const user = useHeicodeAuthStore((s) => s.status?.user ?? null)
if (!user) return null
const name = user.displayName?.trim() || user.email?.trim() || 'You'
const initial = (name.slice(0, 1) || '?').toUpperCase()
// Show role badge only when meaningful (admin/root); plain "user" is noise.
const roleBadge =
user.role && user.role !== 'user' && user.role !== '1'
? user.role
: null
return (
<div
className="mr-2 flex items-center gap-2 rounded-full border border-[var(--color-border)] bg-[var(--color-surface-container)] py-0.5 pl-1 pr-3 text-xs text-[var(--color-text-secondary)] hover:border-[var(--color-border-hi,rgba(255,255,255,0.14))]"
title={user.email || user.userId || ''}
>
<span
className="flex h-5 w-5 items-center justify-center rounded-full text-[10px] font-bold text-white"
style={{
backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)',
}}
>
{initial}
</span>
<span className="max-w-[120px] truncate font-medium text-[var(--color-text-primary)]">
{name}
</span>
{roleBadge ? (
<span className="rounded-[var(--radius-sm)] border border-[var(--color-border)] px-1 text-[9px] uppercase tracking-wider text-[var(--color-text-tertiary)]">
{roleBadge}
</span>
) : null}
</div>
)
}
function TabButton({
active,
onClick,
@@ -43,7 +43,10 @@ export function HeicodeLoginPage() {
data-tauri-drag-region
>
<div className="flex items-center gap-2 px-4" data-tauri-drag-region>
<span className="text-[10px] font-bold tracking-[0.22em] uppercase text-[var(--color-brand)]">
<span
className="text-[10px] font-bold tracking-[0.22em] uppercase bg-clip-text text-transparent"
style={{ backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)' }}
>
Heicode
</span>
</div>
@@ -56,14 +59,20 @@ export function HeicodeLoginPage() {
className="pointer-events-none absolute inset-0"
style={{
backgroundImage:
'radial-gradient(circle at 50% 30%, rgba(197,165,114,0.06), transparent 55%)',
'radial-gradient(circle at 50% 30%, rgba(123,107,227,0.08), transparent 55%)',
}}
/>
{/* Content */}
<div className="relative flex flex-1 flex-col items-center justify-center overflow-auto px-6 py-10">
<div className="mb-10 text-center">
<h1 className="text-5xl font-bold tracking-[0.18em] uppercase text-[var(--color-brand)]">
{/* Wordmark uses the same purple→blue gradient as the H glass logo
so the login surface visually matches the app icon (window
tray icon, taskbar, dock). */}
<h1
className="text-5xl font-bold tracking-[0.18em] uppercase bg-clip-text text-transparent"
style={{ backgroundImage: 'linear-gradient(135deg, #B888E5 0%, #6B7CE0 100%)' }}
>
Heicode
</h1>
<p
+30 -1
View File
@@ -120,6 +120,19 @@ export async function handleHeicodeAuthApi(
models: active.models,
}
: null,
// User profile for the in-app TitleBar pill. Sourced from the
// mcp-server /api/auth/login response captured at login time and
// persisted on the active provider's mcpAuth record.
user:
active?.mcpAuth?.email || active?.mcpAuth?.displayName
? {
userId: active.mcpAuth.userId,
email: active.mcpAuth.email,
displayName: active.mcpAuth.displayName,
role: active.mcpAuth.role,
channelId: active.mcpAuth.channelId,
}
: null,
})
}
@@ -255,7 +268,16 @@ async function handleLoginWithCredentials(req: Request): Promise<Response> {
data?: {
token?: string
refreshToken?: string
user?: { id?: string; email?: string; channelId?: string }
user?: {
id?: string
email?: string
channelId?: string
// mcp-server may return either `name` or `display_name` depending on
// version; try both. role lets the UI badge admin/root.
name?: string
display_name?: string
role?: string
}
}
} = {}
try { mgrJson = (await mgrRes.json()) as typeof mgrJson } catch { /* */ }
@@ -270,6 +292,10 @@ async function handleLoginWithCredentials(req: Request): Promise<Response> {
}
const mcpUserId = mgrJson.data?.user?.id
const mcpChannelId = mgrJson.data?.user?.channelId
const mcpEmail = mgrJson.data?.user?.email
const mcpDisplayName =
mgrJson.data?.user?.display_name || mgrJson.data?.user?.name
const mcpRole = mgrJson.data?.user?.role
// Decode JWT exp claims (no signature verification — we already trust the
// platform we just authenticated against). Used downstream to know when to
@@ -289,6 +315,9 @@ async function handleLoginWithCredentials(req: Request): Promise<Response> {
managerLoginUrl: managerBase,
...(mcpUserId !== undefined && { userId: mcpUserId }),
...(mcpChannelId !== undefined && { channelId: mcpChannelId }),
...(mcpEmail && { email: mcpEmail }),
...(mcpDisplayName && { displayName: mcpDisplayName }),
...(mcpRole && { role: mcpRole }),
}
// ── Step 2: POST <baseUrl>/api/user/session/from-agnet ─────────
+6
View File
@@ -39,6 +39,12 @@ export const McpAuthSchema = z.object({
userId: z.string().optional(),
/** Manager-side channelId for this user. */
channelId: z.string().optional(),
/** Email captured from /api/auth/login response (used for header user pill). */
email: z.string().optional(),
/** Human-readable display name from /api/auth/login response (header user pill). */
displayName: z.string().optional(),
/** Role string returned by Manager (e.g. 'user'/'admin'/'root'). */
role: z.string().optional(),
})
export type McpAuth = z.infer<typeof McpAuthSchema>