From 1b1e5a20f1ce77020d266ca47697df1386e2a918 Mon Sep 17 00:00:00 2001 From: Chris Hasson Date: Wed, 11 Jun 2025 21:09:19 -0700 Subject: [PATCH] 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 --- .changeset/khaki-clocks-float.md | 5 +++ packages/telemetry/src/TelemetryService.ts | 2 +- packages/types/src/vscode.ts | 1 + src/activate/registerCommands.ts | 18 ++++++---- src/core/webview/webviewMessageHandler.ts | 5 +++ src/shared/WebviewMessage.ts | 1 + src/utils/focusPanel.ts | 27 +++++++++++++++ webview-ui/src/App.tsx | 8 +++++ webview-ui/src/components/ui/hooks/index.ts | 1 + .../ui/hooks/useNonInteractiveClick.ts | 34 +++++++++++++++++++ 10 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 .changeset/khaki-clocks-float.md create mode 100644 src/utils/focusPanel.ts create mode 100644 webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts diff --git a/.changeset/khaki-clocks-float.md b/.changeset/khaki-clocks-float.md new file mode 100644 index 0000000000..5e483d9788 --- /dev/null +++ b/.changeset/khaki-clocks-float.md @@ -0,0 +1,5 @@ +--- +"roo-cline": patch +--- + +Always focus the panel when clicked to ensure menu buttons are available diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 956f49313a..728809f8bd 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -173,7 +173,7 @@ export class TelemetryService { itemType, itemName, target, - ... (properties || {}), + ...(properties || {}), }) } diff --git a/packages/types/src/vscode.ts b/packages/types/src/vscode.ts index cc164aadbe..90d6b72665 100644 --- a/packages/types/src/vscode.ts +++ b/packages/types/src/vscode.ts @@ -51,6 +51,7 @@ export const commandIds = [ "focusInput", "acceptInput", + "focusPanel", ] as const export type CommandId = (typeof commandIds)[number] diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index 3ec5d151e1..fc30878c7b 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -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) diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 6568b4aaee..08f8e866c1 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -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() diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d27b931f10..a6e847c907 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -150,6 +150,7 @@ export interface WebviewMessage { | "clearIndexData" | "indexingStatusUpdate" | "indexCleared" + | "focusPanelRequest" | "codebaseIndexConfig" | "setHistoryPreviewCollapsed" | "openExternal" diff --git a/src/utils/focusPanel.ts b/src/utils/focusPanel.ts new file mode 100644 index 0000000000..c57707047b --- /dev/null +++ b/src/utils/focusPanel.ts @@ -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 { + 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`) + } +} diff --git a/webview-ui/src/App.tsx b/webview-ui/src/App.tsx index 505cb0b6ee..be80a436bd 100644 --- a/webview-ui/src/App.tsx +++ b/webview-ui/src/App.tsx @@ -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 } diff --git a/webview-ui/src/components/ui/hooks/index.ts b/webview-ui/src/components/ui/hooks/index.ts index 46aff4f28d..a20daa7f03 100644 --- a/webview-ui/src/components/ui/hooks/index.ts +++ b/webview-ui/src/components/ui/hooks/index.ts @@ -1,2 +1,3 @@ export * from "./useClipboard" export * from "./useRooPortal" +export * from "./useNonInteractiveClick" diff --git a/webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts b/webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts new file mode 100644 index 0000000000..13809ff0c7 --- /dev/null +++ b/webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts @@ -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]) +}