diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx
index 926bd400f0..b01f4fd20e 100644
--- a/webview-ui/src/components/chat/ChatRow.tsx
+++ b/webview-ui/src/components/chat/ChatRow.tsx
@@ -72,8 +72,13 @@ const ChatRow = memo(
// This allows us to detect changes without causing re-renders
const prevHeightRef = useRef(0)
+ // Prevent browser context menu from appearing
+ const handleContextMenu = useCallback((e: React.MouseEvent) => {
+ e.preventDefault()
+ }, [])
+
const [chatrow, { height }] = useSize(
-
+
,
)
diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx
index 6c541353eb..0c35a929b0 100644
--- a/webview-ui/src/components/chat/ChatTextArea.tsx
+++ b/webview-ui/src/components/chat/ChatTextArea.tsx
@@ -1174,6 +1174,11 @@ const ChatTextArea = forwardRef
(
)
+ // Prevent browser context menu from appearing
+ const handleContextMenu = useCallback((e: React.MouseEvent) => {
+ e.preventDefault()
+ }, [])
+
return (
(
"ml-auto",
"mr-auto",
"box-border",
- )}>
+ )}
+ onContextMenu={handleContextMenu}>
{
+ e.preventDefault()
+ }, [])
+
return (
-
+
{(showAnnouncement || showAnnouncementModal) && (
{
diff --git a/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx
new file mode 100644
index 0000000000..e657f11488
--- /dev/null
+++ b/webview-ui/src/components/chat/__tests__/ChatView.context-menu.spec.tsx
@@ -0,0 +1,196 @@
+// npx vitest run src/components/chat/__tests__/ChatView.context-menu.spec.tsx
+
+import React from "react"
+import { render, fireEvent } from "@testing-library/react"
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
+import { vi } from "vitest"
+
+import ChatView, { ChatViewProps } from "../ChatView"
+
+// Mock the ExtensionStateContext
+vi.mock("@/context/ExtensionStateContext", () => ({
+ useExtensionState: () => ({
+ didHydrateState: true,
+ showWelcome: false,
+ shouldShowAnnouncement: false,
+ clineMessages: [],
+ currentTaskItem: null,
+ taskHistory: [],
+ apiConfiguration: null,
+ organizationAllowList: [],
+ mcpServers: [],
+ alwaysAllowBrowser: false,
+ alwaysAllowReadOnly: false,
+ alwaysAllowReadOnlyOutsideWorkspace: false,
+ alwaysAllowWrite: false,
+ alwaysAllowWriteOutsideWorkspace: false,
+ alwaysAllowWriteProtected: false,
+ alwaysAllowExecute: false,
+ alwaysAllowMcp: false,
+ allowedCommands: [],
+ deniedCommands: [],
+ writeDelayMs: 0,
+ followupAutoApproveTimeoutMs: 0,
+ mode: "code",
+ setMode: vi.fn(),
+ autoApprovalEnabled: false,
+ alwaysAllowModeSwitch: false,
+ alwaysAllowSubtasks: false,
+ alwaysAllowFollowupQuestions: false,
+ alwaysAllowUpdateTodoList: false,
+ customModes: [],
+ telemetrySetting: "unset",
+ hasSystemPromptOverride: false,
+ historyPreviewCollapsed: false,
+ soundEnabled: false,
+ soundVolume: 0.5,
+ }),
+}))
+
+// Mock other dependencies
+vi.mock("@/i18n/TranslationContext", () => ({
+ useAppTranslation: () => ({ t: (key: string) => key }),
+}))
+
+vi.mock("@/utils/vscode", () => ({
+ vscode: {
+ postMessage: vi.fn(),
+ },
+}))
+
+vi.mock("../ChatRow", () => ({
+ default: function MockChatRow({ message }: { message: any }) {
+ return {JSON.stringify(message)}
+ },
+}))
+
+vi.mock("../ChatTextArea", () => {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ const React = require("react")
+ return {
+ default: React.forwardRef(function MockChatTextArea(
+ _props: any,
+ ref: React.ForwardedRef<{ focus: () => void }>,
+ ) {
+ React.useImperativeHandle(ref, () => ({
+ focus: vi.fn(),
+ }))
+ return Chat Text Area
+ }),
+ }
+})
+
+vi.mock("../TaskHeader", () => ({
+ default: function MockTaskHeader() {
+ return Task Header
+ },
+}))
+
+vi.mock("../AutoApproveMenu", () => ({
+ default: function MockAutoApproveMenu() {
+ return Auto Approve Menu
+ },
+}))
+
+vi.mock("@src/components/welcome/RooHero", () => ({
+ default: function MockRooHero() {
+ return Roo Hero
+ },
+}))
+
+vi.mock("@src/components/welcome/RooTips", () => ({
+ default: function MockRooTips() {
+ return Roo Tips
+ },
+}))
+
+vi.mock("../history/HistoryPreview", () => ({
+ default: function MockHistoryPreview() {
+ return History Preview
+ },
+}))
+
+vi.mock("@src/components/common/TelemetryBanner", () => ({
+ default: function MockTelemetryBanner() {
+ return Telemetry Banner
+ },
+}))
+
+vi.mock("@src/components/common/VersionIndicator", () => ({
+ default: function MockVersionIndicator() {
+ return Version Indicator
+ },
+}))
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ mutations: { retry: false },
+ },
+})
+
+const defaultProps: ChatViewProps = {
+ isHidden: false,
+ showAnnouncement: false,
+ hideAnnouncement: vi.fn(),
+}
+
+const renderChatView = (props: Partial = {}) => {
+ return render(
+
+
+ ,
+ )
+}
+
+describe("ChatView - Context Menu Prevention", () => {
+ beforeEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it("should prevent default context menu on right-click", () => {
+ const { container } = renderChatView()
+
+ // Find the main chat view container
+ const chatViewContainer = container.querySelector(".fixed")
+ expect(chatViewContainer).toBeTruthy()
+
+ // Create a context menu event
+ const contextMenuEvent = new MouseEvent("contextmenu", {
+ bubbles: true,
+ cancelable: true,
+ button: 2, // Right mouse button
+ })
+
+ // Spy on preventDefault
+ const preventDefaultSpy = vi.spyOn(contextMenuEvent, "preventDefault")
+
+ // Fire the context menu event
+ if (chatViewContainer) {
+ fireEvent(chatViewContainer, contextMenuEvent)
+ }
+
+ // Verify preventDefault was called
+ expect(preventDefaultSpy).toHaveBeenCalled()
+ })
+
+ it("should prevent context menu on nested elements", () => {
+ const { getByTestId } = renderChatView()
+
+ // Test on a nested element (e.g., chat text area)
+ const chatTextArea = getByTestId("chat-text-area")
+
+ const contextMenuEvent = new MouseEvent("contextmenu", {
+ bubbles: true,
+ cancelable: true,
+ button: 2,
+ })
+
+ const preventDefaultSpy = vi.spyOn(contextMenuEvent, "preventDefault")
+
+ fireEvent(chatTextArea, contextMenuEvent)
+
+ // The event should bubble up and be prevented at the parent level
+ expect(preventDefaultSpy).toHaveBeenCalled()
+ })
+})