105 lines
4.3 KiB
TypeScript
105 lines
4.3 KiB
TypeScript
import { useMemo, useState } from 'react';
|
|
import { Link } from 'react-router';
|
|
import { Plus, MessageSquare, Trash2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card } from '@/components/Card';
|
|
import { useSessions, useOpenCodeSessions } from 'widgets/ChatHistory';
|
|
|
|
type Filter = 'all' | 'claude' | 'opencode';
|
|
|
|
export const SessionList = () => {
|
|
const [filter, setFilter] = useState<Filter>('all');
|
|
const claude = useSessions();
|
|
const opencode = useOpenCodeSessions();
|
|
|
|
const merged = useMemo(
|
|
() => [...claude.sessions, ...opencode.sessions].sort((a, b) => b.createdAt - a.createdAt),
|
|
[claude.sessions, opencode.sessions],
|
|
);
|
|
|
|
const filtered = filter === 'all' ? merged : merged.filter((s) => s.provider === filter);
|
|
|
|
const handleDelete = (id: string, provider: 'claude' | 'opencode') => {
|
|
if (provider === 'claude') claude.deleteSession(id);
|
|
else opencode.deleteSession(id);
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col h-full items-center p-4 md:p-6">
|
|
<Card className="w-full max-w-2xl flex flex-col gap-4 h-full p-4 md:p-6 overflow-hidden">
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-bold text-duck-dark/80">Sessions</h2>
|
|
<Button asChild className="bg-duck-teal hover:bg-duck-teal/90 cursor-pointer gap-2">
|
|
<Link to="/chat/new">
|
|
<Plus className="h-4 w-4" />
|
|
New Chat
|
|
</Link>
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Radio filter */}
|
|
<div className="flex items-center gap-1 rounded-lg bg-white/60 p-1">
|
|
{(['all', 'claude', 'opencode'] as const).map((value) => (
|
|
<button
|
|
key={value}
|
|
onClick={() => setFilter(value)}
|
|
className={`flex-1 rounded-md px-3 py-1.5 text-sm font-medium transition-colors cursor-pointer ${
|
|
filter === value ? 'bg-white text-duck-dark shadow-sm' : 'text-duck-dark/70 hover:text-duck-dark/90'
|
|
}`}
|
|
>
|
|
{value === 'all' ? 'All' : value === 'claude' ? 'Claude' : 'OpenCode'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
{/* Session list */}
|
|
<div className="flex-1 min-h-0 overflow-y-auto space-y-2">
|
|
{filtered.length === 0 && (
|
|
<div className="text-center py-16 text-duck-dark/30 text-sm">No sessions yet. Start a new chat!</div>
|
|
)}
|
|
|
|
{filtered.map((session) => (
|
|
<div
|
|
key={`${session.provider}-${session.id}`}
|
|
className="group flex items-center gap-3 rounded-lg border border-duck-dark/10 bg-white/80 hover:bg-white/90 transition-colors"
|
|
>
|
|
<Link
|
|
to={session.provider === 'claude' ? `/chat/${session.id}` : `/chat/opencode/${session.id}`}
|
|
className="flex-1 flex items-center gap-3 px-4 py-3 min-w-0"
|
|
>
|
|
<MessageSquare className="h-4 w-4 shrink-0 text-duck-teal/60" />
|
|
<div className="min-w-0 flex-1">
|
|
<div className="text-sm font-medium text-duck-dark/80 truncate">{session.title}</div>
|
|
<div className="text-xs text-duck-dark/40">
|
|
{new Date(session.createdAt).toLocaleDateString(undefined, {
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})}
|
|
<span
|
|
className={`ml-2 text-xs font-medium ${
|
|
session.provider === 'claude' ? 'text-duck-teal' : 'text-duck-orange'
|
|
}`}
|
|
>
|
|
{session.provider === 'claude' ? 'Claude' : 'OpenCode'}
|
|
</span>
|
|
<span className="ml-2 font-mono text-duck-dark/25">{session.id.slice(0, 8)}</span>
|
|
</div>
|
|
</div>
|
|
</Link>
|
|
<button
|
|
onClick={() => handleDelete(session.id, session.provider)}
|
|
className="shrink-0 p-2 mr-2 text-duck-dark/20 hover:text-red-500 md:opacity-0 md:group-hover:opacity-100 transition-opacity cursor-pointer"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
};
|