Files
codex/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx
Brayden Moon f3d085aaf8 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>
2025-04-17 13:28:58 -07:00

144 lines
3.6 KiB
TypeScript

import { parseApplyPatch } from "../../parse-apply-patch";
import { shortenPath } from "../../utils/short-path";
import chalk from "chalk";
import { Text } from "ink";
import React from "react";
export function TerminalChatToolCallCommand({
commandForDisplay,
explanation,
}: {
commandForDisplay: string;
explanation?: string;
}): React.ReactElement {
// -------------------------------------------------------------------------
// Colorize diff output inside the command preview: we detect individual
// lines that begin with '+' or '-' (excluding the typical diff headers like
// '+++', '---', '++', '--') and apply green/red coloring. This mirrors
// how Git shows diffs and makes the patch easier to review.
// -------------------------------------------------------------------------
const colorizedCommand = commandForDisplay
.split("\n")
.map((line) => {
if (line.startsWith("+") && !line.startsWith("++")) {
return chalk.green(line);
}
if (line.startsWith("-") && !line.startsWith("--")) {
return chalk.red(line);
}
return line;
})
.join("\n");
return (
<>
<Text bold color="green">
Shell Command
</Text>
<Text>
<Text dimColor>$</Text> {colorizedCommand}
</Text>
{explanation && (
<>
<Text bold color="yellow">
Explanation
</Text>
{explanation.split("\n").map((line, i) => {
// Apply different styling to headings (numbered items)
if (line.match(/^\d+\.\s+/)) {
return (
<Text key={i} bold color="cyan">
{line}
</Text>
);
} else if (line.match(/^\s*\*\s+/)) {
// Style bullet points
return (
<Text key={i} color="magenta">
{line}
</Text>
);
} else if (line.match(/^(WARNING|CAUTION|NOTE):/i)) {
// Style warnings
return (
<Text key={i} bold color="red">
{line}
</Text>
);
} else {
return <Text key={i}>{line}</Text>;
}
})}
</>
)}
</>
);
}
export function TerminalChatToolCallApplyPatch({
commandForDisplay,
patch,
}: {
commandForDisplay: string;
patch: string;
}): React.ReactElement {
const ops = React.useMemo(() => parseApplyPatch(patch), [patch]);
const firstOp = ops?.[0];
const title = React.useMemo(() => {
if (!firstOp) {
return "";
}
return capitalize(firstOp.type);
}, [firstOp]);
const filePath = React.useMemo(() => {
if (!firstOp) {
return "";
}
return shortenPath(firstOp.path || ".");
}, [firstOp]);
if (ops == null) {
return (
<>
<Text bold color="red">
Invalid Patch
</Text>
<Text color="red" dimColor>
The provided patch command is invalid.
</Text>
<Text dimColor>{commandForDisplay}</Text>
</>
);
}
if (!firstOp) {
return (
<>
<Text bold color="yellow">
Empty Patch
</Text>
<Text color="yellow" dimColor>
No operations found in the patch command.
</Text>
<Text dimColor>{commandForDisplay}</Text>
</>
);
}
return (
<>
<Text>
<Text bold>{title}</Text> <Text dimColor>{filePath}</Text>
</Text>
<Text>
<Text dimColor>$</Text> {commandForDisplay}
</Text>
</>
);
}
const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);