Skip to content

fix: enable Gemini CLI session deletion and UI state update#598

Open
shrewdact wants to merge 1 commit intositeboon:mainfrom
shrewdact:fix/gemini-session-delete
Open

fix: enable Gemini CLI session deletion and UI state update#598
shrewdact wants to merge 1 commit intositeboon:mainfrom
shrewdact:fix/gemini-session-delete

Conversation

@shrewdact
Copy link
Copy Markdown

@shrewdact shrewdact commented Mar 29, 2026

Summary

  • Add deleteGeminiCliSession() to delete CLI sessions from ~/.gemini/tmp/chats/
  • Update the Gemini delete endpoint to try both UI sessions and CLI sessions
  • Fix handleSessionDelete to also filter geminiSessions and codexSessions so the UI updates immediately without requiring a page refresh

Test plan

  • Delete a Gemini CLI session and verify it disappears from the sidebar immediately
  • Delete a Codex session and verify it also disappears immediately
  • Verify Claude session deletion still works as before
  • Refresh the page after deletion and confirm the session stays deleted

🤖 Generated with Claude Code

Summary by CodeRabbit

Bug Fixes

  • Fixed an issue where session deletion was incomplete, potentially leaving orphaned data in storage. Sessions are now comprehensively removed, including all associated CLI session files, database records, and metadata. This ensures complete cleanup across all storage locations and prevents stale sessions from persisting in the system.

The delete endpoint only handled UI sessions (~/.gemini/sessions/) but
not CLI sessions (~/.gemini/tmp/chats/). Also, handleSessionDelete only
filtered project.sessions, leaving geminiSessions and codexSessions
untouched so the UI didn't reflect the deletion without a refresh.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 29, 2026

📝 Walkthrough

Walkthrough

The PR adds functionality to clean up Gemini CLI session files when sessions are deleted through the UI. A new deletion function scans and removes matching CLI chat files from disk, integrates into the session deletion route handler with error suppression, and ensures frontend state lists stay synchronized during deletion.

Changes

Cohort / File(s) Summary
Gemini CLI Session Deletion
server/projects.js
Added deleteGeminiCliSession(sessionId) function that locates and deletes the corresponding Gemini CLI chat JSON file from ~/.gemini/tmp/<project>/chats/. Throws errors if the directory is unreadable or no matching file is found.
Session Deletion Route Handler
server/routes/gemini.js
Integrated the new CLI deletion function into DELETE /sessions/:sessionId handler. Now attempts two-phase deletion: first the UI session via sessionManager.deleteSession(), then the CLI session file. Both deletions wrapped in independent try/catch blocks with suppressed errors.
Frontend State Synchronization
src/hooks/useProjectsState.ts
Updated handleSessionDelete() to remove deleted sessions from project.codexSessions and project.geminiSessions arrays in addition to project.sessions, preventing stale data.

Possibly related PRs

Suggested reviewers

  • viper151
  • blackmammoth

Poem

🐰 A whisker-twitch, a clever deed,
To sweep the sessions, yes indeed!
Files begone from CLI's nest,
Three layers clean—now put to rest!
Delete with grace, sync with care,

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: enable Gemini CLI session deletion and UI state update' clearly summarizes the main changes: adding Gemini CLI session deletion functionality and fixing the UI state to update immediately after deletion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/useProjectsState.ts (1)

435-443: ⚠️ Potential issue | 🟠 Major

Keep sessionMeta.total tied to Claude deletions only.

This callback now handles Codex/Gemini removals too, but sessionMeta.total still represents the Claude project.sessions count. Decrementing it here for every project means one external-session delete can make Claude totals drift until the next refresh.

