Compare commits

...

7 Commits

Author SHA1 Message Date
abeatrix de30ce7fc5 clean up 2025-07-29 22:43:12 -07:00
abeatrix 2b6601f281 Update toolResult message 2025-07-29 22:39:26 -07:00
abeatrix 02d069cdbf Merge branch 'main' into bee/fix-plan-response 2025-07-29 16:33:02 -07:00
abeatrix d491307140 revert imports 2025-07-29 13:33:42 -07:00
abeatrix f0090c50a4 Merge branch 'main' into bee/fix-plan-response 2025-07-29 13:30:14 -07:00
abeatrix 4b9f47eaa2 changeset added 2025-07-28 22:38:36 -07:00
abeatrix 87d49ed0a8 fix: Prevent assistant from asking repeatedly to switch to Act mode in Plan mode
This PR fixes a bug where the assistant would continuously prompt users to switch from Plan mode to Act mode without letting user to response

Problem

The core issue was in the conversation flow where:
1. Immediate Loop Without User Input: When the assistant suggested switching to Act mode, the system would immediately continue the conversation loop without waiting for user response
2. No Response Opportunity: Users never got the chance to accept, decline, or provide feedback on the mode switch suggestion
3. Infinite Prompting: This created a continuous loop where the assistant would keep suggesting "toggle to Act mode" repeatedly in the same conversation turn
4. Poor Plan Collaboration: Users couldn't effectively iterate on plans because they were trapped in mode-switching prompts

Root Cause

The issue was in the task execution flow where the assistant's response containing mode switch suggestions would not properly end the conversation turn, causing the system to continue processing without user interaction.

Changes

