summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/cli/src/ui/hooks/slashCommandProcessor.ts23
-rw-r--r--packages/core/src/core/logger.test.ts93
-rw-r--r--packages/core/src/core/logger.ts42
3 files changed, 122 insertions, 36 deletions
diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
index c468a444..4d677f1e 100644
--- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts
+++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts
@@ -490,35 +490,42 @@ Add any other context about the problem here.
},
{
name: 'save',
- description: 'save conversation checkpoint',
- action: async (_mainCommand, _subCommand, _args) => {
+ description: 'save conversation checkpoint. Usage: /save [tag]',
+ action: async (_mainCommand, subCommand, _args) => {
+ const tag = (subCommand || '').trim();
const logger = new Logger();
await logger.initialize();
const chat = await config?.getGeminiClient()?.getChat();
const history = chat?.getHistory() || [];
if (history.length > 0) {
- logger.saveCheckpoint(chat?.getHistory() || []);
+ await logger.saveCheckpoint(chat?.getHistory() || [], tag);
+ addMessage({
+ type: MessageType.INFO,
+ content: `Conversation checkpoint saved${tag ? ' with tag: ' + tag : ''}.`,
+ timestamp: new Date(),
+ });
} else {
addMessage({
type: MessageType.INFO,
content: 'No conversation found to save.',
timestamp: new Date(),
});
- return;
}
},
},
{
name: 'resume',
- description: 'resume from last conversation checkpoint',
- action: async (_mainCommand, _subCommand, _args) => {
+ description:
+ 'resume from conversation checkpoint. Usage: /resume [tag]',
+ action: async (_mainCommand, subCommand, _args) => {
+ const tag = (subCommand || '').trim();
const logger = new Logger();
await logger.initialize();
- const conversation = await logger.loadCheckpoint();
+ const conversation = await logger.loadCheckpoint(tag);
if (conversation.length === 0) {
addMessage({
type: MessageType.INFO,
- content: 'No saved conversation found.',
+ content: `No saved checkpoint found${tag ? ' with tag: ' + tag : ''}.`,
timestamp: new Date(),
});
return;
diff --git a/packages/core/src/core/logger.test.ts b/packages/core/src/core/logger.test.ts
index f1d0e4a5..72eb353e 100644
--- a/packages/core/src/core/logger.test.ts
+++ b/packages/core/src/core/logger.test.ts
@@ -426,23 +426,96 @@ describe('Logger', () => {
});
});
+ describe('saveCheckpoint', () => {
+ const conversation: Content[] = [
+ { role: 'user', parts: [{ text: 'Hello' }] },
+ { 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 () => {
+ const tag = 'my-test-tag';
+ await logger.saveCheckpoint(conversation, tag);
+ const taggedFilePath = path.join(
+ process.cwd(),
+ GEMINI_DIR,
+ `${CHECKPOINT_FILE_NAME.replace('.json', '')}-${tag}.json`,
+ );
+ const fileContent = await fs.readFile(taggedFilePath, 'utf-8');
+ expect(JSON.parse(fileContent)).toEqual(conversation);
+ // cleanup
+ await fs.unlink(taggedFilePath);
+ });
+
+ it('should not throw if logger is not initialized', async () => {
+ const uninitializedLogger = new Logger();
+ const consoleErrorSpy = vi
+ .spyOn(console, 'error')
+ .mockImplementation(() => {});
+
+ await expect(
+ uninitializedLogger.saveCheckpoint(conversation),
+ ).resolves.not.toThrow();
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
+ );
+ consoleErrorSpy.mockRestore();
+ });
+ });
+
describe('loadCheckpoint', () => {
- it('should load and parse a valid checkpoint file', async () => {
- const conversation: Content[] = [
- { role: 'user', parts: [{ text: 'Hello' }] },
- { role: 'model', parts: [{ text: 'Hi there' }] },
- ];
+ const conversation: Content[] = [
+ { role: 'user', parts: [{ text: 'Hello' }] },
+ { role: 'model', parts: [{ text: 'Hi there' }] },
+ ];
+
+ beforeEach(async () => {
+ // Create a default checkpoint for some tests
await fs.writeFile(
TEST_CHECKPOINT_FILE_PATH,
JSON.stringify(conversation),
);
- const loadedCheckpoint = await logger.loadCheckpoint();
- expect(loadedCheckpoint).toEqual(conversation);
});
- it('should return an empty array if the checkpoint file does not exist', async () => {
- const loadedCheckpoint = await logger.loadCheckpoint();
- expect(loadedCheckpoint).toEqual([]);
+ 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 () => {
+ const tag = 'my-load-tag';
+ const taggedConversation = [
+ ...conversation,
+ { role: 'user', parts: [{ text: 'Another message' }] },
+ ];
+ const taggedFilePath = path.join(
+ process.cwd(),
+ GEMINI_DIR,
+ `${CHECKPOINT_FILE_NAME.replace('.json', '')}-${tag}.json`,
+ );
+ await fs.writeFile(taggedFilePath, JSON.stringify(taggedConversation));
+
+ const loaded = await logger.loadCheckpoint(tag);
+ expect(loaded).toEqual(taggedConversation);
+
+ // cleanup
+ await fs.unlink(taggedFilePath);
+ });
+
+ it('should return an empty array if a tagged checkpoint file does not exist', async () => {
+ const loaded = await logger.loadCheckpoint('non-existent-tag');
+ expect(loaded).toEqual([]);
+ });
+
+ it('should return an empty array if the default checkpoint file does not exist', async () => {
+ await fs.unlink(TEST_CHECKPOINT_FILE_PATH); // Ensure it's gone
+ const loaded = await logger.loadCheckpoint();
+ expect(loaded).toEqual([]);
});
it('should return an empty array if the file contains invalid JSON', async () => {
diff --git a/packages/core/src/core/logger.ts b/packages/core/src/core/logger.ts
index 9026dc36..ee4ce98c 100644
--- a/packages/core/src/core/logger.ts
+++ b/packages/core/src/core/logger.ts
@@ -25,6 +25,7 @@ export interface LogEntry {
}
export class Logger {
+ private geminiDir: string | undefined;
private logFilePath: string | undefined;
private checkpointFilePath: string | undefined;
private sessionId: number | undefined;
@@ -93,12 +94,12 @@ export class Logger {
return;
}
this.sessionId = Math.floor(Date.now() / 1000);
- const geminiDir = path.resolve(process.cwd(), GEMINI_DIR);
- this.logFilePath = path.join(geminiDir, LOG_FILE_NAME);
- this.checkpointFilePath = path.join(geminiDir, CHECKPOINT_FILE_NAME);
+ this.geminiDir = path.resolve(process.cwd(), GEMINI_DIR);
+ this.logFilePath = path.join(this.geminiDir, LOG_FILE_NAME);
+ this.checkpointFilePath = path.join(this.geminiDir, CHECKPOINT_FILE_NAME);
try {
- await fs.mkdir(geminiDir, { recursive: true });
+ await fs.mkdir(this.geminiDir, { recursive: true });
let fileExisted = true;
try {
await fs.access(this.logFilePath);
@@ -233,26 +234,32 @@ export class Logger {
}
}
- async saveCheckpoint(conversation: Content[]): Promise<void> {
+ _checkpointPath(tag: string | undefined): string {
+ if (!this.checkpointFilePath || !this.geminiDir) {
+ throw new Error('Checkpoint file path not set.');
+ }
+ if (!tag) {
+ return this.checkpointFilePath;
+ }
+ return path.join(this.geminiDir, `checkpoint-${tag}.json`);
+ }
+
+ async saveCheckpoint(conversation: Content[], tag?: string): Promise<void> {
if (!this.initialized || !this.checkpointFilePath) {
console.error(
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
);
return;
}
-
+ const path = this._checkpointPath(tag);
try {
- await fs.writeFile(
- this.checkpointFilePath,
- JSON.stringify(conversation, null),
- 'utf-8',
- );
+ await fs.writeFile(path, JSON.stringify(conversation, null), 'utf-8');
} catch (error) {
console.error('Error writing to checkpoint file:', error);
}
}
- async loadCheckpoint(): Promise<Content[]> {
+ async loadCheckpoint(tag?: string): Promise<Content[]> {
if (!this.initialized || !this.checkpointFilePath) {
console.error(
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
@@ -260,12 +267,14 @@ export class Logger {
return [];
}
+ const path = this._checkpointPath(tag);
+
try {
- const fileContent = await fs.readFile(this.checkpointFilePath, 'utf-8');
+ const fileContent = await fs.readFile(path, 'utf-8');
const parsedContent = JSON.parse(fileContent);
if (!Array.isArray(parsedContent)) {
console.warn(
- `Checkpoint file at ${this.checkpointFilePath} is not a valid JSON array. Returning empty checkpoint.`,
+ `Checkpoint file at ${path} is not a valid JSON array. Returning empty checkpoint.`,
);
return [];
}
@@ -276,10 +285,7 @@ export class Logger {
// File doesn't exist, which is fine. Return empty array.
return [];
}
- console.error(
- `Failed to read or parse checkpoint file ${this.checkpointFilePath}:`,
- error,
- );
+ console.error(`Failed to read or parse checkpoint file ${path}:`, error);
return [];
}
}