🩹 Suggested guard
       setProjects((prevProjects) =>
-        prevProjects.map((project) => ({
-          ...project,
-          sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
-          codexSessions: project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
-          geminiSessions: project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
-          sessionMeta: {
-            ...project.sessionMeta,
-            total: Math.max(0, (project.sessionMeta?.total as number | undefined ?? 0) - 1),
-          },
-        })),
+        prevProjects.map((project) => {
+          const removedClaudeSession =
+            project.sessions?.some((session) => session.id === sessionIdToDelete) ?? false;
+
+          return {
+            ...project,
+            sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
+            codexSessions: project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
+            geminiSessions: project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
+            sessionMeta: removedClaudeSession
+              ? {
+                  ...project.sessionMeta,
+                  total: Math.max(0, (((project.sessionMeta?.total as number | undefined) ?? 0) - 1)),
+                }
+              : project.sessionMeta,
+          };
+        }),
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useProjectsState.ts` around lines 435 - 443, The current updater in
setProjects unconditionally decrements project.sessionMeta.total for any
sessionIdToDelete even when the deleted session was a Codex/Gemini session;
change the logic so you only decrement sessionMeta.total when the deleted id was
present in the Claude sessions array (project.sessions). Specifically, inside
the setProjects mapper (the function updating project), compute a boolean like
"wasClaude = project.sessions?.some(s => s.id === sessionIdToDelete)" and only
apply Math.max(0, (project.sessionMeta?.total ?? 0) - 1) when wasClaude is true;
otherwise keep project.sessionMeta.total unchanged. Keep the existing filtering
of project.sessions, project.codexSessions, and project.geminiSessions as-is and
reference setProjects, project.sessionMeta, project.sessions,
project.codexSessions, project.geminiSessions, and sessionIdToDelete when making
the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@server/projects.js`:
- Around line 2544-2576: Narrow the broad try/catch blocks so real FS errors
aren't masked as "not found": where you call fs.readdir(geminiTmpDir) and
fs.readdir(chatsDir) inspect the caught error (e.g. err.code) and only convert
ENOENT (missing dir) into the session-not-found/continue behavior; rethrow any
other errors. Likewise, in the per-file try that reads/JSON-parses and calls
fs.unlink(filePath), do not swallow non-ENOENT errors—if
fs.readFile/JSON.parse/fs.unlink throw anything other than a missing-file error,
rethrow it so actual delete/read failures surface instead of falling through to
the final `Gemini CLI session file not found` error; reference
variables/functions: geminiTmpDir, projectDirs, chatsDir, fs.readdir,
fs.readFile, fs.unlink, sessionId.

In `@server/routes/gemini.js`:
- Around line 16-29: The current empty catch blocks around
sessionManager.deleteSession(sessionId) and deleteGeminiCliSession(sessionId)
hide real failures; change them to only swallow explicit "not found" errors and
rethrow everything else (e.g. check err.code === 'ENOENT' or err.name/err
instanceof NotFoundError), and ensure sessionNamesDb.deleteName(sessionId,
'gemini') is only reached after both delete attempts either succeeded or threw
not-found (so permission/disk errors from either delete will bubble as 500s);
use the function names sessionManager.deleteSession and deleteGeminiCliSession
and the sessionId variable to locate and update the handlers.

---

Outside diff comments:
In `@src/hooks/useProjectsState.ts`:
- Around line 435-443: The current updater in setProjects unconditionally
decrements project.sessionMeta.total for any sessionIdToDelete even when the
deleted session was a Codex/Gemini session; change the logic so you only
decrement sessionMeta.total when the deleted id was present in the Claude
sessions array (project.sessions). Specifically, inside the setProjects mapper
(the function updating project), compute a boolean like "wasClaude =
project.sessions?.some(s => s.id === sessionIdToDelete)" and only apply
Math.max(0, (project.sessionMeta?.total ?? 0) - 1) when wasClaude is true;
otherwise keep project.sessionMeta.total unchanged. Keep the existing filtering
of project.sessions, project.codexSessions, and project.geminiSessions as-is and
reference setProjects, project.sessionMeta, project.sessions,
project.codexSessions, project.geminiSessions, and sessionIdToDelete when making
the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: fee446e6-c98f-4b84-b72f-35dcc5a58b7f

📥 Commits

Reviewing files that changed from the base of the PR and between 051a6b1 and aa7a9d7.

📒 Files selected for processing (3)
  • server/projects.js
  • server/routes/gemini.js
  • src/hooks/useProjectsState.ts

Comment on lines +2544 to +2576
try {
projectDirs = await fs.readdir(geminiTmpDir);
} catch {
throw new Error(`Gemini CLI session not found: ${sessionId}`);
}

for (const projectDir of projectDirs) {
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
let chatFiles;
try {
chatFiles = await fs.readdir(chatsDir);
} catch {
continue;
}

for (const chatFile of chatFiles) {
if (!chatFile.endsWith('.json')) continue;
try {
const filePath = path.join(chatsDir, chatFile);
const data = await fs.readFile(filePath, 'utf8');
const session = JSON.parse(data);
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
if (fileSessionId === sessionId) {
await fs.unlink(filePath);
return true;
}
} catch {
continue;
}
}
}

throw new Error(`Gemini CLI session file not found: ${sessionId}`);
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.

⚠️ Potential issue | 🟡 Minor

Don't turn real filesystem failures into a fake "not found".

The broad catches here also swallow readdir()/unlink() failures. If the matching file is found but Line 2567 fails, this helper falls through to Gemini CLI session file not found, which hides the real delete failure.

🩹 Suggested narrowing
   let projectDirs;
   try {
     projectDirs = await fs.readdir(geminiTmpDir);
-  } catch {
-    throw new Error(`Gemini CLI session not found: ${sessionId}`);
+  } catch (error) {
+    if (error.code === 'ENOENT') {
+      throw new Error(`Gemini CLI session not found: ${sessionId}`);
+    }
+    throw error;
   }
@@
     for (const chatFile of chatFiles) {
       if (!chatFile.endsWith('.json')) continue;
-      try {
-        const filePath = path.join(chatsDir, chatFile);
-        const data = await fs.readFile(filePath, 'utf8');
-        const session = JSON.parse(data);
-        const fileSessionId = session.sessionId || chatFile.replace('.json', '');
-        if (fileSessionId === sessionId) {
-          await fs.unlink(filePath);
-          return true;
-        }
-      } catch {
+      const filePath = path.join(chatsDir, chatFile);
+      let session;
+      try {
+        session = JSON.parse(await fs.readFile(filePath, 'utf8'));
+      } catch {
         continue;
       }
+
+      const fileSessionId = session.sessionId || chatFile.replace('.json', '');
+      if (fileSessionId !== sessionId) {
+        continue;
+      }
+
+      await fs.unlink(filePath);
+      return true;
     }
   }
📝 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
try {
projectDirs = await fs.readdir(geminiTmpDir);
} catch {
throw new Error(`Gemini CLI session not found: ${sessionId}`);
}
for (const projectDir of projectDirs) {
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
let chatFiles;
try {
chatFiles = await fs.readdir(chatsDir);
} catch {
continue;
}
for (const chatFile of chatFiles) {
if (!chatFile.endsWith('.json')) continue;
try {
const filePath = path.join(chatsDir, chatFile);
const data = await fs.readFile(filePath, 'utf8');
const session = JSON.parse(data);
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
if (fileSessionId === sessionId) {
await fs.unlink(filePath);
return true;
}
} catch {
continue;
}
}
}
throw new Error(`Gemini CLI session file not found: ${sessionId}`);
let projectDirs;
try {
projectDirs = await fs.readdir(geminiTmpDir);
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`Gemini CLI session not found: ${sessionId}`);
}
throw error;
}
for (const projectDir of projectDirs) {
const chatsDir = path.join(geminiTmpDir, projectDir, 'chats');
let chatFiles;
try {
chatFiles = await fs.readdir(chatsDir);
} catch {
continue;
}
for (const chatFile of chatFiles) {
if (!chatFile.endsWith('.json')) continue;
const filePath = path.join(chatsDir, chatFile);
let session;
try {
session = JSON.parse(await fs.readFile(filePath, 'utf8'));
} catch {
continue;
}
const fileSessionId = session.sessionId || chatFile.replace('.json', '');
if (fileSessionId !== sessionId) {
continue;
}
await fs.unlink(filePath);
return true;
}
}
throw new Error(`Gemini CLI session file not found: ${sessionId}`);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/projects.js` around lines 2544 - 2576, Narrow the broad try/catch
blocks so real FS errors aren't masked as "not found": where you call
fs.readdir(geminiTmpDir) and fs.readdir(chatsDir) inspect the caught error (e.g.
err.code) and only convert ENOENT (missing dir) into the
session-not-found/continue behavior; rethrow any other errors. Likewise, in the
per-file try that reads/JSON-parses and calls fs.unlink(filePath), do not
swallow non-ENOENT errors—if fs.readFile/JSON.parse/fs.unlink throw anything
other than a missing-file error, rethrow it so actual delete/read failures
surface instead of falling through to the final `Gemini CLI session file not
found` error; reference variables/functions: geminiTmpDir, projectDirs,
chatsDir, fs.readdir, fs.readFile, fs.unlink, sessionId.

Comment on lines +16 to 29
// Try deleting from UI sessions and CLI sessions
let deleted = false;
try {
await sessionManager.deleteSession(sessionId);
deleted = true;
} catch { }

try {
await deleteGeminiCliSession(sessionId);
deleted = true;
} catch { }

sessionNamesDb.deleteName(sessionId, 'gemini');
res.json({ success: true });
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.

⚠️ Potential issue | 🟠 Major

Only suppress explicit "not found" cases here.

Both catch {} blocks hide real delete failures, and sessionManager.deleteSession() does not tell you whether anything actually existed. That means Line 29 can still return { success: true } and Line 28 can clear the custom name even when the CLI file could not be removed.

If you want DELETE to stay idempotent, keep the success response for true misses, but only swallow the helper's not-found case. Permission/disk errors should still bubble as 500s.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/routes/gemini.js` around lines 16 - 29, The current empty catch blocks
around sessionManager.deleteSession(sessionId) and
deleteGeminiCliSession(sessionId) hide real failures; change them to only
swallow explicit "not found" errors and rethrow everything else (e.g. check
err.code === 'ENOENT' or err.name/err instanceof NotFoundError), and ensure
sessionNamesDb.deleteName(sessionId, 'gemini') is only reached after both delete
attempts either succeeded or threw not-found (so permission/disk errors from
either delete will bubble as 500s); use the function names
sessionManager.deleteSession and deleteGeminiCliSession and the sessionId
variable to locate and update the handlers.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant