mirror of
https://github.com/siteboon/claudecodeui.git
synced 2025-12-09 10:09:37 +00:00
feat: Implement Cursor session fetching and enhance message parsing in ChatInterface
This commit is contained in:
@@ -2,6 +2,10 @@ import { promises as fs } from 'fs';
|
||||
import fsSync from 'fs';
|
||||
import path from 'path';
|
||||
import readline from 'readline';
|
||||
import crypto from 'crypto';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { open } from 'sqlite';
|
||||
import os from 'os';
|
||||
|
||||
// Cache for extracted project directories
|
||||
const projectDirectoryCache = new Map();
|
||||
@@ -207,6 +211,14 @@ async function getProjects() {
|
||||
console.warn(`Could not load sessions for project ${entry.name}:`, e.message);
|
||||
}
|
||||
|
||||
// Also fetch Cursor sessions for this project
|
||||
try {
|
||||
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
||||
} catch (e) {
|
||||
console.warn(`Could not load Cursor sessions for project ${entry.name}:`, e.message);
|
||||
project.cursorSessions = [];
|
||||
}
|
||||
|
||||
projects.push(project);
|
||||
}
|
||||
}
|
||||
@@ -236,9 +248,17 @@ async function getProjects() {
|
||||
fullPath: actualProjectDir,
|
||||
isCustomName: !!projectConfig.displayName,
|
||||
isManuallyAdded: true,
|
||||
sessions: []
|
||||
sessions: [],
|
||||
cursorSessions: []
|
||||
};
|
||||
|
||||
// Try to fetch Cursor sessions for manual projects too
|
||||
try {
|
||||
project.cursorSessions = await getCursorSessions(actualProjectDir);
|
||||
} catch (e) {
|
||||
console.warn(`Could not load Cursor sessions for manual project ${projectName}:`, e.message);
|
||||
}
|
||||
|
||||
projects.push(project);
|
||||
}
|
||||
}
|
||||
@@ -615,6 +635,117 @@ async function addProjectManually(projectPath, displayName = null) {
|
||||
};
|
||||
}
|
||||
|
||||
// Fetch Cursor sessions for a given project path
|
||||
async function getCursorSessions(projectPath) {
|
||||
try {
|
||||
// Calculate cwdID hash for the project path (Cursor uses MD5 hash)
|
||||
const cwdId = crypto.createHash('md5').update(projectPath).digest('hex');
|
||||
const cursorChatsPath = path.join(os.homedir(), '.cursor', 'chats', cwdId);
|
||||
|
||||
// Check if the directory exists
|
||||
try {
|
||||
await fs.access(cursorChatsPath);
|
||||
} catch (error) {
|
||||
// No sessions for this project
|
||||
return [];
|
||||
}
|
||||
|
||||
// List all session directories
|
||||
const sessionDirs = await fs.readdir(cursorChatsPath);
|
||||
const sessions = [];
|
||||
|
||||
for (const sessionId of sessionDirs) {
|
||||
const sessionPath = path.join(cursorChatsPath, sessionId);
|
||||
const storeDbPath = path.join(sessionPath, 'store.db');
|
||||
|
||||
try {
|
||||
// Check if store.db exists
|
||||
await fs.access(storeDbPath);
|
||||
|
||||
// Capture store.db mtime as a reliable fallback timestamp
|
||||
let dbStatMtimeMs = null;
|
||||
try {
|
||||
const stat = await fs.stat(storeDbPath);
|
||||
dbStatMtimeMs = stat.mtimeMs;
|
||||
} catch (_) {}
|
||||
|
||||
// Open SQLite database
|
||||
const db = await open({
|
||||
filename: storeDbPath,
|
||||
driver: sqlite3.Database,
|
||||
mode: sqlite3.OPEN_READONLY
|
||||
});
|
||||
|
||||
// Get metadata from meta table
|
||||
const metaRows = await db.all(`
|
||||
SELECT key, value FROM meta
|
||||
`);
|
||||
|
||||
// Parse metadata
|
||||
let metadata = {};
|
||||
for (const row of metaRows) {
|
||||
if (row.value) {
|
||||
try {
|
||||
// Try to decode as hex-encoded JSON
|
||||
const hexMatch = row.value.toString().match(/^[0-9a-fA-F]+$/);
|
||||
if (hexMatch) {
|
||||
const jsonStr = Buffer.from(row.value, 'hex').toString('utf8');
|
||||
metadata[row.key] = JSON.parse(jsonStr);
|
||||
} else {
|
||||
metadata[row.key] = row.value.toString();
|
||||
}
|
||||
} catch (e) {
|
||||
metadata[row.key] = row.value.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get message count
|
||||
const messageCountResult = await db.get(`
|
||||
SELECT COUNT(*) as count FROM blobs
|
||||
`);
|
||||
|
||||
await db.close();
|
||||
|
||||
// Extract session info
|
||||
const sessionName = metadata.title || metadata.sessionTitle || 'Untitled Session';
|
||||
|
||||
// Determine timestamp - prefer createdAt from metadata, fall back to db file mtime
|
||||
let createdAt = null;
|
||||
if (metadata.createdAt) {
|
||||
createdAt = new Date(metadata.createdAt).toISOString();
|
||||
} else if (dbStatMtimeMs) {
|
||||
createdAt = new Date(dbStatMtimeMs).toISOString();
|
||||
} else {
|
||||
createdAt = new Date().toISOString();
|
||||
}
|
||||
|
||||
sessions.push({
|
||||
id: sessionId,
|
||||
name: sessionName,
|
||||
createdAt: createdAt,
|
||||
lastActivity: createdAt, // For compatibility with Claude sessions
|
||||
messageCount: messageCountResult.count || 0,
|
||||
projectPath: projectPath
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`Could not read Cursor session ${sessionId}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort sessions by creation time (newest first)
|
||||
sessions.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
||||
|
||||
// Return only the first 5 sessions for performance
|
||||
return sessions.slice(0, 5);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error fetching Cursor sessions:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
getProjects,
|
||||
|
||||
@@ -622,15 +622,18 @@ router.get('/sessions/:sessionId', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blob data to extract messages
|
||||
// Parse blob data to extract messages - only include blobs with valid JSON
|
||||
const messages = [];
|
||||
for (const blob of blobs) {
|
||||
try {
|
||||
// Attempt direct JSON parse first
|
||||
const raw = blob.data.toString('utf8');
|
||||
let parsed;
|
||||
let isValidJson = false;
|
||||
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
isValidJson = true;
|
||||
} catch (_) {
|
||||
// If not JSON, try to extract JSON from within binary-looking string
|
||||
const cleaned = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '');
|
||||
@@ -640,20 +643,28 @@ router.get('/sessions/:sessionId', async (req, res) => {
|
||||
const jsonStr = cleaned.slice(start, end + 1);
|
||||
try {
|
||||
parsed = JSON.parse(jsonStr);
|
||||
isValidJson = true;
|
||||
} catch (_) {
|
||||
parsed = null;
|
||||
isValidJson = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (parsed) {
|
||||
|
||||
// Only include blobs that contain valid JSON data
|
||||
if (isValidJson && parsed) {
|
||||
// Filter out ONLY system messages at the server level
|
||||
// Check both direct role and nested message.role
|
||||
const role = parsed?.role || parsed?.message?.role;
|
||||
if (role === 'system') {
|
||||
continue; // Skip only system messages
|
||||
}
|
||||
messages.push({ id: blob.id, content: parsed });
|
||||
} else {
|
||||
// Fallback to cleaned text content
|
||||
const text = raw.replace(/[^\x09\x0A\x0D\x20-\x7E]/g, '').trim();
|
||||
messages.push({ id: blob.id, content: text });
|
||||
}
|
||||
// Skip non-JSON blobs (binary data) completely
|
||||
} catch (e) {
|
||||
messages.push({ id: blob.id, content: blob.data.toString() });
|
||||
// Skip blobs that cause errors
|
||||
console.log(`Skipping blob ${blob.id}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user