mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
Checkpoint
This commit is contained in:
parent
719d661478
commit
1d9d40085b
11 changed files with 428 additions and 77 deletions
|
|
@ -154,6 +154,7 @@ export const clineMessageSchema = z.object({
|
|||
progressStatus: toolProgressStatusSchema.optional(),
|
||||
contextCondense: contextCondenseSchema.optional(),
|
||||
isProtected: z.boolean().optional(),
|
||||
commandPrefix: z.string().optional(),
|
||||
})
|
||||
|
||||
export type ClineMessage = z.infer<typeof clineMessageSchema>
|
||||
|
|
|
|||
|
|
@ -262,6 +262,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
partialMessage?: string,
|
||||
progressStatus?: ToolProgressStatus,
|
||||
isProtected?: boolean,
|
||||
options?: { prefix?: string },
|
||||
) => {
|
||||
const { response, text, images } = await cline.ask(
|
||||
type,
|
||||
|
|
@ -269,6 +270,7 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
false,
|
||||
progressStatus,
|
||||
isProtected || false,
|
||||
options?.prefix,
|
||||
)
|
||||
|
||||
if (response !== "yesButtonClicked") {
|
||||
|
|
|
|||
|
|
@ -6,20 +6,167 @@ Description: Request to execute a CLI command on the system. Use this when you n
|
|||
Parameters:
|
||||
- command: (required) The CLI command to execute. This should be valid for the current operating system. Ensure the command is properly formatted and does not contain any harmful instructions.
|
||||
- cwd: (optional) The working directory to execute the command in (default: ${args.cwd})
|
||||
- prefix: (optional) The command prefix extracted from the command that represents a logical, safe action. This should capture the specific intent of the command rather than just the executable name. For example, use "npm install" for package installation, "git status" for git operations, "docker build" for Docker builds. Avoid generic prefixes like "python" or "node" that could be used for various purposes including malicious ones. For chained commands (using &&, ||, ;, | etc.), do not provide a prefix as these are too complex for safe auto-approval. The prefix should represent a category of single commands that users would feel safe auto-approving.
|
||||
Usage:
|
||||
<execute_command>
|
||||
<command>Your command here</command>
|
||||
<prefix>Command prefix here</prefix>
|
||||
<cwd>Working directory path (optional)</cwd>
|
||||
</execute_command>
|
||||
|
||||
Example: Requesting to execute npm run dev
|
||||
Example: Requesting to execute npm test
|
||||
<execute_command>
|
||||
<command>npm run dev</command>
|
||||
<command>npm test</command>
|
||||
<prefix>npm test</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Requesting to execute git status
|
||||
<execute_command>
|
||||
<command>git status</command>
|
||||
<prefix>git status</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Requesting to execute ls in a specific directory if directed
|
||||
<execute_command>
|
||||
<command>ls -la</command>
|
||||
<prefix>ls</prefix>
|
||||
<cwd>/home/user/projects</cwd>
|
||||
</execute_command>
|
||||
|
||||
Example: NPM package installation
|
||||
<execute_command>
|
||||
<command>npm install express</command>
|
||||
<prefix>npm install</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: NPM script execution
|
||||
<execute_command>
|
||||
<command>npm run build</command>
|
||||
<prefix>npm run</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Yarn package installation
|
||||
<execute_command>
|
||||
<command>yarn add typescript</command>
|
||||
<prefix>yarn add</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Git status check
|
||||
<execute_command>
|
||||
<command>git diff --cached</command>
|
||||
<prefix>git diff</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Git log viewing
|
||||
<execute_command>
|
||||
<command>git log --oneline</command>
|
||||
<prefix>git log</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Git branch operations
|
||||
<execute_command>
|
||||
<command>git checkout -b feature-branch</command>
|
||||
<prefix>git checkout</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Docker build
|
||||
<execute_command>
|
||||
<command>docker build -t myapp .</command>
|
||||
<prefix>docker build</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Docker container listing
|
||||
<execute_command>
|
||||
<command>docker ps -a</command>
|
||||
<prefix>docker ps</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Cargo testing
|
||||
<execute_command>
|
||||
<command>cargo test --release</command>
|
||||
<prefix>cargo test</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Cargo building
|
||||
<execute_command>
|
||||
<command>cargo build --release</command>
|
||||
<prefix>cargo build</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Go testing
|
||||
<execute_command>
|
||||
<command>go test ./...</command>
|
||||
<prefix>go test</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Go module management
|
||||
<execute_command>
|
||||
<command>go mod tidy</command>
|
||||
<prefix>go mod</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Maven clean
|
||||
<execute_command>
|
||||
<command>mvn clean compile</command>
|
||||
<prefix>mvn clean</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Maven testing
|
||||
<execute_command>
|
||||
<command>mvn test</command>
|
||||
<prefix>mvn test</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Pip package installation
|
||||
<execute_command>
|
||||
<command>pip install -r requirements.txt</command>
|
||||
<prefix>pip install</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: File listing
|
||||
<execute_command>
|
||||
<command>ls -la src/</command>
|
||||
<prefix>ls</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Directory creation
|
||||
<execute_command>
|
||||
<command>mkdir -p build/output</command>
|
||||
<prefix>mkdir</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: File copying
|
||||
<execute_command>
|
||||
<command>cp config.example.json config.json</command>
|
||||
<prefix>cp</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: File moving
|
||||
<execute_command>
|
||||
<command>mv old-name.txt new-name.txt</command>
|
||||
<prefix>mv</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Text search
|
||||
<execute_command>
|
||||
<command>grep -r "TODO" src/</command>
|
||||
<prefix>grep</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: File search
|
||||
<execute_command>
|
||||
<command>find . -name "*.ts" -type f</command>
|
||||
<prefix>find</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: File permissions
|
||||
<execute_command>
|
||||
<command>chmod +x build.sh</command>
|
||||
<prefix>chmod</prefix>
|
||||
</execute_command>
|
||||
|
||||
Example: Chained command (no prefix provided for safety)
|
||||
<execute_command>
|
||||
<command>npm run build && npm run test</command>
|
||||
</execute_command>`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -418,6 +418,7 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
partial?: boolean,
|
||||
progressStatus?: ToolProgressStatus,
|
||||
isProtected?: boolean,
|
||||
commandPrefix?: string,
|
||||
): Promise<{ response: ClineAskResponse; text?: string; images?: string[] }> {
|
||||
// If this Cline instance was aborted by the provider, then the only
|
||||
// thing keeping us alive is a promise still running in the background,
|
||||
|
|
@ -446,6 +447,7 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
lastMessage.partial = partial
|
||||
lastMessage.progressStatus = progressStatus
|
||||
lastMessage.isProtected = isProtected
|
||||
lastMessage.commandPrefix = commandPrefix
|
||||
// TODO: Be more efficient about saving and posting only new
|
||||
// data or one whole message at a time so ignore partial for
|
||||
// saves, and only post parts of partial message instead of
|
||||
|
|
@ -457,7 +459,15 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
// state.
|
||||
askTs = Date.now()
|
||||
this.lastMessageTs = askTs
|
||||
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, partial, isProtected })
|
||||
await this.addToClineMessages({
|
||||
ts: askTs,
|
||||
type: "ask",
|
||||
ask: type,
|
||||
text,
|
||||
partial,
|
||||
isProtected,
|
||||
commandPrefix,
|
||||
})
|
||||
throw new Error("Current ask promise was ignored (#2)")
|
||||
}
|
||||
} else {
|
||||
|
|
@ -485,6 +495,7 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
lastMessage.partial = false
|
||||
lastMessage.progressStatus = progressStatus
|
||||
lastMessage.isProtected = isProtected
|
||||
lastMessage.commandPrefix = commandPrefix
|
||||
await this.saveClineMessages()
|
||||
this.updateClineMessage(lastMessage)
|
||||
} else {
|
||||
|
|
@ -494,7 +505,14 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
this.askResponseImages = undefined
|
||||
askTs = Date.now()
|
||||
this.lastMessageTs = askTs
|
||||
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
|
||||
await this.addToClineMessages({
|
||||
ts: askTs,
|
||||
type: "ask",
|
||||
ask: type,
|
||||
text,
|
||||
isProtected,
|
||||
commandPrefix,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -504,7 +522,7 @@ export class Task extends EventEmitter<ClineEvents> {
|
|||
this.askResponseImages = undefined
|
||||
askTs = Date.now()
|
||||
this.lastMessageTs = askTs
|
||||
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected })
|
||||
await this.addToClineMessages({ ts: askTs, type: "ask", ask: type, text, isProtected, commandPrefix })
|
||||
}
|
||||
|
||||
await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 })
|
||||
|
|
|
|||
|
|
@ -51,7 +51,8 @@ export async function executeCommandTool(
|
|||
cline.consecutiveMistakeCount = 0
|
||||
|
||||
command = unescapeHtmlEntities(command) // Unescape HTML entities.
|
||||
const didApprove = await askApproval("command", command)
|
||||
const prefix = block.params.prefix || ""
|
||||
const didApprove = await askApproval("command", command, undefined, false, { prefix })
|
||||
|
||||
if (!didApprove) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -576,6 +576,30 @@ export const webviewMessageHandler = async (
|
|||
|
||||
break
|
||||
}
|
||||
case "alwaysAllowCommand": {
|
||||
// Add a command prefix to the allowed commands list
|
||||
const commandPrefix = message.text?.trim()
|
||||
if (commandPrefix) {
|
||||
const currentCommands = getGlobalState("allowedCommands") ?? []
|
||||
const updatedCommands = [...currentCommands]
|
||||
|
||||
// Only add if not already present
|
||||
if (!updatedCommands.includes(commandPrefix)) {
|
||||
updatedCommands.push(commandPrefix)
|
||||
|
||||
await updateGlobalState("allowedCommands", updatedCommands)
|
||||
|
||||
// Also update workspace settings
|
||||
await vscode.workspace
|
||||
.getConfiguration(Package.name)
|
||||
.update("allowedCommands", updatedCommands, vscode.ConfigurationTarget.Global)
|
||||
|
||||
// Update the webview state
|
||||
await provider.postStateToWebview()
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case "openCustomModesSettings": {
|
||||
const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export interface WebviewMessage {
|
|||
| "getListApiConfiguration"
|
||||
| "customInstructions"
|
||||
| "allowedCommands"
|
||||
| "alwaysAllowCommand"
|
||||
| "alwaysAllowReadOnly"
|
||||
| "alwaysAllowReadOnlyOutsideWorkspace"
|
||||
| "alwaysAllowWrite"
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ export type AskApproval = (
|
|||
partialMessage?: string,
|
||||
progressStatus?: ToolProgressStatus,
|
||||
forceApproval?: boolean,
|
||||
options?: { prefix?: string },
|
||||
) => Promise<boolean>
|
||||
|
||||
export type HandleError = (action: string, error: Error) => Promise<void>
|
||||
|
|
@ -64,6 +65,7 @@ export const toolParamNames = [
|
|||
"end_line",
|
||||
"query",
|
||||
"args",
|
||||
"prefix",
|
||||
] as const
|
||||
|
||||
export type ToolParamName = (typeof toolParamNames)[number]
|
||||
|
|
@ -79,7 +81,7 @@ export interface ToolUse {
|
|||
export interface ExecuteCommandToolUse extends ToolUse {
|
||||
name: "execute_command"
|
||||
// Pick<Record<ToolParamName, string>, "command"> makes "command" required, but Partial<> makes it optional
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd">>
|
||||
params: Partial<Pick<Record<ToolParamName, string>, "command" | "cwd" | "prefix">>
|
||||
}
|
||||
|
||||
export interface ReadFileToolUse extends ToolUse {
|
||||
|
|
|
|||
|
|
@ -50,21 +50,55 @@ interface ChatRowProps {
|
|||
onHeightChange: (isTaller: boolean) => void
|
||||
onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void
|
||||
onBatchFileResponse?: (response: { [key: string]: boolean }) => void
|
||||
alwaysAllowChecked?: boolean
|
||||
onAlwaysAllowChange?: (checked: boolean, commandPrefix?: string) => void
|
||||
allowedCommands?: string[]
|
||||
isAskPending?: boolean
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
interface ChatRowContentProps extends Omit<ChatRowProps, "onHeightChange"> {}
|
||||
interface ChatRowContentProps {
|
||||
message: ClineMessage
|
||||
lastModifiedMessage?: ClineMessage
|
||||
isExpanded: boolean
|
||||
isLast: boolean
|
||||
isStreaming: boolean
|
||||
onToggleExpand: (ts: number) => void
|
||||
onSuggestionClick?: (answer: string, event?: React.MouseEvent) => void
|
||||
onBatchFileResponse?: (response: { [key: string]: boolean }) => void
|
||||
commandProps?: {
|
||||
alwaysAllowChecked?: boolean
|
||||
onAlwaysAllowChange?: (checked: boolean, commandPrefix?: string) => void
|
||||
allowedCommands?: string[]
|
||||
isAskPending?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
const ChatRow = memo(
|
||||
(props: ChatRowProps) => {
|
||||
const { isLast, onHeightChange, message } = props
|
||||
const {
|
||||
isLast,
|
||||
onHeightChange,
|
||||
message,
|
||||
alwaysAllowChecked,
|
||||
onAlwaysAllowChange,
|
||||
allowedCommands,
|
||||
isAskPending,
|
||||
} = props
|
||||
// Store the previous height to compare with the current height
|
||||
// This allows us to detect changes without causing re-renders
|
||||
const prevHeightRef = useRef(0)
|
||||
|
||||
const [chatrow, { height }] = useSize(
|
||||
<div className="px-[15px] py-[10px] pr-[6px]">
|
||||
<ChatRowContent {...props} />
|
||||
<ChatRowContent
|
||||
{...props}
|
||||
commandProps={{
|
||||
alwaysAllowChecked,
|
||||
onAlwaysAllowChange,
|
||||
allowedCommands,
|
||||
isAskPending,
|
||||
}}
|
||||
/>
|
||||
</div>,
|
||||
)
|
||||
|
||||
|
|
@ -99,6 +133,7 @@ export const ChatRowContent = ({
|
|||
onToggleExpand,
|
||||
onSuggestionClick,
|
||||
onBatchFileResponse,
|
||||
commandProps,
|
||||
}: ChatRowContentProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { mcpServers, alwaysAllowMcp, currentCheckpoint } = useExtensionState()
|
||||
|
|
@ -1120,6 +1155,13 @@ export const ChatRowContent = ({
|
|||
text={message.text}
|
||||
icon={icon}
|
||||
title={title}
|
||||
message={message}
|
||||
alwaysAllowChecked={commandProps?.alwaysAllowChecked}
|
||||
onAlwaysAllowChange={(checked: boolean) =>
|
||||
commandProps?.onAlwaysAllowChange?.(checked, message.commandPrefix)
|
||||
}
|
||||
allowedCommands={commandProps?.allowedCommands}
|
||||
isAskPending={commandProps?.isAskPending}
|
||||
/>
|
||||
)
|
||||
case "use_mcp_server":
|
||||
|
|
|
|||
|
|
@ -140,6 +140,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
|
||||
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
|
||||
const [didClickCancel, setDidClickCancel] = useState(false)
|
||||
const [alwaysAllowChecked, setAlwaysAllowChecked] = useState(false)
|
||||
const [commandPrefixToAllow, setCommandPrefixToAllow] = useState<string | undefined>(undefined)
|
||||
const virtuosoRef = useRef<VirtuosoHandle>(null)
|
||||
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
|
||||
const prevExpandedRowsRef = useRef<Record<number, boolean>>()
|
||||
|
|
@ -313,6 +315,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setEnableButtons(!isPartial)
|
||||
setPrimaryButtonText(t("chat:runCommand.title"))
|
||||
setSecondaryButtonText(t("chat:reject.title"))
|
||||
setAlwaysAllowChecked(false) // Reset checkbox for new command
|
||||
setCommandPrefixToAllow(undefined) // Reset stored command prefix for new command
|
||||
break
|
||||
case "command_output":
|
||||
setSendingDisabled(false)
|
||||
|
|
@ -560,6 +564,14 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
case "use_mcp_server":
|
||||
case "resume_task":
|
||||
case "mistake_limit_reached":
|
||||
// Handle "always allow" for commands
|
||||
if (clineAsk === "command" && alwaysAllowChecked && commandPrefixToAllow) {
|
||||
vscode.postMessage({
|
||||
type: "alwaysAllowCommand",
|
||||
text: commandPrefixToAllow,
|
||||
})
|
||||
}
|
||||
|
||||
// Only send text/images if they exist
|
||||
if (trimmedInput || (images && images.length > 0)) {
|
||||
vscode.postMessage({
|
||||
|
|
@ -574,6 +586,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
// Clear input state after sending
|
||||
setInputValue("")
|
||||
setSelectedImages([])
|
||||
setAlwaysAllowChecked(false) // Reset checkbox after use
|
||||
setCommandPrefixToAllow(undefined) // Reset stored command prefix
|
||||
break
|
||||
case "completion_result":
|
||||
case "resume_completed_task":
|
||||
|
|
@ -589,7 +603,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
setClineAsk(undefined)
|
||||
setEnableButtons(false)
|
||||
},
|
||||
[clineAsk, startNewTask],
|
||||
[clineAsk, startNewTask, alwaysAllowChecked, commandPrefixToAllow],
|
||||
)
|
||||
|
||||
const handleSecondaryButtonClick = useCallback(
|
||||
|
|
@ -1241,6 +1255,29 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
isStreaming={isStreaming}
|
||||
onSuggestionClick={handleSuggestionClickInRow} // This was already stabilized
|
||||
onBatchFileResponse={handleBatchFileResponse}
|
||||
alwaysAllowChecked={alwaysAllowChecked}
|
||||
onAlwaysAllowChange={(checked: boolean, commandPrefix?: string) => {
|
||||
setAlwaysAllowChecked(checked)
|
||||
setCommandPrefixToAllow(checked ? commandPrefix : undefined)
|
||||
|
||||
// Handle immediate addition/removal of command from allowed list
|
||||
if (commandPrefix) {
|
||||
if (checked) {
|
||||
// Add command - this is handled by the CommandExecution component
|
||||
// No need to do anything here as the message is already sent
|
||||
} else {
|
||||
// Remove command from allowed list
|
||||
const currentCommands = allowedCommands || []
|
||||
const updatedCommands = currentCommands.filter((cmd) => cmd !== commandPrefix)
|
||||
vscode.postMessage({
|
||||
type: "allowedCommands",
|
||||
commands: updatedCommands,
|
||||
})
|
||||
}
|
||||
}
|
||||
}}
|
||||
allowedCommands={allowedCommands}
|
||||
isAskPending={clineAsk === "command"}
|
||||
/>
|
||||
)
|
||||
},
|
||||
|
|
@ -1253,6 +1290,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
isStreaming,
|
||||
handleSuggestionClickInRow,
|
||||
handleBatchFileResponse,
|
||||
alwaysAllowChecked,
|
||||
allowedCommands,
|
||||
clineAsk,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -1489,69 +1529,74 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
</StandardTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`flex ${
|
||||
primaryButtonText || secondaryButtonText || isStreaming ? "px-[15px] pt-[10px]" : "p-0"
|
||||
} ${
|
||||
primaryButtonText || secondaryButtonText || isStreaming
|
||||
? enableButtons || (isStreaming && !didClickCancel)
|
||||
? "opacity-100"
|
||||
: "opacity-50"
|
||||
: "opacity-0"
|
||||
}`}>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
primaryButtonText === t("chat:retry.title")
|
||||
? t("chat:retry.tooltip")
|
||||
: primaryButtonText === t("chat:save.title")
|
||||
? t("chat:save.tooltip")
|
||||
: primaryButtonText === t("chat:approve.title")
|
||||
? t("chat:approve.tooltip")
|
||||
: primaryButtonText === t("chat:runCommand.title")
|
||||
? t("chat:runCommand.tooltip")
|
||||
: primaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: primaryButtonText === t("chat:resumeTask.title")
|
||||
? t("chat:resumeTask.tooltip")
|
||||
: primaryButtonText === t("chat:proceedAnyways.title")
|
||||
? t("chat:proceedAnyways.tooltip")
|
||||
<div className="flex flex-col">
|
||||
<div
|
||||
className={`flex ${
|
||||
primaryButtonText || secondaryButtonText || isStreaming
|
||||
? "px-[15px] pt-[10px]"
|
||||
: "p-0"
|
||||
} ${
|
||||
primaryButtonText || secondaryButtonText || isStreaming
|
||||
? enableButtons || (isStreaming && !didClickCancel)
|
||||
? "opacity-100"
|
||||
: "opacity-50"
|
||||
: "opacity-0"
|
||||
}`}>
|
||||
{primaryButtonText && !isStreaming && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
primaryButtonText === t("chat:retry.title")
|
||||
? t("chat:retry.tooltip")
|
||||
: primaryButtonText === t("chat:save.title")
|
||||
? t("chat:save.tooltip")
|
||||
: primaryButtonText === t("chat:approve.title")
|
||||
? t("chat:approve.tooltip")
|
||||
: primaryButtonText === t("chat:runCommand.title")
|
||||
? t("chat:runCommand.tooltip")
|
||||
: primaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: primaryButtonText === t("chat:resumeTask.title")
|
||||
? t("chat:resumeTask.tooltip")
|
||||
: primaryButtonText ===
|
||||
t("chat:proceedWhileRunning.title")
|
||||
? t("chat:proceedWhileRunning.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{(secondaryButtonText || isStreaming) && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
isStreaming
|
||||
? t("chat:cancel.tooltip")
|
||||
: secondaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: secondaryButtonText === t("chat:reject.title")
|
||||
? t("chat:reject.tooltip")
|
||||
: secondaryButtonText === t("chat:terminate.title")
|
||||
? t("chat:terminate.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
|
||||
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
t("chat:proceedAnyways.title")
|
||||
? t("chat:proceedAnyways.tooltip")
|
||||
: primaryButtonText ===
|
||||
t("chat:proceedWhileRunning.title")
|
||||
? t("chat:proceedWhileRunning.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
|
||||
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
{(secondaryButtonText || isStreaming) && (
|
||||
<StandardTooltip
|
||||
content={
|
||||
isStreaming
|
||||
? t("chat:cancel.tooltip")
|
||||
: secondaryButtonText === t("chat:startNewTask.title")
|
||||
? t("chat:startNewTask.tooltip")
|
||||
: secondaryButtonText === t("chat:reject.title")
|
||||
? t("chat:reject.tooltip")
|
||||
: secondaryButtonText === t("chat:terminate.title")
|
||||
? t("chat:terminate.tooltip")
|
||||
: undefined
|
||||
}>
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
|
||||
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
|
||||
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
|
||||
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
</StandardTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { useCallback, useState, memo, useMemo } from "react"
|
||||
import { useEvent } from "react-use"
|
||||
import { ChevronDown, Skull } from "lucide-react"
|
||||
import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { CommandExecutionStatus, commandExecutionStatusSchema } from "@roo-code/types"
|
||||
import { CommandExecutionStatus, commandExecutionStatusSchema, ClineMessage } from "@roo-code/types"
|
||||
|
||||
import { ExtensionMessage } from "@roo/ExtensionMessage"
|
||||
import { safeJsonParse } from "@roo/safeJsonParse"
|
||||
|
|
@ -19,9 +20,24 @@ interface CommandExecutionProps {
|
|||
text?: string
|
||||
icon?: JSX.Element | null
|
||||
title?: JSX.Element | null
|
||||
message?: ClineMessage
|
||||
onAlwaysAllowChange?: (checked: boolean, commandPrefix?: string) => void
|
||||
alwaysAllowChecked?: boolean
|
||||
allowedCommands?: string[]
|
||||
isAskPending?: boolean
|
||||
}
|
||||
|
||||
export const CommandExecution = ({ executionId, text, icon, title }: CommandExecutionProps) => {
|
||||
export const CommandExecution = ({
|
||||
executionId,
|
||||
text,
|
||||
icon,
|
||||
title,
|
||||
message,
|
||||
onAlwaysAllowChange,
|
||||
alwaysAllowChecked = false,
|
||||
allowedCommands = [],
|
||||
isAskPending = false,
|
||||
}: CommandExecutionProps) => {
|
||||
const { terminalShellIntegrationDisabled = false } = useExtensionState()
|
||||
|
||||
const { command, output: parsedOutput } = useMemo(() => parseCommandAndOutput(text), [text])
|
||||
|
|
@ -31,6 +47,8 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
const [isExpanded, setIsExpanded] = useState(terminalShellIntegrationDisabled)
|
||||
const [streamingOutput, setStreamingOutput] = useState("")
|
||||
const [status, setStatus] = useState<CommandExecutionStatus | null>(null)
|
||||
// Track if the user has clicked "always allow" for this command to optimistically hide the checkbox
|
||||
const [hasClickedAlwaysAllow, setHasClickedAlwaysAllow] = useState(false)
|
||||
|
||||
// The command's output can either come from the text associated with the
|
||||
// task message (this is the case for completed commands) or from the
|
||||
|
|
@ -82,6 +100,57 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
</div>
|
||||
<div className="flex flex-row items-center justify-between gap-2 px-1">
|
||||
<div className="flex flex-row items-center gap-1">
|
||||
{message?.commandPrefix &&
|
||||
onAlwaysAllowChange &&
|
||||
isAskPending &&
|
||||
!allowedCommands.includes(message.commandPrefix) &&
|
||||
!hasClickedAlwaysAllow && (
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={alwaysAllowChecked}
|
||||
onChange={(e) => {
|
||||
const checked = (e.target as HTMLInputElement).checked
|
||||
const commandPrefix = message.commandPrefix
|
||||
|
||||
// Send message immediately when checkbox is toggled
|
||||
if (checked && commandPrefix) {
|
||||
vscode.postMessage({
|
||||
type: "alwaysAllowCommand",
|
||||
text: commandPrefix,
|
||||
})
|
||||
// Optimistically hide the checkbox
|
||||
setHasClickedAlwaysAllow(true)
|
||||
}
|
||||
|
||||
// Also call the callback for UI state management
|
||||
// The callback will handle removal when unchecked
|
||||
onAlwaysAllowChange(checked, commandPrefix)
|
||||
}}
|
||||
/>
|
||||
<label
|
||||
className="text-sm text-vscode-descriptionForeground cursor-pointer"
|
||||
onClick={() => {
|
||||
const newChecked = !alwaysAllowChecked
|
||||
const commandPrefix = message.commandPrefix
|
||||
|
||||
// Send message immediately when label is clicked
|
||||
if (newChecked && commandPrefix) {
|
||||
vscode.postMessage({
|
||||
type: "alwaysAllowCommand",
|
||||
text: commandPrefix,
|
||||
})
|
||||
// Optimistically hide the checkbox
|
||||
setHasClickedAlwaysAllow(true)
|
||||
}
|
||||
|
||||
// Also call the callback for UI state management
|
||||
// The callback will handle removal when unchecked
|
||||
onAlwaysAllowChange(newChecked, commandPrefix)
|
||||
}}>
|
||||
Always allow <code>{message.commandPrefix}</code>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
{status?.status === "started" && (
|
||||
<div className="flex flex-row items-center gap-2 font-mono text-xs">
|
||||
<div className="rounded-full size-1.5 bg-lime-400" />
|
||||
|
|
@ -120,7 +189,6 @@ export const CommandExecution = ({ executionId, text, icon, title }: CommandExec
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-vscode-editor-background border border-vscode-border rounded-xs p-2">
|
||||
<CodeBlock source={command} language="shell" />
|
||||
<OutputContainer isExpanded={isExpanded} output={output} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue