/** Keyboard actions for the composer `@` mention picker. */ export type MentionPickerKeyAction = | { type: "complete"; index: number } | { type: "move"; index: number } | { type: "dismiss" } | { type: "send" } | { type: "none" }; /** Wrap highlight within `[0, count)`. Returns `0` when the list is empty. */ export function wrapMentionHighlightIndex(index: number, count: number): number { if (count <= 0) return 0; return ((index % count) + count) % count; } /** Keep a stored highlight valid after the option list shrinks or grows. */ export function clampMentionHighlightIndex(index: number, count: number): number { if (count <= 0) return 0; if (index < 0) return 0; if (index >= count) return count - 1; return index; } /** * Resolve composer key handling while (or after) the mention picker is open. * When `optionCount` is 0 the picker is closed or empty: Enter sends (unless Shift). * While IME composition is active, returns `none` so Enter confirms text instead. */ export function resolveMentionPickerKey(input: { key: string; shiftKey?: boolean; isComposing?: boolean; optionCount: number; highlightedIndex: number; }): MentionPickerKeyAction { if (input.isComposing) return { type: "none" }; const { key, shiftKey = false, optionCount } = input; const highlightedIndex = clampMentionHighlightIndex(input.highlightedIndex, optionCount); if (optionCount > 0) { if (key === "ArrowDown") { return { type: "move", index: wrapMentionHighlightIndex(highlightedIndex + 1, optionCount) }; } if (key === "ArrowUp") { return { type: "move", index: wrapMentionHighlightIndex(highlightedIndex - 1, optionCount) }; } if (key === "Enter" && !shiftKey) { return { type: "complete", index: highlightedIndex }; } if (key === "Tab") { return { type: "complete", index: highlightedIndex }; } if (key === "Escape") { return { type: "dismiss" }; } return { type: "none" }; } if (key === "Enter" && !shiftKey) { return { type: "send" }; } return { type: "none" }; }