2026-01-14 10:22:21 -05:00
|
|
|
|
/**
|
|
|
|
|
|
* @license
|
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
2026-03-06 21:18:36 +05:30
|
|
|
|
import { Box, Text, useIsScreenReaderEnabled } from 'ink';
|
2026-01-14 10:22:21 -05:00
|
|
|
|
import type React from 'react';
|
|
|
|
|
|
import { useMemo } from 'react';
|
|
|
|
|
|
import { theme } from '../semantic-colors.js';
|
|
|
|
|
|
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
|
|
|
|
|
import type { RadioSelectItem } from './shared/RadioButtonSelect.js';
|
|
|
|
|
|
import type { FileChangeStats } from '../utils/rewindFileOps.js';
|
|
|
|
|
|
import { useKeypress } from '../hooks/useKeypress.js';
|
|
|
|
|
|
import { formatTimeAgo } from '../utils/formatters.js';
|
2026-03-09 23:26:33 +00:00
|
|
|
|
import { Command } from '../key/keyMatchers.js';
|
2026-03-09 20:48:09 +00:00
|
|
|
|
import { useKeyMatchers } from '../hooks/useKeyMatchers.js';
|
2026-01-14 10:22:21 -05:00
|
|
|
|
|
|
|
|
|
|
export enum RewindOutcome {
|
|
|
|
|
|
RewindAndRevert = 'rewind_and_revert',
|
|
|
|
|
|
RewindOnly = 'rewind_only',
|
|
|
|
|
|
RevertOnly = 'revert_only',
|
|
|
|
|
|
Cancel = 'cancel',
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const REWIND_OPTIONS: Array<RadioSelectItem<RewindOutcome>> = [
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Rewind conversation and revert code changes',
|
|
|
|
|
|
value: RewindOutcome.RewindAndRevert,
|
|
|
|
|
|
key: 'Rewind conversation and revert code changes',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Rewind conversation',
|
|
|
|
|
|
value: RewindOutcome.RewindOnly,
|
|
|
|
|
|
key: 'Rewind conversation',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Revert code changes',
|
|
|
|
|
|
value: RewindOutcome.RevertOnly,
|
|
|
|
|
|
key: 'Revert code changes',
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
label: 'Do nothing (esc)',
|
|
|
|
|
|
value: RewindOutcome.Cancel,
|
|
|
|
|
|
key: 'Do nothing (esc)',
|
|
|
|
|
|
},
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
interface RewindConfirmationProps {
|
|
|
|
|
|
stats: FileChangeStats | null;
|
|
|
|
|
|
onConfirm: (outcome: RewindOutcome) => void;
|
|
|
|
|
|
terminalWidth: number;
|
|
|
|
|
|
timestamp?: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export const RewindConfirmation: React.FC<RewindConfirmationProps> = ({
|
|
|
|
|
|
stats,
|
|
|
|
|
|
onConfirm,
|
|
|
|
|
|
terminalWidth,
|
|
|
|
|
|
timestamp,
|
|
|
|
|
|
}) => {
|
2026-03-09 20:48:09 +00:00
|
|
|
|
const keyMatchers = useKeyMatchers();
|
2026-03-06 21:18:36 +05:30
|
|
|
|
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
2026-01-14 10:22:21 -05:00
|
|
|
|
useKeypress(
|
|
|
|
|
|
(key) => {
|
|
|
|
|
|
if (keyMatchers[Command.ESCAPE](key)) {
|
|
|
|
|
|
onConfirm(RewindOutcome.Cancel);
|
2026-01-27 14:26:00 -08:00
|
|
|
|
return true;
|
2026-01-14 10:22:21 -05:00
|
|
|
|
}
|
2026-01-27 14:26:00 -08:00
|
|
|
|
return false;
|
2026-01-14 10:22:21 -05:00
|
|
|
|
},
|
|
|
|
|
|
{ isActive: true },
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelect = (outcome: RewindOutcome) => {
|
|
|
|
|
|
onConfirm(outcome);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const options = useMemo(() => {
|
|
|
|
|
|
if (stats) {
|
|
|
|
|
|
return REWIND_OPTIONS;
|
|
|
|
|
|
}
|
|
|
|
|
|
return REWIND_OPTIONS.filter(
|
|
|
|
|
|
(option) =>
|
|
|
|
|
|
option.value !== RewindOutcome.RewindAndRevert &&
|
|
|
|
|
|
option.value !== RewindOutcome.RevertOnly,
|
|
|
|
|
|
);
|
|
|
|
|
|
}, [stats]);
|
2026-03-06 21:18:36 +05:30
|
|
|
|
if (isScreenReaderEnabled) {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box flexDirection="column" width={terminalWidth}>
|
|
|
|
|
|
<Text bold>Confirm Rewind</Text>
|
|
|
|
|
|
|
|
|
|
|
|
{stats && (
|
|
|
|
|
|
<Box flexDirection="column">
|
|
|
|
|
|
<Text>
|
|
|
|
|
|
{stats.fileCount === 1
|
|
|
|
|
|
? `File: ${stats.details?.at(0)?.fileName}`
|
|
|
|
|
|
: `${stats.fileCount} files affected`}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text>Lines added: {stats.addedLines}</Text>
|
|
|
|
|
|
<Text>Lines removed: {stats.removedLines}</Text>
|
|
|
|
|
|
{timestamp && <Text>({formatTimeAgo(timestamp)})</Text>}
|
|
|
|
|
|
<Text>
|
|
|
|
|
|
Note: Rewinding does not affect files edited manually or by the
|
|
|
|
|
|
shell tool.
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!stats && (
|
|
|
|
|
|
<Box>
|
|
|
|
|
|
<Text color={theme.text.secondary}>No code changes to revert.</Text>
|
|
|
|
|
|
{timestamp && (
|
|
|
|
|
|
<Text color={theme.text.secondary}>
|
|
|
|
|
|
{' '}
|
|
|
|
|
|
({formatTimeAgo(timestamp)})
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Text>Select an action:</Text>
|
|
|
|
|
|
<Text color={theme.text.secondary}>
|
|
|
|
|
|
Use arrow keys to navigate, Enter to confirm, Esc to cancel.
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
|
|
|
|
|
|
<RadioButtonSelect
|
|
|
|
|
|
items={options}
|
|
|
|
|
|
onSelect={handleSelect}
|
|
|
|
|
|
isFocused={true}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-01-14 10:22:21 -05:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Box
|
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
|
borderStyle="round"
|
|
|
|
|
|
borderColor={theme.border.default}
|
|
|
|
|
|
padding={1}
|
|
|
|
|
|
width={terminalWidth}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Box marginBottom={1}>
|
|
|
|
|
|
<Text bold>Confirm Rewind</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
{stats && (
|
|
|
|
|
|
<Box
|
|
|
|
|
|
flexDirection="column"
|
|
|
|
|
|
marginBottom={1}
|
|
|
|
|
|
borderStyle="single"
|
|
|
|
|
|
borderColor={theme.border.default}
|
|
|
|
|
|
paddingX={1}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text color={theme.text.primary}>
|
|
|
|
|
|
{stats.fileCount === 1
|
|
|
|
|
|
? `File: ${stats.details?.at(0)?.fileName}`
|
|
|
|
|
|
: `${stats.fileCount} files affected`}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Box flexDirection="row">
|
|
|
|
|
|
<Text color={theme.status.success}>
|
|
|
|
|
|
Lines added: {stats.addedLines}{' '}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
<Text color={theme.status.error}>
|
|
|
|
|
|
Lines removed: {stats.removedLines}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{timestamp && (
|
|
|
|
|
|
<Text color={theme.text.secondary}>
|
|
|
|
|
|
{' '}
|
|
|
|
|
|
({formatTimeAgo(timestamp)})
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
<Box marginTop={1}>
|
|
|
|
|
|
<Text color={theme.status.warning}>
|
|
|
|
|
|
ℹ Rewinding does not affect files edited manually or by the shell
|
|
|
|
|
|
tool.
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!stats && (
|
|
|
|
|
|
<Box marginBottom={1}>
|
|
|
|
|
|
<Text color={theme.text.secondary}>No code changes to revert.</Text>
|
|
|
|
|
|
{timestamp && (
|
|
|
|
|
|
<Text color={theme.text.secondary}>
|
|
|
|
|
|
{' '}
|
|
|
|
|
|
({formatTimeAgo(timestamp)})
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<Box marginBottom={1}>
|
|
|
|
|
|
<Text>Select an action:</Text>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
|
|
<RadioButtonSelect
|
|
|
|
|
|
items={options}
|
|
|
|
|
|
onSelect={handleSelect}
|
|
|
|
|
|
isFocused={true}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Box>
|
|
|
|
|
|
);
|
|
|
|
|
|
};
|