Checkpoint

This commit is contained in:
Matt Rubens 2025-06-27 23:53:42 -04:00
parent 719d661478
commit 1d9d40085b
11 changed files with 428 additions and 77 deletions

View file

@ -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>

View file

@ -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") {

View file

@ -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>`
}

View file

@ -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 })

View file

@ -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

View file

@ -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()

View file

@ -31,6 +31,7 @@ export interface WebviewMessage {
| "getListApiConfiguration"
| "customInstructions"
| "allowedCommands"
| "alwaysAllowCommand"
| "alwaysAllowReadOnly"
| "alwaysAllowReadOnlyOutsideWorkspace"
| "alwaysAllowWrite"

View file

@ -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 {

View file

@ -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":

View file

@ -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>
)}
</>

View file

@ -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} />