Files
obsidian_ollama/src/undo-manager.ts
T
fegger a573d33d0a Add undo manager and CoW semantics for tool execution
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
2026-05-26 09:50:23 +02:00

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 = [];
}
}