Always focus the panel when clicked to ensure menu buttons are visible (#4511)

feat: Clicking the panel will always focus it

Adds a new `focusPanel` command and utility function to allow focusing the active webview panel (tab or sidebar). This command is now triggered when a user clicks on non-interactive content within the webview, improving user experience by automatically bringing the panel into focus.

This fixes an issue where sometimes the menu options do not appear because the panel is not in focus:
https://github.com/orgs/Kilo-Org/projects/4/views/1?pane=issue&itemId=113936749&issue=Kilo-Org%7Ckilocode%7C619

**Details**
- **New Command:** Introduced `focusPanel` command to programmatically focus the webview panel.
- **Utility Function:** Created `src/utils/focusPanel.ts` to encapsulate the logic for revealing tab panels or focusing sidebar panels.
- **Webview Integration:** Added a `focusPanelRequest` message type to `WebviewMessage` and implemented a handler in `webviewMessageHandler` to execute the new `focusPanel` command.
- **User Experience:** Implemented a `useAddNonInteractiveClickListener` hook in `webview-ui` that sends a `focusPanelRequest` message to the extension when a click occurs on non-interactive elements, ensuring the webview gains focus.
- **Refactor:** Modified the `focusInput` command to utilize the new `focusPanel` utility, reducing code duplication and centralizing panel focus logic.

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
Chris Hasson 2025-06-11 21:09:19 -07:00 committed by GitHub
parent d5f862a6e0
commit 1b1e5a20f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 94 additions and 8 deletions

View file

@ -0,0 +1,5 @@
---
"roo-cline": patch
---
Always focus the panel when clicked to ensure menu buttons are available

View file

@ -173,7 +173,7 @@ export class TelemetryService {
itemType,
itemName,
target,
... (properties || {}),
...(properties || {}),
})
}

View file

@ -51,6 +51,7 @@ export const commandIds = [
"focusInput",
"acceptInput",
"focusPanel",
] as const
export type CommandId = (typeof commandIds)[number]

View file

@ -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"
@ -172,20 +173,23 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
},
focusInput: async () => {
try {
const panel = getPanel()
await focusPanel(tabPanel, sidebarPanel)
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`)
// 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)

View file

@ -1475,6 +1475,11 @@ export const webviewMessageHandler = async (
}
break
}
case "focusPanelRequest": {
// Execute the focusPanel command to focus the WebView
await vscode.commands.executeCommand(getCommand("focusPanel"))
break
}
case "filterMarketplaceItems": {
// Check if marketplace is enabled before making API calls
const { experiments } = await provider.getState()

View file

@ -150,6 +150,7 @@ export interface WebviewMessage {
| "clearIndexData"
| "indexingStatusUpdate"
| "indexCleared"
| "focusPanelRequest"
| "codebaseIndexConfig"
| "setHistoryPreviewCollapsed"
| "openExternal"

27
src/utils/focusPanel.ts Normal file
View file

@ -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`)
}
}

View file

@ -18,6 +18,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView"
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" | "marketplace" | "account"
@ -135,6 +136,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
}

View file

@ -1,2 +1,3 @@
export * from "./useClipboard"
export * from "./useRooPortal"
export * from "./useNonInteractiveClick"

View file

@ -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])
}