mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-10 22:46:37 +00:00
222 lines
6.6 KiB
TypeScript
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,
|
|
};
|
|
};
|