andy-stack_vaultkeeper-ai/Services/DiffService.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

82 lines
No EOL
2.6 KiB
TypeScript

import * as Diff from 'diff';
import type VaultkeeperAIPlugin from 'main';
import { Resolve } from './DependencyService';
import { Services } from './Services';
import type { EventService } from './EventService';
import { Event } from 'Enums/Event';
import type { Diff2HtmlUIConfig } from 'diff2html/lib/ui/js/diff2html-ui';
import { ColorSchemeType, OutputFormatType } from 'diff2html/lib/types';
import { Platform } from 'obsidian';
interface DiffResult {
accepted: boolean;
suggestion?: string;
}
export class DiffService {
private readonly plugin: VaultkeeperAIPlugin;
private readonly eventService: EventService;
private diffResolve?: (result: DiffResult) => void;
public constructor() {
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
this.eventService = Resolve<EventService>(Services.EventService);
}
public async requestDiff(oldFileName: string, newFileName: string, oldContent: string, newContent: string): Promise<DiffResult> {
const diffString = this.createDiffString(oldFileName, newFileName, oldContent, newContent);
const outputFormat: OutputFormatType = (Platform.isMobile || oldContent.trim() === "") ? "line-by-line" : "side-by-side";
const config: Diff2HtmlUIConfig = {
drawFileList: false,
matching: "words",
outputFormat: outputFormat,
highlight: true,
fileListToggle: false,
fileContentToggle: false,
synchronisedScroll: true,
colorScheme: ColorSchemeType.AUTO
};
return new Promise((resolve) => {
this.diffResolve = resolve;
void this.plugin.activateDiffView(diffString, config);
this.eventService.trigger(Event.DiffOpened);
});
}
public onAccept() {
if (this.diffResolve) {
this.diffResolve({ accepted: true });
}
this.finishDiff();
}
public onReject() {
if (this.diffResolve) {
this.diffResolve({ accepted: false });
}
this.finishDiff();
}
public onSuggest(suggestion: string) {
if (this.diffResolve) {
this.diffResolve({ accepted: false, suggestion: suggestion });
}
this.finishDiff();
}
private createDiffString(oldFileName: string, newFileName: string, oldContent: string, newContent: string): string {
return Diff.createTwoFilesPatch(oldFileName, newFileName, oldContent, newContent);
}
private finishDiff() {
this.diffResolve = undefined;
this.eventService.trigger(Event.DiffClosed);
}
}