mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
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.
177 lines
5.4 KiB
TypeScript
177 lines
5.4 KiB
TypeScript
import { Event } from "Enums/Event";
|
|
import { Copy } from "Enums/Copy";
|
|
import { ItemView, WorkspaceLeaf, Platform, type ViewStateResult } from "obsidian";
|
|
import { mount, unmount } from "svelte";
|
|
import { Resolve } from "Services/DependencyService";
|
|
import type { EventService } from "Services/EventService";
|
|
import type { PlanApprovalService } from "Services/PlanApprovalService";
|
|
import { Services } from "Services/Services";
|
|
import { VIEW_TYPE_MAIN, type MainView } from "./MainView";
|
|
import PlanApprovalWindow from "Components/PlanApprovalWindow.svelte";
|
|
import type { ExecutionPlan } from "Types/ExecutionPlan";
|
|
import { tick } from "svelte";
|
|
|
|
export const VIEW_TYPE_PLAN_APPROVAL = 'vaultkeeper-ai-plan-approval-view';
|
|
|
|
interface PlanApprovalViewState {
|
|
plan: ExecutionPlan;
|
|
}
|
|
|
|
export class PlanApprovalView extends ItemView {
|
|
|
|
private readonly eventService: EventService;
|
|
private readonly planApprovalService: PlanApprovalService;
|
|
|
|
private plan: ExecutionPlan | undefined;
|
|
|
|
private planWindow: ReturnType<typeof PlanApprovalWindow> | undefined;
|
|
private planContainer: HTMLElement | null = null;
|
|
private mobileControlsContainer: HTMLElement | null = null;
|
|
|
|
constructor(leaf: WorkspaceLeaf) {
|
|
super(leaf);
|
|
|
|
this.eventService = Resolve<EventService>(Services.EventService);
|
|
this.planApprovalService = Resolve<PlanApprovalService>(Services.PlanApprovalService);
|
|
|
|
this.registerEvent(this.eventService.on(Event.PlanApprovalClosed, () => {
|
|
this.leaf.detach();
|
|
}));
|
|
}
|
|
|
|
protected override onClose(): Promise<void> {
|
|
// trigger PlanApprovalClosed event in case the user closed the tab
|
|
this.eventService.trigger(Event.PlanApprovalClosed);
|
|
|
|
if (this.planWindow) {
|
|
void unmount(this.planWindow);
|
|
this.planWindow = undefined;
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
public getViewType(): string {
|
|
return VIEW_TYPE_PLAN_APPROVAL;
|
|
}
|
|
|
|
public getDisplayText(): string {
|
|
return Copy.PlanApprovalViewTitle;
|
|
}
|
|
|
|
public async setState(state: PlanApprovalViewState, result: ViewStateResult): Promise<void> {
|
|
this.plan = state.plan;
|
|
|
|
this.renderPlan();
|
|
|
|
return super.setState(state, result);
|
|
}
|
|
|
|
public getState(): Record<string, unknown> {
|
|
return {
|
|
plan: this.plan
|
|
};
|
|
}
|
|
|
|
private renderPlan() {
|
|
const container = this.resetContainer();
|
|
|
|
if (!this.plan) {
|
|
return;
|
|
}
|
|
|
|
if (Platform.isMobile) {
|
|
container.addClass('plan-approval-view-mobile');
|
|
}
|
|
|
|
this.planContainer = container.createDiv({ cls: 'plan-approval-wrapper' });
|
|
|
|
this.planWindow = mount(PlanApprovalWindow, {
|
|
target: this.planContainer,
|
|
props: {
|
|
plan: this.plan
|
|
}
|
|
});
|
|
|
|
if (Platform.isMobile) {
|
|
this.mobileControlsContainer = this.createMobileButtons();
|
|
}
|
|
}
|
|
|
|
private resetContainer(): HTMLElement {
|
|
const container = this.contentEl;
|
|
container.empty();
|
|
|
|
if (this.planWindow) {
|
|
void unmount(this.planWindow);
|
|
this.planWindow = undefined;
|
|
}
|
|
|
|
if (this.planContainer) {
|
|
this.planContainer.remove();
|
|
this.planContainer = null;
|
|
}
|
|
|
|
if (this.mobileControlsContainer) {
|
|
this.mobileControlsContainer.remove();
|
|
this.mobileControlsContainer = null;
|
|
}
|
|
|
|
return container;
|
|
}
|
|
|
|
private createMobileButtons(): HTMLElement {
|
|
const container = this.contentEl.createDiv({ cls: 'plan-approval-mobile-controls' });
|
|
|
|
const approveButton = container.createEl('button', {
|
|
cls: 'plan-approval-mobile-button plan-approval-mobile-approve',
|
|
text: Copy.ButtonApprove
|
|
});
|
|
approveButton.setAttribute('aria-label', Copy.ButtonApprove);
|
|
|
|
const discussButton = container.createEl('button', {
|
|
cls: 'plan-approval-mobile-button plan-approval-mobile-discuss',
|
|
text: Copy.ButtonDiscuss
|
|
});
|
|
discussButton.setAttribute('aria-label', Copy.ButtonDiscuss);
|
|
|
|
const rejectButton = container.createEl('button', {
|
|
cls: 'plan-approval-mobile-button plan-approval-mobile-reject',
|
|
text: Copy.ButtonReject
|
|
});
|
|
rejectButton.setAttribute('aria-label', Copy.ButtonReject);
|
|
|
|
this.registerDomEvent(approveButton, 'click', async () => {
|
|
this.planApprovalService.onApprove();
|
|
await this.refocusMainView();
|
|
});
|
|
|
|
this.registerDomEvent(discussButton, 'click', async () => {
|
|
await this.refocusMainView(true);
|
|
});
|
|
|
|
this.registerDomEvent(rejectButton, 'click', async () => {
|
|
this.planApprovalService.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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|