Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/selfish-eyes-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Add configurable delay after auto-writes to allow diagnostics to catch up
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ type GlobalStateKey =
| "browserLargeViewport"
| "fuzzyMatchThreshold"
| "preferredLanguage" // Language setting for Cline's communication
| "writeDelayMs"

export const GlobalFileNames = {
apiConversationHistory: "api_conversation_history.json",
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -957,6 +962,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume,
browserLargeViewport,
preferredLanguage,
writeDelayMs,
} = await this.getState()

const allowedCommands = vscode.workspace
Expand Down Expand Up @@ -984,6 +990,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
soundVolume: soundVolume ?? 0.5,
browserLargeViewport: browserLargeViewport ?? false,
preferredLanguage: preferredLanguage ?? 'English',
writeDelayMs: writeDelayMs ?? 1000,
}
}

Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
32 changes: 30 additions & 2 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 () => {
Expand All @@ -308,15 +313,15 @@ 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
(vscode.env as any).language = 'unsupported-LANG';

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
Expand All @@ -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)

Expand Down
1 change: 1 addition & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface ExtensionState {
browserLargeViewport?: boolean
fuzzyMatchThreshold?: number
preferredLanguage: string
writeDelayMs: number
}

export interface ClineMessage {
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export interface WebviewMessage {
| "toggleMcpServer"
| "fuzzyMatchThreshold"
| "preferredLanguage"
| "writeDelayMs"
text?: string
disabled?: boolean
askResponse?: ClineAskResponse
Expand Down
15 changes: 11 additions & 4 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions webview-ui/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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()
}
}
Expand Down Expand Up @@ -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 }}>
Expand Down
4 changes: 4 additions & 0 deletions webview-ui/src/context/ExtensionStateContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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>
Expand Down
Loading