/// import * as path from 'node:path' import { matchesKey } from '@earendil-works/pi-tui' import type { UndoOperation } from './reverse-last-state.ts' type PickerContext = { cwd: string hasUI: boolean ui: { setWidget?: (key: string, content: string[] | undefined) => void onTerminalInput?: ( handler: (data: string) => { consume?: boolean; data?: string } | undefined ) => () => void } } function formatCandidateLabel(cwd: string, operation: UndoOperation): string { const stamp = new Date(operation.timestamp).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }) const sample = operation.files .slice(0, 2) .map((file) => pathRelative(cwd, file.absPath)) .join(', ') const suffix = operation.files.length > 2 ? ` +${operation.files.length - 2} more` : '' return `${stamp} ${operation.toolName} (${operation.files.length} file${operation.files.length === 1 ? '' : 's'}) ${sample}${suffix}` } function pathRelative(cwd: string, absPath: string): string { return path.relative(cwd, absPath) || absPath } export async function selectOperationsFromUI( ctx: PickerContext, operations: UndoOperation[] ): Promise | undefined> { const setWidget = ctx.ui.setWidget const onTerminalInput = ctx.ui.onTerminalInput if (!ctx.hasUI || !setWidget || !onTerminalInput) { return undefined } const widgetKey = 'undo-picker' const selected = operations.map(() => false) let cursor = 0 return new Promise((resolve) => { const render = () => { const lines = [ 'Select changes to reverse (since last user message).', 'Space toggle • a select all/none • Enter apply • Esc cancel', '', ...operations.map( (operation, index) => `${index === cursor ? '>' : ' '} ${selected[index] ? '[x]' : '[ ]'} ${formatCandidateLabel(ctx.cwd, operation)}` ) ] setWidget(widgetKey, lines) } const finish = (value: Set | undefined) => { unsubscribe() setWidget(widgetKey, undefined) resolve(value) } render() const unsubscribe = onTerminalInput((data) => { if (matchesKey(data, 'escape') || matchesKey(data, 'ctrl+c')) { finish(undefined) return { consume: true } } if (matchesKey(data, 'enter') || matchesKey(data, 'return')) { const ids = new Set() for (let index = 0; index < operations.length; index++) { if (selected[index]) { ids.add(operations[index].id) } } finish(ids) return { consume: true } } if (data === ' ') { selected[cursor] = !selected[cursor] render() return { consume: true } } if (data === 'a' || data === 'A') { const allSelected = selected.every(Boolean) for (let index = 0; index < selected.length; index++) { selected[index] = !allSelected } render() return { consume: true } } if (matchesKey(data, 'up') || data === 'k') { cursor = cursor > 0 ? cursor - 1 : operations.length - 1 render() return { consume: true } } if (matchesKey(data, 'down') || data === 'j') { cursor = cursor < operations.length - 1 ? cursor + 1 : 0 render() return { consume: true } } return undefined }) }) }