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