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

Reapply "Always focus the panel when clicked to ensure menu buttons are visible" (#4598)

Co-authored-by: Daniel Riccio <[email protected]>
Co-authored-by: Matt Rubens <[email protected]>
Chris Hasson 6 месяцев назад
Родитель
Сommit
e484bfff32

+ 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

@@ -1464,6 +1464,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": {
 			if (marketplaceManager && message.filters) {
 				try {

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -156,6 +156,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 && !panel.active) {
+		// 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`)
+	}
+}

+ 11 - 0
webview-ui/src/App.tsx

@@ -19,6 +19,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"
 
@@ -42,6 +43,7 @@ const App = () => {
 		machineId,
 		cloudUserInfo,
 		cloudIsAuthenticated,
+		renderContext,
 		mdmCompliant,
 	} = useExtensionState()
 
@@ -136,6 +138,15 @@ 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 (only in editor/tab mode)
+	useAddNonInteractiveClickListener(
+		useCallback(() => {
+			// Only send focus request if we're in editor (tab) mode, not sidebar
+			if (renderContext === "editor") {
+				vscode.postMessage({ type: "focusPanelRequest" })
+			}
+		}, [renderContext]),
+	)
 	// Track marketplace tab views
 	useEffect(() => {
 		if (tab === "marketplace") {

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

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

+ 36 - 0
webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts

@@ -0,0 +1,36 @@
+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 !== "SELECT" &&
+				target.tagName !== "TEXTAREA" &&
+				target.tagName !== "VSCODE-TEXT-AREA" &&
+				target.tagName !== "VSCODE-TEXT-FIELD" &&
+				!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])
+}