@@ -21,9 +21,9 @@ async function saveProjectConfig(config) {
21
21
}
22
22
23
23
// 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, '/' ) ;
27
27
28
28
// Try to read package.json from the project path
29
29
try {
@@ -54,6 +54,91 @@ async function generateDisplayName(projectName) {
54
54
return projectPath ;
55
55
}
56
56
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
+
57
142
async function getProjects ( ) {
58
143
const claudeDir = path . join ( process . env . HOME , '.claude' , 'projects' ) ;
59
144
const config = await loadProjectConfig ( ) ;
@@ -69,14 +154,17 @@ async function getProjects() {
69
154
existingProjects . add ( entry . name ) ;
70
155
const projectPath = path . join ( claudeDir , entry . name ) ;
71
156
157
+ // Extract actual project directory from JSONL sessions
158
+ const actualProjectDir = await extractProjectDirectory ( entry . name ) ;
159
+
72
160
// Get display name from config or generate one
73
161
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 ;
76
164
77
165
const project = {
78
166
name : entry . name ,
79
- path : projectPath ,
167
+ path : actualProjectDir ,
80
168
displayName : customName || autoDisplayName ,
81
169
fullPath : fullPath ,
82
170
isCustomName : ! ! customName ,
@@ -105,17 +193,27 @@ async function getProjects() {
105
193
// Add manually configured projects that don't exist as folders yet
106
194
for ( const [ projectName , projectConfig ] of Object . entries ( config ) ) {
107
195
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 ;
109
198
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
+ } ;
119
217
120
218
projects . push ( project ) ;
121
219
}
@@ -463,9 +561,9 @@ async function addProjectManually(projectPath, displayName = null) {
463
561
464
562
return {
465
563
name : projectName ,
466
- path : null ,
564
+ path : absolutePath ,
467
565
fullPath : absolutePath ,
468
- displayName : displayName || await generateDisplayName ( projectName ) ,
566
+ displayName : displayName || await generateDisplayName ( projectName , absolutePath ) ,
469
567
isManuallyAdded : true ,
470
568
sessions : [ ]
471
569
} ;
@@ -483,5 +581,6 @@ module.exports = {
483
581
deleteProject,
484
582
addProjectManually,
485
583
loadProjectConfig,
486
- saveProjectConfig
584
+ saveProjectConfig,
585
+ extractProjectDirectory
487
586
} ;
0 commit comments