feat: CMD+enter from command menu to open in split view (#13073)

* CMD+enter to open in split view

* Fall back to normal navigation when location state is present

Location state cannot be represented in the split query parameter, so
opening in the secondary pane would silently drop it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Tom Moor
2026-07-20 22:28:10 -04:00
committed by GitHub
co-authored by Claude Fable 5
parent ebc4e23dad
commit b67442a475
4 changed files with 116 additions and 8 deletions
+12 -1
View File
@@ -15,6 +15,7 @@ import type {
} from "~/types";
import Analytics from "~/utils/Analytics";
import history from "~/utils/history";
import { pushOrOpenInSplit } from "~/utils/splitView";
import type { Action as KbarAction } from "kbar";
export function resolve<T>(value: unknown, context: ActionContext): T {
@@ -286,7 +287,17 @@ export async function performAction(
action.variant === "action"
? () => action.perform(context)
: action.variant === "internal_link"
? () => history.push(resolve<LocationDescriptor>(action.to, context))
? () => {
const to = resolve<LocationDescriptor>(action.to, context);
// Holding the modifier while triggering a command bar action
// opens the route in the secondary pane of the split view.
if (context.isCommandBar) {
pushOrOpenInSplit(history, to);
} else {
history.push(to);
}
}
: () => window.open(action.url, action.target);
const result = perform();
@@ -11,11 +11,14 @@ import type { match } from "react-router";
import { __RouterContext as RouterContext, matchPath } from "react-router";
import { Link } from "react-router-dom";
import scrollIntoView from "scroll-into-view-if-needed";
import { isModKey } from "@shared/utils/keyboard";
import { useFocusedSplitLocation } from "~/hooks/useFocusedSplitLocation";
import Desktop from "~/utils/Desktop";
import history from "~/utils/history";
import { isSplitablePath, openRouteInSplit } from "~/utils/splitView";
import {
isSplitablePath,
isSplitViewModifierEvent,
openRouteInSplit,
} from "~/utils/splitView";
const resolveToLocation = (
to: LocationDescriptor | ((location: Location) => LocationDescriptor),
@@ -212,9 +215,7 @@ const NavLink = observer(function NavLink({
// standing in for the browser's open-in-new-tab behavior.
if (
Desktop.isElectron() &&
isModKey(event.nativeEvent) &&
!event.shiftKey &&
!event.altKey &&
isSplitViewModifierEvent(event.nativeEvent) &&
toLocation.pathname &&
isSplitablePath(toLocation.pathname)
) {
+6
View File
@@ -27,12 +27,18 @@ import Logger from "./utils/Logger";
import { PluginManager } from "./utils/PluginManager";
import history from "./utils/history";
import { initSentry } from "./utils/sentry";
import { trackSplitViewModifier } from "./utils/splitView";
import { ActionContextProvider } from "./hooks/useActionContext";
// Load plugins as soon as possible
void PluginManager.loadPlugins();
initI18n(env.DEFAULT_LANGUAGE);
// Register ahead of rendering so the capture-phase listeners run before any
// React-mounted listener, such as kbar's Enter handler.
trackSplitViewModifier();
const element = window.document.getElementById("root");
if (env.SENTRY_DSN) {
+92 -2
View File
@@ -1,7 +1,9 @@
import type { History } from "history";
import { parsePath } from "history";
import type { History, LocationDescriptor } from "history";
import { createPath, parsePath } from "history";
import { action, observable } from "mobx";
import queryString from "query-string";
import { isMobile } from "@shared/utils/browser";
import { isModKey } from "@shared/utils/keyboard";
/**
* Name of the query string parameter that holds the route displayed in the
@@ -151,6 +153,94 @@ export function withoutSplitViewNavigation(callback: () => void): void {
}
}
let splitModifierPressed = false;
/**
* Whether an event carries the modifier combination that opens a route in the
* secondary pane of the split view.
*
* @param event the keyboard or mouse event to check.
* @returns true if the split view modifier combination is held.
*/
export function isSplitViewModifierEvent(
event: KeyboardEvent | MouseEvent
): boolean {
return isModKey(event) && !event.shiftKey && !event.altKey;
}
/**
* Starts tracking whether the split view modifier combination is held during
* events dispatched to the window, allowing code without direct access to the
* triggering event, such as command bar actions, to check it with
* isSplitViewModifierPressed. Synthetic clicks, such as the one kbar
* dispatches when Enter is pressed, are ignored so the state of the
* originating keyboard event is preserved.
*
* @returns a function that stops tracking.
*/
export function trackSplitViewModifier(): () => void {
const record = (event: KeyboardEvent | MouseEvent) => {
splitModifierPressed = isSplitViewModifierEvent(event);
};
const handleClick = (event: MouseEvent) => {
// Clicks not produced by a pointer press carry no modifier state.
if (event.detail === 0) {
return;
}
record(event);
};
window.addEventListener("keydown", record, { capture: true });
window.addEventListener("click", handleClick, { capture: true });
return () => {
splitModifierPressed = false;
window.removeEventListener("keydown", record, { capture: true });
window.removeEventListener("click", handleClick, { capture: true });
};
}
/**
* Whether the split view modifier combination was held during the most recent
* tracked event, see trackSplitViewModifier. Intended to be read while that
* event is still being dispatched.
*
* @returns true if the split view modifier is held.
*/
export function isSplitViewModifierPressed(): boolean {
return splitModifierPressed;
}
/**
* Navigates to the given location, opening it in the secondary pane of the
* split view instead when the split view modifier is held and the route can
* render in a pane.
*
* @param history the history instance to navigate with.
* @param to the path or location descriptor to navigate to.
*/
export function pushOrOpenInSplit(
history: History,
to: LocationDescriptor
): void {
if (isSplitViewModifierPressed() && !isMobile()) {
const location = typeof to === "string" ? parsePath(to) : to;
// Location state cannot be represented in the split query parameter, so
// routes that rely on it must navigate normally.
if (
location.state === undefined &&
location.pathname &&
isSplitablePath(location.pathname)
) {
openRouteInSplit(history, createPath(location));
return;
}
}
history.push(to);
}
/**
* Opens the given path in the secondary pane of the split view, keeping the
* current route in the primary pane, and focuses the secondary pane.