Skip to content

fix: sessions not loading on project navigation without restart - #137

Merged
matt1398 merged 1 commit into
matt1398:mainfrom
psypeal:fix/project-loading
Mar 30, 2026
Merged

matt1398 merged 1 commit into
matt1398:mainfrom
psypeal:fix/project-loading

Conversation

@psypeal

@psypeal psypeal commented Mar 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Sessions don't appear when navigating to a project until the app is restarted. Three root causes fixed:

  1. Renderer _sessionCache stale hit: selectProject() returns cached empty sessions from a previous visit. Fixed by clearing the cache entry after showing cached data so fetchSessionsInitial() always overwrites with fresh backend data.

  2. No external cache invalidation on ProjectScanner: contentPresenceCache, sessionMetadataCache, sessionPreviewCache are private Maps with no invalidation API. Added invalidateCachesForProject(projectId) and wired it into FileWatcher.processProjectsChange().

  3. activeSessionFiles never seeded at startup: Sessions existing at app startup aren't added to activeSessionFiles, so the catch-up timer never checks them. Added seedActiveSessionFiles() that scans recent .jsonl files (last hour) on startup.

Test plan

  • pnpm typecheck — no type errors
  • pnpm lint:fix — no lint errors
  • All 653 tests pass
  • Manual: start app, create a new Claude Code session, navigate to the project — sessions should appear immediately
  • Manual: verify existing session pagination still works

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Session data automatically refreshes when switching between projects, ensuring you always see current information
    • Application now detects previously active sessions on startup for quicker initialization
  • Bug Fixes

    • Enhanced synchronization of session data with file system changes to prevent serving stale information

Sessions don't appear when navigating to a project until the app is
restarted. Three root causes:

1. Renderer _sessionCache returns stale empty data on navigation —
   clear cache entry after showing cached data so fetchSessionsInitial
   always overwrites with fresh backend data

2. ProjectScanner internal caches (contentPresenceCache,
   sessionMetadataCache, sessionPreviewCache) have no external
   invalidation — add invalidateCachesForProject() and wire it into
   FileWatcher.processProjectsChange()

3. FileWatcher.activeSessionFiles never seeded at startup — add
   seedActiveSessionFiles() that scans recent .jsonl files so the
   catch-up timer can detect growth in pre-existing sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request resolves issues where project sessions were not consistently loading or appearing after navigating between projects or upon application startup. It introduces robust cache invalidation mechanisms across the renderer and main process, and ensures that active sessions are properly initialized when the application starts, leading to a more reliable and responsive user experience for session management.

Highlights

  • Renderer _sessionCache stale hit: Fixed by clearing the cache entry after showing cached data so fetchSessionsInitial() always overwrites with fresh backend data.
  • No external cache invalidation on ProjectScanner: Added invalidateCachesForProject(projectId) to ProjectScanner and wired it into FileWatcher.processProjectsChange() to invalidate contentPresenceCache, sessionMetadataCache, and sessionPreviewCache.
  • activeSessionFiles never seeded at startup: Added seedActiveSessionFiles() that scans recent .jsonl files (last hour) on startup to ensure existing sessions are added to activeSessionFiles.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@coderabbitai coderabbitai Bot added the bug Something isn't working label Mar 25, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new invalidateCachesForProject method in ProjectScanner to clear various caches for a given project ID. The FileWatcher is updated to utilize this new method for cache invalidation when session files change, and a new seedActiveSessionFiles method is added to pre-populate active session files for the catch-up scan. The ServiceContext now correctly links the FileWatcher and ProjectScanner. Additionally, the projectSlice in the renderer now invalidates its session cache when a project is deselected. A performance improvement opportunity was identified in the seedActiveSessionFiles method, suggesting parallelization of filesystem operations to avoid sequential I/O, especially for remote filesystems.

