mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-29 14:37:44 +02:00
* 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>
147 lines
4.4 KiB
TypeScript
147 lines
4.4 KiB
TypeScript
import { afterEach, describe, expect, it } from "vitest";
|
|
import { createShortcutChord } from "./definitions";
|
|
import { configureShortcutRuntime } from "./platform";
|
|
import {
|
|
findShortcutConflict,
|
|
getShortcut,
|
|
migrateShortcutState,
|
|
sanitizeShortcutOverrides,
|
|
useShortcutStore,
|
|
} from "./store";
|
|
|
|
afterEach(() => {
|
|
useShortcutStore.getState().resetAll();
|
|
configureShortcutRuntime(null);
|
|
});
|
|
|
|
describe("shortcut store", () => {
|
|
it("uses defaults, persists overrides, and can disable an action", () => {
|
|
expect(getShortcut("openSearch")).toEqual(
|
|
createShortcutChord("K", { primary: true }),
|
|
);
|
|
expect(getShortcut("send")).toEqual(
|
|
createShortcutChord("Enter", { primary: true }),
|
|
);
|
|
|
|
const custom = createShortcutChord("J", { primary: true });
|
|
useShortcutStore.getState().setShortcut("openSearch", custom);
|
|
expect(getShortcut("openSearch")).toEqual(custom);
|
|
|
|
useShortcutStore.getState().setShortcut("openSearch", null);
|
|
expect(getShortcut("openSearch")).toBeNull();
|
|
});
|
|
|
|
it("removes structurally equal overrides when restoring a default", () => {
|
|
useShortcutStore.getState().setShortcut(
|
|
"createIssue",
|
|
createShortcutChord("I", { primary: true }),
|
|
);
|
|
useShortcutStore.getState().setShortcut(
|
|
"createIssue",
|
|
createShortcutChord("C"),
|
|
);
|
|
|
|
expect(useShortcutStore.getState().overrides.createIssue).toBeUndefined();
|
|
});
|
|
|
|
it("rejects unsafe programmatic overrides as a store invariant", () => {
|
|
useShortcutStore.getState().setShortcut(
|
|
"send",
|
|
createShortcutChord("C"),
|
|
);
|
|
useShortcutStore.getState().setShortcut(
|
|
"send",
|
|
createShortcutChord("Enter", { shift: true }),
|
|
);
|
|
useShortcutStore.getState().setShortcut(
|
|
"send",
|
|
createShortcutChord("Enter", { primary: true, shift: true }),
|
|
);
|
|
expect(getShortcut("send")).toEqual(
|
|
createShortcutChord("Enter", { primary: true }),
|
|
);
|
|
});
|
|
|
|
it("drops persisted Send overrides outside Enter and Mod+Enter", () => {
|
|
expect(
|
|
sanitizeShortcutOverrides({
|
|
send: createShortcutChord("Enter", { shift: true }),
|
|
}),
|
|
).toEqual({});
|
|
});
|
|
|
|
it("keeps browser-only bindings when the runtime is desktop", () => {
|
|
const cmdP = createShortcutChord("P", { primary: true });
|
|
|
|
configureShortcutRuntime("desktop");
|
|
expect(sanitizeShortcutOverrides({ openSearch: cmdP })).toEqual({
|
|
openSearch: cmdP,
|
|
});
|
|
useShortcutStore.getState().setShortcut("openSearch", cmdP);
|
|
expect(getShortcut("openSearch")).toEqual(cmdP);
|
|
|
|
// The same persisted value hydrating in a browser falls back to default.
|
|
configureShortcutRuntime("web");
|
|
expect(sanitizeShortcutOverrides({ openSearch: cmdP })).toEqual({});
|
|
});
|
|
|
|
it("finds conflicts against defaults and overrides", () => {
|
|
expect(
|
|
findShortcutConflict(
|
|
"createIssue",
|
|
createShortcutChord("K", { primary: true }),
|
|
),
|
|
).toBe("openSearch");
|
|
|
|
const custom = createShortcutChord("1", { primary: true });
|
|
useShortcutStore.getState().setShortcut("goInbox", custom);
|
|
expect(findShortcutConflict("goChat", custom)).toBe("goInbox");
|
|
});
|
|
|
|
it("migrates v1 string overrides without losing disabled actions", () => {
|
|
expect(
|
|
migrateShortcutState({
|
|
overrides: { openSearch: "Mod+Shift+J", createIssue: null },
|
|
}, 1),
|
|
).toEqual({
|
|
overrides: {
|
|
openSearch: createShortcutChord("J", { primary: true, shift: true }),
|
|
createIssue: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
it("drops malformed current-version storage and unknown actions", () => {
|
|
expect(
|
|
sanitizeShortcutOverrides({
|
|
openSearch: { key: "J", modifiers: { primary: true } },
|
|
findInIssue: createShortcutChord("J"),
|
|
send: createShortcutChord("Enter"),
|
|
goInbox: null,
|
|
goChat: createShortcutChord("Meta"),
|
|
unknownAction: createShortcutChord("X"),
|
|
}),
|
|
).toEqual({
|
|
send: createShortcutChord("Enter"),
|
|
goInbox: null,
|
|
});
|
|
|
|
expect(
|
|
migrateShortcutState({ overrides: "corrupt" }, 2),
|
|
).toEqual({ overrides: {} });
|
|
});
|
|
|
|
it("falls back to defaults instead of disabling when legacy data is invalid", () => {
|
|
expect(
|
|
migrateShortcutState({
|
|
overrides: {
|
|
openSearch: "",
|
|
createIssue: 42,
|
|
goInbox: null,
|
|
unknownAction: "Mod+X",
|
|
},
|
|
}, 1),
|
|
).toEqual({ overrides: { goInbox: null } });
|
|
});
|
|
});
|