Files
multica/packages/core/platform/core-provider.tsx
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

150 lines
4.5 KiB
TypeScript

"use client";
import { useEffect, useMemo } from "react";
import { ApiClient } from "../api/client";
import { installFreezeWatchdog } from "../diagnostics/freeze-watchdog";
import { setApiInstance, setSchemaLogger } from "../api";
import { createAuthStore, registerAuthStore } from "../auth";
import { createChatStore, registerChatStore } from "../chat";
import {
I18nProvider,
LocaleAdapterProvider,
UserLocaleSync,
} from "../i18n/react";
import { WSProvider } from "../realtime";
import { QueryProvider } from "../provider";
import { createLogger } from "../logger";
import { defaultStorage } from "./storage";
import { AuthInitializer } from "./auth-initializer";
import type { CoreProviderProps, ClientIdentity } from "./types";
import type { StorageAdapter } from "../types/storage";
import {
configureShortcutPlatform,
configureShortcutRuntime,
} from "../shortcuts/platform";
// Module-level singletons — created once at first render, never recreated.
// Vite HMR preserves module-level state, so these survive hot reloads.
let initialized = false;
let authStore: ReturnType<typeof createAuthStore>;
let chatStore: ReturnType<typeof createChatStore>;
function initCore(
apiBaseUrl: string,
storage: StorageAdapter,
onLogin?: () => void,
onLogout?: () => void,
cookieAuth?: boolean,
identity?: ClientIdentity,
) {
if (initialized) return;
configureShortcutPlatform(
identity?.os === "macos" ||
identity?.os === "windows" ||
identity?.os === "linux" ||
identity?.os === "unknown"
? identity.os
: null,
);
// Authoritative override; before this runs (module-eval store hydration)
// detectShortcutRuntime() reads the preload globals and already agrees.
configureShortcutRuntime(
identity?.platform === "desktop" ? "desktop" : null,
);
const api = new ApiClient(apiBaseUrl, {
logger: createLogger("api"),
onUnauthorized: () => {
storage.removeItem("multica_token");
},
identity,
});
setApiInstance(api);
setSchemaLogger(createLogger("api-schema"));
// In token mode, hydrate token from storage.
if (!cookieAuth) {
const token = storage.getItem("multica_token");
if (token) api.setToken(token);
}
// Workspace identity is URL-driven: the [workspaceSlug] layout resolves
// the slug and calls setCurrentWorkspace(slug, wsId) on mount. The api
// client reads the slug from that singleton for the X-Workspace-Slug
// header. No boot-time hydration from storage is required.
authStore = createAuthStore({ api, storage, onLogin, onLogout, cookieAuth });
registerAuthStore(authStore);
chatStore = createChatStore({ storage });
registerChatStore(chatStore);
initialized = true;
}
export function CoreProvider({
children,
apiBaseUrl = "",
wsUrl = "ws://localhost:8080/ws",
storage = defaultStorage,
cookieAuth,
onLogin,
onLogout,
identity,
locale,
resources,
localeAdapter,
}: CoreProviderProps) {
// Initialize singletons on first render only. Dependencies are read-once:
// apiBaseUrl, storage, and callbacks are set at app boot and never change at runtime.
// eslint-disable-next-line react-hooks/exhaustive-deps
useMemo(() => initCore(apiBaseUrl, storage, onLogin, onLogout, cookieAuth, identity), []);
// Client-only freeze watchdog — shared by web and desktop. No-op on the
// server and idempotent, so mounting it here covers both apps in one place.
useEffect(() => {
installFreezeWatchdog();
}, []);
// I18nProvider wraps everything else: server and client must use the same
// (locale, resources) to avoid hydration mismatch. Language switching goes
// through window.location.reload(), never client-side changeLanguage.
const tree = (
<QueryProvider>
<AuthInitializer
onLogin={onLogin}
onLogout={onLogout}
storage={storage}
cookieAuth={cookieAuth}
identity={identity}
>
<WSProvider
wsUrl={wsUrl}
authStore={authStore}
storage={storage}
cookieAuth={cookieAuth}
identity={identity}
>
{children}
</WSProvider>
</AuthInitializer>
</QueryProvider>
);
// UserLocaleSync requires a LocaleAdapter to persist; only mount it when
// the host app provides one (web layout + desktop App both do).
const withAdapter = localeAdapter ? (
<LocaleAdapterProvider adapter={localeAdapter}>
<UserLocaleSync />
{tree}
</LocaleAdapterProvider>
) : (
tree
);
return (
<I18nProvider locale={locale} resources={resources}>
{withAdapter}
</I18nProvider>
);
}