Merge pull request #220 from RooVetGit/diagnostics_delay

Add configurable delay after auto-writes to allow diagnostics to catch up
This commit is contained in:
Matt Rubens
2024-12-26 10:16:35 -08:00
committed by GitHub
9 changed files with 91 additions and 6 deletions

View File

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

View File

@@ -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 - Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock
- Per-tool MCP auto-approval - Per-tool MCP auto-approval
- Enable/disable MCP servers - Enable/disable MCP servers
- Configurable delay after auto-writes to allow diagnostics to detect potential problems
- Runs alongside the original Cline - Runs alongside the original Cline
## Disclaimer ## Disclaimer

View File

@@ -72,6 +72,7 @@ type GlobalStateKey =
| "browserLargeViewport" | "browserLargeViewport"
| "fuzzyMatchThreshold" | "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication | "preferredLanguage" // Language setting for Cline's communication
| "writeDelayMs"
export const GlobalFileNames = { export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json", apiConversationHistory: "api_conversation_history.json",
@@ -627,6 +628,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
await this.updateGlobalState("preferredLanguage", message.text) await this.updateGlobalState("preferredLanguage", message.text)
await this.postStateToWebview() await this.postStateToWebview()
break break
case "writeDelayMs":
await this.updateGlobalState("writeDelayMs", message.value)
await this.postStateToWebview()
break
} }
}, },
null, null,
@@ -957,6 +962,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume, soundVolume,
browserLargeViewport, browserLargeViewport,
preferredLanguage, preferredLanguage,
writeDelayMs,
} = await this.getState() } = await this.getState()
const allowedCommands = vscode.workspace const allowedCommands = vscode.workspace
@@ -984,6 +990,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume: soundVolume ?? 0.5, soundVolume: soundVolume ?? 0.5,
browserLargeViewport: browserLargeViewport ?? false, browserLargeViewport: browserLargeViewport ?? false,
preferredLanguage: preferredLanguage ?? 'English', preferredLanguage: preferredLanguage ?? 'English',
writeDelayMs: writeDelayMs ?? 1000,
} }
} }
@@ -1080,6 +1087,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
browserLargeViewport, browserLargeViewport,
fuzzyMatchThreshold, fuzzyMatchThreshold,
preferredLanguage, preferredLanguage,
writeDelayMs,
] = await Promise.all([ ] = await Promise.all([
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>, this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | 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("browserLargeViewport") as Promise<boolean | undefined>,
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>, this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
this.getGlobalState("preferredLanguage") as Promise<string | undefined>, this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
this.getGlobalState("writeDelayMs") as Promise<number | undefined>,
]) ])
let apiProvider: ApiProvider let apiProvider: ApiProvider
@@ -1179,6 +1188,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume, soundVolume,
browserLargeViewport: browserLargeViewport ?? false, browserLargeViewport: browserLargeViewport ?? false,
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0, fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
writeDelayMs: writeDelayMs ?? 1000,
preferredLanguage: preferredLanguage ?? (() => { preferredLanguage: preferredLanguage ?? (() => {
// Get VSCode's locale setting // Get VSCode's locale setting
const vscodeLang = vscode.env.language; const vscodeLang = vscode.env.language;

View File

@@ -248,9 +248,13 @@ describe('ClineProvider', () => {
alwaysAllowWrite: false, alwaysAllowWrite: false,
alwaysAllowExecute: false, alwaysAllowExecute: false,
alwaysAllowBrowser: false, alwaysAllowBrowser: false,
alwaysAllowMcp: false,
uriScheme: 'vscode', uriScheme: 'vscode',
soundEnabled: false, soundEnabled: false,
diffEnabled: false, diffEnabled: false,
writeDelayMs: 1000,
browserLargeViewport: false,
fuzzyMatchThreshold: 1.0,
} }
const message: ExtensionMessage = { const message: ExtensionMessage = {
@@ -300,6 +304,7 @@ describe('ClineProvider', () => {
expect(state).toHaveProperty('taskHistory') expect(state).toHaveProperty('taskHistory')
expect(state).toHaveProperty('soundEnabled') expect(state).toHaveProperty('soundEnabled')
expect(state).toHaveProperty('diffEnabled') expect(state).toHaveProperty('diffEnabled')
expect(state).toHaveProperty('writeDelayMs')
}) })
test('preferredLanguage defaults to VSCode language when not set', async () => { test('preferredLanguage defaults to VSCode language when not set', async () => {
@@ -308,7 +313,7 @@ describe('ClineProvider', () => {
const state = await provider.getState(); const state = await provider.getState();
expect(state.preferredLanguage).toBe('Spanish'); expect(state.preferredLanguage).toBe('Spanish');
}); })
test('preferredLanguage defaults to English for unsupported VSCode language', async () => { test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
// Mock VSCode language as an unsupported language // Mock VSCode language as an unsupported language
@@ -316,7 +321,7 @@ describe('ClineProvider', () => {
const state = await provider.getState(); const state = await provider.getState();
expect(state.preferredLanguage).toBe('English'); expect(state.preferredLanguage).toBe('English');
}); })
test('diffEnabled defaults to true when not set', async () => { test('diffEnabled defaults to true when not set', async () => {
// Mock globalState.get to return undefined for diffEnabled // Mock globalState.get to return undefined for diffEnabled
@@ -327,6 +332,29 @@ describe('ClineProvider', () => {
expect(state.diffEnabled).toBe(true) 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 () => { test('updates sound utility when sound setting changes', async () => {
provider.resolveWebviewView(mockWebviewView) provider.resolveWebviewView(mockWebviewView)

View File

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

View File

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

View File

@@ -37,7 +37,7 @@ interface ChatViewProps {
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { 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 = 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) 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 // Only proceed if we have an ask and buttons are enabled
if (!clineAsk || !enableButtons) return if (!clineAsk || !enableButtons) return
const autoApprove = async () => {
if (isAutoApproved(lastMessage)) { if (isAutoApproved(lastMessage)) {
// Add delay for write operations
if (alwaysAllowWrite && isWriteToolAction(lastMessage)) {
await new Promise(resolve => setTimeout(resolve, writeDelayMs))
}
handlePrimaryButtonClick() 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 ( return (
<div <div

View File

@@ -42,6 +42,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
setFuzzyMatchThreshold, setFuzzyMatchThreshold,
preferredLanguage, preferredLanguage,
setPreferredLanguage, setPreferredLanguage,
writeDelayMs,
setWriteDelayMs,
} = useExtensionState() } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined) const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = 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: "browserLargeViewport", bool: browserLargeViewport })
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 }) vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage }) vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
onDone() onDone()
} }
} }
@@ -277,6 +280,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}> <p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
Automatically create and edit files without requiring approval Automatically create and edit files without requiring approval
</p> </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>
<div style={{ marginBottom: 5 }}> <div style={{ marginBottom: 5 }}>

View File

@@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState {
setFuzzyMatchThreshold: (value: number) => void setFuzzyMatchThreshold: (value: number) => void
preferredLanguage: string preferredLanguage: string
setPreferredLanguage: (value: string) => void setPreferredLanguage: (value: string) => void
setWriteDelayMs: (value: number) => void
} }
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined) const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -51,6 +52,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
diffEnabled: false, diffEnabled: false,
fuzzyMatchThreshold: 1.0, fuzzyMatchThreshold: 1.0,
preferredLanguage: 'English', preferredLanguage: 'English',
writeDelayMs: 1000,
}) })
const [didHydrateState, setDidHydrateState] = useState(false) const [didHydrateState, setDidHydrateState] = useState(false)
const [showWelcome, setShowWelcome] = useState(false) const [showWelcome, setShowWelcome] = useState(false)
@@ -139,6 +141,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
filePaths, filePaths,
soundVolume: state.soundVolume, soundVolume: state.soundVolume,
fuzzyMatchThreshold: state.fuzzyMatchThreshold, fuzzyMatchThreshold: state.fuzzyMatchThreshold,
writeDelayMs: state.writeDelayMs,
setApiConfiguration: (value) => setState((prevState) => ({ setApiConfiguration: (value) => setState((prevState) => ({
...prevState, ...prevState,
apiConfiguration: value apiConfiguration: value
@@ -157,6 +160,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })), setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })), setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })), setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
} }
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider> return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>