fix: prevent resource manager right-click menu leak (#2127)

This commit is contained in:
Neil 2026-05-16 16:38:17 -07:00 committed by GitHub
parent 8aae2939f4
commit c771e02cee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 66 additions and 4 deletions

View File

@ -51,6 +51,7 @@ import {
type UnifiedWorktreeRow
} from './mergeSnapshotAndSessions'
import { WorkspaceSpaceCompactPanel } from './WorkspaceSpaceCompactPanel'
import { STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS } from './status-bar-context-menu-policy'
const POLL_MS = 2_000
const SESSIONS_POLL_MS = 10_000
@ -1085,6 +1086,7 @@ export function ResourceUsageStatusSegment({
<PopoverTrigger asChild>
<button
type="button"
{...STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS}
className="relative inline-flex items-center gap-1.5 cursor-pointer rounded px-1 py-0.5 hover:bg-accent/70"
aria-label={
spaceScanReady && !runtimeEnvironmentActive
@ -1142,6 +1144,7 @@ export function ResourceUsageStatusSegment({
side="top"
align="end"
sideOffset={8}
{...STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS}
className="w-[26rem] max-w-[calc(100vw-2rem)] p-0"
onOpenAutoFocus={(event) => event.preventDefault()}
// Why: clicking a terminal row activates a tab, which causes xterm

View File

@ -36,6 +36,7 @@ import { SshStatusSegment } from './SshStatusSegment'
import { UpdateStatusSegment } from './UpdateStatusSegment'
import { ResourceUsageStatusSegment } from './ResourceUsageStatusSegment'
import { isStatusBarItemAvailable } from './status-bar-agent-gating'
import { shouldOpenStatusBarContextMenu } from './status-bar-context-menu-policy'
import { PetStatusSegment } from './PetStatusSegment'
import { TOGGLE_FLOATING_TERMINAL_EVENT } from '@/lib/floating-terminal'
import { FloatingTerminalIconContextMenu } from '@/components/floating-terminal/FloatingTerminalIconContextMenu'
@ -837,10 +838,7 @@ function StatusBarInner({ floatingTerminalOpen }: StatusBarProps): React.JSX.Ele
ref={containerRefCallback}
className="flex items-center h-6 min-h-[24px] px-3 gap-4 border-t border-border bg-[var(--bg-titlebar,var(--card))] text-xs select-none shrink-0 relative"
onContextMenuCapture={(event) => {
if (
event.target instanceof Element &&
event.target.closest('[data-floating-terminal-toggle]')
) {
if (!shouldOpenStatusBarContextMenu(event.target)) {
return
}
// Why: mirror the right-click pattern used across the app

View File

@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest'
import {
STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR,
shouldOpenStatusBarContextMenu
} from './status-bar-context-menu-policy'
function targetMatching(selector: string | null): EventTarget & {
closest: (value: string) => Element | null
} {
return {
closest: (value: string) => (value === selector ? ({} as Element) : null)
} as EventTarget & { closest: (value: string) => Element | null }
}
describe('shouldOpenStatusBarContextMenu', () => {
it('opens for plain status-bar right-clicks', () => {
expect(shouldOpenStatusBarContextMenu(targetMatching(null))).toBe(true)
})
it('ignores right-clicks inside exempt status-bar popovers', () => {
expect(
shouldOpenStatusBarContextMenu(targetMatching(STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR))
).toBe(false)
})
it('keeps the floating terminal context menu independent', () => {
expect(shouldOpenStatusBarContextMenu(targetMatching('[data-floating-terminal-toggle]'))).toBe(
false
)
})
it('opens when the browser gives a non-element target', () => {
expect(shouldOpenStatusBarContextMenu(null)).toBe(true)
})
})

View File

@ -0,0 +1,26 @@
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR = 'data-status-bar-context-menu-exempt'
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR = `[${STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR}]`
export const STATUS_BAR_CONTEXT_MENU_EXEMPT_PROPS = {
[STATUS_BAR_CONTEXT_MENU_EXEMPT_ATTR]: ''
} as const
const FLOATING_TERMINAL_TOGGLE_SELECTOR = '[data-floating-terminal-toggle]'
function hasClosest(target: EventTarget | null): target is EventTarget & {
closest: (selector: string) => Element | null
} {
return typeof (target as { closest?: unknown } | null)?.closest === 'function'
}
export function shouldOpenStatusBarContextMenu(target: EventTarget | null): boolean {
if (!hasClosest(target)) {
return true
}
// Why: Radix portal events can still bubble through the StatusBar React tree;
// nested status-bar surfaces opt out so their right-clicks stay local.
return (
target.closest(FLOATING_TERMINAL_TOGGLE_SELECTOR) === null &&
target.closest(STATUS_BAR_CONTEXT_MENU_EXEMPT_SELECTOR) === null
)
}