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 { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||||
import {
|
import {
|
||||||
Config,
|
Config,
|
||||||
MCPServerStatus,
|
Logger,
|
||||||
getMCPServerStatus,
|
|
||||||
getMCPDiscoveryState,
|
|
||||||
MCPDiscoveryState,
|
MCPDiscoveryState,
|
||||||
|
MCPServerStatus,
|
||||||
|
getMCPDiscoveryState,
|
||||||
|
getMCPServerStatus,
|
||||||
} from '@gemini-cli/core';
|
} from '@gemini-cli/core';
|
||||||
import { Message, MessageType, HistoryItemWithoutId } from '../types.js';
|
import { Message, MessageType, HistoryItemWithoutId } from '../types.js';
|
||||||
import { useSessionStats } from '../contexts/SessionContext.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',
|
name: 'quit',
|
||||||
altName: 'exit',
|
altName: 'exit',
|
||||||
|
|
|
@ -131,7 +131,7 @@ export enum MessageType {
|
||||||
USER = 'user',
|
USER = 'user',
|
||||||
ABOUT = 'about',
|
ABOUT = 'about',
|
||||||
STATS = 'stats',
|
STATS = 'stats',
|
||||||
// Add GEMINI if needed by other commands
|
GEMINI = 'gemini',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simplified message structure for internal feedback
|
// Simplified message structure for internal feedback
|
||||||
|
|
|
@ -16,10 +16,17 @@ import {
|
||||||
import { Logger, MessageSenderType, LogEntry } from './logger.js';
|
import { Logger, MessageSenderType, LogEntry } from './logger.js';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
import { Content } from '@google/genai';
|
||||||
|
|
||||||
const GEMINI_DIR = '.gemini';
|
const GEMINI_DIR = '.gemini';
|
||||||
const LOG_FILE_NAME = 'logs.json';
|
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_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() {
|
async function cleanupLogFile() {
|
||||||
try {
|
try {
|
||||||
|
@ -29,11 +36,22 @@ async function cleanupLogFile() {
|
||||||
// Other errors during unlink are ignored for cleanup purposes
|
// 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 {
|
try {
|
||||||
const geminiDirPath = path.join(process.cwd(), GEMINI_DIR);
|
const geminiDirPath = path.join(process.cwd(), GEMINI_DIR);
|
||||||
const dirContents = await fs.readdir(geminiDirPath);
|
const dirContents = await fs.readdir(geminiDirPath);
|
||||||
for (const file of dirContents) {
|
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 {
|
try {
|
||||||
await fs.unlink(path.join(geminiDirPath, file));
|
await fs.unlink(path.join(geminiDirPath, file));
|
||||||
} catch (_e) {
|
} 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', () => {
|
describe('close', () => {
|
||||||
it('should reset logger state', async () => {
|
it('should reset logger state', async () => {
|
||||||
await logger.logMessage(MessageSenderType.USER, 'A message');
|
await logger.logMessage(MessageSenderType.USER, 'A message');
|
||||||
|
|
|
@ -6,9 +6,11 @@
|
||||||
|
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
|
import { Content } from '@google/genai';
|
||||||
|
|
||||||
const GEMINI_DIR = '.gemini';
|
const GEMINI_DIR = '.gemini';
|
||||||
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',
|
||||||
|
@ -24,6 +26,7 @@ export interface LogEntry {
|
||||||
|
|
||||||
export class Logger {
|
export class Logger {
|
||||||
private logFilePath: string | undefined;
|
private logFilePath: string | undefined;
|
||||||
|
private checkpointFilePath: string | undefined;
|
||||||
private sessionId: number | undefined;
|
private sessionId: number | 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;
|
||||||
|
@ -92,6 +95,7 @@ export class Logger {
|
||||||
this.sessionId = Math.floor(Date.now() / 1000);
|
this.sessionId = Math.floor(Date.now() / 1000);
|
||||||
const geminiDir = path.resolve(process.cwd(), GEMINI_DIR);
|
const geminiDir = path.resolve(process.cwd(), GEMINI_DIR);
|
||||||
this.logFilePath = path.join(geminiDir, LOG_FILE_NAME);
|
this.logFilePath = path.join(geminiDir, LOG_FILE_NAME);
|
||||||
|
this.checkpointFilePath = path.join(geminiDir, CHECKPOINT_FILE_NAME);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.mkdir(geminiDir, { recursive: true });
|
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 {
|
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;
|
||||||
|
|
Loading…
Reference in New Issue