Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/khaki-clocks-float.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"roo-cline": patch
---

Always focus the panel when clicked to ensure menu buttons are available
2 changes: 1 addition & 1 deletion packages/telemetry/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export class TelemetryService {
itemType,
itemName,
target,
... (properties || {}),
...(properties || {}),
})
}

Expand Down
1 change: 1 addition & 0 deletions packages/types/src/vscode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const commandIds = [

"focusInput",
"acceptInput",
"focusPanel",
] as const

export type CommandId = (typeof commandIds)[number]
Expand Down
20 changes: 12 additions & 8 deletions src/activate/registerCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ export interface WebviewMessage {
| "clearIndexData"
| "indexingStatusUpdate"
| "indexCleared"
| "focusPanelRequest"
| "codebaseIndexConfig"
| "setHistoryPreviewCollapsed"
| "openExternal"
Expand Down
27 changes: 27 additions & 0 deletions src/utils/focusPanel.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions webview-ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions webview-ui/src/components/ui/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./useClipboard"
export * from "./useRooPortal"
export * from "./useNonInteractiveClick"
34 changes: 34 additions & 0 deletions webview-ui/src/components/ui/hooks/useNonInteractiveClick.ts
Original file line number Diff line number Diff line change
@@ -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])
}
Loading