Skip to content

Commit e0eb024

Browse files
authored
Merge pull request #220 from RooVetGit/diagnostics_delay
Add configurable delay after auto-writes to allow diagnostics to catch up
2 parents 6126cdf + 03707d5 commit e0eb024

File tree

9 files changed

+91
-6
lines changed

9 files changed

+91
-6
lines changed

.changeset/selfish-eyes-speak.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Add configurable delay after auto-writes to allow diagnostics to catch up

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ A fork of Cline, an autonomous coding agent, with some additional experimental f
1515
- Support for Meta 3, 3.1, and 3.2 models via AWS Bedrock
1616
- Per-tool MCP auto-approval
1717
- Enable/disable MCP servers
18+
- Configurable delay after auto-writes to allow diagnostics to detect potential problems
1819
- Runs alongside the original Cline
1920

2021
## Disclaimer

src/core/webview/ClineProvider.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ type GlobalStateKey =
7272
| "browserLargeViewport"
7373
| "fuzzyMatchThreshold"
7474
| "preferredLanguage" // Language setting for Cline's communication
75+
| "writeDelayMs"
7576

7677
export const GlobalFileNames = {
7778
apiConversationHistory: "api_conversation_history.json",
@@ -627,6 +628,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
627628
await this.updateGlobalState("preferredLanguage", message.text)
628629
await this.postStateToWebview()
629630
break
631+
case "writeDelayMs":
632+
await this.updateGlobalState("writeDelayMs", message.value)
633+
await this.postStateToWebview()
634+
break
630635
}
631636
},
632637
null,
@@ -957,6 +962,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
957962
soundVolume,
958963
browserLargeViewport,
959964
preferredLanguage,
965+
writeDelayMs,
960966
} = await this.getState()
961967

962968
const allowedCommands = vscode.workspace
@@ -984,6 +990,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
984990
soundVolume: soundVolume ?? 0.5,
985991
browserLargeViewport: browserLargeViewport ?? false,
986992
preferredLanguage: preferredLanguage ?? 'English',
993+
writeDelayMs: writeDelayMs ?? 1000,
987994
}
988995
}
989996

@@ -1080,6 +1087,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
10801087
browserLargeViewport,
10811088
fuzzyMatchThreshold,
10821089
preferredLanguage,
1090+
writeDelayMs,
10831091
] = await Promise.all([
10841092
this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
10851093
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@@ -1121,6 +1129,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11211129
this.getGlobalState("browserLargeViewport") as Promise<boolean | undefined>,
11221130
this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
11231131
this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
1132+
this.getGlobalState("writeDelayMs") as Promise<number | undefined>,
11241133
])
11251134

