import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; import type { SessionSummaryMeta } from './types'; const formatDateLabel = (value: string | number) => { const targetDate = new Date(value); const today = new Date(); const isSameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); const yesterday = new Date(today); yesterday.setDate(today.getDate() - 1); if (isSameDay(targetDate, today)) { return 'Today'; } if (isSameDay(targetDate, yesterday)) { return 'Yesterday'; } const formatted = targetDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric', }); return formatted.replace(',', ''); }; export const formatSessionDateLabel = (updatedMs: number): string => { const today = new Date(); const updatedDate = new Date(updatedMs); const isSameDay = (a: Date, b: Date) => a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate(); if (isSameDay(updatedDate, today)) { const diff = Date.now() - updatedMs; if (diff < 60_000) return 'Just now'; if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}min ago`; return `${Math.floor(diff / 3_600_000)}h ago`; } return formatDateLabel(updatedMs); }; export const formatSessionCompactDateLabel = (updatedMs: number): string => { const diff = Math.max(0, Date.now() - updatedMs); const minute = 60_000; const hour = 60 * minute; const day = 24 * hour; const week = 7 * day; const month = 30 * day; const year = 365 * day; if (diff < hour) { return `${Math.max(1, Math.floor(diff / minute))}m`; } if (diff < day) { return `${Math.floor(diff / hour)}h`; } if (diff < week) { return `${Math.floor(diff / day)}d`; } if (diff < 5 * week) { return `${Math.floor(diff / week)}w`; } if (diff < year) { return `${Math.floor(diff / month)}mo`; } return `${Math.floor(diff / year)}y`; }; export const normalizePath = (value?: string | null) => { if (!value) { return null; } const normalized = value.replace(/\\/g, '/').replace(/\/+$/, ''); return normalized.length === 0 ? '/' : normalized; }; export const normalizeForBranchComparison = (value: string): string => { return value .toLowerCase() .replace(/^opencode[/-]?/i, '') .replace(/[-_]/g, '') .trim(); }; export const isBranchDifferentFromLabel = (branch: string | null, label: string): boolean => { if (!branch) return false; return normalizeForBranchComparison(branch) !== normalizeForBranchComparison(label); }; const toFiniteNumber = (value: unknown): number | undefined => { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string' && value.trim().length > 0) { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return undefined; }; const getSessionCreatedAt = (session: Session): number => { return toFiniteNumber(session.time?.created) ?? 0; }; const getSessionUpdatedAt = (session: Session): number => { return toFiniteNumber(session.time?.updated) ?? toFiniteNumber(session.time?.created) ?? 0; }; export const compareSessionsByPinnedAndTime = ( a: Session, b: Session, pinnedSessionIds: Set, ): number => { const aPinned = pinnedSessionIds.has(a.id); const bPinned = pinnedSessionIds.has(b.id); if (aPinned !== bPinned) { return aPinned ? -1 : 1; } if (aPinned && bPinned) { return getSessionCreatedAt(b) - getSessionCreatedAt(a); } return getSessionUpdatedAt(b) - getSessionUpdatedAt(a); }; export const compareSessionsByPinnedAndCreated = ( a: Session, b: Session, pinnedSessionIds: Set, ): number => { const aPinned = pinnedSessionIds.has(a.id); const bPinned = pinnedSessionIds.has(b.id); if (aPinned !== bPinned) { return aPinned ? -1 : 1; } return getSessionCreatedAt(b) - getSessionCreatedAt(a); }; export const dedupeSessionsById = (sessions: Session[]): Session[] => { const byId = new Map(); sessions.forEach((session) => { byId.set(session.id, session); }); return Array.from(byId.values()); }; export const getArchivedScopeKey = (projectRoot: string): string => `__archived__:${projectRoot}`; export const resolveArchivedFolderName = (session: Session, projectRoot: string | null): string => { const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null); const projectWorktree = normalizePath((session as Session & { project?: { worktree?: string | null } | null }).project?.worktree ?? null); const resolved = sessionDirectory ?? projectWorktree; if (!resolved) { return 'unassigned'; } if (projectRoot && resolved === projectRoot) { return 'project root'; } const source = projectRoot && resolved.startsWith(`${projectRoot}/`) ? resolved.slice(projectRoot.length + 1) : resolved; const segments = source.split('/').filter(Boolean); return segments[segments.length - 1] ?? 'unassigned'; }; export const isSessionRelatedToProject = ( session: Session, projectRoot: string, validDirectories?: Set, ): boolean => { const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null); const projectWorktree = normalizePath((session as Session & { project?: { worktree?: string | null } | null }).project?.worktree ?? null); if (projectWorktree && (projectWorktree === projectRoot || projectWorktree.startsWith(`${projectRoot}/`))) { return true; } if (!sessionDirectory) { return false; } if (validDirectories && validDirectories.has(sessionDirectory)) { return true; } return sessionDirectory === projectRoot || sessionDirectory.startsWith(`${projectRoot}/`); }; const parseSummaryCount = (value: number | string | null | undefined): number | null => { if (typeof value === 'number' && Number.isFinite(value)) { return value; } if (typeof value === 'string') { const parsed = Number(value); if (Number.isFinite(parsed)) { return parsed; } } return null; }; export const resolveSessionDiffStats = (summary?: SessionSummaryMeta): { additions: number; deletions: number } | null => { if (!summary) { return null; } const directAdditions = parseSummaryCount(summary.additions); const directDeletions = parseSummaryCount(summary.deletions); if (directAdditions !== null || directDeletions !== null) { const stats = { additions: Math.max(0, directAdditions ?? 0), deletions: Math.max(0, directDeletions ?? 0), }; return stats.additions === 0 && stats.deletions === 0 ? null : stats; } const diffs = Array.isArray(summary.diffs) ? summary.diffs : []; if (diffs.length === 0) { return null; } let additions = 0; let deletions = 0; diffs.forEach((diff) => { additions += Math.max(0, parseSummaryCount(diff.additions) ?? 0); deletions += Math.max(0, parseSummaryCount(diff.deletions) ?? 0); }); return additions === 0 && deletions === 0 ? null : { additions, deletions }; }; export const formatProjectLabel = (label: string): string => { return label .replace(/[-_]/g, ' ') .replace(/\b\w/g, (char) => char.toUpperCase()); }; export const renderHighlightedText = (text: string, query: string): React.ReactNode => { if (!query) { return text; } const loweredText = text.toLowerCase(); const loweredQuery = query.toLowerCase(); const queryLength = loweredQuery.length; if (queryLength === 0) { return text; } const parts: React.ReactNode[] = []; let cursor = 0; let matchIndex = loweredText.indexOf(loweredQuery, cursor); while (matchIndex !== -1) { if (matchIndex > cursor) { parts.push(text.slice(cursor, matchIndex)); } const matchText = text.slice(matchIndex, matchIndex + queryLength); parts.push( {matchText} , ); cursor = matchIndex + queryLength; matchIndex = loweredText.indexOf(loweredQuery, cursor); } if (cursor < text.length) { parts.push(text.slice(cursor)); } return parts.length > 0 ? parts : text; };