Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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/khaki-clocks-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Always focus the panel when clicked to ensure menu buttons are available
1 change: 1 addition & 0 deletions packages/types/src/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export const commandIds = [

"focusInput",
"acceptInput",
"focusPanel",
] as const

export type CommandId = (typeof commandIds)[number]
Expand Down
20 changes: 12 additions & 8 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { Package } from "../shared/package"
import { getCommand } from "../utils/commands"
import { ClineProvider } from "../core/webview/ClineProvider"
import { ContextProxy } from "../core/config/ContextProxy"
import { focusPanel } from "../utils/focusPanel"

import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
import { handleNewTask } from "./handleTask"
Expand Down Expand Up @@ -167,20 +168,23 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
},
focusInput: async () => {
try {
const panel = getPanel()

if (!panel) {
await vscode.commands.executeCommand(`workbench.view.extension.${Package.name}-ActivityBar`)
} else if (panel === tabPanel) {
panel.reveal(vscode.ViewColumn.Active, false)
} else if (panel === sidebarPanel) {
await vscode.commands.executeCommand(`${ClineProvider.sideBarId}.focus`)
await focusPanel(tabPanel, sidebarPanel)

// Send focus input message only for sidebar panels
if (sidebarPanel && getPanel() === sidebarPanel) {
provider.postMessageToWebview({ type: "action", action: "focusInput" })
}
} catch (error) {
outputChannel.appendLine(`Error focusing input: ${error}`)
}
},
focusPanel: async () => {
try {
await focusPanel(tabPanel, sidebarPanel)
} catch (error) {
outputChannel.appendLine(`Error focusing panel: ${error}`)
}
},
acceptInput: () => {
const visibleProvider = getVisibleProviderOrLog(outputChannel)

Expand Down
5 changes: 5 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1461,5 +1461,10 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
}
break
}
case "focusPanelRequest": {
// Execute the focusPanel command to focus the WebView
await vscode.commands.executeCommand(getCommand("focusPanel"))
break
}
}
}
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export interface WebviewMessage {
| "clearIndexData"
| "indexingStatusUpdate"
| "indexCleared"
| "focusPanelRequest"
| "codebaseIndexConfig"
text?: string
disabled?: boolean
Expand Down
27 changes: 27 additions & 0 deletions src/utils/focusPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as vscode from "vscode"
import { Package } from "../shared/package"
import { ClineProvider } from "../core/webview/ClineProvider"

/**
* Focus the active panel (either tab or sidebar)
* @param tabPanel - The tab panel reference
* @param sidebarPanel - The sidebar panel reference
* @returns Promise that resolves when focus is complete
*/
export async function focusPanel(
tabPanel: vscode.WebviewPanel | undefined,
sidebarPanel: vscode.WebviewView | undefined,
): Promise<void> {
const panel = tabPanel || sidebarPanel

if (!panel) {
// If no panel is open, open the sidebar
await vscode.commands.executeCommand(`workbench.view.extension.${Package.name}-ActivityBar`)
} else if (panel === tabPanel) {
// For tab panels, use reveal to focus
panel.reveal(vscode.ViewColumn.Active, false)
} else if (panel === sidebarPanel) {
// For sidebar panels, focus the sidebar
await vscode.commands.executeCommand(`${ClineProvider.sideBarId}.focus`)
}
}
8 changes: 8 additions & 0 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import McpView from "./components/mcp/McpView"
import ModesView from "./components/modes/ModesView"
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
import { AccountView } from "./components/account/AccountView"
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"

type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "account"

Expand Down Expand Up @@ -112,6 +113,13 @@ const App = () => {
// Tell the extension that we are ready to receive messages.
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])

// Focus the WebView when non-interactive content is clicked
useAddNonInteractiveClickListener(
useCallback(() => {
vscode.postMessage({ type: "focusPanelRequest" })
}, []),
)

if (!didHydrateState) {
return null
}
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/ui/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useClipboard"
export * from "./useRooPortal"
export * from "./useNonInteractiveClick"
34 changes: 34 additions & 0 deletions webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { useEffect } from "react"

/**
* Hook that listens for clicks on non-interactive elements and calls the provided handler.
*
* Interactive elements (inputs, textareas, selects, contentEditable) are excluded
* to avoid disrupting user typing or form interactions.
*
* @param handler - Function to call when a non-interactive element is clicked
*/
export function useAddNonInteractiveClickListener(handler: () => void) {
useEffect(() => {
const handleContentClick = (e: MouseEvent) => {
const target = e.target as HTMLElement

// Don't trigger for input elements to avoid disrupting typing
if (
target.tagName !== "INPUT" &&
target.tagName !== "TEXTAREA" &&
target.tagName !== "SELECT" &&
!target.isContentEditable
) {
handler()
}
}

// Add listener to the document body to handle all clicks
document.body.addEventListener("click", handleContentClick)

return () => {
document.body.removeEventListener("click", handleContentClick)
}
}, [handler])
}