mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 10:18:37 +00:00
Why this change was needed: - Project listing had an implicit side effect: every fetch wrote a debug snapshot under `.tmp/project-dumps`. That added unnecessary disk I/O to a hot path, introduced hidden runtime behavior, and created maintenance overhead for code that was not part of product functionality. - Keeping snapshot-specific exports/tests around made the projects module API broader than needed and coupled tests to temporary/debug behavior instead of user-visible behavior. - Codex sessions could remain stuck with a placeholder name (`Untitled Codex Session`) even after a real title became available from newer sync data, which degraded session discoverability in the UI. - Sidebar session rows showed duplicated provider branding and long-form relative times, which added visual noise and reduced scan speed when many sessions are listed. What changed: - Removed temporary projects snapshot dumping from `projects-with-sessions-fetch.service.ts`: - deleted snapshot types/helpers and file-write flow - removed the write call from `getProjectsWithSessions` - Removed snapshot-related surface area from `projects/index.ts`. - Removed the snapshot-focused test `projects.service.test.ts` that only validated removed debug behavior. - Updated `codex-session-synchronizer.provider.ts` to upgrade session names when an existing session still has the placeholder title but a real parsed name is now available. - Updated `SidebarSessionItem.tsx`: - removed duplicate provider logo rendering in each session row - moved age indicator to the right side - made age indicator fade on hover to prioritize action controls - switched to compact relative time format (`<1m`, `Xm`, `Xhr`, `Xd`) for faster list scanning Outcome: - Lower overhead and fewer hidden side effects in project fetches. - Cleaner module boundaries in projects. - Better Codex session naming consistency after sync. - Cleaner sidebar density and clearer hover/action behavior.
120 lines
3.5 KiB
TypeScript
120 lines
3.5 KiB
TypeScript
import os from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
import { sessionsDb } from '@/modules/database/index.js';
|
|
import {
|
|
buildLookupMap,
|
|
extractFirstValidJsonlData,
|
|
findFilesRecursivelyCreatedAfter,
|
|
normalizeSessionName,
|
|
readFileTimestamps,
|
|
} from '@/shared/utils.js';
|
|
import type { IProviderSessionSynchronizer } from '@/shared/interfaces.js';
|
|
|
|
type ParsedSession = {
|
|
sessionId: string;
|
|
projectPath: string;
|
|
sessionName?: string;
|
|
};
|
|
|
|
/**
|
|
* Session indexer for Codex transcript artifacts.
|
|
*/
|
|
export class CodexSessionSynchronizer implements IProviderSessionSynchronizer {
|
|
private readonly provider = 'codex' as const;
|
|
private readonly codexHome = path.join(os.homedir(), '.codex');
|
|
|
|
/**
|
|
* Scans ~/.codex/sessions and upserts discovered sessions into DB.
|
|
*/
|
|
async synchronize(since?: Date): Promise<number> {
|
|
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
|
|
const files = await findFilesRecursivelyCreatedAfter(
|
|
path.join(this.codexHome, 'sessions'),
|
|
'.jsonl',
|
|
since ?? null
|
|
);
|
|
|
|
let processed = 0;
|
|
for (const filePath of files) {
|
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
|
if (!parsed) {
|
|
continue;
|
|
}
|
|
|
|
const existingSession = sessionsDb.getSessionById(parsed.sessionId);
|
|
if (existingSession) {
|
|
// If session name is untitled and we now have a name, update it
|
|
if (existingSession.custom_name === 'Untitled Codex Session' && parsed.sessionName && parsed.sessionName !== 'Untitled Codex Session') {
|
|
sessionsDb.updateSessionCustomName(parsed.sessionId, parsed.sessionName);
|
|
}
|
|
}
|
|
|
|
const timestamps = await readFileTimestamps(filePath);
|
|
sessionsDb.createSession(
|
|
parsed.sessionId,
|
|
this.provider,
|
|
parsed.projectPath,
|
|
parsed.sessionName,
|
|
timestamps.createdAt,
|
|
timestamps.updatedAt,
|
|
filePath
|
|
);
|
|
processed += 1;
|
|
}
|
|
|
|
return processed;
|
|
}
|
|
|
|
/**
|
|
* Parses and upserts one Codex session JSONL file.
|
|
*/
|
|
async synchronizeFile(filePath: string): Promise<string | null> {
|
|
if (!filePath.endsWith('.jsonl')) {
|
|
return null;
|
|
}
|
|
|
|
const nameMap = await buildLookupMap(path.join(this.codexHome, 'session_index.jsonl'), 'id', 'thread_name');
|
|
const parsed = await this.processSessionFile(filePath, nameMap);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
const timestamps = await readFileTimestamps(filePath);
|
|
return sessionsDb.createSession(
|
|
parsed.sessionId,
|
|
this.provider,
|
|
parsed.projectPath,
|
|
parsed.sessionName,
|
|
timestamps.createdAt,
|
|
timestamps.updatedAt,
|
|
filePath
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Extracts session metadata from one Codex JSONL session file.
|
|
*/
|
|
private async processSessionFile(
|
|
filePath: string,
|
|
nameMap: Map<string, string>
|
|
): Promise<ParsedSession | null> {
|
|
return extractFirstValidJsonlData(filePath, (rawData) => {
|
|
const data = rawData as Record<string, unknown>;
|
|
const payload = data.payload as Record<string, unknown> | undefined;
|
|
const sessionId = typeof payload?.id === 'string' ? payload.id : undefined;
|
|
const projectPath = typeof payload?.cwd === 'string' ? payload.cwd : undefined;
|
|
|
|
if (!sessionId || !projectPath) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
sessionId,
|
|
projectPath,
|
|
sessionName: normalizeSessionName(nameMap.get(sessionId), 'Untitled Codex Session'),
|
|
};
|
|
});
|
|
}
|
|
}
|