From 53d619c357272c6c5b8f601df4655206f2f2faa0 Mon Sep 17 00:00:00 2001 From: Nicolas Varrot Date: Thu, 12 Feb 2026 17:59:16 +0000 Subject: [PATCH] feat: relative timestamps, message preview, and recency sort in sidebar - Show relative time (2m, 3h, 1d) next to each session name - Display last message preview below session name (truncated to 80 chars) - Sort sessions by most recently updated (within pinned/unpinned groups) - Map updatedAt and lastMessagePreview from gateway sessions.list response --- src/components/Sidebar.tsx | 12 ++++++++++++ src/hooks/useGateway.ts | 2 ++ src/lib/relativeTime.ts | 19 +++++++++++++++++++ src/types/index.ts | 2 ++ 4 files changed, 35 insertions(+) create mode 100644 src/lib/relativeTime.ts diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index a90a48f..ae254ba 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -4,6 +4,7 @@ import type { Session } from '../types'; import { useT } from '../hooks/useLocale'; import { SessionIcon } from './SessionIcon'; import { sessionDisplayName } from '../lib/sessionName'; +import { relativeTime } from '../lib/relativeTime'; const PINNED_KEY = 'pinchchat-pinned-sessions'; const WIDTH_KEY = 'pinchchat-sidebar-width'; @@ -131,6 +132,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC // Sort pinned sessions to top (preserving relative order within each group) const pinnedList = list.filter(s => pinned.has(s.key)); const unpinnedList = list.filter(s => !pinned.has(s.key)); + // Sort each group by most recently updated + const byRecent = (a: Session, b: Session) => (b.updatedAt || 0) - (a.updatedAt || 0); + pinnedList.sort(byRecent); + unpinnedList.sort(byRecent); return [...pinnedList, ...unpinnedList]; }, [sessions, filter, pinned]); @@ -251,6 +256,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
{sessionDisplayName(s)} + {(() => { + const rel = relativeTime(s.updatedAt); + return rel ? {rel} : null; + })()}
+ {s.lastMessagePreview && ( +

{s.lastMessagePreview.replace(/\s+/g, ' ').slice(0, 80)}

+ )} {(() => { if (!s.contextTokens) return null; const pct = Math.min(100, ((s.totalTokens || 0) / s.contextTokens) * 100); diff --git a/src/hooks/useGateway.ts b/src/hooks/useGateway.ts index 2248bec..0ff764a 100644 --- a/src/hooks/useGateway.ts +++ b/src/hooks/useGateway.ts @@ -97,6 +97,8 @@ export function useGateway() { kind: s.kind as string | undefined, model: s.model as string | undefined, agentId: s.agentId as string | undefined, + updatedAt: s.updatedAt as number | undefined, + lastMessagePreview: s.lastMessagePreview as string | undefined, }))); } } catch { diff --git a/src/lib/relativeTime.ts b/src/lib/relativeTime.ts new file mode 100644 index 0000000..c4c68c1 --- /dev/null +++ b/src/lib/relativeTime.ts @@ -0,0 +1,19 @@ +/** + * Format a timestamp as a short relative time string (e.g. "2m", "3h", "1d"). + * Returns null for invalid/missing timestamps. + */ +export function relativeTime(ts: number | undefined): string | null { + if (!ts) return null; + const now = Date.now(); + const diff = Math.max(0, now - ts); + const seconds = Math.floor(diff / 1000); + if (seconds < 60) return '<1m'; + const minutes = Math.floor(seconds / 60); + if (minutes < 60) return `${minutes}m`; + const hours = Math.floor(minutes / 60); + if (hours < 24) return `${hours}h`; + const days = Math.floor(hours / 24); + if (days < 30) return `${days}d`; + const months = Math.floor(days / 30); + return `${months}mo`; +} diff --git a/src/types/index.ts b/src/types/index.ts index 1d5779e..77572e6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -30,6 +30,8 @@ export interface Session { kind?: string; model?: string; agentId?: string; + updatedAt?: number; + lastMessagePreview?: string; } export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';