mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 06:42:03 +00:00
- Add clickable artifact cards with keyboard support - Create new ArtifactView for displaying file diffs - Update button styles with solid backgrounds and faster transitions - Add auto-cleanup of stale diff and plan approval views on startup - Fix artifact deletion to preserve original file path
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 "Vaultkeeper AI plan";
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
}
|