feat(cli): add --delete flag to /exit command for session deletion (#19332)

Co-authored-by: David Pierce <davidapierce@google.com>
This commit is contained in:
Abdul Tawab
2026-04-29 22:20:57 +05:00
committed by GitHub
parent 2cf0c75a04
commit 011c0f9bc0
9 changed files with 272 additions and 2 deletions
@@ -735,6 +735,62 @@ describe('ChatRecordingService', () => {
});
});
describe('deleteCurrentSessionAsync', () => {
it('should asynchronously delete the current session file and tool outputs', async () => {
await chatRecordingService.initialize();
// Record a message to trigger the file write (writeConversation skips
// writing when there are no messages).
chatRecordingService.recordMessage({
type: 'user',
content: 'test',
model: 'gemini-pro',
});
const conversationFile = chatRecordingService.getConversationFilePath();
expect(conversationFile).not.toBeNull();
// Create a tool output directory matching the session ID used by
// deleteSessionArtifactsAsync (this.sessionId = mockConfig.promptId).
const toolOutputDir = path.join(
testTempDir,
'tool-outputs',
'session-test-session-id',
);
fs.mkdirSync(toolOutputDir, { recursive: true });
fs.writeFileSync(path.join(toolOutputDir, 'output.txt'), 'data');
expect(fs.existsSync(conversationFile!)).toBe(true);
expect(fs.existsSync(toolOutputDir)).toBe(true);
await chatRecordingService.deleteCurrentSessionAsync();
expect(fs.existsSync(conversationFile!)).toBe(false);
expect(fs.existsSync(toolOutputDir)).toBe(false);
});
it('should not throw if the session was never initialized', async () => {
// conversationFile is null when not initialized
await expect(
chatRecordingService.deleteCurrentSessionAsync(),
).resolves.not.toThrow();
});
it('should not throw if session file does not exist on disk', async () => {
// initialize() writes an initial metadata record synchronously, so
// delete the file manually to simulate the "missing on disk" scenario.
await chatRecordingService.initialize();
const conversationFile = chatRecordingService.getConversationFilePath();
expect(conversationFile).not.toBeNull();
if (conversationFile && fs.existsSync(conversationFile)) {
fs.unlinkSync(conversationFile);
}
expect(fs.existsSync(conversationFile!)).toBe(false);
await expect(
chatRecordingService.deleteCurrentSessionAsync(),
).resolves.not.toThrow();
});
});
describe('recordDirectories', () => {
beforeEach(async () => {
await chatRecordingService.initialize();
@@ -792,6 +792,32 @@ export class ChatRecordingService {
}
}
/**
* Asynchronously deletes the current session's chat file and tool outputs.
* This encapsulates the session ID logic and uses non-blocking I/O to avoid
* blocking the event loop on exit.
*/
async deleteCurrentSessionAsync(): Promise<void> {
if (!this.conversationFile) {
return;
}
try {
const tempDir = this.context.config.storage.getProjectTempDir();
// Delete the conversation file directly using the tracked path.
await fs.promises.unlink(this.conversationFile).catch(() => {
// File may not exist; ignore.
});
// Delegate tool-output and log cleanup to the shared utility.
await deleteSessionArtifactsAsync(this.sessionId, tempDir);
} catch (error) {
debugLogger.error('Error deleting current session.', error);
throw error;
}
}
/**
* Rewinds the conversation to the state just before the specified message ID.
* All messages from (and including) the specified ID onwards are removed.