From 14e6b5b7b246a0519cb4db14211d2baf5b5849b9 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:07:54 +0300 Subject: [PATCH] refactor(projects/sidebar): remove temp snapshot side-effects and simplify session metadata UX 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. --- server/modules/projects/index.ts | 2 - .../projects-with-sessions-fetch.service.ts | 70 ----------------- .../projects/tests/projects.service.test.ts | 33 -------- .../codex-session-synchronizer.provider.ts | 8 ++ .../view/subcomponents/SidebarSessionItem.tsx | 76 ++++++++++++------- 5 files changed, 56 insertions(+), 133 deletions(-) delete mode 100644 server/modules/projects/tests/projects.service.test.ts diff --git a/server/modules/projects/index.ts b/server/modules/projects/index.ts index 8a3188de..2adbf7a5 100644 --- a/server/modules/projects/index.ts +++ b/server/modules/projects/index.ts @@ -1,8 +1,6 @@ export { - createProjectsSnapshot, generateDisplayName, getProjectsWithSessions, - writeSnapshot, } from './services/projects-with-sessions-fetch.service.js'; export { updateProjectDisplayName } from './services/project-management.service.js'; export { deleteOrArchiveProject, deleteSessionJsonlFilesForProjectPath } from './services/project-delete.service.js'; diff --git a/server/modules/projects/services/projects-with-sessions-fetch.service.ts b/server/modules/projects/services/projects-with-sessions-fetch.service.ts index 81689be0..54c9e084 100644 --- a/server/modules/projects/services/projects-with-sessions-fetch.service.ts +++ b/server/modules/projects/services/projects-with-sessions-fetch.service.ts @@ -5,7 +5,6 @@ import { projectsDb, sessionsDb } from '@/modules/database/index.js'; import { sessionSynchronizerService } from '@/modules/providers/index.js'; import { WS_OPEN_STATE, connectedClients } from '@/modules/websocket/index.js'; import type { RealtimeClientConnection } from '@/shared/types.js'; -import { findAppRoot, getModuleDir } from '@/utils/runtime-paths.js'; type SessionSummary = { id: string; @@ -32,12 +31,6 @@ export type ProjectListItem = { }; }; -export type ProjectsSnapshot = { - generatedAt: string; - projectCount: number; - projects: ProjectListItem[]; -}; - type ProgressUpdate = { phase: 'loading' | 'complete'; current: number; @@ -49,12 +42,6 @@ type GetProjectsWithSessionsOptions = { skipSynchronization?: boolean; }; -const __dirname = getModuleDir(import.meta.url); -const APP_ROOT = findAppRoot(__dirname); -const PROJECTS_DUMP_DIR = path.join(APP_ROOT, '.tmp', 'project-dumps'); - -let projectsSnapshotCounter: number | null = null; - /** * Generate better display name from path. */ @@ -126,62 +113,6 @@ function buildSessionsByProviderFromDb(projectPath: string): SessionsByProvider return byProvider; } -async function getNextProjectsSnapshotPath(): Promise { - await fs.mkdir(PROJECTS_DUMP_DIR, { recursive: true }); - - if (projectsSnapshotCounter === null) { - const entries = await fs.readdir(PROJECTS_DUMP_DIR).catch(() => []); - projectsSnapshotCounter = entries.reduce((max, entry) => { - const match = entry.match(/^projects-(\d+)\.json$/); - if (!match) { - return max; - } - - return Math.max(max, Number(match[1])); - }, 0); - } - - projectsSnapshotCounter += 1; - const suffix = String(projectsSnapshotCounter).padStart(4, '0'); - return path.join(PROJECTS_DUMP_DIR, `projects-${suffix}.json`); -} - -/** - * Builds a typed snapshot payload for project dumps. - */ -export function createProjectsSnapshot(projects: ProjectListItem[]): ProjectsSnapshot { - return { - generatedAt: new Date().toISOString(), - projectCount: projects.length, - projects, - }; -} - -/** - * Writes a projects snapshot file as an incrementing artifact. - */ -export async function writeSnapshot(projects: ProjectListItem[]): Promise { - try { - const snapshot = createProjectsSnapshot(projects); - const snapshotJson = JSON.stringify(snapshot, (_, value) => (typeof value === 'bigint' ? value.toString() : value), 2); - - while (true) { - const snapshotPath = await getNextProjectsSnapshotPath(); - try { - await fs.writeFile(snapshotPath, snapshotJson, { encoding: 'utf8', flag: 'wx' }); - break; - } catch (error) { - if ((error as NodeJS.ErrnoException).code === 'EEXIST') { - continue; - } - throw error; - } - } - } catch (error) { - console.warn('Could not write projects snapshot:', (error as Error).message); - } -} - // Broadcast progress to all connected WebSocket clients function broadcastProgress(progress: ProgressUpdate) { const message = JSON.stringify({ @@ -261,6 +192,5 @@ export async function getProjectsWithSessions( total: totalProjects, }); - await writeSnapshot(projects); return projects; } diff --git a/server/modules/projects/tests/projects.service.test.ts b/server/modules/projects/tests/projects.service.test.ts deleted file mode 100644 index f7c85266..00000000 --- a/server/modules/projects/tests/projects.service.test.ts +++ /dev/null @@ -1,33 +0,0 @@ -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - createProjectsSnapshot, -} from '@/modules/projects/index.js'; -import { ProjectListItem, ProjectsSnapshot } from '@/modules/projects/services/projects-with-sessions-fetch.service.js'; - -test('createProjectsSnapshot returns an object matching the predefined snapshot type', () => { - const projects: ProjectListItem[] = [ - { - projectId: 'project-1', - path: '/tmp/project-1', - displayName: 'project-1', - fullPath: '/tmp/project-1', - isStarred: false, - sessions: [], - cursorSessions: [], - codexSessions: [], - geminiSessions: [], - sessionMeta: { - hasMore: false, - total: 0, - }, - }, - ]; - - const snapshot: ProjectsSnapshot = createProjectsSnapshot(projects); - - assert.equal(typeof snapshot.generatedAt, 'string'); - assert.equal(snapshot.projectCount, 1); - assert.deepEqual(snapshot.projects, projects); -}); diff --git a/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts b/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts index 5f3ee207..bd1edc0c 100644 --- a/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +++ b/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts @@ -42,6 +42,14 @@ export class CodexSessionSynchronizer implements IProviderSessionSynchronizer { 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, diff --git a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx index 8ba26f18..7da02cb2 100644 --- a/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx +++ b/src/components/sidebar/view/subcomponents/SidebarSessionItem.tsx @@ -1,8 +1,8 @@ -import { Check, Clock, Edit2, Trash2, X } from 'lucide-react'; +import { Check, Edit2, Trash2, X } from 'lucide-react'; import type { TFunction } from 'i18next'; + import { Badge, Button } from '../../../../shared/view/ui'; import { cn } from '../../../../lib/utils'; -import { formatTimeAgo } from '../../../../utils/dateUtils'; import type { Project, ProjectSession, LLMProvider } from '../../../../types/app'; import type { SessionWithProvider } from '../../types/types'; import { createSessionViewModel } from '../../utils/utils'; @@ -30,6 +30,34 @@ type SidebarSessionItemProps = { t: TFunction; }; +/** + * Compact relative time for sidebar rows: + * <1m, Xm, Xhr, Xd. + */ +const formatCompactSessionAge = (dateString: string, currentTime: Date): string => { + const date = new Date(dateString); + if (Number.isNaN(date.getTime())) { + return ''; + } + + const diffInMinutes = Math.floor(Math.max(0, currentTime.getTime() - date.getTime()) / (1000 * 60)); + if (diffInMinutes < 1) { + return '<1m'; + } + + if (diffInMinutes < 60) { + return `${diffInMinutes}m`; + } + + const diffInHours = Math.floor(diffInMinutes / 60); + if (diffInHours < 24) { + return `${diffInHours}hr`; + } + + const diffInDays = Math.floor(diffInHours / 24); + return `${diffInDays}d`; +}; + export default function SidebarSessionItem({ project, session, @@ -48,6 +76,7 @@ export default function SidebarSessionItem({ }: SidebarSessionItemProps) { const sessionView = createSessionViewModel(session, currentTime, t); const isSelected = selectedSession?.id === session.id; + const compactSessionAge = formatCompactSessionAge(sessionView.sessionTime, currentTime); // Sessions are owned by a project identified by `projectId` (DB primary key) // after the projectName → projectId migration. @@ -94,20 +123,18 @@ export default function SidebarSessionItem({
-
{sessionView.sessionName}
-
- - - {formatTimeAgo(sessionView.sessionTime, currentTime, t)} - +
+
{sessionView.sessionName}
+ {compactSessionAge && ( + {compactSessionAge} + )} +
+
{sessionView.messageCount > 0 && ( - + {sessionView.messageCount} )} - - -
@@ -138,23 +165,16 @@ export default function SidebarSessionItem({
-
{sessionView.sessionName}
-
- - - {formatTimeAgo(sessionView.sessionTime, currentTime, t)} - - {sessionView.messageCount > 0 && ( - - {sessionView.messageCount} - +
+
{sessionView.sessionName}
+ {compactSessionAge && ( + + {compactSessionAge} + )} - - - +
+
+ {sessionView.messageCount > 0 && {sessionView.messageCount}}