feat: last-used persona cookie, emoji dropdown, theme support, auth status move

- cx_last_persona cookie set on serve_ui; root/login/help/settings
  redirects use preferred persona from cookie instead of alphabetically first
- /api/personas returns [{name, emoji}] objects; persona switcher dropdown
  renders emoji + name with flex layout and .pd-emoji span
- Help, Settings, Model Registry pages apply localStorage theme on load
  (no flash); CSS variables for dark/light replacing all hardcoded hex values
- Claude CLI auth status moved from prominent chat banner to Anthropic
  provider block in Model Registry — live dot indicator (ok/warn/err)
- Auth banner removed from main chat UI (index.html, app.js, style.css)
- Add Model collapsed into Models section as <details> to shorten page
- Light-mode overrides for provider icons, model badges, ctx-badge, tags
  (Anthropic/Google/local colors now readable in both themes)
- Help page gains table, pre/code, hr styles for HELP.md rendered content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-04-28 22:52:34 -04:00
parent 1222f806ce
commit 66cb197de0
9 changed files with 305 additions and 245 deletions

View File

@@ -56,12 +56,26 @@ def _set_cookie(response: Response, username: str) -> None:
)
_LAST_PERSONA_COOKIE = "cx_last_persona"
def _first_persona(username: str) -> str | None:
"""Return the first available persona for a user, or None."""
names = list_user_personas(username)
return names[0] if names else None
def _preferred_persona(request: Request, username: str) -> str | None:
"""Return the last-visited persona from cookie if valid, else the first available."""
names = list_user_personas(username)
if not names:
return None
cookie_val = request.cookies.get(_LAST_PERSONA_COOKIE, "")
if cookie_val in names:
return cookie_val
return names[0]
# ---------------------------------------------------------------------------
# Favicon — default sparkle; persona pages override via JS
# ---------------------------------------------------------------------------
@@ -85,7 +99,7 @@ async def root(request: Request):
user = _get_session_user(request)
if not user:
return RedirectResponse("/login", status_code=302)
persona = _first_persona(user)
persona = _preferred_persona(request, user)
if not persona:
return HTMLResponse("<h1>No personas configured for your account.</h1>", status_code=500)
return RedirectResponse(f"/{user}/{persona}", status_code=302)
@@ -100,7 +114,7 @@ async def login_page(request: Request):
user = _get_session_user(request)
if user:
# Already logged in — redirect home
persona = _first_persona(user)
persona = _preferred_persona(request, user)
if persona:
return RedirectResponse(f"/{user}/{persona}", status_code=302)
return HTMLResponse((_STATIC / "login.html").read_text())
@@ -254,7 +268,16 @@ async def api_personas(request: Request) -> dict:
if not user:
from fastapi import HTTPException
raise HTTPException(status_code=401, detail="Not authenticated")
return {"user": user, "personas": list_user_personas(user)}
personas_with_emoji = []
for p in list_user_personas(user):
emoji = ""
identity_path = persona_path(user, p) / "IDENTITY.md"
if identity_path.exists():
m = re.search(r"\|\s*Emoji\s*\|\s*(.+?)\s*\|", identity_path.read_text())
if m:
emoji = m.group(1).strip()
personas_with_emoji.append({"name": p, "emoji": emoji})
return {"user": user, "personas": personas_with_emoji}
@router.get("/{username}/{persona}", include_in_schema=False)
@@ -288,4 +311,6 @@ async def serve_ui(username: str, persona: str, request: Request):
f'{{user: "{username}", persona: "{persona}", emoji: "{emoji}"}};</script>'
)
html = html.replace("</head>", f"{config_tag}\n</head>", 1)
return HTMLResponse(html)
resp = HTMLResponse(html)
resp.set_cookie(_LAST_PERSONA_COOKIE, persona, max_age=365 * 86400, httponly=False, samesite="lax")
return resp