Get auto-approval working by checking state before sending asks

This commit is contained in:
Saoud Rizwan 2024-12-17 17:14:16 -08:00
parent d480015b16
commit 93af89dbf9
10 changed files with 379 additions and 303 deletions

View file

@ -66,7 +66,6 @@ export class Cline {
private browserSession: BrowserSession
private didEditFile: boolean = false
customInstructions?: string
alwaysAllowReadOnly: boolean
autoApprovalSettings: AutoApprovalSettings
apiConversationHistory: Anthropic.MessageParam[] = []
clineMessages: ClineMessage[] = []
@ -97,7 +96,6 @@ export class Cline {
apiConfiguration: ApiConfiguration,
autoApprovalSettings: AutoApprovalSettings,
customInstructions?: string,
alwaysAllowReadOnly?: boolean,
task?: string,
images?: string[],
historyItem?: HistoryItem,
@ -109,7 +107,6 @@ export class Cline {
this.browserSession = new BrowserSession(provider.context)
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
this.alwaysAllowReadOnly = alwaysAllowReadOnly ?? false
this.autoApprovalSettings = autoApprovalSettings
if (historyItem) {
this.taskId = historyItem.id
@ -754,6 +751,29 @@ export class Cline {
}
}
shouldAutoApproveTool(toolName: ToolUseName): boolean {
if (this.autoApprovalSettings.enabled) {
switch (toolName) {
case "read_file":
case "list_files":
case "list_code_definition_names":
case "search_files":
return this.autoApprovalSettings.actions.readFiles
case "write_to_file":
case "replace_in_file":
return this.autoApprovalSettings.actions.editFiles
case "execute_command":
return this.autoApprovalSettings.actions.executeCommands
case "browser_action":
return this.autoApprovalSettings.actions.useBrowser
case "access_mcp_resource":
case "use_mcp_tool":
return this.autoApprovalSettings.actions.useMcp
}
}
return false
}
async *attemptApiRequest(previousApiReqIndex: number): ApiStream {
// Wait for MCP servers to be connected before generating system prompt
await pWaitFor(() => this.providerRef.deref()?.mcpHub?.isConnecting !== true, { timeout: 10_000 }).catch(() => {
@ -1110,7 +1130,11 @@ export class Cline {
if (block.partial) {
// update gui message
const partialMessage = JSON.stringify(sharedMessageProps)
await this.ask("tool", partialMessage, block.partial).catch(() => {})
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", partialMessage, undefined, block.partial)
} else {
await this.ask("tool", partialMessage, block.partial).catch(() => {})
}
// update editor
if (!this.diffViewProvider.isEditing) {
// open the editor and prepare to stream content in
@ -1165,11 +1189,17 @@ export class Cline {
// )
// : undefined,
} satisfies ClineSayTool)
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await this.diffViewProvider.revertChanges()
break
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("tool", completeMessage)
if (!didApprove) {
await this.diffViewProvider.revertChanges()
break
}
}
const { newProblemsMessage, userEdits, finalContent } =
await this.diffViewProvider.saveChanges()
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
@ -1224,7 +1254,7 @@ export class Cline {
...sharedMessageProps,
content: undefined,
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", partialMessage, undefined, block.partial)
} else {
await this.ask("tool", partialMessage, block.partial).catch(() => {})
@ -1242,7 +1272,7 @@ export class Cline {
...sharedMessageProps,
content: absolutePath,
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", completeMessage, undefined, false) // need to be sending partialValue bool, since undefined has its own purpose in that the message is treated neither as a partial or completion of a partial, but as a single complete message
} else {
const didApprove = await askApproval("tool", completeMessage)
@ -1274,7 +1304,7 @@ export class Cline {
...sharedMessageProps,
content: "",
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", partialMessage, undefined, block.partial)
} else {
await this.ask("tool", partialMessage, block.partial).catch(() => {})
@ -1294,7 +1324,7 @@ export class Cline {
...sharedMessageProps,
content: result,
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("tool", completeMessage)
@ -1322,7 +1352,7 @@ export class Cline {
...sharedMessageProps,
content: "",
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", partialMessage, undefined, block.partial)
} else {
await this.ask("tool", partialMessage, block.partial).catch(() => {})
@ -1343,7 +1373,7 @@ export class Cline {
...sharedMessageProps,
content: result,
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("tool", completeMessage)
@ -1375,7 +1405,7 @@ export class Cline {
...sharedMessageProps,
content: "",
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", partialMessage, undefined, block.partial)
} else {
await this.ask("tool", partialMessage, block.partial).catch(() => {})
@ -1399,7 +1429,7 @@ export class Cline {
...sharedMessageProps,
content: results,
} satisfies ClineSayTool)
if (this.alwaysAllowReadOnly) {
if (this.shouldAutoApproveTool(block.name)) {
await this.say("tool", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("tool", completeMessage)
@ -1434,11 +1464,20 @@ export class Cline {
try {
if (block.partial) {
if (action === "launch") {
await this.ask(
"browser_action_launch",
removeClosingTag("url", url),
block.partial,
).catch(() => {})
if (this.shouldAutoApproveTool(block.name)) {
await this.say(
"browser_action_launch",
removeClosingTag("url", url),
undefined,
block.partial,
)
} else {
await this.ask(
"browser_action_launch",
removeClosingTag("url", url),
block.partial,
).catch(() => {})
}
} else {
await this.say(
"browser_action",
@ -1464,9 +1503,14 @@ export class Cline {
break
}
this.consecutiveMistakeCount = 0
const didApprove = await askApproval("browser_action_launch", url)
if (!didApprove) {
break
if (this.shouldAutoApproveTool(block.name)) {
await this.say("browser_action_launch", url, undefined, false)
} else {
const didApprove = await askApproval("browser_action_launch", url)
if (!didApprove) {
break
}
}
// NOTE: it's okay that we call this message since the partial inspect_site is finished streaming. The only scenario we have to avoid is sending messages WHILE a partial message exists at the end of the messages array. For example the api_req_finished message would interfere with the partial message, so we needed to remove that.
@ -1568,9 +1612,20 @@ export class Cline {
try {
if (block.partial) {
await this.ask("command", removeClosingTag("command", command), block.partial).catch(
() => {},
)
if (this.shouldAutoApproveTool(block.name)) {
await this.say(
"command",
removeClosingTag("command", command),
undefined,
block.partial,
).catch(() => {})
} else {
await this.ask(
"command",
removeClosingTag("command", command),
block.partial,
).catch(() => {})
}
break
} else {
if (!command) {
@ -1591,13 +1646,20 @@ export class Cline {
break
}
this.consecutiveMistakeCount = 0
const didApprove = await askApproval(
"command",
command + `${requiresApproval ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
break
if (!requiresApproval && this.shouldAutoApproveTool(block.name)) {
await this.say("command", command, undefined, false)
} else {
const didApprove = await askApproval(
"command",
command +
`${this.shouldAutoApproveTool(block.name) && requiresApproval ? COMMAND_REQ_APP_STRING : ""}`, // ugly hack until we refactor combineCommandSequences
)
if (!didApprove) {
break
}
}
const [userRejected, result] = await this.executeCommandTool(command)
if (userRejected) {
this.didRejectTool = true
@ -1622,7 +1684,13 @@ export class Cline {
toolName: removeClosingTag("tool_name", tool_name),
arguments: removeClosingTag("arguments", mcp_arguments),
} satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
if (this.shouldAutoApproveTool(block.name)) {
await this.say("use_mcp_server", partialMessage, undefined, block.partial)
} else {
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
}
break
} else {
if (!server_name) {
@ -1670,10 +1738,16 @@ export class Cline {
toolName: tool_name,
arguments: mcp_arguments,
} satisfies ClineAskUseMcpServer)
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
if (this.shouldAutoApproveTool(block.name)) {
await this.say("use_mcp_server", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
}
}
// now execute the tool
await this.say("mcp_server_request_started") // same as browser_action_result
const toolResult = await this.providerRef
@ -1715,7 +1789,13 @@ export class Cline {
serverName: removeClosingTag("server_name", server_name),
uri: removeClosingTag("uri", uri),
} satisfies ClineAskUseMcpServer)
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
if (this.shouldAutoApproveTool(block.name)) {
await this.say("use_mcp_server", partialMessage, undefined, block.partial)
} else {
await this.ask("use_mcp_server", partialMessage, block.partial).catch(() => {})
}
break
} else {
if (!server_name) {
@ -1738,10 +1818,16 @@ export class Cline {
serverName: server_name,
uri,
} satisfies ClineAskUseMcpServer)
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
if (this.shouldAutoApproveTool(block.name)) {
await this.say("use_mcp_server", completeMessage, undefined, false)
} else {
const didApprove = await askApproval("use_mcp_server", completeMessage)
if (!didApprove) {
break
}
}
// now execute the tool
await this.say("mcp_server_request_started")
const resourceResult = await this.providerRef
@ -1824,6 +1910,7 @@ export class Cline {
// remove the previous partial attempt_completion ask, replace with say, post state to webview, then stream command
// const secondLastMessage = this.clineMessages.at(-2)
// NOTE: we do not want to auto approve a command run as part of the attempt_completion tool
if (lastMessage && lastMessage.ask === "command") {
// update command
await this.ask(

View file

@ -48,7 +48,6 @@ type GlobalStateKey =
| "vertexRegion"
| "lastShownAnnouncementId"
| "customInstructions"
| "alwaysAllowReadOnly"
| "taskHistory"
| "openAiBaseUrl"
| "openAiModelId"
@ -200,29 +199,18 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask() // ensures that an exising task doesn't exist before starting a new one, although this shouldn't be possible since user must clear task before starting a new one
const { apiConfiguration, customInstructions, alwaysAllowReadOnly, autoApprovalSettings } =
await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
autoApprovalSettings,
customInstructions,
alwaysAllowReadOnly,
task,
images,
)
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
this.cline = new Cline(this, apiConfiguration, autoApprovalSettings, customInstructions, task, images)
}
async initClineWithHistoryItem(historyItem: HistoryItem) {
await this.clearTask()
const { apiConfiguration, customInstructions, alwaysAllowReadOnly, autoApprovalSettings } =
await this.getState()
const { apiConfiguration, customInstructions, autoApprovalSettings } = await this.getState()
this.cline = new Cline(
this,
apiConfiguration,
autoApprovalSettings,
customInstructions,
alwaysAllowReadOnly,
undefined,
undefined,
historyItem,
@ -426,13 +414,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
case "customInstructions":
await this.updateCustomInstructions(message.text)
break
case "alwaysAllowReadOnly":
await this.updateGlobalState("alwaysAllowReadOnly", message.bool ?? undefined)
if (this.cline) {
this.cline.alwaysAllowReadOnly = message.bool ?? false
}
await this.postStateToWebview()
break
case "autoApprovalSettings":
if (message.autoApprovalSettings) {
await this.updateGlobalState("autoApprovalSettings", message.autoApprovalSettings)
@ -846,19 +827,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
async getStateToPostToWebview() {
const {
apiConfiguration,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
taskHistory,
autoApprovalSettings,
} = await this.getState()
const { apiConfiguration, lastShownAnnouncementId, customInstructions, taskHistory, autoApprovalSettings } =
await this.getState()
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
customInstructions,
alwaysAllowReadOnly,
uriScheme: vscode.env.uriScheme,
clineMessages: this.cline?.clineMessages || [],
taskHistory: (taskHistory || []).filter((item) => item.ts && item.task).sort((a, b) => b.ts - a.ts),
@ -946,7 +920,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
openRouterModelInfo,
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly,
taskHistory,
autoApprovalSettings,
] = await Promise.all([
@ -976,7 +949,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
this.getGlobalState("customInstructions") as Promise<string | undefined>,
this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
this.getGlobalState("autoApprovalSettings") as Promise<AutoApprovalSettings | undefined>,
])
@ -1024,7 +996,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
},
lastShownAnnouncementId,
customInstructions,
alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
taskHistory,
autoApprovalSettings: autoApprovalSettings || DEFAULT_AUTO_APPROVAL_SETTINGS, // default value can be 0 or empty string
}

View file

@ -41,7 +41,6 @@ export interface ExtensionState {
version: string
apiConfiguration?: ApiConfiguration
customInstructions?: string
alwaysAllowReadOnly?: boolean
uriScheme?: string
clineMessages: ClineMessage[]
taskHistory: HistoryItem[]
@ -82,13 +81,16 @@ export type ClineSay =
| "user_feedback"
| "user_feedback_diff"
| "api_req_retried"
| "command"
| "command_output"
| "tool"
| "shell_integration_warning"
| "browser_action_launch"
| "browser_action"
| "browser_action_result"
| "mcp_server_request_started"
| "mcp_server_response"
| "use_mcp_server"
export interface ClineSayTool {
tool:

View file

@ -5,7 +5,6 @@ export interface WebviewMessage {
type:
| "apiConfiguration"
| "customInstructions"
| "alwaysAllowReadOnly"
| "webviewDidLaunch"
| "newTask"
| "askResponse"

View file

@ -25,13 +25,13 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
// First pass: combine commands with their outputs
for (let i = 0; i < messages.length; i++) {
if (messages[i].type === "ask" && messages[i].ask === "command") {
if (messages[i].type === "ask" && (messages[i].ask === "command" || messages[i].say === "command")) {
let combinedText = messages[i].text || ""
let didAddOutput = false
let j = i + 1
while (j < messages.length) {
if (messages[j].type === "ask" && messages[j].ask === "command") {
if (messages[j].type === "ask" && (messages[j].ask === "command" || messages[j].say === "command")) {
// Stop if we encounter the next command
break
}
@ -63,7 +63,7 @@ export function combineCommandSequences(messages: ClineMessage[]): ClineMessage[
return messages
.filter((msg) => !(msg.ask === "command_output" || msg.say === "command_output"))
.map((msg) => {
if (msg.type === "ask" && msg.ask === "command") {
if (msg.type === "ask" && (msg.ask === "command" || msg.say === "command")) {
const combinedCommand = combinedCommands.find((cmd) => cmd.ts === msg.ts)
return combinedCommand || msg
}

View file

@ -66,7 +66,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
let nextActionMessages: ClineMessage[] = []
messages.forEach((message) => {
if (message.ask === "browser_action_launch") {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// Start first page
currentStateMessages = [message]
} else if (message.say === "browser_action_result") {
@ -137,10 +137,19 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
// Get initial URL from launch message
const initialUrl = useMemo(() => {
const launchMessage = messages.find((m) => m.ask === "browser_action_launch")
const launchMessage = messages.find(
(m) => m.ask === "browser_action_launch" || m.say === "browser_action_launch",
)
return launchMessage?.text || ""
}, [messages])
const isAutoApproved = useMemo(() => {
const launchMessage = messages.find(
(m) => m.ask === "browser_action_launch" || m.say === "browser_action_launch",
)
return launchMessage?.say === "browser_action_launch"
}, [messages])
// Find the latest available URL and screenshot
const latestState = useMemo(() => {
for (let i = pages.length - 1; i >= 0; i--) {
@ -232,7 +241,7 @@ const BrowserSessionRow = memo((props: BrowserSessionRowProps) => {
style={{ color: "var(--vscode-foreground)", marginBottom: "-1.5px" }}></span>
)}
<span style={{ fontWeight: "bold" }}>
<>Cline wants to use the browser:</>
<>{isAutoApproved ? "Cline is using the browser:" : "Cline wants to use the browser:"}</>
</span>
</div>
<div
@ -418,6 +427,25 @@ const BrowserSessionRowContent = ({
marginBottom: "10px",
}
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
return (
<>
<div style={headerStyle}>
<span style={{ fontWeight: "bold" }}>Browser Session Started</span>
</div>
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
</div>
</>
)
}
switch (message.type) {
case "say":
switch (message.say) {
@ -456,24 +484,6 @@ const BrowserSessionRowContent = ({
case "ask":
switch (message.ask) {
case "browser_action_launch":
return (
<>
<div style={headerStyle}>
<span style={{ fontWeight: "bold" }}>Browser Session Started</span>
</div>
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock source={`${"```"}shell\n${message.text}\n${"```"}`} forceWrap={true} />
</div>
</>
)
default:
return null
}

View file

@ -90,7 +90,9 @@ export const ChatRowContent = ({
? lastModifiedMessage?.text
: undefined
const isCommandExecuting =
isLast && lastModifiedMessage?.ask === "command" && lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
isLast &&
(lastModifiedMessage?.ask === "command" || lastModifiedMessage?.say === "command") &&
lastModifiedMessage?.text?.includes(COMMAND_OUTPUT_STRING)
const isMcpServerResponding = isLast && lastModifiedMessage?.say === "mcp_server_request_started"
@ -127,7 +129,9 @@ export const ChatRowContent = ({
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to execute this command:
{message.type === "ask"
? "Cline wants to execute this command:"
: "Cline executed this command:"}
</span>,
]
case "use_mcp_server":
@ -141,8 +145,18 @@ export const ChatRowContent = ({
style={{ color: normalColor, marginBottom: "-1.5px" }}></span>
),
<span style={{ color: normalColor, fontWeight: "bold" }}>
Cline wants to {mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on
the <code>{mcpServerUse.serverName}</code> MCP server:
{message.type === "ask" ? (
<>
Cline wants to{" "}
{mcpServerUse.type === "use_mcp_tool" ? "use a tool" : "access a resource"} on the{" "}
<code>{mcpServerUse.serverName}</code> MCP server:
</>
) : (
<>
Cline {mcpServerUse.type === "use_mcp_tool" ? "used a tool" : "accessed a resource"} on
the <code>{mcpServerUse.serverName}</code> MCP server:
</>
)}
</span>,
]
case "completion_result":
@ -217,6 +231,7 @@ export const ChatRowContent = ({
apiReqCancelReason,
isMcpServerResponding,
message.text,
message.type,
])
const headerStyle: React.CSSProperties = {
@ -253,7 +268,9 @@ export const ChatRowContent = ({
<>
<div style={headerStyle}>
{toolIcon("edit")}
<span style={{ fontWeight: "bold" }}>Cline wants to edit this file:</span>
<span style={{ fontWeight: "bold" }}>
{message.type === "ask" ? "Cline wants to edit this file:" : "Cline edited this file:"}
</span>
</div>
<CodeAccordian
// isLoading={message.partial}
@ -269,7 +286,11 @@ export const ChatRowContent = ({
<>
<div style={headerStyle}>
{toolIcon("new-file")}
<span style={{ fontWeight: "bold" }}>Cline wants to create a new file:</span>
<span style={{ fontWeight: "bold" }}>
{message.type === "ask"
? "Cline wants to create a new file:"
: "Cline created a new file:"}
</span>
</div>
<CodeAccordian
isLoading={message.partial}
@ -453,6 +474,169 @@ export const ChatRowContent = ({
}
}
if (message.ask === "command" || message.say === "command") {
const splitMessage = (text: string) => {
const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
if (outputIndex === -1) {
return { command: text, output: "" }
}
return {
command: text.slice(0, outputIndex).trim(),
output: text
.slice(outputIndex + COMMAND_OUTPUT_STRING.length)
.trim()
.split("")
.map((char) => {
switch (char) {
case "\t":
return "→ "
case "\b":
return "⌫"
case "\f":
return "⏏"
case "\v":
return "⇳"
default:
return char
}
})
.join(""),
}
}
const { command: rawCommand, output } = splitMessage(message.text || "")
const requestsApproval = rawCommand.endsWith(COMMAND_REQ_APP_STRING)
const command = requestsApproval ? rawCommand.slice(0, -COMMAND_REQ_APP_STRING.length) : rawCommand
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
{/* <Terminal
rawOutput={command + (output ? "\n" + output : "")}
shouldAllowInput={!!isCommandExecuting && output.length > 0}
/> */}
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
{output.length > 0 && (
<div style={{ width: "100%" }}>
<div
onClick={onToggleExpand}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
width: "100%",
justifyContent: "flex-start",
cursor: "pointer",
padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
}}>
<span className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>Command Output</span>
</div>
{isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
</div>
)}
</div>
{requestsApproval && (
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: 8,
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
<i className="codicon codicon-warning"></i>
<span>The model has determined this command requires explicit approval</span>
</div>
)}
</>
)
}
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)
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "3px",
padding: "8px 10px",
marginTop: "8px",
}}>
{useMcpServer.type === "access_mcp_resource" && (
<McpResourceRow
item={{
// Use the matched resource/template details, with fallbacks
...(findMatchingResourceOrTemplate(
useMcpServer.uri || "",
server?.resources,
server?.resourceTemplates,
) || {
name: "",
mimeType: "",
description: "",
}),
// Always use the actual URI from the request
uri: useMcpServer.uri || "",
}}
/>
)}
{useMcpServer.type === "use_mcp_tool" && (
<>
<McpToolRow
tool={{
name: useMcpServer.toolName || "",
description:
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
?.description || "",
}}
/>
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
<div style={{ marginTop: "8px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Arguments
</div>
<CodeAccordian
code={useMcpServer.arguments}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
)}
</>
)}
</div>
</>
)
}
switch (message.type) {
case "say":
switch (message.say) {
@ -702,166 +886,7 @@ export const ChatRowContent = ({
<p style={{ ...pStyle, color: "var(--vscode-errorForeground)" }}>{message.text}</p>
</>
)
case "command":
const splitMessage = (text: string) => {
const outputIndex = text.indexOf(COMMAND_OUTPUT_STRING)
if (outputIndex === -1) {
return { command: text, output: "" }
}
return {
command: text.slice(0, outputIndex).trim(),
output: text
.slice(outputIndex + COMMAND_OUTPUT_STRING.length)
.trim()
.split("")
.map((char) => {
switch (char) {
case "\t":
return "→ "
case "\b":
return "⌫"
case "\f":
return "⏏"
case "\v":
return "⇳"
default:
return char
}
})
.join(""),
}
}
const { command: rawCommand, output } = splitMessage(message.text || "")
const requestsApproval = rawCommand.endsWith(COMMAND_REQ_APP_STRING)
const command = requestsApproval ? rawCommand.slice(0, -COMMAND_REQ_APP_STRING.length) : rawCommand
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
{/* <Terminal
rawOutput={command + (output ? "\n" + output : "")}
shouldAllowInput={!!isCommandExecuting && output.length > 0}
/> */}
<div
style={{
borderRadius: 3,
border: "1px solid var(--vscode-editorGroup-border)",
overflow: "hidden",
backgroundColor: CODE_BLOCK_BG_COLOR,
}}>
<CodeBlock source={`${"```"}shell\n${command}\n${"```"}`} forceWrap={true} />
{output.length > 0 && (
<div style={{ width: "100%" }}>
<div
onClick={onToggleExpand}
style={{
display: "flex",
alignItems: "center",
gap: "4px",
width: "100%",
justifyContent: "flex-start",
cursor: "pointer",
padding: `2px 8px ${isExpanded ? 0 : 8}px 8px`,
}}>
<span
className={`codicon codicon-chevron-${isExpanded ? "down" : "right"}`}></span>
<span style={{ fontSize: "0.8em" }}>Command Output</span>
</div>
{isExpanded && <CodeBlock source={`${"```"}shell\n${output}\n${"```"}`} />}
</div>
)}
</div>
{requestsApproval && (
<div
style={{
display: "flex",
alignItems: "center",
gap: 10,
padding: 8,
fontSize: "12px",
color: "var(--vscode-errorForeground)",
}}>
<i className="codicon codicon-warning"></i>
<span>The model has determined this command requires explicit approval</span>
</div>
)}
</>
)
case "use_mcp_server":
const useMcpServer = JSON.parse(message.text || "{}") as ClineAskUseMcpServer
const server = mcpServers.find((server) => server.name === useMcpServer.serverName)
return (
<>
<div style={headerStyle}>
{icon}
{title}
</div>
<div
style={{
background: "var(--vscode-textCodeBlock-background)",
borderRadius: "3px",
padding: "8px 10px",
marginTop: "8px",
}}>
{useMcpServer.type === "access_mcp_resource" && (
<McpResourceRow
item={{
// Use the matched resource/template details, with fallbacks
...(findMatchingResourceOrTemplate(
useMcpServer.uri || "",
server?.resources,
server?.resourceTemplates,
) || {
name: "",
mimeType: "",
description: "",
}),
// Always use the actual URI from the request
uri: useMcpServer.uri || "",
}}
/>
)}
{useMcpServer.type === "use_mcp_tool" && (
<>
<McpToolRow
tool={{
name: useMcpServer.toolName || "",
description:
server?.tools?.find((tool) => tool.name === useMcpServer.toolName)
?.description || "",
}}
/>
{useMcpServer.arguments && useMcpServer.arguments !== "{}" && (
<div style={{ marginTop: "8px" }}>
<div
style={{
marginBottom: "4px",
opacity: 0.8,
fontSize: "12px",
textTransform: "uppercase",
}}>
Arguments
</div>
<CodeAccordian
code={useMcpServer.arguments}
language="json"
isExpanded={true}
onToggleExpand={onToggleExpand}
/>
</div>
)}
</>
)}
</div>
</>
)
case "completion_result":
if (message.text) {
return (

View file

@ -52,8 +52,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>("Approve")
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>("Reject")
const [didClickCancel, setDidClickCancel] = useState(false)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
@ -186,6 +186,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "text":
case "browser_action":
case "browser_action_result":
case "browser_action_launch":
case "command":
case "use_mcp_server":
case "command_output":
case "mcp_server_request_started":
case "mcp_server_response":
@ -210,8 +213,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setTextAreaDisabled(false)
setClineAsk(undefined)
setEnableButtons(false)
setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined)
setPrimaryButtonText("Approve")
setSecondaryButtonText("Reject")
}
}, [messages.length])
@ -462,7 +465,13 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return ["browser_action_launch"].includes(message.ask!)
}
if (message.type === "say") {
return ["api_req_started", "text", "browser_action", "browser_action_result"].includes(message.say!)
return [
"browser_action_launch",
"api_req_started",
"text",
"browser_action",
"browser_action_result",
].includes(message.say!)
}
return false
}
@ -481,7 +490,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
visibleMessages.forEach((message) => {
if (message.ask === "browser_action_launch") {
if (message.ask === "browser_action_launch" || message.say === "browser_action_launch") {
// complete existing browser session if any
endBrowserSession()
// start new
@ -881,7 +890,7 @@ const ScrollToBottomButton = styled.div`
justify-content: center;
align-items: center;
flex: 1;
height: 25px;
height: 24px;
&:hover {
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 90%, transparent);

View file

@ -1,4 +1,4 @@
import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { VSCodeButton, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react"
import { memo, useEffect, useState } from "react"
import { useExtensionState } from "../../context/ExtensionStateContext"
import { validateApiConfiguration, validateModelId } from "../../utils/validate"
@ -12,15 +12,8 @@ type SettingsViewProps = {
}
const SettingsView = ({ onDone }: SettingsViewProps) => {
const {
apiConfiguration,
version,
customInstructions,
setCustomInstructions,
alwaysAllowReadOnly,
setAlwaysAllowReadOnly,
openRouterModels,
} = useExtensionState()
const { apiConfiguration, version, customInstructions, setCustomInstructions, openRouterModels } =
useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
const handleSubmit = () => {
@ -32,7 +25,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
if (!apiValidationResult && !modelIdValidationResult) {
vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
vscode.postMessage({ type: "customInstructions", text: customInstructions })
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
onDone()
}
}
@ -113,23 +105,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
checked={alwaysAllowReadOnly}
onChange={(e: any) => setAlwaysAllowReadOnly(e.target.checked)}>
<span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
When enabled, Cline will automatically view directory contents and read files without requiring
you to click the Approve button.
</p>
</div>
{IS_DEV && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>

View file

@ -22,7 +22,6 @@ interface ExtensionStateContextType extends ExtensionState {
filePaths: string[]
setApiConfiguration: (config: ApiConfiguration) => void
setCustomInstructions: (value?: string) => void
setAlwaysAllowReadOnly: (value: boolean) => void
setShowAnnouncement: (value: boolean) => void
}
@ -123,7 +122,6 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
filePaths,
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
}