feat: setup delete session by id

This commit is contained in:
Haileyesus
2026-03-28 11:30:36 +03:00
parent ce0dfad638
commit e165d2ca24
13 changed files with 554 additions and 134 deletions

View File

@@ -4,8 +4,9 @@ import { promises as fs } from 'fs';
import path from 'path';
import os from 'os';
import TOML from '@iarna/toml';
import { getCodexSessions, deleteCodexSession } from '../../../projects.js';
import { getCodexSessions } from '../../../projects.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
const router = express.Router();
@@ -71,8 +72,7 @@ router.get('/sessions', async (req, res) => {
router.delete('/sessions/:sessionId', async (req, res) => {
try {
const { sessionId } = req.params;
await deleteCodexSession(sessionId);
sessionsDb.deleteSession(sessionId);
await deleteSessionFromProviders(sessionId);
res.json({ success: true });
} catch (error) {
console.error(`Error deleting Codex session ${req.params.sessionId}:`, error);

View File

@@ -1,6 +1,6 @@
import express from 'express';
import sessionManager from '../../../sessionManager.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
const router = express.Router();
@@ -13,7 +13,7 @@ router.delete('/sessions/:sessionId', async (req, res) => {
}
await sessionManager.deleteSession(sessionId);
sessionsDb.deleteSession(sessionId);
await deleteSessionFromProviders(sessionId);
res.json({ success: true });
} catch (error) {
console.error(`Error deleting Gemini session ${req.params.sessionId}:`, error);

View File

@@ -6,12 +6,12 @@ import {
getProjects,
getSessions,
renameProject,
deleteSession,
deleteProject,
searchConversations
} from '../../../projects.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
import { deleteSession as deleteSessionFromProviders } from '@/modules/sessions/sessions.service.js';
import { authenticateToken } from '../auth/auth.middleware.js';
import { getWorkspaceNameFromPath, WORKSPACES_ROOT, validateWorkspacePath } from './projects.utils.js';
@@ -69,8 +69,7 @@ router.delete('/api/projects/:projectName/sessions/:sessionId', authenticateToke
try {
const { projectName, sessionId } = req.params;
console.log(`[API] Deleting session: ${sessionId} from project: ${projectName}`);
await deleteSession(projectName, sessionId);
sessionsDb.deleteSession(sessionId);
await deleteSessionFromProviders(sessionId);
console.log(`[API] Session ${sessionId} deleted successfully`);
res.json({ success: true });
} catch (error) {

View File

@@ -49,3 +49,67 @@ export async function processClaudeSessions() {
}
}
}
function encodeClaudeProjectPath(projectPath: string): string {
return projectPath.replace(/[^a-zA-Z0-9-]/g, '-');
}
async function removeFileIfExists(filePath: string): Promise<boolean> {
try {
await fsp.unlink(filePath);
return true;
} catch (error: any) {
if (error?.code === 'ENOENT') {
return false;
}
throw error;
}
}
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
try {
return await fsp.readdir(directoryPath, { withFileTypes: true });
} catch {
return [];
}
}
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
const matches: string[] = [];
const stack = [rootPath];
while (stack.length > 0) {
const currentPath = stack.pop() as string;
const entries = await listDirectoryEntriesSafe(currentPath);
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile() && entry.name === fileName) {
matches.push(fullPath);
}
}
}
return matches;
}
export async function deleteClaudeSession(sessionId: string, workspacePath?: string): Promise<boolean> {
const claudeProjectsDir = path.join(os.homedir(), '.claude', 'projects');
const fileName = `${sessionId}.jsonl`;
let deleted = false;
if (workspacePath) {
const encodedPath = encodeClaudeProjectPath(workspacePath);
const candidateFilePath = path.join(claudeProjectsDir, encodedPath, fileName);
deleted = (await removeFileIfExists(candidateFilePath)) || deleted;
}
const matches = await findFilesByName(claudeProjectsDir, fileName);
for (const filePath of matches) {
deleted = (await removeFileIfExists(filePath)) || deleted;
}
return deleted;
}

View File

@@ -1,52 +0,0 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
export async function processCodexSessionFile(file: string, nameMap?: Map<string, string>): Promise<SessionData | null> {
if (!nameMap) {
const base = path.join(os.homedir(), '.codex');
nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
}
// Codex nests the required data inside a `payload` object
return extractFirstValidJsonlData(file, (data) => ({
workspacePath: data?.payload?.cwd,
sessionId: data?.payload?.id,
sessionName: nameMap!.get(data?.payload?.id) || 'Untitled Codex Session'
}));
}
export async function processCodexSessions() {
const base = path.join(os.homedir(), '.codex');
// Use the thread_name attribute as requested
const nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
const files = await findFilesRecursivelyCreatedAfterLastScan(path.join(base, 'sessions'), '.jsonl');
for (const file of files) {
const result = await processCodexSessionFile(file, nameMap);
if (result) {
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'codex',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -0,0 +1,151 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { buildLookupMap, extractFirstValidJsonlData, findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
export async function processCodexSessionFile(file: string, nameMap?: Map<string, string>): Promise<SessionData | null> {
if (!nameMap) {
const base = path.join(os.homedir(), '.codex');
nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
}
// Codex nests the required data inside a `payload` object
return extractFirstValidJsonlData(file, (data) => ({
workspacePath: data?.payload?.cwd,
sessionId: data?.payload?.id,
sessionName: nameMap!.get(data?.payload?.id) || 'Untitled Codex Session'
}));
}
export async function processCodexSessions() {
const base = path.join(os.homedir(), '.codex');
// Use the thread_name attribute as requested
const nameMap = await buildLookupMap(path.join(base, 'session_index.jsonl'), 'id', 'thread_name');
const files = await findFilesRecursivelyCreatedAfterLastScan(path.join(base, 'sessions'), '.jsonl');
for (const file of files) {
const result = await processCodexSessionFile(file, nameMap);
if (result) {
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'codex',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}
function buildCodexDatePathParts(createdAt: string): Array<{ year: string; month: string; day: string }> {
const parsedDate = new Date(createdAt);
if (Number.isNaN(parsedDate.getTime())) {
return [];
}
const localDate = {
year: String(parsedDate.getFullYear()),
month: String(parsedDate.getMonth() + 1),
day: String(parsedDate.getDate()),
};
const utcDate = {
year: String(parsedDate.getUTCFullYear()),
month: String(parsedDate.getUTCMonth() + 1),
day: String(parsedDate.getUTCDate()),
};
if (
localDate.year === utcDate.year &&
localDate.month === utcDate.month &&
localDate.day === utcDate.day
) {
return [localDate];
}
return [localDate, utcDate];
}
async function removeFileIfExists(filePath: string): Promise<boolean> {
try {
await fsp.unlink(filePath);
return true;
} catch (error: any) {
if (error?.code === 'ENOENT') {
return false;
}
throw error;
}
}
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
try {
return await fsp.readdir(directoryPath, { withFileTypes: true });
} catch {
return [];
}
}
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
const matches: string[] = [];
const stack = [rootPath];
while (stack.length > 0) {
const currentPath = stack.pop() as string;
const entries = await listDirectoryEntriesSafe(currentPath);
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile() && entry.name === fileName) {
matches.push(fullPath);
}
}
}
return matches;
}
export async function deleteCodexSession(sessionId: string, createdAt?: string): Promise<boolean> {
const codexSessionsDir = path.join(os.homedir(), '.codex', 'sessions');
const fileName = `${sessionId}.jsonl`;
let deleted = false;
if (createdAt) {
const datePathParts = buildCodexDatePathParts(createdAt);
for (const parts of datePathParts) {
const candidateFilePath = path.join(
codexSessionsDir,
parts.year,
parts.month,
parts.day,
fileName,
);
deleted = (await removeFileIfExists(candidateFilePath)) || deleted;
}
}
if (!deleted) {
const matches = await findFilesByName(codexSessionsDir, fileName);
for (const filePath of matches) {
deleted = (await removeFileIfExists(filePath)) || deleted;
}
}
return deleted;
}

View File

@@ -12,6 +12,84 @@ function md5(input: string): string {
return crypto.createHash('md5').update(input).digest('hex');
}
async function removeFileIfExists(filePath: string): Promise<boolean> {
try {
await fsp.unlink(filePath);
return true;
} catch (error: any) {
if (error?.code === 'ENOENT') {
return false;
}
throw error;
}
}
async function removeDirectoryIfExists(directoryPath: string): Promise<boolean> {
try {
await fsp.rm(directoryPath, { recursive: true, force: false });
return true;
} catch (error: any) {
if (error?.code === 'ENOENT') {
return false;
}
throw error;
}
}
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
try {
return await fsp.readdir(directoryPath, { withFileTypes: true });
} catch {
return [];
}
}
async function findDirectoriesByName(rootPath: string, directoryName: string): Promise<string[]> {
const matches: string[] = [];
const stack = [rootPath];
while (stack.length > 0) {
const currentPath = stack.pop() as string;
const entries = await listDirectoryEntriesSafe(currentPath);
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const fullPath = path.join(currentPath, entry.name);
if (entry.name === directoryName) {
matches.push(fullPath);
}
stack.push(fullPath);
}
}
return matches;
}
async function findFilesByName(rootPath: string, fileName: string): Promise<string[]> {
const matches: string[] = [];
const stack = [rootPath];
while (stack.length > 0) {
const currentPath = stack.pop() as string;
const entries = await listDirectoryEntriesSafe(currentPath);
for (const entry of entries) {
const fullPath = path.join(currentPath, entry.name);
if (entry.isDirectory()) {
stack.push(fullPath);
} else if (entry.isFile() && entry.name === fileName) {
matches.push(fullPath);
}
}
}
return matches;
}
export async function extractWorkspacePathFromWorkerLog(filePath: string): Promise<string | null> {
try {
const fileStream = fs.createReadStream(filePath, { encoding: 'utf8' });
@@ -104,3 +182,26 @@ export async function processCursorSessions() {
// Base cursor directory or projects directory likely doesn't exist
}
}
export async function deleteCursorSession(sessionId: string, workspacePath?: string): Promise<boolean> {
const cursorChatsDir = path.join(os.homedir(), '.cursor', 'chats');
let deleted = false;
if (workspacePath) {
const cwdId = md5(workspacePath);
const candidateDir = path.join(cursorChatsDir, cwdId, sessionId);
deleted = (await removeDirectoryIfExists(candidateDir)) || deleted;
}
const sessionDirs = await findDirectoriesByName(cursorChatsDir, sessionId);
for (const directoryPath of sessionDirs) {
deleted = (await removeDirectoryIfExists(directoryPath)) || deleted;
}
const jsonlFiles = await findFilesByName(cursorChatsDir, `${sessionId}.jsonl`);
for (const filePath of jsonlFiles) {
deleted = (await removeFileIfExists(filePath)) || deleted;
}
return deleted;
}

View File

@@ -1,53 +0,0 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
export async function processGeminiSessionFile(file: string): Promise<SessionData | null> {
try {
// Gemini uses standard JSON (not JSONL), so we read the whole file at once
const fileContent = await fsp.readFile(file, 'utf8');
const data = JSON.parse(fileContent);
if (data?.id && data?.projectPath) {
return {
sessionId: data.id,
workspacePath: data.projectPath,
sessionName: data.messages?.[0]?.content || 'New Gemini Chat'
};
}
} catch (e) {
// Ignore parsing error for gemini
}
return null;
}
export async function processGeminiSessions() {
const geminiPath = path.join(os.homedir(), '.gemini', 'sessions');
const files = await findFilesRecursivelyCreatedAfterLastScan(geminiPath, '.json');
for (const file of files) {
const result = await processGeminiSessionFile(file);
if (result) {
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'gemini',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}

View File

@@ -0,0 +1,118 @@
import os from 'os';
import path from 'path';
import fsp from 'node:fs/promises';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { findFilesRecursivelyCreatedAfterLastScan } from '@/modules/providers/shared/session-parser.utils.js';
import { SessionData } from '@/shared/types/session.js';
export async function processGeminiSessionFile(file: string): Promise<SessionData | null> {
try {
// Gemini uses standard JSON (not JSONL), so we read the whole file at once
const fileContent = await fsp.readFile(file, 'utf8');
const data = JSON.parse(fileContent);
if (data?.id && data?.projectPath) {
return {
sessionId: data.id,
workspacePath: data.projectPath,
sessionName: data.messages?.[0]?.content || 'New Gemini Chat'
};
}
} catch (e) {
// Ignore parsing error for gemini
}
return null;
}
export async function processGeminiSessions() {
const geminiPath = path.join(os.homedir(), '.gemini', 'sessions');
const files = await findFilesRecursivelyCreatedAfterLastScan(geminiPath, '.json');
for (const file of files) {
const result = await processGeminiSessionFile(file);
if (result) {
let createdAt: string | undefined;
let updatedAt: string | undefined;
try {
const stat = await fsp.stat(file);
createdAt = stat.birthtime.toISOString();
updatedAt = stat.mtime.toISOString();
} catch {
// Ignore stat failures and let DB defaults handle created_at/updated_at.
}
sessionsDb.createSession(
result.sessionId,
'gemini',
result.workspacePath,
result.sessionName,
createdAt,
updatedAt,
);
}
}
}
async function removeFileIfExists(filePath: string): Promise<boolean> {
try {
await fsp.unlink(filePath);
return true;
} catch (error: any) {
if (error?.code === 'ENOENT') {
return false;
}
throw error;
}
}
async function listDirectoryEntriesSafe(directoryPath: string): Promise<import('node:fs').Dirent[]> {
try {
return await fsp.readdir(directoryPath, { withFileTypes: true });
} catch {
return [];
}
}
export async function deleteGeminiSession(sessionId: string): Promise<boolean> {
const geminiHome = path.join(os.homedir(), '.gemini');
const geminiSessionsDir = path.join(geminiHome, 'sessions');
const geminiTmpDir = path.join(geminiHome, 'tmp');
let deleted = false;
deleted = (await removeFileIfExists(path.join(geminiSessionsDir, `${sessionId}.json`))) || deleted;
deleted = (await removeFileIfExists(path.join(geminiSessionsDir, `${sessionId}.jsonl`))) || deleted;
const projectDirs = await listDirectoryEntriesSafe(geminiTmpDir);
for (const projectDir of projectDirs) {
if (!projectDir.isDirectory()) {
continue;
}
const chatsDir = path.join(geminiTmpDir, projectDir.name, 'chats');
const chatFiles = await listDirectoryEntriesSafe(chatsDir);
for (const chatFile of chatFiles) {
if (!chatFile.isFile() || !chatFile.name.endsWith('.json')) {
continue;
}
const chatFilePath = path.join(chatsDir, chatFile.name);
if (chatFile.name === `${sessionId}.json`) {
deleted = (await removeFileIfExists(chatFilePath)) || deleted;
continue;
}
try {
const content = await fsp.readFile(chatFilePath, 'utf8');
const parsed = JSON.parse(content);
const parsedId = parsed?.sessionId || parsed?.id;
if (parsedId === sessionId) {
deleted = (await removeFileIfExists(chatFilePath)) || deleted;
}
} catch {
// Ignore unreadable/malformed session files.
}
}
}
return deleted;
}

View File

@@ -24,10 +24,10 @@ router.put('/api/sessions/:sessionId/rename', authenticateToken, async (req, res
if (summary.trim().length > 500) {
return res.status(400).json({ error: 'Summary must not exceed 500 characters' });
}
if (!provider || !VALID_PROVIDERS.includes(provider)) {
if (provider && !VALID_PROVIDERS.includes(provider)) {
return res.status(400).json({ error: `Provider must be one of: ${VALID_PROVIDERS.join(', ')}` });
}
sessionsDb.createSessionName(safeSessionId, provider, summary.trim());
sessionsDb.updateSessionCustomName(safeSessionId, summary.trim());
res.json({ success: true });
} catch (error) {
console.error(`[API] Error renaming session ${req.params.sessionId}:`, error);

View File

@@ -1,15 +1,26 @@
import { scanStateDb } from '@/shared/database/repositories/scan-state.db.js';
import { processClaudeSessions } from '@/modules/providers/claude/claude.session-parser.js';
import { processCodexSessions } from '@/modules/providers/codex/codex.session-parser.js';
import { processGeminiSessions } from '@/modules/providers/gemini/gemini.session-parser.js';
import { processCursorSessions } from '@/modules/providers/cursor/cursor.session-parser.js';
import { sessionsDb } from '@/shared/database/repositories/sessions.db.js';
import { processClaudeSessions, deleteClaudeSession } from '@/modules/providers/claude/claude.session-processor.js';
import { processCodexSessions, deleteCodexSession } from '@/modules/providers/codex/codex.session-processor.js';
import { processGeminiSessions, deleteGeminiSession } from '@/modules/providers/gemini/gemini.session-processor.js';
import { processCursorSessions, deleteCursorSession } from '@/modules/providers/cursor/cursor.session-processor.js';
const SESSION_ID_PATTERN = /^[a-zA-Z0-9._-]{1,120}$/;
function sanitizeSessionId(sessionId: string): string {
const value = String(sessionId || '').trim();
if (!SESSION_ID_PATTERN.test(value)) {
throw new Error('Invalid session ID format');
}
return value;
}
export async function processSessions() {
// 1. Start the timer with a unique label
console.time("🚀 Workspace sync total time");
console.time('Workspace sync total time');
console.log("Starting workspace sync...");
console.log('Starting workspace sync...');
try {
// Wrapping in Promise.all allows these to process concurrently, speeding up the boot time
await Promise.allSettled([
@@ -21,12 +32,33 @@ export async function processSessions() {
scanStateDb.updateLastScannedAt();
} catch (error) {
console.error("An error occurred during sync:", error);
console.error('An error occurred during sync:', error);
} finally {
console.log("----------------------------------");
console.log('----------------------------------');
// 2. Stop the timer using the exact same label
// This will print: 🚀 Workspace sync total time: 123.456ms
console.timeEnd("🚀 Workspace sync total time");
console.log("Workspace synchronization complete.");
// This will print: Workspace sync total time: 123.456ms
console.timeEnd('Workspace sync total time');
console.log('Workspace synchronization complete.');
}
}
export async function deleteSession(sessionId: string): Promise<void> {
const safeSessionId = sanitizeSessionId(sessionId);
const existingSession = sessionsDb.getSessionById(safeSessionId);
const workspacePath = existingSession?.workspace_path;
const createdAt = existingSession?.created_at;
const deletionResults = await Promise.allSettled([
deleteClaudeSession(safeSessionId, workspacePath),
deleteCodexSession(safeSessionId, createdAt),
deleteGeminiSession(safeSessionId),
deleteCursorSession(safeSessionId, workspacePath),
]);
const rejectedResult = deletionResults.find((result) => result.status === 'rejected') as PromiseRejectedResult | undefined;
if (rejectedResult) {
throw rejectedResult.reason;
}
sessionsDb.deleteSession(safeSessionId);
}

View File

@@ -4,10 +4,10 @@ import os from "os";
import { promises as fsPromises } from "fs";
import { logger } from "@/shared/utils/logger.js";
import { processSessions } from "@/modules/sessions/sessions.service.js";
import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-parser.js";
import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-parser.js";
import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-parser.js";
import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-parser.js";
import { processClaudeSessionFile } from "@/modules/providers/claude/claude.session-processor.js";
import { processCodexSessionFile } from "@/modules/providers/codex/codex.session-processor.js";
import { processGeminiSessionFile } from "@/modules/providers/gemini/gemini.session-processor.js";
import { processCursorSessionFile } from "@/modules/providers/cursor/cursor.session-processor.js";
import { sessionsDb } from "@/shared/database/repositories/sessions.db.js";
import { LLMProvider } from "@/shared/types/app.js";

View File

@@ -1,6 +1,7 @@
import { workspaceOriginalPathsDb } from '@/shared/database/repositories/workspace-original-paths.db.js';
import { getConnection } from '@/shared/database/connection.js';
import type { SessionWithSummary } from '@/shared/database/types.js';
import path from 'node:path';
import type { SessionsRow, SessionWithSummary } from '@/shared/database/types.js';
// ---------------------------------------------------------------------------
// Queries
@@ -11,6 +12,11 @@ type SessionNameLookupRow = {
custom_name: string;
};
type SessionMetadataLookupRow = Pick<
SessionsRow,
'session_id' | 'provider' | 'workspace_path' | 'created_at' | 'updated_at'
>;
function normalizeTimestamp(value?: string): string | null {
if (!value) return null;
@@ -22,6 +28,34 @@ function normalizeTimestamp(value?: string): string | null {
return parsed.toISOString();
}
function normalizeCodexWorkspacePath(workspacePath: string): string {
const trimmedPath = workspacePath.trim();
if (!trimmedPath) {
return workspacePath;
}
if (process.platform !== 'win32') {
return path.normalize(trimmedPath);
}
let strippedPath = trimmedPath;
if (strippedPath.startsWith('\\\\?\\UNC\\')) {
strippedPath = `\\\\${strippedPath.slice('\\\\?\\UNC\\'.length)}`;
} else if (strippedPath.startsWith('\\\\?\\')) {
strippedPath = strippedPath.slice('\\\\?\\'.length);
}
return path.win32.normalize(strippedPath);
}
function normalizeWorkspacePathForProvider(provider: string, workspacePath: string): string {
if (provider !== 'codex') {
return workspacePath;
}
return normalizeCodexWorkspacePath(workspacePath);
}
export const sessionsDb = {
createSession(
@@ -35,17 +69,30 @@ export const sessionsDb = {
const db = getConnection();
const createdAtValue = normalizeTimestamp(createdAt);
const updatedAtValue = normalizeTimestamp(updatedAt);
const normalizedWorkspacePath = normalizeWorkspacePathForProvider(provider, workspacePath);
// First, ensure the workspace path is recorded in the workspace_original_paths table
// since it's a foreign key in the sessions table.
workspaceOriginalPathsDb.createWorkspacePath(workspacePath);
workspaceOriginalPathsDb.createWorkspacePath(normalizedWorkspacePath);
db.prepare(
`INSERT INTO sessions (session_id, provider, custom_name, workspace_path, created_at, updated_at)
VALUES (?, ?, ?, ?, COALESCE(?, CURRENT_TIMESTAMP), COALESCE(?, CURRENT_TIMESTAMP))
ON CONFLICT(session_id) DO UPDATE SET updated_at = excluded.updated_at
ON CONFLICT(session_id) DO UPDATE SET
updated_at = excluded.updated_at,
workspace_path = excluded.workspace_path
WHERE sessions.provider = excluded.provider`
).run(session_id, provider, customName, workspacePath, createdAtValue, updatedAtValue);
).run(session_id, provider, customName, normalizedWorkspacePath, createdAtValue, updatedAtValue);
},
/** Updates a custom session name by session id, regardless of provider. */
updateSessionCustomName(sessionId: string, customName: string): void {
const db = getConnection();
db.prepare(
`UPDATE sessions
SET custom_name = ?, updated_at = CURRENT_TIMESTAMP
WHERE session_id = ?`
).run(customName, sessionId);
},
/** Updates a custom session name for an existing session row. */
@@ -58,6 +105,19 @@ export const sessionsDb = {
).run(customName, sessionId, provider);
},
getSessionById(sessionId: string): SessionMetadataLookupRow | null {
const db = getConnection();
const row = db
.prepare(
`SELECT session_id, provider, workspace_path, created_at, updated_at
FROM sessions
WHERE session_id = ?`
)
.get(sessionId) as SessionMetadataLookupRow | undefined;
return row ?? null;
},
getSessionName(sessionId: string, provider: string): string | null {
const db = getConnection();
const row = db