188 lines
5.3 KiB
JavaScript
188 lines
5.3 KiB
JavaScript
/**
|
|
* Preview Plan Component - Noob-proof action preview
|
|
*
|
|
* CORE NOOB-PROOF FEATURE:
|
|
* Before running actions, show a numbered list with:
|
|
* - Risk labels (Safe / Needs approval / Manual)
|
|
* - Edit options per step
|
|
* - Default actions: Run / Step-by-step / Edit / Cancel
|
|
*
|
|
* Credit: OpenCode patterns + Windows-Use verification
|
|
*/
|
|
|
|
import React, { useState } from 'react';
|
|
import { Box, Text } from 'ink';
|
|
import { colors } from '../../tui-theme.mjs';
|
|
import { getCapabilities } from '../../terminal-profile.mjs';
|
|
|
|
const h = React.createElement;
|
|
|
|
// Risk levels
|
|
export const RISK_LEVELS = {
|
|
SAFE: 'safe',
|
|
NEEDS_APPROVAL: 'needs_approval',
|
|
MANUAL: 'manual'
|
|
};
|
|
|
|
const RISK_CONFIG = {
|
|
[RISK_LEVELS.SAFE]: {
|
|
label: 'Safe',
|
|
color: 'green',
|
|
icon: '✓',
|
|
iconAscii: '+'
|
|
},
|
|
[RISK_LEVELS.NEEDS_APPROVAL]: {
|
|
label: 'Approval',
|
|
color: 'yellow',
|
|
icon: '⚠',
|
|
iconAscii: '!'
|
|
},
|
|
[RISK_LEVELS.MANUAL]: {
|
|
label: 'Manual',
|
|
color: 'magenta',
|
|
icon: '👤',
|
|
iconAscii: '*'
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Single step in preview
|
|
*/
|
|
const PreviewStep = ({
|
|
index,
|
|
description,
|
|
risk = RISK_LEVELS.SAFE,
|
|
isSelected = false,
|
|
width = 60
|
|
}) => {
|
|
const caps = getCapabilities();
|
|
const riskConfig = RISK_CONFIG[risk];
|
|
const riskIcon = caps.unicodeOK ? riskConfig.icon : riskConfig.iconAscii;
|
|
|
|
// Truncate description
|
|
const maxDescWidth = width - 15;
|
|
const desc = description.length > maxDescWidth
|
|
? description.slice(0, maxDescWidth - 1) + '…'
|
|
: description;
|
|
|
|
return h(Box, { flexDirection: 'row' },
|
|
// Selection indicator
|
|
h(Text, { color: isSelected ? colors.accent : colors.muted },
|
|
isSelected ? '▸ ' : ' '
|
|
),
|
|
|
|
// Step number
|
|
h(Text, { color: colors.muted }, `${index + 1}) `),
|
|
|
|
// Description
|
|
h(Text, { color: colors.fg }, desc),
|
|
|
|
// Risk label
|
|
h(Text, { color: riskConfig.color, dimColor: risk === RISK_LEVELS.SAFE },
|
|
` [${riskIcon} ${riskConfig.label}]`
|
|
)
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Action buttons at bottom
|
|
*/
|
|
const PreviewActions = ({ onRun, onStepByStep, onEdit, onCancel }) => {
|
|
const caps = getCapabilities();
|
|
const separator = caps.unicodeOK ? '│' : '|';
|
|
|
|
return h(Box, { flexDirection: 'row', marginTop: 1, gap: 1 },
|
|
h(Text, { color: colors.success, bold: true }, '[Enter] Run'),
|
|
h(Text, { color: colors.muted }, separator),
|
|
h(Text, { color: colors.accent }, '[s] Step-by-step'),
|
|
h(Text, { color: colors.muted }, separator),
|
|
h(Text, { color: 'yellow' }, '[e] Edit'),
|
|
h(Text, { color: colors.muted }, separator),
|
|
h(Text, { color: colors.error }, '[Esc] Cancel')
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Preview Plan Component
|
|
*
|
|
* Props:
|
|
* - steps: array of { description, risk, target }
|
|
* - title: optional title
|
|
* - selectedIndex: currently selected step (for editing)
|
|
* - onRun: callback when user confirms run
|
|
* - onStepByStep: callback for step-by-step mode
|
|
* - onEdit: callback for edit mode
|
|
* - onCancel: callback for cancel
|
|
* - width: available width
|
|
*/
|
|
const PreviewPlan = ({
|
|
steps = [],
|
|
title = 'Preview Plan',
|
|
selectedIndex = -1,
|
|
onRun = null,
|
|
onStepByStep = null,
|
|
onEdit = null,
|
|
onCancel = null,
|
|
width = 80
|
|
}) => {
|
|
const caps = getCapabilities();
|
|
|
|
// Border characters
|
|
const borderH = caps.unicodeOK ? '─' : '-';
|
|
const cornerTL = caps.unicodeOK ? '┌' : '+';
|
|
const cornerTR = caps.unicodeOK ? '┐' : '+';
|
|
const cornerBL = caps.unicodeOK ? '└' : '+';
|
|
const cornerBR = caps.unicodeOK ? '┘' : '+';
|
|
|
|
const contentWidth = width - 4;
|
|
|
|
// Count risks
|
|
const needsApproval = steps.filter(s => s.risk === RISK_LEVELS.NEEDS_APPROVAL).length;
|
|
const manualSteps = steps.filter(s => s.risk === RISK_LEVELS.MANUAL).length;
|
|
|
|
return h(Box, { flexDirection: 'column', marginY: 1 },
|
|
// Header
|
|
h(Text, { color: colors.accent },
|
|
cornerTL + borderH + ` ${title} (${steps.length} steps) ` +
|
|
borderH.repeat(Math.max(0, contentWidth - title.length - 12)) + cornerTR
|
|
),
|
|
|
|
// Steps list
|
|
h(Box, { flexDirection: 'column', paddingX: 1 },
|
|
...steps.map((step, i) =>
|
|
h(PreviewStep, {
|
|
key: i,
|
|
index: i,
|
|
description: step.description,
|
|
risk: step.risk || RISK_LEVELS.SAFE,
|
|
isSelected: i === selectedIndex,
|
|
width: contentWidth
|
|
})
|
|
)
|
|
),
|
|
|
|
// Risk summary (if any)
|
|
(needsApproval > 0 || manualSteps > 0) ? h(Box, { paddingX: 1, marginTop: 0 },
|
|
needsApproval > 0 ? h(Text, { color: 'yellow', dimColor: true },
|
|
`${needsApproval} step(s) need approval `
|
|
) : null,
|
|
manualSteps > 0 ? h(Text, { color: 'magenta', dimColor: true },
|
|
`${manualSteps} manual step(s)`
|
|
) : null
|
|
) : null,
|
|
|
|
// Action buttons
|
|
h(Box, { paddingX: 1 },
|
|
h(PreviewActions, { onRun, onStepByStep, onEdit, onCancel })
|
|
),
|
|
|
|
// Bottom border
|
|
h(Text, { color: colors.accent },
|
|
cornerBL + borderH.repeat(contentWidth) + cornerBR
|
|
)
|
|
);
|
|
};
|
|
|
|
export default PreviewPlan;
|
|
export { PreviewPlan, PreviewStep };
|