From 63c766f380fd925d4656a5bb095d61989d9bf150 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Sun, 15 Dec 2024 19:47:13 -0800
Subject: [PATCH 01/30] fix soundEnabled init bug
---
src/core/webview/ClineProvider.ts | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index e998332780..15aa32ae0e 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -136,6 +136,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("Resolving webview view")
this.view = webviewView
+ // Initialize sound enabled state
+ this.getState().then(({ soundEnabled }) => {
+ setSoundEnabled(soundEnabled ?? false)
+ })
+
webviewView.webview.options = {
// Allow scripts in the webview
enableScripts: true,
From 09934e20f7bddc4d45ab92a618b838d828d92732 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Sun, 15 Dec 2024 23:13:32 -0800
Subject: [PATCH 02/30] only play sounds on errors, task completion, or when
user intervention is needed
---
webview-ui/src/components/chat/ChatView.tsx | 181 ++++++++++----------
1 file changed, 90 insertions(+), 91 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index e4e0880ba7..777425efcd 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -64,7 +64,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const [isAtBottom, setIsAtBottom] = useState(false)
const [wasStreaming, setWasStreaming] = useState(false)
- const [hasStarted, setHasStarted] = useState(false)
// UI layout depends on the last 2 messages
// (since it relies on the content of these messages, we are deep comparing. i.e. the button state after hitting button sets enableButtons to false, and this effect otherwise would have to true again even if messages didn't change
@@ -75,12 +74,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
vscode.postMessage({ type: "playSound", audioType })
}
- function playSoundOnMessage(audioType: AudioType) {
- if (hasStarted && !isStreaming) {
- playSound(audioType)
- }
- }
-
useDeepCompareEffect(() => {
// if last message is an ask, show user ask UI
// if user finished a task, then start a new task with a new conversation history since in this moment that the extension is waiting for user response, the user could close the extension and the conversation history would be lost.
@@ -91,7 +84,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
const isPartial = lastMessage.partial === true
switch (lastMessage.ask) {
case "api_req_failed":
- playSoundOnMessage("progress_loop")
+ playSound("progress_loop")
setTextAreaDisabled(true)
setClineAsk("api_req_failed")
setEnableButtons(true)
@@ -99,7 +92,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Start New Task")
break
case "mistake_limit_reached":
- playSoundOnMessage("progress_loop")
+ playSound("progress_loop")
setTextAreaDisabled(false)
setClineAsk("mistake_limit_reached")
setEnableButtons(true)
@@ -107,7 +100,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Start New Task")
break
case "followup":
- playSoundOnMessage("notification")
+ playSound("notification")
setTextAreaDisabled(isPartial)
setClineAsk("followup")
setEnableButtons(isPartial)
@@ -115,7 +108,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// setSecondaryButtonText(undefined)
break
case "tool":
- playSoundOnMessage("notification")
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
setTextAreaDisabled(isPartial)
setClineAsk("tool")
setEnableButtons(!isPartial)
@@ -134,7 +129,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
break
case "browser_action_launch":
- playSoundOnMessage("notification")
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
setTextAreaDisabled(isPartial)
setClineAsk("browser_action_launch")
setEnableButtons(!isPartial)
@@ -142,7 +139,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject")
break
case "command":
- playSoundOnMessage("notification")
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
setTextAreaDisabled(isPartial)
setClineAsk("command")
setEnableButtons(!isPartial)
@@ -150,7 +149,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject")
break
case "command_output":
- playSoundOnMessage("notification")
setTextAreaDisabled(false)
setClineAsk("command_output")
setEnableButtons(true)
@@ -166,7 +164,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
break
case "completion_result":
// extension waiting for feedback. but we can just present a new task button
- playSoundOnMessage("celebration")
+ playSound("celebration")
setTextAreaDisabled(isPartial)
setClineAsk("completion_result")
setEnableButtons(!isPartial)
@@ -174,7 +172,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined)
break
case "resume_task":
- playSoundOnMessage("notification")
setTextAreaDisabled(false)
setClineAsk("resume_task")
setEnableButtons(true)
@@ -183,7 +180,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setDidClickCancel(false) // special case where we reset the cancel button state
break
case "resume_completed_task":
- playSoundOnMessage("celebration")
+ playSound("celebration")
setTextAreaDisabled(false)
setClineAsk("resume_completed_task")
setEnableButtons(true)
@@ -482,30 +479,86 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return true
})
}, [modifiedMessages])
- useEffect(() => {
- if (isStreaming) {
- // Set to true once any request has started
- setHasStarted(true)
+
+ const isReadOnlyToolAction = (message: ClineMessage | undefined) => {
+ if (message?.type === "ask" && message.text) {
+ const tool = JSON.parse(message.text)
+ return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
}
+ return false
+ }
+
+ const isWriteToolAction = (message: ClineMessage | undefined) => {
+ if (message?.type === "ask" && message.text) {
+ const tool = JSON.parse(message.text)
+ return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
+ }
+ return false
+ }
+
+ const isMcpToolAlwaysAllowed = (message: ClineMessage | undefined) => {
+ if (message?.type === "ask" && message.ask === "use_mcp_server" && message.text) {
+ const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
+ if (mcpServerUse.type === "use_mcp_tool") {
+ const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
+ const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
+ return tool?.alwaysAllow || false
+ }
+ }
+ return false
+ }
+
+ const isAllowedCommand = (message: ClineMessage | undefined) => {
+ if (message?.type === "ask" && message.text) {
+ const command = message.text
+
+ // Split command by chaining operators
+ const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
+
+ // Check if all individual commands are allowed
+ return commands.every((cmd) => {
+ const trimmedCommand = cmd.toLowerCase()
+ return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
+ })
+ }
+ return false
+ }
+
+ const isAutoApproved = (message: ClineMessage | undefined) => {
+ if (!message || message.type !== "ask") return false
+
+ return (
+ (alwaysAllowBrowser && message.ask === "browser_action_launch") ||
+ (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
+ (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
+ (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
+ (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
+ )
+ }
+
+ useEffect(() => {
// Only execute when isStreaming changes from true to false
if (wasStreaming && !isStreaming && lastMessage) {
// Play appropriate sound based on lastMessage content
if (lastMessage.type === "ask") {
- switch (lastMessage.ask) {
- case "api_req_failed":
- case "mistake_limit_reached":
- playSound("progress_loop")
- break
- case "tool":
- case "followup":
- case "browser_action_launch":
- case "resume_task":
- playSound("notification")
- break
- case "completion_result":
- case "resume_completed_task":
- playSound("celebration")
- break
+ // Don't play sounds for auto-approved actions
+ if (!isAutoApproved(lastMessage)) {
+ switch (lastMessage.ask) {
+ case "api_req_failed":
+ case "mistake_limit_reached":
+ playSound("progress_loop")
+ break
+ case "tool":
+ case "followup":
+ case "browser_action_launch":
+ case "resume_task":
+ playSound("notification")
+ break
+ case "completion_result":
+ case "resume_completed_task":
+ playSound("celebration")
+ break
+ }
}
}
}
@@ -750,61 +803,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
// Only proceed if we have an ask and buttons are enabled
if (!clineAsk || !enableButtons) return
- const isReadOnlyToolAction = () => {
- const lastMessage = messages.at(-1)
- if (lastMessage?.type === "ask" && lastMessage.text) {
- const tool = JSON.parse(lastMessage.text)
- return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
- }
- return false
- }
-
- const isWriteToolAction = () => {
- const lastMessage = messages.at(-1)
- if (lastMessage?.type === "ask" && lastMessage.text) {
- const tool = JSON.parse(lastMessage.text)
- return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
- }
- return false
- }
-
- const isMcpToolAlwaysAllowed = () => {
- const lastMessage = messages.at(-1)
- if (lastMessage?.type === "ask" && lastMessage.ask === "use_mcp_server" && lastMessage.text) {
- const mcpServerUse = JSON.parse(lastMessage.text) as { type: string; serverName: string; toolName: string }
- if (mcpServerUse.type === "use_mcp_tool") {
- const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
- const tool = server?.tools?.find((t: McpTool) => t.name === mcpServerUse.toolName)
- return tool?.alwaysAllow || false
- }
- }
- return false
- }
-
- const isAllowedCommand = () => {
- const lastMessage = messages.at(-1)
- if (lastMessage?.type === "ask" && lastMessage.text) {
- const command = lastMessage.text
-
- // Split command by chaining operators
- const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
-
- // Check if all individual commands are allowed
- return commands.every((cmd) => {
- const trimmedCommand = cmd.toLowerCase()
- return allowedCommands?.some((prefix) => trimmedCommand.startsWith(prefix.toLowerCase()))
- })
- }
- return false
- }
-
- if (
- (alwaysAllowBrowser && clineAsk === "browser_action_launch") ||
- (alwaysAllowReadOnly && clineAsk === "tool" && isReadOnlyToolAction()) ||
- (alwaysAllowWrite && clineAsk === "tool" && isWriteToolAction()) ||
- (alwaysAllowExecute && clineAsk === "command" && isAllowedCommand()) ||
- (alwaysAllowMcp && clineAsk === "use_mcp_server" && isMcpToolAlwaysAllowed())
- ) {
+ if (isAutoApproved(lastMessage)) {
handlePrimaryButtonClick()
}
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])
From 1adc36a2928095371ee03859644aa4018574e8ae Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Sun, 15 Dec 2024 23:22:44 -0800
Subject: [PATCH 03/30] add volume setting
---
package-lock.json | 6 +++
package.json | 1 +
src/core/webview/ClineProvider.ts | 14 +++++-
src/shared/ExtensionMessage.ts | 1 +
src/shared/WebviewMessage.ts | 2 +
src/utils/sound.ts | 17 +++++--
.../src/components/settings/SettingsView.tsx | 50 +++++++++++++++----
.../src/context/ExtensionStateContext.tsx | 4 ++
8 files changed, 78 insertions(+), 17 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 744ce85443..43781fac91 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -38,6 +38,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
+ "sound-play": "^1.1.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
@@ -14001,6 +14002,11 @@
"node": ">= 14"
}
},
+ "node_modules/sound-play": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/sound-play/-/sound-play-1.1.0.tgz",
+ "integrity": "sha512-Bd/L0AoCwITFeOnpNLMsfPXrV5GG5NhrC/T6odveahYbhPZkdTnrFXRia9FCC5WBWdUTw1d+yvLBvi4wnD1xOA=="
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
diff --git a/package.json b/package.json
index 7d9d6d3744..ab18a7c506 100644
--- a/package.json
+++ b/package.json
@@ -220,6 +220,7 @@
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
+ "sound-play": "^1.1.0",
"strip-ansi": "^7.1.0",
"tree-sitter-wasms": "^0.1.11",
"turndown": "^7.2.0",
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index 15aa32ae0e..1ce56ae4c4 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -22,7 +22,7 @@ import { Cline } from "../Cline"
import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
-import { playSound, setSoundEnabled } from "../../utils/sound"
+import { playSound, setSoundEnabled, setSoundVolume } from "../../utils/sound"
/*
https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -66,6 +66,7 @@ type GlobalStateKey =
| "openRouterUseMiddleOutTransform"
| "allowedCommands"
| "soundEnabled"
+ | "soundVolume"
| "diffEnabled"
| "alwaysAllowMcp"
@@ -597,6 +598,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
setSoundEnabled(soundEnabled) // Add this line to update the sound utility
await this.postStateToWebview()
break
+ case "soundVolume":
+ const soundVolume = message.value ?? 0.5
+ await this.updateGlobalState("soundVolume", soundVolume)
+ setSoundVolume(soundVolume)
+ await this.postStateToWebview()
+ break
case "diffEnabled":
const diffEnabled = message.bool ?? true
await this.updateGlobalState("diffEnabled", diffEnabled)
@@ -929,6 +936,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundEnabled,
diffEnabled,
taskHistory,
+ soundVolume,
} = await this.getState()
const allowedCommands = vscode.workspace
@@ -953,6 +961,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled: diffEnabled ?? false,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
+ soundVolume: soundVolume ?? 0.5,
}
}
@@ -1045,6 +1054,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
+ soundVolume,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
this.getGlobalState("apiModelId") as Promise,
@@ -1082,6 +1092,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("allowedCommands") as Promise,
this.getGlobalState("soundEnabled") as Promise,
this.getGlobalState("diffEnabled") as Promise,
+ this.getGlobalState("soundVolume") as Promise,
])
let apiProvider: ApiProvider
@@ -1137,6 +1148,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
+ soundVolume,
}
}
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 608b5e5bb8..07a3dde396 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -51,6 +51,7 @@ export interface ExtensionState {
uriScheme?: string
allowedCommands?: string[]
soundEnabled?: boolean
+ soundVolume?: number
diffEnabled?: boolean
}
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 31802b9680..2864a94e3f 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -32,6 +32,7 @@ export interface WebviewMessage {
| "alwaysAllowMcp"
| "playSound"
| "soundEnabled"
+ | "soundVolume"
| "diffEnabled"
| "openMcpSettings"
| "restartMcpServer"
@@ -43,6 +44,7 @@ export interface WebviewMessage {
apiConfiguration?: ApiConfiguration
images?: string[]
bool?: boolean
+ value?: number
commands?: string[]
audioType?: AudioType
// For toggleToolAutoApprove
diff --git a/src/utils/sound.ts b/src/utils/sound.ts
index 9255db4344..a7f0d73925 100644
--- a/src/utils/sound.ts
+++ b/src/utils/sound.ts
@@ -21,6 +21,7 @@ export const isWAV = (filepath: string): boolean => {
}
let isSoundEnabled = false
+let volume = .5
/**
* Set sound configuration
@@ -30,6 +31,14 @@ export const setSoundEnabled = (enabled: boolean): void => {
isSoundEnabled = enabled
}
+/**
+ * Set sound volume
+ * @param volume number
+ */
+export const setSoundVolume = (newVolume: number): void => {
+ volume = newVolume
+}
+
/**
* Play a sound file
* @param filepath string
@@ -54,11 +63,9 @@ export const playSound = (filepath: string): void => {
return // Skip playback within minimum interval to prevent continuous playback
}
- const player = require("play-sound")()
- player.play(filepath, function (err: any) {
- if (err) {
- throw new Error("Failed to play sound effect")
- }
+ const sound = require("sound-play")
+ sound.play(filepath, volume).catch(() => {
+ throw new Error("Failed to play sound effect")
})
lastPlayedTime = currentTime
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index deab3d6c4a..f4e2da9f32 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -29,6 +29,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setAlwaysAllowMcp,
soundEnabled,
setSoundEnabled,
+ soundVolume,
+ setSoundVolume,
diffEnabled,
setDiffEnabled,
openRouterModels,
@@ -55,6 +57,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "alwaysAllowMcp", bool: alwaysAllowMcp })
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
+ vscode.postMessage({ type: "soundVolume", value: soundVolume })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
onDone()
}
@@ -306,17 +309,42 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
Experimental Features
-
setSoundEnabled(e.target.checked)}>
- Enable sound effects
-
-
- When enabled, Cline will play sound effects for notifications and events.
-
+
+
setSoundEnabled(e.target.checked)}>
+ Enable sound effects
+
+
+ When enabled, Cline will play sound effects for notifications and events.
+
+
+ {soundEnabled && (
+
+
+ Volume
+ setSoundVolume(parseFloat(e.target.value))}
+ style={{
+ flexGrow: 1,
+ accentColor: 'var(--vscode-button-background)',
+ height: '2px'
+ }}
+ />
+
+ {Math.round((soundVolume ?? 0.5) * 100)}%
+
+
+
+ )}
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index f9690b60c2..c8307ff110 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -29,6 +29,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setShowAnnouncement: (value: boolean) => void
setAllowedCommands: (value: string[]) => void
setSoundEnabled: (value: boolean) => void
+ setSoundVolume: (value: number) => void
setDiffEnabled: (value: boolean) => void
}
@@ -42,6 +43,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
shouldShowAnnouncement: false,
allowedCommands: [],
soundEnabled: false,
+ soundVolume: 0.5,
diffEnabled: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
@@ -129,6 +131,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openRouterModels,
mcpServers,
filePaths,
+ soundVolume: state.soundVolume,
setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
@@ -139,6 +142,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState, shouldShowAnnouncement: value })),
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
+ setSoundVolume: (value) => setState((prevState) => ({ ...prevState, soundVolume: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
}
From 5e771cf783cfd7198a975035c4d64569ead25de3 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Sun, 15 Dec 2024 23:44:50 -0800
Subject: [PATCH 04/30] add volume slider tests
---
.../settings/__tests__/SettingsView.test.tsx | 47 +++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
index 50fd597d6b..42d7021c8f 100644
--- a/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
+++ b/webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
@@ -104,6 +104,9 @@ describe('SettingsView - Sound Settings', () => {
name: /Enable sound effects/i
})
expect(soundCheckbox).not.toBeChecked()
+
+ // Volume slider should not be visible when sound is disabled
+ expect(screen.queryByRole('slider')).not.toBeInTheDocument()
})
it('toggles sound setting and sends message to VSCode', () => {
@@ -128,6 +131,50 @@ describe('SettingsView - Sound Settings', () => {
})
)
})
+
+ it('shows volume slider when sound is enabled', () => {
+ renderSettingsView()
+
+ // Enable sound
+ const soundCheckbox = screen.getByRole('checkbox', {
+ name: /Enable sound effects/i
+ })
+ fireEvent.click(soundCheckbox)
+
+ // Volume slider should be visible
+ const volumeSlider = screen.getByRole('slider')
+ expect(volumeSlider).toBeInTheDocument()
+ expect(volumeSlider).toHaveValue('0.5') // Default value
+ })
+
+ it('updates volume and sends message to VSCode when slider changes', () => {
+ renderSettingsView()
+
+ // Enable sound
+ const soundCheckbox = screen.getByRole('checkbox', {
+ name: /Enable sound effects/i
+ })
+ fireEvent.click(soundCheckbox)
+
+ // Change volume
+ const volumeSlider = screen.getByRole('slider')
+ fireEvent.change(volumeSlider, { target: { value: '0.75' } })
+
+ // Verify volume display updates
+ expect(screen.getByText('75%')).toBeInTheDocument()
+
+ // Click Done to save settings
+ const doneButton = screen.getByText('Done')
+ fireEvent.click(doneButton)
+
+ // Verify message sent to VSCode
+ expect(vscode.postMessage).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'soundVolume',
+ value: 0.75
+ })
+ )
+ })
})
describe('SettingsView - Allowed Commands', () => {
From 95222a9a5dd4cb96ed8edc61a63e09d1f68c1566 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Sun, 15 Dec 2024 23:47:10 -0800
Subject: [PATCH 05/30] add sounds to mcp server approval
---
webview-ui/src/components/chat/ChatView.tsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 777425efcd..a959cdbb72 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -156,6 +156,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
+ if (!isAutoApproved(lastMessage)) {
+ playSound("notification")
+ }
setTextAreaDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
@@ -552,6 +555,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "followup":
case "browser_action_launch":
case "resume_task":
+ case "use_mcp_server":
playSound("notification")
break
case "completion_result":
From efdc3a86392c464fbb5f18bb5e638efaea58c031 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 00:26:00 -0800
Subject: [PATCH 06/30] ChatView tests
---
.../chat/__tests__/ChatView.test.tsx | 244 ++++++++++++++++++
1 file changed, 244 insertions(+)
diff --git a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx
index ad7565a287..9e2d2c4218 100644
--- a/webview-ui/src/components/chat/__tests__/ChatView.test.tsx
+++ b/webview-ui/src/components/chat/__tests__/ChatView.test.tsx
@@ -547,3 +547,247 @@ describe('ChatView - Auto Approval Tests', () => {
})
})
})
+
+describe('ChatView - Sound Playing Tests', () => {
+ beforeEach(() => {
+ jest.clearAllMocks()
+ })
+
+ it('does not play sound for auto-approved browser actions', async () => {
+ render(
+
+ {}}
+ showHistoryView={() => {}}
+ />
+
+ )
+
+ // First hydrate state with initial task and streaming
+ mockPostMessage({
+ alwaysAllowBrowser: true,
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'say',
+ say: 'api_req_started',
+ ts: Date.now() - 1000,
+ text: JSON.stringify({}),
+ partial: true
+ }
+ ]
+ })
+
+ // Then send the browser action ask message (streaming finished)
+ mockPostMessage({
+ alwaysAllowBrowser: true,
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'ask',
+ ask: 'browser_action_launch',
+ ts: Date.now(),
+ text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
+ partial: false
+ }
+ ]
+ })
+
+ // Verify no sound was played
+ expect(vscode.postMessage).not.toHaveBeenCalledWith({
+ type: 'playSound',
+ audioType: expect.any(String)
+ })
+ })
+
+ it('plays notification sound for non-auto-approved browser actions', async () => {
+ render(
+
+ {}}
+ showHistoryView={() => {}}
+ />
+
+ )
+
+ // First hydrate state with initial task and streaming
+ mockPostMessage({
+ alwaysAllowBrowser: false,
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'say',
+ say: 'api_req_started',
+ ts: Date.now() - 1000,
+ text: JSON.stringify({}),
+ partial: true
+ }
+ ]
+ })
+
+ // Then send the browser action ask message (streaming finished)
+ mockPostMessage({
+ alwaysAllowBrowser: false,
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'ask',
+ ask: 'browser_action_launch',
+ ts: Date.now(),
+ text: JSON.stringify({ action: 'launch', url: 'http://example.com' }),
+ partial: false
+ }
+ ]
+ })
+
+ // Verify notification sound was played
+ await waitFor(() => {
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: 'playSound',
+ audioType: 'notification'
+ })
+ })
+ })
+
+ it('plays celebration sound for completion results', async () => {
+ render(
+
+ {}}
+ showHistoryView={() => {}}
+ />
+
+ )
+
+ // First hydrate state with initial task and streaming
+ mockPostMessage({
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'say',
+ say: 'api_req_started',
+ ts: Date.now() - 1000,
+ text: JSON.stringify({}),
+ partial: true
+ }
+ ]
+ })
+
+ // Then send the completion result message (streaming finished)
+ mockPostMessage({
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'ask',
+ ask: 'completion_result',
+ ts: Date.now(),
+ text: 'Task completed successfully',
+ partial: false
+ }
+ ]
+ })
+
+ // Verify celebration sound was played
+ await waitFor(() => {
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: 'playSound',
+ audioType: 'celebration'
+ })
+ })
+ })
+
+ it('plays progress_loop sound for api failures', async () => {
+ render(
+
+ {}}
+ showHistoryView={() => {}}
+ />
+
+ )
+
+ // First hydrate state with initial task and streaming
+ mockPostMessage({
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'say',
+ say: 'api_req_started',
+ ts: Date.now() - 1000,
+ text: JSON.stringify({}),
+ partial: true
+ }
+ ]
+ })
+
+ // Then send the api failure message (streaming finished)
+ mockPostMessage({
+ clineMessages: [
+ {
+ type: 'say',
+ say: 'task',
+ ts: Date.now() - 2000,
+ text: 'Initial task'
+ },
+ {
+ type: 'ask',
+ ask: 'api_req_failed',
+ ts: Date.now(),
+ text: 'API request failed',
+ partial: false
+ }
+ ]
+ })
+
+ // Verify progress_loop sound was played
+ await waitFor(() => {
+ expect(vscode.postMessage).toHaveBeenCalledWith({
+ type: 'playSound',
+ audioType: 'progress_loop'
+ })
+ })
+ })
+})
From 81aaa9c993d02d91a3dc952ae82cbb889e3e516f Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 16 Dec 2024 09:21:20 -0500
Subject: [PATCH 07/30] Add a hint that diff editing works best with latest
Sonnet
---
webview-ui/src/components/settings/SettingsView.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index ffa45d00a3..f2765fde8a 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -155,7 +155,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
- When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes.
+ When enabled, Cline will be able to edit files more quickly and will automatically reject truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
From c2b4b0545971721958ff5380bc81596724f8ebfb Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 16 Dec 2024 10:24:08 -0500
Subject: [PATCH 08/30] Diff debugging
---
CHANGELOG.md | 4 ++
package-lock.json | 4 +-
package.json | 2 +-
src/core/Cline.ts | 3 +-
src/core/__tests__/Cline.test.ts | 3 +-
src/core/diff/DiffStrategy.ts | 4 +-
src/core/diff/strategies/search-replace.ts | 56 +++++++++--------
src/core/diff/types.ts | 5 ++
src/core/webview/ClineProvider.ts | 61 ++++++++++++-------
.../misc/__tests__/extract-text.test.ts | 32 ++++++++++
src/integrations/misc/extract-text.ts | 11 ++--
src/shared/ExtensionMessage.ts | 1 +
src/shared/WebviewMessage.ts | 1 +
.../src/components/settings/SettingsView.tsx | 22 ++++++-
.../src/context/ExtensionStateContext.tsx | 11 +++-
15 files changed, 157 insertions(+), 63 deletions(-)
create mode 100644 src/integrations/misc/__tests__/extract-text.test.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85023ec804..eeba9728a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Roo Cline Changelog
+## [2.2.11]
+
+- Added settings checkbox for verbose diff debugging
+
## [2.2.6 - 2.2.10]
- More fixes to search/replace diffs
diff --git a/package-lock.json b/package-lock.json
index 3806ccb46b..4e5c9e40cd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "2.2.10",
+ "version": "2.2.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "2.2.10",
+ "version": "2.2.11",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
diff --git a/package.json b/package.json
index 2b605d313d..c0dd6697b9 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Cline",
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
"publisher": "RooVeterinaryInc",
- "version": "2.2.10",
+ "version": "2.2.11",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index 5b190d773f..e0f7c1b61f 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -97,6 +97,7 @@ export class Cline {
apiConfiguration: ApiConfiguration,
customInstructions?: string,
diffEnabled?: boolean,
+ debugDiffEnabled?: boolean,
task?: string,
images?: string[],
historyItem?: HistoryItem,
@@ -109,7 +110,7 @@ export class Cline {
this.diffViewProvider = new DiffViewProvider(cwd)
this.customInstructions = customInstructions
if (diffEnabled && this.api.getModel().id) {
- this.diffStrategy = getDiffStrategy(this.api.getModel().id)
+ this.diffStrategy = getDiffStrategy(this.api.getModel().id, debugDiffEnabled)
}
if (historyItem) {
this.taskId = historyItem.id
diff --git a/src/core/__tests__/Cline.test.ts b/src/core/__tests__/Cline.test.ts
index f50ed4f21a..041298fa4a 100644
--- a/src/core/__tests__/Cline.test.ts
+++ b/src/core/__tests__/Cline.test.ts
@@ -278,7 +278,8 @@ describe('Cline', () => {
mockProvider,
mockApiConfig,
'custom instructions',
- false,
+ false, // diffEnabled
+ false, // debugDiffEnabled
'test task'
);
diff --git a/src/core/diff/DiffStrategy.ts b/src/core/diff/DiffStrategy.ts
index 355424e48d..c35ea83c95 100644
--- a/src/core/diff/DiffStrategy.ts
+++ b/src/core/diff/DiffStrategy.ts
@@ -6,10 +6,10 @@ import { SearchReplaceDiffStrategy } from './strategies/search-replace'
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
* @returns The appropriate diff strategy for the model
*/
-export function getDiffStrategy(model: string): DiffStrategy {
+export function getDiffStrategy(model: string, debugEnabled?: boolean): DiffStrategy {
// For now, return SearchReplaceDiffStrategy for all models (with a fuzzy threshold of 0.9)
// This architecture allows for future optimizations based on model capabilities
- return new SearchReplaceDiffStrategy(0.9)
+ return new SearchReplaceDiffStrategy(0.9, debugEnabled)
}
export type { DiffStrategy }
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index a950b084b0..65a74dd735 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -1,4 +1,7 @@
import { DiffStrategy, DiffResult } from "../types"
+import { addLineNumbers } from "../../../integrations/misc/extract-text"
+
+const BUFFER_LINES = 5; // Number of extra context lines to show before and after matches
function levenshteinDistance(a: string, b: string): number {
const matrix: number[][] = [];
@@ -48,10 +51,12 @@ function getSimilarity(original: string, search: string): number {
export class SearchReplaceDiffStrategy implements DiffStrategy {
private fuzzyThreshold: number;
+ public debugEnabled: boolean;
- constructor(fuzzyThreshold?: number) {
+ constructor(fuzzyThreshold?: number, debugEnabled?: boolean) {
// Default to exact matching (1.0) unless fuzzy threshold specified
this.fuzzyThreshold = fuzzyThreshold ?? 1.0;
+ this.debugEnabled = debugEnabled ?? false;
}
getToolDescription(cwd: string): string {
@@ -119,15 +124,11 @@ Your search/replace content here
// Extract the search and replace blocks
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
if (!match) {
- // Log detailed format information
- console.log('Invalid Diff Format Debug:', {
- expectedFormat: "<<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE",
- tip: "Make sure to include both SEARCH and REPLACE sections with correct markers"
- });
+ const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
return {
success: false,
- error: "Invalid diff format - missing required SEARCH/REPLACE sections"
+ error: `Invalid diff format - missing required SEARCH/REPLACE sections${debugInfo}`
};
}
@@ -167,15 +168,11 @@ Your search/replace content here
const exactEndIndex = endLine - 1;
if (exactStartIndex < 0 || exactEndIndex >= originalLines.length) {
- // Log detailed debug information
- console.log('Invalid Line Range Debug:', {
- requestedRange: { start: startLine, end: endLine },
- fileBounds: { start: 1, end: originalLines.length }
- });
-
+ const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
+
return {
success: false,
- error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)`,
+ error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
};
}
@@ -198,11 +195,12 @@ Your search/replace content here
if (startLine !== undefined || endLine !== undefined) {
// Convert to 0-based index and add buffer
+ const BUFFER_LINES = 5;
if (startLine !== undefined) {
- searchStartIndex = Math.max(0, startLine - 6);
+ searchStartIndex = Math.max(0, startLine - (BUFFER_LINES + 1));
}
if (endLine !== undefined) {
- searchEndIndex = Math.min(originalLines.length, endLine + 5);
+ searchEndIndex = Math.min(originalLines.length, endLine + BUFFER_LINES);
}
}
@@ -224,17 +222,27 @@ Your search/replace content here
// Require similarity to meet threshold
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
const searchChunk = searchLines.join('\n');
- // Log detailed debug information to console
- console.log('Search/Replace Debug Info:', {
- similarity: bestMatchScore,
- threshold: this.fuzzyThreshold,
- searchContent: searchChunk,
- bestMatch: bestMatchContent || undefined
- });
+ const originalContentSection = startLine !== undefined && endLine !== undefined
+ ? `\n\nOriginal Content:\n${addLineNumbers(
+ originalLines.slice(
+ Math.max(0, startLine - 1 - BUFFER_LINES),
+ Math.min(originalLines.length, endLine + BUFFER_LINES)
+ ).join('\n'),
+ Math.max(1, startLine - BUFFER_LINES)
+ )}`
+ : `\n\nOriginal Content:\n${addLineNumbers(originalLines.join('\n'))}`;
+ const bestMatchSection = bestMatchContent
+ ? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
+ : `\n\nBest Match Found:\n(no match)`;
+
+ const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Line Range: lines ${startLine}-${endLine}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : '';
+
+ const lineRange = startLine !== undefined || endLine !== undefined ?
+ ` at ${startLine !== undefined ? `start: ${startLine}` : 'start'} to ${endLine !== undefined ? `end: ${endLine}` : 'end'}` : '';
return {
success: false,
- error: `No sufficiently similar match found${startLine !== undefined ? ` near lines ${startLine}-${endLine}` : ''} (${Math.round(bestMatchScore * 100)}% similar, needs ${Math.round(this.fuzzyThreshold * 100)}%)`
+ error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)${debugInfo}`
};
}
diff --git a/src/core/diff/types.ts b/src/core/diff/types.ts
index 3957a1f482..a662c479ba 100644
--- a/src/core/diff/types.ts
+++ b/src/core/diff/types.ts
@@ -13,6 +13,11 @@ export type DiffResult =
}};
export interface DiffStrategy {
+ /**
+ * Whether to enable detailed debug logging
+ */
+ debugEnabled?: boolean;
+
/**
* Get the tool description for this diff strategy
* @param cwd The current working directory
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index e998332780..3f17b067bf 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -67,6 +67,7 @@ type GlobalStateKey =
| "allowedCommands"
| "soundEnabled"
| "diffEnabled"
+ | "debugDiffEnabled"
| "alwaysAllowMcp"
export const GlobalFileNames = {
@@ -207,28 +208,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
async initClineWithTask(task?: string, images?: string[]) {
await this.clearTask()
- const {
- apiConfiguration,
- customInstructions,
- diffEnabled,
- } = await this.getState()
-
- this.cline = new Cline(
- this,
- apiConfiguration,
- customInstructions,
- diffEnabled,
- task,
- images
- )
- }
-
- async initClineWithHistoryItem(historyItem: HistoryItem) {
- await this.clearTask()
- const {
- apiConfiguration,
- customInstructions,
+ const {
+ apiConfiguration,
+ customInstructions,
diffEnabled,
+ debugDiffEnabled,
} = await this.getState()
this.cline = new Cline(
@@ -236,6 +220,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
apiConfiguration,
customInstructions,
diffEnabled,
+ debugDiffEnabled,
+ task,
+ images
+ )
+ }
+
+ async initClineWithHistoryItem(historyItem: HistoryItem) {
+ await this.clearTask()
+ const {
+ apiConfiguration,
+ customInstructions,
+ diffEnabled,
+ debugDiffEnabled,
+ } = await this.getState()
+
+ this.cline = new Cline(
+ this,
+ apiConfiguration,
+ customInstructions,
+ diffEnabled,
+ debugDiffEnabled,
undefined,
undefined,
historyItem,
@@ -597,6 +602,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("diffEnabled", diffEnabled)
await this.postStateToWebview()
break
+ case "debugDiffEnabled":
+ const debugDiffEnabled = message.bool ?? false
+ await this.updateGlobalState("debugDiffEnabled", debugDiffEnabled)
+ await this.postStateToWebview()
+ break
}
},
null,
@@ -923,6 +933,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowMcp,
soundEnabled,
diffEnabled,
+ debugDiffEnabled,
taskHistory,
} = await this.getState()
@@ -946,6 +957,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
.sort((a, b) => b.ts - a.ts),
soundEnabled: soundEnabled ?? false,
diffEnabled: diffEnabled ?? false,
+ debugDiffEnabled: debugDiffEnabled ?? false,
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
}
@@ -1040,6 +1052,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
allowedCommands,
soundEnabled,
diffEnabled,
+ debugDiffEnabled,
] = await Promise.all([
this.getGlobalState("apiProvider") as Promise,
this.getGlobalState("apiModelId") as Promise,
@@ -1077,6 +1090,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.getGlobalState("allowedCommands") as Promise,
this.getGlobalState("soundEnabled") as Promise,
this.getGlobalState("diffEnabled") as Promise,
+ this.getGlobalState("debugDiffEnabled") as Promise,
])
let apiProvider: ApiProvider
@@ -1130,8 +1144,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
alwaysAllowMcp: alwaysAllowMcp ?? false,
taskHistory,
allowedCommands,
- soundEnabled,
- diffEnabled,
+ soundEnabled: soundEnabled ?? false,
+ diffEnabled: diffEnabled ?? false,
+ debugDiffEnabled: debugDiffEnabled ?? false,
}
}
diff --git a/src/integrations/misc/__tests__/extract-text.test.ts b/src/integrations/misc/__tests__/extract-text.test.ts
new file mode 100644
index 0000000000..89adbb1ff5
--- /dev/null
+++ b/src/integrations/misc/__tests__/extract-text.test.ts
@@ -0,0 +1,32 @@
+import { addLineNumbers } from '../extract-text';
+
+describe('addLineNumbers', () => {
+ it('should add line numbers starting from 1 by default', () => {
+ const input = 'line 1\nline 2\nline 3';
+ const expected = '1 | line 1\n2 | line 2\n3 | line 3';
+ expect(addLineNumbers(input)).toBe(expected);
+ });
+
+ it('should add line numbers starting from specified line number', () => {
+ const input = 'line 1\nline 2\nline 3';
+ const expected = '10 | line 1\n11 | line 2\n12 | line 3';
+ expect(addLineNumbers(input, 10)).toBe(expected);
+ });
+
+ it('should handle empty content', () => {
+ expect(addLineNumbers('')).toBe('1 | ');
+ expect(addLineNumbers('', 5)).toBe('5 | ');
+ });
+
+ it('should handle single line content', () => {
+ expect(addLineNumbers('single line')).toBe('1 | single line');
+ expect(addLineNumbers('single line', 42)).toBe('42 | single line');
+ });
+
+ it('should pad line numbers based on the highest line number', () => {
+ const input = 'line 1\nline 2';
+ // When starting from 99, highest line will be 100, so needs 3 spaces padding
+ const expected = ' 99 | line 1\n100 | line 2';
+ expect(addLineNumbers(input, 99)).toBe(expected);
+ });
+});
\ No newline at end of file
diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts
index 3f9ff1c68e..576194c650 100644
--- a/src/integrations/misc/extract-text.ts
+++ b/src/integrations/misc/extract-text.ts
@@ -53,15 +53,12 @@ async function extractTextFromIPYNB(filePath: string): Promise {
return addLineNumbers(extractedText)
}
-
-export function addLineNumbers(content: string): string {
+export function addLineNumbers(content: string, startLine: number = 1): string {
const lines = content.split('\n')
- const maxLineNumberWidth = String(lines.length).length
+ const maxLineNumberWidth = String(startLine + lines.length - 1).length
return lines
.map((line, index) => {
- const lineNumber = String(index + 1).padStart(maxLineNumberWidth, ' ')
+ const lineNumber = String(startLine + index).padStart(maxLineNumberWidth, ' ')
return `${lineNumber} | ${line}`
}).join('\n')
-}
-
-
+}
\ No newline at end of file
diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts
index 608b5e5bb8..b9ba21f583 100644
--- a/src/shared/ExtensionMessage.ts
+++ b/src/shared/ExtensionMessage.ts
@@ -52,6 +52,7 @@ export interface ExtensionState {
allowedCommands?: string[]
soundEnabled?: boolean
diffEnabled?: boolean
+ debugDiffEnabled?: boolean
}
export interface ClineMessage {
diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts
index 31802b9680..d4377caf31 100644
--- a/src/shared/WebviewMessage.ts
+++ b/src/shared/WebviewMessage.ts
@@ -33,6 +33,7 @@ export interface WebviewMessage {
| "playSound"
| "soundEnabled"
| "diffEnabled"
+ | "debugDiffEnabled"
| "openMcpSettings"
| "restartMcpServer"
| "toggleToolAlwaysAllow"
diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx
index ffa45d00a3..28e42d3383 100644
--- a/webview-ui/src/components/settings/SettingsView.tsx
+++ b/webview-ui/src/components/settings/SettingsView.tsx
@@ -31,6 +31,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setSoundEnabled,
diffEnabled,
setDiffEnabled,
+ debugDiffEnabled,
+ setDebugDiffEnabled,
openRouterModels,
setAllowedCommands,
allowedCommands,
@@ -46,7 +48,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setApiErrorMessage(apiValidationResult)
setModelIdErrorMessage(modelIdValidationResult)
if (!apiValidationResult && !modelIdValidationResult) {
- vscode.postMessage({ type: "apiConfiguration", apiConfiguration })
+ vscode.postMessage({
+ type: "apiConfiguration",
+ apiConfiguration
+ })
vscode.postMessage({ type: "customInstructions", text: customInstructions })
vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
@@ -56,6 +61,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
vscode.postMessage({ type: "allowedCommands", commands: allowedCommands ?? [] })
vscode.postMessage({ type: "soundEnabled", bool: soundEnabled })
vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
+ vscode.postMessage({ type: "debugDiffEnabled", bool: debugDiffEnabled })
onDone()
}
}
@@ -324,6 +330,20 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
When enabled, Cline will play sound effects for notifications and events.
+
+
+
setDebugDiffEnabled(e.target.checked)}>
+ Debug diff operations
+
+
+ When enabled, Cline will show detailed debug information when applying diffs fails.
+
+
{IS_DEV && (
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx
index f9690b60c2..4d0e97f0a6 100644
--- a/webview-ui/src/context/ExtensionStateContext.tsx
+++ b/webview-ui/src/context/ExtensionStateContext.tsx
@@ -30,6 +30,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setAllowedCommands: (value: string[]) => void
setSoundEnabled: (value: boolean) => void
setDiffEnabled: (value: boolean) => void
+ setDebugDiffEnabled: (value: boolean) => void
}
const ExtensionStateContext = createContext(undefined)
@@ -43,6 +44,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
allowedCommands: [],
soundEnabled: false,
diffEnabled: false,
+ debugDiffEnabled: false,
})
const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false)
@@ -129,7 +131,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
openRouterModels,
mcpServers,
filePaths,
- setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value })),
+ setApiConfiguration: (value) => setState((prevState) => ({
+ ...prevState,
+ apiConfiguration: value
+ })),
setCustomInstructions: (value) => setState((prevState) => ({ ...prevState, customInstructions: value })),
setAlwaysAllowReadOnly: (value) => setState((prevState) => ({ ...prevState, alwaysAllowReadOnly: value })),
setAlwaysAllowWrite: (value) => setState((prevState) => ({ ...prevState, alwaysAllowWrite: value })),
@@ -140,6 +145,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setAllowedCommands: (value) => setState((prevState) => ({ ...prevState, allowedCommands: value })),
setSoundEnabled: (value) => setState((prevState) => ({ ...prevState, soundEnabled: value })),
setDiffEnabled: (value) => setState((prevState) => ({ ...prevState, diffEnabled: value })),
+ setDebugDiffEnabled: (value) => setState((prevState) => ({
+ ...prevState,
+ debugDiffEnabled: value
+ })),
}
return {children}
From 61b21043452e299fa33204361206ba674cb558fd Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 16 Dec 2024 12:22:23 -0500
Subject: [PATCH 09/30] Fix bug where start/end line not passed to diff
---
src/core/Cline.ts | 7 ++++++-
src/core/assistant-message/index.ts | 2 ++
src/core/diff/strategies/search-replace.ts | 17 ++++++++---------
3 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/src/core/Cline.ts b/src/core/Cline.ts
index e0f7c1b61f..e98be5e94b 100644
--- a/src/core/Cline.ts
+++ b/src/core/Cline.ts
@@ -1238,7 +1238,12 @@ export class Cline {
const originalContent = await fs.readFile(absolutePath, "utf-8")
// Apply the diff to the original content
- const diffResult = this.diffStrategy?.applyDiff(originalContent, diffContent) ?? {
+ const diffResult = this.diffStrategy?.applyDiff(
+ originalContent,
+ diffContent,
+ parseInt(block.params.start_line ?? ''),
+ parseInt(block.params.end_line ?? '')
+ ) ?? {
success: false,
error: "No diff strategy available"
}
diff --git a/src/core/assistant-message/index.ts b/src/core/assistant-message/index.ts
index d10967c0d0..241f8c7fe6 100644
--- a/src/core/assistant-message/index.ts
+++ b/src/core/assistant-message/index.ts
@@ -44,6 +44,8 @@ export const toolParamNames = [
"question",
"result",
"diff",
+ "start_line",
+ "end_line",
] as const
export type ToolParamName = (typeof toolParamNames)[number]
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index 65a74dd735..2fbfe5bebd 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -162,12 +162,12 @@ Your search/replace content here
let bestMatchScore = 0;
let bestMatchContent = "";
- if (startLine !== undefined && endLine !== undefined) {
+ if (startLine && endLine) {
// Convert to 0-based index
const exactStartIndex = startLine - 1;
const exactEndIndex = endLine - 1;
- if (exactStartIndex < 0 || exactEndIndex >= originalLines.length) {
+ if (exactStartIndex < 0 || exactEndIndex >= originalLines.length || exactStartIndex > exactEndIndex) {
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
return {
@@ -193,13 +193,12 @@ Your search/replace content here
let searchStartIndex = 0;
let searchEndIndex = originalLines.length;
- if (startLine !== undefined || endLine !== undefined) {
+ if (startLine || endLine) {
// Convert to 0-based index and add buffer
- const BUFFER_LINES = 5;
- if (startLine !== undefined) {
+ if (startLine) {
searchStartIndex = Math.max(0, startLine - (BUFFER_LINES + 1));
}
- if (endLine !== undefined) {
+ if (endLine) {
searchEndIndex = Math.min(originalLines.length, endLine + BUFFER_LINES);
}
}
@@ -236,10 +235,10 @@ Your search/replace content here
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
: `\n\nBest Match Found:\n(no match)`;
- const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Line Range: lines ${startLine}-${endLine}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : '';
+ const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : 'start to end'}\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}` : '';
- const lineRange = startLine !== undefined || endLine !== undefined ?
- ` at ${startLine !== undefined ? `start: ${startLine}` : 'start'} to ${endLine !== undefined ? `end: ${endLine}` : 'end'}` : '';
+ const lineRange = startLine || endLine ?
+ ` at ${startLine ? `start: ${startLine}` : 'start'} to ${endLine ? `end: ${endLine}` : 'end'}` : '';
return {
success: false,
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)${debugInfo}`
From c66fe250601a2d38282334b1d37958502bdd2329 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 09:30:20 -0800
Subject: [PATCH 10/30] ignore command and mcp asks with no text
---
webview-ui/src/components/chat/ChatView.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index a959cdbb72..604285d167 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -139,7 +139,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject")
break
case "command":
- if (!isAutoApproved(lastMessage)) {
+ if (lastMessage.text && !isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial)
@@ -156,7 +156,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
- if (!isAutoApproved(lastMessage)) {
+ if (lastMessage.text && !isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial)
From 05d6c295be548bbee34b1d5cd0f2b81acfa6031a Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 10:41:13 -0800
Subject: [PATCH 11/30] fix auto-approved and duplicate sounds
---
webview-ui/src/components/chat/ChatView.tsx | 31 +++++++++++++++------
1 file changed, 23 insertions(+), 8 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 604285d167..6972430a2f 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -100,7 +100,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Start New Task")
break
case "followup":
- playSound("notification")
setTextAreaDisabled(isPartial)
setClineAsk("followup")
setEnableButtons(isPartial)
@@ -139,7 +138,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText("Reject")
break
case "command":
- if (lastMessage.text && !isAutoApproved(lastMessage)) {
+ if (!isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial)
@@ -156,7 +155,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
- if (lastMessage.text && !isAutoApproved(lastMessage)) {
+ if (!isAutoApproved(lastMessage)) {
playSound("notification")
}
setTextAreaDisabled(isPartial)
@@ -484,7 +483,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}, [modifiedMessages])
const isReadOnlyToolAction = (message: ClineMessage | undefined) => {
- if (message?.type === "ask" && message.text) {
+ if (message?.type === "ask") {
+ if (!message.text) {
+ return true
+ }
const tool = JSON.parse(message.text)
return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
}
@@ -492,7 +494,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
const isWriteToolAction = (message: ClineMessage | undefined) => {
- if (message?.type === "ask" && message.text) {
+ if (message?.type === "ask") {
+ if (!message.text) {
+ return true
+ }
const tool = JSON.parse(message.text)
return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
}
@@ -500,7 +505,10 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
const isMcpToolAlwaysAllowed = (message: ClineMessage | undefined) => {
- if (message?.type === "ask" && message.ask === "use_mcp_server" && message.text) {
+ if (message?.type === "ask" && message.ask === "use_mcp_server") {
+ if (!message.text) {
+ return true
+ }
const mcpServerUse = JSON.parse(message.text) as { type: string; serverName: string; toolName: string }
if (mcpServerUse.type === "use_mcp_tool") {
const server = mcpServers?.find((s: McpServer) => s.name === mcpServerUse.serverName)
@@ -512,8 +520,11 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
const isAllowedCommand = (message: ClineMessage | undefined) => {
- if (message?.type === "ask" && message.text) {
+ if (message?.type === "ask") {
const command = message.text
+ if (!command) {
+ return true
+ }
// Split command by chaining operators
const commands = command.split(/&&|\|\||;|\||\$\(|`/).map(cmd => cmd.trim())
@@ -551,8 +562,12 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
case "mistake_limit_reached":
playSound("progress_loop")
break
- case "tool":
case "followup":
+ if (!lastMessage.partial) {
+ playSound("notification")
+ }
+ break
+ case "tool":
case "browser_action_launch":
case "resume_task":
case "use_mcp_server":
From d4ceee396d12f070b2446d75546629177568a58d Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 10:46:11 -0800
Subject: [PATCH 12/30] uninstall play-sound
---
package-lock.json | 18 +-----------------
package.json | 1 -
2 files changed, 1 insertion(+), 18 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 9790f1d527..ae7aa41ee5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -34,7 +34,6 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
- "play-sound": "^1.1.6",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
@@ -8851,14 +8850,6 @@
"node": ">=8"
}
},
- "node_modules/find-exec": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/find-exec/-/find-exec-1.0.3.tgz",
- "integrity": "sha512-gnG38zW90mS8hm5smNcrBnakPEt+cGJoiMkJwCU0IYnEb0H2NQk0NIljhNW+48oniCriFek/PH6QXbwsJo/qug==",
- "dependencies": {
- "shell-quote": "^1.8.1"
- }
- },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -13103,14 +13094,6 @@
"node": ">=8"
}
},
- "node_modules/play-sound": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/play-sound/-/play-sound-1.1.6.tgz",
- "integrity": "sha512-09eO4QiXNFXJffJaOW5P6x6F5RLihpLUkXttvUZeWml0fU6x6Zp7AjG9zaeMpgH2ZNvq4GR1ytB22ddYcqJIZA==",
- "dependencies": {
- "find-exec": "1.0.3"
- }
- },
"node_modules/possible-typed-array-names": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
@@ -13874,6 +13857,7 @@
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
"integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+ "dev": true,
"engines": {
"node": ">= 0.4"
},
diff --git a/package.json b/package.json
index 2f74913692..98dc4bdd4e 100644
--- a/package.json
+++ b/package.json
@@ -216,7 +216,6 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
- "play-sound": "^1.1.6",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
"serialize-error": "^11.0.3",
From 5eb848b7c8926d5f2fc3895e9c277f59528239d6 Mon Sep 17 00:00:00 2001
From: a8trejo
Date: Mon, 16 Dec 2024 11:21:34 -0800
Subject: [PATCH 13/30] Remove AI releases
---
.github/actions/ai-release-notes/action.yml | 12 +-
.github/workflows/changeset-ai-releases.yml | 216 --------------------
.github/workflows/changeset-release.yml | 91 +++++++++
.gitignore | 4 +
4 files changed, 101 insertions(+), 222 deletions(-)
delete mode 100644 .github/workflows/changeset-ai-releases.yml
create mode 100644 .github/workflows/changeset-release.yml
diff --git a/.github/actions/ai-release-notes/action.yml b/.github/actions/ai-release-notes/action.yml
index 294e64500c..3a74a099b0 100644
--- a/.github/actions/ai-release-notes/action.yml
+++ b/.github/actions/ai-release-notes/action.yml
@@ -20,15 +20,15 @@ inputs:
default: ''
type: string
git_ref:
- required: false
+ required: true
type: string
default: ''
head_ref:
- required: false
+ required: true
type: string
default: main
base_ref:
- required: false
+ required: true
type: string
default: main
@@ -41,9 +41,9 @@ outputs:
value: ${{ steps.ai_prompt.outputs.OPENAI_PROMPT }}
env:
- GITHUB_REF: ${{ inputs.git_ref == '' && github.event.pull_request.head.ref || inputs.git_ref }}
- BASE_REF: ${{ inputs.base_ref == '' && github.base_ref || inputs.base_ref }}
- HEAD_REF: ${{ inputs.head_ref == '' && github.event.pull_request.head.sha || inputs.head_ref }}
+ GITHUB_REF: ${{ inputs.git_ref }}
+ BASE_REF: ${{ inputs.base_ref }}
+ HEAD_REF: ${{ inputs.head_ref }}
runs:
using: "composite"
diff --git a/.github/workflows/changeset-ai-releases.yml b/.github/workflows/changeset-ai-releases.yml
deleted file mode 100644
index d7e7310467..0000000000
--- a/.github/workflows/changeset-ai-releases.yml
+++ /dev/null
@@ -1,216 +0,0 @@
-name: Changeset AI Release
-run-name: Changeset AI Release ${{ github.actor != 'R00-B0T' && '- Create PR' || '- Approve & Release' }}
-
-# This workflow automates the release process by:
-# 1. Creating a version bump PR when changesets are merged to main
-# 2. Using AI to generate release notes for the version bump PR
-# 3. Auto-approving and merging the version bump PR
-# 4. Creating a GitHub release with the AI-generated notes
-
-on:
- # pull_request:
- # types: [closed, opened, synchronize, labeled]
- workflow_dispatch:
-
-env:
- REPO_PATH: ${{ github.repository }}
- GIT_REF: ${{ github.event.pull_request.head.sha }}
-
-jobs:
- # Job 1: Create version bump PR when changesets are merged to main
- changeset-pr-version-bump:
- if: >
- github.event_name == 'pull_request' &&
- github.event.pull_request.merged == true &&
- github.event.pull_request.base.ref == 'main' &&
- github.actor != 'R00-B0T'
- runs-on: ubuntu-latest
- permissions:
- contents: write
- pull-requests: write
- steps:
- - name: Git Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- ref: ${{ env.GIT_REF }}
-
- - name: Setup Node.js
- uses: actions/setup-node@v4
- with:
- node-version: 20
- cache: 'npm'
-
- - name: Install Dependencies
- run: npm install
-
- # Check if there are any new changesets to process
- - name: Check for changesets
- id: check-changesets
- run: |
- NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
- echo "Changesets diff with previous version: $NEW_CHANGESETS"
- echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
-
- # Create version bump PR using changesets/action if there are new changesets
- - name: Changeset Pull Request
- if: steps.check-changesets.outputs.new_changesets != '0'
- id: changesets
- uses: changesets/action@v1
- with:
- commit: "changeset version bump"
- title: "Changeset version bump"
- version: npm run version-packages # This performs the changeset version bump
- env:
- GITHUB_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
-
- # Job 2: Process version bump PR created by R00-B0T
- changeset-pr-approve-merge:
- name: Auto approve and merge Bump version PRs
- runs-on: ubuntu-latest
- permissions:
- contents: write
- pull-requests: write
- if: >
- github.event_name == 'pull_request' &&
- github.event.pull_request.base.ref == 'main' &&
- github.actor == 'R00-B0T' &&
- contains(github.event.pull_request.title, 'Changeset version bump')
-
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
- fetch-depth: 0
- ref: ${{ env.GIT_REF }}
-
- # Get current and previous versions for changelog processing
- - name: Get version
- id: get_version
- run: |
- VERSION=$(git show HEAD:package.json | jq -r '.version')
- echo "version=$VERSION" >> $GITHUB_OUTPUT
- PREV_VERSION=$(git show origin/main:package.json | jq -r '.version')
- echo "prev_version=$PREV_VERSION" >> $GITHUB_OUTPUT
-
- echo "version=$VERSION"
- echo "prev_version=$PREV_VERSION"
-
- # Get previous version refs, GITHUB_OUTPUT: 'BASE_REF' and 'HEAD_REF'
- - name: Get Previous Version Refs
- id: version_refs
- run: python .github/scripts/get_prev_version_refs.py
-
- # Generate release notes using OpenAI if not already edited, GITHUB_OUTPUT: 'RELEASE_NOTES' and 'OPENAI_PROMPT'
- - name: AI Release Notes
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
- uses: ./.github/actions/ai-release-notes
- id: ai_release_notes
- with:
- GHA_PAT: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
- OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- model_name: gpt-4o-mini
- repo_path: ${{ env.REPO_PATH }}
- base_ref: ${{ steps.version_refs.outputs.base_ref }}
- head_ref: ${{ steps.version_refs.outputs.head_ref }}
-
- # Update CHANGELOG.md with AI-generated notes
- - name: Update Changeset Changelog
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
- env:
- VERSION: ${{ steps.get_version.outputs.version }}
- PREV_VERSION: ${{ steps.get_version.outputs.prev_version }}
- NEW_CONTENT: ${{ steps.ai_release_notes.outputs.RELEASE_NOTES }}
- run: python .github/scripts/overwrite_changeset_changelog.py
-
- # Commit and push changelog updates
- - name: Push Changelog updates
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
- run: |
- git config user.name "R00-B0T"
- git config user.email github-actions@github.com
- git status
-
- echo "Updating changelog.md..."
- git add CHANGELOG.md
- git commit -m "Updating changeset changelog"
-
- echo "--------------------------------------------------------------------------------"
- echo "Pushing to remote..."
- echo "--------------------------------------------------------------------------------"
- git push
-
- # Add label to indicate OpenAI has processed this PR
- - name: Add openai-edited label
- if: ${{ !contains(github.event.pull_request.labels.*.name, 'openai-edited') }}
- uses: actions/github-script@v7
- with:
- github-token: ${{ secrets.GITHUB_TOKEN }}
- script: |
- await github.rest.issues.addLabels({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number,
- labels: ['openai-edited']
- });
-
- # Auto-approve PR once OpenAI has processed it
- - name: Auto approve PR
- if: contains(github.event.pull_request.labels.*.name, 'openai-edited')
- uses: hmarr/auto-approve-action@v4
- with:
- review-message: "I'm approving since it's a bump version PR"
-
- # Enable auto-merge for the PR
- - name: Enable automerge on PR
- if: contains(github.event.pull_request.labels.*.name, 'openai-edited')
- run: gh pr merge --squash --auto ${{ github.event.pull_request.number }}
- env:
- GH_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
-
- # Job 3: Create GitHub release after version bump PR is merged
- github-release:
- runs-on: ubuntu-latest
- if: >
- github.event_name == 'pull_request' &&
- github.event.pull_request.merged == true &&
- github.event.pull_request.base.ref == 'main' &&
- github.actor == 'R00-B0T' &&
- contains(github.event.pull_request.title, 'Changeset version bump')
- permissions:
- contents: write
- steps:
- - name: Checkout Repo
- uses: actions/checkout@v4
- with:
- ref: ${{ github.event.pull_request.head.sha }}
- fetch-depth: 0
-
- - name: Get version
- id: get_version
- run: |
- VERSION=$(git show HEAD:package.json | jq -r '.version')
- echo "version=$VERSION" >> $GITHUB_OUTPUT
-
- # Extract release notes from CHANGELOG.md, GITHUB_OUTPUT: 'release-notes'
- - name: Parse CHANGELOG.md
- id: changelog
- env:
- CHANGELOG_PATH: CHANGELOG.md
- VERSION: ${{ steps.get_version.outputs.version }}
- run: python .github/scripts/parse_changeset_changelog.py
-
- # Create GitHub release with extracted notes
- - name: Create or Update Release
- uses: softprops/action-gh-release@v2
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- with:
- tag_name: v${{ steps.get_version.outputs.version }}
- name: Release v${{ steps.get_version.outputs.version }}
- draft: false
- prerelease: false
- append_body: false
- make_latest: true
- body: ${{ steps.changelog.outputs.release-notes }}
diff --git a/.github/workflows/changeset-release.yml b/.github/workflows/changeset-release.yml
new file mode 100644
index 0000000000..2214187a2c
--- /dev/null
+++ b/.github/workflows/changeset-release.yml
@@ -0,0 +1,91 @@
+name: Changeset Release
+run-name: Changeset Release ${{ github.actor != 'R00-B0T' && '- Create PR' || '- Approve & Merge' }}
+
+on:
+ pull_request:
+ types: [closed, opened, synchronize, labeled]
+
+env:
+ REPO_PATH: ${{ github.repository }}
+ GIT_REF: ${{ github.event.pull_request.head.sha }}
+
+jobs:
+ # Job 1: Create version bump PR when changesets are merged to main
+ changeset-pr-version-bump:
+ if: >
+ github.event_name == 'pull_request' &&
+ github.event.pull_request.merged == true &&
+ github.event.pull_request.base.ref == 'main' &&
+ github.actor != 'R00-B0T'
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ steps:
+ - name: Git Checkout
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ ref: ${{ env.GIT_REF }}
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'npm'
+
+ - name: Install Dependencies
+ run: npm install
+
+ # Check if there are any new changesets to process
+ - name: Check for changesets
+ id: check-changesets
+ run: |
+ NEW_CHANGESETS=$(find .changeset -name "*.md" ! -name "README.md" | wc -l | tr -d ' ')
+ echo "Changesets diff with previous version: $NEW_CHANGESETS"
+ echo "new_changesets=$NEW_CHANGESETS" >> $GITHUB_OUTPUT
+
+ # Create version bump PR using changesets/action if there are new changesets
+ - name: Changeset Pull Request
+ if: steps.check-changesets.outputs.new_changesets != '0'
+ id: changesets
+ uses: changesets/action@v1
+ with:
+ commit: "changeset version bump"
+ title: "Changeset version bump"
+ version: npm run version-packages # This performs the changeset version bump
+ env:
+ GITHUB_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
+
+ # Job 2: Process version bump PR created by R00-B0T
+ changeset-pr-approve-merge:
+ name: Auto approve and merge Bump version PRs
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+ pull-requests: write
+ if: >
+ github.event_name == 'pull_request' &&
+ github.event.pull_request.base.ref == 'main' &&
+ github.actor == 'R00-B0T' &&
+ contains(github.event.pull_request.title, 'Changeset version bump')
+
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v4
+ with:
+ token: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
+ fetch-depth: 0
+ ref: ${{ env.GIT_REF }}
+
+ # Auto-approve PR
+ - name: Auto approve PR
+ uses: hmarr/auto-approve-action@v4
+ with:
+ review-message: "I'm approving since it's a bump version PR"
+
+ # Enable auto-merge for the PR
+ - name: Enable automerge on PR
+ run: gh pr merge --squash --auto ${{ github.event.pull_request.number }}
+ env:
+ GH_TOKEN: ${{ secrets.CROSS_REPO_ACCESS_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 6cbcda5ce1..734145844e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,7 @@ node_modules
# Builds
bin/
roo-cline-*.vsix
+
+# Local prompts and rules
+prompts
+.clinerules
From d455428978c23b106a6ae17ac3d146e09a1dff9e Mon Sep 17 00:00:00 2001
From: a8trejo
Date: Mon, 16 Dec 2024 11:24:39 -0800
Subject: [PATCH 14/30] ellipsis comments
---
.github/actions/ai-release-notes/action.yml | 3 ---
1 file changed, 3 deletions(-)
diff --git a/.github/actions/ai-release-notes/action.yml b/.github/actions/ai-release-notes/action.yml
index 3a74a099b0..575e49c97c 100644
--- a/.github/actions/ai-release-notes/action.yml
+++ b/.github/actions/ai-release-notes/action.yml
@@ -22,15 +22,12 @@ inputs:
git_ref:
required: true
type: string
- default: ''
head_ref:
required: true
type: string
- default: main
base_ref:
required: true
type: string
- default: main
outputs:
RELEASE_NOTES:
From 58402f4654c6f1c5475bac1f3e52190223e70512 Mon Sep 17 00:00:00 2001
From: a8trejo
Date: Mon, 16 Dec 2024 12:13:57 -0800
Subject: [PATCH 15/30] PR comments
---
.gitignore | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/.gitignore b/.gitignore
index 734145844e..feb1fc292a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,5 +10,4 @@ bin/
roo-cline-*.vsix
# Local prompts and rules
-prompts
-.clinerules
+local-prompts
From 905c68dd9ea7428867264e026b0a0b5f80044881 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 16 Dec 2024 11:43:08 -0500
Subject: [PATCH 16/30] Handle pure insertions and deletions with diffs
---
CHANGELOG.md | 4 +
package-lock.json | 4 +-
package.json | 2 +-
.../__tests__/search-replace.test.ts | 210 +++++++++++++++++-
src/core/diff/strategies/search-replace.ts | 91 ++++++--
5 files changed, 290 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index eeba9728a0..f7db7df978 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Roo Cline Changelog
+## [2.2.12]
+
+- Better support for pure deletion and insertion diffs
+
## [2.2.11]
- Added settings checkbox for verbose diff debugging
diff --git a/package-lock.json b/package-lock.json
index 4e5c9e40cd..6ea5438041 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "2.2.11",
+ "version": "2.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "2.2.11",
+ "version": "2.2.12",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
diff --git a/package.json b/package.json
index c0dd6697b9..2f76cca5fb 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Cline",
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
"publisher": "RooVeterinaryInc",
- "version": "2.2.11",
+ "version": "2.2.12",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
diff --git a/src/core/diff/strategies/__tests__/search-replace.test.ts b/src/core/diff/strategies/__tests__/search-replace.test.ts
index 8e48680c74..99aff99482 100644
--- a/src/core/diff/strategies/__tests__/search-replace.test.ts
+++ b/src/core/diff/strategies/__tests__/search-replace.test.ts
@@ -711,6 +711,212 @@ this.init();
})
});
+ describe('insertion/deletion', () => {
+ let strategy: SearchReplaceDiffStrategy
+
+ beforeEach(() => {
+ strategy = new SearchReplaceDiffStrategy()
+ })
+
+ describe('deletion', () => {
+ it('should delete code when replace block is empty', () => {
+ const originalContent = `function test() {
+ console.log("hello");
+ // Comment to remove
+ console.log("world");
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+ // Comment to remove
+=======
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`function test() {
+ console.log("hello");
+ console.log("world");
+}`)
+ }
+ })
+
+ it('should delete multiple lines when replace block is empty', () => {
+ const originalContent = `class Example {
+ constructor() {
+ // Initialize
+ this.value = 0;
+ // Set defaults
+ this.name = "";
+ // End init
+ }
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+ // Initialize
+ this.value = 0;
+ // Set defaults
+ this.name = "";
+ // End init
+=======
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`class Example {
+ constructor() {
+ }
+}`)
+ }
+ })
+
+ it('should preserve indentation when deleting nested code', () => {
+ const originalContent = `function outer() {
+ if (true) {
+ // Remove this
+ console.log("test");
+ // And this
+ }
+ return true;
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+ // Remove this
+ console.log("test");
+ // And this
+=======
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`function outer() {
+ if (true) {
+ }
+ return true;
+}`)
+ }
+ })
+ })
+
+ describe('insertion', () => {
+ it('should insert code at specified line when search block is empty', () => {
+ const originalContent = `function test() {
+ const x = 1;
+ return x;
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+=======
+ console.log("Adding log");
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`function test() {
+ console.log("Adding log");
+ const x = 1;
+ return x;
+}`)
+ }
+ })
+
+ it('should preserve indentation when inserting at nested location', () => {
+ const originalContent = `function test() {
+ if (true) {
+ const x = 1;
+ }
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+=======
+ console.log("Before");
+ console.log("After");
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`function test() {
+ if (true) {
+ console.log("Before");
+ console.log("After");
+ const x = 1;
+ }
+}`)
+ }
+ })
+
+ it('should handle insertion at start of file', () => {
+ const originalContent = `function test() {
+ return true;
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+=======
+// Copyright 2024
+// License: MIT
+
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`// Copyright 2024
+// License: MIT
+
+function test() {
+ return true;
+}`)
+ }
+ })
+
+ it('should handle insertion at end of file', () => {
+ const originalContent = `function test() {
+ return true;
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+=======
+
+// End of file
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`function test() {
+ return true;
+}
+
+// End of file`)
+ }
+ })
+
+ it('should insert at the start of the file if no start_line is provided for insertion', () => {
+ const originalContent = `function test() {
+ return true;
+}`
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+=======
+console.log("test");
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe(`console.log("test");
+function test() {
+ return true;
+}`)
+ }
+ })
+ })
+ })
+
describe('fuzzy matching', () => {
let strategy: SearchReplaceDiffStrategy
@@ -1241,8 +1447,8 @@ function two() {
it('should document start_line and end_line parameters', () => {
const description = strategy.getToolDescription('/test')
- expect(description).toContain('start_line: (required) The line number where the search block starts.')
- expect(description).toContain('end_line: (required) The line number where the search block ends.')
+ expect(description).toContain('start_line: (required) The line number where the search block starts (inclusive).')
+ expect(description).toContain('end_line: (required) The line number where the search block ends (inclusive).')
})
})
})
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index 2fbfe5bebd..660d3642b7 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -33,6 +33,10 @@ function levenshteinDistance(a: string, b: string): number {
}
function getSimilarity(original: string, search: string): number {
+ if (original === '' || search === '') {
+ return 1;
+ }
+
// Normalize strings by removing extra whitespace but preserve case
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
@@ -71,8 +75,8 @@ If you're not confident in the exact content to search for, use the read_file to
Parameters:
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
- diff: (required) The search/replace block defining the changes.
-- start_line: (required) The line number where the search block starts.
-- end_line: (required) The line number where the search block ends.
+- start_line: (required) The line number where the search block starts (inclusive).
+- end_line: (required) The line number where the search block ends (inclusive).
Diff format:
\`\`\`
@@ -94,35 +98,84 @@ Original file:
5 | return total
\`\`\`
-Search/Replace content:
+1. Search/replace a specific chunk of code:
\`\`\`
+
+File path here
+
<<<<<<< SEARCH
-def calculate_total(items):
total = 0
for item in items:
total += item
return total
=======
-def calculate_total(items):
"""Calculate total with 10% markup"""
return sum(item * 1.1 for item in items)
>>>>>>> REPLACE
+
+2
+5
+
\`\`\`
-Usage:
+Result:
+\`\`\`
+1 | def calculate_total(items):
+2 | """Calculate total with 10% markup"""
+3 | return sum(item * 1.1 for item in items)
+\`\`\`
+
+2. Insert code at a specific line (start_line and end_line must be the same, and the content gets inserted before whatever is currently at that line):
+\`\`\`
File path here
-Your search/replace content here
+<<<<<<< SEARCH
+=======
+ """TODO: Write a test for this"""
+>>>>>>> REPLACE
-1
+2
+2
+
+\`\`\`
+
+Result:
+\`\`\`
+1 | def calculate_total(items):
+2 | """TODO: Write a test for this"""
+3 | """Calculate total with 10% markup"""
+4 | return sum(item * 1.1 for item in items)
+\`\`\`
+
+3. Delete code at a specific line range:
+\`\`\`
+
+File path here
+
+<<<<<<< SEARCH
+ total = 0
+ for item in items:
+ total += item
+ return total
+=======
+>>>>>>> REPLACE
+
+2
5
-`
+
+\`\`\`
+
+Result:
+\`\`\`
+1 | def calculate_total(items):
+\`\`\`
+`
}
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
// Extract the search and replace blocks
- const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
+ const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
if (!match) {
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
@@ -133,7 +186,7 @@ Your search/replace content here
}
let [_, searchContent, replaceContent] = match;
-
+
// Detect line ending from original content
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
@@ -145,7 +198,7 @@ Your search/replace content here
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
const stripLineNumbers = (content: string) => {
- return content.replace(/^\d+\s+\|(?!\|)/gm, '')
+ return content.replace(/^\d+\s+\|(?!\|)/gm, '');
};
searchContent = stripLineNumbers(searchContent);
@@ -153,8 +206,8 @@ Your search/replace content here
}
// Split content into lines, handling both \n and \r\n
- const searchLines = searchContent.split(/\r?\n/);
- const replaceLines = replaceContent.split(/\r?\n/);
+ const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
+ const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
const originalLines = originalContent.split(/\r?\n/);
// First try exact line range if provided
@@ -167,9 +220,15 @@ Your search/replace content here
const exactStartIndex = startLine - 1;
const exactEndIndex = endLine - 1;
- if (exactStartIndex < 0 || exactEndIndex >= originalLines.length || exactStartIndex > exactEndIndex) {
+ if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
+ // Log detailed debug information
+ console.log('Invalid Line Range Debug:', {
+ requestedRange: { start: startLine, end: endLine },
+ fileBounds: { start: 1, end: originalLines.length }
+ });
+
return {
success: false,
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
@@ -263,7 +322,7 @@ Your search/replace content here
// Apply the replacement while preserving exact indentation
const indentedReplaceLines = replaceLines.map((line, i) => {
// Get the matched line's exact indentation
- const matchedIndent = originalIndents[0];
+ const matchedIndent = originalIndents[0] || '';
// Get the current line's indentation relative to the search content
const currentIndentMatch = line.match(/^[\t ]*/);
From ac2babfd82d0a7b2778f54bf36abafe193484932 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 14:07:07 -0800
Subject: [PATCH 17/30] remove double ping for mcp approval
---
webview-ui/src/components/chat/ChatView.tsx | 3 ---
1 file changed, 3 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 6972430a2f..696df80518 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -155,9 +155,6 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
setSecondaryButtonText(undefined)
break
case "use_mcp_server":
- if (!isAutoApproved(lastMessage)) {
- playSound("notification")
- }
setTextAreaDisabled(isPartial)
setClineAsk("use_mcp_server")
setEnableButtons(!isPartial)
From 8f1fef249b321f4ab48bb8fc829854d82da72f18 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 14:17:38 -0800
Subject: [PATCH 18/30] changeset
---
.changeset/eighty-nails-peel.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 .changeset/eighty-nails-peel.md
diff --git a/.changeset/eighty-nails-peel.md b/.changeset/eighty-nails-peel.md
new file mode 100644
index 0000000000..bfcedc869c
--- /dev/null
+++ b/.changeset/eighty-nails-peel.md
@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add volume slider and change sound effect triggers
From ca4806db224ba1b702deced67ee01139cbb8d18e Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 14:23:13 -0800
Subject: [PATCH 19/30] add details to changeset
---
.changeset/eighty-nails-peel.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.changeset/eighty-nails-peel.md b/.changeset/eighty-nails-peel.md
index bfcedc869c..1810d531c1 100644
--- a/.changeset/eighty-nails-peel.md
+++ b/.changeset/eighty-nails-peel.md
@@ -2,4 +2,4 @@
"roo-cline": patch
---
-Add volume slider and change sound effect triggers
+Add volume slider in settings and change sound effects to only trigger when user intervention is required, an error occurs, or a task is completed.
From 475776da98b4d9434150e2ac5008bd900ca40052 Mon Sep 17 00:00:00 2001
From: Matt Rubens
Date: Mon, 16 Dec 2024 17:40:36 -0500
Subject: [PATCH 20/30] Bugfix to strip line numbers with leading space
---
.../__tests__/search-replace.test.ts | 20 +++++++++++++++++++
src/core/diff/strategies/search-replace.ts | 4 ++--
2 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/src/core/diff/strategies/__tests__/search-replace.test.ts b/src/core/diff/strategies/__tests__/search-replace.test.ts
index 99aff99482..f96aa17d81 100644
--- a/src/core/diff/strategies/__tests__/search-replace.test.ts
+++ b/src/core/diff/strategies/__tests__/search-replace.test.ts
@@ -591,6 +591,26 @@ this.init();
expect(result.content).toBe('function test() {\n return false;\n}\n')
}
})
+
+ it('should strip line numbers with leading spaces', () => {
+ const originalContent = 'function test() {\n return true;\n}\n'
+ const diffContent = `test.ts
+<<<<<<< SEARCH
+ 1 | function test() {
+ 2 | return true;
+ 3 | }
+=======
+ 1 | function test() {
+ 2 | return false;
+ 3 | }
+>>>>>>> REPLACE`
+
+ const result = strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe('function test() {\n return false;\n}\n')
+ }
+ })
it('should not strip when not all lines have numbers in either section', () => {
const originalContent = 'function test() {\n return true;\n}\n'
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index 660d3642b7..9153c4ef03 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -193,12 +193,12 @@ Result:
// Strip line numbers from search and replace content if every line starts with a line number
const hasLineNumbers = (content: string) => {
const lines = content.split(/\r?\n/);
- return lines.length > 0 && lines.every(line => /^\d+\s+\|(?!\|)/.test(line));
+ return lines.length > 0 && lines.every(line => /^\s*\d+\s+\|(?!\|)/.test(line));
};
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
const stripLineNumbers = (content: string) => {
- return content.replace(/^\d+\s+\|(?!\|)/gm, '');
+ return content.replace(/^\s*\d+\s+\|(?!\|)/gm, '');
};
searchContent = stripLineNumbers(searchContent);
From 6db30b5e9022c42e07f6a0ad48fbcf0a0bbd4116 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 15:00:41 -0800
Subject: [PATCH 21/30] fix lint error
---
webview-ui/src/components/chat/ChatView.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 696df80518..811056b897 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -580,7 +580,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
// Update previous value
setWasStreaming(isStreaming)
- }, [isStreaming, lastMessage, wasStreaming])
+ }, [isStreaming, lastMessage, wasStreaming, isAutoApproved])
const isBrowserSessionMessage = (message: ClineMessage): boolean => {
// which of visible messages are browser session messages, see above
@@ -822,7 +822,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
if (isAutoApproved(lastMessage)) {
handlePrimaryButtonClick()
}
- }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers])
+ }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage])
return (
Date: Mon, 16 Dec 2024 15:01:07 -0800
Subject: [PATCH 22/30] run lint on webview-ui
---
package.json | 2 +-
webview-ui/package-lock.json | 3 ++-
webview-ui/package.json | 6 ++++--
3 files changed, 7 insertions(+), 4 deletions(-)
diff --git a/package.json b/package.json
index 5a54432d1c..64a0b1bb19 100644
--- a/package.json
+++ b/package.json
@@ -153,7 +153,7 @@
"compile": "npm run check-types && npm run lint && node esbuild.js",
"compile-tests": "tsc -p . --outDir out",
"install:all": "npm install && cd webview-ui && npm install",
- "lint": "eslint src --ext ts",
+ "lint": "eslint src --ext ts && npm run lint --prefix webview-ui",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
"start:webview": "cd webview-ui && npm run start",
diff --git a/webview-ui/package-lock.json b/webview-ui/package-lock.json
index 189ee43466..ade601f9a5 100644
--- a/webview-ui/package-lock.json
+++ b/webview-ui/package-lock.json
@@ -34,7 +34,8 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
- "@types/vscode-webview": "^1.57.5"
+ "@types/vscode-webview": "^1.57.5",
+ "eslint": "^8.57.0"
}
},
"node_modules/@adobe/css-tools": {
diff --git a/webview-ui/package.json b/webview-ui/package.json
index 3d12cb9abb..2ede80f98e 100644
--- a/webview-ui/package.json
+++ b/webview-ui/package.json
@@ -31,7 +31,8 @@
"start": "react-scripts start",
"build": "node ./scripts/build-react-no-split.js",
"test": "react-scripts test --watchAll=false",
- "eject": "react-scripts eject"
+ "eject": "react-scripts eject",
+ "lint": "eslint src --ext ts,tsx"
},
"eslintConfig": {
"extends": [
@@ -53,7 +54,8 @@
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
- "@types/vscode-webview": "^1.57.5"
+ "@types/vscode-webview": "^1.57.5",
+ "eslint": "^8.57.0"
},
"jest": {
"transformIgnorePatterns": [
From 267fb441d9504feeb346ca84b66a39d38328acf9 Mon Sep 17 00:00:00 2001
From: Justin Quan
Date: Mon, 16 Dec 2024 15:12:49 -0800
Subject: [PATCH 23/30] fix all lint warnings
---
webview-ui/src/components/chat/ChatView.tsx | 49 ++++++++++++-------
.../mcp/__tests__/McpToolRow.test.tsx | 1 -
.../settings/__tests__/SettingsView.test.tsx | 2 +-
3 files changed, 32 insertions(+), 20 deletions(-)
diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx
index 811056b897..ff765e2f7b 100644
--- a/webview-ui/src/components/chat/ChatView.tsx
+++ b/webview-ui/src/components/chat/ChatView.tsx
@@ -479,7 +479,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
})
}, [modifiedMessages])
- const isReadOnlyToolAction = (message: ClineMessage | undefined) => {
+ const isReadOnlyToolAction = useCallback((message: ClineMessage | undefined) => {
if (message?.type === "ask") {
if (!message.text) {
return true
@@ -488,9 +488,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return ["readFile", "listFiles", "listFilesTopLevel", "listFilesRecursive", "listCodeDefinitionNames", "searchFiles"].includes(tool.tool)
}
return false
- }
+ }, [])
- const isWriteToolAction = (message: ClineMessage | undefined) => {
+ const isWriteToolAction = useCallback((message: ClineMessage | undefined) => {
if (message?.type === "ask") {
if (!message.text) {
return true
@@ -499,9 +499,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
return ["editedExistingFile", "appliedDiff", "newFileCreated"].includes(tool.tool)
}
return false
- }
+ }, [])
- const isMcpToolAlwaysAllowed = (message: ClineMessage | undefined) => {
+ const isMcpToolAlwaysAllowed = useCallback((message: ClineMessage | undefined) => {
if (message?.type === "ask" && message.ask === "use_mcp_server") {
if (!message.text) {
return true
@@ -514,9 +514,9 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
}
}
return false
- }
+ }, [mcpServers])
- const isAllowedCommand = (message: ClineMessage | undefined) => {
+ const isAllowedCommand = useCallback((message: ClineMessage | undefined) => {
if (message?.type === "ask") {
const command = message.text
if (!command) {
@@ -533,19 +533,32 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie
})
}
return false
- }
+ }, [allowedCommands])
- const isAutoApproved = (message: ClineMessage | undefined) => {
- if (!message || message.type !== "ask") return false
+ const isAutoApproved = useCallback(
+ (message: ClineMessage | undefined) => {
+ if (!message || message.type !== "ask") return false
- return (
- (alwaysAllowBrowser && message.ask === "browser_action_launch") ||
- (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
- (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
- (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
- (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
- )
- }
+ return (
+ (alwaysAllowBrowser && message.ask === "browser_action_launch") ||
+ (alwaysAllowReadOnly && message.ask === "tool" && isReadOnlyToolAction(message)) ||
+ (alwaysAllowWrite && message.ask === "tool" && isWriteToolAction(message)) ||
+ (alwaysAllowExecute && message.ask === "command" && isAllowedCommand(message)) ||
+ (alwaysAllowMcp && message.ask === "use_mcp_server" && isMcpToolAlwaysAllowed(message))
+ )
+ },
+ [
+ alwaysAllowBrowser,
+ alwaysAllowReadOnly,
+ alwaysAllowWrite,
+ alwaysAllowExecute,
+ alwaysAllowMcp,
+ isReadOnlyToolAction,
+ isWriteToolAction,
+ isAllowedCommand,
+ isMcpToolAlwaysAllowed
+ ]
+ )
useEffect(() => {
// Only execute when isStreaming changes from true to false
diff --git a/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx b/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx
index 9f3cd96bb2..2f4d2865ef 100644
--- a/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx
+++ b/webview-ui/src/components/mcp/__tests__/McpToolRow.test.tsx
@@ -23,7 +23,6 @@ jest.mock('@vscode/webview-ui-toolkit/react', () => ({