@@ -227,102 +227,103 @@ def get_claude_version() -> str:
227227 return "unknown"
228228
229229
230- def list_claude_sessions (working_dir : Path ) -> list [dict ]:
231- """List all available Claude Code sessions across all projects.
230+ def list_claude_sessions (working_dir : Path , limit : int = 50 ) -> list [dict ]:
231+ """List available Claude Code sessions across all projects.
232232
233- Scans all project directories in ~/.claude/projects/ to find sessions.
233+ Scans project directories in ~/.claude/projects/ to find sessions.
234234 Returns sessions sorted by last modified time (most recent first).
235+ Limited to most recent sessions for performance.
236+
237+ Args:
238+ working_dir: The working directory (used for context)
239+ limit: Maximum number of sessions to return (default 50)
235240 """
236241 claude_dir = Path .home () / ".claude" / "projects"
237242 if not claude_dir .exists ():
238243 return []
239244
240- sessions = []
245+ # First, collect all session files with their mod times (fast - just stat)
246+ session_files = []
241247
242- # Scan ALL project directories, not just the working_dir one
243248 for project_dir in claude_dir .iterdir ():
244249 if not project_dir .is_dir ():
245250 continue
246251
247- # The directory name is the encoded path (e.g., -Users-0xdeadbeef-hakc-dev)
248252 encoded_path = project_dir .name
249-
250- # Decode to actual filesystem path for cd'ing into
251253 actual_path = _decode_claude_project_path (encoded_path )
252254
253255 for session_file in project_dir .glob ("*.jsonl" ):
254256 # Skip agent sessions (subagent files)
255257 if session_file .stem .startswith ("agent-" ):
256258 continue
257259
258- # Skip empty files
259260 try :
260261 stat = session_file .stat ()
261262 if stat .st_size == 0 :
262263 continue
264+ session_files .append ((session_file , stat , project_dir , actual_path ))
263265 except (IOError , OSError ):
264266 continue
265267
266- try :
267- # Try to find actual user text for preview
268- preview = ""
269- session_id = session_file .stem
270-
271- # Check if there's a subdirectory with same name (active session)
272- has_subdir = (project_dir / session_id ).is_dir ()
273-
274- # Scan through lines to find real user messages (not IDE context)
275- with open (session_file , "r" ) as f :
276- lines_checked = 0
277- for line in f :
278- lines_checked += 1
279- if lines_checked > 100 : # Don't scan forever
280- break
281- try :
282- data = json .loads (line )
283- if data .get ("type" ) == "user" :
284- msg = data .get ("message" , {})
285- content = msg .get ("content" , [])
286- if isinstance (content , list ) and content :
287- for block in content :
288- if isinstance (block , dict ) and block .get ("type" ) == "text" :
289- text = block .get ("text" , "" ).strip ()
290- # Skip IDE/system messages that start with <
291- if not text .startswith ("<" ) and len (text ) > 5 :
292- # Clean up: take first line, truncate
293- first_line = text .split ("\n " )[0 ][:120 ]
294- preview = first_line
295- break
296- if preview :
297- break
298- except json .JSONDecodeError :
299- continue
268+ # Sort by modification time (newest first) and take only the limit
269+ session_files .sort (key = lambda x : x [1 ].st_mtime , reverse = True )
270+ session_files = session_files [:limit ]
300271
301- # Count messages for sync detection
302- message_count = 0
303- try :
304- with open (session_file , "r" ) as f :
305- for line in f :
306- try :
307- data = json .loads (line )
308- if data .get ("type" ) in ("user" , "assistant" ):
309- message_count += 1
310- except json .JSONDecodeError :
311- continue
312- except (IOError , OSError ):
313- pass
272+ sessions = []
314273
315- sessions .append ({
316- "sessionId" : session_id ,
317- "lastModified" : int (stat .st_mtime * 1000 ),
318- "size" : stat .st_size ,
319- "preview" : preview ,
320- "isActive" : has_subdir ,
321- "projectPath" : actual_path ,
322- "messageCount" : message_count , # For Whisper Sync
323- })
324- except (IOError , OSError ):
325- continue
274+ for session_file , stat , project_dir , actual_path in session_files :
275+ try :
276+ preview = ""
277+ message_count = 0
278+ session_id = session_file .stem
279+
280+ # Check if there's a subdirectory with same name (active session)
281+ has_subdir = (project_dir / session_id ).is_dir ()
282+
283+ # Single pass through file: find preview and count messages
284+ with open (session_file , "r" ) as f :
285+ lines_checked = 0
286+ for line in f :
287+ lines_checked += 1
288+ # Limit lines scanned for preview, but count all messages
289+ if lines_checked > 200 :
290+ # After 200 lines, just count without parsing
291+ message_count += line .count ('"type": "user"' ) + line .count ('"type": "assistant"' )
292+ continue
293+
294+ try :
295+ data = json .loads (line )
296+ msg_type = data .get ("type" )
297+
298+ # Count user/assistant messages
299+ if msg_type in ("user" , "assistant" ):
300+ message_count += 1
301+
302+ # Find preview (first real user message)
303+ if not preview and msg_type == "user" :
304+ msg = data .get ("message" , {})
305+ content = msg .get ("content" , [])
306+ if isinstance (content , list ) and content :
307+ for block in content :
308+ if isinstance (block , dict ) and block .get ("type" ) == "text" :
309+ text = block .get ("text" , "" ).strip ()
310+ if not text .startswith ("<" ) and len (text ) > 5 :
311+ preview = text .split ("\n " )[0 ][:120 ]
312+ break
313+ except json .JSONDecodeError :
314+ continue
315+
316+ sessions .append ({
317+ "sessionId" : session_id ,
318+ "lastModified" : int (stat .st_mtime * 1000 ),
319+ "size" : stat .st_size ,
320+ "preview" : preview ,
321+ "isActive" : has_subdir ,
322+ "projectPath" : actual_path ,
323+ "messageCount" : message_count , # For Whisper Sync
324+ })
325+ except (IOError , OSError ):
326+ continue
326327
327328 # Sort: active sessions first, then by last modified (most recent first)
328329 sessions .sort (key = lambda x : (not x ["isActive" ], - x ["lastModified" ]))
@@ -883,8 +884,12 @@ async def _start_claude_process(self, session: Session, resume_session_id: str =
883884 resume_session_id: Optional Claude session ID to resume (for handoffs)
884885 """
885886 try :
886- # Use validated working directory
887- working_dir = self .config .safe_working_dir
887+ # Use session's working directory if set (e.g., when resuming a session),
888+ # otherwise fall back to config's validated working directory
889+ if session .working_dir and session .working_dir .exists ():
890+ working_dir = session .working_dir
891+ else :
892+ working_dir = self .config .safe_working_dir
888893
889894 # Build command with streaming and permission handling
890895 cmd = [
0 commit comments