- Convert regular imports to type imports where appropriate
- Reorder imports for better organization
- Add early return in togglePlanActModeWithChatSettings when mode unchanged
- Optimize performance by avoiding unnecessary mode switches
- Optimize mode switching by setting loading state instead of using timeout that cause delays between UI and host
- Exit task loop early when assistant mentioned "toggle to act mode" in plan mode response
2025-07-28 22:35:29 -07:00
4 changed files with 44 additions and 32 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
prevent assistant from asking to switch to Act from Plan mode repeatedly
+12 -3
View File
@@ -106,7 +106,12 @@ export class ToolExecutor {
type: ClineAsk,
text?: string,
partial?: boolean,
) => Promise<{ response: ClineAskResponse; text?: string; images?: string[]; files?: string[] }>,
) => Promise<{
response: ClineAskResponse
text?: string
images?: string[]
files?: string[]
}>,
private saveCheckpoint: (isAttemptCompletionMessage?: boolean) => Promise<void>,
private sayAndCreateMissingParamError: (toolName: ToolUseName, paramName: string, relPath?: string) => Promise<any>,
private removeLastPartialMessageIfExistsWithType: (type: "ask" | "say", askOrSay: ClineAsk | ClineSay) => Promise<void>,
@@ -444,7 +449,7 @@ export class ToolExecutor {
case "write_to_file":
case "replace_in_file": {
const relPath: string | undefined = block.params.path
let content: string | undefined = block.params.content // for write_to_file
const content: string | undefined = block.params.content // for write_to_file
let diff: string | undefined = block.params.diff // for replace_in_file
if (!relPath || (!content && !diff)) {
// checking for content/diff ensures relPath is complete
@@ -2190,7 +2195,11 @@ export class ToolExecutor {
} else {
// if we didn't switch to ACT MODE, then we can just send the user_feedback message
this.pushToolResult(
formatResponse.toolResult(`<user_message>\n${text}\n</user_message>`, images, fileContentString),
formatResponse.toolResult(
`The user replied with the following message but has not toggled to ACT MODE:\n<user_message>\n${text}\n</user_message>`,
images,
fileContentString,
),
block,
)
}
+8
View File
@@ -2450,6 +2450,14 @@ export class Task {
content: [{ type: "text", text: assistantMessage }],
})
// Check if assistant message contains "toggle to Act mode" pattern - if so, end the loop to wait for user's response
const trimmedAssistantMessage = assistantMessage?.toLowerCase().trim()
const hasPlanModeResponseTag = trimmedAssistantMessage?.includes("</plan_mode_respond>")
if (hasPlanModeResponseTag && trimmedAssistantMessage && /toggle to act mode\b/i.test(trimmedAssistantMessage)) {
this.taskState.isAwaitingPlanResponse = true
return true
}
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
// in case the content blocks finished
// it may be the api stream finished after the last parsed content block was executed, so we are able to detect out of bounds and set userMessageContentReady to true (note you should not call presentAssistantMessage since if the last block is completed it will be presented again)
+19 -29
View File
@@ -315,6 +315,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
const unsupportedFileTimerRef = useRef<NodeJS.Timeout | null>(null)
const [showDimensionError, setShowDimensionError] = useState(false)
const dimensionErrorTimerRef = useRef<NodeJS.Timeout | null>(null)
const [isSwitchingMode, setIsSwitchingMode] = useState(false)
const [fileSearchResults, setFileSearchResults] = useState<SearchResult[]>([])
const [searchLoading, setSearchLoading] = useState(false)
@@ -988,35 +989,24 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}
}, [apiConfiguration, openRouterModels])
const onModeToggle = useCallback(() => {
// if (textAreaDisabled) return
let changeModeDelay = 0
if (showModelSelector) {
// user has model selector open, so we should save it before switching modes
submitApiConfig()
changeModeDelay = 250 // necessary to let the api config update (we send message and wait for it to be saved) FIXME: this is a hack and we ideally should check for api config changes, then wait for it to be saved, before switching modes
const onModeToggle = useCallback(async () => {
if (isSwitchingMode) return // prevent double toggling
setIsSwitchingMode(true)
const convertedProtoMode = mode === "plan" ? PlanActMode.ACT : PlanActMode.PLAN
const response = await StateServiceClient.togglePlanActModeProto({
mode: convertedProtoMode,
chatContent: {
message: inputValue.trim() ? inputValue : undefined,
images: selectedImages,
files: selectedFiles,
},
})
if (response?.value) {
setInputValue("")
}
setTimeout(async () => {
const convertedProtoMode = mode === "plan" ? PlanActMode.ACT : PlanActMode.PLAN
const response = await StateServiceClient.togglePlanActModeProto(
TogglePlanActModeRequest.create({
mode: convertedProtoMode,
chatContent: {
message: inputValue.trim() ? inputValue : undefined,
images: selectedImages,
files: selectedFiles,
},
}),
)
// Focus the textarea after mode toggle with slight delay
setTimeout(() => {
if (response.value) {
setInputValue("")
}
textAreaRef.current?.focus()
}, 100)
}, changeModeDelay)
}, [mode, showModelSelector, submitApiConfig, inputValue, selectedImages, selectedFiles])
textAreaRef.current?.focus()
setIsSwitchingMode(false)
}, [mode, inputValue, selectedImages, selectedFiles, setInputValue, isSwitchingMode])
useShortcut("Meta+Shift+a", onModeToggle, { disableTextInputs: false }) // important that we don't disable the text input here
@@ -1741,7 +1731,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
visible={shownTooltipMode !== null}
tipText={`In ${shownTooltipMode === "act" ? "Act" : "Plan"} mode, Cline will ${shownTooltipMode === "act" ? "complete the task immediately" : "gather information to architect a plan"}`}
hintText={`Toggle w/ ${metaKeyChar}+Shift+A`}>
<SwitchContainer data-testid="mode-switch" disabled={false} onClick={onModeToggle}>
<SwitchContainer data-testid="mode-switch" disabled={isSwitchingMode} onClick={onModeToggle}>
<Slider isAct={mode === "act"} isPlan={mode === "plan"} />
<SwitchOption
isActive={mode === "plan"}