Cmd/Ctrl+T now opens the focused pane's launcher, while explicit target shortcuts bypass the menu. Dispatcher ownership is scoped to the app tree so retained workspaces cannot intercept commands.
64 lines
2.8 KiB
TypeScript
64 lines
2.8 KiB
TypeScript
import { useEffect, useRef } from "react";
|
|
|
|
import type {
|
|
KeyboardActionDefinition,
|
|
KeyboardActionId,
|
|
} from "@/keyboard/keyboard-action-dispatcher";
|
|
import { useKeyboardActionDispatcher } from "@/keyboard/keyboard-action-dispatcher-context";
|
|
|
|
interface UseKeyboardActionHandlerInput {
|
|
handlerId: string;
|
|
actions: readonly KeyboardActionId[];
|
|
enabled: boolean;
|
|
priority: number;
|
|
isActive?: () => boolean;
|
|
handle: (action: KeyboardActionDefinition) => boolean;
|
|
}
|
|
|
|
/**
|
|
* Registers a keyboard action handler with the global dispatcher.
|
|
*
|
|
* The dispatcher is driven by a native window keydown listener and calls the
|
|
* most-recently-registered matching handler first (see keyboard-action-dispatcher.ts).
|
|
* Two properties must hold or handlers bound to the same action step on each other:
|
|
*
|
|
* 1. Stable registration order. registerHandler bumps a registration counter, so
|
|
* re-registering on every render reshuffles which handler the dispatcher reaches
|
|
* first. A frequently re-rendering control (e.g. a running agent's mode control)
|
|
* would then jump ahead of another (e.g. the New Workspace draft) and consume the
|
|
* key, silently acting on the wrong surface. So we register once per
|
|
* (handlerId, priority, actions) and never re-register just because handle,
|
|
* isActive, or enabled changed.
|
|
*
|
|
* 2. Fresh callbacks. Because we do not re-register on every render, the registered
|
|
* entry must read the latest props at dispatch time. handle and isActive are read
|
|
* live from a ref, and enabled is folded into isActive so the dispatcher
|
|
* re-evaluates it fresh (the plain enabled field it filters on is captured at
|
|
* registration and would otherwise go stale).
|
|
*/
|
|
export function useKeyboardActionHandler(input: UseKeyboardActionHandlerInput) {
|
|
const keyboardActionDispatcher = useKeyboardActionDispatcher();
|
|
const inputRef = useRef(input);
|
|
inputRef.current = input;
|
|
|
|
// Only these identity-affecting fields trigger a re-register. actions is compared by
|
|
// content, not array identity, so inline literals do not churn the registration.
|
|
const actionsKey = input.actions.join(" ");
|
|
useEffect(() => {
|
|
return keyboardActionDispatcher.registerHandler({
|
|
handlerId: inputRef.current.handlerId,
|
|
actions: inputRef.current.actions,
|
|
// Always-on at the coarse filter; the real enable/active gate is re-checked fresh
|
|
// inside isActive below so it can never be stale in the registry entry.
|
|
enabled: true,
|
|
priority: inputRef.current.priority,
|
|
isActive: () => {
|
|
const current = inputRef.current;
|
|
if (!current.enabled) return false;
|
|
return current.isActive ? current.isActive() : true;
|
|
},
|
|
handle: (action) => inputRef.current.handle(action),
|
|
});
|
|
}, [actionsKey, input.handlerId, input.priority, keyboardActionDispatcher]);
|
|
}
|