Skip to content

Commit c4ab5bd

Browse files
committed
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.
1 parent a5ea602 commit c4ab5bd

File tree

9 files changed

+94
-8
lines changed

9 files changed

+94
-8
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/types/src/vscode.ts

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

5151
"focusInput",
5252
"acceptInput",
53+
"focusPanel",
5354
] as const
5455

5556
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"
@@ -167,20 +168,23 @@ const getCommandsMap = ({ context, outputChannel, provider }: RegisterCommandOpt
167168
},
168169
focusInput: async () => {
169170
try {
170-
const panel = getPanel()
171-
172-
if (!panel) {
173-
await vscode.commands.executeCommand(`workbench.view.extension.${Package.name}-ActivityBar`)
174-
} else if (panel === tabPanel) {
175-
panel.reveal(vscode.ViewColumn.Active, false)
176-
} else if (panel === sidebarPanel) {
177-
await vscode.commands.executeCommand(`${ClineProvider.sideBarId}.focus`)
171+
await focusPanel(tabPanel, sidebarPanel)
172+
173+
// Send focus input message only for sidebar panels
174+
if (sidebarPanel && getPanel() === sidebarPanel) {
178175
provider.postMessageToWebview({ type: "action", action: "focusInput" })
179176
}
180177
} catch (error) {
181178
outputChannel.appendLine(`Error focusing input: ${error}`)
182179
}
183180
},
181+
focusPanel: async () => {
182+
try {
183+
await focusPanel(tabPanel, sidebarPanel)
184+
} catch (error) {
185+
outputChannel.appendLine(`Error focusing panel: ${error}`)
186+
}
187+
},
184188
acceptInput: () => {
185189
const visibleProvider = getVisibleProviderOrLog(outputChannel)
186190

src/core/webview/webviewMessageHandler.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,5 +1461,10 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
14611461
}
14621462
break
14631463
}
1464+
case "focusPanelRequest": {
1465+
// Execute the focusPanel command to focus the WebView
1466+
await vscode.commands.executeCommand(getCommand("focusPanel"))
1467+
break
1468+
}
14641469
}
14651470
}

src/shared/WebviewMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export interface WebviewMessage {
148148
| "clearIndexData"
149149
| "indexingStatusUpdate"
150150
| "indexCleared"
151+
| "focusPanelRequest"
151152
| "codebaseIndexConfig"
152153
text?: string
153154
disabled?: boolean

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
@@ -16,6 +16,7 @@ import McpView from "./components/mcp/McpView"
1616
import ModesView from "./components/modes/ModesView"
1717
import { HumanRelayDialog } from "./components/human-relay/HumanRelayDialog"
1818
import { AccountView } from "./components/account/AccountView"
19+
import { useAddNonInteractiveClickListener } from "./components/ui/hooks/useNonInteractiveClick"
1920

2021
type Tab = "settings" | "history" | "mcp" | "modes" | "chat" | "account"
2122

@@ -112,6 +113,13 @@ const App = () => {
112113
// Tell the extension that we are ready to receive messages.
113114
useEffect(() => vscode.postMessage({ type: "webviewDidLaunch" }), [])
114115

116+
// Focus the WebView when non-interactive content is clicked
117+
useAddNonInteractiveClickListener(
118+
useCallback(() => {
119+
vscode.postMessage({ type: "focusPanelRequest" })
120+
}, []),
121+
)
122+
115123
if (!didHydrateState) {
116124
return null
117125
}
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)