فهرست منبع

Merge pull request #220 from RooVetGit/diagnostics_delay

Add configurable delay after auto-writes to allow diagnostics to catch up
Matt Rubens 1 سال پیش
والد
کامیت
e0eb024a8f

+ 5 - 0
.changeset/selfish-eyes-speak.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Add configurable delay after auto-writes to allow diagnostics to catch up

+ 1 - 0
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

+ 10 - 0
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<ApiProvider | undefined>,
 			this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1121,6 +1129,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
 			this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
 			this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
+			this.getGlobalState("writeDelayMs") as Promise<number | undefined>,
 		])
 
 		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;

+ 30 - 2
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)
 

+ 1 - 0
src/shared/ExtensionMessage.ts

@@ -56,6 +56,7 @@ export interface ExtensionState {
 	browserLargeViewport?: boolean
 	fuzzyMatchThreshold?: number
 	preferredLanguage: string
+	writeDelayMs: number
 }
 
 export interface ClineMessage {

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -41,6 +41,7 @@ export interface WebviewMessage {
 		| "toggleMcpServer"
 		| "fuzzyMatchThreshold"
 		| "preferredLanguage"
+		| "writeDelayMs"
 	text?: string
 	disabled?: boolean
 	askResponse?: ClineAskResponse

+ 11 - 4
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 (
 		<div

+ 28 - 0
webview-ui/src/components/settings/SettingsView.tsx

@@ -42,6 +42,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		setFuzzyMatchThreshold,
 		preferredLanguage,
 		setPreferredLanguage,
+		writeDelayMs,
+		setWriteDelayMs,
 	} = useExtensionState()
 	const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
 	const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(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) => {
 						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
 							Automatically create and edit files without requiring approval
 						</p>
+						{alwaysAllowWrite && (
+							<div style={{ marginTop: 10 }}>
+								<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
+									<input
+										type="range"
+										min="0"
+										max="5000"
+										step="100"
+										value={writeDelayMs}
+										onChange={(e) => setWriteDelayMs(parseInt(e.target.value))}
+										style={{
+											flex: 1,
+											accentColor: 'var(--vscode-button-background)',
+											height: '2px'
+										}}
+									/>
+									<span style={{ minWidth: '45px', textAlign: 'left' }}>
+										{writeDelayMs}ms
+									</span>
+								</div>
+								<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
+									Delay after writes to allow diagnostics to detect potential problems
+								</p>
+							</div>
+						)}
 					</div>
 
 					<div style={{ marginBottom: 5 }}>

+ 4 - 0
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<ExtensionStateContextType | undefined>(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 <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>