import React from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { useSessionStore } from '@/stores/useSessionStore'; import { useMessageStore } from '@/stores/messageStore'; import { RiLoader4Line, RiSearchLine, RiTimeLine, RiGitBranchLine, RiArrowGoBackLine } from '@remixicon/react'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import type { Part } from '@opencode-ai/sdk/v2'; interface TimelineDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onScrollToMessage?: (messageId: string) => void; } // Helper: format relative time (e.g., "2 hours ago") function formatRelativeTime(timestamp: number): string { const now = Date.now(); const diffMs = now - timestamp; const diffSecs = Math.floor(diffMs / 1000); const diffMins = Math.floor(diffSecs / 60); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffSecs < 60) return 'just now'; if (diffMins < 60) return `${diffMins}m ago`; if (diffHours < 24) return `${diffHours}h ago`; if (diffDays < 7) return `${diffDays}d ago`; return new Date(timestamp).toLocaleDateString(); } export const TimelineDialog: React.FC = ({ open, onOpenChange, onScrollToMessage }) => { const currentSessionId = useSessionStore((state) => state.currentSessionId); const messages = useMessageStore((state) => currentSessionId ? state.messages.get(currentSessionId) || [] : [] ); const revertToMessage = useSessionStore((state) => state.revertToMessage); const forkFromMessage = useSessionStore((state) => state.forkFromMessage); const loadSessions = useSessionStore((state) => state.loadSessions); const [forkingMessageId, setForkingMessageId] = React.useState(null); const [searchQuery, setSearchQuery] = React.useState(''); // Filter user messages (reversed for newest first) const userMessages = React.useMemo(() => { const filtered = messages.filter(m => m.info.role === 'user'); return filtered.reverse(); }, [messages]); // Filter by search query const filteredMessages = React.useMemo(() => { if (!searchQuery.trim()) return userMessages; const query = searchQuery.toLowerCase(); return userMessages.filter((message) => { const preview = getMessagePreview(message.parts).toLowerCase(); return preview.includes(query); }); }, [userMessages, searchQuery]); // Handle fork with loading state and session refresh const handleFork = async (messageId: string) => { if (!currentSessionId) return; setForkingMessageId(messageId); try { await forkFromMessage(currentSessionId, messageId); await loadSessions(); onOpenChange(false); } finally { setForkingMessageId(null); } }; if (!currentSessionId) return null; return ( Conversation Timeline Navigate to any point in the conversation or fork a new session
setSearchQuery(e.target.value)} className="pl-9 w-full" />
{filteredMessages.length === 0 ? (
{searchQuery ? 'No messages found' : 'No messages in this session yet'}
) : ( filteredMessages.map((message) => { const preview = getMessagePreview(message.parts); const timestamp = message.info.time.created; const relativeTime = formatRelativeTime(timestamp); const messageNumber = userMessages.length - userMessages.indexOf(message); return (
{ onScrollToMessage?.(message.info.id); onOpenChange(false); }} > {messageNumber}.

{preview || '[No text content]'} {preview && preview.length >= 80 && '…'}

{relativeTime}
Revert from here Fork from here
); }) )}

Actions

Click on a message to scroll to it in the conversation
Undo to this point (message text will populate input)
Create a new session starting from here
); }; function getMessagePreview(parts: Part[]): string { const textPart = parts.find(p => p.type === 'text'); if (!textPart || typeof textPart.text !== 'string') return ''; return textPart.text.replace(/\n/g, ' ').slice(0, 80); }