11261135
let apiProvider: ApiProvider
@@ -1179,6 +1188,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
11791188
soundVolume,
11801189
browserLargeViewport: browserLargeViewport ?? false,
11811190
fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
1191+
writeDelayMs: writeDelayMs ?? 1000,
11821192
preferredLanguage: preferredLanguage ?? (() => {
11831193
// Get VSCode's locale setting
11841194
const vscodeLang = vscode.env.language;

src/core/webview/__tests__/ClineProvider.test.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -248,9 +248,13 @@ describe('ClineProvider', () => {
248248
alwaysAllowWrite: false,
249249
alwaysAllowExecute: false,
250250
alwaysAllowBrowser: false,
251+
alwaysAllowMcp: false,
251252
uriScheme: 'vscode',
252253
soundEnabled: false,
253254
diffEnabled: false,
255+
writeDelayMs: 1000,
256+
browserLargeViewport: false,
257+
fuzzyMatchThreshold: 1.0,
254258
}
255259

256260
const message: ExtensionMessage = {
@@ -300,6 +304,7 @@ describe('ClineProvider', () => {
300304
expect(state).toHaveProperty('taskHistory')
301305
expect(state).toHaveProperty('soundEnabled')
302306
expect(state).toHaveProperty('diffEnabled')
307+
expect(state).toHaveProperty('writeDelayMs')
303308
})
304309

305310
test('preferredLanguage defaults to VSCode language when not set', async () => {
@@ -308,15 +313,15 @@ describe('ClineProvider', () => {
308313

309314
const state = await provider.getState();
310315
expect(state.preferredLanguage).toBe('Spanish');
311-
});
316+
})
312317

313318
test('preferredLanguage defaults to English for unsupported VSCode language', async () => {
314319
// Mock VSCode language as an unsupported language
315320
(vscode.env as any).language = 'unsupported-LANG';
316321

317322
const state = await provider.getState();
318323
expect(state.preferredLanguage).toBe('English');
319-
});
324+
})
320325

321326
test('diffEnabled defaults to true when not set', async () => {
322327
// Mock globalState.get to return undefined for diffEnabled
@@ -327,6 +332,29 @@ describe('ClineProvider', () => {
327332
expect(state.diffEnabled).toBe(true)
328333
})
329334

335+
test('writeDelayMs defaults to 1000ms', async () => {
336+
// Mock globalState.get to return undefined for writeDelayMs
337+
(mockContext.globalState.get as jest.Mock).mockImplementation((key: string) => {
338+
if (key === 'writeDelayMs') {
339+
return undefined
340+
}
341+
return null
342+
})
343+
344+
const state = await provider.getState()
345+
expect(state.writeDelayMs).toBe(1000)
346+
})
347+
348+
test('handles writeDelayMs message', async () => {
349+
provider.resolveWebviewView(mockWebviewView)
350+
const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
351+
352+
await messageHandler({ type: 'writeDelayMs', value: 2000 })
353+
354+
expect(mockContext.globalState.update).toHaveBeenCalledWith('writeDelayMs', 2000)
355+
expect(mockPostMessage).toHaveBeenCalled()
356+
})
357+
330358
test('updates sound utility when sound setting changes', async () => {
331359
provider.resolveWebviewView(mockWebviewView)
332360

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface ExtensionState {
5656
browserLargeViewport?: boolean
5757
fuzzyMatchThreshold?: number
5858
preferredLanguage: string
59+
writeDelayMs: number
5960
}
6061

6162
export interface ClineMessage {

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export interface WebviewMessage {
4141
| "toggleMcpServer"
4242
| "fuzzyMatchThreshold"
4343
| "preferredLanguage"
44+
| "writeDelayMs"
4445
text?: string
4546
disabled?: boolean
4647
askResponse?: ClineAskResponse

webview-ui/src/components/chat/ChatView.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface ChatViewProps {
3737
export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
3838

3939
const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => {
40-
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands } = useExtensionState()
40+
const { version, clineMessages: messages, taskHistory, apiConfiguration, mcpServers, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, allowedCommands, writeDelayMs } = useExtensionState()
4141

4242
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
4343
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
831831
// Only proceed if we have an ask and buttons are enabled
832832
if (!clineAsk || !enableButtons) return
833833

834-
if (isAutoApproved(lastMessage)) {
835-
handlePrimaryButtonClick()
834+
const autoApprove = async () => {
835+
if (isAutoApproved(lastMessage)) {
836+
// Add delay for write operations
837+
if (alwaysAllowWrite && isWriteToolAction(lastMessage)) {
838+
await new Promise(resolve => setTimeout(resolve, writeDelayMs))
839+
}
840+
handlePrimaryButtonClick()
841+
}
836842
}
837-
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage])
843+
autoApprove()
844+
}, [clineAsk, enableButtons, handlePrimaryButtonClick, alwaysAllowBrowser, alwaysAllowReadOnly, alwaysAllowWrite, alwaysAllowExecute, alwaysAllowMcp, messages, allowedCommands, mcpServers, isAutoApproved, lastMessage, writeDelayMs, isWriteToolAction])
838845

839846
return (
840847
<div

webview-ui/src/components/settings/SettingsView.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
4242
setFuzzyMatchThreshold,
4343
preferredLanguage,
4444
setPreferredLanguage,
45+
writeDelayMs,
46+
setWriteDelayMs,
4547
} = useExtensionState()
4648
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
4749
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(undefined)
@@ -70,6 +72,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
7072
vscode.postMessage({ type: "browserLargeViewport", bool: browserLargeViewport })
7173
vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
7274
vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
75+
vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
7376
onDone()
7477
}
7578
}
@@ -277,6 +280,31 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
277280
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
278281
Automatically create and edit files without requiring approval
279282
</p>
283+
{alwaysAllowWrite && (
284+
<div style={{ marginTop: 10 }}>
285+
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
286+
<input
287+
type="range"
288+
min="0"
289+
max="5000"
290+
step="100"
291+
value={writeDelayMs}
292+
onChange={(e) => setWriteDelayMs(parseInt(e.target.value))}
293+
style={{
294+
flex: 1,
295+
accentColor: 'var(--vscode-button-background)',
296+
height: '2px'
297+
}}
298+
/>
299+
<span style={{ minWidth: '45px', textAlign: 'left' }}>
300+
{writeDelayMs}ms
301+
</span>
302+
</div>
303+
<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
304+
Delay after writes to allow diagnostics to detect potential problems
305+
</p>
306+
</div>
307+
)}
280308
</div>
281309

282310
<div style={{ marginBottom: 5 }}>

webview-ui/src/context/ExtensionStateContext.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ExtensionStateContextType extends ExtensionState {
3535
setFuzzyMatchThreshold: (value: number) => void
3636
preferredLanguage: string
3737
setPreferredLanguage: (value: string) => void
38+
setWriteDelayMs: (value: number) => void
3839
}
3940

4041
const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
@@ -51,6 +52,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
5152
diffEnabled: false,
5253
fuzzyMatchThreshold: 1.0,
5354
preferredLanguage: 'English',
55+
writeDelayMs: 1000,
5456
})
5557
const [didHydrateState, setDidHydrateState] = useState(false)
5658
const [showWelcome, setShowWelcome] = useState(false)
@@ -139,6 +141,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
139141
filePaths,
140142
soundVolume: state.soundVolume,
141143
fuzzyMatchThreshold: state.fuzzyMatchThreshold,
144+
writeDelayMs: state.writeDelayMs,
142145
setApiConfiguration: (value) => setState((prevState) => ({
143146
...prevState,
144147
apiConfiguration: value
@@ -157,6 +160,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
157160
setBrowserLargeViewport: (value) => setState((prevState) => ({ ...prevState, browserLargeViewport: value })),
158161
setFuzzyMatchThreshold: (value) => setState((prevState) => ({ ...prevState, fuzzyMatchThreshold: value })),
159162
setPreferredLanguage: (value) => setState((prevState) => ({ ...prevState, preferredLanguage: value })),
163+
setWriteDelayMs: (value) => setState((prevState) => ({ ...prevState, writeDelayMs: value })),
160164
}
161165

162166
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>

0 commit comments

Comments
 (0)