Просмотр исходного кода

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 <[email protected]>
Chris Hasson 6 месяцев назад
Родитель
Сommit
1b1e5a20f1

+ 5 - 0
.changeset/khaki-clocks-float.md

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

+ 1 - 1
packages/telemetry/src/TelemetryService.ts

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

+ 1 - 0
packages/types/src/vscode.ts

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

+ 12 - 8
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()
-
-			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)
 

+ 5 - 0
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()

+ 1 - 0
src/shared/WebviewMessage.ts

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

+ 27 - 0
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<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 - 0
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
 	}

+ 1 - 0
webview-ui/src/components/ui/hooks/index.ts

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

+ 34 - 0
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])
+}