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