From 67ff3e2e4ae081a6962031e6f9cf5bd323638d27 Mon Sep 17 00:00:00 2001 From: furyhawk Date: Sat, 13 Jun 2026 00:52:43 +0800 Subject: [PATCH] feat(session): add title management for sessions in backend and frontend --- backend/core/database.py | 28 +++++++++++++++++++++++++ backend/routes/admin.py | 4 ++++ backend/routes/chat.py | 17 ++++++++++++++- backend/routes/users.py | 8 ++++++- frontend/src/Admin.tsx | 14 ++++++++++--- frontend/src/App.tsx | 45 +++++++++++++++++++++++++++++++++------- frontend/src/api.ts | 3 +++ 7 files changed, 107 insertions(+), 12 deletions(-) diff --git a/backend/core/database.py b/backend/core/database.py index a6c3457..019b6b5 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -361,6 +361,7 @@ async def revoke_auth_token( _MESSAGES_KEY = "chat:{session_id}:messages" _SESSIONS_SET = "chat:sessions" _SESSION_USER_KEY = "chat:{session_id}:user_id" +_SESSION_TITLE_KEY = "chat:{session_id}:title" _USER_SESSIONS_KEY = "user:{user_id}:sessions" @@ -428,6 +429,31 @@ async def get_session_user_id( return await valkey.get(key) +async def set_session_title( + session_id: str, + title: str, + valkey: Redis | None = None, +) -> None: + """Store a short human-readable title for a session.""" + if valkey is None: + valkey = await get_valkey() + + key = _SESSION_TITLE_KEY.format(session_id=session_id) + await valkey.set(key, title) + + +async def get_session_title( + session_id: str, + valkey: Redis | None = None, +) -> str | None: + """Return the title for a session, or ``None`` if not set.""" + if valkey is None: + valkey = await get_valkey() + + key = _SESSION_TITLE_KEY.format(session_id=session_id) + return await valkey.get(key) + + async def list_sessions( valkey: Redis | None = None, ) -> list[str]: @@ -466,9 +492,11 @@ async def delete_session( key = _MESSAGES_KEY.format(session_id=session_id) session_user_key = _SESSION_USER_KEY.format(session_id=session_id) + session_title_key = _SESSION_TITLE_KEY.format(session_id=session_id) async with valkey.pipeline(transaction=True) as pipe: pipe.delete(key) pipe.delete(session_user_key) + pipe.delete(session_title_key) pipe.srem(_SESSIONS_SET, session_id) if user_sessions_key is not None: pipe.srem(user_sessions_key, session_id) diff --git a/backend/routes/admin.py b/backend/routes/admin.py index aae876d..8488bbb 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -16,6 +16,7 @@ from backend.core.database import ( delete_user as db_delete_user, get_admin_stats, get_session_messages, + get_session_title, get_session_user_id, get_user, get_user_by_username, @@ -95,6 +96,7 @@ class AdminUserUpdate(BaseModel): class AdminSessionOut(BaseModel): session_id: str + title: str | None = None message_count: int user_id: str | None = None @@ -194,9 +196,11 @@ async def admin_list_sessions( for sid in ids: msgs = await get_session_messages(sid) uid = await get_session_user_id(sid) + title = await get_session_title(sid) result.append( AdminSessionOut( session_id=sid, + title=title, message_count=len(msgs), user_id=uid, ) diff --git a/backend/routes/chat.py b/backend/routes/chat.py index c87c3c8..1379ae9 100644 --- a/backend/routes/chat.py +++ b/backend/routes/chat.py @@ -14,10 +14,12 @@ from pydantic import BaseModel from backend.core.agent import AgentService from backend.core.database import ( get_session_messages, + get_session_title, get_session_user_id, list_sessions, resolve_auth_token, save_message, + set_session_title, ) from backend.core.dependencies import get_agent_service @@ -43,6 +45,7 @@ class MessageOut(BaseModel): class SessionOut(BaseModel): session_id: str + title: str | None = None message_count: int user_id: str | None = None @@ -89,6 +92,14 @@ async def chat_endpoint( user_id=user_id, ) + # Generate a short title from the first user message if not yet set. + existing_title = await get_session_title(session_id) + if existing_title is None: + title = body.message.strip()[:60] + if len(body.message.strip()) > 60: + title += "…" + await set_session_title(session_id, title or "New chat") + # Ask the agent. output = await agent.ask(body.message, session_id=session_id) @@ -133,9 +144,13 @@ async def sessions_list( for sid in ids: msgs = await get_session_messages(sid) uid = await get_session_user_id(sid) + title = await get_session_title(sid) result.append( SessionOut( - session_id=sid, message_count=len(msgs), user_id=uid + session_id=sid, + title=title, + message_count=len(msgs), + user_id=uid, ) ) return result diff --git a/backend/routes/users.py b/backend/routes/users.py index f8dce06..fa1abe5 100644 --- a/backend/routes/users.py +++ b/backend/routes/users.py @@ -14,6 +14,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from backend.core.database import ( create_user, + get_session_title, get_user, get_user_by_username, list_user_sessions, @@ -55,6 +56,7 @@ class UserOut(BaseModel): class UserSessionOut(BaseModel): session_id: str + title: str | None = None # ── Helpers ──────────────────────────────────────────────────────────────── @@ -145,4 +147,8 @@ async def user_sessions_endpoint( ) -> list[UserSessionOut]: """List all session IDs associated with a user.""" session_ids = await list_user_sessions(str(user_id)) - return [UserSessionOut(session_id=sid) for sid in session_ids] + result: list[UserSessionOut] = [] + for sid in session_ids: + title = await get_session_title(sid) + result.append(UserSessionOut(session_id=sid, title=title)) + return result diff --git a/frontend/src/Admin.tsx b/frontend/src/Admin.tsx index 989f8f8..608feb9 100644 --- a/frontend/src/Admin.tsx +++ b/frontend/src/Admin.tsx @@ -421,6 +421,7 @@ function SessionsTab({ + @@ -430,7 +431,7 @@ function SessionsTab({ {sessions.length === 0 ? ( - @@ -440,9 +441,16 @@ function SessionsTab({ key={s.session_id} className="border-b border-gray-800/50 transition hover:bg-gray-800/30" > + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6439736..7df1d84 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -310,6 +310,17 @@ export default function App() { /* ── Session handlers ────────────────────────────────────────────── */ + const handleNewSession = () => { + localStorage.removeItem(STORAGE_SESSION_KEY); + setSessionId(""); + setMessages([ + { + role: "assistant", + content: "Hello! I'm **Agent Alpha**. How can I help you today?", + }, + ]); + }; + const handleViewSessions = async () => { if (!authenticated) return; try { @@ -430,6 +441,15 @@ export default function App() { + {/* New Chat button */} + + {/* Sessions button */} +
+ + +
)} diff --git a/frontend/src/api.ts b/frontend/src/api.ts index d88ca60..5607ca5 100644 --- a/frontend/src/api.ts +++ b/frontend/src/api.ts @@ -36,6 +36,7 @@ export interface MessageData { export interface SessionData { session_id: string; + title: string | null; message_count: number; user_id: string | null; } @@ -74,6 +75,7 @@ export interface UserCreateData { export interface UserSessionData { session_id: string; + title: string | null; } /* ── Auth API ──────────────────────────────────────────────────── */ @@ -241,6 +243,7 @@ export interface AdminUserData { export interface AdminSessionData { session_id: string; + title: string | null; message_count: number; user_id: string | null; }
Title Session ID Messages User ID
+ No sessions yet.
+

+ {s.title ?? ( + Untitled + )} +

+
- - {s.session_id.slice(0, 16)}… + + {s.session_id.slice(0, 12)}… {s.message_count}