/// import type { ExtensionAPI, ExtensionCommandContext } from '@earendil-works/pi-coding-agent' import { Type } from 'typebox' import { selectOperationsFromUI } from './reverse-last-picker' import type { SessionEntryLike, UndoOperation } from './reverse-last-state' type UndoContext = { cwd: string sessionManager: { getSessionId(): string getLeafId(): string | null getBranch?: () => SessionEntryLike[] getEntries(): SessionEntryLike[] } } type UndoResult = { steps: number; restored: string[] } export function registerReverseLastCommands( pi: ExtensionAPI, dependencies: { pending: Map getSelectionCandidates: (ctx: UndoContext) => UndoOperation[] undoChanges: (ctx: UndoContext, count: number) => UndoResult undoSelectedChanges: (ctx: UndoContext, selectedIds: Set) => UndoResult } ): void { const { pending, getSelectionCandidates, undoChanges, undoSelectedChanges } = dependencies pi.registerCommand('undo', { description: 'Undo last Pi write/edit file changes. Usage: /undo [count]', handler: async (args: string, ctx: ExtensionCommandContext) => { await ctx.waitForIdle() if (pending.size > 0) { ctx.ui.notify( '/undo is temporarily unavailable while file changes are still being captured', 'warning' ) return } const trimmed = args.trim() if (!trimmed && ctx.hasUI) { const candidates = getSelectionCandidates(ctx) if (candidates.length === 0) { ctx.ui.notify('Nothing to undo in this session yet.', 'info') return } const selectedIds = await selectOperationsFromUI(ctx, candidates) if (selectedIds === undefined) { ctx.ui.notify('Undo cancelled.', 'info') return } if (selectedIds.size === 0) { ctx.ui.notify('No changes selected.', 'warning') return } const result = undoSelectedChanges(ctx, selectedIds) if (result.steps === 0) { ctx.ui.notify('Nothing to undo in this session yet.', 'info') return } ctx.ui.notify(formatNotification('selected change', result), 'info') return } const count = trimmed ? Number.parseInt(trimmed, 10) : 1 if (!Number.isFinite(count) || count <= 0) { ctx.ui.notify('Usage: /undo [count] (count must be a positive integer)', 'warning') return } const result = undoChanges(ctx, count) if (result.steps === 0) { ctx.ui.notify('Nothing to undo in this session yet.', 'info') return } ctx.ui.notify(formatNotification('change', result), 'info') } }) pi.registerTool({ name: 'undo_files', label: 'Undo Files', description: 'Undo the most recent write/edit file changes captured in this session', parameters: Type.Object({ count: Type.Optional( Type.Number({ minimum: 1, maximum: 20, description: 'How many captured change steps to undo' }) ) }), async execute( _toolCallId: string, params: { count?: number }, _signal: unknown, _onUpdate: unknown, ctx ) { if (pending.size > 0) { throw new Error( 'undo_files is temporarily unavailable while file changes are still being captured' ) } const count = typeof params.count === 'number' ? Math.max(1, Math.min(20, Math.trunc(params.count))) : 1 const result = undoChanges(ctx, count) if (result.steps === 0) { return { content: [{ type: 'text', text: 'Nothing to undo in this session yet.' }], details: result } } return { content: [{ type: 'text', text: formatNotification('change', result, 20) }], details: result } } }) } function formatNotification(kind: string, result: UndoResult, maxFiles = 12): string { const suffix = result.restored.length > maxFiles ? `\n...and ${result.restored.length - maxFiles} more` : '' return `Reverted ${result.steps} ${kind}${result.steps === 1 ? '' : 's'}.\n${result.restored.slice(0, maxFiles).join('\n')}${suffix}` }