Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/main/browser/browser-manager-grab-shortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,10 @@ describe('browserManager grab operations', () => {

describe('guest app shortcut forwarding', () => {
it('forwards Cmd/Ctrl+Shift+B to the renderer and prevents the guest default', () => {
// Fork: New browser tab ships unbound since Mod+Shift+B opens the frontend dev server.
browserManager.setSettingsResolver(() => ({
keybindings: { 'tab.newBrowser': ['Mod+Shift+B'] }
}))
const handlers = guestOnMock.mock.calls
.filter(([eventName]) => eventName === 'before-input-event')
.map(([, handler]) => handler)
Expand Down
4 changes: 4 additions & 0 deletions src/main/browser/browser-manager-guest-shortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ describe('browserManager', () => {
| undefined

expect(beforeInputHandler).toBeTypeOf('function')
// Fork: New browser tab ships unbound since Mod+Shift+B opens the frontend dev server.
browserManager.setSettingsResolver(() => ({
keybindings: { 'tab.newBrowser': ['Mod+Shift+B'] }
}))

const inputs = [
{
Expand Down
58 changes: 58 additions & 0 deletions src/renderer/src/app-shell/source-control-shortcuts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,64 @@ vi.mock('@/lib/worktree-pull-request-url', () => ({

const actions = { openDiffNotesSendMenuForActiveWorktree: () => false }

const frontend = vi.hoisted(() => ({ opened: true, open: vi.fn(() => frontend.opened) }))
vi.mock('@/lib/open-workspace-frontend', () => ({
openActiveWorkspaceFrontend: frontend.open
}))

const nextAction = vi.hoisted(() => ({
action: 'create-pull-request' as string | null,
launch: vi.fn(() => Promise.resolve())
}))
vi.mock('@/components/right-sidebar/workspace-actions/workspace-action-context', () => ({
selectWorkspaceActionContext: () => ({ action: nextAction.action })
}))
vi.mock('@/components/right-sidebar/workspace-actions/launch-workspace-action', () => ({
launchWorkspaceAction: nextAction.launch
}))

describe('next workspace action shortcut', () => {
beforeEach(() => vi.clearAllMocks())
const match = (id: string): boolean => id === 'workspace.runNextAction'

it("runs the top bar's next-action button", () => {
nextAction.action = 'create-pull-request'

expect(takeSourceControlShortcut(match, true, actions)).toBe('workspace.runNextAction')
expect(nextAction.launch).toHaveBeenCalledWith('primary')
expect(toast.info).not.toHaveBeenCalled()
})

it('says so when the workspace has no next action', () => {
nextAction.action = null

expect(takeSourceControlShortcut(match, true, actions)).toBe('workspace.runNextAction')
expect(nextAction.launch).not.toHaveBeenCalled()
expect(toast.info).toHaveBeenCalledWith('This workspace has no next action.')
})
})

describe('frontend dev server shortcut', () => {
beforeEach(() => vi.clearAllMocks())

it("opens the active workspace's running frontend", () => {
frontend.opened = true
const match = (id: string): boolean => id === 'workspace.openFrontend'

expect(takeSourceControlShortcut(match, true, actions)).toBe('workspace.openFrontend')
expect(frontend.open).toHaveBeenCalledOnce()
expect(toast.info).not.toHaveBeenCalled()
})

it('says so when no frontend is running', () => {
frontend.opened = false
const match = (id: string): boolean => id === 'workspace.openFrontend'

expect(takeSourceControlShortcut(match, true, actions)).toBe('workspace.openFrontend')
expect(toast.info).toHaveBeenCalledWith('No frontend dev server is running.')
})
})

describe('pull request shortcuts', () => {
const openUrl = vi.fn(async () => {})
const writeClipboardText = vi.fn(async () => {})
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/src/app-shell/source-control-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import {
requestReviewSummaryFocus
} from '@/components/pending-review/review-summary-focus'
import { activeWorktreePullRequestUrl } from '@/lib/worktree-pull-request-url'
import { openActiveWorkspaceFrontend } from '@/lib/open-workspace-frontend'
import { selectWorkspaceActionContext } from '@/components/right-sidebar/workspace-actions/workspace-action-context'
import { launchWorkspaceAction } from '@/components/right-sidebar/workspace-actions/launch-workspace-action'
import { useAppStore } from '../store'
import type { KeybindingActionId } from '../../../shared/keybindings/types'

Expand All @@ -20,6 +23,8 @@ type SourceControlShortcutId =
| 'sourceControl.focusReviewSummary'
| 'sourceControl.openPullRequest'
| 'sourceControl.copyPullRequestUrl'
| 'workspace.openFrontend'
| 'workspace.runNextAction'

function focusReviewSummaryInOpenPanel(): boolean {
const { activeWorktreeId, rightSidebarOpen, rightSidebarTab } = useAppStore.getState()
Expand Down Expand Up @@ -107,6 +112,20 @@ export function takeSourceControlShortcut(
if (matchShortcut('sourceControl.focusReviewSummary') && focusReviewSummaryInOpenPanel()) {
return 'sourceControl.focusReviewSummary'
}
if (matchShortcut('workspace.runNextAction')) {
if (selectWorkspaceActionContext(useAppStore.getState())?.action) {
void launchWorkspaceAction('primary')
} else {
toast.info(translate('workspace.nextAction.none', 'This workspace has no next action.'))
}
return 'workspace.runNextAction'
}
if (matchShortcut('workspace.openFrontend')) {
if (!openActiveWorkspaceFrontend()) {
toast.info(translate('workspace.frontend.notRunning', 'No frontend dev server is running.'))
}
return 'workspace.openFrontend'
}
if (matchShortcut('sourceControl.openPullRequest')) {
withActivePullRequestUrl((url) => void window.api.shell.openUrl(url))
return 'sourceControl.openPullRequest'
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -21021,6 +21021,12 @@
"openDefault": "Open in Default App"
},
"workspace": {
"frontend": {
"notRunning": "No frontend dev server is running."
},
"nextAction": {
"none": "This workspace has no next action."
},
"links": {
"foreignReview": "This review belongs to another repository. Add reviews from this workspace’s repository.",
"repositoryLoading": "Repository identity is still loading. Try again once it is available.",
Expand Down
33 changes: 33 additions & 0 deletions src/renderer/src/lib/open-workspace-frontend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useAppStore } from '@/store'
import { getWorkspacePortsByWorktreeId } from '@/lib/workspace-port-groups'
import { cachedWorktreePortLabels, pickFrontendPortLabel } from '@/lib/workspace-port-labels'
import { shouldOpenWorkspacePortInOrcaBrowser } from '@/lib/workspace-port-actions'

/** Opens a workspace's frontend dev server the way the top bar button does. */
export function openWorkspaceFrontendUrl(worktreeId: string, url: string): void {
const state = useAppStore.getState()
if (shouldOpenWorkspacePortInOrcaBrowser(state.settings)) {
state.createBrowserTab(worktreeId, url)
} else {
void window.api.shell.openUrl(url)
}
}

/** Fork: the frontend button's target for the active workspace, opened; false when none runs. */
export function openActiveWorkspaceFrontend(): boolean {
const state = useAppStore.getState()
const worktreeId = state.activeWorktreeId
if (!worktreeId) {
return false
}
const ports = getWorkspacePortsByWorktreeId(state.workspacePortScan?.result).get(worktreeId) ?? []
const frontend = pickFrontendPortLabel(
cachedWorktreePortLabels(worktreeId),
ports.map((port) => port.port)
)
if (!frontend) {
return false
}
openWorkspaceFrontendUrl(worktreeId, frontend.url)
return true
}
5 changes: 5 additions & 0 deletions src/renderer/src/lib/workspace-port-labels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export function pickFrontendPortLabel(

const cache = new Map<string, { key: string; labels: PortLabel[] }>()

/** The labels last read for `worktreeId`, for actions that run outside a mounted component. */
export function cachedWorktreePortLabels(worktreeId: string): PortLabel[] {
return cache.get(worktreeId)?.labels ?? NO_LABELS
}

/** Runs the repo's URLs command in the workspace, again only when the command or live ports change. */
export function useWorktreePortLabels(
worktreeId: string | null,
Expand Down
3 changes: 2 additions & 1 deletion src/shared/keybindings/definitions-core-2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ export const KEYBINDING_DEFINITION_CORE_2: readonly KeybindingDefinition[] = [
group: 'Tabs',
scope: 'tabs',
searchKeywords: ['shortcut', 'tab', 'browser', 'new'],
defaultBindings: platformBindings(['Mod+Shift+B'])
// Fork: Mod+Shift+B opens the workspace's frontend dev server instead.
defaultBindings: platformBindings([])
},
{
id: 'tab.newSimulator',
Expand Down
27 changes: 27 additions & 0 deletions src/shared/keybindings/definitions-core-4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,32 @@ export const KEYBINDING_DEFINITION_CORE_4: readonly KeybindingDefinition[] = [
searchKeywords: ['shortcut', 'pull request', 'github', 'copy', 'url', 'link'],
// Why macOS only: Ctrl+Alt+letter is AltGr text input on Windows and Linux.
defaultBindings: { darwin: ['Mod+Alt+Shift+G'], linux: [], win32: [] }
},
{
id: 'workspace.openFrontend',
title: 'Open Frontend Dev Server',
group: 'Global',
scope: 'global',
conflictGroup: 'editor',
searchKeywords: ['shortcut', 'frontend', 'dev server', 'browser', 'open', 'localhost'],
defaultBindings: platformBindings(['Mod+Shift+B'])
},
{
id: 'workspace.runNextAction',
title: 'Run Next Workspace Action',
group: 'Global',
scope: 'global',
conflictGroup: 'editor',
searchKeywords: [
'shortcut',
'next action',
'create pr',
'commit',
'push',
'ready for review',
'merge',
'auto-merge'
],
defaultBindings: platformBindings(['Mod+Alt+Enter'])
}
]
2 changes: 2 additions & 0 deletions src/shared/keybindings/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ export type KeybindingActionId =
| 'sourceControl.sendQueuedComments'
| 'sourceControl.openPullRequest'
| 'sourceControl.copyPullRequestUrl'
| 'workspace.openFrontend'
| 'workspace.runNextAction'
| 'sourceControl.focusReviewSummary'
| 'fileExplorer.undo'
| 'fileExplorer.redo'
Expand Down
Loading