Files
multica/packages/core/shortcuts/platform.ts
Jiayuan Zhang b72bd55d16 feat(shortcuts): make browser-reserved accelerators recordable on desktop (#5327)
* feat(shortcuts): make browser-reserved accelerators recordable on desktop

Cmd/Ctrl+P (and L/T/N/D/U) were rejected by the shortcut recorder on every
platform because a browser tab cannot reliably own them. The Electron
renderer receives these as plain keydowns — neither Electron's default menu
nor the desktop shell binds any of them — so the reservation now only
applies to the web runtime.

Adds a ShortcutRuntime dimension (configured by CoreProvider from the
client identity, with a preload-global fallback that is already correct at
module-eval time so store hydration sanitizes with the right runtime).
App-owned accelerators (W/R/Q, editing keys, zoom row) stay reserved
everywhere.

Closes MUL-4457

Co-authored-by: multica-agent <github@multica.ai>

* fix(shortcuts): limit desktop unlock to bare primary browser accelerators

Review follow-up (MUL-4457): the desktop skip matched primary+key with any
extra modifiers, which would also unreserve OS-owned combos such as
Option+Cmd+D (macOS Dock toggle) and Ctrl+Alt+T (Linux terminal). Only the
bare primary chord is now recordable on desktop; every extra-modifier
variant keeps the historical reservation on both runtimes. Adds regression
tests for the OS combos.

Co-authored-by: multica-agent <github@multica.ai>

---------

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-13 16:05:30 +08:00

76 lines
2.7 KiB
TypeScript

export type ShortcutPlatform = "macos" | "windows" | "linux" | "unknown";
/** Where shortcut handling runs: a browser tab or the Electron renderer. */
export type ShortcutRuntime = "web" | "desktop";
let configuredPlatform: ShortcutPlatform | null = null;
let configuredRuntime: ShortcutRuntime | null = null;
function normalizePlatform(value: string): ShortcutPlatform {
const platform = value.toLowerCase();
if (platform.includes("mac") || platform.includes("darwin")) return "macos";
if (platform.includes("win")) return "windows";
if (platform.includes("linux") || platform.includes("x11")) return "linux";
return "unknown";
}
/** Browser fallback. Desktop injects its authoritative OS through CoreProvider. */
export function detectShortcutPlatform(): ShortcutPlatform {
if (typeof navigator === "undefined") return "unknown";
const nav = navigator as Navigator & {
userAgentData?: { platform?: string };
};
// Some privacy-hardened browsers expose `platform` as an empty string.
// Nullish coalescing would stop there and discard a still-useful userAgent,
// causing Command to be interpreted as Windows/Super on a Mac.
const signals = [nav.userAgentData?.platform, nav.platform, nav.userAgent];
for (const signal of signals) {
if (!signal?.trim()) continue;
const platform = normalizePlatform(signal);
if (platform !== "unknown") return platform;
}
return "unknown";
}
export function configureShortcutPlatform(
platform: ShortcutPlatform | null | undefined,
): void {
configuredPlatform = platform ?? null;
}
export function getShortcutPlatform(): ShortcutPlatform {
return configuredPlatform ?? detectShortcutPlatform();
}
/**
* Environment fallback, and unlike the OS it must be trustworthy at
* module-evaluation time: the shortcut store hydrates (and sanitizes
* persisted overrides) when its module first loads, before CoreProvider
* gets a chance to configure anything. That is safe because the desktop
* preload exposes its bridge globals before any renderer script runs.
* Same signals as `detectClientType` in ../analytics — not imported so
* this module stays dependency-free.
*/
export function detectShortcutRuntime(): ShortcutRuntime {
if (typeof window === "undefined") return "web";
const w = window as unknown as { electron?: unknown; desktopAPI?: unknown };
if (w.electron || w.desktopAPI) return "desktop";
if (
typeof navigator !== "undefined" &&
/Electron/i.test(navigator.userAgent)
) {
return "desktop";
}
return "web";
}
export function configureShortcutRuntime(
runtime: ShortcutRuntime | null | undefined,
): void {
configuredRuntime = runtime ?? null;
}
export function getShortcutRuntime(): ShortcutRuntime {
return configuredRuntime ?? detectShortcutRuntime();
}