moeru-ai/airi · error · Error
Invalid accelerator " ": no key token
Error message
Invalid accelerator "${input}": no key token What it means
Thrown by parseAccelerator when the input contains only modifiers (or nothing but modifier aliases) and no main key token. After the token loop, `key` is still undefined, so strings like 'Ctrl', 'Ctrl+Shift', or '' after trimming of key parts cannot produce a ShortcutAccelerator. Every accelerator must terminate in exactly one non-modifier key.
Solutions
- In capture UI, only commit a binding once a non-modifier key is pressed; ignore modifier-only states
- When serializing, always include the key: `${acc.modifiers.join('+')}+${acc.key}`
- Validate stored settings at load time with isValidAccelerator and fall back to defaults for invalid entries
- Treat an accelerator object with a falsy key as corrupt data and re-prompt the user
Example fix
// before
const acc = parseAccelerator(capturedBinding) // capturedBinding === 'Ctrl+Shift'
// after
// only commit when a main key exists
if (!captured.key) {
// keep listening; do not parse a modifier-only string
} else {
const acc = parseAccelerator(`${[...captured.modifiers, captured.key].join('+')}`)
} Defensive patterns
Strategy: validation
Validate before calling
import { isValidAccelerator } from '../global-shortcut/accelerators'
if (!isValidAccelerator(stored)) {
settings.binding = DEFAULT_BINDING // quarantine corrupt entry
} Type guard
function hasKeyToken(s: string): boolean {
// last '+'-separated segment must be a non-modifier; simplest: reuse the library predicate
return isValidAccelerator(s)
} Try / catch
catch (e) { if (e instanceof Error && e.message.includes('no key token')) return DEFAULT_BINDING; throw e } Prevention
- In capture UI, commit a binding only after a non-modifier key press
- Always serialize as `${modifiers.join('+')}+${key}`
- Validate stored settings at startup and reset invalid bindings to defaults
When it happens
Trigger: Calling parseAccelerator('Ctrl+Shift'), parseAccelerator('Alt'), or passing a stored binding whose key field was lost (e.g. JSON migration dropped the `key` property and it serialized as 'Ctrl+undefined' trimmed to modifiers only).
Common situations: Key-capture UI where the user releases keys before pressing a main key and only modifier keydowns were recorded; serializing an accelerator object back to a string but omitting acc.key; partially migrated settings files from an older keybinding format.
Related errors
- Invalid accelerator " ": duplicate modifier
- Invalid accelerator " ": empty token
- Invalid accelerator " ": multiple non-modifier keys (" ", "…
- Invalid accelerator: empty string
- Invalid accelerator: unknown key
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/3c3c47ea4413f23e.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-shared/src/global-shortcut/accelerators.ts:340
const token = raw.trim()
if (token.length === 0)
throw new Error(`Invalid accelerator "${input}": empty token`)
const modifier = lookupModifierToken(token)
if (modifier !== undefined) {
if (modifiers.includes(modifier))
throw new Error(`Invalid accelerator "${input}": duplicate modifier "${modifier}"`)
modifiers.push(modifier)
continue
}
if (key !== undefined)
throw new Error(`Invalid accelerator "${input}": multiple non-modifier keys ("${key}", "${token}")`)
key = normalizeKeyToken(token)
}
if (key === undefined)
throw new Error(`Invalid accelerator "${input}": no key token`)
return { modifiers, key }
}
/**
* Tests whether `input` is a well-formed accelerator string.
*
* Use when:
* - Gating user input without needing the parsed result
*
* Returns:
* - `true` when `parseAccelerator` would succeed, `false` otherwise
*/
export function isValidAccelerator(input: string): boolean {
try {
parseAccelerator(input)
return true
}View on GitHub (pinned to 677329427f)