make tag required for /chat (#2904)

This commit is contained in:
Seth Troisi 2025-07-01 17:17:08 -07:00 committed by GitHub
parent 34935d6558
commit 38445f63f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 36 additions and 36 deletions

View File

@ -699,7 +699,7 @@ export const useSlashCommandProcessor = (
{ {
name: 'chat', name: 'chat',
description: description:
'Manage conversation history. Usage: /chat <list|save|resume> [tag]', 'Manage conversation history. Usage: /chat <list|save|resume> <tag>',
action: async (_mainCommand, subCommand, args) => { action: async (_mainCommand, subCommand, args) => {
const tag = (args || '').trim(); const tag = (args || '').trim();
const logger = new Logger(config?.getSessionId() || ''); const logger = new Logger(config?.getSessionId() || '');
@ -716,19 +716,27 @@ export const useSlashCommandProcessor = (
if (!subCommand) { if (!subCommand) {
addMessage({ addMessage({
type: MessageType.ERROR, type: MessageType.ERROR,
content: 'Missing command\nUsage: /chat <list|save|resume> [tag]', content: 'Missing command\nUsage: /chat <list|save|resume> <tag>',
timestamp: new Date(), timestamp: new Date(),
}); });
return; return;
} }
switch (subCommand) { switch (subCommand) {
case 'save': { case 'save': {
if (!tag) {
addMessage({
type: MessageType.ERROR,
content: 'Missing tag. Usage: /chat save <tag>',
timestamp: new Date(),
});
return;
}
const history = chat.getHistory(); const history = chat.getHistory();
if (history.length > 0) { if (history.length > 0) {
await logger.saveCheckpoint(chat?.getHistory() || [], tag); await logger.saveCheckpoint(chat?.getHistory() || [], tag);
addMessage({ addMessage({
type: MessageType.INFO, type: MessageType.INFO,
content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`, content: `Conversation checkpoint saved with tag: ${tag}.`,
timestamp: new Date(), timestamp: new Date(),
}); });
} else { } else {
@ -743,11 +751,19 @@ export const useSlashCommandProcessor = (
case 'resume': case 'resume':
case 'restore': case 'restore':
case 'load': { case 'load': {
if (!tag) {
addMessage({
type: MessageType.ERROR,
content: 'Missing tag. Usage: /chat resume <tag>',
timestamp: new Date(),
});
return;
}
const conversation = await logger.loadCheckpoint(tag); const conversation = await logger.loadCheckpoint(tag);
if (conversation.length === 0) { if (conversation.length === 0) {
addMessage({ addMessage({
type: MessageType.INFO, type: MessageType.INFO,
content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`, content: `No saved checkpoint found with tag: ${tag}.`,
timestamp: new Date(), timestamp: new Date(),
}); });
return; return;

View File

@ -393,12 +393,6 @@ describe('Logger', () => {
{ role: 'model', parts: [{ text: 'Hi there' }] }, { role: 'model', parts: [{ text: 'Hi there' }] },
]; ];
it('should save a checkpoint to the default file when no tag is provided', async () => {
await logger.saveCheckpoint(conversation);
const fileContent = await fs.readFile(TEST_CHECKPOINT_FILE_PATH, 'utf-8');
expect(JSON.parse(fileContent)).toEqual(conversation);
});
it('should save a checkpoint to a tagged file when a tag is provided', async () => { it('should save a checkpoint to a tagged file when a tag is provided', async () => {
const tag = 'my-test-tag'; const tag = 'my-test-tag';
await logger.saveCheckpoint(conversation, tag); await logger.saveCheckpoint(conversation, tag);
@ -418,7 +412,7 @@ describe('Logger', () => {
.mockImplementation(() => {}); .mockImplementation(() => {});
await expect( await expect(
uninitializedLogger.saveCheckpoint(conversation), uninitializedLogger.saveCheckpoint(conversation, 'tag'),
).resolves.not.toThrow(); ).resolves.not.toThrow();
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.', 'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
@ -439,11 +433,6 @@ describe('Logger', () => {
); );
}); });
it('should load from the default checkpoint file when no tag is provided', async () => {
const loaded = await logger.loadCheckpoint();
expect(loaded).toEqual(conversation);
});
it('should load from a tagged checkpoint file when a tag is provided', async () => { it('should load from a tagged checkpoint file when a tag is provided', async () => {
const tag = 'my-load-tag'; const tag = 'my-load-tag';
const taggedConversation = [ const taggedConversation = [
@ -468,9 +457,9 @@ describe('Logger', () => {
expect(loaded).toEqual([]); expect(loaded).toEqual([]);
}); });
it('should return an empty array if the default checkpoint file does not exist', async () => { it('should return an empty array if the checkpoint file does not exist', async () => {
await fs.unlink(TEST_CHECKPOINT_FILE_PATH); // Ensure it's gone await fs.unlink(TEST_CHECKPOINT_FILE_PATH); // Ensure it's gone
const loaded = await logger.loadCheckpoint(); const loaded = await logger.loadCheckpoint('missing');
expect(loaded).toEqual([]); expect(loaded).toEqual([]);
}); });
@ -479,11 +468,11 @@ describe('Logger', () => {
const consoleErrorSpy = vi const consoleErrorSpy = vi
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
const loadedCheckpoint = await logger.loadCheckpoint(); const loadedCheckpoint = await logger.loadCheckpoint('missing');
expect(loadedCheckpoint).toEqual([]); expect(loadedCheckpoint).toEqual([]);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('Failed to read or parse checkpoint file'), expect.stringContaining('Failed to read or parse checkpoint file'),
expect.any(SyntaxError), expect.any(Error),
); );
}); });
@ -493,7 +482,7 @@ describe('Logger', () => {
const consoleErrorSpy = vi const consoleErrorSpy = vi
.spyOn(console, 'error') .spyOn(console, 'error')
.mockImplementation(() => {}); .mockImplementation(() => {});
const loadedCheckpoint = await uninitializedLogger.loadCheckpoint(); const loadedCheckpoint = await uninitializedLogger.loadCheckpoint('tag');
expect(loadedCheckpoint).toEqual([]); expect(loadedCheckpoint).toEqual([]);
expect(consoleErrorSpy).toHaveBeenCalledWith( expect(consoleErrorSpy).toHaveBeenCalledWith(
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.', 'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',

View File

@ -10,7 +10,6 @@ import { Content } from '@google/genai';
import { getProjectTempDir } from '../utils/paths.js'; import { getProjectTempDir } from '../utils/paths.js';
const LOG_FILE_NAME = 'logs.json'; const LOG_FILE_NAME = 'logs.json';
const CHECKPOINT_FILE_NAME = 'checkpoint.json';
export enum MessageSenderType { export enum MessageSenderType {
USER = 'user', USER = 'user',
@ -27,7 +26,6 @@ export interface LogEntry {
export class Logger { export class Logger {
private geminiDir: string | undefined; private geminiDir: string | undefined;
private logFilePath: string | undefined; private logFilePath: string | undefined;
private checkpointFilePath: string | undefined;
private sessionId: string | undefined; private sessionId: string | undefined;
private messageId = 0; // Instance-specific counter for the next messageId private messageId = 0; // Instance-specific counter for the next messageId
private initialized = false; private initialized = false;
@ -98,7 +96,6 @@ export class Logger {
this.geminiDir = getProjectTempDir(process.cwd()); this.geminiDir = getProjectTempDir(process.cwd());
this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME); this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME);
this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME);
try { try {
await fs.mkdir(this.geminiDir, { recursive: true }); await fs.mkdir(this.geminiDir, { recursive: true });
@ -234,18 +231,18 @@ export class Logger {
} }
} }
_checkpointPath(tag: string | undefined): string { _checkpointPath(tag: string): string {
if (!this.checkpointFilePath || !this.geminiDir) { if (!tag.length) {
throw new Error('Checkpoint file path not set.'); throw new Error('No checkpoint tag specified.');
} }
if (!tag) { if (!this.geminiDir) {
return this.checkpointFilePath; throw new Error('Checkpoint file path not set.');
} }
return path.join(this.geminiDir, `checkpoint-${tag}.json`); return path.join(this.geminiDir, `checkpoint-${tag}.json`);
} }
async saveCheckpoint(conversation: Content[], tag?: string): Promise<void> { async saveCheckpoint(conversation: Content[], tag: string): Promise<void> {
if (!this.initialized || !this.checkpointFilePath) { if (!this.initialized) {
console.error( console.error(
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.', 'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
); );
@ -259,8 +256,8 @@ export class Logger {
} }
} }
async loadCheckpoint(tag?: string): Promise<Content[]> { async loadCheckpoint(tag: string): Promise<Content[]> {
if (!this.initialized || !this.checkpointFilePath) { if (!this.initialized) {
console.error( console.error(
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.', 'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
); );
@ -268,7 +265,6 @@ export class Logger {
} }
const path = this._checkpointPath(tag); const path = this._checkpointPath(tag);
try { try {
const fileContent = await fs.readFile(path, 'utf-8'); const fileContent = await fs.readFile(path, 'utf-8');
const parsedContent = JSON.parse(fileContent); const parsedContent = JSON.parse(fileContent);
@ -280,12 +276,12 @@ export class Logger {
} }
return parsedContent as Content[]; return parsedContent as Content[];
} catch (error) { } catch (error) {
console.error(`Failed to read or parse checkpoint file ${path}:`, error);
const nodeError = error as NodeJS.ErrnoException; const nodeError = error as NodeJS.ErrnoException;
if (nodeError.code === 'ENOENT') { if (nodeError.code === 'ENOENT') {
// File doesn't exist, which is fine. Return empty array. // File doesn't exist, which is fine. Return empty array.
return []; return [];
} }
console.error(`Failed to read or parse checkpoint file ${path}:`, error);
return []; return [];
} }
} }
@ -293,7 +289,6 @@ export class Logger {
close(): void { close(): void {
this.initialized = false; this.initialized = false;
this.logFilePath = undefined; this.logFilePath = undefined;
this.checkpointFilePath = undefined;
this.logs = []; this.logs = [];
this.sessionId = undefined; this.sessionId = undefined;
this.messageId = 0; this.messageId = 0;