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
This commit is contained in:
@@ -4,6 +4,7 @@ import type { Session } from '../types';
|
|||||||
import { useT } from '../hooks/useLocale';
|
import { useT } from '../hooks/useLocale';
|
||||||
import { SessionIcon } from './SessionIcon';
|
import { SessionIcon } from './SessionIcon';
|
||||||
import { sessionDisplayName } from '../lib/sessionName';
|
import { sessionDisplayName } from '../lib/sessionName';
|
||||||
|
import { relativeTime } from '../lib/relativeTime';
|
||||||
|
|
||||||
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
const PINNED_KEY = 'pinchchat-pinned-sessions';
|
||||||
const WIDTH_KEY = 'pinchchat-sidebar-width';
|
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)
|
// Sort pinned sessions to top (preserving relative order within each group)
|
||||||
const pinnedList = list.filter(s => pinned.has(s.key));
|
const pinnedList = list.filter(s => pinned.has(s.key));
|
||||||
const unpinnedList = 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];
|
return [...pinnedList, ...unpinnedList];
|
||||||
}, [sessions, filter, pinned]);
|
}, [sessions, filter, pinned]);
|
||||||
|
|
||||||
@@ -251,6 +256,10 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="flex-1 truncate">{sessionDisplayName(s)}</span>
|
<span className="flex-1 truncate">{sessionDisplayName(s)}</span>
|
||||||
|
{(() => {
|
||||||
|
const rel = relativeTime(s.updatedAt);
|
||||||
|
return rel ? <span className="text-[10px] text-zinc-500 tabular-nums shrink-0">{rel}</span> : null;
|
||||||
|
})()}
|
||||||
<button
|
<button
|
||||||
onClick={(e) => togglePin(s.key, e)}
|
onClick={(e) => togglePin(s.key, e)}
|
||||||
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
className={`shrink-0 p-0.5 rounded-lg transition-all ${
|
||||||
@@ -277,6 +286,9 @@ export function Sidebar({ sessions, activeSession, onSwitch, onDelete, open, onC
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{s.lastMessagePreview && (
|
||||||
|
<p className="text-[11px] text-zinc-500 truncate mt-0.5 leading-tight">{s.lastMessagePreview.replace(/\s+/g, ' ').slice(0, 80)}</p>
|
||||||
|
)}
|
||||||
{(() => {
|
{(() => {
|
||||||
if (!s.contextTokens) return null;
|
if (!s.contextTokens) return null;
|
||||||
const pct = Math.min(100, ((s.totalTokens || 0) / s.contextTokens) * 100);
|
const pct = Math.min(100, ((s.totalTokens || 0) / s.contextTokens) * 100);
|
||||||
|
|||||||
@@ -97,6 +97,8 @@ export function useGateway() {
|
|||||||
kind: s.kind as string | undefined,
|
kind: s.kind as string | undefined,
|
||||||
model: s.model as string | undefined,
|
model: s.model as string | undefined,
|
||||||
agentId: s.agentId as string | undefined,
|
agentId: s.agentId as string | undefined,
|
||||||
|
updatedAt: s.updatedAt as number | undefined,
|
||||||
|
lastMessagePreview: s.lastMessagePreview as string | undefined,
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
19
src/lib/relativeTime.ts
Normal file
19
src/lib/relativeTime.ts
Normal file
@@ -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`;
|
||||||
|
}
|
||||||
@@ -30,6 +30,8 @@ export interface Session {
|
|||||||
kind?: string;
|
kind?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
agentId?: string;
|
agentId?: string;
|
||||||
|
updatedAt?: number;
|
||||||
|
lastMessagePreview?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
export type ConnectionStatus = 'disconnected' | 'connecting' | 'connected';
|
||||||
|
|||||||
Reference in New Issue
Block a user