From d99f5bf142cebbd753b4c7372a843af9d4647559 Mon Sep 17 00:00:00 2001 From: Frederik Barbre Date: Fri, 23 Oct 2026 14:08:49 +0200 Subject: [PATCH 1/2] feat: Mod+Shift+B opens the workspace's frontend dev server --- .../browser-manager-grab-shortcuts.test.ts | 4 +++ .../browser-manager-guest-shortcuts.test.ts | 4 +++ .../source-control-shortcuts.test.ts | 26 +++++++++++++++ .../src/app-shell/source-control-shortcuts.ts | 8 +++++ src/renderer/src/i18n/locales/en.json | 3 ++ .../src/lib/open-workspace-frontend.ts | 33 +++++++++++++++++++ src/renderer/src/lib/workspace-port-labels.ts | 5 +++ src/shared/keybindings/definitions-core-2.ts | 3 +- src/shared/keybindings/definitions-core-4.ts | 9 +++++ src/shared/keybindings/types.ts | 1 + 10 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 src/renderer/src/lib/open-workspace-frontend.ts diff --git a/src/main/browser/browser-manager-grab-shortcuts.test.ts b/src/main/browser/browser-manager-grab-shortcuts.test.ts index 4540d44ad88f..d04c6bac0c16 100644 --- a/src/main/browser/browser-manager-grab-shortcuts.test.ts +++ b/src/main/browser/browser-manager-grab-shortcuts.test.ts @@ -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) diff --git a/src/main/browser/browser-manager-guest-shortcuts.test.ts b/src/main/browser/browser-manager-guest-shortcuts.test.ts index 022640d4c90d..2a302f470f17 100644 --- a/src/main/browser/browser-manager-guest-shortcuts.test.ts +++ b/src/main/browser/browser-manager-guest-shortcuts.test.ts @@ -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 = [ { diff --git a/src/renderer/src/app-shell/source-control-shortcuts.test.ts b/src/renderer/src/app-shell/source-control-shortcuts.test.ts index 6ca2c6c2ee9a..99e3084b5c0e 100644 --- a/src/renderer/src/app-shell/source-control-shortcuts.test.ts +++ b/src/renderer/src/app-shell/source-control-shortcuts.test.ts @@ -32,6 +32,32 @@ 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 +})) + +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 () => {}) diff --git a/src/renderer/src/app-shell/source-control-shortcuts.ts b/src/renderer/src/app-shell/source-control-shortcuts.ts index 3e836623a804..a55862029454 100644 --- a/src/renderer/src/app-shell/source-control-shortcuts.ts +++ b/src/renderer/src/app-shell/source-control-shortcuts.ts @@ -9,6 +9,7 @@ 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 { useAppStore } from '../store' import type { KeybindingActionId } from '../../../shared/keybindings/types' @@ -20,6 +21,7 @@ type SourceControlShortcutId = | 'sourceControl.focusReviewSummary' | 'sourceControl.openPullRequest' | 'sourceControl.copyPullRequestUrl' + | 'workspace.openFrontend' function focusReviewSummaryInOpenPanel(): boolean { const { activeWorktreeId, rightSidebarOpen, rightSidebarTab } = useAppStore.getState() @@ -107,6 +109,12 @@ export function takeSourceControlShortcut( if (matchShortcut('sourceControl.focusReviewSummary') && focusReviewSummaryInOpenPanel()) { return 'sourceControl.focusReviewSummary' } + 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' diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 29e8976e5594..413a5c35503a 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -21021,6 +21021,9 @@ "openDefault": "Open in Default App" }, "workspace": { + "frontend": { + "notRunning": "No frontend dev server is running." + }, "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.", diff --git a/src/renderer/src/lib/open-workspace-frontend.ts b/src/renderer/src/lib/open-workspace-frontend.ts new file mode 100644 index 000000000000..3d7167c3b625 --- /dev/null +++ b/src/renderer/src/lib/open-workspace-frontend.ts @@ -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 +} diff --git a/src/renderer/src/lib/workspace-port-labels.ts b/src/renderer/src/lib/workspace-port-labels.ts index 216ad279d6ee..f7f314374a98 100644 --- a/src/renderer/src/lib/workspace-port-labels.ts +++ b/src/renderer/src/lib/workspace-port-labels.ts @@ -46,6 +46,11 @@ export function pickFrontendPortLabel( const cache = new Map() +/** 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, diff --git a/src/shared/keybindings/definitions-core-2.ts b/src/shared/keybindings/definitions-core-2.ts index 6d5dee530597..ef6c4def7ebc 100644 --- a/src/shared/keybindings/definitions-core-2.ts +++ b/src/shared/keybindings/definitions-core-2.ts @@ -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', diff --git a/src/shared/keybindings/definitions-core-4.ts b/src/shared/keybindings/definitions-core-4.ts index f130c016da82..dc6a0205d97d 100644 --- a/src/shared/keybindings/definitions-core-4.ts +++ b/src/shared/keybindings/definitions-core-4.ts @@ -190,5 +190,14 @@ 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']) } ] diff --git a/src/shared/keybindings/types.ts b/src/shared/keybindings/types.ts index 86e75cb567aa..bb1e60c14ad0 100644 --- a/src/shared/keybindings/types.ts +++ b/src/shared/keybindings/types.ts @@ -111,6 +111,7 @@ export type KeybindingActionId = | 'sourceControl.sendQueuedComments' | 'sourceControl.openPullRequest' | 'sourceControl.copyPullRequestUrl' + | 'workspace.openFrontend' | 'sourceControl.focusReviewSummary' | 'fileExplorer.undo' | 'fileExplorer.redo' From 2a18e01bcd328ce746d14f9603e091a3b768766c Mon Sep 17 00:00:00 2001 From: Frederik Barbre Date: Fri, 23 Oct 2026 14:14:46 +0200 Subject: [PATCH 2/2] feat: Mod+Alt+Enter runs the workspace's next-action button --- .../source-control-shortcuts.test.ts | 32 +++++++++++++++++++ .../src/app-shell/source-control-shortcuts.ts | 11 +++++++ src/renderer/src/i18n/locales/en.json | 3 ++ src/shared/keybindings/definitions-core-4.ts | 18 +++++++++++ src/shared/keybindings/types.ts | 1 + 5 files changed, 65 insertions(+) diff --git a/src/renderer/src/app-shell/source-control-shortcuts.test.ts b/src/renderer/src/app-shell/source-control-shortcuts.test.ts index 99e3084b5c0e..d940fa1b0742 100644 --- a/src/renderer/src/app-shell/source-control-shortcuts.test.ts +++ b/src/renderer/src/app-shell/source-control-shortcuts.test.ts @@ -37,6 +37,38 @@ 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()) diff --git a/src/renderer/src/app-shell/source-control-shortcuts.ts b/src/renderer/src/app-shell/source-control-shortcuts.ts index a55862029454..8854fd830658 100644 --- a/src/renderer/src/app-shell/source-control-shortcuts.ts +++ b/src/renderer/src/app-shell/source-control-shortcuts.ts @@ -10,6 +10,8 @@ import { } 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' @@ -22,6 +24,7 @@ type SourceControlShortcutId = | 'sourceControl.openPullRequest' | 'sourceControl.copyPullRequestUrl' | 'workspace.openFrontend' + | 'workspace.runNextAction' function focusReviewSummaryInOpenPanel(): boolean { const { activeWorktreeId, rightSidebarOpen, rightSidebarTab } = useAppStore.getState() @@ -109,6 +112,14 @@ 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.')) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 413a5c35503a..b893483fcb20 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -21024,6 +21024,9 @@ "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.", diff --git a/src/shared/keybindings/definitions-core-4.ts b/src/shared/keybindings/definitions-core-4.ts index dc6a0205d97d..177a27c625d5 100644 --- a/src/shared/keybindings/definitions-core-4.ts +++ b/src/shared/keybindings/definitions-core-4.ts @@ -199,5 +199,23 @@ export const KEYBINDING_DEFINITION_CORE_4: readonly KeybindingDefinition[] = [ 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']) } ] diff --git a/src/shared/keybindings/types.ts b/src/shared/keybindings/types.ts index bb1e60c14ad0..b715a6a81f8b 100644 --- a/src/shared/keybindings/types.ts +++ b/src/shared/keybindings/types.ts @@ -112,6 +112,7 @@ export type KeybindingActionId = | 'sourceControl.openPullRequest' | 'sourceControl.copyPullRequestUrl' | 'workspace.openFrontend' + | 'workspace.runNextAction' | 'sourceControl.focusReviewSummary' | 'fileExplorer.undo' | 'fileExplorer.redo'