Add configurable workspace board shortcut (#5638)
This commit is contained in:
parent
0ec3882cb8
commit
8eb96c4ec5
|
|
@ -442,6 +442,8 @@ export function setupGuestShortcutForwarding(args: {
|
|||
renderer.send('ui:openQuickOpen')
|
||||
} else if (action?.type === 'openNewWorkspace') {
|
||||
renderer.send('ui:openNewWorkspace')
|
||||
} else if (action?.type === 'openWorkspaceBoard') {
|
||||
renderer.send('ui:openWorkspaceBoard')
|
||||
} else if (action?.type === 'openTasks') {
|
||||
renderer.send('ui:openTasks')
|
||||
} else if (action?.type === 'openSettings') {
|
||||
|
|
|
|||
|
|
@ -832,6 +832,11 @@ export function createMainWindow(
|
|||
return
|
||||
}
|
||||
|
||||
if (action.type === 'openWorkspaceBoard') {
|
||||
mainWindow.webContents.send('ui:openWorkspaceBoard')
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'openTasks') {
|
||||
mainWindow.webContents.send('ui:openTasks')
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2305,6 +2305,7 @@ export type PreloadApi = {
|
|||
onOpenQuickOpen: (callback: () => void) => () => void
|
||||
onOpenNewWorkspace: (callback: () => void) => () => void
|
||||
onDeleteCurrentWorkspace: (callback: () => void) => () => void
|
||||
onOpenWorkspaceBoard: (callback: () => void) => () => void
|
||||
onOpenTasks: (callback: () => void) => () => void
|
||||
onJumpToWorktreeIndex: (callback: (index: number) => void) => () => void
|
||||
onJumpToTabIndex: (callback: (index: number) => void) => () => void
|
||||
|
|
|
|||
|
|
@ -2744,6 +2744,11 @@ const api = {
|
|||
ipcRenderer.on('ui:deleteCurrentWorkspace', listener)
|
||||
return () => ipcRenderer.removeListener('ui:deleteCurrentWorkspace', listener)
|
||||
},
|
||||
onOpenWorkspaceBoard: (callback: () => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => callback()
|
||||
ipcRenderer.on('ui:openWorkspaceBoard', listener)
|
||||
return () => ipcRenderer.removeListener('ui:openWorkspaceBoard', listener)
|
||||
},
|
||||
onOpenTasks: (callback: () => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent) => callback()
|
||||
ipcRenderer.on('ui:openTasks', listener)
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ import {
|
|||
} from '@/lib/floating-workspace-terminal-actions'
|
||||
import { createFloatingWorkspaceTourInteractionSnapshot } from '@/lib/floating-workspace-tour-interaction-snapshot'
|
||||
import { requestScrollToCurrentWorkspaceRevealAndRename } from '@/lib/scroll-to-current-workspace-status'
|
||||
import { OPEN_WORKSPACE_BOARD_EVENT } from './components/sidebar/useWorkspaceBoardPanel'
|
||||
import { WorkspacePortScanner } from './components/ports/WorkspacePortScanner'
|
||||
import { CrashReportDialog } from './components/crash-report/CrashReportDialog'
|
||||
import NewWorkspaceComposerModal from './components/NewWorkspaceComposerModal'
|
||||
|
|
@ -1531,6 +1532,15 @@ function App(): React.JSX.Element {
|
|||
return
|
||||
}
|
||||
|
||||
if (matchShortcut('workspace.openBoard') && activeView !== 'settings') {
|
||||
e.preventDefault()
|
||||
notifyTerminalCapture('workspace.openBoard')
|
||||
const store = useAppStore.getState()
|
||||
store.setSidebarOpen(true)
|
||||
window.dispatchEvent(new CustomEvent(OPEN_WORKSPACE_BOARD_EVENT))
|
||||
return
|
||||
}
|
||||
|
||||
// Why: Cmd/Ctrl+N is handled via the main-process before-input-event
|
||||
// allowlist (see window-shortcut-policy.ts / useIpcEvents.ts) so it works
|
||||
// globally — including when focus lives inside the markdown rich editor
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@
|
|||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { useWorkspaceBoardPanel, type WorkspaceBoardPanelState } from './useWorkspaceBoardPanel'
|
||||
import {
|
||||
OPEN_WORKSPACE_BOARD_EVENT,
|
||||
useWorkspaceBoardPanel,
|
||||
type WorkspaceBoardPanelState
|
||||
} from './useWorkspaceBoardPanel'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
recordFeatureInteraction: vi.fn()
|
||||
|
|
@ -84,6 +88,18 @@ describe('useWorkspaceBoardPanel', () => {
|
|||
expect(mocks.recordFeatureInteraction).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('opens the board from the shortcut bridge event', async () => {
|
||||
await renderHookProbe()
|
||||
|
||||
await act(async () => {
|
||||
window.dispatchEvent(new CustomEvent(OPEN_WORKSPACE_BOARD_EVENT))
|
||||
})
|
||||
|
||||
expect(panelState().workspaceBoardOpen).toBe(true)
|
||||
expect(panelState().workspaceBoardRenderedOpen).toBe(true)
|
||||
expect(mocks.recordFeatureInteraction).toHaveBeenCalledExactlyOnceWith('workspace-board')
|
||||
})
|
||||
|
||||
it('renders a drag preview without recording an open interaction', async () => {
|
||||
await renderHookProbe()
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ const WORKSPACE_BOARD_ESCAPE_BLOCKING_OVERLAY_SELECTOR = [
|
|||
'[role="listbox"][data-state="open"]'
|
||||
].join(', ')
|
||||
|
||||
export const OPEN_WORKSPACE_BOARD_EVENT = 'orca:open-workspace-board'
|
||||
|
||||
export type WorkspaceBoardPanelState = {
|
||||
workspaceBoardOpen: boolean
|
||||
workspaceBoardRenderedOpen: boolean
|
||||
|
|
@ -137,6 +139,11 @@ export function useWorkspaceBoardPanel(): WorkspaceBoardPanelState {
|
|||
return () => document.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [closeWorkspaceBoard, workspaceBoardMenuOpen, workspaceBoardOpen])
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener(OPEN_WORKSPACE_BOARD_EVENT, openWorkspaceBoard)
|
||||
return () => window.removeEventListener(OPEN_WORKSPACE_BOARD_EVENT, openWorkspaceBoard)
|
||||
}, [openWorkspaceBoard])
|
||||
|
||||
return {
|
||||
workspaceBoardOpen,
|
||||
workspaceBoardRenderedOpen: workspaceBoardOpen || workspaceBoardDragPreviewOpen,
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { activateAndRevealWorktree } from '@/lib/worktree-activation'
|
|||
import { buildLinearIssueLinkedWorkItem } from '@/lib/linear-linked-work-item'
|
||||
import { runWorktreeDelete } from '@/components/sidebar/delete-worktree-flow'
|
||||
import { runSleepWorktree } from '@/components/sidebar/sleep-worktree-flow'
|
||||
import { OPEN_WORKSPACE_BOARD_EVENT } from '@/components/sidebar/useWorkspaceBoardPanel'
|
||||
import {
|
||||
BACKGROUND_MOUNT_TERMINAL_WORKTREE_EVENT,
|
||||
SPLIT_TERMINAL_PANE_EVENT,
|
||||
|
|
@ -1156,6 +1157,19 @@ export function useIpcEvents(): void {
|
|||
)
|
||||
}
|
||||
|
||||
if (window.api.ui.onOpenWorkspaceBoard) {
|
||||
unsubs.push(
|
||||
window.api.ui.onOpenWorkspaceBoard(() => {
|
||||
const store = useAppStore.getState()
|
||||
if (store.activeView === 'settings') {
|
||||
return
|
||||
}
|
||||
store.setSidebarOpen(true)
|
||||
window.dispatchEvent(new CustomEvent(OPEN_WORKSPACE_BOARD_EVENT))
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
unsubs.push(
|
||||
window.api.ui.onOpenTasks(() => {
|
||||
const store = useAppStore.getState()
|
||||
|
|
|
|||
|
|
@ -1988,6 +1988,7 @@ function createWebUiApi(): NonNullable<Partial<PreloadApi>['ui']> {
|
|||
onOpenTasks: () => noopUnsubscribe,
|
||||
onOpenNewWorkspace: () => noopUnsubscribe,
|
||||
onDeleteCurrentWorkspace: () => noopUnsubscribe,
|
||||
onOpenWorkspaceBoard: () => noopUnsubscribe,
|
||||
onJumpToWorktreeIndex: () => noopUnsubscribe,
|
||||
onJumpToTabIndex: () => noopUnsubscribe,
|
||||
onWorktreeHistoryNavigate: () => noopUnsubscribe,
|
||||
|
|
|
|||
|
|
@ -381,6 +381,31 @@ describe('keybindings', () => {
|
|||
).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps workspace board unassigned until users customize it', () => {
|
||||
const binding = {
|
||||
key: 'k',
|
||||
code: 'KeyK',
|
||||
control: true,
|
||||
meta: false,
|
||||
alt: true,
|
||||
shift: false
|
||||
}
|
||||
|
||||
expect(getEffectiveKeybindingsForAction('workspace.openBoard', 'linux')).toEqual([])
|
||||
expect(keybindingMatchesAction('workspace.openBoard', binding, 'linux')).toBe(false)
|
||||
expect(
|
||||
keybindingMatchesAction('workspace.openBoard', binding, 'linux', {
|
||||
'workspace.openBoard': ['Mod+Alt+K']
|
||||
})
|
||||
).toBe(true)
|
||||
|
||||
const definition = getKeybindingDefinition('workspace.openBoard')
|
||||
expect(definition?.title).toBe('Open Workspace Board')
|
||||
expect(definition?.searchKeywords).toEqual(
|
||||
expect.arrayContaining(['workspace', 'board', 'kanban'])
|
||||
)
|
||||
})
|
||||
|
||||
it('defines a macOS-only default for the new agent tab shortcut', () => {
|
||||
expect(getEffectiveKeybindingsForAction('tab.newAgent', 'darwin')).toEqual(['Mod+Alt+T'])
|
||||
expect(getEffectiveKeybindingsForAction('tab.newAgent', 'linux')).toEqual([])
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export type KeybindingActionId =
|
|||
| 'workspace.create'
|
||||
| 'workspace.rename'
|
||||
| 'workspace.delete'
|
||||
| 'workspace.openBoard'
|
||||
| 'voice.dictation'
|
||||
| 'view.tasks'
|
||||
| 'sidebar.left.toggle'
|
||||
|
|
@ -273,6 +274,17 @@ export const KEYBINDING_DEFINITIONS: readonly KeybindingDefinition[] = [
|
|||
defaultBindings: platformBindings([]),
|
||||
allowInTerminal: true
|
||||
},
|
||||
{
|
||||
id: 'workspace.openBoard',
|
||||
title: 'Open Workspace Board',
|
||||
group: 'Global',
|
||||
scope: 'global',
|
||||
searchKeywords: ['shortcut', 'global', 'workspace', 'board', 'kanban', 'worktree'],
|
||||
// Why: make the command configurable without taking a global chord from
|
||||
// terminal/browser/editor users by default.
|
||||
defaultBindings: platformBindings([]),
|
||||
allowInTerminal: true
|
||||
},
|
||||
{
|
||||
id: 'voice.dictation',
|
||||
title: 'Dictation',
|
||||
|
|
|
|||
|
|
@ -239,6 +239,7 @@ describe('resolveWindowShortcutAction', () => {
|
|||
it('applies custom keybinding overrides to main-process shortcuts', () => {
|
||||
const overrides: KeybindingOverrides = {
|
||||
'worktree.quickOpen': ['Mod+Shift+O'],
|
||||
'workspace.openBoard': ['Mod+Alt+B'],
|
||||
'view.tasks': ['Mod+Alt+K']
|
||||
}
|
||||
|
||||
|
|
@ -256,6 +257,13 @@ describe('resolveWindowShortcutAction', () => {
|
|||
overrides
|
||||
)
|
||||
).toEqual({ type: 'openQuickOpen' })
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'KeyB', key: 'b', meta: false, control: true, alt: true, shift: false },
|
||||
'linux',
|
||||
overrides
|
||||
)
|
||||
).toEqual({ type: 'openWorkspaceBoard' })
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'KeyK', key: 'k', meta: false, control: true, alt: true, shift: false },
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export type WindowShortcutAction =
|
|||
| { type: 'openQuickOpen' }
|
||||
| { type: 'openNewWorkspace' }
|
||||
| { type: 'deleteCurrentWorkspace' }
|
||||
| { type: 'openWorkspaceBoard' }
|
||||
| { type: 'openTasks' }
|
||||
| { type: 'switchRecentTab' }
|
||||
| { type: 'jumpToWorktreeIndex'; index: number }
|
||||
|
|
@ -223,6 +224,10 @@ export function resolveWindowShortcutAction(
|
|||
return { type: 'deleteCurrentWorkspace' }
|
||||
}
|
||||
|
||||
if (actionMatches('workspace.openBoard', input, platform, keybindings, options)) {
|
||||
return { type: 'openWorkspaceBoard' }
|
||||
}
|
||||
|
||||
if (actionMatches('voice.dictation', input, platform, keybindings, options)) {
|
||||
return { type: 'dictationKeyDown' }
|
||||
}
|
||||
|
|
@ -291,6 +296,8 @@ export function getWindowShortcutActionId(action: WindowShortcutAction): Keybind
|
|||
return 'workspace.create'
|
||||
case 'deleteCurrentWorkspace':
|
||||
return 'workspace.delete'
|
||||
case 'openWorkspaceBoard':
|
||||
return 'workspace.openBoard'
|
||||
case 'openTasks':
|
||||
return 'view.tasks'
|
||||
case 'switchRecentTab':
|
||||
|
|
|
|||
Loading…
Reference in New Issue