From 422018299ce7e15071f9618a61e1496aa1caaf1e Mon Sep 17 00:00:00 2001 From: weishu Date: Tue, 23 Dec 2025 14:16:44 +0800 Subject: [PATCH] refactor(web): redesign SessionList component with enhanced session metadata Replace Card-based layout with semantic button elements for better accessibility and keyboard navigation. Add session metadata fields (flavor, activeAt, modelMode) and implement helper functions to format session information including agent flavor, model mode, and relative last-seen timestamps. Update hover styles and add focus-visible indicators. --- server/src/web/routes/sessions.ts | 10 ++- web/src/components/SessionList.tsx | 119 ++++++++++++++++++++--------- web/src/index.css | 7 ++ web/src/types/api.ts | 3 + 4 files changed, 100 insertions(+), 39 deletions(-) diff --git a/server/src/web/routes/sessions.ts b/server/src/web/routes/sessions.ts index b03bda01..0502194b 100644 --- a/server/src/web/routes/sessions.ts +++ b/server/src/web/routes/sessions.ts @@ -8,15 +8,18 @@ type SessionSummaryMetadata = { name?: string path: string summary?: { text: string } + flavor?: string | null } type SessionSummary = { id: string active: boolean + activeAt: number updatedAt: number metadata: SessionSummaryMetadata | null todoProgress: { completed: number; total: number } | null pendingRequestsCount: number + modelMode?: 'default' | 'sonnet' | 'opus' | null } function toSessionSummary(session: Session): SessionSummary { @@ -25,7 +28,8 @@ function toSessionSummary(session: Session): SessionSummary { const metadata: SessionSummaryMetadata | null = session.metadata ? { name: session.metadata.name, path: session.metadata.path, - summary: session.metadata.summary ? { text: session.metadata.summary.text } : undefined + summary: session.metadata.summary ? { text: session.metadata.summary.text } : undefined, + flavor: session.metadata.flavor ?? null } : null const todoProgress = session.todos?.length ? { @@ -36,10 +40,12 @@ function toSessionSummary(session: Session): SessionSummary { return { id: session.id, active: session.active, + activeAt: session.activeAt, updatedAt: session.updatedAt, metadata, todoProgress, - pendingRequestsCount + pendingRequestsCount, + modelMode: session.modelMode ?? null } } diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 1ee38413..561b7476 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -1,5 +1,3 @@ -import { Badge } from '@/components/ui/badge' -import { Card, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' import type { SessionSummary } from '@/types/api' function PlusIcon(props: { className?: string }) { @@ -63,6 +61,37 @@ function getTodoProgress(session: SessionSummary): { completed: number; total: n return session.todoProgress } +function getAgentLabel(session: SessionSummary): string { + const flavor = session.metadata?.flavor?.trim() + if (flavor) return flavor + return 'unknown' +} + +function getModelLabel(session: SessionSummary): string { + return session.modelMode ?? 'default' +} + +function formatRelativeTime(value: number): string | null { + const ms = value < 1_000_000_000_000 ? value * 1000 : value + if (!Number.isFinite(ms)) return null + const delta = Date.now() - ms + if (delta < 60_000) return 'just now' + const minutes = Math.floor(delta / 60_000) + if (minutes < 60) return `${minutes}m ago` + const hours = Math.floor(minutes / 60) + if (hours < 24) return `${hours}h ago` + const days = Math.floor(hours / 24) + if (days < 7) return `${days}d ago` + return new Date(ms).toLocaleDateString() +} + +function getLastSeenLabel(session: SessionSummary): string | null { + if (session.active) return null + const lastSeen = formatRelativeTime(session.activeAt ?? session.updatedAt) + if (!lastSeen) return null + return `last seen ${lastSeen}` +} + export function SessionList(props: { sessions: SessionSummary[] onSelect: (sessionId: string) => void @@ -71,54 +100,70 @@ export function SessionList(props: { isLoading: boolean }) { return ( -
-
-
+
+
+
{props.sessions.length} sessions
-
+
{props.sessions.map((s) => ( - props.onSelect(s.id)}> - -
- {getSessionTitle(s)} -
- {(() => { - const progress = getTodoProgress(s) - if (!progress) return null - return ( - - - {progress.completed}/{progress.total} - - ) - })()} - {s.active ? ( - s.pendingRequestsCount > 0 ? ( - {s.pendingRequestsCount} pending - ) : ( - active - ) - ) : ( - inactive - )} + ))}
diff --git a/web/src/index.css b/web/src/index.css index 8cc0c7b8..53ba1c68 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -104,6 +104,13 @@ body { height: 100%; } +@media (hover: hover) and (pointer: fine) { + .session-list-item:hover, + .session-list-new-button:hover { + background-color: var(--app-subtle-bg); + } +} + /* Markdown styles */ .markdown-content a { color: var(--app-link); text-decoration: underline; } .markdown-content code { background: var(--app-inline-code-bg); padding: 0.1em 0.3em; border-radius: 4px; font-size: 0.9em; } diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 31c9cafb..79ef3324 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -62,15 +62,18 @@ export type SessionSummaryMetadata = { name?: string path: string summary?: { text: string } + flavor?: string | null } export type SessionSummary = { id: string active: boolean + activeAt: number updatedAt: number metadata: SessionSummaryMetadata | null todoProgress: { completed: number; total: number } | null pendingRequestsCount: number + modelMode?: ModelMode } export type MessageStatus = 'sending' | 'sent' | 'failed'