Skip to content

Commit 2ff969a

Browse files
committed
feat: redesign command approval UI with separate whitelist functionality
- Remove three-button layout (Run, Add & Run, Reject) - Implement two-row design with Run Command/Reject buttons on top - Add pattern display and 'Always allow' button on bottom row - 'Always allow' only whitelists the pattern without running it - Remove addAndRunButtonClicked handling from backend - Add new addToWhitelist message type and handler - Update translations for new UI elements - Remove obsolete tests for old Add & Run functionality
1 parent 53bb163 commit 2ff969a

File tree

6 files changed

+105
-523
lines changed

6 files changed

+105
-523
lines changed

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -274,41 +274,6 @@ export async function presentAssistantMessage(cline: Task) {
274274
isProtected || false,
275275
)
276276

277-
// Handle "Add & Run" button for command approval
278-
if (response === "addAndRunButtonClicked" && type === "command") {
279-
// The text field contains the extracted command pattern when "Add & Run" is clicked
280-
if (text) {
281-
// Add the command pattern to allowed commands
282-
const provider = cline.providerRef.deref()
283-
if (provider) {
284-
// Get current allowed commands
285-
const currentState = await provider.getState()
286-
const currentAllowedCommands = currentState.allowedCommands || []
287-
288-
// Add the new pattern if it's not already in the list
289-
if (!currentAllowedCommands.includes(text)) {
290-
const updatedCommands = [...currentAllowedCommands, text]
291-
292-
// Update global state using contextProxy
293-
await provider.contextProxy.setValue("allowedCommands", updatedCommands)
294-
295-
// Also update workspace settings
296-
const vscode = await import("vscode")
297-
const { Package } = await import("../../shared/package")
298-
await vscode.workspace
299-
.getConfiguration(Package.name)
300-
.update("allowedCommands", updatedCommands, vscode.ConfigurationTarget.Global)
301-
302-
// Post state update to webview
303-
await provider.postStateToWebview()
304-
}
305-
}
306-
}
307-
308-
// Return true to indicate approval and continue with command execution
309-
return true
310-
}
311-
312277
if (response !== "yesButtonClicked") {
313278
// Handle both messageResponse and noButtonClicked with text.
314279
if (text) {

src/core/webview/webviewMessageHandler.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,30 @@ export const webviewMessageHandler = async (
587587

588588
break
589589
}
590+
case "addToWhitelist": {
591+
// Handle adding a command pattern to the whitelist without running it
592+
if (message.pattern) {
593+
// Get current allowed commands
594+
const currentAllowedCommands = getGlobalState("allowedCommands") || []
595+
596+
// Add the new pattern if it's not already in the list
597+
if (!currentAllowedCommands.includes(message.pattern)) {
598+
const updatedCommands = [...currentAllowedCommands, message.pattern]
599+
600+
// Update global state
601+
await updateGlobalState("allowedCommands", updatedCommands)
602+
603+
// Also update workspace settings
604+
await vscode.workspace
605+
.getConfiguration(Package.name)
606+
.update("allowedCommands", updatedCommands, vscode.ConfigurationTarget.Global)
607+
608+
// Post state update to webview
609+
await provider.postStateToWebview()
610+
}
611+
}
612+
break
613+
}
590614
case "openCustomModesSettings": {
591615
const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()
592616

src/shared/WebviewMessage.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,7 @@ export interface WebviewMessage {
197197
| "checkRulesDirectoryResult"
198198
| "saveCodeIndexSettingsAtomic"
199199
| "requestCodeIndexSecretStatus"
200+
| "addToWhitelist"
200201
text?: string
201202
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
202203
disabled?: boolean
@@ -238,6 +239,7 @@ export interface WebviewMessage {
238239
hasContent?: boolean // For checkRulesDirectoryResult
239240
checkOnly?: boolean // For deleteCustomMode check
240241
commandPattern?: string // For "Add & Run" button - the extracted command pattern to whitelist
242+
pattern?: string // For "addToWhitelist" message - the command pattern to add to whitelist
241243
codeIndexSettings?: {
242244
// Global state settings
243245
codebaseIndexEnabled: boolean

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 75 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
330330
setClineAsk("command")
331331
setEnableButtons(!isPartial)
332332
setPrimaryButtonText(t("chat:runCommand.title"))
333-
setSecondaryButtonText(t("chat:addAndRunCommand.title"))
334-
setTertiaryButtonText(t("chat:reject.title"))
333+
setSecondaryButtonText(t("chat:reject.title"))
334+
setTertiaryButtonText(undefined)
335335
break
336336
case "command_output":
337337
setSendingDisabled(false)
@@ -615,38 +615,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
615615
[clineAsk, startNewTask],
616616
)
617617

618-
const handleTertiaryButtonClick = useCallback(
619-
(_text?: string, images?: string[]) => {
620-
switch (clineAsk) {
621-
case "command":
622-
// For the "Add & Run" button on command approval
623-
// Extract the command pattern for whitelisting
624-
const commandMessage = findLast(
625-
messagesRef.current,
626-
(msg) => msg.type === "ask" && msg.ask === "command",
627-
)
628-
const commandText = commandMessage?.text || ""
629-
const pattern = extractCommandPattern(commandText)
630-
631-
// Send the pattern in the text field as expected by the backend
632-
vscode.postMessage({
633-
type: "askResponse",
634-
askResponse: "addAndRunButtonClicked",
635-
text: pattern, // Send pattern in text field
636-
images: images || [],
637-
})
638-
// Clear input state after sending
639-
setInputValue("")
640-
setSelectedImages([])
641-
break
642-
}
643-
setSendingDisabled(true)
644-
setClineAsk(undefined)
645-
setEnableButtons(false)
646-
},
647-
[clineAsk], // messagesRef is stable
648-
)
649-
650618
const handleSecondaryButtonClick = useCallback(
651619
(text?: string, images?: string[]) => {
652620
const trimmedInput = text?.trim()
@@ -767,9 +735,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
767735
case "secondaryButtonClick":
768736
handleSecondaryButtonClick(message.text ?? "", message.images ?? [])
769737
break
770-
case "tertiaryButtonClick":
771-
handleTertiaryButtonClick(message.text ?? "", message.images ?? [])
772-
break
773738
}
774739
break
775740
case "condenseTaskContextResponse":
@@ -796,7 +761,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
796761
handleSetChatBoxMessage,
797762
handlePrimaryButtonClick,
798763
handleSecondaryButtonClick,
799-
handleTertiaryButtonClick,
800764
],
801765
)
802766

@@ -1693,20 +1657,47 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
16931657
</StandardTooltip>
16941658
) : (
16951659
<>
1696-
{/* Three button layout for command approval */}
1697-
{tertiaryButtonText && clineAsk === "command" && !isStreaming ? (
1660+
{/* Command approval with auto-approve pattern */}
1661+
{clineAsk === "command" && !isStreaming ? (
16981662
<div className="flex flex-col gap-[6px]">
1699-
{/* Top row: Run and Add & Run */}
1663+
{/* Top row: Run Command and Reject */}
17001664
<div className="flex gap-[6px]">
17011665
<StandardTooltip content={t("chat:runCommand.tooltip")}>
17021666
<VSCodeButton
17031667
appearance="primary"
17041668
disabled={!enableButtons}
17051669
className="flex-1"
1706-
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
1670+
onClick={() =>
1671+
handlePrimaryButtonClick(inputValue, selectedImages)
1672+
}>
17071673
{primaryButtonText}
17081674
</VSCodeButton>
17091675
</StandardTooltip>
1676+
<StandardTooltip content={t("chat:reject.tooltip")}>
1677+
<VSCodeButton
1678+
appearance="secondary"
1679+
disabled={!enableButtons}
1680+
className="flex-1"
1681+
onClick={() =>
1682+
handleSecondaryButtonClick(inputValue, selectedImages)
1683+
}>
1684+
{secondaryButtonText}
1685+
</VSCodeButton>
1686+
</StandardTooltip>
1687+
</div>
1688+
{/* Bottom row: Auto-approve pattern */}
1689+
<div className="flex items-center gap-[6px]">
1690+
<div className="flex-1 px-2 py-1 bg-vscode-input-background text-vscode-input-foreground rounded text-sm font-mono">
1691+
{(() => {
1692+
const commandMessage = findLast(
1693+
messagesRef.current,
1694+
(msg) => msg.type === "ask" && msg.ask === "command",
1695+
)
1696+
const commandText = commandMessage?.text || ""
1697+
const pattern = extractCommandPattern(commandText)
1698+
return pattern || commandText
1699+
})()}
1700+
</div>
17101701
<StandardTooltip
17111702
content={(() => {
17121703
const commandMessage = findLast(
@@ -1717,28 +1708,36 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17171708
const pattern = extractCommandPattern(commandText)
17181709
const description = getPatternDescription(pattern)
17191710
return pattern
1720-
? `${t("chat:addAndRunCommand.tooltip")} Will whitelist: "${pattern}" (${description})`
1721-
: t("chat:addAndRunCommand.tooltip")
1711+
? `${t("chat:alwaysAllow.tooltip")} Will whitelist: "${pattern}" (${description})`
1712+
: t("chat:alwaysAllow.tooltip")
17221713
})()}>
17231714
<VSCodeButton
17241715
appearance="secondary"
17251716
disabled={!enableButtons}
1726-
className="flex-1"
1727-
onClick={() => handleTertiaryButtonClick(inputValue, selectedImages)}>
1728-
{secondaryButtonText}
1717+
onClick={() => {
1718+
// Extract the command pattern
1719+
const commandMessage = findLast(
1720+
messagesRef.current,
1721+
(msg) => msg.type === "ask" && msg.ask === "command",
1722+
)
1723+
const commandText = commandMessage?.text || ""
1724+
const pattern = extractCommandPattern(commandText)
1725+
1726+
// Add to whitelist without running
1727+
vscode.postMessage({
1728+
type: "addToWhitelist",
1729+
pattern: pattern,
1730+
})
1731+
1732+
// Clear the ask state
1733+
setSendingDisabled(true)
1734+
setClineAsk(undefined)
1735+
setEnableButtons(false)
1736+
}}>
1737+
{t("chat:alwaysAllow.title")}
17291738
</VSCodeButton>
17301739
</StandardTooltip>
17311740
</div>
1732-
{/* Bottom row: Reject */}
1733-
<StandardTooltip content={t("chat:reject.tooltip")}>
1734-
<VSCodeButton
1735-
appearance="secondary"
1736-
disabled={!enableButtons}
1737-
className="w-full"
1738-
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
1739-
{tertiaryButtonText}
1740-
</VSCodeButton>
1741-
</StandardTooltip>
17421741
</div>
17431742
) : (
17441743
/* Standard two button layout */
@@ -1754,23 +1753,33 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17541753
? t("chat:approve.tooltip")
17551754
: primaryButtonText === t("chat:runCommand.title")
17561755
? t("chat:runCommand.tooltip")
1757-
: primaryButtonText === t("chat:startNewTask.title")
1756+
: primaryButtonText ===
1757+
t("chat:startNewTask.title")
17581758
? t("chat:startNewTask.tooltip")
1759-
: primaryButtonText === t("chat:resumeTask.title")
1759+
: primaryButtonText ===
1760+
t("chat:resumeTask.title")
17601761
? t("chat:resumeTask.tooltip")
17611762
: primaryButtonText ===
17621763
t("chat:proceedAnyways.title")
17631764
? t("chat:proceedAnyways.tooltip")
17641765
: primaryButtonText ===
1765-
t("chat:proceedWhileRunning.title")
1766-
? t("chat:proceedWhileRunning.tooltip")
1766+
t(
1767+
"chat:proceedWhileRunning.title",
1768+
)
1769+
? t(
1770+
"chat:proceedWhileRunning.tooltip",
1771+
)
17671772
: undefined
17681773
}>
17691774
<VSCodeButton
17701775
appearance="primary"
17711776
disabled={!enableButtons}
1772-
className={secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"}
1773-
onClick={() => handlePrimaryButtonClick(inputValue, selectedImages)}>
1777+
className={
1778+
secondaryButtonText ? "flex-1 mr-[6px]" : "flex-[2] mr-0"
1779+
}
1780+
onClick={() =>
1781+
handlePrimaryButtonClick(inputValue, selectedImages)
1782+
}>
17741783
{primaryButtonText}
17751784
</VSCodeButton>
17761785
</StandardTooltip>
@@ -1792,7 +1801,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
17921801
appearance="secondary"
17931802
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
17941803
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
1795-
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
1804+
onClick={() =>
1805+
handleSecondaryButtonClick(inputValue, selectedImages)
1806+
}>
17961807
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
17971808
</VSCodeButton>
17981809
</StandardTooltip>

0 commit comments

Comments
 (0)