make tag required for /chat (#2904)
This commit is contained in:
parent
34935d6558
commit
38445f63f0
|
@ -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;
|
||||||
|
|
|
@ -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.',
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in New Issue