andy-stack_vaultkeeper-ai/Views/DiffView.ts
Andrew Beal 10ddb1da28 feat: add artifact tracking system for agent file operations
Implement comprehensive artifact tracking to record all file modifications made by the AI agent during conversations. Add artifact persistence, garbage collection, and UI updates with new "Discuss" button styling.
2026-07-10 21:23:44 +01:00

164 lines
No EOL
5.1 KiB
TypeScript

import { Diff2HtmlUI, type Diff2HtmlUIConfig } from "diff2html/lib/ui/js/diff2html-ui-base";
import { Event } from "Enums/Event";
import { ItemView, WorkspaceLeaf, Platform, type ViewStateResult } from "obsidian";
import { Resolve } from "Services/DependencyService";
import type { EventService } from "Services/EventService";
import type { DiffService } from "Services/DiffService";
import { Services } from "Services/Services";
import { VIEW_TYPE_MAIN, type MainView } from "./MainView";
import { tick } from "svelte";
import { Copy } from "Enums/Copy";
export const VIEW_TYPE_DIFF = 'vaultkeeper-ai-diff-view';
interface DiffViewState {
diffString: string;
config: Diff2HtmlUIConfig;
}
export class DiffView extends ItemView {
private readonly eventService: EventService;
private readonly diffService: DiffService;
private diffString: string = "";
private config: Diff2HtmlUIConfig = {};
private diffContainer: HTMLElement | null = null;
private mobileButtonsContainer: HTMLElement | null = null;
constructor(leaf: WorkspaceLeaf) {
super(leaf);
this.eventService = Resolve<EventService>(Services.EventService);
this.diffService = Resolve<DiffService>(Services.DiffService);
this.registerEvent(this.eventService.on(Event.DiffClosed, () => {
this.leaf.detach();
}));
}
protected override onClose(): Promise<void> {
// trigger DiffClosed event in case the user closed the tab
this.eventService.trigger(Event.DiffClosed);
return Promise.resolve();
}
public getViewType(): string {
return VIEW_TYPE_DIFF;
}
public getDisplayText(): string {
return "Vaultkeeper AI diff";
}
public async setState(state: DiffViewState, result: ViewStateResult): Promise<void> {
this.diffString = state.diffString;
this.config = state.config;
this.renderDiff();
return super.setState(state, result);
}
public getState(): Record<string, unknown> {
return {
diffString: this.diffString,
config: this.config
};
}
private renderDiff() {
const container = this.resetContainer();
if (Platform.isMobile) {
container.addClass('diff-view-mobile');
}
this.diffContainer = container.createDiv({ cls: 'd2h-wrapper' });
const diff2htmlUi = new Diff2HtmlUI(this.diffContainer, this.diffString, this.config);
diff2htmlUi.draw();
if (Platform.isMobile) {
this.mobileButtonsContainer = this.createMobileButtons();
}
window.requestAnimationFrame(() => {
const firstChange = this.diffContainer?.querySelector('.d2h-change, .d2h-ins, .d2h-del');
firstChange?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
}
private resetContainer(): HTMLElement {
const container = this.contentEl;
container.empty();
if (this.diffContainer) {
this.diffContainer.remove();
this.diffContainer = null;
}
if (this.mobileButtonsContainer) {
this.mobileButtonsContainer.remove();
this.mobileButtonsContainer = null;
}
return container;
}
private createMobileButtons(): HTMLElement {
const container = this.contentEl.createDiv({ cls: 'diff-mobile-controls' });
const acceptButton = container.createEl('button', {
cls: 'diff-mobile-button diff-mobile-accept',
text: Copy.ButtonApprove
});
acceptButton.setAttribute('aria-label', Copy.ButtonApprove);
const discussButton = container.createEl('button', {
cls: 'diff-mobile-button diff-mobile-discuss',
text: Copy.ButtonDiscuss
});
discussButton.setAttribute('aria-label', Copy.ButtonDiscuss);
const rejectButton = container.createEl('button', {
cls: 'diff-mobile-button diff-mobile-reject',
text: Copy.ButtonReject
});
rejectButton.setAttribute('aria-label', Copy.ButtonReject);
this.registerDomEvent(acceptButton, 'click', async () => {
this.diffService.onAccept();
await this.refocusMainView();
});
this.registerDomEvent(discussButton, 'click', async () => {
await this.refocusMainView(true);
});
this.registerDomEvent(rejectButton, 'click', async () => {
this.diffService.onReject();
await this.refocusMainView();
});
return container;
}
private async refocusMainView(focusChatInput: boolean = false): Promise<void> {
await tick().then(async () => {
const { workspace } = this.app;
const leaves = workspace.getLeavesOfType(VIEW_TYPE_MAIN);
if (leaves.length > 0) {
await workspace.revealLeaf(leaves[0]);
workspace.setActiveLeaf(leaves[0], { focus: true });
if (focusChatInput) {
(leaves[0].view as MainView).input?.focusInput(true);
}
}
});
}
}