diff --git a/.changeset/selfish-eyes-speak.md b/.changeset/selfish-eyes-speak.md new file mode 100644 index 0000000..97059e0 --- /dev/null +++ b/.changeset/selfish-eyes-speak.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Add configurable delay after auto-writes to allow diagnostics to catch up diff --git a/README.md b/README.md index 39955dc..ad04fe9 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f - Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock - Per-tool MCP auto-approval - Enable/disable MCP servers +- Configurable delay after auto-writes to allow diagnostics to detect potential problems - Runs alongside the original Cline ## Disclaimer diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 50a1ffa..4e56881 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -72,6 +72,7 @@ type GlobalStateKey = | "browserLargeViewport" | "fuzzyMatchThreshold" | "preferredLanguage" // Language setting for Cline's communication + | "writeDelayMs" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -627,6 +628,10 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.updateGlobalState("preferredLanguage", message.text) await this.postStateToWebview() break + case "writeDelayMs": + await this.updateGlobalState("writeDelayMs", message.value) + await this.postStateToWebview() + break } }, null, @@ -957,6 +962,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume, browserLargeViewport, preferredLanguage, + writeDelayMs, } = await this.getState() const allowedCommands = vscode.workspace @@ -984,6 +990,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume: soundVolume ?? 0.5, browserLargeViewport: browserLargeViewport ?? false, preferredLanguage: preferredLanguage ?? 'English', + writeDelayMs: writeDelayMs ?? 1000, } } @@ -1080,6 +1087,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { browserLargeViewport, fuzzyMatchThreshold, preferredLanguage, + writeDelayMs, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1121,6 +1129,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { this.getGlobalState("browserLargeViewport") as Promise, this.getGlobalState("fuzzyMatchThreshold") as Promise, this.getGlobalState("preferredLanguage") as Promise, + this.getGlobalState("writeDelayMs") as Promise, ]) let apiProvider: ApiProvider @@ -1179,6 +1188,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { soundVolume, browserLargeViewport: browserLargeViewport ?? false, fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, + writeDelayMs: writeDelayMs ?? 1000, preferredLanguage: preferredLanguage ?? (() => { // Get VSCode's locale setting const vscodeLang = vscode.env.language; diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index d3d2c02..16a976e 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -248,9 +248,13 @@ describe('ClineProvider', () => { alwaysAllowWrite: false, alwaysAllowExecute: false, alwaysAllowBrowser: false, + alwaysAllowMcp: false, uriScheme: 'vscode', soundEnabled: false, diffEnabled: false, + writeDelayMs: 1000, + browserLargeViewport: false, + fuzzyMatchThreshold: 1.0, } const message: ExtensionMessage = { @@ -300,6 +304,7 @@ describe('ClineProvider', () => { expect(state).toHaveProperty('taskHistory') expect(state).toHaveProperty('soundEnabled') expect(state).toHaveProperty('diffEnabled') + expect(state).toHaveProperty('writeDelayMs') }) test('preferredLanguage defaults to VSCode language when not set', async () => { @@ -308,7 +313,7 @@ describe('ClineProvider', () => { const state = await provider.getState(); expect(state.preferredLanguage).toBe('Spanish'); - }); + }) test('preferredLanguage defaults to English for unsupported VSCode language', async () => { // Mock VSCode language as an unsupported language @@ -316,7 +321,7 @@ describe('ClineProvider', () => { const state = await provider.getState(); expect(state.preferredLanguage).toBe('English'); - }); + }) test('diffEnabled defaults to true when not set', async () => { // Mock globalState.get to return undefined for diffEnabled @@ -327,6 +332,29 @@ describe('ClineProvider', () => { expect(state.diffEnabled).toBe(true) }) + test('writeDelayMs defaults to 1000ms', async () => { + // Mock globalState.get to return undefined for writeDelayMs + (mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => { + if (key === 'writeDelayMs') { + return undefined + } + return null + }) + + const state = await provider.getState() + expect(state.writeDelayMs).toBe(1000) + }) + + test('handles writeDelayMs message', async () => { + provider.resolveWebviewView(mockWebviewView) + const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] + + await messageHandler({ type: 'writeDelayMs', value: 2000 }) + + expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000) + expect(mockPostMessage).toHaveBeenCalled() + }) + test('updates sound utility when sound setting changes', async () => { provider.resolveWebviewView(mockWebviewView) diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index faef35c..01de0af 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -56,6 +56,7 @@ export interface ExtensionState { browserLargeViewport?: boolean fuzzyMatchThreshold?: number preferredLanguage: string + writeDelayMs: number } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d662475..507bc79 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -41,6 +41,7 @@ export interface WebviewMessage { | "toggleMcpServer" | "fuzzyMatchThreshold" | "preferredLanguage" + | "writeDelayMs" text?: string disabled?: boolean askResponse?: ClineAskResponse diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index fcbbf5b..50954ba 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -37,7 +37,7 @@ interface ChatViewProps { export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { - const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands } = useExtensionState() + const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs } = useExtensionState() //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) @@ -831,10 +831,17 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie // Only proceed if we have an ask and buttons are enabled if (!clineAsk || !enableButtons) return - if (isAutoApproved(lastMessage)) { - handlePrimaryButtonClick() + const autoApprove = async () => { + if (isAutoApproved(lastMessage)) { + // Add delay for write operations + if (alwaysAllowWrite && isWriteToolAction(lastMessage)) { + await new Promise(resolve => setTimeout(resolve, writeDelayMs)) + } + handlePrimaryButtonClick() + } } - }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage]) + autoApprove() + }, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage, writeDelayMs, isWriteToolAction]) return (
{ setFuzzyMatchThreshold, preferredLanguage, setPreferredLanguage, + writeDelayMs, + setWriteDelayMs, } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -70,6 +72,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage }) + vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs }) onDone() } } @@ -277,6 +280,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {

Automatically create and edit files without requiring approval

+ {alwaysAllowWrite && ( +
+
+ setWriteDelayMs(parseInt(e.target.value))} + style={{ + flex: 1, + accentColor: 'var(--vscode-button-background)', + height: '2px' + }} + /> + + {writeDelayMs}ms + +
+

+ Delay after writes to allow diagnostics to detect potential problems +

+
+ )}
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 18ad6f6..634f437 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState { setFuzzyMatchThreshold: (value: number) => void preferredLanguage: string setPreferredLanguage: (value: string) => void + setWriteDelayMs: (value: number) => void } const ExtensionStateContext = createContext(undefined) @@ -51,6 +52,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode diffEnabled: false, fuzzyMatchThreshold: 1.0, preferredLanguage: 'English', + writeDelayMs: 1000, }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) @@ -139,6 +141,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode filePaths, soundVolume: state.soundVolume, fuzzyMatchThreshold: state.fuzzyMatchThreshold, + writeDelayMs: state.writeDelayMs, setApiConfiguration: (value) => setState((prevState) => ({ ...prevState, apiConfiguration: value @@ -157,6 +160,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })), + setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })), } return {children}