Files
codex/codex-cli/src/hooks/use-confirmation.ts
T
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

69 lines
2.0 KiB
TypeScript

// use-confirmation.ts
import type { ReviewDecision } from "../utils/agent/review";
import type React from "react";
import { useState, useCallback, useRef } from "react";
type ConfirmationResult = {
decision: ReviewDecision;
customDenyMessage?: string;
};
type ConfirmationItem = {
prompt: React.ReactNode;
resolve: (result: ConfirmationResult) => void;
explanation?: string;
};
export function useConfirmation(): {
submitConfirmation: (result: ConfirmationResult) => void;
requestConfirmation: (
prompt: React.ReactNode,
explanation?: string,
) => Promise<ConfirmationResult>;
confirmationPrompt: React.ReactNode | null;
explanation?: string;
} {
// The current prompt is just the head of the queue
const [current, setCurrent] = useState<ConfirmationItem | null>(null);
// The entire queue is stored in a ref to avoid re-renders
const queueRef = useRef<Array<ConfirmationItem>>([]);
// Move queue forward to the next prompt
const advanceQueue = useCallback(() => {
const next = queueRef.current.shift() ?? null;
setCurrent(next);
}, []);
// Called whenever someone wants a confirmation
const requestConfirmation = useCallback(
(prompt: React.ReactNode, explanation?: string) => {
return new Promise<ConfirmationResult>((resolve) => {
const wasEmpty = queueRef.current.length === 0;
queueRef.current.push({ prompt, resolve, explanation });
// If the queue was empty, we need to kick off the first prompt
if (wasEmpty) {
advanceQueue();
}
});
},
[advanceQueue],
);
// Called whenever user picks Yes / No
const submitConfirmation = (result: ConfirmationResult) => {
if (current) {
current.resolve(result);
advanceQueue();
}
};
return {
confirmationPrompt: current?.prompt, // the prompt to render now
explanation: current?.explanation, // the explanation to render if available
requestConfirmation,
submitConfirmation,
};
}