Release v1.01 Enhanced: Vi Control, TUI Gen5, Core Stability
This commit is contained in:
187
bin/ui/components/PreviewPlan.mjs
Normal file
187
bin/ui/components/PreviewPlan.mjs
Normal file
@@ -0,0 +1,187 @@
|
||||
/**
|
||||
* 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 };
|
||||
Reference in New Issue
Block a user