Skip to content

Commit 1b1e5a2

Browse files
hassoncsmrubens
andauthored
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]>
1 parent d5f862a commit 1b1e5a2

File tree

10 files changed

+95
-9
lines changed

10 files changed

+95
-9
lines changed

.changeset/khaki-clocks-float.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Always focus the panel when clicked to ensure menu buttons are available

packages/telemetry/src/TelemetryService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ export class TelemetryService {
173173
itemType,
174174
itemName,
175175
target,
176-
... (properties || {}),
176+
...(properties || {}),
177177
})
178178
}
179179

packages/types/src/vscode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export const commandIds = [
5151

5252
"focusInput",
5353
"acceptInput",
54+
"focusPanel",
5455
] as const
5556

5657
export type CommandId = (typeof commandIds)[number]

src/activate/registerCommands.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { Package } from "../shared/package"
88
import { getCommand } from "../utils/commands"
99
import { ClineProvider } from "../core/webview/ClineProvider"
1010
import { ContextProxy } from "../core/config/ContextProxy"
11+
import { focusPanel } from "../utils/focusPanel"
1112

1213
import { registerHumanRelayCallback, unregisterHumanRelayCallback, handleHumanRelayResponse } from "./humanRelay"
1314
import { handleNewTask } from "./handleTask"
@@ -172,20 +173,23 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
172173
},
173174
focusInput: async () => {
174175
try {
175-
const panel = getPanel()
176-
177-
if (!panel) {
178-
await vscode.commands.executeCommand(`workbench.view.extension.${Package.name}-ActivityBar`)
179-
} else if (panel === tabPanel) {
180-
panel.reveal(vscode.ViewColumn.Active, false)
181-
} else if (panel === sidebarPanel) {
182-
await vscode.commands.executeCommand(`${ClineProvider.sideBarId}.focus`)
176+
await focusPanel(tabPanel, sidebarPanel)
177+
178+
// Send focus input message only for sidebar panels
179+
if (sidebarPanel && getPanel() === sidebarPanel) {
183180
provider.postMessageToWebview({ type: "action", action: "focusInput" })
184181
}
185182
} catch (error) {
186183
outputChannel.appendLine(`Error focusing input: ${error}`)
187184
}
188185
},
186+
focusPanel: async () => {
187+
try {
188+
await focusPanel(tabPanel, sidebarPanel)
189+
} catch (error) {
190+
outputChannel.appendLine(`Error focusing panel: ${error}`)
191+
}
192+
},
189193
acceptInput: () => {
190194
const visibleProvider = getVisibleProviderOrLog(outputChannel)
191195

src/core/webview/webviewMessageHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1475,6 +1475,11 @@ export const webviewMessageHandler = async (
14751475
}
14761476
break
14771477
}
1478+
case "focusPanelRequest": {
1479+
// Execute the focusPanel command to focus the WebView
1480+
await vscode.commands.executeCommand(getCommand("focusPanel"))
1481+
break
1482+
}
14781483
case "filterMarketplaceItems": {
14791484
// Check if marketplace is enabled before making API calls
14801485
const { experiments } = await provider.getState()

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export interface WebviewMessage {
150150
| "clearIndexData"
151151
| "indexingStatusUpdate"
152152
| "indexCleared"
153+
| "focusPanelRequest"
153154
| "codebaseIndexConfig"
154155
| "setHistoryPreviewCollapsed"
155156
| "openExternal"

src/utils/focusPanel.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as vscode from "vscode"
2+
import { Package } from "../shared/package"
3+
import { ClineProvider } from "../core/webview/ClineProvider"
4+
5+
/**
6+
* Focus the active panel (either tab or sidebar)
7+
* @param tabPanel - The tab panel reference
8+
* @param sidebarPanel - The sidebar panel reference
9+
* @returns Promise that resolves when focus is complete
10+
*/
11+
export async function focusPanel(
12+
tabPanel: vscode.WebviewPanel | undefined,
13+
sidebarPanel: vscode.WebviewView | undefined,
14+
): Promise<void> {
15+
const panel = tabPanel || sidebarPanel
16+
17+
if (!panel) {
18+
// If no panel is open, open the sidebar
19+
await vscode.commands.executeCommand(`workbench.view.extension.${Package.name}-ActivityBar`)
20+
} else if (panel === tabPanel) {
21+
// For tab panels, use reveal to focus
22+
panel.reveal(vscode.ViewColumn.Active, false)
23+
} else if (panel === sidebarPanel) {
24+
// For sidebar panels, focus the sidebar
25+
await vscode.commands.executeCommand(`${ClineProvider.sideBarId}.focus`)
26+
}
27+
}

webview-ui/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { MarketplaceView } from "./components/marketplace/MarketplaceView"
1818
import ModesView from "./components/modes/ModesView"
1919
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
2020
import { AccountView } from "./components/account/AccountView"
21+
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
2122

2223
type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
2324

@@ -135,6 +136,13 @@ const App = () => {
135136
// Tell the extension that we are ready to receive messages.
136137
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
137138

139+
// Focus the WebView when non-interactive content is clicked
140+
useAddNonInteractiveClickListener(
141+
useCallback(() => {
142+
vscode.postMessage({ type: "focusPanelRequest" })
143+
}, []),
144+
)
145+
138146
if (!didHydrateState) {
139147
return null
140148
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./useClipboard"
22
export * from "./useRooPortal"
3+
export * from "./useNonInteractiveClick"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useEffect } from "react"
2+
3+
/**
4+
* Hook that listens for clicks on non-interactive elements and calls the provided handler.
5+
*
6+
* Interactive elements (inputs, textareas, selects, contentEditable) are excluded
7+
* to avoid disrupting user typing or form interactions.
8+
*
9+
* @param handler - Function to call when a non-interactive element is clicked
10+
*/
11+
export function useAddNonInteractiveClickListener(handler: () => void) {
12+
useEffect(() => {
13+
const handleContentClick = (e: MouseEvent) => {
14+
const target = e.target as HTMLElement
15+
16+
// Don't trigger for input elements to avoid disrupting typing
17+
if (
18+
target.tagName !== "INPUT" &&
19+
target.tagName !== "TEXTAREA" &&
20+
target.tagName !== "SELECT" &&
21+
!target.isContentEditable
22+
) {
23+
handler()
24+
}
25+
}
26+
27+
// Add listener to the document body to handle all clicks
28+
document.body.addEventListener("click", handleContentClick)
29+
30+
return () => {
31+
document.body.removeEventListener("click", handleContentClick)
32+
}
33+
}, [handler])
34+
}

0 commit comments

Comments
 (0)