Skip to content

Commit c5e3bd0

Browse files
committed
Enhance project directory handling by adding extractProjectDirectory function. Update generateDisplayName to utilize actual project directory when available. Adjust getProjects and addProjectManually to incorporate new directory extraction logic for improved project path resolution.
1 parent 27f34db commit c5e3bd0

File tree

1 file changed

+118
-19
lines changed

1 file changed

+118
-19
lines changed

server/projects.js

Lines changed: 118 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ async function saveProjectConfig(config) {
2121
}
2222

2323
// Generate better display name from path
24-
async function generateDisplayName(projectName) {
25-
// Convert "-home-user-projects-myapp" to a readable format
26-
let projectPath = projectName.replace(/-/g, '/');
24+
async function generateDisplayName(projectName, actualProjectDir = null) {
25+
// Use actual project directory if provided, otherwise decode from project name
26+
let projectPath = actualProjectDir || projectName.replace(/-/g, '/');
2727

2828
// Try to read package.json from the project path
2929
try {
@@ -54,6 +54,91 @@ async function generateDisplayName(projectName) {
5454
return projectPath;
5555
}
5656

57+
// Extract the actual project directory from JSONL sessions
58+
async function extractProjectDirectory(projectName) {
59+
const projectDir = path.join(process.env.HOME, '.claude', 'projects', projectName);
60+
const cwdCounts = new Map();
61+
let latestTimestamp = 0;
62+
let latestCwd = null;
63+
64+
try {
65+
const files = await fs.readdir(projectDir);
66+
const jsonlFiles = files.filter(file => file.endsWith('.jsonl'));
67+
68+
if (jsonlFiles.length === 0) {
69+
// Fall back to decoded project name if no sessions
70+
return projectName.replace(/-/g, '/');
71+
}
72+
73+
// Process all JSONL files to collect cwd values
74+
for (const file of jsonlFiles) {
75+
const jsonlFile = path.join(projectDir, file);
76+
const fileStream = require('fs').createReadStream(jsonlFile);
77+
const rl = readline.createInterface({
78+
input: fileStream,
79+
crlfDelay: Infinity
80+
});
81+
82+
for await (const line of rl) {
83+
if (line.trim()) {
84+
try {
85+
const entry = JSON.parse(line);
86+
87+
if (entry.cwd) {
88+
// Count occurrences of each cwd
89+
cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) || 0) + 1);
90+
91+
// Track the most recent cwd
92+
const timestamp = new Date(entry.timestamp || 0).getTime();
93+
if (timestamp > latestTimestamp) {
94+
latestTimestamp = timestamp;
95+
latestCwd = entry.cwd;
96+
}
97+
}
98+
} catch (parseError) {
99+
// Skip malformed lines
100+
}
101+
}
102+
}
103+
}
104+
105+
// Determine the best cwd to use
106+
if (cwdCounts.size === 0) {
107+
// No cwd found, fall back to decoded project name
108+
return projectName.replace(/-/g, '/');
109+
}
110+
111+
if (cwdCounts.size === 1) {
112+
// Only one cwd, use it
113+
return Array.from(cwdCounts.keys())[0];
114+
}
115+
116+
// Multiple cwd values - prefer the most recent one if it has reasonable usage
117+
const mostRecentCount = cwdCounts.get(latestCwd) || 0;
118+
const maxCount = Math.max(...cwdCounts.values());
119+
120+
// Use most recent if it has at least 25% of the max count
121+
if (mostRecentCount >= maxCount * 0.25) {
122+
return latestCwd;
123+
}
124+
125+
// Otherwise use the most frequently used cwd
126+
for (const [cwd, count] of cwdCounts.entries()) {
127+
if (count === maxCount) {
128+
return cwd;
129+
}
130+
}
131+
132+
// Fallback (shouldn't reach here)
133+
return latestCwd || projectName.replace(/-/g, '/');
134+
135+
} catch (error) {
136+
console.error(`Error extracting project directory for ${projectName}:`, error);
137+
// Fall back to decoded project name
138+
return projectName.replace(/-/g, '/');
139+
}
140+
}
141+
57142
async function getProjects() {
58143
const claudeDir = path.join(process.env.HOME, '.claude', 'projects');
59144
const config = await loadProjectConfig();
@@ -69,14 +154,17 @@ async function getProjects() {
69154
existingProjects.add(entry.name);
70155
const projectPath = path.join(claudeDir, entry.name);
71156

157+
// Extract actual project directory from JSONL sessions
158+
const actualProjectDir = await extractProjectDirectory(entry.name);
159+
72160
// Get display name from config or generate one
73161
const customName = config[entry.name]?.displayName;
74-
const autoDisplayName = await generateDisplayName(entry.name);
75-
const fullPath = entry.name.replace(/-/g, '/');
162+
const autoDisplayName = await generateDisplayName(entry.name, actualProjectDir);
163+
const fullPath = actualProjectDir;
76164

77165
const project = {
78166
name: entry.name,
79-
path: projectPath,
167+
path: actualProjectDir,
80168
displayName: customName || autoDisplayName,
81169
fullPath: fullPath,
82170
isCustomName: !!customName,
@@ -105,17 +193,27 @@ async function getProjects() {
105193
// Add manually configured projects that don't exist as folders yet
106194
for (const [projectName, projectConfig] of Object.entries(config)) {
107195
if (!existingProjects.has(projectName) && projectConfig.manuallyAdded) {
108-
const fullPath = projectName.replace(/-/g, '/');
196+
// Use the original path if available, otherwise extract from potential sessions
197+
let actualProjectDir = projectConfig.originalPath;
109198

110-
const project = {
111-
name: projectName,
112-
path: null, // No physical path yet
113-
displayName: projectConfig.displayName || await generateDisplayName(projectName),
114-
fullPath: fullPath,
115-
isCustomName: !!projectConfig.displayName,
116-
isManuallyAdded: true,
117-
sessions: []
118-
};
199+
if (!actualProjectDir) {
200+
try {
201+
actualProjectDir = await extractProjectDirectory(projectName);
202+
} catch (error) {
203+
// Fall back to decoded project name
204+
actualProjectDir = projectName.replace(/-/g, '/');
205+
}
206+
}
207+
208+
const project = {
209+
name: projectName,
210+
path: actualProjectDir,
211+
displayName: projectConfig.displayName || await generateDisplayName(projectName, actualProjectDir),
212+
fullPath: actualProjectDir,
213+
isCustomName: !!projectConfig.displayName,
214+
isManuallyAdded: true,
215+
sessions: []
216+
};
119217

120218
projects.push(project);
121219
}
@@ -463,9 +561,9 @@ async function addProjectManually(projectPath, displayName = null) {
463561

464562
return {
465563
name: projectName,
466-
path: null,
564+
path: absolutePath,
467565
fullPath: absolutePath,
468-
displayName: displayName || await generateDisplayName(projectName),
566+
displayName: displayName || await generateDisplayName(projectName, absolutePath),
469567
isManuallyAdded: true,
470568
sessions: []
471569
};
@@ -483,5 +581,6 @@ module.exports = {
483581
deleteProject,
484582
addProjectManually,
485583
loadProjectConfig,
486-
saveProjectConfig
584+
saveProjectConfig,
585+
extractProjectDirectory
487586
};

0 commit comments

Comments
 (0)