Comment on lines +833 to +879
private async seedActiveSessionFiles(): Promise<void> {
const now = Date.now();
try {
if (!(await this.fsProvider.exists(this.projectsPath))) {
return;
}

const projectDirs = await this.fsProvider.readdir(this.projectsPath);
for (const dir of projectDirs) {
if (!dir.isDirectory()) continue;

const projectPath = path.join(this.projectsPath, dir.name);
let entries: FsDirent[];
try {
entries = await this.fsProvider.readdir(projectPath);
} catch {
continue;
}

for (const entry of entries) {
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;

const fullPath = path.join(projectPath, entry.name);
try {
const stats = await this.fsProvider.stat(fullPath);
if (now - stats.mtimeMs <= CATCH_UP_MAX_AGE_MS) {
const sessionId = path.basename(entry.name, '.jsonl');
this.activeSessionFiles.set(fullPath, {
projectId: dir.name,
sessionId,
});
}
} catch {
continue;
}
}
}

if (this.activeSessionFiles.size > 0) {
logger.info(
`FileWatcher: Seeded ${this.activeSessionFiles.size} active session files`
);
}
} catch (err) {
logger.error('Error seeding active session files:', err);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of seedActiveSessionFiles uses nested loops with await inside, which results in sequential filesystem operations. This can be inefficient, especially for remote filesystems (SSH), if there are many projects and session files.

You can improve performance by parallelizing these operations using Promise.all. This would involve first collecting all file paths and then getting their stats in parallel. This change would also make the code flatter and potentially easier to reason about.

Here is an example of how you could refactor it:

private async seedActiveSessionFiles(): Promise<void> {
  const now = Date.now();
  try {
    if (!(await this.fsProvider.exists(this.projectsPath))) {
      return;
    }

    const projectDirs = await this.fsProvider.readdir(this.projectsPath);

    const allSessionFiles = (await Promise.all(
      projectDirs
        .filter((dir) => dir.isDirectory())
        .map(async (dir) => {
          const projectPath = path.join(this.projectsPath, dir.name);
          try {
            const entries = await this.fsProvider.readdir(projectPath);
            return entries
              .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
              .map((entry) => ({
                fullPath: path.join(projectPath, entry.name),
                dirName: dir.name,
                entryName: entry.name,
              }));
          } catch {
            return []; // Ignore projects that can't be read
          }
        })
    )).flat();

    await Promise.all(
      allSessionFiles.map(async (file) => {
        try {
          const stats = await this.fsProvider.stat(file.fullPath);
          if (now - stats.mtimeMs <= CATCH_UP_MAX_AGE_MS) {
            this.activeSessionFiles.set(file.fullPath, {
              projectId: file.dirName,
              sessionId: path.basename(file.entryName, '.jsonl'),
            });
          }
        } catch {
          // Ignore files that can't be stat-ed
        }
      })
    );

    if (this.activeSessionFiles.size > 0) {
      logger.info(
        `FileWatcher: Seeded ${this.activeSessionFiles.size} active session files`
      );
    }
  } catch (err) {
    logger.error('Error seeding active session files:', err);
  }
}

@coderabbitai

coderabbitai Bot commented Mar 25, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR adds cache invalidation capabilities to ProjectScanner and integrates it with FileWatcher to automatically clear stale session/file presence caches when projects are modified, while also seeding active session files on startup and ensuring the renderer discards cached session data when projects are re-selected.

Changes

Cohort / File(s) Summary
Cache Invalidation Infrastructure
src/main/services/discovery/ProjectScanner.ts, src/main/services/infrastructure/FileWatcher.ts
Added invalidateCachesForProject() method to ProjectScanner that clears content presence, session metadata, and session preview caches for a project. FileWatcher now accepts a ProjectScanner, seeds activeSessionFiles with recently modified session files on start, and calls cache invalidation when processing project session file events.
Service Integration
src/main/services/infrastructure/ServiceContext.ts
Wires projectScanner into fileWatcher during ServiceContext construction.
Renderer Cache Cleanup
src/renderer/store/slices/projectSlice.ts
Deletes _sessionCache entry after populating session state to force subsequent fetches to retrieve fresh data instead of stale cache.

Possibly related PRs

Suggested labels

bug


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/services/discovery/ProjectScanner.ts`:
- Around line 1057-1069: invalidateCachesForProject currently decodes projectId
with decodeURIComponent and uses startsWith(prefix) which can both mis-decode
composite IDs and over-match sibling project paths; replace
decodeURIComponent(projectId) with extractBaseDir(projectId) to get the correct
directory name, compute prefix = path.join(this.projectsDir, baseDir), and
change the cache eviction checks for contentPresenceCache, sessionMetadataCache,
and sessionPreviewCache to only delete keys that are exactly the prefix or start
with prefix + path.sep (i.e., key === prefix || key.startsWith(prefix +
path.sep)) to avoid invalidating sibling projects.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1bf1f30-a42a-4606-aaa2-f9939e694477

📥 Commits

Reviewing files that changed from the base of the PR and between f23e881 and 818fe19.

📒 Files selected for processing (4)
  • src/main/services/discovery/ProjectScanner.ts
  • src/main/services/infrastructure/FileWatcher.ts
  • src/main/services/infrastructure/ServiceContext.ts
  • src/renderer/store/slices/projectSlice.ts

Comment on lines +1057 to +1069
invalidateCachesForProject(projectId: string): void {
// projectId is URL-encoded; the cache keys are absolute file paths containing the decoded dir
const decoded = decodeURIComponent(projectId);
const prefix = path.join(this.projectsDir, decoded);
for (const key of this.contentPresenceCache.keys()) {
if (key.startsWith(prefix)) this.contentPresenceCache.delete(key);
}
for (const key of this.sessionMetadataCache.keys()) {
if (key.startsWith(prefix)) this.sessionMetadataCache.delete(key);
}
for (const key of this.sessionPreviewCache.keys()) {
if (key.startsWith(prefix)) this.sessionPreviewCache.delete(key);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify project-id encoding and invalidation assumptions.
rg -n -C3 "extractBaseDir|isValidEncodedPath|decodeURIComponent|invalidateCachesForProject" \
  src/main/utils/pathDecoder.ts \
  src/main/services/discovery/ProjectScanner.ts \
  src/main/services/infrastructure/FileWatcher.ts

Repository: matt1398/claude-devtools

Length of output: 10043


🏁 Script executed:

# Read pathDecoder.ts to understand encoding scheme
head -250 src/main/utils/pathDecoder.ts | tail -100

Repository: matt1398/claude-devtools

Length of output: 3091


🏁 Script executed:

# Look at the beginning of pathDecoder.ts to see the encoding/decoding functions
head -120 src/main/utils/pathDecoder.ts

Repository: matt1398/claude-devtools

Length of output: 4213


🏁 Script executed:

# Search for other uses of decodeURIComponent in the codebase
rg -n "decodeURIComponent" src/

Repository: matt1398/claude-devtools

Length of output: 169


🏁 Script executed:

# Check if there are any other encoding/decoding utilities we should be aware of
rg -n "decodePath|encodePath" src/main/services/discovery/ProjectScanner.ts

Repository: matt1398/claude-devtools

Length of output: 50


invalidateCachesForProject() uses wrong decoding function and can over-match cache keys.

On Line 1059, decodeURIComponent(projectId) is incorrect because project IDs use slash-to-dash encoding (e.g., /Users/name/project → -Users-name-project), not URI-encoding. Use extractBaseDir() instead, which is already imported and handles both plain and composite IDs. Also, startsWith(prefix) on Lines 1062/1065/1068 can invalidate sibling projects with shared prefixes (e.g., /foo matches /foo-bar).

💡 Proposed fix
   invalidateCachesForProject(projectId: string): void {
-    // projectId is URL-encoded; the cache keys are absolute file paths containing the decoded dir
-    const decoded = decodeURIComponent(projectId);
-    const prefix = path.join(this.projectsDir, decoded);
-    for (const key of this.contentPresenceCache.keys()) {
-      if (key.startsWith(prefix)) this.contentPresenceCache.delete(key);
-    }
-    for (const key of this.sessionMetadataCache.keys()) {
-      if (key.startsWith(prefix)) this.sessionMetadataCache.delete(key);
-    }
-    for (const key of this.sessionPreviewCache.keys()) {
-      if (key.startsWith(prefix)) this.sessionPreviewCache.delete(key);
-    }
+    const baseDir = extractBaseDir(projectId);
+    const projectRoot = path.join(this.projectsDir, baseDir);
+    const projectPrefix = `${projectRoot}${path.sep}`;
+
+    const invalidate = <T>(cache: Map<string, T>): void => {
+      for (const key of cache.keys()) {
+        const normalizedKey = path.normalize(key);
+        if (normalizedKey === projectRoot || normalizedKey.startsWith(projectPrefix)) {
+          cache.delete(key);
+        }
+      }
+    };
+
+    invalidate(this.contentPresenceCache);
+    invalidate(this.sessionMetadataCache);
+    invalidate(this.sessionPreviewCache);
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
invalidateCachesForProject(projectId: string): void {
// projectId is URL-encoded; the cache keys are absolute file paths containing the decoded dir
const decoded = decodeURIComponent(projectId);
const prefix = path.join(this.projectsDir, decoded);
for (const key of this.contentPresenceCache.keys()) {
if (key.startsWith(prefix)) this.contentPresenceCache.delete(key);
}
for (const key of this.sessionMetadataCache.keys()) {
if (key.startsWith(prefix)) this.sessionMetadataCache.delete(key);
}
for (const key of this.sessionPreviewCache.keys()) {
if (key.startsWith(prefix)) this.sessionPreviewCache.delete(key);
}
invalidateCachesForProject(projectId: string): void {
const baseDir = extractBaseDir(projectId);
const projectRoot = path.join(this.projectsDir, baseDir);
const projectPrefix = `${projectRoot}${path.sep}`;
const invalidate = <T>(cache: Map<string, T>): void => {
for (const key of cache.keys()) {
const normalizedKey = path.normalize(key);
if (normalizedKey === projectRoot || normalizedKey.startsWith(projectPrefix)) {
cache.delete(key);
}
}
};
invalidate(this.contentPresenceCache);
invalidate(this.sessionMetadataCache);
invalidate(this.sessionPreviewCache);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/services/discovery/ProjectScanner.ts` around lines 1057 - 1069,
invalidateCachesForProject currently decodes projectId with decodeURIComponent
and uses startsWith(prefix) which can both mis-decode composite IDs and
over-match sibling project paths; replace decodeURIComponent(projectId) with
extractBaseDir(projectId) to get the correct directory name, compute prefix =
path.join(this.projectsDir, baseDir), and change the cache eviction checks for
contentPresenceCache, sessionMetadataCache, and sessionPreviewCache to only
delete keys that are exactly the prefix or start with prefix + path.sep (i.e.,
key === prefix || key.startsWith(prefix + path.sep)) to avoid invalidating
sibling projects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants