Skip to content
Closed
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
13 changes: 12 additions & 1 deletion src/core/mentions/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ jest.mock("../../../integrations/misc/open-file", () => ({
}))
import { openFile } from "../../../integrations/misc/open-file"

jest.mock("../../webview/ClineProvider", () => ({
ClineProvider: {
getVisibleInstance: jest.fn().mockReturnValue({
getCurrentCline: jest.fn().mockReturnValue(null),
contextProxy: {
getValue: jest.fn().mockReturnValue(false),
},
}),
},
}))

jest.mock("../../../integrations/misc/extract-text", () => ({
extractTextFromFile: jest.fn(),
}))
Expand Down Expand Up @@ -256,7 +267,7 @@ Detailed commit message with multiple lines

await openMention(mention)

expect(openFile).toHaveBeenCalledWith(expectedAbsPath)
expect(openFile).toHaveBeenCalledWith(expectedAbsPath, { preserveFocus: false })
expect(vscode.commands.executeCommand).not.toHaveBeenCalled()
})

Expand Down
8 changes: 7 additions & 1 deletion src/core/mentions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getCommitInfo, getWorkingState } from "../../utils/git"
import { getWorkspacePath } from "../../utils/path"

import { openFile } from "../../integrations/misc/open-file"
import { isInAutomatedWorkflowFromVisibleProvider } from "../../utils/workflow-detection"
import { extractTextFromFile } from "../../integrations/misc/extract-text"
import { diagnosticsToProblemsString } from "../../integrations/diagnostics"

Expand All @@ -36,7 +37,12 @@ export async function openMention(mention?: string): Promise<void> {
if (mention.endsWith("/")) {
vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(absPath))
} else {
openFile(absPath)
// Check if we're in an automated workflow to preserve chat focus during AI processing.
// When users mention files (e.g., @/path/to/file.txt) while the AI is actively processing,
// we want to prevent the chatbox from losing focus to avoid accidental input interruption.
const shouldPreserveFocus = isInAutomatedWorkflowFromVisibleProvider()

openFile(absPath, { preserveFocus: shouldPreserveFocus })
}
} else if (mention === "problems") {
vscode.commands.executeCommand("workbench.actions.view.problems")
Expand Down
15 changes: 14 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
import { GetModelsOptions } from "../../shared/api"
import { generateSystemPrompt } from "./generateSystemPrompt"
import { getCommand } from "../../utils/commands"
import { isInAutomatedWorkflowFromProvider } from "../../utils/workflow-detection"

const ALLOWED_VSCODE_SETTINGS = new Set(["terminal.integrated.inheritEnv"])

Expand Down Expand Up @@ -426,7 +427,16 @@ export const webviewMessageHandler = async (
openImage(message.text!)
break
case "openFile":
openFile(message.text!, message.values as { create?: boolean; content?: string; line?: number })
// Check if we're in an automated workflow to preserve chat focus during AI processing.
// This prevents the chatbox from losing focus when the AI is actively working,
// which could cause users to accidentally type sensitive information (like API keys)
// into the wrong window. See issue #4574 for more context.
const shouldPreserveFocus = isInAutomatedWorkflowFromProvider(provider)

openFile(message.text!, {
...(message.values as { create?: boolean; content?: string; line?: number }),
preserveFocus: shouldPreserveFocus,
})
break
case "openMention":
openMention(message.text)
Expand Down Expand Up @@ -481,6 +491,7 @@ export const webviewMessageHandler = async (
const customModesFilePath = await provider.customModesManager.getCustomModesFilePath()

if (customModesFilePath) {
// User-initiated settings opening, keep focus
openFile(customModesFilePath)
}

Expand All @@ -490,6 +501,7 @@ export const webviewMessageHandler = async (
const mcpSettingsFilePath = await provider.getMcpHub()?.getMcpSettingsFilePath()

if (mcpSettingsFilePath) {
// User-initiated settings opening, keep focus
openFile(mcpSettingsFilePath)
}

Expand All @@ -513,6 +525,7 @@ export const webviewMessageHandler = async (
await fs.writeFile(mcpPath, JSON.stringify({ mcpServers: {} }, null, 2))
}

// User-initiated settings opening, keep focus
await openFile(mcpPath)
} catch (error) {
vscode.window.showErrorMessage(t("mcp:errors.create_json", { error: `${error}` }))
Expand Down
33 changes: 24 additions & 9 deletions src/integrations/editor/DiffViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { formatResponse } from "../../core/prompts/responses"
import { diagnosticsToProblemsString, getNewDiagnostics } from "../diagnostics"
import { ClineSayTool } from "../../shared/ExtensionMessage"
import { Task } from "../../core/task/Task"
import { isInAutomatedWorkflowFromVisibleProvider } from "../../utils/workflow-detection"

import { DecorationController } from "./DecorationController"

Expand Down Expand Up @@ -464,10 +465,17 @@ export class DiffViewProvider {
if (this.activeDiffEditor) {
const scrollLine = line + 4

this.activeDiffEditor.revealRange(
new vscode.Range(scrollLine, 0, scrollLine, 0),
vscode.TextEditorRevealType.InCenter,
)
// Check if we're in an automated workflow to preserve chat focus.
// When the AI is actively processing, we skip scrolling to prevent
// the diff view from stealing focus from the chat input.
const shouldPreserveFocus = isInAutomatedWorkflowFromVisibleProvider()

if (!shouldPreserveFocus) {
this.activeDiffEditor.revealRange(
new vscode.Range(scrollLine, 0, scrollLine, 0),
vscode.TextEditorRevealType.InCenter,
)
}
}
}

Expand All @@ -481,13 +489,20 @@ export class DiffViewProvider {

let lineCount = 0

// Check if we're in an automated workflow to preserve chat focus.
// When the AI is actively processing, we skip scrolling to prevent
// the diff view from stealing focus from the chat input.
const shouldPreserveFocus = isInAutomatedWorkflowFromVisibleProvider()

for (const part of diffs) {
if (part.added || part.removed) {
// Found the first diff, scroll to it.
this.activeDiffEditor.revealRange(
new vscode.Range(lineCount, 0, lineCount, 0),
vscode.TextEditorRevealType.InCenter,
)
// Found the first diff, scroll to it only if not in automated workflow
if (!shouldPreserveFocus) {
this.activeDiffEditor.revealRange(
new vscode.Range(lineCount, 0, lineCount, 0),
vscode.TextEditorRevealType.InCenter,
)
}

return
}
Expand Down
2 changes: 2 additions & 0 deletions src/integrations/misc/open-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface OpenFileOptions {
create?: boolean
content?: string
line?: number
preserveFocus?: boolean
}

export async function openFile(filePath: string, options: OpenFileOptions = {}) {
Expand Down Expand Up @@ -148,6 +149,7 @@ export async function openFile(filePath: string, options: OpenFileOptions = {})
await vscode.window.showTextDocument(document, {
preview: false,
selection,
preserveFocus: options.preserveFocus,
})
} catch (error) {
if (error instanceof Error) {
Expand Down
Loading
Loading