Add tab index shortcuts (#3193)
This commit is contained in:
parent
c98eaf7d53
commit
bd37c9ccb5
|
|
@ -389,6 +389,8 @@ export function setupGuestShortcutForwarding(args: {
|
|||
renderer.reloadIgnoringCache()
|
||||
} else if (action?.type === 'jumpToWorktreeIndex') {
|
||||
renderer.send('ui:jumpToWorktreeIndex', action.index)
|
||||
} else if (action?.type === 'jumpToTabIndex') {
|
||||
renderer.send('ui:jumpToTabIndex', action.index)
|
||||
} else if (action?.type === 'dictationKeyDown') {
|
||||
if (!shouldForwardDictationShortcut?.()) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -415,6 +415,51 @@ describe('createMainWindow', () => {
|
|||
expect(webContents.send).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('forwards the platform tab-number jump shortcut to the renderer', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
on: vi.fn((event, handler) => {
|
||||
windowHandlers[event] = handler
|
||||
}),
|
||||
setZoomLevel: vi.fn(),
|
||||
setBackgroundThrottling: vi.fn(),
|
||||
invalidate: vi.fn(),
|
||||
setWindowOpenHandler: vi.fn(),
|
||||
send: vi.fn(),
|
||||
isDevToolsOpened: vi.fn(),
|
||||
openDevTools: vi.fn(),
|
||||
closeDevTools: vi.fn()
|
||||
}
|
||||
const browserWindowInstance = {
|
||||
webContents,
|
||||
on: vi.fn(),
|
||||
isDestroyed: vi.fn(() => false),
|
||||
isMaximized: vi.fn(() => true),
|
||||
isFullScreen: vi.fn(() => false),
|
||||
getSize: vi.fn(() => [1200, 800]),
|
||||
setSize: vi.fn(),
|
||||
maximize: vi.fn(),
|
||||
show: vi.fn(),
|
||||
loadFile: vi.fn(),
|
||||
loadURL: vi.fn()
|
||||
}
|
||||
browserWindowMock.mockImplementation(function () {
|
||||
return browserWindowInstance
|
||||
})
|
||||
|
||||
createMainWindow(null)
|
||||
|
||||
const input =
|
||||
process.platform === 'darwin'
|
||||
? { type: 'keyDown', code: 'Digit5', key: '5', meta: false, control: true, alt: false }
|
||||
: { type: 'keyDown', code: 'Digit5', key: '5', meta: false, control: false, alt: true }
|
||||
const preventDefault = vi.fn()
|
||||
windowHandlers['before-input-event']({ preventDefault } as never, input as never)
|
||||
|
||||
expect(preventDefault).toHaveBeenCalledTimes(1)
|
||||
expect(webContents.send).toHaveBeenCalledWith('ui:jumpToTabIndex', 4)
|
||||
})
|
||||
|
||||
it('forwards Ctrl+Tab keydown and Ctrl release to the renderer switcher', () => {
|
||||
const windowHandlers: Record<string, (...args: any[]) => void> = {}
|
||||
const webContents = {
|
||||
|
|
|
|||
|
|
@ -854,6 +854,11 @@ export function createMainWindow(
|
|||
return
|
||||
}
|
||||
|
||||
if (action.type === 'jumpToTabIndex') {
|
||||
mainWindow.webContents.send('ui:jumpToTabIndex', action.index)
|
||||
return
|
||||
}
|
||||
|
||||
if (action.type === 'worktreeHistoryNavigate') {
|
||||
// Why: routed through main so the chord reaches the renderer even when
|
||||
// a terminal (xterm.js) or a browser guest has focus — both surfaces
|
||||
|
|
|
|||
|
|
@ -1827,6 +1827,7 @@ export type PreloadApi = {
|
|||
onOpenNewWorkspace: (callback: () => void) => () => void
|
||||
onOpenTasks: (callback: () => void) => () => void
|
||||
onJumpToWorktreeIndex: (callback: (index: number) => void) => () => void
|
||||
onJumpToTabIndex: (callback: (index: number) => void) => () => void
|
||||
onWorktreeHistoryNavigate: (callback: (direction: 'back' | 'forward') => void) => () => void
|
||||
onNewBrowserTab: (callback: () => void) => () => void
|
||||
onRequestTabCreate: (
|
||||
|
|
|
|||
|
|
@ -2428,6 +2428,11 @@ const api = {
|
|||
ipcRenderer.on('ui:jumpToWorktreeIndex', listener)
|
||||
return () => ipcRenderer.removeListener('ui:jumpToWorktreeIndex', listener)
|
||||
},
|
||||
onJumpToTabIndex: (callback: (index: number) => void): (() => void) => {
|
||||
const listener = (_event: Electron.IpcRendererEvent, index: number) => callback(index)
|
||||
ipcRenderer.on('ui:jumpToTabIndex', listener)
|
||||
return () => ipcRenderer.removeListener('ui:jumpToTabIndex', listener)
|
||||
},
|
||||
onWorktreeHistoryNavigate: (
|
||||
callback: (direction: 'back' | 'forward') => void
|
||||
): (() => void) => {
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ describe('useIpcEvents browser tab create routing', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -456,6 +457,7 @@ describe('useIpcEvents updater integration', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -691,6 +693,7 @@ describe('useIpcEvents updater integration', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -1073,6 +1076,7 @@ describe('useIpcEvents updater integration', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onCreateTerminal: (
|
||||
|
|
@ -1589,6 +1593,7 @@ describe('useIpcEvents browser tab close routing', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -1801,6 +1806,7 @@ describe('useIpcEvents browser tab close routing', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -2008,6 +2014,7 @@ describe('useIpcEvents browser tab close routing', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
@ -2233,6 +2240,7 @@ describe('useIpcEvents CLI-created worktree activation', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: (
|
||||
listener: (data: {
|
||||
|
|
@ -2462,6 +2470,7 @@ describe('useIpcEvents agent status snapshot integration', () => {
|
|||
onOpenNewWorkspace: () => () => {},
|
||||
onOpenTasks: () => () => {},
|
||||
onJumpToWorktreeIndex: () => () => {},
|
||||
onJumpToTabIndex: () => () => {},
|
||||
onWorktreeHistoryNavigate: () => () => {},
|
||||
onActivateWorktree: () => () => {},
|
||||
onCreateTerminal: () => () => {},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@/constants/terminal'
|
||||
import type { SplitTerminalPaneDetail, CloseTerminalPaneDetail } from '@/constants/terminal'
|
||||
import { getVisibleWorktreeIds } from '@/components/sidebar/visible-worktrees'
|
||||
import { activateTabNumberShortcut } from '@/lib/tab-number-shortcuts'
|
||||
import { nextEditorFontZoomLevel, computeEditorFontSize } from '@/lib/editor-font-zoom'
|
||||
import type {
|
||||
TerminalLayoutSnapshot,
|
||||
|
|
@ -803,6 +804,12 @@ export function useIpcEvents(): void {
|
|||
})
|
||||
)
|
||||
|
||||
unsubs.push(
|
||||
window.api.ui.onJumpToTabIndex((index) => {
|
||||
activateTabNumberShortcut(index)
|
||||
})
|
||||
)
|
||||
|
||||
unsubs.push(
|
||||
window.api.ui.onWorktreeHistoryNavigate((direction) => {
|
||||
const store = useAppStore.getState()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,125 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import type { Tab, TabGroup } from '../../../shared/types'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { resolveTabNumberShortcutTarget } from './tab-number-shortcuts'
|
||||
|
||||
function tab(overrides: Partial<Tab> & Pick<Tab, 'id' | 'groupId'>): Tab {
|
||||
return {
|
||||
id: overrides.id,
|
||||
entityId: overrides.entityId ?? overrides.id,
|
||||
groupId: overrides.groupId,
|
||||
worktreeId: overrides.worktreeId ?? 'wt-1',
|
||||
contentType: overrides.contentType ?? 'terminal',
|
||||
label: overrides.label ?? overrides.id,
|
||||
customLabel: overrides.customLabel ?? null,
|
||||
color: overrides.color ?? null,
|
||||
sortOrder: overrides.sortOrder ?? 0,
|
||||
createdAt: overrides.createdAt ?? 0,
|
||||
isPreview: overrides.isPreview,
|
||||
isPinned: overrides.isPinned
|
||||
}
|
||||
}
|
||||
|
||||
function state(overrides: {
|
||||
activeView?: AppState['activeView']
|
||||
activeWorktreeId?: string | null
|
||||
activeGroupId?: string
|
||||
groups?: TabGroup[]
|
||||
tabs?: Tab[]
|
||||
}): Pick<
|
||||
AppState,
|
||||
| 'activeGroupIdByWorktree'
|
||||
| 'activeView'
|
||||
| 'activeWorktreeId'
|
||||
| 'groupsByWorktree'
|
||||
| 'unifiedTabsByWorktree'
|
||||
> {
|
||||
const worktreeId = overrides.activeWorktreeId ?? 'wt-1'
|
||||
return {
|
||||
activeView: overrides.activeView ?? 'terminal',
|
||||
activeWorktreeId: worktreeId,
|
||||
activeGroupIdByWorktree:
|
||||
worktreeId === null ? {} : { [worktreeId]: overrides.activeGroupId ?? 'group-a' },
|
||||
groupsByWorktree: worktreeId === null ? {} : { [worktreeId]: overrides.groups ?? [] },
|
||||
unifiedTabsByWorktree: worktreeId === null ? {} : { [worktreeId]: overrides.tabs ?? [] }
|
||||
}
|
||||
}
|
||||
|
||||
describe('resolveTabNumberShortcutTarget', () => {
|
||||
it('resolves by the active group tab order', () => {
|
||||
const first = tab({ id: 'tab-1', groupId: 'group-a' })
|
||||
const second = tab({ id: 'tab-2', groupId: 'group-a' })
|
||||
const third = tab({ id: 'tab-3', groupId: 'group-a' })
|
||||
|
||||
expect(
|
||||
resolveTabNumberShortcutTarget(
|
||||
state({
|
||||
groups: [
|
||||
{
|
||||
id: 'group-a',
|
||||
worktreeId: 'wt-1',
|
||||
activeTabId: null,
|
||||
tabOrder: ['tab-2', 'tab-3', 'tab-1']
|
||||
}
|
||||
],
|
||||
tabs: [first, second, third]
|
||||
}),
|
||||
1
|
||||
)
|
||||
).toBe(third)
|
||||
})
|
||||
|
||||
it('ignores stale duplicate ids and appends current group tabs missing from tabOrder', () => {
|
||||
const first = tab({ id: 'tab-1', groupId: 'group-a' })
|
||||
const second = tab({ id: 'tab-2', groupId: 'group-a' })
|
||||
|
||||
expect(
|
||||
resolveTabNumberShortcutTarget(
|
||||
state({
|
||||
groups: [
|
||||
{
|
||||
id: 'group-a',
|
||||
worktreeId: 'wt-1',
|
||||
activeTabId: null,
|
||||
tabOrder: ['stale', 'tab-1', 'tab-1']
|
||||
}
|
||||
],
|
||||
tabs: [first, second]
|
||||
}),
|
||||
1
|
||||
)
|
||||
).toBe(second)
|
||||
})
|
||||
|
||||
it('uses only the active split group', () => {
|
||||
const otherGroupTab = tab({ id: 'tab-other', groupId: 'group-a' })
|
||||
const activeGroupTab = tab({ id: 'tab-active', groupId: 'group-b' })
|
||||
|
||||
expect(
|
||||
resolveTabNumberShortcutTarget(
|
||||
state({
|
||||
activeGroupId: 'group-b',
|
||||
groups: [
|
||||
{ id: 'group-a', worktreeId: 'wt-1', activeTabId: null, tabOrder: ['tab-other'] },
|
||||
{ id: 'group-b', worktreeId: 'wt-1', activeTabId: null, tabOrder: ['tab-active'] }
|
||||
],
|
||||
tabs: [otherGroupTab, activeGroupTab]
|
||||
}),
|
||||
0
|
||||
)
|
||||
).toBe(activeGroupTab)
|
||||
})
|
||||
|
||||
it('returns null outside terminal workspaces or out of range', () => {
|
||||
const only = tab({ id: 'tab-1', groupId: 'group-a' })
|
||||
const base = state({
|
||||
groups: [{ id: 'group-a', worktreeId: 'wt-1', activeTabId: null, tabOrder: ['tab-1'] }],
|
||||
tabs: [only]
|
||||
})
|
||||
|
||||
expect(resolveTabNumberShortcutTarget(base, 2)).toBeNull()
|
||||
expect(resolveTabNumberShortcutTarget({ ...base, activeView: 'settings' }, 0)).toBeNull()
|
||||
expect(resolveTabNumberShortcutTarget({ ...base, activeWorktreeId: null }, 0)).toBeNull()
|
||||
expect(resolveTabNumberShortcutTarget(base, -1)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { focusTerminalTabSurface } from '@/lib/focus-terminal-tab-surface'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import { dedupeTabOrder } from '@/store/slices/tab-group-state'
|
||||
import type { Tab } from '../../../shared/types'
|
||||
import {
|
||||
activateWebRuntimeSessionTab,
|
||||
isWebRuntimeSessionActive
|
||||
} from '@/runtime/web-runtime-session'
|
||||
|
||||
type TabNumberShortcutState = Pick<
|
||||
AppState,
|
||||
| 'activeGroupIdByWorktree'
|
||||
| 'activeView'
|
||||
| 'activeWorktreeId'
|
||||
| 'groupsByWorktree'
|
||||
| 'unifiedTabsByWorktree'
|
||||
>
|
||||
|
||||
export function resolveTabNumberShortcutTarget(
|
||||
state: TabNumberShortcutState,
|
||||
index: number
|
||||
): Tab | null {
|
||||
if (state.activeView !== 'terminal' || state.activeWorktreeId === null || index < 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
const worktreeId = state.activeWorktreeId
|
||||
const groupId = state.activeGroupIdByWorktree[worktreeId]
|
||||
const group =
|
||||
state.groupsByWorktree[worktreeId]?.find((candidate) => candidate.id === groupId) ??
|
||||
state.groupsByWorktree[worktreeId]?.[0] ??
|
||||
null
|
||||
if (!group) {
|
||||
return null
|
||||
}
|
||||
|
||||
const groupTabs = (state.unifiedTabsByWorktree[worktreeId] ?? []).filter(
|
||||
(tab) => tab.groupId === group.id
|
||||
)
|
||||
const tabById = new Map(groupTabs.map((tab) => [tab.id, tab]))
|
||||
// Why: mirror TabBar's reconcile behavior. Stored group tabOrder is the
|
||||
// visible left-to-right source, but stale/missing entries can happen during
|
||||
// hydration and drag races, so append currently mounted group tabs.
|
||||
const orderedIds = dedupeTabOrder([
|
||||
...group.tabOrder.filter((tabId) => tabById.has(tabId)),
|
||||
...groupTabs.map((tab) => tab.id)
|
||||
])
|
||||
|
||||
return tabById.get(orderedIds[index] ?? '') ?? null
|
||||
}
|
||||
|
||||
export function activateTabNumberShortcut(index: number): boolean {
|
||||
const store = useAppStore.getState()
|
||||
const target = resolveTabNumberShortcutTarget(store, index)
|
||||
if (!target) {
|
||||
return false
|
||||
}
|
||||
|
||||
const runtimeEnvironmentId = store.settings?.activeRuntimeEnvironmentId?.trim()
|
||||
const worktreeId = target.worktreeId
|
||||
store.focusGroup(worktreeId, target.groupId)
|
||||
store.activateTab(target.id)
|
||||
|
||||
if (target.contentType === 'terminal') {
|
||||
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
|
||||
void activateWebRuntimeSessionTab({
|
||||
worktreeId,
|
||||
tabId: target.entityId,
|
||||
environmentId: runtimeEnvironmentId
|
||||
})
|
||||
}
|
||||
store.setActiveTab(target.entityId)
|
||||
store.setActiveTabType('terminal')
|
||||
focusTerminalTabSurface(target.entityId)
|
||||
return true
|
||||
}
|
||||
|
||||
if (target.contentType === 'browser') {
|
||||
if (isWebRuntimeSessionActive(runtimeEnvironmentId)) {
|
||||
void activateWebRuntimeSessionTab({
|
||||
worktreeId,
|
||||
tabId: target.id,
|
||||
environmentId: runtimeEnvironmentId
|
||||
})
|
||||
}
|
||||
store.setActiveBrowserTab(target.entityId)
|
||||
store.setActiveTabType('browser')
|
||||
return true
|
||||
}
|
||||
|
||||
store.setActiveFile(target.entityId)
|
||||
store.setActiveTabType('editor')
|
||||
return true
|
||||
}
|
||||
|
|
@ -1648,6 +1648,7 @@ function createWebUiApi(): NonNullable<Partial<PreloadApi>['ui']> {
|
|||
onOpenTasks: () => noopUnsubscribe,
|
||||
onOpenNewWorkspace: () => noopUnsubscribe,
|
||||
onJumpToWorktreeIndex: () => noopUnsubscribe,
|
||||
onJumpToTabIndex: () => noopUnsubscribe,
|
||||
onWorktreeHistoryNavigate: () => noopUnsubscribe,
|
||||
onNewBrowserTab: () => noopUnsubscribe,
|
||||
onRequestTabCreate: () => noopUnsubscribe,
|
||||
|
|
|
|||
|
|
@ -71,6 +71,36 @@ describe('resolveWindowShortcutAction', () => {
|
|||
'darwin'
|
||||
)
|
||||
).toEqual({ type: 'jumpToWorktreeIndex', index: 2 })
|
||||
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit3', key: '3', meta: false, control: true, alt: false, shift: false },
|
||||
'darwin'
|
||||
)
|
||||
).toEqual({ type: 'jumpToTabIndex', index: 2 })
|
||||
})
|
||||
|
||||
it('uses Alt+number for tab jumps on Windows/Linux without stealing workspace jumps', () => {
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit4', key: '4', meta: false, control: true, alt: false, shift: false },
|
||||
'linux'
|
||||
)
|
||||
).toEqual({ type: 'jumpToWorktreeIndex', index: 3 })
|
||||
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit4', key: '4', meta: false, control: false, alt: true, shift: false },
|
||||
'linux'
|
||||
)
|
||||
).toEqual({ type: 'jumpToTabIndex', index: 3 })
|
||||
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit4', key: '4', meta: false, control: false, alt: true, shift: true },
|
||||
'win32'
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps Orca-first active in terminal context but lets Terminal-first pass risky app chords', () => {
|
||||
|
|
@ -111,6 +141,22 @@ describe('resolveWindowShortcutAction', () => {
|
|||
{ context: 'terminal', terminalShortcutPolicy: 'terminal-first' }
|
||||
)
|
||||
).toEqual({ type: 'switchRecentTab' })
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit3', key: '3', meta: false, control: true, alt: false, shift: false },
|
||||
'darwin',
|
||||
undefined,
|
||||
{ context: 'terminal', terminalShortcutPolicy: 'terminal-first' }
|
||||
)
|
||||
).toBeNull()
|
||||
expect(
|
||||
resolveWindowShortcutAction(
|
||||
{ code: 'Digit3', key: '3', meta: false, control: true, alt: false, shift: false },
|
||||
'darwin',
|
||||
undefined,
|
||||
{ context: 'terminal', terminalShortcutPolicy: 'orca-first' }
|
||||
)
|
||||
).toEqual({ type: 'jumpToTabIndex', index: 2 })
|
||||
})
|
||||
|
||||
it('routes menu-backed actions through the same window shortcut policy', () => {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export type WindowShortcutAction =
|
|||
| { type: 'openTasks' }
|
||||
| { type: 'switchRecentTab' }
|
||||
| { type: 'jumpToWorktreeIndex'; index: number }
|
||||
| { type: 'jumpToTabIndex'; index: number }
|
||||
| { type: 'worktreeHistoryNavigate'; direction: 'back' | 'forward' }
|
||||
| { type: 'dictationKeyDown' }
|
||||
|
||||
|
|
@ -107,6 +108,21 @@ function implicitWorktreeIndexShortcutAllowed(options: WindowShortcutResolveOpti
|
|||
return normalizeTerminalShortcutPolicy(options.terminalShortcutPolicy) === 'orca-first'
|
||||
}
|
||||
|
||||
function implicitTabIndexShortcutAllowed(options: WindowShortcutResolveOptions): boolean {
|
||||
return implicitWorktreeIndexShortcutAllowed(options)
|
||||
}
|
||||
|
||||
function tabIndexModifierPressed(input: WindowShortcutInput, platform: NodeJS.Platform): boolean {
|
||||
const meta = Boolean(input.meta ?? input.metaKey)
|
||||
const control = Boolean(input.control ?? input.ctrlKey)
|
||||
const alt = Boolean(input.alt ?? input.altKey)
|
||||
|
||||
// Why: Ctrl+1-9 is free on macOS because workspace jumps use Cmd+1-9.
|
||||
// On Windows/Linux Ctrl+1-9 is already the workspace jump, so Alt+1-9
|
||||
// gives tab indexing a non-conflicting hardcoded chord.
|
||||
return platform === 'darwin' ? control && !meta && !alt : alt && !meta && !control
|
||||
}
|
||||
|
||||
export function resolveWindowShortcutAction(
|
||||
input: WindowShortcutInput,
|
||||
platform: NodeJS.Platform,
|
||||
|
|
@ -205,6 +221,17 @@ export function resolveWindowShortcutAction(
|
|||
return { type: 'jumpToWorktreeIndex', index: parseInt(input.key, 10) - 1 }
|
||||
}
|
||||
|
||||
if (
|
||||
implicitTabIndexShortcutAllowed(options) &&
|
||||
tabIndexModifierPressed(input, platform) &&
|
||||
!input.shift &&
|
||||
input.key &&
|
||||
input.key >= '1' &&
|
||||
input.key <= '9'
|
||||
) {
|
||||
return { type: 'jumpToTabIndex', index: parseInt(input.key, 10) - 1 }
|
||||
}
|
||||
|
||||
// Why: this helper is the explicit allowlist for main-process interception.
|
||||
// Anything not listed here must keep flowing to the renderer/PTTY so readline
|
||||
// chords like Ctrl+R, Ctrl+U, and Ctrl+E are not accidentally stolen while
|
||||
|
|
@ -247,12 +274,13 @@ export function getWindowShortcutActionId(action: WindowShortcutAction): Keybind
|
|||
case 'dictationKeyDown':
|
||||
return 'voice.dictation'
|
||||
case 'jumpToWorktreeIndex':
|
||||
case 'jumpToTabIndex':
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export function windowShortcutActionCapturesTerminal(action: WindowShortcutAction): boolean {
|
||||
if (action.type === 'jumpToWorktreeIndex') {
|
||||
if (action.type === 'jumpToWorktreeIndex' || action.type === 'jumpToTabIndex') {
|
||||
return true
|
||||
}
|
||||
const actionId = getWindowShortcutActionId(action)
|
||||
|
|
|
|||
Loading…
Reference in New Issue