import type { Message } from '@opencode-ai/sdk/v2'; export class MessageFreshnessDetector { private static instance: MessageFreshnessDetector; private sessionStartTimes: Map = new Map(); private seenMessageIds: Set = new Set(); private messageCreationTimes: Map = new Map(); private constructor() {} static getInstance(): MessageFreshnessDetector { if (!MessageFreshnessDetector.instance) { MessageFreshnessDetector.instance = new MessageFreshnessDetector(); } return MessageFreshnessDetector.instance; } recordSessionStart(sessionId: string): void { this.sessionStartTimes.set(sessionId, Date.now()); } getSessionStartTime(sessionId: string): number | undefined { return this.sessionStartTimes.get(sessionId); } shouldAnimateMessage(message: Message, sessionId: string): boolean { if (message.role !== 'assistant') { return false; } if (this.seenMessageIds.has(message.id)) { return false; } const sessionStartTime = this.sessionStartTimes.get(sessionId); if (!sessionStartTime) { this.seenMessageIds.add(message.id); this.messageCreationTimes.set(message.id, message.time.created); return false; } const isFresh = message.time.created > (sessionStartTime - 5000); // Record fresh messages too so they animate at most once per detector // lifetime. The detector is a module singleton that outlives ChatViewport // remounts; without this, switching away and back re-evaluates the same // message against the stale session start time (recordSessionStart runs // in an effect after the first render) and replays the entry animation. this.seenMessageIds.add(message.id); this.messageCreationTimes.set(message.id, message.time.created); return isFresh; } clearSession(sessionId: string): void { this.sessionStartTimes.delete(sessionId); } hasSessionTiming(sessionId: string): boolean { return this.sessionStartTimes.has(sessionId); } hasBeenAnimated(messageId: string): boolean { return this.seenMessageIds.has(messageId); } markMessageAsAnimated(messageId: string, createdTime: number): void { this.seenMessageIds.add(messageId); this.messageCreationTimes.set(messageId, createdTime); } clearAll(): void { this.sessionStartTimes.clear(); this.seenMessageIds.clear(); this.messageCreationTimes.clear(); } getDebugInfo(): { sessionStartTimes: Map; seenMessageIds: Set; messageCreationTimes: Map; } { return { sessionStartTimes: new Map(this.sessionStartTimes), seenMessageIds: new Set(this.seenMessageIds), messageCreationTimes: new Map(this.messageCreationTimes) }; } }