Files
claudecodeui/src/components/sidebar/utils/utils.ts

222 lines
6.6 KiB
TypeScript

import type { TFunction } from 'i18next';
import type { Project } from '../../../types/app';
import type { ProjectSortOrder, SettingsProject, SessionViewModel, SessionWithProvider } from '../types/types';
export const readProjectSortOrder = (): ProjectSortOrder => {
try {
const rawSettings = localStorage.getItem('claude-settings');
if (!rawSettings) {
return 'name';
}
const settings = JSON.parse(rawSettings) as { projectSortOrder?: ProjectSortOrder };
return settings.projectSortOrder === 'date' ? 'date' : 'name';
} catch {
return 'name';
}
};
const LEGACY_STARRED_PROJECTS_STORAGE_KEY = 'starredProjects';
/**
* Reads legacy project stars from localStorage (used only for one-time migration to backend).
*/
export const readLegacyStarredProjectIds = (): string[] => {
try {
const saved = localStorage.getItem(LEGACY_STARRED_PROJECTS_STORAGE_KEY);
if (!saved) {
return [];
}
const parsed = JSON.parse(saved) as unknown;
if (!Array.isArray(parsed)) {
return [];
}
return parsed
.map((value) => String(value).trim())
.filter((value) => value.length > 0);
} catch {
return [];
}
};
/**
* Clears the legacy localStorage stars key after migration to backend completes.
*/
export const clearLegacyStarredProjectIds = () => {
try {
localStorage.removeItem(LEGACY_STARRED_PROJECTS_STORAGE_KEY);
} catch {
// Keep UI responsive even if storage is unavailable.
}
};
const getCreatedTimestamp = (session: SessionWithProvider): string => {
return String(session.createdAt || session.created_at || '');
};
const getUpdatedTimestamp = (session: SessionWithProvider): string => {
return String(session.lastActivity || session.updated_at || '');
};
export const getSessionDate = (session: SessionWithProvider): Date => {
return new Date(getUpdatedTimestamp(session) || getCreatedTimestamp(session) || 0);
};
export const getSessionName = (session: SessionWithProvider, t: TFunction): string => {
return session.summary || session.name || t('projects.newSession');
};
export const getSessionTime = (session: SessionWithProvider): string => {
return getUpdatedTimestamp(session) || getCreatedTimestamp(session);
};
export const createSessionViewModel = (
session: SessionWithProvider,
currentTime: Date,
t: TFunction,
): SessionViewModel => {
const sessionDate = getSessionDate(session);
const diffInMinutes = Math.floor((currentTime.getTime() - sessionDate.getTime()) / (1000 * 60));
return {
isCursorSession: session.__provider === 'cursor',
isCodexSession: session.__provider === 'codex',
isGeminiSession: session.__provider === 'gemini',
isActive: diffInMinutes < 10,
sessionName: getSessionName(session, t),
sessionTime: getSessionTime(session),
messageCount: Number(session.messageCount || 0),
};
};
export const getAllSessions = (project: Project): SessionWithProvider[] => {
const claudeSessions = [...(project.sessions || [])].map((session) => ({
...session,
__provider: 'claude' as const,
}));
const cursorSessions = (project.cursorSessions || []).map((session) => ({
...session,
__provider: 'cursor' as const,
}));
const codexSessions = (project.codexSessions || []).map((session) => ({
...session,
__provider: 'codex' as const,
}));
const geminiSessions = (project.geminiSessions || []).map((session) => ({
...session,
__provider: 'gemini' as const,
}));
return [...claudeSessions, ...cursorSessions, ...codexSessions, ...geminiSessions].sort(
(a, b) => getSessionDate(b).getTime() - getSessionDate(a).getTime(),
);
};
export const getProjectLastActivity = (project: Project): Date => {
const sessions = getAllSessions(project);
if (sessions.length === 0) {
return new Date(0);
}
return sessions.reduce((latest, session) => {
const sessionDate = getSessionDate(session);
return sessionDate > latest ? sessionDate : latest;
}, new Date(0));
};
export const sortProjects = (
projects: Project[],
projectSortOrder: ProjectSortOrder,
): Project[] => {
const byName = [...projects];
byName.sort((projectA, projectB) => {
// Star order now comes from backend `projects.isStarred`.
const aStarred = Boolean(projectA.isStarred);
const bStarred = Boolean(projectB.isStarred);
if (aStarred && !bStarred) {
return -1;
}
if (!aStarred && bStarred) {
return 1;
}
if (projectSortOrder === 'date') {
return getProjectLastActivity(projectB).getTime() - getProjectLastActivity(projectA).getTime();
}
return (projectA.displayName || projectA.projectId).localeCompare(projectB.displayName || projectB.projectId);
});
return byName;
};
export const filterProjects = (projects: Project[], searchFilter: string): Project[] => {
const normalizedSearch = searchFilter.trim().toLowerCase();
if (!normalizedSearch) {
return projects;
}
return projects.filter((project) => {
const displayName = (project.displayName || project.projectId).toLowerCase();
// `project.path`/`fullPath` is the most useful search target now that the
// folder-derived name is gone; fall back to displayName above.
const searchPath = (project.path || project.fullPath || '').toLowerCase();
return displayName.includes(normalizedSearch) || searchPath.includes(normalizedSearch);
});
};
export const getTaskIndicatorStatus = (
project: Project,
mcpServerStatus: { hasMCPServer?: boolean; isConfigured?: boolean } | null,
) => {
const projectConfigured = Boolean(project.taskmaster?.hasTaskmaster);
const mcpConfigured = Boolean(mcpServerStatus?.hasMCPServer && mcpServerStatus?.isConfigured);
if (projectConfigured && mcpConfigured) {
return 'fully-configured';
}
if (projectConfigured) {
return 'taskmaster-only';
}
if (mcpConfigured) {
return 'mcp-only';
}
return 'not-configured';
};
export const normalizeProjectForSettings = (project: Project): SettingsProject => {
const fallbackPath =
typeof project.fullPath === 'string' && project.fullPath.length > 0
? project.fullPath
: typeof project.path === 'string'
? project.path
: '';
// Legacy SettingsProject still expects a `name` field; use the projectId so
// downstream consumers that rely on a stable identifier continue to work.
return {
name: project.projectId,
displayName:
typeof project.displayName === 'string' && project.displayName.trim().length > 0
? project.displayName
: project.projectId,
fullPath: fallbackPath,
path:
typeof project.path === 'string' && project.path.length > 0
? project.path
: fallbackPath,
};
};