a573d33d0a
Replace the pending-actions preview flow with immediate execution and undo support. ToolExecutor now accepts an UndoManager and records create, modify, rename, and trash operations so users can roll back batches. Other fixes included: - Deep-merge nested config objects on settings load to preserve new default fields - Increase retry backoff from 10ms to 1000ms and widen the "invalid response format" check to handle prefixed messages - Fix semantic cache clear to null out the collection reference - Tighten memory regex to require "please always/never" - Increase vault indexing batch size from 1 to 5 - Remove unused modeRequiresPreview helper
103 lines
3.0 KiB
TypeScript
103 lines
3.0 KiB
TypeScript
import { Vault, TFile } from 'obsidian';
|
|
import { Logger } from './utils';
|
|
|
|
export type UndoOperation =
|
|
| { type: 'create'; path: string }
|
|
| { type: 'modify'; path: string; originalContent: string }
|
|
| { type: 'rename'; originalPath: string; newPath: string }
|
|
| { type: 'trash'; path: string; originalContent: string };
|
|
|
|
export interface UndoBatch {
|
|
id: string;
|
|
timestamp: number;
|
|
operations: UndoOperation[];
|
|
}
|
|
|
|
export class UndoManager {
|
|
private batches: UndoBatch[] = [];
|
|
private readonly maxBatches = 10;
|
|
|
|
startBatch(): string {
|
|
const id = crypto.randomUUID?.() ?? `undo-${Date.now()}-${Math.random()}`;
|
|
this.batches.push({ id, timestamp: Date.now(), operations: [] });
|
|
if (this.batches.length > this.maxBatches) {
|
|
this.batches = this.batches.slice(-this.maxBatches);
|
|
}
|
|
return id;
|
|
}
|
|
|
|
recordOperation(batchId: string, op: UndoOperation): void {
|
|
const batch = this.batches.find((b) => b.id === batchId);
|
|
batch?.operations.push(op);
|
|
}
|
|
|
|
getBatch(batchId: string): UndoBatch | undefined {
|
|
return this.batches.find((b) => b.id === batchId);
|
|
}
|
|
|
|
hasBatch(batchId: string): boolean {
|
|
const batch = this.getBatch(batchId);
|
|
return !!(batch && batch.operations.length > 0);
|
|
}
|
|
|
|
async undo(batchId: string, vault: Vault): Promise<{ restored: number; failed: number }> {
|
|
const batch = this.getBatch(batchId);
|
|
if (!batch) return { restored: 0, failed: 0 };
|
|
|
|
let restored = 0;
|
|
let failed = 0;
|
|
|
|
for (const op of [...batch.operations].reverse()) {
|
|
try {
|
|
switch (op.type) {
|
|
case 'create': {
|
|
const file = vault.getAbstractFileByPath(op.path);
|
|
if (file instanceof TFile) {
|
|
await vault.trash(file, true);
|
|
}
|
|
break;
|
|
}
|
|
case 'modify': {
|
|
const file = vault.getAbstractFileByPath(op.path);
|
|
if (file instanceof TFile) {
|
|
await vault.modify(file, op.originalContent);
|
|
}
|
|
break;
|
|
}
|
|
case 'rename': {
|
|
const file = vault.getAbstractFileByPath(op.newPath);
|
|
if (file instanceof TFile) {
|
|
await vault.rename(file, op.originalPath);
|
|
}
|
|
break;
|
|
}
|
|
case 'trash': {
|
|
const existing = vault.getAbstractFileByPath(op.path);
|
|
if (existing instanceof TFile) {
|
|
await vault.modify(existing, op.originalContent);
|
|
} else {
|
|
await vault.create(op.path, op.originalContent);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
restored++;
|
|
} catch (error) {
|
|
const path = 'path' in op ? op.path : 'originalPath' in op ? op.originalPath : '?';
|
|
Logger.warn(
|
|
`Undo failed for ${op.type} on ${path}: ${error instanceof Error ? error.message : String(error)}`,
|
|
'undo-manager'
|
|
);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
this.batches = this.batches.filter((b) => b.id !== batchId);
|
|
return { restored, failed };
|
|
}
|
|
|
|
clear(): void {
|
|
this.batches = [];
|
|
}
|
|
}
|