logConversation
loadConversation /resume clean up for review
This commit is contained in:
parent
d79dafc577
commit
36f58a34b4
|
@ -11,10 +11,11 @@ import process from 'node:process';
|
|||
import { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
import {
|
||||
Config,
|
||||
MCPServerStatus,
|
||||
getMCPServerStatus,
|
||||
getMCPDiscoveryState,
|
||||
Logger,
|
||||
MCPDiscoveryState,
|
||||
MCPServerStatus,
|
||||
getMCPDiscoveryState,
|
||||
getMCPServerStatus,
|
||||
} from '@gemini-cli/core';
|
||||
import { Message, MessageType, HistoryItemWithoutId } from '../types.js';
|
||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||
|
@ -487,6 +488,76 @@ Add any other context about the problem here.
|
|||
})();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'save',
|
||||
description: 'save conversation checkpoint',
|
||||
action: async (_mainCommand, _subCommand, _args) => {
|
||||
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() || []);
|
||||
} 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) => {
|
||||
const logger = new Logger();
|
||||
await logger.initialize();
|
||||
const conversation = await logger.loadCheckpoint();
|
||||
if (conversation.length === 0) {
|
||||
addMessage({
|
||||
type: MessageType.INFO,
|
||||
content: 'No saved conversation found.',
|
||||
timestamp: new Date(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
const chat = await config?.getGeminiClient()?.getChat();
|
||||
clearItems();
|
||||
let i = 0;
|
||||
const rolemap: { [key: string]: MessageType } = {
|
||||
user: MessageType.USER,
|
||||
model: MessageType.GEMINI,
|
||||
};
|
||||
for (const item of conversation) {
|
||||
i += 1;
|
||||
const text =
|
||||
item.parts
|
||||
?.filter((m) => !!m.text)
|
||||
.map((m) => m.text)
|
||||
.join('') || '';
|
||||
if (i <= 2) {
|
||||
// Skip system prompt back and forth.
|
||||
continue;
|
||||
}
|
||||
if (!text) {
|
||||
// Parsing Part[] back to various non-text output not yet implemented.
|
||||
continue;
|
||||
}
|
||||
addItem(
|
||||
{
|
||||
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
|
||||
text,
|
||||
} as HistoryItemWithoutId,
|
||||
i,
|
||||
);
|
||||
chat?.addHistory(item);
|
||||
}
|
||||
console.clear();
|
||||
refreshStatic();
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'quit',
|
||||
altName: 'exit',
|
||||
|
|
|
@ -131,7 +131,7 @@ export enum MessageType {
|
|||
USER = 'user',
|
||||
ABOUT = 'about',
|
||||
STATS = 'stats',
|
||||
// Add GEMINI if needed by other commands
|
||||
GEMINI = 'gemini',
|
||||
}
|
||||
|
||||
// Simplified message structure for internal feedback
|
||||
|
|
|
@ -16,10 +16,17 @@ import {
|
|||
import { Logger, MessageSenderType, LogEntry } from './logger.js';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { Content } from '@google/genai';
|
||||
|
||||
const GEMINI_DIR = '.gemini';
|
||||
const LOG_FILE_NAME = 'logs.json';
|
||||
const CHECKPOINT_FILE_NAME = 'checkpoint.json';
|
||||
const TEST_LOG_FILE_PATH = path.join(process.cwd(), GEMINI_DIR, LOG_FILE_NAME);
|
||||
const TEST_CHECKPOINT_FILE_PATH = path.join(
|
||||
process.cwd(),
|
||||
GEMINI_DIR,
|
||||
CHECKPOINT_FILE_NAME,
|
||||
);
|
||||
|
||||
async function cleanupLogFile() {
|
||||
try {
|
||||
|
@ -29,11 +36,22 @@ async function cleanupLogFile() {
|
|||
// Other errors during unlink are ignored for cleanup purposes
|
||||
}
|
||||
}
|
||||
try {
|
||||
await fs.unlink(TEST_CHECKPOINT_FILE_PATH);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// Other errors during unlink are ignored for cleanup purposes
|
||||
}
|
||||
}
|
||||
try {
|
||||
const geminiDirPath = path.join(process.cwd(), GEMINI_DIR);
|
||||
const dirContents = await fs.readdir(geminiDirPath);
|
||||
for (const file of dirContents) {
|
||||
if (file.startsWith(LOG_FILE_NAME + '.') && file.endsWith('.bak')) {
|
||||
if (
|
||||
(file.startsWith(LOG_FILE_NAME + '.') ||
|
||||
file.startsWith(CHECKPOINT_FILE_NAME + '.')) &&
|
||||
file.endsWith('.bak')
|
||||
) {
|
||||
try {
|
||||
await fs.unlink(path.join(geminiDirPath, file));
|
||||
} catch (_e) {
|
||||
|
@ -408,6 +426,53 @@ describe('Logger', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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' }] },
|
||||
];
|
||||
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 return an empty array if the file contains invalid JSON', async () => {
|
||||
await fs.writeFile(TEST_CHECKPOINT_FILE_PATH, 'invalid json');
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const loadedCheckpoint = await logger.loadCheckpoint();
|
||||
expect(loadedCheckpoint).toEqual([]);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Failed to read or parse checkpoint file'),
|
||||
expect.any(SyntaxError),
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return an empty array if logger is not initialized', async () => {
|
||||
const uninitializedLogger = new Logger();
|
||||
const consoleErrorSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
const loadedCheckpoint = await uninitializedLogger.loadCheckpoint();
|
||||
expect(loadedCheckpoint).toEqual([]);
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
|
||||
);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('close', () => {
|
||||
it('should reset logger state', async () => {
|
||||
await logger.logMessage(MessageSenderType.USER, 'A message');
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
|
||||
import path from 'node:path';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { Content } from '@google/genai';
|
||||
|
||||
const GEMINI_DIR = '.gemini';
|
||||
const LOG_FILE_NAME = 'logs.json';
|
||||
const CHECKPOINT_FILE_NAME = 'checkpoint.json';
|
||||
|
||||
export enum MessageSenderType {
|
||||
USER = 'user',
|
||||
|
@ -24,6 +26,7 @@ export interface LogEntry {
|
|||
|
||||
export class Logger {
|
||||
private logFilePath: string | undefined;
|
||||
private checkpointFilePath: string | undefined;
|
||||
private sessionId: number | undefined;
|
||||
private messageId = 0; // Instance-specific counter for the next messageId
|
||||
private initialized = false;
|
||||
|
@ -92,6 +95,7 @@ export class Logger {
|
|||
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);
|
||||
|
||||
try {
|
||||
await fs.mkdir(geminiDir, { recursive: true });
|
||||
|
@ -229,9 +233,61 @@ export class Logger {
|
|||
}
|
||||
}
|
||||
|
||||
async saveCheckpoint(conversation: Content[]): Promise<void> {
|
||||
if (!this.initialized || !this.checkpointFilePath) {
|
||||
console.error(
|
||||
'Logger not initialized or checkpoint file path not set. Cannot save a checkpoint.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await fs.writeFile(
|
||||
this.checkpointFilePath,
|
||||
JSON.stringify(conversation, null),
|
||||
'utf-8',
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error writing to checkpoint file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async loadCheckpoint(): Promise<Content[]> {
|
||||
if (!this.initialized || !this.checkpointFilePath) {
|
||||
console.error(
|
||||
'Logger not initialized or checkpoint file path not set. Cannot load checkpoint.',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
const fileContent = await fs.readFile(this.checkpointFilePath, '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.`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
return parsedContent as Content[];
|
||||
} catch (error) {
|
||||
const nodeError = error as NodeJS.ErrnoException;
|
||||
if (nodeError.code === 'ENOENT') {
|
||||
// File doesn't exist, which is fine. Return empty array.
|
||||
return [];
|
||||
}
|
||||
console.error(
|
||||
`Failed to read or parse checkpoint file ${this.checkpointFilePath}:`,
|
||||
error,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.initialized = false;
|
||||
this.logFilePath = undefined;
|
||||
this.checkpointFilePath = undefined;
|
||||
this.logs = [];
|
||||
this.sessionId = undefined;
|
||||
this.messageId = 0;
|
||||
|
|
Loading…
Reference in New Issue