feat: shell command explanation option (#173)

# Shell Command Explanation Option

## Description
This PR adds an option to explain shell commands when the user is
prompted to approve them (Fixes #110). When reviewing a shell command,
users can now select "Explain this command" to get a detailed
explanation of what the command does before deciding whether to approve
or reject it.

## Changes
- Added a new "EXPLAIN" option to the `ReviewDecision` enum
- Updated the command review UI to include an "Explain this command (x)"
option
- Implemented the logic to send the command to the LLM for explanation
using the same model as the agent
- Added a display for the explanation in the command review UI
- Updated all relevant components to pass the explanation through the
component tree

## Benefits
- Improves user understanding of shell commands before approving them
- Reduces the risk of approving potentially harmful commands
- Enhances the educational aspect of the tool, helping users learn about
shell commands
- Maintains the same workflow with minimal UI changes

## Testing
- Manually tested the explanation feature with various shell commands
- Verified that the explanation is displayed correctly in the UI
- Confirmed that the user can still approve or reject the command after
viewing the explanation

## Screenshots

![improved_shell_explanation_demo](https://github.com/user-attachments/assets/05923481-29db-4eba-9cc6-5e92301d2be0)


## Additional Notes
The explanation is generated using the same model as the agent, ensuring
consistency in the quality and style of explanations.

---------

Signed-off-by: crazywolf132 <crazywolf132@gmail.com>
This commit is contained in:
Brayden Moon
2025-04-18 06:28:58 +10:00
committed by GitHub
parent 693a6f96cf
commit f3d085aaf8
11 changed files with 352 additions and 68 deletions

View File

@@ -15,11 +15,24 @@ const DEFAULT_DENY_MESSAGE =
export function TerminalChatCommandReview({
confirmationPrompt,
onReviewCommand,
explanation: propExplanation,
}: {
confirmationPrompt: React.ReactNode;
onReviewCommand: (decision: ReviewDecision, customMessage?: string) => void;
explanation?: string;
}): React.ReactElement {
const [mode, setMode] = React.useState<"select" | "input">("select");
const [mode, setMode] = React.useState<"select" | "input" | "explanation">(
"select",
);
const [explanation, setExplanation] = React.useState<string>("");
// If the component receives an explanation prop, update the state
React.useEffect(() => {
if (propExplanation) {
setExplanation(propExplanation);
setMode("explanation");
}
}, [propExplanation]);
const [msg, setMsg] = React.useState<string>("");
// -------------------------------------------------------------------------
@@ -72,6 +85,10 @@ export function TerminalChatCommandReview({
}
opts.push(
{
label: "Explain this command (x)",
value: ReviewDecision.EXPLAIN,
},
{
label: "Edit or give feedback (e)",
value: "edit",
@@ -93,6 +110,8 @@ export function TerminalChatCommandReview({
if (mode === "select") {
if (input === "y") {
onReviewCommand(ReviewDecision.YES);
} else if (input === "x") {
onReviewCommand(ReviewDecision.EXPLAIN);
} else if (input === "e") {
setMode("input");
} else if (input === "n") {
@@ -105,6 +124,11 @@ export function TerminalChatCommandReview({
} else if (key.escape) {
onReviewCommand(ReviewDecision.NO_EXIT);
}
} else if (mode === "explanation") {
// When in explanation mode, any key returns to select mode
if (key.return || key.escape || input === "x") {
setMode("select");
}
} else {
// text entry mode
if (key.return) {
@@ -125,7 +149,44 @@ export function TerminalChatCommandReview({
<Box flexDirection="column" gap={1} borderStyle="round" marginTop={1}>
{confirmationPrompt}
<Box flexDirection="column" gap={1}>
{mode === "select" ? (
{mode === "explanation" ? (
<>
<Text bold color="yellow">
Command Explanation:
</Text>
<Box paddingX={2} flexDirection="column" gap={1}>
{explanation ? (
<>
{explanation.split("\n").map((line, i) => {
// Check if it's an error message
if (
explanation.startsWith("Unable to generate explanation")
) {
return (
<Text key={i} bold color="red">
{line}
</Text>
);
}
// Apply different styling to headings (numbered items)
else if (line.match(/^\d+\.\s+/)) {
return (
<Text key={i} bold color="cyan">
{line}
</Text>
);
} else {
return <Text key={i}>{line}</Text>;
}
})}
</>
) : (
<Text dimColor>Loading explanation...</Text>
)}
<Text dimColor>Press any key to return to options</Text>
</Box>
</>
) : mode === "select" ? (
<>
<Text>Allow command?</Text>
<Box paddingX={2} flexDirection="column" gap={1}>
@@ -141,7 +202,7 @@ export function TerminalChatCommandReview({
/>
</Box>
</>
) : (
) : mode === "input" ? (
<>
<Text>Give the model feedback ( to submit):</Text>
<Box borderStyle="round">
@@ -165,7 +226,7 @@ export function TerminalChatCommandReview({
</Box>
)}
</>
)}
) : null}
</Box>
</Box>
);