mirror of
https://github.com/cline/cline.git
synced 2026-09-15 21:04:27 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32a931b0cc |
@@ -42,6 +42,8 @@ service TaskService {
|
||||
rpc deleteAllTaskHistory(EmptyRequest) returns (DeleteAllTaskHistoryCount);
|
||||
// Explains changes with AI and adds inline comments to the diff view
|
||||
rpc explainChanges(ExplainChangesRequest) returns (Empty);
|
||||
// Sets the approval policy for a specific tool
|
||||
rpc setToolApprovalPolicy(SetToolApprovalPolicyRequest) returns (Empty);
|
||||
}
|
||||
|
||||
// Request message for creating a new task
|
||||
@@ -132,3 +134,10 @@ message ExplainChangesRequest {
|
||||
// Timestamp of the completion message to explain changes for
|
||||
int64 message_ts = 2;
|
||||
}
|
||||
|
||||
// Request for setting tool approval policy
|
||||
message SetToolApprovalPolicyRequest {
|
||||
Metadata metadata = 1;
|
||||
string tool_name = 2;
|
||||
string policy = 3; // "ask_everytime" | "auto_approve" | "never_allow"
|
||||
}
|
||||
|
||||
@@ -28,6 +28,49 @@ export async function updateAutoApprovalSettings(controller: Controller, request
|
||||
},
|
||||
}
|
||||
|
||||
// Check if any action flags were disabled and reset corresponding tool policies
|
||||
if (request.actions) {
|
||||
const updatedToolPolicies = { ...currentSettings.toolPolicies }
|
||||
|
||||
// If readFiles was disabled, reset all read-related tool policies
|
||||
if (request.actions.readFiles === false) {
|
||||
updatedToolPolicies.readFile = "ask_everytime"
|
||||
updatedToolPolicies.listFilesTopLevel = "ask_everytime"
|
||||
updatedToolPolicies.listFilesRecursive = "ask_everytime"
|
||||
updatedToolPolicies.listCodeDefinitionNames = "ask_everytime"
|
||||
updatedToolPolicies.searchFiles = "ask_everytime"
|
||||
}
|
||||
|
||||
// If editFiles was disabled, reset all edit-related tool policies
|
||||
if (request.actions.editFiles === false) {
|
||||
updatedToolPolicies.editedExistingFile = "ask_everytime"
|
||||
updatedToolPolicies.newFileCreated = "ask_everytime"
|
||||
}
|
||||
|
||||
// If executeSafeCommands was disabled, reset safe command policy
|
||||
if (request.actions.executeSafeCommands === false) {
|
||||
updatedToolPolicies.executeSafeCommand = "ask_everytime"
|
||||
}
|
||||
|
||||
// If executeAllCommands was disabled, reset risky command policy
|
||||
if (request.actions.executeAllCommands === false) {
|
||||
updatedToolPolicies.executeRiskyCommand = "ask_everytime"
|
||||
}
|
||||
|
||||
// If useBrowser was disabled, reset browser policy
|
||||
if (request.actions.useBrowser === false) {
|
||||
updatedToolPolicies.useBrowser = "ask_everytime"
|
||||
}
|
||||
|
||||
// If useMcp was disabled, reset MCP policies
|
||||
if (request.actions.useMcp === false) {
|
||||
updatedToolPolicies.useMcpTool = "ask_everytime"
|
||||
updatedToolPolicies.accessMcpResource = "ask_everytime"
|
||||
}
|
||||
|
||||
settings.toolPolicies = updatedToolPolicies
|
||||
}
|
||||
|
||||
controller.stateManager.setGlobalState("autoApprovalSettings", settings)
|
||||
|
||||
await controller.postStateToWebview()
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { ToolApprovalPolicy } from "../../../shared/AutoApprovalSettings"
|
||||
import { Empty } from "../../../shared/proto/cline/common"
|
||||
import { SetToolApprovalPolicyRequest } from "../../../shared/proto/cline/task"
|
||||
import { Controller } from ".."
|
||||
|
||||
/**
|
||||
* Sets the approval policy for a specific tool
|
||||
* @param controller The controller instance
|
||||
* @param request The request containing the tool name and policy
|
||||
* @returns Empty response
|
||||
*/
|
||||
export async function setToolApprovalPolicy(controller: Controller, request: SetToolApprovalPolicyRequest): Promise<Empty> {
|
||||
const { toolName, policy } = request
|
||||
|
||||
// Validate policy value
|
||||
const validPolicies: ToolApprovalPolicy[] = ["ask_everytime", "auto_approve", "never_allow"]
|
||||
if (!validPolicies.includes(policy as ToolApprovalPolicy)) {
|
||||
throw new Error(`Invalid policy: ${policy}. Must be one of: ${validPolicies.join(", ")}`)
|
||||
}
|
||||
|
||||
// Get current auto approval settings
|
||||
const currentSettings = (await controller.getStateToPostToWebview()).autoApprovalSettings
|
||||
|
||||
// Map tool names to their corresponding action flags
|
||||
const getActionFlag = (toolName: string): keyof typeof currentSettings.actions | null => {
|
||||
switch (toolName) {
|
||||
case "readFile":
|
||||
case "listFilesTopLevel":
|
||||
case "listFilesRecursive":
|
||||
case "listCodeDefinitionNames":
|
||||
case "searchFiles":
|
||||
return "readFiles"
|
||||
case "editedExistingFile":
|
||||
case "newFileCreated":
|
||||
return "editFiles"
|
||||
case "executeSafeCommand":
|
||||
return "executeSafeCommands"
|
||||
case "executeRiskyCommand":
|
||||
return "executeAllCommands"
|
||||
case "useBrowser":
|
||||
return "useBrowser"
|
||||
case "useMcpTool":
|
||||
case "accessMcpResource":
|
||||
return "useMcp"
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Update tool policies
|
||||
const updatedSettings = {
|
||||
...currentSettings,
|
||||
toolPolicies: {
|
||||
...currentSettings.toolPolicies,
|
||||
[toolName]: policy as ToolApprovalPolicy,
|
||||
},
|
||||
version: currentSettings.version + 1, // Increment version for race condition prevention
|
||||
}
|
||||
|
||||
// If setting to auto_approve, also enable the corresponding legacy action flag
|
||||
if (policy === "auto_approve") {
|
||||
const actionFlag = getActionFlag(toolName)
|
||||
if (actionFlag) {
|
||||
updatedSettings.actions = {
|
||||
...updatedSettings.actions,
|
||||
[actionFlag]: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save to global state
|
||||
controller.stateManager.setGlobalState("autoApprovalSettings", updatedSettings)
|
||||
|
||||
// Notify all webviews of the update
|
||||
await controller.postStateToWebview()
|
||||
|
||||
return Empty.create()
|
||||
}
|
||||
@@ -64,6 +64,19 @@ export class AutoApprove {
|
||||
|
||||
const autoApprovalSettings = this.stateManager.getGlobalSettingsKey("autoApprovalSettings")
|
||||
|
||||
// Check tool-specific policies first (granular control)
|
||||
const toolPolicyKey = this.getToolPolicyKey(toolName)
|
||||
if (toolPolicyKey && autoApprovalSettings.toolPolicies) {
|
||||
const policy = autoApprovalSettings.toolPolicies[toolPolicyKey as keyof typeof autoApprovalSettings.toolPolicies]
|
||||
if (policy === "auto_approve") {
|
||||
return [true, true] // Auto-approve both local and external
|
||||
} else if (policy === "never_allow") {
|
||||
return [false, false] // Never auto-approve
|
||||
}
|
||||
// If "ask_everytime", fall through to legacy logic
|
||||
}
|
||||
|
||||
// Fall back to legacy broad category flags
|
||||
switch (toolName) {
|
||||
case ClineDefaultTool.FILE_READ:
|
||||
case ClineDefaultTool.LIST_FILES:
|
||||
@@ -92,6 +105,54 @@ export class AutoApprove {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Map ClineDefaultTool enum to toolPolicies key
|
||||
*/
|
||||
private getToolPolicyKey(
|
||||
toolName: ClineDefaultTool,
|
||||
):
|
||||
| "readFile"
|
||||
| "editedExistingFile"
|
||||
| "newFileCreated"
|
||||
| "fileDeleted"
|
||||
| "listFilesTopLevel"
|
||||
| "listFilesRecursive"
|
||||
| "listCodeDefinitionNames"
|
||||
| "searchFiles"
|
||||
| "executeSafeCommand"
|
||||
| "executeRiskyCommand"
|
||||
| "useBrowser"
|
||||
| "useMcpTool"
|
||||
| "accessMcpResource"
|
||||
| null {
|
||||
switch (toolName) {
|
||||
case ClineDefaultTool.FILE_READ:
|
||||
return "readFile"
|
||||
case ClineDefaultTool.FILE_EDIT:
|
||||
case ClineDefaultTool.APPLY_PATCH:
|
||||
return "editedExistingFile"
|
||||
case ClineDefaultTool.FILE_NEW:
|
||||
return "newFileCreated"
|
||||
case ClineDefaultTool.LIST_FILES:
|
||||
return "listFilesTopLevel" // Note: This covers both top-level and recursive
|
||||
case ClineDefaultTool.LIST_CODE_DEF:
|
||||
return "listCodeDefinitionNames"
|
||||
case ClineDefaultTool.SEARCH:
|
||||
return "searchFiles"
|
||||
case ClineDefaultTool.BASH:
|
||||
// Note: BASH tool returns tuple [safe, risky], so this is handled specially in shouldAutoApproveTool
|
||||
return null
|
||||
case ClineDefaultTool.BROWSER:
|
||||
return "useBrowser"
|
||||
case ClineDefaultTool.MCP_USE:
|
||||
return "useMcpTool"
|
||||
case ClineDefaultTool.MCP_ACCESS:
|
||||
return "accessMcpResource"
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the tool should be auto-approved based on the settings
|
||||
// and the path of the action. Returns true if the tool should be auto-approved
|
||||
// based on the user's settings and the path of the action.
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type ToolApprovalPolicy = "ask_everytime" | "auto_approve" | "never_allow"
|
||||
|
||||
export interface AutoApprovalSettings {
|
||||
// Version for race condition prevention (incremented on every change)
|
||||
version: number
|
||||
@@ -21,6 +23,22 @@ export interface AutoApprovalSettings {
|
||||
useBrowser: boolean // Use browser
|
||||
useMcp: boolean // Use MCP servers
|
||||
}
|
||||
// Per-tool approval policies (granular control)
|
||||
toolPolicies?: {
|
||||
readFile?: ToolApprovalPolicy
|
||||
editedExistingFile?: ToolApprovalPolicy
|
||||
newFileCreated?: ToolApprovalPolicy
|
||||
fileDeleted?: ToolApprovalPolicy
|
||||
listFilesTopLevel?: ToolApprovalPolicy
|
||||
listFilesRecursive?: ToolApprovalPolicy
|
||||
listCodeDefinitionNames?: ToolApprovalPolicy
|
||||
searchFiles?: ToolApprovalPolicy
|
||||
executeSafeCommand?: ToolApprovalPolicy
|
||||
executeRiskyCommand?: ToolApprovalPolicy
|
||||
useBrowser?: ToolApprovalPolicy
|
||||
useMcpTool?: ToolApprovalPolicy
|
||||
accessMcpResource?: ToolApprovalPolicy
|
||||
}
|
||||
// Global settings
|
||||
enableNotifications: boolean // Show notifications for approval and task completion
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BROWSER_VIEWPORT_PRESETS } from "@shared/BrowserSettings"
|
||||
import { BrowserAction, BrowserActionResult, ClineMessage, ClineSayBrowserAction } from "@shared/ExtensionMessage"
|
||||
import { StringRequest } from "@shared/proto/cline/common"
|
||||
import { AskResponseRequest, SetToolApprovalPolicyRequest } from "@shared/proto/cline/task"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import { ChevronDownIcon, ChevronRightIcon } from "lucide-react"
|
||||
@@ -12,7 +13,8 @@ import { ChatRowContent, ProgressIndicator } from "@/components/chat/ChatRow"
|
||||
import CodeBlock, { CODE_BLOCK_BG_COLOR } from "@/components/common/CodeBlock"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, TaskServiceClient } from "@/services/grpc-client"
|
||||
import { InlineApprovalCard } from "./InlineApprovalCard"
|
||||
|
||||
interface BrowserSessionRowProps {
|
||||
messages: ClineMessage[]
|
||||
@@ -106,11 +108,49 @@ const headerStyle: CSSProperties = {
|
||||
|
||||
const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
const { messages, isLast, onHeightChange, lastModifiedMessage, onSetQuote } = props
|
||||
const { browserSettings } = useExtensionState()
|
||||
const { browserSettings, autoApprovalSettings } = useExtensionState()
|
||||
const prevHeightRef = useRef(0)
|
||||
const [maxActionHeight, setMaxActionHeight] = useState(0)
|
||||
const [consoleLogsExpanded, setConsoleLogsExpanded] = useState(false)
|
||||
|
||||
// Approval handlers
|
||||
const handleApprove = useCallback(async () => {
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error approving browser:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleReject = useCallback(async () => {
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "noButtonClicked",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error rejecting browser:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePolicyChange = useCallback(async (toolName: string, policy: "ask_everytime" | "auto_approve" | "never_allow") => {
|
||||
try {
|
||||
await TaskServiceClient.setToolApprovalPolicy(
|
||||
SetToolApprovalPolicyRequest.create({
|
||||
toolName,
|
||||
policy,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error saving browser approval policy:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isLastApiReqInterrupted = useMemo(() => {
|
||||
// Check if last api_req_started is cancelled
|
||||
const lastApiReqStarted = [...messages].reverse().find((m) => m.say === "api_req_started")
|
||||
@@ -341,10 +381,11 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
// shouldShowCheckpoints = lastModifiedMessage?.ask === "resume_completed_task" || lastModifiedMessage?.ask === "resume_task"
|
||||
// }
|
||||
|
||||
const _shouldShowSettings = useMemo(() => {
|
||||
// Check if we should show approval (when last message is asking for browser launch AND it's the last message in chat)
|
||||
const showBrowserApproval = useMemo(() => {
|
||||
const lastMessage = messages[messages.length - 1]
|
||||
return lastMessage?.ask === "browser_action_launch" || lastMessage?.say === "browser_action_launch"
|
||||
}, [messages])
|
||||
return lastMessage?.ask === "browser_action_launch" && isLast
|
||||
}, [messages, isLast])
|
||||
|
||||
// Calculate maxWidth
|
||||
const maxWidth = browserSettings.viewport.width < BROWSER_VIEWPORT_PRESETS["Small Desktop (900x600)"].width ? 200 : undefined
|
||||
@@ -365,13 +406,15 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
borderRadius: 3,
|
||||
borderRadius: showBrowserApproval ? "3px 3px 0 0" : 3,
|
||||
border: "1px solid var(--vscode-editorGroup-border)",
|
||||
// overflow: "hidden",
|
||||
backgroundColor: CODE_BLOCK_BG_COLOR,
|
||||
// marginBottom: 10,
|
||||
maxWidth,
|
||||
margin: "0 auto 10px auto", // Center the container
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginTop: 0,
|
||||
marginBottom: showBrowserApproval ? 0 : 10, // Remove bottom margin when approval shown
|
||||
}}>
|
||||
{/* URL Bar */}
|
||||
<div style={urlBarContainerStyle}>
|
||||
@@ -451,6 +494,18 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
|
||||
{/* Action content with min height */}
|
||||
<div style={{ minHeight: maxActionHeight }}>{actionContent}</div>
|
||||
|
||||
{/* Inline approval card for browser launch */}
|
||||
{showBrowserApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={autoApprovalSettings?.toolPolicies?.useBrowser || "ask_everytime"}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="useBrowser"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Pagination moved to bottom */}
|
||||
{pages.length > 1 && (
|
||||
<div style={paginationContainerStyle}>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { COMMAND_OUTPUT_STRING } from "@shared/combineCommandSequences"
|
||||
import { COMMAND_OUTPUT_STRING, COMMAND_REQ_APP_STRING } from "@shared/combineCommandSequences"
|
||||
import {
|
||||
ClineApiReqInfo,
|
||||
ClineAskQuestion,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
COMPLETION_RESULT_CHANGES_FLAG,
|
||||
} from "@shared/ExtensionMessage"
|
||||
import { BooleanRequest, StringRequest } from "@shared/proto/cline/common"
|
||||
import { AskResponseRequest, SetToolApprovalPolicyRequest } from "@shared/proto/cline/task"
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import deepEqual from "fast-deep-equal"
|
||||
import {
|
||||
@@ -46,7 +47,7 @@ import McpResourceRow from "@/components/mcp/configuration/tabs/installed/server
|
||||
import McpToolRow from "@/components/mcp/configuration/tabs/installed/server-row/McpToolRow"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient, UiServiceClient } from "@/services/grpc-client"
|
||||
import { FileServiceClient, TaskServiceClient, UiServiceClient } from "@/services/grpc-client"
|
||||
import { findMatchingResourceOrTemplate, getMcpServerDisplayName } from "@/utils/mcp"
|
||||
import CodeAccordian, { cleanPathPrefix } from "../common/CodeAccordian"
|
||||
import { CommandOutputContent, CommandOutputRow } from "./CommandOutputRow"
|
||||
@@ -54,6 +55,7 @@ import { CompletionOutputRow } from "./CompletionOutputRow"
|
||||
import { DiffEditRow } from "./DiffEditRow"
|
||||
import ErrorRow from "./ErrorRow"
|
||||
import HookMessage from "./HookMessage"
|
||||
import { InlineApprovalCard } from "./InlineApprovalCard"
|
||||
import { MarkdownRow } from "./MarkdownRow"
|
||||
import NewTaskPreview from "./NewTaskPreview"
|
||||
import PlanCompletionOutputRow from "./PlanCompletionOutputRow"
|
||||
@@ -153,6 +155,7 @@ export const ChatRowContent = memo(
|
||||
onRelinquishControl,
|
||||
vscodeTerminalExecutionMode,
|
||||
clineMessages,
|
||||
autoApprovalSettings,
|
||||
} = useExtensionState()
|
||||
const [seeNewChangesDisabled, setSeeNewChangesDisabled] = useState(false)
|
||||
const [explainChangesDisabled, setExplainChangesDisabled] = useState(false)
|
||||
@@ -397,6 +400,66 @@ export const ChatRowContent = memo(
|
||||
return extension ? imageExtensions.includes(`.${extension}`) : false
|
||||
}
|
||||
|
||||
// Helper function to get approval policy for a tool
|
||||
const getToolPolicy = useCallback(
|
||||
(toolName: string): "ask_everytime" | "auto_approve" | "never_allow" => {
|
||||
// Map tool names to their keys in toolPolicies
|
||||
const toolKey = toolName.replace(/ /g, "").replace(/\(|\)/g, "")
|
||||
const policyKey = toolKey.charAt(0).toLowerCase() + toolKey.slice(1)
|
||||
return (
|
||||
autoApprovalSettings?.toolPolicies?.[policyKey as keyof typeof autoApprovalSettings.toolPolicies] ||
|
||||
"ask_everytime"
|
||||
)
|
||||
},
|
||||
[autoApprovalSettings],
|
||||
)
|
||||
|
||||
// Approval handlers for inline approval cards
|
||||
const [approvalPolicy, setApprovalPolicy] = useState<"ask_everytime" | "auto_approve" | "never_allow">("ask_everytime")
|
||||
|
||||
const handleApprove = useCallback(async () => {
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "yesButtonClicked",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error approving tool:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleReject = useCallback(async () => {
|
||||
try {
|
||||
await TaskServiceClient.askResponse(
|
||||
AskResponseRequest.create({
|
||||
responseType: "noButtonClicked",
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error rejecting tool:", error)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePolicyChange = useCallback(
|
||||
async (toolName: string, policy: "ask_everytime" | "auto_approve" | "never_allow") => {
|
||||
setApprovalPolicy(policy)
|
||||
|
||||
// Save policy to backend
|
||||
try {
|
||||
await TaskServiceClient.setToolApprovalPolicy(
|
||||
SetToolApprovalPolicyRequest.create({
|
||||
toolName,
|
||||
policy,
|
||||
}),
|
||||
)
|
||||
} catch (error) {
|
||||
console.error("Error saving tool approval policy:", error)
|
||||
}
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
if (conditionalRulesInfo) {
|
||||
const names = conditionalRulesInfo.rules.map((r: { name: string }) => r.name).join(", ")
|
||||
return (
|
||||
@@ -431,6 +494,7 @@ export const ChatRowContent = memo(
|
||||
const editToolTitle = isApplyingPatch
|
||||
? "Cline is creating patches to edit this file:"
|
||||
: "Cline wants to edit this file:"
|
||||
const showEditApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -441,6 +505,7 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
{backgroundEditEnabled && tool.path && tool.content ? (
|
||||
<DiffEditRow
|
||||
hasApproval={showEditApproval}
|
||||
isLoading={message.partial}
|
||||
patch={tool.content}
|
||||
path={tool.path}
|
||||
@@ -450,14 +515,27 @@ export const ChatRowContent = memo(
|
||||
<CodeAccordian
|
||||
// isLoading={message.partial}
|
||||
code={tool.content}
|
||||
hasApproval={showEditApproval}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
)}
|
||||
{showEditApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("editedExistingFile")}
|
||||
approveButtonLabel="Save"
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="editedExistingFile"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "fileDeleted":
|
||||
const showDeleteApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -469,13 +547,25 @@ export const ChatRowContent = memo(
|
||||
<CodeAccordian
|
||||
// isLoading={message.partial}
|
||||
code={tool.content}
|
||||
hasApproval={showDeleteApproval}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
{showDeleteApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("fileDeleted")}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="fileDeleted"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "newFileCreated":
|
||||
const showNewFileApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -485,20 +575,38 @@ export const ChatRowContent = memo(
|
||||
<span className="font-bold">Cline wants to create a new file:</span>
|
||||
</div>
|
||||
{backgroundEditEnabled && tool.path && tool.content ? (
|
||||
<DiffEditRow patch={tool.content} path={tool.path} startLineNumbers={tool.startLineNumbers} />
|
||||
<DiffEditRow
|
||||
hasApproval={showNewFileApproval}
|
||||
patch={tool.content}
|
||||
path={tool.path}
|
||||
startLineNumbers={tool.startLineNumbers}
|
||||
/>
|
||||
) : (
|
||||
<CodeAccordian
|
||||
code={tool.content!}
|
||||
hasApproval={showNewFileApproval}
|
||||
isExpanded={isExpanded}
|
||||
isLoading={message.partial}
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
)}
|
||||
{showNewFileApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("newFileCreated")}
|
||||
approveButtonLabel="Save"
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="newFileCreated"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "readFile":
|
||||
const isImage = isImageFile(tool.path || "")
|
||||
const showReadApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -507,7 +615,11 @@ export const ChatRowContent = memo(
|
||||
toolIcon("sign-out", "yellow", -90, "This file is outside of your workspace")}
|
||||
<span className="font-bold">Cline wants to read this file:</span>
|
||||
</div>
|
||||
<div className="bg-code rounded-sm overflow-hidden border border-editor-group-border">
|
||||
<div
|
||||
className={cn("bg-code overflow-hidden border border-editor-group-border", {
|
||||
"rounded-sm": !showReadApproval,
|
||||
"rounded-t-sm": showReadApproval,
|
||||
})}>
|
||||
<div
|
||||
className={cn("text-description flex items-center cursor-pointer select-none py-2 px-2.5", {
|
||||
"cursor-default select-text": isImage,
|
||||
@@ -528,9 +640,21 @@ export const ChatRowContent = memo(
|
||||
{!isImage && <SquareArrowOutUpRightIcon className="size-2" />}
|
||||
</div>
|
||||
</div>
|
||||
{showReadApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("readFile")}
|
||||
approveButtonLabel="Read"
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="readFile"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "listFilesTopLevel":
|
||||
const showListTopLevelApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -545,14 +669,26 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={tool.content!}
|
||||
hasApproval={showListTopLevelApproval}
|
||||
isExpanded={isExpanded}
|
||||
language="shell-session"
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
{showListTopLevelApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("listFilesTopLevel")}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="listFilesTopLevel"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "listFilesRecursive":
|
||||
const showListRecursiveApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -567,14 +703,26 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={tool.content!}
|
||||
hasApproval={showListRecursiveApproval}
|
||||
isExpanded={isExpanded}
|
||||
language="shell-session"
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
{showListRecursiveApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("listFilesRecursive")}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="listFilesRecursive"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "listCodeDefinitionNames":
|
||||
const showListCodeApproval = message.ask === "tool" && isLast
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -589,10 +737,21 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
<CodeAccordian
|
||||
code={tool.content!}
|
||||
hasApproval={showListCodeApproval}
|
||||
isExpanded={isExpanded}
|
||||
onToggleExpand={handleToggle}
|
||||
path={tool.path!}
|
||||
/>
|
||||
{showListCodeApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={getToolPolicy("listCodeDefinitionNames")}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName="listCodeDefinitionNames"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
case "searchFiles":
|
||||
@@ -747,17 +906,29 @@ export const ChatRowContent = memo(
|
||||
}, [isCommandMessage, isCommandExecuting, isExpanded, onToggleExpand, message.ts])
|
||||
|
||||
if (message.ask === "command" || message.say === "command") {
|
||||
const showCommandApproval = message.ask === "command" && isLast
|
||||
const commandText = message.text || ""
|
||||
// Check if command is risky by looking for COMMAND_REQ_APP_STRING marker
|
||||
const isRiskyCommand = commandText.includes(COMMAND_REQ_APP_STRING)
|
||||
const commandToolName = isRiskyCommand ? "executeRiskyCommand" : "executeSafeCommand"
|
||||
|
||||
return (
|
||||
<CommandOutputRow
|
||||
approvalPolicy={getToolPolicy(commandToolName)}
|
||||
icon={icon}
|
||||
isBackgroundExec={vscodeTerminalExecutionMode === "backgroundExec"}
|
||||
isCommandCompleted={isCommandCompleted}
|
||||
isCommandExecuting={isCommandExecuting}
|
||||
isCommandPending={isCommandPending}
|
||||
isOutputFullyExpanded={isOutputFullyExpanded}
|
||||
isRiskyCommand={isRiskyCommand}
|
||||
message={message}
|
||||
onApprove={handleApprove}
|
||||
onCancelCommand={onCancelCommand}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
setIsOutputFullyExpanded={setIsOutputFullyExpanded}
|
||||
showApproval={showCommandApproval}
|
||||
title={title}
|
||||
/>
|
||||
)
|
||||
@@ -766,6 +937,9 @@ export const ChatRowContent = memo(
|
||||
if (message.ask === "use_mcp_server" || message.say === "use_mcp_server") {
|
||||
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
|
||||
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
|
||||
// Only show approval if this is the last message and it's asking for approval
|
||||
const showMcpApproval = message.ask === "use_mcp_server" && isLast
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={HEADER_CLASSNAMES}>
|
||||
@@ -773,7 +947,11 @@ export const ChatRowContent = memo(
|
||||
{title}
|
||||
</div>
|
||||
|
||||
<div className="bg-code rounded-xs py-2 px-2.5 mt-2">
|
||||
<div
|
||||
className={cn("bg-code py-2 px-2.5 mt-2 border border-editor-group-border", {
|
||||
"rounded-xs": !showMcpApproval,
|
||||
"rounded-t-xs": showMcpApproval,
|
||||
})}>
|
||||
{useMcpServer.type === "access_mcp_resource" && (
|
||||
<McpResourceRow
|
||||
item={{
|
||||
@@ -821,6 +999,20 @@ export const ChatRowContent = memo(
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{showMcpApproval && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={
|
||||
useMcpServer.type === "use_mcp_tool"
|
||||
? autoApprovalSettings?.toolPolicies?.useMcpTool || "ask_everytime"
|
||||
: autoApprovalSettings?.toolPolicies?.accessMcpResource || "ask_everytime"
|
||||
}
|
||||
isEnabled={true}
|
||||
onApprove={handleApprove}
|
||||
onPolicyChange={handlePolicyChange}
|
||||
onReject={handleReject}
|
||||
toolName={useMcpServer.type === "use_mcp_tool" ? "useMcpTool" : "accessMcpResource"}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import { cn } from "@/lib/utils"
|
||||
import { FileServiceClient } from "@/services/grpc-client"
|
||||
import CodeBlock from "../common/CodeBlock"
|
||||
import ExpandHandle from "./ExpandHandle"
|
||||
import { InlineApprovalCard } from "./InlineApprovalCard"
|
||||
|
||||
export const CommandOutputContent = memo(
|
||||
({
|
||||
@@ -120,6 +121,12 @@ export const CommandOutputRow = memo(
|
||||
title,
|
||||
isOutputFullyExpanded,
|
||||
setIsOutputFullyExpanded,
|
||||
showApproval = false,
|
||||
approvalPolicy = "ask_everytime",
|
||||
onApprove,
|
||||
onReject,
|
||||
onPolicyChange,
|
||||
isRiskyCommand = false,
|
||||
}: {
|
||||
message: ClineMessage
|
||||
isCommandExecuting?: boolean
|
||||
@@ -131,6 +138,12 @@ export const CommandOutputRow = memo(
|
||||
title?: JSX.Element | null
|
||||
isOutputFullyExpanded: boolean
|
||||
setIsOutputFullyExpanded: (expanded: boolean) => void
|
||||
showApproval?: boolean
|
||||
approvalPolicy?: "ask_everytime" | "auto_approve" | "never_allow"
|
||||
onApprove?: () => void
|
||||
onReject?: () => void
|
||||
onPolicyChange?: (toolName: string, policy: "ask_everytime" | "auto_approve" | "never_allow") => void
|
||||
isRiskyCommand?: boolean
|
||||
}) => {
|
||||
const splitMessage = (text: string) => {
|
||||
const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
|
||||
@@ -209,7 +222,10 @@ export const CommandOutputRow = memo(
|
||||
<>
|
||||
{commandHeader}
|
||||
<div
|
||||
className="bg-code rounded-sm border border-editor-group-border"
|
||||
className={cn("bg-code border border-editor-group-border", {
|
||||
"rounded-sm": !showApproval,
|
||||
"rounded-t-sm": showApproval,
|
||||
})}
|
||||
style={{
|
||||
transition: "all 0.3s ease-in-out",
|
||||
}}>
|
||||
@@ -282,6 +298,17 @@ export const CommandOutputRow = memo(
|
||||
<span>The model has determined this command requires explicit approval.</span>
|
||||
</div>
|
||||
)}
|
||||
{showApproval && onApprove && onReject && onPolicyChange && (
|
||||
<InlineApprovalCard
|
||||
approvalPolicy={approvalPolicy}
|
||||
approveButtonLabel="Run"
|
||||
isEnabled={true}
|
||||
onApprove={onApprove}
|
||||
onPolicyChange={onPolicyChange}
|
||||
onReject={onReject}
|
||||
toolName={isRiskyCommand ? "executeRiskyCommand" : "executeSafeCommand"}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
|
||||
@@ -34,9 +34,10 @@ interface DiffEditRowProps {
|
||||
path: string
|
||||
isLoading?: boolean
|
||||
startLineNumbers?: number[]
|
||||
hasApproval?: boolean
|
||||
}
|
||||
|
||||
export const DiffEditRow = memo<DiffEditRowProps>(({ patch, path, isLoading, startLineNumbers }) => {
|
||||
export const DiffEditRow = memo<DiffEditRowProps>(({ patch, path, isLoading, startLineNumbers, hasApproval }) => {
|
||||
const { parsedFiles, isStreaming } = useMemo(() => {
|
||||
const parsed = parsePatch(patch, path)
|
||||
return {
|
||||
@@ -50,7 +51,7 @@ export const DiffEditRow = memo<DiffEditRowProps>(({ patch, path, isLoading, sta
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 rounded-xs">
|
||||
<div className={cn("space-y-4", { "rounded-xs": !hasApproval, "rounded-t-xs": hasApproval })}>
|
||||
{parsedFiles.map((file, index) => (
|
||||
<FileBlock
|
||||
file={file}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
|
||||
interface InlineApprovalCardProps {
|
||||
toolName: string
|
||||
toolDescription?: string
|
||||
approvalPolicy: "ask_everytime" | "auto_approve" | "never_allow"
|
||||
onApprove: () => void
|
||||
onReject: () => void
|
||||
onPolicyChange: (toolName: string, policy: "ask_everytime" | "auto_approve" | "never_allow") => void
|
||||
isEnabled: boolean
|
||||
showDetails?: boolean
|
||||
onToggleDetails?: () => void
|
||||
approveButtonLabel?: string
|
||||
}
|
||||
|
||||
export const InlineApprovalCard: React.FC<InlineApprovalCardProps> = ({
|
||||
toolName,
|
||||
toolDescription,
|
||||
approvalPolicy,
|
||||
onApprove,
|
||||
onReject,
|
||||
onPolicyChange,
|
||||
isEnabled,
|
||||
showDetails = false,
|
||||
onToggleDetails,
|
||||
approveButtonLabel = "Run",
|
||||
}) => {
|
||||
const policyLabels = {
|
||||
ask_everytime: "Ask Everytime",
|
||||
auto_approve: "Auto-approve",
|
||||
never_allow: "Never allow",
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="inline-approval-card bg-code border-x border-b border-editor-group-border rounded-b-sm overflow-hidden">
|
||||
{/* Approval Actions */}
|
||||
<div className="flex items-center gap-2 py-2 px-2.5">
|
||||
{/* Policy Select */}
|
||||
<Select
|
||||
disabled={!isEnabled}
|
||||
onValueChange={(policy) => onPolicyChange(toolName, policy as any)}
|
||||
value={approvalPolicy}>
|
||||
<SelectTrigger className="w-auto items-center [&_span]:-mt-px border-0" size="sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ask_everytime">{policyLabels.ask_everytime}</SelectItem>
|
||||
<SelectItem value="auto_approve">{policyLabels.auto_approve}</SelectItem>
|
||||
<SelectItem value="never_allow">{policyLabels.never_allow}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Spacer */}
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* Action Buttons */}
|
||||
<button
|
||||
className="text-foreground hover:text-link text-sm cursor-pointer border-0 bg-transparent px-2 py-[.2rem] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!isEnabled}
|
||||
onClick={onReject}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="bg-button-background text-button-foreground hover:bg-button-hover border-0 rounded-sm text-sm cursor-pointer px-2 py-[.2rem] disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={!isEnabled}
|
||||
onClick={onApprove}>
|
||||
{approveButtonLabel}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Optional Description */}
|
||||
{toolDescription && showDetails && (
|
||||
<div className="py-2 px-2.5 border-t border-editor-group-border text-sm text-muted-foreground">
|
||||
{toolDescription}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -159,8 +159,12 @@ export const ActionButtons: React.FC<ActionButtonsProps> = ({
|
||||
|
||||
const opacity = canInteract || isStreaming ? 1 : 0.5
|
||||
|
||||
// Hide approve/reject buttons but keep cancel button visible
|
||||
const isApprovalButton =
|
||||
primaryAction === "approve" || primaryAction === "reject" || secondaryAction === "approve" || secondaryAction === "reject"
|
||||
|
||||
return (
|
||||
<div className="flex px-3.5" style={{ opacity }}>
|
||||
<div className="flex px-3.5" style={{ opacity, display: isApprovalButton ? "none" : undefined }}>
|
||||
{primaryText && primaryAction && (
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
|
||||
@@ -15,6 +15,7 @@ interface CodeAccordianProps {
|
||||
isExpanded: boolean
|
||||
onToggleExpand: () => void
|
||||
isLoading?: boolean
|
||||
hasApproval?: boolean
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -35,6 +36,7 @@ const CodeAccordian = ({
|
||||
isExpanded,
|
||||
onToggleExpand,
|
||||
isLoading,
|
||||
hasApproval,
|
||||
}: CodeAccordianProps) => {
|
||||
const inferredLanguage = useMemo(
|
||||
() => code && (language ?? (path ? getLanguageFromPath(path) : undefined)),
|
||||
@@ -49,7 +51,11 @@ const CodeAccordian = ({
|
||||
}, [code])
|
||||
|
||||
return (
|
||||
<div className="bg-code overflow-hidden rounded-xs border border-editor-group-border">
|
||||
<div
|
||||
className={cn("bg-code overflow-hidden border border-editor-group-border", {
|
||||
"rounded-xs": !hasApproval,
|
||||
"rounded-t-xs": hasApproval,
|
||||
})}>
|
||||
{(path || isFeedback || isConsoleLogs) && (
|
||||
<Button
|
||||
aria-label={isExpanded ? "Collapse code block" : "Expand code block"}
|
||||
|
||||
Reference in New Issue
Block a user