Share macOS Option-key tracking

Replaces per-card macOS Option-key listeners with one shared external store while preserving quick-action behavior.
This commit is contained in:
Neil 2026-05-21 20:29:50 -07:00 committed by GitHub
parent b1973657ea
commit 98822fdf7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 147 additions and 20 deletions

View File

@ -48,6 +48,7 @@ import { hasActiveWorkspaceActivity } from '@/lib/worktree-activity-state'
import { runWorktreeDelete } from './delete-worktree-flow'
import { runSleepWorktree } from './sleep-worktree-flow'
import { getWorkspaceQuickActionKind } from './worktree-card-quick-action'
import { useMacOptionKeyPressed } from './mac-option-key-state'
type WorktreeCardProps = {
worktree: Worktree
@ -160,26 +161,7 @@ const WorktreeCard = React.memo(function WorktreeCard({
})
const isSshDisconnected = sshStatus != null && sshStatus !== 'connected'
const [showDisconnectedDialog, setShowDisconnectedDialog] = useState(false)
const [isMacOptionPressed, setIsMacOptionPressed] = useState(false)
useEffect(() => {
const isMac = navigator.userAgent.includes('Mac')
if (!isMac) {
return
}
const handleKeyChange = (event: KeyboardEvent): void => {
setIsMacOptionPressed(event.altKey)
}
const handleWindowBlur = (): void => setIsMacOptionPressed(false)
window.addEventListener('keydown', handleKeyChange, true)
window.addEventListener('keyup', handleKeyChange, true)
window.addEventListener('blur', handleWindowBlur)
return () => {
window.removeEventListener('keydown', handleKeyChange, true)
window.removeEventListener('keyup', handleKeyChange, true)
window.removeEventListener('blur', handleWindowBlur)
}
}, [])
const isMacOptionPressed = useMacOptionKeyPressed()
// Why: on restart the previously-active worktree is auto-restored without a
// click, so the dialog never opens. Auto-show it for the active card when SSH

View File

@ -0,0 +1,80 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
type StoredListener = (event: KeyboardEvent) => void
function createWindowStub(): {
addEventListener: ReturnType<typeof vi.fn>
removeEventListener: ReturnType<typeof vi.fn>
dispatch: (type: string, event: KeyboardEvent) => void
} {
const listeners = new Map<string, Set<StoredListener>>()
return {
addEventListener: vi.fn((type: string, listener: StoredListener) => {
const bucket = listeners.get(type) ?? new Set<StoredListener>()
bucket.add(listener)
listeners.set(type, bucket)
}),
removeEventListener: vi.fn((type: string, listener: StoredListener) => {
listeners.get(type)?.delete(listener)
}),
dispatch: (type, event) => {
for (const listener of listeners.get(type) ?? []) {
listener(event)
}
}
}
}
describe('mac option key state', () => {
afterEach(() => {
vi.unstubAllGlobals()
vi.resetModules()
})
it('shares one window listener set across subscribers and only notifies on value changes', async () => {
const windowStub = createWindowStub()
vi.stubGlobal('navigator', { userAgent: 'Macintosh' })
vi.stubGlobal('window', windowStub)
const { getMacOptionKeySnapshot, subscribeMacOptionKey } =
await import('./mac-option-key-state')
const first = vi.fn()
const second = vi.fn()
const unsubscribeFirst = subscribeMacOptionKey(first)
const unsubscribeSecond = subscribeMacOptionKey(second)
expect(windowStub.addEventListener).toHaveBeenCalledTimes(3)
windowStub.dispatch('keydown', { altKey: true } as KeyboardEvent)
expect(getMacOptionKeySnapshot()).toBe(true)
expect(first).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledTimes(1)
windowStub.dispatch('keydown', { altKey: true } as KeyboardEvent)
expect(first).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledTimes(1)
unsubscribeFirst()
expect(windowStub.removeEventListener).not.toHaveBeenCalled()
windowStub.dispatch('keyup', { altKey: false } as KeyboardEvent)
expect(first).toHaveBeenCalledTimes(1)
expect(second).toHaveBeenCalledTimes(2)
unsubscribeSecond()
expect(windowStub.removeEventListener).toHaveBeenCalledTimes(3)
expect(getMacOptionKeySnapshot()).toBe(false)
})
it('does not attach keyboard listeners on non-mac platforms', async () => {
const windowStub = createWindowStub()
vi.stubGlobal('navigator', { userAgent: 'Windows' })
vi.stubGlobal('window', windowStub)
const { getMacOptionKeySnapshot, subscribeMacOptionKey } =
await import('./mac-option-key-state')
const unsubscribe = subscribeMacOptionKey(vi.fn())
expect(windowStub.addEventListener).not.toHaveBeenCalled()
expect(getMacOptionKeySnapshot()).toBe(false)
unsubscribe()
})
})

View File

@ -0,0 +1,65 @@
import { useSyncExternalStore } from 'react'
type OptionKeyListener = () => void
let optionPressed = false
const listeners = new Set<OptionKeyListener>()
let disposeWindowListeners: (() => void) | null = null
function isMacPlatform(): boolean {
return typeof navigator !== 'undefined' && navigator.userAgent.includes('Mac')
}
function setOptionPressed(nextPressed: boolean): void {
if (optionPressed === nextPressed) {
return
}
optionPressed = nextPressed
for (const listener of listeners) {
listener()
}
}
function startWindowListeners(): void {
if (disposeWindowListeners || !isMacPlatform() || typeof window === 'undefined') {
return
}
const handleKeyChange = (event: KeyboardEvent): void => setOptionPressed(event.altKey)
const handleWindowBlur = (): void => setOptionPressed(false)
window.addEventListener('keydown', handleKeyChange, true)
window.addEventListener('keyup', handleKeyChange, true)
window.addEventListener('blur', handleWindowBlur)
disposeWindowListeners = () => {
window.removeEventListener('keydown', handleKeyChange, true)
window.removeEventListener('keyup', handleKeyChange, true)
window.removeEventListener('blur', handleWindowBlur)
}
}
export function subscribeMacOptionKey(listener: OptionKeyListener): () => void {
if (!isMacPlatform()) {
return () => undefined
}
listeners.add(listener)
startWindowListeners()
return () => {
listeners.delete(listener)
if (listeners.size > 0) {
return
}
disposeWindowListeners?.()
disposeWindowListeners = null
setOptionPressed(false)
}
}
export function getMacOptionKeySnapshot(): boolean {
return isMacPlatform() ? optionPressed : false
}
export function useMacOptionKeyPressed(): boolean {
// Why: the sidebar can render dozens of cards. One shared external store
// avoids a global key listener per card and only re-renders on Option flips.
return useSyncExternalStore(subscribeMacOptionKey, getMacOptionKeySnapshot, () => false)
}