andy-stack_vaultkeeper-ai/Services/ConversationFileSystemService.ts
Andrew Beal 2d5a1b52bf feat: add interactive diff viewer with user approval workflow
Implement a comprehensive diff viewing system that allows users to review and approve/reject file changes before they're applied. The system includes event-driven architecture for managing diff lifecycle and integrates diff2html for rich visual diffs.

Key changes:
- Add DiffService for managing diff approval workflow with accept/reject/suggest actions
- Create EventService for type-safe event handling (DiffOpened/DiffClosed)
- Add DiffView component with diff2html integration for visual diff rendering
- Modify VaultService to propose changes and require confirmation before file operations
- Update FileSystemService to support optional confirmation for write operations
- Add Event enum for centralized event type definitions
- Import custom styles and diff2html styles for proper diff rendering
- Update ConversationContent validation to support optional toolId field
- Remove FileManager from dependency injection (now accessed directly from app)
- Update .gitignore to track styles.css instead of main.css
2025-11-24 21:29:54 +00:00

133 lines
4.8 KiB
TypeScript

import { Path } from "Enums/Path";
import { Resolve } from "./DependencyService";
import { FileSystemService } from "./FileSystemService";
import { Services } from "./Services";
import { Conversation } from "Conversations/Conversation";
import { ConversationContent } from "Conversations/ConversationContent";
import { Copy } from "Enums/Copy";
import { Exception } from "Helpers/Exception";
export class ConversationFileSystemService {
private fileSystemService: FileSystemService;
private currentConversationPath: string | null = null;
public constructor() {
this.fileSystemService = Resolve<FileSystemService>(Services.FileSystemService);
}
public generateConversationPath(conversation: Conversation): string {
return `${Path.Conversations}/${conversation.title}.json`;
}
public async saveConversation(conversation: Conversation): Promise<string | Error> {
if (!this.currentConversationPath) {
this.currentConversationPath = this.generateConversationPath(conversation);
}
conversation.updated = new Date();
const conversationData = {
title: conversation.title,
created: conversation.created.toISOString(),
updated: conversation.updated.toISOString(),
contents: conversation.contents
.filter(content => content.content !== Copy.ApiRequestAborted.toString())
.map(content => ({
role: content.role,
content: content.content,
promptContent: content.promptContent,
functionCall: content.functionCall,
timestamp: content.timestamp.toISOString(),
isFunctionCall: content.isFunctionCall,
isFunctionCallResponse: content.isFunctionCallResponse,
toolId: content.toolId,
errorType: content.errorType
}))
};
const result = await this.fileSystemService.writeObjectToFile(this.currentConversationPath, conversationData, true, false);
if (result instanceof Error) {
return result;
}
return this.currentConversationPath;
}
public resetCurrentConversation() {
this.currentConversationPath = null;
}
public getCurrentConversationPath(): string | null {
return this.currentConversationPath;
}
public setCurrentConversationPath(filePath: string) {
this.currentConversationPath = filePath;
}
public async deleteCurrentConversation(): Promise<void | Error> {
if (!this.currentConversationPath) {
return;
}
const result = await this.fileSystemService.deleteFile(this.currentConversationPath, true);
if (result instanceof Error) {
return result;
}
this.resetCurrentConversation();
}
public async getAllConversations(): Promise<Conversation[]> {
const files = await this.fileSystemService.listFilesInDirectory(Path.Conversations, false, true);
const conversations: Conversation[] = [];
for (const file of files) {
const result = await this.fileSystemService.readObjectFromFile(file.path, true);
if (result instanceof Error) {
Exception.log(`Failed to load conversation: ${file.path}`);
continue;
}
if (Conversation.isConversationData(result)) {
const conversation: Conversation = new Conversation();
conversation.title = result.title;
conversation.created = new Date(result.created);
conversation.updated = new Date(result.updated);
conversation.contents = result.contents.map(content => {
return new ConversationContent(
content.role,
content.content,
content.promptContent,
content.functionCall,
new Date(content.timestamp),
content.isFunctionCall,
content.isFunctionCallResponse,
content.toolId,
content.errorType
);
});
conversations.push(conversation);
}
}
return conversations;
}
public async updateConversationTitle(oldPath: string, newTitle: string): Promise<void | Error> {
const newPath = `${Path.Conversations}/${newTitle}.json`;
const result = await this.fileSystemService.moveFile(oldPath, newPath, true);
if (result instanceof Error) {
return result;
}
if (this.currentConversationPath === oldPath) {
this.currentConversationPath = newPath;
}
}
}