mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-09 22:18:19 +00:00
Users previously had an all-or-nothing choice for completed sessions: either keep them in the active sidebar or permanently delete them. That made long-lived usage brittle because valuable history stayed in the way unless users destroyed it. This change introduces archiving as a first-class lifecycle so completed work can be hidden without losing transcript history, workspace context, or restoreability. The backend now persists session archive state and excludes archived rows from active session queries by default. Dedicated archive queries and routes make archived sessions and archived workspaces addressable on their own, which is necessary once hidden data can no longer be rebuilt from the active project list. Hard-delete behavior still cleans up transcript files so destructive deletes remain truly destructive. The frontend now mirrors that lifecycle in the sidebar. Delete flows distinguish between archive and permanent delete, archived sessions can be restored, archived workspaces appear beside standalone archived sessions, and archived project sessions open with the correct workspace context instead of routing to a session URL that leaves the main view empty. Follow-up archive UI polish keeps the status affordances explicit without competing with workspace names.
91 lines
2.6 KiB
TypeScript
91 lines
2.6 KiB
TypeScript
import { promises as fs } from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
import { projectsDb, sessionsDb } from '@/modules/database/index.js';
|
|
import { AppError } from '@/shared/utils.js';
|
|
|
|
function uniqueJsonlPathsFromSessions(
|
|
sessions: Array<{ jsonl_path: string | null }>,
|
|
): string[] {
|
|
const seen = new Set<string>();
|
|
const result: string[] = [];
|
|
|
|
for (const row of sessions) {
|
|
const raw = row.jsonl_path?.trim();
|
|
if (!raw) {
|
|
continue;
|
|
}
|
|
const absolute = path.isAbsolute(raw) ? path.normalize(raw) : path.resolve(raw);
|
|
if (seen.has(absolute)) {
|
|
continue;
|
|
}
|
|
seen.add(absolute);
|
|
result.push(absolute);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function unlinkJsonlIfExists(filePath: string): Promise<void> {
|
|
try {
|
|
await fs.unlink(filePath);
|
|
} catch (error) {
|
|
const code = (error as NodeJS.ErrnoException).code;
|
|
if (code === 'ENOENT') {
|
|
return;
|
|
}
|
|
console.warn(`[project-delete] Failed to remove ${filePath}:`, (error as Error).message);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Loads all session rows for the project path and removes each distinct `jsonl_path` file on disk.
|
|
*/
|
|
export async function deleteSessionJsonlFilesForProjectPath(projectPath: string): Promise<void> {
|
|
const sessions = sessionsDb.getSessionsByProjectPathIncludingArchived(projectPath);
|
|
const paths = uniqueJsonlPathsFromSessions(sessions);
|
|
|
|
for (const filePath of paths) {
|
|
await unlinkJsonlIfExists(filePath);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* - **Soft delete** (`force` false): set `isArchived` on the `projects` row (hide from the active list; DB only).
|
|
* - **Force** (`force` true): for each session row for that `project_path`, delete the file at `jsonl_path`
|
|
* (when set), then remove session rows and the `projects` row.
|
|
*/
|
|
export async function deleteOrArchiveProject(projectId: string, force: boolean): Promise<void> {
|
|
const row = projectsDb.getProjectById(projectId);
|
|
if (!row) {
|
|
throw new AppError(`Unknown projectId: ${projectId}`, {
|
|
code: 'PROJECT_NOT_FOUND',
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
if (!force) {
|
|
projectsDb.updateProjectIsArchivedById(projectId, true);
|
|
return;
|
|
}
|
|
|
|
await deleteSessionJsonlFilesForProjectPath(row.project_path);
|
|
sessionsDb.deleteSessionsByProjectPath(row.project_path);
|
|
projectsDb.deleteProjectById(projectId);
|
|
}
|
|
|
|
/**
|
|
* Restores one archived project row back into the active project list.
|
|
*/
|
|
export function restoreArchivedProject(projectId: string): void {
|
|
const row = projectsDb.getProjectById(projectId);
|
|
if (!row) {
|
|
throw new AppError(`Unknown projectId: ${projectId}`, {
|
|
code: 'PROJECT_NOT_FOUND',
|
|
statusCode: 404,
|
|
});
|
|
}
|
|
|
|
projectsDb.updateProjectIsArchivedById(projectId, false);
|
|
}
|